2

在我的语言中,当定义出现在方法下方时,我可以在方法中使用类变量。它还可以调用我的方法等下面的方法。没有“标题”。以这个 C# 示例为例。

class A
{
    public void callMethods() { print(); B b; b.notYetSeen();
    public void print() { Console.Write("v = {0}", v); }
    int v=9;
}

class B
{
    public void notYetSeen() { Console.Write("notYetSeen()\n"); }
}

我应该如何编译它?我在想的是:

  • pass1:将所有内容转换为 AST
  • pass2:遍历所有类并构建定义类/变量/等的列表
  • pass3:检查代码并检查是否存在未定义变量、错误使用等错误并创建我的输出

但似乎要让它工作,我必须在做 pass3 之前为所有文件做 pass 1 和 2。在我发现语法错误之前感觉还有很多工作要做(除了可以在解析时完成的明显操作,例如忘记关闭大括号或写入 0xLETTERS 而不是十六进制值)。我的直觉说还有其他方法。

注意:我正在使用 bison/flex 来生成我的编译器。

4

4 回答 4

3

我对处理前向引用的语言的理解是,它们通常只使用第一遍来构建有效名称列表。类似于将条目放入表中(不填写定义)这样的东西,以便稍后在您进行真正的传递以生成定义时指向一些东西。

如果您尝试实际构建完整的定义,您最终将不得不重新扫描,每次都保存对未定义事物的任何引用,直到下一次通过。如果有循环引用,即使这样也会失败。

于 2009-10-16T15:41:35.507 回答
1

我将通过第一遍并收集您所有的类/方法/字段名称和类型,忽略方法主体。然后通过两次检查仅方法体。

于 2009-10-16T15:37:34.907 回答
0

我不知道除了遍历源中的所有文件之外还有其他方法。

我认为你可以把它减少到两遍——在第一遍时,构建 AST,每当你找到一个变量名时,将它添加到包含该块符号的列表中(将该列表添加到树中的相应范围)。第二步是线性遍历树并确保使用的每个符号都引用了该范围内的符号或其上方的范围。

我的描述过于简单,但基本答案是——前瞻至少需要两次通过。

于 2009-10-16T15:47:47.953 回答
0

通常的做法是另存B为“未知”。它可能是某种类型(因为你遇到它的地方)。因此,即使您不知道它到底是什么,您也可以为它保留内存(指针)。

对于方法调用,你无能为力。在动态语言中,您只需将方法的名称保存在某处并在运行时检查它是否存在。在静态语言中,您可以将其与未知类型一起保存在编译器某处的“未知方法”下B。由于方法调用最终会转换为内存地址,因此您可以再次保留内存。

然后,当你遇到B和方法时,你可以清除你的未知数。由于您对它们有所了解,因此您可以说出它们的行为是否像应有的那样,或者第一次使用现在是否是语法错误。

因此,您不必阅读所有文件两次,但它确实使事情变得更简单。

或者,您可以在遇到源代码时生成这些头文件,并将它们保存在您可以再次找到它们的地方。这样,您可以加快编译速度(因为您不必在下一次编译运行中考虑未更改的文件)。

最后,如果您编写一门新语言,则不应再使用 bison 和 flex。现在有更好的工具。例如,ANTLR可以生成一个可以在错误后恢复的解析器,因此您仍然可以解析整个文件。或查看此 Wikipedia 文章以获取更多选项。

于 2009-10-16T15:51:48.777 回答