热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:类Lisp解释器JavaScript实现

篇首语:本文由编程笔记#小编为大家整理,主要介绍了类Lisp解释器JavaScript实现相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了类Lisp解释器Javascript实现相关的知识,希望对你有一定的参考价值。



离职之前得把这坑填了……可能会有些仓促,如果有错误之处之后还请大家自行勘误啦。


类 Lisp 语言语法

(define fib (lambda (n)
(if ( (+ (fib (- n 1)) (fib (- n 2))))))

观察上述斐波那契数生成函数。



  1. 使用 define 关键字绑定变量与实体

  2. 全部使用前缀表达式

  3. 没有 return 关键字

  4. 循环多采用递归实现

  5. 函数被称为“lambda表达式”,使用 lambda 关键字定义

  6. 函数必须有返回值


编译原理基本知识

使用某种语言来编写的一段程序本质上是字符串,编译器/解释器的任务是将这段字符串翻译为某个“执行器”可以理解的编码。

我认为编译器和解释器最大的不同在于,前者存在明显输出,且输出为某执行器可以理解的字符串(编码);后者没有明显输出,它直接执行了该段字符串(源程序)所表达的东西(此处应当被提出质疑)。

本文只介绍解释器的基本组成。

解释器从接受源程序到最终执行的基本过程有:



  1. 词法分析。这个步骤会将源代码分解为该语言的最小组成元素(一个一个的短字符串)。如 var a = 1; 会被分解为 [‘var‘, ‘a‘, ‘=‘, ‘1‘]。形如这样的短字符串,我们称其为“Token”。

  2. 语法分析。这个步骤会根据该语言的自身语法规则分析 Token 列表,检查源代码是否有语法错误,如果没有,则解析出一个抽象语法树(AST),语义及代码块之间的关系都由树状数据结构进行描述。

  3. 执行。当 AST 被生成之后,解释器会继续对 AST 进行分析,遵照 AST 表达出的语义来执行,其执行结果就是源代码所想要的执行结果。一些语言特性,比如闭包,就是在执行函数中实现的。


实现

接下来我们将实现一个支持整数与浮点数运算、数组类型、lambda 表达式,有着基本逻辑语句的类 Lisp 语言解释器。


词法分析

由于类 Lisp 语言本身语法比较简洁,所以词法分析的实现也会比较简单:

function tokenize(program) {
return program.replace(/\(/g, ‘ ( ‘).replace(/\)/g, ‘ ) ‘).split(‘ ‘)
}

语法分析

在生成 AST 的过程中,需要将每一个 Token 的“身份”做确认,比如 var 和 a 是关键字,1 是整数,给 Token 做身份标记。这里使用数组实现 AST。

// 生成抽象语法树
function read_from_tokens(tokens) {
if (tokens.length === 0) {
throw new Error(‘unexpected EOF while reading‘)
}
let token = tokens.shift()
while (token === ‘‘) {
token = tokens.shift()
}
if (‘(‘ === token) {
let L = []
while (tokens[0] === ‘‘) {
tokens.shift()
}
while (tokens[0] !== ‘)‘) {
L.push(read_from_tokens(tokens))
while (tokens[0] === ‘‘) {
tokens.shift()
}
}
tokens.shift()
return L
} else if (‘)‘ === token) {
throw new Error(‘unexpected )‘)
} else {
return atom(token)
}
}
// 元类型 Meta,所有数据类型的基类,随着解释器的完善会变得有用,亦在语义的角度可体现出其合理性,但在本文中不做讨论
class Meta {
constructor(value) {
this.value = value
}
}
// 符号类型 如 if define 自定义变量等语言关键字都属于此类型
class Sym extends Meta {
constructor(value) {
super(value)
}
}
// 将 token 的类型具体化
function atom(token) {
let temp = parseInt(token)
if (isNaN(temp)) {
return new Sym(token)
} else if (token - temp === 0) {
return temp
} else {
return parseFloat(token)
}
}

执行

执行的过程是解释器的核心,在这里实现为一个 eval 函数。


eval 函数

eval 函数的大致结构是一个状态机,按照该语言的语法规则对 AST 进行解析。

function eval(x, env=global_env) {
if (x instanceof Sym) { // 如果该 Token 是关键字
return env.find(x.value)[x.value] // 在当前作用域中寻找与该 Token 绑定的实体
} else if (! (x instanceof Array)) { // 不是数组
return x // 直接返回(因为此时会认为它是整数或浮点数)
} else if (x[0].value == ‘if‘) { // 如果是 if
let [sym, test, conseq, alt] = x // 按照该语言语法中约定的 if 语句的格式提取信息
let exp = (eval(test, env) ? conseq : alt)
return eval(exp, env)
} else if (x[0].value == ‘define‘) { // 如果是 define
let [vari, exp] = x.slice(1)
env.add(vari.value, eval(exp, env))
} else if (x[0].value == ‘lambda‘) { // 如果是 lambda
let [parms, body] = x.slice(1)
return new Procedure(parms, body, env) // 创建一个 Procedure 实例
} else if (x[0].value == ‘quote‘) { // 如果是 quote
let [sym, exp] = x
return exp
} else { // 否则(这里可能的情况是:x 是一个数组或一个过程(函数,在这里的实现为 Procedure 的实例,下面会讲到))
let proc = eval(x[0], env)
let args = []
x.slice(1).forEach(function(arg) {
args.push(eval(arg, env))
})
if (proc instanceof Procedure) {
return proc.execute.call(proc, args)
}
return proc.apply(this, args)
}
}

