diff --git a/phasm/compiler.py b/phasm/compiler.py index 9d8cc0e..3dc9837 100644 --- a/phasm/compiler.py +++ b/phasm/compiler.py @@ -8,6 +8,7 @@ from . import codestyle, ourlang, prelude, 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 functions as type3functions from .type3 import placeholders as type3placeholders from .type3 import typeclasses as type3classes from .type3 import types as type3types @@ -335,7 +336,7 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> # Store each element individually offset = 0 - for element, exp_type3 in zip(inp.elements, args): + for element, exp_type3 in zip(inp.elements, args, strict=True): if isinstance(exp_type3, type3placeholders.PlaceholderForType): assert exp_type3.resolve_as is not None assert isinstance(exp_type3.resolve_as, type3types.Type3) @@ -434,10 +435,10 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: assert isinstance(inp.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR - type_var_map: Dict[type3classes.TypeVariable, type3types.Type3] = {} + type_var_map: Dict[type3functions.TypeVariable, type3types.Type3] = {} - for type_var, arg_expr in zip(inp.operator.signature, [inp.left, inp.right, inp]): - if not isinstance(type_var, type3classes.TypeVariable): + for type_var, arg_expr in zip(inp.operator.signature.args, [inp.left, inp.right, inp], strict=True): + if not isinstance(type_var, type3functions.TypeVariable): # Fixed type, not part of the lookup requirements continue @@ -476,8 +477,8 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: # FIXME: Duplicate code with BinaryOp type_var_map = {} - for type_var, arg_expr in zip(inp.function.signature, inp.arguments + [inp]): - if not isinstance(type_var, type3classes.TypeVariable): + for type_var, arg_expr in zip(inp.function.signature.args, inp.arguments + [inp], strict=True): + if not isinstance(type_var, type3functions.TypeVariable): # Fixed type, not part of the lookup requirements continue diff --git a/phasm/ourlang.py b/phasm/ourlang.py index 77168df..c08496e 100644 --- a/phasm/ourlang.py +++ b/phasm/ourlang.py @@ -5,6 +5,7 @@ import enum from typing import Dict, Iterable, List, Optional, Union from . import prelude +from .type3.functions import FunctionSignature, TypeVariableContext from .type3.placeholders import PlaceholderForType, Type3OrPlaceholder from .type3.typeclasses import Type3ClassMethod from .type3.types import Type3 @@ -300,21 +301,22 @@ class FunctionParam: name: str type3: Type3OrPlaceholder - def __init__(self, name: str, type3: Optional[Type3]) -> None: + def __init__(self, name: str, type3: Type3) -> None: self.name = name - self.type3 = PlaceholderForType([self]) if type3 is None else type3 + self.type3 = type3 class Function: """ A function processes input and produces output """ - __slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'returns_type3', 'posonlyargs', ) + __slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'signature', 'returns_type3', 'posonlyargs', ) name: str lineno: int exported: bool imported: Optional[str] statements: List[Statement] + signature: FunctionSignature returns_type3: Type3 posonlyargs: List[FunctionParam] @@ -324,6 +326,7 @@ class Function: self.exported = False self.imported = None self.statements = [] + self.signature = FunctionSignature(TypeVariableContext(), []) self.returns_type3 = prelude.none # FIXME: This could be a placeholder self.posonlyargs = [] @@ -354,12 +357,14 @@ class StructConstructor(Function): def __init__(self, struct_type3: Type3) -> None: super().__init__(f'@{struct_type3.name}@__init___@', -1) - self.returns_type3 = struct_type3 - st_args = prelude.struct.did_construct(struct_type3) assert st_args is not None for mem, typ in st_args.items(): self.posonlyargs.append(FunctionParam(mem, typ, )) + self.signature.args.append(typ) + + self.returns_type3 = struct_type3 + self.signature.args.append(struct_type3) self.struct_type3 = struct_type3 diff --git a/phasm/parser.py b/phasm/parser.py index ced7d6d..58dd180 100644 --- a/phasm/parser.py +++ b/phasm/parser.py @@ -159,9 +159,18 @@ class OurVisitor: _not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs') for arg in node.args.args: + if arg.annotation is None: + _raise_static_error(node, 'Must give a argument type') + + arg_type = self.visit_type(module, arg.annotation) + + # if isisntance(arg_type, TypeVariable): + # arg_type = PlaceHolderForType() + + function.signature.args.append(arg_type) function.posonlyargs.append(FunctionParam( arg.arg, - self.visit_type(module, arg.annotation) if arg.annotation else None, + arg_type, )) _not_implemented(not node.args.vararg, 'FunctionDef.args.vararg') @@ -202,11 +211,11 @@ class OurVisitor: else: function.imported = 'imports' - if node.returns is not None: # Note: `-> None` would be a ast.Constant - function.returns_type3 = self.visit_type(module, node.returns) - else: - # FIXME: Mostly works already, needs to fix Function.returns_type3 and have it updated - raise NotImplementedError('Function without an explicit return type') + if node.returns is None: # Note: `-> None` would be a ast.Constant + _raise_static_error(node, 'Must give a return type') + return_type = self.visit_type(module, node.returns) + function.signature.args.append(return_type) + function.returns_type3 = return_type _not_implemented(not node.type_comment, 'FunctionDef.type_comment') @@ -518,7 +527,7 @@ class OurVisitor: func = module.functions[node.func.id] - exp_arg_count = len(func.posonlyargs) if isinstance(func, Function) else len(func.signature) - 1 + exp_arg_count = len(func.signature.args) - 1 if exp_arg_count != len(node.args): _raise_static_error(node, f'Function {node.func.id} requires {exp_arg_count} arguments but {len(node.args)} are given') diff --git a/phasm/prelude/__init__.py b/phasm/prelude/__init__.py index 06eda3c..fe2a694 100644 --- a/phasm/prelude/__init__.py +++ b/phasm/prelude/__init__.py @@ -2,7 +2,8 @@ The prelude are all the builtin types, type classes and methods """ -from ..type3.typeclasses import Type3Class, TypeVariable, instance_type_class +from ..type3.functions import TypeVariable +from ..type3.typeclasses import Type3Class, instance_type_class from ..type3.types import ( Type3, TypeConstructor_StaticArray, diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index 26de593..718a8e3 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -186,7 +186,7 @@ class TupleMatchConstraint(ConstraintBase): return [ SameTypeConstraint(arg, oth_arg) - for arg, oth_arg in zip(self.args, tp_args) + for arg, oth_arg in zip(self.args, tp_args, strict=True) ] raise NotImplementedError(exp_type) @@ -369,11 +369,11 @@ class LiteralFitsConstraint(ConstraintBase): res.extend( LiteralFitsConstraint(x, y) - for x, y in zip(tp_args, self.literal.value) + for x, y in zip(tp_args, self.literal.value, strict=True) ) res.extend( SameTypeConstraint(x, y.type3) - for x, y in zip(tp_args, self.literal.value) + for x, y in zip(tp_args, self.literal.value, strict=True) ) return res @@ -417,11 +417,11 @@ class LiteralFitsConstraint(ConstraintBase): res.extend( LiteralFitsConstraint(x, y) - for x, y in zip(st_args.values(), self.literal.value) + for x, y in zip(st_args.values(), self.literal.value, strict=True) ) res.extend( SameTypeConstraint(x_t, y.type3, comment=f'{self.literal.struct_name}.{x_n}') - for (x_n, x_t, ), y in zip(st_args.items(), self.literal.value) + for (x_n, x_t, ), y in zip(st_args.items(), self.literal.value, strict=True) ) return res diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index 724993b..9a7d513 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -6,8 +6,9 @@ The constraints solver can then try to resolve all constraints. from typing import Generator, List from .. import ourlang, prelude +from . import functions as functions from . import placeholders as placeholders -from . import typeclasses as type3typeclasses +from . import typeclasses as typeclasses from . import types as type3types from .constraints import ( CanBeSubscriptedConstraint, @@ -55,81 +56,51 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: raise NotImplementedError(expression, inp, inp.operator) - if isinstance(inp, ourlang.BinaryOp): + if isinstance(inp, ourlang.BinaryOp) or isinstance(inp, ourlang.FunctionCall): + signature = inp.operator.signature if isinstance(inp, ourlang.BinaryOp) else inp.function.signature + arguments = [inp.left, inp.right] if isinstance(inp, ourlang.BinaryOp) else inp.arguments + + func_name = f'({inp.operator.name})' if isinstance(inp, ourlang.BinaryOp) else inp.function.name + type_var_map = { x: placeholders.PlaceholderForType([]) - for x in inp.operator.signature - if isinstance(x, type3typeclasses.TypeVariable) + for x in signature.args + if isinstance(x, functions.TypeVariable) } - yield from expression(ctx, inp.left) - yield from expression(ctx, inp.right) + for call_arg in arguments: + yield from expression(ctx, call_arg) - for type_var in inp.operator.type3_class.args: + for type_var, constraint_list in signature.context.constraints.items(): assert type_var in type_var_map # When can this happen? - yield MustImplementTypeClassConstraint( - inp.operator.type3_class, - type_var_map[type_var], - ) + for constraint in constraint_list: + if isinstance(constraint, functions.TypeVariableConstraint_TypeHasTypeClass): + yield MustImplementTypeClassConstraint( + constraint.type_class3, + type_var_map[type_var], + ) + continue - for sig_part, arg_expr in zip(inp.operator.signature, [inp.left, inp.right, inp]): - if isinstance(sig_part, type3typeclasses.TypeVariable): - yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3) + raise NotImplementedError(constraint) + + for arg_no, (sig_part, arg_expr) in enumerate(zip(signature.args, arguments + [inp], strict=True)): + if arg_no == len(arguments): + comment = f'The type of a function call to {func_name} is the same as the type that the function returns' + else: + comment = f'The type of the value passed to argument {arg_no} of function {func_name} should match the type of that argument' + + if isinstance(sig_part, functions.TypeVariable): + yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3, comment=comment) continue if isinstance(sig_part, type3types.Type3): - yield SameTypeConstraint(sig_part, arg_expr.type3) + yield SameTypeConstraint(sig_part, arg_expr.type3, comment=comment) continue raise NotImplementedError(sig_part) return - if isinstance(inp, ourlang.FunctionCall): - if isinstance(inp.function, type3typeclasses.Type3ClassMethod): - # FIXME: Duplicate code with BinaryOp - - type_var_map = { - x: placeholders.PlaceholderForType([]) - for x in inp.function.signature - if isinstance(x, type3typeclasses.TypeVariable) - } - - for call_arg in inp.arguments: - yield from expression(ctx, call_arg) - - for type_var in inp.function.type3_class.args: - assert type_var in type_var_map # When can this happen? - - yield MustImplementTypeClassConstraint( - inp.function.type3_class, - type_var_map[type_var], - ) - - for sig_part, arg_expr in zip(inp.function.signature, inp.arguments + [inp]): - if isinstance(sig_part, type3typeclasses.TypeVariable): - yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3) - continue - - if isinstance(sig_part, type3types.Type3): - yield SameTypeConstraint(sig_part, arg_expr.type3) - continue - - raise NotImplementedError(sig_part) - return - - yield SameTypeConstraint(inp.function.returns_type3, inp.type3, - comment=f'The type of a function call to {inp.function.name} is the same as the type that the function returns') - - assert len(inp.arguments) == len(inp.function.posonlyargs) # FIXME: Make this a Constraint - - for fun_arg, call_arg in zip(inp.function.posonlyargs, inp.arguments): - yield from expression(ctx, call_arg) - yield SameTypeConstraint(fun_arg.type3, call_arg.type3, - comment=f'The type of the value passed to argument {fun_arg.name} of function {inp.function.name} should match the type of that argument') - - return - if isinstance(inp, ourlang.TupleInstantiation): r_type = [] for arg in inp.elements: diff --git a/phasm/type3/functions.py b/phasm/type3/functions.py new file mode 100644 index 0000000..4536178 --- /dev/null +++ b/phasm/type3/functions.py @@ -0,0 +1,67 @@ +from typing import TYPE_CHECKING, Any, Iterable, List, Union + +if TYPE_CHECKING: + from .typeclasses import Type3Class + from .types import Type3 + + +class TypeVariable: + """ + Types variable are used in function definition. + + They are used in places where you don't know the exact type. + They are different from PlaceholderForType, as those are instanced + during type checking. These type variables are used solely in the + function's definition + """ + __slots__ = ('letter', ) + + letter: str + + def __init__(self, letter: str) -> None: + assert len(letter) == 1, f'{letter} is not a valid type variable' + self.letter = letter + + def __hash__(self) -> int: + return hash(self.letter) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, TypeVariable): + raise NotImplementedError + + return self.letter == other.letter + + def __repr__(self) -> str: + return f'TypeVariable({repr(self.letter)})' + +class TypeVariableConstraintBase: + __slots__ = () + +class TypeVariableConstraint_TypeHasTypeClass(TypeVariableConstraintBase): + __slots__ = ('type_class3', ) + + def __init__(self, type_class3: 'Type3Class') -> None: + self.type_class3 = type_class3 + +class TypeVariableContext: + __slots__ = ('constraints', ) + + constraints: dict[TypeVariable, list[TypeVariableConstraintBase]] + + def __init__(self) -> None: + self.constraints = {} + + def __copy__(self) -> 'TypeVariableContext': + result = TypeVariableContext() + result.constraints.update(self.constraints) + return result + +class FunctionSignature: + __slots__ = ('context', 'args', ) + + context: TypeVariableContext + args: List[Union['Type3', TypeVariable]] + + def __init__(self, context: TypeVariableContext, args: Iterable[Union['Type3', TypeVariable]]) -> None: + self.context = context.__copy__() + self.args = list(args) diff --git a/phasm/type3/typeclasses.py b/phasm/type3/typeclasses.py index 01f3d02..84e4923 100644 --- a/phasm/type3/typeclasses.py +++ b/phasm/type3/typeclasses.py @@ -1,64 +1,26 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Union +from .functions import ( + FunctionSignature, + TypeVariable, + TypeVariableConstraint_TypeHasTypeClass, + TypeVariableContext, +) from .types import Type3, TypeConstructor, TypeConstructor_Struct -class TypeVariable: - __slots__ = ('letter', ) - - letter: str - - def __init__(self, letter: str) -> None: - assert len(letter) == 1, f'{letter} is not a valid type variable' - self.letter = letter - - def __hash__(self) -> int: - return hash(self.letter) - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, TypeVariable): - raise NotImplementedError - - return self.letter == other.letter - - def __repr__(self) -> str: - return f'TypeVariable({repr(self.letter)})' - -class TypeReference: - __slots__ = ('name', ) - - name: str - - def __init__(self, name: str) -> None: - assert len(name) > 1, f'{name} is not a valid type reference' - self.name = name - - def __hash__(self) -> int: - return hash(self.name) - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, TypeReference): - raise NotImplementedError - - return self.name == other.name - - def __repr__(self) -> str: - return f'TypeReference({repr(self.name)})' - class Type3ClassMethod: - __slots__ = ('type3_class', 'name', 'signature', ) + __slots__ = ('name', 'signature', ) - type3_class: 'Type3Class' name: str - signature: List[Union[Type3, TypeVariable]] + signature: FunctionSignature - def __init__(self, type3_class: 'Type3Class', name: str, signature: Iterable[Union[Type3, TypeVariable]]) -> None: - self.type3_class = type3_class + def __init__(self, name: str, signature: FunctionSignature) -> None: self.name = name - self.signature = list(signature) + self.signature = signature def __repr__(self) -> str: - return f'Type3ClassMethod({repr(self.type3_class)}, {repr(self.name)}, {repr(self.signature)})' + return f'Type3ClassMethod({repr(self.name)}, {repr(self.signature)})' class Type3Class: __slots__ = ('name', 'args', 'methods', 'operators', 'inherited_classes', ) @@ -79,12 +41,26 @@ class Type3Class: ) -> None: self.name = name self.args = list(args) + + context = TypeVariableContext() + for arg in args: + context.constraints[arg] = [ + TypeVariableConstraint_TypeHasTypeClass(self) + ] + + # FIXME: Multi parameter class types + # To fix this, realise that an instantiation of a multi paramater type class + # means that the instantiation depends on the combination of type classes + # and so we can't store the type classes on the types anymore + # This also means constraints should store a tuple of types as its key + assert len(context.constraints) <= 1 + self.methods = { - k: Type3ClassMethod(self, k, v) + k: Type3ClassMethod(k, FunctionSignature(context, v)) for k, v in methods.items() } self.operators = { - k: Type3ClassMethod(self, k, v) + k: Type3ClassMethod(k, FunctionSignature(context, v)) for k, v in operators.items() } self.inherited_classes = inherited_classes or [] diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index f682d0b..a6f5994 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -64,7 +64,7 @@ class Suite: write_header(sys.stderr, 'Memory (pre alloc)') runner.interpreter_dump_memory(sys.stderr) - for arg, arg_typ in zip(args, func_args): + for arg, arg_typ in zip(args, func_args, strict=True): assert not isinstance(arg_typ, type3placeholders.PlaceholderForType), \ 'Cannot call polymorphic function from outside' @@ -221,7 +221,7 @@ def _allocate_memory_stored_value( assert len(val) == len(tp_args) offset = adr - for val_el_val, val_el_typ in zip(val, tp_args): + for val_el_val, val_el_typ in zip(val, tp_args, strict=True): assert not isinstance(val_el_typ, type3placeholders.PlaceholderForType) offset += _write_memory_stored_value(runner, offset, val_el_typ, val_el_val) @@ -424,7 +424,7 @@ def _load_tuple_from_address(runner: runners.RunnerBase, typ_args: tuple[type3ty return tuple( _unpack(runner, arg_typ, arg_bytes) - for arg_typ, arg_bytes in zip(typ_args, _split_read_bytes(read_bytes, arg_sizes)) + for arg_typ, arg_bytes in zip(typ_args, _split_read_bytes(read_bytes, arg_sizes), strict=True) ) def _load_struct_from_address(runner: runners.RunnerBase, st_args: dict[str, type3types.Type3], adr: int) -> Any: @@ -444,5 +444,5 @@ def _load_struct_from_address(runner: runners.RunnerBase, st_args: dict[str, typ 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)) + for arg_name, arg_typ, arg_bytes in zip(name_list, typ_list, _split_read_bytes(read_bytes, arg_sizes), strict=True) } diff --git a/tests/integration/test_lang/generator.md b/tests/integration/test_lang/generator.md index 81f256a..529aab5 100644 --- a/tests/integration/test_lang/generator.md +++ b/tests/integration/test_lang/generator.md @@ -333,11 +333,11 @@ def testEntry() -> i32: if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'): expect_type_error( TYPE + ' must be (u32, ) instead', - 'The type of the value passed to argument x of function helper should match the type of that argument', + 'The type of the value passed to argument 0 of function helper should match the type of that argument', ) else: expect_type_error( 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', + 'The type of the value passed to argument 0 of function helper should match the type of that argument', ) ``` diff --git a/tests/integration/test_lang/test_builtins.py b/tests/integration/test_lang/test_builtins.py index 22ed10c..ce45cef 100644 --- a/tests/integration/test_lang/test_builtins.py +++ b/tests/integration/test_lang/test_builtins.py @@ -1,27 +1,8 @@ -import sys - import pytest -from ..helpers import Suite, write_header -from ..runners import RunnerWasmtime +from ..helpers import Suite -def setup_interpreter(phash_code: str) -> RunnerWasmtime: - runner = RunnerWasmtime(phash_code) - - runner.parse() - runner.compile_ast() - runner.compile_wat() - runner.interpreter_setup() - runner.interpreter_load() - - write_header(sys.stderr, 'Phasm') - runner.dump_phasm_code(sys.stderr) - write_header(sys.stderr, 'Assembly') - runner.dump_wasm_wat(sys.stderr) - - return runner - @pytest.mark.integration_test def test_foldl_1(): code_py = """ diff --git a/tests/integration/test_lang/test_typeclass.py b/tests/integration/test_lang/test_typeclass.py new file mode 100644 index 0000000..548709a --- /dev/null +++ b/tests/integration/test_lang/test_typeclass.py @@ -0,0 +1,20 @@ +import pytest + +from phasm.exceptions import StaticError + +from ..helpers import Suite + + +@pytest.mark.integration_test +def test_must_give_type_annotations(): + code_py = """ +def is_equal(lft, rgt) -> bool: + return lft == rgt + +def testEntry() -> bool: + return is_equal(5, 15) +""" + suite = Suite(code_py) + + with pytest.raises(StaticError, match='Must give a argument type'): + suite.run_code()