Skip to content

RQ-60-VFPPRESSURE increment 1: AEABI-routed i64-f32 conversions on single-precision FPU targets - Refs #1069, Refs #869 - #1073

Merged
avrabe merged 4 commits into
mainfrom
feat/aeabi-i64-float-builtins-1069
Aug 26, 2026
Merged

RQ-60-VFPPRESSURE increment 1: AEABI-routed i64-f32 conversions on single-precision FPU targets - Refs #1069, Refs #869#1073
avrabe merged 4 commits into
mainfrom
feat/aeabi-i64-float-builtins-1069

Conversation

@avrabe

@avrabe avrabe commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

RQ-60-VFPPRESSURE increment 1 — the conversion routing only (the S-register pool extension is increment 2, a separate lane). Refs #1069, refs #869.

What this does

A function whose WASM signature contains no f64 anywhere was refused on cortex-m4f/m7 for "needing f64", because the #869 inline lowering of the i64↔float conversions runs on double-precision machinery. That pinned every conversion-carrying function to cortex-m7dp — the reach failure #1069 measured on the falcon cascade.

This routes exactly the six i64/f32-typed family members through the AEABI runtime helpers — the route #869's acceptance comment specified: base-AAPCS core-register calls, zero VFP pressure:

op route fence
f32.convert_i64_s / _u bl __aeabi_l2f / __aeabi_ul2f none needed (total)
i64.trunc_f32_s / _u f32 domain guard (2 UDF) + bl __aeabi_f2lz / __aeabi_f2ulz exact-f32 bounds trap NaN/out-of-range before the call; unsigned (-1,0)→+0 clamp so the C-style helper never sees undefined input
i64.trunc_sat_f32_s / _u inline NaN/saturation select + bl only in-range values reach the helper; no UDF (§4.3.2 never traps)

