Files
tms-c-compiler/README.md
T

341 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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+.
- 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)
- 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:** <https://git.wut.ee/opencode.siil.arti.ee/llvm-tms34010>
(branch `m2`) — the full buildable LLVM 23.1.5 tree with the TMS34010
target, clang bit-address changes and the M2 backend. 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 `m2`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`,
`tms-clang`, symbols, the M2 harness and the spike. 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
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 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) |
|---|---|---|
| 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 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.
```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+<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:
```asm
.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).
- **Tail calls:** ordinary `tail` markers are demoted to ordinary calls;
`musttail` is not implemented (M2 C does not emit it). Real tail calls are
M3.
- **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
```text
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; 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 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 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 (M2, the normal path)
The fork checkout already exists at `build/llvm-clean` on `m2`. Build it in
place:
```sh
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:
```sh
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)
```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 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`:
```sh
LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \
tools/tms-bitaddr/build.sh
```
M0 regression test (optional in M2):
```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
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.