Recursion 一个人如何实现一个;“无堆栈”;解释语言?

Recursion 一个人如何实现一个;“无堆栈”;解释语言?,recursion,lisp,stack,stack-overflow,interpreter,Recursion,Lisp,Stack,Stack Overflow,Interpreter,我正在制作我自己的类似Lisp的解释语言,我想做尾部调用优化。我想把我的解释器从C堆栈中解放出来,这样我就可以管理我自己从一个函数到另一个函数的跳转,以及我自己的堆栈魔术来实现TCO。(我真的不是说stackless本身,只是调用不会向C堆栈添加帧。我想使用我自己的堆栈,它不会随着尾部调用而增长)。像无堆栈Python,不像Ruby或。。。我猜是标准Python 但是,由于我的语言是Lisp派生语言,所有s表达式的求值目前都是递归进行的(因为这是我想到的进行这种非线性、高度分层过程的最明显的方法

我正在制作我自己的类似Lisp的解释语言,我想做尾部调用优化。我想把我的解释器从C堆栈中解放出来,这样我就可以管理我自己从一个函数到另一个函数的跳转,以及我自己的堆栈魔术来实现TCO。(我真的不是说stackless本身,只是调用不会向C堆栈添加帧。我想使用我自己的堆栈,它不会随着尾部调用而增长)。像无堆栈Python,不像Ruby或。。。我猜是标准Python

但是,由于我的语言是Lisp派生语言,所有s表达式的求值目前都是递归进行的(因为这是我想到的进行这种非线性、高度分层过程的最明显的方法)。我有一个eval函数,它在每次遇到函数调用时都调用Lambda::apply函数。apply函数然后调用eval来执行函数体,依此类推。相互堆栈饥饿的非尾部C递归。我目前使用的唯一迭代部分是计算一系列顺序s表达式

(defun f (x y)
    (a x y)) ; tail call! goto instead of call. 
             ; (do not grow the stack, keep return addr)

(defun a (x y)
    (+ x y))

; ...

(print (f 1 2)) ; how does the return work here? how does it know it's supposed to
                ; return the value here to be used by print, and how does it know
                ; how to continue execution here??

那么,如何避免使用C递归呢?或者我可以使用某种跨越c函数的goto吗?也许是longjmp?我真的不知道。请容忍我,我大部分是自学编程的。

你要找的东西叫什么。此样式在每个函数调用中添加一个附加项(如果愿意,可以将其视为一个参数),指定要运行的下一位代码(可以将continuation
k
视为一个接受单个参数的函数)。例如,您可以在CPS中重写您的示例,如下所示:

(defun f (x y k)
    (a x y k))

(defun a (x y k)
    (+ x y k))

(f 1 2 print)
+
的实现将计算
x
y
之和,然后将结果传递给
k
类似的
(k sum)

然后,您的主解释器循环根本不需要是递归的。它将在一个循环中,一个接一个地应用每个函数应用程序,并传递连续性


这需要一点努力才能让你的头脑清醒过来。我推荐一些阅读材料,比如优秀的。

尾部递归可以被认为是对被调用者重用当前用于调用者的相同堆栈框架。因此,您可以重新设置参数并转到函数的开头。

一种解决方案是有时称为“蹦床式”。蹦床是一个顶级的循环,它可以分派给小的函数,这些函数在返回之前执行一些小的计算步骤

我在这里坐了将近半个小时,试图想出一个好的、简短的例子。不幸的是,我不得不做一件无益的事,并将您发送到一个链接:

本文名为“Scheme:ExtendedLambda演算的解释器”,第5节用过时的Lisp方言实现了一个工作Scheme解释器。秘密在于他们如何使用**叮当**而不是堆栈。其他全局变量用于在实现函数(如CPU的寄存器)之间传递数据。我会忽略**队列**、**勾选**和**进程**,因为它们处理线程和假中断**EVLIS**和**UNEVLIS**特别用于计算函数参数。未计算的参数存储在**UNEVLIS**中,直到它们被计算并输出到**EVLIS**

需要注意的功能,以及一些小注意事项:

MLOOP:MLOOP是解释器的主循环,或“蹦床”。忽略**勾选**,它唯一的任务就是调用**PC**中的任何函数。一遍又一遍

SAVEUP:SAVEUP将所有寄存器保存到**CLINK**,这与C在函数调用之前将寄存器保存到堆栈中的情况基本相同。**叮当声**实际上是解释器的“延续”。(continuation只是计算的状态。保存的堆栈帧在技术上也是continuation。因此,一些Lisp将堆栈保存到堆中以实现call/cc。)

还原:还原还原保存在**CLINK**中的“寄存器”。这类似于在基于堆栈的语言中恢复堆栈帧。因此,它基本上是“return”,除了一些函数显式地将返回值插入**值**。(**值**显然不会被RESTORE破坏。)还要注意,RESTORE并不总是必须返回到调用函数。有些函数实际上会保存一个全新的计算,RESTORE会很高兴地“RESTORE”

AEVAL:AEVAL是评估函数

EVLIS:EVLIS用于计算函数的参数,并将函数应用于这些参数。为了避免递归,它保存EVLIS-1。如果代码是递归编写的,EVLIS-1将只是函数应用程序之后的常规旧代码。但是,为了避免递归和堆栈,它是一个单独的“延续”


我希望我能帮上忙。我只希望我的答案(和链接)更短。

我相信这是另一种编程风格。我想要的是在我的语言中添加一个特性(即TCO)。就像Scheme一样,标准要求所有实现都使用它。但是谢谢,我以后一定会检查这个CPS;我已经在维基百科上看过了,但一点也不懂:)CPS不仅仅是另一种“编程风格”(尽管在某些情况下,这些想法可以用于编写代码)。我所说的实际上是一种解释器实现技术,它可以将任何程序源转换为CPS样式,这样解释器就可以轻松地执行TCO之类的操作。好吧,经过一点研究,我发现stackless python过去确实使用了continuations。我将阅读更多关于CPS的内容。我不认为CPS或CPS转换在SICP中有那么多内容,但我可能记错了。不过,EOPL和Lisp的小片段确实涵盖了这一点。尽管如此,SICP仍然是一个很好的推荐书,因为对于没有读过SICP或其他类似书籍的人来说,这些书可能太高级了。当我知道一个词会回到另一个词