Skip to content

feat(#782a): implement the WASM trunc_sat family — nontrapping saturating float→int (i32 forms on ARM32, all 8 on aarch64) - #803

Merged
avrabe merged 4 commits into
mainfrom
feat/48-782a-trunc-sat
Jul 17, 2026
Merged

avrabe merged 4 commits into
mainfrom
feat/48-782a-trunc-sat

Conversation

@avrabe

@avrabe avrabe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Part a of #782 (v0.48 wave 1, lane 1): the 0xFC-prefixed saturating-float-to-int family (i32/i64.trunc_sat_f32/f64_s/u, WASM Core §4.3.2) was never implemented — the decoder dropped all 8 ops and every containing function loud-skipped (falcon's skipping 'ts32'/'ts64' class; falcon-flight-v1.123 carries 7× i32.trunc_sat_f64_s + 1× i32.trunc_sat_f32_s).

Semantics (§4.3.2 — TOTAL, never traps)

NaN → 0; below INT_MIN → INT_MIN; above INT_MAX → INT_MAX (unsigned: negatives → 0, above UINT_MAX → UINT_MAX); else truncate toward zero.

What lands where

op ARM32 Thumb-2 aarch64 RV32
i32.trunc_sat_f32_s/u ✅ bare RZ VCVT.{S32,U32}.F32 (m4f/m7/m7dp) ✅ bare FCVTZS/FCVTZU wd loud-decline
i32.trunc_sat_f64_s/u ✅ bare RZ VCVT.{S32,U32}.F64 (m7dp) ✅ bare FCVTZS/FCVTZU wd, dn loud-decline
i64.trunc_sat_* (4) LOUD-decline (no i64 register-pair conversion path — decline > wrong) ✅ bare FCVTZS/FCVTZU xd (clang-verified sf=1 encodings) loud-decline

The key insight both ways: ARM RZ VCVT / A64 FCVTZ saturate out-of-range and give 0 for NaN — the exact "more-total-than-WASM" behavior the #709 domain guard exists to keep away from the trapping trunc forms is the required semantics for trunc_sat, so the correct lowering is the bare convert with no guard. Both selector paths covered (with-stack = falcon's --relocatable path, and select_default parity); the with-stack lowerings reuse the trapping twins' pinned-home copy dance.

Bug the gate caught before it shipped

The optimized path silently compiled trunc_sat functions to 2-byte bx lr stubs (the #615 silent-NOP class): is_unsupported_float_op didn't list the new ops, so they fell through the IR bridge as no-ops. Fixed by bailing all 8 forms to select_with_stack; the boundary differential is what caught it.

Gates (mechanical, non-vacuous)

Decline-honesty

No existing gate asserted "trunc_sat unsupported" (the drop was the decoder _ => None); the still-declining subset (ARM32 i64 targets, RV32 everything) now has named loud-decline gates in both the Rust tests and the differential.

falcon relevance (#782)

Addresses the GI-FPU-001 unsupported operator class for i32.trunc_sat_f32_s / i32.trunc_sat_f64_s — 2 of the 3 functions in that skip class (the third is f64.reinterpret_i64, a separate lane). The dominant 12× "integer popped f32" class and the rest of the #782 breakdown are out of scope for part a.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 3 commits July 17, 2026 07:45
…owerings

The 8 nontrapping saturating float->int ops (0xFC prefix, WASM §4.3.2) now
decode. ARM32: i32-target forms lower as the bare saturating VCVT (round-
toward-zero VCVT saturates + NaN->0 = exactly trunc_sat; the #709 guard is
for the TRAPPING forms and stays untouched); i64-target forms LOUD-decline
(no i64 register-pair conversion path). aarch64: all 8 via FCVTZS/FCVTZU
(w and clang-verified x forms). RV32 loud-declines via the existing
catch-all. falcon (#782) carries 7x i32.trunc_sat_f64_s + 1x
i32.trunc_sat_f32_s on cortex-m7dp --relocatable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…or trunc_sat

- scripts/repro/trunc_sat_782_differential.py + .wat: NaN/±inf/exact
  INT_MIN-INT_MAX boundaries/±0.5/in-range vs wasmtime, executed under
  unicorn (Thumb-2 m7dp + m4f, aarch64) AND natively (arm64 host, forked
  child) — 536 checks, no traps anywhere; falcon's exact flags
  (-t cortex-m7dp --relocatable) compile-gated: i32 forms must not skip,
  i64 forms must decline loudly by name.
- FIX the differential caught: the OPTIMIZED path silently dropped
  trunc_sat to a 2-byte bx-lr stub (the #615 silent-NOP class) —
  is_unsupported_float_op now bails all 8 forms to select_with_stack.
- Rust gates: ARM32 sat=bare-VCVT/no-Udf + trapping twins KEEP the #709
  Udf guard + i64 loud declines + FPU honest-rejects (synth-synthesis);
  aarch64 one-bare-fcvtz w/x forms; RV32 Unsupported; decoder un-drop.
- Existing trap differentials stay green (f32 708/709: 48/48, f64 369:
  339/339, aarch64 m4: 167 cases); frozen anchors bit-identical (10/10).

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

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.17094% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-synthesis/src/instruction_selector.rs 75.82% 22 Missing ⚠️
crates/synth-backend-riscv/src/selector.rs 94.11% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 25ee3d4 into main Jul 17, 2026
40 checks passed
@avrabe
avrabe deleted the feat/48-782a-trunc-sat branch July 17, 2026 07:30
avrabe added a commit that referenced this pull request Jul 17, 2026
…at ops (#805)

#803 (trunc_sat, aarch64_selector_ops 99→107) and #804 (the generated
artifacts) landed in sequence; #804's committed status.json/FEATURE_MATRIX
snapshotted the pre-#803 op count and merged textually-clean on top of #803,
so the freshness gate went red on main (working as designed — it caught a
genuinely stale generated doc). Regenerated with `--emit-status`; 25/25 claims
hold. No behavior change; generated-artifact refresh only.


Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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