经过前面四篇的铺垫,我们终于拥有了编写语法分析器的强大工具,现在可以正式开发一门编程语言的语法分析器了。我们先来定义miniSharp的语法规则,然后根据LL文法的特点进行一些调整,最后借助解析器组合子生成完整的语法分析器。
miniSharp语言是C#的一个小子集,然而它仍然具有一门完整编程语言的所有要素,而且仍然是一种面向对象的语言。我们把miniSharp的语法分成三类——声明结构、语句和表达式。声明结构就是类、方法、字段的声明。语句就是诸如if-else、while这样特定含义的指令。而表达式则是表示一种有运算结果的结构,如二元运算符表达式、函数调用表达式等。C#中赋值也是一种表达式,但miniSharp为了简化后续代码生成,将赋值当成一种语句。
首先来定义声明结构的文法。为了简化语义分析,我们规定程序中第一个类必须是一个静态类,静态类中只能有一个静态方法Main——这是整个miniSharp唯一允许的静态方法。在静态类之后可以定义多个普通类,普通类之间可以继承。下面定义文法的产生式采用了扩展写法,支持类似克林闭包的*符号。G → X* 代表G → ε; G → H; H → XG。文法中的蓝色字代表终结符(词法分析获得的单词)
|
语句部分我们将要定义语句块和六种语句。其中if-else语句的else部分是不能省略的。while语句不支持break。剩下四种分别是调用Console.WriteLine的语句、赋值语句、数组元素赋值语句和变量声明语句。
|
表达式部分我们将要定义二元、一元、数组长度、数组访问、字面常量、变量、this引用、new运算、方法调用等多种表达式。
|
其中二元运算表达式的op是+、-、*、/、>、<、==、&&和||之一。为了简单起见我们这里的二元运算表达式文法是有歧义而且没有正确定义优先级的。按照C#的语言规范,运算符的优先级关系如下(只提取了miniSharp支持的部分):
1 | (Exp) new this 变量 常量 方法调用 属性访问 数组访问 |
2 | ! |
3 | * / |
4 | + - |
5 | < > == |
6 | && |
7 | || |
有些语法分析器就是使用有歧义的二元运算符文法,在遇到歧义时使用预定义的运算符优先级来解决冲突。现在的语法分析器倾向于直接使用无歧义的文法。下面的文法就是经过精心安排的运算符文法,消除了歧义并使得运算符具有左结合和优先级的区别:
|
这样我们就定义了miniSharp的完整文法。注意,上述文法仍然存在一些左公因式和左递归的现象。我们用的解析器组合子因为独特的广度优先分支判断方式,其支持的文法实际上已经超越了递归下降语法分析器的LL(k)文法,称作LL(∞)的文法,这种文法非常强大,可描述所有确定性下推自动机DPDA接受的语言。但是,它仍然不允许文法存在左递归,而左公因式也会大大降低解析器的效率。所以消除左递归和尽量避免左公因式仍然是真正实现语法分析器时需要着重考虑的任务。
现代语言的语法分析器通常都是将源代码翻译成一棵抽象语法树(Abstract Syntax Tree,AST)。后续的语义分析可以在抽象语法树上继续进行。我们在语法分析篇(六)介绍过“语法分析树”,它是一种在文法推导过程中建立起来的树状数据结构。那么什么是抽象语法树呢?其实就是经过简化和抽象的语法分析树。在完整的语法分析树中每个推导过程的终结符都包含在语法树内,而且每个非终结符都是不同的节点类型。实际上,如果仅仅是要做编译器的话,很多终结符(如关键字、各种标点符号)是无需出现在语法树里的;而前面表达式文法中的Factor、Term也实际上没有必要区分为两种不同的类型,可以将其抽象为BinaryExpression类型。这样简化、抽象之后的语法树,更加利于后续语义分析和代码生成。使用.NET里的面向对象语言来实现语法树,最常见的做法就是用组合模式,将语法树做成一颗对象树,每种抽象语法对应一个节点类。下图就是miniSharp的抽象语法树的所有类。
节选其中一个语法树节点展示一下,比如大家熟悉的IfElse语句的语法树节点类:
public class IfElse : Statement { public Expression Condition { get; private set; } public Statement TruePart { get; private set; } public Statement FalsePart { get; private set; } public SourceSpan IfSpan { get; private set; } public SourceSpan ElseSpan { get; private set; } public IfElse(Expression cond, Statement truePart, Statement falsePart, SourceSpan ifSpan, SourceSpan elseSpan) { Condition = cond; TruePart = truePart; FalsePart = falsePart; IfSpan = ifSpan; ElseSpan = elseSpan; } public override T Accept<T>(IAstVisitor<T> visitor) { return visitor.VisitIfElse(this); } } |
它的结构非常简单,里面保存了所有作为子节点成分的字段,例如IfElse是由一个Condition表达式和TruePart、FalsePart两个语句组成。另外我们还多储存了两个SourceSpan,分别是if语句中“if”关键字和“else”关键字出现的源代码位置(多少行,多少列)。保存位置是为了后续语义分析中提供错误信息的位置。比如if的条件表达式必须是个bool类型的表达式,但语法分析阶段无法做出类型验证,而到了语义分析阶段分析出了语义错误,仍然需要向编译器用户提供错误的位置,这些SourceSpan就可以派上用场。
注意节点类最后还实现了一个Accept方法,用来支持语法树的Visitor模式。我们在语义分析阶段和代码生成阶段,需要一次又一次地遍历抽象语法树。为了简化语法树的访问,我们声明一个IAstVisitor<T>接口作为语法树的Visitor,后续过程需要遍历语法树时,就实现这一接口即可。实际上这个接口有一个默认实现——AstVisitor类,允许只重写一部分成员。
有了Ast,下面我们就开始编写miniSharp的语法分析器。在本系列的第五篇(miniSharp语言的词法分析器)中我们已经用VBF词法分析库定义了miniSharp的词法,生成了一些Token对象。那么接下来就只要使用Linq语法的解析器组合子,根据本篇开头定义的文法进行组合,并适时使用select语句生成语法树节点的对象即可。比如,文法最开始的Program和MainClass的写法如下:
PProgram.Reference = // MainClass ClassDecl* from main in PMainClass from classes in PClassDecl.Many() select new Program(main, classes); PMainClass.Reference = // static class id { public static void Main(string[] id) { statement }} from _static1 in K_STATIC from _class in K_CLASS from className in ID from _1 in LEFT_BR from _public in K_PUBLIC from _static2 in K_STATIC from _void in K_VOID from _main in K_MAIN from _2 in LEFT_PH from _string in K_STRING from _3 in LEFT_BK from _4 in RIGHT_BK from arg in ID from _5 in RIGHT_PH from _6 in LEFT_BR from statements in PStatement.Many1() from _7 in RIGHT_BR from _8 in RIGHT_BR select new MainClass(className, arg, statements); |
这代码是如此的直白以至于没什么可解释的。唯一要注意的是PProgram.Reference这个用法,这里PProgram是ParserReference<T>类的实例。这个类允许先直接new出来,然后再用.Reference = XXX的方式为其指定语法规则。这样就允许一个Parser组合子先使用,后定义(比如上面例子中的PMainClass就先在PProgram的语法定义中使用了,然后下面才定义其语法)。因为文法中的非终结符常常出现递归引用,用ParserReference这个类可以大大简化我们的工作,不用关心Parser的声明先后顺序问题。
我们重点来看一些需要特殊技巧的例子。首先是声明方法形式参数的文法,采用了FormalList → Type ID FormalRest*这样的定义方法,这是避免左递归的技巧。但是这样一来,方法的第一个参数就和其他的参数分别定义在两个语法当中。我们希望生成的抽象语法树不区分第一个参数和其余参数,所以可以在生成语法树时采用一点点小技巧来办到:
var paramFormal = from paramType in PType from paramName in ID select new Formal(paramType, paramName); PFormalList.Reference = // Type id FormalRest* | <empty> (from first in paramFormal from rest in PFormalRest.Many() select new[] { first }.Concat(rest).ToArray()) | Parsers.Succeed(new Formal[0]); PFormalRest.Reference = // , Type id paramFormal.PrefixedBy(COMMA.AsParser()); |
另外注意扩展的产生式“X*”在VBF解析器组合子库中可以直接使用X.Many()的方式实现。VBF中还定义了数个这种方便的扩展组合子。
最后要注意的是二元运算符的分析器。我们前面写出的无歧义符合优先级的二元运算符文法仍然是左递归的,用于解析器组合子时必须像上面的FormalList那样改成右递归的。但是这些运算符都是左结合的,我们不想让生成的抽象语法树也变成右递归的形态。因此,这里我们需要用(传统)Linq的Aggregate扩展方法来处理一下生成的语法树:
var termRest = from op in (ASTERISK.AsParser() | SLASH.AsParser()) from factor in PFactor select new { Op = op, Right = factor }; PTerm.Reference = // term * factor | factor from factor in PFactor from rest in termRest.Many() select rest.Aggregate(factor, (f, r) => new Binary(r.Op, f, r.Right)); var comparandRest = from op in (PLUS.AsParser() | MINUS.AsParser()) from term in PTerm select new { Op = op, Right = term }; PComparand.Reference = // comparand + term | term from term in PTerm from rest in comparandRest.Many() select rest.Aggregate(term, (t, r) => new Binary(r.Op, t, r.Right)); var comparisonRest = from op in (LESS.AsParser() | GREATER.AsParser() | EQUAL.AsParser()) from comparand in PComparand select new { Op = op, Right = comparand }; PComparison.Reference = // comparison < comparand | comparand from comparand in PComparand from rest in comparisonRest.Many() select rest.Aggregate(comparand, (c, r) => new Binary(r.Op, c, r.Right)); |
除此之外,剩下的语法翻译成组合子基本上都是水到渠成的工作了。完整的代码全部都在MiniSharpParser.cs中,大家可以自行下载阅读。经过小小的努力,我们终于能将miniSharp的源代码转换为抽象语法树了,接下来我们就要进入下一个编译器重要的阶段——语义分析。敬请期待下一篇!
希望大家继续关注我的VBF项目:https://github.com/Ninputer/VBF 和我的微博:http://weibo.com/ninputer 多谢大家支持!