Chore: Placeholders are now internal

They were exposed on AST, causing confusion.
Now they're only used in constraints.
This commit is contained in:
Johan B.W. de Vries 2025-05-05 17:33:21 +02:00
parent f6cb1a8c1d
commit d9a08cf0f7
6 changed files with 99 additions and 49 deletions

View File

@ -6,7 +6,6 @@ from typing import Dict, Iterable, List, Optional, Union
from . import prelude from . import prelude
from .type3.functions import FunctionSignature, TypeVariableContext from .type3.functions import FunctionSignature, TypeVariableContext
from .type3.placeholders import PlaceholderForType, Type3OrPlaceholder
from .type3.typeclasses import Type3ClassMethod from .type3.typeclasses import Type3ClassMethod
from .type3.types import Type3 from .type3.types import Type3
@ -17,10 +16,10 @@ class Expression:
""" """
__slots__ = ('type3', ) __slots__ = ('type3', )
type3: Type3OrPlaceholder type3: Type3 | None
def __init__(self) -> None: def __init__(self) -> None:
self.type3 = PlaceholderForType([self]) self.type3 = None
class Constant(Expression): class Constant(Expression):
""" """
@ -198,10 +197,10 @@ class AccessStructMember(Expression):
__slots__ = ('varref', 'struct_type3', 'member', ) __slots__ = ('varref', 'struct_type3', 'member', )
varref: VariableReference varref: VariableReference
struct_type3: Type3OrPlaceholder struct_type3: Type3
member: str member: str
def __init__(self, varref: VariableReference, struct_type3: Type3OrPlaceholder, member: str) -> None: def __init__(self, varref: VariableReference, struct_type3: Type3, member: str) -> None:
super().__init__() super().__init__()
self.varref = varref self.varref = varref
@ -284,7 +283,7 @@ class FunctionParam:
__slots__ = ('name', 'type3', ) __slots__ = ('name', 'type3', )
name: str name: str
type3: Type3OrPlaceholder type3: Type3
def __init__(self, name: str, type3: Type3) -> None: def __init__(self, name: str, type3: Type3) -> None:
self.name = name self.name = name

View File

