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, Clone)]
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    /// A violated invariant of the analyzer IR or of Celox itself. Valid
258    /// source cannot produce this error; it indicates a compiler bug.
259    #[error("Internal compiler error: {feature} ({detail})")]
260    Internal {
261        feature: &'static str,
262        detail: String,
263        source_location: Option<SourceLocation>,
264    },
265
266    #[error("External module `{name}` was not supplied")]
267    MissingExternalModule {
268        name: String,
269        source_location: Option<SourceLocation>,
270    },
271
272    #[error("Invalid memory file input: {detail}")]
273    MemoryFile {
274        detail: String,
275        source_location: Option<SourceLocation>,
276    },
277
278    #[error("Invalid argument binding for `{argument}` in call to function `{function}`: {detail}")]
279    InvalidFunctionArgumentBinding {
280        function: String,
281        argument: String,
282        detail: String,
283        source_location: Option<SourceLocation>,
284    },
285
286    #[error(
287        "Unresolved type width for variable `{variable}` in module `{module}`: \
288             width cannot be determined at compile time (type: {typ})"
289    )]
290    UnresolvedWidth {
291        module: String,
292        variable: String,
293        typ: String,
294        source_location: Option<SourceLocation>,
295    },
296
297    #[error(
298        "DPI-C imports linked to `{name}` have different prototypes: `{first}` and `{second}` \
299         (IEEE 1800-2023 35.5.4)"
300    )]
301    ExternSignatureMismatch {
302        name: String,
303        first: String,
304        second: String,
305    },
306
307    #[error("Top module `{name}` not found in IR")]
308    TopNotFound { name: String },
309
310    #[error("Top module `{name}` is generic and cannot be used as a top-level module")]
311    GenericTop { name: String },
312
313    #[error("SIR verification failed {phase} in {group} unit {unit}: {error}")]
314    SirVerify {
315        phase: &'static str,
316        group: &'static str,
317        unit: usize,
318        #[source]
319        error: SirVerifyError,
320    },
321
322    #[error("SLT verification failed {phase}: {error}")]
323    SltVerify {
324        phase: &'static str,
325        #[source]
326        error: SLTNodeFactsError,
327    },
328
329    #[error("SLT construction failed: {0}")]
330    SltConstruction(#[from] SLTNodeFactsError),
331}
332
333impl From<celox_frontend_core::ParserError> for ParserError {
334    fn from(error: celox_frontend_core::ParserError) -> Self {
335        use celox_frontend_core::ParserError as CoreError;
336        match error {
337            CoreError::Scheduler(error) => Self::Scheduler(error),
338            CoreError::SchedulerWithLocation {
339                error,
340                source_locations,
341            } => Self::SchedulerWithLocation {
342                error,
343                source_locations: source_locations.into_iter().map(Into::into).collect(),
344            },
345            CoreError::Unsupported {
346                issue,
347                phase,
348                feature,
349                detail,
350                source_location,
351            } => Self::Unsupported {
352                issue,
353                phase: phase.into(),
354                feature,
355                detail,
356                source_location: source_location.map(Into::into),
357            },
358            CoreError::IllegalContext {
359                feature,
360                detail,
361                source_location,
362            } => Self::IllegalContext {
363                feature,
364                detail,
365                source_location: source_location.map(Into::into),
366            },
367            CoreError::MemoryFile {
368                detail,
369                source_location,
370            } => Self::MemoryFile {
371                detail,
372                source_location: source_location.map(Into::into),
373            },
374            CoreError::ExternSignatureMismatch {
375                name,
376                first,
377                second,
378            } => Self::ExternSignatureMismatch {
379                name,
380                first,
381                second,
382            },
383            CoreError::TopNotFound { name } => Self::TopNotFound { name },
384            CoreError::GenericTop { name } => Self::GenericTop { name },
385            CoreError::SirVerify {
386                phase,
387                group,
388                unit,
389                error,
390            } => Self::SirVerify {
391                phase,
392                group,
393                unit,
394                error,
395            },
396            CoreError::SltVerify { phase, error } => Self::SltVerify { phase, error },
397            CoreError::SltConstruction(error) => Self::SltConstruction(error),
398        }
399    }
400}
401
402impl ParserError {
403    pub fn unsupported(
404        issue: u32,
405        phase: LoweringPhase,
406        feature: &'static str,
407        detail: impl Into<String>,
408        token: Option<&TokenRange>,
409    ) -> Self {
410        ParserError::Unsupported {
411            issue,
412            phase,
413            feature,
414            detail: detail.into(),
415            source_location: token.map(SourceLocation::from_token),
416        }
417    }
418
419    pub fn illegal_context(
420        feature: &'static str,
421        detail: impl Into<String>,
422        token: Option<&TokenRange>,
423    ) -> Self {
424        ParserError::IllegalContext {
425            feature,
426            detail: detail.into(),
427            source_location: token.map(SourceLocation::from_token),
428        }
429    }
430
431    pub fn internal(
432        feature: &'static str,
433        detail: impl Into<String>,
434        token: Option<&TokenRange>,
435    ) -> Self {
436        ParserError::Internal {
437            feature,
438            detail: detail.into(),
439            source_location: token.map(SourceLocation::from_token),
440        }
441    }
442
443    pub fn memory_file(detail: impl Into<String>, token: Option<&TokenRange>) -> Self {
444        ParserError::MemoryFile {
445            detail: detail.into(),
446            source_location: token.map(SourceLocation::from_token),
447        }
448    }
449
450    pub fn unresolved_width(
451        module: &veryl_analyzer::ir::Module,
452        var: &veryl_analyzer::ir::Variable,
453        typ: impl Into<String>,
454    ) -> Self {
455        ParserError::UnresolvedWidth {
456            module: module.name.to_string(),
457            variable: var.path.to_string(),
458            typ: typ.into(),
459            source_location: Some(SourceLocation::from_token(&var.token)),
460        }
461    }
462
463    pub fn invalid_function_argument_binding(
464        function: impl Into<String>,
465        argument: impl Into<String>,
466        detail: impl Into<String>,
467        token: Option<&TokenRange>,
468    ) -> Self {
469        ParserError::InvalidFunctionArgumentBinding {
470            function: function.into(),
471            argument: argument.into(),
472            detail: detail.into(),
473            source_location: token.map(SourceLocation::from_token),
474        }
475    }
476}
477
478impl miette::Diagnostic for ParserError {
479    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
480        match self {
481            ParserError::Unsupported { phase, .. } => Some(Box::new(format!(
482                "unsupported_{}",
483                match phase {
484                    LoweringPhase::FfLowering => "ff_lowering",
485                    LoweringPhase::CombLowering => "comb_lowering",
486                    LoweringPhase::SimulatorParser => "simulator_parser",
487                }
488            ))),
489            ParserError::IllegalContext { .. } => Some(Box::new("illegal_context")),
490            ParserError::Internal { .. } => Some(Box::new("internal")),
491            ParserError::MissingExternalModule { .. } => Some(Box::new("missing_external_module")),
492            ParserError::MemoryFile { .. } => Some(Box::new("memory_file")),
493            ParserError::InvalidFunctionArgumentBinding { .. } => {
494                Some(Box::new("invalid_function_argument_binding"))
495            }
496            ParserError::UnresolvedWidth { .. } => Some(Box::new("unresolved_width")),
497            ParserError::Scheduler(_) | ParserError::SchedulerWithLocation { .. } => {
498                Some(Box::new("scheduler"))
499            }
500            ParserError::ExternSignatureMismatch { .. } => {
501                Some(Box::new("extern_signature_mismatch"))
502            }
503            ParserError::TopNotFound { .. } => Some(Box::new("top_not_found")),
504            ParserError::GenericTop { .. } => Some(Box::new("generic_top")),
505            ParserError::SirVerify { .. } => Some(Box::new("sir_verify")),
506            ParserError::SltVerify { .. } | ParserError::SltConstruction(_) => {
507                Some(Box::new("slt_verify"))
508            }
509        }
510    }
511
512    fn severity(&self) -> Option<miette::Severity> {
513        Some(miette::Severity::Error)
514    }
515
516    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
517        let location = match self {
518            ParserError::Unsupported {
519                source_location, ..
520            }
521            | ParserError::IllegalContext {
522                source_location, ..
523            }
524            | ParserError::Internal {
525                source_location, ..
526            }
527            | ParserError::MissingExternalModule {
528                source_location, ..
529            }
530            | ParserError::MemoryFile {
531                source_location, ..
532            }
533            | ParserError::InvalidFunctionArgumentBinding {
534                source_location, ..
535            }
536            | ParserError::UnresolvedWidth {
537                source_location, ..
538            } => source_location.as_ref(),
539            ParserError::SchedulerWithLocation {
540                source_locations, ..
541            } => source_locations.first(),
542            _ => None,
543        };
544        location.map(|location| &location.source as &dyn miette::SourceCode)
545    }
546
547    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
548        let location = match self {
549            ParserError::Unsupported {
550                source_location, ..
551            }
552            | ParserError::IllegalContext {
553                source_location, ..
554            }
555            | ParserError::Internal {
556                source_location, ..
557            }
558            | ParserError::MissingExternalModule {
559                source_location, ..
560            }
561            | ParserError::MemoryFile {
562                source_location, ..
563            }
564            | ParserError::InvalidFunctionArgumentBinding {
565                source_location, ..
566            }
567            | ParserError::UnresolvedWidth {
568                source_location, ..
569            } => source_location.as_ref(),
570            _ => None,
571        };
572        if let Some(location) = location {
573            return Some(Box::new(std::iter::once(
574                miette::LabeledSpan::new_with_span(
575                    Some("Error location".to_string()),
576                    location.span,
577                ),
578            )));
579        }
580
581        match self {
582            ParserError::SchedulerWithLocation {
583                source_locations, ..
584            } => {
585                let first_path = source_locations.first().and_then(SourceLocation::path)?;
586                let labels = source_locations
587                    .iter()
588                    .filter(move |location| location.path() == Some(first_path))
589                    .map(|location| {
590                        miette::LabeledSpan::new_with_span(
591                            Some("loop participant".to_string()),
592                            location.span,
593                        )
594                    })
595                    .collect::<Vec<_>>();
596                if labels.is_empty() {
597                    None
598                } else {
599                    Some(Box::new(labels.into_iter()))
600                }
601            }
602            _ => None,
603        }
604    }
605}