Skip to main content

celox_frontend_veryl/
error.rs

1use celox_sir::verify::SirVerifyError;
2use celox_slt::{SLTNodeFactsError, scheduler::SchedulerError};
3use thiserror::Error;
4use veryl_analyzer::multi_sources::{MultiSources, Source};
5use veryl_parser::token_range::TokenRange;
6
7/// Source location information for rich error diagnostics.
8#[derive(Debug, Clone)]
9pub struct SourceLocation {
10    pub source: MultiSources,
11    pub span: miette::SourceSpan,
12}
13
14impl SourceLocation {
15    pub fn from_token(token: &TokenRange) -> Self {
16        let path = token.beg.source.to_string();
17        let text = token.beg.source.get_text();
18        Self {
19            source: MultiSources {
20                sources: vec![Source::new(path, text)],
21            },
22            span: token.into(),
23        }
24    }
25
26    fn path(&self) -> Option<&str> {
27        self.source
28            .sources
29            .first()
30            .map(|source| source.path.as_str())
31    }
32}
33
34impl From<celox_frontend_core::SourceLocation> for SourceLocation {
35    fn from(location: celox_frontend_core::SourceLocation) -> Self {
36        Self {
37            source: MultiSources {
38                sources: vec![Source::new(location.path, location.text.to_string())],
39            },
40            span: location.span,
41        }
42    }
43}
44
45impl From<celox_frontend_core::LoweringPhase> for LoweringPhase {
46    fn from(phase: celox_frontend_core::LoweringPhase) -> Self {
47        match phase {
48            celox_frontend_core::LoweringPhase::FfLowering => Self::FfLowering,
49            celox_frontend_core::LoweringPhase::CombLowering => Self::CombLowering,
50            celox_frontend_core::LoweringPhase::SimulatorParser => Self::SimulatorParser,
51        }
52    }
53}
54
55/// Celox-specific source diagnostics produced after Veryl analysis but before
56/// source identities are discarded by lowering.
57#[derive(Error, Debug)]
58pub enum FrontendDiagnostic {
59    #[error("Function output copy-out order is unspecified: {detail}")]
60    UnspecifiedOutputCopyOrder {
61        detail: String,
62        source_location: SourceLocation,
63    },
64
65    #[error("Loop continuation bound is not stable: {detail}")]
66    MutableForBound {
67        detail: String,
68        source_location: SourceLocation,
69    },
70
71    #[error("Loop continuation bound may change while simulation time advances: {detail}")]
72    TimeAdvancingForBound {
73        detail: String,
74        source_location: SourceLocation,
75    },
76
77    #[error("Unable to prove that the loop continuation bound remains unchanged: {detail}")]
78    UnknownForBoundEffect {
79        detail: String,
80        source_location: SourceLocation,
81    },
82}
83
84impl FrontendDiagnostic {
85    pub fn unspecified_output_copy_order(token: &TokenRange, detail: impl Into<String>) -> Self {
86        Self::UnspecifiedOutputCopyOrder {
87            detail: detail.into(),
88            source_location: SourceLocation::from_token(token),
89        }
90    }
91
92    pub fn mutable_for_bound(token: &TokenRange, detail: impl Into<String>) -> Self {
93        Self::MutableForBound {
94            detail: detail.into(),
95            source_location: SourceLocation::from_token(token),
96        }
97    }
98
99    pub fn unknown_for_bound_effect(token: &TokenRange, detail: impl Into<String>) -> Self {
100        Self::UnknownForBoundEffect {
101            detail: detail.into(),
102            source_location: SourceLocation::from_token(token),
103        }
104    }
105
106    pub fn time_advancing_for_bound(token: &TokenRange, detail: impl Into<String>) -> Self {
107        Self::TimeAdvancingForBound {
108            detail: detail.into(),
109            source_location: SourceLocation::from_token(token),
110        }
111    }
112
113    pub fn is_error(&self) -> bool {
114        matches!(self, Self::MutableForBound { .. })
115    }
116
117    fn source_location(&self) -> &SourceLocation {
118        match self {
119            Self::MutableForBound {
120                source_location, ..
121            }
122            | Self::TimeAdvancingForBound {
123                source_location, ..
124            }
125            | Self::UnknownForBoundEffect {
126                source_location, ..
127            }
128            | Self::UnspecifiedOutputCopyOrder {
129                source_location, ..
130            } => source_location,
131        }
132    }
133}
134
135impl miette::Diagnostic for FrontendDiagnostic {
136    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
137        Some(Box::new(match self {
138            Self::UnspecifiedOutputCopyOrder { .. } => "unspecified_output_copy_order",
139            Self::MutableForBound { .. } => "mutable_for_bound",
140            Self::TimeAdvancingForBound { .. } => "time_advancing_for_bound",
141            Self::UnknownForBoundEffect { .. } => "unknown_for_bound_effect",
142        }))
143    }
144
145    fn severity(&self) -> Option<miette::Severity> {
146        Some(if self.is_error() {
147            miette::Severity::Error
148        } else {
149            miette::Severity::Warning
150        })
151    }
152
153    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
154        Some(Box::new(match self {
155            Self::UnspecifiedOutputCopyOrder { .. } => {
156                "use distinct output destinations, then assign them to shared storage in the intended order after the call"
157            }
158            Self::MutableForBound { .. } => {
159                "copy the bound to a value that is not modified by the loop body"
160            }
161            Self::TimeAdvancingForBound { .. } => {
162                "copy the bound to a procedural let before entering the loop"
163            }
164            Self::UnknownForBoundEffect { .. } => {
165                "avoid opaque or time-advancing calls in the loop, or make the bound independent of mutable state"
166            }
167        }))
168    }
169
170    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
171        Some(&self.source_location().source)
172    }
173
174    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
175        let location = self.source_location();
176        let label = match self {
177            Self::UnspecifiedOutputCopyOrder { .. } => "overlapping output arguments in this call",
178            _ => "loop with an unstable continuation bound",
179        };
180        Some(Box::new(std::iter::once(
181            miette::LabeledSpan::new_with_span(Some(label.to_string()), location.span),
182        )))
183    }
184}
185
186/// The compilation phase where an unsupported feature was encountered.
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum LoweringPhase {
189    FfLowering,
190    CombLowering,
191    SimulatorParser,
192}
193
194impl std::fmt::Display for LoweringPhase {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        match self {
197            LoweringPhase::FfLowering => write!(f, "FF lowering"),
198            LoweringPhase::CombLowering => write!(f, "comb lowering"),
199            LoweringPhase::SimulatorParser => write!(f, "simulator parser"),
200        }
201    }
202}
203
204#[derive(Error, Debug)]
205pub enum ParserError {
206    #[error(transparent)]
207    Scheduler(SchedulerError<String>),
208
209    #[error("{error}")]
210    SchedulerWithLocation {
211        error: SchedulerError<String>,
212        source_locations: Vec<SourceLocation>,
213    },
214
215    #[error("Unsupported in {phase}: {feature} [tracking issue #{issue}] ({detail})")]
216    Unsupported {
217        issue: u32,
218        phase: LoweringPhase,
219        feature: &'static str,
220        detail: String,
221        source_location: Option<SourceLocation>,
222    },
223
224    #[error("Illegal in current context: {feature} ({detail})")]
225    IllegalContext {
226        feature: &'static str,
227        detail: String,
228        source_location: Option<SourceLocation>,
229    },
230
231    #[error("Invalid argument binding for `{argument}` in call to function `{function}`: {detail}")]
232    InvalidFunctionArgumentBinding {
233        function: String,
234        argument: String,
235        detail: String,
236        source_location: Option<SourceLocation>,
237    },
238
239    #[error(
240        "Unresolved type width for variable `{variable}` in module `{module}`: \
241             width cannot be determined at compile time (type: {typ})"
242    )]
243    UnresolvedWidth {
244        module: String,
245        variable: String,
246        typ: String,
247        source_location: Option<SourceLocation>,
248    },
249
250    #[error("Top module `{name}` not found in IR")]
251    TopNotFound { name: String },
252
253    #[error("Top module `{name}` is generic and cannot be used as a top-level module")]
254    GenericTop { name: String },
255
256    #[error("SIR verification failed {phase} in {group} unit {unit}: {error}")]
257    SirVerify {
258        phase: &'static str,
259        group: &'static str,
260        unit: usize,
261        #[source]
262        error: SirVerifyError,
263    },
264
265    #[error("SLT verification failed {phase}: {error}")]
266    SltVerify {
267        phase: &'static str,
268        #[source]
269        error: SLTNodeFactsError,
270    },
271
272    #[error("SLT construction failed: {0}")]
273    SltConstruction(#[from] SLTNodeFactsError),
274}
275
276impl From<celox_frontend_core::ParserError> for ParserError {
277    fn from(error: celox_frontend_core::ParserError) -> Self {
278        use celox_frontend_core::ParserError as CoreError;
279        match error {
280            CoreError::Scheduler(error) => Self::Scheduler(error),
281            CoreError::SchedulerWithLocation {
282                error,
283                source_locations,
284            } => Self::SchedulerWithLocation {
285                error,
286                source_locations: source_locations.into_iter().map(Into::into).collect(),
287            },
288            CoreError::Unsupported {
289                issue,
290                phase,
291                feature,
292                detail,
293                source_location,
294            } => Self::Unsupported {
295                issue,
296                phase: phase.into(),
297                feature,
298                detail,
299                source_location: source_location.map(Into::into),
300            },
301            CoreError::IllegalContext {
302                feature,
303                detail,
304                source_location,
305            } => Self::IllegalContext {
306                feature,
307                detail,
308                source_location: source_location.map(Into::into),
309            },
310            CoreError::TopNotFound { name } => Self::TopNotFound { name },
311            CoreError::GenericTop { name } => Self::GenericTop { name },
312            CoreError::SirVerify {
313                phase,
314                group,
315                unit,
316                error,
317            } => Self::SirVerify {
318                phase,
319                group,
320                unit,
321                error,
322            },
323            CoreError::SltVerify { phase, error } => Self::SltVerify { phase, error },
324            CoreError::SltConstruction(error) => Self::SltConstruction(error),
325        }
326    }
327}
328
329impl ParserError {
330    pub fn unsupported(
331        issue: u32,
332        phase: LoweringPhase,
333        feature: &'static str,
334        detail: impl Into<String>,
335        token: Option<&TokenRange>,
336    ) -> Self {
337        ParserError::Unsupported {
338            issue,
339            phase,
340            feature,
341            detail: detail.into(),
342            source_location: token.map(SourceLocation::from_token),
343        }
344    }
345
346    pub fn illegal_context(
347        feature: &'static str,
348        detail: impl Into<String>,
349        token: Option<&TokenRange>,
350    ) -> Self {
351        ParserError::IllegalContext {
352            feature,
353            detail: detail.into(),
354            source_location: token.map(SourceLocation::from_token),
355        }
356    }
357
358    pub fn unresolved_width(
359        module: &veryl_analyzer::ir::Module,
360        var: &veryl_analyzer::ir::Variable,
361        typ: impl Into<String>,
362    ) -> Self {
363        ParserError::UnresolvedWidth {
364            module: module.name.to_string(),
365            variable: var.path.to_string(),
366            typ: typ.into(),
367            source_location: Some(SourceLocation::from_token(&var.token)),
368        }
369    }
370
371    pub fn invalid_function_argument_binding(
372        function: impl Into<String>,
373        argument: impl Into<String>,
374        detail: impl Into<String>,
375        token: Option<&TokenRange>,
376    ) -> Self {
377        ParserError::InvalidFunctionArgumentBinding {
378            function: function.into(),
379            argument: argument.into(),
380            detail: detail.into(),
381            source_location: token.map(SourceLocation::from_token),
382        }
383    }
384}
385
386impl miette::Diagnostic for ParserError {
387    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
388        match self {
389            ParserError::Unsupported { phase, .. } => Some(Box::new(format!(
390                "unsupported_{}",
391                match phase {
392                    LoweringPhase::FfLowering => "ff_lowering",
393                    LoweringPhase::CombLowering => "comb_lowering",
394                    LoweringPhase::SimulatorParser => "simulator_parser",
395                }
396            ))),
397            ParserError::IllegalContext { .. } => Some(Box::new("illegal_context")),
398            ParserError::InvalidFunctionArgumentBinding { .. } => {
399                Some(Box::new("invalid_function_argument_binding"))
400            }
401            ParserError::UnresolvedWidth { .. } => Some(Box::new("unresolved_width")),
402            ParserError::Scheduler(_) | ParserError::SchedulerWithLocation { .. } => {
403                Some(Box::new("scheduler"))
404            }
405            ParserError::TopNotFound { .. } => Some(Box::new("top_not_found")),
406            ParserError::GenericTop { .. } => Some(Box::new("generic_top")),
407            ParserError::SirVerify { .. } => Some(Box::new("sir_verify")),
408            ParserError::SltVerify { .. } | ParserError::SltConstruction(_) => {
409                Some(Box::new("slt_verify"))
410            }
411        }
412    }
413
414    fn severity(&self) -> Option<miette::Severity> {
415        Some(miette::Severity::Error)
416    }
417
418    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
419        let location = match self {
420            ParserError::Unsupported {
421                source_location, ..
422            }
423            | ParserError::IllegalContext {
424                source_location, ..
425            }
426            | ParserError::InvalidFunctionArgumentBinding {
427                source_location, ..
428            }
429            | ParserError::UnresolvedWidth {
430                source_location, ..
431            } => source_location.as_ref(),
432            ParserError::SchedulerWithLocation {
433                source_locations, ..
434            } => source_locations.first(),
435            _ => None,
436        };
437        location.map(|location| &location.source as &dyn miette::SourceCode)
438    }
439
440    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
441        let location = match self {
442            ParserError::Unsupported {
443                source_location, ..
444            }
445            | ParserError::IllegalContext {
446                source_location, ..
447            }
448            | ParserError::InvalidFunctionArgumentBinding {
449                source_location, ..
450            }
451            | ParserError::UnresolvedWidth {
452                source_location, ..
453            } => source_location.as_ref(),
454            _ => None,
455        };
456        if let Some(location) = location {
457            return Some(Box::new(std::iter::once(
458                miette::LabeledSpan::new_with_span(
459                    Some("Error location".to_string()),
460                    location.span,
461                ),
462            )));
463        }
464
465        match self {
466            ParserError::SchedulerWithLocation {
467                source_locations, ..
468            } => {
469                let first_path = source_locations.first().and_then(SourceLocation::path)?;
470                let labels = source_locations
471                    .iter()
472                    .filter(move |location| location.path() == Some(first_path))
473                    .map(|location| {
474                        miette::LabeledSpan::new_with_span(
475                            Some("loop participant".to_string()),
476                            location.span,
477                        )
478                    })
479                    .collect::<Vec<_>>();
480                if labels.is_empty() {
481                    None
482                } else {
483                    Some(Box::new(labels.into_iter()))
484                }
485            }
486            _ => None,
487        }
488    }
489}