Skip to content

feat(tasks): #408 increment 3 — virtual time (task_sleep/task_now)#482

Merged
InauguralPhysicist merged 1 commit into
mainfrom
tasks-408-virtual-time
Jul 8, 2026
Merged

feat(tasks): #408 increment 3 — virtual time (task_sleep/task_now)#482
InauguralPhysicist merged 1 commit into
mainfrom
tasks-408-virtual-time

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

What

Adds a logical virtual clock to the cooperative task scheduler — increment 3 of #408.

  • 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). 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

  • Sleepers waiting on the clock are no longer mistaken for a deadlock — the trampoline advances the clock (sched_wake_sleepers) before deciding anything is stuck. A genuine deadlock (no sleepers) still errors loudly.
  • 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 re-enqueue guard, so a main that sleeps (or recvs) first is not wrongly re-readied.

Follows the eigenscript-extend-vm checklist: new Task fields (sleeping/wake_at, POD), two deterministic builtins registered, no opcode/AST/CallFrame/JIT-helper sites. task_sleep/task_now are not nondeterministic — they read a logical counter, so no TRACE_NONDET wrapping.

Gates

  • Release suite 2617/2617.
  • ASan (detect_leaks=1) 2617/2617, leak tally 0.
  • New gate: virtual time is deterministic across two fresh processes, replays byte-identically under 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 in tests/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

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>
@InauguralPhysicist InauguralPhysicist merged commit da77e8c into main Jul 8, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the tasks-408-virtual-time branch July 8, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant