Actions.md

来自osdev
跳到导航 跳到搜索

Actions and Attributes

在第10章,属性和动作中,我们学习了如何将动作嵌入到语法中,并研究了最常见的Token和Rule Attributes。 本节总结了该章的重要语法和语义,并提供了所有可用属性的完整列表。 (你可以从关于listeners和actions的免费摘录中了解更多语法中的动作。)

Action是用目标语言编写的文本块,用大括号括起来。 识别器根据它们在语法中的位置触发它们。 例如,以下规则在解析器看到有效declaration后发出“find a decl”:

decl: type ID ';' {System.out.println("found a decl");} ;
type: 'int' | 'float' ;

最常见的情况是,操作访问Token和rule引用的属性:

decl: type ID ';'
      {System.out.println("var "+$ID.text+":"+$type.text+";");}
    | t=ID id=ID ';'
      {System.out.println("var "+$id.text+":"+$t.text+";");}
    ;

Token Attributes

所有tokens都具有预定义的只读属性的集合。 这些属性包括有用的Token属性,例如Token类型和与Token匹配的文本。 操作可以通过$label.attribute访问这些属性,其中Label标记Token引用的特定实例 (以下示例中的 ab 在操作代码中用作 $a$b)。 通常,一个特定的Token在rule中只被引用一次,在这种情况下,token名称本身可以在操作代码中明确使用(tokenINT可以在操作中用作$INT)。 以下示例说明Token属性表达式语法:

r : INT {int x = $INT.line;}
    ( ID {if ($INT.line == $ID.line) ...;} )?
    a=FLOAT b=FLOAT {if ($a.line == $b.line) ...;}
  ;

(...)? subrole中的操作可以在外部级别中看到在其之前匹配的INT token。

因为有两个对FLOAT token的引用,所以在一个动作中对$FLOAT的引用不是唯一的; 您必须使用labels来指定您感兴趣的Token引用。

不同alternatives的Token引用是唯一的,因为对于规则的任何调用,只有其中一个可以匹配。 例如,在以下规则中,两个alternatives的操作都可以直接引用“$ID”,而无需使用Label:

    r : ... ID {System.out.println($ID.text);}
    | ... ID {System.out.println($ID.text);}
    ;

要访问与文字匹配的Token,您必须使用Label:

    stat: r='return' expr ';' {System.out.println("line="+$r.line);} ;

大多数情况下,您访问Token的属性,但有时访问Token对象本身很有用,因为它会聚合所有属性。 此外,您可以使用它来测试可选subrule是否匹配到Token:

    stat: 'if' expr 'then' stat (el='else' stat)?
    {if ( $el!=null ) System.out.println("found an else");}
    | ...
    ;

对于Token名T和Token标签L$T$L取值为Token对象。 对于标签 ll列表,“ $ ll” 计算为 “list ”。 $T.attr的计算结果为下表中为属性'attr'指定的类型和值:

属性 类型 描述
text String 与Token匹配的文本; 转换为对getText的调用。示例: $ID.text。
type int Token的Token类型(非零正整数),例如INT;转换为对getType的调用。示例:$ID.type。
line int Token出现的行号,从1开始计数;转换为对getLine的调用。示例:$ID.line。
pos int Token的第一个字符发生的行中的字符位置从零开始计数; 转换为调用togetCharPositionInLine。示例: $ID.pos。
index int Token流中该Token的总索引,从零开始计数;转化为调用getTokenIndex。示例:$ID.index。
channel int Token的channel号。解析器只调到一个channel,有效地忽略了off-channel Token。默认通道为0(Token.DEFAULT_Channel),默认隐藏通道为Token.HIDDEN_Channel。转换为对getChannel的调用。示例:$ID.channel。
int int 此Token持有的文本的整数值; 它假定文本是有效的数字字符串。 方便制造计算器等。 转换为Integer.valueOf(Token文本)。 例如: $INT.int。

Parser Rule Attributes

ANTLR预定义了许多与parser rule references关联的只读属性,这些属性可用于action。 action只能访问action之前的引用的rule属性。 Rule名称 r 或分配给Rule引用的Label的语法为 $r.attr。 例如,$expr.text返回与前面调用的rule expr匹配的完整文本:

returnStat : 'return' expr {System.out.println("matched "+$expr.text);} ;

使用rule label如下所示:

returnStat : 'return' e=expr {System.out.println("matched "+e.text);} ;

还可以使用 $ 后跟属性的名称来访问与当前正在执行的rule关联的值。 例如,$start是当前规则的起始标记。

returnStat : 'return' expr {System.out.println("first token "+$start.getText());} ;

对于规则名称$r和规则标签rl$rlrl求值为RContext类型的ParserRuleContext对象。 规则列表标签 rll$rll 求值为List<RContext>$r.attr求值为下表为属性 attr 指定的类型和值:

