From b3bad54bb6713bc9d966517fda142b824b0df9f2 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 3 Apr 2026 10:26:15 +0300 Subject: [PATCH 01/22] update `DataDefn` and `ExternDefn`, fix type annotations --- .../src/pykwasm/kdist/wasm-semantics/test.md | 26 ++++---- .../pykwasm/kdist/wasm-semantics/wasm-text.md | 14 ++--- .../src/pykwasm/kdist/wasm-semantics/wasm.md | 19 ++++-- pykwasm/src/pykwasm/kwasm_ast.py | 59 +++++++++++++++---- pykwasm/src/pykwasm/wasm2kast.py | 34 +++++++++-- 5 files changed, 107 insertions(+), 45 deletions(-) diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md index 06266dac5..26f726dc4 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md @@ -321,19 +321,19 @@ The conformance tests contain imports of the `"spectest"` module. start: .EmptyStmts , importDefns: .EmptyStmts , exports: - #export (... name: #token("\"global_i32\"" , "WasmStringToken") , index: 0 ) - #export (... name: #token("\"global_i64\"" , "WasmStringToken") , index: 1 ) - #export (... name: #token("\"global_f32\"" , "WasmStringToken") , index: 2 ) - #export (... name: #token("\"global_f64\"" , "WasmStringToken") , index: 3 ) - #export (... name: #token("\"table\"" , "WasmStringToken") , index: 0 ) - #export (... name: #token("\"memory\"" , "WasmStringToken") , index: 0 ) - #export (... name: #token("\"print\"" , "WasmStringToken") , index: 0 ) - #export (... name: #token("\"print_i32\"" , "WasmStringToken") , index: 1 ) - #export (... name: #token("\"print_i64\"" , "WasmStringToken") , index: 2 ) - #export (... name: #token("\"print_f32\"" , "WasmStringToken") , index: 3 ) - #export (... name: #token("\"print_f64\"" , "WasmStringToken") , index: 4 ) - #export (... name: #token("\"print_i32_f32\"" , "WasmStringToken") , index: 5 ) - #export (... name: #token("\"print_f64_f64\"" , "WasmStringToken") , index: 6 ) + #export (... name: #token("\"global_i32\"" , "WasmStringToken") , index: #externIdxGlobal(0) ) + #export (... name: #token("\"global_i64\"" , "WasmStringToken") , index: #externIdxGlobal(1) ) + #export (... name: #token("\"global_f32\"" , "WasmStringToken") , index: #externIdxGlobal(2) ) + #export (... name: #token("\"global_f64\"" , "WasmStringToken") , index: #externIdxGlobal(3) ) + #export (... name: #token("\"table\"" , "WasmStringToken") , index: #externIdxTable(0) ) + #export (... name: #token("\"memory\"" , "WasmStringToken") , index: #externIdxMemory(0) ) + #export (... name: #token("\"print\"" , "WasmStringToken") , index: #externIdxFunc(0) ) + #export (... name: #token("\"print_i32\"" , "WasmStringToken") , index: #externIdxFunc(1) ) + #export (... name: #token("\"print_i64\"" , "WasmStringToken") , index: #externIdxFunc(2) ) + #export (... name: #token("\"print_f32\"" , "WasmStringToken") , index: #externIdxFunc(3) ) + #export (... name: #token("\"print_f64\"" , "WasmStringToken") , index: #externIdxFunc(4) ) + #export (... name: #token("\"print_i32_f32\"" , "WasmStringToken") , index: #externIdxFunc(5) ) + #export (... name: #token("\"print_f64_f64\"" , "WasmStringToken") , index: #externIdxFunc(6) ) .EmptyStmts , metadata: #meta (... id: , funcIds: .Map , filename: .String ) ) diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md index 4b4b3e233..3c259aad5 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md @@ -998,19 +998,19 @@ Wasm currently supports only one table, so we do not need to resolve any identif Wasm currently supports only one memory, so we do not need to resolve any identifiers. ```k - rule #t2aDefn(( data _:Index (offset IS) DS )) => #data(0, #t2aInstrs(IS), #DS2Bytes(DS)) + rule #t2aDefn(( data _:Index (offset IS) DS )) => #data(#DS2Bytes(DS), #active(0, #t2aInstrs(IS))) ``` #### Exports ```k - rule #t2aDefn(( export ENAME ( func IDENT:Identifier ) )) => #export(ENAME, {IDS[IDENT]}:>Int) requires IDENT in_keys(IDS) - rule #t2aDefn(( export ENAME ( global IDENT:Identifier ) )) => #export(ENAME, {IDS[IDENT]}:>Int) requires IDENT in_keys(IDS) - rule #t2aDefn<_>(( export ENAME ( func I:Int ) )) => #export(ENAME, I) - rule #t2aDefn<_>(( export ENAME ( global I:Int ) )) => #export(ENAME, I) + rule #t2aDefn(( export ENAME ( func IDENT:Identifier ) )) => #export(ENAME, #externIdxFunc({IDS[IDENT]}:>Int)) requires IDENT in_keys(IDS) + rule #t2aDefn(( export ENAME ( global IDENT:Identifier ) )) => #export(ENAME, #externIdxGlobal({IDS[IDENT]}:>Int)) requires IDENT in_keys(IDS) + rule #t2aDefn<_>(( export ENAME ( func I:Int ) )) => #export(ENAME, #externIdxFunc(I)) + rule #t2aDefn<_>(( export ENAME ( global I:Int ) )) => #export(ENAME, #externIdxGlobal(I)) - rule #t2aDefn<_>(( export ENAME ( table _ ) )) => #export(ENAME, 0) - rule #t2aDefn<_>(( export ENAME ( memory _ ) )) => #export(ENAME, 0) + rule #t2aDefn<_>(( export ENAME ( table _ ) )) => #export(ENAME, #externIdxTable(0)) + rule #t2aDefn<_>(( export ENAME ( memory _ ) )) => #export(ENAME, #externIdxMemory(0)) ``` #### Other Definitions diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md index 9ae1404df..80f97ec1c 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md @@ -1762,11 +1762,14 @@ Memories can be initialized with data, specified as a list of bytes together wit The `data` initializer simply puts these bytes into the specified memory, starting at the offset. ```k - syntax DataDefn ::= #data(index : Int, offset : Instrs, data : Bytes) [symbol(aDataDefn)] - | "data" "{" Int Bytes "}" + syntax DataDefn ::= #data(data: Bytes, mode: DataMode) [symbol(aDataDefn)] + syntax DataMode ::= #active(memidx: Int, offset: Instrs) [symbol(aDataModeActive)] + | "#passive" [symbol(aDataModePassive)] // -------------------------------------------- - // Default to memory 0. - rule #data(IDX, IS, DATA) => sequenceInstrs(IS) ~> data { IDX DATA } ... + syntax KItem ::= "data" "{" Int Bytes "}" + + rule #data(DATA, #active(IDX, IS)) => sequenceInstrs(IS) ~> data { IDX DATA } ... + rule #data(_DATA, #passive) => .K ... rule data { MEMIDX DSBYTES } => trap ... < i32 > OFFSET : _STACK @@ -1783,7 +1786,6 @@ The `data` initializer simply puts these bytes into the specified memory, starti requires OFFSET +Int lengthBytes(DSBYTES) >Int SIZE *Int #pageSize() - // For now, deal only with memory 0. rule data { MEMIDX DSBYTES } => .K ... < i32 > OFFSET : STACK => STACK CUR @@ -1828,7 +1830,12 @@ Export Exports make functions, tables, memories and globals available for importing into other modules. ```k - syntax ExportDefn ::= #export(name : WasmString, index : Int) [symbol(aExportDefn)] + syntax ExportDefn ::= #export(name : WasmString, index : ExternIdx) [symbol(aExportDefn)] + syntax ExternIdx ::= #externIdxFunc(Int) [symbol(aExternIdxFunc)] + | #externIdxTable(Int) [symbol(aExternIdxTable)] + | #externIdxMemory(Int) [symbol(aExternIdxMemory)] + | #externIdxGlobal(Int) [symbol(aExternIdxGlobal)] + | #externIdxTag(Int) [symbol(aExternIdxTag)] syntax Alloc ::= ExportDefn // --------------------------- rule #export(ENAME, IDX) => .K ... diff --git a/pykwasm/src/pykwasm/kwasm_ast.py b/pykwasm/src/pykwasm/kwasm_ast.py index a818e0a35..a0480e8a8 100644 --- a/pykwasm/src/pykwasm/kwasm_ast.py +++ b/pykwasm/src/pykwasm/kwasm_ast.py @@ -53,6 +53,8 @@ ELEM = 'aElemDefn' DATA = 'aDataDefn' +DATAMODE_ACTIVE = 'aDataModeActive' +DATAMODE_PASSIVE = 'aDataModePassive' START = 'aStartDefn' @@ -164,6 +166,9 @@ def idx_to_ref(idx: int | None) -> KInner: MUT_CONST = KApply('mutConst', []) MUT_VAR = KApply('mutVar', []) +HEAPTYPE_FUNC = KApply('func', []) +HEAPTYPE_EXTERN = KApply('extern', []) + def vec_type(valtypes: KInner) -> KInner: return KApply(VEC_TYPE, [valtypes]) @@ -173,7 +178,7 @@ def func_type(params: KInner, results: KInner) -> KInner: return KApply(FUNC_TYPE, [params, results]) -def limits(tup: tuple[int, int]) -> KInner: +def limits(tup: tuple[int, int | None]) -> KInner: i = tup[0] j = tup[1] if j is None: @@ -228,9 +233,9 @@ def CALL(function_idx: int) -> KInner: return KApply('aCall', [KInt(function_idx)]) -def CALL_INDIRECT(type_idx: int) -> KInner: +def CALL_INDIRECT(table_idx: int, type_idx: int) -> KInner: type_use = KApply('aTypeUseIndex', [KInt(type_idx)]) - return KApply('aCall_indirect', [KInt(0), type_use]) + return KApply('aCall_indirect', [KInt(table_idx), type_use]) ########################## @@ -245,8 +250,8 @@ def REF_FUNC(func_idx: int) -> KInner: REF_IS_NULL: KInner = KApply('aRef.is_null', []) -def REF_NULL(t: str) -> KInner: - return KApply('aRef.null', [KApply(t, [])]) +def REF_NULL(heaptype: KInner) -> KInner: + return KApply('aRef.null', [heaptype]) ########################## @@ -626,11 +631,11 @@ def global_desc(global_type: KInner, id: KInner = EMPTY_ID) -> KInner: return KApply(GLOBAL_DESC, [id, global_type]) -def table_desc(lim: tuple[int, int], id: KInner = EMPTY_ID) -> KInner: +def table_desc(lim: tuple[int, int | None], id: KInner = EMPTY_ID) -> KInner: return KApply(TABLE_DESC, [id, limits(lim)]) -def memory_desc(lim: tuple[int, int], id: KInner = EMPTY_ID) -> KInner: +def memory_desc(lim: tuple[int, int | None], id: KInner = EMPTY_ID) -> KInner: return KApply(MEMORY_DESC, [id, limits(lim)]) @@ -647,11 +652,11 @@ def func(type: KInner, locals: KInner, body: KInner, metadata: KInner = EMPTY_FU return KApply(FUNC, [type, locals, body, metadata]) -def table(lim: tuple[int, int], typ: KInner, metadata: KInner = EMPTY_ID) -> KInner: +def table(lim: tuple[int, int | None], typ: KInner, metadata: KInner = EMPTY_ID) -> KInner: return KApply(TABLE, [limits(lim), typ, metadata]) -def memory(lim: tuple[int, int], metadata: KInner = EMPTY_ID) -> KInner: +def memory(lim: tuple[int, int | None], metadata: KInner = EMPTY_ID) -> KInner: return KApply(MEMORY, [limits(lim), metadata]) @@ -675,8 +680,16 @@ def elem(typ: KInner, elem_mode: KInner, init: Iterable[int | None], metadata: K return KApply(ELEM, [typ, refs(init), elem_mode, metadata]) -def data(memory_idx: int, offset: KInner, data: bytes) -> KInner: - return KApply(DATA, [KInt(memory_idx), offset, KBytes(data)]) +def data(bs: bytes, mode: KInner) -> KInner: + return KApply(DATA, [KBytes(bs), mode]) + + +def datamode_active(memidx: int, offset: KInner) -> KInner: + return KApply(DATAMODE_ACTIVE, [KInt(memidx), offset]) + + +def datamode_passive() -> KInner: + return KApply(DATAMODE_PASSIVE, []) def start(start_idx: int) -> KInner: @@ -687,8 +700,28 @@ def imp(mod_name: KInner, name: KInner, import_desc: KInner) -> KInner: return KApply(IMPORT, [mod_name, name, import_desc]) -def export(name: KInner, index: int) -> KInner: - return KApply(EXPORT, [name, KInt(index)]) +def export(name: KInner, index: KInner) -> KInner: + return KApply(EXPORT, [name, index]) + + +def externidx_func(i: int) -> KInner: + return KApply('aExternIdxFunc', [KInt(i)]) + + +def externidx_table(i: int) -> KInner: + return KApply('aExternIdxTable', [KInt(i)]) + + +def externidx_memory(i: int) -> KInner: + return KApply('aExternIdxMemory', [KInt(i)]) + + +def externidx_global(i: int) -> KInner: + return KApply('aExternIdxGlobal', [KInt(i)]) + + +def externidx_tag(i: int) -> KInner: + return KApply('aExternIdxTag', [KInt(i)]) def module_metadata(mid: None = None, fids: None = None, filename: str | None = None) -> KInner: diff --git a/pykwasm/src/pykwasm/wasm2kast.py b/pykwasm/src/pykwasm/wasm2kast.py index 91cea1ff6..d899f346e 100644 --- a/pykwasm/src/pykwasm/wasm2kast.py +++ b/pykwasm/src/pykwasm/wasm2kast.py @@ -10,7 +10,19 @@ from typing import TYPE_CHECKING from wasm import instructions -from wasm.datatypes import GlobalType, MemoryType, Mutability, TableType, TypeIdx, ValType, addresses +from wasm.datatypes import ( + FunctionIdx, + GlobalIdx, + GlobalType, + MemoryIdx, + MemoryType, + Mutability, + TableIdx, + TableType, + TypeIdx, + ValType, + addresses, +) from wasm.datatypes.element_segment import ElemModeActive, ElemModeDeclarative, ElemModePassive from wasm.opcodes import BinaryOpcode from wasm.parsers import parse_module @@ -162,7 +174,8 @@ def elem(e: ElementSegment): def data(d: DataSegment): offset = instrs(d.offset) - return a.data(d.memory_idx, offset, d.init) + mode = a.datamode_active(d.memory_idx, offset) + return a.data(d.init, mode) def start(s: StartFunction): @@ -188,7 +201,16 @@ def imp(i: Import): def export(e: Export): name = a.wasm_string(e.name) - idx = e.desc + if isinstance(e.desc, FunctionIdx): + idx = a.externidx_func(e.desc) + elif isinstance(e.desc, GlobalIdx): + idx = a.externidx_global(e.desc) + elif isinstance(e.desc, MemoryIdx): + idx = a.externidx_memory(e.desc) + elif isinstance(e.desc, TableIdx): + idx = a.externidx_table(e.desc) + else: + raise ValueError(f'Invalid extern index: {e.desc}') return a.export(name, idx) @@ -227,7 +249,7 @@ def instr(i): if i.opcode == B.CALL: return a.CALL(i.function_idx) if i.opcode == B.CALL_INDIRECT: - return a.CALL_INDIRECT(i.type_idx) + return a.CALL_INDIRECT(i.table_idx, i.type_idx) if i.opcode == B.ELSE: raise (ValueError('ELSE opcode: should have been filtered out.')) if i.opcode == B.END: @@ -321,9 +343,9 @@ def instr(i): return a.REF_FUNC(i.funcidx) if isinstance(i, instructions.RefNull): if i.reftype is addresses.FunctionAddress: - return a.REF_NULL('func') + return a.REF_NULL(a.HEAPTYPE_FUNC) if i.reftype is addresses.ExternAddress: - return a.REF_NULL('extern') + return a.REF_NULL(a.HEAPTYPE_EXTERN) raise ValueError(f'Unknown heap type: {i}, {i.reftype}') if isinstance(i, instructions.TableGet): return a.TABLE_GET(i.tableidx) From 111360377871cf3541474d189eaa68b8bd364316 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 3 Apr 2026 14:09:46 +0300 Subject: [PATCH 02/22] add binary parser for numbers --- pykwasm/src/pykwasm/binary/floats.py | 21 +++++ pykwasm/src/pykwasm/binary/integers.py | 75 +++++++++++++++++ pykwasm/src/pykwasm/binary/utils.py | 53 ++++++++++++ pykwasm/src/tests/unit/test_binary_parser.py | 86 ++++++++++++++++++++ 4 files changed, 235 insertions(+) create mode 100644 pykwasm/src/pykwasm/binary/floats.py create mode 100644 pykwasm/src/pykwasm/binary/integers.py create mode 100644 pykwasm/src/pykwasm/binary/utils.py create mode 100644 pykwasm/src/tests/unit/test_binary_parser.py diff --git a/pykwasm/src/pykwasm/binary/floats.py b/pykwasm/src/pykwasm/binary/floats.py new file mode 100644 index 000000000..de1dd5359 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/floats.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import struct +from typing import TYPE_CHECKING + +from .utils import read_bytes + +if TYPE_CHECKING: + from .utils import InputStream + + +def f32(s: InputStream) -> float: + bs = read_bytes(4, s) + f = struct.unpack(' float: + bs = read_bytes(8, s) + f = struct.unpack(' int: + n = read_byte(s) + + # No continuation bit + if n < 2**7: + if n >= 2**bits: + raise WasmParseError(f'Value {n} exceeds u{bits} range') + return n + + # Has continuation bit + if bits <= 7: # Not enough bits remaining + raise WasmParseError(f'LEB128 encoding too long for u{bits}') + + m = parse_uint(bits - 7, s) + return (n - 2**7) + (2**7) * m + + +def parse_sint(bits: int, s: InputStream) -> int: + n = read_byte(s) + + # No continuation bit and 0 sign bit + if n < 2**6: + if n >= 2 ** (bits - 1): + raise WasmParseError(f'Value {n} exceeds s{bits} range') + return n + + # No continuation bit and 1 sign bit + if 2**6 <= n and n < 2**7: + signed_value = n - 2**7 + if signed_value < -(2 ** (bits - 1)): + raise WasmParseError(f'Value {signed_value} exceeds s{bits} range') + return n - 2**7 + + # Has continuation bit + if bits <= 7: # Not enough bits remaining + raise WasmParseError(f'LEB128 encoding too long for s{bits}') + + i = parse_sint(bits - 7, s) + return (2**7) * i + (n - 2**7) + + +def u32(s: InputStream) -> int: + return parse_uint(32, s) + + +def u64(s: InputStream) -> int: + return parse_uint(64, s) + + +def to_uninterpreted(bits: int, x: int) -> int: + if 0 <= x: + return x + return x + 2 ** bits + +def parse_iint(bits: int, s: InputStream) -> int: + i = parse_sint(bits, s) + return to_uninterpreted(bits, i) + + +def i32(s: InputStream) -> int: + return parse_iint(32, s) + + +def i64(s: InputStream) -> int: + return parse_iint(64, s) diff --git a/pykwasm/src/pykwasm/binary/utils.py b/pykwasm/src/pykwasm/binary/utils.py new file mode 100644 index 000000000..4efa5d44d --- /dev/null +++ b/pykwasm/src/pykwasm/binary/utils.py @@ -0,0 +1,53 @@ +from typing import BinaryIO, Callable, TypeAlias, TypeVar + +InputStream: TypeAlias = BinaryIO + +A = TypeVar('A') +Parser: TypeAlias = Callable[[InputStream], A] +NParser: TypeAlias = Callable[[int, InputStream], A] + + +def read_bytes(n: int, s: InputStream) -> bytes: + b = s.read(n) + if len(b) != n: + raise WasmEOFError(f'Unexpected EOF. Expected {n} bytes, got {len(b)}') + return b + + +def read_byte(s: InputStream) -> int: + return read_bytes(1, s)[0] + + +def peek_bytes(n: int, s: InputStream) -> bytes: + pos = s.tell() + b = s.read(n) + if len(b) != n: + raise WasmEOFError(f'Unexpected EOF. Expected {n} bytes, got {len(b)}') + reset(pos, s) + return b + + +def peek_byte(s: InputStream) -> int: + return peek_bytes(1, s)[0] + + +def skip(n: int, s: InputStream) -> None: + s.seek(n, 1) + + +def reset(n: int, s: InputStream) -> None: + s.seek(n, 0) + + +def expect_bytes(expected: bytes, s: InputStream) -> None: + bs = read_bytes(len(expected), s) + if bs != expected: + raise WasmParseError(f'Expected {expected!r}, got {bs!r}') + + +class WasmParseError(Exception): + pass + + +class WasmEOFError(WasmParseError): + pass diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py new file mode 100644 index 000000000..f1f469356 --- /dev/null +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -0,0 +1,86 @@ +import io +import struct +import pytest + +from pykwasm.binary.utils import WasmParseError, WasmEOFError +from pykwasm.binary import floats, integers + +def stream(data: bytes) -> io.BytesIO: + """Helper: wrap bytes in a seekable stream.""" + return io.BytesIO(data) + + +class TestFloats: + VALUES = [0.0, 3.14, -1.5, 1.23456789, -9.99, float('inf'), float('-inf')] + + @pytest.mark.parametrize("value", VALUES) + def test_f32(self, value): + encoded = struct.pack(' bytes: + buf = [] + while True: + b = value & 0x7F + value >>= 7 + if value: + buf.append(b | 0x80) + else: + buf.append(b) + break + return bytes(buf) + + @staticmethod + def encode_sleb128(value: int) -> bytes: + buf = [] + while True: + b = value & 0x7F + value >>= 7 + if (value == 0 and b & 0x40 == 0) or (value == -1 and b & 0x40 != 0): + buf.append(b) + break + buf.append(b | 0x80) + return bytes(buf) + + @pytest.mark.parametrize("value", U32_VALUES) + def test_unsigned_32(self, value): + encoded = self.encode_uleb128(value) + assert integers.u32(stream(encoded)) == value + + @pytest.mark.parametrize("value", U64_VALUES) + def test_unsigned_64(self, value): + encoded = self.encode_uleb128(value) + assert integers.u64(stream(encoded)) == value + + @pytest.mark.parametrize("value", I32_VALUES) + def test_uninterpreted_32(self, value): + expected = integers.to_uninterpreted(32, value) + encoded = self.encode_sleb128(value) + assert integers.i32(stream(encoded)) == expected + + @pytest.mark.parametrize("value", I64_VALUES) + def test_uninterpreted_64(self, value): + expected = integers.to_uninterpreted(64, value) + encoded = self.encode_sleb128(value) + assert integers.i64(stream(encoded)) == expected \ No newline at end of file From e3dc7c9880b4980fdc7387f0e112819be6504d9f Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 3 Apr 2026 14:12:02 +0300 Subject: [PATCH 03/22] parse modules --- pykwasm/src/pykwasm/binary/__init__.py | 0 pykwasm/src/pykwasm/binary/combinators.py | 53 ++ pykwasm/src/pykwasm/binary/indices.py | 69 +++ pykwasm/src/pykwasm/binary/instructions.py | 541 +++++++++++++++++++++ pykwasm/src/pykwasm/binary/module.py | 345 +++++++++++++ pykwasm/src/pykwasm/binary/types.py | 204 ++++++++ 6 files changed, 1212 insertions(+) create mode 100644 pykwasm/src/pykwasm/binary/__init__.py create mode 100644 pykwasm/src/pykwasm/binary/combinators.py create mode 100644 pykwasm/src/pykwasm/binary/indices.py create mode 100644 pykwasm/src/pykwasm/binary/instructions.py create mode 100644 pykwasm/src/pykwasm/binary/module.py create mode 100644 pykwasm/src/pykwasm/binary/types.py diff --git a/pykwasm/src/pykwasm/binary/__init__.py b/pykwasm/src/pykwasm/binary/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pykwasm/src/pykwasm/binary/combinators.py b/pykwasm/src/pykwasm/binary/combinators.py new file mode 100644 index 000000000..0adc46731 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/combinators.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .integers import u32 +from .utils import WasmParseError, reset + +if TYPE_CHECKING: + from .utils import A, InputStream, Parser + + +def iterate(f: Parser[A], s: InputStream) -> list[A]: + results = [] + while True: + try: + results.append(f(s)) + except (WasmParseError, IndexError, ValueError): + break + return results + + +def sized(p: Parser[A], s: InputStream) -> A: + size = u32(s) + start_pos = s.tell() + res = p(s) + end_pos = s.tell() + if end_pos - start_pos != size: + raise WasmParseError('Size mismatch') + return res + + +def parse_n(p: Parser[A], n: int, s: InputStream) -> list[A]: + results = [] + for _ in range(n): + x = p(s) + results.append(x) + return results + + +def list_of(p: Parser[A], s: InputStream) -> list[A]: + n = u32(s) + return parse_n(p, n, s) + + +def either(ps: list[Parser[A]], s: InputStream) -> A: + for p in ps: + pos = s.tell() + try: + return p(s) + except WasmParseError: + reset(pos, s) + continue + raise WasmParseError('None of the alternatives succeeded') diff --git a/pykwasm/src/pykwasm/binary/indices.py b/pykwasm/src/pykwasm/binary/indices.py new file mode 100644 index 000000000..2e8d62628 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/indices.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pykwasm.kwasm_ast as wast + +from .integers import u32 +from .utils import WasmParseError, read_byte + +if TYPE_CHECKING: + from pyk.kast.inner import KInner + + from .utils import InputStream + + +def typeidx(s: InputStream) -> int: + return u32(s) + + +def funcidx(s: InputStream) -> int: + return u32(s) + + +def tableidx(s: InputStream) -> int: + return u32(s) + + +def memidx(s: InputStream) -> int: + return u32(s) + + +def globalidx(s: InputStream) -> int: + return u32(s) + + +def tagidx(s: InputStream) -> int: + return u32(s) + + +def elemidx(s: InputStream) -> int: + return u32(s) + + +def dataidx(s: InputStream) -> int: + return u32(s) + + +def localidx(s: InputStream) -> int: + return u32(s) + + +def labelidx(s: InputStream) -> int: + return u32(s) + + +def externidx(s: InputStream) -> KInner: + match read_byte(s): + case 0x00: + return wast.externidx_func(funcidx(s)) + case 0x01: + return wast.externidx_table(tableidx(s)) + case 0x02: + return wast.externidx_memory(memidx(s)) + case 0x03: + return wast.externidx_global(globalidx(s)) + case 0x04: + return wast.externidx_tag(tagidx(s)) + case x: + raise WasmParseError(f'Invalid externidx descriptor: {x}') diff --git a/pykwasm/src/pykwasm/binary/instructions.py b/pykwasm/src/pykwasm/binary/instructions.py new file mode 100644 index 000000000..6d95ad272 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/instructions.py @@ -0,0 +1,541 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pykwasm.kwasm_ast as wast +from pykwasm.binary.combinators import list_of +from pykwasm.binary.floats import f32, f64 +from pykwasm.binary.indices import elemidx, funcidx, globalidx, labelidx, localidx, memidx, tableidx, typeidx +from pykwasm.binary.integers import i32, i64, u32, u64 +from pykwasm.binary.types import heaptype, valtype +from pykwasm.binary.utils import WasmParseError, expect_bytes, peek_byte, read_byte, skip + +if TYPE_CHECKING: + from pyk.kast.inner import KInner + + from pykwasm.binary.utils import InputStream + + +def instr_seq(terminator: int, s: InputStream) -> list[KInner]: + res: list[KInner] = [] + + while peek_byte(s) != terminator: + i = instr(s) + res.append(i) + + expect_bytes(bytes([terminator]), s) + return res + + +def instr(s: InputStream) -> KInner: + def blocktype(s: InputStream) -> KInner: + if peek_byte(s) == 0x40: + skip(1, s) + return wast.vec_type(wast.val_types([])) + + try: + t = valtype(s) + return wast.vec_type(wast.val_types([t])) + except WasmParseError as e: + # TODO handle the `blocktype ::= i:i33` case + raise WasmParseError('Could not parse blocktype') from e + + # returns memory index, alignment, offset + def memarg(s: InputStream) -> tuple[int, int, int]: + n = u32(s) + if n < 2**6: + m = u64(s) + return 0, n, m + elif n < 2**7: + x = memidx(s) + m = u64(s) + return x, n - 2**6, m + else: + raise WasmParseError(f'Invalid memarg. n: {n}') + + opcode = read_byte(s) + # '0xFC' is a special opcode prefix shared by several instructions. + # These instructions are distinguished by a u32 value following the opcode. + additional_value: int | None = None + if opcode == 0xFC: + try: + additional_value = u32(s) + except Exception as e: + raise WasmParseError('Cannot parse opcode, expected u32 after 0xFC') from e + + match opcode: + # Parametric instructions + case 0x00: + return wast.UNREACHABLE + case 0x01: + return wast.NOP + case 0x1A: + return wast.DROP + case 0x1B: + return wast.SELECT + case 0x1C: + # TODO handle ts + ts = list_of(valtype, s) # noqa F841 + return wast.SELECT + + # Control instructions + case 0x02: + bt = blocktype(s) + ins = instr_seq(0x0B, s) + return wast.BLOCK(bt, wast.instrs(ins), wast.KInt(0)) # TODO either track block ids or deprecate them + case 0x03: + bt = blocktype(s) + ins = instr_seq(0x0B, s) + return wast.LOOP(bt, wast.instrs(ins), wast.KInt(0)) # TODO either track block ids or deprecate them + case 0x04: + bt = blocktype(s) + in1 = instr_seq(0x05, s) + in2 = instr_seq(0x0B, s) + return wast.IF( + bt, wast.instrs(in1), wast.instrs(in2), wast.KInt(0) + ) # TODO either track block ids or deprecate them + case 0x0C: + l = labelidx(s) + return wast.BR(l) + case 0x0D: + l = labelidx(s) + return wast.BR_IF(l) + case 0x0E: + ls = list_of(labelidx, s) + ln = labelidx(s) + return wast.BR_TABLE(tuple(ls), ln) + case 0x0F: + return wast.RETURN + case 0x10: + x = funcidx(s) + return wast.CALL(x) + case 0x11: + y = typeidx(s) + x = tableidx(s) + return wast.CALL_INDIRECT(x, y) + + # Variable instructions + case 0x20: + x = localidx(s) + return wast.GET_LOCAL(x) + case 0x21: + x = localidx(s) + return wast.SET_LOCAL(x) + case 0x22: + x = localidx(s) + return wast.TEE_LOCAL(x) + case 0x23: + x = globalidx(s) + return wast.GET_GLOBAL(x) + case 0x24: + x = globalidx(s) + return wast.SET_GLOBAL(x) + + # Table instructions + case 0x25: + x = tableidx(s) + return wast.TABLE_GET(x) + case 0x26: + x = tableidx(s) + return wast.TABLE_SET(x) + case 0xFC if additional_value == 12: + y = elemidx(s) + x = tableidx(s) + return wast.TABLE_INIT(x, y) + case 0xFC if additional_value == 13: + x = elemidx(s) + return wast.ELEM_DROP(x) + case 0xFC if additional_value == 14: + x1 = tableidx(s) + x2 = tableidx(s) + return wast.TABLE_COPY(x1, x2) + case 0xFC if additional_value == 15: + x = tableidx(s) + return wast.TABLE_GROW(x) + case 0xFC if additional_value == 16: + x = tableidx(s) + return wast.TABLE_SIZE(x) + case 0xFC if additional_value == 17: + x = tableidx(s) + return wast.TABLE_FILL(x) + + # Memory instructions + # TODO handle memory index and alignment + case 0x28: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_LOAD(offset) + case 0x29: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD(offset) + case 0x2A: + mx, align, offset = memarg(s) # noqa F841 + return wast.F32_LOAD(offset) + case 0x2B: + mx, align, offset = memarg(s) # noqa F841 + return wast.F64_LOAD(offset) + case 0x2C: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_LOAD8_S(offset) + case 0x2D: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_LOAD8_U(offset) + case 0x2E: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_LOAD16_S(offset) + case 0x2F: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_LOAD16_U(offset) + case 0x30: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD8_S(offset) + case 0x31: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD8_U(offset) + case 0x32: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD16_S(offset) + case 0x33: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD16_U(offset) + case 0x34: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD32_S(offset) + case 0x35: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_LOAD32_U(offset) + case 0x36: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_STORE(offset) + case 0x37: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_STORE(offset) + case 0x38: + mx, align, offset = memarg(s) # noqa F841 + return wast.F32_STORE(offset) + case 0x39: + mx, align, offset = memarg(s) # noqa F841 + return wast.F64_STORE(offset) + case 0x3A: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_STORE8(offset) + case 0x3B: + mx, align, offset = memarg(s) # noqa F841 + return wast.I32_STORE16(offset) + case 0x3C: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_STORE8(offset) + case 0x3D: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_STORE16(offset) + case 0x3E: + mx, align, offset = memarg(s) # noqa F841 + return wast.I64_STORE32(offset) + case 0x3F: + # TODO use memory index + _x = memidx(s) # noqa 841 + return wast.MEMORY_SIZE + case 0x40: + # TODO use memory index + _x = memidx(s) # noqa 841 + return wast.MEMORY_GROW + + # Reference instructions + case 0xD0: + ht = heaptype(s) + return wast.REF_NULL(ht) + case 0xD1: + return wast.REF_IS_NULL + case 0xD2: + x = funcidx(s) + return wast.REF_FUNC(x) + + # Numeric instructions + # Constants + case 0x41: + i = i32(s) + return wast.I32_CONST(i) + case 0x42: + i = i64(s) + return wast.I64_CONST(i) + case 0x43: + f = f32(s) + return wast.F32_CONST(f) + case 0x44: + f = f64(s) + return wast.F64_CONST(f) + + # i32 comparison + case 0x45: + return wast.I32_EQZ + case 0x46: + return wast.I32_EQ + case 0x47: + return wast.I32_NE + case 0x48: + return wast.I32_LT_S + case 0x49: + return wast.I32_LT_U + case 0x4A: + return wast.I32_GT_S + case 0x4B: + return wast.I32_GT_U + case 0x4C: + return wast.I32_LE_S + case 0x4D: + return wast.I32_LE_U + case 0x4E: + return wast.I32_GE_S + case 0x4F: + return wast.I32_GE_U + + # i64 comparison + case 0x50: + return wast.I64_EQZ + case 0x51: + return wast.I64_EQ + case 0x52: + return wast.I64_NE + case 0x53: + return wast.I64_LT_S + case 0x54: + return wast.I64_LT_U + case 0x55: + return wast.I64_GT_S + case 0x56: + return wast.I64_GT_U + case 0x57: + return wast.I64_LE_S + case 0x58: + return wast.I64_LE_U + case 0x59: + return wast.I64_GE_S + case 0x5A: + return wast.I64_GE_U + + # f32 comparison + case 0x5B: + return wast.F32_EQ + case 0x5C: + return wast.F32_NE + case 0x5D: + return wast.F32_LT + case 0x5E: + return wast.F32_GT + case 0x5F: + return wast.F32_LE + case 0x60: + return wast.F32_GE + + # f64 comparison + case 0x61: + return wast.F64_EQ + case 0x62: + return wast.F64_NE + case 0x63: + return wast.F64_LT + case 0x64: + return wast.F64_GT + case 0x65: + return wast.F64_LE + case 0x66: + return wast.F64_GE + + # i32 unary / binary + case 0x67: + return wast.I32_CLZ + case 0x68: + return wast.I32_CTZ + case 0x69: + return wast.I32_POPCNT + case 0x6A: + return wast.I32_ADD + case 0x6B: + return wast.I32_SUB + case 0x6C: + return wast.I32_MUL + case 0x6D: + return wast.I32_DIV_S + case 0x6E: + return wast.I32_DIV_U + case 0x6F: + return wast.I32_REM_S + case 0x70: + return wast.I32_REM_U + case 0x71: + return wast.I32_AND + case 0x72: + return wast.I32_OR + case 0x73: + return wast.I32_XOR + case 0x74: + return wast.I32_SHL + case 0x75: + return wast.I32_SHR_S + case 0x76: + return wast.I32_SHR_U + case 0x77: + return wast.I32_ROTL + case 0x78: + return wast.I32_ROTR + + # i64 unary / binary + case 0x79: + return wast.I64_CLZ + case 0x7A: + return wast.I64_CTZ + case 0x7B: + return wast.I64_POPCNT + case 0x7C: + return wast.I64_ADD + case 0x7D: + return wast.I64_SUB + case 0x7E: + return wast.I64_MUL + case 0x7F: + return wast.I64_DIV_S + case 0x80: + return wast.I64_DIV_U + case 0x81: + return wast.I64_REM_S + case 0x82: + return wast.I64_REM_U + case 0x83: + return wast.I64_AND + case 0x84: + return wast.I64_OR + case 0x85: + return wast.I64_XOR + case 0x86: + return wast.I64_SHL + case 0x87: + return wast.I64_SHR_S + case 0x88: + return wast.I64_SHR_U + case 0x89: + return wast.I64_ROTL + case 0x8A: + return wast.I64_ROTR + + # f32 unary / binary + case 0x8B: + return wast.F32_ABS + case 0x8C: + return wast.F32_NEG + case 0x8D: + return wast.F32_CEIL + case 0x8E: + return wast.F32_FLOOR + case 0x8F: + return wast.F32_TRUNC + case 0x90: + return wast.F32_NEAREST + case 0x91: + return wast.F32_SQRT + case 0x92: + return wast.F32_ADD + case 0x93: + return wast.F32_SUB + case 0x94: + return wast.F32_MUL + case 0x95: + return wast.F32_DIV + case 0x96: + return wast.F32_MIN + case 0x97: + return wast.F32_MAX + case 0x98: + return wast.F32_COPYSIGN + + # f64 unary / binary + case 0x99: + return wast.F64_ABS + case 0x9A: + return wast.F64_NEG + case 0x9B: + return wast.F64_CEIL + case 0x9C: + return wast.F64_FLOOR + case 0x9D: + return wast.F64_TRUNC + case 0x9E: + return wast.F64_NEAREST + case 0x9F: + return wast.F64_SQRT + case 0xA0: + return wast.F64_ADD + case 0xA1: + return wast.F64_SUB + case 0xA2: + return wast.F64_MUL + case 0xA3: + return wast.F64_DIV + case 0xA4: + return wast.F64_MIN + case 0xA5: + return wast.F64_MAX + case 0xA6: + return wast.F64_COPYSIGN + + # conversion instructions + case 0xA7: + return wast.I32_WRAP_I64 + case 0xA8: + return wast.I32_TRUNC_S_F32 + case 0xA9: + return wast.I32_TRUNC_U_F32 + case 0xAA: + return wast.I32_TRUNC_S_F64 + case 0xAB: + return wast.I32_TRUNC_U_F64 + case 0xAC: + return wast.I64_EXTEND_S_I32 + case 0xAD: + return wast.I64_EXTEND_U_I32 + case 0xAE: + return wast.I64_TRUNC_S_F32 + case 0xAF: + return wast.I64_TRUNC_U_F32 + case 0xB0: + return wast.I64_TRUNC_S_F64 + case 0xB1: + return wast.I64_TRUNC_U_F64 + case 0xB2: + return wast.F32_CONVERT_S_I32 + case 0xB3: + return wast.F32_CONVERT_U_I32 + case 0xB4: + return wast.F32_CONVERT_S_I64 + case 0xB5: + return wast.F32_CONVERT_U_I64 + case 0xB6: + return wast.F32_DEMOTE_F64 + case 0xB7: + return wast.F64_CONVERT_S_I32 + case 0xB8: + return wast.F64_CONVERT_U_I32 + case 0xB9: + return wast.F64_CONVERT_S_I64 + case 0xBA: + return wast.F64_CONVERT_U_I64 + case 0xBB: + return wast.F64_PROMOTE_F32 + + # sign-extension instructions + case 0xC0: + return wast.I32_EXTEND8_s + case 0xC1: + return wast.I32_EXTEND16_s + case 0xC2: + return wast.I64_EXTEND8_s + case 0xC3: + return wast.I64_EXTEND16_s + case 0xC4: + return wast.I64_EXTEND32_s + + # Handle unsupported opcodes + case _: + raise WasmParseError(f'Unsupported opcode: {opcode :#04x}, {additional_value}') + + +def expr(s: InputStream) -> list[KInner]: + return instr_seq(0x0B, s) diff --git a/pykwasm/src/pykwasm/binary/module.py b/pykwasm/src/pykwasm/binary/module.py new file mode 100644 index 000000000..38b991078 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/module.py @@ -0,0 +1,345 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pyk.kast.inner import KApply, KLabel, KSort, KToken + +import pykwasm.kwasm_ast as wast +from pykwasm.binary.indices import externidx, funcidx, memidx, tableidx, typeidx +from pykwasm.binary.instructions import expr + +from .combinators import list_of, sized +from .integers import u32 +from .types import externtype_as_import_desc, globaltype, memtype, rectype, reftype, tabletype, valtype +from .utils import WasmEOFError, WasmParseError, peek_byte, peek_bytes, read_byte, read_bytes, skip + +if TYPE_CHECKING: + from pyk.kast.inner import KInner + + from .types import RecType + from .utils import A, InputStream, Parser + +MAGIC = b'\x00\x61\x73\x6d' +VERSION = b'\x01\x00\x00\x00' + +EMPTY_SECTION: KInner = wast.EMPTY_DEFNS + + +def parse_magic(s: InputStream) -> None: + try: + val = s.read(4) + assert val == MAGIC + except Exception as e: + raise WasmParseError('Could not read magic value') from e + + +def parse_version(s: InputStream) -> None: + try: + val = s.read(4) + assert val == VERSION + except Exception as e: + raise WasmParseError('Could not read version') from e + + +def parse_custom_section(s: InputStream) -> bytes: + try: + n = peek_byte(s) + except WasmEOFError: + return b'' + + if n != 0: + return b'' + skip(1, s) + return read_bytes(n, s) + + +def parse_custom_sections(s: InputStream) -> list[bytes]: + sects = [] + while True: + sect = parse_custom_section(s) + if not sect: + break + sects.append(sect) + return sects + + +def section(sec_id: int, p: Parser[A], default: A, s: InputStream) -> A: + try: + n = peek_byte(s) + except WasmEOFError: + return default + + if n != sec_id: + return default + skip(1, s) + return sized(p, s) + + +# TODO support recursive types in type sections +def typesec(s: InputStream) -> KInner: + def rectype_to_type_decl(rc: RecType) -> KInner: + match rc: + case [(_, [], st)]: + return wast.type(st) + case _: + raise WasmParseError('recursive types are not supported') + + rectypes = list_of(rectype, s) + type_decls = [rectype_to_type_decl(t) for t in rectypes] + return wast.defns(type_decls) + + +def byte_list(s: InputStream) -> bytes: + n = u32(s) + return read_bytes(n, s) + + +def name(s: InputStream) -> str: + bs = byte_list(s) + return bs.decode('utf-8') + + +def importsec(s: InputStream) -> KInner: + def import_(s: InputStream) -> KInner: + nm1 = wast.wasm_string(name(s)) + nm2 = wast.wasm_string(name(s)) + desc = externtype_as_import_desc(s) + return wast.imp(nm1, nm2, desc) + + imports = list_of(import_, s) + return wast.defns(imports) + + +def funcsec(s: InputStream) -> list[int]: + return list_of(typeidx, s) + + +def tablesec(s: InputStream) -> KInner: + def table(s: InputStream) -> KInner: + match peek_bytes(2, s): + case b'\x40\x00': + raise WasmParseError('table_tt_with_initializer not implemented') + case _: + # TODO this should return '𝗍𝖺𝖻𝗅𝖾 tt (𝗋𝖾𝖿.π—‡π—Žπ—…π—… ht) if tt=at lim (𝗋𝖾𝖿 π—‡π—Žπ—…π—…? ht)' + _at, lim, rt = tabletype(s) + return wast.table(lim, rt) + + tables = list_of(table, s) + return wast.defns(tables) + + +def memsec(s: InputStream) -> KInner: + mems = list_of(memtype, s) + return wast.defns([wast.memory(lim) for _at, lim in mems]) + + +def globalsec(s: InputStream) -> KInner: + def glob(s: InputStream) -> KInner: + gt = globaltype(s) + e = wast.instrs(expr(s)) + return wast.glob(gt, e) + + return wast.defns(list_of(glob, s)) + + +def exportsec(s: InputStream) -> KInner: + def export(s: InputStream) -> KInner: + nm = name(s) + xx = externidx(s) + return wast.export(wast.wasm_string(nm), index=xx) + + return wast.defns(list_of(export, s)) + + +def startsec(s: InputStream) -> KInner: + start_func = funcidx(s) + return wast.defns([wast.start(start_func)]) + + +def elemsec(s: InputStream) -> KInner: + def elemkind(s: InputStream) -> KInner: + match read_byte(s): + case 0: + return wast.funcref + case x: + raise WasmParseError(f'Invalid elemkind: {x}') + + def elem_init(init: list[list[KInner]]) -> list[int | None]: + def expr_to_int(expr: list[KInner]) -> int | None: + # 'expr' must be a constant expression consisting of a reference instruction + match expr: + case [KApply(KLabel('aRef.func'), (KToken(sfuncidx, KSort('Int')),))]: + return int(sfuncidx) + case [KApply(KLabel('aRef.null'), _)]: + return None + case _: + raise WasmParseError(f'Expected a constant reference instruction as elem initializer, got {expr}') + + return [expr_to_int(e) for e in init] + + def elem(s: InputStream) -> KInner: + match read_byte(s): + case 0: + e0 = expr(s) + ys = list_of(funcidx, s) + return wast.elem(wast.funcref, wast.elem_active(0, wast.instrs(e0)), ys) + case 1: + rt = elemkind(s) + ys = list_of(funcidx, s) + return wast.elem(rt, wast.elem_passive(), ys) + case 2: + x = tableidx(s) + e = expr(s) + rt = elemkind(s) + ys = list_of(funcidx, s) + return wast.elem(rt, wast.elem_active(x, wast.instrs(e)), ys) + case 3: + rt = elemkind(s) + ys = list_of(funcidx, s) + return wast.elem(rt, wast.elem_declarative(), ys) + case 4: + e0 = expr(s) + es = list_of(expr, s) + return wast.elem(wast.funcref, wast.elem_active(0, wast.instrs(e0)), elem_init(es)) + case 5: + rt = reftype(s) + es = list_of(expr, s) + return wast.elem(rt, wast.elem_passive(), elem_init(es)) + case 6: + x = tableidx(s) + e0 = expr(s) + rt = reftype(s) + es = list_of(expr, s) + return wast.elem(rt, wast.elem_active(x, wast.instrs(e0)), elem_init(es)) + case 7: + rt = reftype(s) + es = list_of(expr, s) + return wast.elem(rt, wast.elem_declarative(), elem_init(es)) + case x: + raise WasmParseError(f'Invalid elem descriptor: {x}') + + return wast.defns(list_of(elem, s)) + + +def codesec(s: InputStream) -> list[tuple[list[KInner], list[KInner]]]: + def locals(s: InputStream) -> tuple[int, KInner]: + n = u32(s) + t = valtype(s) + return n, t + + def func(s: InputStream) -> tuple[list[KInner], list[KInner]]: + locs = list_of(locals, s) + body = expr(s) + + locs_flattened = [t for n, t in locs for _ in range(n)] + return locs_flattened, body + + def code(s: InputStream) -> tuple[list[KInner], list[KInner]]: + return sized(func, s) + + return list_of(code, s) + + +def datasec(s: InputStream) -> KInner: + def data(s: InputStream) -> KInner: + match read_byte(s): + case 0: + e = expr(s) + bs = byte_list(s) + return wast.data(bs, wast.datamode_active(0, wast.instrs(e))) + case 1: + bs = byte_list(s) + return wast.data(bs, wast.datamode_passive()) + case 2: + x = memidx(s) + e = expr(s) + bs = byte_list(s) + return wast.data(bs, wast.datamode_active(x, wast.instrs(e))) + case x: + raise WasmParseError(f'Invalid data segment descriptor: {x}') + + return wast.defns(list_of(data, s)) + + +def datacntsec(s: InputStream) -> int | None: + return u32(s) + + +def parse_module(s: InputStream) -> KInner: + parse_magic(s) + parse_version(s) + + custom_sections = [] + customs = lambda: custom_sections.extend(parse_custom_sections(s)) + + customs() + + type_section = section(1, typesec, EMPTY_SECTION, s) + + customs() + + import_section = section(2, importsec, EMPTY_SECTION, s) + + customs() + + function_section: list[int] = section(3, funcsec, [], s) + + customs() + + table_section = section(4, tablesec, EMPTY_SECTION, s) + + customs() + + memory_section = section(5, memsec, EMPTY_SECTION, s) + + customs() + + global_section = section(6, globalsec, EMPTY_SECTION, s) + + customs() + + export_section = section(7, exportsec, EMPTY_SECTION, s) + + customs() + + start_section = section(8, startsec, EMPTY_SECTION, s) + + customs() + + elem_section = section(9, elemsec, EMPTY_SECTION, s) + + customs() + + data_cnt_section = section(12, datacntsec, None, s) # noqa f841 + + customs() + + code_section: list[tuple[list[KInner], list[KInner]]] = section(10, codesec, [], s) + + customs() + + data_section = section(11, datasec, EMPTY_SECTION, s) + + customs() + + functions = [ + wast.func( + wast.KInt(typ_idx), + wast.vec_type(wast.val_types(locals)), + wast.instrs(code), + ) + for typ_idx, (locals, code) in zip(function_section, code_section, strict=True) + ] + + return wast.module( + types=type_section, + funcs=wast.defns(functions), + tables=table_section, + mems=memory_section, + globs=global_section, + elem=elem_section, + data=data_section, + start=start_section, + imports=import_section, + exports=export_section, + ) diff --git a/pykwasm/src/pykwasm/binary/types.py b/pykwasm/src/pykwasm/binary/types.py new file mode 100644 index 000000000..b5cecae18 --- /dev/null +++ b/pykwasm/src/pykwasm/binary/types.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pykwasm.binary.indices import typeidx + +from .. import kwasm_ast as wast +from .combinators import either, list_of +from .integers import u64 +from .utils import WasmParseError, peek_byte, read_byte, skip + +if TYPE_CHECKING: + from typing import TypeAlias + + from pyk.kast.inner import KInner + + from .utils import InputStream + +# subtype struct in recursive types +# subtype ::= sub final? typeuse* comptype +SubType: TypeAlias = tuple[bool, list[int], 'KInner'] + +# A group of mutually recursive composite types +# rectype :: = rec list(subtype) +RecType: TypeAlias = list[SubType] + + +def rectype(s: InputStream) -> RecType: + n = peek_byte(s) + if n == 0x4E: + skip(1, s) + sts = list_of(parse_subtype, s) + return sts + return [parse_subtype(s)] + + +def parse_subtype(s: InputStream) -> SubType: + n = peek_byte(s) + + if n == 0x4F: + skip(1, s) + xs = list_of(typeidx, s) + ct = parse_comptype(s) + return (True, xs, ct) + if n == 0x50: + skip(1, s) + xs = list_of(typeidx, s) + ct = parse_comptype(s) + return (False, xs, ct) + + ct = parse_comptype(s) + return (True, [], ct) + + +def parse_comptype(s: InputStream) -> KInner: + n = read_byte(s) + if n == 0x60: + ts1 = resulttype(s) + ts2 = resulttype(s) + return wast.func_type(wast.vec_type(wast.val_types(ts1)), wast.vec_type(wast.val_types(ts2))) + + if n == 0x5E: + raise WasmParseError('array composite type is not yet supported') + if n == 0x5F: + raise WasmParseError('struct composite type is not yet supported') + raise WasmParseError(f'composite type not yet supported: {n}') + + +def resulttype(s: InputStream) -> list[KInner]: + return list_of(valtype, s) + + +def valtype(s: InputStream) -> KInner: + d = peek_byte(s) + + try: + return either( + [ + numtype, + reftype, + # TODO implement vectypes + # vectype, + ], + s, + ) + except WasmParseError: + raise WasmParseError(f'Could not parse valtype. Descriptor: {d}') from None + + +def reftype(s: InputStream) -> KInner: + match read_byte(s): + case 0x70: + return wast.funcref + case 0x6F: + return wast.externref + case d: + raise WasmParseError(f'Unsupported reftype descriptor: {d}') + + +def numtype(s: InputStream) -> KInner: + match read_byte(s): + case 0x7C: + return wast.f64 + case 0x7D: + return wast.f32 + case 0x7E: + return wast.i64 + case 0x7F: + return wast.i32 + case d: + raise WasmParseError(f'Invalid numtype descriptor: {d}') + + +Limits: TypeAlias = tuple[int, int | None] + + +# Returns (addressType, (n, m)) +def limits(s: InputStream) -> tuple[KInner, Limits]: + match read_byte(s): + case 0x00: + n = u64(s) + return wast.i32, (n, None) + case 0x01: + n = u64(s) + m = u64(s) + return wast.i32, (n, m) + case 0x02: + n = u64(s) + return wast.i64, (n, None) + case 0x03: + n = u64(s) + m = u64(s) + return wast.i64, (n, m) + case d: + raise WasmParseError(f'Invalid limit descriptor: {d}') + + +# Returns (addressType, limits, reftype) +def tabletype(s: InputStream) -> tuple[KInner, Limits, KInner]: + rt = reftype(s) + at, lim = limits(s) + return at, lim, rt + + +# Returns (addressType, limits) +def memtype(s: InputStream) -> tuple[KInner, Limits]: + return limits(s) + + +def mut(s: InputStream) -> KInner: + match read_byte(s): + case 0x00: + return wast.MUT_CONST + case 0x01: + return wast.MUT_VAR + case d: + raise WasmParseError(f'Invalid mut descriptor. Expected 0x00 or 0x01, got: {d}') + + +def globaltype(s: InputStream) -> KInner: + t = valtype(s) + m = mut(s) + return wast.global_type(m, t) + + +def tagtype(s: InputStream) -> int: + match read_byte(s): + case 0x00: + return typeidx(s) + case d: + raise WasmParseError(f'Invalid tagtype descriptor. Expected 0x00, got: {d}') + + +# TODO this function parses externtype from wasm 3.0 but returns ImportDesc from Wasm 1.0. +# Update the ImportDefn K definition to reflect the changes. +def externtype_as_import_desc(s: InputStream) -> KInner: + match read_byte(s): + case 0x00: + x = typeidx(s) + return wast.func_desc(x) + case 0x01: + _at, lim, _rt = tabletype(s) + return wast.table_desc(lim) + case 0x02: + _at, lim = memtype(s) + return wast.memory_desc(lim) + case 0x03: + gt = globaltype(s) + return wast.global_desc(gt) + case 0x04: + # jt = tagtype(s) + raise WasmParseError('externtype tagtype variant is not yet supported.') + case d: + raise WasmParseError(f'Invalid externtype descriptor. Expected [0x00-0x04], got: {d}') + + +def heaptype(s: InputStream) -> KInner: + match read_byte(s): + case 0x6F: + return wast.HEAPTYPE_EXTERN + case 0x70: + return wast.HEAPTYPE_FUNC + case x: + raise WasmParseError(f'Unsupported heaptype descriptor: {x}') From 56e7be26a97a1365357702d35d12800233e59877 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 3 Apr 2026 14:12:33 +0300 Subject: [PATCH 04/22] add integrations tests and compare legacy and new parser --- .../tests/integration/test_binary_parser.py | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/pykwasm/src/tests/integration/test_binary_parser.py b/pykwasm/src/tests/integration/test_binary_parser.py index d0c76404f..20dd00a2e 100644 --- a/pykwasm/src/tests/integration/test_binary_parser.py +++ b/pykwasm/src/tests/integration/test_binary_parser.py @@ -7,18 +7,21 @@ from typing import TYPE_CHECKING import pytest -from pyk.kast.inner import KSequence, KSort, Subst +from pyk.kast.inner import KApply, KLabel, KSequence, KSort, Subst from pyk.kast.manip import split_config_from +from pyk.kast.prelude.utils import token +from pykwasm.binary.module import parse_module from pykwasm.wasm2kast import wasm2kast if TYPE_CHECKING: from pyk.kast import KInner + from pyk.kore.syntax import Pattern from pyk.ktool.krun import KRun BINARY_DIR = Path(__file__).parent / 'binary' -BINARY_WAT_FILES = BINARY_DIR.glob('*.wat') +BINARY_WAT_FILES = list(BINARY_DIR.glob('*.wat')) sys.setrecursionlimit(1500000000) @@ -39,7 +42,49 @@ def test_wasm2kast(krun_llvm: KRun, wat_path: Path) -> None: run_module(krun_llvm, module) -def run_module(krun: KRun, parsed_module: KInner) -> None: +@pytest.mark.parametrize('wat_path', BINARY_WAT_FILES, ids=str) +def test_self_binary_parser(krun_llvm: KRun, wat_path: Path) -> None: + # Given + wat2wasm_cmd = ['wat2wasm', str(wat_path), '--output=/dev/stdout'] + proc_res = run(wat2wasm_cmd, check=True, capture_output=True) + wasm_file = BytesIO(proc_res.stdout) + + assert not proc_res.returncode + + # When + module = parse_module(wasm_file) + + # Then + + # Can convert to Kore successfully + krun_llvm.kast_to_kore(module, KSort('ModuleDecl')) + + # Can run (initiate) successfully + run_module(krun_llvm, module) + + +@pytest.mark.parametrize('wat_path', BINARY_WAT_FILES, ids=str) +def test_diff(krun_llvm: KRun, wat_path: Path) -> None: + # Given + wat2wasm_cmd = ['wat2wasm', str(wat_path), '--output=/dev/stdout'] + proc_res = run(wat2wasm_cmd, check=True, capture_output=True) + + assert not proc_res.returncode + + # When + module_wasm2kast = remove_block_ids(wasm2kast(BytesIO(proc_res.stdout))) + + module_self = parse_module(BytesIO(proc_res.stdout)) + + # Then + assert module_wasm2kast == module_self + + run_wasm2kast = run_module(krun_llvm, module_wasm2kast) + run_self = run_module(krun_llvm, module_self) + assert run_wasm2kast == run_self + + +def run_module(krun: KRun, parsed_module: KInner) -> Pattern: try: # Create an initial config config_kast = krun.definition.init_config(KSort('GeneratedTopCell')) @@ -53,7 +98,23 @@ def run_module(krun: KRun, parsed_module: KInner) -> None: config_kore = krun.kast_to_kore(config_with_module, KSort('GeneratedTopCell')) # Run the config - krun.run_pattern(config_kore) + return krun.run_pattern(config_kore) except Exception as e: raise Exception('Received error while running') from e + + +def remove_block_ids(k: KInner) -> KInner: + match k: + + case KApply(KLabel('aBlock'), (vec_type, instrs, _)): + return KApply('aBlock', (vec_type, remove_block_ids(instrs), token(0))) + + case KApply(KLabel('aIf'), (vec_type, then_instrs, else_instrs, _)): + return KApply('aIf', (vec_type, remove_block_ids(then_instrs), remove_block_ids(else_instrs), token(0))) + + case KApply(KLabel('aLoop'), (vec_type, instrs, _)): + return KApply(KLabel('aLoop'), (vec_type, remove_block_ids(instrs), token(0))) + + case _: + return k.map_inner(remove_block_ids) From 4249fc42875521c96ec3aaf22be0b4f6638ab5aa Mon Sep 17 00:00:00 2001 From: devops Date: Fri, 3 Apr 2026 20:35:22 +0000 Subject: [PATCH 05/22] Set Version: 0.1.149 --- package/version | 2 +- pykwasm/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/version b/package/version index 22a9c16f5..010fbfb88 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.146 +0.1.149 diff --git a/pykwasm/pyproject.toml b/pykwasm/pyproject.toml index a5086deed..f4ae91554 100644 --- a/pykwasm/pyproject.toml +++ b/pykwasm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pykwasm" -version = "0.1.146" +version = "0.1.149" description = "" readme = "README.md" requires-python = "~=3.10" From 9992ca9faafb07d15b409d5c9e041b4bbb4cbf7d Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Mon, 6 Apr 2026 16:42:58 +0300 Subject: [PATCH 06/22] format --- pykwasm/src/pykwasm/binary/integers.py | 3 +- .../tests/integration/test_binary_parser.py | 8 ++--- pykwasm/src/tests/unit/test_binary_parser.py | 34 ++++++++++--------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/integers.py b/pykwasm/src/pykwasm/binary/integers.py index 0f4479178..e62f95a65 100644 --- a/pykwasm/src/pykwasm/binary/integers.py +++ b/pykwasm/src/pykwasm/binary/integers.py @@ -60,7 +60,8 @@ def u64(s: InputStream) -> int: def to_uninterpreted(bits: int, x: int) -> int: if 0 <= x: return x - return x + 2 ** bits + return x + 2**bits + def parse_iint(bits: int, s: InputStream) -> int: i = parse_sint(bits, s) diff --git a/pykwasm/src/tests/integration/test_binary_parser.py b/pykwasm/src/tests/integration/test_binary_parser.py index 20dd00a2e..d15873516 100644 --- a/pykwasm/src/tests/integration/test_binary_parser.py +++ b/pykwasm/src/tests/integration/test_binary_parser.py @@ -106,15 +106,15 @@ def run_module(krun: KRun, parsed_module: KInner) -> Pattern: def remove_block_ids(k: KInner) -> KInner: match k: - + case KApply(KLabel('aBlock'), (vec_type, instrs, _)): return KApply('aBlock', (vec_type, remove_block_ids(instrs), token(0))) - + case KApply(KLabel('aIf'), (vec_type, then_instrs, else_instrs, _)): return KApply('aIf', (vec_type, remove_block_ids(then_instrs), remove_block_ids(else_instrs), token(0))) - + case KApply(KLabel('aLoop'), (vec_type, instrs, _)): return KApply(KLabel('aLoop'), (vec_type, remove_block_ids(instrs), token(0))) - + case _: return k.map_inner(remove_block_ids) diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index f1f469356..9199d216b 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -1,9 +1,11 @@ import io import struct + import pytest -from pykwasm.binary.utils import WasmParseError, WasmEOFError from pykwasm.binary import floats, integers +from pykwasm.binary.utils import WasmEOFError + def stream(data: bytes) -> io.BytesIO: """Helper: wrap bytes in a seekable stream.""" @@ -13,21 +15,21 @@ def stream(data: bytes) -> io.BytesIO: class TestFloats: VALUES = [0.0, 3.14, -1.5, 1.23456789, -9.99, float('inf'), float('-inf')] - @pytest.mark.parametrize("value", VALUES) - def test_f32(self, value): + @pytest.mark.parametrize('value', VALUES) + def test_f32(self, value: float) -> None: encoded = struct.pack(' None: encoded = struct.pack(' None: with pytest.raises(WasmEOFError): floats.f32(stream(b'\x00\x01')) # only 2 bytes, needs 4 - def test_f64_eof_raises(self): + def test_f64_eof_raises(self) -> None: with pytest.raises(WasmEOFError): floats.f64(stream(b'\x00\x01\x02\x03\x04')) # only 5 bytes, needs 8 @@ -63,24 +65,24 @@ def encode_sleb128(value: int) -> bytes: buf.append(b | 0x80) return bytes(buf) - @pytest.mark.parametrize("value", U32_VALUES) - def test_unsigned_32(self, value): + @pytest.mark.parametrize('value', U32_VALUES) + def test_unsigned_32(self, value: int) -> None: encoded = self.encode_uleb128(value) assert integers.u32(stream(encoded)) == value - @pytest.mark.parametrize("value", U64_VALUES) - def test_unsigned_64(self, value): + @pytest.mark.parametrize('value', U64_VALUES) + def test_unsigned_64(self, value: int) -> None: encoded = self.encode_uleb128(value) assert integers.u64(stream(encoded)) == value - @pytest.mark.parametrize("value", I32_VALUES) - def test_uninterpreted_32(self, value): + @pytest.mark.parametrize('value', I32_VALUES) + def test_uninterpreted_32(self, value: int) -> None: expected = integers.to_uninterpreted(32, value) encoded = self.encode_sleb128(value) assert integers.i32(stream(encoded)) == expected - @pytest.mark.parametrize("value", I64_VALUES) - def test_uninterpreted_64(self, value): + @pytest.mark.parametrize('value', I64_VALUES) + def test_uninterpreted_64(self, value: int) -> None: expected = integers.to_uninterpreted(64, value) encoded = self.encode_sleb128(value) - assert integers.i64(stream(encoded)) == expected \ No newline at end of file + assert integers.i64(stream(encoded)) == expected From be1806369fe829ba861ff8f63062858837123d84 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Mon, 6 Apr 2026 16:48:40 +0300 Subject: [PATCH 07/22] pyupgrade --- pykwasm/src/pykwasm/binary/utils.py | 3 ++- pykwasm/uv.lock | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/utils.py b/pykwasm/src/pykwasm/binary/utils.py index 4efa5d44d..7934848dc 100644 --- a/pykwasm/src/pykwasm/binary/utils.py +++ b/pykwasm/src/pykwasm/binary/utils.py @@ -1,4 +1,5 @@ -from typing import BinaryIO, Callable, TypeAlias, TypeVar +from typing import BinaryIO, TypeAlias, TypeVar +from collections.abc import Callable InputStream: TypeAlias = BinaryIO diff --git a/pykwasm/uv.lock b/pykwasm/uv.lock index ece16d297..e716cf377 100644 --- a/pykwasm/uv.lock +++ b/pykwasm/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <4" resolution-markers = [ "python_full_version >= '3.11'", @@ -432,7 +432,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1055,7 +1055,7 @@ wheels = [ [[package]] name = "pykwasm" -version = "0.1.146" +version = "0.1.149" source = { editable = "." } dependencies = [ { name = "kframework" }, From cd839324b95f3a094d4dfb10206f2c4e57f5caa1 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Mon, 6 Apr 2026 17:14:56 +0300 Subject: [PATCH 08/22] organize imports --- pykwasm/src/pykwasm/binary/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/utils.py b/pykwasm/src/pykwasm/binary/utils.py index 7934848dc..232d2dcf0 100644 --- a/pykwasm/src/pykwasm/binary/utils.py +++ b/pykwasm/src/pykwasm/binary/utils.py @@ -1,11 +1,16 @@ -from typing import BinaryIO, TypeAlias, TypeVar -from collections.abc import Callable +from __future__ import annotations -InputStream: TypeAlias = BinaryIO +from typing import TYPE_CHECKING -A = TypeVar('A') -Parser: TypeAlias = Callable[[InputStream], A] -NParser: TypeAlias = Callable[[int, InputStream], A] +if TYPE_CHECKING: + from collections.abc import Callable + from typing import TYPE_CHECKING, BinaryIO, TypeAlias, TypeVar + + InputStream: TypeAlias = BinaryIO + + A = TypeVar('A') + Parser: TypeAlias = Callable[[InputStream], A] + NParser: TypeAlias = Callable[[int, InputStream], A] def read_bytes(n: int, s: InputStream) -> bytes: From 12a946f1ff3f639da295c784213fb93976633960 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Tue, 7 Apr 2026 11:16:37 +0300 Subject: [PATCH 09/22] fix ``: migrate to typed export indices --- pykwasm/src/pykwasm/kdist/wasm-semantics/test.md | 4 ++-- pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md index 26f726dc4..f264266ae 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/test.md @@ -217,7 +217,7 @@ We allow 2 kinds of actions: rule invoke MODIDX:Int ENAME:WasmString => ( invoke FUNCADDRS {{ IDX }} orDefault -1 ):Instr ... MODIDX - ... ENAME |-> IDX ... + ... ENAME |-> #externIdxFunc( IDX ) ... FUNCADDRS ... @@ -232,7 +232,7 @@ We allow 2 kinds of actions: rule get MODIDX:Int NAME:WasmString => VAL ... MODIDX - ... NAME |-> TFIDX ... + ... NAME |-> #externIdxGlobal( TFIDX ) ... IDS ... #ContextLookup(IDS, TFIDX) |-> ADDR ... ... diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md index b6270c398..62b5afc0b 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md @@ -213,7 +213,7 @@ module WASM 0 - .Map + .Map // WasmString -> ExternIdx .Map 0 .ListInt @@ -1872,7 +1872,7 @@ The value of a global gets copied when it is imported. MODIDX FS2 - ... NAME |-> IDX ... + ... NAME |-> #externIdxFunc( IDX ) ... ... requires isListIndex(IDX, FS2) @@ -1911,7 +1911,7 @@ The value of a global gets copied when it is imported. MODIDX IDS' ... #ContextLookup(IDS' , TFIDX) |-> ADDR ... - ... NAME |-> TFIDX ... + ... NAME |-> #externIdxTable( TFIDX ) ... ... @@ -1935,7 +1935,7 @@ The value of a global gets copied when it is imported. MODIDX IDS' ... #ContextLookup(IDS' , TFIDX) |-> ADDR ... - ... NAME |-> TFIDX ... + ... NAME |-> #externIdxMemory( TFIDX ) ... ... @@ -1960,7 +1960,7 @@ The value of a global gets copied when it is imported. MODIDX IDS' ... #ContextLookup(IDS' , TFIDX) |-> ADDR ... - ... NAME |-> TFIDX ... + ... NAME |-> #externIdxGlobal( TFIDX ) ... ... From d16b9cc29fe840be3c0fa489638461576bd8b8ea Mon Sep 17 00:00:00 2001 From: devops Date: Thu, 21 May 2026 18:25:01 +0000 Subject: [PATCH 10/22] Set Version: 0.1.155 --- package/version | 2 +- pykwasm/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/version b/package/version index 3671a92a3..7a29ae6cd 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.154 +0.1.155 diff --git a/pykwasm/pyproject.toml b/pykwasm/pyproject.toml index 7684dc8a1..93762a796 100644 --- a/pykwasm/pyproject.toml +++ b/pykwasm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pykwasm" -version = "0.1.154" +version = "0.1.155" description = "" readme = "README.md" requires-python = "~=3.10" From 7225499ee849f34433ab4064b036ceac1d447813 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Wed, 24 Jun 2026 12:51:55 +0300 Subject: [PATCH 11/22] migrate to the new parser --- pykwasm/pyproject.toml | 3 +- pykwasm/src/pykwasm/binary/__init__.py | 1 + pykwasm/src/pykwasm/wasm2kast.py | 397 +----------------- .../tests/integration/test_binary_parser.py | 62 +-- pykwasm/uv.lock | 118 +----- 5 files changed, 15 insertions(+), 566 deletions(-) diff --git a/pykwasm/pyproject.toml b/pykwasm/pyproject.toml index 93762a796..3e48769e6 100644 --- a/pykwasm/pyproject.toml +++ b/pykwasm/pyproject.toml @@ -9,8 +9,7 @@ description = "" readme = "README.md" requires-python = "~=3.10" dependencies = [ - "kframework>=7.1.323", - "py-wasm@git+https://github.com/runtimeverification/py-wasm.git@0.3.0" + "kframework>=7.1.323" ] [[project.authors]] diff --git a/pykwasm/src/pykwasm/binary/__init__.py b/pykwasm/src/pykwasm/binary/__init__.py index e69de29bb..f0fe51e0c 100644 --- a/pykwasm/src/pykwasm/binary/__init__.py +++ b/pykwasm/src/pykwasm/binary/__init__.py @@ -0,0 +1 @@ +from .module import parse_module diff --git a/pykwasm/src/pykwasm/wasm2kast.py b/pykwasm/src/pykwasm/wasm2kast.py index dcfbf1457..3d85aba79 100644 --- a/pykwasm/src/pykwasm/wasm2kast.py +++ b/pykwasm/src/pykwasm/wasm2kast.py @@ -9,49 +9,12 @@ import sys from typing import TYPE_CHECKING -from wasm import instructions -from wasm.datatypes import ( - FunctionIdx, - GlobalIdx, - GlobalType, - MemoryIdx, - MemoryType, - Mutability, - TableIdx, - TableType, - TypeIdx, - ValType, - addresses, -) -from wasm.datatypes.element_segment import ElemModeActive, ElemModeDeclarative, ElemModePassive -from wasm.instructions import InstructionWithPos -from wasm.opcodes import BinaryOpcode -from wasm.parsers import parse_module - -from pykwasm import kwasm_ast as a +from .binary import parse_module if TYPE_CHECKING: - from collections.abc import Iterable from typing import IO from pyk.kast import KInner - from wasm.datatypes import ( - DataSegment, - ElementSegment, - Export, - Function, - FunctionType, - Global, - Import, - Limits, - Memory, - Module, - RefType, - StartFunction, - Table, - ) - from wasm.datatypes.element_segment import ElemMode - from wasm.instructions import BaseInstruction def main(): @@ -65,354 +28,14 @@ def main(): def wasm2kast(wasm_bytes: IO[bytes], filename=None) -> KInner: - """Returns a dictionary representing the Kast JSON.""" - ast = parse_module(wasm_bytes) - return ast2kast(ast, filename=filename) - - -def ast2kast(wasm_ast: Module, filename=None) -> KInner: - """Returns a dictionary representing the Kast JSON.""" - types = a.defns([typ(x) for x in wasm_ast.types]) - funcs = a.defns([func(x) for x in wasm_ast.funcs]) - tables = a.defns([table(x) for x in wasm_ast.tables]) - mems = a.defns([memory(x) for x in wasm_ast.mems]) - globs = a.defns([glob(x) for x in wasm_ast.globals]) - elems = a.defns([elem(x) for x in wasm_ast.elem]) - datas = a.defns([data(x) for x in wasm_ast.data]) - starts = a.defns(start(wasm_ast.start)) - imports = a.defns([imp(x) for x in wasm_ast.imports]) - exports = a.defns([export(x) for x in wasm_ast.exports]) - meta = a.module_metadata(filename=filename) - return a.module( - types=types, - funcs=funcs, - tables=tables, - mems=mems, - globs=globs, - elem=elems, - data=datas, - start=starts, - imports=imports, - exports=exports, - metadata=meta, - ) - - -######### -# Defns # -######### - - -def typ(t: FunctionType): - return a.type(func_type(t.params, t.results)) - - -def func(f: Function): - type = a.KInt(f.type_idx) - ls_list = [val_type(x) for x in f.locals] - locals = a.vec_type(a.val_types(ls_list)) - body = instrs(f.body) - return a.func(type, locals, body) - - -def table(t: Table): - ls = limits(t.type.limits) - typ = ref_type(t.type.elem_type) - return a.table(ls, typ) - - -def memory(m: Memory): - ls = limits(m.type) - return a.memory(ls) - - -def glob(g: Global): - t = global_type(g.type) - init = instrs(g.init) - return a.glob(t, init) - - -def ref_type(t: RefType): - if t is addresses.FunctionAddress: - return a.funcref - if t is addresses.ExternAddress: - return a.externref - raise ValueError(f'Invalid RefType: {t}') - - -def elem_mode(m: ElemMode) -> KInner: - if isinstance(m, ElemModeActive): - offset = instrs(m.offset) - return a.elem_active(m.table, offset) - if isinstance(m, ElemModeDeclarative): - return a.elem_declarative() - if isinstance(m, ElemModePassive): - return a.elem_passive() - raise ValueError(f'Unknown ElemMode: {m}') - - -def elem_init(init: tuple[Iterable[BaseInstruction], ...]) -> Iterable[int | None]: - def expr_to_int(expr: Iterable[BaseInstruction]) -> int | None: - # 'expr' must be a constant expression consisting of a reference instruction - assert len(expr) == 1 or len(expr) == 2 and expr[1].opcode == BinaryOpcode.END, expr - instr = expr[0] - - if isinstance(instr, InstructionWithPos): - instr = instr.instruction - - if isinstance(instr, instructions.RefFunc): - return instr.funcidx - if isinstance(instr, instructions.RefNull): - return None - raise ValueError(f'Invalid reference expression: {expr}') - - return [expr_to_int(e) for e in init] - - -def elem(e: ElementSegment): - typ = ref_type(e.type) - mode = elem_mode(e.mode) - init = elem_init(e.init) - return a.elem(typ, mode, init) - - -def data(d: DataSegment): - offset = instrs(d.offset) - mode = a.datamode_active(d.memory_idx, offset) - return a.data(d.init, mode) - - -def start(s: StartFunction): - if s is None: - return [] - return [a.start(s.function_idx)] - - -def imp(i: Import): - mod_name = a.wasm_string(i.module_name) - name = a.wasm_string(i.as_name) - t = type(i.desc) - if t is TypeIdx: - desc = a.func_desc(i.desc) - elif t is GlobalType: - desc = a.global_desc(global_type(i.desc)) - elif t is TableType: - desc = a.table_desc(limits(i.desc.limits)) - elif t is MemoryType: - desc = a.memory_desc(limits(i.desc)) - return a.imp(mod_name, name, desc) - - -def export(e: Export): - name = a.wasm_string(e.name) - if isinstance(e.desc, FunctionIdx): - idx = a.externidx_func(e.desc) - elif isinstance(e.desc, GlobalIdx): - idx = a.externidx_global(e.desc) - elif isinstance(e.desc, MemoryIdx): - idx = a.externidx_memory(e.desc) - elif isinstance(e.desc, TableIdx): - idx = a.externidx_table(e.desc) - else: - raise ValueError(f'Invalid extern index: {e.desc}') - return a.export(name, idx) - - -########## -# Instrs # -########## - -block_id = 0 - - -def instrs(iis): - """Turn a list of instructions into KAST.""" - # We ignore `END`. - # The AST supplied by py-wasm has already parsed these and terminated the blocks. - # We also ignore `ELSE`. - # The AST supplied by py-wasm includes the statements in the else-branch as part of the `IF` instruction. - return a.instrs([instr(i) for i in iis if not i.opcode == BinaryOpcode.END and not i.opcode == BinaryOpcode.ELSE]) - - -def instr(i): - B = BinaryOpcode - global block_id - # TODO rewrite 'i.opcode == _' conditions as isinstance for better type-checking - if isinstance(i, InstructionWithPos): - inner = instr(i.instruction) - return a.INSTR_WITH_POS(inner, i.offset, i.length) - if i.opcode == B.BLOCK: - cur_block_id = block_id - block_id += 1 - iis = instrs(i.instructions) - res = vec_type(i.result_type) - return a.BLOCK(res, iis, a.KInt(cur_block_id)) - if i.opcode == B.BR: - return a.BR(i.label_idx) - if i.opcode == B.BR_IF: - return a.BR_IF(i.label_idx) - if i.opcode == B.BR_TABLE: - return a.BR_TABLE(i.label_indices, i.default_idx) - if i.opcode == B.CALL: - return a.CALL(i.function_idx) - if i.opcode == B.CALL_INDIRECT: - return a.CALL_INDIRECT(i.table_idx, i.type_idx) - if i.opcode == B.ELSE: - raise (ValueError('ELSE opcode: should have been filtered out.')) - if i.opcode == B.END: - raise (ValueError('End opcode: should have been filtered out.')) - if i.opcode == B.F32_CONST: - return a.F32_CONST(i.value) - if i.opcode == B.F64_CONST: - return a.F64_CONST(i.value) - if i.opcode == B.F32_REINTERPRET_I32: - raise (ValueError('Reinterpret instructions not implemented.')) - if i.opcode == B.F64_REINTERPRET_I64: - raise (ValueError('Reinterpret instructions not implemented.')) - if i.opcode == B.GET_GLOBAL: - return a.GET_GLOBAL(i.global_idx) - if i.opcode == B.GET_LOCAL: - return a.GET_LOCAL(i.local_idx) - if i.opcode == B.I32_CONST: - return a.I32_CONST(i.value) - if i.opcode == B.I64_CONST: - return a.I64_CONST(i.value) - if i.opcode == B.I32_REINTERPRET_F32: - raise (ValueError('Reinterpret instructions not implemented.')) - if i.opcode == B.I64_REINTERPRET_F64: - raise (ValueError('Reinterpret instructions not implemented.')) - if i.opcode == B.IF: - cur_block_id = block_id - block_id += 1 - thens = instrs(i.instructions) - els = instrs(i.else_instructions) - res = vec_type(i.result_type) - return a.IF(res, thens, els, a.KInt(cur_block_id)) - if i.opcode == B.F32_STORE: - return a.F32_STORE(i.memarg.offset) - if i.opcode == B.F64_STORE: - return a.F64_STORE(i.memarg.offset) - if i.opcode == B.I32_STORE: - return a.I32_STORE(i.memarg.offset) - if i.opcode == B.I64_STORE: - return a.I64_STORE(i.memarg.offset) - if i.opcode == B.I32_STORE16: - return a.I32_STORE16(i.memarg.offset) - if i.opcode == B.I64_STORE16: - return a.I64_STORE16(i.memarg.offset) - if i.opcode == B.I32_STORE8: - return a.I32_STORE8(i.memarg.offset) - if i.opcode == B.I64_STORE8: - return a.I64_STORE8(i.memarg.offset) - if i.opcode == B.I64_STORE32: - return a.I64_STORE32(i.memarg.offset) - if i.opcode == B.F32_LOAD: - return a.F32_LOAD(i.memarg.offset) - if i.opcode == B.F64_LOAD: - return a.F64_LOAD(i.memarg.offset) - if i.opcode == B.I32_LOAD: - return a.I32_LOAD(i.memarg.offset) - if i.opcode == B.I64_LOAD: - return a.I64_LOAD(i.memarg.offset) - if i.opcode == B.I32_LOAD16_S: - return a.I32_LOAD16_S(i.memarg.offset) - if i.opcode == B.I32_LOAD16_U: - return a.I32_LOAD16_U(i.memarg.offset) - if i.opcode == B.I64_LOAD16_S: - return a.I64_LOAD16_S(i.memarg.offset) - if i.opcode == B.I64_LOAD16_U: - return a.I64_LOAD16_U(i.memarg.offset) - if i.opcode == B.I32_LOAD8_S: - return a.I32_LOAD8_S(i.memarg.offset) - if i.opcode == B.I32_LOAD8_U: - return a.I32_LOAD8_U(i.memarg.offset) - if i.opcode == B.I64_LOAD8_S: - return a.I64_LOAD8_S(i.memarg.offset) - if i.opcode == B.I64_LOAD8_U: - return a.I64_LOAD8_U(i.memarg.offset) - if i.opcode == B.I64_LOAD32_S: - return a.I64_LOAD32_S(i.memarg.offset) - if i.opcode == B.I64_LOAD32_U: - return a.I64_LOAD32_U(i.memarg.offset) - if i.opcode == B.LOOP: - cur_block_id = block_id - block_id += 1 - iis = instrs(i.instructions) - res = vec_type(i.result_type) - return a.LOOP(res, iis, a.KInt(cur_block_id)) - if i.opcode == B.SET_GLOBAL: - return a.SET_GLOBAL(i.global_idx) - if i.opcode == B.SET_LOCAL: - return a.SET_LOCAL(i.local_idx) - if i.opcode == B.TEE_LOCAL: - return a.TEE_LOCAL(i.local_idx) - if isinstance(i, instructions.RefFunc): - return a.REF_FUNC(i.funcidx) - if isinstance(i, instructions.RefNull): - if i.reftype is addresses.FunctionAddress: - return a.REF_NULL(a.HEAPTYPE_FUNC) - if i.reftype is addresses.ExternAddress: - return a.REF_NULL(a.HEAPTYPE_EXTERN) - raise ValueError(f'Unknown heap type: {i}, {i.reftype}') - if isinstance(i, instructions.TableGet): - return a.TABLE_GET(i.tableidx) - if isinstance(i, instructions.TableSet): - return a.TABLE_SET(i.tableidx) - if isinstance(i, instructions.TableInit): - return a.TABLE_INIT(i.tableidx, i.elemidx) - if isinstance(i, instructions.ElemDrop): - return a.ELEM_DROP(i.elemidx) - if isinstance(i, instructions.TableCopy): - return a.TABLE_COPY(i.tableidx1, i.tableidx2) - if isinstance(i, instructions.TableGrow): - return a.TABLE_GROW(i.tableidx) - if isinstance(i, instructions.TableSize): - return a.TABLE_SIZE(i.tableidx) - if isinstance(i, instructions.TableFill): - return a.TABLE_FILL(i.tableidx) - - # Catch all for operations without direct arguments. - return eval('a.' + i.opcode.name) - - -######## -# Data # -######## - - -def val_type(t: ValType): - if t == ValType.i32: - return a.i32 - if t == ValType.i64: - return a.i64 - if t == ValType.f32: - return a.f32 - if t == ValType.f64: - return a.f64 - if t == ValType.externref: - return a.externref - if t == ValType.funcref: - return a.funcref - raise ValueError(f'Unknown value type: {t}') - - -def vec_type(ts: Iterable[ValType]): - _ts = [val_type(x) for x in ts] - return a.vec_type(a.val_types(_ts)) - - -def func_type(params, results): - pvec = vec_type(params) - rvec = vec_type(results) - return a.func_type(pvec, rvec) - - -def limits(l: Limits): - return (l.min, l.max) + """ + Parse a WebAssembly binary module into its corresponding KAST representation. + Args: + wasm_bytes: A binary IO stream containing the WebAssembly module bytes. + filename: Optional filename associated with the module. Currently unused. -def global_type(t: GlobalType): - vt = val_type(t.valtype) - if t.mut is Mutability.const: - return a.global_type(a.MUT_CONST, vt) - return a.global_type(a.MUT_VAR, vt) + Returns: + A KAST term of sort `ModuleDecl` representing the parsed WebAssembly module. + """ + return parse_module(wasm_bytes) diff --git a/pykwasm/src/tests/integration/test_binary_parser.py b/pykwasm/src/tests/integration/test_binary_parser.py index d15873516..e396317ad 100644 --- a/pykwasm/src/tests/integration/test_binary_parser.py +++ b/pykwasm/src/tests/integration/test_binary_parser.py @@ -7,11 +7,9 @@ from typing import TYPE_CHECKING import pytest -from pyk.kast.inner import KApply, KLabel, KSequence, KSort, Subst +from pyk.kast.inner import KSequence, KSort, Subst from pyk.kast.manip import split_config_from -from pyk.kast.prelude.utils import token -from pykwasm.binary.module import parse_module from pykwasm.wasm2kast import wasm2kast if TYPE_CHECKING: @@ -42,48 +40,6 @@ def test_wasm2kast(krun_llvm: KRun, wat_path: Path) -> None: run_module(krun_llvm, module) -@pytest.mark.parametrize('wat_path', BINARY_WAT_FILES, ids=str) -def test_self_binary_parser(krun_llvm: KRun, wat_path: Path) -> None: - # Given - wat2wasm_cmd = ['wat2wasm', str(wat_path), '--output=/dev/stdout'] - proc_res = run(wat2wasm_cmd, check=True, capture_output=True) - wasm_file = BytesIO(proc_res.stdout) - - assert not proc_res.returncode - - # When - module = parse_module(wasm_file) - - # Then - - # Can convert to Kore successfully - krun_llvm.kast_to_kore(module, KSort('ModuleDecl')) - - # Can run (initiate) successfully - run_module(krun_llvm, module) - - -@pytest.mark.parametrize('wat_path', BINARY_WAT_FILES, ids=str) -def test_diff(krun_llvm: KRun, wat_path: Path) -> None: - # Given - wat2wasm_cmd = ['wat2wasm', str(wat_path), '--output=/dev/stdout'] - proc_res = run(wat2wasm_cmd, check=True, capture_output=True) - - assert not proc_res.returncode - - # When - module_wasm2kast = remove_block_ids(wasm2kast(BytesIO(proc_res.stdout))) - - module_self = parse_module(BytesIO(proc_res.stdout)) - - # Then - assert module_wasm2kast == module_self - - run_wasm2kast = run_module(krun_llvm, module_wasm2kast) - run_self = run_module(krun_llvm, module_self) - assert run_wasm2kast == run_self - - def run_module(krun: KRun, parsed_module: KInner) -> Pattern: try: # Create an initial config @@ -102,19 +58,3 @@ def run_module(krun: KRun, parsed_module: KInner) -> Pattern: except Exception as e: raise Exception('Received error while running') from e - - -def remove_block_ids(k: KInner) -> KInner: - match k: - - case KApply(KLabel('aBlock'), (vec_type, instrs, _)): - return KApply('aBlock', (vec_type, remove_block_ids(instrs), token(0))) - - case KApply(KLabel('aIf'), (vec_type, then_instrs, else_instrs, _)): - return KApply('aIf', (vec_type, remove_block_ids(then_instrs), remove_block_ids(else_instrs), token(0))) - - case KApply(KLabel('aLoop'), (vec_type, instrs, _)): - return KApply(KLabel('aLoop'), (vec_type, remove_block_ids(instrs), token(0))) - - case _: - return k.map_inner(remove_block_ids) diff --git a/pykwasm/uv.lock b/pykwasm/uv.lock index 9c9b73aaa..23487c486 100644 --- a/pykwasm/uv.lock +++ b/pykwasm/uv.lock @@ -426,64 +426,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "cytoolz" -version = "0.12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/d8/8df71050b214686591241a1826d2e6934b5c295c5bc57f643e4ed697f1eb/cytoolz-0.12.3.tar.gz", hash = "sha256:4503dc59f4ced53a54643272c61dc305d1dbbfbd7d6bdf296948de9f34c3a282", size = 625899, upload-time = "2024-01-25T01:39:13.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/11/ebc8b85c77cc2247c169af808901a80e5d29429b3c9b0d114d4048ad2a5a/cytoolz-0.12.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bbe58e26c84b163beba0fbeacf6b065feabc8f75c6d3fe305550d33f24a2d346", size = 413462, upload-time = "2024-01-25T01:36:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/19/22/57f20a9f135a08a6fc9848744c324c4facca2b1370a1ae4e8333e274c707/cytoolz-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c51b66ada9bfdb88cf711bf350fcc46f82b83a4683cf2413e633c31a64df6201", size = 396040, upload-time = "2024-01-25T01:36:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/337a18b1182888bf5d59357095a83f537a88e2db32ee0381166a83deddda/cytoolz-0.12.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e70d9c615e5c9dc10d279d1e32e846085fe1fd6f08d623ddd059a92861f4e3dd", size = 1933264, upload-time = "2024-01-25T01:36:10.093Z" }, - { url = "https://files.pythonhosted.org/packages/93/a3/c244bfacd046316cd0a4cffd2e245a8dd9b685168497c3b7c776bdb72c59/cytoolz-0.12.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83f4532707963ae1a5108e51fdfe1278cc8724e3301fee48b9e73e1316de64f", size = 2014114, upload-time = "2024-01-25T01:36:12.083Z" }, - { url = "https://files.pythonhosted.org/packages/a6/40/51bdfe7fd1cad802eabebb7bb742e694227db25c4806eebf4358229e3220/cytoolz-0.12.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d028044524ee2e815f36210a793c414551b689d4f4eda28f8bbb0883ad78bf5f", size = 1999458, upload-time = "2024-01-25T01:36:14.135Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/5973e0ceab96349f9cabb3b20ecc3a717a19e355f68d354dfa538bebb226/cytoolz-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c2875bcd1397d0627a09a4f9172fa513185ad302c63758efc15b8eb33cc2a98", size = 1956106, upload-time = "2024-01-25T01:36:15.485Z" }, - { url = "https://files.pythonhosted.org/packages/d3/01/bb850ad1467f0d1dc5a21a05e3df6ad308a19d095f07583c4e65a85e2ba8/cytoolz-0.12.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:131ff4820e5d64a25d7ad3c3556f2d8aa65c66b3f021b03f8a8e98e4180dd808", size = 1861974, upload-time = "2024-01-25T01:36:17.185Z" }, - { url = "https://files.pythonhosted.org/packages/c9/97/207e544a2c9c3d8735102403982a319c252c337e9cf792c2753c22cff1b6/cytoolz-0.12.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:04afa90d9d9d18394c40d9bed48c51433d08b57c042e0e50c8c0f9799735dcbd", size = 1937926, upload-time = "2024-01-25T01:36:18.968Z" }, - { url = "https://files.pythonhosted.org/packages/76/1e/6cc756537caef16cd491de1550c1f25140528785e28263326bd3243a19ed/cytoolz-0.12.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dc1ca9c610425f9854323669a671fc163300b873731584e258975adf50931164", size = 1859141, upload-time = "2024-01-25T01:36:20.62Z" }, - { url = "https://files.pythonhosted.org/packages/20/43/30819bbc4348cabb44d800ab0d0efb29a9d21db8f75060fd1199d5300953/cytoolz-0.12.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa3f8e01bc423a933f2e1c510cbb0632c6787865b5242857cc955cae220d1bf", size = 2013731, upload-time = "2024-01-25T01:36:22.461Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ab/cacabf2d0dfea006575fea900131eb491370ee1664783f80cb131f577001/cytoolz-0.12.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:f702e295dddef5f8af4a456db93f114539b8dc2a7a9bc4de7c7e41d169aa6ec3", size = 2015075, upload-time = "2024-01-25T01:36:24.144Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/98eeee4f735bff536654625e0c38e50c4b01c13fbd9c744d3284dbccc54d/cytoolz-0.12.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0fbad1fb9bb47e827d00e01992a099b0ba79facf5e5aa453be066033232ac4b5", size = 1953391, upload-time = "2024-01-25T01:36:25.818Z" }, - { url = "https://files.pythonhosted.org/packages/6a/81/80bcbc750e3c8f72d07f1e61c25597fdb23fec3c3d52542f220bff87372c/cytoolz-0.12.3-cp310-cp310-win32.whl", hash = "sha256:8587c3c3dbe78af90c5025288766ac10dc2240c1e76eb0a93a4e244c265ccefd", size = 320881, upload-time = "2024-01-25T01:36:27.405Z" }, - { url = "https://files.pythonhosted.org/packages/45/9e/cce296c6da9404ce210c79700b85d9ba582a6e908ee88ec707efec09bcd9/cytoolz-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e45803d9e75ef90a2f859ef8f7f77614730f4a8ce1b9244375734567299d239", size = 362216, upload-time = "2024-01-25T01:36:28.625Z" }, - { url = "https://files.pythonhosted.org/packages/98/8a/618a70326d4a52998a6bbb11ca019979891a51b85cbbfce8f9762eec5d2c/cytoolz-0.12.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ac4f2fb38bbc67ff1875b7d2f0f162a247f43bd28eb7c9d15e6175a982e558d", size = 416356, upload-time = "2024-01-25T01:36:30.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/17/542f708b9116aa8d8c9c5551500bfa6ab1bddd3edc11457070599a97c197/cytoolz-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf1e1e96dd86829a0539baf514a9c8473a58fbb415f92401a68e8e52a34ecd5", size = 398412, upload-time = "2024-01-25T01:36:32.628Z" }, - { url = "https://files.pythonhosted.org/packages/d6/9c/52a0460f2b59009da63794cfe11ad002d55fc88fced6ccc85f7ae10a3259/cytoolz-0.12.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08a438701c6141dd34eaf92e9e9a1f66e23a22f7840ef8a371eba274477de85d", size = 2090389, upload-time = "2024-01-25T01:36:34.398Z" }, - { url = "https://files.pythonhosted.org/packages/c7/6d/284d4a1f88b1e63b8a1c80162244ca5d4e0b31647c4f56186f3385fd0d0e/cytoolz-0.12.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6b6f11b0d7ed91be53166aeef2a23a799e636625675bb30818f47f41ad31821", size = 2187333, upload-time = "2024-01-25T01:36:36.099Z" }, - { url = "https://files.pythonhosted.org/packages/3e/44/44efcf2acf824aefab5f3fdb97235d360c216c2425db391a8dee687f1774/cytoolz-0.12.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7fde09384d23048a7b4ac889063761e44b89a0b64015393e2d1d21d5c1f534a", size = 2173106, upload-time = "2024-01-25T01:36:37.827Z" }, - { url = "https://files.pythonhosted.org/packages/6f/74/7c2d5af25d51f481e9be771bda2cf5496631cd0fba876d2cc0086caa732d/cytoolz-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d3bfe45173cc8e6c76206be3a916d8bfd2214fb2965563e288088012f1dabfc", size = 2098805, upload-time = "2024-01-25T01:36:40.09Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/9995bf66caee139459ab70d8b5820c37718d5f7e7380481adb85ef522e4d/cytoolz-0.12.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27513a5d5b6624372d63313574381d3217a66e7a2626b056c695179623a5cb1a", size = 1995956, upload-time = "2024-01-25T01:36:41.741Z" }, - { url = "https://files.pythonhosted.org/packages/93/73/eee7a7f455d4691f1915b35d1713ba69c6851f940c6f8fffdd2efcbe4411/cytoolz-0.12.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d294e5e81ff094fe920fd545052ff30838ea49f9e91227a55ecd9f3ca19774a0", size = 2074891, upload-time = "2024-01-25T01:36:43.887Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c7/6f0c60bbb911ee3f33b243a695e2fc944285a1865bbb1dfefee1b7c35d62/cytoolz-0.12.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:727b01a2004ddb513496507a695e19b5c0cfebcdfcc68349d3efd92a1c297bf4", size = 1980228, upload-time = "2024-01-25T01:36:45.42Z" }, - { url = "https://files.pythonhosted.org/packages/58/7c/d7524626b415d410208fd2774ff2824fab4dcbe4a496d53bdba85a1407df/cytoolz-0.12.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:fe1e1779a39dbe83f13886d2b4b02f8c4b10755e3c8d9a89b630395f49f4f406", size = 2147616, upload-time = "2024-01-25T01:36:47.06Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9bdf8615e61958386f5c7108d857312214837ae3eacf3589873499d2f0aa/cytoolz-0.12.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:de74ef266e2679c3bf8b5fc20cee4fc0271ba13ae0d9097b1491c7a9bcadb389", size = 2148775, upload-time = "2024-01-25T01:36:48.813Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/b92ab0e29bf7cfaacfedc85391a0d0365a6ac74363f2532ba307a760db14/cytoolz-0.12.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e04d22049233394e0b08193aca9737200b4a2afa28659d957327aa780ddddf2", size = 2079810, upload-time = "2024-01-25T01:36:50.116Z" }, - { url = "https://files.pythonhosted.org/packages/2f/33/8204d65871fa639d1d3ae788e3afc187f610126f53f2324d4aa37caf0630/cytoolz-0.12.3-cp311-cp311-win32.whl", hash = "sha256:20d36430d8ac809186736fda735ee7d595b6242bdb35f69b598ef809ebfa5605", size = 320212, upload-time = "2024-01-25T01:36:51.73Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e3/7ce2efaf243c9ee75fc730cd018981788b56e7ad3767f24e7a9cd14831ac/cytoolz-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:780c06110f383344d537f48d9010d79fa4f75070d214fc47f389357dd4f010b6", size = 363570, upload-time = "2024-01-25T01:36:53.371Z" }, - { url = "https://files.pythonhosted.org/packages/7f/57/b0feabefdff4707d0b96bf75e2298ecb209ba22c2e3fd15e5230dd68441a/cytoolz-0.12.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:86923d823bd19ce35805953b018d436f6b862edd6a7c8b747a13d52b39ed5716", size = 419825, upload-time = "2024-01-25T01:36:54.522Z" }, - { url = "https://files.pythonhosted.org/packages/54/82/795aa9f91ee81818732b435755559857a76e16b7e435e1de434b47ce72b9/cytoolz-0.12.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3e61acfd029bfb81c2c596249b508dfd2b4f72e31b7b53b62e5fb0507dd7293", size = 401673, upload-time = "2024-01-25T01:36:56.435Z" }, - { url = "https://files.pythonhosted.org/packages/23/f3/298f32b2e374f4f716ca74a9e46b64ee1df9f160de262391f7f9bc53ecca/cytoolz-0.12.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd728f4e6051af6af234651df49319da1d813f47894d4c3c8ab7455e01703a37", size = 2096352, upload-time = "2024-01-25T01:36:58.428Z" }, - { url = "https://files.pythonhosted.org/packages/39/44/49929b33bd0cdccc98b281e4ca11f0836b2963c6fdec8ebb7891807ed908/cytoolz-0.12.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe8c6267caa7ec67bcc37e360f0d8a26bc3bdce510b15b97f2f2e0143bdd3673", size = 2161581, upload-time = "2024-01-25T01:36:59.818Z" }, - { url = "https://files.pythonhosted.org/packages/e9/77/980a8bc123490dd7112e1e068da989932b7612504355ddd25593880c3156/cytoolz-0.12.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99462abd8323c52204a2a0ce62454ce8fa0f4e94b9af397945c12830de73f27e", size = 2178057, upload-time = "2024-01-25T01:37:02.23Z" }, - { url = "https://files.pythonhosted.org/packages/23/d9/2f7a4db7f92eab4b567e56cc2fc147fed1ef35f7968e44cefcbbfb366980/cytoolz-0.12.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da125221b1fa25c690fcd030a54344cecec80074df018d906fc6a99f46c1e3a6", size = 2130881, upload-time = "2024-01-25T01:37:03.703Z" }, - { url = "https://files.pythonhosted.org/packages/85/bf/1147229198a8fc3f2090d01263234d47b5b7f8c52c20090b1cceda0929cd/cytoolz-0.12.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c18e351956f70db9e2d04ff02f28e9a41839250d3f936a4c8a1eabd1c3094d2", size = 1979975, upload-time = "2024-01-25T01:37:06.277Z" }, - { url = "https://files.pythonhosted.org/packages/5b/53/b730e9f428e74971f934bf73f0868099d325ffbf0c080a201bb16b6ab508/cytoolz-0.12.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:921e6d2440ac758c4945c587b1d1d9b781b72737ac0c0ca5d5e02ca1db8bded2", size = 2086861, upload-time = "2024-01-25T01:37:07.869Z" }, - { url = "https://files.pythonhosted.org/packages/24/24/df510850c1c57ad69a23a2700083cfd285faf27d16e61f0e503eec122473/cytoolz-0.12.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1651a9bd591a8326329ce1d6336f3129161a36d7061a4d5ea9e5377e033364cf", size = 1973211, upload-time = "2024-01-25T01:37:10.036Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ad/a6e27924a6c8048b4f1ffbd8b9b0b871642bd94d3e457aa42a629c1f667b/cytoolz-0.12.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8893223b87c2782bd59f9c4bd5c7bf733edd8728b523c93efb91d7468b486528", size = 2133017, upload-time = "2024-01-25T01:37:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/26/a3/f9dfb406232d0a100994a86c123259cd2ab023aa8ae325b0d9926f80091f/cytoolz-0.12.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:e4d2961644153c5ae186db964aa9f6109da81b12df0f1d3494b4e5cf2c332ee2", size = 2156771, upload-time = "2024-01-25T01:37:13.62Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bf/ce2c6f6175a29c830904b23faee557de8c167255dbb2bb84e04d7cf5605a/cytoolz-0.12.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:71b6eb97f6695f7ba8ce69c49b707a351c5f46fd97f5aeb5f6f2fb0d6e72b887", size = 2121104, upload-time = "2024-01-25T01:37:16.053Z" }, - { url = "https://files.pythonhosted.org/packages/bf/97/95a5fb2b5db9b1ea8856741a2a94741a71cb5640112403029beb7b3ef1df/cytoolz-0.12.3-cp312-cp312-win32.whl", hash = "sha256:cee3de65584e915053412cd178729ff510ad5f8f585c21c5890e91028283518f", size = 320537, upload-time = "2024-01-25T01:37:18.026Z" }, - { url = "https://files.pythonhosted.org/packages/0a/11/3e69585342ce4e6b7f2f6139c8aa235dab5a4fe5c76a051fe01696618bd0/cytoolz-0.12.3-cp312-cp312-win_amd64.whl", hash = "sha256:9eef0d23035fa4dcfa21e570961e86c375153a7ee605cdd11a8b088c24f707f6", size = 363238, upload-time = "2024-01-25T01:37:19.343Z" }, - { url = "https://files.pythonhosted.org/packages/00/8c/c58e2f14a88132722988ee0951b01ee6cc4880302384f064902d6e8fb048/cytoolz-0.12.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55f9bd1ae6c2a27eda5abe2a0b65a83029d2385c5a1da7b8ef47af5905d7e905", size = 353101, upload-time = "2024-01-25T01:38:37.808Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/6ef817203004c7055370fa1ab3bb0dfc6b7ebd0b4942099ce86007614001/cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2d271393c378282727f1231d40391ae93b93ddc0997448acc21dd0cb6a1e56d", size = 385225, upload-time = "2024-01-25T01:38:39.382Z" }, - { url = "https://files.pythonhosted.org/packages/a2/25/64e5907b15bd16f0f38e825762540ee3293b513f2eb4b9bb51b7f78d5457/cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee98968d6a66ee83a8ceabf31182189ab5d8598998c8ce69b6d5843daeb2db60", size = 405888, upload-time = "2024-01-25T01:38:40.836Z" }, - { url = "https://files.pythonhosted.org/packages/17/c0/0e19ab05222cf2370d580e5500409c26266f89e853fa18b2b12c530f3957/cytoolz-0.12.3-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01cfb8518828c1189200c02a5010ea404407fb18fd5589e29c126e84bbeadd36", size = 396781, upload-time = "2024-01-25T01:38:42.349Z" }, - { url = "https://files.pythonhosted.org/packages/5e/39/bee67e2b541ca3478a982c68d7d23b718fa7f2947bdfc0eecdba9c4e0882/cytoolz-0.12.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:456395d7aec01db32bf9e6db191d667347c78d8d48e77234521fa1078f60dabb", size = 340712, upload-time = "2024-01-25T01:38:45.075Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -997,38 +939,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] -[[package]] -name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, -] - [[package]] name = "packaging" version = "26.2" @@ -1091,17 +1001,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" }, ] -[[package]] -name = "py-wasm" -version = "0.3.0" -source = { git = "https://github.com/runtimeverification/py-wasm.git?rev=0.3.0#07677b120f40b86d5b7839e2dde42889967dcfce" } -dependencies = [ - { name = "cytoolz", marker = "implementation_name == 'cpython'" }, - { name = "mypy-extensions" }, - { name = "numpy" }, - { name = "toolz", marker = "implementation_name == 'pypy'" }, -] - [[package]] name = "pybind11" version = "2.13.6" @@ -1140,11 +1039,10 @@ wheels = [ [[package]] name = "pykwasm" -version = "0.1.154" +version = "0.1.155" source = { editable = "." } dependencies = [ { name = "kframework" }, - { name = "py-wasm" }, ] [package.dev-dependencies] @@ -1167,10 +1065,7 @@ dev = [ ] [package.metadata] -requires-dist = [ - { name = "kframework", specifier = ">=7.1.323" }, - { name = "py-wasm", git = "https://github.com/runtimeverification/py-wasm.git?rev=0.3.0" }, -] +requires-dist = [{ name = "kframework", specifier = ">=7.1.323" }] [package.metadata.requires-dev] dev = [ @@ -1549,15 +1444,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] -[[package]] -name = "toolz" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/bf/5e12db234df984f6df3c7f12f1428aa680ba4e101f63f4b8b3f9e8d2e617/toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d", size = 66550, upload-time = "2024-01-24T03:28:28.047Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/8a/d82202c9f89eab30f9fc05380daae87d617e2ad11571ab23d7c13a29bb54/toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85", size = 56121, upload-time = "2024-01-24T03:28:25.97Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" From f8040370fe17fcf89b0456cb25046c5f4582c4da Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Wed, 24 Jun 2026 12:59:14 +0300 Subject: [PATCH 12/22] add `#instrWithPos` support --- pykwasm/src/pykwasm/binary/instructions.py | 7 +++++++ pykwasm/src/pykwasm/kwasm_ast.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/instructions.py b/pykwasm/src/pykwasm/binary/instructions.py index 6d95ad272..366f7e2b4 100644 --- a/pykwasm/src/pykwasm/binary/instructions.py +++ b/pykwasm/src/pykwasm/binary/instructions.py @@ -28,6 +28,13 @@ def instr_seq(terminator: int, s: InputStream) -> list[KInner]: def instr(s: InputStream) -> KInner: + offset = s.tell() + i = _instr(s) + length = s.tell() - offset + return wast.INSTR_WITH_POS(i, offset, length) + + +def _instr(s: InputStream) -> KInner: def blocktype(s: InputStream) -> KInner: if peek_byte(s) == 0x40: skip(1, s) diff --git a/pykwasm/src/pykwasm/kwasm_ast.py b/pykwasm/src/pykwasm/kwasm_ast.py index 05b29e9d0..7fadba0b9 100644 --- a/pykwasm/src/pykwasm/kwasm_ast.py +++ b/pykwasm/src/pykwasm/kwasm_ast.py @@ -195,8 +195,8 @@ def global_type(mut: KInner, valtype: KInner) -> KInner: ########## -def INSTR_WITH_POS(instruction: KInner, start: int, end: int) -> KInner: - return KApply('aInstrWithPos', [instruction, KInt(start), KInt(end)]) +def INSTR_WITH_POS(instruction: KInner, offset: int, length: int) -> KInner: + return KApply('aInstrWithPos', [instruction, KInt(offset), KInt(length)]) ######################## From 315c57c97b40592ae71668acc9902f3ded6a0558 Mon Sep 17 00:00:00 2001 From: devops Date: Wed, 24 Jun 2026 10:05:04 +0000 Subject: [PATCH 13/22] Set Version: 0.1.157 --- package/version | 2 +- pykwasm/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package/version b/package/version index 0214e6937..b0b5ddaf6 100644 --- a/package/version +++ b/package/version @@ -1 +1 @@ -0.1.156 +0.1.157 diff --git a/pykwasm/pyproject.toml b/pykwasm/pyproject.toml index d8e43c673..61f745a3c 100644 --- a/pykwasm/pyproject.toml +++ b/pykwasm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "pykwasm" -version = "0.1.156" +version = "0.1.157" description = "" readme = "README.md" requires-python = "~=3.10" From c212cbce447a604cb61b93a0281b0c60a57f0ab9 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Thu, 25 Jun 2026 13:34:55 +0300 Subject: [PATCH 14/22] fix `elem_init` after instrWithPos --- pykwasm/src/pykwasm/binary/module.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/module.py b/pykwasm/src/pykwasm/binary/module.py index 38b991078..4cb93eb47 100644 --- a/pykwasm/src/pykwasm/binary/module.py +++ b/pykwasm/src/pykwasm/binary/module.py @@ -167,13 +167,18 @@ def elemkind(s: InputStream) -> KInner: def elem_init(init: list[list[KInner]]) -> list[int | None]: def expr_to_int(expr: list[KInner]) -> int | None: # 'expr' must be a constant expression consisting of a reference instruction - match expr: - case [KApply(KLabel('aRef.func'), (KToken(sfuncidx, KSort('Int')),))]: + if len(expr) != 1: + raise WasmParseError(f'Expected a constant reference instruction as elem initializer, got {expr}') + instr = expr[0] + if isinstance(instr, KApply) and instr.label == KLabel('aInstrWithPos'): + instr = instr.args[0] + match instr: + case KApply(KLabel('aRef.func'), (KToken(sfuncidx, KSort('Int')),)): return int(sfuncidx) - case [KApply(KLabel('aRef.null'), _)]: + case KApply(KLabel('aRef.null'), _): return None case _: - raise WasmParseError(f'Expected a constant reference instruction as elem initializer, got {expr}') + raise WasmParseError(f'Expected a constant reference instruction as elem initializer, got {instr}') return [expr_to_int(e) for e in init] From b902f46f9e7f170f69c77ecf712921c76c288e64 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Thu, 25 Jun 2026 14:34:03 +0300 Subject: [PATCH 15/22] remove unused `BlockMetaData` --- pykwasm/src/pykwasm/binary/instructions.py | 8 +++--- .../pykwasm/kdist/wasm-semantics/wasm-text.md | 8 +++--- .../src/pykwasm/kdist/wasm-semantics/wasm.md | 25 ++++++++----------- pykwasm/src/pykwasm/kwasm_ast.py | 12 ++++----- tests/proofs/loops-spec.k | 9 +++---- 5 files changed, 27 insertions(+), 35 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/instructions.py b/pykwasm/src/pykwasm/binary/instructions.py index 366f7e2b4..c510e1f84 100644 --- a/pykwasm/src/pykwasm/binary/instructions.py +++ b/pykwasm/src/pykwasm/binary/instructions.py @@ -89,18 +89,16 @@ def memarg(s: InputStream) -> tuple[int, int, int]: case 0x02: bt = blocktype(s) ins = instr_seq(0x0B, s) - return wast.BLOCK(bt, wast.instrs(ins), wast.KInt(0)) # TODO either track block ids or deprecate them + return wast.BLOCK(bt, wast.instrs(ins)) case 0x03: bt = blocktype(s) ins = instr_seq(0x0B, s) - return wast.LOOP(bt, wast.instrs(ins), wast.KInt(0)) # TODO either track block ids or deprecate them + return wast.LOOP(bt, wast.instrs(ins)) case 0x04: bt = blocktype(s) in1 = instr_seq(0x05, s) in2 = instr_seq(0x0B, s) - return wast.IF( - bt, wast.instrs(in1), wast.instrs(in2), wast.KInt(0) - ) # TODO either track block ids or deprecate them + return wast.IF(bt, wast.instrs(in1), wast.instrs(in2)) case 0x0C: l = labelidx(s) return wast.BR(l) diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md index 3c259aad5..8810a1d8d 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm-text.md @@ -1173,9 +1173,9 @@ There are several formats of block instructions, and the text-to-abstract transf At this point, all branching identifiers should have been resolved, so we can remove the id. ```k - rule #t2aInstr( block _OID:OptionalId TDS:TypeDecls IS end _OID') => #block(gatherTypes(result, TDS), #t2aInstrs(IS), .Int) - rule #t2aInstr( loop _OID:OptionalId TDS IS end _OID') => #loop(gatherTypes(result, TDS), #t2aInstrs(IS), .Int) - rule #t2aInstr( if _OID:OptionalId TDS IS else _OID':OptionalId IS' end _OID'') => #if(gatherTypes(result, TDS), #t2aInstrs(IS), #t2aInstrs(IS'), .Int) + rule #t2aInstr( block _OID:OptionalId TDS:TypeDecls IS end _OID') => #block(gatherTypes(result, TDS), #t2aInstrs(IS)) + rule #t2aInstr( loop _OID:OptionalId TDS IS end _OID') => #loop(gatherTypes(result, TDS), #t2aInstrs(IS)) + rule #t2aInstr( if _OID:OptionalId TDS IS else _OID':OptionalId IS' end _OID'') => #if(gatherTypes(result, TDS), #t2aInstrs(IS), #t2aInstrs(IS')) ``` #### KWasm Administrative Instructions @@ -1186,7 +1186,7 @@ They are currently supported in KWasm text files, but may be deprecated. ```k rule #t2aInstr<_C>(trap) => trap - rule #t2aInstr(#block(VT:VecType, IS:Instrs, BLOCKINFO)) => #block(VT, #t2aInstrs(IS), BLOCKINFO) + rule #t2aInstr(#block(VT:VecType, IS:Instrs)) => #block(VT, #t2aInstrs(IS)) rule #t2aInstr<_>(init_local I V) => init_local I V rule #t2aInstr<_>(init_locals VS) => init_locals VS diff --git a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md index 238f775a3..1ebaf8887 100644 --- a/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md +++ b/pykwasm/src/pykwasm/kdist/wasm-semantics/wasm.md @@ -507,12 +507,9 @@ It simply executes the block then records a label with an empty continuation. rule label [ TYPES ] { _ } VALSTACK' => .K ... VALSTACK => #take(lengthValTypes(TYPES), VALSTACK) ++ VALSTACK' - syntax BlockMetaData ::= OptionalInt - // ------------------------------------ - - syntax Instr ::= #block(VecType, Instrs, BlockMetaData) [symbol(aBlock)] - // ------------------------------------------------------------------------ - rule #block(VECTYP, IS, _) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... + syntax Instr ::= #block(VecType, Instrs) [symbol(aBlock)] + // --------------------------------------------------------- + rule #block(VECTYP, IS) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... VALSTACK => .ValStack ``` @@ -554,19 +551,19 @@ Note that, unlike in the WebAssembly specification document, we do not need the Finally, we have the conditional and loop instructions. ```k - syntax Instr ::= #if( VecType, then : Instrs, else : Instrs, blockInfo: BlockMetaData) [symbol(aIf)] - // ---------------------------------------------------------------------------------------------------- - rule #if(VECTYP, IS, _, _) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... + syntax Instr ::= #if( VecType, then : Instrs, else : Instrs) [symbol(aIf)] + // -------------------------------------------------------------------------- + rule #if(VECTYP, IS, _) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... < i32 > VAL : VALSTACK => VALSTACK requires VAL =/=Int 0 - rule #if(VECTYP, _, IS, _) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... + rule #if(VECTYP, _, IS) => sequenceInstrs(IS) ~> label VECTYP { .Instrs } VALSTACK ... < i32 > VAL : VALSTACK => VALSTACK requires VAL ==Int 0 - syntax Instr ::= #loop(VecType, Instrs, BlockMetaData) [symbol(aLoop)] - // ---------------------------------------------------------------------- - rule #loop(VECTYP, IS, BLOCKMETA) => sequenceInstrs(IS) ~> label VECTYP { #loop(VECTYP, IS, BLOCKMETA) } VALSTACK ... + syntax Instr ::= #loop(VecType, Instrs) [symbol(aLoop)] + // -------------------------------------------------------- + rule #loop(VECTYP, IS) => sequenceInstrs(IS) ~> label VECTYP { #loop(VECTYP, IS) } VALSTACK ... VALSTACK => .ValStack ``` @@ -1223,7 +1220,7 @@ The `#take` function will return the parameter stack in the reversed order, then // ------------------------------------- rule ( invoke FADDR ) => init_locals #revs(#take(lengthValTypes(TDOMAIN), VALSTACK)) ++ #zero(TLOCALS) - ~> #block([TRANGE], INSTRS, .Int) + ~> #block([TRANGE], INSTRS) ~> frame MODIDX TRANGE #drop(lengthValTypes(TDOMAIN), VALSTACK) LOCAL ... diff --git a/pykwasm/src/pykwasm/kwasm_ast.py b/pykwasm/src/pykwasm/kwasm_ast.py index 7fadba0b9..0bf35ae1f 100644 --- a/pykwasm/src/pykwasm/kwasm_ast.py +++ b/pykwasm/src/pykwasm/kwasm_ast.py @@ -207,16 +207,16 @@ def INSTR_WITH_POS(instruction: KInner, offset: int, length: int) -> KInner: UNREACHABLE = KApply('aUnreachable', []) -def BLOCK(vec_type: KInner, instrs: KInner, block_info: KInner) -> KInner: - return KApply('aBlock', [vec_type, instrs, block_info]) +def BLOCK(vec_type: KInner, instrs: KInner) -> KInner: + return KApply('aBlock', [vec_type, instrs]) -def IF(vec_type: KInner, then_instrs: KInner, else_instrs: KInner, block_info: KInner) -> KInner: - return KApply('aIf', [vec_type, then_instrs, else_instrs, block_info]) +def IF(vec_type: KInner, then_instrs: KInner, else_instrs: KInner) -> KInner: + return KApply('aIf', [vec_type, then_instrs, else_instrs]) -def LOOP(vec_type: KInner, instrs: KInner, block_info: KInner) -> KInner: - return KApply('aLoop', [vec_type, instrs, block_info]) +def LOOP(vec_type: KInner, instrs: KInner) -> KInner: + return KApply('aLoop', [vec_type, instrs]) RETURN = KApply('aReturn', []) diff --git a/tests/proofs/loops-spec.k b/tests/proofs/loops-spec.k index 46e9dea07..d1efb8d4b 100644 --- a/tests/proofs/loops-spec.k +++ b/tests/proofs/loops-spec.k @@ -18,8 +18,7 @@ module LOOPS-SPEC #local.tee(0) ITYPE.eqz #br_if(1) - #br(0), - _ + #br(0) ) } .ValStack @@ -51,10 +50,8 @@ module LOOPS-SPEC #local.tee(0) ITYPE.eqz #br_if(1) - #br(0), - _ - ), - _ + #br(0) + ) ) => .K ... From 70ae762b1bbb14cc1fafdab2a46016619c9ac94d Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 16:55:33 +0300 Subject: [PATCH 16/22] fix `limits` address-type flag bytes for memory64 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. --- pykwasm/src/pykwasm/binary/types.py | 4 +- pykwasm/src/tests/unit/test_binary_parser.py | 45 +++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/types.py b/pykwasm/src/pykwasm/binary/types.py index b5cecae18..322a94ab5 100644 --- a/pykwasm/src/pykwasm/binary/types.py +++ b/pykwasm/src/pykwasm/binary/types.py @@ -124,10 +124,10 @@ def limits(s: InputStream) -> tuple[KInner, Limits]: n = u64(s) m = u64(s) return wast.i32, (n, m) - case 0x02: + case 0x04: n = u64(s) return wast.i64, (n, None) - case 0x03: + case 0x05: n = u64(s) m = u64(s) return wast.i64, (n, m) diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index 9199d216b..dcb701b5d 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -3,8 +3,10 @@ import pytest +from pykwasm import kwasm_ast as wast from pykwasm.binary import floats, integers -from pykwasm.binary.utils import WasmEOFError +from pykwasm.binary.types import limits +from pykwasm.binary.utils import WasmEOFError, WasmParseError def stream(data: bytes) -> io.BytesIO: @@ -12,6 +14,20 @@ def stream(data: bytes) -> io.BytesIO: return io.BytesIO(data) +def uleb128(value: int) -> bytes: + """Helper: encode an unsigned integer as ULEB128.""" + buf = [] + while True: + b = value & 0x7F + value >>= 7 + if value: + buf.append(b | 0x80) + else: + buf.append(b) + break + return bytes(buf) + + class TestFloats: VALUES = [0.0, 3.14, -1.5, 1.23456789, -9.99, float('inf'), float('-inf')] @@ -86,3 +102,30 @@ def test_uninterpreted_64(self, value: int) -> None: expected = integers.to_uninterpreted(64, value) encoded = self.encode_sleb128(value) assert integers.i64(stream(encoded)) == expected + + +class TestLimits: + def test_i32_no_max(self) -> None: + at, lim = limits(stream(b'\x00' + uleb128(1))) + assert at == wast.i32 + assert lim == (1, None) + + def test_i32_with_max(self) -> None: + at, lim = limits(stream(b'\x01' + uleb128(1) + uleb128(2))) + assert at == wast.i32 + assert lim == (1, 2) + + def test_i64_no_max(self) -> None: + at, lim = limits(stream(b'\x04' + uleb128(1))) + assert at == wast.i64 + assert lim == (1, None) + + def test_i64_with_max(self) -> None: + at, lim = limits(stream(b'\x05' + uleb128(1) + uleb128(2))) + assert at == wast.i64 + assert lim == (1, 2) + + @pytest.mark.parametrize('flag', [0x02, 0x03]) + def test_shared_memory_flags_rejected(self, flag: int) -> None: + with pytest.raises(WasmParseError): + limits(stream(bytes([flag]) + uleb128(1))) From cab1047e4fdacf97599ed6ab299d49d9dc35e46c Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 16:58:57 +0300 Subject: [PATCH 17/22] fix custom sections not being consumed 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. --- pykwasm/src/pykwasm/binary/module.py | 11 ++-- pykwasm/src/tests/unit/test_binary_parser.py | 60 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/module.py b/pykwasm/src/pykwasm/binary/module.py index 4cb93eb47..005e5e19a 100644 --- a/pykwasm/src/pykwasm/binary/module.py +++ b/pykwasm/src/pykwasm/binary/module.py @@ -41,23 +41,24 @@ def parse_version(s: InputStream) -> None: raise WasmParseError('Could not read version') from e -def parse_custom_section(s: InputStream) -> bytes: +def parse_custom_section(s: InputStream) -> bytes | None: try: n = peek_byte(s) except WasmEOFError: - return b'' + return None if n != 0: - return b'' + return None skip(1, s) - return read_bytes(n, s) + size = u32(s) + return read_bytes(size, s) def parse_custom_sections(s: InputStream) -> list[bytes]: sects = [] while True: sect = parse_custom_section(s) - if not sect: + if sect is None: break sects.append(sect) return sects diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index dcb701b5d..961e22d3b 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -1,13 +1,21 @@ +from __future__ import annotations + import io import struct +from typing import TYPE_CHECKING import pytest +from pyk.kast.inner import KApply from pykwasm import kwasm_ast as wast from pykwasm.binary import floats, integers +from pykwasm.binary.module import MAGIC, VERSION, parse_module from pykwasm.binary.types import limits from pykwasm.binary.utils import WasmEOFError, WasmParseError +if TYPE_CHECKING: + from pyk.kast.inner import KInner + def stream(data: bytes) -> io.BytesIO: """Helper: wrap bytes in a seekable stream.""" @@ -28,6 +36,27 @@ def uleb128(value: int) -> bytes: return bytes(buf) +def section(sec_id: int, content: bytes) -> bytes: + """Helper: wrap section content with its id and ULEB128-encoded size.""" + return bytes([sec_id]) + uleb128(len(content)) + content + + +def custom_section(name: str, payload: bytes = b'') -> bytes: + """Helper: build a custom section (id 0) with the given name and raw payload.""" + name_bytes = name.encode('utf-8') + content = uleb128(len(name_bytes)) + name_bytes + payload + return section(0, content) + + +def defns_len(k: KInner) -> int: + """Count the number of items in a `Defns` cons-list KAST node.""" + n = 0 + while isinstance(k, KApply) and k.args: + n += 1 + k = k.args[1] + return n + + class TestFloats: VALUES = [0.0, 3.14, -1.5, 1.23456789, -9.99, float('inf'), float('-inf')] @@ -129,3 +158,34 @@ def test_i64_with_max(self) -> None: def test_shared_memory_flags_rejected(self, flag: int) -> None: with pytest.raises(WasmParseError): limits(stream(bytes([flag]) + uleb128(1))) + + +class TestCustomSections: + def _wrap(self, *sections: bytes) -> bytes: + return MAGIC + VERSION + b''.join(sections) + + def test_custom_sections_do_not_break_stream_alignment(self) -> None: + type_section = section(1, uleb128(1) + bytes([0x60]) + uleb128(1) + bytes([0x7F]) + uleb128(1) + bytes([0x7F])) + func_section = section(3, uleb128(1) + uleb128(0)) + export_section = section(7, uleb128(1) + (uleb128(len(b'f')) + b'f' + bytes([0x00]) + uleb128(0))) + code_section = section(10, uleb128(1) + (uleb128(4) + bytes([0x00, 0x20, 0x00, 0x0B]))) + + data = self._wrap( + custom_section('a', b'hello'), + type_section, + custom_section('b'), + func_section, + custom_section('c', b'world'), + export_section, + custom_section('d'), + code_section, + custom_section('e'), + ) + + module = parse_module(stream(data)) + + assert isinstance(module, KApply) + types, funcs, _, _, _, _, _, _, _, exports, _ = module.args + assert defns_len(types) == 1 + assert defns_len(funcs) == 1 + assert defns_len(exports) == 1 From f0d9131e910e9b295d240679f02ad53be3a87168 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 21:19:07 +0300 Subject: [PATCH 18/22] add memory.copy and memory.fill opcode parsing 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. --- pykwasm/src/pykwasm/binary/instructions.py | 9 ++++++ .../src/tests/integration/binary/instrs.wat | 2 ++ pykwasm/src/tests/unit/test_binary_parser.py | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/pykwasm/src/pykwasm/binary/instructions.py b/pykwasm/src/pykwasm/binary/instructions.py index c510e1f84..efe72f4fd 100644 --- a/pykwasm/src/pykwasm/binary/instructions.py +++ b/pykwasm/src/pykwasm/binary/instructions.py @@ -243,6 +243,15 @@ def memarg(s: InputStream) -> tuple[int, int, int]: # TODO use memory index _x = memidx(s) # noqa 841 return wast.MEMORY_GROW + case 0xFC if additional_value == 10: + # TODO use memory index + _x1 = memidx(s) # noqa 841 + _x2 = memidx(s) # noqa 841 + return wast.MEMORY_COPY + case 0xFC if additional_value == 11: + # TODO use memory index + _x = memidx(s) # noqa 841 + return wast.MEMORY_FILL # Reference instructions case 0xD0: diff --git a/pykwasm/src/tests/integration/binary/instrs.wat b/pykwasm/src/tests/integration/binary/instrs.wat index b719c8318..caf22efef 100644 --- a/pykwasm/src/tests/integration/binary/instrs.wat +++ b/pykwasm/src/tests/integration/binary/instrs.wat @@ -155,6 +155,8 @@ i64.store32 offset=10 drop memory.size memory.grow + memory.fill + memory.copy ;; -- unreachable diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index 961e22d3b..d27ea3e35 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -9,6 +9,7 @@ from pykwasm import kwasm_ast as wast from pykwasm.binary import floats, integers +from pykwasm.binary.instructions import instr from pykwasm.binary.module import MAGIC, VERSION, parse_module from pykwasm.binary.types import limits from pykwasm.binary.utils import WasmEOFError, WasmParseError @@ -189,3 +190,34 @@ def test_custom_sections_do_not_break_stream_alignment(self) -> None: assert defns_len(types) == 1 assert defns_len(funcs) == 1 assert defns_len(exports) == 1 + + +def unwrap_instr(k: KApply) -> KApply: + """Helper: strip the `aInstrWithPos` position wrapper added by `instr()`.""" + assert k.label.name == 'aInstrWithPos' + inner = k.args[0] + assert isinstance(inner, KApply) + return inner + + +class TestInstructions: + # non-zero, multi-byte memidx values plus a trailing sentinel byte prove the parser + # consumes exactly the memidx operands, not just a fixed number of bytes that + # happens to work for the single-byte/zero case + @pytest.mark.parametrize('dst_memidx, src_memidx', [(0, 0), (300, 65536)], ids=['zero', 'multi_byte']) + def test_memory_copy(self, dst_memidx: int, src_memidx: int) -> None: + # 0xFC 10 dst_memidx src_memidx + s = stream(bytes([0xFC]) + uleb128(10) + uleb128(dst_memidx) + uleb128(src_memidx) + b'\x01') + i = instr(s) + assert isinstance(i, KApply) + assert unwrap_instr(i) == wast.MEMORY_COPY + assert s.read(1) == b'\x01' + + @pytest.mark.parametrize('memidx', [0, 300], ids=['zero', 'multi_byte']) + def test_memory_fill(self, memidx: int) -> None: + # 0xFC 11 memidx + s = stream(bytes([0xFC]) + uleb128(11) + uleb128(memidx) + b'\x01') + i = instr(s) + assert isinstance(i, KApply) + assert unwrap_instr(i) == wast.MEMORY_FILL + assert s.read(1) == b'\x01' From afc6f6dd995ab5832592f0903319108e3aa54bbf Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 23:38:46 +0300 Subject: [PATCH 19/22] fix binary parser to reject trailing data after module end 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. --- pykwasm/src/pykwasm/binary/module.py | 7 +++++++ pykwasm/src/tests/unit/test_binary_parser.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/pykwasm/src/pykwasm/binary/module.py b/pykwasm/src/pykwasm/binary/module.py index 005e5e19a..86eb4a27c 100644 --- a/pykwasm/src/pykwasm/binary/module.py +++ b/pykwasm/src/pykwasm/binary/module.py @@ -328,6 +328,13 @@ def parse_module(s: InputStream) -> KInner: customs() + try: + peek_byte(s) + except WasmEOFError: + pass + else: + raise WasmParseError('Unexpected data after end of module') + functions = [ wast.func( wast.KInt(typ_idx), diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index d27ea3e35..05602ddb7 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -192,6 +192,22 @@ def test_custom_sections_do_not_break_stream_alignment(self) -> None: assert defns_len(exports) == 1 +class TestTrailingData: + def test_empty_module_parses(self) -> None: + parse_module(stream(MAGIC + VERSION)) + + def test_trailing_garbage_rejected(self) -> None: + with pytest.raises(WasmParseError): + parse_module(stream(MAGIC + VERSION + b'\xff\xff\xff')) + + def test_trailing_garbage_after_sections_rejected(self) -> None: + # trailing byte doesn't match any expected section id, and isn't a custom section (id 0) either, + # so it must be caught by the final EOF check rather than any individual `section()` call. + type_section = section(1, uleb128(1) + bytes([0x60]) + uleb128(0) + uleb128(0)) + with pytest.raises(WasmParseError): + parse_module(stream(MAGIC + VERSION + type_section + b'\xff')) + + def unwrap_instr(k: KApply) -> KApply: """Helper: strip the `aInstrWithPos` position wrapper added by `instr()`.""" assert k.label.name == 'aInstrWithPos' From f9a42722f3ab1b81fb6f16612bfa42932c6081ee Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 23:42:49 +0300 Subject: [PATCH 20/22] remove unused iterate combinator from binary parser 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. --- pykwasm/src/pykwasm/binary/combinators.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/combinators.py b/pykwasm/src/pykwasm/binary/combinators.py index 0adc46731..52c56d53b 100644 --- a/pykwasm/src/pykwasm/binary/combinators.py +++ b/pykwasm/src/pykwasm/binary/combinators.py @@ -9,16 +9,6 @@ from .utils import A, InputStream, Parser -def iterate(f: Parser[A], s: InputStream) -> list[A]: - results = [] - while True: - try: - results.append(f(s)) - except (WasmParseError, IndexError, ValueError): - break - return results - - def sized(p: Parser[A], s: InputStream) -> A: size = u32(s) start_pos = s.tell() From 979e0cf9b0dc4998ac79b640615ac6f60d8642ab Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Fri, 10 Jul 2026 23:49:40 +0300 Subject: [PATCH 21/22] raise WasmParseError instead of ValueError on func/code length mismatch 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. --- pykwasm/src/pykwasm/binary/module.py | 5 +++++ pykwasm/src/tests/unit/test_binary_parser.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/pykwasm/src/pykwasm/binary/module.py b/pykwasm/src/pykwasm/binary/module.py index 86eb4a27c..353e7f7ec 100644 --- a/pykwasm/src/pykwasm/binary/module.py +++ b/pykwasm/src/pykwasm/binary/module.py @@ -335,6 +335,11 @@ def parse_module(s: InputStream) -> KInner: else: raise WasmParseError('Unexpected data after end of module') + if len(function_section) != len(code_section): + raise WasmParseError( + f'Function and code section length mismatch: {len(function_section)} != {len(code_section)}' + ) + functions = [ wast.func( wast.KInt(typ_idx), diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index 05602ddb7..cf330cd81 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -208,6 +208,20 @@ def test_trailing_garbage_after_sections_rejected(self) -> None: parse_module(stream(MAGIC + VERSION + type_section + b'\xff')) +class TestFuncCodeLengthMismatch: + def test_more_funcs_than_code_entries_rejected(self) -> None: + func_section = section(3, uleb128(1) + uleb128(0)) + code_section = section(10, uleb128(0)) + with pytest.raises(WasmParseError): + parse_module(stream(MAGIC + VERSION + func_section + code_section)) + + def test_more_code_entries_than_funcs_rejected(self) -> None: + func_section = section(3, uleb128(0)) + code_section = section(10, uleb128(1) + (uleb128(2) + bytes([0x00, 0x0B]))) + with pytest.raises(WasmParseError): + parse_module(stream(MAGIC + VERSION + func_section + code_section)) + + def unwrap_instr(k: KApply) -> KApply: """Helper: strip the `aInstrWithPos` position wrapper added by `instr()`.""" assert k.label.name == 'aInstrWithPos' From 57278aa488b8828bb80b783621d5435357ef5299 Mon Sep 17 00:00:00 2001 From: Burak Bilge Yalcinkaya Date: Sat, 11 Jul 2026 00:43:19 +0300 Subject: [PATCH 22/22] fix peek_bytes to restore stream position on partial-EOF read reset(pos, s) only ran on the success path, so a short read at EOF (fewer than n bytes remaining but more than zero) left the stream advanced past the bytes it consumed while trying, violating peek's no-side-effects contract. --- pykwasm/src/pykwasm/binary/utils.py | 2 +- pykwasm/src/tests/unit/test_binary_parser.py | 23 +++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pykwasm/src/pykwasm/binary/utils.py b/pykwasm/src/pykwasm/binary/utils.py index 232d2dcf0..fe39595d6 100644 --- a/pykwasm/src/pykwasm/binary/utils.py +++ b/pykwasm/src/pykwasm/binary/utils.py @@ -27,9 +27,9 @@ def read_byte(s: InputStream) -> int: def peek_bytes(n: int, s: InputStream) -> bytes: pos = s.tell() b = s.read(n) + reset(pos, s) if len(b) != n: raise WasmEOFError(f'Unexpected EOF. Expected {n} bytes, got {len(b)}') - reset(pos, s) return b diff --git a/pykwasm/src/tests/unit/test_binary_parser.py b/pykwasm/src/tests/unit/test_binary_parser.py index cf330cd81..1093cf896 100644 --- a/pykwasm/src/tests/unit/test_binary_parser.py +++ b/pykwasm/src/tests/unit/test_binary_parser.py @@ -12,7 +12,7 @@ from pykwasm.binary.instructions import instr from pykwasm.binary.module import MAGIC, VERSION, parse_module from pykwasm.binary.types import limits -from pykwasm.binary.utils import WasmEOFError, WasmParseError +from pykwasm.binary.utils import WasmEOFError, WasmParseError, peek_bytes if TYPE_CHECKING: from pyk.kast.inner import KInner @@ -161,6 +161,27 @@ def test_shared_memory_flags_rejected(self, flag: int) -> None: limits(stream(bytes([flag]) + uleb128(1))) +class TestPeekBytes: + def test_successful_peek_does_not_advance_stream(self) -> None: + s = stream(b'\x01\x02\x03') + assert peek_bytes(2, s) == b'\x01\x02' + assert s.tell() == 0 + + def test_partial_eof_restores_stream_position(self) -> None: + # only 1 byte available, but 2 are requested: the short read must not leave the + # stream advanced past the byte it couldn't help but consume while trying. + s = stream(b'\x40') + with pytest.raises(WasmEOFError): + peek_bytes(2, s) + assert s.tell() == 0 + + def test_true_eof_restores_stream_position(self) -> None: + s = stream(b'') + with pytest.raises(WasmEOFError): + peek_bytes(1, s) + assert s.tell() == 0 + + class TestCustomSections: def _wrap(self, *sections: bytes) -> bytes: return MAGIC + VERSION + b''.join(sections)