本文介绍
由于最近在看《计算机程序的构造和解释》这本书以及相应的视频,觉得书中很多的理论知识都对我有很大的启发作用,让我对写程序或者说如何构建一个健壮的系统有了更深的理解,于是找来了github上的mal(make a lisp)项目,想通过此项目来实践一下学到的理论知识
本文目标
使用Javascript 实现一个lisp 解释器中的第一步: read_print, 即对输入的字符串进行词法及语法分析,并打印输入的内容
github原文地址:
实现的线路图
为什么选择lisp 语言?
lisp 是高级语言,越抽象的语言越接近数学,是理论,像数学公式一样随着时间的推移而不会过时 具体可以参考以下文章
学习lisp的方式
强烈推荐MIT公开课视频github地址,以及《计算机程序的构造和解释》这本书
实现REPL (读取-求值-打印-循环)
read_print.js
主文件:用来不断的做LOOP循环
if (typeof module !== 'undefined') {
var readline = require('./node_readline')
var printer = require('./printer')
var reader = require('./reader')
}
function READ(str) {
return reader.read_str(str)
}
function EVAL(ast, env) {
return ast
}
function PRINT(exp) {
return printer._pr_str(exp, true)
}
var rep = function(str){ return PRINT(EVAL(READ(str)))}
// repl loop 循环打印
// 检查当前环境是否为node
if (typeof require !== 'undefined' && require.main === module) {
while (true) {
var line = readline.readline("user>")
if (line === null) { break}
try {
if (line) {printer.println(rep(line))}
} catch (exc) {
if (exc instanceof reader.BlankException) {continue}
if (exx instanceof Error) {console.warn(exc.stack)} else {
console.warn('Error: ' + printer._pr_str(exc, true))}
}
}
}
node_readline.js 文件
该文件需要实现从读取从终端中输入的字符串
// 使用c++ 原生模块,同步方式读取命令行,而不是用异步
var RL_LIB = "libreadline"
// 引入 c++ 动态链接库
var ffi = require("ffi-napi")
// 模块命名空间
var rlwrap = {}
var rllib = ffi.Library(RL_LIB, {
'readline': ['string', [ 'string' ] ]
})
exports.readline = rlwrap.readline = function(prompt) {
prompt = prompt || "user>"
var line = rllib.readline(prompt)
console.log(rllib.readline, 'readline')
return line
}
reader.js 文件
词法分析及语法分析的主要文件,读取输入的字符,再根据正则做词法分析,输出字符串,这里的语法分析也比较简单,直接根据不同的数据类型生成ast
以下主要实现一下几个函数
Reader
: 一个类,用于保存读取的字符串,以及读取到的位置
tokenize
: 该函数接受一个字符串,返回一个数组/列表,里面包含了所有的token
read_form
: 读取toke字符,分类做判断, 返回一个ast
read_list
: 对子树(指的是 ()
里的内容) 反复调用 read_form 分情况讨论, 将结果收集到list中
read_atom
: 解析token内容,并返回简单,非复合的数据,即没有 ()
// 用来保存reader函数
var reader = {}
if (typeof module !== 'undefined') {
var types = require('./types');
} else {
var exports = reader;
}
class Reader {
constructor(tokens) {
this.tokens=tokens,
this.position=0
}
// 返回当前位置的token,并且增大position
next() {
return this.tokens[this.position++];
}
// 返回当前位置的token
peek() {
return this.tokens[this.position]
}
}
// 词法分析
function read_str(str) {
var tokens = tokenize(str) // [ '(', '+', '1', '2', ')' ]
if (tokens.length === 0) {throw new BlankException() }
return read_form(new Reader(tokens))
}
// 该函数接受一个字符串,返回一个数组/列表,里面包含了所有的token
function tokenize(str) {
var re = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"?|;.*|[^\s\[\]{}('"`,;)]*)/g;
var results = [];
while ((match = re.exec(str)[1]) != '') {
// console.log(match, 'match')
if (match[0] === ';') {continue;}
results.push(match)
}
console.log(results, 'results')
return results
}
// 读取toke字符,分类做判断, 返回一个ast
function read_form(reader) {
var token = reader.peek();
console.log(token, 'token')
// 分情况讨论, 至顶向下递归调用分析
switch (token) {
case ';': return null
case '\'':
console.log(token, 'token_form')
reader.next();
return [types._symbol('quote'), read_form(reader)]
case '`': reader.next()
return [types._symbol('quasiquote'), read_form(reader)]
// list ()
case ')': throw new Error("unexpected ')' ")
case '(': return read_list(reader)
// atom 基本字符
default: return read_atom(reader)
}
}
// 对子树反复调用 read_form 分情况讨论, 将结果收集到list中
function read_list(reader, start, end) {
start = start || '(';
end = end || ')';
var ast = []
var token = reader.next(); // 拿到当前的字符,并且指针指向下一项
console.log(token, 'token_list');
if (token !== start) {
throw new Error("expectef '" + start + "'")
}
while((token = reader.peek()) !== end) { // 当前字符不等于 “ )”
if (!token) {
throw new Error("Expected '" + end + "', but got EOF")
}
ast.push(read_form(reader))
}
// ()使用reader收集完成,此时当前字符为 “ )”
reader.next()
return ast
}
// 解析token内容,并返回简单,非复合的数据,即没有()
function read_atom(reader) {
var token = reader.next();
console.log(token, 'token_atom')
if (token.match(/^-?[0-9]+$/)) { // integer
return parseInt(token, 10);
} else if (token.match(/^-?[0-9][0-9.]*$/)) { // float}
return parseFloat(token, 10)
} else if (token.match(/^"(?:\\.|[^\\"]*"$)/)) {
console.log(token, 'symbol-')
} else if (token === 'true') {
return true
} else if (token === 'false') {
return false
} else { // 记录数学含义符号 + - * /
return types._symbol(token)
}
}
exports.read_str = reader.read_str = read_str
exports.tokenize = reader.tokenize = tokenize
exports.read_form = reader.read_form = read_form
exports.Reader = reader.Reader = Reader
types.js 文件:
用来给不同的符号表示类型如:
+
--> Symbol { value: '+' }
-
--> Symbol { value: '-' }
'1'
--> [ Symbol { value: 'quote' }, 1 ]
(+ 1 2)
--> [ Symbol { value: '+' }, 1, 2 ]
var types = {}
function Symbol(name) {
this.value = name;
return this;
}
function _symbol(name) { return new Symbol(name)}
function _symbol_Q(obj) { return obj instanceof Symbol}
// lists
function _list() { return Array.prototype.slice.call(arguments, 0)}
function _list_Q(obj){ return Array.isArray(obj)}
// 区分类型
function _obj_type(obj) {
if (_symbol_Q(obj)) { return 'symbol'}
else if (_list_Q(obj)) { return 'list'}
else {
switch (typeof(obj)) {
case 'number': return 'number'
case 'function': return 'function'
case 'string': return 'string'
default: throw new Error("Unknown type '" + typeof(obj) + "'")
}
}
}
exports._symbol = types._symbol = _symbol;
exports._symbol_Q = types._symbol_Q = _symbol_Q;
exports._obj_type = types._obj_type = _obj_type;
exports._list = types._list = _list;
exports._list_Q = types._list_Q = _list_Q;
printer.js 文件
(可做代码优化)这里实现比较简单,直接把输出的ast转换为字符输出
var printer = {}
if (typeof module !== 'undefined') {
var types = require('./types')
printer.println = exports.println = function () {
console.log.apply(console, arguments)
}
}
function _pr_str(obj, print_readably = true) {
var _r = print_readably
var type = types._obj_type(obj) // 判断类型
switch (type) {
case 'list':
var ret = obj.map(function(it){return _pr_str(it, _r)})
console.log(ret, 'ret')
return "(" + ret.join(" ") + ")"
case 'symbol':
return obj.value
default:
return obj.toString()
}
}
exports._pr_str = printer._pr_str = _pr_str
总结
至此,实现了一个简易的支持lisp 输入lisp字符,并输出的版本,但是并不会对输入的表达式进行求值,求值的部分为第二步需要对EVAL 进行改写。