Skip to content

fix(verify): prove i64 (and all-type) inlining via by-body call modeling — closes #151 (v1.1.4)#152

Merged
avrabe merged 4 commits into
mainfrom
fix/151-verified-i64-inline
May 30, 2026
Merged

fix(verify): prove i64 (and all-type) inlining via by-body call modeling — closes #151 (v1.1.4)#152
avrabe merged 4 commits into
mainfrom
fix/151-verified-i64-inline

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 30, 2026

gale #151 — verified i64 inlining

Bug-fix release v1.1.4. On v1.1.3, loom optimize --passes inline reverted every i64 inline — even a trivial i64.add callee — as "unproven", making the verified inliner a no-op for i64 modules and blocking the gale C↔Rust seam (z_impl_k_sem_give inlining gale_k_sem_give_decide -> i64).

Root cause (deeper than reported)

Ground-truthing falsified the "i64-specific" framing — an i32 control reverted too. The Z3 translation validator modeled call F as an opaque uninterpreted function pure_call_F(args). That encoding proves two identical calls equal (CSE / directize depend on it) but can never prove a call equal to its own inlined body. So the original (with the call) and the optimized (with F inlined) never matched → every inline reverted. i32 was a no-op for the same reason; the report only noticed i64 because that was the gale use case.

Fix

  • By-body call modeling: when F is a pure, no-trap, leaf, straight-line, single-result callee, model call F(args) by symbolically executing F's own body on the argument bit-vectors instead of an opaque symbol. Original and optimized then compute the identical expression → Z3 proves them equal → the inline is kept with a real proof. Callees outside the whitelist fall back to the conservative opaque encoding (at worst a spurious revert, never an unsound accept); a defensive width check rejects any wrong-width result.
  • Consistency: both the direct call F and the resolved call_indirect paths use the by-body model, keeping directize's call_indirect ⇔ call F equivalence intact.
  • i64 in nested blocks: encode_simple_instruction now models the full i64 arithmetic / bitwise / comparison / eqz set + total width conversions (extend_i32_{s,u}, wrap_i64) — previously every i64 op inside a block/loop/if was an opaque BV32 (latent gap beyond inlining).

Soundness

test_inline_verifier_proves_correct_and_rejects_wrong_i64_inline locks in both directions: a correct x+1 inline of add1 is proven equivalent, and a deliberately wrong x+2 "inline" is rejected (counterexample). The previously-#[ignore]'d test_inline_pass_actually_inlines_i64_helper (#147 follow-up) is re-enabled and passing.

Validation

  • 385 lib tests pass (incl. soundness guard + directize consistency)
  • trivial i64 + i32 repros now inline (call eliminated, valid output, 0 reverts)
  • real gale wasm + full default pipeline dogfood clean (valid output)

Known-red gates (pre-existing, not introduced here)

Closes #151.

🤖 Generated with Claude Code

avrabe added 4 commits May 30, 2026 17:53
…ing — closes #151

gale follow-up (#151): on v1.1.3 `inline_functions` reverted EVERY i64
inline — even a trivial `i64.add` callee — as "unproven", making the
verified inliner a no-op for i64 modules and blocking the gale C↔Rust
seam (z_impl_k_sem_give inlining gale_k_sem_give_decide -> i64).

Ground-truthing falsified the "i64-specific" framing: an i32 control
reverted too. Root cause is deeper — the Z3 translation validator
modeled `call F` as an OPAQUE uninterpreted function `pure_call_F(args)`.
That encoding proves two IDENTICAL calls equal (CSE / directize rely on
it) but can NEVER prove a call equal to its own inlined body, so the
original (with the call) and the optimized (with F inlined) never
matched → every inline reverted. i32 was a no-op for the same reason;
the report only saw i64 because that was the gale use case.

Fix (loom-core/src/verify.rs):
- When F is a pure, no-trap, leaf, straight-line, single-result callee
  (callee_inlinable_by_body whitelist), model `call F(args)` by
  SYMBOLICALLY EXECUTING F's own body on the arg bit-vectors
  (encode_inlinable_callee_result) instead of an opaque symbol. Original
  and optimized then compute the identical expression and Z3 proves them
  equal — the inline is KEPT WITH A REAL PROOF. Callees outside the
  whitelist fall back to the opaque encoding (at worst a spurious revert,
  never an unsound accept). A defensive width check rejects any
  wrong-width result.
- Both the direct `call F` and the resolved `call_indirect` paths use
  the by-body model, keeping directize's call_indirect ⇔ call F
  equivalence intact (test_directize_folds_known_indirect_call).
- VerificationSignatureContext now carries callee bodies (Arc, cheap to
  clone); inline_functions builds the context so the validator can reach
  them (previously it used new() with no context → opaque calls).
- encode_simple_instruction: added the full i64 arithmetic / bitwise /
  comparison / eqz set + total width conversions (extend_i32_{s,u},
  wrap_i64), mirroring the i32 arms at width 64. Previously every i64 op
  inside a block/loop/if fell through to an opaque BV32 — a latent
  verification gap beyond inlining.

Soundness guard: test_inline_verifier_proves_correct_and_rejects_wrong_i64_inline
locks in BOTH directions — a correct x+1 inline of add1 is proven
equivalent, a wrong x+2 "inline" is rejected (counterexample). The
previously-#[ignore]'d test_inline_pass_actually_inlines_i64_helper
(#147 follow-up) is re-enabled and passing.

385 lib tests pass; trivial i64 + i32 repros now inline (valid output,
0 reverts); real gale wasm + full pipeline dogfood clean. The 7
pre-existing LICM/DCE integration failures (#150) are unrelated and
unchanged.

Closes #151.

Trace: REQ-7
@avrabe avrabe merged commit cc66f39 into main May 30, 2026
15 of 21 checks passed
@avrabe avrabe deleted the fix/151-verified-i64-inline branch May 30, 2026 16:54
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.

inline_functions reverts ALL i64 inlines (even trivial i64.add) — verifier can't prove i64 inline soundness; no-op for i64 modules

1 participant