迷途书童

敏感、勤学、多思
随笔 - 77, 文章 - 4, 评论 - 86, 引用 - 0
数据加载中……

从lex&yacc说到编译器(5.实用javacc)

前言

本系列的文章的宗旨是让大家能够写出自己的编译器,解释器或者脚本引擎,所以每到理论介绍到一个程度后,我都会来讨论实践问题.理论方面,编译原理的教材已经是够多了,而实践的问题却很少讨论.

前几节文章只讨论到了词法分析和LL文法分析,关键的LR文法分析这里却还没有讲,我们先不要管复杂的LR文法和算法,让我们使用LL算法来实际做一些东西后再说.本文将介绍一个在JAVA上广泛使用的LL算法分析工具Javacc.(这是我唯一能找到的使用LL算法的语法分析器构造工具).这一节的文章并非只针对JAVA开发者,如果你是C/C++开发者,那么也请你来看看这个JAVA下的优秀工具,或许你将来也用得着它.

Lex和yacc这两个工具是经典的词法分析和语法分析工具,但是它们都是基于C语言下面的工具,而使用JAVA的朋友们就用不上了.但是JAVA下已经有了lex和yacc的替代品javacc( Java Compiler Compiler ) .同时javacc也是使用LL算法的工具,我们也可以实践一下前面学的LL算法.

首先声明我不是一个JAVA专家,我也是刚刚才接触JAVA.Java里面或许有很多类似javacc一样的工具,但是据我所知,javacc还是最广泛,最标准的JAVA下的词法语法分析器.

Javacc的获取

lex和yacc一样,javacc也是一个免费可以获取的通用工具,它可以在很多JAVA相关的工具下载网站下载,当然,javacc所占的磁盘空间比起lex和yacc更大一些,里面有标准的文档和examples.相对lex和yacc来说,javacc做得更人性化,更容易一些.如果你实在找不到javacc,还是可以联系我,我这里有.现在最新的就是javacc 3.2版本.

Javacc的原理

Javacc可以同时完成对text的词法分析和语法分析的工作,使用起来相当方便.同样,它和lex和yacc一样,先输入一个按照它规定的格式的文件,然后javacc根据你输入的文件来生成相应的词法分析于语法分析程序.同时,新版本的Javacc除了常规的词法分析和语法分析以外,还提供JJTree等工具来帮助我们建立语法树.总之,Javacc在很多地方做得都比lex和yacc要人性化,这个在后面的输入文件格式中也能体现出来.

Javacc的输入文件

Javacc的输入文件格式做得比较简单.每个非终结符产生式对应一个Class中的函数,函数中可以嵌入相应的识别出该终结符文法时候的处理代码(也叫动作).这个与YACC中是一致的.

Javacc的输入文件中,有一系列的系统参数,比如其中lookahead可以设置成大于1的整数,那么就是说,它可以为我们生成LL(k)算法(k>=1),而不是简单的递归下降那样的LL(1)算法了.要知道,LL(2)文法比起前面讨论的LL(1)文法判断每个非终结符时候需要看前面两个记号而不是一个,那么对于文法形式的限制就更少.不过LL(2)的算法当然也比LL(1)算法慢了不少.作为一般的计算机程序设计语言,LL(1)算法已经是足够了.就算不是LL(1)算法,我们也可以通过前面讲的左提公因式把它变成一个LL(1)文法来处理.不过既然javacc都把lookahead选择做出来了,那么在某些特定的情况下,我们可以直接调整一个lookahead的参数就可以,而不必纠正我们的文法.

下面我们来看看Javacc中自带的example中的例子.

5.1

这个例子可以在javacc-3.2/doc/examples/SimpleExamples/Simple1.jj看到

PARSER_BEGIN(Simple1)

public class Simple1 {

public static void main(String args[]) throws ParseException {

    Simple1 parser = new Simple1(System.in);

    parser.Input();

  }

}

PARSER_END(Simple1)

void Input() :

{}

{

  MatchedBraces() ("\n"|"\r")* <EOF>

}

void MatchedBraces() :

{}

{

"{" [ MatchedBraces() ] "}"

}

设置好javacc的bin目录后,在命令提示符下输入

javacc Simple1.jj

然后 javacc 就会为你生成下面几个 java 源代码文件

Simple1.java

Simple1TokenManager.java

Simple1Constants.java

SimpleCharStream.java

Token.java

TokenMgrError.java

其中Simple1就是你的语法分析器的对象,它的构造函数参数就是要分析的输入流,这里的是System.in.

class Simple1就定义在标记 PARSER_BEGIN(Simple1)

PARSER_END(Simple1)之间.

但是必须清楚的是,PARSER_BEGIN和PARSER_END中的名字必须是词法分析器的名字(这里是Simple1).

PARSER_END下面的定义就是文法非终结符号的定义了.

Simple1的文法基本就是:

Input -> MatchedBraces ("\n"|"\r")* <EOF>

MatchedBraces -> { MatchedBraces }

