对于SNL语言解释器的内容可以参考我的前一篇文章《使用antlr4及java实现snl语言的解释器》。此文只讲一下“尾递归优化”是如何实现的——“尾递归优化”并不是一个语言实现必须要做的,但这是一个比较有趣的东西,所以我还是想拿来讲一讲。

在前一篇文章中有一个例子:


program recursion
    procedure f(integer d);
    begin
        write(d);
        f(d + 1)
    end
begin
    f(1)
end


这个例子是我用来测试我写的解释器是否正常实现了最基本的尾递归优化的。

这段代码用JAVA翻译过来是这样的:


public class Test {
    static void f(long l) {
        System.out.println(l);
        f(l + 1);
    }
    public static void main(String[] args) {
        f(0);
    }
}


就是对一个方法f进行递归调用并且没有任何正常的退出逻辑(要退出就只能抛出异常或我们主动杀死那个进程)。

这段JAVA代码如果运行起来,很快就会发生堆栈溢出的错误(java.lang.StackOverflowError),但我写的SNL代码运行在我实现的SNL解释器中却会一直运行下去,直到那个进程被杀死(不会有类似堆栈溢出的问题抛出来)。之所以会这样就是因为我的解释器实现了“尾递归优化”。

什么是“尾递归优化”呢?

我对尾递归优化的定义是:一个过程返回之前,调用的最后一条语句如果是对当前过程的递归调用时,则可以用一个小技巧把递归调用变成循环。这里使用到的“小技巧”就是尾递归优化了。

科班出身的程序员都受过这样的训练:把各种递归的程序修改成等价的没有递归调用的程序。这其中的各种方法一般都很难实现一个通用的转换程序——输入一个递归的代码,输出一段等价的,无递归的代码——但对于最后一条执行语句是对当前过程的递归时,这个转换过程就比较容易实现了。

从尾递归优化的定义中可以看出两个关键点:

  1. 先找到最后一条可执行语句(并判断是否是对当前过程的递归)。
  2. 使用那个“小技巧”。

首先,我们如何知道最后一条语句是哪条呢?

我们知道,SNL语言的语句有七种:条件语句;循环语句;输入语句;输出语句;赋值语句;过程调用语句;return语句

同时,我们知道,SNL文法中,我们解析stmList时,是要先解析第一条stm,然后再看后面的是否还有其它stmList。

这样我们有以下逻辑可以判断当前识别到的stm是否可能是最后一条:

  1. 如果当前stm之后不再有stmList,则可以认为这个stm就是最后一条了。
  2. 如果之后再有stmList,且这个stmList下的第一个stm是return语句,则可以认为这个stm就是最后一条了。

有了这两个逻辑还不算完,因为“条件语句”内部可以复合stmList的,这样我就需要在“当前stmList的最后一条语句是条件语句”的情况下还需要判断这个条件语句的两个可能的分支中的stmList的最后一条语句的最后一条语句是哪条了(即然是分支,那么两个分支都自己的最后一条,也就是说可能有多条语句都是最后一条)。

注:对于“循环语句”也是可以在内部复合stmList的,但因为一般我们只有在运行时才知道最后一次循环是什么时候结束的,所以我们暂时不考虑循环语句。

找到最后一条语句(或多条都可能在自己的分支内是最后一条)之后,判断是否为当前过程的递归就简单了:首先它必须是“过程调用语句”,然后其过程签名必须与当前过程完全相同(我在实现SNL语言的解释器时,为了实现简单,直接用过程名做为过程的签名了,但这样实现存在一个问题:在这个SNL语言的实现中就不支持重载了)。

现在我们已经找到了一些可能是最后一条的语句了,那么只要运用那个“小技巧”就可以了。

这个小技巧非常简单:跳转到当前过程的最开头(跳转之前,需要清理一下当前过程创建的上下文——也就是当前过程自己定义的变量等内容)即可。

其实我们自己想像一下也可以想到:递归调用其实就是跳转到最开头,这就是循环了。

到这里还没完——我们如何跳到最开头去呢?

如果我们写的是编译器就比较简单:直接用一个类似JMP或GOTO这样的指令就可以了。

但我们现在写的是解释器,解释器在解释一个proc时几乎不会把整个解释的逻辑放到同一个方法中来实现的(也是几乎不可能做到的),所以当我们当前所在的是用来解析callStm的方法时,如何可以跳转到很多步调用之前的解析procDeclare方法中去呢?

JAVA中只有一种语言机制可以帮助我们做到这点,即:抛异常。

