标记表的布局的巨大度跟语言的语义法则的巨大度有关。对付C#来说,每一个标记都附带了一大堆信息,譬如位置啦,地址的namespace啦,范例啦什么的。对付JavaScript来说,标记表险些是不需要的,因为对象都动态了,编译时险些不查抄内容。语义阐明的输出是标记表,代码生成的输入是标记表和语法树。因此语法树除了放语法相关的内容,语义相关的内容最好放到标记表内里(譬如说表达式的范例啦,语句的scope功效啦)。关于一个现实中的标记表组织可以看CMinus的语义阐明功效。
首先我们要办理范例的表达问题。一门巨大的语言的范例有许多种。这里的种类指的不是int和string的区别,而是函数范例、布局范例这种区别。每一种范例尚有许多附带的属性。在语义阐明的进程中,我们常常要较量两个范例是否一致。于是标记表的范例表达要设计成易于读取、修改和较量。
我们凡是由两种办理要领。第一种要领是用一个担任布局来表达。界说一个基类TypeBase,然后底下一堆担任。乍一看很OOP,实际否则。语义阐明的时候我们对每一种非凡的范例都有一些非凡的操纵,我们照旧举谁人判定范例是否相等的操纵来说明一下。我们知道OOP内里的虚函数办理了一维的分配问题。我们拿到一个Base,对Base->Method求值,老是可以按照Base的实际范例来求值。假如我们需要对两个范例同时举办分配呢?譬如说Equal(Base1,Base2),这种操纵当且仅当Base1和Base2的实际种类沟通才有较量的意义。这个时候我们改革成Base1->Equal(Base2)的话,也是免不了对Base2举办一下dynamic_cast照旧什么雷同的操纵的。
所以我小我私家较量方向于第二种做法。我们为每一个范例建设一个独一的ID。譬如说int 是0啦,int(int,int)是1啦,int*是2什么的。较量两个范例是否相等就直接拿ID去较量,ID相等则范例相等,ID不相等则范例不相等。在实际操纵上怎么做呢?我们知道语义阐明的进程中会发生出一堆(理论上可觉得无穷多的)新范例。每一种范例都有一些属性。譬如说根基范例是有限的,可以用enum来表达。而函数范例需要返回值和参数范例表。于是我们拿属性去要一个ID的时候,标记表首先查抄这个范例是否已经存在,存在则返回对应的ID,不存在则建设一条新的记录,然后绑定一个新的ID。譬如CMinus的范例表回收如下接口分派ID:
class VL_CMinusTypeTable : public VL_Base
{
public:
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);
};
假如我们已知一个范例的ID,求其指针范例的ID,就挪用GetPointer(TypeID)。颠末这一套函数的处理惩罚,我们老是可以不消担忧是否在什么处所让两个ID指向了沟通的范例,可能一个范例不小心拥有了多个ID,十分好打点。
第二个问题就是要生存每一个表达式的范例和语句的Scope了。我不发起将这些信息生存在语法树内里。原因较量巨大,因为一份代码在差异的上下文中大概有差异的意思,然后我们有一天溘然有需要将这些情况中的这份代码的语义阐明功效保存下来的话,假如对象原本是存在语法树内里的,那就垮台了,只能去复制语法树了。于是我发起将语法阐明得不到的信息通通存进标记表。因为表达式和语句都是指针,我们只需要一些map就可以将表达式和语句的附加信息存起来了。
第三个问题是scope。一个变量或参数的浸染范畴是有限的,于是我们只好建设一个scope树,个中每一个节点都看获得父节点,至于能不能看到子节点我以为是无所谓的。于是对付一个详细的scope来说,一个scope就酿成了一个链表,生存了当前scope的所有标记名,然后还能知道直接或间接的父scope。下面举个直观的例子。假设我们有代码:
int A=0;
int B(int C,int D)
{
int E=0;
}
为了处理惩罚这份代码,我们成立了三个scope。第一个是全局scope,记录了A和B。第二个是函数scope,记录了C和D。第三个是属于语句的一个scope,记录了E。于是我们用一个链表把他们串起来:语句scope -> 函数scope -> 全局scope。
这样做的长处是我们查找scope会变得很利便。譬如此刻的上下文是语句scope,那么它理应可以瞥见变量、参数、全局函数和全局变量。添加一个标记也很利便,只要当前的scope没有这个名字,不管上面的scope有没有我们都可以添加,添加完就把上面的scope的同名标记给包围了。
#p#分页标题#e#
一个scope其实还可以记录其他的对象的,譬如间隔最近的轮回表达式啦(用来判定break是否应该存在),所属的函数啦(return后头要不要接表达式),尚有其他的许多杂七杂八的对象。
第四个问题是如何建设标记表。之前的文章我们把语句和表达式都成立成了两个大型的担任布局。表达式添加一个函数叫GetType,返回一个ID。语句成立一个函数叫Validate,用来验证语句是否正当。他们的参数都是标记表和当前的scope,这样的话,表达式为了建设范例就会发生出一堆ID,语句为了让表达式可以知道每一个变量的范例就要建设scope。这么一递归下去,标记表也有了,范例也查抄完了。所以上文才会说语义阐明发生标记表。
标记表就先容到这里了。一个高级语言所碰着的根基的问题其实都讲得差不多了。接下来的文章就针对详细的问题举办讲授了,譬如担任、反射、垃圾收集等等的跟详细语言相关的问题。