Files
arti de5a365596
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
require-team-membership/action.yml (push) Invalid workflow file
unprivileged-download-artifact/action.yml (push) Invalid workflow file
tms34010: clarify M4 carry-forwards and hash wording
2026-11-12 12:23:01 +00:00

33 KiB
Raw Permalink Blame History

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.
  • 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 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

  • 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.

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. MOVB covers signed char and the FS0 MOVE covers signed short; there is no zero-extending pattern. unsigned char, unsigned short and _Bool loads reach ZEXTLOAD i8/i16 and llc stops with a raw Cannot select. m3-reject-zext.ll pins that failure so the limitation stays visible; M4 adds the zero-extending patterns.
  • -O2 loop exit-value rewriting is not lowered. IndVarSimplify can turn sumto's loop into a closed-form i33 multiply, which M3 cannot select, so the M3 harness 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 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: DIVS or DIVU.
  • 32/32 remainder: quotient, MPYS by the divisor, then SUB.

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 after tms-bitaddr --scale, and --normalize can 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.ll pins the target-level behaviour and scripts/m4/switch.c is 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 XPutImage exceeds the ROM's 16 KiB request buffer, so the image is emitted in 15 bands of 32 rows; the bitmap path ignores src_y, so each band selects its rows by moving XImage.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) and XCopyArea'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. XCopyArea copies IMG_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/i16 loads (m4-zext.ll; the old m3-reject-zext.ll was removed);
  • module-unique private labels (m4-private.ll and the two-TU private.sh), with the GSPA 31-character caveat above;
  • unsigned/<= runtime coverage (m4-branches.ll, and the MAME unsigned.sh markers for JALE/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 -c or native COFF. GSPA/GSPLNK still produce the COFF. The JR/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 or i64 arguments, returns, arithmetic or branches.
  • General varargs: no va_list, va_start or va_arg; only the contiguous &last_fixed pattern 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/i16 returns remain rejected; fixed i8/i16 arguments 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, or XDestroyImage on a non-heap image.
  • JR/relaxation and deeper libc/runtime coverage: M5 owns the code-size pass and broader RTS.LIB integration.

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: a JAUC self-loop and an LBB label, with CHECK-NOT: .LBB.
  • m3-loop: CMP immediately followed by JALE, an unconditional back-edge (JAUC) and RETS 2.
  • m3-if: CMP + JAGT and RETS 2.
  • m3-cmpi: the constant-right compare becomes CMPI + JALT, at -O0 and -O2.
  • m3-setcc: the SELECT_CC 1/0 diamond (CMP/JALT/MOVI 0/MOVI 1).
  • m3-unsigned: JALO/JAHI for the unsigned conditions.
  • m3-ops: SUB/AND/OR/XOR/SLL/SRL/SRA, plus NEG/NOT/ANDI and a constant shift.
  • m3-unaligned: align-1 i32 load/store, pointer load and i16 sign-extending load select direct MOVE forms and never MOVB.

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).