《Language Implementation Patterns》之 数据聚合符号表
本章学习一种新的作用域,叫做数据聚合作用域(data aggregate scope),和其他作用域一样包含符号,并在scope tree里面占据一个位置。
区别在于:作用域之外的代码能够通过一种特殊的表达式user.name
来访问数据成员;以下两个模式分别描述非面向对象语言和面向对象语言的数据聚合作用域。
- Pattern 18, Symbol Table for Data Aggregates,描述了如何定义和访问简单的数据聚合,比如C struct;
- Pattern 19, Symbol Table for Classes, 描述了如何处理拥有基类、包含方法的数据聚合。
struct和class非常相似,都是一个符号,自定义类型,一个作用域;最大的区别在于class有一个superclass,相当于一个外围嵌套作用域。、
struct scope
先看一个struct的例子:
// start of global scope
struct A {
int x;
struct B { int y; };
B b;
struct C { int z; };
C c;
};
A a;
void f()
{
struct D {
int i;
};
D d;
d.i = a.b.y;
}
对应的scope tree如下:
stuct定义了一个scope,该scope的外围scope就是定义所处的scope;在struct内部,我们像之前一样寻找符号,因此int符号最终引用的是全局的int类型;对于像expr.x这样的表达式,要确定expr的类型,然后在类型对应的scope里面寻找x。使问题变得复杂的地方在于,表达式expr.x在寻找x的时候,只能严格地在对应类型的scope里面寻找,而不能像普通名字一样,继续在外围scope寻找。
class scope
class可以有一个superclass,这就使得class可能有两个父scope,一个是通常的外围嵌套的scope,一个是superclass对应的scope。在做符号引用的时候,追随哪个父scope都有可能。
// start of global scope
class A {
public:
int x;
void foo()
{ ; }
};
class B : public A {
int y;
void foo()
{
int z = x + y;
}
};
上面类定义对应的scope tree如下:
从Scope B指向Scope A的箭头是横向的,这里表达的意思是,ClassB和ClassA的scope处在同一层级。在成员函数内部,面向对象语言一般会优先使用superClass这条路径来搜寻符号。
前置引用
class里面允许提前引用一个名字定义:
class A {
void foo() { x = 3; } // forward reference to field x
int x;
};
解决这个问题,可以使用多轮次处理AST的方式,第一轮只进行符号定义,第二轮再处理符号引用。
但是又会引入另一个问题,有时候前置引用一个名字是非法的,比如以下的C++代码:
// globals
int main() {
x = y; // shouldn't see global x or local y; ERROR!
int y;
}
int x;
解决这个问题的方法是引入token index的概念,在引用local和global scope的场合,如果引用处的token index小于定义处的token index,那么该引用是非法的。
Pattern 18, Symbol Table for Data Aggregates
关于如何实现简单的struct scope,还是用类似上一章的表格来表示,仅添加几项如下:
Upon | Actions |
struct declaration S | def S as a StructSymbol object in the current scope and push it as the current scope. |
Member access «expr».x | Compute the type of «expr» using the previous rule and this one recursively. Ref x only in that type’s scope, not in any enclosing scopes. |
Pattern 19, Symbol Table for Classes
上面说过需要多轮次的AST访问才能构建好Class的scope tree;AST node里面记录对应的scope信息以便下一轮访问。
为了看清楚这种联系,看一段简单的代码:
int main() {
int x;
x = 3;
}
最终的scope tree和AST如下图所示,省略了中间步骤:
x对应的VariableSymbol包含了一个指向定义AST node的指针;x的引用节点和定义节点都指向了同一个Symbol。
Class的scope有两个可能的父scope,因此在scope的类里面定义了两个方法:
/** Where to look next for symbols; superclass or enclosing scope */
public Scope getParentScope();
/** Scope in which this scope defined. For global scope, it's null */
public Scope getEnclosingScope();
对于非Class的scope来说,getParentScope返回的就是EnclosingScope;对于Class的scope来说,getParentScope默认返回superClassScope,如果没有则返回EnclosingScope。
Class的scope tree构造需要两轮AST遍历,第一轮构造出struct的Scope Tree结构:
Upon | Actions |
Class declaration C | 在当前scope构造ClassSymbol,sym,并push sym入栈成为新的current scope,sym的def字段指向对应类名ID的AST节点,ID AST节点的symbol字段指向sym;sym的superclasss scope指向自身 |
第二轮将superclass scope指针设置好:
Upon | Actions |
Class declaration C | 设t为C的superclasss的ID AST节点. 引用t所定义的scope, 得到sym. 设置t.symbol=sym. 设置C对应scope的superclass指针 =sym. |
在class的方法内部访问符号x的规则如下:先在class scope里面寻找,然后顺着superClass链寻找,最后在global scope里面寻找。
通过《expr》.x的方式访问名字x的规则:先在class scope里面寻找,然后顺着superClass链寻找。
具体的实现代码,请参考原书。