(基于Java)编写编译器和解释器第4章:符号表(连载)

作为语义分析的一部分,解释器/编译器的解析器在整个翻译过程中创建和维护符号表。符号表用来存储源文件中的token数据信息,基本上跟标识符有关。如你在图1-3和2-1中所看到的,符号表是横在前端和后端之间即中间层的一个核心组件。

==>> 本章中文版源代码下载:svn co http://wci.googlecode.com/svn/branches/ch4/ 源代码使用了UTF-8编码,下载到局部请修改!

目标与方法

对于编译器开发者来说,维护一个组织良好的符号表(Symbol Table)是一个重要技能。当编译器/解释器翻译源程序时,它必须能够快速有效地建立新数据,访问和更新现存数据。否则翻译过程会变慢或变糟,生成不正确的结果。

本章目标是:

  • 一个灵活的,语言无关的符号表。
  • 一个简单的实用程序用来解析Pascal 源程序,并生成一个标识符(ID)交叉引用(cross-reference)列表。

方法先建立符号表的概念设计,接着开发表现此设计的Java接口,最后编写Java类实现接口。交叉引用的实用程序将帮助验证你代码的正确性,它将通过建立(entering),查找(finding),和更新(updating)数据等操作来使用符号表。

符号表概念设计

在翻译过程中,编译器/解释器创建和更新符号表的表项(entry),表项用来存储源程序中某些token的信息。每个表项有个名字即token的文本串。例如,存储标识符 token的表项用了标识符的名字,它还包括标识符的其它信息。在源程序翻译过程中,编译器/解释器查找和更新这些信息。

符号表要放什么样的数据? 有用的数据!有关标识符的符号表项一般会包括它的类型,结构,以及是怎么定义的。(符号表概念设计)目标之一保持符号表的灵活性,使其不仅限于Pascal。不管符号表存储什么样的信息,它必须要支持的基本操作有:

  • 建立新数据(enter new information)
  • 查找现存数据(look up existing information)
  • 更新现存数据(update existing information)

符号表栈


为解析块状结构化的语言比如Pascal,你实际上需要多个符号表(当然类型只有一个,这儿是说多个实例)-- 全局符号表一个,每个主程序一个,每个过程(procedure)和函数(function)一个,每个记录(Record)类型一个。因为Pascal函数和记录可以嵌套(routine,procedure和function都是routine),符号表必须在一个栈上维护。栈顶的符号表维护当前解析器正处理的程序、函数、结构、记录的相关信息。当解析器按照某种方式解析一个Pascal程序时,碰到进入和离开嵌套函数和记录类型定义的情况,分别压入符号表(入栈)和弹出符号表(出栈)。栈顶的符号表就是俗称的局部表(local table)。(如果全局表是栈中唯一元素,那么它也是局部表)

图4-1 展示了符号表栈、符号表、符号表项的概念设计。在概念设计中,符号表栈包含一个或多个符号表,而每个表中又包含多个表项。每个符号表项包含一个一般为标识符 token的信息,还包含表项名称和属性形式的token信息。符号表以表项名为搜索关键字搜索相关表项。

image

此时你不需知道也不需关心该用何种数据结构构建符号表或该如何存储表项。根据概念设计,你仅需要明白符号表有哪些重要组件,它们各扮演什么角色,以及相互之间的关系。

设计笔记

理想情况下,编译器/解释器的其它组件不必知道太多符号表概念层面以外的东西(比如实现),这维护了主要组件之间的松耦合关系。

直到第9章,你仅会在栈中看到一个符号表。现在定义符号表将会使得后续涉及到多个符号表的翻译过程更轻松。

(图中的Symbol table为符号表,Entry为符号表中的一项。符号表项有名称name和各式其它属性attributes)。

点击图片放大看

 

符号表接口

根据图4-1所示的符号表组件,你可得到一个接口的UML设计图,这些接口放在包intermediate中。

图4-2:符号表接口

image

尽管目前栈中只有一个(符号)表,你也能引入符号表的一些关键操作:

* 在局部符号表中建立一个新的表项,当前的表在栈顶。

* 通过搜索局部符号表查找某个表项。

* 在栈中的所有表中查找某个表项。

一旦某个符号表项被找到,你就能更新它的内容。注意你只能在局部表中建立新表项。然后你能在局部表或栈中的所有表中查找某表项。

