用js 实现一个lisp 解释器的词法语法结构分析

699 阅读4分钟

本文介绍

由于最近在看《计算机程序的构造和解释》这本书以及相应的视频,觉得书中很多的理论知识都对我有很大的启发作用,让我对写程序或者说如何构建一个健壮的系统有了更深的理解,于是找来了github上的mal(make a lisp)项目,想通过此项目来实践一下学到的理论知识

本文目标

使用Javascript 实现一个lisp 解释器中的第一步: read_print, 即对输入的字符串进行词法及语法分析,并打印输入的内容

github原文地址:

英文版 中文版

实现的线路图

step1_read_print.png

为什么选择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 进行改写。