9.9 KiB
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. A13is FP,A14is STK (the program stack),A15is SP (the system stack).A8andA13–A15are reserved; the allocatable set isA0–A7andA9–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 withSETF 16, 0, 0andSETF 32, 0, 1. - Callee: set
SETF 16, 0, 0andSETF 32, 0, 1; save the old FP and each used callee-saved register on the system stack;FP = STK; allocate the frame withADDI <bits>, STK. The first explicit argument is atFP(-32), the second atFP(-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 withADDI -<frame_bits>, A14. Then pop the saved registers in reverse order, andRETS 2when the function has explicit arguments orRETS 0otherwise. Afternsystem-stack pushes the caller's saved STK is atSP + (n+1)*32(the formula applies only when arguments were passed, i.e. the caller saved STK);RETS Npops the return PC and advances SP by32 + 16N. - Callee-saved registers are
A0–A7andA9–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. FS0must be 16 (forshortaccesses) andFS1must be 32 (for 32-bit words) at function entry and after every call. The prologue emitsSETF 16, 0, 0thenSETF 32, 0, 1;LowerCallemits the same pair immediately after eachCALLA.
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
ADDfori32;MOVIfor constants;SETF 16, 0, 0andSETF 32, 0, 1;RETS N.MOVE ..., 1(FS1) for 32-bit load/store,MOVE ..., 0(FS0) fori16, sign-extendingMOVBfori8.- Argument/return lowering with
PUSH32,POP32,PUSHARG,SETFP,ADDSTK,MOVEFP,LOADSTK,SETF16,SETF32,CALLAand theGETFIpseudo. - Constant global addresses as
MOVI _sym+bits; surviving GEP offsets are bit offsets (ADDI 24, notADDI 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 anddis34, 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:
i8scalar or byte/string array:.bytewith two hex digits prefixed by0(0%02Xh, for example068h).i16scalar:.wordwith a leading0(0%04Xh) so high-bit values assemble.i32scalar:.long.- pointer:
.long _symbol+<bit addend>; the addend is already in bits and is not scaled again. .evenbefore a.dataglobal whose alignment is at least 16 bits, so the label and the following.word/.longagree. This is required when a short string literal precedes anint: GSPA aligns the value, not the label..evenis a 16-bit alignment only, so explicitalign 4/align 8on.datais 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).