From 97b61e3ee12d73386451aef09cc5d2e01dd0b44c Mon Sep 17 00:00:00 2001 From: "Johan B.W. de Vries" Date: Sat, 11 Nov 2023 12:02:37 +0100 Subject: [PATCH] Test generation framework with typing improvements Prior to this PR, each type would have its own handwritten test suite. The end result was that not all types were tested for all situations. This PR adds a framework based on a Markdown file, which generates the basic tests for the types defined in json files. These are auto generated and updated by the Makefile before the test suite is run. Also, a number of unsupported type combinations are now supported. Also, we now support negative literals. Also, allocation calculation fixes for nested types. Also, the test helpers can now properly import and export typed variables such as bytes, static arrays and tuples. This may come in handy when it comes to phasm platform wanting to route data. Also, adds better support for i8 type. Also, started on a runtime.py, since there's quite some code now that deals with compile time handling of WebAssembly stuff. Also, minor improvement to the type constrains, namely we better match 'tuple' literals with static array types. Also, reduced spam when printing the type analysis results; constraints that go back on the backlog are now no longer printed one by one. It now also prints the end results of the typing analysis. Also, reorganized the big test_primitives test into type classes. Also, replaced pylint with ruff. --- .gitignore | 2 + Makefile | 17 +- phasm/__main__.py | 3 +- phasm/codestyle.py | 3 +- phasm/compiler.py | 112 ++-- phasm/ourlang.py | 9 +- phasm/parser.py | 86 ++-- phasm/runtime.py | 69 +++ phasm/stdlib/alloc.py | 3 +- phasm/stdlib/types.py | 5 +- phasm/type3/constraints.py | 44 +- phasm/type3/constraintsgenerator.py | 27 +- phasm/type3/entry.py | 37 +- phasm/wasm.py | 1 + phasm/wasmgenerator.py | 5 +- pyproject.toml | 3 + requirements.txt | 2 +- tests/integration/constants.py | 16 - tests/integration/helpers.py | 394 +++++++++++++- tests/integration/runners.py | 12 +- .../integration/test_examples/test_buffer.py | 1 + tests/integration/test_examples/test_crc32.py | 9 +- tests/integration/test_examples/test_fib.py | 1 + tests/integration/test_lang/generator.md | 358 +++++++++++++ tests/integration/test_lang/generator.py | 154 ++++++ .../test_lang/generator_bytes.json | 4 + .../integration/test_lang/generator_f32.json | 4 + .../integration/test_lang/generator_f64.json | 4 + .../integration/test_lang/generator_i32.json | 4 + .../integration/test_lang/generator_i64.json | 4 + ...enerator_static_array_tuple_u32_u32_3.json | 5 + .../generator_static_array_u64_32.json | 5 + .../generator_static_array_with_structs.json | 33 ++ .../generator_struct_all_primitives.json | 40 ++ .../test_lang/generator_struct_nested.json | 25 + .../test_lang/generator_struct_one_field.json | 12 + .../generator_tuple_all_primitives.json | 5 + .../test_lang/generator_tuple_nested.json | 5 + .../test_lang/generator_tuple_u64_u32_u8.json | 5 + .../integration/test_lang/generator_u32.json | 4 + .../integration/test_lang/generator_u64.json | 4 + tests/integration/test_lang/test_bits.py | 115 +++++ tests/integration/test_lang/test_builtins.py | 1 + tests/integration/test_lang/test_bytes.py | 55 +- tests/integration/test_lang/test_floating.py | 18 + .../integration/test_lang/test_fractional.py | 33 ++ .../test_lang/test_function_calls.py | 34 ++ tests/integration/test_lang/test_if.py | 1 + .../{test_interface.py => test_imports.py} | 1 + tests/integration/test_lang/test_integral.py | 33 ++ tests/integration/test_lang/test_literals.py | 17 + tests/integration/test_lang/test_num.py | 121 +++++ .../integration/test_lang/test_primitives.py | 486 ------------------ .../test_lang/test_static_array.py | 123 +---- tests/integration/test_lang/test_struct.py | 88 +--- tests/integration/test_lang/test_tuple.py | 212 -------- tests/integration/test_stdlib/test_alloc.py | 1 + 57 files changed, 1736 insertions(+), 1139 deletions(-) create mode 100644 phasm/runtime.py create mode 100644 pyproject.toml delete mode 100644 tests/integration/constants.py create mode 100644 tests/integration/test_lang/generator.md create mode 100644 tests/integration/test_lang/generator.py create mode 100644 tests/integration/test_lang/generator_bytes.json create mode 100644 tests/integration/test_lang/generator_f32.json create mode 100644 tests/integration/test_lang/generator_f64.json create mode 100644 tests/integration/test_lang/generator_i32.json create mode 100644 tests/integration/test_lang/generator_i64.json create mode 100644 tests/integration/test_lang/generator_static_array_tuple_u32_u32_3.json create mode 100644 tests/integration/test_lang/generator_static_array_u64_32.json create mode 100644 tests/integration/test_lang/generator_static_array_with_structs.json create mode 100644 tests/integration/test_lang/generator_struct_all_primitives.json create mode 100644 tests/integration/test_lang/generator_struct_nested.json create mode 100644 tests/integration/test_lang/generator_struct_one_field.json create mode 100644 tests/integration/test_lang/generator_tuple_all_primitives.json create mode 100644 tests/integration/test_lang/generator_tuple_nested.json create mode 100644 tests/integration/test_lang/generator_tuple_u64_u32_u8.json create mode 100644 tests/integration/test_lang/generator_u32.json create mode 100644 tests/integration/test_lang/generator_u64.json create mode 100644 tests/integration/test_lang/test_bits.py create mode 100644 tests/integration/test_lang/test_floating.py create mode 100644 tests/integration/test_lang/test_fractional.py create mode 100644 tests/integration/test_lang/test_function_calls.py rename tests/integration/test_lang/{test_interface.py => test_imports.py} (99%) create mode 100644 tests/integration/test_lang/test_integral.py create mode 100644 tests/integration/test_lang/test_literals.py create mode 100644 tests/integration/test_lang/test_num.py delete mode 100644 tests/integration/test_lang/test_primitives.py diff --git a/.gitignore b/.gitignore index b8a823a..29c7ccf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ /.coverage /venv +/tests/integration/test_lang/test_generated_*.py + __pycache__ diff --git a/Makefile b/Makefile index c371232..dc4a68b 100644 --- a/Makefile +++ b/Makefile @@ -24,14 +24,14 @@ WASM2C := $(WABT_DIR)/bin/wasm2c examples: venv/.done $(subst .py,.wasm,$(wildcard examples/*.py)) $(subst .py,.wat.html,$(wildcard examples/*.py)) $(subst .py,.py.html,$(wildcard examples/*.py)) venv/bin/python3 -m http.server --directory examples -test: venv/.done +test: venv/.done $(subst .json,.py,$(subst /generator_,/test_generated_,$(wildcard tests/integration/test_lang/generator_*.json))) venv/bin/pytest tests $(TEST_FLAGS) lint: venv/.done - venv/bin/pylint phasm + venv/bin/ruff check phasm tests typecheck: venv/.done - venv/bin/mypy --strict phasm tests/integration/runners.py + venv/bin/mypy --strict phasm tests/integration/helpers.py tests/integration/runners.py venv/.done: requirements.txt python3.10 -m venv venv @@ -39,9 +39,20 @@ venv/.done: requirements.txt venv/bin/python3 -m pip install -r $^ touch $@ +tests/integration/test_lang/test_generated_%.py: venv/.done tests/integration/test_lang/generator.py tests/integration/test_lang/generator.md tests/integration/test_lang/generator_%.json + venv/bin/python3 tests/integration/test_lang/generator.py tests/integration/test_lang/generator.md tests/integration/test_lang/generator_$*.json > $@ + clean-examples: rm -f examples/*.wat examples/*.wasm examples/*.wat.html examples/*.py.html +clean-generated-tests: + rm -f tests/integration/test_lang/test_generated_*.py + .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/__main__.py b/phasm/__main__.py index 97615e0..8afa522 100644 --- a/phasm/__main__.py +++ b/phasm/__main__.py @@ -4,9 +4,10 @@ Functions for using this module from CLI import sys +from .compiler import phasm_compile from .parser import phasm_parse from .type3.entry import phasm_type3 -from .compiler import phasm_compile + def main(source: str, sink: str) -> int: """ diff --git a/phasm/codestyle.py b/phasm/codestyle.py index ea5c207..62f60b1 100644 --- a/phasm/codestyle.py +++ b/phasm/codestyle.py @@ -9,6 +9,7 @@ from . import ourlang from .type3 import types as type3types from .type3.types import TYPE3_ASSERTION_ERROR, Type3, Type3OrPlaceholder + def phasm_render(inp: ourlang.Module) -> str: """ Public method for rendering a Phasm module into Phasm code @@ -39,7 +40,7 @@ def type3(inp: Type3OrPlaceholder) -> str: assert isinstance(inp.args[0], Type3), TYPE3_ASSERTION_ERROR assert isinstance(inp.args[1], type3types.IntType3), TYPE3_ASSERTION_ERROR - return inp.args[0].name + '[' + inp.args[1].name + ']' + return type3(inp.args[0]) + '[' + inp.args[1].name + ']' return inp.name diff --git a/phasm/compiler.py b/phasm/compiler.py index 2a59f2c..988fbe9 100644 --- a/phasm/compiler.py +++ b/phasm/compiler.py @@ -1,21 +1,20 @@ """ This module contains the code to convert parsed Ourlang into WebAssembly code """ -from typing import List, Union - import struct +from typing import List, Optional -from . import codestyle -from . import ourlang -from .type3 import types as type3types -from . import wasm - +from . import codestyle, ourlang, wasm +from .runtime import calculate_alloc_size, calculate_member_offset from .stdlib import alloc as stdlib_alloc from .stdlib import types as stdlib_types +from .type3 import types as type3types from .wasmgenerator import Generator as WasmGenerator LOAD_STORE_TYPE_MAP = { + 'i8': 'i32', # Have to use an u32, since there is no native i8 type 'u8': 'i32', # Have to use an u32, since there is no native u8 type + 'i32': 'i32', 'i64': 'i64', 'u32': 'i32', @@ -56,6 +55,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() @@ -173,7 +177,7 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> wgn.add_statement('nop', comment=f'{tmp_var.name} := ({comment_elements})') # Allocated the required amounts of bytes in memory - wgn.i32.const(_calculate_alloc_size(inp.type3, is_member=False)) + wgn.i32.const(calculate_alloc_size(inp.type3, is_member=False)) wgn.call(stdlib_alloc.__alloc__) wgn.local.set(tmp_var) @@ -186,7 +190,7 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> assert element.type3 == exp_type3 - if isinstance(exp_type3, type3types.AppliedType3) and exp_type3.base is type3types.tuple: + if isinstance(exp_type3, type3types.StructType3) or isinstance(exp_type3, type3types.AppliedType3): mtyp = 'i32' else: assert isinstance(exp_type3, type3types.PrimitiveType3), NotImplementedError('Tuple of applied types / structs') @@ -198,7 +202,7 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> wgn.add_statement(f'{mtyp}.store', 'offset=' + str(offset)) wgn.add_statement('nop', comment='POST') - offset += _calculate_alloc_size(exp_type3, is_member=True) + offset += calculate_alloc_size(exp_type3, is_member=True) # Return the allocated address wgn.local.get(tmp_var) @@ -210,7 +214,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: if isinstance(inp, ourlang.ConstantPrimitive): assert isinstance(inp.type3, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR - if inp.type3 == type3types.u8: + if inp.type3 in (type3types.i8, type3types.u8, ): # No native u8 type - treat as i32, with caution assert isinstance(inp.value, int) wgn.i32.const(inp.value) @@ -435,7 +439,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: wgn.unreachable(comment='Out of bounds') wgn.local.get(tmp_var) - wgn.i32.const(_calculate_alloc_size(el_type)) + wgn.i32.const(calculate_alloc_size(el_type)) wgn.i32.mul() wgn.i32.add() @@ -452,7 +456,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: offset = 0 for el_type in inp.varref.type3.args[0:inp.index.value]: assert isinstance(el_type, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR - offset += _calculate_alloc_size(el_type) + offset += calculate_alloc_size(el_type) # This doubles as the out of bounds check el_type = inp.varref.type3.args[inp.index.value] @@ -478,7 +482,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: mtyp = LOAD_STORE_TYPE_MAP[inp.struct_type3.members[inp.member].name] expression(wgn, inp.varref) - wgn.add_statement(f'{mtyp}.load', 'offset=' + str(_calculate_member_offset( + wgn.add_statement(f'{mtyp}.load', 'offset=' + str(calculate_member_offset( inp.struct_type3, inp.member ))) return @@ -664,7 +668,7 @@ def module_data_u8(inp: int) -> bytes: # FIXME: All u8 values are stored as u32 """ - return struct.pack(' bytes: """ @@ -678,6 +682,14 @@ def module_data_u64(inp: int) -> bytes: """ return struct.pack(' bytes: + """ + Compile: module data, i8 value + + # FIXME: All i8 values are stored as i32 + """ + return struct.pack(' bytes: """ Compile: module data, i32 value @@ -745,6 +757,12 @@ def module_data(inp: ourlang.ModuleData) -> bytes: data_list.append(module_data_u64(constant.value)) continue + if constant.type3 == type3types.i8: + assert isinstance(constant, ourlang.ConstantPrimitive) + assert isinstance(constant.value, int) + data_list.append(module_data_i8(constant.value)) + continue + if constant.type3 == type3types.i32: assert isinstance(constant, ourlang.ConstantPrimitive) assert isinstance(constant.value, int) @@ -830,74 +848,26 @@ def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstruc tmp_var = wgn.temp_var_i32('struct_adr') # Allocated the required amounts of bytes in memory - wgn.i32.const(_calculate_alloc_size(inp.struct_type3)) + wgn.i32.const(calculate_alloc_size(inp.struct_type3)) wgn.call(stdlib_alloc.__alloc__) wgn.local.set(tmp_var) # 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) or isinstance(mtyp3, type3types.AppliedType3): + 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) wgn.add_statement('local.get', f'${memname}') - wgn.add_statement(f'{mtyp}.store', 'offset=' + str(_calculate_member_offset( + wgn.add_statement(f'{mtyp}.store', 'offset=' + str(calculate_member_offset( inp.struct_type3, memname ))) # Return the allocated address wgn.local.get(tmp_var) - -def _calculate_alloc_size(typ: Union[type3types.StructType3, type3types.Type3], is_member: bool = False) -> int: - if typ == type3types.u8: - return 4 # FIXME: We allocate 4 bytes for every u8 since you load them into an i32 - - if typ in (type3types.u32, type3types.i32, type3types.f32, ): - return 4 - - if typ in (type3types.u64, type3types.i64, type3types.f64, ): - return 8 - - if isinstance(typ, type3types.StructType3): - if is_member: - # Structs referred to by other structs or tuples are pointers - return 4 - - return sum( - _calculate_alloc_size(x) - for x in typ.members.values() - ) - - if isinstance(typ, type3types.AppliedType3): - if typ.base is type3types.tuple: - if is_member: - # tuples referred to by other structs or tuples are pointers - return 4 - - size = 0 - for arg in typ.args: - assert not isinstance(arg, type3types.IntType3) - - if isinstance(arg, type3types.PlaceholderForType): - assert not arg.resolve_as is None - arg = arg.resolve_as - - size += _calculate_alloc_size(arg, is_member=True) - - return size - - raise NotImplementedError(_calculate_alloc_size, typ) - -def _calculate_member_offset(struct_type3: type3types.StructType3, member: str) -> int: - result = 0 - - for mem, memtyp in struct_type3.members.items(): - if member == mem: - return result - - result += _calculate_alloc_size(memtyp, is_member=True) - - raise Exception(f'{member} not in {struct_type3}') diff --git a/phasm/ourlang.py b/phasm/ourlang.py index d602051..eeb1b1e 100644 --- a/phasm/ourlang.py +++ b/phasm/ourlang.py @@ -1,18 +1,17 @@ """ Contains the syntax tree for ourlang """ -from typing import Dict, Iterable, List, Optional, Union - import enum +from typing import Dict, Iterable, List, Optional, Union from typing_extensions import Final +from .type3 import types as type3types +from .type3.types import PlaceholderForType, StructType3, Type3, Type3OrPlaceholder + WEBASSEMBLY_BUILTIN_FLOAT_OPS: Final = ('abs', 'sqrt', 'ceil', 'floor', 'trunc', 'nearest', ) WEBASSEMBLY_BUILTIN_BYTES_OPS: Final = ('len', ) -from .type3 import types as type3types -from .type3.types import Type3, Type3OrPlaceholder, PlaceholderForType, StructType3 - class Expression: """ An expression within a statement diff --git a/phasm/parser.py b/phasm/parser.py index e3799ab..0aaa2b6 100644 --- a/phasm/parser.py +++ b/phasm/parser.py @@ -1,37 +1,39 @@ """ Parses the source code from the plain text into a syntax tree """ -from typing import Any, Dict, List, NoReturn, Union - import ast - -from .type3 import types as type3types +from typing import Any, Dict, NoReturn, Union from .exceptions import StaticError from .ourlang import ( WEBASSEMBLY_BUILTIN_FLOAT_OPS, - - Module, ModuleDataBlock, - Function, - - Expression, + AccessStructMember, BinaryOp, - ConstantPrimitive, ConstantMemoryStored, - ConstantBytes, ConstantTuple, ConstantStruct, - TupleInstantiation, - - FunctionCall, AccessStructMember, Subscript, - StructDefinition, StructConstructor, - UnaryOp, VariableReference, - + ConstantBytes, + ConstantPrimitive, + ConstantStruct, + ConstantTuple, + Expression, Fold, - - Statement, - StatementIf, StatementPass, StatementReturn, - + Function, + FunctionCall, FunctionParam, + Module, ModuleConstantDef, + ModuleDataBlock, + Statement, + StatementIf, + StatementPass, + StatementReturn, + StructConstructor, + StructDefinition, + Subscript, + TupleInstantiation, + UnaryOp, + VariableReference, ) +from .type3 import types as type3types + def phasm_parse(source: str) -> Module: """ @@ -39,11 +41,39 @@ def phasm_parse(source: str) -> Module: """ res = ast.parse(source, '') + res = OptimizerTransformer().visit(res) + our_visitor = OurVisitor() return our_visitor.visit_Module(res) OurLocals = Dict[str, Union[FunctionParam]] # FIXME: Does it become easier if we add ModuleConstantDef to this dict? +class OptimizerTransformer(ast.NodeTransformer): + """ + This class optimizes the Python AST, to prepare it for parsing + by the OurVisitor class below. + """ + def visit_UnaryOp(self, node: ast.UnaryOp) -> Union[ast.UnaryOp, ast.Constant]: + """ + UnaryOp optimizations + + In the given example: + ```py + x = -4 + ``` + Python will parse it as a unary minus operation on the constant four. + For Phasm purposes, this counts as a literal -4. + """ + if ( + isinstance(node.op, (ast.UAdd, ast.USub, )) + and isinstance(node.operand, ast.Constant) + and isinstance(node.operand.value, (int, float, )) + ): + if isinstance(node.op, ast.USub): + node.operand.value = -node.operand.value + return node.operand + return node + class OurVisitor: """ Class to visit a Python syntax tree and create an ourlang syntax tree @@ -52,6 +82,9 @@ class OurVisitor: At some point, we may deviate from Python syntax. If nothing else, we probably won't keep up with the Python syntax changes. + + See OptimizerTransformer for the changes we make after the Python + parsing is done but before the phasm parsing is done. """ # pylint: disable=C0103,C0116,C0301,R0201,R0912 @@ -188,7 +221,7 @@ class OurVisitor: if not isinstance(stmt.target, ast.Name): raise NotImplementedError('Class with default values') - if not stmt.value is None: + if stmt.value is not None: raise NotImplementedError('Class with default values') if stmt.simple != 1: @@ -400,7 +433,7 @@ class OurVisitor: arguments = [ self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_node) for arg_node in node.elts - if isinstance(arg_node, (ast.Constant, ast.Tuple, )) + if isinstance(arg_node, (ast.Constant, ast.Tuple, ast.Call, )) ] if len(arguments) != len(node.elts): @@ -560,7 +593,7 @@ class OurVisitor: if not isinstance(node.func.ctx, ast.Load): _raise_static_error(node.func, 'Must be load context') - if not node.func.id in module.struct_definitions: + if node.func.id not in module.struct_definitions: _raise_static_error(node.func, 'Undefined struct') if node.keywords: @@ -613,8 +646,6 @@ class OurVisitor: _raise_static_error(node, f'Unrecognized type {node.id}') if isinstance(node, ast.Subscript): - if not isinstance(node.value, ast.Name): - _raise_static_error(node, 'Must be name') if isinstance(node.slice, ast.Slice): _raise_static_error(node, 'Must subscript using an index') if not isinstance(node.slice, ast.Constant): @@ -624,9 +655,6 @@ class OurVisitor: if not isinstance(node.ctx, ast.Load): _raise_static_error(node, 'Must be load context') - if node.value.id not in type3types.LOOKUP_TABLE: # FIXME: Tuple of tuples? - _raise_static_error(node, f'Unrecognized type {node.value.id}') - return type3types.AppliedType3( type3types.static_array, [self.visit_type(module, node.value), type3types.IntType3(node.slice.value)], diff --git a/phasm/runtime.py b/phasm/runtime.py new file mode 100644 index 0000000..cded709 --- /dev/null +++ b/phasm/runtime.py @@ -0,0 +1,69 @@ +from .type3 import types as type3types + + +def calculate_alloc_size(typ: type3types.Type3, is_member: bool = False) -> int: + if typ in (type3types.u8, type3types.i8, ): + return 4 # FIXME: We allocate 4 bytes for every u8 since you load them into an i32 + + if typ in (type3types.u32, type3types.i32, type3types.f32, ): + return 4 + + if typ in (type3types.u64, type3types.i64, type3types.f64, ): + return 8 + + if typ == type3types.bytes: + if is_member: + return 4 + + raise NotImplementedError # When does this happen? + + if isinstance(typ, type3types.StructType3): + if is_member: + # Structs referred to by other structs or tuples are pointers + return 4 + + return sum( + calculate_alloc_size(x, is_member=True) + for x in typ.members.values() + ) + + if isinstance(typ, type3types.AppliedType3): + if typ.base is type3types.static_array: + if is_member: + # tuples referred to by other structs or tuples are pointers + return 4 + + assert isinstance(typ.args[0], type3types.Type3) + assert isinstance(typ.args[1], type3types.IntType3) + + return typ.args[1].value * calculate_alloc_size(typ.args[0], is_member=True) + + if typ.base is type3types.tuple: + if is_member: + # tuples referred to by other structs or tuples are pointers + return 4 + + size = 0 + for arg in typ.args: + assert not isinstance(arg, type3types.IntType3) + + if isinstance(arg, type3types.PlaceholderForType): + assert arg.resolve_as is not None + arg = arg.resolve_as + + size += calculate_alloc_size(arg, is_member=True) + + return size + + raise NotImplementedError(calculate_alloc_size, typ) + +def calculate_member_offset(struct_type3: type3types.StructType3, member: str) -> int: + result = 0 + + for mem, memtyp in struct_type3.members.items(): + if member == mem: + return result + + result += calculate_alloc_size(memtyp, is_member=True) + + raise Exception(f'{member} not in {struct_type3}') diff --git a/phasm/stdlib/alloc.py b/phasm/stdlib/alloc.py index 8c5742d..4473d13 100644 --- a/phasm/stdlib/alloc.py +++ b/phasm/stdlib/alloc.py @@ -1,7 +1,8 @@ """ stdlib: Memory allocation """ -from phasm.wasmgenerator import Generator, VarType_i32 as i32, func_wrapper +from phasm.wasmgenerator import Generator, func_wrapper +from phasm.wasmgenerator import VarType_i32 as i32 IDENTIFIER = 0xA1C0 diff --git a/phasm/stdlib/types.py b/phasm/stdlib/types.py index b902642..5166421 100644 --- a/phasm/stdlib/types.py +++ b/phasm/stdlib/types.py @@ -1,9 +1,10 @@ """ stdlib: Standard types that are not wasm primitives """ -from phasm.wasmgenerator import Generator, VarType_i32 as i32, func_wrapper - from phasm.stdlib import alloc +from phasm.wasmgenerator import Generator, func_wrapper +from phasm.wasmgenerator import VarType_i32 as i32 + @func_wrapper() def __alloc_bytes__(g: Generator, length: i32) -> i32: diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index a5017fb..e46b376 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -3,12 +3,12 @@ This module contains possible constraints generated based on the AST These need to be resolved before the program can be compiled. """ -from typing import Dict, Optional, List, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union from .. import ourlang - from . import types + class Error: """ An error returned by the check functions for a contraint @@ -103,7 +103,6 @@ class SameTypeConstraint(ConstraintBase): def check(self) -> CheckResult: known_types: List[types.Type3] = [] placeholders = [] - do_applied_placeholder_check: bool = False for typ in self.type_list: if isinstance(typ, types.IntType3): known_types.append(typ) @@ -115,7 +114,6 @@ class SameTypeConstraint(ConstraintBase): if isinstance(typ, types.AppliedType3): known_types.append(typ) - do_applied_placeholder_check = True continue if isinstance(typ, types.PlaceholderForType): @@ -135,12 +133,30 @@ class SameTypeConstraint(ConstraintBase): first_type = known_types[0] for typ in known_types[1:]: if isinstance(first_type, types.AppliedType3) and isinstance(typ, types.AppliedType3): - if len(first_type.args) != len(typ.args): - return Error('Mismatch between applied types argument count', comment=self.comment) + if first_type.base is types.tuple and typ.base is types.static_array: + # Swap so we can reuse the code below + # Hope that it still gives proper type errors + first_type, typ = typ, first_type + + if first_type.base is types.static_array and typ.base is types.tuple: + assert isinstance(first_type.args[1], types.IntType3) + length = first_type.args[1].value + + if len(typ.args) != length: + return Error('Mismatch between applied types argument count', comment=self.comment) + + for typ_arg in typ.args: + new_constraint_list.append(SameTypeConstraint( + first_type.args[0], typ_arg + )) + continue if first_type.base != typ.base: return Error('Mismatch between applied types base', comment=self.comment) + if len(first_type.args) != len(typ.args): + return Error('Mismatch between applied types argument count', comment=self.comment) + for first_type_arg, typ_arg in zip(first_type.args, typ.args): new_constraint_list.append(SameTypeConstraint( first_type_arg, typ_arg @@ -365,11 +381,11 @@ class LiteralFitsConstraint(ConstraintBase): try: self.literal.value.to_bytes(bts, 'big', signed=sgn) except OverflowError: - return Error(f'Must fit in {bts} byte(s)') # FIXME: Add line information + return Error(f'Must fit in {bts} byte(s)', comment=self.comment) # FIXME: Add line information return None - return Error('Must be integer') # FIXME: Add line information + return Error('Must be integer', comment=self.comment) # FIXME: Add line information if self.type3.name in float_table: _ = float_table[self.type3.name] @@ -379,23 +395,23 @@ class LiteralFitsConstraint(ConstraintBase): return None - return Error('Must be real') # FIXME: Add line information + return Error('Must be real', comment=self.comment) # FIXME: Add line information if self.type3 is types.bytes: if isinstance(self.literal.value, bytes): return None - return Error('Must be bytes') # FIXME: Add line information + return Error('Must be bytes', comment=self.comment) # FIXME: Add line information res: NewConstraintList if isinstance(self.type3, types.AppliedType3): if self.type3.base == types.tuple: if not isinstance(self.literal, ourlang.ConstantTuple): - return Error('Must be tuple') + return Error('Must be tuple', comment=self.comment) if len(self.type3.args) != len(self.literal.value): - return Error('Tuple element count mismatch') + return Error('Tuple element count mismatch', comment=self.comment) res = [] @@ -412,13 +428,13 @@ class LiteralFitsConstraint(ConstraintBase): if self.type3.base == types.static_array: if not isinstance(self.literal, ourlang.ConstantTuple): - return Error('Must be tuple') + return Error('Must be tuple', comment=self.comment) assert 2 == len(self.type3.args) assert isinstance(self.type3.args[1], types.IntType3) if self.type3.args[1].value != len(self.literal.value): - return Error('Member count mismatch') + return Error('Member count mismatch', comment=self.comment) res = [] diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index 134da41..d0db253 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -6,16 +6,16 @@ The constraints solver can then try to resolve all constraints. from typing import Generator, List from .. import ourlang - -from .constraints import ( - Context, - - ConstraintBase, - CastableConstraint, CanBeSubscriptedConstraint, - LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint, -) - from . import types as type3types +from .constraints import ( + CanBeSubscriptedConstraint, + CastableConstraint, + ConstraintBase, + Context, + LiteralFitsConstraint, + MustImplementTypeClassConstraint, + SameTypeConstraint, +) ConstraintGenerator = Generator[ConstraintBase, None, None] @@ -26,7 +26,10 @@ def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase def constant(ctx: Context, inp: ourlang.Constant) -> ConstraintGenerator: if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)): - yield LiteralFitsConstraint(inp.type3, inp) + yield LiteralFitsConstraint( + inp.type3, inp, + comment='The given literal must fit the expected type' + ) return raise NotImplementedError(constant, inp) @@ -135,7 +138,7 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: yield SameTypeConstraint( inp.type3, type3types.AppliedType3(type3types.tuple, r_type), - comment=f'The type of a tuple is a combination of its members' + comment='The type of a tuple is a combination of its members' ) return @@ -175,7 +178,7 @@ def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) yield from expression(ctx, inp.test) yield SameTypeConstraint(inp.test.type3, type3types.bool_, - comment=f'Must pass a boolean expression to if') + comment='Must pass a boolean expression to if') for stmt in inp.statements: yield from statement(ctx, fun, stmt) diff --git a/phasm/type3/entry.py b/phasm/type3/entry.py index c054077..bf6e54a 100644 --- a/phasm/type3/entry.py +++ b/phasm/type3/entry.py @@ -1,14 +1,26 @@ """ Entry point to the type3 system """ -from typing import Any, Dict, List, Set +from typing import Dict, List -from .. import codestyle -from .. import ourlang - -from .constraints import ConstraintBase, Error, RequireTypeSubstitutes, SameTypeConstraint, SubstitutionMap +from .. import codestyle, ourlang +from .constraints import ( + ConstraintBase, + Error, + RequireTypeSubstitutes, + SameTypeConstraint, + SubstitutionMap, +) from .constraintsgenerator import phasm_type3_generate_constraints -from .types import AppliedType3, IntType3, PlaceholderForType, PrimitiveType3, StructType3, Type3, Type3OrPlaceholder +from .types import ( + AppliedType3, + IntType3, + PlaceholderForType, + PrimitiveType3, + StructType3, + Type3, + Type3OrPlaceholder, +) MAX_RESTACK_COUNT = 100 @@ -33,6 +45,8 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: old_constraint_ids = {id(x) for x in constraint_list} old_placeholder_substitutes_len = len(placeholder_substitutes) + back_on_todo_list_count = 0 + new_constraint_list = [] for constraint in constraint_list: check_result = constraint.check() @@ -60,9 +74,7 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: if isinstance(check_result, RequireTypeSubstitutes): new_constraint_list.append(constraint) - if verbose: - print_constraint(placeholder_id_map, constraint) - print('-> Back on the todo list') + back_on_todo_list_count += 1 continue if isinstance(check_result, list): @@ -75,6 +87,9 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: raise NotImplementedError(constraint, check_result) + if verbose and 0 < back_on_todo_list_count: + print(f'{back_on_todo_list_count} constraints skipped for now') + if not new_constraint_list: constraint_list = new_constraint_list break @@ -91,6 +106,10 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: constraint_list = new_constraint_list + if verbose: + print() + print_constraint_list(placeholder_id_map, constraint_list, placeholder_substitutes) + if constraint_list: raise Exception(f'Cannot type this program - tried {MAX_RESTACK_COUNT} iterations') diff --git a/phasm/wasm.py b/phasm/wasm.py index 7c5a982..c473e63 100644 --- a/phasm/wasm.py +++ b/phasm/wasm.py @@ -5,6 +5,7 @@ and being able to conver it to Web Assembly Text Format from typing import Iterable, List, Optional, Tuple + class WatSerializable: """ Mixin for clases that can be serialized as WebAssembly Text diff --git a/phasm/wasmgenerator.py b/phasm/wasmgenerator.py index 48cca1a..3cb55a1 100644 --- a/phasm/wasmgenerator.py +++ b/phasm/wasmgenerator.py @@ -1,9 +1,8 @@ """ Helper functions to generate WASM code by writing Python functions """ -from typing import Any, Callable, Dict, List, Optional, Type - import functools +from typing import Any, Callable, Dict, List, Optional, Type from . import wasm @@ -45,7 +44,7 @@ class Generator_i32i64: self.store = functools.partial(self.generator.add_statement, f'{prefix}.store') def const(self, value: int, comment: Optional[str] = None) -> None: - self.generator.add_statement(f'{self.prefix}.const', f'0x{value:08x}', comment=comment) + self.generator.add_statement(f'{self.prefix}.const', f'{value}', comment=comment) class Generator_i32(Generator_i32i64): def __init__(self, generator: 'Generator') -> None: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..178dff7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.ruff.lint] +select = ["F", "E", "W", "I"] +ignore = ["E501"] diff --git a/requirements.txt b/requirements.txt index 64dce30..f797665 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ mypy==0.991 pygments==2.12.0 -pylint==2.15.9 pytest==7.2.0 pytest-integration==0.2.2 pywasm==1.0.7 pywasm3==0.5.0 +ruff==0.1.5 wasmer==1.1.0 wasmer_compiler_cranelift==1.1.0 wasmtime==3.0.0 diff --git a/tests/integration/constants.py b/tests/integration/constants.py deleted file mode 100644 index c2f8860..0000000 --- a/tests/integration/constants.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Constants for use in the tests -""" - -ALL_INT_TYPES = ['u8', 'u32', 'u64', 'i32', 'i64'] -COMPLETE_INT_TYPES = ['u32', 'u64', 'i32', 'i64'] - -ALL_FLOAT_TYPES = ['f32', 'f64'] -COMPLETE_FLOAT_TYPES = ALL_FLOAT_TYPES - -TYPE_MAP = { - **{x: int for x in ALL_INT_TYPES}, - **{x: float for x in ALL_FLOAT_TYPES}, -} - -COMPLETE_NUMERIC_TYPES = COMPLETE_INT_TYPES + COMPLETE_FLOAT_TYPES diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 3cd682c..db4574f 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -1,13 +1,18 @@ +import struct import sys +from typing import Any, Generator, Iterable, List, TextIO, Union +from phasm import compiler from phasm.codestyle import phasm_render +from phasm.runtime import calculate_alloc_size +from phasm.type3 import types as type3types from . import runners DASHES = '-' * 16 class SuiteResult: - def __init__(self): + def __init__(self) -> None: self.returned_value = None RUNNER_CLASS_MAP = { @@ -21,10 +26,10 @@ class Suite: """ WebAssembly test suite """ - def __init__(self, code_py): + def __init__(self, code_py: str) -> None: self.code_py = code_py - def run_code(self, *args, runtime='pywasm3', func_name='testEntry', imports=None): + def run_code(self, *args: Any, runtime: str = 'pywasm3', func_name: str = 'testEntry', imports: runners.Imports = None) -> Any: """ Compiles the given python code into wasm and then runs it @@ -35,36 +40,69 @@ class Suite: 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.compile_wasm() runner.interpreter_setup() runner.interpreter_load(imports) - write_header(sys.stderr, 'Phasm') - runner.dump_phasm_code(sys.stderr) - write_header(sys.stderr, 'Assembly') - runner.dump_wasm_wat(sys.stderr) - # 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 + + if isinstance(arg_typ, type3types.StructType3): + adr = _allocate_memory_stored_value(runner, arg_typ, arg) wasm_args.append(adr) continue @@ -76,10 +114,334 @@ class Suite: 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, msg: str) -> None: +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 + + 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( + 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) + + 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 + + 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( + runner: runners.RunnerBase, + func_name: str, + wasm_value: Any, + ) -> Any: + ret_type3 = runner.phasm_ast.functions[func_name].returns_type3 + + if ret_type3 is type3types.none: + return None + + if ret_type3 in (type3types.i32, type3types.i64): + assert isinstance(wasm_value, int), wasm_value + return wasm_value + + if ret_type3 in (type3types.u8, type3types.u32, type3types.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 (type3types.f32, type3types.f64, ): + assert isinstance(wasm_value, float), wasm_value + return wasm_value + + if ret_type3 is type3types.bytes: + assert isinstance(wasm_value, int), wasm_value + + return _load_bytes_from_address(runner, ret_type3, wasm_value) + + if isinstance(ret_type3, type3types.AppliedType3): + if ret_type3.base is type3types.static_array: + assert isinstance(wasm_value, int), wasm_value + + return _load_static_array_from_address(runner, ret_type3, wasm_value) + + if ret_type3.base is type3types.tuple: + assert isinstance(wasm_value, int), wasm_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: + if typ is type3types.u8: + # See compiler.py, LOAD_STORE_TYPE_MAP and module_data_u8 + assert len(inp) == 4 + return struct.unpack(' bytes: + 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]: + offset = 0 + for size in split_sizes: + yield all_bytes[offset:offset + size] + 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 + + assert not isinstance(sub_typ, type3types.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: 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 + + typ_list = [ + x + for x in typ.args + if not isinstance(x, type3types.PlaceholderForType) + ] + assert len(typ_list) == len(typ.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 tuple( + _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/runners.py b/tests/integration/runners.py index 91a29fd..f608f96 100644 --- a/tests/integration/runners.py +++ b/tests/integration/runners.py @@ -1,21 +1,21 @@ """ Runners to help run WebAssembly code on various interpreters """ -from typing import Any, Callable, Dict, Iterable, Optional, TextIO - import ctypes import io +from typing import Any, Callable, Dict, Iterable, Optional, TextIO import pywasm.binary import wasm3 import wasmer import wasmtime +from phasm import ourlang, wasm from phasm.compiler import phasm_compile from phasm.parser import phasm_parse from phasm.type3.entry import phasm_type3 -from phasm import ourlang -from phasm import wasm + +Imports = Optional[Dict[str, Callable[[Any], Any]]] class RunnerBase: """ @@ -73,7 +73,7 @@ class RunnerBase: """ raise NotImplementedError - def interpreter_load(self, imports: Optional[Dict[str, Callable[[Any], Any]]] = None) -> None: + def interpreter_load(self, imports: Imports = None) -> None: """ Loads the code into the interpreter """ @@ -166,7 +166,7 @@ class RunnerPywasm3(RunnerBase): def interpreter_read_memory(self, offset: int, length: int) -> bytes: memory = self.rtime.get_memory(0) - return memory[offset:length].tobytes() + return memory[offset:offset + length].tobytes() def interpreter_dump_memory(self, textio: TextIO) -> None: _dump_memory(textio, self.rtime.get_memory(0)) diff --git a/tests/integration/test_examples/test_buffer.py b/tests/integration/test_examples/test_buffer.py index b987c69..78622ea 100644 --- a/tests/integration/test_examples/test_buffer.py +++ b/tests/integration/test_examples/test_buffer.py @@ -2,6 +2,7 @@ import pytest from ..helpers import Suite + @pytest.mark.slow_integration_test def test_index(): with open('examples/buffer.py', 'r', encoding='ASCII') as fil: diff --git a/tests/integration/test_examples/test_crc32.py b/tests/integration/test_examples/test_crc32.py index d9f74bf..f768d6f 100644 --- a/tests/integration/test_examples/test_crc32.py +++ b/tests/integration/test_examples/test_crc32.py @@ -1,10 +1,10 @@ import binascii -import struct import pytest from ..helpers import Suite + @pytest.mark.slow_integration_test def test_crc32(): # FIXME: Stub @@ -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_examples/test_fib.py b/tests/integration/test_examples/test_fib.py index 9474a20..0e25ea3 100644 --- a/tests/integration/test_examples/test_fib.py +++ b/tests/integration/test_examples/test_fib.py @@ -2,6 +2,7 @@ import pytest from ..helpers import Suite + @pytest.mark.slow_integration_test def test_fib(): with open('./examples/fib.py', 'r', encoding='UTF-8') as fil: diff --git a/tests/integration/test_lang/generator.md b/tests/integration/test_lang/generator.md new file mode 100644 index 0000000..b4a4e48 --- /dev/null +++ b/tests/integration/test_lang/generator.md @@ -0,0 +1,358 @@ +# runtime_extract_value_literal + +As a developer +I want to extract a $TYPE value +In order get the result I calculated with my phasm code + +```py +@exported +def testEntry() -> $TYPE: + return $VAL0 +``` + +```py +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 +I want to define $TYPE module constants +In order to make hardcoded values more visible +and to make it easier to change hardcoded values + +```py +CONSTANT: $TYPE = $VAL0 + +@exported +def testEntry() -> i32: + return 9 +``` + +```py +expect(9) +``` + +# module_constant_def_bad + +As a developer +I want to receive a type error on an invalid assignment on a $TYPE module constant +In order to make debugging easier + +```py +CONSTANT: (u32, ) = $VAL0 +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + 'Tuple element count mismatch', + 'The given literal must fit the expected type', + ) +else: + expect_type_error( + 'Must be tuple', + 'The given literal must fit the expected type', + ) +``` + +# function_result_is_literal_ok + +As a developer +I want to use return a literal from a function +In order to define constants in a more dynamic way + +```py +def drop_arg_return_9(x: $TYPE) -> i32: + return 9 + +def constant() -> $TYPE: + return $VAL0 + +@exported +def testEntry() -> i32: + return drop_arg_return_9(constant()) +``` + +```py +expect(9) +``` + +# function_result_is_literal_bad + +As a developer +I want to receive a type error when returning a $TYPE literal for a function that doesn't return that type +In order to make debugging easier + +```py +def drop_arg_return_9(x: (u32, )) -> i32: + return 9 + +def constant() -> (u32, ): + return $VAL0 + +@exported +def testEntry() -> i32: + return drop_arg_return_9(constant()) +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + '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', + 'The given literal must fit the expected type', + ) +``` + +# function_result_is_module_constant_ok + +As a developer +I want to use return a $TYPE module constant from a function +In order to use my module constants in return statements + +```py +CONSTANT: $TYPE = $VAL0 + +def helper(x: $TYPE) -> i32: + return 9 + +def constant() -> $TYPE: + return CONSTANT + +@exported +def testEntry() -> i32: + return helper(constant()) +``` + +```py +expect(9) +``` + +# function_result_is_module_constant_bad + +As a developer +I want to receive a type error when returning a $TYPE module constant for a function that doesn't return that type +In order to make debugging easier + +```py +CONSTANT: $TYPE = $VAL0 + +def drop_arg_return_9(x: (u32, )) -> i32: + return 9 + +def constant() -> (u32, ): + return CONSTANT + +@exported +def testEntry() -> i32: + return drop_arg_return_9(constant()) +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + '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', + 'The type of the value returned from function constant should match its return type', + ) +``` + +# function_result_is_arg_ok + +As a developer +I want to use return a $TYPE function argument +In order to make it possible to select a value using a function + +```py +CONSTANT: $TYPE = $VAL0 + +def drop_arg_return_9(x: $TYPE) -> i32: + return 9 + +def select(x: $TYPE) -> $TYPE: + return x + +@exported +def testEntry() -> i32: + return drop_arg_return_9(select(CONSTANT)) +``` + +```py +expect(9) +``` + +# function_result_is_arg_bad + +As a developer +I want to receive a type error when returning a $TYPE argument for a function that doesn't return that type +In order to make debugging easier + +```py +def drop_arg_return_9(x: (u32, )) -> i32: + return 9 + +def select(x: $TYPE) -> (u32, ): + return x +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + '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', + 'The type of the value returned from function select should match its return type', + ) +``` + +# function_arg_literal_ok + +As a developer +I want to use a $TYPE literal by passing it to a function +In order to use a pre-existing function with the values I specify + +```py +def helper(x: $TYPE) -> i32: + return 9 + +@exported +def testEntry() -> i32: + return helper($VAL0) +``` + +```py +expect(9) +``` + +# function_arg_literal_bad + +As a developer +I want to receive a type error when passing a $TYPE literal to a function that does not accept it +In order to make debugging easier + +```py +def helper(x: (u32, )) -> i32: + return 9 + +@exported +def testEntry() -> i32: + return helper($VAL0) +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + 'Mismatch between applied types argument count', + # 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', + 'The given literal must fit the expected type', + ) +``` + +# function_arg_module_constant_def_ok + +As a developer +I want to use a $TYPE module constant by passing it to a function +In order to use my defined value with a pre-existing function + +```py +CONSTANT: $TYPE = $VAL0 + +def helper(x: $TYPE) -> i32: + return 9 + +@exported +def testEntry() -> i32: + return helper(CONSTANT) +``` + +```py +expect(9) +``` + +# function_arg_module_constant_def_bad + +As a developer +I want to receive a type error when passing a $TYPE module constant to a function that does not accept it +In order to make debugging easier + +```py +CONSTANT: $TYPE = $VAL0 + +def helper(x: (u32, )) -> i32: + return 9 + +@exported +def testEntry() -> i32: + return helper(CONSTANT) +``` + +```py +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): + expect_type_error( + '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', + 'The type of the value passed to argument x of function helper should match the type of that argument', + ) +``` diff --git a/tests/integration/test_lang/generator.py b/tests/integration/test_lang/generator.py new file mode 100644 index 0000000..771f363 --- /dev/null +++ b/tests/integration/test_lang/generator.py @@ -0,0 +1,154 @@ +import functools +import json +import sys +from typing import Any + +import marko +import marko.md_renderer + + +def get_tests(template): + test_data = None + for el in template.children: + if isinstance(el, marko.block.BlankLine): + continue + + if isinstance(el, marko.block.Heading): + if test_data is not None: + yield test_data + + test_data = [] + test_data.append(el) + continue + + if test_data is not None: + test_data.append(el) + + if test_data is not None: + yield test_data + +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 + +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): + result.append('with pytest.raises(Type3Exception) as exc_info:') + result.append(' Suite(code_py).run_code()') + 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_or_tuple_values_fix(inp: Any): + if isinstance(inp, (int, float, )): + return inp + + if isinstance(inp, str): + if inp.startswith('bytes:'): + return inp[6:].encode() + return inp + + if isinstance(inp, list): + return tuple(map(json_does_not_support_byte_or_tuple_values_fix, inp)) + + if isinstance(inp, dict): + return { + key: json_does_not_support_byte_or_tuple_values_fix(val) + for key, val in inp.items() + } + + raise NotImplementedError(inp) + +def generate_assertions(settings, result_code): + result = [] + + locals_ = { + '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(json_does_not_support_byte_or_tuple_values_fix(settings['PYTHON'])) + + if 'VAL0' not in locals_: + locals_['VAL0'] = eval(settings['VAL0']) + + exec(result_code, {}, locals_) + + return ' ' + '\n '.join(result) + '\n' + +def generate_code(markdown, template, settings): + type_name = settings['TYPE_NAME'] + + print('"""') + print('AUTO GENERATED') + print() + print('TEMPLATE:', sys.argv[1]) + print('SETTINGS:', sys.argv[2]) + print('"""') + print('import pytest') + print() + print('from phasm.type3.entry import Type3Exception') + print() + print('from ..helpers import Suite') + print() + + for test in get_tests(template): + assert len(test) == 4, test + heading, paragraph, code_block1, code_block2 = test + + assert isinstance(heading, marko.block.Heading) + assert isinstance(paragraph, marko.block.Paragraph) + assert isinstance(code_block1, marko.block.FencedCode) + assert isinstance(code_block2, marko.block.FencedCode) + + test_id = apply_settings(settings, heading.children[0].children) + user_story = apply_settings(settings, markdown.renderer.render(paragraph)) + inp_code = apply_settings(settings, code_block1.children[0].children) + + result_code = markdown.renderer.render_children(code_block2) + + print('@pytest.mark.integration_test') + print(f'def test_{type_name}_{test_id}():') + print(' """') + 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() + +def main(): + markdown = marko.Markdown( + renderer=marko.md_renderer.MarkdownRenderer, + ) + with open(sys.argv[1], 'r', encoding='utf-8') as fil: + template = markdown.parse(fil.read()) + + with open(sys.argv[2], 'r', encoding='utf-8') as fil: + settings = json.load(fil) + + if 'TYPE_NAME' not in settings: + settings['TYPE_NAME'] = settings['TYPE'] + + generate_code(markdown, template, settings) + +if __name__ == '__main__': + main() diff --git a/tests/integration/test_lang/generator_bytes.json b/tests/integration/test_lang/generator_bytes.json new file mode 100644 index 0000000..bde3ec8 --- /dev/null +++ b/tests/integration/test_lang/generator_bytes.json @@ -0,0 +1,4 @@ +{ + "TYPE": "bytes", + "VAL0": "b'ABCDEFG'" +} diff --git a/tests/integration/test_lang/generator_f32.json b/tests/integration/test_lang/generator_f32.json new file mode 100644 index 0000000..2fbc930 --- /dev/null +++ b/tests/integration/test_lang/generator_f32.json @@ -0,0 +1,4 @@ +{ + "TYPE": "f32", + "VAL0": "1000000.125" +} diff --git a/tests/integration/test_lang/generator_f64.json b/tests/integration/test_lang/generator_f64.json new file mode 100644 index 0000000..f57ecea --- /dev/null +++ b/tests/integration/test_lang/generator_f64.json @@ -0,0 +1,4 @@ +{ + "TYPE": "f64", + "VAL0": "1000000.125" +} diff --git a/tests/integration/test_lang/generator_i32.json b/tests/integration/test_lang/generator_i32.json new file mode 100644 index 0000000..14e1b83 --- /dev/null +++ b/tests/integration/test_lang/generator_i32.json @@ -0,0 +1,4 @@ +{ + "TYPE": "i32", + "VAL0": "1000000" +} diff --git a/tests/integration/test_lang/generator_i64.json b/tests/integration/test_lang/generator_i64.json new file mode 100644 index 0000000..a4d6439 --- /dev/null +++ b/tests/integration/test_lang/generator_i64.json @@ -0,0 +1,4 @@ +{ + "TYPE": "i64", + "VAL0": "1000000" +} diff --git a/tests/integration/test_lang/generator_static_array_tuple_u32_u32_3.json b/tests/integration/test_lang/generator_static_array_tuple_u32_u32_3.json new file mode 100644 index 0000000..284ead9 --- /dev/null +++ b/tests/integration/test_lang/generator_static_array_tuple_u32_u32_3.json @@ -0,0 +1,5 @@ +{ + "TYPE_NAME": "static_array_tuple_u32_u32_3", + "TYPE": "(u32, u32, )[3]", + "VAL0": "((1, 100, ), (2, 200, ), (3, 300, ), )" +} diff --git a/tests/integration/test_lang/generator_static_array_u64_32.json b/tests/integration/test_lang/generator_static_array_u64_32.json new file mode 100644 index 0000000..5d65de6 --- /dev/null +++ b/tests/integration/test_lang/generator_static_array_u64_32.json @@ -0,0 +1,5 @@ +{ + "TYPE_NAME": "static_array_u64_32", + "TYPE": "u64[32]", + "VAL0": "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, )" +} diff --git a/tests/integration/test_lang/generator_static_array_with_structs.json b/tests/integration/test_lang/generator_static_array_with_structs.json new file mode 100644 index 0000000..3fff78c --- /dev/null +++ b/tests/integration/test_lang/generator_static_array_with_structs.json @@ -0,0 +1,33 @@ +{ + "TYPE_NAME": "static_array_with_structs", + "TYPE": "StructMain[3]", + "VAL0": "(StructMain(1, (StructCode(-4), 4, 4.0, ), (StructCode(-1), StructCode(-2), )), StructMain(2, (StructCode(-16), 16, 16.0, ), (StructCode(3), StructCode(14), )), StructMain(3, (StructCode(-256), 256, 256.0, ), (StructCode(-9), StructCode(-98), )), )", + "CODE_HEADER": [ + "class StructCode:", + " code: i32", + "", + "class StructMain:", + " val00: u8", + " val01: (StructCode, u64, f32, )", + " val02: StructCode[2]" + ], + "PYTHON": { + "VAL0": [ + { + "val00": 1, + "val01": [{"code": -4}, 4, 4.0], + "val02": [{"code": -1}, {"code": -2}] + }, + { + "val00": 2, + "val01": [{"code": -16}, 16, 16.0], + "val02": [{"code": 3}, {"code": 14}] + }, + { + "val00": 3, + "val01": [{"code": -256}, 256, 256.0], + "val02": [{"code": -9}, {"code": -98}] + } + ] + } +} 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} + } +} diff --git a/tests/integration/test_lang/generator_tuple_all_primitives.json b/tests/integration/test_lang/generator_tuple_all_primitives.json new file mode 100644 index 0000000..e391d9b --- /dev/null +++ b/tests/integration/test_lang/generator_tuple_all_primitives.json @@ -0,0 +1,5 @@ +{ + "TYPE_NAME": "tuple_all_primitives", + "TYPE": "(u8, u32, u64, i8, i8, i32, i32, i64, i64, f32, f32, f64, f64, bytes, )", + "VAL0": "(1, 4, 8, 1, -1, 4, -4, 8, -8, 125.125, -125.125, 5000.5, -5000.5, b'Hello, world!', )" +} diff --git a/tests/integration/test_lang/generator_tuple_nested.json b/tests/integration/test_lang/generator_tuple_nested.json new file mode 100644 index 0000000..8d966b4 --- /dev/null +++ b/tests/integration/test_lang/generator_tuple_nested.json @@ -0,0 +1,5 @@ +{ + "TYPE_NAME": "tuple_nested", + "TYPE": "(u64, (u32, bytes, u32, ), (u8, u32[3], u8, ), )", + "VAL0": "(1000000, (1, b'test', 2, ), (1, (4, 4, 4, ), 1, ), )" +} diff --git a/tests/integration/test_lang/generator_tuple_u64_u32_u8.json b/tests/integration/test_lang/generator_tuple_u64_u32_u8.json new file mode 100644 index 0000000..bf404a0 --- /dev/null +++ b/tests/integration/test_lang/generator_tuple_u64_u32_u8.json @@ -0,0 +1,5 @@ +{ + "TYPE_NAME": "tuple_u64_u32_u8", + "TYPE": "(u64, u32, u8, )", + "VAL0": "(1000000, 1000, 1, )" +} diff --git a/tests/integration/test_lang/generator_u32.json b/tests/integration/test_lang/generator_u32.json new file mode 100644 index 0000000..5843ed9 --- /dev/null +++ b/tests/integration/test_lang/generator_u32.json @@ -0,0 +1,4 @@ +{ + "TYPE": "u32", + "VAL0": "1000000" +} diff --git a/tests/integration/test_lang/generator_u64.json b/tests/integration/test_lang/generator_u64.json new file mode 100644 index 0000000..174ca14 --- /dev/null +++ b/tests/integration/test_lang/generator_u64.json @@ -0,0 +1,4 @@ +{ + "TYPE": "u64", + "VAL0": "1000000" +} diff --git a/tests/integration/test_lang/test_bits.py b/tests/integration/test_lang/test_bits.py new file mode 100644 index 0000000..e873ee6 --- /dev/null +++ b/tests/integration/test_lang/test_bits.py @@ -0,0 +1,115 @@ +import pytest + +from phasm.type3.entry import Type3Exception + +from ..helpers import Suite + + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['u32', 'u64']) # FIXME: Support u8, requires an extra AND operation +def test_logical_left_shift(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 << 3 +""" + + result = Suite(code_py).run_code() + + assert 80 == result.returned_value + assert isinstance(result.returned_value, int) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['u32', 'u64']) +def test_logical_right_shift_left_bit_zero(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 >> 3 +""" + + # Check with wasmtime, as other engines don't mind if the type + # doesn't match. They'll complain when: (>>) : u32 -> u64 -> u32 + result = Suite(code_py).run_code(runtime='wasmtime') + + assert 1 == result.returned_value + assert isinstance(result.returned_value, int) + +@pytest.mark.integration_test +def test_logical_right_shift_left_bit_one(): + code_py = """ +@exported +def testEntry() -> u32: + return 4294967295 >> 16 +""" + + result = Suite(code_py).run_code() + + assert 0xFFFF == result.returned_value + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) +def test_bitwise_or_uint(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 | 3 +""" + + result = Suite(code_py).run_code() + + assert 11 == result.returned_value + assert isinstance(result.returned_value, int) + +@pytest.mark.integration_test +def test_bitwise_or_inv_type(): + code_py = """ +@exported +def testEntry() -> f64: + return 10.0 | 3.0 +""" + + with pytest.raises(Type3Exception, match='f64 does not implement the BitWiseOperation type class'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +def test_bitwise_or_type_mismatch(): + code_py = """ +CONSTANT1: u32 = 3 +CONSTANT2: u64 = 3 + +@exported +def testEntry() -> u64: + return CONSTANT1 | CONSTANT2 +""" + + with pytest.raises(Type3Exception, match='u64 must be u32 instead'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) +def test_bitwise_xor(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 ^ 3 +""" + + result = Suite(code_py).run_code() + + assert 9 == result.returned_value + assert isinstance(result.returned_value, int) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) +def test_bitwise_and(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 & 3 +""" + + result = Suite(code_py).run_code() + + assert 2 == result.returned_value + assert isinstance(result.returned_value, int) diff --git a/tests/integration/test_lang/test_builtins.py b/tests/integration/test_lang/test_builtins.py index 2b06afd..756e679 100644 --- a/tests/integration/test_lang/test_builtins.py +++ b/tests/integration/test_lang/test_builtins.py @@ -5,6 +5,7 @@ import pytest from ..helpers import Suite, write_header from ..runners import RunnerPywasm + def setup_interpreter(phash_code: str) -> RunnerPywasm: runner = RunnerPywasm(phash_code) diff --git a/tests/integration/test_lang/test_bytes.py b/tests/integration/test_lang/test_bytes.py index 6a3fe07..3e72c40 100644 --- a/tests/integration/test_lang/test_bytes.py +++ b/tests/integration/test_lang/test_bytes.py @@ -4,19 +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(): @@ -31,7 +18,7 @@ def testEntry(f: bytes) -> u32: assert 24 == result.returned_value @pytest.mark.integration_test -def test_bytes_index(): +def test_bytes_index_ok(): code_py = """ @exported def testEntry(f: bytes) -> u8: @@ -42,25 +29,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] """ @@ -69,30 +42,12 @@ def testEntry(f: bytes) -> u8: assert 0 == result.returned_value @pytest.mark.integration_test -def test_function_call_element_ok(): - code_py = """ -@exported -def testEntry(f: bytes) -> u8: - return helper(f[0]) - -def helper(x: u8) -> u8: - return x -""" - - result = Suite(code_py).run_code(b'Short') - - assert 83 == result.returned_value - -@pytest.mark.integration_test -def test_function_call_element_type_mismatch(): +def test_bytes_index_invalid_type(): code_py = """ @exported def testEntry(f: bytes) -> u64: - return helper(f[0]) - -def helper(x: u64) -> u64: - return x + return f[50] """ with pytest.raises(Type3Exception, match=r'u64 must be u8 instead'): - Suite(code_py).run_code() + Suite(code_py).run_code(b'Short') diff --git a/tests/integration/test_lang/test_floating.py b/tests/integration/test_lang/test_floating.py new file mode 100644 index 0000000..79ce4d1 --- /dev/null +++ b/tests/integration/test_lang/test_floating.py @@ -0,0 +1,18 @@ +import pytest + +from ..helpers import Suite + + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', ['f32', 'f64']) +def test_builtins_sqrt(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return sqrt(25.0) +""" + + result = Suite(code_py).run_code() + + assert 5 == result.returned_value + assert isinstance(result.returned_value, float) diff --git a/tests/integration/test_lang/test_fractional.py b/tests/integration/test_lang/test_fractional.py new file mode 100644 index 0000000..3ad8a41 --- /dev/null +++ b/tests/integration/test_lang/test_fractional.py @@ -0,0 +1,33 @@ +import pytest + +from ..helpers import Suite + +TYPE_LIST = ['f32', 'f64'] + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', TYPE_LIST) +def test_division_float(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10.0 / 8.0 +""" + + result = Suite(code_py).run_code() + + assert 1.25 == result.returned_value + assert isinstance(result.returned_value, float) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', TYPE_LIST) +def test_division_zero_let_it_crash_float(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10.0 / 0.0 +""" + + # WebAssembly dictates that float division follows the IEEE rules + # https://www.w3.org/TR/wasm-core-1/#-hrefop-fdivmathrmfdiv_n-z_1-z_2 + result = Suite(code_py).run_code() + assert float('+inf') == result.returned_value diff --git a/tests/integration/test_lang/test_function_calls.py b/tests/integration/test_lang/test_function_calls.py new file mode 100644 index 0000000..aab2fe2 --- /dev/null +++ b/tests/integration/test_lang/test_function_calls.py @@ -0,0 +1,34 @@ +import pytest + +from ..helpers import Suite + + +@pytest.mark.integration_test +def test_call_pre_defined(): + code_py = """ +def helper(left: i32) -> i32: + return left + +@exported +def testEntry() -> i32: + return helper(13) +""" + + result = Suite(code_py).run_code() + + assert 13 == result.returned_value + +@pytest.mark.integration_test +def test_call_post_defined(): + code_py = """ +@exported +def testEntry() -> i32: + return helper(10, 3) + +def helper(left: i32, right: i32) -> i32: + return left - right +""" + + result = Suite(code_py).run_code() + + assert 7 == result.returned_value diff --git a/tests/integration/test_lang/test_if.py b/tests/integration/test_lang/test_if.py index 5d77eb9..ff37761 100644 --- a/tests/integration/test_lang/test_if.py +++ b/tests/integration/test_lang/test_if.py @@ -2,6 +2,7 @@ import pytest from ..helpers import Suite + @pytest.mark.integration_test @pytest.mark.parametrize('inp', [9, 10, 11, 12]) def test_if_simple(inp): diff --git a/tests/integration/test_lang/test_interface.py b/tests/integration/test_lang/test_imports.py similarity index 99% rename from tests/integration/test_lang/test_interface.py rename to tests/integration/test_lang/test_imports.py index a8dfc32..1762a0b 100644 --- a/tests/integration/test_lang/test_interface.py +++ b/tests/integration/test_lang/test_imports.py @@ -4,6 +4,7 @@ from phasm.type3.entry import Type3Exception from ..helpers import Suite + @pytest.mark.integration_test def test_imported_ok(): code_py = """ diff --git a/tests/integration/test_lang/test_integral.py b/tests/integration/test_lang/test_integral.py new file mode 100644 index 0000000..63486da --- /dev/null +++ b/tests/integration/test_lang/test_integral.py @@ -0,0 +1,33 @@ +import pytest + +from ..helpers import Suite + +TYPE_LIST = ['u32', 'u64', 'i32', 'i64'] + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', TYPE_LIST) +def test_division_int(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 / 3 +""" + + result = Suite(code_py).run_code() + + assert 3 == result.returned_value + assert isinstance(result.returned_value, int) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', TYPE_LIST) +def test_division_zero_let_it_crash_int(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 / 0 +""" + + # WebAssembly dictates that integer division is a partial operator (e.g. unreachable for 0) + # https://www.w3.org/TR/wasm-core-1/#-hrefop-idiv-umathrmidiv_u_n-i_1-i_2 + with pytest.raises(Exception): + Suite(code_py).run_code() diff --git a/tests/integration/test_lang/test_literals.py b/tests/integration/test_lang/test_literals.py new file mode 100644 index 0000000..42484cd --- /dev/null +++ b/tests/integration/test_lang/test_literals.py @@ -0,0 +1,17 @@ +import pytest + +from phasm.type3.entry import Type3Exception + +from ..helpers import Suite + + +@pytest.mark.integration_test +def test_expr_constant_literal_does_not_fit(): + code_py = """ +@exported +def testEntry() -> u8: + return 1000 +""" + + with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): + Suite(code_py).run_code() diff --git a/tests/integration/test_lang/test_num.py b/tests/integration/test_lang/test_num.py new file mode 100644 index 0000000..d2db0cf --- /dev/null +++ b/tests/integration/test_lang/test_num.py @@ -0,0 +1,121 @@ +import pytest + +from ..helpers import Suite + +INT_TYPES = ['u32', 'u64', 'i32', 'i64'] +FLOAT_TYPES = ['f32', 'f64'] + +TYPE_MAP = { + 'u32': int, + 'u64': int, + 'i32': int, + 'i64': int, + 'f32': float, + 'f64': float, +} + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', INT_TYPES) +def test_addition_int(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 + 3 +""" + + result = Suite(code_py).run_code() + + assert 13 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', FLOAT_TYPES) +def test_addition_float(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 32.0 + 0.125 +""" + + result = Suite(code_py).run_code() + + assert 32.125 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', INT_TYPES) +def test_subtraction_int(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 - 3 +""" + + result = Suite(code_py).run_code() + + assert 7 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', FLOAT_TYPES) +def test_subtraction_float(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 100.0 - 67.875 +""" + + result = Suite(code_py).run_code() + + assert 32.125 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) + +@pytest.mark.integration_test +@pytest.mark.skip('TODO: Runtimes return a signed value, which is difficult to test') +@pytest.mark.parametrize('type_', ('u32', 'u64')) # FIXME: u8 +def test_subtraction_underflow(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return 10 - 11 +""" + + result = Suite(code_py).run_code() + + assert 0 < result.returned_value + +# TODO: Multiplication + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', INT_TYPES) +def test_call_with_expression_int(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return helper(10 + 20, 3 + 5) + +def helper(left: {type_}, right: {type_}) -> {type_}: + return left - right +""" + + result = Suite(code_py).run_code() + + assert 22 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_', FLOAT_TYPES) +def test_call_with_expression_float(type_): + code_py = f""" +@exported +def testEntry() -> {type_}: + return helper(10.078125 + 90.046875, 63.0 + 5.0) + +def helper(left: {type_}, right: {type_}) -> {type_}: + return left - right +""" + + result = Suite(code_py).run_code() + + assert 32.125 == result.returned_value + assert TYPE_MAP[type_] == type(result.returned_value) diff --git a/tests/integration/test_lang/test_primitives.py b/tests/integration/test_lang/test_primitives.py deleted file mode 100644 index 41a8211..0000000 --- a/tests/integration/test_lang/test_primitives.py +++ /dev/null @@ -1,486 +0,0 @@ -import pytest - -from phasm.type3.entry import Type3Exception - -from ..helpers import Suite -from ..constants import ALL_INT_TYPES, ALL_FLOAT_TYPES, COMPLETE_INT_TYPES, TYPE_MAP - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_INT_TYPES) -def test_expr_constant_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 13 -""" - - result = Suite(code_py).run_code() - - assert 13 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_expr_constant_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 32.125 -""" - - result = Suite(code_py).run_code() - - assert 32.125 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -def test_expr_constant_literal_does_not_fit(): - code_py = """ -@exported -def testEntry() -> u8: - return 1000 -""" - - with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_INT_TYPES) -def test_module_constant_int(type_): - code_py = f""" -CONSTANT: {type_} = 13 - -@exported -def testEntry() -> {type_}: - return CONSTANT -""" - - result = Suite(code_py).run_code() - - assert 13 == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_module_constant_float(type_): - code_py = f""" -CONSTANT: {type_} = 32.125 - -@exported -def testEntry() -> {type_}: - return CONSTANT -""" - - result = Suite(code_py).run_code() - - assert 32.125 == result.returned_value - -@pytest.mark.integration_test -def test_module_constant_type_failure(): - code_py = """ -CONSTANT: u8 = 1000 - -@exported -def testEntry() -> u32: - return 14 -""" - - with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u32', 'u64']) # FIXME: Support u8, requires an extra AND operation -def test_logical_left_shift(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 << 3 -""" - - result = Suite(code_py).run_code() - - assert 80 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u32', 'u64']) -def test_logical_right_shift_left_bit_zero(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 >> 3 -""" - - # Check with wasmtime, as other engines don't mind if the type - # doesn't match. They'll complain when: (>>) : u32 -> u64 -> u32 - result = Suite(code_py).run_code(runtime='wasmtime') - - assert 1 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -def test_logical_right_shift_left_bit_one(): - code_py = """ -@exported -def testEntry() -> u32: - return 4294967295 >> 16 -""" - - result = Suite(code_py).run_code() - - assert 0xFFFF == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) -def test_bitwise_or_uint(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 | 3 -""" - - result = Suite(code_py).run_code() - - assert 11 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -def test_bitwise_or_inv_type(): - code_py = """ -@exported -def testEntry() -> f64: - return 10.0 | 3.0 -""" - - with pytest.raises(Type3Exception, match='f64 does not implement the BitWiseOperation type class'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_bitwise_or_type_mismatch(): - code_py = """ -CONSTANT1: u32 = 3 -CONSTANT2: u64 = 3 - -@exported -def testEntry() -> u64: - return CONSTANT1 | CONSTANT2 -""" - - with pytest.raises(Type3Exception, match='u64 must be u32 instead'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) -def test_bitwise_xor(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 ^ 3 -""" - - result = Suite(code_py).run_code() - - assert 9 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) -def test_bitwise_and(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 & 3 -""" - - result = Suite(code_py).run_code() - - assert 2 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_addition_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 + 3 -""" - - result = Suite(code_py).run_code() - - assert 13 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_addition_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 32.0 + 0.125 -""" - - result = Suite(code_py).run_code() - - assert 32.125 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_subtraction_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 - 3 -""" - - result = Suite(code_py).run_code() - - assert 7 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_subtraction_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 100.0 - 67.875 -""" - - result = Suite(code_py).run_code() - - assert 32.125 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.skip('TODO: Runtimes return a signed value, which is difficult to test') -@pytest.mark.parametrize('type_', ('u32', 'u64')) # FIXME: u8 -def test_subtraction_underflow(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 - 11 -""" - - result = Suite(code_py).run_code() - - assert 0 < result.returned_value - -# TODO: Multiplication - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_division_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 / 3 -""" - - result = Suite(code_py).run_code() - - assert 3 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_division_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10.0 / 8.0 -""" - - result = Suite(code_py).run_code() - - assert 1.25 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_division_zero_let_it_crash_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10 / 0 -""" - - # WebAssembly dictates that integer division is a partial operator (e.g. unreachable for 0) - # https://www.w3.org/TR/wasm-core-1/#-hrefop-idiv-umathrmidiv_u_n-i_1-i_2 - with pytest.raises(Exception): - Suite(code_py).run_code() - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_division_zero_let_it_crash_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return 10.0 / 0.0 -""" - - # WebAssembly dictates that float division follows the IEEE rules - # https://www.w3.org/TR/wasm-core-1/#-hrefop-fdivmathrmfdiv_n-z_1-z_2 - result = Suite(code_py).run_code() - assert float('+inf') == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['f32', 'f64']) -def test_builtins_sqrt(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return sqrt(25.0) -""" - - result = Suite(code_py).run_code() - - assert 5 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', TYPE_MAP.keys()) -def test_function_argument(type_): - code_py = f""" -@exported -def testEntry(a: {type_}) -> {type_}: - return a -""" - - result = Suite(code_py).run_code(125) - - assert 125 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.skip('TODO') -def test_explicit_positive_number(): - code_py = """ -@exported -def testEntry() -> i32: - return +523 -""" - - result = Suite(code_py).run_code() - - assert 523 == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.skip('TODO') -def test_explicit_negative_number(): - code_py = """ -@exported -def testEntry() -> i32: - return -19 -""" - - result = Suite(code_py).run_code() - - assert -19 == result.returned_value - -@pytest.mark.integration_test -def test_call_no_args(): - code_py = """ -def helper() -> i32: - return 19 - -@exported -def testEntry() -> i32: - return helper() -""" - - result = Suite(code_py).run_code() - - assert 19 == result.returned_value - -@pytest.mark.integration_test -def test_call_pre_defined(): - code_py = """ -def helper(left: i32) -> i32: - return left - -@exported -def testEntry() -> i32: - return helper(13) -""" - - result = Suite(code_py).run_code() - - assert 13 == result.returned_value - -@pytest.mark.integration_test -def test_call_post_defined(): - code_py = """ -@exported -def testEntry() -> i32: - return helper(10, 3) - -def helper(left: i32, right: i32) -> i32: - return left - right -""" - - result = Suite(code_py).run_code() - - assert 7 == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_call_with_expression_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return helper(10 + 20, 3 + 5) - -def helper(left: {type_}, right: {type_}) -> {type_}: - return left - right -""" - - result = Suite(code_py).run_code() - - assert 22 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_call_with_expression_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return helper(10.078125 + 90.046875, 63.0 + 5.0) - -def helper(left: {type_}, right: {type_}) -> {type_}: - return left - right -""" - - result = Suite(code_py).run_code() - - assert 32.125 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -def test_call_invalid_return_type(): - code_py = """ -def helper() -> i64: - return 19 - -@exported -def testEntry() -> i32: - return helper() -""" - - with pytest.raises(Type3Exception, match=r'i64 must be i32 instead'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_call_invalid_arg_type(): - code_py = """ -def helper(left: u8) -> u8: - return left - -@exported -def testEntry() -> u8: - return helper(500) -""" - - with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): - Suite(code_py).run_code() diff --git a/tests/integration/test_lang/test_static_array.py b/tests/integration/test_lang/test_static_array.py index 4d32cd8..e804363 100644 --- a/tests/integration/test_lang/test_static_array.py +++ b/tests/integration/test_lang/test_static_array.py @@ -2,132 +2,31 @@ import pytest from phasm.type3.entry import Type3Exception -from ..constants import ( - ALL_FLOAT_TYPES, ALL_INT_TYPES, COMPLETE_INT_TYPES, COMPLETE_NUMERIC_TYPES, TYPE_MAP -) from ..helpers import Suite + @pytest.mark.integration_test -def test_module_constant_def(): +def test_static_array_index_ok(): code_py = """ -CONSTANT: u8[3] = (24, 57, 80, ) - @exported -def testEntry() -> i32: - return 0 +def testEntry(f: u64[3]) -> u64: + return f[2] """ - result = Suite(code_py).run_code() + result = Suite(code_py).run_code((1, 2, 3, )) - assert 0 == result.returned_value + assert 3 == result.returned_value @pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_INT_TYPES) -def test_module_constant_3(type_): - code_py = f""" -CONSTANT: {type_}[3] = (24, 57, 80, ) - -@exported -def testEntry() -> {type_}: - return CONSTANT[1] -""" - - result = Suite(code_py).run_code() - - assert 57 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_function_call_int(type_): - code_py = f""" -CONSTANT: {type_}[3] = (24, 57, 80, ) - -@exported -def testEntry() -> {type_}: - return helper(CONSTANT) - -def helper(array: {type_}[3]) -> {type_}: - return array[0] + array[1] + array[2] -""" - - result = Suite(code_py).run_code() - - assert 161 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_function_call_float(type_): - code_py = f""" -CONSTANT: {type_}[3] = (24.0, 57.5, 80.75, ) - -@exported -def testEntry() -> {type_}: - return helper(CONSTANT) - -def helper(array: {type_}[3]) -> {type_}: - return array[0] + array[1] + array[2] -""" - - result = Suite(code_py).run_code() - - assert 162.25 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -def test_function_call_element_ok(): +def test_static_array_index_invalid_type(): code_py = """ -CONSTANT: u64[3] = (250, 250000, 250000000, ) - @exported -def testEntry() -> u64: - return helper(CONSTANT[0]) - -def helper(x: u64) -> u64: - return x +def testEntry(f: f32[3]) -> u64: + return f[0] """ - result = Suite(code_py).run_code() - - assert 250 == result.returned_value - -@pytest.mark.integration_test -def test_function_call_element_type_mismatch(): - code_py = """ -CONSTANT: u64[3] = (250, 250000, 250000000, ) - -@exported -def testEntry() -> u8: - return helper(CONSTANT[0]) - -def helper(x: u8) -> u8: - return x -""" - - with pytest.raises(Type3Exception, match=r'u8 must be u64 instead'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_module_constant_type_mismatch_bitwidth(): - code_py = """ -CONSTANT: u8[3] = (24, 57, 280, ) -""" - - with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_return_as_int(): - code_py = """ -CONSTANT: u8[3] = (24, 57, 80, ) - -def testEntry() -> u32: - return CONSTANT -""" - - with pytest.raises(Type3Exception, match=r'static_array \(u8\) \(3\) must be u32 instead'): - Suite(code_py).run_code() + with pytest.raises(Type3Exception, match=r'u64 must be f32 instead'): + Suite(code_py).run_code((0.0, 1.5, 2.25, )) @pytest.mark.integration_test def test_module_constant_type_mismatch_not_subscriptable(): diff --git a/tests/integration/test_lang/test_struct.py b/tests/integration/test_lang/test_struct.py index bcef772..63e9b15 100644 --- a/tests/integration/test_lang/test_struct.py +++ b/tests/integration/test_lang/test_struct.py @@ -2,61 +2,20 @@ import pytest from phasm.type3.entry import Type3Exception -from ..constants import ( - ALL_INT_TYPES, TYPE_MAP -) from ..helpers import Suite -@pytest.mark.integration_test -def test_module_constant_def(): - code_py = """ -class SomeStruct: - value0: u8 - value1: u32 - value2: u64 -CONSTANT: SomeStruct = SomeStruct(250, 250000, 250000000) +@pytest.mark.integration_test +def test_struct_0(): + code_py = """ +class CheckedValue: + value: i32 @exported def testEntry() -> i32: - return 0 -""" - - result = Suite(code_py).run_code() - - assert 0 == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_INT_TYPES) -def test_module_constant(type_): - code_py = f""" -class CheckedValue: - value: {type_} - -CONSTANT: CheckedValue = CheckedValue(24) - -@exported -def testEntry() -> {type_}: - return CONSTANT.value -""" - - result = Suite(code_py).run_code() - - assert 24 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_INT_TYPES) -def test_struct_0(type_): - code_py = f""" -class CheckedValue: - value: {type_} - -@exported -def testEntry() -> {type_}: return helper(CheckedValue(23)) -def helper(cv: CheckedValue) -> {type_}: +def helper(cv: CheckedValue) -> i32: return cv.value """ @@ -104,41 +63,6 @@ def helper(shape1: Rectangle, shape2: Rectangle) -> i32: assert 545 == result.returned_value -@pytest.mark.integration_test -def test_returned_struct(): - code_py = """ -class CheckedValue: - value: u8 - -CONSTANT: CheckedValue = CheckedValue(199) - -def helper() -> CheckedValue: - return CONSTANT - -def helper2(x: CheckedValue) -> u8: - return x.value - -@exported -def testEntry() -> u8: - return helper2(helper()) -""" - - result = Suite(code_py).run_code() - - assert 199 == result.returned_value - -@pytest.mark.integration_test -def test_type_mismatch_arg_module_constant(): - code_py = """ -class Struct: - param: f32 - -STRUCT: Struct = Struct(1) -""" - - with pytest.raises(Type3Exception, match='Must be real'): - Suite(code_py).run_code() - @pytest.mark.integration_test @pytest.mark.parametrize('type_', ['i32', 'i64', 'f32', 'f64']) def test_type_mismatch_struct_member(type_): diff --git a/tests/integration/test_lang/test_tuple.py b/tests/integration/test_lang/test_tuple.py index 61332be..66d24ee 100644 --- a/tests/integration/test_lang/test_tuple.py +++ b/tests/integration/test_lang/test_tuple.py @@ -2,51 +2,8 @@ import pytest from phasm.type3.entry import Type3Exception -from ..constants import ALL_FLOAT_TYPES, COMPLETE_INT_TYPES, TYPE_MAP from ..helpers import Suite -@pytest.mark.integration_test -def test_module_constant_def(): - code_py = """ -CONSTANT: (u8, u32, u64, ) = (250, 250000, 250000000, ) - -@exported -def testEntry() -> i32: - return 0 -""" - - result = Suite(code_py).run_code() - - assert 0 == result.returned_value - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ['u8', 'u32', 'u64', ]) -def test_module_constant_1(type_): - code_py = f""" -CONSTANT: ({type_}, ) = (65, ) - -@exported -def testEntry() -> {type_}: - return CONSTANT[0] -""" - - result = Suite(code_py).run_code() - - assert 65 == result.returned_value - -@pytest.mark.integration_test -def test_module_constant_6(): - code_py = """ -CONSTANT: (u8, u8, u32, u32, u64, u64, ) = (11, 22, 3333, 4444, 555555, 666666, ) - -@exported -def testEntry() -> u32: - return CONSTANT[2] -""" - - result = Suite(code_py).run_code() - - assert 3333 == result.returned_value @pytest.mark.integration_test def test_function_call_element_ok(): @@ -65,175 +22,6 @@ def helper(x: u64) -> u64: assert 250000000 == result.returned_value -@pytest.mark.integration_test -def test_tuple(): - code_py = """ -def l1(c: (u64, )) -> u64: - return c[0] - -@exported -def testEntry() -> u64: - return l1((32, )) -""" - - result = Suite(code_py).run_code() - - assert 32 == result.returned_value - -@pytest.mark.integration_test -def test_tuple_of_tuple(): - code_py = """ -def l1(c: (u64, )) -> u64: - return c[0] - -def l2(c: ((u64, ), u64, )) -> u64: - return l1(c[0]) - -@exported -def testEntry() -> u64: - return l2(((64, ), 32, )) -""" - - result = Suite(code_py).run_code() - - assert 64 == result.returned_value - -@pytest.mark.integration_test -def test_tuple_of_tuple_of_tuple(): - code_py = """ -def l1(c: (u64, )) -> u64: - return c[0] - -def l2(c: ((u64, ), u64, )) -> u64: - return l1(c[0]) - -def l3(c: (((u64, ), u64, ), u64, )) -> u64: - return l2(c[0]) - -@exported -def testEntry() -> u64: - return l3((((128, ), 64, ), 32, )) -""" - - result = Suite(code_py).run_code() - - assert 128 == result.returned_value - -@pytest.mark.integration_test -def test_constant_tuple_of_tuple_of_tuple(): - code_py = """ -CONSTANT: (((u64, ), u64, ), u64, ) = (((128, ), 64, ), 32, ) - -def l1(c: (u64, )) -> u64: - return c[0] - -def l2(c: ((u64, ), u64, )) -> u64: - return l1(c[0]) - -def l3(c: (((u64, ), u64, ), u64, )) -> u64: - return l2(c[0]) - -@exported -def testEntry() -> u64: - return l3(CONSTANT) -""" - - result = Suite(code_py).run_code() - - assert 128 == result.returned_value - -@pytest.mark.integration_test -def test_bytes_as_part_of_tuple(): - code_py = """ -CONSTANT: (bytes, u64, ) = (b'ABCDEF', 19, ) - -def l0(c: bytes) -> u8: - return c[0] - -def l1(c: (bytes, u64, )) -> u8: - return l0(c[0]) - -@exported -def testEntry() -> u8: - return l1(CONSTANT) -""" - - result = Suite(code_py).run_code() - - assert 0x41 == result.returned_value - -@pytest.mark.integration_test -def test_struct_as_part_of_tuple(): - code_py = """ -class ValueStruct: - value: i32 - -CONSTANT: (ValueStruct, u64, ) = (ValueStruct(234), 19, ) - -def l0(c: ValueStruct) -> i32: - return c.value - -def l1(c: (ValueStruct, u64, )) -> i32: - return l0(c[0]) - -@exported -def testEntry() -> i32: - return l1(CONSTANT) -""" - - result = Suite(code_py).run_code() - - assert 234 == result.returned_value - -@pytest.mark.integration_test -def test_function_call_element_type_mismatch(): - code_py = """ -CONSTANT: (u8, u32, u64, ) = (250, 250000, 250000000, ) - -@exported -def testEntry() -> u8: - return helper(CONSTANT[2]) - -def helper(x: u8) -> u8: - return x -""" - - with pytest.raises(Type3Exception, match=r'u8 must be u64 instead'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', COMPLETE_INT_TYPES) -def test_tuple_simple_constructor_int(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return helper((24, 57, 80, )) - -def helper(vector: ({type_}, {type_}, {type_}, )) -> {type_}: - return vector[0] + vector[1] + vector[2] -""" - - result = Suite(code_py).run_code() - - assert 161 == result.returned_value - assert TYPE_MAP[type_] == type(result.returned_value) - -@pytest.mark.integration_test -@pytest.mark.parametrize('type_', ALL_FLOAT_TYPES) -def test_tuple_simple_constructor_float(type_): - code_py = f""" -@exported -def testEntry() -> {type_}: - return helper((1.0, 2.0, 3.0, )) - -def helper(v: ({type_}, {type_}, {type_}, )) -> {type_}: - return sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) -""" - - result = Suite(code_py).run_code() - - assert 3.74 < result.returned_value < 3.75 - @pytest.mark.integration_test @pytest.mark.skip('SIMD support is but a dream') def test_tuple_i32x4(): diff --git a/tests/integration/test_stdlib/test_alloc.py b/tests/integration/test_stdlib/test_alloc.py index 96d1fd6..34a833e 100644 --- a/tests/integration/test_stdlib/test_alloc.py +++ b/tests/integration/test_stdlib/test_alloc.py @@ -5,6 +5,7 @@ import pytest from ..helpers import write_header from ..runners import RunnerPywasm3 as Runner + def setup_interpreter(phash_code: str) -> Runner: runner = Runner(phash_code)