经过前面四篇的铺垫,我们终于拥有了编写语法分析器的强大工具,现在可以正式开发一门编程语言的语法分析器了。我们先来定义miniSharp的语法规则,然后根据LL文法的特点进行一些调整,最后借助解析器组合子生成完整的语法分析器。

miniSharp语言是C#的一个小子集,然而它仍然具有一门完整编程语言的所有要素,而且仍然是一种面向对象的语言。我们把miniSharp的语法分成三类——声明结构、语句和表达式。声明结构就是类、方法、字段的声明。语句就是诸如if-else、while这样特定含义的指令。而表达式则是表示一种有运算结果的结构,如二元运算符表达式、函数调用表达式等。C#中赋值也是一种表达式,但miniSharp为了简化后续代码生成,将赋值当成一种语句。

首先来定义声明结构的文法。为了简化语义分析,我们规定程序中第一个类必须是一个静态类,静态类中只能有一个静态方法Main——这是整个miniSharp唯一允许的静态方法。在静态类之后可以定义多个普通类,普通类之间可以继承。下面定义文法的产生式采用了扩展写法,支持类似克林闭包的*符号。G → X* 代表G → ε; G → H; H → XG。文法中的蓝色字代表终结符(词法分析获得的单词)

Program MainClass ClassDecl*
MainClass static class ID { public static void Main (string[] ID)
{ Statement+ } } 
ClassDecl class ID { FieldDecl* MethodDecl* }
  class ID : ID { FieldDecl* MethodDecl* }
FieldDecl Type ID;
MethodDecl public Type ID (FormalList)
{
Statement* return Exp ; }
FormalList Type ID FormalRest*
  ε
FormalRest , Type ID
Type int[]
  bool
  int
  ID

语句部分我们将要定义语句块和六种语句。其中if-else语句的else部分是不能省略的。while语句不支持break。剩下四种分别是调用Console.WriteLine的语句、赋值语句、数组元素赋值语句和变量声明语句。

Statement { Statement* }
  if ( Exp ) Statement else Statement
  while ( Exp ) Statement
  System.Console.WriteLine( Exp )
  ID = Exp ;
  ID [ Exp ] = Exp ;
  Type ID ;

表达式部分我们将要定义二元、一元、数组长度、数组访问、字面常量、变量、this引用、new运算、方法调用等多种表达式。

Exp Exp op Exp
  Exp[ Exp ]
  Exp .Length
  Exp .ID ( ExpList )
  INTEGER_LITERAL
  true
  false
  ID
  this
  new int [ Exp ]
  new ID ()
  ! Exp
  ( Exp )
ExpList Exp ExpRest*
  ε
ExpRest , Exp

其中二元运算表达式的op是+、-、*、/、>、<、==、&&和||之一。为了简单起见我们这里的二元运算表达式文法是有歧义而且没有正确定义优先级的。按照C#的语言规范,运算符的优先级关系如下(只提取了miniSharp支持的部分):

1 (Exp)  new this 变量 常量
方法调用 属性访问 数组访问
2 !
3 * /
4 + -
5 < > ==
6 &&
7 ||

有些语法分析器就是使用有歧义的二元运算符文法,在遇到歧义时使用预定义的运算符优先级来解决冲突。现在的语法分析器倾向于直接使用无歧义的文法。下面的文法就是经过精心安排的运算符文法,消除了歧义并使得运算符具有左结合和优先级的区别:

BasicExp 括号、new、this、变量、常量、方法调用、属性访问、数组访问
Factor BasicExp
  ! Factor
Term Factor
  Term op Factor   其中 op 是 * /
Comparand Term
  Comparand op Term   其中 op 是 + -
Comparison Comparand
  Comparison op Comparand    其中 op 是 < > ==
Logical Comparison
  Logical && Comparison
Exp Logical
  Exp || Logical

这样我们就定义了miniSharp的完整文法。注意,上述文法仍然存在一些左公因式和左递归的现象。我们用的解析器组合子因为独特的广度优先分支判断方式,其支持的文法实际上已经超越了递归下降语法分析器的LL(k)文法,称作LL(∞)的文法,这种文法非常强大,可描述所有确定性下推自动机DPDA接受的语言。但是,它仍然不允许文法存在左递归,而左公因式也会大大降低解析器的效率。所以消除左递归和尽量避免左公因式仍然是真正实现语法分析器时需要着重考虑的任务。

