简介
查看文档,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中也有体现。根据这些,必须执行以下操作:
-
覆盖
emit(Token)
:默认情况下不支持每次
nextToken
调用多次发出 出于效率原因。 子类并重写此方法,nextToken
, 和getToken
(将令牌推送到列表中并从该列表中提取 而不是像此实现那样的单个变量)。 -
覆盖
nextToken()
。 -
覆盖
getToken()
:如果发出多个令牌,则覆盖。
-
确保将
_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 '..' ;
然而,所有的位置计算都感觉很乏味,并给了我另一个(至少对于这个应用程序更好)的想法,我将在矛头答案中发布。