2017-05-30
编程语言
解释器
自制解释器 - 函数及作用域
第一篇解决了解析 S-表达式 的问题
第二篇引入了变量声明和一个简单的作用域模型(堆栈)
在上面的基础上实现了解释以下代码的能力
00Q(`
01(
02    (let [x 5])
03    (+ 1 x)
04)
05`);
06// 得到结果 6
这一篇要在上面的基础上,完成函数声明和调用,以及完成动态作用域。

# 什么是动态作用域

00var hello = 'world in global'; 
01
02function a(){
03    console.log(hello); 
04}
05
06function b(){
07    var hello = 'world in b'; 
08
09    a(); 
10}
11
12// 调用 b 
13b();
上面的代码在 JavaScript 这种词法作用域(也称作静态作用域)的语言里面输出 world in global,而动态作用域则输出 world in b,动态体现在函数执行调用的时候,静态则体现在代码的词法上。
这里的 Q-lang 采用动态作用域。

# 函数调用的细节

当调用函数的时候,读取参数列表(形参),然后读取函数后跟的参数(实参),然后创建一个新作用域 newScope 然后在上面用上一篇的方法 声明形参 把实参的值 绑定 上去,然后执行函数体。
  1. 寻找形参 比如函数 function test(a, b){ } a 和 b 就是形参(形式参数)
  2. 寻找实参 比如调用 test(1, 2) 其中的 1 和 2 就是实参(实际参数)
  3. 创建空作用域并把它压入栈
  4. 绑定实参到形参 let a = 2 把 2 绑定到变量 a 上。(声明初始化变量的意味)
  5. 执行函数体 在 Q-lang 里函数体就是 S-表达式
  6. 返回值 弹出顶部作用域
上述就是 Q-lang 函数调用的过程。

# 每一个S-表达式都是一次函数调用

回头看看一个简单的 S-表达式 (+ 1 2) 他的值是 3
再看看 (add 1 2)
对比一下前后,我们发现除了 函数名 不同,其他都一样,他们具有形式一致性,加法这样的四则运算其实也是 函数 ,进一步来讲每一个 S-表达式 其实都是类似的结构,他们都是函数调用的形式。
所以每进入一个 S-表达式其实就是调用一次函数 都会创建一个新作用域(入栈),而每从一个 S-表达式 里返回值的时候都会销毁一个作用域(出栈)
在入栈的时候,我们把形参声明出来,并把实参绑定上去,出栈的时候这些东西都会被销毁,这样就可以完成调用函数而不留下痕迹了。
因为连 + - * / 等四则运算都是函数,即每一个 S-表达式 都是函数。
所以,每一次对 S-表达式 求值的时候都应该压入一个新作用域到 scopes ,每一次求值结束准备返回值的时候都应该把顶部作用域弹出。

# 声明的语法

函数调用很简单,前面说过每一个 S-表达式 都是函数,所以S-表达式的第一个元素是函数,其余紧接着参数。
而声明,上一篇的变量声明是利用 let 关键字辅助中括号实现的,这一次也采取类似的做法:
00Q(`
01(
02    (let [
03        add4 (lambda (x y) (+ x y))
04    ])
05    (add4 2 3)
06)
07`);
语法格式:
let [函数名 (lambda (参数列表) (函数体))]

# 观察 AST 结构

直接用上一篇写的代码来解析刚刚的代码,看看生成的 AST 是如何的。
AST 结构
AST 结构
格式化之后像下面这样
00var ast = {
01    "o":{
02        "o":"let",
03        "a":"[",
04        "b":"add4",
05        "c":{
06            "o":"lambda",
07            "a":{
08                "o":"x",
09                "a":"y"
10            },
11            "b":{
12                "o":"+",
13                "a":"x",
14                "b":"y"
15            }
16        },
17        "d":"]"
18    },
19    "a":{
20        "o":"add4",
21        "a":2,
22        "b":3
23    }
24}
ast.a 如预期所想,o是函数,a b都是参数,这点没问题。
关键在于 ast.o :
把 ast.o 下的元素排列起来就是: let [ add4 Lambda表达式 ]
上一节写的对 let关键字 的读取在这里依然可以用。
因此我们能解析出 add4lambda 表达式:(lambda (x y) (+ x y))
而 lambda 表达式其实也是 S-表达式,只是没那么标准。 调用第一篇用的去括号的方法,把lambda表达式的tokens取出来就可以得到形参和函数体了。

