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

### Fixed
- **Same-instant sleeper wake order no longer depends on handle IDs (#535).**
`sched_wake_sleepers` woke due sleepers in ascending handle-id order, but
ids come from a rotating next-fit cursor — so once slots recycle (#530),
the tie-break encoded the process's whole allocation history and two
identical seeded runs in one process could interleave differently
(surfaced by liferaft's in-sweep fault verify failing to reproduce
standalone). Tasks now carry a monotonic `spawn_seq` and same-instant
wakes order by it — a pure function of the run. Regression-pinned with a
wraparound-straddling sleeper race (`tests/test_task_sleep_order.eigs`).
- **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 /
Expand Down
31 changes: 24 additions & 7 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -5191,6 +5191,7 @@ typedef struct {
int active; /* armed on first spawn */
int dead_letters; /* inc 2: sends to finished/unknown tasks */
int detached_err_count; /* #530: reaped detached tasks that died unobserved (#493 gate) */
uint64_t spawn_counter; /* #535: monotonically increasing; stamps Task.spawn_seq */
double now; /* inc 3: virtual clock (logical, starts 0) */
int seeded; /* inc 4: 1 once task_sched_seed installs a seed */
uint64_t rng_state; /* inc 4: splitmix64 state for the seeded pick */
Expand Down Expand Up @@ -5382,6 +5383,12 @@ void task_sched_on_spawn(int id) {
TaskScheduler *s = sched_ensure();
s->active = 1;
s->live++;
/* #535: stamp spawn order. Handle IDs come from a rotating cursor, so
* they encode the process's WHOLE allocation history; any id-ordered
* tie-break makes the interleaving history-dependent. Spawn order is a
* pure function of the run. Main is 0 (spawn_counter starts at 1). */
Task *t = sched_lookup(s, id);
if (t) t->spawn_seq = ++s->spawn_counter;
sched_ready_push(s, id);
}

Expand Down Expand Up @@ -5510,18 +5517,28 @@ static int sched_wake_sleepers(TaskScheduler *s) {
}
if (!have_best) return 0;
s->now = best;
/* Wake main first, then tasks in ascending id order — a fixed tie-break. */
/* Wake main first, then tasks in ascending SPAWN order (#535) — NOT id
* order: ids come from a rotating next-fit cursor, so id order encodes
* the process's whole allocation history and two identical seeded runs
* in one process could interleave differently once slots recycle
* (surfaced by liferaft's in-sweep fault verify failing to reproduce
* standalone). Spawn order is a pure function of the run itself. */
if (s->main_task.state == TASK_SUSPENDED && s->main_task.sleeping &&
s->main_task.wake_at <= s->now) {
s->main_task.sleeping = 0;
sched_ready_push(s, 0);
}
for (int i = 1; i < HANDLE_TABLE_SIZE; i++) {
Task *t = (Task *)handle_lookup(i, HANDLE_TASK);
if (t && t->state == TASK_SUSPENDED && t->sleeping && t->wake_at <= s->now) {
t->sleeping = 0;
sched_ready_push(s, t->id);
}
for (;;) {
Task *next = NULL;
for (int i = 1; i < HANDLE_TABLE_SIZE; i++) {
Task *t = (Task *)handle_lookup(i, HANDLE_TASK);
if (t && t->state == TASK_SUSPENDED && t->sleeping && t->wake_at <= s->now &&
(!next || t->spawn_seq < next->spawn_seq))
next = t;
}
if (!next) break;
next->sleeping = 0;
sched_ready_push(s, next->id);
}
return 1;
}
Expand Down
4 changes: 4 additions & 0 deletions src/vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ typedef struct Task {
* handle slot released) the moment it finishes or is killed, instead of
* lingering joinable until process exit. */
int detached;
/* #535: monotonic spawn stamp (scheduler counter; main is 0). The
* same-instant sleeper-wake tie-break orders by THIS, never by handle
* id — ids come from a rotating cursor and encode allocation history. */
uint64_t spawn_seq;
} Task;

/* Free a Task's held refs and the struct. Safe on any state; called by
Expand Down
1 change: 1 addition & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2177,6 +2177,7 @@ check_eigs_suite "cooperative tasks: yield/join/deadlock/teardown (#408)" test_t
# 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
check_eigs_suite "sleeper wake order is allocation-history-independent (#535)" test_task_sleep_order.eigs "sleeper-order: 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
41 changes: 41 additions & 0 deletions tests/test_task_sleep_order.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# #535: same-instant sleeper wake order must be a pure function of the run,
# not of handle-id allocation history. Ids come from a rotating next-fit
# cursor, so after enough churn a batch of spawns straddles the table
# wraparound and id order diverges from spawn order. Pre-fix the wake
# tie-break was ascending handle id — the SAME race observed a different
# order depending on how much allocation history preceded it. Post-fix the
# tie-break is spawn order: both observations must be identical.
g_order is []
define racer(tagout) as:
task_sleep of 1
append of [g_order, tagout]

define run_race() as:
g_order is []
local a is task_spawn of [racer, "a"]
task_detach of a
local b is task_spawn of [racer, "b"]
task_detach of b
local c is task_spawn of [racer, "c"]
task_detach of c
task_sleep of 2
return f"{g_order[0]}{g_order[1]}{g_order[2]}"

define churn_w() as:
return 1

first is run_race of null
# March the slot cursor to just before the 255-slot wrap so the second
# race's three racers get ids like (254, 255, 1) — id order then disagrees
# with spawn order. 250 = 253 - the 3 slots the first race consumed.
n is 0
loop while n < 250:
t is task_spawn of churn_w
task_detach of t
task_yield of null
n is n + 1
second is run_race of null
if first == second:
print of f"sleeper-order: all passed ({first})"
else:
print of f"sleeper-order: FAILED first={first} second={second}"
Loading