《Language Implementation Patterns》之 符号表
前面的章节我们学会了如何解析语言、构建AST,如何访问重写AST,有了这些基础,我们可以开始进行“语义分析”了。
在分析语义的一个基本方面是要追踪“符号”,符号是语句定义的变量、函数,我们通过建立一种叫做“符号表”的基础结构来完成此项工作。
有两种模式的符号表:
- Pattern 16, Symbol Table for Monolithic Scope,所有的符号存在于单一的作用域内,早期的BASIC使用这种模式;
- Pattern 17, Symbol Table for Nested Scopes,符号存在于多个作用域,”作用域“之间可以嵌套,C语言使用这种模式;
收集语言实体信息
先看一段C++代码:
class T { ... }; // define class T
T f() { ... } // define function f returning type T
int x; // define variable x of type int
这个段代码实际上定义了3个符号,在一个语言处理程序里面,可能以下面的方式来收集信息:
Type c = new ClassSymbol("T");
MethodSymbol m = new MethodSymbol("f", c);
Type intType = new BuiltInTypeSymbol("int");
VariableSymbol v = new VariableSymbol("x", intType);
每个符号都包含了以下几项信息:
- 名字:一般是一个标记符,也有可能是一个操作符;
- 类别:类、函数、变量;
- 类型:类型允许语言程序判定表达式的有效性,静态类型的语言在编译时做类型检查,动态类型的语言延迟到运行时。
符号表实现用一个class来表示一个”类别“,包含”名字“和”类型“字段:
public class Symbol {
public String name; // All symbols at least have a name public
Type type; // Symbols have types
}
public class VariableSymbol extends Symbol {
public VariableSymbol(String name, Type type) { super(name, type); }
}
出于一致性考虑,Class和Struct类别的符号也从Symbol集成,为了区别于其他类别的符号,我们通过一个Type接口来给符号打个标签:
public class BuiltInTypeSymbol extends Symbol implements Type {
public BuiltInTypeSymbol(String name) { super(name);
}
public interface Type { public String getName(); }
}
Type接口本身并不包含任何有意义的内容,它仅仅是一个标签来说明这个Symbol能承担某种角色。
符号作用域
一个作用域是一个代码有明确范围的代码区域,对符号定义进行分组。比如类作用域将成员定义分成一个组;函数作用域将参数变量、局部变脸分成一个组。
作用域一般与某些特定token重合,比如括号;这样的作用域叫做词法作用域;或许”静态作用域“的叫法更好,因为光看代码就可以确定作用域了。
下面列出了作用域的一些特征,对不同的语言有不同的值:
- 静态VS动态, 大部分语言都是静态作用域,少数(LISP和PostScript)有动态作用域;
- 具名作用域,类、函数作用域有名字,其他一些作用域,比如全局作用域、局部作用域,没有名字;
- 嵌套,语言一般都允许作用域嵌套,一般对嵌套层数有限制;
- 内容,有些作用域只允许变量定义,有些则只允许其他语句,比如C struct的作用域只允许变量定义;
- 可见性,一个作用域的符号对其他代码段可见或不可见;
与上面类似,我们通过接口来标记一个Class、Function是一个作用域:
public interface Scope {
public String getScopeName(); //do I have a name
public Scope getEnclosingScope(); //am I nested in another scope
public void define(Symbol sym); //define sym in this scope
public Symbol resolve(String name); // look up name in scope
}
scope并不追踪对应的代码区域,反过来,代码区域的AST指向所述的scope;这个设计是很有意义的,因为访问AST的时候需要经常查找遇到的符号。
单一作用域
早期的Basic拥有单一的作用域,而现在只有配置文件这种及其简单的语言才有单一的作用域。
追踪符号只需要一个符号集合,遇到重复的符号定义要么覆盖之前的定义,要么被认为是一个错误;遇到新的符号定义则加入集合。
一个<符号名:符号对象>的map可以很好地承担这个职责。
嵌套作用域
多个作用域允许我们用同一名字来代表不同的语法实体。编程语言一般用上下文来区分相同的名字,”上下文”对应某个作用域以及外围嵌套的作用域;作用域嵌套就像一个栈一样,遇到一个新的scope,我们push进栈,栈顶的scope被成为当前scope,当从一个scope退出时,我们执行pop操作。
看一段c代码:
// start of global scope
int x; // define variable x in global scope
void f() { // define function f in global scope
int y; // define variable y in local scope of f
{ int i; } // define variable i in nested local scope
{ int j; } // define variable j in another nested local scope
}
void g() { // define function g in global scope
int i; // define variable i in local scope of g
}
包含的scope如下图:

