Skip to main content

step_io/parser/
entity.rs

1use std::collections::BTreeMap;
2
3use crate::parser::lexer::{LexError, Span};
4
5/// A parsed attribute value from a Part 21 entity instance.
6///
7/// Covers every construct allowed in a parameter position by ISO 10303-21.
8#[derive(Debug, Clone, PartialEq)]
9pub enum Attribute {
10    Integer(i64),
11    Real(f64),
12    /// String with outer quotes stripped. The `''` escape is decoded to a
13    /// single `'` by the lexer. Wide-char escapes (`\X\HH`, `\X2\...\X0\`)
14    /// are **not** decoded — that step is deferred to a later stage.
15    String(String),
16    /// Enumeration value without the surrounding dots (e.g. `.T.` → `"T"`).
17    Enum(String),
18    /// Hex-encoded binary without the surrounding quotes.
19    Binary(String),
20    /// Entity reference `#N`.
21    EntityRef(u64),
22    /// Unset / omitted attribute (`$`).
23    Unset,
24    /// Derived attribute (`*`).
25    Derived,
26    /// Parenthesised list of parameters. May be nested.
27    List(Vec<Attribute>),
28    /// Typed parameter such as `LENGTH_MEASURE(1.E-07)`.
29    Typed {
30        type_name: String,
31        value: Box<Attribute>,
32    },
33}
34
35/// A single sub-entity inside a complex entity instance.
36#[derive(Debug, Clone, PartialEq)]
37pub struct RawEntityPart {
38    /// Entity type name (always stored **upper-cased**).
39    pub name: String,
40    pub attributes: Vec<Attribute>,
41}
42
43/// A parsed Part 21 entity instance (one line in the DATA section).
44#[derive(Debug, Clone, PartialEq)]
45pub enum RawEntity {
46    Simple {
47        id: u64,
48        /// Entity type name (always stored **upper-cased**).
49        name: String,
50        attributes: Vec<Attribute>,
51        /// Position of the `#N` token that opened this instance.
52        span: Span,
53    },
54    Complex {
55        id: u64,
56        parts: Vec<RawEntityPart>,
57        /// Position of the `#N` token that opened this instance.
58        span: Span,
59    },
60}
61
62impl RawEntity {
63    /// The numeric identifier (`#N`).
64    #[must_use]
65    pub fn id(&self) -> u64 {
66        match self {
67            Self::Simple { id, .. } | Self::Complex { id, .. } => *id,
68        }
69    }
70
71    /// Source position of the `#N` token.
72    #[must_use]
73    pub fn span(&self) -> Span {
74        match self {
75            Self::Simple { span, .. } | Self::Complex { span, .. } => *span,
76        }
77    }
78}
79
80/// The complete result of parsing a Part 21 file.
81#[derive(Debug)]
82pub struct Graph {
83    /// Identified application protocol (AP family, edition, stage). The raw
84    /// `FILE_SCHEMA` string list is preserved inside [`SchemaId`](crate::parser::SchemaId) itself.
85    pub schema: super::schema::SchemaId,
86    /// Raw HEADER entities (`FILE_DESCRIPTION`, `FILE_NAME`, `FILE_SCHEMA`).
87    pub header: Vec<RawEntity>,
88    /// DATA section entities keyed by their `#N` identifier.
89    /// `BTreeMap` gives deterministic iteration order (useful for the writer stage).
90    pub entities: BTreeMap<u64, RawEntity>,
91    /// P21 edition 3 `REFERENCE` section: `#N -> "<url#anchor>"`. Each entry
92    /// is an entity id resolved externally rather than in the DATA section.
93    /// `BTreeMap` keeps the deterministic id order the writer relies on.
94    pub external_references: BTreeMap<u64, String>,
95    /// P21 edition 3 `ANCHOR` section: `(<name>, #N)` pairs naming an entity
96    /// (in this corpus, always an external reference). Order-preserving.
97    pub anchors: Vec<(String, u64)>,
98    /// Non-fatal issues observed while parsing. Lenient recoveries
99    /// (missing semicolons, empty attribute slots) and P21 edition 3
100    /// sections that step-io does not yet model land here so callers can
101    /// surface them without aborting the parse.
102    pub warnings: Vec<ParseWarning>,
103}
104
105/// Non-fatal issues observed during parsing.
106///
107/// The parser admits a handful of spec-bending inputs to survive
108/// real-world STEP files; each tolerance pushes a [`ParseWarning`] so
109/// downstream stages can surface what was repaired or discarded. The
110/// IR itself never carries the non-standard form — input is normalised
111/// to a spec-conformant shape before it reaches [`Graph`].
112#[derive(Debug, Clone, PartialEq)]
113pub enum ParseWarning {
114    /// A HEADER entity is missing its terminating `;`. The next keyword
115    /// is treated as the start of the next entity; the IR shows no trace.
116    MissingHeaderSemicolon {
117        entity_name: String,
118        span: super::lexer::Span,
119    },
120    /// An attribute position is blank — `(a, , b)` or trailing `(a, )`.
121    /// The slot is normalised to [`Attribute::Unset`] (the spec form `$`).
122    EmptyAttribute { span: super::lexer::Span },
123    /// A P21 edition 3 section was encountered (`ANCHOR`, `REFERENCE`,
124    /// or `SIGNATURE`) and discarded. step-io does not yet model these
125    /// sections in the IR.
126    Ed3SectionDiscarded {
127        section: String,
128        span: super::lexer::Span,
129    },
130}
131
132impl Graph {
133    /// Look up an entity by its `#N` identifier.
134    #[must_use]
135    pub fn get(&self, id: u64) -> Option<&RawEntity> {
136        self.entities.get(&id)
137    }
138}
139
140/// Parse error kinds specific to the Part 21 parser.
141///
142/// Lexer errors are wrapped in [`Error::Lex`]; the remaining variants
143/// describe structural problems detected during parsing.
144#[derive(Debug, Clone, PartialEq)]
145pub enum Error {
146    /// A lexical error bubbled up from the tokenizer.
147    Lex(LexError),
148    /// The token stream ended before the parser expected.
149    UnexpectedEof { expected: &'static str },
150    /// A token was found where a different kind was expected.
151    UnexpectedToken {
152        expected: &'static str,
153        found: super::lexer::TokenKind,
154        span: Span,
155    },
156    /// Two entities share the same `#N` identifier.
157    DuplicateEntityId { id: u64, span: Span },
158    /// A required HEADER entity is missing.
159    MissingHeaderEntity { name: &'static str },
160    /// `FILE_SCHEMA` does not contain a list of strings.
161    MalformedFileSchema { span: Span },
162    /// An attribute appeared in an invalid position (e.g. `$` inside a list).
163    InvalidAttributePosition { span: Span, detail: &'static str },
164    /// A parameter is nested deeper than [`MAX_NESTING_DEPTH`] — guards against a
165    /// stack overflow on adversarially deep `(((…)))` / `A(B(C(…)))` input.
166    ///
167    /// [`MAX_NESTING_DEPTH`]: super::p21::MAX_NESTING_DEPTH
168    NestingTooDeep { span: Span },
169}
170
171impl From<LexError> for Error {
172    fn from(err: LexError) -> Self {
173        Self::Lex(err)
174    }
175}
176
177impl std::fmt::Display for Error {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match self {
180            Self::Lex(inner) => inner.fmt(f),
181            Self::UnexpectedEof { expected } => {
182                write!(
183                    f,
184                    "parse error: unexpected end of file, expected {expected}"
185                )
186            }
187            Self::UnexpectedToken {
188                expected,
189                found,
190                span,
191            } => write!(
192                f,
193                "parse error at line {}, column {}: expected {expected}, found {found:?}",
194                span.line, span.column,
195            ),
196            Self::DuplicateEntityId { id, span } => write!(
197                f,
198                "parse error at line {}, column {}: duplicate entity #{id}",
199                span.line, span.column,
200            ),
201            Self::MissingHeaderEntity { name } => {
202                write!(f, "parse error: missing required HEADER entity {name}")
203            }
204            Self::MalformedFileSchema { span } => write!(
205                f,
206                "parse error at line {}, column {}: malformed FILE_SCHEMA",
207                span.line, span.column,
208            ),
209            Self::InvalidAttributePosition { span, detail } => write!(
210                f,
211                "parse error at line {}, column {}: {detail}",
212                span.line, span.column,
213            ),
214            Self::NestingTooDeep { span } => write!(
215                f,
216                "parse error at line {}, column {}: parameter nesting too deep",
217                span.line, span.column,
218            ),
219        }
220    }
221}
222
223impl std::error::Error for Error {
224    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
225        match self {
226            Self::Lex(inner) => Some(inner),
227            _ => None,
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn raw_entity_id_simple() {
238        let e = RawEntity::Simple {
239            id: 42,
240            name: "TEST".into(),
241            attributes: vec![],
242            span: Span {
243                start: 0,
244                end: 3,
245                line: 1,
246                column: 1,
247            },
248        };
249        assert_eq!(e.id(), 42);
250    }
251
252    #[test]
253    fn raw_entity_id_complex() {
254        let e = RawEntity::Complex {
255            id: 99,
256            parts: vec![],
257            span: Span {
258                start: 0,
259                end: 3,
260                line: 1,
261                column: 1,
262            },
263        };
264        assert_eq!(e.id(), 99);
265    }
266
267    #[test]
268    fn raw_entity_span_accessor() {
269        let span = Span {
270            start: 10,
271            end: 20,
272            line: 3,
273            column: 5,
274        };
275        let e = RawEntity::Simple {
276            id: 1,
277            name: "A".into(),
278            attributes: vec![],
279            span,
280        };
281        assert_eq!(e.span(), span);
282    }
283
284    #[test]
285    fn parse_error_from_lex_error() {
286        let lex_err = LexError {
287            kind: crate::parser::lexer::LexErrorKind::UnexpectedCharacter,
288            span: Span {
289                start: 0,
290                end: 1,
291                line: 1,
292                column: 1,
293            },
294            snippet: "@".into(),
295        };
296        let parse_err = Error::from(lex_err.clone());
297        assert_eq!(parse_err, Error::Lex(lex_err));
298    }
299
300    #[test]
301    fn parse_error_display_delegates_for_lex() {
302        let lex_err = LexError {
303            kind: crate::parser::lexer::LexErrorKind::UnexpectedCharacter,
304            span: Span {
305                start: 0,
306                end: 1,
307                line: 1,
308                column: 1,
309            },
310            snippet: "@".into(),
311        };
312        let parse_err = Error::Lex(lex_err.clone());
313        // Lex variant delegates to LexError's Display — no "parse error:" prefix.
314        assert_eq!(parse_err.to_string(), lex_err.to_string());
315    }
316
317    #[test]
318    fn parse_error_implements_std_error() {
319        fn assert_error<E: std::error::Error>(_: &E) {}
320        let err = Error::UnexpectedEof {
321            expected: "semicolon",
322        };
323        assert_error(&err);
324    }
325
326    #[test]
327    fn entity_graph_get_returns_entity() {
328        let span = Span {
329            start: 0,
330            end: 1,
331            line: 1,
332            column: 1,
333        };
334        let entity = RawEntity::Simple {
335            id: 1,
336            name: "X".into(),
337            attributes: vec![],
338            span,
339        };
340        let mut entities = BTreeMap::new();
341        entities.insert(1, entity.clone());
342        let graph = Graph {
343            schema: super::super::schema::SchemaId::default(),
344            header: vec![],
345            entities,
346            external_references: BTreeMap::new(),
347            anchors: vec![],
348            warnings: vec![],
349        };
350        assert_eq!(graph.get(1), Some(&entity));
351        assert_eq!(graph.get(999), None);
352    }
353}