从它的定义我们可以看到 , 每个非终结符号对于一个过程 .

比如 Input 的过程

void Input() :

{}

{

  MatchedBraces() ("\n"|"\r")* <EOF>

}

在定义 void Input 后面记住需要加上一个冒号 ”:”, 然后接下来是两个块 {} 的定义 .

第一个 {} 中的代码是定义数据 , 初试化数据的代码 . 第二个 {} 中的部分就是真正定义 Input 的产生式了 .

每个产生式之间用 ”|” 符号连接 .

注意 : 这里的产生式并非需要严格 BNF 范式文法 , 它的文法既可以是 BNF, 同时还可以是混合了正则表达式中的定义方法 . 比如上面的

Input -> MatchedBraces ("\n"|"\r")* <EOF>

(“\n”|”\r”)* 就是个正则表达式 , 表示的是 \n 或者 \r 0 个到无限个的重复的记号 .

<EOF> javacc 系统定义的记号 (TOKEN), 表示文件结束符号 .

除了 <EOF>, 无论是系统定义的 TOKEN, 还是自定义的 TOKEN, 里面的 TOKEN 都是以 <token’s name> 的方式表示 .

每个非终结符号 (Input MatchedBraces) 都会在 javacc 生成的 Simple1.java 中形成 Class Simple1 的成员函数 . 当你在外部调用 Simple1 Input, 那么语法分析器就会开始进行语法分析了 .

5.2

javacc 提供的 example 里面没有 .javacc 提供的 example 里面提供的例子中 SimpleExamples 过于简单 , 而其它例子又过于庞大 . 下面我以我们最常见的数学四则混合运算的文法来构造一个 javacc 的文法识别器 . 这个例子是我自己写的 , 十分简单 ,. 其中还包括了文法识别同时嵌入的构建语法树 Parse-Tree 的代码 . 不过由于篇幅的原因 , 我并没有给出全部的代码 , 这里只给了 javacc 输入部分相关的代码 . Parse-tree 就是一个普通的 4 叉树 ,3 child,1 next( 平行结点 ), 相信大家在学习数据结构的时候应该都是学过的 . 所以这里就省略过去了 .

在大家看这些输入代码之前 , 我先给出它所使用的文法定义 , 好让大家有个清楚的框架 .

Expression -> Term{ Addop Term }
Addop -> "+" | "-"
Term -> Factor { Mulop Factor }
Mulop -> "*" | "/"
Factor -> ID | NUM | "("Expression")"

这里的文法可能和BNF范式有点不同.{}的意思就是0次到无限次重复,它跟我们在学习正则表达式的时候的”*”符号相同,所以,在Javacc中的文法表示的时候,{…}部分的就是用(…)*来表示.

为了让词法分析做得更简单 , 我们通常都不会在文法分析的时候 , 使用 ”(”,”)“ 等字符号串来表示终结符号 , 而需要转而使用 LPAREN , RPAREN 这样的整型符号来表示 .

PARSER_BEGIN(Grammar)

public class Grammar implements NodeType {

  public ParseTreeNode GetParseTree(InputStream in) throws ParseException

  {

       Grammar parser =new Grammar(in);

       return parser.Expression();

  }

 

}

PARSER_END(Grammar)

SKIP :

{

  " " | "\t" | "\n" | "\r"

}

TOKEN :

{

  < ID: ["a"-"z","A"-"Z","_"] ( ["a"-"z","A"-"Z","_","0"-"9"] )* >

|  < NUM: ( ["0"-"9"] )+ >

|  < PLUS:   "+" >

|  < MINUS:  "-" >

|  < TIMERS: "*" >

|  < OVER:   "/" >

|  < LPAREN: "(" >

|  < RPAREN: ")" >

}

ParseTreeNode Expression() :

{

         ParseTreeNode ParseTree = null;

         ParseTreeNode node;

}

{                

 ( node=Simple_Expression()

 {

    if(ParseTree == null)

             ParseTree =node;

    else

    {

                 ParseTreeNode t;

           t= ParseTree;

                 while(t.next != null)

                  t=t.next;

               t.next = node;

    }

 }

)*

  { return ParseTree;}

  <EOF>

}

ParseTreeNode Simple_Expression() :

{

         ParseTreeNode node;

         ParseTreeNode t;

         int op;

}

{

  node=Term(){}

  (

  op=addop() t=Term()

{

                  ParseTreeNode newNode = new ParseTreeNode();

                  newNode.nodetype = op;

                  newNode.child[0] = node;

                  newNode.child[1] = t;

                  switch(op)

posted on 2006-05-06 16:18 迷途书童 阅读(961) 评论(1)  编辑  收藏 所属分类: 编译原理

评论

# re: 从lex&yacc说到编译器(5.实用javacc)  回复  更多评论   

如果不是写的,请注明转载
如果转载,请转载完整,ok?
2006-08-02 15:52 | fff

只有注册用户登录后才能发表评论。


网站导航: