Parser-rules.md
Parser Rules(解析器中的规则)
Parsers由一组在parser/combined grammar 中的Parser Rules组成的。 Java应用程序通过调用由ANTLR生成的与所需启动rule相关联的rule function来启动Parser。 最基本的rule只是一个rule名称,后跟以分号终止的单个alternative:
/** Javadoc注释可以位于rule之前 */
retstat : 'return' expr ';' ;
Rules也可以有由|分割
operator:
stat: retstat
| 'break' ';'
| 'continue' ';'
;
Alternatives要么是rule元素列表,要么是空的。 例如,这里有一个带有空替代项的rule,使整个规则成为可选规则:
superClass
: 'extends' ID
| // 为空表示其他alternative是可选的
;
Alternative Labels (候选标记)
正如我们在第7.4节中看到的那样,针对Precise Event Methods的Labeling Rule Alternatives,我们可以通过使用#符号来labeling一个rule的outermost alternatives来获得更精确的parse-tree listener事件。 Rule中的所有alternatives都必须labeled,或者一个都不labeled。 以下是两条带有labeled alternatives的Rules。
grammar T;
stat: 'return' e ';' # Return
| 'break' ';' # Break
;
e : e '*' e # Mult
| e '+' e # Add
| INT # Int
;
Alternative labels不必在行的末尾,并且在#符号之后也不必有空格。 ANTLR为每个Label生成Rule上下文类定义。例如,以下是ANTLR生成的listener:
public interface AListener extends ParseTreeListener {
void enterReturn(AParser.ReturnContext ctx);
void exitReturn(AParser.ReturnContext ctx);
void enterBreak(AParser.BreakContext ctx);
void exitBreak(AParser.BreakContext ctx);
void enterMult(AParser.MultContext ctx);
void exitMult(AParser.MultContext ctx);
void enterAdd(AParser.AddContext ctx);
void exitAdd(AParser.AddContext ctx);
void enterInt(AParser.IntContext ctx);
void exitInt(AParser.IntContext ctx);
}
有与每个labeled alternative相关联的enter和exit方法。 这些方法的参数特定于alternatives。
您可以在多个alternatives上重复使用相同的Label,以指示parse tree walker应该为这些alternatives触发相同的事件。 例如,以下是上述grammar A中Rule e的变体:
e : e '*' e # BinaryOp
| e '+' e # BinaryOp
| INT # Int
;
ANTLR将为e生成以下listener方法:
void enterBinaryOp(AParser.BinaryOpContext ctx);
void exitBinaryOp(AParser.BinaryOpContext ctx);
void enterInt(AParser.IntContext ctx);
void exitInt(AParser.IntContext ctx);
如果alternative名称与rule名称冲突,ANTLR会给出错误。 下面是rule e的另一个重写,其中两个alternative labels与rule名称冲突:
e : e '*' e # e
| e '+' e # Stat
| INT # Int
;
从rule名称和Label生成的上下文对象大写,因此label Stat与rule stat冲突:(译者注:这里Stat是更前面的代码示例里面的)
$ antlr4 A.g4
error(124): A.g4:5:23: rule alt label e conflicts with rule e
error(124): A.g4:6:23: rule alt label Stat conflicts with rule stat
warning(125): A.g4:2:13: implicit definition of token INT in parser
Rule Context Objects(rule上下文对象)
ANTLR会生成用于访问与每个rule reference相关联的rule context objects (parse tree nodes) 的方法。 对于具有单个rule reference的rule,ANTLR生成没有参数的方法。 考虑下面的rule。
inc : e '++' ;
ANTLR生成此上下文类:
public static class IncContext extends ParserRuleContext {
public EContext e() { ... } // 返回与e关联的上下文对象
...
}
当对rule有多个reference时,ANTLR还提供访问上下文对象的支持:
field : e '.' e ;
ANTLR生成一个方法,该方法具有访问第i个元素的索引,以及获取该规则所有引用的上下文的方法:
public static class FieldContext extends ParserRuleContext {
public EContext e(int i) { ... } // get ith e context
public List<EContext> e() { ... } // return ALL e contexts
...
}
如果我们有另一个rule s,references到field,则嵌入式action可以访问e rule匹配项列表,该列表可由field使用:
s : field
{
List<EContext> x = $field.ctx.e();
...
}
;
listener或visitor也可以做同样的事情。 指向FieldContext的f对象后,f.e()将返回List
Rule Element Labels(Rule元素标记)
您可以使用=符号来label rule元素(译者注:label做动词,标记),以向rule上下文对象添加字段:
stat: 'return' value=e ';' # Return
| 'break' ';' # Break
;
这里,value是对rule e返回值的label,该返回值已在其他地方定义。 Labels成为适当的解析树节点类中的字段。 在这种情况下,由于使用ReturnContext替代Label,Label Value将成为ReturnContext中的一个字段:
public static class ReturnContext extends StatContext {
public EContext value;
...
}
跟踪大量令牌通常很方便,可以使用+=“list label”操作符来完成。 例如,以下规则创建与简单数组结构匹配的令牌对象列表:
array : '{' el+=INT (',' el+=INT)* '}' ;
ANTLR在适当的Rule上下文类中生成列表字段:
public static class ArrayContext extends ParserRuleContext {
public List<Token> el = new ArrayList<Token>();
...
}
这些Label列表也适用于rule references:
elist : exprs+=e (',' exprs+=e)* ;
ANTLR生成一个保存上下文对象列表的字段:
public static class ElistContext extends ParserRuleContext {
public List<EContext> exprs = new ArrayList<EContext>();
...
}
Rule元素
Rule元素指定parser在给定时刻应该做什么,就像编程语言中的语句一样。 元素可以是rule、token、string literal,如表达式、ID和‘return’。 以下是rule元素的完整列表 (稍后我们将详细介绍actions和predicates):
Syntax | Description |
---|---|
T | 在当前输入位置匹配TokenT。Token始终以大写字母开头。 |
’literal’ | 在当前输入位置匹配string literal。string literal只是带有固定字符串的token |
r | 在当前输入位置匹配Rule r,这相当于像调用函数一样调用Rule。Parser rule名称始终以小写字母开头。 |
r [«args»] | 在当前输入位置匹配Rule r,传递参数列表就像函数调用一样。 方括号内的参数采用目标语言的语法,通常是一个逗号分隔的表达式列表 |
{«action»} | 在备选元素之前或之后的执行一个action。 该action符合目标语言的语法。 除了替换$x和$x.y等属性和token引用之外,ANTLR会将操作代码逐字复制到生成的类中。 |
{«p»}? | 运行语义谓词 «p»。 如果«p»在运行时的计算结果为false,则不要继续分析谓词。 预测期间遇到的谓词,当ANTLR区分备选方案时,启用或禁用谓词周围的备选方案。 |
. | 匹配除文件结尾以外的任何单个Token。“点” 运算符称为通配符。 |
如果要匹配除特定Token或Token集之外的所有内容,请使用~
“非”运算符。 此操作符在解析器中很少使用,但可用。 '~ INT' 匹配除 'INT' Token以外的任何Token。' ~ ',''匹配除逗号以外的任何Token。 ~(INT | ID)
匹配除INT或ID之外的任何Token。
Token、string literal和semantic predicate rule元素可以接受选项。 请参阅规则元素选项。
Subrules(子规则)
Rule可以包含称为Subrules的alternative块(在扩展BNF Notation:EBNF中允许)。 subrule类似于缺少名称并用括号括起来的规则。 Subrules可以在括号内有一个或多个alternatives。 Subrules不能像rules一样使用局部变量定义属性和返回值。 有四种子规则(x、y和z表示语法片段):
Syntax | Description |
---|---|
(x|y|z).
精确匹配子规则中的任何选项一次。例子:
|
|
(x|y|z)?
子规则中不匹配任何项或任何alternative。示例:
: 'class' ID (typeParameters)? ('extends' type)? ('implements' typeList)? classBody ;
| |
(x|y|z)*
在子零次或更多次内匹配alternative。示例:
| |
(x|y|z)+
将subrule中的alternative匹配一次或多次。例子:
|
您可以将 ?
, *
, 和 +
subrule运算符加上非贪心匹配运算符后缀,和正则表达式一样也是一个问号: ??
, *?
, 和 +?
。 参见第15.6节,Wildcard Operator and Nongreedy Subrules(通配符运算符和非通配符子规则)。
作为速记,您可以省略由具有单个Rule元素引用的单个alternative组成的subrole的括号。 例如annotation+
与 (annotation)+
相同,ID+
与 (ID)+
相同。 Labels也可以使用简写法。ids+=INT+
产生INT
token对象列表。
捕获异常
当规则中出现语法错误时,ANTLR会捕获异常,报告错误,尝试恢复(可能通过使用更多Token),然后从rule返回。 每个rule都包装在 “try/catch/finalally” 语句中:
void r() throws RecognitionException {
try {
rule-body
}
catch (RecognitionException re) {
_errHandler.reportError(this, re);
_errHandler.recover(this, re);
}
finally {
exitRule();
}
}
在第9.5节“改变ANTLR的错误处理策略”中,我们看到了如何使用Strategy对象来改变ANTLR的错误处理。 但是,替换strategy会更改所有rule的strategy。 要更改单个rule的异常处理,请在rule定义后指定异常:
r : ...
;
catch[RecognitionException e] { throw e; }
该示例演示了如何避免默认错误报告和恢复。 r重新抛出异常,当需要对更高级别的rule报告更有意义的错误时,这很有用。 指定任何exception子句会阻止ANTLR生成子句来处理RecognitionException
。
您也可以指定其他异常:
r : ...
;
catch[FailedPredicateException fpe] { ... }
catch[RecognitionException e] { ... }
大括号内的代码片段和异常“参数”操作必须用目标语言编写; 以上是Java的写法。 当你即使发生异常,也需要执行操作,可以把它到 'finaly' 子句中:
r : ...
;
// catch blocks go first
finally { System.out.println("exit rule r"); }
finally子句在规则触发exitRule
之前执行,然后返回。 如果您想要在rule匹配完alternatives之后,但在它执行清理工作之前执行某个操作,请使用after
操作。
以下是例外的完整列表:
Exception name | Description |
---|---|
RecognitionException | ANTLR生成的识别器引发的所有异常的超类。 它是RuntimeException的子类,以避免检查异常的麻烦。 此异常记录了识别器 (lexer或parser) 在输入中的位置,它在ATN (表示语法的内部图形数据结构) 中的位置,规则调用堆栈以及发生了什么样的问题。 |
NoViableAltException | 指示parser无法通过查看剩余的输入来决定采用两条或多条路径中的哪一条。 此异常跟踪违规输入的起始Token,并且还知道错误发生时parser在各种路径中的位置。 |
LexerNoViableAltException | 相当于NoViableAltException,但仅适用于lexers。 |
InputMismatchException | 当前输入Token与parser预期的不匹配。 |
FailedPredicateException | 在预测(predicting)期间计算为False的语义谓词将使周围的alternative不可用。 当一条规则正在预测(predicting)采取哪种替代方案时,就会发生Prediction。 如果所有可行的路径都消失了,parser将抛出NoViableAltException。 在匹配Token和调用Rule的正常解析过程中,当语义谓词在预测之外评估为false时,解析器会抛出此谓词。 |
Rule Attribute Definitions(规则属性定义)
有许多action相关的语法元素和rule相关需要注意。 Rules可以有参数、返回值和局部变量,就像编程语言中的函数一样。 (Rule可以在Rule元素中嵌入action,正如我们将在第15.4节 “操作和属性” 中看到的那样。) ANTLR收集您定义的所有变量,并将它们存储在Rule上下文对象中。 这些变量通常称为属性。 下面是显示所有可能的属性定义位置的通用语法:
rulename[args] returns [retvals] locals [localvars] : ... ;
这些[…]中定义的属性可以像任何其他变量一样使用。 以下是复制参数以返回值的示例rule:
// 返回参数加上INT Token的整数值
add[int x] returns [int result] : '+=' INT {$result = $x + $INT.int;} ;
参数,局部变量和返回值[...]
通常是使用目标语言,但有一些限制。 [...]
字符串是一个逗号分隔的声明列表,可以使用前缀或后缀类型表示法,也可以不使用类型表示法。 元素可以有[int x = 32, float y]
这样的初始值设定项,但是不要太疯狂,因为我们正在用ScopeParser.手动解析这个泛型文本
- Java,CSharp,C++ 使用
int x
表示法,但C++必须对数组引用int[] x
使用稍微更改的表示法,以适合 type id 语法。 - Go和Swift在变量名后给出类型,但Swift需要在两者之间加一个
:
。 Goi int
, Swifti:int
. Go目标必须使用int i
或i:int
。 - Python和JavaScript不指定静态类型,因此操作只是标识符列表,例如 “[i,j]”。
从技术上讲,任何目标都可以使用这两种符号。 有关示例,请参见TestScopeParsing.
与grammar级别一样,您可以指定rule级别的命名操作。 对于rules,有效名称是init
和after
。 顾名思义,parsers在尝试匹配关联rule之前立即执行init操作,在匹配rule之后立即执行after操作。 ANTLR中的after操作不会作为生成的rule函数的最终代码块的一部分执行。 使用ANTLR finally操作将代码放入生成的规则函数finally 代码块中。
这些操作位于任何参数、返回值或本地属性定义操作之后。 第10.2节 “访问Token和rule属性” 中的 row
rule前导码很好地说明了语法: actions/CSV.g4
/** Derived from rule "row : field (',' field)* '\r'? '\n' ;" */
row[String[] columns]
returns [Map<String,String> values]
locals [int col=0]
@init {
$values = new HashMap<String,String>();
}
@after {
if ($values!=null && $values.size()>0) {
System.out.println("values = "+$values);
}
}
: ...
;
Rule row包含参数columns,返回值,并定义局部变量col。 方括号中的“actions”被直接复制到生成的代码中:
public class CSVParser extends Parser {
...
public static class RowContext extends ParserRuleContext {
public String [] columns;
public Map<String,String> values;
public int col=0;
...
}
...
}
生成的rule函数还将rule参数指定为函数参数,并且它们会快速复制到本地RowContext对象中:
public class CSVParser extends Parser {
...
public final RowContext row(String [] columns) throws RecognitionException {
RowContext _localctx = new RowContext(_ctx, 4, columns);
enterRule(_localctx, RULE_row);
...
}
...
}
ANTLR在action中跟踪嵌套的“[…]”,以便正确解析String[]
column。 它还跟踪尖括号,以便泛型类型参数中的逗号不表示另一个属性的开始。 Map<String,String>
值是一个属性定义。
每个动作中可以有多个属性,即使是返回值也是如此。 使用逗号分隔同一action中的属性:
a[Map<String,String> x, int y] : ... ;
ANTLR解释该action为定义两个参数x和y:
public final AContext a(Map<String,String> x, int y)
throws RecognitionException
{
AContext _localctx = new AContext(_ctx, 0, x, y);
enterRule(_localctx, RULE_a);
...
}
Start Rules and EOF(起始和结尾Rule)
start rule是parser最先使用的rule;它是语言应用程序调用的rule函数。 例如,解析为Java代码的语言应用程序可能是一个JavaParser
类的对象名为parser
,它可以调用parser.compilationUnit()
方法 grammar中的任何rule都可以作为起始rule。
起始Rule不一定消耗所有输入。 它们仅消耗所需的输入,以匹配Rule的alternative。 例如,考虑下面的Rule,根据输入,匹配一个、两个或三个Token。
s : ID
| ID '+'
| ID '+' INT
;
在a+3
时,rules
匹配第三个选项。 在a+b
时,它与第二种选择匹配,并忽略最终的 b
token。 在a b
时,它匹配第一个alternative,忽略b
标记。 在后两种情况下,parser不会使用完整的输入,因为rule`s‘没有显式地说明文件结尾必须在匹配rule的alternative之后才会出现。
这个默认功能对于构建像ide这样的东西非常有用。 假设IDE想在大java文件中间解析某个方法。 调用rulemethod Declaration
应该尝试只匹配一个方法,而忽略接下来发生的任何事情。
另一方面,描述整个输入文件的规则应引用特殊的预定义令牌 EOF
。 如果他们没有,你可能会挠头想一想,为什么不管你给出什么,起始rule都不会报告任何输入的错误。 以下是读取配置文件的语法的一部分:
config : element*; // 即使输入无效,也可以 “匹配”。
无效输入将导致config
立即返回,而不匹配任何输入,也不报告错误。 以下是适当的specification:
file : element* EOF; // 不要过早的停止。必须匹配所有输入