From d48fc0826e02af22f3419daf5543405629559876 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 22:07:29 -0400 Subject: [PATCH 1/2] Refactor parser grammar and expression precedence handling --- Cargo.toml | 2 +- src/parser.rs | 667 +++++++++++++++++++++++--------------------------- 2 files changed, 311 insertions(+), 358 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa65733..99cd017 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ harness = false anyhow = "1.0.100" ariadne = "0.5.1" astro-float = { version = "0.9.5", default-features = false, features = ["std"] } -chumsky = "0.10.1" +chumsky = { version = "0.10.1", features = ["pratt"] } clap = { version = "4.5.51", features = ["derive"] } [target.'cfg(not(target_family = "wasm"))'.dependencies] diff --git a/src/parser.rs b/src/parser.rs index 0f064d0..b39def6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,10 @@ -use super::*; +use { + super::*, + chumsky::input::MapExtra, + chumsky::pratt::{infix, left, postfix, prefix, right}, +}; + +type ParserError<'a> = extra::Err>; /// # Errors /// @@ -18,53 +24,66 @@ pub fn parse(input: &str) -> Result>, Vec> { } fn program_parser<'a>() --> impl Parser<'a, &'a str, Spanned>, extra::Err>> + Clone -{ - let statement = statement_parser(); +-> impl Parser<'a, &'a str, Spanned>, ParserError<'a>> + Clone { + statement_list_parser(statement_parser()) + .map(Program::Statements) + .map_with(|ast, error| (ast, error.span())) +} - statement - .then(just(';').padded().or_not()) - .map(|(stmt, _)| stmt) - .repeated() +fn comma_separated_parser<'a, P, T>( + parser: P, +) -> impl Parser<'a, &'a str, Vec, ParserError<'a>> + Clone +where + P: Parser<'a, &'a str, T, ParserError<'a>> + Clone, +{ + parser + .separated_by(just(',').padded()) + .allow_trailing() .collect::>() - .map(Program::Statements) - .map_with(|ast, e| (ast, e.span())) } fn index_parser<'a, P>( expression: P, -) -> impl Parser< - 'a, - &'a str, - (Spanned>, SimpleSpan), - extra::Err>, -> + Clone +) -> impl Parser<'a, &'a str, (Spanned>, SimpleSpan), ParserError<'a>> ++ Clone where - P: Parser<'a, &'a str, Spanned>, extra::Err>> - + Clone, + P: Parser<'a, &'a str, Spanned>, ParserError<'a>> + Clone, { expression .delimited_by(just('['), just(']')) .padded() - .map_with(|expression, e| (expression, e.span())) + .map_with(|expression, error| (expression, error.span())) +} + +fn keyword_parser<'a>( + keyword: &'static str, +) -> impl Parser<'a, &'a str, (), ParserError<'a>> + Clone { + text::keyword(keyword).padded().ignored() +} + +fn statement_list_parser<'a, P>( + statement: P, +) -> impl Parser<'a, &'a str, Vec>>, ParserError<'a>> + Clone +where + P: Parser<'a, &'a str, Spanned>, ParserError<'a>> + Clone, +{ + statement + .then(just(';').padded().or_not()) + .map(|(statement, _)| statement) + .repeated() + .collect::>() } fn statement_parser<'a>() --> impl Parser<'a, &'a str, Spanned>, extra::Err>> -+ Clone { +-> impl Parser<'a, &'a str, Spanned>, ParserError<'a>> + Clone { let expression = expression_parser(); recursive(|statement| { - let statement_block = statement - .clone() - .then(just(';').padded().or_not()) - .map(|(statement, _)| statement) - .repeated() - .collect::>() + let statement_block = statement_list_parser(statement.clone()) .delimited_by(just('{').padded(), just('}').padded()); - let simple_ident = text::ident().padded().map_with(|name, e| { - let span = e.span(); + let simple_ident = text::ident().padded().map_with(|name, error| { + let span = error.span(); (AssignmentTarget::Identifier(name), span) }); @@ -84,88 +103,75 @@ fn statement_parser<'a>() .then_ignore(just('=').padded()) .then(expression.clone()) .map(|(lhs, rhs)| Statement::Assignment(lhs, rhs)) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let function_statement = just("fn") - .padded() + let function_statement = keyword_parser("fn") .ignore_then(text::ident().padded()) .then( - text::ident() - .padded() - .separated_by(just(',')) - .allow_trailing() - .collect::>() + comma_separated_parser(text::ident().padded()) .delimited_by(just('(').padded(), just(')').padded()), ) .then(statement_block.clone()) .map(|((name, params), body)| Statement::Function(name, params, body)) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let block_statement = statement_block .clone() .map(Statement::Block) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let condition_parser = expression .clone() .delimited_by(just('(').padded(), just(')').padded()); - let if_statement = just("if") - .padded() + let if_statement = keyword_parser("if") .ignore_then(condition_parser.clone()) .then(statement_block.clone()) .then( - just("else") - .padded() + keyword_parser("else") .ignore_then(statement_block.clone()) .or_not(), ) .map(|((condition, then_branch), else_branch)| { Statement::If(condition, then_branch, else_branch) }) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let while_statement = just("while") - .padded() + let while_statement = keyword_parser("while") .ignore_then(condition_parser) .then(statement_block.clone()) .map(|(condition, body)| Statement::While(condition, body)) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let for_statement = just("for") - .padded() + let for_statement = keyword_parser("for") .ignore_then(text::ident().padded()) - .then_ignore(just("in").padded()) + .then_ignore(keyword_parser("in")) .then(expression.clone()) .then(statement_block.clone()) .map(|((name, iterable), body)| Statement::For(name, iterable, body)) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let loop_statement = just("loop") - .padded() + let loop_statement = keyword_parser("loop") .ignore_then(statement_block.clone()) .map(Statement::Loop) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let return_statement = just("return") - .padded() + let return_statement = keyword_parser("return") .ignore_then(expression.clone().or_not()) .map(Statement::Return) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let break_statement = just("break") - .padded() - .map(|_| Statement::Break) - .map_with(|ast, e| (ast, e.span())); + let break_statement = keyword_parser("break") + .map(|()| Statement::Break) + .map_with(|ast, error| (ast, error.span())); - let continue_statement = just("continue") - .padded() - .map(|_| Statement::Continue) - .map_with(|ast, e| (ast, e.span())); + let continue_statement = keyword_parser("continue") + .map(|()| Statement::Continue) + .map_with(|ast, error| (ast, error.span())); let expression_statement = expression .map(Statement::Expression) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); choice(( assignment_statement, @@ -186,8 +192,7 @@ fn statement_parser<'a>() } fn expression_parser<'a>() --> impl Parser<'a, &'a str, Spanned>, extra::Err>> -+ Clone { +-> impl Parser<'a, &'a str, Spanned>, ParserError<'a>> + Clone { let identifier = text::ident().padded(); recursive(|expression| { @@ -197,56 +202,49 @@ fn expression_parser<'a>() .from_str() .unwrapped() .map(Expression::Number) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); - let boolean = choice((just("true").to(true), just("false").to(false))) - .map(Expression::Boolean) - .map_with(|ast, e| (ast, e.span())); + let boolean = choice(( + keyword_parser("true").to(true), + keyword_parser("false").to(false), + )) + .map(Expression::Boolean) + .map_with(|ast, error| (ast, error.span())); - let null = just("null") - .map(|_| Expression::Null) - .map_with(|ast, e| (ast, e.span())); + let null = keyword_parser("null") + .map(|()| Expression::Null) + .map_with(|ast, error| (ast, error.span())); let double_quoted_string = just('"') .ignore_then(none_of('"').repeated().to_slice()) .then_ignore(just('"')) .map(Expression::String) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let single_quoted_string = just('\'') .ignore_then(none_of('\'').repeated().to_slice()) .then_ignore(just('\'')) .map(Expression::String) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let string = double_quoted_string.or(single_quoted_string); let function_call = identifier .then( - expression - .clone() - .separated_by(just(',')) - .allow_trailing() - .collect::>() + comma_separated_parser(expression.clone()) .delimited_by(just('('), just(')')), ) .map(|(name, arguments)| Expression::FunctionCall(name, arguments)) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let identifier = identifier .map(Expression::Identifier) - .map_with(|ast, e| (ast, e.span())); - - let items = expression - .clone() - .separated_by(just(',')) - .allow_trailing() - .collect::>(); + .map_with(|ast, error| (ast, error.span())); - let list = items + let list = comma_separated_parser(expression.clone()) .delimited_by(just('['), just(']')) .map(Expression::List) - .map_with(|ast, e| (ast, e.span())); + .map_with(|ast, error| (ast, error.span())); let atom = number .or(boolean) @@ -258,132 +256,84 @@ fn expression_parser<'a>() .or(string) .padded(); - let list_access = atom.clone().foldl( - index_parser(expression.clone()).repeated(), - |list: Spanned>, - (index, span): (Spanned>, SimpleSpan)| { - let span = (list.1.start..span.end).into(); - - let expression = - Expression::ListAccess(Box::new(list), Box::new(index)); - - (expression, span) - }, - ); - - let op = |c| just(c).padded(); - - let unary = choice((op('-').to(UnaryOp::Negate), op('!').to(UnaryOp::Not))) - .repeated() - .foldr(list_access, |op, rhs| { - let span = rhs.1; - (Expression::UnaryOp(op, Box::new(rhs)), span) - }); - - let power = recursive(|power| { - unary - .clone() - .then( - just('^') - .padded() - .ignore_then(power.or(unary.clone())) - .or_not(), + let binary = + |lhs: Spanned>, + op: BinaryOp, + rhs: Spanned>, + error: &mut MapExtra<'a, '_, &'a str, ParserError<'a>>| { + ( + Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), + error.span(), ) - .map(|(lhs, rhs)| match rhs { - Some(rhs) => { - let span = (lhs.1.start..rhs.1.end).into(); - - let expression = Expression::BinaryOp( - BinaryOp::Power, - Box::new(lhs), - Box::new(rhs), - ); - - (expression, span) - } - None => lhs, - }) - }); - - let product = power.clone().foldl( - choice(( - op('%').to(BinaryOp::Modulo), - op('*').to(BinaryOp::Multiply), - op('/').to(BinaryOp::Divide), - )) - .then(power.clone()) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ); - - let sum = product.clone().foldl( - choice((op('+').to(BinaryOp::Add), op('-').to(BinaryOp::Subtract))) - .then(product) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ); - - let relational = sum.clone().foldl( - choice(( - just(">=").padded().to(BinaryOp::GreaterThanEqual), - just("<=").padded().to(BinaryOp::LessThanEqual), - just(">").padded().to(BinaryOp::GreaterThan), - just("<").padded().to(BinaryOp::LessThan), - )) - .boxed() - .then(sum.clone().boxed()) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ); - - let equality = relational.clone().foldl( - choice(( - just("==").padded().to(BinaryOp::Equal), - just("!=").padded().to(BinaryOp::NotEqual), - )) - .boxed() - .then(relational.clone().boxed()) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ); - - let logical_and = equality.clone().foldl( - just("&&") - .padded() - .to(BinaryOp::LogicalAnd) - .boxed() - .then(equality.clone().boxed()) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ); - - logical_and.clone().foldl( - just("||") - .padded() - .to(BinaryOp::LogicalOr) - .boxed() - .then(logical_and.clone().boxed()) - .repeated(), - |lhs, (op, rhs)| { - let span = (lhs.1.start..rhs.1.end).into(); - (Expression::BinaryOp(op, Box::new(lhs), Box::new(rhs)), span) - }, - ) + }; + + let unary = + |op: UnaryOp, + rhs: Spanned>, + error: &mut MapExtra<'a, '_, &'a str, ParserError<'a>>| { + (Expression::UnaryOp(op, Box::new(rhs)), error.span()) + }; + + atom.pratt(( + postfix( + 8, + index_parser(expression.clone()), + |list, + (index, _), + error: &mut MapExtra<'a, '_, &'a str, ParserError<'a>>| { + let span = error.span(); + + let expression = + Expression::ListAccess(Box::new(list), Box::new(index)); + + (expression, span) + }, + ), + prefix(7, just('-').padded().to(UnaryOp::Negate), unary), + prefix(7, just('!').padded().to(UnaryOp::Not), unary), + infix(right(6), just('^').padded().to(BinaryOp::Power), binary), + infix( + left(5), + choice(( + just('%').padded().to(BinaryOp::Modulo), + just('*').padded().to(BinaryOp::Multiply), + just('/').padded().to(BinaryOp::Divide), + )), + binary, + ), + infix( + left(4), + choice(( + just('+').padded().to(BinaryOp::Add), + just('-').padded().to(BinaryOp::Subtract), + )), + binary, + ), + infix( + left(3), + choice(( + just(">=").padded().to(BinaryOp::GreaterThanEqual), + just("<=").padded().to(BinaryOp::LessThanEqual), + just(">").padded().to(BinaryOp::GreaterThan), + just("<").padded().to(BinaryOp::LessThan), + )), + binary, + ), + infix( + left(2), + choice(( + just("==").padded().to(BinaryOp::Equal), + just("!=").padded().to(BinaryOp::NotEqual), + )), + binary, + ), + infix( + left(1), + just("&&").padded().to(BinaryOp::LogicalAnd), + binary, + ), + infix(left(0), just("||").padded().to(BinaryOp::LogicalOr), binary), + )) }) } @@ -434,42 +384,6 @@ mod tests { } } - #[test] - fn integer_literal() { - Test::new() - .program("25") - .ast("statements(expression(number(25)))") - .run(); - } - - #[test] - fn operator_precedence() { - Test::new() - .program("2 + 3 * 4") - .ast("statements(expression(binary_op(+, number(2), binary_op(*, number(3), number(4)))))") - .run(); - - Test::new() - .program("2 * 3 + 4") - .ast("statements(expression(binary_op(+, binary_op(*, number(2), number(3)), number(4))))") - .run(); - - Test::new() - .program("2 * 3 / 4") - .ast("statements(expression(binary_op(/, binary_op(*, number(2), number(3)), number(4))))") - .run(); - - Test::new() - .program("2 ^ 3 * 4") - .ast("statements(expression(binary_op(*, binary_op(^, number(2), number(3)), number(4))))") - .run(); - - Test::new() - .program("!2 + 3") - .ast("statements(expression(binary_op(+, unary_op(!, number(2)), number(3))))") - .run(); - } - #[test] fn assignment() { Test::new() @@ -484,52 +398,18 @@ mod tests { } #[test] - fn whitespace_handling() { - Test::new() - .program(" 2 + 3 ") - .ast("statements(expression(binary_op(+, number(2), number(3))))") - .run(); - - Test::new() - .program("\n5\n*\n2\n") - .ast("statements(expression(binary_op(*, number(5), number(2))))") - .run(); - - Test::new() - .program("\t8\t/\t4\t") - .ast("statements(expression(binary_op(/, number(8), number(4))))") - .run(); - } - - #[test] - fn multiple_top_level_statements() { - Test::new().program("1 + 2; 3 * 4").ast("statements(expression(binary_op(+, number(1), number(2))), expression(binary_op(*, number(3), number(4))))").run(); + fn break_statement() { + Test::new().program("break").ast("statements(break)").run(); } #[test] - fn multiple_statements_in_block() { + fn continue_statement() { Test::new() - .program("1 + 2; { 3 * 4; 5 - 6 }; 7") - .ast("statements(expression(binary_op(+, number(1), number(2))), block(expression(binary_op(*, number(3), number(4))), expression(binary_op(-, number(5), number(6)))), expression(number(7)))") + .program("continue") + .ast("statements(continue)") .run(); } - #[test] - fn newline_separated_statements() { - Test::new() - .program("1 + 2\n3 * 4") - .ast("statements(expression(binary_op(+, number(1), number(2))), expression(binary_op(*, number(3), number(4))))") - .run(); - } - - #[test] - fn while_loop() { - Test::new() - .program("while (x < 10) { x = x + 1; }") - .ast("statements(while(binary_op(<, identifier(x), number(10)), block(assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") - .run(); - } - #[test] fn for_loop() { Test::new() @@ -539,18 +419,10 @@ mod tests { } #[test] - fn nested_while_loops() { - Test::new() - .program("while (x < 10) { while (y < 5) { y = y + 1; }; x = x + 1; }") - .ast("statements(while(binary_op(<, identifier(x), number(10)), block(while(binary_op(<, identifier(y), number(5)), block(assignment(identifier(y), binary_op(+, identifier(y), number(1))))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") - .run(); - } - - #[test] - fn if_statement() { + fn function_with_return() { Test::new() - .program("if (x > 5) { y = 10; }") - .ast("statements(if(binary_op(>, identifier(x), number(5)), block(assignment(identifier(y), number(10)))))") + .program("fn add(a, b) { return a + b; }") + .ast("statements(function(add, [a, b], block(return(binary_op(+, identifier(a), identifier(b))))))") .run(); } @@ -563,32 +435,30 @@ mod tests { } #[test] - fn nested_if_statements() { + fn if_statement() { Test::new() - .program("if (x > 5) { if (y > 2) { z = 1; } else { z = 2; } } else { z = 3; }") - .ast("statements(if(binary_op(>, identifier(x), number(5)), block(if(binary_op(>, identifier(y), number(2)), block(assignment(identifier(z), number(1))), block(assignment(identifier(z), number(2))))), block(assignment(identifier(z), number(3)))))") + .program("if (x > 5) { y = 10; }") + .ast("statements(if(binary_op(>, identifier(x), number(5)), block(assignment(identifier(y), number(10)))))") .run(); } #[test] - fn return_statement() { - Test::new() - .program("return 5") - .ast("statements(return(number(5)))") - .run(); - + fn integer_literal() { Test::new() - .program("return") - .ast("statements(return())") + .program("25") + .ast("statements(expression(number(25)))") .run(); } #[test] - fn function_with_return() { + fn invalid_operator() { Test::new() - .program("fn add(a, b) { return a + b; }") - .ast("statements(function(add, [a, b], block(return(binary_op(+, identifier(a), identifier(b))))))") - .run(); + .program("2 +* 3") + .errors(vec![Error::new( + SimpleSpan::from(2..3), + "found '+' expected identifier, '(', '[', '\"', ''', or end of input", + )]) + .run(); } #[test] @@ -608,72 +478,118 @@ mod tests { } #[test] - fn nested_list_access() { + fn list_access_with_expressions() { Test::new() - .program("a = [[1, 2], [3, 4]]; a[0][1]") - .ast("statements(assignment(identifier(a), list(list(number(1), number(2)), list(number(3), number(4)))), expression(list_access(list_access(identifier(a), number(0)), number(1))))") + .program("a = [1, 2, 3]; a[1 + 1]") + .ast("statements(assignment(identifier(a), list(number(1), number(2), number(3))), expression(list_access(identifier(a), binary_op(+, number(1), number(1)))))") .run(); } #[test] - fn list_access_with_expressions() { + fn loop_statement() { Test::new() - .program("a = [1, 2, 3]; a[1 + 1]") - .ast("statements(assignment(identifier(a), list(number(1), number(2), number(3))), expression(list_access(identifier(a), binary_op(+, number(1), number(1)))))") - .run(); + .program("loop { x = x + 1; }") + .ast("statements(loop(block(assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); } #[test] - fn break_statement() { - Test::new().program("break").ast("statements(break)").run(); + fn loop_with_break() { + Test::new() + .program("loop { if (x > 10) { break; }; x = x + 1; }") + .ast("statements(loop(block(if(binary_op(>, identifier(x), number(10)), block(break)), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); } #[test] - fn continue_statement() { + fn loop_with_continue() { Test::new() - .program("continue") - .ast("statements(continue)") + .program("loop { if (x % 2 == 0) { continue; }; println(x); x = x + 1; }") + .ast("statements(loop(block(if(binary_op(==, binary_op(%, identifier(x), number(2)), number(0)), block(continue)), expression(function_call(println,identifier(x))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); + } + + #[test] + fn missing_closing_parenthesis() { + Test::new() + .program("(2 + 3") + .errors(vec![Error::new( + SimpleSpan::from(6..6), + "found end of input expected any, '.', '[', '^', '%', '*', '/', '+', '-', '>', '<', '=', '!', '&', '|', or ')'", + )]) + .run(); + } + #[test] + fn multiple_statements_in_block() { + Test::new() + .program("1 + 2; { 3 * 4; 5 - 6 }; 7") + .ast("statements(expression(binary_op(+, number(1), number(2))), block(expression(binary_op(*, number(3), number(4))), expression(binary_op(-, number(5), number(6)))), expression(number(7)))") .run(); } #[test] - fn while_with_break() { + fn multiple_top_level_statements() { + Test::new().program("1 + 2; 3 * 4").ast("statements(expression(binary_op(+, number(1), number(2))), expression(binary_op(*, number(3), number(4))))").run(); + } + + #[test] + fn nested_if_statements() { Test::new() - .program("while (x < 10) { if (x == 5) { break; }; x = x + 1; }") - .ast("statements(while(binary_op(<, identifier(x), number(10)), block(if(binary_op(==, identifier(x), number(5)), block(break)), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .program("if (x > 5) { if (y > 2) { z = 1; } else { z = 2; } } else { z = 3; }") + .ast("statements(if(binary_op(>, identifier(x), number(5)), block(if(binary_op(>, identifier(y), number(2)), block(assignment(identifier(z), number(1))), block(assignment(identifier(z), number(2))))), block(assignment(identifier(z), number(3)))))") .run(); } #[test] - fn while_with_continue() { + fn nested_list_access() { Test::new() - .program("while (x < 10) { if (x % 2 == 0) { continue; }; println(x); x = x + 1; }") - .ast("statements(while(binary_op(<, identifier(x), number(10)), block(if(binary_op(==, binary_op(%, identifier(x), number(2)), number(0)), block(continue)), expression(function_call(println,identifier(x))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") - .run(); + .program("a = [[1, 2], [3, 4]]; a[0][1]") + .ast("statements(assignment(identifier(a), list(list(number(1), number(2)), list(number(3), number(4)))), expression(list_access(list_access(identifier(a), number(0)), number(1))))") + .run(); } #[test] - fn loop_statement() { + fn nested_while_loops() { Test::new() - .program("loop { x = x + 1; }") - .ast("statements(loop(block(assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .program("while (x < 10) { while (y < 5) { y = y + 1; }; x = x + 1; }") + .ast("statements(while(binary_op(<, identifier(x), number(10)), block(while(binary_op(<, identifier(y), number(5)), block(assignment(identifier(y), binary_op(+, identifier(y), number(1))))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") .run(); } #[test] - fn loop_with_break() { + fn newline_separated_statements() { Test::new() - .program("loop { if (x > 10) { break; }; x = x + 1; }") - .ast("statements(loop(block(if(binary_op(>, identifier(x), number(10)), block(break)), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .program("1 + 2\n3 * 4") + .ast("statements(expression(binary_op(+, number(1), number(2))), expression(binary_op(*, number(3), number(4))))") .run(); } #[test] - fn loop_with_continue() { + fn operator_precedence() { Test::new() - .program("loop { if (x % 2 == 0) { continue; }; println(x); x = x + 1; }") - .ast("statements(loop(block(if(binary_op(==, binary_op(%, identifier(x), number(2)), number(0)), block(continue)), expression(function_call(println,identifier(x))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") - .run(); + .program("2 + 3 * 4") + .ast("statements(expression(binary_op(+, number(2), binary_op(*, number(3), number(4)))))") + .run(); + + Test::new() + .program("2 * 3 + 4") + .ast("statements(expression(binary_op(+, binary_op(*, number(2), number(3)), number(4))))") + .run(); + + Test::new() + .program("2 * 3 / 4") + .ast("statements(expression(binary_op(/, binary_op(*, number(2), number(3)), number(4))))") + .run(); + + Test::new() + .program("2 ^ 3 * 4") + .ast("statements(expression(binary_op(*, binary_op(^, number(2), number(3)), number(4))))") + .run(); + + Test::new() + .program("!2 + 3") + .ast("statements(expression(binary_op(+, unary_op(!, number(2)), number(3))))") + .run(); } #[test] @@ -694,6 +610,19 @@ mod tests { .run(); } + #[test] + fn return_statement() { + Test::new() + .program("return 5") + .ast("statements(return(number(5)))") + .run(); + + Test::new() + .program("return") + .ast("statements(return())") + .run(); + } + #[test] fn unclosed_string() { Test::new() @@ -706,21 +635,45 @@ mod tests { } #[test] - fn invalid_operator() { + fn while_loop() { Test::new() - .program("2 +* 3") - .errors(vec![Error::new(SimpleSpan::from(3..4), "found '*' expected '-', '!', non-zero digit, '0', 't', 'f', 'n', '(', identifier, '[', '\"', or '''")]) - .run(); + .program("while (x < 10) { x = x + 1; }") + .ast("statements(while(binary_op(<, identifier(x), number(10)), block(assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); } #[test] - fn missing_closing_parenthesis() { + fn while_with_break() { Test::new() - .program("(2 + 3") - .errors(vec![Error::new( - SimpleSpan::from(6..6), - "found end of input expected any, '.', '[', '^', '%', '*', '/', '+', '-', '>', '<', '=', '!', '&', '|', or ')'", - )]) + .program("while (x < 10) { if (x == 5) { break; }; x = x + 1; }") + .ast("statements(while(binary_op(<, identifier(x), number(10)), block(if(binary_op(==, identifier(x), number(5)), block(break)), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); + } + + #[test] + fn while_with_continue() { + Test::new() + .program("while (x < 10) { if (x % 2 == 0) { continue; }; println(x); x = x + 1; }") + .ast("statements(while(binary_op(<, identifier(x), number(10)), block(if(binary_op(==, binary_op(%, identifier(x), number(2)), number(0)), block(continue)), expression(function_call(println,identifier(x))), assignment(identifier(x), binary_op(+, identifier(x), number(1))))))") + .run(); + } + + #[test] + fn whitespace_handling() { + Test::new() + .program(" 2 + 3 ") + .ast("statements(expression(binary_op(+, number(2), number(3))))") + .run(); + + Test::new() + .program("\n5\n*\n2\n") + .ast("statements(expression(binary_op(*, number(5), number(2))))") + .run(); + + Test::new() + .program("\t8\t/\t4\t") + .ast("statements(expression(binary_op(/, number(8), number(4))))") .run(); } + } From 13851db62441c1142215f6d6faeb82978fefbe6a Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 4 May 2026 22:09:14 -0400 Subject: [PATCH 2/2] Format --- src/parser.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index b39def6..c6a9df7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -675,5 +675,4 @@ mod tests { .ast("statements(expression(binary_op(/, number(8), number(4))))") .run(); } - }