13 Commits
m1 .. m2
22 changed files with 2095 additions and 117 deletions

No files matched your search

+214 -113
View File
@@ -3,83 +3,220 @@
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 `tms34010` is
the M1 tip and keeps the original M1 backend and suite.
- **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 a 32-bit slot), emits `CALLA`,
then re-establishes both field sizes (`SETF 16,0,0`, `SETF 32,0,1`).
- A callee starts with `SETF 16,0,0`/`SETF 32,0,1`, 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-seven `llvm-lit`/FileCheck tests under
`llvm/test/CodeGen/TMS34010/m2-*.ll`, including negative tests that pin the
loud refusals. The ABI/data coverage includes `m2-returned`,
`m2-returned-call`, `m2-call-noret`, `m2-data-align`, `m2-data-avail`,
`m2-data-z64` and `m2-data-i1`.
- 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 | `FS0 = 16` and `FS1 = 32`, 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`. `FS0` must be 16 and `FS1` must be 32 at function entry and
immediately after every `CALLA` (`SETF 16,0,0` then `SETF 32,0,1`), because
ROM code may change either field size. FS0 drives `short` loads/stores and FS1
drives 32-bit words.
## 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: 37 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. GSPA's `.even` aligns to a 16-bit boundary only, so an explicit
`align 4`/`align 8` on a `.data` object is **not** honored in M2: the target's
standalone object alignment is 2 bytes. Private/local symbol names are
sanitized to GSPA-safe names (`_HH` for non-alphanumerics). Unsupported
initializer shapes — including arrays whose element width has no GSPA
directive (`i1`, `i64`) — 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).
- **Tail calls:** ordinary `tail` markers are demoted to ordinary calls;
`musttail` is not implemented (M2 C does not emit it). Real tail calls are
M3.
- **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; also on branch `m1`, not an M2 gate)
scripts/tests/m1/ M1 gates (historical; also on 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 +229,12 @@ 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 no
DOSBox/MAME/SDK; it needs only the built `clang`, `opt`, `llc` and
`tools/tms-bitaddr/tms-bitaddr`. On Debian:
```sh
sudo apt-get install -y cmake ninja-build ccache dosbox mtools
@@ -112,13 +251,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 +266,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 +283,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 +308,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.
File diff suppressed because it is too large. Load diff
+3
View File
@@ -0,0 +1,3 @@
__attribute__((noinline)) int add2(int a, int b) { return a + b; }
__attribute__((noinline)) int first(int a, int b) { return a; }
__attribute__((noinline)) int second(int a, int b) { return b; }
+2
View File
@@ -0,0 +1,2 @@
extern int add2(int, int);
int caller(void) { return add2(20, 22); }
+3
View File
@@ -0,0 +1,3 @@
/* First ROM call: strlen("0::0") must be 4. */
extern int strlen(const char *);
int check(void) { return strlen("0::0"); }
+327
View File
@@ -0,0 +1,327 @@
#!/bin/sh
# M2 dis34 round-trip.
#
# scripts/m2/dis-roundtrip.sh
#
# Extracts the linked .text/.data from build/m2/m2.coff (using the COFF
# section table, not hard-coded file offsets), disassembles the text with the
# VT1200 tree's dis34 at the linked bit address, and checks that GSPA encoded
# the TI-ABI code llc printed:
#
# _strlen JAUC 3FB458D0 (the ROM stub, ABI-preserving)
# _first MOVE A13,A? ; ADDI -32,A? ; RETS 2
# _second ADDI -64,A? ; RETS 2
# _keep old-STK push, argument pushes, CALLA, SETF, LOADSTK, RETS 2
# _locals nonzero frame ADDI, a frame access, RETS 0
#
# It also checks the old-STK operand against the number of pushes before
# SETFP, and the .data bytes contain the "0::0" string literal.
#
# Overridable: M2_COFF DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
COFF=${M2_COFF:-$ROOT/build/m2/m2.coff}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M2 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m2/run.sh)"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m2-dis34.XXXXXX")
FAIL_LOG=${M2_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m2-dis34-failure.txt}
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# Read little-endian fields out of the COFF. od keeps this dependency-free.
read_u8s() {
od -An -v -tu1 -j "$1" -N "$2" "$COFF"
}
u16() {
read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }'
}
u32() {
read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }'
}
echo "== parse the COFF section table ($COFF)"
nscns=$(u16 2)
opthdr=$(u16 16)
[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections (nscns='$nscns')"
[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header (opthdr='$opthdr')"
TEXT_ADDR=
TEXT_BITS=
TEXT_PTR=
DATA_ADDR=
DATA_BITS=
DATA_PTR=
sh_base=$((20 + opthdr))
i=0
while [ "$i" -lt "$nscns" ]; do
o=$((sh_base + i * 40))
name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000')
if [ "$name" = ".text" ]; then
TEXT_ADDR=$(u32 $((o + 8)))
TEXT_BITS=$(u32 $((o + 16)))
TEXT_PTR=$(u32 $((o + 20)))
elif [ "$name" = ".data" ]; then
DATA_ADDR=$(u32 $((o + 8)))
DATA_BITS=$(u32 $((o + 16)))
DATA_PTR=$(u32 $((o + 20)))
fi
i=$((i + 1))
done
[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section"
[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty"
[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes"
[ -n "$DATA_ADDR" ] || fail "COFF has no .data section"
[ "$DATA_BITS" -gt 0 ] 2>/dev/null || fail ".data is empty"
[ $((DATA_BITS % 8)) -eq 0 ] || fail ".data size $DATA_BITS bits is not whole bytes"
TEXT_HEX=$(printf '%08X' "$TEXT_ADDR")
ENTRY=$(u32 $((20 + 16)))
ENTRY_HEX=$(printf '%08X' "$ENTRY")
TEXT_BYTES=$((TEXT_BITS / 8))
DATA_BYTES=$((DATA_BITS / 8))
[ "$TEXT_HEX" = "018A0000" ] \
|| fail ".text is linked at $TEXT_HEX, want 018A0000"
[ "$ENTRY_HEX" = "018A0000" ] \
|| fail "COFF entry point is $ENTRY_HEX, want 018A0000"
[ "$TEXT_PTR" -gt 0 ] || fail ".text has no raw data (scnptr=0)"
[ "$DATA_PTR" -gt 0 ] || fail ".data has no raw data (scnptr=0)"
echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes), scnptr=$TEXT_PTR"
echo " .data at $(printf '%08X' "$DATA_ADDR"): $DATA_BITS bits ($DATA_BYTES bytes), scnptr=$DATA_PTR"
echo " entry point $ENTRY_HEX"
dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/text.bin") .text bytes, want $TEXT_BYTES"
dd if="$COFF" of="$TMP/data.bin" bs=1 skip="$DATA_PTR" count="$DATA_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/data.bin")" -eq "$DATA_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/data.bin") .data bytes, want $DATA_BYTES"
echo "== resolve the linked symbols from the COFF symbol table"
symptr=$(u32 8)
nsyms=$(u32 12)
[ "$symptr" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table"
[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF symbol table is empty"
sym_addr() {
_want=$1
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
if [ "$_n" = "$_want" ]; then
u32 $((_o + 8))
return 0
fi
_aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }')
_i=$((_i + 1 + _aux))
done
return 1
}
get_sym() {
_name=$1
_addr=$(sym_addr "$_name") || fail "COFF symbol table has no $_name"
[ -n "$_addr" ] || fail "COFF symbol table has no $_name"
printf '%s' "$_addr"
}
STRLEN=$(get_sym _strlen)
FIRST=$(get_sym _first)
SECOND=$(get_sym _second)
KEEP=$(get_sym _keep)
LOCALS=$(get_sym _locals)
ADD2=$(get_sym _add2)
STRLEN_HEX=$(printf '%08X' "$STRLEN")
FIRST_HEX=$(printf '%08X' "$FIRST")
SECOND_HEX=$(printf '%08X' "$SECOND")
KEEP_HEX=$(printf '%08X' "$KEEP")
LOCALS_HEX=$(printf '%08X' "$LOCALS")
ADD2_HEX=$(printf '%08X' "$ADD2")
echo " _strlen=$STRLEN_HEX _first=$FIRST_HEX _second=$SECOND_HEX"
echo " _add2=$ADD2_HEX _keep=$KEEP_HEX _locals=$LOCALS_HEX"
echo "== disassemble the emitted .text with dis34"
"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \
|| { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; }
[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output"
# match_at FILE DECIMAL_ADDR ERE
# dis34 -l prints "018a05c0:\tmove a13, a0\t; 4da0". The operand must be
# followed by optional whitespace and then the comment/end-of-line marker, so
# "rets 1" cannot satisfy a "rets" pattern.
match_at() {
_file=$1
_addr=$2
_pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
# expect_at LABEL DECIMAL_ADDR EXPECTED_TEXT ERE
expect_at() {
_label=$1
_addr=$2
_want=$3
_pat=$4
_laddr=$(printf '%08x' "$_addr")
if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then
echo " ok $_label @ 0x$_laddr: $_want"
return 0
fi
{
echo "M2 dis34 round-trip: FAIL"
echo " $_label at bit 0x$_laddr: expected $_want"
echo " dis34 output near 0x$_laddr:"
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { seen = 1 }
seen { print " " $0; n++ }
seen && n >= 4 { exit }
' "$TMP/dis34.txt"
if cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null; then
echo " full disassembly preserved at: $FAIL_LOG"
else
echo " full disassembly: $TMP/dis34.txt (temporary; removed on exit)"
fi
} >&2
exit 1
}
# func_body DECIMAL_ADDR > FILE: the function's instructions, through RETS.
func_body() {
_laddr=$(printf '%08x' "$1")
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { on = 1 }
on { print }
on && low ~ /^[0-9a-f]+:[[:space:]]+rets([[:space:]]|;|$)/ { exit }
' "$TMP/dis34.txt"
}
# expect_in BODY EXPECTED_TEXT ERE
expect_in() {
_file=$1
_want=$2
_pat=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_want"
return 0
fi
echo "M2 dis34 round-trip: FAIL: missing $_want" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
# expect_seq BODY REGEX...: each pattern must occur, in order, after the last.
expect_seq() {
_file=$1
shift
_last=0
for _pat in "$@"; do
_line=$(awk -v start="$_last" -v pat="$_pat" '
NR > start && tolower($0) ~ pat { print NR; exit }
' "$_file")
[ -n "$_line" ] || {
echo "M2 dis34 round-trip: FAIL: call sequence missing /$_pat/" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
_last=$_line
done
echo " ok call sequence ($# instructions/operands, in order)"
}
echo "== check the linked instructions"
# ROM import stub: absolute unconditional jump, ABI-preserving.
expect_at "_strlen" "$STRLEN" "JAUC 3FB458D0" \
'jauc[[:space:]]+0x3fb458d0'
# _first must materialise its frame pointer and load argument 0 at FP(-32).
func_body "$FIRST" > "$TMP/first.txt"
expect_in "$TMP/first.txt" "_first MOVEFP (MOVE A13,A?)" \
'move[[:space:]]+a13,[[:space:]]*a[0-9]+'
expect_in "$TMP/first.txt" "_first ADDI -32" \
'addi[[:space:]]+(0xffe0|-0x20|-32),'
expect_in "$TMP/first.txt" "_first RETS 2" \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _second loads argument 1 at FP(-64).
func_body "$SECOND" > "$TMP/second.txt"
expect_in "$TMP/second.txt" "_second ADDI -64" \
'addi[[:space:]]+(0xffc0|-0x40|-64),'
expect_in "$TMP/second.txt" "_second RETS 2" \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _keep exercises the whole call/return path against _add2.
func_body "$KEEP" > "$TMP/keep.txt"
expect_seq "$TMP/keep.txt" \
'move[[:space:]]+a14,[[:space:]]*-\*sp,[[:space:]]*1' \
'move[[:space:]]+[^,]+,[[:space:]]*\*a14\+,[[:space:]]*1' \
'calla' \
'setf[[:space:]]+16,[[:space:]]*0,[[:space:]]*0' \
'setf[[:space:]]+32,[[:space:]]*0,[[:space:]]*1' \
'move[[:space:]]+\*sp\(0x[0-9a-f]+\),[[:space:]]*a14,[[:space:]]*1' \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _locals has a nonzero frame allocation, a frame access and RETS 0.
func_body "$LOCALS" > "$TMP/locals.txt"
expect_in "$TMP/locals.txt" "_locals frame ADDI" \
'addi[[:space:]]+0x0*[1-9a-f][0-9a-f]*,[[:space:]]*a14'
expect_in "$TMP/locals.txt" "_locals frame access" \
'move[[:space:]]+(a[0-9]+,[[:space:]]*\*a[0-9]+|\*a[0-9]+,[[:space:]]*a[0-9]+)'
expect_in "$TMP/locals.txt" "_locals RETS 0" \
'rets([[:space:]]+0)?([[:space:]]*(;|$))'
# Old-STK offset: the LOADSTK operand must count every callee-saved push
# before SETFP, plus the caller's pushed old STK. This is the check the M2
# lit tests deliberately leave to dis34.
echo "== check the old-STK restore offsets"
check_stk() {
_label=$1
_addr=$2
func_body "$_addr" > "$TMP/func.txt"
_pushes=$(awk '
tolower($0) ~ /move[[:space:]]+[a-z0-9]+,[[:space:]]*-\*sp,[[:space:]]*1/ { n++ }
tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*a13/ { print n; exit }
' "$TMP/func.txt")
[ -n "$_pushes" ] || fail "$_label has no SETFP (MOVE A14,A13)"
_operand=$(grep -Eio '\*sp\(0x[0-9a-f]+\)' "$TMP/func.txt" | head -1 \
| sed 's/.*0x//; s/).*//')
[ -n "$_operand" ] || fail "$_label has no LOADSTK (*SP(n),A14)"
_want=$(( (_pushes + 1) * 32 ))
_got=$((0x$_operand))
[ "$_got" -eq "$_want" ] \
|| fail "$_label restores A14 from *SP(0x$_operand) ($_got bits) but $_pushes pushes need $_want bits"
echo " ok $_label: $_pushes pushes -> *SP($(printf '0x%x' "$_want")), A14, 1"
}
check_stk "_add2" "$ADD2"
check_stk "_first" "$FIRST"
check_stk "_second" "$SECOND"
check_stk "_keep" "$KEEP"
# Negative control for the RETS operand boundary: "rets 1" must not match.
printf '%08x:\trets\t1\t; 0961\n' "$ADD2" > "$TMP/rets-neg.txt"
if match_at "$TMP/rets-neg.txt" "$ADD2" 'rets([[:space:]]+0)?'; then
fail "negative control: the RETS pattern accepted 'rets 1'"
fi
echo " ok negative control @ 0x$(printf '%08x' "$ADD2"): 'rets 1' rejected"
echo "== check the copied .data bytes"
# The check.c string literal "0::0\0" must survive llc -> GSPA -> COFF.
DATA_HEX=$(od -An -v -tx1 "$TMP/data.bin" | tr -d ' \n')
case "$DATA_HEX" in
*303a3a3000*) echo " ok .data contains 30 3A 3A 30 00 (\"0::0\")" ;;
*) fail ".data does not contain 30 3A 3A 30 00 (got $DATA_HEX)" ;;
esac
echo "M2 dis34 round-trip: PASS"
+59
View File
@@ -0,0 +1,59 @@
; entry.asm -- M2 TI ABI smoke-test entry, linked at bit 018A0000.
;
; Markers (0x20 bits apart):
; 018A8000 check() strlen("0::0") == 4
; 018A8020 first(20,22) == 20
; 018A8040 second(20,22) == 22
; 018A8060 caller() add2(20,22) == 42
; 018A8080 locals() == 7
; 018A80A0 keep(10) == 16 (g == 5 before the call, + 11)
.text
.globl _start
_start:
setf 32, 0, 1
setf 16, 0, 0
movi _cstack, a14
move a14, a13
calla _check
movi 018A8000h, a2
move a8, *a2, 1
; first(20, 22): last argument first, on the program stack
move a14, -*sp, 1
movi 22, a0
move a0, *a14+, 1
movi 20, a0
move a0, *a14+, 1
calla _first
movi 018A8020h, a2
move a8, *a2, 1
; second(20, 22)
move a14, -*sp, 1
movi 22, a0
move a0, *a14+, 1
movi 20, a0
move a0, *a14+, 1
calla _second
movi 018A8040h, a2
move a8, *a2, 1
calla _caller
movi 018A8060h, a2
move a8, *a2, 1
calla _locals
movi 018A8080h, a2
move a8, *a2, 1
move a14, -*sp, 1
movi 10, a0
move a0, *a14+, 1
calla _keep
movi 018A80A0h, a2
move a8, *a2, 1
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Generate absolute-jump stubs for the ROM entry points in a per-ROM symbol file.
# Usage: gen-rom-imports.sh symbols/vt1200-v22.syms OUT.asm
set -eu
SYMS=${1:?symbol file}
OUT=${2:?output file}
printf '\t.text\n' > "$OUT"
awk '
/^[[:space:]]*#/ || NF < 2 { next }
{
name = $1
addr = $2
sub(/^0x/, "", addr)
printf "\t.globl _%s\n_%s:\n\tjauc %sh\n", name, name, addr
}
' "$SYMS" >> "$OUT"
+3
View File
@@ -0,0 +1,3 @@
extern int add2(int, int);
int g = 5; /* initialized .data: also tests the loader copy */
int keep(int a) { int x = g; int y = add2(a, 1); return x + y; }
+16
View File
@@ -0,0 +1,16 @@
-a
-o M2.OUT
-m M2.MAP
-e _start
M2.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 018A9000h, length = 40000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
+1
View File
@@ -0,0 +1 @@
int locals(void) { volatile int x = 7; return x; }
+51
View File
@@ -0,0 +1,51 @@
-- m2-check.lua -- load the M2 COFF with dwt/loader/loader.lua and read the
-- marker words the compiled TI-ABI code wrote.
--
-- M2_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- M2_EXPECT_CHECK 4 (strlen("0::0"))
-- M2_EXPECT_FIRST 20 (first(20,22))
-- M2_EXPECT_SECOND 22 (second(20,22))
-- M2_EXPECT_CALLER 42 (add2(20,22))
-- M2_EXPECT_LOCALS 7
-- M2_EXPECT_KEEP 16 (g == 5, + keep(10))
--
-- The marker read runs one frame before loader.lua's inspection/exit frame.
dofile(os.getenv("M2_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local EXPECT_CHECK = tonumber(os.getenv("M2_EXPECT_CHECK") or "4")
local EXPECT_FIRST = tonumber(os.getenv("M2_EXPECT_FIRST") or "20")
local EXPECT_SECOND = tonumber(os.getenv("M2_EXPECT_SECOND") or "22")
local EXPECT_CALLER = tonumber(os.getenv("M2_EXPECT_CALLER") or "42")
local EXPECT_LOCALS = tonumber(os.getenv("M2_EXPECT_LOCALS") or "7")
local EXPECT_KEEP = tonumber(os.getenv("M2_EXPECT_KEEP") or "16")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local check = prog:read_u32(0x018A8000)
local first = prog:read_u32(0x018A8020)
local second = prog:read_u32(0x018A8040)
local caller = prog:read_u32(0x018A8060)
local locals = prog:read_u32(0x018A8080)
local keep = prog:read_u32(0x018A80A0)
print(string.format(
"M2: check=%d first=%d second=%d caller=%d locals=%d keep=%d",
check, first, second, caller, locals, keep))
if check == EXPECT_CHECK and first == EXPECT_FIRST
and second == EXPECT_SECOND and caller == EXPECT_CALLER
and locals == EXPECT_LOCALS and keep == EXPECT_KEEP then
print("M2 MAME: PASS")
else
print(string.format(
"M2 MAME: FAIL (want check=%d first=%d second=%d caller=%d locals=%d keep=%d)",
EXPECT_CHECK, EXPECT_FIRST, EXPECT_SECOND, EXPECT_CALLER,
EXPECT_LOCALS, EXPECT_KEEP))
end
end
end, "m2-check")
+171
View File
@@ -0,0 +1,171 @@
#!/bin/sh
# M2 end-to-end smoke test: TI ABI + a real ROM strlen call.
#
# scripts/m2/run.sh
#
# Compiles five C translation units with scripts/tms-clang + llc -O0, assembles
# and links them with the 1987 GSPA/GSPLNK 2.00 under DOSBox, then loads the
# COFF into MAME through dwt/loader/loader.lua and checks the marker words:
#
# check() strlen("0::0") == 4
# first(20,22) == 20
# second(20,22) == 22
# caller() add2(20,22) == 42
# locals() == 7
# keep(10) == 16
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF
# SDK_IMG M2_WORK M2_COFF M2_SDK LOADER_AT LOADER_END
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$ROOT/scripts/m2
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}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${M2_WORK:-$ROOT/build/m2}
COFF=${M2_COFF:-$WORK/m2.coff}
SDK=${M2_SDK:-$ROOT/build/m2-sdk}
ASM=$WORK/asm
LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua
LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
LOADER_AT=${LOADER_AT:-2500}
LOADER_END=${LOADER_END:-2700}
SYMS=${M2_SYMS:-$ROOT/symbols/vt1200-v22.syms}
fail() {
echo "m2: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg"
mkdir -p "$(dirname "$COFF")"
echo "== compile and select"
export TMS_CLANG TMS_OPT TMS_BITADDR
for name in check args caller locals keep; do
"$ROOT/scripts/tms-clang" -O2 -o "$ASM/$name.ll" "$DIR/$name.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $name.c"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
# strlen("0::0") must be a real ROM call, not a folded 4.
grep -q 'CALLA _strlen' "$ASM/check.s" \
|| fail "check.s has no 'CALLA _strlen' (strlen was folded or not called)"
echo " check.s: $(grep -E 'CALLA _strlen' "$ASM/check.s" | head -1 | sed 's/^[[:space:]]*//')"
echo "== generate the per-ROM import stubs"
"$DIR/gen-rom-imports.sh" "$SYMS" "$WORK/rom-imports.asm"
grep -q '^_strlen:' "$WORK/rom-imports.asm" \
|| fail "rom-imports.asm has no _strlen stub"
grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | grep -qi 'jauc 3FB458D0h' \
|| fail "rom-imports.asm _strlen stub does not jump to 3FB458D0h"
echo " _strlen: $(grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | tail -1 | sed 's/^[[:space:]]*//')"
echo "== extract the 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
cat "$DIR/entry.asm" "$ASM/check.s" "$ASM/args.s" "$ASM/caller.s" \
"$ASM/locals.s" "$ASM/keep.s" "$WORK/rom-imports.asm" > "$SDK/M2.ASM"
# The linker command file must keep CRLF line endings.
sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M2.CMD"
rm -f "$SDK/M2.OBJ" "$SDK/M2.OUT" "$SDK/M2.MAP" "$SDK/M2.LST"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa M2 M2.obj M2.lst -l" \
-c "gsplnk M2.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; }
[ -f "$SDK/M2.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/M2.LST" > "$WORK/m2.lst"
grep -q 'No Errors' "$WORK/m2.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/m2.lst" >&2 || true; fail "GSPA reported errors"; }
grep -q 'No Warnings' "$WORK/m2.lst" \
|| { grep -i -B2 -A2 'warning' "$WORK/m2.lst" >&2 || true; fail "GSPA reported warnings"; }
[ -f "$SDK/M2.OUT" ] || fail "GSPLNK produced no M2.OUT"
sed 's/\r$//' "$SDK/M2.MAP" > "$WORK/m2.map"
cp "$SDK/M2.OUT" "$COFF"
cp "$SDK/M2.ASM" "$WORK/m2.asm"
ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m2.map")
[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000"
grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m2.map" \
|| fail "linker map has no _start entry point"
BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m2.map")
BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m2.map")
[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section"
[ "$BSS_ORIGIN" = "018A9000" ] \
|| fail "linker .bss origin is $BSS_ORIGIN, want 018A9000 (link.cmd ram origin)"
[ "$BSS_LENGTH" = "00008000" ] \
|| fail ".bss is $BSS_LENGTH bits, expected 00008000 (_cstack)"
DATA_ORIGIN=$(awk '$1 == ".data" && $2 == "0" { print $3; exit }' "$WORK/m2.map")
DATA_LENGTH=$(awk '$1 == ".data" && $2 == "0" { print $4; exit }' "$WORK/m2.map")
[ -n "$DATA_LENGTH" ] || fail "linker map has no .data section"
[ "$DATA_LENGTH" != "00000000" ] || fail ".data is empty (no initialized globals)"
echo " linked .text at 018A0000, .data at $DATA_ORIGIN ($DATA_LENGTH bits), .bss at $BSS_ORIGIN ($BSS_LENGTH bits)"
echo "== run the COFF in MAME"
[ -x "$MAME" ] || fail "MAME binary not built: $MAME"
[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA"
[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN"
VT1200_SKIP_LEVEL1=1 \
LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \
LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \
LOADER_OUT="$WORK/evidence" \
M2_LOADER_LUA="$LOADER_LUA" \
M2_EXPECT_CHECK=4 M2_EXPECT_FIRST=20 M2_EXPECT_SECOND=22 \
M2_EXPECT_CALLER=42 M2_EXPECT_LOCALS=7 M2_EXPECT_KEEP=16 \
timeout "${MAME_TIMEOUT:-600}" "$MAME" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$DIR/m2-check.lua" -autoboot_delay 0 \
> "$WORK/mame.log" 2>&1 \
|| { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; }
MARKERS=$(grep -E '^M2: check=' "$WORK/mame.log" | tail -1 || true)
[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M2 markers"; }
echo " $MARKERS"
grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \
|| { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; }
grep -q 'placed \.text .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .text bytes"
grep -q 'placed \.data .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .data bytes"
grep -q '^M2 MAME: PASS' "$WORK/mame.log" \
|| { tail -30 "$WORK/mame.log" >&2; fail "marker check failed"; }
echo "M2 MAME: PASS"
+133
View File
@@ -0,0 +1,133 @@
#!/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" | sed -n '1p')
if [ -z "$_reg" ]; then
echo "M2 diff: FAIL: our add2 has no ADDI $_off frame offset for $_desc" >&2
sed 's/^/ /' "$_file" >&2
exit 1
fi
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
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# M2 test gate: rebuild the linked COFF through the normal GSPA/GSPLNK path,
# then verify dis34 decodes the TI-ABI instructions at the linked addresses.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
# Always rebuild: a stale build/m2/m2.coff would let an asm-encoding
# regression pass without ever touching the compiler or assembler. Keep
# dis-roundtrip.sh pointed at the file this run produces, including when
# M2_WORK/M2_COFF are overridden.
WORK=${M2_WORK:-$ROOT/build/m2}
M2_COFF=${M2_COFF:-$WORK/m2.coff}
export M2_COFF
"$ROOT/scripts/m2/run.sh" "$@"
exec "$ROOT/scripts/m2/dis-roundtrip.sh" "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# M2 test gate: compile five C functions, assemble/link with GSPA/GSPLNK,
# load the COFF in MAME and check the TI-ABI marker words and ROM strlen call.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
exec "$ROOT/scripts/m2/run.sh" "$@"
+2 -1
View File
@@ -43,7 +43,8 @@ trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# shellcheck disable=SC2086
"$TMS_CLANG" --target=tms34010-unknown-none -O0 -Xclang -disable-O0-optnone \
-S -emit-llvm -ffreestanding -nostdinc $INCLUDES -o "$TMP/step1.ll" "$IN"
-S -emit-llvm -ffreestanding -fno-builtin -nostdinc $INCLUDES \
-o "$TMP/step1.ll" "$IN"
"$TMS_BITADDR" --scale "$TMP/step1.ll" "$TMP/scaled.ll" 2>/dev/null
+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.