Implemented round trip input / output

This commit is contained in:
Johan B.W. de Vries 2023-11-13 14:32:17 +01:00
parent f4f068137a
commit 0131b84146
5 changed files with 168 additions and 49 deletions

View File

@ -1,8 +1,9 @@
from typing import Any, Generator, Iterable, TextIO from typing import Any, Generator, Iterable, List, TextIO, Union
import struct import struct
import sys import sys
from phasm import compiler
from phasm.codestyle import phasm_render from phasm.codestyle import phasm_render
from phasm.type3 import types as type3types from phasm.type3 import types as type3types
from phasm.runtime import calculate_alloc_size from phasm.runtime import calculate_alloc_size
@ -55,21 +56,47 @@ class Suite:
# Check if code formatting works # Check if code formatting works
assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests assert self.code_py == '\n' + phasm_render(runner.phasm_ast) # \n for formatting in tests
wasm_args = [] 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: if args:
write_header(sys.stderr, 'Memory (pre alloc)') write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr) runner.interpreter_dump_memory(sys.stderr)
for arg in args: for arg, arg_typ in zip(args, func_args):
if isinstance(arg, (int, float, )): assert not isinstance(arg_typ, type3types.PlaceholderForType), \
'Cannot call polymorphic function from outside'
if arg_typ in (type3types.u8, type3types.u32, type3types.u64, ):
assert isinstance(arg, int)
wasm_args.append(arg) wasm_args.append(arg)
continue continue
if isinstance(arg, bytes): if arg_typ in (type3types.i8, type3types.i32, type3types.i64, ):
adr = runner.call('stdlib.types.__alloc_bytes__', len(arg)) assert isinstance(arg, int)
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(arg)}\n') wasm_args.append(arg)
continue
runner.interpreter_write_memory(adr + 4, arg) if arg_typ in (type3types.f32, type3types.f64, ):
assert isinstance(arg, float)
wasm_args.append(arg)
continue
if arg_typ is type3types.bytes:
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
wasm_args.append(adr)
continue
if isinstance(arg_typ, type3types.AppliedType3):
if arg_typ.base is type3types.static_array:
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
wasm_args.append(adr)
continue
if arg_typ.base is type3types.tuple:
adr = _allocate_memory_stored_value(runner, arg_typ, arg)
wasm_args.append(adr) wasm_args.append(adr)
continue continue
@ -95,6 +122,98 @@ class Suite:
def write_header(textio: TextIO, msg: str) -> None: def write_header(textio: TextIO, msg: str) -> None:
textio.write(f'{DASHES} {msg.ljust(16)} {DASHES}\n') 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 type3types.bytes:
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
return 4
if isinstance(val_typ, type3types.PrimitiveType3):
to_write = WRITE_LOOKUP_MAP[val_typ.name](val)
runner.interpreter_write_memory(adr, to_write)
return len(to_write)
if isinstance(val_typ, type3types.AppliedType3):
if val_typ.base in (type3types.static_array, type3types.tuple, ):
adr2 = _allocate_memory_stored_value(runner, val_typ, val)
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
return 4
raise NotImplementedError(val_typ, val)
def _allocate_memory_stored_value(
runner: runners.RunnerBase,
val_typ: type3types.Type3,
val: Any
) -> int:
if val_typ is type3types.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
if isinstance(val_typ, type3types.AppliedType3):
if val_typ.base is type3types.static_array:
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')
val_el_typ = val_typ.args[0]
assert not isinstance(val_el_typ, type3types.PlaceholderForType)
val_el_alloc_size = calculate_alloc_size(val_el_typ)
tuple_len_obj = val_typ.args[1]
assert isinstance(tuple_len_obj, type3types.IntType3)
tuple_len = tuple_len_obj.value
assert tuple_len == len(val)
offset = adr
for val_el_val in val:
offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val)
return adr
if val_typ.base is type3types.tuple:
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(val_typ.args)
offset = adr
for val_el_val, val_el_typ in zip(val, val_typ.args):
assert not isinstance(val_el_typ, type3types.PlaceholderForType)
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( def _load_memory_stored_returned_value(
runner: runners.RunnerBase, runner: runners.RunnerBase,
func_name: str, func_name: str,
@ -111,7 +230,19 @@ def _load_memory_stored_returned_value(
if ret_type3 in (type3types.u8, type3types.u32, type3types.u64): if ret_type3 in (type3types.u8, type3types.u32, type3types.u64):
assert isinstance(wasm_value, int), wasm_value assert isinstance(wasm_value, int), wasm_value
assert 0 <= wasm_value, 'TODO: Extract negative values'
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 return wasm_value
if ret_type3 in (type3types.f32, type3types.f64, ): if ret_type3 in (type3types.f32, type3types.f64, ):
@ -192,12 +323,11 @@ def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> An
raise NotImplementedError(typ, inp) raise NotImplementedError(typ, inp)
def _load_bytes_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> bytes: def _load_bytes_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> bytes:
sys.stderr.write(f'Reading 0x{adr:08x}\n') sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n')
read_bytes = runner.interpreter_read_memory(adr, 4) read_bytes = runner.interpreter_read_memory(adr, 4)
bytes_len, = struct.unpack('<I', read_bytes) bytes_len, = struct.unpack('<I', read_bytes)
adr += 4 adr += 4
sys.stderr.write(f'Reading 0x{adr:08x}\n')
return runner.interpreter_read_memory(adr, bytes_len) return runner.interpreter_read_memory(adr, bytes_len)
def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator[bytes, None, None]: def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator[bytes, None, None]:
@ -207,6 +337,8 @@ def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator
offset += size offset += size
def _load_static_array_from_address(runner: runners.RunnerBase, typ: type3types.AppliedType3, adr: int) -> Any: def _load_static_array_from_address(runner: runners.RunnerBase, typ: type3types.AppliedType3, adr: int) -> Any:
sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n')
assert 2 == len(typ.args) assert 2 == len(typ.args)
sub_typ, len_typ = typ.args sub_typ, len_typ = typ.args
@ -226,6 +358,8 @@ def _load_static_array_from_address(runner: runners.RunnerBase, typ: type3types.
) )
def _load_tuple_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> Any: def _load_tuple_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> Any:
sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n')
assert isinstance(typ, type3types.AppliedType3) assert isinstance(typ, type3types.AppliedType3)
assert typ.base is type3types.tuple assert typ.base is type3types.tuple

View File

@ -31,9 +31,4 @@ def testEntry(data: bytes) -> u32:
result = Suite(code_py).run_code(b'a') result = Suite(code_py).run_code(b'a')
# exp_result returns a unsigned integer, as is proper assert exp_result == result.returned_value
exp_data = struct.pack('I', exp_result)
# ints extracted from WebAssembly are always signed
data = struct.pack('i', result.returned_value)
assert exp_data == data

View File

@ -1,4 +1,4 @@
# runtime_extract_value # runtime_extract_value_literal
As a developer As a developer
I want to extract a $TYPE value I want to extract a $TYPE value
@ -14,6 +14,22 @@ def testEntry() -> $TYPE:
expect(VAL0) expect(VAL0)
``` ```
# runtime_extract_value_round_trip
As a developer
I want to extract a $TYPE value that I've input
In order let the code select a value that I've predefined
```py
@exported
def testEntry(x: $TYPE) -> $TYPE:
return x
```
```py
expect(VAL0, given=[VAL0])
```
# module_constant_def_ok # module_constant_def_ok
As a developer As a developer

View File

@ -30,8 +30,10 @@ def apply_settings(settings, txt):
txt = txt.replace(f'${k}', v) txt = txt.replace(f'${k}', v)
return txt return txt
def generate_assertion_expect(result, arg): def generate_assertion_expect(result, arg, given=None):
result.append('result = Suite(code_py).run_code()') given = given or []
result.append('result = Suite(code_py).run_code(' + ', '.join(repr(x) for x in given) + ')')
result.append(f'assert {repr(arg)} == result.returned_value') result.append(f'assert {repr(arg)} == result.returned_value')
def generate_assertion_expect_type_error(result, error_msg, error_comment = None): def generate_assertion_expect_type_error(result, error_msg, error_comment = None):

View File

@ -4,20 +4,6 @@ from phasm.type3.entry import Type3Exception
from ..helpers import Suite from ..helpers import Suite
@pytest.mark.integration_test
def test_bytes_address():
code_py = """
@exported
def testEntry(f: bytes) -> bytes:
return f
"""
result = Suite(code_py).run_code(b'This is a test')
# THIS DEPENDS ON THE ALLOCATOR
# A different allocator will return a different value
assert 20 == result.returned_value
@pytest.mark.integration_test @pytest.mark.integration_test
def test_bytes_length(): def test_bytes_length():
code_py = """ code_py = """
@ -42,25 +28,11 @@ def testEntry(f: bytes) -> u8:
assert 0x61 == result.returned_value assert 0x61 == result.returned_value
@pytest.mark.integration_test
def test_constant():
code_py = """
CONSTANT: bytes = b'ABCDEF'
@exported
def testEntry() -> u8:
return CONSTANT[0]
"""
result = Suite(code_py).run_code()
assert 0x41 == result.returned_value
@pytest.mark.integration_test @pytest.mark.integration_test
def test_bytes_index_out_of_bounds(): def test_bytes_index_out_of_bounds():
code_py = """ code_py = """
@exported @exported
def testEntry(f: bytes) -> u8: def testEntry(f: bytes, g: bytes) -> u8:
return f[50] return f[50]
""" """