refactor: 抽离测试代码
This commit is contained in:
+2
-1268
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2
-413
@@ -346,417 +346,6 @@ fn is_ident_part(c: char) -> bool {
|
|||||||
is_ident_start(c) || c.is_ascii_digit()
|
is_ident_start(c) || c.is_ascii_digit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Tests
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
/// Helper: tokenize input and return only TokenKinds (drop EOF and positions).
|
|
||||||
fn tokenize(input: &str) -> Vec<TokenKind> {
|
|
||||||
let (tokens, _errors) = Lexer::new(input).tokenize();
|
|
||||||
tokens.into_iter().filter_map(|t| {
|
|
||||||
if matches!(t.kind, TokenKind::EOF) { None } else { Some(t.kind) }
|
|
||||||
}).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: tokenize and return full tokens (with positions).
|
|
||||||
fn tokenize_full(input: &str) -> (Vec<Token>, Vec<RuntimeError>) {
|
|
||||||
Lexer::new(input).tokenize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- single-character tokens -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_char_tokens() {
|
|
||||||
let kinds = tokenize("(){},;.:");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::LeftParen, TokenKind::RightParen,
|
|
||||||
TokenKind::LeftBrace, TokenKind::RightBrace,
|
|
||||||
TokenKind::Comma, TokenKind::Semicolon,
|
|
||||||
TokenKind::Dot, TokenKind::Colon,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn brackets() {
|
|
||||||
let kinds = tokenize("[]");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::LeftBracket, TokenKind::RightBracket]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- operators -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn arithmetic_operators() {
|
|
||||||
let kinds = tokenize("+ - * / %");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::Plus, TokenKind::Minus, TokenKind::Star,
|
|
||||||
TokenKind::Slash, TokenKind::Percent,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comparison_operators() {
|
|
||||||
let kinds = tokenize("< <= > >=");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::Less, TokenKind::LessEqual,
|
|
||||||
TokenKind::Greater, TokenKind::GreaterEqual,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn equality_operators() {
|
|
||||||
let kinds = tokenize("== !=");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::EqualEqual, TokenKind::BangEqual]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn logical_operators() {
|
|
||||||
let kinds = tokenize("&& || !");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::AndAnd, TokenKind::OrOr, TokenKind::Bang]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn assignment_operators() {
|
|
||||||
let kinds = tokenize("= += -= *= /= %=");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::Equal,
|
|
||||||
TokenKind::PlusEqual, TokenKind::MinusEqual,
|
|
||||||
TokenKind::StarEqual, TokenKind::SlashEqual,
|
|
||||||
TokenKind::PercentEqual,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn operator_proximity_no_spaces() {
|
|
||||||
let kinds = tokenize("a+b-c*d/e%f");
|
|
||||||
// identifiers a, b, c, d, e, f interleaved with operators
|
|
||||||
let ids: Vec<_> = kinds.iter().filter(|k| matches!(k, TokenKind::Identifier(_))).collect();
|
|
||||||
assert_eq!(ids.len(), 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- numbers -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn integer_literal() {
|
|
||||||
let kinds = tokenize("42");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_literal() {
|
|
||||||
let kinds = tokenize("3.14");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(3.14)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn zero_and_negative() {
|
|
||||||
// Note: `-` is a separate unary operator, but `0` should lex correctly
|
|
||||||
let kinds = tokenize("0");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(0.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- strings -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_literal() {
|
|
||||||
let kinds = tokenize("\"hello\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_string() {
|
|
||||||
let kinds = tokenize("\"\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_with_spaces() {
|
|
||||||
let kinds = tokenize("\"hello world\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("hello world".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiple_strings() {
|
|
||||||
let kinds = tokenize("\"a\" \"b\"");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::String("a".into()),
|
|
||||||
TokenKind::String("b".into()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_escape_newline() {
|
|
||||||
let kinds = tokenize("\"a\\nb\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_escape_tab() {
|
|
||||||
let kinds = tokenize("\"a\\tb\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_escape_quote() {
|
|
||||||
let kinds = tokenize("\"he\\\"llo\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_escape_backslash() {
|
|
||||||
let kinds = tokenize("\"a\\\\b\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_escape_carriage_return() {
|
|
||||||
let kinds = tokenize("\"a\\rb\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_unknown_escape_is_error() {
|
|
||||||
let (_, errors) = tokenize_full("\"\\x\"");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unknown escape"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- single-quoted strings -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_quoted_string() {
|
|
||||||
let kinds = tokenize("'hello'");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_quoted_empty_string() {
|
|
||||||
let kinds = tokenize("''");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_quoted_with_escape() {
|
|
||||||
let kinds = tokenize("'a\\'b'");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a'b".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_quoted_with_newline_escape() {
|
|
||||||
let kinds = tokenize("'a\\nb'");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mixed_quote_strings() {
|
|
||||||
let kinds = tokenize("\"double\" 'single'");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::String("double".into()),
|
|
||||||
TokenKind::String("single".into()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_quote_can_contain_double_quote() {
|
|
||||||
let kinds = tokenize("'he\"llo'");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn double_quote_can_contain_single_quote() {
|
|
||||||
let kinds = tokenize("\"it's\"");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::String("it's".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unterminated_single_quoted_string() {
|
|
||||||
let (_, errors) = tokenize_full("'unterminated");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unterminated"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- identifiers and keywords -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn identifiers() {
|
|
||||||
let kinds = tokenize("foo bar _private x1");
|
|
||||||
let ids: Vec<_> = kinds.iter().filter_map(|k| {
|
|
||||||
if let TokenKind::Identifier(s) = k { Some(s.clone()) } else { None }
|
|
||||||
}).collect();
|
|
||||||
assert_eq!(ids, vec!["foo", "bar", "_private", "x1"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn keywords() {
|
|
||||||
let kinds = tokenize("let const fn if else while for in break continue return true false nil");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::Let, TokenKind::Const, TokenKind::Fn,
|
|
||||||
TokenKind::If, TokenKind::Else,
|
|
||||||
TokenKind::While, TokenKind::For, TokenKind::In,
|
|
||||||
TokenKind::Break, TokenKind::Continue,
|
|
||||||
TokenKind::Return,
|
|
||||||
TokenKind::True, TokenKind::False,
|
|
||||||
TokenKind::Nil,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn identifier_looks_like_keyword_prefix() {
|
|
||||||
// "lettuce" should be identifier, not "let" + error
|
|
||||||
let kinds = tokenize("lettuce");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Identifier("lettuce".into())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- comments -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_comment_ignored() {
|
|
||||||
let kinds = tokenize("// this is a comment\n42");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comment_at_end_of_file() {
|
|
||||||
let kinds = tokenize("42 // trailing comment");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comment_only_line() {
|
|
||||||
let kinds = tokenize("// just a comment");
|
|
||||||
assert!(kinds.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn comment_between_tokens() {
|
|
||||||
let kinds = tokenize("1 // comment\n2");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- whitespace -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mixed_whitespace() {
|
|
||||||
let kinds = tokenize(" \t 42\r\n 84");
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(84.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_whitespace() {
|
|
||||||
let kinds = tokenize(" \n\t \n ");
|
|
||||||
assert!(kinds.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- errors -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unterminated_string() {
|
|
||||||
let (_, errors) = tokenize_full("\"no closing quote");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unterminated"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bare_ampersand() {
|
|
||||||
let (_, errors) = tokenize_full("&");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unexpected '&'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bare_pipe() {
|
|
||||||
let (_, errors) = tokenize_full("|");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unexpected '|'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unexpected_character() {
|
|
||||||
let (_, errors) = tokenize_full("@");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
assert!(errors[0].to_string().contains("Unexpected character"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_does_not_block_rest() {
|
|
||||||
// Bare `&` produces an error, but tokenization continues
|
|
||||||
let (tokens, errors) = tokenize_full("& 42");
|
|
||||||
assert_eq!(errors.len(), 1);
|
|
||||||
let kinds: Vec<_> = tokens.iter().filter(|t| !matches!(t.kind, TokenKind::EOF)).map(|t| t.kind.clone()).collect();
|
|
||||||
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- position tracking -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn positions_single_line() {
|
|
||||||
let (tokens, _) = tokenize_full("let x = 5");
|
|
||||||
// let @ 1:1
|
|
||||||
// x @ 1:5
|
|
||||||
// = @ 1:7
|
|
||||||
// 5 @ 1:9
|
|
||||||
// EOF
|
|
||||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (&t.kind, t.line, t.column)).collect();
|
|
||||||
// Check positions for the first token (let)
|
|
||||||
assert_eq!(token_kinds[0], (&TokenKind::Let, 1, 1));
|
|
||||||
// Check positions for fourth token (5)
|
|
||||||
assert_eq!(token_kinds[3], (&TokenKind::Number(5.0), 1, 9));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn positions_multi_line() {
|
|
||||||
let (tokens, _) = tokenize_full("let\nx\n=");
|
|
||||||
// let @ 1:1
|
|
||||||
// x @ 2:1
|
|
||||||
// = @ 3:1
|
|
||||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
|
||||||
assert_eq!(token_kinds[0], (1, 1)); // let
|
|
||||||
assert_eq!(token_kinds[1], (2, 1)); // x
|
|
||||||
assert_eq!(token_kinds[2], (3, 1)); // =
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn position_after_string() {
|
|
||||||
let (tokens, _) = tokenize_full("\"hi\" 42");
|
|
||||||
// "hi" @ 1:1
|
|
||||||
// 42 @ 1:6 (after "hi" which is 4 chars + space)
|
|
||||||
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
|
||||||
assert_eq!(token_kinds[0], (1, 1)); // "hi" (the opening quote position)
|
|
||||||
assert_eq!(token_kinds[1], (1, 6)); // 42
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- edge cases -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_input() {
|
|
||||||
let kinds = tokenize("");
|
|
||||||
assert!(kinds.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn complex_expression() {
|
|
||||||
let kinds = tokenize("if (x >= 0 && y < 10) { return x + y; }");
|
|
||||||
assert_eq!(kinds, vec![
|
|
||||||
TokenKind::If,
|
|
||||||
TokenKind::LeftParen,
|
|
||||||
TokenKind::Identifier("x".into()),
|
|
||||||
TokenKind::GreaterEqual,
|
|
||||||
TokenKind::Number(0.0),
|
|
||||||
TokenKind::AndAnd,
|
|
||||||
TokenKind::Identifier("y".into()),
|
|
||||||
TokenKind::Less,
|
|
||||||
TokenKind::Number(10.0),
|
|
||||||
TokenKind::RightParen,
|
|
||||||
TokenKind::LeftBrace,
|
|
||||||
TokenKind::Return,
|
|
||||||
TokenKind::Identifier("x".into()),
|
|
||||||
TokenKind::Plus,
|
|
||||||
TokenKind::Identifier("y".into()),
|
|
||||||
TokenKind::Semicolon,
|
|
||||||
TokenKind::RightBrace,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Helper: tokenize input and return only TokenKinds (drop EOF and positions).
|
||||||
|
fn tokenize(input: &str) -> Vec<TokenKind> {
|
||||||
|
let (tokens, _errors) = Lexer::new(input).tokenize();
|
||||||
|
tokens.into_iter().filter_map(|t| {
|
||||||
|
if matches!(t.kind, TokenKind::EOF) { None } else { Some(t.kind) }
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: tokenize and return full tokens (with positions).
|
||||||
|
fn tokenize_full(input: &str) -> (Vec<Token>, Vec<RuntimeError>) {
|
||||||
|
Lexer::new(input).tokenize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- single-character tokens -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_char_tokens() {
|
||||||
|
let kinds = tokenize("(){},;.:");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::LeftParen, TokenKind::RightParen,
|
||||||
|
TokenKind::LeftBrace, TokenKind::RightBrace,
|
||||||
|
TokenKind::Comma, TokenKind::Semicolon,
|
||||||
|
TokenKind::Dot, TokenKind::Colon,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn brackets() {
|
||||||
|
let kinds = tokenize("[]");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::LeftBracket, TokenKind::RightBracket]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- operators -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_operators() {
|
||||||
|
let kinds = tokenize("+ - * / %");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::Plus, TokenKind::Minus, TokenKind::Star,
|
||||||
|
TokenKind::Slash, TokenKind::Percent,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparison_operators() {
|
||||||
|
let kinds = tokenize("< <= > >=");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::Less, TokenKind::LessEqual,
|
||||||
|
TokenKind::Greater, TokenKind::GreaterEqual,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn equality_operators() {
|
||||||
|
let kinds = tokenize("== !=");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::EqualEqual, TokenKind::BangEqual]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_operators() {
|
||||||
|
let kinds = tokenize("&& || !");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::AndAnd, TokenKind::OrOr, TokenKind::Bang]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assignment_operators() {
|
||||||
|
let kinds = tokenize("= += -= *= /= %=");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::Equal,
|
||||||
|
TokenKind::PlusEqual, TokenKind::MinusEqual,
|
||||||
|
TokenKind::StarEqual, TokenKind::SlashEqual,
|
||||||
|
TokenKind::PercentEqual,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn operator_proximity_no_spaces() {
|
||||||
|
let kinds = tokenize("a+b-c*d/e%f");
|
||||||
|
// identifiers a, b, c, d, e, f interleaved with operators
|
||||||
|
let ids: Vec<_> = kinds.iter().filter(|k| matches!(k, TokenKind::Identifier(_))).collect();
|
||||||
|
assert_eq!(ids.len(), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- numbers -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn integer_literal() {
|
||||||
|
let kinds = tokenize("42");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_literal() {
|
||||||
|
let kinds = tokenize("3.14");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(3.14)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_and_negative() {
|
||||||
|
// Note: `-` is a separate unary operator, but `0` should lex correctly
|
||||||
|
let kinds = tokenize("0");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(0.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- strings -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_literal() {
|
||||||
|
let kinds = tokenize("\"hello\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_string() {
|
||||||
|
let kinds = tokenize("\"\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_with_spaces() {
|
||||||
|
let kinds = tokenize("\"hello world\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("hello world".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_strings() {
|
||||||
|
let kinds = tokenize("\"a\" \"b\"");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::String("a".into()),
|
||||||
|
TokenKind::String("b".into()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_newline() {
|
||||||
|
let kinds = tokenize("\"a\\nb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_tab() {
|
||||||
|
let kinds = tokenize("\"a\\tb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\tb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_quote() {
|
||||||
|
let kinds = tokenize("\"he\\\"llo\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_backslash() {
|
||||||
|
let kinds = tokenize("\"a\\\\b\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\\b".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_escape_carriage_return() {
|
||||||
|
let kinds = tokenize("\"a\\rb\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\rb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_unknown_escape_is_error() {
|
||||||
|
let (_, errors) = tokenize_full("\"\\x\"");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unknown escape"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- single-quoted strings -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_quoted_string() {
|
||||||
|
let kinds = tokenize("'hello'");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("hello".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_quoted_empty_string() {
|
||||||
|
let kinds = tokenize("''");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_quoted_with_escape() {
|
||||||
|
let kinds = tokenize("'a\\'b'");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a'b".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_quoted_with_newline_escape() {
|
||||||
|
let kinds = tokenize("'a\\nb'");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("a\nb".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_quote_strings() {
|
||||||
|
let kinds = tokenize("\"double\" 'single'");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::String("double".into()),
|
||||||
|
TokenKind::String("single".into()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_quote_can_contain_double_quote() {
|
||||||
|
let kinds = tokenize("'he\"llo'");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("he\"llo".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_quote_can_contain_single_quote() {
|
||||||
|
let kinds = tokenize("\"it's\"");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::String("it's".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unterminated_single_quoted_string() {
|
||||||
|
let (_, errors) = tokenize_full("'unterminated");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unterminated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- identifiers and keywords -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identifiers() {
|
||||||
|
let kinds = tokenize("foo bar _private x1");
|
||||||
|
let ids: Vec<_> = kinds.iter().filter_map(|k| {
|
||||||
|
if let TokenKind::Identifier(s) = k { Some(s.clone()) } else { None }
|
||||||
|
}).collect();
|
||||||
|
assert_eq!(ids, vec!["foo", "bar", "_private", "x1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keywords() {
|
||||||
|
let kinds = tokenize("let const fn if else while for in break continue return true false nil");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::Let, TokenKind::Const, TokenKind::Fn,
|
||||||
|
TokenKind::If, TokenKind::Else,
|
||||||
|
TokenKind::While, TokenKind::For, TokenKind::In,
|
||||||
|
TokenKind::Break, TokenKind::Continue,
|
||||||
|
TokenKind::Return,
|
||||||
|
TokenKind::True, TokenKind::False,
|
||||||
|
TokenKind::Nil,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identifier_looks_like_keyword_prefix() {
|
||||||
|
// "lettuce" should be identifier, not "let" + error
|
||||||
|
let kinds = tokenize("lettuce");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Identifier("lettuce".into())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- comments -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_comment_ignored() {
|
||||||
|
let kinds = tokenize("// this is a comment\n42");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comment_at_end_of_file() {
|
||||||
|
let kinds = tokenize("42 // trailing comment");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comment_only_line() {
|
||||||
|
let kinds = tokenize("// just a comment");
|
||||||
|
assert!(kinds.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comment_between_tokens() {
|
||||||
|
let kinds = tokenize("1 // comment\n2");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(1.0), TokenKind::Number(2.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- whitespace -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_whitespace() {
|
||||||
|
let kinds = tokenize(" \t 42\r\n 84");
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(42.0), TokenKind::Number(84.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_whitespace() {
|
||||||
|
let kinds = tokenize(" \n\t \n ");
|
||||||
|
assert!(kinds.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- errors -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unterminated_string() {
|
||||||
|
let (_, errors) = tokenize_full("\"no closing quote");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unterminated"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_ampersand() {
|
||||||
|
let (_, errors) = tokenize_full("&");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unexpected '&'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_pipe() {
|
||||||
|
let (_, errors) = tokenize_full("|");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unexpected '|'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unexpected_character() {
|
||||||
|
let (_, errors) = tokenize_full("@");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
assert!(errors[0].to_string().contains("Unexpected character"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_does_not_block_rest() {
|
||||||
|
// Bare `&` produces an error, but tokenization continues
|
||||||
|
let (tokens, errors) = tokenize_full("& 42");
|
||||||
|
assert_eq!(errors.len(), 1);
|
||||||
|
let kinds: Vec<_> = tokens.iter().filter(|t| !matches!(t.kind, TokenKind::EOF)).map(|t| t.kind.clone()).collect();
|
||||||
|
assert_eq!(kinds, vec![TokenKind::Number(42.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- position tracking -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positions_single_line() {
|
||||||
|
let (tokens, _) = tokenize_full("let x = 5");
|
||||||
|
// let @ 1:1
|
||||||
|
// x @ 1:5
|
||||||
|
// = @ 1:7
|
||||||
|
// 5 @ 1:9
|
||||||
|
// EOF
|
||||||
|
let token_kinds: Vec<_> = tokens.iter().map(|t| (&t.kind, t.line, t.column)).collect();
|
||||||
|
// Check positions for the first token (let)
|
||||||
|
assert_eq!(token_kinds[0], (&TokenKind::Let, 1, 1));
|
||||||
|
// Check positions for fourth token (5)
|
||||||
|
assert_eq!(token_kinds[3], (&TokenKind::Number(5.0), 1, 9));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positions_multi_line() {
|
||||||
|
let (tokens, _) = tokenize_full("let\nx\n=");
|
||||||
|
// let @ 1:1
|
||||||
|
// x @ 2:1
|
||||||
|
// = @ 3:1
|
||||||
|
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
||||||
|
assert_eq!(token_kinds[0], (1, 1)); // let
|
||||||
|
assert_eq!(token_kinds[1], (2, 1)); // x
|
||||||
|
assert_eq!(token_kinds[2], (3, 1)); // =
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn position_after_string() {
|
||||||
|
let (tokens, _) = tokenize_full("\"hi\" 42");
|
||||||
|
// "hi" @ 1:1
|
||||||
|
// 42 @ 1:6 (after "hi" which is 4 chars + space)
|
||||||
|
let token_kinds: Vec<_> = tokens.iter().map(|t| (t.line, t.column)).collect();
|
||||||
|
assert_eq!(token_kinds[0], (1, 1)); // "hi" (the opening quote position)
|
||||||
|
assert_eq!(token_kinds[1], (1, 6)); // 42
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- edge cases -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_input() {
|
||||||
|
let kinds = tokenize("");
|
||||||
|
assert!(kinds.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_expression() {
|
||||||
|
let kinds = tokenize("if (x >= 0 && y < 10) { return x + y; }");
|
||||||
|
assert_eq!(kinds, vec![
|
||||||
|
TokenKind::If,
|
||||||
|
TokenKind::LeftParen,
|
||||||
|
TokenKind::Identifier("x".into()),
|
||||||
|
TokenKind::GreaterEqual,
|
||||||
|
TokenKind::Number(0.0),
|
||||||
|
TokenKind::AndAnd,
|
||||||
|
TokenKind::Identifier("y".into()),
|
||||||
|
TokenKind::Less,
|
||||||
|
TokenKind::Number(10.0),
|
||||||
|
TokenKind::RightParen,
|
||||||
|
TokenKind::LeftBrace,
|
||||||
|
TokenKind::Return,
|
||||||
|
TokenKind::Identifier("x".into()),
|
||||||
|
TokenKind::Plus,
|
||||||
|
TokenKind::Identifier("y".into()),
|
||||||
|
TokenKind::Semicolon,
|
||||||
|
TokenKind::RightBrace,
|
||||||
|
]);
|
||||||
|
}
|
||||||
+2
-826
@@ -617,830 +617,6 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
|
||||||
// Tests
|
|
||||||
// ========================================================================
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
use crate::lexer::Lexer;
|
|
||||||
|
|
||||||
/// Helper: lex + parse, return (stmts, errors).
|
|
||||||
fn parse(input: &str) -> (Vec<Stmt>, Vec<RuntimeError>) {
|
|
||||||
let (tokens, _) = Lexer::new(input).tokenize();
|
|
||||||
Parser::new(tokens).parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: lex + parse single expression (wraps in `let _ = <expr>;`).
|
|
||||||
fn parse_expr(input: &str) -> Expr {
|
|
||||||
let (stmts, errors) = parse(&format!("let __x = {};", input));
|
|
||||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
|
||||||
match &stmts[0] {
|
|
||||||
Stmt::Let { initializer, .. } => initializer.clone(),
|
|
||||||
_ => panic!("Expected Let statement"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: lex + parse single statement.
|
|
||||||
fn parse_stmt(input: &str) -> Stmt {
|
|
||||||
let (stmts, errors) = parse(input);
|
|
||||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
|
||||||
assert_eq!(stmts.len(), 1);
|
|
||||||
stmts[0].clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- literals -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_number() {
|
|
||||||
match parse_expr("42") {
|
|
||||||
Expr::Literal(Literal::Number(42.0)) => {}
|
|
||||||
e => panic!("Expected Number(42.0), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_float() {
|
|
||||||
match parse_expr("3.14") {
|
|
||||||
Expr::Literal(Literal::Number(n)) => assert!((n - 3.14).abs() < 0.001),
|
|
||||||
e => panic!("Expected Number, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_string() {
|
|
||||||
match parse_expr("\"hello\"") {
|
|
||||||
Expr::Literal(Literal::String(s)) => assert_eq!(s, "hello"),
|
|
||||||
e => panic!("Expected String, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_bool_true() {
|
|
||||||
match parse_expr("true") {
|
|
||||||
Expr::Literal(Literal::Bool(true)) => {}
|
|
||||||
e => panic!("Expected Bool(true), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_bool_false() {
|
|
||||||
match parse_expr("false") {
|
|
||||||
Expr::Literal(Literal::Bool(false)) => {}
|
|
||||||
e => panic!("Expected Bool(false), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_nil() {
|
|
||||||
match parse_expr("nil") {
|
|
||||||
Expr::Literal(Literal::Nil) => {}
|
|
||||||
e => panic!("Expected Nil, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- variables -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_variable() {
|
|
||||||
match parse_expr("x") {
|
|
||||||
Expr::Variable(name) => assert_eq!(name, "x"),
|
|
||||||
e => panic!("Expected Variable, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- unary -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_negation() {
|
|
||||||
match parse_expr("-5") {
|
|
||||||
Expr::Unary { op: UnaryOp::Negate, right } => {
|
|
||||||
match *right {
|
|
||||||
Expr::Literal(Literal::Number(5.0)) => {}
|
|
||||||
e => panic!("Expected Number(5), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Unary(Negate, Number), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_not() {
|
|
||||||
match parse_expr("!true") {
|
|
||||||
Expr::Unary { op: UnaryOp::Not, right } => {
|
|
||||||
match *right {
|
|
||||||
Expr::Literal(Literal::Bool(true)) => {}
|
|
||||||
e => panic!("Expected Bool(true), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Unary(Not, Bool), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_double_negation() {
|
|
||||||
match parse_expr("!!true") {
|
|
||||||
Expr::Unary { op: UnaryOp::Not, right } => {
|
|
||||||
match *right {
|
|
||||||
Expr::Unary { op: UnaryOp::Not, .. } => {}
|
|
||||||
e => panic!("Expected nested Unary, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Unary(Not, ...), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- binary precedence -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_addition() {
|
|
||||||
match parse_expr("1 + 2") {
|
|
||||||
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
|
||||||
match (*left, *right) {
|
|
||||||
(Expr::Literal(Literal::Number(1.0)), Expr::Literal(Literal::Number(2.0))) => {}
|
|
||||||
e => panic!("Expected (1, 2), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_multiplication_precedence() {
|
|
||||||
// 1 + 2 * 3 => 1 + (2 * 3)
|
|
||||||
match parse_expr("1 + 2 * 3") {
|
|
||||||
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
|
||||||
match *left {
|
|
||||||
Expr::Literal(Literal::Number(1.0)) => {}
|
|
||||||
e => panic!("Expected left=1, got {:?}", e),
|
|
||||||
}
|
|
||||||
match *right {
|
|
||||||
Expr::Binary { op: BinaryOp::Mul, .. } => {}
|
|
||||||
e => panic!("Expected right=Binary(Mul), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_modulo() {
|
|
||||||
match parse_expr("7 % 3") {
|
|
||||||
Expr::Binary { op: BinaryOp::Mod, .. } => {}
|
|
||||||
e => panic!("Expected Binary(Mod), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- comparison -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_comparison_chain() {
|
|
||||||
// a < b == c
|
|
||||||
match parse_expr("a < b == c") {
|
|
||||||
Expr::Binary { op: BinaryOp::Equal, left, right } => {
|
|
||||||
match *left {
|
|
||||||
Expr::Binary { op: BinaryOp::Less, .. } => {}
|
|
||||||
e => panic!("Expected left=Binary(Less), got {:?}", e),
|
|
||||||
}
|
|
||||||
match *right {
|
|
||||||
Expr::Variable(name) => assert_eq!(name, "c"),
|
|
||||||
e => panic!("Expected right=Variable(c), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Binary(Equal), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- logical -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_logical_and() {
|
|
||||||
match parse_expr("true && false") {
|
|
||||||
Expr::Logical { op: LogicalOp::And, .. } => {}
|
|
||||||
e => panic!("Expected Logical(And), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_logical_or() {
|
|
||||||
match parse_expr("true || false") {
|
|
||||||
Expr::Logical { op: LogicalOp::Or, .. } => {}
|
|
||||||
e => panic!("Expected Logical(Or), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_logical_precedence() {
|
|
||||||
// a || b && c => a || (b && c)
|
|
||||||
match parse_expr("a || b && c") {
|
|
||||||
Expr::Logical { op: LogicalOp::Or, left, right } => {
|
|
||||||
match *left {
|
|
||||||
Expr::Variable(name) => assert_eq!(name, "a"),
|
|
||||||
e => panic!("Expected left=a, got {:?}", e),
|
|
||||||
}
|
|
||||||
match *right {
|
|
||||||
Expr::Logical { op: LogicalOp::And, .. } => {}
|
|
||||||
e => panic!("Expected right=Logical(And), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Logical(Or), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- ternary -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_ternary_simple() {
|
|
||||||
match parse_expr("a ? 1 : 2") {
|
|
||||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
|
||||||
assert!(matches!(*condition, Expr::Variable(_)));
|
|
||||||
assert!(matches!(*then_branch, Expr::Literal(Literal::Number(1.0))));
|
|
||||||
assert!(matches!(*else_branch, Expr::Literal(Literal::Number(2.0))));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Ternary, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_ternary_nested_right_assoc() {
|
|
||||||
// a ? b : c ? d : e → a ? b : (c ? d : e)
|
|
||||||
match parse_expr("a ? b : c ? d : e") {
|
|
||||||
Expr::Ternary { condition, then_branch, else_branch } => {
|
|
||||||
assert!(matches!(*condition, Expr::Variable(_)));
|
|
||||||
assert!(matches!(*then_branch, Expr::Variable(_)));
|
|
||||||
assert!(matches!(*else_branch, Expr::Ternary { .. }));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Ternary with nested Ternary else, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_ternary_with_assignment_in_else() {
|
|
||||||
// a ? b : c = d → a ? b : (c = d)
|
|
||||||
match parse_expr("a ? b : c = 5") {
|
|
||||||
Expr::Ternary { else_branch, .. } => {
|
|
||||||
assert!(matches!(*else_branch, Expr::Assign { .. }));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Ternary with Assign else, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- assignment -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_simple_assignment() {
|
|
||||||
match parse_expr("x = 5") {
|
|
||||||
Expr::Assign { name, op: AssignOp::Equal, value } => {
|
|
||||||
assert_eq!(name, "x");
|
|
||||||
match *value {
|
|
||||||
Expr::Literal(Literal::Number(5.0)) => {}
|
|
||||||
e => panic!("Expected Number(5), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Assign, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_compound_assignment() {
|
|
||||||
match parse_expr("x += 2") {
|
|
||||||
Expr::Assign { name, op: AssignOp::PlusEqual, value } => {
|
|
||||||
assert_eq!(name, "x");
|
|
||||||
match *value {
|
|
||||||
Expr::Literal(Literal::Number(2.0)) => {}
|
|
||||||
e => panic!("Expected Number(2), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Assign(PlusEqual), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_all_compound_ops() {
|
|
||||||
let ops = [
|
|
||||||
("+=", AssignOp::PlusEqual),
|
|
||||||
("-=", AssignOp::MinusEqual),
|
|
||||||
("*=", AssignOp::StarEqual),
|
|
||||||
("/=", AssignOp::SlashEqual),
|
|
||||||
("%=", AssignOp::PercentEqual),
|
|
||||||
];
|
|
||||||
for (op_str, expected_op) in &ops {
|
|
||||||
let expr = parse_expr(&format!("x {} 1", op_str));
|
|
||||||
match expr {
|
|
||||||
Expr::Assign { op, .. } => {
|
|
||||||
assert_eq!(std::mem::discriminant(&op), std::mem::discriminant(expected_op));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Assign for '{}', got {:?}", op_str, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- property access -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_property_get() {
|
|
||||||
match parse_expr("obj.name") {
|
|
||||||
Expr::Get { object, name } => {
|
|
||||||
match *object {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "obj"),
|
|
||||||
e => panic!("Expected Variable(obj), got {:?}", e),
|
|
||||||
}
|
|
||||||
assert_eq!(name, "name");
|
|
||||||
}
|
|
||||||
e => panic!("Expected Get, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_property_set() {
|
|
||||||
match parse_expr("obj.name = 5") {
|
|
||||||
Expr::Set { object, name, op: AssignOp::Equal, value } => {
|
|
||||||
match *object {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "obj"),
|
|
||||||
e => panic!("Expected Variable(obj), got {:?}", e),
|
|
||||||
}
|
|
||||||
assert_eq!(name, "name");
|
|
||||||
match *value {
|
|
||||||
Expr::Literal(Literal::Number(5.0)) => {}
|
|
||||||
e => panic!("Expected Number(5), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Set, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_chained_property_access() {
|
|
||||||
match parse_expr("a.b.c") {
|
|
||||||
Expr::Get { object, name } => {
|
|
||||||
assert_eq!(name, "c");
|
|
||||||
match *object {
|
|
||||||
Expr::Get { name: ref inner, .. } => assert_eq!(inner, "b"),
|
|
||||||
e => panic!("Expected chained Get, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Get, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- index access -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_index_get() {
|
|
||||||
match parse_expr("arr[0]") {
|
|
||||||
Expr::IndexGet { array, index } => {
|
|
||||||
match *array {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "arr"),
|
|
||||||
e => panic!("Expected Variable(arr), got {:?}", e),
|
|
||||||
}
|
|
||||||
match *index {
|
|
||||||
Expr::Literal(Literal::Number(0.0)) => {}
|
|
||||||
e => panic!("Expected Number(0), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected IndexGet, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_index_set() {
|
|
||||||
match parse_expr("arr[i] = 42") {
|
|
||||||
Expr::IndexSet { array, index, op: AssignOp::Equal, value } => {
|
|
||||||
match *array {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "arr"),
|
|
||||||
e => panic!("Expected Variable(arr), got {:?}", e),
|
|
||||||
}
|
|
||||||
match *index {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "i"),
|
|
||||||
e => panic!("Expected Variable(i), got {:?}", e),
|
|
||||||
}
|
|
||||||
match *value {
|
|
||||||
Expr::Literal(Literal::Number(42.0)) => {}
|
|
||||||
e => panic!("Expected Number(42), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected IndexSet, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_nested_index() {
|
|
||||||
match parse_expr("a[1][2]") {
|
|
||||||
Expr::IndexGet { array, .. } => {
|
|
||||||
match *array {
|
|
||||||
Expr::IndexGet { .. } => {}
|
|
||||||
e => panic!("Expected nested IndexGet, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected IndexGet, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- function calls -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_call_no_args() {
|
|
||||||
match parse_expr("f()") {
|
|
||||||
Expr::Call { callee, arguments } => {
|
|
||||||
match *callee {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "f"),
|
|
||||||
e => panic!("Expected Variable(f), got {:?}", e),
|
|
||||||
}
|
|
||||||
assert!(arguments.is_empty());
|
|
||||||
}
|
|
||||||
e => panic!("Expected Call, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_call_with_args() {
|
|
||||||
match parse_expr("add(1, 2, 3)") {
|
|
||||||
Expr::Call { callee, arguments } => {
|
|
||||||
match *callee {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "add"),
|
|
||||||
e => panic!("Expected Variable(add), got {:?}", e),
|
|
||||||
}
|
|
||||||
assert_eq!(arguments.len(), 3);
|
|
||||||
}
|
|
||||||
e => panic!("Expected Call, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_chained_calls() {
|
|
||||||
match parse_expr("f()()") {
|
|
||||||
Expr::Call { callee, .. } => {
|
|
||||||
match *callee {
|
|
||||||
Expr::Call { callee, .. } => {
|
|
||||||
match *callee {
|
|
||||||
Expr::Variable(n) => assert_eq!(n, "f"),
|
|
||||||
e => panic!("Expected Variable(f), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected inner Call, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Call, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_method_style_call() {
|
|
||||||
// obj.method()
|
|
||||||
match parse_expr("obj.method()") {
|
|
||||||
Expr::Call { callee, .. } => {
|
|
||||||
match *callee {
|
|
||||||
Expr::Get { .. } => {}
|
|
||||||
e => panic!("Expected Get, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected Call, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- lambda -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_simple_lambda() {
|
|
||||||
match parse_expr("fn(x, y) { return x + y; }") {
|
|
||||||
Expr::Lambda { params, body } => {
|
|
||||||
assert_eq!(params, vec!["x", "y"]);
|
|
||||||
assert_eq!(body.len(), 1);
|
|
||||||
}
|
|
||||||
e => panic!("Expected Lambda, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_lambda_no_params() {
|
|
||||||
match parse_expr("fn() { return 42; }") {
|
|
||||||
Expr::Lambda { params, body } => {
|
|
||||||
assert!(params.is_empty());
|
|
||||||
assert_eq!(body.len(), 1);
|
|
||||||
}
|
|
||||||
e => panic!("Expected Lambda, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- object literal -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_empty_object() {
|
|
||||||
match parse_expr("{}") {
|
|
||||||
Expr::ObjectLiteral { properties } => assert!(properties.is_empty()),
|
|
||||||
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_object_with_properties() {
|
|
||||||
match parse_expr("{ name: \"aster\", version: 1 }") {
|
|
||||||
Expr::ObjectLiteral { properties } => {
|
|
||||||
assert_eq!(properties.len(), 2);
|
|
||||||
assert_eq!(properties[0].0, "name");
|
|
||||||
assert_eq!(properties[1].0, "version");
|
|
||||||
}
|
|
||||||
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- array literal -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_empty_array() {
|
|
||||||
match parse_expr("[]") {
|
|
||||||
Expr::ArrayLiteral { elements } => assert!(elements.is_empty()),
|
|
||||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_array_with_elements() {
|
|
||||||
match parse_expr("[1, 2, 3]") {
|
|
||||||
Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3),
|
|
||||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_nested_arrays() {
|
|
||||||
match parse_expr("[[1, 2], [3, 4]]") {
|
|
||||||
Expr::ArrayLiteral { elements } => {
|
|
||||||
assert_eq!(elements.len(), 2);
|
|
||||||
for e in &elements {
|
|
||||||
assert!(matches!(e, Expr::ArrayLiteral { .. }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- statements -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_let_statement() {
|
|
||||||
match parse_stmt("let x = 5;") {
|
|
||||||
Stmt::Let { name, initializer, .. } => {
|
|
||||||
assert_eq!(name, "x");
|
|
||||||
assert!(matches!(initializer, Expr::Literal(Literal::Number(5.0))));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Let, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_let_without_semicolon() {
|
|
||||||
// Semicolons are optional
|
|
||||||
match parse_stmt("let x = 5") {
|
|
||||||
Stmt::Let { name, .. } => assert_eq!(name, "x"),
|
|
||||||
e => panic!("Expected Let, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_const_statement() {
|
|
||||||
match parse_stmt("const x = 5;") {
|
|
||||||
Stmt::Let { name, mutable, .. } => {
|
|
||||||
assert_eq!(name, "x");
|
|
||||||
assert!(!mutable, "const should set mutable=false");
|
|
||||||
}
|
|
||||||
e => panic!("Expected Let (const), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_expression_statement() {
|
|
||||||
match parse_stmt("x;") {
|
|
||||||
Stmt::ExprStmt(Expr::Variable(name)) => assert_eq!(name, "x"),
|
|
||||||
e => panic!("Expected ExprStmt(Variable), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_if_statement() {
|
|
||||||
match parse_stmt("if (x) { 1; }") {
|
|
||||||
Stmt::If { condition, then_branch, else_branch } => {
|
|
||||||
assert!(matches!(condition, Expr::Variable(_)));
|
|
||||||
assert!(matches!(*then_branch, Stmt::Block(_)));
|
|
||||||
assert!(else_branch.is_none());
|
|
||||||
}
|
|
||||||
e => panic!("Expected If, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_if_else_statement() {
|
|
||||||
match parse_stmt("if (x) { 1; } else { 2; }") {
|
|
||||||
Stmt::If { else_branch, .. } => {
|
|
||||||
assert!(else_branch.is_some());
|
|
||||||
}
|
|
||||||
e => panic!("Expected If with else, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_while_statement() {
|
|
||||||
match parse_stmt("while (x < 10) { x = x + 1; }") {
|
|
||||||
Stmt::While { condition, body } => {
|
|
||||||
assert!(matches!(condition, Expr::Binary { .. }));
|
|
||||||
assert!(matches!(*body, Stmt::Block(_)));
|
|
||||||
}
|
|
||||||
e => panic!("Expected While, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_statement() {
|
|
||||||
match parse_stmt("for (let i = 0; i < 10; i = i + 1) { }") {
|
|
||||||
Stmt::For { initializer, condition, step, body } => {
|
|
||||||
assert!(initializer.is_some());
|
|
||||||
assert!(condition.is_some());
|
|
||||||
assert!(step.is_some());
|
|
||||||
assert!(matches!(*body, Stmt::Block(_)));
|
|
||||||
}
|
|
||||||
e => panic!("Expected For, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_empty_clauses() {
|
|
||||||
match parse_stmt("for (;;) { }") {
|
|
||||||
Stmt::For { initializer, condition, step, .. } => {
|
|
||||||
assert!(initializer.is_none());
|
|
||||||
assert!(condition.is_none());
|
|
||||||
assert!(step.is_none());
|
|
||||||
}
|
|
||||||
e => panic!("Expected For, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_in_array() {
|
|
||||||
match parse_stmt("for (x in arr) { print(x); }") {
|
|
||||||
Stmt::ForIn { var_name, iterable, body } => {
|
|
||||||
assert_eq!(var_name, "x");
|
|
||||||
assert!(matches!(iterable, Expr::Variable(_)));
|
|
||||||
assert!(matches!(*body, Stmt::Block(_)));
|
|
||||||
}
|
|
||||||
e => panic!("Expected ForIn, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_in_with_object_literal() {
|
|
||||||
match parse_stmt("for (k in {a:1,b:2}) { print(k); }") {
|
|
||||||
Stmt::ForIn { var_name, iterable, .. } => {
|
|
||||||
assert_eq!(var_name, "k");
|
|
||||||
assert!(matches!(iterable, Expr::ObjectLiteral { .. }));
|
|
||||||
}
|
|
||||||
e => panic!("Expected ForIn, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_in_with_expression() {
|
|
||||||
match parse_stmt("for (v in get_list()) { }") {
|
|
||||||
Stmt::ForIn { var_name, .. } => {
|
|
||||||
assert_eq!(var_name, "v");
|
|
||||||
}
|
|
||||||
e => panic!("Expected ForIn, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_for_in_distinct_from_c_style() {
|
|
||||||
// "for (x = 0; ...)" is C-style for, not for-in
|
|
||||||
match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") {
|
|
||||||
Stmt::For { .. } => {}
|
|
||||||
e => panic!("Expected C-style For, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_block() {
|
|
||||||
match parse_stmt("{ let x = 1; x; }") {
|
|
||||||
Stmt::Block(stmts) => assert_eq!(stmts.len(), 2),
|
|
||||||
e => panic!("Expected Block, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_return_statement() {
|
|
||||||
match parse_stmt("return 42;") {
|
|
||||||
Stmt::Return(Some(expr)) => {
|
|
||||||
assert!(matches!(expr, Expr::Literal(Literal::Number(42.0))));
|
|
||||||
}
|
|
||||||
e => panic!("Expected Return(42), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_return_nil() {
|
|
||||||
match parse_stmt("return;") {
|
|
||||||
Stmt::Return(None) => {}
|
|
||||||
e => panic!("Expected Return(None), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_break_statement() {
|
|
||||||
match parse_stmt("break;") {
|
|
||||||
Stmt::Break => {}
|
|
||||||
e => panic!("Expected Break, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_continue_statement() {
|
|
||||||
match parse_stmt("continue;") {
|
|
||||||
Stmt::Continue => {}
|
|
||||||
e => panic!("Expected Continue, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_function_declaration() {
|
|
||||||
match parse_stmt("fn add(a, b) { return a + b; }") {
|
|
||||||
Stmt::Function { name, params, body } => {
|
|
||||||
assert_eq!(name, "add");
|
|
||||||
assert_eq!(params, vec!["a", "b"]);
|
|
||||||
assert_eq!(body.len(), 1);
|
|
||||||
}
|
|
||||||
e => panic!("Expected Function, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_function_no_params() {
|
|
||||||
match parse_stmt("fn greet() { print(\"hi\"); }") {
|
|
||||||
Stmt::Function { name, params, .. } => {
|
|
||||||
assert_eq!(name, "greet");
|
|
||||||
assert!(params.is_empty());
|
|
||||||
}
|
|
||||||
e => panic!("Expected Function, got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- error recovery -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_recovery_skips_past_semicolon() {
|
|
||||||
// First statement is invalid, second should parse fine
|
|
||||||
let (stmts, errors) = parse("let = 5; let y = 10;");
|
|
||||||
assert!(!errors.is_empty(), "Should have parse errors");
|
|
||||||
// Should have at least the second statement
|
|
||||||
assert!(stmts.len() >= 1);
|
|
||||||
// The second statement should be a valid Let for y
|
|
||||||
match &stmts.last().unwrap() {
|
|
||||||
Stmt::Let { name, .. } => assert_eq!(name, "y"),
|
|
||||||
e => panic!("Expected Let(y), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_recovery_at_statement_boundary() {
|
|
||||||
let (stmts, errors) = parse("let x = @@@; let y = 10;");
|
|
||||||
assert!(!errors.is_empty());
|
|
||||||
assert!(stmts.len() >= 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- parse errors -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_invalid_assignment_target() {
|
|
||||||
let (_stmts, errors) = parse("5 = 10;");
|
|
||||||
assert!(!errors.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_missing_closing_paren() {
|
|
||||||
let (_stmts, errors) = parse("if (x { 1; }");
|
|
||||||
assert!(!errors.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn error_missing_semicolon_after_for_condition() {
|
|
||||||
// for(;;){} is valid — test something actually invalid
|
|
||||||
let (_stmts, _errors) = parse("for (let i = 0 i < 10) { }");
|
|
||||||
assert!(!_errors.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- parenthesized expression -----
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_parenthesized_expression() {
|
|
||||||
match parse_expr("(1 + 2)") {
|
|
||||||
Expr::Binary { op: BinaryOp::Add, .. } => {}
|
|
||||||
e => panic!("Expected Binary(Add), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_nested_parentheses() {
|
|
||||||
match parse_expr("((42))") {
|
|
||||||
Expr::Literal(Literal::Number(42.0)) => {}
|
|
||||||
e => panic!("Expected Number(42), got {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,820 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::lexer::Lexer;
|
||||||
|
|
||||||
|
/// Helper: lex + parse, return (stmts, errors).
|
||||||
|
fn parse(input: &str) -> (Vec<Stmt>, Vec<RuntimeError>) {
|
||||||
|
let (tokens, _) = Lexer::new(input).tokenize();
|
||||||
|
Parser::new(tokens).parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: lex + parse single expression (wraps in `let _ = <expr>;`).
|
||||||
|
fn parse_expr(input: &str) -> Expr {
|
||||||
|
let (stmts, errors) = parse(&format!("let __x = {};", input));
|
||||||
|
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||||
|
match &stmts[0] {
|
||||||
|
Stmt::Let { initializer, .. } => initializer.clone(),
|
||||||
|
_ => panic!("Expected Let statement"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper: lex + parse single statement.
|
||||||
|
fn parse_stmt(input: &str) -> Stmt {
|
||||||
|
let (stmts, errors) = parse(input);
|
||||||
|
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||||
|
assert_eq!(stmts.len(), 1);
|
||||||
|
stmts[0].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- literals -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_number() {
|
||||||
|
match parse_expr("42") {
|
||||||
|
Expr::Literal(Literal::Number(42.0)) => {}
|
||||||
|
e => panic!("Expected Number(42.0), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_float() {
|
||||||
|
match parse_expr("3.14") {
|
||||||
|
Expr::Literal(Literal::Number(n)) => assert!((n - 3.14).abs() < 0.001),
|
||||||
|
e => panic!("Expected Number, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_string() {
|
||||||
|
match parse_expr("\"hello\"") {
|
||||||
|
Expr::Literal(Literal::String(s)) => assert_eq!(s, "hello"),
|
||||||
|
e => panic!("Expected String, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_bool_true() {
|
||||||
|
match parse_expr("true") {
|
||||||
|
Expr::Literal(Literal::Bool(true)) => {}
|
||||||
|
e => panic!("Expected Bool(true), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_bool_false() {
|
||||||
|
match parse_expr("false") {
|
||||||
|
Expr::Literal(Literal::Bool(false)) => {}
|
||||||
|
e => panic!("Expected Bool(false), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nil() {
|
||||||
|
match parse_expr("nil") {
|
||||||
|
Expr::Literal(Literal::Nil) => {}
|
||||||
|
e => panic!("Expected Nil, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- variables -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_variable() {
|
||||||
|
match parse_expr("x") {
|
||||||
|
Expr::Variable(name) => assert_eq!(name, "x"),
|
||||||
|
e => panic!("Expected Variable, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- unary -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_negation() {
|
||||||
|
match parse_expr("-5") {
|
||||||
|
Expr::Unary { op: UnaryOp::Negate, right } => {
|
||||||
|
match *right {
|
||||||
|
Expr::Literal(Literal::Number(5.0)) => {}
|
||||||
|
e => panic!("Expected Number(5), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Unary(Negate, Number), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_not() {
|
||||||
|
match parse_expr("!true") {
|
||||||
|
Expr::Unary { op: UnaryOp::Not, right } => {
|
||||||
|
match *right {
|
||||||
|
Expr::Literal(Literal::Bool(true)) => {}
|
||||||
|
e => panic!("Expected Bool(true), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Unary(Not, Bool), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_double_negation() {
|
||||||
|
match parse_expr("!!true") {
|
||||||
|
Expr::Unary { op: UnaryOp::Not, right } => {
|
||||||
|
match *right {
|
||||||
|
Expr::Unary { op: UnaryOp::Not, .. } => {}
|
||||||
|
e => panic!("Expected nested Unary, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Unary(Not, ...), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- binary precedence -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_addition() {
|
||||||
|
match parse_expr("1 + 2") {
|
||||||
|
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
||||||
|
match (*left, *right) {
|
||||||
|
(Expr::Literal(Literal::Number(1.0)), Expr::Literal(Literal::Number(2.0))) => {}
|
||||||
|
e => panic!("Expected (1, 2), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multiplication_precedence() {
|
||||||
|
// 1 + 2 * 3 => 1 + (2 * 3)
|
||||||
|
match parse_expr("1 + 2 * 3") {
|
||||||
|
Expr::Binary { op: BinaryOp::Add, left, right } => {
|
||||||
|
match *left {
|
||||||
|
Expr::Literal(Literal::Number(1.0)) => {}
|
||||||
|
e => panic!("Expected left=1, got {:?}", e),
|
||||||
|
}
|
||||||
|
match *right {
|
||||||
|
Expr::Binary { op: BinaryOp::Mul, .. } => {}
|
||||||
|
e => panic!("Expected right=Binary(Mul), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_modulo() {
|
||||||
|
match parse_expr("7 % 3") {
|
||||||
|
Expr::Binary { op: BinaryOp::Mod, .. } => {}
|
||||||
|
e => panic!("Expected Binary(Mod), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- comparison -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_comparison_chain() {
|
||||||
|
// a < b == c
|
||||||
|
match parse_expr("a < b == c") {
|
||||||
|
Expr::Binary { op: BinaryOp::Equal, left, right } => {
|
||||||
|
match *left {
|
||||||
|
Expr::Binary { op: BinaryOp::Less, .. } => {}
|
||||||
|
e => panic!("Expected left=Binary(Less), got {:?}", e),
|
||||||
|
}
|
||||||
|
match *right {
|
||||||
|
Expr::Variable(name) => assert_eq!(name, "c"),
|
||||||
|
e => panic!("Expected right=Variable(c), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Binary(Equal), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- logical -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_logical_and() {
|
||||||
|
match parse_expr("true && false") {
|
||||||
|
Expr::Logical { op: LogicalOp::And, .. } => {}
|
||||||
|
e => panic!("Expected Logical(And), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_logical_or() {
|
||||||
|
match parse_expr("true || false") {
|
||||||
|
Expr::Logical { op: LogicalOp::Or, .. } => {}
|
||||||
|
e => panic!("Expected Logical(Or), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_logical_precedence() {
|
||||||
|
// a || b && c => a || (b && c)
|
||||||
|
match parse_expr("a || b && c") {
|
||||||
|
Expr::Logical { op: LogicalOp::Or, left, right } => {
|
||||||
|
match *left {
|
||||||
|
Expr::Variable(name) => assert_eq!(name, "a"),
|
||||||
|
e => panic!("Expected left=a, got {:?}", e),
|
||||||
|
}
|
||||||
|
match *right {
|
||||||
|
Expr::Logical { op: LogicalOp::And, .. } => {}
|
||||||
|
e => panic!("Expected right=Logical(And), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Logical(Or), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- ternary -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ternary_simple() {
|
||||||
|
match parse_expr("a ? 1 : 2") {
|
||||||
|
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||||
|
assert!(matches!(*condition, Expr::Variable(_)));
|
||||||
|
assert!(matches!(*then_branch, Expr::Literal(Literal::Number(1.0))));
|
||||||
|
assert!(matches!(*else_branch, Expr::Literal(Literal::Number(2.0))));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Ternary, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ternary_nested_right_assoc() {
|
||||||
|
// a ? b : c ? d : e → a ? b : (c ? d : e)
|
||||||
|
match parse_expr("a ? b : c ? d : e") {
|
||||||
|
Expr::Ternary { condition, then_branch, else_branch } => {
|
||||||
|
assert!(matches!(*condition, Expr::Variable(_)));
|
||||||
|
assert!(matches!(*then_branch, Expr::Variable(_)));
|
||||||
|
assert!(matches!(*else_branch, Expr::Ternary { .. }));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Ternary with nested Ternary else, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_ternary_with_assignment_in_else() {
|
||||||
|
// a ? b : c = d → a ? b : (c = d)
|
||||||
|
match parse_expr("a ? b : c = 5") {
|
||||||
|
Expr::Ternary { else_branch, .. } => {
|
||||||
|
assert!(matches!(*else_branch, Expr::Assign { .. }));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Ternary with Assign else, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- assignment -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple_assignment() {
|
||||||
|
match parse_expr("x = 5") {
|
||||||
|
Expr::Assign { name, op: AssignOp::Equal, value } => {
|
||||||
|
assert_eq!(name, "x");
|
||||||
|
match *value {
|
||||||
|
Expr::Literal(Literal::Number(5.0)) => {}
|
||||||
|
e => panic!("Expected Number(5), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Assign, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_compound_assignment() {
|
||||||
|
match parse_expr("x += 2") {
|
||||||
|
Expr::Assign { name, op: AssignOp::PlusEqual, value } => {
|
||||||
|
assert_eq!(name, "x");
|
||||||
|
match *value {
|
||||||
|
Expr::Literal(Literal::Number(2.0)) => {}
|
||||||
|
e => panic!("Expected Number(2), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Assign(PlusEqual), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_all_compound_ops() {
|
||||||
|
let ops = [
|
||||||
|
("+=", AssignOp::PlusEqual),
|
||||||
|
("-=", AssignOp::MinusEqual),
|
||||||
|
("*=", AssignOp::StarEqual),
|
||||||
|
("/=", AssignOp::SlashEqual),
|
||||||
|
("%=", AssignOp::PercentEqual),
|
||||||
|
];
|
||||||
|
for (op_str, expected_op) in &ops {
|
||||||
|
let expr = parse_expr(&format!("x {} 1", op_str));
|
||||||
|
match expr {
|
||||||
|
Expr::Assign { op, .. } => {
|
||||||
|
assert_eq!(std::mem::discriminant(&op), std::mem::discriminant(expected_op));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Assign for '{}', got {:?}", op_str, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- property access -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_property_get() {
|
||||||
|
match parse_expr("obj.name") {
|
||||||
|
Expr::Get { object, name } => {
|
||||||
|
match *object {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "obj"),
|
||||||
|
e => panic!("Expected Variable(obj), got {:?}", e),
|
||||||
|
}
|
||||||
|
assert_eq!(name, "name");
|
||||||
|
}
|
||||||
|
e => panic!("Expected Get, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_property_set() {
|
||||||
|
match parse_expr("obj.name = 5") {
|
||||||
|
Expr::Set { object, name, op: AssignOp::Equal, value } => {
|
||||||
|
match *object {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "obj"),
|
||||||
|
e => panic!("Expected Variable(obj), got {:?}", e),
|
||||||
|
}
|
||||||
|
assert_eq!(name, "name");
|
||||||
|
match *value {
|
||||||
|
Expr::Literal(Literal::Number(5.0)) => {}
|
||||||
|
e => panic!("Expected Number(5), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Set, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_chained_property_access() {
|
||||||
|
match parse_expr("a.b.c") {
|
||||||
|
Expr::Get { object, name } => {
|
||||||
|
assert_eq!(name, "c");
|
||||||
|
match *object {
|
||||||
|
Expr::Get { name: ref inner, .. } => assert_eq!(inner, "b"),
|
||||||
|
e => panic!("Expected chained Get, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Get, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- index access -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_index_get() {
|
||||||
|
match parse_expr("arr[0]") {
|
||||||
|
Expr::IndexGet { array, index } => {
|
||||||
|
match *array {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "arr"),
|
||||||
|
e => panic!("Expected Variable(arr), got {:?}", e),
|
||||||
|
}
|
||||||
|
match *index {
|
||||||
|
Expr::Literal(Literal::Number(0.0)) => {}
|
||||||
|
e => panic!("Expected Number(0), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected IndexGet, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_index_set() {
|
||||||
|
match parse_expr("arr[i] = 42") {
|
||||||
|
Expr::IndexSet { array, index, op: AssignOp::Equal, value } => {
|
||||||
|
match *array {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "arr"),
|
||||||
|
e => panic!("Expected Variable(arr), got {:?}", e),
|
||||||
|
}
|
||||||
|
match *index {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "i"),
|
||||||
|
e => panic!("Expected Variable(i), got {:?}", e),
|
||||||
|
}
|
||||||
|
match *value {
|
||||||
|
Expr::Literal(Literal::Number(42.0)) => {}
|
||||||
|
e => panic!("Expected Number(42), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected IndexSet, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nested_index() {
|
||||||
|
match parse_expr("a[1][2]") {
|
||||||
|
Expr::IndexGet { array, .. } => {
|
||||||
|
match *array {
|
||||||
|
Expr::IndexGet { .. } => {}
|
||||||
|
e => panic!("Expected nested IndexGet, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected IndexGet, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- function calls -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_call_no_args() {
|
||||||
|
match parse_expr("f()") {
|
||||||
|
Expr::Call { callee, arguments } => {
|
||||||
|
match *callee {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "f"),
|
||||||
|
e => panic!("Expected Variable(f), got {:?}", e),
|
||||||
|
}
|
||||||
|
assert!(arguments.is_empty());
|
||||||
|
}
|
||||||
|
e => panic!("Expected Call, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_call_with_args() {
|
||||||
|
match parse_expr("add(1, 2, 3)") {
|
||||||
|
Expr::Call { callee, arguments } => {
|
||||||
|
match *callee {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "add"),
|
||||||
|
e => panic!("Expected Variable(add), got {:?}", e),
|
||||||
|
}
|
||||||
|
assert_eq!(arguments.len(), 3);
|
||||||
|
}
|
||||||
|
e => panic!("Expected Call, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_chained_calls() {
|
||||||
|
match parse_expr("f()()") {
|
||||||
|
Expr::Call { callee, .. } => {
|
||||||
|
match *callee {
|
||||||
|
Expr::Call { callee, .. } => {
|
||||||
|
match *callee {
|
||||||
|
Expr::Variable(n) => assert_eq!(n, "f"),
|
||||||
|
e => panic!("Expected Variable(f), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected inner Call, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Call, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_method_style_call() {
|
||||||
|
// obj.method()
|
||||||
|
match parse_expr("obj.method()") {
|
||||||
|
Expr::Call { callee, .. } => {
|
||||||
|
match *callee {
|
||||||
|
Expr::Get { .. } => {}
|
||||||
|
e => panic!("Expected Get, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected Call, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- lambda -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple_lambda() {
|
||||||
|
match parse_expr("fn(x, y) { return x + y; }") {
|
||||||
|
Expr::Lambda { params, body } => {
|
||||||
|
assert_eq!(params, vec!["x", "y"]);
|
||||||
|
assert_eq!(body.len(), 1);
|
||||||
|
}
|
||||||
|
e => panic!("Expected Lambda, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_lambda_no_params() {
|
||||||
|
match parse_expr("fn() { return 42; }") {
|
||||||
|
Expr::Lambda { params, body } => {
|
||||||
|
assert!(params.is_empty());
|
||||||
|
assert_eq!(body.len(), 1);
|
||||||
|
}
|
||||||
|
e => panic!("Expected Lambda, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- object literal -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_object() {
|
||||||
|
match parse_expr("{}") {
|
||||||
|
Expr::ObjectLiteral { properties } => assert!(properties.is_empty()),
|
||||||
|
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_object_with_properties() {
|
||||||
|
match parse_expr("{ name: \"aster\", version: 1 }") {
|
||||||
|
Expr::ObjectLiteral { properties } => {
|
||||||
|
assert_eq!(properties.len(), 2);
|
||||||
|
assert_eq!(properties[0].0, "name");
|
||||||
|
assert_eq!(properties[1].0, "version");
|
||||||
|
}
|
||||||
|
e => panic!("Expected ObjectLiteral, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- array literal -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_empty_array() {
|
||||||
|
match parse_expr("[]") {
|
||||||
|
Expr::ArrayLiteral { elements } => assert!(elements.is_empty()),
|
||||||
|
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_array_with_elements() {
|
||||||
|
match parse_expr("[1, 2, 3]") {
|
||||||
|
Expr::ArrayLiteral { elements } => assert_eq!(elements.len(), 3),
|
||||||
|
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nested_arrays() {
|
||||||
|
match parse_expr("[[1, 2], [3, 4]]") {
|
||||||
|
Expr::ArrayLiteral { elements } => {
|
||||||
|
assert_eq!(elements.len(), 2);
|
||||||
|
for e in &elements {
|
||||||
|
assert!(matches!(e, Expr::ArrayLiteral { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => panic!("Expected ArrayLiteral, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- statements -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_let_statement() {
|
||||||
|
match parse_stmt("let x = 5;") {
|
||||||
|
Stmt::Let { name, initializer, .. } => {
|
||||||
|
assert_eq!(name, "x");
|
||||||
|
assert!(matches!(initializer, Expr::Literal(Literal::Number(5.0))));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Let, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_let_without_semicolon() {
|
||||||
|
// Semicolons are optional
|
||||||
|
match parse_stmt("let x = 5") {
|
||||||
|
Stmt::Let { name, .. } => assert_eq!(name, "x"),
|
||||||
|
e => panic!("Expected Let, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_const_statement() {
|
||||||
|
match parse_stmt("const x = 5;") {
|
||||||
|
Stmt::Let { name, mutable, .. } => {
|
||||||
|
assert_eq!(name, "x");
|
||||||
|
assert!(!mutable, "const should set mutable=false");
|
||||||
|
}
|
||||||
|
e => panic!("Expected Let (const), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_expression_statement() {
|
||||||
|
match parse_stmt("x;") {
|
||||||
|
Stmt::ExprStmt(Expr::Variable(name)) => assert_eq!(name, "x"),
|
||||||
|
e => panic!("Expected ExprStmt(Variable), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_if_statement() {
|
||||||
|
match parse_stmt("if (x) { 1; }") {
|
||||||
|
Stmt::If { condition, then_branch, else_branch } => {
|
||||||
|
assert!(matches!(condition, Expr::Variable(_)));
|
||||||
|
assert!(matches!(*then_branch, Stmt::Block(_)));
|
||||||
|
assert!(else_branch.is_none());
|
||||||
|
}
|
||||||
|
e => panic!("Expected If, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_if_else_statement() {
|
||||||
|
match parse_stmt("if (x) { 1; } else { 2; }") {
|
||||||
|
Stmt::If { else_branch, .. } => {
|
||||||
|
assert!(else_branch.is_some());
|
||||||
|
}
|
||||||
|
e => panic!("Expected If with else, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_while_statement() {
|
||||||
|
match parse_stmt("while (x < 10) { x = x + 1; }") {
|
||||||
|
Stmt::While { condition, body } => {
|
||||||
|
assert!(matches!(condition, Expr::Binary { .. }));
|
||||||
|
assert!(matches!(*body, Stmt::Block(_)));
|
||||||
|
}
|
||||||
|
e => panic!("Expected While, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_statement() {
|
||||||
|
match parse_stmt("for (let i = 0; i < 10; i = i + 1) { }") {
|
||||||
|
Stmt::For { initializer, condition, step, body } => {
|
||||||
|
assert!(initializer.is_some());
|
||||||
|
assert!(condition.is_some());
|
||||||
|
assert!(step.is_some());
|
||||||
|
assert!(matches!(*body, Stmt::Block(_)));
|
||||||
|
}
|
||||||
|
e => panic!("Expected For, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_empty_clauses() {
|
||||||
|
match parse_stmt("for (;;) { }") {
|
||||||
|
Stmt::For { initializer, condition, step, .. } => {
|
||||||
|
assert!(initializer.is_none());
|
||||||
|
assert!(condition.is_none());
|
||||||
|
assert!(step.is_none());
|
||||||
|
}
|
||||||
|
e => panic!("Expected For, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_in_array() {
|
||||||
|
match parse_stmt("for (x in arr) { print(x); }") {
|
||||||
|
Stmt::ForIn { var_name, iterable, body } => {
|
||||||
|
assert_eq!(var_name, "x");
|
||||||
|
assert!(matches!(iterable, Expr::Variable(_)));
|
||||||
|
assert!(matches!(*body, Stmt::Block(_)));
|
||||||
|
}
|
||||||
|
e => panic!("Expected ForIn, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_in_with_object_literal() {
|
||||||
|
match parse_stmt("for (k in {a:1,b:2}) { print(k); }") {
|
||||||
|
Stmt::ForIn { var_name, iterable, .. } => {
|
||||||
|
assert_eq!(var_name, "k");
|
||||||
|
assert!(matches!(iterable, Expr::ObjectLiteral { .. }));
|
||||||
|
}
|
||||||
|
e => panic!("Expected ForIn, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_in_with_expression() {
|
||||||
|
match parse_stmt("for (v in get_list()) { }") {
|
||||||
|
Stmt::ForIn { var_name, .. } => {
|
||||||
|
assert_eq!(var_name, "v");
|
||||||
|
}
|
||||||
|
e => panic!("Expected ForIn, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_for_in_distinct_from_c_style() {
|
||||||
|
// "for (x = 0; ...)" is C-style for, not for-in
|
||||||
|
match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") {
|
||||||
|
Stmt::For { .. } => {}
|
||||||
|
e => panic!("Expected C-style For, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_block() {
|
||||||
|
match parse_stmt("{ let x = 1; x; }") {
|
||||||
|
Stmt::Block(stmts) => assert_eq!(stmts.len(), 2),
|
||||||
|
e => panic!("Expected Block, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_return_statement() {
|
||||||
|
match parse_stmt("return 42;") {
|
||||||
|
Stmt::Return(Some(expr)) => {
|
||||||
|
assert!(matches!(expr, Expr::Literal(Literal::Number(42.0))));
|
||||||
|
}
|
||||||
|
e => panic!("Expected Return(42), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_return_nil() {
|
||||||
|
match parse_stmt("return;") {
|
||||||
|
Stmt::Return(None) => {}
|
||||||
|
e => panic!("Expected Return(None), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_break_statement() {
|
||||||
|
match parse_stmt("break;") {
|
||||||
|
Stmt::Break => {}
|
||||||
|
e => panic!("Expected Break, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_continue_statement() {
|
||||||
|
match parse_stmt("continue;") {
|
||||||
|
Stmt::Continue => {}
|
||||||
|
e => panic!("Expected Continue, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_function_declaration() {
|
||||||
|
match parse_stmt("fn add(a, b) { return a + b; }") {
|
||||||
|
Stmt::Function { name, params, body } => {
|
||||||
|
assert_eq!(name, "add");
|
||||||
|
assert_eq!(params, vec!["a", "b"]);
|
||||||
|
assert_eq!(body.len(), 1);
|
||||||
|
}
|
||||||
|
e => panic!("Expected Function, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_function_no_params() {
|
||||||
|
match parse_stmt("fn greet() { print(\"hi\"); }") {
|
||||||
|
Stmt::Function { name, params, .. } => {
|
||||||
|
assert_eq!(name, "greet");
|
||||||
|
assert!(params.is_empty());
|
||||||
|
}
|
||||||
|
e => panic!("Expected Function, got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- error recovery -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_recovery_skips_past_semicolon() {
|
||||||
|
// First statement is invalid, second should parse fine
|
||||||
|
let (stmts, errors) = parse("let = 5; let y = 10;");
|
||||||
|
assert!(!errors.is_empty(), "Should have parse errors");
|
||||||
|
// Should have at least the second statement
|
||||||
|
assert!(stmts.len() >= 1);
|
||||||
|
// The second statement should be a valid Let for y
|
||||||
|
match &stmts.last().unwrap() {
|
||||||
|
Stmt::Let { name, .. } => assert_eq!(name, "y"),
|
||||||
|
e => panic!("Expected Let(y), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_recovery_at_statement_boundary() {
|
||||||
|
let (stmts, errors) = parse("let x = @@@; let y = 10;");
|
||||||
|
assert!(!errors.is_empty());
|
||||||
|
assert!(stmts.len() >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- parse errors -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_invalid_assignment_target() {
|
||||||
|
let (_stmts, errors) = parse("5 = 10;");
|
||||||
|
assert!(!errors.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_missing_closing_paren() {
|
||||||
|
let (_stmts, errors) = parse("if (x { 1; }");
|
||||||
|
assert!(!errors.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_missing_semicolon_after_for_condition() {
|
||||||
|
// for(;;){} is valid — test something actually invalid
|
||||||
|
let (_stmts, _errors) = parse("for (let i = 0 i < 10) { }");
|
||||||
|
assert!(!_errors.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- parenthesized expression -----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_parenthesized_expression() {
|
||||||
|
match parse_expr("(1 + 2)") {
|
||||||
|
Expr::Binary { op: BinaryOp::Add, .. } => {}
|
||||||
|
e => panic!("Expected Binary(Add), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_nested_parentheses() {
|
||||||
|
match parse_expr("((42))") {
|
||||||
|
Expr::Literal(Literal::Number(42.0)) => {}
|
||||||
|
e => panic!("Expected Number(42), got {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user