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

8

介绍

观察文档,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

虽然我可以理解更智能的解析器实现如何解决这些歧义,但我不明白这对于词法分析器会如何运作。

重现示例

为了确定,让我们尝试一下:

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,我注意到它不允许实数以.结尾,后面没有数字,因此从那里学习解决方案似乎不是一个选项。
我看到了ANTLR4中的语义谓词?从Antlr3升级到Antlr4的句法谓词。两者都讨论了解析器规则中的句法谓词。后者还提供了一个带有词法分析器规则的示例,但是向前查看与其后跟随的规则相同,这意味着规则可以被删除而不会产生不良影响。而在我上面的例子中,情况并非如此。
回答在词法分析器中检查先前/左侧标记提到了词法分析器的emit方法,并引用了如何在每个词法分析器规则中发出多个令牌? ANTLR 3 wiki 中的FAQ页面,所以我想那可能是一种方法。如果没有其他人比我更快地做到并且我能在我的示例中使其工作,我将把它变成一个答案。

ANTLR4负向预查在词法分析器中的应用的答案利用了_input.LA(int)方法来检查前瞻。ANTLR 4 词法分析FAQ提到了_input.LA,但没有详细说明。这对上面的示例应该也适用,但对于需要考虑多个字符前瞻的情况会更加困难。

3个回答

4
这里有一个非常简短的解决方案:
@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()调用覆盖状态。如果有疑问,可以使用多个这样的函数或数组索引,使每个匹配都不含糊。


找了好久才弄清楚如何“倒带”词法分析器。终于,谢谢你! - undefined
过早宣布胜利了。实际上在4.13版本上并没有起作用,但我会尝试调试一下。 - undefined
解析器和词法分析器的标记代码不匹配。似乎在较新的版本中,您不能使用您在此处建议的“hack”,而是需要重写方法,并将YourParser.INT/YourParser.RANGE提供给词法分析器的_factory - undefined

3
当前(本文撰写时)Lexer实现的源码包含有关发出多个标记的docstring条目。当然,这些在Lexer API JavaDoc中也有表示。根据这些,您需要执行以下操作:
  1. 重写emit(Token):

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

  2. 重写nextToken()

  3. 重写getToken():

    如果要发出多个标记,请进行重写。

  4. 确保将_token设置为非空:

    如果您子类化以允许多个令牌的发出,则将其设置为与最后匹配的标记或某些非空值相同,以便自动标记发出机制不会发出另一个标记。

然而,我没有看到为什么重写 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 '..' ;

所有的位置计算都感觉非常繁琐,但是我有一个(至少对于这个应用来说更好)的想法,我会在另一个答案中发布。


0
这是另一次尝试实现前瞻。
主要是为了记录和验证。
我有一种类似于XML的语言,但规则不那么严格。
以下是一个例子:
<myTag>a bit of <text </myTag>

应该被分词为:
<               TagOpen
myTag           TagName
>               TagClose
a bit of <text  Text
</              TagOpenClose
myTag           TagName
>               TagClose

注意文本中的<不会干扰。
为了做到这一点,我需要向前查看以验证<是否是有效标签的一部分,然后是有效元素的一部分。

在下面的代码片段中,请注意如何使用isValidTag函数。 我创建了一个新的词法分析器实例,传入相同的_input流,但在方法结束时恢复其位置。

@lexer::members {
  private boolean isValidTag() {
    final int mark = _input.mark();
    final int index = _input.index();
    final Lexer lexer = new MyCustomLexer(_input);
    lexer.mode(TAG);

    Token token = lexer.nextToken();
    boolean isMatch = false;

    if (token.getType() == TagName) {
      token = lexer.nextToken();

      if (token.getType() == TagClose ||
          token.getType() == AttrName) {
        isMatch = true;
      }
    }

    _input.seek(index);
    _input.release(mark);
    return isMatch;
  }
}

TagOpenClose      : '</'                    -> mode(TAG);
TagOpen           : '<' { isValidTag() }?   -> mode(TAG);
Text              : ~[<\n]+;
TextTagOpen       : '<'                     -> more;
NewLine           : ('\r\n' | '\r' | '\n');
...

这似乎还行,虽然我不确定性能和边缘情况。

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