From 0131b84146e05e99fd9efde6e9370ed94148cf8f Mon Sep 17 00:00:00 2001 From: "Johan B.W. de Vries" Date: Mon, 13 Nov 2023 14:32:17 +0100 Subject: [PATCH] Implemented round trip input / output --- tests/integration/helpers.py | 156 ++++++++++++++++-- tests/integration/test_examples/test_crc32.py | 7 +- tests/integration/test_lang/generator.md | 18 +- tests/integration/test_lang/generator.py | 6 +- tests/integration/test_lang/test_bytes.py | 30 +--- 5 files changed, 168 insertions(+), 49 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 2f21776..c73c9e0 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -1,8 +1,9 @@ -from typing import Any, Generator, Iterable, TextIO +from typing import Any, Generator, Iterable, List, TextIO, Union import struct import sys +from phasm import compiler from phasm.codestyle import phasm_render from phasm.type3 import types as type3types from phasm.runtime import calculate_alloc_size @@ -55,24 +56,50 @@ class Suite: # Check if code formatting works 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: write_header(sys.stderr, 'Memory (pre alloc)') runner.interpreter_dump_memory(sys.stderr) - for arg in args: - if isinstance(arg, (int, float, )): + for arg, arg_typ in zip(args, func_args): + 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) continue - if isinstance(arg, bytes): - adr = runner.call('stdlib.types.__alloc_bytes__', len(arg)) - sys.stderr.write(f'Allocation 0x{adr:08x} {repr(arg)}\n') + if arg_typ in (type3types.i8, type3types.i32, type3types.i64, ): + assert isinstance(arg, int) + 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) + continue + raise NotImplementedError(arg) write_header(sys.stderr, 'Memory (pre run)') @@ -95,6 +122,98 @@ class Suite: 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 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( runner: runners.RunnerBase, func_name: str, @@ -111,7 +230,19 @@ def _load_memory_stored_returned_value( if ret_type3 in (type3types.u8, type3types.u32, type3types.u64): 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 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) 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) bytes_len, = struct.unpack(' Generator[bytes, None, None]: @@ -207,6 +337,8 @@ def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator offset += size 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) 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: + sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n') + assert isinstance(typ, type3types.AppliedType3) assert typ.base is type3types.tuple diff --git a/tests/integration/test_examples/test_crc32.py b/tests/integration/test_examples/test_crc32.py index d9f74bf..daf5cb4 100644 --- a/tests/integration/test_examples/test_crc32.py +++ b/tests/integration/test_examples/test_crc32.py @@ -31,9 +31,4 @@ def testEntry(data: bytes) -> u32: result = Suite(code_py).run_code(b'a') - # exp_result returns a unsigned integer, as is proper - 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 + assert exp_result == result.returned_value diff --git a/tests/integration/test_lang/generator.md b/tests/integration/test_lang/generator.md index f5041b7..28c88e5 100644 --- a/tests/integration/test_lang/generator.md +++ b/tests/integration/test_lang/generator.md @@ -1,4 +1,4 @@ -# runtime_extract_value +# runtime_extract_value_literal As a developer I want to extract a $TYPE value @@ -14,6 +14,22 @@ def testEntry() -> $TYPE: 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 As a developer diff --git a/tests/integration/test_lang/generator.py b/tests/integration/test_lang/generator.py index 7cb486a..0b67bd4 100644 --- a/tests/integration/test_lang/generator.py +++ b/tests/integration/test_lang/generator.py @@ -30,8 +30,10 @@ def apply_settings(settings, txt): txt = txt.replace(f'${k}', v) return txt -def generate_assertion_expect(result, arg): - result.append('result = Suite(code_py).run_code()') +def generate_assertion_expect(result, arg, given=None): + 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') def generate_assertion_expect_type_error(result, error_msg, error_comment = None): diff --git a/tests/integration/test_lang/test_bytes.py b/tests/integration/test_lang/test_bytes.py index 6a3fe07..6449831 100644 --- a/tests/integration/test_lang/test_bytes.py +++ b/tests/integration/test_lang/test_bytes.py @@ -4,20 +4,6 @@ from phasm.type3.entry import Type3Exception 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 def test_bytes_length(): code_py = """ @@ -42,25 +28,11 @@ def testEntry(f: bytes) -> u8: 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 def test_bytes_index_out_of_bounds(): code_py = """ @exported -def testEntry(f: bytes) -> u8: +def testEntry(f: bytes, g: bytes) -> u8: return f[50] """