现代语言的语法分析器通常都是将源代码翻译成一棵抽象语法树(Abstract Syntax Tree,AST)。后续的语义分析可以在抽象语法树上继续进行。我们在语法分析篇(六)介绍过“语法分析树”,它是一种在文法推导过程中建立起来的树状数据结构。那么什么是抽象语法树呢?其实就是经过简化和抽象的语法分析树。在完整的语法分析树中每个推导过程的终结符都包含在语法树内,而且每个非终结符都是不同的节点类型。实际上,如果仅仅是要做编译器的话,很多终结符(如关键字、各种标点符号)是无需出现在语法树里的;而前面表达式文法中的Factor、Term也实际上没有必要区分为两种不同的类型,可以将其抽象为BinaryExpression类型。这样简化、抽象之后的语法树,更加利于后续语义分析和代码生成。使用.NET里的面向对象语言来实现语法树,最常见的做法就是用组合模式,将语法树做成一颗对象树,每种抽象语法对应一个节点类。下图就是miniSharp的抽象语法树的所有类。

节选其中一个语法树节点展示一下,比如大家熟悉的IfElse语句的语法树节点类:

它的结构非常简单,里面保存了所有作为子节点成分的字段,例如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这个用法,这里PProgram是ParserReference<T>类的实例。这个类允许先直接new出来,然后再用.Reference = XXX的方式为其指定语法规则。这样就允许一个Parser组合子先使用,后定义(比如上面例子中的PMainClass就先在PProgram的语法定义中使用了,然后下面才定义其语法)。因为文法中的非终结符常常出现递归引用,用ParserReference这个类可以大大简化我们的工作,不用关心Parser的声明先后顺序问题。

我们重点来看一些需要特殊技巧的例子。首先是声明方法形式参数的文法,采用了FormalList → Type ID FormalRest*这样的定义方法,这是避免左递归的技巧。但是这样一来,方法的第一个参数就和其他的参数分别定义在两个语法当中。我们希望生成的抽象语法树不区分第一个参数和其余参数,所以可以在生成语法树时采用一点点小技巧来办到:

另外注意扩展的产生式“X*”在VBF解析器组合子库中可以直接使用X.Many()的方式实现。VBF中还定义了数个这种方便的扩展组合子。

最后要注意的是二元运算符的分析器。我们前面写出的无歧义符合优先级的二元运算符文法仍然是左递归的,用于解析器组合子时必须像上面的FormalList那样改成右递归的。但是这些运算符都是左结合的,我们不想让生成的抽象语法树也变成右递归的形态。因此,这里我们需要用(传统)Linq的Aggregate扩展方法来处理一下生成的语法树:

除此之外,剩下的语法翻译成组合子基本上都是水到渠成的工作了。完整的代码全部都在MiniSharpParser.cs中,大家可以自行下载阅读。经过小小的努力,我们终于能将miniSharp的源代码转换为抽象语法树了,接下来我们就要进入下一个编译器重要的阶段——语义分析。敬请期待下一篇!

希望大家继续关注我的VBF项目:https://github.com/Ninputer/VBF 和我的微博:http://weibo.com/ninputer 多谢大家支持!

