前面的章节我们学会了如何解析语言、构建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. WPF中使用WebBrowser

    最近在做北京现代项目的时候,遇到一个需求将韩国那边写好的网页嵌套到WPF程序中显示. 开始的时候使用的是第三方的浏览器控件:awesomium,在本地测试,显示没有问题.但是拿到客户现场,只显示半屏. ...

  2. Xcode 9.0 新增功能大全

    Xcode是用于为Apple TV,Apple Watch,iPad,iPhone和Mac创建应用程序的完整开发人员工具集.Xcode开发环境采用tvOS SDK,watchOS SDK,iOS SD ...

  3. [BZOJ1007] [HNOI2008] 水平可见直线 (凸包)

    Description 在xoy直角坐标平面上有n条直线L1,L2,...Ln,若在y值为正无穷大处往下看,能见到Li的某个子线段,则称Li为可见的,否则Li为被覆盖的. 例如,对于直线:L1:y=x ...

  4. linux字符集介绍及解决中文乱码实战

    字符集对应配置文件:/etc/sysconfig/i18n 先备份再修改: 备份:cp /etc/sysconfig/i18n /etc/sysconfig/i18n.oldboy.20150521 ...

  5. 图之单源Dijkstra算法、带负权值最短路径算法

    1.图类基本组成 存储在邻接表中的基本项 /** * Represents an edge in the graph * */ class Edge implements Comparable< ...

  6. 仿QQ发语音、图片选择、表情选择demo

    一款仿QQ发语音.图片选择.调用拍照.表情选择的demo git地址:https://github.com/PureLovePeter/pic.git.  喜欢的请 star  star star,共 ...

  7. 深入理解Java虚拟机到底是什么

    摘自:http://blog.csdn.net/zhangjg_blog/article/details/20380971 什么是Java虚拟机 我们都知道Java程序必须在虚拟机上运行.那么虚拟机到 ...

  8. jni 类初始化失败(nested exception is java.lang.NoClassDefFoundError)

    nested exception is java.lang.NoClassDefFoundError: Could not initialize class com.netease.facedetec ...

  9. UML系列图

    用例图: 时序图:

  10. FMDatabaseQueue 如何保证线程安全

    这篇文章原来在用 Github Pages 搭建的博客上,现在决定重新用回博客园,所以把文章搬回来. FMDB 是 OC 针对 sqlite 的封装.在其文档的线程安全部分这样讲:同时从多个线程使用同 ...