Files
tms-c-compiler/README.md
T
arti eacaab68f6 docs: correct the GEP scaling warning and M1 data limits
Raw clang IR has byte GEP offsets that llc treats as bits, so addresses
are 8x too small; always run tms-clang/tms-bitaddr --scale first.
Document that M1 supports only zero-initialized globals and rejects
.data/string literals loudly, and defer the spec's .data initializer
probe to M2.
2026-11-11 10:15:32 +00:00

10 KiB
Raw Blame History

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.

Repositories

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

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:

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 (M1, the normal path)

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

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:

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)

./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:

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

M0 regression test (optional in M1):

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:

# 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

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.