Skip to content

Latest commit

 

History

History
126 lines (72 loc) · 7.88 KB

语法分析和语义分析.md

File metadata and controls

126 lines (72 loc) · 7.88 KB

语法分析和语义分析

-- 读《自制编译器》第一章到第十一章读后小结

在正式进入这个话题之前先说明一下一个我在学习计算机之前和大家一样可能会弄混淆的问题

编译器和IDE

经常听到有同学说我这个代码出现了“红线”这个一定是编译器的问题;我重新开了一下编译器我的代码就可以跑起来了! 虽然编译器会对你的错误提出警告(这里所说的是语法层面上的错误)但是实际上,红线标注这个事情是IDE完成的。 另外,有些IDE是自带编译器的,为了避免操作上面的麻烦,比如宇宙第一的IDE VC 6++,Code Blocks 这些,因为为了避免在Windows上面配置编译器的麻烦。 有时候IDE却是会有BUG,但是这种BUG基本和编译器没有什么关系。


进入正题:

先提出几个问题:

下面的语言环境如果没有提出特殊说明都是在C语言前提下:

  • 我们写的代码是如何被编译器识别和解释的? 一个int 为什么被解释成了一个32或者的宽度的数字? 为什么将+ 解释为 加法,而不是-/*呢????!!!

  • 如何实现运算的左结合和右结合性?

  • 在声明一个变量的时候为什么局部变量能够“替代”全局变量,起作用?

  • if-else 语法块中 else 是跟在那个if后面?

if() 
   if()
   else
  • Java 中的import语句是怎么将一个id表示的类的代码导入你所需要的项目/类文件中的?

  • 现代语言中的类型推导是怎么实现的?

这些是我在看书的时候提出的问题,同样有一些也是作者提出来的问题,我根据的思考下面一一回答:

简单说明

JavaCC通过制定类似于Java 中正则表达式的规则去扫描代码。扫描完成的代码Token 表示 Token 可能是一个关键字,当然这个规则也可以定义重复和()*或者| . 但是有些时候扫描的目的不仅仅是为了扫描到某个数值 然后把这个数值赋值给另一个,有些时候是为了触发这个扫描的副作用,这个副作用称为action

action 定义在一个扫描的语句后面 一般来说,只要扫描的结果完成就可以触发这个action: action

治好你的颈椎病...

但是这样的简单的action 怎么能够! 所以在有些定义的时候会使用一些比较复杂的action

复杂一点定义

这个代码里面的Node提示我们整个文件的解析过程可能和“树”结构有点关系,事实证明确实如此。

使用JavaCC 解析和生成中间代码直到运行的过程中,会将一整个文件以语法树的形式保存,这个语法树简称为AST。 其中的节点比如函数和变量将转化为:VaribleNode 表达式将转化为StmtNode 等。

在扫描的过程通过扫描到相关的语句触发这样的action,就会调用一些函数 将Token或者是扫描到的Node变成对应的数据结构储存起来。

这些Node我们怎么处理呢? 比如3 = 1 + 2 这样的表达式 就会将 + 变成一个Node,叫做BinaryOpNode 然后 左边和右边的数字将使用ExprNode储存

可以想象到这样的表达式只会是 一整个抽象语法树的一部分。

同样的一些流程控制语句,比如if while 这些 还有return 都会成为node节点,整棵抽象语法树会包含这些节点。

但是如果全部是这些节点松散的聚合在一起的话会非常的乱,而且没有办法区分那一块是局部定义的变量的作用域,那些是全局变量作用的作用域。

所以一个AST在解析(resolve)的时候,会先定义一个TopLevel,这个TopLevel包含了所有全局变量的声明,函数的声明。

另外,从外部import的类中所定义的全局变量和函数也会加到这个TopLevel中去。

然后在后面解析的过程中,针对函数或者其他语法块(Block)会定义LocalLevel,存放这里定义的变量等等。不管是在全局定义的变量还是在局部定义的变量都会检查是否重复命名,如果重复命名的花将会抛出异常。

这个重复的异常的信息和Java中抛出的异常的信息会很相似,也有是那一行抛出的错误等。

之所以编译器知道是那一行抛出的错误,是因为在构建节点的过程中,很多Node的构造函数会传入一个Location 信息,这个location包含了这个代码在源文件的行数信息。如果没有在构造函数中定义location的,可从自身定义的其他节点获取行数信息,比如BianryOpNode。

回答上面提出的问题

  • 关于int 为什么是解释为int的问题有点复杂,这里直接上一下书里面的代码:

可以看到Integer类型是被解释为了IntegerNode Charater类型被解释为了IntegerLiteralNode int的字面量。 这些被解释为Node的类型会被放到AST中。 虽然我生成中间代码还有链接外部库什么的还没有看,据我推测,int 被解释为 32位是在生成中间代码的时候进行了处理

  • 关于运算的左结合 和 右结合 性可以通过设置扫描器的规则确定: 左结合性: 1 - 2 -3 = (1 - 2)- 3
expr1()("-" expr1())*

右结合性: x = y = 0

expr():{}
{
	term() "=" expr()
}

左结合性的语法是扫描到了左边就会生成一个表达式就会将这个表达式作为一个节点: 1 - 2 然后扫描到了最后再将这些表达连接起来:

但是右结合性的表达式的递归的意味更强一些,如果不扫描到最后的结尾,什么都无法生成!

最后生成的语法树是这样的:

  • if-else块的问题涉及到了语法二义性的问题,也就是说,同样的语法规则可能导出两颗不一样的语法树。 好在可以借用C语言中的经验,规定后面的else块是紧接着上面的一个if的,这样的话规则可以这样写:

  • import 的问题已经说过了,是在生成语法树的时候会将import语句导入的外部文件作为全局变量使用

  • 类型推导:如果像上面一样,i = a -b 的话由于类型转换会将操作数的类型全部转换为int类型,这是在扫描AST的时候会比较left和right,哪一种在“规则”中的等级比较高就会转换成哪种类型。 这个是因为编译器为了简单编写起见,遍历的过程是从下往上的,但是如果采用了从上向下进行扫描的时候,只要确定操作符左右两端是同样的类型的话其实不需要显式地声明类型