# 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:** (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:** (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+` (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.