自己动手开发编译器(十)miniSharp语法分析器相关推荐

  1. 自己动手开发编译器(我们的朋友 -- 装配脑袋走了)

    编者: 昨天在微信群里得知 我们的朋友--装配脑袋(施凡老师),因白血病离我们而去,自从得病以来,施老师在积极的接受治疗,8.27 得到的消息还是比较让人开心的,大家都在期待着他能够重出江湖.装配脑袋 ...

  2. 编译器入门 语法分析器 java_从零开始写个编译器吧 - Parser 语法分析器

    Parser(语法分析器)的编写相对于 Tokenizer (词法分析器)要复杂得多,因此,在编写之前可能也会铺垫得更多一些.当然,本系列旨在"写出"一个编译器,所以理论方面只会简 ...

  3. 自己动手开发编译器(四)利用DFA转换表建立扫描器

    上回我们介绍了两种有穷自动机模型--确定性有穷自动机DFA和非确定性有穷自动机,以及从正则表达式经过NFA最终转化为DFA的算法.有些同学表示还是难以理解NFA到底怎么转化为DFA.所以本篇开头时我想 ...

  4. 自己动手开发编译器特别篇——用词法分析器解决背诵圣经问题

    这几天比较忙,让大家久等了.但是我语法分析篇还需要一些准备,所以今天带来一个特别娱乐项目.其实也正好想多举一些例子,介绍VBF.Compilers.Scanner库的使用方法.今天的问题来自于一道腾讯 ...

  5. java实现语法分析器_200 行 JS 代码,带你实现代码编译器

    一.前言 对于前端同学来说,编译器可能适合神奇的魔盒 ,表面普通,但常常给我们惊喜. 编译器,顾名思义,用来编译,编译什么呢?当然是编译代码咯 . 其实我们也经常接触到编译器的使用场景: React ...

  6. Vczh Library++ 语法分析器开发指南

    Vczh Library++ 语法分析器开发指南 陈梓瀚 前言 在日常的开发工作中我们总是时不时需要写一些语法分析器.语法分析器不一定指的是一门语言的编译器前端,也有可能仅仅是一个自己设计格式的配置文 ...

  7. 简易编译器实现(二)使用Bison创建语法分析器

    你也可以通过我的独立博客 -- www.huliujia.com 获取本篇文章 简易编译器实现(一)使用Flex创建词法分析器一文介绍了编译器的概念和七个阶段,并说明了如何使用Flex创建词法分析器. ...

  8. 动手写的Python的HTML语法分析器(面向对象)

    动手写的Python的HTML语法分析器(面向对象) 主要包括4个文件,util.py文件主要负责截取每个块. rules.py文件定义两个类,超类Rule和分别对应的子类,子类定义了不同的划分块的要 ...

  9. java c:if语句_java开发编译器:C语言逻辑控制语句if else if 的语法解析

    具体的代码讲解和演示过程请参看视频: 用java开发编译器 从这节开始,我们看看解析器如何对逻辑控制语句,例如if else, for, while , do-while, goto 等语句进行相应的 ...

最新文章

  1. linux32内核下载rpm,Vivaldi TP4 for Linux Rpm (32bit)
  2. Struts2+Android (3) 多种方式向服务器发送信息
  3. 拦截QT关闭窗口的CloseEvent
  4. 2020年周记(2/50)
  5. MVC — 初步理解IIS工作流程
  6. Oracle 10g 报ORA-12514错误
  7. 服务器 python cant open file_如何删除分析*。gcda:无法打开python virtualenv builder出错?...
  8. linux查看cuda版本_查看Linux系统版本命令集合
  9. QTTabBar 使用教程:用浏览器的方式管理 Windows 资源管理器
  10. Lookup函数的使用方法介绍(含VLookup和HLookup)
  11. sap事务代码如何收藏_SAP仓库管理模块事务代码大全
  12. C语言试题七十九之请编写函数实现自然底数 e=2.718281828
  13. 常用的四拍子,八拍子
  14. 启动SpringBoot项目时,报程序包不存在或者找不到符号的错误
  15. C++RTSP服务端(附源码)
  16. [业界] 关于诺基亚,上来吐槽两句埃洛普,顺便爆点料。
  17. QCC30DFU流程android说明,qcc512x qcc302x qcc303x earbud 软件GAIA OTA DFU 空中升级实现方法以及升级步骤...
  18. 《钢琴调律师 五级》 笔记
  19. Poor China
  20. 【cartographer】(2)分枝定界算法

热门文章

  1. Windows Server入门系列之十 注册表的基本使用
  2. 单例模式的练习-如何正确构建
  3. 问题分享:最近测试VDI-in-a-Box使用AD做身份验证出现以下错误提示:
  4. 多玩家游戏设计注意思的地方(转)
  5. JSF技术的相关网站和BBS
  6. SQL性能第2篇:查询分析和访问路径制定
  7. golang CI: Use result of type assertion to simplify cases SCC-S1034
  8. final修饰符、抽象类、接口、多态、内部类的简单小结
  9. easycode不推荐使用_为什么MySQL不推荐使用uuid或者雪花id作为主键?
  10. Java实现文件夹打包