【问题标题】:Syntactic predicates in ANTLR lexer rulesANTLR 词法分析器规则中的句法谓词
【发布时间】:2016-03-01 13:20:26
【问题描述】:

简介

查看文档,ANTLR 2 曾经有一个叫做 predicated lexing 的东西,有这样的例子(受 Pascal 启发):

RANGE_OR_INT
    :   ( INT ".." ) => INT  { $setType(INT); }
    |   ( INT '.' )  => REAL { $setType(REAL); }
    |   INT                  { $setType(INT); }
    ;    

在我看来,这本质上是规则开头的积极前瞻断言:如果前瞻与 INT ".." 匹配,则将应用第一条规则(并匹配该输入的 INT 部分) 等等。

我还没有在 ANTLR 4 中找到类似的东西。 2 to 3 migration guide 似乎没有提到这一点,而 3 to 4 changes document 声明:

ANTLR 3 和 4 之间最大的区别是 ANTLR 4 接受你给它的任何语法,除非语法有间接左递归。这意味着我们不需要语法谓词或回溯,因此 ANTLR 4 不支持该语法;使用时会收到警告。

如果我基本上保持原样,这与我收到的错误消息一致:

(...)=> syntactic predicates are not supported in ANTLR 4

虽然我可以理解更智能的 解析器 实现如何解决这些歧义,但我看不出这对 lexers 是如何工作的。

重现示例

为了确定,让我们试试这个:

grammar Demo;
prog:   atom (',' atom)* ;
atom:   INT  { System.out.println("INT:   " + $INT.getText()); }
    |   REAL { System.out.println("REAL:  " + $REAL.getText()); }
    |   a=INT RANGE b=INT { System.out.println("RANGE: " +
                              $a.getText() + " .. " + $b.getText()); }
    ;
WS  :   (' ' | '\t' | '\n' | '\r')+ -> skip ;
INT :   ('0'..'9')+ ;
REAL:   INT '.' INT? | '.' INT ;
RANGE:  '..' ;

保存到Demo.g,然后编译运行:

$ wget -nc http://www.antlr.org/download/antlr-4.5.2-complete.jar
$ java -jar antlr-4.5.2-complete.jar Demo.g
$ javac -cp antlr-4.5.2-complete.jar Demo*.java
$ java -cp .:antlr-4.5.2-complete.jar org.antlr.v4.gui.TestRig \
  Demo prog <<< '1,2.,3.4,5 ..6,7..8'
INT:   1
REAL:  2.
REAL:  3.4
RANGE: 5 .. 6
REAL:  7.
line 1:17 extraneous input '.8' expecting {<EOF>, ','}

看来我是对的:虽然删除句法前置词可能适合解析器,但词法分析器不会突然猜出正确的标记类型。

核心问题

那么如何将这个特定示例转换为 ANTLR 4? 有没有一种方法可以表达前瞻条件?或者也许有一种方法可以让像INT '..' 这样的单一规则发出两个不同的令牌?

参考和可能的解决方案

查看 ANTLR 4 Pascal grammar,我注意到它不允许实数以 . 结尾而没有数字,因此从那里学习解决方案似乎不是一个选择。

我见过Semantic predicates in ANTLR4?syntactic predicates - Upgrading from Antlr 3 to Antlr 4。两者都讨论了解析器规则中的句法谓词。后者也有一个带有词法分析器规则的示例,但前瞻与遵循它的规则相同,这意味着可以删除规则而不会产生不利影响。在我上面的示例中不是这种情况。

check previous/left token in lexer 的回答提到了词法分析器的emit 方法,并附有引用ANTLR 3 wiki 中How can I emit more than a single token per lexer rule? 常见问题解答页面的评论,所以我想这是一种方法。如果没有人超过我并且我可以让它在我的示例中工作,我会将它变成一个答案。

ANTLR4 negative lookahead in lexer 的答案使用_input.LA(int) 方法来检查前瞻。 ANTLR 4 lexical analysis faq 提到了_input.LA,但没有详细说明。这也应该适用于上面的示例,但对于需要考虑多个前瞻字符的场景来说会很困难。

