Implements sum for Foldable types

Foldable take a TypeConstructor. The first argument must be a
NatNum.
This commit is contained in:
Johan B.W. de Vries 2025-05-05 14:09:38 +02:00
parent f6cb1a8c1d
commit 5a149b7796
10 changed files with 199 additions and 39 deletions

View File

@ -12,9 +12,12 @@
- Also, check the codes for FIXME and TODO - Also, check the codes for FIXME and TODO
- Allocation is done using pointers for members, is this desired? - Allocation is done using pointers for members, is this desired?
- See if we want to replace Fractional with Real, and add Rational, Irrationl, Algebraic, Transendental - See if we want to replace Fractional with Real, and add Rational, Irrationl, Algebraic, Transendental
- Implement q32? q64? Two i32/i64 divided?
- Does Subscript do what we want? It's a language feature rather a normal typed thing. How would you implement your own Subscript-able type? - Does Subscript do what we want? It's a language feature rather a normal typed thing. How would you implement your own Subscript-able type?
- Clean up Subscript implementation - it's half implemented in the compiler. Makes more sense to move more parts to stdlib_types. - 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. - Have a set of rules or guidelines for the constraint comments, they're messy.
- Do we need to store the placeholders on the expressions? They're only temporary while the type checker is running
- Might not even need to store them at all outside the generated constraints?
- Parser is putting stuff in ModuleDataBlock - Parser is putting stuff in ModuleDataBlock
- Surely the compiler should build data blocks - Surely the compiler should build data blocks

View File

