Skip to content

Commit 10c0921

Browse files
committed
feat: integrate try-finally semantics for using declarations
Signed-off-by: Abhinav Sharma <abhinavs1920bpl@gmail.com>
1 parent 2d6f4d9 commit 10c0921

2 files changed

Lines changed: 171 additions & 12 deletions

File tree

core/engine/src/bytecompiler/statement/block.rs

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
use crate::bytecompiler::ByteCompiler;
2-
#[cfg(not(feature = "experimental"))]
2+
#[cfg(feature = "experimental")]
3+
use crate::bytecompiler::jump_control::JumpControlInfoFlags;
34
use boa_ast::statement::Block;
45
#[cfg(feature = "experimental")]
56
use boa_ast::{
67
declaration::LexicalDeclaration,
78
operations::{LexicallyScopedDeclaration, lexically_scoped_declarations},
8-
statement::Block,
99
};
1010

1111
impl ByteCompiler<'_> {
1212
/// Compile a [`Block`] `boa_ast` node
1313
pub(crate) fn compile_block(&mut self, block: &Block, use_expr: bool) {
14-
let scope = self.push_declarative_scope(block.scope());
15-
self.block_declaration_instantiation(block);
16-
17-
// Count how many `using` bindings are in this block (statically known at compile time)
14+
// Check if this block has `using` declarations
1815
#[cfg(feature = "experimental")]
1916
let using_count: u32 = lexically_scoped_declarations(block)
2017
.iter()
@@ -30,13 +27,100 @@ impl ByteCompiler<'_> {
3027
})
3128
.sum();
3229

33-
self.compile_statement_list(block.statement_list(), use_expr, true);
34-
35-
// Emit DisposeResources with the static count if there are any using declarations
3630
#[cfg(feature = "experimental")]
37-
if using_count > 0 {
38-
self.bytecode.emit_dispose_resources(using_count.into());
31+
let has_using = using_count > 0;
32+
33+
#[cfg(not(feature = "experimental"))]
34+
let has_using = false;
35+
36+
if has_using {
37+
// Blocks with `using` declarations need try-finally semantics
38+
// to ensure disposal happens on all exit paths (normal, exception, break, continue, return)
39+
self.compile_block_with_disposal(
40+
block,
41+
use_expr,
42+
#[cfg(feature = "experimental")]
43+
using_count,
44+
);
45+
} else {
46+
// Normal block compilation
47+
self.compile_block_without_disposal(block, use_expr);
3948
}
49+
}
50+
51+
/// Compile a block without disposal semantics (normal block)
52+
fn compile_block_without_disposal(&mut self, block: &Block, use_expr: bool) {
53+
let scope = self.push_declarative_scope(block.scope());
54+
self.block_declaration_instantiation(block);
55+
self.compile_statement_list(block.statement_list(), use_expr, true);
56+
self.pop_declarative_scope(scope);
57+
}
58+
59+
/// Compile a block with disposal semantics (block with `using` declarations)
60+
///
61+
/// This wraps the block in try-finally semantics to ensure resources are disposed
62+
/// on all exit paths: normal completion, exceptions, and early exits (break/continue/return).
63+
#[cfg(feature = "experimental")]
64+
fn compile_block_with_disposal(&mut self, block: &Block, use_expr: bool, using_count: u32) {
65+
let scope = self.push_declarative_scope(block.scope());
66+
self.block_declaration_instantiation(block);
67+
68+
// Allocate registers for finally control flow (same pattern as try-finally)
69+
let finally_re_throw = self.register_allocator.alloc();
70+
let finally_jump_index = self.register_allocator.alloc();
71+
72+
self.bytecode.emit_store_true(finally_re_throw.variable());
73+
self.bytecode.emit_store_zero(finally_jump_index.variable());
74+
75+
// Push jump control info to handle break/continue/return through disposal
76+
self.push_try_with_finally_control_info(&finally_re_throw, &finally_jump_index, use_expr);
77+
78+
// Push exception handler to catch any exceptions during block execution
79+
let handler = self.push_handler();
80+
81+
// Compile the block body (this includes the `using` declarations)
82+
self.compile_statement_list(block.statement_list(), use_expr, true);
83+
84+
// Normal exit: mark that we don't need to re-throw
85+
self.bytecode.emit_store_false(finally_re_throw.variable());
86+
87+
let finally_jump = self.jump();
88+
89+
// Exception path: patch the handler
90+
self.patch_handler(handler);
91+
92+
let catch_handler = self.push_handler();
93+
let error = self.register_allocator.alloc();
94+
self.bytecode.emit_exception(error.variable());
95+
self.bytecode.emit_store_true(finally_re_throw.variable());
96+
97+
let no_throw = self.jump();
98+
self.patch_handler(catch_handler);
99+
100+
self.patch_jump(no_throw);
101+
self.patch_jump(finally_jump);
102+
103+
// Finally block: dispose resources
104+
let finally_start = self.next_opcode_location();
105+
self.jump_info
106+
.last_mut()
107+
.expect("there should be a jump control info")
108+
.flags |= JumpControlInfoFlags::IN_FINALLY;
109+
110+
// Emit disposal logic
111+
self.bytecode.emit_dispose_resources(using_count.into());
112+
113+
// Re-throw if there was an exception
114+
let do_not_throw_exit = self.jump_if_false(&finally_re_throw);
115+
self.bytecode.emit_throw(error.variable());
116+
self.register_allocator.dealloc(error);
117+
self.patch_jump(do_not_throw_exit);
118+
119+
// Pop jump control info (this handles break/continue/return via jump table)
120+
self.pop_try_with_finally_control_info(finally_start);
121+
122+
self.register_allocator.dealloc(finally_re_throw);
123+
self.register_allocator.dealloc(finally_jump_index);
40124

41125
self.pop_declarative_scope(scope);
42126
}

