Skip to main content

byteflow/scheduler/
runtime.rs

1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::sync::{Arc, Condvar, Mutex};
3use std::thread::JoinHandle;
4
5use crate::bytecode::{Cap, CapRights, CapTarget, Chunk, NativeMask, Value};
6use crate::vm::{NativeGate, NativeTable, Vm};
7use crossbeam_deque::{Injector, Stealer, Worker as LocalDeque};
8
9use super::directory::Directory;
10use super::error::SpawnError;
11use super::handle::FlowHandle;
12use super::mailbox::{Delivery, Mailbox, MailboxConfig, MailboxFullReason};
13use super::metrics::{RuntimeMetrics, RuntimeMetricsSnapshot};
14use super::process::{Flow, FlowId, RestartPolicy};
15use super::supervisor::SupervisorLink;
16use super::timer::TimerWheel;
17use super::worker;
18
19/// Default instruction budget per scheduling turn (design notes §10).
20/// Chosen as a middle ground: large enough that the per-yield bookkeeping
21/// cost is amortized over meaningful work, small enough that a
22/// pathological `loop {}` in one flow can't visibly stall the others —
23/// at 10k simple instructions/turn and even a conservative tens-of-millions
24/// of instructions/sec per core, worst-case added latency for a sibling
25/// flow is sub-millisecond.
26pub const DEFAULT_QUANTUM: u32 = 10_000;
27
28/// Tunables for [`Runtime::new`]. Everything has a sensible default via
29/// [`RuntimeConfig::default`] so the common case is `Runtime::new(chunk)`.
30#[derive(Clone, Debug)]
31pub struct RuntimeConfig {
32    /// Number of worker OS threads. Defaults to the number of logical CPUs
33    /// — one worker per core is the right starting point for a CPU-bound
34    /// M:N scheduler; embedders running alongside other CPU-heavy work on
35    /// the same machine may want fewer.
36    pub workers: usize,
37    /// Instructions a flow runs before being preempted back to the
38    /// scheduler even if it never hits `Yield`.
39    pub quantum: u32,
40    /// Memory + overflow contract applied to **every** flow mailbox
41    /// spawned by this runtime (bytecode `Spawn` and host `spawn`).
42    /// See [`MailboxConfig`] / `docs/mailbox.md`.
43    pub mailbox: MailboxConfig,
44    /// Hard cap on concurrently live flows (`0` = unlimited).
45    /// Checked on every host and bytecode `spawn`.
46    pub max_flows: u32,
47    /// Constant-pool trust for [`crate::verify_with`] at runtime construction.
48    /// Default is [`crate::TrustLevel::Untrusted`] (fail closed).
49    pub trust: crate::bytecode::TrustLevel,
50    /// Where bytecode `print` (native index 0) writes when using
51    /// [`crate::std_native_table_with`]. Ignored if you supply your own table.
52    pub output: Arc<dyn crate::OutputSink>,
53    /// Per-flow CPU / memory / spawn-send rate budgets.
54    pub quota: super::quota::QuotaConfig,
55    /// Trace JIT settings (`feature = "jit"`). Ignored when the feature is off.
56    #[cfg(feature = "jit")]
57    pub jit: JitConfig,
58}
59
60/// Trace JIT toggles for [`RuntimeConfig`] (`feature = "jit"`).
61#[cfg(feature = "jit")]
62#[derive(Clone, Debug)]
63pub struct JitConfig {
64    /// When true, workers attempt compiled traces before interpreting.
65    pub enabled: bool,
66    /// How many times a `(function, pc)` pair must run before compilation.
67    pub hot_threshold: u32,
68}
69
70#[cfg(feature = "jit")]
71impl Default for JitConfig {
72    fn default() -> Self {
73        JitConfig {
74            enabled: false,
75            hot_threshold: crate::jit::HOT_THRESHOLD,
76        }
77    }
78}
79
80impl Default for RuntimeConfig {
81    fn default() -> Self {
82        RuntimeConfig {
83            workers: num_cpus::get().max(1),
84            quantum: DEFAULT_QUANTUM,
85            mailbox: MailboxConfig::DEFAULT,
86            max_flows: 0,
87            trust: crate::bytecode::TrustLevel::Untrusted,
88            output: Arc::new(crate::output::NullSink),
89            quota: super::quota::QuotaConfig::default(),
90            #[cfg(feature = "jit")]
91            jit: JitConfig::default(),
92        }
93    }
94}
95
96/// State shared by every worker thread and the timer thread. Everything in
97/// here is either internally synchronized (`Injector`, `Directory`,
98/// `CapTable`, `TimerWheel`, the `RuntimeMetrics` atomics) or immutable after
99/// construction (`stealers`, `quantum`) — there is no top-level lock
100/// covering the whole runtime, by design: a global lock is exactly what an
101/// M:N scheduler exists to avoid.
102pub struct Shared {
103    pub(crate) injector: Injector<Box<Flow>>,
104    pub(crate) stealers: Vec<Stealer<Box<Flow>>>,
105    /// FlowId → mailbox (delivery after Cap resolution).
106    pub(crate) directory: Directory,
107    /// CapId → { FlowId, rights } (bytecode Send/Ask addressing — FlowCap).
108    pub(crate) caps: super::capability::CapTable,
109    pub(crate) timer: Arc<TimerWheel>,
110    pub(crate) notify: (Mutex<()>, Condvar),
111    pub(crate) metrics: RuntimeMetrics,
112    pub(crate) shutdown: AtomicBool,
113    pub(crate) quantum: u32,
114    pub(crate) mailbox: MailboxConfig,
115    pub(crate) max_flows: u32,
116    pub(crate) quota: super::quota::QuotaConfig,
117    pub(crate) quotas: super::quota::QuotaTable,
118    pub(crate) monitors: super::monitor::MonitorStore,
119    pub(crate) links: super::link::LinkStore,
120    pub(crate) registry: super::registry::RegistryStore,
121    pub(crate) kill_signals: super::finalize::KillSignals,
122    pub(crate) waiting_send_at: super::finalize::WaitingSendIndex,
123    pub(crate) ask_waits: super::finalize::AskWaitIndex,
124    /// Correlation ids for host `Runtime::send` when `request_id == 0`.
125    /// Starts at 1; `0` stays the unset sentinel.
126    pub(crate) host_next_request_id: AtomicU64,
127    /// Shared trace JIT state (`feature = "jit"`).
128    #[cfg(feature = "jit")]
129    pub(crate) jit: Option<std::sync::Arc<crate::jit::JitRuntime>>,
130}
131
132/// A running Byteflow runtime: worker pool + timer thread over one shared
133/// [`Chunk`].
134///
135/// Owns M:N scheduling for **flows** (spawn, yield, sleep, mailboxes,
136/// FlowCap resolution, supervised restarts). Optional trace JIT when built
137/// with `feature = "jit"` and enabled in [`RuntimeConfig::jit`].
138///
139/// Construct with [`Runtime::new`] (no natives) or
140/// [`Runtime::with_natives`] when the chunk uses `CallNative` /
141/// [`crate::std_native_table`].
142pub struct Runtime {
143    shared: Arc<Shared>,
144    chunk: Arc<Chunk>,
145    natives: Arc<NativeTable>,
146    workers: Vec<JoinHandle<()>>,
147    timer_thread: Option<JoinHandle<()>>,
148    trust: crate::bytecode::TrustLevel,
149}
150
151impl Runtime {
152    /// Convenience constructor for chunks that never call out through
153    /// `Opcode::CallNative`. Equivalent to
154    /// `Runtime::with_natives(chunk, NativeTable::empty())`.
155    ///
156    /// Returns [`SpawnError`] instead of panicking: verify failures and OS
157    /// thread-spawn refusals are category-A errors (see
158    /// [`docs::error_model`](crate::docs::error_model)).
159    pub fn new(chunk: Chunk) -> Result<Self, SpawnError> {
160        Self::with_config(chunk, RuntimeConfig::default())
161    }
162
163    /// Construct a runtime whose flows can call into `natives` via
164    /// `Opcode::CallNative` — the host FFI boundary.
165    pub fn with_natives(chunk: Chunk, natives: Arc<NativeTable>) -> Result<Self, SpawnError> {
166        Self::with_natives_and_config(chunk, natives, RuntimeConfig::default())
167    }
168
169    pub fn with_config(chunk: Chunk, config: RuntimeConfig) -> Result<Self, SpawnError> {
170        Self::with_natives_and_config(chunk, NativeTable::empty(), config)
171    }
172
173    /// Like [`Runtime::with_config`] but wires [`crate::std_native_table_with`]
174    /// using [`RuntimeConfig::output`] for the `print` native.
175    pub fn with_std_natives_and_config(
176        chunk: Chunk,
177        config: RuntimeConfig,
178    ) -> Result<Self, SpawnError> {
179        Self::with_natives_and_config(
180            chunk,
181            crate::std_native_table_with(Arc::clone(&config.output)),
182            config,
183        )
184    }
185
186    /// Verify `chunk`, spawn the worker pool + timer thread, and return a
187    /// live [`Runtime`].
188    ///
189    /// Failures here mean the runtime was **never** started (no orphan
190    /// threads): either the bytecode is invalid
191    /// ([`SpawnError::VerifyFailed`]) or the OS refused a thread
192    /// ([`SpawnError::ThreadSpawnFailed`]).
193    pub fn with_natives_and_config(
194        chunk: Chunk,
195        natives: Arc<NativeTable>,
196        config: RuntimeConfig,
197    ) -> Result<Self, SpawnError> {
198        crate::bytecode::verify_with(
199            &chunk,
200            crate::bytecode::VerifyConfig {
201                trust: config.trust,
202            },
203        )
204        .map_err(SpawnError::VerifyFailed)?;
205        let chunk = Arc::new(chunk);
206        let workers_n = config.workers.max(1);
207
208        let locals: Vec<LocalDeque<Box<Flow>>> =
209            (0..workers_n).map(|_| LocalDeque::new_fifo()).collect();
210        let stealers: Vec<Stealer<Box<Flow>>> = locals.iter().map(|l| l.stealer()).collect();
211
212        #[cfg(feature = "jit")]
213        let jit = if config.jit.enabled {
214            Some(super::jit::new_runtime(chunk.clone(), config.jit.hot_threshold))
215        } else {
216            None
217        };
218
219        let shared = Arc::new(Shared {
220            injector: Injector::new(),
221            stealers,
222            directory: Directory::new(),
223            caps: super::capability::CapTable::new(),
224            timer: TimerWheel::new(),
225            notify: (Mutex::new(()), Condvar::new()),
226            metrics: RuntimeMetrics::default(),
227            shutdown: AtomicBool::new(false),
228            quantum: config.quantum,
229            mailbox: config.mailbox,
230            max_flows: config.max_flows,
231            quota: config.quota,
232            quotas: super::quota::QuotaTable::new(),
233            monitors: super::monitor::MonitorStore::new(),
234            links: super::link::LinkStore::new(),
235            registry: super::registry::RegistryStore::new(),
236            kill_signals: super::finalize::KillSignals::new(),
237            waiting_send_at: super::finalize::WaitingSendIndex::new(),
238            ask_waits: super::finalize::AskWaitIndex::new(),
239            host_next_request_id: AtomicU64::new(1),
240            #[cfg(feature = "jit")]
241            jit,
242        });
243
244        let mut workers = Vec::with_capacity(workers_n);
245        for local in locals {
246            let shared = shared.clone();
247            let handle = std::thread::Builder::new()
248                .name("byteflow-worker".into())
249                .spawn(move || worker::run_worker(shared, local))
250                .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
251            workers.push(handle);
252        }
253
254        let shared_timer = shared.clone();
255        let timer_thread = std::thread::Builder::new()
256            .name("byteflow-timer".into())
257            .spawn(move || {
258                shared_timer
259                    .timer
260                    .clone()
261                    .drive(
262                        &shared_timer.injector,
263                        &shared_timer.notify,
264                        &shared_timer.ask_waits,
265                    )
266            })
267            .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
268
269        Ok(Runtime {
270            shared,
271            chunk,
272            natives,
273            workers,
274            timer_thread: Some(timer_thread),
275            trust: config.trust,
276        })
277    }
278
279    /// Spawn a top-level flow starting at `function` in this runtime's
280    /// chunk, returning a [`FlowHandle`] the caller can `.join()`.
281    ///
282    /// Returns [`SpawnError::BadFunction`] if `function` is out of range.
283    pub fn spawn(&self, function: u32, args: &[Value]) -> Result<FlowHandle, SpawnError> {
284        spawn_on(
285            &self.shared,
286            &self.chunk,
287            &self.natives,
288            function,
289            args,
290            RestartPolicy::Never,
291            None,
292            None,
293            None,
294        )
295    }
296
297    /// A cheap, `Send + Sync` handle that can spawn processes into this
298    /// runtime from any thread, independent of `Runtime`'s own lifetime
299    /// bookkeeping (worker `JoinHandle`s). Used by [`super::supervisor::Supervisor`].
300    pub fn spawner(&self) -> RuntimeSpawner {
301        RuntimeSpawner { shared: self.shared.clone(), chunk: self.chunk.clone(), natives: self.natives.clone() }
302    }
303
304    /// A [`super::supervisor::Supervisor`] bound to this runtime, ready to
305    /// take supervised children (design notes §15).
306    pub fn supervisor(&self) -> Result<super::supervisor::Supervisor, SpawnError> {
307        super::supervisor::Supervisor::new(self.spawner())
308    }
309
310    /// Look up a function by name in the runtime's chunk — convenience for
311    /// callers that built their chunk with [`crate::Program`]
312    /// and don't want to thread raw indices through their own code.
313    pub fn function_index(&self, name: &str) -> Option<u32> {
314        self.chunk.functions.iter().position(|f| f.name == name).map(|i| i as u32)
315    }
316
317    pub fn metrics(&self) -> RuntimeMetricsSnapshot {
318        self.shared.metrics.snapshot()
319    }
320
321    /// Number of flows currently registered in the directory — i.e.
322    /// alive (running, ready, sleeping, or waiting), not counting ones that
323    /// have already completed or failed.
324    pub fn live_flows(&self) -> usize {
325        self.shared.directory.len()
326    }
327
328    pub fn worker_count(&self) -> usize {
329        self.workers.len()
330    }
331
332    /// Deliver an **Atomic Hop** (`Value::Message`) to `target` from the
333    /// embedder (not from bytecode).
334    ///
335    /// # Host trust boundary
336    ///
337    /// This path takes a [`FlowId`] directly — **no Cap required**. The host
338    /// is trusted; bytecode must use `Value::Cap` via `Opcode::Send` /
339    /// `Ask`. The hop still goes through the same authentication choke-point
340    /// as bytecode: `sender` is stamped [`FlowId::HOST`] (`0`), `request_id`
341    /// is minted when unset, and payload Caps are reissued to the recipient.
342    /// The host has no mailbox, so `reply_cap` is [`crate::bytecode::CapId::NONE`].
343    /// Bare scalars are rejected ([`SendError::NotAHop`]).
344    pub fn send(&self, target: FlowId, message: Value) -> Result<(), SendError> {
345        let Some(msg) = message.as_message().cloned() else {
346            return Err(SendError::NotAHop {
347                got: message.type_name(),
348            });
349        };
350        let mailbox = match self.shared.directory.lookup(target) {
351            Ok(Some(m)) => m,
352            Ok(None) => return Err(SendError::NoSuchFlow(target)),
353            Err(e) => {
354                super::error::report_fault(e);
355                return Err(SendError::NoSuchFlow(target));
356            }
357        };
358        let stamped = match worker::authenticate_host_outgoing_message(&self.shared, target, msg) {
359            Ok(m) => Value::Message(m),
360            Err(_) => return Err(SendError::Capability),
361        };
362        match mailbox.push(stamped.clone()) {
363            Ok(Ok(Delivery::Queued | Delivery::QueuedDropOldest | Delivery::DroppedNewest)) => {
364                Ok(())
365            }
366            Ok(Ok(Delivery::Handoff(mut flow))) => {
367                let _ = self.shared.ask_waits.remove_asker(flow.id);
368                if let Some(dest) = flow.last_receive_dest {
369                    let _ = flow.vm.resume_with(dest, stamped);
370                }
371                self.shared.injector.push(flow);
372                wake_workers(&self.shared);
373                Ok(())
374            }
375            Ok(Err(full)) => Err(SendError::MailboxFull {
376                flow: target,
377                reason: full.reason(),
378            }),
379            Err(e) => {
380                super::error::report_fault(e);
381                Err(SendError::NoSuchFlow(target))
382            }
383        }
384    }
385
386    fn require_live(&self, id: FlowId) -> Result<(), super::error::LifecycleError> {
387        match self.shared.directory.lookup(id) {
388            Ok(Some(_)) => Ok(()),
389            Ok(None) => Err(super::error::LifecycleError::NoSuchFlow(id)),
390            Err(e) => Err(self.unavailable(e)),
391        }
392    }
393
394    fn unavailable(&self, err: super::error::RuntimeError) -> super::error::LifecycleError {
395        super::error::report_fault(err);
396        super::error::LifecycleError::Unavailable
397    }
398
399    /// Mint a SEND|ASK Cap for a live flow (host equivalent of `SelfPid`).
400    pub fn mint_cap(&self, flow: FlowId) -> Result<crate::bytecode::CapId, super::error::LifecycleError> {
401        self.require_live(flow)?;
402        self.shared
403            .caps
404            .mint(flow, flow, super::capability::CapRights::ADDRESSING)
405            .map_err(|e| self.unavailable(e))
406    }
407
408    /// Mint a Cap held by `holder` targeting `target` (host introduction).
409    pub fn grant_cap(
410        &self,
411        holder: FlowId,
412        target: FlowId,
413    ) -> Result<crate::bytecode::CapId, super::error::LifecycleError> {
414        self.require_live(holder)?;
415        self.require_live(target)?;
416        self.shared
417            .caps
418            .mint(holder, target, super::capability::CapRights::ADDRESSING)
419            .map_err(|e| self.unavailable(e))
420    }
421
422    /// Watch `target`; when it exits, `owner` receives a [`crate::TAG_SYS_DOWN`] hop.
423    ///
424    /// Both flows must be live. `owner == target` is [`LifecycleError::SelfRelation`].
425    pub fn monitor(
426        &self,
427        owner: FlowId,
428        target: FlowId,
429    ) -> Result<super::monitor::MonitorRef, super::error::LifecycleError> {
430        if owner == target {
431            return Err(super::error::LifecycleError::SelfRelation);
432        }
433        self.require_live(owner)?;
434        self.require_live(target)?;
435        let mon = self
436            .shared
437            .monitors
438            .create(owner, target)
439            .map_err(|e| self.unavailable(e))?;
440        // Target may have finalized between the live check and insert.
441        // Synthesize DOWN and drop the now-useless relation (owner still live).
442        if self.require_live(target).is_err() {
443            let _ = self.shared.monitors.remove_owned(owner, mon);
444            super::finalize::deliver_down(
445                &self.shared,
446                super::monitor::DownEvent {
447                    monitor: mon,
448                    owner,
449                    target,
450                    reason: super::monitor::FlowExitReason::Fault,
451                },
452            );
453        }
454        Ok(mon)
455    }
456
457    /// Drop `monitor` if `owner` still owns it.
458    pub fn demonitor(
459        &self,
460        owner: FlowId,
461        monitor: super::monitor::MonitorRef,
462    ) -> Result<(), super::error::LifecycleError> {
463        self.require_live(owner)?;
464        match self.shared.monitors.remove_owned(owner, monitor) {
465            Ok(inner) => inner,
466            Err(e) => Err(self.unavailable(e)),
467        }
468    }
469
470    /// Bidirectional link. Abnormal exit of either side kills the peer.
471    pub fn link(
472        &self,
473        a: FlowId,
474        b: FlowId,
475    ) -> Result<super::link::LinkId, super::error::LifecycleError> {
476        if a == b {
477            return Err(super::error::LifecycleError::SelfRelation);
478        }
479        self.require_live(a)?;
480        self.require_live(b)?;
481        match self.shared.links.link(a, b) {
482            Ok(inner) => inner,
483            Err(e) => Err(self.unavailable(e)),
484        }
485    }
486
487    /// Drop `link` if `owner` is one of the endpoints.
488    pub fn unlink(
489        &self,
490        owner: FlowId,
491        link: super::link::LinkId,
492    ) -> Result<(), super::error::LifecycleError> {
493        self.require_live(owner)?;
494        match self.shared.links.unlink_owned(owner, link) {
495            Ok(inner) => inner,
496            Err(e) => Err(self.unavailable(e)),
497        }
498    }
499
500    /// Bind `name` to a live Cap (address). Names are swept when that flow exits.
501    pub fn register_name(
502        &self,
503        name: &str,
504        cap: crate::bytecode::CapId,
505    ) -> Result<(), super::error::LifecycleError> {
506        let entry = match self.shared.caps.lookup(cap) {
507            Ok(Some(e)) => e,
508            Ok(None) => return Err(super::error::LifecycleError::InvalidCapability),
509            Err(e) => return Err(self.unavailable(e)),
510        };
511        let target = match entry.target() {
512            Some(id) => id,
513            None => return Err(super::error::LifecycleError::InvalidCapability),
514        };
515        self.require_live(target)?;
516        match self.shared.registry.register(
517            super::registry::RegistryName::from(name),
518            cap,
519            target,
520        ) {
521            Ok(inner) => inner,
522            Err(e) => Err(self.unavailable(e)),
523        }
524    }
525
526    /// Look up a registered Cap, or `None` if the name is free / was swept.
527    pub fn whereis(&self, name: &str) -> Result<Option<crate::bytecode::CapId>, super::error::LifecycleError> {
528        self.shared
529            .registry
530            .whereis(name)
531            .map_err(|e| self.unavailable(e))
532    }
533
534    /// Cooperative abort. Parked flows finalize immediately; a running flow
535    /// dies at the next quantum with [`super::monitor::FlowExitReason::Killed`].
536    pub fn kill(&self, id: FlowId) -> Result<(), super::error::LifecycleError> {
537        self.require_live(id)?;
538        super::finalize::request_kill(&self.shared, id, super::monitor::FlowExitReason::Killed);
539        Ok(())
540    }
541
542    /// Mint a scheduler ADMIN cap for a live flow (host introduction).
543    pub fn mint_admin_cap(
544        &self,
545        holder: FlowId,
546    ) -> Result<crate::bytecode::CapId, super::error::LifecycleError> {
547        self.require_live(holder)?;
548        let cap = crate::bytecode::Cap::root(
549            crate::bytecode::CapTarget::Scheduler,
550            CapRights::ADMIN,
551            None,
552            self.shared.caps.scheduler_cell().as_ref(),
553        );
554        self.shared
555            .caps
556            .grant(holder, cap)
557            .map_err(|e| self.unavailable(e))
558    }
559
560    /// Kill `target` only if `holder` presents a live ADMIN scheduler cap.
561    pub fn admin_kill(
562        &self,
563        holder: FlowId,
564        cap: crate::bytecode::CapId,
565        target: FlowId,
566    ) -> Result<(), super::error::LifecycleError> {
567        self.require_admin(holder, cap)?;
568        self.kill(target)
569    }
570
571    /// Top up `target`'s CPU budget. Requires a live ADMIN scheduler cap.
572    pub fn admin_top_up_cpu(
573        &self,
574        holder: FlowId,
575        cap: crate::bytecode::CapId,
576        target: FlowId,
577        extra: i64,
578    ) -> Result<(), super::error::LifecycleError> {
579        self.require_admin(holder, cap)?;
580        let quota = match self.shared.quotas.get(target) {
581            Ok(Some(q)) => q,
582            Ok(None) => return Err(super::error::LifecycleError::NoSuchFlow(target)),
583            Err(e) => return Err(self.unavailable(e)),
584        };
585        quota.top_up_cpu(extra);
586        Ok(())
587    }
588
589    /// Raise `target`'s heap limit. Requires a live ADMIN scheduler cap.
590    pub fn admin_top_up_mem(
591        &self,
592        holder: FlowId,
593        cap: crate::bytecode::CapId,
594        target: FlowId,
595        extra: usize,
596    ) -> Result<(), super::error::LifecycleError> {
597        self.require_admin(holder, cap)?;
598        let quota = match self.shared.quotas.get(target) {
599            Ok(Some(q)) => q,
600            Ok(None) => return Err(super::error::LifecycleError::NoSuchFlow(target)),
601            Err(e) => return Err(self.unavailable(e)),
602        };
603        quota.top_up_mem(extra);
604        Ok(())
605    }
606
607    /// Credit `target`'s send bucket. Requires a live ADMIN scheduler cap.
608    pub fn admin_top_up_send(
609        &self,
610        holder: FlowId,
611        cap: crate::bytecode::CapId,
612        target: FlowId,
613        extra: i64,
614    ) -> Result<(), super::error::LifecycleError> {
615        self.require_admin(holder, cap)?;
616        let quota = match self.shared.quotas.get(target) {
617            Ok(Some(q)) => q,
618            Ok(None) => return Err(super::error::LifecycleError::NoSuchFlow(target)),
619            Err(e) => return Err(self.unavailable(e)),
620        };
621        quota.top_up_send(extra);
622        Ok(())
623    }
624
625    fn require_admin(
626        &self,
627        holder: FlowId,
628        cap: crate::bytecode::CapId,
629    ) -> Result<(), super::error::LifecycleError> {
630        let entry = match self.shared.caps.resolve(cap, holder, CapRights::ADMIN) {
631            Ok(e) => e,
632            Err(_) => return Err(super::error::LifecycleError::InvalidCapability),
633        };
634        super::link_admin::check_admin(&entry.cap, self.shared.caps.scheduler_cell().as_ref())
635            .map_err(|_| super::error::LifecycleError::InvalidCapability)
636    }
637
638    /// Remove a name without waiting for the flow to exit. `false` if unknown.
639    pub fn unregister_name(&self, name: &str) -> Result<bool, super::error::LifecycleError> {
640        self.shared
641            .registry
642            .unregister(name)
643            .map_err(|e| self.unavailable(e))
644    }
645
646    /// Replace the image used by **new host** [`Self::spawn`] calls and
647    /// drop JIT traces. Live flows and bytecode `Spawn` keep the parent's
648    /// existing `Vm` chunk.
649    pub fn reload_chunk(&mut self, chunk: Chunk) -> Result<(), SpawnError> {
650        crate::bytecode::verify_with(
651            &chunk,
652            crate::bytecode::VerifyConfig {
653                trust: self.trust,
654            },
655        )
656        .map_err(SpawnError::VerifyFailed)?;
657        let chunk = Arc::new(chunk);
658        self.chunk = chunk.clone();
659        #[cfg(feature = "jit")]
660        if let Some(jit) = &self.shared.jit {
661            jit.reload(chunk);
662        }
663        Ok(())
664    }
665
666    /// Signal workers and the timer to stop. Does **not** join threads —
667    /// use [`Self::shutdown`] for a deterministic join. [`Drop`] only signals.
668    fn request_shutdown(&self) {
669        self.shared.shutdown.store(true, Ordering::Release);
670        self.shared.timer.shutdown();
671        let (lock, cvar) = &self.shared.notify;
672        match super::sync_lock::lock(lock, "Runtime::request_shutdown") {
673            Ok(_g) => cvar.notify_all(),
674            Err(e) => super::error::report_fault(e),
675        }
676    }
677
678    /// Stop accepting new scheduling work and join every worker + the timer
679    /// thread. Processes that are mid-quantum are allowed to reach their
680    /// next natural suspension point; this does **not** forcibly abort
681    /// running bytecode (there is no safe way to do that to an OS thread
682    /// mid-instruction — see design notes §11 on why preemption here is
683    /// cooperative/budgeted rather than signal-based).
684    pub fn shutdown(mut self) {
685        self.request_shutdown();
686        for w in self.workers.drain(..) {
687            let _ = w.join();
688        }
689        if let Some(t) = self.timer_thread.take() {
690            let _ = t.join();
691        }
692    }
693}
694
695impl Drop for Runtime {
696    fn drop(&mut self) {
697        if !self.shared.shutdown.load(Ordering::Acquire) {
698            self.request_shutdown();
699        }
700    }
701}
702
703/// A convenience Pid constructor for embedders that stored a raw `u64`
704/// (e.g. round-tripped through `Value::Pid`) and need a [`FlowId`] to
705/// call APIs that take one.
706pub fn flow_id_from_u64(raw: u64) -> FlowId {
707    FlowId(raw)
708}
709
710/// Why [`Runtime::send`] could not deliver a hop.
711#[derive(Debug, Clone, PartialEq, Eq)]
712pub enum SendError {
713    NoSuchFlow(FlowId),
714    /// Atomic Hop rule: only [`crate::Value::Message`] may cross `Send`.
715    NotAHop { got: &'static str },
716    /// Target inbox is at one of its logical bounds
717    /// ([`OverflowPolicy::Reject`](crate::OverflowPolicy::Reject)). `reason` says which — see
718    /// [`MailboxFullReason`].
719    MailboxFull {
720        flow: FlowId,
721        reason: MailboxFullReason,
722    },
723    /// Host hop authentication failed (payload Cap reissue).
724    Capability,
725}
726
727impl std::fmt::Display for SendError {
728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
729        match self {
730            SendError::NoSuchFlow(id) => write!(f, "no live flow {id}"),
731            SendError::NotAHop { got } => {
732                write!(f, "atomic hop requires Value::Message, got {got}")
733            }
734            SendError::MailboxFull { flow, reason } => {
735                write!(f, "mailbox full for {flow} ({reason})")
736            }
737            SendError::Capability => {
738                write!(f, "host send could not reissue a capability in the hop")
739            }
740        }
741    }
742}
743
744impl std::error::Error for SendError {}
745
746/// Context for a bytecode `Spawn`: parent self-authority is attenuated into
747/// the child. Host spawn passes `None` and mints a trusted root instead.
748pub(crate) struct BytecodeSpawn<'a> {
749    pub authority: &'a Cap,
750    pub cell: &'a crate::bytecode::RevocationCell,
751    pub quota: &'a super::quota::FlowQuota,
752    pub requested_rights: CapRights,
753}
754
755/// Shared machinery behind `Runtime::spawn` and `RuntimeSpawner::spawn`
756/// (and, transitively, `Supervisor`): build a fresh `Flow` (VM +
757/// mailbox + completion channel), register it in the directory, and push
758/// it onto the global injector for any worker to pick up.
759///
760/// Returns [`SpawnError`] on bad function index / VM init / directory
761/// poison — never panics. Bytecode `Opcode::Spawn` that fails here turns
762/// into `FlowOutcome::Failed` for the *parent* (see `worker`).
763pub(crate) fn spawn_on(
764    shared: &Arc<Shared>,
765    chunk: &Arc<Chunk>,
766    natives: &Arc<NativeTable>,
767    function: u32,
768    args: &[Value],
769    restart_policy: RestartPolicy,
770    supervisor: Option<SupervisorLink>,
771    parent: Option<FlowId>,
772    bytecode: Option<BytecodeSpawn<'_>>,
773) -> Result<FlowHandle, SpawnError> {
774    if shared.max_flows > 0 {
775        let current = shared.directory.len();
776        if current >= shared.max_flows as usize {
777            return Err(SpawnError::FlowLimit {
778                current,
779                max: shared.max_flows,
780            });
781        }
782    }
783    let id = super::process::next_flow_id();
784    let cell = shared
785        .caps
786        .bind_flow(id)
787        .map_err(|_| SpawnError::Unavailable)?;
788    let authority = match bytecode {
789        None => Cap::root(
790            CapTarget::Flow(id.as_u64()),
791            CapRights::ROOT,
792            Some(NativeMask::full(natives.len())),
793            cell.as_ref(),
794        ),
795        Some(ctx) => super::spawn::exec_spawn_authority(
796            ctx.authority,
797            ctx.cell,
798            ctx.quota,
799            id.as_u64(),
800            ctx.requested_rights,
801            None,
802            cell.as_ref(),
803        )
804        .map_err(|e| SpawnError::SpawnDenied(e.to_string()))?,
805    };
806    let args = grant_caps_in_args(shared, parent, id, args)?;
807    let gate = NativeGate::from_authority(
808        &authority,
809        Arc::clone(&cell),
810        shared.caps.native_cell(),
811        natives.len(),
812    );
813    let quota = Arc::new(super::quota::FlowQuota::from_config(shared.quota));
814    let mut vm = Vm::with_native_gate(chunk.clone(), natives.clone(), gate, function, args.as_slice())?;
815    vm.set_quota(Arc::clone(&quota))?;
816    let mailbox = Arc::new(Mailbox::with_config(shared.mailbox));
817    if let Err(e) = shared.directory.register(id, mailbox.clone()) {
818        super::error::report_fault(e);
819        return Err(SpawnError::Unavailable);
820    }
821    if let Err(e) = shared.quotas.insert(id, Arc::clone(&quota)) {
822        super::error::report_fault(e);
823        return Err(SpawnError::Unavailable);
824    }
825    let (tx, rx) = super::oneshot::channel();
826    let mut flow = Box::new(Flow::new(id, vm, mailbox, restart_policy, tx));
827    flow.authority = authority;
828    flow.cell = cell;
829    flow.quota = quota;
830    flow.supervisor = supervisor;
831    RuntimeMetrics::inc(&shared.metrics.processes_spawned);
832    shared.injector.push(flow);
833    wake_workers(shared);
834    Ok(FlowHandle { id, receiver: rx })
835}
836
837fn grant_caps_in_args(
838    shared: &Shared,
839    parent: Option<FlowId>,
840    child: FlowId,
841    args: &[Value],
842) -> Result<Vec<Value>, SpawnError> {
843    let mut out = Vec::with_capacity(args.len());
844    for arg in args {
845        match arg {
846            Value::Cap(id) => {
847                let granted = match parent {
848                    None => shared.caps.reissue_for(*id, child),
849                    Some(p) => shared.caps.delegate(*id, p, child),
850                };
851                match granted {
852                    Ok(cap) => out.push(Value::Cap(cap)),
853                    Err(_) => return Err(SpawnError::InvalidCapability),
854                }
855            }
856            other => out.push(other.clone()),
857        }
858    }
859    Ok(out)
860}
861
862pub(crate) fn wake_workers(shared: &Shared) {
863    let (lock, cvar) = &shared.notify;
864    match super::sync_lock::lock(lock, "wake_workers") {
865        Ok(_g) => cvar.notify_one(),
866        Err(e) => super::error::report_fault(e),
867    }
868}
869
870/// A `Send + Sync`, freely cloneable capability to spawn processes into a
871/// [`Runtime`], detached from the `Runtime` value itself. Exists because
872/// [`super::supervisor::Supervisor`] needs to respawn processes from a
873/// background monitor thread whose lifetime isn't tied to the `Runtime`
874/// object's own (which owns non-`Sync` `JoinHandle`s for its workers).
875#[derive(Clone)]
876pub struct RuntimeSpawner {
877    pub(crate) shared: Arc<Shared>,
878    pub(crate) chunk: Arc<Chunk>,
879    pub(crate) natives: Arc<NativeTable>,
880}
881
882impl RuntimeSpawner {
883    pub fn spawn(
884        &self,
885        function: u32,
886        args: &[Value],
887        restart_policy: RestartPolicy,
888    ) -> Result<FlowHandle, SpawnError> {
889        spawn_on(
890            &self.shared,
891            &self.chunk,
892            &self.natives,
893            function,
894            args,
895            restart_policy,
896            None,
897            None,
898            None,
899        )
900    }
901
902    pub(crate) fn spawn_linked(
903        &self,
904        function: u32,
905        args: &[Value],
906        restart_policy: RestartPolicy,
907        supervisor: SupervisorLink,
908    ) -> Result<FlowHandle, SpawnError> {
909        spawn_on(
910            &self.shared,
911            &self.chunk,
912            &self.natives,
913            function,
914            args,
915            restart_policy,
916            Some(supervisor),
917            None,
918            None,
919        )
920    }
921
922    pub fn metrics(&self) -> RuntimeMetricsSnapshot {
923        self.shared.metrics.snapshot()
924    }
925
926    pub(crate) fn request_kill(&self, id: FlowId, reason: super::monitor::FlowExitReason) {
927        super::finalize::request_kill(&self.shared, id, reason);
928    }
929}
930
931#[cfg(test)]
932mod tests {
933    use super::*;
934    use crate::bytecode::{builder::ChunkBuilder, Opcode, Value};
935    use crate::scheduler::FlowOutcome;
936    use std::time::Duration;
937
938    fn add_chunk() -> Chunk {
939        let mut b = ChunkBuilder::new("test");
940        b.begin_function("main", 0, 2);
941        b.emit_load_imm(0, 41);
942        b.emit_load_imm(1, 1);
943        b.emit_binop(Opcode::Add, 0, 0, 1);
944        b.emit_return(0);
945        b.finish()
946    }
947
948    /// Sleeps `millis` inside the flow, then returns 7. The sleep is what
949    /// makes "still running" an observable state from the host thread.
950    fn sleep_then_return_chunk(millis: i32) -> Chunk {
951        let mut b = ChunkBuilder::new("test");
952        b.begin_function("main", 0, 2);
953        b.emit_load_imm(0, millis);
954        b.emit_sleep(0);
955        b.emit_load_imm(0, 7);
956        b.emit_return(0);
957        b.finish()
958    }
959
960    /// The host thread must be able to ask "done yet?" and to wait under a
961    /// bound *it* chooses, instead of surrendering itself to `join` for
962    /// however long the bytecode decides to take.
963    #[test]
964    fn polling_and_bounded_waits_never_commit_the_host_thread() -> Result<(), Box<dyn std::error::Error>>
965    {
966        const FLOW_SLEEP: i32 = 150;
967        let rt = Runtime::with_config(
968            sleep_then_return_chunk(FLOW_SLEEP),
969            RuntimeConfig {
970                workers: 1,
971                quantum: 1_000,
972                mailbox: MailboxConfig::DEFAULT,
973                ..Default::default()
974            },
975        )?;
976        let handle = rt.spawn(0, &[])?;
977
978        // The flow cannot possibly be finished yet: it has to be picked up
979        // and then sleep. A poll must say so without waiting.
980        if let Some(outcome) = handle.try_join() {
981            rt.shutdown();
982            return Err(format!("try_join answered too early: {outcome:?}").into());
983        }
984
985        // A bound well below the flow's sleep must expire and hand control
986        // back, not block until the flow happens to finish.
987        if let Some(outcome) = handle.join_timeout(Duration::from_millis(20)) {
988            rt.shutdown();
989            return Err(format!("join_timeout answered too early: {outcome:?}").into());
990        }
991
992        // A generous bound collects the real outcome through the same
993        // (non-consuming) handle.
994        let outcome = handle.join_timeout(Duration::from_secs(10));
995        rt.shutdown();
996        match outcome {
997            Some(FlowOutcome::Completed(Value::Int(7))) => Ok(()),
998            other => Err(format!("unexpected outcome: {other:?}").into()),
999        }
1000    }
1001
1002    #[test]
1003    fn spawn_and_join_add() -> Result<(), Box<dyn std::error::Error>> {
1004        let rt = Runtime::with_config(
1005            add_chunk(),
1006            RuntimeConfig {
1007                workers: 1,
1008                quantum: 1_000,
1009                mailbox: MailboxConfig::DEFAULT,
1010                ..Default::default()
1011            },
1012        )?;
1013        let outcome = rt.spawn(0, &[])?.join();
1014        rt.shutdown();
1015        match outcome {
1016            FlowOutcome::Completed(Value::Int(42)) => Ok(()),
1017            other => Err(format!("unexpected outcome: {other:?}").into()),
1018        }
1019    }
1020
1021    fn receive_forever_chunk() -> Chunk {
1022        let mut b = ChunkBuilder::new("recv");
1023        b.begin_function("main", 0, 1);
1024        b.emit_receive(0);
1025        b.emit_return(0);
1026        b.finish()
1027    }
1028
1029    #[test]
1030    fn kill_parked_flow_joins_failed() -> Result<(), Box<dyn std::error::Error>> {
1031        let rt = Runtime::with_config(
1032            receive_forever_chunk(),
1033            RuntimeConfig {
1034                workers: 1,
1035                quantum: 1_000,
1036                mailbox: MailboxConfig::DEFAULT,
1037                ..Default::default()
1038            },
1039        )?;
1040        let handle = rt.spawn(0, &[])?;
1041        rt.kill(handle.id())?;
1042        let outcome = handle.join();
1043        rt.shutdown();
1044        assert!(
1045            matches!(outcome, FlowOutcome::Failed(_)),
1046            "kill must fail the joiner, got {outcome:?}"
1047        );
1048        Ok(())
1049    }
1050
1051    #[test]
1052    fn max_flows_rejects_extra_spawn() -> Result<(), Box<dyn std::error::Error>> {
1053        let rt = Runtime::with_config(
1054            receive_forever_chunk(),
1055            RuntimeConfig {
1056                workers: 1,
1057                quantum: 1_000,
1058                mailbox: MailboxConfig::DEFAULT,
1059                max_flows: 1,
1060                ..Default::default()
1061            },
1062        )?;
1063        let first = rt.spawn(0, &[])?;
1064        let second = rt.spawn(0, &[]);
1065        rt.kill(first.id())?;
1066        let _ = first.join();
1067        rt.shutdown();
1068        match second {
1069            Err(SpawnError::FlowLimit { current, max }) => {
1070                assert_eq!(current, 1);
1071                assert_eq!(max, 1);
1072                Ok(())
1073            }
1074            other => Err(format!(
1075                "expected FlowLimit, got {}",
1076                match &other {
1077                    Ok(_) => "Ok(handle)".into(),
1078                    Err(e) => format!("Err({e})"),
1079                }
1080            )
1081            .into()),
1082        }
1083    }
1084
1085    #[cfg(feature = "jit")]
1086    #[test]
1087    fn runtime_with_jit_enabled_completes_add() -> Result<(), Box<dyn std::error::Error>> {
1088        use crate::JitConfig;
1089
1090        let rt = Runtime::with_config(
1091            add_chunk(),
1092            RuntimeConfig {
1093                workers: 1,
1094                quantum: 1_000,
1095                mailbox: MailboxConfig::DEFAULT,
1096                jit: JitConfig {
1097                    enabled: true,
1098                    hot_threshold: 1,
1099                },
1100                ..Default::default()
1101            },
1102        )?;
1103        let outcome = rt.spawn(0, &[])?.join();
1104        rt.shutdown();
1105        match outcome {
1106            FlowOutcome::Completed(Value::Int(42)) => Ok(()),
1107            other => Err(format!("unexpected outcome: {other:?}").into()),
1108        }
1109    }
1110}