45 Commits
m0 .. m4
Author SHA1 Message Date
arti 5d980dc6e3 tests/docs: final M4 review fixes 2026-11-12 12:23:04 +00:00
arti 1511919437 tests/docs: fix M4 review findings in bands, symbols and diff check 2026-11-12 12:04:36 +00:00
arti 2d5ed90fb2 tests: M4 Xlib images, events and the termtris integration 2026-11-12 11:49:32 +00:00
arti 1a91329864 tests: M4 termtris integration and varargs ABI gate
Build the pinned termtris branch (be5b323) with this compiler, assemble
and link it with the TI 1987 GSPA/GSPLNK and RTS.LIB, and run the proven
play_keys board/move/panels/quit paths in MAME with the M4 loader.  Add
the contiguous-stack varargs MAME probe and the switch-to-lookup driver
regression check.  The termtris build found the i8/i16 argument, i1/
anyext load, nested-data, MULHS and late-GEP gaps fixed in the compiler.
2026-11-12 10:51:15 +00:00
arti fb0159620e tests: validate the image blitter copy and assert window focus 2026-11-12 09:40:45 +00:00
arti 26697b9585 tests: M4 1-bit XImage, real key events, and the MAME image gate 2026-11-12 09:21:17 +00:00
arti 7946519828 symbols: correct XMapWindow, add XImage/XG entries and validation 2026-11-12 07:19:42 +00:00
arti 8cb060cdb4 tests: M4 X11R4 XImage/XEvent/error-event layout 2026-11-12 06:53:29 +00:00
arti 7a89a1fe4c tests: M4 function-pointer/indirect-call gate
Add the fnptr C callback test (direct symbol, volatile local pointer and
global pointer), its GSPA entry and marker script, and scripts/tests/m4/
fnptr.sh.  The gate compiles at the default -O2 pipeline, requires all four
markers to be 42 in MAME, and checks the linked assembly: _apply calls through
an A register (CALL, never CALLA) after the argument push, while _fnptr_test
keeps the direct CALLA and materialises inc with MOVI.  It also pins the GSPA
0x0920 encoding and requires dis34 to decode the call as 'call aN'.
2026-11-12 06:40:12 +00:00
arti d8efad2479 tests: use an unsigned helper for the wide multiply marker
The full-width marker called the signed mul helper with a constant whose
product overflows int, which is UB and made the assertion rely on wrap-around.
Add mulu(unsigned, unsigned) for marker 10; the low-32 product and MPYS
selection are unchanged.  Check the optimized IR has no nsw on mulu.
2026-11-12 06:21:31 +00:00
arti 00522b56fc tests: exercise wide 32-bit products and unsigned umul16
Add runtime markers for a full-width 32x32 multiply (0x12345678 * 0x9abcdef0
= 0x242d2080) and for a wide-dividend remainder, so a 16-bit-truncated MPYS or
a wrong FS1 cannot still produce the expected small results.  Compute the 16x16
unsigned product in unsigned arithmetic to remove the signed-overflow
annotation, and reject a regressed nuw nsw multiply in the optimized IR.
2026-11-12 06:17:29 +00:00
arti 7cd9a48ee3 tests: M4 multiply/divide and -O2 loop gates
Add a runtime marker test for 32x32 multiply, the 16x16 MPYU path, signed and
unsigned divide/remainder, and for sumto/squares compiled through the ordinary
tms-clang -O2 driver.  The linked assembly is also checked for MPYS/MPYU/
DIVS/DIVU and for a surviving sumto loop.
2026-11-12 06:04:56 +00:00
arti cfe751ccab driver: keep -O2 loops for the i64-less target
Scripts/tms-clang runs the generic opt pipeline without a target machine, so
the target cannot veto IndVarSimplify's exit-value replacement through TTI.
That rewrite turns sumto's closed form into an i33 multiply, which llc cannot
select.  Disable only that rewrite with -replexitval=never and document why.
2026-11-12 06:04:56 +00:00
arti 57ea80a5c0 tests: exercise every signed and unsigned predicate at runtime 2026-11-12 05:46:38 +00:00
arti 28c9ee7e3c tests: M4 unsigned runtime and private-label gates 2026-11-12 05:38:22 +00:00
arti dbf71bdc5f docs: M4 Xlib images, events and termtris integration plan 2026-11-12 05:23:33 +00:00
arti 24f877362b docs: finalize M3 limitations and M4 prerequisites 2026-11-11 20:30:37 +00:00
arti 2681be26c0 tests: anchor the M3 Xlib displacement checks 2026-11-11 20:06:58 +00:00
arti ed58040fbd docs: M3 control flow and first Xlib window 2026-11-11 19:56:19 +00:00
arti 09db9e22e9 tests: M3 branches and first Xlib window in MAME 2026-11-11 19:46:00 +00:00
arti b34016a9f3 docs: correct M3 ROM bit displacements and unaligned-load carry-forward 2026-11-11 19:14:05 +00:00
arti 2187f59b20 tests: M3 Xlib Display/Screen layout 2026-11-11 19:07:44 +00:00
arti d1ba1e4d86 docs: M3 control flow and first Xlib window plan 2026-11-11 17:54:01 +00:00
arti 2d4919db36 docs: finalize M2 ABI and tail-call notes 2026-11-11 17:36:17 +00:00
arti f1a604d70e docs: correct M2 branch and epilogue guidance 2026-11-11 17:15:43 +00:00
arti 3a188b35fb docs: M2 TI ABI, ROM strlen and golden ABI check 2026-11-11 17:05:33 +00:00
arti fa00b8740b docs: finish the M2 loader and returned reconciliation 2026-11-11 16:59:30 +00:00
arti b614e70851 docs: reconcile M2 plan with JAUC, returned, loader and .data alignment 2026-11-11 16:59:23 +00:00
arti 0008d7be20 tests: M2 TI ABI and ROM strlen in MAME 2026-11-11 16:50:06 +00:00
arti d4f165fa7d docs: record the M2 .data width and pointer-init rules 2026-11-11 16:35:43 +00:00
arti 3f30806a6d docs: require width-based .data emission for i8/i16 scalars 2026-11-11 16:24:29 +00:00
arti e7ba50d7a3 docs: add caller-side rejection tests to M2 2026-11-11 16:10:38 +00:00
arti 469b1d870e docs: require a spill-slot frame assertion in m2-spill 2026-11-11 15:55:18 +00:00
arti e60a1fdee9 docs: pin the M2 callee stack arithmetic in m2-args 2026-11-11 15:54:59 +00:00
arti 5e8dbffb01 docs: move the M2 spill test to Task 3 2026-11-11 15:21:09 +00:00
arti c0746338b0 docs: M2 TI ABI and ROM call implementation plan 2026-11-11 14:57:27 +00:00
arti 7749cc810b scripts: fix stale M1 comments and tighten the harness checks
Preserve the dis34 output on a failed round-trip instead of claiming a
deleted temp file is kept, require GSPA "No Warnings" in addition to
"No Errors", and correct the six-function and M1 pipeline comments.
2026-11-11 10:15:34 +00:00
arti eacaab68f6 docs: correct the GEP scaling warning and M1 data limits
Raw clang IR has byte GEP offsets that llc treats as bits, so addresses
are 8x too small; always run tms-clang/tms-bitaddr --scale first.
Document that M1 supports only zero-initialized globals and rejects
.data/string literals loudly, and defer the spec's .data initializer
probe to M2.
2026-11-11 10:15:32 +00:00
arti 937a8cc800 tms-bitaddr: make --normalize reject typed and vector GEPs
--normalize lowers GEP offsets as already-scaled bit offsets.  A typed
GEP still carries a DataLayout byte offset and a vector GEP has no
scalar lowering, so accepting either produced addresses 8x too small.
Check all instruction and constant-expression GEPs and fail clearly;
--scale still scales typed GEPs normally.  Add positive and negative
tool tests.
2026-11-11 10:15:30 +00:00
arti 56d6a71cb7 docs: M1 minimal backend and test convention 2026-11-11 09:53:23 +00:00
arti 755b3cb7b2 tests: link M1 char helpers and check MOVB 2026-11-11 08:53:41 +00:00
arti b638ab0fad tests: M1 dis34 round-trip 2026-11-11 08:43:17 +00:00
arti 6f776e1758 tests: pin the M1 .bss origin and second pointer marker 2026-11-11 08:39:58 +00:00
arti e0a721e3e1 tests: M1 GSPA/MAME smoke test 2026-11-11 08:31:19 +00:00
arti e081921b72 plan: M1 minimal TMS34010 backend 2026-11-11 06:47:05 +00:00
117 changed files with 11647 additions and 103 deletions

No files matched your search

+630 -59
View File
@@ -3,56 +3,581 @@
A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of
upstream LLVM. upstream LLVM.
**Status: M0.** Clang understands the `tms34010-unknown-none` target, the TI **Status: M4.** `llc -march=tms34010` emits GSPA-compatible assembly using the
data model, and bit-addressed pointers. There is **no machine-code backend TI ABI: integer/pointer arguments are pushed on the program stack, the result is
yet**: M0 stops at LLVM IR. The backend, TI ABI, ROM calls, Xlib and real returned in `A8`, frames use `A13`/`A14`/`A15`, and functions end in `RETS 2`
clients are M1–M5 and are separate plans (see the design spec). (`RETS 0` without arguments). M4 adds function pointers and `CALL Rs`, 32-bit
multiply/divide, zero-extending narrow loads, module-unique private labels,
`i8`/`i16` fixed arguments and the contiguous-stack varargs form, the X11R4
`XImage`/`XEvent` layouts, the full image/event client, the corrected 30-entry
ROM symbol table, and the pinned termtris full-game integration. It is still
**assembly-only**: no object writer, no `clang -c`, and no native COFF;
GSPA/GSPLNK under DOSBox produce the COFF. Floats, `long long`, general
varargs, bitfields, structs by value, narrow returns and native COFF are M5.
Branch `m4` is the current milestone snapshot in both repositories; the
default branch is `m3` until the controller/user updates it after the final
review. Branch `m3` remains the previous snapshot, `m2` is the M2 tip and
`m1` the reproducible M1 snapshot.
- Design: [`docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md`](docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md) - 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) - 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)
- M4 plan: [`docs/superpowers/plans/2026-11-12-tms34010-m4-xlib-image-events-and-termtris.md`](docs/superpowers/plans/2026-11-12-tms34010-m4-xlib-image-events-and-termtris.md)
- Spike: [`spike/REPORT-23.md`](spike/REPORT-23.md) (current, LLVM 23.1.5); - Spike: [`spike/REPORT-23.md`](spike/REPORT-23.md) (current, LLVM 23.1.5);
[`spike/REPORT.md`](spike/REPORT.md) (historical, 19.1.7) [`spike/REPORT.md`](spike/REPORT.md) (historical, 19.1.7)
## Repositories ## Repositories
- **Compiler fork:** <https://git.wut.ee/opencode.siil.arti.ee/llvm-tms34010> - **Compiler fork:** <https://git.wut.ee/opencode.siil.arti.ee/llvm-tms34010>
(branch `tms34010`) — the full buildable LLVM 23.1.5 tree with the TMS34010 (branch `m4`) — the full buildable LLVM 23.1.5 tree with the TMS34010
target and clang bit-address changes. target, clang bit-address changes and the M2/M3/M4 backend. Branch `m3`
(the repository default) is the previous snapshot; branch `m2` is the M2
tip and 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> - **This project:** <https://git.wut.ee/opencode.siil.arti.ee/tms-c-compiler>
— plans/specs, VT1200 integration tooling, `tms-bitaddr`, `tms-clang`, (branch `m4`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`,
symbols and the spike. `tms-clang`, symbols, the M2/M3/M4 harnesses and the spike. Branch `m3`
(the repository default) is the previous snapshot; branch `m2` 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 LLVM changes. `patches/llvm` The compiler fork is the canonical source for all target code. `patches/llvm`
here is a lightweight mirror used by `scripts/build-llvm.sh`, so the project here is a **frozen M0 mirror** used by the default upstream+patches build;
can be built without cloning 3 GB of LLVM; keep it in sync with the fork's M2/M3/M4 do not change it.
`tms34010` branch.
## What M0 provides ## What M4 provides
- `clang --target=tms34010-unknown-none`: 32-bit bit-addressed pointers, M4 closes the remaining Xlib path to a real image/event client and a full-game
8-bit signed `char`, 16-bit `short`, 32-bit `int`/`long`, packed structs, integration, still assembly-only:
`sizeof(struct { char; int; void*; }) == 9`.
- Pointer arithmetic scaled by 8; pointer difference divided by 8 - **Function pointers and `CALL Rs`.** A function pointer is a 32-bit bit
(`char *` → `/8`, `int *` → `/32`), while `(int)(p+1) - (int)p` stays an address; an indirect call lowers to the one-word register call `CALL Rs`
integer subtraction. (GSPA `0x0920`, `dis34` `call aN`), with the same old-STK save, argument
- `tools/tms-bitaddr`: GEP `--scale` and late `--normalize` passes, including pushes, `SETF` fixup and `RETS 2` as a direct call. Direct calls remain
constant-expression GEPs. `CALLA _sym`. `m4-fnptr.ll` + `scripts/tests/m4/fnptr.sh`.
- `scripts/tms-clang`: the M0 pipeline - **32-bit multiply/divide.** `MPYS`/`MPYU`/`DIVS`/`DIVU` with an odd
`clang -O0` → `tms-bitaddr --scale` → `opt -O2` → `tms-bitaddr --normalize`. destination (the even form is the `Rd:Rd+1` pair and would clobber the next
- Lit tests for the triple, data model and pointer difference; an integration register); remainder is quotient + `MPYS` + `SUB`. There is no compiler
test over a real Xlib client IR. multiply/divide helper in `RTS.LIB`; `_div` is only the C `div()` wrapper.
`m4-mul.ll` + `scripts/tests/m4/mul.sh`.
- **Zero-extending and extending narrow loads.** `ZEXTLOAD`/`EXTLOAD` i8/i16
lower to the sign-extending load plus `ANDI 255`/`ANDI 65535`; an i1 load is
a byte load masked to one bit. `m4-zext.ll`, `m4-anyext.ll`,
`m4-i1load.ll`.
- **Module-unique private labels.** Every local symbol gets a custom 64-bit
string-hash token (FNV-style, not canonical FNV-1a), so two TUs with literals
no longer collide in GSPA's flat table; public names are unchanged.
`m4-private.ll` and the two-TU `scripts/tests/m4/private.sh`.
- **`i8`/`i16` fixed arguments.** Promoted to a 4-byte stack slot; the caller
sign/zero-extends per the parameter attribute and the callee truncates.
`i8`/`i16`/`i64`/float/aggregate returns remain rejected. `m4-arg8.ll`.
- **Contiguous-stack varargs.** Only the `&last_fixed` walk used by termtris'
`sprintf`; no `va_list`/`va_start`/`va_arg`. Zero-fixed and i8/i16-anchor
definitions are rejected. Static variadic definitions can lose `...` under
`-O2` dead-argument elimination; external termtris `sprintf` is unaffected.
`m4-varargs.ll`, `m4-reject-vararg0.ll`,
`m4-reject-vararg-narrow{,16}.ll`, `scripts/tests/m4/varargs.sh`.
- **Driver policies.** `scripts/tms-clang` passes `-replexitval=never` (the
i33 exit-value multiply is unselectable on this i64-less target) and
`-switch-to-lookup=false` (a late byte-offset GEP after `--scale` would be
read as a bit offset, an 8x address bug). Both are driver policies because
`opt` has no target machine.
- **X11R4 `XImage`/`XEvent`/`XErrorEvent` layouts and the image client.** The
real 32-bit packed offsets: `XImage` data 16, byte_order 20, bytes_per_line
40, bits_per_pixel 44, f 64, `sizeof` 88; `XEvent` 96; `XErrorEvent` 20
(with the explicit X11R4 object-size compatibility pad); `Display.screens`
byte 0x90 / bit 0x480, `Screen.root` byte 0x8 / bit 0x40. The client draws a
1-bit LSBFirst checker through an aligned pixmap + `XCopyArea` (12-pixel
phase) and 15 bands of 32 rows (the ROM request buffer is 16 KiB), with
`.bss` at `0x01A00000` and `prog` at `0x018A0000`. `XCopyArea` copies
`IMG_W - 12` = 628 columns, so the rightmost 12 source columns (the last
visible cell's final 12 px) are not displayed; the last visible cell is
20 px, not 32. It asserts the checker pixels, the real LK401 `q`
(keycode 193 / keysym `0x71`), window focus and zero X errors in MAME.
- **Corrected 30-entry v2.2 symbol table.** `XMapWindow` is `0x3FD64F60`
(MapWindow opcode 8); `0x3FD645B0` is `XRaiseWindow` (ConfigureWindow
opcode 12, `CWStackMode=Above`). The XG block and the image/error-handler
entries are validated by `scripts/m4/validate-syms.sh` and
`scripts/tests/m4/symbols.sh`.
- **Pinned termtris.** The full game builds with this compiler and the 1987
GSPA/GSPLNK + `rts.lib`, at the pinned `vt1200-termtris` commit, and runs
board/move/panels/quit in MAME with real key injection, the `0x40000`-bit
`prog` region and the required thresholds. The tree is test-only (GPLv3).
- **Differential goldens.** The 1987 TI compiler's function-pointer and
image/struct-offset assembly is stored under `scripts/tests/m4/diff/`; the
check compares ABI shape without running DOSBox.
## What M3 provides (branch `m3`)
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 on the `m3` branch (46 with the M2 suite); M4
removes five obsolete M2 refusal tests (`m2-reject-arg8`,
`m2-reject-call-arg8`, `m2-reject-call-indirect`, `m2-reject-call-vararg`,
`m2-reject-vararg`) and `m3-reject-zext.ll`, and adds 18 `m4-*` tests:
46 - 5 - 1 + 18 = 58 total with the M2 suite.
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 adds
multiply/divide and moves the `-replexitval=never` policy into
`scripts/tms-clang`; see "What M4 provides".
## 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/M4) |
|---|---|---|
| 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 M4 gates
These are the documented commands on branch `m4`; all must pass. The MAME
gates are slow and are run once.
```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: 58 passed (32 m2-* + 8 m3-* + 18 m4-*)
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean/llvm/test/CodeGen/TMS34010 --filter='(m2|m3|m4)-'
# 3. M4 image/event client in MAME (also runs layout.sh + validate-syms.sh)
scripts/tests/m4/run.sh
# -> m4 xshow: PASS (1-bit image + real q key + window focus + 0 X errors)
# 4. Linked .text decoded with dis34 (ROM stubs, CALL A?, SETF, RETS, checker)
scripts/tests/m4/dis-roundtrip.sh
# -> M4 dis34 round-trip: PASS
# 5. Layouts, the 30-entry symbol table, and the correction negative control
scripts/tests/m4/layout.sh
scripts/tests/m4/symbols.sh
# 6. The pinned termtris full-game integration
scripts/tests/m4/termtris.sh
# -> m4 termtris: PASS (board/move/panels/quit; .../termtris_*_3200.png)
# 7. Differential TI-compiler function-pointer/image ABI check (no DOSBox)
scripts/tests/m4/diff/check.sh
# -> M4 diff: PASS (goldens + negative controls)
# 8. The smaller M4 runtime gates
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
# 9. M2/M3 regressions and the GEP tool tests
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
scripts/tests/m3/layout.sh
scripts/tests/m2/run.sh
scripts/tests/m2/dis-roundtrip.sh
scripts/tests/m2/diff/check.sh
tools/tms-bitaddr/tests/run.sh
# -> tms-bitaddr: PASS
```
`scripts/tests/m4/run.sh` first runs `layout.sh` and
`scripts/m4/validate-syms.sh`, then compiles `scripts/m4/xshow.c` and
`scripts/m4/xgfix.c` with `scripts/tms-clang -O2`, runs
`llc -march=tms34010 -O0 -filetype=asm`, generates the ROM import stubs,
concatenates `scripts/m4/xshow-entry.asm`, assembles/links with GSPA/GSPLNK
2.00 under DOSBox, loads the COFF in MAME, injects the real LK401 `Q`, and
requires the checker pixels, `quit=1`, keycode 193 / keysym `0x71`,
`xerrors=0`, focus `0x400001` and the last event KeyPress. The 12-pixel
prefix is checked as the preceding checker cell in bands 0 and 1.
`scripts/tests/m4/termtris.sh` runs the pinned tree through
`scripts/m4/termtris/build.sh` and `play_keys.sh`; it enforces `loops >= 100`
for all four modes, `events >= 6` for move/panels/quit (`>= 2` for board),
`quit = 1` only for quit, focus = the created termtris window,
`status=0 (OK)` and a non-empty screenshot each time. The vendored tree is
test-only (GPLv3).
`scripts/tests/m4/diff/check.sh` is the new CI golden check. It compiles our
function-pointer and image equivalents with `scripts/tms-clang` + `llc` and
compares ABI shape with `scripts/tests/m4/diff/ti-fnptr.s` and
`ti-image.s`: an indirect `CALL A?`, the old-STK save, argument/return/`RETS
2`, ten `XCreateImage` and ten `XPutImage` pushes (the plan's 10/9 is a typo;
`XPutImage` takes display, drawable, gc, image, src_x, src_y, dest_x, dest_y,
width, height), nine `XCreateSimpleWindow` pushes, the leading
constant/Display-last order, and the Display/Screen/XImage bit displacements.
It then mutates copies of the goldens (removing the indirect call and
reversing the `XPutImage` argument pushes) and requires the check to fail;
the stored goldens are not modified. The goldens were generated once with
GSP C Compiler 2.01 under DOSBox; CI never runs DOSBox.
Paths are overridable with `TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`,
`MAME`, `DOSBOX`, `SDK_IMG`, the per-gate work/COFF variables and `M4_SYMS`.
## Run the M3 gates (branch `m3`)
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 (branch `m2`)
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.
### M3 carry-forwards in M4
M4 closes three of the five final-M3 prerequisites: zero-extending loads
replaced the reject test, private labels are module-unique, and unsigned/`<=`
runtime coverage exists. 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. Those M3-strength fixes remain
test-hardening follow-ups; the M3 gates pass as-is. See the M4 feature list
above.
## 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 and MMTM/MMFM
register-list saves are M5 (the current per-register system-stack pushes
have the identical ABI layout).
- **Floating point 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 used by termtris `sprintf`.
- **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.
- **Deeper libc/runtime coverage:** M5 owns the broader `RTS.LIB` integration.
The pinned termtris tree is test-only (GPLv3) and is not part of the compiler
or project distribution.
## Repository layout ## Repository layout
```text ```text
build/llvm-src/ local fork (ignored; created by build-llvm.sh or manually) build/llvm-clean/ compiler fork checkout used for M2/M3/M4 (ignored)
build/llvm-build/ Ninja build of the fork (ignored) build/llvm-clean-build/ its Ninja build tree (ignored)
build/llvm-clean*/ clean CI clone/build (ignored) build/llvm-src/ M0 working fork (ignored)
patches/llvm/ the complete fork delta against llvmorg-23.1.5 patches/llvm/ frozen M0 patch series against llvmorg-23.1.5
tools/tms-bitaddr/ GEP scale/normalize tool tools/tms-bitaddr/ GEP scale/normalize tool
scripts/tms-clang M0 pipeline wrapper 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/m4/ M4 harness: unsigned/private/mul/fnptr/varargs, XImage client,
XG fix, termtris build, symbols validator
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/tests/m4/ M4 gates (run/dis-roundtrip/layout/symbols/termtris and the
unsigned/private/mul/fnptr/varargs gates)
scripts/tests/m4/diff/ TI function-pointer/image goldens + the differential check
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/tests/m0/ M0 integration tests
scripts/build-llvm.sh clean clone + patches + build scripts/build-llvm.sh fork or upstream+patches build
scripts/fetch-vt1200.sh fetch the VT1200 RE tree at its pinned commit scripts/fetch-vt1200.sh fetch the VT1200 RE tree at its pinned commit
spike/ measurement corpus and reports spike/ measurement corpus and reports
symbols/ per-ROM import tables (v2.2 subset) symbols/ per-ROM import tables (v2.2 subset)
@@ -63,10 +588,16 @@ docs/ design and plans
## Prerequisites ## Prerequisites
`cmake`, `ninja-build`, `ccache`, a C++17 host compiler (GCC 14 is used here), `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. On Debian: Python 3, and about 20 GB of disk for the LLVM source and build. The M2/M3/M4
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 termtris gate also needs the pinned
`vendor/vt1200-termtris` tree (test-only). 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 ```sh
sudo apt-get install -y cmake ninja-build ccache sudo apt-get install -y cmake ninja-build ccache dosbox mtools
``` ```
## Fetch the VT1200 assets ## Fetch the VT1200 assets
@@ -78,54 +609,94 @@ Xlib call sequence, `as34`, `dis34`, SDKs, manuals). It is not vendored:
./scripts/fetch-vt1200.sh # -> vendor/vt1200, pinned 17dfd24... ./scripts/fetch-vt1200.sh # -> vendor/vt1200, pinned 17dfd24...
``` ```
## Build the pinned fork ## Build the compiler
### Compiler fork (M4, the normal path)
The fork checkout already exists at `build/llvm-clean` on `m4`. Build it in
place:
```sh
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m4 ./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=m4 ./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 ```sh
./scripts/build-llvm.sh ./scripts/build-llvm.sh
# binaries: build/llvm-clean-build/bin/{clang,opt,llvm-config,FileCheck,llvm-lit}
``` ```
The script clones `llvmorg-23.1.5`, applies `patches/llvm/*.patch`, and builds This mode clones `llvmorg-23.1.5`, applies `patches/llvm/*.patch`, and builds
`clang`/`opt` with X86 (host) and TMS34010. A first build takes roughly `build/llvm-clean-build`. It owns and resets only a plain `llvm-project` clone:
20–40 minutes; rebuilds are incremental. if `build/llvm-clean` has some other origin (for example the M2/M3/M4 fork),
it refuses rather than destroying it. Use fork mode for that checkout.
For development, `build/llvm-src` is the working fork and `build/llvm-build` is `scripts/export-patches.sh` regenerates the committed M0 patch series with
its build tree. `scripts/export-patches.sh` regenerates the committed patch `git diff` (including new files via `git add -N`); it is not used for
series with `git diff` (including new files via `git add -N`). M1/M2/M3/M4.
## Build the M0 tool and run the tests ## Build the M0 support tools
The M2/M3/M4 pipeline uses `tms-bitaddr` and `tms-clang`:
```sh ```sh
LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \ LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \
tools/tms-bitaddr/build.sh tools/tms-bitaddr/build.sh
```
M0 regression test (optional in M2/M3/M4):
```sh
TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \ TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \
TMS_OPT=$PWD/build/llvm-clean-build/bin/opt \ TMS_OPT=$PWD/build/llvm-clean-build/bin/opt \
TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \ TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \
./scripts/tests/m0/run.sh ./scripts/tests/m0/run.sh
# -> M0 pipeline: PASS # -> M0 pipeline: PASS
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean-build/tools/clang/test --filter='TMS34010'
# -> 3 passed
``` ```
## Use ## Emit assembly by hand
```sh ```sh
scripts/tms-clang -O2 -I spike -o /tmp/client.ll spike/xclient.c 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 output is LLVM IR in which every address is a bit address. It is not The resulting function loads its arguments at `FP(-32)` and `FP(-64)`, returns
assembly or an object file; `llc` support is M1. 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.
## What is deliberately missing ## Later milestones
- TMS34010 instruction selection, registers, frame lowering, asm/MC output. M5 adds the native COFF path (`MCCodeEmitter`/object writer), floats,
- The TI ABI: A8 return, A13/A14/A15 dual stack, `RETS 2`, `FS1 = 32`. `long long`, general varargs (`va_list`), bitfields, structs by value and the
- ROM import symbols beyond the subset in `symbols/`. `JR`/branch-relaxation code-size pass, plus MMTM/MMFM register-list saves and
- Xlib headers/clients on the target, `memcpy`/`byval`/varargs/float/bitfield deeper libc/runtime coverage. 2ine r5.01 is a later speed-up for the
lowering, native COFF. GSPA/GSPLNK step.
These are M1–M5 in the design spec, written as separate plans because their
tasks depend on what M0/M1 measure.
@@ -0,0 +1,386 @@
# TMS34010 M1 — Minimal Backend Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `llc -march=tms34010` emits GSPA-compatible assembly for a leaf `int add(int, int)`, `int load(int *)` and `char` load/store, and that assembly assembles, links and runs in MAME. No TI ABI yet.
**Architecture:** Fill in the M0 target skeleton with TableGen register/instruction definitions, a minimal SelectionDAG selector, a provisional register-argument test convention, and an asm printer for GSPA syntax. Use `llc -filetype=asm` and the existing GSPA/gsplnk + COFF loader path. Object emission and the full TI ABI are later milestones.
**Tech Stack:** LLVM/Clang 23.1.5 fork (`llvm-tms34010`, branch `tms34010`), C++17, CMake/Ninja, `llc`, GSPA/gsplnk under 2ine, MAME, `dis34`, `llvm-lit`/FileCheck.
**Spec:** `docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md` (§7.4–§7.6, M1)
## Global Constraints
- Compiler work happens in **`llvm-tms34010`**, branch `tms34010` (clone `build/llvm-m1`). Project plans/tests live in **`tms-c-compiler`**, branch `m1`.
- **M1 calling convention is a test convention, not the TI ABI:** integer/pointer arguments in `A0, A1, A2, A3`; return value in `A0`; leaf functions only; no calls, no stack arguments, no callee-saves, no structures/floats/varargs. M2 replaces this with A8/A13/A14/A15, `RETS 2`, and `FS1 = 32`.
- M1 compiles at `-O2`; any function needing a stack frame, a call, a structure, float, or an unsupported operation is a hard error, not a silent fallback.
- Output is **assembly only**: `llc -march=tms34010 -filetype=asm`. No LLVM object writer, `MCCodeEmitter` or linker in M1; GSPA assembles, gsplnk links, the existing COFF loader loads.
- M1 assembles with the **proven 1987 GSPA/GSPLNK 2.00 under DOSBox** (the path exercised by `vendor/vt1200/harness/c-hello/build.sh`), not the external 2ine r5.01 tools. 2ine is an M2 speed-up. The asm printer targets GSPA 2.00 syntax.
- GSPA syntax: hex constants `0…h`, lowercase mnemonics accepted, `movi` long form for 32-bit constants, field-select `,1` on 32-bit `MOVE`; the M1 entry sets `SETF 32,0,1` before calling compiled code.
- Symbols use the GSPA/TI leading underscore (`_add`); code is linked at a fixed bit address.
- No ROM/Xlib imports, no `malloc`, no `strlen`, no TI runtime in M1. M2 owns those.
- The M0 `patches/llvm` mirror is frozen; M1 uses the full compiler repo, not the patch mirror.
- Tests: LLVM lit/FileCheck for asm; a GSPA/2ine assemble-link step; a MAME run with marker words; a `dis34` round-trip of the emitted asm.
## Review Focus
1. **Test convention leakage:** no code path may claim TI ABI compatibility; calls/stack args must fail loudly. Tests in Task 2 and Task 4.
2. **`-O2` stack/spill/frame:** leaf functions must not silently use an unimplemented frame; `FrameLowering` must reject frame indices it cannot lower. Tests in Task 2/3.
3. **GSPA syntax and field select:** 32-bit `MOVE` must carry `,1`; `MOVB` is the 8-bit access and sign-extends. Tests in Task 3/4.
4. **Symbol/link address:** `_add` label, absolute `CALLA`, and bit-address placement must be consistent with the linker. Test in Task 4.
5. **Asm/encoding agreement:** GSPA's encoding of the emitted asm must match `dis34` for `ADD`, `MOVE`, `MOVB`, `RETS`; a mismatch means the asm printer is wrong. Test in Task 5.
---
### Task 1: M1 compiler checkout, build, and failing `llc` test
**Files:**
- Create: `build/llvm-m1/llvm/test/CodeGen/TMS34010/m1-add.ll` (compiler repo)
- Create: `build/llvm-m1/llvm/test/CodeGen/TMS34010/m1-call.ll`, `m1-frame.ll`, `m1-late-gep.ll` (compiler repo)
- Modify: `patches/llvm` is **not** changed in M1.
**Interfaces:**
- Consumes: M0 `tms34010` branch.
- Produces: a built `llc` that lists `tms34010`; the failing test that every later task makes pass.
- [ ] **Step 1: Clone the compiler repo and build**
```sh
mkdir -p build
git clone --depth 1 --branch tms34010 \
gitea@git.wut.ee:opencode.siil.arti.ee/llvm-tms34010.git \
build/llvm-m1
cmake -S build/llvm-m1/llvm -B build/llvm-m1-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/llvm-m1-build llc opt clang llvm-config FileCheck count not
```
Expected: build succeeds; `build/llvm-m1-build/bin/llc --version` lists `tms34010`.
- [ ] **Step 2: Write the failing asm test**
`build/llvm-m1/llvm/test/CodeGen/TMS34010/m1-add.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @add(i32 %a, i32 %b) {
entry:
%r = add i32 %a, %b
ret i32 %r
}
; CHECK-LABEL: _add:
; CHECK: ADD A{{[0-9]+}}, A{{[0-9]+}}
; CHECK: RETS 0
```
- [ ] **Step 3: Run it to verify it fails**
```sh
build/llvm-m1-build/bin/llvm-lit -v \
build/llvm-m1-build/test/CodeGen/TMS34010 --filter='m1-add'
```
Expected: FAIL — `llc` reports that the target cannot emit assembly / no instruction selector (the M0 `addPassesToEmitFile` is still a stub).
- [ ] **Step 4: Commit the failing test in the compiler repo**
```sh
git -C build/llvm-m1 add llvm/test/CodeGen/TMS34010/m1-add.ll
git -C build/llvm-m1 commit -m "tms34010: add failing M1 add codegen test"
```
### Task 2: Minimal `add` codegen (registers, ISel, asm printer)
**Files:**
- Create/modify under `build/llvm-m1/llvm/lib/Target/TMS34010/`:
- `TMS34010.td`, `TMS34010RegisterInfo.td`, `TMS34010InstrInfo.td`
- `TMS34010RegisterInfo.{h,cpp}`, `TMS34010InstrInfo.{h,cpp}`
- `TMS34010ISelLowering.{h,cpp}`, `TMS34010ISelDAGToDAG.cpp`
- `TMS34010FrameLowering.{h,cpp}`
- `TMS34010AsmPrinter.cpp`, `TMS34010MCInstLower.{h,cpp}`
- `MCTargetDesc/TMS34010MCAsmInfo.{h,cpp}`, `TMS34010InstPrinter.{h,cpp}`, `TMS34010MCTargetDesc.{h,cpp}`
- `TMS34010Subtarget.{h,cpp}`, `TMS34010TargetMachine.{h,cpp}`, CMake files
- Test: `build/llvm-m1/llvm/test/CodeGen/TMS34010/m1-add.ll`
**Interfaces:**
- Consumes: `llc`/M0 target skeleton.
- Produces: `TMS34010TargetLowering` with a provisional `LowerFormalArguments`/`LowerReturn`; `TMS34010InstrInfo` with `ADD`/`RETS`/`MOVI`; `TMS34010AsmPrinter` emitting GSPA asm; `TMS34010Subtarget` wired into a `CodeGenTargetMachineImpl`.
- [ ] **Step 1: Define registers and the minimal instructions**
`TMS34010RegisterInfo.td`: define `A0`–`A15` and `B0`–`B15` (B regs reserved for a later task), an `ARegs` register class (use A0–A7 in M1 patterns). `TMS34010InstrInfo.td`: define `MOVI` (32-bit immediate, 3 words), `ADD` (register), `RETS` (with N immediate), `CLR`, `NOP`; include `AsmString` for GSPA syntax. Keep the encodings in the definitions, but M1 only needs the asm printer.
- [ ] **Step 2: Wire the target machine and subtarget**
Switch `TMS34010TargetMachine` to `CodeGenTargetMachineImpl` with the M0 data layout string, `TMS34010Subtarget` and `TargetLoweringObjectFileELF`; implement `createPassConfig` (`addInstSelector`, asm printer), `getSubtargetImpl`, `getObjFileLowering`, and `addInstSelector` returning a `TMS34010ISelDag`. Register `MCAsmInfo`, `MCInstrInfo`, `MCRegisterInfo`, `MCSubtargetInfo`, `MCInstPrinter`; no code emitter.
- [ ] **Step 3: Implement the provisional convention and patterns**
`TMS34010TargetLowering`:
- `addRegisterClass(MVT::i32, &TMS34010::ARegsRegClass)`, `computeRegisterProperties`.
- `LowerFormalArguments`: `i32`/pointer args 0..3 → `CCValAssign::getReg` `A0..A3`; more args → `llvm_unreachable`/`report_fatal_error("M1 supports at most four register arguments")`. M1 accepts only 32-bit scalar/pointer arguments; narrow C values are promoted/truncated in IR.
- `LowerReturn`: `i32`/pointer result → `A0`; `void` is allowed; anything else → `report_fatal_error`.
- `LowerCall`: `report_fatal_error("M1 has no calls; M2 adds the TI ABI")`.
- `CanLowerReturn` only for `i32`/pointer.
TableGen patterns: `add i32` → `ADD`; `ret` with A0 → `RETS 0`; `i32` constants → `MOVI`. `TMS34010FrameLowering` has no prologue/epilogue and `getFrameIndexReference` calls `report_fatal_error` for any frame index (M1 has no stack).
M1 is asm-only: define the instruction encodings in TableGen for future use, but do not add an `MCCodeEmitter` or object writer. GSPA/dis34 validate the asm in Task 5.
`m1-call.ll` pins the call refusal:
```llvm
; RUN: not llc -march=tms34010 -O2 -filetype=asm < %s 2>&1 | FileCheck %s
; CHECK: M1 has no calls
declare i32 @callee(i32)
define i32 @caller(i32 %x) {
%r = call i32 @callee(i32 %x)
ret i32 %r
}
```
`m1-late-gep.ll` pins the §7.3.1 contract at `llc`: a surviving GEP offset is in bits, not bytes.
```llvm
; RUN: llc -march=tms34010 -O0 -filetype=asm < %s | FileCheck %s
define i32 @late(ptr %p) {
%g = getelementptr i8, ptr %p, i32 24
%v = ptrtoint ptr %g to i32
ret i32 %v
}
; CHECK: ADDI 24
; CHECK-NOT: ADDI 192
```
`m1-frame.ll` pins the no-stack refusal:
```llvm
; RUN: not llc -march=tms34010 -O0 -filetype=asm < %s 2>&1 | FileCheck %s
; CHECK: M1 has no stack frame
define i32 @frame(i32 %n) {
%a = alloca i32
store i32 %n, ptr %a
%v = load i32, ptr %a
ret i32 %v
}
```
- [ ] **Step 4: Run the lit tests to verify they pass**
```sh
build/llvm-m1-build/bin/llvm-lit -v \
build/llvm-m1-build/test/CodeGen/TMS34010 --filter='m1-(add|call|frame|late-gep)'
```
Expected: PASS — the `add` asm, the call/alloca refusals, and `ADDI 24` (not 192) for the bit-offset GEP.
- [ ] **Step 5: Commit**
```sh
git -C build/llvm-m1 add llvm/lib/Target/TMS34010 llvm/test/CodeGen/TMS34010/m1-add.ll \
llvm/test/CodeGen/TMS34010/m1-call.ll llvm/test/CodeGen/TMS34010/m1-frame.ll \
llvm/test/CodeGen/TMS34010/m1-late-gep.ll
git -C build/llvm-m1 commit -m "tms34010: minimal add codegen with GSPA asm"
```
### Task 3: Loads, stores and 8-bit accesses
**Files:**
- Modify: `build/llvm-m1/llvm/lib/Target/TMS34010/TMS34010InstrInfo.td`, `TMS34010ISelLowering.cpp`, `TMS34010ISelDAGToDAG.cpp`, `TMS34010FrameLowering.cpp`
- Create: `build/llvm-m1/llvm/test/CodeGen/TMS34010/m1-load.ll`, `m1-char.ll`
**Interfaces:**
- Consumes: Task 2 selector/asm printer.
- Produces: `MOVE` (32-bit load/store) and `MOVB` (8-bit load/store) patterns with GSPA field select.
- [ ] **Step 1: Write the failing tests**
`m1-load.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @load(ptr %p) {
%v = load i32, ptr %p
ret i32 %v
}
; CHECK-LABEL: _load:
; CHECK: MOVE *A{{[0-9]+}}, A{{[0-9]+}}, 1
; CHECK: RETS 0
```
`m1-char.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @loadb(ptr %p) {
%v = load i8, ptr %p
%r = sext i8 %v to i32
ret i32 %r
}
define void @storeb(ptr %p, i32 %v) {
%t = trunc i32 %v to i8
store i8 %t, ptr %p
ret void
}
; CHECK-LABEL: _loadb:
; CHECK: MOVB *A{{[0-9]+}}, A{{[0-9]+}}
; CHECK-LABEL: _storeb:
; CHECK: MOVB A{{[0-9]+}}, *A{{[0-9]+}}
```
- [ ] **Step 2: Run them to verify they fail**
```sh
build/llvm-m1-build/bin/llvm-lit -v \
build/llvm-m1-build/test/CodeGen/TMS34010 --filter='m1-(load|char)'
```
Expected: FAIL — no load/store patterns or `MOVE`/`MOVB` definitions.
- [ ] **Step 3: Add `MOVE`/`MOVB` and selection**
Define `MOVE` with a field-select immediate and `MOVB` in the `.td`; add DAG patterns for `load i32`/`store i32` → `MOVE` with `,1`, and `load i8`/`store i8` → `MOVB`. Set `i8`/`i16` load/store legalization to custom where needed. Keep `-O2` leaf-only; any frame index remains a hard error.
- [ ] **Step 4: Run the tests to verify they pass**
Run the same lit filter. Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git -C build/llvm-m1 add llvm/lib/Target/TMS34010 llvm/test/CodeGen/TMS34010/m1-load.ll llvm/test/CodeGen/TMS34010/m1-char.ll
git -C build/llvm-m1 commit -m "tms34010: add MOVE/MOVB load-store codegen"
```
### Task 4: Assemble, link and run in MAME
**Files:**
- Create: `scripts/m1/add.c`, `scripts/m1/load.c`, `scripts/m1/late.c`, `scripts/m1/const_gep.c`, `scripts/m1/entry.asm`, `scripts/m1/link.cmd`, `scripts/m1/run.sh` (project repo)
- Create: `scripts/tests/m1/run.sh` (project repo)
- Reuse: `vendor/vt1200`, `dwt/loader`, `harness/c-hello` patterns
**Interfaces:**
- Consumes: `llc` asm from Tasks 2–3; GSPA/GSPLNK 2.00 under DOSBox; existing COFF loader.
- Produces: a MAME run whose marker words prove `add(20,22) == 42`, a word load, the late-GEP offset 24, and a `&arr[1]` constant-GEP relocation.
- [ ] **Step 1: Write the harness**
`scripts/m1/entry.asm` sets `SETF 32,0,1`, loads `A0 = 20`, `A1 = 22`, `CALLA _add`, stores `A0` at marker `0x018A8000`, repeats a `load` test, then calls `_second` and stores the returned `&arr[1]`, and `RETS`. Link `.text` at bit `0x018A0000` with `gspa`/`gsplnk` 2.00 under DOSBox and a `link.cmd` written with CRLF.
`add.c` is `int add(int a,int b){return a+b;}`, `load.c` is `int load(int *p){return *p;}`, `late.c` is `int late(char *p, char *q){return (char *)((unsigned)p + 24) == q;}`, and `const_gep.c` is `static int arr[4]; int *second(void){return &arr[1];}`. The harness pipeline is:
```sh
TMS_CLANG=$PWD/build/llvm-m1-build/bin/clang \
TMS_OPT=$PWD/build/llvm-m1-build/bin/opt \
TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \
scripts/tms-clang -O2 -o /tmp/add.ll scripts/m1/add.c
build/llvm-m1-build/bin/llc -march=tms34010 -O0 -filetype=asm \
/tmp/add.ll -o /tmp/add.s
```
`llc -O0` is deliberate: the IR is already scaled/normalized by `tms-clang`, and M1 does not want a second optimization pipeline recreating GEPs. The target must still treat a surviving GEP offset as bits (`m1-late-gep.ll`). Repeat for `load.c`, `late.c` and `const_gep.c`; check the `late.s` for `ADDI 24` (not 192), and the `const_gep.s` for `@arr+32` (bits, not `+4` bytes). Concatenate with `entry.asm`, assemble/link, and load via `dwt/loader/loader.lua`/MAME.
The SDK is the 1987 DOSBox path:
```sh
sudo apt-get install -y dosbox mtools
IMG="$PWD/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img"
mkdir -p build/m1-sdk
mcopy -s -n -i "$IMG" "::/A2/*" build/m1-sdk/
mcopy -s -n -i "$IMG" "::/C2/*" build/m1-sdk/
```
Invoke `gspa`/`gsplnk` in DOSBox the way `vendor/vt1200/harness/c-hello/build.sh` does; keep the linker command file CRLF.
- [ ] **Step 2: Run it to verify it fails**
Run `scripts/tests/m1/run.sh`. Expected: FAIL because `llc` does not yet emit the expected asm or the harness is missing.
- [ ] **Step 3: Fix the asm/link integration**
Adjust the asm printer's directives/labels or the linker command as needed; if `late.c`/`const_gep.c` expose missing GEP or global-address lowering, fix it in the compiler repo (target ISel/asm printer), not in the harness. Keep the harness in the project repo. Do not add ROM calls or TI ABI.
- [ ] **Step 4: Run it to verify it passes**
Expected: marker `42` (and the loaded word marker) in MAME; `scripts/tests/m1/run.sh` prints `M1 MAME: PASS`.
- [ ] **Step 5: Commit both repos**
```sh
git -C build/llvm-m1 add llvm/lib/Target/TMS34010
git -C build/llvm-m1 commit -m "tms34010: align asm output with GSPA link path"
git add scripts/m1 scripts/tests/m1
git commit -m "tests: M1 GSPA/MAME smoke test"
```
### Task 5: `dis34` round-trip of the emitted assembly
**Files:**
- Create: `scripts/m1/dis-roundtrip.sh` (project repo)
- Create: `scripts/tests/m1/dis-roundtrip.sh` (project repo)
**Interfaces:**
- Consumes: the linked/extracted `.text` bytes and `tools/smart34010/dis34`.
- Produces: a byte/mnemonic check that GSPA's encoding of the emitted `ADD`/`MOVE`/`MOVB`/`RETS` matches `dis34`.
- [ ] **Step 1: Write the failing check**
The script extracts the linked `.text` bytes, runs `dis34`, and greps for the emitted instructions at the expected addresses. Initially it fails because the harness/asm path is not yet complete.
- [ ] **Step 2: Run it to verify it fails**
Run `scripts/tests/m1/dis-roundtrip.sh`. Expected: FAIL.
- [ ] **Step 3: Reconcile asm and encoding**
If a mnemonic or operand is wrong, fix the asm printer (compiler repo); if the byte extraction/address is wrong, fix the script. Do not add an LLVM object writer or MCCodeEmitter in M1.
- [ ] **Step 4: Run it to verify it passes**
Expected: `M1 dis34 round-trip: PASS`.
- [ ] **Step 5: Commit**
```sh
git add scripts/m1/dis-roundtrip.sh scripts/tests/m1/dis-roundtrip.sh
git commit -m "tests: M1 dis34 round-trip"
```
### Task 6: Document and push M1
**Files:**
- Modify: `README.md` (project repo) — M1 status/commands
- Modify: `build/llvm-m1/TMS34010.md` (compiler repo) — M1 backend/test convention
- Modify: `scripts/build-llvm.sh` (project repo) — optional `LLVM_FORK`/branch path
- Push: compiler repo `tms34010`, project repo `m1`
**Interfaces:**
- Consumes: all previous tasks.
- Produces: a documented M1 snapshot and branches others can clone.
- [ ] **Step 1: Update the docs**
State clearly: M1 is a **test-only register convention**, no TI ABI, no calls/ROM; M2 is the ABI. Document `llc -march=tms34010 -O2 -filetype=asm`, the GSPA/2ine path, and the MAME test. Do not claim `clang -c` or object output works.
- [ ] **Step 2: Run the M1 gates from the documented commands**
Run the lit filter, `scripts/tests/m1/run.sh`, and `scripts/tests/m1/dis-roundtrip.sh` using the documented build path. Expected: all green.
- [ ] **Step 3: Commit and push**
```sh
git -C build/llvm-m1 push origin tms34010
git add README.md docs scripts
git commit -m "docs: M1 minimal backend and test convention"
git push origin m1
```
---
## Execution Handoff
This plan is M1 only. The TI ABI (A8/A13/A14/A15, dual stack, `RETS 2`, `FS1`), ROM imports, Xlib and real clients are M2–M5 and get separate plans. The compiler repo is the canonical source for target code; this project repo holds the plans, wrappers, symbols and integration tests.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,685 @@
# TMS34010 M3 — Control Flow and the First Xlib Window Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** A compiled C program calls the ROM Xlib (`XOpenDisplay`, `XCreateSimpleWindow`, `XMapWindow`, `XSync`), creates and maps a window, and that window appears in MAME; the backend also gains integer control flow (branches, comparisons, loops) for real C.
**Architecture:** M3 keeps the M2 asm-only path and TI ABI. It adds `JAcc`-based branches with GSPA-safe basic-block labels, `CMP`/condition-code lowering for `BRCOND`/`BR_CC`/`SETCC`/`SELECT_CC`, and the integer ALU/shift operations C control flow needs. It then adds a ROM-verified minimal `Display`/`Screen` layout and the four proven Xlib entry points, and an M3 harness that compiles C, links the `JAUC` stubs, loads the COFF in MAME, and checks both ABI markers and a window pixel.
**Tech Stack:** LLVM/Clang 23.1.5 fork (`llvm-tms34010`, branch `m3`), C++17, CMake/Ninja, `llc`, GSPA/GSPLNK 2.00 under DOSBox, COFF loader, MAME, `dis34`, `llvm-lit`/FileCheck, 1987 TI compiler as a differential oracle.
**Spec:** `docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md` (§4.2, §7.3.1, §7.4–§7.7, §7.9, M3, M4)
## Global Constraints
- Compiler work happens in **`llvm-tms34010`**, branch `m3`, created from `m2` at `39512c272d0f`. Use the existing full-fork checkout `build/llvm-clean` and build tree `build/llvm-clean-build`; do not clone another LLVM tree.
- Project plans/tests live in **`tms-c-compiler`**, branch `m3`, created from `m2` at `2d4919d` plus the M3 plan commit. The M2 branches stay the default snapshot until M3 is reviewed; do not merge M2 into `m0`.
- The M2 TI ABI is unchanged: A8 return, A13 FP, A14 STK, A15 SP; reverse argument pushes; first argument at `FP(-32)`; old-STK save/restore; `RETS 2/0`; callee-saved A0–A12 except A8; `SETF 16,0,0` + `SETF 32,0,1` at entry and after every call; `.data` width/alignment rules.
- M3 output is still **assembly only**: `llc -march=tms34010 -filetype=asm`; GSPA/GSPLNK 2.00 under DOSBox; the existing COFF loader. No LLVM object writer, `MCCodeEmitter`, asm parser or linker.
- **Branches use `JAcc` absolute 3-word forms in M3** (`JAUC`/`JAEQ`/`JALT`/…). Do not add `JR` relative branches or branch relaxation in M3; `JR`/relaxation is a later code-size task. GSPA resolves the absolute target.
- **Basic-block labels must be GSPA-valid.** GSPA 2.00 rejects dotted ELF names (`.L`, `_.str`). Set the MC private/linker-private prefixes so block labels are `LBB…`/`L…` with no dot; test a two-block function through GSPA.
- **Flags are modeled with a reserved `ST` pseudo-register:** `CMP`/`CMPI` define it, conditional branches and `SETCC` sequences use it. Do not silently drop the compare-to-branch dependency.
- **CMP operand order:** for `icmp <pred> a, b` the compare must compute `a - b` (the branch condition reads the flags for `a <pred> b`). The signed/unsigned conditions map to the TMS34010 condition codes in `dis34`: `uc/p/ls/hi/lt/ge/le/gt/c/nc/eq/ne/v/nv/n/nn` (0–15).
- M3 scalar control flow: i32/pointer values, signed and unsigned integer compares, `and`/`or`/`xor`/`shl`/`lshr`/`ashr`/`sub`/`neg`/`not`; no switch tables, indirect branches, floats, i64, or function pointers in M3.
- **Xlib is ROM-v2.2 only.** Use the four known-good entries from `symbols/vt1200-v22.syms`: `XOpenDisplay 0x3F9C2570`, `XCreateSimpleWindow 0x3FD41B90`, `XMapWindow 0x3FD64F60`, `XSync 0x3FD9F6B0`. (`0x3FD645B0` is `XRaiseWindow`, not `XMapWindow`; the M4 correction verified it.) The stubs are `JAUC <address>h`; `CALLA _XOpenDisplay` reaches the ROM function.
- **Display/Screen layout is ROM-specific:** xhello's `*a11(0480h)`/`*a10(040h)` are **bit displacements**, so `Display.screens` is the 32-bit pointer at **byte 0x90** (bit `0x480`) and `Screen.root` is the 32-bit root id at **byte 0x8** (bit `0x40`). M3 uses a minimal `scripts/m3/vt1200_xlib.h` with byte pads 0x90/0x8 and `_Static_assert`s, returning the ROM's bit offsets `0x480`/`0x40`; do not assume the host X11R7 `_XDisplay` layout matches the ROM. The full X11R4 `XImage`/`XEvent` headers are M4.
- `XOpenDisplay("0::0")` must run in the loader-created `create_process` child, not the rdebug dispatcher; the M2 `_start`/loader path satisfies this.
- `XCreateSimpleWindow` takes 9 arguments: `display, parent, x, y, width, height, border_width, border, background`; the first argument is at `FP(-32)`, the ninth at `FP(-288)`; `RETS 2`.
- M3 must observe a window, not only markers: the harness creates the window with **background pixel 1 (white)**, maps it, calls `XSync`, and checks a pixel inside the client area in emulated VRAM (base bit `0x04000000`, VRAM pitch 2048 bits on the default VR319 monitor, LSB-first). No fake pass.
- The M2 `m2-*` suite must stay green; M3 adds an `m3-*` suite. Do not regress the single-basic-block ABI tests.
- Tests: LLVM lit/FileCheck for branch/compare/op shapes; a GSPA assemble/link probe for block labels; a MAME run with branch results and a window pixel; a `dis34` round-trip; a TI-compiler golden check for branch/struct/call shape.
## Review Focus
1. **Branch target and label correctness:** GSPA block labels must have no dot; `JAUC`/`JCC` must target the right block; a wrong label jumps into data. Tests in Task 1 (`m3-branch`, GSPA probe) and Task 2 (`m3-loop`).
2. **Compare polarity and signedness:** the `CMP` operand order and `JALT`/`JALO`/`JAHI`/`JAEQ` mapping must match the C predicate; a reversed condition silently returns the wrong value. Tests in Task 2 (`m3-setcc`, `m3-if`, `m3-unsigned`) and Task 4 (`sumto(10)==55`, `max(20,22)==22`, `lt(5,7)==1` in MAME).
3. **Stack/FS invariants on every path:** prologue, multiple return blocks, loops and calls must keep A14/A15 balanced and FS0/FS1 correct; the old-STK/`RETS` rules still apply. Tests in Task 2 (loop returns) and Task 4 (MAME).
4. **Display/Screen offsets and root retrieval:** `Display.screens` at byte 0x90 (bit displacement 0x480), then `Screen.root` at byte 0x8 (bit 0x40); a wrong screen/root pointer yields no window or a crash. Tests in Task 3 (static asserts + TI golden) and Task 4 (window).
5. **Nine-argument `XCreateSimpleWindow` order:** all nine slots, first at `FP(-32)`, last at `FP(-288)`; a wrong order maps the wrong rectangle or parent. Tests in Task 4 (`dis34` push count and MAME window).
6. **The window really appears:** the loader child, the X server, `XSync`, a white client-area pixel; marker-only success is not enough. Test in Task 4.
---
### Task 1: Branch infrastructure, unconditional branches, GSPA-safe block labels
**Files:**
- Modify: `build/llvm-clean/llvm/lib/Target/TMS34010/TMS34010InstrInfo.td`, `TMS34010InstrInfo.cpp`, `TMS34010InstrInfo.h`
- Modify: `build/llvm-clean/llvm/lib/Target/TMS34010/MCTargetDesc/TMS34010MCAsmInfo.cpp`
- Test: `build/llvm-clean/llvm/test/CodeGen/TMS34010/m3-branch.ll`
**Interfaces:**
- Consumes: M2 target, MCInstLower's `MO_MachineBasicBlock` lowering, asm printer.
- Produces: `JUncond` (`JAUC <target>`), a `brtarget` operand, `AnalyzeBranch`/`InsertBranch`/`RemoveBranch`, GSPA-safe block labels.
- [ ] **Step 1: Check out the M3 branches**
The controller creates compiler `m3` from `m2` at `39512c272d0f` and project `m3` from `m2` at `2d4919d` plus the M3 plan commit.
```sh
cd /home/arti/code/tms-c-compiler
git checkout m3
git -C build/llvm-clean checkout m3
git -C build/llvm-clean rev-parse --short HEAD # based on 39512c272d0f
git rev-parse --short HEAD # plan commit on top of 2d4919d
```
If a branch is missing, create it with `git checkout -b m3 m2` (project) or `git -C build/llvm-clean checkout -b m3 m2` (compiler).
- [ ] **Step 2: Write the failing branch test**
`m3-branch.ll`:
```llvm
; RUN: llc -march=tms34010 -O0 -filetype=asm < %s | FileCheck %s
define void @spin() {
entry:
br label %loop
loop:
br label %loop
}
; CHECK-LABEL: _spin:
; CHECK: [[L:LBB[0-9_]+]]:
; CHECK: JAUC [[L]]
```
The self-loop keeps the unconditional branch through code layout; a `br` to the immediate fallthrough can be removed by block placement even at `-O0`. This is a compile-only shape test; the executable branch tests are in Task 2 and Task 4.
- [ ] **Step 3: Run it red**
```sh
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m3-branch'
```
Expected: FAIL — `Cannot select: br` (M2 has no branch).
- [ ] **Step 4: Add `JUncond`, the branch target operand, and block-label prefixes**
In `TMS34010InstrInfo.td`:
```tablegen
def JUncond : TMS34010Inst<(outs), (ins brtarget:$target), "JAUC $target",
[(br bb:$target)]> {
let isBranch = 1;
let isTerminator = 1;
let isBarrier = 1;
let Size = 6;
}
```
`brtarget` is the standard `Target.td` branch-target operand; the `br` pattern selects the unconditional branch. `JAUC` is the GSPA unconditional absolute jump (the M2 ROM stub already uses it; bare `ja` is rejected by GSPA 2.00).
In `TMS34010MCAsmInfo.cpp`, set GSPA-safe label prefixes (the default `.L`/`l` contains a dot):
```cpp
PrivateGlobalPrefix = "L";
LinkerPrivateGlobalPrefix = "L";
```
The base asm printer then emits block labels such as `LBB0_1`; verify the actual spelling with the test and adjust the prefix if needed (no dot, GSPA-valid).
- [ ] **Step 5: Implement the branch hooks**
In `TMS34010InstrInfo`:
- `analyzeBranch(MBB, TBB, FBB, Cond)`: if the last terminator is `JUncond`, set `TBB` to its target and `FBB` to null; leave `Cond` empty. Return false. (M3 branches are self-contained `JAcc` instructions; do not invent a separate condition instruction.)
- `insertBranch(MBB, TBB, FBB, Cond, DL)`: insert `JUncond` for `TBB`; M3 has no conditional branch yet, so `FBB` must be null.
- `removeBranch(MBB)`: remove `JUncond` terminators.
- Keep `getInstSizeInBytes` at the `Size` field (bytes); `JUncond` is 6 bytes.
The MCInstLower already lowers `MO_MachineBasicBlock` to the block symbol; the InstPrinter prints the expression; the `PrivateGlobalPrefix` handles the label text.
- [ ] **Step 6: Run the lit test and the GSPA label probe**
```sh
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m3-branch'
```
Expected: PASS. Then:
```sh
build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm \
build/llvm-clean/llvm/test/CodeGen/TMS34010/m3-branch.ll -o /tmp/m3-branch.s
printf '\t.end\n' >> /tmp/m3-branch.s
# assemble under DOSBox with the M1 recipe; require No Errors, No Warnings
```
The GSPA listing must show the two `JAUC LBB…` instructions and the matching `LBB…:` label, with no `.L`/`_` dotted label.
- [ ] **Step 7: Commit**
```sh
git -C build/llvm-clean add llvm/lib/Target/TMS34010 llvm/test/CodeGen/TMS34010/m3-branch.ll
git -C build/llvm-clean commit -m "tms34010: add JAcc branches and GSPA-safe block labels"
```
### Task 2: Comparisons, conditional branches, SETCC/SELECT, loops, and integer ALU
**Files:**
- Modify: `build/llvm-clean/llvm/lib/Target/TMS34010/TMS34010RegisterInfo.td` (reserved `ST`)
- Modify: `TMS34010InstrInfo.td` (`CMP`, `CMPI`, `JAEQ`…`JANN`, `AND`/`OR`/`XOR`, `SLL`/`SRL`/`SRA`, `SUB`, `NEG`, `NOT`)
- Modify: `TMS34010ISelLowering.cpp/.h` (`LowerBR_CC`, `LowerBRCOND`, `LowerSETCC`, `LowerSELECT_CC`, operation actions)
- Modify: `TMS34010InstrInfo.cpp` (conditional `AnalyzeBranch`/`InsertBranch`/`RemoveBranch`)
- Test: `m3-if.ll`, `m3-setcc.ll`, `m3-loop.ll`, `m3-unsigned.ll`, `m3-ops.ll`
**Interfaces:**
- Consumes: Task 1's `JUncond`, block labels, asm printer.
- Produces: `CMP`/`CMPI` with a reserved `ST` flag register; the 16 `JA<cc>` branch instructions; `SETCC`/`SELECT_CC`/`BRCOND` lowering; ALU/shift patterns.
- [ ] **Step 1: Write the failing tests**
`m3-setcc.ll`:
```llvm
; RUN: llc -march=tms34010 -O0 -filetype=asm < %s | FileCheck %s
define i32 @lt(i32 %a, i32 %b) {
entry:
%c = icmp slt i32 %a, %b
%r = zext i1 %c to i32
ret i32 %r
}
; CHECK-LABEL: _lt:
; CHECK: CMP
; CHECK: JALT
; CHECK: MOVI 0
; CHECK: MOVI 1
; CHECK: RETS 2
```
`m3-if.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @max(i32 %a, i32 %b) {
entry:
%c = icmp sgt i32 %a, %b
%r = select i1 %c, i32 %a, i32 %b
ret i32 %r
}
; CHECK-LABEL: _max:
; CHECK: CMP
; CHECK: JAGT
; CHECK: RETS 2
```
`m3-loop.ll` (a real phi/loop, run at `-O0` to keep the loop shape):
```llvm
; RUN: llc -march=tms34010 -O0 -filetype=asm < %s | FileCheck %s
define i32 @sumto(i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [ 1, %entry ], [ %i.next, %loop ]
%s = phi i32 [ 0, %entry ], [ %s.next, %loop ]
%s.next = add i32 %s, %i
%i.next = add i32 %i, 1
%c = icmp sle i32 %i, %n
br i1 %c, label %loop, label %exit
exit:
ret i32 %s
}
; CHECK-LABEL: _sumto:
; CHECK: CMP
; CHECK: JALE
; CHECK: JAUC
; CHECK: RETS 2
```
`m3-unsigned.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @ult(i32 %a, i32 %b) {
entry:
%c = icmp ult i32 %a, %b
%r = zext i1 %c to i32
ret i32 %r
}
define i32 @ugt(i32 %a, i32 %b) {
entry:
%c = icmp ugt i32 %a, %b
%r = zext i1 %c to i32
ret i32 %r
}
; CHECK-LABEL: _ult:
; CHECK: JALO
; CHECK-LABEL: _ugt:
; CHECK: JAHI
```
`m3-ops.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @ops(i32 %a, i32 %b) {
entry:
%s = sub i32 %a, %b
%an = and i32 %s, %a
%o = or i32 %an, %b
%x = xor i32 %o, %s
%l = shl i32 %x, 3
%r = lshr i32 %l, 2
%q = ashr i32 %r, 1
ret i32 %q
}
; CHECK: SUB
; CHECK: AND
; CHECK: OR
; CHECK: XOR
; CHECK: SLL
; CHECK: SRL
; CHECK: SRA
; CHECK: RETS 2
```
- [ ] **Step 2: Run them red**
```sh
build/llvm-clean-build/bin/llvm-lit -v \
build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m3-(if|setcc|loop|unsigned|ops)'
```
Expected: FAIL — `Cannot select: brcond`, `setcc`, ALU/shift opcodes.
- [ ] **Step 3: Add the reserved status register and instructions**
In `TMS34010RegisterInfo.td`, add a reserved non-allocatable `ST` pseudo-register (`Register<32>` with `Namespace = "TMS34010"`; not in `ARegs`/`ARegsAll`/`BRegs`). Reserve it in `getReservedRegs`.
In `TMS34010InstrInfo.td`, add:
- `CMP` (`CMP $src, $dst`, 1 word, `isCompare = 1`, `Defs = [ST]`; encoding base `0x4800`).
- `CMPI` (`CMPI $imm, $dst`, 3 words, `Defs = [ST]`; encoding base `0x0B60`, word form `0x0B40`).
- `AND`/`OR`/`XOR` (`$src, $dst`, base `0x5000`/`0x5400`/`0x5600`; `XOR` self is `CLR`).
- `SUB` (`SUB $src, $dst`, base `0x4400`), `NEG` (`NEG $dst`, base `0x03A0`), `NOT` (`NOT $dst`, base `0x03E0`).
- `SLL`/`SRL`/`SRA` register forms (`0x6200`/`0x6600`/`0x6400`) and immediate-constant forms if needed for the patterns.
- 16 absolute conditional branches: `JAEQ`, `JANE`, `JALT`, `JALE`, `JAGT`, `JAGE`, `JALO`/`JAC`, `JAHS`/`JANC`, `JALS`, `JAHI`, `JAP`, `JAN`, `JAV`, `JANV`, `JANN`, and `JAUC`; each `(ins brtarget:$target)`, `isBranch=1`, `isTerminator=1`, `Uses=[ST]` (except `JAUC`), `Size=6`; conditional branches are **not** `isBarrier` (they fall through), while `JAUC` is `isBarrier=1`; asm `"JA<cc> $target"` (GSPA mnemonics; `UC` unconditional).
Add patterns for the register/immediate ALU forms where the SelectionDAG can select them directly; otherwise lower them in `TMS34010ISelLowering`/`ISelDAGToDAG`.
- [ ] **Step 4: Lower comparisons and branches**
In `TMS34010TargetLowering`:
- `setOperationAction(ISD::SETCC, MVT::i32, Expand)`; `SELECT`/`SELECT_CC` `Expand`; `BR_CC`/`BRCOND` `Custom`.
- Override `LowerBR_CC` and `LowerBRCOND`:
- For `icmp <pred> a, b`, emit `CMP b, a` (so the flags represent `a - b`), then the `JA<cc>` selected from the predicate and signedness; the target is the `BR_CC`/`BRCOND` destination. Use `CMPI` when `b` is a constant.
- For a general `BRCOND` value (not an `ISD::SETCC`), emit `CMPI 0, value` + `JANE`/`JAEQ` as appropriate.
- Keep the `ST` dependency: `CMP`/`CMPI` define `ST`; the branch uses it.
- Let `SETCC`/`SELECT_CC` expand to the generic `BRCOND`/`SELECT`/PHI form; the `SETCC` result is 0/1 (`ZeroOrOneBooleanContent`). The `m3-setcc` sequence should be `MOVI 0`; a conditional `JA<cc>` over the `MOVI 1`; the join label.
- The condition mapping (TMS34010 `cc` from `dis34`): signed `LT/GE/LE/GT` = `JALT/JAGE/JALE/JAGT`; unsigned `LT/GE/LE/GT` = `JALO` (`JAC`)/`JAHS` (`JANC`)/`JALS`/`JAHI`; equality `JAEQ`/`JANE`.
Extend `analyzeBranch`/`insertBranch`/`removeBranch` for `JA<cc>`: a conditional branch has `TBB` = target, `FBB` = fallthrough, `Cond` empty (the compare is fused in the branch instruction); `insertBranch` emits the `JA<cc>`; `removeBranch` removes it. Keep the `MachineBlockPlacement` conservative.
- [ ] **Step 5: Build and run the tests**
```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-build/test/CodeGen/TMS34010 --filter='m3-(if|setcc|loop|unsigned|ops)'
```
Expected: 5/5 PASS. Also run the M2 suite (`--filter='m2-'`) and confirm it remains 37/37.
- [ ] **Step 6: Commit**
```sh
git -C build/llvm-clean add llvm/lib/Target/TMS34010 llvm/test/CodeGen/TMS34010
git -C build/llvm-clean commit -m "tms34010: add integer comparisons, branches and ALU ops"
```
### Task 3: Xlib `Display`/`Screen` layout and header
**Files:**
- Create: `scripts/m3/vt1200_xlib.h`, `scripts/m3/layout.c`, `scripts/m3/layout-ti.c`
- Test: `scripts/tests/m3/layout.sh` (or the M3 harness markers; see Task 4)
**Interfaces:**
- Consumes: `xhello.asm`'s proven offsets (display+0x480, screen+0x40).
- Produces: `Display`/`Screen`/`Window`/`XID` types, the four Xlib prototypes, `vt1200_root()`, and static offset assertions.
- [ ] **Step 1: Write the failing offset test**
`scripts/m3/vt1200_xlib.h`:
```c
#ifndef VT1200_XLIB_H
#define VT1200_XLIB_H
typedef unsigned int XID;
typedef unsigned int Window;
/* ROM v2.2 layout (xhello.asm): Display.screens is the pointer at byte 0x90
(bit 0x480); Screen.root is the 32-bit root id at byte 0x8 (bit 0x40). */
typedef struct {
unsigned char pad[0x90]; /* bit 0x480 */
void *screens;
} Display;
typedef struct {
unsigned char pad[0x8]; /* bit 0x40 */
unsigned int root;
} Screen;
extern Display *XOpenDisplay(const char *name);
extern Window XCreateSimpleWindow(Display *display, Window parent,
int x, int y, unsigned width,
unsigned height, unsigned border_width,
unsigned long border,
unsigned long background);
extern int XMapWindow(Display *display, Window window);
extern int XSync(Display *display, int discard);
static unsigned int vt1200_root(Display *d) {
return ((Screen *)((Display *)d)->screens)->root;
}
int layout_screens(void);
int layout_root(void);
#endif
```
`scripts/m3/layout.c`:
```c
#include "vt1200_xlib.h"
_Static_assert(__builtin_offsetof(Display, screens) == 0x90,
"Display.screens must be at byte 0x90 (bit 0x480)");
_Static_assert(__builtin_offsetof(Screen, root) == 0x8,
"Screen.root must be at byte 0x8 (bit 0x40)");
int layout_screens(void) { return (int)__builtin_offsetof(Display, screens) * 8; }
int layout_root(void) { return (int)__builtin_offsetof(Screen, root) * 8; }
```
`scripts/m3/layout-ti.c` (K&R, for the 1987 compiler):
```c
struct display { char pad[0x90]; void *screens; };
struct screen { char pad[0x8]; unsigned int root; };
int layout_screens() { return (int)&((struct display *)0)->screens; }
int layout_root() { return (int)&((struct screen *)0)->root; }
```
- [ ] **Step 2: Run it red/verify the target layout**
```sh
scripts/tms-clang -O0 -o /tmp/opencode/m3-layout.ll scripts/m3/layout.c
build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm \
/tmp/opencode/m3-layout.ll -o /tmp/opencode/m3-layout.s
```
Expected initially: the `_Static_assert`s fail if the target's struct layout pads `Display.screens` away from byte 0x90 (bit 0x480) or `Screen.root` away from byte 0x8 (bit 0x40). Fix the header (the padded structs are the intended ROM layout) or the target's struct layout if the padded struct itself is laid out wrongly. Do not use the host X11R7 `_XDisplay` layout.
- [ ] **Step 3: Build the TI golden**
Compile `scripts/m3/layout-ti.c` with the 1987 TI compiler (`gspcpp`/`gspcc`/`gspcg` under DOSBox, the `c-hello/build.sh` pattern). Store the LF-normalized asm as `scripts/m3/layout-ti.s`. The two functions must return `0x480` and `0x40`; check the asm's `MOVI` constants.
- [ ] **Step 4: Add the layout gate**
`scripts/tests/m3/layout.sh`:
- compile `layout.c` with `tms-clang` + `llc`; require exit 0 (the static asserts);
- check `layout.s` and `layout-ti.s` contain the constants `0x480`/`0x40` (allowed spellings: `480h`, `1152`, etc.);
- fail loudly on a mismatch.
Run it; expected PASS.
- [ ] **Step 5: Commit**
```sh
git add scripts/m3/vt1200_xlib.h scripts/m3/layout.c scripts/m3/layout-ti.c \
scripts/m3/layout-ti.s scripts/tests/m3/layout.sh
git commit -m "tests: M3 Xlib Display/Screen layout"
```
### Task 4: Xlib client, ROM imports, branch/Xlib MAME harness, window
**Files:**
- Create: `scripts/m3/xhello.c`, `branch.c`, `entry.asm`, `link.cmd`, `run.sh`, `m3-check.lua`, `dis-roundtrip.sh`, `validate-syms.sh`
- Modify: `build/llvm-clean/llvm/lib/Target/TMS34010/TMS34010ISelLowering.cpp/.h` (misaligned accesses)
- Test: `build/llvm-clean/llvm/test/CodeGen/TMS34010/m3-unaligned.ll`
- Create: `scripts/tests/m3/run.sh`, `scripts/tests/m3/dis-roundtrip.sh`
- Test: the MAME window and the branch markers
**Interfaces:**
- Consumes: Tasks 1–3's branches, layout, and the M2 `.data`/ABI/loader.
- Produces: `M3 MAME: PASS` with the Xlib markers, branch results, and a white window pixel; `M3 dis34 round-trip: PASS`.
- [ ] **Step 1: Fix the packed-struct unaligned-load gap (compiler)**
`vt1200_root()` loads the `Display.screens` pointer and `Screen.root` from packed structs (`clang` `PackStruct = 1` → every field load is `align 1`). The DAG legalizer currently splits those into four `zext i8` loads, and M2/M3 only has a sign-extending `MOVB`, so `llc` aborts. Add `TMS34010TargetLowering::allowsMisalignedMemoryAccesses` returning true for i16/i32/pointers in address space 0 (the ISA allows misaligned accesses; spec §7.5), so the existing `MOVE`/FS0 forms select directly. Add a focused lit test (`m3-unaligned.ll`: `load i32, ptr %p, align 1` and `store i32 ..., align 1` → `MOVE`/`MOVE ...,1`; a pointer load; an i16 align-1 load). Also add a `vt1200_root` probe to `layout.c` (or a small TU) that calls the helper and require the selected asm to contain the `0480h`/`040h` bit displacements. Do not paper over this with `__attribute__((aligned(2)))`.
- [ ] **Step 2: Write the C client and branch test**
`scripts/m3/xhello.c`:
```c
#include "vt1200_xlib.h"
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
int xhello(void) {
Display *d = XOpenDisplay("0::0");
MARK(0) = (unsigned int)d;
if (!d)
return -1;
unsigned int root = vt1200_root(d);
MARK(1) = root;
Window w = XCreateSimpleWindow(d, root, 100, 100, 400, 200, 2, 0, 1);
MARK(2) = (unsigned int)w;
XMapWindow(d, w);
int s = XSync(d, 0);
MARK(3) = (unsigned int)s;
return (int)w;
}
```
`scripts/m3/branch.c`:
```c
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
int sumto(int n) {
int s = 0;
int i;
for (i = 1; i <= n; i++)
s += i;
return s;
}
int max2(int a, int b) { return a > b ? a : b; }
int lt(int a, int b) { return a < b; }
int branch_test(void) {
MARK(4) = (unsigned int)sumto(10);
MARK(5) = (unsigned int)max2(20, 22);
MARK(6) = (unsigned int)lt(5, 7);
MARK(7) = (unsigned int)layout_screens();
MARK(8) = (unsigned int)layout_root();
return 0;
}
```
The `layout_screens`/`layout_root` functions come from `layout.c` (Task 3); compile it into the M3 image.
- [ ] **Step 3: Write the entry and linker command**
`scripts/m3/entry.asm`:
```asm
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _branch_test
calla _xhello
rets
.bss _cstack, 32768, 16 ; 4096 bytes
```
`scripts/m3/link.cmd` (CRLF; same shape as M2):
```text
-a
-o M3.OUT
-m M3.MAP
-e _start
M3.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 018A9000h, length = 40000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
```
- [ ] **Step 4: Validate the ROM symbols**
`scripts/m3/validate-syms.sh`:
- require the four entries in `symbols/vt1200-v22.syms`;
- require each address in the ROM bit range `3F200000h..3FFFFFFFh` and 16-bit aligned;
- run `dis34 -b <addr> -s <byte-offset>` for each and require a plausible function prologue (first instruction is a `MOVE`/`MMTM`/`SETF`/`CMP`/`ADDI`, not data);
- record the known-good `xhello.asm` evidence in `symbols/README.md`.
This is a regression check, not a re-extraction; the four entries are proven by `xhello.asm`. M4 extends the table for `XImage`/`XEvent`.
- [ ] **Step 5: Write the M3 runner and check**
`scripts/m3/run.sh` adapts `scripts/m2/run.sh` with:
- `M3_WORK=${M3_WORK:-$ROOT/build/m3}`, `M3_COFF`, `M3_SDK`;
- compile `xhello.c`, `branch.c`, `layout.c` with `scripts/tms-clang -O2` + `llc -march=tms34010 -O0 -filetype=asm` (keep `layout.c` compiled with the client; the static asserts must pass);
- generate `rom-imports.asm` from `symbols/vt1200-v22.syms`;
- concatenate `entry.asm`, the three `.s` files, and `rom-imports.asm`; assemble/link with GSPA/GSPLNK under DOSBox; require `No Errors`, `No Warnings`; `_start=018A0000`, `.bss` origin `018A9000`, non-empty `.data`, loader `.text`/`.data` 0 differing;
- run MAME with `VT1200_SKIP_LEVEL1=1`, the loader, `LOADER_AT=2500`, `LOADER_END=3400` (adjust if the X server/window needs more frames), `M3_LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua`, `-autoboot_script $ROOT/scripts/m3/m3-check.lua`.
`scripts/m3/m3-check.lua` reads at frame `ENDF-1`:
| marker | expected |
|---|---|
| `0x018A8000` | nonzero Display pointer |
| `0x018A8020` | nonzero root window id |
| `0x018A8040` | nonzero window id |
| `0x018A8060` | `XSync` result (diagnostic only; do not gate the pass on it) |
| `0x018A8080` | `55` (`sumto(10)`) |
| `0x018A80A0` | `22` (`max2(20,22)`) |
| `0x018A80C0` | `1` (`lt(5,7)`) |
| `0x018A80E0` | `0x480` (`layout_screens`) |
| `0x018A8100` | `0x40` (`layout_root`) |
Then read the VRAM pixel inside the window: the window is at `(100,100)`, 400×200; background pixel 1 (white). On the default VR319 monitor the VRAM base is bit `0x04000000` and the row pitch is 2048 bits; read a 32-bit word containing `(150,150)` and require the bit `1 << (150 & 31)` to be set. Print `M3: ...` and exactly `M3 MAME: PASS` on success. The window must be observed; a markers-only pass is not enough.
- [ ] **Step 6: Write the `dis34` round-trip**
`scripts/m3/dis-roundtrip.sh` adapts the M2 COFF parser and symbol lookup. It must require:
- the `_XOpenDisplay`/`_XCreateSimpleWindow`/`_XMapWindow`/`_XSync` `JAUC` stubs at `3F9C2570`/`3FD41B90`/`3FD64F60`/`3FD9F6B0`;
- a `CALLA _XOpenDisplay` with one argument push; `CALLA _XCreateSimpleWindow` with **nine** `MOVE ...,*A14+,1` pushes in order (the first argument at the top of the program stack); `CALLA _XMapWindow` with two; `CALLA _XSync` with two;
- the `SETF 16,0,0` + `SETF 32,0,1` after each call; `RETS 2` for the nine-argument function;
- the branch code: `CMP`, `JALT`/`JAGT`/`JALE`/`JALO`/`JAHI`, `JAUC`, and the `LBB…` labels (no dot);
- the `.data` bytes `30 3A 3A 30 00` (`"0::0"`);
- the old-STK push-count `(pushes+1)*32` for the branch/Xlib functions; the `rets 1` negative control.
- [ ] **Step 7: Run the M3 gates**
```sh
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
```
Expected: `M3 MAME: PASS` with `display/root/window/sync`, `sum=55 max=22 lt=1`, `screens=0x480 root=0x40`, and the window pixel white; `M3 dis34 round-trip: PASS`.
- [ ] **Step 8: Commit**
```sh
git add scripts/m3 scripts/tests/m3
git commit -m "tests: M3 branches and first Xlib window in MAME"
```
### Task 5: Encoding/differential checks, docs, and push
**Files:**
- Create: `scripts/tests/m3/diff/ti-branch.c`, `ti-branch.s`, `ti-xhello.c`, `ti-xhello.s`, `check.sh`
- Modify: `README.md` (project), `build/llvm-clean/TMS34010.md` (compiler), `symbols/README.md`
- Push: compiler `m3`, project `m3`
**Interfaces:**
- Consumes: all previous tasks.
- Produces: stored TI goldens for branch/struct/call shape, documented M3, pushed `m3` branches.
- [ ] **Step 1: Produce the TI goldens**
Using the 1987 compiler under DOSBox, compile K&R versions:
`scripts/tests/m3/diff/ti-branch.c`:
```c
int sumto(n) int n; { int s; int i; s = 0; for (i = 1; i <= n; i++) s = s + i; return s; }
int max2(a, b) int a; int b; { if (a > b) return a; return b; }
```
`ti-xhello.c`: the K&R Xlib client with the same `Display`/`Screen` padded structs and the four calls.
Store the LF-normalized asm as `ti-branch.s`/`ti-xhello.s`.
- [ ] **Step 2: Write the differential check**
`scripts/tests/m3/diff/check.sh`:
- generate our `branch.s`/`xhello.s` with `scripts/tms-clang -O2` + `llc -O0`;
- in both the TI golden and our asm require: a loop back-edge (`JALE`/`JLE` and `JAUC`/`JRU`), `max2`'s conditional branch, the `sumto` `RETS 2`/`max2` `RETS 2`, the nine `XCreateSimpleWindow` argument pushes, and the `Display`/`Screen` offsets `0x480`/`0x40`;
- accept TI spelling variants (`0FFE0h`, `ADDK`, `JRU`/`JAUC`); do not require byte-for-byte equality. No DOSBox in CI.
- [ ] **Step 3: Update the docs**
- Project `README.md`: M3 status; branch/loop support and its limits (i32/pointer, signed/unsigned compares, `and/or/xor/shift/sub`); the Xlib four-call window; the `Display`/`Screen` offsets; the commands for `scripts/tests/m3/run.sh`, `dis-roundtrip.sh`, `diff/check.sh`; M3 non-goals (XImage/XEvent/real client/structs by value/function pointers/floats/i64/object writer); the `m3` branch and M2 default; the M2/M1 snapshots.
- Compiler `TMS34010.md`: `JAcc` branches, the `ST` flag register, the condition mapping, GSPA-safe `LBB` labels, the `CMP` order, the `SETCC`/`SELECT_CC` approach, the new ALU/shift instructions, and the Xlib client evidence.
- `symbols/README.md`: the four Xlib entries are validated by `xhello.asm` and `validate-syms.sh`; M4 extends the table.
- [ ] **Step 4: Run every M3 gate**
```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-build/test/CodeGen/TMS34010 --filter='(m2|m3)-'
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
scripts/tests/m3/diff/check.sh
tools/tms-bitaddr/tests/run.sh
```
Expected: all green; the M2 suite remains 37/37; M3 adds 5 branch tests plus the layout/harness gates.
- [ ] **Step 5: Commit and push**
```sh
git -C build/llvm-clean add TMS34010.md
git -C build/llvm-clean commit -m "docs: M3 branches and Xlib window"
git -C build/llvm-clean push origin m3
git add README.md symbols scripts/tests/m3 scripts/m3
git commit -m "docs: M3 control flow and first Xlib window"
git push origin m3
```
Do not force-push, merge into `m0`, or change the default branch; the controller changes the remote default after the final M3 review if the user asks.
---
## Execution Handoff
M3 is the first two halves of the remaining Xlib path: integer control flow (needed by real C) and the four-call window. M4 owns the real client, `XImage`/`XEvent`, the full X11R4 headers, structs by value and function pointers. M5 owns the native COFF path, floats, varargs, bitfields, `long long` and the `JR`/branch-relaxation code-size pass. The compiler repo is canonical for target code; this project repo holds the plans, wrappers, symbols and integration tests.
@@ -0,0 +1,548 @@
# TMS34010 M4 — Xlib Images, Events, and the termtris Integration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** A compiled C program displays a 1-bit `XImage` through the ROM Xlib, handles real X events and keyboard input, and exits cleanly; the project also builds the WIP `termtris` VT1200 client with this compiler and runs its move/panels/quit acceptance in MAME.
**Architecture:** M4 keeps the M3 asm-only path and TI ABI. It fills the scalar gaps real clients need (zero-extending narrow loads, module-unique private labels, unsigned/`<=` runtime coverage, 32-bit multiply/divide through `rts.lib`), adds function pointers/indirect `CALL`, adds the X11R4-compatible `XImage`/`XEvent`/`XErrorEvent` layouts, corrects the ROM symbol table, and implements the mandatory per-process XG block so a loader child can receive events. Two small clients prove the image and event paths; the pinned `termtris` branch is a third, full-game integration test.
**Tech Stack:** LLVM/Clang 23.1.5 fork (`llvm-tms34010`, branch `m4`), C++17, CMake/Ninja, `llc`, GSPA/GSPLNK 2.00 under DOSBox, COFF loader, MAME, `dis34`, `llvm-lit`/FileCheck, 1987 TI compiler, the pinned `termtris` branch.
**Spec:** `docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md` (§4.2, §7.3.1, §7.4–§7.7, §7.9, M4, M5)
## Global Constraints
- Compiler work happens in **`llvm-tms34010`**, branch `m4`, created from `m3` at `73c85350a401`. Use `build/llvm-clean` and `build/llvm-clean-build`; do not clone another LLVM tree.
- Project plans/tests live in **`tms-c-compiler`**, branch `m4`, created from `m3` at `24f8773` plus the M4 plan commit. `m3` stays the default snapshot until M4 is reviewed; do not merge M2/M3 into `m0`.
- The M3 TI ABI, `.data`, branch/loop, FS0/FS1 and old-STK rules are unchanged. M4 adds: function pointers/indirect calls, zero-extending narrow loads, module-unique private labels, 32-bit multiply/divide, XImage/XEvent/error-handler layouts, the XG block, and the wider Xlib.
- Output remains **assembly only**: `llc -march=tms34010 -filetype=asm`; GSPA/GSPLNK 2.00 under DOSBox; the existing COFF loader. No LLVM object writer/MCCodeEmitter (M5).
- **ROM v2.2 only.** Use the termtris `xlib.h` table and the ROM disassembly. Correct the table: **`XMapWindow = 0x3FD64F60`** (request opcode 8); **`0x3FD645B0` is `XRaiseWindow`** (ConfigureWindow, opcode 12, value-mask `0x40`/`CWStackMode`, value 0/`Above`), not a mapping entry. The M3 table/`xhello` used `0x3FD645B0` under the `XMapWindow` name; its MAME gate still saw a solid-white client area, so re-run that gate with `0x3FD64F60` and record whether the old pass depended on server-side create/configure behavior. The M3 harness/docs/dis34 expectations are updated.
- **Per-process XG block is mandatory before `XOpenDisplay` in a loader child.** The termtris fix: `proc = *(u32*)0x001B3900; pp = *(u32*)(proc+0x100); *(u32*)pp = (u32)zeroed_1KiB;`. Without it `_XEnq` sees `XG == NULL` and `XNextEvent` delivers garbage/nothing. The ROM is untouched.
- Known text/event/GC entries (termtris `xlib.h`): `XOpenDisplay 0x3F9C2570`, `XCreateSimpleWindow 0x3FD41B90`, `XSelectInput 0x3FD97D50`, `XSync 0x3FD9F6B0`, `XFlush 0x3F9B1030`, `XNextEvent 0x3F9C1CF0`, `XLookupString 0x3F9A9C30`, `XCreateGC 0x3F997000`, `XSetForeground 0x3FD99A60`, `XFillRectangle 0x3F99E8F0`, `XDrawString 0x3FDA0000`, `XPending 0x3FD67620`, `XGetInputFocus 0x3FD55210`, `XLoadFont 0x3FD4AE80`, `XCopyArea 0x3FD3E220`, `sleep 0x3FB33A20`. The `XImage` entries (`XCreateImage`, `XPutImage`, `XDestroyImage`, optional `XGetImage`) and `XSetErrorHandler`/`XSetIOErrorHandler` must be extracted/validated before use; do not guess.
- `XMapWindow` is the mapping API for new clients; the M4 image/event clients use the true entry. The termtris client uses `XMapWindow 0x3FD64F60`.
- The **termtris additional integration** is pinned at branch `termtris`, commit `be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf`. Its `apps/termtris` tree is GPLv3; vendor/read it with `COPYING` intact and test only. Build it with **this compiler**, not its TI r5.01/2ine path. Its acceptance is the proven `play_keys` run: `move`/`panels`/`quit`, `loops >= 100`, `events >= 6`, `focus` = the window, `quit = 1` for `q`, and the captured frames.
- The **primary M4 acceptance** is the image/event client: a 1-bit `XImage` (LSBFirst) appears in MAME, no X errors are reported by the handler, and a real key press is delivered and processed. The termtris run is the additional, full-game integration test.
- `XImage` data is `unsigned char`; zero-extending `unsigned char`/`unsigned short` loads must work. The program stack is A14 (grows up); the system stack A15; `RETS 2/0` and FS0/FS1 apply to indirect calls too.
- X11 headers: use an X11R4-compatible subset (system Xlib/Xutil or a vendored minimal header). `Display.screens` remains byte `0x90` (bit `0x480`); `Screen.root` byte `0x8` (bit `0x40`). Verify the `XImage`/`XEvent`/`XErrorEvent` field offsets with `_Static_assert`s and a TI/host golden before relying on them.
- The M2/M3 tests must stay green. Add `m4-*` lit tests and `scripts/tests/m4/*` gates.
- No XImage/XEvent/the full Xlib beyond the listed calls; no floats/i64/varargs/bitfields/structs by value/native COFF in M4. The termtris compile may expose a gap; fix it or rule it out explicitly in the report.
## Review Focus
1. **Function pointer/indirect call ABI:** `CALL Rs`; the call-site old-STK save iff args; argument order; A8 return; FS0/FS1; the callee `RETS`. A wrong `CALL` target or stack leaves the caller corrupted. Tests in Task 3 (`m4-fnptr`, a C callback) and Task 7 (termtris callbacks).
2. **XImage/XEvent/GC/XErrorEvent layout and semantics:** `XImage.byte_order`/`bitmap_bit_order` LSBFirst, `data`, `bytes_per_line`; `XEvent.type`/`xkey` fields; `XErrorEvent` fields. A wrong field or offset yields a black/garbled image or no key. Tests in Task 4 (static offsets) and Task 6 (image pixel/no-X-errors/key).
3. **The XG block/event loop:** without `vt_install_xg()` before `XOpenDisplay` the loader child receives no events; the termtris tree documents the exact failure. Tests in Task 5 (XG gate) and Tasks 6/7 (keys/focus/quit).
4. **`XMapWindow` vs `XRaiseWindow`:** the M3 table's `XMapWindow` (`0x3FD645B0`) is actually `XRaiseWindow` (ConfigureWindow opcode 12); the real `XMapWindow` is `0x3FD64F60` (opcode 8). Use it for new clients and correct the M3 harness/docs. Tests in Task 5 and the M3 re-run.
5. **Narrow loads/unsigned/multiply/loop prerequisites:** `unsigned char` XImage data, `unsigned short`, `<=`/unsigned branches, and the `-O2` closed-form loop/multiply. A missing feature aborts a real client. Tests in Tasks 1/2 and the termtris run.
6. **The termtris integration:** the pinned branch, the function-pointer callbacks, the event loop, the keys/panels/quit, the screenshots, the `loops/events/focus/quit` markers. Test in Task 7.
7. **X11R4 layout vs the ROM:** the byte/bit `Display`/`Screen` and the `XImage`/`XEvent` fields; a header mismatch produces plausible but wrong data. Tests in Task 4 (static asserts) and Task 6.
---
### Task 1: M4 branches, zero-extending narrow loads, private labels, and unsigned runtime
**Files:**
- Modify: `build/llvm-clean/llvm/lib/Target/TMS34010/TMS34010ISelLowering.cpp`, `TMS34010InstrInfo.td`
- Modify: `TMS34010TargetMachine.cpp` / `TMS34010AsmPrinter.cpp` (private symbol naming)
- Test: `build/llvm-clean/llvm/test/CodeGen/TMS34010/m4-zext.ll`, `m4-private.ll` (or a two-TU shell test)
- Create: `scripts/m4/unsigned.c`, `scripts/m4/private-a.c`, `private-b.c`, `scripts/tests/m4/{unsigned,private}.sh`
**Interfaces:**
- Consumes: M3 target and tests.
- Produces: `ZEXTLOAD i8/i16` selection; module-unique private symbols; runtime `JALS`/`JAHS`/`JANE`/`JAGE`/`JALE`/`JALO`/`JAHI` coverage.
- [ ] **Step 1: Write the failing tests**
`m4-zext.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @zext8(ptr %p) {
%v = load i8, ptr %p, align 1
%r = zext i8 %v to i32
ret i32 %r
}
define i32 @zext16(ptr %p) {
%v = load i16, ptr %p, align 1
%r = zext i16 %v to i32
ret i32 %r
}
; CHECK-LABEL: _zext8:
; CHECK: MOVB *A{{[0-9]+}}, A{{[0-9]+}}
; CHECK: ANDI 255
; CHECK-LABEL: _zext16:
; CHECK: MOVE *A{{[0-9]+}}, A{{[0-9]+}}, 0
; CHECK: ANDI 65535
```
The `ANDI` forms are the 1's-complement immediates; the masks must be exact. `m4-private` compiles two TUs with the same private string literal and requires the GSPA link to succeed (no duplicate private label).
`scripts/m4/unsigned.c`:
```c
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n)*0x20u))
int le(int a, int b) { return a <= b; }
unsigned ule(unsigned a, unsigned b) { return a <= b; }
int sum_le(int n) { int s = 0, i; for (i = 0; i <= n; i++) s += i; return s; }
int unsigned_test(void) {
MARK(0) = (unsigned)le(5, 7);
MARK(1) = (unsigned)ule(7u, 5u);
MARK(2) = (unsigned)sum_le(10);
return 0;
}
```
`scripts/m4/private-a.c` and `private-b.c` each define a string literal and a function returning it, e.g. `char *a(void) { return "same"; }`; the linker must resolve both without a duplicate label.
- [ ] **Step 2: Run them red**
```sh
build/llvm-clean-build/bin/llvm-lit -v build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m4-(zext|private)'
```
Expected: FAIL — `Cannot select: ... zext from i8/i16`, and the two-TU private link collides.
- [ ] **Step 3: Implement the zext loads and private naming**
- Set `ZEXTLOAD i8/i16` legal and add patterns: `MOVB` (which sign-extends) + `ANDI 255`, and FS0 `MOVE16` + `ANDI 65535`; reuse `ANDI` (the M4 `.td` fix already complements the immediate). Add the `ANDI` immediate 1's complement if needed.
- Make private/string-literal symbols module-unique so two TUs with the same `.str`/private name do not collide in GSPA's flat symbol table. Keep public names unchanged; the sanitizer remains GSPA-valid. The two-TU test must assemble and link with `No Errors, No Warnings`.
- [ ] **Step 4: Run the MAME unsigned test**
Build `unsigned.c` with `tms-clang -O2` + `llc -O0`, link with the M3 `scripts/m4` harness/entry (a minimal entry calling `_unsigned_test`), and check `le(5,7)=1`, `ule(7,5)=0`, `sum_le(10)=55`. The assembly must contain `JALE`/`JALS` (or the correct unsigned `<=` form) and `JAHS`/`JAHI`.
- [ ] **Step 5: Run the suite and commit**
```sh
build/llvm-clean-build/bin/llvm-lit -v build/llvm-clean-build/test/CodeGen/TMS34010 --filter='(m2|m3|m4)-'
```
Expected: all green (the M3 suite plus the new tests). Commit the compiler changes and the project harness/tests; the report.
### Task 2: 32-bit multiply/divide and the `-O2` loop pipeline
**Files:**
- Modify: `TMS34010InstrInfo.td`, `TMS34010ISelLowering.cpp/.h`, `TMS34010TargetMachine.cpp` (TTI/pass policy)
- Create: `scripts/m4/mul.c`, `scripts/m4/loop.c`, `scripts/tests/m4/mul.sh`
- Link: the 1987 `RTS.LIB` (the `_mul`/`_div`/`_mod` entries) or a target sequence
**Interfaces:**
- Consumes: Task 1 and M3's `JAcc`/`ST`.
- Produces: `MPYS`/`MPYU` 16×16; 32×32 multiply/divide via `rts.lib` libcalls or a sequence; an `-O2` loop policy that does not produce an unsupported i33 multiply.
- [ ] **Step 1: Write the failing tests**
`m4-mul.ll` (or C through `tms-clang`):
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @mul(i32 %a, i32 %b) {
%r = mul i32 %a, %b
ret i32 %r
}
; CHECK: MPYS
; CHECK: MPYU
```
`scripts/m4/loop.c`:
```c
int sumto(int n) { int s = 0, i; for (i = 1; i <= n; i++) s += i; return s; }
int squares(int n) { int s = 0, i; for (i = 0; i < n; i++) s += i * i; return s; }
```
Compile with the **default** `tms-clang -O2` (no `-replexitval=never`) and require success + correct MAME results (`sumto(10)=55`, `squares(5)=30`).
- [ ] **Step 2: Run it red**
```sh
scripts/tms-clang -O2 -o /tmp/opencode/m4-mul.ll scripts/m4/mul.c
build/llvm-clean-build/bin/llc -march=tms34010 -O0 -filetype=asm ...
```
Expected: `Cannot select: mul` / `umul_lohi` / the i33 multiply / missing `_mul`.
- [ ] **Step 3: Implement the arithmetic and loop policy**
- Add `MPYS`/`MPYU` 16×16 and the 32×32 multiply/divide/remainder libcalls. Prefer linking the 1987 `RTS.LIB` `_mul`/`_div`/`_mod` entries (reuse the TI runtime; no new libc); wire the `TargetLowering` libcall names and the M4 `link.cmd` (`-l RTS.LIB`). If the runtime entry names differ, extract them from the SDK `RTS.LIB`/map and record them.
- Address the `IndVarSimplify` exit-value i33 multiply: add a target `TargetTransformInfo`/pass policy that makes the closed-form replacement unprofitable for this target, or lower the i33/i64 multiply through the runtime. The M3 `m3` harness may keep `-replexitval=never`, but M4's ordinary `-O2` loop must compile.
- Keep the M3 `shouldFoldConstantShiftPairToMask` behavior; the multiply/divide tests.
- [ ] **Step 4: Run the MAME multiply/loop tests**
`mul(6,7)=42`, `sumto(10)=55`, `squares(5)=30`; the `-O2` pipeline without the exit-value wrapper. Record the `RTS.LIB` link line and the symbols.
- [ ] **Step 5: Run the suite and commit**
Commit the compiler arithmetic/policy and the M4 tests; the report.
### Task 3: Function pointers and indirect calls
**Files:**
- Modify: `TMS34010InstrInfo.td` (`CALL Rs`), `TMS34010ISelLowering.cpp/.h` (`LowerCall` indirect), `TMS34010MCInstLower.cpp` if needed
- Test: `m4-fnptr.ll`, `scripts/m4/fnptr.c`, `scripts/tests/m4/fnptr.sh`
**Interfaces:**
- Consumes: M3 `LowerCall`/`CALLA`/frames; Task 1/2.
- Produces: `CALL Rs` (1-word indirect call; GSPA `CALL $rs`), function addresses as bit addresses, function-pointer parameters/returns, callback ABI.
- [ ] **Step 1: Write the failing test**
`m4-fnptr.ll`:
```llvm
; RUN: llc -march=tms34010 -O2 -filetype=asm < %s | FileCheck %s
define i32 @inc(i32 %x) {
%r = add i32 %x, 1
ret i32 %r
}
define i32 @apply(ptr %f, i32 %x) {
%r = call i32 %f(i32 %x)
ret i32 %r
}
; CHECK-LABEL: _apply:
; CHECK: CALL A{{[0-9]+}}
; CHECK: RETS 2
```
`scripts/m4/fnptr.c`:
```c
typedef int (*fn)(int);
int inc(int x) { return x + 1; }
int apply(fn f, int x) { return f(x); }
int fnptr_test(void) { return apply(inc, 41); } /* 42 */
```
- [ ] **Step 2: Run it red**
Expected: `M2/M3 supports only direct calls` fatal for the indirect call.
- [ ] **Step 3: Implement indirect calls**
- Define `CALL` (`CALL $rs`, 1 word, `isCall`, `Uses=[A14]`, `Defs=[A8,A14]`, 2 bytes) and the asm printer.
- Extend `LowerCall`: accept a `RegisterSDNode`/`CopyFromReg` callee; emit the same old-STK/argument/`CALL Rs`/FS0-FS1/`A8` sequence as `CALLA`; reject only unsupported callee shapes. A function pointer is a 32-bit bit address; `MOVI _inc, A?` or `MOVI 0xADDR, A?` materializes it.
- Keep the direct `CALLA` path and the `getCallPreservedMask`.
- Test a function pointer from a global symbol and from an absolute address (`((fn)0x3F...)`), the latter used by the Xlib header.
- [ ] **Step 4: Run the MAME callback test**
`fnptr_test()=42`; the termtris function-pointer callbacks (`term_reset` etc.) are the integration case. Record the `CALL` encoding/`dis34`.
- [ ] **Step 5: Commit**
Commit the compiler/test changes; the report.
### Task 4: X11R4-compatible headers and XImage/XEvent/error-event layouts
**Files:**
- Create: `scripts/m4/x11m4.h` (or vendor an X11R4 subset), `scripts/m4/layout.c`, `scripts/m4/layout-ti.c`
- Create: `scripts/tests/m4/layout.sh`
- Reuse: the Task 3 `vt1200_xlib.h` byte/bit layout
**Interfaces:**
- Consumes: the M3 `Display`/`Screen` layout (byte 0x90/0x8).
- Produces: `XImage`, `XEvent`/`XKeyEvent`/`XExposeEvent`/`XFocusChangeEvent`/`XButtonEvent`, `XErrorEvent`, `GC`/`Visual`, the function prototypes/constants, and verified field offsets.
- [ ] **Step 1: Write the failing layout test**
`scripts/m4/x11m4.h` contains at least:
```c
typedef struct _XImage {
int width, height;
int xoffset;
int format;
char *data;
int byte_order;
int bitmap_unit;
int bitmap_bit_order;
int bitmap_pad;
int depth;
int bytes_per_line;
int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct funcs *f;
} XImage;
typedef union _XEvent {
int type;
struct { int type; unsigned long serial; int send_event; void *display;
unsigned long window, root, subwindow; unsigned long time;
int x, y, x_root, y_root; unsigned state; unsigned keycode;
int same_screen; } xkey;
struct { int type; unsigned long serial; int send_event; void *display;
unsigned long window; int x, y, width, height, count; } xexpose;
/* xfocus/xbutton/... as needed */
} XEvent;
typedef struct _XErrorEvent {
int type;
void *display;
unsigned long resourceid, serial;
unsigned char error_code, request_code, minor_code;
} XErrorEvent;
```
`scripts/m4/layout.c`:
```c
#include "x11m4.h"
_Static_assert(__builtin_offsetof(Display, screens) == 0x90, "...");
_Static_assert(__builtin_offsetof(Screen, root) == 0x8, "...");
_Static_assert(__builtin_offsetof(XImage, data) == 20, "...");
_Static_assert(__builtin_offsetof(XImage, byte_order) == 24, "...");
_Static_assert(__builtin_offsetof(XImage, bytes_per_line) == 40, "...");
_Static_assert(__builtin_offsetof(XEvent, type) == 0, "...");
_Static_assert(sizeof(XEvent) >= 96, "...");
```
Use the actual 32-bit X11R4/system header field offsets; if the system header's `XImage`/`XEvent` match the ROM, assert them; otherwise document the ROM-verified subset. The `XImage` size must cover what `XCreateImage` returns; the `XEvent` buffer is the termtris `vt_u32 evbuf[24]` (96 bytes).
- [ ] **Step 2: Run it red/verify**
Compile with `scripts/tms-clang -O0` + `llc`; the static asserts must pass. If a field offset differs, fix the header (or the target if the packed struct is wrong) and report it. Do not use the host 64-bit layout without the 32-bit type mapping.
- [ ] **Step 3: Build the TI/host golden**
Compile `layout-ti.c` (the `Display`/`Screen`/`XImage`/`XEvent` offset functions) with the 1987 compiler; store the asm; require the same constants. The 1987 compiler's `&` is bit-addressed; convert to byte offsets consistently.
- [ ] **Step 4: Run the layout gate and commit**
`scripts/tests/m4/layout.sh` compiles the static asserts, checks the target and TI golden constants, and fails loudly. Commit the header/layout test.
### Task 5: ROM symbols, `XMapWindow` correction, and the XG block
**Files:**
- Modify: `symbols/vt1200-v22.syms`, `symbols/README.md`
- Modify: `scripts/m3/xhello.c`/`scripts/m3/dis-roundtrip.sh`/M3 docs (the `XMapWindow`/`XRaiseWindow` correction)
- Create: `scripts/m4/xgfix.c`, `scripts/m4/validate-syms.sh`, `scripts/tests/m4/symbols.sh`
**Interfaces:**
- Consumes: the termtris `xlib.h` table and the M3 loader.
- Produces: the corrected/extended v2.2 symbol table; `vt_install_xg()`; a symbol-validation gate.
- [ ] **Step 1: Correct and extend the symbol table**
- `XMapWindow = 0x3FD64F60`; add `XRaiseWindow = 0x3FD645B0` (not `XMapRaised`; the ROM entry sends ConfigureWindow with `CWStackMode=Above`). If a client wants map-and-raise, call `XMapWindow` then `XRaiseWindow`.
- Add the termtris-proven entries: `XSelectInput 0x3FD97D50`, `XCreateGC 0x3F997000`, `XSetForeground 0x3FD99A60`, `XFillRectangle 0x3F99E8F0`, `XDrawString 0x3FDA0000`, `XNextEvent 0x3F9C1CF0`, `XLookupString 0x3F9A9C30`, `XFlush 0x3F9B1030`, `XPending 0x3FD67620`, `XGetInputFocus 0x3FD55210`, `XLoadFont 0x3FD4AE80`, `XCopyArea 0x3FD3E220`, `sleep 0x3FB33A20`.
- Note: `XLoadFont`/`XCopyArea` are **not** in the pinned termtris `xlib.h`; they are ROM-validated by their request builders (OpenFont opcode 45 and CopyArea opcode 62), as `symbols/README.md` records.
- Extract/validate the `XImage` entries (`XCreateImage`, `XPutImage`, `XDestroyImage`, optional `XGetImage`) and `XSetErrorHandler`/`XSetIOErrorHandler` from the ROM (Ghidra/`ptr_constants.py`, the `XPutImage`/`XGetImage` strings at `0x3F9D0E80`/`0x3FD567A0`). Do not guess an address; if an entry cannot be validated, report it and use the fallback (the termtris text client) for that part.
- Update `scripts/m3/xhello.c`/the comments to call the true `XMapWindow` (`0x3FD64F60`), the M3 `dis-roundtrip.sh` stub expectation, and the M3 docs. Do not use `0x3FD645B0` as a mapping call. Re-run the M3 MAME/dis34 gates; the window must still appear, and record whether the old `0x3FD645B0` pass depended on server-side create/configure behavior.
- [ ] **Step 2: Implement the XG fix**
`scripts/m4/xgfix.c` (the termtris fix):
```c
int vt_install_xg(void) {
unsigned int proc = *(volatile unsigned int *)0x001B3900u;
if (proc < 0x100u || proc > 0x01FFFFFFu) return 0;
unsigned int pp = *(volatile unsigned int *)(proc + 0x100u);
if (pp < 0x100u || pp > 0x01FFFFFFu) return 0;
static unsigned int xg[256]; /* 1 KiB zeroed .bss */
*(volatile unsigned int *)pp = (unsigned int)xg;
return 1;
}
```
Call it **before** `XOpenDisplay` in the event/image clients and the termtris build.
- [ ] **Step 3: Validate the symbols**
`scripts/m4/validate-syms.sh` checks the v2.2 entries are in ROM range, 16-bit aligned, and start with a plausible function prologue (`dis34`); the termtris `xlib.h` is the evidence. Add a test that `0x3FD645B0` is reported as `XRaiseWindow` (ConfigureWindow opcode 12), so the correction is pinned.
- [ ] **Step 4: Run the M3 correction and the symbol gate**
Re-run `scripts/tests/m3/run.sh`/`dis-roundtrip.sh`; the M3 window still appears with the true `XMapWindow 0x3FD64F60`. Run `scripts/tests/m4/symbols.sh`; all entries validated. Commit.
### Task 6: The M4 image/event client and the MAME image/key gate
**Files:**
- Create: `scripts/m4/xshow.c`, `scripts/m4/entry.asm`, `scripts/m4/link.cmd`, `scripts/m4/run.sh`, `scripts/m4/m4-check.lua`, `scripts/m4/dis-roundtrip.sh`
- Create: `scripts/tests/m4/run.sh`, `scripts/tests/m4/dis-roundtrip.sh`
- Reuse: `scripts/m4/x11m4.h`, `xgfix.c`, `validate-syms.sh`
**Interfaces:**
- Consumes: Tasks 1–5.
- Produces: `M4 MAME: PASS` — a 1-bit `XImage` displayed, no X errors, a real key press processed, focus/window markers, and the window/image pixel evidence.
- [ ] **Step 1: Write the image/event client**
`scripts/m4/xshow.c` (the `xshow` shape, VT1200 Xlib):
```c
#include "x11m4.h"
static volatile unsigned int *MARK(unsigned n) { return (volatile unsigned int *)(0x018A8000u + n*0x20u); }
static int xerrors;
static int on_error(void *d, XErrorEvent *e) { xerrors++; return 0; }
int xshow(void) {
if (!vt_install_xg()) return -1;
Display *d = XOpenDisplay("0::0");
MARK(0)[0] = (unsigned)d; if (!d) return -1;
Window w = XCreateSimpleWindow(d, vt_root(d), 20, 20, 640, 480, 0, 0, 0);
MARK(1)[0] = (unsigned)w;
XSelectInput(d, w, ExposureMask | KeyPressMask | ButtonPressMask | StructureNotifyMask);
GC gc = XCreateGC(d, w, 0, 0);
XSetForeground(d, gc, 1);
/* 1-bit checker image, LSBFirst */
int bpl = ((640 + 31) / 32) * 4;
static unsigned char data[480 * 80]; /* 640/8 = 80 bytes/row */
for (int y = 0; y < 480; y++) for (int x = 0; x < 640; x++)
if (((x/32) ^ (y/32)) & 1) data[y*80 + (x>>3)] |= 1u << (x & 7);
XImage *img = XCreateImage(d, 0, 1, XYBitmap, 0, (char *)data, 640, 480, 32, bpl);
MARK(2)[0] = (unsigned)img; if (!img) return -1;
img->byte_order = LSBFirst; img->bitmap_bit_order = LSBFirst;
XSetErrorHandler(on_error);
XMapWindow(d, w);
XPutImage(d, w, gc, img, 0, 0, 0, 0, 640, 480);
XSync(d, 0);
for (;;) {
XEvent ev;
while (XPending(d)) {
XNextEvent(d, &ev);
MARK(4)[0]++;
if (ev.type == KeyPress) {
char buf[8]; unsigned long ks = 0;
int n = XLookupString(&ev.xkey, buf, 8, &ks, 0);
MARK(5)[0] = (unsigned)ev.xkey.keycode;
MARK(6)[0] = (unsigned)ks;
if (buf[0] == 'q') { MARK(3)[0] = 1; return 0; }
} else if (ev.type == Expose) {
XPutImage(d, w, gc, img, 0, 0, 0, 0, 640, 480);
}
}
MARK(7)[0] = (unsigned)xerrors;
XFlush(d);
sleep(5); /* the ROM sleep; keep the process alive */
}
}
```
Use the exact Task 4 field names/offsets; `XCreateImage`'s `visual` may be 0 for a 1-bit default; if the ROM requires the default visual, use `DefaultVisual` or the display's default; verify. The window is at (20,20), 640×480; the image checker is LSB-first; the key `q` quits. The `XSetErrorHandler` is a function pointer (Task 3); the `on_error` increments.
- [ ] **Step 2: Entry/link/runner**
`scripts/m4/entry.asm` sets FS0/FS1, STK/FP, calls `_xshow`, `rets`; `_cstack` `.bss` 32768 bits. `link.cmd` places `.text`/`.data` > prog at `018A0000h`, `.bss` > ram at `018A9000h`; link `RTS.LIB` if the compiler/libcall path needs it. `scripts/m4/run.sh` compiles `xshow.c`/`layout.c`/`xgfix.c` with `tms-clang -O2` + `llc -O0`, generates the ROM stubs/function-pointer addresses, GSPA/GSPLNK under DOSBox, loads in MAME with `VT1200_SKIP_LEVEL1=1`, the loader, `LOADER_AT=2500`, `LOADER_END=3400` (adjust), the `VT1200_MONITOR=VR319` pin, and `m4-check.lua`.
`scripts/m4/m4-check.lua` reads the markers: display/window/image nonzero, `xerrors==0`, `events>0`, the keycode/keysym for a `q` injected key, the quit marker, and the image checker at several window pixels (e.g. (30,30) and (50,50) alternate; the checker pattern at 32-pixel cells). Use the VRAM base bit `0x04000000`, VR319 2048-bit pitch, LSB-first; the window is at (20,20). Require `M4 MAME: PASS`; markers alone are not enough.
- [ ] **Step 3: Write the `dis34` round-trip**
`scripts/m4/dis-roundtrip.sh` checks the linked bytes for `CALL`/`CALLA` (the indirect call), the `XOpenDisplay`/`XCreateSimpleWindow`/`XSelectInput`/`XCreateGC`/`XSetForeground`/`XCreateImage`/`XSetErrorHandler`/`XMapWindow`/`XPutImage`/`XSync`/`XNextEvent`/`XLookupString` calls, the 9-argument `XCreateSimpleWindow` order, the `XSetErrorHandler` callback pointer, the `SETF`/`RETS`/old-STK rules, and the checker data in `.data`/`.bss`. The image client's `CALL` encoding is the Task 3 contract.
- [ ] **Step 4: Run the gates**
```sh
scripts/tests/m4/run.sh
scripts/tests/m4/dis-roundtrip.sh
```
Expected: `M4 MAME: PASS` with the image checker, `xerrors=0`, a real key event, and the window; `M4 dis34 round-trip: PASS`.
- [ ] **Step 5: Commit**
Commit the client/harness/tests; the report.
### Task 7: The pinned `termtris` additional integration test
**Files:**
- Fetch: `https://git.wut.ee/opencode.energia.arti.ee/vt1200.git`, branch `termtris`, commit `be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf`, into `vendor/vt1200-termtris` (test-only, GPLv3 `COPYING` intact)
- Create: `scripts/m4/termtris/build.sh`, `scripts/m4/termtris/play_keys.lua` (adapted from the vendored `apps/termtris/tests/play_keys.lua`), `scripts/m4/termtris/play_keys.sh`
- Create: `scripts/tests/m4/termtris.sh`
**Interfaces:**
- Consumes: Tasks 1–6; the termtris `apps/termtris/src/vt1200/{xlib.h,xgfix.c,main.c,x11.c,compat.c}`, `src/game.c`, `src/pieces.h`, `src/game.h`, `src/term.h`, `src/vt1200/scoredb.c`, `src/vt1200/stdio.h`, `src/vt1200/inttypes.h`.
- Produces: the termtris board, panels, move, and quit in MAME, with the proven `play_keys` marker acceptance.
- [ ] **Step 1: Fetch and pin**
```sh
git clone --depth 1 --branch termtris \
https://git.wut.ee/opencode.energia.arti.ee/vt1200.git \
vendor/vt1200-termtris
git -C vendor/vt1200-termtris rev-parse HEAD # be5b323...
```
Keep `vendor/` ignored; the project test references the pinned checkout. The termtris `VT1200.md`/spec/plan and `apps/termtris/tests/play_keys.{sh,lua}` are the reference.
- [ ] **Step 2: Build termtris with this compiler**
`scripts/m4/termtris/build.sh` compiles the termtris sources with `tms-clang -O2 -I vendor/vt1200-termtris/apps/termtris/src -I .../src/vt1200` + `llc -O0`, concatenates the M4 entry (`_start` calls `_main`; `.data` is copied by the loader; `_cstack`) with the termtris `.s` and the function-pointer Xlib header, links with GSPA/GSPLNK (RTS.LIB for multiply/divide), and emits `build/termtris.coff`/`.map`.
- [ ] **Step 3: Run the play_keys acceptance**
Adapt the termtris `play_keys.lua`/`play_keys.sh`: the `move`/`panels`/`quit` modes, the tablet click + `A/D/S/W/0/P/R/H/Q` injection, the `DIAG_LOOPS/EVENTS/FOCUS/REV/QUIT/TICK` addresses from the M4 `.map`, `LOADER_AT=2500`, `LOADER_END=3200`, `LOADER_SNAP=1`, `VT1200_SKIP_LEVEL1=1`, the loader, and the M4 COFF. Require:
- `loops >= 100` and `events >= 6` for `move`/`panels`;
- `quit = 0` for `move`/`panels`, `quit = 1` for `quit`;
- `focus` = the termtris window (nonzero);
- the board/panels/move/quit screenshots.
The termtris's proven values were `move loops=333 events=18`, `panels loops=323 events=12`, `quit loops=277 events=10 quit=1`; the M4 run may differ in counts, but the thresholds and quit/focus must hold.
- [ ] **Step 4: Record and commit**
The termtris build proves: function pointers (`term_*` callbacks/absolute Xlib pointers), indirect calls, `XSelectInput`/`XNextEvent`/`XLookupString`, the XG block, `XCreateGC`/`XSetForeground`/`XFillRectangle`/`XDrawString`, the timer/sleep, multiply/divide, `sprintf`/compat, and the loader/child. If a termtris feature is genuinely outside M4, report it with a ruling and a specific follow-up; do not weaken the thresholds or fake the run.
Commit the M4 termtris scripts/tests and a short report; keep the vendored tree ignored.
### Task 8: Differential/dis34, docs, and push
**Files:**
- Create: `scripts/tests/m4/diff/{ti-fnptr.c,ti-fnptr.s,ti-image.c,ti-image.s,check.sh}`
- Modify: `README.md` (project), `build/llvm-clean/TMS34010.md` (compiler), `symbols/README.md`
- Push: compiler `m4`, project `m4`
**Interfaces:**
- Consumes: all previous tasks.
- Produces: stored TI goldens for function-pointer/call/image/struct-offset shape, M4 docs, and pushed `m4` branches.
- [ ] **Step 1: Produce the TI goldens**
Compile K&R/ANSI C for a function-pointer apply/callback and the Xlib image-call shape with the 1987 compiler; store `ti-fnptr.s`/`ti-image.s`; no DOSBox in CI. The check compares ABI shape: `CALL Rs`/indirect call, the old-STK push, the argument/return/`RETS 2`, the `XCreateImage`/`XPutImage` 10/9 argument pushes, the `Display`/`Screen`/`XImage` offsets (byte 0x90/0x8; the XImage fields).
- [ ] **Step 2: Write the diff check**
`scripts/tests/m4/diff/check.sh` generates our asm with `tms-clang` + `llc` and greps the stored goldens; accept TI spelling variants; fail loudly. Add negative controls for the `CALL`/the image argument order.
- [ ] **Step 3: Update the docs**
README M4: function pointers/indirect calls; zext narrow loads; multiply/divide/`rts.lib`; the image/event clients; the XG fix; the corrected `XMapWindow`/`XRaiseWindow`; the `XImage`/`XEvent` layouts; the termtris additional integration and its pinned branch; commands; M4 non-goals (XImage/XEvent beyond the tested calls, native COFF, floats/i64/varargs/structs by value; the M4/M5 boundary). TMS34010.md: the `CALL Rs` ABI, function pointers, zext patterns, multiply/divide libcalls, the TTI/loop policy, the XImage/XEvent lowering, the XG/ROM notes. symbols README: the corrected/extended v2.2 entries and the evidence.
- [ ] **Step 4: Run every M4 gate**
```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-build/test/CodeGen/TMS34010 --filter='(m2|m3|m4)-'
scripts/tests/m4/run.sh
scripts/tests/m4/dis-roundtrip.sh
scripts/tests/m4/layout.sh
scripts/tests/m4/symbols.sh
scripts/tests/m4/termtris.sh
scripts/tests/m4/diff/check.sh
tools/tms-bitaddr/tests/run.sh
scripts/tests/m3/run.sh
scripts/tests/m3/dis-roundtrip.sh
```
All green; the M3 gates still pass with the corrected `XMapWindow`/`XRaiseWindow`.
- [ ] **Step 5: Commit and push**
```sh
git -C build/llvm-clean add llvm/lib/Target/TMS34010 llvm/test/CodeGen/TMS34010 TMS34010.md
git -C build/llvm-clean commit -m "tms34010: M4 function pointers, XImage/events and termtris support"
git -C build/llvm-clean push origin m4
git add README.md symbols scripts/m4 scripts/tests/m4 scripts/m3
git commit -m "tests: M4 Xlib images, events and the termtris integration"
git push origin m4
```
Do not force-push, merge into `m0`, or change the default; the controller updates the git.wut.ee default to `m4` after the final review if the user asks.
---
## Execution Handoff
M4 closes the remaining Xlib path to a real image/event client and a full-game integration. M5 owns the native COFF/MCCodeEmitter, floats, varargs, bitfields, `long long`, the `JR`/branch-relaxation code-size pass, MMTM/MMFM, and deeper libc/runtime coverage. The compiler repo is canonical for target code; this project repo holds the plans, wrappers, symbols and integration tests. The pinned `termtris` tree is test-only (GPLv3), not part of the compiler or project distribution.
@@ -345,12 +345,15 @@ non-constant pointer expression by 8×.
`harness/sdk-scratch/dis2as34.py`). Emit section sizes and labels in bits. `harness/sdk-scratch/dis2as34.py`). Emit section sizes and labels in bits.
- Linker command files are CRLF; `SECTIONS` origins are bit addresses. - Linker command files are CRLF; `SECTIONS` origins are bit addresses.
- **Constant GEPs** become `symbol + bit_addend` (for example - **Constant GEPs** become `symbol + bit_addend` (for example
`inttoptr(add(ptrtoint(@.str), 16))`). Before M1 relies on it, verify that `inttoptr(add(ptrtoint(@.str), 16))`). M1 verifies the `MOVI` immediate
GSPA/gsplnk can carry and relocate a bit addend in both a `MOVI` immediate form with `&arr[1]`. M1 does not emit `.data`, `.rodata` or string literals
and a `.data` initializer. Fallbacks if they cannot: leave `symbol+addend` at all: any non-null initializer makes `llc` fail loudly. Verifying a
entirely to link time, materialize the addend at runtime (`MOVI symbol; `symbol+bit_addend` relocation in a `.data` initializer, a string literal
ADDI addend`), or pre-resolve in the linker script. Test a string literal with an offset, and a global pointer initializer is **deferred to M2**
with an offset, `&arr[1]`, and a global pointer initializer. along with `.data` support. Fallbacks if GSPA/gsplnk cannot carry the
addend then: leave `symbol+addend` entirely to link time, materialize the
addend at runtime (`MOVI symbol; ADDI addend`), or pre-resolve in the
linker script.
- Later: native `as34`/`ld34` plus a COFF converter/loader extension, or an - Later: native `as34`/`ld34` plus a COFF converter/loader extension, or an
LLVM MC COFF writer. `ld34` output is not COFF, so this is a separate step. LLVM MC COFF writer. `ld34` output is not COFF, so this is a separate step.
@@ -386,8 +389,9 @@ non-constant pointer expression by 8×.
verifier test; M1 adds the same program through `llc`, checking that late GEPs verifier test; M1 adds the same program through `llc`, checking that late GEPs
carry bit offsets and are not scaled twice (§7.3.1). The spike only reached carry bit offsets and are not scaled twice (§7.3.1). The spike only reached
`opt`; this is a new gate. `opt`; this is a new gate.
- **Constant-GEP relocation test**: `symbol + bit_addend` in `MOVI` and in - **Constant-GEP relocation test**: M1 covers `symbol + bit_addend` in a
`.data`, plus `&arr[1]` and a global pointer initializer. `MOVI` with `&arr[1]`. The `.data` initializer, string-literal and global
pointer initializer parts are deferred to M2 with `.data` support.
- MAME end-to-end: compile a C file, assemble/link, load via - MAME end-to-end: compile a C file, assemble/link, load via
`dwt/loader/loader.lua`, check markers and frame. This is the CI gate. `dwt/loader/loader.lua`, check markers and frame. This is the CI gate.
- Real unit: `dwt/loader/hostload.py`, rdebug. Periodic/optional confirmation, - Real unit: `dwt/loader/hostload.py`, rdebug. Periodic/optional confirmation,
+80 -16
View File
@@ -1,28 +1,92 @@
#!/bin/sh #!/bin/sh
# Build the pinned LLVM 23.1.5 fork from a clean clone and the committed # Build the TMS34010 LLVM compiler.
# patch series. Intended for CI and for verifying that patches/llvm is the #
# complete fork delta. # Two modes:
#
# upstream + patches (M0, default)
# ./scripts/build-llvm.sh
# Clones llvmorg-23.1.5 into build/llvm-clean, applies patches/llvm and
# builds it. This mode owns its clone and resets it, but refuses to touch
# a checkout whose origin is not llvm-project.
#
# compiler fork (M1 and later)
# LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=tms34010 \
# ./scripts/build-llvm.sh
# LLVM_REPO=gitea@git.wut.ee:opencode.siil.arti.ee/llvm-tms34010.git \
# LLVM_BRANCH=tms34010 ./scripts/build-llvm.sh
# Build the fork branch in place. Fork mode never checks out, resets,
# cleans or patches the source tree; it only configures and builds it.
#
# Environment:
# LLVM_SRC source tree (default build/llvm-clean)
# LLVM_BUILD build tree (default build/llvm-clean-build)
# LLVM_REPO git URL to clone when LLVM_SRC does not exist
# LLVM_BRANCH branch to clone/build; setting this (or LLVM_REPO) selects
# fork mode
set -eu set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd) ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" cd "$ROOT"
mkdir -p build mkdir -p build
SRC=${LLVM_SRC:-build/llvm-clean} SRC=${LLVM_SRC:-build/llvm-clean}
BUILD=${LLVM_BUILD:-build/llvm-clean-build} BUILD=${LLVM_BUILD:-build/llvm-clean-build}
REPO=${LLVM_REPO:-}
BRANCH=${LLVM_BRANCH:-}
TAG=llvmorg-23.1.5 TAG=llvmorg-23.1.5
if [ ! -d "$SRC/.git" ]; then origin_url() {
git clone --depth 1 --branch "$TAG" \ git -C "$1" config --get remote.origin.url 2>/dev/null || true
https://github.com/llvm/llvm-project.git "$SRC" }
fi
git -C "$SRC" checkout --detach "$TAG"
git -C "$SRC" reset --hard
git -C "$SRC" clean -fdx
PATCHROOT=$(pwd)/patches/llvm if [ -n "$REPO" ] || [ -n "$BRANCH" ]; then
for p in "$PATCHROOT"/*.patch; do echo "== compiler fork mode: $SRC${BRANCH:+ (branch $BRANCH)}"
[ -e "$p" ] || continue if [ ! -e "$SRC/.git" ]; then
git -C "$SRC" apply "$p" [ -n "$REPO" ] || {
done echo "build-llvm: $SRC is not a git checkout; set LLVM_REPO to clone it" >&2
exit 1
}
if [ -n "$BRANCH" ]; then
git clone --branch "$BRANCH" "$REPO" "$SRC"
else
git clone "$REPO" "$SRC"
fi
fi
if [ -n "$BRANCH" ]; then
CURRENT=$(git -C "$SRC" rev-parse --abbrev-ref HEAD)
if [ "$CURRENT" != "$BRANCH" ]; then
echo "build-llvm: $SRC is on '$CURRENT', not '$BRANCH'" >&2
echo "build-llvm: check the branch out first; fork mode never" >&2
echo "build-llvm: modifies the source tree" >&2
exit 1
fi
fi
else
echo "== upstream + patches mode: $SRC (tag $TAG)"
if [ -e "$SRC/.git" ]; then
URL=$(origin_url "$SRC")
case "$URL" in
*llvm-project*) ;;
*)
echo "build-llvm: refusing to reset $SRC: origin is" >&2
echo "build-llvm: '${URL:-<none>}', not llvm-project." >&2
echo "build-llvm: use LLVM_REPO/LLVM_BRANCH for a compiler fork" >&2
echo "build-llvm: checkout (default mode must not reset it)" >&2
exit 1
;;
esac
else
git clone --depth 1 --branch "$TAG" \
https://github.com/llvm/llvm-project.git "$SRC"
fi
git -C "$SRC" checkout --detach "$TAG"
git -C "$SRC" reset --hard
git -C "$SRC" clean -fdx
PATCHROOT=$(pwd)/patches/llvm
for p in "$PATCHROOT"/*.patch; do
[ -e "$p" ] || continue
git -C "$SRC" apply "$p"
done
fi
cmake -S "$SRC/llvm" -B "$BUILD" -G Ninja \ cmake -S "$SRC/llvm" -B "$BUILD" -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON \ -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON \
@@ -30,4 +94,4 @@ cmake -S "$SRC/llvm" -B "$BUILD" -G Ninja \
-DLLVM_TARGETS_TO_BUILD="X86;TMS34010" \ -DLLVM_TARGETS_TO_BUILD="X86;TMS34010" \
-DLLVM_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=OFF \ -DLLVM_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=OFF \
-DLLVM_INCLUDE_BENCHMARKS=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF
ninja -C "$BUILD" clang opt llvm-config FileCheck count not ninja -C "$BUILD" clang opt llc llvm-config FileCheck count not
+1
View File
@@ -0,0 +1 @@
int add(int a, int b) { return a + b; }
+22
View File
@@ -0,0 +1,22 @@
; char-test.asm -- run the compiled char helpers and publish their results.
;
; Appended after char.s so _loadb/_storeb (and every earlier function) keep
; their linked addresses. _start calls this instead of calling _second
; directly, which is the same one-word-size calla; _char_test calls _second
; and returns its result in A0 so the existing second() marker still works.
;
; storeb(018A8080, 0x81) writes the byte 0x81
; loadb(018A8080) returns the sign-extended 0xFFFFFF81
.text
.globl _char_test
_char_test:
calla _second ; A0 = &arr[1]
move a0, a3 ; save it for the second() marker
movi 018A8080h, a0 ; p = storeb/loadb marker byte
movi 81h, a1 ; v = 0x81 (low byte stored by MOVB)
calla _storeb
calla _loadb ; A0 is still 018A8080
movi 018A8060h, a2
move a0, *a2, 1 ; 32-bit loadb marker
move a3, a0 ; restore &arr[1]
rets
+2
View File
@@ -0,0 +1,2 @@
int loadb(char *p) { return *p; }
void storeb(char *p, int v) { *p = v; }
+3
View File
@@ -0,0 +1,3 @@
static int arr[4];
int *second(void) { return &arr[1]; }
+232
View File
@@ -0,0 +1,232 @@
#!/bin/sh
# M1 dis34 round-trip.
#
# scripts/m1/dis-roundtrip.sh
#
# Extracts the linked .text bytes from build/m1/m1.coff (using the COFF
# section table, not a hard-coded file offset), disassembles them with the
# VT1200 tree's dis34 at the linked bit address, and checks that GSPA encoded
# the assembly llc printed:
#
# _add ADD A0, A1 ; MOVE A1, A0 ; RETS 0
# _load MOVE *A0, A0, 1 ; RETS 0
# _late ADDI 24, A0 ; RETS 0
# _second MOVI _arr+32, A0 ; RETS 0
# _loadb MOVB *A0, A0 ; RETS 0
# _storeb MOVB A1, *A0 ; RETS 0
#
# This verifies the 1987 GSPA assembler's encoding, not a dormant LLVM Inst
# encoding. Overridable: M1_COFF DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
COFF=${M1_COFF:-$ROOT/build/m1/m1.coff}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M1 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m1/run.sh)"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m1-dis34.XXXXXX")
FAIL_LOG=${M1_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m1-dis34-failure.txt}
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# Read little-endian fields out of the COFF. od keeps this dependency-free.
read_u8s() {
od -An -v -tu1 -j "$1" -N "$2" "$COFF"
}
u16() {
read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }'
}
u32() {
read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }'
}
echo "== parse the COFF section table ($COFF)"
nscns=$(u16 2)
opthdr=$(u16 16)
[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections (nscns='$nscns')"
[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header (opthdr='$opthdr')"
TEXT_ADDR=
TEXT_BITS=
TEXT_PTR=
sh_base=$((20 + opthdr))
i=0
while [ "$i" -lt "$nscns" ]; do
o=$((sh_base + i * 40))
name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000')
if [ "$name" = ".text" ]; then
TEXT_ADDR=$(u32 $((o + 8)))
TEXT_BITS=$(u32 $((o + 16)))
TEXT_PTR=$(u32 $((o + 20)))
break
fi
i=$((i + 1))
done
[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section"
[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty"
[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes"
# The linked layout this check was written against.
TEXT_HEX=$(printf '%08X' "$TEXT_ADDR")
ENTRY=$(u32 $((20 + 16)))
ENTRY_HEX=$(printf '%08X' "$ENTRY")
TEXT_BYTES=$((TEXT_BITS / 8))
[ "$TEXT_HEX" = "018A0000" ] \
|| fail ".text is linked at $TEXT_HEX, want 018A0000"
[ "$ENTRY_HEX" = "018A0000" ] \
|| fail "COFF entry point is $ENTRY_HEX, want 018A0000"
[ "$TEXT_PTR" -gt 0 ] || fail ".text has no raw data (scnptr=0)"
echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes), scnptr=$TEXT_PTR"
echo " entry point $ENTRY_HEX"
dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/text.bin") bytes, want $TEXT_BYTES"
echo "== resolve the linked symbols from the COFF symbol table"
symptr=$(u32 8)
nsyms=$(u32 12)
[ "$symptr" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table"
[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF symbol table is empty"
sym_addr() {
_want=$1
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
if [ "$_n" = "$_want" ]; then
u32 $((_o + 8))
return 0
fi
_aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }')
_i=$((_i + 1 + _aux))
done
return 1
}
get_sym() {
_name=$1
_addr=$(sym_addr "$_name") || fail "COFF symbol table has no $_name"
[ -n "$_addr" ] || fail "COFF symbol table has no $_name"
printf '%s' "$_addr"
}
ADD=$(get_sym _add)
LOAD=$(get_sym _load)
LATE=$(get_sym _late)
SECOND=$(get_sym _second)
LOADB=$(get_sym _loadb)
STOREB=$(get_sym _storeb)
ADD_HEX=$(printf '%08X' "$ADD")
LOAD_HEX=$(printf '%08X' "$LOAD")
LATE_HEX=$(printf '%08X' "$LATE")
SECOND_HEX=$(printf '%08X' "$SECOND")
LOADB_HEX=$(printf '%08X' "$LOADB")
STOREB_HEX=$(printf '%08X' "$STOREB")
[ "$ADD_HEX" = "018A0240" ] || fail "_add linked at $ADD_HEX, want 018A0240"
[ "$LOAD_HEX" = "018A0270" ] || fail "_load linked at $LOAD_HEX, want 018A0270"
[ "$LATE_HEX" = "018A0290" ] || fail "_late linked at $LATE_HEX, want 018A0290"
[ "$SECOND_HEX" = "018A02C0" ] || fail "_second linked at $SECOND_HEX, want 018A02C0"
[ "$LOADB_HEX" = "018A0300" ] || fail "_loadb linked at $LOADB_HEX, want 018A0300"
[ "$STOREB_HEX" = "018A0320" ] || fail "_storeb linked at $STOREB_HEX, want 018A0320"
echo " _add=$ADD_HEX _load=$LOAD_HEX _late=$LATE_HEX _second=$SECOND_HEX"
echo " _loadb=$LOADB_HEX _storeb=$STOREB_HEX (appended after _second)"
echo "== disassemble the emitted .text with dis34"
"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \
|| { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; }
[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output"
# match_at FILE DECIMAL_ADDR ERE
# dis34 -l prints "018a0240:\tadd a0, a1\t; 4001". The operand must be
# followed by optional whitespace and then the comment/end-of-line marker, so
# "rets 1" cannot satisfy a "rets" pattern.
match_at() {
_file=$1
_addr=$2
_pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
# expect_at LABEL DECIMAL_ADDR EXPECTED_TEXT ERE
expect_at() {
_label=$1
_addr=$2
_want=$3
_pat=$4
_laddr=$(printf '%08x' "$_addr")
if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then
echo " ok $_label @ 0x$_laddr: $_want"
return 0
fi
{
echo "M1 dis34 round-trip: FAIL"
echo " $_label at bit 0x$_laddr: expected $_want"
echo " dis34 output near 0x$_laddr:"
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { seen = 1 }
seen { print " " $0; n++ }
seen && n >= 3 { exit }
' "$TMP/dis34.txt"
if cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null; then
echo " full disassembly preserved at: $FAIL_LOG"
else
echo " full disassembly: $TMP/dis34.txt (temporary; removed on exit)"
fi
} >&2
exit 1
}
expect_label() {
_addr=$1
# dis34 labels branch targets as L followed by %X (no zero padding).
_laddr=$(printf '%x' "$_addr")
grep -Eiq "^L${_laddr}:" "$TMP/dis34.txt" \
|| fail "dis34 did not label the branch target L$(printf '%X' "$_addr")"
echo " ok branch target L$(printf '%X' "$_addr") is labelled"
}
echo "== check the linked instructions"
expect_label "$ADD"
expect_at "_add" "$ADD" "ADD A0, A1" 'add[[:space:]]+a0,[[:space:]]*a1'
expect_at "_add+16" $((ADD + 16)) "MOVE A1, A0" 'move[[:space:]]+a1,[[:space:]]*a0'
expect_at "_add+32" $((ADD + 32)) "RETS 0" 'rets([[:space:]]+0)?'
expect_label "$LOAD"
expect_at "_load" "$LOAD" "MOVE *A0, A0, 1" 'move[[:space:]]+\*a0,[[:space:]]*a0,[[:space:]]*1'
expect_at "_load+16" $((LOAD + 16)) "RETS 0" 'rets([[:space:]]+0)?'
expect_at "_late" "$LATE" "ADDI 24, A0" 'addi[[:space:]]+(0x18|24),[[:space:]]*a0'
expect_at "_late+32" $((LATE + 32)) "RETS 0" 'rets([[:space:]]+0)?'
expect_label "$SECOND"
expect_at "_second" "$SECOND" "MOVI _arr+32, A0" 'movi[[:space:]]+(0x)?18a9020,[[:space:]]*a0'
expect_at "_second+48" $((SECOND + 48)) "RETS 0" 'rets([[:space:]]+0)?'
# Negative control for the RETS operand boundary: "rets 1" must not match.
printf '018a0260:\trets\t1\t; 0960\n' > "$TMP/rets-neg.txt"
if match_at "$TMP/rets-neg.txt" $((ADD + 32)) 'rets([[:space:]]+0)?'; then
fail "negative control: the RETS pattern accepted 'rets 1'"
fi
echo " ok negative control @ 0x$(printf '%08x' $((ADD + 32))): 'rets 1' rejected"
expect_label "$LOADB"
expect_at "_loadb" "$LOADB" "MOVB *A0, A0" 'movb[[:space:]]+\*a0,[[:space:]]*a0'
expect_at "_loadb+16" $((LOADB + 16)) "RETS 0" 'rets([[:space:]]+0)?'
expect_at "_storeb" "$STOREB" "MOVB A1, *A0" 'movb[[:space:]]+a1,[[:space:]]*\*a0'
expect_at "_storeb+16" $((STOREB + 16)) "RETS 0" 'rets([[:space:]]+0)?'
echo "M1 dis34 round-trip: PASS"
+44
View File
@@ -0,0 +1,44 @@
; entry.asm -- M1 smoke-test entry, linked at bit 018A0000.
;
; M1 has no stack frame, no calls and no TI ABI. The generated functions are
; leaf functions: i32/pointer arguments in A0-A3, result in A0.
;
; Marker words (bit addresses, 0x20 bits apart = 4 bytes):
; 018A8000 add(20, 22)
; 018A8020 load(018A8100)
; 018A8040 second() == &arr[1]
; 018A8060 loadb(018A8080) == 0xFFFFFF81 (sign-extended 0x81)
; 018A8080 storeb(018A8080, 0x81) leaves the byte 0x81
;
; _late is linked but not called: it is a compile-time check that a surviving
; GEP offset is 24 bits, not 192.
;
; The char helpers are exercised by _char_test (char-test.asm, appended after
; the compiled functions). Calling it here instead of calling _second keeps
; this entry's size -- and therefore every function's linked address -- the
; same; _char_test calls _second and returns its result in A0.
.text
.globl _start
_start:
setf 32, 0, 1 ; FS1 = 32 for 32-bit MOVE
setf 16, 0, 0 ; FS0 = 16, for any i16 code
movi 20, a0
movi 22, a1
calla _add
movi 018A8000h, a2
move a0, *a2, 1
movi 018A8100h, a2
movi 12345678h, a3
move a3, *a2, 1
move a2, a0
calla _load
movi 018A8020h, a2
move a0, *a2, 1
calla _char_test
movi 018A8040h, a2
move a0, *a2, 1
rets
+9
View File
@@ -0,0 +1,9 @@
/* Late-GEP check: p + 3 bytes must reach llc as ADDI 24 -- the address unit is
* a bit and tms-bitaddr has already scaled the offset -- not ADDI 192.
*
* The plan's source also compared p + 3 with another pointer. That form
* lowers to setcc/branches, which the M1 backend deliberately does not have;
* this form exercises the same late-GEP contract without it. See the M1
* implementation design, sections 7.3 and 7.5.
*/
int late(char *p) { return (int)(p + 3); }
+16
View File
@@ -0,0 +1,16 @@
-a
-o M1.OUT
-m M1.MAP
-e _start
M1.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 018A9000h, length = 1000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
+1
View File
@@ -0,0 +1 @@
int load(int *p) { return *p; }
+44
View File
@@ -0,0 +1,44 @@
-- m1-check.lua -- load the M1 COFF with dwt/loader/loader.lua and read the
-- marker words the generated code wrote.
--
-- M1_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- M1_EXPECT_LOAD expected load() result (default 0x12345678)
-- M1_EXPECT_SECOND expected &arr[1] in bits
-- M1_EXPECT_LOADB expected sign-extended byte load (default 0xFFFFFF81)
-- M1_EXPECT_STOREB expected byte store (default 0x81)
--
-- The marker read runs one frame before loader.lua's inspection/exit frame.
dofile(os.getenv("M1_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local EXPECT_LOAD = tonumber(os.getenv("M1_EXPECT_LOAD") or "0x12345678")
local EXPECT_SECOND = tonumber(os.getenv("M1_EXPECT_SECOND") or "0x018A9020")
local EXPECT_LOADB = tonumber(os.getenv("M1_EXPECT_LOADB") or "0xFFFFFF81")
local EXPECT_STOREB = tonumber(os.getenv("M1_EXPECT_STOREB") or "0x81")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local add = prog:read_u32(0x018A8000)
local load = prog:read_u32(0x018A8020)
local second = prog:read_u32(0x018A8040)
local loadb = prog:read_u32(0x018A8060)
local storeb = prog:read_u8(0x018A8080)
print(string.format(
"M1: add=%d load=0x%08X second=0x%08X loadb=0x%08X storeb=0x%02X",
add, load, second, loadb, storeb))
if add == 42 and load == EXPECT_LOAD and second == EXPECT_SECOND
and loadb == EXPECT_LOADB and storeb == EXPECT_STOREB then
print("M1 MAME: PASS")
else
print(string.format(
"M1 MAME: FAIL (want add=42 load=0x%08X second=0x%08X loadb=0x%08X storeb=0x%02X)",
EXPECT_LOAD, EXPECT_SECOND, EXPECT_LOADB, EXPECT_STOREB))
end
end
end, "m1-check")
+167
View File
@@ -0,0 +1,167 @@
#!/bin/sh
# M1 end-to-end smoke test.
#
# scripts/m1/run.sh
#
# Compiles six C functions with scripts/tms-clang + llc -O0, assembles and
# links them with the 1987 GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF
# into MAME through dwt/loader/loader.lua and checks the marker words:
#
# add(20, 22) == 42
# load(&word) == 0x12345678
# second() == &arr[1] (arr + 32 bits)
# loadb(&byte) == 0xFFFFFF81 (sign-extended 0x81)
# storeb(&byte, 0x81) leaves the byte 0x81
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF
# SDK_IMG M1_WORK M1_COFF M1_SDK LOADER_AT LOADER_END
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$ROOT/scripts/m1
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${M1_WORK:-$ROOT/build/m1}
COFF=${M1_COFF:-$WORK/m1.coff}
SDK=${M1_SDK:-$ROOT/build/m1-sdk}
ASM=$WORK/asm
LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua
LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
LOADER_AT=${LOADER_AT:-2500}
LOADER_END=${LOADER_END:-2700}
fail() {
echo "m1: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg"
mkdir -p "$(dirname "$COFF")"
echo "== compile and select"
export TMS_CLANG TMS_OPT TMS_BITADDR
for name in add load late const_gep char; do
"$ROOT/scripts/tms-clang" -O2 -o "$ASM/$name.ll" "$DIR/$name.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $name.c"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
echo "== check the bit-address constants"
grep -Eq '^[[:space:]]*ADDI[[:space:]]+24,[[:space:]]*A0' "$ASM/late.s" \
|| fail "late.s has no ADDI 24 (late GEP got scaled again?)"
grep -Eq 'ADDI[[:space:]]+192' "$ASM/late.s" \
&& fail "late.s contains ADDI 192: late GEP was scaled twice"
grep -Eq '^[[:space:]]*MOVI[[:space:]]+_arr\+32,[[:space:]]*A0' "$ASM/const_gep.s" \
|| fail "const_gep.s has no 'MOVI _arr+32': constant GEP addend is not in bits"
grep -Eq '_arr\+4[^0-9]' "$ASM/const_gep.s" \
&& fail "const_gep.s references _arr+4 (bytes) instead of _arr+32 (bits)"
echo " late.s: $(grep -E 'ADDI' "$ASM/late.s" | head -1 | sed 's/^[[:space:]]*//')"
echo " const_gep.s: $(grep -E 'MOVI' "$ASM/const_gep.s" | head -1 | sed 's/^[[:space:]]*//')"
echo "== extract the 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
cat "$DIR/entry.asm" "$ASM/add.s" "$ASM/load.s" "$ASM/late.s" \
"$ASM/const_gep.s" "$ASM/char.s" "$DIR/char-test.asm" > "$SDK/M1.ASM"
# The linker command file must keep CRLF line endings.
sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M1.CMD"
rm -f "$SDK/M1.OBJ" "$SDK/M1.OUT" "$SDK/M1.MAP" "$SDK/M1.LST"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa M1 M1.obj M1.lst -l" \
-c "gsplnk M1.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; }
[ -f "$SDK/M1.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/M1.LST" > "$WORK/m1.lst"
grep -q 'No Errors' "$WORK/m1.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/m1.lst" >&2 || true; fail "GSPA reported errors"; }
grep -q 'No Warnings' "$WORK/m1.lst" \
|| { grep -i -B2 -A2 'warning' "$WORK/m1.lst" >&2 || true; fail "GSPA reported warnings"; }
[ -f "$SDK/M1.OUT" ] || fail "GSPLNK produced no M1.OUT"
sed 's/\r$//' "$SDK/M1.MAP" > "$WORK/m1.map"
cp "$SDK/M1.OUT" "$COFF"
cp "$SDK/M1.ASM" "$WORK/m1.asm"
ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m1.map")
[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000"
grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m1.map" \
|| fail "linker map has no _start entry point"
BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m1.map")
BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m1.map")
[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section"
[ "$BSS_LENGTH" = "00000080" ] \
|| fail ".bss is $BSS_LENGTH bits, expected 00000080 (only arr[4])"
# Pin the required layout independently of the map: &arr[1] is arr + 32 bits
# and arr is the first object in the .bss section that link.cmd puts at ram.
[ "$BSS_ORIGIN" = "018A9000" ] \
|| fail "linker .bss origin is $BSS_ORIGIN, want 018A9000 (link.cmd ram origin)"
EXPECT_SECOND=0x018A9020
DERIVED_SECOND=$(printf '0x%08X' "$((0x$BSS_ORIGIN + 32))")
[ "$DERIVED_SECOND" = "$EXPECT_SECOND" ] \
|| fail ".bss origin gives &arr[1] = $DERIVED_SECOND, want $EXPECT_SECOND"
echo " linked .text at 018A0000, .bss at $BSS_ORIGIN (128 bits)"
echo " expected &arr[1] = $EXPECT_SECOND"
echo "== run the COFF in MAME"
[ -x "$MAME" ] || fail "MAME binary not built: $MAME"
[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA"
[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN"
VT1200_SKIP_LEVEL1=1 \
LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \
LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \
LOADER_OUT="$WORK/evidence" \
M1_LOADER_LUA="$LOADER_LUA" \
M1_EXPECT_LOAD=0x12345678 M1_EXPECT_SECOND="$EXPECT_SECOND" \
timeout "${MAME_TIMEOUT:-600}" "$MAME" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$DIR/m1-check.lua" -autoboot_delay 0 \
> "$WORK/mame.log" 2>&1 \
|| { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; }
MARKERS=$(grep -E '^M1: add=' "$WORK/mame.log" | tail -1 || true)
[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M1 markers"; }
echo " $MARKERS"
grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \
|| { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; }
grep -q 'placed .text .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .text bytes"
grep -q '^M1 MAME: PASS' "$WORK/mame.log" \
|| { tail -30 "$WORK/mame.log" >&2; fail "marker check failed"; }
echo "M1 MAME: PASS"
+3
View File
@@ -0,0 +1,3 @@
__attribute__((noinline)) int add2(int a, int b) { return a + b; }
__attribute__((noinline)) int first(int a, int b) { return a; }
__attribute__((noinline)) int second(int a, int b) { return b; }
+2
View File
@@ -0,0 +1,2 @@
extern int add2(int, int);
int caller(void) { return add2(20, 22); }
+3
View File
@@ -0,0 +1,3 @@
/* First ROM call: strlen("0::0") must be 4. */
extern int strlen(const char *);
int check(void) { return strlen("0::0"); }
+327
View File
@@ -0,0 +1,327 @@
#!/bin/sh
# M2 dis34 round-trip.
#
# scripts/m2/dis-roundtrip.sh
#
# Extracts the linked .text/.data from build/m2/m2.coff (using the COFF
# section table, not hard-coded file offsets), disassembles the text with the
# VT1200 tree's dis34 at the linked bit address, and checks that GSPA encoded
# the TI-ABI code llc printed:
#
# _strlen JAUC 3FB458D0 (the ROM stub, ABI-preserving)
# _first MOVE A13,A? ; ADDI -32,A? ; RETS 2
# _second ADDI -64,A? ; RETS 2
# _keep old-STK push, argument pushes, CALLA, SETF, LOADSTK, RETS 2
# _locals nonzero frame ADDI, a frame access, RETS 0
#
# It also checks the old-STK operand against the number of pushes before
# SETFP, and the .data bytes contain the "0::0" string literal.
#
# Overridable: M2_COFF DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
COFF=${M2_COFF:-$ROOT/build/m2/m2.coff}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M2 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m2/run.sh)"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m2-dis34.XXXXXX")
FAIL_LOG=${M2_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m2-dis34-failure.txt}
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# Read little-endian fields out of the COFF. od keeps this dependency-free.
read_u8s() {
od -An -v -tu1 -j "$1" -N "$2" "$COFF"
}
u16() {
read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }'
}
u32() {
read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }'
}
echo "== parse the COFF section table ($COFF)"
nscns=$(u16 2)
opthdr=$(u16 16)
[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections (nscns='$nscns')"
[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header (opthdr='$opthdr')"
TEXT_ADDR=
TEXT_BITS=
TEXT_PTR=
DATA_ADDR=
DATA_BITS=
DATA_PTR=
sh_base=$((20 + opthdr))
i=0
while [ "$i" -lt "$nscns" ]; do
o=$((sh_base + i * 40))
name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000')
if [ "$name" = ".text" ]; then
TEXT_ADDR=$(u32 $((o + 8)))
TEXT_BITS=$(u32 $((o + 16)))
TEXT_PTR=$(u32 $((o + 20)))
elif [ "$name" = ".data" ]; then
DATA_ADDR=$(u32 $((o + 8)))
DATA_BITS=$(u32 $((o + 16)))
DATA_PTR=$(u32 $((o + 20)))
fi
i=$((i + 1))
done
[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section"
[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty"
[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes"
[ -n "$DATA_ADDR" ] || fail "COFF has no .data section"
[ "$DATA_BITS" -gt 0 ] 2>/dev/null || fail ".data is empty"
[ $((DATA_BITS % 8)) -eq 0 ] || fail ".data size $DATA_BITS bits is not whole bytes"
TEXT_HEX=$(printf '%08X' "$TEXT_ADDR")
ENTRY=$(u32 $((20 + 16)))
ENTRY_HEX=$(printf '%08X' "$ENTRY")
TEXT_BYTES=$((TEXT_BITS / 8))
DATA_BYTES=$((DATA_BITS / 8))
[ "$TEXT_HEX" = "018A0000" ] \
|| fail ".text is linked at $TEXT_HEX, want 018A0000"
[ "$ENTRY_HEX" = "018A0000" ] \
|| fail "COFF entry point is $ENTRY_HEX, want 018A0000"
[ "$TEXT_PTR" -gt 0 ] || fail ".text has no raw data (scnptr=0)"
[ "$DATA_PTR" -gt 0 ] || fail ".data has no raw data (scnptr=0)"
echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes), scnptr=$TEXT_PTR"
echo " .data at $(printf '%08X' "$DATA_ADDR"): $DATA_BITS bits ($DATA_BYTES bytes), scnptr=$DATA_PTR"
echo " entry point $ENTRY_HEX"
dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/text.bin") .text bytes, want $TEXT_BYTES"
dd if="$COFF" of="$TMP/data.bin" bs=1 skip="$DATA_PTR" count="$DATA_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/data.bin")" -eq "$DATA_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/data.bin") .data bytes, want $DATA_BYTES"
echo "== resolve the linked symbols from the COFF symbol table"
symptr=$(u32 8)
nsyms=$(u32 12)
[ "$symptr" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table"
[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF symbol table is empty"
sym_addr() {
_want=$1
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
if [ "$_n" = "$_want" ]; then
u32 $((_o + 8))
return 0
fi
_aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }')
_i=$((_i + 1 + _aux))
done
return 1
}
get_sym() {
_name=$1
_addr=$(sym_addr "$_name") || fail "COFF symbol table has no $_name"
[ -n "$_addr" ] || fail "COFF symbol table has no $_name"
printf '%s' "$_addr"
}
STRLEN=$(get_sym _strlen)
FIRST=$(get_sym _first)
SECOND=$(get_sym _second)
KEEP=$(get_sym _keep)
LOCALS=$(get_sym _locals)
ADD2=$(get_sym _add2)
STRLEN_HEX=$(printf '%08X' "$STRLEN")
FIRST_HEX=$(printf '%08X' "$FIRST")
SECOND_HEX=$(printf '%08X' "$SECOND")
KEEP_HEX=$(printf '%08X' "$KEEP")
LOCALS_HEX=$(printf '%08X' "$LOCALS")
ADD2_HEX=$(printf '%08X' "$ADD2")
echo " _strlen=$STRLEN_HEX _first=$FIRST_HEX _second=$SECOND_HEX"
echo " _add2=$ADD2_HEX _keep=$KEEP_HEX _locals=$LOCALS_HEX"
echo "== disassemble the emitted .text with dis34"
"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \
|| { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; }
[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output"
# match_at FILE DECIMAL_ADDR ERE
# dis34 -l prints "018a05c0:\tmove a13, a0\t; 4da0". The operand must be
# followed by optional whitespace and then the comment/end-of-line marker, so
# "rets 1" cannot satisfy a "rets" pattern.
match_at() {
_file=$1
_addr=$2
_pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
# expect_at LABEL DECIMAL_ADDR EXPECTED_TEXT ERE
expect_at() {
_label=$1
_addr=$2
_want=$3
_pat=$4
_laddr=$(printf '%08x' "$_addr")
if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then
echo " ok $_label @ 0x$_laddr: $_want"
return 0
fi
{
echo "M2 dis34 round-trip: FAIL"
echo " $_label at bit 0x$_laddr: expected $_want"
echo " dis34 output near 0x$_laddr:"
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { seen = 1 }
seen { print " " $0; n++ }
seen && n >= 4 { exit }
' "$TMP/dis34.txt"
if cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null; then
echo " full disassembly preserved at: $FAIL_LOG"
else
echo " full disassembly: $TMP/dis34.txt (temporary; removed on exit)"
fi
} >&2
exit 1
}
# func_body DECIMAL_ADDR > FILE: the function's instructions, through RETS.
func_body() {
_laddr=$(printf '%08x' "$1")
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { on = 1 }
on { print }
on && low ~ /^[0-9a-f]+:[[:space:]]+rets([[:space:]]|;|$)/ { exit }
' "$TMP/dis34.txt"
}
# expect_in BODY EXPECTED_TEXT ERE
expect_in() {
_file=$1
_want=$2
_pat=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_want"
return 0
fi
echo "M2 dis34 round-trip: FAIL: missing $_want" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
# expect_seq BODY REGEX...: each pattern must occur, in order, after the last.
expect_seq() {
_file=$1
shift
_last=0
for _pat in "$@"; do
_line=$(awk -v start="$_last" -v pat="$_pat" '
NR > start && tolower($0) ~ pat { print NR; exit }
' "$_file")
[ -n "$_line" ] || {
echo "M2 dis34 round-trip: FAIL: call sequence missing /$_pat/" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
_last=$_line
done
echo " ok call sequence ($# instructions/operands, in order)"
}
echo "== check the linked instructions"
# ROM import stub: absolute unconditional jump, ABI-preserving.
expect_at "_strlen" "$STRLEN" "JAUC 3FB458D0" \
'jauc[[:space:]]+0x3fb458d0'
# _first must materialise its frame pointer and load argument 0 at FP(-32).
func_body "$FIRST" > "$TMP/first.txt"
expect_in "$TMP/first.txt" "_first MOVEFP (MOVE A13,A?)" \
'move[[:space:]]+a13,[[:space:]]*a[0-9]+'
expect_in "$TMP/first.txt" "_first ADDI -32" \
'addi[[:space:]]+(0xffe0|-0x20|-32),'
expect_in "$TMP/first.txt" "_first RETS 2" \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _second loads argument 1 at FP(-64).
func_body "$SECOND" > "$TMP/second.txt"
expect_in "$TMP/second.txt" "_second ADDI -64" \
'addi[[:space:]]+(0xffc0|-0x40|-64),'
expect_in "$TMP/second.txt" "_second RETS 2" \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _keep exercises the whole call/return path against _add2.
func_body "$KEEP" > "$TMP/keep.txt"
expect_seq "$TMP/keep.txt" \
'move[[:space:]]+a14,[[:space:]]*-\*sp,[[:space:]]*1' \
'move[[:space:]]+[^,]+,[[:space:]]*\*a14\+,[[:space:]]*1' \
'calla' \
'setf[[:space:]]+16,[[:space:]]*0,[[:space:]]*0' \
'setf[[:space:]]+32,[[:space:]]*0,[[:space:]]*1' \
'move[[:space:]]+\*sp\(0x[0-9a-f]+\),[[:space:]]*a14,[[:space:]]*1' \
'rets[[:space:]]+2([[:space:]]*(;|$))'
# _locals has a nonzero frame allocation, a frame access and RETS 0.
func_body "$LOCALS" > "$TMP/locals.txt"
expect_in "$TMP/locals.txt" "_locals frame ADDI" \
'addi[[:space:]]+0x0*[1-9a-f][0-9a-f]*,[[:space:]]*a14'
expect_in "$TMP/locals.txt" "_locals frame access" \
'move[[:space:]]+(a[0-9]+,[[:space:]]*\*a[0-9]+|\*a[0-9]+,[[:space:]]*a[0-9]+)'
expect_in "$TMP/locals.txt" "_locals RETS 0" \
'rets([[:space:]]+0)?([[:space:]]*(;|$))'
# Old-STK offset: the LOADSTK operand must count every callee-saved push
# before SETFP, plus the caller's pushed old STK. This is the check the M2
# lit tests deliberately leave to dis34.
echo "== check the old-STK restore offsets"
check_stk() {
_label=$1
_addr=$2
func_body "$_addr" > "$TMP/func.txt"
_pushes=$(awk '
tolower($0) ~ /move[[:space:]]+[a-z0-9]+,[[:space:]]*-\*sp,[[:space:]]*1/ { n++ }
tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*a13/ { print n; exit }
' "$TMP/func.txt")
[ -n "$_pushes" ] || fail "$_label has no SETFP (MOVE A14,A13)"
_operand=$(grep -Eio '\*sp\(0x[0-9a-f]+\)' "$TMP/func.txt" | head -1 \
| sed 's/.*0x//; s/).*//')
[ -n "$_operand" ] || fail "$_label has no LOADSTK (*SP(n),A14)"
_want=$(( (_pushes + 1) * 32 ))
_got=$((0x$_operand))
[ "$_got" -eq "$_want" ] \
|| fail "$_label restores A14 from *SP(0x$_operand) ($_got bits) but $_pushes pushes need $_want bits"
echo " ok $_label: $_pushes pushes -> *SP($(printf '0x%x' "$_want")), A14, 1"
}
check_stk "_add2" "$ADD2"
check_stk "_first" "$FIRST"
check_stk "_second" "$SECOND"
check_stk "_keep" "$KEEP"
# Negative control for the RETS operand boundary: "rets 1" must not match.
printf '%08x:\trets\t1\t; 0961\n' "$ADD2" > "$TMP/rets-neg.txt"
if match_at "$TMP/rets-neg.txt" "$ADD2" 'rets([[:space:]]+0)?'; then
fail "negative control: the RETS pattern accepted 'rets 1'"
fi
echo " ok negative control @ 0x$(printf '%08x' "$ADD2"): 'rets 1' rejected"
echo "== check the copied .data bytes"
# The check.c string literal "0::0\0" must survive llc -> GSPA -> COFF.
DATA_HEX=$(od -An -v -tx1 "$TMP/data.bin" | tr -d ' \n')
case "$DATA_HEX" in
*303a3a3000*) echo " ok .data contains 30 3A 3A 30 00 (\"0::0\")" ;;
*) fail ".data does not contain 30 3A 3A 30 00 (got $DATA_HEX)" ;;
esac
echo "M2 dis34 round-trip: PASS"
+59
View File
@@ -0,0 +1,59 @@
; entry.asm -- M2 TI ABI smoke-test entry, linked at bit 018A0000.
;
; Markers (0x20 bits apart):
; 018A8000 check() strlen("0::0") == 4
; 018A8020 first(20,22) == 20
; 018A8040 second(20,22) == 22
; 018A8060 caller() add2(20,22) == 42
; 018A8080 locals() == 7
; 018A80A0 keep(10) == 16 (g == 5 before the call, + 11)
.text
.globl _start
_start:
setf 32, 0, 1
setf 16, 0, 0
movi _cstack, a14
move a14, a13
calla _check
movi 018A8000h, a2
move a8, *a2, 1
; first(20, 22): last argument first, on the program stack
move a14, -*sp, 1
movi 22, a0
move a0, *a14+, 1
movi 20, a0
move a0, *a14+, 1
calla _first
movi 018A8020h, a2
move a8, *a2, 1
; second(20, 22)
move a14, -*sp, 1
movi 22, a0
move a0, *a14+, 1
movi 20, a0
move a0, *a14+, 1
calla _second
movi 018A8040h, a2
move a8, *a2, 1
calla _caller
movi 018A8060h, a2
move a8, *a2, 1
calla _locals
movi 018A8080h, a2
move a8, *a2, 1
move a14, -*sp, 1
movi 10, a0
move a0, *a14+, 1
calla _keep
movi 018A80A0h, a2
move a8, *a2, 1
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Generate absolute-jump stubs for the ROM entry points in a per-ROM symbol file.
# Usage: gen-rom-imports.sh symbols/vt1200-v22.syms OUT.asm
set -eu
SYMS=${1:?symbol file}
OUT=${2:?output file}
printf '\t.text\n' > "$OUT"
awk '
/^[[:space:]]*#/ || NF < 2 { next }
{
name = $1
addr = $2
sub(/^0x/, "", addr)
printf "\t.globl _%s\n_%s:\n\tjauc %sh\n", name, name, addr
}
' "$SYMS" >> "$OUT"
+3
View File
@@ -0,0 +1,3 @@
extern int add2(int, int);
int g = 5; /* initialized .data: also tests the loader copy */
int keep(int a) { int x = g; int y = add2(a, 1); return x + y; }
+16
View File
@@ -0,0 +1,16 @@
-a
-o M2.OUT
-m M2.MAP
-e _start
M2.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 018A9000h, length = 40000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
+1
View File
@@ -0,0 +1 @@
int locals(void) { volatile int x = 7; return x; }
+51
View File
@@ -0,0 +1,51 @@
-- m2-check.lua -- load the M2 COFF with dwt/loader/loader.lua and read the
-- marker words the compiled TI-ABI code wrote.
--
-- M2_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- M2_EXPECT_CHECK 4 (strlen("0::0"))
-- M2_EXPECT_FIRST 20 (first(20,22))
-- M2_EXPECT_SECOND 22 (second(20,22))
-- M2_EXPECT_CALLER 42 (add2(20,22))
-- M2_EXPECT_LOCALS 7
-- M2_EXPECT_KEEP 16 (g == 5, + keep(10))
--
-- The marker read runs one frame before loader.lua's inspection/exit frame.
dofile(os.getenv("M2_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local EXPECT_CHECK = tonumber(os.getenv("M2_EXPECT_CHECK") or "4")
local EXPECT_FIRST = tonumber(os.getenv("M2_EXPECT_FIRST") or "20")
local EXPECT_SECOND = tonumber(os.getenv("M2_EXPECT_SECOND") or "22")
local EXPECT_CALLER = tonumber(os.getenv("M2_EXPECT_CALLER") or "42")
local EXPECT_LOCALS = tonumber(os.getenv("M2_EXPECT_LOCALS") or "7")
local EXPECT_KEEP = tonumber(os.getenv("M2_EXPECT_KEEP") or "16")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local check = prog:read_u32(0x018A8000)
local first = prog:read_u32(0x018A8020)
local second = prog:read_u32(0x018A8040)
local caller = prog:read_u32(0x018A8060)
local locals = prog:read_u32(0x018A8080)
local keep = prog:read_u32(0x018A80A0)
print(string.format(
"M2: check=%d first=%d second=%d caller=%d locals=%d keep=%d",
check, first, second, caller, locals, keep))
if check == EXPECT_CHECK and first == EXPECT_FIRST
and second == EXPECT_SECOND and caller == EXPECT_CALLER
and locals == EXPECT_LOCALS and keep == EXPECT_KEEP then
print("M2 MAME: PASS")
else
print(string.format(
"M2 MAME: FAIL (want check=%d first=%d second=%d caller=%d locals=%d keep=%d)",
EXPECT_CHECK, EXPECT_FIRST, EXPECT_SECOND, EXPECT_CALLER,
EXPECT_LOCALS, EXPECT_KEEP))
end
end
end, "m2-check")
+171
View File
@@ -0,0 +1,171 @@
#!/bin/sh
# M2 end-to-end smoke test: TI ABI + a real ROM strlen call.
#
# scripts/m2/run.sh
#
# Compiles five C translation units with scripts/tms-clang + llc -O0, assembles
# and links them with the 1987 GSPA/GSPLNK 2.00 under DOSBox, then loads the
# COFF into MAME through dwt/loader/loader.lua and checks the marker words:
#
# check() strlen("0::0") == 4
# first(20,22) == 20
# second(20,22) == 22
# caller() add2(20,22) == 42
# locals() == 7
# keep(10) == 16
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF
# SDK_IMG M2_WORK M2_COFF M2_SDK LOADER_AT LOADER_END
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$ROOT/scripts/m2
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${M2_WORK:-$ROOT/build/m2}
COFF=${M2_COFF:-$WORK/m2.coff}
SDK=${M2_SDK:-$ROOT/build/m2-sdk}
ASM=$WORK/asm
LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua
LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
LOADER_AT=${LOADER_AT:-2500}
LOADER_END=${LOADER_END:-2700}
SYMS=${M2_SYMS:-$ROOT/symbols/vt1200-v22.syms}
fail() {
echo "m2: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg"
mkdir -p "$(dirname "$COFF")"
echo "== compile and select"
export TMS_CLANG TMS_OPT TMS_BITADDR
for name in check args caller locals keep; do
"$ROOT/scripts/tms-clang" -O2 -o "$ASM/$name.ll" "$DIR/$name.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $name.c"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
# strlen("0::0") must be a real ROM call, not a folded 4.
grep -q 'CALLA _strlen' "$ASM/check.s" \
|| fail "check.s has no 'CALLA _strlen' (strlen was folded or not called)"
echo " check.s: $(grep -E 'CALLA _strlen' "$ASM/check.s" | head -1 | sed 's/^[[:space:]]*//')"
echo "== generate the per-ROM import stubs"
"$DIR/gen-rom-imports.sh" "$SYMS" "$WORK/rom-imports.asm"
grep -q '^_strlen:' "$WORK/rom-imports.asm" \
|| fail "rom-imports.asm has no _strlen stub"
grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | grep -qi 'jauc 3FB458D0h' \
|| fail "rom-imports.asm _strlen stub does not jump to 3FB458D0h"
echo " _strlen: $(grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | tail -1 | sed 's/^[[:space:]]*//')"
echo "== extract the 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
cat "$DIR/entry.asm" "$ASM/check.s" "$ASM/args.s" "$ASM/caller.s" \
"$ASM/locals.s" "$ASM/keep.s" "$WORK/rom-imports.asm" > "$SDK/M2.ASM"
# The linker command file must keep CRLF line endings.
sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M2.CMD"
rm -f "$SDK/M2.OBJ" "$SDK/M2.OUT" "$SDK/M2.MAP" "$SDK/M2.LST"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa M2 M2.obj M2.lst -l" \
-c "gsplnk M2.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; }
[ -f "$SDK/M2.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/M2.LST" > "$WORK/m2.lst"
grep -q 'No Errors' "$WORK/m2.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/m2.lst" >&2 || true; fail "GSPA reported errors"; }
grep -q 'No Warnings' "$WORK/m2.lst" \
|| { grep -i -B2 -A2 'warning' "$WORK/m2.lst" >&2 || true; fail "GSPA reported warnings"; }
[ -f "$SDK/M2.OUT" ] || fail "GSPLNK produced no M2.OUT"
sed 's/\r$//' "$SDK/M2.MAP" > "$WORK/m2.map"
cp "$SDK/M2.OUT" "$COFF"
cp "$SDK/M2.ASM" "$WORK/m2.asm"
ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m2.map")
[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000"
grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m2.map" \
|| fail "linker map has no _start entry point"
BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m2.map")
BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m2.map")
[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section"
[ "$BSS_ORIGIN" = "018A9000" ] \
|| fail "linker .bss origin is $BSS_ORIGIN, want 018A9000 (link.cmd ram origin)"
[ "$BSS_LENGTH" = "00008000" ] \
|| fail ".bss is $BSS_LENGTH bits, expected 00008000 (_cstack)"
DATA_ORIGIN=$(awk '$1 == ".data" && $2 == "0" { print $3; exit }' "$WORK/m2.map")
DATA_LENGTH=$(awk '$1 == ".data" && $2 == "0" { print $4; exit }' "$WORK/m2.map")
[ -n "$DATA_LENGTH" ] || fail "linker map has no .data section"
[ "$DATA_LENGTH" != "00000000" ] || fail ".data is empty (no initialized globals)"
echo " linked .text at 018A0000, .data at $DATA_ORIGIN ($DATA_LENGTH bits), .bss at $BSS_ORIGIN ($BSS_LENGTH bits)"
echo "== run the COFF in MAME"
[ -x "$MAME" ] || fail "MAME binary not built: $MAME"
[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA"
[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN"
VT1200_SKIP_LEVEL1=1 \
LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \
LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \
LOADER_OUT="$WORK/evidence" \
M2_LOADER_LUA="$LOADER_LUA" \
M2_EXPECT_CHECK=4 M2_EXPECT_FIRST=20 M2_EXPECT_SECOND=22 \
M2_EXPECT_CALLER=42 M2_EXPECT_LOCALS=7 M2_EXPECT_KEEP=16 \
timeout "${MAME_TIMEOUT:-600}" "$MAME" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$DIR/m2-check.lua" -autoboot_delay 0 \
> "$WORK/mame.log" 2>&1 \
|| { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; }
MARKERS=$(grep -E '^M2: check=' "$WORK/mame.log" | tail -1 || true)
[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M2 markers"; }
echo " $MARKERS"
grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \
|| { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; }
grep -q 'placed \.text .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .text bytes"
grep -q 'placed \.data .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .data bytes"
grep -q '^M2 MAME: PASS' "$WORK/mame.log" \
|| { tail -30 "$WORK/mame.log" >&2; fail "marker check failed"; }
echo "M2 MAME: PASS"
+23
View File
@@ -0,0 +1,23 @@
#include "vt1200_xlib.h"
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
int sumto(int n) {
int s = 0;
int i;
for (i = 1; i <= n; i++)
s += i;
return s;
}
int max2(int a, int b) { return a > b ? a : b; }
int lt(int a, int b) { return a < b; }
int branch_test(void) {
MARK(4) = (unsigned int)sumto(10);
MARK(5) = (unsigned int)max2(20, 22);
MARK(6) = (unsigned int)lt(5, 7);
MARK(7) = (unsigned int)layout_screens();
MARK(8) = (unsigned int)layout_root();
return 0;
}
+464
View File
@@ -0,0 +1,464 @@
#!/bin/sh
# M3 dis34 round-trip.
#
# scripts/m3/dis-roundtrip.sh
#
# Parses build/m3/m3.coff, disassembles its linked .text at 018A0000 with the
# VT1200 tree's dis34, and checks that GSPA encoded the compiler output:
#
# _XOpenDisplay/_XCreateSimpleWindow/_XMapWindow/_XSync JAUC <ROM bit addr>
# (XMapWindow is 3FD64F60, MapWindow opcode 8; 3FD645B0 is XRaiseWindow)
# _xhello: 1 push -> CALLA _XOpenDisplay, 9 pushes -> CALLA
# _XCreateSimpleWindow (arguments 1,0,2,200,400,100,100,root,
# display in that order, display pushed last), 2 -> _XMapWindow,
# 2 -> _XSync, each followed by the ABI's SETF 16/32 pair
# branch code: CMP, JALT, JAGT, JAEQ, JAUC and LBB… labels (never .LBB)
# .data: 30 3A 3A 30 00 ("0::0")
# old-STK restore offsets: *SP((pushes+1)*32),A14
#
# Overridable: M3_COFF M3_ASM DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
WORK=${M3_WORK:-$ROOT/build/m3}
COFF=${M3_COFF:-$WORK/m3.coff}
ASM=${M3_ASM:-$WORK/m3.asm}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M3 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m3/run.sh)"
[ -f "$ASM" ] || fail "linked assembly not found: $ASM (run scripts/m3/run.sh)"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m3-dis34.XXXXXX")
FAIL_LOG=${M3_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m3-dis34-failure.txt}
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# Read little-endian fields out of the COFF. od keeps this dependency-free.
read_u8s() {
od -An -v -tu1 -j "$1" -N "$2" "$COFF"
}
u16() {
read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }'
}
u32() {
read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }'
}
echo "== parse the COFF section table ($COFF)"
nscns=$(u16 2)
opthdr=$(u16 16)
[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections (nscns='$nscns')"
[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header (opthdr='$opthdr')"
TEXT_ADDR=
TEXT_BITS=
TEXT_PTR=
DATA_ADDR=
DATA_BITS=
DATA_PTR=
sh_base=$((20 + opthdr))
i=0
while [ "$i" -lt "$nscns" ]; do
o=$((sh_base + i * 40))
name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000')
if [ "$name" = ".text" ]; then
TEXT_ADDR=$(u32 $((o + 8)))
TEXT_BITS=$(u32 $((o + 16)))
TEXT_PTR=$(u32 $((o + 20)))
elif [ "$name" = ".data" ]; then
DATA_ADDR=$(u32 $((o + 8)))
DATA_BITS=$(u32 $((o + 16)))
DATA_PTR=$(u32 $((o + 20)))
fi
i=$((i + 1))
done
[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section"
[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty"
[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes"
[ -n "$DATA_ADDR" ] || fail "COFF has no .data section"
[ "$DATA_BITS" -gt 0 ] 2>/dev/null || fail ".data is empty"
[ $((DATA_BITS % 8)) -eq 0 ] || fail ".data size $DATA_BITS bits is not whole bytes"
TEXT_HEX=$(printf '%08X' "$TEXT_ADDR")
ENTRY=$(u32 $((20 + 16)))
ENTRY_HEX=$(printf '%08X' "$ENTRY")
TEXT_BYTES=$((TEXT_BITS / 8))
DATA_BYTES=$((DATA_BITS / 8))
[ "$TEXT_HEX" = "018A0000" ] \
|| fail ".text is linked at $TEXT_HEX, want 018A0000"
[ "$ENTRY_HEX" = "018A0000" ] \
|| fail "COFF entry point is $ENTRY_HEX, want 018A0000"
[ "$TEXT_PTR" -gt 0 ] || fail ".text has no raw data (scnptr=0)"
[ "$DATA_PTR" -gt 0 ] || fail ".data has no raw data (scnptr=0)"
echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes), scnptr=$TEXT_PTR"
echo " .data at $(printf '%08X' "$DATA_ADDR"): $DATA_BITS bits ($DATA_BYTES bytes), scnptr=$DATA_PTR"
echo " entry point $ENTRY_HEX"
dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/text.bin") .text bytes, want $TEXT_BYTES"
dd if="$COFF" of="$TMP/data.bin" bs=1 skip="$DATA_PTR" count="$DATA_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/data.bin")" -eq "$DATA_BYTES" ] \
|| fail "extracted $(wc -c < "$TMP/data.bin") .data bytes, want $DATA_BYTES"
echo "== resolve the linked symbols from the COFF symbol table"
symptr=$(u32 8)
nsyms=$(u32 12)
[ "$symptr" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table"
[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF symbol table is empty"
sym_addr() {
_want=$1
_strtab=$((symptr + nsyms * 18))
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
# COFF inline names are 8 bytes; longer names are 0,0,0,0 + a
# string-table offset (the table starts with its own 4-byte size).
if [ "$(u32 "$_o")" -eq 0 ]; then
_soff=$(u32 $((_o + 4)))
_n=$(dd if="$COFF" bs=1 skip=$((_strtab + _soff)) count=64 2>/dev/null |
tr '\000' '\n' | head -1)
else
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
fi
if [ "$_n" = "$_want" ]; then
u32 $((_o + 8))
return 0
fi
_aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }')
_i=$((_i + 1 + _aux))
done
return 1
}
get_sym() {
_name=$1
_addr=$(sym_addr "$_name") || fail "COFF symbol table has no $_name"
[ -n "$_addr" ] || fail "COFF symbol table has no $_name"
printf '%s' "$_addr"
}
XOPEN=$(get_sym _XOpenDisplay)
XCREATE=$(get_sym _XCreateSimpleWindow)
XMAP=$(get_sym _XMapWindow)
XSYNC=$(get_sym _XSync)
XHELLO=$(get_sym _xhello)
SUMTO=$(get_sym _sumto)
MAX2=$(get_sym _max2)
LT=$(get_sym _lt)
BRANCH=$(get_sym _branch_test)
ROOTPROBE=$(get_sym _root_probe)
XOPEN_HEX=$(printf '%08X' "$XOPEN")
XCREATE_HEX=$(printf '%08X' "$XCREATE")
XMAP_HEX=$(printf '%08X' "$XMAP")
XSYNC_HEX=$(printf '%08X' "$XSYNC")
XHELLO_HEX=$(printf '%08X' "$XHELLO")
XOPEN_L=$(printf '%x' "$XOPEN")
XCREATE_L=$(printf '%x' "$XCREATE")
XMAP_L=$(printf '%x' "$XMAP")
XSYNC_L=$(printf '%x' "$XSYNC")
echo " _XOpenDisplay=$XOPEN_HEX _XCreateSimpleWindow=$XCREATE_HEX"
echo " _XMapWindow=$XMAP_HEX _XSync=$XSYNC_HEX"
echo " _xhello=$XHELLO_HEX _sumto=$(printf '%08X' "$SUMTO") _max2=$(printf '%08X' "$MAX2") _lt=$(printf '%08X' "$LT")"
echo " _branch_test=$(printf '%08X' "$BRANCH") _root_probe=$(printf '%08X' "$ROOTPROBE")"
echo "== disassemble the emitted .text with dis34"
"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \
|| { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; }
[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output"
# match_at FILE DECIMAL_ADDR ERE
match_at() {
_file=$1
_addr=$2
_pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
expect_at() {
_label=$1
_addr=$2
_want=$3
_pat=$4
_laddr=$(printf '%08x' "$_addr")
if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then
echo " ok $_label @ 0x$_laddr: $_want"
return 0
fi
{
echo "M3 dis34 round-trip: FAIL"
echo " $_label at bit 0x$_laddr: expected $_want"
echo " dis34 output near 0x$_laddr:"
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { seen = 1 }
seen { print " " $0; n++ }
seen && n >= 4 { exit }
' "$TMP/dis34.txt"
if cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null; then
echo " full disassembly preserved at: $FAIL_LOG"
else
echo " full disassembly: $TMP/dis34.txt (temporary; removed on exit)"
fi
} >&2
exit 1
}
func_body() {
_laddr=$(printf '%08x' "$1")
awk -v a="$_laddr" '
{ low = tolower($0) }
low ~ ("^" a ":") { on = 1 }
on { print }
on && low ~ /^[0-9a-f]+:[[:space:]]+rets([[:space:]]|;|$)/ { exit }
' "$TMP/dis34.txt"
}
expect_in() {
_file=$1
_want=$2
_pat=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_want"
return 0
fi
echo "M3 dis34 round-trip: FAIL: missing $_want" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
# count_between FILE START_RE END_RE PUSH_RE: count push encodings after the
# START_RE line up to (not including) the END_RE line.
count_between() {
_file=$1
_start=$2
_end=$3
_push=$4
awk -v s="$_start" -v e="$_end" -v p="$_push" '
{ low = tolower($0) }
on && low ~ e { exit }
on && low ~ p { n++ }
low ~ s { on = 1 }
END { print n + 0 }
' "$_file"
}
echo "== check the ROM import stubs"
expect_at "_XOpenDisplay" "$XOPEN" "JAUC 3F9C2570" \
'jauc[[:space:]]+0x3f9c2570'
expect_at "_XCreateSimpleWindow" "$XCREATE" "JAUC 3FD41B90" \
'jauc[[:space:]]+0x3fd41b90'
expect_at "_XMapWindow" "$XMAP" "JAUC 3FD64F60" \
'jauc[[:space:]]+0x3fd64f60'
expect_at "_XSync" "$XSYNC" "JAUC 3FD9F6B0" \
'jauc[[:space:]]+0x3fd9f6b0'
echo "== check the compiled Xlib call sequence"
func_body "$XHELLO" > "$TMP/xhello.txt"
expect_in "$TMP/xhello.txt" "CALLA _XOpenDisplay ($XOPEN_HEX)" \
"calla[[:space:]]+l${XOPEN_L}"
expect_in "$TMP/xhello.txt" "CALLA _XCreateSimpleWindow ($XCREATE_HEX)" \
"calla[[:space:]]+l${XCREATE_L}"
expect_in "$TMP/xhello.txt" "CALLA _XMapWindow ($XMAP_HEX)" \
"calla[[:space:]]+l${XMAP_L}"
expect_in "$TMP/xhello.txt" "CALLA _XSync ($XSYNC_HEX)" \
"calla[[:space:]]+l${XSYNC_L}"
# The arguments are pushed last-first onto the program stack; the first
# argument (background=1 for XCreateSimpleWindow) is pushed last.
P1=$(count_between "$TMP/xhello.txt" '^[0-9a-f]+:[[:space:]]+setf[[:space:]]+16' \
"calla[[:space:]]+l${XOPEN_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P2=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XOPEN_L}" \
"calla[[:space:]]+l${XCREATE_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P3=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XCREATE_L}" \
"calla[[:space:]]+l${XMAP_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P4=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XMAP_L}" \
"calla[[:space:]]+l${XSYNC_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
[ "$P1" -eq 1 ] || fail "XOpenDisplay has $P1 argument pushes, want 1"
[ "$P2" -eq 9 ] || fail "XCreateSimpleWindow has $P2 argument pushes, want 9"
[ "$P3" -eq 2 ] || fail "XMapWindow has $P3 argument pushes, want 2"
[ "$P4" -eq 2 ] || fail "XSync has $P4 argument pushes, want 2"
echo " ok argument pushes: XOpenDisplay=1 XCreateSimpleWindow=9 XMapWindow=2 XSync=2"
# Counting pushes is not enough: the nine words must reach the callee in the
# ABI order. build/m3/m3.lst records background 1, border 0, border width 2,
# height 200, width 400, y 100, then x 100 (x reuses y's register, so there are
# six MOVIs for the seven constants), followed by the root (A2) and display
# (A0) pushes. Assert the immediates and push registers as one sequence, so a
# swapped pair fails even though the count stays nine, and require the final
# push before CALLA to be the display register.
awk '
tolower($0) ~ /calla[[:space:]]+l'"$XOPEN_L"'/ { on = 1; next }
tolower($0) ~ /calla[[:space:]]+l'"$XCREATE_L"'/ { exit }
!on { next }
tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*-\*sp,[[:space:]]*1/ { started = 1; next }
!started { next }
tolower($0) ~ /movi[[:space:]]+/ {
line = $0
sub(/^[^:]*:[[:space:]]+movi[[:space:]]+/, "", line)
sub(/[[:space:]]*;.*/, "", line)
sub(/[[:space:]]+/, " ", line)
split(line, p, /,[[:space:]]*/)
printf "IMM %s %s\n", tolower(p[1]), tolower(p[2])
next
}
tolower($0) ~ /move[[:space:]]+a[0-9]+,[[:space:]]*\*a14\+,[[:space:]]*1/ {
line = $0
sub(/^[^:]*:[[:space:]]+move[[:space:]]+/, "", line)
sub(/,.*/, "", line)
printf "PUSH %s\n", tolower(line)
next
}
' "$TMP/xhello.txt" > "$TMP/xcreate.seq"
cat > "$TMP/xcreate.want" <<'EOF'
IMM 0x1 a1
PUSH a1
IMM 0x0 a1
PUSH a1
IMM 0x2 a3
PUSH a3
IMM 0xc8 a3
PUSH a3
IMM 0x190 a3
PUSH a3
IMM 0x64 a3
PUSH a3
PUSH a3
PUSH a2
PUSH a0
EOF
if ! cmp -s "$TMP/xcreate.want" "$TMP/xcreate.seq"; then
{
echo "M3 dis34 round-trip: FAIL: XCreateSimpleWindow argument order"
echo " expected (background,border,border_width,height,width,y,x,root,display):"
sed 's/^/ /' "$TMP/xcreate.want"
echo " got:"
sed 's/^/ /' "$TMP/xcreate.seq"
} >&2
exit 1
fi
echo " ok XCreateSimpleWindow argument order: 1,0,2,200,400,100,100,root,display (display pushed last)"
# Xlib clobbers SETF; the compiler must re-establish 16/FS0 and 32/FS1 after
# every call. Check in the linked assembly, where CALLA symbols survive.
awk '
/^_xhello:/ { on = 1; next }
on && /-- End function/ { exit }
on && /^[[:space:]]*CALLA/ { pending = 1; next }
on && pending && /SETF[[:space:]]+16,[[:space:]]*0,[[:space:]]*0/ { saw16 = 1; next }
on && pending && saw16 && /SETF[[:space:]]+32,[[:space:]]*0,[[:space:]]*1/ {
n++; pending = 0; saw16 = 0; next
}
END { print n + 0 }
' "$ASM" > "$TMP/setf_count"
SETF_COUNT=$(cat "$TMP/setf_count")
[ "$SETF_COUNT" -eq 4 ] \
|| fail "found $SETF_COUNT post-call SETF 16/32 pairs in _xhello, want 4"
echo " ok four post-call SETF 16,0,0 + SETF 32,0,1 pairs"
# _xhello takes no arguments of its own (the nine-argument call is inside it),
# so it returns with RETS 0; the argument-taking branch functions use RETS 2.
func_body "$XHELLO" > "$TMP/xhello.txt"
expect_in "$TMP/xhello.txt" "_xhello RETS 0" 'rets([[:space:]]+0)?([[:space:]]*(;|$))'
for pair in "$SUMTO:_sumto" "$MAX2:_max2" "$LT:_lt"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
expect_in "$TMP/fn.txt" "$label RETS 2" 'rets[[:space:]]+2([[:space:]]*(;|$))'
done
echo "== check the branch code"
for pair in "$SUMTO:_sumto" "$MAX2:_max2" "$LT:_lt"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
if ! grep -Eiq 'cmp' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no CMP"
fi
if ! grep -Eiq '\bjauc\b' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no JAUC"
fi
echo " ok $label: CMP + JAUC"
done
for pair in "$XHELLO:_xhello"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
if ! grep -Eiq '\bjauc\b' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no JAUC"
fi
echo " ok $label: JAUC"
done
func_body "$BRANCH" > "$TMP/branch.txt"
for callee in _sumto _max2 _lt _layout_screens _layout_root; do
grep -Eiq "calla[[:space:]]+L$(get_sym "$callee" | xargs printf '%X')" "$TMP/branch.txt" \
|| fail "_branch_test does not call $callee"
done
echo " ok _branch_test calls sumto, max2, lt, layout_screens, layout_root"
# Conditional branch opcodes emitted by this compiled image. JALE/JALO/JAHI
# coverage lives in the m3-loop/m3-unsigned/m3-cmpi lit tests; this image has
# the signed compare, loop-exit and smax branches.
for op in jalt jagt jaeq; do
grep -Eiq "^[0-9a-f]+:[[:space:]]+$op([[:space:]]|;|$)" "$TMP/dis34.txt" \
|| fail "no $op instruction in the linked .text"
echo " ok $op present"
done
# GSPA rejects dotted .L labels; the harness gives each TU an LBB prefix.
grep -q 'LBB' "$ASM" || fail "linked assembly has no LBB block labels"
if grep -q '\.LBB' "$ASM"; then
fail "linked assembly contains dotted .LBB labels (GSPA rejects them)"
fi
echo " ok LBB… block labels (no dot)"
echo "== check the old-STK restore offsets"
check_stk() {
_label=$1
_addr=$2
func_body "$_addr" > "$TMP/func.txt"
_pushes=$(awk '
tolower($0) ~ /move[[:space:]]+[a-z0-9]+,[[:space:]]*-\*sp,[[:space:]]*1/ { n++ }
tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*a13/ { print n; exit }
' "$TMP/func.txt")
[ -n "$_pushes" ] || fail "$_label has no SETFP (MOVE A14,A13)"
_operand=$(grep -Eio '\*sp\(0x[0-9a-f]+\)' "$TMP/func.txt" | head -1 \
| sed 's/.*0x//; s/).*//')
[ -n "$_operand" ] || fail "$_label has no LOADSTK (*SP(n),A14)"
_want=$(( (_pushes + 1) * 32 ))
_got=$((0x$_operand))
[ "$_got" -eq "$_want" ] \
|| fail "$_label restores A14 from *SP(0x$_operand) ($_got bits) but $_pushes pushes need $_want bits"
echo " ok $_label: $_pushes pushes -> *SP($(printf '0x%x' "$_want")), A14, 1"
}
check_stk "_sumto" "$SUMTO"
check_stk "_max2" "$MAX2"
check_stk "_lt" "$LT"
check_stk "_root_probe" "$ROOTPROBE"
# Negative control for the RETS operand boundary: "rets 1" must not match.
printf '%08x:\trets\t1\t; 0961\n' "$SUMTO" > "$TMP/rets-neg.txt"
if match_at "$TMP/rets-neg.txt" "$SUMTO" 'rets([[:space:]]+0)?'; then
fail "negative control: the RETS pattern accepted 'rets 1'"
fi
echo " ok negative control @ 0x$(printf '%08x' "$SUMTO"): 'rets 1' rejected"
echo "== check the copied .data bytes"
DATA_HEX=$(od -An -v -tx1 "$TMP/data.bin" | tr -d ' \n')
case "$DATA_HEX" in
*303a3a3000*) echo " ok .data contains 30 3A 3A 30 00 (\"0::0\")" ;;
*) fail ".data does not contain 30 3A 3A 30 00 (got $DATA_HEX)" ;;
esac
echo "M3 dis34 round-trip: PASS"
+25
View File
@@ -0,0 +1,25 @@
; entry.asm -- M3 entry, linked at bit 018A0000.
;
; Calls the compiled branch/loop tests and then the first compiled C Xlib
; client. Markers (0x20 bits apart):
; 018A8000 Display pointer (xhello)
; 018A8020 root window (xhello)
; 018A8040 window id (xhello)
; 018A8060 XSync diagnostic (xhello)
; 018A8080 sumto(10) == 55 (branch_test)
; 018A80A0 max2(20,22) == 22 (branch_test)
; 018A80C0 lt(5,7) == 1 (branch_test)
; 018A80E0 layout_screens() == 0x480
; 018A8100 layout_root() == 0x40
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _branch_test
calla _xhello
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+7
View File
@@ -0,0 +1,7 @@
struct display { char pad[0x90]; void *screens; };
struct screen { char pad[0x8]; unsigned int root; };
/* The 1987 compiler's &-of-member is a bit address, so these return the
ROM's bit offsets 0x480/0x40 directly. */
int layout_screens() { return (int)&((struct display *)0)->screens; }
int layout_root() { return (int)&((struct screen *)0)->root; }
+29
View File
@@ -0,0 +1,29 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _layout_screens
.file "LAYOUT.c"
******************************************************
* FUNCTION DEF : _layout_screens
******************************************************
_layout_screens:
MOVE A7,-*SP,1
MOVI 1152,A7
MOVE A7,A8
EPI0_1:
MOVE *SP+,A7,1
RETS 0
.globl _layout_root
******************************************************
* FUNCTION DEF : _layout_root
******************************************************
_layout_root:
MOVE A7,-*SP,1
MOVI 64,A7
MOVE A7,A8
EPI0_2:
MOVE *SP+,A7,1
RETS 0
.end
+21
View File
@@ -0,0 +1,21 @@
#include "vt1200_xlib.h"
/* The ROM's indirect displacements are bit offsets (xhello.asm):
Display.screens at bit 0x480, Screen.root at bit 0x40. C struct offsets
are bytes, so assert the byte offsets and return the ROM's bit offsets. */
_Static_assert(__builtin_offsetof(Display, screens) == 0x90,
"Display.screens must be at byte 0x90 (bit 0x480)");
_Static_assert(__builtin_offsetof(Screen, root) == 0x8,
"Screen.root must be at byte 0x8 (bit 0x40)");
int layout_screens(void) {
return (int)__builtin_offsetof(Display, screens) * 8;
}
int layout_root(void) {
return (int)__builtin_offsetof(Screen, root) * 8;
}
/* Exercise the real access path: vt1200_root() follows Display.screens (bit
0x480) and reads Screen.root (bit 0x40). The align-1 loads this generates
must select MOVE directly (see m3-unaligned.ll). */
unsigned int root_probe(Display *d) { return vt1200_root(d); }
+16
View File
@@ -0,0 +1,16 @@
-a
-o M3.OUT
-m M3.MAP
-e _start
M3.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 018A9000h, length = 40000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
+79
View File
@@ -0,0 +1,79 @@
-- m3-check.lua -- load the M3 COFF with dwt/loader/loader.lua, read the
-- compiled branch/Xlib marker words, and prove a window was mapped by reading
-- the white background pixel in VRAM.
--
-- M3_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_END loader inspection/exit frame
--
-- The marker read runs one frame before loader.lua's inspection/exit frame.
-- XSync is diagnostic only; the pass requires the visible white pixel.
--
-- The DWT root window is a fine monochrome weave, so one set bit at (150,150)
-- cannot distinguish the mapped client window from the root. Require the
-- whole 32-bit VRAM word containing (150,150) to be 0xFFFFFFFF, and check a
-- second interior word at (250,200) so the client area is proven solid, not
-- just a lucky white stripe.
dofile(os.getenv("M3_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "3400")
-- Window: XCreateSimpleWindow(display, root, 100, 100, 400, 200, 2, 0, 1).
-- The client area starts at (102,102); sample well inside it.
local VRAM_BASE = 0x04000000
local VRAM_PITCH = 2048 -- VR319: 2048 bits per row
local WHITE_WORD = 0xFFFFFFFF
local PX, PY = 150, 150
local PX2, PY2 = 250, 200
local frames = 0
local checked = false
local function vram_word(prog, x, y)
local bit = VRAM_BASE + y * VRAM_PITCH + x
local word = bit - (bit % 32)
return prog:read_u32(word), word, bit
end
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local display = prog:read_u32(0x018A8000)
local root = prog:read_u32(0x018A8020)
local window = prog:read_u32(0x018A8040)
local sync = prog:read_u32(0x018A8060)
local sum = prog:read_u32(0x018A8080)
local maxv = prog:read_u32(0x018A80A0)
local ltv = prog:read_u32(0x018A80C0)
local screens = prog:read_u32(0x018A80E0)
local rootoff = prog:read_u32(0x018A8100)
-- The full words containing each sample pixel.
local pix, word, bit = vram_word(prog, PX, PY)
local pix2, word2, bit2 = vram_word(prog, PX2, PY2)
local solid = pix == WHITE_WORD
local solid2 = pix2 == WHITE_WORD
print(string.format(
"M3: display=%u root=%u window=%u sync=%d sum=%d max=%d lt=%d screens=0x%x root_off=0x%x",
display, root, window, sync, sum, maxv, ltv, screens, rootoff))
print(string.format(
"M3: pixel(%d,%d) bit=0x%08X word=0x%08X value=0x%08X %s",
PX, PY, bit, word, pix, solid and "SOLID-WHITE" or "not-solid-white"))
print(string.format(
"M3: pixel(%d,%d) bit=0x%08X word=0x%08X value=0x%08X %s",
PX2, PY2, bit2, word2, pix2, solid2 and "SOLID-WHITE" or "not-solid-white"))
local ok = display ~= 0 and root ~= 0 and window ~= 0
and sum == 55 and maxv == 22 and ltv == 1
and screens == 0x480 and rootoff == 0x40
and solid and solid2
if ok then
print("M3 MAME: PASS")
else
print("M3 MAME: FAIL")
end
end
end, "m3-check")
+226
View File
@@ -0,0 +1,226 @@
#!/bin/sh
# M3 end-to-end test: compiled branches/loops plus the first compiled C Xlib
# client, linked with the 1987 GSPA/GSPLNK toolchain and run in MAME.
#
# scripts/m3/run.sh
#
# Compiles xhello.c, branch.c and layout.c with scripts/tms-clang -O2 and
# llc -O0, assembles/links them with the SDK under DOSBox, loads the COFF
# through dwt/loader/loader.lua, and checks:
#
# 018A8000 Display pointer (nonzero)
# 018A8020 root window id (nonzero)
# 018A8040 window id (nonzero)
# 018A8060 XSync result (diagnostic only)
# 018A8080 sumto(10) == 55
# 018A80A0 max2(20,22)== 22
# 018A80C0 lt(5,7) == 1
# 018A80E0 layout_screens() == 0x480
# 018A8100 layout_root() == 0x40
#
# XMapWindow is the true MapWindow entry 3FD64F60 (request opcode 8);
# 3FD645B0 is XRaiseWindow (ConfigureWindow opcode 12), not a mapping call.
#
# and, decisively, the full all-white VRAM word at (150,150) and another at
# (250,200) inside the mapped window. A markers-only or root-window bit is a
# failure.
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF
# SDK_IMG M3_WORK M3_COFF M3_SDK LOADER_AT LOADER_END
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$ROOT/scripts/m3
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${M3_WORK:-$ROOT/build/m3}
COFF=${M3_COFF:-$WORK/m3.coff}
SDK=${M3_SDK:-$ROOT/build/m3-sdk}
ASM=$WORK/asm
LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua
LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
LOADER_AT=${LOADER_AT:-2500}
LOADER_END=${LOADER_END:-3400}
SYMS=${M3_SYMS:-$ROOT/symbols/vt1200-v22.syms}
fail() {
echo "m3: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg"
mkdir -p "$(dirname "$COFF")"
# IndVarSimplify's exit-value replacement turns sumto's loop into an i33
# multiply (n*(n+1)/2), and this target has no multiply or i64 support: llc
# then dies selecting umul_lohi. Keep the loop (which is the point of the M3
# branch test) by disabling only that rewrite, and only for branch.c; xhello
# and layout use the normal optimizer pipeline. branch.c itself is unchanged.
OPT_NOREPLEXIT=$WORK/opt-keep-loops
printf '#!/bin/sh\nexec "%s" -replexitval=never "$@"\n' "$TMS_OPT" > "$OPT_NOREPLEXIT"
chmod +x "$OPT_NOREPLEXIT"
echo "== compile and select"
export TMS_CLANG TMS_BITADDR
for name in xhello branch layout; do
opt=$TMS_OPT
[ "$name" = branch ] && opt=$OPT_NOREPLEXIT
TMS_OPT=$opt "$ROOT/scripts/tms-clang" -O2 -I "$DIR" -o "$ASM/$name.ll" "$DIR/$name.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $name.c"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
# The Xlib calls must be real ROM imports, and the window must use background
# pixel 1 (white).
grep -q 'CALLA _XOpenDisplay' "$ASM/xhello.s" \
|| fail "xhello.s has no 'CALLA _XOpenDisplay'"
grep -q 'CALLA _XCreateSimpleWindow' "$ASM/xhello.s" \
|| fail "xhello.s has no 'CALLA _XCreateSimpleWindow'"
grep -q 'CALLA _XMapWindow' "$ASM/xhello.s" \
|| fail "xhello.s has no 'CALLA _XMapWindow'"
grep -q 'CALLA _XSync' "$ASM/xhello.s" \
|| fail "xhello.s has no 'CALLA _XSync'"
# 9 arguments: background=1, border=0, width=2, height=200, width=400, y=100,
# x=100, root, display. The first argument (background) is the last pushed,
# i.e. the first MOVI before the nine '*A14+' stores.
PUSHES=$(awk '
/MOVE .*, *\*A14\+, *1/ { n++ }
/CALLA/ { if ($0 ~ /_XCreateSimpleWindow/) print n; n = 0 }
' "$ASM/xhello.s")
[ "$PUSHES" = "9" ] || fail "XCreateSimpleWindow has $PUSHES pushes, want 9"
grep -q 'MOVI 1, ' "$ASM/xhello.s" \
|| fail "xhello.s has no background pixel 1 (white)"
echo " XCreateSimpleWindow: 9 argument pushes, background pixel 1"
echo "== generate the per-ROM import stubs"
"$ROOT/scripts/m2/gen-rom-imports.sh" "$SYMS" "$WORK/rom-imports.asm"
for sym in XOpenDisplay XCreateSimpleWindow XMapWindow XSync; do
grep -q "^_$sym:" "$WORK/rom-imports.asm" \
|| fail "rom-imports.asm has no _$sym stub"
done
grep -A1 '^_XOpenDisplay:' "$WORK/rom-imports.asm" | grep -qi 'jauc 3F9C2570h' \
|| fail "rom-imports.asm _XOpenDisplay stub does not jump to 3F9C2570h"
echo " _XOpenDisplay: $(grep -A1 '^_XOpenDisplay:' "$WORK/rom-imports.asm" | tail -1 | sed 's/^[[:space:]]*//')"
echo "== extract the 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
# GSPA 2.00 has one flat symbol table per assembly, and llc names basic blocks
# LBB0_1, LBB1_2, ... restarting in every translation unit. Concatenating the
# three TUs would therefore collide (DUPLICATE DEFINITION). Give each TU its
# own LBB prefix; the labels stay LBB… (GSPA rejects dotted .L labels).
for pair in branch:B layout:L xhello:X; do
src=${pair%%:*}
tag=${pair##*:}
sed "s/LBB\([0-9][0-9]*_[0-9][0-9]*\)/LBB$tag\1/g" \
"$ASM/$src.s" > "$ASM/$src.unique.s"
done
cat "$DIR/entry.asm" "$ASM/branch.unique.s" "$ASM/layout.unique.s" \
"$ASM/xhello.unique.s" "$WORK/rom-imports.asm" > "$SDK/M3.ASM"
# The linker command file must keep CRLF line endings.
sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M3.CMD"
rm -f "$SDK/M3.OBJ" "$SDK/M3.OUT" "$SDK/M3.MAP" "$SDK/M3.LST"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa M3 M3.obj M3.lst -l" \
-c "gsplnk M3.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; }
[ -f "$SDK/M3.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/M3.LST" > "$WORK/m3.lst"
grep -q 'No Errors' "$WORK/m3.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/m3.lst" >&2 || true; fail "GSPA reported errors"; }
grep -q 'No Warnings' "$WORK/m3.lst" \
|| { grep -i -B2 -A2 'warning' "$WORK/m3.lst" >&2 || true; fail "GSPA reported warnings"; }
[ -f "$SDK/M3.OUT" ] || fail "GSPLNK produced no M3.OUT"
sed 's/\r$//' "$SDK/M3.MAP" > "$WORK/m3.map"
cp "$SDK/M3.OUT" "$COFF"
cp "$SDK/M3.ASM" "$WORK/m3.asm"
ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m3.map")
[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000"
grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m3.map" \
|| fail "linker map has no _start entry point"
BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m3.map")
BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m3.map")
[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section"
[ "$BSS_ORIGIN" = "018A9000" ] \
|| fail "linker .bss origin is $BSS_ORIGIN, want 018A9000 (link.cmd ram origin)"
[ "$BSS_LENGTH" = "00008000" ] \
|| fail ".bss is $BSS_LENGTH bits, expected 00008000 (_cstack)"
DATA_ORIGIN=$(awk '$1 == ".data" && $2 == "0" { print $3; exit }' "$WORK/m3.map")
DATA_LENGTH=$(awk '$1 == ".data" && $2 == "0" { print $4; exit }' "$WORK/m3.map")
[ -n "$DATA_LENGTH" ] || fail "linker map has no .data section"
[ "$DATA_LENGTH" != "00000000" ] || fail ".data is empty (no \"0::0\" literal)"
echo " linked .text at 018A0000, .data at $DATA_ORIGIN ($DATA_LENGTH bits), .bss at $BSS_ORIGIN ($BSS_LENGTH bits)"
echo "== run the COFF in MAME"
[ -x "$MAME" ] || fail "MAME binary not built: $MAME"
[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA"
[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN"
# Start from empty NVRAM and pin the monitor: the pixel check assumes the real
# VR319 2048-bit row pitch, and stale NVRAM must not change the boot state.
rm -rf "$WORK/run/nvram"
mkdir -p "$WORK/run/nvram"
VT1200_MONITOR=VR319 \
VT1200_SKIP_LEVEL1=1 \
LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \
LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \
LOADER_OUT="$WORK/evidence" \
M3_LOADER_LUA="$LOADER_LUA" \
timeout "${MAME_TIMEOUT:-900}" "$MAME" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$DIR/m3-check.lua" -autoboot_delay 0 \
> "$WORK/mame.log" 2>&1 \
|| { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; }
MARKERS=$(grep -E '^M3: display=' "$WORK/mame.log" | tail -1 || true)
[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M3 markers"; }
echo " $MARKERS"
grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \
|| { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; }
grep -q 'placed \.text .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .text bytes"
grep -q 'placed \.data .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .data bytes"
grep -q '^M3 MAME: PASS' "$WORK/mame.log" \
|| { tail -30 "$WORK/mame.log" >&2; fail "M3 marker/window check failed"; }
echo "M3 MAME: PASS"
+65
View File
@@ -0,0 +1,65 @@
#!/bin/sh
# M3 ROM symbol validation: the four Xlib entry points used by the compiled
# client must exist in the v2.2 table, sit in the ROM's bit range, be 16-bit
# aligned, and disassemble to a plausible function prologue.
#
# This is a regression check, not a re-extraction: dwt/gui/xhello.asm and the
# termtris xlib.h are the evidence that these entries work. XMapWindow is the
# true map entry (MapWindow request opcode 8); 0x3FD645B0 is XRaiseWindow
# (ConfigureWindow opcode 12), never a mapping call. M4 extends the table for
# XImage/XEvent and validates every entry independently in
# scripts/m4/validate-syms.sh.
#
# scripts/m3/validate-syms.sh [symbols/vt1200-v22.syms]
#
# Overridable: M3_SYMS DIS34 ROM
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
SYMS=${1:-${M3_SYMS:-$ROOT/symbols/vt1200-v22.syms}}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
ROM=${ROM:-$ROOT/vendor/vt1200/vt1200_v22.bin}
ROM_LO=$((0x3F200000))
ROM_HI=$((0x3FFFFFFF))
fail() {
echo "m3 validate-syms: FAIL: $*" >&2
exit 1
}
[ -f "$SYMS" ] || fail "symbol file not found: $SYMS"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
[ -f "$ROM" ] || fail "ROM image not found: $ROM"
check_sym() { # check_sym NAME EXPECTED_HEX
name=$1
expected=$2
got=$(awk -v n="$name" '$1 == n { print $2; exit }' "$SYMS")
[ -n "$got" ] || fail "$SYMS has no $name entry"
[ "$got" = "$expected" ] \
|| fail "$name is $got, expected $expected (the xhello.asm-proven entry)"
addr=$((got))
[ "$addr" -ge "$ROM_LO" ] && [ "$addr" -le "$ROM_HI" ] \
|| fail "$name $got is outside the ROM bit range 3F200000h..3FFFFFFFh"
[ $((addr % 16)) -eq 0 ] \
|| fail "$name $got is not 16-bit aligned"
off=$(( (addr - ROM_LO) / 8 ))
first=$("$DIS34" -b "$got" -s "$off" "$ROM" 2>/dev/null |
awk 'NF && $1 !~ /^L[0-9A-Fa-f]+:$/ { print tolower($1); exit }')
[ -n "$first" ] || fail "$name $got disassembled to nothing"
case " move mmtm setf cmp addi " in
*" $first "*) ;;
*) fail "$name $got starts with '$first', not a plausible function prologue" ;;
esac
echo " ok $name $got: $first ..."
}
check_sym XOpenDisplay 0x3F9C2570
check_sym XCreateSimpleWindow 0x3FD41B90
check_sym XMapWindow 0x3FD64F60
check_sym XSync 0x3FD9F6B0
echo "m3 validate-syms: PASS (four v2.2 Xlib entries in ROM, aligned, prologues valid)"
+43
View File
@@ -0,0 +1,43 @@
#ifndef VT1200_XLIB_H
#define VT1200_XLIB_H
typedef unsigned int XID;
typedef unsigned int Window;
/* ROM v2.2 layout (xhello.asm). TMS34010 indirect displacements are *bit*
offsets: xhello's `*a11(0480h)` / `*a10(040h)` are bit offsets 0x480/0x40,
which are byte offsets 0x90/0x8. Cross-checks: the classic 32-bit
Screen.root is at byte 8, and the Terminal Manager scales its screen index
by 0x280 = sizeof(Screen) (0x50 bytes) * 8. */
typedef struct {
unsigned char pad[0x90];
void *screens;
} Display;
typedef struct {
unsigned char pad[0x8];
unsigned int root;
} Screen;
extern Display *XOpenDisplay(const char *name);
extern Window XCreateSimpleWindow(Display *display, Window parent,
int x, int y, unsigned width,
unsigned height, unsigned border_width,
unsigned long border,
unsigned long background);
extern int XMapWindow(Display *display, Window window);
extern int XSync(Display *display, int discard);
/* clang is invoked at -O0 (the optimizer runs separately), which marks every
definition noinline. Keep this helper always-inline so layout.c and the
Xlib client do not each emit a local _vt1200_root for GSPA/GSPLNK to
collide on. */
static inline __attribute__((always_inline)) unsigned int
vt1200_root(Display *d) {
return ((Screen *)((Display *)d)->screens)->root;
}
int layout_screens(void);
int layout_root(void);
#endif
+23
View File
@@ -0,0 +1,23 @@
#include "vt1200_xlib.h"
/*
* XMapWindow maps with the true MapWindow entry (0x3FD64F60, request opcode
* 8), not 0x3FD645B0: that address is XRaiseWindow (ConfigureWindow opcode
* 12, CWStackMode=Above).
*/
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
int xhello(void) {
Display *d = XOpenDisplay("0::0");
MARK(0) = (unsigned int)d;
if (!d)
return -1;
unsigned int root = vt1200_root(d);
MARK(1) = root;
Window w = XCreateSimpleWindow(d, root, 100, 100, 400, 200, 2, 0, 1);
MARK(2) = (unsigned int)w;
XMapWindow(d, w);
int s = XSync(d, 0);
MARK(3) = (unsigned int)s;
return (int)w;
}
+312
View File
@@ -0,0 +1,312 @@
#!/bin/sh
# M4 dis34 round-trip for the image/event client.
#
# scripts/m4/dis-roundtrip.sh
#
# Parses build/m4-xshow/m4.coff, disassembles its linked .text at 018A0000
# with dis34, and checks that GSPA encoded:
#
# - every ROM Xlib import as JAUC <bit address> (the v2.2 table);
# - the image client's direct calls: XOpenDisplay, XCreateSimpleWindow,
# XSelectInput, XCreateGC, XSetForeground, XCreatePixmap, XCreateImage,
# XSetErrorHandler, XMapWindow, XRaiseWindow, XSetInputFocus, XPutImage,
# XCopyArea, XFillRectangle, XSync, XPending, XNextEvent, XLookupString,
# XFlush, XGetInputFocus;
# - the ROM sleep through a function pointer (CALL Rs, encoded 0x0920), the
# Task 3 indirect-call contract;
# - the nine-argument XCreateSimpleWindow order 0,0,0,480,640,20,20,root,
# display (display pushed last);
# - the XSetErrorHandler callback pointer (MOVI _on_error);
# - SETF 16/32 after every call and the RETS/old-STK rules;
# - the 480x80-byte checker .bss and its word-fill loop.
#
# Overridable: M4_COFF M4_ASM DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
WORK=${M4_WORK:-$ROOT/build/m4-xshow}
COFF=${M4_COFF:-$WORK/m4.coff}
ASM=${M4_ASM:-$WORK/m4.asm}
LST=${M4_LST:-$WORK/m4.lst}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M4 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m4/run.sh)"
[ -f "$ASM" ] || fail "linked assembly not found: $ASM"
[ -f "$LST" ] || fail "GSPA listing not found: $LST"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m4-dis34.XXXXXX")
FAIL_LOG=${M4_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m4-dis34-failure.txt}
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
read_u8s() { od -An -v -tu1 -j "$1" -N "$2" "$COFF"; }
u16() { read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }'; }
u32() { read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }'; }
echo "== parse the COFF section table ($COFF)"
nscns=$(u16 2)
opthdr=$(u16 16)
[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections"
[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header"
TEXT_ADDR=; TEXT_BITS=; TEXT_PTR=
DATA_ADDR=; DATA_BITS=; DATA_PTR=
BSS_ADDR=; BSS_BITS=
sh_base=$((20 + opthdr))
i=0
while [ "$i" -lt "$nscns" ]; do
o=$((sh_base + i * 40))
name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000')
case "$name" in
.text) TEXT_ADDR=$(u32 $((o + 8))); TEXT_BITS=$(u32 $((o + 16))); TEXT_PTR=$(u32 $((o + 20))) ;;
.data) DATA_ADDR=$(u32 $((o + 8))); DATA_BITS=$(u32 $((o + 16))); DATA_PTR=$(u32 $((o + 20))) ;;
.bss) BSS_ADDR=$(u32 $((o + 8))); BSS_BITS=$(u32 $((o + 16))) ;;
esac
i=$((i + 1))
done
[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section"
[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty"
[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes"
[ -n "$DATA_ADDR" ] || fail "COFF has no .data section"
[ -n "$BSS_ADDR" ] || fail "COFF has no .bss section"
[ "$BSS_BITS" -ge $((0x4B000)) ] 2>/dev/null \
|| fail ".bss is $BSS_BITS bits, too small for the 640x480 image (0x4B000 bits)"
TEXT_HEX=$(printf '%08X' "$TEXT_ADDR")
ENTRY=$(u32 $((20 + 16)))
ENTRY_HEX=$(printf '%08X' "$ENTRY")
TEXT_BYTES=$((TEXT_BITS / 8))
[ "$TEXT_HEX" = "018A0000" ] || fail ".text is linked at $TEXT_HEX, want 018A0000"
[ "$ENTRY_HEX" = "018A0000" ] || fail "COFF entry point is $ENTRY_HEX, want 018A0000"
BSS_HEX=$(printf '%08X' "$BSS_ADDR")
[ "$BSS_HEX" = "01A00000" ] || fail ".bss is at $BSS_HEX, want 01A00000"
echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes)"
echo " .data at $(printf '%08X' "$DATA_ADDR"): $DATA_BITS bits"
echo " .bss at $BSS_HEX: $BSS_BITS bits"
echo " entry point $ENTRY_HEX"
dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null
[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] || fail "could not extract .text"
echo "== resolve linked symbols"
symptr=$(u32 8)
nsyms=$(u32 12)
[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table"
sym_addr() {
_want=$1
_strtab=$((symptr + nsyms * 18))
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
if [ "$(u32 "$_o")" -eq 0 ]; then
_soff=$(u32 $((_o + 4)))
_n=$(dd if="$COFF" bs=1 skip=$((_strtab + _soff)) count=64 2>/dev/null |
tr '\000' '\n' | head -1)
else
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
fi
if [ "$_n" = "$_want" ]; then
u32 $((_o + 8))
return 0
fi
_aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }')
_i=$((_i + 1 + _aux))
done
return 1
}
get_sym() {
_a=$(sym_addr "$1") || fail "COFF symbol table has no $1"
printf '%s' "$_a"
}
XOPEN=$(get_sym _XOpenDisplay)
XCREATE=$(get_sym _XCreateSimpleWindow)
XSELECT=$(get_sym _XSelectInput)
XGC=$(get_sym _XCreateGC)
XSETFG=$(get_sym _XSetForeground)
XCPM=$(get_sym _XCreatePixmap)
XCI=$(get_sym _XCreateImage)
XERR=$(get_sym _XSetErrorHandler)
XMAP=$(get_sym _XMapWindow)
XRAISE=$(get_sym _XRaiseWindow)
XSETFOCUS=$(get_sym _XSetInputFocus)
XPUT=$(get_sym _XPutImage)
XCOPY=$(get_sym _XCopyArea)
XFILL=$(get_sym _XFillRectangle)
XSYNC=$(get_sym _XSync)
XPEND=$(get_sym _XPending)
XNEXT=$(get_sym _XNextEvent)
XLOOKUP=$(get_sym _XLookupString)
XFLUSH=$(get_sym _XFlush)
XGETFOCUS=$(get_sym _XGetInputFocus)
XSLEEP=$(get_sym _sleep)
XBLIT=$(get_sym _blit_copy)
XSHOW=$(get_sym _xshow)
echo "== disassemble the emitted .text with dis34"
"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \
|| { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; }
[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output"
match_at() {
_file=$1; _addr=$2; _pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
expect_at() {
_label=$1; _addr=$2; _want=$3; _pat=$4
_laddr=$(printf '%08x' "$_addr")
if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then
echo " ok $_label @ 0x$_laddr: $_want"
return 0
fi
{
echo "M4 dis34 round-trip: FAIL"
echo " $_label at bit 0x$_laddr: expected $_want"
awk -v a="$_laddr" '{ low=tolower($0) } low ~ ("^" a ":") { seen=1 } seen { print " " $0; n++ } seen && n>=4 { exit }' "$TMP/dis34.txt"
cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null || true
} >&2
exit 1
}
echo "== check the ROM import stubs"
expect_at _XOpenDisplay "$XOPEN" "JAUC 3F9C2570" 'jauc[[:space:]]+0x3f9c2570'
expect_at _XCreateSimpleWindow "$XCREATE" "JAUC 3FD41B90" 'jauc[[:space:]]+0x3fd41b90'
expect_at _XSelectInput "$XSELECT" "JAUC 3FD97D50" 'jauc[[:space:]]+0x3fd97d50'
expect_at _XCreateGC "$XGC" "JAUC 3F997000" 'jauc[[:space:]]+0x3f997000'
expect_at _XSetForeground "$XSETFG" "JAUC 3FD99A60" 'jauc[[:space:]]+0x3fd99a60'
expect_at _XCreatePixmap "$XCPM" "JAUC 3FD416E0" 'jauc[[:space:]]+0x3fd416e0'
expect_at _XCreateImage "$XCI" "JAUC 3F9A0490" 'jauc[[:space:]]+0x3f9a0490'
expect_at _XSetErrorHandler "$XERR" "JAUC 3FD45700" 'jauc[[:space:]]+0x3fd45700'
expect_at _XMapWindow "$XMAP" "JAUC 3FD64F60" 'jauc[[:space:]]+0x3fd64f60'
expect_at _XRaiseWindow "$XRAISE" "JAUC 3FD645B0" 'jauc[[:space:]]+0x3fd645b0'
expect_at _XSetInputFocus "$XSETFOCUS" "JAUC 3FD536B0" 'jauc[[:space:]]+0x3fd536b0'
expect_at _XPutImage "$XPUT" "JAUC 3F9CEC70" 'jauc[[:space:]]+0x3f9cec70'
expect_at _XCopyArea "$XCOPY" "JAUC 3FD3E220" 'jauc[[:space:]]+0x3fd3e220'
expect_at _XFillRectangle "$XFILL" "JAUC 3F99E8F0" 'jauc[[:space:]]+0x3f99e8f0'
expect_at _XSync "$XSYNC" "JAUC 3FD9F6B0" 'jauc[[:space:]]+0x3fd9f6b0'
expect_at _XPending "$XPEND" "JAUC 3FD67620" 'jauc[[:space:]]+0x3fd67620'
expect_at _XNextEvent "$XNEXT" "JAUC 3F9C1CF0" 'jauc[[:space:]]+0x3f9c1cf0'
expect_at _XLookupString "$XLOOKUP" "JAUC 3F9A9C30" 'jauc[[:space:]]+0x3f9a9c30'
expect_at _XFlush "$XFLUSH" "JAUC 3F9B1030" 'jauc[[:space:]]+0x3f9b1030'
expect_at _XGetInputFocus "$XGETFOCUS" "JAUC 3FD55210" 'jauc[[:space:]]+0x3fd55210'
expect_at _sleep "$XSLEEP" "JAUC 3FB33A20" 'jauc[[:space:]]+0x3fb33a20'
expect_at _blit_copy "$XBLIT" "JAUC 3FB44400" 'jauc[[:space:]]+0x3fb44400'
echo "== check the compiled client calls (linked assembly keeps the names)"
for sym in XOpenDisplay XCreateSimpleWindow XSelectInput XCreateGC XSetForeground \
XCreatePixmap XCreateImage XSetErrorHandler XMapWindow XRaiseWindow \
XSetInputFocus XPutImage XCopyArea XFillRectangle XSync XPending \
XNextEvent XLookupString XFlush XGetInputFocus; do
grep -q "CALLA _${sym}" "$ASM" || fail "linked assembly has no CALLA _$sym"
done
echo " ok direct CALLA imports: 20 Xlib entries"
grep -Eq '^[[:space:]]*MOVI _on_error_[A-Za-z0-9_]+, A[0-9]+' "$ASM" \
|| fail "no MOVI _on_error before XSetErrorHandler"
echo " ok XSetErrorHandler callback pointer: MOVI _on_error…"
grep -Eq '^[[:space:]]*CALL A[0-9]+' "$ASM" \
|| fail "no indirect CALL (Task 3 CALL Rs) in the client"
echo " ok indirect sleep call: CALL A?"
grep -q 'CALLA _blit_copy' "$ASM" \
|| fail "linked assembly has no CALLA _blit_copy (ROM copy entry)"
echo " ok ROM blit_copy entry: CALLA _blit_copy (JAUC 3FB44400)"
# XCreateSimpleWindow argument order: push 0,0,0,480,640,20,20,root,display.
LN=$(grep -n 'CALLA _XCreateSimpleWindow' "$ASM" | head -1 | cut -d: -f1)
[ -n "$LN" ] || fail "linked assembly has no CALLA _XCreateSimpleWindow"
sed -n "$((LN - 24)),$((LN))p" "$ASM" > "$TMP/create.block"
awk '
/^[[:space:]]*MOVI[[:space:]]/ {
line=$0; sub(/.*MOVI[[:space:]]+/, "", line); sub(/[[:space:]]*;.*/, "", line);
split(line, p, /,[[:space:]]*/); gsub(/[[:space:]]/, "", p[1]); gsub(/[[:space:]]/, "", p[2]);
val[p[2]] = p[1]; next
}
/^[[:space:]]*MOVE[[:space:]]+A[0-9]+,[[:space:]]*\*A14\+,/ {
line=$0; sub(/.*MOVE[[:space:]]+/, "", line); sub(/,.*/, "", line);
v = val[line]; if (line == "A1") v = "root"; if (line == "A0") v = "display";
print v
}
' "$TMP/create.block" > "$TMP/create.vals"
printf '%s\n' 0 0 0 480 640 20 20 root display > "$TMP/create.want"
if ! cmp -s "$TMP/create.want" "$TMP/create.vals"; then
{
echo "M4 dis34 round-trip: FAIL: XCreateSimpleWindow argument order"
echo " expected (background,border,border_width,height,width,y,x,root,display):"
sed 's/^/ /' "$TMP/create.want"
echo " got:"
sed 's/^/ /' "$TMP/create.vals"
} >&2
exit 1
fi
echo " ok XCreateSimpleWindow order: 0,0,0,480,640,20,20,root,display (display last)"
# Every call must be followed by the SETF 16/32 pair. The hand-written
# _start trampoline (`calla _xshow` + rets) is not a compiler call and is
# checked separately; the scan starts at _xshow: so lower-case call spellings
# inside the compiler output are still matched.
grep -Eq '^[[:space:]]*calla[[:space:]]+_xshow' "$ASM" \
|| fail "entry.asm does not call _xshow"
awk '
{ low = tolower($0) }
/^_xshow:/ { on = 1 }
low ~ /^[[:space:]]*;/ { next }
!on { next }
expect == 1 {
if ($0 ~ /SETF 16, 0, 0/) { expect = 2 }
else { bad++; expect = 0 }
next
}
expect == 2 {
if ($0 ~ /SETF 32, 0, 1/) { ok++; expect = 0 }
else { bad++; expect = 0 }
next
}
low ~ /^[[:space:]]*(calla|call)[[:space:]]/ { calls++; expect = 1 }
END { printf "%d %d %d\n", calls + 0, ok + 0, bad + 0 }
' "$ASM" > "$TMP/setf.count"
read CALLS SETF_OK SETF_BAD < "$TMP/setf.count"
[ "$CALLS" -gt 0 ] || fail "linked assembly has no calls"
[ "$SETF_BAD" -eq 0 ] || fail "$SETF_BAD calls lack the SETF 16/32 pair"
[ "$SETF_OK" -eq "$CALLS" ] || fail "$CALLS calls but $SETF_OK post-call SETF 16/32 pairs"
echo " ok $CALLS calls each followed by SETF 16,0,0 + SETF 32,0,1"
# Old-STK: on_error saves 3 registers; its epilogue restores A14 from
# *SP((3+1)*32) = *SP(0x80). The prologue pushes A13, A0 and A1.
awk '/^_on_error_/ { on=1 } on { print } on && /rets[[:space:]]+2/ { exit }' "$ASM" > "$TMP/onerr.s"
grep -Eq 'MOVE \*SP\((0x80|128)\), A14, 1' "$TMP/onerr.s" \
|| { sed 's/^/ /' "$TMP/onerr.s" >&2; fail "on_error does not restore A14 from *SP(0x80/128)"; }
grep -Eq '^[[:space:]]*RETS 2' "$TMP/onerr.s" || fail "on_error has no RETS 2"
echo " ok on_error: 3 pushes -> *SP(0x80),A14 and RETS 2"
# Task 3 CALL encoding in the GSPA listing.
grep -Eq '^[[:space:]]*[0-9]+[[:space:]]+[0-9A-F]+[[:space:]]+0920[[:space:]]+CALL[[:space:]]+A[0-9]+' "$LST" \
|| fail "GSPA listing has no 0x0920 CALL A?"
echo " ok GSPA encoded the indirect call as 0x0920"
# dis34 must decode the indirect call as `call aN`.
grep -Eq '^[0-9a-f]+:[[:space:]]+call[[:space:]]+a[0-9]+' "$TMP/dis34.txt" \
|| fail "dis34 did not decode any CALL Rs as 'call aN'"
echo " ok dis34 decodes CALL Rs as 'call aN'"
# The checker data lives in .bss: 480*80 bytes, built as 32-bit words.
grep -Eq '\.bss[[:space:]]+_data_[A-Za-z0-9_]+,307200,32' "$ASM" \
|| fail "linked assembly has no .bss _data…,307200,32 (480*80 bytes)"
grep -Eq '^[[:space:]]*MOVI -1, A[0-9]+' "$ASM" \
|| fail "checker fill loop has no MOVI -1 (all-ones cell)"
grep -Eq '^[[:space:]]*NOT A[0-9]+' "$ASM" \
|| fail "checker fill loop has no NOT (all-zero cell)"
echo " ok checker .bss: 480*80 bytes, MOVI -1 / NOT cell fill"
# The client must actually set the XImage byte/bit order to LSBFirst. The
# layout gate pins data=16 (0x80), byte_order=20 (0xA0) and
# bitmap_bit_order=28 (0xE0); here the linked code stores zero at 0xA0/0xE0.
grep -Eq 'ADDI 160, A[0-9]+' "$ASM" || fail "no XImage.byte_order (bit 0xA0) store"
grep -Eq 'ADDI 224, A[0-9]+' "$ASM" || fail "no XImage.bitmap_bit_order (bit 0xE0) store"
echo " ok XImage byte_order/bitmap_bit_order stores (LSBFirst at 0xA0/0xE0)"
echo "M4 dis34 round-trip: PASS"
+17
View File
@@ -0,0 +1,17 @@
; entry.asm -- M4 entry, linked at bit 018A0000.
;
; Calls the compiled unsigned runtime test. Markers (0x20 bits apart):
; 018A8000 le(5,7) == 1
; 018A8020 ule(7,5) == 0 (unsigned <=)
; 018A8040 sum_le(10) == 55
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _unsigned_test
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+43
View File
@@ -0,0 +1,43 @@
-- fnptr-check.lua -- M4 function-pointer/indirect-call markers.
--
-- M4_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_END loader inspection/exit frame
--
-- Markers (0x20 bits apart):
-- 018A8000 apply(inc, 41) == 42
-- 018A8020 apply(vf, 41) == 42 (volatile local function pointer)
-- 018A8040 apply(g_fn, 43) == 42 (global function pointer = dec)
-- 018A8060 fnptr_test() == 42 (return value, stored by the entry)
dofile(os.getenv("M4_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local m = {}
for i = 0, 3 do
m[i] = prog:read_u32(0x018A8000 + i * 0x20)
end
print(string.format("M4: fnptr=%d fnptr_volatile=%d fnptr_global=%d fnptr_ret=%d",
m[0], m[1], m[2], m[3]))
local want = {42, 42, 42, 42}
local ok = true
for i = 0, 3 do
if m[i] ~= want[i + 1] then
ok = false
end
end
if ok then
print("M4 MAME: PASS")
else
print("M4 MAME: FAIL (want fnptr=42 fnptr_volatile=42 fnptr_global=42 fnptr_ret=42)")
end
end
end, "fnptr-check")
+17
View File
@@ -0,0 +1,17 @@
; fnptr-entry.asm -- M4 function-pointer/indirect-call test entry.
;
; Calls the function-pointer translation unit and stores its return value in
; marker 3. Markers live at 0x018A8000 + n*0x20 (bit addresses).
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _fnptr_test
movi 018A8060h, a0
move a8, *a0, 1
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+29
View File
@@ -0,0 +1,29 @@
/* fnptr.c -- M4 function-pointer/indirect-call runtime test.
*
* Markers live at 0x018A8000 + n*0x20 (bit addresses):
* 0 apply(inc, 41) == 42 direct symbol passed as a pointer
* 1 apply(vf, 41) == 42 volatile local function pointer
* 2 apply(g_fn, 43) == 42 global function pointer holding dec
* 3 fnptr_test() == 42 return value, stored by fnptr-entry.asm
*
* apply/inc/dec are noinline so -O2 cannot inline the helper and devirtualise
* the callback into a direct call; the call inside apply is a real `call i32
* %f` and lowers to CALL Rs.
*/
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
typedef int (*fn)(int);
__attribute__((noinline)) int inc(int x) { return x + 1; }
__attribute__((noinline)) int dec(int x) { return x - 1; }
__attribute__((noinline)) int apply(fn f, int x) { return f(x); }
static fn volatile g_fn = dec;
int fnptr_test(void) {
volatile fn vf = inc;
MARK(0) = (unsigned)apply(inc, 41);
MARK(1) = (unsigned)apply(vf, 41);
MARK(2) = (unsigned)apply(g_fn, 43);
return apply(vf, 41);
}
+102
View File
@@ -0,0 +1,102 @@
/*
* Offset-only equivalent of scripts/m4/x11m4.h for the 1987 TI GSP C
* compiler (GSPCC/GSPCG 2.01). The VT1200 C ABI packs struct members, so
* these structs have the same byte layout as the target's x11m4.h. The
* XImage function table is modelled as six 32-bit void* slots: only its
* size and the offset of f are needed here, and that is layout-equivalent
* to the target's six function pointers.
*
* The 1987 compiler's &-of-member is a bit address, so these functions
* return the ROM's bit displacements directly.
*/
struct display {
char pad[0x90];
void *screens;
};
struct screen {
char pad[0x8];
unsigned int root;
};
struct ximage_funcs {
void *create_image;
void *destroy_image;
void *get_pixel;
void *put_pixel;
void *sub_image;
void *add_pixel;
};
struct ximage {
int width, height;
int xoffset;
int format;
char *data;
int byte_order;
int bitmap_unit;
int bitmap_bit_order;
int bitmap_pad;
int depth;
int bytes_per_line;
int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct ximage_funcs f;
};
struct xerror {
int type;
void *display;
unsigned long resourceid;
unsigned long serial;
unsigned char error_code;
unsigned char request_code;
unsigned char minor_code;
unsigned char abi_pad;
};
struct xkey {
int type;
unsigned long serial;
int send_event;
void *display;
unsigned long window;
unsigned long root;
unsigned long subwindow;
unsigned long time;
int x, y;
int x_root, y_root;
unsigned int state;
unsigned int keycode;
int same_screen;
};
struct xexpose {
int type;
unsigned long serial;
int send_event;
void *display;
unsigned long window;
int x, y;
int width, height;
int count;
};
union xevent {
int type;
struct xkey xkey;
struct xexpose xexpose;
long pad[24];
};
int layout_display_screens() { return (int)&((struct display *)0)->screens; }
int layout_screen_root() { return (int)&((struct screen *)0)->root; }
int layout_image_data() { return (int)&((struct ximage *)0)->data; }
int layout_image_byte_order() { return (int)&((struct ximage *)0)->byte_order; }
int layout_image_bytes_per_line() { return (int)&((struct ximage *)0)->bytes_per_line; }
int layout_image_bits_per_pixel() { return (int)&((struct ximage *)0)->bits_per_pixel; }
int layout_image_f() { return (int)&((struct ximage *)0)->f; }
int layout_error_error_code() { return (int)&((struct xerror *)0)->error_code; }
int layout_event_xkey_keycode() { return (int)&((struct xevent *)0)->xkey.keycode; }
int layout_event_size() { return (int)sizeof(union xevent); }
+114
View File
@@ -0,0 +1,114 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _layout_display_screens
.file "LAYOUT.c"
******************************************************
* FUNCTION DEF : _layout_display_screens
******************************************************
_layout_display_screens:
MOVE A7,-*SP,1
MOVI 1152,A7
MOVE A7,A8
EPI0_1:
MOVE *SP+,A7,1
RETS 0
.globl _layout_screen_root
******************************************************
* FUNCTION DEF : _layout_screen_root
******************************************************
_layout_screen_root:
MOVE A7,-*SP,1
MOVI 64,A7
MOVE A7,A8
EPI0_2:
MOVE *SP+,A7,1
RETS 0
.globl _layout_image_data
******************************************************
* FUNCTION DEF : _layout_image_data
******************************************************
_layout_image_data:
MOVE A7,-*SP,1
MOVI 128,A7
MOVE A7,A8
EPI0_3:
MOVE *SP+,A7,1
RETS 0
.globl _layout_image_byte_order
******************************************************
* FUNCTION DEF : _layout_image_byte_order
******************************************************
_layout_image_byte_order:
MOVE A7,-*SP,1
MOVI 160,A7
MOVE A7,A8
EPI0_4:
MOVE *SP+,A7,1
RETS 0
.globl _layout_image_bytes_per_line
******************************************************
* FUNCTION DEF : _layout_image_bytes_per_line
******************************************************
_layout_image_bytes_per_line:
MOVE A7,-*SP,1
MOVI 320,A7
MOVE A7,A8
EPI0_5:
MOVE *SP+,A7,1
RETS 0
.globl _layout_image_bits_per_pixel
******************************************************
* FUNCTION DEF : _layout_image_bits_per_pixel
******************************************************
_layout_image_bits_per_pixel:
MOVE A7,-*SP,1
MOVI 352,A7
MOVE A7,A8
EPI0_6:
MOVE *SP+,A7,1
RETS 0
.globl _layout_image_f
******************************************************
* FUNCTION DEF : _layout_image_f
******************************************************
_layout_image_f:
MOVE A7,-*SP,1
MOVI 512,A7
MOVE A7,A8
EPI0_7:
MOVE *SP+,A7,1
RETS 0
.globl _layout_error_error_code
******************************************************
* FUNCTION DEF : _layout_error_error_code
******************************************************
_layout_error_error_code:
MOVE A7,-*SP,1
MOVI 128,A7
MOVE A7,A8
EPI0_8:
MOVE *SP+,A7,1
RETS 0
.globl _layout_event_xkey_keycode
******************************************************
* FUNCTION DEF : _layout_event_xkey_keycode
******************************************************
_layout_event_xkey_keycode:
MOVE A7,-*SP,1
MOVI 416,A7
MOVE A7,A8
EPI0_9:
MOVE *SP+,A7,1
RETS 0
.globl _layout_event_size
******************************************************
* FUNCTION DEF : _layout_event_size
******************************************************
_layout_event_size:
MOVI 96,A8
EPI0_10:
RETS 0
.end
+120
View File
@@ -0,0 +1,120 @@
#include "x11m4.h"
/*
* Byte offsets are C offsets; the TMS34010 ROM's indirect displacements are
* bit offsets, so every layout_* function below returns offset * 8. The one
* exception is layout_event_size(), which reports sizeof(XEvent) in bytes
* because the termtris buffer is sized in bytes (vt_u32 evbuf[24]).
*/
_Static_assert(sizeof(void *) == 4, "the VT1200 X11 ABI is 32-bit");
_Static_assert(sizeof(long) == 4, "the VT1200 X11 ABI is 32-bit");
_Static_assert(sizeof(XID) == 4, "XID must be 32-bit");
_Static_assert(sizeof(Time) == 4, "Time must be 32-bit");
_Static_assert(__builtin_offsetof(Display, screens) == 0x90,
"Display.screens must be at byte 0x90 (bit 0x480)");
_Static_assert(__builtin_offsetof(Screen, root) == 0x8,
"Screen.root must be at byte 0x8 (bit 0x40)");
/* 32-bit X11R4 XImage: data 16, byte_order 20, bytes_per_line 40. */
_Static_assert(__builtin_offsetof(XImage, width) == 0, "XImage.width");
_Static_assert(__builtin_offsetof(XImage, height) == 4, "XImage.height");
_Static_assert(__builtin_offsetof(XImage, xoffset) == 8, "XImage.xoffset");
_Static_assert(__builtin_offsetof(XImage, format) == 12, "XImage.format");
_Static_assert(__builtin_offsetof(XImage, data) == 16,
"XImage.data must be at byte 16 (bit 0x80)");
_Static_assert(__builtin_offsetof(XImage, byte_order) == 20,
"XImage.byte_order must be at byte 20 (bit 0xA0)");
_Static_assert(__builtin_offsetof(XImage, bitmap_unit) == 24, "XImage.bitmap_unit");
_Static_assert(__builtin_offsetof(XImage, bitmap_bit_order) == 28,
"XImage.bitmap_bit_order");
_Static_assert(__builtin_offsetof(XImage, bitmap_pad) == 32, "XImage.bitmap_pad");
_Static_assert(__builtin_offsetof(XImage, depth) == 36, "XImage.depth");
_Static_assert(__builtin_offsetof(XImage, bytes_per_line) == 40,
"XImage.bytes_per_line must be at byte 40 (bit 0x140)");
_Static_assert(__builtin_offsetof(XImage, bits_per_pixel) == 44,
"XImage.bits_per_pixel must be at byte 44 (bit 0x160)");
_Static_assert(__builtin_offsetof(XImage, red_mask) == 48, "XImage.red_mask");
_Static_assert(__builtin_offsetof(XImage, green_mask) == 52, "XImage.green_mask");
_Static_assert(__builtin_offsetof(XImage, blue_mask) == 56, "XImage.blue_mask");
_Static_assert(__builtin_offsetof(XImage, obdata) == 60, "XImage.obdata");
_Static_assert(__builtin_offsetof(XImage, f) == 64,
"XImage.f must be at byte 64 (bit 0x200)");
_Static_assert(__builtin_offsetof(struct funcs, destroy_image) == 4,
"XImage.f.destroy_image must be 4 bytes into the inline funcs");
_Static_assert(__builtin_offsetof(XImage, f.destroy_image) == 68,
"XImage.f.destroy_image must be at byte 68 (bit 0x220)");
_Static_assert(sizeof(struct funcs) == 24, "the X11R4 funcs tail is six pointers");
_Static_assert(sizeof(XImage) == 88,
"sizeof(XImage) must cover the ROM's full inline-funcs object");
/* 32-bit X11R4 XErrorEvent: error_code 16, request_code 17, minor_code 18. */
_Static_assert(__builtin_offsetof(XErrorEvent, type) == 0, "XErrorEvent.type");
_Static_assert(__builtin_offsetof(XErrorEvent, display) == 4, "XErrorEvent.display");
_Static_assert(__builtin_offsetof(XErrorEvent, resourceid) == 8,
"XErrorEvent.resourceid");
_Static_assert(__builtin_offsetof(XErrorEvent, serial) == 12, "XErrorEvent.serial");
_Static_assert(__builtin_offsetof(XErrorEvent, error_code) == 16,
"XErrorEvent.error_code must be at byte 16 (bit 0x80)");
_Static_assert(__builtin_offsetof(XErrorEvent, request_code) == 17,
"XErrorEvent.request_code");
_Static_assert(__builtin_offsetof(XErrorEvent, minor_code) == 18,
"XErrorEvent.minor_code");
/* The packed object would be 19 bytes; one X11R4 object-size compatibility
* pad byte keeps the 32-bit object at 20. The client reads only the
* handler-passed fields. */
_Static_assert(sizeof(XErrorEvent) == 20, "sizeof(XErrorEvent) must be 20");
/* XEvent: type 0, xkey.window 16, xkey.keycode 52, xexpose.window 16, x 20. */
_Static_assert(__builtin_offsetof(XEvent, type) == 0, "XEvent.type");
_Static_assert(__builtin_offsetof(XEvent, xkey.window) == 16, "XEvent.xkey.window");
_Static_assert(__builtin_offsetof(XEvent, xkey.keycode) == 52,
"XEvent.xkey.keycode must be at byte 52 (bit 0x1A0)");
_Static_assert(__builtin_offsetof(XEvent, xexpose.window) == 16,
"XEvent.xexpose.window");
_Static_assert(__builtin_offsetof(XEvent, xexpose.x) == 20, "XEvent.xexpose.x");
_Static_assert(__builtin_offsetof(XEvent, xfocus.window) == 16,
"XEvent.xfocus.window");
_Static_assert(__builtin_offsetof(XEvent, xbutton.window) == 16,
"XEvent.xbutton.window");
_Static_assert(__builtin_offsetof(XEvent, xconfigure.window) == 20,
"XEvent.xconfigure.window");
_Static_assert(__builtin_offsetof(XEvent, xclient.window) == 16,
"XEvent.xclient.window");
_Static_assert(sizeof(XEvent) >= 96,
"XEvent must cover the termtris 96-byte vt_u32 evbuf[24]");
_Static_assert(sizeof(XEvent) == 96, "the X11R4 XEvent union is 96 bytes");
int layout_display_screens(void) {
return (int)__builtin_offsetof(Display, screens) * 8;
}
int layout_screen_root(void) {
return (int)__builtin_offsetof(Screen, root) * 8;
}
int layout_image_data(void) {
return (int)__builtin_offsetof(XImage, data) * 8;
}
int layout_image_byte_order(void) {
return (int)__builtin_offsetof(XImage, byte_order) * 8;
}
int layout_image_bytes_per_line(void) {
return (int)__builtin_offsetof(XImage, bytes_per_line) * 8;
}
int layout_image_bits_per_pixel(void) {
return (int)__builtin_offsetof(XImage, bits_per_pixel) * 8;
}
int layout_image_f(void) {
return (int)__builtin_offsetof(XImage, f) * 8;
}
int layout_error_error_code(void) {
return (int)__builtin_offsetof(XErrorEvent, error_code) * 8;
}
int layout_event_xkey_keycode(void) {
return (int)__builtin_offsetof(XEvent, xkey.keycode) * 8;
}
int layout_event_size(void) {
return (int)sizeof(XEvent);
}
/* Exercise the real access path: follows Display.screens and reads Screen.root. */
unsigned int root_probe(Display *d) { return vt_root(d); }
+16
View File
@@ -0,0 +1,16 @@
-a
-o M4.OUT
-m M4.MAP
-e _start
M4.obj
MEMORY
{
prog: origin = 018A0000h, length = 8000h
ram: origin = 01A00000h, length = 80000h
}
SECTIONS
{
.text: {} > prog
.data: {} > prog
.bss: {} > ram
}
+30
View File
@@ -0,0 +1,30 @@
/* m4 loop.c -- an ordinary -O2 loop test. The driver wrapper disables
* IndVarSimplify's exit-value replacement (-replexitval=never): this target has
* no i64, and the closed form of sumto turns into an i33 multiply that llc
* cannot select. The loops themselves must survive and run.
*
* Markers (0x20 bits apart, written by loop_test):
* 018A8100 sumto(10) == 55
* 018A8120 squares(5) == 30
*/
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n)*0x20u))
int sumto(int n) {
int s = 0, i;
for (i = 1; i <= n; i++)
s += i;
return s;
}
int squares(int n) {
int s = 0, i;
for (i = 0; i < n; i++)
s += i * i;
return s;
}
int loop_test(void) {
MARK(8) = (unsigned)sumto(10);
MARK(9) = (unsigned)squares(5);
return 0;
}
+52
View File
@@ -0,0 +1,52 @@
-- m4-check.lua -- load the M4 COFF with dwt/loader/loader.lua and read the
-- marker words written by unsigned.c.
--
-- M4_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_END loader inspection/exit frame
--
-- The marker read runs one frame before loader.lua's inspection/exit frame.
-- Markers (0x20 bits apart):
-- 018A8000 le(5,7) == 1
-- 018A8020 ule(7,5) == 0 (unsigned <=)
-- 018A8040 sum_le(10) == 55
-- 018A8060 ge(7,5) == 1 (signed >=)
-- 018A8080 uge(5,7) == 0 (unsigned >=)
-- 018A80A0 gt(5,7) == 0 (signed >)
-- 018A80C0 ugt(7,5) == 1 (unsigned >)
-- 018A80E0 ne(7,5) == 1
-- 018A8100 lt(7,5) == 0 (signed <)
-- 018A8120 ult(5,7) == 1 (unsigned <)
dofile(os.getenv("M4_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local m = {}
for i = 0, 9 do
m[i] = prog:read_u32(0x018A8000 + i * 0x20)
end
print(string.format(
"M4: le=%d ule=%d sum_le=%d ge=%d uge=%d gt=%d ugt=%d ne=%d lt=%d ult=%d",
m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9]))
local want = {1, 0, 55, 1, 0, 0, 1, 1, 0, 1}
local ok = true
for i = 0, 9 do
if m[i] ~= want[i + 1] then
ok = false
end
end
if ok then
print("M4 MAME: PASS")
else
print("M4 MAME: FAIL (want le=1 ule=0 sum_le=55 ge=1 uge=0 gt=0 ugt=1 ne=1 lt=0 ult=1)")
end
end
end, "m4-check")
+54
View File
@@ -0,0 +1,54 @@
-- mul-check.lua -- M4 multiply/divide and -O2 loop markers.
--
-- M4_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_END loader inspection/exit frame
--
-- Markers (0x20 bits apart):
-- 018A8000 mul(6,7) == 42
-- 018A8020 umul16(0xffff,0xffff) == 0xfffe0001
-- 018A8040 sdiv(100,7) == 14
-- 018A8060 udiv(0xfffffffe,7) == 613566756
-- 018A8080 srem(100,7) == 2
-- 018A80A0 urem(100,7) == 2
-- 018A80C0 sdiv(-100,7) == -14
-- 018A80E0 srem(-100,7) == -2
-- 018A8100 sumto(10) == 55
-- 018A8120 squares(5) == 30
-- 018A8140 mul(0x12345678,0x9abcdef0) == 0x242d2080
-- 018A8160 urem(0xfffffffe,7) == 2
dofile(os.getenv("M4_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local m = {}
for i = 0, 11 do
m[i] = prog:read_u32(0x018A8000 + i * 0x20)
end
print(string.format(
"M4: mul=%d umul16=%d sdiv=%d udiv=%d srem=%d urem=%d sdivneg=%d sremneg=%d sumto=%d squares=%d widemul=%d widerem=%d",
m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9],
m[10], m[11]))
local want = {42, 0xfffe0001, 14, 613566756, 2, 2,
0xfffffff2, 0xfffffffe, 55, 30, 0x242d2080, 2}
local ok = true
for i = 0, 11 do
if m[i] ~= want[i + 1] then
ok = false
end
end
if ok then
print("M4 MAME: PASS")
else
print("M4 MAME: FAIL (want mul=42 umul16=4294836225 sdiv=14 udiv=613566756 srem=2 urem=2 sdivneg=-14 sremneg=-2 sumto=55 squares=30 widemul=606937216 widerem=2)")
end
end
end, "mul-check")
+16
View File
@@ -0,0 +1,16 @@
; mul-entry.asm -- M4 multiply/divide and -O2 loop test entry.
;
; Calls the multiply/divide and loop translation units. Markers written by
; mul.c and loop.c live at 0x018A8000 + n*0x20.
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _mul_test
calla _loop_test
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+49
View File
@@ -0,0 +1,49 @@
/* m4 mul.c -- 32-bit multiply/divide marker test.
*
* Markers (0x20 bits apart, written by mul_test):
* 018A8000 mul(6,7) == 42
* 018A8020 umul16(0xffff,0xffff) == 0xfffe0001 (16x16 MPYU path)
* 018A8040 sdiv(100,7) == 14
* 018A8060 udiv(0xfffffffe,7) == 613566756
* 018A8080 srem(100,7) == 2
* 018A80A0 urem(100,7) == 2
* 018A80C0 sdiv(-100,7) == -14
* 018A80E0 srem(-100,7) == -2
* 018A8140 mulu(0x12345678,0x9ABCDEF0) == 0x242D2080 (wide 32x32 product)
* 018A8160 urem(0xfffffffe,7) == 2 (wide quotient in the remainder)
*/
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n)*0x20u))
int mul(int a, int b) { return a * b; }
/* A full-width unsigned product. The 32x32 low bits are identical for signed
* and unsigned values, so the target selects the same MPYS; using unsigned
* operands keeps a wide marker from being signed-overflow UB. */
unsigned mulu(unsigned a, unsigned b) { return a * b; }
/* A 16x16 unsigned product: both operands are masked to 16 bits, which is the
* form the target selects to MPYU. The product must be computed in unsigned
* arithmetic: 0xffff * 0xffff does not fit in signed int, so `int * int` would
* be signed overflow and the optimizer could narrow or rename the result. */
int umul16(int a, int b) {
return (int)((unsigned)(a & 0xffff) * (unsigned)(b & 0xffff));
}
int sdiv(int a, int b) { return a / b; }
unsigned udiv(unsigned a, unsigned b) { return a / b; }
int srem(int a, int b) { return a % b; }
unsigned urem(unsigned a, unsigned b) { return a % b; }
int mul_test(void) {
MARK(0) = (unsigned)mul(6, 7);
MARK(1) = (unsigned)umul16(0xffff, 0xffff);
MARK(2) = (unsigned)sdiv(100, 7);
MARK(3) = udiv(0xfffffffeu, 7u);
MARK(4) = (unsigned)srem(100, 7);
MARK(5) = urem(100u, 7u);
MARK(6) = (unsigned)sdiv(-100, 7);
MARK(7) = (unsigned)srem(-100, 7);
MARK(10) = mulu(0x12345678u, 0x9ABCDEF0u);
MARK(11) = urem(0xfffffffeu, 7u);
return 0;
}
+1
View File
@@ -0,0 +1 @@
char *a(void) { return "same"; }
+1
View File
@@ -0,0 +1 @@
char *b(void) { return "same"; }
+16
View File
@@ -0,0 +1,16 @@
; private-entry.asm -- M4 two-TU private-symbol link test entry.
;
; Calls both translation units so the linker keeps their functions and the
; string literals that those functions reference.
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _a
calla _b
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+190
View File
@@ -0,0 +1,190 @@
#!/bin/sh
# M4 end-to-end runner: compile translation units, assemble/link with the 1987
# GSPA/GSPLNK toolchain under DOSBox, load the COFF in MAME, and evaluate the
# marker Lua script.
#
# scripts/m4/run.sh
#
# The default is unsigned.c at -O0, linked with entry.asm and checked by
# m4-check.lua. Other M4 tasks can reuse this runner:
#
# M4_SOURCES="foo bar" C basenames under scripts/m4 (default: unsigned)
# M4_ENTRY=/path/entry.asm assembly entry file (default: scripts/m4/entry.asm)
# M4_CHECK_LUA=/path.lua MAME marker script (default: scripts/m4/m4-check.lua)
# M4_CLANG_OPT=-O2 tms-clang optimization level (default: -O0)
# M4_LLC_OPT=-O0 llc optimization level (default: -O0)
# M4_ROM_SYMBOLS=1 append the per-ROM import stubs from symbols
# M4_EXTRA_ASM="/a.asm /b.asm" extra assembly to concatenate
# M4_SKIP_MAME=1 stop after a clean GSPA/GSPLNK link
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF
# SDK_IMG M4_WORK M4_COFF M4_SDK M4_SYMS LOADER_AT LOADER_END
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
DIR=$ROOT/scripts/m4
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${M4_WORK:-$ROOT/build/m4}
COFF=${M4_COFF:-$WORK/m4.coff}
SDK=${M4_SDK:-$ROOT/build/m4-sdk}
ASM=$WORK/asm
LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua
LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
LOADER_AT=${LOADER_AT:-2500}
LOADER_END=${LOADER_END:-2700}
SYMS=${M4_SYMS:-$ROOT/symbols/vt1200-v22.syms}
SOURCES=${M4_SOURCES:-unsigned}
ENTRY=${M4_ENTRY:-$DIR/entry.asm}
CHECK_LUA=${M4_CHECK_LUA:-$DIR/m4-check.lua}
CLANG_OPT=${M4_CLANG_OPT:--O0}
LLC_OPT=${M4_LLC_OPT:--O0}
fail() {
echo "m4: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$ENTRY" ] || fail "missing entry: $ENTRY"
mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg"
mkdir -p "$(dirname "$COFF")"
echo "== compile and select ($CLANG_OPT / llc $LLC_OPT)"
export TMS_CLANG TMS_OPT TMS_BITADDR
for name in $SOURCES; do
src=$DIR/$name.c
[ -f "$src" ] || fail "missing source: $src"
"$ROOT/scripts/tms-clang" "$CLANG_OPT" -o "$ASM/$name.ll" "$src"
"$TMS_LLC" -march=tms34010 "$LLC_OPT" -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $name.c"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
echo "== extract the 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
# .bss is placed at 0x01A00000 (link.cmd). The 640x480 1-bit image is
# 38400 bytes; with the 4 KiB C stack and 1 KiB XG block it cannot fit the old
# 32 KiB region at 0x018A9000. 0x01A00000 is the region the termtris loader
# uses for its 125984-bit .bss (termtris.map 0x01A00000..0x01A1EC20), while the
# X server's request buffer lives in low RAM (0x00988FD0..0x0098CFD0).
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
# GSPA 2.00 has one flat symbol table per assembly, and llc names basic blocks
# LBB0_1, LBB1_2, ... restarting in every translation unit. Give each TU its
# own LBB prefix, as the M3 runner does, so a multi-TU client (xshow + xgfix)
# cannot collide. The per-TU .s files keep their original labels.
tag=0
for name in $SOURCES; do
tag=$((tag + 1))
sed "s/LBB\([0-9][0-9]*_[0-9][0-9]*\)/LBB$tag\1/g" \
"$ASM/$name.s" > "$ASM/$name.unique.s"
done
cat "$ENTRY" > "$SDK/M4.ASM"
for name in $SOURCES; do
cat "$ASM/$name.unique.s" >> "$SDK/M4.ASM"
done
if [ -n "${M4_EXTRA_ASM:-}" ]; then
for extra in $M4_EXTRA_ASM; do
[ -f "$extra" ] || fail "missing extra assembly: $extra"
cat "$extra" >> "$SDK/M4.ASM"
done
fi
if [ -n "${M4_ROM_SYMBOLS:-}" ]; then
"$ROOT/scripts/m2/gen-rom-imports.sh" "$SYMS" "$WORK/rom-imports.asm"
cat "$WORK/rom-imports.asm" >> "$SDK/M4.ASM"
fi
# The linker command file must keep CRLF line endings.
sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M4.CMD"
rm -f "$SDK/M4.OBJ" "$SDK/M4.OUT" "$SDK/M4.MAP" "$SDK/M4.LST"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa M4 M4.obj M4.lst -l" \
-c "gsplnk M4.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; }
[ -f "$SDK/M4.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/M4.LST" > "$WORK/m4.lst"
grep -q 'No Errors' "$WORK/m4.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/m4.lst" >&2 || true; fail "GSPA reported errors"; }
grep -q 'No Warnings' "$WORK/m4.lst" \
|| { grep -i -B2 -A2 'warning' "$WORK/m4.lst" >&2 || true; fail "GSPA reported warnings"; }
[ -f "$SDK/M4.OUT" ] || fail "GSPLNK produced no M4.OUT"
sed 's/\r$//' "$SDK/M4.MAP" > "$WORK/m4.map"
cp "$SDK/M4.OUT" "$COFF"
cp "$SDK/M4.ASM" "$WORK/m4.asm"
ENTRY_ADDR=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m4.map")
[ "$ENTRY_ADDR" = "018A0000" ] || fail "linker entry _start is '$ENTRY_ADDR', want 018A0000"
grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m4.map" \
|| fail "linker map has no _start entry point"
echo " linked .text at 018A0000, M4.OUT -> $COFF"
if [ -n "${M4_SKIP_MAME:-}" ]; then
echo "M4 LINK: PASS"
exit 0
fi
echo "== run the COFF in MAME"
[ -x "$MAME" ] || fail "MAME binary not built: $MAME"
[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA"
[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN"
[ -f "$CHECK_LUA" ] || fail "marker Lua missing: $CHECK_LUA"
rm -rf "$WORK/run/nvram"
mkdir -p "$WORK/run/nvram"
VT1200_MONITOR=VR319 \
VT1200_SKIP_LEVEL1=1 \
LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \
LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \
LOADER_OUT="$WORK/evidence" \
M4_LOADER_LUA="$LOADER_LUA" \
timeout "${MAME_TIMEOUT:-900}" "$MAME" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$CHECK_LUA" -autoboot_delay 0 \
> "$WORK/mame.log" 2>&1 \
|| { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; }
MARKERS=$(grep -E '^M4: ' "$WORK/mame.log" | tail -1 || true)
[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M4 markers"; }
echo " $MARKERS"
grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \
|| { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; }
grep -q 'placed \.text .* 0 differing' "$WORK/mame.log" \
|| fail "COFF loader could not verify the placed .text bytes"
grep -q '^M4 MAME: PASS' "$WORK/mame.log" \
|| { tail -30 "$WORK/mame.log" >&2; fail "M4 marker check failed"; }
echo "M4 MAME: PASS"
+37
View File
@@ -0,0 +1,37 @@
/*
* switch.c -- a dense char switch, used by tests/m4/termtris.sh to pin the
* driver's late-GEP policy.
*
* The optimizer's switch-to-lookup-table transform creates a new i8 GEP after
* tms-bitaddr --scale; --normalize cannot tell that index apart from an
* already-scaled bit offset, so the table would be indexed one bit per slot.
* tms-clang passes -switch-to-lookup=false; this source makes the hazard
* reachable at -O2 (termtris' init_game and runesc switches are the real
* cases).
*/
int sw_tile(int c)
{
switch(c) {
case '#': return 2;
case '.': return 1;
case '{': return 11;
case '}': return 12;
case '[': return 13;
case ']': return 14;
case '>': return 15;
case '<': return 16;
case '-': return 17;
case '(': return 18;
case ')': return 19;
default: return 0;
}
}
int sw_sum(const char *s)
{
int r = 0;
while(*s) {
r += sw_tile(*s++);
}
return r;
}
+226
View File
@@ -0,0 +1,226 @@
#!/bin/sh
# build.sh -- build the pinned termtris COFF with this compiler (tms-clang +
# llc), then assemble/link with the TI GSPA/GSPLNK toolchain.
#
# scripts/m4/termtris/build.sh
#
# Inputs: the test-only pinned checkout vendor/vt1200-termtris (branch
# `termtris`, commit be5b323...). Output: build/m4-termtris/termtris.coff and
# termtris.map, plus the generated TERMTRIS.ASM/LST for the dis34-style checks.
#
# Overridable: VT1200_TERMTRIS TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC
# TERMTRIS_WORK TERMTRIS_SDK DOSBOX DOSBOX_CONF SDK_IMG
# TERMTRIS_CC_OPT TERMTRIS_LLC_OPT
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
DIR=$ROOT/scripts/m4/termtris
VENDOR=${VT1200_TERMTRIS:-$ROOT/vendor/vt1200-termtris}
SRC=$VENDOR/apps/termtris/src
ENTRY_SRC=$SRC/vt1200
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
MAME=${MAME:-$ROOT/build/mame-src/vt1200}
DOSBOX=${DOSBOX:-dosbox}
DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf}
SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img}
WORK=${TERMTRIS_WORK:-$ROOT/build/m4-termtris}
SDK=${TERMTRIS_SDK:-$ROOT/build/m4-termtris-sdk}
ASM=$WORK/asm
CC_OPT=${TERMTRIS_CC_OPT:--O2}
LLC_OPT=${TERMTRIS_LLC_OPT:--O0}
SOURCES="main game scoredb x11 compat xgfix"
fail() {
echo "termtris build: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -d "$SRC" ] || fail "missing pinned termtris source: $SRC"
# The pinned commit matters: a newer branch may have moved the DIAG markers or
# changed the game loop. Fail loudly rather than silently test another tree.
if git -C "$VENDOR" rev-parse --git-dir >/dev/null 2>&1; then
rev=$(git -C "$VENDOR" rev-parse HEAD)
[ "$rev" = "be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf" ] \
|| fail "vendor tree is $rev, want the pinned be5b323f5d2cc4f7a7d25e7143c67bd9828ccbdf"
fi
mkdir -p "$ASM" "$WORK"
echo "== compile the pinned sources with this compiler ($CC_OPT) and llc ($LLC_OPT)"
export TMS_CLANG TMS_OPT TMS_BITADDR
# -I order: quoted game headers, then the vt1200 shims (stdio.h/inttypes.h),
# then the small local shims for stdlib/string/time. tms-clang passes
# -nostdinc, so every angle-bracket header must resolve here.
INCLUDES="-I $SRC -I $SRC/vt1200 -I $DIR/include"
for name in $SOURCES; do
case $name in
game) src=$SRC/game.c ;;
main|x11|compat|xgfix) src=$SRC/vt1200/$name.c ;;
scoredb) src=$SRC/vt1200/scoredb.c ;;
*) fail "unknown source $name" ;;
esac
[ -f "$src" ] || fail "missing source: $src"
# shellcheck disable=SC2086
"$ROOT/scripts/tms-clang" "$CC_OPT" $INCLUDES -o "$ASM/$name.ll" "$src" \
|| fail "tms-clang failed for $src"
"$TMS_LLC" -march=tms34010 "$LLC_OPT" -filetype=asm \
"$ASM/$name.ll" -o "$ASM/$name.s" \
|| fail "llc failed for $src"
echo " $name.s: $(grep -c . "$ASM/$name.s") lines"
done
echo "== extract the TI 1987 SDK"
if [ ! -f "$SDK/GSPA.EXE" ]; then
command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required"
[ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG"
mkdir -p "$SDK"
mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/"
mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/"
fi
[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK"
[ -f "$SDK/RTS.LIB" ] || fail "RTS.LIB not extracted into $SDK"
# GSPA has one flat symbol table per assembly, and llc restarts LBB0_1 in every
# translation unit. Give each TU its own LBB prefix (the M4 run.sh pattern).
tag=0
for name in $SOURCES; do
tag=$((tag + 1))
sed "s/LBB\([0-9][0-9]*_[0-9][0-9]*\)/LBB$tag\1/g" \
"$ASM/$name.s" > "$ASM/$name.unique.s"
done
# The body (entry, code, libc) without the .cinit member: the linker takes the
# empty CINIT.asm first, and the caller relinks with CINITZ.asm when the map
# says .cinit is empty.
# The vendored entry/cinit files end with `.end`, which stops GSPA at that
# point when the pieces are concatenated into one assembly (the vendored
# build assembles each file separately). Drop the terminator; GSPA's own
# `.end` is appended by the cinit member.
strip_end() {
sed '/^[[:space:]]*\.end[[:space:]]*$/d' "$1"
}
: > "$WORK/code.asm"
strip_end "$ENTRY_SRC/entry.asm" >> "$WORK/code.asm"
for name in $SOURCES; do
cat "$ASM/$name.unique.s" >> "$WORK/code.asm"
done
# GSPA needs an explicit `.globl` declaration for a symbol the linker will
# resolve from another object (rts.lib); llc prints the call/reference but no
# declaration. Collect the CALLA/MOVI references and declare the ones this
# assembly does not define itself.
sed -n \
-e 's/.*CALLA[[:space:]]*\(_[A-Za-z0-9_][A-Za-z0-9_]*\).*/\1/p' \
-e 's/.*MOVI[[:space:]]*\(_[A-Za-z0-9_][A-Za-z0-9_]*\).*/\1/p' \
"$WORK/code.asm" | sort -u > "$WORK/refs.txt"
sed -n 's/^\(_[A-Za-z0-9_][A-Za-z0-9_]*\):.*/\1/p' "$WORK/code.asm" \
| sort -u > "$WORK/defs.txt"
: > "$WORK/externs.asm"
while read -r sym; do
[ -n "$sym" ] || continue
grep -qxF "$sym" "$WORK/defs.txt" || printf '\t.globl\t%s\n' "$sym" >> "$WORK/externs.asm"
done < "$WORK/refs.txt"
: > "$WORK/body.asm"
cat "$WORK/externs.asm" "$WORK/code.asm" > "$WORK/body.asm"
strip_end "$ENTRY_SRC/cinit.asm" > "$WORK/cinit.asm"
strip_end "$ENTRY_SRC/cinitz.asm" > "$WORK/cinitz.asm"
# The link command file must keep CRLF line endings for the DOS tool.
write_cmd() {
{
printf '%s\n' '-a'
printf '%s\n' '-o TERMTRIS.OUT'
printf '%s\n' '-m TERMTRIS.MAP'
printf '%s\n' '-e _start'
printf '%s\n' 'TERMTRIS.obj'
printf '%s\n' '-l rts.lib'
printf '%s\n' 'MEMORY'
printf '%s\n' '{'
# The vintage r5.01 build's text+data is 10.6+0.6 KiB and fits the
# 20000h-bit program region. This compiler's -O2/llc -O0 output is
# about twice as large (see the report), so the program region is
# widened to 40000h bits (32 KiB). It ends at 018E0000, below the
# loader's COFF buffer at 01900000 and the .bss region at 01A00000.
printf '%s\n' ' prog: origin = 018A0000h, length = 40000h'
printf '%s\n' ' ram: origin = 01A00000h, length = 40000h'
printf '%s\n' '}'
printf '%s\n' 'SECTIONS'
printf '%s\n' '{'
printf '%s\n' ' .text: {} > prog'
printf '%s\n' ' .data: {} > prog'
printf '%s\n' ' .cinit: {} > prog'
printf '%s\n' ' .bss: {} > ram'
printf '%s\n' '}'
} > "$WORK/termtris.cmd"
sed 's/$/\r/' "$WORK/termtris.cmd" > "$SDK/TERMTRIS.CMD"
}
assemble_link() {
cinit=$1
cat "$WORK/body.asm" "$WORK/$cinit" > "$SDK/TERMTRIS.ASM"
rm -f "$SDK/TERMTRIS.OBJ" "$SDK/TERMTRIS.LST" \
"$SDK/TERMTRIS.OUT" "$SDK/TERMTRIS.MAP"
write_cmd
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \
-noprimaryconf -nolocalconf \
-c "mount c $SDK" -c "c:" \
-c "gspa TERMTRIS TERMTRIS.obj TERMTRIS.lst -l" \
-c "gsplnk TERMTRIS.cmd" \
-c "exit" -exit > "$WORK/dosbox.log" 2>&1 \
|| { tail -30 "$WORK/dosbox.log" >&2; fail "DOSBox failed ($cinit)"; }
[ -f "$SDK/TERMTRIS.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)"
sed 's/\r$//' "$SDK/TERMTRIS.LST" > "$WORK/termtris.lst"
grep -q 'No Errors' "$WORK/termtris.lst" \
|| { grep -i -B2 -A2 'error' "$WORK/termtris.lst" >&2 || true; \
fail "GSPA reported an error"; }
# GSPA's symbol table caps names at 31 characters. llc's path-hashed
# static/switch-table names can exceed that; the truncation is consistent
# between definition and reference and the map below is checked for
# duplicates. Any other warning is fatal.
nwarn=$(grep -c 'WARNING' "$WORK/termtris.lst" || true)
ntrunc=$(grep -c 'WARNING - SYMBOL TRUNCATED' "$WORK/termtris.lst" || true)
if [ "$nwarn" -ne "$ntrunc" ]; then
grep -i -B2 -A2 'warning' "$WORK/termtris.lst" >&2 || true
fail "GSPA reported a non-truncation warning"
fi
[ "$nwarn" -eq 0 ] || echo " GSPA: $nwarn long symbol name(s) truncated to 31 chars"
[ -f "$SDK/TERMTRIS.OUT" ] || fail "GSPLNK produced no TERMTRIS.OUT"
sed 's/\r$//' "$SDK/TERMTRIS.MAP" > "$WORK/termtris.map"
}
echo "== assemble and link with GSPA/GSPLNK under DOSBox"
assemble_link cinit.asm
CSIZE=$(awk '$1 == ".cinit" {print $4; exit}' "$WORK/termtris.map")
if [ "$CSIZE" = "00000000" ]; then
# An empty .cinit makes the linker point `cinit` at .text; the entry's
# walker would read an instruction as a record count. Relink with the
# one-word terminator, exactly as the vendored build.sh does.
echo " .cinit is empty; relinking with the CINITZ terminator"
assemble_link cinitz.asm
fi
ENTRY_ADDR=$(awk '$2 == "_start" { print $1; exit }' "$WORK/termtris.map")
[ -n "$ENTRY_ADDR" ] || fail "link map has no _start"
[ "$ENTRY_ADDR" = "018a0000" ] || [ "$ENTRY_ADDR" = "018A0000" ] \
|| fail "linker _start is '$ENTRY_ADDR', want 018a0000"
cp "$SDK/TERMTRIS.OUT" "$WORK/termtris.coff"
cp "$SDK/TERMTRIS.ASM" "$WORK/termtris.asm"
echo " linked .text at $ENTRY_ADDR, TERMTRIS.OUT -> $WORK/termtris.coff"
awk '/^\.text|^\.data|^\.cinit|^\.bss/ || /_dbg_loops|_dbg_events|_dbg_last|_dbg_focus|_dbg_rev|_quit|_tick_interval/ {print}' \
"$WORK/termtris.map" | sed -n '1,40p'
+12
View File
@@ -0,0 +1,12 @@
/*
* stdlib.h -- the subset of <stdlib.h> termtris' game.c uses, for the
* freestanding tms-clang build. The implementations come from the TI 1987
* RTS.LIB (rand/srand); there is no heap allocator in the game.
*/
#ifndef VT_TERMTRIS_STDLIB_H_
#define VT_TERMTRIS_STDLIB_H_
int rand(void);
void srand(unsigned seed);
#endif
+15
View File
@@ -0,0 +1,15 @@
/*
* string.h -- the subset of <string.h> termtris uses, for the freestanding
* tms-clang build. The implementations come from the TI 1987 RTS.LIB.
*/
#ifndef VT_TERMTRIS_STRING_H_
#define VT_TERMTRIS_STRING_H_
void *memcpy(void *dst, const void *src, unsigned n);
void *memset(void *dst, int c, unsigned n);
unsigned strlen(const char *s);
char *strcpy(char *dst, const char *src);
char *strncpy(char *dst, const char *src, unsigned n);
int strcmp(const char *a, const char *b);
#endif
+8
View File
@@ -0,0 +1,8 @@
/*
* time.h -- game.c includes <time.h>, but the VT1200 port replaces time(0)
* with the ROM's free-running tick (vt_rand_seed); no time function is used.
*/
#ifndef VT_TERMTRIS_TIME_H_
#define VT_TERMTRIS_TIME_H_
#endif
+393
View File
@@ -0,0 +1,393 @@
-- play_keys.lua -- M4 termtris acceptance harness (adapted from the pinned
-- apps/termtris/tests/play_keys.lua).
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local PLAY_MODE = os.getenv("PLAY_MODE") or "move"
print("PLAYKEYS: mode="..PLAY_MODE)
local DL = tonumber(os.getenv("DIAG_LOOPS") or "0")
local DE = tonumber(os.getenv("DIAG_EVENTS") or "0")
local DT = tonumber(os.getenv("DIAG_LAST") or "0")
local DF = tonumber(os.getenv("DIAG_FOCUS") or "0")
local DR = tonumber(os.getenv("DIAG_REV") or "0")
local DQ = tonumber(os.getenv("DIAG_QUIT") or "0")
local DTICK = tonumber(os.getenv("DIAG_TICK") or "0")
local DW = tonumber(os.getenv("DIAG_WIN") or "0")
local ports = machine.ioport.ports
local key_a = ports[":lk401:K26"].fields["A"]
local key_d = ports[":lk401:K28"].fields["D"]
local key_s = ports[":lk401:K27"].fields["S"]
local key_w = ports[":lk401:K14"].fields["W"]
local key_0 = ports[":lk401:K10"].fields["0 )"]
local key_p = ports[":lk401:K22"].fields["P"]
local key_r = ports[":lk401:K16"].fields["R"]
local key_h = ports[":lk401:K31"].fields["H"]
local key_q = ports[":lk401:K13"].fields["Q"]
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if DL ~= 0 then
print(string.format("DIAG: loops=%d events=%d last=%d focus=%08X rev=%d quit=%d window=%08X", r32(DL), r32(DE), r32(DT), DF ~= 0 and r32(DF) or 0, DR ~= 0 and r32(DR) or 0, DQ ~= 0 and r32(DQ) or 0, DW ~= 0 and r32(DW) or 0))
if DTICK ~= 0 then
print(string.format("DIAG: tick=%d", r32(DTICK)))
end
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAYKEYS: move") end
if frames == 3000 then tabb:set_value(1); print("PLAYKEYS: click down") end
if frames == 3020 then tabb:set_value(0); print("PLAYKEYS: click up") end
if PLAY_MODE == "move" then
if frames == 3100 then key_a:set_value(1) end
if frames == 3110 then key_a:set_value(0) end
if frames == 3120 then key_d:set_value(1) end
if frames == 3130 then key_d:set_value(0) end
if frames == 3140 then key_s:set_value(1) end
if frames == 3150 then key_s:set_value(0) end
if frames == 3160 then key_w:set_value(1) end
if frames == 3170 then key_w:set_value(0) end
if frames == 3180 then key_0:set_value(1) end
if frames == 3190 then key_0:set_value(0) end
elseif PLAY_MODE == "panels" then
if frames == 3100 then key_h:set_value(1) end
if frames == 3110 then key_h:set_value(0) end
if frames == 3120 then key_r:set_value(1) end
if frames == 3130 then key_r:set_value(0) end
elseif PLAY_MODE == "quit" then
if frames == 3095 then print("PLAYKEYS: quit arm") end
if frames == 3100 then print("PLAYKEYS: Q down"); key_q:set_value(1) end
if frames == 3110 then print("PLAYKEYS: Q up"); key_q:set_value(0) end
end
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+104
View File
@@ -0,0 +1,104 @@
#!/bin/sh
# play_keys.sh -- run the M4-built termtris COFF in MAME through the proven
# play_keys paths (board, move, panels, quit), capture a snapshot per mode,
# and print the DIAG line. Threshold enforcement lives in
# scripts/tests/m4/termtris.sh.
#
# scripts/m4/termtris/play_keys.sh
#
# Env: MODES (default "board move panels quit"), TERMTRIS_NOBUILD=1,
# MAME, TERMTRIS_WORK, LOADER_AT/LOADER_END.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
DIR=$ROOT/scripts/m4/termtris
VENDOR=${VT1200_TERMTRIS:-$ROOT/vendor/vt1200-termtris}
WORK=${TERMTRIS_WORK:-$ROOT/build/m4-termtris}
M=${MAME:-$ROOT/build/mame-src/vt1200}
COFF=$WORK/termtris.coff
MAP=$WORK/termtris.map
EV=$WORK/evidence
LOADER=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin
fail() {
echo "termtris play_keys: $*" >&2
exit 1
}
if [ "${TERMTRIS_NOBUILD:-}" != 1 ]; then
"$DIR/build.sh" > "$WORK/build.log" 2>&1 \
|| { tail -40 "$WORK/build.log" >&2; fail "build failed"; }
fi
[ -f "$COFF" ] || fail "missing COFF: $COFF"
[ -f "$MAP" ] || fail "missing link map: $MAP"
[ -x "$M" ] || fail "MAME binary not built: $M"
[ -f "$LOADER" ] || fail "COFF loader missing: $LOADER"
mkdir -p "$EV" "$WORK/run/nvram" "$WORK/run/cfg"
# The link map prints two address/name pairs per line, so the symbol address
# is the field before the name (the vendored play_keys.sh does the same).
addr() {
awk -v sym="$1" '{for (i = 2; i <= NF; i++) if ($i == sym) print $(i - 1)}' \
"$MAP" | head -1
}
DIAG_LOOPS=$(addr _dbg_loops)
DIAG_EVENTS=$(addr _dbg_events)
DIAG_LAST=$(addr _dbg_last)
DIAG_FOCUS=$(addr _dbg_focus)
DIAG_REV=$(addr _dbg_rev)
DIAG_QUIT=$(addr _quit)
DIAG_TICK=$(addr _tick_interval)
DIAG_WIN=$(addr _vt_win)
for sym in "$DIAG_LOOPS" "$DIAG_EVENTS" "$DIAG_LAST" "$DIAG_FOCUS" \
"$DIAG_REV" "$DIAG_QUIT" "$DIAG_TICK" "$DIAG_WIN"; do
[ -n "$sym" ] || fail "a termtris DIAG symbol is missing from $MAP"
done
echo "== termtris markers: loops=$DIAG_LOOPS events=$DIAG_EVENTS last=$DIAG_LAST"
echo " focus=$DIAG_FOCUS rev=$DIAG_REV quit=$DIAG_QUIT tick=$DIAG_TICK win=$DIAG_WIN"
for mode in ${MODES:-board move panels quit}; do
LOG=$WORK/play_$mode.log
rm -f "$EV/loader_frame3200.png" "$EV/loader_frame"*.png
echo "== play_keys: $mode"
VT1200_SKIP_LEVEL1=1 \
VT1200_MONITOR=VR319 \
LOADER_SNAP=1 \
PLAY_MODE=$mode \
DIAG_LOOPS="0x$DIAG_LOOPS" \
DIAG_EVENTS="0x$DIAG_EVENTS" \
DIAG_LAST="0x$DIAG_LAST" \
DIAG_FOCUS="0x$DIAG_FOCUS" \
DIAG_REV="0x$DIAG_REV" \
DIAG_QUIT="0x$DIAG_QUIT" \
DIAG_TICK="0x$DIAG_TICK" \
DIAG_WIN="0x$DIAG_WIN" \
LOADER_AT="${LOADER_AT:-2500}" \
LOADER_END="${LOADER_END:-3200}" \
LOADER_BIN="$LOADER" \
LOADER_COFF="$COFF" \
LOADER_OUT="$EV" \
timeout "${MAME_TIMEOUT:-900}" "$M" vt1200 \
-rompath "$ROOT/vendor/vt1200/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-nvram_directory "$WORK/run/nvram" \
-cfg_directory "$WORK/run/cfg" \
-debug -debugger none \
-autoboot_script "$DIR/play_keys.lua" -autoboot_delay 0 \
> "$LOG" 2>&1 \
|| { tail -40 "$LOG" >&2; fail "MAME run failed for $mode"; }
grep -E 'PLAYKEYS:|LOADER: result|LOADER: snapshot' "$LOG" | tail -5 \
|| true
DIAG=$(grep -a 'DIAG: loops' "$LOG" | tail -1 || true)
[ -n "$DIAG" ] || { tail -40 "$LOG" >&2; \
fail "no DIAG line for $mode (see $LOG)"; }
echo " $DIAG"
echo "$DIAG" > "$WORK/diag_$mode.txt"
[ -s "$EV/loader_frame${LOADER_END:-3200}.png" ] \
|| fail "no $mode snapshot in $EV"
cp "$EV/loader_frame${LOADER_END:-3200}.png" \
"$EV/termtris_${mode}_${LOADER_END:-3200}.png"
echo " $EV/termtris_${mode}_${LOADER_END:-3200}.png"
done
+24
View File
@@ -0,0 +1,24 @@
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n)*0x20u))
int le(int a, int b) { return a <= b; }
unsigned ule(unsigned a, unsigned b) { return a <= b; }
int sum_le(int n) { int s = 0, i; for (i = 0; i <= n; i++) s += i; return s; }
int ge(int a, int b) { return a >= b; }
unsigned uge(unsigned a, unsigned b) { return a >= b; }
int gt(int a, int b) { return a > b; }
unsigned ugt(unsigned a, unsigned b) { return a > b; }
int ne(int a, int b) { return a != b; }
int lt(int a, int b) { return a < b; }
unsigned ult(unsigned a, unsigned b) { return a < b; }
int unsigned_test(void) {
MARK(0) = (unsigned)le(5, 7);
MARK(1) = (unsigned)ule(7u, 5u);
MARK(2) = (unsigned)sum_le(10);
MARK(3) = (unsigned)ge(7, 5);
MARK(4) = (unsigned)uge(5u, 7u);
MARK(5) = (unsigned)gt(5, 7);
MARK(6) = (unsigned)ugt(7u, 5u);
MARK(7) = (unsigned)ne(7, 5);
MARK(8) = (unsigned)lt(7, 5);
MARK(9) = (unsigned)ult(5u, 7u);
return 0;
}
+278
View File
@@ -0,0 +1,278 @@
#!/bin/sh
# M4 ROM symbol validation.
#
# Every entry in the v2.2 table must:
# - sit in the ROM bit range 3F200000h..3FFFFFFFh,
# - be 16-bit aligned (TMS34010 instructions are 16-bit words), and
# - start with a plausible function prologue (MMTM/MOVE/SETF/CMP/ADDI).
#
# The correction-sensitive entries are also checked semantically with dis34:
# - XMapWindow must be 0x3FD64F60 and build MapWindow request opcode 8;
# - XRaiseWindow must be 0x3FD645B0 and build ConfigureWindow opcode 12 with
# CWStackMode=Above (value mask 0x40, value 0);
# - XPutImage builds request opcode 0x48 and XGetImage opcode 0x49;
# - XLoadFont builds OpenFont opcode 45 (0x2d) and XCopyArea CopyArea
# opcode 62 (0x3e) — both from the ROM request builders, not termtris;
# - XCreatePixmap builds CreatePixmap opcode 0x35 and XSetInputFocus builds
# SetInputFocus opcode 0x2a;
# - XCreateImage allocates 0x58 bytes (sizeof(XImage) == 88);
# - XSetErrorHandler / XSetIOErrorHandler install the ROM defaults into the
# per-process XG block at +0x60 / +0x40.
#
# This is a separate gate from scripts/m3/validate-syms.sh: M3 is a four-entry
# regression, M4 walks the whole table. Both use the same range/alignment/
# prologue rules and the same corrected XMapWindow address.
#
# scripts/m4/validate-syms.sh [symbols/vt1200-v22.syms]
#
# Overridable: M4_SYMS DIS34 ROM
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
SYMS=${1:-${M4_SYMS:-$ROOT/symbols/vt1200-v22.syms}}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
ROM=${ROM:-$ROOT/vendor/vt1200/vt1200_v22.bin}
ROM_LO=$((0x3F200000))
ROM_HI=$((0x3FFFFFFF))
MAPWINDOW=$((0x3FD64F60))
RAISEWINDOW=$((0x3FD645B0))
fail() {
echo "m4 validate-syms: FAIL: $*" >&2
exit 1
}
[ -f "$SYMS" ] || fail "symbol file not found: $SYMS"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
[ -f "$ROM" ] || fail "ROM image not found: $ROM"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m4-syms.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
sym_addr() { # sym_addr NAME
awk -v n="$1" '$1 == n { print $2; exit }' "$SYMS"
}
dis_at() { # dis_at BIT_ADDR BYTE_COUNT OUT
_daddr=$1
_dcount=$2
_dout=$3
_doff=$(( (_daddr - ROM_LO) / 8 ))
dd if="$ROM" of="$_dout.bin" bs=1 skip="$_doff" count="$_dcount" 2>/dev/null
# dis34 reports "unexpected end of input" when the last instruction is
# truncated; the leading instructions are still valid, so ignore its exit
# status and parse the output.
"$DIS34" -b "$(printf '%08X' "$_daddr")" -o "$_dout.bin" \
> "$_dout" 2>/dev/null || true
}
first_insn() { # first_insn DIS_TEXT
awk 'NF && $1 !~ /^L[0-9A-Fa-f]+:$/ { print tolower($1); exit }' "$1"
}
require_match() { # require_match DIS_TEXT ERE WHAT
grep -Eiq "$2" "$1" || {
sed 's/^/ /' "$1" >&2
fail "$3"
}
}
check_prologue() { # check_prologue NAME ADDR
_name=$1
_addr=$2
case "$_addr" in
0x* | 0X*) ;;
*) fail "$_name has malformed address '$_addr' (want 0x...)" ;;
esac
_dec=$((_addr))
[ "$_dec" -ge "$ROM_LO" ] && [ "$_dec" -le "$ROM_HI" ] \
|| fail "$_name $_addr is outside the ROM bit range 3F200000h..3FFFFFFFh"
[ $((_dec % 16)) -eq 0 ] \
|| fail "$_name $_addr is not 16-bit aligned"
dis_at "$_dec" 64 "$TMP/insn"
_first=$(first_insn "$TMP/insn")
[ -n "$_first" ] || fail "$_name $_addr disassembled to nothing"
case " move mmtm setf cmp addi " in
*" $_first "*) ;;
*) fail "$_name $_addr starts with '$_first', not a plausible function prologue" ;;
esac
echo " ok $_name $_addr: $_first ..."
}
# XMapWindow must be the MapWindow entry, not the ConfigureWindow entry.
check_mapwindow() {
_addr=$(sym_addr XMapWindow)
[ -n "$_addr" ] || fail "table has no XMapWindow entry"
_dec=$((_addr))
if [ "$_dec" -eq "$RAISEWINDOW" ]; then
fail "XMapWindow is $_addr, which is XRaiseWindow (ConfigureWindow opcode 12), not the MapWindow entry (0x3FD64F60): wrong address"
fi
[ "$_dec" -eq "$MAPWINDOW" ] \
|| fail "XMapWindow is $_addr, expected 0x3FD64F60 (MapWindow request opcode 8); wrong address"
dis_at "$_dec" 512 "$TMP/mapwindow"
require_match "$TMP/mapwindow" 'movk[[:space:]]+8,[[:space:]]+a11' \
"XMapWindow does not build MapWindow request opcode 8"
require_match "$TMP/mapwindow" 'movb[[:space:]]+a11,[[:space:]]+\*a10' \
"XMapWindow does not write the request opcode to the display buffer"
echo " ok XMapWindow builds MapWindow opcode 8 (XRaiseWindow is 0x3FD645B0)"
}
# XRaiseWindow must be the ConfigureWindow/Above entry.
check_raisewindow() {
_addr=$(sym_addr XRaiseWindow)
[ -n "$_addr" ] || fail "table has no XRaiseWindow entry"
_dec=$((_addr))
if [ "$_dec" -eq "$MAPWINDOW" ]; then
fail "XRaiseWindow is $_addr, which is XMapWindow (MapWindow opcode 8), not ConfigureWindow: wrong address"
fi
[ "$_dec" -eq "$RAISEWINDOW" ] \
|| fail "XRaiseWindow is $_addr, expected 0x3FD645B0 (ConfigureWindow opcode 12)"
dis_at "$_dec" 512 "$TMP/raisewindow"
require_match "$TMP/raisewindow" 'movk[[:space:]]+12,[[:space:]]+a[0-9]+' \
"XRaiseWindow does not build ConfigureWindow request opcode 12"
require_match "$TMP/raisewindow" 'movi[[:space:]]+0x40,[[:space:]]+a[0-9]+' \
"XRaiseWindow does not set the CWStackMode value mask 0x40"
require_match "$TMP/raisewindow" '\*a10\(0x40\)' \
"XRaiseWindow does not write the CWStackMode value-mask field"
require_match "$TMP/raisewindow" '\*a10\(0x60\)' \
"XRaiseWindow does not write the CWStackMode=Above value"
echo " ok XRaiseWindow builds ConfigureWindow opcode 12, CWStackMode=Above"
}
# XPutImage/XGetImage request opcodes, per the ROM strings they reference.
check_image_requests() {
_addr=$(sym_addr XPutImage)
[ -n "$_addr" ] || fail "table has no XPutImage entry"
dis_at "$((_addr))" 544 "$TMP/putimage"
require_match "$TMP/putimage" 'movi[[:space:]]+0x48,[[:space:]]+a[0-9]+' \
"XPutImage does not build PutImage request opcode 0x48"
require_match "$TMP/putimage" 'movb[[:space:]]+a[0-9]+,[[:space:]]+\*a[0-9]+' \
"XPutImage does not write its request opcode to the buffer"
echo " ok XPutImage builds PutImage opcode 0x48"
_addr=$(sym_addr XGetImage)
[ -n "$_addr" ] || fail "table has no XGetImage entry"
dis_at "$((_addr))" 512 "$TMP/getimage"
require_match "$TMP/getimage" 'movi[[:space:]]+0x49,[[:space:]]+a[0-9]+' \
"XGetImage does not build GetImage request opcode 0x49"
require_match "$TMP/getimage" 'movb[[:space:]]+a[0-9]+,[[:space:]]+\*a[0-9]+' \
"XGetImage does not write its request opcode to the buffer"
echo " ok XGetImage builds GetImage opcode 0x49"
}
# XLoadFont/XCopyArea are inherited from the M4 plan, not the termtris xlib.h.
# Their ROM bodies build OpenFont opcode 45 (0x2d) and CopyArea opcode 62
# (0x3e); both write the opcode to the request buffer.
check_font_copy() {
_addr=$(sym_addr XLoadFont)
[ -n "$_addr" ] || fail "table has no XLoadFont entry"
dis_at "$((_addr))" 512 "$TMP/loadfont"
require_match "$TMP/loadfont" 'movi[[:space:]]+0x2d,[[:space:]]+a[0-9]+' \
"XLoadFont does not build OpenFont request opcode 45 (0x2d)"
require_match "$TMP/loadfont" 'movb[[:space:]]+a[0-9]+,[[:space:]]+\*a[0-9]+' \
"XLoadFont does not write its request opcode to the buffer"
echo " ok XLoadFont builds OpenFont opcode 45 (not termtris xlib.h)"
_addr=$(sym_addr XCopyArea)
[ -n "$_addr" ] || fail "table has no XCopyArea entry"
dis_at "$((_addr))" 512 "$TMP/copyarea"
require_match "$TMP/copyarea" 'movi[[:space:]]+0x3e,[[:space:]]+a[0-9]+' \
"XCopyArea does not build CopyArea request opcode 62 (0x3e)"
require_match "$TMP/copyarea" 'movb[[:space:]]+a[0-9]+,[[:space:]]+\*a[0-9]+' \
"XCopyArea does not write its request opcode to the buffer"
echo " ok XCopyArea builds CopyArea opcode 62 (not termtris xlib.h)"
}
# XCreatePixmap/XSetInputFocus are used by the image client. Their request
# builders are CreatePixmap opcode 0x35 and SetInputFocus opcode 0x2a.
check_pixmap_focus() {
_addr=$(sym_addr XCreatePixmap)
[ -n "$_addr" ] || fail "table has no XCreatePixmap entry"
dis_at "$((_addr))" 512 "$TMP/createpixmap"
require_match "$TMP/createpixmap" 'movi[[:space:]]+0x35,[[:space:]]+a[0-9]+' \
"XCreatePixmap does not build CreatePixmap request opcode 0x35"
require_match "$TMP/createpixmap" 'movb[[:space:]]+a[0-9]+,[[:space:]]+\*a[0-9]+' \
"XCreatePixmap does not write its request opcode to the buffer"
echo " ok XCreatePixmap builds CreatePixmap opcode 0x35"
_addr=$(sym_addr XSetInputFocus)
[ -n "$_addr" ] || fail "table has no XSetInputFocus entry"
dis_at "$((_addr))" 256 "$TMP/setinputfocus"
require_match "$TMP/setinputfocus" 'movi[[:space:]]+0x2a,[[:space:]]+a[0-9]+' \
"XSetInputFocus does not build SetInputFocus request opcode 0x2a"
require_match "$TMP/setinputfocus" 'calla[[:space:]]+0x3fd526e0' \
"XSetInputFocus does not call the SetInputFocus request builder"
require_match "$TMP/setinputfocus" '\*a13\(0xffa0\)' \
"XSetInputFocus does not read the revert_to argument"
require_match "$TMP/setinputfocus" '\*a13\(0xffc0\)' \
"XSetInputFocus does not read the focus argument"
require_match "$TMP/setinputfocus" '\*a13\(0xffe0\)' \
"XSetInputFocus does not read the display argument"
echo " ok XSetInputFocus builds SetInputFocus opcode 0x2a"
}
check_xcreateimage() {
_addr=$(sym_addr XCreateImage)
[ -n "$_addr" ] || fail "table has no XCreateImage entry"
dis_at "$((_addr))" 256 "$TMP/createimage"
require_match "$TMP/createimage" 'movi[[:space:]]+0x58,[[:space:]]+a[0-9]+' \
"XCreateImage does not allocate 0x58 bytes (sizeof(XImage) == 88)"
require_match "$TMP/createimage" 'calla[[:space:]]+0x3f703d90' \
"XCreateImage does not call the ROM zeroing allocator (0x3F703D90)"
echo " ok XCreateImage allocates an 88-byte XImage"
}
# The error-handler setters write the per-process XG block: IO at +0x40,
# protocol error at +0x60. The defaults are the ROM's _XDefaultIOError and
# _XDefaultError.
check_error_handlers() {
_addr=$(sym_addr XSetErrorHandler)
[ -n "$_addr" ] || fail "table has no XSetErrorHandler entry"
dis_at "$((_addr))" 448 "$TMP/seterror"
require_match "$TMP/seterror" 'movi[[:space:]]+0x3f9bd9f0,[[:space:]]+a[0-9]+' \
"XSetErrorHandler does not install _XDefaultError (0x3F9BD9F0) when the handler is null"
require_match "$TMP/seterror" '\*a11\(0x60\)' \
"XSetErrorHandler does not store into the XG block at +0x60"
echo " ok XSetErrorHandler stores XG+0x60, default _XDefaultError"
_addr=$(sym_addr XSetIOErrorHandler)
[ -n "$_addr" ] || fail "table has no XSetIOErrorHandler entry"
dis_at "$((_addr))" 448 "$TMP/setioerror"
require_match "$TMP/setioerror" 'movi[[:space:]]+0x3f9bcd40,[[:space:]]+a[0-9]+' \
"XSetIOErrorHandler does not install _XDefaultIOError (0x3F9BCD40) when the handler is null"
require_match "$TMP/setioerror" '\*a11\(0x40\)' \
"XSetIOErrorHandler does not store into the XG block at +0x40"
echo " ok XSetIOErrorHandler stores XG+0x40, default _XDefaultIOError"
}
# Walk every symbol line. Keep this generic so a newly added entry cannot
# silently bypass the range/alignment/prologue gate.
echo "== validate every v2.2 table entry"
count=0
while read -r name addr rest; do
case "$name" in
'' | '#'*) continue ;;
esac
[ -n "$addr" ] || fail "malformed symbol line for '$name' (no address)"
check_prologue "$name" "$addr"
count=$((count + 1))
done < "$SYMS"
[ "$count" -gt 0 ] || fail "no entries found in $SYMS"
echo "== validate the correction-sensitive entries"
check_mapwindow
check_raisewindow
check_image_requests
check_font_copy
check_pixmap_focus
check_xcreateimage
check_error_handlers
echo "m4 validate-syms: PASS ($count v2.2 entries in ROM, aligned, prologues valid; XMapWindow/XRaiseWindow pinned)"
+31
View File
@@ -0,0 +1,31 @@
-- varargs-check.lua -- load the varargs ABI probe with the M4 COFF loader and
-- check the three sums. The callee walks the contiguous program stack below
-- its last fixed argument; these values prove the variadic slots are real.
--
-- M4_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_END loader inspection/exit frame
dofile(os.getenv("M4_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local frames = 0
local checked = false
emu.register_frame_done(function()
frames = frames + 1
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local m = {}
for i = 0, 2 do
m[i] = prog:read_u32(0x018A8000 + i * 0x20)
end
print(string.format("M4: varargs sum1=%d sum2=%d sum3=%d",
m[0], m[1], m[2]))
if m[0] == 15 and m[1] == 150 and m[2] == 1500 then
print("M4 MAME: PASS")
else
print("M4 MAME: FAIL (want 15, 150, 1500)")
end
end
end, "varargs-check")
+13
View File
@@ -0,0 +1,13 @@
; varargs-entry.asm -- M4 entry for the varargs ABI probe.
.text
.globl _start
.globl _varargs_test
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _varargs_test
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+35
View File
@@ -0,0 +1,35 @@
/*
* varargs.c -- the contiguous-stack varargs ABI probe.
*
* M4 supports only the TI ABI's stack layout: a variadic callee takes the
* address of its last fixed argument and walks down one 32-bit slot per extra
* argument. There is no va_start/va_arg/va_list. The termtris compat.c
* sprintf uses exactly this pattern; this probe proves the values arrive.
*
* Markers (0x20 bits apart, written by the test):
* 018A8000 sumvar(1,2,3,4,5) == 15
* 018A8020 sumvar(10,20,30,40,50) == 150
* 018A8040 sumvar(100,200,300,400,500) == 1500
*/
#define MARK(n) (*(volatile int *)(0x018A8000UL + (n) * 0x20UL))
static int sumvar(int a, int b, ...)
{
int *p = &b;
int s = a + b;
p--;
s += *p; /* first variadic argument */
p--;
s += *p; /* second */
p--;
s += *p; /* third */
return s;
}
void varargs_test(void)
{
MARK(0) = sumvar(1, 2, 3, 4, 5);
MARK(1) = sumvar(10, 20, 30, 40, 50);
MARK(2) = sumvar(100, 200, 300, 400, 500);
}
+314
View File
@@ -0,0 +1,314 @@
/*
* Minimal X11R4-compatible declarations for the M4 VT1200 clients.
*
* The VT1200 X server is X11R4 on a 32-bit TMS34010, so this header models
* that ABI: XID/Window/KeySym/Time are 32-bit unsigned long, GC is a 32-bit
* pointer, and XImage carries the inline six-entry struct funcs tail.
*
* Display/Screen deliberately do NOT include the host X11R4 header. The
* VT1200 ROM's Display is the M3-verified layout: Display.screens at byte
* 0x90 (bit 0x480) and Screen.root at byte 0x8 (bit 0x40). Do not include
* scripts/m3/vt1200_xlib.h here; its XOpenDisplay/XCreateSimpleWindow
* prototypes conflict with the wider M4 ones below.
*
* Offset assertions live in scripts/m4/layout.c; the TI golden in
* scripts/m4/layout-ti.s carries the same bit displacements.
*/
#ifndef X11M4_H
#define X11M4_H
typedef unsigned long XID;
typedef XID Window;
typedef XID Drawable;
typedef XID Pixmap;
typedef XID Atom;
typedef XID Colormap;
typedef XID Cursor;
typedef XID Font;
typedef XID GContext;
typedef XID KeySym;
typedef unsigned long Mask;
typedef unsigned long Time;
typedef int Bool;
typedef unsigned char KeyCode;
typedef char *XPointer;
typedef struct _XGC *GC;
typedef struct _Visual Visual; /* opaque: clients only pass a Visual * */
typedef struct _XImage XImage;
/*
* The VT1200 ROM layout, reused from scripts/m3/vt1200_xlib.h. TMS34010
* indirect displacements are bit offsets, so screens is at byte 0x90 and
* root at byte 0x8.
*/
typedef struct _Display {
unsigned char pad[0x90];
void *screens;
} Display;
typedef struct _Screen {
unsigned char pad[0x8];
unsigned int root;
} Screen;
/* X11R4 xlib.h: the image function table is inline in XImage, not a pointer. */
struct funcs {
XImage *(*create_image)(Display *, Visual *, unsigned int, int, int, char *,
unsigned int, unsigned int, int, int);
int (*destroy_image)(XImage *);
unsigned long (*get_pixel)(XImage *, int, int);
int (*put_pixel)(XImage *, int, int, unsigned long);
XImage *(*sub_image)(XImage *, int, int, unsigned int, unsigned int);
int (*add_pixel)(XImage *, long);
};
typedef struct _XImage {
int width, height;
int xoffset;
int format;
char *data;
int byte_order;
int bitmap_unit;
int bitmap_bit_order;
int bitmap_pad;
int depth;
int bytes_per_line;
int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct funcs f;
} XImage;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
} XAnyEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
Window root;
Window subwindow;
Time time;
int x, y;
int x_root, y_root;
unsigned int state;
unsigned int keycode;
Bool same_screen;
} XKeyEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
Window root;
Window subwindow;
Time time;
int x, y;
int x_root, y_root;
unsigned int state;
unsigned int button;
Bool same_screen;
} XButtonEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
int mode;
int detail;
} XFocusChangeEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
int x, y;
int width, height;
int count;
} XExposeEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window event;
Window window;
int x, y;
int width, height;
int border_width;
Window above;
Bool override_redirect;
} XConfigureEvent;
typedef struct {
int type;
unsigned long serial;
Bool send_event;
Display *display;
Window window;
Atom message_type;
int format;
union {
char b[20];
short s[10];
long l[5];
} data;
} XClientMessageEvent;
typedef struct {
int type;
Display *display;
XID resourceid;
unsigned long serial;
unsigned char error_code;
unsigned char request_code;
unsigned char minor_code;
/*
* X11R4's 32-bit XErrorEvent is 20 bytes. The TMS34010 C ABI packs
* structs (and TI C does too), so the three trailing chars would make the
* packed object 19 bytes. This byte is an X11R4 object-size compatibility
* pad; the client reads only the handler-passed fields.
*/
unsigned char abi_pad;
} XErrorEvent;
/*
* X11R4's XEvent is 96 bytes: the largest member is pad[24] (the protocol
* xEvent is 24 longs). The termtris event buffer is vt_u32 evbuf[24].
*/
typedef union _XEvent {
int type;
XAnyEvent xany;
XKeyEvent xkey;
XButtonEvent xbutton;
XFocusChangeEvent xfocus;
XExposeEvent xexpose;
XConfigureEvent xconfigure;
XClientMessageEvent xclient;
XErrorEvent xerror;
long pad[24];
} XEvent;
/* Event codes (X11R4). */
#define KeyPress 2
#define KeyRelease 3
#define ButtonPress 4
#define ButtonRelease 5
#define FocusIn 9
#define FocusOut 10
#define Expose 12
/* Event masks (X11R4). */
#define NoEventMask 0L
#define KeyPressMask (1L << 0)
#define KeyReleaseMask (1L << 1)
#define ButtonPressMask (1L << 2)
#define ButtonReleaseMask (1L << 3)
#define ExposureMask (1L << 15)
#define StructureNotifyMask (1L << 17)
#define FocusChangeMask (1L << 21)
/* Image byte/bit order and format. */
#define LSBFirst 0
#define MSBFirst 1
#define XYBitmap 0
#define XYPixmap 1
#define ZPixmap 2
#define True 1
#define False 0
#define None 0L
typedef int (*XErrorHandler)(Display *, XErrorEvent *);
typedef int (*XIOErrorHandler)(Display *);
/*
* Real Xlib entry points. The VT1200 ROM exports these names; Task 5
* validates their bit addresses in symbols/vt1200-v22.syms.
*
* XCreateGC's XGCValues* and XLookupString's XComposeStatus* are modelled as
* void*: the layout is not needed by the M4 clients and the 32-bit pointer
* ABI is unchanged.
*/
extern Display *XOpenDisplay(const char *name);
extern Window XCreateSimpleWindow(Display *display, Window parent, int x, int y,
unsigned int width, unsigned int height,
unsigned int border_width,
unsigned long border,
unsigned long background);
extern int XMapWindow(Display *display, Window window);
/* CreatePixmap opcode 0x35; depth 1 for the image client's 1-bit pixmap. */
extern Pixmap XCreatePixmap(Display *display, Drawable drawable,
unsigned int width, unsigned int height,
unsigned int depth);
/* ConfigureWindow(CWStackMode=Above): map-and-raise for a client that must be
* above the Terminal Manager (XMapWindow alone does not raise). */
extern int XRaiseWindow(Display *display, Window window);
extern int XSync(Display *display, Bool discard);
extern int XFlush(Display *display);
extern int XSelectInput(Display *display, Window window, long event_mask);
extern GC XCreateGC(Display *display, Drawable drawable,
unsigned long valuemask, void *values);
extern int XSetForeground(Display *display, GC gc, unsigned long foreground);
extern int XFillRectangle(Display *display, Drawable drawable, GC gc, int x,
int y, unsigned int width, unsigned int height);
extern int XDrawString(Display *display, Drawable drawable, GC gc, int x, int y,
const char *string, int length);
extern int XNextEvent(Display *display, XEvent *event);
extern int XLookupString(XKeyEvent *event, char *buffer, int bytes,
KeySym *keysym, void *compose_status);
extern int XPending(Display *display);
/* The ROM's ms sleep (symbols/vt1200-v22.syms: 0x3FB33A20); yields so the
* X server and the other firmware processes keep running. */
extern int sleep(int ms);
extern int XGetInputFocus(Display *display, Window *focus, int *revert_to);
/*
* The ROM's XSetInputFocus reads three arguments (display, focus, revert_to)
* and supplies CurrentTime inside the request builder; the disassembly at
* 0x3FD536B0 reads 0xffe0/0xffc0/0xffa0 and builds SetInputFocus opcode 0x2a.
*/
#define RevertToParent 2
extern int XSetInputFocus(Display *display, Window focus, int revert_to);
/* ROM blitter memcpy used by _XPutImage; the ROM order is (src, dst, bytes). */
extern int blit_copy(void *src, void *dst, int nbytes);
extern Font XLoadFont(Display *display, const char *name);
extern int XCopyArea(Display *display, Drawable src, Drawable dest, GC gc,
int src_x, int src_y, unsigned int width,
unsigned int height, int dest_x, int dest_y);
extern XImage *XCreateImage(Display *display, Visual *visual,
unsigned int depth, int format, int offset,
char *data, unsigned int width,
unsigned int height, int bitmap_pad,
int bytes_per_line);
extern int XPutImage(Display *display, Drawable drawable, GC gc, XImage *image,
int src_x, int src_y, int dest_x, int dest_y,
unsigned int width, unsigned int height);
extern int XDestroyImage(XImage *image);
extern XErrorHandler XSetErrorHandler(XErrorHandler handler);
extern XIOErrorHandler XSetIOErrorHandler(XIOErrorHandler handler);
/* Install the per-process XG block before XOpenDisplay (scripts/m4/xgfix.c). */
extern int vt_install_xg(void);
/* Follow Display.screens (bit 0x480) and read Screen.root (bit 0x40). */
static inline __attribute__((always_inline)) unsigned int
vt_root(Display *display) {
return ((Screen *)((Display *)display)->screens)->root;
}
#endif /* X11M4_H */
+21
View File
@@ -0,0 +1,21 @@
/*
* Per-process Xlib context (XG) installer for the VT1200 loader child.
*
* The ROM Xlib keeps its per-process state in the user word at
* *(current_process + 0x100). A process created by the loader inherits a
* zeroed cell there, so the ROM's _XEnq sees XG == NULL and XNextEvent reads
* garbage or never reports events. Points that word at a zeroed 1 KiB block
* before XOpenDisplay.
*
* This is a RAM write only; the ROM image is untouched. Must be called before
* XOpenDisplay, once per process.
*/
int vt_install_xg(void) {
unsigned int proc = *(volatile unsigned int *)0x001B3900u;
if (proc < 0x100u || proc > 0x01FFFFFFu) return 0;
unsigned int pp = *(volatile unsigned int *)(proc + 0x100u);
if (pp < 0x100u || pp > 0x01FFFFFFu) return 0;
static unsigned int xg[256]; /* 1 KiB zeroed .bss */
*(volatile unsigned int *)pp = (unsigned int)xg;
return 1;
}
+153
View File
@@ -0,0 +1,153 @@
-- xshow-check.lua -- load the M4 image/event COFF with dwt/loader/loader.lua,
-- inject a real LK401 `Q` key, and prove the mapped 1-bit XImage in VRAM.
--
-- M4_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua
-- LOADER_AT injection frame (default 2500)
-- LOADER_END loader inspection/exit frame (default 3400)
--
-- Markers (0x20 bits apart):
-- 018A8000 display nonzero
-- 018A8020 window nonzero
-- 018A8040 image nonzero
-- 018A8060 quit 1 after the `q` KeyPress
-- 018A8080 events count of XNextEvent calls (> 0)
-- 018A80A0 keycode ev.xkey.keycode (193 for the LK401 Q)
-- 018A80C0 keysym XLookupString keysym (XK_q = 0x71)
-- 018A80E0 xerrors 0
-- 018A8100 focus XGetInputFocus after XSetInputFocus
-- 018A8120 last last X event type (2 = KeyPress)
--
-- The client window is XCreateSimpleWindow(..., 20, 20, 640, 480, ...) with
-- no border. The VT1200 server copies a 1-bit bitmap correctly only when the
-- destination x is 32-bit aligned, so the client draws the aligned pixmap at
-- window x = 12 (screen x = 32) and fills the 12-pixel prefix with the
-- preceding cell's colour. The checker cells are 32 pixels wide with a
-- 12-pixel phase: image data x = sx - 20 - 12. The window-relative points
-- (30,30) and (50,30) are adjacent cells (0,0) and (1,0), as are (30,62) and
-- (50,62). The samples at data (368,280), (468,380) and (568,430) prove the
-- banded PutImage assembled the whole 640x480 image, not just its first band.
-- The prefix samples at screen (25,30)/(25,62) prove the preceding-cell fill.
-- The pixel is read from the exact VRAM bit address (VR319: 2048-bit rows,
-- LSB-first), not from a whole unaligned word that would cross a cell.
--
-- The focus path is the tablet click: XSetInputFocus alone leaves the DWT WM
-- at PointerRoot (verified: focus=0x1 and no key), so the gate moves the
-- absolute tablet (0..2199) to (700,500), the termtris-proven in-window point,
-- and clicks. That point maps inside termtris' (80..560,80..392) rectangle,
-- which is contained in this client's (20..660,20..500) window. The focus
-- marker must become the client's own window; the ButtonRelease handler
-- samples it after the click.
dofile(os.getenv("M4_LOADER_LUA"))
local ENDF = tonumber(os.getenv("LOADER_END") or "3400")
local ports = manager.machine.ioport.ports
local key_q = ports[":lk401:K13"].fields["Q"]
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local MOVE_AT = 2900
local CLICK_DOWN = 2920
local CLICK_UP = 2930
local Q_DOWN = 3000
local Q_UP = 3010
local VRAM_BASE = 0x04000000
local VRAM_PITCH = 2048 -- VR319: 2048 bits per row
local WIN_X, WIN_Y = 20, 20
local IMG_PHASE = 12 -- pixmap is copied to window x=12 (screen x=32)
local frames = 0
local checked = false
local function read_bit(prog, sx, sy)
local bit = VRAM_BASE + sy * VRAM_PITCH + sx
return prog:read_u16(bit) % 2, bit
end
emu.register_frame_done(function()
frames = frames + 1
if frames == MOVE_AT then
tabx:set_value(700)
taby:set_value(500)
print(string.format("M4: inject frame %d tablet (700,500)", frames))
elseif frames == CLICK_DOWN then
tabb:set_value(1)
print(string.format("M4: inject frame %d button down", frames))
elseif frames == CLICK_UP then
tabb:set_value(0)
print(string.format("M4: inject frame %d button up", frames))
elseif frames == Q_DOWN then
key_q:set_value(1)
print(string.format("M4: inject frame %d Q down", frames))
elseif frames == Q_UP then
key_q:set_value(0)
print(string.format("M4: inject frame %d Q up", frames))
end
if frames == ENDF - 1 and not checked then
checked = true
local prog = manager.machine.devices[':maincpu'].spaces['program']
local m = {}
for i = 0, 9 do
m[i] = prog:read_u32(0x018A8000 + i * 0x20)
end
print(string.format(
"M4: display=%u window=%u image=%u quit=%d events=%d keycode=%d keysym=0x%X xerrors=%d focus=0x%X last=%d",
m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9]))
-- screen -> image (window-relative) -> 32-pixel cell
local function sample(sx, sy)
local b, bit = read_bit(prog, sx, sy)
local rx, ry = sx - WIN_X - IMG_PHASE, sy - WIN_Y
local cx, cy = math.floor(rx / 32), math.floor(ry / 32)
print(string.format(
"M4: pixel screen(%d,%d) image(%d,%d) cell(%d,%d) bit=0x%08X value=%d %s",
sx, sy, rx, ry, cx, cy, bit, b,
b == 1 and "white" or "black"))
return b
end
-- The task's exact pair plus the orthogonal pair, at cell centres and
-- at points well inside the cells (away from a 32-pixel boundary).
local b00 = sample(50, 50) -- data (18,30) cell (0,0) -> black
local b10 = sample(70, 50) -- data (38,30) cell (1,0) -> white
local b01 = sample(50, 82) -- data (18,62) cell (0,1) -> white
local b11 = sample(70, 82) -- data (38,62) cell (1,1) -> black
local c00 = sample(36, 36) -- data (4,16) cell (0,0) -> black
local c10 = sample(68, 36) -- data (36,16) cell (1,0) -> white
local c01 = sample(36, 68) -- data (4,48) cell (0,1) -> white
local c11 = sample(68, 68) -- data (36,48) cell (1,1) -> black
-- later bands, so a repeated first-band bug cannot pass
local d1 = sample(400, 300) -- data (368,280) cell (11,8) -> white
local d2 = sample(500, 400) -- data (468,380) cell (14,11) -> white
local d3 = sample(600, 450) -- data (568,430) cell (17,13) -> black
-- the 12-pixel prefix is the preceding cell: white in band 0, black
-- in band 1, so it cannot be a constant left stripe.
local p0 = sample(25, 30) -- data (-7,10) cell (-1,0) -> white
local p1 = sample(25, 62) -- data (-7,42) cell (-1,1) -> black
local keysym_ok = (m[6] == 0x71)
local image_ok = b00 == 0 and b10 == 1 and b01 == 1 and b11 == 0
and c00 == 0 and c10 == 1 and c01 == 1 and c11 == 0
and d1 == 1 and d2 == 1 and d3 == 0
and p0 == 1 and p1 == 0
local ok = m[0] ~= 0 and m[1] ~= 0 and m[2] ~= 0
and m[3] == 1 and m[4] > 0 and m[7] == 0
and m[5] == 193 and keysym_ok
and m[8] == m[1] and m[8] ~= 0 and m[9] == 2
and image_ok
if ok then
print("M4 MAME: PASS")
else
print(string.format(
"M4 MAME: FAIL (markers display/window/image=%u/%u/%u quit=%d events=%d keycode=%d keysym=0x%X xerrors=%d focus=0x%X last=%d; image %s)",
m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9],
image_ok and "checker-ok" or "checker-mismatch"))
end
end
end, "xshow-check")
+22
View File
@@ -0,0 +1,22 @@
; xshow-entry.asm -- M4 image/event client entry, linked at bit 018A0000.
;
; Calls the compiled xshow() client. Markers (0x20 bits apart):
; 018A8000 display nonzero
; 018A8020 window nonzero
; 018A8040 image nonzero
; 018A8060 quit 1 after the real `q` KeyPress
; 018A8080 events count of XNextEvent calls
; 018A80A0 keycode ev.xkey.keycode
; 018A80C0 keysym XLookupString keysym
; 018A80E0 xerrors 0
.text
.globl _start
_start:
setf 16, 0, 0
setf 32, 0, 1
movi _cstack, a14
move a14, a13
calla _xshow
rets
.bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes
+280
View File
@@ -0,0 +1,280 @@
/*
* xshow.c -- the M4 1-bit XImage + real-key-event client.
*
* The client installs the per-process XG block (Task 5), opens the VT1200 X
* display, creates a 640x480 window at (20,20), draws a 32-pixel checkerboard
* through a 1-bit LSBFirst XYBitmap XImage, then drains X events and quits on
* the real keyboard's `q`.
*
* Markers live at 0x018A8000 + n*0x20 (bit addresses):
* 0 Display * nonzero
* 1 Window nonzero
* 2 XImage * nonzero
* 3 quit 1 after the `q` KeyPress
* 4 events count of every XNextEvent
* 5 keycode ev.xkey.keycode of the last KeyPress
* 6 keysym XLookupString keysym of the last KeyPress
* 7 xerrors count of X protocol errors seen by on_error
* 8 focus XGetInputFocus window (0x400001 after the click)
* 9 last last X event type (2 = KeyPress)
*
* The static image is 480 * 80 bytes in .bss (640 pixels / 8 = 80 bytes per
* row). It is intentionally never passed to XDestroyImage: the ROM destructor
* frees the data pointer, which is .bss here, not heap memory.
*
* Two VT1200 server/Xlib constraints shape the drawing path. A single 38 KiB
* PutImage request does not complete (the ROM request buffer is 16 KiB), so the
* image is emitted in 32-row bands and each band selects its rows by moving
* XImage.data (the server ignores src_y). The server also copies a 1-bit
* bitmap correctly only when the destination x is 32-bit aligned, so the bands
* go into an aligned depth-1 pixmap and XCopyArea places the image at window
* x=12 (screen x=32); the 12-pixel prefix is filled with the first cell's
* colour. The checker cells are still 32 pixels, with a 12-pixel phase inside
* the (20,20) window.
*/
#include "x11m4.h"
#define MARK(n) (*(volatile unsigned int *)(0x018A8000u + (n) * 0x20u))
#define IMG_W 640
#define IMG_H 480
#define IMG_BPL 80 /* ((640 + 31) / 32) * 4 */
static int xerrors;
/*
* The image bytes: 480 rows * 80 bytes. It is built as 32-bit words because
* each word spans exactly one 32-pixel cell and LSBFirst makes word bit i
* pixel x = 32*cell + i. (The brief's per-bit byte OR loop compiles to an
* i8 load with `anyext`, which this target cannot select at -O2; the word
* form writes the identical bytes.) The 4-byte alignment lets the compiler
* use whole-word stores.
*/
static unsigned char data[IMG_H * IMG_BPL] __attribute__((aligned(4)));
/*
* The ROM calls this through the pointer installed by XSetErrorHandler. The
* Task 4 header's XErrorHandler is authoritative: int (Display*, XErrorEvent*).
*/
static int on_error(Display *d, XErrorEvent *e) {
(void)d;
(void)e;
xerrors++;
return 0;
}
/*
* Call the ROM sleep through a function pointer: the image then exercises the
* Task 3 indirect-call contract (CALL Rs) as well as the direct CALLA imports.
* It is volatile so the optimizer cannot devirtualise the load into a direct
* CALLA and erase that coverage. The pointer lives in .data as `.long _sleep`.
*/
static int (*volatile vt_sleep)(int) = sleep;
/*
* The ROM Xlib copies an XImage into its request buffer with the TMS34010
* blitter. `blit_copy` is the validated ROM entry _XPutImage calls
* (0x3F9CBFE0 -> 0x3FB44400): (source, destination, byte count). A process
* created by the loader inherits a zeroed blitter window/pitch context, so the
* first copy writes nothing; prime it once with a four-byte self-copy.
*/
static void prime_blitter(void) {
blit_copy(data, data, 4);
}
/*
* A single 640x480 1-bit PutImage is 0xC0 + 480*80 = 38592 bytes, but the ROM
* Xlib's request buffer is only 16384 bytes. XPutImage's direct-send path for
* an image larger than the buffer does not make progress in the harness, so
* emit the image in 32-row bands: each band's request fits the buffer, and the
* explicit flush/yield lets the X server drain it. The assembled drawable is
* still the whole 640x480 image.
*/
static void present(Display *d, Drawable target, Window w, GC gc) {
int y;
/*
* The server copies 1-bit bitmaps correctly only when the destination x is
* 32-bit aligned. Draw the aligned pixmap starting at window x = 12
* (screen x = 32) and fill the 12-pixel prefix with the preceding cell's
* colour, band by band, so the visible 32-pixel phase is preserved. The
* checker cells are still 32 pixels wide; the phase is offset by 12 pixels
* inside the (20,20) window. Only IMG_W - 12 = 628 columns are copied, so
* the rightmost 12 source columns (the final 12 px of the last cell) are
* not displayed and the last visible cell is 20 px, not 32.
*/
XCopyArea(d, target, w, gc, 0, 0, IMG_W - 12, IMG_H, 12, 0);
for (y = 0; y < IMG_H; y += 32) {
XSetForeground(d, gc, ((y / 32) & 1) == 0 ? 1 : 0);
XFillRectangle(d, w, gc, 0, y, 12, 32);
}
XSetForeground(d, gc, 1);
XFlush(d);
}
static void draw_image(Display *d, Drawable target, Window w, GC gc,
XImage *img) {
char *base = (char *)data;
int y;
prime_blitter();
for (y = 0; y < IMG_H; y += 32) {
/*
* src_y is ignored by the bitmap path, so select the band by moving
* the image data pointer.
*/
img->data = base + y * IMG_BPL;
XPutImage(d, target, gc, img, 0, 0, 0, y, IMG_W, 32);
XFlush(d);
vt_sleep(1);
}
img->data = base;
present(d, target, w, gc);
}
int xshow(void) {
Display *d;
Window w;
Pixmap pm;
GC gc;
XImage *img;
int tries;
int x, y;
if (!vt_install_xg())
return -1;
/*
* The display server may still be starting when the loader creates this
* process; retry as the termtris client does. XOpenDisplay allocates the
* connection, so one success ends the loop.
*/
d = 0;
for (tries = 0; tries < 40; tries++) {
d = XOpenDisplay("0::0");
if (d)
break;
vt_sleep(50);
}
MARK(0) = (unsigned)d;
if (!d)
return -1;
w = XCreateSimpleWindow(d, vt_root(d), 20, 20, IMG_W, IMG_H, 0, 0, 0);
MARK(1) = (unsigned)w;
/* These are counters/state, not loader-zeroed .bss: clear them first. */
MARK(3) = 0;
MARK(4) = 0;
MARK(5) = 0;
MARK(6) = 0;
MARK(7) = 0;
XSelectInput(d, w, ExposureMask | KeyPressMask | KeyReleaseMask |
ButtonPressMask | ButtonReleaseMask |
StructureNotifyMask | FocusChangeMask);
gc = XCreateGC(d, w, 0, 0);
XSetForeground(d, gc, 1);
pm = XCreatePixmap(d, w, IMG_W, IMG_H, 1);
/*
* 32-pixel checkerboard, LSB-first in each byte/word. Build whole 32-bit
* words (each word is exactly one 32-pixel cell) and toggle the row fill
* every 32 rows; this avoids the i8 anyext load and the i1
* sign_extend_inreg that the brief's per-bit loop creates at -O2.
*/
{
unsigned int fill = 0xFFFFFFFFu;
unsigned int band = 0;
for (y = 0; y < IMG_H; y++) {
unsigned int *row = (unsigned int *)(data + y * IMG_BPL);
unsigned int v = fill;
for (x = 0; x < IMG_BPL / 4; x++) {
row[x] = v;
v = ~v;
}
if (++band == 32) {
band = 0;
fill = ~fill;
}
}
}
/*
* visual 0 is safe for the ROM XCreateImage: it only dereferences the
* visual when the pointer is nonzero, and a depth-1 XYBitmap takes its
* byte/bit order from the Display. We set both orders explicitly below.
*/
img = XCreateImage(d, 0, 1, XYBitmap, 0, (char *)data, IMG_W, IMG_H, 32,
IMG_BPL);
MARK(2) = (unsigned)img;
if (!img)
return -1;
img->byte_order = LSBFirst;
img->bitmap_bit_order = LSBFirst;
XSetErrorHandler(on_error);
XMapWindow(d, w);
XRaiseWindow(d, w);
XSetInputFocus(d, w, RevertToParent);
draw_image(d, pm, w, gc, img);
XSync(d, 0);
{
Window fw = 0;
int rv = 0;
XGetInputFocus(d, &fw, &rv);
MARK(8) = (unsigned)fw;
}
for (;;) {
XEvent ev;
while (XPending(d)) {
XNextEvent(d, &ev);
MARK(4) = MARK(4) + 1;
MARK(9) = (unsigned)ev.type;
if (ev.type == KeyPress) {
char buf[8];
unsigned long ks = 0;
int n;
buf[0] = 0;
n = XLookupString(&ev.xkey, buf, 8, &ks, 0);
MARK(5) = (unsigned)ev.xkey.keycode;
MARK(6) = (unsigned)ks;
/* The server may hand back the shifted form. */
if (n > 0 && (buf[0] == 'q' || buf[0] == 'Q')) {
MARK(3) = 1;
break;
}
} else if (ev.type == ButtonRelease) {
Window fw = 0;
int rv = 0;
XGetInputFocus(d, &fw, &rv);
MARK(8) = (unsigned)fw;
} else if (ev.type == Expose) {
present(d, pm, w, gc);
}
}
MARK(7) = (unsigned)xerrors;
if (MARK(3))
break;
XFlush(d);
vt_sleep(5);
}
/*
* Quit: stop serving events, but keep the process alive so the harness can
* read the mapped image out of VRAM at LOADER_END. Returning here would
* let the firmware tear the process (and its window) down before the read.
*/
for (;;) {
MARK(7) = (unsigned)xerrors;
XFlush(d);
vt_sleep(5);
}
}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# M1 test gate: rebuild the linked COFF through the normal GSPA/GSPLNK path,
# then verify dis34 decodes the emitted instructions at the linked addresses.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
# Always rebuild: a stale build/m1/m1.coff would let an asm-encoding
# regression pass without ever touching the compiler or assembler. Keep
# dis-roundtrip.sh pointed at the file this run produces, including when
# M1_WORK/M1_COFF are overridden.
WORK=${M1_WORK:-$ROOT/build/m1}
M1_COFF=${M1_COFF:-$WORK/m1.coff}
export M1_COFF
"$ROOT/scripts/m1/run.sh" "$@"
exec "$ROOT/scripts/m1/dis-roundtrip.sh" "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# M1 test gate: compile six C functions, assemble/link with GSPA/GSPLNK,
# load the COFF in MAME and check the marker words.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
exec "$ROOT/scripts/m1/run.sh" "$@"
+133
View File
@@ -0,0 +1,133 @@
#!/bin/sh
# M2 differential TI-compiler ABI check.
#
# scripts/tests/m2/diff/check.sh
#
# The stored ti-add2.s/ti-locals.s are one-time outputs of the 1987 TI
# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this
# script greps the stored golden files and compares their ABI shape with the
# assembly our compiler emits for the same K&R sources.
#
# The oracle is the TI ABI layout, not instruction selection:
#
# add2 first argument at FP(-32), second at FP(-64), result in A8, RETS 2
# locals a nonzero frame, result in A8, RETS 0
#
# TI prints the negative offsets as -32/-64; 0FFE0h/0FFC0h are also accepted
# because the 1987 assembler may format the same displacement in hex.
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_ADD2 TI_LOCALS
set -eu
ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
DIR=$ROOT/scripts/tests/m2/diff
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
TI_ADD2=${TI_ADD2:-$DIR/ti-add2.s}
TI_LOCALS=${TI_LOCALS:-$DIR/ti-locals.s}
export TMS_CLANG TMS_OPT TMS_BITADDR
fail() {
echo "M2 diff: FAIL: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$TI_ADD2" ] || fail "stored TI golden missing: $TI_ADD2"
[ -f "$TI_LOCALS" ] || fail "stored TI golden missing: $TI_LOCALS"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m2-diff.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# Our add2 is built exactly like the M2 gate: tms-clang -O2, then llc -O0.
# locals uses -O0 because at -O2 the optimizer folds `x = 7; return x` to a
# constant and no frame is emitted; the TI compiler keeps the frame, so -O0 is
# the level at which the ABI frame shape is comparable.
echo "== compile our add2.s and locals.s"
"$ROOT/scripts/tms-clang" -O2 -o "$TMP/add2.ll" "$DIR/ti-add2.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/add2.ll" -o "$TMP/add2.s"
"$ROOT/scripts/tms-clang" -O0 -o "$TMP/locals.ll" "$DIR/ti-locals.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/locals.ll" -o "$TMP/locals.s"
echo " add2.s: $(grep -c . "$TMP/add2.s") lines"
echo " locals.s: $(grep -c . "$TMP/locals.s") lines"
# Ignore our `;` end-of-line comments so a comment cannot satisfy a check.
sed 's/;.*//' "$TMP/add2.s" > "$TMP/add2.code"
sed 's/;.*//' "$TMP/locals.s" > "$TMP/locals.code"
require() {
_file=$1
_pat=$2
_what=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_what"
return 0
fi
echo "M2 diff: FAIL: missing $_what" >&2
echo " pattern: $_pat" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
require_rets() {
require "$1" "^[[:space:]]*RETS[[:space:]]+$2([[:space:]]|;|$)" "$3"
}
# TI golden: the argument reads are direct FP-relative loads.
require "$TI_ADD2" 'MOVE[[:space:]]+\*FP\((-32|0FFE0h)\)' \
"TI add2 reads the first argument at FP(-32)"
require "$TI_ADD2" 'MOVE[[:space:]]+\*FP\((-64|0FFC0h)\)' \
"TI add2 reads the second argument at FP(-64)"
require "$TI_ADD2" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \
"TI add2 returns in A8"
require_rets "$TI_ADD2" 2 "TI add2 ends with RETS 2"
require "$TI_LOCALS" '(ADDK|ADDI)[[:space:]]+\+?[1-9][0-9]*[[:space:]]*,[[:space:]]*(STK|A14)' \
"TI locals allocates a nonzero frame"
require "$TI_LOCALS" 'MOVE[[:space:]]+(STK[[:space:]]*,[[:space:]]*(FP|A13)|A14[[:space:]]*,[[:space:]]*(FP|A13))' \
"TI locals establishes FP from STK"
require "$TI_LOCALS" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \
"TI locals returns in A8"
require_rets "$TI_LOCALS" 0 "TI locals ends with RETS 0"
# Our asm: FP is materialised with MOVE A13,A<n>; the argument read is at an
# ADDI offset from that register. Check the offset and that the register is
# then dereferenced.
check_our_arg() {
_file=$1
_off=$2
_desc=$3
_reg=$(sed -nE "s/.*ADDI[[:space:]]+($_off)[[:space:]]*,[[:space:]]*A([0-9]+).*/\2/p" \
"$_file" | sed -n '1p')
if [ -z "$_reg" ]; then
echo "M2 diff: FAIL: our add2 has no ADDI $_off frame offset for $_desc" >&2
sed 's/^/ /' "$_file" >&2
exit 1
fi
require "$_file" "MOVE[[:space:]]+A13[[:space:]]*,[[:space:]]*A$_reg" \
"our add2 materialises FP for $_desc"
require "$_file" "MOVE[[:space:]]+\*A$_reg([[:space:]]|,|$)" \
"our add2 reads $_desc through the FP register"
}
check_our_arg "$TMP/add2.code" '-32|0FFE0h' "the first argument at FP(-32)"
check_our_arg "$TMP/add2.code" '-64|0FFC0h' "the second argument at FP(-64)"
require "$TMP/add2.code" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \
"our add2 returns in A8"
require_rets "$TMP/add2.code" 2 "our add2 ends with RETS 2"
require "$TMP/locals.code" 'ADDI[[:space:]]+\+?[1-9][0-9]*[[:space:]]*,[[:space:]]*A14' \
"our locals allocates a nonzero frame"
require "$TMP/locals.code" 'MOVE[[:space:]]+A14[[:space:]]*,[[:space:]]*A13' \
"our locals establishes FP from STK"
require "$TMP/locals.code" 'MOVE[[:space:]]+[^,]+[[:space:]]*,[[:space:]]*A8([[:space:]]|,|$)' \
"our locals returns in A8"
require_rets "$TMP/locals.code" 0 "our locals ends with RETS 0"
echo "M2 diff: PASS"
+1
View File
@@ -0,0 +1 @@
int add2(a, b) int a; int b; { return a + b; }
+22
View File
@@ -0,0 +1,22 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _add2
.file "ADD2.c"
******************************************************
* FUNCTION DEF : _add2
******************************************************
_add2:
MMTM SP,A5,A7,FP
MOVE STK,FP
MOVE *FP(-32),A7,1
MOVE *FP(-64),A5,1
ADD A5,A7
MOVE A7,A8
EPI0_1:
MOVE *SP(128),STK,1
MMFM SP,A5,A7,FP
RETS 2
.end
+1
View File
@@ -0,0 +1 @@
int locals() { int x; x = 7; return x; }
+22
View File
@@ -0,0 +1,22 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _locals
.file "LOCALS.c"
******************************************************
* FUNCTION DEF : _locals
******************************************************
_locals:
MMTM SP,A12,FP
MOVE STK,FP
ADDK 32,STK
MOVK 7,A12
MOVE A12,*FP,1
MOVE *FP,A8,1
EPI0_1:
SUBK 32,STK
MMFM SP,A12,FP
RETS 0
.end
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# M2 test gate: rebuild the linked COFF through the normal GSPA/GSPLNK path,
# then verify dis34 decodes the TI-ABI instructions at the linked addresses.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
# Always rebuild: a stale build/m2/m2.coff would let an asm-encoding
# regression pass without ever touching the compiler or assembler. Keep
# dis-roundtrip.sh pointed at the file this run produces, including when
# M2_WORK/M2_COFF are overridden.
WORK=${M2_WORK:-$ROOT/build/m2}
M2_COFF=${M2_COFF:-$WORK/m2.coff}
export M2_COFF
"$ROOT/scripts/m2/run.sh" "$@"
exec "$ROOT/scripts/m2/dis-roundtrip.sh" "$@"
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# M2 test gate: compile five C functions, assemble/link with GSPA/GSPLNK,
# load the COFF in MAME and check the TI-ABI marker words and ROM strlen call.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
exec "$ROOT/scripts/m2/run.sh" "$@"
+268
View File
@@ -0,0 +1,268 @@
#!/bin/sh
# M3 differential TI-compiler control-flow/Xlib check.
#
# scripts/tests/m3/diff/check.sh
#
# The stored ti-branch.s/ti-xhello.s are one-time outputs of the 1987 TI
# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this
# script greps the stored golden files and compares their ABI shape with the
# assembly our compiler emits for the equivalent C.
#
# The oracle is the ABI shape, not instruction selection. Each requirement
# accepts the TI spellings and ours:
#
# sumto a conditional branch, an unconditional loop back-edge, and a last
# RETS of RETS 2
# max2 a conditional branch, and a last RETS of RETS 2
# xhello nine argument pushes before _XCreateSimpleWindow, and the ROM bit
# displacements 0x480 (Display.screens) / 0x40 (Screen.root)
#
# This check does not verify the result register, the argument order, or the
# "background first, display last" sequence: the MAME gate checks the returned
# values, and dis-roundtrip.sh checks the push order.
#
# TI emits JRLE/JRUC where we emit JAxx/JAUC; both are accepted. TI prints the
# displacements as decimal 1152/64, so 480h/40h are accepted too. A
# displacement only counts inside the load that uses it: a bare 64/1152
# elsewhere in the body (an unrelated local at *FP(64), the constant 0x400,
# an 0x4800 that merely shares a prefix) is not evidence.
#
# Our branch.c is built exactly like the M3 gate: tms-clang -O2 with the
# -replexitval=never optimizer (that rewrite would turn sumto's loop into an
# i33 multiply, which the target cannot select), then llc -O0. Dropping
# -replexitval=never makes this check fail to compile the loop, not pass.
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_BRANCH TI_XHELLO
set -eu
ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
DIR=$ROOT/scripts/tests/m3/diff
SRC=$ROOT/scripts/m3
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
TI_BRANCH=${TI_BRANCH:-$DIR/ti-branch.s}
TI_XHELLO=${TI_XHELLO:-$DIR/ti-xhello.s}
export TMS_CLANG TMS_OPT TMS_BITADDR
fail() {
echo "M3 diff: FAIL: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$TI_BRANCH" ] || fail "stored TI golden missing: $TI_BRANCH"
[ -f "$TI_XHELLO" ] || fail "stored TI golden missing: $TI_XHELLO"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m3-diff.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# branch.c is generated through a wrapper so only this file keeps its loop.
OPT_NOREPLEXIT=$TMP/opt-noreplexit
printf '#!/bin/sh\nexec "%s" -replexitval=never "$@"\n' "$TMS_OPT" > "$OPT_NOREPLEXIT"
chmod +x "$OPT_NOREPLEXIT"
echo "== compile our branch.s and xhello.s"
TMS_OPT=$OPT_NOREPLEXIT "$ROOT/scripts/tms-clang" -O2 -I "$SRC" \
-o "$TMP/branch.ll" "$SRC/branch.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/branch.ll" -o "$TMP/branch.s"
"$ROOT/scripts/tms-clang" -O2 -I "$SRC" \
-o "$TMP/xhello.ll" "$SRC/xhello.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm "$TMP/xhello.ll" -o "$TMP/xhello.s"
echo " branch.s: $(grep -c . "$TMP/branch.s") lines"
echo " xhello.s: $(grep -c . "$TMP/xhello.s") lines"
# Our llc output has `;` end-of-line comments (`; %bb.0`). Strip them so a
# comment cannot satisfy a check; TI asm has none.
for f in "$TMP/branch.s" "$TMP/xhello.s"; do
sed 's/;.*//' "$f" > "$f.code"
done
# body FILE FUNC: from `_func:` to the next .globl/.end.
body() {
awk -v want="_$2:" '
$1 == want { infn = 1; next }
infn && ($1 == ".globl" || $1 == ".end") { exit }
infn { print }
' "$1"
}
require() {
_file=$1
_pat=$2
_what=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_what"
return 0
fi
echo "M3 diff: FAIL: missing $_what" >&2
echo " pattern: $_pat" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
# require_last_rets2 FILE WHAT: the last RETS instruction in the body (the
# epilogue) is RETS 2. Matching any RETS 2 elsewhere would let a wrong
# epilogue through.
require_last_rets2() {
if awk '
/^[[:space:]]*RETS([[:space:]]|$)/ { last = $0; seen = 1 }
END {
if (!seen)
exit 1
exit (last ~ /^[[:space:]]*RETS[[:space:]]+2[[:space:]]*$/) ? 0 : 1
}
' "$1"; then
echo " ok $2"
return 0
fi
echo "M3 diff: FAIL: $2 (last RETS is not RETS 2)" >&2
sed 's/^/ /' "$1" >&2
exit 1
}
# A conditional branch: JA/JR plus a condition suffix. JAUC/JRUC/JRU do not
# match because their suffix is U/UC.
COND='^[[:space:]]*J[A-Z]*(EQ|NE|LT|LE|GT|GE|V|NV|P|N|Z|NZ|PL|MI)[[:space:]]'
# An unconditional branch: our JAUC, TI's JRUC (and bare JRU).
UNCOND='^[[:space:]]*J(AUC|RUC|RU|UMP)[[:space:]]'
# has_backedge FILE: true when a branch targets a label defined earlier in the
# same function body. TI closes the loop with the conditional JRLE L4; we
# close it with the unconditional JAUC LBB0_1, so either form counts.
has_backedge() {
awk '
/^[A-Za-z_][A-Za-z0-9_]*:/ {
lbl = $1
sub(/:$/, "", lbl)
pos[lbl] = NR
next
}
/^[[:space:]]*J/ {
if ($2 in pos && pos[$2] < NR)
found = 1
}
END { exit found ? 0 : 1 }
' "$1"
}
check_branch() { # check_branch FILE LABEL
_f=$1
body "$_f" sumto > "$TMP/sumto.body"
body "$_f" max2 > "$TMP/max2.body"
require "$TMP/sumto.body" "$COND" "$2: sumto has a conditional branch"
require "$TMP/sumto.body" "$UNCOND" "$2: sumto has an unconditional branch"
if ! has_backedge "$TMP/sumto.body"; then
echo "M3 diff: FAIL: $2: sumto has no loop back-edge" >&2
sed 's/^/ /' "$TMP/sumto.body" >&2
exit 1
fi
echo " ok $2: sumto branches back to the loop"
require_last_rets2 "$TMP/sumto.body" "$2: sumto ends with RETS 2"
require "$TMP/max2.body" "$COND" "$2: max2 has a conditional branch"
require_last_rets2 "$TMP/max2.body" "$2: max2 ends with RETS 2"
}
echo "== ABI shape: TI golden"
check_branch "$TI_BRANCH" "TI"
echo "== ABI shape: our compiler"
check_branch "$TMP/branch.s.code" "ours"
# Count the argument pushes since the previous CALLA, then require nine for
# XCreateSimpleWindow. Both compilers push the last argument first: display,
# root, x, y, width, height, border_width, border, background.
count_xcreate_pushes() {
awk '
/^_xhello:/ { infn = 1; n = 0; next }
!infn { next }
/CALLA[[:space:]]+_XCreateSimpleWindow/ { print n; exit }
/CALLA/ { n = 0; next }
/MOVE[[:space:]]+[^,]+,[[:space:]]*\*(STK|A14)\+/ { n++ }
' "$1"
}
for pair in "$TI_XHELLO:TI" "$TMP/xhello.s.code:ours"; do
f=${pair%%:*}
who=${pair##*:}
pushes=$(count_xcreate_pushes "$f")
[ -n "$pushes" ] || fail "$who xhello has no CALLA _XCreateSimpleWindow"
[ "$pushes" = "9" ] \
|| fail "$who XCreateSimpleWindow has $pushes argument pushes, want 9"
echo " ok $who: XCreateSimpleWindow takes nine argument pushes"
done
# TI: Display.screens is the indexed load `MOVE *A7(1152),A7,1`; Screen.root
# is loaded by `MOVE *A7(64),*FP(32),1`. The closing paren and the comma keep
# the boundary exact, so 0x4800/0x400 and decimal 11520/640 do not match; the
# register names may vary.
TI_SCREENS='^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((1152|0x480|480h)\),'
TI_ROOT='^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((64|0x40|40h)\),[[:space:]]*\*(A[0-9]+|FP|SP)\([0-9]+\)'
# has_addi_load FILE screens|root: our displacement is materialised by
# `ADDI <disp>, A<n>` and then used by a load through that same register
# (`MOVE *A<n>,...`). Only that pairing counts, not any ADDI in the body.
has_addi_load() {
awk -v kind="$2" '
function disp_ok(d) {
if (kind == "screens")
return d == "1152" || d == "0x480" || d == "480h"
return d == "64" || d == "0x40" || d == "40h"
}
/^[[:space:]]*ADDI[[:space:]]/ {
line = $0
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
split(line, f, /[[:space:]]*,[[:space:]]*/)
disp = f[1]
reg = f[2]
if (getline nxt <= 0)
next
while (nxt ~ /^[[:space:]]*$/) {
if (getline nxt <= 0)
next
}
if (disp_ok(disp) &&
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+\\*" reg "([[:space:],]|$)"))
found = 1
}
END { exit found ? 0 : 1 }
' "$1"
}
check_offsets() { # check_offsets FILE WHO ti|ours
_f=$1
_who=$2
_side=$3
body "$_f" xhello > "$TMP/xhello.body"
if [ "$_side" = ti ]; then
require "$TMP/xhello.body" "$TI_SCREENS" \
"$_who: Display.screens is loaded at bit 0x480 (*A?(1152))"
require "$TMP/xhello.body" "$TI_ROOT" \
"$_who: Screen.root is loaded at bit 0x40 (*A?(64),*A?(...))"
return 0
fi
if has_addi_load "$TMP/xhello.body" screens; then
echo " ok $_who: Display.screens is loaded at bit 0x480 (ADDI 1152 + load)"
else
echo "M3 diff: FAIL: missing $_who: Display.screens load at bit 0x480" >&2
sed 's/^/ /' "$TMP/xhello.body" >&2
exit 1
fi
if has_addi_load "$TMP/xhello.body" root; then
echo " ok $_who: Screen.root is loaded at bit 0x40 (ADDI 64 + load)"
else
echo "M3 diff: FAIL: missing $_who: Screen.root load at bit 0x40" >&2
sed 's/^/ /' "$TMP/xhello.body" >&2
exit 1
fi
}
echo "== ROM bit displacements"
check_offsets "$TI_XHELLO" "TI" ti
check_offsets "$TMP/xhello.s.code" "ours" ours
echo "M3 diff: PASS"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Negative controls for scripts/tests/m3/diff/check.sh.
#
# scripts/tests/m3/diff/negative-control.sh
#
# The differential check must reject a TI golden whose real Screen.root or
# Display.screens displacement is wrong. This is a guard on the oracle
# itself: an earlier check matched `64`/`1152` anywhere in the function body,
# so the unrelated local `*FP(64)` (and the prefix `0x400`) satisfied it even
# after the real load was changed to 512. Each mutation below must make
# check.sh fail; the stored goldens are never modified.
#
# Overridable: the same variables as check.sh.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m3-diff-neg.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
expect_fail() { # expect_fail WHAT MUTATED_GOLDEN
if TI_XHELLO=$2 "$DIR/check.sh" > "$TMP/out" 2>&1; then
echo "M3 diff negative control: FAIL: check.sh accepted $1" >&2
sed 's/^/ /' "$TMP/out" >&2
exit 1
fi
echo " ok negative control: $1 rejected"
grep 'M3 diff: FAIL' "$TMP/out" | sed 's/^/ /'
}
# Screen.root: *A7(64),*FP(32),1 -> *A7(512),*FP(32),1
sed 's/\*A7(64),\*FP(32)/*A7(512),*FP(32)/' \
"$DIR/ti-xhello.s" > "$TMP/root-512.s"
grep -q '\*A7(512),\*FP(32)' "$TMP/root-512.s" \
|| { echo "M3 diff negative control: mutation did not apply" >&2; exit 1; }
expect_fail "Screen.root displacement 512" "$TMP/root-512.s"
# Display.screens: *A7(1152) -> *A7(512)
sed 's/\*A7(1152)/*A7(512)/' \
"$DIR/ti-xhello.s" > "$TMP/screens-512.s"
grep -q '\*A7(512)' "$TMP/screens-512.s" \
|| { echo "M3 diff negative control: mutation did not apply" >&2; exit 1; }
expect_fail "Display.screens displacement 512" "$TMP/screens-512.s"
# Boundary: *A7(1152) -> *A7(0x4800), which shares the 0x480 prefix.
sed 's/\*A7(1152)/*A7(0x4800)/' \
"$DIR/ti-xhello.s" > "$TMP/screens-4800.s"
grep -q '\*A7(0x4800)' "$TMP/screens-4800.s" \
|| { echo "M3 diff negative control: mutation did not apply" >&2; exit 1; }
expect_fail "Display.screens displacement 0x4800 (hex boundary)" "$TMP/screens-4800.s"
echo "M3 diff negative controls: PASS"
+4
View File
@@ -0,0 +1,4 @@
/* K&R source for the M3 differential golden: the same two functions as
scripts/m3/branch.c, compiled by the 1987 TI compiler. */
int sumto(n) int n; { int s; int i; s = 0; for (i = 1; i <= n; i++) s = s + i; return s; }
int max2(a, b) int a; int b; { if (a > b) return a; return b; }
+56
View File
@@ -0,0 +1,56 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _sumto
.file "BRANCH.c"
******************************************************
* FUNCTION DEF : _sumto
******************************************************
_sumto:
MMTM SP,A5,A7,A12,FP
MOVE STK,FP
ADDI 64,STK
CLR A12
MOVE A12,*FP,1
ADDK 1,A12
MOVE A12,*FP(32),1
JRUC L1
L4:
MOVE *FP(32),A7,1
MOVE *FP,A5,1
ADD A7,A5
MOVE A5,*FP,1
ADDK 1,A7
MOVE A7,*FP(32),1
L1:
MOVE *FP(32),A7,1
MOVE *FP(-32),A5,1
CMP A5,A7
JRLE L4
MOVE *FP,A8,1
EPI0_1:
MOVE *SP(160),STK,1
MMFM SP,A5,A7,A12,FP
RETS 2
.globl _max2
******************************************************
* FUNCTION DEF : _max2
******************************************************
_max2:
MMTM SP,A5,A7,FP
MOVE STK,FP
MOVE *FP(-32),A7,1
MOVE *FP(-64),A5,1
CMP A5,A7
JRLE L5
MOVE A7,A8
JRUC EPI0_2
L5:
MOVE A5,A8
EPI0_2:
MOVE *SP(128),STK,1
MMFM SP,A5,A7,FP
RETS 2
.end
+37
View File
@@ -0,0 +1,37 @@
/* K&R source for the M3 differential golden: the same four-call Xlib client
as scripts/m3/xhello.c, compiled by the 1987 TI compiler.
The ROM's Display/Screen fields are at bit displacements 0x480/0x40, i.e.
byte offsets 0x90/0x8; the 1987 compiler's &-of-member arithmetic is in
bits, so it emits 1152 and 64 for these pads. */
struct display {
char pad[0x90];
void *screens;
};
struct screen {
char pad[0x8];
unsigned int root;
};
extern struct display *XOpenDisplay();
extern int XCreateSimpleWindow();
extern int XMapWindow();
extern int XSync();
int xhello()
{
struct display *d;
unsigned int root;
int w;
int s;
d = XOpenDisplay("0::0");
if (!d)
return -1;
root = ((struct screen *)d->screens)->root;
w = XCreateSimpleWindow(d, root, 100, 100, 400, 200, 2, 0, 1);
XMapWindow(d, w);
s = XSync(d, 0);
return w;
}
+69
View File
@@ -0,0 +1,69 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _XOpenDisplay
.globl _XCreateSimpleWindow
.globl _XMapWindow
.globl _XSync
.globl _xhello
.file "XHELLO.c"
******************************************************
* FUNCTION DEF : _xhello
******************************************************
_xhello:
MMTM SP,A7,FP
MOVE STK,FP
ADDI 128,STK
MOVE STK,-*SP,1
MOVI SL1,A8
MOVE A8,*STK+,1
CALLA _XOpenDisplay
MOVE A8,*FP,1
MOVE *FP,A7,1
JRNZ L1
MOVI -1,A8
JRUC EPI0_1
L1:
MOVE *A7(1152),A7,1
MOVE *A7(64),*FP(32),1
MOVE STK,-*SP,1
MOVK 1,A7
MOVE A7,*STK+,1
SUB A7,A7
MOVE A7,*STK+,1
ADDK 2,A7
MOVE A7,*STK+,1
MOVI 200,A7
MOVE A7,*STK+,1
MOVI 400,A7
MOVE A7,*STK+,1
MOVI 100,A7
MOVE A7,*STK+,1
MOVE A7,*STK+,1
MOVE *FP(32),*STK+,1
MOVE *FP(0),*STK+,1
CALLA _XCreateSimpleWindow
MOVE A8,*FP(64),1
MOVE STK,-*SP,1
MOVE *FP(64),*STK+,1
MOVE *FP(0),*STK+,1
CALLA _XMapWindow
MOVE STK,-*SP,1
CLR A7
MOVE A7,*STK+,1
MOVE *FP(0),*STK+,1
CALLA _XSync
MOVE A8,*FP(96),1
MOVE *FP(64),A8,1
EPI0_1:
ADDI -128,STK
MMFM SP,A7,FP
RETS 0
******************************************************
* DEFINE STRING CONSTANTS *
******************************************************
.text
SL1: .string "0::0",0
.end
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
# M3 test gate: rebuild the linked COFF through the normal GSPA/GSPLNK path,
# then verify dis34 decodes the Xlib call sequence, branch code and old-STK
# restore offsets at the linked addresses.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
# Always rebuild: a stale build/m3/m3.coff would let an assembler/compiler
# regression pass without ever being reassembled. Keep dis-roundtrip.sh
# pointed at the file this run produces, including when M3_WORK/M3_COFF are
# overridden.
WORK=${M3_WORK:-$ROOT/build/m3}
M3_COFF=${M3_COFF:-$WORK/m3.coff}
export M3_COFF
"$ROOT/scripts/m3/run.sh" "$@"
exec "$ROOT/scripts/m3/dis-roundtrip.sh" "$@"
+92
View File
@@ -0,0 +1,92 @@
#!/bin/sh
# M3 layout gate: Display.screens and Screen.root must be at the ROM's bit
# displacements 0x480 and 0x40.
#
# The ROM's indirect displacements are *bit* offsets (TMS34010), so those
# displacements are byte offsets 0x90 and 0x8. layout.c asserts the byte
# layout and returns the bit displacements; layout-ti.c does the same through
# the 1987 compiler's bit-addressed &-of-member. Both asm files must carry
# 0x480 / 0x40 (480h, 1152, ...).
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
WORK=${M3_LAYOUT_WORK:-$ROOT/build/m3-layout}
GOLDEN=$ROOT/scripts/m3/layout-ti.s
fail() {
echo "layout.sh: FAIL: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_LLC" "$TMS_BITADDR"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$GOLDEN" ] || fail "missing TI golden: $GOLDEN"
mkdir -p "$WORK"
# Exit 0 here means the _Static_asserts in layout.c held on the target.
TMS_CLANG="$TMS_CLANG" TMS_OPT="$TMS_OPT" TMS_BITADDR="$TMS_BITADDR" \
"$ROOT/scripts/tms-clang" -O0 -o "$WORK/layout.ll" \
"$ROOT/scripts/m3/layout.c" \
|| fail "target compile failed (Display/Screen static asserts?)"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$WORK/layout.ll" -o "$WORK/layout.s" \
|| fail "llc failed for layout.ll"
# Print the body of one function, from its label to the next .globl/.end.
body() { # body FILE FUNC
awk -v want="_$2:" '
$1 == want { infn = 1; next }
infn && ($1 == ".globl" || $1 == ".end") { exit }
infn { print }
' "$1"
}
# Allowed spellings for a constant: C hex, assembler hex with `h`, decimal.
has_const() { # has_const FILE FUNC ERE
body "$1" "$2" | tr 'A-Z' 'a-z' | grep -Eq "$3"
}
check() { # check FILE FUNC ERE WHAT
if ! has_const "$1" "$2" "$3"; then
echo "layout.sh: $1: _$2 body:" >&2
body "$1" "$2" >&2
fail "_$2 in $1 does not contain $4"
fi
}
HEX480='(0x480|480h|[^0-9a-f]1152[^0-9a-f])'
HEX40='(0x40|40h|[^0-9a-f]64[^0-9a-f])'
check "$WORK/layout.s" layout_screens "$HEX480" "0x480 (Display.screens)"
check "$WORK/layout.s" layout_root "$HEX40" "0x40 (Screen.root)"
check "$GOLDEN" layout_screens "$HEX480" "0x480 (Display.screens)"
check "$GOLDEN" layout_root "$HEX40" "0x40 (Screen.root)"
# root_probe exercises the real access path: vt1200_root() follows
# Display.screens (bit 0x480) and reads Screen.root (bit 0x40). The helper is
# always_inline, so the incoming align-1 loads select MOVE directly in
# root_probe (see m3-unaligned.ll). Require both displacements and reject a
# MOVB byte split.
check "$WORK/layout.s" root_probe "$HEX480" "0x480 (Display.screens)"
check "$WORK/layout.s" root_probe "$HEX40" "0x40 (Screen.root)"
if body "$WORK/layout.s" root_probe | grep -q 'MOVB'; then
fail "root_probe contains MOVB: align-1 field loads did not select MOVE"
fi
# Guard against the old byte-pad mistake: 0x480*8 / 0x40*8 (9216 / 512)
# means the struct was padded in bytes past the ROM's bit displacements.
if body "$GOLDEN" layout_screens | tr 'A-Z' 'a-z' |
grep -Eq '(^|[^0-9])(9216|2400h)([^0-9a-f]|$)' ||
body "$GOLDEN" layout_root | tr 'A-Z' 'a-z' |
grep -Eq '(^|[^0-9])(512|200h)([^0-9a-f]|$)'; then
fail "TI golden carries byte-pad offsets (9216/512); ROM displacements are bits 0x480/0x40"
fi
echo "layout.sh: PASS (Display.screens=0x480, Screen.root=0x40; target + TI golden)"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# M3 test gate: validate the Display/Screen layout and ROM entries, compile the
# branch/loop tests and the first C Xlib client, assemble/link with GSPA/GSPLNK,
# load the COFF in MAME, and require the branch markers plus a solid white
# window area.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
# These two checks are part of the gate, not manual steps: layout.sh proves the
# 0x480/0x40 root probe still selects the ROM displacements, and
# validate-syms.sh proves the four Xlib imports are still the v2.2 entries.
"$ROOT/scripts/tests/m3/layout.sh"
"$ROOT/scripts/m3/validate-syms.sh"
exec "$ROOT/scripts/m3/run.sh" "$@"
+568
View File
@@ -0,0 +1,568 @@
#!/bin/sh
# M4 differential TI-compiler check: function pointers and the Xlib image ABI.
#
# scripts/tests/m4/diff/check.sh
#
# The stored ti-fnptr.s/ti-image.s are one-time outputs of the 1987 TI
# compiler (GSP C Compiler 2.01) under DOSBox. CI never runs DOSBox: this
# script greps the stored golden files and compares their ABI shape with the
# assembly our compiler emits for the equivalent C.
#
# The oracle is the ABI shape, not instruction selection:
#
# apply the old-STK save, one program-stack argument push, a call through
# an A register (TI CALL, ours CALL; CALLR is accepted too), and a
# last RETS of RETS 2. A direct CALLA in apply is a failure.
# test the direct CALLA _apply calls (at least three; TI has three,
# ours has four because the fixture calls apply again in the return).
# image XCreateImage and XPutImage take ten argument pushes, XCreatePixmap
# five, and XCreateSimpleWindow nine. Every call saves old STK.
# The first pushes are the last arguments, so the leading constants
# are XCreateImage 80,32,480,640; XPutImage 32,640; XCreatePixmap
# 1,480,640; XCreateSimpleWindow 0,0,0,480,640. The final push is
# the Display pointer, not a constant. This is what makes the
# reversed-argument negative control fail.
# offsets Display.screens (bit 0x480, byte 0x90), Screen.root (bit 0x40,
# byte 0x8), and XImage data/byte_order/bytes_per_line/bits_per_pixel/
# f (bits 0x80/0xA0/0x140/0x160/0x200). TI uses the packed direct
# displacements; ours materialises an ADDI and the matching load or
# store through the same register.
#
# The plan's "XCreateImage/XPutImage 10/9" is a typo for 10/10: XPutImage is
# (display, drawable, gc, image, src_x, src_y, dest_x, dest_y, width, height),
# ten arguments. Nine is XCreateSimpleWindow.
#
# Negative controls run at the end: the check is re-run on a copy of the
# fnptr golden with the indirect call removed, on a copy of the image golden
# with the XPutImage argument pushes fully reversed, and on a copy with only
# the Display and drawable pushes swapped. All must fail. The stored goldens
# are never modified.
#
# Our fnptr source is scripts/m4/fnptr.c (the runtime gate fixture). The image
# equivalent is generated here because it needs only the layout declarations.
#
# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC TI_FNPTR TI_IMAGE
# OUR_FNPTR; M4_DIFF_POSITIVE_ONLY=1 skips the negative controls.
set -eu
ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
DIR=$ROOT/scripts/tests/m4/diff
LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin}
TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang}
TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt}
TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr}
TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc}
TI_FNPTR=${TI_FNPTR:-$DIR/ti-fnptr.s}
TI_IMAGE=${TI_IMAGE:-$DIR/ti-image.s}
OUR_FNPTR=${OUR_FNPTR:-$ROOT/scripts/m4/fnptr.c}
export TMS_CLANG TMS_OPT TMS_BITADDR
fail() {
echo "M4 diff: FAIL: $*" >&2
exit 1
}
for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do
[ -x "$tool" ] || fail "missing tool: $tool"
done
[ -f "$TI_FNPTR" ] || fail "stored TI golden missing: $TI_FNPTR"
[ -f "$TI_IMAGE" ] || fail "stored TI golden missing: $TI_IMAGE"
[ -f "$OUR_FNPTR" ] || fail "our fnptr source missing: $OUR_FNPTR"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m4-diff.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
echo "== compile our fnptr.s and image.s"
TMS_CLANG="$TMS_CLANG" TMS_OPT="$TMS_OPT" TMS_BITADDR="$TMS_BITADDR" \
"$ROOT/scripts/tms-clang" -O2 -o "$TMP/fnptr.ll" "$OUR_FNPTR"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$TMP/fnptr.ll" -o "$TMP/fnptr.s"
cat > "$TMP/image.c" <<'EOF'
struct display { unsigned char pad[0x90]; void *screens; };
struct screen { unsigned char pad[0x8]; unsigned int root; };
struct ximage_funcs {
void *create_image; void *destroy_image; void *get_pixel;
void *put_pixel; void *sub_image; void *add_pixel;
};
struct ximage {
int width, height; int xoffset; int format; char *data; int byte_order;
int bitmap_unit; int bitmap_bit_order; int bitmap_pad; int depth;
int bytes_per_line; int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct ximage_funcs f;
};
extern struct display *XOpenDisplay(const char *);
extern int XCreateSimpleWindow(struct display *, unsigned int, int, int,
unsigned int, unsigned int, unsigned int,
unsigned long, unsigned long);
extern unsigned int XCreatePixmap(struct display *, unsigned int, unsigned int,
unsigned int, unsigned int);
extern struct ximage *XCreateImage(struct display *, void *, unsigned int, int,
int, char *, unsigned int, unsigned int,
int, int);
extern int XPutImage(struct display *, unsigned int, void *, struct ximage *,
int, int, int, int, unsigned int, unsigned int);
extern int XMapWindow(struct display *, unsigned int);
extern int XSync(struct display *, int);
__attribute__((noinline))
unsigned int image(struct display *d, unsigned int w, void *gc, char *data) {
struct ximage *img;
unsigned int root;
unsigned int pm;
root = ((struct screen *)d->screens)->root;
img = XCreateImage(d, 0, 1, 0, 0, data, 640, 480, 32, 80);
img->byte_order = 0;
img->bytes_per_line = 80;
img->bits_per_pixel = 1;
img->f.create_image = 0;
pm = XCreatePixmap(d, w, 640, 480, 1);
XPutImage(d, pm, gc, img, 0, 0, 0, 0, 640, 32);
img->data = data;
XMapWindow(d, w);
XSync(d, 0);
return root + pm;
}
__attribute__((noinline))
unsigned int window(struct display *d) {
unsigned int root = ((struct screen *)d->screens)->root;
return XCreateSimpleWindow(d, root, 20, 20, 640, 480, 0, 0, 0);
}
EOF
TMS_CLANG="$TMS_CLANG" TMS_OPT="$TMS_OPT" TMS_BITADDR="$TMS_BITADDR" \
"$ROOT/scripts/tms-clang" -O2 -o "$TMP/image.ll" "$TMP/image.c"
"$TMS_LLC" -march=tms34010 -O0 -filetype=asm \
"$TMP/image.ll" -o "$TMP/image.s"
echo " fnptr.s: $(grep -c . "$TMP/fnptr.s") lines, image.s: $(grep -c . "$TMP/image.s") lines"
# Our llc output has `;` end-of-line comments. Strip them so a comment cannot
# satisfy a check; TI asm has none.
for f in "$TMP/fnptr.s" "$TMP/image.s"; do
sed 's/;.*//' "$f" > "$f.code"
done
# body FILE FUNC: from `_func:` to the next function label or section directive.
body() {
awk -v want="_$2:" '
$1 == want { infn = 1; next }
infn {
if ($0 ~ /^[[:space:]]*\.(globl|sect|text|bss|data|end|file)/) exit
print
}
' "$1"
}
require() {
_file=$1
_pat=$2
_what=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_what"
return 0
fi
echo "M4 diff: FAIL: missing $_what" >&2
echo " pattern: $_pat" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
require_last_rets2() {
if awk '
/^[[:space:]]*RETS([[:space:]]|$)/ { last = $0; seen = 1 }
END {
if (!seen)
exit 1
exit (last ~ /^[[:space:]]*RETS[[:space:]]+2[[:space:]]*$/) ? 0 : 1
}
' "$1"; then
echo " ok $2"
return 0
fi
echo "M4 diff: FAIL: $2 (last RETS is not RETS 2)" >&2
sed 's/^/ /' "$1" >&2
exit 1
}
# call_info FILE FUNC CALLEE: one line
# "N PUSHES OLDSTK LASTDISP TOKENS..." for the first call to CALLEE in the
# function body. Each TOKENS element is a resolved decimal constant for a
# push whose source register last held a constant, or X for anything else.
# LASTDISP is 1 when the final push's source is the first-argument slot: the
# TI golden's `*FP(-32)`, or our register loaded from the ADDI -32 slot.
call_info() {
body "$1" "$2" | awk -v want="$3" '
function isreg(x) { return x ~ /^A([0-9]|1[0-5])$/ }
function trim(x) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", x); return x }
function unknown(r) { has[r] = 0 }
function operands(line, a, n, i) {
n = split(line, a, ",")
for (i = 1; i <= n; i++)
a[i] = trim(a[i])
return n
}
/^[[:space:]]*MOVE[[:space:]]/ {
line = $0
sub(/^[[:space:]]*MOVE[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 3 && a[2] ~ /^\*(STK|A14)\+$/ && a[3] == "1") {
npush++
if (isreg(a[1]) && has[a[1]])
consts = consts " " val[a[1]]
else
consts = consts " X"
if (a[1] == "*FP(-32)" ||
(isreg(a[1]) && hasdisp[a[1]]))
lastdisp = 1
else
lastdisp = 0
next
}
if (n >= 3 && a[2] ~ /^-\*SP$/ && (a[1] == "STK" || a[1] == "A14")) {
oldstk = 1
next
}
if (n >= 2 && isreg(a[2])) {
dsrc = 0
if (a[1] == "*FP(-32)")
dsrc = 1
else if (a[1] ~ /^\*A[0-9]+$/ && firstaddr[substr(a[1], 2)])
dsrc = 1
else if (isreg(a[1]) && hasdisp[a[1]])
dsrc = 1
if (a[1] ~ /^-?[0-9]+$/) {
val[a[2]] = a[1]; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (isreg(a[1]) && has[a[1]]) {
val[a[2]] = val[a[1]]; has[a[2]] = 1; hasdisp[a[2]] = dsrc
} else {
has[a[2]] = 0; hasdisp[a[2]] = dsrc
}
}
next
}
/^[[:space:]]*(MOVI|MOVK)[[:space:]]/ {
line = $0
sub(/^[[:space:]]*(MOVI|MOVK)[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] ~ /^-?[0-9]+$/) {
val[a[2]] = a[1]; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*CLR[[:space:]]/ {
line = $0
sub(/^[[:space:]]*CLR[[:space:]]+/, "", line)
if (isreg(trim(line))) {
val[trim(line)] = 0; has[trim(line)] = 1
hasdisp[trim(line)] = 0
}
next
}
/^[[:space:]]*SUB[[:space:]]/ {
line = $0
sub(/^[[:space:]]*SUB[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] == a[2]) {
val[a[2]] = 0; has[a[2]] = 1; hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*(ADDK|SUBK)[[:space:]]/ {
op = $1
line = $0
sub(/^[[:space:]]*(ADDK|SUBK)[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2]) && a[1] ~ /^-?[0-9]+$/ && has[a[2]]) {
val[a[2]] = val[a[2]] + (op == "ADDK" ? a[1] : -a[1])
hasdisp[a[2]] = 0
} else if (n >= 2 && isreg(a[2])) {
unknown(a[2])
}
next
}
/^[[:space:]]*ADDI[[:space:]]/ {
line = $0
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
n = operands(line, a)
if (n >= 2 && isreg(a[2])) {
unknown(a[2])
if (a[1] == "-32")
firstaddr[a[2]] = 1
else
firstaddr[a[2]] = 0
}
next
}
/^[[:space:]]*CALLA[[:space:]]/ {
callee = $2
sub(/^_/, "", callee)
if (callee == want) {
print npush, oldstk, lastdisp, consts
exit
}
npush = 0; oldstk = 0; lastdisp = 0; consts = ""
next
}
'
}
# check_call FILE FUNC CALLEE NPUSH "CONST PREFIX"
check_call() {
_file=$1
_func=$2
_callee=$3
_wantn=$4
_wantc=$5
_info=$(call_info "$_file" "$_func" "$_callee")
[ -n "$_info" ] || fail "$_func has no CALLA $_callee"
set -- $_info
_n=$1
_stk=$2
_disp=$3
shift 3
_tokens="$*"
[ "$_n" = "$_wantn" ] \
|| fail "$_func/$_callee has $_n argument pushes, want $_wantn"
[ "$_stk" = 1 ] || fail "$_func/$_callee does not save old STK"
case "$_tokens" in
"$_wantc "*) ;;
*)
echo "M4 diff: FAIL: $_func/$_callee leading constants are '$_tokens', want '$_wantc ...'" >&2
exit 1
;;
esac
_last=${_tokens##* }
[ "$_last" = X ] \
|| fail "$_func/$_callee last push looks like a constant, not the Display argument"
[ "$_disp" = 1 ] \
|| fail "$_func/$_callee last push is not the Display argument (first-argument slot)"
echo " ok $_func/$_callee: $_n pushes, old STK, leading [$_wantc], Display last"
}
# check_fnptr FILE WHO TESTFUNC
check_fnptr() {
_f=$1
_who=$2
_test=$3
body "$_f" apply > "$TMP/apply.body"
require "$TMP/apply.body" '^[[:space:]]*CALL(R)?[[:space:]]+A[0-9]+' \
"$_who: apply calls through an A register"
if grep -Eq '^[[:space:]]*CALLA[[:space:]]' "$TMP/apply.body"; then
sed 's/^/ /' "$TMP/apply.body" >&2
fail "$_who: apply still contains a direct CALLA"
fi
require "$TMP/apply.body" '^[[:space:]]*MOVE[[:space:]]+(STK|A14),[[:space:]]*-\*SP' \
"$_who: apply saves the old STK"
require "$TMP/apply.body" \
'^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*(STK|A14)\+,[[:space:]]*1' \
"$_who: apply pushes the callback argument"
require_last_rets2 "$TMP/apply.body" "$_who: apply ends with RETS 2"
body "$_f" "$_test" > "$TMP/$_test.body"
_calls=$(grep -Ec '^[[:space:]]*CALLA[[:space:]]+_apply' "$TMP/$_test.body" || true)
[ "$_calls" -ge 3 ] \
|| { sed 's/^/ /' "$TMP/$_test.body" >&2; fail "$_who: $_test makes $_calls direct CALLA _apply calls, want at least 3"; }
echo " ok $_who: $_test makes $_calls direct CALLA _apply calls (want >= 3)"
}
# has_addi_access FILE KIND DISP: an ADDI DISP,REG immediately followed by a
# MOVE that uses *REG as a load (KIND=load) or store destination (KIND=store).
has_addi_access() {
awk -v kind="$2" -v disp="$3" '
function disp_ok(d) {
if (d == disp) return 1
if (disp == "1152" && (d == "0x480" || d == "480h")) return 1
if (disp == "64" && (d == "0x40" || d == "40h")) return 1
if (disp == "128" && (d == "0x80" || d == "80h")) return 1
if (disp == "160" && (d == "0xA0" || d == "0a0h")) return 1
if (disp == "320" && (d == "0x140" || d == "140h")) return 1
if (disp == "352" && (d == "0x160" || d == "160h")) return 1
if (disp == "512" && (d == "0x200" || d == "200h")) return 1
return 0
}
/^[[:space:]]*ADDI[[:space:]]/ {
line = $0
sub(/^[[:space:]]*ADDI[[:space:]]+/, "", line)
split(line, f, /[[:space:]]*,[[:space:]]*/)
d = f[1]
reg = f[2]
if (!disp_ok(d))
next
if (getline nxt <= 0)
next
while (nxt ~ /^[[:space:]]*$/) {
if (getline nxt <= 0)
next
}
if (kind == "load" &&
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+\\*" reg "([,\\(]|[[:space:]]|$)"))
found = 1
if (kind == "store" &&
nxt ~ ("^[[:space:]]*MOVE[[:space:]]+[^,]+, ?\\*" reg "([,\\(]|[[:space:]]|$)"))
found = 1
}
END { exit found ? 0 : 1 }
' "$1"
}
check_offsets() {
_f=$1
_who=$2
_side=$3
body "$_f" image > "$TMP/image.body"
if [ "$_side" = ti ]; then
require "$TMP/image.body" \
'^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((1152|0x480|480h)\),' \
"$_who: Display.screens is loaded at bit 0x480 (byte 0x90)"
require "$TMP/image.body" \
'^[[:space:]]*MOVE[[:space:]]+\*(A[0-9]+|FP|SP)\((64|0x40|40h)\),[[:space:]]*\*(A[0-9]+|FP|SP)\([0-9]+\)' \
"$_who: Screen.root is loaded at bit 0x40 (byte 0x8)"
for pair in 128:data 160:byte_order 320:bytes_per_line 352:bits_per_pixel 512:f; do
d=${pair%%:*}
name=${pair##*:}
case "$d" in
128) pat='(128|0x80|80h)' ;;
160) pat='(160|0xA0|0a0h)' ;;
320) pat='(320|0x140|140h)' ;;
352) pat='(352|0x160|160h)' ;;
512) pat='(512|0x200|200h)' ;;
esac
require "$TMP/image.body" \
"^[[:space:]]*MOVE[[:space:]]+.*\\*A[0-9]+\\($pat\\)," \
"$_who: XImage.$name is at bit $d"
done
return 0
fi
for pair in load:1152:screens load:64:root store:128:data store:160:byte_order \
store:320:bytes_per_line store:352:bits_per_pixel store:512:f; do
kind=${pair%%:*}
rest=${pair#*:}
d=${rest%%:*}
name=${rest##*:}
if has_addi_access "$TMP/image.body" "$kind" "$d"; then
echo " ok $_who: XImage/Display/Screen $name access at bit $d (ADDI + $kind)"
else
sed 's/^/ /' "$TMP/image.body" >&2
fail "$_who: no $kind through bit displacement $d for $name"
fi
done
}
# check_image FILE WHO TI|ours
check_image() {
_f=$1
_who=$2
_side=$3
check_call "$_f" image XCreateImage 10 "80 32 480 640"
check_call "$_f" image XCreatePixmap 5 "1 480 640"
check_call "$_f" image XPutImage 10 "32 640"
check_call "$_f" window XCreateSimpleWindow 9 "0 0 0 480 640"
body "$_f" image > "$TMP/rets.body"
require_last_rets2 "$TMP/rets.body" "$_who: image ends with RETS 2"
body "$_f" window > "$TMP/rets2.body"
require_last_rets2 "$TMP/rets2.body" "$_who: window ends with RETS 2"
check_offsets "$_f" "$_who" "$_side"
}
echo "== ABI shape: TI fnptr golden"
check_fnptr "$TI_FNPTR" "TI" test
echo "== ABI shape: our fnptr"
check_fnptr "$TMP/fnptr.s.code" "ours" fnptr_test
echo "== ABI shape: TI image golden"
check_image "$TI_IMAGE" "TI" ti
echo "== ABI shape: our image"
check_image "$TMP/image.s.code" "ours" ours
if [ "${M4_DIFF_POSITIVE_ONLY:-0}" = 1 ]; then
echo "M4 diff: PASS"
exit 0
fi
echo "== negative controls"
# 1. Remove the indirect call from the fnptr golden: apply must then fail.
sed 's/^[[:space:]]*CALL[[:space:]].*/ CALLA _dec/' "$TI_FNPTR" \
> "$TMP/fnptr-no-indirect.s"
grep -q 'CALLA _dec' "$TMP/fnptr-no-indirect.s" \
|| fail "could not construct the no-indirect-call fnptr golden"
if M4_DIFF_POSITIVE_ONLY=1 TI_FNPTR="$TMP/fnptr-no-indirect.s" \
"$0" > "$TMP/neg-fnptr.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-fnptr.out" >&2
fail "check accepted a fnptr golden without the indirect call"
fi
echo " ok negative control: fnptr without the indirect call rejected"
grep 'M4 diff: FAIL' "$TMP/neg-fnptr.out" | head -1 | sed 's/^/ /'
# 2. Reverse the ten XPutImage argument pushes: Display must no longer be the
# last push and the leading constants must change.
awk '
{ lines[NR] = $0 }
/^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*STK\+,[[:space:]]*1[[:space:]]*$/ {
npush++
pos[npush] = NR
push[npush] = $0
next
}
/^[[:space:]]*CALLA[[:space:]]+_XPutImage/ {
for (i = 1; i <= npush; i++)
lines[pos[i]] = push[npush - i + 1]
npush = 0
next
}
/^[[:space:]]*CALLA[[:space:]]/ { npush = 0; next }
END { for (i = 1; i <= NR; i++) print lines[i] }
' "$TI_IMAGE" > "$TMP/image-reversed.s"
if cmp -s "$TI_IMAGE" "$TMP/image-reversed.s"; then
fail "reversed-argument mutation did not change the image golden"
fi
if M4_DIFF_POSITIVE_ONLY=1 TI_IMAGE="$TMP/image-reversed.s" \
"$0" > "$TMP/neg-image.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-image.out" >&2
fail "check accepted the reversed XPutImage argument order"
fi
echo " ok negative control: reversed XPutImage arguments rejected"
grep 'M4 diff: FAIL' "$TMP/neg-image.out" | head -1 | sed 's/^/ /'
# 3. Swap only the last two XPutImage pushes (Display and drawable). The
# leading constants and the non-constant last-token rule still hold, so
# only the first-argument (Display) check can reject this.
awk '
{ lines[NR] = $0 }
/^[[:space:]]*MOVE[[:space:]]+[^,]+,[[:space:]]*\*STK\+,[[:space:]]*1[[:space:]]*$/ {
npush++
pos[npush] = NR
next
}
/^[[:space:]]*CALLA[[:space:]]+_XPutImage/ {
if (npush >= 2) {
tmp = lines[pos[npush]]
lines[pos[npush]] = lines[pos[npush - 1]]
lines[pos[npush - 1]] = tmp
}
npush = 0
next
}
/^[[:space:]]*CALLA[[:space:]]/ { npush = 0; next }
END { for (i = 1; i <= NR; i++) print lines[i] }
' "$TI_IMAGE" > "$TMP/image-swapped.s"
if cmp -s "$TI_IMAGE" "$TMP/image-swapped.s"; then
fail "Display/drawable swap mutation did not change the image golden"
fi
if M4_DIFF_POSITIVE_ONLY=1 TI_IMAGE="$TMP/image-swapped.s" \
"$0" > "$TMP/neg-swap.out" 2>&1; then
sed 's/^/ /' "$TMP/neg-swap.out" >&2
fail "check accepted the Display/drawable push swap"
fi
echo " ok negative control: Display swapped with the drawable rejected"
grep 'M4 diff: FAIL' "$TMP/neg-swap.out" | head -1 | sed 's/^/ /'
echo "M4 diff: PASS"
+33
View File
@@ -0,0 +1,33 @@
/* K&R source for the M4 differential function-pointer golden.
*
* The 1987 TI compiler (GSP C Compiler 2.01) does not accept a typedef for a
* function pointer, nor the volatile qualifier on the local, so the callback
* is spelled as a plain `int (*f)()` parameter and the local pointer is
* unqualified. `apply` is the call-through-pointer proof: the caller pushes
* `x` and the callback, saves STK, and calls through the loaded pointer (TI
* `CALL A7`, ours `CALL A?`). The direct calls to `apply` from `test()` stay
* `CALLA _apply`.
*/
int inc(x) int x; { return x + 1; }
int dec(x) int x; { return x - 1; }
int apply(f, x)
int (*f)();
int x;
{
return (*f)(x);
}
int (*g_dec)() = dec;
int test()
{
int (*vf)();
int a;
int b;
vf = inc;
a = apply(inc, 41);
b = apply(vf, 41);
return apply(g_dec, 43) + a + b;
}
+95
View File
@@ -0,0 +1,95 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _inc
.file "TI-FNPTR.c"
******************************************************
* FUNCTION DEF : _inc
******************************************************
_inc:
MMTM SP,A7,FP
MOVE STK,FP
MOVE *FP(-32),A7,1
ADDK 1,A7
MOVE A7,A8
EPI0_1:
MOVE *SP(96),STK,1
MMFM SP,A7,FP
RETS 2
.globl _dec
******************************************************
* FUNCTION DEF : _dec
******************************************************
_dec:
MMTM SP,A7,FP
MOVE STK,FP
MOVE *FP(-32),A7,1
SUBK 1,A7
MOVE A7,A8
EPI0_2:
MOVE *SP(96),STK,1
MMFM SP,A7,FP
RETS 2
.globl _apply
******************************************************
* FUNCTION DEF : _apply
******************************************************
_apply:
MMTM SP,A7,FP
MOVE STK,FP
MOVE STK,-*SP,1
MOVE *FP(-64),*STK+,1
MOVE *FP(-32),A7,1
CALL A7
EPI0_3:
MOVE *SP(96),STK,1
MMFM SP,A7,FP
RETS 2
.sect ".cinit"
.word IS1
.long _g_dec+0
.field _dec+0,32
IS1 .set 2
.text
.globl _g_dec
.bss _g_dec,32,16
.globl _test
******************************************************
* FUNCTION DEF : _test
******************************************************
_test:
MMTM SP,A0,A5,A7,A12,FP
MOVE STK,FP
ADDI 96,STK
MOVI _inc,A0
MOVE A0,*FP,1
MOVE STK,-*SP,1
MOVI 41,A12
MOVE A12,*STK+,1
MOVE A0,*STK+,1
CALLA _apply
MOVE A8,*FP(32),1
MOVE STK,-*SP,1
MOVE A12,*STK+,1
MOVE *FP(0),*STK+,1
CALLA _apply
MOVE A8,*FP(64),1
MOVE STK,-*SP,1
ADDK 2,A12
MOVE A12,*STK+,1
MOVE @_g_dec,*STK+,1
CALLA _apply
MOVE A8,A7
MOVE *FP(32),A5,1
ADD A5,A7
MOVE *FP(64),A5,1
ADD A5,A7
MOVE A7,A8
EPI0_4:
ADDI -96,STK
MMFM SP,A0,A5,A7,A12,FP
RETS 0
.end
+86
View File
@@ -0,0 +1,86 @@
/* K&R source for the M4 differential Xlib-image golden.
*
* It models the X11R4 32-bit XImage/Display/Screen layout and the ten-argument
* XCreateImage/XPutImage, five-argument XCreatePixmap and nine-argument
* XCreateSimpleWindow calls. The 1987 TI compiler packs struct members and
* its &-of-member is already a bit address, so the golden carries the ROM
* bit displacements directly: Display.screens 1152 (byte 0x90), Screen.root
* 64 (byte 0x8), and XImage data/byte_order/bytes_per_line/bits_per_pixel/f
* 128/160/320/352/512 (bytes 16/20/40/44/64).
*/
struct display {
char pad[0x90];
void *screens;
};
struct screen {
char pad[0x8];
unsigned int root;
};
struct ximage_funcs {
void *create_image;
void *destroy_image;
void *get_pixel;
void *put_pixel;
void *sub_image;
void *add_pixel;
};
struct ximage {
int width, height;
int xoffset;
int format;
char *data;
int byte_order;
int bitmap_unit;
int bitmap_bit_order;
int bitmap_pad;
int depth;
int bytes_per_line;
int bits_per_pixel;
unsigned long red_mask, green_mask, blue_mask;
void *obdata;
struct ximage_funcs f;
};
extern struct display *XOpenDisplay();
extern int XCreateSimpleWindow();
extern unsigned int XCreatePixmap();
extern struct ximage *XCreateImage();
extern int XPutImage();
extern int XMapWindow();
extern int XSync();
int image(d, w, gc, data)
struct display *d;
unsigned int w;
void *gc;
char *data;
{
struct ximage *img;
unsigned int root;
unsigned int pm;
root = ((struct screen *)d->screens)->root;
img = XCreateImage(d, 0, 1, 0, 0, data, 640, 480, 32, 80);
img->byte_order = 0;
img->bytes_per_line = 80;
img->bits_per_pixel = 1;
img->f.create_image = 0;
pm = XCreatePixmap(d, w, 640, 480, 1);
XPutImage(d, pm, gc, img, 0, 0, 0, 0, 640, 32);
img->data = data;
XMapWindow(d, w);
XSync(d, 0);
return root + pm;
}
int window(d)
struct display *d;
{
unsigned int root;
root = ((struct screen *)d->screens)->root;
return XCreateSimpleWindow(d, root, 20, 20, 640, 480, 0, 0, 0);
}
+127
View File
@@ -0,0 +1,127 @@
*******************************************************
* GSP C COMPILER, Version 2.01, 87.300 *
*******************************************************
FP .set A13
STK .set A14
.globl _XOpenDisplay
.globl _XCreateSimpleWindow
.globl _XCreatePixmap
.globl _XCreateImage
.globl _XPutImage
.globl _XMapWindow
.globl _XSync
.globl _image
.file "TI-IMAGE.c"
******************************************************
* FUNCTION DEF : _image
******************************************************
_image:
MMTM SP,A5,A7,A12,FP
MOVE STK,FP
ADDI 96,STK
MOVE *FP(-32),A7,1
MOVE *A7(1152),A7,1
MOVE *A7(64),*FP(32),1
MOVE STK,-*SP,1
MOVI 80,A7
MOVE A7,*STK+,1
MOVK 32,A7
MOVE A7,*STK+,1
MOVI 480,A7
MOVE A7,*STK+,1
MOVI 640,A7
MOVE A7,*STK+,1
MOVE *FP(-128),*STK+,1
CLR A7
MOVE A7,*STK+,1
MOVE A7,*STK+,1
ADDK 1,A7
MOVE A7,*STK+,1
SUB A7,A7
MOVE A7,*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XCreateImage
MOVE A8,*FP,1
MOVE *FP,A7,1
CLR A12
MOVE A12,*A7(160),1
MOVI 80,A12
MOVE A12,*A7(320),1
MOVK 1,A12
MOVE A12,*A7(352),1
SUB A12,A12
MOVE A12,*A7(512),1
MOVE STK,-*SP,1
ADDK 1,A12
MOVE A12,*STK+,1
MOVI 480,A12
MOVE A12,*STK+,1
MOVI 640,A12
MOVE A12,*STK+,1
MOVE *FP(-64),*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XCreatePixmap
MOVE A8,*FP(64),1
MOVE STK,-*SP,1
MOVK 32,A12
MOVE A12,*STK+,1
MOVI 640,A12
MOVE A12,*STK+,1
CLR A12
MOVE A12,*STK+,1
MOVE A12,*STK+,1
MOVE A12,*STK+,1
MOVE A12,*STK+,1
MOVE A7,*STK+,1
MOVE *FP(-96),*STK+,1
MOVE *FP(64),*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XPutImage
MOVE *FP(-128),*A7(128),1
MOVE STK,-*SP,1
MOVE *FP(-64),*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XMapWindow
MOVE STK,-*SP,1
MOVE A12,*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XSync
MOVE *FP(32),A7,1
MOVE *FP(64),A5,1
ADD A5,A7
MOVE A7,A8
EPI0_1:
MOVE *SP(160),STK,1
MMFM SP,A5,A7,A12,FP
RETS 2
.globl _window
******************************************************
* FUNCTION DEF : _window
******************************************************
_window:
MMTM SP,A7,FP
MOVE STK,FP
ADDK 32,STK
MOVE *FP(-32),A7,1
MOVE *A7(1152),A7,1
MOVE *A7(64),*FP(0),1
MOVE STK,-*SP,1
CLR A7
MOVE A7,*STK+,1
MOVE A7,*STK+,1
MOVE A7,*STK+,1
MOVI 480,A7
MOVE A7,*STK+,1
MOVI 640,A7
MOVE A7,*STK+,1
MOVK 20,A7
MOVE A7,*STK+,1
MOVE A7,*STK+,1
MOVE *FP(0),*STK+,1
MOVE *FP(-32),*STK+,1
CALLA _XCreateSimpleWindow
EPI0_2:
MOVE *SP(96),STK,1
MMFM SP,A7,FP
RETS 2
.end
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# M4 dis34 gate: rebuild the image client through the normal GSPA/GSPLNK path,
# then verify the ROM import stubs, the direct/indirect call encodings,
# XCreateSimpleWindow's nine-argument order, the XSetErrorHandler callback,
# SETF/RETS/old-STK, and the checker .bss.
set -eu
ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
DIR=$ROOT/scripts/m4
WORK=${M4_XSHOW_WORK:-$ROOT/build/m4-xshow}
M4_SKIP_MAME=1 \
M4_SOURCES="xshow xgfix" \
M4_ENTRY="$DIR/xshow-entry.asm" \
M4_CLANG_OPT=-O2 \
M4_ROM_SYMBOLS=1 \
M4_WORK="$WORK" \
"$ROOT/scripts/m4/run.sh"
M4_WORK="$WORK" exec "$ROOT/scripts/m4/dis-roundtrip.sh"
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more