• JavaScript 编写的迷你 Lisp 解释器


    感谢@李欲纯 的热心翻译。如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线。】

    Little Lisp是一个解释器,支持函数调用、lambda表达式、 变量绑定(let)、数字、字符串、几个库函数和列表(list)。我写这个是为了在Hacker School(一所位于纽约的程序员培训学校)的一个闪电秀中展示写一个解释器不是很难。一共只有116行的JavaScript代码,下文我会解释它是如何运行的。

     

    首先,让我们学习一些Lisp。

    Lisp基础

    这是一个原子,最简单的Lisp形式:

    1
    1

    这是另一个原子,一个字符串:

    1
    "a"

    这是一个空列表:

    ()

    这是一个包含了一个原子的列表:

    1
    (1)

    这是一个包含了两个原子的列表:

    1
    (1 2)

    这是一个包含了一个原子和另一个列表的列表:

    1
    (1 (2))

    这是一个函数调用。函数调用由一个列表组成,列表的第一个元素是要调用的函数,其余的元素是函数的参数。函数first接受一个参数(1 2),返回1

    1
    2
    3
    (first (1 2))
     
     => 1

    这是一个lambda表达式,即一个函数定义。这个函数接受一个参数x,然后原样返回它。

    1
    2
    (lambda (x)
     x)

    这是一个lambda调用。lambda调用由一个列表组成,列表的第一个元素是一个lambda表达式,其余的元素是由lambda表达式所定义的函数的参数。这个lambda表达式接受一个参数"lisp"并返回它。

    1
    2
    3
    4
    5
    ((lambda (x)
      x)
     "Lisp")
     
     => "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
    var tokenize = function(input) {
      return replace(/(/g, ' ( ')
            .replace(/)/g, ' ) ')
            .trim()
            .split(/s+/);
    };

    parenthesize()接受由tokenize()产生的词元列表,生成一个嵌套的数组来模拟出Lisp代码的结构。在这个嵌套的数组中的每个原子会被标记为标识符或文字表达式。例如,['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']被变换为:

    1
    2
    3
    [[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }],
      { type: 'identifier', value: 'x' }],
     { type: 'literal', value: 'Lisp' }]

    parenthesize()一个挨一个地遍历词元。如果当前词元是左括号,就开始构建一个新的数组。如果当前词元是原子,就标记其类型并将其添加到当前数组中。如果当前词元是右括号,就停止当前数组的构建,继续构建外层的数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var parenthesize = function(input, list) {
         if (list === undefined) {
           return parenthesize(input, []);
         } else {
           var token = input.shift();
           if (token === undefined) {
             return list.pop();
           } else if (token === "(") {
             list.push(parenthesize(input, []));
            return parenthesize(input, list);
          } else if (token === ")") {
            return list;
          } else {
            return parenthesize(input, list.concat(categorize(token)));
          }
        }
    };

    parenthesize()第一次被调用时,input参数包含由tokenize()返回的词元列表数组。例如:

    1
    ['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']

    第一次调用parenthesize()时,参数listundefined,第2-3行运行,递归调用parenthesize()list被设置为空数组。

    在递归中,第5行运行,input的第一个左括号被移除。第9行中,传一个新的空数组给递归调用,开始一个新的空列表。

    在新的递归中,第5行运行,从input中移除了另一个左括号。与前面类似,第9行中,传另一个新的空数组给递归调用,开始另一个新的空列表。

    继续进入递归,现在input['lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']。第14行运行,token被设置为lambda,调用categorize()函数并传递lambda作为参数。categorize()的第7行运行,返回一个对象,其type属性被设置为identifiervalue属性被设置为lambda

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var categorize = function(input) {
      if (!isNaN(parseFloat(input))) {
         return { type:'literal', value: parseFloat(input) };
       } else if (input[0] === '"' && input.slice(-1) === '"') {
         return { type:'literal', value: input.slice(1, -1) };
       } else {
        return { type:'identifier', value: input };
      }
    };

    parenthesize()的第14行向list中加入由categorize()返回的对象,然后用input的剩余元素和list进一步递归。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var parenthesize = function(input, list) {
      if (list === undefined) {
        return parenthesize(input, []);
     } else {
        var token = input.shift();
        if (token === undefined) {
          return list.pop();
        } else if (token === "(") {
          list.push(parenthesize(input, []));
          return parenthesize(input, list);
         } else if (token === ")") {
          return list;
        } else {
          return parenthesize(input, list.concat(categorize(token)));
        }
      }
     };

    在递归中,下一个词元是括号。parenthesize()的第9行用一个新的空数组递归创建一个新的空列表,进入新的递归,这时input['x', ')', 'x', ')', '"Lisp"', ')']。第14行运行,token被设置成x,这样创建了一个新的对象,其值为x,类型为identifier,然后将这个对象加入到list中,然后接着递归。

    在递归中,下一个词元是右括号,第12行运行,返回完成了的list[{ type: 'identifier', value: 'x' }]

    parenthesize()继续递归直到它处理完全部的输入词元,最后返回由包含了类型信息的原子所组成的嵌套数组。

    parse()tokenize()parenthesize()的组合调用:

    1
    2
    3
    var parse = function(input) {
      return parenthesize(tokenize(input));
    };

    如果原始的输入给的是((lambda (x) x) "Lisp"),则分析器给出的最后输出是:

    1
    2
    3
    [[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }],
      { type: 'identifier', value: 'x' }],
     { type: 'literal', value: 'Lisp' }]

     

    解释器

    在分析结束后,解释就开始了。

    interpret()接收parse()的输出并执行它。提供上例中的输出,interpret()会构造一个lambda表达式,然后用"Lisp"作为参数调用它。lambda调用会返回"Lisp",这就是整个程序的输出。

    除了要执行的输入外,interpret()还接收一个执行上下文。执行上下文是变量和变量对应的值所存储的地方。当一段Lisp代码被interpret()执行时,执行上下文包含着这段代码可访问的变量。

    这些变量是分层存储的。当前作用域的的变量处在最底层,在包含域中的变量处在上一层,包含域的上一层包含域中的变量处于更上层,依次类推。例如,在下面的代码中:

    1
    2
    3
    4
    5
    ((lambda (a)
      ((lambda (b)
        (b a))
       "b"))
     "a")

    第3行,执行上下文有两个活动的作用域。内层的lambda形成了当前作用域。外层的lambda形成了包含作用域。当前作用域中b被绑定到"b",包含作用域中a被绑定到"a"。当第3行运行时,解释器尝试在作用域中去查找b,它检查当前作用域,发现了b并返回它的值。还是在第3行上,解释器尝试去查找a,它检查当前作用域,结果没找到a,所以它尝试去包含域找,在那里它找到了a并返回它的值。

    在Little Lisp中,执行上下文用一个对象来表示,这个对象通过调用Context构造函数来生成。这个函数接受scope参数,即一个由在当前作用域中的变量和值组成的对象;还接受parent参数,如果parentundefined,作用域即位于顶层,或者说是全局的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Context = function(scope, parent) {
       this.scope = scope;
       this.parent = parent;
     
       this.get = function(identifier) {
         if (identifier in this.scope) {
           return this.scope[identifier];
         } else if (this.parent !== undefined) {
           return this.parent.get(identifier);
        }
      };
    };

    我们已看到((lambda (x) x) "Lisp")是如何被分析的,现在让我们看看分析过后的代码是如何被执行的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var interpret = function(input, context) {
     if (context === undefined) {
       return interpret(input, new Context(library));
       } else if (input instanceof Array) {
      return interpretList(input, context);
     } else if (input.type === "identifier") {
      return context.get(input.value);
     } else {
       return input.value;
     }
    };

    interpret()第一次被调用时,contextundefined,第2-3行运行,创建一个执行上下文。

    当初始上下文被实例化时,构造函数接受了一个叫library的对象。这个对象包含了内建在语言中的函数:firstrestprint。这些函数是用JavaScript写的。

    interpret()用原始的输入和新的上下文进行递归。

    input包含了上节中例子产生的输出:

    1
    2
    3
    [[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }],
      { type: 'identifier', value: 'x' }],
     { type: 'literal', value: 'Lisp' }]

    因为input是数组而且context已定义,第4-5行运行,interpretList()被调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var interpretList = function(input, context) {
         if (input[0].value in special) {
           return special[input[0].value](input, context);
         } else {
           var list = input.map(function(x) { return interpret(x, context); });
           if (list[0] instanceof Function) {
             return list[0].apply(undefined, list.slice(1));
           } else {
             return list;
          }
        }
    };

    interpretList()中,第5行遍历input数组,对每个元素调用interpret()。当interpret()在lambda定义上调用时,interpretList()再一次被调用。这次,interpretList()input参数为:

    1
    2
    [{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }],
      { type: 'identifier', value: 'x' }]

    interpretList()的第3行被调用,因为数组的第一个元素lambda是特殊形式。lambda()被调用来创建lambda函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var special = {
         lambda: function(input, context) {
           return function() {
             var lambdaArguments = arguments;
             var lambdaScope = input[1].reduce(function(acc, x, i) {
               acc[x.value] = lambdaArguments[i];
               return acc;
             }, {});
     
            return interpret(input[2], new Context(lambdaScope, context));
          };
        }
    };

    special.lambda()接受input中定义lambda的部分,返回一个函数,当这个函数被调用时,会对一些参数调用这个lambda函数。

    第3行开始lambda调用函数的定义。第4行保存了传递给lambda调用的参数。第5行开始为lambda调用创建一个新的作用域,收集input中定义lambda的参数的部分: [{ type: 'identifier', value: 'x' }],针对input中的每一个lambda形参和传递给lambda的对应实参,往lambda作用域中添加一个键值对。第10行对lambda的主体调用interpret(){ type: 'identifier', value: 'x' }。它传递给的lambda上下文包含lambda的作用域和父上下文。

    lambda现在就变成了被special.lambda()返回的函数。

    interpretList() 继续遍历input数组,对列表的第二个元素调用interpret():字符串"Lisp"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var interpret = function(input, context) {
         if (context === undefined) {
           return interpret(input, new Context(library));
         } else if (input instanceof Array) {
           return interpretList(input, context);
         } else if (input.type === "identifier") {
           return context.get(input.value);
         } else {
           return input.value;
        }
    };

    interpret()的第9行运行,这行做的事情仅仅是返回字面量对象的value属性'Lisp'interpretList()的第5行的map操作至此完成。list成为:

    1
    2
    [function(args) { /* code to invoke lambda */ },
     'Lisp']

    interpretList()的第6行运行,发现List的第一个元素是一个Javascript函数,这意味着list是一个函数调用。第7行运行,调用lambda函数,并将list的剩余部分作为参数传递。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var interpretList = function(input, context) {
         if (input[0].value in special) {
           return special[input[0].value](input, context);
         } else {
           var list = input.map(function(x) { return interpret(x, context); });
           if (list[0] instanceof Function) {
             return list[0].apply(undefined, list.slice(1));
           } else {
             return list;
          }
        }
    };

    在lambda调用函数中,第8行对lambda主体调用interpret(){ type: 'identifier', value: 'x' }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function() {
         var lambdaArguments = arguments;
         var lambdaScope = input[1].reduce(function(acc, x, i) {
           acc[x.value] = lambdaArguments[i];
           return acc;
         }, {});
     
         return interpret(input[2], new Context(lambdaScope, context));
    };

    interpret()的第6行发现input是一个标识符类型的原子,第7行去上下文里查找标识符x,返回'Lisp'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var interpret = function(input, context) {
         if (context === undefined) {
           return interpret(input, new Context(library));
         } else if (input instanceof Array) {
           return interpretList(input, context);
         } else if (input.type === "identifier") {
           return context.get(input.value);
         } else {
           return input.value;
        }
    };

    'Lisp'被lambda调用函数返回,接着被interpretList()返回,接着被interpret()返回,就是这样。

    全部的代码见GitHub repository。还可以看看lis.py,一个优秀而简单的Scheme解释器,由Peter Norvig用Python编写。

  • 相关阅读:
    MySQL面试题-基础
    IDEA 创建Spring项目后org.springframework.boot报错
    IDEA上传项目到SVN
    解决:idea中右键项目找不到subversion
    关于fromdata的上传文件问题
    PHP清除数组中为0的元素
    PHP删除数组中重复的元素
    PHP代码篇(六)--如何根据邀请人id查询满足条件的会员上级
    PHP对URL进行字符串编码
    PHP时间戳相互转换
  • 原文地址:https://www.cnblogs.com/daichangya/p/12959242.html
Copyright © 2020-2023  润新知