Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/backends/interpreter/builtins/concurrency.c
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,9 @@ Value builtin_join(Value *args, int num_args, ExecutionContext *ctx) {
// Check if task threw an exception
if (task->ctx->exception_state.is_throwing) {
// Re-throw the exception in the current context
// Retain the exception value since both task and caller will release it
ctx->exception_state = task->ctx->exception_state;
VALUE_RETAIN(ctx->exception_state.exception_value);
pthread_mutex_unlock((pthread_mutex_t*)task->task_mutex);
return val_null();
}
Expand Down
8 changes: 8 additions & 0 deletions src/backends/interpreter/runtime/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,9 @@ void defer_stack_execute(DeferStack *stack, ExecutionContext *ctx) {
int was_throwing = ctx->exception_state.is_throwing;
Value saved_exception = ctx->exception_state.exception_value;

// Retain saved exception to prevent it from being freed during defer execution
VALUE_RETAIN(saved_exception);

// Temporarily clear exception state to allow defer to run
ctx->exception_state.is_throwing = 0;

Expand All @@ -458,6 +461,11 @@ void defer_stack_execute(DeferStack *stack, ExecutionContext *ctx) {
if (!ctx->exception_state.is_throwing) {
ctx->exception_state.is_throwing = was_throwing;
ctx->exception_state.exception_value = saved_exception;
// Release our extra reference since we're restoring the value
VALUE_RELEASE(saved_exception);
} else {
// Defer threw a new exception - release saved exception since it won't be used
VALUE_RELEASE(saved_exception);
}

// Clean up the deferred expression and release environment
Expand Down
11 changes: 11 additions & 0 deletions src/backends/interpreter/runtime/statements.c
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,10 @@ void eval_stmt(Stmt *stmt, Environment *env, ExecutionContext *ctx) {
int was_breaking = ctx->loop_state.is_breaking;
int was_continuing = ctx->loop_state.is_continuing;

// Retain saved values to prevent them from being freed while we execute finally
VALUE_RETAIN(saved_return);
VALUE_RETAIN(saved_exception);

// Clear states before finally
ctx->return_state.is_returning = 0;
ctx->exception_state.is_throwing = 0;
Expand All @@ -749,6 +753,13 @@ void eval_stmt(Stmt *stmt, Environment *env, ExecutionContext *ctx) {
ctx->exception_state.exception_value = saved_exception;
ctx->loop_state.is_breaking = was_breaking;
ctx->loop_state.is_continuing = was_continuing;
// Release our extra references since we're restoring the values
VALUE_RELEASE(saved_return);
VALUE_RELEASE(saved_exception);
} else {
// Finally changed control flow - release saved values since they won't be used
VALUE_RELEASE(saved_return);
VALUE_RELEASE(saved_exception);
}
}
break;
Expand Down
20 changes: 20 additions & 0 deletions tests/memory/defer_exception_leak.hml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Test for memory leak when defer throws while exception is pending
// Previously, the original exception would leak when defer threw

fn throw_error() {
throw "defer exception";
}

fn test_defer_throws() {
defer throw_error();
throw "original exception";
}

// Test the scenario
try {
test_defer_throws();
} catch (e) {
print("Caught: " + e);
}

print("Test passed - no memory leak");
19 changes: 19 additions & 0 deletions tests/memory/finally_exception_leak.hml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Test for memory leak when finally block throws a new exception
// Previously, the original exception would leak when finally threw

fn test_finally_throws() {
try {
throw "original exception";
} finally {
throw "finally exception";
}
}

// Test the scenario
try {
test_finally_throws();
} catch (e) {
print("Caught: " + e);
}

print("Test passed - no memory leak");
16 changes: 16 additions & 0 deletions tests/memory/join_exception_refcount.hml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Test for refcount mismatch when joining a task that threw an exception
// Previously, the exception value was copied without retaining

async fn throws_error() {
throw "task exception";
}

// Test the scenario
try {
let task = spawn(throws_error);
join(task);
} catch (e) {
print("Caught: " + e);
}

print("Test passed - no refcount mismatch");
Loading