解析器生成器之 JavaCC(4):如何基于 JavaCC 生成抽象语法树

Author: stormQ

Created: Sunday, 14. March 2021 04:19PM

Last Modified: Tuesday, 23. March 2021 09:55PM



摘要

本文介绍了基于 JavaCC 生成抽象语法树时所使用的action功能,以及用于半自动化生成action和节点类的工具JJTree的使用方法。


概述

JavaCC 中的action功能是指当token序列和语法规则匹配时能够执行任意的 Java 代码。

action可以写在任何地方,即在语法规则的前面、中间和后面都可以。当解析进行到写有action的地方时,action就会被执行。


如何设置和获取终端符号的语义值

1) 设置终端符号的语义值

终端符号的语义值就是Token类的实例。Token类是由 JavaCC 自动生成的。

a) 设置终端符号<IDENTIFIER>的语义值

void name():
{
  Token x;
}
{
  x=<IDENTIFIER>
}

上述语法描述中,通过书写x=<IDENTIFIER>(即Token 类的变量名=<终端符号名>的形式),从而将该终端符号的语义值设置到临时变量x中。

2) 获取终端符号的语义值

Token类中,image字段的值就是终端符号的语义值。

void name():
{
  Token x;
}
{
  x=<IDENTIFIER>
    {
      System.out.println(x.image);
    }
}

上述语法描述中,通过访问Token类实例x中的image字段来获取终端符号的语义值。


如何设置和获取非终端符号的语义值

1) 设置非终端符号的语义值

如果要设置非终端符号的语义值,可以使用return语句从action返回语义值。

a) 设置非终端符号name()的语义值

String name():
{
  Token x;
}
{
  x=<IDENTIFIER>
    {
      return x.image;
    }
}

上述语法描述中,通过在action中返回临时变量x中的image字段的值,从而作为非终端符号name()的语义值。这里返回的语义值的数据类型为String

2) 获取非终端符号的语义值

a) 获取非终端符号name()的语义值

void import_stmt():
{
  String x;
}
{
  <IMPORT> x=name() ("." name())* ";"
}

上述语法描述中,通过x=name()将非终端符号name()的语义值赋值给临时变量x

注意: 由于非终端符号name()的语义值的数据类型为String。因此。临时变量x的数据类型也必须是String


JavaCC 中 action 的使用方法

1) 选择规则中设置 action

a) 选择规则中,为各选项分别设置 action

String expr():
{
  Token x, y;
}
{
  x=<IDENTIFIER>
    {
      return x.image;
    }
|   y=<STRING>
    {
      return y.image;
    }
}

上述语法描述表示,如果是标识符,那么返回标识符的字面值;否则,如果是字符串,那么返回字符串的字面值;否则,为语法错误。

注意: 上述两个action只有一个可能被执行。

b) 选择规则中,为所有选项设置共同的 action

String expr():
{
  Token x;
}
{
  (x=<IDENTIFIER> |   x=<STRING>)
    {
      return x.image;
    }
}

上述语法描述表示,如果是标识符或字符串,那么返回其字面值;否则,为语法错误。

2) 重复规则中设置 action

a) 重复规则中,为每次重复设置 action

void import_stmt():
{
  String x;
}
{
  <IMPORT> name()
  (
    "." x=name() { System.out.println(x); }
  )*
  ";"
}

上述语法描述表示,通过将action写在重复规则的里面,从而每次匹配. name()后都会执行action——System.out.println(x);

b) 重复规则中,仅为最后一次重复设置 action

void import_stmt():
{
  String x = "";
}
{
  <IMPORT> name()
  (
    "." x=name()
  )*
    {
      System.out.println(x);
    }
  ";"
}

上述语法描述表示,通过将action写在重复规则的外面,从而仅在最后一次匹配. name()后才执行action——System.out.println(x);


利用 JJTree 生成抽象语法树

step 1: 编写语法描述文件

1) 修改编译单元的语法规则

将编译单元符号的语义值设置为返回节点对象。修改后的内容如下所示。

SimpleNode compilation_unit():
{}
{
  import_stmts() <EOF>
    {
      return jjtThis;
    }
}

注:SimpleNode类表示节点,是由 JJTree 自动生成的。

2) 生成SimpleNode类,并打印抽象语法树

在解析器的主函数中添加如下内容:

SimpleNode node = parser.compilation_unit();
node.dump("");

3) 语法描述文件示例

options {
  STATIC = false;
}

PARSER_BEGIN(Parser)

import java.io.*;

public class Parser {
  static public void main(String[] args) {
    for (String arg : args) {
      try {
        parser(arg);
      }
      catch (ParseException ex) {
        System.err.println(ex.getMessage());
      }
    }
  }

  static public void parser(String file) throws ParseException {
    try {
      InputStream input = new FileInputStream(file);
      Parser parser = new Parser(input);
      SimpleNode node = parser.compilation_unit();
      node.dump("");
    }
    catch (FileNotFoundException ex) {
      System.err.println(ex.getMessage());
    }
  }
}

PARSER_END(Parser)

SKIP:
{
  <[" ","\t","\n"]>
}

TOKEN: {
    <IMPORT: "import">
}

TOKEN [IGNORE_CASE] :
{
  < IDENTIFIER: ["a"-"z""_"] (["a"-"z","0"-"9","_"])* >
}

SimpleNode compilation_unit():
{}
{
  import_stmts() <EOF>
    {
      return jjtThis;
    }
}

void import_stmts():
{}
{
  (import_stmt())*
}

void import_stmt():
{}
{
  <IMPORT> name() ("." name())* ";"
}

void name():
{}
{
  <IDENTIFIER>
}

step 2: 运行 JJTree

运行 JJTree:

$ jjtree ./CbParser_part.jj

注:JJTree 的输入文件的后缀可以不是.jjt

JJTree 生成的文件如下所示:

CbParser_part.jj.jj  Node.java                 SimpleNode.java
JJTParserState.java  ParserTreeConstants.java

其中,CbParser_part.jj.jj作为后续要使用的语法描述文件。

step 3: 运行解析器

1) 生成解析器

$ javacc ./CbParser_part.jj.jj 
Java Compiler Compiler Version 5.0 (Parser Generator)
(type "javacc" with no arguments for help)
Reading from file ./CbParser_part.jj.jj . . .
File "TokenMgrError.java" does not exist.  Will create one.
File "ParseException.java" does not exist.  Will create one.
File "Token.java" does not exist.  Will create one.
File "SimpleCharStream.java" does not exist.  Will create one.
Parser generated successfully.

2) 编译解析器

$ javac Parser.java

3) 运行解析器

$ java Parser ./hello_part.cb 
compilation_unit
 import_stmts
  import_stmt
   name
  import_stmt
   name
   name

从上面的结果中可以看出,生成了一棵语法树。其形状如下:

          compilation_unit
                  |
            import_stmts
              /       \
   import_stmt        import_stmt
         /             /       \
       name          name     name

hello_part.cb 的内容如下:

import stdio;
import sys.time;

References


下一篇:上一级目录

上一篇:解析器生成器之 JavaCC(3):如何基于 JavaCC 描述解析器

首页