Parser-rules.md

来自osdev
Zhang3讨论 | 贡献2022年2月23日 (三) 07:22的版本 (创建页面,内容为“{{MARKDOWN}} # 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: retst…”
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

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):

SyntaxDescription
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表示语法片段):

SyntaxDescription
(x|y|z).

精确匹配子规则中的任何选项一次。例子:
returnType : (type | 'void') ;

(x|y|z)?

子规则中不匹配任何项或任何alternative。示例:
classDeclaration

   : 'class' ID (typeParameters)? ('extends' type)?
     ('implements' typeList)?
      classBody
   ;

(x|y|z)*

在子零次或更多次内匹配alternative。示例:
annotationName : ID ('.' ID)* ;

(x|y|z)+

将subrule中的alternative匹配一次或多次。例子:
annotations : (annotation)+ ;

您可以将 ?, *, 和 + subrule运算符加上非贪心匹配运算符后缀,和正则表达式一样也是一个问号: ??, *?, 和 +?。 参见第15.6节,Wildcard Operator and Nongreedy Subrules(通配符运算符和非通配符子规则)。

作为速记,您可以省略由具有单个Rule元素引用的单个alternative组成的subrole的括号。 例如annotation+(annotation)+ 相同,ID+(ID)+ 相同。 Labels也可以使用简写法。ids+=INT+产生INTtoken对象列表。

捕获异常

当规则中出现语法错误时,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 nameDescription
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需要在两者之间加一个:。 Go i int, Swift i:int. Go目标必须使用int ii:int
  • Python和JavaScript不指定静态类型,因此操作只是标识符列表,例如 “[i,j]”。

从技术上讲,任何目标都可以使用这两种符号。 有关示例,请参见TestScopeParsing.

与grammar级别一样,您可以指定rule级别的命名操作。 对于rules,有效名称是initafter。 顾名思义,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; // 不要过早的停止。必须匹配所有输入