Commit Graph
5 Commits
Author SHA1 Message Date
smarm-agent 95306c7f60 style: cargo fmt sweep under rustc 1.97.1 (toolchain reformat, no semantic change) 2026-08-13 05:56:49 +00:00
smarm-agent a875fa8285 core: rewrite panic sites as explicit match+panic
Replace implicit unwrap()/expect() in the lock-ordered core with explicit
match arms. Lock-poison sites use one uniform message
("smarm: <lock> lock poisoned (core corrupt): {e}"); invariant sites panic
with a descriptive message naming the violated invariant. No behaviour
change: each rewrite preserves the prior panic-on-bad-arm semantics. Also
clears the accompanying clippy hygiene in these files (redundant_closure,
len_without_is_empty, too_many_arguments, unnecessary_sort_by,
missing_safety_doc, nonminimal_bool/unnecessary_unwrap).
2026-06-20 17:47:33 +00:00
smarm-agent f5fbb5b144 mailbox: Phase 3 — typed Pid<A> over a raw inner identity (RFC 014)
Fold the made-up Addr<A> back into the pid, where it belonged. The typed
handle IS the pid now.

- RawPid { index, generation }: today's Pid, renamed — the raw numbers, the key
  for identity-only plumbing and the heterogeneous monitor/link/pg tables.
- Pid<A = Erased>: RawPid + phantom actor type. Both an identity and a direct,
  identity-bound address; when A: Addressable a send delivers A::Msg to exactly
  this incarnation (next phase). Hand-written impls (eq/hash/fmt on the raw
  only, no A bounds); fn()->A phantom keeps it Copy+Send+Sync for any A.
- Erased: uninhabited, deliberately NOT Addressable, so a typed send to a
  Pid<Erased> won't compile — that's what send_dyn (section 4.6) will be for. It
  is the default type arg, so every plain "Pid" written today still compiles as
  Pid<Erased>; that, plus the fact that nothing destructures pid fields, is why
  an identity change touching 18 files cascaded to zero internal edits.
- Addr<A> deleted; Name<M> / the Phase-2 registry unchanged (they key on raw).

Per the call to make the consumers take typed pids: the user-facing identity
APIs — monitor, link, unlink, request_stop, spawn_under(supervisor), pg::join,
pg::leave — are now generic over A and erase at the boundary; their bodies are
byte-for-byte unchanged. Pure plumbing (unpark, set_current_pid,
register_supervisor_channel) stays erased — it only ever sees internal identity.

Phase 1's Addr tests become Pid<A> tests (identity/copy/erase/Debug, Send+Sync).
Full suite + order-checker green, warning-free across all targets.
2026-06-16 22:09:27 +00:00
Claude 5e0c9d45da feat(runtime): phase 2 — fixed slab, per-slot packed-atomic state machine, raw cold locks
The slot table split (ROADMAP_v0.5 phase 2): slot lookup is lock-free, the
run path (yield/park/unpark/pop/resume) takes zero locks beyond the queue
mutex itself, and SharedState shrinks to { run_queue }.

Core pieces:

- src/raw_mutex.rs: hand-rolled 3-state futex mutex (Drepper mutex3),
  non-poisoning; the guard enters NoPreempt, which both bounds hold times and
  structurally closes the unwind-under-lock hole (the stop sentinel shares
  the gate). Used for per-slot cold data, the free list, and the stack pool.

- Fixed slab Box<[Slot]> (default max_actors = 16_384, ~4 MiB, align(128)
  against false sharing). Slots never move -> stable addresses, lock-free
  index. Exhaustion panics loudly, naming Config::max_actors(n) as the fix.
  Unbounded/segmented slab stays deferred (see ROADMAP).

- Per-slot AtomicU64 packing (generation << 32 | state), states
  Vacant/Queued/Running/RunningNotified/Parked/Done. Every transition CASes
  the packed word, so the generation check is atomic with the transition: no
  ABA, no spurious unparks on recycled slots. RunningNotified replaces the
  pending_unpark bool (the lost-wakeup window is a state, not a flag) and
  uniformly fixes a LATENT LOST WAKEUP in the old Blocking-IO completion
  path, which set the result for a still-Running actor without flagging it.

- Invariant: a pid is in the run queue at most once (pushes pair 1:1 with
  transitions into Queued; only the scheduler does Queued->Running). This is
  what makes phase 3's bounded rings sound.

- sp -> relaxed AtomicUsize; stop flag + first-resume closure as AtomicPtrs
  (closure double-boxed for a thin pointer, swap-to-take): the resume path is
  fully atomic.

- finalize_actor: Done published under the dying slot's cold lock (join's
  check-or-register is linearized by it); link cascade locks peers ONE AT A
  TIME (cold locks are leaves) with the acyclicity argument written at the
  site; link() registers on the target first, then self (stale self-entries
  are benign, every walk re-verifies the peer's word).

- Termination by counters: live_actors incremented in spawn pre-enqueue,
  decremented at the very END of finalize after all wakeup enqueues; exit on
  io_out == 0 (read before the queue lock, phase-1 ordering) && queue empty
  && live == 0. Soundness note at the site: any enqueue targets a live actor.

- spawn boxes the closure and acquires the stack BEFORE any runtime lock: no
  allocation ever happens under a global lock anymore.

- with_runtime/try_with_runtime now enter NoPreempt for their full span.
  This fixes a bug the rework exposed: install_actor allocated with
  preemption enabled while the RUNTIME RefCell borrow was live; a timeslice
  preemption there migrates the actor across OS threads and the borrow guard
  increments one thread's RefCell count and decrements another's — underflow
  to 'permanently mutably borrowed', cascading panics (caught by stress
  suite: deterministic non-unwinding-panic abort in lost_wakeup_many_pairs).
  The old code was safe only by accident; now it's structural.

- Behavior note: request_stop on a RUNNING target now marks it
  RunningNotified, so its next park returns immediately to an observation
  point — faster stop observation; parked/queued/done semantics unchanged.

Validated: 22 suites green in release (1/2/8-thread oversubscribed on the
1-core sandbox) and in debug with all debug_asserts live; stress suite x5.
2026-06-09 20:08:12 +00:00
smarm-dev 8ac11a57ac feat(link): bidirectional links + trap_exit (roadmap #3)
Add Erlang-style process links so an abnormal death fate-shares across a
link set, with trap_exit to convert that into a message instead.

- Slot.links: Vec<Pid>, bidirectional; reset in all three lifecycle sites.
- Actor.trap: Option<Sender<ExitSignal>>, fresh per spawn (a restarted
  child starts un-trapped; no fourth reset site).
- link/unlink free functions on self_pid(); trap_exit() -> Receiver<ExitSignal>
  (a dedicated inbox, distinct from the monitor Down channel).
- finalize_actor clears reverse links under the lock (always; keeps the
  cascade acyclic), then propagates abnormal deaths post-lock: trapping peer
  gets an ExitSignal message and survives, non-trapping peer is request_stop'd.
  Normal exit never propagates. Dead-pid link delivers an immediate NoProc
  (message if trapping, else request_stop(self) -- never a silent no-op).
- ExitSignal reuses DownReason and carries no panic payload (joiner-only).

Tests in tests/link.rs cover the propagate/trap/normal/dead-pid/unlink cases.
2026-06-07 21:51:56 +00:00