This commit is contained in:
Johan B.W. de Vries 2025-07-12 11:31:05 +02:00
parent 1a3bc19dce
commit 10946486fc
26 changed files with 1387 additions and 49 deletions

22
TODO.md
View File

@ -22,3 +22,25 @@
- Try to implement the min and max functions using select - Try to implement the min and max functions using select
- Read https://bytecodealliance.org/articles/multi-value-all-the-wasm - Read https://bytecodealliance.org/articles/multi-value-all-the-wasm
- GRose :: (* -> *) -> * -> *
- skolem => variable that cannot be unified
Limitations (for now):
- no type level nats
- only support first order kinds
Do not support yet:
```
data Record f = Record {
field: f Int
}
Record :: (* -> *) -> *
```
(Nested arrows)
- only support rank 1 types
```
mapRecord :: forall f g. (forall a. f a -> f b) -> Record f -> Record g
```
(Nested forall)

View File

@ -27,6 +27,8 @@ from ..type3.types import (
TypeConstructor_Struct, TypeConstructor_Struct,
TypeConstructor_Tuple, TypeConstructor_Tuple,
) )
from ..type5 import kindexpr as type5kindexpr
from ..type5 import typeexpr as type5typeexpr
from ..wasm import WasmType, WasmTypeInt32, WasmTypeNone from ..wasm import WasmType, WasmTypeInt32, WasmTypeNone
from . import builtins from . import builtins
@ -55,7 +57,9 @@ class MissingImplementationWarning(Warning):
class BuildBase[G]: class BuildBase[G]:
__slots__ = ( __slots__ = (
'dynamic_array', 'dynamic_array',
'dynamic_array_type5_constructor',
'function', 'function',
'function_type5_constructor',
'static_array', 'static_array',
'struct', 'struct',
'tuple_', 'tuple_',
@ -67,6 +71,7 @@ class BuildBase[G]:
'type_info_constructed', 'type_info_constructed',
'types', 'types',
'type5s',
'type_classes', 'type_classes',
'type_class_instances', 'type_class_instances',
'type_class_instance_methods', 'type_class_instance_methods',
@ -84,6 +89,8 @@ class BuildBase[G]:
determined length, and each argument is the same. determined length, and each argument is the same.
""" """
dynamic_array_type5_constructor: type5typeexpr.TypeConstructor
function: TypeConstructor_Function function: TypeConstructor_Function
""" """
This is a function. This is a function.
@ -91,6 +98,8 @@ class BuildBase[G]:
It should be applied with one or more arguments. The last argument is the 'return' type. It should be applied with one or more arguments. The last argument is the 'return' type.
""" """
function_type5_constructor: type5typeexpr.TypeConstructor
static_array: TypeConstructor_StaticArray static_array: TypeConstructor_StaticArray
""" """
This is a fixed length piece of memory. This is a fixed length piece of memory.
@ -142,6 +151,11 @@ class BuildBase[G]:
Types that are available without explicit import. Types that are available without explicit import.
""" """
type5s: dict[str, type5typeexpr.TypeExpr]
"""
Types that are available without explicit import.
"""
type_classes: dict[str, Type3Class] type_classes: dict[str, Type3Class]
""" """
Type classes that are available without explicit import. Type classes that are available without explicit import.
@ -179,6 +193,11 @@ class BuildBase[G]:
self.struct = builtins.struct self.struct = builtins.struct
self.tuple_ = builtins.tuple_ self.tuple_ = builtins.tuple_
S = type5kindexpr.Star()
self.function_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> (S >> S), name="function")
self.dynamic_array_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> S, name="dynamic_array")
self.bool_ = builtins.bool_ self.bool_ = builtins.bool_
self.none_ = builtins.none_ self.none_ = builtins.none_
@ -193,6 +212,7 @@ class BuildBase[G]:
'None': self.none_, 'None': self.none_,
'bool': self.bool_, 'bool': self.bool_,
} }
self.type5s = {}
self.type_classes = {} self.type_classes = {}
self.type_class_instances = set() self.type_class_instances = set()
self.type_class_instance_methods = {} self.type_class_instance_methods = {}

View File

@ -14,6 +14,8 @@ from ..type3.types import (
TypeConstructor_Struct, TypeConstructor_Struct,
TypeConstructor_Tuple, TypeConstructor_Tuple,
) )
from ..type5.kindexpr import Star
from ..type5.typeexpr import TypeConstructor as Type5Constructor
dynamic_array = TypeConstructor_DynamicArray('dynamic_array') dynamic_array = TypeConstructor_DynamicArray('dynamic_array')
function = TypeConstructor_Function('function') function = TypeConstructor_Function('function')
@ -24,3 +26,6 @@ tuple_ = TypeConstructor_Tuple('tuple')
bool_ = Type3('bool', TypeApplication_Nullary(None, None)) bool_ = Type3('bool', TypeApplication_Nullary(None, None))
none_ = Type3('None', TypeApplication_Nullary(None, None)) none_ = Type3('None', TypeApplication_Nullary(None, None))
s = Star()
static_array5 = Type5Constructor(name="static_array", kind=s >> s)

View File

