diff --git a/TODO.md b/TODO.md index ec58fb0..5f8de64 100644 --- a/TODO.md +++ b/TODO.md @@ -24,7 +24,4 @@ - Read https://bytecodealliance.org/articles/multi-value-all-the-wasm - Implement type class 'inheritance' -- Remove FunctionInstance, replace with a substitutions dict - - See phft2 in fromast.py:expression_function_call -- Move unify into the typeconstraints (or other way around) - it's done on two levels now (partly in solver) - Rework type classes - already started on a separate dir for those, but quite a few things are still in other places. diff --git a/phasm/type5/constrainedexpr.py b/phasm/type5/constrainedexpr.py index 14d42ff..5e6b501 100644 --- a/phasm/type5/constrainedexpr.py +++ b/phasm/type5/constrainedexpr.py @@ -10,7 +10,7 @@ from .typeexpr import TypeExpr, TypeVariable, instantiate class TypeConstraint: """ - Base class for type contraints + Base class for type constraints """ __slots__ = () diff --git a/phasm/type5/constraints.py b/phasm/type5/constraints.py index f95d817..7db7c87 100644 --- a/phasm/type5/constraints.py +++ b/phasm/type5/constraints.py @@ -75,6 +75,33 @@ class Context: assert tvar not in self.ptst_update self.ptst_update[tvar] = (arg, orig_var) +class Success: + """ + The contsraint checks out. + + Nothing new was learned and nothing new needs to be checked. + """ + + def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str: + return '(ok)' + +class SkipForNow: + """ + Not enough information to resolve this constraint + """ + + def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str: + return '(skip for now)' + +class ConstraintList(list['ConstraintBase']): + """ + A new list of constraints. + + Sometimes, checking one constraint means you get a list of new contraints. + """ + def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str: + return f'(got {len(self)} new constraints)' + @dataclasses.dataclass class Failure: """ @@ -82,52 +109,38 @@ class Failure: """ msg: str + def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str: + return f'ERR: {self.msg}' + @dataclasses.dataclass class ReplaceVariable: + """ + A variable should be replaced. + + Either by another variable or by a (concrete) type. + """ var: TypeVariable typ: TypeExpr -@dataclasses.dataclass -class CheckResult: - # TODO: Refactor this, don't think we use most of the variants - _: dataclasses.KW_ONLY - done: bool = True - replace: ReplaceVariable | None = None - new_constraints: list[ConstraintBase] = dataclasses.field(default_factory=list) - failures: list[Failure] = dataclasses.field(default_factory=list) - def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str: - if not self.done and not self.replace and not self.new_constraints and not self.failures: - return '(skip for now)' + return f'{{{self.var.name} := {type_namer(self.typ)}}}' - if self.done and not self.replace and not self.new_constraints and not self.failures: - return '(ok)' - - if self.done and self.replace and not self.new_constraints and not self.failures: - return f'{{{self.replace.var.name} := {type_namer(self.replace.typ)}}}' - - if self.done and not self.replace and self.new_constraints and not self.failures: - return f'(got {len(self.new_constraints)} new constraints)' - - if self.done and not self.replace and not self.new_constraints and self.failures: - return 'ERR: ' + '; '.join(x.msg for x in self.failures) - - return f'{self.done} {self.replace} {self.new_constraints} {self.failures}' - -def skip_for_now() -> CheckResult: - return CheckResult(done=False) - -def replace(var: TypeVariable, typ: TypeExpr) -> CheckResult: - return CheckResult(replace=ReplaceVariable(var, typ)) - -def new_constraints(lst: Iterable[ConstraintBase]) -> CheckResult: - return CheckResult(new_constraints=list(lst)) +CheckResult: TypeAlias = Success | SkipForNow | ConstraintList | Failure | ReplaceVariable def ok() -> CheckResult: - return CheckResult(done=True) + return Success() + +def skip_for_now() -> CheckResult: + return SkipForNow() + +def new_constraints(lst: Iterable[ConstraintBase]) -> CheckResult: + return ConstraintList(lst) def fail(msg: str) -> CheckResult: - return CheckResult(failures=[Failure(msg)]) + return Failure(msg) + +def replace(var: TypeVariable, typ: TypeExpr) -> CheckResult: + return ReplaceVariable(var, typ) class ConstraintBase: __slots__ = ("ctx", "sourceref", ) @@ -399,7 +412,7 @@ class CanBeSubscriptedConstraint(ConstraintBase): def check(self) -> CheckResult: if not is_concrete(self.container_type5): - return CheckResult(done=False) + return skip_for_now() tp_args = self.ctx.build.type5_is_tuple(self.container_type5) if tp_args is not None: @@ -462,7 +475,7 @@ class CanAccessStructMemberConstraint(ConstraintBase): def check(self) -> CheckResult: if not is_concrete(self.struct_type5): - return CheckResult(done=False) + return skip_for_now() st_args = self.ctx.build.type5_is_struct(self.struct_type5) if st_args is None: @@ -514,11 +527,11 @@ class FromTupleConstraint(ConstraintBase): def check(self) -> CheckResult: if not is_concrete(self.ret_type5): - return CheckResult(done=False) + return skip_for_now() da_arg = self.ctx.build.type5_is_dynamic_array(self.ret_type5) if da_arg is not None: - return CheckResult(new_constraints=[ + return new_constraints([ UnifyTypesConstraint(self.ctx, self.sourceref, da_arg, x) for x in self.member_type5_list ]) @@ -529,7 +542,7 @@ class FromTupleConstraint(ConstraintBase): if sa_len != len(self.member_type5_list): return fail('Tuple element count mismatch') - return CheckResult(new_constraints=[ + return new_constraints([ UnifyTypesConstraint(self.ctx, self.sourceref, sa_typ, x) for x in self.member_type5_list ]) @@ -539,7 +552,7 @@ class FromTupleConstraint(ConstraintBase): if len(tp_args) != len(self.member_type5_list): return fail('Tuple element count mismatch') - return CheckResult(new_constraints=[ + return new_constraints([ UnifyTypesConstraint(self.ctx, self.sourceref, act_typ, exp_typ) for act_typ, exp_typ in zip(tp_args, self.member_type5_list, strict=True) ]) diff --git a/phasm/type5/fromast.py b/phasm/type5/fromast.py index f3e62a4..401c4bf 100644 --- a/phasm/type5/fromast.py +++ b/phasm/type5/fromast.py @@ -241,7 +241,7 @@ def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.Statement type5 = fun.type5.expr if isinstance(fun.type5, ConstrainedExpr) else fun.type5 yield from expression(ctx, inp.value, phft) - yield UnifyTypesConstraint(ctx, inp.sourceref, type5, phft, prefix=f'{fun.name} returns') + yield UnifyTypesConstraint(ctx, inp.sourceref, type5, phft, prefix=f'{fun.name}(...)') def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator: test_phft = ctx.make_placeholder(inp.test) diff --git a/phasm/type5/solver.py b/phasm/type5/solver.py index e97022f..71ece98 100644 --- a/phasm/type5/solver.py +++ b/phasm/type5/solver.py @@ -1,7 +1,7 @@ from typing import Any from ..ourlang import Module -from .constraints import ConstraintBase, Context +from .constraints import ConstraintBase, ConstraintList, Context, Failure, ReplaceVariable, SkipForNow, Success from .fromast import phasm_type5_generate_constraints from .typeexpr import TypeExpr, TypeVariable, is_concrete, replace_variable @@ -30,58 +30,68 @@ def phasm_type5(inp: Module[Any], verbose: bool = False) -> None: print("Validating") new_constraint_list: list[ConstraintBase] = [] - for constraint in sorted(constraint_list, key=lambda x: x.complexity()): + + # Iterate using a while and pop since on ReplaceVariable + # we want to iterate over the list as well, and since on + # ConstraintList we want to treat those first. + remaining_constraint_list = sorted(constraint_list, key=lambda x: x.complexity()) + while remaining_constraint_list: + constraint = remaining_constraint_list.pop(0) result = constraint.check() if verbose: print(f"{constraint.sourceref!s} {constraint!s}") print(f"{constraint.sourceref!s} => {result.to_str(inp.build.type5_name)}") - if not result: - # None or empty list - # Means it checks out and we don't need do anything - continue + match result: + case Success(): + # This constraint was valid + continue + case SkipForNow(): + # We have to check later + new_constraint_list.append(constraint) + continue + case ConstraintList(items): + # This constraint was valid, but we have new once + # Do this as the first next items, so when users are reading the + # solver output they don't need to context switch. + remaining_constraint_list = items + remaining_constraint_list - if result.replace is not None: - action_var = result.replace.var - assert action_var not in placeholder_types # When does this happen? + if verbose: + for new_const in items: + print(f"{constraint.sourceref!s} => + {new_const!s}") - action_typ: TypeExpr = result.replace.typ - assert not isinstance(action_typ, TypeVariable) or action_typ not in placeholder_types # When does this happen? + continue + case Failure(msg): + error_list.append((str(constraint.sourceref), str(constraint), msg, )) + continue + case ReplaceVariable(action_var, action_typ): + assert action_var not in placeholder_types # When does this happen? + assert not isinstance(action_typ, TypeVariable) or action_typ not in placeholder_types # When does this happen? + assert action_var != action_typ # When does this happen? - assert action_var != action_typ # When does this happen? + # Ensure all existing found types are updated + # if they have this variable somewhere inside them. + placeholder_types = { + k: replace_variable(v, action_var, action_typ) + for k, v in placeholder_types.items() + } - # Ensure all existing found types are updated - placeholder_types = { - k: replace_variable(v, action_var, action_typ) - for k, v in placeholder_types.items() - } - placeholder_types[action_var] = action_typ + # Add the new variable to the registry + placeholder_types[action_var] = action_typ - for oth_const in new_constraint_list + constraint_list: - if oth_const is constraint and result.done: - continue + # Also update all constraints that may refer to this variable + # that they now have more detailed information. + for oth_const in new_constraint_list + remaining_constraint_list: + old_str = str(oth_const) + oth_const.replace_variable(action_var, action_typ) + new_str = str(oth_const) - old_str = str(oth_const) - oth_const.replace_variable(action_var, action_typ) - new_str = str(oth_const) + if verbose and old_str != new_str: + print(f"{oth_const.sourceref!s} => - {old_str!s}") + print(f"{oth_const.sourceref!s} => + {new_str!s}") - if verbose and old_str != new_str: - print(f"{oth_const.sourceref!s} => - {old_str!s}") - print(f"{oth_const.sourceref!s} => + {new_str!s}") - - for failure in result.failures: - error_list.append((str(constraint.sourceref), str(constraint), failure.msg, )) - - new_constraint_list.extend(result.new_constraints) - if verbose: - for new_const in result.new_constraints: - print(f"{oth_const.sourceref!s} => + {new_const!s}") - - if result.done: - continue - - new_constraint_list.append(constraint) + continue if error_list: raise Type5SolverException(error_list)