如何在ANTLR3树解析器中的@init操作中获取行号

3
在ANTLR 3中,如何在高级树解析器规则的@init操作中获取行号?例如,在下面的@init操作中,我想要将行号与句子文本一起推送。
sentence
    @init { myNodeVisitor.pushScriptContext( new MyScriptContext( $sentence.text )); }
    : assignCommand 
    | actionCommand;
    finally {
        m_nodeVisitor.popScriptContext();
    }

我需要在规则中与符号相关的操作执行之前推送上下文。以下方法不起作用:
- 使用`$sentence.line` —— 它未定义,即使有`$sentence.text`。 - 将释义推入规则操作中。放置在规则之前,规则中没有可用令牌。放置在规则之后,该操作会在与规则符号相关的操作之后发生。 - 在@init操作中使用此表达式,它可以编译但返回值为0:getTreeNodeStream().getTreeAdaptor().getToken( $sentence.start ).getLine()。实际上,如果$sentence.start是真实令牌或具有引用的虚拟令牌,则此表达式确实有效。
似乎如果我可以在@init规则中轻松获取匹配文本和第一个匹配令牌,那么也应该有一种简单的方法来获取行号。
1个回答

7
你可以使用以下代码在树语法的令牌/树流中向前查看 1 步: CommonTree ahead = (CommonTree)input.LT(1),你可以将其放置在@init部分。每个CommonTree(ANTLR 中默认的Tree实现)都具有一个getToken()方法,该方法返回与此树关联的Token。每个Token都有一个getLine()方法,该方法返回此标记的行号。因此,如果执行以下操作:
sentence
@init {
  CommonTree ahead = (CommonTree)input.LT(1);
  int line = ahead.getToken().getLine();
  System.out.println("line=" + line);
}
  :  assignCommand 
  |  actionCommand
  ;

你应该能够看到一些正确的行号被打印出来。我说“一些”,因为这不会在所有情况下都按计划进行。让我用一个简单的示例语法来演示:

grammar ASTDemo;

options { 
  output=AST;
}

tokens {
  ROOT;
  ACTION;
}

parse
  :  sentence+ EOF -> ^(ROOT sentence+)
  ;

sentence
  :  assignCommand 
  |  actionCommand
  ;

assignCommand
  :  ID ASSIGN NUMBER -> ^(ASSIGN ID NUMBER)
  ;

actionCommand
  :  action ID -> ^(ACTION action ID)
  ;

action
  :  START
  |  STOP
  ;

ASSIGN : '=';
START  : 'start';
STOP   : 'stop';
ID     : ('a'..'z' | 'A'..'Z')+;
NUMBER : '0'..'9'+;
SPACE  : (' ' | '\t' | '\r' | '\n')+ {skip();};

其树形语法如下:

tree grammar ASTDemoWalker;

options {
  output=AST;
  tokenVocab=ASTDemo;
  ASTLabelType=CommonTree;
}


walk
  :  ^(ROOT sentence+)
  ;

sentence
@init {
  CommonTree ahead = (CommonTree)input.LT(1);
  int line = ahead.getToken().getLine();
  System.out.println("line=" + line);
}
  :  assignCommand 
  |  actionCommand
  ;

assignCommand
  :  ^(ASSIGN ID NUMBER)
  ;

actionCommand
  :  ^(ACTION action ID)
  ;

action
  :  START
  |  STOP
  ;

如果您运行以下测试类:

import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;

public class Main {
  public static void main(String[] args) throws Exception {
    String src = "\n\n\nABC = 123\n\nstart ABC";
    ASTDemoLexer lexer = new ASTDemoLexer(new ANTLRStringStream(src));
    ASTDemoParser parser = new ASTDemoParser(new CommonTokenStream(lexer));
    CommonTree root = (CommonTree)parser.parse().getTree();
    ASTDemoWalker walker = new ASTDemoWalker(new CommonTreeNodeStream(root));
    walker.walk();
  }
}

您将看到打印以下内容:
line=4
line=0

你可以看到,"ABC = 123"产生了预期的输出(第4行),但"start ABC"没有(第0行)。这是因为action规则的根是一个ACTION标记,而此标记在词法分析器中从未定义,只在tokens{...}块中定义。并且由于它在输入中实际上不存在,所以默认情况下将其附加到第0行。如果你想改变行号,你需要向这个所谓的虚拟ACTION标记提供一个“参考”标记作为参数,它会使用该标记将属性复制到自身。
因此,如果你将合并语法中的actionCommand规则更改为:
actionCommand
  :  ref=action ID -> ^(ACTION[$ref.start] action ID)
  ;

行号应该是预期的(第6行)。

请注意,每个解析规则都有一个startend属性,分别是第一个和最后一个标记的引用。如果action是词法分析规则(比如FOO),则可以省略其中的.start

actionCommand
  :  ref=FOO ID -> ^(ACTION[$ref] action ID)
  ;

现在,ACTION令牌已经从其指向的任何$ref中复制了所有属性,除了令牌类型,当然是int ACTION。但这也意味着它复制了text属性,因此在我的示例中,通过ref=action ID -> ^(ACTION[$ref.start] action ID)创建的AST可能如下所示:
            [text=START,type=ACTION]
                  /         \
                 /           \
                /             \
   [text=START,type=START]  [text=ABC,type=ID]

当然,这是一个适当的AST,因为节点类型是唯一的,但它使得调试混乱,因为ACTION和START共享相同的.text属性。您可以将所有属性复制到一个“虚拟”的标记中,除了.text和.type,方法是提供第二个字符串参数,例如:
actionCommand
  :  ref=action ID -> ^(ACTION[$ref.start, "Action"] action ID)
  ;

如果您现在再次运行相同的测试类,将会看到以下内容:

line=4
line=6

如果您检查生成的树,它将是这样的:

[type=ROOT, text='ROOT']
  [type=ASSIGN, text='=']
    [type=ID, text='ABC']
    [type=NUMBER, text='123']
  [type=ACTION, text='Action']
    [type=START, text='start']
    [type=ID, text='ABC']

在我的情况下,我有没有参考标记的虚构标记。(而这很愚蠢,因为我在其他地方添加了参考标记。)有了参考标记,您的解决方案和我的bullet with $sentence.start都可以工作--它们只是获取起始标记的一种方式。顺便说一句,这个答案是我在SO上看到的最好的答案之一。它写得很好,有帮助性并且完整。你太棒了,先生。 - Andy Thomas
@AndyThomas-Cramer,谢谢您的赞美之词,我很感激。 - Bart Kiers
我后来才注意到,通过 getTreeNodeStream().getTreeAdaptor().getToken( $sentence.start ).getLine(),你做的与我差不多! :) - Bart Kiers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接