Test generation framework with typing improvements

Prior to this PR, each type would have its own handwritten
test suite. The end result was that not all types were tested
for all situations.

This PR adds a framework based on a Markdown file, which
generates the basic tests for the types defined in json
files. These are auto generated and updated by the Makefile
before the test suite is run.

Also, a number of unsupported type combinations are now
supported.

Also, we now support negative literals.

Also, allocation calculation fixes for nested types.

Also, the test helpers can now properly import and export
typed variables such as bytes, static arrays and tuples. This
may come in handy when it comes to phasm platform wanting to
route data.

Also, adds better support for i8 type.

Also, started on a runtime.py, since there's quite some code
now that deals with compile time handling of WebAssembly stuff.

Also, minor improvement to the type constrains, namely we
better match 'tuple' literals with static array types.

Also, reduced spam when printing the type analysis results;
constraints that go back on the backlog are now no longer
printed one by one. It now also prints the end results of
the typing analysis.

Also, reorganized the big test_primitives test into type
classes.

Also, replaced pylint with ruff.
This commit is contained in:
Johan B.W. de Vries 2023-11-11 12:02:37 +01:00
parent fd3939a680
commit 97b61e3ee1
57 changed files with 1736 additions and 1139 deletions

2
.gitignore vendored
View File

@ -3,4 +3,6 @@
/.coverage
/venv
/tests/integration/test_lang/test_generated_*.py
__pycache__

View File

@ -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:

View File

@ -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:
"""

View File

@ -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

View File

@ -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('<i', inp) # Should be B
return struct.pack('<I', inp) # Should be 'B'
def module_data_u32(inp: int) -> bytes:
"""
@ -678,6 +682,14 @@ def module_data_u64(inp: int) -> bytes:
"""
return struct.pack('<Q', inp)
def module_data_i8(inp: int) -> bytes:
"""
Compile: module data, i8 value
# FIXME: All i8 values are stored as i32
"""
return struct.pack('<i', inp) # Should be a 'b'
def module_data_i32(inp: int) -> 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: 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}')

View File

@ -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

View File

@ -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)],

69
phasm/runtime.py Normal file
View File

@ -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}')

View File

@ -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

View File

@ -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:

View File

@ -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):
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 = []

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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:

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[tool.ruff.lint]
select = ["F", "E", "W", "I"]
ignore = ["E501"]

View File

@ -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

View File

@ -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

View File

@ -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('<I', inp)[0]
if typ is type3types.u32:
assert len(inp) == 4
return struct.unpack('<I', inp)[0]
if typ is type3types.u64:
assert len(inp) == 8
return struct.unpack('<Q', inp)[0]
if typ is type3types.i8:
# See compiler.py, LOAD_STORE_TYPE_MAP and module_data_i8
assert len(inp) == 4
return struct.unpack('<i', inp)[0]
if typ is type3types.i32:
assert len(inp) == 4
return struct.unpack('<i', inp)[0]
if typ is type3types.i64:
assert len(inp) == 8
return struct.unpack('<q', inp)[0]
if typ is type3types.f32:
assert len(inp) == 4
return struct.unpack('<f', inp)[0]
if typ is type3types.f64:
assert len(inp) == 8
return struct.unpack('<d', inp)[0]
if typ is type3types.bytes:
# Note: For bytes, inp should contain a 4 byte pointer
assert len(inp) == 4
adr = struct.unpack('<I', inp)[0]
return _load_bytes_from_address(runner, typ, adr)
if isinstance(typ, type3types.AppliedType3):
# Note: For applied types, inp should contain a 4 byte pointer
assert len(inp) == 4
adr = struct.unpack('<I', inp)[0]
if typ.base is type3types.static_array:
return _load_static_array_from_address(runner, typ, adr)
if typ.base is type3types.tuple:
return _load_tuple_from_address(runner, typ, adr)
if isinstance(typ, type3types.StructType3):
# Note: For structs, inp should contain a 4 byte pointer
assert len(inp) == 4
adr = struct.unpack('<I', inp)[0]
return _load_struct_from_address(runner, typ, adr)
raise NotImplementedError(typ, inp)
def _load_bytes_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> bytes:
sys.stderr.write(f'Reading 0x{adr:08x} {typ:s}\n')
read_bytes = runner.interpreter_read_memory(adr, 4)
bytes_len, = struct.unpack('<I', read_bytes)
adr += 4
return runner.interpreter_read_memory(adr, bytes_len)
def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> 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))
}

View File

@ -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))

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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',
)
```

View File

@ -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()

View File

@ -0,0 +1,4 @@
{
"TYPE": "bytes",
"VAL0": "b'ABCDEFG'"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "f32",
"VAL0": "1000000.125"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "f64",
"VAL0": "1000000.125"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "i32",
"VAL0": "1000000"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "i64",
"VAL0": "1000000"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "static_array_tuple_u32_u32_3",
"TYPE": "(u32, u32, )[3]",
"VAL0": "((1, 100, ), (2, 200, ), (3, 300, ), )"
}

View File

@ -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, )"
}

View File

@ -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}]
}
]
}
}

View File

@ -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!"
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,12 @@
{
"TYPE_NAME": "struct_one_field",
"TYPE": "StructOneField",
"VAL0": "StructOneField(4)",
"CODE_HEADER": [
"class StructOneField:",
" value: u32"
],
"PYTHON": {
"VAL0": {"value": 4}
}
}

View File

@ -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!', )"
}

View File

@ -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, ), )"
}

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "tuple_u64_u32_u8",
"TYPE": "(u64, u32, u8, )",
"VAL0": "(1000000, 1000, 1, )"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "u32",
"VAL0": "1000000"
}

View File

@ -0,0 +1,4 @@
{
"TYPE": "u64",
"VAL0": "1000000"
}

View File

@ -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)

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -4,6 +4,7 @@ from phasm.type3.entry import Type3Exception
from ..helpers import Suite
@pytest.mark.integration_test
def test_imported_ok():
code_py = """

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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():

View File

@ -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_):

View File

@ -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():

View File

@ -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)