diff --git a/toy-impl/README.md b/toy-impl/README.md index 43cfec1..f9fea16 100644 --- a/toy-impl/README.md +++ b/toy-impl/README.md @@ -21,7 +21,6 @@ for simplicity reasons: * method override check modulo variable renaming. These features are currently not implemented, and would be added in due course: -* Whole script check. * Type guards. diff --git a/toy-impl/examples/a.py b/toy-impl/examples/a.py new file mode 100644 index 0000000..3ee1510 --- /dev/null +++ b/toy-impl/examples/a.py @@ -0,0 +1,14 @@ +I = TypeVar('I', int32, int64) + +class Vec: + v: list[int32] + def __init__(self, v: list[int32]): + self.v = v + + def __add__(self, other: int32) -> Vec: + return Vec([v + other for v in self.v]) + + +def addI(a: I, b: I) -> I: + return a + b + diff --git a/toy-impl/helper.py b/toy-impl/helper.py index 14020a6..88ba4e3 100644 --- a/toy-impl/helper.py +++ b/toy-impl/helper.py @@ -1,5 +1,8 @@ import ast +def quote_text(text): + return '\n'.join([f' {t}' for t in text.split('\n')]) + class CustomError(Exception): def __init__(self, msg, node = None): self.msg = msg @@ -9,7 +12,7 @@ class CustomError(Exception): def at(self, node): self.msg = f'Error at {node.lineno}:{node.col_offset+1}-' \ f'{node.end_lineno}:{node.end_col_offset+1} ' \ - f'"{ast.unparse(node)}"\n{self.msg}' + f'\n{quote_text(ast.unparse(node))}\n{self.msg}' return self def stringify_subst(subst): diff --git a/toy-impl/main.py b/toy-impl/main.py new file mode 100644 index 0000000..1662f61 --- /dev/null +++ b/toy-impl/main.py @@ -0,0 +1,41 @@ +import ast +import sys +from helper import CustomError +from type_def import SelfType +from parse_stmt import parse_stmts +from primitives import simplest_ctx +from top_level import parse_top_level + +if len(sys.argv) != 2: + print('please pass the python script name as argument') + exit() + +with open(sys.argv[1], 'r') as f: + source = f.read() + +tree = ast.parse(source, filename=sys.argv[1]) + +try: + ctx, fns = parse_top_level(simplest_ctx, tree) + for c, name, fn in fns: + if c is None: + params, result, _ = ctx.functions[name] + else: + params, result, _ = ctx.types[c].methods[name] + # check if fully annotated all params + sym_table = {} + for n, ty in zip(fn.args.args, params): + if ty is None: + raise CustomError( + 'Function parameters must be annotated', + fn) + if isinstance(ty, SelfType): + ty = ctx.types[c] + sym_table[n.arg] = ty + _, _, returned = parse_stmts(ctx, sym_table, sym_table, result, fn.body) + if result is not None and not returned: + raise CustomError('Function may have no return value', fn) +except CustomError as e: + print('Error while type checking:') + print(e.msg) + diff --git a/toy-impl/parse_expr.py b/toy-impl/parse_expr.py index 23251db..a4979b0 100644 --- a/toy-impl/parse_expr.py +++ b/toy-impl/parse_expr.py @@ -106,7 +106,7 @@ def parse_tuple(ctx: Context, def parse_attribute(ctx: Context, sym_table: dict[str, Type], node): - obj = parse_expr(node.value) + obj = parse_expr(ctx, sym_table, node.value) if node.attr in obj.fields: return obj.fields[node.attr] raise CustomError(f'unknown field {node.attr} in {obj}', node) diff --git a/toy-impl/top_level.py b/toy-impl/top_level.py index c6dd59a..33ffa2c 100644 --- a/toy-impl/top_level.py +++ b/toy-impl/top_level.py @@ -105,8 +105,44 @@ def parse_class(ctx, c: ast.ClassDef): return functions +def parse_type_var(ctx: Context, node): + if isinstance(node, ast.Assign): + if not isinstance(node.value, ast.Call) or\ + not isinstance(node.value.func, ast.Name) or\ + node.value.func.id != 'TypeVar': + return + if not len(node.targets) == 1 or\ + not isinstance(node.targets[0], ast.Name): + raise CustomError( + 'complicated variable assignment is not allowed', + node) + if len(node.value.args) == 0 or\ + not isinstance(node.value.args[0], ast.Constant) or\ + not isinstance(node.value.args[0].value, str): + raise CustomError('call to TypeVar must at least have a name', + node.value) + name = node.value.args[0] + if name in ctx.variables: + raise CustomError('redefining type variable is not allowed', node) + constraints = [] + for v in node.value.args[1:]: + if isinstance(v, ast.Constant): + value = v.value + elif isinstance(v, ast.Name): + value = v.id + else: + raise CustomError( + 'TypeVar constraints must be either string or type name', + node) + if value not in ctx.types: + raise CustomError(f'unbounded type {value}', node) + constraints.append(ctx.types[value]) + ctx.variables[node.targets[0].id] = TypeVariable(name, constraints) + + def parse_top_level(ctx: Context, module: ast.Module): to_be_processed = [] + assignments = [] # first pass, obtain all type names for element in module.body: if isinstance(element, ast.ClassDef): @@ -117,8 +153,14 @@ def parse_top_level(ctx: Context, module: ast.Module): to_be_processed.append(element) elif isinstance(element, ast.FunctionDef): to_be_processed.append(element) + elif isinstance(element, ast.Assign): + assignments.append(element) - # second pass, obtain all function types + # second pass, obtain all type variables + for node in assignments: + parse_type_var(ctx, node) + + # third pass, obtain all function types function_stmts = [] for element in to_be_processed: if isinstance(element, ast.ClassDef): @@ -131,7 +173,7 @@ def parse_top_level(ctx: Context, module: ast.Module): raise CustomError(f"Function name {name} clashed with type name") args, result, var = parse_function(ctx, None, element) ctx.functions[name] = (args, result, var) - function_stmts.append(element) + function_stmts.append((None, name, element)) return ctx, function_stmts