From a70e5fbc303860b4f9735875afb947a7ee16877e Mon Sep 17 00:00:00 2001 From: 0264408 Date: Fri, 26 Jun 2026 14:37:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20try-catch-finally=20=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 语法: 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 --- aster-core/src/analysis.rs | 13 ++ aster-core/src/ast/stmt.rs | 11 ++ aster-core/src/lexer/lexer.rs | 4 + aster-core/src/lexer/token.rs | 4 + aster-core/src/parser/parser.rs | 41 ++++- aster-core/src/runtime/mod.rs | 16 +- aster-core/src/vm/compiler.rs | 89 +++++++++- aster-core/src/vm/opcode.rs | 5 +- aster-core/src/vm/tests.rs | 302 ++++++++++++++++++++++++++++++++ aster-core/src/vm/vm.rs | 147 ++++++++++++---- aster-lsp/src/completion.rs | 1 + 11 files changed, 595 insertions(+), 38 deletions(-) diff --git a/aster-core/src/analysis.rs b/aster-core/src/analysis.rs index 35dc398..c6f2a20 100644 --- a/aster-core/src/analysis.rs +++ b/aster-core/src/analysis.rs @@ -102,6 +102,19 @@ fn collect_stmt(stmt: &Stmt, symbols: &mut Vec) { 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), } } diff --git a/aster-core/src/ast/stmt.rs b/aster-core/src/ast/stmt.rs index 87b3fb1..b5c9a5e 100644 --- a/aster-core/src/ast/stmt.rs +++ b/aster-core/src/ast/stmt.rs @@ -58,4 +58,15 @@ pub enum Stmt { /// continue Continue, + + /// try { body } catch (e) { catch_body } finally { finally_body } + Try { + body: Vec, + catch_var: Option, + catch_body: Option>, + finally_body: Option>, + }, + + /// throw expr; + Throw(Expr), } diff --git a/aster-core/src/lexer/lexer.rs b/aster-core/src/lexer/lexer.rs index a3a4042..2678c1d 100644 --- a/aster-core/src/lexer/lexer.rs +++ b/aster-core/src/lexer/lexer.rs @@ -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), }; diff --git a/aster-core/src/lexer/token.rs b/aster-core/src/lexer/token.rs index 0ea8c8f..1a7aa9b 100644 --- a/aster-core/src/lexer/token.rs +++ b/aster-core/src/lexer/token.rs @@ -39,6 +39,10 @@ pub enum TokenKind { True, False, Nil, + Try, + Catch, + Finally, + Throw, EOF, } diff --git a/aster-core/src/parser/parser.rs b/aster-core/src/parser/parser.rs index e77519d..0a99d4e 100644 --- a/aster-core/src/parser/parser.rs +++ b/aster-core/src/parser/parser.rs @@ -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 { + 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 { + let expr = self.expression()?; + self.match_kind(&[TokenKind::Semicolon]); // 分号可选 + Ok(Stmt::Throw(expr)) + } + fn block(&mut self) -> Result, 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(); } diff --git a/aster-core/src/runtime/mod.rs b/aster-core/src/runtime/mod.rs index 5bf6bce..5055d85 100644 --- a/aster-core/src/runtime/mod.rs +++ b/aster-core/src/runtime/mod.rs @@ -24,8 +24,19 @@ pub struct FunctionProto { pub constants: Vec, pub protos: Vec>, 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, +} + +/// 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(), } } diff --git a/aster-core/src/vm/compiler.rs b/aster-core/src/vm/compiler.rs index fe7d748..cccd300 100644 --- a/aster-core/src/vm/compiler.rs +++ b/aster-core/src/vm/compiler.rs @@ -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>, + finally_body: Option<&Vec>, + ) -> 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 // ======================================================================== diff --git a/aster-core/src/vm/opcode.rs b/aster-core/src/vm/opcode.rs index 3f379ff..158eb5c 100644 --- a/aster-core/src/vm/opcode.rs +++ b/aster-core/src/vm/opcode.rs @@ -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 { match byte { - 0..=41 | 44..=45 => Some(unsafe { std::mem::transmute::(byte) }), + 0..=41 | 44..=46 => Some(unsafe { std::mem::transmute::(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 diff --git a/aster-core/src/vm/tests.rs b/aster-core/src/vm/tests.rs index 5b4bf79..9c69b97 100644 --- a/aster-core/src/vm/tests.rs +++ b/aster-core/src/vm/tests.rs @@ -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); +} diff --git a/aster-core/src/vm/vm.rs b/aster-core/src/vm/vm.rs index e0ed9f8..89640dd 100644 --- a/aster-core/src/vm/vm.rs +++ b/aster-core/src/vm/vm.rs @@ -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 to access code without holding self.frames borrow - let proto: Rc = 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 { + 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 to access code without holding self.frames borrow + let proto: Rc = 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 // ======================================================================== diff --git a/aster-lsp/src/completion.rs b/aster-lsp/src/completion.rs index 85fd7e1..0a75170 100644 --- a/aster-lsp/src/completion.rs +++ b/aster-lsp/src/completion.rs @@ -9,6 +9,7 @@ use lsp_types::{ const KEYWORDS: &[&str] = &[ "let", "const", "fn", "if", "else", "while", "for", "in", "break", "continue", "return", "true", "false", "nil", + "try", "catch", "finally", "throw", ]; const BUILTINS: &[&str] = &[