Attribute Type Description
text String 与规则匹配的文本或从规则开始到“$text”表达式计算点匹配的文本。 请注意,这包括所有标记的文本,包括隐藏通道上的标记,这是您想要的,因为它通常包含所有空格和注释。 当引用当前规则时,此属性可用于任何操作,包括任何异常操作。
start Token 由主token channel上的rule潜在匹配的第一Token;换句话说,该属性永远不是隐藏的Token。 对于最终没有匹配任何Token的规则,此属性指向此规则可能匹配的第一个Token。 当引用当前Rule时,该属性可用于规则内的任何操作。
stop Token 规则匹配的最后一个非隐藏通道Token。 当引用当前规则时,此属性仅对之后和最后操作可用。
ctx ParserRuleContext 与规则调用关联的规则上下文对象。 所有其他属性都可以通过此属性使用。 例如,$ctx.start 访问当前规则上下文对象中的start字段。 它与$start相同。

Dynamically-Scoped Attributes

您可以使用参数和返回值在规则之间传递信息,就像通用编程语言中的函数一样。 但是,编程语言不允许函数访问调用函数的本地变量或参数。 例如,在Java中,从嵌套方法调用中引用局部变量x是非法的:

void f() {
    int x = 0;
    g();
}
void g() {
    h();
}
void h() {
    int y = x; // 对f的局部变量x的引用无效
}

变量 x仅在 f 的范围内可用,f 是用大括号从词法上分隔的文本。 因此,据说Java使用lexical scoping。 Lexical scoping是大多数编程语言的标准。 允许调用链中进一步向下的方法访问先前定义的本地变量的语言被称为使用动态作用域(dynamic scoping)。 术语“动态”指的是编译器不能静态地确定可见变量集的现象。 这是因为方法可见的变量集根据调用该方法的人的不同而不同。

事实证明,在语法领域,遥远的规则有时需要相互通信,主要是为规则调用链中的下面匹配的规则提供上下文信息。 (当然,这假设您直接在语法中使用操作,而不是parse-tree listener event机制。) ANTLR允许动态作用域,因为Action可以使用语法$r::x从调用规则中访问属性,其中r是规则名称,x是该规则中的属性。 程序员必须确保 r 实际上是当前rule的调用rule。 当您访问$r::x时,如果r不在当前调用链中,则会发生运行时异常。

要说明动态作用域的用法,请考虑定义变量和确保定义表达式中的变量的实际问题。 以下语法定义了block规则中属于的symbols属性,但在规则 decl 中添加了变量名称。 规则stat然后查阅列表,查看是否定义了变量。

grammar DynScope;

prog: block ;

block
    /* 本代码块内定义的符号列表*/
    locals [
    List<String> symbols = new ArrayList<String>()
    ]
    : '{' decl* stat+ '}'
    // 打印出代码块中找到的所有符号
    // $block::symbols的计算结果为范围中定义的列表
    {System.out.println("symbols="+$symbols);}
    ;

/** 匹配declaration并将identifier名称添加到符号列表 */
decl: 'int' ID {$block::symbols.add($ID.text);} ';' ;

/** 匹配assignment,然后测试要验证的符号列表
 * 它包含赋值左边的变量。
 * contains()方法为list.contains(),因为$block::symbols
 * 是一个列表。
 */
stat: ID '=' INT ';'
    {
    if ( !$block::symbols.contains($ID.text) ) {
    System.err.println("undefined variable: "+$ID.text);
    }
    }
    | block
    ;

ID : [a-z]+ ;
INT : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ;

下面是一个简单的构建和测试序列:

$ antlr4 DynScope.g4
$ javac DynScope*.java
$ grun DynScope prog
=>  {
=>  int i;
=>  i = 0;
=>  j = 3;
=>  }
=>  EOF
<=  undefined variable: j
    symbols=[i]

@members操作中的简单字段声明和动态作用域之间有一个重要的区别。 symbols是一个局部变量,因此每次调用规则block都有一个副本。 这正是嵌套块所需要的,这样我们就可以在内部block中重用相同的输入变量名。 例如,下面的嵌套代码block在内部作用域中重新定义了i。 这个新定义必须将定义隐藏在外部范围内。

{
    int i;
    int j;
    i = 0;
    {
        int i;
        int x;
        x = 5;
    }
    x = 3;
}

以下是DynScope为该输入生成的输出:

$ grun DynScope prog nested-input
symbols=[i, x]
undefined variable: x
symbols=[i, j]

引用$block::symbols访问最近调用的block的规则上下文对象的symbols字段。 如果您需要从调用链上更远的rule调用中访问symbols实例,则可以从当前上下文 $ctx 开始向后走。 使用getParent向上走。