PA1-A 实验说明
Last updated
Was this helpful?
Last updated
Was this helpful?
在本阶段中,大家的任务是编码实现 Decaf 语言编译器的词法分析和语法分析部分,同时生成抽象语法树。
首先是词法分析 (lexical analysis)。 你需要运用“词法分析程序自动构造工具” (lexer generator) 生成 Decaf 语言的“词法分析程序” (lexer/scanner)。 词法分析程序是前端分析的第一部分,它的功能是从左至右扫描 Decaf 源语言程序, 识别出标识符、关键字、整数常量、操作符等各种单词符号,并把识别结果以单词 (token) 的形式返回给语法分析程序。 这一部分的实验目的是要掌握自动工具的用法,体会正规表达式、有限自动机等理论的应用,并对词法分析程序的工作机制有比较深入的理解。
之后是语法分析 (parsing)。 你需要运用“语法分析程序自动构造工具” (parser generator) 生成 Decaf 语言的“语法分析程序” (parser),并最终生成抽象语法树。 这一部分的实验目的是要掌握自动工具工具的用法,体会上下文无关文法、LALR(1) 或 LL(*) 分析等理论在编译器构造中的实际应用。
PA1-A 的两部分是密切相关的:语法分析程序并不直接对 Decaf 源程序进行处理,而是调用词法分析程序对 Decaf 源程序进行词法分析, 然后对词法分析程序返回的终结符序列进行归约,也就是说,词法分析程序输出的结果才是语法分析的输入。
当语法分析程序运行结束,如果没有发现语法错误,PA1-A 会生成与 Decaf 源程序对应的抽象语法树。这就是 PA1-A 的最终产物。
PA1-A 是整个实验的热身阶段,需要自己完成的代码量较少,主要的工作在于建立编程环境、熟悉代码框架、熟悉 Decaf 语言以及掌握自动工具的具体用法等。 不熟悉 Git 的同学,请在这一阶段逐渐掌握 Git 的基本使用,包括 clone/pull/commit/push 等入门功能, 以保证在截止日前前你能将代码提交到指定仓库的 master 分支上。
本阶段的测试方法是:编译器格式化输出抽象语法树,测试程序比较其与标准输出是否完全一致。 我们保留了一些测试例子没有公开,所以请自己再编写一些测例,以更加充分地测试你的实现是否符合新特性的规范。
函数式 (functional) 语言的一个显著特征是,函数是“一等公民” (first-class citizens),即函数能像普通的值(如整数、字符串、对象等)一样, 作为函数调用的参数和返回值进行传递。 Lambda 表达式提供了一种不用取名字就能直接定义出来一个函数的方法。它是所有函数式语言(如 Haskell/Ocaml/Scala)的标配。 很多不以函数式作为典型语言特征的语言,如 C#、Rust、C++ 甚至 Java 都开始支持 lambda 表达式。
老版本的 Java 不支持 lambda 表达式,为了实现整数到布尔值的函数类型,我们不得不通过接口来定义:
虽然新版的 Java 支持了 lambda 表达式,但它本质上还是会对应到接口 Function
/Consumer
/Supplier
等。 因此在实际使用中,Java 的函数式特性比普通函数式语言要不方便得多。
Lambda 表达式能你的程序更可读、更可维护、更健壮。比较下面的 C 代码
和 Haskell 代码
后者更可读,更精准的传达了程序员的意图:“先去掉偶数,然后乘三”。
所谓抽象语法树 (Abstract Syntax Tree, AST),是指一种只跟我们关心的内容有关的语法树表示形式。 抽象语法是相对于具体语法而言的,所谓具体语法是指针对字符串形式的语法规则,而且这样的语法规则没有二义性,适合于指导语法分析过程。 抽象语法树是一种非常接近源代码的中间表示,它的特点是:
不含我们不关心的终结符,例如逗号等(实际上只含标识符、常量等终结符)。
不具体体现语法分析的细节步骤,例如对于 List ::= List Elem | Elem
这样的规则,
按照语法分析的细节步骤来记录的话应该是一棵二叉树,但是在抽象语法树中我们
只需要表示成一个列表,这样更便于后续处理。
可能在结构上含有二义性,例如加法表达式在抽象语法中可能是 Expr -> Expr + Expr
,
但是这种二义性对抽象语法树而言是无害的——因为这种二义性已经在语法分析阶段解决了。
体现源程序的语法结构,尤其是树形结构。
使用抽象语法树表示程序的最大好处是把语法分析结果保存下来,后面可以反复利用。 在面向对象的语言中描述抽象语法树是非常简单的:我们只需要为每种非终结符创建一个类。
在我们的代码框架中我们已经为你定义好各种符号在 AST 中对应的数据结构。请在实验开始前阅读相关代码。
使用最朴素的 BNF (Backus–Naur Form),Decaf 语言的一个文法片段描述如下:
表示的是
一个 topLevel
有至少一个 classDef
。
一个 classDef
首先是关键字 class
,跟上id
,然后是一个可选的 extendsClause
,最后是大括号包起来的零个或多个成员定义。
我们可以写成如下 EBNF (Extended Backus–Naur Form),更精确和简明地反映上述含义:
在 EBNF 中,各符号(或者符号串)允许添加如下拓展标记:
+
:在它前面的符号(或者符号串)出现一次或者多次
*
:在它前面的符号(或者符号串)出现零次或者多次
?
:在它前面的符号(或者符号串)出现零次或者一次
(
)
:分组
|
:选择,原来也有,但允许选择出现在分组里。例:s ::= ('a' | 'b') 'c'
等价于 s ::= 'a' 'c' | 'b' 'c'
加入这些拓展可以有效简化语法。作为一个例子,Scala 版本使用的 Antlr4 就支持这些拓展。 现在 Antlr4 的文法描述文件的语法部分只有 80 行, 而 Jacc 版本有 126 行,多了 50% 以上。
备注:语法测量去除了文件头、语法动作、注释、空行,Scala 文法文件被改写成和 Java 版本一样的风格,2019 年 9 月 27 日测量。
所以,后面我们会使用 EBNF 来描述文法。 另外的好处是,这样能够防止你们直接抄实验指导书。
本次实验给出了基础的 Decaf 框架,它完成了《Decaf语言规范》。 本次实验你的任务是,在这个框架的基础上,完成新特性的词法语法分析。
以下给出各新特性对应的语法规范。注意:这是规范而不是实现,你的实现应当符合此规范。
加入 abstract
关键字,用来修饰类和成员函数。例如,
语法规范:将原来的
变成
加入 var
关键字,用来修饰局部变量。例如
语法规范:
这是本学期新特性中最复杂的一部分。幸运的是,在 PA1-A 中你只需考虑词法和语法分析的问题。 具体地,你需要支持以下三种新语法:
语法规范:
括号左边的是返回值的类型,括号内的是诸参数的类型。
有两种, block lambda 和 expression lambda。Lambda 表达式的类型是函数类型。
语法规范:
其中新增的箭头操作符 '=>'
左边是参数列表,右边是返回值。'=>'
的优先级最低。 fun
为新增的关键字。 Block lambda 可以包含 return 语句表示返回值(当然,没有 return 语句的话返回类型是 void)。
原来只能调用成员方法和静态方法,现在可以调用任意类型为函数类型的表达式(其本质就是个函数)。
语法规范:将原来的
变为
对部分同学,lambda 表达式还是个新东西。所以下面是一些例子,帮助你理解。
函数类型:
Lambda 表达式:
函数调用:
一个小故事:
我们采用 C / C++ 风格来写函数类型。你可能觉得这种写法很怪。这样设计主要是为了让 PA1-B 阶段更容易。 还有更怪的:C 中声明一个函数指针
a
要写成int (*a)(int, int);
但是,你可以认为这样的类型写法指明的是 “我们应该如何使用这个类型的变量” 比如上面的a
,它的用法是int b = (*a)(4, 5)
-- 和签名太像了。 也许这也能帮助你理解,为什么 C 和 C++ 数组声明是int a[10]
而不是int[10] a
, 以及为什么有些人写char *a
而不是char* a
。
在 PA1-A 阶段,我们最终会将你构造出来的 AST 进行格式化打印,并与标准输出比对是否一致。 所有 AST 结点的打印格式都遵循如下流程:
打印结点名称和位置
增加缩进
遍历所有元素(子树),依次递归打印它们
减少缩进
新增特性的打印格式如下:
打印抽象类/方法时,其修饰符 abstract
打印为 ABSTRACT
。如
对应的 AST 打印为(省略了位置,下同)
var
局部变量定义打印用 var
修饰的局部变量定义时,按照标准的 LocalVarDef
来打印,只不过将其第一个元素(类型)打印为空 <none>
。如
对应的 AST 打印为
结点名称为 TLambda
。包含两个元素:第一个元素是返回值类型。第二个元素一个类型的列表,分别表示其参数类型。如
对应的 AST 打印为
结点名称为 Lambda
。包含两个元素:第一个元素是参数列表,各参数按照 LocalVarDef
来打印。 第二个元素是返回值,它可能是一个表达式,也可能是一个语句块。如
对应的 AST 打印为
结点名称为 Call
。包含两个元素:第一个元素是一个表达式,代表要调用的函数。 第二个元素是表达式的列表,代表调用参数。如
对应的 AST 打印为
测例中给出了更多标准输出作为例子。 在实验开始前,建议你阅读所使用框架的实验指导书,搞清楚框架是如何定义各 AST 结点,并完成格式化打印的。
实验评分分两部分:
评测结果:80%。这部分是机器检查,要求你的输出和标准输出一模一样。我们会有未公开的测例。
实验报告(根目录下 report-PA1-A.pdf
文件):20%。要求用中文简要叙述你的工作内容。此外,请在报告中回答以下问题:
Q1. 请根据你选择的实验框架回答不同的问题:
(Java & Scala) AST 结点间是有继承关系的。若结点 A
继承了 B
,那么语法上会不会 A
和 B
有什么关系?限用 100 字符内一句话说明。
(Rust) 有一部分 AST 结点是枚举类型。若结点 B
是枚举类型,结点 A
是它的一个 variant,那么语法上会不会 A
和 B
有什么关系?限用 100 字符内一句话说明。
Q2. 原有框架是如何解决空悬 else (dangling-else) 问题的?限用 100 字符内说明。
Q3. PA1-A 在概念上,如下图所示:
输入程序 lex 完得到一个终结符序列,然后构建出具体语法树,最后从具体语法树构建抽象语法树。 这个概念模型与框架的实现有什么区别?我们的具体语法树在哪里?限用 120 字符内说明。