如何避免使用连续传递风格的堆栈?
对于我的文凭论文,我选择实现 ICFP 2004 的任务竞赛。
我自己翻译的任务是编写一个编译器,将高级 ant 语言转换为低级 ant 汇编。就我而言,这意味着使用以 Clojure(一种 Lisp 方言)编写的 DSL 作为高级 ant 语言来生成 ant 汇编。
更新:
ant 汇编有几个限制:没有用于调用函数的汇编指令(也就是说,我不能编写 CALL function1, param1
),也不能返回来自函数,也不将返回地址压入堆栈。此外,根本没有堆栈(用于传递参数),也没有任何堆或任何类型的内存。我唯一拥有的是 GOTO/JUMP 指令。
实际上,蚂蚁组装是为了描述状态机(=蚂蚁的“大脑”)的转换。对于“函数调用”(=状态转换),我所拥有的只是跳转/转到。
虽然没有堆栈、堆或适当的 CALL 指令之类的东西,但我仍然希望能够调用 ant 程序集中的函数(通过跳转到某些标签)。 我在几个地方读到,将 Clojure DSL 函数调用转换为连续传递样式 (CPS),我可以避免使用堆栈 [1],并且可以将我的 ant 汇编函数调用转换为普通的 JUMP(或 GOTO)。这正是我所需要的,因为在 ant 汇编中我根本没有堆栈,只有 GOTO 指令。
我的问题是,在 ant-Assembly 函数完成后,我无法告诉解释器(解释 ant-Assembly 指令)在哪里继续。也许一个例子有帮助:
高级 Clojure DSL:
(defn search-for-food [cont]
(sense-food-here? ; a conditional w/ 2 branches
(pickup-food ; true branch, food was found
(go-home ; ***
(drop-food
(search-for-food cont))))
(move ; false branch, continue searching
(search-for-food cont))))
(defn run-away-from-enemy [cont]
(sense-enemy-here? ; a conditional w/ 2 branches
(go-home ; ***
(call-help-from-others cont))
(search-for-food cont)))
(defn go-home [cont]
(turn-backwards
; don't bother that this "while" is not in CPS now
(while (not (sense-home-here?))
(move)))
(cont))
我想从 go-home
函数生成的 ant-assemble 是:
FUNCTION-GO-HOME:
turn left nextline
turn left nextline
turn left nextline ; now we turned backwards
SENSE-HOME:
sense here home WE-ARE-AT-HOME CONTINUE-MOVING
CONTINUE-MOVING:
move SENSE-HOME
WE-ARE-AT-HOME:
JUMP ???
FUNCTION-DROP-FOOD:
...
FUNCTION-CALL-HELP-FROM-OTHERS:
...
上面 ant-asm 指令的语法:
turn direction which-line-to-jump
sense direction what jump-if-true jump-if-false
move which-line-to-jump
我的问题是我无法找出要写入程序集中最后一行的内容(JUMP ???
)。因为 - 正如您在示例中看到的 - go-home
可以通过两个不同的延续来调用:
(go-home
(drop-food))
并且
(go-home
(call-help-from-others))
在 go-home
完成后我想调用drop-food
或 call-help-from-others
。在组装中:当我到家后(=WE-ARE-AT-HOME
标签),我想跳到标签 FUNCTION-DROP-FOOD
或到FUNCTION-CALL-HELP-FROM-OTHERS
。
我怎样才能在没有堆栈的情况下做到这一点,而不将下一条指令的地址 (=FUNCTION-DROP-FOOD
/ FUNCTION-CALL-HELP-FROM-OTHERS
) 推送到堆栈?我的问题是我不明白连续传递风格(=无堆栈,只有 GOTO/JUMP)如何帮助我解决这个问题。
(如果上面的事情难以理解,我可以尝试再次解释这一点。)
非常感谢您的帮助!
--
[1]“解释它不需要控制堆栈或其他无限的临时存储”。 Steele:Rabbit:Scheme 的编译器。
For my diploma thesis I chose to implement the task of the ICFP 2004 contest.
The task--as I translated it to myself--is to write a compiler which translates a high-level ant-language into a low-level ant-assembly. In my case this means using a DSL written in Clojure (a Lisp dialect) as the high-level ant-language to produce ant-assembly.
UPDATE:
The ant-assembly has several restrictions: there are no assembly-instructions for calling functions (that is, I can't write CALL function1, param1
), nor returning from functions, nor pushing return addresses onto a stack. Also, there is no stack at all (for passing parameters), nor any heap, or any kind of memory. The only thing I have is a GOTO/JUMP instruction.
Actually, the ant-assembly is for to describe the transitions of a state machine (=the ants' "brain"). For "function calls" (=state transitions) all I have is a JUMP/GOTO.
While not having anything like a stack, heap or a proper CALL instruction, I still would like to be able to call functions in the ant-assembly (by JUMPing to certain labels).
At several places I read that transforming my Clojure DSL function calls into continuation-passing style (CPS) I can avoid using the stack[1], and I can translate my ant-assembly function calls into plain JUMPs (or GOTOs). Which is exactly what I need, because in the ant-assembly I have no stack at all, only a GOTO instruction.
My problem is that after an ant-assembly function has finished, I have no way to tell the interpreter (which interprets the ant-assembly instructions) where to continue. Maybe an example helps:
The high-level Clojure DSL:
(defn search-for-food [cont]
(sense-food-here? ; a conditional w/ 2 branches
(pickup-food ; true branch, food was found
(go-home ; ***
(drop-food
(search-for-food cont))))
(move ; false branch, continue searching
(search-for-food cont))))
(defn run-away-from-enemy [cont]
(sense-enemy-here? ; a conditional w/ 2 branches
(go-home ; ***
(call-help-from-others cont))
(search-for-food cont)))
(defn go-home [cont]
(turn-backwards
; don't bother that this "while" is not in CPS now
(while (not (sense-home-here?))
(move)))
(cont))
The ant-assembly I'd like to produce from the go-home
function is:
FUNCTION-GO-HOME:
turn left nextline
turn left nextline
turn left nextline ; now we turned backwards
SENSE-HOME:
sense here home WE-ARE-AT-HOME CONTINUE-MOVING
CONTINUE-MOVING:
move SENSE-HOME
WE-ARE-AT-HOME:
JUMP ???
FUNCTION-DROP-FOOD:
...
FUNCTION-CALL-HELP-FROM-OTHERS:
...
The syntax for the ant-asm instructions above:
turn direction which-line-to-jump
sense direction what jump-if-true jump-if-false
move which-line-to-jump
My problem is that I fail to find out what to write to the last line in the assembly (JUMP ???
). Because--as you can see in the example--go-home
can be invoked with two different continuations:
(go-home
(drop-food))
and
(go-home
(call-help-from-others))
After go-home
has finished I'd like to call either drop-food
or call-help-from-others
. In assembly: after I arrived at home (=the WE-ARE-AT-HOME
label) I'd like to jump either to the label FUNCTION-DROP-FOOD
or to the FUNCTION-CALL-HELP-FROM-OTHERS
.
How could I do that without a stack, without PUSHing the address of the next instruction (=FUNCTION-DROP-FOOD
/ FUNCTION-CALL-HELP-FROM-OTHERS
) to the stack? My problem is that I don't understand how continuation-passing style (=no stack, only a GOTO/JUMP) could help me solving this problem.
(I can try to explain this again if the things above are incomprehensible.)
And huge thanks in advance for your help!
--
[1] "interpreting it requires no control stack or other unbounded temporary storage". Steele: Rabbit: a compiler for Scheme.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
是的,您已经为连续传递风格提供了精确的动机。
看起来您已将代码部分翻译为连续传递风格,但并未完全翻译。
我建议你看一下 PLAI,但我可以向你展示一些你的函数是如何的转换,假设我可以猜测 clojure 语法,并混合在方案的 lambda 中。
无论你是否在这里感觉到食物,你都在寻找食物,这一事实让我有点困惑,我发现自己怀疑这要么是奇怪的半翻译代码,要么只是并不完全符合你的想法方法。
希望这有帮助!
真的:去看看 PLAI。那里详细介绍了 CPS 变换,不过还有很多内容需要您先阅读。
Yes, you've provided the precise motivation for continuation-passing style.
It looks like you've partially translated your code into continuation-passing-style, but not completely.
I would advise you to take a look at PLAI, but I can show you a bit of how your function would be transformed, assuming I can guess at clojure syntax, and mix in scheme's lambda.
I'm a bit confused by the fact that you're searching for food whether or not you sense food here, and I find myself suspicious that either this is weird half-translated code, or just doesn't mean exactly what you think it means.
Hope this helps!
And really: go take a look at PLAI. The CPS transform is covered in good detail there, though there's a bunch of stuff for you to read first.
你的蚂蚁汇编语言甚至不是图灵完备的。你说它没有内存,那么你应该如何为你的函数调用分配环境呢?你最多可以让它接受常规语言并模拟有限自动机:任何更复杂的东西都需要内存。为了实现图灵完备,您需要相当于垃圾收集堆的东西。为了完成评估 CPS 术语所需的一切,您还需要一个间接 GOTO 原语。 CPS 中的函数调用基本上(可能是间接的)提供参数传递的 GOTO,而你传递的参数需要内存。
Your ant assembly language is not even Turing-complete. You said it has no memory, so how are you supposed to allocate the environments for your function calls? You can at most get it to accept regular languages and simulate finite automata: anything more complex requires memory. To be Turing-complete you'll need what amounts to a garbage-collected heap. To do everything you need to do to evaluate CPS terms you'll also need an indirect GOTO primitive. Function calls in CPS are basically (possibly indirect) GOTOs that provide parameter passing, and the parameters you pass require memory.
显然,您的两个基本选择是内联所有内容,没有“外部”过程(要获得额外的学分,请在此处查找“内部”和“外部”的原始含义),或者以某种方式“记住”您需要“返回”的位置”来自过程“调用”(其中返回点不一定需要落在紧随“调用”点之后的物理位置)。基本上,返回点标识符可以是代码地址、分支表的索引,甚至是字符符号——它只需要标识相对于被调用过程的返回目标。
这里最明显的是在编译器中跟踪给定调用目标的所有返回目标,然后在被调用过程的末尾构建一个分支表(或分支梯形图)以从多个返回目标之一中进行选择可能的回报目标。 (在大多数情况下,只有少数可能的返回目标,但对于常用的过程,可能有数百或数千个。)然后,在调用点,调用者需要加载一个参数及其返回点的索引 相对于被调用的过程。
显然,如果被调用者依次调用另一个过程,则必须以某种方式保留第一个返回点标识符。
毕竟,连续传递只是返回地址的更通用形式。
Clearly, your two basic options are to inline everything, with no "external" procedures (for extra credit look up the original meaning of "internal" and "external" here), or somehow "remember" where you need to go on "return" from a procedure "call" (where the return point does not necessarily need to fall in the physical locations immediately following the "calling" point). Basically, the return point identifier can be a code address, an index into a branch table, or even a character symbol -- it just needs to identify the return target relative to the called procedure.
The most obvious here would be to track, in your compiler, all of the return targets for a given call target, then, at the end of the called procedure, build a branch table (or branch ladder) to select from one of the several possible return targets. (In most cases there are only a handful of possible return targets, though for commonly used procedures there could be hundreds or thousands.) Then, at the call point, the caller needs to load a parameter with the index of its return point relative to the called procedure.
Obviously, if the callee in turn calls another procedure, the first return point identifier must be preserved somehow.
Continuation passing is, after all, just a more generalized form of a return address.
您可能对 Andrew Appel 的书Compiling with Continuations感兴趣。
You might be interested in Andrew Appel's book Compiling with Continuations.