diff --git a/examples/forin.ast b/examples/forin.ast new file mode 100644 index 0000000..cd7476b --- /dev/null +++ b/examples/forin.ast @@ -0,0 +1,25 @@ +// for-in loop demo +print("=== array ===") +let arr = [10, 20, 30] +for (x in arr) { + print(x) +} + +print("=== object keys ===") +let obj = {name: "Aster", year: 2025} +let keys = "" +for (k in obj) { + keys = keys + k + " " +} +print(keys) + +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) +} diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 17f54f3..87b3fb1 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -28,7 +28,7 @@ pub enum Stmt { body: Box, }, - /// for 循环 + /// for 循环 (C-style) For { initializer: Option>, condition: Option, @@ -36,6 +36,13 @@ pub enum Stmt { body: Box, }, + /// for-in 循环: for (var in iterable) body + ForIn { + var_name: String, + iterable: Expr, + body: Box, + }, + /// 函数声明 Function { name: String, diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index a979d38..9f54414 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -102,6 +102,42 @@ impl Interpreter { } Ok(Signal::None) } + Stmt::ForIn { var_name, iterable, body } => { + let iter_val = self.evaluate(iterable)?; + let items: Vec = 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, @@ -1612,5 +1648,87 @@ mod tests { }); 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"); + } } diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index 71f9215..f6a4f86 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -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, diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 38f0995..0ea8c8f 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -32,6 +32,7 @@ pub enum TokenKind { Else, While, For, + In, Break, Continue, Return, diff --git a/src/parser/parser.rs b/src/parser/parser.rs index ab05d89..4fe9a62 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -134,6 +134,20 @@ impl Parser { fn for_statement(&mut self) -> Result { 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; }") {