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 {}