phasm/phasm/build/base.py
Johan B.W. de Vries 45956f6d8a Notes
2025-08-02 14:58:08 +02:00

552 lines
18 KiB
Python

"""
The base class for build environments.
Contains nothing but the explicit compiler builtins.
"""
from typing import Any, Callable, NamedTuple, Sequence, Type
from warnings import warn
from ..type3.functions import (
TypeConstructorVariable,
TypeVariable,
)
from ..type3.routers import (
NoRouteForTypeException,
TypeApplicationRouter,
TypeClassArgsRouter,
TypeVariableLookup,
)
from ..type3.typeclasses import Type3Class, Type3ClassMethod
from ..type3.types import (
IntType3,
Type3,
TypeConstructor_Base,
TypeConstructor_DynamicArray,
TypeConstructor_Function,
TypeConstructor_StaticArray,
TypeConstructor_Struct,
TypeConstructor_Tuple,
)
from ..type5 import kindexpr as type5kindexpr
from ..type5 import record as type5record
from ..type5 import typeexpr as type5typeexpr
from ..wasm import WasmType, WasmTypeInt32, WasmTypeNone
from . import builtins
from .typerouter import TypeName
TypeInfo = NamedTuple('TypeInfo', [
# Name of the type
('typ', str, ),
# What WebAssembly type to use when passing this value around
# For example in function arguments
('wasm_type', Type[WasmType]),
# What WebAssembly function to use when loading a value from memory
('wasm_load_func', str),
# What WebAssembly function to use when storing a value to memory
('wasm_store_func', str),
# When storing this value in memory, how many bytes do we use?
# Only valid for non-constructed types, see calculate_alloc_size
# Should match wasm_load_func / wasm_store_func
('alloc_size', int),
# When storing integers, the values can be stored as natural number
# (False) or as integer number (True). For other types, this is None.
('signed', bool | None),
])
class MissingImplementationWarning(Warning):
pass
class BuildBase[G]:
__slots__ = (
'dynamic_array',
'dynamic_array_type5_constructor',
'function',
'function_type5_constructor',
'static_array',
'static_array_type5_constructor',
'struct',
'tuple_',
'tuple_type5_constructor_map',
'none_',
'none_type5',
'unit_type5',
'bool_',
'bool_type5',
'u8_type5',
'u32_type5',
'type_info_map',
'type_info_constructed',
'types',
'type5s',
'type_classes',
'type_class_instances',
'type_class_instance_methods',
'methods',
'operators',
'type5_name',
'alloc_size_router',
)
dynamic_array: TypeConstructor_DynamicArray
"""
This is a dynamic length piece of memory.
It should be applied with two arguments. It has a runtime
determined length, and each argument is the same.
"""
dynamic_array_type5_constructor: type5typeexpr.TypeConstructor
function: TypeConstructor_Function
"""
This is a function.
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
"""
This is a fixed length piece of memory.
It should be applied with two arguments. It has a compile time
determined length, and each argument is the same.
"""
static_array_type5_constructor: type5typeexpr.TypeConstructor
struct: TypeConstructor_Struct
"""
This is like a tuple, but each argument is named, so that developers
can get and set fields by name.
"""
tuple_: TypeConstructor_Tuple
"""
This is a fixed length piece of memory.
It should be applied with zero or more arguments. It has a compile time
determined length, and each argument can be different.
"""
tuple_type5_constructor_map: dict[int, type5typeexpr.TypeConstructor]
none_: Type3
"""
The none type, for when functions simply don't return anything. e.g., IO().
"""
none_type5: type5typeexpr.AtomicType
unit_type5: type5typeexpr.AtomicType
bool_: Type3
"""
The bool type, either True or False
"""
bool_type5: type5typeexpr.AtomicType
u8_type5: type5typeexpr.AtomicType
u32_type5: type5typeexpr.AtomicType
type_info_map: dict[str, TypeInfo]
"""
Map from type name to the info of that type
"""
type_info_constructed: TypeInfo
"""
By default, constructed types are passed as pointers
NOTE: ALLOC SIZE IN THIS STRUCT DOES NOT WORK FOR CONSTRUCTED TYPES
USE calculate_alloc_size FOR ACCURATE RESULTS
Functions count as constructed types - even though they are
not memory pointers but table addresses instead.
"""
types: dict[str, Type3]
"""
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 that are available without explicit import.
"""
type_class_instances: set[tuple[Type3Class, tuple[Type3 | TypeConstructor_Base[Any], ...]]]
"""
Type class instances that are available without explicit import.
"""
type_class_instance_methods: dict[Type3ClassMethod, TypeClassArgsRouter[G, None]]
"""
Methods (and operators) for type class instances that are available without explicit import.
"""
methods: dict[str, Type3ClassMethod]
"""
Methods that are available without explicit import.
"""
operators: dict[str, Type3ClassMethod]
"""
Operators that are available without explicit import.
"""
type5_name: TypeName
"""
Helper router to turn types into their human readable names.
"""
alloc_size_router: TypeApplicationRouter['BuildBase[G]', int]
"""
Helper value for calculate_alloc_size.
"""
def __init__(self) -> None:
self.dynamic_array = builtins.dynamic_array
self.function = builtins.function
self.static_array = builtins.static_array
self.struct = builtins.struct
self.tuple_ = builtins.tuple_
S = type5kindexpr.Star()
N = type5kindexpr.Nat()
self.function_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> (S >> S), name="function")
self.tuple_type5_constructor_map = {}
self.dynamic_array_type5_constructor = type5typeexpr.TypeConstructor(kind=S >> S, name="dynamic_array")
self.static_array_type5_constructor = type5typeexpr.TypeConstructor(kind=N >> (S >> S), name='static_array')
self.bool_ = builtins.bool_
self.bool_type5 = type5typeexpr.AtomicType('bool')
self.unit_type5 = type5typeexpr.AtomicType('()')
self.none_ = builtins.none_
self.none_type5 = type5typeexpr.AtomicType('None')
self.u8_type5 = type5typeexpr.AtomicType('u8')
self.u32_type5 = type5typeexpr.AtomicType('u32')
self.type_info_map = {
'None': TypeInfo('ptr', WasmTypeNone, 'unreachable', 'unreachable', 0, None),
'bool': TypeInfo('bool', WasmTypeInt32, 'unreachable', 'unreachable', 0, None),
'ptr': TypeInfo('ptr', WasmTypeInt32, 'i32.load', 'i32.store', 4, False),
}
self.type_info_constructed = self.type_info_map['ptr']
self.types = {
'None': self.none_,
'bool': self.bool_,
}
self.type5s = {
'bool': self.bool_type5,
'u8': self.u8_type5,
'u32': self.u32_type5,
}
self.type_classes = {}
self.type_class_instances = set()
self.type_class_instance_methods = {}
self.methods = {}
self.operators = {}
self.alloc_size_router = TypeApplicationRouter['BuildBase[G]', int]()
self.alloc_size_router.add(self.static_array, self.__class__.calculate_alloc_size_static_array)
self.alloc_size_router.add(self.struct, self.__class__.calculate_alloc_size_struct)
self.alloc_size_router.add(self.tuple_, self.__class__.calculate_alloc_size_tuple)
self.type5_name = TypeName(self)
def register_type_class(self, cls: Type3Class) -> None:
"""
Register that the given type class exists
"""
old_len_methods = len(self.methods)
old_len_operators = len(self.operators)
self.type_classes[cls.name] = cls
self.methods.update(cls.methods)
self.operators.update(cls.operators)
assert len(self.methods) == old_len_methods + len(cls.methods), 'Duplicated method detected'
assert len(self.operators) == old_len_operators + len(cls.operators), 'Duplicated operator detected'
def instance_type_class(
self,
cls: Type3Class,
*typ: Type3 | TypeConstructor_Base[Any],
methods: dict[str, Callable[[G, TypeVariableLookup], None]] = {},
operators: dict[str, Callable[[G, TypeVariableLookup], None]] = {},
) -> None:
"""
Registered the given type class and its implementation
"""
assert len(cls.args) == len(typ)
for incls in cls.inherited_classes:
if (incls, tuple(typ), ) not in self.type_class_instances:
warn(MissingImplementationWarning(
incls.name + ' ' + ' '.join(x.name for x in typ) + ' - required for ' + cls.name
))
# First just register the type
self.type_class_instances.add((cls, tuple(typ), ))
# Then make the implementation findable
# We route based on the type class arguments.
tv_map: dict[TypeVariable, Type3] = {}
tc_map: dict[TypeConstructorVariable, TypeConstructor_Base[Any]] = {}
for arg_tv, arg_tp in zip(cls.args, typ, strict=True):
if isinstance(arg_tv, TypeVariable):
assert isinstance(arg_tp, Type3)
tv_map[arg_tv] = arg_tp
elif isinstance(arg_tv, TypeConstructorVariable):
assert isinstance(arg_tp, TypeConstructor_Base)
tc_map[arg_tv] = arg_tp
else:
raise NotImplementedError(arg_tv, arg_tp)
for method_name, method in cls.methods.items():
router = self.type_class_instance_methods.get(method)
if router is None:
router = TypeClassArgsRouter[G, None](cls.args)
self.type_class_instance_methods[method] = router
try:
generator = methods[method_name]
except KeyError:
warn(MissingImplementationWarning(str(method), cls.name + ' ' + ' '.join(x.name for x in typ)))
continue
router.add(tv_map, tc_map, generator)
for operator_name, operator in cls.operators.items():
router = self.type_class_instance_methods.get(operator)
if router is None:
router = TypeClassArgsRouter[G, None](cls.args)
self.type_class_instance_methods[operator] = router
try:
generator = operators[operator_name]
except KeyError:
warn(MissingImplementationWarning(str(operator), cls.name + ' ' + ' '.join(x.name for x in typ)))
continue
router.add(tv_map, tc_map, generator)
def calculate_alloc_size_static_array(self, args: tuple[Type3, IntType3]) -> int:
"""
Helper method for calculate_alloc_size - static_array
"""
sa_type, sa_len = args
return sa_len.value * self.calculate_alloc_size(sa_type, is_member=True)
def calculate_alloc_size_tuple(self, args: tuple[Type3, ...]) -> int:
"""
Helper method for calculate_alloc_size - tuple
"""
return sum(
self.calculate_alloc_size(x, is_member=True)
for x in args
)
def calculate_alloc_size_struct(self, args: tuple[tuple[str, Type3], ...]) -> int:
"""
Helper method for calculate_alloc_size - struct
"""
return sum(
self.calculate_alloc_size(x, is_member=True)
for _, x in args
)
def calculate_alloc_size(self, typ: Type3, is_member: bool = False) -> int:
"""
Calculates how much bytes you need to allocate when reserving memory for the given type.
"""
typ_info = self.type_info_map.get(typ.name)
if typ_info is not None:
return typ_info.alloc_size
if is_member:
return self.type_info_constructed.alloc_size
try:
return self.alloc_size_router(self, typ)
except NoRouteForTypeException:
raise NotImplementedError(typ)
def calculate_member_offset(self, st_name: str, st_args: tuple[tuple[str, Type3], ...], needle: str) -> int:
"""
Calculates the amount of bytes that should be skipped in memory befor reaching the struct's property with the given name.
"""
result = 0
for memnam, memtyp in st_args:
if needle == memnam:
return result
result += self.calculate_alloc_size(memtyp, is_member=True)
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_make_tuple(self, args: Sequence[type5typeexpr.TypeExpr]) -> type5typeexpr.TypeApplication:
if not args:
raise TypeError("Tuples must at least one field")
arlen = len(args)
constructor = self.tuple_type5_constructor_map.get(arlen)
if constructor is None:
star = type5kindexpr.Star()
kind: type5kindexpr.Arrow = star >> star
for _ in range(len(args) - 1):
kind = star >> kind
constructor = type5typeexpr.TypeConstructor(kind=kind, name=f'tuple_{arlen}')
self.tuple_type5_constructor_map[arlen] = constructor
result: type5typeexpr.TypeApplication | None = None
for arg in args:
if result is None:
result = type5typeexpr.TypeApplication(
constructor=constructor,
argument=arg
)
continue
result = type5typeexpr.TypeApplication(
constructor=result,
argument=arg
)
assert result is not None # type hint
result.name = '(' + ', '.join(x.name for x in args) + ', )'
return result
def type5_is_tuple(self, typeexpr: type5typeexpr.TypeExpr) -> list[type5typeexpr.TypeExpr] | None:
arg_list = []
while isinstance(typeexpr, type5typeexpr.TypeApplication):
arg_list.append(typeexpr.argument)
typeexpr = typeexpr.constructor
if not isinstance(typeexpr, type5typeexpr.TypeConstructor):
return None
if typeexpr not in self.tuple_type5_constructor_map.values():
return None
return list(reversed(arg_list))
def type5_make_record(self, name: str, fields: tuple[tuple[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication], ...]) -> type5record.Record:
return type5record.Record(name, fields)
def type5_is_record(self, arg: type5typeexpr.TypeExpr) -> tuple[tuple[str, type5typeexpr.AtomicType | type5typeexpr.TypeApplication], ...] | None:
if not isinstance(arg, type5record.Record):
return None
return arg.fields
def type5_make_dynamic_array(self, arg: type5typeexpr.TypeExpr) -> type5typeexpr.TypeApplication:
return type5typeexpr.TypeApplication(
constructor=self.dynamic_array_type5_constructor,
argument=arg,
)
def type5_is_dynamic_array(self, typeexpr: type5typeexpr.TypeExpr) -> type5typeexpr.TypeExpr | None:
"""
Check if the given type expr is a concrete dynamic array type.
The element argument type is returned if so. Else, None is returned.
"""
if not isinstance(typeexpr, type5typeexpr.TypeApplication):
return None
if typeexpr.constructor != self.dynamic_array_type5_constructor:
return None
return typeexpr.argument
def type5_make_static_array(self, len: int, arg: type5typeexpr.TypeExpr) -> type5typeexpr.TypeApplication:
return type5typeexpr.TypeApplication(
constructor=type5typeexpr.TypeApplication(
constructor=self.static_array_type5_constructor,
argument=type5typeexpr.TypeLevelNat(len),
),
argument=arg,
)
def type5_is_static_array(self, typeexpr: type5typeexpr.TypeExpr) -> tuple[int, 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.static_array_type5_constructor:
return None
assert isinstance(typeexpr.constructor.argument, type5typeexpr.TypeLevelNat) # type hint
return (
typeexpr.constructor.argument.value,
typeexpr.argument,
)