Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e47a274
feat: Implement core disposal logic for explicit resource management
abhinavs1920 Mar 14, 2026
992583f
Merge branch 'main' into feat/res2
abhinavs1920 Mar 15, 2026
81a3f59
fix: opcode enum
abhinavs1920 Mar 15, 2026
92b7fa0
fix: formatting and lint issues
abhinavs1920 Mar 16, 2026
d3aa12e
Merge branch 'main' into feat/res2
abhinavs1920 Mar 16, 2026
201be32
Merge branch 'main' into feat/res2
abhinavs1920 Mar 16, 2026
d0517fe
Merge branch 'main' into feat/res2
abhinavs1920 Mar 16, 2026
2b99ecd
refactor: encode using declaration count statically in DisposeResourc…
abhinavs1920 Mar 16, 2026
42af9af
Merge branch 'main' into feat/res2
abhinavs1920 Mar 17, 2026
6935488
Merge branch 'main' into feat/res2
abhinavs1920 Mar 18, 2026
af80f7a
fix: restore Reserved1/2/3, remove global opcodes re-added by mistake
abhinavs1920 Mar 18, 2026
373889d
feat: gate using declarations behind experimental feature flag
abhinavs1920 Mar 18, 2026
a684036
Merge branch 'main' into feat/res2
abhinavs1920 Mar 18, 2026
abc7fc7
Merge branch 'main' into feat/res2
abhinavs1920 Mar 20, 2026
601309c
Merge branch 'main' into feat/res2
abhinavs1920 Mar 21, 2026
2d6f4d9
Merge branch 'main' into feat/res2
abhinavs1920 Mar 26, 2026
a438cee
feat: integrate try-finally semantics for using declarations
abhinavs1920 Apr 6, 2026
3d22954
fix: wrap function bodies with `using` declarations in try-finally fo…
abhinavs1920 Apr 11, 2026
16dbb0d
Merge branch 'main' into feat/res2
abhinavs1920 Apr 11, 2026
e3149cb
fix: formatting issue
abhinavs1920 Apr 11, 2026
17abdf8
feat: Add SuppressedError builtin
abhinavs1920 Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/engine/src/builtins/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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:
Expand Down
135 changes: 135 additions & 0 deletions core/engine/src/builtins/error/suppressed.rs
Original file line number Diff line number Diff line change
@@ -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::<Self>(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<JsValue> {
// 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())
}
}
5 changes: 4 additions & 1 deletion core/engine/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -433,6 +435,7 @@ pub(crate) fn set_default_global_bindings(context: &mut Context) -> JsResult<()>
global_binding::<EvalError>(context)?;
global_binding::<UriError>(context)?;
global_binding::<AggregateError>(context)?;
global_binding::<SuppressedError>(context)?;
global_binding::<Reflect>(context)?;
global_binding::<Promise>(context)?;
global_binding::<EncodeUri>(context)?;
Expand Down
118 changes: 117 additions & 1 deletion core/engine/src/bytecompiler/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
35 changes: 27 additions & 8 deletions core/engine/src/bytecompiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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,
Expand Down
Loading
Loading