Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >手写编程语言-递归函数是如何实现的?

手写编程语言-递归函数是如何实现的?

作者头像
crossoverJie
发布于 2022-12-20 03:44:18
发布于 2022-12-20 03:44:18
70600
代码可运行
举报
文章被收录于专栏:crossoverJiecrossoverJie
运行总次数:0
代码可运行

前言

本篇文章主要是记录一下在 GScript 中实现递归调用时所遇到的坑,类似的问题在中文互联网上我几乎没有找到相关的内容,所以还是很有必要记录一下。

在开始之前还是简单介绍下本次更新的 GScript v0.0.9 所包含的内容:

  • 支持可变参数
  • 优化 append 函数语义
  • 优化编译错误信息
  • 最后一个就是支持递归调用

先看第一个可变参数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//formats according to a format specifier and writes to standard output.
printf(string format, any ...a){}

//formats according to a format specifier and returns the resulting string.
string sprintf(string format, any ...a){}

以上是随着本次更新新增的两个标准函数,均支持可变参数,其中使用 ... 表示可变参数,调用时如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
printf("hello %s ","123");
printf("hello-%s-%s ","123","abc");
printf("hello-%s-%d ","123",123);
string format = "this is %s ";
printf(format, "gscript");

string s = sprintf("nice to meet %s", "you");
assertEqual(s,"nice to meet you");

与大部分语言类似,可变参数本质上就是一个数组,所以可以拿来循环遍历:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int add(string s, int ...num){
 println(s);
 int sum = 0;
 for(int i=0;i<len(num);i++){
  int v = num[i];
  sum = sum+v;
 }
 return sum;
}
int x = add("abc", 1,2,3,4);
println(x);
assertEqual(x, 10);

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// appends "v" to the end of a array "a"
append(any[] a, any v){}

之后是优化了内置函数 append() 的语义,本次优化来自于 issue12 的建议:https://github.com/crossoverJie/gscript/issues/12

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Before
int[] a={1,2,3};
println(a);
println();
a = append(a,4);
println(a);
// Output: [1 2 3 4]

// Now
int[] a={1,2,3};
println(a);
println();
append(a,4);
int b = a[3];
assertEqual(4, b);
println(a);
// Output: [1 2 3 4]

现在 append 之后不需要再重新赋值,也会追加数据,优化后这里看起来是一个值/引用传递的问题,但其实底层也是值传递,只是在语法上增加了这样的语法糖,帮使用者重新做了一次赋值。


之后是新增了编译错误信息提示,比如下面这段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a+2;
b+c;

使用没有声明的变量,现在会直接编译失败:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1:0: undefined: a
2:0: undefined: b
2:2: undefined: c
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class T{}
class T{}

// output:
2:0: class T redeclared in this block

重复声明之类的语法错误也有相关提示。


最后一个才是本次讨论的重点,也就是递归函数的支持。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int num(int x,int y){
 if (y==1 || y ==x) {
  return 1;
 }
 int v1 = num(x - 1, y - 1);
 return c;
}

再上一个版本中 int v1 = num(x - 1, y - 1); 这行代码是不会执行的,具体原因后文会分析。

现在利用递归便可以实现类似于打印杨辉三角之类的程序了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int num(int x,int y){
 if (y==1 || y ==x) {
  return 1;
 }
    int v1 = num(x - 1, y - 1);
    int v2 = num(x - 1, y);
 int c = v1+v2;
    // int c = num(x - 1, y - 1)+num(x - 1, y);
 return c;
}
printTriangle(int row){
 for (int i = 1; i <= row; i++) {
        for (int j = 1; j <= row - i; j++) {
           print(" ");
        }
        for (int j = 1; j <= i; j++) {
            print(num(i, j) + " ");
        }
        println("");
    }
}
printTriangle(7);

// output:
      1 
     1 1 
    1 2 1 
   1 3 3 1 
  1 4 6 4 1 
 1 5 10 10 5 1 
1 6 15 20 15 6 1 

函数中的 return

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int num(int x,int y){
 if (y==1 || y ==x) {
  return 1;
 }
 int v1 = num(x - 1, y - 1);
 return c;
}

现在我们来看看这样的代码为什么执行完 return 1 之后就不会执行后边的语句了。

其实在此之前我首先解决的时候函数 return 后不能执行后续 statement 的需求,其实正好就是上文提到的逻辑,只是这里是递归而已。

先把代码简化一下方便分析:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int f1(int a){
 if (a==10){
  return 10;
 }
 println("abc");
}

当参数 a 等于 10 的时候确实不能执行后续的打印语句了,那么如何实现该需求呢?

以正常人类的思考方式:当我们执行完 return 语句的时候,就应该标记该语句所属的函数直接返回,不能在执行后续的 statement

可是这应该如何实操呢?

其实看看 AST 就能明白了:

当碰到 return 语句的时,会递归向上遍历语法树,标记上所有 block 节点表明这个 block 后续的语句不再执行了,同时还得把返回值记录下来。

这样当执行到下一个 statement 时,也就是 println("abc"); 则会判断他所属的 block 是否有被标记,如果有则直接返回,这样便实现了 return 语句不执行后续代码。

部分实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 在 return 的时候递归向上扫描所有的 Block,并打上标记,用于后面执行 return 的时候直接返回。
func (v *Visitor) scanBlockStatementCtx(tree antlr.ParseTree, value interface{}) {
 context, ok := tree.(*parser.BlockContext)
 if ok {
  if v.blockCtx2Mark == nil {
   v.blockCtx2Mark = make(map[*parser.BlockContext]interface{})
  }
  v.blockCtx2Mark[context] = value
 }
 if tree.GetParent() != nil {
  v.scanBlockStatementCtx(tree.GetParent().(antlr.ParseTree), value)
 }
}

源码地址:https://github.com/crossoverJie/gscript/blob/793d196244416574bd6be641534742e57c54db7a/visitor.go#L182

递归的问题

但同时问题也来了,就是递归的时候也不会执行后续的递归代码了。

其实解决问题的方法也很简单,就是在判断是否需要直接返回那里新增一个条件,这个 block 中不存在递归调用。

所以我们就得先知道这个 block 中是否存在递归调用。

整个过程有以下几步:

  • 编译期:在函数声明处记录下函数与当前 context 的映射关系。
  • 编译期:扫描 statement 时,取出该 statementcontext 所对应的函数。
  • 编译期:扫描到的 statement 如果是一个函数调用,则判断该函数是否为该 block 中的函数,也就是第二步取出的函数。
  • 编译期:如果两个函数相等,则将当前 block 标记为递归调用。
  • 运行期:在刚才判断 return 语句处,额外多出判断当前 block 是否为递归调用,如果是则不能返回。

部分代码如下:

https://github.com/crossoverJie/gscript/blob/3e179f27cb30ca5c3af57b3fbf2e46075baa266b/resolver/ref_resolver.go#L70

总结

这里的递归调用其实卡了我挺长时间的,思路是有的,但是写出来的代码总是和预期不符,当天晚上坐在电脑面前到凌晨两三点,百思不得其解。

最后受不了上床休息的时候,突然一个灵光乍现让我想到了解决方案,于是第二天起了个早床赶忙实践,还真给解决了。

所以有些时候碰到棘手问题时给自己放松一下,往往会有出其不意的效果。

最后是目前的递归在某些情况下性能还有些问题,后续会尽量将这些标记过程都放在编译期,编译慢点没事,但运行时慢那就有问题了。

之后还会继续优化运行时的异常,目前是直接 panic,堆栈也没有,体感非常不好;欢迎感兴趣的朋友试用反馈bug。

源码地址:

