我听说有些语言是通过“展开解释器循环”从解释到编译的。
假设我有一个伪树的伪c代码解释器。
int interpret(node)
{
switch(node) {
case PLUS:
return interpret(child(0))+interpret(child(1));
case MINUS:
return interpret(child(0))-interpret(child(1));
}
}
如何展开此循环以创建已编译的程序?
我看到你们所有人都在低调,因为我不知道我在说什么,但这是维基百科的一句话,说明了我所描述的内容。
“因素最初只是解释,但现在已完全编译(非优化编译器基本上展开解释器循环”
答案 0 :(得分:14)
“展开循环”通常意味着用一系列动作替换重复。循环:
for (int i = 0; i < 4; ++i) {
a[i] = b[i] + c[i];
}
会展开到等效内容:
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
a[2] = b[2] + c[2];
a[3] = b[3] + c[3];
在我看来,维基百科引用的任何人都在某种隐喻意义上使用了这个短语。所以,从那个意义上说......
您的示例通常会在一个遍历AST节点树的解释器中调用,这可能看起来像这样:
ASSIGN
|
+--+---+
| |
REF MINUS
| |
x +--+---+
| |
VAR PLUS
| |
a +--+--+
| |
VAR CONST
| |
b 3
并且interpret
函数会有其他选项:
int interpret(node) {
switch(node) {
case PLUS:
return interpret(child(0))+interpret(child(1));
case MINUS:
return interpret(child(0))-interpret(child(1));
case ASSIGN:
return set(child(0), interpret(child(1));
case VAR:
return fetch(child(0));
case CONST:
return value(child(0));
...
}
}
如果你使用interpet
函数(实际执行操作)来执行AST,那么你就是在解释。但是,如果函数记录要执行的操作,而不是执行它们,那么您正在编译。在伪代码中(实际上,伪代码两次,因为我假设一个假设的堆栈机器作为编译目标):
string compile(node) {
switch(node) {
case PLUS:
return(compile(child(0))) + compile(child(1)) + ADD);
case MINUS:
return(compile(child(0))) + compile(child(1)) + SUB);
case ASSIGN:
return(PUSHA(child(0))) + compile(child(1)) + STORE);
case REF:
return(PUSHA(child(0)));
case VAR:
return(PUSHA(child(0)) + FETCH);
case CONST:
return(PUSHLIT + value(child(0)));
...
}
}
在该AST上调用compile
(忽略任何伪代码错误;-)会吐出类似的内容:
PUSHA x
PUSHA a
FETCH
PUSHA b
FETCH
PUSHLIT 3
ADD
SUB
STORE
FWIW,我倾向于将其视为展开AST,而不是展开翻译,但如果不在上下文中阅读,就不会批评别人的比喻。
答案 1 :(得分:3)
您可以使用GCC编译此程序。然后你将有一个编译的程序,虽然编译的程序将解释AST。
将此转换为编译器的一种方法是,而不是执行return interpret(child(0))+interpret(child(1));
,而是生成汇编指令,而不是将其添加到文件中。
答案 2 :(得分:2)
由于并非interpret
的所有调用都是尾调用,因此您实际上没有循环。
最接近你的编译器,假设堆栈模型......
int compile(node)
{
switch(node) {
case PLUS:
return compile(child(0))&&compile(child(1))&&compile_op(op_plus);
case MINUS:
return compile(child(0))&&interpret(child(1))&&compile_op(op_minus);
}
}
但我认为在此上下文中展开更适用于字节码解释器而不是AST解释器。字节码指令通常在循环中解释。然后“展开”技术是发出对应于每个字节码指令的代码。
因子类似于FORTH。通常,FORTH有一个外部解释器,用于生成线程代码。线程代码可以设想一个函数指针数组(有几种变体,直接线程,间接线程,子程序线程等)。线程代码由内部解释器执行。在这种情况下展开解释器是指内部解释器,并且是连接线程代码的问题。
答案 3 :(得分:2)
Factory是基于堆栈的语言,而不是基于AST的解释器。
我已经将基于堆栈的语言用于演员解释器,所以这就是我的工作方式,这可能与Factor不完全不同。
每个函数都是作为一个函数实现的,它接受一个堆栈,并返回一个堆栈(在我的例子中是同一堆栈的变异版本,我不确定因果是否正常或变异)。在我的解释器中,每个函数还将函数的延续放在堆栈的顶部,因此解释器知道下一步该做什么:
所以解释器调用堆栈上的下一个函数就像:
for (;;)
stack = (stack[0].function_pointer)(stack);
考虑函数foo:
def foo (x,y):
print( add(x, y) )
add可以定义为:
pop a
pop b
stack[ return_offset ] = a + b
return stack
和foo as:
pop x
pop y
push _
push &print
push y
push x
push &add
并且用于调用foo(5,6)的堆栈将在循环的每个步骤中演变为这样:
>> foo(5,6)
[&foo, 5, 6]
[&add, 5, 6, &print, _]
[&print, 11]
=> 11
[]
一个简单的编译器可以为foo函数展开循环,生成等效的线程代码:
compiled_foo (stack):
stack = begin_foo(stack) // arranges stack for call to add
stack = add(stack)
stack = print(stack)
return stack
答案 4 :(得分:2)
这可能没有关系,但也请查看第二个Futamura投影
http://en.wikipedia.org/wiki/Futamura_projection
表示编译器只是一个具有部分评估/常数折叠的解释器(理论上很好但不是实践)。
答案 5 :(得分:1)
在this article中,我通过一个自动将解释器转换为编译器的示例(虽然编译为Scheme而不是机器代码)。这与其他人在此处给出的想法相同,但您可能会发现将其自动化是有帮助的。
答案 6 :(得分:0)
解释器在运行时扫描每个字节码(或AST节点)并调度到函数调用(通常使用无限循环中的switch语句)。
编译器基本上做同样的事情,但是在编译时。编译器在编译时扫描每个字节码(或AST节点)并发出代码(机器代码或某些更高级的中间语言,如C),以便在运行时调用相应的函数。
答案 7 :(得分:0)
我认为这意味着不是循环遍历语句并执行它们,而是遍历语句并输出本已执行的解释器代码。
基本上,正在发生的事情是将在解释器循环中执行的代码内联到新函数中。循环得到“展开”,因为当代码执行时,它不再在解释器循环中,它只是生成函数的线性路径。