JavaScript 编写的迷你 Lisp 解释器
感谢@李欲纯 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线。】
Little Lisp是一个解释器,支持函数调用、lambda表达式、 变量绑定(let)、数字、字符串、几个库函数和列表(list)。我写这个是为了在Hacker School(一所位于纽约的程序员培训学校)的一个闪电秀中展示写一个解释器不是很难。一共只有116行的JavaScript代码,下文我会解释它是如何运行的。
首先,让我们学习一些Lisp。
Lisp基础
这是一个原子,最简单的Lisp形式:
|
1
|
1 |
这是另一个原子,一个字符串:
|
1
|
"a" |
这是一个空列表:
()
这是一个包含了一个原子的列表:
|
1
|
(1) |
这是一个包含了两个原子的列表:
|
1
|
(1 |
这是一个包含了一个原子和另一个列表的列表:
|
1
|
(1 |
这是一个函数调用。函数调用由一个列表组成,列表的第一个元素是要调用的函数,其余的元素是函数的参数。函数first接受一个参数(1,返回
2)1。
|
1
2
3
|
(first => |
这是一个lambda表达式,即一个函数定义。这个函数接受一个参数x,然后原样返回它。
|
1
2
|
(lambda x) |
这是一个lambda调用。lambda调用由一个列表组成,列表的第一个元素是一个lambda表达式,其余的元素是由lambda表达式所定义的函数的参数。这个lambda表达式接受一个参数"lisp"并返回它。
|
1
2
3
4
5
|
((lambda x) "Lisp") => |
Little Lisp是如何运行的
写一个Lisp解释器真的很容易。
Little Lisp的代码包括两部分:分析器和解释器
分析器
分析分两个阶段:分词(tokenizing)和加括号(parenthesizing)。
tokenize()接受一个Lisp字符串,在每个括号周围加上空格,然后用空格作为分隔符拆分整个字符串。举个例子,它接受((lambda,将它变换为
(x) x) "Lisp")( ( lambda ( x ) x ) "Lisp" ),然后进一步变换为['(',。
'(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']
|
1
2
3
4
5
6
|
varfunction(input) return') .replace(/\)/g,') .trim() .split(/\s+/);}; |
parenthesize()接受由tokenize()产生的词元列表,生成一个嵌套的数组来模拟出Lisp代码的结构。在这个嵌套的数组中的每个原子会被标记为标识符或文字表达式。例如,['(',被变换为:
'(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']
|
1
2
3
|
[[{ { { |
parenthesize()一个挨一个地遍历词元。如果当前词元是左括号,就开始构建一个新的数组。如果当前词元是原子,就标记其类型并将其添加到当前数组中。如果当前词元是右括号,就停止当前数组的构建,继续构建外层的数组。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
varfunction(input, if return }else var if return }else "(") list.push(parenthesize(input, return }else ")") return }else return } }}; |
当parenthesize()第一次被调用时,input参数包含由tokenize()返回的词元列表数组。例如:
|
1
|
['(', |
第一次调用parenthesize()时,参数list是undefined,第2-3行运行,递归调用parenthesize(),list被设置为空数组。
在递归中,第5行运行,input的第一个左括号被移除。第9行中,传一个新的空数组给递归调用,开始一个新的空列表。
在新的递归中,第5行运行,从input中移除了另一个左括号。与前面类似,第9行中,传另一个新的空数组给递归调用,开始另一个新的空列表。
继续进入递归,现在input是['lambda',。第14行运行,
'(', 'x', ')', 'x', ')', '"Lisp"', ')']token被设置为lambda,调用categorize()函数并传递lambda作为参数。categorize()的第7行运行,返回一个对象,其type属性被设置为identifier,value属性被设置为lambda。
|
1
2
3
4
5
6
7
8
9
|
varfunction(input) ifisNaN(parseFloat(input))) return'literal',parseFloat(input) }else 0]'"'1)'"') return'literal',1,1) }else return'identifier', }}; |
parenthesize()的第14行向list中加入由categorize()返回的对象,然后用input的剩余元素和list进一步递归。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
varfunction(input, ifundefined) return }else var ifundefined) return }else "(") list.push(parenthesize(input, return }else ")") return }else return } } }; |
在递归中,下一个词元是括号。parenthesize()的第9行用一个新的空数组递归创建一个新的空列表,进入新的递归,这时input是['x',。第14行运行,
')', 'x', ')', '"Lisp"', ')']token被设置成x,这样创建了一个新的对象,其值为x,类型为identifier,然后将这个对象加入到list中,然后接着递归。
在递归中,下一个词元是右括号,第12行运行,返回完成了的list:[{。
type: 'identifier', value: 'x' }]
parenthesize()继续递归直到它处理完全部的输入词元,最后返回由包含了类型信息的原子所组成的嵌套数组。
parse()是tokenize()和parenthesize()的组合调用:
|
1
2
3
|
varfunction(input) return}; |
如果原始的输入给的是((lambda (x) x) "Lisp"),则分析器给出的最后输出是:
|
1
2
3
|
[[{ { { |
解释器
在分析结束后,解释就开始了。
interpret()接收parse()的输出并执行它。提供上例中的输出,interpret()会构造一个lambda表达式,然后用"Lisp"作为参数调用它。lambda调用会返回"Lisp",这就是整个程序的输出。
除了要执行的输入外,interpret()还接收一个执行上下文。执行上下文是变量和变量对应的值所存储的地方。当一段Lisp代码被interpret()执行时,执行上下文包含着这段代码可访问的变量。
这些变量是分层存储的。当前作用域的的变量处在最底层,在包含域中的变量处在上一层,包含域的上一层包含域中的变量处于更上层,依次类推。例如,在下面的代码中:
|
1
2
3
4
5
|
((lambda ((lambda (b "b")) "a") |
第3行,执行上下文有两个活动的作用域。内层的lambda形成了当前作用域。外层的lambda形成了包含作用域。当前作用域中b被绑定到"b",包含作用域中a被绑定到"a"。当第3行运行时,解释器尝试在作用域中去查找b,它检查当前作用域,发现了b并返回它的值。还是在第3行上,解释器尝试去查找a,它检查当前作用域,结果没找到a,所以它尝试去包含域找,在那里它找到了a并返回它的值。
在Little Lisp中,执行上下文用一个对象来表示,这个对象通过调用Context构造函数来生成。这个函数接受scope参数,即一个由在当前作用域中的变量和值组成的对象;还接受parent参数,如果parent是undefined,作用域即位于顶层,或者说是全局的。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
varfunction(scope, this.scope this.parent this.getfunction(identifier) ifin.scope) return.scope[identifier]; }else this.parentundefined) return.parent.get(identifier); } };}; |
我们已看到((lambda (x) x) "Lisp")是如何被分析的,现在让我们看看分析过后的代码是如何被执行的。
|
1
2
3
4
5
6
7
8
9
10
11
|
var if return } return } return } return }}; |
interpret()第一次被调用时,context是undefined,第2-3行运行,创建一个执行上下文。
当初始上下文被实例化时,构造函数接受了一个叫library的对象。这个对象包含了内建在语言中的函数:first, rest和print。这些函数是用JavaScript写的。
interpret()用原始的输入和新的上下文进行递归。
input包含了上节中例子产生的输出:
|
1
2
3
|
[[{ { { |
因为input是数组而且context已定义,第4-5行运行,interpretList()被调用。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
varfunction(input, ifin return }else varfunction(x)return ifinstanceof return }else return } }}; |
在interpretList()中,第5行遍历input数组,对每个元素调用interpret()。当interpret()在lambda定义上调用时,interpretList()再一次被调用。这次,interpretList()的input参数为:
|
1
2
|
[{ { |
interpretList()的第3行被调用,因为数组的第一个元素lambda是特殊形式。lambda()被调用来创建lambda函数。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var lambda:function(input, return() var varfunction(acc, acc[x.value] return }, returnnew }; }}; |
special.lambda()接受input中定义lambda的部分,返回一个函数,当这个函数被调用时,会对一些参数调用这个lambda函数。
第3行开始lambda调用函数的定义。第4行保存了传递给lambda调用的参数。第5行开始为lambda调用创建一个新的作用域,收集input中定义lambda的参数的部分: [{,针对
type: 'identifier', value: 'x' }]input中的每一个lambda形参和传递给lambda的对应实参,往lambda作用域中添加一个键值对。第10行对lambda的主体调用interpret():{。它传递给的lambda上下文包含lambda的作用域和父上下文。
type: 'identifier', value: 'x' }
lambda现在就变成了被special.lambda()返回的函数。
interpretList() 继续遍历input数组,对列表的第二个元素调用interpret():字符串"Lisp"。
|
1
2
3
4
5
6
7
8
9
10
11
|
varfunction(input, if returnnew }else instanceof return }else "identifier") return }else return }}; |
interpret()的第9行运行,这行做的事情仅仅是返回字面量对象的value属性'Lisp'。interpretList()的第5行的map操作至此完成。list成为:
|
1
2
|
[function(args) 'Lisp'] |
interpretList()的第6行运行,发现List的第一个元素是一个Javascript函数,这意味着list是一个函数调用。第7行运行,调用lambda函数,并将list的剩余部分作为参数传递。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
varfunction(input, ifin return }else varfunction(x)return ifinstanceof return }else return } }}; |
在lambda调用函数中,第8行对lambda主体调用interpret(),{。
type: 'identifier', value: 'x' }
|
1
2
3
4
5
6
7
8
9
|
function() var var acc[x.value] return }, return}; |
interpret()的第6行发现input是一个标识符类型的原子,第7行去上下文里查找标识符x,返回'Lisp'。
|
1
2
3
4
5
6
7
8
9
10
11
|
varfunction(input, if returnnew }else instanceof return }else "identifier") return }else return }}; |
'Lisp'被lambda调用函数返回,接着被interpretList()返回,接着被interpret()返回,就是这样。
全部的代码见GitHub repository。还可以看看lis.py,一个优秀而简单的Scheme解释器,由Peter
Norvig用Python编写。
JavaScript 编写的迷你 Lisp 解释器的更多相关文章
- artDialog是一个基于javascript编写的对话框组件,它拥有精致的界面与友好的接口
artDialog是一个基于javascript编写的对话框组件,它拥有精致的界面与友好的接口 自适应内容 artDialog的特殊UI框架能够适应内容变化,甚至连外部程序动态插入的内容它仍然能自适应 ...
- 用Javascript编写Chrome浏览器插件
原文:http://homepage.yesky.com/62/11206062.shtml 用Javascript编写Chrome浏览器插件 2010-04-12 07:30 来源:天极网软件频道 ...
- javascript 编写的贪吃蛇
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- three.js是JavaScript编写的WebGL第 三方库
three.js是JavaScript编写的WebGL第 三方库.提供了非常多的3D显示功能.Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它创建各种三维场景,包括了摄影机.光影.材质 ...
- JavaScript编写了一个计时器
初学JavaScript,用JavaScript编写了一个计时器. 设计思想: 1.借助于Date()对象,来不断获取时间点: 2.然后用两次时间点的毫秒数相减,算出时间差: 3.累加时间差,这样就能 ...
- javascript编写一个简单的编译器(理解抽象语法树AST)
javascript编写一个简单的编译器(理解抽象语法树AST) 编译器 是一种接收一段代码,然后把它转成一些其他一种机制.我们现在来做一个在一张纸上画出一条线,那么我们画出一条线需要定义的条件如下: ...
- canvas :原生javascript编写动态时钟
canvas :原生javascript编写动态时钟 此时针是以画布的中心为圆心: g.translate(width/2,width/2); 此函数是将画布的原点移到(width/2,wid ...
- JavaScript学习总结(十四)——JavaScript编写类的扩展方法
在JavaScript中可以使用类的prototype属性来扩展类的属性和方法,在实际开发当中,当JavaScript内置的那些类所提供的动态 ...
- JavaScript学习总结(十二)——JavaScript编写类
在工作中经常用到JavaScript,今天总结一下JavaScript编写类的几种写法以及这几种写法的优缺点,关于JavaScript编写类的方式,在网上看到很多,而且每个人的写法都不太一样,经常看到 ...
- 【教程】HTML5+JavaScript编写flappy bird
作者: 风小锐 新浪微博ID:永远de风小锐 QQ:547953539 转载请注明出处 PS:新修复了两个bug,已下载代码的同学请查看一下 大学立即要毕业了. ...
随机推荐
- SpringMVC:文件上传和下载
文件下载 ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文 使用ResponseEntity实现下载文件的功能 @RequestMapping(& ...
- 【YashanDB知识库】主备延迟故障分析方法
[标题]主备延迟故障分析方法 [问题分类]故障分析 [关键字]Yashandb.主备延迟 [问题描述]当数据库备机出现回放延迟时,需要通过一些手段分析延迟的原因.通过数据库的系统视图或操作系统监控数据 ...
- RuleLinKClient - 再也不担心表达引擎宕机了
原来有这么多时间 六月的那么一天,天气比以往时候都更凉爽,媳妇边收拾桌子,边漫不经心的对我说:你最近好像都没怎么阅读了. 正刷着新闻我,如同被一记响亮的晴空霹雳击中一般,不知所措.是了,最近几月诸事凑 ...
- maven jetty指定端口号启动
mvn jetty 启动指定端口号 方法 mvn jetty:run -Djetty.port=端口号 备注: 通过以上命令在 windows 中 powershell 下运行时,可能会碰到以下问题: ...
- Angular 18+ 高级教程 – Angular 的局限和 Github Issues
前言 Angular 绝对有很多缺陷,Issue 非常多,workaround 非常多. 我以前至少有 subscribe 超过 20 个 Issues,几年都没有 right way 处理的. An ...
- 反DDD模式之“复用”
本文书接上回<反DDD模式之关系型数据库>,关注公众号(老肖想当外语大佬)获取信息: 最新文章更新: DDD框架源码(.NET.Java双平台): 加群畅聊,建模分析.技术实现交流: 视频 ...
- angularjs中控制视图的控制器的两种注入依赖项及服务的写法
在AngularJS中,控制器是用于控制视图行为的重要组件.当定义控制器时,有两种主要的方式注入依赖项: 1. 显式依赖注入,聚聚使用字符串数组形式来注入依赖项: myapp.controller(' ...
- Linux中的一些命令
1.新增新用户lili,不允许登录系统,用户ID为3000===useradd -u 3000 -s /sbin/nologin lili2.循环创建目录 /www/wwwroot/html/test ...
- 五行八字在线排盘api接口免费版_json数据格式奥顺互联内部接口
「八字在线排盘」谁都想知道自己一生中的事业.财运.婚姻.功名.健康.性格.流年运程将是怎样,通过八字排盘,四柱八字排盘会有你想知道的答案.一个人出生的年月时天干地支的排列组合(即八字)就是命.不过仅凭 ...
- js中数据的基本类型
有5种基本数据类型分类 : 1. 数字型 number 2. 字符型 string 3. 布尔型 boolean 4. undefined 未定义 就是声明了但是没有赋值 5. null 空指针 ...