From 3bac625714f0e36f4c8e464ad8bcf15f95018b63 Mon Sep 17 00:00:00 2001 From: "Johan B.W. de Vries" Date: Fri, 25 Nov 2022 15:04:15 +0100 Subject: [PATCH] Introduced type classes, added debugging info for constraint --- TODO.md | 2 + phasm/type3/constraints.py | 96 +++++++++++++++++-- phasm/type3/constraintsgenerator.py | 14 ++- phasm/type3/entry.py | 54 ++++++++++- phasm/type3/types.py | 4 + tests/integration/runners.py | 2 +- .../integration/test_lang/test_primitives.py | 28 +++++- 7 files changed, 188 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index d2a31cc..69f765f 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,8 @@ - Figure out how to do type classes - Implement structs again, with the `.foo` notation working +- Rename constant to literal + - 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. diff --git a/phasm/type3/constraints.py b/phasm/type3/constraints.py index 4acf8c8..4835759 100644 --- a/phasm/type3/constraints.py +++ b/phasm/type3/constraints.py @@ -23,6 +23,8 @@ CheckResult = Union[None, Error, RequireTypeSubstitutes] SubstitutionMap = Dict[types.PlaceholderForType, types.Type3] +HumanReadableRet = Tuple[str, Dict[str, Union[str, ourlang.Expression, types.Type3, types.PlaceholderForType]]] + class Context: """ Context for constraints @@ -56,23 +58,29 @@ class ConstraintBase: """ raise NotImplementedError(self, self.substitute_placeholders) + def human_readable(self) -> HumanReadableRet: + """ + Returns a more human readable form of this constraint + """ + return repr(self), {} + class SameTypeConstraint(ConstraintBase): """ Verifies that an expression has an expected type """ __slots__ = ('expected', 'actual', 'message', ) - expected: types.Type3 + expected: types.Type3OrPlaceholder actual: types.Type3OrPlaceholder message: str - def __init__(self, expected: types.Type3, actual: types.Type3OrPlaceholder, message: str) -> None: + def __init__(self, expected: types.Type3OrPlaceholder, actual: types.Type3OrPlaceholder, message: str) -> None: self.expected = expected self.actual = actual self.message = message def check(self) -> CheckResult: - if isinstance(self.actual, types.PlaceholderForType): + if isinstance(self.expected, types.PlaceholderForType) or isinstance(self.actual, types.PlaceholderForType): return RequireTypeSubstitutes() if self.expected is self.actual: @@ -81,21 +89,88 @@ class SameTypeConstraint(ConstraintBase): return Error(f'{self.expected:s} must be {self.actual:s} instead') def get_new_placeholder_substitutes(self) -> SubstitutionMap: - if isinstance(self.actual, types.PlaceholderForType): - return { + result: SubstitutionMap = {} + + if isinstance(self.expected, types.Type3) and isinstance(self.actual, types.PlaceholderForType): + result = { self.actual: self.expected } - return {} + self.actual.get_substituted(self.expected) + self.actual = self.expected + + + if isinstance(self.actual, types.Type3) and isinstance(self.expected, types.PlaceholderForType): + result = { + self.expected: self.actual + } + + self.expected.get_substituted(self.actual) + self.expected = self.actual + + return result def substitute_placeholders(self, smap: SubstitutionMap) -> None: + if isinstance(self.expected, types.PlaceholderForType) and self.expected in smap: # FIXME: Check recursive? + self.expected.get_substituted(smap[self.expected]) + self.expected = smap[self.expected] + if isinstance(self.actual, types.PlaceholderForType) and self.actual in smap: # FIXME: Check recursive? self.actual.get_substituted(smap[self.actual]) self.actual = smap[self.actual] + def human_readable(self) -> HumanReadableRet: + return ( + '{expected} == {actual}', + { + 'expected': self.expected, + 'actual': self.actual, + 'comment': self.message, + }, + ) + def __repr__(self) -> str: return f'SameTypeConstraint({repr(self.expected)}, {repr(self.actual)}, {repr(self.message)})' +class MustImplementTypeClassConstraint(ConstraintBase): + """ + A type must implement a given type class + """ + __slots__ = ('type_class3', 'type3', ) + + type_class3: str + type3: types.Type3OrPlaceholder + + def __init__(self, type_class3: str, type3: types.Type3OrPlaceholder) -> None: + self.type_class3 = type_class3 + self.type3 = type3 + + def substitute_placeholders(self, smap: SubstitutionMap) -> None: + 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] + + def check(self) -> CheckResult: + if isinstance(self.type3, types.PlaceholderForType): + return RequireTypeSubstitutes() + + if 'BitWiseOr' == self.type_class3 and (self.type3 is types.u8 or self.type3 is types.u32 or self.type3 is types.u64): + return None + + return Error(f'{self.type3.name} does not implement the {self.type_class3} type class') + + def human_readable(self) -> HumanReadableRet: + return ( + '{type3} derives {type_class3}', + { + 'type_class3': self.type_class3, + 'type3': self.type3, + }, + ) + + def __repr__(self) -> str: + return f'MustImplementTypeClassConstraint({repr(self.type_class3)}, {repr(self.type3)})' + class LiteralFitsConstraint(ConstraintBase): """ A literal value fits a given type @@ -159,5 +234,14 @@ class LiteralFitsConstraint(ConstraintBase): self.type3.get_substituted(smap[self.type3]) self.type3 = smap[self.type3] + def human_readable(self) -> HumanReadableRet: + return ( + '{literal} : {type3}', + { + 'literal': self.literal, + 'type3': self.type3, + }, + ) + def __repr__(self) -> str: return f'LiteralFitsConstraint({repr(self.type3)}, {repr(self.literal)})' diff --git a/phasm/type3/constraintsgenerator.py b/phasm/type3/constraintsgenerator.py index 3a06de5..a44c7f2 100644 --- a/phasm/type3/constraintsgenerator.py +++ b/phasm/type3/constraintsgenerator.py @@ -11,7 +11,7 @@ from .constraints import ( Context, ConstraintBase, - LiteralFitsConstraint, SameTypeConstraint, + LiteralFitsConstraint, MustImplementTypeClassConstraint, SameTypeConstraint, ) def phasm_type3_generate_constraints(inp: ourlang.Module) -> List[ConstraintBase]: @@ -35,6 +35,18 @@ def expression(ctx: Context, inp: ourlang.Expression) -> Generator[ConstraintBas yield SameTypeConstraint(inp.variable.type3, inp.type3, f'The type of a variable reference is the same as the type of variable {inp.variable.name}') return + if isinstance(inp, ourlang.BinaryOp): + if '|' == inp.operator: + yield from expression(ctx, inp.left) + yield from expression(ctx, inp.right) + + yield MustImplementTypeClassConstraint('BitWiseOr', inp.left.type3) + yield SameTypeConstraint(inp.right.type3, inp.left.type3, '(|) :: a -> a -> a') + yield SameTypeConstraint(inp.type3, inp.right.type3, '(|) :: a -> a -> a') + return + + raise NotImplementedError(expression, inp) + if isinstance(inp, ourlang.FunctionCall): yield SameTypeConstraint(inp.function.returns_type3, inp.type3, f'The type of a function call to {inp.function.name} is the same as the type that the function returns') diff --git a/phasm/type3/entry.py b/phasm/type3/entry.py index 792fc61..02b4cda 100644 --- a/phasm/type3/entry.py +++ b/phasm/type3/entry.py @@ -3,20 +3,21 @@ Entry point to the type3 system """ from typing import Dict, List +from .. import codestyle from .. import ourlang -from .constraints import Error, RequireTypeSubstitutes +from .constraints import ConstraintBase, Error, RequireTypeSubstitutes, SameTypeConstraint, SubstitutionMap from .constraintsgenerator import phasm_type3_generate_constraints from .types import PlaceholderForType, Type3 -MAX_RESTACK_COUNT = 10 +MAX_RESTACK_COUNT = 100 class Type3Exception(BaseException): """ Thrown when the Type3 system detects constraints that do not hold """ -def phasm_type3(inp: ourlang.Module) -> None: +def phasm_type3(inp: ourlang.Module, verbose: bool = False) -> None: constraint_list = phasm_type3_generate_constraints(inp) assert constraint_list @@ -26,6 +27,10 @@ def phasm_type3(inp: ourlang.Module) -> None: error_list: List[Error] = [] while constraint_list: + if verbose: + print() + print_constraint_list(constraint_list, placeholder_substitutes) + constraint = constraint_list.pop(0) constraint.substitute_placeholders(placeholder_substitutes) @@ -33,10 +38,14 @@ def phasm_type3(inp: ourlang.Module) -> None: check_result = constraint.check() if check_result is None: + if verbose: + print('Constraint checks out') continue if isinstance(check_result, Error): error_list.append(check_result) + if verbose: + print('Got an error') continue if isinstance(check_result, RequireTypeSubstitutes): @@ -46,6 +55,8 @@ def phasm_type3(inp: ourlang.Module) -> None: raise Exception('This looks like an infinite loop', constraint_list) constraint_list.append(constraint) + if verbose: + print('Back on the todo list') continue raise NotImplementedError(constraint, check_result) @@ -54,3 +65,40 @@ def phasm_type3(inp: ourlang.Module) -> None: raise Type3Exception(error_list) # TODO: Implement type substitution on the AST + +def print_constraint(placeholder_id_map: Dict[int, str], constraint: ConstraintBase) -> None: + txt, fmt = constraint.human_readable() + act_fmt: Dict[str, str] = {} + for fmt_key, fmt_val in fmt.items(): + if isinstance(fmt_val, ourlang.Expression): + fmt_val = codestyle.expression(fmt_val) + + if isinstance(fmt_val, Type3): + fmt_val = fmt_val.name + + if isinstance(fmt_val, PlaceholderForType): + placeholder_id = id(fmt_val) + if placeholder_id not in placeholder_id_map: + placeholder_id_map[placeholder_id] = 'T' + str(len(placeholder_id_map) + 1) + fmt_val = placeholder_id_map[placeholder_id] + + if not isinstance(fmt_val, str): + fmt_val = repr(fmt_val) + + act_fmt[fmt_key] = fmt_val + + if 'comment' in act_fmt: + print('- ' + txt.format(**act_fmt).ljust(40) + '; ' + act_fmt['comment']) + else: + print('- ' + txt.format(**act_fmt)) + +def print_constraint_list(constraint_list: List[ConstraintBase], placeholder_substitutes: SubstitutionMap) -> None: + placeholder_id_map: Dict[int, str] = {} + + print('=== v type3 constraint_list v === ') + for psk, psv in placeholder_substitutes.items(): + print_constraint(placeholder_id_map, SameTypeConstraint(psk, psv, 'Deduced type')) + + for constraint in constraint_list: + print_constraint(placeholder_id_map, constraint) + print('=== ^ type3 constraint_list ^ === ') diff --git a/phasm/type3/types.py b/phasm/type3/types.py index 0486d34..ee58220 100644 --- a/phasm/type3/types.py +++ b/phasm/type3/types.py @@ -78,6 +78,10 @@ class PlaceholderForType: self.update_on_substitution = [] + @property + def name(self) -> str: + return f'T{id(self)}' + def __repr__(self) -> str: uos = ', '.join(repr(x) for x in self.update_on_substitution) diff --git a/tests/integration/runners.py b/tests/integration/runners.py index 2f437ae..c87942f 100644 --- a/tests/integration/runners.py +++ b/tests/integration/runners.py @@ -41,7 +41,7 @@ class RunnerBase: Parses the Phasm code into an AST """ self.phasm_ast = phasm_parse(self.phasm_code) - phasm_type3(self.phasm_ast) + phasm_type3(self.phasm_ast, verbose=True) def compile_ast(self) -> None: """ diff --git a/tests/integration/test_lang/test_primitives.py b/tests/integration/test_lang/test_primitives.py index b01acc0..2b7ad3e 100644 --- a/tests/integration/test_lang/test_primitives.py +++ b/tests/integration/test_lang/test_primitives.py @@ -131,7 +131,7 @@ def testEntry() -> u32: @pytest.mark.integration_test @pytest.mark.parametrize('type_', ['u8', 'u32', 'u64']) -def test_bitwise_or(type_): +def test_bitwise_or_uint(type_): code_py = f""" @exported def testEntry() -> {type_}: @@ -143,6 +143,32 @@ def testEntry() -> {type_}: assert 11 == result.returned_value assert TYPE_MAP[type_] == type(result.returned_value) +@pytest.mark.integration_test +def test_bitwise_or_inv_type(): + code_py = """ +@exported +def testEntry() -> f64: + return 10.0 | 3.0 +""" + + with pytest.raises(Type3Exception, match='f64 does not implement the BitWiseOr type class'): + Suite(code_py).run_code() + +@pytest.mark.integration_test +def test_bitwise_or_type_mismatch(): + code_py = """ +CONSTANT1: u32 = 3 +CONSTANT2: u64 = 3 + +@exported +def testEntry() -> u64: + return CONSTANT1 | CONSTANT2 +""" + + 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']) def test_bitwise_xor(type_):