值得注意的是,这个树形图的节点之间的指针方向,是从子节点指向父节点,这是符号的搜寻方向。
于是,scope栈的操作过程,恰好定义了上面的scope树:
<b>//push</b>
currentScope = new LocalScope(currentScope);
<b>//pop</b>
currentScope = currentScope.getEnclosingScope();
<b>//def</b>
Symbol s = 《some-new-symbol》;
currentScope.define(s);
因此对于上面的c代码,Parser会执行以下的scope操作序列(暂时不去考虑如何引发这些操作):
1. push global scope .
2. def variable x in current scope, .
3. def method f in scope and push scope
4. def variable y.
5. push local scope.
6. def variable i.
...
解析符号引用
当代码里面遇到一个符号,需要解析它所引用的对象。对于单一的作用域,这个操作很简单:`myOnlyScope.resolve(《symbol-name》)。
当存在多个作用域的时候,符号引用的对象取决于它所处的位置;符号引用的scope栈是引用所处的scope到scope tree根节点的路径,这个栈称之为语义上下文(semantic context)。于是,解析一个符号引用,就是在它的语义上下文寻找他,从当前的scope开始一直到栈顶scope。
public Symbol resolve(String name) {
Symbol s = members.get(name); // look in this scope
if ( s!=null ) return s; // return it if in this scope
if ( enclosingScope != null ) { // have an enclosing scope?
return enclosingScope.resolve(name); // check enclosing scope
}
return null; // not found in this scope or there's no scope above
}
从代码可以看出,scope树的巧妙设计,让这个过程变得简单直观。
Pattern 16 Symbol Table for Monolithic Scope
单一层次的符号表,适合简单的语言(没有函数),比如配置文件、小型图形语言或小型DSL。
下面的表格展示了,基于语言输入构建scope的操作:
| Upon | Actions |
| Start of file | push a GlobalScope. def BuiltInType objects for any built-in types such as int and float. |
| Declaration x | ref x’s type (if any). def x in the current scope. |
| Reference x | ref x starting in the current scope. |
| End of file | pop the GlobalScope. |
Pattern 17 Symbol Tale for Nested Scope
该模式构建一棵scope tree,包含多个、嵌套的scope
看下面一段代码:
// start of global scope
int i = 9;
float f(int x, float y) {
float i;
{ float z = x+y; i = z; }
{ float z = i+1; i = z; }
return i;
}
void g() {
f(i,2);
}
对应的scope tree如下:

函数定义的对应的MethodScope包含了参数符号,里面嵌套一个LocalScope包含所有的局部变量。
同样用一个表格来描述Parse过程中的Scope构建操作:
| Upon | Actions |
| Start of file | push a GlobalScope. def BuiltInType objects for any built-in types such as int and float. |
| Declaration x | ref x’s type (if any). def x in the current scope. |
| Method declaration f | ref f’s return type. def f in the current scope and push it as the current scope. |
| { | push a LocalScope as the new current scope. |
| } | pop, revealing previous scope as current scope. |
| End of method | pop the MethodSymbol scope (the parameters). |
| Reference x | ref x starting in the current scope. If not found, look in the immediately enclosing scope (if any). |
| End of file | pop the GlobalScope. |
具体实现上,应用AST Tree Pattern Matcher可以方便地构建出符号表。
《Language Implementation Patterns》之 符号表的更多相关文章
- 《Language Implementation Patterns》之 解释器
前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式). 高级解释器比较适合DS ...
- 《Language Implementation Patterns》之 数据聚合符号表
本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置. 区别在于:作用域之外的代码能够通过一种特殊 ...
- 《Language Implementation Patterns》之 强类型规则
语句的语义取决于其语法结构和相关符号:前者说明了了要"做什么",后者说明了操作"什么对象".所以即使语法结构正确的,如果被操作的对象不合法,语句也是不合法的.语 ...
- 《Language Implementation Patterns》之 构建语法树
如果要解释执行或转换一段语言,那么就无法在识别语法规则的同时达到目标,只有那些简单的,比如将wiki markup转换成html的功能,可以通过一遍解析来完成,这种应用叫做 syntax-direct ...
- 《Language Implementation Patterns》之 增强解析模式
上一章节讲述了基本的语言解析模式,LL(k)足以应付大多数的任务,但是对一些复杂的语言仍然显得不足,已付出更多的复杂度.和运行时效率为代价,我们可以得到能力更强的Parser. Pattern 5 : ...
- 《Language Implementation Patterns》之访问&重写语法树
每个编程的人都学习过树遍历算法,但是AST的遍历并不是开始想象的那么简单.有几个因素会影响遍历算法:1)是否拥有节点的源码:2)是否子节点的访问方式是统一的:3)ast是homogeneous或het ...
- 《Language Implementation Patterns》之 语言翻译器
语言翻译器可以从一种计算机语言翻译成另外一种语言,比如一种DSL的标量乘法axb翻译成java就变成a*b:如果DSL里面有矩阵运算,就需要翻译成for循环.翻译器需要完全理解输入语言的所有结构,并选 ...
- iOS 符号表恢复 & 逆向支付宝
推荐序 本文介绍了恢复符号表的技巧,并且利用该技巧实现了在 Xcode 中对目标程序下符号断点调试,该技巧可以显著地减少逆向分析时间.在文章的最后,作者以支付宝为例,展示出通过在 UIAlertVie ...
- 符号表实现(Symbol Table Implementations)
符号表的实现有很多方式,下面介绍其中的几种. 乱序(未排序)数组实现 这种情况,不需要改变数组,操作就在这个数组上执行.在最坏的情况下插入,搜索,删除时间复杂度为O(n). 有序(已排序)数组实现 这 ...
随机推荐
- VS2017 启动调试报错:ID为{....}进程未启动解决方案
今天遇到这么一个问题,打开VS启动调试,始终报错,如下图: 我重启VS,甚至重启电脑都不得行,那个进程号还在变化,就在网上查找资料,各式各样的解决方案,这里我记录我成功的方案. 打开项目文件地址,在解 ...
- 使用myeclipse出现中文乱码的情况以及解决办法
一:在jsp页面使用中文在浏览器中显示的时候出现乱码,解决问题的办法: 1)直接在<mete>标签中修改charset属性为"utf-8"或者为"gb2312 ...
- Gedit : 我的开场白 [TPLY]
为什么用Gedit 在学校的高一新生里,好像就只有我使用Gedit 大家都笑我是"用记事本编程的人" 我就想 到考场看看你们笑得出来不 先放一个高配emacs配置 (global- ...
- 期望$DP$ 方法总结
期望\(DP\) 方法总结 这个题目太大了,变化也层出不穷,这里只是我的一点心得,不定期更新! 1. 递推式问题 对于无穷进行的操作期望步数问题,一般可用递推式解决. 对于一个问题\(ans[x]\) ...
- 【HNOI2012】永无乡(splay,启发式合并)
题解 Description 永无乡包含 n 座岛,编号从 1 到 n,每座岛都有自己的独一无二的重要度,按照重要度可 以将这 n 座岛排名,名次用 1 到 n 来表示.某些岛之间由巨大的桥连接,通过 ...
- JavaScript:['1','2','3'].map(parseInt)问题解析
最近碰到了['1','2','3'].map(parseInt)这种看似不起眼陷阱却极大的问题. 这乍一看,感觉应该会输出[1,2,3].但是,实际上并不是我们想的这样.你可以现在打开console, ...
- php表单提交时获取不到post数据的解决方法
找到了一位博主的方法完美解决,链接如下: http://blog.csdn.net/whd526/article/details/53263181
- 关于LINUX各类系统资源整合
在系统维护的过程中,随时可能有需要查看 CPU和内存的使用率,并根据相应信息分析系统状况的需求.本文介绍一下几种常见的Linux系统资源查看命令. 1.总体内存占用的查看 命令:free 图1 fre ...
- 关于虚拟机打开ubuntu黑屏的问题
取消勾选“加速3D图形“后重启即可.
- win8快捷键
win+Q/S搜索所有位置 win+W搜索设置 win+E文件资源管理器 win+R运行 win+T选中第一个应用程序(不确定) win+U轻松使用设置中心 win+I设置 win+P投影 win+D ...