用antlr解析简单的语法很简单 。 您要做的就是使用正则表达式描述您的语言,并让antlr生成词法分析器和解析器。 解析大型或复杂的语言有时会需要更多,因为仅使用正则表达式描述它们是困难的,甚至是不可能的。
语义谓词是在语法内部编写的Java(或C ++,JavaScript或…)条件。 Antlr使用它们在多个替代方案之间进行选择,或者作为要检查的其他声明。 它们可以放在lexer和解析器中,但是本文仅着重于它们在解析器中的用法。 它们为antlr增加了很多功能。
这篇文章假定您对什么是antlr以及生成的解析器如何工作有一般的了解。 如果您不这样做,请先阅读链接的文章。 它们包含所需的一切。
第一章包含两个动机用例。 第二章介绍语法,术语,并显示简单的失败的语义谓词。 然后,该帖子解释了语义谓词如何影响预测和生成的代码。 它还显示了如何编写有用的条件以及如何解决初始用例。 最后一章将其总结为简短的结论。
这篇文章中使用的所有示例和语法都可以在Github上找到 。
目录
- 用例
- 关键字–第n个
- 基本
- 句法
- 吊装和预测
- 这是什么
- 回溯
- 写作条件
- 输入令牌流
- 解决初始用例
- 关键字–第n个
- 包起来
- 验证语义谓词
- 资源资源
用例
当我们花一些时间解析类css的语言时 ,两个用例都描述了在编写该解析器的css部分时必须解决的问题。 第一个是在处理伪类时遇到的问题,第二个是在选择器中处理棘手的空格。
如果您从未使用过CSS或对用例不感兴趣,请跳过本章。
关键字–第n个
一些css 伪 类需要参数,该参数可以是数字,标识符,选择器或称为nth表达式的参数。 仅在某些伪类内允许第N个表达式,并且这些伪类的名称不是CSS中的保留关键字。
第N个表达式是形式为an+b
的表达式,其中a
和b
是可选整数。 第n个有效表达式的示例: 5n+2
, -5n-2
, -5n
, -2
, -n
, n
。
我们希望语法接受第n个表达式,但仅将其作为实际允许使用的伪类的参数。 我们希望它拒绝第n个表达式作为所有其余伪类的参数。
所有名称通常对应于IDENT
令牌。 创建与第n个伪类名称相对应的特殊标记是不切实际的,因为它们不是保留关键字。 例如,它们也是完全有效的类名称或元素名称。 拥有特殊令牌将迫使我们用IDENT | NTH
替换几乎所有IDENT
事件IDENT | NTH
IDENT | NTH
。
因此,我们剩下的通用标识符IDENT
可以是普通或第n个伪类名称。 标准句法正则表达式无法区分它们,但是语义谓词可以。
链接到解决方案。
重要空白
CSS具有半重要的空格。 半重要性意味着它们中的大多数仅表示令牌的结尾,也就是它们的作用结束的地方。 例如,声明中的空格是不相关的。 以下声明是相等的:
padding : 2;
padding:2;
大多数CSS语法的行为均与上述相同,因此强烈倾向于将所有空格都丢弃。 但是,如果这样做,那么接下来的两个选择器将最终成为相同的IDENT COLON IDENT LPAREN COLON IDENT RPAREN COLON IDENT LPAREN COLON IDENT RPAREN LBRACE
令牌流:
div :not(:enabled) :not(:disabled) {}
div:not(:enabled):not(:disabled) {}
选择器中的空格很重要。 第一个选择器等效于div *:not(:enabled) *:not(:disabled)
而第二个选择器则不是。
注意:
可从antlr网站获得的CSS 2.1语法忽略了此问题。 如果要使用它,则必须先对其进行修复。
一种解决方案是停止隐藏空格。 这将迫使我们在所有解析器规则的所有可能位置中添加显式空白WS*
。 这将需要大量工作,并且所产生的语法将难以理解。
也有可能放弃antlr解析器中的选择器树构建,并为其编写定制的手工树构建器。 这是我们最初执行此操作的方式,我们可以放心地说它可以工作,但是比基于最终语义谓词的解决方案需要更多的时间和调试时间。
链接到解决方案。
基本
我们从语义谓词语法和一些需要的术语开始。 本章还概述了断言失败时的基本情况。 我们不会详细介绍,这些将在下一章中进行介绍。
句法
语义谓词总是括在花括号内,后跟问号或问号和双箭头:
-
{ condition }?
-
{ condition }?=>
第一个示例使用简单
1+2==3
和2+3==5
条件。 语法存储在Basics.g文件中:
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;NUMERAL: '0'..'9';
number: {2+3==5}?=> NUMERAL NUMERAL;
术语
根据所使用的语法以及语法的位置,语义谓词可以通过以下三种不同的名称之一进行调用:
- 歧义语义谓词,
- 验证语义谓词,
- 门控语义谓词。
歧义语义谓词
消除歧义的谓词使用较短的{...}?
句法。 但是,仅当谓词位于规则的开头或替代项的开头时,才称为歧义消除。
歧义语义谓词:
LETTER : 'a'..'z' | 'A'..'Z';
// beginning of a rule
rule: {1+2==3}? LETTER LETTER;// beginning of an alternative
alternatives: LETTER ({2+3==5}? LETTER*| {2+3==5}? LETTER+
);
验证语义谓词
验证谓词还使用较短的{...}?
句法。 消除歧义谓词的区别仅在于位置。 验证谓词位于规则的中间或替代的中间:
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;
门控语义谓词
门控语义谓词使用更长的{...}?=>
语法。 该条件可以放置在任何地方。
门控语义谓词:
NUMERAL: '0'..'9';
number: {2+3==5}?=> NUMERAL NUMERAL;
谓词失败
正如我们在表达式语言教程文章中所解释的那样,解析器开始知道哪个规则应该对应于输入,然后尝试将其与输入匹配。 匹配总是从规则的最左边的元素开始,一直到右边。
如果匹配遇到语义谓词,则它将测试条件是否满足。 如果不满意,则抛出FailedPredicateException
异常。
请考虑本章开头显示的Basics.g语法:
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;NUMERAL: '0'..'9';
number: {2+3==5}?=> NUMERAL NUMERAL;
如果打开生成的BasicsParser
类,您会发现每个规则都有相同名称的对应方法。 它们都包含谓词的副本,并且如果不满足条件,它们都将引发异常:
// inside the generated word() method
if ( !((1+2==3)) ) {throw new FailedPredicateException(input, 'word', '1+2==3');
}
// inside the generated number() method
if ( !((2+3==5)) ) {throw new FailedPredicateException(input, 'number', '2+3==5');
}
预测,例如,如果解析器同时遇到带有多个备选和谓词的规则,会发生什么情况
start: word | number
start: word | number
规则,将在下一章中介绍。
吊装和预测
根据使用语义谓词的位置和方式,解析器可能会尝试避免失败的谓词异常。 使用的策略称为“提升”,这使谓词变得有用。
本章说明了什么是起重以及其产生的后果。 然后我们将说明何时使用和不使用它。
这是什么
遇到具有多个备选方案的规则的解析器必须决定应使用这些备选方案中的哪个。 如果其中一些以谓词开头,则解析器可以使用该谓词来帮助决策。
考虑存储在DisambiguatingHoistingNeeded.g文件中的语法:
LETTER : 'a'..'z' | 'A'..'Z';
word: {1+2==3}? LETTER LETTER;
sameWord: {1+2!=3}? LETTER LETTER;start: word | sameWord;
生成的解析器的word()
和sameWord()
方法都包含通常的失败谓词检查。
DisambiguatingHoistingNeededParser
类摘录:
//inside the word() method
if ( !((1+2==3)) ) {throw new FailedPredicateException(input, 'word', '1+2==3');
}
//inside the sameWord() method
if ( !((1+2!=3)) ) {throw new FailedPredicateException(input, 'sameWord', '1+2!=3');
}
另外,与start
规则相对应的代码包含word
和sameWord
语义谓词的副本。 选择下一步使用哪个规则的部分包含以下代码(注释是我的):
int LA1_2 = input.LA(3);
//predicate copied from the word rule
if ( ((1+2==3)) ) {alt1=1;
} //predicate copied from the sameWord rule
else if ( ((1+2!=3)) ) {alt1=2;
}
else {NoViableAltException nvae = new NoViableAltException('', 1, 2, input);throw nvae;
}
将谓词复制到生成的解析器的预测部分中的操作称为提升。
后果
如果没有提升,则谓词仅充当断言。 我们可以使用它们来验证某些条件,仅此而已。 上面的语法是非法的–如果您忽略谓词,它有两种语法上等效的替代方法。
由于提升的语法在整个语法中都占主导地位,因此对它们也有一些局限性。 您可以放心地忽略不只是在后台发生的事情:
- 每个谓词可以运行多次,
- 谓词运行的顺序可能很难预测,
- 本地变量或参数可能在悬挂式副本中不可用。
条件必须无副作用,可重复且评估顺序无关紧要。 如果将它们提升为其他规则,则它们将无法引用局部变量或参数。
只有放在规则开头的谓词才会被提升到其他规则中。 在其他情况下吊装仅在规则之内。 因此,如果谓词未放在规则的开头,则可以破坏第三条规则。
使用时
仅当解析器必须在多个规则或替代方法之间做出决定并且其中一些规则以谓词开头时,才使用提升。 如果它是门控谓词,例如{...}?=>
语法中的条件,则无论如何都将提升谓词。
如果它是消除歧义的谓词,例如{...}?
内的条件{...}?
语法,则仅在确实需要谓词时才会挂起谓词。 术语“实际需要”是指多个备选方案可以匹配相同的输入。 否则,仅当某些输入的多个替代项不明确时才使用它。
置于规则中间或替代中间的谓词永远不会被提升。
如果需要吊装-消除谓词歧义
考虑规则
start
DisambiguatingHoistingNotNeeded.g语法start
:
LETTER : 'a'..'z' | 'A'..'Z';
NUMBER : '0'..'9';
word: {1+2==3}? LETTER LETTER;
differentWord: {1+2!=3}? LETTER NUMBER;start: word | differentWord;
规则
start
必须在word
规则和differentWord
规则之间进行选择。 它们都以谓词开头,但是不需要谓词来区分它们。 word
的第二个标记是LETTER
而differentWord
的第二个标记是NUMBER
。
不会使用吊装。 相反,语法将研究即将到来的2个标记,以区分这些规则。 为了进行验证,请在我们的示例项目中打开生成的DisambiguatingHoistingNotNeededParser
类的start()
方法:既没有复制1+2==3
也没有复制1+2!=3
条件。
int alt1=2;
switch ( input.LA(1) ) {
case LETTER: {switch ( input.LA(2) ) {case LETTER: {alt1=1;}break;case NUMBER: {alt1=2;}break;default:NoViableAltException nvae =new NoViableAltException('', 1, 1, input);throw nvae;
}
另一方面,请考虑从DisambiguatingHoistingNeeded.g语法中start
的规则:
LETTER : 'a'..'z' | 'A'..'Z';
word: {1+2==3}? LETTER LETTER;
sameWord: {1+2!=3}? LETTER LETTER;start: word | sameWord;
规则start
必须在word
规则和sameWord
规则之间进行选择。 这两个规则完全匹配相同的标记序列,并且仅谓词不同。
将使用吊装。 要进行验证,请在我们的示例项目中打开生成的DisambiguatingHoistingNeededParser
类的start()
方法。 它包含1+2==3
和1+2!=3
条件的副本。
int alt1=2;
switch ( input.LA(1) ) {
case LETTER: {switch ( input.LA(2) ) {case LETTER: {int LA1_2 = input.LA(3);if ( ((1+2==3)) ) {alt1=1;} else if ( ((1+2!=3)) ) {alt1=2;} else { /* ... */ }}break;default: // ...}
}
break;
default: // ...
}
谓词中的歧义歧义正在发生完全相同的事情。
这将不会被吊起(DisambiguatingHoistingNotNeeded.g语法):
LETTER : 'a'..'z' | 'A'..'Z';
alternatives: LETTER ({2+3==5}? LETTER| {2+3==5}? NUMBER
);
这将被悬挂(DeambiguatingHoistingNeeded.g语法):
LETTER : 'a'..'z' | 'A'..'Z';
alternatives: LETTER ({2+3==5}? LETTER| {2+3==5}? LETTER
);
始终吊起–门控规则
查看GatedHoisting.g语法中的start
规则:
LETTER : 'a'..'z' | 'A'..'Z';
NUMBER: '0'..'9';word: {1+2==3}?=> LETTER LETTER;
differentWord: {1+2!=3}?=> LETTER NUMBER;start: word | differentWord;
规则start
必须在word
和differentWord
单词之间进行选择。 它们都以谓词开头,不需要谓词来区分它们。
但是,将使用卷扬,因为我们使用了门控语义谓词。 要进行验证,请在我们的示例项目中打开生成的GatedHoisting
类的start()
方法。 它包含1+2==3
和1+2!=3
条件的副本。
int LA1_0 = input.LA(1);if ( (LA1_0==LETTER) && (((1+2==3)||(1+2!=3)))) {int LA1_1 = input.LA(2);if ( (LA1_1==LETTER) && ((1+2==3))) {alt1=1;}else if ( (LA1_1==NUMBER) && ((1+2!=3))) {alt1=2;}else {NoViableAltException nvae =new NoViableAltException('', 1, 1, input);throw nvae;}
}
else {NoViableAltException nvae =new NoViableAltException('', 1, 0, input);throw nvae;
}
在替代方案中,门控谓词发生了完全相同的事情。 这将被提升(GatedHoisting.g语法):
LETTER : 'a'..'z' | 'A'..'Z';
NUMBER: '0'..'9';alternatives: LETTER ({2+3==5}?=> LETTER| {2+3==5}?=>NUMBER
);
永不吊装-一条规矩
如果谓词位于规则或替代规则的中间,则永远不要使用提升。 使用哪种谓词类型都没有关系。 因此,如果您的规则仅因谓词而不同,则该谓词必须放在规则或替代规则的开头。
非提升的门控谓词(GatedNoHoisting.g):
LETTER: 'a'..'z' | 'A'..'Z';
NUMBER: '0'..'9';//gated predicate in the middle of a rule
word: LETTER {1+2==3}?=> LETTER;
differentWord: LETTER {1+2!=3}?=> NUMBER;start: word | differentWord;
另一个非悬挂式门控谓词(GatedNoHoisting.g):
LETTER: 'a'..'z' | 'A'..'Z';
NUMBER: '0'..'9';//gated predicate in the middle of an alternative
alternatives: LETTER (LETTER {2+3==5}?=> LETTER| LETTER {2+3==5}?=> NUMBER
);
生成的解析器在GatedNoHoistingParser
类中。
最重要的一点是,如果您的规则仅因谓词而不同,并且该谓词位于规则的中间,则antlr将拒绝生成相应的解析器。 下一个可扩展框包含语法错误的几个语法示例,以及它们引起的反错误。
不正确的语法(SyntacticallyIncorrect.g):
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;
sameWord: LETTER {1+2!=3}? LETTER;start: word | sameWord;
控制台错误:
warning(200): org\meri\antlr\predicates\SyntacticallyIncorrect.g:28:6: Decision can match input such as 'LETTER LETTER' using multiple alternatives: 1, 2
As a result, alternative(s) 2 were disabled for that input
error(201): org\meri\antlr\predicates\SyntacticallyIncorrect.g:28:6: The following alternatives can never be matched: 2
另一个不正确的语法(SyntacticallyIncorrect.g):
alternativesStart: LETTER (LETTER {1+2==3}?| LETTER {1+2!=3}?
);
控制台错误:
warning(200): org\meri\antlr\predicates\SyntacticallyIncorrect.g:31:27: Decision can match input such as 'LETTER' using multiple alternatives: 1, 2
As a result, alternative(s) 2 were disabled for that input
error(201): org\meri\antlr\predicates\SyntacticallyIncorrect.g:31:27: The following alternatives can never be matched: 2
另一个不正确的语法(SyntacticallyIncorrect.g):
LETTER : 'a'..'z' | 'A'..'Z';
gatedWord: LETTER {1+2==3}?=> LETTER;
gatedSameWord: LETTER {1+2!=3}?=> LETTER;gatedStart: gatedWord | gatedSameWord;
控制台错误:
warning(200): org\meri\antlr\predicates\SyntacticallyIncorrect.g:40:11: Decision can match input such as 'LETTER LETTER' using multiple alternatives: 1, 2
As a result, alternative(s) 2 were disabled for that input
error(201): org\meri\antlr\predicates\SyntacticallyIncorrect.g:40:11: The following alternatives can never be matched: 2
上次不正确的语法(SyntacticallyIncorrect.g):
LETTER : 'a'..'z' | 'A'..'Z';
gatedAlternativesStart: LETTER (LETTER {1+2==3}?=> LETTER| LETTER {1+2!=3}?=> LETTER
);
控制台错误:
warning(200): org\meri\antlr\predicates\SyntacticallyIncorrect.g:43:32: Decision can match input such as 'LETTER LETTER' using multiple alternatives: 1, 2
As a result, alternative(s) 2 were disabled for that input
error(201): org\meri\antlr\predicates\SyntacticallyIncorrect.g:43:32: The following alternatives can never be matched: 2
细微差别
上一章“何时使用”子章节显示了谓词在明显悬挂和明显不悬挂的情况下的行为。 我们选择了一些示例,以显示尽可能简单明了的情况。
本子章节包含不同的示例集。 我们选择了我们所知道的最有可能引起混淆的地方。 所有使用的示例都位于Nuances.g文件中。
消除歧义谓词–高级
仅当多个替代方案对于当前输入不明确时,才使用提升歧义的谓词。 否则,仅当实际输入可以由多个替代方案解析时,才运行谓词的提升后的副本。
示例:以下规则中的替代项在语法上不等效,因为它们与同一组输入不匹配。 第一个备选方案完全匹配两个字母,第二个备选方案匹配任意数量的字母:
advancedDisambiguating: LETTER ({1+2==3}? LETTER LETTER| {1+2!=3}? LETTER*
);
如果输入正好以一个LETTER
开头,则第一个选择不能解析它。 因为只有第二个替代匹配,所以不会使用谓词。 解析器将使用第二个替代方法,如果1+2!=3
条件恰好为false
,则解析器将引发失败的谓词异常。
但是,如果输入以两个字母开头,则两个选项都可以匹配它,并且将使用谓词。 这是生成的代码的样子:
int alt4=2;
switch ( input.LA(1) ) {
case LETTER: {switch ( input.LA(2) ) {case LETTER: {int LA4_3 = input.LA(3);//predicate is used only if first two tokens are LETTERif ( ((1+2==3)) ) {alt4=1;}else if ( ((1+2!=3)) ) {alt4=2;}else {// ... irrelevant code ... }}break;//if the second token is not LETTER, predicate is not usedcase EOF: { alt4=2; } break;default: // ... }}break;
//if the first token is not LETTER, predicate is not used
case EOF: // ...
default: // ...
}
将其与非常相似的门控规则进行比较:
compareGated: LETTER ({1+2==3}?=> LETTER LETTER| {1+2!=3}?=> LETTER*
);
解析器将不使用谓词。 永远不会输入第二个选择,因为永远不会满足谓词1+2!=3
:
int alt6=2;
int LA6_0 = input.LA(1);if ( (LA6_0==LETTER) && (((1+2==3)||(1+2!=3)))) {int LA6_1 = input.LA(2);if ( (LA6_1==LETTER) && (((1+2==3)||(1+2!=3)))) {int LA6_3 = input.LA(3);
在这种情况下,门控谓词会使antlr引发不同类型的异常。 正如我们将在本文后面所展示的,门控谓词和歧义谓词的不同提升对于更复杂的谓词而言可以带来更大的不同。 即,它可以在接受和拒绝输入之间有所不同。
循环尽管乍看起来并不像循环,但是循环也是替代方法。 他们使用预测来猜测是否应该再进行一轮还是应该结束。
仅在谓词返回true时才使用谓词:
LETTER : 'a'..'z' | 'A'..'Z';
loop: ( {somePredicate()}?=> LETTER )*;
loop
规则将匹配字母,直到函数somePredicate()
返回false
或直到规则用完LETTER
令牌为止。
loop1:
do {int alt1=2;int LA1_0 = input.LA(1);// predicate is used during the predictionif ( (LA1_0==LETTER) && ((somePredicate()))) {alt1=1;}//matching: either jump out or match another LETTERswitch (alt1) {case 1: {if ( !((somePredicate())) ) {throw new FailedPredicateException(...);}// ... match LETTER ...}break;default:break loop1;}
} while (true);
消除歧义的谓词不能用于此目的。 下一个谓词将不会用于决定解析器是否应留在循环中:
LETTER : 'a'..'z' | 'A'..'Z';
loopDisambiguating: ( {somePredicate()}? LETTER )*;
从技术上讲,循环由LETTER
和<nothing>
替代方案决定。 这些在语法上是不同的,并且只有在必须在语法上模棱两可的替代方案之间做出决定时,预测才使用歧义谓词。
loopDisambiguating
规则将匹配字母,直到用完LETTER
令牌为止。 如果函数somePredicate()
在此期间返回false
,则该规则将引发FailedPredicateException
异常。
生成的代码与前一个代码非常相似,只是预测部分发生了变化。 谓词不使用:
loop2:
do {int alt2=2;//prediction ignores the predicateswitch ( input.LA(1) ) {case LETTER: {alt2=1;}break;}//matching: either jump out or match another LETTERswitch (alt2) {case 1: {if ( !((somePredicate())) ) {throw new FailedPredicateException(...);}// ... match LETTER ...}break;default:break loop2;}
} while (true);
未发现的替代品
保留一些替代方案是完全可以的。 带有谓词的替代方案将按预期工作。 只有在存在多个模棱两可的替代方案时,才会始终悬挂门控谓词,并且会消除歧义谓词。
门控谓词总是被悬挂:
uncoveredGated: LETTER ({3+4==7}?=> LETTER| NUMBER
);
悬挂歧义谓词:
uncoveredDisambiguatingHoisted: LETTER ({2+5==7}? LETTER*| LETTER+
);
非悬挂式歧义谓词:
uncoveredDisambiguatingNotHoisted: LETTER ({2+4==6}? LETTER| NUMBER
);
门控谓词和歧义消除谓词的组合如果一个备选方案被门控,而另一种歧义消除了歧义,则始终会提升门控谓词,只有在实际需要时才解除歧义消除谓词。
提升了门控谓词,而没有消除歧义谓词:
combinationDisambiguatingNotHoisted: LETTER ({1+4==5}?=> LETTER| {1+4!=5}? NUMBER
);
两个谓词都被悬挂:
combinationDisambiguatingHoisted: LETTER ({1+5==6}?=> LETTER*| {1+5!=6}? LETTER+
);
附加括号
如果在括号中关闭歧义谓词,则该谓词仍将被视为歧义谓词。
编写歧义谓词的另一种方法:
stillDisambiguating: ({2+2==4}?) LETTER;
testStillDisambiguating: stillDisambiguating | LETTER;
如果在门控谓词周围附加括号,则该谓词将被忽略:
ignored: ({3+3==6}?)=>LETTER;
回溯
即使解析器正在回溯,谓词也会运行,例如,如果它在语法谓词内部。 如果解析器正在回溯并且谓词失败,那么回溯也会失败。 仅当解析器未回溯时,才会引发失败的谓词异常。
backtrack
规则将启动回溯(Backtracking.g):
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;
number: LETTER {2+3!=5}? LETTER;backtrack: (number)=> number | word;
由于可以进行回溯,因此生成的谓词检查是不同的:
if ( !((2+3!=5)) ) {if (state.backtracking>0) {state.failed=true; return retval;}throw new FailedPredicateException(input, 'number', '2+3!=5');
}
回溯是条件不得产生副作用,必须可重复且评估顺序不重要的另一个原因。
写作条件
本章说明如何为语义谓词编写高级条件。 首先,我们将展示如何访问和使用输入令牌流。 我们还将说明如何引用标记的令牌。 最后一部分是关于非悬挂条件下的局部变量。
除非另有说明,否则所有使用的示例都位于Environnement.g文件中。
输入令牌流
每个生成的解析器都有公共TokenStream input
字段。 该字段提供对整个输入令牌流以及该令牌流中当前位置的访问。 它最重要的方法是Token LT(int k)
方法。 参数k
包含您感兴趣的令牌的相对位置。
数字1表示“先看一个令牌”,数字2表示“先看第二令牌”,依此类推。 负数表示传递的令牌:-1将返回前一个令牌,-2将返回前一个令牌,依此类推。 不要使用0。其含义是undefined,并且默认解析器返回null
。
注意:即使语法处于回溯状态,相对引用也可以正常工作。 -1始终是前一个标记,1始终是下一个标记。
例子
消除歧义:如果
word
以字母开头
a
,那么它必须至少包含两个字母:
word: LETTER ({ input.LT(-1).getText().equals('a')}? LETTER+| { !input.LT(-1).getText().equals('a')}? LETTER*);
门控:如果所述的第二个数字number
是9
,那么它必须具有完全相同3标记:
number: NUMERAL ({input.LT(1).getText().equals('9')}?=> NUMERAL NUMERAL| {!input.LT(1).getText().equals('9')}?=> NUMERAL*
);
注:谓语的选择略有事情在这种情况下。 如果输入不符合规则,它将影响将引发何种类型的错误。
标签和清单
谓词可以像操作一样引用和使用任何先前定义的标签或标签列表。
标签示例
如果单词的第一个字母是a
,那么这个词必须至少有两个字母:
labeledWord: a=LETTER ({ $a.getText().equals('a')}? LETTER+| { !$a.getText().equals('a')}? LETTER*);
标签列表示例如果单词以少于3个字母开头,则它必须以数字结尾:
labeledListWord: a+=LETTER+ ({ $a.size() < 3 }?=> NUMERAL| { $a.size() >= 3}?=> );
注意:在这种情况下,谓词的选择确实很重要。 上面的示例只有在使用门控{...}?=>
谓词而不是消除{...}?
歧义的情况下才能正常工作{...}?
一。 NUMERAL
和<nothing>
在语法上是不同的。 消除歧义的谓词将不会用于预测,例如不会被提升。 解析器将仅根据下一个标记(是NUMERAL
吗?)做出决定。
该条件将用作事后声明,以检查字母数是否正确。 这样的语法会在abcd9
输入上引发异常,而我们的输入会接受它。
未定义标签
谓词不能引用尚未定义的标签。 解析器已生成,但首次尝试使用该规则会在运行时引发空指针异常:
//this would cause null pointer exception
nullPointerAtPredicate: LETTER { $a.getText().equals('a') }? a=LETTER;
标签和吊装其他规则
由于必须在使用谓词之前先定义标签,并且仅当谓词位于规则的开头时才将其复制到其他规则中,因此您不必担心提升到其他规则中。
访问局部变量
Antlr允许您定义自定义局部变量并与一个规则一起使用。 如果您确定谓词不会被复制到其他规则中,则可以使用它们。 当然,如果可以将谓词复制到其他规则中,则使用局部变量将导致错误的解析器。
创建局部变量并在谓词中使用它们。 如果单词以少于10个字母开头,则必须以数字结尾
localVariables
@init {int num=0;} //define local variable num: (LETTER { num++; })* //raise the num variable by 1 for each letter ( // what should follow depends on the variable value{ num < 10 }?=> NUMERAL| { num >= 10}?=> );
注意:适用与之前相同的警告。 我们必须使用门控谓词。
您必须特别小心,不要在潜在的提升谓词中使用局部变量。 例如,Antlr参考书建议遵循以下规则以仅匹配由少于四个数字组成的数字(ANTLRReference3Error.g):
localVariablesWarning
@init {int n=1;} // n becomes a local variable: ( {n<=4}?=> NUMERAL {n++;} )+ // enter loop only if n<=4;
上面的规则可以很好地隔离工作,即在其他规则中不使用时。 不幸的是,如果将其包含在其他规则中,则该谓词可能会被提升到该其他规则中(ANTLRReference3Error.g):
// syntax error in generated parser
syntaxError: localVariablesWarning | LETTER;
n<=4
谓词将被复制到syntaxError
规则中。 在该规则内无法访问变量n
,并且生成的解析器在语法上将是错误的。
解决初始用例
最后,我们将解决激励性章节中描述的两个用例。
关键字–第n个
链接到原始用例。
我们创建了isInsideNth
函数,该函数仅在先前的令牌与某个第n个伪类的名称匹配时才返回true
。 该函数用作门控谓词内部的条件。 且仅当输入在第n个伪类内时,生成的解析器才会假定输入包含第n个表达式。
UseCasesNth.g文件:
@parser::members {private static Set<String> NTH_PSEUDOCLASSES = new HashSet<String>();static {NTH_PSEUDOCLASSES.add('nth-child');NTH_PSEUDOCLASSES.add('nth-last-child');NTH_PSEUDOCLASSES.add('nth-of-type');NTH_PSEUDOCLASSES.add('nth-last-of-type');}public boolean isInsideNth() {return isNthPseudoClass(input.LT(-1));}private boolean isNthPseudoClass(Token a) {if (a == null)return false;String text = a.getText();if (text == null)return false;return NTH_PSEUDOCLASSES.contains(text.toLowerCase());}}LPAREN: '(';
RPAREN: ')';
COLON: ':';
COMMA: ',';
IDENT : ('a'..'z' | 'A'..'Z')+;//pseudoparameters and nth with dummy syntax
pseudoparameters: IDENT (COMMA IDENT)*;
nth: IDENT; //real nth syntax ommited for simplicity sake// pseudoclass
pseudo: COLON COLON? IDENT (({ isInsideNth()}?=> LPAREN nth RPAREN| LPAREN pseudoparameters RPAREN)?);
具有标签和重写规则的替代解决方案:
//different solution - note that we need to use rewrite rules in this case
pseudoDifferentSolution: COLON COLON? name=IDENT (({ isNthPseudoClass($name)}?=> LPAREN nthParameters=nth RPAREN| LPAREN parameters=pseudoparameters RPAREN)?)-> $name $nthParameters? $parameters? ;
重要空白
链接到原始用例。
CSS选择器可以由用组合符>
, +
, ~
和<space>
分隔的多个部分组成。 每个称为简单选择器的部分都以一个可选的元素名称开头,然后可以跟随多个伪类,属性和类似结构。
忽略空格作为组合器的问题,简化的简单选择器语法如下所示:
COLON: ':';
STAR: '*';
NUMBER: ('0'..'9')+;
IDENT : ('a'..'z' | 'A'..'Z')+;//some options have been removed from following rules for simplicity sake
elementName: IDENT | STAR | NUMBER;
pseudoClass: COLON COLON? IDENT;
elementSubsequent: pseudoClass; simpleSelectorWrong: (elementName elementSubsequent*)| elementSubsequent+
;
上面的simpleSelectorWrong
规则匹配有效的简单选择器: h1
, h1:first-child:hover
, :first-child:hover
和:hover
。
不幸的是,随着空白的消失,上述规则的匹配程度更高。 例如,它也将匹配h1:first-child :hover
,这应该与h1:first-child *:hover
选择器完全一样地解释,例如,两个简单的选择器由<space>
。
我们创建了仅在上一个和下一个标记之间没有隐藏标记的情况下才返回true的方法。 除非另有配置,否则所有令牌都是CommonToken
类的实例。 由于通用令牌知道其起始索引和终止索引,因此我们可以对其进行转换和比较,以查看它们之间是否存在某些事物。
新的解析器方法(UseCasesSelectors.g):
@parser::members {public boolean onEmptyCombinator(TokenStream input) {return !directlyFollows(input.LT(-1), input.LT(1));}private boolean directlyFollows(Token first, Token second) {CommonToken firstT = (CommonToken) first;CommonToken secondT = (CommonToken) second;if (firstT.getStopIndex() + 1 != secondT.getStartIndex())return false;return true;}
}
固定的简单选择器使用gated谓词来检查是否应该继续添加后续元素(UseCasesSelectors.g):
simpleSelector: (elementName ({!onEmptyCombinator(input)}?=>elementSubsequent)*) | (elementSubsequent ({!onEmptyCombinator(input)}?=>elementSubsequent)*);
在这种情况下,我们必须使用门控谓词。 如果使用歧义谓词,则生成的解析器将不会使用谓词来决定是否保留在循环内。 这是因为循环在技术上决定了elementSubsequent
和<nothing>
替代方案,而这些替代方案在语法上是不同的。 {...}?
谓词不会在预测期间使用,只会偶尔抛出异常。
包起来
语义谓词是在语法内部编写的Java条件。 它们将被原样复制到生成的解析器中,而没有任何更改。 如果令牌匹配算法到达语义谓词并且该谓词失败,则抛出FailedPredicateException
异常。
如果规则或替代规则以语义谓词开头,则可以在预测阶段使用该语义谓词。 预测阶段中失败的谓词永远不会引发异常,但是它们可能会禁用某些替代项。 这称为吊装。
条件必须无副作用,可重复且评估顺序无关紧要。 如果将它们提升为其他规则,则它们将无法引用局部变量或参数。
语义谓词以三种不同的方式使用:作为验证语义谓词,作为歧义语义谓词和作为门控语义谓词。
验证语义谓词
验证语义谓词仅充当断言。 结果,永远不会悬挂验证语义谓词。
条件在花括号内关闭,后跟问号{ condition }?
。 必须将其放置在规则的中间或替代的中间:
LETTER : 'a'..'z' | 'A'..'Z';
word: LETTER {1+2==3}? LETTER;
歧义语义谓词
歧义性语义谓词有助于在语法上等效的替代方案之间进行选择。 结果,仅当解析器必须在多个模棱两可的备选方案之间进行选择时,才会消除歧义的语义谓词。
歧义语义谓词使用与验证谓词完全相同的语法。 条件在花括号内关闭,后跟问号{ condition }?
。 但是,必须将它们放在规则的开头或替代的开头:
LETTER : 'a'..'z' | 'A'..'Z';
// beginning of an alternative
alternatives: LETTER ({2+3==5}? LETTER*| {2+3==5}? LETTER+
);
门控语义谓词
门控语义谓语用于动态打开和关闭语法部分。 结果,提升了放置在规则或替代规则开头的所有门控谓词。 置于规则或替代规则中间的门控谓词永远不会被悬挂。
条件在花括号内关闭,后跟问号和双箭头{ condition }?=>
:
NUMERAL: '0'..'9';
number: {2+3==5}?=> NUMERAL NUMERAL;
- Wincent维基
- 堆栈溢出问题
- 权威的ANTLR参考
参考: ANTLR –我们的JCG合作伙伴 Maria Jurcovicova的“ 语义谓词”在“ This is Stuff”博客上。
翻译自: https://www.javacodegeeks.com/2012/12/antlr-semantic-predicates.html