feat: 添加for-in循环支持,更新解析器、解释器和相关测试用例
This commit is contained in:
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub enum TokenKind {
|
|||||||
Else,
|
Else,
|
||||||
While,
|
While,
|
||||||
For,
|
For,
|
||||||
|
In,
|
||||||
Break,
|
Break,
|
||||||
Continue,
|
Continue,
|
||||||
Return,
|
Return,
|
||||||
|
|||||||
@@ -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; }") {
|
||||||
|
|||||||
Reference in New Issue
Block a user