From c4b441a1a86fa59d9eb22588c5694a26dc6e6765 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 11 Oct 2026 08:30:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20CustomEvent=20=E8=BD=BD=E8=8D=B7?= =?UTF-8?q?=E7=9B=92=E5=8A=A0=20GC=20=E5=8A=A0=E6=9E=84=E9=80=A0=E5=99=A8?= =?UTF-8?q?=E5=8A=A0=E5=8E=9F=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 38 ++-- Cargo.toml | 2 +- crates/oxide_builtins/src/event.rs | 222 +++++++++++++++++++- crates/oxide_vm/src/bindings/bind_event.rs | 80 ++++++- crates/oxide_vm/src/bindings/mod.rs | 4 +- crates/oxide_vm/src/native_box_dispatch.rs | 6 +- crates/oxide_vm/src/session_gc.rs | 6 +- crates/oxide_vm/tests/event_target_tests.rs | 45 ++++ 8 files changed, 361 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9295193d..707010a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.14.13" +version = "0.14.14" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.14.13" +version = "0.14.14" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.14.13" +version = "0.14.14" dependencies = [ "ansi_term", "bumpalo", @@ -1227,7 +1227,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.14.13" +version = "0.14.14" dependencies = [ "lru", "oxide_bytecode", @@ -1236,7 +1236,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1256,7 +1256,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.14.13" +version = "0.14.14" dependencies = [ "num-bigint", "oxc_allocator", @@ -1285,7 +1285,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1297,7 +1297,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.14.13" +version = "0.14.14" dependencies = [ "bumpalo", "dashmap", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.14.13" +version = "0.14.14" dependencies = [ "tracing", "tracing-appender", @@ -1341,7 +1341,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.14.13" +version = "0.14.14" dependencies = [ "num-bigint", "num-traits", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.14.13" +version = "0.14.14" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.14.13" +version = "0.14.14" dependencies = [ "bumpalo", "dashmap", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.14.13" +version = "0.14.14" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f32540de..9f5b438d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.14.13" +version = "0.14.14" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/event.rs b/crates/oxide_builtins/src/event.rs index be88df2a..ef64e371 100644 --- a/crates/oxide_builtins/src/event.rs +++ b/crates/oxide_builtins/src/event.rs @@ -1,16 +1,17 @@ -//! Event 基类与 MessageEvent / ErrorEvent 派生类行为面:`Event` 构造器 -//! (`new Event(type, init)`)、原型 getter(type / target / currentTarget / -//! eventPhase / isTrusted / bubbles / cancelable / defaultPrevented / -//! composed)与原型方法(preventDefault / stopPropagation / +//! Event 基类与 MessageEvent / ErrorEvent / CustomEvent 派生类行为面: +//! `Event` 构造器(`new Event(type, init)`)、原型 getter(type / target / +//! currentTarget / eventPhase / isTrusted / bubbles / cancelable / +//! defaultPrevented / composed)与原型方法(preventDefault / stopPropagation / //! stopImmediatePropagation / composedPath);`MessageEvent` 构造器 //! (`new MessageEvent(type, init)`)与原型 getter(data / origin / lastEpoch / //! source / ports);`ErrorEvent` 构造器(`new ErrorEvent(type, init)`)与 -//! 原型 getter(message / filename / lineno / colno / error)。 +//! 原型 getter(message / filename / lineno / colno / error);`CustomEvent` +//! 构造器(`new CustomEvent(type, init)`)与原型 getter(detail)。 //! //! 事件载荷盒(type 串与九项状态位,MessageEvent 加 data / origin / //! lastEpoch / source / ports 五字段,ErrorEvent 加 message / filename / -//! lineno / colno / error 五字段)存于对象 `native_fn` 槽,向 GC 家族表 -//! 暴露 edges/size/drop 三自由函数。 +//! lineno / colno / error 五字段,CustomEvent 加 detail 一字段)存于对象 +//! `native_fn` 槽,向 GC 家族表暴露 edges/size/drop 三自由函数。 use std::mem; @@ -73,6 +74,18 @@ struct ErrorEventInner { error: JsValue, } +/// CustomEvent 载荷盒:事件基类字段(`EventInner` 嵌入,位于偏移 0)加详情 +/// 字段(detail)。存于对象 `native_fn` 槽(`Box::into_raw`,realm 局部 +/// 单线程)。 +/// +/// `#[repr(C)]` 固定 `base` 位于偏移 0:派生类盒指针转基类指针读首字段依赖该 +/// 偏移稳定(与 `EventInner` 同型约束)。 +#[repr(C)] +struct CustomEventInner { + base: EventInner, + detail: JsValue, +} + /// 是否事件系对象(Event / MessageEvent / ErrorEvent / CustomEvent 四标签)。 pub fn is_event_family_obj(obj: &JsObject) -> bool { matches!( @@ -1233,3 +1246,198 @@ pub fn error_event_error_getter(vm: &mut H, args: &[u8]) -> NativeRes }; NativeResult::Ok(inner.error) } + +// —— CustomEvent 派生类 —— + +/// 取 CustomEvent 载荷盒指针:仅 CustomEvent 标签对象返回 `Some`,其余标签 +/// 返回 `None`。 +fn custom_event_payload_ptr(obj: &JsObject) -> Option<*mut CustomEventInner> { + if obj.type_tag != JsObject::OBJ_TYPE_CUSTOM_EVENT { + return None; + } + obj.native_fn().map(|ptr| ptr.as_ptr() as *mut CustomEventInner) +} + +/// 收集 CustomEvent 的引用边(基类 type / target / current_target 边加 +/// detail 边)。 +/// +/// `target` / `current_target` 空窗口(构造期回填之前)为 null,边函数自守, +/// 语义自含。 +pub fn custom_event_native_edges(obj: &JsObject) -> Vec { + let Some(ptr) = custom_event_payload_ptr(obj) else { + return Vec::new(); + }; + if ptr.is_null() { + return Vec::new(); + } + // SAFETY: ptr 指向有效载荷盒(槽位仅构造期写入、释放时置空),只读字段。 + let inner = unsafe { &*ptr }; + let mut edges = vec![inner.base.r#type, inner.base.target, inner.base.current_target]; + edges.push(inner.detail); + edges +} + +/// 只读核算 CustomEvent 载荷盒字节(不释放)。 +pub fn custom_event_native_size(obj: &JsObject) -> u64 { + let Some(ptr) = custom_event_payload_ptr(obj) else { + return 0; + }; + if ptr.is_null() { + return 0; + } + mem::size_of::() as u64 +} + +/// 释放 CustomEvent 载荷盒(`Box`),返回释放字节数。 +/// +/// 盒 drop 时基类字段与 detail 的 JsValue 边随盒释放(GC 已判定死对象); +/// 槽置空保证重复调用零释放(幂等)。 +pub fn drop_custom_event_native(obj: &mut JsObject) -> u64 { + let bytes = custom_event_native_size(obj); + if bytes == 0 { + return 0; + } + let Some(ptr) = custom_event_payload_ptr(obj) else { + return 0; + }; + // SAFETY: ptr 非空(custom_event_native_size 已验证),Box::from_raw 恰好 + // 释放一次。 + unsafe { + drop(Box::from_raw(ptr)); + } + obj.set_native_fn(None); + bytes +} + +/// CustomEvent 构造器:`new CustomEvent(type, init)`。 +/// +/// # 步骤 +/// 1. 非构造形态(`!constructing_native`)抛 TypeError(HTML 规范只有 +/// [[Construct]],无 [[Call]]) +/// 2. type 经完整 ToString 转串;空串抛 SyntaxError +/// 3. init 字典读 detail(缺省 null) +/// 4. 建载荷盒入 receiver 的 `native_fn` 槽,置 type_tag +/// 5. 返回 receiver +/// +/// # 边界与前提 +/// - receiver(`args[0]`)是 VM 按构造器 prototype 新分配的对象,proto 已 +/// 指向 %CustomEvent.prototype%; +/// - init 缺省(undefined)或非对象时 detail 取缺省。 +/// +/// # 副作用 +/// - receiver 的 `native_fn` 槽写入载荷盒、`type_tag` 置 CustomEvent 标签。 +pub fn custom_event_constructor(vm: &mut H, args: &[u8]) -> NativeResult { + if !vm.constructing_native() { + return NativeResult::Err(crate::error::create_type_error(vm, "CustomEvent constructor requires new")); + } + + let this_val = vm.reg(args[0]); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "CustomEvent constructor requires new")); + } + + // type 经完整 ToString 转串(对象经 ToPrimitive,Symbol 抛 TypeError)。 + let type_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let type_str = match oxide_runtime_api::to_string_full(type_val, vm) { + Ok(s) => s, + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string")); + } + }; + if type_str.is_empty() { + return NativeResult::Err(crate::error::create_syntax_error(vm, "The provided type is empty")); + } + + // init 字典读 detail(缺省或非对象时取缺省)。 + let mut detail = JsValue::null(); + if args.len() > 2 { + let init_val = vm.reg(args[2]); + if init_val.is_object() { + let init_ptr = init_val.as_js_object_ptr(); + // SAFETY: init_val 已确认是对象值,借用即时消费。 + let init_obj = unsafe { &*init_ptr }; + let si = vm.perm_intern("detail"); + match vm.ordinary_get(init_obj, si, init_val) { + Ok(v) => { + if !v.is_undefined() { + detail = v; + } + } + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error( + vm, + "CustomEvent init property getter failed", + )); + } + } + } + } + + // 建载荷盒入 receiver 的 native_fn 槽,置 CustomEvent 标签。 + let type_js = vm.new_string(&type_str); + let inner = Box::into_raw(Box::new(CustomEventInner { + base: EventInner { + r#type: type_js, + target: JsValue::null(), + current_target: JsValue::null(), + event_phase: 0, + is_trusted: false, + bubbles: false, + cancelable: false, + default_prevented: false, + propagation_stopped: false, + immediate_stopped: false, + composed: false, + }, + detail, + })); + // SAFETY: receiver 不可调用,native_fn 存不透明 `Box` + // 指针,与 ArrayBuffer / RegExp 的类型化存储模式一致。 + let obj = unsafe { &mut *this_val.as_js_object_ptr() }; + obj.type_tag = JsObject::OBJ_TYPE_CUSTOM_EVENT; + obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(inner as *const ()) })); + NativeResult::Ok(this_val) +} + +/// 解析 this 为 CustomEvent 载荷盒共享引用:非 CustomEvent 对象或无载荷盒 +/// 返回 None。 +/// +/// # 注意事项 +/// 返回引用按 `'a` 声称为存活,实际存活至载荷盒被 GC 释放;调用方须在无 GC +/// 窗口的单 native 函数内消费,不得跨用户调用窗口持有。 +fn custom_event_inner_of<'a>(this_val: JsValue) -> Option<&'a CustomEventInner> { + if !this_val.is_object() { + return None; + } + let obj_ptr = this_val.as_js_object_ptr(); + if obj_ptr.is_null() { + return None; + } + // SAFETY: this_val 已确认是对象值,obj_ptr 指向合法 JsObject。 + let obj = unsafe { &*obj_ptr }; + let box_ptr = custom_event_payload_ptr(obj)?; + if box_ptr.is_null() { + return None; + } + // SAFETY: box_ptr 指向有效载荷盒(构造期写入、释放时置空);'a 存活由 + // 调用方「无 GC 窗口内消费」约定保证。 + Some(unsafe { &*box_ptr }) +} + +/// `CustomEvent.prototype.detail`:返回事件详情(缺省为 null)。 +pub fn custom_event_detail_getter(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let Some(inner) = custom_event_inner_of(this_val) else { + return NativeResult::Err(crate::error::create_type_error( + vm, + "CustomEvent.prototype.detail getter called on non-CustomEvent object", + )); + }; + NativeResult::Ok(inner.detail) +} diff --git a/crates/oxide_vm/src/bindings/bind_event.rs b/crates/oxide_vm/src/bindings/bind_event.rs index 3e321dbd..683dd9c6 100644 --- a/crates/oxide_vm/src/bindings/bind_event.rs +++ b/crates/oxide_vm/src/bindings/bind_event.rs @@ -3,10 +3,9 @@ //! 就地填充 `message_event_constructor` / `message_event_proto` 两枚 P 字段 //! 占位(构造器本体与原型 getter),并绑全局 `MessageEvent`;就地填充 //! `error_event_constructor` / `error_event_proto` 两枚 P 字段占位(构造器 -//! 本体与原型 getter),并绑全局 `ErrorEvent`。 -//! -//! 九枚 P 字段中其余两枚(CustomEvent 一对)由后续子任务就地填充,本函数 -//! 不读不写。 +//! 本体与原型 getter),并绑全局 `ErrorEvent`;就地填充 +//! `custom_event_constructor` / `custom_event_proto` 两枚 P 字段占位(构造器 +//! 本体与原型 getter),并绑全局 `CustomEvent`。 use std::sync::Arc; @@ -16,12 +15,14 @@ use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; -/// 把 Event / MessageEvent / ErrorEvent 构造器及原型方法绑定到 global。 +/// 把 Event / MessageEvent / ErrorEvent / CustomEvent 构造器及原型方法绑定到 +/// global。 /// /// `event_constructor` / `event_proto`、`message_event_constructor` / -/// `message_event_proto` 与 `error_event_constructor` / `error_event_proto` -/// 六枚 P 字段是空对象占位,本函数就地填充(不新建对象——P 字段本身就是 -/// 槽位,快照/脏检查/收尾枚举已覆盖)。 +/// `message_event_proto`、`error_event_constructor` / `error_event_proto` 与 +/// `custom_event_constructor` / `custom_event_proto` 八枚 P 字段是空对象占位, +/// 本函数就地填充(不新建对象——P 字段本身就是槽位,快照/脏检查/收尾枚举已 +/// 覆盖)。 /// /// # 幂等 /// 本函数从三条路径到达:`init_kernel_builtins`(占位是空对象)、 @@ -314,6 +315,69 @@ pub fn bind_event(core: &Arc, session: &KernelSession, global: &mut } } + // ---- 填 CustomEvent 构造器与原型占位 ---- + let ce_ctor_ptr = world.custom_event_constructor.as_ptr() as *mut JsObject; + let ce_ctor = unsafe { &mut *ce_ctor_ptr }; + let ce_proto_ptr = world.custom_event_proto.as_ptr() as *mut JsObject; + let ce_proto = unsafe { &mut *ce_proto_ptr }; + + // 构造器:function 标志、prototype 槽、name 槽、[[Prototype]] → + // Function.prototype(幂等,裸写槽)。 + ce_ctor.set_function(true); + if sh.lookup_position(ce_ctor.shape_id(), si_prototype).is_none() { + let shape = sh.make_shape(ce_ctor.shape_id(), si_prototype); + ce_ctor.set_shape_id(shape); + let pos = ce_ctor.push_prop(JsValue::from_js_object(ce_proto_ptr)); + ce_ctor.set_data_meta(pos, PropAttributes::new(false, false, false)); + ce_ctor.bump_generation(); + } + if sh.lookup_position(ce_ctor.shape_id(), si_name).is_none() { + let shape = sh.make_shape(ce_ctor.shape_id(), si_name); + ce_ctor.set_shape_id(shape); + ce_ctor + .ensure_hash_props() + .push(JsValue::perm_string(sf.string_ptr(sf.intern("CustomEvent").0))); + let pos = ce_ctor.hash_props_vec().map_or(0, |v| v.len() as u32).saturating_sub(1); + ce_ctor.set_data_meta(pos, PropAttributes::new(false, false, true)); + ce_ctor.bump_generation(); + } + let _ = ce_ctor.set_proto(fn_proto_val); + + configure_native_constructor( + ce_ctor, + oxide_builtins::event::custom_event_constructor:: as *const (), + 2, + ); + bind_constructor!( + core, + global, + "CustomEvent", + ce_ctor_ptr, + oxide_builtins::event::custom_event_constructor::, + 2, + hash: true + ); + + // 原型:constructor 槽、[[Prototype]] → Event.prototype、一个 getter + // (detail,先 lookup_position 守卫)。 + if sh.lookup_position(ce_proto.shape_id(), si_constructor).is_none() { + let shape = sh.make_shape(ce_proto.shape_id(), si_constructor); + ce_proto.set_shape_id(shape); + let pos = ce_proto.push_prop(JsValue::from_js_object(ce_ctor_ptr)); + ce_proto.set_data_meta(pos, PropAttributes::new(true, false, true)); + ce_proto.bump_generation(); + } + let _ = ce_proto.set_proto(event_proto_val); + + let ce_getter_fns: [(&str, *const ()); 1] = + [("detail", oxide_builtins::event::custom_event_detail_getter:: as *const ())]; + for (name, fn_ptr) in ce_getter_fns { + let si = sf.intern(name).0; + if sh.lookup_position(ce_proto.shape_id(), si).is_none() { + bind_accessor_getter(core, session, ce_proto, name, fn_ptr); + } + } + // ---- 填 EventTarget 原型占位(无构造器,仅暴露原型与三方法)---- // 幂等:三方法经 lookup_position 守卫,[[Prototype]] 裸写槽幂等。 let et_proto_ptr = world.event_target_proto.as_ptr() as *mut JsObject; diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 9e18a687..d7967ce9 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -26,8 +26,8 @@ pub mod bind_date; pub mod bind_disposable_stack; /// Error 家族构造器与原型的 native 方法绑定(含各子类型构造器创建)。 pub mod bind_error; -/// Event 体系绑定入口(Event / MessageEvent / ErrorEvent 构造器与原型就地 -/// 填充,CustomEvent 一对由后续任务填充)。 +/// Event 体系绑定入口(Event / MessageEvent / ErrorEvent / CustomEvent 构造器 +/// 与原型就地填充)。 pub mod bind_event; /// Function 构造器与原型的 native 方法绑定。 pub mod bind_function; diff --git a/crates/oxide_vm/src/native_box_dispatch.rs b/crates/oxide_vm/src/native_box_dispatch.rs index 7edd27ef..863ba0d1 100644 --- a/crates/oxide_vm/src/native_box_dispatch.rs +++ b/crates/oxide_vm/src/native_box_dispatch.rs @@ -255,10 +255,10 @@ pub(crate) fn ops_for(family: NativeBoxFamily) -> NativeBoxOps { string_edges: None, cell_edges: None, }, - // CustomEvent 载荷盒由后续子任务填充;本臂当前无引用边,仅 size/drop - // 链消费。 + // CustomEvent 派生类载荷盒:基类 type / target / current_target 边加 + // detail 边。 NativeBoxFamily::CustomEvent => NativeBoxOps { - object_edges: None, + object_edges: Some(event::custom_event_native_edges), string_edges: None, cell_edges: None, }, diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 76b278fa..721b2298 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -135,6 +135,7 @@ impl SessionGc { bytes += event::event_native_size(obj); bytes += event::message_event_native_size(obj); bytes += event::error_event_native_size(obj); + bytes += event::custom_event_native_size(obj); bytes += crate::arguments_gc::arguments_native_size(obj); bytes @@ -379,13 +380,13 @@ impl SessionGc { } } // 事件系载荷盒(Event / MessageEvent / ErrorEvent / CustomEvent)字符串 - // 边标记:按标签取对应边函数;CustomEvent 载荷盒由后续子任务填充, - // 当前取空边。 + // 边标记:按标签取对应边函数。 if event::is_event_family_obj(obj) { let edges = match obj.type_tag { JsObject::OBJ_TYPE_EVENT => event::event_native_edges(obj), JsObject::OBJ_TYPE_MESSAGE_EVENT => event::message_event_native_edges(obj), JsObject::OBJ_TYPE_ERROR_EVENT => event::error_event_native_edges(obj), + JsObject::OBJ_TYPE_CUSTOM_EVENT => event::custom_event_native_edges(obj), _ => Vec::new(), }; for value in edges { @@ -596,6 +597,7 @@ impl SessionGc { freed_bytes += event::drop_event_native(obj); freed_bytes += event::drop_message_event_native(obj); freed_bytes += event::drop_error_event_native(obj); + freed_bytes += event::drop_custom_event_native(obj); freed_bytes += crate::arguments_gc::drop_arguments_native(obj); freed_bytes diff --git a/crates/oxide_vm/tests/event_target_tests.rs b/crates/oxide_vm/tests/event_target_tests.rs index d6f770fd..084e249c 100644 --- a/crates/oxide_vm/tests/event_target_tests.rs +++ b/crates/oxide_vm/tests/event_target_tests.rs @@ -289,3 +289,48 @@ fn error_event_box_edge_survives_gc() { vm.collect_session_gc(); assert_eq!(vm_eval(&mut vm, "globalThis.e.error.tag;"), "1"); } + +/// CustomEvent 构造器:init 的 detail 经原型 getter 可读。 +#[test] +fn custom_event_constructor_detail() { + assert_eq!(eval("new CustomEvent('x', { detail: { a: 1 } }).detail.a;"), "1"); +} + +/// CustomEvent 缺省字段:detail 为 null。 +#[test] +fn custom_event_detail_defaults_null() { + assert_eq!(eval("new CustomEvent('x').detail === null;"), "true"); +} + +/// CustomEvent 原型链:instanceof CustomEvent 与 instanceof Event 均真, +/// 原型双亲为 Event.prototype。 +#[test] +fn custom_event_proto_chain() { + assert_eq!( + eval( + "var e = new CustomEvent('x'); \ + e instanceof CustomEvent && e instanceof Event && \ + Object.getPrototypeOf(e) === CustomEvent.prototype && \ + Object.getPrototypeOf(CustomEvent.prototype) === Event.prototype;" + ), + "true" + ); +} + +/// CustomEvent 非构造形态抛 TypeError。 +#[test] +fn custom_event_requires_new() { + assert_eq!( + eval("try { CustomEvent('x'); 'no-throw'; } catch (e) { e instanceof TypeError; }"), + "true" + ); +} + +/// CustomEvent 盒边 GC 保活:detail 字段引用对象经完整收集后仍存活。 +#[test] +fn custom_event_box_edge_survives_gc() { + let mut vm = vm_with_script("globalThis.e = new CustomEvent('x', { detail: { a: 1 } });"); + // 强制完整收集:detail 字段是盒内 GC 边,对象应保活。 + vm.collect_session_gc(); + assert_eq!(vm_eval(&mut vm, "globalThis.e.detail.a;"), "1"); +}