Subscriptable is now less hardcoded

Now only the tuple variant is hardcoded. The rest is via
a typeclass.
This commit is contained in:
Johan B.W. de Vries 2025-06-02 19:01:20 +02:00
parent 6f40276a9c
commit 3cb4860973
7 changed files with 201 additions and 121 deletions

View File

@ -12,8 +12,6 @@
- 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? - 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?
- 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.
- calculate_alloc_size can be reworked; is_member isn't useful with TYPE_INFO_MAP - calculate_alloc_size can be reworked; is_member isn't useful with TYPE_INFO_MAP

View File

@ -34,6 +34,7 @@ from .typeclasses import (
promotable, promotable,
reinterpretable, reinterpretable,
sized, sized,
subscriptable,
) )
@ -88,7 +89,7 @@ class BuildDefault(BuildBase[Generator]):
convertable, reinterpretable, convertable, reinterpretable,
natnum, intnum, fractional, floating, natnum, intnum, fractional, floating,
integral, integral,
foldable, foldable, subscriptable,
sized, sized,
] ]

View File

@ -0,0 +1,147 @@
"""
The Eq type class is defined for types that can be compered based on equality.
"""
from typing import Any
from ...type3.functions import TypeConstructorVariable, make_typevar
from ...type3.routers import TypeVariableLookup
from ...type3.typeclasses import Type3Class
from ...type3.types import IntType3, Type3
from ...wasmgenerator import Generator as WasmGenerator
from ..base import BuildBase
def load(build: BuildBase[Any]) -> None:
a = make_typevar('a')
t = TypeConstructorVariable('t')
u32 = build.types['u32']
Subscriptable = Type3Class('Subscriptable', (t, ), methods={}, operators={
'[]': [t(a), u32, a],
})
build.register_type_class(Subscriptable)
class SubscriptableCodeGenerator:
def __init__(self, build: BuildBase[WasmGenerator]) -> None:
self.build = build
def wasm_dynamic_array_getitem(self, g: WasmGenerator, 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']
assert isinstance(sa_type, Type3)
ptr_type_info = self.build.type_info_map['ptr']
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
getitem_adr = g.temp_var_t(u32_type_info.wasm_type, 'getitem_adr')
getitem_idx = g.temp_var_t(u32_type_info.wasm_type, 'getitem_idx')
# Stack: [varref: *ard, idx: u32]
g.local.set(getitem_idx)
# Stack: [varref: *ard]
g.local.set(getitem_adr)
# Stack: []
# Out of bounds check based on memory stored length
# Stack: []
g.local.get(getitem_idx)
# Stack: [idx: u32]
g.local.get(getitem_adr)
# Stack: [idx: u32, varref: *ard]
g.i32.load()
# Stack: [idx: u32, len: u32]
g.i32.ge_u()
# Stack: [res: bool]
with g.if_():
g.unreachable(comment='Out of bounds')
# Stack: []
g.local.get(getitem_adr)
# Stack: [varref: *ard]
g.i32.const(4)
# Stack: [varref: *ard, 4]
g.i32.add()
# Stack: [firstel: *ard]
g.local.get(getitem_idx)
# Stack: [firstel: *ard, idx: u32]
g.i32.const(sa_type_info.alloc_size)
# Stack: [firstel: *ard, idx: u32, as: u32]
g.i32.mul()
# Stack: [firstel: *ard, offset: u32]
g.i32.add()
# Stack: [eladr: *ard]
g.add_statement(sa_type_info.wasm_load_func)
# Stack: [el]
def wasm_static_array_getitem(self, g: WasmGenerator, 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)
ptr_type_info = self.build.type_info_map['ptr']
u32_type_info = self.build.type_info_map['u32']
sa_type_info = self.build.type_info_map.get(sa_type.name)
if sa_type_info is None:
sa_type_info = ptr_type_info
# OPTIMIZE: If index is a constant, we can use offset instead of multiply
# and we don't need to do the out of bounds check
getitem_idx = g.temp_var_t(u32_type_info.wasm_type, 'getitem_idx')
# Stack: [varref: *ard, idx: u32]
g.local.tee(getitem_idx)
# Stack: [varref: *ard, idx: u32]
# Out of bounds check based on sa_len.value
g.i32.const(sa_len.value)
# Stack: [varref: *ard, idx: u32, len: u32]
g.i32.ge_u()
# Stack: [varref: *ard, res: bool]
with g.if_():
g.unreachable(comment='Out of bounds')
# Stack: [varref: *ard]
g.local.get(getitem_idx)
# Stack: [varref: *ard, idx: u32]
g.i32.const(sa_type_info.alloc_size)
# Stack: [varref: *ard, idx: u32, as: u32]
g.i32.mul()
# Stack: [varref: *ard, offset: u32]
g.i32.add()
# Stack: [eladr: *ard]
g.add_statement(sa_type_info.wasm_load_func)
# Stack: [el]
def wasm(build: BuildBase[WasmGenerator]) -> None:
Subscriptable = build.type_classes['Subscriptable']
gen = SubscriptableCodeGenerator(build)
build.instance_type_class(Subscriptable, build.dynamic_array, operators={
'[]': gen.wasm_dynamic_array_getitem,
})
build.instance_type_class(Subscriptable, build.static_array, operators={
'[]': gen.wasm_static_array_getitem,
})

View File

@ -133,87 +133,13 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator],
# Return the allocated address # Return the allocated address
wgn.local.get(tmp_var) wgn.local.get(tmp_var)
def expression_subscript_dynamic_array( def expression_subscript_tuple(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Subscript) -> None:
attrs: tuple[WasmGenerator, ourlang.Module[WasmGenerator], ourlang.Subscript],
args: tuple[Type3],
) -> None:
wgn, mod, inp = attrs
el_type, = args
el_type_info = mod.build.type_info_map.get(el_type.name)
if el_type_info is None:
el_type_info = mod.build.type_info_map['ptr']
tmp_idx = wgn.temp_var_i32('tmp_idx')
tmp_adr = wgn.temp_var_i32('tmp_adr')
expression(wgn, mod, inp.varref)
expression(wgn, mod, inp.index)
wgn.local.set(tmp_idx)
wgn.local.set(tmp_adr)
# Out of bounds check based on size stored in memory
wgn.local.get(tmp_idx)
wgn.local.get(tmp_adr)
wgn.i32.load()
wgn.i32.ge_u()
with wgn.if_():
wgn.unreachable(comment='Out of bounds')
# tmp_ard + 4 + (tmp_idx * alloc_size)
wgn.local.get(tmp_adr)
wgn.i32.const(4)
wgn.i32.add()
wgn.local.get(tmp_idx)
wgn.i32.const(el_type_info.alloc_size)
wgn.i32.mul()
wgn.i32.add()
wgn.add_statement(el_type_info.wasm_load_func)
def expression_subscript_static_array(
attrs: tuple[WasmGenerator, ourlang.Module[WasmGenerator], ourlang.Subscript],
args: tuple[Type3, IntType3],
) -> None:
wgn, mod, inp = attrs
el_type, el_len = args
# OPTIMIZE: If index is a constant, we can use offset instead of multiply
# and we don't need to do the out of bounds check
tmp_var = wgn.temp_var_i32('index')
expression(wgn, mod, inp.varref)
expression(wgn, mod, inp.index)
wgn.local.tee(tmp_var)
# Out of bounds check based on el_len.value
wgn.i32.const(el_len.value)
wgn.i32.ge_u()
with wgn.if_():
wgn.unreachable(comment='Out of bounds')
el_type_info = mod.build.type_info_map.get(el_type.name)
if el_type_info is None:
el_type_info = mod.build.type_info_map['ptr']
wgn.local.get(tmp_var)
wgn.i32.const(el_type_info.alloc_size)
wgn.i32.mul()
wgn.i32.add()
wgn.add_statement(el_type_info.wasm_load_func)
def expression_subscript_tuple(
attrs: tuple[WasmGenerator, ourlang.Module[WasmGenerator], ourlang.Subscript],
args: tuple[Type3, ...],
) -> None:
wgn, mod, inp = attrs
assert isinstance(inp.index, ourlang.ConstantPrimitive) assert isinstance(inp.index, ourlang.ConstantPrimitive)
assert isinstance(inp.index.value, int) assert isinstance(inp.index.value, int)
assert inp.varref.type3 is not None, TYPE3_ASSERTION_ERROR
assert isinstance(inp.varref.type3.application, TypeApplication_TypeStar)
args = inp.varref.type3.application.arguments
offset = 0 offset = 0
for el_type in args[0:inp.index.value]: for el_type in args[0:inp.index.value]:
@ -233,11 +159,6 @@ def expression_subscript_tuple(
el_type_info = mod.build.type_info_map['ptr'] el_type_info = mod.build.type_info_map['ptr']
wgn.add_statement(el_type_info.wasm_load_func, f'offset={offset}') wgn.add_statement(el_type_info.wasm_load_func, f'offset={offset}')
SUBSCRIPT_ROUTER = TypeApplicationRouter[tuple[WasmGenerator, ourlang.Module[WasmGenerator], ourlang.Subscript], None]()
SUBSCRIPT_ROUTER.add(builtins.dynamic_array, expression_subscript_dynamic_array)
SUBSCRIPT_ROUTER.add(builtins.static_array, expression_subscript_static_array)
SUBSCRIPT_ROUTER.add(builtins.tuple_, expression_subscript_tuple)
def expression(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Expression) -> None: def expression(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourlang.Expression) -> None:
""" """
Compile: Any expression Compile: Any expression
@ -388,10 +309,18 @@ def expression(wgn: WasmGenerator, mod: ourlang.Module[WasmGenerator], inp: ourl
return return
if isinstance(inp, ourlang.Subscript): if isinstance(inp, ourlang.Subscript):
assert inp.type3 is not None, TYPE3_ASSERTION_ERROR
assert inp.varref.type3 is not None, TYPE3_ASSERTION_ERROR assert inp.varref.type3 is not None, TYPE3_ASSERTION_ERROR
# Type checker guarantees we don't get routing errors if inp.varref.type3.application.constructor is mod.build.tuple_:
SUBSCRIPT_ROUTER((wgn, mod, inp, ), inp.varref.type3) expression_subscript_tuple(wgn, mod, inp)
return
inp_as_fc = ourlang.FunctionCall(mod.build.type_classes['Subscriptable'].operators['[]'])
inp_as_fc.type3 = inp.type3
inp_as_fc.arguments = [inp.varref, inp.index]
expression(wgn, mod, inp_as_fc)
return return
if isinstance(inp, ourlang.AccessStructMember): if isinstance(inp, ourlang.AccessStructMember):

View File

@ -634,34 +634,9 @@ class CanBeSubscriptedConstraint(ConstraintBase):
self.index_type3 = index_type3 self.index_type3 = index_type3
self.index_const = index_const self.index_const = index_const
self.generate_router = TypeApplicationRouter()
self.generate_router.add(context.build.dynamic_array, self.__class__._generate_dynamic_array)
self.generate_router.add(context.build.static_array, self.__class__._generate_static_array)
self.generate_router.add(context.build.tuple_, self.__class__._generate_tuple)
def _generate_dynamic_array(self, da_args: tuple[Type3]) -> CheckResult:
da_type, = da_args
return [
SameTypeConstraint(self.context, self.context.build.types['u32'], self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(self.context, da_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
]
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
sa_type, sa_len = sa_args
if self.index_const is not None and (self.index_const < 0 or sa_len.value <= self.index_const):
return Error('Tuple index out of range')
return [
SameTypeConstraint(self.context, self.context.build.types['u32'], self.index_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
SameTypeConstraint(self.context, sa_type, self.ret_type3, comment='([]) :: Subscriptable a => a b -> u32 -> b'),
]
def _generate_tuple(self, tp_args: tuple[Type3, ...]) -> CheckResult: def _generate_tuple(self, tp_args: tuple[Type3, ...]) -> CheckResult:
# We special case tuples to allow for ease of use to the programmer # We special case tuples to allow for ease of use to the programmer
# e.g. rather than having to do `fst a` and `snd a` and only have to-sized tuples # e.g. rather than having to do `fst a` and `snd a` and only have tuples of size 2
# we use a[0] and a[1] and allow for a[2] and on. # we use a[0] and a[1] and allow for a[2] and on.
if self.index_const is None: if self.index_const is None:
@ -681,10 +656,40 @@ class CanBeSubscriptedConstraint(ConstraintBase):
exp_type = self.type3.resolve_as exp_type = self.type3.resolve_as
try: if exp_type.application.constructor == self.context.build.tuple_:
return self.generate_router(self, exp_type) return self._generate_tuple(exp_type.application.arguments)
except NoRouteForTypeException:
return Error(f'{exp_type.name} cannot be subscripted') result: NewConstraintList = []
result.extend([
MustImplementTypeClassConstraint(
self.context,
self.context.build.type_classes['Subscriptable'],
[exp_type],
),
SameTypeConstraint(
self.context,
self.context.build.types['u32'],
self.index_type3,
),
])
if isinstance(exp_type.application, (TypeApplication_Type, TypeApplication_TypeInt, )):
result.extend([
SameTypeConstraint(
self.context,
exp_type.application.arguments[0],
self.ret_type3,
),
])
# else: The MustImplementTypeClassConstraint will catch this
if exp_type.application.constructor == self.context.build.static_array:
_, sa_len = exp_type.application.arguments
if self.index_const is not None and (self.index_const < 0 or sa_len.value <= self.index_const):
return Error('Tuple index out of range')
return result
def human_readable(self) -> HumanReadableRet: def human_readable(self) -> HumanReadableRet:
return ( return (

View File

@ -100,7 +100,7 @@ class TypeConstructorVariable:
return False return False
if not isinstance(other, TypeConstructorVariable): if not isinstance(other, TypeConstructorVariable):
raise NotImplementedError raise NotImplementedError(other)
return (self.name == other.name) return (self.name == other.name)

View File

@ -147,5 +147,5 @@ def testEntry(x: u8) -> u8:
return x[0] return x[0]
""" """
with pytest.raises(Type3Exception, match='u8 cannot be subscripted'): with pytest.raises(Type3Exception, match='Missing type class instantation: Subscriptable u8'):
Suite(code_py).run_code() Suite(code_py).run_code()