接口SymTabStack支持上面的这些操作。方法enterLocal在局部表中建立一个新项。方法lookupLocal搜索局部表,还有方法lookup搜索栈中所有表。接口SymTabStack定义了另两个方法:getCurrentNestingLevel()返回当前的嵌套层级及getLocalSymTab()返回栈顶的局部符号表。直到第9章才涉及到嵌套(多个符号表,目前只有一个)。

接口SymTabEntry 表示一个符号表项。方法getName()获取项的名称,方法setAttributes和getAttributes分别设置和返回表项的属性信息。为支持交叉引用,方法appendLineNumber() 在每次表项名称出现时,存储对应的源代码行位置。方法getLineNumbers()返回表项所有的行位置信息。每个表项保留一个指向包含它的符号表的引用,方法getSymTab返回这个引用。

根据图4-2中的UML类图很自然的创建这些Java接口(为毛不用代码生成,还要手写??)。清单4-1 展示了SymTabStack接口。

 

点击图片放大看

 

 

 

清单4-1:SymTabStack接口 详细参见本章源代码,这里不再显示。

第二章有SymTab接口的早期版本。清单4-2 展示了一个内容更丰富的版本。详细参见本章源代码,这里不再显示。

清单4-3 展示了SymTabEntry接口。详细参见本章源代码,这里不再显示。

最后,清单4-4 展示SymTabKey的占位接口,它用来表示表项的属性键。 详细参见本章源代码,这里不再显示。

符号表工厂

图4-3 展示了符号表实现类的UML类图,这些类都在包intermediate.symtabimpl中。

image

设计笔记

拥有权(ownership)箭号(arrow)的箭头端的星号'*'表示重数(multiplicity,多个的意思)。左边的图展示了一个SymtabStackImpl对象可拥有0或多个SymTab对象,一个SymTab对象能拥有0或多个SymTabEntry对象。

设计笔记

