fix(wasm): three Python-vs-Wasm parity fixes (parse_float helper, capability error text, variant receiver)#80
Merged
Merged
Conversation
parse_float lowers to $parse_float, whose hard-rounding slow path calls $bn_mul_pow10, which in turn calls $pow10_i32. That helper was emitted only under the Float-format gate (_uses_format_str AND _uses_float_format), so a program that parses a Float but never interpolates one left $pow10_i32 undefined and the Wasm module failed to assemble with "unknown func: failed to find name $pow10_i32", whether the parse result was bound or discarded. Give $pow10_i32 its own idempotency latch (_pow10_emitted) and have _emit_bignum_helpers pull it in, mirroring how the Grisu float-format path already emits it. Over-emitting an unused helper is harmless dead code; a called-but-unemitted one is a hard build failure, so err toward over-emission. Tests: - Un-exempt parse_float from the discarded-call sweep and add it to the free-function recipes so it is compiled + validated. - Add TestWatCallClosure, a feature-agnostic guard asserting no emitted module calls a function it neither defines nor imports, over a corpus that lights up each gated helper family (including parse_float without any Float format). Confirmed it bites when $pow10_i32 is withheld. - Add a no-format parse_float cross-backend parity case (the existing probe always formatted the Float, masking the bug).
Reading a missing file printed "failed to read 'x': [Errno 2] ..." on
the Python backend but a bare "[Errno 2] ..." on both Wasm hosts,
because the host binders passed the raw str(OSError) as the whole
message and dropped the "failed to <op> '<path>'" wrap. Permission
denials were terser too: they used "<op>: <path>" with no restriction
cause, where Python names the op, the path repr, and the current
allowed-prefix / restriction set via the shared <Cap>._deny helpers.
Mirror the Python wording in both capa/runtime/_wasm_host.py and
capa/runtime/_wasm_component_host.py:
- OSError arms now build message=f"failed to <op> {path!r}",
cause=str(e), matching capa/runtime/_capabilities.py. The component
host also stops using type(e).__name__ as the cause.
- Every deny arm (pre-check and post-open) now routes through the
shared Fs._deny / Db._deny / Proc._deny so the message and the
restriction cause are byte-identical to the Python backend.
The operation itself is unchanged: each host keeps its own syscall for
its TOCTOU / sandbox guarantees, only the surfaced text is aligned. The
--wasi compiled path (capa/ir/_emit_wasm/_wasi/_fs.py) is deliberately
left alone; its message is a documented path-less contract with a
discriminant-only parity test.
Adds TestCapErrorMessageParity: runs the same fs read/write/mkdir/
list_dir failure and one deny per cap on the Python backend, the core
WasmHost, and the Component Model host, asserting all three print the
identical Err payload. Existing TOCTOU substring tests still pass.
A payloadless variant literal bound by an unannotated let/var (``let l = Leaf`` for ``type Tree = Leaf | Node(Int)``) is typed by the lowerer as the VARIANT name (``Leaf``), not the owning sum (``Tree``). The method-table, the multi-impl candidate table, and the sum-layout table are all keyed by the sum, so a method call (``l.val_of()``) raised "MethodCall on receiver of type 'Leaf'" and a match on the same binding raised "Match on scrutinee of type 'Leaf'". Both ran fine on the Python backend; only the Wasm emitter diverged. Resolve the variant head to its owning sum via ``_variant_to_sum`` at the consumer lookups, mirroring what _equality._normalize_eq_ty already does for ==: - _dispatch.py: normalise recv_head before the method-table / multi-impl routing gate. - _traits.py: normalise recv_ty in _emit_trait_method_call, which re-derives the receiver type and performs the authoritative lookup. - _match.py: normalise the sum head before the sum-layout lookup. The lowerer's ``.ty = variant-name`` is deliberately relied on as the payloadless-variant heap-pointer signal by the encoding / tuple / struct / value sites, so it is left intact; only the three lookups learn to fall back through _variant_to_sum. The variant-in-aggregate positions (variant in tuple / list / struct / Result, and equality) are unchanged and still pass. Tests: emitter-only cases in TestWasmEmissionShape (the method call and the match no longer raise; $Tree_val_of is emitted) and execution cases in TestWasmSumAndStruct for both the let and var forms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three pre-existing parity bugs surfaced by the codegen work in #78/#79. Each passes
capa --checkand behaves one way on Python while the Wasm backend rejects it or diverges. Each was sibling-swept before fixing, and each is a separate commit.1.
parse_floatwas unusable on Wasm unless the program also formatted a Floaterror: unknown func: failed to find name $pow10_i32, whether the result was bound or discarded.$pow10_i32was gated onformat_str AND float_format, butparse_floatpulls in the bignum helpers whose$bn_mul_pow10calls it. Emission now lives inside_emit_bignum_helpersbehind an idempotent latch, so every caller of the bignum family pulls it in exactly once. That also coversparse_jsonof a float literal, which was a second instance of the same breakage.Guard: a feature-agnostic WAT closure test over an 11-program corpus asserts
called - defined - importedis empty, so any future callable-but-unemitted helper fails CI. Proven to bite by withholding the helper.parse_floatmoved out of the sweep's exemption list, and the parity probe no longer masks the bug by always formatting the Float.2. Fs/Db/Proc error text diverged between the Python and Wasm hosts
fs.readof a missing file gavefailed to read 'x': [Errno 2] ...on Python but a bare[Errno 2] ...on Wasm; deny messages were terser and dropped the allowed-prefix cause. The sweep found the same divergence acrossfs.read/write/mkdir/list_dirand the Fs/Db/Proc deny paths, on both the core and component hosts.net.*already had parity because its binder delegates to the Python object, which is the template.Both hosts now wrap the OSError identically and route denies through the shared
Fs._deny/Db._deny/Proc._deny. This is a text change only: each host keeps its own syscall for its TOCTOU and sandbox guarantees, and a real out-of-prefix read still denies with no leak. The--wasicompiled path is deliberately untouched, since it has a documented path-less contract with discriminant-only parity.3. A method call or match on a payloadless-variant receiver failed on Wasm
let l = Leafthenl.val_of()raisedMethodCall on receiver of type 'Leaf', andmatch lraised the scrutinee equivalent. The analyzer types the binding as the sum; the lowerer narrows it to the variant name, and the dispatch, trait-call and match lookups keyed on that name.Fixed consumer-side only, resolving the head through
_variant_to_sumexactly as_equality._normalize_eq_tyalready does. The lowerer's variant-name type is left intact, because it is the load-bearing signal that a value is a payloadless-variant pointer for the encoding, tuple, struct and value sites. Three lookup sites needed it, including_emit_trait_method_call, which re-derives the receiver type and was proven load-bearing by neutralizing it in isolation.Verification
Suite 4257 passed, 18 skipped. Adversarially reviewed per ticket: the closure guard and the third dispatch site were each proven to bite, deny enforcement was re-verified as unweakened, and the variant-in-tuple/list/struct/Result/equality positions were re-checked for regressions.