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

    sprintf用来格式化字符串 说明 string sprintf ( string $format [, mixed $args [, mixed $... ]] 参数 $format 可能的格式值 ...

  2. JAVA流式布局管理器--JAVA基础

    JAVA流式布局管理器的使用: FlowLayoutDeme.java: import java.awt.*;import javax.swing.*;public class FlowLayoutD ...

  3. SpringMVC 框架系列之组件概述与配置详解

    在上一篇文章 SpringMVC 框架系列之初识与入门实例 的实例中,我们已经知道,SpringMVC 框架是一个 web 层的框架,本篇文章就详细解释一下 SpringMVC 框架具体文件的配置以及 ...

  4. 【BZOJ3277】串(后缀自动机)

    [BZOJ3277]串(后缀自动机) 题面 BZOJ 题解 广义后缀自动机??? 照着别人的打了一遍.. 相当于每个串都构建一个后缀自动机 构建完一个串之后,直接把当前的last指回root就行了?? ...

  5. Tomcat 设置开机自启

    操作系统centos6.5: Vim /etc/rc.local在末尾添加一下两行 source /etc/profile /test/tomcat/bin/startup.sh 我解释下为什么要加s ...

  6. 配置linux软件下载跟新地址

    /etc/yum.repos.d/CentOS-Base.repo 直接到mirrors.aliyum.com下载 diff:比较

  7. C# Redis实战(一)

    一.初步准备 Redis 是一个开源的使用ANSI C 语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value 数据库.Redis的出现,很大程度补偿了memcached这类key/va ...

  8. HashSet实现不重复储值原理-附源码解析

    在HashSet中,基本的操作都是由HashMap底层实现的,因为HashSet底层是用HashMap存储数据.当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后用这个(元素的 ...

  9. 【Unity与23种设计模式】访问者模式(Visitor)

    GoF中定义: "定义一个能够在一个对象结构中对于所有元素执行的操作.访问者让你可以定义一个新的操作,而不必更改到被操作元素的类接口." 暂时没有完全搞明白 直接上代码 //访问者 ...

  10. 设计模式——桥接模式(C++实现)

    [root@ ~/learn_code/design_pattern/18_bridge]$ cat Abstraction.h #ifndef _ABSTRACTION_H_ #define _AB ...