kit

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

Plan: 32-bit ARM (ARMv7 Thumb-2 — freestanding, Linux, FreeBSD)

Status — 2026-06-17 — arm32 core closed

The arm32 core checklist is closed. The toy ARM32 cross lane is 423/0/2 (the 2 skips are the aarch64-only privileged-baremetal case), test-smoke-arm32 is 4/4, make rt-arm-eabi-thumb2 self-builds AEABI + coro with kit's assembler, and the bare coroutine runtime case now returns correctly under qemu. The Cortex-M reset stub in test/lib/exec_bare.sh is kit-assembled, ARM ELF objects emit .ARM.attributes, ARM32 .thumb_func symbols carry the ABI Thumb bit, and C inline asm templates now route real Thumb-2 mnemonics through src/arch/arm32/asm.c.

Backend-refactor audit — 2026-07-19

The post-8d7927db audit is complete. The two O1 aggregate-return failures were reproduced on that revision and are covered by a shared full-four-register return regression; frame/stack parts load directly into their final ABI result registers, without consuming an extra instruction-local temporary.

The audit corrected the soft-float AAPCS32 aggregate contract: composites larger than one word return indirectly through r0, while fixed-size composite parameters of any size remain by-value across r0-r3 and the stack. Independent Clang↔Kit tests cover both directions for an 8-byte return and a 32-byte argument at O0/O1. Known-frame inline-asm clobber preservation is now shared across native targets and covers ARM32 r4; structural r7/sp/pc clobbers are rejected. See BACKEND_REFACTOR_CROSS_TEST.md for the full evidence and the two isolated-green QEMU timeout artifacts from the full ARM32 cross run.

Follow-on landed — 2026-06-17 — DSP, flag/macro parsing, code density, disasm

Four follow-on items landed (parallel worktree agents, serial cherry-pick behind the qemu gate). See the "Follow-on variants / polish" checklist below for status:

