diff --git a/il65/compile.py b/il65/compile.py index d360008f6..9803c57f2 100644 --- a/il65/compile.py +++ b/il65/compile.py @@ -31,6 +31,7 @@ class PlyParser: try: module = parse_file(filename, self.lexer_error) self.check_directives(module) + module.scope.define_builtin_functions() self.process_imports(module) self.check_all_symbolnames(module) self.create_multiassigns(module) @@ -431,7 +432,6 @@ class PlyParser: if not filename: raise ParseError("imported file not found", directive.sourceref) imported_module, import_parse_errors = self.import_file(filename) - imported_module.scope.parent_scope = module.scope imported.append(imported_module) self.parse_errors += import_parse_errors if not self.imported_module: @@ -439,7 +439,6 @@ class PlyParser: filename = self.find_import_file("il65lib", module.sourceref.file) if filename: imported_module, import_parse_errors = self.import_file(filename) - imported_module.scope.parent_scope = module.scope imported.append(imported_module) self.parse_errors += import_parse_errors else: diff --git a/il65/plyparse.py b/il65/plyparse.py index d8a71e86c..dbd58adc7 100644 --- a/il65/plyparse.py +++ b/il65/plyparse.py @@ -30,7 +30,7 @@ class ZpOptions(enum.Enum): CLOBBER_RESTORE = "clobber_restore" -math_functions = {name: func for name, func in vars(math).items() if inspect.isbuiltin(func)} +math_functions = {name: func for name, func in vars(math).items() if inspect.isbuiltin(func) and name != "pow"} builtin_functions = {name: func for name, func in vars(builtins).items() if inspect.isbuiltin(func)} @@ -121,7 +121,6 @@ class Scope(AstNode): nodes = attr.ib(type=list, init=True) # requires nodes in __init__ symbols = attr.ib(init=False) name = attr.ib(init=False) # will be set by enclosing block, or subroutine etc. - parent_scope = attr.ib(init=False, default=None) # will be wired up later _save_registers = attr.ib(type=bool, default=None, init=False) @property @@ -137,6 +136,13 @@ class Scope(AstNode): def save_registers(self, save: bool) -> None: self._save_registers = save + @property + def parent_scope(self) -> Optional['Scope']: + parent_scope = self.parent + while parent_scope and not isinstance(parent_scope, Scope): + parent_scope = parent_scope.parent + return parent_scope + def __attrs_post_init__(self): # populate the symbol table for this scope for fast lookups via scope.lookup("name") or scope.lookup("dotted.name") self.symbols = {} @@ -147,28 +153,38 @@ class Scope(AstNode): def _populate_symboltable(self, node: AstNode) -> None: if isinstance(node, (Label, VarDef)): if node.name in self.symbols: - raise ParseError("symbol already defined at {}".format(self.symbols[node.name].sourceref), node.sourceref) + raise ParseError("symbol '{}' already defined at {}".format(node.name, self.symbols[node.name].sourceref), node.sourceref) self.symbols[node.name] = node - if isinstance(node, Subroutine): + elif isinstance(node, (Subroutine, BuiltinFunction)): if node.name in self.symbols: - raise ParseError("symbol already defined at {}".format(self.symbols[node.name].sourceref), node.sourceref) + raise ParseError("symbol '{}' already defined at {}".format(node.name, self.symbols[node.name].sourceref), node.sourceref) self.symbols[node.name] = node - if node.scope: - node.scope.parent_scope = self - if isinstance(node, Block): + elif isinstance(node, Block): if node.name: if node.name != "ZP" and node.name in self.symbols: - raise ParseError("symbol already defined at {}".format(self.symbols[node.name].sourceref), node.sourceref) + raise ParseError("symbol '{}' already defined at {}" + .format(node.name, self.symbols[node.name].sourceref), node.sourceref) self.symbols[node.name] = node - node.scope.parent_scope = self + + @no_type_check + def define_builtin_functions(self) -> None: + for name, func in math_functions.items(): + f = BuiltinFunction(name=name, func=func, sourceref=self.sourceref) + self.add_node(f) + for name, func in builtin_functions.items(): + f = BuiltinFunction(name=name, func=func, sourceref=self.sourceref) + self.add_node(f) def lookup(self, name: str) -> AstNode: assert isinstance(name, str) if '.' in name: # look up the dotted name starting from the topmost scope scope = self - while scope.parent_scope: - scope = scope.parent_scope + node = self + while node.parent: + if isinstance(node.parent, Scope): + scope = node.parent + node = node.parent for namepart in name.split('.'): if isinstance(scope, (Block, Subroutine)): scope = scope.scope @@ -182,8 +198,11 @@ class Scope(AstNode): # find the name in nested scope hierarchy if name in self.symbols: return self.symbols[name] - if self.parent_scope: - return self.parent_scope.lookup(name) + parent_scope = self.parent + while parent_scope and not isinstance(parent_scope, Scope): + parent_scope = parent_scope.parent + if parent_scope: + return parent_scope.lookup(name) raise UndefinedSymbolError("undefined symbol: " + name) def remove_node(self, node: AstNode) -> None: @@ -387,6 +406,15 @@ class DatatypeNode(AstNode): }[self.name] +@attr.s(cmp=False, repr=False) +class BuiltinFunction(AstNode): + # This is a pseudo-node that will be artificially injected in the top-most scope, + # to represent all supported built-in functions or math-functions. + # No child nodes. + name = attr.ib(type=str) + func = attr.ib(type=callable) + + @attr.s(cmp=False, repr=False) class Subroutine(AstNode): # one subnode: the Scope. diff --git a/tests/test_parser.py b/tests/test_parser.py index fcb6aa2db..bb7446b44 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,8 +1,10 @@ +import math import pytest from il65.plylex import lexer, tokens, find_tok_column, literals, reserved, SourceRef from il65.plyparse import parser, connect_parents, TokenFilter, Module, Subroutine, Block, IncrDecr, Scope, \ - VarDef, Register, ExpressionWithOperator, LiteralValue, Label, SubCall, Dereference -from il65.datatypes import DataType + VarDef, Register, ExpressionWithOperator, LiteralValue, Label, SubCall, Dereference,\ + BuiltinFunction, UndefinedSymbolError +from il65.datatypes import DataType, VarType def lexer_error(sourceref: SourceRef, fmtstring: str, *args: str) -> None: @@ -296,3 +298,58 @@ def test_incrdecr(): IncrDecr(operator="??", sourceref=sref) i = IncrDecr(operator="++", sourceref=sref) assert i.howmuch == 1 + + +def test_symbol_lookup(): + sref = SourceRef("test", 1, 1) + var1 = VarDef(name="var1", vartype="const", datatype=DataType.WORD, sourceref=sref) + var1.value = LiteralValue(value=42, sourceref=sref) + var1.value.parent = var1 + var2 = VarDef(name="var2", vartype="const", datatype=DataType.FLOAT, sourceref=sref) + var2.value = LiteralValue(value=123.456, sourceref=sref) + var2.value.parent = var2 + label1 = Label(name="outerlabel", sourceref=sref) + label2 = Label(name="innerlabel", sourceref=sref) + scope_inner = Scope(nodes=[ + label2, + var2 + ], level="block", sourceref=sref) + scope_inner.name = "inner" + var2.parent = label2.parent = scope_inner + scope_outer = Scope(nodes=[ + label1, + var1, + scope_inner + ], level="block", sourceref=sref) + scope_outer.name = "outer" + scope_outer.define_builtin_functions() + var1.parent = label1.parent = scope_inner.parent = scope_outer + assert scope_inner.parent_scope is scope_outer + assert scope_outer.parent_scope is None + assert label1.my_scope() is scope_outer + assert var1.my_scope() is scope_outer + assert scope_inner.my_scope() is scope_outer + assert label2.my_scope() is scope_inner + assert var2.my_scope() is scope_inner + with pytest.raises(LookupError): + scope_outer.my_scope() + with pytest.raises(UndefinedSymbolError): + scope_inner.lookup("unexisting") + with pytest.raises(UndefinedSymbolError): + scope_outer.lookup("unexisting") + assert scope_inner.lookup("innerlabel") is label2 + assert scope_inner.lookup("var2") is var2 + assert scope_inner.lookup("outerlabel") is label1 + assert scope_inner.lookup("var1") is var1 + with pytest.raises(UndefinedSymbolError): + scope_outer.lookup("innerlabel") + with pytest.raises(UndefinedSymbolError): + scope_outer.lookup("var2") + assert scope_outer.lookup("var1") is var1 + assert scope_outer.lookup("outerlabel") is label1 + math_func = scope_inner.lookup("sin") + assert isinstance(math_func, BuiltinFunction) + assert math_func.name == "sin" and math_func.func is math.sin + builtin_func = scope_inner.lookup("max") + assert isinstance(builtin_func, BuiltinFunction) + assert builtin_func.name == "max" and builtin_func.func is max