commit fdf54b7c184c74cc10d483882d6110dd809dee69
parent cc7a68d9dd2cd09a8fbf02e115f49030fc99d5e3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 13:56:44 -0700
test/parse,doc/ARM32: close arm32 parse-corpus emit/codegen gaps
The 5 emit/codegen reds on the arm32 parse lane are closed:
gnu_labels_as_values_threaded by the computed-goto scratch fix; asm_01_grammar
and builtin_31_readcyclecounter by skip sidecars.
asm_01_grammar's templates are aarch64-specific (%w0 modifiers, mov w0, dmb sy,
x0 clobbers) and never assemble on arm32 — skipped exactly as on rv32/rv64/x64;
its real arm32 surface (operand binding incl. frame-resident "r" inputs, real
Thumb-2 mnemonics) is covered by test/arch/arm32_inline_test.c. The inline-asm
panic it raised, and the add/sub assembler bug that surfaced behind it, are both
fixed in the preceding commits.
builtin_31_readcyclecounter is skipped because ARMv7-M has no architectural
always-on cycle counter reachable as a single instruction (DWT CYCCNT is
optional and needs privileged enable) — same rationale as the wasm skip.
Parse lane: 929 pass / 0 fail / 64 skip.
Diffstat:
3 files changed, 46 insertions(+), 20 deletions(-)
diff --git a/doc/plan/ARM32.md b/doc/plan/ARM32.md
@@ -33,34 +33,58 @@ the qemu gate). See the "Follow-on variants / polish" checklist below for status
(and MVN shift suffix) matching llvm-objdump.
Verification note: `make test-cross TARGET=freestanding-arm32 DEPTH=full` toy lane
-is **423/0/2**; the broad parse lane is **928 pass / 5 fail / 62 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 62 skips are the
-i128 + binary128-long-double cases plus the two data-model-only cases
-(`6_5_64_unsigned_size_division`, `rv64_atomic_widths_orders`), all
-`.arm32.skip`-sidecar'd to match rv32. The remaining **5 fails are all
-emit/codegen gaps** (`asm_01_grammar`, `builtin_31_readcyclecounter`,
-`gnu_labels_as_values_threaded`) — see "Broad parse-corpus triage" below; the 12
-wrong-runtime-result reds are CLOSED (2026-06-17). The earlier fix that took the
+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 an emit scratch (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 — wrong-results CLOSED 2026-06-17
+### 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). Only the 5 emit/codegen
-gaps remain on the parse lane:
-- **Emit/codegen gaps (5) — outstanding:**
- - [ ] `asm_01_grammar` O0+O1 — `arm32 inline asm: optimizer asm input not in a
- register` (inline-asm operand binding at the parse-corpus's harder cases).
- - [ ] `builtin_31_readcyclecounter` O0+O1 — `arm32` does not support the
- `readcyclecounter` intrinsic (needs a DWT/CYCCNT or unsupported gate).
- - [ ] `gnu_labels_as_values_threaded` O1 — `opt native emit: no scratch
- register` (computed-goto under -O1 register pressure).
+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):
+- **Emit/codegen gaps (5) — DONE:**
+ - [x] `gnu_labels_as_values_threaded` O1 — `opt native emit: no scratch
+ register` was a computed-goto store whose address consumed both arm32
+ opt-scratch (`ip`+`lr`) as base+index, leaving none to materialize the
+ stored value. `collapse_addr_to_reg` (shared `pass_native_emit.c`) now
+ reuses the base when it is itself a reserved scratch (a transient dead
+ after the access): it folds the index into the base in place, freeing the
+ other scratch. Arch-neutral; only triggers when no third scratch exists,
+ so it is a no-op on register-rich backends. Runs `35` at O0+O1.
+ - [x] `asm_01_grammar` O0+O1 — `.arm32.skip`. The templates are aarch64-specific
+ (`%w0` modifiers, `mov w0`, `dmb sy`, `x0` clobbers) and never assemble on
+ arm32, exactly as on rv32/rv64/x64. The underlying error (`optimizer asm
+ input not in a register`) was a *real* arm32 bug too — a register-
+ constrained input whose value is a frame-resident local (e.g. a value
+ that is `+r` inout AND returned) arrives as a frame loc, not a register.
+ Fixed in `arm32 native.c` `arm_asm_block_native`: stage such inputs into a
+ reserved scratch (IP, then LR) before binding, mirroring the aa64 path.
+ Surfaced and fixed a second bug in `arm32 asm.c`: `add`/`sub rd, rn,
+ {#imm | rm}` only had the 2-operand hi-register `add` and the sp-adjust
+ rows, so `add r4, r4, #1` assembled as `add r4, r4` (×2) and `sub r4, r4,
+ #1` as `sub sp, sp, #0`. A shared `arm_asm_addsub_third` now picks the
+ densest legal form (16-bit sp-adjust → 32-bit `add.w`/`sub.w` modimm →
+ `addw`/`subw`), byte-matching llvm-mc; the standalone `kit as`/inline-asm
+ `add`/`sub` immediate + 3-register forms now encode correctly.
+ - [x] `builtin_31_readcyclecounter` O0+O1 — `.arm32.skip`. ARMv7-M has no
+ architectural always-on cycle counter reachable as a single instruction
+ (DWT CYCCNT is optional and needs privileged DEMCR/DWT_CTRL enable), so
+ the intrinsic is unsupported — the same rationale as the wasm skip.
- **Wrong runtime result (12) — DONE:**
- [x] `6_5_2_2_06_struct_param_mixed_fp_int` O0+O1 — AAPCS32 8-byte-aligned
aggregate (struct{double;long;}) was mis-split between core regs and stack;
diff --git a/test/parse/cases/asm_01_grammar.arm32.skip b/test/parse/cases/asm_01_grammar.arm32.skip
@@ -0,0 +1 @@
+asm_01_grammar's templates are aarch64-specific (w0/x0 register names, %w0 operand modifiers, `dmb sy`, `x0` clobbers), so the template never assembles for arm32 — same reason it is skipped on rv32/rv64/x64. arm32 inline-asm coverage (operand binding incl. frame-resident "r" inputs, real Thumb-2 mnemonics) lives in test/arch/arm32_inline_test.c.
diff --git a/test/parse/cases/builtin_31_readcyclecounter.arm32.skip b/test/parse/cases/builtin_31_readcyclecounter.arm32.skip
@@ -0,0 +1 @@
+ARMv7-M (Cortex-M) has no architectural always-on cycle counter reachable as a single instruction. The DWT CYCCNT counter is optional and requires privileged setup (DEMCR.TRCENA + DWT_CTRL.CYCCNTENA) that a bare __builtin_readcyclecounter() cannot express, so arm32 reports the intrinsic unsupported — the same rationale as the wasm skip.