本文是关于编译器和编程语言实现的深入探讨。在开始之前,建议读者先阅读之前关于此主题的文章。正如之前提到的,输入程序是通过一个名为program.txt
的文件提供的,附件中提供了一系列程序,以展示这种语言的语法。这些程序利用了其函数调用、数组和自动变量的能力。
由于语言的输出是汇编语言,因此需要有较强的汇编语言编程背景。为了便于查看结果,已经生成了一个包含内联汇编的C文件。将扩展语言以支持一些简单的概念(通常认为是理所当然的)。和以前一样,语法必须支持编译器和解释器两种版本,这允许使用这种语言的用户保持代码的一致性。
通过数组索引访问数组是支持的,使用以下语法:
z=1
s=20
ARRAY:a1:1+z=24+3*s
在这里,可以看到数组'a1'在索引1+z处被写入,写入的值是24+3*s。同样地:
d=ARRAY:a1:z*2
在这里,读取数组a1在索引z*2处的值。
这些变量的作用域仅限于函数调用,即它们的作用域从它们被定义的标签下开始,直到程序遇到RETURN。使用自动变量:
AUTO:a=10*20
这将创建一个自动变量'a',并将值200与之关联。
c=AUTO:a
这将从自动变量'a'中读取值,这个变量必须存在于函数调用的作用域内,否则生成的汇编代码将是错误的(总是可以放置一个简单的检查来确保这一点)。
这种优化是为VS2012发布版构建和gcc -o3优化启用的。这是一种简单的优化,它检查是否可以忽略额外的堆栈帧创建,并且可以替换'call'生成的代码为'jmp'(这节省了将返回地址推送到堆栈的麻烦),这种优化的'return'迫使代码返回到先前的函数。
和之前的文章一样,代码必须始终参考并调试,以更好地理解其工作原理。让看看数组支持。首先通过声明数组来开始:
DIM:a1:10
这创建了一个大小为10的一维数组,这在编译阶段是必需的,因为必须全局声明这个数组的大小,对于解释器,这被忽略了。
为了使用数组,需要两个临时变量:
__TarrayIndexProcessing_, __TarrayValueProcessing_.
这些变量用于计算和保存数组索引和该索引处的值。会发现编译器的索引处理有一个额外的步骤:乘以4,因为int的大小是4个字节,以正确访问所需的元素。对于解释器来说,这不是必需的,因为它使用结构体来保存值:
struct arrayvar{
string m_name;
int index;
DWORD sz;
};
map arrayList;
请注意,arrayvar(由解释器使用)使用索引和名称来唯一标识一个元素,请参考代码以查看map所需的比较函数。
对于解释器来说,堆栈上的变量相当简单,必须为每个函数调用维护堆栈帧。这是通过以下代码行完成的:
stack
这维护了当前的堆栈帧。在程序开始时创建堆栈帧,并且始终指向堆栈的顶部,当遇到函数调用时,会创建一个新的堆栈帧,并且堆栈顶部会更新,这确保了当前堆栈始终得到维护。
请参考解释器的CALL功能实现。同样地,RETURN功能会弹出堆栈并更新堆栈顶部。编译器的堆栈实现并不那么简单,因为代码需要多次解析以找到当前标签作用域中这个变量的定义。
如果变量在标签作用域内没有找到,那么通过调用push element创建一个变量。下面的函数从源代码中查找自动变量,由于处于编译阶段,源代码是可用的,以找到与LABEL关键字相关的索引。
int getVariablesIndex(string varName, UINT uiLineNumber)
如果变量没有找到,它返回-1,否则返回堆栈帧中的索引,如果找到了变量。例如:
AUTO:a=10
b=20
AUTO:c=30
自动变量'a'将在索引1处,自动变量'c'将在索引2处。通过EBP(这个寄存器用于持有帧指针)访问这些变量。从不使用ESP,因为它的值在遇到push/pop/call时可能会改变。
a将通过EBP-4访问,c将通过EBP-8访问。
对于编译器和解释器来说,这种优化是相似的。在处理CALL之后解析文件,寻找立即的RETURN,忽略使用'#'生成的注释。如果RETURN立即在CALL之后找到(忽略注释),知道这段代码可以通过放置一个'JMP'而不是'call'来尾调用优化,如果使用了AUTOs,那么在RETURN之前需要额外的代码进行堆栈清理,并且不会执行尾调用优化。
如果没有立即遇到RETURN,那么代码必须返回到CALL之后的某个点以执行RETURN之前找到的语句,因此将使用'call'而不是'jmp'。这种优化在评估'CALL'语言关键字的地方编码,请参考代码。