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)