暗无天日

=============>DarkSun的个人博客

读:把 JSON 当编程语言执行——一个迷你解释器的构造过程

原文 做了一个有趣的技术实验:把 JSON 当编程语言用。你写一个 JSON 数组,里面每个对象带一个 type 属性标明"这句是干什么的",然后喂给一个 JavaScript 写的解释器,它就会按顺序执行并返回结果。

本质上,这是在 JSON 语法里塞了一个微型 Lisp 解释器。原文的副标题 "With a Lithp" 也暗示了这一点(Lithp = Lisp 的大舌头读法)。

"怎么用 JS 写解释器"的教程已经很多了。这篇有意思的地方是:它用 JSON 做了一个具体的、可运行的"代码即数据"实例。数据格式本身什么都没变,只是有人写了个解释器按特定方式去解读它,JSON 就突然能执行了。

解释器的架构是经典三段式:reader → evaluator → printer。代码用 JavaScript,逻辑直白,不写 JS 也能看懂。

Reader 和 Printer:最简单的两个组件

Reader 的任务是把用户输入转成解释器能处理的数据结构。因为已经定好了"输入就是 JSON",reader 只需要做一件事:调 =JSON.parse=,解析失败就当普通字符串原样返回。

function jread(v) {
  let ret = null;
  try {
    ret = JSON.parse(v);
  } catch (e) {
    ret = v;  // 解析失败就当字符串处理
  }
  return ret;
}

Printer 更简单:直接返回计算结果。因为假设解释器在命令行里跑,命令行自己会负责显示。

function jprint(v) {
  return v;
}

解释器的主体骨架就是把这两个组件串起来,遍历输入数组,逐条读取、求值、打印:

function interpreter(input, env) {
  let ret = null;
  for (let i = 0; i < input.length; ++i) {
    ret = jread(input[i]);
    ret = jeval(ret, env);   // 核心:求值
    ret = jprint(ret);
  }
  return ret;
}

Evaluator:类型标签分发

Evaluator 是整个解释器的核心。它的逻辑是:遇到"简单值"(数字、字符串、数组)直接返回;遇到"对象"就检查 type 属性,按类型标签分发到不同的处理逻辑。

function jeval(v, env) {
  // 原子值:直接返回(null 需单独判断,因为 typeof null === 'object')
  if (v === null || typeof v !== 'object' || v instanceof Array) {
    return v;
  }

  let ev = null;
  switch (v.type) {
    case 'num':
    case 'str':
    case 'vec':
      // 变量定义:先求值,再存入环境
      ev = jeval(v.value, env);
      if (v.name) {
        jupdate(v.name, ev, env);
      }
      break;
    case 'var':
      // 变量引用:从环境里取
      ev = jlook(v.name, env);
      if (typeof ev === 'object' && ev !== null && !(ev instanceof Array)) {
        return '<function ' + v.name + '>';
      }
      break;
    case 'fn':
      // 函数定义:捕获当前环境,存入变量
      ev = jfn(v.value, env);
      if (v.name) {
        jupdate(v.name, ev, env);
      }
      break;
    case 'call':
      // 函数调用:查找函数,传入参数求值
      ev = jcall(jlook(v.name, env), v.args, env);
      break;
    case 'if':
      // 条件:null 为假,非 null 为真
      ev = jif(v.cond, v.then, v.else, env);
      break;
    case 'while':
      // 循环:条件为真时重复执行
      ev = jloop(v.cond, v.body, env);
      break;
    default:
      ev = null;
      break;
  }
  return ev;
}

变量定义用的标签包括三种 *值的类型*(=num=、=str=、=vec=),和一个统一的 =var=。这样做的好处是 evaluator 可以在定义变量时顺手做类型检查,代价是标签列表会随类型增加而膨胀。

环境与作用域:用 __parent 链实现静态作用域

变量存在哪里?存在"环境"(environment)里。环境就是一个普通的 JS 对象,key 是变量名,value 是值。

function jlook(key, env) {
  let e = env;
  let r = undefined;
  while (r === undefined && e !== undefined) {
    r = e[key];
    if (r === undefined) e = e.__parent;  // 找不到才向父环境找
  }
  return r;
}

function jupdate(key, val, env) {
  let e = env;
  let r = undefined;
  while (r === undefined && e !== undefined) {
    r = e[key];
    if (r === undefined) e = e.__parent;  // 找到了就不再上移
  }
  if (e === undefined) e = env;  // 找不到就在当前环境创建
  e[key] = val;
  return val;
}