https://github.com/crossoverJie/gscript

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 crossoverJie 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
手写编程语言-实现运算符重载
运算符重载其实也是多态的一种表现形式,我们可以重写运算符的重载函数,从而改变他们的计算规则。
crossoverJie
2022/12/20
3530
手写编程语言-实现运算符重载
手写编程语言-如何为 GScript 编写标准库
这是一个可以在线运行 GScript 脚本的网站,其本质原理是接收用户的输入源码从而在服务器上运行的服务;这简直就是后门大开的 XSS 攻击,为保住服务器我设置了运行 API 的后端服务的用户权限,这样可以避免执行一些恶意的请求。
crossoverJie
2022/12/20
4970
手写编程语言-如何为 GScript 编写标准库
终于实现了一门属于自己的编程语言
都说程序员的三大浪漫是:操作系统、编译原理、图形学;最后的图形学确实是特定的专业领域,我们几乎接触不到,所以对我来说换成网络更合适一些,最后再加上一个数据库。
crossoverJie
2022/12/20
5450
终于实现了一门属于自己的编程语言
里程碑!用自己的编程语言实现了一个网站
在上一篇《终于实现了一门属于自己的编程语言》 介绍了自己写的编程语言 GScript ,在文中提到希望最终可以使用 GScript 开发一个网站。
crossoverJie
2022/12/20
3280
里程碑!用自己的编程语言实现了一个网站
《Go语言程序设计》读书笔记(二)函数
《Go 语言程序设计》在线阅读地址:https://yar999.gitbooks.io/gopl-zh/content/
KevinYan
2019/12/30
4550
Golang中函数的使用
闭包:闭包是指一个函数内部定义的函数,它可以访问外部函数的变量,并将这些变量与函数绑定,形成一个闭合的环境。
周小末天天开心
2023/10/16
2270
go-函数
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:
新人小试
2020/03/05
9110
go-函数
4.Go编程快速入门学习
描述: Go 语言中的指针区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算是安全指针。
全栈工程师修炼指南
2022/09/29
6790
4.Go编程快速入门学习
7-函数
当存在多个默认参数的时候,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 7),意思是,除了name,gender这两个参数外,最后1个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。
用户3106371
2019/03/11
7500
7-函数
Python函数
位置可变参数可以在普通参数之前, 但是在位置可变参数之后的普通参数变成了keyword-only参数:
职场亮哥
2020/10/10
2.6K0
彻底理解闭包实现原理
闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 Python 和 Go 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理。
crossoverJie
2022/12/20
3800
彻底理解闭包实现原理
Python学习笔记(二)·函数
当代码出现有规律的重复的时候,你就需要当心了,每次写3.14 * x * x不仅很麻烦,而且,如果要把3.14改成3.14159265359的时候,得全部替换。
公爵
2022/09/28
1.7K0
Python学习笔记(二)·函数
Go函数介绍与一等公民
函数对应的英文单词是 Function,Function 这个单词原本是功能、职责的意思。编程语言使用 Function 这个单词,表示将一个大问题分解后而形成的、若干具有特定功能或职责的小任务,可以说十分贴切。函数代表的小任务可以在一个程序中被多次使用,甚至可以在不同程序中被使用,因此函数的出现也提升了整个程序界代码复用的水平。
贾维斯Echo
2023/10/18
2070
Go函数介绍与一等公民
递归函数和匿名函数
生信喵实验柴
2023/09/04
1760
递归函数和匿名函数
一门语言的作用域和函数调用是如何实现的
上次利用 Antlr 重构一版 用 Antlr 重构脚本解释器 之后便着手新增其他功能,也就是现在看到的支持了作用域以及函数调用。
crossoverJie
2022/10/27
6040
一门语言的作用域和函数调用是如何实现的
长文详解:C语言预处理命令
预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。
C语言与CPP编程
2020/12/02
3K0
Python 函数引入
由若干语句组成的语句块,函数名称,参数列表构成,它是组织代码的最小单元,完成一定功能。
江小白
2019/06/08
9260
[日常] Go语言圣经-可变参数习题
1.参数数量可变的函数称为为可变参数函数,例子就是fmt.Printf和类似函数 2.参数列表的最后一个参数类型之前加上省略符号“...” 3.虽然在可变参数函数内部,...int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的 类型不同:fmt.Printf("%T\n", f) 4.函数名的后缀f是一种通用的命名规范,代表该可变参数函数可以接收Printf风格的格式化字符串 5.interfac{}表示函数的最后一个参数可以接收任意类型
唯一Chat
2019/09/10
5950
[日常] Go语言圣经-可变参数习题
C/C++开发基础——可变参数与可变参数模板
1.如果可变参数的参数类型相同,可以使用标准库中的initializer_list。
Coder-ZZ
2023/09/04
7790
C/C++开发基础——可变参数与可变参数模板
【C】函数和递归的使用
数学中我们常见到函数的概念。但是你了解C语言中的函数吗? 维基百科中对函数的定义:子程序 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
阿伟@t
2023/10/10
2660
【C】函数和递归的使用
相关推荐
手写编程语言-实现运算符重载
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验