33 KiB
LLVM 23.1.5 fork for TMS34010
Branch m4 is llvmorg-23.1.5 plus the TMS34010 target and clang
bit-address changes (M0), a minimal TMS34010 backend (M1), the TI ABI, calls
and .data support (M2), integer control flow plus the first compiled C Xlib
window (M3), and M4's function pointers, multiply/divide, zero-extending
narrow loads, the XImage/event client and the pinned termtris integration.
This branch is the canonical source for the target; the project's
patches/llvm mirror is frozen at M0. Branch m3 is the previous milestone
snapshot and the fork's default branch; branch m2 is the M2 tip and 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 remain a later milestone (M4+). Narrow
loads/stores inside a function are supported only in their sign-extending
forms: MOVB for signed char and the FS0 MOVE for signed short.
Zero-extending narrow loads (unsigned char, unsigned short, _Bool) have
no pattern in M2/M3 and are M4 work; m3-reject-zext.ll pins the loud failure.
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.
M3 state
M3 adds the control flow and ALU operations that real C needs, and proves them
with the first compiled C Xlib client. It is still assembly-only and still
i32/pointer only.
M3 limitations
- Narrow loads are sign-extending only.
MOVBcovers signedcharand the FS0MOVEcovers signedshort; there is no zero-extending pattern.unsigned char,unsigned shortand_Boolloads reachZEXTLOAD i8/i16andllcstops with a rawCannot select.m3-reject-zext.llpins that failure so the limitation stays visible; M4 adds the zero-extending patterns. -O2loop exit-value rewriting is not lowered. IndVarSimplify can turnsumto's loop into a closed-formi33multiply, which M3 cannot select, so the M3 harness buildsbranch.cwith-replexitval=never. Ordinarywhile/forloops that keep a real back-edge and phi work. M4/M5 must add multiply/i64lowering, a target-level pass policy, or a target diagnostic instead of the raw abort.
Branches, CMP and the status register
The TMS34010 ST status register holds the condition flags. It is not a
general-purpose register: it is outside every register class, CMP/CMPI and
the ALU/shift instructions define it, and the conditional branches use it.
CMP Rs, Rd sets the flags from Rd - Rs. The lowering always wants the flags
to represent LHS - RHS, so it emits CMP RHS, LHS (the compare's first
operand is the subtrahend). A constant right-hand side becomes CMPI imm, LHS;
a constant left-hand side is swapped with the right and the condition code is
swapped with it. The condition map is the dis34 order:
| IR condition | branch | JAcc cc |
meaning |
|---|---|---|---|
SETEQ |
JAEQ |
0xA |
equal (Z) |
SETNE |
JANE |
0xB |
not equal (~Z) |
SETLT |
JALT |
0x4 |
signed less than |
SETGE |
JAGE |
0x5 |
signed greater or equal |
SETLE |
JALE |
0x6 |
signed less or equal |
SETGT |
JAGT |
0x7 |
signed greater than |
SETULT |
JALO |
0x8 |
unsigned lower (C) |
SETUGE |
JAHS |
0x9 |
unsigned higher or same (~C) |
SETULE |
JALS |
0x2 |
unsigned lower or same |
SETUGT |
JAHI |
0x3 |
unsigned higher |
JAEQ/JANE/JALT/JALE/JAGT/JAGE are the signed comparisons;
JALO/JALS/JAHI/JAHS are the unsigned ones. JAP/JAN/JAV/JANV/
JANN exist in the instruction set and the .td file but no IR condition
selects them yet.
A JAcc encodes as 1100 cccc 1000 0000 plus a 32-bit absolute target (three
words, Size = 6); a conditional branch falls through when its condition is
false, so it is a terminator and a branch but not a barrier. JAUC is the
three-word unconditional absolute jump. GSPA 2.00 rejects a bare ja, so the
backend prints the absolute form; the relative JR/branch-relaxation code-size
pass is M5.
A compare feeding a branch is chosen in LowerBR_CC/LowerBRCOND and emitted
with a glue result that the branch consumes. The glue forces the
CMP/CMPI to be scheduled immediately before its Jcc, so no other
flag-writing instruction can slip between them. A general boolean branch
(brcond on a non-setcc value) compares the value with zero and branches
SETNE. A constant-true condition becomes JAUC; a constant-false condition
fails loudly rather than silently dropping a CFG edge, because the fallthrough
block cannot be named in this lowering.
SELECTCC and setcc
There is no conditional move and no dedicated setcc result, so
SETCC/SELECT are expanded by the legalizer to SELECT_CC over the values
1 and 0. LowerSELECT_CC turns SELECT_CC into the SELECTCC pseudo, which
emitSelectCC expands after instruction selection:
ThisMBB: CMP/CMPI; Jcc TrueMBB
IfFalseBB: <false value>; JAUC SinkMBB (fallthrough path)
TrueMBB: <true value>
SinkMBB: PHI [true, TrueMBB], [false, IfFalseBB]
Values may be registers or immediates; each is materialised in its own block,
so a constant does not cost a MOVI on the other path. This is the ?:
diamond (and the setcc 1/0 diamond); m3-setcc.ll and m3-if.ll pin it.
GSPA-safe LBB labels
The generic LLVM block label prefix is .L (for example .LBB0_1), but GSPA
2.00 identifiers accept only alphanumerics and _. TMS34010MCAsmInfo sets
InternalSymbolPrefix and LinkerPrivateGlobalPrefix to L, so basic blocks
print as LBB0_1, LBB0_2, … and the MCInstLower path prints the block
symbol by name. The lit tests require the undotted LBB prefix, and
m3-branch additionally asserts CHECK-NOT: .LBB. The linked-image gate
(scripts/tests/m3/dis-roundtrip.sh) is what rejects a dotted .LBB in the
concatenated assembly.
llc restarts the LBB<n>_<m> numbering in every translation unit. GSPA 2.00
has one flat symbol table per assembly, so the M3 harness gives each TU its
own prefix when it concatenates the generated asm (LBBB…, LBBL…, LBBX…);
the labels stay dot-free LBB… names.
Integer ALU and shifts
Selected by pattern: SUB, AND, OR, XOR (with CLR as the
XOR Rd, Rd form), unary NEG and NOT, and the shifted-register forms
SLL, SRL, SRA. Immediate forms: ADDI, ANDI, and SLLK, SRAK,
SRLK for the shift-by-constant cases. The shift count is masked by the
hardware exactly as C requires for the 32-bit type.
Misaligned accesses
MOVE, MOVB and the FS0/FS1 field-size forms have no alignment requirement:
the TMS34010 performs unaligned field accesses (TMS34010 User's Guide §7.5).
TMS34010TargetLowering::allowsMisalignedMemoryAccesses (both the EVT and LLT
overloads) therefore reports integer/pointer accesses of at most 32 bits in
address space 0 as legal and fast. Without it the legalizer split an align-1
load into byte accesses, which cannot select because only a sign-extending
MOVB load exists. Packed/bit-addressed struct fields now select a direct
MOVE ... , 1 (or the FS0 form for i16); m3-unaligned.ll pins that no
MOVB appears.
GSPA and the first compiled C Xlib window
The M3 evidence is a compiled C client, not hand-written asm. scripts/m3 in
the project tree builds branch.c (sumto/max2/lt), layout.c (the
Display/Screen layout asserts) and xhello.c (the four-call client) with
tms-clang -O2, llc -O0, then GSPA/GSPLNK 2.00. The linked COFF runs in
MAME and produces:
CALLA _XOpenDisplay ; "0::0"
ADDI 1152, A1 ; 0x480 Display.screens (byte 0x90)
ADDI 64, A1 ; 0x40 Screen.root (byte 0x8)
MOVE A1, *A14+, 1 ; ... nine XCreateSimpleWindow pushes ...
CALLA _XCreateSimpleWindow
CALLA _XMapWindow
CALLA _XSync
The ROM's own hand-written xhello.asm uses the same displacements
(*a11(0480h), *a10(040h)), which confirms that TMS34010 indirect
displacements are bit offsets. The MAME gate pins VT1200_MONITOR=VR319 and
requires two all-white 32-bit client words at (150,150) and (250,200), not
just nonzero return markers. The differential CI check
(scripts/tests/m3/diff/check.sh) compares this shape with stored 1987 TI
compiler output for K&R sources and needs no DOSBox; the stored assembly uses
TI's JRLE/JRUC spellings, which the check accepts alongside JALE/JAUC.
M4 state
M4 closes the remaining Xlib path to a real image/event client and a
full-game integration. It is still assembly-only: GSPA 2.00 and GSPLNK
2.00 under DOSBox produce the COFF. There is no MCCodeEmitter, object
writer, llc -filetype=obj or clang -c.
Function pointers and CALL Rs
A function pointer is an ordinary 32-bit value holding a code bit address.
Taking a function's address materialises MOVI _inc, A?; an initialized
global function pointer is a .data word (.long _dec). LowerCall accepts
a callee that is not a GlobalAddress and emits the one-word register call
CALL Rs (GSPA encodes it as 0x0920; dis34 prints call aN). The call
still saves the old STK (MOVE A14, -*SP, 1), pushes its arguments
last-to-first on the program stack, and re-establishes SETF 16, 0, 0 /
SETF 32, 0, 1; the callee's RETS 2 restores A14. Direct calls remain
CALLA _sym. m4-fnptr.ll pins the selection, scripts/tests/m4/fnptr.sh
runs the indirect path in MAME and checks dis34 at the linked address, and
scripts/tests/m4/diff/ti-fnptr.s is the 1987-compiler ABI golden.
32-bit multiply and divide
MPYS (0x5C00), MPYU (0x5E00), DIVS (0x5800) and DIVU
(0x5A00) are selected directly; there is no integer multiply/divide helper
in RTS.LIB (_div is only the C div() wrapper; FD$MUL/FD$DIV are
floating point). The destination must be an odd A register (A1, A3,
…, A11): an even Rd denotes the Rd:Rd+1 pair and would clobber the next
register (A15/SP in particular), so ARegsOdd ties the single-register form.
- 32×32 low product: one
MPYS(the low 32 bits are identical for signed and unsigned). - 16×16 unsigned product: one
MPYU(the natural shape after zero-extension to i32). - 32/32 quotient:
DIVSorDIVU. - 32/32 remainder: quotient,
MPYSby the divisor, thenSUB.
SREM/UREM are custom-lowered this way so C truncation semantics hold and
no even-destination pair is needed. m4-mul.ll pins MPYS/MPYU/DIVS/
DIVU and the remainder sequence.
The -O2 loop policy is -replexitval=never in scripts/tms-clang.
opt runs without a target machine at that point, so the driver cannot
express this as a TTI policy; IndVarSimplify's exit-value replacement turns
sumto's loop into a closed-form i33 multiply, which this i64-less
target cannot select. With the flag, sumto keeps a real back-edge and
squares uses MPYS. m4-divconst.ll also pins that the magic-number
divide combine does not fire: MULHS/MULHU/SMUL_LOHI/UMUL_LOHI are
Expand, so divide-by-constant stays DIVS/DIVU.
Zero-extending / extending narrow loads
ZEXTLOAD i8 and ZEXTLOAD i16 are custom-lowered to the existing
sign-extending load (MOVB or the FS0 MOVE) plus ANDI 255 / ANDI 65535,
reusing the original chain and MMO so volatile and ordering behaviour is
preserved. EXTLOAD (the anyext form) lowers the same way, and an i1
load is a byte load masked to one bit. m4-zext.ll, m4-anyext.ll and
m4-i1load.ll pin the sequences. The old M3 m3-reject-zext.ll was removed.
Module-unique private labels
Every private/local symbol (GV->hasLocalLinkage()) gets a deterministic
suffix _<8 uppercase hex digits> derived from a custom 64-bit string hash
(FNV-style, not canonical FNV-1a) of Module::getModuleIdentifier(). Two
translation units with string literals therefore no longer collide in GSPA's
flat symbol table; public names are byte-for-byte unchanged. llc file.ll
uses the file name (or IR source_filename) as the identifier; llc < file.ll uses the stable <stdin> fallback. m4-private.ll and the two-TU
scripts/tests/m4/private.sh pin it.
GSPA 2.00 truncates identifiers at 31 characters. A private name long enough
to lose the _<8 hex> token at that limit will collide loudly (a duplicate
definition from GSPA), not silently; the naming scheme is unchanged in M4 and
a long-name two-TU link test is an M5 follow-up.
i8/i16 fixed arguments
i8 and i16 fixed parameters are promoted to one 4-byte program-stack
slot. The caller extends the actual according to the parameter's
signext/zeroext attribute (SLL 16/SRA 16 for a signed short,
ANDI 255 for an unsigned char), and the callee truncates on load.
m4-arg8.ll pins both sides. i8, i16, i64, float and aggregate
returns remain rejected, as do i64/float/aggregate arguments.
Varargs: the contiguous-stack &last_fixed pattern
M4 implements only the form termtris's compat.c uses. Fixed arguments live
at FP-32, FP-64, … and the variadic actuals occupy the following
contiguous slots (FP-96, FP-128, …). The callee walks from
(char *)&last_fixed by decreasing the pointer by sizeof(long) (4 bytes),
which reads the first variadic slot. There is no va_list, va_start or
va_arg.
A variadic definition with no fixed argument is rejected
(TMS34010 varargs requires at least one fixed argument): the walk has no
anchor and the epilogue could not tell whether the caller saved STK. An
i8/i16 last fixed argument is also rejected
(TMS34010 varargs requires an i32 or pointer last fixed argument): the fixed
object is always 4 bytes, but LLVM's argument-copy elision requires the fixed
object and the parameter alloca to have equal size, so a narrow anchor would
silently read a private local instead of the caller's slot. The supported
anchor is i32 or a pointer. m4-varargs.ll, m4-varargs-anchor.ll,
m4-reject-vararg0.ll and m4-reject-vararg-narrow{,16}.ll pin the rules;
scripts/tests/m4/varargs.sh runs three contiguous arguments in MAME.
A static variadic definition can lose ... under -O2 dead-argument
elimination; the external termtris sprintf is unaffected (it is not a
static definition in a TU where the optimizer can see all callers).
Driver policies
scripts/tms-clang invokes opt with two explicit policies:
-replexitval=never— the i33 exit-value multiply described above.-switch-to-lookup=false— the optimizer's switch-to-lookup transform creates a new byte-offset GEP aftertms-bitaddr --scale, and--normalizecan only treat surviving GEP offsets as bit offsets. Such a GEP would index the table one bit per slot (an 8× address bug). The backend also has no jump-table lowering, so compare/branch is correct. termtris's five-case X-event switch is the motivating case;m4-switch.llpins the target-level behaviour andscripts/m4/switch.cis the driver probe.
XImage, XEvent and the ROM layouts
scripts/m4/x11m4.h models the 32-bit X11R4 ABI. The TMS34010 C ABI packs
aggregates, so the offsets are the packed object offsets:
| object | byte offset | bit displacement |
|---|---|---|
Display.screens |
0x90 |
0x480 |
Screen.root |
0x8 |
0x40 |
XImage.data |
16 | 0x80 |
XImage.byte_order |
20 | 0xA0 |
XImage.bytes_per_line |
40 | 0x140 |
XImage.bits_per_pixel |
44 | 0x160 |
XImage.f |
64 | 0x200 |
sizeof(XImage) |
88 | — |
XErrorEvent.error_code |
16 | 0x80 |
sizeof(XErrorEvent) |
20 | — |
XEvent.xkey.keycode |
52 | 0x1A0 |
sizeof(XEvent) |
96 | — |
XEvent is 96 bytes (its largest member is the protocol pad[24] of 32-bit
longs), and the termtris event buffer is vt_u32 evbuf[24]. XErrorEvent is
20 bytes on the 32-bit ABI but would be 19 packed, so x11m4.h and the TI
golden declare one explicit trailing abi_pad byte. layout.sh asserts every
offset with _Static_assert and checks the same constants in the 1987 TI
golden; scripts/tests/m4/diff/ti-image.s additionally checks the XImage
field displacements in a real image-call function.
The XG block and the corrected symbol table
The VT1200 Xlib keeps per-process state in an XG block reached through the
process user area. scripts/m4/xgfix.c installs a 1 KiB zeroed block before
XOpenDisplay; XSetErrorHandler/XSetIOErrorHandler store into that block
at +0x60/+0x40. The client's int (Display *, XErrorEvent *) handler
therefore receives the ROM's real error events.
The v2.2 table has 30 entries. M4 corrected the M3 name: XMapWindow is
0x3FD64F60 (MapWindow request opcode 8), while 0x3FD645B0 is
XRaiseWindow (ConfigureWindow opcode 12, CWStackMode = Above). The old M3
pass with 0x3FD645B0 was the server's configure-side effect, not a mapping
call. A client that wants map-and-raise calls both.
scripts/m4/validate-syms.sh checks the ROM range, 16-bit alignment and a
plausible prologue for all 30 entries, and the opcode semantics of the
correction-sensitive ones (XMapWindow, XRaiseWindow, XPutImage,
XGetImage, XCreateImage, the two error-handler setters).
The image/event client
scripts/m4/xshow.c is a compiled C client: install the XG block, open
"0::0" with a retry, create a 640×480 window, select
key/button/expose/focus masks, create a GC, build a 1-bit LSBFirst XYBitmap
checker, call XCreateImage, set byte_order/bitmap_bit_order,
XSetErrorHandler, map and raise, set input focus, draw, then drain events
and quit on the real LK401 q.
Two server/Xlib constraints shape the drawing path:
- A single 38 KiB
XPutImageexceeds the ROM's 16 KiB request buffer, so the image is emitted in 15 bands of 32 rows; the bitmap path ignoressrc_y, so each band selects its rows by movingXImage.data. - The server's 1-bit drawing path needs a 32-bit-aligned destination, so each
band is drawn into an aligned depth-1 pixmap (
XCreatePixmap) andXCopyArea'd to window x=12 (screen x=32). The 12-pixel prefix is filled with the preceding cell's colour. The checker cells remain 32 pixels; the phase inside the (20,20) window is 12 pixels.XCopyAreacopiesIMG_W - 12= 628 columns, so the rightmost 12 source columns (the final 12 px of the last cell) are not displayed; the last visible cell is 20 px, not 32.
The 38,400-byte image and the 1 KiB XG block live in .bss at 0x01A00000
(the termtris-proven region); prog stays at 0x018A0000. The client primes
the ROM blitter (blit_copy, 0x3FB44400) with one 4-byte self-copy before
the first XPutImage. scripts/tests/m4/run.sh requires the image pixels,
keycode 193 / keysym 0x71, quit=1, zero X errors, focus = the client
window 0x400001, the last event being KeyPress, and a loader status of 0.
Pinned termtris
The full-game integration is scripts/tests/m4/termtris.sh plus
scripts/m4/termtris/build.sh, pinned to the vt1200-termtris tree at
commit be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf (branch termtris). Six
translation units (main, game, scoredb, x11, compat, xgfix) are
compiled with this compiler, assembled and linked by the 1987 GSPA/GSPLNK
2.00 with the 1987 rts.lib, and run in MAME. The regions are the
0x40000-bit prog at 0x018A0000 and ram at 0x01A00000. The final map
is .text 0x2E210 bits (23,618 bytes), .data 0x21A0 bits, .bss 0x1DBD0
bits; it fits prog and the loader reports status=0 (OK) with no
UNDEFED symbols. The gate runs board/move/panels/quit modes with real key
injection and enforces loops >= 100 (board/move/panels/quit), events >= 6
for move/panels/quit (board >= 2), quit = 1 only for quit, focus = the
created termtris window (0x00400001), loader status 0 and a non-empty
snapshot each time. The pinned tree is test-only (GPLv3) and is not part
of the compiler or project distribution.
M3 carry-forwards in M4
M4 closes three of the five M3 carry-forwards:
- zero-extending
i8/i16loads (m4-zext.ll; the oldm3-reject-zext.llwas removed); - module-unique private labels (
m4-private.lland the two-TUprivate.sh), with the GSPA 31-character caveat above; - unsigned/
<=runtime coverage (m4-branches.ll, and the MAMEunsigned.shmarkers forJALE/JALS/JAGT/JAGE/JAHS/JAHI/JANE/JALT/JALO).
The new M4 harness and scripts/tests/m4/dis-roundtrip.sh are
operand/register-independent, and layout.sh was tightened. The M3 regression
is deliberately unchanged: m3-cmpi.ll still uses CHECK-DAG: MOVI 0 /
MOVI 1 (the 1/0 order is not pinned), and scripts/m3/dis-roundtrip.sh
still hardcodes the nine XCreateSimpleWindow push registers
(A2/A0). Tightening those M3 tests is a test-hardening follow-up; the M3
suite (m3-*.ll and scripts/tests/m3/) passes as-is, with only the obsolete
zext-reject test removed.
M4 non-goals and the M5 boundary
M4 deliberately stops before:
- Native object output: no
MCCodeEmitter, object writer,llc -filetype=obj,clang -cor native COFF. GSPA/GSPLNK still produce the COFF. TheJR/branch-relaxation code-size pass is M5, as are MMTM/MMFM register-list saves (the current per-register system-stack pushes have the identical ABI layout). - Floats and
long long: no float/double ori64arguments, returns, arithmetic or branches. - General varargs: no
va_list,va_startorva_arg; only the contiguous&last_fixedpattern above. Non-contiguous/register varargs are M5. - Bitfields and aggregates by value: no structs/unions by value,
sret,byval, bitfields, or aggregate returns. Struct fields through pointers are supported. - Narrow returns:
i8/i16returns remain rejected; fixedi8/i16arguments are supported. - Xlib beyond the tested calls: the client uses the validated image,
event, GC, window and error-handler entries. It does not implement the
whole Xlib surface,
XGetImage, orXDestroyImageon a non-heap image. JR/relaxation and deeper libc/runtime coverage: M5 owns the code-size pass and broaderRTS.LIBintegration.
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 full M2+M3+M4
backend suite:
build/bin/llvm-lit -v build/test/CodeGen/TMS34010 --filter='(m2|m3|m4)-'
# -> 58 passed (32 m2-*, 8 m3-*, 18 m4-*)
The M2 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-sret,m2-reject-structret,m2-reject-i8ret,m2-reject-float.
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.
The M3 tests are:
m3-branch: aJAUCself-loop and anLBBlabel, withCHECK-NOT: .LBB.m3-loop:CMPimmediately followed byJALE, an unconditional back-edge (JAUC) andRETS 2.m3-if:CMP+JAGTandRETS 2.m3-cmpi: the constant-right compare becomesCMPI+JALT, at-O0and-O2.m3-setcc: theSELECT_CC1/0 diamond (CMP/JALT/MOVI 0/MOVI 1).m3-unsigned:JALO/JAHIfor the unsigned conditions.m3-ops:SUB/AND/OR/XOR/SLL/SRL/SRA, plusNEG/NOT/ANDIand a constant shift.m3-unaligned: align-1i32load/store, pointer load andi16sign-extending load select directMOVEforms and neverMOVB.
The M4 tests are:
- Loads and data:
m4-zext,m4-anyext,m4-i1load,m4-data-nested. - Arithmetic and control flow:
m4-mul,m4-divconst,m4-cmp-highbit,m4-branches,m4-switch,m4-vector-copy. - ABI and labels:
m4-fnptr,m4-arg8,m4-private,m4-varargs,m4-varargs-anchor. - Loud refusals:
m4-reject-vararg0,m4-reject-vararg-narrow,m4-reject-vararg-narrow16.
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 (branch m4):
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m4 ./scripts/build-llvm.sh
ninja -C build/llvm-clean-build llc opt clang llvm-config FileCheck count not
build/llvm-clean-build/bin/llvm-lit -v build/llvm-clean/llvm/test/CodeGen/TMS34010 \
--filter='(m2|m3|m4)-'
scripts/tests/m4/run.sh # XImage/event client in MAME
scripts/tests/m4/dis-roundtrip.sh # linked .text decoded with dis34
scripts/tests/m4/layout.sh # XImage/XEvent/error-event offsets
scripts/tests/m4/symbols.sh # 30-entry v2.2 table and the correction
scripts/tests/m4/termtris.sh # pinned full-game integration
scripts/tests/m4/diff/check.sh # TI function-pointer/image ABI golden
scripts/tests/m4/unsigned.sh
scripts/tests/m4/private.sh
scripts/tests/m4/mul.sh
scripts/tests/m4/fnptr.sh
scripts/tests/m4/varargs.sh
tools/tms-bitaddr/tests/run.sh # GEP scaling/normalization
scripts/tests/m3/run.sh # M3 regression
scripts/tests/m3/dis-roundtrip.sh
scripts/tests/m2/run.sh # M2 regression
scripts/tests/m2/dis-roundtrip.sh
Expected: m4 xshow: PASS, M4 dis34 round-trip: PASS, layout.sh: PASS,
symbols.sh: PASS, m4 termtris: PASS, M4 diff: PASS, the unsigned /
private / mul / fnptr / varargs PASS lines, tms-bitaddr: PASS, and the M3/M2
regression PASS lines. The MAME gates pin VT1200_MONITOR=VR319; the image
gate requires the checker pixels, the real LK401 q (keycode 193, keysym
0x71), focus 0x400001, zero X errors and a loader status of 0. The
differential check uses stored golden assembly from the 1987 TI compiler and
does not run DOSBox; the MAME/dis34 gates do. The pinned termtris tree is
test-only. The project README lists the prerequisites (DOSBox, mtools, the
VT1200 SDK image, MAME).