diff --git a/TODO.md b/TODO.md index 69f765f..d1b5a15 100644 --- a/TODO.md +++ b/TODO.md @@ -2,8 +2,7 @@ - Implement a proper type matching / checking system - Implement subscript as an operator - - Implement another operator - - Figure out how to do type classes + - Re-implement Subscript contraints - Doing the LiteralFitsConstraint with a tuple doesn't put the types on the tuple elements - Implement structs again, with the `.foo` notation working - Rename constant to literal diff --git a/phasm/ourlang.py b/phasm/ourlang.py index bc7a6dd..4200f2f 100644 --- a/phasm/ourlang.py +++ b/phasm/ourlang.py @@ -59,6 +59,9 @@ class ConstantTuple(Constant): super().__init__() self.value = value + def __repr__(self) -> str: + return f'ConstantTuple({repr(self.value)})' + class VariableReference(Expression): """ An variable reference expression within a statement diff --git a/phasm/parser.py b/phasm/parser.py index 0ab3537..3cadcdc 100644 --- a/phasm/parser.py +++ b/phasm/parser.py @@ -714,13 +714,11 @@ class OurVisitor: if not isinstance(node.ctx, ast.Load): _raise_static_error(node, 'Must be load context') - elements = ', '.join( - self.visit_type(module, elt).name - for elt in node.elts + return type3types.AppliedType3( + type3types.tuple, + (self.visit_type(module, elt) for elt in node.elts) ) - return type3types.Type3(f'({elements}, )') - raise NotImplementedError(f'{node} as type') def _not_implemented(check: Any, msg: str) -> None: diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index 4835759..c22aa54 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -178,18 +178,13 @@ class LiteralFitsConstraint(ConstraintBase): __slots__ = ('type3', 'literal', ) type3: types.Type3OrPlaceholder - literal: ourlang.ConstantPrimitive + literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple] - def __init__(self, type3: types.Type3OrPlaceholder, literal: ourlang.ConstantPrimitive) -> None: + def __init__(self, type3: types.Type3OrPlaceholder, literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple]) -> None: self.type3 = type3 self.literal = literal def check(self) -> CheckResult: - if isinstance(self.type3, types.PlaceholderForType): - return RequireTypeSubstitutes() - - val = self.literal.value - int_table: Dict[str, Tuple[int, bool]] = { 'u8': (1, False), 'u32': (4, False), @@ -199,35 +194,60 @@ class LiteralFitsConstraint(ConstraintBase): 'i64': (8, True), } - if self.type3.name in int_table: - bts, sgn = int_table[self.type3.name] - - if isinstance(val, int): - try: - val.to_bytes(bts, 'big', signed=sgn) - except OverflowError: - return Error(f'Must fit in {bts} byte(s)') # FIXME: Add line information - - return None - - return Error('Must be integer') # FIXME: Add line information - float_table: Dict[str, None] = { 'f32': None, 'f64': None, } - if self.type3.name in float_table: - _ = float_table[self.type3.name] + def _check(type3: types.Type3OrPlaceholder, literal: Union[ourlang.ConstantPrimitive, ourlang.ConstantTuple]) -> CheckResult: + if isinstance(type3, types.PlaceholderForType): + return RequireTypeSubstitutes() - if isinstance(val, float): - # FIXME: Bit check + val = literal.value + + if type3.name in int_table: + bts, sgn = int_table[type3.name] + + if isinstance(val, int): + try: + val.to_bytes(bts, 'big', signed=sgn) + except OverflowError: + return Error(f'Must fit in {bts} byte(s)') # FIXME: Add line information + + return None + + return Error('Must be integer') # FIXME: Add line information + + + if type3.name in float_table: + _ = float_table[type3.name] + + if isinstance(val, float): + # FIXME: Bit check + + return None + + return Error('Must be real') # FIXME: Add line information + + if isinstance(type3, types.AppliedType3) and type3.base is types.tuple: + if not isinstance(literal, ourlang.ConstantTuple): + return Error('Must be tuple') + + assert isinstance(val, list) # type hint + + if len(type3.args) != len(val): + return Error('Tuple element count mismatch') + + for elt_typ, elt_lit in zip(type3.args, val): + res = _check(elt_typ, elt_lit) + if res is not None: + return res return None - return Error('Must be real') # FIXME: Add line information + raise NotImplementedError - raise NotImplementedError + return _check(self.type3, self.literal) def substitute_placeholders(self, smap: SubstitutionMap) -> None: # FIXME: Duplicate code if isinstance(self.type3, types.PlaceholderForType) and self.type3 in smap: # FIXME: Check recursive? @@ -245,3 +265,62 @@ class LiteralFitsConstraint(ConstraintBase): def __repr__(self) -> str: return f'LiteralFitsConstraint({repr(self.type3)}, {repr(self.literal)})' + +class CanBeSubscriptedConstraint(ConstraintBase): + """ + A value that is subscipted, i.e. a[0] (tuple) or a[b] (static array) + """ + __slots__ = ('type3', 'index', 'index_type3', ) + + type3: types.Type3OrPlaceholder + index: ourlang.Expression + index_type3: types.Type3OrPlaceholder + + def __init__(self, type3: types.Type3OrPlaceholder, index: ourlang.Expression) -> None: + self.type3 = type3 + self.index = index + self.index_type3 = index.type3 + + def check(self) -> CheckResult: + if isinstance(self.type3, types.PlaceholderForType): + return RequireTypeSubstitutes() + + if isinstance(self.index_type3, types.PlaceholderForType): + return RequireTypeSubstitutes() + + if not isinstance(self.type3, types.AppliedType3): + return Error(f'Cannot subscript {self.type3:s}') + + if self.type3.base is types.tuple: + return None + + raise NotImplementedError(self.type3) + + def get_new_placeholder_substitutes(self) -> SubstitutionMap: + if isinstance(self.type3, types.AppliedType3) and self.type3.base is types.tuple and isinstance(self.index_type3, types.PlaceholderForType): + return { + self.index_type3: types.u32, + } + + return {} + + def substitute_placeholders(self, smap: SubstitutionMap) -> None: # FIXME: Duplicate code + if isinstance(self.type3, types.PlaceholderForType) and self.type3 in smap: # FIXME: Check recursive? + self.type3.get_substituted(smap[self.type3]) + self.type3 = smap[self.type3] + + if isinstance(self.index_type3, types.PlaceholderForType) and self.index_type3 in smap: # FIXME: Check recursive? + self.index_type3.get_substituted(smap[self.index_type3]) + self.index_type3 = smap[self.index_type3] + + def human_readable(self) -> HumanReadableRet: + return ( + '{type3}[{index}]', + { + 'type3': self.type3, + 'index': self.index, + }, + ) + + def __repr__(self) -> str: + return f'CanBeSubscriptedConstraint({repr(self.type3)}, {repr(self.index)})' diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index a44c7f2..299d386 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -11,6 +11,7 @@ from .constraints import ( Context, ConstraintBase, + CanBeSubscriptedConstraint, LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint, ) @@ -20,7 +21,7 @@ def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase return [*module(ctx, inp)] def constant(ctx: Context, inp: ourlang.Constant) -> Generator[ConstraintBase, None, None]: - if isinstance(inp, ourlang.ConstantPrimitive): + if isinstance(inp, (ourlang.ConstantPrimitive, ourlang.ConstantTuple, )): yield LiteralFitsConstraint(inp.type3, inp) return @@ -59,6 +60,13 @@ def expression(ctx: Context, inp: ourlang.Expression) -> Generator[ConstraintBas return + if isinstance(inp, ourlang.Subscript): + yield from expression(ctx, inp.varref) + yield from expression(ctx, inp.index) + + yield CanBeSubscriptedConstraint(inp.varref.type3, inp.index) + return + if isinstance(inp, ourlang.AccessStructMember): yield SameTypeConstraint(inp.struct_type3.members[inp.member], inp.type3, f'The type of a struct member reference is the same as the type of struct member {inp.struct_type3.name}.{inp.member}') diff --git a/phasm/type3/entry.py b/phasm/type3/entry.py index 02b4cda..5bc6762 100644 --- a/phasm/type3/entry.py +++ b/phasm/type3/entry.py @@ -22,6 +22,7 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: assert constraint_list placeholder_substitutes: Dict[PlaceholderForType, Type3] = {} + placeholder_id_map: Dict[int, str] = {} restack_counter = 0 @@ -29,7 +30,7 @@ def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: while constraint_list: if verbose: print() - print_constraint_list(constraint_list, placeholder_substitutes) + print_constraint_list(placeholder_id_map, constraint_list, placeholder_substitutes) constraint = constraint_list.pop(0) @@ -92,9 +93,7 @@ def print_constraint(placeholder_id_map: Dict[int, str], constraint: ConstraintB else: print('- ' + txt.format(**act_fmt)) -def print_constraint_list(constraint_list: List[ConstraintBase], placeholder_substitutes: SubstitutionMap) -> None: - placeholder_id_map: Dict[int, str] = {} - +def print_constraint_list(placeholder_id_map: Dict[int, str], constraint_list: List[ConstraintBase], placeholder_substitutes: SubstitutionMap) -> None: print('=== v type3 constraint_list v === ') for psk, psv in placeholder_substitutes.items(): print_constraint(placeholder_id_map, SameTypeConstraint(psk, psv, 'Deduced type')) diff --git a/phasm/type3/types.py b/phasm/type3/types.py index ee58220..265fd80 100644 --- a/phasm/type3/types.py +++ b/phasm/type3/types.py @@ -130,6 +130,8 @@ class AppliedType3(Type3): """ def __init__(self, base: Type3, args: Iterable[Type3OrPlaceholder]) -> None: + args = [*args] + super().__init__( base.name + ' (' @@ -138,7 +140,7 @@ class AppliedType3(Type3): ) self.base = base - self.args = [*args] + self.args = args def __repr__(self) -> str: return f'AppliedType3({repr(self.base)}, {repr(self.args)})' @@ -232,7 +234,16 @@ static_array = Type3('static_array') """ This is a fixed length piece of memory that can be indexed at runtime. -It should be applied with one argument. +It should be applied with one argument. It has a runtime-dynamic length +of the same type repeated. +""" + +tuple = Type3('tuple') # pylint: disable=W0622 +""" +This is a fixed length piece of memory. + +It should be applied with zero or more arguments. It has a compile time +determined length, and each argument can be different. """ LOOKUP_TABLE: Dict[str, Type3] = { diff --git a/tests/integration/test_lang/test_primitives.py b/tests/integration/test_lang/test_primitives.py index 2b7ad3e..d4faa5f 100644 --- a/tests/integration/test_lang/test_primitives.py +++ b/tests/integration/test_lang/test_primitives.py @@ -167,7 +167,6 @@ def testEntry() -> u64: with pytest.raises(Type3Exception, match='u64 must be u32 instead'): Suite(code_py).run_code() - assert False @pytest.mark.integration_test @pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) diff --git a/tests/integration/test_lang/test_tuple.py b/tests/integration/test_lang/test_tuple.py index c0d59c9..fd74a72 100644 --- a/tests/integration/test_lang/test_tuple.py +++ b/tests/integration/test_lang/test_tuple.py @@ -1,5 +1,7 @@ import pytest +from phasm.type3.entry import Type3Exception + from ..constants import COMPLETE_NUMERIC_TYPES, TYPE_MAP from ..helpers import Suite @@ -11,10 +13,7 @@ CONSTANT: ({type_}, ) = (65, ) @exported def testEntry() -> {type_}: - return helper(CONSTANT) - -def helper(vector: ({type_}, )) -> {type_}: - return vector[0] + return CONSTANT[0] """ result = Suite(code_py).run_code() @@ -28,10 +27,7 @@ CONSTANT: (u8, u8, u32, u32, u64, u64, ) = (11, 22, 3333, 4444, 555555, 666666, @exported def testEntry() -> u32: - return helper(CONSTANT) - -def helper(vector: (u8, u8, u32, u32, u64, u64, )) -> u32: - return vector[2] + return CONSTANT[2] """ result = Suite(code_py).run_code() @@ -83,13 +79,22 @@ def testEntry() -> i32x4: assert (1, 2, 3, 0) == result.returned_value +@pytest.mark.integration_test +def test_assign_to_tuple_with_tuple(): + code_py = """ +CONSTANT: (u32, ) = 0 +""" + + with pytest.raises(Type3Exception, match='Must be tuple'): + Suite(code_py).run_code() + @pytest.mark.integration_test def test_tuple_constant_too_few_values(): code_py = """ CONSTANT: (u32, u8, u8, ) = (24, 57, ) """ - with pytest.raises(StaticError, match='Static error on line 2: Invalid number of tuple values'): + with pytest.raises(Type3Exception, match='Tuple element count mismatch'): Suite(code_py).run_code() @pytest.mark.integration_test @@ -98,7 +103,7 @@ def test_tuple_constant_too_many_values(): CONSTANT: (u32, u8, u8, ) = (24, 57, 1, 1, ) """ - with pytest.raises(StaticError, match='Static error on line 2: Invalid number of tuple values'): + with pytest.raises(Type3Exception, match='Tuple element count mismatch'): Suite(code_py).run_code() @pytest.mark.integration_test @@ -107,5 +112,29 @@ def test_tuple_constant_type_mismatch(): CONSTANT: (u32, u8, u8, ) = (24, 4000, 1, ) """ - with pytest.raises(StaticError, match='Static error on line 2: Integer value out of range; expected 0..255, actual 4000'): + with pytest.raises(Type3Exception, match='Must fit in 1 byte(s)'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +def test_tuple_must_use_literal_for_indexing(): + code_py = """ +CONSTANT: u32 = 0 + +@exported +def testEntry(x: (u8, u32, u64)) -> u64: + return x[CONSTANT] +""" + + with pytest.raises(Type3Exception, match='Tuples must be indexed with literals'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +def test_tuple_must_use_integer_for_indexing(): + code_py = """ +@exported +def testEntry(x: (u8, u32, u64)) -> u64: + return x[0.0] +""" + + with pytest.raises(Type3Exception, match='Must be integer'): Suite(code_py).run_code()