ANTLR 词法分析器规则中的句法谓词



简介

查看文档,ANTLR 2曾经有过一种叫做谓词法的东西,有这样的例子(灵感来自Pascal):

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

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

我还没有在 ANTLR 4 中找到这样的东西。2 到 3 迁移指南似乎没有提到这一点,而 3 到 4 更改文档指出:

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

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

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

虽然我可以理解更智能的解析器实现如何解决这些歧义,但我看不出这将如何适用于词法分析器

重现示例

可以肯定的是,让我们尝试一下:

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 语法,我注意到它不允许实数以 . 结尾而没有数字,因此从那里学习解决方案似乎不是一种选择。

我在ANTLR4中看到了语义谓词?和句法谓词 - 从Antlr 3升级到Antlr 4。两者都讨论了解析器规则中的语法谓词。后者也有一个词法分析器规则的示例,但前瞻与遵循的规则相同,这意味着规则可以被删除而不会产生不利影响。在我上面的例子中,情况并非如此。

在词法分析

器中检查上一个/左标记的答案提到了词法分析器的emit方法,并附有注释引用了如何为每个词法分析器规则发出多个令牌?ANTLR 3 维基中的常见问题解答页面,所以我想这是一种方法。如果没有人打败我,如果我能让它在我的例子中发挥作用,我会把它变成一个答案。

词法分析器中ANTLR4负前瞻的答案是利用_input.LA(int)方法来检查前瞻。ANTLR 4 词法分析常见问题解答提到了_input.LA,但没有详细说明。这也适用于上面的示例,但对于有多个前瞻字符需要考虑的情况,则很难。

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

@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()调用。如有疑问,可以使用多个此类函数或数组索引,以使每个匹配项明确无误。

当前(截至撰写本文时)Lexer实现的来源包含多个有关多个令牌的分发的文档字符串条目。当然,这些在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 '..' ;

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

相关内容

  • 没有找到相关文章

最新更新