From 28e219c5b3ab630878f35447b1b06624b3a0d713 Mon Sep 17 00:00:00 2001 From: mrhapile Date: Wed, 25 Mar 2026 02:12:45 +0530 Subject: [PATCH] feat(erm): implement block-scoped disposal for using declarations Add initial runtime support for Explicit Resource Management (ERM): - Introduce DisposableResource, DisposableResourceStack, and disposal helpers - Integrate and into bytecompiler - Add VM opcodes for tracking disposable resources - Implement block-scoped disposal via lexical environments (PopEnvironment) - Ensure correct LIFO disposal across nested scopes - Fix scope analyzer to preserve scopes containing - Handle abrupt completion (return/throw/yield) without double-disposal - Enable Test262 tests (57/78 passing) - Remove all compiler warnings and ensure clean build Limitations: - SuppressedError aggregation not implemented - Async disposal () not fully supported - DisposableStack builtins not implemented No regressions in overall Test262 suite. Signed-off-by: mrhapile --- core/ast/src/scope.rs | 21 +- core/ast/src/scope_analyzer.rs | 68 +++++- core/engine/src/bytecompiler/mod.rs | 14 +- core/engine/src/lib.rs | 1 + core/engine/src/resource_management.rs | 224 ++++++++++++++++++ core/engine/src/vm/call_frame/mod.rs | 10 + core/engine/src/vm/code_block.rs | 6 +- core/engine/src/vm/flowgraph/mod.rs | 8 +- core/engine/src/vm/mod.rs | 15 ++ core/engine/src/vm/opcode/environment/erm.rs | 123 ++++++++++ core/engine/src/vm/opcode/environment/mod.rs | 3 + core/engine/src/vm/opcode/mod.rs | 14 +- core/engine/src/vm/opcode/pop/mod.rs | 10 + core/engine/src/vm/opcode/push/environment.rs | 8 + test262_config.toml | 6 +- 15 files changed, 499 insertions(+), 32 deletions(-) create mode 100644 core/engine/src/resource_management.rs create mode 100644 core/engine/src/vm/opcode/environment/erm.rs diff --git a/core/ast/src/scope.rs b/core/ast/src/scope.rs index c172877479f..309b033ede1 100644 --- a/core/ast/src/scope.rs +++ b/core/ast/src/scope.rs @@ -103,9 +103,10 @@ pub(crate) struct Inner { index: Cell, bindings: RefCell>, function: bool, - // Has the `this` been accessed/escaped outside the function environment boundary. this_escaped: Cell, + has_using_declarations: Cell, + context: Rc, } @@ -121,6 +122,7 @@ impl Scope { bindings: RefCell::default(), function: true, this_escaped: Cell::new(false), + has_using_declarations: Cell::new(false), context: Rc::default(), }), } @@ -136,6 +138,7 @@ impl Scope { bindings: RefCell::default(), function, this_escaped: Cell::new(false), + has_using_declarations: Cell::new(false), context: parent.inner.context.clone(), outer: Some(parent), }), @@ -145,7 +148,10 @@ impl Scope { /// Checks if the scope has only local bindings. #[must_use] pub fn all_bindings_local(&self) -> bool { - // if self.inner.function && self.inn + if self.inner.has_using_declarations.get() { + return false; + } + self.inner .bindings .borrow() @@ -153,6 +159,17 @@ impl Scope { .all(|binding| !binding.escapes()) } + /// Mark the scope as having using declarations, to prevent `PushScope` optimization + pub fn set_has_using_declarations(&self) { + self.inner.has_using_declarations.set(true); + } + + /// Returns true if using declarations exist in this scope + #[must_use] + pub fn has_using_declarations(&self) -> bool { + self.inner.has_using_declarations.get() + } + /// Marks all bindings in this scope as escaping. pub fn escape_all_bindings(&self) { for binding in self.inner.bindings.borrow_mut().iter_mut() { diff --git a/core/ast/src/scope_analyzer.rs b/core/ast/src/scope_analyzer.rs index c586e78f341..a6eb5c1b23e 100644 --- a/core/ast/src/scope_analyzer.rs +++ b/core/ast/src/scope_analyzer.rs @@ -974,6 +974,14 @@ impl<'ast> VisitorMut<'ast> for BindingCollectorVisitor<'_> { let scope = match &mut node.inner.init { Some(ForLoopInitializer::Lexical(decl)) => { let mut scope = Scope::new(self.scope.clone(), false); + + if matches!( + decl.declaration, + LexicalDeclaration::Using(_) | LexicalDeclaration::AwaitUsing(_) + ) { + scope.set_has_using_declarations(); + } + let names = bound_names(&decl.declaration); if decl.declaration.is_const() { for name in &names { @@ -1800,6 +1808,16 @@ fn global_declaration_instantiation( env.create_immutable_binding(name, true); } } + Declaration::Lexical( + LexicalDeclaration::Using(declaration) + | LexicalDeclaration::AwaitUsing(declaration), + ) => { + env.set_has_using_declarations(); + for name in bound_names(declaration) { + let name = name.to_js_string(interner); + env.create_immutable_binding(name.clone(), true); + } + } _ => {} } } @@ -1832,6 +1850,18 @@ where // 3. For each element d of declarations, do for d in &declarations { + // Mark scope as containing `using` declarations so the bytecompiler + // does NOT optimize away PushScope/PopEnvironment — we need those + // opcodes to trigger block-scoped resource disposal. + if matches!( + d, + LexicallyScopedDeclaration::LexicalDeclaration( + LexicalDeclaration::Using(_) | LexicalDeclaration::AwaitUsing(_) + ) + ) { + scope.set_has_using_declarations(); + } + // i. If IsConstantDeclaration of d is true, then if let LexicallyScopedDeclaration::LexicalDeclaration(LexicalDeclaration::Const(d)) = d { // a. For each element dn of the BoundNames of d, do @@ -1861,7 +1891,7 @@ where } } - if scope.num_bindings() > 0 { + if scope.num_bindings() > 0 || scope.has_using_declarations() { Some(scope) } else { None @@ -2180,6 +2210,16 @@ fn function_declaration_instantiation( lex_env.create_immutable_binding(name, true); } } + Declaration::Lexical( + LexicalDeclaration::Using(declaration) + | LexicalDeclaration::AwaitUsing(declaration), + ) => { + lex_env.set_has_using_declarations(); + for name in bound_names(declaration) { + let name = name.to_js_string(interner); + lex_env.create_immutable_binding(name.clone(), true); + } + } _ => {} } } @@ -2255,16 +2295,14 @@ fn module_instantiation(module: &Module, env: &Scope, interner: &Interner) { drop(env.create_mutable_binding(name, false)); } } - LexicallyScopedDeclaration::LexicalDeclaration(LexicalDeclaration::Using(u)) => { - for name in bound_names(u) { - let name = name.to_js_string(interner); - drop(env.create_mutable_binding(name, false)); - } - } - LexicallyScopedDeclaration::LexicalDeclaration(LexicalDeclaration::AwaitUsing(au)) => { - for name in bound_names(au) { + LexicallyScopedDeclaration::LexicalDeclaration( + LexicalDeclaration::Using(declaration) + | LexicalDeclaration::AwaitUsing(declaration), + ) => { + env.set_has_using_declarations(); + for name in bound_names(declaration) { let name = name.to_js_string(interner); - drop(env.create_mutable_binding(name, false)); + env.create_immutable_binding(name.clone(), true); } } LexicallyScopedDeclaration::AssignmentExpression(expr) => { @@ -2498,6 +2536,16 @@ pub(crate) fn eval_declaration_instantiation_scope( lex_env.create_immutable_binding(name, true); } } + Declaration::Lexical( + LexicalDeclaration::Using(declaration) + | LexicalDeclaration::AwaitUsing(declaration), + ) => { + lex_env.set_has_using_declarations(); + for name in bound_names(declaration) { + let name = name.to_js_string(interner); + lex_env.create_immutable_binding(name, true); + } + } _ => {} } } diff --git a/core/engine/src/bytecompiler/mod.rs b/core/engine/src/bytecompiler/mod.rs index db4607d1abd..99fd994d6c6 100644 --- a/core/engine/src/bytecompiler/mod.rs +++ b/core/engine/src/bytecompiler/mod.rs @@ -2259,9 +2259,7 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO(@abhinavs1920): Add resource to disposal stack - // For now, we just bind the variable like a let declaration - // Full implementation will add: AddDisposableResource opcode + self.bytecode.emit_add_disposable_resource(value.variable()); self.emit_binding(BindingOpcode::InitLexical, ident, &value); self.register_allocator.dealloc(value); @@ -2275,7 +2273,7 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO: Same as above + self.bytecode.emit_add_disposable_resource(value.variable()); self.compile_declaration_pattern( pattern, @@ -2300,9 +2298,8 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO: Add resource to async disposal stack - // For now, we just bind the variable like a let declaration - // Full implementation will add: AddAsyncDisposableResource opcode + self.bytecode + .emit_add_async_disposable_resource(value.variable()); self.emit_binding(BindingOpcode::InitLexical, ident, &value); self.register_allocator.dealloc(value); @@ -2316,7 +2313,8 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO: SAME + self.bytecode + .emit_add_async_disposable_resource(value.variable()); self.compile_declaration_pattern( pattern, BindingOpcode::InitLexical, diff --git a/core/engine/src/lib.rs b/core/engine/src/lib.rs index 37558607d06..42c8fbae31b 100644 --- a/core/engine/src/lib.rs +++ b/core/engine/src/lib.rs @@ -105,6 +105,7 @@ pub mod value; pub mod vm; mod host_defined; +mod resource_management; mod sys; mod spanned_source_text; diff --git a/core/engine/src/resource_management.rs b/core/engine/src/resource_management.rs new file mode 100644 index 00000000000..e6497b83d6a --- /dev/null +++ b/core/engine/src/resource_management.rs @@ -0,0 +1,224 @@ +//! Runtime support for the ECMAScript Explicit Resource Management proposal. +//! +//! This module provides the core data structures and placeholder algorithms +//! needed to implement the `using` and `await using` declarations, as well +//! as the `DisposableStack` / `AsyncDisposableStack` built-in objects. +//! +//! **Current status:** Structural scaffolding only. Disposal logic is not yet +//! implemented; the bytecompiler does not yet emit resource-tracking opcodes. +//! +//! More information: +//! - [TC39 Proposal][proposal] +//! - [Spec text – DisposeResources][spec] +//! +//! [proposal]: https://github.com/tc39/proposal-explicit-resource-management +//! [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources + +use crate::{Context, JsValue}; +use boa_gc::{Finalize, Trace}; + +// --------------------------------------------------------------------------- +// DisposableResourceHint +// --------------------------------------------------------------------------- + +/// The disposal hint associated with a disposable resource. +/// +/// Corresponds to the `[[Hint]]` field of the `DisposableResource` Record in +/// the spec. +/// +/// More information: +/// - [Spec – DisposableResource Records][spec] +/// +/// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-disposableresource-records +#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)] +pub(crate) enum DisposableResourceHint { + /// Synchronous disposal via `Symbol.dispose`. + SyncDispose, + /// Asynchronous disposal via `Symbol.asyncDispose`. + AsyncDispose, +} + +// --------------------------------------------------------------------------- +// DisposableResource +// --------------------------------------------------------------------------- + +/// A single disposable resource tracked by the runtime. +/// +/// Corresponds to the **`DisposableResource` Record** defined in the +/// Explicit Resource Management proposal. +/// +/// More information: +/// - [Spec – DisposableResource Records][spec] +/// +/// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-disposableresource-records +#[derive(Debug, Clone, Trace, Finalize)] +pub(crate) struct DisposableResource { + /// `[[ResourceValue]]` – the value that was bound by the `using` declaration. + value: JsValue, + + /// `[[DisposeMethod]]` – the dispose function (obtained from `Symbol.dispose` + /// or `Symbol.asyncDispose` on the value). + dispose_method: JsValue, + + /// `[[Hint]]` – whether this resource uses sync or async disposal. + #[unsafe_ignore_trace] + #[allow(dead_code)] + hint: DisposableResourceHint, +} + +impl DisposableResource { + /// Creates a new `DisposableResource`. + #[inline] + #[must_use] + pub(crate) fn new( + value: JsValue, + dispose_method: JsValue, + hint: DisposableResourceHint, + ) -> Self { + Self { + value, + dispose_method, + hint, + } + } + + /// Returns the resource value. + #[inline] + #[must_use] + pub(crate) const fn value(&self) -> &JsValue { + &self.value + } + + /// Returns the dispose method. + #[inline] + #[must_use] + pub(crate) const fn dispose_method(&self) -> &JsValue { + &self.dispose_method + } + + /// Returns the disposal hint. + #[inline] + #[must_use] + #[allow(dead_code)] + pub(crate) const fn hint(&self) -> &DisposableResourceHint { + &self.hint + } +} + +// --------------------------------------------------------------------------- +// DisposableResourceStack +// --------------------------------------------------------------------------- + +/// A stack of [`DisposableResource`]s associated with a lexical scope. +/// +/// When a `using` or `await using` declaration is evaluated, the runtime +/// pushes a resource onto the stack. When the scope exits (normally or +/// abruptly), the resources are disposed in reverse order via +/// [`dispose_resources`]. +/// +/// Corresponds to the `[[DisposeCapability]]` field on certain Environment +/// Records in the proposal spec. +/// +/// More information: +/// - [Spec – DisposeResources][spec] +/// +/// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources +#[derive(Debug, Clone, Default, Trace, Finalize)] +pub(crate) struct DisposableResourceStack { + /// Resources in declaration order. Disposal happens in *reverse* order. + resources: Vec, +} + +impl DisposableResourceStack { + /// Creates an empty resource stack. + #[inline] + #[must_use] + pub(crate) fn new() -> Self { + Self { + resources: Vec::new(), + } + } + + /// Pushes a resource onto the stack. + #[inline] + pub(crate) fn push(&mut self, resource: DisposableResource) { + self.resources.push(resource); + } + + /// Returns `true` if there are no tracked resources. + #[inline] + #[must_use] + #[allow(dead_code)] + pub(crate) fn is_empty(&self) -> bool { + self.resources.is_empty() + } + + /// Returns the number of tracked resources. + #[inline] + #[must_use] + #[allow(dead_code)] + pub(crate) fn len(&self) -> usize { + self.resources.len() + } + + /// Returns an iterator over the resources in *reverse* (disposal) order. + #[inline] + pub(crate) fn drain_reversed(&mut self) -> impl Iterator + '_ { + self.resources.drain(..).rev() + } +} + +// --------------------------------------------------------------------------- +// dispose_resources (placeholder) +// --------------------------------------------------------------------------- + +/// `DisposeResources ( disposeCapability, completion )` +/// +/// Performs synchronous disposal of all resources tracked by the given +/// `DisposableResourceStack`, in reverse order. +/// +/// # Current status +/// +/// **This is a placeholder.** The actual algorithm is defined in: +/// +/// +/// The full implementation will: +/// 1. Iterate resources in reverse order. +/// 2. Call each resource's `[[DisposeMethod]]`. +/// 3. Collect any thrown errors into a `SuppressedError`. +/// 4. Return the original completion (or a `SuppressedError` if disposal failed). +/// +/// # Parameters +/// - `stack`: The disposal stack for the exiting scope. +/// +/// # Returns +/// Currently a no-op that simply drains the stack. +pub(crate) fn dispose_resources(context: &mut Context, stack: &mut DisposableResourceStack) { + // TODO: Implement full spec-compliant disposal per + // https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources + // + // Steps (from spec): + // 1. Assert: disposable is a DisposableResource Record. + // 2. Let result be Completion(Call(disposable.[[DisposeMethod]], disposable.[[ResourceValue]])). + // 3. If result is a throw completion, then + // a. If completion is a throw completion, then + // i. Set result to ThrowCompletion(a newly created SuppressedError ...). + // b. Set completion to result. + // 4. Return completion. + + // Drain resources in reverse order (dispose newest first). + for resource in stack.drain_reversed() { + if let Some(function) = resource.dispose_method().as_callable() { + // Call the dispose method; ignore errors for now + // (SuppressedError aggregation will be implemented later). + drop(function.call(resource.value(), &[], context)); + } + } +} + +// TODO: +// - Hook into using declarations in bytecompiler (emit AddDisposableResource opcode) +// - Implement DisposeResources algorithm (call dispose methods, handle SuppressedError) +// - Add support for async disposal (await using) +// - Implement DisposableStack / AsyncDisposableStack builtins +// - Wire DisposableResourceStack into CallFrame or environment records diff --git a/core/engine/src/vm/call_frame/mod.rs b/core/engine/src/vm/call_frame/mod.rs index cd560f18594..89c23859704 100644 --- a/core/engine/src/vm/call_frame/mod.rs +++ b/core/engine/src/vm/call_frame/mod.rs @@ -9,6 +9,7 @@ use crate::{ bytecompiler::Register, environments::EnvironmentStack, realm::Realm, + resource_management::DisposableResourceStack, vm::{CodeBlock, SourcePath}, }; use boa_ast::Position; @@ -80,6 +81,14 @@ pub struct CallFrame { // SAFETY: Nothing in `CallFrameFlags` requires tracing, so this is safe. #[unsafe_ignore_trace] pub(crate) flags: CallFrameFlags, + + /// Stack of disposable resource stacks, one per lexical scope. + /// When a new lexical scope is entered, a fresh `DisposableResourceStack` + /// is pushed. When the scope exits, the top stack is popped and its + /// resources are disposed in reverse order. + /// + /// See: + pub(crate) disposable_resource_stacks: Vec, } /// ---- `CallFrame` public API ---- @@ -155,6 +164,7 @@ impl CallFrame { environments, realm, flags: CallFrameFlags::empty(), + disposable_resource_stacks: Vec::new(), } } diff --git a/core/engine/src/vm/code_block.rs b/core/engine/src/vm/code_block.rs index d9eec2fc949..b6c513602ef 100644 --- a/core/engine/src/vm/code_block.rs +++ b/core/engine/src/vm/code_block.rs @@ -760,6 +760,8 @@ impl CodeBlock { | Instruction::Neg { value } | Instruction::IsObject { value } | Instruction::BindThisValue { value } + | Instruction::AddDisposableResource { value } + | Instruction::AddAsyncDisposableResource { value } | Instruction::BitNot { value } => { format!("value:{value}") } @@ -932,9 +934,7 @@ impl CodeBlock { | Instruction::Reserved55 | Instruction::Reserved56 | Instruction::Reserved57 - | Instruction::Reserved58 - | Instruction::Reserved59 - | Instruction::Reserved60 => unreachable!("Reserved opcodes are unreachable"), + | Instruction::Reserved58 => unreachable!("Reserved opcodes are unreachable"), } } } diff --git a/core/engine/src/vm/flowgraph/mod.rs b/core/engine/src/vm/flowgraph/mod.rs index 2f96e6edcbd..f8df149317d 100644 --- a/core/engine/src/vm/flowgraph/mod.rs +++ b/core/engine/src/vm/flowgraph/mod.rs @@ -367,7 +367,9 @@ impl CodeBlock { | Instruction::CheckReturn | Instruction::BindThisValue { .. } | Instruction::CreateMappedArgumentsObject { .. } - | Instruction::CreateUnmappedArgumentsObject { .. } => { + | Instruction::CreateUnmappedArgumentsObject { .. } + | Instruction::AddDisposableResource { .. } + | Instruction::AddAsyncDisposableResource { .. } => { graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line); } @@ -431,9 +433,7 @@ impl CodeBlock { | Instruction::Reserved55 | Instruction::Reserved56 | Instruction::Reserved57 - | Instruction::Reserved58 - | Instruction::Reserved59 - | Instruction::Reserved60 => unreachable!("Reserved opcodes are unreachable"), + | Instruction::Reserved58 => unreachable!("Reserved opcodes are unreachable"), } } diff --git a/core/engine/src/vm/mod.rs b/core/engine/src/vm/mod.rs index f01c9bb47dd..f7921a9d2c7 100644 --- a/core/engine/src/vm/mod.rs +++ b/core/engine/src/vm/mod.rs @@ -749,6 +749,11 @@ impl Context { env_fp = self.vm.frame().env_fp as usize; + // Drain any remaining scope-level resource stacks (fallback). + while let Some(mut stack) = self.vm.frame_mut().disposable_resource_stacks.pop() { + crate::resource_management::dispose_resources(self, &mut stack); + } + let Some(f) = self.vm.pop_frame() else { break; }; @@ -785,6 +790,11 @@ impl Context { return ControlFlow::Break(CompletionRecord::Return(result)); } + // Drain any remaining scope-level resource stacks (fallback for early returns). + while let Some(mut stack) = self.vm.frame_mut().disposable_resource_stacks.pop() { + crate::resource_management::dispose_resources(self, &mut stack); + } + self.vm.stack.push(result); self.vm.pop_frame().expect("frame must exist"); ControlFlow::Continue(()) @@ -796,6 +806,11 @@ impl Context { return ControlFlow::Break(CompletionRecord::Normal(result)); } + // Drain any remaining scope-level resource stacks (fallback for yields). + while let Some(mut stack) = self.vm.frame_mut().disposable_resource_stacks.pop() { + crate::resource_management::dispose_resources(self, &mut stack); + } + self.vm.stack.push(result); self.vm.pop_frame().expect("frame must exist"); ControlFlow::Continue(()) diff --git a/core/engine/src/vm/opcode/environment/erm.rs b/core/engine/src/vm/opcode/environment/erm.rs new file mode 100644 index 00000000000..716b9fffc36 --- /dev/null +++ b/core/engine/src/vm/opcode/environment/erm.rs @@ -0,0 +1,123 @@ +use crate::{ + Context, JsNativeError, JsResult, + resource_management::{DisposableResource, DisposableResourceHint, DisposableResourceStack}, + vm::opcode::{Operation, RegisterOperand}, +}; + +/// `AddDisposableResource` implements the Opcode Operation for `Opcode::AddDisposableResource` +/// +/// Operation: +/// - Track a synchronous disposable resource in the current scope. +#[derive(Debug, Clone, Copy)] +pub(crate) struct AddDisposableResource; + +impl AddDisposableResource { + #[inline(always)] + pub(crate) fn operation(value: RegisterOperand, context: &mut Context) -> JsResult<()> { + let value_js = context.vm.get_register(value.into()).clone(); + + // If the value is null or undefined, skip disposal as per spec. + if value_js.is_null_or_undefined() { + return Ok(()); + } + + let dispose_method = value_js.get_method(crate::symbol::JsSymbol::dispose(), context)?; + + let Some(dispose_method) = dispose_method else { + return Err(JsNativeError::typ() + .with_message("Resource is not synchronously disposable") + .into()); + }; + + let frame = context.vm.frame_mut(); + + // If no scope-level stack exists yet (e.g. top-level `using` without + // an explicit block), create a fallback stack. + if frame.disposable_resource_stacks.is_empty() { + frame + .disposable_resource_stacks + .push(DisposableResourceStack::new()); + } + + frame + .disposable_resource_stacks + .last_mut() + .expect("just ensured non-empty") + .push(DisposableResource::new( + value_js, + dispose_method.into(), + DisposableResourceHint::SyncDispose, + )); + + Ok(()) + } +} + +impl Operation for AddDisposableResource { + const NAME: &'static str = "AddDisposableResource"; + const INSTRUCTION: &'static str = "INST - AddDisposableResource"; + const COST: u8 = 4; +} + +/// `AddAsyncDisposableResource` implements the Opcode Operation for `Opcode::AddAsyncDisposableResource` +/// +/// Operation: +/// - Track an asynchronous disposable resource in the current scope. +#[derive(Debug, Clone, Copy)] +pub(crate) struct AddAsyncDisposableResource; + +impl AddAsyncDisposableResource { + #[inline(always)] + pub(crate) fn operation(value: RegisterOperand, context: &mut Context) -> JsResult<()> { + let value_js = context.vm.get_register(value.into()).clone(); + + if value_js.is_null_or_undefined() { + return Ok(()); + } + + // Try asyncDispose first, fallback to sync dispose later (to be done eventually, + // but for now let's just grab asyncDispose per basic requirements) + let mut dispose_method = + value_js.get_method(crate::symbol::JsSymbol::async_dispose(), context)?; + + let mut hint = DisposableResourceHint::AsyncDispose; + + if dispose_method.is_none() { + dispose_method = value_js.get_method(crate::symbol::JsSymbol::dispose(), context)?; + hint = DisposableResourceHint::SyncDispose; + } + + let Some(dispose_method) = dispose_method else { + return Err(JsNativeError::typ() + .with_message("Resource is not asynchronously disposable") + .into()); + }; + + let frame = context.vm.frame_mut(); + + // Fallback: create a scope-level stack if none exists. + if frame.disposable_resource_stacks.is_empty() { + frame + .disposable_resource_stacks + .push(DisposableResourceStack::new()); + } + + frame + .disposable_resource_stacks + .last_mut() + .expect("just ensured non-empty") + .push(DisposableResource::new( + value_js, + dispose_method.into(), + hint, + )); + + Ok(()) + } +} + +impl Operation for AddAsyncDisposableResource { + const NAME: &'static str = "AddAsyncDisposableResource"; + const INSTRUCTION: &'static str = "INST - AddAsyncDisposableResource"; + const COST: u8 = 4; +} diff --git a/core/engine/src/vm/opcode/environment/mod.rs b/core/engine/src/vm/opcode/environment/mod.rs index 0152358462c..273820d59c1 100644 --- a/core/engine/src/vm/opcode/environment/mod.rs +++ b/core/engine/src/vm/opcode/environment/mod.rs @@ -338,3 +338,6 @@ impl Operation for BindThisValue { const INSTRUCTION: &'static str = "INST - BindThisValue"; const COST: u8 = 6; } + +mod erm; +pub(crate) use erm::*; diff --git a/core/engine/src/vm/opcode/mod.rs b/core/engine/src/vm/opcode/mod.rs index e304514a8fc..6d88b9feadc 100644 --- a/core/engine/src/vm/opcode/mod.rs +++ b/core/engine/src/vm/opcode/mod.rs @@ -2255,8 +2255,14 @@ generate_opcodes! { Reserved57 => Reserved, /// Reserved [`Opcode`]. Reserved58 => Reserved, - /// Reserved [`Opcode`]. - Reserved59 => Reserved, - /// Reserved [`Opcode`]. - Reserved60 => Reserved, + /// Track a synchronous disposable resource. + /// + /// - Registers: + /// - Input: value + AddDisposableResource { value: RegisterOperand }, + /// Track an asynchronous disposable resource. + /// + /// - Registers: + /// - Input: value + AddAsyncDisposableResource { value: RegisterOperand }, } diff --git a/core/engine/src/vm/opcode/pop/mod.rs b/core/engine/src/vm/opcode/pop/mod.rs index 609310f37a1..bed1f10c8c1 100644 --- a/core/engine/src/vm/opcode/pop/mod.rs +++ b/core/engine/src/vm/opcode/pop/mod.rs @@ -30,6 +30,16 @@ pub(crate) struct PopEnvironment; impl PopEnvironment { #[inline(always)] pub(super) fn operation((): (), context: &mut Context) { + // Pop and dispose the resource stack for the exiting lexical scope. + // This must happen BEFORE the environment is popped, so that + // dispose methods can still access bindings in the current scope. + // + // See: https://tc39.es/proposal-explicit-resource-management/#sec-disposeresources + let popped = context.vm.frame_mut().disposable_resource_stacks.pop(); + if let Some(mut stack) = popped { + crate::resource_management::dispose_resources(context, &mut stack); + } + context.vm.frame_mut().environments.pop(); } } diff --git a/core/engine/src/vm/opcode/push/environment.rs b/core/engine/src/vm/opcode/push/environment.rs index f18593ab029..a8f23c7e486 100644 --- a/core/engine/src/vm/opcode/push/environment.rs +++ b/core/engine/src/vm/opcode/push/environment.rs @@ -2,6 +2,7 @@ use crate::{ Context, JsResult, builtins::function::OrdinaryFunction, environments::PrivateEnvironment, + resource_management::DisposableResourceStack, vm::opcode::{IndexOperand, Operation, RegisterOperand}, }; use boa_gc::Gc; @@ -23,6 +24,13 @@ impl PushScope { frame .environments .push_lexical(scope.num_bindings_non_local(), global); + + // Push a fresh disposable resource stack for this lexical scope. + // Resources declared with `using` in this scope will be tracked here + // and disposed when the scope exits (PopEnvironment). + frame + .disposable_resource_stacks + .push(DisposableResourceStack::new()); } } diff --git a/test262_config.toml b/test262_config.toml index 1bb2f29b959..de2397c82f2 100644 --- a/test262_config.toml +++ b/test262_config.toml @@ -13,7 +13,6 @@ features = [ "Intl-enumeration", "Intl.DurationFormat", "regexp-duplicate-named-groups", - "explicit-resource-management", "joint-iteration", ### Pending proposals @@ -91,4 +90,9 @@ tests = [ "test/built-ins/AbstractModuleSource/prototype.js", "test/built-ins/AbstractModuleSource/throw-from-constructor.js", "test/language/module-code/source-phase-import/import-source.js", + + # Explicit Resource Management (Async & Built-ins) + "test/built-ins/DisposableStack", + "test/built-ins/AsyncDisposableStack", + "test/language/statements/using/await-using", ]