feat(tasks): #408 increment 3 — virtual time (task_sleep/task_now)#482
Merged
Conversation
Add a logical virtual clock to the cooperative task scheduler. - `task_sleep of ticks` suspends the current task until the clock reaches now + ticks; `task_now of null` reads the clock. The clock starts at 0 and only ever jumps FORWARD, to the earliest sleeper, when nothing else is runnable (discrete-event simulation) — so a program that sleeps runs in zero real time and stays deterministic by construction, exactly like the rest of the task layer. No wall clock, no tape records. - Sleepers waiting on the clock are no longer mistaken for a deadlock: the trampoline advances the clock (sched_wake_sleepers) before it decides anything is stuck. Ties at the same wake time break by ascending task id (main = 0 first), keeping the interleaving deterministic. - A negative sleep clamps to 0; `task_sleep of 0` is a same-tick yield; with no task ever spawned it is a no-op, like `task_yield`. The initial vm_execute suspension guard is aligned with the trampoline's re-enqueue guard so a main that sleeps (or recvs) first is not wrongly re-readied. This is the election-timeout / heartbeat primitive the deterministic Raft simulator (liferaft) needs. Not yet: the pluggable seeded-strategy hook for the DST consumer. Gates: release 2617/2617 and ASan (detect_leaks=1) 2617/2617, leak tally 0. New gate: task_sleep/task_now is deterministic across two processes, replays byte-identically, and records ZERO tape `N` records (the clock is not a nondeterministic source). SPEC "Virtual time" section + BUILTINS + examples/task_virtual_time.eigs + 12 new assertions in test_tasks.eigs. Refs #408. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a logical virtual clock to the cooperative task scheduler — increment 3 of #408.
task_sleep of tickssuspends the current task until the clock reachesnow + ticks.task_now of nullreads the clock.The clock starts at 0 and only ever jumps forward, to the earliest sleeper, when nothing else is runnable (discrete-event simulation). A program that sleeps therefore runs in zero real time and stays deterministic by construction — no wall clock, no tape records — exactly like the rest of the task layer.
Why
This is the election-timeout / heartbeat primitive the deterministic Raft simulator (liferaft) needs to migrate onto the upstream task layer (the #408 differential-proof step). Timeouts and periodic intervals are inherently time-based; without a virtual clock a DST would have to hand-roll one (which liferaft currently does).
Behavior details
sched_wake_sleepers) before deciding anything is stuck. A genuine deadlock (no sleepers) still errors loudly.task_sleep of 0is a same-tick yield; with no task ever spawned it is a no-op, liketask_yield.vm_executesuspension guard is aligned with the trampoline re-enqueue guard, so amainthat sleeps (or recvs) first is not wrongly re-readied.Follows the
eigenscript-extend-vmchecklist: new Task fields (sleeping/wake_at, POD), two deterministic builtins registered, no opcode/AST/CallFrame/JIT-helper sites.task_sleep/task_noware not nondeterministic — they read a logical counter, so noTRACE_NONDETwrapping.Gates
detect_leaks=1) 2617/2617, leak tally 0.EIGS_TRACE/EIGS_REPLAY, and records zero tape `N` records (a positive assertion that the clock leaks no nondeterminism).docs/SPEC.md"Virtual time" section (byte-for-byte doc-example test),docs/BUILTINS.md,examples/task_virtual_time.eigs(a watchdog/timeout race), and 12 new assertions intests/test_tasks.eigs.Noted (pre-existing, not in this diff)
While adversarially probing, I confirmed that a program which deadlocks (or otherwise dies of an uncaught error with live tasks) leaks the
TaskScheduler+ suspended-task slices at its fatal exit — a process-exit leak, not steady-state, on a path no test reaches (so the leak tally is unaffected). This is pre-existing: the deadlock path allocates nothing new under this change. Filing separately rather than scope-creeping this increment.Refs #408.
🤖 Generated with Claude Code