feat: try-catch-finally 异常处理

语法: try { ... } catch (e) { ... } finally { ... }
catch 和 finally 都是可选的,但至少需要一个。throw expr 抛出任意值。

- Lexer: 新增 try, catch, finally, throw 关键字
- AST: Stmt::Try { body, catch_var, catch_body, finally_body } + Stmt::Throw(Expr)
- Parser: try/catch/finally/throw 语句解析,更新 synchronize()
- Opcode: Throw = 46,FunctionProto 新增 exception_handlers 表
- ExceptionHandler: { try_start, try_end, catch_ip, catch_slot, finally_ip }
- Compiler: compile_try 将 finally 在成功/异常两条路径各内联一次
- VM: unwind() 栈展开 + find_handler() 处理器搜索
- RuntimeError(除零、未定义变量等)在 try 块内自动转为可捕获异常
- LSP: 补全关键字列表追加

已知限制: return/break/continue 在 try-finally 内部不会先执行 finally

16 个新测试,全部 343 个测试通过。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0264408
2026-06-26 14:37:38 +08:00
parent deab894864
commit a70e5fbc30
11 changed files with 595 additions and 38 deletions
+13
View File
@@ -102,6 +102,19 @@ fn collect_stmt(stmt: &Stmt, symbols: &mut Vec<Symbol>) {
collect_expr(expr, symbols);
}
Stmt::Return(None) | Stmt::Break | Stmt::Continue => {}
Stmt::Try { body, catch_var, catch_body, finally_body } => {
collect_stmts(body, symbols);
if let Some(var) = catch_var {
symbols.push(Symbol { name: var.clone(), kind: SymbolKind::Variable });
}
if let Some(cb) = catch_body {
collect_stmts(cb, symbols);
}
if let Some(fb) = finally_body {
collect_stmts(fb, symbols);
}
}
Stmt::Throw(expr) => collect_expr(expr, symbols),
}
}
+11
View File
@@ -58,4 +58,15 @@ pub enum Stmt {
/// continue
Continue,
/// try { body } catch (e) { catch_body } finally { finally_body }
Try {
body: Vec<Stmt>,
catch_var: Option<String>,
catch_body: Option<Vec<Stmt>>,
finally_body: Option<Vec<Stmt>>,
},
/// throw expr;
Throw(Expr),
}
+4
View File
@@ -360,6 +360,10 @@ impl Lexer {
"true" => TokenKind::True,
"false" => TokenKind::False,
"nil" => TokenKind::Nil,
"try" => TokenKind::Try,
"catch" => TokenKind::Catch,
"finally" => TokenKind::Finally,
"throw" => TokenKind::Throw,
_ => TokenKind::Identifier(s),
};
+4
View File
@@ -39,6 +39,10 @@ pub enum TokenKind {
True,
False,
Nil,
Try,
Catch,
Finally,
Throw,
EOF,
}
+40 -1
View File
@@ -54,6 +54,10 @@ impl Parser {
self.continue_statement()
} else if self.match_kind(&[TokenKind::Return]) {
self.return_statement()
} else if self.match_kind(&[TokenKind::Try]) {
self.try_statement()
} else if self.match_kind(&[TokenKind::Throw]) {
self.throw_statement()
} else if self.match_kind(&[TokenKind::LeftBrace]) {
Ok(Stmt::Block(self.block()?))
} else {
@@ -201,6 +205,39 @@ impl Parser {
Ok(Stmt::Return(value))
}
fn try_statement(&mut self) -> Result<Stmt, RuntimeError> {
self.consume(TokenKind::LeftBrace, "Expected '{' after 'try'.")?;
let body = self.block()?;
let mut catch_var = None;
let mut catch_body = None;
if self.match_kind(&[TokenKind::Catch]) {
self.consume(TokenKind::LeftParen, "Expected '(' after 'catch'.")?;
catch_var = Some(self.consume_ident("Expected exception variable.")?);
self.consume(TokenKind::RightParen, "Expected ')' after catch variable.")?;
self.consume(TokenKind::LeftBrace, "Expected '{' before catch body.")?;
catch_body = Some(self.block()?);
}
let mut finally_body = None;
if self.match_kind(&[TokenKind::Finally]) {
self.consume(TokenKind::LeftBrace, "Expected '{' after 'finally'.")?;
finally_body = Some(self.block()?);
}
if catch_var.is_none() && finally_body.is_none() {
return Err(RuntimeError::parse("Expected 'catch' or 'finally' after 'try'.", self.peek().clone()));
}
Ok(Stmt::Try { body, catch_var, catch_body, finally_body })
}
fn throw_statement(&mut self) -> Result<Stmt, RuntimeError> {
let expr = self.expression()?;
self.match_kind(&[TokenKind::Semicolon]); // 分号可选
Ok(Stmt::Throw(expr))
}
fn block(&mut self) -> Result<Vec<Stmt>, RuntimeError> {
let mut statements = Vec::new();
@@ -616,7 +653,9 @@ impl Parser {
| TokenKind::For
| TokenKind::Return
| TokenKind::Break
| TokenKind::Continue => return,
| TokenKind::Continue
| TokenKind::Try
| TokenKind::Throw => return,
_ => {
self.advance();
}
+14 -2
View File
@@ -24,8 +24,19 @@ pub struct FunctionProto {
pub constants: Vec<Value>,
pub protos: Vec<Rc<FunctionProto>>,
pub upvalue_count: u8,
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
pub exception_handlers: Vec<ExceptionHandler>,
}
/// Exception handler entry: defines a try-catch-finally region.
#[derive(Debug, Clone)]
pub struct ExceptionHandler {
pub try_start: usize, // inclusive
pub try_end: usize, // exclusive
pub catch_ip: usize, // catch handler IP (0 if no catch clause)
pub catch_slot: u8, // local slot for catch variable (0 if no catch)
pub finally_ip: usize, // finally rethrow entry (0 if no finally)
}
impl FunctionProto {
@@ -39,6 +50,7 @@ impl FunctionProto {
upvalue_count: 0,
upvalues: Vec::new(),
lines: Vec::new(),
exception_handlers: Vec::new(),
}
}
+88 -1
View File
@@ -7,7 +7,7 @@
use crate::ast::*;
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
use crate::error::RuntimeError;
use crate::runtime::{Value, FunctionProto};
use crate::runtime::{Value, FunctionProto, ExceptionHandler};
use super::opcode::*;
use std::rc::Rc;
@@ -139,6 +139,12 @@ impl Compiler {
Stmt::Continue => {
self.compile_continue()?;
}
Stmt::Try { body, catch_var, catch_body, finally_body } => {
self.compile_try(body, catch_var.as_deref(), catch_body.as_ref(), finally_body.as_ref())?;
}
Stmt::Throw(expr) => {
self.compile_throw(expr)?;
}
}
Ok(())
}
@@ -375,6 +381,87 @@ impl Compiler {
Ok(())
}
fn compile_throw(&mut self, expr: &Expr) -> Result<(), RuntimeError> {
self.compile_expr(expr)?;
self.emit_op(OpCode::Throw);
Ok(())
}
fn compile_try(
&mut self,
body: &[Stmt],
catch_var: Option<&str>,
catch_body: Option<&Vec<Stmt>>,
finally_body: Option<&Vec<Stmt>>,
) -> Result<(), RuntimeError> {
let try_start = self.function.code.len();
// Compile try body
self.begin_scope();
self.compile_stmts(body)?;
self.end_scope();
let try_end = self.function.code.len();
// ── Normal path: jump past catch to finally (or end) ──
let skip_catch = self.emit_jump(OpCode::Jump);
// ── Catch handler (entry via unwind) ──
let (catch_ip, catch_slot) = if let (Some(var), Some(cb)) = (catch_var, catch_body) {
let ip = self.function.code.len();
self.begin_scope();
let slot = self.locals.len() as u8;
self.locals.push(Local {
name: var.to_string(),
depth: self.scope_depth,
is_captured: false,
is_const: false,
});
// VM pushes exception value before jumping here; StoreLocal peeks it
emit_u8(&mut self.function.code, OpCode::StoreLocal, slot);
self.compile_stmts(cb)?;
self.end_scope();
(ip, slot)
} else {
(0, 0)
};
self.patch_jump(skip_catch);
// ── Finally: inline for success paths (try/catch fall through here) ──
let finally_block_ip = if let Some(fb) = finally_body {
let ip = self.function.code.len();
self.begin_scope();
self.compile_stmts(fb)?;
self.end_scope();
let done_jump = self.emit_jump(OpCode::Jump);
// ── Finally rethrow entry (for exception path) ──
let rethrow_ip = self.function.code.len();
// Duplicate finally body for exception path
self.begin_scope();
self.compile_stmts(fb)?;
self.end_scope();
self.emit_op(OpCode::Throw); // re-throw after finally
self.patch_jump(done_jump);
(ip, rethrow_ip)
} else {
(0, 0)
};
let (_, rethrow_ip) = finally_block_ip;
// Register exception handler
self.function.exception_handlers.push(ExceptionHandler {
try_start,
try_end,
catch_ip,
catch_slot,
finally_ip: rethrow_ip,
});
Ok(())
}
// ========================================================================
// Expression compilation
// ========================================================================
+3 -2
View File
@@ -80,12 +80,13 @@ pub enum OpCode {
CompoundAssignIndex = 41, // operand: u8 compound-op tag
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
CompoundAssignGlobal = 45, // operand: u16 name idx + u8 compound-op tag
Throw = 46, // pops exception value, begins stack unwind
}
impl OpCode {
pub fn from_u8(byte: u8) -> Option<Self> {
match byte {
0..=41 | 44..=45 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
0..=41 | 44..=46 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
42 => Some(OpCode::LoadUpvalue),
43 => Some(OpCode::StoreUpvalue),
_ => None,
@@ -93,7 +94,7 @@ impl OpCode {
}
/// Total number of distinct opcodes
pub const COUNT: usize = 46;
pub const COUNT: usize = 47;
}
/// Compound assignment operator tags (used as operand byte after
+302
View File
@@ -1737,3 +1737,305 @@ fn eval_require_empty_module() {
v => panic!("Expected Object, got {:?}", v),
}
}
// =========================================================================
// Exception handling: throw
// =========================================================================
#[test]
fn eval_throw_caught() {
let vm = run("
let result = 'none';
try {
throw 'caught!';
} catch (e) {
result = e;
}
");
match get_var(&vm, "result") {
Value::String(s) => assert_eq!(s, "caught!"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_throw_preserves_value_type() {
let vm = run("
let msg = '';
let code = 0;
try { throw 'error'; } catch (e) { msg = e; }
try { throw 42; } catch (e) { code = e; }
");
match get_var(&vm, "msg") {
Value::String(s) => assert_eq!(s, "error"),
v => panic!("Expected String, got {:?}", v),
}
assert_num(&get_var(&vm, "code"), 42.0);
}
#[test]
fn eval_throw_caught_correct_handler() {
let vm = run("
let outer = 'none';
let inner = 'none';
try {
try {
throw 'inner';
} catch (e) {
inner = e;
}
} catch (e) {
outer = e;
}
");
match get_var(&vm, "inner") {
Value::String(s) => assert_eq!(s, "inner"),
v => panic!("Expected String, got {:?}", v),
}
match get_var(&vm, "outer") {
Value::String(s) => assert_eq!(s, "none"),
v => panic!("Expected String, got {:?}", v),
}
}
// =========================================================================
// Exception handling: runtime errors caught
// =========================================================================
#[test]
fn eval_catch_division_by_zero() {
let vm = run("
let msg = 'none';
try {
let x = 1 / 0;
} catch (e) {
msg = e;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert!(s.contains("Division by zero") || s.contains("division"), "got: {}", s),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_catch_undefined_variable() {
let vm = run("
let msg = 'none';
try {
let x = no_such_var;
} catch (e) {
msg = e;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert!(s.contains("Undefined"), "got: {}", s),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_catch_index_out_of_bounds() {
let vm = run("
let msg = 'none';
try {
let x = [1, 2][99];
} catch (e) {
msg = e;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert!(s.contains("out of bounds"), "got: {}", s),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_catch_call_non_function() {
let vm = run("
let msg = 'none';
try {
42();
} catch (e) {
msg = e;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert!(s.contains("non-function"), "got: {}", s),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_no_error_runs_normally() {
let vm = run("
let result = 0;
try {
result = 42;
} catch (e) {
result = -1;
}
");
assert_num(&get_var(&vm, "result"), 42.0);
}
// =========================================================================
// Exception handling: finally
// =========================================================================
#[test]
fn eval_finally_runs_on_success() {
let vm = run("
let flag = 0;
try {
flag = 1;
} finally {
flag = flag + 10;
}
");
assert_num(&get_var(&vm, "flag"), 11.0);
}
#[test]
fn eval_finally_runs_on_throw() {
let vm = run("
let flag = 0;
let msg = 'none';
try {
throw 'oops';
} catch (e) {
msg = e;
} finally {
flag = flag + 10;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert_eq!(s, "oops"),
v => panic!("Expected String, got {:?}", v),
}
assert_num(&get_var(&vm, "flag"), 10.0);
}
#[test]
fn eval_finally_runs_on_runtime_error() {
let vm = run("
let flag = 0;
let msg = 'none';
try {
let x = 1 / 0;
} catch (e) {
msg = 'caught';
} finally {
flag = 1;
}
");
match get_var(&vm, "msg") {
Value::String(s) => assert_eq!(s, "caught"),
v => panic!("Expected String, got {:?}", v),
}
assert_num(&get_var(&vm, "flag"), 1.0);
}
#[test]
fn eval_finally_without_catch() {
// try-finally without catch: finally runs, then rethrows
let result = std::panic::catch_unwind(|| {
run("
let flag = 0;
try {
throw 'boom';
} finally {
flag = 1;
}
");
});
assert!(result.is_err(), "Expected uncaught exception");
}
#[test]
fn eval_nested_try_catch() {
let vm = run("
let outer = 'none';
let inner = 'none';
try {
try {
throw 'inner_error';
} catch (e) {
inner = e;
}
} catch (e) {
outer = e;
}
");
match get_var(&vm, "inner") {
Value::String(s) => assert_eq!(s, "inner_error"),
v => panic!("Expected String, got {:?}", v),
}
match get_var(&vm, "outer") {
Value::String(s) => assert_eq!(s, "none"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_throw_in_catch_propagates() {
let vm = run("
let outer = 'none';
let inner = 'none';
try {
try {
throw 'first';
} catch (e) {
inner = e;
throw 'second';
}
} catch (e) {
outer = e;
}
");
match get_var(&vm, "inner") {
Value::String(s) => assert_eq!(s, "first"),
v => panic!("Expected String, got {:?}", v),
}
match get_var(&vm, "outer") {
Value::String(s) => assert_eq!(s, "second"),
v => panic!("Expected String, got {:?}", v),
}
}
#[test]
fn eval_try_catch_in_function() {
let vm = run("
fn safe_div(a, b) {
try {
return a / b;
} catch (e) {
return -1;
}
}
let r1 = safe_div(10, 2);
let r2 = safe_div(10, 0);
");
assert_num(&get_var(&vm, "r1"), 5.0);
assert_num(&get_var(&vm, "r2"), -1.0);
}
#[test]
fn eval_finally_known_limitation_return() {
// Note: return/break/continue inside try-finally do NOT yet run finally
// (this is a known limitation). This test documents current behavior.
let vm = run("
let flag = 0;
fn test() {
try {
return 42;
} finally {
flag = 1;
}
}
let r = test();
");
assert_num(&get_var(&vm, "r"), 42.0);
// flag stays 0 because finally doesn't run before return (known limitation)
assert_num(&get_var(&vm, "flag"), 0.0);
}
+115 -32
View File
@@ -4,7 +4,7 @@
//! call frames. Implements the `Runtime` trait for native function support.
use crate::error::RuntimeError;
use crate::runtime::{Value, Runtime, FunctionProto, Closure, UpvalueObj};
use crate::runtime::{Value, Runtime, FunctionProto, Closure, UpvalueObj, ExceptionHandler};
use crate::lexer::Lexer;
use crate::parser::Parser;
use super::opcode::*;
@@ -109,41 +109,59 @@ impl Vm {
fn execute_loop(&mut self) -> Result<(), RuntimeError> {
loop {
if self.frames.is_empty() {
return Ok(());
}
// Snapshot frame state (must drop borrow before mutating self)
let ip = self.frames.last().unwrap().ip;
let code_len = self.frames.last().unwrap().closure.proto.code.len();
if ip >= code_len {
// End of function — implicit return nil
let frame = self.frames.pop().unwrap();
self.close_upvalues(frame.stack_base);
if self.frames.is_empty() {
return Ok(());
match self.execute_one_step() {
Ok(true) => continue, // more instructions to execute
Ok(false) => return Ok(()), // VM finished
Err(runtime_err) => {
// Try to handle as exception
let exc = Value::String(runtime_err.to_string());
self.unwind(exc)?; // Err if no handler found
continue; // Handler found, continue executing
}
// Remove callee + args + function locals, push nil result
self.stack.truncate(frame.stack_base.saturating_sub(1));
self.stack.push(Value::Nil);
continue;
}
}
}
// Clone Rc<FunctionProto> to access code without holding self.frames borrow
let proto: Rc<FunctionProto> = Rc::clone(&self.frames.last().unwrap().closure.proto);
/// Execute a single bytecode instruction. Returns:
/// - Ok(true): continue the loop
/// - Ok(false): VM is done (frames empty)
/// - Err(...): runtime error to potentially catch
fn execute_one_step(&mut self) -> Result<bool, RuntimeError> {
if self.frames.is_empty() {
return Ok(false);
}
// Read opcode byte
let op = OpCode::from_u8(proto.code[ip])
.ok_or_else(|| RuntimeError::RuntimeError {
message: format!("Unknown opcode: {}", proto.code[ip]),
token: None,
})?;
// Snapshot frame state (must drop borrow before mutating self)
let ip = self.frames.last().unwrap().ip;
let code_len = self.frames.last().unwrap().closure.proto.code.len();
// Local reference to code (borrows from proto, not self)
let code = &proto.code;
if ip >= code_len {
// End of function — implicit return nil
let frame = self.frames.pop().unwrap();
self.close_upvalues(frame.stack_base);
if self.frames.is_empty() {
return Ok(false);
}
// Remove callee + args + function locals, push nil result
self.stack.truncate(frame.stack_base.saturating_sub(1));
self.stack.push(Value::Nil);
return Ok(true);
}
match op {
// Clone Rc<FunctionProto> to access code without holding self.frames borrow
let proto: Rc<FunctionProto> = Rc::clone(&self.frames.last().unwrap().closure.proto);
// Read opcode byte
let op = OpCode::from_u8(proto.code[ip])
.ok_or_else(|| RuntimeError::RuntimeError {
message: format!("Unknown opcode: {}", proto.code[ip]),
token: None,
})?;
// Local reference to code (borrows from proto, not self)
let code = &proto.code;
match op {
OpCode::Pop => {
self.stack.pop();
self.advance_ip(SIZE_OP);
@@ -445,7 +463,7 @@ impl Vm {
self.stack.truncate(frame.stack_base.saturating_sub(1));
self.stack.push(result);
if self.frames.is_empty() {
return Ok(());
return Ok(false);
}
}
OpCode::Closure => {
@@ -645,8 +663,16 @@ impl Vm {
self.stack.push(result);
self.advance_ip(SIZE_U16_PLUS1);
}
}
OpCode::Throw => {
let exc = self.stack.pop().unwrap();
self.unwind(exc)?;
// unwind() returns Ok only if it found a handler and jumped there
return Ok(true);
}
}
// All ops (except Throw) fall through to here
Ok(true)
}
// ========================================================================
@@ -665,6 +691,63 @@ impl Vm {
self.frame_mut().ip = target;
}
// ========================================================================
// Exception handling (stack unwind)
// ========================================================================
/// Search for an exception handler covering the current IP.
/// Returns the handler and true if a handler was found.
fn find_handler(proto: &FunctionProto, ip: usize) -> Option<&ExceptionHandler> {
// Search in insertion order: innermost try is compiled (and registered) first,
// so forward iteration finds the innermost matching handler first.
proto.exception_handlers.iter().find(|h| ip >= h.try_start && ip < h.try_end)
}
/// Unwind the call stack to find an exception handler. On success,
/// the VM's IP is set to the handler and execution continues.
/// Returns Err if no handler is found (uncaught exception).
fn unwind(&mut self, exception: Value) -> Result<(), RuntimeError> {
loop {
if self.frames.is_empty() {
return Err(RuntimeError::RuntimeError {
message: format!("Uncaught exception: {}", exception),
token: None,
});
}
let ip = self.frames.last().unwrap().ip;
let proto = Rc::clone(&self.frames.last().unwrap().closure.proto);
if let Some(h) = Self::find_handler(&proto, ip) {
if h.finally_ip != 0 {
if h.catch_ip != 0 {
// Has catch: push exception, jump to catch.
// Catch body falls through to inline finally.
self.stack.push(exception);
self.frame_mut().ip = h.catch_ip;
return Ok(());
} else {
// No catch, only finally: push exception, jump to finally
// rethrow entry. The finally body runs, then Throw.
self.stack.push(exception);
self.frame_mut().ip = h.finally_ip;
return Ok(());
}
} else if h.catch_ip != 0 {
// Only catch, no finally
self.stack.push(exception);
self.frame_mut().ip = h.catch_ip;
return Ok(());
}
}
// No handler — pop frame and continue in caller
let frame = self.frames.pop().unwrap();
self.close_upvalues(frame.stack_base);
self.stack.truncate(frame.stack_base.saturating_sub(1));
}
}
// ========================================================================
// Function calls
// ========================================================================