22 Commits
m0 .. m2
Author SHA1 Message Date
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
43 changed files with 3333 additions and 89 deletions

No files matched your search

+268 -59
View File
@@ -3,56 +3,222 @@
A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of
upstream LLVM.
**Status: M0.** Clang understands the `tms34010-unknown-none` target, the TI
data model, and bit-addressed pointers. There is **no machine-code backend
yet**: M0 stops at LLVM IR. The backend, TI ABI, ROM calls, Xlib and real
clients are M1–M5 and are separate plans (see the design spec).
**Status: M2.** `llc -march=tms34010` emits GSPA-compatible assembly using the
TI ABI: integer/pointer arguments are pushed on the program stack, the result is
returned in `A8`, frames use `A13`/`A14`/`A15`, and functions end in `RETS 2`
(`RETS 0` without arguments). M2 can call a ROM entry point (`strlen`) and emit
initialized `.data` and string literals. It is still **assembly-only**: no
object writer, no `clang -c`, and no native COFF; GSPA/GSPLNK under DOSBox
produce the COFF. Control flow, floats, structs, varargs and function pointers
remain M3+.
- 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)
- 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)
- Spike: [`spike/REPORT-23.md`](spike/REPORT-23.md) (current, LLVM 23.1.5);
[`spike/REPORT.md`](spike/REPORT.md) (historical, 19.1.7)
## Repositories
- **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
target and clang bit-address changes.
(branch `m2`) — the full buildable LLVM 23.1.5 tree with the TMS34010
target, clang bit-address changes and the M2 backend. 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>
— plans/specs, VT1200 integration tooling, `tms-bitaddr`, `tms-clang`,
symbols and the spike.
(branch `m2`) — plans/specs, VT1200 integration tooling, `tms-bitaddr`,
`tms-clang`, symbols, the M2 harness and the spike. Branch `m1` is the
reproducible M1 snapshot, including the retired M1 harness.
The compiler fork is the canonical source for LLVM changes. `patches/llvm`
here is a lightweight mirror used by `scripts/build-llvm.sh`, so the project
can be built without cloning 3 GB of LLVM; keep it in sync with the fork's
`tms34010` branch.
The compiler fork is the canonical source for all target code. `patches/llvm`
here is a **frozen M0 mirror** used by the default upstream+patches build; M2
does not change it.
## What M0 provides
## What M2 provides
- `clang --target=tms34010-unknown-none`: 32-bit bit-addressed pointers,
8-bit signed `char`, 16-bit `short`, 32-bit `int`/`long`, packed structs,
`sizeof(struct { char; int; void*; }) == 9`.
- Pointer arithmetic scaled by 8; pointer difference divided by 8
(`char *` → `/8`, `int *` → `/32`), while `(int)(p+1) - (int)p` stays an
integer subtraction.
- `tools/tms-bitaddr`: GEP `--scale` and late `--normalize` passes, including
constant-expression GEPs.
- `scripts/tms-clang`: the M0 pipeline
`clang -O0` → `tms-bitaddr --scale` → `opt -O2` → `tms-bitaddr --normalize`.
- Lit tests for the triple, data model and pointer difference; an integration
test over a real Xlib client IR.
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) |
|---|---|---|
| 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 M2 gates
These are the documented commands; all must 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`; later milestones extend and validate the table and
add the Xlib call sequence. Do not add Xlib dependencies to M2.
## 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 are
M3.
- **Narrow ABI types:** `i8`/`i16` parameters, `i64`, and narrow returns; M3
adds the widen/truncate rules. Narrow loads/stores inside a function remain
supported.
- **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.
## Repository layout
```text
build/llvm-src/ local fork (ignored; created by build-llvm.sh or manually)
build/llvm-build/ Ninja build of the fork (ignored)
build/llvm-clean*/ clean CI clone/build (ignored)
patches/llvm/ the complete fork delta against llvmorg-23.1.5
build/llvm-clean/ compiler fork checkout used for M2 (ignored)
build/llvm-clean-build/ its Ninja build tree (ignored)
build/llvm-src/ M0 working fork (ignored)
patches/llvm/ frozen M0 patch series against llvmorg-23.1.5
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/tests/m2/ M2 gates (run.sh, dis-roundtrip.sh)
scripts/tests/m2/diff/ TI golden asm and the differential ABI 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/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
spike/ measurement corpus and reports
symbols/ per-ROM import tables (v2.2 subset)
@@ -63,10 +229,15 @@ docs/ design and plans
## Prerequisites
`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 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 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
sudo apt-get install -y cmake ninja-build ccache
sudo apt-get install -y cmake ninja-build ccache dosbox mtools
```
## Fetch the VT1200 assets
@@ -78,54 +249,92 @@ Xlib call sequence, `as34`, `dis34`, SDKs, manuals). It is not vendored:
./scripts/fetch-vt1200.sh # -> vendor/vt1200, pinned 17dfd24...
```
## Build the pinned fork
## Build the compiler
### Compiler fork (M2, the normal path)
The fork checkout already exists at `build/llvm-clean` on `m2`. Build it in
place:
```sh
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=m2 ./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=m2 ./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
./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
`clang`/`opt` with X86 (host) and TMS34010. A first build takes roughly
20–40 minutes; rebuilds are incremental.
This mode clones `llvmorg-23.1.5`, applies `patches/llvm/*.patch`, and builds
`build/llvm-clean-build`. It owns and resets only a plain `llvm-project` clone:
if `build/llvm-clean` has some other origin (for example the M2 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
its build tree. `scripts/export-patches.sh` regenerates the committed patch
series with `git diff` (including new files via `git add -N`).
`scripts/export-patches.sh` regenerates the committed M0 patch series with
`git diff` (including new files via `git add -N`); it is not used for M1/M2.
## Build the M0 tool and run the tests
## Build the M0 support tools
The M2 pipeline uses `tms-bitaddr` and `tms-clang`:
```sh
LLVM_CONFIG=$PWD/build/llvm-clean-build/bin/llvm-config \
tools/tms-bitaddr/build.sh
```
M0 regression test (optional in M2):
```sh
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/tests/m0/run.sh
# -> 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
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
assembly or an object file; `llc` support is M1.
The resulting function loads its arguments at `FP(-32)` and `FP(-64)`, returns
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.
- The TI ABI: A8 return, A13/A14/A15 dual stack, `RETS 2`, `FS1 = 32`.
- ROM import symbols beyond the subset in `symbols/`.
- Xlib headers/clients on the target, `memcpy`/`byval`/varargs/float/bitfield
lowering, native COFF.
These are M1–M5 in the design spec, written as separate plans because their
tasks depend on what M0/M1 measure.
M3 adds branches/control flow, the Xlib call sequence and a wider validated ROM
symbol table. M4 adds the real client plus structs and function pointers. M5
adds the native COFF path, floats, varargs, bitfields and `long long`; 2ine
r5.01 is a later speed-up for the GSPA/GSPLNK step.
@@ -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
@@ -345,12 +345,15 @@ non-constant pointer expression by 8×.
`harness/sdk-scratch/dis2as34.py`). Emit section sizes and labels in bits.
- Linker command files are CRLF; `SECTIONS` origins are bit addresses.
- **Constant GEPs** become `symbol + bit_addend` (for example
`inttoptr(add(ptrtoint(@.str), 16))`). Before M1 relies on it, verify that
GSPA/gsplnk can carry and relocate a bit addend in both a `MOVI` immediate
and a `.data` initializer. Fallbacks if they cannot: leave `symbol+addend`
entirely to link time, materialize the addend at runtime (`MOVI symbol;
ADDI addend`), or pre-resolve in the linker script. Test a string literal
with an offset, `&arr[1]`, and a global pointer initializer.
`inttoptr(add(ptrtoint(@.str), 16))`). M1 verifies the `MOVI` immediate
form with `&arr[1]`. M1 does not emit `.data`, `.rodata` or string literals
at all: any non-null initializer makes `llc` fail loudly. Verifying a
`symbol+bit_addend` relocation in a `.data` initializer, a string literal
with an offset, and a global pointer initializer is **deferred to M2**
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
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
carry bit offsets and are not scaled twice (§7.3.1). The spike only reached
`opt`; this is a new gate.
- **Constant-GEP relocation test**: `symbol + bit_addend` in `MOVI` and in
`.data`, plus `&arr[1]` and a global pointer initializer.
- **Constant-GEP relocation test**: M1 covers `symbol + bit_addend` in a
`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
`dwt/loader/loader.lua`, check markers and frame. This is the CI gate.
- Real unit: `dwt/loader/hostload.py`, rdebug. Periodic/optional confirmation,
+80 -16
View File
@@ -1,28 +1,92 @@
#!/bin/sh
# Build the pinned LLVM 23.1.5 fork from a clean clone and the committed
# patch series. Intended for CI and for verifying that patches/llvm is the
# complete fork delta.
# Build the TMS34010 LLVM compiler.
#
# 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
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
mkdir -p build
SRC=${LLVM_SRC:-build/llvm-clean}
BUILD=${LLVM_BUILD:-build/llvm-clean-build}
REPO=${LLVM_REPO:-}
BRANCH=${LLVM_BRANCH:-}
TAG=llvmorg-23.1.5
if [ ! -d "$SRC/.git" ]; then
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
origin_url() {
git -C "$1" config --get remote.origin.url 2>/dev/null || true
}
PATCHROOT=$(pwd)/patches/llvm
for p in "$PATCHROOT"/*.patch; do
[ -e "$p" ] || continue
git -C "$SRC" apply "$p"
done
if [ -n "$REPO" ] || [ -n "$BRANCH" ]; then
echo "== compiler fork mode: $SRC${BRANCH:+ (branch $BRANCH)}"
if [ ! -e "$SRC/.git" ]; then
[ -n "$REPO" ] || {
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 \
-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_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=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"
+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" "$@"
+5 -3
View File
@@ -1,10 +1,11 @@
#!/bin/sh
# tms-clang -- M0 pipeline: clang IR, scale GEPs, optimize, normalize GEPs.
# tms-clang -- M1 IR pipeline: clang IR, scale GEPs, optimize, normalize GEPs.
#
# Usage: tms-clang [-O0|-O2] [-I dir] -o OUT.ll IN.c
#
# -O2 (the default) scales, optimizes, then normalizes; -O0 skips the optimizer.
# No machine code is produced in M0.
# The output is normalized IR for llc -march=tms34010; this wrapper does not
# invoke llc itself.
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
@@ -42,7 +43,8 @@ trap 'rm -rf "$TMP"' EXIT HUP INT TERM
# shellcheck disable=SC2086
"$TMS_CLANG" --target=tms34010-unknown-none -O0 -Xclang -disable-O0-optnone \
-S -emit-llvm -ffreestanding -nostdinc $INCLUDES -o "$TMP/step1.ll" "$IN"
-S -emit-llvm -ffreestanding -fno-builtin -nostdinc $INCLUDES \
-o "$TMP/step1.ll" "$IN"
"$TMS_BITADDR" --scale "$TMP/step1.ll" "$TMP/scaled.ll" 2>/dev/null
+10 -3
View File
@@ -4,9 +4,16 @@ One file per ROM revision. Addresses are **bit** addresses and are not assumed
stable across firmware revisions; v2.2 is the first target.
`vt1200-v22.syms` contains only the entries already proven by the hand-written
payloads in `vendor/vt1200/dwt/gui/xhello.asm` and the README. The compiler
milestone that first calls Xlib must extend it from the Ghidra project /
`ptr_constants.py` and validate each entry on MAME, then on the real unit.
payloads in `vendor/vt1200/dwt/gui/xhello.asm` and the README. **M2 consumes
only `strlen`**: `scripts/m2/gen-rom-imports.sh` emits an ABI-preserving
`jauc` stub for every entry, and the M2 harness calls `_strlen` at
`0x3FB458D0`. The other entries are recorded for later milestones and are not
yet exercised by compiled C.
M3 owns the wider table: it extends the entries from the Ghidra project /
`ptr_constants.py`, adds the Xlib call sequence, and validates each entry on
MAME, then on the real unit. Do not assume an address here is validated just
because it is listed.
The build resolves these as absolute symbols (or ABI-preserving stubs). Do not
bake them into the compiler: keep them in a per-ROM linker script.
+80
View File
@@ -212,6 +212,78 @@ static void collect_const_geps(Value *V, std::vector<ConstantExpr *> &Out,
collect_const_geps(Op.get(), Out, depth + 1);
}
// --normalize lowers a GEP offset directly as a bit offset (shift 0). That is
// only correct for an i8 GEP produced by the optimizer after scaling. A
// typed GEP still carries a byte offset (DataLayout strides), and a vector GEP
// is not handled at all; treating either as bits would be an 8x address bug.
static bool check_normalize_gep(Type *ResultTy, Type *SourceTy,
std::string &Msg)
{
if (ResultTy->isVectorTy()) {
Msg = "bitscale: --normalize does not support vector GEPs";
return false;
}
if (!SourceTy->isIntegerTy(8)) {
std::string TyStr;
raw_string_ostream OS(TyStr);
SourceTy->print(OS);
Msg = "bitscale: --normalize requires i8 GEPs; found a getelementptr "
"with source element type " + OS.str();
return false;
}
return true;
}
// Collect every constant-expression GEP, including vector GEPs (the normal
// const-GEP collector skips those because it cannot rewrite them).
static void collect_all_const_geps(Value *V, std::vector<ConstantExpr *> &Out,
unsigned depth = 0)
{
if (!V || depth > 64)
return;
auto *C = dyn_cast<Constant>(V);
if (!C)
return;
if (auto *CE = dyn_cast<ConstantExpr>(C)) {
if (CE->getOpcode() == Instruction::GetElementPtr)
Out.push_back(CE);
}
for (auto &Op : C->operands())
collect_all_const_geps(Op.get(), Out, depth + 1);
}
static bool check_normalize_inputs(Module &M, std::string &Msg)
{
for (Function &F : M)
for (BasicBlock &BB : F)
for (Instruction &I : BB)
if (auto *GEP = dyn_cast<GetElementPtrInst>(&I))
if (!check_normalize_gep(GEP->getType(),
GEP->getSourceElementType(), Msg))
return false;
std::vector<ConstantExpr *> Work;
for (GlobalVariable &G : M.globals())
if (G.hasInitializer())
collect_all_const_geps(G.getInitializer(), Work);
for (Function &F : M)
for (BasicBlock &BB : F)
for (Instruction &I : BB)
for (Value *Op : I.operands())
collect_all_const_geps(Op, Work);
std::set<ConstantExpr *> Seen;
for (ConstantExpr *CE : Work) {
if (!Seen.insert(CE).second)
continue;
auto *GEP = cast<GEPOperator>(CE);
if (!check_normalize_gep(GEP->getType(), GEP->getSourceElementType(),
Msg))
return false;
}
return true;
}
int main(int argc, char **argv)
{
if (argc < 3) {
@@ -242,6 +314,14 @@ int main(int argc, char **argv)
return 1;
}
if (GepShift == 0) {
std::string Msg;
if (!check_normalize_inputs(*M, Msg)) {
errs() << Msg << "\n";
return 1;
}
}
Stats Before = collect(*M);
// Constant-expression GEPs (e.g. `"hello" + 2`) cannot be replaced by an
@@ -0,0 +1,12 @@
; RUN: not %tms-bitaddr --normalize %s 2>&1 | FileCheck %s
;
; --normalize treats GEP offsets as bits (shift 0). A typed GEP still has a
; byte offset from DataLayout, so accepting it would produce an 8x-small
; address. The tool must reject it.
%struct.S = type { i32, i32 }
define ptr @typed(ptr %p) {
%q = getelementptr %struct.S, ptr %p, i32 0, i32 1
ret ptr %q
}
; CHECK: bitscale: --normalize requires i8 GEPs
@@ -0,0 +1,8 @@
; RUN: not %tms-bitaddr --normalize %s 2>&1 | FileCheck %s
;
; Vector GEPs have no scalar lowering and must be rejected by --normalize.
define <4 x ptr> @vec(<4 x ptr> %p) {
%q = getelementptr i8, <4 x ptr> %p, i32 1
ret <4 x ptr> %q
}
; CHECK: bitscale: --normalize does not support vector GEPs
+29
View File
@@ -3,6 +3,35 @@ set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
TOOL=${TMS_BITADDR:-$ROOT/tms-bitaddr}
FILECHECK=${FILECHECK:-$ROOT/../../build/llvm-build/bin/FileCheck}
TMP=$(mktemp -d "${TMPDIR:-/tmp}/tms-bitaddr.XXXXXX")
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
"$TOOL" --scale "$ROOT/tests/scale.ll" | "$FILECHECK" "$ROOT/tests/scale.ll"
"$TOOL" --scale "$ROOT/tests/scale-typed.ll" | "$FILECHECK" "$ROOT/tests/scale-typed.ll"
"$TOOL" --scale "$ROOT/tests/const-gep.ll" | "$FILECHECK" "$ROOT/tests/const-gep.ll"
"$TOOL" --normalize "$ROOT/tests/normalize.ll" | "$FILECHECK" "$ROOT/tests/normalize.ll"
# --normalize must fail loudly for a typed (byte-offset) GEP and a vector GEP
# instead of treating their byte offsets as bits.
for test in normalize-reject normalize-vector; do
if "$TOOL" --normalize "$ROOT/tests/$test.ll" > "$TMP/$test.out" 2> "$TMP/$test.err"; then
echo "tms-bitaddr: FAIL: --normalize accepted $test.ll" >&2
exit 1
fi
if [ "$test" = normalize-reject ]; then
grep -q 'requires i8 GEPs' "$TMP/$test.err" || {
cat "$TMP/$test.err" >&2
echo "tms-bitaddr: FAIL: typed-GEP rejection message missing" >&2
exit 1
}
else
grep -q 'does not support vector GEPs' "$TMP/$test.err" || {
cat "$TMP/$test.err" >&2
echo "tms-bitaddr: FAIL: vector-GEP rejection message missing" >&2
exit 1
}
fi
done
echo "tms-bitaddr: PASS"
+11
View File
@@ -0,0 +1,11 @@
; RUN: %tms-bitaddr --scale %s | FileCheck %s
;
; --scale is allowed to see typed GEPs: it converts the DataLayout byte
; offset into a bit offset. i32 * 3 is 12 bytes, so the bit addend is 96.
define ptr @typed(ptr %p) {
%q = getelementptr i32, ptr %p, i32 3
ret ptr %q
}
; CHECK-LABEL: @typed
; CHECK: %bit.addr = add i32 %bit.base, 96
; CHECK-NOT: getelementptr