@ -11,6 +11,7 @@ from ..type3.types import (
Type3, Type3,
TypeApplication_Nullary, TypeApplication_Nullary,
) )
from ..type5 import typeexpr as type5typeexpr
from ..wasm import ( from ..wasm import (
WasmTypeFloat32, WasmTypeFloat32,
WasmTypeFloat64, WasmTypeFloat64,
@ -82,6 +83,13 @@ class BuildDefault(BuildBase[Generator]):
'bytes': bytes_, 'bytes': bytes_,
}) })
u8_5 = type5typeexpr.AtomicType('u8')
self.type5s.update({
'u8': u8_5,
'i32': type5typeexpr.AtomicType('i32'),
'bytes': type5typeexpr.TypeApplication(self.dynamic_array_type5_constructor, u8_5),
})
tc_list = [ tc_list = [
bits, bits,
eq, ord, eq, ord,

View File

@ -175,8 +175,8 @@ def module(inp: ourlang.Module[Any]) -> str:
result += constant_definition(cdef) result += constant_definition(cdef)
for func in inp.functions.values(): for func in inp.functions.values():
if func.lineno < 0: if func.sourceref is None:
# Builtin (-2) or auto generated (-1) # Builtin or auto generated
continue continue
if result: if result:

View File

@ -314,7 +314,7 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourl
expression_subscript_tuple(wgn, mod, inp) expression_subscript_tuple(wgn, mod, inp)
return return
inp_as_fc = ourlang.FunctionCall(mod.build.type_classes['Subscriptable'].operators['[]']) inp_as_fc = ourlang.FunctionCall(mod.build.type_classes['Subscriptable'].operators['[]']) # type: ignore # TODO
inp_as_fc.type3 = inp.type3 inp_as_fc.type3 = inp.type3
inp_as_fc.arguments = [inp.varref, inp.index] inp_as_fc.arguments = [inp.varref, inp.index]

View File

@ -7,18 +7,35 @@ from .build.base import BuildBase
from .type3.functions import FunctionSignature, TypeVariableContext from .type3.functions import FunctionSignature, TypeVariableContext
from .type3.typeclasses import Type3ClassMethod from .type3.typeclasses import Type3ClassMethod
from .type3.types import Type3, TypeApplication_Struct from .type3.types import Type3, TypeApplication_Struct
from .type5 import typeexpr as type5typeexpr
class SourceRef:
__slots__ = ('filename', 'lineno', 'colno', )
filename: str | None
lineno: int | None
colno: int | None
def __init__(self, filename: str | None, lineno: int | None = None, colno: int | None = None) -> None:
self.filename = filename
self.lineno = lineno
self.colno = colno
class Expression: class Expression:
""" """
An expression within a statement An expression within a statement
""" """
__slots__ = ('type3', ) __slots__ = ('type3', 'type5', 'sourceref', )
sourceref: SourceRef | None
type3: Type3 | None type3: Type3 | None
type5: type5typeexpr.TypeExpr | None
def __init__(self) -> None: def __init__(self, *, sourceref: SourceRef | None = None) -> None:
self.sourceref = sourceref
self.type3 = None self.type3 = None
self.type5 = None
class Constant(Expression): class Constant(Expression):
""" """
@ -36,8 +53,8 @@ class ConstantPrimitive(Constant):
value: Union[int, float] value: Union[int, float]
def __init__(self, value: Union[int, float]) -> None: def __init__(self, value: Union[int, float], sourceref: SourceRef) -> None:
super().__init__() super().__init__(sourceref=sourceref)
self.value = value self.value = value
def __repr__(self) -> str: def __repr__(self) -> str:
@ -121,8 +138,8 @@ class VariableReference(Expression):
variable: Union['ModuleConstantDef', 'FunctionParam'] # also possibly local variable: Union['ModuleConstantDef', 'FunctionParam'] # also possibly local
def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam']) -> None: def __init__(self, variable: Union['ModuleConstantDef', 'FunctionParam'], sourceref: SourceRef) -> None:
super().__init__() super().__init__(sourceref=sourceref)
self.variable = variable self.variable = variable
class BinaryOp(Expression): class BinaryOp(Expression):
@ -154,8 +171,8 @@ class FunctionCall(Expression):
function: Union['Function', 'FunctionParam', Type3ClassMethod] function: Union['Function', 'FunctionParam', Type3ClassMethod]
arguments: List[Expression] arguments: List[Expression]
def __init__(self, function: Union['Function', 'FunctionParam', Type3ClassMethod]) -> None: def __init__(self, function: Union['Function', 'FunctionParam', Type3ClassMethod], sourceref: SourceRef) -> None:
super().__init__() super().__init__(sourceref=sourceref)
self.function = function self.function = function
self.arguments = [] self.arguments = []
@ -222,7 +239,12 @@ class Statement:
""" """
A statement within a function A statement within a function
""" """
__slots__ = () __slots__ = ("sourceref", )
sourceref: SourceRef | None
def __init__(self, *, sourceref: SourceRef | None = None) -> None:
self.sourceref = sourceref
class StatementPass(Statement): class StatementPass(Statement):
""" """
@ -236,7 +258,9 @@ class StatementReturn(Statement):
""" """
__slots__ = ('value', ) __slots__ = ('value', )
def __init__(self, value: Expression) -> None: def __init__(self, value: Expression, sourceref: SourceRef) -> None:
super().__init__(sourceref=sourceref)
self.value = value self.value = value
def __repr__(self) -> str: def __repr__(self) -> str:
@ -261,14 +285,18 @@ class FunctionParam:
""" """
A parameter for a Function A parameter for a Function
""" """
__slots__ = ('name', 'type3', ) __slots__ = ('name', 'type3', 'type5', )
name: str name: str
type3: Type3 type3: Type3
type5: type5typeexpr.TypeExpr
def __init__(self, name: str, type3: Type3, type5: type5typeexpr.TypeExpr) -> None:
assert type5typeexpr.is_concrete(type5)
def __init__(self, name: str, type3: Type3) -> None:
self.name = name self.name = name
self.type3 = type3 self.type3 = type3
self.type5 = type5
def __repr__(self) -> str: def __repr__(self) -> str:
return f'FunctionParam({self.name!r}, {self.type3!r})' return f'FunctionParam({self.name!r}, {self.type3!r})'
@ -277,39 +305,41 @@ class Function:
""" """
A function processes input and produces output A function processes input and produces output
""" """
__slots__ = ('name', 'lineno', 'exported', 'imported', 'statements', 'signature', 'returns_type3', 'posonlyargs', ) __slots__ = ('name', 'sourceref', 'exported', 'imported', 'statements', 'signature', 'returns_type3', 'type5', 'posonlyargs', )
name: str name: str
lineno: int sourceref: SourceRef
exported: bool exported: bool
imported: Optional[str] imported: Optional[str]
statements: List[Statement] statements: List[Statement]
signature: FunctionSignature signature: FunctionSignature
returns_type3: Type3 returns_type3: Type3
type5: type5typeexpr.TypeExpr | None
posonlyargs: List[FunctionParam] posonlyargs: List[FunctionParam]
def __init__(self, name: str, lineno: int, returns_type3: Type3) -> None: def __init__(self, name: str, sourceref: SourceRef, returns_type3: Type3) -> None:
self.name = name self.name = name
self.lineno = lineno self.sourceref = sourceref
self.exported = False self.exported = False
self.imported = None self.imported = None
self.statements = [] self.statements = []
self.signature = FunctionSignature(TypeVariableContext(), []) self.signature = FunctionSignature(TypeVariableContext(), [])
self.returns_type3 = returns_type3 self.returns_type3 = returns_type3
self.type5 = None
self.posonlyargs = [] self.posonlyargs = []
class StructDefinition: class StructDefinition:
""" """
The definition for a struct The definition for a struct
""" """
__slots__ = ('struct_type3', 'lineno', ) __slots__ = ('struct_type3', 'sourceref', )
struct_type3: Type3 struct_type3: Type3
lineno: int sourceref: SourceRef
def __init__(self, struct_type3: Type3, lineno: int) -> None: def __init__(self, struct_type3: Type3, sourceref: SourceRef) -> None:
self.struct_type3 = struct_type3 self.struct_type3 = struct_type3
self.lineno = lineno self.sourceref = sourceref
class StructConstructor(Function): class StructConstructor(Function):
""" """
@ -323,12 +353,12 @@ class StructConstructor(Function):
struct_type3: Type3 struct_type3: Type3
def __init__(self, struct_type3: Type3) -> None: def __init__(self, struct_type3: Type3) -> None:
super().__init__(f'@{struct_type3.name}@__init___@', -1, struct_type3) super().__init__(f'@{struct_type3.name}@__init___@', -1, struct_type3) # type: ignore # TODO
assert isinstance(struct_type3.application, TypeApplication_Struct) assert isinstance(struct_type3.application, TypeApplication_Struct)
for mem, typ in struct_type3.application.arguments: for mem, typ in struct_type3.application.arguments:
self.posonlyargs.append(FunctionParam(mem, typ, )) self.posonlyargs.append(FunctionParam(mem, typ)) # type: ignore # TODO
self.signature.args.append(typ) self.signature.args.append(typ)
self.signature.args.append(struct_type3) self.signature.args.append(struct_type3)
@ -339,17 +369,19 @@ class ModuleConstantDef:
""" """
A constant definition within a module A constant definition within a module
""" """
__slots__ = ('name', 'lineno', 'type3', 'constant', ) __slots__ = ('name', 'sourceref', 'type3', 'type5', 'constant', )
name: str name: str
lineno: int sourceref: SourceRef
type3: Type3 type3: Type3
type5: type5typeexpr.TypeExpr
constant: Constant constant: Constant
def __init__(self, name: str, lineno: int, type3: Type3, constant: Constant) -> None: def __init__(self, name: str, sourceref: SourceRef, type3: Type3, type5: type5typeexpr.TypeExpr, constant: Constant) -> None:
self.name = name self.name = name
self.lineno = lineno self.sourceref = sourceref
self.type3 = type3 self.type3 = type3
self.type5 = type5
self.constant = constant self.constant = constant
class ModuleDataBlock: class ModuleDataBlock:
@ -383,11 +415,13 @@ class Module[G]:
""" """
A module is a file and consists of functions A module is a file and consists of functions
""" """
__slots__ = ('build', 'data', 'types', 'struct_definitions', 'constant_defs', 'functions', 'methods', 'operators', 'functions_table', ) __slots__ = ('build', 'filename', 'data', 'types', 'type5s', 'struct_definitions', 'constant_defs', 'functions', 'methods', 'operators', 'functions_table', )
build: BuildBase[G] build: BuildBase[G]
filename: str
data: ModuleData data: ModuleData
types: dict[str, Type3] types: dict[str, Type3]
type5s: dict[str, type5typeexpr.TypeExpr]
struct_definitions: Dict[str, StructDefinition] struct_definitions: Dict[str, StructDefinition]
constant_defs: Dict[str, ModuleConstantDef] constant_defs: Dict[str, ModuleConstantDef]
functions: Dict[str, Function] functions: Dict[str, Function]
@ -395,11 +429,13 @@ class Module[G]:
operators: Dict[str, Type3ClassMethod] operators: Dict[str, Type3ClassMethod]
functions_table: dict[Function, int] functions_table: dict[Function, int]
def __init__(self, build: BuildBase[G]) -> None: def __init__(self, build: BuildBase[G], filename: str) -> None:
self.build = build self.build = build
self.filename = filename
self.data = ModuleData() self.data = ModuleData()
self.types = {} self.types = {}
self.type5s = {}
self.struct_definitions = {} self.struct_definitions = {}
self.constant_defs = {} self.constant_defs = {}
self.functions = {} self.functions = {}

View File

@ -22,6 +22,7 @@ from .ourlang import (
Module, Module,
ModuleConstantDef, ModuleConstantDef,
ModuleDataBlock, ModuleDataBlock,
SourceRef,
Statement, Statement,
StatementIf, StatementIf,
StatementPass, StatementPass,
@ -34,6 +35,7 @@ from .ourlang import (
) )
from .type3.typeclasses import Type3ClassMethod from .type3.typeclasses import Type3ClassMethod
from .type3.types import IntType3, Type3 from .type3.types import IntType3, Type3
from .type5 import typeexpr as type5typeexpr
from .wasmgenerator import Generator from .wasmgenerator import Generator
@ -96,11 +98,12 @@ class OurVisitor[G]:
self.build = build self.build = build
def visit_Module(self, node: ast.Module) -> Module[G]: def visit_Module(self, node: ast.Module) -> Module[G]:
module = Module(self.build) module = Module(self.build, "-")
module.methods.update(self.build.methods) module.methods.update(self.build.methods)
module.operators.update(self.build.operators) module.operators.update(self.build.operators)
module.types.update(self.build.types) module.types.update(self.build.types)
module.type5s.update(self.build.type5s)
_not_implemented(not node.type_ignores, 'Module.type_ignores') _not_implemented(not node.type_ignores, 'Module.type_ignores')
@ -112,7 +115,7 @@ class OurVisitor[G]:
if isinstance(res, ModuleConstantDef): if isinstance(res, ModuleConstantDef):
if res.name in module.constant_defs: if res.name in module.constant_defs:
raise StaticError( raise StaticError(
f'{res.name} already defined on line {module.constant_defs[res.name].lineno}' f'{res.name} already defined on line {module.constant_defs[res.name].sourceref.lineno}'
) )
module.constant_defs[res.name] = res module.constant_defs[res.name] = res
@ -131,7 +134,7 @@ class OurVisitor[G]:
if isinstance(res, Function): if isinstance(res, Function):
if res.name in module.functions: if res.name in module.functions:
raise StaticError( raise StaticError(
f'{res.name} already defined on line {module.functions[res.name].lineno}' f'{res.name} already defined on line {module.functions[res.name].sourceref.lineno}'
) )
module.functions[res.name] = res module.functions[res.name] = res
@ -156,15 +159,20 @@ class OurVisitor[G]:
raise NotImplementedError(f'{node} on Module') raise NotImplementedError(f'{node} on Module')
def pre_visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> Function: def pre_visit_Module_FunctionDef(self, module: Module[G], node: ast.FunctionDef) -> Function:
function = Function(node.name, node.lineno, self.build.none_) function = Function(node.name, SourceRef(module.filename, node.lineno, node.col_offset), self.build.none_)
_not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs') _not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs')
arg_type5_list = []
for arg in node.args.args: for arg in node.args.args:
if arg.annotation is None: if arg.annotation is None:
_raise_static_error(node, 'Must give a argument type') _raise_static_error(node, 'Must give a argument type')
arg_type = self.visit_type(module, arg.annotation) arg_type = self.visit_type(module, arg.annotation)
arg_type5 = self.visit_type5(module, arg.annotation)
arg_type5_list.append(arg_type5)
# FIXME: Allow TypeVariable in the function signature # FIXME: Allow TypeVariable in the function signature
# This would also require FunctionParam to accept a placeholder # This would also require FunctionParam to accept a placeholder
@ -173,6 +181,7 @@ class OurVisitor[G]:
function.posonlyargs.append(FunctionParam( function.posonlyargs.append(FunctionParam(
arg.arg, arg.arg,
arg_type, arg_type,
arg_type5,
)) ))
_not_implemented(not node.args.vararg, 'FunctionDef.args.vararg') _not_implemented(not node.args.vararg, 'FunctionDef.args.vararg')
@ -216,9 +225,24 @@ class OurVisitor[G]:
if node.returns is None: # Note: `-> None` would be a ast.Constant if node.returns is None: # Note: `-> None` would be a ast.Constant
_raise_static_error(node, 'Must give a return type') _raise_static_error(node, 'Must give a return type')
return_type = self.visit_type(module, node.returns) return_type = self.visit_type(module, node.returns)
arg_type5_list.append(self.visit_type5(module, node.returns))
function.signature.args.append(return_type) function.signature.args.append(return_type)
function.returns_type3 = return_type function.returns_type3 = return_type
for arg_type5 in reversed(arg_type5_list):
if function.type5 is None:
function.type5 = arg_type5
continue
function.type5 = type5typeexpr.TypeApplication(
constructor=type5typeexpr.TypeApplication(
constructor=module.build.function_type5_constructor,
argument=arg_type5,
),
argument=function.type5,
)
_not_implemented(not node.type_comment, 'FunctionDef.type_comment') _not_implemented(not node.type_comment, 'FunctionDef.type_comment')
return function return function
@ -249,7 +273,10 @@ class OurVisitor[G]:
members[stmt.target.id] = self.visit_type(module, stmt.annotation) members[stmt.target.id] = self.visit_type(module, stmt.annotation)
return StructDefinition(module.build.struct(node.name, tuple(members.items())), node.lineno) return StructDefinition(
module.build.struct(node.name, tuple(members.items())),
SourceRef(module.filename, node.lineno, node.col_offset),
)
def pre_visit_Module_AnnAssign(self, module: Module[G], node: ast.AnnAssign) -> ModuleConstantDef: def pre_visit_Module_AnnAssign(self, module: Module[G], node: ast.AnnAssign) -> ModuleConstantDef:
if not isinstance(node.target, ast.Name): if not isinstance(node.target, ast.Name):
@ -262,8 +289,9 @@ class OurVisitor[G]:
return ModuleConstantDef( return ModuleConstantDef(
node.target.id, node.target.id,
node.lineno, SourceRef(module.filename, node.lineno, node.col_offset),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -275,8 +303,9 @@ class OurVisitor[G]:
# Then return the constant as a pointer # Then return the constant as a pointer
return ModuleConstantDef( return ModuleConstantDef(
node.target.id, node.target.id,
node.lineno, SourceRef(module.filename, node.lineno, node.col_offset),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -288,8 +317,9 @@ class OurVisitor[G]:
# Then return the constant as a pointer # Then return the constant as a pointer
return ModuleConstantDef( return ModuleConstantDef(
node.target.id, node.target.id,
node.lineno, SourceRef(module.filename, node.lineno, node.col_offset),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -328,7 +358,8 @@ class OurVisitor[G]:
_raise_static_error(node, 'Return must have an argument') _raise_static_error(node, 'Return must have an argument')
return StatementReturn( return StatementReturn(
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value) self.visit_Module_FunctionDef_expr(module, function, our_locals, node.value),
SourceRef(module.filename, node.lineno, node.col_offset),
) )
if isinstance(node, ast.If): if isinstance(node, ast.If):
@ -443,11 +474,11 @@ class OurVisitor[G]:
if node.id in our_locals: if node.id in our_locals:
param = our_locals[node.id] param = our_locals[node.id]
return VariableReference(param) return VariableReference(param, SourceRef(module.filename, node.lineno, node.col_offset))
if node.id in module.constant_defs: if node.id in module.constant_defs:
cdef = module.constant_defs[node.id] cdef = module.constant_defs[node.id]
return VariableReference(cdef) return VariableReference(cdef, SourceRef(module.filename, node.lineno, node.col_offset))
if node.id in module.functions: if node.id in module.functions:
fun = module.functions[node.id] fun = module.functions[node.id]
@ -490,7 +521,7 @@ class OurVisitor[G]:
func = module.functions[node.func.id] func = module.functions[node.func.id]
result = FunctionCall(func) result = FunctionCall(func, sourceref=SourceRef(module.filename, node.lineno, node.col_offset))
result.arguments.extend( result.arguments.extend(
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_expr) self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_expr)
for arg_expr in node.args for arg_expr in node.args
@ -527,10 +558,10 @@ class OurVisitor[G]:
varref: VariableReference varref: VariableReference
if node.value.id in our_locals: if node.value.id in our_locals:
param = our_locals[node.value.id] param = our_locals[node.value.id]
varref = VariableReference(param) varref = VariableReference(param, SourceRef(module.filename, node.lineno, node.col_offset))
elif node.value.id in module.constant_defs: elif node.value.id in module.constant_defs:
constant_def = module.constant_defs[node.value.id] constant_def = module.constant_defs[node.value.id]
varref = VariableReference(constant_def) varref = VariableReference(constant_def, SourceRef(module.filename, node.lineno, node.col_offset))
else: else:
_raise_static_error(node, f'Undefined variable {node.value.id}') _raise_static_error(node, f'Undefined variable {node.value.id}')
@ -588,7 +619,7 @@ class OurVisitor[G]:
_not_implemented(node.kind is None, 'Constant.kind') _not_implemented(node.kind is None, 'Constant.kind')
if isinstance(node.value, (int, float, )): if isinstance(node.value, (int, float, )):
return ConstantPrimitive(node.value) return ConstantPrimitive(node.value, SourceRef(module.filename, node.lineno, node.col_offset))
if isinstance(node.value, bytes): if isinstance(node.value, bytes):
data_block = ModuleDataBlock([]) data_block = ModuleDataBlock([])
@ -664,6 +695,18 @@ class OurVisitor[G]:
raise NotImplementedError(f'{node} as type') raise NotImplementedError(f'{node} as type')
def visit_type5(self, module: Module[G], node: ast.expr) -> type5typeexpr.TypeExpr:
if isinstance(node, ast.Name):
if not isinstance(node.ctx, ast.Load):
_raise_static_error(node, 'Must be load context')
if node.id in module.type5s:
return module.type5s[node.id]
_raise_static_error(node, f'Unrecognized type {node.id}')
raise NotImplementedError
def _not_implemented(check: Any, msg: str) -> None: def _not_implemented(check: Any, msg: str) -> None:
if not check: if not check:
raise NotImplementedError(msg) raise NotImplementedError(msg)

0
phasm/type4/__init__.py Normal file
View File

131
phasm/type4/classes.py Normal file
View File

@ -0,0 +1,131 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TypeAlias
## Kind * (part 1)
@dataclass
class Atomic:
"""
Atomic (or base or fundamental) type - it doesn't construct anything and wasn't constructed
"""
name: str
class Struct(Atomic):
"""
Structs are a fundamental type. But we need to store some extra info.
"""
fields: list[tuple[str, Type4]]
@dataclass
class Variable:
"""
Type variable of kind *.
"""
name: str
@dataclass
class Nat:
"""
Some type constructors take a natural number as argument, rather than a type.
"""
value: int
@property
def name(self) -> str:
return str(self.value)
## Kind * -> *
@dataclass
class ConcreteConSS:
"""
An concrete type construtor of kind * -> *, like dynamic array.
"""
name: str
@dataclass
class VariableConSS:
"""
A type variable of kind * -> *.
"""
name: str
@dataclass
class AppliedConSS:
"""
An application of a type constructor of kind * -> *.
The result is a type of kind *.
"""
con: ConcreteConSS | VariableConSS | AppliedConSSS | AppliedConNSS
arg: Type4
@property
def name(self) -> str:
return f'{self.con.name} {self.arg.name}'
## Kind * -> * -> *
@dataclass
class ConcreteConSSS:
"""
An concrete type construtor of kind * -> * -> *, like function, tuple or Either.
"""
name: str
@dataclass
class VariableConSSS:
"""
A type variable of kind * -> * -> *.
"""
name: str
@dataclass
class AppliedConSSS:
"""
An application of a type constructor of kind * -> * -> *.
The result is a type construtor of kind * -> *.
"""
con: ConcreteConSSS | VariableConSSS
arg: Type4
@property
def name(self) -> str:
return f'{self.con.name} {self.arg.name}'
## Kind Nat -> * -> *
@dataclass
class ConcreteConNSS:
"""
An concrete type construtor of kind Nat -> * -> *, like static array.
"""
name: str
@dataclass
class VariableConNSS:
"""
A type variable of kind Nat -> * -> *.
"""
name: str
@dataclass
class AppliedConNSS:
"""
An application of a type constructor of kind Nat -> * -> *.
The result is a type construtor of kind * -> *.
"""
con: ConcreteConNSS | VariableConNSS
arg: Nat
@property
def name(self) -> str:
return f'{self.con.name} {self.arg.name}'
# Kind * (part 2)
Type4: TypeAlias = Atomic | Variable | AppliedConSS

202
phasm/type4/unify.py Normal file
View File

@ -0,0 +1,202 @@
from dataclasses import dataclass
from .classes import (
AppliedConNSS,
AppliedConSS,
AppliedConSSS,
Atomic,
ConcreteConNSS,
ConcreteConSS,
ConcreteConSSS,
Type4,
Variable,
VariableConNSS,
VariableConSS,
VariableConSSS,
)
@dataclass
class Failure:
"""
Both types are already different - cannot be unified.
"""
msg: str
@dataclass
class Action:
pass
UnifyResult = Failure | list[Action]
@dataclass
class SetTypeForVariable(Action):
var: Variable
type: Atomic | AppliedConSS
@dataclass
class SetTypeForConSSVariable(Action):
var: VariableConSS
type: ConcreteConSS | AppliedConSSS | AppliedConNSS
@dataclass
class SetTypeForConSSSVariable(Action):
var: VariableConSSS
type: ConcreteConSSS
@dataclass
class ReplaceVariable(Action):
before: Variable
after: Variable
@dataclass
class ReplaceConSSVariable(Action):
before: VariableConSS
after: VariableConSS
def unify(lft: Type4, rgt: Type4) -> UnifyResult:
"""
Tries to unify the given two types.
"""
if isinstance(lft, Atomic) and isinstance(rgt, Atomic):
if lft == rgt:
return []
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, Atomic) and isinstance(rgt, Variable):
return [SetTypeForVariable(rgt, lft)]
if isinstance(lft, Atomic) and isinstance(rgt, AppliedConSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, Variable) and isinstance(rgt, Atomic):
return [SetTypeForVariable(lft, rgt)]
if isinstance(lft, Variable) and isinstance(rgt, Variable):
return [ReplaceVariable(lft, rgt)]
if isinstance(lft, Variable) and isinstance(rgt, AppliedConSS):
return [SetTypeForVariable(lft, rgt)]
if isinstance(lft, AppliedConSS) and isinstance(rgt, Atomic):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, AppliedConSS) and isinstance(rgt, Variable):
return [SetTypeForVariable(rgt, lft)]
if isinstance(lft, AppliedConSS) and isinstance(rgt, AppliedConSS):
con_res = unify_con_ss(lft.con, rgt.con)
if isinstance(con_res, Failure):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
arg_res = unify(lft.arg, rgt.arg)
if isinstance(arg_res, Failure):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
return con_res + arg_res
raise NotImplementedError
def unify_con_ss(
lft: ConcreteConSS | VariableConSS | AppliedConSSS | AppliedConNSS,
rgt: ConcreteConSS | VariableConSS | AppliedConSSS | AppliedConNSS,
) -> UnifyResult:
"""
Tries to unify the given two constuctors of kind * -> *.
"""
if isinstance(lft, ConcreteConSS) and isinstance(rgt, ConcreteConSS):
if lft == rgt:
return []
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, ConcreteConSS) and isinstance(rgt, VariableConSS):
return [SetTypeForConSSVariable(rgt, lft)]
if isinstance(lft, ConcreteConSS) and isinstance(rgt, AppliedConSSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, ConcreteConSS) and isinstance(rgt, AppliedConNSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, VariableConSS) and isinstance(rgt, ConcreteConSS):
return [SetTypeForConSSVariable(lft, rgt)]
if isinstance(lft, VariableConSS) and isinstance(rgt, VariableConSS):
return [ReplaceConSSVariable(lft, rgt)]
if isinstance(lft, VariableConSS) and isinstance(rgt, AppliedConSSS):
return [SetTypeForConSSVariable(lft, rgt)]
if isinstance(lft, VariableConSS) and isinstance(rgt, AppliedConNSS):
return [SetTypeForConSSVariable(lft, rgt)]
if isinstance(lft, AppliedConSSS) and isinstance(rgt, ConcreteConSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, AppliedConSSS) and isinstance(rgt, VariableConSS):
return [SetTypeForConSSVariable(rgt, lft)]
if isinstance(lft, AppliedConSSS) and isinstance(rgt, AppliedConSSS):
con_res = unify_con_sss(lft.con, rgt.con)
if isinstance(con_res, Failure):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
arg_res = unify(lft.arg, rgt.arg)
if isinstance(arg_res, Failure):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
return con_res + arg_res
if isinstance(lft, AppliedConSSS) and isinstance(rgt, AppliedConNSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, AppliedConNSS) and isinstance(rgt, ConcreteConSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, AppliedConNSS) and isinstance(rgt, VariableConSS):
return [SetTypeForConSSVariable(rgt, lft)]
if isinstance(lft, AppliedConNSS) and isinstance(rgt, AppliedConSSS):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if isinstance(lft, AppliedConNSS) and isinstance(rgt, AppliedConNSS):
con_res = unify_con_nss(lft.con, rgt.con)
if isinstance(con_res, Failure):
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
if lft.arg.value != rgt.arg.value:
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
return con_res
raise NotImplementedError
def unify_con_sss(
lft: ConcreteConSSS | VariableConSSS,
rgt: ConcreteConSSS | VariableConSSS,
) -> UnifyResult:
"""
Tries to unify the given two constuctors of kind * -> * -> *.
"""
if isinstance(lft, ConcreteConSSS) and isinstance(rgt, ConcreteConSSS):
if lft == rgt:
return []
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
raise NotImplementedError
def unify_con_nss(
lft: ConcreteConNSS | VariableConNSS,
rgt: ConcreteConNSS | VariableConNSS,
) -> UnifyResult:
"""
Tries to unify the given two constuctors of kind * -> * -> *.
"""
if isinstance(lft, ConcreteConNSS) and isinstance(rgt, ConcreteConNSS):
if lft == rgt:
return []
return Failure(f'Cannot unify {lft.name} and {rgt.name}')
raise NotImplementedError

0
phasm/type5/__init__.py Normal file
View File

93
phasm/type5/__main__.py Normal file
View File

@ -0,0 +1,93 @@
from .kindexpr import Star
from .record import Record
from .typeexpr import AtomicType, TypeApplication, TypeConstructor, TypeVariable
from .unify import unify
def main() -> None:
S = Star()
a_var = TypeVariable(name="a", kind=S)
b_var = TypeVariable(name="b", kind=S)
t_var = TypeVariable(name="t", kind=S >> S)
r_var = TypeVariable(name="r", kind=S >> S)
print(a_var)
print(b_var)
print(t_var)
print(r_var)
print()
u32_type = AtomicType(name="u32")
f64_type = AtomicType(name="f64")
print(u32_type)
print(f64_type)
print()
maybe_constructor = TypeConstructor(name="Maybe", kind=S >> S)
maybe_a_type = TypeApplication(constructor=maybe_constructor, argument=a_var)
maybe_u32_type = TypeApplication(constructor=maybe_constructor, argument=u32_type)
print(maybe_constructor)
print(maybe_a_type)
print(maybe_u32_type)
print()
either_constructor = TypeConstructor(name="Either", kind=S >> (S >> S))
either_a_constructor = TypeApplication(constructor=either_constructor, argument=a_var)
either_a_a_type = TypeApplication(constructor=either_a_constructor, argument=a_var)
either_u32_constructor = TypeApplication(constructor=either_constructor, argument=u32_type)
either_u32_f64_type = TypeApplication(constructor=either_u32_constructor, argument=f64_type)
either_u32_a_type = TypeApplication(constructor=either_u32_constructor, argument=a_var)
print(either_constructor)
print(either_a_constructor)
print(either_a_a_type)
print(either_u32_constructor)
print(either_u32_f64_type)
print(either_u32_a_type)
print()
t_a_type = TypeApplication(constructor=t_var, argument=a_var)
t_u32_type = TypeApplication(constructor=t_var, argument=u32_type)
print(t_a_type)
print(t_u32_type)
print()
shape_record = Record("Shape", [
("width", u32_type),
("height", either_u32_f64_type),
])
print('shape_record', shape_record)
print()
values = [
a_var,
b_var,
u32_type,
f64_type,
t_var,
t_a_type,
r_var,
maybe_constructor,
maybe_u32_type,
maybe_a_type,
either_u32_constructor,
either_u32_a_type,
either_u32_f64_type,
]
seen_exprs: set[str] = set()
for lft in values:
for rgt in values:
expr = f"{lft.name} ~ {rgt.name}"
if expr in seen_exprs:
continue
print(expr.ljust(40) + " => " + str(unify(lft, rgt)))
inv_expr = f"{rgt.name} ~ {lft.name}"
seen_exprs.add(inv_expr)
print()
if __name__ == '__main__':
main()

131
phasm/type5/constraints.py Normal file
View File

@ -0,0 +1,131 @@
from typing import Any, Protocol
from ..build.base import BuildBase
from ..ourlang import SourceRef
from ..wasm import WasmTypeInt32, WasmTypeInt64
from .kindexpr import KindExpr, Star
from .typeexpr import TypeApplication, TypeExpr, TypeVariable, is_concrete, replace_variable
from .unify import ActionList, Failure, ReplaceVariable, unify
class ExpressionProtocol(Protocol):
"""
A protocol for classes that should be updated on substitution
"""
type5: TypeExpr | None
"""
The type to update
"""
class Context:
__slots__ = ("build", "placeholder_update", )
build: BuildBase[Any]
placeholder_update: dict[TypeVariable, ExpressionProtocol]
def __init__(self, build: BuildBase[Any]) -> None:
self.build = build
self.placeholder_update = {}
def make_placeholder(self, arg: ExpressionProtocol, kind: KindExpr = Star()) -> TypeVariable:
res = TypeVariable(kind, f"p_{len(self.placeholder_update)}")
self.placeholder_update[res] = arg
return res
class SkipForNow:
def __str__(self) -> str:
return '(skip for now)'
CheckResult = None | ActionList | Failure | SkipForNow
class ConstraintBase:
__slots__ = ("ctx", "sourceref", "comment",)
ctx: Context
sourceref: SourceRef | None
comment: str | None
def __init__(self, ctx: Context, sourceref: SourceRef | None, comment: str | None = None) -> None:
self.ctx = ctx
self.sourceref = sourceref
self.comment = comment
def check(self) -> CheckResult:
raise NotImplementedError(self)
def apply(self, action_list: ActionList) -> None:
for action in action_list:
if isinstance(action, ReplaceVariable):
self.replace_variable(action.var, action.typ)
continue
raise NotImplementedError(action)
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
pass
class LiteralFitsConstraint(ConstraintBase):
__slots__ = ("type", "literal",)
def __init__(self, ctx: Context, sourceref: SourceRef | None, type: TypeExpr, literal: Any, *, comment: str | None = None) -> None:
super().__init__(ctx, sourceref, comment)
self.type = type
self.literal = literal
def check(self) -> CheckResult:
if not is_concrete(self.type):
return SkipForNow()
type_info = self.ctx.build.type_info_map.get(self.type.name)
if type_info is not None and (type_info.wasm_type is WasmTypeInt32 or type_info.wasm_type is WasmTypeInt64):
assert type_info.signed is not None
if not isinstance(self.literal.value, int):
return Failure('Must be integer')
try:
self.literal.value.to_bytes(type_info.alloc_size, 'big', signed=type_info.signed)
except OverflowError:
return Failure(f'Must fit in {type_info.alloc_size} byte(s)')
return None
if isinstance(self.type, TypeApplication) and self.type.constructor == self.ctx.build.dynamic_array_type5_constructor:
if self.type.argument == self.ctx.build.type5s['u8']:
if not isinstance(self.literal.value, bytes):
return Failure('Must be bytes')
return None
raise NotImplementedError(type_info)
raise NotImplementedError(type_info)
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.type = replace_variable(self.type, var, typ)
def __str__(self) -> str:
return f"{self.type} can contain {self.literal!r}"
class UnifyTypesConstraint(ConstraintBase):
__slots__ = ("lft", "rgt",)
def __init__(self, ctx: Context, sourceref: SourceRef | None, lft: TypeExpr, rgt: TypeExpr, *, comment: str | None = None) -> None:
super().__init__(ctx, sourceref, comment)
self.lft = lft
self.rgt = rgt
def check(self) -> CheckResult:
return unify(self.lft, self.rgt)
def replace_variable(self, var: TypeVariable, typ: TypeExpr) -> None:
self.lft = replace_variable(self.lft, var, typ)
self.rgt = replace_variable(self.rgt, var, typ)
def __str__(self) -> str:
return f"{self.lft} ~ {self.rgt}"

110
phasm/type5/fromast.py Normal file
View File

@ -0,0 +1,110 @@
from typing import Any, Generator
from .. import ourlang
from .constraints import (
ConstraintBase,
Context,
LiteralFitsConstraint,
UnifyTypesConstraint,
)
from .typeexpr import TypeApplication, TypeExpr, TypeVariable
ConstraintGenerator = Generator[ConstraintBase, None, None]
def phasm_type5_generate_constraints(ctx: Context, inp: ourlang.Module[Any]) -> list[ConstraintBase]:
return [*module(ctx, inp)]
def expression_constant(ctx: Context, inp: ourlang.Constant, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantBytes, ourlang.ConstantTuple, ourlang.ConstantStruct)):
yield LiteralFitsConstraint(
ctx, inp.sourceref, phft, inp,
comment='The given literal must fit the expected type'
)
return
raise NotImplementedError(inp)
def expression_variable_reference(ctx: Context, inp: ourlang.VariableReference, phft: TypeVariable) -> ConstraintGenerator:
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.variable.type5, phft)
def expression_function_call(ctx: Context, inp: ourlang.FunctionCall, phft: TypeVariable) -> ConstraintGenerator:
arg_typ_list = []
for arg in inp.arguments:
arg_tv = ctx.make_placeholder(arg)
yield from expression(ctx, arg, arg_tv)
arg_typ_list.append(arg_tv)
if not hasattr(inp.function, 'type5'):
raise NotImplementedError
assert isinstance(inp.function.type5, TypeExpr)
expr_type: TypeExpr = phft
for arg_tv in reversed(arg_typ_list):
expr_type = TypeApplication(
constructor=TypeApplication(
constructor=ctx.build.function_type5_constructor,
argument=arg_tv,
),
argument=expr_type,
)
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.function.type5, expr_type)
def expression(ctx: Context, inp: ourlang.Expression, phft: TypeVariable) -> ConstraintGenerator:
if isinstance(inp, ourlang.Constant):
yield from expression_constant(ctx, inp, phft)
return
if isinstance(inp, ourlang.VariableReference):
yield from expression_variable_reference(ctx, inp, phft)
return
if isinstance(inp, ourlang.FunctionCall):
yield from expression_function_call(ctx, inp, phft)
return
raise NotImplementedError(inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:
phft = ctx.make_placeholder(inp.value)
if fun.type5 is None:
raise NotImplementedError("Deducing function type - you'll have to annotate it.")
type5 = fun.type5
while isinstance(type5, TypeApplication) and isinstance(type5.constructor, TypeApplication) and type5.constructor.constructor == ctx.build.function_type5_constructor:
type5 = type5.argument
yield UnifyTypesConstraint(ctx, inp.sourceref, type5, phft)
yield from expression(ctx, inp.value, phft)
def statement(ctx: Context, fun: ourlang.Function, inp: ourlang.Statement) -> ConstraintGenerator:
if isinstance(inp, ourlang.StatementReturn):
yield from statement_return(ctx, fun, inp)
return
raise NotImplementedError(inp)
def function(ctx: Context, inp: ourlang.Function) -> ConstraintGenerator:
for stmt in inp.statements:
yield from statement(ctx, inp, stmt)
def module_constant_def(ctx: Context, inp: ourlang.ModuleConstantDef) -> ConstraintGenerator:
phft = ctx.make_placeholder(inp.constant)
yield from expression_constant(ctx, inp.constant, phft)
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.type5, phft)
def module(ctx: Context, inp: ourlang.Module[Any]) -> ConstraintGenerator:
for cdef in inp.constant_defs.values():
yield from module_constant_def(ctx, cdef)
for func in inp.functions.values():
if func.imported:
continue
yield from function(ctx, func)
# TODO: Generalize?

