Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一门语言的作用域和函数调用是如何实现的

一门语言的作用域和函数调用是如何实现的

作者头像
crossoverJie
发布于 2022-10-27 05:27:51
发布于 2022-10-27 05:27:51
63400
代码可运行
举报
文章被收录于专栏:crossoverJiecrossoverJie
运行总次数:0
代码可运行

前言

上次利用 Antlr 重构一版 用 Antlr 重构脚本解释器 之后便着手新增其他功能,也就是现在看到的支持了作用域以及函数调用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int b= 10;
int foo(int age){
 for(int i=0;i<10;i++){
  age++;
 }
 return b+age;
}
int add(int a,int b) {
 int e = foo(10);
 e = e+10;
 return a+b+3+e;
}
add(2,20);
// Output:65

整个语法规则大部分参考了 Java,现阶段支持了:

  • 函数声明与调用。
  • 函数调用的入栈和出栈,保证了函数局部变量在函数退出时销毁。
  • 作用域支持,内部作用域可以访问外部作用域的变量。
  • 基本的表达式语句,如 i++, !=,==

这次实现的重点与难点则是作用域与函数调用,实现之后也算是满足了我的好奇心,不过在讲作用域与函数调用之前先来看看一个简单的变量声明与访问语句是如何实现的,这样后续的理解会更加容易。

变量声明

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int a=10;
a;

由于还没有实现内置函数,比如控制台输出函数 print(),所以这里就直接访问变量也能拿到数据

运行后结果如下:

首先看看变量声明语句的语法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
variableDeclarators
    : typeType variableDeclarator (',' variableDeclarator)*
    ;

variableDeclarator
    : variableDeclaratorId ('=' variableInitializer)?
    ;
typeList
    : typeType (',' typeType)*
    ;
typeType
    : (functionType | primitiveType) ('[' ']')*
    ;
primitiveType
    : INT
    | STRING
    | FLOAT
    | BOOLEAN
    ;

只看语法不太直观,直接看下生成的 AST 树就明白了:

编译期 左边这棵 BlockVardeclar 树对应的就是 int a=10;,右边的 blockStm 对应的就是变量访问 a

整个程序的运行过程分为编译期和运行期,对应的流程:

  • 遍历 AST 树,做语义分析,生成对应的符号表、类型表、引用消解、还有一些语法校验,比如变量名、函数名是否重复、是否能访问私有变量等。
  • 运行期:从编译期中生成的符号表、类型表中获取数据,执行具体的代码逻辑。

访问 AST

对于刚才提到的编译期和运行期其实分别对应两种访问 AST 的方式,这也是 Antlr 所提供两种方式。

Listener 模式

第一种是 Listener 模式,就这名字也能猜到是如何运行的;我们需要实现 Antlr 所提供的接口,这些接口分别对应 AST 树中的不同节点。

接着 Antlr 会自动遍历这棵树,当访问和退出某个节点时变会回调我们自定义的方法,这些接口都是没有返回值的,所以我们需要将遍历过程中的数据自行存放起来。

这点非常适合上文提到的编译期,遍历过程中产生的数据自然就会存放到符号表、类型表这些容器中。

以这段代码为例,我们实现了程序根节点、for循环节点的进入和退出 Listener,当 Antlr 运行到这些节点时便会执行其中的逻辑。

https://github.com/crossoverJie/gscript/blob/main/resolver/type_scope_resolver.go

Visitor 模式

Visitor 模式正好和 Listener 相反,这是由我们自行控制需要访问哪个 AST 节点,同时需要在每次访问之后返回数据,这点非常适合来做程序运行期。

配合在编译期中存放的数据,便可以实现各种特性了。

以上图为例,在访问 Prog 节点时便可以从编译期中拿到当前节点所对应的作用域 scope,同时我们可以自行控制访问下一个节点 VisitBlockStms,访问其他节点当然也是可以的,不过通常我们还是按照语法中定义的结构进行访问。

作用域

即便是同一个语法生成的 AST 是相同的,但我们在遍历 AST 时实现不同也就会导致不同的语义,这就是各个语言语义分析的不同之处。

