Skip to main content

miden_assembly_syntax/parser/
error.rs

1// Allow unused assignments - required by miette::Diagnostic derive macro
2#![allow(unused_assignments)]
3
4use alloc::{string::String, sync::Arc, vec::Vec};
5use core::{fmt, ops::Range};
6
7use miden_debug_types::{SourceId, SourceSpan};
8use miden_utils_diagnostics::{Diagnostic, miette};
9
10// LITERAL ERROR KIND
11// ================================================================================================
12
13#[derive(Debug, Copy, Clone, PartialEq, Eq)]
14pub enum LiteralErrorKind {
15    /// The input was empty
16    Empty,
17    /// The input contained an invalid digit
18    InvalidDigit,
19    /// The value overflows `u32::MAX`
20    U32Overflow,
21    /// The value overflows `Felt::ORDER_U64`
22    FeltOverflow,
23    /// The value was expected to be a value < 63
24    InvalidBitSize,
25}
26
27impl fmt::Display for LiteralErrorKind {
28    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29        match self {
30            Self::Empty => f.write_str("input was empty"),
31            Self::InvalidDigit => f.write_str("invalid digit"),
32            Self::U32Overflow => f.write_str("value overflowed the u32 range"),
33            Self::FeltOverflow => f.write_str("value overflowed the field modulus"),
34            Self::InvalidBitSize => {
35                f.write_str("expected value to be a valid bit size, e.g. 0..63")
36            },
37        }
38    }
39}
40
41// HEX ERROR KIND
42// ================================================================================================
43
44#[derive(Debug, Copy, Clone, PartialEq, Eq)]
45pub enum HexErrorKind {
46    /// Expected two hex digits for every byte, but had fewer than that
47    MissingDigits,
48    /// Valid hex-encoded integers are expected to come in sizes of 8, 16, or 64 digits,
49    /// but the input consisted of an invalid number of digits.
50    Invalid,
51    /// Occurs when a hex-encoded value overflows `Felt::ORDER_U64`, the maximum integral value
52    Overflow,
53    /// Occurs when the hex-encoded value is > 64 digits
54    TooLong,
55}
56
57impl fmt::Display for HexErrorKind {
58    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59        match self {
60            Self::MissingDigits => {
61                f.write_str("expected number of hex digits to be a multiple of 2")
62            },
63            Self::Invalid => f.write_str("expected 2, 4, 8, 16, or 64 hex digits"),
64            Self::Overflow => f.write_str("value overflowed the field modulus"),
65            Self::TooLong => f.write_str(
66                "value has too many digits, long hex strings must contain exactly 64 digits",
67            ),
68        }
69    }
70}
71
72// BINARY ERROR KIND
73// ================================================================================================
74
75#[derive(Debug, Copy, Clone, PartialEq, Eq)]
76pub enum BinErrorKind {
77    /// Occurs when the bin-encoded value is > 32 digits
78    TooLong,
79}
80
81impl fmt::Display for BinErrorKind {
82    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83        match self {
84            Self::TooLong => f.write_str(
85                "value has too many digits, binary string can contain no more than 32 digits",
86            ),
87        }
88    }
89}
90
91// PARSING ERROR
92// ================================================================================================
93
94#[derive(Debug, Default, thiserror::Error, Diagnostic)]
95#[repr(u8)]
96pub enum ParsingError {
97    #[default]
98    #[error("parsing failed due to unexpected input")]
99    #[diagnostic()]
100    Failed = 0,
101    #[error("expected input to be valid utf8, but invalid byte sequences were found")]
102    #[diagnostic()]
103    InvalidUtf8 {
104        #[label("invalid byte sequence starts here")]
105        span: SourceSpan,
106    },
107    #[error(
108        "expected input to be valid utf8, but end-of-file was reached before final codepoint was read"
109    )]
110    #[diagnostic()]
111    IncompleteUtf8 {
112        #[label("the codepoint starting here is incomplete")]
113        span: SourceSpan,
114    },
115    #[error("invalid syntax")]
116    #[diagnostic()]
117    InvalidToken {
118        #[label("occurs here")]
119        span: SourceSpan,
120    },
121    #[error("invalid syntax: {message}")]
122    #[diagnostic()]
123    InvalidSyntax {
124        #[label("{message}")]
125        span: SourceSpan,
126        message: String,
127    },
128    #[error("invalid syntax")]
129    #[diagnostic(help("expected {}", expected.as_slice().join(", or ")))]
130    UnrecognizedToken {
131        #[label("found a {token} here")]
132        span: SourceSpan,
133        token: String,
134        expected: Vec<String>,
135    },
136    #[error("unexpected trailing tokens")]
137    #[diagnostic()]
138    ExtraToken {
139        #[label("{token} was found here, but was not expected")]
140        span: SourceSpan,
141        token: String,
142    },
143    #[error("unexpected end of file")]
144    #[diagnostic(help("expected {}", expected.as_slice().join(", or ")))]
145    UnrecognizedEof {
146        #[label("reached end of file here")]
147        span: SourceSpan,
148        expected: Vec<String>,
149    },
150    #[error("{error}")]
151    #[diagnostic(help(
152        "bare identifiers must be lowercase alphanumeric with '_', quoted identifiers can include any graphical character"
153    ))]
154    InvalidIdentifier {
155        #[source]
156        #[diagnostic(source)]
157        error: crate::ast::IdentError,
158        #[label]
159        span: SourceSpan,
160    },
161    #[error("unclosed quoted identifier")]
162    #[diagnostic()]
163    UnclosedQuote {
164        #[label("no match for quotation mark starting here")]
165        start: SourceSpan,
166    },
167    #[error("too many instructions in a single code block")]
168    #[diagnostic()]
169    CodeBlockTooBig {
170        #[label]
171        span: SourceSpan,
172    },
173    #[error("invalid constant expression: division by zero")]
174    DivisionByZero {
175        #[label]
176        span: SourceSpan,
177    },
178    #[error("doc comment is too large")]
179    #[diagnostic(help("make sure it is less than u16::MAX bytes in length"))]
180    DocsTooLarge {
181        #[label]
182        span: SourceSpan,
183    },
184    #[error("invalid literal: {}", kind)]
185    #[diagnostic()]
186    InvalidLiteral {
187        #[label]
188        span: SourceSpan,
189        kind: LiteralErrorKind,
190    },
191    #[error("invalid literal: {}", kind)]
192    #[diagnostic()]
193    InvalidHexLiteral {
194        #[label]
195        span: SourceSpan,
196        kind: HexErrorKind,
197    },
198    #[error("invalid literal: {}", kind)]
199    #[diagnostic()]
200    InvalidBinaryLiteral {
201        #[label]
202        span: SourceSpan,
203        kind: BinErrorKind,
204    },
205    #[error("invalid MAST root literal")]
206    InvalidMastRoot {
207        #[label]
208        span: SourceSpan,
209    },
210    #[error("invalid library path: {}", message)]
211    InvalidLibraryPath {
212        #[label]
213        span: SourceSpan,
214        message: String,
215    },
216    #[error("invalid immediate: value must be in the range {}..{} (exclusive)", range.start, range.end)]
217    ImmediateOutOfRange {
218        #[label]
219        span: SourceSpan,
220        range: Range<usize>,
221    },
222    #[error("too many procedures in this module")]
223    #[diagnostic()]
224    ModuleTooLarge {
225        #[label]
226        span: SourceSpan,
227    },
228    #[error("too many re-exported procedures in this module")]
229    #[diagnostic()]
230    ModuleTooManyReexports {
231        #[label]
232        span: SourceSpan,
233    },
234    #[error(
235        "too many operands for `push`: tried to push {} elements, but only 16 can be pushed at one time",
236        count
237    )]
238    #[diagnostic()]
239    PushOverflow {
240        #[label]
241        span: SourceSpan,
242        count: usize,
243    },
244    #[error("expected a fully-qualified module path, e.g. `std::u64`")]
245    UnqualifiedImport {
246        #[label]
247        span: SourceSpan,
248    },
249    #[error(
250        "source-level digest re-exports are not supported; re-export a named item with `pub use {{item}} from module`"
251    )]
252    UnnamedReexportOfMastRoot {
253        #[label]
254        span: SourceSpan,
255    },
256    #[error("conflicting attributes for procedure definition")]
257    #[diagnostic()]
258    AttributeConflict {
259        #[label(
260            "conflict occurs because an attribute with the same name has already been defined"
261        )]
262        span: SourceSpan,
263        #[label("previously defined here")]
264        prev: SourceSpan,
265    },
266    #[error("conflicting key-value attributes for procedure definition")]
267    #[diagnostic()]
268    AttributeKeyValueConflict {
269        #[label(
270            "conflict occurs because a key with the same name has already been set in a previous declaration"
271        )]
272        span: SourceSpan,
273        #[label("previously defined here")]
274        prev: SourceSpan,
275    },
276    #[error("invalid Advice Map key")]
277    #[diagnostic()]
278    InvalidAdvMapKey {
279        #[label(
280            "an Advice Map key must be a word, either in 64-character hex format or in array-like format `[f0,f1,f2,f3]`"
281        )]
282        span: SourceSpan,
283    },
284    #[error("invalid slice constant")]
285    #[diagnostic()]
286    InvalidSliceConstant {
287        #[label("slices are only supported over word-sized constants")]
288        span: SourceSpan,
289    },
290    #[error("invalid slice: expected valid range")]
291    #[diagnostic()]
292    InvalidRange {
293        #[label("range used for the word constant slice is malformed: `{range:?}`")]
294        span: SourceSpan,
295        range: Range<usize>,
296    },
297    #[error("invalid slice: expected non-empty range")]
298    #[diagnostic()]
299    EmptySlice {
300        #[label("range used for the word constant slice is empty: `{range:?}`")]
301        span: SourceSpan,
302        range: Range<usize>,
303    },
304    #[error("unrecognized calling convention")]
305    #[diagnostic(help("expected one of: 'fast', 'C', 'wasm', 'canon-lift', or 'canon-lower'"))]
306    UnrecognizedCallConv {
307        #[label]
308        span: SourceSpan,
309    },
310    #[error("invalid struct annotation")]
311    #[diagnostic(help(
312        "expected one of: '@packed', '@packed(N)', '@transparent', '@bigendian', or '@align(N)'"
313    ))]
314    InvalidStructAnnotation {
315        #[label]
316        span: SourceSpan,
317    },
318    #[error("invalid struct representation")]
319    #[diagnostic()]
320    InvalidStructRepr {
321        #[label("{message}")]
322        span: SourceSpan,
323        message: String,
324    },
325    #[error("deprecated instruction: `{instruction}` has been removed")]
326    #[diagnostic(help("use `{}` instead", replacement))]
327    DeprecatedInstruction {
328        #[label("this instruction is no longer supported")]
329        span: SourceSpan,
330        instruction: String,
331        replacement: String,
332    },
333    #[error("invalid procedure @locals attribute")]
334    #[diagnostic()]
335    InvalidLocalsAttr {
336        #[label("{message}")]
337        span: SourceSpan,
338        message: String,
339    },
340    #[error("invalid padding value for the `adv.push_mapvaln` instruction: {padding}")]
341    #[diagnostic(help("valid padding values are 0, 4, and 8"))]
342    InvalidPadValue {
343        #[label]
344        span: SourceSpan,
345        padding: u8,
346    },
347    #[error(
348        "invalid submodule declaration '{name}': could not find module sources at '{directory}/{basename}.masm' or '{directory}/{basename}/mod.masm'"
349    )]
350    UndefinedSubmodule {
351        name: crate::ast::Ident,
352        basename: alloc::boxed::Box<str>,
353        directory: miden_debug_types::Uri,
354        #[label]
355        span: SourceSpan,
356        #[source_code]
357        source_file: Option<Arc<miden_debug_types::SourceFile>>,
358    },
359    #[error(
360        "invalid submodule declaration '{name}': submodules must not have the same name as their parent"
361    )]
362    #[diagnostic(help("occurred while parsing {parent_module_uri}"))]
363    SelfReferentialSubmodule {
364        name: crate::ast::Ident,
365        parent_module_uri: miden_debug_types::Uri,
366        #[label(
367            "module source resolution rules require this declaration to resolve to the current source file"
368        )]
369        span: SourceSpan,
370        #[source_code]
371        source_file: Option<Arc<miden_debug_types::SourceFile>>,
372    },
373    #[error(
374        "conflicting submodule paths detected: '{name}' can be parsed from either '{first}' and '{second}', but not both"
375    )]
376    AmbiguousSubmoduleLocation {
377        name: crate::ast::Ident,
378        first: miden_debug_types::Uri,
379        second: miden_debug_types::Uri,
380        #[label]
381        span: SourceSpan,
382        #[source_code]
383        source_file: Option<Arc<miden_debug_types::SourceFile>>,
384    },
385    #[error(
386        "invalid submodule declaration '{name}': module source '{module_uri}' is already reachable through another submodule declaration"
387    )]
388    #[diagnostic(help("each module source file can only be owned by one module in a module tree"))]
389    DuplicateSubmoduleSource {
390        name: crate::ast::Ident,
391        module_uri: miden_debug_types::Uri,
392        #[label("this declaration resolves to an already visited module source")]
393        span: SourceSpan,
394        #[source_code]
395        source_file: Option<Arc<miden_debug_types::SourceFile>>,
396    },
397}
398
399impl ParsingError {
400    fn tag(&self) -> u8 {
401        // SAFETY: This is safe because we have given this enum a
402        // primitive representation with #[repr(u8)], with the first
403        // field of the underlying union-of-structs the discriminant
404        //
405        // See the section on "accessing the numeric value of the discriminant"
406        // here: https://doc.rust-lang.org/std/mem/fn.discriminant.html
407        unsafe { *<*const _>::from(self).cast::<u8>() }
408    }
409}
410
411impl Eq for ParsingError {}
412
413impl PartialEq for ParsingError {
414    fn eq(&self, other: &Self) -> bool {
415        match (self, other) {
416            (Self::Failed, Self::Failed) => true,
417            (Self::InvalidSyntax { message: l, .. }, Self::InvalidSyntax { message: r, .. }) => {
418                l == r
419            },
420            (Self::InvalidLiteral { kind: l, .. }, Self::InvalidLiteral { kind: r, .. }) => l == r,
421            (Self::InvalidHexLiteral { kind: l, .. }, Self::InvalidHexLiteral { kind: r, .. }) => {
422                l == r
423            },
424            (
425                Self::InvalidLibraryPath { message: l, .. },
426                Self::InvalidLibraryPath { message: r, .. },
427            ) => l == r,
428            (
429                Self::ImmediateOutOfRange { range: l, .. },
430                Self::ImmediateOutOfRange { range: r, .. },
431            ) => l == r,
432            (Self::PushOverflow { count: l, .. }, Self::PushOverflow { count: r, .. }) => l == r,
433            (
434                Self::UnrecognizedToken { token: ltok, expected: lexpect, .. },
435                Self::UnrecognizedToken { token: rtok, expected: rexpect, .. },
436            ) => ltok == rtok && lexpect == rexpect,
437            (Self::ExtraToken { token: ltok, .. }, Self::ExtraToken { token: rtok, .. }) => {
438                ltok == rtok
439            },
440            (
441                Self::UnrecognizedEof { expected: lexpect, .. },
442                Self::UnrecognizedEof { expected: rexpect, .. },
443            ) => lexpect == rexpect,
444            (x, y) => x.tag() == y.tag(),
445        }
446    }
447}
448
449impl ParsingError {
450    pub fn from_utf8_error(source_id: SourceId, err: core::str::Utf8Error) -> Self {
451        let start = u32::try_from(err.valid_up_to()).ok().unwrap_or(u32::MAX);
452        match err.error_len() {
453            None => Self::IncompleteUtf8 { span: SourceSpan::at(source_id, start) },
454            Some(len) => Self::InvalidUtf8 {
455                span: SourceSpan::new(source_id, start..(start + len as u32)),
456            },
457        }
458    }
459}