Compare commits
2 Commits
c7e4e0dce0
...
983a7d8b34
| Author | SHA1 | Date | |
|---|---|---|---|
| 983a7d8b34 | |||
| a76cd79be2 |
@@ -0,0 +1,58 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
|
||||
# Run REPL (interactive)
|
||||
cargo run
|
||||
|
||||
# Execute a script file
|
||||
cargo run -- examples/script.ast
|
||||
|
||||
# Run tests (none yet; the project has no test/ directory)
|
||||
cargo test
|
||||
```
|
||||
|
||||
This is a pure-Rust project with no external dependencies (`edition = "2024"`).
|
||||
|
||||
## Architecture
|
||||
|
||||
**Aster** is a tree-walking interpreter for a custom dynamically-typed scripting language. The pipeline follows the classic compiler frontend pattern:
|
||||
|
||||
```
|
||||
Source text → Lexer → Tokens → Parser → AST → Interpreter → Output
|
||||
```
|
||||
|
||||
### Module map
|
||||
|
||||
| Module | Responsibility |
|
||||
|---|---|
|
||||
| `lexer/` | Tokenizer. `Lexer::tokenize()` takes `&str` and returns `(Vec<Token>, Vec<RuntimeError>)`. Tokens carry `line`/`column` for error reporting. Supports `//` single-line comments. |
|
||||
| `parser/` | Recursive-descent parser (Pratt-style for expressions). `Parser::parse()` returns `(Vec<Stmt>, Vec<RuntimeError>)`. Uses `synchronize()` for error recovery — skips to the next statement boundary on parse failure. |
|
||||
| `ast/` | AST node definitions. `Expr` (in `expr.rs`) covers literals, variables, assignment, property access/indexing, unary/binary/logical ops, calls, lambdas, and object/array literals. `Stmt` (in `stmt.rs`) covers let, expression stmts, blocks, if/while/for, functions, return/break/continue. |
|
||||
| `interpreter/` | Tree-walking evaluator. `Interpreter` holds an `env: Rc<RefCell<Env>>`. `Env` is a linked-list of scopes for lexical nesting. The `Signal` enum propagates `Return`/`Break`/`Continue` through the call stack. `builtins/` registers native functions (`print`, `input`, `clock`) organized as `io` and `os` modules (also available as global functions). |
|
||||
| `error/` | Three error variants: `LexError` (line+column), `ParseError` (token), `RuntimeError` (optional token). All implement `Display` and `std::error::Error`. |
|
||||
| `main.rs` | Two modes: `run_file` (parse-then-execute, exits with code 65 on lex/parse errors, 70 on runtime errors) and `run_repl` (per-line evaluation with `:exit`/`:reset` commands). |
|
||||
|
||||
### Key design decisions
|
||||
|
||||
- **No external crate dependencies.** Everything is built on Rust's stdlib.
|
||||
- **Rc<RefCell<>> for shared ownership.** `Env` chains, `Value::Object`, `Value::Array`, and `Value::Function` all use `Rc<RefCell<>>` for interior mutability — matching the dynamic, garbage-collected feel without a GC.
|
||||
- **Error-tolerant parsing.** Both lexer and parser collect errors into a `Vec` rather than failing on the first error, so multiple diagnostics can be reported per run.
|
||||
- **Closures capture by environment.** `Function` structs store the `Env` at definition time; lambda expressions (`fn(params) { body }`) work the same way.
|
||||
- **Compound assignment operators** (`+=`, `-=`, `*=`, `/=`, `%=`) are parsed into `Expr::Assign`/`Set`/`IndexSet` with an `AssignOp` variant and evaluated by `apply_assign_op`.
|
||||
|
||||
### Language syntax overview
|
||||
|
||||
Variables: `let x = 42;`
|
||||
Functions: `fn add(a, b) { return a + b; }`
|
||||
Closures: `fn(x, y) { return x + y; }`
|
||||
Control flow: `if`/`while`/`for` (C-style), `break`/`continue`, `return`
|
||||
Data: objects `{ key: val }`, arrays `[1, 2, 3]`, indexing `arr[i]`, property access `obj.prop`
|
||||
Operators: `+ - * / %`, `== != < > <= >=`, `&& || !`, `= += -= *= /= %=`
|
||||
Builtins: `print(...)`, `input(prompt?)`, `os.clock()` (also `clock()` directly)
|
||||
@@ -25,4 +25,6 @@ pub fn register_all(env: &Rc<RefCell<Env>>) {
|
||||
let mut os = HashMap::new();
|
||||
os.insert("clock".into(), Value::NativeFunction(os::clock));
|
||||
e.define("os".into(), Value::Object(Rc::new(RefCell::new(os))));
|
||||
// clock can also be used as a global function for convenience
|
||||
e.define("clock".into(), Value::NativeFunction(os::clock));
|
||||
}
|
||||
|
||||
@@ -487,3 +487,684 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
|
||||
/// Full pipeline: source → tokens → ast → interpret → last expression value.
|
||||
/// Wraps in `let __result = <expr>;` so we can read the value back.
|
||||
fn eval_expr(input: &str) -> Value {
|
||||
let wrapped = format!("let __result = {};", input);
|
||||
let (tokens, _) = Lexer::new(&wrapped).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, errors) = parser.parse();
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
let mut interpreter = Interpreter::new();
|
||||
interpreter.interpret(stmts).expect("Runtime error");
|
||||
interpreter.env.borrow().get("__result").expect("No __result in env")
|
||||
}
|
||||
|
||||
/// Full pipeline for multiple statements. Returns the interpreter for env inspection.
|
||||
fn run(input: &str) -> Interpreter {
|
||||
let (tokens, _) = Lexer::new(input).tokenize();
|
||||
let mut parser = Parser::new(tokens);
|
||||
let (stmts, errors) = parser.parse();
|
||||
assert!(errors.is_empty(), "Parse errors: {:?}", errors);
|
||||
let mut interpreter = Interpreter::new();
|
||||
interpreter.interpret(stmts).expect("Runtime error");
|
||||
interpreter
|
||||
}
|
||||
|
||||
/// Helper: get a variable from the interpreter's environment.
|
||||
fn get_var(interp: &Interpreter, name: &str) -> Value {
|
||||
interp.env.borrow().get(name).unwrap_or(Value::Nil)
|
||||
}
|
||||
|
||||
/// Helper: assert a number value.
|
||||
fn assert_num(val: &Value, expected: f64) {
|
||||
match val {
|
||||
Value::Number(n) => assert!((n - expected).abs() < f64::EPSILON,
|
||||
"Expected {}, got {}", expected, n),
|
||||
_ => panic!("Expected Number({}), got {:?}", expected, val),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: assert a bool value.
|
||||
fn assert_bool(val: &Value, expected: bool) {
|
||||
match val {
|
||||
Value::Bool(b) => assert_eq!(*b, expected),
|
||||
_ => panic!("Expected Bool({}), got {:?}", expected, val),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: assert nil.
|
||||
fn assert_nil(val: &Value) {
|
||||
match val {
|
||||
Value::Nil => {}
|
||||
_ => panic!("Expected Nil, got {:?}", val),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Literals
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_number() {
|
||||
assert_num(&eval_expr("42"), 42.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string() {
|
||||
match eval_expr("\"hello\"") {
|
||||
Value::String(s) => assert_eq!(s, "hello"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_true() {
|
||||
assert_bool(&eval_expr("true"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_false() {
|
||||
assert_bool(&eval_expr("false"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nil() {
|
||||
assert_nil(&eval_expr("nil"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Arithmetic
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_add() {
|
||||
assert_num(&eval_expr("1 + 2"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_sub() {
|
||||
assert_num(&eval_expr("5 - 3"), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_mul() {
|
||||
assert_num(&eval_expr("4 * 3"), 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_div() {
|
||||
assert_num(&eval_expr("10 / 4"), 2.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_mod() {
|
||||
assert_num(&eval_expr("7 % 3"), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_complex_arithmetic() {
|
||||
assert_num(&eval_expr("1 + 2 * 3"), 7.0);
|
||||
assert_num(&eval_expr("(1 + 2) * 3"), 9.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// String concatenation
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_string_concat() {
|
||||
match eval_expr("\"hello \" + \"world\"") {
|
||||
Value::String(s) => assert_eq!(s, "hello world"),
|
||||
v => panic!("Expected String, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Comparison
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_equal_numbers() {
|
||||
assert_bool(&eval_expr("1 == 1"), true);
|
||||
assert_bool(&eval_expr("1 == 2"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_not_equal() {
|
||||
assert_bool(&eval_expr("1 != 2"), true);
|
||||
assert_bool(&eval_expr("1 != 1"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_greater_less() {
|
||||
assert_bool(&eval_expr("5 > 3"), true);
|
||||
assert_bool(&eval_expr("3 > 5"), false);
|
||||
assert_bool(&eval_expr("5 >= 5"), true);
|
||||
assert_bool(&eval_expr("3 < 5"), true);
|
||||
assert_bool(&eval_expr("5 < 3"), false);
|
||||
assert_bool(&eval_expr("3 <= 3"), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_equal_strings() {
|
||||
assert_bool(&eval_expr("\"a\" == \"a\""), true);
|
||||
assert_bool(&eval_expr("\"a\" == \"b\""), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_equal_bools() {
|
||||
assert_bool(&eval_expr("true == true"), true);
|
||||
assert_bool(&eval_expr("true == false"), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nil_equals_nil() {
|
||||
assert_bool(&eval_expr("nil == nil"), true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Logical operators
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_logical_and_short_circuit() {
|
||||
assert_bool(&eval_expr("true && true"), true);
|
||||
assert_bool(&eval_expr("true && false"), false);
|
||||
assert_bool(&eval_expr("false && true"), false);
|
||||
// Short-circuit: false && <anything> returns false (the left value)
|
||||
match eval_expr("false && 999") {
|
||||
Value::Bool(false) => {}
|
||||
v => panic!("Expected Bool(false), got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_logical_or_short_circuit() {
|
||||
assert_bool(&eval_expr("true || false"), true);
|
||||
assert_bool(&eval_expr("false || true"), true);
|
||||
assert_bool(&eval_expr("false || false"), false);
|
||||
// Short-circuit: true || <anything> returns true (the left value)
|
||||
match eval_expr("true || 999") {
|
||||
Value::Bool(true) => {}
|
||||
v => panic!("Expected Bool(true), got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Unary
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_negation() {
|
||||
assert_num(&eval_expr("-5"), -5.0);
|
||||
assert_num(&eval_expr("--5"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_not() {
|
||||
assert_bool(&eval_expr("!true"), false);
|
||||
assert_bool(&eval_expr("!false"), true);
|
||||
assert_bool(&eval_expr("!nil"), true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Assignment and variables
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_variable_definition_and_use() {
|
||||
let interp = run("let x = 10;");
|
||||
assert_num(&get_var(&interp, "x"), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_simple_assignment() {
|
||||
let interp = run("let x = 5; x = 10;");
|
||||
assert_num(&get_var(&interp, "x"), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_plus_equal() {
|
||||
let interp = run("let x = 5; x += 3;");
|
||||
assert_num(&get_var(&interp, "x"), 8.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_minus_equal() {
|
||||
let interp = run("let x = 10; x -= 3;");
|
||||
assert_num(&get_var(&interp, "x"), 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_star_equal() {
|
||||
let interp = run("let x = 4; x *= 3;");
|
||||
assert_num(&get_var(&interp, "x"), 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_slash_equal() {
|
||||
let interp = run("let x = 10; x /= 2;");
|
||||
assert_num(&get_var(&interp, "x"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_compound_percent_equal() {
|
||||
let interp = run("let x = 7; x %= 3;");
|
||||
assert_num(&get_var(&interp, "x"), 1.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scope
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_block_scope_isolation() {
|
||||
let interp = run("{ let x = 42; }");
|
||||
let val = get_var(&interp, "x");
|
||||
assert_nil(&val); // x should not exist outside the block
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_block_can_access_outer() {
|
||||
let interp = run("let x = 10; { let y = x + 1; }");
|
||||
assert_num(&get_var(&interp, "x"), 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_shadowing() {
|
||||
let interp = run("let x = 10; { let x = 20; }");
|
||||
assert_num(&get_var(&interp, "x"), 10.0); // Outer x unchanged
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_assign_outer_from_inner() {
|
||||
let interp = run("let x = 10; { x = 20; }");
|
||||
assert_num(&get_var(&interp, "x"), 20.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Control flow: if
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_if_true_branch() {
|
||||
let interp = run("let x = 0; if (true) { x = 1; }");
|
||||
assert_num(&get_var(&interp, "x"), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_if_false_branch_skipped() {
|
||||
let interp = run("let x = 0; if (false) { x = 1; }");
|
||||
assert_num(&get_var(&interp, "x"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_if_else_true() {
|
||||
let interp = run("let x = 0; if (true) { x = 1; } else { x = 2; }");
|
||||
assert_num(&get_var(&interp, "x"), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_if_else_false() {
|
||||
let interp = run("let x = 0; if (false) { x = 1; } else { x = 2; }");
|
||||
assert_num(&get_var(&interp, "x"), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_if_truthy() {
|
||||
// Non-nil, non-false values are truthy
|
||||
let interp = run("let x = 0; if (1) { x = 42; }");
|
||||
assert_num(&get_var(&interp, "x"), 42.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Control flow: while
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_while_loop() {
|
||||
let interp = run("let i = 0; while (i < 5) { i = i + 1; }");
|
||||
assert_num(&get_var(&interp, "i"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_while_never_runs() {
|
||||
let interp = run("let x = 0; while (false) { x = 1; }");
|
||||
assert_num(&get_var(&interp, "x"), 0.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Control flow: for
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_for_loop() {
|
||||
let interp = run("let sum = 0; for (let i = 0; i < 5; i = i + 1) { sum = sum + i; }");
|
||||
assert_num(&get_var(&interp, "sum"), 10.0); // 0+1+2+3+4
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_for_empty_clauses() {
|
||||
// Infinite loop with break — just test it parses and doesn't crash on one iter
|
||||
let interp = run("let x = 0; for (;;) { x = 1; break; }");
|
||||
assert_num(&get_var(&interp, "x"), 1.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Control flow: break / continue
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_break() {
|
||||
let interp = run("let i = 0; while (i < 100) { if (i == 5) { break; } i = i + 1; }");
|
||||
assert_num(&get_var(&interp, "i"), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_continue() {
|
||||
let interp = run("let sum = 0; let j = 0; while (j < 5) { j = j + 1; if (j == 3) { continue; } sum = sum + j; }");
|
||||
assert_num(&get_var(&interp, "sum"), 12.0); // 1+2+4+5 = 12 (skips 3)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_break_in_for() {
|
||||
let interp = run("let found = -1; for (let k = 0; k < 100; k = k + 1) { if (k * k > 50) { found = k; break; } }");
|
||||
assert_num(&get_var(&interp, "found"), 8.0); // 8*8=64 > 50
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Functions
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_function_call() {
|
||||
let interp = run("fn add(a, b) { return a + b; } let result = add(3, 4);");
|
||||
assert_num(&get_var(&interp, "result"), 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_function_no_return() {
|
||||
let interp = run("fn foo() { 42; } let result = foo();");
|
||||
assert_nil(&get_var(&interp, "result")); // No explicit return → nil
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_recursion() {
|
||||
let interp = run("fn fib(n) { if (n <= 1) { return n; } return fib(n-1) + fib(n-2); } let f = fib(10);");
|
||||
assert_num(&get_var(&interp, "f"), 55.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nested_function_calls() {
|
||||
let interp = run("fn double(x) { return x * 2; } let r = double(double(3));");
|
||||
assert_num(&get_var(&interp, "r"), 12.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Closures
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_simple_closure() {
|
||||
let interp = run("
|
||||
fn make_adder(x) { return fn(y) { return x + y; }; }
|
||||
let add10 = make_adder(10);
|
||||
let r = add10(5);
|
||||
");
|
||||
assert_num(&get_var(&interp, "r"), 15.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_closure_captures_by_value_semantics() {
|
||||
// Each call to make_adder creates an independent closure
|
||||
let interp = run("
|
||||
fn make_adder(x) { return fn(y) { return x + y; }; }
|
||||
let add5 = make_adder(5);
|
||||
let add10 = make_adder(10);
|
||||
let r1 = add5(0);
|
||||
let r2 = add10(0);
|
||||
");
|
||||
assert_num(&get_var(&interp, "r1"), 5.0);
|
||||
assert_num(&get_var(&interp, "r2"), 10.0);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Arrays
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_array_creation() {
|
||||
match eval_expr("[1, 2, 3]") {
|
||||
Value::Array(arr) => {
|
||||
let arr = arr.borrow();
|
||||
assert_eq!(arr.len(), 3);
|
||||
assert_num(&arr[0], 1.0);
|
||||
assert_num(&arr[1], 2.0);
|
||||
assert_num(&arr[2], 3.0);
|
||||
}
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_empty_array() {
|
||||
match eval_expr("[]") {
|
||||
Value::Array(arr) => assert_eq!(arr.borrow().len(), 0),
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_index_access() {
|
||||
let interp = run("let arr = [10, 20, 30]; let x = arr[0]; let y = arr[2];");
|
||||
assert_num(&get_var(&interp, "x"), 10.0);
|
||||
assert_num(&get_var(&interp, "y"), 30.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_index_assignment() {
|
||||
let interp = run("let arr = [1, 2, 3]; arr[0] = 99;");
|
||||
match get_var(&interp, "arr") {
|
||||
Value::Array(arr) => assert_num(&arr.borrow()[0], 99.0),
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_compound_index_assignment() {
|
||||
let interp = run("let arr = [1, 2, 3]; arr[0] += 10;");
|
||||
match get_var(&interp, "arr") {
|
||||
Value::Array(arr) => assert_num(&arr.borrow()[0], 11.0),
|
||||
v => panic!("Expected Array, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_nested_arrays() {
|
||||
let interp = run("let arr = [[1, 2], [3, 4]]; let x = arr[1][0];");
|
||||
assert_num(&get_var(&interp, "x"), 3.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_array_equality() {
|
||||
assert_bool(&eval_expr("[1, 2, 3] == [1, 2, 3]"), true);
|
||||
assert_bool(&eval_expr("[1, 2] == [3, 4]"), false);
|
||||
assert_bool(&eval_expr("[1] == [1, 2]"), false);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Strings as indexable
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_string_index() {
|
||||
match eval_expr("\"hello\"[0]") {
|
||||
Value::String(s) => assert_eq!(s, "h"),
|
||||
v => panic!("Expected String for 'h', got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_string_index_last() {
|
||||
match eval_expr("\"abc\"[2]") {
|
||||
Value::String(s) => assert_eq!(s, "c"),
|
||||
v => panic!("Expected String for 'c', got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Objects
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_object_creation() {
|
||||
match eval_expr("{ name: \"aster\", version: 1 }") {
|
||||
Value::Object(obj) => {
|
||||
let obj = obj.borrow();
|
||||
match obj.get("name") {
|
||||
Some(Value::String(s)) => assert_eq!(s, "aster"),
|
||||
v => panic!("Expected String 'aster', got {:?}", v),
|
||||
}
|
||||
match obj.get("version") {
|
||||
Some(Value::Number(n)) => assert!((n - 1.0).abs() < f64::EPSILON),
|
||||
v => panic!("Expected Number 1, got {:?}", v),
|
||||
}
|
||||
}
|
||||
v => panic!("Expected Object, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_object_property_access() {
|
||||
let interp = run("let obj = { name: \"aster\" }; let n = obj.name;");
|
||||
match get_var(&interp, "n") {
|
||||
Value::String(s) => assert_eq!(s, "aster"),
|
||||
v => panic!("Expected String 'aster', got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_object_property_assignment() {
|
||||
let interp = run("let obj = { name: \"old\" }; obj.name = \"new\";");
|
||||
match get_var(&interp, "obj") {
|
||||
Value::Object(obj) => {
|
||||
match obj.borrow().get("name") {
|
||||
Some(Value::String(s)) => assert_eq!(s, "new"),
|
||||
v => panic!("Expected String 'new', got {:?}", v),
|
||||
}
|
||||
}
|
||||
v => panic!("Expected Object, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_object_compound_property_assignment() {
|
||||
let interp = run("let obj = { age: 26 }; obj.age -= 2;");
|
||||
match get_var(&interp, "obj") {
|
||||
Value::Object(obj) => {
|
||||
assert_num(obj.borrow().get("age").unwrap(), 24.0);
|
||||
}
|
||||
v => panic!("Expected Object, got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Built-in functions
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn eval_builtin_clock() {
|
||||
// clock() returns a number (Unix timestamp in seconds)
|
||||
match eval_expr("clock()") {
|
||||
Value::Number(n) => assert!(n > 0.0, "clock should return positive number"),
|
||||
v => panic!("Expected Number from clock(), got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_builtin_os_clock() {
|
||||
match eval_expr("os.clock()") {
|
||||
Value::Number(n) => assert!(n > 0.0),
|
||||
v => panic!("Expected Number from os.clock(), got {:?}", v),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_print_returns_nil() {
|
||||
// print() returns nil
|
||||
assert_nil(&eval_expr("print(\"test\")"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Runtime errors
|
||||
// =========================================================================
|
||||
|
||||
#[test]
|
||||
fn error_undefined_variable() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
run("let x = y;");
|
||||
});
|
||||
// run() asserts no errors, so this should panic
|
||||
assert!(result.is_err(), "Expected error for undefined variable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_type_mismatch_negate_string() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("-\"hello\"");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for negating a string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_type_mismatch_add_number_and_string() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("1 + \"hello\"");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for adding number + string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_call_non_function() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("42()");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for calling non-function");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_index_out_of_bounds() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("[1, 2][5]");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for index out of bounds");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_property_on_non_object() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("42.name");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for property on non-object");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_index_on_non_array() {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
eval_expr("42[0]");
|
||||
});
|
||||
assert!(result.is_err(), "Expected error for index on non-array");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -309,3 +309,327 @@ fn is_ident_start(c: char) -> bool {
|
||||
fn is_ident_part(c: char) -> bool {
|
||||
is_ident_start(c) || c.is_ascii_digit()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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()),
|
||||
]);
|
||||
}
|
||||
|
||||
// ----- 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 fn if else while for break continue return true false nil");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Let, TokenKind::Fn,
|
||||
TokenKind::If, TokenKind::Else,
|
||||
TokenKind::While, TokenKind::For,
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+744
-1
@@ -552,6 +552,12 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn synchronize(&mut self) {
|
||||
// Always consume at least one token to guarantee forward progress,
|
||||
// preventing infinite loops when previous() is already a semicolon.
|
||||
if !self.is_at_end() {
|
||||
self.advance();
|
||||
}
|
||||
|
||||
while !self.is_at_end() {
|
||||
if self.current > 0 && matches!(self.previous().kind, TokenKind::Semicolon) {
|
||||
return;
|
||||
@@ -572,4 +578,741 @@ impl Parser {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Tests
|
||||
// ========================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 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_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_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