Browse Source

LibJS: Save scheduled jumps when entering unwind contexts

These are then restored upon `ContinuePendingUnwind`.
This stops us from forgetting where we needed to jump when we do extra
try-catches in finally blocks.

Co-Authored-By: Jesús "gsus" Lapastora <cyber.gsuscode@gmail.com>
Hendiadyoin1 1 year ago
parent
commit
1341f4438d

+ 0 - 1
Userland/Libraries/LibJS/Bytecode/BasicBlock.h

@@ -15,7 +15,6 @@ namespace JS::Bytecode {
 
 struct UnwindInfo {
     Executable const* executable;
-
     JS::GCPtr<Environment> lexical_environment;
 
     bool handler_called { false };

+ 9 - 1
Userland/Libraries/LibJS/Bytecode/Interpreter.cpp

@@ -246,7 +246,7 @@ void Interpreter::run_bytecode()
                 enter_unwind_context();
                 m_current_block = &static_cast<Op::EnterUnwindContext const&>(instruction).entry_point().block();
                 goto start;
-            case Instruction::Type::ContinuePendingUnwind:
+            case Instruction::Type::ContinuePendingUnwind: {
                 if (auto exception = reg(Register::exception()); !exception.is_empty()) {
                     result = throw_completion(exception);
                     break;
@@ -255,14 +255,20 @@ void Interpreter::run_bytecode()
                     do_return(saved_return_value());
                     break;
                 }
+                auto const* old_scheduled_jump = call_frame().previously_scheduled_jumps.take_last();
                 if (m_scheduled_jump) {
                     // FIXME: If we `break` or `continue` in the finally, we need to clear
                     //        this field
+                    //        Same goes for popping an old_scheduled_jump form the stack
                     m_current_block = exchange(m_scheduled_jump, nullptr);
                 } else {
                     m_current_block = &static_cast<Op::ContinuePendingUnwind const&>(instruction).resume_target().block();
+                    // set the scheduled jump to the old value if we continue
+                    // where we left it
+                    m_scheduled_jump = old_scheduled_jump;
                 }
                 goto start;
+            }
             case Instruction::Type::ScheduleJump: {
                 m_scheduled_jump = &static_cast<Op::ScheduleJump const&>(instruction).target().block();
                 auto const* finalizer = m_current_block->finalizer();
@@ -418,6 +424,8 @@ void Interpreter::enter_unwind_context()
     unwind_contexts().empend(
         m_current_executable,
         vm().running_execution_context().lexical_environment);
+    call_frame().previously_scheduled_jumps.append(m_scheduled_jump);
+    m_scheduled_jump = nullptr;
 }
 
 void Interpreter::leave_unwind_context()

+ 1 - 0
Userland/Libraries/LibJS/Bytecode/Interpreter.h

@@ -32,6 +32,7 @@ struct CallFrame {
     Vector<Value> registers;
     Vector<GCPtr<Environment>> saved_lexical_environments;
     Vector<UnwindInfo> unwind_contexts;
+    Vector<BasicBlock const*> previously_scheduled_jumps;
 };
 
 class Interpreter {

+ 85 - 0
Userland/Libraries/LibJS/Tests/try-finally-break.js

@@ -361,3 +361,88 @@ test("Throw while breaking", () => {
 
     expect(executionOrder).toEqual([1, 2, 3]);
 });
+
+test("Throw while breaking with nested try-catch in finalizer", () => {
+    const executionOrder = [];
+    try {
+        for (const i = 1337; ; expect().fail("Jumped to for loop update block")) {
+            try {
+                executionOrder.push(1);
+                break;
+            } finally {
+                try {
+                    throw 1;
+                } catch {
+                    executionOrder.push(2);
+                }
+                executionOrder.push(3);
+            }
+            expect().fail("Jumped out of inner finally statement");
+        }
+    } finally {
+        executionOrder.push(4);
+    }
+    expect(() => {
+        i;
+    }).toThrowWithMessage(ReferenceError, "'i' is not defined");
+
+    expect(executionOrder).toEqual([1, 2, 3, 4]);
+});
+
+test("Throw while breaking with nested try-catch-finally in finalizer", () => {
+    const executionOrder = [];
+    try {
+        for (const i = 1337; ; expect().fail("Jumped to for loop update block")) {
+            try {
+                executionOrder.push(1);
+                break;
+            } finally {
+                try {
+                    executionOrder.push(2);
+                } catch {
+                    expect.fail("Entered catch");
+                } finally {
+                    executionOrder.push(3);
+                }
+                executionOrder.push(4);
+            }
+            expect().fail("Jumped out of inner finally statement");
+        }
+    } finally {
+        executionOrder.push(5);
+    }
+    expect(() => {
+        i;
+    }).toThrowWithMessage(ReferenceError, "'i' is not defined");
+
+    expect(executionOrder).toEqual([1, 2, 3, 4, 5]);
+});
+
+test("Throw while breaking with nested try-catch-finally with throw in finalizer", () => {
+    const executionOrder = [];
+    try {
+        for (const i = 1337; ; expect().fail("Jumped to for loop update block")) {
+            try {
+                executionOrder.push(1);
+                break;
+            } finally {
+                try {
+                    throw 1;
+                } catch {
+                    executionOrder.push(2);
+                } finally {
+                    executionOrder.push(3);
+                }
+                executionOrder.push(4);
+            }
+            expect().fail("Jumped out of inner finally statement");
+        }
+    } finally {
+        executionOrder.push(5);
+    }
+    expect(() => {
+        i;
+    }).toThrowWithMessage(ReferenceError, "'i' is not defined");
+
+    expect(executionOrder).toEqual([1, 2, 3, 4, 5]);
+});