They look a lot like placeholders, but they exist before the typing system takes place. And there's a (much smaller) context to deal with. For now, removes Placeholders in user function definitions as they were not implemented. Adds signature to function to try to get them closer to type class methods. Already seeing some benefit in the constraint generator. Stricter zipping for safety.
449 lines
15 KiB
Python
449 lines
15 KiB
Python
import struct
|
|
import sys
|
|
from typing import Any, Generator, Iterable, List, TextIO, Union
|
|
|
|
from phasm import compiler, prelude
|
|
from phasm.codestyle import phasm_render
|
|
from phasm.runtime import calculate_alloc_size
|
|
from phasm.type3 import placeholders as type3placeholders
|
|
from phasm.type3 import types as type3types
|
|
|
|
from . import runners
|
|
|
|
DASHES = '-' * 16
|
|
|
|
class SuiteResult:
|
|
def __init__(self) -> None:
|
|
self.returned_value = None
|
|
|
|
RUNNER_CLASS_MAP = {
|
|
'wasmtime': runners.RunnerWasmtime,
|
|
}
|
|
|
|
class Suite:
|
|
"""
|
|
WebAssembly test suite
|
|
"""
|
|
def __init__(self, code_py: str) -> None:
|
|
self.code_py = code_py
|
|
|
|
def run_code(self, *args: Any, runtime: str = 'wasmtime', func_name: str = 'testEntry', imports: runners.Imports = None, do_format_check: bool = True) -> Any:
|
|
"""
|
|
Compiles the given python code into wasm and
|
|
then runs it
|
|
|
|
Returned is an object with the results set
|
|
"""
|
|
class_ = RUNNER_CLASS_MAP[runtime]
|
|
|
|
runner = class_(self.code_py)
|
|
|
|
write_header(sys.stderr, 'Phasm')
|
|
runner.dump_phasm_code(sys.stderr)
|
|
|
|
runner.parse()
|
|
runner.compile_ast()
|
|
runner.compile_wat()
|
|
|
|
write_header(sys.stderr, 'Assembly')
|
|
runner.dump_wasm_wat(sys.stderr)
|
|
|
|
runner.interpreter_setup()
|
|
runner.interpreter_load(imports)
|
|
|
|
# Check if code formatting works
|
|
if do_format_check:
|
|
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
|
|
|
|
func_args = [x.type3 for x in runner.phasm_ast.functions[func_name].posonlyargs]
|
|
if len(func_args) != len(args):
|
|
raise RuntimeError(f'Invalid number of args for {func_name}')
|
|
|
|
wasm_args: List[Union[float, int]] = []
|
|
if args:
|
|
write_header(sys.stderr, 'Memory (pre alloc)')
|
|
runner.interpreter_dump_memory(sys.stderr)
|
|
|
|
for arg, arg_typ in zip(args, func_args, strict=True):
|
|
assert not isinstance(arg_typ, type3placeholders.PlaceholderForType), \
|
|
'Cannot call polymorphic function from outside'
|
|
|
|
if arg_typ in (prelude.u8, prelude.u32, prelude.u64, ):
|
|
assert isinstance(arg, int)
|
|
wasm_args.append(arg)
|
|
continue
|
|
|
|
if arg_typ in (prelude.i8, prelude.i32, prelude.i64, ):
|
|
assert isinstance(arg, int)
|
|
wasm_args.append(arg)
|
|
continue
|
|
|
|
if arg_typ in (prelude.f32, prelude.f64, ):
|
|
assert isinstance(arg, float)
|
|
wasm_args.append(arg)
|
|
continue
|
|
|
|
if arg_typ is prelude.bytes_:
|
|
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
|
|
wasm_args.append(adr)
|
|
continue
|
|
|
|
assert isinstance(arg_typ, type3types.Type3)
|
|
sa_args = prelude.static_array.did_construct(arg_typ)
|
|
if sa_args is not None:
|
|
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
|
|
wasm_args.append(adr)
|
|
continue
|
|
|
|
tp_args = prelude.tuple_.did_construct(arg_typ)
|
|
if tp_args is not None:
|
|
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
|
|
wasm_args.append(adr)
|
|
continue
|
|
|
|
st_args = prelude.struct.did_construct(arg_typ)
|
|
if st_args is not None:
|
|
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
|
|
wasm_args.append(adr)
|
|
continue
|
|
|
|
raise NotImplementedError(arg_typ, arg)
|
|
|
|
write_header(sys.stderr, 'Memory (pre run)')
|
|
runner.interpreter_dump_memory(sys.stderr)
|
|
|
|
result = SuiteResult()
|
|
result.returned_value = runner.call(func_name, *wasm_args)
|
|
|
|
result.returned_value = _load_memory_stored_returned_value(
|
|
runner,
|
|
func_name,
|
|
result.returned_value,
|
|
)
|
|
|
|
write_header(sys.stderr, 'Memory (post run)')
|
|
runner.interpreter_dump_memory(sys.stderr)
|
|
|
|
return result
|
|
|
|
def write_header(textio: TextIO, msg: str) -> None:
|
|
textio.write(f'{DASHES} {msg.ljust(16)} {DASHES}\n')
|
|
|
|
WRITE_LOOKUP_MAP = {
|
|
'u8': compiler.module_data_u8,
|
|
'u32': compiler.module_data_u32,
|
|
'u64': compiler.module_data_u64,
|
|
'i8': compiler.module_data_i8,
|
|
'i32': compiler.module_data_i32,
|
|
'i64': compiler.module_data_i64,
|
|
'f32': compiler.module_data_f32,
|
|
'f64': compiler.module_data_f64,
|
|
}
|
|
|
|
def _write_memory_stored_value(
|
|
runner: runners.RunnerBase,
|
|
adr: int,
|
|
val_typ: type3types.Type3,
|
|
val: Any,
|
|
) -> int:
|
|
if val_typ is prelude.bytes_:
|
|
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
|
|
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
|
|
return 4
|
|
|
|
st_args = prelude.struct.did_construct(val_typ)
|
|
if st_args is not None:
|
|
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
|
|
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
|
|
return 4
|
|
|
|
sa_args = prelude.static_array.did_construct(val_typ)
|
|
if sa_args is not None:
|
|
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
|
|
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
|
|
return 4
|
|
|
|
tp_args = prelude.tuple_.did_construct(val_typ)
|
|
if tp_args is not None:
|
|
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
|
|
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
|
|
return 4
|
|
|
|
to_write = WRITE_LOOKUP_MAP[val_typ.name](val)
|
|
runner.interpreter_write_memory(adr, to_write)
|
|
return len(to_write)
|
|
|
|
def _allocate_memory_stored_value(
|
|
runner: runners.RunnerBase,
|
|
val_typ: type3types.Type3,
|
|
val: Any
|
|
) -> int:
|
|
if val_typ is prelude.bytes_:
|
|
assert isinstance(val, bytes)
|
|
|
|
adr = runner.call('stdlib.types.__alloc_bytes__', len(val))
|
|
assert isinstance(adr, int)
|
|
|
|
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
|
|
runner.interpreter_write_memory(adr + 4, val)
|
|
return adr
|
|
|
|
sa_args = prelude.static_array.did_construct(val_typ)
|
|
if sa_args is not None:
|
|
assert isinstance(val, tuple)
|
|
|
|
sa_type, sa_len = sa_args
|
|
|
|
alloc_size = calculate_alloc_size(val_typ)
|
|
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
|
|
assert isinstance(adr, int)
|
|
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
|
|
|
|
tuple_len = sa_len.value
|
|
assert tuple_len == len(val)
|
|
|
|
offset = adr
|
|
for val_el_val in val:
|
|
offset += _write_memory_stored_value(runner, offset, sa_type, val_el_val)
|
|
return adr
|
|
|
|
val_el_typ: type3types.Type3
|
|
|
|
tp_args = prelude.tuple_.did_construct(val_typ)
|
|
if tp_args is not None:
|
|
assert isinstance(val, tuple)
|
|
|
|
alloc_size = calculate_alloc_size(val_typ)
|
|
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
|
|
assert isinstance(adr, int)
|
|
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
|
|
|
|
assert len(val) == len(tp_args)
|
|
|
|
offset = adr
|
|
for val_el_val, val_el_typ in zip(val, tp_args, strict=True):
|
|
assert not isinstance(val_el_typ, type3placeholders.PlaceholderForType)
|
|
|
|
offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val)
|
|
return adr
|
|
|
|
st_args = prelude.struct.did_construct(val_typ)
|
|
if st_args is not None:
|
|
assert isinstance(val, dict)
|
|
|
|
alloc_size = calculate_alloc_size(val_typ)
|
|
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
|
|
assert isinstance(adr, int)
|
|
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
|
|
|
|
assert list(val.keys()) == list(st_args)
|
|
|
|
offset = adr
|
|
for val_el_name, val_el_typ in st_args.items():
|
|
assert not isinstance(val_el_typ, type3placeholders.PlaceholderForType)
|
|
|
|
val_el_val = val[val_el_name]
|
|
offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val)
|
|
return adr
|
|
|
|
raise NotImplementedError(val_typ, val)
|
|
|
|
def _load_memory_stored_returned_value(
|
|
runner: runners.RunnerBase,
|
|
func_name: str,
|
|
wasm_value: Any,
|
|
) -> Any:
|
|
ret_type3 = runner.phasm_ast.functions[func_name].returns_type3
|
|
|
|
if ret_type3 is prelude.none:
|
|
return None
|
|
|
|
if ret_type3 is prelude.bool_:
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
return 0 != wasm_value
|
|
|
|
if ret_type3 in (prelude.i8, prelude.i32, prelude.i64):
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
return wasm_value
|
|
|
|
if ret_type3 in (prelude.u8, prelude.u32, prelude.u64):
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
|
|
if wasm_value < 0:
|
|
# WASM does not support unsigned values through its interface
|
|
# Cast and then reinterpret
|
|
|
|
letter = {
|
|
'u32': 'i',
|
|
'u64': 'q',
|
|
}[ret_type3.name]
|
|
|
|
data = struct.pack(f'<{letter}', wasm_value)
|
|
wasm_value, = struct.unpack(f'<{letter.upper()}', data)
|
|
|
|
return wasm_value
|
|
|
|
if ret_type3 in (prelude.f32, prelude.f64, ):
|
|
assert isinstance(wasm_value, float), wasm_value
|
|
return wasm_value
|
|
|
|
if ret_type3 is prelude.bytes_:
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
|
|
return _load_bytes_from_address(runner, ret_type3, wasm_value)
|
|
|
|
assert isinstance(ret_type3, type3types.Type3) # Type hint
|
|
|
|
sa_args = prelude.static_array.did_construct(ret_type3)
|
|
if sa_args is not None:
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
|
|
return _load_static_array_from_address(runner, sa_args[0], sa_args[1], wasm_value)
|
|
|
|
tp_args = prelude.tuple_.did_construct(ret_type3)
|
|
if tp_args is not None:
|
|
assert isinstance(wasm_value, int), wasm_value
|
|
|
|
return _load_tuple_from_address(runner, tp_args, wasm_value)
|
|
|
|
st_args = prelude.struct.did_construct(ret_type3)
|
|
if st_args is not None:
|
|
return _load_struct_from_address(runner, st_args, wasm_value)
|
|
|
|
raise NotImplementedError(ret_type3, wasm_value)
|
|
|
|
def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> Any:
|
|
if typ is prelude.u8:
|
|
# See compiler.py, LOAD_STORE_TYPE_MAP and module_data_u8
|
|
assert len(inp) == 4
|
|
return struct.unpack('<I', inp)[0]
|
|
|
|
if typ is prelude.u32:
|
|
assert len(inp) == 4
|
|
return struct.unpack('<I', inp)[0]
|
|
|
|
if typ is prelude.u64:
|
|
assert len(inp) == 8
|
|
return struct.unpack('<Q', inp)[0]
|
|
|
|
if typ is prelude.i8:
|
|
# See compiler.py, LOAD_STORE_TYPE_MAP and module_data_i8
|
|
assert len(inp) == 4
|
|
return struct.unpack('<i', inp)[0]
|
|
|
|
if typ is prelude.i32:
|
|
assert len(inp) == 4
|
|
return struct.unpack('<i', inp)[0]
|
|
|
|
if typ is prelude.i64:
|
|
assert len(inp) == 8
|
|
return struct.unpack('<q', inp)[0]
|
|
|
|
if typ is prelude.f32:
|
|
assert len(inp) == 4
|
|
return struct.unpack('<f', inp)[0]
|
|
|
|
if typ is prelude.f64:
|
|
assert len(inp) == 8
|
|
return struct.unpack('<d', inp)[0]
|
|
|
|
if typ is prelude.bytes_:
|
|
# Note: For bytes, inp should contain a 4 byte pointer
|
|
assert len(inp) == 4
|
|
adr = struct.unpack('<I', inp)[0]
|
|
|
|
return _load_bytes_from_address(runner, typ, adr)
|
|
|
|
if prelude.InternalPassAsPointer in typ.classes:
|
|
# Note: For applied types, inp should contain a 4 byte pointer
|
|
assert len(inp) == 4
|
|
adr = struct.unpack('<I', inp)[0]
|
|
|
|
assert isinstance(typ, type3types.Type3)
|
|
|
|
sa_args = prelude.static_array.did_construct(typ)
|
|
if sa_args is not None:
|
|
sa_type, sa_len = sa_args
|
|
return _load_static_array_from_address(runner, sa_type, sa_len, adr)
|
|
|
|
tp_args = prelude.tuple_.did_construct(typ)
|
|
if tp_args is not None:
|
|
return _load_tuple_from_address(runner, tp_args, adr)
|
|
|
|
st_args = prelude.struct.did_construct(typ)
|
|
if st_args is not None:
|
|
# Note: For structs, inp should contain a 4 byte pointer
|
|
assert len(inp) == 4
|
|
adr = struct.unpack('<I', inp)[0]
|
|
|
|
return _load_struct_from_address(runner, st_args, adr)
|
|
|
|
raise NotImplementedError(typ, inp)
|
|
|
|
def _load_bytes_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> bytes:
|
|
sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n')
|
|
read_bytes = runner.interpreter_read_memory(adr, 4)
|
|
bytes_len, = struct.unpack('<I', read_bytes)
|
|
|
|
adr += 4
|
|
return runner.interpreter_read_memory(adr, bytes_len)
|
|
|
|
def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator[bytes, None, None]:
|
|
offset = 0
|
|
for size in split_sizes:
|
|
yield all_bytes[offset:offset + size]
|
|
offset += size
|
|
|
|
def _load_static_array_from_address(runner: runners.RunnerBase, sub_typ: type3types.Type3, len_typ: type3types.IntType3, adr: int) -> Any:
|
|
sys.stderr.write(f'Reading 0x{adr:08x} {sub_typ:s} {len_typ:s}\n')
|
|
|
|
assert not isinstance(sub_typ, type3placeholders.PlaceholderForType)
|
|
assert isinstance(len_typ, type3types.IntType3)
|
|
|
|
sa_len = len_typ.value
|
|
|
|
arg_size_1 = calculate_alloc_size(sub_typ, is_member=True)
|
|
arg_sizes = [arg_size_1 for _ in range(sa_len)] # _split_read_bytes requires one arg per value
|
|
|
|
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
|
|
|
|
return tuple(
|
|
_unpack(runner, sub_typ, arg_bytes)
|
|
for arg_bytes in _split_read_bytes(read_bytes, arg_sizes)
|
|
)
|
|
|
|
def _load_tuple_from_address(runner: runners.RunnerBase, typ_args: tuple[type3types.Type3, ...], adr: int) -> Any:
|
|
sys.stderr.write(f'Reading 0x{adr:08x} tuple {len(typ_args)}\n')
|
|
|
|
arg_sizes = [
|
|
calculate_alloc_size(x, is_member=True)
|
|
for x in typ_args
|
|
]
|
|
|
|
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
|
|
|
|
return tuple(
|
|
_unpack(runner, arg_typ, arg_bytes)
|
|
for arg_typ, arg_bytes in zip(typ_args, _split_read_bytes(read_bytes, arg_sizes), strict=True)
|
|
)
|
|
|
|
def _load_struct_from_address(runner: runners.RunnerBase, st_args: dict[str, type3types.Type3], adr: int) -> Any:
|
|
sys.stderr.write(f'Reading 0x{adr:08x} struct {list(st_args)}\n')
|
|
|
|
name_list = list(st_args)
|
|
|
|
typ_list = list(st_args.values())
|
|
assert len(typ_list) == len(st_args)
|
|
|
|
arg_sizes = [
|
|
calculate_alloc_size(x, is_member=True)
|
|
for x in typ_list
|
|
]
|
|
|
|
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
|
|
|
|
return {
|
|
arg_name: _unpack(runner, arg_typ, arg_bytes)
|
|
for arg_name, arg_typ, arg_bytes in zip(name_list, typ_list, _split_read_bytes(read_bytes, arg_sizes), strict=True)
|
|
}
|