This commit is contained in:
Johan B.W. de Vries 2025-07-12 11:31:05 +02:00
parent 1a3bc19dce
commit 14c316a0dc
27 changed files with 1610 additions and 74 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

@ -3,7 +3,7 @@ The base class for build environments.
Contains nothing but the explicit compiler builtins. Contains nothing but the explicit compiler builtins.
""" """
from typing import Any, Callable, NamedTuple, Type from typing import Any, Callable, NamedTuple, Sequence, Type
from warnings import warn from warnings import warn
from ..type3.functions import ( from ..type3.functions import (
@ -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,18 +57,22 @@ 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_',
'none_', 'none_',
'unit_type5',
'bool_', 'bool_',
'type_info_map', 'type_info_map',
'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 +90,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 +99,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.
@ -118,6 +128,8 @@ class BuildBase[G]:
The none type, for when functions simply don't return anything. e.g., IO(). The none type, for when functions simply don't return anything. e.g., IO().
""" """
unit_type5: type5typeexpr.TypeExpr
bool_: Type3 bool_: Type3
""" """
The bool type, either True or False The bool type, either True or False
@ -142,6 +154,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,7 +196,13 @@ 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.unit_type5 = type5typeexpr.AtomicType('()')
self.none_ = builtins.none_ self.none_ = builtins.none_
self.type_info_map = { self.type_info_map = {
@ -193,6 +216,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 = {}
@ -337,3 +361,61 @@ class BuildBase[G]:
result += self.calculate_alloc_size(memtyp, is_member=True) result += self.calculate_alloc_size(memtyp, is_member=True)
raise Exception(f'{needle} not in {st_name}') raise Exception(f'{needle} not in {st_name}')
def type5_make_function(self, args: Sequence[type5typeexpr.TypeExpr]) -> type5typeexpr.TypeExpr:
if not args:
raise TypeError("Functions must at least have a return type")
if len(args) == 1:
# Functions always take an argument
# To distinguish between a function without arguments and a value
# of the type, we have a unit type
# This type has one value so it can always be called
args = [self.unit_type5, *args]
res_type5 = None
for arg_type5 in reversed(args):
if res_type5 is None:
res_type5 = arg_type5
continue
res_type5 = type5typeexpr.TypeApplication(
constructor=type5typeexpr.TypeApplication(
constructor=self.function_type5_constructor,
argument=arg_type5,
),
argument=res_type5,
)
assert res_type5 is not None # type hint
return res_type5
def type5_is_function(self, typeexpr: type5typeexpr.TypeExpr) -> list[type5typeexpr.TypeExpr] | None:
if not isinstance(typeexpr, type5typeexpr.TypeApplication):
return None
if not isinstance(typeexpr.constructor, type5typeexpr.TypeApplication):
return None
if typeexpr.constructor.constructor != self.function_type5_constructor:
return None
arg0 = typeexpr.constructor.argument
if arg0 is self.unit_type5:
my_args = []
else:
my_args = [arg0]
arg1 = typeexpr.argument
more_args = self.type5_is_function(arg1)
if more_args is None:
return my_args + [arg1]
return my_args + more_args
def type5_name(self, typ: type5typeexpr.TypeExpr) -> str:
func_args = self.type5_is_function(typ)
if func_args is None:
return typ.name
return 'Callable[' + ', '.join(map(self.type5_name, func_args)) + ']'

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,14 @@ class BuildDefault(BuildBase[Generator]):
'bytes': bytes_, 'bytes': bytes_,
}) })
u8_5 = type5typeexpr.AtomicType('u8')
self.type5s.update({
'u8': u8_5,
'i32': type5typeexpr.AtomicType('i32'),
'f32': type5typeexpr.AtomicType('f32'),
'bytes': type5typeexpr.TypeApplication(self.dynamic_array_type5_constructor, u8_5),
})
tc_list = [ tc_list = [
bits, bits,
eq, ord, eq, ord,

View File

@ -7,6 +7,7 @@ from typing import Any, Generator
from . import ourlang from . import ourlang
from .type3.types import Type3, TypeApplication_Struct from .type3.types import Type3, TypeApplication_Struct
from .type5 import typeexpr as type5typeexpr
def phasm_render(inp: ourlang.Module[Any]) -> str: def phasm_render(inp: ourlang.Module[Any]) -> str:
@ -23,6 +24,12 @@ def type3(inp: Type3) -> str:
""" """
return inp.name return inp.name
def type5(mod: ourlang.Module[Any], inp: type5typeexpr.TypeExpr) -> str:
"""
Render: type's name
"""
return mod.build.type5_name(inp)
def struct_definition(inp: ourlang.StructDefinition) -> str: def struct_definition(inp: ourlang.StructDefinition) -> str:
""" """
Render: TypeStruct's definition Render: TypeStruct's definition
@ -128,7 +135,7 @@ def statement(inp: ourlang.Statement) -> Statements:
raise NotImplementedError(statement, inp) raise NotImplementedError(statement, inp)
def function(inp: ourlang.Function) -> str: def function(mod: ourlang.Module[Any], inp: ourlang.Function) -> str:
""" """
Render: Function body Render: Function body
@ -142,7 +149,7 @@ def function(inp: ourlang.Function) -> str:
result += '@imported\n' result += '@imported\n'
args = ', '.join( args = ', '.join(
f'{p.name}: {type3(p.type3)}' f'{p.name}: {type5(mod, p.type5)}'
for p in inp.posonlyargs for p in inp.posonlyargs
) )
@ -175,12 +182,12 @@ 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:
result += '\n' result += '\n'
result += function(func) result += function(inp, func)
return result return 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,41 @@ 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
def __repr__(self) -> str:
return f"SourceRef({self.filename!r}, {self.lineno!r}, {self.colno!r})"
def __str__(self) -> str:
return f"{self.filename}:{self.lineno:>4}:{self.colno:<3}"
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 +59,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 +144,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):
@ -135,8 +158,8 @@ class BinaryOp(Expression):
left: Expression left: Expression
right: Expression right: Expression
def __init__(self, operator: Type3ClassMethod, left: Expression, right: Expression) -> None: def __init__(self, operator: Type3ClassMethod, left: Expression, right: Expression, sourceref: SourceRef) -> None:
super().__init__() super().__init__(sourceref=sourceref)
self.operator = operator self.operator = operator
self.left = left self.left = left
@ -154,8 +177,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 +245,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 +264,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 +291,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 +311,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 +359,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 +375,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 +421,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 +435,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, srf(module, node), 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,13 @@ 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
function.type5 = module.build.type5_make_function(arg_type5_list)
_not_implemented(not node.type_comment, 'FunctionDef.type_comment') _not_implemented(not node.type_comment, 'FunctionDef.type_comment')
return function return function
@ -249,7 +262,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())),
srf(module, node),
)
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 +278,9 @@ class OurVisitor[G]:
return ModuleConstantDef( return ModuleConstantDef(
node.target.id, node.target.id,
node.lineno, srf(module, node),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -275,8 +292,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, srf(module, node),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -288,8 +306,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, srf(module, node),
self.visit_type(module, node.annotation), self.visit_type(module, node.annotation),
self.visit_type5(module, node.annotation),
value_data, value_data,
) )
@ -328,7 +347,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),
srf(module, node),
) )
if isinstance(node, ast.If): if isinstance(node, ast.If):
@ -389,6 +409,7 @@ class OurVisitor[G]:
module.operators[operator], module.operators[operator],
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left), self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.right), self.visit_Module_FunctionDef_expr(module, function, our_locals, node.right),
srf(module, node),
) )
if isinstance(node, ast.Compare): if isinstance(node, ast.Compare):
@ -417,6 +438,7 @@ class OurVisitor[G]:
module.operators[operator], module.operators[operator],
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left), self.visit_Module_FunctionDef_expr(module, function, our_locals, node.left),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.comparators[0]), self.visit_Module_FunctionDef_expr(module, function, our_locals, node.comparators[0]),
srf(module, node),
) )
if isinstance(node, ast.Call): if isinstance(node, ast.Call):
@ -443,11 +465,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, srf(module, node))
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, srf(module, node))
if node.id in module.functions: if node.id in module.functions:
fun = module.functions[node.id] fun = module.functions[node.id]
@ -490,7 +512,7 @@ class OurVisitor[G]:
func = module.functions[node.func.id] func = module.functions[node.func.id]
result = FunctionCall(func) result = FunctionCall(func, sourceref=srf(module, node))
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 +549,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, srf(module, node))
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, srf(module, node))
else: else:
_raise_static_error(node, f'Undefined variable {node.value.id}') _raise_static_error(node, f'Undefined variable {node.value.id}')
@ -588,7 +610,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, srf(module, node))
if isinstance(node.value, bytes): if isinstance(node.value, bytes):
data_block = ModuleDataBlock([]) data_block = ModuleDataBlock([])
@ -664,6 +686,34 @@ 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}')
if isinstance(node, ast.Subscript):
if isinstance(node.value, ast.Name) and node.value.id == 'Callable':
func_arg_types: list[ast.expr]
if isinstance(node.slice, ast.Name):
func_arg_types = [node.slice]
elif isinstance(node.slice, ast.Tuple):
func_arg_types = node.slice.elts
else:
_raise_static_error(node, 'Must subscript using a list of types')
return module.build.type5_make_function([
self.visit_type5(module, e)
for e in func_arg_types
])
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)
@ -672,3 +722,6 @@ def _raise_static_error(node: Union[ast.stmt, ast.expr], msg: str) -> NoReturn:
raise StaticError( raise StaticError(
f'Static error on line {node.lineno}: {msg}' f'Static error on line {node.lineno}: {msg}'
) )
def srf(mod: Module[Any], node: ast.stmt | ast.expr) -> SourceRef:
return SourceRef(mod.filename, node.lineno, node.col_offset)

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

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

