Compare commits
4 Commits
db541c315b
...
5e431dbba4
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e431dbba4 | |||
| 8b20580402 | |||
| 75c1d230ad | |||
| 18fa53cf4a |
@@ -0,0 +1,21 @@
|
||||
// Quick smoke test for new builtins
|
||||
print("--- len ---")
|
||||
print(len("hello"))
|
||||
print(len([1, 2, 3]))
|
||||
print(len({a: 1, b: 2, c: 3}))
|
||||
|
||||
print("--- typeof ---")
|
||||
print(typeof(42))
|
||||
print(typeof("hi"))
|
||||
print(typeof(true))
|
||||
print(typeof(nil))
|
||||
print(typeof([1, 2]))
|
||||
print(typeof({a: 1}))
|
||||
print(typeof(print))
|
||||
|
||||
print("--- push/pop ---")
|
||||
let arr = [1, 2]
|
||||
print("push:", push(arr, 3))
|
||||
print("arr:", arr)
|
||||
print("pop:", pop(arr))
|
||||
print("arr:", arr)
|
||||
@@ -0,0 +1,23 @@
|
||||
// for-in loop demo
|
||||
print("=== array ===")
|
||||
let arr = [10, 20, 30]
|
||||
for (x in arr) {
|
||||
print(x)
|
||||
}
|
||||
|
||||
print("=== object keys + values ===")
|
||||
let obj = {name: "Aster", year: 2025}
|
||||
for (k in obj) {
|
||||
print(k + ": " + obj[k])
|
||||
}
|
||||
|
||||
print("=== string chars ===")
|
||||
for (c in "Hi") {
|
||||
print(c)
|
||||
}
|
||||
|
||||
print("=== break ===")
|
||||
for (x in [1, 2, 3, 4, 5]) {
|
||||
if (x == 3) { break }
|
||||
print(x)
|
||||
}
|
||||
+8
-1
@@ -28,7 +28,7 @@ pub enum Stmt {
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// for 循环
|
||||
/// for 循环 (C-style)
|
||||
For {
|
||||
initializer: Option<Box<Stmt>>,
|
||||
condition: Option<Expr>,
|
||||
@@ -36,6 +36,13 @@ pub enum Stmt {
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// for-in 循环: for (var in iterable) body
|
||||
ForIn {
|
||||
var_name: String,
|
||||
iterable: Expr,
|
||||
body: Box<Stmt>,
|
||||
},
|
||||
|
||||
/// 函数声明
|
||||
Function {
|
||||
name: String,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! 标准库:core(len, typeof, push, pop)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
|
||||
pub fn len(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "len() expects 1 argument".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(s) => Ok(Value::Number(s.chars().count() as f64)),
|
||||
Value::Array(arr) => Ok(Value::Number(arr.borrow().len() as f64)),
|
||||
Value::Object(obj) => Ok(Value::Number(obj.borrow().len() as f64)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "len() expects a string, array, or object".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn typeof_fn(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "typeof() expects 1 argument".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let s = match &args[0] {
|
||||
Value::Number(_) => "number",
|
||||
Value::String(_) => "string",
|
||||
Value::Bool(_) => "bool",
|
||||
Value::Nil => "nil",
|
||||
Value::Object(_) => "object",
|
||||
Value::Array(_) => "array",
|
||||
Value::Function(_) => "function",
|
||||
Value::NativeFunction(_) => "native_function",
|
||||
};
|
||||
Ok(Value::String(s.into()))
|
||||
}
|
||||
|
||||
pub fn push(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.len() < 2 {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "push() expects 2 arguments (array, value)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
let val = args[1].clone();
|
||||
match &args[0] {
|
||||
Value::Array(arr) => {
|
||||
arr.borrow_mut().push(val.clone());
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "push() expects an array as first argument".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if args.is_empty() {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: "pop() expects 1 argument (array)".into(),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Array(arr) => arr
|
||||
.borrow_mut()
|
||||
.pop()
|
||||
.ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: "pop() on empty array".into(),
|
||||
token: None,
|
||||
}),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "pop() expects an array as first argument".into(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
//! 标准库:io(print, input)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
|
||||
pub fn print(args: Vec<Value>) -> Value {
|
||||
pub fn print(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
for arg in args.iter() {
|
||||
print!("{}", arg);
|
||||
}
|
||||
println!();
|
||||
Value::Nil
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
|
||||
pub fn input(args: Vec<Value>) -> Value {
|
||||
pub fn input(args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
if let Some(prompt) = args.get(0) {
|
||||
print!("{}", prompt);
|
||||
std::io::Write::flush(&mut std::io::stdout()).unwrap();
|
||||
}
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_line(&mut buffer).unwrap();
|
||||
Value::String(buffer.trim_end().to_string())
|
||||
Ok(Value::String(buffer.trim_end().to_string()))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! 内置函数模块:按功能分组注册,便于扩展和维护。
|
||||
|
||||
mod core;
|
||||
mod io;
|
||||
mod os;
|
||||
|
||||
@@ -27,4 +28,10 @@ pub fn register_all(env: &Rc<RefCell<Env>>) {
|
||||
e.define("os".into(), Value::Object(Rc::new(RefCell::new(os))), true);
|
||||
// clock can also be used as a global function for convenience
|
||||
e.define("clock".into(), Value::NativeFunction(os::clock), true);
|
||||
|
||||
// core — len, typeof, push, pop
|
||||
e.define("len".into(), Value::NativeFunction(core::len), true);
|
||||
e.define("typeof".into(), Value::NativeFunction(core::typeof_fn), true);
|
||||
e.define("push".into(), Value::NativeFunction(core::push), true);
|
||||
e.define("pop".into(), Value::NativeFunction(core::pop), true);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! 标准库:os(clock)
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
|
||||
pub fn clock(_args: Vec<Value>) -> Value {
|
||||
Value::Number(
|
||||
pub fn clock(_args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
Ok(Value::Number(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs_f64(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
+515
-27
@@ -102,6 +102,42 @@ impl Interpreter {
|
||||
}
|
||||
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,
|
||||
@@ -208,10 +244,24 @@ impl Interpreter {
|
||||
}
|
||||
}
|
||||
Expr::IndexGet { array, index } => {
|
||||
let arr = self.evaluate(*array)?;
|
||||
let idx = self.evaluate(*index)?;
|
||||
let i = self.as_array_index(&idx)?;
|
||||
match arr {
|
||||
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 {
|
||||
@@ -227,17 +277,40 @@ impl Interpreter {
|
||||
})
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Index access on non-array, non-string value".to_string(),
|
||||
message: "Index access on non-array, non-string, non-object value".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Expr::IndexSet { array, index, op, value } => {
|
||||
let arr = self.evaluate(*array)?;
|
||||
let idx = self.evaluate(*index)?;
|
||||
let target = self.evaluate(*array)?;
|
||||
let idx_val = self.evaluate(*index)?;
|
||||
let rhs = self.evaluate(*value)?;
|
||||
let i = self.as_array_index(&idx)?;
|
||||
match arr {
|
||||
// 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() {
|
||||
@@ -257,7 +330,7 @@ impl Interpreter {
|
||||
Ok(val)
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Index assignment on non-array value".to_string(),
|
||||
message: "Index assignment on non-array, non-object value".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
@@ -294,14 +367,20 @@ impl Interpreter {
|
||||
let l = self.evaluate(*left)?;
|
||||
let r = self.evaluate(*right)?;
|
||||
match op {
|
||||
crate::ast::expr::BinaryOp::Add => match (l, r) {
|
||||
(Value::Number(a), Value::Number(b)) => Ok(Value::Number(a+b)),
|
||||
(Value::String(a), Value::String(b)) => Ok(Value::String(a+&b)),
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: "Invalid '+' operands".to_string(),
|
||||
token: None,
|
||||
}),
|
||||
},
|
||||
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 {
|
||||
@@ -391,7 +470,7 @@ impl Interpreter {
|
||||
fn call_function(&mut self, func_val: Value, args: Vec<Value>) -> Result<Value, RuntimeError> {
|
||||
match func_val {
|
||||
Value::NativeFunction(native_fn) => {
|
||||
Ok(native_fn(args))
|
||||
native_fn(args)
|
||||
}
|
||||
Value::Function(f) => {
|
||||
let env = Rc::new(RefCell::new(Env::new(Some(Rc::clone(&f.env)))));
|
||||
@@ -468,9 +547,13 @@ impl Interpreter {
|
||||
match op {
|
||||
AssignOp::Equal => Ok(right),
|
||||
AssignOp::PlusEqual => {
|
||||
let left_num = self.as_number(&left)?;
|
||||
let right_num = self.as_number(&right)?;
|
||||
Ok(Value::Number(left_num + right_num))
|
||||
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)?;
|
||||
@@ -667,6 +750,55 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
// =========================================================================
|
||||
@@ -1324,11 +1456,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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");
|
||||
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]
|
||||
@@ -1362,5 +1494,361 @@ mod tests {
|
||||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use std::fmt;
|
||||
|
||||
pub type NativeFn = fn(Vec<Value>) -> Value;
|
||||
pub type NativeFn = fn(Vec<Value>) -> Result<Value, crate::error::RuntimeError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Value {
|
||||
|
||||
+3
-2
@@ -322,6 +322,7 @@ impl Lexer {
|
||||
"else" => TokenKind::Else,
|
||||
"while" => TokenKind::While,
|
||||
"for" => TokenKind::For,
|
||||
"in" => TokenKind::In,
|
||||
"break" => TokenKind::Break,
|
||||
"continue" => TokenKind::Continue,
|
||||
"return" => TokenKind::Return,
|
||||
@@ -534,11 +535,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn keywords() {
|
||||
let kinds = tokenize("let const fn if else while for break continue return true false nil");
|
||||
let kinds = tokenize("let const fn if else while for in break continue return true false nil");
|
||||
assert_eq!(kinds, vec![
|
||||
TokenKind::Let, TokenKind::Const, TokenKind::Fn,
|
||||
TokenKind::If, TokenKind::Else,
|
||||
TokenKind::While, TokenKind::For,
|
||||
TokenKind::While, TokenKind::For, TokenKind::In,
|
||||
TokenKind::Break, TokenKind::Continue,
|
||||
TokenKind::Return,
|
||||
TokenKind::True, TokenKind::False,
|
||||
|
||||
@@ -32,6 +32,7 @@ pub enum TokenKind {
|
||||
Else,
|
||||
While,
|
||||
For,
|
||||
In,
|
||||
Break,
|
||||
Continue,
|
||||
Return,
|
||||
|
||||
@@ -134,6 +134,20 @@ impl Parser {
|
||||
|
||||
fn for_statement(&mut self) -> Result<Stmt, RuntimeError> {
|
||||
self.consume(TokenKind::LeftParen, "Expected '(' after 'for'.")?;
|
||||
|
||||
// Detect for-in: for (Identifier in expr) body
|
||||
if let TokenKind::Identifier(var_name) = &self.peek().kind {
|
||||
let var_name = var_name.clone();
|
||||
if matches!(self.peek_next().kind, TokenKind::In) {
|
||||
self.advance(); // consume identifier
|
||||
self.advance(); // consume 'in'
|
||||
let iterable = self.expression()?;
|
||||
self.consume(TokenKind::RightParen, "Expected ')' after for-in expression.")?;
|
||||
let body = Box::new(self.statement()?);
|
||||
return Ok(Stmt::ForIn { var_name, iterable, body });
|
||||
}
|
||||
}
|
||||
|
||||
let initializer = if self.match_kind(&[TokenKind::Semicolon]) {
|
||||
None
|
||||
} else if self.match_kind(&[TokenKind::Let]) {
|
||||
@@ -534,6 +548,10 @@ impl Parser {
|
||||
&self.tokens[self.current]
|
||||
}
|
||||
|
||||
fn peek_next(&self) -> &Token {
|
||||
&self.tokens[self.current + 1]
|
||||
}
|
||||
|
||||
fn previous(&self) -> &Token {
|
||||
&self.tokens[self.current - 1]
|
||||
}
|
||||
@@ -1257,6 +1275,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_array() {
|
||||
match parse_stmt("for (x in arr) { print(x); }") {
|
||||
Stmt::ForIn { var_name, iterable, body } => {
|
||||
assert_eq!(var_name, "x");
|
||||
assert!(matches!(iterable, Expr::Variable(_)));
|
||||
assert!(matches!(*body, Stmt::Block(_)));
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_with_object_literal() {
|
||||
match parse_stmt("for (k in {a:1,b:2}) { print(k); }") {
|
||||
Stmt::ForIn { var_name, iterable, .. } => {
|
||||
assert_eq!(var_name, "k");
|
||||
assert!(matches!(iterable, Expr::ObjectLiteral { .. }));
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_with_expression() {
|
||||
match parse_stmt("for (v in get_list()) { }") {
|
||||
Stmt::ForIn { var_name, .. } => {
|
||||
assert_eq!(var_name, "v");
|
||||
}
|
||||
e => panic!("Expected ForIn, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_for_in_distinct_from_c_style() {
|
||||
// "for (x = 0; ...)" is C-style for, not for-in
|
||||
match parse_stmt("for (x = 0; x < 10; x = x + 1) { }") {
|
||||
Stmt::For { .. } => {}
|
||||
e => panic!("Expected C-style For, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_block() {
|
||||
match parse_stmt("{ let x = 1; x; }") {
|
||||
|
||||
Reference in New Issue
Block a user