46
phasm/type5/kindexpr.py Normal file
View File

@ -0,0 +1,46 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TypeAlias
@dataclass
class Star:
def __rshift__(self, other: KindExpr) -> Arrow:
return Arrow(self, other)
def __str__(self) -> str:
return "*"
def __hash__(self) -> int:
return 0 # All Stars are the same
@dataclass
class Arrow:
"""
Represents an arrow kind `K1 -> K2`.
To create K1 -> K2 -> K3, pass an Arrow for result_kind.
For now, we do not support Arrows as arguments (i.e.,
no higher order kinds).
"""
arg_kind: Star
result_kind: KindExpr
def __str__(self) -> str:
if isinstance(self.arg_kind, Star):
arg_kind = "*"
else:
arg_kind = f"({str(self.arg_kind)})"
if isinstance(self.result_kind, Star):
result_kind = "*"
else:
result_kind = f"({str(self.result_kind)})"
return f"{arg_kind} -> {result_kind}"
def __hash__(self) -> int:
return hash((self.arg_kind, self.result_kind, ))
KindExpr: TypeAlias = Star | Arrow

42
phasm/type5/record.py Normal file
View File

@ -0,0 +1,42 @@
from dataclasses import dataclass
from .kindexpr import Star
from .typeexpr import AtomicType, TypeApplication, TypeExpr
@dataclass
class Record(TypeExpr):
"""
Records are a fundamental type. But we need to store some extra info.
"""
fields: list[tuple[str, AtomicType | TypeApplication]]
def __init__(self, name: str, fields: list[tuple[str, AtomicType | TypeApplication]]) -> None:
for field_name, field_type in fields:
if field_type.kind == Star():
continue
raise TypeError(f"Record fields must be concrete types ({field_name} :: {field_type})")
super().__init__(Star(), name)
self.fields = fields
def __str__(self) -> str:
args = ", ".join(
f"{field_name} :: {field_type}"
for field_name, field_type in self.fields
)
return f"{self.name} {{{args}}} :: {self.kind}"
@dataclass
class RecordConstructor(TypeExpr):
"""
TODO.
i.e.:
```
class Foo[T, R]:
lft: T
rgt: R
"""
name: str

