前面的章节我们学会了如何解析语言、构建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. 从零一起学Spring Boot之LayIM项目长成记(三) 数据库的简单设计和JPA的简单使用。

    前言 今天是第三篇了,上一篇简单模拟了数据,实现了LayIM页面的数据加载.那么今天呢就要用数据库的数据了.闲言少叙,书归正传,让我们开始吧. 数据库 之前有好多小伙伴问我数据库是怎么设计的.我个人用 ...

  2. table问题汇总

    平时要使用 table 的次数说多不多,说少不少,但每次使用必定会被几个问题"坑"一下,所以这篇博客用来记录下table那些"小坑".但 table 也会有很多 ...

  3. SpringBoot特性

    一.SpringBoot解决的问题 1.使编码变得简单 2.使配置变得简单 3.使部署变得简单 4.使监控变得简单 二.springboot主要特性 1.遵循习惯优于配置的原则.使用springboo ...

  4. CentOS 7.x 防火墙开放端口相关用法记录

    前言 防火墙对服务器起到一定的保护作用,所以了解一些相关的操作是很有必要的. 在CentOS 7.x中,有了一种新的防火墙策略,FireWall , 还记得在6.x中用的还是iptables. 这几天 ...

  5. [SHOI2012]随机树

    [SHOI2012]随机树 题目大意( 网址戳我! ) 随机树是一颗完全二叉树,初始状态下只有一个节点. 随机树的生成如下:每次随机选择一个叶子节点,扩展出两个儿子. 现在给定一个正整数\(n\)(\ ...

  6. CDQ 分治算法模板

    CDQ分治 1.三维偏序问题:三维偏序(陌上花开) #include<bits/stdc++.h> #define RG register #define IL inline #defin ...

  7. Hadoop 安装流程

    前言:因项目中需要数据分析,因而使用hadoop集群通过离线的方式分析数据 参考着网上的分享的文章实施整合的一篇文章,实施记录 安装流程: 1.设置各个机器建的ssh 无密码登陆 2.安装JDK 3. ...

  8. Django入门-基本数据库API

    # 现在系统里还没有 Question 对象 >>> Question.objects.all() <QuerySet []> # 创建新 Question # 在 se ...

  9. Selenium元素定位之Xpath

    Xpath非常强大,使用Xpath可以代替前六种基本的定位方式,这种定位方式几乎可以定位到页面上的任何元素. Xpath简介 Xpath就是xml path,是一种在xml中查找信息的语言,因为htm ...

  10. Java的类C结构体的实现,以及对类的某一属性进行排序

    public static class Foo { public int x1; public int x2; public int day; } public static Foo[] bridge ...