diff --git a/Makefile b/Makefile index 6422080..20dd290 100644 --- a/Makefile +++ b/Makefile @@ -51,3 +51,8 @@ clean-generated-tests: .SECONDARY: # Keep intermediate files .PHONY: examples + +# So generally the right thing to do is to delete the target file if the recipe fails after beginning to change the file. +# make will do this if .DELETE_ON_ERROR appears as a target. +# This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it. +.DELETE_ON_ERROR: diff --git a/phasm/compiler.py b/phasm/compiler.py index 7258ef6..669c8cd 100644 --- a/phasm/compiler.py +++ b/phasm/compiler.py @@ -1,7 +1,7 @@ """ This module contains the code to convert parsed Ourlang into WebAssembly code """ -from typing import List, Union +from typing import List, Optional, Union import struct @@ -59,6 +59,11 @@ def type3(inp: type3types.Type3OrPlaceholder) -> wasm.WasmType: if inp == type3types.u64: return wasm.WasmTypeInt64() + if inp == type3types.i8: + # WebAssembly has only support for 32 and 64 bits + # So we need to store more memory per byte + return wasm.WasmTypeInt32() + if inp == type3types.i32: return wasm.WasmTypeInt32() @@ -853,10 +858,13 @@ def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstruc # Store each member individually for memname, mtyp3 in inp.struct_type3.members.items(): - mtyp = LOAD_STORE_TYPE_MAP.get(mtyp3.name) + mtyp: Optional[str] + if isinstance(mtyp3, type3types.StructType3): + mtyp = 'i32' + else: + mtyp = LOAD_STORE_TYPE_MAP.get(mtyp3.name) + if mtyp is None: - # In the future might extend this by having structs or tuples - # as members of struct or tuples raise NotImplementedError(expression, inp, mtyp3) wgn.local.get(tmp_var) diff --git a/phasm/runtime.py b/phasm/runtime.py index a09c29c..3db1b69 100644 --- a/phasm/runtime.py +++ b/phasm/runtime.py @@ -24,7 +24,7 @@ def calculate_alloc_size(typ: type3types.Type3, is_member: bool = False) -> int: return 4 return sum( - calculate_alloc_size(x) + calculate_alloc_size(x, is_member=True) for x in typ.members.values() ) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 321c147..b96cedd 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -102,6 +102,11 @@ class Suite: wasm_args.append(adr) continue + if isinstance(arg_typ, type3types.StructType3): + adr = _allocate_memory_stored_value(runner, arg_typ, arg) + wasm_args.append(adr) + continue + raise NotImplementedError(arg) write_header(sys.stderr, 'Memory (pre run)') @@ -157,6 +162,11 @@ def _write_memory_stored_value( runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2)) return 4 + if isinstance(val_typ, type3types.StructType3): + 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( @@ -214,6 +224,24 @@ def _allocate_memory_stored_value( offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val) return adr + if isinstance(val_typ, type3types.StructType3): + 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(val_typ.members.keys()) + + offset = adr + for val_el_name, val_el_typ in val_typ.members.items(): + assert not isinstance(val_el_typ, type3types.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( @@ -267,6 +295,9 @@ def _load_memory_stored_returned_value( return _load_tuple_from_address(runner, ret_type3, wasm_value) + if isinstance(ret_type3, type3types.StructType3): + return _load_struct_from_address(runner, ret_type3, wasm_value) + raise NotImplementedError(ret_type3, wasm_value) def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> Any: @@ -305,7 +336,7 @@ def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> An return struct.unpack(' An if typ.base is type3types.tuple: return _load_tuple_from_address(runner, typ, adr) + if isinstance(typ, type3types.StructType3): + # Note: For structs, inp should contain a 4 byte pointer + assert len(inp) == 4 + adr = struct.unpack(' bytes: @@ -383,3 +421,29 @@ def _load_tuple_from_address(runner: runners.RunnerBase, typ: type3types.Type3, _unpack(runner, arg_typ, arg_bytes) for arg_typ, arg_bytes in zip(typ_list, _split_read_bytes(read_bytes, arg_sizes)) ) + +def _load_struct_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.StructType3) + + name_list = list(typ.members) + + typ_list = [ + x + for x in typ.members.values() + if not isinstance(x, type3types.PlaceholderForType) + ] + assert len(typ_list) == len(typ.members) + + 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)) + } diff --git a/tests/integration/test_lang/generator.md b/tests/integration/test_lang/generator.md index 28c88e5..b4a4e48 100644 --- a/tests/integration/test_lang/generator.md +++ b/tests/integration/test_lang/generator.md @@ -118,6 +118,11 @@ if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): 'Mismatch between applied types argument count', 'The type of the value returned from function constant should match its return type', ) +elif TYPE_NAME.startswith('struct_'): + expect_type_error( + TYPE + ' must be tuple (u32) instead', + 'The type of the value returned from function constant should match its return type', + ) else: expect_type_error( 'Must be tuple', @@ -175,6 +180,11 @@ if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): 'Mismatch between applied types argument count', 'The type of the value returned from function constant should match its return type', ) +elif TYPE_NAME.startswith('struct_'): + expect_type_error( + TYPE + ' must be tuple (u32) instead', + 'The type of the value returned from function constant should match its return type', + ) else: expect_type_error( TYPE_NAME + ' must be tuple (u32) instead', @@ -226,6 +236,11 @@ if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): 'Mismatch between applied types argument count', 'The type of the value returned from function select should match its return type', ) +elif TYPE_NAME.startswith('struct_'): + expect_type_error( + TYPE + ' must be tuple (u32) instead', + 'The type of the value returned from function select should match its return type', + ) else: expect_type_error( TYPE_NAME + ' must be tuple (u32) instead', @@ -274,6 +289,11 @@ if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): # FIXME: Shouldn't this be the same as for the else statement? 'The type of the value passed to argument x of function helper should match the type of that argument', ) +elif TYPE_NAME.startswith('struct_'): + expect_type_error( + TYPE + ' must be tuple (u32) instead', + 'The type of the value passed to argument x of function helper should match the type of that argument', + ) else: expect_type_error( 'Must be tuple', @@ -325,6 +345,11 @@ if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): 'Mismatch between applied types argument count', 'The type of the value passed to argument x of function helper should match the type of that argument', ) +elif TYPE_NAME.startswith('struct_'): + expect_type_error( + TYPE + ' must be tuple (u32) instead', + 'The type of the value passed to argument x of function helper should match the type of that argument', + ) else: expect_type_error( TYPE_NAME + ' must be tuple (u32) instead', diff --git a/tests/integration/test_lang/generator.py b/tests/integration/test_lang/generator.py index 0b67bd4..872dacf 100644 --- a/tests/integration/test_lang/generator.py +++ b/tests/integration/test_lang/generator.py @@ -1,3 +1,5 @@ +from typing import Any, Dict + import functools import json import sys @@ -27,6 +29,9 @@ def get_tests(template): def apply_settings(settings, txt): for k, v in settings.items(): + if k in ('CODE_HEADER', 'PYTHON'): + continue + txt = txt.replace(f'${k}', v) return txt @@ -42,16 +47,41 @@ def generate_assertion_expect_type_error(result, error_msg, error_comment = None result.append(f'assert {repr(error_msg)} == exc_info.value.args[0][0].msg') result.append(f'assert {repr(error_comment)} == exc_info.value.args[0][0].comment') +def json_does_not_support_byte_values_fix(inp: Dict[Any, Any]): + key_names = list(inp) + for key in key_names: + value = inp[key] + + if isinstance(value, str): + if value.startswith('bytes:'): + inp[key] = value[6:].encode() + continue + + if isinstance(value, dict): + json_does_not_support_byte_values_fix(value) + continue + + if isinstance(value, list): + raise NotImplementedError + def generate_assertions(settings, result_code): result = [] locals_ = { - 'VAL0': eval(settings['VAL0']), + 'TYPE': settings['TYPE'], 'TYPE_NAME': settings['TYPE_NAME'], 'expect': functools.partial(generate_assertion_expect, result), 'expect_type_error': functools.partial(generate_assertion_expect_type_error, result), } + if 'PYTHON' in settings: + locals_.update(settings['PYTHON']) + + if 'VAL0' not in locals_: + locals_['VAL0'] = eval(settings['VAL0']) + + json_does_not_support_byte_values_fix(locals_) + exec(result_code, {}, locals_) return ' ' + '\n '.join(result) + '\n' @@ -93,9 +123,14 @@ def generate_code(markdown, template, settings): print(' ' + user_story.replace('\n', '\n ')) print(' """') print(' code_py = """') + if 'CODE_HEADER' in settings: + for lin in settings['CODE_HEADER']: + print(lin) + print() print(inp_code.rstrip('\n')) print('"""') print() + print(generate_assertions(settings, result_code)) print() diff --git a/tests/integration/test_lang/generator_struct_all_primitives.json b/tests/integration/test_lang/generator_struct_all_primitives.json new file mode 100644 index 0000000..b697761 --- /dev/null +++ b/tests/integration/test_lang/generator_struct_all_primitives.json @@ -0,0 +1,40 @@ +{ + "TYPE_NAME": "struct_all_primitives", + "TYPE": "StructallPrimitives", + "VAL0": "StructallPrimitives(1, 4, 8, 1, -1, 4, -4, 8, -8, 125.125, -125.125, 5000.5, -5000.5, b'Hello, world!')", + "CODE_HEADER": [ + "class StructallPrimitives:", + " val00: u8", + " val01: u32", + " val02: u64", + " val10: i8", + " val11: i8", + " val12: i32", + " val13: i32", + " val14: i64", + " val15: i64", + " val20: f32", + " val21: f32", + " val22: f64", + " val23: f64", + " val30: bytes" + ], + "PYTHON": { + "VAL0": { + "val00": 1, + "val01": 4, + "val02": 8, + "val10": 1, + "val11": -1, + "val12": 4, + "val13": -4, + "val14": 8, + "val15": -8, + "val20": 125.125, + "val21": -125.125, + "val22": 5000.5, + "val23": -5000.5, + "val30": "bytes:Hello, world!" + } + } +} diff --git a/tests/integration/test_lang/generator_struct_nested.json b/tests/integration/test_lang/generator_struct_nested.json new file mode 100644 index 0000000..c96b960 --- /dev/null +++ b/tests/integration/test_lang/generator_struct_nested.json @@ -0,0 +1,25 @@ +{ + "TYPE_NAME": "struct_nested", + "TYPE": "StructNested", + "VAL0": "StructNested(4, SubStruct(8, 16), 20)", + "CODE_HEADER": [ + "class SubStruct:", + " val00: u8", + " val01: u8", + "", + "class StructNested:", + " val00: u64", + " val01: SubStruct", + " val02: u64" + ], + "PYTHON": { + "VAL0": { + "val00": 4, + "val01": { + "val00": 8, + "val01": 16 + }, + "val02": 20 + } + } +} diff --git a/tests/integration/test_lang/generator_struct_one_field.json b/tests/integration/test_lang/generator_struct_one_field.json new file mode 100644 index 0000000..7f4ce54 --- /dev/null +++ b/tests/integration/test_lang/generator_struct_one_field.json @@ -0,0 +1,12 @@ +{ + "TYPE_NAME": "struct_one_field", + "TYPE": "StructOneField", + "VAL0": "StructOneField(4)", + "CODE_HEADER": [ + "class StructOneField:", + " value: u32" + ], + "PYTHON": { + "VAL0": {"value": 4} + } +}