The fences matter because the AEABI helpers are more-total than WASM (the #633/#666/#709/#665/#642 class) — the exact soundness constraint from #869's acceptance comment, honored here on the builtin route.

Red first (literal output)

The eight-line repro on current main:

$ synth compile conv.wat -t cortex-m4f --relocatable --all-exports
warning: skipping function 'conv': ... GI-FPU-002 phase 2: scalar f64 requires a
double-precision FPU target (cortex-m7dp); 'thumbv7em-none-eabihf' has a
single-precision FPU (f32 only) — refusing to emit f64 (declining the function, #369)
Error: no functions compiled successfully (1 skipped) — nothing to emit   [exit 1]

The new test suite against pre-change sources (git stash of the impl, test kept):

test m4f_relocatable_routes_all_six_through_named_aeabi_builtins ... FAILED
test m4f_non_relocatable_still_declines_and_names_the_route ... FAILED
thread '...' panicked: F32ConvertI64S must lower on single-precision m4f --relocatable, got decline: ...
test result: FAILED. 3 passed; 2 failed

After (this branch):

$ synth compile conv.wat -t cortex-m4f --relocatable --all-exports
Compiled 1 functions ... 44 bytes; RELOCATION: R_ARM_THM_CALL __aeabi_ul2f
test result: ok. 5 passed; 0 failed   (tests/aeabi_i64_float_1069.rs)

Design decisions

1. Unconditional or pressure-heuristic? Neither, exactly: the builtin route applies only where inline is impossible — single-precision FPU targets, where the family previously had no lowering at all. On m7dp the #869 inline path is untouched. Rationale: predictable, and it moves zero shipping bytes (see below). What would make this the wrong call: if position#tick's m7dp D-exhaustion must be fixed on m7dp — that would need a pressure heuristic choosing builtin-over-inline there, and it belongs with the pressure work, stated as a named follow-up, not drifted into here. (Measured mitigation that exists today: on cortex-m7 — single-precision, the actual RT1176 core for an f32-only cascade — the route makes position#tick compile fully; see below.)

2. Which conversions. Measured on main: all 12 family members decline on m4f. Routed: the six whose WASM types are i64/f32 only (their f64 was an artifact of the lowering, not the program). Left declined by name: f64.convert_i64_s/u, i64.trunc_f64_s/u, i64.trunc_sat_f64_s/u — their WASM types carry f64, which a single-precision target genuinely cannot represent (any producer/consumer of that f64 value declines anyway). Routing them would be reach theater.

3. m7dp emitted bytes: none moved. The route is gated on FPUPrecision::Single, so m7dp never enters it. Verified three ways: pre/post cmp on the compiled minimal repro object (byte-identical), all frozen anchors green (cargo test --workspace, 151 suites, 0 failures — includes the frozen-fixture differentials), and the new test pins that no __aeabi_* call ever appears on m7dp. The pre-existing #869 (96,296 checks) and #782 (192,686 checks) oracles re-run green.

Gating on --relocatable: the bl __aeabi_* gets the same R_ARM_THM_CALL-against-undefined-symbol shape as every #197 external call, so the host linker resolves it. A self-contained image has no linker — non-relocatable single-precision keeps the loud decline, whose message now names the closable gap (compile with --relocatable to route ... through the AEABI builtins). This is a distinct mechanism from the existing --builtins <kiln-builtins.o> CLI flag (which links an object into --link mode); reusing that flag name for selection-time routing would conflate "what to link" with "how to lower", so the route keys on target capability + relocatable mode instead, and the flag keeps its one meaning.

The runtime-library dependency, stated

The emitted object now imports up to four symbols the embedder must provide at link time: __aeabi_l2f, __aeabi_ul2f, __aeabi_f2lz, __aeabi_f2ulz. Any of libgcc (-lgcc), compiler-rt builtins, or kiln's builtins archive satisfies them (jess links a kiln bridge already). Verified end-to-end: the m4f object links cleanly against real arm-none-eabi libgcc (link-exit=0), and the linked ELF — real libgcc code, no stubs — executes 88/88 boundary checks bit-exact vs wasmtime under unicorn, including the trap rows and the 0x8000008000000001 double-rounding killer.

Execution differential (CI-wired)

scripts/repro/aeabi_i64_float_1069_differential.py24,270 checks, 0 failures (19,139 stubbed builtin calls) vs wasmtime under unicorn on the m4f-compiled object:

Wired per #890/#910: ci-status: wired, emulations >= 24000, new oracle_run step; --min-emulation-floor 298754 → 322754 moved together on all four surfaces (ci.yml, claims.yaml pins, ORACLE_WIRING.md, feature-matrix template).

Real falcon components (the #1069 gate, run locally)

Pulled the published falcon 1.134.1 components from ghcr (the issue's public repro) and compiled with -t cortex-m4f --relocatable --all-exports --embedder-data-init --embedder-global-init:

component main this branch
rate rate@0.7.0#tick skipped (GI-FPU-002 phase 2), exit non-zero 6/6 compile, #tick exported, sole external __aeabi_ul2f
position position@0.7.0#tick skipped 8/8 compile, #tick exported, externals __aeabi_ul2f, __aeabi_f2lz, __aeabi_l2f
position on cortex-m7 #tick skipped (m7dp-only pinning → D0..D7 exhausted) 8/8 compile — the route removes the f64 pressure entirely on the single-precision M7

So of #1069's three named blockers, position#tick is now compilable on the M7 (as -t cortex-m7; on m7dp the inline path is deliberately unchanged and still D-exhausts). attitude#tick / ekf#estimate are phase-1 S-register exhaustion — increment 2's scope, untouched here.

Gates

Known residuals (named, not hidden)

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

@avrabe avrabe changed the title RQ-60-VFPPRESSURE increment 1 (#1069): route the i64<->f32 conversion family through AEABI builtins on single-precision FPU targets RQ-60-VFPPRESSURE increment 1: AEABI-routed i64-f32 conversions on single-precision FPU targets - Refs #1069, Refs #869 Aug 26, 2026
avrabe and others added 4 commits August 26, 2026 19:15
… family through AEABI builtins on single-precision FPU targets

A function whose WASM signature contains NO f64 anywhere (e.g.
`(param i64) (result f32)` + f32.convert_i64_u) was declined on
cortex-m4f/m7 for "needing f64", because the #869 inline lowering of the
i64<->float conversions runs on double-precision machinery. That pinned
every conversion-carrying function to cortex-m7dp — the reach failure
jess #1069 measured (the falcon estimator cannot be built for the M4
core it is architecturally assigned to).

The route is the one #869's acceptance comment specified: the AEABI
runtime helpers (__aeabi_l2f/ul2f for the converts, __aeabi_f2lz/f2ulz
for the truncations) — base-AAPCS core-register calls, zero VFP
pressure. Active ONLY on a single-precision FPU target under
--relocatable (the host linker resolves the symbols from the embedder's
runtime: libgcc / compiler-rt / kiln builtins); the m7dp inline path is
untouched and byte-identical (pre/post cmp on the minimal repro), and
non-relocatable single-precision keeps the loud decline, now naming the
closable gap.

Soundness fences (the helpers are MORE-TOTAL than WASM,
#633/#666/#709/#665/#642 class):
- trapping truncs: two-bound f32 domain guard (exact f32 bounds,
  2 UDF sites) BEFORE the call; the unsigned (-1,0)->+0 residue is
  clamped so the C-style helper never sees undefined input;
- trunc_sat: NaN/saturation results selected inline; only in-range
  values reach the helper; no UDF (never traps);
- converts: total, no fence.
The six f64-TYPED family members (f64.convert_i64_*, i64.trunc[_sat]_f64_*)
stay declined by name on single-precision — their WASM types carry f64.

Red-first: tests/aeabi_i64_float_1069.rs fails on pre-change sources
(every routed row declined) and pins the named builtin per op, the Udf
guard geometry, the f64-machinery-free property, the non-relocatable
and f64-typed declines, and the m7dp no-__aeabi invariant.

Ratchet: selector_lines_code 18288 -> 18907 (+619), waived with reason
(new reach where the family had no lowering; the frozen m7dp path
could not be deleted in exchange); selector_lines_total 28929 -> 29553.

Refs #1069, refs #869.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
… AEABI route, CI-wired

scripts/repro/aeabi_i64_float_1069_differential.py — the m4f execution
oracle for the routed family: 24,270 checks (19,139 stubbed builtin
calls) vs wasmtime under unicorn, boundary rows (0/±1/2^53/2^63/2^64
edges, the #869 double-rounding killer 0x8000008000000001, largest-f32-
below-bound rows, NaN payloads, ±inf, -0.0) + 4,000 fixed-seed fuzz
patterns per direction, all trap rows EXECUTED on both sides (wasmtime
trap must be an ARM UDF stop).

Trust boundary stated in the script: the __aeabi_* builtins are the
embedder's link obligation, provided here as spec-exact stubs
(integer-arithmetic RNE for l2f/ul2f, truncation for f2lz/f2ulz); every
stub ASSERTS its C-defined input domain, so a guard hole that lets
NaN/out-of-range reach the helper reddens the run even when the numeric
answer coincides. What the oracle verifies is everything synth EMITS:
marshalling, caller-saved preservation, the f32 domain guard, the
unsigned (-1,0)->+0 clamp, inline saturation/NaN selection, result
capture.

Negative control executed: an off-by-one mutation in the f2ulz stub
reddens the run with 4,088 failures.

Wiring per #890/#910: ci-status wired + `emulations >= 24000` floor;
new oracle_run step in ci.yml; --min-emulation-floor 298754 -> 322754
moved together on all four surfaces (ci.yml, claims.yaml pins,
ORACLE_WIRING.md, feature_matrix template) with the count-min floors
bumped (151/147/160).

Refs #1069, refs #869.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e-derive ratchet pins (+3 comment lines)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e-precision targets

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.22633% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/instruction_selector.rs 93.98% 24 Missing ⚠️
...esis/src/instruction_selector/select_with_stack.rs 97.05% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit e6a3b27 into main Aug 26, 2026
62 checks passed
@avrabe
avrabe deleted the feat/aeabi-i64-float-builtins-1069 branch August 26, 2026 22:19
avrabe added a commit that referenced this pull request Aug 26, 2026
#1073 merged as e6a3b27 and did not flip its own status. That is three in
v0.59 and now three in v0.60, in the release whose RQ-60-FLIPCOUPLE artifact
exists to fix exactly this.

Verified ON MAIN, not inferred: 8 `__aeabi_ul2f`/`__aeabi_f2lz` references in
crates/synth-synthesis/src/, and scripts/repro/aeabi_i64_float_1069_differential.py
present.

Six for six, every one caught by querying AFTER the merge rather than by any
mechanism. That is the argument for RQ-60-FLIPCOUPLE stated as data rather than
as a worry: a discipline that depends on somebody remembering fails on the tick
they do not.

Verified BY ID: CANARY / A64IMPORT / VFPPRESSURE all `[implemented]`;
CI-filter OURS=0; measured 473 = floor 473; claim_check exit 0.

Refs #1064, Refs #1069
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 26, 2026
I flipped it mechanically on "#1073 merged" without reading the artifact's own
DEFINITION OF DONE:

    those three symbols reaching `nm -> T` — 5 of 5 stages exported

Increment 1 did not achieve that. It cleared `position#tick` on single-precision
cortex-m7; on **m7dp — jess's actual flight core — all three symbols still fail
on main**. Increment 2 (#1075, open) is what delivers 5/5. The flip belongs
there.

This is the MIRROR of the orphaned-flip class I have been chasing all release.
Six times I caught a status left `proposed` over shipped code; here I set one to
`implemented` over code that had not shipped the thing the artifact promises.
Both directions under-report the truth, and RQ-60-FLIPCOUPLE should derive
status from EVIDENCE precisely so neither is a judgement call — "the PR merged"
is not the same claim as "the artifact's stated outcome holds".

ALSO: increment 2's written plan is marked RETIRED BY MEASUREMENT rather than
deleted, so it is not re-proposed. The plan was "extend the S pool into
callee-saved S16..S31". A discriminating variant — same pressure, different
RESIDENCE — showed the wall is not capacity: 60 simultaneously-live f32 on the
OPERAND STACK compile while 14 in HOMED LOCALS do not. Pool extension moves the
wall 13 -> ~29: a bigger number and the same wall further out, paid with
prologue/epilogue push/pop. Recorded because a retired plan that leaves no trace
gets proposed again.

v0.60 reads 2/8; VFPPRESSURE lands with #1075.
CI-filter OURS=0, measured 473 = floor 473, claim_check exit 0.

Refs #1069, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
avrabe added a commit that referenced this pull request Aug 27, 2026
…d 5th orphaned flips, plus a floor left slack (#1074)

* chore(rivet): RQ-60-CANARY + RQ-60-A64IMPORT implemented — the 4th and 5th orphaned flips

RQ-60-FLIPCOUPLE is now demonstrating itself. That artifact was filed BECAUSE
three status flips were orphaned in v0.59; here are two more, in the release
that scopes the fix:

  RQ-60-CANARY     its work SHIPPED IN v0.59.0 (#1061, tag-verified) — the
                   artifact was written describing already-delivered work and
                   landed as `proposed` anyway
  RQ-60-A64IMPORT  #1071 merged minutes ago and did not flip its own status

Verified ON MAIN rather than inferred from the merge list:
  expansion_canary_gate_1021.py present, 1 CI invocation, `git tag --contains`
    reports v0.59.0
  aarch64_import_dispatch_1017_differential.py present, 1 CI invocation

ALSO TIGHTENED THE FLOOR 472 -> 473. #1071 added SWVER-025 without raising it.
A floor only fails BELOW itself, so 473-measured against 472-pinned would have
passed QUIETLY — slack, not red. The gate's own rule is that a PR adding
artifacts raises the floor in that PR, and leaving it loose is the first crack
in the discipline the gate exists to create.

That is the same failure shape twice in one commit: a number that drifts in the
direction that still looks green. Both are exactly what RQ-60-FLIPCOUPLE is
scoped to make mechanical instead of vigilant.

Verified BY ID: both resolve `[implemented]`; CI-filter OURS=0; measured 473 =
floor 473; claim_check exit 0.

Refs #1064, Refs #1017, Refs #1021
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* chore(rivet): RQ-60-VFPPRESSURE implemented — the SIXTH orphaned flip

#1073 merged as e6a3b27 and did not flip its own status. That is three in
v0.59 and now three in v0.60, in the release whose RQ-60-FLIPCOUPLE artifact
exists to fix exactly this.

Verified ON MAIN, not inferred: 8 `__aeabi_ul2f`/`__aeabi_f2lz` references in
crates/synth-synthesis/src/, and scripts/repro/aeabi_i64_float_1069_differential.py
present.

Six for six, every one caught by querying AFTER the merge rather than by any
mechanism. That is the argument for RQ-60-FLIPCOUPLE stated as data rather than
as a worry: a discipline that depends on somebody remembering fails on the tick
they do not.

Verified BY ID: CANARY / A64IMPORT / VFPPRESSURE all `[implemented]`;
CI-filter OURS=0; measured 473 = floor 473; claim_check exit 0.

Refs #1064, Refs #1069
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

* fix(rivet): RQ-60-VFPPRESSURE back to `proposed` — my flip was PREMATURE

I flipped it mechanically on "#1073 merged" without reading the artifact's own
DEFINITION OF DONE:

    those three symbols reaching `nm -> T` — 5 of 5 stages exported

Increment 1 did not achieve that. It cleared `position#tick` on single-precision
cortex-m7; on **m7dp — jess's actual flight core — all three symbols still fail
on main**. Increment 2 (#1075, open) is what delivers 5/5. The flip belongs
there.

This is the MIRROR of the orphaned-flip class I have been chasing all release.
Six times I caught a status left `proposed` over shipped code; here I set one to
`implemented` over code that had not shipped the thing the artifact promises.
Both directions under-report the truth, and RQ-60-FLIPCOUPLE should derive
status from EVIDENCE precisely so neither is a judgement call — "the PR merged"
is not the same claim as "the artifact's stated outcome holds".

ALSO: increment 2's written plan is marked RETIRED BY MEASUREMENT rather than
deleted, so it is not re-proposed. The plan was "extend the S pool into
callee-saved S16..S31". A discriminating variant — same pressure, different
RESIDENCE — showed the wall is not capacity: 60 simultaneously-live f32 on the
OPERAND STACK compile while 14 in HOMED LOCALS do not. Pool extension moves the
wall 13 -> ~29: a bigger number and the same wall further out, paid with
prologue/epilogue push/pop. Recorded because a retired plan that leaves no trace
gets proposed again.

v0.60 reads 2/8; VFPPRESSURE lands with #1075.
CI-filter OURS=0, measured 473 = floor 473, claim_check exit 0.

Refs #1069, Refs #1064
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
avrabe added a commit that referenced this pull request Aug 27, 2026
…cknowledge #1073; RQ-60-FLIPCOUPLE is implemented

- RQ-60-CANARY / RQ-60-A64IMPORT / RQ-60-CFOBLIG / RQ-60-FLIPCOUPLE carry
  in-repo signatures (contains: predicates on their delivered/planned
  surfaces); CFOBLIG's (WasmInstructions.v gains BrIf) is deliberately FALSE
  today and flips the moment the model extension lands.
- RQ-60-RACOST / RQ-60-ARTIFACTSPLIT / RQ-60-WCETKEY / RQ-60-VFPPRESSURE are
  honestly manual: their definitions of done are measured verdicts or
  external runs with no single in-repo signature — for these, Direction-A
  protection rests on the delivery-commit floor, and any future implemented
  flip must carry a written verified-by basis.
- RQ-60-VFPPRESSURE gains landed: '#1073' — the machine-readable statement
  'increment 1 landed, the stated outcome (5-of-5 cascade stages) does not
  yet hold', which is the distinction the seven misses collapsed.
- RQ-60-FLIPCOUPLE proposed -> implemented, with the delivered mechanism,
  its replay result, and its stated residuals recorded in the description.

rivet validate: error set byte-identical to baseline (40, all foreign-prefix
cross-repo); rivet list: 473 artifacts, unchanged — floor untouched.

Refs #1064

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant