如何实现“无堆栈”解释语言?

Posted

技术标签:

【中文标题】如何实现“无堆栈”解释语言?【英文标题】:How does one implement a "stackless" interpreted language? 【发布时间】:2011-08-24 13:41:44 【问题描述】:

我正在制作我自己的类似 Lisp 的解释语言,并且我想做尾调用优化。我想将我的解释器从 C 堆栈中解放出来,这样我就可以管理我自己从函数到函数的跳转以及我自己的堆栈魔法来实现 TCO。 (我真的不是说堆栈本身,只是调用不会将帧添加到 C 堆栈的事实。我想使用我自己的堆栈,它不会随着尾调用而增长)。像 Stackless 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,也许?我真的不知道。请多多包涵,我主要是自学(互联网)编程。

【问题讨论】:

【参考方案1】:

尾递归可以被认为是为被调用者重用您当前用于调用者的相同堆栈帧。所以你可以重新设置参数并转到函数的开头。

【讨论】:

我们曾经在 Forth 中这样做过。当我知道一个词要返回到一个刚要返回的词时,我们从堆栈中弹出返回地址,然后返回,有效地返回到被调用者的调用者。它有效。【参考方案2】:

一种解决方案有时被称为“蹦床式”。 trampoline 是一个***循环,它分派给小函数,这些函数在返回之前执行一些小步骤。

我已经在这里坐了将近半个小时,试图设计一个简短的好例子。不幸的是,我不得不做无用的事情,并给你一个链接:

http://en.wikisource.org/wiki/Scheme:_An_Interpreter_for_Extended_Lambda_Calculus/Section_5

这篇论文叫做“Scheme: An Interpreter for Extended Lambda Calculus”,第 5 节用一种过时的 Lisp 方言实现了一个工作模式解释器。秘诀在于他们如何使用 **CLINK** 而不是堆栈。其他全局变量用于在实现函数之间传递数据,例如 CPU 的寄存器。我会忽略 **QUEUE**、**TICK** 和 **PROCESS**,因为它们处理线程和假中断。 **EVLIS** 和 **UNEVLIS** 专门用于评估函数参数。未评估的 args 存储在 **UNEVLIS** 中,直到它们被评估并输出到 **EVLIS** 中。

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

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

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

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

AEVAL:AEVAL 是 EVAL 函数。

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

我希望我能有所帮助。我只是希望我的答案(和链接)更短。

【讨论】:

【参考方案3】:

您要查找的内容称为continuation-passing style。这种样式为每个函数调用添加了一个附加项(如果您愿意,可以将其视为参数),指定要运行的 next 位代码(可以认为延续 k of 作为一个接受单个参数的函数)。例如,您可以像这样在 CPS 中重写您的示例:

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

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

(f 1 2 print)

+ 的实现将计算xy 的总和,然后将结果传递给k,类似于(k sum)

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

您需要花点功夫才能理解这一点。推荐一些阅读材料,比如优秀的SICP。

【讨论】:

我相信这是另一种编程风格。我想要的是为我的语言(即 TCO)添加一个功能。像 Scheme,按照标准,所有实现都需要它。但是谢谢,我稍后一定会检查这个 CPS;我已经在 Wikipedia 上看到过,但一个都不懂 :) CPS 不仅仅是另一种“编程风格”(尽管这些想法在某些情况下可用于编写代码)。我所说的实际上是一种解释器实现技术,可以将任何程序源转换为CPS样式,以便解释器可以轻松完成TCO之类的事情。 好吧,经过一番研究,我发现 stackless python 过去确实使用过延续。我会阅读更多关于 CPS 的内容。 我认为 CPS 或 CPS 转换在 SICP 中并没有真正涵盖那么多,但我可能记错了。 EOPL 和 Lisp In Small Pieces 确实涵盖了它。尽管如此,SICP 还是一个很好的推荐,因为对于没有读过 SICP 或该级别内容的人来说,这些其他书籍可能太高级了。

以上是关于如何实现“无堆栈”解释语言?的主要内容,如果未能解决你的问题,请参考以下文章

Python如何规避全局解释器锁(GIL)带来的限制

COM如何实现语言互操作?

C语言 如何实现保留三位小数,第四位四舍五入的程序,详细解释一下程序,网上的看不懂

如何理解printf变参函数的实现

编程语言是无堆栈的到底意味着啥?

有没有办法创建一个硬件实现的高级语言解释器?