Skip to content

feat(tasks): #408 increment 1a — task_spawn + task_alive foundation#472

Merged
InauguralPhysicist merged 1 commit into
mainfrom
feat-408-tasks-inc1a
Jul 7, 2026
Merged

feat(tasks): #408 increment 1a — task_spawn + task_alive foundation#472
InauguralPhysicist merged 1 commit into
mainfrom
feat-408-tasks-inc1a

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Refs #408 (increment 1a — the layer is multi-increment; this is the foundation, not the whole feature).

What

The lowest-risk foundational slice of the deterministic cooperative task layer: the Task struct, HANDLE_TASK, task_spawn/task_alive, and leak-clean teardown. No suspend machinery yet — the copying-stack scheduler that runs and interleaves tasks (the vm_run suspend/resume surgery, task_yield/task_join) is increment 1b, deliberately split out as focused core-VM work. This slice validates the table, args deep-copy, held-ref lifetime, and teardown independently first.

Full design trail on the issue: two design comments, two adversarially-verified prior-art research rounds, a code-grounded memory-design pass, and the implementation blueprint.

Design decisions realized here

  • Single OS thread — never flips g_vm_multithreaded, so the JIT and non-atomic refcount fast paths stay live (the whole point of the cooperative model vs spawn's Data races: parallel workers executing the same shared chunk (exec_count, JIT counters, inline caches, shared-env refcounts) #297 cliff).
  • Share-nothing boundarytask_spawn deep-copies args via val_clone_for_send, unlike spawn's by-reference threads.
  • HANDLE_TASK reuses the id-keyed handle table + handle_table_drain — the same deterministic-teardown path channels/threads already use.
  • No trace records — scheduling order is a pure function of program order, not host nondeterminism (the deterministic-by-construction ruling), so the builtins are deliberately not TRACE_NONDET_RET-wrapped. Documented so a reviewer doesn't "fix" the missing wrap.
  • GC — held refs need no root registration. Reading the collector showed it's trial-deletion (Bacon–Rajan), not mark-from-roots: a counted ref keeps its object live because it exceeds the internal-edge count within the candidate set U, exactly as ThreadHandle->fn does. (This finding also de-risks 1b — save-buffer refs auto-protect if ownership moves with the memcpy'd bytes.)

Verification

  • Release 2610/2610; ASan+UBSan detect_leaks=1 2610/2610, leak tally 0
  • New test_tasks.eigs: spawn returns an id, alive/unknown-id, args form, distinct ids, non-callable → type_mismatch, compound-arg hold — and the ASan half proves an unrun task holding a closure cycle + list args is reclaimed clean at teardown
  • doc-drift + freestanding (EigenOS profile) green; task_spawn/task_alive documented in BUILTINS.md (the CI gate: every builtin and public stdlib function must be documented (regex_* currently appear nowhere) #393 discoverability gate)

Not in this PR (increment 1b)

The vm_run vm_suspend_halt label + vm_run_resume re-entry + scheduler_drive above vm_execute + real task_yield/task_join + the two planted-fault validations (arena guard, save-buffer lifetime). Blueprint is on the issue.

🤖 Generated with Claude Code

The foundational, lowest-risk slice of the deterministic cooperative task
layer (design + prior-art research + memory pass all on issue #408): the
Task struct, HANDLE_TASK, task_spawn/task_alive, and leak-clean teardown.
The copying-stack scheduler that actually runs and interleaves tasks — the
vm_run suspend/resume surgery, task_yield/task_join — is the next
increment (1b); this slice validates the table, args deep-copy, held-ref
lifetime, and teardown independently before the delicate core-VM work.

- Task (vm.h): full struct incl. the copying-stack save-buffer fields
  (unused until 1b) so 1b doesn't reshape it. TaskState enum.
- HANDLE_TASK reuses the id-keyed handle table + handle_table_drain, the
  same deterministic-teardown path channels/threads use.
- task_spawn: deep-copies args via val_clone_for_send (share-nothing at the
  boundary, unlike spawn's by-reference threads), holds the entry closure,
  returns a numeric task id. Non-callable target → type_mismatch (#406).
- task_alive: 1 while READY/RUNNING/SUSPENDED, 0 on DONE/DEAD/unknown id.
- Single OS thread: never flips g_vm_multithreaded, so the JIT and the
  non-atomic refcount fast paths stay live (the #297-cliff-avoidance the
  cooperative model is for).
- GC: held refs need NO root registration — the trial-deletion collector
  (Bacon-Rajan) already keeps them live because a counted ref exceeds the
  internal-edge count within its candidate set U, exactly as ThreadHandle
  ->fn does. (This also de-risks 1b: save-buffer refs auto-protect if
  ownership moves with the memcpy'd bytes.)
- No trace records: scheduling order is a pure function of program order,
  not host nondeterminism (the deterministic-by-construction ruling) — so
  the builtins are deliberately NOT TRACE_NONDET_RET-wrapped.

Gates: release 2610/2610, ASan+UBSan detect_leaks=1 2610/2610 tally 0
(new test_tasks.eigs exercises unrun tasks holding a closure cycle + list
args → reclaimed clean at teardown), doc-drift + freestanding green.

Refs #408

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@InauguralPhysicist InauguralPhysicist merged commit d5300e9 into main Jul 7, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the feat-408-tasks-inc1a branch July 7, 2026 21:04
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