Skip to main content

byteflow/scheduler/
runtime.rs

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