Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d4919db36 | ||
|
|
f1a604d70e | ||
|
|
3a188b35fb | ||
|
|
fa00b8740b | ||
|
|
b614e70851 | ||
|
|
0008d7be20 | ||
|
|
d4f165fa7d | ||
|
|
3f30806a6d | ||
|
|
e7ba50d7a3 | ||
|
|
469b1d870e | ||
|
|
e60a1fdee9 | ||
|
|
5e8dbffb01 | ||
|
|
c0746338b0 |
No files matched your search
@@ -3,83 +3,220 @@
|
||||
A modern C toolchain for the DEC VT1200 / TI TMS34010, built as a small fork of
|
||||
upstream LLVM.
|
||||
|
||||
**Status: M1.** `llc -march=tms34010` emits GSPA-compatible assembly for a
|
||||
small set of leaf functions, and that assembly assembles, links and runs in
|
||||
MAME. M1 is an **assembly-only, test-only** snapshot: it uses an A0–A3/A0
|
||||
register convention, not the TI ABI, and has no object writer, calls or runtime.
|
||||
**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, clang bit-address changes and the M1 backend.
|
||||
(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, the M1 harness 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 all target code. `patches/llvm`
|
||||
here is a **frozen M0 mirror** used by the default upstream+patches build; M1
|
||||
here is a **frozen M0 mirror** used by the default upstream+patches build; M2
|
||||
does not change it.
|
||||
|
||||
## What M1 provides
|
||||
## What M2 provides
|
||||
|
||||
M1 fills in the M0 target skeleton and stops at assembly:
|
||||
M2 replaces the M1 test convention with the TI ABI and stops at assembly:
|
||||
|
||||
- `llc -march=tms34010 -filetype=asm` for leaf 32-bit functions:
|
||||
integer/pointer arguments in `A0`–`A3`, result in `A0`, return via `RETS 0`.
|
||||
- `ADD` for `i32`, `MOVI` for constants, `MOVE ..., 1` (FS1) for 32-bit
|
||||
load/store, `MOVE ..., 0` (FS0) for `i16`, and sign-extending `MOVB` for
|
||||
`i8` load/store.
|
||||
- Constant global addresses as `MOVI _sym+bits`; a surviving GEP offset is a
|
||||
bit offset (`ADDI 24`, not `ADDI 192`).
|
||||
- Hard errors, not silent fallbacks, for calls (`M1 has no calls; M2 adds the
|
||||
TI ABI`), stack frames (`M1 has no stack frame`), more than four register
|
||||
arguments, unsupported signatures (varargs, `i64`/float/aggregate
|
||||
parameters, `sret`/`byval`/`inreg`/`nest`/`returned`), unsupported return
|
||||
types, and initialized globals or string literals.
|
||||
- Seventeen `llvm-lit`/FileCheck tests under
|
||||
`llvm/test/CodeGen/TMS34010/m1-*.ll` in the compiler fork, including
|
||||
negative tests that pin the loud refusals.
|
||||
- An end-to-end path: `scripts/tms-clang` IR → `llc -O0 -filetype=asm` →
|
||||
**1987 GSPA/GSPLNK 2.00 under DOSBox** → COFF → the VT1200 loader in MAME.
|
||||
The run checks `add(20, 22) == 42`, `load(&word) == 0x12345678`,
|
||||
`second() == &arr[1]` (a `symbol+32` bit relocation), `loadb` of a byte
|
||||
`0x81` returns the sign-extended `0xFFFFFF81`, and `storeb` leaves `0x81`
|
||||
in memory.
|
||||
- A `dis34` round-trip that reads the linked `.text` from the COFF and checks
|
||||
the instructions GSPA encoded at their linked bit addresses.
|
||||
- `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).
|
||||
|
||||
## M1 is not the TI ABI
|
||||
## The TI ABI
|
||||
|
||||
| | M1 test convention | TI ABI (M2) |
|
||||
| | M1 test convention (branch `m1`) | TI ABI (M2) |
|
||||
|---|---|---|
|
||||
| Integer/pointer arguments | `A0`–`A3`, four maximum | program stack (via `A14` STK) |
|
||||
| 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 |
|
||||
| Return instruction | `RETS 0` | `RETS 2` |
|
||||
| Locals | none | positive FP offsets |
|
||||
| Return instruction | `RETS 0` | `RETS 2` with arguments, `RETS 0` without |
|
||||
| Calls, callee-saves, stack args | rejected | required |
|
||||
| Field select | harness sets FS1/FS0 before the leaf | `FS1 = 32` preserved across calls |
|
||||
| Field size | harness sets FS1/FS0 before entry | `FS0 = 16` and `FS1 = 32`, set in the prologue and after every call |
|
||||
|
||||
M1 assembly is not ABI-compatible with TI's compiler or the ROM Xlib. M2
|
||||
replaces the convention; do not build on the M1 register assignment.
|
||||
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-clean/ compiler fork checkout used for M1 (ignored)
|
||||
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 IR pipeline wrapper (clang + tms-bitaddr + opt)
|
||||
scripts/m1/ M1 harness: C inputs, entry.asm, link.cmd, checks
|
||||
scripts/tests/m1/ M1 gates (run.sh, dis-roundtrip.sh)
|
||||
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 fork or upstream+patches build
|
||||
scripts/fetch-vt1200.sh fetch the VT1200 RE tree at its pinned commit
|
||||
@@ -92,10 +229,12 @@ 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. The M1 gates
|
||||
additionally need `dosbox`, `mtools` (`mcopy`), the VT1200 SDK image, the
|
||||
`dwt` COFF loader, and a built MAME `vt1200` binary
|
||||
(`build/mame-src/vt1200`). 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 dosbox mtools
|
||||
@@ -112,13 +251,13 @@ Xlib call sequence, `as34`, `dis34`, SDKs, manuals). It is not vendored:
|
||||
|
||||
## Build the compiler
|
||||
|
||||
### Compiler fork (M1, the normal path)
|
||||
### Compiler fork (M2, the normal path)
|
||||
|
||||
The fork checkout already exists at `build/llvm-clean` on `tms34010`. Build it
|
||||
in place:
|
||||
The fork checkout already exists at `build/llvm-clean` on `m2`. Build it in
|
||||
place:
|
||||
|
||||
```sh
|
||||
LLVM_SRC=$PWD/build/llvm-clean LLVM_BRANCH=tms34010 ./scripts/build-llvm.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
|
||||
```
|
||||
@@ -127,7 +266,7 @@ 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=tms34010 ./scripts/build-llvm.sh
|
||||
LLVM_BRANCH=m2 ./scripts/build-llvm.sh
|
||||
```
|
||||
|
||||
Fork mode **never** checks out, resets, cleans or patches the source tree; it
|
||||
@@ -144,22 +283,22 @@ TMS34010; a first build takes roughly 20–40 minutes.
|
||||
|
||||
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 M1 fork), it
|
||||
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.
|
||||
|
||||
`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.
|
||||
`git diff` (including new files via `git add -N`); it is not used for M1/M2.
|
||||
|
||||
## Build the M0 support tools
|
||||
|
||||
The M1 pipeline still uses `tms-bitaddr` and `tms-clang`:
|
||||
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 M1):
|
||||
M0 regression test (optional in M2):
|
||||
|
||||
```sh
|
||||
TMS_CLANG=$PWD/build/llvm-clean-build/bin/clang \
|
||||
@@ -169,71 +308,33 @@ TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \
|
||||
# -> M0 pipeline: PASS
|
||||
```
|
||||
|
||||
## Run the M1 gates
|
||||
|
||||
These are the documented commands; all three must pass:
|
||||
|
||||
```sh
|
||||
# 1. LLVM codegen tests: 17 passed
|
||||
build/llvm-clean-build/bin/llvm-lit -v \
|
||||
build/llvm-clean-build/test/CodeGen/TMS34010 --filter='m1-'
|
||||
|
||||
# 2. Compile, assemble, link and run in MAME
|
||||
./scripts/tests/m1/run.sh
|
||||
# -> M1 MAME: PASS
|
||||
|
||||
# 3. Rebuild the COFF and check the GSPA encodings with dis34
|
||||
./scripts/tests/m1/dis-roundtrip.sh
|
||||
# -> M1 dis34 round-trip: PASS
|
||||
```
|
||||
|
||||
`run.sh` compiles five C inputs with `scripts/tms-clang -O2`, runs
|
||||
`llc -march=tms34010 -O0 -filetype=asm` on the normalized IR, concatenates
|
||||
the result with `scripts/m1/entry.asm` and `scripts/m1/char-test.asm`,
|
||||
assembles/links with GSPA/GSPLNK 2.00 under DOSBox, then loads the COFF in
|
||||
MAME. `dis-roundtrip.sh` runs the same build and then disassembles the linked
|
||||
`.text` at the symbol addresses from the COFF symbol table. Paths are
|
||||
overridable with `TMS_CLANG`, `TMS_OPT`, `TMS_BITADDR`, `TMS_LLC`, `MAME`,
|
||||
`DOSBOX`, `SDK_IMG`, `M1_WORK`, `M1_COFF` and `M1_SDK`.
|
||||
|
||||
## Emit assembly by hand
|
||||
|
||||
```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/tms-clang -O2 -o /tmp/add.ll scripts/m1/add.c
|
||||
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/add.ll -o /tmp/add.s
|
||||
/tmp/add2.ll -o /tmp/add2.s
|
||||
```
|
||||
|
||||
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 now 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.
|
||||
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.
|
||||
|
||||
## Not yet supported (M2 and later)
|
||||
## Later milestones
|
||||
|
||||
- **Data:** only zero-initialized mutable globals (`.bss`) are supported.
|
||||
Initialized globals, string literals and constant/private data (`.data`,
|
||||
`.rodata`) make `llc` fail loudly; the `.data` initializer probe moves to
|
||||
M2.
|
||||
- **Object emission:** no `MCCodeEmitter`, object writer, `llc -filetype=obj`,
|
||||
`clang -c`, or native COFF. GSPA assembles and GSPLNK links; the existing
|
||||
COFF loader loads.
|
||||
- **TI ABI:** `A8` return, `A13`/`A14`/`A15` dual stack, `RETS 2`, `FS1 = 32`
|
||||
across calls, callee-saves, stack arguments, more than four arguments.
|
||||
- **C features:** calls, locals/`alloca`, structures/`byval`, floats, varargs,
|
||||
bitfields, 64-bit arithmetic, `memcpy`/`alloca` lowering.
|
||||
- **Runtime and clients:** ROM/Xlib imports, `strlen`/`malloc`, X11 headers,
|
||||
real VT1200 clients, debug info.
|
||||
- **2ine:** M1 uses the proven 1987 GSPA/GSPLNK under DOSBox, not the external
|
||||
2ine r5.01 tools; 2ine is an M2 speed-up.
|
||||
|
||||
These are M2–M5 in the design spec, each with its own plan.
|
||||
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.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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; }
|
||||
@@ -0,0 +1,2 @@
|
||||
extern int add2(int, int);
|
||||
int caller(void) { return add2(20, 22); }
|
||||
@@ -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"); }
|
||||
Executable
+327
@@ -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"
|
||||
@@ -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
|
||||
Executable
+16
@@ -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"
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
int locals(void) { volatile int x = 7; return x; }
|
||||
@@ -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")
|
||||
Executable
+171
@@ -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"
|
||||
Executable
+133
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
int add2(a, b) int a; int b; { return a + b; }
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
int locals() { int x; x = 7; return x; }
|
||||
@@ -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
|
||||
Executable
+16
@@ -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" "$@"
|
||||
Executable
+6
@@ -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" "$@"
|
||||
+2
-1
@@ -43,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
@@ -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.
|
||||
Reference in new issue
Block a user