commit 9cfa2d235c1d428fafc81a54d3808d37b0617204
parent 3b67636bfc544336856a69b9a7f419d30b0f0858
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 16:32:54 -0700
doc/plan/ARM32.md: record Phase 1 as delivered; re-scope Phase 2
Rewrite the Phase 1 bring-up section as a delivered record (what landed in the
backend / ABI / ELF / cross-test lane + the -O0 native spine + bring-up
findings), and re-scope Phase 2: drop the items Phase 1 delivered
(THM_CALL/JUMP24, sdiv/udiv+mls, extends, the basic frame/calls) and surface the
gaps found during implementation (global address materialization blocks all
globals; -O1 known-frame forces the smoke lane to -O0; the kit-built runtime).
Diffstat:
| M | doc/plan/ARM32.md | | | 197 | ++++++++++++++++++++++++++++++++++++++----------------------------------------- |
1 file changed, 94 insertions(+), 103 deletions(-)
diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md
@@ -366,109 +366,100 @@ 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).
+### Phase 1 — the walking skeleton — DELIVERED (main `3b67636b`)
+
+`make test-cross TARGET=freestanding-arm32 DEPTH=smoke` is green: kit cc compiles
+`arm-none-eabi` objects, kit ld links them with a clang-assembled Cortex-M reset
+stub + an `AT>`-based linker script, `qemu-system-arm -M mps2-an385` runs the
+image, and the semihosting `SYS_EXIT_EXTENDED` exit code matches.
+
+**Landed:**
+- **Gate + registration:** `KIT_ARCH_ARM32_ENABLED`, `arch_impl_arm32`,
+ `aapcs32_vtable` across the arch/abi/obj registries + `config_assert` +
+ `mk/lib_srcs.mk` + the disasm/dbg/link stub files.
+- **`src/arch/arm32/`:** `arch.c` (ArchImpl + the intra-function Thumb-2 branch
+ label-fixup patcher for B.W / B`<cond>`.W + predefined macros + target-feature
+ hooks), `isa.h` (header-only Thumb-2 encoders + `ThumbExpandImm` — no
+ descriptor table / `isa.c` yet), `native.c` (the -O0 spine, below), `reloc.c`
+ (THM_CALL/JUMP24 split-immediate patcher), `link.c`, `regs`, `variant`,
+ `dbg.c` (BKPT, len 2/4); `disasm.c`/`asm.c` are NULL stubs.
+- **`abi_aapcs32.c`:** soft-float AAPCS32 — r0–r3 then stack, i64/double as a
+ 2-lane GPR pair, sret in r0, aggregates ≤16 B in regs else INDIRECT,
+ `scalar_split_lane_size=4`.
+- **ELF:** `EM_ARM` + `EF_ARM_*` + `R_ARM_*` (obj.h tail + elf.h), `reloc_arm.c`
+ mapper, `obj/registry.c` arch-ops row, `EM_ARM` e_flags synthesis (soft = no
+ float bit, matching clang/gas).
+- **Cross-test lane:** `hosted.sh` (`freestanding-arm32`), `exec_bare.sh`
+ (`_bare_emit_arm32`: mps2-an385, `BKPT #0xAB` + `SYS_EXIT_EXTENDED` oracle,
+ clang reset stub, `MEMORY`/`AT>` linker script), `cross_test.sh` (arm32 smoke
+ forced to -O0), `mk/test.mk` + `test/smoke/arm32.sh` + `check_arm32_env.sh`.
+
+**The -O0 native spine (`native.c`):** FP-anchored frame (r7 anchor; deferred
+`SUB sp` patched in `func_end`; FP-relative `[r7,#-off]` slots; `PUSH {r7,lr}` /
+`POP {r7,pc}`). r0–r3 NDT cache pool with Lever-1 arg-fronting; IP(r12) NDT
+scratch; LR(r14) the backend div/mod temp. MOVW/MOVT + ThumbExpandImm
+immediates; ADD/SUB/AND/ORR/EOR/MUL/SDIV/UDIV/(SDIV+MLS rem)/LSL/LSR/ASR;
+SXT/UXT/trunc converts; CMP+IT compare-materialize and CMP+B`<cond>`.W
+cmp_branch; AAPCS calls/returns (≤4 int args in regs, the rest on the stack);
+Thumb bit on `STT_FUNC` symbols. Validated under qemu: return-const, ALU,
+signed/unsigned compare + if/else, recursion, mul/div/mod, ≤4- and >4-arg calls;
+disassembly matches `llvm-objdump`.
+
+**Bring-up findings (carry into Phase 2):** clang `start.o` uses REL relocs
+(implicit addend; Thumb bit in `st_value`) — the BL/B.W patcher uses
+`(S&~1)+A−P−4` and the label-fixup applies the same −4 Thumb PC bias; `func_end`
+**must** publish the function symbol address (else multi-function objects
+misresolve); kit must **not** set `EF_ARM_ABI_FLOAT_SOFT` (soft is the ecosystem
+default — the freestanding linker's strict e_flags check rejects a mismatch with
+float-agnostic asm stubs). Lanes A/B/C (qemu-lane de-risk, reloc/ELF, harness)
+were built by parallel agents.
+
+### Phase 2 — remaining tracks
+
+Same seams as before (`func_begin`/`plan_call`/`spill`; `binop`/`emit` + the isa
+table; `arm32_reloc_apply_insn`; `rt/`). Most of `native.c`/`abi`/`reloc` is in
+place; the unimplemented ops `compiler_panic` with a clear message. Gate stays
+**run-correctness under qemu**, reds left red. Op-groups contend on `native.c` /
+the new `isa.c` at merge time — run them worktree-isolated, small diffs, serial
+cherry-pick behind the qemu gate (anchor cwd to the main checkout).
+
+**Highest priority — unblocks real C programs:**
+- **Global address materialization** — `NATIVE_ADDR_BASE_GLOBAL` panics today, so
+ *any* global variable is uncompilable. Needs MOVW/MOVT-of-symbol +
+ `R_ARM_THM_MOVW_ABS_NC`/`_MOVT_ABS` relocs (the patcher's `imm4:i:imm3:imm8`
+ scatter) + real `load_addr`/`load_const`/`load_label_addr`/`tls_addr_of`.
+- **`-O1` known-frame path** — `func_begin_known_frame` panics; the cross smoke
+ lane forces -O0 for arm32 (`smopt=O0` in `cross_test.sh`) until this lands.
+- **64-bit / soft-double** — `binop`/`convert` panic on 8-byte / FP. Reuses the
+ arch-neutral rv32 wide8 path; needs LDRD/STRD + UMULL/SMULL lane-emit +
+ ADDS/ADCS + the `__aeabi_*`/`__*di3` libcalls, which in turn needs the driver
+ runtime-variant row (`driver/lib/runtime.c`) + a kit-built `libkit_rt.a` (the
+ smoke set needs none today).
+
+**Op-groups (isa + native-emit + test):** bitfield (BFI/UBFX/SBFX), byte-reverse
+(REV/REV16/REVSH), shifted-register operands, switch tables (CBZ/CBNZ/TBB/TBH via
+`indirect_branch`), long multiply, 16-bit-encoding density (only mov-hi/bx/bkpt
+are 16-bit today — the rest are `.W`), large frames/offsets via IP (frame >4095
+panics).
+
+**Struct-by-value / varargs / atomics / TLS / inline asm** (all panic-stubbed):
+`copy_bytes`/`set_bytes` + INDIRECT params & returns; `va_*`; LDREX/STREX + DMB
+for ≤4-byte atomics (8-byte → spinlock libcall); `TLS_LE32` + the runtime tp
+contract; `asm_block` + ARM register/memory constraints.
+
+**Tooling + tests:** descriptor-driven disassembler (`isa.c` table + `disasm.c`,
+IT-state + 16/32-bit width edges) and standalone assembler (`asm.c`,
+`.syntax unified` + IT syntax, byte-golden vs llvm-mc); unit tests
+(`arm32_decode_test` → test-isa, `elf/unit/arm32_class32` → test-elf, host-gated
+`arm32_jit_test`); the DEPTH=full toy/parse corpus lanes (`cross_one_arm32`,
+parse E-lane + `kit_test_target.h`); `test/extlink` ld.lld check; provisioning +
+the PORT.md cross-status row.
+
+**Follow-on variants:** hard-float (FPv4-SP, `arm-none-eabihf`), DSP/saturating
+builtins (ARMv7E-M), ARMv6-M/Thumb-1, A-profile; full
+`-mcpu`/`-march`/`-mfpu`/`-mfloat-abi` parsing + the complete `__ARM_FEATURE_*`
+macro set; runtime verification; `.ARM.attributes`; -O0 density toward tcc; the
+kit-compiled reset stub (needs kit `as` for MSR/MRS/BKPT or a C+inline-asm stub).
---