- 浏览: 61875 次
- 性别:
- 来自: 上海
最新评论
-
daizhenze:
我的邮箱daizhenze@126.com
推荐新书《搜索引擎零距离--基于Ruby+Java的搜索引擎原理与实现》 -
daizhenze:
能把SbaleCC的定义代码和语法分析代码发上来吗,谢谢!
推荐新书《搜索引擎零距离--基于Ruby+Java的搜索引擎原理与实现》 -
webcgo:
不是叫IRS吗,与IRPL一样意思吧
时代变迁 -
webcgo:
IRPL开源吗,我们可以使用不?
时代变迁 -
yuankai:
LZ卖书的?
今天看了一眼我的书架,发现清华出版社出的书真是多啊
3.3 词法分析和语法分析[size=large][/size]
为了理解IRS语言编译器的实现原理,我们首先要了解关于词法分析和语法分析的知识。
3.3.1 定义与简介
所谓词法分析,就是把文本中的内容按照一定规则识别为一系列的词语单元。例如,假设我们规定“连续的一串字符叫做一个WORD,句号叫做DOT”,那么,序列I love this game就能够被识别为WORD WORD WORD WORD DOT。
所谓语法分析,就是按照一定的规则把多个连读的个词语单元识别为一系列语义单元。例如,假设我们规定“连续的WORD 跟上一个DOT 叫做一个SENTENSE(句子)”,那么上例中的WORD WORD WORD WORD DOT就被识别为SENTENSE。以一段C语言的源码为例:
int main() {
return 0 ;
}
C语言的词法分析器会把这段代码分解为如下的一些标识符:
"int ", " ", "main ", " (",")","","{","\n ", "\t ",
"return " " ", "0 ", " ", "; ", "\n ", "} ", "\n ", " "
还会把这些标识符归类,在上面的例子中,这些标识符的类别为:
KWINT, SPACE, ID, OPAR, CPAR,
SPACE, OBRACE, SPACE, SPACE, KWRETURN,
SPACE, OCTALCONST, SPACE, SEMICOLON, SPACE,
CBRACE, SPACE, EOF
其中EOF类别的标识符代表源文件的结束。这些标识符被传送给语法分析器。在C语言中,语法分析不需要所有的这些类别,在本例中,SPACE类别的标识符不传送给语法分析器。语法分析器会分析这些标识符,判断程序的结构。在许多编译程序中,语法分析器用语法树来表示程序的结构,而编译器通过这棵树来产生代码。
“词法分析”和“语法分析”是计算机本科专业课程《编译原理》中的两个重要概念,具体可以参考以下两本经典教材:
Compilers Principles :Techniques and Tools(英文版) 中文名:编译原理 技术与工具,就是“龙书”,最经典的编译原理教材。
Compiler Construction - Principles and Practice(英文版) 中文名:编译原理及实践,附tiny编译器的全套源码。
由于手工编写词法分析器和语法分析器通常非常枯燥烦琐而且容易出错,因此在实际项目中“词法分析器”和“语法分析器”都是用一定的工具来完成的。在Java领域中常用的工具有三种。
1.JavaCC
网址: https://javacc.dev.java.net/ Java技术合作网上的JavaCC项目的主页
英文简介:
Java Compiler Compiler [tm] (JavaCC [tm]) is the most popular parser generator for use with Java [tm] applications. A parser generator is a tool that reads a grammar specification and converts it to a Java program that can recognize matches to the grammar. In addition to the parser generator itself, JavaCC provides other standard capabilities related to parser generation such as tree building (via a tool called JJTree included with JavaCC), actions, debugging, etc.
翻译:
JavaCC [tm]是Java领域中最流行的语法分析器生成器。语法分析器生成器指是这样一个工具:它读入语法定义文件并将其转化为能够识别这种语法的Java程序。JavaCC除了是一个“语法分析器生成器”之外,它还提供了“语法树生成”,“语义动作”,“调试”等功能。
3. ANTLR
网址:http://www.antlr.org/ ANTLR项目官网
英文简介:
ANTLR, ANother Tool for Language Recognition, is a language tool that provides a framework for constructing recognizers, interpreters, compilers, and translators from grammatical descriptions containing actions in a variety of target languages. ANTLR provides excellent support for tree construction, tree walking, translation, error recovery, and error reporting.
ANTLR currently generates Java, C#, C++, and Python.
翻译:
ANTLR(ANother Tool for Language Recognition)是这样一个工具:它是从包含语法描述以及语义动作描述的文本中构建多种目标语言上的识别器、解释器、编译器及翻译器的工具。ANTLR对语法树构建、语法树遍历、转换、错误恢复及错误报告等等这些功能提供了出色的支持。
ANTLR当前支持Java、C#、 C++、和 Python等多种语言的代码生成。
3.SableCC
网址:http://sablecc.org/ SableCC项目官网
英文简介:
SableCC is a parser generator which generates fully featured object-oriented frameworks for building compilers, interpreters and other text parsers. In particular, generated frameworks include intuitive strictly-typed abstract syntax trees and tree walkers. SableCC also keeps a clean separation between machine-generated code and user-written code which leads to a shorter development cycle.
翻译:
SableCC是一个分析器生成器,它可以通过生成全特性的、面对对象的框架代码来构建编译器、解释器和其他文本分析器。特别是它可以生成容易理解的、类型严格的抽象语法树和语法树遍历器。SableCC同时也把机器生成的代码和用户手写的代码清楚地分隔开来,以便获得较短的开发周期。
你可以在以下网址看到更多的词法/语法解析器生成工具:
http://www.bearcave.com/software/antlr/antlr_expr.html
由于SableCC的面向对象特征以及易用性,笔者在IRS语言的解释器开发过程中使用了SableCC作为词法、语法分析器生成工具。因此,下文将着重讲述SableCC的方方面面。
3.3.2 SableCC
根据笔者的计算机本科课程的学习经历,众多的专业课程中,最让学生们觉得迷惘的应该就是《编译原理》这门课,迷迷糊糊学了半年,90%的学生都不知道这门课上学的东西怎么与实际应用结合起来。
SableCC作为一个优秀的编译器生成工具,它的使用有比较高的复杂性,出于普及这一优秀工具的目的,笔者选择了一些优秀的英语文章进行翻译,向普通的Java开发者打开一扇通向编译器开发的大门。
3.3.2.1 使用SableCC编写一个编译器
原文网址: http://www.brainycreatures.org/compiler/sablecc.asp
作者联系方式: fidel.viegas@brainycreatures.co.uk
我们要按照以下步骤使用SableCC来编写一个编译器:
(1) 生成一个包含我们设计的目标语言的词法和语法定义的SableCC定义文件。
(2) .生成好这个SableCC定义文件之后,通过在这个定义文件上启动SableCC来生成框架代码。
(3) 本步骤生成工作类。这是我们用JavaTM编写的唯一代码。
(4) 本步骤编写语义分析器、代码生成器,或者可能还有代码优化器。 在这个解释器的例子里,编写一个简单的类。这些工作类可能是由SableCC在上一个步骤中生成的analysis子文件夹中的某个类的子类。在本步骤中,生成编译器的驱动类,它被用来激活词法分析器、语法分析器和工作类。
(5) 最后,使用JavaTM编译器来编译编译器。
为了清楚地展现这个过程,下面按照以上的步骤实现一个Pascal语言的子集
下面开始具体的开始实现工作:SableCC规格文件。
在开始编写Pascal语言子集的SableCC规格定义文件的时候,我们先用BNF来描述一下SableCC的语法
正如我们从语法中看到的,SableCC规格定义文件可以是一个空文件。你是否记得在[和]之间的内容的含义?这代表[和]之间的内容可以为空或者出现一次。
从SableCC规格定义文件所允许的段落来看,我们将不考虑<state declarations>,因为我们不准备使用这个功能。但是如果读者对它的工作原理感兴趣,那么可以看参考文献[1]。现在我们将要描述其他产生式的语法。<package declaration>用来命名目标根目录,也就是所有生成的文件放置的目录。下面按照如下格式声明包名:
Package uk.co.brainycreatures.smallpascal;
在这个例子中,根目录是uk\co\brainycreatures\smallpascal,所有生成的子文件夹将被放在这个目录之下。一个包名可以是一个以字母开头、后面跟上0个或多个字母或数字的、由”.”分隔的标识符。
id = [‘a’ .. ‘z’] ([‘a’ .. ‘z’] | [‘0’ .. ‘9’])*
在这个例子中,我们通过定义digit = [‘0’ .. ‘9’] 和letter = [‘a’ .. ‘z’]来简化这个正则表达式,于是,我们的正则表达式ID变成
id = letter (letter | digit)*
letter和digit是我们的辅助元素,下面是我们如何在SableCC中定义辅助元素:
<helper declarations>后面跟着的是<token declarations>,这段内容里定义终结符。下面用例子来展示如何在SableCC中定义终结符。
这种格式与在正则表达式教程中的正则表达式非常相似。单引号之间的一串字符词语可以定义一个词语单元(Token),每个声明用一个分号结尾。在<token declarations>段后面是<ignored tokens>段, 也就是说,声明的Token这部分会被分析器忽略。例如:注释、空格、和回车等等。
以下是一个例子:
本例中,注释和空格将被分析器忽略。
最后,是<productions>段,我们将为这个语言定义产生式语法。产生式是由BNF或者EBNF定义的。
expression = identifier plus identifier minus number ;
正如我们看到的,它的形式和在语法教程中表述的“上下文无关语法”相似。唯一的不同点是在SableCC中我们不使用”<”和”>”来包围一个非终结符,而却我们用”=”代替了“→”。在产生式段里,有时需要在词语单元(Token)前放一个”T.”,在产生式(production)前放一个”P.”
既然我们已经熟悉SableCC语法了,让我们开始实现我们的Pascal语言子集
我们的包的根是uk.co.brainycreatures.smallpascal,于是在“包声明段”我们将得到:
Package uk.co.brainycreatures.smallpascal;
定义好包之后,我们将如下定义我们的辅助元素:
注意Pascal子集语言关键字定义中的字母之间的空格,在这个例子中我们需要他们,因为每个字符都是一个辅助元素(Helpers)单元,因此,在定义Token的时候,我们有一个由空格分隔的Helpers序列。
最后,描述这个语言的语法,如下:
生成工作类:
现在为这个语言实现语义分析和代码生成,先按如下方式实现语义分析。
1. 语义分析器
语义分析器处理语法分析器传递给她的语句,通过确保语句是否有有意义,完成源码的静态分析阶段。例如,Pascal赋值语句的语法没有要求标识符是已声明过的、是变量、属于类似的类型、是否可以在这个类型上操作。所有这些约束是由Pascal的静态语义定义的,由语义分析器来执行相关的检查。为了执行这些检查,语义分析器完整的处理所有声明,并产生对应的信息(属性信息)。这些信息被存储在称为“符号表”的数据结构里。符号表被用来提供与标识符相关的信息。
对于这个小的Pascal语言子集,它所需要进行的唯一的标识符检查是标识符在使用之前必须声明过,而且只能声明一次。不需要检查类型,因为所有标识符是相同类型的。语义分析过程是通过深度优先遍历抽象语法树来完成的,当访问过包含变量声明的产生式子树之后,这个过程把标识符的信息存储在符号表里。当我们遇到因子和变量等等标识符的产生式的时候,将用到符号表里的信息。
既然已经有了实现这个语义分析器所需的信息,现在讲述如何实现它。回到之前提到的语法,现在要解释每条产生式的替换式中{和}之间的名字的作用。语法的产生式中每条替换式的名字有一前缀A并且跟着一个产生式的名字,这样就为这条替换式构造了一个类型名。。例如,对于下面的产生式
Factor = {identifier} identifier | {integer} integer_literal | … ;
一个命名为AIdentifierFactor和一个命名为AIntegerFactor的类型将被生成。
在获得以上信息的前提下,通过定义替换式和标识符上的方法来设计语义分析器。也就是,替换式AsSingleIdentifierList、ASequenceIdentifierList、AAssignStatement 和 AIdentifierFactor。并且,我们也需要处理替换式 AVarDecl。需要注意, var_decl的替换式没有名字,在SableCC中,在以下条件下,这是允许的:如果如果产生式只由一个替换式定义,并且结果类型是以A为前缀的产生式的名字。下面是语义分析器的实现:
通过扩展DepthFirstAdapter(深度优先访问类)来实现SemanticAnalyser类。
DepthFirstAdapter类自动提供了对AST(抽象语法树)的深度优先访问。为了检查标识符的声明,我们只需要outASingleIdentifierList、outASequenceIdentifierList方法。为了检查标识符在使用前是否已经被声明过,我们只需要outAAssignStatement和outAIdentifierFactor方法。想更详细的了解SableCC如何生成方法,请参考文献[1]的第5、6章。现在我们已经完成了语义分析器。除了代码生成之外,我们还需要处理符号表里的标识符。
2. 类生成器
类生成器也是类DepthFirstAdapter的子类。为了实现代码生成器,我们需要对语法树做深度优先遍历,正如我们实现语义分析器时所做的一样。但是对于代码生成来说,我们也会使用其他产生式中的替换式。例如,如果程序中出现program heading的话,就使用跟在program关键字后面的标识符的名字作为类名。也就是说,如果程序中有program heading的话,就使用跟在program关键字后的标识符的名字作为类名。如果程序中没有出现program heading,输出的类名是a.out, 这个基于Unix的系统上的C编译器的执行结果相同。下面开始描述我们的代码生成器的设计。比如,从产生式开始:
program_heading =
{non_empty} T.program identifier semicolon |
{empty} ;
我们将要定义方法outANonEmptyProgramHeading 和outAEmptyProgramHeading。为了使用Jas API来生成class文件,我们需要先生成一个类ClassEnv的实例,然后我们设置类属性:类名、父类、源代码和访问控制标识。这些都在上面说明过的方法里做好了。我们还需要生成一个默认构造函数。尽管我们不实现,Java编译器为我们生成了一个默认构造函数。以下是Java汇编代码看上去的样子:
.method public <init>()V
aload_0
invokevirtual java⁄lang⁄Object⁄<init>()V
return
.end method
我们可以在program_heading的产生式或者在program产生式遍历子树之前进行方法的定义――这取决于程序员的偏好。在我们的例子中,将要在方法inAProgram定义这个构造函数:
这些代码是很浅显的,所以我们不做过多解释。
方法outAEmptyProgramHeading的代码和上面的代码相似,除了类名之外。它的类名是a.out。继续处理语法,现在要定义处理标识符的代码,也就是定义标识符的代码。Pascal变量将被定义为Java字段。为了使用Jas API定义字段,我们使用类ClassEnv上的addField方法。生成后的变量名应该是小写。我们使用它所在类的类名而不是使用一个符号表来获取字段的签名。以下是实现:
以上的代码等于以下的Java声明:
public static int <var_name> = 0;
其中的<var_name>与所声明的变量名相同。方法outASequenceIdentifierList的代码和上面outASingleIdentifierList的代码相同。因此不需要再次描述它。
现在来描述如何实现“语句”(statements)。在进一步表述之前,先分析一下如何把Pascal语句转化为Java汇编码。因为Java虚拟机是基于堆栈的,我们需要把所有的有名的因子压栈,并且弹出结果,放入左面的变量。例如,赋值语句:
a := b + c
将被翻译成如下的java汇编代码:
getstatic <class name>⁄b I
getstatic <class name>⁄c I
iadd
putstatic <class name>⁄a I
<class name>是字段a、b和c所在的类的类名。在Java里,使用.,但是在Java汇编代码里,使用/来访问方法或者字段。变量名后面的I代表这个变量是int类型的。指令getstatic被用来把值存储进类的静态字段。另外将被用在这个语言子集代码生成过程的算术指令是:
isub –整数相减。这条执行期望在栈顶找到两个整数,如果找到的话,弹出他们,把他们相减,然后把结果压到栈顶。imul –与isub相同,但是不是相减操作而是相乘操作。idiv –用来整数相除
为了生成合适的代码,需要遍历相关操作符的语法子树,访问他们之后再针对操作符来生成相关的操作码。在乘积因子中遇到数字和标识符的时候,就需要把它们压栈,然后生成乘积表达式和项的算术指令的操作符。下面是实现:
3. 主类
既然已经实现了工作类,现在要来实现主类。这个类简单地激活词法分析器、语法分析器、语义分析器和类生成器 :
既然已经实现了每个功能,现在该编译所有代码了。假设当前工作目录是我们的CLASSPATH,我们可以按如下编译这个编译器:
C:\mycompilers\javac uk\co\brainycreatures\Main.java
为了理解IRS语言编译器的实现原理,我们首先要了解关于词法分析和语法分析的知识。
3.3.1 定义与简介
所谓词法分析,就是把文本中的内容按照一定规则识别为一系列的词语单元。例如,假设我们规定“连续的一串字符叫做一个WORD,句号叫做DOT”,那么,序列I love this game就能够被识别为WORD WORD WORD WORD DOT。
所谓语法分析,就是按照一定的规则把多个连读的个词语单元识别为一系列语义单元。例如,假设我们规定“连续的WORD 跟上一个DOT 叫做一个SENTENSE(句子)”,那么上例中的WORD WORD WORD WORD DOT就被识别为SENTENSE。以一段C语言的源码为例:
int main() {
return 0 ;
}
C语言的词法分析器会把这段代码分解为如下的一些标识符:
"int ", " ", "main ", " (",")","","{","\n ", "\t ",
"return " " ", "0 ", " ", "; ", "\n ", "} ", "\n ", " "
还会把这些标识符归类,在上面的例子中,这些标识符的类别为:
KWINT, SPACE, ID, OPAR, CPAR,
SPACE, OBRACE, SPACE, SPACE, KWRETURN,
SPACE, OCTALCONST, SPACE, SEMICOLON, SPACE,
CBRACE, SPACE, EOF
其中EOF类别的标识符代表源文件的结束。这些标识符被传送给语法分析器。在C语言中,语法分析不需要所有的这些类别,在本例中,SPACE类别的标识符不传送给语法分析器。语法分析器会分析这些标识符,判断程序的结构。在许多编译程序中,语法分析器用语法树来表示程序的结构,而编译器通过这棵树来产生代码。
“词法分析”和“语法分析”是计算机本科专业课程《编译原理》中的两个重要概念,具体可以参考以下两本经典教材:
Compilers Principles :Techniques and Tools(英文版) 中文名:编译原理 技术与工具,就是“龙书”,最经典的编译原理教材。
Compiler Construction - Principles and Practice(英文版) 中文名:编译原理及实践,附tiny编译器的全套源码。
由于手工编写词法分析器和语法分析器通常非常枯燥烦琐而且容易出错,因此在实际项目中“词法分析器”和“语法分析器”都是用一定的工具来完成的。在Java领域中常用的工具有三种。
1.JavaCC
网址: https://javacc.dev.java.net/ Java技术合作网上的JavaCC项目的主页
英文简介:
Java Compiler Compiler [tm] (JavaCC [tm]) is the most popular parser generator for use with Java [tm] applications. A parser generator is a tool that reads a grammar specification and converts it to a Java program that can recognize matches to the grammar. In addition to the parser generator itself, JavaCC provides other standard capabilities related to parser generation such as tree building (via a tool called JJTree included with JavaCC), actions, debugging, etc.
翻译:
JavaCC [tm]是Java领域中最流行的语法分析器生成器。语法分析器生成器指是这样一个工具:它读入语法定义文件并将其转化为能够识别这种语法的Java程序。JavaCC除了是一个“语法分析器生成器”之外,它还提供了“语法树生成”,“语义动作”,“调试”等功能。
3. ANTLR
网址:http://www.antlr.org/ ANTLR项目官网
英文简介:
ANTLR, ANother Tool for Language Recognition, is a language tool that provides a framework for constructing recognizers, interpreters, compilers, and translators from grammatical descriptions containing actions in a variety of target languages. ANTLR provides excellent support for tree construction, tree walking, translation, error recovery, and error reporting.
ANTLR currently generates Java, C#, C++, and Python.
翻译:
ANTLR(ANother Tool for Language Recognition)是这样一个工具:它是从包含语法描述以及语义动作描述的文本中构建多种目标语言上的识别器、解释器、编译器及翻译器的工具。ANTLR对语法树构建、语法树遍历、转换、错误恢复及错误报告等等这些功能提供了出色的支持。
ANTLR当前支持Java、C#、 C++、和 Python等多种语言的代码生成。
3.SableCC
网址:http://sablecc.org/ SableCC项目官网
英文简介:
SableCC is a parser generator which generates fully featured object-oriented frameworks for building compilers, interpreters and other text parsers. In particular, generated frameworks include intuitive strictly-typed abstract syntax trees and tree walkers. SableCC also keeps a clean separation between machine-generated code and user-written code which leads to a shorter development cycle.
翻译:
SableCC是一个分析器生成器,它可以通过生成全特性的、面对对象的框架代码来构建编译器、解释器和其他文本分析器。特别是它可以生成容易理解的、类型严格的抽象语法树和语法树遍历器。SableCC同时也把机器生成的代码和用户手写的代码清楚地分隔开来,以便获得较短的开发周期。
你可以在以下网址看到更多的词法/语法解析器生成工具:
http://www.bearcave.com/software/antlr/antlr_expr.html
由于SableCC的面向对象特征以及易用性,笔者在IRS语言的解释器开发过程中使用了SableCC作为词法、语法分析器生成工具。因此,下文将着重讲述SableCC的方方面面。
3.3.2 SableCC
根据笔者的计算机本科课程的学习经历,众多的专业课程中,最让学生们觉得迷惘的应该就是《编译原理》这门课,迷迷糊糊学了半年,90%的学生都不知道这门课上学的东西怎么与实际应用结合起来。
SableCC作为一个优秀的编译器生成工具,它的使用有比较高的复杂性,出于普及这一优秀工具的目的,笔者选择了一些优秀的英语文章进行翻译,向普通的Java开发者打开一扇通向编译器开发的大门。
3.3.2.1 使用SableCC编写一个编译器
原文网址: http://www.brainycreatures.org/compiler/sablecc.asp
作者联系方式: fidel.viegas@brainycreatures.co.uk
我们要按照以下步骤使用SableCC来编写一个编译器:
(1) 生成一个包含我们设计的目标语言的词法和语法定义的SableCC定义文件。
(2) .生成好这个SableCC定义文件之后,通过在这个定义文件上启动SableCC来生成框架代码。
(3) 本步骤生成工作类。这是我们用JavaTM编写的唯一代码。
(4) 本步骤编写语义分析器、代码生成器,或者可能还有代码优化器。 在这个解释器的例子里,编写一个简单的类。这些工作类可能是由SableCC在上一个步骤中生成的analysis子文件夹中的某个类的子类。在本步骤中,生成编译器的驱动类,它被用来激活词法分析器、语法分析器和工作类。
(5) 最后,使用JavaTM编译器来编译编译器。
为了清楚地展现这个过程,下面按照以上的步骤实现一个Pascal语言的子集
下面开始具体的开始实现工作:SableCC规格文件。
在开始编写Pascal语言子集的SableCC规格定义文件的时候,我们先用BNF来描述一下SableCC的语法
<grammar> → [<package declaration>] [<helper declarations>] [<states declarations>] [<token declarations>] [<ignored tokes>] [<productions>]
正如我们从语法中看到的,SableCC规格定义文件可以是一个空文件。你是否记得在[和]之间的内容的含义?这代表[和]之间的内容可以为空或者出现一次。
从SableCC规格定义文件所允许的段落来看,我们将不考虑<state declarations>,因为我们不准备使用这个功能。但是如果读者对它的工作原理感兴趣,那么可以看参考文献[1]。现在我们将要描述其他产生式的语法。<package declaration>用来命名目标根目录,也就是所有生成的文件放置的目录。下面按照如下格式声明包名:
Package uk.co.brainycreatures.smallpascal;
在这个例子中,根目录是uk\co\brainycreatures\smallpascal,所有生成的子文件夹将被放在这个目录之下。一个包名可以是一个以字母开头、后面跟上0个或多个字母或数字的、由”.”分隔的标识符。
id = [‘a’ .. ‘z’] ([‘a’ .. ‘z’] | [‘0’ .. ‘9’])*
在这个例子中,我们通过定义digit = [‘0’ .. ‘9’] 和letter = [‘a’ .. ‘z’]来简化这个正则表达式,于是,我们的正则表达式ID变成
id = letter (letter | digit)*
letter和digit是我们的辅助元素,下面是我们如何在SableCC中定义辅助元素:
Helpers letter = [‘a’ .. ‘z’]; digit = [‘0’ .. ‘9’];
<helper declarations>后面跟着的是<token declarations>,这段内容里定义终结符。下面用例子来展示如何在SableCC中定义终结符。
Tokens id = letter (letter | digit)*;
这种格式与在正则表达式教程中的正则表达式非常相似。单引号之间的一串字符词语可以定义一个词语单元(Token),每个声明用一个分号结尾。在<token declarations>段后面是<ignored tokens>段, 也就是说,声明的Token这部分会被分析器忽略。例如:注释、空格、和回车等等。
以下是一个例子:
Helpers any_charater = [0x0 .. 0xfffff]; nl = '\n '; Tokens comment = '⁄⁄ ' any_character nl blank = 10 | 10 13 | 9; Ignored Tokens comment, blank;
本例中,注释和空格将被分析器忽略。
最后,是<productions>段,我们将为这个语言定义产生式语法。产生式是由BNF或者EBNF定义的。
Tokens identifier = letter (letter | digit)*; number = digit+; plus = ‘+’; Productions
expression = identifier plus identifier minus number ;
正如我们看到的,它的形式和在语法教程中表述的“上下文无关语法”相似。唯一的不同点是在SableCC中我们不使用”<”和”>”来包围一个非终结符,而却我们用”=”代替了“→”。在产生式段里,有时需要在词语单元(Token)前放一个”T.”,在产生式(production)前放一个”P.”
Tokens program = ‘program’; semicolon = ‘;’ ; Productions program = T.program identifier semicolon ;
既然我们已经熟悉SableCC语法了,让我们开始实现我们的Pascal语言子集
我们的包的根是uk.co.brainycreatures.smallpascal,于是在“包声明段”我们将得到:
Package uk.co.brainycreatures.smallpascal;
定义好包之后,我们将如下定义我们的辅助元素:
Helpers a = 'a '| 'A '; b = 'b ' | 'B '; e = 'e '| 'E '; g = 'g '| 'G '; i = 'i ' | 'I '; m = 'm ' | 'M '; n = 'n ' | 'N '; o = 'o ' | 'O '; p = 'p '| ' P '; r = 'r ' | 'R '; t = 't '| 'T '; v = 'v ' | 'V '; w = 'w' | 'W '; cr = 13 ; ⁄⁄ carriage return lf = 10 ; ⁄⁄ line feed tab = 9 ; ⁄⁄ tab char ascii_char = [32 .. 127] ; blank = ‘’ ; digit = ['0 ' .. '9 '] ; letter = [['a '.. 'z '] + ['A ' .. 'Z ']] ; l_brace = '{ ' ; r_brace = '} ' ; l_paren_star = ' (*'; r_paren_star = '*) ';
注意Pascal子集语言关键字定义中的字母之间的空格,在这个例子中我们需要他们,因为每个字符都是一个辅助元素(Helpers)单元,因此,在定义Token的时候,我们有一个由空格分隔的Helpers序列。
最后,描述这个语言的语法,如下:
Productions program = program_heading global_declaration_part begin main_statement_part end dot; program_heading = {non_empty} program identifier semicolon | {empty} ; global_declaration_part = var_declaration; var_declaration = var var_decl+; var_decl = identifier_list colon type; identifier_list = {single} identifier | {sequence} identifier_list comma identifier; type = integer; main_statement_part = statement_sequence; statement_sequence = {single} statement | {sequence} statement_sequence semicolon statement ; statement = {assign} identifier assignop expression | {writeln} writeln_stmt ; writeln_stmt = {simple}writeln | {arguments} writeln l_paren expression_list r_paren ; expression_list = {single} expression | {sequence} expression_list comma expression ; expression = {term} term | {plus} expression plus term | {minus} expression minus term ; term = {factor} factor | {mult} term mult factor | {div} term div factor; factor = {identifier} identifier | {integer} integer;
生成工作类:
现在为这个语言实现语义分析和代码生成,先按如下方式实现语义分析。
1. 语义分析器
语义分析器处理语法分析器传递给她的语句,通过确保语句是否有有意义,完成源码的静态分析阶段。例如,Pascal赋值语句的语法没有要求标识符是已声明过的、是变量、属于类似的类型、是否可以在这个类型上操作。所有这些约束是由Pascal的静态语义定义的,由语义分析器来执行相关的检查。为了执行这些检查,语义分析器完整的处理所有声明,并产生对应的信息(属性信息)。这些信息被存储在称为“符号表”的数据结构里。符号表被用来提供与标识符相关的信息。
对于这个小的Pascal语言子集,它所需要进行的唯一的标识符检查是标识符在使用之前必须声明过,而且只能声明一次。不需要检查类型,因为所有标识符是相同类型的。语义分析过程是通过深度优先遍历抽象语法树来完成的,当访问过包含变量声明的产生式子树之后,这个过程把标识符的信息存储在符号表里。当我们遇到因子和变量等等标识符的产生式的时候,将用到符号表里的信息。
既然已经有了实现这个语义分析器所需的信息,现在讲述如何实现它。回到之前提到的语法,现在要解释每条产生式的替换式中{和}之间的名字的作用。语法的产生式中每条替换式的名字有一前缀A并且跟着一个产生式的名字,这样就为这条替换式构造了一个类型名。。例如,对于下面的产生式
Factor = {identifier} identifier | {integer} integer_literal | … ;
一个命名为AIdentifierFactor和一个命名为AIntegerFactor的类型将被生成。
在获得以上信息的前提下,通过定义替换式和标识符上的方法来设计语义分析器。也就是,替换式AsSingleIdentifierList、ASequenceIdentifierList、AAssignStatement 和 AIdentifierFactor。并且,我们也需要处理替换式 AVarDecl。需要注意, var_decl的替换式没有名字,在SableCC中,在以下条件下,这是允许的:如果如果产生式只由一个替换式定义,并且结果类型是以A为前缀的产生式的名字。下面是语义分析器的实现:
package uk.co.brainycreatures.smallpascal; import uk.co.brainycreatures.smallpascal.node.*; import uk.co.brainycreatures.smallpascal.analysis.*; import java.util.*; ⁄⁄ for the Hashtable public class SemanticAnalyzer extends DepthFirstAdapter { ⁄⁄ stores the identifiers being defined Hashtable symbol_table = new Hashtable(); ⁄⁄ create a new table ⁄*检查标识符是否已经在符号表里了 *check if the identifier is already in the table and report an errorif it is*/ public void outASingleIdentifierList(AidentifierList node) { ⁄⁄ identifier to be stored in the symbol table TIdentifier ident = node.getIdentifier(); ⁄⁄ name of the identifier to be stored in the table String key = ident.getText().toUpperCase(); ⁄⁄ is the identifier in the table? if (symbol_table.containsKey(key)) { ⁄⁄ report an error System.out.println(“Identifier already defined.”); System.exit(0); } else { symbol_table.put(key, key); } } public void outASingleIdentifierList(AidentifierList node) { ⁄⁄ identifier to be stored in the symbol table TIdentifier ident = node.getIdentifier(); ⁄⁄ name of the identifier to be stored in the table String key = ident.getText().toUpperCase(); ⁄⁄ is the identifier in the table? if (symbol_table.containsKey(key)) { ⁄ report an error System.out.println("Error: ["ident.getLine() + "," + ident.getPos() + "] Identifier already defined. "); System.exit(0); } else { symbol_table.put(key, key); } } ⁄*检查赋值语句中的标识符是否已经声明过,如果没有的话就报错 checks if the identifier in the assignment statement was previously declared, and report an error if it wasn’t*/ public void outAAssignStatement(AAssignStatement node) { Tidentifier ident = node.getIdentifier(); String key = ident.getText().toUpperCase(); ⁄⁄ Is the identifier in the table? ⁄⁄ if not report error if (!symbol_table.containsKey(key)) { System.out.println("Error: [" + ident.getLine() + "," ident.getPos() + "] Unknown identifier. "); System.exit(0); } } public void outAIdentifierFactor(AIdentifierFactor node) { Tidentifier ident = node.getIdentifier(); String key = ident.getText().toUpperCase(); ⁄⁄ Is the identifier in the table? ⁄⁄ if not report error if (!symbol_table.containsKey(key)) { System.out.println("Error: [" + ident.getLine() + "," + ident.getPos() + "] Unknown identifier. "); System.exit(0); } } }
通过扩展DepthFirstAdapter(深度优先访问类)来实现SemanticAnalyser类。
DepthFirstAdapter类自动提供了对AST(抽象语法树)的深度优先访问。为了检查标识符的声明,我们只需要outASingleIdentifierList、outASequenceIdentifierList方法。为了检查标识符在使用前是否已经被声明过,我们只需要outAAssignStatement和outAIdentifierFactor方法。想更详细的了解SableCC如何生成方法,请参考文献[1]的第5、6章。现在我们已经完成了语义分析器。除了代码生成之外,我们还需要处理符号表里的标识符。
2. 类生成器
类生成器也是类DepthFirstAdapter的子类。为了实现代码生成器,我们需要对语法树做深度优先遍历,正如我们实现语义分析器时所做的一样。但是对于代码生成来说,我们也会使用其他产生式中的替换式。例如,如果程序中出现program heading的话,就使用跟在program关键字后面的标识符的名字作为类名。也就是说,如果程序中有program heading的话,就使用跟在program关键字后的标识符的名字作为类名。如果程序中没有出现program heading,输出的类名是a.out, 这个基于Unix的系统上的C编译器的执行结果相同。下面开始描述我们的代码生成器的设计。比如,从产生式开始:
program_heading =
{non_empty} T.program identifier semicolon |
{empty} ;
我们将要定义方法outANonEmptyProgramHeading 和outAEmptyProgramHeading。为了使用Jas API来生成class文件,我们需要先生成一个类ClassEnv的实例,然后我们设置类属性:类名、父类、源代码和访问控制标识。这些都在上面说明过的方法里做好了。我们还需要生成一个默认构造函数。尽管我们不实现,Java编译器为我们生成了一个默认构造函数。以下是Java汇编代码看上去的样子:
.method public <init>()V
aload_0
invokevirtual java⁄lang⁄Object⁄<init>()V
return
.end method
我们可以在program_heading的产生式或者在program产生式遍历子树之前进行方法的定义――这取决于程序员的偏好。在我们的例子中,将要在方法inAProgram定义这个构造函数:
public void inAProgram(AProgram node) { ⁄⁄ create an new instance of CodeAttr init = new CodeAttr(); main_code = new CodeAttr(); ⁄⁄ create a new object for main program body try { ⁄⁄ define the method init.addInsn(new Insn(opc_aload_0)); init.addInsn(new Insn(opc_invokevirtual, new MethodCP(" java/lang/Object ", " <init> ", " ()V "))); init.addInsn(new Insn(opc_return)); } catch (Exception e) { System.out.println(e); } } 定义好这个方法之后,我们需要把它加到类上去,这个工作可以在outAEmptyProgramHeading方法或者outANonEmptyProgramHeading方法里进行。以下是实现: public void outANonEmptyProgramHeading(ANonEmptyProgramHeading node) { ⁄⁄ name of the class to be generated class_name = node.getIdentifier().getText(); try { ⁄⁄ set class attributes main_class.setClass((new ClassCP(class_name)); main_class.setSuper(new ClassCP(“java/lang/Object”)); ⁄⁄ source from which it is compiled main_class.setSource(new SourceAttr(source_file_name)); ⁄⁄ make class public main_class.setClassAccess((short) ACC_PUBLIC); ⁄⁄ add the constructor method to the class main_class.addMethod((short) ACC_PUBLIC, , “<init>”, “()V”, init, null); } catch (Exception e) { System.out.println(e); } }
这些代码是很浅显的,所以我们不做过多解释。
方法outAEmptyProgramHeading的代码和上面的代码相似,除了类名之外。它的类名是a.out。继续处理语法,现在要定义处理标识符的代码,也就是定义标识符的代码。Pascal变量将被定义为Java字段。为了使用Jas API定义字段,我们使用类ClassEnv上的addField方法。生成后的变量名应该是小写。我们使用它所在类的类名而不是使用一个符号表来获取字段的签名。以下是实现:
public void outASingleIdentifierList(AsingleIdentifierList node) { ⁄⁄ get the name of the name of the variable in lower case String var_name = node.getIdentifier().getText().toLowerCase(); try { ⁄⁄ add the field to the class main_class.addField(new Var((short) (ACC_STATIC | ACC_PUBLIC)), new AsciiCP(var_name), new AsciiCP(“I”), new ConstAttr(new IntegerCP(0))))); } catch (Exception e) { System.out.println(e); } }
以上的代码等于以下的Java声明:
public static int <var_name> = 0;
其中的<var_name>与所声明的变量名相同。方法outASequenceIdentifierList的代码和上面outASingleIdentifierList的代码相同。因此不需要再次描述它。
现在来描述如何实现“语句”(statements)。在进一步表述之前,先分析一下如何把Pascal语句转化为Java汇编码。因为Java虚拟机是基于堆栈的,我们需要把所有的有名的因子压栈,并且弹出结果,放入左面的变量。例如,赋值语句:
a := b + c
将被翻译成如下的java汇编代码:
getstatic <class name>⁄b I
getstatic <class name>⁄c I
iadd
putstatic <class name>⁄a I
<class name>是字段a、b和c所在的类的类名。在Java里,使用.,但是在Java汇编代码里,使用/来访问方法或者字段。变量名后面的I代表这个变量是int类型的。指令getstatic被用来把值存储进类的静态字段。另外将被用在这个语言子集代码生成过程的算术指令是:
isub –整数相减。这条执行期望在栈顶找到两个整数,如果找到的话,弹出他们,把他们相减,然后把结果压到栈顶。imul –与isub相同,但是不是相减操作而是相乘操作。idiv –用来整数相除
为了生成合适的代码,需要遍历相关操作符的语法子树,访问他们之后再针对操作符来生成相关的操作码。在乘积因子中遇到数字和标识符的时候,就需要把它们压栈,然后生成乘积表达式和项的算术指令的操作符。下面是实现:
public void caseAIdentifierFactor(AidentifierFactor node) { String var_name = node.getIdentifier().getText().toLowerCase(); try { ⁄⁄ getstatic <class_name>⁄<var_name> I code.addInsn(new Insn(opc_getstatic, new FieldCP(class_name, var_name, " I "))); } catch (Exception e) { System.out.println(e); } } (1) 整数因子的代码 public void caseAIntegerFactor(AIntegerFactor node) { ⁄⁄ get the string value String num_image = node.getIdentifier().getText(); int value = Integer.parseInt(num_image); try { switch(value) { case –1 : code.addInsn(new Insn(opc_iconst_m1)); break; case 0: code.addInsn(new Insn(opc_iconst_0)); break; case 1 : code.addInsn(new Insn(opc_iconst_1)); break; case 2: code.addInsn(new Insn(opc_iconst_2)); break; case 3: code.addInsn(new Insn(opc_iconst_3)); break; case 4: code.addInsn(new Insn(opc_iconst_4)); break; case 5: code.addInsn(new Insn(opc_iconst_5)); break; default: if (value => –128 && value <= 127) { code.addInsn(new Insn(opc_bipush, value)); } else if (value >= –65536 && value <= 65535) { code.addInsn(new Insn(opc_sipush, value)); } else { code.addInsn(new Insn(opc_ldc, value)); } break; } } } (2) 加操作符的代码 public void outAPlusExpression(APlusExpression node) { try { code.addInsn(new Insn(opc_iadd)); } catch (Exception e) { System.out.println(e); } } (3) 减操作符的代码 public void outAMinusExpression(AMinusExpression node) { try { code.addInsn(new Insn(opc_isub)); } catch (Exception e) { System.out.println(e); } } (4) 乘操作符的代码 public void outAMultTerm(AMultTerm node) { try { code.addInsn(new Insn(opc_imul)); } catch (Exception e) { System.out.println(e); } } (5) 除操作符的代码 public void outADivTerm(ADivTerm node) { try { code.addInsn(new Insn(opc_idiv)); } catch (Exception e) { System.out.println(e); } } (6) 赋值语句的代码 public void outAAssignStatement(AassignStatement node) { String var_name = node.getIdentifier().getText().toLowerCase(); try { code.addInsn(new Insn(opc_putstatic, new FieldCP(class_name, var_name, “I”))); } catch (Exception e) { System.out.println(e); } } (7) writeln语句的代码 public void inAWritelnStatement(AwritelnStatement node) { try { code.addInsn(new Insn(opc_getstatic, new FieldCP(“java/lang/Object”, “out”, “Ljava/io/PrintStream;”))); } catch (Exception e) { System.out.println(e); } } public void outAWritelnStatement(AwritelnStatement node) { try { code.addInsn(new Insn(opc_invokevirtual, new MethodCP(“java/io/PrintStream”, “println”, “(I)V”))); } catch (Exception e) { System.out.println(e); } } 生成所有代码之后,加上额外的返回指令。把这个方法加到类上,然后输出这个类。这些工作在方法outAProgram里进行,这里就是退出程序的地方。以下是代码: public void outAProgram(AProgram node) { try { ⁄⁄ add return instruction to the end of the method code.addInsn(new Insn(opc_return); ⁄⁄ add main method to main_class main_class.addMethod((short)( ACC_PUBLIC | ACC_STATIC), “main”, “([Ljava/lang/String;)V”, code, null)); ⁄⁄ generate class file main_class.write(new DataOutputStream(new FileOutputStream(class_name + “.class”))); ⁄⁄ output status System.out.println(“Wrote “ + class_name + “.class”]”); } catch (Exception e) { System.out.println(e); } }
3. 主类
既然已经实现了工作类,现在要来实现主类。这个类简单地激活词法分析器、语法分析器、语义分析器和类生成器 :
package uk.co.brainycreatures.smallpascal; import uk.co.brainycreatures.smallpascal.semantic.SemanticAnalyser; import uk.co.brainycreatures.smallpascal.code.ClassGenerator; import uk.co.brainycreatures.smallpascal.parser.*; import uk.co.brainycreatures.smallpascal.lexer.*; import uk.co.brainycreatures.smallpascal.node.*; import java.io.*; public class Main { public static void main(String[] args) { long start_time, stop_time; ⁄⁄ times compilation if (args.length < 1) { System.out.println(“Usage:”); System.out.println(“ java uk.co.brainycreatures.smallpascal.Main <filename>”); } try { start_time = System.currentTimeMillis(); ⁄⁄ create lexer Lexer lexer = new Lexer (new PushbackReader(new BufferedReader(new FileReader(args[0])), 1024)); ⁄⁄ parser program Parser parser = new Parser(lexer); Start ast = parser.parse(); ⁄⁄ check program semantics ast.apply(new SemanticAnalyser()); ⁄⁄ generate class file ast.apply(new ClassGenerator()); } catch (Exception e) { System.out.println(e); } } }
既然已经实现了每个功能,现在该编译所有代码了。假设当前工作目录是我们的CLASSPATH,我们可以按如下编译这个编译器:
C:\mycompilers\javac uk\co\brainycreatures\Main.java
相关推荐
根据给定的文件信息,我们可以深入探讨IRS2453DPBF这款自振荡全桥驱动集成电路的关键特性、工作原理以及应用领域。 ### IRS2453DPBF概述 IRS2453DPBF是一款高性能的自振荡全桥驱动集成电路,基于广受欢迎的IR2153...
4. **设计与实现**:可能涉及IRS的物理结构设计、材料选择、以及实现低成本大规模集成的方法。 5. **优化算法**:介绍用于优化IRS相位配置的数学模型和算法,例如基于机器学习的方法,以实现最佳的无线通信性能。 ...
辐射定标是IRS数据预处理的第一步,其目的是将原始传感器记录转换为物理意义明确的地表反射率或发射率。这一步骤对于后续的数据分析至关重要。 **扩展工具定标:** 1. **下载并安装扩展工具:** - 下载链接:...
基于输入电压转换电路、正弦波发生电路和信号放大电路的原理设计了系统总体框架,重点探讨了该方法在硬件电路上的实现,并进行逆变电源性能测试。测试结果表明:该方法效率高,正弦波频率可通过简单的软件配置后进行调整...
### IRS2453D 自震荡全桥驱动芯片解析 #### 概述 IRS2453D是一款自震荡全桥驱动芯片,该芯片能够承受高达600伏特的电压,适用于高压环境下对功率器件进行高效驱动。该芯片继承了IR2153自震荡半桥驱动芯片的特点,...
超强PSP视频输出专用优化版RemoteJoy4iRS
IRS系统使用手册.pdf IRS系统使用手册.pdf是关于互联网报告系统的使用指南,本手册涵盖了网络数据挖掘、网页数据的结构与特点、网页数据挖掘的基本方法、智能网络爬虫等多方面的知识点。 网络数据挖掘 网络数据...
1. **RemoteJoy4iRS介绍**:RemoteJoy4iRS是一个第三方软件,它通过USB连接将PSP与电脑相连接,让用户可以在电脑上实时查看PSP的屏幕显示,并可以进行录制。这使得玩家无需额外硬件即可捕获游戏视频。 2. **功能...
"IRS数据交易系统"是一个专为处理利率互换(Interest Rate Swap, IRS)交易而设计的平台。在构建这样的系统时,我们需要深入了解金融市场的运作机制,特别是利率互换这一金融衍生品的特性。利率互换是两个或多个实体...
"irs-server"是一个基于Web服务技术构建的交易系统,它采用了Spring容器进行开发,并使用Java语言作为主要编程语言。这个系统的设计和实现充分利用了Java的强类型和面向对象特性,以及Spring框架的强大功能,旨在...
标题中的“IRS.rar_IRS_IRS classroom_IRS.rar_点名_课堂教学系统”指的是一个名为“IRS”的教学互动反馈系统,其相关文件被压缩在名为“IRS.rar”的压缩包内。这个系统主要用于课堂上的教学互动,如点名功能,使...
### 利用DBMS与IRS实现中文全文检索的研究 #### 引言 在信息技术迅速发展的今天,数据库管理系统(DBMS)与信息存储检索系统(IRS)作为数据管理和检索的重要工具,各自发挥着不可或缺的作用。然而,面对海量非...
3. 三相位芯片(IRS2334SPbF/MPbF): - IR推出适用于反相电机驱动器的紧凑型三相位芯片,适合节能家电等应用。 - IRS2334SPbF采用SOIC20WB封装,而IRS2334MPbF采用QFN5X5封装,两种封装占用面积小,仅为25mm²。...
在这个电机驱动板设计中,MC34063AD主要负责调节电源电压,控制电机的工作状态,而IRS2184则负责驱动电机的功率MOSFET,实现电机的正反转及速度控制。两者协同工作,确保了电机驱动板的高效运行。 硬件原理图是理解...
随着无线通信技术的飞速发展,智能反射面(IRS)作为该领域的一个新兴研究热点,正逐步成为提升通信安全性的重要工具。IRS,也被称作可重构智能表面,是一种通过编程调整表面单元反射相位的先进技术,能够智能操纵...
### 一体化数字资源系统(IRS)架构及管理规范解析 #### 一、IRS概述 **一体化数字资源系统(IRS)**是一种先进的技术框架,旨在通过整合各类数字资源来支持高效的数字化改革进程。它作为一体化智能化公共数据平台的...
了解IRS的数学原理和定价机制对于市场参与者来说至关重要,尤其是对于涉及到IRS项目的发起人和管理人,他们需要清楚初始成本、市场价值和终止成本等相关费用。因此,本文件旨在提供一个基础的概览,帮助财资管理者和...
在当今通信技术迅猛发展的背景下,第六代移动通信技术(6G)成为全球关注的焦点。随着第五代移动通信技术(5G)的普及与成熟,学术界、产业界和标准化组织已经开始对6G的愿景、需求和技术展开了深入研究。6G预计将...
"IRS图像字符识别系统"是一个基于VC++开发的软件,主要功能是识别图像中的中文、英文等字符。这个系统的高识别率超过90%,显示了其在光学字符识别(OCR,Optical Character Recognition)领域的优秀性能。OCR技术是...