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

MvG*_*MvG 5 antlr lexical-analysis antlr4 regex-lookarounds

介绍

查看文档,ANTLR 2曾经有一个称为谓词词法的东西,例如这样的例子(受Pascal启发):

RANGE_OR_INT
    :   ( INT ".." ) => INT  { $setType(INT); }
    |   ( INT '.' )  => REAL { $setType(REAL); }
    |   INT                  { $setType(INT); }
    ;    
Run Code Online (Sandbox Code Playgroud)

我的看法是,在规则开始处本质上是一个肯定的前瞻性断言:如果前瞻性匹配INT "..",则将应用第一个规则(并匹配该INT输入的部分),依此类推。

我尚未在ANTLR 4中找到类似的东西。在2至3的迁移指南似乎不同时提此,3〜4的变化记录状态:

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

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

(...)=> syntactic predicates are not supported in ANTLR 4
Run Code Online (Sandbox Code Playgroud)

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

复制示例

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

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:  '..' ;
Run Code Online (Sandbox Code Playgroud)

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

RANGE_OR_INT
    :   ( INT ".." ) => INT  { $setType(INT); }
    |   ( INT '.' )  => REAL { $setType(REAL); }
    |   INT                  { $setType(INT); }
    ;    
Run Code Online (Sandbox Code Playgroud)

因此,看来我是正确的:虽然语法分析谓词的删除可能适用于解析器,但词法分析器不会突然猜测正确的标记类型。

核心问题

那么如何将这一特定示例转换为ANTLR 4?有没有一种表达提前条件的方法?还是有办法让一个规则像INT '..'发出两个不同的标记?

参考和可能的解决方案

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

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

检查词法分析器中上一个/左标记的答案提到了词法分析器emit方法,并附带注释:“ 如何根据每个词法分析器规则发射多个标记?ANTLR 3 Wiki中的FAQ页面,所以我想这是一种方法。如果没有人击败我,并且在我的示例中可以使它起作用,我将把它变成答案。

在lexer中ANTLR4否定前瞻的答案使用该_input.LA(int)方法来检查前瞻。ANTLR 4 词法分析常见问题未提及_input.LA而提及。这也适用于上面的示例,但是对于要考虑的不只是单个预告字符的情况将很困难。

MvG*_*MvG 3

当前(截至撰写本文时)Lexer实现的源代码包含多个有关多个令牌发射的文档字符串条目。当然,这些也体现在API LexerJavaDoc中。根据这些,人们必须做以下事情:

  1. 覆盖emit(Token)

    nextToken出于效率原因,默认情况下不支持每次调用多次发出。子类化并重写此方法、nextTokengetToken(将标记推入列表并从该列表中提取,而不是像此实现那样使用单个变量)。

  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 '..' ;
Run Code Online (Sandbox Code Playgroud)

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