手把手教你写脚本引擎(五)——简单的高级语言(3,符号表) 陈梓瀚 华南理工大学软件本科05级
. J3 E; v2 i7 h$ F6 w& p符号表的结构的复杂度跟语言的语义规则的复杂度有关。对于C#来说,每一个符号都附带了一大堆信息,譬如位置啦,所在的namespace啦,类型啦什么的。对于JavaScript来说,符号表几乎是不需要的,因为东西都动态了,编译时几乎不检查内容。语义分析的输出是符号表,代码生成的输入是符号表和语法树。因此语法树除了放语法相关的内容,语义相关的内容最好放到符号表里面(譬如说表达式的类型啦,语句的scope结果啦)。关于一个现实中的符号表组织可以看CMinus的语义分析结果。0 L# v! y) U+ Y' `) u- }+ X
2 I. k1 B4 ^ @' q5 e首先我们要解决类型的表达问题。一门复杂的语言的类型有很多种。这里的种类指的不是int和string的区别,而是函数类型、结构类型这种区别。每一种类型还有很多附带的属性。在语义分析的过程中,我们经常要比较两个类型是否一致。于是符号表的类型表达要设计成易于读取、修改和比较。9 A5 l/ z3 u( _
1 ~/ J# [0 z# M9 x% i: T
我们通常由两种解决方法。第一种方法是用一个继承结构来表达。定义一个基类TypeBase,然后底下一堆继承。乍一看很OOP,实际不然。语义分析的时候我们对每一种特殊的类型都有一些特殊的操作,我们还是举那个判断类型是否相等的操作来说明一下。我们知道OOP里面的虚函数解决了一维的分派问题。我们拿到一个Base,对Base->Method求值,总是可以根据Base的实际类型来求值。如果我们需要对两个类型同时进行分派呢?譬如说Equal(Base1,Base2),这种操作当且仅当Base1和Base2的实际种类相同才有比较的意义。这个时候我们改造成Base1->Equal(Base2)的话,也是免不了对Base2进行一下dynamic_cast还是什么类似的操作的。& Q# p0 H0 Q$ p- t0 d, { ?
, d4 Z0 P% n, y+ ^: h所以我个人比较偏向于第二种做法。我们为每一个类型创建一个唯一的ID。譬如说int 是0啦,int(int,int)是1啦,int*是2什么的。比较两个类型是否相等就直接拿ID去比较,ID相等则类型相等,ID不相等则类型不相等。在实际操作上怎么做呢?我们知道语义分析的过程中会产生出一堆(理论上可以为无穷多的)新类型。每一种类型都有一些属性。譬如说基本类型是有限的,可以用enum来表达。而函数类型需要返回值和参数类型表。于是我们拿属性去要一个ID的时候,符号表首先检查这个类型是否已经存在,存在则返回对应的ID,不存在则创建一条新的记录,然后绑定一个新的ID。譬如CMinus的类型表采用如下接口分配ID:9 ]+ z! e. m- u) L1 P% d
, ~; s2 j9 ~6 A+ T3 L/ G# Q( A4 Jclass VL_CMinusTypeTable : public VL_Base {6 q+ U7 J, a' X% J. q; U1 x
public:- a0 }2 t U) {! K1 x
VInt GetPrimitiveType(VLE_CMinusPrimitiveType Type); VInt GetPointer(VInt Type); VInt GetArray(VInt Type , VInt Count); VInt GetFunction(VInt ReturnType , VL_List<VInt , true>& ParameterTypes); VInt CreateStruct(); VL_CMinusTypeSlot* GetType(VInt Type); };
* @6 h! T3 Y6 K/ u 3 |, A% l. _; H: f
如果我们已知一个类型的ID,求其指针类型的ID,就调用GetPointer(TypeID)。经过这一套函数的处理,我们总是可以不用担心是否在什么地方让两个ID指向了相同的类型,或者一个类型不小心拥有了多个ID,十分好管理。
) p" e/ f- M) W
, n4 _% ^5 @3 O5 } b) B第二个问题就是要保存每一个表达式的类型和语句的Scope了。我不建议将这些信息保存在语法树里面。原因比较复杂,因为一份代码在不同的上下文中可能有不同的意思,然后我们有一天突然有需要将这些环境中的这份代码的语义分析结果保留下来的话,如果东西原本是存在语法树里面的,那就完蛋了,只能去复制语法树了。于是我建议将语法分析得不到的信息通通存进符号表。因为表达式和语句都是指针,我们只需要一些map就可以将表达式和语句的附加信息存起来了。4 Z% @9 g3 w! ^) S7 z, I
8 ?3 E& f* g, a* v1 \# {4 C第三个问题是scope。一个变量或参数的作用范围是有限的,于是我们只好创建一个scope树,其中每一个节点都看得到父节点,至于能不能看到子节点我觉得是无所谓的。于是对于一个具体的scope来说,一个scope就变成了一个链表,保存了当前scope的所有符号名,然后还能知道直接或间接的父scope。下面举个直观的例子。假设我们有代码:
, X9 O$ m! E4 L% i8 p3 z' j8 N
( D3 o% L( T. @int A=0;! i& Z$ \& ~8 ?) t+ ?0 l
int B(int C,int D) {
4 U9 R0 |2 N# U2 Y8 N int E=0;
. B, @1 |5 m1 e, x, l4 C}
: V' V. X: O0 y5 U6 _ % m) p' T A' h* H1 [6 q7 p
为了处理这份代码,我们建立了三个scope。第一个是全局scope,记录了A和B。第二个是函数scope,记录了C和D。第三个是属于语句的一个scope,记录了E。于是我们用一个链表把他们串起来:语句scope -> 函数scope -> 全局scope。
. k' l t! W( y" Q/ t
+ c: k$ p: s) u, H$ I. X这样做的好处是我们查找scope会变得很方便。譬如现在的上下文是语句scope,那么它理应可以看见变量、参数、全局函数和全局变量。添加一个符号也很方便,只要当前的scope没有这个名字,不管上面的scope有没有我们都可以添加,添加完就把上面的scope的同名符号给覆盖了。. S2 E* @1 }/ l* ^4 E
" l1 x0 P* E1 [一个scope其实还可以记录其他的东西的,譬如距离最近的循环表达式啦(用来判断break是否应该存在),所属的函数啦(return后面要不要接表达式),还有其他的很多杂七杂八的东西。
" D! ~; Q( J3 X( e. f - l6 I% S: j: K/ W
第四个问题是如何创建符号表。之前的文章我们把语句和表达式都建立成了两个大型的继承结构。表达式添加一个函数叫GetType,返回一个ID。语句建立一个函数叫Validate,用来验证语句是否合法。他们的参数都是符号表和当前的scope,这样的话,表达式为了创建类型就会产生出一堆ID,语句为了让表达式可以知道每一个变量的类型就要创建scope。这么一递归下去,符号表也有了,类型也检查完了。所以上文才会说语义分析产生符号表。5 C" p: q# D) M! D+ n
* A8 T5 B& }5 G6 n符号表就介绍到这里了。一个高级语言所遇到的基本的问题其实都讲得差不多了。接下来的文章就针对具体的问题进行讲解了,譬如继承、反射、垃圾收集等等的跟具体语言相关的问题。4 _- w* N$ w5 n
|