tms-c-compiler

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

Status: M2. 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). M2 can call a ROM entry point (strlen) and emit initialized .data and string literals. It is still assembly-only: no object writer, no clang -c, and no native COFF; GSPA/GSPLNK under DOSBox produce the COFF. Control flow, floats, structs, varargs and function pointers remain M3+.

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; M2 does not change it.

What M2 provides

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 widened to 32 bits), emits CALLA, then re-establishes FS1.
  • A callee 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-three llvm-lit/FileCheck tests under llvm/test/CodeGen/TMS34010/m2-*.ll, including negative tests that pin the loud refusals.
  • 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)
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 FS1 = 32 (32-bit words), 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. FS1 must be 32 at function entry and immediately after every CALLA (SETF 32,0,1), because ROM code may change the field-size state.

Run the M2 gates

These are the documented commands; all must 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: 33 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. Private/local symbol names are sanitized to GSPA-safe names (_HH for non-alphanumerics). Unsupported initializer shapes 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; later milestones extend and validate the table and add the Xlib call sequence. Do not add Xlib dependencies to M2.

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).
  • Narrow ABI types: i8/i16 parameters, i64, and narrow returns; M3 adds the widen/truncate rules. Narrow loads/stores inside a function remain supported.
  • 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.

Repository layout

build/llvm-clean/        compiler fork checkout used for M2 (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/tests/m2/        M2 gates (run.sh, dis-roundtrip.sh)
scripts/tests/m2/diff/   TI golden asm and the differential ABI check
scripts/m1/              M1 harness (historical; branch m1, not an M2 gate)
scripts/tests/m1/        M1 gates (historical; 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 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 differential golden check needs only the built compiler. 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 (M2, the normal path)

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

LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m2 ./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=m2 ./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 M2 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.

Build the M0 support tools

The M2 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):

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

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

M3 adds branches/control flow, the Xlib call sequence and a wider validated ROM symbol table. M4 adds the real client plus structs and function pointers. M5 adds the native COFF path, floats, varargs, bitfields and long long; 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
739 KiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 61.1%
C++ 15%
C 13.3%
Assembly 4.9%
Lua 4.1%
Other 1.6%