8
phasm/type5/router.py Normal file
View File

@ -0,0 +1,8 @@
from typing import Callable
from .typeexpr import TypeExpr
class TypeRouter[S, R]:
def add(self, typ: TypeExpr, mtd: Callable[[S, TypeExpr], R]) -> None:
raise NotImplementedError

72
phasm/type5/solver.py Normal file
View File

@ -0,0 +1,72 @@
from typing import Any
from ..ourlang import Module, SourceRef
from .constraints import Context, SkipForNow
from .fromast import phasm_type5_generate_constraints
from .typeexpr import TypeExpr, TypeVariable
from .unify import ActionList, Failure, ReplaceVariable
MAX_RESTACK_COUNT = 100
class Type5SolverException(Exception):
pass
def phasm_type5(inp: Module[Any], verbose: bool = False) -> None:
ctx = Context(inp.build)
constraint_list = phasm_type5_generate_constraints(ctx, inp)
assert constraint_list
placeholder_types: dict[TypeVariable, TypeExpr] = {}
error_list: list[Failure] = []
for _ in range(MAX_RESTACK_COUNT):
for constraint in constraint_list:
sourceref = constraint.sourceref or SourceRef("?", 0, 0)
print(f"{sourceref.lineno:>4}:{sourceref.colno:<3} {constraint!s:<20}")
print()
new_constraint_list = []
for constraint in constraint_list:
result = constraint.check()
sourceref = constraint.sourceref or SourceRef("?", 0, 0)
print(f"{sourceref.lineno:>4}:{sourceref.colno:<3} {constraint!s:<30} {result}")
if not result:
# None or empty list
# Means it checks out and we don't need do anything
continue
if isinstance(result, SkipForNow):
new_constraint_list.append(constraint)
continue
if isinstance(result, Failure):
error_list.append(result)
continue
if isinstance(result, ActionList):
for action in result:
if isinstance(action, ReplaceVariable):
assert action.var not in placeholder_types
placeholder_types[action.var] = action.typ
continue
for constraint in constraint_list:
constraint.apply(result)
continue
raise NotImplementedError(result)
if error_list:
raise Type5SolverException(error_list)
if not new_constraint_list:
break
constraint_list = new_constraint_list
for placeholder, expression in ctx.placeholder_update.items():
expression.type5 = placeholder_types[placeholder]

