Skip to content

Commit a438cee

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

File tree

2 files changed

+164
-8
lines changed

2 files changed

+164
-8
lines changed

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

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
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<'_> {
@@ -30,12 +30,90 @@ impl ByteCompiler<'_> {
3030
})
3131
.sum();
3232

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
3633
#[cfg(feature = "experimental")]
37-
if using_count > 0 {
38-
self.bytecode.emit_dispose_resources(using_count.into());
34+
let has_using = using_count > 0;
35+
36+
#[cfg(not(feature = "experimental"))]
37+
let has_using = false;
38+
39+
if has_using {
40+
#[cfg(feature = "experimental")]
41+
{
42+
// Blocks with `using` declarations need try-finally semantics
43+
// Allocate registers for finally control flow (same pattern as try-finally)
44+
let finally_re_throw = self.register_allocator.alloc();
45+
let finally_jump_index = self.register_allocator.alloc();
46+
47+
self.bytecode.emit_store_true(finally_re_throw.variable());
48+
self.bytecode.emit_store_zero(finally_jump_index.variable());
49+
50+
// Push jump control info to handle break/continue/return through disposal
51+
self.push_try_with_finally_control_info(
52+
&finally_re_throw,
53+
&finally_jump_index,
54+
use_expr,
55+
);
56+
57+
// Push exception handler to catch any exceptions during block execution
58+
let handler = self.push_handler();
59+
60+
// Compile the block body (this includes the `using` declarations)
61+
self.compile_statement_list(block.statement_list(), use_expr, true);
62+
63+
// Normal exit: mark that we don't need to re-throw
64+
self.bytecode.emit_store_false(finally_re_throw.variable());
65+
66+
let finally_jump = self.jump();
67+
68+
// Exception path: patch the handler
69+
self.patch_handler(handler);
70+
71+
// Push a second handler for exceptions during exception handling
72+
let catch_handler = self.push_handler();
73+
let error = self.register_allocator.alloc();
74+
self.bytecode.emit_exception(error.variable());
75+
self.bytecode.emit_store_true(finally_re_throw.variable());
76+
77+
let no_throw = self.jump();
78+
self.patch_handler(catch_handler);
79+
80+
self.patch_jump(no_throw);
81+
self.patch_jump(finally_jump);
82+
83+
// Finally block: dispose resources
84+
let finally_start = self.next_opcode_location();
85+
self.jump_info
86+
.last_mut()
87+
.expect("there should be a jump control info")
88+
.flags |= JumpControlInfoFlags::IN_FINALLY;
89+
90+
// Save accumulator (disposal might modify it, similar to compile_finally_stmt)
91+
let value = self.register_allocator.alloc();
92+
self.bytecode
93+
.emit_set_register_from_accumulator(value.variable());
94+
95+
// Emit disposal logic
96+
self.bytecode.emit_dispose_resources(using_count.into());
97+
98+
// Restore accumulator
99+
self.bytecode.emit_set_accumulator(value.variable());
100+
self.register_allocator.dealloc(value);
101+
102+
// Re-throw if there was an exception
103+
let do_not_throw_exit = self.jump_if_false(&finally_re_throw);
104+
self.bytecode.emit_throw(error.variable());
105+
self.register_allocator.dealloc(error);
106+
self.patch_jump(do_not_throw_exit);
107+
108+
// Pop jump control info (this handles break/continue/return via jump table)
109+
self.pop_try_with_finally_control_info(finally_start);
110+
111+
self.register_allocator.dealloc(finally_re_throw);
112+
self.register_allocator.dealloc(finally_jump_index);
113+
}
114+
} else {
115+
// Normal block compilation (no using declarations)
116+
self.compile_statement_list(block.statement_list(), use_expr, true);
39117
}
40118

41119
self.pop_declarative_scope(scope);

core/engine/tests/disposal.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ fn basic_disposal() {
2525
",
2626
));
2727

28+
if let Err(ref e) = result {
29+
eprintln!("Error: {e:?}");
30+
}
2831
assert!(result.is_ok());
2932
let value = result.unwrap();
3033
assert_eq!(value, JsValue::from(true));
@@ -99,7 +102,6 @@ fn disposal_with_no_method() {
99102
}
100103

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

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

0 commit comments

Comments
 (0)