Verification note: make test-cross TARGET=freestanding-arm32 DEPTH=full toy lane is 423/0/2; the broad parse lane is now 929 pass / 0 fail / 64 skip (the run also showed one transient 6_5_41_ptr_sub_ptr/O1/E qemu timeout under parallel load — got 124 = GNU timeout's exit code at 22 s vs the usual 140 ms; it passes 3/3 isolated, an infrastructure flake, not a red). The 64 skips are the i128 + binary128-long-double cases, the two data-model-only cases (6_5_64_unsigned_size_division, rv64_atomic_widths_orders), and the two aa64-template / no-cycle-counter cases (asm_01_grammar, builtin_31_readcyclecounter), all .arm32.skip-sidecar'd. The 5 emit/codegen gaps are CLOSED (2026-06-17)gnu_labels_as_values_threaded fixed (computed- goto scratch), asm_01_grammar + builtin_31_readcyclecounter skipped (see "Broad parse-corpus triage" below); the 12 wrong-runtime-result reds were already CLOSED (2026-06-17). The earlier fix that took the lane from 33 to 17 fails: the O1 frameless-leaf tier returned through a bare BX lr while the backend uses LR as a backend-private temporary (global-address staging, overflow/popcount/bitfield/byte-copy), so any such leaf clobbered its return address (the 16-case O1 globals/statics/attributes rc 134 cluster). The slim tier now saves lr (PUSH {lr}/POP {pc}).

Broad parse-corpus triage — ALL reds CLOSED 2026-06-17

The 12 wrong-runtime-result reds are all resolved (3 codegen fixes, 2 data-model-agnostic test rewrites, 2 data-model skips). The 5 remaining emit/codegen gaps are now closed too (1 codegen fix, 1 inline-asm fix + the assembler bug it surfaced, 2 aa64-template / no-cycle-counter skips):

Core checklist — remaining items (single source of truth)

What is left to call the arm32 core complete, plus the follow-on variants that are explicitly out of "core" scope. This list is authoritative; the prose "Remaining work" §1–§5 below is the original design rationale (its done items are now checked there too).

Core — outstanding: none.

Core — done (closed; details under "Residual reds" + §1/§2 below):

Follow-on variants / polish (NOT required for "core"; the variant seam, §4/§5):


Phase 3 — hosted ARM32: Linux + FreeBSD

Background

Linux, FreeBSD, and (historically) Windows ship ARM32 for A-profile processors (ARMv7-A, Cortex-A8/A9/A15) — not M-profile. A-profile has a full MMU, CP15 system registers, and supports both A32 and Thumb-2 execution states. The key fact for kit is that user-space code on A-profile runs in Thumb-2 by default on all these OSes (Linux armhf/armel, FreeBSD armv7, Windows ARM32 all compile to Thumb-2). The Thumb-2 ISA is identical between M-profile and A-profile, so the entire src/arch/arm32/ backend — ISA encoding, AAPCS32 calling convention, ELF32 output, relocations — carries over unchanged.

The differences between the bare-metal core and a hosted target are:

Windows ARM32 is out of scope (would need COFF/PE ARM support, which does not exist, and ARM32 Windows is extremely niche today).

Phase 3 checklist (single source of truth)

Sequence A — arm-linux-gnueabi (soft-float):

Sequence B — arm-freebsd-eabi (soft-float, after A is green):

Sequence C — arm-linux-gnueabihf (hard-float, after VFP follow-on lands):

§3.1 Dynamic linking design (the dominant item)

The static-only src/arch/arm32/link.c currently provides LinkArchDesc with no PLT. The hosted path follows the same structure as src/arch/aa64/link.c and src/arch/x64/link.c:

PLT stub layout (Thumb-2, position-independent):

  PUSH {r0–r3, lr}          @ preserve arg regs + lr (per GNU ld Thumb PLT)
  LDR  r12, [pc, #off]      @ load GOT entry address (literal pool or MOVW/MOVT)
  LDR  pc,  [r12]           @ tail-branch through GOT slot
  .word  <GOT entry abs addr>

The resolver stub (PLT[0]) jumps to the dynamic linker; subsequent stubs are 12 or 16 bytes depending on whether the GOT slot is PC-relative-reachable. Use the Thumb-2 interworking BX for indirect branches since the dynamic linker may be in ARM state. Mark each PLT entry STT_FUNC with the Thumb bit set.

Relocations to add (src/obj/obj.h RelocKind + src/obj/elf/reloc_arm.c):

kit RelocKind ELF R_ARM_* Purpose
R_ARM_JUMP_SLOT same PLT GOT slot — patched by dynamic linker at load
R_ARM_GLOB_DAT same GOT data slot for a symbol
R_ARM_RELATIVE same base-relative fixup (for PIE / ASLR)
R_ARM_COPY same copy reloc for non-function symbols
R_ARM_TLS_DTPMOD32 same TLS module index (dynamic TLS)
R_ARM_TLS_DTPOFF32 same TLS offset within module
R_ARM_TLS_TPOFF32 same TLS TP-relative offset (static TLS via LE)

TLS models for A-profile hosted:

Kit's driver should default to local-exec for executables and general-dynamic for shared objects, matching GCC/clang defaults.

PIE: R_ARM_RELATIVE applied at load time for all absolute data refs in a position-independent executable. The linker emits a .rel.dyn section. This enables ASLR on modern Linux/FreeBSD and is required for executables on hardened distributions.

§3.2 A-profile vs M-profile divergence in the backend

The Thumb-2 ISA is binary-identical across profiles. The divergences are localized to:

  1. arm_tls_addr_of (native.c) — gate on KIT_OS_FREESTANDING for the static path; emit MRC p15, 0, Rd, c13, c0, 3 for Linux/FreeBSD.
  2. .ARM.attributes (obj/elf/emit.c:158) — Tag_CPU_arch_profile = 'A' for hosted; 'M' for freestanding. Also Tag_CPU_arch = 10 (ARMv7) stays the same; Tag_CPU_arch_profile is the only field that changes.
  3. arch.c predefined macros — __ARM_ARCH_7A__ vs __ARM_ARCH_7M__; __linux__ / __FreeBSD__ gated on OS.
  4. link.c — PLT/GOT gated on KIT_OS != KIT_OS_FREESTANDING.

No fork of native.c or the ABI vtable is needed for the soft-float hosted path.

§3.3 Test infrastructure

qemu-user-arm is the right exec backend for arm32-linux. It runs A-profile ELF32 user-space binaries directly on the host without a full system emulator, intercepts Linux syscalls, and is widely available (apt install qemu-user-static on Debian/Ubuntu). This is the same model used for other hosted cross targets (exec_target seam in test/lib/exec_target.sh).

FreeBSD armv7: options are (a) the FreeBSD armv7 QEMU system image (heavier, already the model for FreeBSD aa64), or (b) qemu-user-arm + a FreeBSD armv7 sysroot (lighter, but less battle-tested). The system-image path is more consistent with the existing exec_vm.sh infrastructure.

armhf (gnueabihf): qemu-user-arm handles hard-float ARMv7-A binaries; no separate emulator is needed. The sysroot must be an armhf sysroot (e.g., Debian armhf cross-compilation sysroot).

The exec lane for arm32-linux slots naturally into the existing exec_target harness alongside the other cross-hosted lanes. make provision TARGET=arm32-linux installs qemu-user-arm, the cross sysroot, and any required binfmt_misc registration.

§3.4 Sequencing rationale

arm-linux-gnueabi first, because:

The dynamic linking work (§3.1) is the critical-path item for both Linux and FreeBSD. It can be developed once and reused; the only FreeBSD-specific linker concern is .interp and DT_NEEDED for libc.so.7 vs libc.so.6.


Status — 2026-06-16 — Phase 2b — memory/frame/ABI correctness landed

The arm32 backend is a working -O0/-O1 Cortex-M C toolchain: it compiles, links (both kit ld and ld.lld), and runs the full test-smoke-arm32 set and almost all of the cross corpus. The Phase-2b correctness pass (below) redesigned the memory layer correct-by-construction and fixed frame/ABI/varargs across the board; the toy corpus went from 355/425 to ~418/425.

Phase 2b — landed (the memory / frame / ABI correctness pass)

Residual reds — ALL CLOSED (2026-06-17); toy DEPTH=full arm32 = 423/0/2

The toy corpus is fully green (the 2 skips are the aarch64-only privileged- baremetal case). Each red was fixed correct-by-construction, not patched:

§3 toolchain — full rt self-builds (2026-06-17)

make rt-arm-eabi-thumb2 now builds the whole runtime with kit's own assembler — AEABI alias layer + coroutine layer, no exclusions (the §3 exit criterion). Enablers:

Still TODO (Remaining work, below)

(Minor: the disassembler renders shifted-MOV forms — LSR/ASR mov.w rd,rs — as a plain mov, losing the shift in listings. Cosmetic; the encodings are correct.)


The original Phase-2 inventory below remains the implementation map. It is not yet a complete, correctly-designed backend across the board — the "Remaining work" checklist records what is left, and (this is the point) those items must be designed and implemented correctly, not patched in piecemeal:

make test-cross TARGET=freestanding-arm32 DEPTH=full (toy X) runs 355/425 green; the ~70 reds are left RED on purpose (not skipped) and map onto the checklist below.

Remaining work — design these correctly; do NOT keep patching

For the scannable list of what is left, see the Core checklist near the top — it is the single source of truth. The §1–§5 below are the original design rationale, with done items checked in place.

Phase 2 surfaced a clear lesson. Several separate correctness bugs — a branch placeholder addend, two scratch-register clobbers (str lr,[lr]; ip-resident-base clobber), and an incomplete wide-access split — all lived in the memory-access / addressing / scratch layer and were each fixed cell-by-cell. The matrix that layer must cover —

{base: frame | reg | global} × {offset: in-range | out-of-range}
  × {width: 1/2/4/8} × {scratch aliases neither the transfer reg nor a live base}

— was never designed as a whole, so each new combination found a new hole (the i64/wide-store-to-global bug in §1 is the latest). The items below must be done as coherent, correct-by-construction designs, not as more spot patches. Each names the design, not a workaround.

1. Correctness — the wide / addressing memory layer (DONE — Phase 2b)

2. Functional features — real implementations, not stubs

3. Toolchain / runtime completeness

4. Profiles / ABIs — new variants via the clean variant seam, not forks

5. Polish (correctness-neutral, do last)

Exit criterion

Core closure is the top checklist: toy X 423/0/2, test-smoke-arm32, the kit-built runtime/coro repro, kit-assembled reset stub, ARM attributes, and inline asm via asm.c. The broader make test-cross TARGET=freestanding-arm32 DEPTH=full parse lane remains a follow-on portability/skip-triage task; it is not part of the closed core checklist above.

Status — 2026-06-16 — Phase 1 (walking skeleton) LANDED

Phase 1 is green: make test-cross TARGET=freestanding-arm32 DEPTH=smoke passes — kit cc -target arm-none-eabi -mcpu=cortex-m3 -ffreestanding compiles the smoke set, kit ld links it with the Cortex-M reset stub + linker script, qemu-system-arm -M mps2-an385 runs it, and the ARM-semihosting SYS_EXIT_EXTENDED exit code matches. The reset stub was clang-assembled during the initial walking skeleton and is kit-assembled in the closed core. The whole src/arch/arm32/ backend (arch/isa/native/reloc/link/regs/variant/dbg/disasm/asm), the AAPCS32 ABI vtable (src/abi/abi_aapcs32.c), the ARM ELF relocations (src/obj/elf/reloc_arm.c + EM_ARM/EF_ARM_*/R_ARM_*), the KIT_ARCH_ARM32_ENABLED gate + arch_impl_arm32 registration, and the freestanding-arm32 cross-test lane are in. Validated at -O0: return-const, ALU (add/sub/mul/and/or/eor/shift), sdiv/udiv + mls remainder, signed/unsigned compare + if/else, recursion, and ≤4-arg + >4-arg (stack) calls — all correct under qemu; disassembly matches llvm-objdump.

Scoped to Phase 2 (panic-stubbed or deferred today, per the plan below): the -O1 known-frame path (Phase 1 is -O0 only — the cross smoke lane forces -O0 for arm32), i64/soft-double wide8 lowering, atomics/TLS/varargs/inline-asm/ struct-by-value, the descriptor-driven disassembler + standalone assembler, the runtime build with kit (the smoke set needs no libkit_rt.a), and a kit-ld linker script with a real SRAM .data LMA (kit ld's script front end lacks AT>, so the global-free Phase-1 lane keeps everything in flash).

The "not started" inventory + ISA/backend/cross-test spec below remains the source of truth for the remaining (Phase 2) work.

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). 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)

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 — caller-saved; r0(:r1) returns scalars; O1 instruction-scoped operand-temp bank
r4–r11 callee-saved variable registers (r9 is general here — bare-metal, no platform/static-base reservation)
r12 (IP) intra-procedure temporary — caller-saved; O0 scratch policy and backend-private address/immediate staging
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

Explicitly out of scope


Confirmed scope decisions


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 ArchImplsrc/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 (defined in src/arch/native_target.h) + NativeOps: O0 cache pool r0–r3 and scratch IP; O1 persistent allocation r4–r11, scoped emit_temps r0–r3, and asm-only LR; 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 RelocKinds (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


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 "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_ofarm-none-eabi, and tag_ofarm32-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)

Lanes

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:

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 — 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 the 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. The reset stub is now kit-assembled in the closed core.

Landed:

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/marshal_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:

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 breadth; -O0 density toward tcc.


Where to look