对SNL语言的解释器实现尾递归优化的更多相关文章

  1. 使用antlr4及java实现snl语言的解释器

    对于antlr4的基础使用,请参考我的前一篇文章<用antlr4来实现<按编译原理的思路设计的一个计算器>中的计算器>. 其实我对于antlr4的理解也仅限于那篇文章的范围,但 ...

  2. 用VC编译lua源码,生成lua语言的解释器和编译器

    用VC编译lua源码,生成lua语言的解释器和编译器 1.去网址下载源码 http://www.lua.org/download.html 2.装一个VC++,我用的是VC6.0 3.接下来我们开始编 ...

  3. 在线C语言编译器/解释器

    在线C语言编译器/解释器 本文介绍两个C语言在线解释器/编译器,这些工具可以提高代码片段检测方便的工作效率,并可以保证这些代码的正确性,而且还可以和别人一起编辑/分享之间的代码,这样可以共同分析代码并 ...

  4. 用C语言写解释器(一)——我们的目标

    声明 为提高教学质量,我所在的学院正在筹划编写C语言教材.<用C语言写解释器>系列文章经整理后将收入书中"综合实验"一章.因此该系列的文章主要阅读对象定为刚学完C语言的 ...

  5. Scala进阶之路-尾递归优化

    Scala进阶之路-尾递归优化 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 递归调用有时候能被转换成循环,这样能节约栈空间.在函数式编程中,这是很重要的,我们通常会使用递归方法来 ...

  6. .NET 4.6的RyuJIT尾递归优化的Bug

    今天看到园子里有一篇新闻稿.NET 4.6的RyuJIT编译器中发现严重的Bug提到,在.Net 4.6的x64程序中默认启用新的JIT程序RyuJIT在处理尾递归指令的时候有一个Bug,导致无法得到 ...

  7. Python尾递归优化

    Python开启尾递归优化 cpython本身不支持尾递归优化, 但是一个牛人想出的解决办法:实现一个 tail_call_optimized 装饰器 #!/usr/bin/env python2.4 ...

  8. kotlin递归&尾递归优化

    递归: 对于递归最经典的应用当然就是阶乘的计算啦,所以下面用kotlin来用递归实现阶乘的计算: 编译运行: 那如果想看100的阶乘是多少呢? 应该是结果数超出了Int的表述范围,那改成Long型再试 ...

  9. 【Scala】尾递归优化

    以递归方式思考 递归通过灵巧的函数定义,告诉计算机做什么.在函数式编程中,随处可见递归思想的运用.下面给出几个递归函数的例子: object RecursiveExample extends App{ ...

随机推荐

  1. 9种样式CSS3 渐变button集

    <!DOCTYPE html> <head> <meta http-equiv="Content-Type" content="text/h ...

  2. PHP在浏览器上跟踪调试的方法以及使用ChromePhp、FirePHP的简介

    之前用ThinkPHP时发现有个 trace 函数能够跟踪调试,感觉非常有意思.网上搜索了下类似的东西.发现了 ChromePhp ,曾经没想过这样来调试 PHP 程序.感觉非常方便,非常实用. Th ...

  3. 十分简便的APK反编译(Mac 版本号 具体解释)

    之前參考了网上大神们介绍的apk for mac  的反编译的文章,里面写的十分具体而有用,可是因为apk for mac中反编译细节十分繁琐,过程也相对照较复杂,针对这个缺陷本人对其反编译的过程进行 ...

  4. java多线程之 ---- 线程死锁

    java多线程之线程死锁 产生死锁的主要原因: 由于系统资源不足. 进程执行推进的顺序不合适. 资源分配不当等. 假设系统资源充足.进程的资源请求都可以得到满足,死锁出现的可能性就非常低.否则就会因争 ...

  5. 12. Ext.Ajax 对ajax的支持

    转自:http://www.cnblogs.com/lipan/archive/2011/12/09/2272793.html 本篇主要介绍一下ExtJs常用的几个对JS语法的扩展支持,包括Ajax封 ...

  6. Vue Router过渡动效

    <router-view> 是基本的动态组件,所以我们可以用 <transition> 组件给它添加一些过渡效果: <transition> <router- ...

  7. SQL Server 方言类型映射问题

    关于SQL Server的类型映射问题,例如,nvarchar无法进行hibernate类型映射,需要通过convert进行类型转换方可进行获取

  8. P3043 [USACO12JAN]牛联盟Bovine Alliance(并查集)

    P3043 [USACO12JAN]牛联盟Bovine Alliance 题目描述 Bessie and her bovine pals from nearby farms have finally ...

  9. sql server 触发器详细应用

    SQL Server 触发器 触发器是一种特殊类型的存储过程,它不同于之前的我们介绍的存储过程.触发器主要是通过事件进行触发被自动调用执行的.而存储过程可以通过存储过程的名称被调用.  Ø 什么是触发 ...

  10. c++ gets函数

    函数名称:gets函数 函数结构:gets() 所需头文件:#include<cstdio> 函数作用:持续读入,直到遇到换行停止输出.