Files

703 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tms-c-compiler
A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of
upstream LLVM.
**Status: M4.** `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). M4 adds function pointers and `CALL Rs`, 32-bit
multiply/divide, zero-extending narrow loads, module-unique private labels,
`i8`/`i16` fixed arguments and the contiguous-stack varargs form, the X11R4
`XImage`/`XEvent` layouts, the full image/event client, the corrected 30-entry
ROM symbol table, and the pinned termtris full-game integration. It is still
**assembly-only**: no object writer, no `clang -c`, and no native COFF;
GSPA/GSPLNK under DOSBox produce the COFF. Floats, `long long`, general
varargs, bitfields, structs by value, narrow returns and native COFF are M5.
Branch `m4` is the current milestone snapshot in both repositories; the
default branch is `m3` until the controller/user updates it after the final
review. Branch `m3` remains the previous snapshot, `m2` is the M2 tip and
`m1` the reproducible M1 snapshot.
- 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)
- M3 plan: [`docs/superpowers/plans/2026-11-11-tms34010-m3-control-flow-and-xlib-window.md`](docs/superpowers/plans/2026-11-11-tms34010-m3-control-flow-and-xlib-window.md)
- M4 plan: [`docs/superpowers/plans/2026-11-12-tms34010-m4-xlib-image-events-and-termtris.md`](docs/superpowers/plans/2026-11-12-tms34010-m4-xlib-image-events-and-termtris.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 `m4`) — the full buildable LLVM 23.1.5 tree with the TMS34010
target, clang bit-address changes and the M2/M3/M4 backend. Branch `m3`
(the repository default) is the previous snapshot; branch `m2` is the M2
tip and 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>
(branch `m4`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`,
`tms-clang`, symbols, the M2/M3/M4 harnesses and the spike. Branch `m3`
(the repository default) is the previous snapshot; branch `m2` is the M2
snapshot and 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;
M2/M3/M4 do not change it.
## What M4 provides
M4 closes the remaining Xlib path to a real image/event client and a full-game
integration, still assembly-only:
- **Function pointers and `CALL Rs`.** A function pointer is a 32-bit bit
address; an indirect call lowers to the one-word register call `CALL Rs`
(GSPA `0x0920`, `dis34` `call aN`), with the same old-STK save, argument
pushes, `SETF` fixup and `RETS 2` as a direct call. Direct calls remain
`CALLA _sym`. `m4-fnptr.ll` + `scripts/tests/m4/fnptr.sh`.
- **32-bit multiply/divide.** `MPYS`/`MPYU`/`DIVS`/`DIVU` with an odd
destination (the even form is the `Rd:Rd+1` pair and would clobber the next
register); remainder is quotient + `MPYS` + `SUB`. There is no compiler
multiply/divide helper in `RTS.LIB`; `_div` is only the C `div()` wrapper.
`m4-mul.ll` + `scripts/tests/m4/mul.sh`.
- **Zero-extending and extending narrow loads.** `ZEXTLOAD`/`EXTLOAD` i8/i16
lower to the sign-extending load plus `ANDI 255`/`ANDI 65535`; an i1 load is
a byte load masked to one bit. `m4-zext.ll`, `m4-anyext.ll`,
`m4-i1load.ll`.
- **Module-unique private labels.** Every local symbol gets a custom 64-bit
string-hash token (FNV-style, not canonical FNV-1a), so two TUs with literals
no longer collide in GSPA's flat table; public names are unchanged.
`m4-private.ll` and the two-TU `scripts/tests/m4/private.sh`.
- **`i8`/`i16` fixed arguments.** Promoted to a 4-byte stack slot; the caller
sign/zero-extends per the parameter attribute and the callee truncates.
`i8`/`i16`/`i64`/float/aggregate returns remain rejected. `m4-arg8.ll`.
- **Contiguous-stack varargs.** Only the `&last_fixed` walk used by termtris'
`sprintf`; no `va_list`/`va_start`/`va_arg`. Zero-fixed and i8/i16-anchor
definitions are rejected. Static variadic definitions can lose `...` under
`-O2` dead-argument elimination; external termtris `sprintf` is unaffected.
`m4-varargs.ll`, `m4-reject-vararg0.ll`,
`m4-reject-vararg-narrow{,16}.ll`, `scripts/tests/m4/varargs.sh`.
- **Driver policies.** `scripts/tms-clang` passes `-replexitval=never` (the
i33 exit-value multiply is unselectable on this i64-less target) and
`-switch-to-lookup=false` (a late byte-offset GEP after `--scale` would be
read as a bit offset, an 8x address bug). Both are driver policies because
`opt` has no target machine.
- **X11R4 `XImage`/`XEvent`/`XErrorEvent` layouts and the image client.** The
real 32-bit packed offsets: `XImage` data 16, byte_order 20, bytes_per_line
40, bits_per_pixel 44, f 64, `sizeof` 88; `XEvent` 96; `XErrorEvent` 20
(with the explicit X11R4 object-size compatibility pad); `Display.screens`
byte 0x90 / bit 0x480, `Screen.root` byte 0x8 / bit 0x40. The client draws a
1-bit LSBFirst checker through an aligned pixmap + `XCopyArea` (12-pixel
phase) and 15 bands of 32 rows (the ROM request buffer is 16 KiB), with
`.bss` at `0x01A00000` and `prog` at `0x018A0000`. `XCopyArea` copies
`IMG_W - 12` = 628 columns, so the rightmost 12 source columns (the last
visible cell's final 12 px) are not displayed; the last visible cell is
20 px, not 32. It asserts the checker pixels, the real LK401 `q`
(keycode 193 / keysym `0x71`), window focus and zero X errors in MAME.
- **Corrected 30-entry v2.2 symbol table.** `XMapWindow` is `0x3FD64F60`
(MapWindow opcode 8); `0x3FD645B0` is `XRaiseWindow` (ConfigureWindow
opcode 12, `CWStackMode=Above`). The XG block and the image/error-handler
entries are validated by `scripts/m4/validate-syms.sh` and
`scripts/tests/m4/symbols.sh`.
- **Pinned termtris.** The full game builds with this compiler and the 1987
GSPA/GSPLNK + `rts.lib`, at the pinned `vt1200-termtris` commit, and runs
board/move/panels/quit in MAME with real key injection, the `0x40000`-bit
`prog` region and the required thresholds. The tree is test-only (GPLv3).
- **Differential goldens.** The 1987 TI compiler's function-pointer and
image/struct-offset assembly is stored under `scripts/tests/m4/diff/`; the
check compares ABI shape without running DOSBox.
## What M3 provides (branch `m3`)
M3 adds integer control flow and the first compiled C Xlib client, still
assembly-only:
- Conditional and unconditional branches for `i32` and pointer values:
`CMP`/`CMPI` plus `JAEQ`/`JANE`, the signed `JALT`/`JALE`/`JAGT`/`JAGE`, the
unsigned `JALO`/`JALS`/`JAHI`/`JAHS`, and `JAUC` for an unconditional jump.
Ordinary `while`/`for` loops (subject to the M3 limitations below), if/else
and `?:` work; `?:` expands to a compare plus a two-block diamond
(`SELECTCC`), and `setcc` becomes that diamond over 1/0.
- Integer ALU and shift operations: `SUB`, `AND`, `OR`, `XOR`, `NEG`, `NOT`,
`SLL`, `SRL`, `SRA`, plus `ADDI`/`ANDI`/`SLLK`/`SRAK`/`SRLK` immediates.
- The first compiled C Xlib window: `XOpenDisplay`, `XCreateSimpleWindow`
(nine arguments in the ROM's order: display, root, x, y, width, height,
border width, border, background), `XMapWindow` and `XSync`. All four go
through validated ROM import stubs.
- The ROM's `Display`/`Screen` field displacements are **bit** offsets
`0x480` (`Display.screens`) and `0x40` (`Screen.root`), i.e. byte offsets
`0x90` and `0x8` in `scripts/m3/vt1200_xlib.h`.
- Misaligned accesses: `MOVE`/`MOVB` and the FS0/FS1 field-size forms have no
alignment requirement on the TMS34010, so packed and bit-addressed struct
fields select directly instead of being split into byte-wide accesses.
- Nine `m3-*.ll` lit tests on the `m3` branch (46 with the M2 suite); M4
removes five obsolete M2 refusal tests (`m2-reject-arg8`,
`m2-reject-call-arg8`, `m2-reject-call-indirect`, `m2-reject-call-vararg`,
`m2-reject-vararg`) and `m3-reject-zext.ll`, and adds 18 `m4-*` tests:
46 - 5 - 1 + 18 = 58 total with the M2 suite.
M3 remains an `i32`/pointer milestone. `setcc` is supported, but narrow
(`i8`/`i16`) parameters and returns are still rejected. Branches on `i64`,
floats, aggregates, varargs and indirect calls are not supported. The backend
still emits assembly only.
### M3 limitations
- **Narrow loads are sign-extending only.** `MOVB` handles signed `char` and
the FS0 `MOVE` handles signed `short`; `unsigned char`, `unsigned short` and
`_Bool` loads need a zero-extending pattern that M3 does not have. They stop
`llc` with a raw `Cannot select`, which `m3-reject-zext.ll` pins. M4 adds the
zero-extending loads; do not assume general narrow-load support in M3.
- **`-O2` loop exit-value rewriting is not lowered.** IndVarSimplify can turn
`sumto`'s loop into a closed-form `i33` multiply, which M3 cannot select. The
M3 harness therefore builds `branch.c` with `-replexitval=never`. Ordinary
`while`/`for` loops that keep a real back-edge and phi work. M4 adds
multiply/divide and moves the `-replexitval=never` policy into
`scripts/tms-clang`; see "What M4 provides".
## What M2 provides (branch `m2`)
M2 replaces the M1 test convention with the TI ABI and stops at assembly:
- `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).
## The TI ABI
| | M1 test convention (branch `m1`) | TI ABI (M2/M3/M4) |
|---|---|---|
| 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 |
| Locals | none | positive FP offsets |
| Return instruction | `RETS 0` | `RETS 2` with arguments, `RETS 0` without |
| Calls, callee-saves, stack args | rejected | required |
| Field size | harness sets FS1/FS0 before entry | `FS0 = 16` and `FS1 = 32`, set in the prologue and after every call |
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 M4 gates
These are the documented commands on branch `m4`; all must pass. The MAME
gates are slow and are run once.
```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: 58 passed (32 m2-* + 8 m3-* + 18 m4-*)
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean/llvm/test/CodeGen/TMS34010 --filter='(m2|m3|m4)-'
# 3. M4 image/event client in MAME (also runs layout.sh + validate-syms.sh)
scripts/tests/m4/run.sh
# -> m4 xshow: PASS (1-bit image + real q key + window focus + 0 X errors)
# 4. Linked .text decoded with dis34 (ROM stubs, CALL A?, SETF, RETS, checker)
scripts/tests/m4/dis-roundtrip.sh
# -> M4 dis34 round-trip: PASS
# 5. Layouts, the 30-entry symbol table, and the correction negative control
scripts/tests/m4/layout.sh
scripts/tests/m4/symbols.sh
# 6. The pinned termtris full-game integration
scripts/tests/m4/termtris.sh
# -> m4 termtris: PASS (board/move/panels/quit; .../termtris_*_3200.png)
# 7. Differential TI-compiler function-pointer/image ABI check (no DOSBox)
scripts/tests/m4/diff/check.sh
# -> M4 diff: PASS (goldens + negative controls)
# 8. The smaller M4 runtime gates
scripts/tests/m4/unsigned.sh
scripts/tests/m4/private.sh
scripts/tests/m4/mul.sh
scripts/tests/m4/fnptr.sh
scripts/tests/m4/varargs.sh
# 9. M2/M3 regressions and the GEP tool tests
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
scripts/tests/m3/layout.sh
scripts/tests/m2/run.sh
scripts/tests/m2/dis-roundtrip.sh
scripts/tests/m2/diff/check.sh
tools/tms-bitaddr/tests/run.sh
# -> tms-bitaddr: PASS
```
`scripts/tests/m4/run.sh` first runs `layout.sh` and
`scripts/m4/validate-syms.sh`, then compiles `scripts/m4/xshow.c` and
`scripts/m4/xgfix.c` with `scripts/tms-clang -O2`, runs
`llc -march=tms34010 -O0 -filetype=asm`, generates the ROM import stubs,
concatenates `scripts/m4/xshow-entry.asm`, assembles/links with GSPA/GSPLNK
2.00 under DOSBox, loads the COFF in MAME, injects the real LK401 `Q`, and
requires the checker pixels, `quit=1`, keycode 193 / keysym `0x71`,
`xerrors=0`, focus `0x400001` and the last event KeyPress. The 12-pixel
prefix is checked as the preceding checker cell in bands 0 and 1.
`scripts/tests/m4/termtris.sh` runs the pinned tree through
`scripts/m4/termtris/build.sh` and `play_keys.sh`; it enforces `loops >= 100`
for all four modes, `events >= 6` for move/panels/quit (`>= 2` for board),
`quit = 1` only for quit, focus = the created termtris window,
`status=0 (OK)` and a non-empty screenshot each time. The vendored tree is
test-only (GPLv3).
`scripts/tests/m4/diff/check.sh` is the new CI golden check. It compiles our
function-pointer and image equivalents with `scripts/tms-clang` + `llc` and
compares ABI shape with `scripts/tests/m4/diff/ti-fnptr.s` and
`ti-image.s`: an indirect `CALL A?`, the old-STK save, argument/return/`RETS
2`, ten `XCreateImage` and ten `XPutImage` pushes (the plan's 10/9 is a typo;
`XPutImage` takes display, drawable, gc, image, src_x, src_y, dest_x, dest_y,
width, height), nine `XCreateSimpleWindow` pushes, the leading
constant/Display-last order, and the Display/Screen/XImage bit displacements.
It then mutates copies of the goldens (removing the indirect call and
reversing the `XPutImage` argument pushes) and requires the check to fail;
the stored goldens are not modified. The goldens were generated once with
GSP C Compiler 2.01 under DOSBox; CI never runs DOSBox.
Paths are overridable with `TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`,
`MAME`, `DOSBOX`, `SDK_IMG`, the per-gate work/COFF variables and `M4_SYMS`.
## Run the M3 gates (branch `m3`)
These are the documented commands on branch `m3`; all must pass.
```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: 46 passed (37 m2-* + 9 m3-*)
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean-build/test/CodeGen/TMS34010 --filter='(m2|m3)-'
# 3. Layout/symbols, then compile, assemble, link and run in MAME
scripts/tests/m3/run.sh
# -> layout.sh: PASS (Display.screens=0x480, Screen.root=0x40; ...)
# -> M3 MAME: PASS
# (display/root/window nonzero, sum=55 max=22 lt=1, screens=0x480 root=0x40)
# 4. Rebuild the COFF and check the GSPA encodings with dis34
scripts/tests/m3/dis-roundtrip.sh
# -> M3 dis34 round-trip: PASS
# 5. Differential TI-compiler control-flow/Xlib check (no DOSBox)
scripts/tests/m3/diff/check.sh
# -> M3 diff: PASS
# 6. Negative controls: the same check must reject a wrong TI displacement
scripts/tests/m3/diff/negative-control.sh
# -> M3 diff negative controls: PASS
# 7. GEP scaling/normalization tool tests
tools/tms-bitaddr/tests/run.sh
# -> tms-bitaddr: PASS
```
`scripts/tests/m3/run.sh` first runs `scripts/tests/m3/layout.sh` (the ROM's
`0x480`/`0x40` bit displacements in both our asm and the 1987 golden) and
`scripts/m3/validate-syms.sh` (the four Xlib imports), then compiles
`scripts/m3/branch.c`, `layout.c` and `xhello.c` with `scripts/tms-clang -O2`,
runs `llc -march=tms34010 -O0 -filetype=asm`, generates the per-ROM import
stubs, concatenates the result with `scripts/m3/entry.asm`, assembles/links
with GSPA/GSPLNK 2.00 under DOSBox, and loads the COFF in MAME. `branch.c` is
built with `opt -replexitval=never`, because the default exit-value rewrite
turns `sumto`'s loop into an `i33` multiply that this target cannot select;
the loop itself is the point of the test.
The MAME run pins `VT1200_MONITOR=VR319` (the real 2048-bit row pitch) and
checks the ABI markers — nonzero display/root/window ids, `sumto(10) == 55`,
`max2(20, 22) == 22`, `lt(5, 7) == 1`, `layout_screens() == 0x480`,
`layout_root() == 0x40` — and, decisively, two all-white 32-bit client words in
VRAM at (150,150) and (250,200) inside the mapped window. A markers-only run or
a root-window bit is a failure. Paths are overridable with `TMS_CLANG`,
`TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`, `MAME`, `DOSBOX`, `SDK_IMG`, `M3_WORK`,
`M3_COFF` and `M3_SDK`.
`scripts/tests/m3/dis-roundtrip.sh` rebuilds through the normal GSPA/GSPLNK
path and uses `dis34` to check the linked Xlib stubs (`JAUC` to the ROM bit
addresses), the nine-argument `XCreateSimpleWindow` push order, the branch code
(`CMP`, `JALT`, `JAGT`, `JAEQ`, `JAUC`, `LBB…` labels), the old-STK restore
offsets, and the `"0::0"` `.data` bytes.
`scripts/tests/m3/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: a conditional branch and an unconditional loop back-edge in
`sumto`, a conditional branch in `max2`, the function's last `RETS` being
`RETS 2`, nine `XCreateSimpleWindow` argument pushes, and the `0x480`/`0x40`
bit displacements. The displacements must appear in the load that uses them
(TI `*A7(1152)` and `*A7(64),*FP(32),1`; ours an `ADDI 1152`/`ADDI 64` feeding
a load through the same register), with exact token boundaries, so an
unrelated local at `*FP(64)`, the constant `0x400`, or a prefix such as
`0x4800` cannot satisfy them. `scripts/tests/m3/diff/negative-control.sh`
mutates the stored TI displacement to 512 (and to `0x4800`) and requires
`check.sh` to fail. The check does not verify the result register or argument
order: the MAME gate checks the returned values and `dis-roundtrip.sh` checks
the push order. It does not compare instruction selection byte-for-byte. The
stored `ti-branch.s` and `ti-xhello.s` were generated once with
`gspcpp`/`gspcc`/`gspcg` (GSP C Compiler 2.01) under DOSBox; CI never runs
DOSBox, it greps the stored files. As in the MAME gate, `branch.c` must be
built with `-replexitval=never`; the default exit-value rewrite turns
`sumto`'s loop into an unselectable `i33` multiply.
## Run the M2 gates (branch `m2`)
These are the M2 commands; they also pass on `m3` (verified on branch `m3` on
2026-11-11: `M2 MAME: PASS`, `M2 dis34 round-trip: PASS`, `M2 diff: 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`. M3 adds the four Xlib entries
(`XOpenDisplay`, `XCreateSimpleWindow`, `XMapWindow`, `XSync`) and validates
their stubs in `scripts/m3/validate-syms.sh` and in MAME; `symbols/README.md`
records the evidence. Do not add other Xlib dependencies until they are
validated.
## 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 remain
a later milestone.
- **Narrow ABI types:** `i8`/`i16` parameters, `i64`, and narrow returns remain
rejected. Narrow accesses inside a function are sign-extending only: `MOVB`
for signed `char` and FS0 `MOVE` for signed `short`; zero-extending
`unsigned char`/`unsigned short`/`_Bool` loads are M4 work.
- **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.
## M3 non-goals
M3 deliberately stops at the first window and ordinary integer control flow:
- **XImage/XEvent and the real client:** no image transfer, event handling,
full X11R4 headers or real VT1200 client (M4). M3 uses exactly the four Xlib
calls above and no other Xlib entry.
- **Aggregates by value:** structs/unions passed or returned by value, `sret`,
`byval`, bitfields (M4/M5). Struct *fields* through pointers are supported.
- **Function pointers and indirect calls:** the callee must be a direct global
symbol (M4).
- **Floating point and `i64`:** no float/double or 64-bit integer arguments,
returns, arithmetic or branches (M5).
- **Object output:** still no `MCCodeEmitter`, object writer,
`llc -filetype=obj`, `clang -c` or native COFF; GSPA/GSPLNK produce the COFF.
The `JR`/branch-relaxation code-size pass is M5 as well.
- **Varargs, `musttail`, narrow ABI parameters/returns:** unchanged from M2.
### M3 carry-forwards in M4
M4 closes three of the five final-M3 prerequisites: zero-extending loads
replaced the reject test, private labels are module-unique, and unsigned/`<=`
runtime coverage exists. The new M4 harness and
`scripts/tests/m4/dis-roundtrip.sh` are operand/register-independent, and
`layout.sh` was tightened. The M3 regression is deliberately unchanged:
`m3-cmpi.ll` still uses `CHECK-DAG: MOVI 0`/`MOVI 1` (the 1/0 order is not
pinned), and `scripts/m3/dis-roundtrip.sh` still hardcodes the nine
`XCreateSimpleWindow` push registers. Those M3-strength fixes remain
test-hardening follow-ups; the M3 gates pass as-is. See the M4 feature list
above.
## M4 non-goals and the M5 boundary
M4 deliberately stops before:
- **Native object output:** no `MCCodeEmitter`, object writer,
`llc -filetype=obj`, `clang -c` or native COFF; GSPA/GSPLNK still produce
the COFF. The `JR`/branch-relaxation code-size pass and MMTM/MMFM
register-list saves are M5 (the current per-register system-stack pushes
have the identical ABI layout).
- **Floating point and `long long`:** no float/double or `i64` arguments,
returns, arithmetic or branches.
- **General varargs:** no `va_list`, `va_start` or `va_arg`; only the
contiguous `&last_fixed` pattern used by termtris `sprintf`.
- **Bitfields and aggregates by value:** no structs/unions by value, `sret`,
`byval`, bitfields or aggregate returns. Struct fields through pointers are
supported.
- **Narrow returns:** `i8`/`i16` returns remain rejected; fixed i8/i16
arguments are supported.
- **Xlib beyond the tested calls:** the client uses the validated image,
event, GC, window and error-handler entries; it does not implement the whole
Xlib surface, `XGetImage`, or `XDestroyImage` on a non-heap image.
- **Deeper libc/runtime coverage:** M5 owns the broader `RTS.LIB` integration.
The pinned termtris tree is test-only (GPLv3) and is not part of the compiler
or project distribution.
## Repository layout
```text
build/llvm-clean/ compiler fork checkout used for M2/M3/M4 (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/m2/ M2 harness: C inputs, entry.asm, link.cmd, stubs, checks
scripts/m3/ M3 harness: branch/xhello/layout inputs, Xlib header, entry/link
scripts/m4/ M4 harness: unsigned/private/mul/fnptr/varargs, XImage client,
XG fix, termtris build, symbols validator
scripts/tests/m2/ M2 gates (run.sh, dis-roundtrip.sh)
scripts/tests/m2/diff/ TI golden asm and the differential ABI check
scripts/tests/m3/ M3 gates (run.sh, dis-roundtrip.sh, layout.sh)
scripts/tests/m3/diff/ TI branch/Xlib goldens, differential check + negative controls
scripts/tests/m4/ M4 gates (run/dis-roundtrip/layout/symbols/termtris and the
unsigned/private/mul/fnptr/varargs gates)
scripts/tests/m4/diff/ TI function-pointer/image goldens + the differential 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
spike/ measurement corpus and reports
symbols/ per-ROM import tables (v2.2 subset)
vendor/ fetched assets and LLVM sources (ignored)
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 M2/M3/M4
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 termtris gate also needs the pinned
`vendor/vt1200-termtris` tree (test-only). 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
```
## Fetch the VT1200 assets
The compiler contract is checked against the VT1200 tree (TI `hello.s`, the
Xlib call sequence, `as34`, `dis34`, SDKs, manuals). It is not vendored:
```sh
./scripts/fetch-vt1200.sh # -> vendor/vt1200, pinned 17dfd24...
```
## Build the compiler
### Compiler fork (M4, the normal path)
The fork checkout already exists at `build/llvm-clean` on `m4`. Build it in
place:
```sh
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m4 ./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
```
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=m4 ./scripts/build-llvm.sh
```
Fork mode **never** checks out, resets, cleans or patches the source tree; it
only runs CMake and Ninja. Local commits on the branch are safe. Use
`LLVM_BUILD=...` to keep a separate build directory. It builds `llc`, clang,
opt, `llvm-config`, FileCheck and the test helpers with X86 (host) and
TMS34010; a first build takes roughly 20–40 minutes.
### Upstream + patches (frozen M0 path)
```sh
./scripts/build-llvm.sh
```
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 M2/M3/M4 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/M2/M3/M4.
## Build the M0 support tools
The M2/M3/M4 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 M2/M3/M4):
```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/tests/m0/run.sh
# -> M0 pipeline: PASS
```
## 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/add2.ll scripts/tests/m2/diff/ti-add2.c
build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm \
/tmp/add2.ll -o /tmp/add2.s
```
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.
## Later milestones
M5 adds the native COFF path (`MCCodeEmitter`/object writer), floats,
`long long`, general varargs (`va_list`), bitfields, structs by value and the
`JR`/branch-relaxation code-size pass, plus MMTM/MMFM register-list saves and
deeper libc/runtime coverage. 2ine r5.01 is a later speed-up for the
GSPA/GSPLNK step.