1855 lines
64 KiB
Rust
1855 lines
64 KiB
Rust
use crate::ast::*;
|
|
use crate::error::RuntimeError;
|
|
use crate::interpreter::Signal;
|
|
use super::{Value, Env, Function};
|
|
use std::collections::HashMap;
|
|
use std::rc::Rc;
|
|
use std::cell::RefCell;
|
|
|
|
pub struct Interpreter {
|
|
pub env: Rc<RefCell<Env>>,
|
|
}
|
|
|
|
impl Interpreter {
|
|
pub fn new() -> Self {
|
|
let env = Rc::new(RefCell::new(Env::new(None)));
|
|
super::builtins::register_all(&env);
|
|
Self { env }
|
|
}
|
|
|
|
pub fn interpret(&mut self, statements: Vec<Stmt>) -> Result<(), RuntimeError> {
|
|
for stmt in statements {
|
|
self.execute(stmt)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn execute(&mut self, stmt: Stmt) -> Result<Signal, RuntimeError> {
|
|
match stmt {
|
|
Stmt::Let { name, initializer, mutable } => {
|
|
let val = self.evaluate(initializer)?;
|
|
self.env.borrow_mut().define(name, val, mutable);
|
|
Ok(Signal::None)
|
|
}
|
|
Stmt::ExprStmt(expr) => {
|
|
self.evaluate(expr)?;
|
|
Ok(Signal::None)
|
|
}
|
|
Stmt::Block(stmts) => {
|
|
let previous = Rc::clone(&self.env);
|
|
self.env = Rc::new(RefCell::new(Env::new(Some(previous))));
|
|
let mut signal = Signal::None;
|
|
for s in stmts {
|
|
signal = self.execute(s)?;
|
|
if !matches!(signal, Signal::None) {
|
|
break;
|
|
}
|
|
}
|
|
let parent = self.env.borrow().parent.as_ref().unwrap().clone();
|
|
self.env = parent;
|
|
Ok(signal)
|
|
}
|
|
Stmt::If { condition, then_branch, else_branch } => {
|
|
let cond_val = self.evaluate(condition)?;
|
|
if self.is_truthy(&cond_val) {
|
|
self.execute(*then_branch)
|
|
} else if let Some(else_branch) = else_branch {
|
|
self.execute(*else_branch)
|
|
} else {
|
|
Ok(Signal::None)
|
|
}
|
|
}
|
|
Stmt::While { condition, body } => {
|
|
loop {
|
|
let cond_val = self.evaluate(condition.clone())?;
|
|
if !self.is_truthy(&cond_val) {
|
|
break;
|
|
}
|
|
match self.execute(*body.clone())? {
|
|
Signal::Break => break,
|
|
Signal::Continue => continue,
|
|
sig @ Signal::Return(_) => return Ok(sig),
|
|
Signal::None => {}
|
|
}
|
|
}
|
|
Ok(Signal::None)
|
|
}
|
|
Stmt::For { initializer, condition, step, body } => {
|
|
if let Some(init) = initializer {
|
|
self.execute(*init)?;
|
|
}
|
|
loop {
|
|
if let Some(cond) = &condition {
|
|
let cond_val = self.evaluate(cond.clone())?;
|
|
if !self.is_truthy(&cond_val) {
|
|
break;
|
|
}
|
|
}
|
|
match self.execute(*body.clone())? {
|
|
Signal::Break => break,
|
|
Signal::Continue => {
|
|
if let Some(step) = &step {
|
|
self.evaluate(step.clone())?;
|
|
}
|
|
continue;
|
|
}
|
|
sig @ Signal::Return(_) => return Ok(sig),
|
|
Signal::None => {}
|
|
}
|
|
if let Some(step) = &step {
|
|
self.evaluate(step.clone())?;
|
|
}
|
|
}
|
|
Ok(Signal::None)
|
|
}
|
|
Stmt::ForIn { var_name, iterable, body } => {
|
|
let iter_val = self.evaluate(iterable)?;
|
|
let items: Vec<Value> = match &iter_val {
|
|
Value::Array(arr) => arr.borrow().clone(),
|
|
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
|
|
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
|
|
_ => {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: "for-in requires an array, object, or string".into(),
|
|
token: None,
|
|
});
|
|
}
|
|
};
|
|
|
|
let mut signal = Signal::None;
|
|
for item in items {
|
|
// Create a new scope for each iteration so the loop variable
|
|
// is isolated and break/continue clean up correctly.
|
|
let previous = Rc::clone(&self.env);
|
|
self.env = Rc::new(RefCell::new(Env::new(Some(previous))));
|
|
self.env.borrow_mut().define(var_name.clone(), item, true);
|
|
|
|
signal = self.execute(*body.clone())?;
|
|
// Restore parent scope before checking signal
|
|
let parent = self.env.borrow().parent.as_ref().unwrap().clone();
|
|
self.env = parent;
|
|
|
|
match signal {
|
|
Signal::Break => { signal = Signal::None; break; }
|
|
Signal::Continue => { signal = Signal::None; continue; }
|
|
sig @ Signal::Return(_) => return Ok(sig),
|
|
Signal::None => {}
|
|
}
|
|
}
|
|
Ok(signal)
|
|
}
|
|
Stmt::Function { name, params, body } => {
|
|
let func = Value::Function(Rc::new(Function {
|
|
params,
|
|
body,
|
|
env: Rc::clone(&self.env),
|
|
name: Some(name.clone()),
|
|
}));
|
|
self.env.borrow_mut().define(name, func, true);
|
|
Ok(Signal::None)
|
|
}
|
|
Stmt::Return(expr_opt) => {
|
|
if let Some(expr) = expr_opt {
|
|
Ok(Signal::Return(self.evaluate(expr)?))
|
|
} else {
|
|
Ok(Signal::Return(Value::Nil))
|
|
}
|
|
}
|
|
Stmt::Break => Ok(Signal::Break),
|
|
Stmt::Continue => Ok(Signal::Continue),
|
|
}
|
|
}
|
|
|
|
fn evaluate(&mut self, expr: Expr) -> Result<Value, RuntimeError> {
|
|
match expr {
|
|
Expr::Literal(lit) => Ok(match lit {
|
|
crate::ast::expr::Literal::Number(n) => Value::Number(n),
|
|
crate::ast::expr::Literal::String(s) => Value::String(s),
|
|
crate::ast::expr::Literal::Bool(b) => Value::Bool(b),
|
|
crate::ast::expr::Literal::Nil => Value::Nil,
|
|
}),
|
|
Expr::Variable(name) => {
|
|
match self.env.borrow().get(&name) {
|
|
Some(val) => Ok(val),
|
|
None => Err(RuntimeError::RuntimeError {
|
|
message: format!("Undefined variable '{}'", name),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
Expr::Assign { name, op, value } => {
|
|
let rhs = self.evaluate(*value)?;
|
|
let val = match op {
|
|
AssignOp::Equal => rhs,
|
|
_ => {
|
|
let current = self.env.borrow().get(&name).ok_or_else(|| RuntimeError::RuntimeError { message: format!("Undefined variable '{}'", name), token: None })?;
|
|
self.apply_assign_op(current.clone(), rhs, op)?
|
|
}
|
|
};
|
|
match self.env.borrow_mut().assign(&name, val.clone()) {
|
|
Ok(true) => {}
|
|
Ok(false) => {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: format!("Undefined variable '{}'", name),
|
|
token: None,
|
|
});
|
|
}
|
|
Err(msg) => {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: msg,
|
|
token: None,
|
|
});
|
|
}
|
|
}
|
|
Ok(val)
|
|
}
|
|
Expr::Get { object, name } => {
|
|
let obj = self.evaluate(*object)?;
|
|
match obj {
|
|
Value::Object(map) => {
|
|
match map.borrow().get(&name) {
|
|
Some(val) => Ok(val.clone()),
|
|
None => Err(RuntimeError::RuntimeError {
|
|
message: format!("Undefined property '{}'", name),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Only objects have properties".to_string(),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
Expr::Set { object, name, op, value } => {
|
|
let obj = self.evaluate(*object)?;
|
|
let rhs = self.evaluate(*value)?;
|
|
match obj {
|
|
Value::Object(map) => {
|
|
let val = match op {
|
|
AssignOp::Equal => rhs,
|
|
_ => {
|
|
let map_borrow = map.borrow();
|
|
let current = map_borrow.get(&name).ok_or_else(|| RuntimeError::RuntimeError { message: format!("Property '{}' does not exist", name), token: None })?;
|
|
self.apply_assign_op(current.clone(), rhs, op)?
|
|
}
|
|
};
|
|
map.borrow_mut().insert(name, val.clone());
|
|
Ok(val)
|
|
}
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Only objects have properties".to_string(),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
Expr::IndexGet { array, index } => {
|
|
let target = self.evaluate(*array)?;
|
|
let idx_val = self.evaluate(*index)?;
|
|
// Object index access: obj["key"]
|
|
if let Value::Object(map) = &target {
|
|
let key = match &idx_val {
|
|
Value::String(s) => s.clone(),
|
|
_ => return Err(RuntimeError::RuntimeError {
|
|
message: "Object index must be a string".into(),
|
|
token: None,
|
|
}),
|
|
};
|
|
return map.borrow().get(&key).cloned().ok_or_else(|| RuntimeError::RuntimeError {
|
|
message: format!("Undefined property '{}'", key),
|
|
token: None,
|
|
});
|
|
}
|
|
let i = self.as_array_index(&idx_val)?;
|
|
match target {
|
|
Value::Array(vec) => {
|
|
let vec = vec.borrow();
|
|
vec.get(i).cloned().ok_or_else(|| RuntimeError::RuntimeError {
|
|
message: format!("Index {} out of bounds (len {})", i, vec.len()),
|
|
token: None,
|
|
})
|
|
}
|
|
Value::String(s) => {
|
|
let chars: Vec<char> = s.chars().collect();
|
|
chars.get(i).map(|&c| Value::String(c.to_string())).ok_or_else(|| RuntimeError::RuntimeError {
|
|
message: format!("Index {} out of bounds (len {})", i, chars.len()),
|
|
token: None,
|
|
})
|
|
}
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Index access on non-array, non-string, non-object value".to_string(),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
Expr::IndexSet { array, index, op, value } => {
|
|
let target = self.evaluate(*array)?;
|
|
let idx_val = self.evaluate(*index)?;
|
|
let rhs = self.evaluate(*value)?;
|
|
// Object index assignment: obj["key"] = value
|
|
if let Value::Object(map) = &target {
|
|
let key = match &idx_val {
|
|
Value::String(s) => s.clone(),
|
|
_ => return Err(RuntimeError::RuntimeError {
|
|
message: "Object index must be a string".into(),
|
|
token: None,
|
|
}),
|
|
};
|
|
let val = match op {
|
|
AssignOp::Equal => rhs,
|
|
_ => {
|
|
let map_borrow = map.borrow();
|
|
let current = map_borrow.get(&key).ok_or_else(|| RuntimeError::RuntimeError {
|
|
message: format!("Property '{}' does not exist", key),
|
|
token: None,
|
|
})?;
|
|
self.apply_assign_op(current.clone(), rhs, op)?
|
|
}
|
|
};
|
|
map.borrow_mut().insert(key, val.clone());
|
|
return Ok(val);
|
|
}
|
|
let i = self.as_array_index(&idx_val)?;
|
|
match target {
|
|
Value::Array(vec) => {
|
|
let mut vec = vec.borrow_mut();
|
|
if i >= vec.len() {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: format!("Index {} out of bounds (len {})", i, vec.len()),
|
|
token: None,
|
|
});
|
|
}
|
|
let val = match op {
|
|
AssignOp::Equal => rhs,
|
|
_ => {
|
|
let current = vec[i].clone();
|
|
self.apply_assign_op(current, rhs, op)?
|
|
}
|
|
};
|
|
vec[i] = val.clone();
|
|
Ok(val)
|
|
}
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Index assignment on non-array, non-object value".to_string(),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
Expr::ObjectLiteral { properties } => {
|
|
let mut map = HashMap::new();
|
|
for (key, value_expr) in properties {
|
|
let value = self.evaluate(value_expr)?;
|
|
map.insert(key, value);
|
|
}
|
|
Ok(Value::Object(Rc::new(RefCell::new(map))))
|
|
}
|
|
Expr::ArrayLiteral { elements } => {
|
|
let mut arr = Vec::new();
|
|
for e in elements {
|
|
arr.push(self.evaluate(e)?);
|
|
}
|
|
Ok(Value::Array(Rc::new(RefCell::new(arr))))
|
|
}
|
|
Expr::Unary { op, right } => {
|
|
let val = self.evaluate(*right)?;
|
|
match op {
|
|
crate::ast::expr::UnaryOp::Negate => match val {
|
|
Value::Number(n) => Ok(Value::Number(-n)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Unary '-' on non-number".to_string(),
|
|
token: None,
|
|
}),
|
|
},
|
|
crate::ast::expr::UnaryOp::Not => Ok(Value::Bool(!self.is_truthy(&val))),
|
|
}
|
|
}
|
|
Expr::Binary { left, op, right } => {
|
|
let l = self.evaluate(*left)?;
|
|
let r = self.evaluate(*right)?;
|
|
match op {
|
|
crate::ast::expr::BinaryOp::Add => {
|
|
// If either operand is a string, coerce both to string and concatenate.
|
|
if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
|
|
Ok(Value::String(format!("{}{}", l, r)))
|
|
} else {
|
|
match (l, r) {
|
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a+b)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Invalid '+' operands".to_string(),
|
|
token: None,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
crate::ast::expr::BinaryOp::Sub => match (l,r) {
|
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a-b)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Invalid '-' operands".to_string(),
|
|
token: None,
|
|
}),
|
|
},
|
|
crate::ast::expr::BinaryOp::Mul => match (l,r) {
|
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a*b)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Invalid '*' operands".to_string(),
|
|
token: None,
|
|
}),
|
|
},
|
|
crate::ast::expr::BinaryOp::Div => match (l,r) {
|
|
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
|
Err(RuntimeError::RuntimeError {
|
|
message: "Division by zero".to_string(),
|
|
token: None,
|
|
})
|
|
}
|
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a/b)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Invalid '/' operands".to_string(),
|
|
token: None,
|
|
}),
|
|
},
|
|
crate::ast::expr::BinaryOp::Mod => match (l,r) {
|
|
(Value::Number(_), Value::Number(b)) if b == 0.0 => {
|
|
Err(RuntimeError::RuntimeError {
|
|
message: "Modulo by zero".to_string(),
|
|
token: None,
|
|
})
|
|
}
|
|
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a%b)),
|
|
_ => Err(RuntimeError::RuntimeError {
|
|
message: "Invalid '%' operands".to_string(),
|
|
token: None,
|
|
}),
|
|
},
|
|
crate::ast::expr::BinaryOp::Greater => Ok(Value::Bool(self.as_number(&l)? > self.as_number(&r)?)),
|
|
crate::ast::expr::BinaryOp::GreaterEqual => Ok(Value::Bool(self.as_number(&l)? >= self.as_number(&r)?)),
|
|
crate::ast::expr::BinaryOp::Less => Ok(Value::Bool(self.as_number(&l)? < self.as_number(&r)?)),
|
|
crate::ast::expr::BinaryOp::LessEqual => Ok(Value::Bool(self.as_number(&l)? <= self.as_number(&r)?)),
|
|
crate::ast::expr::BinaryOp::Equal => Ok(Value::Bool(self.is_equal(&l,&r))),
|
|
crate::ast::expr::BinaryOp::NotEqual => Ok(Value::Bool(!self.is_equal(&l,&r))),
|
|
}
|
|
}
|
|
Expr::Logical { left, op, right } => {
|
|
let l = self.evaluate(*left)?;
|
|
match op {
|
|
crate::ast::expr::LogicalOp::And => {
|
|
Ok(if !self.is_truthy(&l) { l } else { self.evaluate(*right)? })
|
|
}
|
|
crate::ast::expr::LogicalOp::Or => {
|
|
Ok(if self.is_truthy(&l) { l } else { self.evaluate(*right)? })
|
|
}
|
|
}
|
|
}
|
|
Expr::Ternary { condition, then_branch, else_branch } => {
|
|
let cond = self.evaluate(*condition)?;
|
|
if self.is_truthy(&cond) {
|
|
self.evaluate(*then_branch)
|
|
} else {
|
|
self.evaluate(*else_branch)
|
|
}
|
|
}
|
|
Expr::Call { callee, arguments } => {
|
|
let func = self.evaluate(*callee)?;
|
|
let mut args = Vec::new();
|
|
for e in arguments {
|
|
args.push(self.evaluate(e)?);
|
|
}
|
|
self.call_function(func, args)
|
|
}
|
|
Expr::Lambda { params, body } => {
|
|
Ok(Value::Function(Rc::new(Function {
|
|
params,
|
|
body,
|
|
env: Rc::clone(&self.env),
|
|
name: None,
|
|
})))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn call_function(&mut self, func_val: Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
|
match func_val {
|
|
Value::NativeFunction(native_fn) => {
|
|
native_fn(args)
|
|
}
|
|
Value::Function(f) => {
|
|
let env = Rc::new(RefCell::new(Env::new(Some(Rc::clone(&f.env)))));
|
|
|
|
if let Some(name) = &f.name {
|
|
env.borrow_mut().define(name.clone(), Value::Function(Rc::clone(&f)), true);
|
|
}
|
|
|
|
for (i,param) in f.params.iter().enumerate() {
|
|
let val = args.get(i).cloned().unwrap_or(Value::Nil);
|
|
env.borrow_mut().define(param.clone(), val, true);
|
|
}
|
|
|
|
let previous = Rc::clone(&self.env);
|
|
self.env = env;
|
|
let mut ret = Value::Nil;
|
|
for stmt in &f.body {
|
|
match self.execute(stmt.clone())? {
|
|
Signal::Return(val) => { ret = val; break; }
|
|
Signal::None => {}
|
|
Signal::Break | Signal::Continue => {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: "break/continue outside of loop".to_string(),
|
|
token: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
self.env = previous;
|
|
Ok(ret)
|
|
}
|
|
_ => {
|
|
Err(RuntimeError::RuntimeError {
|
|
message: "Attempt to call non-function".to_string(),
|
|
token: None,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_truthy(&self, val: &Value) -> bool {
|
|
match val {
|
|
Value::Nil => false,
|
|
Value::Bool(b) => *b,
|
|
_ => true,
|
|
}
|
|
}
|
|
|
|
fn is_equal(&self, a: &Value, b: &Value) -> bool {
|
|
match (a,b) {
|
|
(Value::Nil, Value::Nil) => true,
|
|
(Value::Bool(x), Value::Bool(y)) => x==y,
|
|
(Value::Number(x), Value::Number(y)) => x==y,
|
|
(Value::String(x), Value::String(y)) => x==y,
|
|
(Value::Array(x), Value::Array(y)) => {
|
|
let x = x.borrow();
|
|
let y = y.borrow();
|
|
if x.len() != y.len() { return false; }
|
|
x.iter().zip(y.iter()).all(|(a,b)| self.is_equal(a,b))
|
|
}
|
|
(Value::Object(x), Value::Object(y)) => {
|
|
let x = x.borrow();
|
|
let y = y.borrow();
|
|
if x.len() != y.len() { return false; }
|
|
x.iter().all(|(k, v)| {
|
|
y.get(k).map_or(false, |yv| self.is_equal(v, yv))
|
|
})
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn apply_assign_op(&self, left: Value, right: Value, op: AssignOp) -> Result<Value, RuntimeError> {
|
|
match op {
|
|
AssignOp::Equal => Ok(right),
|
|
AssignOp::PlusEqual => {
|
|
if let Value::String(s) = &left {
|
|
Ok(Value::String(format!("{}{}", s, right)))
|
|
} else {
|
|
let left_num = self.as_number(&left)?;
|
|
let right_num = self.as_number(&right)?;
|
|
Ok(Value::Number(left_num + right_num))
|
|
}
|
|
}
|
|
AssignOp::MinusEqual => {
|
|
let left_num = self.as_number(&left)?;
|
|
let right_num = self.as_number(&right)?;
|
|
Ok(Value::Number(left_num - right_num))
|
|
}
|
|
AssignOp::StarEqual => {
|
|
let left_num = self.as_number(&left)?;
|
|
let right_num = self.as_number(&right)?;
|
|
Ok(Value::Number(left_num * right_num))
|
|
}
|
|
AssignOp::SlashEqual => {
|
|
let left_num = self.as_number(&left)?;
|
|
let right_num = self.as_number(&right)?;
|
|
Ok(Value::Number(left_num / right_num))
|
|
}
|
|
AssignOp::PercentEqual => {
|
|
let left_num = self.as_number(&left)?;
|
|
let right_num = self.as_number(&right)?;
|
|
Ok(Value::Number(left_num % right_num))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn as_number(&self, val: &Value) -> Result<f64, RuntimeError> {
|
|
if let Value::Number(n) = val {
|
|
Ok(*n)
|
|
} else {
|
|
Err(RuntimeError::RuntimeError {
|
|
message: "Expected number".to_string(),
|
|
token: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn as_array_index(&self, val: &Value) -> Result<usize, RuntimeError> {
|
|
if let Value::Number(n) = val {
|
|
if *n < 0.0 || n.fract() != 0.0 {
|
|
return Err(RuntimeError::RuntimeError {
|
|
message: format!("Index must be a non-negative integer, got {}", n),
|
|
token: None,
|
|
});
|
|
}
|
|
Ok(*n as usize)
|
|
} else {
|
|
Err(RuntimeError::RuntimeError {
|
|
message: "Index must be a number".to_string(),
|
|
token: None,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_string_plus_number() {
|
|
match eval_expr("\"a\" + 42") {
|
|
Value::String(s) => assert_eq!(s, "a42"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_number_plus_string() {
|
|
match eval_expr("42 + \"a\"") {
|
|
Value::String(s) => assert_eq!(s, "42a"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_string_plus_bool() {
|
|
match eval_expr("\"t\" + true") {
|
|
Value::String(s) => assert_eq!(s, "ttrue"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_bool_plus_string() {
|
|
match eval_expr("false + \"x\"") {
|
|
Value::String(s) => assert_eq!(s, "falsex"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_string_plus_nil() {
|
|
match eval_expr("\"x\" + nil") {
|
|
Value::String(s) => assert_eq!(s, "xnil"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_plusequal_string_concat() {
|
|
let interp = run("let s = \"hello\"; s += \" world\"; s += 42;");
|
|
match get_var(&interp, "s") {
|
|
Value::String(s) => assert_eq!(s, "hello world42"),
|
|
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),
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Ternary
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_ternary_true_condition() {
|
|
assert_num(&eval_expr("true ? 1 : 2"), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_false_condition() {
|
|
assert_num(&eval_expr("false ? 1 : 2"), 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_truthy_condition() {
|
|
// Non-nil, non-false values are truthy
|
|
assert_num(&eval_expr("1 ? 42 : 99"), 42.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_nil_condition() {
|
|
assert_num(&eval_expr("nil ? 1 : 2"), 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_nested() {
|
|
let interp = run("let x = 5; let r = x > 3 ? (x > 10 ? 100 : 50) : 0;");
|
|
assert_num(&get_var(&interp, "r"), 50.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_with_computed_branches() {
|
|
let interp = run("let x = 10; let r = x > 5 ? x * 2 : x / 2;");
|
|
assert_num(&get_var(&interp, "r"), 20.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_short_circuit_then() {
|
|
// When condition is false, then_branch is not evaluated
|
|
let interp = run("let x = 0; let r = false ? (x = 999) : (x = 42);");
|
|
assert_num(&get_var(&interp, "x"), 42.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_ternary_short_circuit_else() {
|
|
// When condition is true, else_branch is not evaluated
|
|
let interp = run("let x = 0; let r = true ? (x = 42) : (x = 999);");
|
|
assert_num(&get_var(&interp, "x"), 42.0);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Const
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_const_definition() {
|
|
let interp = run("const x = 42;");
|
|
assert_num(&get_var(&interp, "x"), 42.0);
|
|
}
|
|
|
|
#[test]
|
|
fn error_const_reassignment() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
run("const x = 5; x = 10;");
|
|
});
|
|
assert!(result.is_err(), "Expected error for reassigning const");
|
|
}
|
|
|
|
#[test]
|
|
fn error_const_compound_assignment() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
run("const x = 5; x += 1;");
|
|
});
|
|
assert!(result.is_err(), "Expected error for compound assignment to const");
|
|
}
|
|
|
|
#[test]
|
|
fn eval_const_shadowing() {
|
|
// Inner block can shadow outer const with its own binding
|
|
let interp = run("const x = 10; { let x = 20; }");
|
|
assert_num(&get_var(&interp, "x"), 10.0); // Outer unchanged
|
|
}
|
|
|
|
#[test]
|
|
fn eval_const_object_mutation() {
|
|
// const prevents rebinding, not property mutation
|
|
let interp = run("const obj = { a: 1 }; obj.a = 2;");
|
|
match get_var(&interp, "obj") {
|
|
Value::Object(obj) => {
|
|
let obj = obj.borrow();
|
|
assert_num(obj.get("a").unwrap(), 2.0);
|
|
}
|
|
v => panic!("Expected Object, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_const_array_mutation() {
|
|
// const prevents rebinding, not index mutation
|
|
let interp = run("const arr = [1, 2]; arr[0] = 99;");
|
|
match get_var(&interp, "arr") {
|
|
Value::Array(arr) => {
|
|
assert_num(&arr.borrow()[0], 99.0);
|
|
}
|
|
v => panic!("Expected Array, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Object equality
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_object_equality_same() {
|
|
let interp = run("let a = {x: 1, y: 2}; let b = {x: 1, y: 2}; let r = a == b;");
|
|
assert_bool(&get_var(&interp, "r"), true);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_equality_different_values() {
|
|
let interp = run("let a = {x: 1}; let b = {x: 2}; let r = a == b;");
|
|
assert_bool(&get_var(&interp, "r"), false);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_equality_different_keys() {
|
|
let interp = run("let a = {x: 1}; let b = {y: 1}; let r = a == b;");
|
|
assert_bool(&get_var(&interp, "r"), false);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_equality_different_size() {
|
|
let interp = run("let a = {x: 1}; let b = {x: 1, y: 2}; let r = a == b;");
|
|
assert_bool(&get_var(&interp, "r"), false);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_equality_empty() {
|
|
let interp = run("let a = {}; let b = {}; let r = a == b;");
|
|
assert_bool(&get_var(&interp, "r"), true);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 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_division_by_zero() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("1 / 0");
|
|
});
|
|
assert!(result.is_err(), "Expected error for division by zero");
|
|
}
|
|
|
|
#[test]
|
|
fn error_modulo_by_zero() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("1 % 0");
|
|
});
|
|
assert!(result.is_err(), "Expected error for modulo by zero");
|
|
}
|
|
|
|
#[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 eval_number_plus_string_concatenates() {
|
|
match eval_expr("1 + \"hello\"") {
|
|
Value::String(s) => assert_eq!(s, "1hello"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Object index access: obj[key]
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_object_index_get() {
|
|
assert_num(&eval_expr("{a: 42}[\"a\"]"), 42.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_get_nested() {
|
|
assert_num(&eval_expr("{inner: {val: 7}}[\"inner\"][\"val\"]"), 7.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_get_variable_key() {
|
|
let interp = run("let obj = {x: 10, y: 20}; let k = \"y\"; let result = obj[k];");
|
|
assert_num(&get_var(&interp, "result"), 20.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_get_undefined() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("{}[\"x\"]");
|
|
});
|
|
assert!(result.is_err(), "Expected error for undefined property");
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_set_existing() {
|
|
let interp = run("let obj = {a: 1}; obj[\"a\"] = 99;");
|
|
match get_var(&interp, "obj") {
|
|
Value::Object(map) => {
|
|
let map = map.borrow();
|
|
assert_num(map.get("a").unwrap(), 99.0);
|
|
}
|
|
v => panic!("Expected Object, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_set_new_key() {
|
|
let interp = run("let obj = {}; obj[\"name\"] = \"Aster\";");
|
|
match get_var(&interp, "obj") {
|
|
Value::Object(map) => {
|
|
let map = map.borrow();
|
|
match map.get("name").unwrap() {
|
|
Value::String(s) => assert_eq!(s, "Aster"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
v => panic!("Expected Object, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_object_index_compound_assign() {
|
|
let interp = run("let obj = {count: 10}; obj[\"count\"] += 5;");
|
|
match get_var(&interp, "obj") {
|
|
Value::Object(map) => {
|
|
let map = map.borrow();
|
|
assert_num(map.get("count").unwrap(), 15.0);
|
|
}
|
|
v => panic!("Expected Object, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn error_object_index_non_string() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("{a: 1}[42]");
|
|
});
|
|
assert!(result.is_err(), "Expected error for non-string object index");
|
|
}
|
|
|
|
#[test]
|
|
fn error_object_index_set_non_string() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("let obj = {a: 1}; obj[true] = 2;");
|
|
});
|
|
assert!(result.is_err(), "Expected error for non-string object index set");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Builtins: len
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_len_string() {
|
|
assert_num(&eval_expr("len(\"hello\")"), 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_len_empty_string() {
|
|
assert_num(&eval_expr("len(\"\")"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_len_array() {
|
|
assert_num(&eval_expr("len([1, 2, 3])"), 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_len_empty_array() {
|
|
assert_num(&eval_expr("len([])"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_len_object() {
|
|
assert_num(&eval_expr("len({a: 1, b: 2})"), 2.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_len_empty_object() {
|
|
assert_num(&eval_expr("len({})"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn error_len_no_args() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("len()");
|
|
});
|
|
assert!(result.is_err(), "Expected error for len() with no args");
|
|
}
|
|
|
|
#[test]
|
|
fn error_len_number() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("len(42)");
|
|
});
|
|
assert!(result.is_err(), "Expected error for len(42)");
|
|
}
|
|
|
|
// =========================================================================
|
|
// Builtins: typeof
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_typeof_number() {
|
|
match eval_expr("typeof(42)") {
|
|
Value::String(s) => assert_eq!(s, "number"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_string() {
|
|
match eval_expr("typeof(\"hello\")") {
|
|
Value::String(s) => assert_eq!(s, "string"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_bool() {
|
|
match eval_expr("typeof(true)") {
|
|
Value::String(s) => assert_eq!(s, "bool"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_nil() {
|
|
match eval_expr("typeof(nil)") {
|
|
Value::String(s) => assert_eq!(s, "nil"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_object() {
|
|
match eval_expr("typeof({a: 1})") {
|
|
Value::String(s) => assert_eq!(s, "object"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_array() {
|
|
match eval_expr("typeof([1, 2])") {
|
|
Value::String(s) => assert_eq!(s, "array"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_typeof_function() {
|
|
let interp = run("fn f() {}");
|
|
// f is a variable, not an expression — use get_var
|
|
match get_var(&interp, "f") {
|
|
Value::Function(_) => {} // just verify it's a function
|
|
v => panic!("Expected Function, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Builtins: push / pop
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_push_returns_value() {
|
|
assert_num(&eval_expr("push([1, 2], 3)"), 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_push_modifies_array() {
|
|
let interp = run("let arr = [1, 2]; push(arr, 3);");
|
|
match get_var(&interp, "arr") {
|
|
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_push_string_to_array() {
|
|
let interp = run("let arr = [\"a\"]; push(arr, \"b\");");
|
|
match get_var(&interp, "arr") {
|
|
Value::Array(arr) => {
|
|
let arr = arr.borrow();
|
|
assert_eq!(arr.len(), 2);
|
|
}
|
|
v => panic!("Expected Array, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_pop_returns_last() {
|
|
assert_num(&eval_expr("pop([1, 2, 3])"), 3.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_pop_modifies_array() {
|
|
let interp = run("let arr = [1, 2, 3]; pop(arr);");
|
|
match get_var(&interp, "arr") {
|
|
Value::Array(arr) => {
|
|
let arr = arr.borrow();
|
|
assert_eq!(arr.len(), 2);
|
|
assert_num(&arr[0], 1.0);
|
|
assert_num(&arr[1], 2.0);
|
|
}
|
|
v => panic!("Expected Array, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn error_pop_empty_array() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("pop([])");
|
|
});
|
|
assert!(result.is_err(), "Expected error for pop([])");
|
|
}
|
|
|
|
#[test]
|
|
fn error_push_non_array() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("push(42, 1)");
|
|
});
|
|
assert!(result.is_err(), "Expected error for push on non-array");
|
|
}
|
|
|
|
#[test]
|
|
fn error_pop_non_array() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
eval_expr("pop(42)");
|
|
});
|
|
assert!(result.is_err(), "Expected error for pop on non-array");
|
|
}
|
|
|
|
// =========================================================================
|
|
// For-In loops
|
|
// =========================================================================
|
|
|
|
#[test]
|
|
fn eval_forin_array_elements() {
|
|
// Sum elements via for-in
|
|
let interp = run("let sum = 0; let arr = [1, 2, 3]; for (x in arr) { sum = sum + x; }");
|
|
assert_num(&get_var(&interp, "sum"), 6.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_empty_array() {
|
|
let interp = run("let count = 0; let arr = []; for (x in arr) { count = count + 1; }");
|
|
assert_num(&get_var(&interp, "count"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_object_keys() {
|
|
let interp = run("let keys = \"\"; let obj = {a: 1, b: 2}; for (k in obj) { keys = keys + k; }");
|
|
match get_var(&interp, "keys") {
|
|
Value::String(s) => {
|
|
// Object iteration order is not guaranteed, so check length
|
|
assert_eq!(s.len(), 2);
|
|
assert!(s.contains('a'));
|
|
assert!(s.contains('b'));
|
|
}
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_string_chars() {
|
|
let interp = run("let result = \"\"; for (c in \"ab\") { result = result + c; }");
|
|
match get_var(&interp, "result") {
|
|
Value::String(s) => assert_eq!(s, "ab"),
|
|
v => panic!("Expected String, got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_break() {
|
|
let interp = run("let sum = 0; for (x in [1, 2, 3, 4]) { if (x == 3) { break; } sum = sum + x; }");
|
|
assert_num(&get_var(&interp, "sum"), 3.0); // 1 + 2
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_continue() {
|
|
let interp = run("let sum = 0; for (x in [1, 2, 3]) { if (x == 2) { continue; } sum = sum + x; }");
|
|
assert_num(&get_var(&interp, "sum"), 4.0); // 1 + 3
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_loop_var_doesnt_leak() {
|
|
// Loop variable should not be accessible outside the loop
|
|
let interp = run("for (x in [1]) { let _ = x; }");
|
|
// x should not exist in the outer scope
|
|
match get_var(&interp, "x") {
|
|
Value::Nil => {} // Expected: x is not defined
|
|
v => panic!("Expected Nil (undefined), got {:?}", v),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn eval_forin_with_object_array() {
|
|
// Iterate over an array of objects
|
|
let interp = run("
|
|
let arr = [{v: 10}, {v: 20}];
|
|
let total = 0;
|
|
for (obj in arr) { total = total + obj.v; }
|
|
");
|
|
assert_num(&get_var(&interp, "total"), 30.0);
|
|
}
|
|
|
|
#[test]
|
|
fn error_forin_non_iterable() {
|
|
let result = std::panic::catch_unwind(|| {
|
|
run("for (x in 42) { let _ = x; }"); // Can't iterate a number
|
|
});
|
|
assert!(result.is_err(), "Expected error for for-in on non-iterable");
|
|
}
|
|
}
|
|
|