关键机制是 __parent 指针。每个环境对象可以有一个 __parent 指向父环境,查找变量时沿着这条链一路向上。这正是静态作用域(lexical scoping)的标准实现方式:函数定义时捕获的环境链决定了它运行时能看到哪些变量。

函数:闭包的本质就是捕获环境

函数的定义分两步:先用 fn 标签声明,值是一个包含 args=(参数列表)和 =body=(函数体)的对象。解释器收到 =fn 表达式后,调用 jfn 创建一个"函数对象",把当前环境挂到 __parent 上:

function jfn(v, env) {
  let f = {};
  f.env = { __parent: env };  // 捕获定义时的环境
  f.args = v.args;
  f.body = v.body;
  return f;
}

调用函数时(=call= 标签),=jcall= 创建一个新环境来存参数,然后把函数体里的表达式逐条求值:

function jcall(f, args, env) {
  let e = { __parent: f.env };  // 新环境链到函数定义时捕获的环境
  for (let i = 0; i < f.args.length; ++i) {
    e[f.args[i]] = jeval(args[i], env);  // 参数在调用环境中求值
  }
  let ret = null;
  for (let i = 0; i < f.body.length; ++i) {
    ret = jeval(f.body[i], e);  // 函数体在新环境中执行
  }
  return ret;
}

这就是闭包的本质:函数对象带着一个 __parent 指针,指向它定义时所在的环境。以后无论在哪里调用,变量查找都从自己环境开始,沿 __parent 链向上。JavaScript 的闭包、Lisp 的词法作用域,跟这是同一套机制。

条件与循环

条件表达式用 if 标签,=cond= 是条件,=then= 和 else 各是一个表达式数组。关于"真"和"假"的约定很简单:=null= 是假,任何非 null 值都是真。

function jif(cond, thenExpr, elseExpr, env) {
  let c = jeval(cond, env);
  let branch = (c === null) ? elseExpr : thenExpr;
  let ret = null;
  for (let i = 0; i < branch.length; ++i) {
    ret = jeval(branch[i], env);
  }
  return ret;
}

循环用 while 标签:只要条件不等于 =null=,就重复执行 body。

function jloop(cond, body, env) {
  let r = null;
  while (jeval(cond, env) !== null) {
    for (let i = 0; i < body.length; ++i) {
      r = jeval(body[i], env);
    }
  }
  return r;
}

算术与逻辑:设计原始操作时的取舍

JSON 语法不能直接写 "a + b" 这样的表达式,所以算术和逻辑运算也必须用类型标签(或内置函数)来实现。两种方案各有代价:

  • *做成类型标签*:运算行为不可被用户覆盖,但没法作为参数传给高阶函数
  • *做成内置函数*:可以传给高阶函数,但用户可能意外覆盖它们

原文选了类型标签方案(只展示了实现,没展开讨论)。

**算术运算**中,加减乘除各有语义设计。加法和乘法最直接,累加/累乘所有参数。减法和除法有趣一些:一个参数时取负/取倒数,多个参数时从左到右依次运算。

function jadd(args, env) {
  let r = 0;
  for (let i = 0; i < args.length; ++i) {
    r = r + jeval(args[i], env);
  }
  return r;
}

function jsub(args, env) {
  if (args.length === 1) {
    return -(jeval(args[0], env));
  }
  let r = jeval(args[0], env);
  for (let i = 1; i < args.length; ++i) {
    r = r - jeval(args[i], env);
  }
  return r;
}

function jmul(args, env) {
  let r = 1;
  for (let i = 0; i < args.length; ++i) {
    r = r * jeval(args[i], env);
  }
  return r;
}

function jdiv(args, env) {
  if (args.length === 1) {
    return 1 / jeval(args[0], env);
  }
  let r = jeval(args[0], env);
  for (let i = 1; i < args.length; ++i) {
    r = r / jeval(args[i], env);
  }
  return r;
}

**比较运算**(小于、大于等)按序检查参数:只要发现一对违反顺序的就立即返回 =null=,全部通过则返回 ="true"=。

function jlt(args, env) {
  let m = jeval(args[0], env);
  for (let i = 1; i < args.length; ++i) {
    let n = jeval(args[i], env);
    if (!(m < n)) return null;
    m = n;
  }
  return 'true';
}

