Implement zero-dependency Wasm binary parser#754
Conversation
Wasm 3.0 compliance review — binary parser (PR #754)Review of the in-repo binary parser in The PR body declares a set of intentional divergences (typed select, blocktype Summary
Verified correct while reviewing: signed/unsigned LEB128 range logic, Finding 1 —
|
The i64 (memory64/table64) limit encodings are 0x04/0x05 per the Wasm 3.0 grammar; 0x02/0x03 are the shared-memory (threads) flags for i32 and were being misdecoded as i64. Address type is discarded downstream regardless, so this only affects which flag byte is recognized as which type. Adds regression tests covering the i32/i64 limit cases and confirming the shared-memory flags are now rejected instead of misdecoded.
parse_custom_section skipped the section id byte but then read_bytes(n, s) used n (the id, always 0) as the length instead of reading the section's size:u32 first. This left the stream misaligned after any custom section, silently dropping every section that followed. Also switch the "no more custom sections" sentinel from falsy bytes to explicit None, since an empty-payload custom section is valid and isn't the same as "not a custom section". Adds a regression test that builds raw bytes with custom sections interleaved between real sections and confirms the type/function/export sections still parse correctly.
0xFC 10 (memory.copy) and 0xFC 11 (memory.fill) map to the existing aCopy/aFill K constructs. Their memory index operands are parsed and discarded, matching the existing memory.size/memory.grow handling, since the K semantics has a single implicit memory. Adds parametrized unit tests (zero and multi-byte memidx values, plus a trailing sentinel byte) proving the parser consumes exactly the memidx operands rather than a fixed byte count, and exercises both opcodes through the full K interpreter via instrs.wat.
Went through each finding:
|
parse_module never checked that the input stream was fully consumed, so an unrecognized section id or garbage appended after a well-formed module was silently ignored instead of raising a parse error.
Dead code: nothing in the parser calls it, and its broad exception swallow (WasmParseError | IndexError | ValueError) risked masking real bugs if it were ever reused.
zip(function_section, code_section, strict=True) surfaced a bare Python ValueError on mismatch, unlike every other malformed-input path in the parser, which raises WasmParseError.
|
Fixed after self review:
|
This PR adds a new zero-dependency binary parser in
pykwasmthat parses Wasm bytecode and produces KAst terms of wasm-semantics. The implementation closely follows the WebAssembly 3.0 binary format specification.Previously, the parser relied on a py-wasm fork, as described in issue #524. That dependency has been unmaintained since 2019 and blocked support for WASM 2.0+ opcodes.
Changes
BlockMetaDatafield, which was previously used for coverage trackingImplement differential testing to compare the old py-wasm-based parser with the new parserSpec Divergences in the Binary Parser
The parser follows the current WebAssembly spec (WASM 3.0) for decoding, but the K semantics predates several additions. In the places below, bytes are decoded correctly but information is discarded because the semantics has no representation for it.
Blocktype type-index form (
instructions.py:47) — Ablocktypecan be a type index encoded asi33. Only the empty and single-valtype cases are handled; thei33case raises a parse error. The K semantics block types cannot reference the type section.Typed select (
instructions.py:83) — The typedselectvariant (0x1C) carries a value-type vector. It is parsed and discarded; the untypedSELECTnode is emitted. The K semantics has a single untypedselect.Memory load/store memarg (
instructions.py:170) —memargencodes an alignment hint and an optional memory index alongside the offset. Only the offset is forwarded to the AST; alignment and memory index are discarded. Affects all 23 load/store opcodes. The K semantics models a single memory with offset-only instructions.memory.size/memory.growmemory index (instructions.py:241) — Both instructions encode a memory index with the multi-memory proposal. It is parsed and discarded; the K semantics has a single implicit memory.Recursive types (
module.py:78) — The spec wraps all type definitions inrectypegroups that can be mutually recursive. We only accept singleton groups; actual recursion raises a parse error. The K semantics has no recursive type construct.Table inline initializer (
module.py:119) — The spec allows an optional inline initializer for tables (prefix0x40 0x00). Encountering it raises a parse error. No counterpart exists in the K semantics.Address type in limits (
types.py:117) — Limit encodings0x02/0x03signal 64-bit addressing. The address type is parsed but discarded by every caller (tablesec,memsec,externtype_as_import_desc). The K semantics assumes 32-bit addressing throughout.Import section externtype (
types.py:174) — Import descriptors are encoded as WASM 3.0externtypebut returned as WASM 1.0ImportDescnodes. The tag type variant (0x04) raises a parse error. The KImportDefnsort still matches the WASM 1.0 structure.Only two reference types are supported:
funcrefandexternref(types.py:90,197) — The GC proposal added further reference types (any,eq,i31,struct,array,none,noextern,nofunc,exn,noexn) and a general0x63/0x64encoding for(ref null? <heap type>). None of these are recognized; onlyfuncref(0x70) andexternref(0x6F) parse.v128(SIMD) is likewise unrecognized wherever a value type is expected. The K semantics predates the GC and SIMD proposals and has no construct for any of these types.