byteflow/scheduler/error.rs
1//! Infrastructure failures of the scheduler (category C in the error model).
2//!
3//! # Failure taxonomy (Byteflow)
4//!
5//! | Kind | Example | Surface |
6//! |------|---------|---------|
7//! | A — user / API | `spawn` bad function index | `Result<_, SpawnError>` |
8//! | B — flow | `HwError`, native fault | `FlowOutcome::Failed` → Supervisor |
9//! | C — infrastructure | mutex poison, dead worker | [`RuntimeError`] + fail-closed |
10//! | D — invariant | empty frame stack while running | types / `debug_assert` — not `unwrap` |
11//!
12//! **Panic is a runtime bug, not an error-handling mechanism.** Device faults
13//! kill actors; scheduler faults are explicit and fail-closed.
14
15use std::fmt;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// Count of infrastructure faults observed since flow start (Relaxed).
19static FAULTS: AtomicU64 = AtomicU64::new(0);
20
21/// Scheduler / host infrastructure error — not a bytecode flow fault.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum RuntimeError {
24 /// A `Mutex` was poisoned: another thread panicked while holding it.
25 /// Shared tables may be inconsistent; callers must not continue using them.
26 PoisonedLock(&'static str),
27}
28
29impl fmt::Display for RuntimeError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 RuntimeError::PoisonedLock(where_) => {
33 write!(f, "runtime mutex poisoned at {where_}")
34 }
35 }
36 }
37}
38
39impl std::error::Error for RuntimeError {}
40
41/// Record an infrastructure fault (stderr + counter). Does not panic.
42#[cold]
43pub fn report_fault(err: RuntimeError) {
44 FAULTS.fetch_add(1, Ordering::Relaxed);
45 eprintln!("byteflow: {err} — fail-closed");
46}
47
48/// How many [`report_fault`] calls have been made (tests / diagnostics).
49pub fn fault_count() -> u64 {
50 FAULTS.load(Ordering::Relaxed)
51}
52
53/// User-facing spawn / load errors (category A in the taxonomy above).
54///
55/// These used to be `.expect(...)` panics on `Runtime::new` / `spawn` /
56/// OS thread creation. Panic is reserved for *bugs*; a bad function index
57/// or a refused `thread::spawn` is an embedder-visible failure and must
58/// surface as `Result` so the host can recover without taking workers down.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum SpawnError {
61 /// `function` index is outside the runtime chunk's function table.
62 BadFunction { index: u32, table_size: u32 },
63 /// Chunk failed verification before the runtime could start.
64 VerifyFailed(String),
65 /// OS refused to create a worker / timer / supervisor thread.
66 ThreadSpawnFailed(String),
67 /// `Vm::new` failed for a reason other than a bad function index
68 /// (mapped from [`crate::Fault`] via `From`).
69 VmInit(String),
70}
71
72impl fmt::Display for SpawnError {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 SpawnError::BadFunction { index, table_size } => {
76 write!(
77 f,
78 "spawn: function index {index} out of range (table size {table_size})"
79 )
80 }
81 SpawnError::VerifyFailed(msg) => write!(f, "chunk verification failed: {msg}"),
82 SpawnError::ThreadSpawnFailed(msg) => {
83 write!(f, "failed to spawn runtime thread: {msg}")
84 }
85 SpawnError::VmInit(msg) => write!(f, "vm init failed: {msg}"),
86 }
87 }
88}
89
90impl std::error::Error for SpawnError {}
91
92impl From<crate::vm::Fault> for SpawnError {
93 fn from(fault: crate::vm::Fault) -> Self {
94 match fault {
95 crate::vm::Fault::BadFunction { index, table_size } => {
96 SpawnError::BadFunction { index, table_size }
97 }
98 other => SpawnError::VmInit(other.to_string()),
99 }
100 }
101}