From d20eadacccafd64fd63d10917a16659aea07b06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Tue, 9 Jun 2026 17:08:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B3=A8=E5=85=A5=20harness=20?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E9=BD=90=E5=85=B3=E9=94=AE=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=86=85=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 1 + .../tests/compile_expr_tests.rs | 6 + crates/oxide_kernel/src/builtin.rs | 13 ++ crates/oxide_test262/src/main.rs | 182 +++++++++++++----- crates/oxide_vm/src/bindings/bind_array.rs | 1 + crates/oxide_vm/src/bindings/bind_object.rs | 1 + crates/oxide_vm/src/bindings/mod.rs | 2 + crates/oxide_vm/src/builtins/array.rs | 15 ++ crates/oxide_vm/src/builtins/object.rs | 6 + crates/oxide_vm/tests/builtins_array_tests.rs | 9 + .../oxide_vm/tests/builtins_function_tests.rs | 2 +- .../oxide_vm/tests/builtins_globals_tests.rs | 7 + .../oxide_vm/tests/builtins_object_tests.rs | 12 ++ 13 files changed, 209 insertions(+), 48 deletions(-) diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 94534d4..d3b4dd6 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -226,6 +226,7 @@ impl CompileCtx { "NaN", "undefined", "Infinity", + "globalThis", "Object", "Array", "String", diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index b3dd0be..f3c735f 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -398,6 +398,12 @@ fn compile_infinity_ok() { assert!(!module.bytecode.is_empty()); } +#[test] +fn compile_global_this_ok() { + let module = compile_source("globalThis"); + assert!(!module.bytecode.is_empty()); +} + #[test] fn compile_neg_infinity_ok() { let module = compile_source("1 / -Infinity"); diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 3c836a5..27a0f10 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -42,6 +42,7 @@ pub struct ObjectMethods { pub keys: *const (), pub create: *const (), pub assign: *const (), + pub is: *const (), pub define_property: *const (), pub get_own_property_descriptor: *const (), pub freeze: *const (), @@ -60,6 +61,7 @@ pub struct ObjectMethods { } pub struct ArrayMethods { + pub is_array: *const (), pub push: *const (), pub pop: *const (), pub slice: *const (), @@ -389,6 +391,7 @@ impl BuiltinWorld { ("keys", methods.keys, 1), ("create", methods.create, 2), ("assign", methods.assign, 2), + ("is", methods.is, 2), ("defineProperty", methods.define_property, 3), ("getOwnPropertyDescriptor", methods.get_own_property_descriptor, 2), ("freeze", methods.freeze, 1), @@ -408,6 +411,10 @@ impl BuiltinWorld { } pub fn bind_array_methods(&self, methods: &ArrayMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + let ctor_ptr = P::as_ptr(&self.array_constructor) as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + bind_methods!(self, ctor, string_forge, shape_forge, ("isArray", methods.is_array, 1),); + let proto_ptr = P::as_ptr(&self.array_proto) as *mut JsObject; let proto = unsafe { &mut *proto_ptr }; bind_methods!( @@ -591,6 +598,12 @@ impl BuiltinWorld { wrapper.set_function(true); wrapper.set_native_fn(Some(native_fn_ptr)); wrapper.set_native_arg_count(arg_count); + let si_name = string_forge.intern("name").0; + let name_shape = shape_forge.make_shape(wrapper.shape_id(), si_name); + wrapper.set_shape_id(name_shape); + wrapper + .ensure_hash_props() + .push(Box::new(JsValue::string(string_forge.intern(method_name).0, 0))); let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper)); let new_shape = shape_forge.make_shape(proto.shape_id(), si); proto.set_shape_id(new_shape); diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 6f1da40..6f19613 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -1,7 +1,6 @@ #![allow(clippy::arc_with_non_send_sync)] use oxide_compiler::compiler::Compiler; -use oxide_compiler::module::CompiledModule; use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_vm::vm::{init_kernel_builtins, Vm}; use serde::Deserialize; @@ -115,6 +114,115 @@ struct RunConfig { no_skip: bool, } +struct HarnessSources { + sources: HashMap<&'static str, &'static str>, +} + +impl HarnessSources { + fn new() -> Self { + let mut sources = HashMap::new(); + sources.insert("sta.js", include_str!("../../../tests/test262/harness/sta.js")); + sources.insert("assert.js", include_str!("../../../tests/test262/harness/assert.js")); + sources.insert("propertyHelper.js", include_str!("../../../tests/test262/harness/propertyHelper.js")); + sources.insert("compareArray.js", include_str!("../../../tests/test262/harness/compareArray.js")); + sources.insert("fnGlobalObject.js", include_str!("../../../tests/test262/harness/fnGlobalObject.js")); + sources.insert("nans.js", include_str!("../../../tests/test262/harness/nans.js")); + sources.insert("dateConstants.js", include_str!("../../../tests/test262/harness/dateConstants.js")); + sources.insert( + "decimalToHexString.js", + include_str!("../../../tests/test262/harness/decimalToHexString.js"), + ); + sources.insert("isConstructor.js", include_str!("../../../tests/test262/harness/isConstructor.js")); + sources.insert("nativeErrors.js", include_str!("../../../tests/test262/harness/nativeErrors.js")); + sources.insert( + "nativeFunctionMatcher.js", + include_str!("../../../tests/test262/harness/nativeFunctionMatcher.js"), + ); + sources.insert("regExpUtils.js", include_str!("../../../tests/test262/harness/regExpUtils.js")); + sources.insert( + "assertRelativeDateMs.js", + include_str!("../../../tests/test262/harness/assertRelativeDateMs.js"), + ); + sources.insert( + "wellKnownIntrinsicObjects.js", + include_str!("../../../tests/test262/harness/wellKnownIntrinsicObjects.js"), + ); + sources.insert("typeCoercion.js", include_str!("../../../tests/test262/harness/typeCoercion.js")); + sources.insert("deepEqual.js", include_str!("../../../tests/test262/harness/deepEqual.js")); + Self { sources } + } + + fn get(&self, name: &str) -> Option<&'static str> { + self.sources.get(name).copied() + } +} + +fn is_blacklisted_harness(name: &str) -> bool { + matches!( + name, + "testTypedArray.js" + | "testIntl.js" + | "testAtomics.js" + | "atomicsHelper.js" + | "proxyTrapsHelper.js" + | "temporalHelpers.js" + | "tcoHelper.js" + | "asyncHelpers.js" + | "promiseHelper.js" + | "detachArrayBuffer.js" + | "resizableArrayBufferUtils.js" + | "byteConversionValues.js" + | "compareIterator.js" + | "iteratorZipUtils.js" + | "doneprintHandle.js" + ) +} + +fn test262_error_prelude() -> &'static str { + r#" +function Test262Error(message) { + this.message = message; + this.name = "Test262Error"; +} +Test262Error.prototype = new Error(); +Test262Error.prototype.constructor = Test262Error; +"# +} + +fn append_source_chunk(out: &mut String, name: &str, source: &str) { + out.push_str("\n// ---- test262 harness: "); + out.push_str(name); + out.push_str(" ----\n"); + out.push_str(source); + out.push('\n'); +} + +fn build_test_source(meta: &TestMeta, source: &str, harness: &HarnessSources) -> Result { + let mut full_source = String::new(); + append_source_chunk(&mut full_source, "Test262Error prelude", test262_error_prelude()); + append_source_chunk( + &mut full_source, + "sta.js", + harness.get("sta.js").ok_or_else(|| String::from("unknown harness: sta.js"))?, + ); + append_source_chunk( + &mut full_source, + "assert.js", + harness + .get("assert.js") + .ok_or_else(|| String::from("unknown harness: assert.js"))?, + ); + for include in &meta.includes { + if is_blacklisted_harness(include) { + return Err(format!("out-of-scope harness: {include}")); + } + let include_source = harness.get(include).ok_or_else(|| format!("unknown harness: {include}"))?; + append_source_chunk(&mut full_source, include, include_source); + } + append_source_chunk(&mut full_source, "test source", strip_meta(source)); + Ok(full_source) +} + impl RunConfig { fn parse(args: &[String]) -> Result { let mut config = Self::default(); @@ -223,33 +331,13 @@ fn is_skipped(meta: &TestMeta) -> Option { None } -fn compile_harness(_kernel: &Arc) -> Result { - let prelude = r#" -function Test262Error(message) { - this.message = message; - this.name = "Test262Error"; -} -Test262Error.prototype = new Error(); -Test262Error.prototype.constructor = Test262Error; -"#; - let assert_js = include_str!("../../../tests/test262/harness/assert.js"); - let full_source = format!("{prelude}\n{assert_js}"); - - let alloc = oxide_parser::Allocator::default(); - let program = - oxide_parser::parse(&alloc, &full_source).map_err(|e| format!("harness parse error: {}", e[0].message))?; - - Compiler::new().compile(&program) -} - fn run_test( - path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness_module: &CompiledModule, - no_skip: bool, + path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness: &HarnessSources, no_skip: bool, ) -> TestResult { let start = std::time::Instant::now(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - run_test_inner(path, source, meta, kernel, harness_module, no_skip) + run_test_inner(path, source, meta, kernel, harness, no_skip) })); match result { @@ -262,15 +350,23 @@ fn run_test( } fn run_test_inner( - path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness_module: &CompiledModule, - no_skip: bool, + path: &Path, source: &str, meta: &TestMeta, kernel: &Arc, harness: &HarnessSources, no_skip: bool, ) -> TestResult { let start = std::time::Instant::now(); - let code = strip_meta(source); + let code = match build_test_source(meta, source, harness) { + Ok(code) => code, + Err(e) => { + let dur = start.elapsed().as_millis() as u64; + if no_skip { + return TestResult::fail(path.to_path_buf(), dur, e); + } + return TestResult::skip(path.to_path_buf(), e); + } + }; let alloc = oxide_parser::Allocator::default(); - let program = match oxide_parser::parse(&alloc, code) { + let program = match oxide_parser::parse(&alloc, &code) { Ok(p) => p, Err(errs) => { let dur = start.elapsed().as_millis() as u64; @@ -314,15 +410,6 @@ fn run_test_inner( let mut vm = Vm::with_kernel(Arc::clone(kernel)); - if let Err(e) = vm.run(harness_module) { - let dur = start.elapsed().as_millis() as u64; - let msg = format!("harness injection failed: {e}"); - if no_skip { - return TestResult::fail(path.to_path_buf(), dur, msg); - } - return TestResult::skip(path.to_path_buf(), msg); - } - match vm.run(&module) { Ok(result) => { let dur = start.elapsed().as_millis() as u64; @@ -417,6 +504,14 @@ fn categorize_fail(msg: &str) -> String { } } else if msg.contains("engine panic") { "engine panic".into() + } else if msg.contains("out-of-scope harness:") { + "harness: blacklisted".into() + } else if msg.contains("unknown harness:") { + "harness: unknown".into() + } else if msg.contains("harness compile error:") { + "harness: compile error".into() + } else if msg.contains("harness runtime error:") { + "harness: runtime error".into() } else if msg.contains("expected runtime error") { "expected runtime error".into() } else { @@ -471,7 +566,7 @@ fn run_tests() -> bool { std::process::exit(1); } - let filter = config.filter.clone(); + let filter = config.filter.clone().map(|f| f.replace('\\', "/")); eprintln!("discovering tests in: {}", test262_root.display()); if config.no_skip { @@ -482,14 +577,7 @@ fn run_tests() -> bool { let kernel = Arc::new(OxideKernel::new(KernelConfig::minimal())); init_kernel_builtins(&kernel); - - let harness_module = match compile_harness(&kernel) { - Ok(m) => m, - Err(e) => { - eprintln!("FATAL: Failed to compile test262 harness: {e}"); - std::process::exit(1); - } - }; + let harness_sources = HarnessSources::new(); let mut stats = RunStats::default(); let mut results: Vec = Vec::new(); @@ -527,7 +615,7 @@ fn run_tests() -> bool { }; if let Some(filter_str) = &filter { - let path_str = path.to_string_lossy(); + let path_str = path.to_string_lossy().replace('\\', "/"); if !path_str.contains(filter_str.as_str()) { results.push(TestResult::skip(path.clone(), "filtered".into())); stats.skip += 1; @@ -550,7 +638,7 @@ fn run_tests() -> bool { } } - let result = run_test(path, &source, &meta, &kernel, &harness_module, config.no_skip); + let result = run_test(path, &source, &meta, &kernel, &harness_sources, config.no_skip); match &result.outcome { TestOutcome::Pass(_) => stats.pass += 1, TestOutcome::Fail(msg) => { diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index efa664c..57c475c 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -7,6 +7,7 @@ use oxide_types::object::JsObject; pub fn bind_array(kernel: &Arc, global: &mut JsObject) { let _array_methods = ArrayMethods { + is_array: crate::builtins::array::array_is_array as *const (), push: crate::builtins::array::array_push as *const (), pop: crate::builtins::array::array_pop as *const (), slice: crate::builtins::array::array_slice as *const (), diff --git a/crates/oxide_vm/src/bindings/bind_object.rs b/crates/oxide_vm/src/bindings/bind_object.rs index 54fac79..7e0d81c 100644 --- a/crates/oxide_vm/src/bindings/bind_object.rs +++ b/crates/oxide_vm/src/bindings/bind_object.rs @@ -10,6 +10,7 @@ pub fn bind_object(kernel: &Arc, global: &mut JsObject) { keys: crate::builtins::object::object_keys as *const (), create: crate::builtins::object::object_create as *const (), assign: crate::builtins::object::object_assign as *const (), + is: crate::builtins::object::object_is as *const (), define_property: crate::builtins::object::object_define_property as *const (), get_own_property_descriptor: crate::builtins::object::object_get_own_property_descriptor as *const (), freeze: crate::builtins::object::object_freeze as *const (), diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index b6c393a..751929f 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -90,4 +90,6 @@ pub fn init_kernel_builtins(kernel: &Arc) { bind_function::bind_function(kernel); bind_regexp::bind_regexp(kernel, global); bind_symbol::bind_symbol(kernel, global); + + bind_global_value(kernel, global, "globalThis", JsValue::from_js_object(global_ptr)); } diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index f26f064..79be100 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -105,6 +105,21 @@ pub fn array_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { Ok(JsValue::from_js_object(arr)) } +pub fn array_is_array(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let val = vm.reg(args[1]); + if !val.is_object() { + return Ok(JsValue::bool(false)); + } + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + return Ok(JsValue::bool(false)); + } + Ok(JsValue::bool(unsafe { &*ptr }.is_array())) +} + pub fn array_push(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr_ptr = array_ptr!(vm, args); let arr = unsafe { &mut *arr_ptr }; diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index e699052..89ce3c9 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -131,6 +131,12 @@ pub fn object_assign(vm: &mut Vm, args: &[u8]) -> NativeResult { Ok(target_val) } +pub fn object_is(vm: &mut Vm, args: &[u8]) -> NativeResult { + let lhs = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let rhs = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + Ok(JsValue::bool(coercion::same_value(lhs, rhs))) +} + pub fn object_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 4 { return Err(JsValue::undefined()); diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index 5c3790c..1804e6a 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -187,6 +187,15 @@ fn array_empty_literal() { assert_eq!(obj.prop_count(), 0); } +#[test] +fn array_is_array_static_method() { + let (_vm, result) = eval("Array.isArray([])").unwrap(); + assert_eq!(result, JsValue::bool(true)); + + let (_vm, result) = eval("Array.isArray({})").unwrap(); + assert_eq!(result, JsValue::bool(false)); +} + #[test] fn array_shift_removes_first() { let (_vm, result) = eval("[1,2,3].shift()").unwrap(); diff --git a/crates/oxide_vm/tests/builtins_function_tests.rs b/crates/oxide_vm/tests/builtins_function_tests.rs index 0624041..cf9746b 100644 --- a/crates/oxide_vm/tests/builtins_function_tests.rs +++ b/crates/oxide_vm/tests/builtins_function_tests.rs @@ -36,7 +36,7 @@ fn function_to_string_includes_function() { let result = eval(&mut vm, "Math.max.toString()").unwrap(); assert!(result.is_string()); let rendered = vm.kernel().string_forge().lookup(result.as_string_index()).unwrap_or_default(); - assert!(rendered.contains("function"), "got: {}", rendered); + assert_eq!(rendered, "function max() { [native code] }"); } #[test] diff --git a/crates/oxide_vm/tests/builtins_globals_tests.rs b/crates/oxide_vm/tests/builtins_globals_tests.rs index 37032e5..dfbc85a 100644 --- a/crates/oxide_vm/tests/builtins_globals_tests.rs +++ b/crates/oxide_vm/tests/builtins_globals_tests.rs @@ -76,3 +76,10 @@ fn division_by_zero_gives_infinity() { assert!(result.is_double()); assert!(result.as_double().is_infinite()); } + +#[test] +fn global_this_points_to_global_object() { + let result = eval("globalThis.Object === Object").unwrap(); + assert!(result.is_bool()); + assert!(result.as_bool()); +} diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 5ff5520..daba6a2 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -37,6 +37,18 @@ fn object_assign_copies() { assert!(result.is_object()); } +#[test] +fn object_is_uses_same_value_semantics() { + let (_vm, result) = eval("Object.is(NaN, NaN)").unwrap(); + assert_eq!(result, JsValue::bool(true)); + + let (_vm, result) = eval("Object.is(1, 1)").unwrap(); + assert_eq!(result, JsValue::bool(true)); + + let (_vm, result) = eval("Object.is(1, '1')").unwrap(); + assert_eq!(result, JsValue::bool(false)); +} + #[test] fn object_get_own_property_descriptor_value() { let (_vm, result) = eval("Object.getOwnPropertyDescriptor({a:1},'a')").unwrap();