前面的章节我们学会了如何解析语言、构建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》之 符号表的更多相关文章

  1. 《Language Implementation Patterns》之 解释器

    前面讲述了如何验证语句,这章讲述如何构建一个解释器来执行语句,解释器有两种,高级解释器直接执行语句源码或AST这样的中间结构,低级解释器执行执行字节码(更接近机器指令的形式). 高级解释器比较适合DS ...

  2. 《Language Implementation Patterns》之 数据聚合符号表

    本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置. 区别在于:作用域之外的代码能够通过一种特殊 ...

  3. 《Language Implementation Patterns》之 强类型规则

    语句的语义取决于其语法结构和相关符号:前者说明了了要"做什么",后者说明了操作"什么对象".所以即使语法结构正确的,如果被操作的对象不合法,语句也是不合法的.语 ...

  4. 《Language Implementation Patterns》之 构建语法树

    如果要解释执行或转换一段语言,那么就无法在识别语法规则的同时达到目标,只有那些简单的,比如将wiki markup转换成html的功能,可以通过一遍解析来完成,这种应用叫做 syntax-direct ...

  5. 《Language Implementation Patterns》之 增强解析模式

    上一章节讲述了基本的语言解析模式,LL(k)足以应付大多数的任务,但是对一些复杂的语言仍然显得不足,已付出更多的复杂度.和运行时效率为代价,我们可以得到能力更强的Parser. Pattern 5 : ...

  6. 《Language Implementation Patterns》之访问&重写语法树

    每个编程的人都学习过树遍历算法,但是AST的遍历并不是开始想象的那么简单.有几个因素会影响遍历算法:1)是否拥有节点的源码:2)是否子节点的访问方式是统一的:3)ast是homogeneous或het ...

  7. 《Language Implementation Patterns》之 语言翻译器

    语言翻译器可以从一种计算机语言翻译成另外一种语言,比如一种DSL的标量乘法axb翻译成java就变成a*b:如果DSL里面有矩阵运算,就需要翻译成for循环.翻译器需要完全理解输入语言的所有结构,并选 ...

  8. iOS 符号表恢复 & 逆向支付宝

    推荐序 本文介绍了恢复符号表的技巧,并且利用该技巧实现了在 Xcode 中对目标程序下符号断点调试,该技巧可以显著地减少逆向分析时间.在文章的最后,作者以支付宝为例,展示出通过在 UIAlertVie ...

  9. 符号表实现(Symbol Table Implementations)

    符号表的实现有很多方式,下面介绍其中的几种. 乱序(未排序)数组实现 这种情况,不需要改变数组,操作就在这个数组上执行.在最坏的情况下插入,搜索,删除时间复杂度为O(n). 有序(已排序)数组实现 这 ...

随机推荐

  1. PyTorch官方中文文档:torch.optim

    torch.optim torch.optim是一个实现了各种优化算法的库.大部分常用的方法得到支持,并且接口具备足够的通用性,使得未来能够集成更加复杂的方法. 如何使用optimizer 为了使用t ...

  2. json 的循环输出

    json不能用for-of循环,会报错 可以用for-in循环: var json = {'a':'apple','b':'banana','c':'orange','d':'pear'}; for( ...

  3. JVM介绍&自动内存管理机制

    1.介绍JVM(Java Virtual Machine,Java虚拟机) JVM是Java Virtual Machine的缩写,通常成为java虚拟机,作为Java可以进行一次编写,到处执行(Wr ...

  4. [BZOJ4736]温暖会指引我们前行

    BZOJ(BZOJ上的是什么鬼...) UOJ 任务描述 虽然小R住的宿舍楼早已来了暖气,但是由于某些原因,宿舍楼中的某些窗户仍然开着(例如厕所的窗户),这就使得宿舍楼中有一些路上的温度还是很低. 小 ...

  5. golang channel的使用以及调度原理

    golang channel的使用以及调度原理 为了并发的goroutines之间的通讯,golang使用了管道channel. 可以通过一个goroutines向channel发送数据,然后从另一个 ...

  6. Ansible学习总结(1)

    ---恢复内容开始--- 1. Ansible概述 ansible是新出现的自动化运维工具,基于Python开发,集合了众多运维工具(puppet.cfengine.chef.func.fabric) ...

  7. 特定场景下Ajax技术的使用

    ajax介绍 jax技术包含了几种技术:javascript.xml.css.xstl.dom.xhtml和XMLHttpRequest七种技术,所以ajax就像是粘合剂把七种技术整合到一起,从而发挥 ...

  8. 关于Eclipse无法识别手机或者模拟器的解决方案

    Android开发的时候经常会出现eclipse devices中不显示手机或模拟器的情况 网上有很多方法,但是都不实用.这里我提供一种方法: 如果手机连接上了不显示的话首先我们要确定我们手机的驱动是 ...

  9. Unity3D NGUI事件监听的综合管理

    首先,将Event Listener挂在按钮上 Event Listener的源码很简单 就是利用C#的时间委托机制 注册了UI场景的事件而已 public class UIEventListener ...

  10. 黄金K线理论简述

    黄金K线理论简述 [Ⅰ]. 隐藏在K线背后的多空搏杀 黄金K线的多空搏杀理论,说到底,其核心就是研判K线时,必须从多空搏杀的角度去认知,否则仅仅从表面到表面,是无法掌握K线精髓的.具体来说,多方和空方 ...