121
phasm/type5/typeexpr.py Normal file
View File

@ -0,0 +1,121 @@
from __future__ import annotations
from dataclasses import dataclass
from .kindexpr import Arrow, KindExpr, Star
@dataclass
class TypeExpr:
kind: KindExpr
name: str
def __str__(self) -> str:
return f"{self.name} :: {self.kind}"
@dataclass
class AtomicType(TypeExpr):
def __init__(self, name: str) -> None:
super().__init__(Star(), name)
@dataclass
class TypeVariable(TypeExpr):
"""
A placeholder in a type expression
"""
def __hash__(self) -> int:
return hash((self.kind, self.name))
@dataclass
class TypeConstructor(TypeExpr):
name: str
def __init__(self, kind: Arrow, name: str) -> None:
super().__init__(kind, name)
@dataclass
class TypeApplication(TypeExpr):
constructor: TypeConstructor | TypeApplication | TypeVariable
argument: TypeExpr
def __init__(self, constructor: TypeConstructor | TypeApplication | TypeVariable, argument: TypeExpr) -> None:
if isinstance(constructor.kind, Star):
raise TypeError("A constructor cannot be a concrete type")
if constructor.kind.arg_kind != argument.kind:
raise TypeError("Argument does match construtor's expectations")
super().__init__(
constructor.kind.result_kind,
f"{constructor.name} {argument.name}",
)
self.constructor = constructor
self.argument = argument
def occurs(lft: TypeVariable, rgt: TypeApplication) -> bool:
"""
Checks whether the given variable occurs in the given application.
"""
if lft == rgt.constructor:
return True
if lft == rgt.argument:
return True
if isinstance(rgt.argument, TypeApplication):
return occurs(lft, rgt.argument)
return False
def is_concrete(lft: TypeExpr) -> bool:
"""
A concrete type has no variables in it.
This is also known as a monomorphic type.
"""
if isinstance(lft, AtomicType):
return True
if isinstance(lft, TypeVariable):
return False
if isinstance(lft, TypeConstructor):
return True
if isinstance(lft, TypeApplication):
return is_concrete(lft.constructor) and is_concrete(lft.argument)
raise NotImplementedError
def is_polymorphic(lft: TypeExpr) -> bool:
"""
A polymorphic type has one or more variables in it.
"""
return not is_concrete(lft)
def replace_variable(expr: TypeExpr, var: TypeVariable, rep_expr: TypeExpr) -> TypeExpr:
if isinstance(expr, AtomicType):
# Nothing to replace
return expr
if isinstance(expr, TypeVariable):
if expr == var:
return rep_expr
return expr
if isinstance(expr, TypeConstructor):
return expr
if isinstance(expr, TypeApplication):
new_constructor = replace_variable(expr.constructor, var, rep_expr)
assert isinstance(new_constructor, TypeConstructor | TypeApplication | TypeVariable) # type hint
return TypeApplication(
constructor=new_constructor,
argument=replace_variable(expr.argument, var, rep_expr),
)
raise NotImplementedError