function jgt(args, env) {
  let m = jeval(args[0], env);
  for (let i = 1; i < args.length; ++i) {
    let n = jeval(args[i], env);
    if (!(m > n)) return null;
    m = n;
  }
  return 'true';
}

**逻辑运算**也是短路求值,跟 JS 的语义一致:=and= 遇到 null 就停,=or= 遇到非 null 就停,=not= 把 null 和非 null 互换。

function jand(args, env) {
  let c = jeval(args[0], env);
  for (let i = 1; i < args.length && c !== null; ++i) {
    c = jeval(args[i], env);
  }
  return c;
}

function jor(args, env) {
  let c = jeval(args[0], env);
  for (let i = 1; i < args.length && c === null; ++i) {
    c = jeval(args[i], env);
  }
  return c;
}

function jnot(args, env) {
  return (jeval(args[0], env) === null) ? 'true' : null;
}

这些运算函数需要在 jeval 里注册为对应的类型标签。把以下 case 加入 jeval 的 switch:

// 以下代码会覆盖前面的 jeval 定义,补充算术和逻辑运算的 case
function jeval(v, env) {
  if (v === null || typeof v !== 'object' || v instanceof Array) {
    return v;
  }

  let ev = null;
  switch (v.type) {
    case 'num':
    case 'str':
    case 'vec':
      ev = jeval(v.value, env);
      if (v.name) {
        jupdate(v.name, ev, env);
      }
      break;
    case 'var':
      ev = jlook(v.name, env);
      if (typeof ev === 'object' && ev !== null && !(ev instanceof Array)) {
        return '<function ' + v.name + '>';
      }
      break;
    case 'fn':
      ev = jfn(v.value, env);
      if (v.name) {
        jupdate(v.name, ev, env);
      }
      break;
    case 'call':
      ev = jcall(jlook(v.name, env), v.args, env);
      break;
    case 'if':
      ev = jif(v.cond, v.then, v.else, env);
      break;
    case 'while':
      ev = jloop(v.cond, v.body, env);
      break;
    case 'add':
      ev = jadd(v.args, env);
      break;
    case 'sub':
      ev = jsub(v.args, env);
      break;
    case 'mul':
      ev = jmul(v.args, env);
      break;
    case 'div':
      ev = jdiv(v.args, env);
      break;
    case 'lt':
      ev = jlt(v.args, env);
      break;
    case 'gt':
      ev = jgt(v.args, env);
      break;
    case 'and':
      ev = jand(v.args, env);
      break;
    case 'or':
      ev = jor(v.args, env);
      break;
    case 'not':
      ev = jnot(v.args, env);
      break;
    default:
      ev = null;
      break;
  }
  return ev;
}

完整示例

把以上组件拼起来,用测试案例覆盖解释器的全部运算单元。

四则混合运算: 一个嵌套表达式覆盖加、减、乘、除四种运算,相当于 =(1+2)*3 - 4/2 = 7=。

console.log("(1+2)*3 - 4/2 =", interpreter([{ type: 'sub', args: [
  { type: 'mul', args: [{ type: 'add', args: [1, 2] }, 3] },
  { type: 'div', args: [4, 2] }
]}], {}));
(1+2)*3 - 4/2 = 7

比较和逻辑运算: 小于/大于按序检查,=and= 短路到 null=,=or 短路到非 null=,=not 互换真假。

// 比较运算
console.log("1<2<3 =", interpreter([{ type: 'lt', args: [1, 2, 3] }], {}));
console.log("1<3<2 =", interpreter([{ type: 'lt', args: [1, 3, 2] }], {}));
console.log("3>2>1 =", interpreter([{ type: 'gt', args: [3, 2, 1] }], {}));

// 逻辑运算
console.log("and(1,2,3) =", interpreter([{ type: 'and', args: [1, 2, 3] }], {}));
console.log("and(1,null,3) =", interpreter([{ type: 'and', args: [1, null, 3] }], {}));
console.log("or(null,null,42) =", interpreter([{ type: 'or', args: [null, null, 42] }], {}));
console.log("not(null) =", interpreter([{ type: 'not', args: [null] }], {}));
console.log("not(1) =", interpreter([{ type: 'not', args: [1] }], {}));
1<2<3 = true
1<3<2 = null
3>2>1 = true
and(1,2,3) = 3
and(1,null,3) = null
or(null,null,42) = 42
not(null) = true
not(1) = null

条件表达式: null 为假走 else=,非 =null 为真走 =then=。

