Skip to content
Merged
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ All notable changes to EigenScript are documented here.
(the one legitimate empty-expression position) still yields `null` (#494).

### Fixed
- **JIT: task code now stays interpreted at EVERY entry point (#533).** The
#408 ruling ("task code runs interpreted") was enforced only at the
fresh-entry gate: the OSR back-edge counter/handoff and the OP_CALL /
OP_DISPATCH thunk entries weren't task-gated, so a task's hot loop was
JIT-compiled MID-TASK after ~OSR-threshold iterations — and
`jit_helper_call` has no suspend check, so a blocking `task_recv`'s
placeholder null flowed onward as the received message and the leaked
suspend flag suspended the task mid-expression at a later call site.
Symptom: mass task death / `null` from `task_recv` / corrupted locals
after ~5000 loop iterations (first seen as liferaft's node tasks dying en
masse under chaos — reachable at all only once #530 lifted the lifetime
spawn cap). All four entry points now check `g_task_sched`;
regression-pinned with an OSR-threshold-crossing recv loop
(`tests/test_task_osr.eigs`, red pre-fix / green post-fix).
- **`dict_remove` no longer inflates the dict's hash table exponentially.**
The re-index rebuild after a removal reused the grow path's blind
capacity-doubling, so N removes on one dict grew its table by 2^N —
Expand Down
27 changes: 20 additions & 7 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,7 @@ int jit_helper_call(EigsChunk *caller_chunk, int argc, int resume_off) {
!g_trace_hist && vm_leaf_accessor_exec(fn_chunk, argc))
return 0;
if (fn_chunk->jit_state != 2 || !fn_chunk->jit_code) return 1;
if (g_task_sched) return 1; /* #533: task code runs interpreted */
if (g_vm.frame_count >= VM_FRAMES_MAX) return 1;
if (g_native_call_depth >= JIT_NATIVE_CALL_DEPTH_MAX) return 1;

Expand Down Expand Up @@ -2913,8 +2914,16 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) {
* per-chunk JIT state. Compilation is already gated off under MT
* (#296); skip the counter and the OSR slot scan/execute too while
* multithreaded so parallel workers running the same chunk don't race
* on back_edge_count / jit_osr (they interpret hot loops instead). */
if (!g_vm_multithreaded) {
* on back_edge_count / jit_osr (they interpret hot loops instead).
* #533: ALSO skip while a cooperative task scheduler is active — the
* #408 ruling is that task code runs interpreted, but only the
* fresh-entry gate enforced it. A task loop crossing the OSR
* threshold was JIT'd MID-TASK, and jit_helper_call has no suspend
* check: a blocking task_recv's placeholder null flowed on as the
* received message and the leaked suspend flag fired at a random
* later call site — state corruption after ~OSR-threshold
* iterations (first seen as liferaft node tasks dying en masse). */
if (!g_vm_multithreaded && !g_task_sched) {
chunk->back_edge_count++;

/* OSR trigger. For "called once, loops a lot" chunks (the gauntlet
Expand Down Expand Up @@ -3295,8 +3304,12 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) {
* the caller's stack. Resync to the (now-current) caller
* frame or, if we've fallen below base_frame, hand the
* result back to C. */
if (fn_chunk->jit_state == 0) jit_try_compile_chunk(fn_chunk);
if (fn_chunk->jit_code) {
if (fn_chunk->jit_state == 0 && !g_task_sched) jit_try_compile_chunk(fn_chunk);
/* #533: never enter a thunk while the task scheduler is active,
* even one compiled before the first spawn — task code runs
* interpreted (suspension points exist only in the interpreter
* loop's builtin-call check). */
if (fn_chunk->jit_code && !g_task_sched) {
((JitChunkFn)fn_chunk->jit_code)();
if (fn_chunk->jit_advance == -1) {
/* jit_helper_return popped fn_chunk's frame but left
Expand Down Expand Up @@ -5043,9 +5056,9 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) {
frame->call_argc = 1;
fn_chunk->exec_count++;

/* JIT hook (mirror of OP_CALL bytecode-fn path). */
if (fn_chunk->jit_state == 0) jit_try_compile_chunk(fn_chunk);
if (fn_chunk->jit_code) {
/* JIT hook (mirror of OP_CALL bytecode-fn path; #533 task gate). */
if (fn_chunk->jit_state == 0 && !g_task_sched) jit_try_compile_chunk(fn_chunk);
if (fn_chunk->jit_code && !g_task_sched) {
((JitChunkFn)fn_chunk->jit_code)();
if (fn_chunk->jit_advance == -1) {
/* Stage 4s: OP_RETURN sentinel — see OP_CALL hook.
Expand Down
4 changes: 4 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2173,6 +2173,10 @@ check_eigs_suite "worker arena return deep-copied before detach" test_spawn_aren
# #408 cooperative task layer: spawn/alive/yield/join/deadlock + leak-clean
# teardown of suspended/killed tasks (incl. heap-on-saved-stack + arena-dier).
check_eigs_suite "cooperative tasks: yield/join/deadlock/teardown (#408)" test_tasks.eigs "All tests passed" 1
# #533: task loops must stay interpreted past the OSR threshold (lowered here
# so the recv loop crosses it fast) — a mid-task OSR compile made task_recv
# return its placeholder null and corrupted the task at the next call site.
EIGS_JIT_OSR_THRESHOLD=20 check_eigs_suite "task loops stay interpreted past the OSR threshold (#533)" test_task_osr.eigs "task-osr: all passed" 1

# lib/sync — cooperative-task lock gives mutual exclusion across yield points
# (#488): unlocked non-atomic RMW loses updates, the lock closes the race,
Expand Down
30 changes: 30 additions & 0 deletions tests/test_task_osr.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# #533: a task's hot loop must NOT be OSR-compiled mid-task — task code runs
# interpreted (the #408 ruling). Pre-fix, once a recv-loop crossed the OSR
# threshold it was JIT'd mid-task and the next blocking task_recv went through
# jit_helper_call (which has no suspend check): the placeholder null flowed
# onward as the "received" message and the leaked suspend flag corrupted the
# task at a later call site. The suite runs this with EIGS_JIT_OSR_THRESHOLD=20
# so the loops cross the threshold quickly: every received message must still
# be a real string, all 200 of them.
define worker() as:
local n is 0
loop while 1:
local m is task_recv of null
if m == "stop":
return n
if (type of m) != "str":
return 0 - 1
n is n + 1

w is task_spawn of worker
i is 0
loop while i < 200:
task_send of [w, f"msg{i}"]
task_yield of null
i is i + 1
task_send of [w, "stop"]
got is task_join of w
if got == 200:
print of "task-osr: all passed"
else:
print of f"task-osr: FAILED got={got}"
Loading