118
phasm/type5/unify.py Normal file
View File

@ -0,0 +1,118 @@
from dataclasses import dataclass
from .typeexpr import (
AtomicType,
TypeApplication,
TypeConstructor,
TypeExpr,
TypeVariable,
is_concrete,
occurs,
)
@dataclass
class Failure:
"""
Both types are already different - cannot be unified.
"""
msg: str
@dataclass
class Action:
pass
class ActionList(list[Action]):
def __str__(self) -> str:
return '{' + ', '.join(map(str, self)) + '}'
UnifyResult = Failure | ActionList
@dataclass
class ReplaceVariable(Action):
var: TypeVariable
typ: TypeExpr
def __str__(self) -> str:
return f'{self.var.name} := {self.typ.name}'
def unify(lft: TypeExpr, rgt: TypeExpr) -> UnifyResult:
if lft == rgt:
return ActionList()
if lft.kind != rgt.kind:
return Failure("Kind mismatch")
if isinstance(lft, AtomicType) and isinstance(rgt, AtomicType):
return Failure("Not the same type")
if isinstance(lft, AtomicType) and isinstance(rgt, TypeVariable):
return ActionList([ReplaceVariable(rgt, lft)])
if isinstance(lft, AtomicType) and isinstance(rgt, TypeConstructor):
raise NotImplementedError # Should have been caught by kind check above
if isinstance(lft, AtomicType) and isinstance(rgt, TypeApplication):
if is_concrete(rgt):
return Failure("Not the same type")
return Failure("Type shape mismatch")
if isinstance(lft, TypeVariable) and isinstance(rgt, AtomicType):
return unify(rgt, lft)
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeVariable):
return ActionList([ReplaceVariable(lft, rgt)])
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeConstructor):
return ActionList([ReplaceVariable(lft, rgt)])
if isinstance(lft, TypeVariable) and isinstance(rgt, TypeApplication):
if occurs(lft, rgt):
return Failure("One type occurs in the other")
return ActionList([ReplaceVariable(lft, rgt)])
if isinstance(lft, TypeConstructor) and isinstance(rgt, AtomicType):
return unify(rgt, lft)
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeVariable):
return unify(rgt, lft)
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeConstructor):
return Failure("Not the same type constructor")
if isinstance(lft, TypeConstructor) and isinstance(rgt, TypeApplication):
return Failure("Not the same type constructor")
if isinstance(lft, TypeApplication) and isinstance(rgt, AtomicType):
return unify(rgt, lft)
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeVariable):
return unify(rgt, lft)
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeConstructor):
return unify(rgt, lft)
if isinstance(lft, TypeApplication) and isinstance(rgt, TypeApplication):
con_res = unify(lft.constructor, rgt.constructor)
if isinstance(con_res, Failure):
return con_res
arg_res = unify(lft.argument, rgt.argument)
if isinstance(arg_res, Failure):
return arg_res
return ActionList(con_res + arg_res)
return Failure('Not implemented')