// 条件
console.log("if(null) =", interpreter([
  { type: 'if', cond: null,
    then: [{ type: 'str', value: 'yes' }],
    else: [{ type: 'str', value: 'no' }] }
], {}));
console.log("if(gt(42,0)) =", interpreter([
  { type: 'if', cond: { type: 'gt', args: [42, 0] },
    then: [{ type: 'str', value: 'positive' }],
    else: [{ type: 'str', value: 'non-positive' }] }
], {}));
if(null) = no
if(gt(42,0)) = positive

变量 + 循环:while 累加求和 =1+2+...+5=。

// 累加 1+2+...+5
let sumExample = [
  { type: 'num', name: 'i', value: 0 },
  { type: 'num', name: 'n', value: 5 },
  { type: 'num', name: 'sum', value: 0 },
  { type: 'while',
    cond: { type: 'lt', args: [{ type: 'var', name: 'i' }, { type: 'var', name: 'n' }] },
    body: [
      { type: 'num', name: 'i', value: { type: 'add', args: [{ type: 'var', name: 'i' }, 1] } },
      { type: 'num', name: 'sum', value: { type: 'add', args: [{ type: 'var', name: 'sum' }, { type: 'var', name: 'i' }] } },
    ]
  },
  { type: 'var', name: 'sum' }
];
console.log("1+2+...+5 =", interpreter(sumExample, {}));
1+2+...+5 = 15

函数 + 闭包: 定义一个阶乘函数 fact(5)=,内部用 =while 循环。

// 阶乘 fact(5) = 120
let factExample = [
  { type: 'fn', name: 'fact',
    value: { args: ['n'],
             body: [
               { type: 'num', name: 'result', value: 1 },
               { type: 'while',
                 cond: { type: 'gt', args: [{ type: 'var', name: 'n' }, 0] },
                 body: [
                   { type: 'num', name: 'result',
                     value: { type: 'mul', args: [{ type: 'var', name: 'result' }, { type: 'var', name: 'n' }] } },
                   { type: 'num', name: 'n',
                     value: { type: 'sub', args: [{ type: 'var', name: 'n' }, 1] } },
                 ]
               },
               { type: 'var', name: 'result' }
             ]
           }
  },
  { type: 'call', name: 'fact', args: [5] },
];
console.log("fact(5) =", interpreter(factExample, {}));
fact(5) = 120

字符串 + 数组: 字符串变量和数组变量都能正确定义和引用。

// 字符串 + 数组
console.log(interpreter([
  { type: 'str', name: 'msg', value: 'Hello World!' },
  { type: 'fn', name: 'greet', value: { args: [], body: [{ type: 'var', name: 'msg' }] } },
  { type: 'call', name: 'greet', args: [] },
], {}));
console.log(interpreter([
  { type: 'vec', name: 'xs', value: [1, 2, 3] },
  { type: 'var', name: 'xs' }
], {}));
Hello World!
[ 1, 2, 3 ]

为什么说它是"微型 Lisp"

回头看这个解释器的结构,它跟 Lisp 解释器的相似之处不是巧合:

  • Lisp 的 S-expression 是 =(fn arg1 arg2...)=,这里的 JSON 表达是 ={type: "fn", args: [...], body: [...]}=。本质相同,只是穿了一层 JSON 的外衣
  • Lisp 用 car/cdr 取列表头和尾,这里用 v.type/v.name/v.value 取对象属性
  • Lisp 的词法作用域通过环境链实现,这里用 __parent 指针,机制一模一样
  • Lisp 的 evalapply 循环,对应这里的 jevaljcall

原文把 Lisp 写成了"Lithp",大舌头发不准 s 音的梗。但玩笑归玩笑,这确实是一个"代码即数据"的微型演示:JSON 既是数据格式,又因为解释器的存在变成了可执行的语言。格式本身没有变,变的是解读它的方式。

如何自己跑一遍

文中所有代码块都设置了 :tangle json-interpreter.js=。如果你在用 Emacs 读这篇博文,在任意一个代码块里按 =C-c C-v t=,Org-mode 会把所有代码块拼接成一个完整的 =json-interpreter.js 文件。

如果不用 Emacs,可以直接下载我拼好的完整文件 json-interpreter.js(跟这篇博文在同一个目录下),然后:

node json-interpreter.js
Hello World!

你也可以改 helloworld 数组里的内容,试试文中的其他表达式(=if=、=while= 等)。

编程之旅 : interpreter : json : javascript