Files
arti 39512c272d
require-team-membership/action.yml (push) Invalid workflow file
unprivileged-download-artifact/action.yml (push) Invalid workflow file
upload-release-artifact/action.yml (push) Invalid workflow file
validate-release-version/action.yml (push) Invalid workflow file
get-llvm-version/action.yml (push) Invalid workflow file
tms34010: fix M2 data widths and FS0 after calls
2026-11-11 17:36:15 +00:00

9.9 KiB
Raw Permalink Blame History

LLVM 23.1.5 fork for TMS34010

Branch m2 is llvmorg-23.1.5 plus the TMS34010 target and clang bit-address changes (M0), a minimal TMS34010 backend (M1) and the TI ABI, calls and .data support (M2). This branch is the canonical source for the target; the project's patches/llvm mirror is frozen at M0. Branch tms34010 keeps the M1 backend and its m1-* suite.

M2 state

M2 is an assembly-only milestone. It emits GSPA-compatible assembly with llc -march=tms34010 -filetype=asm; it has no MCCodeEmitter, object writer, llc -filetype=obj or clang -c, and no native COFF. The 1987 GSPA assembler and GSPLNK linker produce the COFF that the VT1200 loader runs in MAME.

The calling convention is the TI ABI:

  • Integer/pointer result in A8.
  • A13 is FP, A14 is STK (the program stack), A15 is SP (the system stack). A8 and A13–A15 are reserved; the allocatable set is A0–A7 and A9–A12.
  • Caller: if it passes arguments, save the old STK on the system stack with MOVE A14, -*SP, 1; push arguments on the program stack in reverse order (MOVE <arg>, *A14+, 1), each already a 32-bit slot; CALLA; then re-establish both field sizes with SETF 16, 0, 0 and SETF 32, 0, 1.
  • Callee: set SETF 16, 0, 0 and SETF 32, 0, 1; save the old FP and each used callee-saved register on the system stack; FP = STK; allocate the frame with ADDI <bits>, STK. The first explicit argument is at FP(-32), the second at FP(-64), and so on; locals are at positive FP offsets.
  • Epilogue: when the function has explicit arguments, restore STK from the caller's saved slot with MOVE *SP((n+1)*32), A14, 1; a no-argument function instead releases its own frame with ADDI -<frame_bits>, A14. Then pop the saved registers in reverse order, and RETS 2 when the function has explicit arguments or RETS 0 otherwise. After n system-stack pushes the caller's saved STK is at SP + (n+1)*32 (the formula applies only when arguments were passed, i.e. the caller saved STK); RETS N pops the return PC and advances SP by 32 + 16N.
  • Callee-saved registers are A0–A7 and A9–A12. A value live across a call must survive in one of them (or a spill slot), because a call clobbers all other state except the reserved registers.
  • FS0 must be 16 (for short accesses) and FS1 must be 32 (for 32-bit words) at function entry and after every call. The prologue emits SETF 16, 0, 0 then SETF 32, 0, 1; LowerCall emits the same pair immediately after each CALLA.

Supported signatures are i32 and pointer arguments, and void, i32 or pointer returns. Narrow (i8/i16) parameters, i64, floats/aggregates, varargs, sret/byval/inreg/nest and indirect calls fail loudly with an M2 ... diagnostic. Ordinary tail markers are demoted to ordinary calls (LowerCall clears IsTailCall); musttail is not implemented and M2 C does not emit it. Real tail calls are M3. Narrow loads/stores inside a function stay supported (MOVB, FS0 MOVE). The optimizer's returned parameter attribute is accepted: it is only an optimization hint and does not change the stack-slot ABI. m2-returned.ll pins that; m2-reject-sret.ll keeps the rejection of the ABI-affecting attributes.

Implemented selection and printing

  • ADD for i32; MOVI for constants; SETF 16, 0, 0 and SETF 32, 0, 1; RETS N.
  • MOVE ..., 1 (FS1) for 32-bit load/store, MOVE ..., 0 (FS0) for i16, sign-extending MOVB for i8.
  • Argument/return lowering with PUSH32, POP32, PUSHARG, SETFP, ADDSTK, MOVEFP, LOADSTK, SETF16, SETF32, CALLA and the GETFI pseudo.
  • Constant global addresses as MOVI _sym+bits; surviving GEP offsets are bit offsets (ADDI 24, not ADDI 192).
  • GSPA syntax: leading-underscore symbols, hex ...h (or decimal), signed decimal immediates (ADDI -32, A5, MOVE *SP(128), A14, 1), field-select operands. Output is asm only; encodings are validated by GSPA and dis34, not by an LLVM encoder.

Frame-index materialisation (GETFI)

An ISD::FrameIndex is selected to the GETFI pseudo with a TargetFrameIndex operand. eliminateFrameIndex resolves it to the FP-relative bit offset (fixed objects use ObjectOffset*8; locals use (ObjectOffset + StackSize)*8). expandPostRAPseudo then emits:

	MOVE A13, <dst>
	ADDI <bit offset>, <dst>

The ADDI is omitted for offset 0. Spill-slot addresses use the same GETFI materialisation followed by a 32-bit MOVE store or load; the backend never reports a fatal error for a spill.

Per-register system-stack saves

