use crate::frontend::ast::{CatchClause, Expr, ExprKind, Pattern};
use crate::frontend::lexer::Token;
use crate::frontend::parser::{bail, ParserState, Result};
pub(in crate::frontend::parser) fn parse_try_catch(state: &mut ParserState) -> Result<Expr> {
let start_span = state.tokens.expect(&Token::Try)?;
let try_block = parse_try_block(state)?;
let catch_clauses = parse_catch_clauses(state)?;
let finally_block = parse_finally_block(state)?;
validate_try_catch_structure(&catch_clauses, finally_block.as_deref())?;
Ok(Expr::new(
ExprKind::TryCatch {
try_block,
catch_clauses,
finally_block,
},
start_span,
))
}
fn parse_try_block(state: &mut ParserState) -> Result<Box<Expr>> {
Ok(Box::new(crate::frontend::parser::collections::parse_block(
state,
)?))
}
fn parse_catch_clauses(state: &mut ParserState) -> Result<Vec<CatchClause>> {
let mut catch_clauses = Vec::new();
while matches!(state.tokens.peek(), Some((Token::Catch, _))) {
state.tokens.advance(); let pattern = parse_catch_pattern(state)?;
let body = parse_catch_body(state)?;
catch_clauses.push(CatchClause { pattern, body });
}
Ok(catch_clauses)
}
fn parse_catch_pattern(state: &mut ParserState) -> Result<Pattern> {
let has_parens = matches!(state.tokens.peek(), Some((Token::LeftParen, _)));
if has_parens {
state.tokens.expect(&Token::LeftParen)?;
}
let pattern = if let Some((Token::Identifier(name), _)) = state.tokens.peek() {
let name = name.clone();
state.tokens.advance();
Pattern::Identifier(name)
} else {
bail!("Expected identifier in catch clause");
};
if has_parens {
state.tokens.expect(&Token::RightParen)?;
}
Ok(pattern)
}
fn parse_catch_body(state: &mut ParserState) -> Result<Box<Expr>> {
Ok(Box::new(crate::frontend::parser::collections::parse_block(
state,
)?))
}
fn parse_finally_block(state: &mut ParserState) -> Result<Option<Box<Expr>>> {
if matches!(state.tokens.peek(), Some((Token::Finally, _))) {
state.tokens.advance(); Ok(Some(Box::new(
crate::frontend::parser::collections::parse_block(state)?,
)))
} else {
Ok(None)
}
}
fn validate_try_catch_structure(
catch_clauses: &[CatchClause],
finally_block: Option<&Expr>,
) -> Result<()> {
if catch_clauses.is_empty() && finally_block.is_none() {
bail!("Try block must have at least one catch clause or a finally block");
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::frontend::parser::Parser;
#[test]
fn test_try_catch_finally() {
let code = "try { connect() } catch (e) { log(e) } finally { cleanup() }";
let result = Parser::new(code).parse();
assert!(
result.is_ok(),
"Try-catch-finally should parse successfully"
);
}
#[test]
fn test_try_finally_no_catch() {
let code = "try { operation() } finally { cleanup() }";
let result = Parser::new(code).parse();
assert!(result.is_ok(), "Try-finally without catch should parse");
}
#[test]
fn test_try_multiple_catch() {
let code = "try { process() } catch (NetworkError) { retry() } catch (e) { fallback(e) }";
let result = Parser::new(code).parse();
assert!(
result.is_ok(),
"Try with multiple catch clauses should parse"
);
}
#[test]
fn test_try_without_catch_or_finally() {
let code = "try { operation() }";
let result = Parser::new(code).parse();
assert!(
result.is_err(),
"Try without catch or finally should fail validation"
);
}
#[test]
fn test_try_catch_with_complex_body() {
let code = "try { let x = 10; if x > 5 { risky(x) } else { safe(x) } } catch (e) { log(e); recover() }";
let result = Parser::new(code).parse();
assert!(
result.is_ok(),
"Try-catch with complex body should parse successfully"
);
}
use crate::frontend::ast::{Expr, ExprKind};
use crate::frontend::parser::Result;
fn parse(code: &str) -> Result<Expr> {
Parser::new(code).parse()
}
fn get_block_exprs(expr: &Expr) -> Option<&Vec<Expr>> {
match &expr.kind {
ExprKind::Block(exprs) => Some(exprs),
_ => None,
}
}
#[test]
fn test_try_catch_produces_try_catch_expr() {
let expr = parse("try { 1 } catch (e) { 0 }").unwrap();
if let Some(exprs) = get_block_exprs(&expr) {
assert!(
matches!(&exprs[0].kind, ExprKind::TryCatch { .. }),
"Should produce TryCatch ExprKind"
);
}
}
#[test]
fn test_try_catch_simple_bodies() {
let result = parse("try { foo() } catch (e) { bar() }");
assert!(result.is_ok(), "Simple try-catch should parse");
}
#[test]
fn test_try_catch_with_return() {
let result = parse("try { return value } catch (e) { return default }");
assert!(result.is_ok(), "Try-catch with return should parse");
}
#[test]
fn test_try_catch_with_throw() {
let result = parse("try { throw error } catch (e) { process(e) }");
assert!(result.is_ok(), "Try-catch with throw should parse");
}
#[test]
fn test_catch_with_parens() {
let result = parse("try { op() } catch (err) { recover(err) }");
assert!(result.is_ok(), "Catch with parens should parse");
}
#[test]
fn test_catch_without_parens() {
let result = parse("try { op() } catch err { recover(err) }");
assert!(result.is_ok(), "Catch without parens should parse");
}
#[test]
fn test_catch_single_letter_name() {
let result = parse("try { op() } catch (e) { recover(e) }");
assert!(result.is_ok(), "Catch with single letter should parse");
}
#[test]
fn test_catch_long_name() {
let result = parse("try { op() } catch (errorObject) { recover(errorObject) }");
assert!(result.is_ok(), "Catch with long name should parse");
}
#[test]
fn test_two_catch_clauses() {
let result = parse("try { op() } catch (e1) { h1() } catch (e2) { h2() }");
assert!(result.is_ok(), "Two catch clauses should parse");
}
#[test]
fn test_three_catch_clauses() {
let result = parse("try { op() } catch (a) { } catch (b) { } catch (c) { }");
assert!(result.is_ok(), "Three catch clauses should parse");
}
#[test]
fn test_catch_clause_count() {
let expr = parse("try { 1 } catch (a) { } catch (b) { }").unwrap();
if let Some(exprs) = get_block_exprs(&expr) {
if let ExprKind::TryCatch { catch_clauses, .. } = &exprs[0].kind {
assert_eq!(catch_clauses.len(), 2, "Should have 2 catch clauses");
}
}
}
#[test]
fn test_finally_only() {
let result = parse("try { op() } finally { cleanup() }");
assert!(result.is_ok(), "Try-finally only should parse");
}
#[test]
fn test_finally_has_block() {
let expr = parse("try { 1 } finally { cleanup() }").unwrap();
if let Some(exprs) = get_block_exprs(&expr) {
if let ExprKind::TryCatch { finally_block, .. } = &exprs[0].kind {
assert!(finally_block.is_some(), "finally_block should be Some");
}
}
}
#[test]
fn test_catch_and_finally() {
let result = parse("try { op() } catch (e) { recover(e) } finally { cleanup() }");
assert!(result.is_ok(), "Catch and finally should parse");
}
#[test]
fn test_multiple_catch_and_finally() {
let result = parse("try { op() } catch (a) { } catch (b) { } finally { cleanup() }");
assert!(result.is_ok(), "Multiple catch and finally should parse");
}
#[test]
fn test_try_with_let_binding() {
let result = parse("try { let x = compute(); x + 1 } catch (e) { 0 }");
assert!(result.is_ok(), "Try with let should parse");
}
#[test]
fn test_try_with_if_expression() {
let result = parse("try { if cond { a() } else { b() } } catch (e) { c() }");
assert!(result.is_ok(), "Try with if should parse");
}
#[test]
fn test_try_with_match() {
let result = parse("try { match x { Some(v) => v, None => 0 } } catch (e) { -1 }");
assert!(result.is_ok(), "Try with match should parse");
}
#[test]
fn test_catch_with_multiple_statements() {
let result = parse("try { risky() } catch (e) { log(e); notify(); recover() }");
assert!(
result.is_ok(),
"Catch with multiple statements should parse"
);
}
#[test]
fn test_finally_with_multiple_statements() {
let result = parse("try { op() } finally { close(); cleanup(); log() }");
assert!(
result.is_ok(),
"Finally with multiple statements should parse"
);
}
#[test]
fn test_nested_try_in_try() {
let result = parse("try { try { inner() } catch (e1) { } } catch (e2) { }");
assert!(result.is_ok(), "Nested try in try should parse");
}
#[test]
fn test_nested_try_in_catch() {
let result = parse("try { op() } catch (e) { try { recover() } catch (e2) { } }");
assert!(result.is_ok(), "Nested try in catch should parse");
}
#[test]
fn test_nested_try_in_finally() {
let result = parse("try { op() } finally { try { cleanup() } catch (e) { } }");
assert!(result.is_ok(), "Nested try in finally should parse");
}
#[test]
fn test_try_alone_fails() {
let result = parse("try { operation() }");
assert!(result.is_err(), "Try alone should fail");
}
#[test]
fn test_catch_alone_fails() {
let result = parse("catch (e) { recover(e) }");
assert!(result.is_err(), "Catch alone should fail");
}
#[test]
fn test_finally_alone_fails() {
let result = parse("finally { cleanup() }");
assert!(result.is_err(), "Finally alone should fail");
}
#[test]
fn test_try_with_method_call() {
let result = parse("try { obj.method() } catch (e) { }");
assert!(result.is_ok(), "Try with method call should parse");
}
#[test]
fn test_try_catch_returning_value() {
let result = parse("let x = try { get_value() } catch (e) { default }");
assert!(result.is_ok(), "Try-catch returning value should parse");
}
#[test]
fn test_try_with_await() {
let result = parse("try { await fetch(url) } catch (e) { }");
assert!(result.is_ok(), "Try with await should parse");
}
#[test]
fn test_try_catch_in_function() {
let result = parse("fun safe_op() { try { risky() } catch (e) { None } }");
assert!(result.is_ok(), "Try-catch in function should parse");
}
#[test]
fn test_try_catch_finally_chain() {
let result = parse("try { a() } catch (e1) { b() } catch (e2) { c() } finally { d() }");
assert!(result.is_ok(), "Try-catch-catch-finally should parse");
}
#[test]
fn test_try_with_if() {
let result = parse("try { if x { risky() } } catch (e) { }");
assert!(result.is_ok(), "Try with if should parse");
}
#[test]
fn test_deeply_nested_try() {
let result =
parse("try { try { try { x } catch (e3) { } } catch (e2) { } } catch (e1) { }");
assert!(result.is_ok(), "Deeply nested try should parse");
}
#[test]
fn test_try_with_match_inside() {
let result = parse("try { match x { 1 => a(), _ => b() } } catch (e) { }");
assert!(result.is_ok(), "Try with match should parse");
}
#[test]
fn test_finally_with_return() {
let result = parse("fun f() { try { x } catch (e) { } finally { return 0 } }");
assert!(result.is_ok(), "Finally with return should parse");
}
#[test]
fn test_try_in_lambda() {
let result = parse("|x| try { process(x) } catch (e) { default }");
assert!(result.is_ok(), "Try in lambda should parse");
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
#[ignore = "Property tests run with --ignored flag"] fn prop_try_catch_always_parses(_seed in any::<u32>()) {
let code = "try { 42 } catch (e) { 0 }";
let result = Parser::new(code).parse();
prop_assert!(result.is_ok());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_try_catch_with_identifier(err_name in "[a-z]+") {
let code = format!("try {{ 42 }} catch ({err_name}) {{ 0 }}");
let result = Parser::new(&code).parse();
prop_assert!(result.is_ok());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_try_finally_parses(val in 0i32..100) {
let code = format!("try {{ {val} }} finally {{ cleanup() }}");
let result = Parser::new(&code).parse();
prop_assert!(result.is_ok());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_multiple_catch_parses(n in 1usize..5) {
let mut code = String::from("try { risky() }");
for i in 0..n {
code.push_str(&format!(" catch (e{i}) {{ recover{i}() }}"));
}
let result = Parser::new(&code).parse();
prop_assert!(result.is_ok());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_try_without_handlers_fails(_seed in any::<u32>()) {
let code = "try { operation() }";
let result = Parser::new(code).parse();
prop_assert!(result.is_err());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_nested_try_catch_parses(depth in 1usize..4) {
let mut code = String::new();
for _ in 0..depth {
code.push_str("try { ");
}
code.push_str("42");
for i in 0..depth {
code.push_str(&format!(" }} catch (e{i}) {{ 0 }}"));
}
let result = Parser::new(&code).parse();
prop_assert!(result.is_ok());
}
#[test]
#[ignore = "Property tests run with --ignored flag"]
fn prop_catch_without_parens_parses(err_name in "[a-z]+") {
let code = format!("try {{ 42 }} catch {err_name} {{ 0 }}");
let result = Parser::new(&code).parse();
prop_assert!(result.is_ok());
}
}
}
}