feat: 添加for-in循环支持,更新解析器、解释器和相关测试用例

This commit is contained in:
0264408
2026-06-16 19:10:26 +08:00
parent 75c1d230ad
commit 8b20580402
6 changed files with 215 additions and 3 deletions
+25
View File
@@ -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)
}
+8 -1
View File
@@ -28,7 +28,7 @@ pub enum Stmt {
body: Box<Stmt>, body: Box<Stmt>,
}, },
/// for 循环 /// for 循环 (C-style)
For { For {
initializer: Option<Box<Stmt>>, initializer: Option<Box<Stmt>>,
condition: Option<Expr>, condition: Option<Expr>,
@@ -36,6 +36,13 @@ pub enum Stmt {
body: Box<Stmt>, body: Box<Stmt>,
}, },
/// for-in 循环: for (var in iterable) body
ForIn {
var_name: String,
iterable: Expr,
body: Box<Stmt>,
},
/// 函数声明 /// 函数声明
Function { Function {
name: String, name: String,
+118
View File
@@ -102,6 +102,42 @@ impl Interpreter {
} }
Ok(Signal::None) 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 } => { Stmt::Function { name, params, body } => {
let func = Value::Function(Rc::new(Function { let func = Value::Function(Rc::new(Function {
params, params,
@@ -1612,5 +1648,87 @@ mod tests {
}); });
assert!(result.is_err(), "Expected error for pop on non-array"); 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");
}
} }
+3 -2
View File
@@ -322,6 +322,7 @@ impl Lexer {
"else" => TokenKind::Else, "else" => TokenKind::Else,
"while" => TokenKind::While, "while" => TokenKind::While,
"for" => TokenKind::For, "for" => TokenKind::For,
"in" => TokenKind::In,
"break" => TokenKind::Break, "break" => TokenKind::Break,
"continue" => TokenKind::Continue, "continue" => TokenKind::Continue,
"return" => TokenKind::Return, "return" => TokenKind::Return,
@@ -534,11 +535,11 @@ mod tests {
#[test] #[test]
fn keywords() { 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![ assert_eq!(kinds, vec![
TokenKind::Let, TokenKind::Const, TokenKind::Fn, TokenKind::Let, TokenKind::Const, TokenKind::Fn,
TokenKind::If, TokenKind::Else, TokenKind::If, TokenKind::Else,
TokenKind::While, TokenKind::For, TokenKind::While, TokenKind::For, TokenKind::In,
TokenKind::Break, TokenKind::Continue, TokenKind::Break, TokenKind::Continue,
TokenKind::Return, TokenKind::Return,
TokenKind::True, TokenKind::False, TokenKind::True, TokenKind::False,
+1
View File
@@ -32,6 +32,7 @@ pub enum TokenKind {
Else, Else,
While, While,
For, For,
In,
Break, Break,
Continue, Continue,
Return, Return,
+60
View File
@@ -134,6 +134,20 @@ impl Parser {
fn for_statement(&mut self) -> Result<Stmt, RuntimeError> { fn for_statement(&mut self) -> Result<Stmt, RuntimeError> {
self.consume(TokenKind::LeftParen, "Expected '(' after 'for'.")?; 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]) { let initializer = if self.match_kind(&[TokenKind::Semicolon]) {
None None
} else if self.match_kind(&[TokenKind::Let]) { } else if self.match_kind(&[TokenKind::Let]) {
@@ -534,6 +548,10 @@ impl Parser {
&self.tokens[self.current] &self.tokens[self.current]
} }
fn peek_next(&self) -> &Token {
&self.tokens[self.current + 1]
}
fn previous(&self) -> &Token { fn previous(&self) -> &Token {
&self.tokens[self.current - 1] &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] #[test]
fn parse_block() { fn parse_block() {
match parse_stmt("{ let x = 1; x; }") { match parse_stmt("{ let x = 1; x; }") {