错误机制:让报错说人话

目标

没有错误处理的语言,一出错就崩溃得莫名其妙。本节课给解释器加两类错误:解析错误(ParseError)与运行时错误(RuntimeError),让报错带上信息,方便调试。下面给出完整可运行程序。

运行环境:Node.js v18+。运行方式:保存为 errors.js,执行 node errors.js(复用 data.js 解释器,增加错误类与除法检查)。

// errors.js —— 完整可运行:带错误处理的解释器
class ParseError extends Error {}
class RuntimeError extends Error {}

// 在 ev 的 binary 除法分支主动抛运行时错误:
//   case 'binary': {
//     const a = ev(node.left, env), b = ev(node.right, env)
//     if (node.op === '/') { if (b === 0) throw new RuntimeError('除以零:' + a + ' / 0') }
//     if (node.op === '+' && (typeof a === 'string' || typeof b === 'string')) return String(a) + String(b)
//     switch (node.op) { case '+': return a + b; ... }
//   }
// 其余 interpreter(tokenize/parse/Environment/ev/run)与 data.js 相同,此处省略重复。

function run(src) {
  let ast
  try { ast = parse(src) }                                     // 解析阶段出错 -> ParseError
  catch (e) { throw new ParseError('解析失败:' + e.message) }
  const env = new Environment()
  env.define('print', (...xs) => console.log(...xs))
  env.define('len', x => x.length)
  try {
    for (const s of ast) ev(s, env)                             // 执行阶段出错 -> RuntimeError
  } catch (e) {
    if (e instanceof RuntimeError) throw e
    throw new RuntimeError('运行时错误:' + e.message)
  }
}

// ===== 调用示例 =====
try {
  run('print(10 / 0);')
} catch (e) {
  console.log('捕获到:', e.constructor.name, '->', e.message)
}
// 输出:捕获到: RuntimeError -> 除以零:10 / 0

把错误分成"解析"和"运行"两个阶段,用户能立刻知道是"代码写错了"还是"逻辑跑挂了"——这是所有成熟语言的标配。

名词解释

ParseError(解析错误):代码还没开始跑、在"变成 AST"那一步就失败了,比如少个括号、关键字拼错。它属于语法层面。
RuntimeError(运行时错误):语法没问题、跑的过程中才爆发,比如除以零、访问未定义变量。它属于语义/执行层面。
栈追踪(stack trace):错误发生时记录"从哪调用到哪"的调用链。我们这里简化了,真实语言会打印它帮你定位出错行。

课后练习

练习:在 ev 的 ident 分支,把"未定义变量"也从通用 Error 改成抛 RuntimeError。

答案:

case 'ident': {
  if (!env.has(node.name)) throw new RuntimeError('未定义变量: ' + node.name)
  return env.get(node.name)
}
// 需要在 Environment 上加 has()(见 l4 课后练习)

总结

错误处理不是"锦上添花",而是语言能不能被人用的分水岭。没有清晰错误信息的语言,调试全靠猜。把错误分阶段(Parse vs Runtime)是第一个重要设计:它告诉用户"错在写法和错在逻辑"是两回事,排错路径完全不同。第二个设计是"报错带上下文"——除以零时告诉你是 10 / 0 而不是只说"error"。你写的这两行 throw,本质是替未来的使用者省下几小时的抓瞎。好语言的标准之一,就是它出错时说的每句话都有用。