函数与环境

在该语言中,我们规定,作用域的界线是函数,且该作用域是词法作用域(与 Javascript 相同)。
所以每当碰到函数调用的时候,我们都需要创建一个新的求值环境,并使该函数被调用时所在的环境成为这个新的求值环境的父环境(如果不明白为什么这样规定,请自行搜索词法作用域)。故当我们在任何地方查找变量时,都应顺着环境链(作用域链)向上查找,直到找到。如果找不到,则抛异常。
由此,我们可以将作用域理解为一个类似单向链表的数据结构。

// Procedure 类,该语言中函数的实现
class Procedure {
constructor(parms, body, env) {
this.parms = parms
this.body = body
this.env = env
}
execute(args) {
return eval(this.body, new Env(this.parms, args, this.env))
}
}
// Env 类,该语言中求值环境的实现
class Env {
constructor(parms=[], args=[], outer=null) {
this.e = new Object()
this.init(parms, args)
this.outer = outer // 父环境
}
// 在该环境中查找某个变量
find(vari) {
if ((! (vari in this.e)) && (! this.outer)) {
throw new ReferenceError(‘variable ‘ + vari + ‘ is undefined.‘)
}
return vari in this.e ? this.e : this.outer.find(vari)
}
init(keys, values) {
keys.forEach((key, index) => {
this.e[key.value] = values[index]
})
}
assign(subEnv) {
Object.assign(this.e, subEnv)
}
add(key, value) {
this.e[key] = value
}
}
// 初始化一个全局环境
let global_env = new Env()
global_env.assign(baseEnv)

尾声

至此,这个简单的解释器就已经完成了,涉及到更多细节,如异常定义、全局环境定义,可以点此查看完整的代码。

我建议大家如果有兴趣可以自己动手实现一下,对解释器原理以及闭包会有更深刻的理解。

本文的完成比较仓促,如果大家有什么疑问可以点此阅读该解释器的 Python 实现,作者讲解得非常详细,也有很多测试用例,我正是阅读了这篇文章才写了 Javascript 版。

如果有任何错误之处,请自行勘误,或者通过邮箱([email protected])和 GitHub (Sevenskey)联系我。

感谢阅读qwq





推荐阅读
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • 本文介绍了win7系统休眠功能无法启动和关闭的解决方法,包括在控制面板中启用休眠功能、设置系统休眠的时间、通过命令行定时休眠、手动进入休眠状态等方法。 ... [详细]
  • 小程序wxs中的时间格式化以及格式化时间和date时间互转
    本文介绍了在小程序wxs中进行时间格式化操作的问题,并提供了解决方法。同时还介绍了格式化时间和date时间的互相转换的方法。 ... [详细]
  • 用Vue实现的Demo商品管理效果图及实现代码
    本文介绍了一个使用Vue实现的Demo商品管理的效果图及实现代码。 ... [详细]
  • Iamtryingtocreateanarrayofstructinstanceslikethis:我试图创建一个这样的struct实例数组:letinstallers: ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 本文整理了Java中org.apache.solr.common.SolrDocument.setField()方法的一些代码示例,展示了SolrDocum ... [详细]
  • Inno Setup区段之Components篇相关知识详解
    本文详细介绍了Inno Setup区段之Components篇相关的知识,包括Components和Types的使用方式以及各个参数的说明,希望对读者有一定的参考价值。内容涵盖了ComponentsName、Description、Types、ExtraDiskSpaceRequired、ExtraDiskSpaceRequiredFlags等多个关键词,帮助读者更好地理解和应用Inno Setup区段之Components篇的知识。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 本文讨论了在PHP中将空格转换为问号的问题,并提供了解决方案。文章指出,空格不是标准的空格,而是特殊的0xC2 0xA0字符。作者尝试使用mb_convert_encoding函数将utf8字符串转换为gbk编码,但未成功。文章建议检查编辑器是否对空格进行了特殊处理,并提供了使用base64_encode函数打印结果的方法。最后,给出了完整的代码示例。 ... [详细]
  • 本文介绍了利用ARMA模型对平稳非白噪声序列进行建模的步骤及代码实现。首先对观察值序列进行样本自相关系数和样本偏自相关系数的计算,然后根据这些系数的性质选择适当的ARMA模型进行拟合,并估计模型中的位置参数。接着进行模型的有效性检验,如果不通过则重新选择模型再拟合,如果通过则进行模型优化。最后利用拟合模型预测序列的未来走势。文章还介绍了绘制时序图、平稳性检验、白噪声检验、确定ARMA阶数和预测未来走势的代码实现。 ... [详细]
author-avatar
一叶之秋
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有