Compare commits

...

2 Commits

Author SHA1 Message Date
Johan B.W. de Vries
0e8540c611 Removes the special casing for foldl
Had to implement both functions as arguments and type
place holders (variables) for type constructors.

Had to implement functions as a type as well.

Still have to figure out how to pass functions around.
2025-05-19 21:06:40 +02:00
Johan B.W. de Vries
46b06dbcf1 Cleanup: TYPE_INFO_MAP
This also removes the InternalPassAsPointer experiment.

This also fixes that u8 values were stores as 32 bits
in structs and tuples (but not dynamic arrays since that
is special cased as bytes).

Also, fixes allocation issue wi	th dynamic arrays, it
would allocate quadratic amount of memory.
2025-05-19 21:04:13 +02:00
19 changed files with 787 additions and 471 deletions

View File

@ -7,7 +7,6 @@
- Implement a trace() builtin for debugging
- Check if we can use DataView in the Javascript examples, e.g. with setUint32
- Storing u8 in memory still claims 32 bits (since that's what you need in local variables). However, using load8_u / loadu_s we can optimize this.
- Implement a FizzBuzz example
- Also, check the codes for FIXME and TODO
- Allocation is done using pointers for members, is this desired?
@ -17,9 +16,11 @@
- Clean up Subscript implementation - it's half implemented in the compiler. Makes more sense to move more parts to stdlib_types.
- Have a set of rules or guidelines for the constraint comments, they're messy.
- Why is expression_subscript_bytes using a helper method but expression_subscript_static_array is not?
- calculate_alloc_size can be reworked; is_member isn't useful with TYPE_INFO_MAP
- Parser is putting stuff in ModuleDataBlock
- Surely the compiler should build data blocks
- Also put the struct.pack constants in TYPE_INFO_MAP
- Make prelude more an actual thing
- Merge in compiler.INSTANCES
- Make it less build in - have a environment class of some kind
@ -30,3 +31,5 @@
- Functions don't seem to be a thing on typing level yet?
- Related to the FIXME in phasm_type3?
- Type constuctor should also be able to constuct placeholders - somehow.
- Read https://bytecodealliance.org/articles/multi-value-all-the-wasm

View File

@ -105,10 +105,6 @@ def expression(inp: ourlang.Expression) -> str:
if isinstance(inp, ourlang.AccessStructMember):
return f'{expression(inp.varref)}.{inp.member}'
if isinstance(inp, ourlang.Fold):
fold_name = 'foldl' if ourlang.Fold.Direction.LEFT == inp.dir else 'foldr'
return f'{fold_name}({inp.func.name}, {expression(inp.base)}, {expression(inp.iter)})'
raise NotImplementedError(expression, inp)
def statement(inp: ourlang.Statement) -> Statements:

View File

@ -2,13 +2,14 @@
This module contains the code to convert parsed Ourlang into WebAssembly code
"""
import struct
from typing import List, Optional
from typing import List
from . import codestyle, ourlang, prelude, wasm
from . import ourlang, prelude, wasm
from .runtime import calculate_alloc_size, calculate_member_offset
from .stdlib import alloc as stdlib_alloc
from .stdlib import types as stdlib_types
from .type3.functions import TypeVariable
from .stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from .type3.functions import FunctionArgument, TypeVariable
from .type3.routers import NoRouteForTypeException, TypeApplicationRouter
from .type3.typeclasses import Type3ClassMethod
from .type3.types import (
@ -27,20 +28,6 @@ from .wasmgenerator import Generator as WasmGenerator
TYPE3_ASSERTION_ERROR = 'You must call phasm_type3 after calling phasm_parse before your program can be compiled'
LOAD_STORE_TYPE_MAP = {
'i8': 'i32', # Have to use an u32, since there is no native i8 type
'u8': 'i32', # Have to use an u32, since there is no native u8 type
'i32': 'i32',
'i64': 'i64',
'u32': 'i32',
'u64': 'i64',
'f32': 'f32',
'f64': 'f64',
'bytes': 'i32', # Bytes are passed around as pointers
}
def phasm_compile(inp: ourlang.Module) -> wasm.Module:
"""
Public method for compiling a parsed Phasm module into
@ -55,53 +42,8 @@ def type3(inp: Type3) -> wasm.WasmType:
Types are used for example in WebAssembly function parameters
and return types.
"""
assert inp is not None, TYPE3_ASSERTION_ERROR
if inp == prelude.none:
return wasm.WasmTypeNone()
if inp == prelude.bool_:
# WebAssembly stores booleans as i32
# See e.g. f32.eq, which is [f32 f32] -> [i32]
return wasm.WasmTypeInt32()
if inp == prelude.u8:
# WebAssembly has only support for 32 and 64 bits
# So we need to store more memory per byte
return wasm.WasmTypeInt32()
if inp == prelude.u32:
return wasm.WasmTypeInt32()
if inp == prelude.u64:
return wasm.WasmTypeInt64()
if inp == prelude.i8:
# WebAssembly has only support for 32 and 64 bits
# So we need to store more memory per byte
return wasm.WasmTypeInt32()
if inp == prelude.i32:
return wasm.WasmTypeInt32()
if inp == prelude.i64:
return wasm.WasmTypeInt64()
if inp == prelude.f32:
return wasm.WasmTypeFloat32()
if inp == prelude.f64:
return wasm.WasmTypeFloat64()
if inp == prelude.bytes_:
# bytes are passed as pointer
# And pointers are i32
return wasm.WasmTypeInt32()
if (prelude.InternalPassAsPointer, (inp, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
return wasm.WasmTypeInt32()
raise NotImplementedError(type3, inp)
typ_info = TYPE_INFO_MAP.get(inp.name, TYPE_INFO_CONSTRUCTED)
return typ_info.wasm_type()
def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.TupleInstantiation) -> None:
"""
@ -121,7 +63,9 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
sa_type, = inp.type3.application.arguments
args = tuple(sa_type for _ in inp.elements)
alloc_size = 4 + calculate_alloc_size(sa_type, is_member=False) * len(inp.elements)
# Can't use calculate_alloc_size directly since that doesn't
# know the dynamic array's length
alloc_size = 4 + calculate_alloc_size(sa_type, is_member=True) * len(inp.elements)
alloc_size_header = len(inp.elements)
elif isinstance(inp.type3.application, TypeApplication_TypeStar):
# Possibly paranoid assert. If we have a future variadic type,
@ -165,15 +109,12 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
for element, exp_type3 in zip(inp.elements, args, strict=True):
assert element.type3 == exp_type3
if (prelude.InternalPassAsPointer, (exp_type3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
mtyp = 'i32'
else:
mtyp = LOAD_STORE_TYPE_MAP[exp_type3.name]
exp_type_info = TYPE_INFO_MAP.get(exp_type3.name, TYPE_INFO_CONSTRUCTED)
wgn.add_statement('nop', comment='PRE')
wgn.local.get(tmp_var)
expression(wgn, mod, element)
wgn.add_statement(f'{mtyp}.store', 'offset=' + str(offset))
wgn.add_statement(exp_type_info.wasm_store_func, 'offset=' + str(offset))
wgn.add_statement('nop', comment='POST')
offset += calculate_alloc_size(exp_type3, is_member=True)
@ -213,14 +154,14 @@ def expression_subscript_static_array(
with wgn.if_():
wgn.unreachable(comment='Out of bounds')
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
wgn.local.get(tmp_var)
wgn.i32.const(calculate_alloc_size(el_type))
wgn.i32.const(el_type_info.alloc_size)
wgn.i32.mul()
wgn.i32.add()
mtyp = LOAD_STORE_TYPE_MAP[el_type.name]
wgn.add_statement(f'{mtyp}.load')
wgn.add_statement(el_type_info.wasm_load_func)
def expression_subscript_tuple(
attrs: tuple[WasmGenerator, ourlang.Module, ourlang.Subscript],
@ -234,19 +175,16 @@ def expression_subscript_tuple(
offset = 0
for el_type in args[0:inp.index.value]:
assert el_type is not None, TYPE3_ASSERTION_ERROR
offset += calculate_alloc_size(el_type)
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
offset += el_type_info.alloc_size
el_type = args[inp.index.value]
assert el_type is not None, TYPE3_ASSERTION_ERROR
expression(wgn, mod, inp.varref)
if (prelude.InternalPassAsPointer, (el_type, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
mtyp = 'i32'
else:
mtyp = LOAD_STORE_TYPE_MAP[el_type.name]
wgn.add_statement(f'{mtyp}.load', f'offset={offset}')
el_type_info = TYPE_INFO_MAP.get(el_type.name, TYPE_INFO_CONSTRUCTED)
wgn.add_statement(el_type_info.wasm_load_func, f'offset={offset}')
SUBSCRIPT_ROUTER = TypeApplicationRouter[tuple[WasmGenerator, ourlang.Module, ourlang.Subscript], None]()
SUBSCRIPT_ROUTER.add_n(prelude.bytes_, expression_subscript_bytes)
@ -305,7 +243,7 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
if isinstance(inp.variable, ourlang.ModuleConstantDef):
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
if (prelude.InternalPassAsPointer, (inp.type3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
if inp.type3.name not in TYPE_INFO_MAP:
assert isinstance(inp.variable.constant, (ourlang.ConstantBytes, ourlang.ConstantStruct, ourlang.ConstantTuple, ))
address = inp.variable.constant.data_block.address
@ -335,6 +273,10 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
type_var_map[type_var] = arg_expr.type3
continue
if isinstance(type_var, FunctionArgument):
# Fixed type, not part of the lookup requirements
continue
raise NotImplementedError(type_var, arg_expr.type3)
router = prelude.PRELUDE_TYPE_CLASS_INSTANCE_METHODS[inp.operator]
@ -360,6 +302,10 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
type_var_map[type_var] = arg_expr.type3
continue
if isinstance(type_var, FunctionArgument):
# Fixed type, not part of the lookup requirements
continue
raise NotImplementedError(type_var, arg_expr.type3)
router = prelude.PRELUDE_TYPE_CLASS_INSTANCE_METHODS[inp.function]
@ -413,99 +359,16 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Expression)
assert isinstance(inp.struct_type3.application, TypeApplication_Struct)
member_type = dict(inp.struct_type3.application.arguments)[inp.member]
mtyp = LOAD_STORE_TYPE_MAP[member_type.name]
member_type_info = TYPE_INFO_MAP.get(member_type.name, TYPE_INFO_CONSTRUCTED)
expression(wgn, mod, inp.varref)
wgn.add_statement(f'{mtyp}.load', 'offset=' + str(calculate_member_offset(
wgn.add_statement(member_type_info.wasm_load_func, 'offset=' + str(calculate_member_offset(
inp.struct_type3.name, inp.struct_type3.application.arguments, inp.member
)))
return
if isinstance(inp, ourlang.Fold):
expression_fold(wgn, mod, inp)
return
raise NotImplementedError(expression, inp)
def expression_fold(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Fold) -> None:
"""
Compile: Fold expression
"""
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
if inp.iter.type3 is not prelude.bytes_:
raise NotImplementedError(expression_fold, inp, inp.iter.type3)
wgn.add_statement('nop', comment='acu :: u8')
acu_var = wgn.temp_var_u8(f'fold_{codestyle.type3(inp.type3)}_acu')
wgn.add_statement('nop', comment='adr :: bytes*')
adr_var = wgn.temp_var_i32('fold_i32_adr')
wgn.add_statement('nop', comment='len :: i32')
len_var = wgn.temp_var_i32('fold_i32_len')
wgn.add_statement('nop', comment='acu = base')
expression(wgn, mod, inp.base)
wgn.local.set(acu_var)
wgn.add_statement('nop', comment='adr = adr(iter)')
expression(wgn, mod, inp.iter)
wgn.local.set(adr_var)
wgn.add_statement('nop', comment='len = len(iter)')
wgn.local.get(adr_var)
wgn.i32.load()
wgn.local.set(len_var)
wgn.add_statement('nop', comment='i = 0')
idx_var = wgn.temp_var_i32(f'fold_{codestyle.type3(inp.type3)}_idx')
wgn.i32.const(0)
wgn.local.set(idx_var)
wgn.add_statement('nop', comment='if i < len')
wgn.local.get(idx_var)
wgn.local.get(len_var)
wgn.i32.lt_u()
with wgn.if_():
# From here on, adr_var is the address of byte we're referencing
# This is akin to calling stdlib_types.__subscript_bytes__
# But since we already know we are inside of bounds,
# can just bypass it and load the memory directly.
wgn.local.get(adr_var)
wgn.i32.const(3) # Bytes header -1, since we do a +1 every loop
wgn.i32.add()
wgn.local.set(adr_var)
wgn.add_statement('nop', comment='while True')
with wgn.loop():
wgn.add_statement('nop', comment='acu = func(acu, iter[i])')
wgn.local.get(acu_var)
# Get the next byte, write back the address
wgn.local.get(adr_var)
wgn.i32.const(1)
wgn.i32.add()
wgn.local.tee(adr_var)
wgn.i32.load8_u()
wgn.add_statement('call', f'${inp.func.name}')
wgn.local.set(acu_var)
wgn.add_statement('nop', comment='i = i + 1')
wgn.local.get(idx_var)
wgn.i32.const(1)
wgn.i32.add()
wgn.local.set(idx_var)
wgn.add_statement('nop', comment='if i >= len: break')
wgn.local.get(idx_var)
wgn.local.get(len_var)
wgn.i32.lt_u()
wgn.br_if(0)
# return acu
wgn.local.get(acu_var)
def statement_return(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.StatementReturn) -> None:
"""
Compile: Return statement
@ -600,10 +463,8 @@ def function(mod: ourlang.Module, inp: ourlang.Function) -> wasm.Function:
def module_data_u8(inp: int) -> bytes:
"""
Compile: module data, u8 value
# FIXME: All u8 values are stored as u32
"""
return struct.pack('<I', inp) # Should be 'B'
return struct.pack('<B', inp)
def module_data_u32(inp: int) -> bytes:
"""
@ -620,10 +481,8 @@ def module_data_u64(inp: int) -> bytes:
def module_data_i8(inp: int) -> bytes:
"""
Compile: module data, i8 value
# FIXME: All i8 values are stored as i32
"""
return struct.pack('<i', inp) # Should be a 'b'
return struct.pack('<b', inp)
def module_data_i32(inp: int) -> bytes:
"""
@ -812,18 +671,11 @@ def _generate_struct_constructor(wgn: WasmGenerator, inp: ourlang.StructConstruc
# Store each member individually
for memname, mtyp3 in st_args:
mtyp: Optional[str]
if (prelude.InternalPassAsPointer, (mtyp3, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
mtyp = 'i32'
else:
mtyp = LOAD_STORE_TYPE_MAP.get(mtyp3.name)
if mtyp is None:
raise NotImplementedError(expression, inp, mtyp3)
mtyp3_info = TYPE_INFO_MAP.get(mtyp3.name, TYPE_INFO_CONSTRUCTED)
wgn.local.get(tmp_var)
wgn.add_statement('local.get', f'${memname}')
wgn.add_statement(f'{mtyp}.store', 'offset=' + str(calculate_member_offset(
wgn.add_statement(mtyp3_info.wasm_store_func, 'offset=' + str(calculate_member_offset(
inp.struct_type3.name, st_args, memname
)))

View File

@ -1,7 +1,6 @@
"""
Contains the syntax tree for ourlang
"""
import enum
from typing import Dict, Iterable, List, Optional, Union
from . import prelude
@ -219,36 +218,6 @@ class AccessStructMember(Expression):
self.struct_type3 = struct_type3
self.member = member
class Fold(Expression):
"""
A (left or right) fold
"""
class Direction(enum.Enum):
"""
Which direction to fold in
"""
LEFT = 0
RIGHT = 1
dir: Direction
func: 'Function'
base: Expression
iter: Expression
def __init__(
self,
dir_: Direction,
func: 'Function',
base: Expression,
iter_: Expression,
) -> None:
super().__init__()
self.dir = dir_
self.func = func
self.base = base
self.iter = iter_
class Statement:
"""
A statement within a function

View File

@ -14,7 +14,6 @@ from .ourlang import (
ConstantStruct,
ConstantTuple,
Expression,
Fold,
Function,
FunctionCall,
FunctionParam,
@ -467,7 +466,7 @@ class OurVisitor:
raise NotImplementedError(f'{node} as expr in FunctionDef')
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Call) -> Union[Fold, FunctionCall]:
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, node: ast.Call) -> Union[FunctionCall]:
if node.keywords:
_raise_static_error(node, 'Keyword calling not supported') # Yet?
@ -480,28 +479,6 @@ class OurVisitor:
if node.func.id in PRELUDE_METHODS:
func = PRELUDE_METHODS[node.func.id]
elif node.func.id == 'foldl':
if 3 != len(node.args):
_raise_static_error(node, f'Function {node.func.id} requires 3 arguments but {len(node.args)} are given')
# TODO: This is not generic, you cannot return a function
subnode = node.args[0]
if not isinstance(subnode, ast.Name):
raise NotImplementedError(f'Calling methods that are not a name {subnode}')
if not isinstance(subnode.ctx, ast.Load):
_raise_static_error(subnode, 'Must be load context')
if subnode.id not in module.functions:
_raise_static_error(subnode, 'Reference to undefined function')
func = module.functions[subnode.id]
if 2 != len(func.posonlyargs):
_raise_static_error(node, f'Function {node.func.id} requires a function with 2 arguments but a function with {len(func.posonlyargs)} args is given')
return Fold(
Fold.Direction.LEFT,
func,
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[1]),
self.visit_Module_FunctionDef_expr(module, function, our_locals, node.args[2]),
)
elif node.func.id in our_locals:
func = our_locals[node.func.id]
else:

View File

@ -16,7 +16,6 @@ from ..type3.functions import (
from ..type3.routers import TypeClassArgsRouter, TypeVariableLookup
from ..type3.typeclasses import Type3Class, Type3ClassMethod
from ..type3.types import (
IntType3,
Type3,
TypeApplication_Nullary,
TypeConstructor_Base,
@ -159,10 +158,7 @@ f64 = Type3('f64', TypeApplication_Nullary(None, None))
A 32-bits IEEE 754 float, of 64 bits width.
"""
def da_on_create(args: tuple[Type3], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ)
dynamic_array = TypeConstructor_DynamicArray('dynamic_array', on_create=da_on_create)
dynamic_array = TypeConstructor_DynamicArray('dynamic_array')
"""
This is a dynamic length piece of memory.
@ -170,10 +166,7 @@ It should be applied with two arguments. It has a runtime
determined length, and each argument is the same.
"""
def sa_on_create(args: tuple[Type3, IntType3], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ)
static_array = TypeConstructor_StaticArray('static_array', on_create=sa_on_create)
static_array = TypeConstructor_StaticArray('static_array')
"""
This is a fixed length piece of memory.
@ -181,10 +174,7 @@ It should be applied with two arguments. It has a compile time
determined length, and each argument is the same.
"""
def tp_on_create(args: tuple[Type3, ...], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ)
tuple_ = TypeConstructor_Tuple('tuple', on_create=tp_on_create)
tuple_ = TypeConstructor_Tuple('tuple')
"""
This is a fixed length piece of memory.
@ -192,22 +182,14 @@ It should be applied with zero or more arguments. It has a compile time
determined length, and each argument can be different.
"""
def fn_on_create(args: tuple[Type3, ...], typ: Type3) -> None:
# Not really a pointer; but still a i32
# (It's actually a table lookup)
instance_type_class(InternalPassAsPointer, typ)
function = TypeConstructor_Function('function', on_create=fn_on_create)
function = TypeConstructor_Function('function')
"""
This is a function.
It should be applied with one or more arguments. The last argument is the 'return' type.
"""
def st_on_create(args: tuple[tuple[str, Type3], ...], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ)
struct = TypeConstructor_Struct('struct', on_create=st_on_create)
struct = TypeConstructor_Struct('struct')
"""
This is like a tuple, but each argument is named, so that developers
can get and set fields by name.
@ -218,16 +200,6 @@ b = TypeVariable('b', TypeVariableApplication_Nullary(None, None))
t = TypeConstructorVariable('t')
InternalPassAsPointer = Type3Class('InternalPassAsPointer', (a, ), methods={}, operators={})
"""
Internal type class to keep track which types we pass arounds as a pointer.
"""
# instance_type_class(InternalPassAsPointer, bytes_)
# instance_type_class(InternalPassAsPointer, static_array)
# instance_type_class(InternalPassAsPointer, tuple_)
# instance_type_class(InternalPassAsPointer, struct)
Eq = Type3Class('Eq', (a, ), methods={}, operators={
'==': [a, a, bool_],
'!=': [a, a, bool_],
@ -581,12 +553,21 @@ instance_type_class(Promotable, f32, f64, methods={
Foldable = Type3Class('Foldable', (t, ), methods={
'sum': [t(a), a],
'foldl': [[b, a, b], b, t(a), b],
'foldr': [[a, b, b], b, t(a), b],
}, operators={}, additional_context={
'sum': [Constraint_TypeClassInstanceExists(NatNum, (a, ))],
})
instance_type_class(Foldable, dynamic_array, methods={
'sum': stdtypes.dynamic_array_sum,
'foldl': stdtypes.dynamic_array_foldl,
'foldr': stdtypes.dynamic_array_foldr,
})
instance_type_class(Foldable, static_array, methods={
'sum': stdtypes.static_array_sum,
'foldl': stdtypes.static_array_foldl,
'foldr': stdtypes.static_array_foldr,
})
bytes_ = dynamic_array(u8)

View File

@ -1,11 +1,12 @@
from . import prelude
from .stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from .type3.routers import NoRouteForTypeException, TypeApplicationRouter
from .type3.types import IntType3, Type3
def calculate_alloc_size_static_array(is_member: bool, args: tuple[Type3, IntType3]) -> int:
if is_member:
return 4
return TYPE_INFO_CONSTRUCTED.alloc_size
sa_type, sa_len = args
@ -13,7 +14,7 @@ def calculate_alloc_size_static_array(is_member: bool, args: tuple[Type3, IntTyp
def calculate_alloc_size_tuple(is_member: bool, args: tuple[Type3, ...]) -> int:
if is_member:
return 4
return TYPE_INFO_CONSTRUCTED.alloc_size
return sum(
calculate_alloc_size(x, is_member=True)
@ -22,7 +23,7 @@ def calculate_alloc_size_tuple(is_member: bool, args: tuple[Type3, ...]) -> int:
def calculate_alloc_size_struct(is_member: bool, args: tuple[tuple[str, Type3], ...]) -> int:
if is_member:
return 4
return TYPE_INFO_CONSTRUCTED.alloc_size
return sum(
calculate_alloc_size(x, is_member=True)
@ -35,14 +36,9 @@ ALLOC_SIZE_ROUTER.add(prelude.struct, calculate_alloc_size_struct)
ALLOC_SIZE_ROUTER.add(prelude.tuple_, calculate_alloc_size_tuple)
def calculate_alloc_size(typ: Type3, is_member: bool = False) -> int:
if typ in (prelude.u8, prelude.i8, ):
return 4 # FIXME: We allocate 4 bytes for every u8 since you load them into an i32
if typ in (prelude.u32, prelude.i32, prelude.f32, ):
return 4
if typ in (prelude.u64, prelude.i64, prelude.f64, ):
return 8
typ_info = TYPE_INFO_MAP.get(typ.name)
if typ_info is not None:
return typ_info.alloc_size
try:
return ALLOC_SIZE_ROUTER(is_member, typ)
@ -50,7 +46,7 @@ def calculate_alloc_size(typ: Type3, is_member: bool = False) -> int:
if is_member:
# By default, 'boxed' or 'constructed' types are
# stored as pointers when a member of a struct or tuple
return 4
return TYPE_INFO_CONSTRUCTED.alloc_size
raise NotImplementedError(typ)

View File

@ -1,13 +1,63 @@
"""
stdlib: Standard types that are not wasm primitives
"""
from typing import Mapping, NamedTuple, Type
from phasm.stdlib import alloc
from phasm.type3.routers import TypeVariableLookup
from phasm.type3.types import IntType3, Type3
from phasm.wasmgenerator import Generator, func_wrapper
from phasm.wasm import (
WasmType,
WasmTypeFloat32,
WasmTypeFloat64,
WasmTypeInt32,
WasmTypeInt64,
WasmTypeNone,
)
from phasm.wasmgenerator import Generator, VarType_Base, func_wrapper
from phasm.wasmgenerator import VarType_f32 as f32
from phasm.wasmgenerator import VarType_f64 as f64
from phasm.wasmgenerator import VarType_i32 as i32
from phasm.wasmgenerator import VarType_i64 as i64
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),
])
TYPE_INFO_MAP: Mapping[str, TypeInfo] = {
'none': TypeInfo('none', WasmTypeNone, 'nop', 'nop', 0),
'bool': TypeInfo('bool', WasmTypeInt32, 'i32.load8_u', 'i32.store8', 1),
'u8': TypeInfo('u8', WasmTypeInt32, 'i32.load8_u', 'i32.store8', 1),
'i8': TypeInfo('i8', WasmTypeInt32, 'i32.load8_s', 'i32.store8', 1),
'u32': TypeInfo('u32', WasmTypeInt32, 'i32.load', 'i32.store', 4),
'u64': TypeInfo('u64', WasmTypeInt64, 'i64.load', 'i64.store', 8),
'i32': TypeInfo('i32', WasmTypeInt32, 'i32.load', 'i32.store', 4),
'i64': TypeInfo('i64', WasmTypeInt64, 'i64.load', 'i64.store', 8),
'f32': TypeInfo('f32', WasmTypeFloat32, 'f32.load', 'f32.store', 4),
'f64': TypeInfo('f64', WasmTypeFloat64, 'f64.load', 'f64.store', 8),
'ptr': TypeInfo('ptr', WasmTypeInt32, 'i32.load', 'i32.store', 4),
}
# By default, constructed types are passed as pointers
# NOTE: ALLOC SIZE HERE DOES NOT WORK FOR CONSTRUCTED TYPES
# USE runtime.calculate_alloc_size FOR ACCURATE RESULTS
# Functions count as constructed types - even though they are
# not memory pointers but table addresses instead.
TYPE_INFO_CONSTRUCTED = TypeInfo('t a', WasmTypeInt32, 'i32.load', 'i32.store', 4)
@func_wrapper()
def __alloc_bytes__(g: Generator, length: i32) -> i32:
@ -1011,12 +1061,16 @@ def dynamic_array_sized_len(g: Generator, tv_map: TypeVariableLookup) -> None:
# The length is stored in the first 4 bytes
g.i32.load()
def static_array_sized_len(g: Generator, tv_map: TypeVariableLookup) -> None:
assert len(tv_map) == 1
sa_type, sa_len = next(iter(tv_map.values()))
assert isinstance(sa_type, Type3)
assert isinstance(sa_len, IntType3)
def static_array_sized_len(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
tvn_map = {
x.name: y
for x, y in tv_map.items()
}
sa_len = tvn_map['a*']
assert isinstance(sa_len, IntType3)
g.i32.const(sa_len.value)
## ###
@ -1089,35 +1143,32 @@ def f32_f64_demote(g: Generator, tv_map: TypeVariableLookup) -> None:
del tv_map
g.f32.demote_f64()
def static_array_sum(g: Generator, tv_map: TypeVariableLookup) -> None:
assert len(tv_map) == 1
sa_type, sa_len = next(iter(tv_map.values()))
## ###
## Foldable
def dynamic_array_sum(g: Generator, tvl: TypeVariableLookup) -> None:
raise NotImplementedError
def static_array_sum(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
tvn_map = {
x.name: y
for x, y in tv_map.items()
}
sa_type = tvn_map['a']
sa_len = tvn_map['a*']
assert isinstance(sa_type, Type3)
assert isinstance(sa_len, IntType3)
if sa_len.value < 1:
raise NotImplementedError('Default value in case sum is empty')
# FIXME: We should probably use LOAD_STORE_TYPE_MAP for this?
mtyp_map = {
'u32': 'i32',
'u64': 'i64',
'i32': 'i32',
'i64': 'i64',
'f32': 'f32',
'f64': 'f64',
}
# FIXME: We should probably use calc_alloc_size for this?
type_var_size_map = {
'u32': 4,
'u64': 8,
'i32': 4,
'i64': 8,
'f32': 4,
'f64': 8,
}
sa_type_info = TYPE_INFO_MAP.get(sa_type.name, TYPE_INFO_CONSTRUCTED)
# FIXME: This breaks when users start implementing their own NatNum classes
type_var_add_generator = {
'u32': u32_natnum_add,
'u64': u64_natnum_add,
@ -1126,11 +1177,6 @@ def static_array_sum(g: Generator, tv_map: TypeVariableLookup) -> None:
'f32': f32_natnum_add,
'f64': f64_natnum_add,
}
# By default, constructed types are passed as pointers
# FIXME: We don't know what add function to call
sa_type_mtyp = mtyp_map.get(sa_type.name, 'i32')
sa_type_alloc_size = type_var_size_map.get(sa_type.name, 4)
sa_type_add_gen = type_var_add_generator[sa_type.name]
# Definitions
@ -1149,7 +1195,7 @@ def static_array_sum(g: Generator, tv_map: TypeVariableLookup) -> None:
# Stack: []
g.nop(comment='Calculate address at which to stop looping')
g.local.get(sum_adr)
g.i32.const(sa_len.value * sa_type_alloc_size)
g.i32.const(sa_len.value * sa_type_info.alloc_size)
g.i32.add()
g.local.set(sum_stop)
@ -1157,30 +1203,30 @@ def static_array_sum(g: Generator, tv_map: TypeVariableLookup) -> None:
# Stack: [] -> [sum]
g.nop(comment='Get the first array value as starting point')
g.local.get(sum_adr)
g.add_statement(f'{sa_type_mtyp}.load')
g.add_statement(sa_type_info.wasm_load_func)
# Since we did the first one, increase adr
# adr = adr + sa_type_alloc_size
# Stack: [sum] -> [sum]
g.local.get(sum_adr)
g.i32.const(sa_type_alloc_size)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.set(sum_adr)
if sa_len.value > 1:
with g.loop(params=[sa_type_mtyp], result=sa_type_mtyp):
with g.loop(params=[sa_type_info.wasm_type().to_wat()], result=sa_type_info.wasm_type().to_wat()):
# sum = sum + *adr
# Stack: [sum] -> [sum + *adr]
g.nop(comment='Add array value')
g.local.get(sum_adr)
g.add_statement(f'{sa_type_mtyp}.load')
sa_type_add_gen(g, {})
g.add_statement(sa_type_info.wasm_load_func)
sa_type_add_gen(g, ({}, {}, ))
# adr = adr + sa_type_alloc_size
# Stack: [sum] -> [sum]
g.nop(comment='Calculate address of the next value')
g.local.get(sum_adr)
g.i32.const(sa_type_alloc_size)
g.i32.const(sa_type_info.alloc_size)
g.i32.add()
g.local.tee(sum_adr)
@ -1193,3 +1239,396 @@ def static_array_sum(g: Generator, tv_map: TypeVariableLookup) -> None:
g.nop(comment=f'Completed sum for {sa_type.name}[{sa_len.value}]')
# End result: [sum]
def dynamic_array_foldl(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
tvn_map = {
x.name: y
for x, y in tv_map.items()
}
sa_type = tvn_map['a']
res_type = tvn_map['b']
assert isinstance(sa_type, Type3)
assert isinstance(res_type, Type3)
# FIXME: We should probably use LOAD_STORE_TYPE_MAP for this?
load_map = {
'u8': 'i32.load8_u',
'u32': 'i32.load',
'u64': 'i64.load',
'i8': 'i32.load8_s',
'i32': 'i32.load',
'i64': 'i64.load',
'f32': 'f32.load',
'f64': 'f64.load',
}
mtyp_map = {
'u32': 'i32',
'u64': 'i64',
'i32': 'i32',
'i64': 'i64',
'f32': 'f32',
'f64': 'f64',
}
mtyp_f_map: dict[str, type[VarType_Base]] = {
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
}
# FIXME: We should probably use calc_alloc_size for this?
type_var_size_map = {
'u8': 1,
'u32': 4,
'u64': 8,
'i8': 1,
'i32': 4,
'i64': 8,
'f32': 4,
'f64': 8,
}
# By default, constructed types are passed as pointers
# FIXME: We don't know what add function to call
sa_type_load = load_map.get(sa_type.name, 'i32')
sa_type_mtyp = mtyp_map.get(sa_type.name, 'i32')
sa_type_alloc_size = type_var_size_map.get(sa_type.name, 4)
res_type_mtyp = mtyp_map.get(res_type.name, 'i32')
res_type_mtyp_f = mtyp_f_map[res_type_mtyp]
# Definitions
fold_adr = g.temp_var(i32('fold_adr'))
fold_stop = g.temp_var(i32('fold_stop'))
fold_init = g.temp_var(res_type_mtyp_f('fold_init'))
fold_func = g.temp_var(i32('fold_func'))
fold_len = g.temp_var(i32('fold_len'))
with g.block(params=['i32', res_type_mtyp, 'i32'], result=res_type_mtyp, comment=f'foldl a={sa_type.name} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b]
g.local.tee(fold_adr) # Store address, but also keep it for loading the length
g.i32.load() # Load the length
g.local.set(fold_len) # Store the length
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_init)
# Stack: [fn*] -> []
g.local.set(fold_func)
# Stack: [] -> [b]
g.nop(comment='No applications if array is empty')
g.local.get(fold_init)
g.local.get(fold_len)
g.i32.eqz() # If the array is empty
g.br_if(0) # Then the base value is the result
# Stack: [b] -> [b] ; fold_adr=fold_adr + 4
g.nop(comment='Skip the header')
g.local.get(fold_adr)
g.i32.const(4)
g.i32.add()
g.local.set(fold_adr)
# Stack: [b] -> [b]
g.nop(comment='Apply the first function call')
g.local.get(fold_adr)
g.add_statement(sa_type_load)
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {res_type_mtyp} {sa_type_mtyp}) (result {res_type_mtyp})')
# Stack: [b] -> [b]
g.nop(comment='No loop if there is only one item')
g.local.get(fold_len)
g.i32.const(1)
g.i32.eq()
g.br_if(0) # just one value, don't need to loop
# Stack: [b] -> [b] ; fold_stop=fold_adr + (sa_len.value * sa_type_alloc_size)
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.local.get(fold_len)
g.i32.const(sa_type_alloc_size)
g.i32.mul()
g.i32.add()
g.local.set(fold_stop)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.add()
g.local.set(fold_adr)
with g.loop(params=[res_type_mtyp], result=res_type_mtyp):
# Stack: [b] -> [b]
g.nop(comment='Apply function call')
g.local.get(fold_adr)
g.add_statement(sa_type_load)
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {res_type_mtyp} {sa_type_mtyp}) (result {res_type_mtyp})')
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.add()
g.local.tee(fold_adr)
# loop if adr > stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.lt_u()
g.br_if(0)
# Stack: [b]
def static_array_foldl(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
tvn_map = {
x.name: y
for x, y in tv_map.items()
}
sa_type = tvn_map['a']
sa_len = tvn_map['a*']
res_type = tvn_map['b']
assert isinstance(sa_type, Type3)
assert isinstance(sa_len, IntType3)
assert isinstance(res_type, Type3)
# FIXME: We should probably use LOAD_STORE_TYPE_MAP for this?
mtyp_map = {
'u32': 'i32',
'u64': 'i64',
'i32': 'i32',
'i64': 'i64',
'f32': 'f32',
'f64': 'f64',
}
mtyp_f_map: dict[str, type[VarType_Base]] = {
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
}
# FIXME: We should probably use calc_alloc_size for this?
type_var_size_map = {
'u32': 4,
'u64': 8,
'i32': 4,
'i64': 8,
'f32': 4,
'f64': 8,
}
# By default, constructed types are passed as pointers
# FIXME: We don't know what add function to call
sa_type_mtyp = mtyp_map.get(sa_type.name, 'i32')
sa_type_alloc_size = type_var_size_map.get(sa_type.name, 4)
res_type_mtyp = mtyp_map.get(res_type.name, 'i32')
res_type_mtyp_f = mtyp_f_map[res_type_mtyp]
# Definitions
fold_adr = g.temp_var(i32('fold_adr'))
fold_stop = g.temp_var(i32('fold_stop'))
fold_init = g.temp_var(res_type_mtyp_f('fold_init'))
fold_func = g.temp_var(i32('fold_func'))
with g.block(params=['i32', res_type_mtyp, 'i32'], result=res_type_mtyp, comment=f'foldl a={sa_type.name} a*={sa_len.value} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b]
g.local.set(fold_adr)
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_init)
# Stack: [fn*] -> []
g.local.set(fold_func)
if sa_len.value < 1:
g.local.get(fold_init)
return
# Stack: [] -> [b]
g.nop(comment='Apply the first function call')
g.local.get(fold_init)
g.local.get(fold_adr)
g.add_statement(f'{sa_type_mtyp}.load')
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {res_type_mtyp} {sa_type_mtyp}) (result {res_type_mtyp})')
if sa_len.value > 1:
# Stack: [b] -> [b] ; fold_stop=fold_adr + (sa_len.value * sa_type_alloc_size)
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.i32.const(sa_len.value * sa_type_alloc_size)
g.i32.add()
g.local.set(fold_stop)
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.add()
g.local.set(fold_adr)
with g.loop(params=[res_type_mtyp], result=res_type_mtyp):
# Stack: [b] -> [b]
g.nop(comment='Apply function call')
g.local.get(fold_adr)
g.add_statement(f'{sa_type_mtyp}.load')
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {res_type_mtyp} {sa_type_mtyp}) (result {res_type_mtyp})')
# Stack: [b] -> [b] ; fold_adr = fold_adr + sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.add()
g.local.tee(fold_adr)
# loop if adr > stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.lt_u()
g.br_if(0)
# else: just one value, don't need to loop
# Stack: [b]
def dynamic_array_foldr(g: Generator, tvl: TypeVariableLookup) -> None:
raise NotImplementedError
def static_array_foldr(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
tvn_map = {
x.name: y
for x, y in tv_map.items()
}
sa_type = tvn_map['a']
sa_len = tvn_map['a*']
res_type = tvn_map['b']
assert isinstance(sa_type, Type3)
assert isinstance(sa_len, IntType3)
assert isinstance(res_type, Type3)
# FIXME: We should probably use LOAD_STORE_TYPE_MAP for this?
mtyp_map = {
'u32': 'i32',
'u64': 'i64',
'i32': 'i32',
'i64': 'i64',
'f32': 'f32',
'f64': 'f64',
}
mtyp_f_map: dict[str, type[VarType_Base]] = {
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
}
# FIXME: We should probably use calc_alloc_size for this?
type_var_size_map = {
'u32': 4,
'u64': 8,
'i32': 4,
'i64': 8,
'f32': 4,
'f64': 8,
}
# By default, constructed types are passed as pointers
sa_type_mtyp = mtyp_map.get(sa_type.name, 'i32')
sa_type_alloc_size = type_var_size_map.get(sa_type.name, 4)
res_type_mtyp = mtyp_map.get(res_type.name, 'i32')
res_type_mtyp_f = mtyp_f_map[res_type_mtyp]
# Definitions
fold_adr = g.temp_var(i32('fold_adr'))
fold_stop = g.temp_var(i32('fold_stop'))
fold_tmp = g.temp_var(res_type_mtyp_f('fold_tmp'))
fold_func = g.temp_var(i32('fold_func'))
with g.block(params=['i32', res_type_mtyp, 'i32'], result=res_type_mtyp, comment=f'foldr a={sa_type.name} a*={sa_len.value} b={res_type.name}'):
# Stack: [fn*, b, sa*] -> [fn*, b] ; fold_adr=fn*, fold_tmp=b, fold_func=fn*
g.local.set(fold_adr)
# Stack: [fn*, b] -> [fn*]
g.local.set(fold_tmp)
# Stack: [fn*] -> []
g.local.set(fold_func)
if sa_len.value < 1:
g.local.get(fold_tmp)
return
# Stack: [] -> [] ; fold_stop=fold_adr
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.local.set(fold_stop)
# Stack: [] -> [] ; fold_adr=fold_adr + (sa_len.value - 1) * sa_type_alloc_size
g.nop(comment='Calculate address at which to stop looping')
g.local.get(fold_adr)
g.i32.const((sa_len.value - 1) * sa_type_alloc_size)
g.i32.add()
g.local.set(fold_adr)
# Stack: [] -> [b]
g.nop(comment='Get the init value and first array value as starting point')
g.local.get(fold_adr)
g.add_statement(f'{sa_type_mtyp}.load')
g.local.get(fold_tmp)
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {sa_type_mtyp} {res_type_mtyp}) (result {res_type_mtyp})')
if sa_len.value > 1:
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.sub()
g.local.set(fold_adr)
with g.loop(params=[res_type_mtyp], result=res_type_mtyp):
g.nop(comment='Apply function call')
# Stack [b] since we don't have proper stack switching opcodes
# Stack: [b] -> []
g.local.set(fold_tmp)
# Stack: [] -> [a]
g.local.get(fold_adr)
g.add_statement(f'{sa_type_mtyp}.load')
# Stack [a] -> [a, b]
g.local.get(fold_tmp)
# Stack [a, b] -> [b]
g.local.get(fold_func)
g.add_statement(f'call_indirect (param {sa_type_mtyp} {res_type_mtyp}) (result {res_type_mtyp})')
# Stack: [b] -> [b] ; fold_adr = fold_adr - sa_type_alloc_size
g.nop(comment='Calculate address of the next value')
g.local.get(fold_adr)
g.i32.const(sa_type_alloc_size)
g.i32.sub()
g.local.tee(fold_adr)
# loop if adr >= stop
# Stack: [b] -> [b]
g.nop(comment='Check if address exceeds array bounds')
g.local.get(fold_stop)
g.i32.ge_u()
g.br_if(0)
# else: just one value, don't need to loop
# Stack: [b]

View File

@ -183,7 +183,7 @@ class SameTypeArgumentConstraint(ConstraintBase):
self.arg_var = arg_var
def check(self) -> CheckResult:
if self.tc_var.resolve_as is None or self.arg_var.resolve_as is None:
if self.tc_var.resolve_as is None:
return RequireTypeSubstitutes()
tc_typ = self.tc_var.resolve_as
@ -200,12 +200,16 @@ class SameTypeArgumentConstraint(ConstraintBase):
# So we can let the MustImplementTypeClassConstraint handle it.
return None
if isinstance(tc_typ.application, TypeApplication_Type):
return [SameTypeConstraint(
tc_typ.application.arguments[0],
self.arg_var,
comment=self.comment,
)]
# FIXME: This feels sketchy. Shouldn't the type variable
# have the exact same number as arguments?
if isinstance(tc_typ.application, TypeApplication_TypeInt):
if tc_typ.application.arguments[0] == arg_typ:
return None
return [SameTypeConstraint(
tc_typ.application.arguments[0],
self.arg_var,
@ -346,14 +350,10 @@ class MustImplementTypeClassConstraint(ConstraintBase):
__slots__ = ('context', 'type_class3', 'types', )
context: Context
type_class3: Union[str, Type3Class]
type_class3: Type3Class
types: list[Type3OrPlaceholder]
DATA = {
'dynamic_array': {'Foldable'},
}
def __init__(self, context: Context, type_class3: Union[str, Type3Class], typ_list: list[Type3OrPlaceholder], comment: Optional[str] = None) -> None:
def __init__(self, context: Context, type_class3: Type3Class, typ_list: list[Type3OrPlaceholder], comment: Optional[str] = None) -> None:
super().__init__(comment=comment)
self.context = context
@ -381,13 +381,9 @@ class MustImplementTypeClassConstraint(ConstraintBase):
assert len(typ_list) == len(self.types)
if isinstance(self.type_class3, Type3Class):
key = (self.type_class3, tuple(typ_list), )
if key in self.context.type_class_instances_existing:
return None
else:
if self.type_class3 in self.__class__.DATA.get(typ_list[0].name, set()):
return None
key = (self.type_class3, tuple(typ_list), )
if key in self.context.type_class_instances_existing:
return None
typ_cls_name = self.type_class3 if isinstance(self.type_class3, str) else self.type_class3.name
typ_name_list = ' '.join(x.name for x in typ_list)

View File

@ -202,6 +202,10 @@ def _expression_function_call(
yield SameTypeConstraint(sig_part, arg_placeholders[arg_expr], comment=comment)
continue
if isinstance(sig_part, FunctionArgument):
yield SameTypeConstraint(func_var_map[sig_part], arg_placeholders[arg_expr], comment=comment)
continue
raise NotImplementedError(sig_part)
return
@ -265,19 +269,6 @@ def expression(ctx: Context, inp: ourlang.Expression, phft: PlaceholderForType)
comment=f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}')
return
if isinstance(inp, ourlang.Fold):
base_phft = PlaceholderForType([inp.base])
iter_phft = PlaceholderForType([inp.iter])
yield from expression(ctx, inp.base, base_phft)
yield from expression(ctx, inp.iter, iter_phft)
yield SameTypeConstraint(inp.func.posonlyargs[0].type3, inp.func.returns_type3, base_phft, phft,
comment='foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b')
yield MustImplementTypeClassConstraint(ctx, 'Foldable', [iter_phft])
return
raise NotImplementedError(expression, inp)
def statement_return(ctx: Context, fun: ourlang.Function, inp: ourlang.StatementReturn) -> ConstraintGenerator:

View File

@ -3,6 +3,7 @@ from typing import Any, Callable
from .functions import (
TypeConstructorVariable,
TypeVariable,
TypeVariableApplication_Nullary,
TypeVariableApplication_Unary,
)
from .typeclasses import Type3ClassArgs
@ -60,7 +61,10 @@ class TypeApplicationRouter[S, R]:
raise NoRouteForTypeException(arg0, typ)
TypeVariableLookup = dict[TypeVariable, tuple[KindArgument, ...]]
TypeVariableLookup = tuple[
dict[TypeVariable, KindArgument],
dict[TypeConstructorVariable, TypeConstructor_Base[Any]],
]
class TypeClassArgsRouter[S, R]:
"""
@ -95,11 +99,12 @@ class TypeClassArgsRouter[S, R]:
def __call__(self, arg0: S, tv_map: dict[TypeVariable, Type3]) -> R:
key: list[Type3 | TypeConstructor_Base[Any]] = []
arguments: TypeVariableLookup = {}
arguments: TypeVariableLookup = (dict(tv_map), {}, )
for tc_arg in self.args:
if isinstance(tc_arg, TypeVariable):
key.append(tv_map[tc_arg])
arguments[0][tc_arg] = tv_map[tc_arg]
continue
for tvar, typ in tv_map.items():
@ -108,16 +113,24 @@ class TypeClassArgsRouter[S, R]:
continue
key.append(typ.application.constructor)
arguments[1][tc_arg] = typ.application.constructor
if isinstance(tvar.application, TypeVariableApplication_Unary):
if isinstance(typ.application, TypeApplication_Type):
arguments[tvar.application.arguments] = typ.application.arguments
da_type, = typ.application.arguments
sa_type_tv = tvar.application.arguments
arguments[0][sa_type_tv] = da_type
continue
# FIXME: This feels sketchy. Shouldn't the type variable
# have the exact same number as arguments?
if isinstance(typ.application, TypeApplication_TypeInt):
arguments[tvar.application.arguments] = typ.application.arguments
sa_type, sa_len = typ.application.arguments
sa_type_tv = tvar.application.arguments
sa_len_tv = TypeVariable(sa_type_tv.name + '*', TypeVariableApplication_Nullary(None, None))
arguments[0][sa_type_tv] = sa_type
arguments[0][sa_len_tv] = sa_len
continue
raise NotImplementedError(tvar.application, typ.application)

View File

@ -1,4 +1,4 @@
from typing import Dict, Iterable, List, Mapping, Optional, Union
from typing import Dict, Iterable, List, Mapping, Optional
from .functions import (
Constraint_TypeClassInstanceExists,
@ -42,8 +42,8 @@ class Type3Class:
self,
name: str,
args: Type3ClassArgs,
methods: Mapping[str, Iterable[Union[Type3, TypeVariable]]],
operators: Mapping[str, Iterable[Union[Type3, TypeVariable]]],
methods: Mapping[str, Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]]],
operators: Mapping[str, Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]]],
inherited_classes: Optional[List['Type3Class']] = None,
additional_context: Optional[Mapping[str, Iterable[ConstraintBase]]] = None,
) -> None:
@ -71,19 +71,23 @@ class Type3Class:
return self.name
def _create_signature(
method_arg_list: Iterable[Type3 | TypeVariable],
method_arg_list: Iterable[Type3 | TypeVariable | list[Type3 | TypeVariable]],
type_class3: Type3Class,
) -> FunctionSignature:
context = TypeVariableContext()
if not isinstance(type_class3.args[0], TypeConstructorVariable):
context.constraints.append(Constraint_TypeClassInstanceExists(type_class3, type_class3.args))
signature_args: list[Type3 | TypeVariable] = []
signature_args: list[Type3 | TypeVariable | list[Type3 | TypeVariable]] = []
for method_arg in method_arg_list:
if isinstance(method_arg, Type3):
signature_args.append(method_arg)
continue
if isinstance(method_arg, list):
signature_args.append(method_arg)
continue
if isinstance(method_arg, TypeVariable):
type_constructor = method_arg.application.constructor
if type_constructor is None:

View File

@ -3,7 +3,6 @@ Contains the final types for use in Phasm, as well as construtors.
"""
from typing import (
Any,
Callable,
Hashable,
Self,
Tuple,
@ -141,27 +140,21 @@ class TypeConstructor_Base[T]:
"""
Base class for type construtors
"""
__slots__ = ('name', 'on_create', '_cache', )
__slots__ = ('name', '_cache', )
name: str
"""
The name of the type constructor
"""
on_create: Callable[[T, Type3], None]
"""
Who to let know if a type is created
"""
_cache: dict[T, Type3]
"""
When constructing a type with the same arguments,
it should produce the exact same result.
"""
def __init__(self, name: str, on_create: Callable[[T, Type3], None]) -> None:
def __init__(self, name: str) -> None:
self.name = name
self.on_create = on_create
self._cache = {}
@ -188,7 +181,6 @@ class TypeConstructor_Base[T]:
result = self._cache.get(key, None)
if result is None:
self._cache[key] = result = Type3(self.make_name(key), self.make_application(key))
self.on_create(key, result)
return result
@ -292,7 +284,6 @@ class TypeConstructor_Struct(TypeConstructor_Base[tuple[tuple[str, Type3], ...]]
def __call__(self, name: str, args: tuple[tuple[str, Type3], ...]) -> Type3:
result = Type3(name, self.make_application(args))
self.on_create(args, result)
return result
class TypeApplication_Struct(TypeApplication_Base[TypeConstructor_Struct, tuple[tuple[str, Type3], ...]]):

View File

@ -170,11 +170,12 @@ class Generator_Local:
self.generator.add_statement('local.tee', variable.name_ref, comment=comment)
class GeneratorBlock:
def __init__(self, generator: 'Generator', name: str, params: Iterable[str] = (), result: str | None = None) -> None:
def __init__(self, generator: 'Generator', name: str, params: Iterable[str] = (), result: str | None = None, comment: str | None = None) -> None:
self.generator = generator
self.name = name
self.params = params
self.result = result
self.comment = comment
def __enter__(self) -> None:
stmt = self.name
@ -186,7 +187,7 @@ class GeneratorBlock:
if self.result:
stmt = f'{stmt} (result {self.result})'
self.generator.add_statement(stmt)
self.generator.add_statement(stmt, comment=self.comment)
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
if not exc_type:
@ -208,7 +209,7 @@ class Generator:
# 2.4.5 Control Instructions
self.nop = functools.partial(self.add_statement, 'nop')
self.unreachable = functools.partial(self.add_statement, 'unreachable')
# block
self.block = functools.partial(GeneratorBlock, self, 'block')
self.loop = functools.partial(GeneratorBlock, self, 'loop')
self.if_ = functools.partial(GeneratorBlock, self, 'if')
# br

View File

@ -1,3 +1,4 @@
import os
import struct
import sys
from typing import Any, Generator, Iterable, List, TextIO, Union
@ -10,6 +11,7 @@ from phasm.runtime import (
calculate_alloc_size_struct,
calculate_alloc_size_tuple,
)
from phasm.stdlib.types import TYPE_INFO_CONSTRUCTED, TYPE_INFO_MAP
from phasm.type3 import types as type3types
from phasm.type3.routers import NoRouteForTypeException, TypeApplicationRouter
@ -35,26 +37,39 @@ class Suite:
def __init__(self, code_py: str) -> None:
self.code_py = code_py
def run_code(self, *args: Any, runtime: str = 'wasmtime', func_name: str = 'testEntry', imports: runners.Imports = None, do_format_check: bool = True) -> Any:
def run_code(
self,
*args: Any,
runtime: str = 'wasmtime',
func_name: str = 'testEntry',
imports: runners.Imports = None,
do_format_check: bool = True,
verbose: bool | None = None,
) -> Any:
"""
Compiles the given python code into wasm and
then runs it
Returned is an object with the results set
"""
if verbose is None:
verbose = bool(os.environ.get('VERBOSE'))
class_ = RUNNER_CLASS_MAP[runtime]
runner = class_(self.code_py)
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
if verbose:
write_header(sys.stderr, 'Phasm')
runner.dump_phasm_code(sys.stderr)
runner.parse()
runner.parse(verbose=verbose)
runner.compile_ast()
runner.compile_wat()
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
if verbose:
write_header(sys.stderr, 'Assembly')
runner.dump_wasm_wat(sys.stderr)
runner.interpreter_setup()
runner.interpreter_load(imports)
@ -69,8 +84,9 @@ class Suite:
wasm_args: List[Union[float, int]] = []
if args:
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
if verbose:
write_header(sys.stderr, 'Memory (pre alloc)')
runner.interpreter_dump_memory(sys.stderr)
for arg, arg_typ in zip(args, func_args, strict=True):
if arg_typ in (prelude.u8, prelude.u32, prelude.u64, ):
@ -94,8 +110,9 @@ class Suite:
except NoRouteForTypeException:
raise NotImplementedError(arg_typ, arg)
write_header(sys.stderr, 'Memory (pre run)')
runner.interpreter_dump_memory(sys.stderr)
if verbose:
write_header(sys.stderr, 'Memory (pre run)')
runner.interpreter_dump_memory(sys.stderr)
result = SuiteResult()
result.returned_value = runner.call(func_name, *wasm_args)
@ -106,8 +123,9 @@ class Suite:
result.returned_value,
)
write_header(sys.stderr, 'Memory (post run)')
runner.interpreter_dump_memory(sys.stderr)
if verbose:
write_header(sys.stderr, 'Memory (post run)')
runner.interpreter_dump_memory(sys.stderr)
return result
@ -134,7 +152,7 @@ def _write_memory_stored_value(
try:
adr2 = ALLOCATE_MEMORY_STORED_ROUTER((runner, val), val_typ)
runner.interpreter_write_memory(adr, compiler.module_data_u32(adr2))
return 4
return TYPE_INFO_CONSTRUCTED.alloc_size
except NoRouteForTypeException:
to_write = WRITE_LOOKUP_MAP[val_typ.name](val)
runner.interpreter_write_memory(adr, to_write)
@ -286,43 +304,36 @@ def _load_memory_stored_returned_value(
return LOAD_FROM_ADDRESS_ROUTER((runner, wasm_value), ret_type3)
def _unpack(runner: runners.RunnerBase, typ: type3types.Type3, inp: bytes) -> Any:
typ_info = TYPE_INFO_MAP.get(typ.name, TYPE_INFO_CONSTRUCTED)
assert len(inp) == typ_info.alloc_size
if typ is prelude.u8:
# See compiler.py, LOAD_STORE_TYPE_MAP and module_data_u8
assert len(inp) == 4
return struct.unpack('<I', inp)[0]
return struct.unpack('<B', inp)[0]
if typ is prelude.u32:
assert len(inp) == 4
return struct.unpack('<I', inp)[0]
if typ is prelude.u64:
assert len(inp) == 8
return struct.unpack('<Q', inp)[0]
if typ is prelude.i8:
# See compiler.py, LOAD_STORE_TYPE_MAP and module_data_i8
assert len(inp) == 4
return struct.unpack('<i', inp)[0]
return struct.unpack('<b', inp)[0]
if typ is prelude.i32:
assert len(inp) == 4
return struct.unpack('<i', inp)[0]
if typ is prelude.i64:
assert len(inp) == 8
return struct.unpack('<q', inp)[0]
if typ is prelude.f32:
assert len(inp) == 4
return struct.unpack('<f', inp)[0]
if typ is prelude.f64:
assert len(inp) == 8
return struct.unpack('<d', inp)[0]
if (prelude.InternalPassAsPointer, (typ, )) in prelude.PRELUDE_TYPE_CLASS_INSTANCES_EXISTING:
if typ_info is TYPE_INFO_CONSTRUCTED:
# Note: For applied types, inp should contain a 4 byte pointer
assert len(inp) == 4
adr = struct.unpack('<I', inp)[0]
return LOAD_FROM_ADDRESS_ROUTER((runner, adr), typ)

View File

@ -32,12 +32,12 @@ class RunnerBase:
"""
_dump_code(textio, self.phasm_code)
def parse(self) -> None:
def parse(self, verbose: bool = True) -> None:
"""
Parses the Phasm code into an AST
"""
self.phasm_ast = phasm_parse(self.phasm_code)
phasm_type3(self.phasm_ast, verbose=True)
phasm_type3(self.phasm_ast, verbose=verbose)
def compile_ast(self) -> None:
"""
@ -120,6 +120,8 @@ class RunnerWasmtime(RunnerBase):
if vartype is int:
params.append(wasmtime.ValType.i32())
elif vartype is float:
params.append(wasmtime.ValType.f32())
else:
raise NotImplementedError
@ -128,6 +130,8 @@ class RunnerWasmtime(RunnerBase):
pass # No return value
elif func.__annotations__['return'] is int:
results.append(wasmtime.ValType.i32())
elif func.__annotations__['return'] is float:
results.append(wasmtime.ValType.f32())
else:
raise NotImplementedError('Return type', func.__annotations__['return'])

View File

@ -1,61 +0,0 @@
import pytest
from ..helpers import Suite
@pytest.mark.integration_test
def test_foldl_1():
code_py = """
def u8_or(l: u8, r: u8) -> u8:
return l | r
@exported
def testEntry(b: bytes) -> u8:
return foldl(u8_or, 128, b)
"""
suite = Suite(code_py)
result = suite.run_code(b'')
assert 128 == result.returned_value
result = suite.run_code(b'\x80')
assert 128 == result.returned_value
result = suite.run_code(b'\x80\x40')
assert 192 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10')
assert 240 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10\x08\x04\x02\x01')
assert 255 == result.returned_value
@pytest.mark.integration_test
def test_foldl_2():
code_py = """
def xor(l: u8, r: u8) -> u8:
return l ^ r
@exported
def testEntry(a: bytes, b: bytes) -> u8:
return foldl(xor, 0, a) ^ foldl(xor, 0, b)
"""
suite = Suite(code_py)
result = suite.run_code(b'\x55\x0F', b'\x33\x80')
assert 233 == result.returned_value
@pytest.mark.integration_test
def test_foldl_3():
code_py = """
def xor(l: u32, r: u8) -> u32:
return l ^ extend(r)
@exported
def testEntry(a: bytes) -> u32:
return foldl(xor, 0, a)
"""
suite = Suite(code_py)
result = suite.run_code(b'\x55\x0F\x33\x80')
assert 233 == result.returned_value

View File

@ -23,6 +23,23 @@ def testEntry(f: {type_}) -> u8:
assert exp_result == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('type_, in_put, exp_result', [
('(u8, u8, u8, )', (45, 46, 47), 46, ),
('u8[5]', (45, 46, 47, 48, 49), 46, ),
('bytes', b'This is a test', 104)
])
def test_subscript_1(type_, in_put, exp_result):
code_py = f"""
@exported
def testEntry(f: {type_}) -> u8:
return f[1]
"""
result = Suite(code_py).run_code(in_put)
assert exp_result == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('type_, in_put, exp_result', [
('(u8, u8, u8, )', (45, 46, 47), 47, ),
@ -82,7 +99,7 @@ def testEntry(x: (u8, u32, u64)) -> u64:
@pytest.mark.parametrize('type_, in_put', [
('(u8, u8, )', (45, 46), ),
('u8[2]', (45, 46), ),
# bytes isn't known at runtime so works like normal
# dynamic_array isn't known at runtime so works like normal
])
def test_subscript_oob_constant_low(type_, in_put):
code_py = f"""

View File

@ -36,6 +36,142 @@ def testEntry(x: Foo[4]) -> Foo:
with pytest.raises(Type3Exception, match='Missing type class instantation: NatNum Foo'):
Suite(code_py).run_code()
@pytest.mark.integration_test
@pytest.mark.parametrize('length', [1, 5, 13])
@pytest.mark.parametrize('direction', ['foldl', 'foldr'])
def test_foldable_foldl_foldr_size(direction, length):
code_py = f"""
def u64_add(l: u64, r: u64) -> u64:
return l + r
@exported
def testEntry(b: u64[{length}]) -> u64:
return {direction}(u64_add, 100, b)
"""
suite = Suite(code_py)
in_put = tuple(range(1, length + 1))
result = suite.run_code(in_put)
assert (100 + sum(in_put)) == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('direction', ['foldl', 'foldr'])
def test_foldable_foldl_foldr_compounded_type(direction):
code_py = f"""
def combine_foldl(b: u64, a: (u32, u32, )) -> u64:
return extend(a[0] * a[1]) + b
def combine_foldr(a: (u32, u32, ), b: u64) -> u64:
return extend(a[0] * a[1]) + b
@exported
def testEntry(b: (u32, u32, )[3]) -> u64:
return {direction}(combine_{direction}, 10000, b)
"""
suite = Suite(code_py)
result = suite.run_code(((2, 5), (25, 4), (125, 8)))
assert 11110 == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('in_put, direction, exp_result', [
([], 'foldl', 0, ),
([], 'foldr', 0, ),
([1], 'foldl', -1, ),
([1], 'foldr', 1, ),
([1,2], 'foldl', -3, ),
([1,2], 'foldr', -1, ),
([1,2,3], 'foldl', -6, ),
([1,2,3], 'foldr', 2, ),
([1,2,3,4], 'foldl', -10, ),
([1,2,3,4], 'foldr', -2, ),
([1,2,3,4,5], 'foldl', -15, ),
([1,2,3,4,5], 'foldr', 3, ),
([1,2,3,4,5,6], 'foldl', -21, ),
([1,2,3,4,5,6], 'foldr', -3, ),
([1,2,3,4,5,6,7], 'foldl', -28, ),
([1,2,3,4,5,6,7], 'foldr', 4, ),
([1,2,3,4,5,6,7,8], 'foldl', -36, ),
([1,2,3,4,5,6,7,8], 'foldr', -4, ),
])
def test_foldable_foldl_foldr_result(direction, in_put, exp_result):
# See https://stackoverflow.com/a/13280185
code_py = f"""
def i32_sub(l: i32, r: i32) -> i32:
return l - r
@exported
def testEntry(b: i32[{len(in_put)}]) -> i32:
return {direction}(i32_sub, 0, b)
"""
suite = Suite(code_py)
result = suite.run_code(tuple(in_put))
assert exp_result == result.returned_value
@pytest.mark.integration_test
def test_foldable_foldl_bytes():
code_py = """
def u8_or(l: u8, r: u8) -> u8:
return l | r
@exported
def testEntry(b: bytes) -> u8:
return foldl(u8_or, 0, b)
"""
suite = Suite(code_py)
result = suite.run_code(b'')
assert 0 == result.returned_value
result = suite.run_code(b'\x80')
assert 128 == result.returned_value
result = suite.run_code(b'\x80\x40')
assert 192 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10')
assert 240 == result.returned_value
result = suite.run_code(b'\x80\x40\x20\x10\x08\x04\x02\x01')
assert 255 == result.returned_value
@pytest.mark.integration_test
@pytest.mark.parametrize('in_typ', ['i8', 'i8[3]'])
def test_foldable_argument_must_be_a_function(in_typ):
code_py = f"""
@exported
def testEntry(x: {in_typ}, y: i32, z: i64[3]) -> i32:
return foldl(x, y, z)
"""
r_in_typ = in_typ.replace('[', '\\[').replace(']', '\\]')
with pytest.raises(Type3Exception, match=f'{r_in_typ} must be a function instead'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_foldable_argument_must_be_right_function():
code_py = """
def foo(l: i32, r: i64) -> i64:
return extend(l) + r
@exported
def testEntry(i: i64, l: i64[3]) -> i64:
return foldr(foo, i, l)
"""
with pytest.raises(Type3Exception, match=r'Callable\[i64, i64, i64\] must be Callable\[i32, i64, i64\] instead'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_foldable_invalid_return_type():