tms-c-compiler

A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of upstream LLVM.

Status: M5. llc -march=tms34010 -filetype=obj now emits TI-COFF objects that GSPLNK 2.00 links and the existing VT1200 loader runs; the M4 -filetype=asm → GSPA/GSPLNK path remains the oracle and fallback. The TI ABI is unchanged: 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). M5 adds the native object path, the full MC encoder/assembler/disassembler, JR/branch relaxation, MMTM/MMFM register-list saves, dynamic alloca, byval, narrow returns, target memcpy/memset, the switch-table GEP fix that removes the -switch-to-lookup=false driver workaround, long private-name capping, and the first pinned external conformance tier (LLVM CodeGen/Generic, GCC 16.2 c-torture and c-testsuite). Floats/double, long long, general va_list varargs, bitfields, aggregate returns/sret, debug info and the full external suites are M6.

Branch m5 is the current milestone snapshot in both repositories; the default branch is m4 until the controller/user updates it after the final review. Branch m4 remains the previous snapshot, m3 the M3 snapshot, m2 the M2 tip and m1 the reproducible M1 snapshot.

Repositories

  • Compiler fork: https://git.wut.ee/opencode.siil.arti.ee/llvm-tms34010 (branch m5) — the full buildable LLVM 23.1.5 tree with the TMS34010 target, clang bit-address changes and the M2/M3/M4/M5 backend. Branch m4 (the repository default) is the previous snapshot; branch m3 is before that, 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 m5) — plans/specs, VT1200 integration tooling, tms-bitaddr, tms-clang, symbols, the M2/M3/M4/M5 harnesses and the conformance tier. Branch m4 (the repository default) is the previous snapshot; branch m3 is before that, m2 is the M2 snapshot and 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–M5 do not change it.

What M5 provides