比如 Java 不允许在子作用域中声明和父作用域中相同的变量,但 JavaScript 却是可以的。

有了上面的基础下面我们来看看作用域是如何实现的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int a=10;
a;

还是以这段代码为例:

这里我简单画了下流程:

在编译期间会会为当前节点写入一个 scope,以及在 scope 中写入变量 “a”

这里的写入 scope 和写入变量是分为两次 Listener 进行的,具体代码实现在下面查看源码。

第一次:https://github.com/crossoverJie/gscript/blob/main/resolver/type_scope_resolver.go#L21

第二次:https://github.com/crossoverJie/gscript/blob/main/resolver/type_resolver.go#L59

接着是运行期,从编译期中生成的数据拿到 scope 以及其中的变量,获取变量时有一个细节:当前 scope 中如果获取不到需要尝试从父级 scope 中获取,比如如下情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int b= 10;
int foo(){
 return b;
}

这里的 b 在当前函数作用域中是获取不到的,只能在父级 scope 中获取。

父级 scope 的关系是在创建 scope 的时候维护进去的,默认当前 scope 就是写入时 scope 的父级。

关键代码试下如下图:

第四步获取变量的值也是需要访问到 AST 中的字面量节点获取值即可,核心代码如下:

函数

函数的调用最核心的就是在运行时需要把当前函数中的所有数据入栈,访问完毕后出栈,这样才能实现函数退出后自动释放函数体类的数据。

核心代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int b= 10;
int foo(){
 return b;
}
int func(int a,int b) {
 int e = foo();
 return a+b+3+e;
}
func(2,20);

即便是有上面这类函数类调其他函数情况也不必担心,无非就是在执行函数体的时候再往栈中写入数据而已,函数退出后会依次退出栈帧。

有点类似于匹配括号的算法 {[()]},本质上就是递归调用。

总结

限于篇幅其中的许多细节没有仔细讨论,感兴趣的朋友可以直接跑跑单测,debug 试试。

https://github.com/crossoverJie/gscript/blob/main/compiler_test.go

目前的版本还比较初级,比如基本类型还只有 int,也没有一些常用的内置函数。

后续会逐步完善,比如新增:

  • 函数多返回值。
  • 自定义类型
  • 闭包

等特性,这个坑会一直填下去,希望在年底可以用 gscript 写一个 web 服务端那就算是里程碑完成了。

现阶段也实现了一个简易的 REPL 工具,大家可以安装试用:

源码地址:https://github.com/crossoverJie/gscript

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
几百行代码实现一个脚本解释器
最近又在重新学习编译原理了,其实两年前也复习过,当初是为了能实现通过 MySQL 的 DDL 生成 Python 中 sqlalchemy 的 model。
crossoverJie
2022/10/27
5980
几百行代码实现一个脚本解释器
用 Antlr 重构脚本解释器
在上一个版本实现的脚本解释器 GScript 中实现了基本的四则运算以及 AST 的生成。
crossoverJie
2022/10/27
8300
用 Antlr 重构脚本解释器
从 JavaScript 作用域说开去
在电脑程序设计中,作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding)保持有效的那部分计算机程序。不同的编程语言可能有不同的作用域和名字解析。而同一语言内也可能存在多种作用域,随实体的类型变化而不同。作用域类别影响变量的绑定方式,根据语言使用静态作用域还是动态作用域变量的取值可能会有不同的结果。
一缕殇流化隐半边冰霜
2018/08/29
9000
从 JavaScript 作用域说开去
终于实现了一门属于自己的编程语言
都说程序员的三大浪漫是:操作系统、编译原理、图形学;最后的图形学确实是特定的专业领域,我们几乎接触不到,所以对我来说换成网络更合适一些,最后再加上一个数据库。
crossoverJie
2022/12/20
5610
终于实现了一门属于自己的编程语言
手写编程语言-实现运算符重载
运算符重载其实也是多态的一种表现形式,我们可以重写运算符的重载函数,从而改变他们的计算规则。
crossoverJie
2022/12/20
3650
手写编程语言-实现运算符重载
作用域和闭包
几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。
Cellinlab
2023/05/17
7850
作用域和闭包
java作用域-javaScript预编译、作用域,作用域链详解
  ES5中只分为全局作用域和函数作用域java作用域,也就是说for,if,while等语句是不会创建作用域的。ES6(let,const)除外。
宜轩
2022/12/29
1.5K0
作用域
几乎所有编程语言最基本的功能之一,就是能够存储变量当中的值,并且能在之后对这个值进行访问或修改。那么变量存储在哪里,程序需要时怎么去找到它们?一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量,这套规则就被称为作用域
Karl Du
2020/10/23
9170
函数与作用域
1.函数声明和函数表达式有什么区别 函数就是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值。 JavaScript有三种方法,可以声明一个函数。 1.function命令 function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面传入函数的参数。函数体放在大括号里面。 function add(s) { console.log(s) } 上面的代码命名了一个print函数,以后使用print()这种形式,就可以调用相应
小胖
2018/06/27
8700
理解JavaScript的作用域
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。作用域嵌套的查询规则:
4O4
2022/04/25
7390
JavaScript 作用域和作用域链
作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。
零式的天空
2022/03/02
1.8K0
JavaScript作用域及作用域链
作用域 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。 JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。 因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。 而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
青梅煮码
2023/03/02
1.6K0
详解作用域链
在本文中,我们将着重讨论作用域链。首先我们会了解作用域、块级作用域、相关的一些重要概念等前置基础知识,接着我们会通过几个例子来对作用域链进行详细讲解,最后我们还会涉及作用域链延长的问题。在了解完上述知识之后,在本文主要内容的最后,我们还精选了网上几个作用域链相关的题目供小伙伴思考。
石璞东
2020/05/22
5870
彻底理解闭包实现原理
闭包对于一个长期写 Java 的开发者来说估计鲜有耳闻,我在写 Python 和 Go 之前也是没怎么了解,光这名字感觉就有点"神秘莫测",这篇文章的主要目的就是从编译器的角度来分析闭包,彻底搞懂闭包的实现原理。
crossoverJie
2022/12/20
3880
彻底理解闭包实现原理
手写编程语言-递归函数是如何实现的?
本篇文章主要是记录一下在 GScript 中实现递归调用时所遇到的坑,类似的问题在中文互联网上我几乎没有找到相关的内容,所以还是很有必要记录一下。
crossoverJie
2022/12/20
7400
手写编程语言-递归函数是如何实现的?
JS编译原理,LHS与RHS查询,作用域
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
用户10106350
2022/10/28
6570
进阶 | 在chrome开发者工具中观察函数调用栈、作用域链与闭包
在前端开发中,有一个非常重要的技能,叫做断点调试。 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量对象,闭包,this等关键信息的变化。因此,断点调试对于快速定位代码错误,快速了解代码的执行过程有着非常重要的作用,这也是我们前端开发者必不可少的一个高级技能。 当然如果你对JavaScript的这些基础概念[执行上下文,变量对象,闭包,this等]了解还不够的话,想要透彻掌握断点调试可能会有一些困难。但是好在在前
用户1097444
2022/06/29
2.8K0
进阶 | 在chrome开发者工具中观察函数调用栈、作用域链与闭包
《你不知道的JavaScript》-- 作用域(笔记)
将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(Abstract Syntax Tree,AST,抽象语法树)。
爱学习的程序媛
2022/04/07
7240
《你不知道的JavaScript》-- 作用域(笔记)
《你不知道的JavaScript(上)之作用域》读书笔记
程序设计的概念:一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
after the rain
2022/08/08
5350
[2] 使用 LLVM 实现一门简单的语言
IR 指中间表达方式,介于高级语言和汇编语言之间。与高级语言相比,丢弃了语法和语义特征,比如作用域、面向对象等;与汇编语言相比,不会有硬件相关的细节,比如目标机器架构、操作系统等。
谛听
2022/03/06
2.6K0
相关推荐
几百行代码实现一个脚本解释器
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验