@ -3,10 +3,11 @@ This module contains possible constraints generated based on the AST
These need to be resolved before the program can be compiled. These need to be resolved before the program can be compiled.
""" """
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, Iterable, List, Optional, Tuple, Union
from .. import ourlang, prelude from .. import ourlang, prelude
from . import placeholders, typeclasses, types from . import placeholders, typeclasses, types
from .placeholders import PlaceholderForType
class Error: class Error:
@ -157,7 +158,7 @@ class SameTypeConstraint(ConstraintBase):
return f'SameTypeConstraint({args}, comment={repr(self.comment)})' return f'SameTypeConstraint({args}, comment={repr(self.comment)})'
class TupleMatchConstraint(ConstraintBase): class TupleMatchConstraint(ConstraintBase):
def __init__(self, exp_type: placeholders.Type3OrPlaceholder, args: List[placeholders.Type3OrPlaceholder], comment: str): def __init__(self, exp_type: placeholders.Type3OrPlaceholder, args: Iterable[placeholders.Type3OrPlaceholder], comment: str):
super().__init__(comment=comment) super().__init__(comment=comment)
self.exp_type = exp_type self.exp_type = exp_type
@ -348,8 +349,12 @@ class LiteralFitsConstraint(ConstraintBase):
LiteralFitsConstraint(x, y) LiteralFitsConstraint(x, y)
for x, y in zip(tp_args, self.literal.value, strict=True) for x, y in zip(tp_args, self.literal.value, strict=True)
) )
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend( res.extend(
SameTypeConstraint(x, y.type3) SameTypeConstraint(x, PlaceholderForType([y]))
for x, y in zip(tp_args, self.literal.value, strict=True) for x, y in zip(tp_args, self.literal.value, strict=True)
) )
@ -371,8 +376,12 @@ class LiteralFitsConstraint(ConstraintBase):
LiteralFitsConstraint(sa_type, y) LiteralFitsConstraint(sa_type, y)
for y in self.literal.value for y in self.literal.value
) )
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend( res.extend(
SameTypeConstraint(sa_type, y.type3) SameTypeConstraint(sa_type, PlaceholderForType([y]))
for y in self.literal.value for y in self.literal.value
) )
@ -396,8 +405,12 @@ class LiteralFitsConstraint(ConstraintBase):
LiteralFitsConstraint(x, y) LiteralFitsConstraint(x, y)
for x, y in zip(st_args.values(), self.literal.value, strict=True) for x, y in zip(st_args.values(), self.literal.value, strict=True)
) )
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend( res.extend(
SameTypeConstraint(x_t, y.type3, comment=f'{self.literal.struct_name}.{x_n}') SameTypeConstraint(x_t, PlaceholderForType([y]), comment=f'{self.literal.struct_name}.{x_n}')
for (x_n, x_t, ), y in zip(st_args.items(), self.literal.value, strict=True) for (x_n, x_t, ), y in zip(st_args.items(), self.literal.value, strict=True)
) )
@ -421,20 +434,27 @@ class CanBeSubscriptedConstraint(ConstraintBase):
""" """
A value that is subscipted, i.e. a[0] (tuple) or a[b] (static array) A value that is subscipted, i.e. a[0] (tuple) or a[b] (static array)
""" """
__slots__ = ('ret_type3', 'type3', 'index', 'index_type3', ) __slots__ = ('ret_type3', 'type3', 'index', 'index_phft', )
ret_type3: placeholders.Type3OrPlaceholder ret_type3: placeholders.Type3OrPlaceholder
type3: placeholders.Type3OrPlaceholder type3: placeholders.Type3OrPlaceholder
index: ourlang.Expression index: ourlang.Expression
index_type3: placeholders.Type3OrPlaceholder index_phft: placeholders.Type3OrPlaceholder
def __init__(self, ret_type3: placeholders.Type3OrPlaceholder, type3: placeholders.Type3OrPlaceholder, index: ourlang.Expression, comment: Optional[str] = None) -> None: def __init__(
self,
ret_type3: placeholders.PlaceholderForType,
type3: placeholders.PlaceholderForType,
index: ourlang.Expression,
index_phft: placeholders.PlaceholderForType,
comment: Optional[str] = None,
) -> None:
super().__init__(comment=comment) super().__init__(comment=comment)
self.ret_type3 = ret_type3 self.ret_type3 = ret_type3
self.type3 = type3 self.type3 = type3
self.index = index self.index = index
self.index_type3 = index.type3 self.index_phft = index_phft
def check(self) -> CheckResult: def check(self) -> CheckResult:
exp_type = self.type3 exp_type = self.type3
@ -451,7 +471,7 @@ class CanBeSubscriptedConstraint(ConstraintBase):
sa_type, sa_len = sa_args sa_type, sa_len = sa_args
result: List[ConstraintBase] = [ result: List[ConstraintBase] = [
SameTypeConstraint(prelude.u32, self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'), SameTypeConstraint(prelude.u32, self.index_phft, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(sa_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'), SameTypeConstraint(sa_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
] ]
@ -478,13 +498,13 @@ class CanBeSubscriptedConstraint(ConstraintBase):
return Error('Tuple index out of range') return Error('Tuple index out of range')
return [ return [
SameTypeConstraint(prelude.u32, self.index_type3, comment=f'Tuple subscript index {self.index.value}'), SameTypeConstraint(prelude.u32, self.index_phft, comment=f'Tuple subscript index {self.index.value}'),
SameTypeConstraint(tp_args[self.index.value], self.ret_type3, comment=f'Tuple subscript index {self.index.value}'), SameTypeConstraint(tp_args[self.index.value], self.ret_type3, comment=f'Tuple subscript index {self.index.value}'),
] ]
if exp_type is prelude.bytes_: if exp_type is prelude.bytes_:
return [ return [
SameTypeConstraint(prelude.u32, self.index_type3, comment='([]) :: bytes -> u32 -> u8'), SameTypeConstraint(prelude.u32, self.index_phft, comment='([]) :: bytes -> u32 -> u8'),
SameTypeConstraint(prelude.u8, self.ret_type3, comment='([]) :: bytes -> u32 -> u8'), SameTypeConstraint(prelude.u8, self.ret_type3, comment='([]) :: bytes -> u32 -> u8'),
] ]

View File

@ -19,6 +19,7 @@ from .constraints import (
SameTypeConstraint, SameTypeConstraint,
TupleMatchConstraint, TupleMatchConstraint,
) )
from .placeholders import PlaceholderForType
ConstraintGenerator = Generator[ConstraintBase, None, None] ConstraintGenerator = Generator[ConstraintBase, None, None]
@ -28,23 +29,23 @@ def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase
return [*module(ctx, inp)] return [*module(ctx, inp)]
def constant(ctx: Context, inp: ourlang.Constant) -> ConstraintGenerator: def constant(ctx: Context, inp: ourlang.Constant, phft: placeholders.PlaceholderForType) -> ConstraintGenerator:
if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)): if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)):
yield LiteralFitsConstraint( yield LiteralFitsConstraint(
inp.type3, inp, phft, inp,
comment='The given literal must fit the expected type' comment='The given literal must fit the expected type'
) )
return return
raise NotImplementedError(constant, inp) raise NotImplementedError(constant, inp)
def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator: def expression(ctx: Context, inp: ourlang.Expression, phft: placeholders.PlaceholderForType) -> ConstraintGenerator:
if isinstance(inp, ourlang.Constant): if isinstance(inp, ourlang.Constant):
yield from constant(ctx, inp) yield from constant(ctx, inp, phft)
return return
if isinstance(inp, ourlang.VariableReference): if isinstance(inp, ourlang.VariableReference):
yield SameTypeConstraint(inp.variable.type3, inp.type3, yield SameTypeConstraint(inp.variable.type3, phft,
comment=f'typeOf("{inp.variable.name}") == typeOf({inp.variable.name})') comment=f'typeOf("{inp.variable.name}") == typeOf({inp.variable.name})')
return return
@ -60,8 +61,14 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
if isinstance(x, functions.TypeVariable) if isinstance(x, functions.TypeVariable)
} }
for call_arg in arguments: arg_placeholders = {
yield from expression(ctx, call_arg) arg_expr: PlaceholderForType([arg_expr])
for arg_expr in arguments
}
arg_placeholders[inp] = phft
for arg_expr in arguments:
yield from expression(ctx, arg_expr, arg_placeholders[arg_expr])
for constraint in signature.context.constraints: for constraint in signature.context.constraints:
if isinstance(constraint, functions.Constraint_TypeClassInstanceExists): if isinstance(constraint, functions.Constraint_TypeClassInstanceExists):
@ -81,11 +88,11 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
comment = f'The type of the value passed to argument {arg_no} of function {func_name} should match the type of that argument' 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): if isinstance(sig_part, functions.TypeVariable):
yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3, comment=comment) yield SameTypeConstraint(type_var_map[sig_part], arg_placeholders[arg_expr], comment=comment)
continue continue
if isinstance(sig_part, type3types.Type3): if isinstance(sig_part, type3types.Type3):
yield SameTypeConstraint(sig_part, arg_expr.type3, comment=comment) yield SameTypeConstraint(sig_part, arg_placeholders[arg_expr], comment=comment)
continue continue
raise NotImplementedError(sig_part) raise NotImplementedError(sig_part)
@ -94,11 +101,12 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
if isinstance(inp, ourlang.TupleInstantiation): if isinstance(inp, ourlang.TupleInstantiation):
r_type = [] r_type = []
for arg in inp.elements: for arg in inp.elements:
yield from expression(ctx, arg) arg_phft = PlaceholderForType([arg])
r_type.append(arg.type3) yield from expression(ctx, arg, arg_phft)
r_type.append(arg_phft)
yield TupleMatchConstraint( yield TupleMatchConstraint(
inp.type3, phft,
r_type, r_type,
comment='The type of a tuple is a combination of its members' comment='The type of a tuple is a combination of its members'
) )
@ -106,10 +114,13 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
return return
if isinstance(inp, ourlang.Subscript): if isinstance(inp, ourlang.Subscript):
yield from expression(ctx, inp.varref) varref_phft = PlaceholderForType([inp.varref])
yield from expression(ctx, inp.index) index_phft = PlaceholderForType([inp.index])
yield CanBeSubscriptedConstraint(inp.type3, inp.varref.type3, inp.index) yield from expression(ctx, inp.varref, varref_phft)
yield from expression(ctx, inp.index, index_phft)
yield CanBeSubscriptedConstraint(phft, varref_phft, inp.index, index_phft)
return return
if isinstance(inp, ourlang.AccessStructMember): if isinstance(inp, ourlang.AccessStructMember):
@ -117,33 +128,40 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
st_args = prelude.struct.did_construct(inp.struct_type3) st_args = prelude.struct.did_construct(inp.struct_type3)
assert st_args is not None # FIXME: See test_struct.py::test_struct_not_accessible assert st_args is not None # FIXME: See test_struct.py::test_struct_not_accessible
yield from expression(ctx, inp.varref) yield from expression(ctx, inp.varref, PlaceholderForType([inp.varref])) # TODO
yield SameTypeConstraint(st_args[inp.member], inp.type3, yield SameTypeConstraint(st_args[inp.member], phft,
comment=f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}') comment=f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}')
return return
if isinstance(inp, ourlang.Fold): if isinstance(inp, ourlang.Fold):
yield from expression(ctx, inp.base) base_phft = PlaceholderForType([inp.base])
yield from expression(ctx, inp.iter) iter_phft = PlaceholderForType([inp.iter])
yield SameTypeConstraint(inp.func.posonlyargs[0].type3, inp.func.returns_type3, inp.base.type3, inp.type3, yield from expression(ctx, inp.base, base_phft)
yield from expression(ctx, inp.iter, iter_phft)
yield SameTypeConstraint(inp.func.posonlyargs[0].type3, inp.func.returns_type3, base_phft, phft,
comment='foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b') comment='foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b')
yield MustImplementTypeClassConstraint(ctx, 'Foldable', [inp.iter.type3]) yield MustImplementTypeClassConstraint(ctx, 'Foldable', [iter_phft])
return return
raise NotImplementedError(expression, inp) raise NotImplementedError(expression, inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator: def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:
yield from expression(ctx, inp.value) phft = PlaceholderForType([inp.value])
yield SameTypeConstraint(fun.returns_type3, inp.value.type3, yield from expression(ctx, inp.value, phft)
yield SameTypeConstraint(fun.returns_type3, phft,
comment=f'The type of the value returned from function {fun.name} should match its return type') comment=f'The type of the value returned from function {fun.name} should match its return type')
def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator: def statement_if(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementIf) -> ConstraintGenerator:
yield from expression(ctx, inp.test) test_phft = PlaceholderForType([inp.test])
yield SameTypeConstraint(inp.test.type3, prelude.bool_, yield from expression(ctx, inp.test, test_phft)
yield SameTypeConstraint(test_phft, prelude.bool_,
comment='Must pass a boolean expression to if') comment='Must pass a boolean expression to if')
for stmt in inp.statements: for stmt in inp.statements:
@ -173,8 +191,10 @@ def function(ctx: Context, inp: ourlang.Function) -> ConstraintGenerator:
yield from statement(ctx, inp, stmt) yield from statement(ctx, inp, stmt)
def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> ConstraintGenerator: def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> ConstraintGenerator:
yield from constant(ctx, inp.constant) phft = PlaceholderForType([inp.constant])
yield SameTypeConstraint(inp.type3, inp.constant.type3,
yield from constant(ctx, inp.constant, phft)
yield SameTypeConstraint(inp.type3, phft,
comment=f'The type of the value for module constant definition {inp.name} should match the type of that constant') comment=f'The type of the value for module constant definition {inp.name} should match the type of that constant')
def module(ctx: Context, inp: ourlang.Module) -> ConstraintGenerator: def module(ctx: Context, inp: ourlang.Module) -> ConstraintGenerator:

View File

@ -115,8 +115,6 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None:
# FIXME: This doesn't work with e.g. `:: [a] -> a`, as the placeholder is inside a type # FIXME: This doesn't work with e.g. `:: [a] -> a`, as the placeholder is inside a type
for plh, typ in placeholder_substitutes.items(): for plh, typ in placeholder_substitutes.items():
for expr in plh.update_on_substitution: for expr in plh.update_on_substitution:
assert expr.type3 is plh
expr.type3 = typ expr.type3 = typ
def print_constraint(placeholder_id_map: Dict[int, str], constraint: ConstraintBase) -> None: def print_constraint(placeholder_id_map: Dict[int, str], constraint: ConstraintBase) -> None:

View File

@ -14,7 +14,7 @@ class ExpressionProtocol(Protocol):
A protocol for classes that should be updated on substitution A protocol for classes that should be updated on substitution
""" """
type3: 'Type3OrPlaceholder' type3: Type3 | None
""" """
The type to update The type to update
""" """

View File

@ -6,7 +6,20 @@ from ..helpers import Suite
@pytest.mark.integration_test @pytest.mark.integration_test
def test_expr_constant_literal_does_not_fit(): def test_expr_constant_literal_does_not_fit_module_constant():
code_py = """
CONSTANT: u8 = 1000
@exported
def testEntry() -> u8:
return CONSTANT
"""
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_expr_constant_literal_does_not_fit_return():
code_py = """ code_py = """
@exported @exported
def testEntry() -> u8: def testEntry() -> u8: