From da553739c51927bb39506e90475be0e97c35e034 Mon Sep 17 00:00:00 2001 From: arti Date: Tue, 10 Nov 2026 14:42:56 +0000 Subject: [PATCH] docs: TMS34010 LLVM design, spike, and workspace setup --- .gitignore | 8 + ...34010-m0-fork-and-bit-address-semantics.md | 660 ++++++++++++++++++ .../specs/2026-11-10-tms34010-llvm-design.md | 495 +++++++++++++ scripts/fetch-vt1200.sh | 34 + spike/REPORT.md | 277 ++++++++ spike/bitscale.cpp | 292 ++++++++ spike/layout.c | 12 + spike/ptrdiff.c | 4 + spike/run.sh | 55 ++ spike/stress.c | 81 +++ spike/xclient.c | 136 ++++ spike/xlib_min.h | 350 ++++++++++ symbols/README.md | 12 + symbols/vt1200-v22.syms | 14 + 14 files changed, 2430 insertions(+) create mode 100644 .gitignore create mode 100644 docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md create mode 100644 docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md create mode 100755 scripts/fetch-vt1200.sh create mode 100644 spike/REPORT.md create mode 100644 spike/bitscale.cpp create mode 100644 spike/layout.c create mode 100644 spike/ptrdiff.c create mode 100755 spike/run.sh create mode 100644 spike/stress.c create mode 100644 spike/xclient.c create mode 100644 spike/xlib_min.h create mode 100644 symbols/README.md create mode 100644 symbols/vt1200-v22.syms diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb0c979 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# LLVM source pulled for the spike; not part of this project. +vendor/ +# Spike build artefacts. +spike/bitscale +spike/*.ll +spike/*.o +build/ +tools/tms-bitaddr/tms-bitaddr diff --git a/docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md b/docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md new file mode 100644 index 0000000..a8af603 --- /dev/null +++ b/docs/superpowers/plans/2026-11-10-tms34010-m0-fork-and-bit-address-semantics.md @@ -0,0 +1,660 @@ +# TMS34010 M0 — Fork and Bit-Address Semantics 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:** Make clang 23.1.5 understand TMS34010 addresses as bits: a `tms34010-unknown-none` target with TI's data model, pointer arithmetic scaled by 8, pointer differences divided by 8, and a verified early-GEP / late-normalization pipeline. No machine-code generation yet. + +**Architecture:** A pinned LLVM 23.1.5 fork adds the triple, clang target info, a stub target machine, and one pointer-difference patch. GEP scaling and late normalization are a project tool (`tools/tms-bitaddr`), not an LLVM change, so the fork stays small. A wrapper (`scripts/tms-clang`) runs clang at `-O0`, scales, optimizes, then normalizes and verifies. + +**Tech Stack:** LLVM/Clang 23.1.5, C++17, CMake/Ninja, `llvm-lit`/FileCheck, POSIX shell. + +**Spec:** `docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md` + +## Global Constraints + +- LLVM tag: `llvmorg-23.1.5`. No LLVM source is vendored in the project; the fork lives in `build/llvm-src`, and `patches/llvm/*.patch` is the committed delta. +- Target triple: `tms34010-unknown-none`. Little-endian, 32-bit pointers, 8-bit signed `char`, 16-bit `short`, 32-bit `int`/`long`/`size_t`/`ptrdiff_t`, 32-bit `wchar_t`, 32-bit `float`, 64-bit `double`/`long double`. +- Data layout: `e-m:e-p:32:16-i8:8-i16:16-i32:16-i64:16-f32:16-f64:16-n8:16:32`; all struct types are packed (`Opts.PackStruct = 1` in `TargetInfo::adjust`). +- Addresses are bit addresses after the GEP pass; `sizeof`, `offsetof`, `memcpy`/`memset` lengths and `alloca` sizes stay in bytes. +- Pointer difference: divide by `sizeof(T) * 8`; `(int)(p + n) - (int)p` must stay an ordinary integer subtraction. +- GEP contract: after the pass, a GEP offset is in bits; the backend must never apply `DataLayout` byte scaling to it (§7.3.1 of the spec). +- M0 stops at LLVM IR and the `opt`-level normalizer. No `llc`, no ABI, no ROM calls, no Xlib. The `llc` half of the GEP-contract test is M1. +- Build with `X86` (host tools) and `TMS34010`; Release with assertions; `clang`, `opt`, `llvm-lit`, `llvm-config` are the build outputs. +- Test inputs are the spike corpus (`spike/xclient.c`, `spike/ptrdiff.c`, `spike/stress.c`) plus the new `scripts/tests/m0/*` files. No real hardware in M0. +- VT1200 assets come from `scripts/fetch-vt1200.sh` into `vendor/vt1200`; they are ignored by git. +- Every task ends with a commit. LLVM changes are committed in `build/llvm-src`; the project commit contains the re-exported patch (and any project files). + +## Review Focus + +1. `p - q` versus `(int)p - (int)q`: the first must divide by 8, the second must not. Tests in Task 3 and Task 5. +2. Late GEPs created after `opt` (by InstCombine/CodeGenPrepare) must not be scaled a second time. Tests in Task 4 (`--normalize`) and Task 5 (`-O2` then normalize). +3. A constant GEP (`"str" + 2`, `&arr[1]`) must become `symbol + bit_addend`, not a byte addend. Test in Task 4. +4. Packed struct layout and `_Alignof`: `sizeof(struct{char;int;void*}) == 9`, offsets 1/5, `_Alignof(int) == 2`, `_Alignof(struct) == 1`. Test in Task 2. +5. `char` signedness and Xlib pointer fields: `(char)-1 < 0`; the Xlib client's `Display->screens` and `Screen` stride survive scaling. Tests in Task 2 and Task 5. + +## Repository Layout + +```text +build/llvm-src/ local LLVM clone at the tag; edited fork; ignored +build/llvm-build/ Ninja build tree; ignored +patches/llvm/ exported fork delta; committed +tools/tms-bitaddr/ GEP scale/normalize tool; committed +scripts/ build, wrapper, tests, fetch; committed +spike/ measurement corpus and reports; committed +docs/ design and plan; committed +symbols/ per-ROM import tables; committed +vendor/ fetched VT1200 tree; ignored +``` + +## Preflight + +- [ ] `git init` in `/home/arti/code/tms-c-compiler` if it is not a repo. +- [ ] Ensure `.gitignore` contains `build/`, `vendor/`, `spike/*.ll`, `spike/bitscale`, `tools/tms-bitaddr/tms-bitaddr`. +- [ ] Run `scripts/fetch-vt1200.sh` and confirm `vendor/vt1200/harness/c-hello/build/hello.s` exists. +- [ ] Install `cmake`, `ninja-build`, `ccache` (`sudo apt-get install -y cmake ninja-build ccache`). +- [ ] Commit the design doc, spike, scripts, and symbols as the initial state: + ```sh + git add docs spike scripts symbols .gitignore + git commit -m "docs: TMS34010 LLVM design, spike, and workspace setup" + ``` + +--- + +### Task 1: Pinned fork, triple, clang target info, stub target machine + +**Files:** +- Create: `build/llvm-src/clang/test/CodeGen/TMS34010/triple.c` (fork) +- Modify: `build/llvm-src/llvm/include/llvm/TargetParser/Triple.h` (fork) +- Modify: `build/llvm-src/llvm/lib/TargetParser/Triple.cpp` (fork) +- Create: `build/llvm-src/llvm/lib/Target/TMS34010/` (fork: `CMakeLists.txt`, `TMS34010TargetMachine.{h,cpp}`, `TMS34010Subtarget.{h,cpp}`, `TargetInfo/`, `MCTargetDesc/`) +- Modify: `build/llvm-src/llvm/lib/Target/CMakeLists.txt`, `build/llvm-src/llvm/CMakeLists.txt` (fork) +- Create: `build/llvm-src/clang/lib/Basic/Targets/TMS34010.{h,cpp}` (fork) +- Modify: `build/llvm-src/clang/lib/Basic/Targets.cpp`, `build/llvm-src/clang/lib/Basic/Targets/CMakeLists.txt` (fork) +- Create: `scripts/export-patches.sh` (project) +- Create: `patches/llvm/0001-triple.patch`, `patches/llvm/0002-target-skeleton.patch` (project, generated) + +**Interfaces:** +- Consumes: nothing. +- Produces: `llvm::Triple::tms34010`; `clang::TMS34010TargetInfo`; `llvm::TMS34010TargetMachine` (stub; `addPassesToEmitFile` returns `true`); target registration symbols `LLVMInitializeTMS34010TargetInfo`, `LLVMInitializeTMS34010Target`, `LLVMInitializeTMS34010TargetMC`. + +- [ ] **Step 1: Clone and build the pinned tree** + +```sh +mkdir -p build +git clone --depth 1 --branch llvmorg-23.1.5 \ + https://github.com/llvm/llvm-project.git build/llvm-src +cmake -S build/llvm-src/llvm -B build/llvm-build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DLLVM_TARGETS_TO_BUILD=X86 \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=TMS34010 \ + -DLLVM_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_BENCHMARKS=OFF +ninja -C build/llvm-build clang opt llvm-lit llvm-config FileCheck +``` + +Expected: build completes; `build/llvm-build/bin/clang --version` reports 23.1.5. + +- [ ] **Step 2: Write the failing lit test** + +`build/llvm-src/clang/test/CodeGen/TMS34010/triple.c`: + +```c +// RUN: %clang_cc1 -triple tms34010-unknown-none -ffreestanding -emit-llvm -o - %s | FileCheck %s +// CHECK: target triple = "tms34010-unknown-none" +// CHECK: target datalayout = "e-m:e-p:32:16-i8:8-i16:16-i32:16-i64:16-f32:16-f64:16-n8:16:32" +int add(int a, int b) { return a + b; } +``` + +- [ ] **Step 3: Run it to verify it fails** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: FAIL with `unknown target triple 'tms34010-unknown-none'` (or `unknown target`). + +- [ ] **Step 4: Implement the triple and target skeleton** + +In `Triple.h`, add `tms34010` to `enum ArchType` (next to the other 32-bit CISC entries). In `Triple.cpp`, add `"tms34010"` to the arch-name/parse/print paths, make `getArchPointerBitWidth` return 32, `isArch32Bit` return true, and `getDefaultFormat` return `ELF` for now (M0 does not emit objects). + +Add the target directory with the minimum LLVM target: + +- `TMS34010TargetMachine` constructor and `getSubtargetImpl`; `addPassesToEmitFile` returns `true` (not implemented in M0). +- `TargetInfo/TMS34010TargetInfo.cpp` registers the target for `Triple::tms34010` with `RegisterTarget`. +- `MCTargetDesc/TMS34010MCTargetDesc.{h,cpp}` with `TMS34010MCAsmInfo`, `TMS34010MCInstrInfo`, `TMS34010MCRegisterInfo`, `TMS34010MCSubtargetInfo` and the init functions. +- Add the directory to `llvm/lib/Target/CMakeLists.txt` and the target name to `llvm/CMakeLists.txt` (`LLVM_ALL_TARGETS` / experimental list). + +Add `clang/lib/Basic/Targets/TMS34010.{h,cpp}`: + +```cpp +class LLVM_LIBRARY_VISIBILITY TMS34010TargetInfo : public TargetInfo { +public: + TMS34010TargetInfo(const llvm::Triple &Triple, const TargetOptions &Opts); + void getTargetDefines(const LangOptions &Opts, + MacroBuilder &Builder) const override; + bool hasFeature(StringRef Feature) const override { return false; } +}; +``` + +Set `DataLayoutString` / `DescriptionString` to the Global Constraints layout, and register the target in the `clang/lib/Basic/Targets.cpp` switch and CMake list. Add defines `__tms34010__`, `__TMS34010__`, `__CHAR_BIT__ 8`, `__SIZEOF_POINTER__ 4`, `__ILP32__ 1`. + +Rebuild: `ninja -C build/llvm-build clang opt llvm-lit`. + +- [ ] **Step 5: Run the test to verify it passes** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: PASS. + +- [ ] **Step 6: Export the fork delta and commit** + +`scripts/export-patches.sh`: + +```sh +#!/bin/sh +set -eu +SRC=${LLVM_SRC:-build/llvm-src} +TAG=llvmorg-23.1.5 +OUT=${1:-patches/llvm} +mkdir -p "$OUT" +git -C "$SRC" diff "$TAG" -- \ + llvm/include/llvm/TargetParser llvm/lib/TargetParser \ + > "$OUT/0001-triple.patch" +git -C "$SRC" diff "$TAG" -- \ + llvm/lib/Target/TMS34010 llvm/lib/Target/CMakeLists.txt llvm/CMakeLists.txt \ + > "$OUT/0002-target-skeleton.patch" +git -C "$SRC" diff "$TAG" -- \ + clang/lib/Basic/Targets clang/lib/Basic/Targets.cpp \ + clang/lib/Basic/Targets/CMakeLists.txt clang/test/CodeGen/TMS34010 \ + > "$OUT/0003-clang-target.patch" +git -C "$SRC" diff "$TAG" -- clang/lib/CodeGen/CGExprScalar.cpp \ + > "$OUT/0004-clang-ptrdiff.patch" +find "$OUT" -type f -empty -delete +``` + +Run it, then: +```sh +git add scripts/export-patches.sh patches/llvm +git commit -m "llvm: add tms34010 triple, clang target info, and target skeleton" +``` + +### Task 2: Data model and packed struct tests + +**Files:** +- Create: `build/llvm-src/clang/test/CodeGen/TMS34010/layout.c` (fork) +- Modify: `build/llvm-src/clang/lib/Basic/Targets/TMS34010.{h,cpp}` (fork) +- Modify: `patches/llvm/0003-clang-target.patch` (project, regenerated) + +**Interfaces:** +- Consumes: `clang::TMS34010TargetInfo` from Task 1. +- Produces: the type model and `TMS34010TargetInfo::adjust(DiagnosticsEngine &, LangOptions &Opts, const TargetInfo *Aux)` setting `Opts.PackStruct = 1`. + +- [ ] **Step 1: Write the failing test** + +`build/llvm-src/clang/test/CodeGen/TMS34010/layout.c`: + +```c +// RUN: %clang_cc1 -triple tms34010-unknown-none -ffreestanding -fsyntax-only %s +struct S { char c; int i; void *p; }; +enum E { E0 = 0, E1 = 1 }; + +_Static_assert(sizeof(struct S) == 9, "packed struct"); +_Static_assert(__builtin_offsetof(struct S, i) == 1, "packed i"); +_Static_assert(__builtin_offsetof(struct S, p) == 5, "packed p"); +_Static_assert(_Alignof(struct S) == 1, "packed struct alignment"); +_Static_assert(_Alignof(int) == 2, "object alignment"); +_Static_assert(sizeof(int) == 4, "int"); +_Static_assert(sizeof(long) == 4, "long"); +_Static_assert(sizeof(void *) == 4, "pointer"); +_Static_assert(sizeof(short) == 2, "short"); +_Static_assert(sizeof(long double) == 8, "long double"); +_Static_assert(sizeof(float) == 4, "float"); +_Static_assert(sizeof(double) == 8, "double"); +_Static_assert(sizeof(long long) == 8, "long long"); +_Static_assert(sizeof(_Bool) == 1, "_Bool"); +_Static_assert(sizeof(__WCHAR_TYPE__) == 4, "wchar_t"); +_Static_assert(sizeof(__SIZE_TYPE__) == 4, "size_t"); +_Static_assert(sizeof(__PTRDIFF_TYPE__) == 4, "ptrdiff_t"); +_Static_assert((char)-1 < 0, "char is signed"); +_Static_assert(_Alignof(char) == 1, "char align"); +_Static_assert(_Alignof(short) == 2, "short align"); +_Static_assert(_Alignof(void *) == 2, "pointer align"); +_Static_assert(_Alignof(double) == 2, "double align"); +_Static_assert(sizeof(enum E) == 4, "enum"); +``` + +- [ ] **Step 2: Run it to verify it fails** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: FAIL on the packed-struct assertions (currently 12/4/8) and/or enum/alignment. + +- [ ] **Step 3: Complete the target info** + +Set the `TargetInfo` widths and alignments from Global Constraints: `CharWidth=8`, `CharAlign=8`, `CharIsSigned=true`; `ShortWidth=16`; `IntWidth=32`; `LongWidth=32`; `LongLongWidth=64`; `PointerWidth=32`, `PointerAlign=16`; `FloatWidth=32`, `DoubleWidth=64`, `LongDoubleWidth=64`; `WCharType=SignedInt`, `WCharWidth=32`; `SizeType=UnsignedInt`, `PtrDiffType=SignedInt`, `IntMaxType=SignedLongLong`; `BoolWidth=8`, `BoolAlign=16`; scalar alignments (Int/Long/Float/Double/LongDouble) 16 bits. + +Implement `adjust(DiagnosticsEngine &Diags, LangOptions &Opts, const TargetInfo *Aux) override { Opts.PackStruct = 1; }`. + +Enums are 32-bit; the test gates `sizeof(enum E) == 4`. TI makes enums unsigned; if a later ABI test depends on the signedness, add the target's enum-type hook then. + +- [ ] **Step 4: Run the test to verify it passes** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: PASS. + +- [ ] **Step 5: Regenerate the patch and commit** + +```sh +./scripts/export-patches.sh +git add patches/llvm +git commit -m "clang: tms34010 data model and packed structs" +``` + +### Task 3: Address-unit hook and pointer-difference scaling + +**Files:** +- Create: `build/llvm-src/clang/test/CodeGen/TMS34010/ptrdiff.c` (fork) +- Modify: `build/llvm-src/clang/include/clang/Basic/TargetInfo.h` (fork) +- Modify: `build/llvm-src/clang/lib/Basic/Targets/TMS34010.h` (fork) +- Modify: `build/llvm-src/clang/lib/CodeGen/CGExprScalar.cpp` (fork) +- Modify: `patches/llvm/0003-clang-target.patch`, `patches/llvm/0004-clang-ptrdiff.patch` (project, regenerated) + +**Interfaces:** +- Consumes: `TMS34010TargetInfo` from Task 1/2. +- Produces: `virtual unsigned TargetInfo::getAddressUnitBits() const { return 8; }`; `TMS34010TargetInfo::getAddressUnitBits() const override { return 1; }`; pointer-difference IR of `sdiv exact ... sizeof(T)*8`. + +- [ ] **Step 1: Write the failing test** + +`build/llvm-src/clang/test/CodeGen/TMS34010/ptrdiff.c`: + +```c +// RUN: %clang_cc1 -triple tms34010-unknown-none -O0 -emit-llvm -o - %s | FileCheck %s +struct S { char c; int i; void *p; }; + +long pd_char(char *a, char *b) { return a - b; } +long pd_int(int *a, int *b) { return a - b; } +long pd_struct(struct S *a, struct S *b) { return a - b; } +long pd_vla(int n, char (*a)[n], char (*b)[n]) { return a - b; } +int int_diff(char *p) { return (int)(p + 1) - (int)p; } +int later(void) { return 0; } + +// CHECK-LABEL: @pd_char +// CHECK: sub.ptr.sub +// CHECK: sdiv exact i32 %{{.*}}, 8 +// CHECK-LABEL: @pd_int +// CHECK: sdiv exact i32 %{{.*}}, 32 +// CHECK-LABEL: @pd_struct +// CHECK: sdiv exact i32 %{{.*}}, 72 +// CHECK-LABEL: @pd_vla +// CHECK: mul {{.*}}, 8 +// CHECK-LABEL: @int_diff +// CHECK-NOT: sdiv +// CHECK-LABEL: @later +``` + +- [ ] **Step 2: Run it to verify it fails** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: FAIL — `pd_char` has no divide, `pd_int` divides by 4, `pd_struct` by 9. + +- [ ] **Step 3: Implement the hook and the EmitSub change** + +Add the virtual hook to `TargetInfo.h` next to `getCharWidth()`: + +```cpp +virtual unsigned getAddressUnitBits() const { return 8; } +``` + +Override it in `TMS34010TargetInfo` to return 1. + +In `ScalarExprEmitter::EmitSub` (pointer-difference branch, around the `diffInChars` and `elementSize` code), compute `unsigned Scale = 8 / CGF.getTarget().getAddressUnitBits();` and: + +- Do not early-return for `elementSize.isOne()` unless `Scale == 1`. +- `divisor = CGF.CGM.getSize(elementSize * Scale);` +- For the VLA branch, multiply the VLA element-size divisor by `Scale` as well. +- Keep `CreateExactSDiv(diffInChars, divisor, "sub.ptr.div")`. + +Do not touch the integer `sub` path; `(int)(p + 1) - (int)p` must keep its plain subtraction. + +Rebuild: `ninja -C build/llvm-build clang opt llvm-lit`. + +- [ ] **Step 4: Run the test to verify it passes** + +Run: +```sh +build/llvm-build/bin/llvm-lit -v \ + build/llvm-build/tools/clang/test --filter='TMS34010' +``` +Expected: PASS. + +- [ ] **Step 5: Regenerate the patches and commit** + +```sh +./scripts/export-patches.sh +git add patches/llvm +git commit -m "clang: bit-address pointer difference scaling" +``` + +### Task 4: `tms-bitaddr` scale and normalize tool + +**Files:** +- Create: `tools/tms-bitaddr/bitscale.cpp` +- Create: `tools/tms-bitaddr/build.sh` +- Create: `tools/tms-bitaddr/tests/scale.ll` +- Create: `tools/tms-bitaddr/tests/const-gep.ll` +- Create: `tools/tms-bitaddr/tests/normalize.ll` +- Create: `tools/tms-bitaddr/tests/run.sh` + +**Interfaces:** +- Consumes: the fork's `llvm-config`, `opt`, `FileCheck`. +- Produces: `tms-bitaddr --scale IN.ll OUT.ll` and `tms-bitaddr --normalize IN.ll OUT.ll`; `--scale` rewrites every non-vector GEP (instruction and constant expression) as `base + (byte_offset << 3)`; `--normalize` rewrites every remaining GEP (instruction and constant expression) as `base + offset` with the offset treated as bits (no further scaling). + +- [ ] **Step 1: Write the failing tests** + +Also create `tools/tms-bitaddr/tests/run.sh`: + +```sh +#!/bin/sh +set -eu +ROOT=$(cd "$(dirname "$0")/.." && pwd) +TOOL=${TMS_BITADDR:-$ROOT/tms-bitaddr} +FILECHECK=${FILECHECK:-$ROOT/../../build/llvm-build/bin/FileCheck} +"$TOOL" --scale "$ROOT/tests/scale.ll" | "$FILECHECK" "$ROOT/tests/scale.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" +``` + +`tools/tms-bitaddr/tests/scale.ll`: + +```llvm +; RUN: %tms-bitaddr --scale %s | FileCheck %s +define ptr @gep(ptr %p) { + %q = getelementptr i8, ptr %p, i32 3 + ret ptr %q +} +; CHECK-LABEL: @gep +; CHECK: %bit.addr = add i32 %bit.base, 24 +; CHECK-NOT: getelementptr +``` + +`tools/tms-bitaddr/tests/const-gep.ll`: + +```llvm +; RUN: %tms-bitaddr --scale %s | FileCheck %s +@g = global [4 x i8] zeroinitializer +@s = global ptr getelementptr ([4 x i8], ptr @g, i32 0, i32 2) +define ptr @c() { + ret ptr getelementptr ([4 x i8], ptr @g, i32 0, i32 2) +} +; CHECK: @s = global ptr inttoptr (i32 add (i32 ptrtoint (ptr @g to i32), i32 16) to ptr) +; CHECK: inttoptr (i32 add (i32 ptrtoint (ptr @g to i32), i32 16) to ptr) +``` + +`tools/tms-bitaddr/tests/normalize.ll`: + +```llvm +; RUN: %tms-bitaddr --normalize %s | FileCheck %s +define ptr @n(ptr %p) { + %q = getelementptr i8, ptr %p, i32 16 + ret ptr %q +} +; CHECK-LABEL: @n +; CHECK: %bit.addr = add i32 %bit.base, 16 +; CHECK-NOT: getelementptr +``` + +- [ ] **Step 2: Run the tests to verify they fail** + +Run `tools/tms-bitaddr/tests/run.sh` (the tool does not exist yet). Expected: FAIL / command not found. + +- [ ] **Step 3: Implement the tool** + +Adapt `spike/bitscale.cpp` to LLVM 23.1.5. Keep the two modes and the stats output. `--scale` uses the existing constant-GEP rewrite and instruction GEP rewrite; `--normalize` rewrites the GEP to `inttoptr(add(ptrtoint(base), offset))` without shifting the offset. `build.sh`: + +```sh +#!/bin/sh +set -eu +LLVM_CONFIG=${LLVM_CONFIG:-build/llvm-build/bin/llvm-config} +cd "$(dirname "$0")" +c++ -std=c++17 $("$LLVM_CONFIG" --cxxflags) bitscale.cpp \ + $("$LLVM_CONFIG" --ldflags --libs core irreader support --system-libs) \ + -o tms-bitaddr +``` + +`tests/run.sh` is the driver above; it invokes the tool and `FileCheck` for each `.ll` file. The `RUN:` lines in the `.ll` files are documentation only in M0. + +- [ ] **Step 4: Build and run the tests** + +```sh +LLVM_CONFIG=build/llvm-build/bin/llvm-config tools/tms-bitaddr/build.sh +tools/tms-bitaddr/tests/run.sh +``` +Expected: all three tests PASS; the scale test leaves zero GEPs; the constant GEP has a 16-bit addend; normalize adds 16, not 128. + +- [ ] **Step 5: Commit** + +```sh +git add tools/tms-bitaddr +git commit -m "tools: add tms-bitaddr GEP scale and normalize tool" +``` + +### Task 5: Pipeline wrapper and integration tests + +**Files:** +- Create: `scripts/tms-clang` +- Create: `scripts/tests/m0/ptrdiff.c` +- Create: `scripts/tests/m0/run.sh` +- Modify: `.gitignore` if needed + +**Interfaces:** +- Consumes: `clang`, `opt`, `tms-bitaddr` from Tasks 1–4. +- Produces: `scripts/tms-clang [-O0|-O2] [-I dir] -o OUT.ll IN.c`, which runs clang `-O0`, `tms-bitaddr --scale`, `opt -passes='default'` (unless `-O0`), and `tms-bitaddr --normalize`. + +- [ ] **Step 1: Write the failing integration test** + +`scripts/tests/m0/ptrdiff.c`: + +```c +int int_diff(char *p) { return (int)(p + 1) - (int)p; } +int int_diff4(int *p) { return (int)(p + 1) - (int)p; } +long pd_char(char *a, char *b) { return a - b; } +long pd_int(int *a, int *b) { return a - b; } +``` + +`scripts/tests/m0/run.sh`: + +```sh +#!/bin/sh +set -eu +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +OUT=${TMPDIR:-/tmp}/tms-m0 +mkdir -p "$OUT" +TMS_CLANG=${TMS_CLANG:-$ROOT/build/llvm-build/bin/clang} +TMS_OPT=${TMS_OPT:-$ROOT/build/llvm-build/bin/opt} +TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr} +export TMS_CLANG TMS_OPT TMS_BITADDR + +"$ROOT/scripts/tms-clang" -O2 -o "$OUT/pdiff.ll" "$ROOT/scripts/tests/m0/ptrdiff.c" +grep -A2 'define.*@int_diff(' "$OUT/pdiff.ll" | grep -q 'ret i32 8' +grep -A2 'define.*@int_diff4' "$OUT/pdiff.ll" | grep -q 'ret i32 32' +grep -A6 'define.*@pd_char' "$OUT/pdiff.ll" | grep -Eq 'sdiv|ashr|udiv' +grep -A6 'define.*@pd_int' "$OUT/pdiff.ll" | grep -Eq 'sdiv|ashr|udiv' +"$TMS_OPT" -passes=verify -disable-output "$OUT/pdiff.ll" + +"$ROOT/scripts/tms-clang" -O2 -I "$ROOT/spike" -o "$OUT/xclient.ll" "$ROOT/spike/xclient.c" +test "$(grep -c getelementptr "$OUT/xclient.ll")" -eq 0 +grep -Eq 'add i32 %bit.base[0-9]*, 1056' "$OUT/xclient.ll" +grep -Eq 'mul i32 [^,]+, 640' "$OUT/xclient.ll" +"$TMS_OPT" -passes=verify -disable-output "$OUT/xclient.ll" + +"$ROOT/scripts/tms-clang" -O0 -o "$OUT/pdiff.o0.ll" "$ROOT/scripts/tests/m0/ptrdiff.c" +test "$(grep -c getelementptr "$OUT/pdiff.o0.ll")" -eq 0 +echo "M0 pipeline: PASS" +``` + +- [ ] **Step 2: Run it to verify it fails** + +Run `scripts/tests/m0/run.sh`. Expected: FAIL (wrapper missing). + +- [ ] **Step 3: Implement the wrapper** + +`scripts/tms-clang` must: + +1. Parse `-O0`/`-O2` (default `-O2` after scaling) and `-I` passes; pass `-ffreestanding -nostdinc` plus the include paths to clang. +2. Run `"$TMS_CLANG" --target=tms34010-unknown-none -O0 -Xclang -disable-O0-optnone -S -emit-llvm -o tmp.ll "$IN"`. +3. Run `"$TMS_BITADDR" --scale tmp.ll tmp.scaled.ll`. +4. Unless `-O0`, run `"$TMS_OPT" -passes='default' -S tmp.scaled.ll -o tmp.opt.ll`. +5. Run `"$TMS_BITADDR" --normalize tmp.input.ll "$OUT"`. +6. Leave temporary files under `${TMPDIR:-/tmp}` and clean them up. + +Do not invoke `llc`; M0 output is IR. + +- [ ] **Step 4: Run the integration test to verify it passes** + +Run `scripts/tests/m0/run.sh`. Expected: `M0 pipeline: PASS`. + +- [ ] **Step 5: Commit** + +```sh +git add scripts/tms-clang scripts/tests/m0 .gitignore +git commit -m "scripts: add M0 clang/scale/opt/normalize wrapper and tests" +``` + +### Task 6: Re-run the spike on 23.1.5 and record it + +**Files:** +- Modify: `spike/run.sh` (accept `CLANG`, `OPT`, `LLVMCONFIG` from the environment) +- Create: `spike/REPORT-23.md` + +**Interfaces:** +- Consumes: the Task 5 wrapper/tool and the 23.1.5 build. +- Produces: a recorded 23.1.5 measurement, replacing the 19.1.7-only evidence as the current result. + +- [ ] **Step 1: Make `spike/run.sh` use the environment** + +Default `CLANG=clang-19`, `OPT=opt-19`, `LLVMCONFIG=llvm-config-19`, `CXX=clang++-19`; allow the caller to override all four. Use `TMS_BITADDR` if set, otherwise build `spike/bitscale` as today. + +- [ ] **Step 2: Run the spike against 23.1.5** + +```sh +CLANG=$PWD/build/llvm-build/bin/clang \ +OPT=$PWD/build/llvm-build/bin/opt \ +LLVMCONFIG=$PWD/build/llvm-build/bin/llvm-config \ +CXX=$PWD/build/llvm-build/bin/clang++ \ +TMS_BITADDR=$PWD/tools/tms-bitaddr/tms-bitaddr \ +./spike/run.sh +``` + +- [ ] **Step 3: Record the results** + +Create `spike/REPORT-23.md` with: LLVM 23.1.5, the same table as `REPORT.md`, the 35 → 0 Xlib GEP result, `int_diff` = 8, `int_diff4` = 32, `pd_char` = bytes, and any differences from 19.1.7. State explicitly that this is the current measured result and 19.1.7 is historical. + +- [ ] **Step 4: Commit** + +```sh +git add spike/run.sh spike/REPORT-23.md +git commit -m "spike: re-run on LLVM 23.1.5" +``` + +### Task 7: Clean-build patch series and documentation + +**Files:** +- Create: `scripts/build-llvm.sh` +- Create: `README.md` (or `docs/build.md`) +- Modify: `patches/llvm/*.patch` (final export) +- Modify: `.gitignore` + +**Interfaces:** +- Consumes: all previous tasks. +- Produces: a clean clone + patch + build path and a documented M0 test command. + +- [ ] **Step 1: Write the clean-build script** + +`scripts/build-llvm.sh`: + +```sh +#!/bin/sh +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} +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 +PATCHROOT=$(pwd)/patches/llvm +for p in "$PATCHROOT"/*.patch; do + [ -e "$p" ] || continue + git -C "$SRC" apply "$p" +done +cmake -S "$SRC/llvm" -B "$BUILD" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DLLVM_TARGETS_TO_BUILD=X86 \ + -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=TMS34010 \ + -DLLVM_INCLUDE_TESTS=ON -DLLVM_INCLUDE_EXAMPLES=OFF \ + -DLLVM_INCLUDE_BENCHMARKS=OFF +ninja -C "$BUILD" clang opt llvm-lit llvm-config FileCheck +``` + +- [ ] **Step 2: Run the clean build** + +```sh +./scripts/build-llvm.sh +``` +Expected: patches apply cleanly; build succeeds. If a patch does not apply, regenerate it with `scripts/export-patches.sh` and retry. + +- [ ] **Step 3: Run the M0 tests against the clean build** + +```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 +``` +Expected: `M0 pipeline: PASS`, plus the lit tests: +```sh +build/llvm-clean-build/bin/llvm-lit -v \ + build/llvm-clean-build/tools/clang/test --filter='TMS34010' +``` +Expected: PASS. + +- [ ] **Step 4: Document** + +`README.md` (or `docs/build.md`) must state: the pinned tag, the repository layout, the build command, how to run the M0 tests, the fact that `vendor/` is fetched and ignored, and that M1 (backend/ABI) follows. Do not claim code generation works. + +- [ ] **Step 5: Commit** + +```sh +git add scripts/build-llvm.sh README.md docs .gitignore patches/llvm +git commit -m "build: clean LLVM fork build from pinned tag and patches" +``` + +--- + +## Execution Handoff + +This plan is M0 only. M1–M5 (backend, TI ABI/ROM calls, Xlib, client, robustness) are separate plans because their tasks depend on what M0/M1 measure; the spec's milestones define their boundaries. + +After implementation, the expected state is: a clean LLVM 23.1.5 build with the `tms34010-unknown-none` clang target; correct IR for pointer arithmetic and pointer differences; a scale/normalize tool; and an integration test proving the Xlib client's IR is bit-addressed — no TMS machine code yet. diff --git a/docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md b/docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md new file mode 100644 index 0000000..65ac05b --- /dev/null +++ b/docs/superpowers/specs/2026-11-10-tms34010-llvm-design.md @@ -0,0 +1,495 @@ +# TMS34010 LLVM toolchain — design + +**Status:** proposed for approval, 2026-11-10. +**Scope:** a modern C toolchain for the DEC VT1200 (TI TMS34010), built as a +minimal fork of upstream LLVM 23.1.5. + +The working directory is `/home/arti/code/tms-c-compiler`. The bit-address +spike lives in `spike/`; its report is `spike/REPORT.md`. The spike used LLVM +19.1.7 and is reference only; the implementation pins **LLVM 23.1.5**. + +--- + +## 1. Intent + +The VT1200 runs its X server, terminal manager, VT emulator and network stack +from ROM on a TMS34010. Its only C compilers are TI's 1987 K&R compiler and the +1990 r5.01 OS/2 tools; both are awkward to run and cannot express modern C. + +We want `clang` to compile C for the TMS34010 with the **TI ABI**, so that a +compiled program can call the ROM's `XOpenDisplay`, `XCreateSimpleWindow`, +`malloc`, `strlen`, `XSync`, and so on, and can be loaded by the existing COFF +loader. The fork must stay small against upstream: a new target machine, a +small generic address-unit hook in clang, one pointer-difference fix, and an +early GEP-scaling pass. + +## 2. Goals + +- `clang --target=tms34010-unknown-none -O2` produces code that runs on a real + VT1200 and in MAME. +- TI ABI compatibility: call and be called by ROM Xlib, `rts.lib`, `flib.lib` + and `malloc`; match data layout, calling convention, stacks, field-size + state and struct layout. +- Ordinary C11/C17 (whatever clang supports); no C++ initially. +- Minimal fork: TMS34010 target under `llvm/lib/Target/TMS34010`, clang target + info, one generic `TargetInfo` hook, one pointer-difference patch, one + bit-address IR pass. +- Reproducible Linux build from a pinned LLVM tag; no DOSBox or OS/2 in the + compiler path. +- First output path: GSPA-compatible assembly → `gspa`/`gsplnk` under 2ine → + COFF → existing loader. Native LLVM COFF later. +- Test on MAME and real hardware; use the TI compiler as an oracle. + +## 3. Non-goals + +- Rebuilding the ROM/firmware or a complete OS. +- C++, exceptions, RTTI, threads, dynamic linking, MMU support. +- DWARF/GDB in the first milestone; symbol map and line info first. +- A new libc; reuse the ROM runtime and `rts.lib`/`flib.lib`. +- Bitfields, varargs and floating point in the first milestone (needed later; + a minimal Xlib client does not use them). +- Upstream acceptance; the target may remain out-of-tree. +- Any byte-address workaround that breaks Xlib structs or binary compatibility. + +## 4. Constraints and facts + +### 4.1 CPU and C model + +- TMS34010: 32-bit CISC, bit-addressed, 16-bit instruction words, 1–3-word + instructions, 256-byte instruction cache, no MMU, 2–4 MB RAM, little-endian. +- Pointers are 32-bit **bit** addresses: `char *p; p + 1` adds 8. +- `char` is **signed** 8 bits; `unsigned char` 8; `short` 16; `int` and `long` + are 32; `double` and `long double` are 64; `float` 32; `enum` is unsigned + 32-bit; `wchar_t` is 32; `size_t`/`ptrdiff_t` 32. (C guide §4.2, §6.) +- All objects except struct/array members are 16-bit aligned. Structure and + array members are **fully packed**: `sizeof(struct { char c; int i; void *p; })` + is 9, offsets 1 and 5. Only the first struct in an array is 16-bit aligned. + (C guide §4.3, §5.1.6.) +- Structures passed by value are rounded up to the next 16-bit boundary. + +### 4.2 ABI + +- Registers: A0–A12 general, A8 return value, A13 FP, A14 STK (program stack), + A15 system stack. B0–B14 are unused by TI's C compiler. +- Caller: if arguments are passed or a float/double is returned, push the old + STK on the system stack (`MOVE STK,-*SP`); push arguments on the program + stack in reverse order, widened to 32 bits; structs by value rounded to + 16 bits; struct return address pushed on the program stack; call with + `CALLA` (direct) or `CALL Rs` (indirect). +- Callee: save used A registers except A8 on the system stack (`MMTM SP,…`); + set `FP = STK`; `ADDI frame_size,STK`; locals at positive FP offsets, first + argument at `FP(-32)`; restore STK from `*SP(offset)`; `MMFM`; `RETS 2` if + arguments or stack return, `RETS 0` otherwise. +- `FS1` must be 32 inside C code and `FE0` must be 0; `FS0` may be changed + without restoring. Xlib clobbers the field-size state, so FS1 must be + re-established after every call. +- Struct return: caller allocates, pushes the address; callee copies. Float/ + double return: caller allocates space on the program stack, callee copies. + (C guide §5.3.) +- Symbols use a leading underscore (`_main`); sections are `.text`, `.data`, + `.bss`; code is position-dependent and linked at fixed bit addresses. + +### 4.3 Toolchain and loader + +- Section origins and sizes, and assembler/linker location counters, are in + **bits**. `SECTIONS { .text 3F208000h: {} }` is a bit address. +- The existing COFF loader places sections at header addresses, zeroes `.bss`, + and enters at the header entry point. Payloads are loaded into free RAM at + fixed bit addresses. +- GSPA/gsplnk under 2ine produce COFF and run in ~50 ms. `as34`/`ld34` are + native but emit a custom `linkhdr`, not COFF. +- The VT1200 tree is fetched by `scripts/fetch-vt1200.sh` at pinned commit + `17dfd24340fe3346433f90457cb48cb8786dff39` into `vendor/vt1200` (it is not + vendored; `vendor/` is ignored). It provides `harness/c-hello` including + `build/hello.s`, `dwt/loader`, `dwt/gui/xhello.asm`, `tools/as34`, + `tools/smart34010/dis34.c`, `tools/tms34010-sdk`, `docs/TMS34010-users-guide.txt` + and the Ghidra notes. MAME itself is an external build; the repo contains the + driver and run scripts, not the emulator binary. +- Xlib headers are not usable files in that tree (`reference/x11/r4` and `/r5` + are gitlinks without a URL). Use X11R4 headers from X.Org, or the system + X11R7 headers, and verify the core `Display`/`Screen`/`XImage`/`XEvent` + layouts against the ROM before relying on them. +- ROM import symbols live in `symbols/vt1200-v22.syms`. It contains only the + entries already proven by `xhello.asm`/the README; the rest must be extracted + from the Ghidra project or `ptr_constants.py` and validated before the first + Xlib call. Addresses are per-ROM-revision; do not assume they are stable + across v2.1/v2.2 or across units. + +### 4.4 LLVM + +- Pin `llvmorg-23.1.5` (latest stable, 2026-11-03). Build with + `LLVM_TARGETS_TO_BUILD="X86;TMS34010"` (or the experimental-target variable), + assertions on, clang enabled. +- The spike on 19.1.7 showed: a 30-line GEP-scaling pass scales all GEPs in a + real Xlib client (35 → 0), verifies, and survives `-O2`; pointer subtraction + needs a frontend patch; `-fpack-struct=1` matches TI's packed layout; the + dual-stack ABI is the larger backend job. Re-validate on 23.1.5 first. +- The pinned source is `vendor/llvm-project-23.1.5`. That checkout is sparse + (IR/CodeGen/target/CodeGen dirs only); a real build needs a full checkout or + a fresh clone at the same tag. + +## 5. Prior art + +- **TI 2.01 K&R and 5.01 ANSI C**: the ABI and runtime we must match; use as a + differential oracle for layout and call behaviour. +- **Andrew Morton's GCC 2.5.8 port**: proves C/C++ on the CPU, but uses a + different ABI (A15 stack, A14 FP, A0 return, FS0=32/FS1=16) and about 110 + `AKPM` changes spread through GCC. It cannot link `rts.lib` or call Xlib + as-is. Reference for bit-address pitfalls, not a base. +- **as34/ld34**: native assembler/linker from Morton's toolchain; custom + object format, not COFF. +- **GSPA/gsplnk**: TI assembler/linker, COFF output, runs under 2ine. +- **smart34010/dis34**: disassembler with two encoding fixes, validated + against GSPA by byte-exact ROM round-trip. +- **MAME `34010ops.hxx`**: compact opcode table with encodings; useful to + generate and cross-check the LLVM instruction definitions. +- **Spike 2026-11-10**: see `spike/REPORT.md`. + +## 6. Approaches considered + +1. **LLVM fork — chosen.** Best optimizer and C/C++ frontend; existing MC, + asm-printer and object infrastructure; the target is a clean new directory. + Cost: a real backend plus a small clang change. +2. **GCC port — rejected.** Forward-porting Morton's 2.5.8, or a new GCC 15 + target, repeats the bit-address hacks across a compiler whose interfaces + are less modular, and the old port's ABI is not TI's. Similar effort for + less maintainability. +3. **Small custom backend / QBE / vbcc — rejected for now.** Fastest to a + first binary and smallest codebase, but loses clang's C11/C++ and the LLVM + optimizer, and we would own the frontend. Keep as a fallback if the LLVM + target proves impractical. +4. **LLVM with byte addresses + boundary conversions — rejected.** Xlib's + `Display`, `Screen`, `XImage`, `XEvent`, `XTextProperty` and `XClassHint` + contain pointers; standard macros (`ScreenOfDisplay`, `RootWindow`, + `DefaultVisual`) traverse them. A byte-address view would need conversions + at every pointer field, or custom wrapper macros. + +## 7. Design + +### 7.1 Fork and build + +- Pin `llvmorg-23.1.5`; keep a fork remote and an exported patch series so the + delta is reviewable (the `tools/as34` pattern). Do not vendor the LLVM tree + in the final repo; `vendor/` is spike-only. +- Install `cmake`, `ninja-build`, `ccache`; build X86 (host tools) plus + TMS34010; Release with assertions. +- Patch sets: + 1. clang: `TargetInfo::getAddressUnitBits()` (new; default 8; TMS34010 returns + 1) and the TMS34010 `TargetInfo`. The hook is a scalar used **only** by the + pointer-difference divisor. It does not change `CharUnits`, `sizeof`, + GEP emission or `DataLayout`; object sizes and `memcpy` lengths stay in + bytes. + 2. clang: pointer-difference scaling in `CGExprScalar.cpp`. + 3. project tool `tools/tms-bitaddr` (GEP scaling and late normalization), + run by the driver wrapper; not an LLVM patch initially, so the fork stays + smaller. + 4. llvm: the TMS34010 target. +- Keep all TMS-specific code inside the target and the target-info files; the + only generic clang change is the address-unit query and the pointer-difference + scale, both small enough to be upstreamable independently. If the hook is + rejected upstream, the pointer-difference patch can read the triple directly. + +### 7.2 Target description + +- Triple: `tms34010-unknown-none` (bare metal). +- Little-endian; 32-bit pointers; 8-bit `char`; 16-bit `short`; 32-bit `int`, + `long`, `size_t`, `ptrdiff_t`; 32-bit `wchar_t`; 32-bit `float`; 64-bit + `double`/`long double`. +- Data layout: scalars 16-bit aligned (`i32:16`, `i64:16`, `f32:16`, `f64:16`, + `p:32:16`) with **packed struct types** (`<{ … }>`), not globally 1-byte + scalar alignment. `-fpack-struct=1` is the spike's approximation; the target + should make packed structs the default. +- `char` signed; `enum` unsigned 32-bit; `_Bool` 8-bit (object alignment 2, + member alignment 1). `long long` is 64-bit but has no TI oracle. +- `_Alignof`/`alignof`: scalar objects report 2 bytes; a packed struct reports + 1 in the frontend because its LLVM type is `<{...}>`. The backend still aligns + standalone globals, allocas and frame objects to 2 bytes. TI C89 has no + `_Alignof`, so this is a documented modern-C difference, not an ABI break for + ROM code; a custom AST layout hook can revisit it if user code depends on + `_Alignof(struct)`. +- `Bool` in Xlib is `int`, unaffected. + +### 7.3 Bit-address semantics in clang/LLVM + +The spike established the split: + +- **GEPs and pointer arithmetic**: a `tms-bitaddr` project tool rewrites every + `getelementptr` instruction and constant-expression GEP to + `base + (byte_offset << 3)`. It must run before clang's optimizer; start with + a driver step (`clang -O0 -emit-llvm` → `tms-bitaddr --scale` → `opt -O2` + → `tms-bitaddr --normalize`) and integrate at an early pass-builder extension + point later. +- **Pointer difference**: patch `ScalarExprEmitter::EmitSub` in + `clang/lib/CodeGen/CGExprScalar.cpp`. The raw difference of two bit addresses + is a bit count; the divisor must be `sizeof(T) * 8`, including `char` where + the byte-machine division disappears. A pure IR pass cannot distinguish + `p - q` from `(int)p - (int)q`, so this must happen where the AST still knows + the expression is a pointer subtraction. +- **Sizes stay in bytes**: `sizeof`, `offsetof`, `memcpy`/`memset` lengths, + `alloca` sizes and `lifetime` sizes are byte quantities. The backend + multiplies by 8 when converting them to bit counts or bit addresses. +- **Pointer-to-integer casts** keep their bit-address values; no scaling. + +Tests: `p + 1`, `p - q`, `(int)(p + 1) - (int)p`, arrays, structs, function +pointers, string literals, global initializers. FileCheck the IR, then execute +the same cases on MAME. + +#### 7.3.1 GEP contract after the pass + +The pass runs before optimization, and later passes can recreate GEPs. +`InstCombine::visitIntToPtr` turns `inttoptr(add(ptrtoint Base, Off))` back into +`getelementptr i8, Base, Off` when the pointer is only compared or cast, and +`CodeGenPrepare` can sink addresses into GEPs. Those GEPs carry an offset that +is already in **bits**, not bytes. The design therefore fixes one contract: + +- After the pass, every IR address is a bit address. A GEP offset is an address + offset **in bits**. The backend must never apply `DataLayout` type-size + scaling to a GEP offset. +- The target overrides `TargetSubtargetInfo::addrSinkUsingGEPs()` to `false`, so + `CodeGenPrepare` does not introduce byte-offset GEPs. +- A late pre-ISel normalization pass canonicalizes any remaining GEP (including + ones created by InstCombine) into `base + offset` with the offset treated as + bits, and verifies that no typed GEP with a byte addend survives. +- ISel lowers a GEP as `base + offset`, not via + `DataLayout::getIndexedOffsetInType` byte arithmetic. + +This must be tested with `llc` on pointer-arithmetic programs, not only with +`opt`; the spike stopped before ISel. A wrong choice here miscompiles every +non-constant pointer expression by 8×. + +### 7.4 ABI implementation + +- Custom calling convention `CC_TMS34010`. +- `LowerFormalArguments` / `LowerCall` / `LowerReturn` implement the caller and + callee rules in §4.2 exactly; `sret` and `byval` follow the TI layout. +- **Explicit arguments**: pushed on the program stack (A14) in reverse order, + each widened to 32 bits. The first explicit argument is at `FP(-32)`, the + second at `FP(-64)`, etc. +- **Struct return**: the caller pushes the return-space address **after** the + explicit arguments, so it is the top program-stack slot: the callee reads it + at `FP(-32)` and the first explicit argument is at `FP(-64)`. +- **Old STK**: the caller saves A14 on the system stack only when it passes + arguments or the callee returns a float/double. A no-argument function with + locals has no saved old STK; the callee restores STK from FP before `RETS 0`. +- **System stack layout** (grows down), after the callee's `MMTM` of `n` + registers: + + ```text + SP + (n+1)*32 old STK (if saved) + SP + n*32 return PC + SP + ... saved registers + SP top of system stack + ``` + + `RETS N` loads PC and advances SP by `32 + 16N` bits in total (the 32 is the + PC itself); `RETS 2` is the PC + old-STK case (64 bits), `RETS 0` is PC only + (32 bits). +- **Frame**: `FP = STK`; `ADDI frame_bits,STK`; locals at positive FP offsets, + arguments at negative offsets. Frame size, saved-register offsets and `ADDI` + immediates are in bits. Prologue/epilogue use `MMTM`/`MMFM` and `RETS`. +- `FS1 = 32` is an invariant at function entry and after calls; a target pass + or pseudo-instruction inserts `SETF 32,0,1` after call sites and before + field-size-dependent operations when the state is unknown. +- `CALL`/`CALLA`/`CALLR` selection: direct calls to local functions may use + `CALLR`; external/ROM calls use `CALLA`; function pointers use `CALL Rs`. +- Symbol prefix `_`; sections `.text`/`.data`/`.bss`; initialized globals go to + `.data` for the payload path (the loader copies sections), so `.cinit` is + not required initially. ROM builds may add `.cinit` later. +- ROM import library: `symbols/vt1200-v22.syms` maps `_XOpenDisplay`, + `_XCreateSimpleWindow`, `_XMapWindow`, `_XSync`, `_strlen`, `_malloc`, `_free` + and later entries to bit addresses, as linker absolute symbols or + ABI-preserving stubs. The file starts with the entries already proven by + `xhello.asm`; the M3 task extends it from Ghidra/`ptr_constants.py` and + validates each one. v2.1 and other revisions get separate tables; addresses + are not assumed stable. + +### 7.5 Backend + +`llvm/lib/Target/TMS34010/`: + +- Target machine, subtarget, register info, instruction info, frame lowering, + ISel lowering and DAG-to-DAG selection, asm printer, MC instruction lowering, + `MCTargetDesc` (MCAsmInfo, MCInstrInfo, MCRegisterInfo, MCSubtargetInfo, + MCCodeEmitter, MCAsmBackend), target-info registration. +- TableGen: `TMS34010.td`, `TMS34010RegisterInfo.td`, `TMS34010InstrInfo.td`, + `TMS34010CallingConv.td`. +- M1 instruction subset: `MOVI`, `MOVE`, `MOVB`, `ADD`, `SUB`, `AND`, `OR`, + `XOR`, `CMP`, `ADDK`, `SUBK`, `SLL`, `SRL`, `SRA`, `MPYS`, `MPYU`, `DIVS`, + `DIVU`, `JR`, `JA`, `CALL`, `CALLA`, `CALLR`, `RETS`, `SETF`, `MMTM`, + `MMFM`, `CLR`, `NOP`. +- Encodings are 1–3 16-bit words; implement a custom MC encoder/decoder where + TableGen's fixed-width assumptions do not fit. Validate against `dis34`, + MAME, and the byte-exact ROM round-trip. +- Loads/stores: misaligned accesses allowed; `MOVB` for i8, `MOVE` with FS0 for + i16, `MOVE` with FS1 for i32/pointers; i64 as two 32-bit accesses. Ensure + FS1 is 32; use FS0 for 16-bit. +- `memcpy`/`memmove`/`memset`: lower to byte/word loops; lengths are bytes, + addresses bits; overlap semantics for `memmove`. +- `alloca`/VLA: size ×8; 16-bit alignment for standalone objects. +- `byval`: copy `sizeof` rounded to 16 bits. +- Multiplication/division: 16×16 `MPYS`/`MPYU`; 32×32 and 64-bit via libcalls + or sequences. Float/double via libcalls to `flib.lib` or compiler-rt later. +- Branches: `JR` range, `JA` absolute; branch relaxation. +- Constants: `MOVI` long/short, `ADDK`/`SUBK` small immediates, constant pool + for large/float constants. +- varargs and bitfields are out of M1; add once the Xlib path works. + +### 7.6 Output path + +- M1: emit GSPA-compatible assembly; assemble and link with `gspa`/`gsplnk` + under 2ine; feed the existing COFF loader. +- Respect GSPA syntax: hex constants `0…h`, `movi` long/short forms, `sp` vs + `b15`, `JR` vs `JA` word counts (see `tools/tms34010-sdk/` notes and + `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. +- 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. + +### 7.7 Runtime and libraries + +- Reuse `rts.lib`/`flib.lib` and the ROM Xlib; do not write a libc first. +- Xlib headers: X11R4-compatible; `XLIB_ILLEGAL_ACCESS`/`_XPrivDisplay` exposes + `Display` layout for `ScreenOfDisplay`; `Screen`/`XImage`/`XEvent` definitions + must match the ROM. Core Xlib structs are all 4-byte fields, so packed and + 4-byte alignment give the same offsets; verify with the TI guide and a + compiled offset test. +- ROM imports: Xlib, `strlen`, `malloc`, `free`, etc. at fixed bit addresses. + `malloc` returns a bit address. `XOpenDisplay` must be called from a + `create_process` child, not the rdebug dispatcher (repo evidence). +- Startup for payloads: `entry.asm`-style `_start` sets STK/FP, calls `_main`, + `rets`; no `.cinit`. Standalone `_c_int00`/`.cinit` later. +- Initialized globals go in `.data`; the loader copies them. + +### 7.8 Tooling + +- `tms-clang` wrapper: clang → bit-address pass → optimizer → asm → GSPA/gsplnk + → COFF; `tms-run` for MAME or rdebug. +- Build scripts and patch series; CI builds LLVM/clang once, caches it, runs lit + and MAME smoke tests. +- Symbol map and line-table output first; DWARF/GDB later. + +### 7.9 Testing + +- `lit`: clang IR semantics, llc instruction selection/encoding, asm printer. +- Encoder/decoder round-trip against `dis34`; byte-exact reassembly of a ROM + region. +- **GEP-contract test**: M0 gets an `opt`-level late-GEP normalization and + 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. +- 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, + not a CI dependency; the unit is not always available. +- Differential: compile the same C with TI 5.01 (and 2.01) **offline**, store + `.s`/`.map`/behaviour as golden files, and compare sizes, struct offsets and + call results in CI. CI does not need DOSBox or 2ine. +- ABI conformance: call ROM `strlen`, `malloc`, `XOpenDisplay`; be called from + a ROM-created process; pass structs/pointers. +- Xlib: `XOpenDisplay`, `XCreateSimpleWindow`, `XMapWindow`, `XSync`, `XEvent`, + `XImage`, `XTextProperty`; verify struct offsets against a TI-compiled + offset test before the first call. + +## 8. Milestones + +**M0 — fork and semantics (1–2 weeks).** LLVM 23.1.5 builds; clang target emits +IR; bit-address pass and pointer-difference patch pass lit tests; spike re-run +on 23.1.5. Acceptance: `int_diff` = 8, `int_diff4` = 32, `p - q` = bytes, +Xlib client IR has no GEPs and verifies; an `opt`-level late-GEP normalization +test confirms the §7.3.1 contract. The `llc` half of that contract test lands in +M1, when the backend exists. + +**M1 — minimal backend (2–4 weeks).** `int add(int,int)` and `ret`; llc emits +assembly; assemble/link/load in MAME; `int load(int*)`; `char` load/store; +constant-GEP relocation probe. Acceptance: marker word and return value correct +in MAME. + +**M2 — ABI and ROM calls (2–4 weeks).** Arguments/locals, callee saves, FP/STK, +`RETS`, FS1; call ROM `strlen`; call a function with two arguments; no-arg +function with locals. Acceptance: `strlen("0::0") == 4` in MAME (real unit +when available). + +**M3 — Xlib (2–4 weeks).** Extend and validate `symbols/vt1200-v22.syms` from +Ghidra; `XOpenDisplay`, `XCreateSimpleWindow`, `XMapWindow`, `XSync`; +`Display`/`Screen` macros; X11 header offset check. Acceptance: a window +appears in MAME (real unit when available). + +**M4 — real client (2–4 weeks).** `hello.c` and a small `xshow`-like client; +`XImage`/`XEvent`; structs and function pointers. Acceptance: image on screen, +no X errors. + +**M5 — robustness and native path.** `memcpy`, `alloca`, `byval`, varargs +(`sprintf`), floats, bitfields, `long long`; native COFF and debug info. + +## 9. Risks and mitigations + +| risk | mitigation | +|---|---| +| clang fork drifts | pin 23.1.5; keep the generic hook and pointer patch small; export patches | +| late GEPs / double scaling | §7.3.1 contract: bit offsets only, disable `addrSinkUsingGEPs`, late normalization + `llc` verifier | +| constant-GEP relocation (`symbol+bit addend`) | M1 probe; fallbacks: leave to link time, runtime add, or pre-resolve | +| dual-stack frame lowering | prototype ABI in M2 with a ROM call before Xlib | +| FS1 clobbering | model it as a pseudo-instruction/after-call reset; ABI test | +| instruction encoding errors | byte-exact ROM round-trip plus `dis34`/MAME | +| packed struct/bitfield mismatch | offset tests against TI; defer bitfields | +| X11 header mismatch | X11R4 headers or system headers; compiled offset test before M3 | +| ROM symbol extraction | `symbols/vt1200-v22.syms` starts with proven entries; extend/validate in M3 | +| missing assets/oracle | `scripts/fetch-vt1200.sh`; TI oracle is offline/golden files; hardware is periodic | +| GSPA/2ine dependency | acceptable initially; native `as34`/`ld34` + converter fallback | +| ROM address changes | versioned import tables; v2.2 first | +| LLVM 23 API changes | pin; spike on 19.1.7 is reference only | +| code size/performance | measure `-O2`/`-Os` against TI `-o2`; tune later | + +## 10. Open decisions left to the builder + +- Exact bitfield layout (defer). +- Whether to add a custom AST/record-layout hook so `_Alignof(struct)` reports + TI's 2-byte object alignment instead of LLVM's packed 1. +- X11R4 headers from X.Org vs. the system X11R7 headers. +- Scope of ROM symbol extraction for M3 (known Xlib entries vs. full table). +- B-register policy: A-only initially; later allocate B0–B14 as caller-saved. +- `CALLR` vs `CALLA` for local calls. +- `.data` vs `.cinit` on the ROM-image path. +- Native COFF timeline and object-writer scope. +- Debug-info format/scope. +- Varargs/float priority after M4. +- Whether to attempt upstreaming the address-unit hook. + +## 11. Size of work + +This is a project, not a small change: multiple months for a complete toolchain. +A first useful Xlib client is realistically 2–3 months with one experienced +LLVM engineer; M1/M2 (backend skeleton and dual-stack ABI) carry most of the +risk. The bit-address work is the smallest part; the backend and ABI are the +bulk. + +## 12. References + +- TI, *TMS34010 C Compiler Reference Guide* (SPVU005A, 1988-08), §4.2, §4.3, + §5.1.6, §5.2, §5.3. +- `scripts/fetch-vt1200.sh`; paths below are relative to `vendor/vt1200` + (pinned commit `17dfd24…`). +- `harness/c-hello/build/hello.s` (TI 2.01 codegen), `dwt/gui/xhello.asm` + (Xlib call sequence), `harness/c-hello/README.md` (pointer trap). +- `spike/REPORT.md`, `spike/bitscale.cpp`, `spike/run.sh`. +- `tools/as34/README.md`, `tools/tms34010-sdk/OS2-501.md`, + `tools/tms34010-sdk/2INE-501.md`, `reference/tms34010-sdk.md`, + `docs/TMS34010-users-guide.txt`. +- `symbols/vt1200-v22.syms`, `symbols/README.md`. +- X11R4 headers: X.Org release, or system X11R7 headers; validate the core + struct layouts. +- LLVM 23.1.5: `clang/lib/CodeGen/CGExprScalar.cpp` + (`EmitPointerArithmetic`, `EmitSub`), `clang/lib/CodeGen/CGExpr.cpp` + (`EmitArraySubscriptExpr`, GEP emission), `llvm/include/llvm/IR/DataLayout.h`, + `llvm/lib/Transforms/InstCombine/InstCombineCasts.cpp` and + `llvm/lib/CodeGen/CodeGenPrepare.cpp` for the late-GEP contract. +- MAME `src/devices/cpu/tms34010/34010ops.hxx`, `tools/smart34010/dis34.c`. diff --git a/scripts/fetch-vt1200.sh b/scripts/fetch-vt1200.sh new file mode 100755 index 0000000..22d2e9e --- /dev/null +++ b/scripts/fetch-vt1200.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Fetch the VT1200 reverse-engineering tree at the pinned commit. +# +# The compiler design uses this tree as its ABI/oracle/loader source: +# harness/c-hello TI C -> COFF -> real unit +# dwt/loader COFF loader and hostload +# dwt/gui xhello.asm, the Xlib call sequence +# tools/as34 native assembler/linker +# tools/smart34010 dis34 +# tools/tms34010-sdk TI SDKs +# reference, docs manuals and notes +# +# It is NOT vendored into the compiler repo; this script is the reproducible +# way to materialise it. Override the destination with $1. +set -eu + +DEST=${1:-vendor/vt1200} +REPO=https://git.k-space.ee/arti/vt1200 +COMMIT=17dfd24340fe3346433f90457cb48cb8786dff39 + +if [ ! -d "$DEST/.git" ]; then + mkdir -p "$(dirname "$DEST")" + git clone --filter=blob:none --no-checkout "$REPO" "$DEST" +fi + +cd "$DEST" +git fetch --depth 1 origin "$COMMIT" 2>/dev/null || true +git checkout --detach "$COMMIT" +git sparse-checkout init --cone +git sparse-checkout set docs reference tools harness dwt +printf '%s\n' "$COMMIT" > VENDORED_AT + +echo "vt1200 tree at $DEST, commit $COMMIT" +du -sh . diff --git a/spike/REPORT.md b/spike/REPORT.md new file mode 100644 index 0000000..9729ea3 --- /dev/null +++ b/spike/REPORT.md @@ -0,0 +1,277 @@ +# Bit-addressed pointers in LLVM — throwaway spike + +**Status: spike only. Nothing here is a compiler, and nothing was run on a +TMS34010.** The purpose was to measure one question: if a TMS34010 target keeps +TI's pointer representation (a pointer is a 32-bit *bit* address), how much of +LLVM has to change, and where does it hurt? + +Measured 2026-11-10 against LLVM 19.1.7 on x86-64. The LLVM source is checked +out in `vendor/llvm-project` at tag `llvmorg-19.1.7` (commit `cd70802`), sparse +(134 MB); the host toolchain is Debian's `clang-19`/`llvm-19-dev`. + +## The question, restated + +TI's C ABI does not use byte addresses. From the 1988 *TMS34010 C Compiler +Reference Guide*: + +> All objects except structure members and array members are aligned on a +> 16-bit (one-word) boundary. ... Structure or array members are not aligned to +> 16-bit boundaries. (§4.3) + +and the generated code in `harness/c-hello/build/hello.s` shows the arithmetic: +a `char` step is `SLL 3` (×8), a 32-byte array row is `SLL 8` (×256), and +`.bss STATIC_3,18432,16` is 18432 **bits** for 2304 bytes. + +LLVM's IR and `DataLayout` define the address unit as the 8-bit byte: +`getelementptr i8, ptr %p, i64 1` advances one unit. There is no `DataLayout` +knob for "one bit per address unit" while keeping `CHAR_BIT == 8`. + +## What was built + +* `xclient.c` — a small but pointer-heavy Xlib client: `XOpenDisplay`, + `XCreateWindow`, `XCreateImage`, `XPutImage`, `XNextEvent`, + `XLookupString`, `XGetWMName`/`XmbTextPropertyToTextList`, `XSetClassHint`, + `XSetWMProperties`, plus the standard `RootWindow`/`DefaultVisual`/ + `DefaultGC`/`BlackPixel` macros. It touches `Display->screens`, + `Screen->root_visual`/`root`/`default_gc`, `XImage->data` and + `XImage->f.put_pixel`, `XEvent.xkey.display`, `XTextProperty.value`, and + `XClassHint.res_name`/`res_class`. +* `stress.c` — the IR shapes the client does not have: a constant-expression + GEP (`"hello" + 2`), a by-value struct argument, varargs, a VLA, a + jump-table `switch`, and a 64-bit divide. +* `ptrdiff.c` — pointer arithmetic edge cases. +* `layout.c` — TI's packed struct layout. +* `bitscale.cpp` — a standalone LLVM IR tool that rewrites every + non-constant GEP to `base + 8*(byte offset)`, including constant-expression + GEPs. It leaves loads/stores, `alloca`, `memcpy` and `lifetime` alone. +* `xlib_min.h` — minimal Xlib declarations (32-bit `long` and pointers, + packed structs where appropriate). + +Build and run: `./run.sh` (or see the commands at the end). + +## Result 1 — the GEP transform is small and works + +`bitscale` is about 30 lines of real logic: + +```c++ +// for every getelementptr: base_int + (byte_offset << 3) +Value *BaseInt = B.CreatePtrToInt(Base, i32); +Value *BitOff = B.CreateShl(ByteOffset, 3); +Value *NewPtr = B.CreateIntToPtr(B.CreateAdd(BaseInt, BitOff), ptr); +``` + +On `xclient.c`: + +| IR | GEPs | allocas | memintrinsics | loads/stores | ptr subtractions | +|---|---:|---:|---:|---:|---:| +| `xclient.O0.ll` | 61 | 35 | 1 | 155 / 54 | 0 | +| `xclient.O2.ll` | 35 | 10 | 3 | 49 / 24 | 0 | +| `xclient.packed.O2.ll` | 35 | 10 | 3 | 49 / 24 | 0 | +| after `bitscale` | **0** | 10 | 3 | 49 / 24 | 0 | + +`opt -passes=verify` accepts the result. The Xlib object chain survives as +expected. For `DefaultScreen`/`RootWindow`/`DefaultVisual`, the IR becomes: + +```llvm +%bit.base = ptrtoint ptr %dpy to i32 +%bit.screens = add i32 %bit.base, 1056 ; Display->screens at byte 132, ×8 +%p = inttoptr i32 %bit.screens to ptr +%scr = load ptr, ptr %p +%bit.base2 = ptrtoint ptr %scr to i32 +%off = mul i32 %scr_index, 640 ; sizeof(Screen)=80 bytes, ×8 +%rootv = add i32 %bit.base2, %off +... + 320 for Screen->root_visual (byte 40), +64 for root (byte 8), ... +``` + +Those constants are exactly TI's bit addresses: `Display->screens` is byte 132 +→ bit 1056; `Screen` is 80 bytes → 640 bits; `root_visual` is byte 40 → 320 +bits. `instcombine` at `-O2` does **not** reintroduce GEPs in this corpus, and +the scaled constants survive. + +## Result 2 — the optimizer order matters, and pointer subtraction is the trap + +The GEP transform must run **before** clang's optimizer, not after. `ptrdiff.c` +shows why. The practical sequence is `clang -O0 -emit-llvm` (with +`-Xclang -disable-O0-optnone`) → `bitscale` → `opt -O2`, or a clang pass plugin +at an early extension point. `xclient.c` also survives the late order because +it has no pointer casts or subtractions, but that is not a general guarantee. + +```c +int int_diff(char *p) { return (int)(p + 1) - (int)p; } +int int_diff4(int *p) { return (int)(p + 1) - (int)p; } +long ptr_diff(char *a, char *b) { return a - b; } +``` + +* At `-O2` **before** scaling, clang folds `int_diff` to `1` and `int_diff4` to + `4`; the GEP is already gone, so a late pass cannot fix it. +* At `-O0` **then** `bitscale` **then** `-O2`, they become **8** and **32** — + correct. +* `addr_of_next` becomes `base + 8` — correct. + +That is the good news. The bad news is pointer subtraction: + +```llvm +; ptr_diff(char *a, char *b), after bitscale + -O2 +%a = ptrtoint ptr %0 to i32 +%b = ptrtoint ptr %1 to i32 +%s = sub i32 %a, %b +ret i32 %s ; bits, but C wants bytes +``` + +```llvm +; pdiff_int(int *a, int *b), after bitscale + -O2 +%s = sub i32 %a, %b +%d = ashr exact i32 %s, 2 ; clang's /4; TI needs /32 +``` + +`bitscale` cannot repair these, and a pure IR pass **cannot reliably tell them +apart from an integer cast**: + +```c +(int)(p + 1) - (int)p // same IR shape: sub(ptrtoint, ptrtoint) +p - q // C pointer difference: must be divided by 8 +``` + +There is also the constant-expression case: `stride()` in `layout.c` is +`(char *)&arr[1] - (char *)&arr[0]`; clang folds it to `9` at `-O2` (correct), +but if you scale the constant GEP first it becomes `72`, and only a +frontend-aware pointer-difference rewrite can turn it back into `9`. + +**Consequence:** a GEP-scaling pass is enough for a client like `xclient.c`, +which contains **zero** pointer subtractions. A general C compiler needs either: + +1. a small **clang CodeGen change** that scales GEPs and emits the pointer + difference divisor as `sizeof(T) * 8` (the clean solution), or +2. a **frontend-generated marker/metadata** on pointer-difference `sub`s so a + later IR pass can distinguish them from explicit integer casts, or +3. a documented dialect restriction that `(int)(p+n) - (int)p` is undefined / + unsupported (not acceptable for ordinary C). + +This is the modern analogue of Andrew Morton's warning: the frontend, not just +the backend, assumes `(char *)p + 1 == (long)p + 1`. In LLVM the damage is +concentrated in `clang/lib/CodeGen/CGExpr.cpp` (GEP and pointer-difference +emission), not spread across 110 GCC 2.5.8 sites. + +## Result 3 — struct layout is solvable, and TI's rule is fully packed + +TI packs structure members at bit granularity and aligns only the structure +itself to 16 bits. From `layout.c`: + +| | `sizeof(S)` | `offsetof(i)` | `offsetof(p)` | stride of `S[2]` | +|---|---:|---:|---:|---:| +| clang i386 default | 12 | 4 | 8 | 12 | +| `clang -fpack-struct=1` | **9** | **1** | **5** | **9** | +| TI guide (§4.3, §5.1.6) | 9 | 1 | 5 | 9 | + +So the correct LLVM model is **packed struct types** (`%struct.S = type <{ i8, +i32, ptr }>`) with scalar object alignment of 16 bits — not "make every scalar +1-byte aligned". `-fpack-struct=1` reproduces it for the spike; a real target +sets the same policy in its `TargetInfo`/`ASTContext` (and must decide what +`_Alignof(int)` reports, because TI aligns objects, not members). + +For the Xlib structs in this client (`Display`, `Screen`, `XImage`, `XEvent`, +`XTextProperty`, `XClassHint`, `XSizeHints`) all members are 4 bytes, so packed +and 4-byte alignments give the same offsets. User structs and X11 extensions +with a `char` before an `int` will differ. + +## Result 4 — the TI ABI itself is the non-standard part + +The pointer model is only half of "TI-compatible". From the C guide §5.2–5.3 +and `dwt/gui/xhello.asm`: + +* Return value in **A8**; **A13 = FP**, **A14 = STK** (program stack), **A15 = + system stack** (interrupt stack). +* Arguments are pushed **upward through A14** in reverse order and widened to + 32 bits. The caller first pushes the old A14 on **A15**; `CALLA` pushes the + return address on **A15**; `RETS 2` pops the return address and old STK. + Callee-saved registers are saved on **A15**, locals live on **A14**. +* **FS1 must be 32 inside C code**; Xlib clobbers the field-size state, so the + caller must re-establish `FS1=32` after every external call. `FS0` may be + changed without restoring. +* Structures are packed for layout but **rounded up to a word boundary when + passed by value**. + +This dual-stack convention is not a standard LLVM calling convention. LLVM's +`LowerCall`/`LowerReturn` and frame lowering can be customized for it, but the +backend must model A15 as a second stack (or use custom prologue/epilogue +nodes), and `FS1` behaves like a call-clobbered status register. That is more +work than the pointer scaling, but it is bounded and well documented. + +## What remains after the pass + +`bitscale` deliberately leaves these alone; each is a backend boundary: + +| IR construct | `xclient` count | what the backend must do | +|---|---:|---| +| `llvm.memcpy` of `XEvent` | 3 (96 bytes) | copy 96 × 8 bits | +| `alloca` | 10 | allocate `size × 8` bits, frame offsets in bits | +| `byval(%struct.S)` | 0 here, 1 in `stress.c` | copy `sizeof` bytes rounded to 16 bits | +| `llvm.va_start` | 0 here, 1 in `stress.c` | TI-compatible `va_list` if calling `sprintf` | +| indirect call `img->f.put_pixel` | 1 | function pointers are bit addresses; no change | +| constant-expression GEP | 0 here, 1 in `stress.c` | rewritten by `bitscale` | +| `align 4` hints | many | ignore or lower to TI's 2-byte object alignment | +| 64-bit divide | 0 here, 1 in `stress.c` | libcall or sequence (no 64-bit divide) | + +Loads and stores themselves are the easy part on this CPU: `MOVB` moves 8 bits +at any bit address, and `MOVE ...,FS1` moves 32 bits at any bit address, so a +bit-address `load i8`/`load i32` is one instruction, not a shift/mask sequence. + +## Verdict + +**Not horrible at the pointer level.** A 30-line IR pass scales every GEP in a +real Xlib client, LLVM verifies it, `-O2` preserves it, and the result is +directly interoperable with Xlib's bit-addressed structs. A byte-address model +would instead need conversions at every pointer field the standard macros +traverse (`screens`, `root_visual`, `XImage->data`, `put_pixel`, +`XEvent.xkey.display`, `XTextProperty.value`, …), which is worse. + +**The three real costs are:** + +1. **A clang CodeGen change for pointer difference** (and the GEP scaling, if + you want it before clang's optimizer). A pure post-IR pass cannot do it + correctly. This is a fork of clang, not just an LLVM backend. +2. **The dual-stack / FS1 TI ABI.** Custom call, return and frame lowering; + more work than the pointer model. +3. **The backend itself** — instruction selection, register allocation, asm + printer/relocations to `as34`/GSPA, `memcpy`/byval/varargs, and a C library + (reuse `rts.lib`/Xlib or build one). This is the usual target-port scale. + +None of these is a blocker. The spike says the bit-address question is answerable +with a small, well-localized change, and the remaining project is an ordinary +(if unusual) LLVM target plus the TI ABI. + +## Reproduce + +```sh +cd spike +clang++-19 -std=c++17 $(llvm-config-19 --cxxflags) bitscale.cpp \ + $(llvm-config-19 --ldflags --libs core irreader support --system-libs) -o bitscale + +clang-19 -target i386-unknown-none -ffreestanding -nostdinc -fno-builtin \ + -fpack-struct=1 -O2 -S -emit-llvm xclient.c -o xclient.packed.O2.ll +./bitscale xclient.packed.O2.ll xclient.packed.O2.bits.ll +opt-19 -passes=verify -disable-output xclient.packed.O2.bits.ll +opt-19 -passes='default' -S xclient.packed.O2.bits.ll -o /tmp/x.ll +grep -c getelementptr /tmp/x.ll # 0 +grep -o 'add i32 %bit.base[0-9]*, [0-9]*' /tmp/x.ll | head + +# early scaling is required for (int)(p+1) - (int)p: +clang-19 -target i386-unknown-none -ffreestanding -nostdinc -fno-builtin \ + -Xclang -disable-O0-optnone -O0 -S -emit-llvm ptrdiff.c -o ptrdiff.O0.ll +./bitscale ptrdiff.O0.ll ptrdiff.O0.bits.ll +opt-19 -passes='default' -S ptrdiff.O0.bits.ll -o /tmp/p.ll +grep -A2 'define.*@int_diff' /tmp/p.ll # ret i32 8 +grep -A2 'define.*@int_diff4' /tmp/p.ll # ret i32 32 +grep -A6 'define.*@ptr_diff' /tmp/p.ll # still bits: needs frontend fix +``` + +## Sources + +* TI, *TMS34010 C Compiler Reference Guide* (SPVU005A, 1988-08), §4.3, §5.1.6, + §5.2.1, §5.3. +* `harness/c-hello/build/hello.s` (TI 2.01 output: `SLL 3`, `SLL 8`, `.bss` in + bits). +* `dwt/gui/xhello.asm` (A8/A13/A14, `CALLA`, `FS0=16`/`FS1=32` after each call). +* LLVM 19.1.7 `llvm/include/llvm/IR/DataLayout.h` (`getTypeAllocSize` returns + bytes; `getIndexedOffsetInType` is byte-based) and + `clang/lib/CodeGen/CGExpr.cpp` (GEP and pointer-difference emission). diff --git a/spike/bitscale.cpp b/spike/bitscale.cpp new file mode 100644 index 0000000..2d71949 --- /dev/null +++ b/spike/bitscale.cpp @@ -0,0 +1,292 @@ +// bitscale -- throwaway spike tool. +// +// Rewrites every non-constant getelementptr so that its byte offset is +// multiplied by 8. The result is an IR in which every pointer value is a +// *bit* address, while loads/stores still carry their normal LLVM byte-sized +// types. A real TMS34010 backend would lower "load i8 at bit address A" as +// MOVB [A] and "load i32 at bit address A" as MOVE [A],FS0-in-32-bit-mode. +// +// Build: +// clang++-19 -std=c++17 $(llvm-config-19 --cxxflags) bitscale.cpp \ +// $(llvm-config-19 --ldflags --libs core irreader support) -o bitscale + +#include "llvm/IR/Constants.h" +#include "llvm/IR/IRBuilder.h" +#include "llvm/IR/DataLayout.h" +#include "llvm/IR/GetElementPtrTypeIterator.h" +#include "llvm/IR/Instructions.h" +#include "llvm/IR/LLVMContext.h" +#include "llvm/IR/Module.h" +#include "llvm/IR/Operator.h" +#include "llvm/IRReader/IRReader.h" +#include "llvm/Support/SourceMgr.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include + +using namespace llvm; + +struct Stats { + unsigned geps = 0; + unsigned const_geps = 0; + unsigned vector_geps = 0; + unsigned allocas = 0; + unsigned memintrinsics = 0; + unsigned vaarg = 0; + unsigned byval_args = 0; + unsigned indirect_calls = 0; + unsigned loads = 0; + unsigned stores = 0; + unsigned ptr_subs = 0; + unsigned const_ptr_subs = 0; +}; + +static void count_constants(Value *V, Stats &S, unsigned depth = 0) +{ + if (!V || depth > 64) + return; + auto *C = dyn_cast(V); + if (!C) + return; + if (auto *CE = dyn_cast(C)) { + if (CE->getOpcode() == Instruction::Sub) { + auto *L = dyn_cast(CE->getOperand(0)); + auto *R = dyn_cast(CE->getOperand(1)); + if (L && R && L->getOpcode() == Instruction::PtrToInt && + R->getOpcode() == Instruction::PtrToInt) + ++S.const_ptr_subs; + } + } + if (auto *GEP = dyn_cast(C)) { + ++S.const_geps; + count_constants(GEP->getPointerOperand(), S, depth + 1); + for (auto &Op : GEP->indices()) + count_constants(Op.get(), S, depth + 1); + return; + } + for (auto &Op : C->operands()) + count_constants(Op.get(), S, depth + 1); +} + +static void count_constant_geps(const Constant *C, Stats &S, unsigned depth = 0) +{ + count_constants(const_cast(C), S, depth); +} + +static Stats collect(Module &M) +{ + Stats S; + for (Function &F : M) { + for (Argument &A : F.args()) + if (A.hasAttribute(Attribute::ByVal)) + ++S.byval_args; + for (BasicBlock &BB : F) { + for (Instruction &I : BB) { + if (auto *GEP = dyn_cast(&I)) { + if (GEP->getType()->isVectorTy()) + ++S.vector_geps; + else + ++S.geps; + } + if (isa(&I)) + ++S.allocas; + if (isa(&I)) + ++S.loads; + if (isa(&I)) + ++S.stores; + if (auto *BO = dyn_cast(&I)) { + if (BO->getOpcode() == Instruction::Sub && + isa(BO->getOperand(0)) && + isa(BO->getOperand(1))) + ++S.ptr_subs; + } + if (auto *CB = dyn_cast(&I)) { + if (Function *Callee = CB->getCalledFunction()) { + if (Callee->isIntrinsic()) { + StringRef N = Callee->getName(); + if (N.starts_with("llvm.memcpy") || N.starts_with("llvm.memmove") || + N.starts_with("llvm.memset")) + ++S.memintrinsics; + if (N.starts_with("llvm.va_start") || N.starts_with("llvm.va_arg")) + ++S.vaarg; + } + } else { + ++S.indirect_calls; + } + for (unsigned i = 0; i < CB->arg_size(); ++i) + if (CB->paramHasAttr(i, Attribute::ByVal)) + ++S.byval_args; + } + for (Value *Op : I.operands()) + count_constants(Op, S); + } + } + } + for (GlobalVariable &G : M.globals()) + if (G.hasInitializer()) + count_constant_geps(G.getInitializer(), S); + return S; +} + +static bool scale_constant_gep(ConstantExpr *CE, const DataLayout &DL) +{ + if (CE->getType()->isVectorTy()) + return false; + + auto *GEP = cast(CE); + SmallVector Indices; + for (auto &Op : GEP->indices()) + Indices.push_back(Op.get()); + for (Value *Idx : Indices) + if (!isa(Idx)) + return false; // cannot fold a non-constant constant-expression GEP + + int64_t Off = DL.getIndexedOffsetInType(GEP->getSourceElementType(), Indices); + Constant *Base = cast(GEP->getPointerOperand()); + Constant *BaseInt = ConstantExpr::getPtrToInt(Base, Type::getInt32Ty(CE->getContext())); + Constant *BitOff = ConstantInt::get(Type::getInt32Ty(CE->getContext()), Off * 8); + Constant *BitAddr = ConstantExpr::getAdd(BaseInt, BitOff); + Constant *New = ConstantExpr::getIntToPtr(BitAddr, CE->getType()); + CE->replaceAllUsesWith(New); + return true; +} + +// base_int + (byte offset computed from the GEP's indices) << 3 +static bool scale_gep(GetElementPtrInst *GEP) +{ + if (GEP->getType()->isVectorTy()) + return false; + Value *Base = GEP->getPointerOperand(); + Type *PtrTy = GEP->getType(); + const DataLayout &DL = GEP->getModule()->getDataLayout(); + + IRBuilder<> B(GEP); + Value *BaseInt = B.CreatePtrToInt(Base, B.getInt32Ty(), "bit.base"); + Value *ByteOff = ConstantInt::get(B.getInt32Ty(), 0); + + for (auto GTI = gep_type_begin(GEP), GTE = gep_type_end(GEP); GTI != GTE; ++GTI) { + Value *Idx = GTI.getOperand(); + Value *Idx32 = B.CreateSExtOrTrunc(Idx, B.getInt32Ty()); + if (StructType *STy = GTI.getStructTypeOrNull()) { + unsigned FieldNo = cast(Idx)->getZExtValue(); + uint64_t FieldOff = DL.getStructLayout(STy)->getElementOffset(FieldNo); + ByteOff = B.CreateAdd(ByteOff, ConstantInt::get(B.getInt32Ty(), FieldOff)); + } else { + uint64_t Stride = GTI.getSequentialElementStride(DL); + Value *Scaled = B.CreateMul(Idx32, ConstantInt::get(B.getInt32Ty(), Stride)); + ByteOff = B.CreateAdd(ByteOff, Scaled); + } + } + + Value *BitOff = B.CreateShl(ByteOff, 3, "bit.off"); + Value *BitAddr = B.CreateAdd(BaseInt, BitOff, "bit.addr"); + Value *NewPtr = B.CreateIntToPtr(BitAddr, PtrTy, "bit.ptr"); + + GEP->replaceAllUsesWith(NewPtr); + GEP->eraseFromParent(); + return true; +} + +static void collect_const_geps(Value *V, std::vector &Out, + unsigned depth = 0) +{ + if (!V || depth > 64) + return; + auto *C = dyn_cast(V); + if (!C) + return; + if (auto *CE = dyn_cast(C)) { + if (CE->getOpcode() == Instruction::GetElementPtr && !CE->getType()->isVectorTy()) { + Out.push_back(CE); + return; + } + } + for (auto &Op : C->operands()) + collect_const_geps(Op.get(), Out, depth + 1); +} + +int main(int argc, char **argv) +{ + if (argc != 3) { + errs() << "usage: " << argv[0] << " input.ll output.ll\n"; + return 2; + } + + LLVMContext Ctx; + SMDiagnostic Err; + std::unique_ptr M = parseIRFile(argv[1], Err, Ctx); + if (!M) { + Err.print(argv[0], errs()); + return 1; + } + + Stats Before = collect(*M); + + // Constant-expression GEPs (e.g. `"hello" + 2`) cannot be replaced by an + // instruction, so rewrite them as constant inttoptr(add(ptrtoint, 8*off)). + unsigned ConstScaled = 0; + const DataLayout &DL = M->getDataLayout(); + bool Changed = true; + while (Changed) { + Changed = false; + std::vector Work; + for (GlobalVariable &G : M->globals()) + if (G.hasInitializer()) + collect_const_geps(G.getInitializer(), Work); + for (Function &F : *M) + for (BasicBlock &BB : F) + for (Instruction &I : BB) + for (Value *Op : I.operands()) + collect_const_geps(Op, Work); + + std::set Seen; + for (ConstantExpr *CE : Work) { + if (!Seen.insert(CE).second) + continue; + if (scale_constant_gep(CE, DL)) { + ++ConstScaled; + Changed = true; + } + } + } + + unsigned Scaled = 0; + for (Function &F : *M) { + std::vector Work; + for (BasicBlock &BB : F) + for (Instruction &I : BB) + if (auto *GEP = dyn_cast(&I)) + Work.push_back(GEP); + for (GetElementPtrInst *GEP : Work) + if (scale_gep(GEP)) + ++Scaled; + } + Stats After = collect(*M); + + errs() << "bitscale: scaled " << Scaled << " GEPs, " << ConstScaled + << " constant GEPs\n"; + errs() << " before: geps=" << Before.geps << " const_geps=" << Before.const_geps + << " vector_geps=" << Before.vector_geps << " allocas=" << Before.allocas + << " memintrinsics=" << Before.memintrinsics << " va=" << Before.vaarg + << " byval=" << Before.byval_args << " indirect=" << Before.indirect_calls + << " loads=" << Before.loads << " stores=" << Before.stores + << " ptr_subs=" << Before.ptr_subs << " const_ptr_subs=" << Before.const_ptr_subs << "\n"; + errs() << " after: geps=" << After.geps << " const_geps=" << After.const_geps + << " vector_geps=" << After.vector_geps << " allocas=" << After.allocas + << " memintrinsics=" << After.memintrinsics << " va=" << After.vaarg + << " byval=" << After.byval_args << " indirect=" << After.indirect_calls + << " loads=" << After.loads << " stores=" << After.stores + << " ptr_subs=" << After.ptr_subs << " const_ptr_subs=" << After.const_ptr_subs << "\n"; + + std::error_code EC; + raw_fd_ostream Out(argv[2], EC); + if (EC) { + errs() << "cannot write " << argv[2] << ": " << EC.message() << "\n"; + return 1; + } + M->print(Out, nullptr); + return 0; +} diff --git a/spike/layout.c b/spike/layout.c new file mode 100644 index 0000000..1716eb6 --- /dev/null +++ b/spike/layout.c @@ -0,0 +1,12 @@ +struct S { + char c; + int i; + void *p; +}; + +struct S arr[2]; + +int size_S(void) { return (int)sizeof(struct S); } +int off_i(void) { return (int)__builtin_offsetof(struct S, i); } +int off_p(void) { return (int)__builtin_offsetof(struct S, p); } +int stride(void) { return (int)((char *)&arr[1] - (char *)&arr[0]); } diff --git a/spike/ptrdiff.c b/spike/ptrdiff.c new file mode 100644 index 0000000..a622f44 --- /dev/null +++ b/spike/ptrdiff.c @@ -0,0 +1,4 @@ +int int_diff(char *p) { return (int)(p + 1) - (int)p; } +int int_diff4(int *p) { return (int)(p + 1) - (int)p; } +long ptr_diff(char *a, char *b) { return a - b; } +int addr_of_next(char *p) { return (int)(p + 1); } diff --git a/spike/run.sh b/spike/run.sh new file mode 100755 index 0000000..fa772a2 --- /dev/null +++ b/spike/run.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Reproduce the bit-address spike. Throwaway: build artefacts are not +# interesting beyond the numbers in REPORT.md. +set -eu +cd "$(dirname "$0")" + +CC=clang-19 +CXX=clang++-19 +OPT=opt-19 +LLVMCONFIG=llvm-config-19 + +echo "== build bitscale" +$CXX -std=c++17 $($LLVMCONFIG --cxxflags) bitscale.cpp \ + $($LLVMCONFIG --ldflags --libs core irreader support --system-libs) \ + -o bitscale + +COMMON="-target i386-unknown-none -ffreestanding -nostdinc -fno-builtin -fno-discard-value-names" + +echo "== Xlib client" +$CC $COMMON -fpack-struct=1 -O2 -S -emit-llvm xclient.c -o xclient.packed.O2.ll +./bitscale xclient.packed.O2.ll xclient.packed.O2.bits.ll +$OPT -passes=verify -disable-output xclient.packed.O2.bits.ll +$OPT -passes='default' -S xclient.packed.O2.bits.ll -o /tmp/xclient.bits.O2.ll +printf 'GEPs after scaling+O2: ' +grep -c getelementptr /tmp/xclient.bits.O2.ll || true +printf 'memcpy count: ' +grep -c 'llvm.memcpy' /tmp/xclient.bits.O2.ll || true + +echo "== early scaling is required for pointer arithmetic" +$CC $COMMON -Xclang -disable-O0-optnone -O0 -S -emit-llvm ptrdiff.c -o ptrdiff.O0.ll +./bitscale ptrdiff.O0.ll ptrdiff.O0.bits.ll +$OPT -passes='default' -S ptrdiff.O0.bits.ll -o /tmp/ptrdiff.bits.O2.ll +grep -A2 'define.*@int_diff(' /tmp/ptrdiff.bits.O2.ll +grep -A2 'define.*@int_diff4' /tmp/ptrdiff.bits.O2.ll +echo "-- pointer difference still in bit units (needs frontend fix):" +grep -A6 'define.*@ptr_diff' /tmp/ptrdiff.bits.O2.ll + +echo "== constant GEP, byval, varargs, VLA" +$CC $COMMON -O2 -S -emit-llvm stress.c -o stress.O2.ll 2>/dev/null +./bitscale stress.O2.ll stress.O2.bits.ll +$OPT -passes=verify -disable-output stress.O2.bits.ll + +echo "== TI struct layout" +$CC $COMMON -O2 -S -emit-llvm layout.c -o layout.O2.ll +$CC $COMMON -fpack-struct=1 -O2 -S -emit-llvm layout.c -o layout.packed.O2.ll +echo "-- default:" +grep -A2 'define.*@size_S' layout.O2.ll | tail -1 +grep -A2 'define.*@off_i' layout.O2.ll | tail -1 +grep -A2 'define.*@stride' layout.O2.ll | tail -1 +echo "-- packed:" +grep -A2 'define.*@size_S' layout.packed.O2.ll | tail -1 +grep -A2 'define.*@off_i' layout.packed.O2.ll | tail -1 +grep -A2 'define.*@stride' layout.packed.O2.ll | tail -1 + +echo "done" diff --git a/spike/stress.c b/spike/stress.c new file mode 100644 index 0000000..3cd924c --- /dev/null +++ b/spike/stress.c @@ -0,0 +1,81 @@ +/* Extra IR shapes that the Xlib client does not contain. */ +typedef __builtin_va_list va_list; +#define va_start(ap, last) __builtin_va_start(ap, last) +#define va_arg(ap, type) __builtin_va_arg(ap, type) +#define va_end(ap) __builtin_va_end(ap) + +struct S { + char c; + int i; + void *p; +}; + +static char *file_ptr = "hello" + 2; /* constant-expression GEP */ +static char *tab[] = { "a", "bb", "ccc" }; /* pointer relocations */ +static int (*fns[])(int) = { 0, 0 }; /* function pointer table */ + +int take(struct S s) /* byval argument */ +{ + return s.c + s.i + (s.p != 0); +} + +struct S make(int i) +{ + struct S s; + s.c = (char)i; + s.i = i; + s.p = tab[i & 1]; + return s; /* aggregate return */ +} + +int sum(int n, ...) /* varargs */ +{ + va_list ap; + int s = 0; + va_start(ap, n); + while (n-- > 0) + s += va_arg(ap, int); + va_end(ap); + return s; +} + +int vla(int n) /* dynamic alloca */ +{ + int a[n]; + int i; + for (i = 0; i < n; i++) + a[i] = i * i; + return a[n - 1]; +} + +long long div64(long long a, long long b) /* libcall */ +{ + return a / b; +} + +int sw(int n) /* jump table */ +{ + switch (n) { + case 0: return 1; + case 1: return 2; + case 2: return 3; + case 3: return 5; + case 4: return 7; + case 5: return 11; + case 6: return 13; + case 7: return 17; + case 8: return 19; + case 9: return 23; + default: return 0; + } +} + +char *get_file_ptr(void) +{ + return file_ptr; +} + +char *pick(int i) +{ + return tab[i]; +} diff --git a/spike/xclient.c b/spike/xclient.c new file mode 100644 index 0000000..5400675 --- /dev/null +++ b/spike/xclient.c @@ -0,0 +1,136 @@ +/* + * A small but pointer-heavy Xlib client, used only to measure what + * bit-addressed pointers do to LLVM IR. + * + * It deliberately touches: + * - Display->screens[] and Screen->root_visual, root, default_gc + * - XImage->data and the XImage->f.put_pixel function pointer + * - XEvent->xkey.display and XEvent->xkey.keycode + * - XTextProperty.value and XClassHint.res_name/res_class + * - char * arithmetic over a scanline + * - a struct copy (llvm.memcpy) and an indirect call + */ +#include "xlib_min.h" + +_Static_assert(sizeof(void *) == 4, "spike wants 32-bit pointers"); +_Static_assert(sizeof(long) == 4, "TI long is 32 bits"); +_Static_assert(sizeof(int) == 4, "TI int is 32 bits"); +_Static_assert(sizeof(char) == 1, "TI char is 8 bits"); + +static void pattern(char *data, int bpl, int w, int h) +{ + int x, y; + + memset(data, 0, (unsigned long)bpl * h); + for (y = 0; y < h; y++) { + char *row = data + y * bpl; + for (x = 0; x < w; x++) + row[x >> 3] |= (char)(1 << (x & 7)); + } +} + +static int handle(Display *dpy, XEvent *ev) +{ + char buf[8]; + KeySym ks; + + switch (ev->type) { + case Expose: + return ev->xexpose.width * ev->xexpose.height; + case KeyPress: + XLookupString(&ev->xkey, buf, sizeof buf, &ks, 0); + if (ev->xkey.display != dpy) + return -1; + return (int)ev->xkey.keycode + buf[0] + (int)ks; + default: + return 0; + } +} + +int main(int argc, char **argv) +{ + Display *dpy; + int scr, w = 640, h = 480, bpl; + Window root, win; + Visual *vis; + GContext gc; + XImage *img; + XEvent ev, copy; + XTextProperty tp; + XClassHint ch; + XSizeHints hints; + XSetWindowAttributes attrs; + char *data; + char **list; + int count, i; + + dpy = XOpenDisplay(argc > 1 ? argv[1] : "0:0"); + if (!dpy) + return 1; + + scr = DefaultScreen(dpy); + root = RootWindow(dpy, scr); + vis = DefaultVisual(dpy, scr); + gc = DefaultGC(dpy, scr); + + attrs.background_pixel = WhitePixel(dpy, scr); + attrs.border_pixel = BlackPixel(dpy, scr); + attrs.colormap = 0; + win = XCreateWindow(dpy, root, 20, 20, (unsigned)w, (unsigned)h, 0, + DefaultDepth(dpy, scr), 1, vis, 0L, &attrs); + win = XCreateSimpleWindow(dpy, root, 20, 20, (unsigned)w, (unsigned)h, 0, + BlackPixel(dpy, scr), WhitePixel(dpy, scr)); + XStoreName(dpy, win, "bit-address spike"); + XSelectInput(dpy, win, ExposureMask | KeyPressMask | StructureNotifyMask); + XMapWindow(dpy, win); + + bpl = ((w + 31) / 32) * 4; + data = malloc((unsigned long)bpl * h); + if (!data) + return 1; + pattern(data, bpl, w, h); + + img = XCreateImage(dpy, vis, (unsigned)DefaultDepth(dpy, scr), ZPixmap, 0, + data, (unsigned)w, (unsigned)h, 32, bpl); + if (!img) + return 1; + img->byte_order = LSBFirst; + img->bitmap_bit_order = LSBFirst; + img->f.put_pixel(img, 3, 4, 1); + XPutImage(dpy, win, gc, img, 0, 0, 0, 0, (unsigned)w, (unsigned)h); + + for (i = 0; i < 3; i++) { + XNextEvent(dpy, &ev); + copy = ev; + count = handle(dpy, ©); + if (count < 0) + break; + } + + if (XGetWMName(dpy, win, &tp) == 0) { + list = 0; + count = 0; + if (XmbTextPropertyToTextList(dpy, &tp, &list, &count) >= 0) { + for (i = 0; i < count; i++) + list[i][0] = 'x'; + XFreeStringList(list); + } + XFree(tp.value); + } + + ch.res_name = argv[0]; + ch.res_class = "Spike"; + hints.flags = 0; + hints.x = 20; + hints.y = 20; + hints.width = w; + hints.height = h; + XSetClassHint(dpy, win, &ch); + XSetWMProperties(dpy, win, &tp, &tp, argv, argc, &hints, 0, &ch); + + XSetForeground(dpy, gc, BlackPixel(dpy, scr)); + XSetBackground(dpy, gc, WhitePixel(dpy, scr)); + XSync(dpy, 0); + XCloseDisplay(dpy); + return 0; +} diff --git a/spike/xlib_min.h b/spike/xlib_min.h new file mode 100644 index 0000000..a642f70 --- /dev/null +++ b/spike/xlib_min.h @@ -0,0 +1,350 @@ +/* + * Minimal Xlib declarations for the bit-address spike. + * + * Only the parts a small X client touches. The layouts follow X11R4/Xlib.h; + * the point is to exercise pointer-bearing structs and the standard + * ScreenOfDisplay()/RootWindow()/DefaultVisual() macros, not to be a complete + * Xlib. + */ +#ifndef XLIB_MIN_H +#define XLIB_MIN_H + +typedef unsigned long XID; +typedef XID Window; +typedef XID Drawable; +typedef XID Pixmap; +typedef XID Atom; +typedef XID Colormap; +typedef XID Cursor; +typedef XID Font; +typedef XID GContext; +typedef XID KeySym; +typedef int Bool; +typedef unsigned char KeyCode; +typedef unsigned long Mask; + +typedef struct _Display Display; +typedef struct _Screen Screen; +typedef struct _Visual Visual; +typedef struct _XImage XImage; +typedef struct _XScreenFormat ScreenFormat; +typedef struct _XExtData XExtData; + +struct _XScreenFormat { + int depth; + int bits_per_pixel; + int scanline_pad; +}; + +struct _XExtData { + int number; + XExtData *next; + int (*free_private)(XExtData *); + char *private_data; +}; + +struct _Visual { + XExtData *ext_data; + Visual *visualid; /* really VisualID, but size/alignment is what matters */ + int class; + unsigned long red_mask; + unsigned long green_mask; + unsigned long blue_mask; + int bits_per_rgb; + int map_entries; +}; + +struct _Screen { + XExtData *ext_data; + Display *display; + Window root; + int width, height; + int mwidth, mheight; + int ndepths; + void *depths; + int root_depth; + Visual *root_visual; + GContext default_gc; + Colormap cmap; + unsigned long white_pixel; + unsigned long black_pixel; + int max_maps, min_maps; + int backing_store; + Bool save_unders; + long root_input_mask; +}; + +struct _Display { + XExtData *ext_data; + void *private1; + int fd; + int private2; + int proto_major_version; + int proto_minor_version; + char *vendor; + XID private3; + XID private4; + XID private5; + int private6; + XID (*resource_alloc)(Display *); + int byte_order; + int bitmap_unit; + int bitmap_pad; + int bitmap_bit_order; + int nformats; + ScreenFormat *pixmap_format; + int private8; + int release; + void *private9; + void *private10; + int qlen; + unsigned long last_request_read; + unsigned long request; + void *private11; + void *private12; + void *private13; + void *private14; + unsigned max_request_size; + void *db; + int default_screen; + int nscreens; + Screen *screens; + unsigned long motion_buffer; +}; + +struct _XImage { + int width, height; + int xoffset; + int format; + char *data; + int byte_order; + int bitmap_unit; + int bitmap_bit_order; + int bitmap_pad; + int depth; + int bytes_per_line; + int bits_per_pixel; + unsigned long red_mask; + unsigned long green_mask; + unsigned long blue_mask; + void *obdata; + struct funcs { + XImage *(*create_image)(Display *, Visual *, unsigned int, int, int, char *, + unsigned int, unsigned int, int, int); + int (*destroy_image)(XImage *); + unsigned long (*get_pixel)(XImage *, int, int); + int (*put_pixel)(XImage *, int, int, unsigned long); + XImage *(*sub_image)(XImage *, int, int, unsigned int, unsigned int); + int (*add_pixel)(XImage *, long); + } f; +}; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; +} XAnyEvent; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; + Window root; + Window subwindow; + unsigned long time; + int x, y; + int x_root, y_root; + unsigned int state; + unsigned int keycode; + Bool same_screen; +} XKeyEvent; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; + Window root; + Window subwindow; + unsigned long time; + int x, y; + int x_root, y_root; + unsigned int state; + unsigned int button; + Bool same_screen; +} XButtonEvent; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; + int x, y; + int width, height; + int count; +} XExposeEvent; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window event; + Window window; + int x, y; + int width, height; + int border_width; + Window above; + Bool override_redirect; +} XConfigureEvent; + +typedef struct { + int type; + unsigned long serial; + Bool send_event; + Display *display; + Window window; + Atom message_type; + int format; + union { + char b[20]; + short s[10]; + long l[5]; + } data; +} XClientMessageEvent; + +typedef struct { + int type; + Display *display; + XID resourceid; + unsigned long serial; + unsigned char error_code; + unsigned char request_code; + unsigned char minor_code; +} XErrorEvent; + +typedef union _XEvent { + int type; + XAnyEvent xany; + XKeyEvent xkey; + XButtonEvent xbutton; + XExposeEvent xexpose; + XConfigureEvent xconfigure; + XClientMessageEvent xclient; + XErrorEvent xerror; + long pad[24]; +} XEvent; + +typedef struct { + unsigned char *value; + Atom encoding; + int format; + unsigned long nitems; +} XTextProperty; + +typedef struct { + char *res_name; + char *res_class; +} XClassHint; + +typedef struct { + long flags; + int x, y; + int width, height; + int min_width, min_height; + int max_width, max_height; + int width_inc, height_inc; + struct { + int x; + int y; + } min_aspect, max_aspect; + int base_width, base_height; + int win_gravity; +} XSizeHints; + +typedef struct { + Pixmap background_pixmap; + unsigned long background_pixel; + Pixmap border_pixmap; + unsigned long border_pixel; + int bit_gravity; + int win_gravity; + int backing_store; + unsigned long backing_planes; + unsigned long backing_pixel; + Bool save_under; + long event_mask; + long do_not_propagate_mask; + Bool override_redirect; + Colormap colormap; + Cursor cursor; +} XSetWindowAttributes; + +#define DefaultScreen(dpy) ((dpy)->default_screen) +#define ScreenOfDisplay(dpy, scr) ((dpy)->screens + (scr)) +#define RootWindow(dpy, scr) (ScreenOfDisplay((dpy), (scr))->root) +#define DefaultVisual(dpy, scr) (ScreenOfDisplay((dpy), (scr))->root_visual) +#define DefaultDepth(dpy, scr) (ScreenOfDisplay((dpy), (scr))->root_depth) +#define DefaultGC(dpy, scr) (ScreenOfDisplay((dpy), (scr))->default_gc) +#define BlackPixel(dpy, scr) (ScreenOfDisplay((dpy), (scr))->black_pixel) +#define WhitePixel(dpy, scr) (ScreenOfDisplay((dpy), (scr))->white_pixel) +#define DisplayWidth(dpy, scr) (ScreenOfDisplay((dpy), (scr))->width) +#define DisplayHeight(dpy, scr) (ScreenOfDisplay((dpy), (scr))->height) + +#define LSBFirst 0 +#define MSBFirst 1 +#define XYBitmap 0 +#define XYPixmap 1 +#define ZPixmap 2 + +#define KeyPress 2 +#define Expose 12 +#define ConfigureNotify 22 + +#define ExposureMask (1L << 15) +#define KeyPressMask (1L << 0) +#define ButtonPressMask (1L << 2) +#define StructureNotifyMask (1L << 17) +#define NoEventMask 0L + +extern Display *XOpenDisplay(const char *); +extern int XCloseDisplay(Display *); +extern Window XCreateSimpleWindow(Display *, Window, int, int, unsigned int, + unsigned int, unsigned int, unsigned long, + unsigned long); +extern Window XCreateWindow(Display *, Window, int, int, unsigned int, + unsigned int, unsigned int, int, unsigned int, + Visual *, unsigned long, XSetWindowAttributes *); +extern int XChangeWindowAttributes(Display *, Window, unsigned long, + XSetWindowAttributes *); +extern int XStoreName(Display *, Window, const char *); +extern int XSelectInput(Display *, Window, long); +extern int XMapWindow(Display *, Window); +extern GContext XCreateGC(Display *, Drawable, unsigned long, void *); +extern int XSetForeground(Display *, GContext, unsigned long); +extern int XSetBackground(Display *, GContext, unsigned long); +extern XImage *XCreateImage(Display *, Visual *, unsigned int, int, int, char *, + unsigned int, unsigned int, int, int); +extern int XPutImage(Display *, Drawable, GContext, XImage *, int, int, int, int, + unsigned int, unsigned int); +extern int XSync(Display *, Bool); +extern int XNextEvent(Display *, XEvent *); +extern int XLookupString(XKeyEvent *, char *, int, KeySym *, void *); +extern int XGetWMName(Display *, Window, XTextProperty *); +extern int XmbTextPropertyToTextList(Display *, const XTextProperty *, char ***, int *); +extern void XFreeStringList(char **); +extern int XFree(void *); +extern int XSetClassHint(Display *, Window, XClassHint *); +extern int XSetWMProperties(Display *, Window, XTextProperty *, XTextProperty *, + char **, int, XSizeHints *, void *, XClassHint *); +extern void *malloc(unsigned long); +extern void *memset(void *, int, unsigned long); +extern void *memcpy(void *, const void *, unsigned long); + +#endif diff --git a/symbols/README.md b/symbols/README.md new file mode 100644 index 0000000..5093fca --- /dev/null +++ b/symbols/README.md @@ -0,0 +1,12 @@ +# ROM import tables + +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. + +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. diff --git a/symbols/vt1200-v22.syms b/symbols/vt1200-v22.syms new file mode 100644 index 0000000..5066f09 --- /dev/null +++ b/symbols/vt1200-v22.syms @@ -0,0 +1,14 @@ +# VT1200 v2.2 ROM entry points, bit addresses. +# Source: dwt/gui/xhello.asm comments and README.md (v2.2 image md5 +# b46a3e442ccb86134d2356bd70d22fd2). These are the known-good entries used +# by the existing hand-written payloads; the rest must be extracted from the +# Ghidra project / ROM before the Xlib client milestone. +# +# C name bit address +XOpenDisplay 0x3F9C2570 +XCreateSimpleWindow 0x3FD41B90 +XMapWindow 0x3FD645B0 +XSync 0x3FD9F6B0 +strlen 0x3FB458D0 +malloc 0x3F701230 +free 0x3F702440