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
- 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.

View File

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

View File

@ -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)
])

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
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)

View File

@ -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)