Skip to main content

flux_lang/
error.rs

1//! The crate error type. Library code returns [`Result`]; the phases (`parse`/`analyze`/`compile`/
2//! `runtime`) each map onto a variant so diagnostics stay attributable.
3
4/// The result alias for flux-flow.
5pub type Result<T> = std::result::Result<T, FlowError>;
6
7/// A failure in one of the Flux-Lang pipeline phases.
8#[derive(Debug, thiserror::Error)]
9pub enum FlowError {
10    /// The compact syntax or JSON AST could not be parsed.
11    #[error("parse error: {0}")]
12    Parse(String),
13
14    /// The analyzer rejected the AST (unknown op, unbound symbol, illegal expression position,
15    /// invalid name, arity/required-param or scalar type mismatch, unbounded loop, …). Effect
16    /// *authorization* is not the analyzer's job — policy decides allow/deny/approve at dispatch.
17    #[error("analyze error: {0}")]
18    Analyze(String),
19
20    /// The LLM front-end failed to produce a valid AST (after repair attempts).
21    #[error("compile error: {0}")]
22    Compile(String),
23
24    /// A failure while executing a plan.
25    #[error("runtime error: {0}")]
26    Runtime(String),
27
28    /// The host's safety envelope **refused to run** an op — a policy / permission-rule /
29    /// capability-scope / user-approval denial, marked by the host on the dispatch outcome
30    /// ([`OpOutcome::denied`](crate::host::OpOutcome)). Structured (not the in-band error string a
31    /// failed op surfaces as) so [`is_fatal`](Self::is_fatal) can see it: re-attempting a
32    /// deliberate refusal can never succeed and only re-prompts/re-audits the same denial (L-21).
33    #[error("op denied: {0}")]
34    Denied(String),
35
36    /// An error bubbled up from a lower flux layer.
37    #[error(transparent)]
38    Core(#[from] flux_core::Error),
39}
40
41impl FlowError {
42    /// Whether this error is **fatal** — a deliberate refusal (a denied `confirm`, an op
43    /// [`Denied`](Self::Denied) by the host's safety envelope) or a failed invariant
44    /// (`AssertFailed`) — rather than a transient failure. Fatal errors must never be re-attempted
45    /// or silently absorbed by the reliability wrappers: `retry` propagates them immediately, and
46    /// the wrap points (`loop`'s body re-wrap, the composite-op boundary) preserve their structural
47    /// identity instead of stringifying them, so an enclosing `retry` still sees them.
48    pub fn is_fatal(&self) -> bool {
49        matches!(
50            self,
51            FlowError::Denied(_)
52                | FlowError::Core(flux_core::Error::AssertFailed(_))
53                | FlowError::Core(flux_core::Error::ConfirmDenied(_))
54        )
55    }
56}