【问题讨论】:

    标签: antlr antlr4 regex-lookarounds lexical-analysis


    【解决方案1】:

    这是一个非常简短的解决方案:

    @lexer::members { private int _pos; }
    INT_RANGE: INT  { _pos=_input.index(); setType(INT); emit(); }
               '..' { _input.seek(_pos); };
    

    这匹配整个INT '..' 表达式,但随后将输入倒回到INT 之后我们发出令牌并保存位置的位置。然后在规则的末尾使用该位置以更永久的方式回退输入。

    但是,有一个问题:生成的标记将包含不正确的位置信息,因为 _input.seek 不会影响 getCharPositionInLine 返回的内容。在这种情况下可以做

    setCharPositionInLine(getCharPositionInLine() - 2)
    

    在规则的末尾,但如果不是.. 处理可变长度的输入,则该方法将不起作用。我曾希望我能够在第一个动作中保存getCharPositionInLine() 的结果,但不幸的是,这已经反映了整个表达式的结束。

    看着LexerATNSimulator.evaluatePredicate,我看到这个方法努力恢复给定的位置状态。所以我们可以通过滥用语义谓词来获得正确的状态:

    @lexer::members {
        private int _savedIndex, _savedLine, _savedColumn;
        private boolean remember() {
            _savedIndex = _input.index();
            _savedLine = getLine();
            _savedColumn = getCharPositionInLine();
            return true;
        }
        private void recall(int type) {
            _input.seek(_savedIndex);
            setLine(_savedLine);
            setCharPositionInLine(_savedColumn);
            setType(type);
        }
    }
    INT_RANGE: INT { remember() }? '..' { recall(INT); } ;
    

    请记住,语义谓词将在尚未保证整个表达式实际匹配的时间点执行。因此,如果您在多个地方使用此技巧,则必须小心不要让来自不同规则的remember() 调用覆盖状态。如果有疑问,您可以使用多个此类函数或数组索引,以使每个匹配项明确。

    【讨论】:

      【解决方案2】:

      sources of the current (as of this writing) Lexer implementation 包含多个关于发射多个令牌的文档字符串条目。这些当然也出现在the Lexer API JavaDoc 中。根据这些,必须执行以下操作:

      1. 覆盖emit(Token)

        默认不支持每次nextToken 调用多次发射 出于效率原因。子类化并覆盖此方法,nextToken, 和getToken(将令牌​​推入列表并从该列表中提取 而不是像此实现那样的单个变量)。

      2. 覆盖nextToken()

      3. 覆盖getToken()

        如果发出多个令牌,则覆盖。

      4. 确保将_token 设置为非null

        如果你的子类允许多个令牌 排放,然后将其设置为要匹配的最后一个标记或 一些非空的东西,以便自动令牌发出机制不会 发出另一个令牌。

      但是,我不明白为什么覆盖 getToken 很重要,因为我在运行时库中的任何地方都没有看到对该方法的调用。如果你设置了_token,那么这也是getToken的输出。

      所以我从一个规则中发出两个令牌是这样的:

      @lexer::members {
      
          private Token _queued;
      
          @Override public Token nextToken() {
              if (_queued != null) {
                  emit(_queued);
                  _queued = null;
                  return getToken();
              }
              return super.nextToken();
          }
      
          @Override public Token emit() {
              if (_type != INT_RANGE)
                  return super.emit();
              Token t = _factory.create(
                  _tokenFactorySourcePair, INT, null, _channel,
                  _tokenStartCharIndex, getCharIndex()-3,
                  _tokenStartLine, _tokenStartCharPositionInLine);
              _queued = _factory.create(
                  _tokenFactorySourcePair, RANGE, null, _channel,
                  getCharIndex()-2, getCharIndex()-1, _tokenStartLine,
                  _tokenStartCharPositionInLine + getCharIndex()-2 -
                  _tokenStartCharIndex);
              emit(t);
              return t;
          }
      }
      
      INT_RANGE: INT '..' ;
      

      然而,所有的位置计算都感觉相当乏味,并且给了我另一个(至少对于这个应用程序来说更好)的想法,我将在单独的答案中发布。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-06-29
        • 1970-01-01
        • 2012-03-04
        • 2021-11-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多