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