Skip to main content

celox/simulator/
error.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Render a `miette::Diagnostic` to a plain-text string (no ANSI colors).
5///
6/// Uses `GraphicalReportHandler` with `ThemeStyles::none()` so the output is
7/// safe for NAPI / JS error messages.
8pub fn render_diagnostic(diag: &dyn miette::Diagnostic) -> String {
9    use miette::{GraphicalReportHandler, GraphicalTheme, ThemeCharacters, ThemeStyles};
10
11    let theme = GraphicalTheme {
12        characters: ThemeCharacters::unicode(),
13        styles: ThemeStyles::none(),
14    };
15    let handler = GraphicalReportHandler::new_themed(theme)
16        .with_links(false)
17        .with_width(120);
18    let mut buf = String::new();
19    match handler.render_report(&mut buf, diag) {
20        Ok(()) => buf,
21        // Fallback: if rendering fails, use Display
22        Err(_) => diag.to_string(),
23    }
24}
25
26fn render_analyzer_error(error: &veryl_analyzer::AnalyzerError) -> String {
27    let mut rendered = render_diagnostic(error);
28    if let veryl_analyzer::AnalyzerError::UnresolvableGenericExpression { identifier, .. } = error {
29        rendered.push_str("\nhelp: if this is a module `param ");
30        rendered.push_str(identifier);
31        rendered.push_str(
32            ": type`, declare it as a module generic parameter like `module ModuleName::<",
33        );
34        rendered.push_str(identifier);
35        rendered.push_str(": type>` instead");
36    }
37    rendered
38}
39
40/// A non-fatal source diagnostic retained by a successfully built simulator.
41#[derive(Debug)]
42pub enum CompilationWarning {
43    Analyzer(veryl_analyzer::AnalyzerError),
44    Frontend(celox_frontend_veryl::FrontendDiagnostic),
45}
46
47impl fmt::Display for CompilationWarning {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::Analyzer(diagnostic) => diagnostic.fmt(f),
51            Self::Frontend(diagnostic) => diagnostic.fmt(f),
52        }
53    }
54}
55
56impl std::error::Error for CompilationWarning {}
57
58impl miette::Diagnostic for CompilationWarning {
59    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
60        match self {
61            Self::Analyzer(diagnostic) => miette::Diagnostic::code(diagnostic),
62            Self::Frontend(diagnostic) => miette::Diagnostic::code(diagnostic),
63        }
64    }
65
66    fn severity(&self) -> Option<miette::Severity> {
67        match self {
68            Self::Analyzer(diagnostic) => miette::Diagnostic::severity(diagnostic),
69            Self::Frontend(diagnostic) => miette::Diagnostic::severity(diagnostic),
70        }
71    }
72
73    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
74        match self {
75            Self::Analyzer(diagnostic) => miette::Diagnostic::help(diagnostic),
76            Self::Frontend(diagnostic) => miette::Diagnostic::help(diagnostic),
77        }
78    }
79
80    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
81        match self {
82            Self::Analyzer(diagnostic) => miette::Diagnostic::url(diagnostic),
83            Self::Frontend(diagnostic) => miette::Diagnostic::url(diagnostic),
84        }
85    }
86
87    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
88        match self {
89            Self::Analyzer(diagnostic) => miette::Diagnostic::source_code(diagnostic),
90            Self::Frontend(diagnostic) => miette::Diagnostic::source_code(diagnostic),
91        }
92    }
93
94    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
95        match self {
96            Self::Analyzer(diagnostic) => miette::Diagnostic::labels(diagnostic),
97            Self::Frontend(diagnostic) => miette::Diagnostic::labels(diagnostic),
98        }
99    }
100}
101
102/// The specific kind of simulator error.
103#[derive(Debug)]
104pub enum SimulatorErrorKind {
105    FrontendArtifact(crate::FrontendArtifactError),
106    SIRParser(crate::ParserError),
107    Analyzer(Vec<veryl_analyzer::AnalyzerError>),
108    Frontend(Vec<celox_frontend_veryl::FrontendDiagnostic>),
109    Runtime(crate::RuntimeErrorCode),
110    Codegen(CodegenError),
111}
112
113/// Structured failure while preparing executable simulator code.
114#[derive(Debug, Error)]
115#[non_exhaustive]
116pub enum CodegenError {
117    #[cfg(feature = "host-runtime")]
118    #[error("{0}")]
119    Cranelift(
120        #[from]
121        #[source]
122        celox_backend_cranelift::CraneliftError,
123    ),
124
125    #[error("{context}: {source}")]
126    Optimization {
127        context: &'static str,
128        #[source]
129        source: celox_sir_opt::OptimizationError,
130    },
131
132    #[error("{phase}: {source}")]
133    SirVerification {
134        phase: String,
135        #[source]
136        source: celox_sir::verify::SirVerifyError,
137    },
138
139    #[cfg(all(
140        feature = "host-runtime",
141        any(
142            target_arch = "x86_64",
143            feature = "arm64-codegen",
144            target_arch = "aarch64"
145        )
146    ))]
147    #[error("native emission failed: {source}")]
148    NativePipeline {
149        #[source]
150        source: crate::backend::native::emit::ChainedEmitError,
151    },
152
153    #[cfg(all(
154        feature = "host-runtime",
155        any(
156            target_arch = "x86_64",
157            feature = "arm64-codegen",
158            target_arch = "aarch64"
159        )
160    ))]
161    #[error("native emission failed: {source}")]
162    NativeEmission {
163        #[source]
164        source: crate::backend::native::emit::EmitError,
165    },
166
167    #[cfg(all(
168        feature = "host-runtime",
169        any(
170            target_arch = "x86_64",
171            feature = "arm64-codegen",
172            target_arch = "aarch64"
173        )
174    ))]
175    #[error("failed to allocate executable native memory: {source}")]
176    NativeMemory {
177        #[source]
178        source: std::io::Error,
179    },
180
181    #[cfg(feature = "host-runtime")]
182    #[error("WASM {stage} failed: {source}")]
183    Wasm {
184        stage: &'static str,
185        #[source]
186        source: wasmtime::Error,
187    },
188
189    #[cfg(feature = "host-runtime")]
190    #[error("{message}")]
191    Message { message: String },
192
193    /// A compilation pipeline observed its cancellation token and unwound
194    /// before producing a result.
195    #[error("compilation was cancelled")]
196    Cancelled,
197}
198
199#[cfg(feature = "host-runtime")]
200impl CodegenError {
201    pub(crate) fn message(message: impl Into<String>) -> Self {
202        Self::Message {
203            message: message.into(),
204        }
205    }
206}
207
208/// A simulator error that may also carry accumulated compilation warnings.
209#[derive(Debug)]
210pub struct SimulatorError {
211    kind: Box<SimulatorErrorKind>,
212    warnings: Vec<CompilationWarning>,
213}
214
215impl SimulatorError {
216    /// Create a new `SimulatorError` with no warnings.
217    pub fn new(kind: SimulatorErrorKind) -> Self {
218        Self {
219            kind: Box::new(kind),
220            warnings: Vec::new(),
221        }
222    }
223
224    /// Attach warnings to this error.
225    pub fn with_warnings(mut self, warnings: Vec<CompilationWarning>) -> Self {
226        self.warnings = warnings;
227        self
228    }
229
230    /// Returns a reference to the error kind.
231    pub fn kind(&self) -> &SimulatorErrorKind {
232        &self.kind
233    }
234
235    /// Returns accumulated compilation warnings.
236    pub fn warnings(&self) -> &[CompilationWarning] {
237        &self.warnings
238    }
239}
240
241impl fmt::Display for SimulatorError {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        match self.kind.as_ref() {
244            SimulatorErrorKind::FrontendArtifact(error) => write!(f, "{error}")?,
245            SimulatorErrorKind::SIRParser(e) => f.write_str(&render_diagnostic(e))?,
246            SimulatorErrorKind::Analyzer(errors) => {
247                for (i, e) in errors.iter().enumerate() {
248                    if i > 0 {
249                        f.write_str("\n")?;
250                    }
251                    f.write_str(&render_analyzer_error(e))?;
252                }
253            }
254            SimulatorErrorKind::Frontend(diagnostics) => {
255                for (i, diagnostic) in diagnostics.iter().enumerate() {
256                    if i > 0 {
257                        f.write_str("\n")?;
258                    }
259                    f.write_str(&render_diagnostic(diagnostic))?;
260                }
261            }
262            SimulatorErrorKind::Runtime(e) => write!(f, "Runtime error: {e}")?,
263            SimulatorErrorKind::Codegen(error) => write!(f, "JIT Code generation error: {error}")?,
264        }
265        if !self.warnings.is_empty() {
266            f.write_str("\n\n--- warnings ---\n\n")?;
267            for (i, w) in self.warnings.iter().enumerate() {
268                if i > 0 {
269                    f.write_str("\n")?;
270                }
271                f.write_str(&render_diagnostic(w))?;
272            }
273        }
274        Ok(())
275    }
276}
277
278impl std::error::Error for SimulatorError {
279    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
280        match self.kind.as_ref() {
281            SimulatorErrorKind::FrontendArtifact(error) => Some(error),
282            SimulatorErrorKind::SIRParser(e) => Some(e),
283            SimulatorErrorKind::Runtime(e) => Some(e),
284            SimulatorErrorKind::Codegen(error) => Some(error),
285            _ => None,
286        }
287    }
288}
289
290impl From<crate::FrontendArtifactError> for SimulatorError {
291    fn from(error: crate::FrontendArtifactError) -> Self {
292        SimulatorError::new(SimulatorErrorKind::FrontendArtifact(error))
293    }
294}
295
296impl From<crate::RuntimeErrorCode> for SimulatorError {
297    fn from(e: crate::RuntimeErrorCode) -> Self {
298        SimulatorError::new(SimulatorErrorKind::Runtime(e))
299    }
300}
301
302impl From<crate::ParserError> for SimulatorError {
303    fn from(e: crate::ParserError) -> Self {
304        SimulatorError::new(SimulatorErrorKind::SIRParser(e))
305    }
306}
307
308impl From<CodegenError> for SimulatorError {
309    fn from(error: CodegenError) -> Self {
310        SimulatorError::new(SimulatorErrorKind::Codegen(error))
311    }
312}
313
314#[cfg(feature = "host-runtime")]
315impl From<celox_backend_cranelift::CraneliftError> for SimulatorError {
316    fn from(error: celox_backend_cranelift::CraneliftError) -> Self {
317        CodegenError::from(error).into()
318    }
319}
320
321#[cfg(all(test, feature = "host-runtime"))]
322mod tests {
323    use std::error::Error as _;
324
325    use super::{CodegenError, SimulatorError, SimulatorErrorKind};
326
327    #[test]
328    fn codegen_error_is_available_as_the_simulator_error_source() {
329        let error = SimulatorError::from(CodegenError::message("compile worker stopped"));
330
331        assert!(matches!(error.kind(), SimulatorErrorKind::Codegen(_)));
332        assert_eq!(
333            error.to_string(),
334            "JIT Code generation error: compile worker stopped"
335        );
336        assert_eq!(
337            error.source().unwrap().to_string(),
338            "compile worker stopped"
339        );
340    }
341
342    #[test]
343    fn simulator_error_keeps_the_backend_source_chain() {
344        let backend = celox_backend_cranelift::CraneliftError::NativeTarget {
345            message: "unsupported test target",
346        };
347        let error = SimulatorError::from(backend);
348
349        let codegen = error.source().expect("codegen source");
350        assert!(codegen.to_string().contains("native target"));
351        let backend = codegen.source().expect("backend source");
352        assert!(backend.to_string().contains("unsupported test target"));
353    }
354}