我正在写一个递归下降解析器,而我正处于不确定如何验证所有内容的地步。我甚至不确定我是否应该在解析器的阶段这样做。我的意思是,我可以有一些语法,即:
int x = 5
int x = 5
这样会有效,解析器会检查x是否已经定义了吗?如果是这样,我会使用hashmap吗?我需要存储什么样的信息,比如我如何处理变量的范围,因为x可以在本地和全局范围内的函数中定义:
int x = 5;
void main() {
int x = 2;
}
最后,当我存储到hashmap时,如何区分这些类型?例如,我可以有一个名为foo
的变量,以及一个名为foo
的结构。因此,当我将foo
放入散列映射时,它可能会导致一些错误。我想我可以将它作为结构struct_xyz
的hashmaps键存储,其中xyz是结构的名称,变量int_xyz
?
谢谢:))
答案 0 :(得分:2)
我将假设无论您选择哪种方法,您的解析器都将构建某种抽象语法树。你现在有两个选择。或者,解析器可以使用标识符节点填充树,该标识符节点存储它们引用的变量或函数的名称。如许多编译器教科书所倡导的那样,这使得范围解决问题成为后来的过程。
另一个选择是让解析器立即在它构建的符号表中查找标识符,并在抽象语法树节点中存储指向该符号的指针。如果您的语言不允许对尚未声明的名称进行隐式前向引用,则此方法可以很好地工作。
我最近在我正在编写的编译器中实现了后一种方法,到目前为止我对结果非常满意。我将简要介绍下面的解决方案。
符号存储在如下所示的结构中:
typedef struct symbol {
char *name;
Type *type;
Scope *scope; // Points to the scope in which the symbol was defined.
} Symbol;
那么这个Scope
是什么东西?我编译的语言是词法范围的,每个函数定义,块等都引入了一个新的范围。范围形成堆栈,其中底部元素是全局范围。这是结构:
typedef struct scope {
struct scope *parent;
Symbol *buckets;
size_t nbuckets;
} Scope;
buckets
和nbuckets
字段是标识符(字符串)到Symbol
指针的哈希映射。通过遵循parent
指针,可以在搜索标识符时遍历作用域堆栈。
有了数据结构,就可以很容易地编写一个根据词法范围规则解析名称的解析器。
Scope
推送到堆栈。新范围的parent
字段指向旧范围。parent
作用域等中递归递延。如果找不到相应的Symbol
,则会引发错误。如果查找成功,解析器将创建一个带有指向符号的指针的AST节点。某些语言使用多个命名空间。例如,在Erlang中,函数和变量占用不同的名称空间,需要像fun foo:bar/1
这样的笨拙语法来获取函数的值。通过保留多个Scope
堆栈(每个命名空间一个堆栈),可以在上面概述的模型中轻松实现这一点。
答案 1 :(得分:0)
如果我们定义"范围"或" context"作为从变量名称到类型的映射(以及可能的一些更多信息,例如作用域深度),它的自然实现是hashmap或某种搜索树。在到达任何变量定义时,编译器应将具有相应类型的名称插入此数据结构中。什么时候结束范围'遇到运营商,我们必须已经有足够的信息来回溯'将此映射更改为其先前的状态。
对于hashmap实现,对于每个变量定义,我们可以存储此名称的先前映射,并在我们到达范围的末尾时恢复此映射'运营商。我们应该保留这些更改的堆栈(每个当前打开的作用域一个堆栈),并在每个作用域的末尾回溯最顶层的更改。
这种方法的一个缺点是我们必须在一次传递中完成编译,或者在某个地方存储程序中每个标识符的映射,因为我们不能多次检查任何范围,或者顺序不是在源文件(或AST)中出现。
对于基于树的实现,可以使用所谓的persistent trees轻松实现。我们只是保持一堆树木,每个范围一个,按我们打开的方式推动。一些范围,并在范围结束时弹出。
范围的深度&#39;足以选择在新变量名与映射中的变量名冲突的情况下要做什么。只需检查old depth < new depth
并覆盖成功,或报告失败时的错误。
要区分函数名和变量名,可以对这些对象使用单独(但相似或相同)的映射。如果某些上下文仅允许函数或仅允许变量名称,则您已经知道要查找的位置。如果两者都在某些上下文中被允许,则在两个结构中执行查找,并报告&#34;模糊错误&#34;如果name同时对应一个函数和一个变量。
答案 2 :(得分:-1)
最好的方法是使用一个类来定义像HashMap这样的结构,它允许您对变量的类型和/或存在进行控制。该类应该具有与解析器中编写的语法规则接口的静态方法。