M5 adds the native object path and the first external conformance tier while keeping the M4 assembly path as the oracle:

  • Native TI-COFF objects. llc -march=tms34010 -filetype=obj writes a relocatable TI-COFF object with .text/.data/.bss, three section symbols with GSPA's 18-byte aux records, C_EXT globals and undefined externals, and 10-byte R-RELLONG (0x11) relocations whose addends are bit addresses. GSPLNK 2.00 links the object (same-section 0xFFFF, cross-section section symbols, and section-0 externals), and the existing dwt loader runs the linked image. Sizes in the object are bits, as in GSPA; .data and .bss are word-granular because the loader requires whole 16-bit words. There is no new linker and no new object format: the M4 asm path is unchanged and is still the default in scripts/m4/run.sh. scripts/m5/native.sh and scripts/tests/m5/native.sh build xshow and the six-TU termtris through the object path; the native and asm linked .text decode to the same instruction streams (753 xshow and 6,356 termtris instructions), and the native termtris links .text 166,608 bits (20,826 bytes), .data 9,536 bits (1,192 bytes) and .bss 121,808 bits with loader status 0.
  • Full MC layer. TMS34010MCCodeEmitter, MCAsmBackend, the TI-COFF MCObjectWriter/MCStreamer, an MCAsmParser and an MCDisassembler cover the supported instruction table. llvm-mc -triple=tms34010 assembles/disassembles it; encodings.s pins 126 -show-encoding cases, disassembler.s 77 inverse checks, and roundtrip.s 88 instructions round-trip through the 1987 dis34. The expected bytes come from GSPA listings, not from the new encoder. llvm-mc -filetype=obj uses LLVM's generic ELF writer; only llc -filetype=obj produces TI-COFF. The 10-byte MOVE @a,@b/MOVB @a,@b forms are not modeled and fail loudly.
  • JR and branch relaxation. Selection still builds the three-word absolute JAUC/JA<cc> branches; a pre-emit TMS34010BranchRelaxation pass then assumes the one-word relative JR/JR<cc> form and grows a branch back to JA* when its signed 8-bit word displacement is out of range (±127 words = ±2032 bits), zero, odd, or its target is unknown. The MC backend independently relaxes a symbolic JR with the FK_JR8 fixup, so llvm-mc input relaxes too. Relative to the M4 938-byte M3 image, 14 JR branches save 56 bytes and the MMTM/MMFM change saves another 8, giving 874 (the current all-JA baseline is 930).
  • MMTM/MMFM register-list saves. The prologue saves the callee-saved set with one MMTM SP, ... and the epilogue restores it with one MMFM SP, ... (ascending register list; MMTM mask 0xd400 / MMFM mask 0x002b for A0,A1,A3,A5). The old-STK slot, A13 save, A8 return and RETS 2 arithmetic are unchanged. For a list of n saved registers the old per-register sequence cost 4n bytes and the new pair costs 8: n=1 grows by 4 bytes, n=2 breaks even, n=3 saves 4 and n=4 saves 8.
  • alloca, byval, narrow returns and memcpy/memset. Dynamic alloca adjusts A14 in bits (size << 3), honors the requested alignment in bits, and supports stacksave/stackrestore; a no-argument function with a VLA rebuilds A14 from FP in the epilogue. byval copies the object to the program stack and advances A14 by alignTo(size,2) bytes (×8 bits). i8/i16 returns leave A8 with the declared zeroext/signext and the caller truncates/extends. Constant-size memcpy/memset become word/byte copies and runtime sizes become inline loops with bit-scaled GEPs. A constant memmove (including sizes <= 64 bytes, which PreISel leaves alone) is expanded by a target pass into the same overlap-safe forward/backward loops; no _memcpy/_memset/_memmove libcall is emitted. robust.sh checks alloca=408 byval=64 narrow=1056 mem=438 alloca_use=1496 byval6=64 memmove=2148 in both the asm and native paths.
  • Driver policies. -switch-to-lookup=false is gone. tms-bitaddr recognizes SimplifyCFG's switch.table.* GEP and shifts its byte/element index by 3, so the M4 switch and termtris lookups use bit-indexed tables under a plain tms-clang -O2. -replexitval=never remains a documented tms-clang TMS34010 policy until i64/M6: llc now rejects a raw-pipeline i33 multiply with an actionable diagnostic (and keeps the 64-bit multiply diagnostic separate), and tms-clang -O2 rejects a static variadic definition before dead-argument elimination can drop its .... -O0 and the external sprintf still work.
  • Private-label cap. GSPA keeps only 31 identifier characters. M5 keeps the _<8 hex> module token, caps the base at 22 characters, and folds a longer base into a second 8-hex token before appending the module token, so two TUs with the same long static name still link. Public names longer than 31 characters are still GSPA-truncated.
  • Conformance fixes. The tier exposed and fixed several real defects: SETF16 must be sign-extending (SETF 16, 1, 0, 0x0570; fe=0 would zero-extend), a PHI copy can land between CMP and Jcc (MOVE writes the flags), so selection now emits a fused CMPBR/CMPIBR that is split after register allocation, and gc.statepoint calls get a target diagnostic instead of a StatepointLowering assertion. Object-mode .bss word granularity and the object SRAK/SRLK encoding were fixed as well.
  • M3 tightening. m3-cmpi.ll pins the -O0/-O2 MOVI order with CHECK/CHECK-NEXT, and scripts/m3/dis-roundtrip.sh parses the nine-argument XCreateSimpleWindow pushes by value/register instead of hardcoding the registers.
  • Conformance tier. scripts/tests/m5/conformance/run.sh compiles a test with tms-clang, links the shim.c + entry.asm harness (native TI-COFF first, asm/GSPA fallback for the exploratory runner), runs it in MAME through the pinned loader, and compares a status/output block (01B00000/01B10000). The c-testsuite tier forces --mode native, so an asm fallback is a gate failure. classify.sh rejects unsupported features with a reason; a skip is never a pass. The corpus is pinned to GCC 16.2.0 (gcc-16.2.0.tar.xz, SHA-256 e6738e29597f733270731aa90600f37ffdc045079dfc27ec7e8192cc81085c3e) and c-testsuite commit 5c7275656d751de0e68b2d340a95b5681858ed07, materialized under the ignored vendor/conformance (test-only; GCC is GPL and c-testsuite keeps its own per-test licenses; neither is distributed). The GCC version is only the c-torture corpus source; the compiler under test is tms-clang. The three gates are LLVM CodeGen/Generic (197 .ll: 107 pass / 90 unsupported / 0 crash, plus a 24-file curated allowlist), GCC c-torture execute (59/59 at -O0 and -O2; the corpus classifier reports 1,112 supported / 580 skipped from 1,692 tests), and c-testsuite single-exec (29 pass / 191 skip / 220, with exact .c.expected output). The shim provides printf/putchar/puts through the M4 contiguous &fmt walk (no va_list), exit/abort, a 16 KiB stdout buffer and a small string/integer surface. stderr is not captured, so stderr-writing tests are skipped rather than passed. On the reference machine the Generic gate takes about 6–7 minutes, c-torture about 16–17 minutes and c-testsuite about 4–5 minutes; the serial MAME boot dominates.

What M4 provides (branch m4, the previous snapshot)

