docs: M2 TI ABI, ROM strlen and golden ABI check

This commit is contained in:
arti committed 2026-11-11 17:05:33 +00:00
1 parent fa00b8740b
commit 3a188b35fb
7 files changed
+386 -116

No files matched your search

+201 -113
View File
@@ -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:** <https://git.wut.ee/opencode.siil.arti.ee/llvm-tms34010>
(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:** <https://git.wut.ee/opencode.siil.arti.ee/tms-c-compiler>
— 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+<bit addend>` (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.
+129
View File
@@ -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<n>; 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"
+1
View File
@@ -0,0 +1 @@
int add2(a, b) int a; int b; { return a + b; }
+22
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
int locals() { int x; x = 7; return x; }
+22
View File
@@ -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
+10 -3
View File
@@ -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.