原文地址:
http://www.flipcode.com/articles/scripting_issue04.shtml
作者:Jan Niestadt
译者:Tony Qu
介绍
既然我们在上两部分中做了一些有益的事,我们就需要把我们从程序的数据结构中收集的数据保存起来,这就是我们接下来要做的事情,其中有两个相当重要的东西:符号表和文法树
符号表,有点像命名建议,它是一张包含了所有我们的程序中需要用到的符号的表,在我们的案例中,也包括所有的字符串变量和常量字符串。如果你有语言包括函数和类,那么它们也将在符号表中出现。
文法树是我们的程序结构的一种树形的表示形式,看看下面的图。在下一部分中,我们将用这个表示形式来生成中间代码,虽然并没有强制规定一定要生成文法树(因为我们已经从解析器那里获得了所有的程序结构信息),我觉得这使得编译器更加透明,这也是我写这篇文章的原因。
这将是第一个拥有“真正”代码的部分,在你看到它之前,我想澄清一点这些代码是为了更好的理解而写的,但可能结构方面不是很好,它将满足我们正在制作的编译器的需要,但真实的编译器远远不止这些。我会在遇到实际问题时,提到一些真实编译器的东西。
在规则间传递信息
很明显,我们不得不向解析器添加功能——当我们找到一个符号时,我们把它添加到符号表中,但是我们也需要“家长”规则来了解符号的描述信息(家长规则是指真正使用唯一标识符的规则)。
当我们构建一棵文法树的时候,一些相似的东西是必须的。我们需要家长规则拥有一个指向孩子规则结点的指针(孩子规则是由家长规则构建而成的)
还记得yylval集吗?Yacc也使用这个集合在规则间传递信息。在yylval集中,每个规则会有一个相关联字段,那是规则的类型。在string.y代码的最上面,你可以看到下面的类型声明
%type <symbol> identifier string
%type <tnode> statement expression
symbol和tnode是这个集合的新的结点,它们分别代表指向符号描述信息的指针和指向文法树结点的指针。
现在声明规则使用下面的类型
| expression END_STMT {$$ = new TreeNode (EXPR_STMT, $1);}
这意味着:如果你找到一个表达式声明,构建一个新END_STMT类型的树结点(返回结点指针),这个结点有一个孩子结点,它指向这个声明的组成。$$表
示规则的返回值,$1是由规则定义(表达式)中第一个符号返回的值。$2在这里没有任何意义,因为词法分析器没有为END_STMT符号设置yylval
成员。
我希望这个解释足够清楚,因为它很重要。从本质上讲,这些规则是有层次的,每个规则会返回一个值给更高层次的规则。
现在让我们看看,我们使用什么数据结构来描述符号表和文法表。
符号表
我们例子中的规则表仅有很少的信息,基本上只有变量名称和它第一次被定义的行所在的行号,当然在之后我们要用符号表来保存更多的数据。
实现其实很简单:用一个单向链表保存符号描述信息,当我们要获得一个符号的时候,可以线性搜索这个链表(看看symtab.cpp,这个文件很直观)。对于一个真实的编译器,符号表通常是用二叉搜索树或哈希表实现的,只有这样,我们才能快速获得一个符号。
当解析器找到一个符号后,我们要把符号放入符号表,需要做以下动作
identifier
: ID
{
$$ = st.Find ($1);
if ($$ == NULL) { // doesn't exist yet; create it
$$ = new SymDesc ($1, STR_VAR, NULL, lineno);
st.Add ($$);
}
}
;
我们把字符串常量当变量处理,这样我们可以为他们生成一个名字,并且把它放入符号表中。
要注意的是,对于更高级的编译器来说,它很有可能让词法分析器保存或获得标识符,这是因为在一个复杂的语言中有很多不同意思的标识符,比如说变量、函数、
类型等。词法分析器能够获得标识符的描述信息,并且直接向解析器返回一个合适的token。由于我们的标识符总是变量,所以我就直接让解析器处理了。
文法树
对于文法树,我已经创建了一个简单的TreeNode类,该类仅保存孩子结点的指针和一些额外信息(如结点类型、一个符号的链接指针)看看吧,其实没有什么东西是难的。
正如你之前看到的,我们可以很容易地根据可识别的解析器规则构建我们的文法树:
equal_expression
: expression EQUAL assign_expression {$$ = newTreeNode(EQUAL_EXPR, $1, $3);}
| assign_expression {$$ = $1;}
;
你会发现,我们有时仅仅是从传送孩子规则传递信息给家长规则,且传送的信息保持不变;如果你的等于表达式与你的赋值表达式相同的话,就不需要特地为等于表达式产生一个额外的结点,你仅仅是使用为赋值表达式创建的结点来表示等于表达式。
这个部分(以及接下来的部分)的编译和前一个部分相同,这个程序还可以支持语法修正程序,但是现在展示的是它负责创建的符号表和语法树。
太酷了,但是……
好了,它会读取程序和分析程序了,但是它无法做一些有用的事,不是吗?
当然现在不能,我们还需要实现更多的组件,下一个部分将涵盖语法检测和中间代码生成,这将是我们向编译程序跨出的巨大一步。
我希望我的进度没有让你觉得太慢了,我其实是把注意力集中在每一个独立的组件上,而不是匆匆而过。如果你立即理解所有这些东西,那你应该感到高兴,并且可以着手试验了。
下次见
Jan Niestadt