M4 closed the remaining Xlib path to a real image/event client and a full-game integration, still assembly-only on that branch. M5 keeps this as the regression baseline and adds the native object path beside it.

  • 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. M5 caps a long private base so the module token survives GSPA's 31-character limit; see "What M5 provides".
  • i8/i16 fixed arguments (M4; M5 adds narrow returns). 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 are rejected on m4; M5 adds i8/i16 returns in A8. m4-arg8.ll, m5-narrow-ret.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. On m4, static variadic definitions can lose ... under -O2 dead-argument elimination; M5 adds the tms-clang diagnostic and the llc check, while the external termtris sprintf remains supported. m4-varargs.ll, m4-reject-vararg0.ll, m4-reject-vararg-narrow{,16}.ll, scripts/tests/m4/varargs.sh, m5-reject-static-vararg.ll.
  • Driver policies (M4 branch). scripts/tms-clang passes -replexitval=never (the i33 exit-value multiply is unselectable on this i64-less target); M5 keeps it as a documented policy and adds the llc diagnostic for the raw pipeline. M4 also passed -switch-to-lookup=false (a late byte-offset GEP after --scale would be read as a bit offset, an 8x address bug); M5 removes that flag by fixing the switch-table GEP in tms-bitaddr. Both policies are driver-side 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 source columns (0–627) to window x = 12–639, so the image's rightmost 12 source columns (628–639, the final 12 px of the last 32-px cell) are never displayed; that last cell is visible only as its left 20 px. The 12-pixel prefix at window x = 0–11 is filled with the preceding cell's colour, so the checker phase is shifted by 12 px inside the (20,20) window while the cells remain 32 px. 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,1,0, SETF 32,0,1).
  • A callee starts with SETF 16,1,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–M5)
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, fe = 1 (sign-extending) 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 with the field-extension bit set (SETF 16,1,0, sign-extending) and FS1 must be 32 at function entry and immediately after every CALLA (SETF 16,1,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 M5 gates

These are the documented commands on branch m5; all must pass. The MAME and conformance gates are slow, share the M4 SDK image, and must run serially (no two gates at once).

# 1. Build llc and the assembler/disassembler driver
ninja -C build/llvm-clean-build llc llvm-mc

# 2. LLVM codegen tests: 76 passed (35 m2-* + 8 m3-* + 18 m4-* + 15 m5-*)
build/llvm-clean-build/bin/llvm-lit -v \
  build/llvm-clean/llvm/test/CodeGen/TMS34010 --filter='(m2|m3|m4|m5)-'

# 3. MC tests: 4 passed (encodings, disassembler, round-trip, JR range)
build/llvm-clean-build/bin/llvm-lit -v build/llvm-clean/llvm/test/MC/TMS34010

# 4. Native object, link and MC round-trip
scripts/tests/m5/object-smoke.sh      # native .text == GSPA .text
scripts/tests/m5/native-link.sh       # GSPLNK link + loader/MAME markers
scripts/tests/m5/mc-roundtrip.sh      # 88 instructions through dis34

# 5. The real M4 clients through the native object path
scripts/tests/m5/native.sh
# -> native xshow markers + termtris board/move/panels/quit + dis34 compare
scripts/tests/m5/jr.sh                 # JR/relaxation + M3 MAME
scripts/tests/m5/mmtm.sh               # MMTM/MMFM + M2/M3 MAME
scripts/tests/m5/robust.sh             # alloca/byval/narrow/mem, both paths

# 6. Driver policy, private names and the conformance harness
scripts/tests/m5/policy.sh
scripts/tests/m5/private-long.sh
scripts/tests/m5/conformance/selftest.sh

# 7. The conformance tiers
scripts/tests/m5/generic.sh            # Generic allowlist + no-crash sweep
scripts/tests/m5/c-torture.sh          # 59 tests at -O0 and -O2
scripts/tests/m5/c-testsuite.sh        # 29 output tests + 191 reported skips

# 8. M4, M3 and M2 regressions and the GEP tool
scripts/tests/m4/run.sh
scripts/tests/m4/dis-roundtrip.sh
scripts/tests/m4/layout.sh
scripts/tests/m4/symbols.sh
scripts/tests/m4/termtris.sh
scripts/tests/m4/diff/check.sh
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
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
scripts/tests/m3/layout.sh
scripts/tests/m3/diff/check.sh
scripts/tests/m3/diff/negative-control.sh
scripts/tests/m2/run.sh
scripts/tests/m2/dis-roundtrip.sh
scripts/tests/m2/diff/check.sh
tools/tms-bitaddr/tests/run.sh

scripts/fetch-conformance.sh materializes the pinned corpora under the ignored vendor/conformance (GCC 16.2.0 gcc.c-torture/execute and c-testsuite single-exec; vendor/conformance/PINS records the pins). The gate log prints each tier's counts; skipped tests carry a feature reason and are never counted as passes. The conformance tiers run serially because M4_SDK is shared; the MAME boot dominates the runtime.

Run the M4 gates (branch m4; also green on m5)

These are the documented commands on branch m4; all must pass. On branch m5, use the M5 list above (it includes llvm-mc and the M5 gates); the M4 commands below remain the historical regression set and use the m4 test counts. The MAME gates are slow and are run once.

# 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.

# 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.

# 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:

	.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 (M6).
  • Aggregates: structs/unions by value, sret, byval, bitfields (M5 adds byval; the rest are M6).
  • Varargs: ... functions and calls (M4 adds the contiguous form; general va_list is M6).
  • 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, bitfields (M5 adds byval; the rest are M6). 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 (M6).
  • Object output: still no MCCodeEmitter, object writer, llc -filetype=obj, clang -c or native COFF; GSPA/GSPLNK produce the COFF. M5 adds the native path and the JR/branch-relaxation pass.
  • 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. M5 completes the remaining M3 test-hardening carry-forwards: m3-cmpi.ll now pins the -O0/-O2 MOVI order with CHECK/CHECK-NEXT, and scripts/m3/dis-roundtrip.sh parses the nine XCreateSimpleWindow push values/registers instead of hardcoding them. The M3 MAME values are unchanged.

M5 non-goals and the M6 boundary

M5 closes the M4 gaps for the native object path, JR/relaxation, MMTM/MMFM, narrow returns, byval and the switch-table policy. It deliberately stops before:

  • Floating point and long long: no float/double or i64 arguments, returns, arithmetic or branches; the i33/-replexitval policy stays until the i64 lowering lands.
  • General varargs: no va_list, va_start or va_arg; only the contiguous &last_fixed pattern used by termtris sprintf. M5 uses that same pattern inside the conformance shim; true va_list tests are skipped.
  • Bitfields and aggregates by value: bitfields, sret, aggregate returns and general aggregate arguments remain unsupported; byval arguments and struct fields through pointers are supported.
  • Debug info and the wider Xlib surface: no debug info, and the client uses only 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.
  • Full external suites and stderr: the three tiers are curated subsets, not the full GCC/LLVM suites. The conformance shim captures stdout only, so tests that write stderr are skipped; stderr capture is M6.
  • Deeper libc/runtime coverage: the conformance shim is freestanding and does not add RTS.LIB entries. Broader libc/runtime integration is M6.
  • Native .cinit and public long names: the object writer accepts only .text/.data/.bss (termtris's .cinit is GSPA assembly and fails loudly if a C TU tries to emit it), and public identifiers longer than GSPA's 31 characters are still truncated (only private names are capped).

The pinned GCC and c-testsuite trees are test-only and are not part of the compiler or project distribution (GCC is GPL; c-testsuite keeps its own per-test licenses).

Repository layout

build/llvm-clean/        compiler fork checkout used for M2–M5 (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/m5/              M5 harness: native entry/link files and long-private-name inputs
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/tests/m5/        M5 gates (object/link/MC/native/JR/MMTM/robust/policy/private)
scripts/tests/m5/conformance/
                         conformance runner, shim, entry, classifier, pins, manifests
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–M5 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 conformance tiers need the pinned vendor/conformance corpora (test-only; see scripts/fetch-conformance.sh). 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:

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:

./scripts/fetch-vt1200.sh          # -> vendor/vt1200, pinned 17dfd24...

Build the compiler

Compiler fork (M5, the normal path)

The fork checkout already exists at build/llvm-clean on m5. Build it in place:

LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m5 ./scripts/build-llvm.sh
# binaries: build/llvm-clean-build/bin/{llc,llvm-mc,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:

LLVM_REPO=gitea@git.wut.ee:opencode.siil.arti.ee/llvm-tms34010.git \
LLVM_BRANCH=m5 ./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, llvm-mc, 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)

./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–M5 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–M5.

Build the M0 support tools

The M2–M5 pipeline uses tms-bitaddr and tms-clang:

LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \
  tools/tms-bitaddr/build.sh

M0 regression test (optional in M2–M5):

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 or a native object by hand

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
build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=obj \
  /tmp/add2.ll -o /tmp/add2.o        # TI-COFF, link with GSPLNK

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 switch.table.*) 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. -filetype=obj writes a TI-COFF object that GSPLNK 2.00 links; the existing loader runs the linked image. llvm-mc -triple=tms34010 assembles and disassembles the same instruction table.

Later milestones

M6 adds floats/double, long long/i64 (and removes the i33 -replexitval=never workaround), general varargs (va_list), bitfields, structs by value, sret, aggregate returns, debug info, the wider Xlib surface, stderr capture in the conformance harness, and the full external suites. 2ine r5.01 is a later speed-up for the GSPA/GSPLNK step.

S
Description
TMS34010/VT1200 compiler project: plans, integration tooling, spike, symbols
Readme
1,020 KiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 59.2%
C 18%
Lua 9.7%
C++ 7.2%
Assembly 5.1%
Other 0.8%