Cleanup to type5 solver

- Replaces weird CheckResult class with proper data classes.
- When a constraint results in new constraints, those are
  treated first
- Minor readability change to function return type
- Code cleanup
- TODO cleanup
- Typo fixes
This commit is contained in:
Johan B.W. de Vries 2025-08-30 14:45:12 +02:00
parent 71691d68e9
commit d1854d7a38
5 changed files with 106 additions and 86 deletions

View File

@ -24,7 +24,4 @@
- Read https://bytecodealliance.org/articles/multi-value-all-the-wasm - Read https://bytecodealliance.org/articles/multi-value-all-the-wasm
- Implement type class 'inheritance' - 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. - Rework type classes - already started on a separate dir for those, but quite a few things are still in other places.

View File

@ -10,7 +10,7 @@ from .typeexpr import TypeExpr, TypeVariable, instantiate
class TypeConstraint: class TypeConstraint:
""" """
Base class for type contraints Base class for type constraints
""" """
__slots__ = () __slots__ = ()

View File

@ -75,6 +75,33 @@ class Context:
assert tvar not in self.ptst_update assert tvar not in self.ptst_update
self.ptst_update[tvar] = (arg, orig_var) 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 @dataclasses.dataclass
class Failure: class Failure:
""" """
@ -82,52 +109,38 @@ class Failure:
""" """
msg: str msg: str
def to_str(self, type_namer: Callable[[TypeExpr], str]) -> str:
return f'ERR: {self.msg}'
@dataclasses.dataclass @dataclasses.dataclass
class ReplaceVariable: class ReplaceVariable:
"""
A variable should be replaced.
Either by another variable or by a (concrete) type.
"""
var: TypeVariable var: TypeVariable
typ: TypeExpr 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: 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 f'{{{self.var.name} := {type_namer(self.typ)}}}'
return '(skip for now)'
if self.done and not self.replace and not self.new_constraints and not self.failures: CheckResult: TypeAlias = Success | SkipForNow | ConstraintList | Failure | ReplaceVariable
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))
def ok() -> CheckResult: 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: 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: class ConstraintBase:
__slots__ = ("ctx", "sourceref", ) __slots__ = ("ctx", "sourceref", )
@ -399,7 +412,7 @@ class CanBeSubscriptedConstraint(ConstraintBase):
def check(self) -> CheckResult: def check(self) -> CheckResult:
if not is_concrete(self.container_type5): 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) tp_args = self.ctx.build.type5_is_tuple(self.container_type5)
if tp_args is not None: if tp_args is not None:
@ -462,7 +475,7 @@ class CanAccessStructMemberConstraint(ConstraintBase):
def check(self) -> CheckResult: def check(self) -> CheckResult:
if not is_concrete(self.struct_type5): 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) st_args = self.ctx.build.type5_is_struct(self.struct_type5)
if st_args is None: if st_args is None:
@ -514,11 +527,11 @@ class FromTupleConstraint(ConstraintBase):
def check(self) -> CheckResult: def check(self) -> CheckResult:
if not is_concrete(self.ret_type5): 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) da_arg = self.ctx.build.type5_is_dynamic_array(self.ret_type5)
if da_arg is not None: if da_arg is not None:
return CheckResult(new_constraints=[ return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, da_arg, x) UnifyTypesConstraint(self.ctx, self.sourceref, da_arg, x)
for x in self.member_type5_list for x in self.member_type5_list
]) ])
@ -529,7 +542,7 @@ class FromTupleConstraint(ConstraintBase):
if sa_len != len(self.member_type5_list): if sa_len != len(self.member_type5_list):
return fail('Tuple element count mismatch') return fail('Tuple element count mismatch')
return CheckResult(new_constraints=[ return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, sa_typ, x) UnifyTypesConstraint(self.ctx, self.sourceref, sa_typ, x)
for x in self.member_type5_list for x in self.member_type5_list
]) ])
@ -539,7 +552,7 @@ class FromTupleConstraint(ConstraintBase):
if len(tp_args) != len(self.member_type5_list): if len(tp_args) != len(self.member_type5_list):
return fail('Tuple element count mismatch') return fail('Tuple element count mismatch')
return CheckResult(new_constraints=[ return new_constraints([
UnifyTypesConstraint(self.ctx, self.sourceref, act_typ, exp_typ) UnifyTypesConstraint(self.ctx, self.sourceref, act_typ, exp_typ)
for act_typ, exp_typ in zip(tp_args, self.member_type5_list, strict=True) for act_typ, exp_typ in zip(tp_args, self.member_type5_list, strict=True)
]) ])

View File

@ -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 type5 = fun.type5.expr if isinstance(fun.type5, ConstrainedExpr) else fun.type5
yield from expression(ctx, inp.value, phft) 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: def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator:
test_phft = ctx.make_placeholder(inp.test) test_phft = ctx.make_placeholder(inp.test)

View File

@ -1,7 +1,7 @@
from typing import Any from typing import Any
from ..ourlang import Module 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 .fromast import phasm_type5_generate_constraints
from .typeexpr import TypeExpr, TypeVariable, is_concrete, replace_variable 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") print("Validating")
new_constraint_list: list[ConstraintBase] = [] 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() result = constraint.check()
if verbose: if verbose:
print(f"{constraint.sourceref!s} {constraint!s}") print(f"{constraint.sourceref!s} {constraint!s}")
print(f"{constraint.sourceref!s} => {result.to_str(inp.build.type5_name)}") print(f"{constraint.sourceref!s} => {result.to_str(inp.build.type5_name)}")
if not result: match result:
# None or empty list case Success():
# Means it checks out and we don't need do anything # This constraint was valid
continue 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: if verbose:
action_var = result.replace.var for new_const in items:
assert action_var not in placeholder_types # When does this happen? print(f"{constraint.sourceref!s} => + {new_const!s}")
action_typ: TypeExpr = result.replace.typ continue
assert not isinstance(action_typ, TypeVariable) or action_typ not in placeholder_types # When does this happen? 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 # Add the new variable to the registry
placeholder_types = { placeholder_types[action_var] = action_typ
k: replace_variable(v, action_var, action_typ)
for k, v in placeholder_types.items()
}
placeholder_types[action_var] = action_typ
for oth_const in new_constraint_list + constraint_list: # Also update all constraints that may refer to this variable
if oth_const is constraint and result.done: # that they now have more detailed information.
continue 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) if verbose and old_str != new_str:
oth_const.replace_variable(action_var, action_typ) print(f"{oth_const.sourceref!s} => - {old_str!s}")
new_str = str(oth_const) print(f"{oth_const.sourceref!s} => + {new_str!s}")
if verbose and old_str != new_str: continue
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)
if error_list: if error_list:
raise Type5SolverException(error_list) raise Type5SolverException(error_list)