520 lines
25 KiB
Markdown
520 lines
25 KiB
Markdown
# tms-c-compiler
|
||
|
||
A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of
|
||
upstream LLVM.
|
||
|
||
**Status: M3.** `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). M3 adds conditional/unconditional branches,
|
||
ordinary `while`/`for` loops, integer ALU/shift operations, and the first
|
||
compiled C Xlib client: a four-call window opened with `XOpenDisplay`, created with
|
||
`XCreateSimpleWindow`, mapped with `XMapWindow` and synchronized with `XSync`.
|
||
It is still **assembly-only**: no object writer, no `clang -c`, and no native
|
||
COFF; GSPA/GSPLNK under DOSBox produce the COFF. XImage/XEvent, the real client,
|
||
structs by value, function pointers, floats and `i64` remain M4/M5.
|
||
|
||
- 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)
|
||
- M3 plan: [`docs/superpowers/plans/2026-11-11-tms34010-m3-control-flow-and-xlib-window.md`](docs/superpowers/plans/2026-11-11-tms34010-m3-control-flow-and-xlib-window.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 `m3`) — the full buildable LLVM 23.1.5 tree with the TMS34010
|
||
target, clang bit-address changes and the M2/M3 backend. Branch `m2` (the
|
||
repository default) is the M2 snapshot; 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 `m3`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`,
|
||
`tms-clang`, symbols, the M2/M3 harnesses and the spike. Branch `m2` (the
|
||
repository default) is the M2 snapshot and 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/M3
|
||
do not change it.
|
||
|
||
## What M3 provides
|
||
|
||
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, for a total of 46 with the M2 suite; the M3
|
||
end-to-end, dis34, layout and differential gates (below).
|
||
|
||
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/M5 must add
|
||
multiply/`i64` lowering, a target-level pass policy, or a target diagnostic
|
||
instead of a raw abort.
|
||
|
||
## 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,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/M3) |
|
||
|---|---|---|
|
||
| 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 M3 gates
|
||
|
||
These are the documented commands on branch `m3`; all must pass.
|
||
|
||
```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: 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 (default branch)
|
||
|
||
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.
|
||
|
||
```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`. 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 (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 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`,
|
||
`byval`, bitfields (M4/M5). 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 (M5).
|
||
- **Object output:** still no `MCCodeEmitter`, object writer,
|
||
`llc -filetype=obj`, `clang -c` or native COFF; GSPA/GSPLNK produce the COFF.
|
||
The `JR`/branch-relaxation code-size pass is M5 as well.
|
||
- **Varargs, `musttail`, narrow ABI parameters/returns:** unchanged from M2.
|
||
|
||
## M4 prerequisites (carry-forwards)
|
||
|
||
The final M3 review left these concrete prerequisites for M4:
|
||
|
||
- **Add zero-extending narrow loads.** M3 rejects `unsigned char`,
|
||
`unsigned short` and `_Bool` loads (pinned by `m3-reject-zext.ll`); M4 must
|
||
add the `ZEXTLOAD i8`/`i16` patterns and replace the reject test.
|
||
- **Namespace every private label.** `scripts/tests/m3/run.sh` prefixes
|
||
`LBB<n>_<m>` per translation unit, but deterministic private string labels
|
||
are not renamed. Two TUs with literals therefore collide in GSPA's flat
|
||
symbol table. M4 must namespace all private labels or make compiler private
|
||
names module-unique.
|
||
- **Add unsigned/`<=` runtime coverage.** `JALE`, `JALO`, `JAHI` and `JALS`,
|
||
`JAHS`, `JANE`, `JAGE` are covered by lit tests only and do not appear in the
|
||
compiled M3 image. M4 should add an unsigned/`<=` runtime marker.
|
||
- **Replace fragile test plumbing.** The `always_inline` root helper and the
|
||
register-hardcoded nine-argument dis34 check are test-strength aids that will
|
||
not survive real M4 code; make them operand- and register-independent.
|
||
- **Tighten test strength.** `m3-cmpi`'s `CHECK-DAG` for 0/1 does not pin their
|
||
order, and `layout.sh` pairs the root probe with fixed constants; neither
|
||
weakens the current pass, but both should be tightened.
|
||
|
||
## Repository layout
|
||
|
||
```text
|
||
build/llvm-clean/ compiler fork checkout used for M2/M3 (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/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/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/M3 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 (M3, the normal path)
|
||
|
||
The fork checkout already exists at `build/llvm-clean` on `m3`. Build it in
|
||
place:
|
||
|
||
```sh
|
||
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m3 ./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=m3 ./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/M3 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/M3.
|
||
|
||
## Build the M0 support tools
|
||
|
||
The M2/M3 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/M3):
|
||
|
||
```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
|
||
|
||
M4 adds the real Xlib client (`XImage`/`XEvent`, the full X11R4 headers),
|
||
structs by value and function pointers. M5 adds the native COFF path, floats,
|
||
varargs, bitfields, `long long` and the `JR`/branch-relaxation code-size pass;
|
||
2ine r5.01 is a later speed-up for the GSPA/GSPLNK step.
|