From a2e1dfd799515d2ddf286a301f03ada295ef2c68 Mon Sep 17 00:00:00 2001 From: "Johan B.W. de Vries" Date: Sun, 27 Apr 2025 17:45:13 +0200 Subject: [PATCH] Reworks type class instantiation Before, a type class was a property of a type. But that doesn't make any sense for multi parameter type classes. Had to make a hacky way for type classes with type constructors. --- phasm/compiler.py | 10 ++-- phasm/parser.py | 2 +- phasm/prelude/__init__.py | 56 ++++++++++++------ phasm/type3/constraints.py | 53 +++++++++++------ phasm/type3/constraintsgenerator.py | 23 ++++---- phasm/type3/functions.py | 27 ++++++--- phasm/type3/typeclasses.py | 24 ++------ phasm/type3/types.py | 67 ++++++---------------- tests/integration/helpers.py | 2 +- tests/integration/test_lang/test_eq.py | 4 +- tests/integration/test_lang/test_natnum.py | 2 +- 11 files changed, 137 insertions(+), 133 deletions(-) diff --git a/phasm/compiler.py b/phasm/compiler.py index 3dc9837..2a9e7ad 100644 --- a/phasm/compiler.py +++ b/phasm/compiler.py @@ -296,7 +296,7 @@ def type3(inp: type3placeholders.Type3OrPlaceholder) -> wasm.WasmType: # And pointers are i32 return wasm.WasmTypeInt32() - if prelude.InternalPassAsPointer in inp.classes: + if (prelude.InternalPassAsPointer, (inp, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: return wasm.WasmTypeInt32() raise NotImplementedError(type3, inp) @@ -344,7 +344,7 @@ def tuple_instantiation(wgn: WasmGenerator, inp: ourlang.TupleInstantiation) -> assert element.type3 == exp_type3 - if prelude.InternalPassAsPointer in exp_type3.classes: + if (prelude.InternalPassAsPointer, (exp_type3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: mtyp = 'i32' else: assert isinstance(exp_type3, type3types.Type3), NotImplementedError('Tuple of applied types / structs') @@ -413,7 +413,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: if isinstance(inp.variable, ourlang.ModuleConstantDef): assert isinstance(inp.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR - if prelude.InternalPassAsPointer in inp.type3.classes: + if (prelude.InternalPassAsPointer, (inp.type3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: assert isinstance(inp.variable.constant, (ourlang.ConstantBytes, ourlang.ConstantStruct, ourlang.ConstantTuple, )) address = inp.variable.constant.data_block.address @@ -560,7 +560,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None: expression(wgn, inp.varref) - if prelude.InternalPassAsPointer in el_type.classes: + if (prelude.InternalPassAsPointer, (el_type, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: mtyp = 'i32' else: assert isinstance(el_type, type3types.Type3), NotImplementedError('Tuple of applied types / structs') @@ -972,7 +972,7 @@ def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstruc # Store each member individually for memname, mtyp3 in st_args.items(): mtyp: Optional[str] - if prelude.InternalPassAsPointer in mtyp3.classes: + if (prelude.InternalPassAsPointer, (mtyp3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: mtyp = 'i32' else: mtyp = LOAD_STORE_TYPE_MAP.get(mtyp3.name) diff --git a/phasm/parser.py b/phasm/parser.py index 58dd180..831b435 100644 --- a/phasm/parser.py +++ b/phasm/parser.py @@ -247,7 +247,7 @@ class OurVisitor: members[stmt.target.id] = self.visit_type(module, stmt.annotation) - return StructDefinition(prelude.struct(node.name, members, set()), node.lineno) + return StructDefinition(prelude.struct(node.name, members), node.lineno) def pre_visit_Module_AnnAssign(self, module: Module, node: ast.AnnAssign) -> ModuleConstantDef: if not isinstance(node.target, ast.Name): diff --git a/phasm/prelude/__init__.py b/phasm/prelude/__init__.py index fe2a694..9d0ce55 100644 --- a/phasm/prelude/__init__.py +++ b/phasm/prelude/__init__.py @@ -3,7 +3,7 @@ The prelude are all the builtin types, type classes and methods """ from ..type3.functions import TypeVariable -from ..type3.typeclasses import Type3Class, instance_type_class +from ..type3.typeclasses import Type3Class from ..type3.types import ( Type3, TypeConstructor_StaticArray, @@ -11,40 +11,50 @@ from ..type3.types import ( TypeConstructor_Tuple, ) -none = Type3('none', []) +PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: set[tuple[Type3Class, tuple[Type3, ...]]] = set() + + +def instance_type_class(cls: Type3Class, typ: Type3) -> None: + global PRELUDE_TYPE_CLASS_INSTANCES_EXISTING + + # TODO: Check for required existing instantiations + + PRELUDE_TYPE_CLASS_INSTANCES_EXISTING.add((cls, (typ, ), )) + +none = Type3('none') """ The none type, for when functions simply don't return anything. e.g., IO(). """ -bool_ = Type3('bool', []) +bool_ = Type3('bool') """ The bool type, either True or False Suffixes with an underscores, as it's a Python builtin """ -u8 = Type3('u8', []) +u8 = Type3('u8') """ The unsigned 8-bit integer type. Operations on variables employ modular arithmetic, with modulus 2^8. """ -u32 = Type3('u32', []) +u32 = Type3('u32') """ The unsigned 32-bit integer type. Operations on variables employ modular arithmetic, with modulus 2^32. """ -u64 = Type3('u64', []) +u64 = Type3('u64') """ The unsigned 64-bit integer type. Operations on variables employ modular arithmetic, with modulus 2^64. """ -i8 = Type3('i8', []) +i8 = Type3('i8') """ The signed 8-bit integer type. @@ -52,7 +62,7 @@ Operations on variables employ modular arithmetic, with modulus 2^8, but with the middel point being 0. """ -i32 = Type3('i32', []) +i32 = Type3('i32') """ The unsigned 32-bit integer type. @@ -60,7 +70,7 @@ Operations on variables employ modular arithmetic, with modulus 2^32, but with the middel point being 0. """ -i64 = Type3('i64', []) +i64 = Type3('i64') """ The unsigned 64-bit integer type. @@ -68,22 +78,25 @@ Operations on variables employ modular arithmetic, with modulus 2^64, but with the middel point being 0. """ -f32 = Type3('f32', []) +f32 = Type3('f32') """ A 32-bits IEEE 754 float, of 32 bits width. """ -f64 = Type3('f64', []) +f64 = Type3('f64') """ A 32-bits IEEE 754 float, of 64 bits width. """ -bytes_ = Type3('bytes', []) +bytes_ = Type3('bytes') """ This is a runtime-determined length piece of memory that can be indexed at runtime. """ -static_array = TypeConstructor_StaticArray('static_array', [], []) +def sa_on_create(typ: Type3) -> None: + instance_type_class(InternalPassAsPointer, typ) + +static_array = TypeConstructor_StaticArray('static_array', on_create=sa_on_create) """ A type constructor. @@ -93,7 +106,10 @@ It should be applied with one argument. It has a runtime-dynamic length of the same type repeated. """ -tuple_ = TypeConstructor_Tuple('tuple', [], []) +def tp_on_create(typ: Type3) -> None: + instance_type_class(InternalPassAsPointer, typ) + +tuple_ = TypeConstructor_Tuple('tuple', on_create=tp_on_create) """ This is a fixed length piece of memory. @@ -101,7 +117,10 @@ It should be applied with zero or more arguments. It has a compile time determined length, and each argument can be different. """ -struct = TypeConstructor_Struct('struct', [], []) +def st_on_create(typ: Type3) -> None: + instance_type_class(InternalPassAsPointer, typ) + +struct = TypeConstructor_Struct('struct', on_create=st_on_create) """ This is like a tuple, but each argument is named, so that developers can get and set fields by name. @@ -130,9 +149,9 @@ Internal type class to keep track which types we pass arounds as a pointer. """ instance_type_class(InternalPassAsPointer, bytes_) -instance_type_class(InternalPassAsPointer, static_array) -instance_type_class(InternalPassAsPointer, tuple_) -instance_type_class(InternalPassAsPointer, struct) +# instance_type_class(InternalPassAsPointer, static_array) +# instance_type_class(InternalPassAsPointer, tuple_) +# instance_type_class(InternalPassAsPointer, struct) Eq = Type3Class('Eq', [a], methods={}, operators={ '==': [a, a, bool_], @@ -148,7 +167,6 @@ instance_type_class(Eq, i32) instance_type_class(Eq, i64) instance_type_class(Eq, f32) instance_type_class(Eq, f64) -instance_type_class(Eq, static_array) Ord = Type3Class('Ord', [a], methods={ 'min': [a, a, a], diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index 718a8e3..256daa0 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -45,7 +45,13 @@ class Context: Context for constraints """ - __slots__ = () + __slots__ = ('type_class_instances_existing', ) + + # Constraint_TypeClassInstanceExists + type_class_instances_existing: set[tuple[typeclasses.Type3Class, tuple[types.Type3, ...]]] + + def __init__(self) -> None: + self.type_class_instances_existing = set() class ConstraintBase: """ @@ -239,49 +245,64 @@ class MustImplementTypeClassConstraint(ConstraintBase): """ A type must implement a given type class """ - __slots__ = ('type_class3', 'type3', ) + __slots__ = ('context', 'type_class3', 'types', ) + context: Context type_class3: Union[str, typeclasses.Type3Class] - type3: placeholders.Type3OrPlaceholder + types: list[placeholders.Type3OrPlaceholder] DATA = { 'bytes': {'Foldable'}, } - def __init__(self, type_class3: Union[str, typeclasses.Type3Class], type3: placeholders.Type3OrPlaceholder, comment: Optional[str] = None) -> None: + def __init__(self, context: Context, type_class3: Union[str, typeclasses.Type3Class], types: list[placeholders.Type3OrPlaceholder], comment: Optional[str] = None) -> None: super().__init__(comment=comment) + self.context = context self.type_class3 = type_class3 - self.type3 = type3 + self.types = types def check(self) -> CheckResult: - typ = self.type3 - if isinstance(typ, placeholders.PlaceholderForType) and typ.resolve_as is not None: - typ = typ.resolve_as + typ_list = [] + for typ in self.types: + if isinstance(typ, placeholders.PlaceholderForType) and typ.resolve_as is not None: + typ = typ.resolve_as - if isinstance(typ, placeholders.PlaceholderForType): - return RequireTypeSubstitutes() + if isinstance(typ, placeholders.PlaceholderForType): + return RequireTypeSubstitutes() + + typ_list.append(typ) + + assert len(typ_list) == len(self.types) if isinstance(self.type_class3, typeclasses.Type3Class): - if self.type_class3 in typ.classes: + key = (self.type_class3, tuple(typ_list), ) + if key in self.context.type_class_instances_existing: return None else: - if self.type_class3 in self.__class__.DATA.get(typ.name, set()): + if self.type_class3 in self.__class__.DATA.get(typ_list[0].name, set()): return None - return Error(f'{typ.name} does not implement the {self.type_class3} type class') + typ_cls_name = self.type_class3 if isinstance(self.type_class3, str) else self.type_class3.name + typ_name_list = ' '.join(x.name for x in typ_list) + return Error(f'Missing type class instantation: {typ_cls_name} {typ_name_list}') def human_readable(self) -> HumanReadableRet: + keys = { + f'type{idx}': typ + for idx, typ in enumerate(self.types) + } + return ( - '{type3} derives {type_class3}', + 'Exists instance {type_class3} ' + ' '.join(f'{{{x}}}' for x in keys), { 'type_class3': str(self.type_class3), - 'type3': self.type3, + **keys, }, ) def __repr__(self) -> str: - return f'MustImplementTypeClassConstraint({repr(self.type_class3)}, {repr(self.type3)}, comment={repr(self.comment)})' + return f'MustImplementTypeClassConstraint({repr(self.type_class3)}, {repr(self.types)}, comment={repr(self.comment)})' class LiteralFitsConstraint(ConstraintBase): """ diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index 9a7d513..5d0d99e 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -25,6 +25,7 @@ ConstraintGenerator = Generator[ConstraintBase, None, None] def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase]: ctx = Context() + ctx.type_class_instances_existing.update(prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING) return [*module(ctx, inp)] @@ -71,18 +72,16 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: for call_arg in arguments: yield from expression(ctx, call_arg) - for type_var, constraint_list in signature.context.constraints.items(): - assert type_var in type_var_map # When can this happen? + for constraint in signature.context.constraints: + if isinstance(constraint, functions.Constraint_TypeClassInstanceExists): + yield MustImplementTypeClassConstraint( + ctx, + constraint.type_class3, + [type_var_map[x] for x in constraint.types], + ) + continue - for constraint in constraint_list: - if isinstance(constraint, functions.TypeVariableConstraint_TypeHasTypeClass): - yield MustImplementTypeClassConstraint( - constraint.type_class3, - type_var_map[type_var], - ) - continue - - raise NotImplementedError(constraint) + raise NotImplementedError(constraint) for arg_no, (sig_part, arg_expr) in enumerate(zip(signature.args, arguments + [inp], strict=True)): if arg_no == len(arguments): @@ -138,7 +137,7 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: yield SameTypeConstraint(inp.func.posonlyargs[0].type3, inp.func.returns_type3, inp.base.type3, inp.type3, comment='foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b') - yield MustImplementTypeClassConstraint('Foldable', inp.iter.type3) + yield MustImplementTypeClassConstraint(ctx, 'Foldable', [inp.iter.type3]) return diff --git a/phasm/type3/functions.py b/phasm/type3/functions.py index 4536178..d0147bc 100644 --- a/phasm/type3/functions.py +++ b/phasm/type3/functions.py @@ -34,26 +34,37 @@ class TypeVariable: def __repr__(self) -> str: return f'TypeVariable({repr(self.letter)})' -class TypeVariableConstraintBase: +class ConstraintBase: __slots__ = () -class TypeVariableConstraint_TypeHasTypeClass(TypeVariableConstraintBase): - __slots__ = ('type_class3', ) +class Constraint_TypeClassInstanceExists(ConstraintBase): + __slots__ = ('type_class3', 'types', ) - def __init__(self, type_class3: 'Type3Class') -> None: + type_class3: 'Type3Class' + types: list[TypeVariable] + + def __init__(self, type_class3: 'Type3Class', types: Iterable[TypeVariable]) -> None: self.type_class3 = type_class3 + self.types = list(types) + + # Sanity check. AFAIK, if you have a multi-parameter type class, + # you can only add a constraint by supplying types for all variables + assert len(self.type_class3.args) == len(self.types) class TypeVariableContext: - __slots__ = ('constraints', ) + __slots__ = ('variables', 'constraints', ) - constraints: dict[TypeVariable, list[TypeVariableConstraintBase]] + variables: set[TypeVariable] + constraints: list[ConstraintBase] def __init__(self) -> None: - self.constraints = {} + self.variables = set() + self.constraints = [] def __copy__(self) -> 'TypeVariableContext': result = TypeVariableContext() - result.constraints.update(self.constraints) + result.variables.update(self.variables) + result.constraints.extend(self.constraints) return result class FunctionSignature: diff --git a/phasm/type3/typeclasses.py b/phasm/type3/typeclasses.py index 84e4923..dcb63b6 100644 --- a/phasm/type3/typeclasses.py +++ b/phasm/type3/typeclasses.py @@ -1,12 +1,12 @@ -from typing import Any, Dict, Iterable, List, Mapping, Optional, Union +from typing import Dict, Iterable, List, Mapping, Optional, Union from .functions import ( + Constraint_TypeClassInstanceExists, FunctionSignature, TypeVariable, - TypeVariableConstraint_TypeHasTypeClass, TypeVariableContext, ) -from .types import Type3, TypeConstructor, TypeConstructor_Struct +from .types import Type3 class Type3ClassMethod: @@ -43,17 +43,7 @@ class Type3Class: 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 + context.constraints.append(Constraint_TypeClassInstanceExists(self, args)) self.methods = { k: Type3ClassMethod(k, FunctionSignature(context, v)) @@ -67,9 +57,3 @@ class Type3Class: def __repr__(self) -> str: return self.name - -def instance_type_class(cls: Type3Class, typ: Type3 | TypeConstructor[Any] | TypeConstructor_Struct) -> None: - if isinstance(typ, Type3): - typ.classes.add(cls) - else: - typ.type_classes.add(cls) diff --git a/phasm/type3/types.py b/phasm/type3/types.py index b815f02..6dad13c 100644 --- a/phasm/type3/types.py +++ b/phasm/type3/types.py @@ -2,18 +2,13 @@ Contains the final types for use in Phasm, as well as construtors. """ from typing import ( - TYPE_CHECKING, Any, + Callable, Generic, - Iterable, - Set, Tuple, TypeVar, ) -if TYPE_CHECKING: - from .typeclasses import Type3Class - class KindArgument: pass @@ -25,32 +20,18 @@ class Type3(KindArgument): (Having a separate name makes it easier to distinguish from Python's Type) """ - __slots__ = ('name', 'classes', ) + __slots__ = ('name', ) name: str """ The name of the string, as parsed and outputted by codestyle. """ - classes: Set['Type3Class'] - """ - The type classes that this type implements - """ - - def __init__(self, name: str, classes: Iterable['Type3Class']) -> None: + def __init__(self, name: str) -> None: self.name = name - self.classes = set(classes) - - for cls in self.classes: - for inh_cls in cls.inherited_classes: - if inh_cls not in self.classes: - raise Exception( - f'No instance for ({inh_cls} {self.name})' - f'; required for ({cls} {self.name})' - ) def __repr__(self) -> str: - return f'Type3({repr(self.name)}, {repr(self.classes)})' + return f'Type3({repr(self.name)})' def __str__(self) -> str: return self.name @@ -118,21 +99,16 @@ class TypeConstructor(Generic[T]): """ Base class for type construtors """ - __slots__ = ('name', 'classes', 'type_classes', '_cache', '_reverse_cache') + __slots__ = ('name', 'on_create', '_cache', '_reverse_cache') name: str """ The name of the type constructor """ - classes: Set['Type3Class'] + on_create: Callable[[Type3], None] """ - The type classes that this constructor implements - """ - - type_classes: Set['Type3Class'] - """ - The type classes that the constructed types implement + Who to let know if a type is created """ _cache: dict[T, Type3] @@ -146,10 +122,9 @@ class TypeConstructor(Generic[T]): Sometimes we need to know the key that created a type. """ - def __init__(self, name: str, classes: Iterable['Type3Class'], type_classes: Iterable['Type3Class']) -> None: + def __init__(self, name: str, on_create: Callable[[Type3], None]) -> None: self.name = name - self.classes = set(classes) - self.type_classes = set(type_classes) + self.on_create = on_create self._cache = {} self._reverse_cache = {} @@ -175,8 +150,9 @@ class TypeConstructor(Generic[T]): """ result = self._cache.get(key, None) if result is None: - self._cache[key] = result = Type3(self.make_name(key), self.type_classes) + self._cache[key] = result = Type3(self.make_name(key)) self._reverse_cache[result] = key + self.on_create(result) return result @@ -225,21 +201,16 @@ class TypeConstructor_Struct: """ Base class for type construtors """ - __slots__ = ('name', 'classes', 'type_classes', '_cache', '_reverse_cache') + __slots__ = ('name', 'on_create', '_cache', '_reverse_cache') name: str """ The name of the type constructor """ - classes: Set['Type3Class'] + on_create: Callable[[Type3], None] """ - The type classes that this constructor implements - """ - - type_classes: Set['Type3Class'] - """ - The type classes that the constructed types implement + Who to let know if a type is created """ _cache: dict[str, Type3] @@ -254,10 +225,9 @@ class TypeConstructor_Struct: used for making the type """ - def __init__(self, name: str, classes: Iterable['Type3Class'], type_classes: Iterable['Type3Class']) -> None: + def __init__(self, name: str, on_create: Callable[[Type3], None]) -> None: self.name = name - self.classes = set(classes) - self.type_classes = set(type_classes) + self.on_create = on_create self._cache = {} self._reverse_cache = {} @@ -270,8 +240,9 @@ class TypeConstructor_Struct: """ return self._reverse_cache.get(typ) - def __call__(self, name: str, args: dict[str, Type3], classes: Set['Type3Class']) -> Type3: - result = Type3(name, classes | self.type_classes) + def __call__(self, name: str, args: dict[str, Type3]) -> Type3: + result = Type3(name) self._reverse_cache[result] = args + self.on_create(result) return result diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index a6f5994..0d8d670 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -354,7 +354,7 @@ def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> An return _load_bytes_from_address(runner, typ, adr) - if prelude.InternalPassAsPointer in typ.classes: + if (prelude.InternalPassAsPointer, (typ, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: # Note: For applied types, inp should contain a 4 byte pointer assert len(inp) == 4 adr = struct.unpack(' Foo: return x == y """ - with pytest.raises(Type3Exception, match='Foo does not implement the Eq type class'): + with pytest.raises(Type3Exception, match='Missing type class instantation: Eq Foo'): Suite(code_py).run_code() @pytest.mark.integration_test @@ -111,7 +111,7 @@ def testEntry(x: Foo, y: Foo) -> Foo: return x != y """ - with pytest.raises(Type3Exception, match='Foo does not implement the Eq type class'): + with pytest.raises(Type3Exception, match='Missing type class instantation: Eq Foo'): Suite(code_py).run_code() @pytest.mark.integration_test diff --git a/tests/integration/test_lang/test_natnum.py b/tests/integration/test_lang/test_natnum.py index 1649787..0f7e149 100644 --- a/tests/integration/test_lang/test_natnum.py +++ b/tests/integration/test_lang/test_natnum.py @@ -27,7 +27,7 @@ def testEntry(x: Foo, y: Foo) -> Foo: return x + y """ - with pytest.raises(Type3Exception, match='Foo does not implement the NatNum type class'): + with pytest.raises(Type3Exception, match='Missing type class instantation: NatNum Foo'): Suite(code_py).run_code() @pytest.mark.integration_test