fix: for-in 整数索引升序+字符串插入序枚举,null/undefined 空遍历

This commit is contained in:
爱特每羊队
2026-06-27 17:15:32 +08:00
parent 6e74881cde
commit ec380802a8
5 changed files with 176 additions and 8 deletions
@@ -0,0 +1,58 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
#[test]
fn for_in_null_empty_loop() {
assert_eq!(eval("var r=0;for(var k in null){r=r+1;}r"), "0", "for-in null iterates zero times");
}
#[test]
fn for_in_undefined_empty_loop() {
assert_eq!(
eval("var r=0;for(var k in undefined){r=r+1;}r"),
"0",
"for-in undefined iterates zero times"
);
}
#[test]
fn for_in_null_does_not_execute_body() {
assert_eq!(
eval("var hit=false;for(var k in null){hit=true;}hit"),
"false",
"for-in null skips the body"
);
}
#[test]
fn for_in_null_does_not_throw() {
// Must NOT produce a "vm error: ..." — a quiet empty loop, then a sentinel value.
assert_eq!(eval("for(var k in null){} 7"), "7", "for-in null must not throw");
}
#[test]
fn for_in_number_primitive_currently_throws() {
// Interim behavior: ToObject coercion for non-null/undefined primitives is not
// implemented yet, so a number right-hand side still throws TypeError. When
// ToObject lands this becomes an empty loop and this assertion should change.
let out = eval("for(var k in 42){}");
assert!(out.contains("TypeError"), "expected TypeError for number primitive, got: {out}");
}
@@ -0,0 +1,82 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {e}"),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{result}"),
Err(e) => format!("vm error: {e}"),
}
}
// Order is asserted via a position map built inside JS, so the comparison
// returns a boolean (JsValue Display only reveals number/bool, not string
// contents). This is also tolerant of any inherited enumerable keys the
// engine appends after the object's own keys.
#[test]
fn for_in_ordering_integer_indices_first() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {b:2,a:1,'2':3,'1':4}){pos[x]=i;i=i+1;}\
pos['1']<pos['2'] && pos['2']<pos['b'] && pos['b']<pos['a']"
),
"true",
"integer indices ascending, then string keys in insertion order"
);
}
#[test]
fn for_in_ordering_string_only_insertion_order() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {c:1,b:2,a:3}){pos[x]=i;i=i+1;}\
pos['c']<pos['b'] && pos['b']<pos['a']"
),
"true",
"string-only keys keep insertion order"
);
}
#[test]
fn for_in_ordering_mixed_boundary() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {a:1,'0':2,b:3}){pos[x]=i;i=i+1;}\
pos['0']<pos['a'] && pos['a']<pos['b']"
),
"true",
"integer index '0' enumerates before string keys"
);
}
#[test]
fn for_in_ordering_integer_indices_are_numeric_not_lexicographic() {
assert_eq!(
eval(
"var pos={};var i=0;for(var x in {'10':1,'2':2,'1':3}){pos[x]=i;i=i+1;}\
pos['1']<pos['2'] && pos['2']<pos['10']"
),
"true",
"index order is 1,2,10 (numeric) not 1,10,2 (lexicographic)"
);
}
#[test]
fn for_in_still_enumerates_own_keys() {
assert_eq!(
eval("var c=0;for(var k in {a:1,b:2}){c=c+1;}c>=2"),
"true",
"for-in still enumerates the object's own enumerable keys"
);
}