From ee18205df1fef73dcdac3e4eb955f882564b3b10 Mon Sep 17 00:00:00 2001 From: Amade Nemes Date: Sun, 24 Sep 2023 13:00:58 +0200 Subject: [PATCH] Implement optional reification of ast locations via toggleable option. --- src/renopro/asp/ast.lp | 1 + src/renopro/predicates.py | 22 +++++++++++++++++++--- src/renopro/rast.py | 31 +++++++++++++++++++++++++------ tests/test_reify_reflect.py | 8 ++++++-- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/renopro/asp/ast.lp b/src/renopro/asp/ast.lp index 8e5d2a1..60543ee 100644 --- a/src/renopro/asp/ast.lp +++ b/src/renopro/asp/ast.lp @@ -1,4 +1,5 @@ % tag ast facts. +ast(location(Id,position(F1,B1,E1),position(F2,B2,E2))) :- location(Id,position(F1,B1,E1),position(F2,B2,E2)). ast(string(Id,Val)) :- string(Id,Val). ast(number(Id,Val)) :- number(Id,Val). ast(variable(Id,Name)) :- variable(Id,Name). diff --git a/src/renopro/predicates.py b/src/renopro/predicates.py index 5e84ad4..73a09b5 100644 --- a/src/renopro/predicates.py +++ b/src/renopro/predicates.py @@ -314,9 +314,6 @@ def id_body(ns): kwds={"name": underscore_lower_cls_name}, exec_body=id_body, ) - # The unary and non_unary attributes are created by the metaclass, - # and are only defined in AstPredicate and UnaryAstPredicate - # to supply type hints cls = super().__new__( mcs, cls_name, bases, namespace, name=underscore_lower_cls_name, **kwargs ) @@ -329,6 +326,23 @@ class AstPredicate(Predicate, metaclass=_AstPredicateMeta): """A predicate representing an AST node.""" +class Position(ComplexTerm): + """Complex field representing a position in a text file.""" + + filename = StringField + line = IntegerField + column = IntegerField + + +class Location(Predicate): + """Predicate linking an AST identifier to the range in a text + file from where it was reified.""" + + id = IdentifierField + begin = Position.Field + end = Position.Field + + class String(AstPredicate): """Predicate representing a string term. @@ -1264,6 +1278,7 @@ class TheoryDefinition(AstPredicate): AstPred = Union[ + Location, String, Number, Variable, @@ -1324,6 +1339,7 @@ class TheoryDefinition(AstPredicate): ] AstPreds = [ + Location, String, Number, Variable, diff --git a/src/renopro/rast.py b/src/renopro/rast.py index d9359cf..05a1526 100644 --- a/src/renopro/rast.py +++ b/src/renopro/rast.py @@ -131,13 +131,13 @@ class ReifiedAST: representation of ASP programs. """ - def __init__(self, parse_theory_atoms: bool = False): + def __init__(self, reify_location: bool = False): self._reified = FactBase() self._program_ast: Sequence[AST] = [] self._current_statement: Tuple[int, int] = (0, 0) self._tuple_pos: count = count() self._init_overrides() - self.parse_theory_atoms = parse_theory_atoms + self.reify_location = reify_location def add_reified_facts(self, reified_facts: Iterator[preds.AstPred]) -> None: """Add iterator of reified AST facts to internal factbase.""" @@ -234,7 +234,9 @@ def _init_overrides(self): "node": { # we don't include this node in our reified representation # as it doesn't add much from a knowledge representation standpoint. - ASTType.SymbolicTerm: lambda node: self.reify_node(node.symbol), + ASTType.SymbolicTerm: lambda node: self.reify_node( + node.symbol, location=node.location + ), ASTType.Id: self._reify_id, ASTType.Function: self._reify_function, }, @@ -385,6 +387,15 @@ def _get_type_constructor_from_node( return symbol_type, symbol_constructor raise TypeError(f"Node must be of type AST or Symbol, got: {type(node)}") + def _reify_location(self, id_term, location: ast.Location): + begin = preds.Position( + location.begin.filename, location.begin.line, location.begin.column + ) + end = preds.Position( + location.end.filename, location.end.line, location.end.column + ) + self._reified.add(preds.Location(id_term.id, begin, end)) + def _reify_attr(self, annotation: Type, attr: NodeAttr, field: BaseField): """Reify an AST node's attribute attr based on the type hint for the respective argument in the AST node's constructor. @@ -404,21 +415,26 @@ def _reify_attr(self, annotation: Type, attr: NodeAttr, field: BaseField): return attr raise RuntimeError("Code should be unreachable.") # nocoverage - def reify_node(self, node: Union[AST, Symbol], predicate=None, id_term=None): + def reify_node( + self, node: Union[AST, Symbol], predicate=None, id_term=None, location=None + ): """Reify the input ast node by adding it's clorm fact representation to the internal fact base, and recursively reify child nodes. """ node_type, node_constructor = self._get_type_constructor_from_node(node) - if node_override_func := self._reify_overrides["node"].get(node_type): return node_override_func(node) - annotations = node_constructor.__annotations__ predicate = getattr(preds, node_type.name) if predicate is None else predicate id_term = predicate.unary() if id_term is None else id_term kwargs_dict = {"id": id_term.id} + if self.reify_location: + if location := ( + getattr(node, "location", None) if location is None else location + ): + self._reify_location(id_term, location) for key in predicate.meta.keys(): # the id field has no corresponding attribute in nodes to be reified @@ -443,6 +459,7 @@ def reify_node(self, node: Union[AST, Symbol], predicate=None, id_term=None): field = getattr(predicate, key).meta.field reified_attr = self._reify_attr(annotation, attr, field) kwargs_dict.update({key: reified_attr}) + reified_fact = predicate(**kwargs_dict) self._reified.add(reified_fact) if predicate is preds.Program: @@ -505,6 +522,8 @@ def _reify_function(self, node): arguments=self._reify_ast_sequence(node.arguments, preds.Terms), ) ) + if self.reify_location: + self._reify_location(func1, node.location) return func1 def _reify_id(self, node): diff --git a/tests/test_reify_reflect.py b/tests/test_reify_reflect.py index 4241f03..1f20eff 100644 --- a/tests/test_reify_reflect.py +++ b/tests/test_reify_reflect.py @@ -104,13 +104,13 @@ class TestReifyReflect(TestReifiedAST): base_str = "#program base.\n" - def assertReifyEqual(self, file_name: str): # pylint: disable=invalid-name + def assertReifyEqual(self, file_name: str, reify_location: bool = False): # pylint: disable=invalid-name """Assert that reification of file_name under reflected_files results in file_name under reified_files.""" reified_file = reified_files / file_name reflected_file = reflected_files / file_name reflected_str = reflected_file.read_text().strip() - rast1 = ReifiedAST() + rast1 = ReifiedAST(reify_location=reify_location) rast1.reify_string(reflected_str) reified_facts = rast1.reified_facts rast2 = ReifiedAST() @@ -146,6 +146,10 @@ class TestReifyReflectNormalPrograms(TestReifyReflect): """Test cases for reification and reflection of disjunctive logic program AST nodes.""" + def test_rast_location(self): + "Test reification of AST node locations" + self.assertReifyEqual("location.lp", reify_location=True) + def test_rast_prop_fact(self): "Test reification and reflection of a propositional fact." self.assertReifyReflectEqual("prop_fact.lp")