错误机制:让报错说人话
目标
没有错误处理的语言,一出错就崩溃得莫名其妙。本节课给解释器加两类错误:解析错误(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,本质是替未来的使用者省下几小时的抓瞎。好语言的标准之一,就是它出错时说的每句话都有用。