Skip to main content

a2l_parse/
error.rs

1//! Typed, exhaustive error taxonomy for A2L parsing.
2//!
3//! The enum is deliberately **exhaustive** (no `#[non_exhaustive]`):
4//! callers match every variant, and a new variant is a semver-minor event
5//! by design — the same policy as `can-core`'s `CanError` and `xcp-core`'s
6//! `XcpError`. Every malformed input resolves to one of these variants,
7//! never a panic.
8use alloc::string::String;
9
10use core::fmt;
11
12/// The single crate-level error for lexing and parsing.
13#[derive(Debug, Clone, PartialEq)]
14pub enum A2lError {
15    /// A quoted string runs to end of input without a closing quote.
16    UnterminatedString {
17        /// Line the string started on (1-based).
18        line: usize,
19    },
20    /// A `/* */` comment runs to end of input without `*/`.
21    UnterminatedComment {
22        /// Line the comment started on (1-based).
23        line: usize,
24    },
25    /// A numeric literal is not a valid number (includes hex literals
26    /// with bad digits and malformed floats).
27    InvalidNumber {
28        /// The offending text.
29        text: String,
30        /// Line the literal starts on (1-based).
31        line: usize,
32    },
33    /// A number that must be a 32-bit ECU address is negative or exceeds
34    /// `u32::MAX`.
35    InvalidAddress {
36        /// The offending value.
37        value: f64,
38        /// Line the literal starts on (1-based).
39        line: usize,
40    },
41    /// Input ended where a token was required.
42    UnexpectedEof {
43        /// What the parser was looking for.
44        context: &'static str,
45    },
46    /// A token of a different kind appeared where one was required.
47    UnexpectedToken {
48        /// What the parser required (human-readable).
49        expected: &'static str,
50        /// What was found.
51        got: String,
52        /// Line of the offending token (1-based).
53        line: usize,
54    },
55    /// A `DATATYPE` keyword outside the ASAP2 data-type set.
56    InvalidDatatype {
57        /// The offending keyword.
58        text: String,
59        /// Line (1-based).
60        line: usize,
61    },
62    /// A CHARACTERISTIC type keyword outside the ASAP2 set (VALUE, CURVE,
63    /// MAP, VAL_BLK, ASCII).
64    InvalidCharType {
65        /// The offending keyword.
66        text: String,
67        /// Line (1-based).
68        line: usize,
69    },
70    /// A COMPU_METHOD conversion type outside the supported set (RAT_FUNC,
71    /// LINEAR, TABLE, IDENTITY/IDENTICAL).
72    InvalidConversionType {
73        /// The offending keyword.
74        text: String,
75        /// Line (1-based).
76        line: usize,
77    },
78    /// A `/begin NAME` block ran to end of input without its `/end NAME`.
79    UnterminatedBlock {
80        /// The block keyword.
81        block: String,
82        /// Line the `/begin` appeared on (1-based).
83        line: usize,
84    },
85    /// A `/end NAME` closed a different block than the innermost open one.
86    BlockMismatch {
87        /// The innermost open block keyword.
88        opened: String,
89        /// The `/end` keyword.
90        closed: String,
91        /// Line of the `/end` (1-based).
92        line: usize,
93    },
94    /// No `/begin PROJECT` block was found at the top level.
95    MissingProject,
96    /// `/begin` blocks nested deeper than the parser's limit
97    /// ([`MAX_DEPTH`](crate::MAX_DEPTH)) — a guard against pathological
98    /// input, never triggered by conforming files.
99    DepthLimitExceeded {
100        /// Line of the offending `/begin` (1-based).
101        line: usize,
102    },
103}
104
105impl A2lError {
106    /// The 1-based line the error was detected on, when applicable.
107    #[must_use]
108    pub fn line(&self) -> Option<usize> {
109        Some(match self {
110            Self::UnterminatedString { line }
111            | Self::UnterminatedComment { line }
112            | Self::InvalidNumber { line, .. }
113            | Self::InvalidAddress { line, .. }
114            | Self::UnexpectedToken { line, .. }
115            | Self::InvalidDatatype { line, .. }
116            | Self::InvalidCharType { line, .. }
117            | Self::InvalidConversionType { line, .. }
118            | Self::UnterminatedBlock { line, .. }
119            | Self::BlockMismatch { line, .. }
120            | Self::DepthLimitExceeded { line } => *line,
121            Self::UnexpectedEof { .. } | Self::MissingProject => return None,
122        })
123    }
124}
125
126impl fmt::Display for A2lError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            Self::UnterminatedString { line } => write!(f, "line {line}: unterminated string"),
130            Self::UnterminatedComment { line } => write!(f, "line {line}: unterminated /* comment"),
131            Self::InvalidNumber { text, line } => {
132                write!(f, "line {line}: invalid number `{text}`")
133            }
134            Self::InvalidAddress { value, line } => {
135                write!(f, "line {line}: value {value} is not a 32-bit ECU address")
136            }
137            Self::UnexpectedEof { context } => {
138                write!(f, "unexpected end of input: expected {context}")
139            }
140            Self::UnexpectedToken {
141                expected,
142                got,
143                line,
144            } => {
145                write!(f, "line {line}: expected {expected}, found `{got}`")
146            }
147            Self::InvalidDatatype { text, line } => {
148                write!(f, "line {line}: unknown ASAP2 datatype `{text}`")
149            }
150            Self::InvalidCharType { text, line } => {
151                write!(f, "line {line}: unknown CHARACTERISTIC type `{text}`")
152            }
153            Self::InvalidConversionType { text, line } => {
154                write!(
155                    f,
156                    "line {line}: unknown COMPU_METHOD conversion type `{text}`"
157                )
158            }
159            Self::UnterminatedBlock { block, line } => {
160                write!(
161                    f,
162                    "line {line}: `/begin {block}` without matching `/end {block}`"
163                )
164            }
165            Self::BlockMismatch {
166                opened,
167                closed,
168                line,
169            } => {
170                write!(f, "line {line}: `/end {closed}` closes `/begin {opened}`")
171            }
172            Self::MissingProject => f.write_str("no `/begin PROJECT` block found"),
173            Self::DepthLimitExceeded { line } => {
174                write!(f, "line {line}: block nesting exceeds the parser limit")
175            }
176        }
177    }
178}
179
180impl core::error::Error for A2lError {}