Compare commits

..

2 Commits

Author SHA1 Message Date
Johan B.W. de Vries
e8e7acc102 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-18 15:46:39 +02:00
Johan B.W. de Vries
83186cce78 Reworks bytes into dynamic array
bytes continues to be the preferred name for u8[...].
Also, putting bytes values into the VM and taking them
out still uses Python bytes values.

This also lets used use the len function on them, for
whatever that's worth.
2025-05-18 15:37:13 +02:00
13 changed files with 239 additions and 40 deletions

View File

@ -15,8 +15,10 @@ from .type3.types import (
IntType3,
Type3,
TypeApplication_Struct,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeApplication_TypeStar,
TypeConstructor_DynamicArray,
TypeConstructor_Function,
TypeConstructor_StaticArray,
TypeConstructor_Tuple,
@ -109,12 +111,25 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
args: tuple[Type3, ...]
if isinstance(inp.type3.application, TypeApplication_TypeStar):
alloc_size_header = None
if isinstance(inp.type3.application, TypeApplication_Type):
# Possibly paranoid assert. If we have a future variadic type,
# does it also do this tuple instantation like this?
assert isinstance(inp.type3.application.constructor, TypeConstructor_DynamicArray)
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)
alloc_size_header = len(inp.elements)
elif isinstance(inp.type3.application, TypeApplication_TypeStar):
# Possibly paranoid assert. If we have a future variadic type,
# does it also do this tuple instantation like this?
assert isinstance(inp.type3.application.constructor, TypeConstructor_Tuple)
args = inp.type3.application.arguments
alloc_size = calculate_alloc_size(inp.type3, is_member=False)
elif isinstance(inp.type3.application, TypeApplication_TypeInt):
# Possibly paranoid assert. If we have a future type of kind * -> Int -> *,
# does it also do this tuple instantation like this?
@ -123,6 +138,7 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
sa_type, sa_len = inp.type3.application.arguments
args = tuple(sa_type for _ in range(sa_len.value))
alloc_size = calculate_alloc_size(inp.type3, is_member=False)
else:
raise NotImplementedError('tuple_instantiation', inp.type3)
@ -135,12 +151,17 @@ def tuple_instantiation(wgn: WasmGenerator, mod: ourlang.Module, inp: ourlang.Tu
wgn.add_statement('nop', comment=f'{tmp_var.name} := ({comment_elements})')
# Allocated the required amounts of bytes in memory
wgn.i32.const(calculate_alloc_size(inp.type3, is_member=False))
wgn.i32.const(alloc_size)
wgn.call(stdlib_alloc.__alloc__)
wgn.local.set(tmp_var)
if alloc_size_header is not None:
wgn.local.get(tmp_var)
wgn.i32.const(alloc_size_header)
wgn.i32.store()
# Store each element individually
offset = 0
offset = 0 if alloc_size_header is None else 4
for element, exp_type3 in zip(inp.elements, args, strict=True):
assert element.type3 == exp_type3

View File

@ -270,6 +270,9 @@ class FunctionParam:
self.name = name
self.type3 = type3
def __repr__(self) -> str:
return f'FunctionParam({self.name!r}, {self.type3!r})'
class Function:
"""
A function processes input and produces output

View File

@ -632,8 +632,15 @@ class OurVisitor:
if isinstance(node.slice, ast.Slice):
_raise_static_error(node, 'Must subscript using an index')
if not isinstance(node.slice, ast.Constant):
_raise_static_error(node, 'Must subscript using a constant index')
if node.slice.value is Ellipsis:
return prelude.dynamic_array(
self.visit_type(module, node.value),
)
if not isinstance(node.slice.value, int):
_raise_static_error(node, 'Must subscript using a constant integer index')
if not isinstance(node.ctx, ast.Load):

View File

@ -20,6 +20,7 @@ from ..type3.types import (
Type3,
TypeApplication_Nullary,
TypeConstructor_Base,
TypeConstructor_DynamicArray,
TypeConstructor_Function,
TypeConstructor_StaticArray,
TypeConstructor_Struct,
@ -158,9 +159,15 @@ f64 = Type3('f64', TypeApplication_Nullary(None, None))
A 32-bits IEEE 754 float, of 64 bits width.
"""
bytes_ = Type3('bytes', TypeApplication_Nullary(None, None))
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)
"""
This is a runtime-determined length piece of memory that can be indexed at runtime.
This is a dynamic length piece of memory.
It should be applied with two arguments. It has a runtime
determined length, and each argument is the same.
"""
def sa_on_create(args: tuple[Type3, IntType3], typ: Type3) -> None:
@ -168,12 +175,10 @@ def sa_on_create(args: tuple[Type3, IntType3], typ: Type3) -> None:
static_array = TypeConstructor_StaticArray('static_array', on_create=sa_on_create)
"""
A type constructor.
This is a fixed length piece of memory.
Any static array is a fixed length piece of memory that can be indexed at runtime.
It should be applied with one argument. It has a runtime-dynamic length
of the same type repeated.
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:
@ -208,20 +213,6 @@ This is like a tuple, but each argument is named, so that developers
can get and set fields by name.
"""
PRELUDE_TYPES: dict[str, Type3] = {
'none': none,
'bool': bool_,
'u8': u8,
'u32': u32,
'u64': u64,
'i8': i8,
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
'bytes': bytes_,
}
a = TypeVariable('a', TypeVariableApplication_Nullary(None, None))
b = TypeVariable('b', TypeVariableApplication_Nullary(None, None))
@ -232,7 +223,7 @@ InternalPassAsPointer = Type3Class('InternalPassAsPointer', (a, ), methods={}, o
Internal type class to keep track which types we pass arounds as a pointer.
"""
instance_type_class(InternalPassAsPointer, bytes_)
# instance_type_class(InternalPassAsPointer, bytes_)
# instance_type_class(InternalPassAsPointer, static_array)
# instance_type_class(InternalPassAsPointer, tuple_)
# instance_type_class(InternalPassAsPointer, struct)
@ -537,12 +528,15 @@ instance_type_class(Floating, f64, methods={
'sqrt': stdtypes.f64_floating_sqrt,
})
Sized_ = Type3Class('Sized', (a, ), methods={
'len': [a, u32],
Sized_ = Type3Class('Sized', (t, ), methods={
'len': [t(a), u32],
}, operators={}) # FIXME: Once we get type class families, add [] here
instance_type_class(Sized_, bytes_, methods={
'len': stdtypes.bytes_sized_len,
instance_type_class(Sized_, dynamic_array, methods={
'len': stdtypes.dynamic_array_sized_len,
})
instance_type_class(Sized_, static_array, methods={
'len': stdtypes.static_array_sized_len,
})
Extendable = Type3Class('Extendable', (a, b, ), methods={
@ -593,12 +587,33 @@ Foldable = Type3Class('Foldable', (t, ), methods={
'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)
PRELUDE_TYPES: dict[str, Type3] = {
'none': none,
'bool': bool_,
'u8': u8,
'u32': u32,
'u64': u64,
'i8': i8,
'i32': i32,
'i64': i64,
'f32': f32,
'f64': f64,
'bytes': bytes_,
}
PRELUDE_TYPE_CLASSES = {
'Eq': Eq,
'Ord': Ord,

View File

@ -1008,11 +1008,23 @@ def f64_intnum_neg(g: Generator, tv_map: TypeVariableLookup) -> None:
## ###
## Class Sized
def bytes_sized_len(g: Generator, tv_map: TypeVariableLookup) -> None:
def dynamic_array_sized_len(g: Generator, tv_map: TypeVariableLookup) -> None:
del tv_map
# The length is stored in the first 4 bytes
g.i32.load()
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)
## ###
## Extendable
@ -1083,6 +1095,12 @@ def f32_f64_demote(g: Generator, tv_map: TypeVariableLookup) -> None:
del tv_map
g.f32.demote_f64()
## ###
## 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
@ -1196,6 +1214,9 @@ def static_array_sum(g: Generator, tvl: 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:
raise NotImplementedError
def static_array_foldl(g: Generator, tvl: TypeVariableLookup) -> None:
tv_map, tc_map = tvl
@ -1311,6 +1332,9 @@ def static_array_foldl(g: Generator, tvl: TypeVariableLookup) -> None:
# 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

View File

@ -15,6 +15,7 @@ from .types import (
Type3,
TypeApplication_Nullary,
TypeApplication_Struct,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeApplication_TypeStar,
TypeConstructor_Base,
@ -199,6 +200,13 @@ 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):
@ -289,6 +297,14 @@ class TupleMatchConstraint(ConstraintBase):
self.exp_type = exp_type
self.args = list(args)
def _generate_dynamic_array(self, sa_args: tuple[Type3]) -> CheckResult:
sa_type, = sa_args
return [
SameTypeConstraint(arg, sa_type)
for arg in self.args
]
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
sa_type, sa_len = sa_args
@ -310,6 +326,7 @@ class TupleMatchConstraint(ConstraintBase):
]
GENERATE_ROUTER = TypeApplicationRouter['TupleMatchConstraint', CheckResult]()
GENERATE_ROUTER.add(prelude.dynamic_array, _generate_dynamic_array)
GENERATE_ROUTER.add(prelude.static_array, _generate_static_array)
GENERATE_ROUTER.add(prelude.tuple_, _generate_tuple)
@ -356,7 +373,7 @@ class MustImplementTypeClassConstraint(ConstraintBase):
typ_list.append(typ)
continue
if isinstance(typ.application, (TypeApplication_TypeInt, TypeApplication_TypeStar)):
if isinstance(typ.application, (TypeApplication_Type, TypeApplication_TypeInt, TypeApplication_TypeStar)):
typ_list.append(typ.application.constructor)
continue
@ -409,6 +426,29 @@ class LiteralFitsConstraint(ConstraintBase):
self.type3 = type3
self.literal = literal
def _generate_dynamic_array(self, da_args: tuple[Type3]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple', comment=self.comment)
da_type, = da_args
res: list[ConstraintBase] = []
res.extend(
LiteralFitsConstraint(da_type, y)
for y in self.literal.value
)
# Generate placeholders so each Literal expression
# gets updated when we figure out the type of the
# expression the literal is used in
res.extend(
SameTypeConstraint(da_type, PlaceholderForType([y]))
for y in self.literal.value
)
return res
def _generate_static_array(self, sa_args: tuple[Type3, IntType3]) -> CheckResult:
if not isinstance(self.literal, ourlang.ConstantTuple):
return Error('Must be tuple', comment=self.comment)
@ -490,6 +530,7 @@ class LiteralFitsConstraint(ConstraintBase):
return res
GENERATE_ROUTER = TypeApplicationRouter['LiteralFitsConstraint', CheckResult]()
GENERATE_ROUTER.add(prelude.dynamic_array, _generate_dynamic_array)
GENERATE_ROUTER.add(prelude.static_array, _generate_static_array)
GENERATE_ROUTER.add(prelude.struct, _generate_struct)
GENERATE_ROUTER.add(prelude.tuple_, _generate_tuple)

View File

@ -176,7 +176,10 @@ def _expression_function_call(
if not isinstance(sig_arg.application, TypeVariableApplication_Unary):
raise NotImplementedError(sig_arg.application)
assert sig_arg.application.arguments in type_var_map # When does this happen?
if sig_arg.application.arguments not in type_var_map:
# e.g., len :: t a -> u32
# i.e. "a" does not matter at all
continue
yield SameTypeArgumentConstraint(
type_var_map[sig_arg],

View File

@ -7,7 +7,13 @@ from .functions import (
TypeVariableApplication_Unary,
)
from .typeclasses import Type3ClassArgs
from .types import KindArgument, Type3, TypeApplication_TypeInt, TypeConstructor_Base
from .types import (
KindArgument,
Type3,
TypeApplication_Type,
TypeApplication_TypeInt,
TypeConstructor_Base,
)
class NoRouteForTypeException(Exception):
@ -110,6 +116,12 @@ class TypeClassArgsRouter[S, R]:
arguments[1][tc_arg] = typ.application.constructor
if isinstance(tvar.application, TypeVariableApplication_Unary):
if isinstance(typ.application, TypeApplication_Type):
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):

View File

@ -195,6 +195,26 @@ class TypeConstructor_Base[T]:
def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.name!r}, ...)'
class TypeConstructor_Type(TypeConstructor_Base[Tuple[Type3]]):
"""
Base class type constructors of kind: * -> *
Notably, static array.
"""
__slots__ = ()
def make_application(self, key: Tuple[Type3]) -> 'TypeApplication_Type':
return TypeApplication_Type(self, key)
def make_name(self, key: Tuple[Type3]) -> str:
return f'{self.name} {key[0].name} '
def __call__(self, arg0: Type3) -> Type3:
return self.construct((arg0, ))
class TypeApplication_Type(TypeApplication_Base[TypeConstructor_Type, Tuple[Type3]]):
pass
class TypeConstructor_TypeInt(TypeConstructor_Base[Tuple[Type3, IntType3]]):
"""
Base class type constructors of kind: * -> Int -> *
@ -231,6 +251,13 @@ class TypeConstructor_TypeStar(TypeConstructor_Base[Tuple[Type3, ...]]):
class TypeApplication_TypeStar(TypeApplication_Base[TypeConstructor_TypeStar, Tuple[Type3, ...]]):
pass
class TypeConstructor_DynamicArray(TypeConstructor_Type):
def make_name(self, key: Tuple[Type3]) -> str:
if 'u8' == key[0].name:
return 'bytes'
return f'{key[0].name}[...]'
class TypeConstructor_StaticArray(TypeConstructor_TypeInt):
def make_name(self, key: Tuple[Type3, IntType3]) -> str:
return f'{key[0].name}[{key[1].value}]'

View File

@ -152,6 +152,25 @@ def _allocate_memory_stored_bytes(attrs: tuple[runners.RunnerBase, bytes]) -> in
runner.interpreter_write_memory(adr + 4, val)
return adr
def _allocate_memory_stored_dynamic_array(attrs: tuple[runners.RunnerBase, Any], da_args: tuple[type3types.Type3]) -> int:
runner, val = attrs
da_type, = da_args
if not isinstance(val, tuple):
raise InvalidArgumentException(f'Expected tuple; got {val!r} instead')
alloc_size = 4 + len(val) * calculate_alloc_size(da_type, True)
adr = runner.call('stdlib.alloc.__alloc__', alloc_size)
assert isinstance(adr, int) # Type int
sys.stderr.write(f'Allocation 0x{adr:08x} {repr(val)}\n')
offset = adr
offset += _write_memory_stored_value(runner, offset, prelude.u32, len(val))
for val_el_val in val:
offset += _write_memory_stored_value(runner, offset, da_type, val_el_val)
return adr
def _allocate_memory_stored_static_array(attrs: tuple[runners.RunnerBase, Any], sa_args: tuple[type3types.Type3, type3types.IntType3]) -> int:
runner, val = attrs
@ -211,6 +230,7 @@ def _allocate_memory_stored_tuple(attrs: tuple[runners.RunnerBase, Any], tp_args
ALLOCATE_MEMORY_STORED_ROUTER = TypeApplicationRouter[tuple[runners.RunnerBase, Any], Any]()
ALLOCATE_MEMORY_STORED_ROUTER.add_n(prelude.bytes_, _allocate_memory_stored_bytes)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.dynamic_array, _allocate_memory_stored_dynamic_array)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.static_array, _allocate_memory_stored_static_array)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.struct, _allocate_memory_stored_struct)
ALLOCATE_MEMORY_STORED_ROUTER.add(prelude.tuple_, _allocate_memory_stored_tuple)
@ -325,6 +345,26 @@ def _split_read_bytes(all_bytes: bytes, split_sizes: Iterable[int]) -> Generator
yield all_bytes[offset:offset + size]
offset += size
def _load_dynamic_array_from_address(attrs: tuple[runners.RunnerBase, int], da_args: tuple[type3types.Type3]) -> Any:
runner, adr = attrs
da_type, = da_args
sys.stderr.write(f'Reading 0x{adr:08x} {da_type:s}[...]\n')
read_bytes = runner.interpreter_read_memory(adr, 4)
array_len, = struct.unpack('<I', read_bytes)
adr += 4
arg_size_1 = calculate_alloc_size(da_type, is_member=True)
arg_sizes = [arg_size_1 for _ in range(array_len)] # _split_read_bytes requires one arg per value
read_bytes = runner.interpreter_read_memory(adr, sum(arg_sizes))
return tuple(
_unpack(runner, da_type, arg_bytes)
for arg_bytes in _split_read_bytes(read_bytes, arg_sizes)
)
def _load_static_array_from_address(attrs: tuple[runners.RunnerBase, int], sa_args: tuple[type3types.Type3, type3types.IntType3]) -> Any:
runner, adr = attrs
sub_typ, len_typ = sa_args
@ -379,6 +419,7 @@ def _load_tuple_from_address(attrs: tuple[runners.RunnerBase, int], tp_args: tup
LOAD_FROM_ADDRESS_ROUTER = TypeApplicationRouter[tuple[runners.RunnerBase, int], Any]()
LOAD_FROM_ADDRESS_ROUTER.add_n(prelude.bytes_, _load_bytes_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.dynamic_array, _load_dynamic_array_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.static_array, _load_static_array_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.struct, _load_struct_from_address)
LOAD_FROM_ADDRESS_ROUTER.add(prelude.tuple_, _load_tuple_from_address)

View File

@ -60,7 +60,7 @@ CONSTANT: (u32, ) = $VAL0
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error(
'Tuple element count mismatch',
'The given literal must fit the expected type',
@ -113,7 +113,7 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error(
'Mismatch between applied types argument count',
'The type of a tuple is a combination of its members',
@ -175,7 +175,7 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value returned from function constant should match its return type',
@ -226,7 +226,7 @@ def select(x: $TYPE) -> (u32, ):
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value returned from function select should match its return type',
@ -273,7 +273,7 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_'):
expect_type_error(
'Mismatch between applied types argument count',
# FIXME: Shouldn't this be the same as for the else statement?
@ -330,7 +330,7 @@ def testEntry() -> i32:
```
```py
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('struct_'):
if TYPE_NAME.startswith('tuple_') or TYPE_NAME.startswith('static_array_') or TYPE_NAME.startswith('dynamic_array_') or TYPE_NAME.startswith('struct_'):
expect_type_error(
TYPE + ' must be (u32, ) instead',
'The type of the value passed to argument 0 of function helper should match the type of that argument',

View File

@ -0,0 +1,5 @@
{
"TYPE_NAME": "dynamic_array_u64",
"TYPE": "u64[...]",
"VAL0": "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, )"
}

View File

@ -6,7 +6,7 @@ from ..helpers import Suite
@pytest.mark.integration_test
@pytest.mark.parametrize('type_, in_put, exp_result', [
('bytes', b'Hello, world!', 13),
# ('u8[4]', (1, 2, 3, 4), 4), # FIXME: Implement this
('u8[4]', (1, 2, 3, 4), 4),
])
def test_len(type_, in_put, exp_result):
code_py = f"""