core/engine/tests/disposal.rs

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ fn disposal_with_no_method() {
9999
}
100100

101101
#[test]
102-
#[ignore = "Disposal on exception requires try-finally integration - will be implemented in next phase"]
103102
fn disposal_on_exception() {
104103
let mut context = Context::default();
105104

@@ -186,3 +185,79 @@ fn multiple_resources_in_one_declaration() {
186185
// Should dispose in reverse order: b, then a
187186
assert_eq!(value.to_string(&mut context).unwrap(), "b,a");
188187
}
188+
189+
#[test]
190+
fn disposal_on_return() {
191+
let mut context = Context::default();
192+
193+
let result = context.eval(Source::from_bytes(
194+
r"
195+
let disposed = false;
196+
function test() {
197+
using x = {
198+
[Symbol.dispose]() {
199+
disposed = true;
200+
}
201+
};
202+
return 'early';
203+
}
204+
test();
205+
// Return the disposed flag
206+
disposed;
207+
",
208+
));
209+
210+
assert!(result.is_ok());
211+
let value = result.unwrap();
212+
assert_eq!(value, JsValue::from(true));
213+
}
214+
215+
#[test]
216+
fn disposal_on_break() {
217+
let mut context = Context::default();
218+
219+
let result = context.eval(Source::from_bytes(
220+
r"
221+
let disposed = false;
222+
while (true) {
223+
using x = {
224+
[Symbol.dispose]() {
225+
disposed = true;
226+
}
227+
};
228+
break;
229+
}
230+
disposed;
231+
",
232+
));
233+
234+
assert!(result.is_ok());
235+
let value = result.unwrap();
236+
assert_eq!(value, JsValue::from(true));
237+
}
238+
239+
#[test]
240+
fn disposal_on_continue() {
241+
let mut context = Context::default();
242+
243+
let result = context.eval(Source::from_bytes(
244+
r"
245+
let disposed = false;
246+
let count = 0;
247+
while (count < 2) {
248+
count++;
249+
using x = {
250+
[Symbol.dispose]() {
251+
disposed = true;
252+
}
253+
};
254+
continue;
255+
}
256+
disposed;
257+
",
258+
));
259+
260+
assert!(result.is_ok());
261+
let value = result.unwrap();
262+
assert_eq!(value, JsValue::from(true));
263+
}

0 commit comments

Comments
 (0)