View File

@ -11,6 +11,7 @@ from phasm.compiler import phasm_compile
from phasm.optimise.removeunusedfuncs import removeunusedfuncs from phasm.optimise.removeunusedfuncs import removeunusedfuncs
from phasm.parser import phasm_parse from phasm.parser import phasm_parse
from phasm.type3.entry import phasm_type3 from phasm.type3.entry import phasm_type3
from phasm.type5.solver import phasm_type5
from phasm.wasmgenerator import Generator as WasmGenerator from phasm.wasmgenerator import Generator as WasmGenerator
Imports = Optional[Dict[str, Callable[[Any], Any]]] Imports = Optional[Dict[str, Callable[[Any], Any]]]
@ -39,6 +40,7 @@ class RunnerBase:
Parses the Phasm code into an AST Parses the Phasm code into an AST
""" """
self.phasm_ast = phasm_parse(self.phasm_code) self.phasm_ast = phasm_parse(self.phasm_code)
phasm_type5(self.phasm_ast, verbose=verbose)
phasm_type3(self.phasm_ast, verbose=verbose) phasm_type3(self.phasm_ast, verbose=verbose)
def compile_ast(self) -> None: def compile_ast(self) -> None:

View File

@ -3,6 +3,21 @@ import pytest
from ..helpers import Suite from ..helpers import Suite
@pytest.mark.integration_test
def test_call_nullary():
code_py = """
def helper() -> i32:
return 3
@exported
def testEntry() -> i32:
return helper()
"""
result = Suite(code_py).run_code()
assert 3 == result.returned_value
@pytest.mark.integration_test @pytest.mark.integration_test
def test_call_pre_defined(): def test_call_pre_defined():
code_py = """ code_py = """

