From 17e49165e40b07e5a87a35596d64a48ebec96388 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 16 Aug 2026 19:32:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20DisposableStack=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E4=B9=9D=E7=BB=84=EF=BC=88=E6=9E=84=E9=80=A0=E5=99=A8/?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E8=AF=AD=E4=B9=89/=E5=8E=9F=E5=9E=8B?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6/GC=20=E4=BA=A4=E5=8F=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/disposable_stack.rs | 27 +- crates/oxide_builtins/src/lib.rs | 4 +- .../src/bindings/bind_disposable_stack.rs | 33 +- .../tests/builtins_disposable_stack_tests.rs | 434 ++++++++++++++++++ 4 files changed, 483 insertions(+), 15 deletions(-) create mode 100644 crates/oxide_vm/tests/builtins_disposable_stack_tests.rs diff --git a/crates/oxide_builtins/src/disposable_stack.rs b/crates/oxide_builtins/src/disposable_stack.rs index 7b15e1b..4d55923 100644 --- a/crates/oxide_builtins/src/disposable_stack.rs +++ b/crates/oxide_builtins/src/disposable_stack.rs @@ -141,7 +141,12 @@ pub fn disposable_stack_use(vm: &mut H, args: &[u8]) -> NativeResult if method.is_null() || method.is_undefined() || !crate::iterator::is_callable(method) { return NativeResult::Err(crate::error::create_type_error(vm, "value[Symbol.dispose] is not callable")); } - cap_ref.entries.push(DisposeEntry { value, method, hint: 0, arg_style: false }); + cap_ref.entries.push(DisposeEntry { + value, + method, + hint: 0, + arg_style: false, + }); NativeResult::Ok(value) } @@ -165,7 +170,12 @@ pub fn disposable_stack_adopt(vm: &mut H, args: &[u8]) -> NativeResul if !crate::iterator::is_callable(on_dispose) { return NativeResult::Err(crate::error::create_type_error(vm, "onDispose is not callable")); } - cap_ref.entries.push(DisposeEntry { value, method: on_dispose, hint: 0, arg_style: true }); + cap_ref.entries.push(DisposeEntry { + value, + method: on_dispose, + hint: 0, + arg_style: true, + }); NativeResult::Ok(value) } @@ -188,9 +198,12 @@ pub fn disposable_stack_defer(vm: &mut H, args: &[u8]) -> NativeResul if !crate::iterator::is_callable(on_dispose) { return NativeResult::Err(crate::error::create_type_error(vm, "onDispose is not callable")); } - cap_ref - .entries - .push(DisposeEntry { value: JsValue::undefined(), method: on_dispose, hint: 0, arg_style: false }); + cap_ref.entries.push(DisposeEntry { + value: JsValue::undefined(), + method: on_dispose, + hint: 0, + arg_style: false, + }); NativeResult::Ok(JsValue::undefined()) } @@ -217,7 +230,8 @@ pub fn disposable_stack_dispose(vm: &mut H, args: &[u8]) -> NativeRes } // 先置位再执行:执行中调用 use/adopt/defer 一律命中 Disposed 分支。 cap_ref.state = DisposeState::Disposed; - let mut completion: Option = None; for entry in cap_ref.entries.iter().rev() { + let mut completion: Option = None; + for entry in cap_ref.entries.iter().rev() { if entry.method.is_undefined() { continue; } @@ -283,7 +297,6 @@ pub fn disposable_stack_disposed_getter(vm: &mut H, args: &[u8]) -> N NativeResult::Ok(JsValue::bool(state != DisposeState::Pending)) } - /// 栈生命周期状态:Pending 可入栈,Disposed 后所有操作抛 ReferenceError; /// Disposing 为异步资源栈 disposeAsync 执行期标记(当前未构造,语义预留)。 #[derive(Clone, Copy, PartialEq, Eq, Debug)] diff --git a/crates/oxide_builtins/src/lib.rs b/crates/oxide_builtins/src/lib.rs index c666745..75d197c 100644 --- a/crates/oxide_builtins/src/lib.rs +++ b/crates/oxide_builtins/src/lib.rs @@ -19,10 +19,10 @@ pub mod boolean; pub mod builtins_log; /// DataView 内置对象实现(对 ArrayBuffer 的定点读写视图)。 pub mod data_view; -/// DisposableStack/AsyncDisposableStack 内置对象实现(资源栈状态盒与 GC 追踪)。 -pub mod disposable_stack; /// Date 内置对象实现(时间戳存取、get/set 系列与 to*String 系列)。 pub mod date; +/// DisposableStack/AsyncDisposableStack 内置对象实现(资源栈状态盒与 GC 追踪)。 +pub mod disposable_stack; /// Error 内置对象实现(各类 Error 构造函数与 stack/toString)。 pub mod error; /// Function 内置对象实现(call/apply/bind/toString)。 diff --git a/crates/oxide_vm/src/bindings/bind_disposable_stack.rs b/crates/oxide_vm/src/bindings/bind_disposable_stack.rs index 8234d44..00b3ce4 100644 --- a/crates/oxide_vm/src/bindings/bind_disposable_stack.rs +++ b/crates/oxide_vm/src/bindings/bind_disposable_stack.rs @@ -29,11 +29,31 @@ pub fn bind_disposable_stack_protos(core: &Arc, session: &KernelSess proto, core, &[ - ("use", oxide_builtins::disposable_stack::disposable_stack_use:: as *const (), 1), - ("adopt", oxide_builtins::disposable_stack::disposable_stack_adopt:: as *const (), 2), - ("defer", oxide_builtins::disposable_stack::disposable_stack_defer:: as *const (), 1), - ("dispose", oxide_builtins::disposable_stack::disposable_stack_dispose:: as *const (), 0), - ("move", oxide_builtins::disposable_stack::disposable_stack_move:: as *const (), 0), + ( + "use", + oxide_builtins::disposable_stack::disposable_stack_use:: as *const (), + 1, + ), + ( + "adopt", + oxide_builtins::disposable_stack::disposable_stack_adopt:: as *const (), + 2, + ), + ( + "defer", + oxide_builtins::disposable_stack::disposable_stack_defer:: as *const (), + 1, + ), + ( + "dispose", + oxide_builtins::disposable_stack::disposable_stack_dispose:: as *const (), + 0, + ), + ( + "move", + oxide_builtins::disposable_stack::disposable_stack_move:: as *const (), + 0, + ), ], ); bind_accessor_getter( @@ -109,7 +129,8 @@ pub fn bind_disposable_stack(core: &Arc, session: &KernelSession, gl ctor.set_shape_id(ctor_shape3); ctor.ensure_hash_props() .push(JsValue::from_js_object(world.disposable_stack_proto.as_ptr() as *mut JsObject)); - ctor.ensure_hash_props().push(JsValue::perm_string(sf.string_ptr(sf.intern("DisposableStack").0))); + ctor.ensure_hash_props() + .push(JsValue::perm_string(sf.string_ptr(sf.intern("DisposableStack").0))); ctor.ensure_hash_props().push(JsValue::int(0)); ctor.set_data_meta(0u32, PropAttributes::new(false, false, false)); ctor.set_data_meta(1u32, PropAttributes::new(false, false, true)); diff --git a/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs b/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs new file mode 100644 index 0000000..73581a9 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs @@ -0,0 +1,434 @@ +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; + +fn make_vm() -> Vm { + Vm::new() +} + +fn eval_in(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?; + vm.run(&module) +} + +/// 断言 eval 返回字符串值(session 串在 Vm 内解析)。 +fn eval_str(vm: &mut Vm, source: &str) -> String { + let result = eval_in(vm, source).expect("run"); + vm.lookup_str(result).unwrap_or_default().to_string() +} + +fn eval_str_fresh(source: &str) -> String { + let mut vm = make_vm(); + eval_str(&mut vm, source) +} + +/// 构造器基本形状:new 建对象、disposed=false、proto/instanceof、原型链到 Object.prototype。 +#[test] +fn constructor_shapes() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); \ + s.disposed + '|' + (Object.getPrototypeOf(s) === DisposableStack.prototype) + '|' + \ + (s instanceof DisposableStack) + '|' + \ + (Object.getPrototypeOf(DisposableStack.prototype) === Object.prototype)", + ), + "false|true|true|true", + ); +} + +/// 构造器 length/name 与 prototype 槽描述符 {f,f,f}、proto.constructor={t,f,t}。 +#[test] +fn constructor_identity() { + assert_eq!( + eval_str_fresh( + "var p = Object.getOwnPropertyDescriptor(DisposableStack, 'prototype'); \ + var c = Object.getOwnPropertyDescriptor(DisposableStack.prototype, 'constructor'); \ + DisposableStack.length + '|' + DisposableStack.name + '|' + \ + p.writable + '|' + p.enumerable + '|' + p.configurable + '|' + \ + c.writable + '|' + c.enumerable + '|' + c.configurable + '|' + \ + (DisposableStack.prototype.constructor === DisposableStack)", + ), + "0|DisposableStack|false|false|false|true|false|true|true", + ); +} + +/// 普通调用(无 new)抛 TypeError。 +#[test] +fn constructor_without_new_throws() { + assert_eq!( + eval_str_fresh("try { DisposableStack(); 'no-throw' } catch (e) { e.constructor.name }",), + "TypeError", + ); +} + +/// use:push + dispose 以 receiver=value、无参调用;返回原 value。 +#[test] +fn use_pushes_and_disposes_with_receiver() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + var o = { v: 7 }; \ + o[Symbol.dispose] = function() { log.push(this.v); }; \ + var r = s.use(o); \ + (r === o) + '|' + s.dispose() + '|' + log.join(',')", + ), + "true|undefined|7", + ); +} + +/// use:@@dispose getter 只读一次(不得重复读)。 +#[test] +fn use_reads_dispose_once() { + assert_eq!( + eval_str_fresh( + "var count = 0; \ + var o = { get [Symbol.dispose]() { count++; return function() {}; } }; \ + var s = new DisposableStack(); s.use(o); s.dispose(); count + ''", + ), + "1", + ); +} + +/// use(null)/use(undefined):返回原值且不入栈(dispose 无任何调用)。 +#[test] +fn use_null_undefined_not_pushed() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + var r1 = s.use(null); var r2 = s.use(undefined); \ + s.dispose(); log.join(',') + '|' + (r1 === null) + '|' + (r2 === undefined)", + ), + "|true|true", + ); +} + +/// use:原始值 / 缺 / null / undefined / 非 callable 的 @@dispose 一律 TypeError。 +#[test] +fn use_invalid_dispose_throws() { + assert_eq!( + eval_str_fresh( + "function kind(fn) { try { var s = new DisposableStack(); fn(s); s.dispose(); return 'no' } catch (e) { return e.constructor.name } } \ + var noSym = function(s) { s.use({}); }; \ + var nullSym = function(s) { s.use({ [Symbol.dispose]: null }); }; \ + var undefSym = function(s) { s.use({ [Symbol.dispose]: undefined }); }; \ + var nonCallable = function(s) { s.use({ [Symbol.dispose]: 42 }); }; \ + var primitive = function(s) { s.use(42); }; \ + [kind(noSym), kind(nullSym), kind(undefSym), kind(nonCallable), kind(primitive)].join(',')", + ), + "TypeError,TypeError,TypeError,TypeError,TypeError", + ); +} + +/// use/adopt/defer 在 disposed 后调用抛 ReferenceError。 +#[test] +fn mutators_after_dispose_throw() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); s.dispose(); \ + function k(fn) { try { fn(); return 'no' } catch (e) { return e.constructor.name } } \ + k(function() { s.use({ [Symbol.dispose]: function(){} }); }) + ',' + \ + k(function() { s.adopt(1, function(){}); }) + ',' + \ + k(function() { s.defer(function(){}); }) + ',' + \ + k(function() { s.move(); })", + ), + "ReferenceError,ReferenceError,ReferenceError,ReferenceError", + ); +} + +/// use 在非栈 this(普通对象/原始值)上调用抛 TypeError。 +#[test] +fn use_incompatible_receiver_throws() { + assert_eq!( + eval_str_fresh( + "function k(t) { try { DisposableStack.prototype.use.call(t, { [Symbol.dispose]: function(){} }); return 'no' } catch (e) { return e.constructor.name } } \ + k({}) + ',' + k(42)", + ), + "TypeError,TypeError", + ); +} + +/// adopt:任意 value(含原始值)接受并返回;dispose 时 arg-style 调用 +/// (value 作首参、this 保持 undefined——严格模式可观察)。 +#[test] +fn adopt_arg_style_semantics() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + var r = s.adopt('v', function(a) { 'use strict'; log.push(a + ':' + (this === undefined)); }); \ + (r === 'v') + '|' + s.dispose() + '|' + log.join(',')", + ), + "true|undefined|v:true", + ); +} + +/// adopt:非 callable 回调抛 TypeError。 +#[test] +fn adopt_non_callable_throws() { + assert_eq!( + eval_str_fresh("try { var s = new DisposableStack(); s.adopt(1, 42); 'no' } catch (e) { e.constructor.name }",), + "TypeError", + ); +} + +/// defer:返回 undefined、dispose 时以 undefined receiver 无参调用。 +#[test] +fn defer_semantics() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + var r = s.defer(function() { 'use strict'; log.push('this:' + (this === undefined)); }); \ + (r === undefined) + '|' + s.dispose() + '|' + log.join(',')", + ), + "true|undefined|this:true", + ); +} + +/// defer:非 callable 抛 TypeError。 +#[test] +fn defer_non_callable_throws() { + assert_eq!( + eval_str_fresh("try { var s = new DisposableStack(); s.defer(null); 'no' } catch (e) { e.constructor.name }",), + "TypeError", + ); +} + +/// dispose:use/adopt/defer 混栈逆序执行(后入先释放)。 +#[test] +fn dispose_reverse_order() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + s.defer(function() { log.push('A'); }); \ + s.adopt(1, function() { log.push('B'); }); \ + s.use({ [Symbol.dispose]: function() { log.push('C'); } }); \ + s.dispose(); log.join(',')", + ), + "C,B,A", + ); +} + +/// dispose:二次调用幂等(回调不重入),disposed 变 true。 +#[test] +fn dispose_idempotent() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var n = 0; \ + s.defer(function() { n++; }); \ + s.dispose(); s.dispose(); n + '|' + s.disposed", + ), + "1|true", + ); +} + +/// dispose:单错原样抛(同一错误对象引用)。 +#[test] +fn dispose_single_error_preserved() { + assert_eq!( + eval_str_fresh( + "var err = new Error('boom'); \ + var s = new DisposableStack(); s.defer(function() { throw err; }); \ + try { s.dispose(); 'no' } catch (e) { String(e === err) }", + ), + "true", + ); +} + +/// dispose:三错合并为 SuppressedError 链(error=后抛、suppressed=前值嵌套)。 +#[test] +fn dispose_multiple_errors_suppressed_chain() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); \ + s.defer(function() { throw new Error('e1'); }); \ + s.defer(function() { throw new Error('e2'); }); \ + s.defer(function() { throw new Error('e3'); }); \ + try { s.dispose(); 'no' } catch (e) { \ + (e instanceof SuppressedError) + '|' + e.error.message + '|' + \ + e.suppressed.error.message + '|' + e.suppressed.suppressed.message }", + ), + "true|e1|e2|e3", + ); +} + +/// dispose:错误不中断循环,剩余条目全部执行。 +#[test] +fn dispose_error_does_not_abort_loop() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + s.defer(function() { log.push('A'); }); \ + s.defer(function() { log.push('B'); throw new Error('x'); }); \ + try { s.dispose(); 'no' } catch (e) { log.join(',') + ':' + e.message }", + ), + "B,A:x", + ); +} + +/// dispose:执行中调用 use/adopt/defer/move 均 ReferenceError。 +#[test] +fn dispose_reentrancy_guard() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + s.defer(function() { \ + try { s.use({ [Symbol.dispose]: function(){} }); log.push('use:no'); } catch (e) { log.push(e.constructor.name); } \ + try { s.adopt(1, function(){}); log.push('adopt:no'); } catch (e) { log.push(e.constructor.name); } \ + try { s.defer(function(){}); log.push('defer:no'); } catch (e) { log.push(e.constructor.name); } \ + try { s.move(); log.push('move:no'); } catch (e) { log.push(e.constructor.name); } \ + }); \ + s.dispose(); log.join(',')", + ), + "ReferenceError,ReferenceError,ReferenceError,ReferenceError", + ); +} + +/// move:entries 转移、源置 Disposed、新栈 Pending 且可继续 dispose。 +#[test] +fn move_transfers_entries() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + s.defer(function() { log.push('A'); }); \ + var m = s.move(); \ + var first = String(s.disposed) + '|' + String(m.disposed) + '|'; \ + m.dispose(); first + log.join(',')", + ), + "true|false|A", + ); +} + +/// move:源栈 dispose 幂等(不执行资源回调、不抛错),新栈独占执行。 +#[test] +fn move_source_disposed() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + s.defer(function() { log.push('A'); }); \ + var m = s.move(); \ + s.dispose(); log.push('src:ok'); \ + m.dispose(); log.join(',')", + ), + "src:ok,A", + ); +} + +/// move:子类实例 move 结果 proto 固定为 DisposableStack.prototype(非子类)。 +#[test] +fn move_subclass_proto_fixed() { + assert_eq!( + eval_str_fresh( + "class S extends DisposableStack {} \ + var s = new S(); s.defer(function(){}); \ + var m = s.move(); \ + (m instanceof DisposableStack) + '|' + (m instanceof S) + '|' + \ + (Object.getPrototypeOf(m) === DisposableStack.prototype)", + ), + "true|false|true", + ); +} + +/// disposed getter:非对象/无槽 this 抛 TypeError。 +#[test] +fn disposed_getter_incompatible_throws() { + assert_eq!( + eval_str_fresh( + "function k(t) { try { DisposableStack.prototype.disposed.call(t); return 'no' } catch (e) { return e.constructor.name } } \ + k({}) + ',' + k(42)", + ), + "TypeError,TypeError", + ); +} + +/// 原型形状:@@dispose 别名同一函数对象、@@toStringTag 值+描述符 {f,f,t}、 +/// 方法 length/name/{t,f,t}、disposed 为访问器 getter。 +#[test] +fn prototype_shape() { + assert_eq!( + eval_str_fresh( + "var p = DisposableStack.prototype; \ + var tag = Object.getOwnPropertyDescriptor(p, Symbol.toStringTag); \ + var use = Object.getOwnPropertyDescriptor(p, 'use'); \ + var disp = Object.getOwnPropertyDescriptor(p, 'dispose'); \ + var acc = Object.getOwnPropertyDescriptor(p, 'disposed'); \ + (p[Symbol.dispose] === p.dispose) + '|' + \ + Object.prototype.toString.call(new DisposableStack()) + '|' + \ + tag.value + '|' + tag.writable + '|' + tag.enumerable + '|' + tag.configurable + '|' + \ + use.value.length + '|' + use.value.name + '|' + use.writable + '|' + use.enumerable + '|' + use.configurable + '|' + \ + disp.value.length + '|' + disp.value.name + '|' + \ + (typeof acc.get === 'function') + '|' + (acc.set === undefined)", + ), + "true|[object DisposableStack]|DisposableStack|false|false|true|1|use|true|false|true|0|dispose|true|true", + ); +} + +/// 原型形状:Symbol.dispose 键读取命中资源方法(use 读 @@dispose 的实际路径)。 +#[test] +fn use_reads_symbol_dispose_key() { + assert_eq!( + eval_str_fresh( + "var s = new DisposableStack(); var log = []; \ + var o = {}; \ + Object.defineProperty(o, Symbol.dispose, { value: function() { log.push('via-symbol'); } }); \ + s.use(o); s.dispose(); log.join(',')", + ), + "via-symbol", + ); +} + +/// 低阈值执行期 GC:栈 use 资源对象后触发移动式清扫,dispose 仍按 entries 正确释放。 +#[test] +fn gc_runtime_collection_keeps_entries() { + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(4096); + let mut vm = Vm::with_kernel_core(KernelCore::new(config)); + let text = eval_str( + &mut vm, + "var log = []; \ + for (var i = 0; i < 300; i++) { \ + var s = new DisposableStack(); \ + s.use({ [Symbol.dispose]: function() { log.push('d'); } }); \ + globalThis['k' + i] = s; \ + } \ + for (var i = 0; i < 300; i++) { globalThis['k' + i].dispose(); } \ + log.length + ''", + ); + assert_eq!(text, "300"); + assert!(vm.session_gc_stats().total_collections > 0, "低阈值应触发执行期 GC"); +} + +/// promote 交叉:栈的 entries 引用 epoch 对象(value 对象 + 释放函数),写 global +/// 触发 promote,reset 释放 epoch 后状态盒存活:disposed 可读、move 可转移 entries。 +/// (释放函数对象跨 run 调用受引擎 sub_module 索引边界限制,不在此断言。) +#[test] +fn promote_then_reset_keeps_entries_alive() { + let mut vm = make_vm(); + eval_in( + &mut vm, + "globalThis.log = []; \ + var s = new DisposableStack(); \ + s.use({ v: 1, [Symbol.dispose]: function() { globalThis.log.push('d'); } }); \ + s.defer(function() { globalThis.log.push('x'); }); \ + globalThis.keep = s; 0", + ) + .expect("run1"); + vm.reset(); + let text = eval_str( + &mut vm, + "var m = globalThis.keep.move(); \ + String(globalThis.keep.disposed) + '|' + String(m.disposed)", + ); + assert_eq!(text, "true|false"); +} + +/// drop 记账:无引用的栈对象被收集时释放状态盒(freed 字节增长)。 +#[test] +fn drop_accounts_capability_bytes() { + let mut vm = make_vm(); + eval_in(&mut vm, "for (var i = 0; i < 500; i++) { new DisposableStack(); } 0").expect("run"); + let before = vm.session_gc_stats().total_bytes_freed; + vm.reset(); + let after = vm.session_gc_stats().total_bytes_freed; + assert!(after > before, "reset 应回收栈对象状态盒,before={before} after={after}"); +}