The prologue saves A13 and each callee-saved register used by the function as individual system-stack pushes (MOVE <reg>, -*SP, 1), and the epilogue pops them in reverse order after restoring STK with MOVE *SP((n+1)*32), A14, 1. This is a deliberate decision: the stack layout is identical to MMTM/MMFM (one 32-bit slot per register, old STK at SP+(n+1)*32), so the ABI-visible contract is unchanged, while the asm-only printer does not need a variable register-list operand. Callee-saved spill slots are not allocated on the program stack; the prologue/epilogue own the saves and restores.

.data and string literals

Zero-initialized, non-constant, non-private sectionless globals keep the .bss path. Every other constant definition is emitted as raw GSPA .data text from TMS34010AsmPrinter::doInitialization (so initialized data precedes the code), and .text is re-emitted before the first function.

Width and alignment rules:

  • i8 scalar or byte/string array: .byte with two hex digits prefixed by 0 (0%02Xh, for example 068h).
  • i16 scalar: .word with a leading 0 (0%04Xh) so high-bit values assemble.
  • i32 scalar: .long.
  • pointer: .long _symbol+<bit addend>; the addend is already in bits and is not scaled again.
  • .even before a .data global whose alignment is at least 16 bits, so the label and the following .word/.long agree. This is required when a short string literal precedes an int: GSPA aligns the value, not the label. .even is a 16-bit alignment only, so explicit align 4/align 8 on .data is not honored in M2; the target's standalone object alignment is 2 bytes.

A scalar's directive must match its width, or the next global's offset is wrong. Arrays accept only i8, i16, i32 and pointer elements; an i1 or i64 array (including zeroinitializer) is rejected rather than emitted at the wrong size. Private/local symbol names are sanitized to GSPA-safe names (every non-alphanumeric/non-_ character becomes _HH, keeping the leading _), so .L.str becomes a deterministic valid label. ELF directives (.rodata, .asciz, .p2align, .section) are never emitted. Unsupported initializer shapes fail with M2 .data supports only integer, pointer and byte-array initializers; available_externally declarations are skipped rather than emitted.

ROM imports

ROM entry points stay out of the compiler. The project's scripts/m2/gen-rom-imports.sh turns symbols/vt1200-v22.syms into ABI-preserving absolute-jump stubs:

	.globl _strlen
_strlen:
	jauc 3FB458D0h

JAUC is the TMS34010 unconditional absolute jump; GSPA 2.00 rejects bare ja. CALLA _strlen pushes the return PC, the stub jumps to the ROM address, and the ROM's RETS 2 returns directly to our caller. M2 consumes strlen only; later milestones extend and validate the table.

Build

cmake -S llvm -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON \
  -DLLVM_ENABLE_PROJECTS=clang \
  -DLLVM_TARGETS_TO_BUILD="X86;TMS34010" \
  -DLLVM_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=OFF \
  -DLLVM_INCLUDE_BENCHMARKS=OFF
ninja -C build llc clang opt llvm-config FileCheck count not

build/bin/llvm-lit is generated by CMake configure. Run the M2 backend tests:

build/bin/llvm-lit -v build/test/CodeGen/TMS34010 --filter='m2-'
# -> 37 passed

The tests are llvm/test/CodeGen/TMS34010/m2-*.ll:

  • Migrated/legacy selection: m2-add, m2-load, m2-char, m2-i16, m2-store, m2-global, m2-late-gep.
  • ABI: m2-args, m2-noret, m2-call-noret, m2-locals, m2-args-locals, m2-call, m2-call3, m2-callee-save, m2-fs1, m2-spill, m2-returned, m2-returned-call.
  • Data: m2-data, m2-data-avail, m2-data-align, m2-data-z64, m2-data-i1, m2-string, m2-ptrinit.
  • Loud refusals: m2-reject-i64, m2-reject-struct, m2-reject-vararg, m2-reject-sret, m2-reject-structret, m2-reject-arg8, m2-reject-i8ret, m2-reject-float, m2-reject-call-indirect, m2-reject-call-arg8, m2-reject-call-vararg.

m2-add uses RETS 2; m2-global returns in A8 with RETS 0; m2-locals/m2-args-locals require a real frame at -O0; m2-data-align reproduces the string-then-int .data alignment case; and the call tests pin the TI caller/callee sequence, the FS1 fixup, and a real spill.

Integration test

The VT1200 project holds the harness, linker command file, COFF loader and MAME checks: https://git.wut.ee/opencode.siil.arti.ee/tms-c-compiler. From a project checkout that has this fork at build/llvm-clean:

LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m2 ./scripts/build-llvm.sh
scripts/tests/m2/run.sh             # GSPA/GSPLNK under DOSBox, then MAME
scripts/tests/m2/dis-roundtrip.sh   # linked .text decoded with dis34
scripts/tests/m2/diff/check.sh      # ABI shape vs. the 1987 TI compiler
tools/tms-bitaddr/tests/run.sh      # GEP scaling/normalization

Expected: M2 MAME: PASS, M2 dis34 round-trip: PASS, M2 diff: PASS and tms-bitaddr: PASS. The differential check uses stored golden assembly from the 1987 TI compiler and does not run DOSBox; the other two gates do. The M1 harness (scripts/tests/m1/) lives on the project's m1 branch and is not an M2 gate. The project README lists the prerequisites (DOSBox, mtools, the VT1200 SDK image, MAME).