# tms-c-compiler A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of upstream LLVM. **Status: M1.** `llc -march=tms34010` emits GSPA-compatible assembly for a small set of leaf functions, and that assembly assembles, links and runs in MAME. M1 is an **assembly-only, test-only** snapshot: it uses an A0–A3/A0 register convention, not the TI ABI, and has no object writer, calls or runtime. - 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) - 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 `tms34010`) — the full buildable LLVM 23.1.5 tree with the TMS34010 target, clang bit-address changes and the M1 backend. - **This project:** — plans/specs, VT1200 integration tooling, `tms-bitaddr`, `tms-clang`, symbols, the M1 harness and the spike. The compiler fork is the canonical source for all target code. `patches/llvm` here is a **frozen M0 mirror** used by the default upstream+patches build; M1 does not change it. ## What M1 provides M1 fills in the M0 target skeleton and stops at assembly: - `llc -march=tms34010 -filetype=asm` for leaf 32-bit functions: integer/pointer arguments in `A0`–`A3`, result in `A0`, return via `RETS 0`. - `ADD` for `i32`, `MOVI` for constants, `MOVE ..., 1` (FS1) for 32-bit load/store, `MOVE ..., 0` (FS0) for `i16`, and sign-extending `MOVB` for `i8` load/store. - Constant global addresses as `MOVI _sym+bits`; a surviving GEP offset is a bit offset (`ADDI 24`, not `ADDI 192`). - Hard errors, not silent fallbacks, for calls (`M1 has no calls; M2 adds the TI ABI`), stack frames (`M1 has no stack frame`), more than four register arguments, unsupported signatures (varargs, `i64`/float/aggregate parameters, `sret`/`byval`/`inreg`/`nest`/`returned`), unsupported return types, and initialized globals or string literals. - Seventeen `llvm-lit`/FileCheck tests under `llvm/test/CodeGen/TMS34010/m1-*.ll` in the compiler fork, including negative tests that pin the loud refusals. - An end-to-end path: `scripts/tms-clang` IR → `llc -O0 -filetype=asm` → **1987 GSPA/GSPLNK 2.00 under DOSBox** → COFF → the VT1200 loader in MAME. The run checks `add(20, 22) == 42`, `load(&word) == 0x12345678`, `second() == &arr[1]` (a `symbol+32` bit relocation), `loadb` of a byte `0x81` returns the sign-extended `0xFFFFFF81`, and `storeb` leaves `0x81` in memory. - A `dis34` round-trip that reads the linked `.text` from the COFF and checks the instructions GSPA encoded at their linked bit addresses. ## M1 is not the TI ABI | | M1 test convention | TI ABI (M2) | |---|---|---| | Integer/pointer arguments | `A0`–`A3`, four maximum | program stack (via `A14` STK) | | Return value | `A0` | `A8` | | Frame | none; leaf functions only | `A13` FP, `A14` STK, `A15` system stack | | Return instruction | `RETS 0` | `RETS 2` | | Calls, callee-saves, stack args | rejected | required | | Field select | harness sets FS1/FS0 before the leaf | `FS1 = 32` preserved across calls | M1 assembly is not ABI-compatible with TI's compiler or the ROM Xlib. M2 replaces the convention; do not build on the M1 register assignment. ## Repository layout ```text build/llvm-clean/ compiler fork checkout used for M1 (ignored) build/llvm-clean-build/ its Ninja build tree (ignored) build/llvm-src/ M0 working fork (ignored) patches/llvm/ frozen M0 patch series against llvmorg-23.1.5 tools/tms-bitaddr/ GEP scale/normalize tool scripts/tms-clang IR pipeline wrapper (clang + tms-bitaddr + opt) scripts/m1/ M1 harness: C inputs, entry.asm, link.cmd, checks scripts/tests/m1/ M1 gates (run.sh, dis-roundtrip.sh) scripts/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 M1 gates additionally need `dosbox`, `mtools` (`mcopy`), the VT1200 SDK image, the `dwt` COFF loader, and a built MAME `vt1200` binary (`build/mame-src/vt1200`). On Debian: ```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 (M1, the normal path) The fork checkout already exists at `build/llvm-clean` on `tms34010`. Build it in place: ```sh LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=tms34010 ./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=tms34010 ./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 M1 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. ## Build the M0 support tools The M1 pipeline still uses `tms-bitaddr` and `tms-clang`: ```sh LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \ tools/tms-bitaddr/build.sh ``` M0 regression test (optional in M1): ```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 ``` ## Run the M1 gates These are the documented commands; all three must pass: ```sh # 1. LLVM codegen tests: 17 passed build/llvm-clean-build/bin/llvm-lit -v \ build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m1-' # 2. Compile, assemble, link and run in MAME ./scripts/tests/m1/run.sh # -> M1 MAME: PASS # 3. Rebuild the COFF and check the GSPA encodings with dis34 ./scripts/tests/m1/dis-roundtrip.sh # -> M1 dis34 round-trip: PASS ``` `run.sh` compiles five C inputs with `scripts/tms-clang -O2`, runs `llc -march=tms34010 -O0 -filetype=asm` on the normalized IR, concatenates the result with `scripts/m1/entry.asm` and `scripts/m1/char-test.asm`, assembles/links with GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF in MAME. `dis-roundtrip.sh` runs the same build and then disassembles the linked `.text` at the symbol addresses from the COFF symbol table. Paths are overridable with `TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`, `MAME`, `DOSBOX`, `SDK_IMG`, `M1_WORK`, `M1_COFF` and `M1_SDK`. ## Emit assembly by hand ```sh TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \ TMS_OPT=$PWD/build/llvm-clean-build/bin/opt \ TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \ scripts/tms-clang -O2 -o /tmp/add.ll scripts/m1/add.c build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm \ /tmp/add.ll -o /tmp/add.s ``` Always produce the IR with `scripts/tms-clang` (or run `tms-bitaddr --scale`) before `llc`: raw clang IR carries byte GEP offsets, but `llc` treats every surviving GEP offset as a *bit* offset, so addresses would be 8× too small. Run `llc -O0` on the normalized IR that `tms-clang` already optimized and scaled. `tms-bitaddr --normalize` consumes that scaled IR and now rejects a typed (non-`i8`) or vector GEP instead of emitting its byte offset as bits. The lit tests use `llc -march=tms34010 -O2 -filetype=asm` on hand-written IR to exercise the selector pipeline directly. Output is assembly only: `llc` cannot emit an object file or a usable COFF for this target. ## Not yet supported (M2 and later) - **Data:** only zero-initialized mutable globals (`.bss`) are supported. Initialized globals, string literals and constant/private data (`.data`, `.rodata`) make `llc` fail loudly; the `.data` initializer probe moves to M2. - **Object emission:** no `MCCodeEmitter`, object writer, `llc -filetype=obj`, `clang -c`, or native COFF. GSPA assembles and GSPLNK links; the existing COFF loader loads. - **TI ABI:** `A8` return, `A13`/`A14`/`A15` dual stack, `RETS 2`, `FS1 = 32` across calls, callee-saves, stack arguments, more than four arguments. - **C features:** calls, locals/`alloca`, structures/`byval`, floats, varargs, bitfields, 64-bit arithmetic, `memcpy`/`alloca` lowering. - **Runtime and clients:** ROM/Xlib imports, `strlen`/`malloc`, X11 headers, real VT1200 clients, debug info. - **2ine:** M1 uses the proven 1987 GSPA/GSPLNK under DOSBox, not the external 2ine r5.01 tools; 2ine is an M2 speed-up. These are M2–M5 in the design spec, each with its own plan.