phasm/tests/integration/helpers.py
Johan B.W. de Vries c8009403c4 Separates out TypeVariable and constraints
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.
2025-04-27 15:28:08 +02:00

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)
}