使用接口定义全部符号表组件使得其它组件(语法分析器等)在代码调用符号表的时候只需要关注接口,而不需要知道任何符号表的具体实现。这样的松耦合为最大程度上支持了灵活性。你可将符号表实现成你任意喜欢的那种,不管实现怎么改,只要接口不变,其它组件的调用也就不需要改。换句话说,所有调用者仅需要明白概念级别上的符号表即可。(而不需要关心实现上的符号表

因为你希望符号表的调用代码仅关注它的接口,你得搞个符号表工厂将调用者与实现细节隔离开来。清单 4-5 展示了类SymTabFactory。每个方法可构造某个此实现类的实例,返回实现的接口。详细参见本章源代码,这里不再显示。

因为一个编译器/解释器仅有单个符号表栈,你能通过一个静态域指向它。清单展示了框架类Parser中的静态域symTabStack。

清单4-6 Parser类的静态域(符号表栈)

protected ICode iCode;      // 语法树根节点。
//符号表栈
protected static SymTabStack symTabStack = SymTabFactory.createSymTabStack();

符号表实现

现在将准备开发符号表接口的实现了。图4-3 展示了实现接口的类以及对更下级(被包含的元素是更下级元素)接口的拥有关系(ownership relationship)。例如,类SymTabImpl实现了接口SymTab,每个SymTabImpl对象拥有0个或多个SymTabEntry对象。再比如,一个SymTabEntryImpl对象引用包含它的SymTab对象。

设计笔记

因为实现类自己只对接口(这里肯定是除实现接口以外的其它接口了)编码,所以没有实现类依赖其他实现类(比如SymTabImpl只对SymTabEntry接口编码,所以它不依赖SymTabEntryImpl)。

类SymTabStackImpl实现了SymTabStack接口并扩展了java.util.ArrayList<SymTab>。换句话说,将符号表堆栈用数组表(array list)实现。清单4-7 展现了这个类的关键方法。留意在构造函数中创建和添加了一个符号表在堆栈中(也就是默认的全局表)。

public class SymTabStackImpl
extends ArrayList<SymTab>
implements SymTabStack
{
private int currentNestingLevel; // 当前嵌套,默认为全局的0
public SymTabStackImpl()
{
this.currentNestingLevel = 0;
add(SymTabFactory.createSymTab(currentNestingLevel));
}
public SymTabEntry enterLocal(String name)
{
return get(currentNestingLevel).enter(name);
}
public SymTabEntry lookupLocal(String name)
{
return get(currentNestingLevel).lookup(name);
}
public SymTabEntry lookup(String name)
{
//目前只有一个符号表在栈中,所以全局即局部
return lookupLocal(name);
}
}

方法enterLocal和lookupLocal仅牵涉到栈顶的局部符号表。目前域currentNestingLevel总是返回0(因为只有一个全局表,没有实际嵌套发生)。还有,因为只有栈中只有一个符号表,方法lookup()和lookupLocal()功能上是一样的。方法get()被基类的ArrayList定义。

清单4.8 展示了类SymTabImpl的关键方法,它实现SymTab接口并扩展了java.util.TreeMap即以键的升序来存储每项的哈希表。

public class SymTabImpl
extends TreeMap<String, SymTabEntry>
implements SymTab
{
private final int nestingLevel; //所在嵌套层次

public SymTabImpl(int nestingLevel)
{
this.nestingLevel = nestingLevel;
}
public SymTabEntry enter(String name)
{
SymTabEntry entry = SymTabFactory.createSymTabEntry(name, this);
put(name, entry);

return entry;
}
public SymTabEntry lookup(String name)
{
return get(name);
}
@Override
public Collection<SymTabEntry> sortedEntries() {
return Collections.unmodifiableCollection(values());
}
}
每当有符号表被创建,它的nestingLevel域被设置成当前的嵌套层级。目前总是0。方法enter()和lookup()分别调用了底层哈希表的put()和get()方法。enter()首先用符号表工厂以某个给定名字创建一个SymTabEntry对象。方法sortedEntries()返回只读的有序表项。
类SymTabEntryImpl实现了SymTabEntry接口并扩展了java.util.HashMap。清单4-9 展示了它的关键方法。正如你在图4-1看到的概念设计一样,每个符号表项包含一个名字和一堆属性。在类图4-3中你添加了域symTab用来保留一个指向包含此项的符号表引用,还添加了lineNumbers用来维护源文件中此项出现的所有行位置信息。
清单4-9:类SymTabEntryImpl中的关键方法
   1: public class SymTabEntryImpl
   2:     extends HashMap<SymTabKey, Object>
   3:     implements SymTabEntry
   4: {
   5:     private String name;                     // 名称
   6:     private SymTab symTab;                   // 所在表
   7:     private ArrayList<Integer> lineNumbers;  // 所有出现的行位置
   8:  
   9:     public SymTabEntryImpl(String name, SymTab symTab)
  10:     {
  11:         this.name = name;
  12:         this.symTab = symTab;
  13:         this.lineNumbers = new ArrayList<Integer>();
  14:     }
  15:     public void appendLineNumber(int lineNumber)
  16:     {
  17:         lineNumbers.add(lineNumber);
  18:     }
  19:     public void setAttribute(SymTabKey key, Object value)
  20:     {
  21:         put(key, value);
  22:     }
  23:     public Object getAttribute(SymTabKey key)
  24:     {
  25:         return get(key);
  26:     }
  27: }

方法setAttribute()和getAttribute分别调用了父哈希表的put()和get()方法。

设计笔记

就每项你能存储什么这点来说,使用hashmap实现符号表项的设计提供了最大的灵活性。

清单4-10 展示了枚举类型SymTabKeyImpl,它实现了接口SymTabKey。后续章节将会用到这些键。详细参见本章源代码,这里不再显示。

到此完成了本章的第一个目标,即建立一个灵活的,语言无关的符号表。符号表的灵活性体现在调用者只需关注它的接口,因而容许后续实现的改进。符号表项能存储任意信息,并没有Pascal特定限制。

程序4:Pascal交叉引用 I

你即将验证新创建的符号表。你可通过生成Pascal源程序中的标识符(ID)交叉引用列表来完成这个目标(即本章的第二个目标)。清单4-11 展示了用类似下面的命令行产生的输出样例。

java -classpath classes Pascal compile -x newton.pas

-x选项用来生成交叉引用列表。

清单4-11:一个生成的交叉引用清单。


----------代码解析统计信--------------
源文件共有 36行。
有 0个语法错误.
解析共耗费 0.01秒.

============ 交叉引用列表 ========

Identifier 所在行位置
---------- ------------
abs 031 033
epsilon 004 033
input 001
integer 007
newton 001
number 007 014 016 017 019 023 024 029 033 035
output 001
read 014
real 008
root 008 027 029 029 029 030 031 033
sqr 033
sqroot 008 023 024 031 031
sqrt 023
write 013
writeln 012 017 020 024 025 030

----------编译统计信--------------
共生成 0 条指令
代码生成共耗费 0.00秒
如清单所示,所有标识符(identifier列)按照字母次序列出来。每个标识符名字后是它出现过的行位置。留意所有的标识符名字被转化成为小写。
修改Parser子类PascalParserTD中的parse()方法方便符号表加入新标识符。如下清单。
清单4-12:PascalParserTD中的parse方法
   1: public void parse() throws Exception {
   2:     Token token;
   3:     long startTime = System.currentTimeMillis();
   4:  
   5:     try {
   6:         while (!((token = nextToken()) instanceof EofToken)) {
   7:             TokenType tokenType = token.getType();
   8:             if (tokenType == PascalTokenType.IDENTIFIER){
   9:                 String entry_name = token.getText().toLowerCase();
  10:                 //是否出现过
  11:                 SymTabEntry entry_existed = symTabStack.lookup(entry_name);
  12:                 if (null == entry_existed){
  13:                     //第一次出现?
  14:                     entry_existed = symTabStack.enterLocal(entry_name);
  15:                 }
  16:                 entry_existed.appendLineNumber(token.getLineNumber());
  17:             }else if (tokenType==PascalTokenType.ERROR){
  18:                 // 留意当token有问题是,它的值表示其错误编码
  19:                 errorHandler.flag(token,
  20:                         (PascalErrorCode) token.getValue(), this);
  21:             }else{
  22:                 //其它暂且忽略
  23:             }
  24:         }
  25:         // 发送编译摘要信息
  26:         float elapsedTime = (System.currentTimeMillis() - startTime) / 1000f;
  27:         sendMessage(new Message(PARSER_SUMMARY, new Number[] {
  28:                 token.getLineNumber(), getErrorCount(), elapsedTime }));
  29:     } catch (IOException e) {
  30:         errorHandler.abortTranslation(PascalErrorCode.IO_ERROR, this);
  31:     }
  32: }
上面代码中的第8到23行为修改的部分。parse()方法遍历源程序中所有token,如果token的类型为IDENTIFIER,它尝试在局部(local)符号表中查找这个标识符,假如没找到,此方法在局部表中为此标识符建一个新的项。接着方法调用entry_existed.appendLineNumber()添加当前token所在行位置。记住因为Pascal不分大小写,须调用String的方法toLowerCase()将标识符的名字变成小写存入。
String name = token.getText().toLowerCase();

清单4-13 展示了util包中的新辅助类CrossReferencer。详细参见本章源代码,这里不再显示。

方法printSymTab()遍历符号表的有序表项。对每个项,它先打印出标识符的名字,接着是所有行位置的明细。

在第9章,在学过怎么解析Pascal声明(declarations)后,你将会写一个更NB的CrosssReferencer版本。

为支持打印交叉引用表,须在Pascal主程序的构造函数中做些小改动。

首先将源程序的行输出注释掉,因为没啥意义。详见源程序中的第49行。

//source.addMessageListener(new SourceMessageListener());
然后添加一个域stack指向程序运行过程中的符号表堆栈:
private SymTabStack stack;
接着在Pascal构造函数中的62行处,增加打印交叉引用语句,详见下面的粗体部分。
// 生成中间码和符号表
iCode = parser.getICode();
//symTab = parser.getSymTab();
stack = parser.getSymTabStack();
if (xref){
CrossReferencer cr = new CrossReferencer();
cr.print(stack);
}
// 交由后端处理
backend.process(iCode, stack);
注意这儿有个较大的变化就是前面章节中的symTab域将会被stack取代(作者刚开始为简单没有引入符号表堆栈,只有个符号表占位,现在加进了,所有有好几api要改)。构造函数现在从parser中取符号表栈,而不是符号表。你需要修改Backend抽象类的process方法,将第二个参数的类型由SymTab改成SymTabStack。因为抽象process是个抽象函数,你还必须改它的两个继承类的实现:compiler和executor。
 
如果 "-x"命令行参数存在,则构造函数创建一个新的CrossReferencer对象,然后调用它的print方法生成一个交叉引用列表。
原文地址:https://www.cnblogs.com/lifesting/p/2599150.html