# 实现

先引入工具函数 treeToArr pop push
00var treeToArr = tree => keysTo.map(key => {
01    return tree[key]
02}).filter(e => e !== undefined);
03
04var pop = () => scopes.pop(); 
05
06var push = newScope => scopes.push(newScope);
treeToArr 可以把树转化成数组(根据 keysTo 上的顺序)
pop用于弹出顶部作用域, push用于创建一个作用域。
只需要改动遍历 AST 的时候调用的 calc 就可以了。
00// 从语法树得到值 
01// 遍历 
02var calc = tree => {
03    let oType = typeof tree.o;
04
05    if (oType === 'object'){
06        // 调用 scopeCalc 去处理作用域 
07        scopeCalc(tree.o); 
08        
09        // 如果存在下一个S-表达式 则继续递归树 
10        if (tree.a) {
11            let returnVal = calc(tree.a);
12            // returnVal 表示 tree.a 已经求出值 需要弹出顶部作用域
13            pop(); 
14            return returnVal; 
15        }
16    } else if (oType === 'string') {
17        // 创建一个作用域 
18        push(Object.create(null));
19
20        let o = find(tree.o); 
21        
22        // 这里需要改动 return 前应该 pop() 弹出顶部作用域 
23        var vals = keysTo.slice(1).filter(key => tree[key]).map((cur, idx, its) => {
24            let t = tree[cur]
25              , is_ns = typeof t === 'number' || typeof t === 'string'
26              , keyInTree = tree[its[idx]]
27
28            if (is_ns){
29                return find(keyInTree)
30            } else {
31                let returnVal = calc(keyInTree); 
32                pop(); 
33                return returnVal; 
34            }
35        }); 
36
37        // 这里是新增的 
38        if (o.o === 'lambda'){
39            // lambda 的处理 
40            let lambda = treeToArr(o);
41            let list = treeToArr(lambda[1]); 
42            // 创建新作用域 newScope 
43            let newScope = list.reduce((acc, name, idx) => {
44                acc[name] = vals[idx]; 
45                return acc; 
46            }, Object.create(null)); 
47  
48            // 把新作用域压入栈 
49            push(newScope); 
50
51            // 执行函数体 
52            var returnVal = calc(lambda[2]);
53
54            // 弹出作用域
55            pop(); 
56
57            // 调试 
58            console.log('newScope:', newScope)
59            console.log('returnVal:', returnVal);
60            return returnVal;
61        } else {
62            // 从这里可以看出一个一个参数的传递 
63            // 换言之 Q-lang 只接受柯里化的函数 
64            let returnVal = vals.reduce((acc, val) => {
65                return acc(val); 
66            }, o); 
67
68            pop();
69            return returnVal;
70        }
71    }
72}
做测试:
00var a = Q(`
01(
02    (let [
03        add4 (lambda (x y) (+ x y))
04    ])
05    (add4 2 3)
06)
07`); // 期望 a 等于 5 
08
09var b = Q(`
10(
11    (let [
12        mul (lambda (x y) (* x y))
13    ])
14    (mul 1 (mul 2 (mul 3 (mul 4 5))))
15)
16`); // 期望 b 等于 120 
17
18console.log('>>>> Q-lang:')
19console.log('>>>> a:', a, 'b:', b);
测试结果
测试结果

# 不和谐

或许你注意到了,lambda表达式声明的函数不是柯里化的,它跟定义在全局的那些函数不一样,换句话说,不是一视同仁,这个问题将会在后几篇里面解决。
( 本次写的代码都已注入本页面 打开浏览器 F12 即可使用 Q )




回到顶部