From 3a188b35fb98af2bd426539e021e8772976acaca Mon Sep 17 00:00:00 2001 From: arti Date: Wed, 11 Nov 2026 17:05:33 +0000 Subject: [PATCH] docs: M2 TI ABI, ROM strlen and golden ABI check --- README.md | 314 +++++++++++++++++++----------- scripts/tests/m2/diff/check.sh | 129 ++++++++++++ scripts/tests/m2/diff/ti-add2.c | 1 + scripts/tests/m2/diff/ti-add2.s | 22 +++ scripts/tests/m2/diff/ti-locals.c | 1 + scripts/tests/m2/diff/ti-locals.s | 22 +++ symbols/README.md | 13 +- 7 files changed, 386 insertions(+), 116 deletions(-) create mode 100755 scripts/tests/m2/diff/check.sh create mode 100644 scripts/tests/m2/diff/ti-add2.c create mode 100644 scripts/tests/m2/diff/ti-add2.s create mode 100644 scripts/tests/m2/diff/ti-locals.c create mode 100644 scripts/tests/m2/diff/ti-locals.s diff --git a/README.md b/README.md index 97d3ce1..f3c0496 100644 --- a/README.md +++ b/README.md @@ -3,83 +3,208 @@ A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of upstream LLVM. -**Status: M1.** `llc -march=tms34010` emits GSPA-compatible assembly for a -small set of leaf functions, and that assembly assembles, links and runs in -MAME. M1 is an **assembly-only, test-only** snapshot: it uses an A0–A3/A0 -register convention, not the TI ABI, and has no object writer, calls or runtime. +**Status: M2.** `llc -march=tms34010` emits GSPA-compatible assembly using the +TI ABI: integer/pointer arguments are pushed on the program stack, the result is +returned in `A8`, frames use `A13`/`A14`/`A15`, and functions end in `RETS 2` +(`RETS 0` without arguments). M2 can call a ROM entry point (`strlen`) and emit +initialized `.data` and string literals. It is still **assembly-only**: no +object writer, no `clang -c`, and no native COFF; GSPA/GSPLNK under DOSBox +produce the COFF. Control flow, floats, structs, varargs and function pointers +remain M3+. - Design: [`docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md`](docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md) - M0 plan: [`docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md`](docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md) - M1 plan: [`docs/superpowers/plans/2026-11-11-tms34010-m1-minimal-backend.md`](docs/superpowers/plans/2026-11-11-tms34010-m1-minimal-backend.md) +- M2 plan: [`docs/superpowers/plans/2026-11-11-tms34010-m2-ti-abi-and-rom-calls.md`](docs/superpowers/plans/2026-11-11-tms34010-m2-ti-abi-and-rom-calls.md) - Spike: [`spike/REPORT-23.md`](spike/REPORT-23.md) (current, LLVM 23.1.5); [`spike/REPORT.md`](spike/REPORT.md) (historical, 19.1.7) ## Repositories - **Compiler fork:** - (branch `tms34010`) — the full buildable LLVM 23.1.5 tree with the TMS34010 - target, clang bit-address changes and the M1 backend. + (branch `m2`) — the full buildable LLVM 23.1.5 tree with the TMS34010 + target, clang bit-address changes and the M2 backend. Branch `m1` keeps the + original M1 backend and suite; branch `tms34010` is the M1 tip. - **This project:** - — plans/specs, VT1200 integration tooling, `tms-bitaddr`, `tms-clang`, - symbols, the M1 harness and the spike. + (branch `m2`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`, + `tms-clang`, symbols, the M2 harness and the spike. Branch `m1` is the + reproducible M1 snapshot, including the retired M1 harness. The compiler fork is the canonical source for all target code. `patches/llvm` -here is a **frozen M0 mirror** used by the default upstream+patches build; M1 +here is a **frozen M0 mirror** used by the default upstream+patches build; M2 does not change it. -## What M1 provides +## What M2 provides -M1 fills in the M0 target skeleton and stops at assembly: +M2 replaces the M1 test convention with the TI ABI and stops at assembly: -- `llc -march=tms34010 -filetype=asm` for leaf 32-bit functions: - integer/pointer arguments in `A0`–`A3`, result in `A0`, return via `RETS 0`. -- `ADD` for `i32`, `MOVI` for constants, `MOVE ..., 1` (FS1) for 32-bit - load/store, `MOVE ..., 0` (FS0) for `i16`, and sign-extending `MOVB` for - `i8` load/store. -- Constant global addresses as `MOVI _sym+bits`; a surviving GEP offset is a - bit offset (`ADDI 24`, not `ADDI 192`). -- Hard errors, not silent fallbacks, for calls (`M1 has no calls; M2 adds the - TI ABI`), stack frames (`M1 has no stack frame`), more than four register - arguments, unsupported signatures (varargs, `i64`/float/aggregate - parameters, `sret`/`byval`/`inreg`/`nest`/`returned`), unsupported return - types, and initialized globals or string literals. -- Seventeen `llvm-lit`/FileCheck tests under - `llvm/test/CodeGen/TMS34010/m1-*.ll` in the compiler fork, including - negative tests that pin the loud refusals. -- An end-to-end path: `scripts/tms-clang` IR → `llc -O0 -filetype=asm` → - **1987 GSPA/GSPLNK 2.00 under DOSBox** → COFF → the VT1200 loader in MAME. - The run checks `add(20, 22) == 42`, `load(&word) == 0x12345678`, - `second() == &arr[1]` (a `symbol+32` bit relocation), `loadb` of a byte - `0x81` returns the sign-extended `0xFFFFFF81`, and `storeb` leaves `0x81` - in memory. -- A `dis34` round-trip that reads the linked `.text` from the COFF and checks - the instructions GSPA encoded at their linked bit addresses. +- `llc -march=tms34010 -filetype=asm` for straight-line `i32`/pointer + functions with calls, frames and locals. +- A caller saves `A14` (STK) on the system stack, pushes arguments on the + program stack **in reverse order** (each widened to 32 bits), emits `CALLA`, + then re-establishes `FS1`. +- A callee saves the used A0–A7/A9–A12 registers and the old FP on the system + stack, sets `FP = STK`, allocates the frame with `ADDI`, and restores both + stacks in the epilogue. The first explicit argument is at `FP(-32)`, the + second at `FP(-64)`, and so on; locals live at positive FP offsets. +- `void`, `i32` and pointer returns, in `A8`; `RETS 2` when the function has + explicit arguments, `RETS 0` otherwise. +- Initialized globals, string/byte-array literals and pointer initializers in + `.data`; zero-initialized mutable globals stay in `.bss`. The loader copies + `.data` along with `.text`. +- ROM import stubs generated from `symbols/vt1200-v22.syms`. M2 consumes + `strlen` only: `_strlen: jauc 3FB458D0h`. `CALLA _strlen` pushes the return + PC, the stub jumps to the ROM, and the ROM's `RETS 2` returns directly to + our caller. +- Thirty-three `llvm-lit`/FileCheck tests under + `llvm/test/CodeGen/TMS34010/m2-*.ll`, including negative tests that pin the + loud refusals. +- End-to-end MAME and dis34 gates, plus a differential check against assembly + generated once by the 1987 TI compiler (see below). -## M1 is not the TI ABI +## The TI ABI -| | M1 test convention | TI ABI (M2) | +| | M1 test convention (branch `m1`) | TI ABI (M2) | |---|---|---| -| Integer/pointer arguments | `A0`–`A3`, four maximum | program stack (via `A14` STK) | +| Integer/pointer arguments | `A0`–`A3`, four maximum | program stack via `A14` (STK), pushed in reverse | +| Argument slots | registers | first at `FP(-32)`, second at `FP(-64)`, … | | Return value | `A0` | `A8` | | Frame | none; leaf functions only | `A13` FP, `A14` STK, `A15` system stack | -| Return instruction | `RETS 0` | `RETS 2` | +| Locals | none | positive FP offsets | +| Return instruction | `RETS 0` | `RETS 2` with arguments, `RETS 0` without | | Calls, callee-saves, stack args | rejected | required | -| Field select | harness sets FS1/FS0 before the leaf | `FS1 = 32` preserved across calls | +| Field size | harness sets FS1/FS0 before entry | `FS1 = 32` (32-bit words), set in the prologue and after every call | -M1 assembly is not ABI-compatible with TI's compiler or the ROM Xlib. M2 -replaces the convention; do not build on the M1 register assignment. +The callee-saved set is `A0`–`A7` and `A9`–`A12`; `A8` is the return register +and `A13`–`A15` are reserved. After `n` system-stack pushes the saved old STK +is at `SP + (n+1)*32`, and `RETS N` pops the return PC and advances `SP` by +`32 + 16N`. `FS1` must be 32 at function entry and immediately after every +`CALLA` (`SETF 32,0,1`), because ROM code may change the field-size state. + +## Run the M2 gates + +These are the documented commands; all must pass. The M1 harness and suite are +on branch `m1` and are **not** M2 gates: they encode the retired M1 register +convention. + +```sh +# 1. Build llc, opt, clang and the test tools +ninja -C build/llvm-clean-build llc opt clang llvm-config FileCheck count not + +# 2. LLVM codegen tests: 33 passed +build/llvm-clean-build/bin/llvm-lit -v \ + build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m2-' + +# 3. Compile, assemble, link and run in MAME +scripts/tests/m2/run.sh +# -> M2 MAME: PASS +# (check=4 first=20 second=22 caller=42 locals=7 keep=16) + +# 4. Rebuild the COFF and check the GSPA encodings with dis34 +scripts/tests/m2/dis-roundtrip.sh +# -> M2 dis34 round-trip: PASS + +# 5. Differential TI-compiler ABI check (no DOSBox) +scripts/tests/m2/diff/check.sh +# -> M2 diff: PASS + +# 6. GEP scaling/normalization tool tests +tools/tms-bitaddr/tests/run.sh +# -> tms-bitaddr: PASS +``` + +`scripts/tests/m2/run.sh` compiles five C inputs with `scripts/tms-clang -O2`, +runs `llc -march=tms34010 -O0 -filetype=asm` on the normalized IR, generates +the per-ROM import stubs, concatenates the result with `scripts/m2/entry.asm`, +assembles/links with GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF in +MAME. It checks `strlen("0::0") == 4`, two-argument order, a direct call, a +local frame, and callee-saved preservation. Paths are overridable with +`TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`, `MAME`, `DOSBOX`, `SDK_IMG`, +`M2_WORK`, `M2_COFF` and `M2_SDK`. + +`dis-roundtrip.sh` runs the same build and disassembles the linked `.text` with +the VT1200 tree's `dis34`, checking the TI-ABI instructions at their linked bit +addresses, the old-STK restore offset against the push count, and the copied +`.data` bytes. + +`scripts/tests/m2/diff/check.sh` is the CI golden check. It compares the ABI +shape of our assembly with stored output from the 1987 TI compiler for the same +K&R sources: first argument at `FP(-32)`, second at `FP(-64)`, result in `A8`, +`RETS 2` for `add2`, a nonzero frame and `RETS 0` for `locals`. It does not +compare instruction selection byte-for-byte. The stored `ti-add2.s` and +`ti-locals.s` were generated once with `gspcpp`/`gspcc`/`gspcg` (GSP C Compiler +2.01) under DOSBox; CI never runs DOSBox, it greps the stored files. + +## `.data`, strings and the loader + +GSPA 2.00 has no ELF sections. M2 emits raw `.data` text, never `.rodata`, +`.asciz`, `.p2align` or `.section`: + +- `i8` scalar or byte/string array: `.byte` with GSPA hex (`068h`) +- `i16` scalar: `.word` +- `i32` scalar: `.long` +- pointer: `.long _symbol+` (the addend is already in bits) +- `.even` before a `.data` global whose alignment is at least 16 bits, so the + label and the following `.word`/`.long` agree. + +A scalar's directive must match its width, otherwise the next global's offset +is wrong. Private/local symbol names are sanitized to GSPA-safe names +(`_HH` for non-alphanumerics). Unsupported initializer shapes fail with +`M2 .data supports only integer, pointer and byte-array initializers`. +Zero-initialized mutable globals remain `.bss` and are zeroed by the loader. + +## ROM imports + +ROM entry points are per-ROM-revision bit addresses, kept out of the compiler +in `symbols/vt1200-v22.syms`. `scripts/m2/gen-rom-imports.sh` turns each entry +into an absolute-jump stub: + +```asm + .globl _strlen +_strlen: + jauc 3FB458D0h +``` + +`JAUC` is the TMS34010 unconditional absolute jump; GSPA 2.00 rejects bare +`ja`. M2 uses only `strlen`; later milestones extend and validate the table and +add the Xlib call sequence. Do not add Xlib dependencies to M2. + +## M2 non-goals + +M2 deliberately rejects or omits: + +- **Branches/control flow:** conditional and unconditional branches, loops, + and spill/fixup paths that need them. A branch that reaches ISel fails + loudly; M3 owns control flow. +- **Floating point:** float/double arguments, returns and arithmetic (M5). +- **Aggregates:** structs/unions by value, `sret`, `byval`, bitfields (M4/M5). +- **Varargs:** `...` functions and calls (M5). +- **Function pointers and indirect calls** (M4). +- **Narrow ABI types:** `i8`/`i16` parameters, `i64`, and narrow returns; M3 + adds the widen/truncate rules. Narrow loads/stores inside a function remain + supported. +- **Object output:** no `MCCodeEmitter`, object writer, `llc -filetype=obj`, + `clang -c` or native COFF. GSPA assembles and GSPLNK links the asm. +- **Xlib and clients:** no X11 headers, Xlib calls or real VT1200 clients in + M2; only the `strlen` ROM call. + +The optimizer's `returned` argument attribute is accepted because it does not +affect the stack-slot ABI; `sret`/`byval`/`inreg`/`nest` remain rejected. ## Repository layout ```text -build/llvm-clean/ compiler fork checkout used for M1 (ignored) +build/llvm-clean/ compiler fork checkout used for M2 (ignored) build/llvm-clean-build/ its Ninja build tree (ignored) build/llvm-src/ M0 working fork (ignored) patches/llvm/ frozen M0 patch series against llvmorg-23.1.5 tools/tms-bitaddr/ GEP scale/normalize tool scripts/tms-clang IR pipeline wrapper (clang + tms-bitaddr + opt) -scripts/m1/ M1 harness: C inputs, entry.asm, link.cmd, checks -scripts/tests/m1/ M1 gates (run.sh, dis-roundtrip.sh) +scripts/m2/ M2 harness: C inputs, entry.asm, link.cmd, stubs, checks +scripts/tests/m2/ M2 gates (run.sh, dis-roundtrip.sh) +scripts/tests/m2/diff/ TI golden asm and the differential ABI check +scripts/m1/ M1 harness (historical; branch m1, not an M2 gate) +scripts/tests/m1/ M1 gates (historical; branch m1, not an M2 gate) scripts/tests/m0/ M0 integration tests scripts/build-llvm.sh fork or upstream+patches build scripts/fetch-vt1200.sh fetch the VT1200 RE tree at its pinned commit @@ -92,10 +217,11 @@ docs/ design and plans ## Prerequisites `cmake`, `ninja-build`, `ccache`, a C++17 host compiler (GCC 14 is used here), -Python 3, and about 20 GB of disk for the LLVM source and build. The M1 gates -additionally need `dosbox`, `mtools` (`mcopy`), the VT1200 SDK image, the -`dwt` COFF loader, and a built MAME `vt1200` binary -(`build/mame-src/vt1200`). On Debian: +Python 3, and about 20 GB of disk for the LLVM source and build. The M2 MAME +and dis34 gates additionally need `dosbox`, `mtools` (`mcopy`), the VT1200 SDK +image, the `dwt` COFF loader, `dis34`, and a built MAME `vt1200` binary +(`build/mame-src/vt1200`). The differential golden check needs only the built +compiler. On Debian: ```sh sudo apt-get install -y cmake ninja-build ccache dosbox mtools @@ -112,13 +238,13 @@ Xlib call sequence, `as34`, `dis34`, SDKs, manuals). It is not vendored: ## Build the compiler -### Compiler fork (M1, the normal path) +### Compiler fork (M2, the normal path) -The fork checkout already exists at `build/llvm-clean` on `tms34010`. Build it -in place: +The fork checkout already exists at `build/llvm-clean` on `m2`. Build it in +place: ```sh -LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=tms34010 ./scripts/build-llvm.sh +LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m2 ./scripts/build-llvm.sh # binaries: build/llvm-clean-build/bin/{llc,clang,opt,llvm-config,FileCheck} # lit driver: build/llvm-clean-build/bin/llvm-lit ``` @@ -127,7 +253,7 @@ To clone the fork first instead of using an existing checkout: ```sh LLVM_REPO=gitea@git.wut.ee:opencode.siil.arti.ee/llvm-tms34010.git \ -LLVM_BRANCH=tms34010 ./scripts/build-llvm.sh +LLVM_BRANCH=m2 ./scripts/build-llvm.sh ``` Fork mode **never** checks out, resets, cleans or patches the source tree; it @@ -144,22 +270,22 @@ TMS34010; a first build takes roughly 20–40 minutes. This mode clones `llvmorg-23.1.5`, applies `patches/llvm/*.patch`, and builds `build/llvm-clean-build`. It owns and resets only a plain `llvm-project` clone: -if `build/llvm-clean` has some other origin (for example the M1 fork), it +if `build/llvm-clean` has some other origin (for example the M2 fork), it refuses rather than destroying it. Use fork mode for that checkout. `scripts/export-patches.sh` regenerates the committed M0 patch series with -`git diff` (including new files via `git add -N`); it is not used for M1. +`git diff` (including new files via `git add -N`); it is not used for M1/M2. ## Build the M0 support tools -The M1 pipeline still uses `tms-bitaddr` and `tms-clang`: +The M2 pipeline uses `tms-bitaddr` and `tms-clang`: ```sh LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \ tools/tms-bitaddr/build.sh ``` -M0 regression test (optional in M1): +M0 regression test (optional in M2): ```sh TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \ @@ -169,71 +295,33 @@ TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \ # -> M0 pipeline: PASS ``` -## Run the M1 gates - -These are the documented commands; all three must pass: - -```sh -# 1. LLVM codegen tests: 17 passed -build/llvm-clean-build/bin/llvm-lit -v \ - build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m1-' - -# 2. Compile, assemble, link and run in MAME -./scripts/tests/m1/run.sh -# -> M1 MAME: PASS - -# 3. Rebuild the COFF and check the GSPA encodings with dis34 -./scripts/tests/m1/dis-roundtrip.sh -# -> M1 dis34 round-trip: PASS -``` - -`run.sh` compiles five C inputs with `scripts/tms-clang -O2`, runs -`llc -march=tms34010 -O0 -filetype=asm` on the normalized IR, concatenates -the result with `scripts/m1/entry.asm` and `scripts/m1/char-test.asm`, -assembles/links with GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF in -MAME. `dis-roundtrip.sh` runs the same build and then disassembles the linked -`.text` at the symbol addresses from the COFF symbol table. Paths are -overridable with `TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`, `MAME`, -`DOSBOX`, `SDK_IMG`, `M1_WORK`, `M1_COFF` and `M1_SDK`. - ## Emit assembly by hand ```sh TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \ TMS_OPT=$PWD/build/llvm-clean-build/bin/opt \ TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \ - scripts/tms-clang -O2 -o /tmp/add.ll scripts/m1/add.c + scripts/tms-clang -O2 -o /tmp/add2.ll scripts/tests/m2/diff/ti-add2.c build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm \ - /tmp/add.ll -o /tmp/add.s + /tmp/add2.ll -o /tmp/add2.s ``` -Always produce the IR with `scripts/tms-clang` (or run `tms-bitaddr --scale`) -before `llc`: raw clang IR carries byte GEP offsets, but `llc` treats every -surviving GEP offset as a *bit* offset, so addresses would be 8× too small. -Run `llc -O0` on the normalized IR that `tms-clang` already optimized and -scaled. `tms-bitaddr --normalize` consumes that scaled IR and now rejects a -typed (non-`i8`) or vector GEP instead of emitting its byte offset as bits. -The lit tests use `llc -march=tms34010 -O2 -filetype=asm` on hand-written IR -to exercise the selector pipeline directly. Output is assembly only: `llc` -cannot emit an object file or a usable COFF for this target. +The resulting function loads its arguments at `FP(-32)` and `FP(-64)`, returns +in `A8`, and ends in `RETS 2`. Always produce the IR with `scripts/tms-clang` +(or run `tms-bitaddr --scale`) before `llc`: raw clang IR carries byte GEP +offsets, but `llc` treats every surviving GEP offset as a *bit* offset, so +addresses would be 8× too small. Run `llc -O0` on the normalized IR that +`tms-clang` already optimized and scaled. `tms-bitaddr --normalize` consumes +that scaled IR and rejects a typed (non-`i8`) or vector GEP instead of emitting +its byte offset as bits. The lit tests use +`llc -march=tms34010 -O2 -filetype=asm` on hand-written IR to exercise the +selector pipeline directly. Output is assembly only: `llc` cannot emit an +object file or a usable COFF for this target. -## Not yet supported (M2 and later) +## Later milestones -- **Data:** only zero-initialized mutable globals (`.bss`) are supported. - Initialized globals, string literals and constant/private data (`.data`, - `.rodata`) make `llc` fail loudly; the `.data` initializer probe moves to - M2. -- **Object emission:** no `MCCodeEmitter`, object writer, `llc -filetype=obj`, - `clang -c`, or native COFF. GSPA assembles and GSPLNK links; the existing - COFF loader loads. -- **TI ABI:** `A8` return, `A13`/`A14`/`A15` dual stack, `RETS 2`, `FS1 = 32` - across calls, callee-saves, stack arguments, more than four arguments. -- **C features:** calls, locals/`alloca`, structures/`byval`, floats, varargs, - bitfields, 64-bit arithmetic, `memcpy`/`alloca` lowering. -- **Runtime and clients:** ROM/Xlib imports, `strlen`/`malloc`, X11 headers, - real VT1200 clients, debug info. -- **2ine:** M1 uses the proven 1987 GSPA/GSPLNK under DOSBox, not the external - 2ine r5.01 tools; 2ine is an M2 speed-up. - -These are M2–M5 in the design spec, each with its own plan. +M3 adds branches/control flow, the Xlib call sequence and a wider validated ROM +symbol table. M4 adds the real client plus structs and function pointers. M5 +adds the native COFF path, floats, varargs, bitfields and `long long`; 2ine +r5.01 is a later speed-up for the GSPA/GSPLNK step. diff --git a/scripts/tests/m2/diff/check.sh b/scripts/tests/m2/diff/check.sh new file mode 100755 index 0000000..67724dd --- /dev/null +++ b/scripts/tests/m2/diff/check.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# M2 differential TI-compiler ABI check. +# +# scripts/tests/m2/diff/check.sh +# +# The stored ti-add2.s/ti-locals.s are one-time outputs of the 1987 TI +# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this +# script greps the stored golden files and compares their ABI shape with the +# assembly our compiler emits for the same K&R sources. +# +# The oracle is the TI ABI layout, not instruction selection: +# +# add2 first argument at FP(-32), second at FP(-64), result in A8, RETS 2 +# locals a nonzero frame, result in A8, RETS 0 +# +# TI prints the negative offsets as -32/-64; 0FFE0h/0FFC0h are also accepted +# because the 1987 assembler may format the same displacement in hex. +# +# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_ADD2 TI_LOCALS +set -eu + +ROOT=$(cd "$(dirname "$0")/../../../.." && pwd) +DIR=$ROOT/scripts/tests/m2/diff + +LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin} +TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang} +TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt} +TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr} +TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc} +TI_ADD2=${TI_ADD2:-$DIR/ti-add2.s} +TI_LOCALS=${TI_LOCALS:-$DIR/ti-locals.s} +export TMS_CLANG TMS_OPT TMS_BITADDR + +fail() { + echo "M2 diff: FAIL: $*" >&2 + exit 1 +} + +for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do + [ -x "$tool" ] || fail "missing tool: $tool" +done +[ -f "$TI_ADD2" ] || fail "stored TI golden missing: $TI_ADD2" +[ -f "$TI_LOCALS" ] || fail "stored TI golden missing: $TI_LOCALS" + +TMP=$(mktemp -d "${TMPDIR:-/tmp}/m2-diff.XXXXXX") +trap 'rm -rf "$TMP"' EXIT HUP INT TERM + +# Our add2 is built exactly like the M2 gate: tms-clang -O2, then llc -O0. +# locals uses -O0 because at -O2 the optimizer folds `x = 7; return x` to a +# constant and no frame is emitted; the TI compiler keeps the frame, so -O0 is +# the level at which the ABI frame shape is comparable. +echo "== compile our add2.s and locals.s" +"$ROOT/scripts/tms-clang" -O2 -o "$TMP/add2.ll" "$DIR/ti-add2.c" +"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/add2.ll" -o "$TMP/add2.s" +"$ROOT/scripts/tms-clang" -O0 -o "$TMP/locals.ll" "$DIR/ti-locals.c" +"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/locals.ll" -o "$TMP/locals.s" +echo " add2.s: $(grep -c . "$TMP/add2.s") lines" +echo " locals.s: $(grep -c . "$TMP/locals.s") lines" + +# Ignore our `;` end-of-line comments so a comment cannot satisfy a check. +sed 's/;.*//' "$TMP/add2.s" > "$TMP/add2.code" +sed 's/;.*//' "$TMP/locals.s" > "$TMP/locals.code" + +require() { + _file=$1 + _pat=$2 + _what=$3 + if grep -Eiq "$_pat" "$_file"; then + echo " ok $_what" + return 0 + fi + echo "M2 diff: FAIL: missing $_what" >&2 + echo " pattern: $_pat" >&2 + sed 's/^/ /' "$_file" >&2 + exit 1 +} + +require_rets() { + require "$1" "^[[:space:]]*RETS[[:space:]]+$2([[:space:]]|;|$)" "$3" +} + +# TI golden: the argument reads are direct FP-relative loads. +require "$TI_ADD2" 'MOVE[[:space:]]+\*FP\((-32|0FFE0h)\)' \ + "TI add2 reads the first argument at FP(-32)" +require "$TI_ADD2" 'MOVE[[:space:]]+\*FP\((-64|0FFC0h)\)' \ + "TI add2 reads the second argument at FP(-64)" +require "$TI_ADD2" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \ + "TI add2 returns in A8" +require_rets "$TI_ADD2" 2 "TI add2 ends with RETS 2" + +require "$TI_LOCALS" '(ADDK|ADDI)[[:space:]]+\+?[1-9][0-9]*[[:space:]]*,[[:space:]]*(STK|A14)' \ + "TI locals allocates a nonzero frame" +require "$TI_LOCALS" 'MOVE[[:space:]]+(STK[[:space:]]*,[[:space:]]*(FP|A13)|A14[[:space:]]*,[[:space:]]*(FP|A13))' \ + "TI locals establishes FP from STK" +require "$TI_LOCALS" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \ + "TI locals returns in A8" +require_rets "$TI_LOCALS" 0 "TI locals ends with RETS 0" + +# Our asm: FP is materialised with MOVE A13,A; the argument read is at an +# ADDI offset from that register. Check the offset and that the register is +# then dereferenced. +check_our_arg() { + _file=$1 + _off=$2 + _desc=$3 + _reg=$(sed -nE "s/.*ADDI[[:space:]]+($_off)[[:space:]]*,[[:space:]]*A([0-9]+).*/\2/p" \ + "$_file" | head -1) + [ -n "$_reg" ] || fail "our add2 has no ADDI $_off frame offset for $_desc" + require "$_file" "MOVE[[:space:]]+A13[[:space:]]*,[[:space:]]*A$_reg" \ + "our add2 materialises FP for $_desc" + require "$_file" "MOVE[[:space:]]+\*A$_reg([[:space:]]|,|$)" \ + "our add2 reads $_desc through the FP register" +} + +check_our_arg "$TMP/add2.code" '-32|0FFE0h' "the first argument at FP(-32)" +check_our_arg "$TMP/add2.code" '-64|0FFC0h' "the second argument at FP(-64)" +require "$TMP/add2.code" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \ + "our add2 returns in A8" +require_rets "$TMP/add2.code" 2 "our add2 ends with RETS 2" + +require "$TMP/locals.code" 'ADDI[[:space:]]+\+?[1-9][0-9]*[[:space:]]*,[[:space:]]*A14' \ + "our locals allocates a nonzero frame" +require "$TMP/locals.code" 'MOVE[[:space:]]+A14[[:space:]]*,[[:space:]]*A13' \ + "our locals establishes FP from STK" +require "$TMP/locals.code" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \ + "our locals returns in A8" +require_rets "$TMP/locals.code" 0 "our locals ends with RETS 0" + +echo "M2 diff: PASS" diff --git a/scripts/tests/m2/diff/ti-add2.c b/scripts/tests/m2/diff/ti-add2.c new file mode 100644 index 0000000..5f1c27b --- /dev/null +++ b/scripts/tests/m2/diff/ti-add2.c @@ -0,0 +1 @@ +int add2(a, b) int a; int b; { return a + b; } diff --git a/scripts/tests/m2/diff/ti-add2.s b/scripts/tests/m2/diff/ti-add2.s new file mode 100644 index 0000000..afb8d47 --- /dev/null +++ b/scripts/tests/m2/diff/ti-add2.s @@ -0,0 +1,22 @@ +******************************************************* +* GSP C COMPILER, Version 2.01, 87.300 * +******************************************************* +FP .set A13 +STK .set A14 + .globl _add2 + .file "ADD2.c" +****************************************************** +* FUNCTION DEF : _add2 +****************************************************** +_add2: + MMTM SP,A5,A7,FP + MOVE STK,FP + MOVE *FP(-32),A7,1 + MOVE *FP(-64),A5,1 + ADD A5,A7 + MOVE A7,A8 +EPI0_1: + MOVE *SP(128),STK,1 + MMFM SP,A5,A7,FP + RETS 2 + .end diff --git a/scripts/tests/m2/diff/ti-locals.c b/scripts/tests/m2/diff/ti-locals.c new file mode 100644 index 0000000..03c7765 --- /dev/null +++ b/scripts/tests/m2/diff/ti-locals.c @@ -0,0 +1 @@ +int locals() { int x; x = 7; return x; } diff --git a/scripts/tests/m2/diff/ti-locals.s b/scripts/tests/m2/diff/ti-locals.s new file mode 100644 index 0000000..f8ef94c --- /dev/null +++ b/scripts/tests/m2/diff/ti-locals.s @@ -0,0 +1,22 @@ +******************************************************* +* GSP C COMPILER, Version 2.01, 87.300 * +******************************************************* +FP .set A13 +STK .set A14 + .globl _locals + .file "LOCALS.c" +****************************************************** +* FUNCTION DEF : _locals +****************************************************** +_locals: + MMTM SP,A12,FP + MOVE STK,FP + ADDK 32,STK + MOVK 7,A12 + MOVE A12,*FP,1 + MOVE *FP,A8,1 +EPI0_1: + SUBK 32,STK + MMFM SP,A12,FP + RETS 0 + .end diff --git a/symbols/README.md b/symbols/README.md index 5093fca..4927d19 100644 --- a/symbols/README.md +++ b/symbols/README.md @@ -4,9 +4,16 @@ One file per ROM revision. Addresses are **bit** addresses and are not assumed stable across firmware revisions; v2.2 is the first target. `vt1200-v22.syms` contains only the entries already proven by the hand-written -payloads in `vendor/vt1200/dwt/gui/xhello.asm` and the README. The compiler -milestone that first calls Xlib must extend it from the Ghidra project / -`ptr_constants.py` and validate each entry on MAME, then on the real unit. +payloads in `vendor/vt1200/dwt/gui/xhello.asm` and the README. **M2 consumes +only `strlen`**: `scripts/m2/gen-rom-imports.sh` emits an ABI-preserving +`jauc` stub for every entry, and the M2 harness calls `_strlen` at +`0x3FB458D0`. The other entries are recorded for later milestones and are not +yet exercised by compiled C. + +M3 owns the wider table: it extends the entries from the Ghidra project / +`ptr_constants.py`, adds the Xlib call sequence, and validates each entry on +MAME, then on the real unit. Do not assume an address here is validated just +because it is listed. The build resolves these as absolute symbols (or ABI-preserving stubs). Do not bake them into the compiler: keep them in a per-ROM linker script.