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