REPL:把前面所有拼成可交互解释器

目标

把前 10 节串起来:做一个 REPL(Read-Eval-Print Loop)——读一行、执行、打印结果、再读下一行。这是你亲手写的"迷你 Node 终端"。下面给出完整可运行程序。

运行环境:Node.js v18+。运行方式:保存为 repl.js,执行 node repl.js。真实 REPL 用 readline 从终端逐行读;为便于教学,这里用一个"行数组"模拟用户输入,逻辑完全一致。

// repl.js —— 完整可运行:复用前几节的 parse/ev/Environment/标准库
function createGlobalEnv() {
  const env = new Environment()
  env.define('print', (...xs) => console.log('»', ...xs))
  env.define('len', x => x.length)
  env.define('map', (a, fn) => a.map(e => fn(e)))
  return env
}

// 执行一段源码(可包含多条语句),返回最后一条表达式的值
function run(src, env) {
  const ast = parse(src)
  let last
  for (const s of ast) last = ev(s, env)
  return last
}

// REPL 主循环:读一行 -> 求值 -> 打印
function repl(lines) {
  const env = createGlobalEnv()
  for (const line of lines) {
    if (line.trim() === '') continue
    if (line.trim() === ':q') { console.log('bye'); return }
    try {
      const v = run(line, env)
      if (v !== undefined) console.log('=>', v)
    } catch (e) {
      console.log('错误:', e.message)
    }
  }
}

// ===== 调用示例(模拟用户在终端逐行输入)=====
repl([
  'var x = 10;',
  'x = x * x;',
  'print(x);',
  'print(len("hi"));',
  ':q',
])

真实场景里,把 repl(['...']) 换成 readline 的逐行回调即可——run 和 ev 完全不变。这就是"语言内核"与"交互外壳"解耦的好处。

名词解释

REPL:Read(读)– Eval(求值)– Print(打印)– Loop(循环)的缩写。它是很多动态语言(Python、Node、Lua)的交互式入口,边写边看结果。
交互式 vs 脚本式:REPL 是交互式(一行一行来,保留状态);把代码写进文件执行是脚本式。二者共用同一套 parse/ev,只差"输入从哪来"。
状态保持:REPL 的 env 在循环外创建,所以上一行定义的 x,下一行还能用——这正是"会话"的感觉。

课后练习

练习:改造 repl,让输入 :q 时退出循环(打印 "bye")。

答案:已在上方完整代码中实现——循环内判断 line.trim() === ':q' 时打印 bye 并 return。

总结

REPL 是这门小语言的高光时刻:它证明前 10 节不是零散练习,而是一套完整语言的零件。最值得学的设计是内核与外壳解耦——parse/ev 是语言本身,REPL 只是"怎么把输入喂给它"的一层皮。同一套内核,套上文件执行器就是脚本语言,套上浏览器就是网页脚本语言,套上 REPL 就是交互终端。你此刻拥有的,已经是一个能跑、能交互、能报错的迷你语言内核了。下一步往哪走,取决于你想让它更"快"(编译,见第12节)还是更"大"(更多语法)。