@ -2,7 +2,7 @@
This module contains the code to convert parsed Ourlang into WebAssembly code This module contains the code to convert parsed Ourlang into WebAssembly code
""" """
import struct import struct
from typing import Dict, List, Optional from typing import Dict, List, Optional, Union
from . import codestyle, ourlang, prelude, wasm from . import codestyle, ourlang, prelude, wasm
from .runtime import calculate_alloc_size, calculate_member_offset from .runtime import calculate_alloc_size, calculate_member_offset
@ -259,6 +259,10 @@ INSTANCES = {
prelude.Promotable.methods['demote']: { prelude.Promotable.methods['demote']: {
'a=f32,b=f64': stdlib_types.f32_f64_demote, 'a=f32,b=f64': stdlib_types.f32_f64_demote,
}, },
prelude.Foldable.methods['sum']: {
'a=i32,t=i32[4]': stdlib_types.static_array_i32_4_sum,
'a=i32,t=i32[5]': stdlib_types.static_array_i32_5_sum,
},
} }
def phasm_compile(inp: ourlang.Module) -> wasm.Module: def phasm_compile(inp: ourlang.Module) -> wasm.Module:
@ -457,7 +461,7 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None:
assert isinstance(inp.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR assert isinstance(inp.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR
type_var_map: Dict[type3functions.TypeVariable, type3types.Type3] = {} type_var_map: Dict[Union[type3functions.TypeVariable, type3functions.TypeConstructorVariable], type3types.Type3] = {}
for type_var, arg_expr in zip(inp.operator.signature.args, [inp.left, inp.right, inp], strict=True): for type_var, arg_expr in zip(inp.operator.signature.args, [inp.left, inp.right, inp], strict=True):
if not isinstance(type_var, type3functions.TypeVariable): if not isinstance(type_var, type3functions.TypeVariable):
@ -488,12 +492,16 @@ def expression(wgn: WasmGenerator, inp: ourlang.Expression) -> None:
type_var_map = {} type_var_map = {}
for type_var, arg_expr in zip(inp.function.signature.args, inp.arguments + [inp], strict=True): for type_var, arg_expr in zip(inp.function.signature.args, inp.arguments + [inp], strict=True):
if not isinstance(type_var, type3functions.TypeVariable): if isinstance(type_var, type3types.Type3):
# Fixed type, not part of the lookup requirements # Fixed type, not part of the lookup requirements
continue continue
if isinstance(type_var, (type3functions.TypeVariable, type3functions.TypeConstructorVariable, )):
assert isinstance(arg_expr.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR assert isinstance(arg_expr.type3, type3types.Type3), type3placeholders.TYPE3_ASSERTION_ERROR
type_var_map[type_var] = arg_expr.type3 type_var_map[type_var] = arg_expr.type3
continue
raise NotImplementedError
instance_key = ','.join( instance_key = ','.join(
f'{k.letter}={v.name}' f'{k.letter}={v.name}'
@ -960,6 +968,7 @@ def module(inp: ourlang.Module) -> wasm.Module:
stdlib_types.__u32_pow2__, stdlib_types.__u32_pow2__,
stdlib_types.__u8_rotl__, stdlib_types.__u8_rotl__,
stdlib_types.__u8_rotr__, stdlib_types.__u8_rotr__,
stdlib_types.__sa_i32_sum__,
] + [ ] + [
function(x) function(x)
for x in inp.functions.values() for x in inp.functions.values()

View File

@ -1,20 +1,27 @@
""" """
The prelude are all the builtin types, type classes and methods The prelude are all the builtin types, type classes and methods
""" """
from typing import Any, Union
from ..type3.functions import TypeVariable from ..type3.functions import (
Constraint_TypeClassInstanceExists,
TypeConstructorVariable,
TypeVariable,
)
from ..type3.typeclasses import Type3Class from ..type3.typeclasses import Type3Class
from ..type3.types import ( from ..type3.types import (
IntType3,
Type3, Type3,
TypeConstructor,
TypeConstructor_StaticArray, TypeConstructor_StaticArray,
TypeConstructor_Struct, TypeConstructor_Struct,
TypeConstructor_Tuple, TypeConstructor_Tuple,
) )
PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: set[tuple[Type3Class, tuple[Type3, ...]]] = set() PRELUDE_TYPE_CLASS_INSTANCES_EXISTING: set[tuple[Type3Class, tuple[Union[Type3, TypeConstructor[Any], TypeConstructor_Struct], ...]]] = set()
def instance_type_class(cls: Type3Class, *typ: Type3) -> None: def instance_type_class(cls: Type3Class, *typ: Union[Type3, TypeConstructor[Any], TypeConstructor_Struct]) -> None:
global PRELUDE_TYPE_CLASS_INSTANCES_EXISTING global PRELUDE_TYPE_CLASS_INSTANCES_EXISTING
# TODO: Check for required existing instantiations # TODO: Check for required existing instantiations
@ -93,7 +100,7 @@ bytes_ = Type3('bytes')
This is a runtime-determined length piece of memory that can be indexed at runtime. This is a runtime-determined length piece of memory that can be indexed at runtime.
""" """
def sa_on_create(typ: Type3) -> None: def sa_on_create(args: tuple[Type3, IntType3], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ) instance_type_class(InternalPassAsPointer, typ)
static_array = TypeConstructor_StaticArray('static_array', on_create=sa_on_create) static_array = TypeConstructor_StaticArray('static_array', on_create=sa_on_create)
@ -106,7 +113,7 @@ It should be applied with one argument. It has a runtime-dynamic length
of the same type repeated. of the same type repeated.
""" """
def tp_on_create(typ: Type3) -> None: def tp_on_create(args: tuple[Type3, ...], typ: Type3) -> None:
instance_type_class(InternalPassAsPointer, typ) instance_type_class(InternalPassAsPointer, typ)
tuple_ = TypeConstructor_Tuple('tuple', on_create=tp_on_create) tuple_ = TypeConstructor_Tuple('tuple', on_create=tp_on_create)
@ -143,6 +150,7 @@ PRELUDE_TYPES: dict[str, Type3] = {
a = TypeVariable('a') a = TypeVariable('a')
b = TypeVariable('b') b = TypeVariable('b')
t = TypeConstructorVariable('t', [])
InternalPassAsPointer = Type3Class('InternalPassAsPointer', [a], methods={}, operators={}) InternalPassAsPointer = Type3Class('InternalPassAsPointer', [a], methods={}, operators={})
""" """
@ -286,6 +294,14 @@ Promotable = Type3Class('Promotable', [a, b], methods={
instance_type_class(Promotable, f32, f64) instance_type_class(Promotable, f32, f64)
Foldable = Type3Class('Foldable', [t], methods={
'sum': [t(a), a],
}, operators={}, additional_context={
'sum': [Constraint_TypeClassInstanceExists(NatNum, [a])],
})
instance_type_class(Foldable, static_array)
PRELUDE_TYPE_CLASSES = { PRELUDE_TYPE_CLASSES = {
'Eq': Eq, 'Eq': Eq,
'Ord': Ord, 'Ord': Ord,
@ -321,4 +337,5 @@ PRELUDE_METHODS = {
**Sized_.methods, **Sized_.methods,
**Extendable.methods, **Extendable.methods,
**Promotable.methods, **Promotable.methods,
**Foldable.methods,
} }

View File

@ -384,6 +384,50 @@ def __u8_rotr__(g: Generator, x: i32, r: i32) -> i32:
return i32('return') # To satisfy mypy return i32('return') # To satisfy mypy
@func_wrapper()
def __sa_i32_sum__(g: Generator, adr: i32, arlen: i32) -> i32:
i32_size = 4
s = i32('s')
stop = i32('stop')
# stop = adr + ar_len * i32_size
g.local.get(adr)
g.local.get(arlen)
g.i32.const(i32_size)
g.i32.mul()
g.i32.add()
g.local.set(stop)
# sum = 0
g.i32.const(0)
g.local.set(s)
with g.loop():
# sum = sum + *adr
g.local.get(adr)
g.i32.load()
g.local.get(s)
g.i32.add()
g.local.set(s)
# adr = adr + i32_size
g.local.get(adr)
g.i32.const(i32_size)
g.i32.add()
g.local.tee(adr)
# loop if adr < stop
g.local.get(stop)
g.i32.lt_u()
g.br_if(0)
# return sum
g.local.get(s)
g.return_()
return i32('return') # To satisfy mypy
## ### ## ###
## class Eq ## class Eq
@ -920,3 +964,11 @@ def f32_f64_promote(g: Generator) -> None:
def f32_f64_demote(g: Generator) -> None: def f32_f64_demote(g: Generator) -> None:
g.f32.demote_f64() g.f32.demote_f64()
def static_array_i32_4_sum(g: Generator) -> None:
g.i32.const(4)
g.add_statement('call $stdlib.types.__sa_i32_sum__')
def static_array_i32_5_sum(g: Generator) -> None:
g.i32.const(5)
g.add_statement('call $stdlib.types.__sa_i32_sum__')

View File

@ -3,7 +3,7 @@ This module contains possible constraints generated based on the AST
These need to be resolved before the program can be compiled. These need to be resolved before the program can be compiled.
""" """
from typing import Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from .. import ourlang, prelude from .. import ourlang, prelude
from . import placeholders, typeclasses, types from . import placeholders, typeclasses, types
@ -48,7 +48,7 @@ class Context:
__slots__ = ('type_class_instances_existing', ) __slots__ = ('type_class_instances_existing', )
# Constraint_TypeClassInstanceExists # Constraint_TypeClassInstanceExists
type_class_instances_existing: set[tuple[typeclasses.Type3Class, tuple[types.Type3, ...]]] type_class_instances_existing: set[tuple[typeclasses.Type3Class, tuple[Union[types.Type3, types.TypeConstructor[Any], types.TypeConstructor_Struct], ...]]]
def __init__(self) -> None: def __init__(self) -> None:
self.type_class_instances_existing = set() self.type_class_instances_existing = set()

View File

@ -49,20 +49,20 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
return return
if isinstance(inp, ourlang.BinaryOp) or isinstance(inp, ourlang.FunctionCall): if isinstance(inp, ourlang.BinaryOp) or isinstance(inp, ourlang.FunctionCall):
signature = inp.operator.signature if isinstance(inp, ourlang.BinaryOp) else inp.function.signature
arguments = [inp.left, inp.right] if isinstance(inp, ourlang.BinaryOp) else inp.arguments
func_name = f'({inp.operator.name})' if isinstance(inp, ourlang.BinaryOp) else inp.function.name func_name = f'({inp.operator.name})' if isinstance(inp, ourlang.BinaryOp) else inp.function.name
arguments = [inp.left, inp.right] if isinstance(inp, ourlang.BinaryOp) else inp.arguments
for call_arg in arguments:
yield from expression(ctx, call_arg)
signature = inp.operator.signature if isinstance(inp, ourlang.BinaryOp) else inp.function.signature
type_var_map = { type_var_map = {
x: placeholders.PlaceholderForType([]) x: placeholders.PlaceholderForType([])
for x in signature.args for x in signature.args
if isinstance(x, functions.TypeVariable) if isinstance(x, functions.TypeVariable)
or isinstance(x, functions.TypeConstructorVariable)
} }
for call_arg in arguments:
yield from expression(ctx, call_arg)
for constraint in signature.context.constraints: for constraint in signature.context.constraints:
if isinstance(constraint, functions.Constraint_TypeClassInstanceExists): if isinstance(constraint, functions.Constraint_TypeClassInstanceExists):
yield MustImplementTypeClassConstraint( yield MustImplementTypeClassConstraint(
@ -80,7 +80,7 @@ def expression(ctx: Context, inp: ourlang.Expression) -> ConstraintGenerator:
else: else:
comment = f'The type of the value passed to argument {arg_no} of function {func_name} should match the type of that argument' comment = f'The type of the value passed to argument {arg_no} of function {func_name} should match the type of that argument'
if isinstance(sig_part, functions.TypeVariable): if isinstance(sig_part, (functions.TypeVariable, functions.TypeConstructorVariable, )):
yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3, comment=comment) yield SameTypeConstraint(type_var_map[sig_part], arg_expr.type3, comment=comment)
continue continue

View File

@ -34,6 +34,25 @@ class TypeVariable:
def __repr__(self) -> str: def __repr__(self) -> str:
return f'TypeVariable({repr(self.letter)})' return f'TypeVariable({repr(self.letter)})'
class TypeConstructorVariable:
"""
Types constructor variable are used in function definition.
They are a lot like TypeVariable, except that they represent a
type constructor rather than a type directly.
"""
__slots__ = ('letter', 'args', )
def __init__(self, letter: str, args: Iterable[TypeVariable]) -> None:
self.letter = letter
self.args = list(args)
def __call__(self, tvar: TypeVariable) -> 'TypeConstructorVariable':
return TypeConstructorVariable(self.letter, self.args + [tvar])
def __repr__(self) -> str:
return f'TypeConstructorVariable({self.letter!r}, {self.args!r})'
class ConstraintBase: class ConstraintBase:
__slots__ = () __slots__ = ()
@ -41,9 +60,9 @@ class Constraint_TypeClassInstanceExists(ConstraintBase):
__slots__ = ('type_class3', 'types', ) __slots__ = ('type_class3', 'types', )
type_class3: 'Type3Class' type_class3: 'Type3Class'
types: list[TypeVariable] types: list[Union[TypeVariable, TypeConstructorVariable]]
def __init__(self, type_class3: 'Type3Class', types: Iterable[TypeVariable]) -> None: def __init__(self, type_class3: 'Type3Class', types: Iterable[Union[TypeVariable, TypeConstructorVariable]]) -> None:
self.type_class3 = type_class3 self.type_class3 = type_class3
self.types = list(types) self.types = list(types)
@ -52,27 +71,22 @@ class Constraint_TypeClassInstanceExists(ConstraintBase):
assert len(self.type_class3.args) == len(self.types) assert len(self.type_class3.args) == len(self.types)
class TypeVariableContext: class TypeVariableContext:
__slots__ = ('variables', 'constraints', ) __slots__ = ('constraints', )
variables: set[TypeVariable]
constraints: list[ConstraintBase] constraints: list[ConstraintBase]
def __init__(self) -> None: def __init__(self, constraints: Iterable[ConstraintBase] = ()) -> None:
self.variables = set() self.constraints = list(constraints)
self.constraints = []
def __copy__(self) -> 'TypeVariableContext': def __copy__(self) -> 'TypeVariableContext':
result = TypeVariableContext() return TypeVariableContext(self.constraints)
result.variables.update(self.variables)
result.constraints.extend(self.constraints)
return result
class FunctionSignature: class FunctionSignature:
__slots__ = ('context', 'args', ) __slots__ = ('context', 'args', )
context: TypeVariableContext context: TypeVariableContext
args: List[Union['Type3', TypeVariable]] args: List[Union['Type3', TypeVariable, TypeConstructorVariable]]
def __init__(self, context: TypeVariableContext, args: Iterable[Union['Type3', TypeVariable]]) -> None: def __init__(self, context: TypeVariableContext, args: Iterable[Union['Type3', TypeVariable, TypeConstructorVariable]]) -> None:
self.context = context.__copy__() self.context = context.__copy__()
self.args = list(args) self.args = list(args)

View File

@ -2,7 +2,9 @@ from typing import Dict, Iterable, List, Mapping, Optional, Union
from .functions import ( from .functions import (
Constraint_TypeClassInstanceExists, Constraint_TypeClassInstanceExists,
ConstraintBase,
FunctionSignature, FunctionSignature,
TypeConstructorVariable,
TypeVariable, TypeVariable,
TypeVariableContext, TypeVariableContext,
) )
@ -26,7 +28,7 @@ class Type3Class:
__slots__ = ('name', 'args', 'methods', 'operators', 'inherited_classes', ) __slots__ = ('name', 'args', 'methods', 'operators', 'inherited_classes', )
name: str name: str
args: List[TypeVariable] args: List[Union[TypeVariable, TypeConstructorVariable]]
methods: Dict[str, Type3ClassMethod] methods: Dict[str, Type3ClassMethod]
operators: Dict[str, Type3ClassMethod] operators: Dict[str, Type3ClassMethod]
inherited_classes: List['Type3Class'] inherited_classes: List['Type3Class']
@ -34,10 +36,11 @@ class Type3Class:
def __init__( def __init__(
self, self,
name: str, name: str,
args: Iterable[TypeVariable], args: Iterable[Union[TypeVariable, TypeConstructorVariable]],
methods: Mapping[str, Iterable[Union[Type3, TypeVariable]]], methods: Mapping[str, Iterable[Union[Type3, TypeVariable, TypeConstructorVariable]]],
operators: Mapping[str, Iterable[Union[Type3, TypeVariable]]], operators: Mapping[str, Iterable[Union[Type3, TypeVariable, TypeConstructorVariable]]],
inherited_classes: Optional[List['Type3Class']] = None, inherited_classes: Optional[List['Type3Class']] = None,
additional_context: Optional[Mapping[str, Iterable[ConstraintBase]]] = None,
) -> None: ) -> None:
self.name = name self.name = name
self.args = list(args) self.args = list(args)
@ -55,5 +58,12 @@ class Type3Class:
} }
self.inherited_classes = inherited_classes or [] self.inherited_classes = inherited_classes or []
if additional_context:
for func_name, constraint_list in additional_context.items():
func = self.methods.get(func_name) or self.operators.get(func_name)
assert func is not None # type hint
func.signature.context.constraints.extend(constraint_list)
def __repr__(self) -> str: def __repr__(self) -> str:
return self.name return self.name

View File

@ -106,7 +106,7 @@ class TypeConstructor(Generic[T]):
The name of the type constructor The name of the type constructor
""" """
on_create: Callable[[Type3], None] on_create: Callable[[T, Type3], None]
""" """
Who to let know if a type is created Who to let know if a type is created
""" """
@ -122,7 +122,7 @@ class TypeConstructor(Generic[T]):
Sometimes we need to know the key that created a type. Sometimes we need to know the key that created a type.
""" """
def __init__(self, name: str, on_create: Callable[[Type3], None]) -> None: def __init__(self, name: str, on_create: Callable[[T, Type3], None]) -> None:
self.name = name self.name = name
self.on_create = on_create self.on_create = on_create
@ -152,7 +152,7 @@ class TypeConstructor(Generic[T]):
if result is None: if result is None:
self._cache[key] = result = Type3(self.make_name(key)) self._cache[key] = result = Type3(self.make_name(key))
self._reverse_cache[result] = key self._reverse_cache[result] = key
self.on_create(result) self.on_create(key, result)
return result return result

View File

@ -0,0 +1,55 @@
import pytest
from phasm.type3.entry import Type3Exception
from ..helpers import Suite
@pytest.mark.integration_test
def test_foldable_sum():
code_py = """
@exported
def testEntry(x: i32[5]) -> i32:
return sum(x)
"""
result = Suite(code_py).run_code((4, 5, 6, 7, 8, ))
assert 30 == result.returned_value
@pytest.mark.integration_test
def test_foldable_sum_not_natnum():
code_py = """
class Foo:
bar: i32
@exported
def testEntry(x: Foo[4]) -> Foo:
return sum(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: NatNum Foo'):
Suite(code_py).run_code()
@pytest.mark.integration_test
def test_foldable_invalid_return_type():
code_py = """
@exported
def testEntry(x: i32[5]) -> f64:
return sum(x)
"""
with pytest.raises(Type3Exception, match='f64 must be i32 instead'):
Suite(code_py).run_code((4, 5, 6, 7, 8, ))
@pytest.mark.integration_test
def test_foldable_not_foldable():
code_py = """
@exported
def testEntry(x: i32) -> i32:
return sum(x)
"""
with pytest.raises(Type3Exception, match='Missing type class instantation: Foldable i32'):
Suite(code_py).run_code()