@ -0,0 +1,145 @@
from typing import Any, Protocol
from ..build.base import BuildBase
from ..ourlang import SourceRef
from ..wasm import WasmTypeFloat32, WasmTypeFloat64, 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 type_info is not None and (type_info.wasm_type is WasmTypeFloat32 or type_info.wasm_type is WasmTypeFloat64):
if isinstance(self.literal.value, float):
# FIXME: Bit check
return None
return Failure('Must be real')
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.ctx.build.type5_name(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.ctx.build.type5_name(self.lft)} ~ {self.ctx.build.type5_name(self.rgt)}"

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

@ -0,0 +1,161 @@
from typing import Any, Generator
from .. import ourlang
from ..type3 import functions as type3functions
from ..type3 import types as type3types
from .constraints import (
ConstraintBase,
Context,
LiteralFitsConstraint,
UnifyTypesConstraint,
)
from .kindexpr import Star
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_binary_operator(ctx: Context, inp: ourlang.BinaryOp, phft: TypeVariable) -> ConstraintGenerator:
yield from expression_function_call(
ctx,
_binary_op_to_function(ctx, inp),
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 = ctx.build.type5_make_function(arg_typ_list + [phft])
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.function.type5, expr_type)
def expression_function_reference(ctx: Context, inp: ourlang.FunctionReference, phft: TypeVariable) -> ConstraintGenerator:
assert inp.function.type5 is not None # Todo: Make not nullable
yield UnifyTypesConstraint(ctx, inp.sourceref, inp.function.type5, phft)
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.BinaryOp):
yield from expression_binary_operator(ctx, inp, phft)
return
if isinstance(inp, ourlang.FunctionCall):
yield from expression_function_call(ctx, inp, phft)
return
if isinstance(inp, ourlang.FunctionReference):
yield from expression_function_reference(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.")
if isinstance(fun.type5, TypeApplication):
args = ctx.build.type5_is_function(fun.type5)
assert args is not None
type5 = args[-1]
else:
type5 = fun.type5
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?
def _binary_op_to_function(ctx: Context, inp: ourlang.BinaryOp) -> ourlang.FunctionCall:
"""
Temporary method while migrating from type3 to type5
"""
opr = inp.operator
fun = ourlang.Function(opr.name, ourlang.SourceRef("(generated)", -1, -1), ctx.build.none_)
fun.type5 = _signature_to_type5(ctx, opr.signature)
assert inp.sourceref is not None # TODO: sourceref required
call = ourlang.FunctionCall(fun, inp.sourceref)
call.arguments = [inp.left, inp.right]
return call
def _signature_to_type5(ctx: Context, signature: type3functions.FunctionSignature) -> TypeExpr:
tv_map: dict[type3functions.TypeVariable, TypeVariable] = {}
s = Star()
args: list[TypeExpr] = []
for t3arg in signature.args:
if isinstance(t3arg, type3types.Type3):
args.append(ctx.build.type5s[t3arg.name])
continue
if isinstance(t3arg, type3functions.TypeVariable):
tv_map.setdefault(t3arg, TypeVariable(kind=s, name=t3arg.name))
args.append(tv_map[t3arg])
continue
raise NotImplementedError(t3arg)
return ctx.build.type5_make_function(args)

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

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

@ -0,0 +1,79 @@
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[tuple[str, str, str]] = []
for _ in range(MAX_RESTACK_COUNT):
if verbose:
for constraint in constraint_list:
sourceref = constraint.sourceref or SourceRef("?", 0, 0)
print(f"{sourceref!s} {constraint!s:<20}")
print()
new_constraint_list = []
for constraint in constraint_list:
result = constraint.check()
sourceref = constraint.sourceref or SourceRef("?", 0, 0)
if verbose:
print(f"{sourceref!s} {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((str(sourceref), str(constraint), result.msg, ))
continue
if isinstance(result, ActionList):
for action in result:
if isinstance(action, ReplaceVariable):
exist_typ = placeholder_types.get(action.var)
if exist_typ is None:
placeholder_types[action.var] = action.typ
else:
if exist_typ != action.typ:
raise Exception('When does this happen?', exist_typ, 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

@ -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
@ -78,11 +78,29 @@ def testEntry() -> i32:
assert 42 == result.returned_value assert 42 == result.returned_value
@pytest.mark.integration_test @pytest.mark.integration_test
def test_sof_wrong_argument_type(): def test_sof_function_with_wrong_argument_type_use():
code_py = """ code_py = """
def double(left: f32) -> f32: def double(left: i32) -> i32:
return left * 2 return left * 2
def action(applicable: Callable[i32, i32], left: f32) -> i32:
return applicable(left)
@exported
def testEntry() -> i32:
return action(double, 13.0)
"""
match = r'Callable\[i32, i32\] ~ Callable\[f32, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_function_with_wrong_argument_type_pass():
code_py = """
def double(left: f32) -> i32:
return truncate(left) * 2
def action(applicable: Callable[i32, i32], left: i32) -> i32: def action(applicable: Callable[i32, i32], left: i32) -> i32:
return applicable(left) return applicable(left)
@ -91,11 +109,12 @@ def testEntry() -> i32:
return action(double, 13) return action(double, 13)
""" """
with pytest.raises(Type3Exception, match=r'Callable\[f32, f32\] must be Callable\[i32, i32\] instead'): match = r'Callable\[Callable\[i32, i32\], i32, i32\] ~ Callable\[Callable\[f32, i32\], p_[0-9]+, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code() Suite(code_py).run_code()
@pytest.mark.integration_test @pytest.mark.integration_test
def test_sof_wrong_return(): def test_sof_function_with_wrong_return_type_use():
code_py = """ code_py = """
def double(left: i32) -> i32: def double(left: i32) -> i32:
return left * 2 return left * 2
@ -103,17 +122,35 @@ def double(left: i32) -> i32:
def action(applicable: Callable[i32, i32], left: i32) -> f32: def action(applicable: Callable[i32, i32], left: i32) -> f32:
return applicable(left) return applicable(left)
@exported
def testEntry() -> f32:
return action(double, 13)
"""
match = r'Callable\[i32, i32\] ~ Callable\[i32, f32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_sof_function_with_wrong_return_type_pass():
code_py = """
def double(left: i32) -> f32:
return convert(left) * 2
def action(applicable: Callable[i32, i32], left: i32) -> i32:
return applicable(left)
@exported @exported
def testEntry() -> i32: def testEntry() -> i32:
return action(double, 13) return action(double, 13)
""" """
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'): match = r'Callable\[Callable\[i32, i32\], i32, i32\] ~ Callable\[Callable\[i32, f32\], p_[0-9]+, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code() Suite(code_py).run_code()
@pytest.mark.integration_test @pytest.mark.integration_test
@pytest.mark.skip('FIXME: Probably have the remainder be the a function type') def test_sof_not_enough_args_use():
def test_sof_wrong_not_enough_args_call():
code_py = """ code_py = """
def add(left: i32, right: i32) -> i32: def add(left: i32, right: i32) -> i32:
return left + right return left + right
@ -126,11 +163,12 @@ def testEntry() -> i32:
return action(add, 13) return action(add, 13)
""" """
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'): match = r'Callable\[i32, i32, i32\] ~ Callable\[i32, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code() Suite(code_py).run_code()
@pytest.mark.integration_test @pytest.mark.integration_test
def test_sof_wrong_not_enough_args_refere(): def test_sof_not_enough_args_pass():
code_py = """ code_py = """
def double(left: i32) -> i32: def double(left: i32) -> i32:
return left * 2 return left * 2
@ -143,12 +181,12 @@ def testEntry() -> i32:
return action(double, 13, 14) return action(double, 13, 14)
""" """
with pytest.raises(Type3Exception, match=r'Callable\[i32, i32\] must be Callable\[i32, i32, i32\] instead'): match = r'Callable\[Callable\[i32, i32, i32\], i32, i32, i32\] ~ Callable\[Callable\[i32, i32\], p_[0-9]+, p_[0-9]+, i32\]'
with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code() Suite(code_py).run_code()
@pytest.mark.integration_test @pytest.mark.integration_test
@pytest.mark.skip('FIXME: Probably have the remainder be the a function type') def test_sof_too_many_args_use():
def test_sof_wrong_too_many_args_call():
code_py = """ code_py = """
def thirteen() -> i32: def thirteen() -> i32:
return 13 return 13
@ -161,22 +199,24 @@ def testEntry() -> i32:
return action(thirteen, 13) return action(thirteen, 13)
""" """
with pytest.raises(Type3Exception, match=r'f32 must be i32 instead'): match = r'Callable\[i32\] ~ Callable\[i32, i32\]'
Suite(code_py).run_code() with pytest.raises(Type5SolverException, match=match):
Suite(code_py).run_code(verbose=True)
@pytest.mark.integration_test @pytest.mark.integration_test
def test_sof_wrong_too_many_args_refere(): def test_sof_too_many_args_pass():
code_py = """ code_py = """
def double(left: i32) -> i32: def double(left: i32) -> i32:
return left * 2 return left * 2
def action(applicable: Callable[i32]) -> i32: def action(applicable: Callable[i32], left: i32, right: i32) -> i32:
return applicable() return applicable()
@exported @exported
def testEntry() -> i32: def testEntry() -> i32:
return action(double) return action(double, 13, 14)
""" """
with pytest.raises(Type3Exception, match=r'Callable\[i32, i32\] must be Callable\[i32\] instead'): match = r'Callable\[Callable\[i32\], i32, i32, i32\] ~ Callable\[Callable\[i32, i32\], p_[0-9]+, p_[0-9]+, i32\]'
with pytest.raises(Type5SolverException, match=match):
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)