Merge pull request 'try-catch' (#3) from try-catch into vm-bytecode
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
@@ -102,6 +102,19 @@ fn collect_stmt(stmt: &Stmt, symbols: &mut Vec<Symbol>) {
|
|||||||
collect_expr(expr, symbols);
|
collect_expr(expr, symbols);
|
||||||
}
|
}
|
||||||
Stmt::Return(None) | Stmt::Break | Stmt::Continue => {}
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,4 +58,15 @@ pub enum Stmt {
|
|||||||
|
|
||||||
/// continue
|
/// continue
|
||||||
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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,6 +360,10 @@ impl Lexer {
|
|||||||
"true" => TokenKind::True,
|
"true" => TokenKind::True,
|
||||||
"false" => TokenKind::False,
|
"false" => TokenKind::False,
|
||||||
"nil" => TokenKind::Nil,
|
"nil" => TokenKind::Nil,
|
||||||
|
"try" => TokenKind::Try,
|
||||||
|
"catch" => TokenKind::Catch,
|
||||||
|
"finally" => TokenKind::Finally,
|
||||||
|
"throw" => TokenKind::Throw,
|
||||||
_ => TokenKind::Identifier(s),
|
_ => TokenKind::Identifier(s),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ pub enum TokenKind {
|
|||||||
True,
|
True,
|
||||||
False,
|
False,
|
||||||
Nil,
|
Nil,
|
||||||
|
Try,
|
||||||
|
Catch,
|
||||||
|
Finally,
|
||||||
|
Throw,
|
||||||
|
|
||||||
EOF,
|
EOF,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ impl Parser {
|
|||||||
self.continue_statement()
|
self.continue_statement()
|
||||||
} else if self.match_kind(&[TokenKind::Return]) {
|
} else if self.match_kind(&[TokenKind::Return]) {
|
||||||
self.return_statement()
|
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]) {
|
} else if self.match_kind(&[TokenKind::LeftBrace]) {
|
||||||
Ok(Stmt::Block(self.block()?))
|
Ok(Stmt::Block(self.block()?))
|
||||||
} else {
|
} else {
|
||||||
@@ -201,6 +205,39 @@ impl Parser {
|
|||||||
Ok(Stmt::Return(value))
|
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> {
|
fn block(&mut self) -> Result<Vec<Stmt>, RuntimeError> {
|
||||||
let mut statements = Vec::new();
|
let mut statements = Vec::new();
|
||||||
|
|
||||||
@@ -616,7 +653,9 @@ impl Parser {
|
|||||||
| TokenKind::For
|
| TokenKind::For
|
||||||
| TokenKind::Return
|
| TokenKind::Return
|
||||||
| TokenKind::Break
|
| TokenKind::Break
|
||||||
| TokenKind::Continue => return,
|
| TokenKind::Continue
|
||||||
|
| TokenKind::Try
|
||||||
|
| TokenKind::Throw => return,
|
||||||
_ => {
|
_ => {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ pub struct FunctionProto {
|
|||||||
pub upvalue_count: u8,
|
pub upvalue_count: u8,
|
||||||
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
|
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
|
||||||
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
|
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 {
|
impl FunctionProto {
|
||||||
@@ -39,6 +50,7 @@ impl FunctionProto {
|
|||||||
upvalue_count: 0,
|
upvalue_count: 0,
|
||||||
upvalues: Vec::new(),
|
upvalues: Vec::new(),
|
||||||
lines: Vec::new(),
|
lines: Vec::new(),
|
||||||
|
exception_handlers: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
|
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
|
||||||
use crate::error::RuntimeError;
|
use crate::error::RuntimeError;
|
||||||
use crate::runtime::{Value, FunctionProto};
|
use crate::runtime::{Value, FunctionProto, ExceptionHandler};
|
||||||
use super::opcode::*;
|
use super::opcode::*;
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -139,6 +139,12 @@ impl Compiler {
|
|||||||
Stmt::Continue => {
|
Stmt::Continue => {
|
||||||
self.compile_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -375,6 +381,87 @@ impl Compiler {
|
|||||||
Ok(())
|
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
|
// Expression compilation
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ pub enum OpCode {
|
|||||||
CompoundAssignIndex = 41, // operand: u8 compound-op tag
|
CompoundAssignIndex = 41, // operand: u8 compound-op tag
|
||||||
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
|
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
|
||||||
CompoundAssignGlobal = 45, // operand: u16 name 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 {
|
impl OpCode {
|
||||||
pub fn from_u8(byte: u8) -> Option<Self> {
|
pub fn from_u8(byte: u8) -> Option<Self> {
|
||||||
match byte {
|
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),
|
42 => Some(OpCode::LoadUpvalue),
|
||||||
43 => Some(OpCode::StoreUpvalue),
|
43 => Some(OpCode::StoreUpvalue),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -93,7 +94,7 @@ impl OpCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Total number of distinct opcodes
|
/// Total number of distinct opcodes
|
||||||
pub const COUNT: usize = 46;
|
pub const COUNT: usize = 47;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compound assignment operator tags (used as operand byte after
|
/// Compound assignment operator tags (used as operand byte after
|
||||||
|
|||||||
@@ -1737,3 +1737,305 @@ fn eval_require_empty_module() {
|
|||||||
v => panic!("Expected Object, got {:?}", v),
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+88
-5
@@ -4,7 +4,7 @@
|
|||||||
//! call frames. Implements the `Runtime` trait for native function support.
|
//! call frames. Implements the `Runtime` trait for native function support.
|
||||||
|
|
||||||
use crate::error::RuntimeError;
|
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::lexer::Lexer;
|
||||||
use crate::parser::Parser;
|
use crate::parser::Parser;
|
||||||
use super::opcode::*;
|
use super::opcode::*;
|
||||||
@@ -109,8 +109,26 @@ impl Vm {
|
|||||||
|
|
||||||
fn execute_loop(&mut self) -> Result<(), RuntimeError> {
|
fn execute_loop(&mut self) -> Result<(), RuntimeError> {
|
||||||
loop {
|
loop {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
if self.frames.is_empty() {
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot frame state (must drop borrow before mutating self)
|
// Snapshot frame state (must drop borrow before mutating self)
|
||||||
@@ -122,12 +140,12 @@ impl Vm {
|
|||||||
let frame = self.frames.pop().unwrap();
|
let frame = self.frames.pop().unwrap();
|
||||||
self.close_upvalues(frame.stack_base);
|
self.close_upvalues(frame.stack_base);
|
||||||
if self.frames.is_empty() {
|
if self.frames.is_empty() {
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
// Remove callee + args + function locals, push nil result
|
// Remove callee + args + function locals, push nil result
|
||||||
self.stack.truncate(frame.stack_base.saturating_sub(1));
|
self.stack.truncate(frame.stack_base.saturating_sub(1));
|
||||||
self.stack.push(Value::Nil);
|
self.stack.push(Value::Nil);
|
||||||
continue;
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone Rc<FunctionProto> to access code without holding self.frames borrow
|
// Clone Rc<FunctionProto> to access code without holding self.frames borrow
|
||||||
@@ -445,7 +463,7 @@ impl Vm {
|
|||||||
self.stack.truncate(frame.stack_base.saturating_sub(1));
|
self.stack.truncate(frame.stack_base.saturating_sub(1));
|
||||||
self.stack.push(result);
|
self.stack.push(result);
|
||||||
if self.frames.is_empty() {
|
if self.frames.is_empty() {
|
||||||
return Ok(());
|
return Ok(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::Closure => {
|
OpCode::Closure => {
|
||||||
@@ -645,8 +663,16 @@ impl Vm {
|
|||||||
self.stack.push(result);
|
self.stack.push(result);
|
||||||
self.advance_ip(SIZE_U16_PLUS1);
|
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;
|
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
|
// Function calls
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use lsp_types::{
|
|||||||
const KEYWORDS: &[&str] = &[
|
const KEYWORDS: &[&str] = &[
|
||||||
"let", "const", "fn", "if", "else", "while", "for", "in",
|
"let", "const", "fn", "if", "else", "while", "for", "in",
|
||||||
"break", "continue", "return", "true", "false", "nil",
|
"break", "continue", "return", "true", "false", "nil",
|
||||||
|
"try", "catch", "finally", "throw",
|
||||||
];
|
];
|
||||||
|
|
||||||
const BUILTINS: &[&str] = &[
|
const BUILTINS: &[&str] = &[
|
||||||
|
|||||||
@@ -41,14 +41,8 @@ pub fn handle_goto_definition(
|
|||||||
let location = Location {
|
let location = Location {
|
||||||
uri: uri.clone(),
|
uri: uri.clone(),
|
||||||
range: Range {
|
range: Range {
|
||||||
start: Position {
|
start: Position { line: l, character: c },
|
||||||
line: l,
|
end: Position { line: l, character: c + word.len() as u32 },
|
||||||
character: c,
|
|
||||||
},
|
|
||||||
end: Position {
|
|
||||||
line: l,
|
|
||||||
character: c + word.len() as u32,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let response: GotoDefinitionResponse = GotoDefinitionResponse::Scalar(location);
|
let response: GotoDefinitionResponse = GotoDefinitionResponse::Scalar(location);
|
||||||
|
|||||||
+11
-1
@@ -28,7 +28,17 @@ pub fn run() {
|
|||||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||||
TextDocumentSyncKind::FULL,
|
TextDocumentSyncKind::FULL,
|
||||||
)),
|
)),
|
||||||
completion_provider: Some(lsp_types::CompletionOptions::default()),
|
completion_provider: Some(lsp_types::CompletionOptions {
|
||||||
|
trigger_characters: Some(
|
||||||
|
(b'a'..=b'z').chain(b'A'..=b'Z')
|
||||||
|
.map(|c| (c as char).to_string())
|
||||||
|
.chain(std::iter::once(".".to_string()))
|
||||||
|
.chain(std::iter::once("_".to_string()))
|
||||||
|
.collect()
|
||||||
|
),
|
||||||
|
resolve_provider: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)),
|
||||||
signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
|
signature_help_provider: Some(lsp_types::SignatureHelpOptions::default()),
|
||||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"patterns": [
|
"patterns": [
|
||||||
{
|
{
|
||||||
"name": "keyword.control.aster",
|
"name": "keyword.control.aster",
|
||||||
"match": "\\b(let|const|fn|if|else|while|for|in|break|continue|return)\\b"
|
"match": "\\b(let|const|fn|if|else|while|for|in|break|continue|return|try|catch|finally|throw)\\b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user