diff --git a/TODO.md b/TODO.md index 1de5d35..dac1bea 100644 --- a/TODO.md +++ b/TODO.md @@ -14,10 +14,8 @@ - Functions don't seem to be a thing on typing level yet? - static_array and tuple should probably not be PrimitiveType3, but instead subclass AppliedType3? - See if we want to replace Fractional with Real, and add Rational, Irrationl, Algebraic, Transendental +- Does Subscript do what we want? It's a language feature rather a normal typed thing. How would you implement your own Subscript-able type? -- test_bitwise_or_inv_type -- test_bytes_index_out_of_bounds vs static trap(?) -- test_num.py is probably best as part of the generator? - Find pytest.mark.skip - There's a weird resolve_as reference in calculate_alloc_size - Either there should be more of them or less @@ -36,3 +34,9 @@ - Filter out methods that aren't used; other the other way around (easier?) only add __ methods when needed - Move UnaryOp.operator into type class methods - Move foldr into type class methods +- PrimitiveType is just the type, nothing primitive about it (change the name). u32 are still basic types or something. + + +To review before MR: +- tuple_instantiation? +- .. go over rest of code diff --git a/phasm/codestyle.py b/phasm/codestyle.py index cd1804a..cb3cdc8 100644 --- a/phasm/codestyle.py +++ b/phasm/codestyle.py @@ -27,21 +27,6 @@ def type3(inp: Type3OrPlaceholder) -> str: if inp is type3types.none: return 'None' - if isinstance(inp, type3types.AppliedType3): - if inp.base == type3types.tuple: - return '(' + ', '.join( - type3(x) - for x in inp.args - if isinstance(x, Type3) # Skip ints, not allowed here anyhow - ) + ', )' - - if inp.base == type3types.static_array: - assert 2 == len(inp.args) - assert isinstance(inp.args[0], Type3), TYPE3_ASSERTION_ERROR - assert isinstance(inp.args[1], type3types.IntType3), TYPE3_ASSERTION_ERROR - - return type3(inp.args[0]) + '[' + inp.args[1].name + ']' - return inp.name def struct_definition(inp: ourlang.StructDefinition) -> str: diff --git a/phasm/compiler.py b/phasm/compiler.py index 8ae5ab7..3ad79df 100644 --- a/phasm/compiler.py +++ b/phasm/compiler.py @@ -286,7 +286,7 @@ def type3(inp: type3types.Type3OrPlaceholder) -> wasm.WasmType: if inp == type3types.f64: return wasm.WasmTypeFloat64() - if inp == type3types.bytes: + if inp == type3types.bytes_: # bytes are passed as pointer # And pointers are i32 return wasm.WasmTypeInt32() @@ -295,14 +295,8 @@ def type3(inp: type3types.Type3OrPlaceholder) -> wasm.WasmType: # Structs are passed as pointer, which are i32 return wasm.WasmTypeInt32() - if isinstance(inp, type3types.AppliedType3): - if inp.base == type3types.static_array: - # Static Arrays are passed as pointer, which are i32 - return wasm.WasmTypeInt32() - - if inp.base == type3types.tuple: - # Tuples are passed as pointer, which are i32 - return wasm.WasmTypeInt32() + if type3classes.InternalPassAsPointer in inp.classes: + return wasm.WasmTypeInt32() raise NotImplementedError(type3, inp) @@ -310,9 +304,21 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> """ Compile: Instantiation (allocation) of a tuple """ - assert isinstance(inp.type3, type3types.AppliedType3) - assert inp.type3.base is type3types.tuple - assert len(inp.elements) == len(inp.type3.args) + assert isinstance(inp.type3, type3types.PrimitiveType3) + + args: list[type3types.PrimitiveType3] = [] + + sa_args = type3types.static_array.did_construct(inp.type3) + if sa_args is not None: + sa_type, sa_len = sa_args + args = [sa_type for _ in range(sa_len.value)] + + if not args: + tp_args = type3types.tuple_.did_construct(inp.type3) + if tp_args is None: + raise NotImplementedError + + args = list(tp_args) comment_elements = '' for element in inp.elements: @@ -329,14 +335,17 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> # Store each element individually offset = 0 - for element, exp_type3 in zip(inp.elements, inp.type3.args): + for element, exp_type3 in zip(inp.elements, args): if isinstance(exp_type3, type3types.PlaceholderForType): assert exp_type3.resolve_as is not None + assert isinstance(exp_type3.resolve_as, type3types.PrimitiveType3) exp_type3 = exp_type3.resolve_as assert element.type3 == exp_type3 - if isinstance(exp_type3, type3types.StructType3) or isinstance(exp_type3, type3types.AppliedType3): + if type3classes.InternalPassAsPointer in exp_type3.classes: + mtyp = 'i32' + elif isinstance(exp_type3, type3types.StructType3): mtyp = 'i32' else: assert isinstance(exp_type3, type3types.PrimitiveType3), NotImplementedError('Tuple of applied types / structs') @@ -357,6 +366,10 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: """ Compile: Any expression """ + if isinstance(inp, (ourlang.ConstantStruct, ourlang.ConstantTuple, )): + # These are implemented elsewhere + raise Exception + if isinstance(inp, ourlang.ConstantPrimitive): assert isinstance(inp.type3, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR @@ -401,11 +414,16 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: if isinstance(inp.variable, ourlang.ModuleConstantDef): assert isinstance(inp.type3, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR - if isinstance(inp.type3, type3types.PrimitiveType3): - expression(wgn, inp.variable.constant) + if type3classes.InternalPassAsPointer in inp.type3.classes: + # FIXME: Artifact from older version + assert isinstance(inp.variable.constant, ourlang.ConstantTuple) + + address = inp.variable.constant.data_block.address + assert address is not None, 'Value not allocated' + wgn.i32.const(address) return - if inp.type3 is type3types.bytes: + if inp.type3 is type3types.bytes_: assert isinstance(inp.variable.constant, ourlang.ConstantBytes) address = inp.variable.constant.data_block.address @@ -421,24 +439,9 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: wgn.i32.const(address) return - if isinstance(inp.type3, type3types.AppliedType3): - if inp.type3.base == type3types.static_array: - assert isinstance(inp.variable.constant, ourlang.ConstantTuple) - - address = inp.variable.constant.data_block.address - assert address is not None, 'Value not allocated' - wgn.i32.const(address) - return - - if inp.type3.base == type3types.tuple: - assert isinstance(inp.variable.constant, ourlang.ConstantTuple) - - address = inp.variable.constant.data_block.address - assert address is not None, 'Value not allocated' - wgn.i32.const(address) - return - - raise NotImplementedError(expression, inp.variable, inp.type3.base) + if isinstance(inp.type3, type3types.PrimitiveType3): + expression(wgn, inp.variable.constant) + return raise NotImplementedError(expression, inp) @@ -479,7 +482,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: if inp.type3 == type3types.u32: if inp.operator == 'len': - if inp.right.type3 == type3types.bytes: + if inp.right.type3 == type3types.bytes_: wgn.i32.load() return @@ -528,19 +531,17 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: if isinstance(inp, ourlang.Subscript): assert isinstance(inp.varref.type3, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR - if inp.varref.type3 is type3types.bytes: + if inp.varref.type3 is type3types.bytes_: expression(wgn, inp.varref) expression(wgn, inp.index) wgn.call(stdlib_types.__subscript_bytes__) return - if isinstance(inp.varref.type3, type3types.AppliedType3): - if inp.varref.type3.base == type3types.static_array: - assert 2 == len(inp.varref.type3.args) - el_type = inp.varref.type3.args[0] - assert isinstance(el_type, type3types.Type3) - el_len = inp.varref.type3.args[1] - assert isinstance(el_len, type3types.IntType3) + assert isinstance(inp.varref.type3, type3types.PrimitiveType3) + + sa_args = type3types.static_array.did_construct(inp.varref.type3) + if sa_args is not None: + el_type, el_len = sa_args # OPTIMIZE: If index is a constant, we can use offset instead of multiply # and we don't need to do the out of bounds check @@ -568,24 +569,24 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: wgn.add_statement(f'{mtyp}.load') return - if inp.varref.type3.base == type3types.tuple: + tp_args = type3types.tuple_.did_construct(inp.varref.type3) + if tp_args is not None: assert isinstance(inp.index, ourlang.ConstantPrimitive) assert isinstance(inp.index.value, int) offset = 0 - for el_type in inp.varref.type3.args[0:inp.index.value]: + for el_type in tp_args[0:inp.index.value]: assert isinstance(el_type, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR offset += calculate_alloc_size(el_type) - # This doubles as the out of bounds check - el_type = inp.varref.type3.args[inp.index.value] + el_type = tp_args[inp.index.value] assert isinstance(el_type, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR expression(wgn, inp.varref) if isinstance(el_type, type3types.StructType3): mtyp = 'i32' - elif isinstance(el_type, type3types.AppliedType3) and el_type.base is type3types.tuple: + elif type3classes.InternalPassAsPointer in el_type.classes: mtyp = 'i32' else: assert isinstance(el_type, type3types.PrimitiveType3), NotImplementedError('Tuple of applied types / structs') @@ -618,7 +619,7 @@ def expression_fold(wgn: WasmGenerator, inp: ourlang.Fold) -> None: """ assert isinstance(inp.type3, type3types.Type3), type3types.TYPE3_ASSERTION_ERROR - if inp.iter.type3 is not type3types.bytes: + if inp.iter.type3 is not type3types.bytes_: raise NotImplementedError(expression_fold, inp, inp.iter.type3) wgn.add_statement('nop', comment='acu :: u8') @@ -906,7 +907,7 @@ def module_data(inp: ourlang.ModuleData) -> bytes: data_list.append(module_data_f64(constant.value)) continue - if constant.type3 == type3types.bytes: + if constant.type3 == type3types.bytes_: assert isinstance(constant, ourlang.ConstantBytes) assert isinstance(constant.value, bytes) data_list.append(module_data_u32(len(constant.value))) @@ -987,7 +988,9 @@ def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstruc # 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): + if type3classes.InternalPassAsPointer in mtyp3.classes: + mtyp = 'i32' + elif isinstance(mtyp3, type3types.StructType3): mtyp = 'i32' else: mtyp = LOAD_STORE_TYPE_MAP.get(mtyp3.name) diff --git a/phasm/ourlang.py b/phasm/ourlang.py index 407c277..2c0a4d9 100644 --- a/phasm/ourlang.py +++ b/phasm/ourlang.py @@ -275,6 +275,9 @@ class StatementReturn(Statement): def __init__(self, value: Expression) -> None: self.value = value + def __repr__(self) -> str: + return f'StatementReturn({repr(self.value)})' + class StatementIf(Statement): """ An if statement within a function diff --git a/phasm/parser.py b/phasm/parser.py index eb8eef9..227d84a 100644 --- a/phasm/parser.py +++ b/phasm/parser.py @@ -664,7 +664,7 @@ class OurVisitor: raise NotImplementedError(f'{node.value} as constant') - def visit_type(self, module: Module, node: ast.expr) -> type3types.Type3: + def visit_type(self, module: Module, node: ast.expr) -> type3types.PrimitiveType3: if isinstance(node, ast.Constant): if node.value is None: return type3types.none @@ -693,18 +693,17 @@ class OurVisitor: if not isinstance(node.ctx, ast.Load): _raise_static_error(node, 'Must be load context') - return type3types.AppliedType3( - type3types.static_array, - [self.visit_type(module, node.value), type3types.IntType3(node.slice.value)], + return type3types.static_array( + self.visit_type(module, node.value), + type3types.IntType3(node.slice.value), ) if isinstance(node, ast.Tuple): if not isinstance(node.ctx, ast.Load): _raise_static_error(node, 'Must be load context') - return type3types.AppliedType3( - type3types.tuple, - (self.visit_type(module, elt) for elt in node.elts) + return type3types.tuple_( + *(self.visit_type(module, elt) for elt in node.elts) ) raise NotImplementedError(f'{node} as type') diff --git a/phasm/runtime.py b/phasm/runtime.py index cded709..45019da 100644 --- a/phasm/runtime.py +++ b/phasm/runtime.py @@ -11,7 +11,7 @@ def calculate_alloc_size(typ: type3types.Type3, is_member: bool = False) -> int: if typ in (type3types.u64, type3types.i64, type3types.f64, ): return 8 - if typ == type3types.bytes: + if typ == type3types.bytes_: if is_member: return 4 @@ -27,28 +27,30 @@ def calculate_alloc_size(typ: type3types.Type3, is_member: bool = False) -> int: for x in typ.members.values() ) - if isinstance(typ, type3types.AppliedType3): - if typ.base is type3types.static_array: + assert isinstance(typ, type3types.PrimitiveType3) + + sa_args = type3types.static_array.did_construct(typ) + if sa_args is not None: 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) + sa_type, sa_len = sa_args - return typ.args[1].value * calculate_alloc_size(typ.args[0], is_member=True) + return sa_len.value * calculate_alloc_size(sa_type, is_member=True) - if typ.base is type3types.tuple: + tp_args = type3types.tuple_.did_construct(typ) + if tp_args is not None: if is_member: # tuples referred to by other structs or tuples are pointers return 4 size = 0 - for arg in typ.args: + for arg in tp_args: assert not isinstance(arg, type3types.IntType3) if isinstance(arg, type3types.PlaceholderForType): - assert arg.resolve_as is not None + assert isinstance(arg.resolve_as, type3types.PrimitiveType3) arg = arg.resolve_as size += calculate_alloc_size(arg, is_member=True) diff --git a/phasm/stdlib/types.py b/phasm/stdlib/types.py index 70c1709..b7192bb 100644 --- a/phasm/stdlib/types.py +++ b/phasm/stdlib/types.py @@ -48,21 +48,20 @@ def __subscript_bytes__(g: Generator, adr: i32, ofs: i32) -> i32: g.local.get(ofs) g.local.get(adr) g.i32.load() - g.i32.lt_u() + g.i32.ge_u() with g.if_(): - # The offset is less than the length + # The offset is outside the allocated bytes + g.unreachable(comment='Out of bounds') - g.local.get(adr) - g.i32.const(4) # Bytes header - g.i32.add() - g.local.get(ofs) - g.i32.add() - g.i32.load8_u() - g.return_() + # The offset is less than the length - # The offset is outside the allocated bytes - g.i32.const(0) + g.local.get(adr) + g.i32.const(4) # Bytes header + g.i32.add() + g.local.get(ofs) + g.i32.add() + g.i32.load8_u() g.return_() return i32('return') # To satisfy mypy diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index e0f9e43..60a545d 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -112,10 +112,6 @@ class SameTypeConstraint(ConstraintBase): known_types.append(typ) continue - if isinstance(typ, types.AppliedType3): - known_types.append(typ) - continue - if isinstance(typ, types.PlaceholderForType): if typ.resolve_as is not None: known_types.append(typ.resolve_as) @@ -132,38 +128,6 @@ 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 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 - )) - - continue - if typ != first_type: return Error(f'{typ:s} must be {first_type:s} instead', comment=self.comment) @@ -198,6 +162,47 @@ class SameTypeConstraint(ConstraintBase): return f'SameTypeConstraint({args}, comment={repr(self.comment)})' +class TupleMatchConstraint(ConstraintBase): + def __init__(self, exp_type: types.Type3OrPlaceholder, args: List[types.Type3OrPlaceholder], comment: str): + super().__init__(comment=comment) + + self.exp_type = exp_type + self.args = list(args) + + def check(self) -> CheckResult: + exp_type = self.exp_type + if isinstance(exp_type, types.PlaceholderForType): + if exp_type.resolve_as is None: + return RequireTypeSubstitutes() + + exp_type = exp_type.resolve_as + + assert isinstance(exp_type, types.PrimitiveType3) + + sa_args = types.static_array.did_construct(exp_type) + if sa_args is not None: + sa_type, sa_len = sa_args + + if sa_len.value != len(self.args): + return Error('Mismatch between applied types argument count', comment=self.comment) + + return [ + SameTypeConstraint(arg, sa_type) + for arg in self.args + ] + + tp_args = types.tuple_.did_construct(exp_type) + if tp_args is not None: + if len(tp_args) != len(self.args): + return Error('Mismatch between applied types argument count', comment=self.comment) + + return [ + SameTypeConstraint(arg, oth_arg) + for arg, oth_arg in zip(self.args, tp_args) + ] + + raise NotImplementedError(exp_type) + class IntegerCompareConstraint(ConstraintBase): """ Verifies that the given IntType3 are in order (<=) @@ -218,7 +223,7 @@ class IntegerCompareConstraint(ConstraintBase): prev_val = val_list.pop(0) for next_val in val_list: if prev_val > next_val: - return Error(f'{prev_val} must be less or equal than {next_val}') + return Error(f'{prev_val} must be less or equal to {next_val}') prev_val = next_val @@ -292,14 +297,7 @@ class MustImplementTypeClassConstraint(ConstraintBase): type3: types.Type3OrPlaceholder DATA = { - 'u8': {'BitWiseOperation', 'EqualComparison', 'StrictPartialOrder'}, - 'u32': {'BitWiseOperation', 'EqualComparison', 'StrictPartialOrder'}, - 'u64': {'BitWiseOperation', 'EqualComparison', 'StrictPartialOrder'}, - 'i32': {'EqualComparison', 'StrictPartialOrder'}, - 'i64': {'EqualComparison', 'StrictPartialOrder'}, 'bytes': {'Foldable', 'Sized'}, - 'f32': {'Fractional', 'FloatingPoint'}, - 'f64': {'Fractional', 'FloatingPoint'}, } def __init__(self, type_class3: Union[str, typeclasses.Type3Class], type3: types.Type3OrPlaceholder, comment: Optional[str] = None) -> None: @@ -401,7 +399,7 @@ class LiteralFitsConstraint(ConstraintBase): return Error('Must be real', comment=self.comment) # FIXME: Add line information - if self.type3 is types.bytes: + if self.type3 is types.bytes_: if isinstance(self.literal.value, bytes): return None @@ -409,45 +407,47 @@ class LiteralFitsConstraint(ConstraintBase): res: NewConstraintList - if isinstance(self.type3, types.AppliedType3): - if self.type3.base == types.tuple: + assert isinstance(self.type3, types.PrimitiveType3) + + tp_args = types.tuple_.did_construct(self.type3) + if tp_args is not None: if not isinstance(self.literal, ourlang.ConstantTuple): return Error('Must be tuple', comment=self.comment) - if len(self.type3.args) != len(self.literal.value): + if len(tp_args) != len(self.literal.value): return Error('Tuple element count mismatch', comment=self.comment) res = [] res.extend( LiteralFitsConstraint(x, y) - for x, y in zip(self.type3.args, self.literal.value) + for x, y in zip(tp_args, self.literal.value) ) res.extend( SameTypeConstraint(x, y.type3) - for x, y in zip(self.type3.args, self.literal.value) + for x, y in zip(tp_args, self.literal.value) ) return res - if self.type3.base == types.static_array: + sa_args = types.static_array.did_construct(self.type3) + if sa_args is not None: if not isinstance(self.literal, ourlang.ConstantTuple): return Error('Must be tuple', comment=self.comment) - assert 2 == len(self.type3.args) - assert isinstance(self.type3.args[1], types.IntType3) + sa_type, sa_len = sa_args - if self.type3.args[1].value != len(self.literal.value): + if sa_len.value != len(self.literal.value): return Error('Member count mismatch', comment=self.comment) res = [] res.extend( - LiteralFitsConstraint(self.type3.args[0], y) + LiteralFitsConstraint(sa_type, y) for y in self.literal.value ) res.extend( - SameTypeConstraint(self.type3.args[0], y.type3) + SameTypeConstraint(sa_type, y.type3) for y in self.literal.value ) @@ -511,57 +511,58 @@ class CanBeSubscriptedConstraint(ConstraintBase): self.index_type3 = index.type3 def check(self) -> CheckResult: - if isinstance(self.type3, types.PlaceholderForType): - if self.type3.resolve_as is None: + exp_type = self.type3 + if isinstance(exp_type, types.PlaceholderForType): + if exp_type.resolve_as is None: return RequireTypeSubstitutes() - self.type3 = self.type3.resolve_as + exp_type = exp_type.resolve_as + + assert isinstance(exp_type, types.PrimitiveType3) + + sa_args = types.static_array.did_construct(exp_type) + if sa_args is not None: + sa_type, sa_len = sa_args - if isinstance(self.type3, types.AppliedType3): - if self.type3.base == types.static_array: result: List[ConstraintBase] = [ SameTypeConstraint(types.u32, self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'), - SameTypeConstraint(self.type3.args[0], self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'), + SameTypeConstraint(sa_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'), ] if isinstance(self.index, ourlang.ConstantPrimitive): assert isinstance(self.index.value, int) - assert isinstance(self.type3.args[1], types.IntType3) - result.append( - IntegerCompareConstraint( - types.IntType3(0), types.IntType3(self.index.value), types.IntType3(self.type3.args[1].value - 1), - comment='Subscript static array must fit the size of the array' - ) - ) + if self.index.value < 0 or sa_len.value <= self.index.value: + return Error('Tuple index out of range') return result - if self.type3.base == types.tuple: + # We special case tuples to allow for ease of use to the programmer + # e.g. rather than having to do `fst a` and `snd a` and only have to-sized tuples + # we use a[0] and a[1] and allow for a[2] and on. + tp_args = types.tuple_.did_construct(exp_type) + if tp_args is not None: if not isinstance(self.index, ourlang.ConstantPrimitive): return Error('Must index with literal') if not isinstance(self.index.value, int): return Error('Must index with integer literal') - if self.index.value < 0 or len(self.type3.args) <= self.index.value: + if self.index.value < 0 or len(tp_args) <= self.index.value: return Error('Tuple index out of range') return [ SameTypeConstraint(types.u32, self.index_type3, comment=f'Tuple subscript index {self.index.value}'), - SameTypeConstraint(self.type3.args[self.index.value], self.ret_type3, comment=f'Tuple subscript index {self.index.value}'), + SameTypeConstraint(tp_args[self.index.value], self.ret_type3, comment=f'Tuple subscript index {self.index.value}'), ] - if self.type3 is types.bytes: + if exp_type is types.bytes_: return [ SameTypeConstraint(types.u32, self.index_type3, comment='([]) :: bytes -> u32 -> u8'), SameTypeConstraint(types.u8, self.ret_type3, comment='([]) :: bytes -> u32 -> u8'), ] - if self.type3.name in types.LOOKUP_TABLE: - return Error(f'{self.type3.name} cannot be subscripted') - - raise NotImplementedError(self.type3) + return Error(f'{exp_type.name} cannot be subscripted') def human_readable(self) -> HumanReadableRet: return ( diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index 3b9e6de..c1a1f39 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -16,6 +16,7 @@ from .constraints import ( LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint, + TupleMatchConstraint, ) ConstraintGenerator = Generator[ConstraintBase, None, None] @@ -142,9 +143,9 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: yield from expression(ctx, arg) r_type.append(arg.type3) - yield SameTypeConstraint( + yield TupleMatchConstraint( inp.type3, - type3types.AppliedType3(type3types.tuple, r_type), + r_type, comment='The type of a tuple is a combination of its members' ) diff --git a/phasm/type3/entry.py b/phasm/type3/entry.py index bf6e54a..0e0fda8 100644 --- a/phasm/type3/entry.py +++ b/phasm/type3/entry.py @@ -13,7 +13,6 @@ from .constraints import ( ) from .constraintsgenerator import phasm_type3_generate_constraints from .types import ( - AppliedType3, IntType3, PlaceholderForType, PrimitiveType3, @@ -153,14 +152,6 @@ def get_printable_type_name(inp: Type3OrPlaceholder, placeholder_id_map: Dict[in placeholder_id_map[placeholder_id] = 'T' + str(len(placeholder_id_map) + 1) return placeholder_id_map[placeholder_id] - if isinstance(inp, AppliedType3): - return ( - get_printable_type_name(inp.base, placeholder_id_map) - + ' (' - + ') ('.join(get_printable_type_name(x, placeholder_id_map) for x in inp.args) - + ')' - ) - raise NotImplementedError(inp) def print_constraint_list(placeholder_id_map: Dict[int, str], constraint_list: List[ConstraintBase], placeholder_substitutes: SubstitutionMap) -> None: diff --git a/phasm/type3/typeclasses.py b/phasm/type3/typeclasses.py index a412179..6841c73 100644 --- a/phasm/type3/typeclasses.py +++ b/phasm/type3/typeclasses.py @@ -93,6 +93,8 @@ class Type3Class: def __repr__(self) -> str: return self.name +InternalPassAsPointer = Type3Class('InternalPassAsPointer', ['a'], methods={}, operators={}) + Eq = Type3Class('Eq', ['a'], methods={}, operators={ '==': 'a -> a -> bool', '!=': 'a -> a -> bool', diff --git a/phasm/type3/types.py b/phasm/type3/types.py index 6da5729..e14778c 100644 --- a/phasm/type3/types.py +++ b/phasm/type3/types.py @@ -4,7 +4,19 @@ Contains the final types for use in Phasm These are actual, instantiated types; not the abstract types that the constraint generator works with. """ -from typing import Any, Dict, Iterable, List, Optional, Protocol, Set, Union +from typing import ( + Any, + Dict, + Generic, + Iterable, + List, + Optional, + Protocol, + Set, + Tuple, + TypeVar, + Union, +) from .typeclasses import ( Bits, @@ -12,6 +24,7 @@ from .typeclasses import ( Floating, Fractional, Integral, + InternalPassAsPointer, IntNum, NatNum, Ord, @@ -83,7 +96,7 @@ class Type3: return not self.__eq__(other) def __hash__(self) -> int: - raise NotImplementedError + return hash(self.name) def __bool__(self) -> bool: raise NotImplementedError @@ -98,6 +111,9 @@ class PrimitiveType3(Type3): class IntType3(Type3): """ Sometimes you can have an int as type, e.g. when using static arrays + + This is not the same as an int on the language level. + [1.0, 1.2] :: Float[2] :: * -> Int -> * """ __slots__ = ('value', ) @@ -107,7 +123,6 @@ class IntType3(Type3): def __init__(self, value: int) -> None: super().__init__(str(value), []) - assert 0 <= value self.value = value def __eq__(self, other: Any) -> bool: @@ -119,6 +134,9 @@ class IntType3(Type3): raise NotImplementedError + def __hash__(self) -> int: + return hash(self.value) + class PlaceholderForType: """ A placeholder type, for when we don't know the final type yet @@ -166,64 +184,100 @@ class PlaceholderForType: Type3OrPlaceholder = Union[Type3, PlaceholderForType] -class AppliedType3(Type3): - """ - A Type3 that has been applied to another type - """ - __slots__ = ('base', 'args', ) +T = TypeVar('T') - base: PrimitiveType3 +class TypeConstructor(Generic[T]): """ - The base type + Base class for type construtors + """ + __slots__ = ('name', 'classes', 'type_classes', '_cache', '_reverse_cache') + + name: str + """ + The name of the type constructor """ - args: List[Type3OrPlaceholder] + classes: Set[Type3Class] """ - The applied types (or placeholders there for) + The type classes that this constructor implements """ - def __init__(self, base: PrimitiveType3, args: Iterable[Type3OrPlaceholder]) -> None: - args = [*args] - assert args, 'Must at least one argument' + type_classes: Set[Type3Class] + """ + The type classes that the constructed types implement + """ - super().__init__( - base.name - + ' (' - + ') ('.join(str(x) for x in args) # FIXME: Do we need to redo the name on substitution? - + ')', - [] - ) + _cache: dict[T, PrimitiveType3] + """ + When constructing a type with the same arguments, + it should produce the exact same result. + """ - self.base = base - self.args = args + _reverse_cache: dict[PrimitiveType3, T] + """ + Sometimes we need to know the key that created a type. + """ - @property - def has_placeholders(self) -> bool: - return any( - isinstance(x, PlaceholderForType) - for x in self.args - ) + def __init__(self, name: str, classes: Iterable[Type3Class], type_classes: Iterable[Type3Class]) -> None: + self.name = name + self.classes = set(classes) + self.type_classes = set(type_classes) - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Type3): - raise NotImplementedError + self._cache = {} + self._reverse_cache = {} - if not isinstance(other, AppliedType3): - return False + def make_name(self, key: T) -> str: + raise NotImplementedError - return ( - self.base == other.base - and len(self.args) == len(other.args) - and all( - s == x - for s, x in zip(self.args, other.args) - ) - ) + def did_construct(self, typ: PrimitiveType3) -> T | None: + return self._reverse_cache.get(typ) - def __repr__(self) -> str: - return f'AppliedType3({repr(self.base)}, {repr(self.args)})' + def construct(self, key: T) -> PrimitiveType3: + result = self._cache.get(key, None) + if result is None: + self._cache[key] = result = PrimitiveType3(self.make_name(key), self.type_classes) + self._reverse_cache[result] = key -class StructType3(Type3): + return result + +class TypeConstructor_Type(TypeConstructor[PrimitiveType3]): + """ + Base class type constructors of kind: * -> * + """ + __slots__ = () + + def __call__(self, arg: PrimitiveType3) -> PrimitiveType3: + raise NotImplementedError + +class TypeConstructor_TypeInt(TypeConstructor[Tuple[PrimitiveType3, IntType3]]): + """ + Base class type constructors of kind: * -> Int -> * + """ + __slots__ = () + + def make_name(self, key: Tuple[PrimitiveType3, IntType3]) -> str: + return f'{self.name} {key[0].name} {key[1].value}' + + def __call__(self, arg0: PrimitiveType3, arg1: IntType3) -> PrimitiveType3: + return self.construct((arg0, arg1)) + +class TypeConstructor_TypeStar(TypeConstructor[Tuple[PrimitiveType3, ...]]): + """ + Base class type constructors of variadic kind + """ + def __call__(self, *args: PrimitiveType3) -> PrimitiveType3: + key: Tuple[PrimitiveType3, ...] = tuple(args) + return self.construct(key) + +class TypeConstructor_StaticArray(TypeConstructor_TypeInt): + def make_name(self, key: Tuple[PrimitiveType3, IntType3]) -> str: + return f'{key[0].name}[{key[1].value}]' + +class TypeConstructor_Tuple(TypeConstructor_TypeStar): + def make_name(self, key: Tuple[PrimitiveType3, ...]) -> str: + return '(' + ', '.join(x.name for x in key) + ', )' + +class StructType3(PrimitiveType3): """ A Type3 struct with named members """ @@ -315,20 +369,27 @@ f64 = PrimitiveType3('f64', [Eq, Floating, Fractional, IntNum, NatNum, Ord]) A 32-bits IEEE 754 float, of 64 bits width. """ -bytes = PrimitiveType3('bytes', []) +bytes_ = PrimitiveType3('bytes', []) """ This is a runtime-determined length piece of memory that can be indexed at runtime. """ -static_array = PrimitiveType3('static_array', []) +static_array = TypeConstructor_StaticArray('static_array', [], [ + Eq, + InternalPassAsPointer, +]) """ -This is a fixed length piece of memory that can be indexed at runtime. +A type constructor. + +Any static array is a fixed length piece of memory that can be indexed at runtime. It should be applied with one argument. It has a runtime-dynamic length of the same type repeated. """ -tuple = PrimitiveType3('tuple', []) # pylint: disable=W0622 +tuple_ = TypeConstructor_Tuple('tuple', [], [ + InternalPassAsPointer, +]) """ This is a fixed length piece of memory. @@ -336,7 +397,7 @@ It should be applied with zero or more arguments. It has a compile time determined length, and each argument can be different. """ -LOOKUP_TABLE: Dict[str, Type3] = { +LOOKUP_TABLE: Dict[str, PrimitiveType3] = { 'none': none, 'bool': bool_, 'u8': u8, @@ -347,5 +408,5 @@ LOOKUP_TABLE: Dict[str, Type3] = { 'i64': i64, 'f32': f32, 'f64': f64, - 'bytes': bytes, + 'bytes': bytes_, } diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index efdcc68..d413b87 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -5,6 +5,7 @@ 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 typeclasses as type3classes from phasm.type3 import types as type3types from . import runners @@ -82,18 +83,20 @@ class Suite: wasm_args.append(arg) continue - if arg_typ is type3types.bytes: + 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: + assert isinstance(arg_typ, type3types.PrimitiveType3) + sa_args = type3types.static_array.did_construct(arg_typ) + if sa_args is not None: adr = _allocate_memory_stored_value(runner, arg_typ, arg) wasm_args.append(adr) continue - if arg_typ.base is type3types.tuple: + tp_args = type3types.tuple_.did_construct(arg_typ) + if tp_args is not None: adr = _allocate_memory_stored_value(runner, arg_typ, arg) wasm_args.append(adr) continue @@ -103,7 +106,7 @@ class Suite: wasm_args.append(adr) continue - raise NotImplementedError(arg) + raise NotImplementedError(arg_typ, arg) write_header(sys.stderr, 'Memory (pre run)') runner.interpreter_dump_memory(sys.stderr) @@ -142,27 +145,33 @@ def _write_memory_stored_value( val_typ: type3types.Type3, val: Any, ) -> int: - if val_typ is type3types.bytes: + 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 + if isinstance(val_typ, type3types.PrimitiveType3): + sa_args = type3types.static_array.did_construct(val_typ) + if sa_args is not None: + adr2 = _allocate_memory_stored_value(runner, val_typ, val) + runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2)) + return 4 + + tp_args = type3types.tuple_.did_construct(val_typ) + if tp_args is not None: + adr2 = _allocate_memory_stored_value(runner, val_typ, val) + runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2)) + return 4 + + to_write = WRITE_LOOKUP_MAP[val_typ.name](val) + runner.interpreter_write_memory(adr, to_write) + return len(to_write) + raise NotImplementedError(val_typ, val) def _allocate_memory_stored_value( @@ -170,7 +179,7 @@ def _allocate_memory_stored_value( val_typ: type3types.Type3, val: Any ) -> int: - if val_typ is type3types.bytes: + if val_typ is type3types.bytes_: assert isinstance(val, bytes) adr = runner.call('stdlib.types.__alloc_bytes__', len(val)) @@ -180,29 +189,30 @@ def _allocate_memory_stored_value( 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_typ, type3types.PrimitiveType3) + sa_args = type3types.static_array.did_construct(val_typ) + if sa_args is not None: assert isinstance(val, tuple) + sa_type, sa_len = sa_args + 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 + tuple_len = sa_len.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) + offset += _write_memory_stored_value(runner, offset, sa_type, val_el_val) return adr - if val_typ.base is type3types.tuple: + val_el_typ: type3types.Type3 + + tp_args = type3types.tuple_.did_construct(val_typ) + if tp_args is not None: assert isinstance(val, tuple) alloc_size = calculate_alloc_size(val_typ) @@ -210,10 +220,10 @@ def _allocate_memory_stored_value( assert isinstance(adr, int) sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n') - assert len(val) == len(val_typ.args) + assert len(val) == len(tp_args) offset = adr - for val_el_val, val_el_typ in zip(val, val_typ.args): + for val_el_val, val_el_typ in zip(val, tp_args): assert not isinstance(val_el_typ, type3types.PlaceholderForType) offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val) @@ -278,21 +288,24 @@ def _load_memory_stored_returned_value( assert isinstance(wasm_value, float), wasm_value return wasm_value - if ret_type3 is type3types.bytes: + 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(ret_type3, type3types.PrimitiveType3) # Type hint + + sa_args = type3types.static_array.did_construct(ret_type3) + if sa_args is not None: assert isinstance(wasm_value, int), wasm_value - return _load_static_array_from_address(runner, ret_type3, wasm_value) + return _load_static_array_from_address(runner, sa_args[0], sa_args[1], wasm_value) - if ret_type3.base is type3types.tuple: + tp_args = type3types.tuple_.did_construct(ret_type3) + if tp_args is not None: assert isinstance(wasm_value, int), wasm_value - return _load_tuple_from_address(runner, ret_type3, wasm_value) + return _load_tuple_from_address(runner, tp_args, wasm_value) if isinstance(ret_type3, type3types.StructType3): return _load_struct_from_address(runner, ret_type3, wasm_value) @@ -334,23 +347,28 @@ def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> An assert len(inp) == 8 return struct.unpack(' Generator 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 +def _load_static_array_from_address(runner: runners.RunnerBase, sub_typ: type3types.PrimitiveType3, len_typ: type3types.IntType3, adr: int) -> Any: + sys.stderr.write(f'Reading 0x{adr:08x} {sub_typ:s} {len_typ:s}\n') assert not isinstance(sub_typ, type3types.PlaceholderForType) assert isinstance(len_typ, type3types.IntType3) @@ -396,29 +411,19 @@ def _load_static_array_from_address(runner: runners.RunnerBase, typ: type3types. 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) +def _load_tuple_from_address(runner: runners.RunnerBase, typ_args: tuple[type3types.PrimitiveType3, ...], adr: int) -> Any: + sys.stderr.write(f'Reading 0x{adr:08x} tuple {len(typ_args)}\n') arg_sizes = [ calculate_alloc_size(x, is_member=True) - for x in typ_list + for x in typ_args ] 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)) + for arg_typ, arg_bytes in zip(typ_args, _split_read_bytes(read_bytes, arg_sizes)) ) def _load_struct_from_address(runner: runners.RunnerBase, typ: type3types.Type3, adr: int) -> Any: diff --git a/tests/integration/test_lang/generator.md b/tests/integration/test_lang/generator.md index b4a4e48..81f256a 100644 --- a/tests/integration/test_lang/generator.md +++ b/tests/integration/test_lang/generator.md @@ -116,11 +116,11 @@ def testEntry() -> i32: 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', + 'The type of a tuple is a combination of its members', ) elif TYPE_NAME.startswith('struct_'): expect_type_error( - TYPE + ' must be tuple (u32) instead', + TYPE + ' must be (u32, ) instead', 'The type of the value returned from function constant should match its return type', ) else: @@ -175,19 +175,14 @@ def testEntry() -> i32: ``` ```py -if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'): 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', + TYPE + ' must be (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', + TYPE_NAME + ' must be (u32, ) instead', 'The type of the value returned from function constant should match its return type', ) ``` @@ -231,19 +226,14 @@ def select(x: $TYPE) -> (u32, ): ``` ```py -if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'): 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', + TYPE + ' must be (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', + TYPE_NAME + ' must be (u32, ) instead', 'The type of the value returned from function select should match its return type', ) ``` @@ -287,11 +277,11 @@ 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', + 'The type of a tuple is a combination of its members', ) elif TYPE_NAME.startswith('struct_'): expect_type_error( - TYPE + ' must be tuple (u32) instead', + TYPE + ' must be (u32, ) instead', 'The type of the value passed to argument x of function helper should match the type of that argument', ) else: @@ -340,19 +330,14 @@ def testEntry() -> i32: ``` ```py -if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'): +if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'): 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', + TYPE + ' must be (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', + TYPE_NAME + ' must be (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/test_bytes.py b/tests/integration/test_lang/test_bytes.py index 3e72c40..324fa62 100644 --- a/tests/integration/test_lang/test_bytes.py +++ b/tests/integration/test_lang/test_bytes.py @@ -1,7 +1,5 @@ import pytest -from phasm.type3.entry import Type3Exception - from ..helpers import Suite @@ -16,38 +14,3 @@ def testEntry(f: bytes) -> u32: result = Suite(code_py).run_code(b'This yet is another test') assert 24 == result.returned_value - -@pytest.mark.integration_test -def test_bytes_index_ok(): - code_py = """ -@exported -def testEntry(f: bytes) -> u8: - return f[8] -""" - - result = Suite(code_py).run_code(b'This is another test') - - assert 0x61 == result.returned_value - -@pytest.mark.integration_test -def test_bytes_index_out_of_bounds(): - code_py = """ -@exported -def testEntry(f: bytes, g: bytes) -> u8: - return f[50] -""" - - result = Suite(code_py).run_code(b'Short', b'Long' * 100) - - assert 0 == result.returned_value - -@pytest.mark.integration_test -def test_bytes_index_invalid_type(): - code_py = """ -@exported -def testEntry(f: bytes) -> u64: - return f[50] -""" - - with pytest.raises(Type3Exception, match=r'u64 must be u8 instead'): - Suite(code_py).run_code(b'Short') diff --git a/tests/integration/test_lang/test_static_array.py b/tests/integration/test_lang/test_static_array.py index fa3ed3e..add37c0 100644 --- a/tests/integration/test_lang/test_static_array.py +++ b/tests/integration/test_lang/test_static_array.py @@ -1,73 +1,10 @@ import pytest -import wasmtime from phasm.type3.entry import Type3Exception from ..helpers import Suite -@pytest.mark.integration_test -def test_static_array_index_ok(): - code_py = """ -@exported -def testEntry(f: u64[3]) -> u64: - return f[2] -""" - - result = Suite(code_py).run_code((1, 2, 3, )) - - assert 3 == result.returned_value - -@pytest.mark.integration_test -def test_static_array_index_invalid_type(): - code_py = """ -@exported -def testEntry(f: f32[3]) -> u64: - return f[0] -""" - - 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(): - code_py = """ -CONSTANT: u8 = 24 - -@exported -def testEntry() -> u8: - return CONSTANT[0] -""" - - with pytest.raises(Type3Exception, match='u8 cannot be subscripted'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_module_constant_type_mismatch_index_out_of_range_constant(): - code_py = """ -CONSTANT: u8[3] = (24, 57, 80, ) - -@exported -def testEntry() -> u8: - return CONSTANT[3] -""" - - with pytest.raises(Type3Exception, match='3 must be less or equal than 2'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_module_constant_type_mismatch_index_out_of_range_variable(): - code_py = """ -CONSTANT: u8[3] = (24, 57, 80, ) - -@exported -def testEntry(x: u32) -> u8: - return CONSTANT[x] -""" - - with pytest.raises(wasmtime.Trap): - Suite(code_py).run_code(3) - @pytest.mark.integration_test def test_static_array_constant_too_few_values(): code_py = """ diff --git a/tests/integration/test_lang/test_struct.py b/tests/integration/test_lang/test_struct.py index 63e9b15..7f8f456 100644 --- a/tests/integration/test_lang/test_struct.py +++ b/tests/integration/test_lang/test_struct.py @@ -74,5 +74,5 @@ def testEntry(arg: Struct) -> (i32, i32, ): return arg.param """ - with pytest.raises(Type3Exception, match=type_ + r' must be tuple \(i32\) \(i32\) instead'): + with pytest.raises(Type3Exception, match=type_ + r' must be \(i32, i32, \) instead'): Suite(code_py).run_code() diff --git a/tests/integration/test_lang/test_subscriptable.py b/tests/integration/test_lang/test_subscriptable.py new file mode 100644 index 0000000..61d00e5 --- /dev/null +++ b/tests/integration/test_lang/test_subscriptable.py @@ -0,0 +1,116 @@ +import pytest +import wasmtime + +from phasm.type3.entry import Type3Exception + +from ..helpers import Suite + + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_, in_put, exp_result', [ + ('(u8, u8, )', (45, 46), 45, ), + ('u8[2]', (45, 46), 45, ), + ('bytes', b'This is a test', 84) +]) +def test_subscript_0(type_, in_put, exp_result): + code_py = f""" +@exported +def testEntry(f: {type_}) -> u8: + return f[0] +""" + + result = Suite(code_py).run_code(in_put) + + assert exp_result == result.returned_value + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_, in_put, exp_result', [ + ('(u8, u8, )', (45, 46), 45, ), + ('u8[2]', (45, 46), 45, ), + ('bytes', b'This is a test', 84) +]) +def test_subscript_invalid_type(type_, in_put, exp_result): + code_py = f""" +@exported +def testEntry(f: {type_}) -> u32: + return f[0] +""" + + with pytest.raises(Type3Exception, match='u32 must be u8 instead'): + Suite(code_py).run_code(in_put) + +@pytest.mark.integration_test +def test_subscript_tuple_must_be_literal(): + code_py = """ +@exported +def testEntry(x: (u8, u32, u64), y: u8) -> u64: + return x[y] +""" + + with pytest.raises(Type3Exception, match='Must index with literal'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +def test_subscript_tuple_must_be_int(): + code_py = """ +@exported +def testEntry(x: (u8, u32, u64)) -> u64: + return x[0.0] +""" + + with pytest.raises(Type3Exception, match='Must index with integer literal'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_, in_put', [ + ('(u8, u8, )', (45, 46), ), + ('u8[2]', (45, 46), ), + # bytes isn't known at runtime so works like normal +]) +def test_subscript_oob_constant_low(type_, in_put): + code_py = f""" +@exported +def testEntry(x: {type_}) -> u8: + return x[-1] +""" + + with pytest.raises(Type3Exception, match='Tuple index out of range'): + Suite(code_py).run_code(in_put) + +@pytest.mark.integration_test +def test_subscript_oob_constant_high(): + code_py = """ +@exported +def testEntry(x: (u8, u32, u64)) -> u64: + return x[4] +""" + + with pytest.raises(Type3Exception, match='Tuple index out of range'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +@pytest.mark.parametrize('type_, in_put', [ + # Cannot Subscript tuple without a constant + ('u8[2]', (45, 46), ), + ('bytes', b'This is a test', ), +]) +def test_subscript_oob_normal(type_, in_put): + code_py = f""" +@exported +def testEntry(x: {type_}, y: u32) -> u8: + return x[y] +""" + + with pytest.raises(wasmtime.Trap): + Suite(code_py).run_code(in_put, 255) + +@pytest.mark.integration_test +def test_subscript_not_subscriptable(): + code_py = """ +@exported +def testEntry(x: u8) -> u8: + return x[0] +""" + + with pytest.raises(Type3Exception, match='u8 cannot be subscripted'): + Suite(code_py).run_code() diff --git a/tests/integration/test_lang/test_tuple.py b/tests/integration/test_lang/test_tuple.py index cef2824..3b4aebc 100644 --- a/tests/integration/test_lang/test_tuple.py +++ b/tests/integration/test_lang/test_tuple.py @@ -70,25 +70,3 @@ CONSTANT: (u32, u8, u8, ) = (24, 4000, 1, ) with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_tuple_must_use_literal_for_indexing(): - code_py = """ -@exported -def testEntry(x: (u8, u32, u64), y: u8) -> u64: - return x[y] -""" - - with pytest.raises(Type3Exception, match='Must index with literal'): - Suite(code_py).run_code() - -@pytest.mark.integration_test -def test_tuple_must_use_integer_for_indexing(): - code_py = """ -@exported -def testEntry(x: (u8, u32, u64)) -> u64: - return x[0.0] -""" - - with pytest.raises(Type3Exception, match='Must index with integer literal'): - Suite(code_py).run_code()