第一篇解决了解析 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
然后在上面用上一篇的方法 声明形参
把实参的值 绑定
上去,然后执行函数体。- 寻找形参 比如函数 function test(a, b){ } a 和 b 就是形参(形式参数)
- 寻找实参 比如调用 test(1, 2) 其中的 1 和 2 就是实参(实际参数)
- 创建空作用域并把它压入栈
- 绑定实参到形参
let a = 2
把 2 绑定到变量 a 上。(声明初始化变量的意味) - 执行函数体 在
Q-lang
里函数体就是S-表达式
- 返回值 弹出顶部作用域
上述就是
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 结构
格式化之后像下面这样
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关键字
的读取在这里依然可以用。因此我们能解析出
add4
和 lambda
表达式:(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用于创建一个作用域。
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 )