# 语义分析
# 符号表
用符号表保存我们已经定义了的变量或者函数以及其相关信息,在遍历语法分析树时,如果发现当前新出现的某些单元与符号表中的信息有冲突,我们就可以发现语义错误。
# 实现符号表
首先构建作用域树:
我们在这里犯了一个小错误,即把函数参数的作用域单独独立出来了,以便于我们编码,实际上函数参数作用域和函数作用域应该是要合并的。
作用域的指针指向父作用域,在查找某个变量是在哪个作用域时要循环从当前作用域到父作用域遍历。
之后可以通过递归遍历等方式进行语义相关的检查。
某个利用 antlr4 实现符号表的类图:
scope 是作用域(持有一个 Map (name, symbol) 的符号表数据类型),symbol 是符号 (持有 name 和 type,是符号表的基本单元),type 是符号所持有的类型(比如基本类型,array), 通过遍历时
浅绿框起来的是接口。
# 属性文法
# 为什么要引入属性文法
之前引入上下文无关文法的时候,是因为正则表达式无法表达 anbn 这样的表达式,但是上下文无关文法也有局限,无法帮助我们进行语义相关的检查。于是引入 Knuth 发明的属性文法。
接下来以实现一个交互式的简易计算器为例
前几节课写的简易计算器采用的是 offline 的方法:
本次将采用 online 的方式实现,即在构建语法分析树的过程中加入我们想实现的 action。
之前实现计算器的时候遇到了一个问题,listener 模式下,访问某个节点的 enterXXX 与 exitXXX 方法没有返回值,而当前节点的 value 依赖于子节点 value 的值,当时采用了标注语法树的方法。
antlr4 的另一个解决方案:
在.g4 文件的 expr 定义后 加上 returns [int val] 后,在 antlr4 生成的 expr 对应的节点类中会附带一个 val 属性。
每一行后大括号裹起来的黑体字是会在语法分析时我们加入的 java 代码,具体可在生成的 parser 类中查看。之后在 @package 中加入需要的头文件并在 @members 中加入 eval,memory 等的 java 实现。(实际上可在 @parser::members 中加入,这样在 lexer 中就不会生成这些无用的代码)
像 expr 这种从子节点的 expr 值计算得到的属性,叫做综合属性。
至于 “交互式” 的实现,主要是 main 函数的实现方式,参照代码仓库。
第二个例子:类型声明文法:
第一条语义规则的问题:将 L 的类型赋值为 T 的类型,可是在递归调用的时候,L 并不知道 T 返回了一个类型是 float 还是 int。
解决方法,给 L 一个 inh (inherit) 属性值,这种从左兄弟或者父节点继承来的值,叫做继承属性。
antlr4 实现的语法:
vars [String typeStr] 代表 vars 节点将携带一个属性 TypeStr。
# 语法制导定义
像上面的 g4 语法定义都属于 SDD,每个节点可以有多个属性
继承属性用于在表达式中从左向右传递中间计算结果
比如:累乘的右递归文法
信息流向:先从左到右利用继承属性传递信息,再利用综合属性从下到上传递信息。
属性文法的本质:信息的有序流动
# L 属性定义
例子 1:
用属性文法实现得到表达式的后缀表示形式
例子 2:得到数组的类型表达式
比如 int [2] [3] -> (2,(3,int))
antlr4 实现文法:
例子 3:判断赋值号左右类型是否相等
数组声明相关文法已经在例子 2 中完成了。
具体代码可在 github 代码仓库中查找。
属性文法缺点:要在 antlr 中编写 java 代码,而且要完成编译器的话会需要很多代码,这些代码与 antlr 的文法混合在一起。
# 总结
两种语义分析方法,各有优势。Offline 简单,Online 性能更高。