Skip to main content

vyre_foundation/
error.rs

1//! Typed failures for semantic IR transformation and versioned Program wire data.
2
3use thiserror::Error;
4
5/// Result for foundation-owned IR and Program wire operations.
6pub type IrResult<T, E = IrError> = std::result::Result<T, E>;
7
8/// Failure produced by foundation-owned IR transformation or Program wire boundaries.
9#[derive(Debug, Clone, PartialEq, Eq, Error)]
10#[non_exhaustive]
11pub enum IrError {
12    /// A recursive composition cycle was found during operation inlining.
13    #[error(
14        "IR inlining cycle at operation `{op_id}`. Fix: remove the recursive Expr::Call chain or split the recursive algorithm into an explicit bounded Loop."
15    )]
16    InlineCycle {
17        /// The operation identifier that closed the cycle.
18        op_id: String,
19    },
20
21    /// Operation inlining could not resolve an operation id.
22    #[error(
23        "IR inlining could not resolve operation `{op_id}`. Fix: register a Category A operation with this id before lowering or replace the call with inline IR."
24    )]
25    InlineUnknownOp {
26        /// The missing operation identifier.
27        op_id: String,
28    },
29
30    /// Operation inlining rejected an operation that must dispatch separately.
31    #[error(
32        "IR inlining rejected non-inlinable operation `{op_id}`. Fix: this op processes buffer inputs and must be dispatched as a separate kernel, not composed via Expr::Call."
33    )]
34    InlineNonInlinable {
35        /// The operation identifier that cannot be inlined.
36        op_id: String,
37    },
38
39    /// The number of arguments passed to an inlined operation did not match.
40    #[error(
41        "IR inlining argument count mismatch for operation `{op_id}`: expected {expected}, got {got}. Fix: pass exactly one argument for each ReadOnly or Uniform input buffer declared by the callee program."
42    )]
43    InlineArgCountMismatch {
44        /// The operation identifier being expanded.
45        op_id: String,
46        /// The number of arguments the callee expects.
47        expected: usize,
48        /// The number of arguments the caller provided.
49        got: usize,
50    },
51
52    /// The inlined operation never wrote to its declared output buffer.
53    #[error(
54        "IR inlining found no output write for operation `{op_id}`. Fix: Ensure the op's program() body writes to its output buffer at least once."
55    )]
56    InlineNoOutput {
57        /// The operation identifier being expanded.
58        op_id: String,
59    },
60
61    /// The inlined operation declared an invalid number of output buffers.
62    #[error(
63        "IR inlining found {got} declared output buffers for operation `{op_id}`. Fix: mark exactly one result buffer with BufferDecl::output(...)."
64    )]
65    InlineOutputCountMismatch {
66        /// The operation identifier being expanded.
67        op_id: String,
68        /// The actual number of buffers marked as outputs.
69        got: usize,
70    },
71
72    /// Structural validation rejected the Program with typed issues.
73    #[error("IR validation rejected the Program: {issues:?}")]
74    Validation {
75        /// Foundation-owned validation issues in deterministic emission order.
76        issues: Vec<crate::validate::ValidationError>,
77    },
78
79    /// Wire-format payload failed validation checks.
80    #[error(
81        "Wire-format validation failed: {message}. Fix: recompile the frontend program set and ensure the compiler only emits valid instructions."
82    )]
83    WireFormatValidation {
84        /// Human-readable description of the validation failure.
85        message: String,
86    },
87
88    /// target-text lowering failed before a shader could be emitted.
89    #[error(
90        "vyre target-text lowering: {message}. Fix: inspect the Program shape, backend capability report, and emitted shader diagnostics before retrying."
91    )]
92    Lowering {
93        /// Human-readable description of the lowering failure.
94        message: String,
95    },
96
97    /// Wire-format schema version mismatch.
98    #[error(
99        "Wire-format version mismatch: expected {expected}, found {found}. Fix: re-encode with a matching vyre version or upgrade this runtime."
100    )]
101    VersionMismatch {
102        /// The schema version this runtime understands.
103        expected: u32,
104        /// The schema version present on the wire.
105        found: u32,
106    },
107
108    /// Unknown dialect on the wire.
109    #[error(
110        "Unknown dialect `{name}` (requested version `{requested}`). Fix: link the dialect crate providing `{name}` into this runtime or drop the op that uses it before encoding."
111    )]
112    UnknownDialect {
113        /// The dialect identifier on the wire (e.g. `"workgroup"`).
114        name: String,
115        /// The version string the encoder recorded for the dialect.
116        requested: String,
117    },
118
119    /// Unknown op inside a known dialect.
120    #[error(
121        "Unknown op `{op}` in dialect `{dialect}`. Fix: upgrade the runtime to a version that includes this op, or drop the op before encoding."
122    )]
123    UnknownOp {
124        /// The dialect that should contain the op.
125        dialect: String,
126        /// The op identifier that could not be resolved.
127        op: String,
128    },
129}
130
131impl IrError {
132    /// Build a target-text lowering error with actionable guidance.
133    #[must_use]
134    pub fn lowering(message: impl Into<String>) -> Self {
135        Self::Lowering {
136            message: message.into(),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn lowering_helper_contains_fix_hint() {
147        let err = IrError::lowering("buffer too large");
148        let msg = err.to_string();
149        assert!(msg.contains("buffer too large"));
150        assert!(msg.contains("Fix:"));
151    }
152
153    #[test]
154    fn inline_cycle_display() {
155        let err = IrError::InlineCycle {
156            op_id: "math::add".into(),
157        };
158        assert!(err.to_string().contains("math::add"));
159        assert!(err.to_string().contains("cycle"));
160    }
161
162    #[test]
163    fn version_mismatch_display() {
164        let err = IrError::VersionMismatch {
165            expected: 6,
166            found: 5,
167        };
168        let msg = err.to_string();
169        assert!(msg.contains("6"));
170        assert!(msg.contains("5"));
171    }
172
173    #[test]
174    fn unknown_dialect_display() {
175        let err = IrError::UnknownDialect {
176            name: "my-dialect".into(),
177            requested: "1.0".into(),
178        };
179        assert!(err.to_string().contains("my-dialect"));
180    }
181
182    #[test]
183    fn error_is_clone_and_eq() {
184        let a = IrError::lowering("test");
185        let b = a.clone();
186        assert_eq!(a, b);
187    }
188
189    #[test]
190    fn inline_arg_count_mismatch_display() {
191        let err = IrError::InlineArgCountMismatch {
192            op_id: "test::op".into(),
193            expected: 3,
194            got: 1,
195        };
196        let msg = err.to_string();
197        assert!(msg.contains("expected 3"));
198        assert!(msg.contains("got 1"));
199    }
200}