From 4e2644016b54b6f80597bbf705ed1c5736dd7a14 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 16 Aug 2026 02:47:02 +0800 Subject: [PATCH] =?UTF-8?q?test:=20$262=20=E5=AE=BF=E4=B8=BB=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=9C=80=E5=B0=8F=E7=BB=91=E5=AE=9A=20+=20is=20not=20?= =?UTF-8?q?defined=20=E5=88=A4=E5=AE=9A=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引擎侧:绑定 $262 宿主对象(evalScript/global/gc 可用,detachArrayBuffer/ createRealm/agent 抛 not supported 而非 ReferenceError),并加入编译期 已知全局表;引用 $262.* 的测试从全量 skip 转为真实执行。 runner 侧:is not defined 不再一票吞 skip——解析错误消息中的标识符名, 仅在缺失全局白名单($262/structuredClone/queueMicrotask)内才按能力 缺失跳过;其余 is not defined 按真实失败计入,修复后应转 PASS 的 测试恢复可见。 --- crates/oxide_emit/src/emit.rs | 2 + crates/oxide_test262/src/main.rs | 138 ++++++++++++++++---- crates/oxide_vm/src/bindings/mod.rs | 2 + crates/oxide_vm/src/lib.rs | 2 + crates/oxide_vm/src/test262_host.rs | 113 ++++++++++++++++ crates/oxide_vm/tests/test262_host_tests.rs | 75 +++++++++++ 6 files changed, 304 insertions(+), 28 deletions(-) create mode 100644 crates/oxide_vm/src/test262_host.rs create mode 100644 crates/oxide_vm/tests/test262_host_tests.rs diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 7bba8bb3..ca1efdd5 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -126,6 +126,8 @@ const BUILTIN_GLOBALS: &[&str] = &[ "decodeURI", "encodeURIComponent", "decodeURIComponent", + // test262 宿主对象:编译期按已知全局解析,运行期由 VM 绑定(详见 bind_test262_host)。 + "$262", ]; pub(crate) struct FieldBuffer { diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index a2f45678..9a4d33fd 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -543,25 +543,11 @@ fn run_test_inner( Err(e) => { let dur = start.elapsed().as_millis() as u64; let msg = format!("compile error: {e}"); - if meta.negative.is_some() { - return TestResult::pass(path.to_path_buf(), dur, msg); + match classify_compile_error(&msg, meta.negative.is_some(), no_skip) { + TestOutcome::Pass(m) => return TestResult::pass(path.to_path_buf(), dur, m), + TestOutcome::Fail(m) => return TestResult::fail(path.to_path_buf(), dur, m), + TestOutcome::Skip(m) => return TestResult::skip(path.to_path_buf(), m), } - if e.contains("not yet implemented") - || e.contains("not yet supported") - || e.contains("not supported") - || e.contains("unsupported") - || e.contains("is not defined") - || e.contains("SpreadElement") - || e.contains("already been declared") - || e.contains("parser panicked") - || e.contains("too many registers") - { - if no_skip { - return TestResult::fail(path.to_path_buf(), dur, msg); - } - return TestResult::skip(path.to_path_buf(), msg); - } - return TestResult::fail(path.to_path_buf(), dur, msg); } }; @@ -659,12 +645,68 @@ fn judge_vm_error(path: &Path, e: &str, meta: &TestMeta, dur: u64, no_skip: bool } } +/// 从 `is not defined` 错误消息中解析未绑定标识符名。 +/// +/// # 边界与前提 +/// - 运行期形态:`uncaught ReferenceError: {name} is not defined` +/// - 编译期形态:`Identifier '{name}' is not defined`(可带 `compile error: ` 前缀) +/// - 两种形态都不匹配返回 `None`(不属未绑定标识符错误)。 +fn parse_undefined_ident(e: &str) -> Option<&str> { + let e = e.strip_prefix("compile error: ").unwrap_or(e); + let e = e.strip_prefix("uncaught ").unwrap_or(e); + if let Some(rest) = e.strip_prefix("ReferenceError: ") { + if let Some(ident) = rest.strip_suffix(" is not defined") { + let ident = ident.trim(); + return (!ident.is_empty()).then_some(ident); + } + } + if let Some(rest) = e.strip_prefix("Identifier '") { + if let Some(ident) = rest.strip_suffix("' is not defined") { + let ident = ident.trim(); + return (!ident.is_empty()).then_some(ident); + } + } + None +} + +/// 已知缺失的宿主/标准全局白名单:这些标识符未绑定是能力缺失(skip), +/// 其它 `is not defined` 是引擎回归或语义缺口(fail)。 +const KNOWN_MISSING_GLOBALS: &[&str] = &["$262", "structuredClone", "queueMicrotask"]; + +/// 判定编译期错误结果:compile 错误无条件放行 negative;能力未实现形态 +/// (含白名单内的未绑定标识符)→ Skip(`--no-skip` 下为 Fail);其余一律 Fail。 +fn classify_compile_error(e: &str, has_negative: bool, no_skip: bool) -> TestOutcome { + if has_negative { + return TestOutcome::Pass(format!("compile error: {e}")); + } + let unimplemented = e.contains("not yet implemented") + || e.contains("not yet supported") + || e.contains("not supported") + || e.contains("unsupported") + || e.contains("SpreadElement") + || e.contains("already been declared") + || e.contains("parser panicked") + || e.contains("too many registers"); + // `is not defined` 仅当标识符在缺失全局白名单内才 skip;其余(引擎该有 + // 而未提供的标识符、回归)按真实失败计入,保证修复可见性。 + let whitelisted_undefined = + e.contains("is not defined") && parse_undefined_ident(e).is_some_and(|i| KNOWN_MISSING_GLOBALS.contains(&i)); + if unimplemented || whitelisted_undefined { + if no_skip { + return TestOutcome::Fail(format!("compile error: {e}")); + } + return TestOutcome::Skip(format!("compile error: {e}")); + } + TestOutcome::Fail(format!("compile error: {e}")) +} + /// 判定运行期错误结果:negative 期望匹配 → Pass;能力未实现形态 → Skip /// (`--no-skip` 下为 Fail);其余一律 Fail。 /// /// # 边界与前提 /// - receiver 校验、栈溢出、不可调用、ToPrimitive 缺口等错误是引擎语义与测试 /// 期望不符的真实失败,不再被 skip 子串吞没(引擎做对了反而计 skip 属误判)。 +/// - `is not defined` 是否 skip 由标识符白名单判定(见 [`parse_undefined_ident`])。 fn classify_vm_error(e: &str, neg: Option<&Negative>, no_skip: bool) -> TestOutcome { if let Some(neg) = neg { if e.contains("TypeError") && neg.error_type == "TypeError" { @@ -681,12 +723,11 @@ fn classify_vm_error(e: &str, neg: Option<&Negative>, no_skip: bool) -> TestOutc } return TestOutcome::Fail(format!("expected {} error, got: {e}", neg.error_type)); } - if e.contains("not yet implemented") + let unimplemented = e.contains("not yet implemented") || e.contains("not yet supported") || e.contains("not supported") || e.contains("unsupported") || e.contains("step limit") - || e.contains("is not defined") || e.contains("NEW_EXPRESSION") || e.contains("IC_GET_PROP on non-object") || e.contains("GET_PROP_DYNAMIC on non-object") @@ -695,17 +736,18 @@ fn classify_vm_error(e: &str, neg: Option<&Negative>, no_skip: bool) -> TestOutc || e.contains("CALL_NATIVE target") || e.contains("is not implemented") || e.contains("unexpected tail call") - || e.contains("__proto__ must be an object") - // 私有字段未实现。 - { + || e.contains("__proto__ must be an object"); + // `is not defined` 不再一票吞 skip:仅在标识符属缺失全局白名单时按能力 + // 缺失跳过;否则是真实 ReferenceError(含修复后应转 PASS 的回归)。 + let whitelisted_undefined = + e.contains("is not defined") && parse_undefined_ident(e).is_some_and(|i| KNOWN_MISSING_GLOBALS.contains(&i)); + if unimplemented || whitelisted_undefined { if no_skip { - TestOutcome::Fail(format!("vm error: {e}")) - } else { - TestOutcome::Skip(format!("vm: {e}")) + return TestOutcome::Fail(format!("vm error: {e}")); } - } else { - TestOutcome::Fail(format!("vm error: {e}")) + return TestOutcome::Skip(format!("vm: {e}")); } + TestOutcome::Fail(format!("vm error: {e}")) } /// 递归发现 test262 根目录下全部 `.js` 测试文件(排序后返回)。 @@ -1590,4 +1632,44 @@ mod tests { assert_outcome("ReferenceError: boom", Some(&neg("ReferenceError")), false, &TestOutcome::Pass("".into())); assert_outcome("TypeError: boom", Some(&neg("RangeError")), false, &TestOutcome::Fail("".into())); } + + /// `is not defined` 标识符解析:运行期 `uncaught` 前缀、编译期 `Identifier '..'` + /// 两种形态都能取出标识符名;非该形态返回 None。 + #[test] + fn parse_undefined_ident_extracts_name() { + assert_eq!(parse_undefined_ident("uncaught ReferenceError: $262 is not defined"), Some("$262")); + assert_eq!(parse_undefined_ident("ReferenceError: foo is not defined"), Some("foo")); + assert_eq!( + parse_undefined_ident("compile error: Identifier 'structuredClone' is not defined"), + Some("structuredClone") + ); + assert_eq!(parse_undefined_ident("Identifier 'queueMicrotask' is not defined"), Some("queueMicrotask")); + assert_eq!(parse_undefined_ident("TypeError: x is not callable"), None); + assert_eq!(parse_undefined_ident("uncaught ReferenceError: boom"), None); + } + + /// `is not defined` 白名单化:白名单内标识符(缺失全局)→ skip;白名单外 → + /// 真实失败(保证修复后应转 PASS 的测试可见)。 + #[test] + fn undefined_identifier_whitelisted_skip() { + for e in [ + "uncaught ReferenceError: $262 is not defined", + "compile error: Identifier '$262' is not defined", + "uncaught ReferenceError: structuredClone is not defined", + "compile error: Identifier 'queueMicrotask' is not defined", + ] { + assert_outcome(e, None, false, &TestOutcome::Skip("".into())); + assert_outcome(e, None, true, &TestOutcome::Fail("".into())); + } + } + + #[test] + fn undefined_identifier_outside_whitelist_is_failure() { + for e in [ + "uncaught ReferenceError: foo is not defined", + "compile error: Identifier 'whatever' is not defined", + ] { + assert_outcome(e, None, false, &TestOutcome::Fail("".into())); + } + } } diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index e86c8695..07255f9d 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -737,6 +737,7 @@ pub fn bind_global_builtin_slots(core: &Arc, session: &KernelSession bind_stub_globals(core, session, global); bind_bigint::bind_bigint(core, session, global); bind_global_functions(core, session, global); + crate::test262_host::bind_test262_host(core, session, global); let global_this = JsValue::from_js_object(global as *mut JsObject); bind_existing_global(core, global, "globalThis", global_this); } @@ -825,5 +826,6 @@ pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) bind_reflect::bind_reflect(core, session, global); bind_global::bind_global(core, session, global); bind_global_value(core, global, "globalThis", JsValue::from_js_object(global_ptr)); + crate::test262_host::bind_test262_host(core, session, global); session.record_snapshot(); } diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index 875b0cfa..956f99cd 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -23,6 +23,8 @@ mod session_arena; /// session 级 mark-sweep GC:标记-清扫 session arena 对象与 session 字符串。 pub mod session_gc; mod suspended; +/// test262 宿主对象 `$262` 的最小实现(非标准全局,供 test262 套件使用)。 +pub mod test262_host; /// 寄存器 VM 主类型([`vm::Vm`])与其执行状态定义。 pub mod vm; mod vm_dispatch_ctrl; diff --git a/crates/oxide_vm/src/test262_host.rs b/crates/oxide_vm/src/test262_host.rs new file mode 100644 index 00000000..1620bb49 --- /dev/null +++ b/crates/oxide_vm/src/test262_host.rs @@ -0,0 +1,113 @@ +//! test262 宿主对象 `$262` 的最小实现。 +//! +//! `$262` 是 test262 套件约定的宿主注入对象(非 ECMAScript 标准全局): +//! 测试经它访问当前 realm 全局、执行脚本、触发 GC 等。最小绑定使引用 +//! `$262.*` 的测试真实执行而非一律抛 `ReferenceError` 被 runner 吞为 skip; +//! 未实现的方法抛 `not supported` 错误,runner 按能力缺失归类。 + +use std::sync::Arc; + +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_runtime_api::{to_string_full, NativeResult}; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::bindings::{apply_binding_table, bind_global_value}; +use crate::vm::Vm; + +/// `$262` 未实现方法统一抛错:消息带 `not supported`,runner 据此按能力缺失跳过。 +fn not_supported(vm: &mut Vm, feature: &str) -> NativeResult { + NativeResult::Err(oxide_builtins::error::create_type_error(vm, &format!("{feature} is not supported"))) +} + +/// `$262.evalScript(code)`:在当前 realm 编译执行一段脚本并返回其完成值。 +/// +/// # 步骤 +/// 1. 实参 ToString 取脚本源码。 +/// 2. 经动态函数构造编译执行(`create_dynamic_function` + 同步调用)。 +/// 3. 编译失败转 SyntaxError;运行异常原样透传异常值。 +/// +/// # 边界与前提 +/// - 最小实现:函数作用域语义——var/函数声明不泄漏到全局,完成值仅限显式 +/// `return`(全局作用域 eval 落地前为已知上限)。 +/// - 无实参或非字符串实参按 ToString 规范处理。 +pub fn eval_script(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return NativeResult::Ok(JsValue::undefined()); + } + let code = match to_string_full(vm.reg(args[1]), vm) { + Ok(s) => s, + Err(e) => return NativeResult::Err(oxide_builtins::error::create_from_text(vm, &e)), + }; + match vm.create_dynamic_function(&[], &code) { + Ok(func) => { + let global = JsValue::from_js_object(vm.session().global_object().as_ptr() as *mut JsObject); + match vm.call_function_sync(func, global, &[]) { + Ok(val) => NativeResult::Ok(val), + Err(e) => NativeResult::Err(oxide_builtins::error::create_from_text(vm, &e)), + } + } + Err(e) => NativeResult::Err(oxide_builtins::error::create_from_text( + vm, + &format!("SyntaxError: {e}"), + )), + } +} + +/// `$262.detachArrayBuffer(buf)`:ArrayBuffer detach 未实现,抛能力缺失错误。 +pub fn detach_array_buffer(vm: &mut Vm, _args: &[u8]) -> NativeResult { + not_supported(vm, "detachArrayBuffer") +} + +/// `$262.createRealm()`:跨 realm 未支持,抛能力缺失错误。 +pub fn create_realm(vm: &mut Vm, _args: &[u8]) -> NativeResult { + not_supported(vm, "createRealm") +} + +/// `$262.gc()`:请求一次 session 全量回收(不计返回值)。 +/// +/// # 副作用 +/// - 执行 mark-sweep 搬移存活对象并释放不可达对象,会重写 session 对象指针。 +pub fn gc(vm: &mut Vm, _args: &[u8]) -> NativeResult { + let mut session_gc = std::mem::take(&mut vm.gc_state.session_gc); + session_gc.sweep(vm); + vm.gc_state.session_gc = session_gc; + NativeResult::Ok(JsValue::undefined()) +} + +/// `$262.agent`:agent 线程接口依赖 SharedArrayBuffer 族,未支持,抛能力缺失错误。 +pub fn agent(vm: &mut Vm, _args: &[u8]) -> NativeResult { + not_supported(vm, "agent") +} + +/// 把 `$262` 宿主对象绑定到 global:对象本体 + 标准方法 + `global` 数据属性。 +/// +/// # 步骤 +/// 1. 建 `$262` 普通对象(Object.prototype)。 +/// 2. 绑定 evalScript/detachArrayBuffer/createRealm/gc/agent 方法。 +/// 3. `global` 数据属性指向当前 session global;`$262` 本体挂到 global。 +/// +/// # 注意事项 +/// - 宿主对象经 `Box::into_raw` 持有,随 session 生命周期存活(与 Reflect/Iterator +/// 等全局对象同一约定);每次调用新建对象,重复绑定会泄漏旧对象(全量初始化 +/// 与 full_reset 频率低,可接受)。 +pub fn bind_test262_host(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let object_proto = session.builtin_world().object_proto.as_ptr() as *mut JsObject; + let mut host = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); + apply_binding_table( + session.builtin_world(), + &mut host, + core, + &[ + ("evalScript", eval_script as *const (), 1), + ("detachArrayBuffer", detach_array_buffer as *const (), 1), + ("createRealm", create_realm as *const (), 0), + ("gc", gc as *const (), 0), + ("agent", agent as *const (), 0), + ], + ); + let global_this = JsValue::from_js_object(global as *mut JsObject); + bind_global_value(core, &mut host, "global", global_this); + bind_global_value(core, global, "$262", JsValue::from_js_object(Box::into_raw(Box::new(host)))); +} diff --git a/crates/oxide_vm/tests/test262_host_tests.rs b/crates/oxide_vm/tests/test262_host_tests.rs new file mode 100644 index 00000000..261cd791 --- /dev/null +++ b/crates/oxide_vm/tests/test262_host_tests.rs @@ -0,0 +1,75 @@ +//! test262 宿主对象 `$262` 的最小绑定验证:对象存在、`global` 指向当前 realm +//! 全局、evalScript 可执行、未实现方法抛能力缺失错误。 + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +/// 单测辅助:源串 → 结果串(字符串值解包实际内容,运行错误以 `vm error: ...` 呈现)。 +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) => vm.lookup_str(result).unwrap_or_else(|| format!("{result}")), + Err(e) => format!("vm error: {e}"), + } +} + +/// `$262` 作为编译期已知全局绑定:typeof 为 object,`global` 即 globalThis。 +#[test] +fn host_object_exists_and_exposes_global() { + assert_eq!(eval("typeof $262"), "object"); + assert_eq!(eval("$262.global === globalThis"), "true"); +} + +/// `$262` 方法面:evalScript/gc 为可调用函数。 +#[test] +fn host_methods_are_callable() { + assert_eq!(eval("typeof $262.evalScript"), "function"); + assert_eq!(eval("typeof $262.gc"), "function"); + assert_eq!(eval("typeof $262.detachArrayBuffer"), "function"); + assert_eq!(eval("typeof $262.createRealm"), "function"); +} + +/// evalScript 执行脚本并返回显式 `return` 的完成值;无 return 为 undefined。 +#[test] +fn eval_script_runs_code_and_returns_value() { + assert_eq!(eval("$262.evalScript('return 40 + 2')"), "42"); + assert_eq!(eval("$262.evalScript('var y = 1; y')"), "undefined"); +} + +/// evalScript 编译失败转 SyntaxError(可由 JS catch 捕获)。 +#[test] +fn eval_script_syntax_error_is_catchable() { + let r = eval("try { $262.evalScript('if (') ; 'no-throw' } catch (e) { e.name }"); + assert_eq!(r, "SyntaxError"); +} + +/// 未实现方法抛能力缺失错误(消息带 `not supported`),不抛 ReferenceError。 +#[test] +fn unsupported_methods_throw_not_supported() { + let r = eval("try { $262.detachArrayBuffer(new ArrayBuffer(8)); 'no-throw' } catch (e) { e.name + ':' + e.message }"); + assert!(r.contains("TypeError"), "期望 TypeError,实际 {r}"); + assert!(r.contains("not supported"), "期望能力缺失消息,实际 {r}"); + + let r = eval("try { $262.createRealm(); 'no-throw' } catch (e) { e.name + ':' + e.message }"); + assert!(r.contains("TypeError"), "期望 TypeError,实际 {r}"); + assert!(r.contains("not supported"), "期望能力缺失消息,实际 {r}"); + + let r = eval("try { $262.agent(); 'no-throw' } catch (e) { e.name + ':' + e.message }"); + assert!(r.contains("TypeError"), "期望 TypeError,实际 {r}"); +} + +/// `$262.gc()` 触发一次 session 回收且不抛错。 +#[test] +fn gc_runs_without_error() { + assert_eq!(eval("$262.gc(); 'ok'"), "ok"); +}