(single-thread → multi-core → distributed) with deterministic testing, trace replay, and formalizable semantics
North Star: inside Asupersync’s capability boundary, every concurrent program has (1) a well‑founded ownership tree, (2) explicit cancellation driven to quiescence, (3) deterministic resource cleanup, and (4) compositional building blocks whose semantics are lawful and testable under a deterministic lab runtime.
This is a blank‑slate design: Asupersync owns the scheduler, cancellation protocol, region model, and (optionally) the I/O reactor. It is built only on Rust’s stable async/await and core::future::Future + std::task::{Waker, RawWaker}.
Asupersync is not “an executor plus helpers.” It is a semantic platform:
- A small kernel of primitives with a precise operational semantics.
- A capability/effect boundary (
Cx) that prevents ambient authority and makes determinism + distribution real. - Structured concurrency by construction (tasks are owned; regions close to quiescence).
- Cancellation as a protocol: request → drain → finalize (idempotent, budgeted, schedulable).
- Two‑phase effects by default where cancellation can otherwise lose data: reserve/commit + ack tokens + leases.
- Obligation tracking (permits/acks/leaves/finalizers) so “futurelock” and effect leaks become detectable and testable.
- A lab runtime with virtual time and deterministic scheduling (plus DPOR‑class exploration hooks) so concurrency bugs become testable.
- A distributed story that is honest: named computations + leases + idempotency + sagas, not “serialize closures across machines.”
If you use Asupersync primitives, you get cancel correctness, no orphan tasks, bounded cleanup, predictable shutdown, and replayable traces.
- No orphaned work: all spawned work is owned by a region; region close guarantees quiescence.
- Cancellation you can reason about: explicit request with downward propagation, upward outcome reporting, and bounded cleanup.
- No silent data loss under cancellation for library primitives (channels, streams, queues, RPC handles).
- Local reasoning: inside a region, “when I leave this block, nothing from it is still running, and its resources are closed.”
- Composable concurrency: join/race/timeout/hedge/quorum/pipeline/retry are safe and interoperable.
- Performance + scalability: zero-cost where possible; predictable overhead where not.
- Distributed orchestration: remote tasks behave like local tasks semantically, with explicit leases + idempotency.
- Deterministic testability: virtual time, deterministic scheduling, trace replay, schedule exploration hooks.
- Not a full web framework.
- Not “exactly once” distributed execution (we provide idempotency + leases; exactly once is a system property).
- Not magical cancellation for arbitrary user futures: non‑cooperative futures can still stall. We define escalation boundaries explicitly.
- Not a compiler feature: we do not require language changes (but we can optionally integrate nightly features).
Rust today cannot safely support “spawn arbitrary borrowing future onto an arbitrary worker thread” without restrictions. Asupersync encodes this honestly via tiers:
- Fibers: borrow-friendly, region‑pinned, same-thread execution.
- Tasks: parallel,
Send, migrate across worker threads; captures must beSend+ valid for region lifetime via region heap. - Actors: long-lived supervised tasks (still region-owned; no “detached by default”).
- Remote tasks: named computations executed elsewhere with leases + idempotency.
This tiering is the mechanism that makes “best ever” credible instead of hand‑wavy.
- Region: the unit of structured concurrency. Owns tasks/fibers/actors/resources/finalizers; closes to quiescence.
- Scope: the typed user handle to a region (
Scope<'r, …>). - Quiescence: no live children + all registered finalizers have run to completion (or escalated per policy).
- Outcome: terminal result of a task/region: Ok / Err / Cancelled / Panicked.
- Cancellation checkpoint: an explicit observation point where cancellation can take effect.
- Masking: temporarily defer responding to cancellation (bounded by budgets).
- Obligation: a linear “must be resolved” token (permit/ack/lease/finalizer handle). Dropping an obligation has defined semantics (abort/nack) and is detectable in lab mode.
- Two-phase effect: reserve (cancel-safe) then commit (linear, bounded masking).
- Lab runtime: deterministic scheduler + virtual time + trace capture + replay.
This section is not decoration. Every algebraic claim corresponds to a real engineering win: fewer surprises, more optimization freedom, and test oracles.
We model terminal states with a four-valued outcome:
Ok(V) < Err(E) < Cancelled(R) < Panicked(P)
This is a default severity order used for:
- region outcome aggregation defaults,
- supervision escalation defaults,
- trace summarization.
Policies may override combination behavior, but must remain monotone: “worse” states cannot become “better” when aggregated.
Two core combinators:
⊗= Join: run both, wait both, combine outcomes under policy.⊕= Race: first terminal outcome wins; losers are cancelled and drained.
We treat laws as semantic (observational equivalence under the spec), not “Rust tuple ordering.”
-
Associativity:
(a ⊗ b) ⊗ c ≃ a ⊗ (b ⊗ c)(a ⊕ b) ⊕ c ≃ a ⊕ (b ⊕ c)
-
Identities:
a ⊗ ⊤ ≃ awhere⊤is “immediate unit”a ⊕ ⊥ ≃ awhere⊥is “never completes”
-
Cancellation correctness law:
- Race never abandons losers:
a ⊕ bmust (1) choose winner, (2) cancel losers, (3) drain losers, then (4) return winner.
- Race never abandons losers:
-
Commutativity:
a ⊗ b ≃ b ⊗ aif combining is commutative or treated as multiset.a ⊕ b ≃ b ⊕ awhen symmetric (same type, no schedule-sensitive side effects).
They enable:
- derived combinators with consistent semantics,
- DAG-level optimization when using the
planmodule, - deterministic test oracles.
Most runtimes specify concurrency via interleavings (total orders of steps). That is an implementation model, not the right mathematics. The right semantic object is a trace equivalence class:
- define an independence relation
Ion observable labels (e.g. “two steps commute because they touch disjoint regions/obligations”), - quotient schedules by adjacent swaps of independent actions (
…ab… ≃ …ba…when(a,b) ∈ I), - reason about programs up to this equivalence (a Mazurkiewicz trace / partial order), not up to a brittle single schedule.
Why this matters inside Asupersync:
- DPOR becomes semantics-preserving, not a heuristic: the lab runtime explores one representative per trace class (see §18).
- Trace replay becomes canonicalizable: you can normalize executions (Foata normal form / “parallel layers”) and compare runs robustly.
- Observational equivalence (
≃) becomes a real thing: many semiring laws are “true up to commuting independent work,” which is exactly what we want for lawful rewrites and plan optimization.
There is an even deeper view (useful later, not required day‑1): the space of executions forms a directed topological space; commutations become 2‑cells (squares), higher commutations become higher cubes, and schedule equivalence is directed homotopy (dihomotopy). This is the geometric backbone behind "don't explore the same concurrency twice."
Practical note: For finite discrete systems, Mazurkiewicz trace equivalence is the discrete version of dihomotopy equivalence—optimal DPOR already achieves the topological reduction. The d‑space perspective is a cleaner mathematical lens, not a more powerful algorithm.
Goal: define a canonical representative per trace class that reduces context switches while preserving observable meaning.
Normalization procedure (small model):
- Define an independence relation
Ion observable labels (same notion as DPOR). - Build a dependency DAG: for each pair
i < j, add edgei → jif(label_i, label_j) ∉ I. - Compute the Foata normal form by repeatedly taking all minimal elements (no incoming edges) as a "parallel layer".
- Linearize each layer with a stable order (task id, then original index). This yields a deterministic schedule.
- Optional refinement: define switch cost (# of task changes) and choose the minimum-switch linear extension. The geodesic is the shortest path in the "swap graph" of adjacent independent swaps.
Toy example (two tasks, independent steps):
- Task A steps:
a1, a2; Task B steps:b1, b2. - Raw schedule
S = a1 b1 a2 b2has 3 task switches. - Foata layers:
[a1 b1][a2 b2]. - Canonical linearization (stable by task id):
a1 a2 b1 b2has 1 switch. - Both schedules are trace-equivalent; normalization reduces switches without changing observable outcomes.
Comparison metrics: switch count, swap distance to normal form, and trace length (should be identical). Normalized traces should be shorter in "visual entropy" and more stable for diff/replay.
Tie-in to DPOR/Mazurkiewicz: adjacent swaps of independent actions are 2-cells; the space of schedules is a cubical complex. Geodesic normalization picks a shortest path in that complex. DPOR already picks one representative per trace class; this normalization makes that representative deterministic and human-readable for replay and debugging.
Goal: prioritize schedules that expose "essential holes" (ordering constraints, deadlock shapes) in the execution space.
Data required from executions:
- event structure / partial order with independence relation
I - happens-before edges and resource-acquire edges
- cancellation points (to identify truncated regions)
- schedule prefix lengths (for filtration)
Concrete proxy + filtration + scoring (spec):
Proxy complex (local commutation square complex):
- 0-cells are events in the prefix, indexed by sequence number.
- 1-cells are dependency edges from the trace poset (event structure) restricted to the prefix.
- 2-cells are commuting diamonds:
a→b,a→c,b→d,c→dwithb < canda < b,c < d. - This is the same local commutation proxy used by
src/trace/boundary.rs.
Filtration (exact):
- Parameter
tis prefix length (t = 1..n). K_tis the square complex built from the trace poset restricted to the firsttevents.- Monotone:
K_t ⊆ K_{t+1}by construction (we only add events/edges/squares astgrows).
Scoring (exact):
- Compute H1 persistence pairs over GF(2) for the filtered complexes
K_1..K_n. - Let each pair be
(birth, death)withdeath = n+1if unpaired. - Define
persistence = death - birth. - Score is a deterministic lexicographic tuple:
(long_lived, total_persistence, beta1_final, -n, fingerprint)- Rank by lexicographic descending order (higher is better).
long_lived= count of pairs withpersistence ≥ P_min(defaultP_min = 3).total_persistence= sum ofpersistencefor all pairs.beta1_final= H1 Betti number ofK_n.fingerprint= trace fingerprint (Foata/trace hash) as stable tie-break.
Performance bounds + fallback:
- Proxy size:
|V| = O(n),|E| = O(n·d),|S| = O(n²·d)wheredis max out-degree. - Persistence reduction is cubic in matrix size; cap by fixed limits:
MAX_VERTICES = 512,MAX_EDGES = 20_000,MAX_SQUARES = 200_000,MAX_MATRIX_BYTES = 64 MiB.- If any cap is exceeded, skip persistence and use fallback score:
fallback_score = (beta1_final, indep_density, -n, fingerprint)- Rank fallback score lexicographically descending.
indep_density = (# independent pairs in prefix) / (t·(t-1)/2)from the trace poset.
Toy example (classic deadlock shape):
- Two tasks acquire locks in opposite order:
A: L1 → L2,B: L2 → L1. - The independence relation admits interleavings that form a cycle in the wait-for graph.
- The cubical complex contains a 1-cycle corresponding to "either order leads to deadlock".
- The heuristic should prioritize the schedule prefix that creates the cycle early, surfacing the deadlock faster than uniform exploration.
Success metric: compared to uniform exploration, the heuristic reaches known deadlock or ordering bugs in fewer schedules (lower expected exploration count) in a deterministic lab benchmark.
Goal: conservatively flag code paths that may exit a scope while still holding unresolved obligations.
Abstract domain (flow-sensitive, may-analysis):
Held ⊆ ObligationKind— the set of obligation kinds that may be pending at a program point.⊔(join) is set union,⊥is empty set.
Transfer rules (core subset):
reserve(kind)⇒Held := Held ∪ {kind}commit(kind)/abort(kind)⇒Held := Held \\ {kind}- unknown call ⇒
Held := Held ∪ Summary(call)(conservative summary) - scope exit / function return: if
Heldnon-empty ⇒ emit warning.
Prototype scope (small model):
- Model only a restricted IR: a list of operations
{reserve, commit, abort, call, branch, loop}. - Provide summaries for a small set of functions that manipulate obligations (e.g., semaphore acquire/release, pool checkout/return).
- Diagnostics sorted by (file, line, obligation kind) for determinism.
Toy example:
reserve(permit)
if cond { commit(permit) }
return
- The analysis reports:
permitmay be leaked on thecond = falsebranch. - If both branches commit/abort, no warning is emitted.
Prototype deliverable: a deterministic checker that runs on a hand-written IR (or a tiny subset extracted from sync/ primitives), emits stable warnings, and is wired into CI as a non-flaky report (warning-only at first).
Tie-in: complements the dynamic ObligationLeakOracle by catching obvious leaks earlier, without compromising determinism.
Goal: make “no obligation leaks” a type error in an opt-in surface and encode budget usage as a resource grade.
Sketch (obligations):
Obligation<K, n>wherenis a type-level natural (how many unresolved obligations of kindKare held).reserve<K>() -> Obligation<K, 1>commit(ob: Obligation<K, 1>) -> Obligation<K, 0>abort(ob: Obligation<K, 1>) -> Obligation<K, 0>scoperequires allObligation<*, n>to ben = 0at exit.
Sketch (budgets):
Budget<d, q, c>where grades track deadline/quotas (or a single scalar for now).spend(b: Budget<d, q, c>, cost) -> Budget<d', q', c'>withd' ≤ d,q' ≤ q,c' ≤ c.fork/joinoperations require grades to satisfy the semiring laws (min on constraints, add on sequential cost).
Toy API (leak is untypeable):
fn safe_path() {
let permit: Obligation<Permit, 1> = reserve::<Permit>();
let _done: Obligation<Permit, 0> = commit(permit);
}
fn leak_path() {
let _permit: Obligation<Permit, 1> = reserve::<Permit>();
// no commit/abort => does not type-check at scope exit
}
Prototype plan: implement a tiny opt-in module using const generics or typenum (no runtime cost), and prove with compile-fail tests that leaking obligations is rejected. Extend later to encode budget grades.
Budgets propagate by “stricter wins”:
Budget = Deadline × PollQuota × CostQuota × TraceCtx × Priority
combine(parent, child) = componentwise_min(parent, child) // except priority: max
This gives automatic propagation for deadlines and quotas and makes “why did this cancel?” reasoning local.
The product budget above is an idempotent algebra (“tightening twice is the same as tightening once”). When we start doing planning (pipelines, DAGs, retries, hedges), we also need a second composition mode:
- Sequential composition accumulates time/cost (
+). - Constraint propagation tightens deadlines/quotas (
min/ meet).
This lands naturally in the world of tropical / idempotent semirings (e.g. (ℝ∪{∞}, min, +) for best‑case bounds, or (ℝ∪{∞}, max, +) for worst‑case critical paths).
Practical payoff:
- the
planmodule can compute critical paths, slack, and "where did my budget go?" explanations using shortest‑path style algorithms; - policies and governors can treat budgets as grades: a task is scheduled only when it can make progress without violating its grade (deadline/poll/cost).
Tropical matrix example: Budget propagation through a task tree is tropical matrix multiplication:
effective_budget[leaf] = min_{path root→leaf} Σ edge_costs
This is Floyd-Warshall in the tropical semiring (ℝ∪{∞}, min, +). Critical path = longest path in the (max, +) dual.
Every live task/fiber/actor is owned by exactly one region; regions form a rooted tree.
A region cannot finish until:
- all children reach terminal outcomes, and
- all registered finalizers have run (subject to budgets/escalation policy), and
- all in-flight obligations registered to the region are resolved.
Cancellation is request → drain → finalize, driven by the scheduler.
If cancellation can lose data, the safe pattern is the natural one.
Any combinator that stops awaiting a branch must still drive it to terminal (or escalate).
Every kernel primitive has a deterministic lab interpretation.
All effects flow through explicit capabilities (Cx).
- No hidden globals required for correctness.
- Effects require explicit capabilities.
- Deterministic substitution: swap
Cxto change interpretation (prod vs lab vs remote).
Treat the Cx surface as an effect signature (checkpoint, sleep, trace, reserve/commit, etc.) and each runtime (prod/lab/remote) as a handler.
The purpose is not academic purity; it is to make these facts precise:
- same user program, different interpretation (lab vs prod) without changing its meaning,
- explicit equational laws for optimization and testing.
Example laws we want to hold (up to observational equivalence):
trace(e1); trace(e2) ≃ trace(e2); trace(e1)whene1ande2are independent (different tasks/regions),checkpoint(); checkpoint() ≃ checkpoint()when no cancel is requested,sleep_until(t1); sleep_until(t2) ≃ sleep_until(max(t1,t2))in a model where sleeps only delay readiness.
- identity:
region_id(),task_id() - budgets:
budget(),now() - cancellation:
is_cancel_requested(),checkpoint(),with_cancel_mask() - scheduling:
yield_now() - timers:
sleep_until() - tracing:
trace(event)
FiberCap<'r>: spawn fibers, borrow'r, notSend.TaskCap<'r>: spawnSendtasks with region-safe storage.IoCap<'r>: submit I/O; binds in-flight ops to region quiescence.RemoteCap<'r>: remote named tasks with leases.SupervisorCap<'r>: supervised actors/restarts.
Open → Closing → Draining → Finalizing → Closed(outcome)
- Mark closing (spawns forbidden).
- If policy dictates or cancel requested, cancel remaining children.
- Drain children to terminal outcomes (cancel lane prioritized).
- Run finalizers (masked, budgeted).
- Resolve obligations (permits/acks/leases/in-flight I/O).
- Compute region outcome (policy-defined).
'rties handles to region lifetime.- Handles are affine; join consumes.
- Dropping handle does not detach work; region still owns task.
Asupersync’s invariants become dramatically easier to verify if we commit to a compositional logic in the docs:
- Separation logic / separation algebras: region resources (tasks, obligations, finalizers, in‑flight I/O) are owned, and ownership composes with
*(“disjoint union”).- The frame rule is the workhorse: proving one component doesn’t require re‑proving the whole world.
- Rely/Guarantee: every primitive states what it relies on (e.g. fairness assumptions, “parent won’t reclaim region heap while I run”) and what it guarantees (e.g. “I checkpoint at most every
kpolls,” “I resolve all obligations before completion”).
This is the right formal home for "Region close = quiescence" and "no obligation leaks" as compositional contracts, not folklore.
Separation logic assertion syntax:
region(r, s, B) region r in state s with budget B
task(t, r, S) task t owned by r in state S
obligation(o, k, t, r) obligation o of kind k held by t in region r
P * Q P and Q hold for disjoint resources
emp empty (no resources)
Example invariant: region(r, Open, B) * task(t, r, Running) * obligation(o, Permit, t, r) asserts disjoint ownership of region, task, and obligation.
Created
Running
CancelRequested { reason, budget }
Cancelling { deadline, poll_budget }
Finalizing { deadline, poll_budget }
Completed(outcome)
Multiple cancel requests merge: earlier deadline + stricter quotas + higher severity wins.
Primitives declare cancellation behavior:
- checkpointing
- masked (bounded)
- commit (linear token; bounded mask by construction)
Scheduler lanes:
- cancel
- timed (EDF-ish)
- ready
Modes:
- Soft: wait indefinitely (strict correctness).
- Bounded: after deadline/budget, abort-by-drop or panic (policy-controlled, trace-recorded).
Cancellation is not a flag; it is a protocol between:
- System (scheduler/runtime): wants quiescence within a budget.
- Task (user future): may cooperate, delay (mask), or stall.
Think of it as a two‑player game with bounded resources:
- System move:
request_cancel(reason, budget) - Task moves:
checkpoint,mask(k)(bounded),work - System wins iff the task reaches
Completed(Cancelled(_))(or other terminal) within the budget under fair scheduling.
Spec requirement (the real "math" promise): primitives must publish a cancellation responsiveness bound—at least "max polls between checkpoints" and "max masking depth." Budgets are then not vibes; they are sufficient conditions for the system to have a winning strategy (LaSalle/Lyapunov style arguments in §11.5 can mechanize this).
Theorem (Cancellation Completeness):
For any task with mask depth M and checkpoint interval C, if cleanup_budget ≥ M × C × poll_cost, then System wins (task reaches terminal state within budget under fair scheduling).
For deep reasoning and future tooling, it is useful to view cancellation as introducing a zero/annihilator into a computation’s interaction graph:
- “commit sections” are bounded feedback loops,
- cancellation forces certain paths to evaluate to
0(no further effect) unless masked, - obligations ensure that even when a branch is annihilated, its linear resources are conserved (aborted/nacked) rather than leaked.
You do not need this to implement Phase‑0, but it is a powerful conceptual model for bounding cleanup cost and proving "cancellation cannot silently drop linear effects."
Practical note: The full GoI formalism (nilpotent operators, trace in a *‑algebra) would require encoding programs as interaction nets—a research project. The operational approach (bounded masks + checkpoint contracts + the Completeness theorem in §7.6) provides equivalent static guarantees with far less machinery.
Linear tokens:
SendPermit<T>→sendorabortAck→commitornackLease→ renew or expireIoOp→ complete/cancel before close
Tracked in obligation registry; Drop is safe-by-default (abort/nack) and can be “panic-on-drop” in lab.
The obligation system is not “like” linear logic — it is a linear resource discipline:
- obligations live in a linear context
Δ(“must be used exactly once”) rather than the unrestricted contextΓ, reserveintroduces a linear resource (Δ := Δ, o),commit/abort/nack/expireeliminates it (Δ := Δ \\ {o}),- reaching the end of a scope with
Δ ≠ ∅is an obligation leak, i.e. a semantic error.
This same idea reappears in session types for communication:
- Sender protocol:
reserve → (send | abort) - Receiver protocol:
recv → (commit | nack)
We can start with runtime enforcement (obligation registry + lab checks) and later add stronger static structure (#[must_use], typestate, session-typed endpoints) without changing the meaning.
Session type notation for two-phase send:
S = !reserve.(?abort.end ⊕ !T.end)
R = dual(S) = ?reserve.(!abort.end ⊕ ?T.end)
Reading: Sender (S) outputs reserve, then either inputs abort and terminates, or outputs payload T and terminates. Receiver (R) is the dual. The ⊕ is internal choice (sender picks); the corresponding & in the dual is external choice (receiver follows).
let permit = tx.reserve(cx).await?;
permit.send(msg);
Drop permit => abort and release capacity; message not moved => no silent loss.
let (item, ack) = rx.recv_with_ack(cx).await?;
process(item, cx).await?;
ack.commit(); // drop => nack
Reserve slot + idempotency key; commit sends; cancel triggers best-effort cancel; lease bounds orphan work.
Entities
- Origin node: owns the region/handle; initiates spawn/cancel.
- Remote node: executes named computation; sends ack/result/lease renewals.
Message types (Phase 1+)
SpawnRequest { remote_task_id, computation, input, lease, idempotency_key, budget, origin_node, origin_region, origin_task }SpawnAck { remote_task_id, status: Accepted | Rejected(reason), assigned_node }CancelRequest { remote_task_id, reason, origin_node }ResultDelivery { remote_task_id, outcome, execution_time }LeaseRenewal { remote_task_id, new_lease, current_state, node }
Envelope + serialization (Phase 1+)
- All messages are carried inside an explicit envelope:
RemoteEnvelope { version, sender, sender_time, payload }sender_timeis a logical clock snapshot (vector clock or equivalent).
- Transport framing is transport-specific (length prefix; optional magic for stream resync).
- Serialization is canonical CBOR (RFC 8949):
- Deterministic map key ordering; no map-order dependence.
- Sets/collections must be encoded in deterministic order.
- JSON is allowed for debug/test vectors only (not the wire format).
- Unknown fields: ignored for forward compatibility; unknown variants: reject.
Versioning rules
major.minorversioning is carried inRemoteEnvelope.version:- Unknown major: reject the message and close transport.
- Unknown minor: accept if all required fields are present; ignore unknown fields.
- Any change to semantics of existing fields requires a major bump.
- New optional fields require a minor bump and must be ignorable.
Handshake + capability checks
- A transport-level handshake MUST occur before any
RemoteMessageis accepted:Hello { protocol_version, node_id, clock_mode, max_lease, idempotency_ttl, computation_registry_hash }HelloAck { accepted_version, clock_mode, assigned_node_id }
- Capability checks are mandatory:
SpawnRequestmust reference a registered computation name.- The remote node must validate authorization policy for
origin_node(ACL or capability token). budgetis clamped to remote policy caps;leaseis clamped to max lease.idempotency_keymust be unique per(computation, input_schema_hash)or rejected.
Test vectors (canonical examples)
SpawnRequest (new):
{ remote_task_id: 42, computation: "encode_block", input: "0xdeadbeef",
lease: 30s, idempotency_key: IK-0001, budget: {poll_quota: 1000},
origin_node: "node-a", origin_region: 7, origin_task: 9 }
Expect:
SpawnAck { remote_task_id: 42, status: Accepted, assigned_node: "node-b" }
ResultDelivery { remote_task_id: 42, outcome: Ok, execution_time: 5ms }
SpawnRequest (duplicate, same key + inputs):
same as above, re-sent
Expect:
SpawnAck { remote_task_id: 42, status: Accepted, assigned_node: "node-b" }
ResultDelivery (cached outcome) if already completed
SpawnRequest (idempotency conflict):
{ remote_task_id: 43, computation: "encode_block", input: "0xBEEF",
idempotency_key: IK-0001, ... }
Expect:
SpawnAck { remote_task_id: 43, status: Rejected(IdempotencyConflict), assigned_node: "node-b" }
CancelRequest (best-effort):
{ remote_task_id: 42, reason: Timeout, origin_node: "node-a" }
Expect:
ResultDelivery { remote_task_id: 42, outcome: Cancelled, ... } (if cancel wins)
Stub implementation hooks
- Phase 1 transport integration should implement:
RemoteTransport::send(to, MessageEnvelope<RemoteMessage>)RemoteTransport::try_recv() -> Option<MessageEnvelope<RemoteMessage>>
- The transport is responsible for envelope framing, version checks, and handshake.
- The runtime remains message-driven and deterministic in lab mode; the lab harness
can bypass serialization by injecting
MessageEnvelope<RemoteMessage>directly.
Origin-side states (RemoteHandle)
Pending --(SpawnAck:Accepted)--> Running
Pending --(SpawnAck:Rejected)--> Failed(Rejected)
Running --(ResultDelivery:Success)--> Completed
Running --(ResultDelivery:Failed/Panicked)--> Failed
Running --(ResultDelivery:Cancelled)--> Cancelled
Running --(lease timeout)--> LeaseExpired
LeaseExpired --(ResultDelivery:any)--> terminal (Completed/Failed/Cancelled)
State transitions must be monotone and idempotent. Duplicate messages are legal and must not regress state.
Origin-side transition table (Phase 1+)
| Current | Input | Next | Notes |
|---|---|---|---|
| Pending | SpawnAck:Accepted | Running | Record assigned node and lease. |
| Pending | SpawnAck:Rejected | Failed | Rejection reason is terminal. |
| Pending | CancelRequest (local) | Pending | Cancel is best-effort before ack. |
| Running | ResultDelivery:Success | Completed | Terminal. |
| Running | ResultDelivery:Failed/Panicked | Failed | Terminal. |
| Running | ResultDelivery:Cancelled | Cancelled | Terminal. |
| Running | Lease timeout | LeaseExpired | Escalate via policy. |
| LeaseExpired | ResultDelivery:any | Completed/Failed/Cancelled | Terminal and idempotent. |
Remote-side behavior
SpawnRequest-> checkidempotency_keyagainst a dedup store keyed by(key, computation):SpawnRequestnew -> record entry, start task, sendSpawnAck:Accepted.SpawnRequestduplicate -> resend cachedSpawnAck, and if outcome known, resendResultDelivery.SpawnRequestconflict -> sendSpawnAck:Rejected(IdempotencyConflict).SpawnRequestreject -> if computation unknown or capacity exceeded, reject with reason (no task start).CancelRequest-> mark task cancel requested; eventually deliverResultDelivery:Cancelled.- Completion or cancel -> emit exactly one terminal
ResultDelivery. - Dedup entries expire after a TTL; expiry is a policy knob and must be traceable.
Lease semantics
- Leases are obligations; the origin's region cannot close while a lease is active.
LeaseRenewalextends liveness; lack of renewal within the lease window moves origin state toLeaseExpired.- After
LeaseExpired, the origin may issueCancelRequestas a best-effort fence and must surface a deterministic outcome (policy: fail region, retry, or escalate).
Determinism invariants
- For each
RemoteTaskId, at most one terminal outcome is accepted. IdempotencyKeydeterministically maps to a single(computation,input)tuple.- Message handling is order-agnostic: causal time orders only when required; duplicates are safe.
- Retries reuse the same
IdempotencyKey; the remote responds with the originalremote_task_idand any cached outcome.
Release mode: auto-abort/nack + telemetry. Debug/lab: configurable panic.
Detect “holds obligations but stops being polled” conditions; fail in lab/debug.
For verification and schedule exploration, treat obligations as a vector addition system:
- each
reserve(kind)adds a token to a place, - each
commit/abort/nack/expireremoves a token, - region close requires the marking to be zero.
This yields simple linear invariants (“no negative tokens,” “close implies zero marking”) that are easy to check from traces and can be used as property‑based test oracles.
Even without a full type system, we can build a sound static check:
- abstract state: “may hold unresolved obligations of kind K” per scope/task,
reservesets “may hold,”commit/abortclears,- exiting a scope with “may hold” is a compile‑time warning/error.
This is an abstract interpretation in the Cousot–Cousot sense: sound, possibly conservative, and extremely valuable as the codebase grows.
defer_async and defer_sync. Run after drain, under cancel mask, LIFO.
bracket(acquire, use, release) with release masked/budgeted.
commit_section(fut) for bounded masked critical commits.
Future-proof; not required.
- Each region owns a
RegionHeapfor all region-scoped allocations. - Handles are indices with generations (no pointer identity leaks).
RRef<'r, T>is a typed handle tied to the region lifetime'r.RRef<'r, T>isSend/SyncwhenTis, but only valid while the region is open.
- Reclamation happens only at region close after quiescence.
RegionRecord::clear_heap()callsRegionHeap::reclaim_all()exactly once, during theFinalizing -> Closedtransition.- All admission paths are closed before draining; no new tasks/children/obligations can enter once closing begins.
- Stale handles are rejected:
HeapIndexgeneration guards against ABA reuse andRRef::get()returnsAllocationInvalidafter close.
- Any operation that holds region-owned memory across an await must create an obligation (permit/ack/lease) so region close blocks until it resolves.
- Region close is permitted iff:
children = 0 ∧ tasks = 0 ∧ obligations = 0 ∧ finalizers = 0. - Region admission enforces
RegionLimits::max_obligationsat reserve time and maps rejections toAdmissionDenied(create-obligation path).
- Deterministic counters (
GLOBAL_ALLOC_COUNT,HeapStats) provide leak visibility. - Lab oracles assert
global_alloc_count() == 0after region close. - Production uses structured trace + metrics for leak reporting.
- Tests cover ABA safety and deterministic reuse patterns in the heap, plus
witness-based access rejection (
WrongRegion,RegionClosed). - Heap admission enforces
RegionLimits::max_heap_bytesusing live-bytes tracked inHeapStats. - Free-list reuse is deterministic (LIFO), which makes
HeapIndexreuse patterns reproducible under fixed schedules.
- Safety: no use-after-free;
RRefis invalid after close. - Liveness: if a region reaches quiescence, heap reclamation occurs.
- Determinism:
HeapIndexgeneration prevents ABA; no pointer identity leaks. - Access control: witness validation rejects wrong-region and closed-region access attempts.
- Deterministic lab runs that stress region close under mixed obligations, heap allocations, and admission limits.
- Emit structured traces + allocation counters; assert
global_alloc_count() == 0andpending_obligations == 0at quiescence. - Record seeds and replay traces on failure for leak triage.
Cancel > timed (EDF bounded) > ready.
Avoid starvation via poll budgets and fairness injection.
Concrete bound: with cancel_streak_limit = L, if any ready or due‑timed task
remains continuously enabled, then within at most L + 1 dispatches the
scheduler must select a non‑cancel task (fairness yield). This bound is enforced
by the scheduler’s cancel‑streak counter and exercised in lab tests.
Yield at checkpoints; poll budget and optional CPU budget.
Throttle spawn/admission per region; backpressure at reserve points; priorities in budget.
Pluggable controller adjusts runtime knobs from telemetry; default is static.
Schedulers are usually heuristics; Asupersync has enough structure to do better.
Define a potential function V(Σ) over runtime state (regions/tasks/obligations), e.g.:
- number of live children (region “mass”),
- outstanding obligations weighted by age/priority,
- remaining finalizers,
- deadline slack / poll quota pressure.
Then require the governor/scheduler to choose steps that (in expectation or under a bound) decrease V, or decrease it under cancellation lanes first.
Under standard assumptions (cooperative checkpoints, bounded masking, fairness), LaSalle‑style arguments give: cancellation converges to quiescence rather than "we hope it drains."
Implementation note: The intuition here is sufficient for design; formal V(Σ) transition rules can be added to the operational semantics when the scheduler is actually built and needs verification.
The governor must plug into the scheduler through a narrow policy seam so the core scheduler remains correct-by-construction. The policy can influence, not override, the schedule.
Allowed influence surface (explicit):
- deterministic tie-breaking among runnable tasks within the same lane,
- selection among multiple ready queues when semantics permit,
- optional bounded promotion (e.g., run cancel-debt tasks earlier) only if cancel-lane strictness is preserved.
Hard invariants (non-negotiable):
- Cancel lane strictness unless a formal proof allows relaxation.
- Determinism: no wall-clock, no ambient RNG, stable iteration order.
- Bounded fairness: a runnable task cannot be starved indefinitely.
- No semantic changes: only reordering of runnable work, never skipping required protocol steps.
Policy interface (conceptual):
- Input: immutable
RuntimeSnapshot(no hot-path allocs). - Output: a deterministic ranking or choice among eligible tasks.
- Tie-break rule is fixed and stable (e.g., by TaskId then insertion order).
Evidence ledger (debug-only, trace-backed):
- record
Vdecomposition for each decision, - record candidate comparisons (why X beat Y),
- record policy constraints that forced suboptimal choices.
plan module builds DAG nodes, applies rewrites, dedupes shared work, schedules locally or remotely.
Patterns use the Plan IR node names: Join[...], Race[...], Timeout(duration, child).
All rules require explicit policy enablement plus side-condition checks
(obligation/cancellation safety, budget monotonicity, deterministic ordering).
| Rule | Pattern → Replacement | Required law | Rationale |
|---|---|---|---|
JoinAssoc |
Join[a, Join[b, c]] → Join[a, b, c] |
Join associativity | Flatten join trees for simpler scheduling and downstream dedup. |
RaceAssoc |
Race[a, Race[b, c]] → Race[a, b, c] |
Race associativity | Flatten race trees to reduce depth and canonicalize structure. |
JoinCommute |
Join[a, b] → Join[b, a] |
Join commutativity + independence | Canonical ordering when children are independent; enables stable certs. |
RaceCommute |
Race[a, b] → Race[b, a] |
Race commutativity + independence | Canonical ordering for deterministic certificates and replay. |
DedupRaceJoin |
Race[Join[s, a], Join[s, b]] → Join[s, Race[a, b]] |
Join/Race laws + shared-leaf safety | Deduplicate shared work while preserving loser-drain semantics. |
TimeoutMin |
Timeout(t1, Timeout(t2, x)) → Timeout(min(t1, t2), x) |
Timeout idempotence | Tightest timeout dominates; avoids redundant timers. |
All derived from kernel ops + join/race semantics with drained losers:
- join_all, race_all
- timeout
- first_ok
- quorum(k)
- hedge(delay)
- retry(strategy)
- pipeline
- map_reduce (monoid-based)
Base two-phase channels are default. Optional session-typed channels provide compile-time protocol conformance (dual types, affine endpoints).
Session types scale beyond channels:
- actor request/response and mailbox semantics,
- lease renewal protocols,
- distributed sagas (compensation as a structured dual protocol),
- multiparty protocols (global type → projected local types) for n‑party workflows.
The point is not “types for types’ sake”: session typing gives by construction guarantees like “no one can forget to ack,” and can be layered on top of runtime obligation tracking.
Actors are region-owned; no detached by default. Supervision policies (one-for-one, etc.) integrate with region close. Mailboxes are two-phase.
In-flight I/O ops are obligations tied to region. Region memory buffers are safe for zero-copy if region cannot reclaim until op completes/cancels. I/O submissions can be two-phase. Reactor is pluggable; lab backend simulates I/O deterministically.
Remote tasks are named computations (no closure shipping). Handles include leases + idempotency keys. Sagas are structured finalizers. Durable workflows are an extension crate.
Distributed semantics needs two additional mathematical commitments:
- Causal time: traces are partially ordered; use vector clocks (or an equivalent) so we never impose a fake total order on concurrent remote events.
- Convergent obligation state: obligation/lease state should form a join-semilattice so replicas converge (a CRDT-style view).
Reserved < Committed,Reserved < Aborted.Committed ⊔ Aborted = Conflict(protocol violation; surfaced deterministically in traces).
This makes “distributed structured concurrency” honest: we get determinism where possible (causal ordering), and explicit, detectable protocol violations where not.
Emit causal DAG trace events: parent/child, cancel edges, error edges, obligations, time. Supports postmortem “why” and replay.
Make the trace model match the true-concurrency semantics:
- record enough edges to reconstruct a happens-before partial order,
- normalize traces up to independence (commutation) so replay and diffing are stable,
- keep a small set of “semantic events” (spawn/complete/cancel/reserve/resolve/finalize) that can drive both debugging and proofs.
Virtual time + deterministic scheduling + trace capture/replay. Schedule exploration hooks (DPOR-class foundation). Property assertions: no task leaks, quiescence, finalizers exactly once, no unresolved obligations, losers drained, deadlines respected. Operational semantics is TLA+-friendly.
For schedule exploration, “DPOR-class” should mean optimal DPOR:
- define independence
Ion labels (from §3.2), - explore exactly one execution per Mazurkiewicz trace (equivalence class),
- use wakeup trees / source sets / sleep sets to avoid redundancy.
Longer-term, directed topological methods (dihomotopy classes of execution paths) can subsume some POR cases, but optimal DPOR is the practical, proven sweet spot.
Small-step kernel state Σ = (R, T, O, Now) with explicit rules for spawn, cancel, join, close, obligations.
(See file content in the diff for full skeleton; it includes Scope, Cx, Policy, Budget, Outcome, and two-phase channels.)
Single-thread deterministic-ready executor with:
- arenas for tasks/regions/obligations
- cancel + ready queues
- timers heap
- RawWaker that schedules TaskId with dedup
- JoinCell waiters and region close barrier waiters
- obligation registry + close waits on obligations too
- trace capture
Phase 0 kernel → Phase 1 parallel scheduler + region heap → Phase 2 I/O → Phase 3 actors/sessions → Phase 4 distributed → Phase 5 DPOR + TLA+ tooling.
Never allow a library primitive to stop being polled while holding an obligation without either transferring it to a drain/finalizer task, aborting/nacking it, or escalating (trace-recorded).
If you want, next I can also produce a single cohesive “crate layout + file-by-file skeleton” (with module stubs and the exact structs/enums to implement Phase‑0) that matches this Bible one-to-one.