一种语言的作用域和函数调用是如何实现的作用域和函数调用是支持的。intb=10;intfoo(intage){for(inti=0;i<10;i++){age++;}returnb+age;}intadd(inta,intb){inte=foo(10);e=e+10;returna+b+3+e;}add(2,20);//Output:65大部分语法规则参考Java,现阶段支持:函数声明和调用。函数调用的入栈和出栈保证了函数的局部变量在函数退出时被销毁。作用域支持,内部作用域可以访问外部作用域的变量。i++、!=、==等基本表达式语句,本次实现的重点和难点在于作用域和函数调用。实现之后,也算是满足了我的好奇心,不过在说作用域和函数调用之前,我们先来看看一个简单的变量声明和访问语句是如何实现的,这样后续的理解会更容易一些。变量声明inta=10;a;由于内置函数还没有实现,比如控制台输出函数print(),这里直接访问变量也可以获取数据。运行后结果如下:首先看一下变量声明语句的语法:;typeList:typeType(','typeType)*;typeType:(functionType|primitiveType)('['']')*;primitiveType:INT|字符串|浮动|BOOLEAN;光看语法不是很直观,看生成的AST树就明白了:编译期左边的BlockVardeclar树对应的是inta=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.goVisitor模式Visitor模式正好与Listener模式相反,由我们控制需要访问哪个AST节点,需要访问每次访问后返回数据,非常适合程序运行时使用。通过在编译期间存储的数据,可以实现各种功能。以上图为例。在访问Prog节点时,我们可以从编译期得到当前节点对应的作用域。同时我们可以控制下一个节点VisitBlockStms的访问。当然也可以访问其他节点,但是通常我们还是按照语法定义的结构进行访问。即使作用域相同,同样的语法生成的AST也是一样的,但是当我们遍历AST时,不同的实现会导致不同的语义,这就是各个语言在语义分析上的差异。例如,Java不允许在子作用域中声明与在父作用域中相同的变量,但JavaScript允许。有了上面的基础,我们再来看看作用域是如何实现的。inta=10;a;还是以这段代码为例:这里我简单画出如下过程:编译时,会为当前节点写入一个scope,在scope中写入变量“a”。这里的写入作用域和变量写入分为两个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之后是运行时,作用域及其变量是从编译期间生成的数据中获取的。获取变量时有一个细节:如果当前作用域无法获取,则需要尝试从父作用域获取,如以下情况:intb=10;intfoo(){returnb;}bhere不能在当前函数作用域中获取,只能在父作用域中获取。创建作用域时会维护父作用域的关系。默认情况下,当前作用域是编写时作用域的父级。试试下图关键代码:第四步获取变量的值同样需要访问AST中的literal节点获取值。核心代码如下:函数调用的核心是在运行时调用当前函数。所有数据入栈,访问完成后出栈,这样函数体类的数据可以在函数退出后自动释放。核心代码如下:intb=10;intfoo(){returnb;}intfunc(inta,intb){inte=foo();返回a+b+3+e;}func(2,20);即使有上述类型的函数调用其他函数,也不用担心。无非是在函数体执行时向栈中写入数据。函数退出后,会一个一个退出栈帧。有点类似于匹配括号的算法{[()]},本质上是递归调用。总结由于篇幅有限,很多细节没有详细讨论。有兴趣的朋友可以直接跑单测试试调试。https://github.com/crossoverJie/gscript/blob/main/compiler_test.go目前的版本还是比较初级的。比如基本类型只有int,没有常用的内置函数。后续会逐步完善,比如增加:函数返回更多的值。对于自定义类型闭包等特性,这个坑会继续填,希望年底能用gscript写出web服务器,算是一个里程碑式的完成。现阶段也实现了一个简单的REPL工具,大家可以安装试用:源码地址:https://github.com/crossoverJie/gscript
