Skip to content

Commit

Permalink
Ensure that listener sees correct name when lexer rule refers to anot…
Browse files Browse the repository at this point in the history
…her lexer rule (#239)

Since token sub-rules (e.g., fragments) are not represented by
unique nodes in the tree, even though listeners are called when the
generation of a sub-rule starts or ends, the listeners only see the
top-level token node and the name of the top-level lexer rule.

This commit changes this by faking the top-level token node to have
the name of the sub-rule rule while generating the sub-rule.

Co-authored-by: Renata Hodovan <[email protected]>
  • Loading branch information
akosthekiss and renatahodovan authored Oct 28, 2024
1 parent 76e8eb5 commit f3c0d14
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 0 deletions.
16 changes: 16 additions & 0 deletions grammarinator/runtime/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,28 @@ class UnlexerRuleContext(RuleContext):

def __init__(self, gen, name, parent=None):
if isinstance(parent, UnlexerRule):
# If parent node is also an UnlexerRule then this is a sub-rule and
# actually no child node is created, but the parent is kept as the
# current node
super().__init__(gen, parent)
self._start_depth = None
# So, save the name of the parent node and also that of the sub-rule
self._parent_name = parent.name
self._name = name
else:
node = UnlexerRule(name=name)
if parent:
parent += node
super().__init__(gen, node)
self._start_depth = self.gen._size.depth
self._parent_name = None
self._name = None

def __enter__(self):
# When entering a sub-rule, rename the current node to reflect the name
# of the sub-rule
if self._name is not None and self._parent_name is not None:
self.node.name = self._name
super().__enter__()
# Increment token count with the current token.
self.gen._size.tokens += 1
Expand All @@ -84,6 +96,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
super().__exit__(exc_type, exc_val, exc_tb)
if self._start_depth is not None:
self.node.size.depth -= self._start_depth
# When exiting a sub-rule, change the name of the current node back to
# that of the parent
if self._name is not None and self._parent_name is not None:
self.node.name = self._parent_name


class UnparserRuleContext(RuleContext):
Expand Down
48 changes: 48 additions & 0 deletions tests/grammars/Fragment.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2024 Renata Hodovan, Akos Kiss.
*
* Licensed under the BSD 3-Clause License
* <LICENSE.rst or https://opensource.org/licenses/BSD-3-Clause>.
* This file may not be copied, modified, or distributed except
* according to those terms.
*/

/*
* This test checks whether listeners see the correct names when lexer rules
* refer to other lexer rules (usually, fragment rules).
*/

// TEST-PROCESS: {grammar}.g4 -o {tmpdir}
// TEST-GENERATE: {grammar}Generator.{grammar}Generator -r start -j 1 -n 1 --listener {grammar}Generator.CustomListener -o {tmpdir}/{grammar}%d.txt

grammar Fragment;

@header {
from collections import Counter
from grammarinator.runtime import Listener
class CustomListener(Listener):
def __init__(self):
self.cnt_enters = Counter()
self.cnt_exits = Counter()
def enter_rule(self, node):
self.cnt_enters[node.name] += 1
def exit_rule(self, node):
self.cnt_exits[node.name] += 1
if node.name == 'start':
assert self.cnt_enters == Counter(start=1, A=1, B=1, C=1, D=1, E=1), self.cnt_enters
assert self.cnt_exits == Counter(start=1, A=1, B=1, C=1, D=1, E=1), self.cnt_exits
}


start : A;
A : 'a' B ;
B : 'b' C ;
fragment C : 'c' D ;
fragment D : 'd' E ;
E : 'e' ;

0 comments on commit f3c0d14

Please sign in to comment.