kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 2e3847ef14119de226f9941f6de96ca204877e2a
parent 32370256c3e48008e1c170caf80adeec41f9e5cc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 14:55:44 -0700

doc/plan/ARM32.md: plan 32-bit ARM (ARMv7-M Thumb-2) support

Spec the supported ISA (full Thumb-2, MOVW/MOVT constants, hardware
SDIV/UDIV, BFI/BFX, IT blocks; soft-float v1), the AAPCS32 ABI, the ARM
ELF relocations, the runtime reuse (the rt AEABI/coro/rt.mk scaffolding
already exists), and the qemu-system-arm cross-test lane mirroring rv32.
Includes a tracer-bullet bring-up plan: a seam-first walking skeleton to
test-cross smoke level with a clang-assembled stub, then tiered parallel
expansion tracks. Index the doc in doc/plan/README.md.

Diffstat:
Adoc/plan/ARM32.md | 478+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/README.md | 1+
2 files changed, 479 insertions(+), 0 deletions(-)

diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md @@ -0,0 +1,478 @@ +# Plan: 32-bit ARM (`arm-none-eabi`, ARMv7-M / ARMv7E-M, Thumb-2) + +## Status — 2026-06-16 — not started; scaffolding inventory + spec + +No `arm32` code backend exists yet (`src/arch/` has `aa64`, `riscv`, `x64`, +`wasm`, `c_target` — no `arm32`). This doc specs the ISA, the backend shape, and +the cross-test surface so the work is farmable. + +The target is a freestanding **Cortex-M** toolchain: `kit cc/as/ld/objdump/disas` +producing and consuming correct `arm-none-eabi` ELFCLASS32 objects and static +executables for **ARMv7-M (Cortex-M3)** and **ARMv7E-M (Cortex-M4/M7)**, +validated under `qemu-system-arm` exactly the way `riscv32-none-elf` is validated +under `qemu-system-riscv32` (the `cross` matrix, `freestanding-*` only — see +[PORT.md](PORT.md)). The recently-closed rv32 work is the direct precedent: it +solved 32-bit-on-a-32-bit-arch legalization and the freestanding bare-metal +test lane; arm32 reuses both. + +### Already in place (head start) +- **`KIT_ARCH_ARM_32` enum** — `include/kit/core.h:134`. +- **Triple parse** — `arm` / `armv7` → `KIT_ARCH_ARM_32`, `ptr_size=4` + (`src/api/target.c:61`); reverse name `arm` (`:253`); `arch_kind_name` → + `"arm32"` (`src/arch/registry.c:78`). +- **Runtime AEABI helpers — built and shipping** — `rt/lib/arm/aeabi_thumb2.S` + (`__aeabi_{l,ul}divmod`, `__aeabi_{i,ui}divmod`, soft-float compares, sized + `__aeabi_mem*`), `aeabi_thumb1.S` (ARMv6-M, future), `aeabi.c` + (`__aeabi_drsub`/`__aeabi_frsub`). +- **Coro** — `rt/lib/coro/arm32.c` (+ `arm32_thumb1.c`). +- **`mk/rt.mk` variant recipes** — `RT_arm-eabi-thumb2` (ilp32, `CORO=arm32`, + `AEABI=thumb2`) and `RT_arm-eabi-thumb1`; both reuse `RT_ABI_SRCS_ilp32` + (`int32.c`) + `-Irt/lib/include/ilp32_le`, shared with rv32. +- **64-bit-value legalization** — the wide8 path (`src/cg/wide.c`, + `src/cg/arith.c`) built for rv32 (`i64`/soft-`double` made memory-resident, + inline lane add/sub/logic, libcalls for the rest) is arch-neutral and applies + to arm32 unchanged. +- **`ilp32_le` data model** — runtime includes + `RT_ABI_SRCS_ilp32` exist. + +### Not yet built (the work) +The whole `src/arch/arm32/` backend, the AAPCS32 ABI vtable, the ARM ELF +relocations, the `KIT_ARCH_ARM32_ENABLED` gate + `arch_impl_arm32` registration, +the driver `-mcpu`/`-march`/`-mfpu`/`-mfloat-abi` plumbing + predefined macros + +runtime-variant selection, and the `freestanding-arm32` cross-test lane. + +--- + +## Context + +kit today has no 32-bit ARM. We add a freestanding microcontroller target: + +``` +--target=arm-none-eabi (alias: thumbv7m-none-eabi / thumbv7em-none-eabi) +-mcpu=cortex-m3 | cortex-m4 | cortex-m7 +-march=armv7-m | armv7e-m +-mfloat-abi=soft (v1; softfp/hard are follow-ons) +-mthumb (implied — M-profile is Thumb-only) +``` + +This is **Thumb-2 only**: the M-profile has no ARM (A32) execution state, so +every instruction is a Thumb 16- or 32-bit encoding and every code address has +its low bit (the Thumb bit) set. `double` and `long long` are not native to the +integer core and are lowered (reusing the rv32 wide8 path). v1 is +soft-float so it runs on a plain Cortex-M3 with no FPU; single-precision +hard-float (FPv4-SP, Cortex-M4F) is a fast follow. + +--- + +## ISA spec — exactly what we support + +**Profile:** ARMv7-M (Cortex-M3) is the baseline; ARMv7E-M (Cortex-M4/M7) is +ARMv7-M **plus the DSP extension** (saturating + packed-SIMD instructions). The +core integer backend targets the common ARMv7-M subset; the E-M DSP ops are +gated behind a target feature and used only to lower saturating builtins +(a follow-on). One backend serves both — the difference is a feature flag, not a fork. + +**Endianness:** little-endian only (`__ARMEL__`). Big-endian (BE-8) is out. + +### Registers (AAPCS) +| reg | role | +|---|---| +| r0–r3 | argument / result / scratch — **caller-saved**; r0(:r1) returns scalars | +| r4–r11 | **callee-saved** variable registers (r9 is general here — bare-metal, no platform/static-base reservation) | +| r12 (IP) | intra-procedure scratch — **caller-saved**; kit's codegen scratch for address/immediate materialization (the analog of aa64 x16/x17, rv `t`-regs) | +| r13 (SP) | stack pointer — 8-byte aligned at public interfaces (AAPCS) | +| r14 (LR) | link register | +| r15 (PC) | program counter (reads as +4, Thumb) | +| APSR | flags N, Z, C, V (+ Q saturation, GE[3:0] on E-M) | +| s0–s31 / d0–d15 | VFP (hard-float only, a follow-on): s0–s15/d0–d7 args, s16–s31/d8–d15 callee-saved | + +Special registers (PRIMASK/FAULTMASK/BASEPRI/CONTROL via MSR/MRS, plus +ISB/DSB/DMB barriers and WFI/WFE) are assembler-only — emitted for inline asm +and the reset stub, not by the C codegen. + +### Instruction classes in scope +- **Full Thumb-2**, mixed 16-bit and 32-bit encodings. The backend prefers the + 16-bit form when operands fit (`r0–r7`, small immediates) and falls back to the + 32-bit (`.W`) form otherwise; the assembler/disassembler handle both widths. +- **Constant / address materialization via MOVW/MOVT — no literal pools.** An + arbitrary 32-bit immediate is `MOVW rd,#imm16` + `MOVT rd,#imm16:16`. A symbol + address is the same pair with `R_ARM_THM_MOVW_ABS_NC` + `R_ARM_THM_MOVT_ABS` + (static/absolute — no GOT in the freestanding model). This is the explicit + house strategy: it keeps I-cache clean and matches the request. ALU ops still + use the Thumb modified-immediate (`ThumbExpandImm`, the rotated-8-bit form) + when the constant fits; otherwise MOVW/MOVT into IP (r12) then operate. +- **Data processing:** ADD/SUB/RSB/ADC/SBC, AND/ORR/EOR/BIC/ORN, MVN, TST/TEQ, + CMP/CMN, MOV/MOVS, shifts LSL/LSR/ASR/ROR (immediate and register), and the + shifted-register operand form (`add r0,r1,r2,lsl #3`). +- **Multiply / divide:** MUL, MLA, MLS; long multiply UMULL/SMULL/UMLAL/SMLAL + (for `i64`). **Hardware divide: SDIV / UDIV** (present on all ARMv7-M). No + hardware modulo — `a % b` lowers to `SDIV/UDIV` + `MLS` (no `__aeabi_idiv` + needed for 32-bit; the AEABI div helpers remain for `i64` and Thumb-1). +- **Bitfield:** **BFI** (insert), BFC (clear), **SBFX / UBFX** (signed/unsigned + extract) — the primitives for C bitfield load/store. CLZ, RBIT, + REV/REV16/REVSH (byte-swap), SXTB/SXTH/UXTB/UXTH (sign/zero extend). +- **Load / store:** LDR/STR (+ LDRB/LDRH/LDRSB/LDRSH) with immediate, register, + and shifted-register offsets; **LDRD/STRD** (register pair — `i64`/`double`); + LDM/STM and PUSH/POP (prologue/epilogue, register-list save/restore). +- **Branches:** B (uncond and B`<cond>`), BL, BX/BLX (register, indirect call / + return), **CBZ/CBNZ** (16-bit compare-and-branch-against-zero, forward short), + and **TBB/TBH** (table-branch — switch jump tables). Range discipline (the + linker range-checks, like aa64 CALL26): BL/B.W (T1/T4) ±16 MB, B`<cond>`.W (T3) + ±1 MB, 16-bit B ±2 KB / B`<cond>` ±256 B, CBZ/CBNZ +0..126 B forward only. +- **IT blocks:** `IT{x{y{z}}} <cond>` predicates the next 1–4 instructions. This + is how Thumb-2 does branchless conditional-select (`CMP; ITE <cond>; + MOV<cond>; MOV<!cond>`) — there are no per-instruction condition codes outside + IT. The native backend emits IT for `cmp`→select and small conditional + sequences; the assembler tracks IT state and the disassembler renders it. +- **DSP / saturating (ARMv7E-M only, follow-on):** SSAT/USAT, QADD/QSUB, and the + packed SIMD set — emitted only to lower saturating/`__builtin` intrinsics, + gated on the `dsp` target feature. +- **Floating-point (hard-float only, follow-on):** VADD/VSUB/VMUL/VDIV.F32, + VCVT, VMOV (core↔FP), VLDR/VSTR, VCMP. FPv4-SP (Cortex-M4F, single-only) and + FPv5/FPv5-D16 (Cortex-M7). + +### Explicitly out of scope +- **ARM (A32) execution state** — M-profile has none; Thumb-only. +- **Thumb-1-only subset / ARMv6-M (Cortex-M0/M0+)** — no IT, no MOVW/MOVT, no + hardware divide, no 32-bit Thumb-2. A future variant (the rt `thumb1` recipe + + coro already anticipate it) — a follow-on. +- **NEON / Advanced SIMD** — not present on M-profile. +- **`__int128`** — `INT128=0` (as on rv32); reject at the frontend or leave-red. +- **ELF32 dynamic / PIE, GOT/PLT** — arm32 is static-only for v1 (mirrors the + rv32 "static-only, clean-panic on dynamic" decision). +- **The emulator** (`src/emu`, `src/os`) — stays non-arm32, as for rv32. + +--- + +## Confirmed scope decisions + +- **One backend, ARMv7-M Thumb-2.** ARMv7-M vs ARMv7E-M is a target-feature + (`dsp`) difference, and soft vs hard float is the `KitTargetSpec.float_abi` + axis (already exists from rv32) — neither forks the backend. Build the backend + with a clean `const`-descriptor seam (reached through the codegen context, per + "no global state") so a future ARMv6-M/Thumb-1 or A-profile/A32 variant can be + added the way `riscv/variant.h` added rv32 — but v1 ships a single variant. +- **Subsystems in scope:** compile + assemble + link + disasm; runtime; JIT + `run`/`dbg` plumbing wired but **host-gated** (an arm32 JIT must execute + AArch32 in-process, which this arm64-macOS dev host cannot do — gate to exit-77 + exactly as rv32's JIT test does on non-rv hosts). Emulator out. +- **ABIs:** AAPCS / ARM EABI (AEABI). v1 `-mfloat-abi=soft` (`arm-none-eabi`): + FP args in core registers, all FP via soft helpers. A follow-on adds + single-precision hard-float (`arm-none-eabihf`, FPv4-SP); `double` stays soft on FPv4-SP, the + exact parallel of rv32 `ilp32f`. +- **Object format:** ELF only (ELFCLASS32, EABI version 5). `EF_ARM_ABI_FLOAT_*` + e_flags carry the float ABI (like rv32's RISC-V float-ABI e_flags). A minimal + `.ARM.attributes` (Tag_ABI build attributes) is deferred (a follow-on) — needed only + for interop with external ARM toolchains; kit's own `ld` + qemu don't require + it. +- **Addressing:** MOVW/MOVT absolute (static), no literal pools, no GOT. Large + frame offsets / out-of-range immediates go through IP (r12). + +--- + +## Backend shape (mirror `src/arch/riscv` / `src/arch/aa64`) + +New `src/arch/arm32/`, one file per concern, following the reference backends +(`aa64` is "the reference"; `riscv` is the closest analog as a 32-bit RISC-style +target): + +| file | role | reference | +|---|---|---| +| `arch.c` | `arch_impl_arm32` (the `ArchImpl` — `src/arch/arch.h:263`): factories, register metadata, CFI defaults (`return_addr_reg=14`, `code_align=2`, `data_align=-4`, `cfa_init_reg=13`), predefined macros, target features, `backend_features=STRICT_ALIGNMENT`, `atomic_lock_free_max=4`, `supports_call_conv`/`supports_intrinsic` | `aa64/arch.c:208` | +| `isa.h` / `isa.c` | Thumb-2 format enums, the descriptor table (`{mnemonic, match, mask, fmt, flags, av}`), match/mask macros, inline encoders, operand printers. **Two width families** — a 16-bit and a 32-bit half-word table, decoded by leading bits | `riscv/isa.{h,c}`, `aa64/isa.{h,c}` | +| `disasm.c` | descriptor-driven decode (read first half-word; if `[15:11] ∈ {0b11101,0b11110,0b11111}` it is a 32-bit instruction → read second half-word); IT-state tracking; `.inst`/`.hword` fallback | `riscv/disasm.c` | +| `asm.c` | operand parsing, `ThumbExpandImm`/MOVW-MOVT/branch immediate encoding, IT-block syntax, `.syntax unified` | `aa64/asm.c` | +| `regs.h` / `regs.c` | DWARF index ↔ name table (r0–r15, then s0–s31/d0–d15 for VFP) | `aa64/regs.c` | +| `native.c` | the `NativeTarget` (`src/arch/native_target.h:459`) + `NativeOps`: register pools (`arm_int_allocable[]` = r0–r3 then r4–r11; scratch = IP/r12, with r0–r3 fronted as arg regs per the rv32 Lever-1 forward-order rule), frame layout (PUSH/POP, prologue/epilogue), all ops, IT-based `cmp`→select, MOVW/MOVT materialization, SDIV/UDIV+MLS, BFI/UBFX bitfields, LDRD/STRD + lane ops for `i64` | `riscv/native.c`, `aa64/native.c` | +| `reloc.c` | `arm32_reloc_desc` table + `arm32_reloc_apply_insn` (the Thumb-2 split-immediate byte patcher) + `link_arch_arm32` | `riscv/reloc.c`, `aa64/reloc.c` | +| `link.c` | `LinkArchDesc` (`src/link/link_arch.h:67`) — static-only, no PLT for v1 | `riscv/link.c` | +| `dbg.c` | breakpoint (BKPT) + displaced-step shim; `min_insn_len=2, max_insn_len=4` | `riscv/dbg.c` | + +Instruction emission goes through the shared `MCEmitter` (`src/arch/mc.h`): +emit 16-bit half-words via `mc_emit_bytes`/a thin `arm_emit16`/`arm_emit32` +wrapper that also feeds debug rows, exactly as `rv64_emit32` does. Label fixups +flow through `arch_impl_arm32.apply_label_fixup`. + +**ABI** lives in `src/abi/abi_aapcs32.c` + a `registry.c` entry +`{KIT_ARCH_ARM_32, KIT_OBJ_ELF, &aapcs32_vtable}` (`src/abi/registry.c:21`). The +`ABIVtable.compute_func_info` (`src/abi/abi_internal.h:13`) implements AAPCS: +r0–r3 then stack; **8-byte-aligned scalars (`i64`/`double`) consume an even/odd +register pair** (r0:r1 or r2:r3) and 8-byte-align on the stack — the notable +32-bit rule, structurally the same as rv32's aligned-GPR-pair +(`abi_rv64.c` `RiscvAbiDesc`); aggregates ≤ 4 words pass in registers, larger via +`sret` pointer in r0; `va_list` is a plain `void*` (`ABI_VA_LIST_POINTER`, 4 B, +gp_slot 4) — like rv32. `scalar_split_lane_size` returns 4 (so `i64` splits into +two 32-bit GPR lanes), reusing the rv32 mechanism. + +**Relocations.** Add ARM-specific `RelocKind`s (`src/obj/obj.h`) and an ELF +mapper `src/obj/elf/reloc_arm.c` (`elf_arm_reloc_{to,from,name}`, paralleling +`reloc_riscv32.c`). Reuse neutral `R_ABS32`/`R_REL32`/`R_ABS16`/`R_ABS8` for +data; the ARM-specific set is: + +| kit RelocKind | ELF | site | field | +|---|---|---|---| +| `R_ARM_THM_CALL` | `R_ARM_THM_CALL` | BL/BLX (T1) | 25-bit `{S,I1,I2,imm10,imm11}`, I1=¬(J1⊕S), I2=¬(J2⊕S); ±16 MB | +| `R_ARM_THM_JUMP24` | `R_ARM_THM_JUMP24` | B.W (T4) | same 25-bit field | +| `R_ARM_THM_JUMP19` | `R_ARM_THM_JUMP19` | B`<cond>`.W (T3) | 21-bit `{S,J2,J1,imm6,imm11}`; ±1 MB | +| `R_ARM_THM_MOVW_ABS_NC` | same | MOVW (T3) | imm16 `imm4:i:imm3:imm8`, = `(S+A)[15:0]` | +| `R_ARM_THM_MOVT_ABS` | same | MOVT (T3) | imm16, = `(S+A)[31:16]` | +| `R_ARM_THM_MOVW_PREL_NC` / `..._MOVT_PREL` | same | PC-rel MOVW/MOVT | `(S+A−P)` (PIC; follow-on) | +| `R_ARM_TLS_LE32` | same | TLS local-exec | `S+A−tp` (static TLS, variant I) | + +The split-immediate patcher is the **single trickiest piece** — the BL/B.W +J1/J2-XOR-S encoding and the MOVW/MOVT `imm4:i:imm3:imm8` scatter are error-prone; +write it test-first against `llvm-mc`/`clang` goldens. The Thumb bit +(symbol low bit set on `STT_FUNC` Thumb symbols) must be masked correctly in +branch targets; since everything is Thumb on Cortex-M, BL never relaxes to BLX. + +--- + +## Runtime (`rt/`) — mostly built; document in [../RUNTIME.md](../RUNTIME.md) + +- **Soft-float + AEABI:** `rt/lib/arm/aeabi_thumb2.S` + `aeabi.c` already provide + the AEABI div/mod, `i64` div/mod, soft-float compares, and `__aeabi_mem*`. The + generic soft-float (`rt/lib/fp/fp.c`) and 64-bit int helpers + (`rt/lib/int32/int32.c`: `__ashldi3`/`__ashrdi3`/`__lshrdi3`/`__muldi3` + 64-bit + divide) are the same ones rv32 uses. Frontend routes `i64` mul/div/shift, + `double`, and `i64`↔float to these. +- **TLS:** variant I (TCB ahead of `.tdata`), the same model as AArch64/RISC-V. + The freestanding image's reset stub sets the thread pointer and seeds the + static `.tdata`/`.tbss` image (the bare lane does this — see Testing). Document + the TCB size + `tp` convention in RUNTIME.md. +- **64-bit atomics:** ARMv7-M has `LDREX`/`STREX` (32-bit) but **no** 64-bit + exclusive (`LDREXD`/`STREXD` is A/R-profile only on most M parts) → 8-byte + `_Atomic` lowers to the spinlock-backed `__atomic_*_8` + (`rt/lib/atomic/atomic_freestanding.c`), and `__atomic_always_lock_free(8,…)` + reports false — identical to the rv32 contract. Document in RUNTIME.md. +- **Driver runtime variants** (`driver/lib/runtime.c`): add `arm-none-eabi` + (soft) → rt variant `arm-eabi-thumb2`, selected by the EABI float-ABI e_flag + via the existing `float_abi`/`isa`/`abi` axis on `RuntimeVariant` (added for + rv32). A follow-on adds the hard-float variant (a new `arm-eabihf-thumb2` rt recipe: + ilp32 + `ARCH_FLAGS=-mfpu=fpv4-sp-d16 -mfloat-abi=hard`). + +--- + +## Testing strategy — `test-cross` over `qemu-system-arm` (mirror rv32) + +arm32 is **freestanding, cross-only**: it joins the `cross` matrix as +`freestanding-arm32`, never `selfhost` (no OS to run a compiler on). The exec +seam already has the right backend — `test/lib/exec_bare.sh`, the `qemu-system` +bare-metal front door ([PORT.md](PORT.md) "One exec front door, three +backends"). We add an `arm32` arm to it, plus a smoke oracle and the corpus +lanes. + +### Support-set wiring (`scripts/hosted.sh`) +Add `freestanding-arm32` to `SUPPORT_SET`; teach `parse_target` the aliases +(`arm32`/`armv7m`/`armv7em`/`cortex-m3`… → canonical `arm32`), `triple_of` → +`arm-none-eabi`, and `tag_of` → `arm32-freestanding`. Then +`scripts/hosted.sh list freestanding` includes it and `make test-cross +TARGET=freestanding-arm32` resolves. + +### QEMU machine + exit oracle (`test/lib/exec_bare.sh`) +- **Emulator:** `qemu-system-arm` (already the family used for aa64 semihosting; + not yet wired for any arch). **Machine:** `mps2-an385` (Cortex-M3, the + maintained ARM MPS2 FPGA model; flash at `0x00000000`, SRAM at `0x20000000`), + with `mps2-an386`/`an500`/`an505` available for M4/M7 lanes; `lm3s6965evb` is + a fallback Cortex-M3 model. One machine for the default lane. +- **Exit-code oracle:** **ARM semihosting** via `BKPT #0xAB`, calling + **`SYS_EXIT_EXTENDED` (0x20)** with `{ADP_Stopped_ApplicationExit, code}` so an + *arbitrary* exit code reaches qemu's process exit (plain `SYS_EXIT` (0x18) can + only signal 0/1). Run with `-semihosting-config enable=on,target=native + -nographic -no-reboot`. This slots into the PORT.md per-arch oracle table: + + | arch | mechanism | decode | + |---|---|---| + | arm32 | ARM semihosting `BKPT #0xAB` + `SYS_EXIT_EXTENDED` | qemu rc = guest code | + + (aa64 uses the same family with `HLT #0xF000`; rv32/rv64 use the SiFive + finisher MMIO; x64 uses `isa-debug-exit`.) +- **Reset stub + linker script** (the per-arch scaffolding `exec_bare.sh` owns, + generalizing the rv32 stub): Cortex-M boots from a vector table at `0x0` — + first word = initial MSP, second word = reset handler `| 1` (Thumb bit). The + stub vector-table + reset handler: set SP, **copy `.data` from its flash LMA to + the SRAM VMA, zero `.bss`, seed the static `.tdata`/`.tbss` TLS image and set + `tp`** (mirroring the rv32 reset stub), then `BL main`, then semihosting-exit + with the return value. Linker script: `.text` (vector table first) at `0x0`; + `.data`/`.bss`/`.tdata`/`.tbss` at `0x20000000` with `AT>` LMA in flash. Loaded + via `-kernel image.elf`. The startup stub is clang-assembled until the kit + assembler grows the few system mnemonics it needs (a follow-on). + +### Lanes +- **Smoke** `test/smoke/arm32.sh` (→ `make test-smoke-arm32`): prereq check + (clang `arm-none-eabi` target + `qemu-system-arm`, like rv32.sh's + `check_rv32_env`), then `kit cc -target arm-none-eabi -mcpu=cortex-m3 + -ffreestanding -c` → `kit ld` (auto-links the runtime, no explicit + `libkit_rt.a`) → run under qemu, assert the semihosting exit code. Cover `i64`, + soft-`double`, `i64` div/shift, a switch (TBB/TBH), and a negative control. +- **Toy / parse corpus** (`DEPTH=full`): add `cross_one_arm32` to + `test/toy/run.sh` (path X) and the `arm32` branch to `test/parse/run.sh`'s + `kit_lane_E` + `kit_test_target.h`, both routing through `exec_bare_run arm32`. + Reds left red (no skip sidecars), enumerated as gaps — same discipline as rv32 + (expect `i64` atomics, TLS-needs-thread-pointer, `__int128` to surface). +- **Unit:** `test/arch/arm32_decode_test.c` (→ `test-isa`, ISA encode/decode + round-trip for the 16- and 32-bit families + IT + MOVW/MOVT + BL split), + `test/elf/unit/arm32_class32.c` (ELFCLASS32 round-trip, → `test-elf`), + `test/link/arm32_jit_test.c` (→ `test-arm32-jit`, **host-gated exit-77** — no + AArch32 execution on this host), and a `test/asm/` byte-golden lane + + `regen-arm32.sh` against clang/llvm-objdump. +- **External-linker check:** add arm32 objects to `test/extlink` (link kit + objects with `ld.lld`) once relocations land — catches `.ARM.attributes` / + e_flags / Thumb-bit issues kit's own `ld` would tolerate. + +### Provisioning +`make provision TARGET=freestanding-arm32` installs `qemu-system-arm` + a clang +with the `arm-none-eabi` target (provision-or-error, never silent skip — +PORT.md). Update the PORT.md `cross` status table with a `freestanding-arm32` +row. + +--- + +## Bring-up & parallelization + +The strategy is tracer-bullet: drive one thin thread through every layer first, +then thicken each layer independently. The reason the later work parallelizes is +the internal seams — `NativeTarget` (`src/arch/native_target.h`), `ArchImpl` +(`src/arch/arch.h`), the ABI vtable (`src/abi/abi_internal.h`), and +`reloc_apply_insn`. Each later track lives behind exactly one of them: + +- frame/ABI work behind `func_begin` / `plan_call` / `spill` / `frame_slot` +- op-group work behind `binop` / `emit` + the isa table +- reloc work behind `arm32_reloc_apply_insn` + the descriptor table +- runtime entirely in `rt/` + +So bring-up's real deliverable is **nailing those boundaries**, not running a +trivial program: an op-group agent that only touches isa rows + its op's emit + +asm parse never touches frame internals, and the frame agent can rewrite the +prologue underneath it without conflict. The tracks then collide only on the +shared isa table, which is append-mostly. + +### Phase 1 — the walking skeleton (one owner) + +**Done when** `make test-cross TARGET=freestanding-arm32 DEPTH=smoke` is green: +kit cc compiles a small smoke set for `arm-none-eabi`, kit ld links it with a +**clang-assembled** reset stub + linker script, `qemu-system-arm` runs it, and +the semihosting exit code matches. Every `src/arch/arm32/` file exists (thin), +the ArchImpl + ABI vtable are registered, and the reloc plumbing covers the +relocs the seam uses. + +The smoke set is chosen to force the structure to be real, not just `return 42`: +a return-constant, basic ALU (add/sub/mul/and/or/shift), a compare + if/else, +and **a call to a second kit function with ≤4 int args** — the last validates +the call/return ABI early, where arch ABIs are subtlest. + +The linear spine (critical path), owned by one agent: +1. Config gate (`KIT_ARCH_ARM32_ENABLED`) + empty `src/arch/arm32/` files + + `registry.c` + `mk/lib_srcs.mk` → `make lib` compiles with a stub arch. + *Defines the file layout; unblocks the fan-out.* +2. ABI vtable (`abi_aapcs32.c`, minimal: void/scalar-int, r0–r3 + r0 return) + + registry entry. +3. isa.c encoders for the smoke set only (movw/movt + modified-imm, add/sub/mul, + logic, shifts, cmp, b/b.cond, bl, bx, push/pop, ldr/str). +4. native.c minimal `NativeTarget` (func_begin/end, load_imm, move, the ALU + binops, cmp+branch, plan_call/emit_call, ret, spill/reload/frame_slot). +5. ELF: ELFCLASS32 path + STT_FUNC Thumb-bit + e_flags. +6. link.c `LinkArchDesc` + `link_reloc_apply` dispatch wired to arm32. + +Three pieces fan out to parallel sub-agents from the start, off the critical +path: +- **A — de-risk the qemu lane with clang first.** Build the `exec_bare.sh` arm32 + backend (machine `mps2-an385`, semihosting `BKPT #0xAB` + `SYS_EXIT_EXTENDED` + oracle, reset stub, linker script with `.data`/`.bss`/`.tdata` init) and prove + it runs a **clang-built** `return 42` to the right exit code. Retires the three + biggest unknowns (semihosting oracle, linker-script/init, qemu machine) + independently of the compiler. Start this first — it is the long pole. +- **B — reloc/ELF skeleton.** RelocKind additions, `elf.h` `R_ARM_*`, + `reloc_arm.c` mapper, and the descriptor + patcher for just `THM_CALL` + + `ABS32` (the stub→main call and vector-table words). The Thumb-2 + split-immediate patcher is the single trickiest thing in the project — + test-first against `llvm-mc` goldens here, in isolation. +- **C — data tables.** `regs.c` register file + predefined macros + driver + `-target arm-none-eabi` flag acceptance. + +The owner builds the spine, integrates B and C, then swaps the clang-linked +binary in lane A for the kit-linked one and iterates to green. + +### Phase 2 — parallel tracks + +Op-groups are parallel in design but contend on `isa.c` / `native.c` at merge +time. Run them **worktree-isolated, small diffs, serial cherry-pick merge behind +the qemu gate** (anchor cwd to the main checkout to avoid the empty-cherry-pick +trap); isa.c is append-mostly, so conflicts are usually adjacent-row trivia. + +**Tier 1 — start immediately, mutually independent:** + +| Track | Touches | Notes | +|---|---|---| +| **Frame / ABI correctness** | native.c (behind `func_begin`/`plan_call`/`spill`), `abi_aapcs32.c` | Stack args >4, 8-byte arg alignment, sret, struct-by-value, varargs save area, large frames via IP, callee-save minimization. Highest leverage (every op test rides the frame), but stays behind the seam so it doesn't block op-groups. | +| **Op-group expansion** (N agents) | isa + native-emit + asm-parse + test, per group | Groups: multiply/long-multiply, divide (SDIV/UDIV+MLS), bitfield (BFI/UBFX/SBFX), extends/byte-reverse, load/store widths (incl. LDRD/STRD), shifted-register operands, branches/switch (CBZ/TBB/TBH), IT-select. **Disasm is mostly free** — table-driven decode just needs the rows; only IT-state + 16/32-bit width edges need hand work. | +| **Reloc expansion** | reloc.c, reloc_arm.c, elf.h | THM_JUMP24, THM_JUMP19, MOVW/MOVT_ABS, TLS_LE32. Mostly independent; pair the MOVW/MOVT relocs with the address-materialization codegen slice. | +| **Runtime audit** | `rt/` only | Largely built (`aeabi_thumb2.S`, coro, rt.mk variant). Audit source completeness now; *verification* is gated below. | + +**Tier 2 — gated on Tier 1:** +- **64-bit legalization** — needs the load/store group (LDRD/STRD), the multiply + group (UMULL/SMULL), and call/ABI (for `__aeabi_*` libcalls). Reuses the + arch-neutral rv32 wide8 path, so it is wiring + arm lane-emit (ADDS/ADCS), not + new infrastructure. +- **Atomics + TLS codegen** — LDREX/STREX + DMB for ≤4-byte atomics (8-byte → + spinlock libcall); TLS local-exec needs the `TLS_LE32` reloc + the runtime tp + contract. +- **dbg support** — needs stable isa tables (displaced-step analysis) + BKPT; + builds in parallel but execution is **host-gated** (no AArch32 on the arm64 dev + host), like rv32's JIT test. + +**Tier 3 — gated on assembler / codegen maturity:** +- **Replace the clang stub with a kit-compiled one** — needs kit `as` to handle + the stub's system mnemonics (MSR/MRS/BKPT/vector table) or a C+inline-asm stub. + Genuinely late; depends on the asm op-groups + inline asm. +- **Runtime verification** — build + run the `rt/` checks once codegen compiles + the rt sources. +- **External-linker compatibility** (`test/extlink`, ld.lld) — `.ARM.attributes`, + e_flags, Thumb-bit; catches what kit's own ld tolerates. + +**Other tracks (parallel, no special ordering):** +- **Standalone assembler frontend** — op-groups add the encoders; the asm + frontend (directives, IT-block syntax, expression eval, reloc-from-text) + the + byte-golden lane (`regen-arm32.sh` vs clang/llvm-mc) is its own item. +- **Inline asm** (`asm_block` + ARM register/memory constraints) — distinct from + the standalone assembler; needed for real C. +- **Full driver/frontend surface** — `-mcpu`/`-march`/`-mfpu`/`-mfloat-abi` + parsing + the complete `__ARM_FEATURE_*` predefined-macro set real headers test + (phase 1 does only the minimum). +- **objdump / disas tool wiring** + arm32 disasm tests beyond the free decode. +- **Hard-float (FPv4-SP) variant**, **DSP/saturating builtins (ARMv7E-M)**, and + the **ARMv6-M / A-profile variants** — the follow-ons named above. +- **Code-quality tracks** — `-O0` density toward tcc, then `-O1` — parallel once + correctness lands. + +**Gate discipline.** Every Tier-1/2 slice keeps `make test-cross +TARGET=freestanding-arm32` green and adds its own targeted test. The codegen gate +is **run-correctness under qemu** (not byte-identity — as for rv32); the +asm/disasm byte-golden gate turns on once that lane exists. Reds are left red and +enumerated as gaps (rv32 discipline). + +--- + +## Where to look +- **Backend:** `src/arch/arm32/{arch,isa,disasm,asm,regs,native,reloc,link,dbg}.c` + — pattern off `src/arch/riscv/*` (32-bit analog) and `src/arch/aa64/*` + (reference). Shared emitter `src/arch/mc.{h,c}`; contracts `src/arch/arch.h`, + `src/arch/native_target.h`, `src/cg/native_direct_target.h`. +- **ABI:** `src/abi/abi_aapcs32.c` (new) + `src/abi/registry.c`; template + `src/abi/abi_rv64.c` (pair/lane rules) and `src/abi/abi_aapcs64.c` (AAPCS + shape). Vtable contract `src/abi/abi_internal.h`. +- **64-bit legalization:** `src/cg/wide.c`, `src/cg/arith.c`, + `src/cg/native_direct_target.c` (`nd_*` panics), `src/opt/{cg_ir_lower, + pass_native_emit}.c` — arch-neutral, already proven on rv32. +- **ELF / link / reloc:** `src/obj/obj.h` (RelocKind), `src/obj/reloc.{h,c}` + (neutral descriptors), `src/obj/elf/{elf.h,emit.c,read.c,link.c}` + + `reloc_arm.c` (new, template `reloc_riscv32.c`), `src/link/{link_reloc_apply.c, + link_reloc_desc.h,link_arch.h}`. +- **Driver / frontend:** `src/api/target.c` (triple), `src/api/object_detect.c` + (e_flags → float_abi), `driver/cmd/{cc,ld}.c`, `driver/lib/{runtime,target}.c`. +- **Runtime:** `rt/lib/arm/aeabi_thumb2.S`, `rt/lib/coro/arm32.c`, + `rt/lib/{int32,fp,atomic}/…`, `mk/rt.mk` (`RT_arm-eabi-thumb2*`). + [../RUNTIME.md](../RUNTIME.md) for the TLS / atomics / soft-float contracts. +- **Tests:** `test/lib/exec_bare.sh` (add arm32), `test/smoke/arm32.sh` (new, + template `rv32.sh`), `test/toy/run.sh` (`cross_one_arm32`), `test/parse/run.sh` + (E lane) + `test/lib/kit_test_target.h`, `test/arch/arm32_decode_test.c`, + `test/elf/unit/arm32_class32.c`, `test/link/arm32_jit_test.c`, + `scripts/hosted.sh`, `mk/{test,test_unit}.mk`. Design of record: [PORT.md](PORT.md). diff --git a/doc/plan/README.md b/doc/plan/README.md @@ -29,4 +29,5 @@ shrinks to whatever remains open. | [BUILD.md](BUILD.md) | A new content-addressed build coordinator (Bazel/Nix-style incremental builds layered on the CAS) — storage state machine, caching algorithm, recipe protocol. Distinct from `../BUILD.md` (kit's own Makefile build). | — (new subsystem) | | [BUILD_COMMANDS.md](BUILD_COMMANDS.md) | The kit-native `build-exe`/`build-lib`/`build-obj` verbs that replace `compile`: polyglot, in-memory compile+link with `--group` flag scoping and full link-flag control. Distinct from `BUILD.md` (the CAS coordinator). | [../DRIVER.md](../DRIVER.md) | | [LLGEN_IMPORT.md](LLGEN_IMPORT.md) | Importing the standalone LL(1)/Pratt parser and lexer generator into libkit, including public API renames, file moves, build gates, and a `kit llgen` command. | — | +| [ARM32.md](ARM32.md) | 32-bit ARM (`arm-none-eabi`, ARMv7-M/ARMv7E-M Thumb-2, Cortex-M3/M4/M7) freestanding backend: the supported ISA spec, AAPCS32 ABI, ARM ELF relocations, runtime reuse, and the `qemu-system-arm` cross-test lane. | [../ARCH.md](../ARCH.md), [PORT.md](PORT.md) | | [TODO.md](TODO.md) | Open deferred fixes and code smells only. Completed items are removed instead of checked off. Not a roadmap; a current backlog. | — |