diff --git a/core/engine/src/builtins/error/mod.rs b/core/engine/src/builtins/error/mod.rs index 44c8f561b9b..b8091cc95a4 100644 --- a/core/engine/src/builtins/error/mod.rs +++ b/core/engine/src/builtins/error/mod.rs @@ -34,6 +34,7 @@ pub(crate) mod aggregate; pub(crate) mod eval; pub(crate) mod range; pub(crate) mod reference; +pub(crate) mod suppressed; pub(crate) mod syntax; pub(crate) mod r#type; pub(crate) mod uri; @@ -45,6 +46,7 @@ pub(crate) use self::aggregate::AggregateError; pub(crate) use self::eval::EvalError; pub(crate) use self::range::RangeError; pub(crate) use self::reference::ReferenceError; +pub(crate) use self::suppressed::SuppressedError; pub(crate) use self::syntax::SyntaxError; pub(crate) use self::r#type::TypeError; pub(crate) use self::uri::UriError; @@ -66,6 +68,14 @@ pub enum ErrorKind { /// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects Aggregate, + /// The `SuppressedError` object type. + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror-objects + Suppressed, + /// The `Error` object type. /// /// More information: diff --git a/core/engine/src/builtins/error/suppressed.rs b/core/engine/src/builtins/error/suppressed.rs new file mode 100644 index 00000000000..e0653b914a0 --- /dev/null +++ b/core/engine/src/builtins/error/suppressed.rs @@ -0,0 +1,135 @@ +//! This module implements the global `SuppressedError` object. +//! +//! More information: +//! - [TC39 proposal][spec] +//! +//! [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror-objects + +use crate::{ + Context, JsArgs, JsExpect, JsResult, JsString, JsValue, + builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{JsObject, internal_methods::get_prototype_from_constructor}, + property::{Attribute, PropertyDescriptorBuilder}, + realm::Realm, + string::StaticJsStrings, +}; + +use super::{Error, ErrorKind}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct SuppressedError; + +impl IntrinsicObject for SuppressedError { + fn init(realm: &Realm) { + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + BuiltInBuilder::from_standard_constructor::(realm) + .prototype(realm.intrinsics().constructors().error().constructor()) + .inherits(Some(realm.intrinsics().constructors().error().prototype())) + .property(js_string!("name"), Self::NAME, attribute) + .property(js_string!("message"), js_string!(), attribute) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInObject for SuppressedError { + const NAME: JsString = StaticJsStrings::SUPPRESSED_ERROR; +} + +impl BuiltInConstructor for SuppressedError { + const CONSTRUCTOR_ARGUMENTS: usize = 3; + const PROTOTYPE_STORAGE_SLOTS: usize = 2; + const CONSTRUCTOR_STORAGE_SLOTS: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::suppressed_error; + + /// [`SuppressedError ( error, suppressed, message [ , options ] )`][spec] + /// + /// Creates a new suppressed error object. + /// + /// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror-constructor + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + let new_target = &if new_target.is_undefined() { + context + .active_function_object() + .unwrap_or_else(|| { + context + .intrinsics() + .constructors() + .suppressed_error() + .constructor() + }) + .into() + } else { + new_target.clone() + }; + + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%SuppressedError.prototype%", « [[ErrorData]] »). + let prototype = get_prototype_from_constructor( + new_target, + StandardConstructors::suppressed_error, + context, + )?; + let o = JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + prototype, + Error::with_caller_position(ErrorKind::Suppressed, context), + ) + .upcast(); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(2); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw(js_string!("message"), msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(3), context)?; + + // 5. Perform ! DefinePropertyOrThrow(O, "error", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: error }). + let error_val = args.get_or_undefined(0).clone(); + o.define_property_or_throw( + js_string!("error"), + PropertyDescriptorBuilder::new() + .configurable(true) + .enumerable(false) + .writable(true) + .value(error_val) + .build(), + context, + ) + .js_expect("should not fail according to spec")?; + + // 6. Perform ! DefinePropertyOrThrow(O, "suppressed", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: suppressed }). + let suppressed_val = args.get_or_undefined(1).clone(); + o.define_property_or_throw( + js_string!("suppressed"), + PropertyDescriptorBuilder::new() + .configurable(true) + .enumerable(false) + .writable(true) + .value(suppressed_val) + .build(), + context, + ) + .js_expect("should not fail according to spec")?; + + // 7. Return O. + Ok(o.into()) + } +} diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index 6b6e8470ec3..84038681171 100644 --- a/core/engine/src/builtins/mod.rs +++ b/core/engine/src/builtins/mod.rs @@ -64,7 +64,8 @@ pub(crate) use self::{ dataview::DataView, date::Date, error::{ - AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, UriError, + AggregateError, EvalError, RangeError, ReferenceError, SuppressedError, SyntaxError, + TypeError, UriError, }, eval::Eval, finalization_registry::FinalizationRegistry, @@ -300,6 +301,7 @@ impl Realm { EvalError::init(self); UriError::init(self); AggregateError::init(self); + SuppressedError::init(self); Reflect::init(self); Generator::init(self); GeneratorFunction::init(self); @@ -433,6 +435,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context) -> JsResult<()> global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; + global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; global_binding::(context)?; diff --git a/core/engine/src/bytecompiler/function.rs b/core/engine/src/bytecompiler/function.rs index 9316e28ae6b..4ef81f48d2e 100644 --- a/core/engine/src/bytecompiler/function.rs +++ b/core/engine/src/bytecompiler/function.rs @@ -219,7 +219,123 @@ impl FunctionCompiler { { let mut compiler = compiler.position_guard(body); - compiler.compile_statement_list(body.statement_list(), false, false); + + // Check if the function body contains `using` declarations + #[cfg(feature = "experimental")] + let using_count: u32 = { + use boa_ast::{ + declaration::LexicalDeclaration, + operations::{LexicallyScopedDeclaration, lexically_scoped_declarations}, + }; + + lexically_scoped_declarations(body.statement_list()) + .iter() + .filter_map(|decl| { + if let LexicallyScopedDeclaration::LexicalDeclaration( + LexicalDeclaration::Using(u) | LexicalDeclaration::AwaitUsing(u), + ) = decl + { + Some(u.as_ref().len() as u32) + } else { + None + } + }) + .sum() + }; + + #[cfg(not(feature = "experimental"))] + let using_count: u32 = 0; + + if using_count > 0 { + #[cfg(feature = "experimental")] + { + use crate::bytecompiler::jump_control::JumpControlInfoFlags; + + // Function body with `using` declarations needs try-finally semantics + let finally_re_throw = compiler.register_allocator.alloc(); + let finally_jump_index = compiler.register_allocator.alloc(); + + compiler + .bytecode + .emit_store_true(finally_re_throw.variable()); + compiler + .bytecode + .emit_store_zero(finally_jump_index.variable()); + + // Push jump control info to handle break/continue/return through disposal + compiler.push_try_with_finally_control_info( + &finally_re_throw, + &finally_jump_index, + false, + ); + + // Push exception handler + let handler = compiler.push_handler(); + + // Compile the function body + compiler.compile_statement_list(body.statement_list(), false, false); + + // Normal exit: mark that we don't need to re-throw + compiler + .bytecode + .emit_store_false(finally_re_throw.variable()); + + let finally_jump = compiler.jump(); + + // Exception path: patch the handler + compiler.patch_handler(handler); + + // Push a second handler for exceptions during exception handling + let catch_handler = compiler.push_handler(); + let error = compiler.register_allocator.alloc(); + compiler.bytecode.emit_exception(error.variable()); + compiler + .bytecode + .emit_store_true(finally_re_throw.variable()); + + let no_throw = compiler.jump(); + compiler.patch_handler(catch_handler); + + compiler.patch_jump(no_throw); + compiler.patch_jump(finally_jump); + + // Finally block: dispose resources + let finally_start = compiler.next_opcode_location(); + compiler + .jump_info + .last_mut() + .expect("there should be a jump control info") + .flags |= JumpControlInfoFlags::IN_FINALLY; + + // Save accumulator + let value = compiler.register_allocator.alloc(); + compiler + .bytecode + .emit_set_register_from_accumulator(value.variable()); + + // Emit disposal logic + compiler.bytecode.emit_dispose_resources(using_count.into()); + + // Restore accumulator + compiler.bytecode.emit_set_accumulator(value.variable()); + compiler.register_allocator.dealloc(value); + + // Re-throw if there was an exception + let do_not_throw_exit = compiler.jump_if_false(&finally_re_throw); + compiler.bytecode.emit_throw(error.variable()); + compiler.register_allocator.dealloc(error); + compiler.patch_jump(do_not_throw_exit); + + // Pop jump control info (this handles break/continue/return via jump table) + compiler.pop_try_with_finally_control_info(finally_start); + + compiler.register_allocator.dealloc(finally_re_throw); + compiler.register_allocator.dealloc(finally_jump_index); + } + } else { + // Normal function body compilation (no using declarations) + compiler.compile_statement_list(body.statement_list(), false, false); + } } compiler.params = parameters.clone(); diff --git a/core/engine/src/bytecompiler/mod.rs b/core/engine/src/bytecompiler/mod.rs index 2efa019a694..f32405169b0 100644 --- a/core/engine/src/bytecompiler/mod.rs +++ b/core/engine/src/bytecompiler/mod.rs @@ -2271,9 +2271,14 @@ 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 + // Add resource to disposal stack + #[cfg(feature = "experimental")] + self.bytecode.emit_add_disposable_resource(value.variable()); + + #[cfg(not(feature = "experimental"))] + self.emit_type_error( + "using declarations require the 'experimental' feature", + ); self.emit_binding(BindingOpcode::InitLexical, ident, &value); self.register_allocator.dealloc(value); @@ -2287,7 +2292,14 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO: Same as above + // Add resource to disposal stack + #[cfg(feature = "experimental")] + self.bytecode.emit_add_disposable_resource(value.variable()); + + #[cfg(not(feature = "experimental"))] + self.emit_type_error( + "using declarations require the 'experimental' feature", + ); self.compile_declaration_pattern( pattern, @@ -2312,9 +2324,11 @@ 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 + // await using is not yet implemented even under experimental + #[cfg(not(feature = "experimental"))] + self.emit_type_error( + "await using declarations require the 'experimental' feature", + ); self.emit_binding(BindingOpcode::InitLexical, ident, &value); self.register_allocator.dealloc(value); @@ -2328,7 +2342,12 @@ impl<'ctx> ByteCompiler<'ctx> { self.bytecode.emit_store_undefined(value.variable()); } - // TODO: SAME + // await using is not yet implemented even under experimental + #[cfg(not(feature = "experimental"))] + self.emit_type_error( + "await using declarations require the 'experimental' feature", + ); + self.compile_declaration_pattern( pattern, BindingOpcode::InitLexical, diff --git a/core/engine/src/bytecompiler/statement/block.rs b/core/engine/src/bytecompiler/statement/block.rs index 99da2023627..400cd3a80a2 100644 --- a/core/engine/src/bytecompiler/statement/block.rs +++ b/core/engine/src/bytecompiler/statement/block.rs @@ -1,12 +1,121 @@ use crate::bytecompiler::ByteCompiler; +#[cfg(feature = "experimental")] +use crate::bytecompiler::jump_control::JumpControlInfoFlags; use boa_ast::statement::Block; +#[cfg(feature = "experimental")] +use boa_ast::{ + declaration::LexicalDeclaration, + operations::{LexicallyScopedDeclaration, lexically_scoped_declarations}, +}; impl ByteCompiler<'_> { /// Compile a [`Block`] `boa_ast` node pub(crate) fn compile_block(&mut self, block: &Block, use_expr: bool) { let scope = self.push_declarative_scope(block.scope()); self.block_declaration_instantiation(block); - self.compile_statement_list(block.statement_list(), use_expr, true); + + // Count how many `using` bindings are in this block (statically known at compile time) + #[cfg(feature = "experimental")] + let using_count: u32 = lexically_scoped_declarations(block) + .iter() + .filter_map(|decl| { + if let LexicallyScopedDeclaration::LexicalDeclaration( + LexicalDeclaration::Using(u) | LexicalDeclaration::AwaitUsing(u), + ) = decl + { + Some(u.as_ref().len() as u32) + } else { + None + } + }) + .sum(); + + #[cfg(feature = "experimental")] + let has_using = using_count > 0; + + #[cfg(not(feature = "experimental"))] + let has_using = false; + + if has_using { + #[cfg(feature = "experimental")] + { + // Blocks with `using` declarations need try-finally semantics + // Allocate registers for finally control flow (same pattern as try-finally) + let finally_re_throw = self.register_allocator.alloc(); + let finally_jump_index = self.register_allocator.alloc(); + + self.bytecode.emit_store_true(finally_re_throw.variable()); + self.bytecode.emit_store_zero(finally_jump_index.variable()); + + // Push jump control info to handle break/continue/return through disposal + self.push_try_with_finally_control_info( + &finally_re_throw, + &finally_jump_index, + use_expr, + ); + + // Push exception handler to catch any exceptions during block execution + let handler = self.push_handler(); + + // Compile the block body (this includes the `using` declarations) + self.compile_statement_list(block.statement_list(), use_expr, true); + + // Normal exit: mark that we don't need to re-throw + self.bytecode.emit_store_false(finally_re_throw.variable()); + + let finally_jump = self.jump(); + + // Exception path: patch the handler + self.patch_handler(handler); + + // Push a second handler for exceptions during exception handling + let catch_handler = self.push_handler(); + let error = self.register_allocator.alloc(); + self.bytecode.emit_exception(error.variable()); + self.bytecode.emit_store_true(finally_re_throw.variable()); + + let no_throw = self.jump(); + self.patch_handler(catch_handler); + + self.patch_jump(no_throw); + self.patch_jump(finally_jump); + + // Finally block: dispose resources + let finally_start = self.next_opcode_location(); + self.jump_info + .last_mut() + .expect("there should be a jump control info") + .flags |= JumpControlInfoFlags::IN_FINALLY; + + // Save accumulator (disposal might modify it, similar to compile_finally_stmt) + let value = self.register_allocator.alloc(); + self.bytecode + .emit_set_register_from_accumulator(value.variable()); + + // Emit disposal logic + self.bytecode.emit_dispose_resources(using_count.into()); + + // Restore accumulator + self.bytecode.emit_set_accumulator(value.variable()); + self.register_allocator.dealloc(value); + + // Re-throw if there was an exception + let do_not_throw_exit = self.jump_if_false(&finally_re_throw); + self.bytecode.emit_throw(error.variable()); + self.register_allocator.dealloc(error); + self.patch_jump(do_not_throw_exit); + + // Pop jump control info (this handles break/continue/return via jump table) + self.pop_try_with_finally_control_info(finally_start); + + self.register_allocator.dealloc(finally_re_throw); + self.register_allocator.dealloc(finally_jump_index); + } + } else { + // Normal block compilation (no using declarations) + self.compile_statement_list(block.statement_list(), use_expr, true); + } + self.pop_declarative_scope(scope); } } diff --git a/core/engine/src/context/intrinsics.rs b/core/engine/src/context/intrinsics.rs index 98e601c9d4f..4be4376ae5b 100644 --- a/core/engine/src/context/intrinsics.rs +++ b/core/engine/src/context/intrinsics.rs @@ -146,6 +146,7 @@ pub struct StandardConstructors { eval_error: StandardConstructor, uri_error: StandardConstructor, aggregate_error: StandardConstructor, + suppressed_error: StandardConstructor, map: StandardConstructor, set: StandardConstructor, typed_array: StandardConstructor, @@ -242,6 +243,7 @@ impl Default for StandardConstructors { eval_error: StandardConstructor::default(), uri_error: StandardConstructor::default(), aggregate_error: StandardConstructor::default(), + suppressed_error: StandardConstructor::default(), map: StandardConstructor::default(), set: StandardConstructor::default(), typed_array: StandardConstructor::default(), @@ -569,6 +571,18 @@ impl StandardConstructors { &self.aggregate_error } + /// Returns the `SuppressedError` constructor. + /// + /// More information: + /// - [TC39 proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-explicit-resource-management/#sec-suppressederror-constructor + #[inline] + #[must_use] + pub const fn suppressed_error(&self) -> &StandardConstructor { + &self.suppressed_error + } + /// Returns the `Map` constructor. /// /// More information: diff --git a/core/engine/src/error/mod.rs b/core/engine/src/error/mod.rs index 1fc5c934c51..95890630309 100644 --- a/core/engine/src/error/mod.rs +++ b/core/engine/src/error/mod.rs @@ -601,6 +601,11 @@ impl JsError { ErrorKind::Reference => JsNativeErrorKind::Reference, ErrorKind::Syntax => JsNativeErrorKind::Syntax, ErrorKind::Uri => JsNativeErrorKind::Uri, + ErrorKind::Suppressed => { + // SuppressedError is treated as a generic Error for native conversion + // The .error and .suppressed properties are preserved on the JS object + JsNativeErrorKind::Error + } ErrorKind::Aggregate => { let errors = obj.get(js_string!("errors"), context).map_err(|e| { TryNativeError::InaccessibleProperty { diff --git a/core/engine/src/vm/code_block.rs b/core/engine/src/vm/code_block.rs index d9eec2fc949..587ee193c1e 100644 --- a/core/engine/src/vm/code_block.rs +++ b/core/engine/src/vm/code_block.rs @@ -875,6 +875,8 @@ impl CodeBlock { | Instruction::PopPrivateEnvironment | Instruction::Generator | Instruction::AsyncGenerator => String::new(), + Instruction::AddDisposableResource { value } => format!("value: {value}"), + Instruction::DisposeResources { count } => format!("count: {count}"), Instruction::Reserved1 | Instruction::Reserved2 | Instruction::Reserved3 @@ -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..ab6087a89e4 100644 --- a/core/engine/src/vm/flowgraph/mod.rs +++ b/core/engine/src/vm/flowgraph/mod.rs @@ -374,6 +374,14 @@ impl CodeBlock { Instruction::Return => { graph.add_node(previous_pc, NodeShape::Diamond, label.into(), Color::Red); } + Instruction::AddDisposableResource { value } => { + let label = format!("AddDisposableResource value: {value}"); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + } + Instruction::DisposeResources { count } => { + let label = format!("DisposeResources count: {count}"); + graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None); + } Instruction::Reserved1 | Instruction::Reserved2 | Instruction::Reserved3 @@ -431,9 +439,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 dc6b1dca985..9d83e4ee319 100644 --- a/core/engine/src/vm/mod.rs +++ b/core/engine/src/vm/mod.rs @@ -96,6 +96,12 @@ pub struct Vm { pub(crate) shadow_stack: ShadowStack, + /// Stack of disposable resources for explicit resource management. + /// + /// Resources are added via `using` declarations and disposed in reverse order (LIFO) + /// when the scope exits. + pub(crate) disposal_stack: Vec<(JsValue, JsValue)>, + #[cfg(feature = "trace")] pub(crate) trace: bool, #[cfg(feature = "trace")] @@ -421,6 +427,7 @@ impl Vm { native_active_function: None, host_call_depth: 0, shadow_stack: ShadowStack::default(), + disposal_stack: Vec::new(), #[cfg(feature = "trace")] trace: false, #[cfg(feature = "trace")] @@ -673,6 +680,16 @@ impl Vm { pub(crate) fn take_return_value(&mut self) -> JsValue { std::mem::take(&mut self.return_value) } + + /// Push a disposable resource onto the disposal stack. + pub(crate) fn push_disposable_resource(&mut self, value: JsValue, method: JsValue) { + self.disposal_stack.push((value, method)); + } + + /// Pop a disposable resource from the disposal stack. + pub(crate) fn pop_disposable_resource(&mut self) -> Option<(JsValue, JsValue)> { + self.disposal_stack.pop() + } } #[allow(clippy::print_stdout)] diff --git a/core/engine/src/vm/opcode/disposal/add_disposable.rs b/core/engine/src/vm/opcode/disposal/add_disposable.rs new file mode 100644 index 00000000000..cd6648f5ac0 --- /dev/null +++ b/core/engine/src/vm/opcode/disposal/add_disposable.rs @@ -0,0 +1,49 @@ +use crate::{ + Context, JsResult, + vm::opcode::{Operation, RegisterOperand}, +}; + +/// `AddDisposableResource` implements the AddDisposableResource operation. +/// +/// This opcode adds a resource to the disposal stack for later cleanup. +/// +/// Operation: +/// - Stack: **=>** +/// - Registers: +/// - Input: value +pub(crate) struct AddDisposableResource; + +impl AddDisposableResource { + pub(crate) fn operation(value: RegisterOperand, context: &mut Context) -> JsResult<()> { + let value = context.vm.get_register(value.into()).clone(); + + // Per spec: If value is null or undefined, return + if value.is_null_or_undefined() { + return Ok(()); + } + + // Get the dispose method (value[Symbol.dispose]) + let key = crate::JsSymbol::dispose(); + let dispose_method = value.get_method(key, context)?; + + // If dispose method is None, throw TypeError (per spec) + let Some(dispose_method) = dispose_method else { + return Err(crate::JsNativeError::typ() + .with_message("The value is not disposable") + .into()); + }; + + // Add to disposal stack + context + .vm + .push_disposable_resource(value, dispose_method.into()); + + Ok(()) + } +} + +impl Operation for AddDisposableResource { + const NAME: &'static str = "AddDisposableResource"; + const INSTRUCTION: &'static str = "INST - AddDisposableResource"; + const COST: u8 = 3; +} diff --git a/core/engine/src/vm/opcode/disposal/dispose_resources.rs b/core/engine/src/vm/opcode/disposal/dispose_resources.rs new file mode 100644 index 00000000000..92b099f93e0 --- /dev/null +++ b/core/engine/src/vm/opcode/disposal/dispose_resources.rs @@ -0,0 +1,73 @@ +use crate::{ + Context, JsError, JsValue, + vm::opcode::{IndexOperand, Operation}, +}; + +/// `DisposeResources` implements the DisposeResources operation. +/// +/// This opcode disposes the last `count` resources from the disposal stack. +/// The count is statically determined by the bytecompiler. +/// +/// Operation: +/// - Stack: **=>** +pub(crate) struct DisposeResources; + +impl DisposeResources { + pub(crate) fn operation(count: IndexOperand, context: &mut Context) -> crate::JsResult<()> { + let count = u32::from(count) as usize; + let mut suppressed_error: Option = None; + + // Dispose exactly `count` resources in reverse order (LIFO) + for _ in 0..count { + if let Some((value, method)) = context.vm.pop_disposable_resource() { + let result = method.call(&value, &[], context); + + if let Err(err) = result { + suppressed_error = Some(match suppressed_error { + None => err, + Some(previous) => create_suppressed_error(err, &previous, context), + }); + } + } + } + + if let Some(err) = suppressed_error { + return Err(err); + } + + Ok(()) + } +} + +impl Operation for DisposeResources { + const NAME: &'static str = "DisposeResources"; + const INSTRUCTION: &'static str = "INST - DisposeResources"; + const COST: u8 = 5; +} + +/// Helper function to create a SuppressedError +fn create_suppressed_error(error: JsError, suppressed: &JsError, context: &mut Context) -> JsError { + // Create a proper SuppressedError using the builtin constructor + // Call SuppressedError(error, suppressed) + let Ok(error_val) = error.into_opaque(context) else { + // If we can't convert the error, just return it + return suppressed.clone(); + }; + let Ok(suppressed_val) = suppressed.clone().into_opaque(context) else { + return JsError::from_opaque(error_val); + }; + + let args = [error_val, suppressed_val]; + + let suppressed_error_constructor = context + .intrinsics() + .constructors() + .suppressed_error() + .constructor(); + + // Call the constructor as a function + match suppressed_error_constructor.call(&JsValue::undefined(), &args, context) { + Ok(obj) => JsError::from_opaque(obj), + Err(e) => e, // Fallback if construction fails + } +} diff --git a/core/engine/src/vm/opcode/disposal/mod.rs b/core/engine/src/vm/opcode/disposal/mod.rs new file mode 100644 index 00000000000..28518342a7e --- /dev/null +++ b/core/engine/src/vm/opcode/disposal/mod.rs @@ -0,0 +1,5 @@ +mod add_disposable; +mod dispose_resources; + +pub(crate) use add_disposable::*; +pub(crate) use dispose_resources::*; diff --git a/core/engine/src/vm/opcode/mod.rs b/core/engine/src/vm/opcode/mod.rs index f01c7afecbe..5439ecc85bf 100644 --- a/core/engine/src/vm/opcode/mod.rs +++ b/core/engine/src/vm/opcode/mod.rs @@ -33,6 +33,7 @@ mod control_flow; mod copy; mod define; mod delete; +mod disposal; mod environment; mod function; mod generator; @@ -72,6 +73,8 @@ pub(crate) use define::*; #[doc(inline)] pub(crate) use delete::*; #[doc(inline)] +pub(crate) use disposal::*; +#[doc(inline)] pub(crate) use environment::*; #[doc(inline)] pub(crate) use function::*; @@ -2152,6 +2155,23 @@ generate_opcodes! { /// - Output: dst CreateUnmappedArgumentsObject { dst: RegisterOperand }, + /// Add a disposable resource to the disposal stack. + /// + /// This opcode implements the AddDisposableResource abstract operation. + /// It gets the dispose method from the value and adds it to the disposal stack. + /// + /// - Registers: + /// - Input: value + AddDisposableResource { #[allow(dead_code)] value: RegisterOperand }, + + /// Dispose all resources in the current disposal stack. + /// + /// This opcode implements the DisposeResources abstract operation. + /// It calls the last `count` dispose methods in reverse order (LIFO). + /// The count is statically determined by the bytecompiler. + /// + /// - Stack: **=>** + DisposeResources { count: IndexOperand }, /// Reserved [`Opcode`]. Reserved1 => Reserved, /// Reserved [`Opcode`]. @@ -2268,8 +2288,4 @@ generate_opcodes! { Reserved57 => Reserved, /// Reserved [`Opcode`]. Reserved58 => Reserved, - /// Reserved [`Opcode`]. - Reserved59 => Reserved, - /// Reserved [`Opcode`]. - Reserved60 => Reserved, } diff --git a/core/engine/tests/disposal.rs b/core/engine/tests/disposal.rs new file mode 100644 index 00000000000..f4ccd7d94ad --- /dev/null +++ b/core/engine/tests/disposal.rs @@ -0,0 +1,308 @@ +//! Tests for explicit resource management (using declarations). +//! +//! This module tests the core disposal mechanism for `using` declarations, +//! verifying that resources are properly disposed when scopes exit. + +#![allow(unused_crate_dependencies)] + +use boa_engine::{Context, JsValue, Source}; + +#[test] +fn basic_disposal() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let disposed = false; + { + using x = { + [Symbol.dispose]() { + disposed = true; + } + }; + } + disposed; + ", + )); + + if let Err(ref e) = result { + eprintln!("Error: {e:?}"); + } + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value, JsValue::from(true)); +} + +#[test] +fn disposal_order() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let order = []; + { + using a = { + [Symbol.dispose]() { + order.push('a'); + } + }; + using b = { + [Symbol.dispose]() { + order.push('b'); + } + }; + } + order.join(','); + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + // Should dispose in reverse order: b, then a + assert_eq!(value.to_string(&mut context).unwrap(), "b,a"); +} + +#[test] +fn null_undefined_disposal() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + { + using x = null; + using y = undefined; + } + 'ok'; + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value.to_string(&mut context).unwrap(), "ok"); +} + +#[test] +fn disposal_with_no_method() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + { + using x = { + // No Symbol.dispose method + }; + } + 'ok'; + ", + )); + + // Should throw TypeError for missing dispose method + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("not disposable")); +} + +#[test] +fn disposal_on_exception() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let disposed = false; + try { + using x = { + [Symbol.dispose]() { + disposed = true; + } + }; + throw new Error('test error'); + } catch (e) { + // Disposal should happen before catch + } + disposed; + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value, JsValue::from(true)); +} + +#[test] +fn nested_scopes() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let order = []; + { + using a = { + [Symbol.dispose]() { + order.push('a'); + } + }; + { + using b = { + [Symbol.dispose]() { + order.push('b'); + } + }; + } + // b should be disposed here + order.push('middle'); + } + // a should be disposed here + order.join(','); + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + // Should dispose b first, then a + assert_eq!(value.to_string(&mut context).unwrap(), "b,middle,a"); +} + +#[test] +fn multiple_resources_in_one_declaration() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let order = []; + { + using a = { + [Symbol.dispose]() { + order.push('a'); + } + }, b = { + [Symbol.dispose]() { + order.push('b'); + } + }; + } + order.join(','); + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + // Should dispose in reverse order: b, then a + assert_eq!(value.to_string(&mut context).unwrap(), "b,a"); +} + +#[test] +fn disposal_on_return() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let disposed = false; + function test() { + using x = { + [Symbol.dispose]() { + disposed = true; + } + }; + return 'early'; + } + test(); + // Return the disposed flag + disposed; + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value, JsValue::from(true)); +} + +#[test] +fn disposal_on_break() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let disposed = false; + while (true) { + using x = { + [Symbol.dispose]() { + disposed = true; + } + }; + break; + } + disposed; + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value, JsValue::from(true)); +} + +#[test] +fn disposal_on_continue() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let disposed = false; + let count = 0; + while (count < 2) { + count++; + using x = { + [Symbol.dispose]() { + disposed = true; + } + }; + continue; + } + disposed; + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value, JsValue::from(true)); +} + +#[test] +fn suppressed_error_on_multiple_dispose_errors() { + let mut context = Context::default(); + + let result = context.eval(Source::from_bytes( + r" + let errors = []; + try { + using x = { + [Symbol.dispose]() { + throw new Error('first error'); + } + }; + using y = { + [Symbol.dispose]() { + throw new Error('second error'); + } + }; + } catch (e) { + errors.push(e.name); + if (e.error) { + errors.push(e.error.message); + } + if (e.suppressed) { + errors.push(e.suppressed.message); + } + } + errors.join('|'); + ", + )); + + assert!(result.is_ok()); + let value = result.unwrap(); + let result_str = value.to_string(&mut context).unwrap(); + let result_str = result_str.to_std_string().unwrap(); + // Should have SuppressedError with first error and suppressed second error + assert!(result_str.contains("SuppressedError")); + assert!(result_str.contains("first error")); + assert!(result_str.contains("second error")); +} diff --git a/core/string/src/common.rs b/core/string/src/common.rs index bf6e0e12a3e..38370a77d7d 100644 --- a/core/string/src/common.rs +++ b/core/string/src/common.rs @@ -134,6 +134,7 @@ impl StaticJsStrings { (DATE, "Date"), (ERROR, "Error"), (AGGREGATE_ERROR, "AggregateError"), + (SUPPRESSED_ERROR, "SuppressedError"), (EVAL_ERROR, "EvalError"), (RANGE_ERROR, "RangeError"), (REFERENCE_ERROR, "ReferenceError"), @@ -282,6 +283,7 @@ const RAW_STATICS: &[StaticString] = &[ StaticString::new(JsStr::latin1("Date".as_bytes())), StaticString::new(JsStr::latin1("Error".as_bytes())), StaticString::new(JsStr::latin1("AggregateError".as_bytes())), + StaticString::new(JsStr::latin1("SuppressedError".as_bytes())), StaticString::new(JsStr::latin1("EvalError".as_bytes())), StaticString::new(JsStr::latin1("RangeError".as_bytes())), StaticString::new(JsStr::latin1("ReferenceError".as_bytes())),