View File

@ -1,6 +1,6 @@
import pytest import pytest
from phasm.type3.entry import Type3Exception from phasm.type5.solver import Type5SolverException
from ..helpers import Suite from ..helpers import Suite
@ -15,7 +15,7 @@ def testEntry() -> u8:
return CONSTANT return CONSTANT
""" """
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): with pytest.raises(Type5SolverException, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code() Suite(code_py).run_code()
@pytest.mark.integration_test @pytest.mark.integration_test
@ -26,5 +26,5 @@ def testEntry() -> u8:
return 1000 return 1000
""" """
with pytest.raises(Type3Exception, match=r'Must fit in 1 byte\(s\)'): with pytest.raises(Type5SolverException, match=r'Must fit in 1 byte\(s\)'):
Suite(code_py).run_code() Suite(code_py).run_code()

View File

View File

@ -0,0 +1,113 @@
import pytest
from phasm.type4 import classes
from phasm.type4 import unify as sut
class TestNamespace:
## Kind * (part 1)
u32 = classes.Atomic('u32')
f64 = classes.Atomic('f64')
a = classes.Variable('a')
b = classes.Variable('b')
## Kind * -> *
dynamic_array = classes.ConcreteConSS('dynamic_array')
maybe = classes.ConcreteConSS('maybe')
f = classes.VariableConSS('f')
t = classes.VariableConSS('t')
## Kind * -> * -> *
function = classes.ConcreteConSSS('function')
either = classes.ConcreteConSSS('either')
## Kind Nat -> * -> *
static_array = classes.ConcreteConNSS('static_array')
## Kind * -> * (part 2)
function_u32 = classes.AppliedConSSS(function, u32)
function_f64 = classes.AppliedConSSS(function, f64)
either_u32 = classes.AppliedConSSS(either, u32)
either_f64 = classes.AppliedConSSS(either, f64)
static_array_4 = classes.AppliedConNSS(static_array, classes.Nat(4))
static_array_8 = classes.AppliedConNSS(static_array, classes.Nat(8))
## Kind * (part 2)
dynamic_array_u32 = classes.AppliedConSS(dynamic_array, u32)
dynamic_array_f64 = classes.AppliedConSS(dynamic_array, f64)
maybe_u32 = classes.AppliedConSS(maybe, u32)
maybe_f64 = classes.AppliedConSS(maybe, f64)
t_u32 = classes.AppliedConSS(t, u32)
t_f64 = classes.AppliedConSS(t, f64)
f_u32 = classes.AppliedConSS(f, u32)
f_f64 = classes.AppliedConSS(f, f64)
function_f64_u32 = classes.AppliedConSS(function_f64, u32)
either_f64_u32 = classes.AppliedConSS(either_f64, u32)
static_array_4_u32 = classes.AppliedConSS(static_array_4, u32)
static_array_8_u32 = classes.AppliedConSS(static_array_8, u32)
TEST_LIST: list[tuple[classes.Type4, classes.Type4, sut.UnifyResult]] = [
# Atomic / Atomic
(u32, u32, []),
(u32, f64, sut.Failure('Cannot unify u32 and f64')),
# Atomic / Variable
(u32, a, [sut.SetTypeForVariable(a, u32)]),
# Atomic / AppliedConSS
(u32, t_f64, sut.Failure('Cannot unify u32 and t f64')),
# Variable / Atomic
(a, u32, [sut.SetTypeForVariable(a, u32)]),
# Variable / Variable
(a, b, [sut.ReplaceVariable(a, b)]),
# Variable / AppliedConSS
(a, t_f64, [sut.SetTypeForVariable(a, t_f64)]),
# AppliedConSS / Atomic
(t_f64, u32, sut.Failure('Cannot unify t f64 and u32')),
# AppliedConSS / Variable
(t_f64, a, [sut.SetTypeForVariable(a, t_f64)]),
# AppliedConSS ConcreteConSS / AppliedConSS ConcreteConSS
(dynamic_array_u32, dynamic_array_u32, []),
(dynamic_array_u32, maybe_u32, sut.Failure('Cannot unify dynamic_array u32 and maybe u32')),
# AppliedConSS ConcreteConSS / AppliedConSS VariableConSS
(dynamic_array_u32, t_u32, [sut.SetTypeForConSSVariable(t, dynamic_array)]),
# AppliedConSS ConcreteConSS / AppliedConSS AppliedConSSS
(dynamic_array_u32, function_f64_u32, sut.Failure('Cannot unify dynamic_array u32 and function f64 u32')),
# AppliedConSS ConcreteConSS / AppliedConSS AppliedConNSS
(dynamic_array_u32, static_array_4_u32, sut.Failure('Cannot unify dynamic_array u32 and static_array 4 u32')),
# AppliedConSS VariableConSS / AppliedConSS ConcreteConSS
(t_u32, dynamic_array_u32, [sut.SetTypeForConSSVariable(t, dynamic_array)]),
# AppliedConSS VariableConSS / AppliedConSS VariableConSS
(t_u32, f_u32, [sut.ReplaceConSSVariable(t, f)]),
# AppliedConSS VariableConSS / AppliedConSS AppliedConSSS
(t_u32, function_f64_u32, [sut.SetTypeForConSSVariable(t, function_f64)]),
# AppliedConSS VariableConSS / AppliedConSS AppliedConNSS
(t_u32, static_array_4_u32, [sut.SetTypeForConSSVariable(t, static_array_4)]),
# AppliedConSS AppliedConSSS / AppliedConSS ConcreteConSS
(function_f64_u32, dynamic_array_u32, sut.Failure('Cannot unify function f64 u32 and dynamic_array u32')),
# AppliedConSS AppliedConSSS / AppliedConSS VariableConSS
(function_f64_u32, t_u32, [sut.SetTypeForConSSVariable(t, function_f64)]),
# AppliedConSS AppliedConSSS / AppliedConSS AppliedConSSS
# (function_f64_u32, function_f64_u32, []),
(function_f64_u32, either_f64_u32, sut.Failure('Cannot unify function f64 u32 and either f64 u32')),
# AppliedConSS AppliedConSSS / AppliedConSS AppliedConNSS
(function_f64_u32, static_array_4_u32, sut.Failure('Cannot unify function f64 u32 and static_array 4 u32')),
# AppliedConSS AppliedConNSS / AppliedConSS ConcreteConSS
(static_array_4_u32, dynamic_array_u32, sut.Failure('Cannot unify static_array 4 u32 and dynamic_array u32')),
# AppliedConSS AppliedConNSS / AppliedConSS VariableConSS
(static_array_4_u32, t_u32, [sut.SetTypeForConSSVariable(t, static_array_4)]),
# AppliedConSS AppliedConNSS / AppliedConSS AppliedConSSS
(static_array_4_u32, function_f64_u32, sut.Failure('Cannot unify static_array 4 u32 and function f64 u32')),
# AppliedConSS AppliedConNSS / AppliedConSS AppliedConNSS
(static_array_4_u32, static_array_4_u32, []),
(static_array_4_u32, static_array_8_u32, sut.Failure('Cannot unify static_array 4 u32 and static_array 8 u32')),
]
@pytest.mark.parametrize('lft,rgt,exp_out', TestNamespace.TEST_LIST)
def test_unify(lft: classes.Type4, rgt: classes.Type4, exp_out: sut.UnifyResult) -> None:
assert exp_out == sut.unify(lft, rgt)