Skip to main content

openbim_step/
parser.rs

1//! Semantic parser and event/sink interface.
2//!
3//! Parsing is structural: record names and parameters are retained without a
4//! domain schema. Unknown header and data records therefore survive a
5//! parse/write/reparse cycle.
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10use crate::escape;
11use crate::lexer::{Lexer, Token};
12use crate::recovery::{Diagnostic, OnMalformed, ParseOptions, ParseOutcome};
13use crate::references::ReferenceCheck;
14use crate::{
15    DataRecord, DataSection, Exchange, HeaderRecord, HeaderSection, Instance, InstanceId,
16    Parameter, Record, Span, Spanned, StepError, Str,
17};
18
19/// A semantic parser event.
20#[derive(Debug, Clone, PartialEq)]
21pub enum Event<S = Str> {
22    /// Entered `HEADER;`.
23    StartHeader,
24    /// Parsed one header record.
25    HeaderRecord(HeaderRecord<S>),
26    /// Reached the header `ENDSEC;`.
27    EndHeader,
28    /// Entered `DATA;`.
29    StartData,
30    /// Parsed one data record.
31    DataRecord(DataRecord<S>),
32    /// Reached the data `ENDSEC;`.
33    EndData,
34}
35
36/// Consumer for parse events.
37pub trait EventSink<S = Str> {
38    /// Receives one event. Events are delivered in source order.
39    fn event(&mut self, event: Event<S>);
40}
41
42impl<S, F> EventSink<S> for F
43where
44    F: FnMut(Event<S>),
45{
46    fn event(&mut self, event: Event<S>) {
47        self(event);
48    }
49}
50
51/// Parses a physical file into an owned generic exchange structure.
52/// # Errors
53///
54/// Returns [`StepError`] for the wrong
55/// physical-file marker, or a spanned lexical/syntax diagnostic otherwise.
56pub fn parse(input: &[u8]) -> Result<Exchange, StepError> {
57    parse_with(input, ParseOptions::strict()).map(|outcome| outcome.exchange)
58}
59
60/// Parses a physical file under explicit [`ParseOptions`].
61///
62/// With [`OnMalformed::Skip`] an unparsable data record is reported as a
63/// [`Diagnostic`] and the parser resynchronizes on the next record or section
64/// end, so a consumer can load a damaged file and still show exactly what was
65/// dropped. Header structure stays strict under every policy: mandatory header
66/// records are file-level invariants, not recoverable payload.
67/// # Errors
68///
69/// Returns [`StepError`] for the wrong physical-file marker, for any header or
70/// section-structure defect, and for data defects when the policy is
71/// [`OnMalformed::Abort`].
72pub fn parse_with(input: &[u8], options: ParseOptions) -> Result<ParseOutcome, StepError> {
73    #[derive(Default)]
74    struct Builder {
75        header: HeaderSection,
76        data: DataSection,
77    }
78
79    impl EventSink for Builder {
80        fn event(&mut self, event: Event) {
81            match event {
82                Event::HeaderRecord(record) => self.header.records.push(record),
83                Event::DataRecord(record) => self.data.records.push(record),
84                Event::StartHeader | Event::EndHeader | Event::StartData | Event::EndData => {}
85            }
86        }
87    }
88
89    let mut builder = Builder::default();
90    let diagnostics = parse_events_with(input, &mut builder, options)?;
91    // The record array grew by doubling; give the unused tail back once.
92    // A reallocation of the final size, instead of up to half again as
93    // much memory for as long as the exchange lives.
94    builder.data.records.shrink_to_fit();
95    Ok(ParseOutcome {
96        exchange: Exchange {
97            header: builder.header,
98            data: builder.data,
99        },
100        diagnostics,
101    })
102}
103
104/// Parses a physical file and sends semantic records to `sink`.
105///
106/// Unlike [`parse`], this API does not accumulate an [`Exchange`]. It is useful
107/// for import pipelines that index, validate, or transform records as they are
108/// read.
109/// # Errors
110///
111/// Returns the same physical-file and syntax diagnostics as [`parse`].
112pub fn parse_events(input: &[u8], sink: &mut impl EventSink) -> Result<(), StepError> {
113    parse_events_with(input, sink, ParseOptions::strict()).map(|_| ())
114}
115
116/// Streams semantic records under explicit [`ParseOptions`], returning the
117/// non-fatal diagnostics collected on the way.
118/// # Errors
119///
120/// Returns the same physical-file, header, and structure diagnostics as
121/// [`parse_with`].
122pub fn parse_events_with(
123    input: &[u8],
124    sink: &mut impl EventSink,
125    options: ParseOptions,
126) -> Result<Vec<Diagnostic>, StepError> {
127    if !crate::is_step_file(input) {
128        return Err(StepError::not_step("missing ISO-10303-21 marker"));
129    }
130    let mut parser = Parser::new(input);
131    parser.options = options;
132    parser.references = options.check_references.then(ReferenceCheck::default);
133    parser.parse(sink)?;
134    Ok(parser.diagnostics)
135}
136
137/// Streams semantic records whose text borrows from `input` where it can.
138///
139/// Identical to [`parse_events_with`] -- same events, order, diagnostics and
140/// errors -- except that names, numbers, enumerations and binaries are
141/// `Cow::Borrowed` slices of `input` unless the source interrupted them with
142/// an ignored control (TAB, LF, CR, FF), and strings are borrowed unless they
143/// contain an escape or a quote. Name case is preserved as written (the owned
144/// API upper-cases); compare with `eq_ignore_ascii_case`. A consumer that
145/// converts every value anyway skips one allocation per value this way.
146/// # Errors
147///
148/// Returns the same diagnostics as [`parse_events_with`].
149pub fn parse_events_borrowed<'a>(
150    input: &'a [u8],
151    sink: &mut impl EventSink<Cow<'a, str>>,
152    options: ParseOptions,
153) -> Result<Vec<Diagnostic>, StepError> {
154    if !crate::is_step_file(input) {
155        return Err(StepError::not_step("missing ISO-10303-21 marker"));
156    }
157    let mut parser = Parser::new(input);
158    parser.options = options;
159    parser.references = options.check_references.then(ReferenceCheck::default);
160    parser.parse(sink)?;
161    Ok(parser.diagnostics)
162}
163
164/// How the parser turns lexemes into the caller's string type.
165///
166/// [`Str`] is the owned API: names upper-cased, lossy UTF-8, short values
167/// inline, and every distinct long name allocated once per parse and shared. `Cow<'a, str>` borrows
168/// from the input whenever the bytes are valid UTF-8 and need no rewriting,
169/// and keeps name case as written.
170trait Text<'a>: Sized {
171    /// Per-parse state for names.
172    type Names;
173    /// Name state for a whole-file parse.
174    fn names() -> Self::Names;
175    /// Name state for decoding one record, where a lookup table would cost
176    /// more than it saves.
177    fn record_names() -> Self::Names;
178    /// A record, typed-parameter or enumeration name.
179    fn name(bytes: Cow<'a, [u8]>, names: &mut Self::Names) -> Self;
180    /// A number or binary lexeme.
181    fn lexeme(bytes: Cow<'a, [u8]>) -> Self;
182    /// An escaped string body.
183    fn text(raw: Cow<'a, [u8]>) -> Self;
184}
185
186/// Shared upper-cased long names by their bytes as lexed. `None` disables
187/// it. Short names are stored inline and never looked up.
188///
189/// The keys come from the input, so the table keeps std's randomly keyed
190/// hasher: a fixed fast hash would let a crafted file force collisions.
191pub(crate) type NameTable = Option<HashMap<Box<[u8]>, Str>>;
192
193/// Names up to this length fit inline in a [`Str`], so sharing them saves
194/// nothing.
195const INLINE_NAME: usize = 22;
196
197impl<'a> Text<'a> for Str {
198    type Names = NameTable;
199
200    fn names() -> Self::Names {
201        Some(HashMap::new())
202    }
203
204    fn record_names() -> Self::Names {
205        None
206    }
207
208    fn name(bytes: Cow<'a, [u8]>, names: &mut Self::Names) -> Self {
209        let Some(table) = names.as_mut().filter(|_| bytes.len() > INLINE_NAME) else {
210            return upper_str(&bytes);
211        };
212        if let Some(name) = table.get(&*bytes) {
213            return name.clone();
214        }
215        let name = upper_str(&bytes);
216        table.insert(bytes.into_owned().into_boxed_slice(), name.clone());
217        name
218    }
219
220    fn lexeme(bytes: Cow<'a, [u8]>) -> Self {
221        Str::from(&*borrowed_str(bytes))
222    }
223
224    fn text(raw: Cow<'a, [u8]>) -> Self {
225        // Without a quote or backslash there is nothing to decode: the text
226        // is the body itself, copied once instead of through a `String`.
227        if memchr::memchr2(b'\\', b'\'', &raw).is_none() {
228            return Str::from(&*borrowed_str(raw));
229        }
230        Str::from(escape::decode(&raw))
231    }
232}
233
234impl<'a> Text<'a> for Cow<'a, str> {
235    type Names = ();
236
237    fn names() -> Self::Names {}
238
239    fn record_names() -> Self::Names {}
240
241    fn name(bytes: Cow<'a, [u8]>, (): &mut Self::Names) -> Self {
242        borrowed_str(bytes)
243    }
244
245    fn lexeme(bytes: Cow<'a, [u8]>) -> Self {
246        borrowed_str(bytes)
247    }
248
249    fn text(raw: Cow<'a, [u8]>) -> Self {
250        // Without a quote or backslash there is nothing to decode, so the
251        // decoded text is the raw body itself.
252        match raw {
253            Cow::Borrowed(bytes) if memchr::memchr2(b'\\', b'\'', bytes).is_none() => {
254                borrowed_str(Cow::Borrowed(bytes))
255            }
256            raw => Cow::Owned(escape::decode(&raw)),
257        }
258    }
259}
260
261/// Borrows valid UTF-8 as `str`; otherwise the same lossy conversion as the
262/// owned API.
263fn borrowed_str(bytes: Cow<'_, [u8]>) -> Cow<'_, str> {
264    match bytes {
265        // `from_utf8` first: names, numbers and ids are ASCII by
266        // construction, and its validation is much cheaper than the chunked
267        // walk `from_utf8_lossy` does before it can return a borrow. The
268        // result is identical: lossy conversion of valid UTF-8 borrows it.
269        Cow::Borrowed(bytes) => match std::str::from_utf8(bytes) {
270            Ok(text) => Cow::Borrowed(text),
271            Err(_) => String::from_utf8_lossy(bytes),
272        },
273        Cow::Owned(bytes) => Cow::Owned(lexeme_string(Cow::Owned(bytes))),
274    }
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278enum Phase {
279    BeforeStart,
280    BeforeHeader,
281    Header,
282    BeforeData,
283    Data,
284    BeforeEnd,
285    Done,
286}
287
288struct Parser<'a, S: Text<'a>> {
289    input: &'a [u8],
290    lexer: Lexer<'a>,
291    lookahead: Option<Spanned<Token<'a>>>,
292    last_end: usize,
293    phase: Phase,
294    header_records_seen: usize,
295    options: ParseOptions,
296    diagnostics: Vec<Diagnostic>,
297    /// Present only when the caller opted into reference checking.
298    references: Option<ReferenceCheck>,
299    /// Where to stop early; only the parallel driver sets anything else.
300    stop: Stop,
301    /// The offset the parse stopped at, when it stopped as `stop` asked.
302    stopped: Option<usize>,
303    /// Source span of every data record emitted, when collected.
304    record_spans: Option<Vec<Span>>,
305    /// Parameters of the lists being parsed, innermost last. Each list
306    /// pushes onto it from its own base and drains its tail into a `Vec`
307    /// of exactly its length, so no list carries growth slack or pays for
308    /// regrowth, and the stack's own buffer is reused for the whole parse.
309    scratch: Vec<Parameter<S>>,
310    /// Name state; see [`Text::names`].
311    names: S::Names,
312}
313
314/// Where a restricted parse ends. The parallel driver parses the header up
315/// to `DATA;` and then each slice of the data section on its own thread.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub(crate) enum Stop {
318    /// Parse the whole input.
319    Never,
320    /// Stop right after `DATA;`, before the first data record.
321    AfterDataStart,
322    /// Stop when a record ends exactly at this offset.
323    AtOffset(usize),
324}
325
326/// One slice of the data section, parsed on its own.
327pub(crate) struct Chunk {
328    pub(crate) records: Vec<DataRecord>,
329    /// `records[i]` spans `spans[i]`, as the reference check needs.
330    pub(crate) spans: Vec<Span>,
331    pub(crate) diagnostics: Vec<Diagnostic>,
332    /// Whether the slice ended exactly at the requested offset. `false`
333    /// means the split point was not a record boundary for the parser.
334    pub(crate) aligned: bool,
335}
336
337/// Parses the header through `DATA;`. Returns the header records and the
338/// offset just past `DATA;`, or `None` when the parse never reaches a data
339/// section. There are no diagnostics to return: header and structure
340/// defects are fatal under every policy, and recovery and the reference
341/// check only act inside `DATA`.
342pub(crate) fn parse_prefix(
343    input: &[u8],
344    options: ParseOptions,
345) -> Result<Option<(HeaderSection, usize)>, StepError> {
346    let mut header = HeaderSection::default();
347    let mut parser = Parser::new(input);
348    parser.options = options;
349    parser.stop = Stop::AfterDataStart;
350    parser.parse(&mut |event: Event| {
351        if let Event::HeaderRecord(record) = event {
352            header.records.push(record);
353        }
354    })?;
355    debug_assert!(parser.diagnostics.is_empty());
356    Ok(parser.stopped.map(|offset| (header, offset)))
357}
358
359/// Parses the data section from `start`, which must be a record boundary,
360/// to `end` (`None`: to the end of the file, including `ENDSEC` and the end
361/// marker). The parser state at a record boundary is fully determined by
362/// the offset, so this is exactly what a whole-file parse does there.
363pub(crate) fn parse_chunk(
364    input: &[u8],
365    options: ParseOptions,
366    start: usize,
367    end: Option<usize>,
368) -> Result<Chunk, StepError> {
369    let mut records = Vec::new();
370    let mut parser = Parser::new(input);
371    parser.options = options;
372    parser.phase = Phase::Data;
373    parser.enter_data();
374    parser.lexer.resume_at(start);
375    parser.last_end = start;
376    parser.record_spans = Some(Vec::new());
377    parser.stop = end.map_or(Stop::Never, Stop::AtOffset);
378    parser.parse(&mut |event: Event| {
379        if let Event::DataRecord(record) = event {
380            records.push(record);
381        }
382    })?;
383    Ok(Chunk {
384        records,
385        spans: parser.record_spans.take().unwrap_or_default(),
386        diagnostics: parser.diagnostics,
387        aligned: end.is_none() || parser.stopped.is_some(),
388    })
389}
390
391/// Scratch capacity for decoding a single record: records of real files
392/// rarely hold more than this many parameters across their open lists.
393const RECORD_SCRATCH: usize = 32;
394
395/// Parses the one data record that occupies exactly `span`. Between records
396/// the parser's whole state is its offset, so this is what a whole-file parse
397/// produces for that record, and a record that does not end exactly at
398/// `span.end` is an error rather than a different record.
399fn parse_record_at<'a, S: Text<'a>>(
400    input: &'a [u8],
401    span: Span,
402    options: ParseOptions,
403) -> Result<(DataRecord<S>, Vec<Diagnostic>), StepError> {
404    if span.start > span.end || span.end > input.len() {
405        return Err(StepError::invalid_argument(format!(
406            "record span {}..{} is outside the {}-byte input",
407            span.start,
408            span.end,
409            input.len()
410        )));
411    }
412    let mut parser = Parser::new(input);
413    // One allocation that fits the parameters of almost every record,
414    // instead of regrowing the stack from empty for each decoded record.
415    parser.scratch = Vec::with_capacity(RECORD_SCRATCH);
416    parser.names = S::record_names();
417    parser.options = options;
418    parser.phase = Phase::Data;
419    parser.enter_data();
420    parser.lexer.resume_at(span.start);
421    parser.last_end = span.start;
422    let token = parser
423        .next()?
424        .ok_or_else(|| StepError::syntax(span, "expected a data record"))?;
425    let Token::Id(id) = token.value else {
426        return Err(StepError::syntax(token.span, "expected a data record"));
427    };
428    let record = parser.parse_data_record(&id, token.span)?;
429    if parser.lexer.offset() != span.end {
430        return Err(StepError::syntax(
431            Span::new(span.start, parser.lexer.offset()),
432            "data record does not end at the end of its span",
433        ));
434    }
435    parser.take_reals_without_point();
436    Ok((record, parser.diagnostics))
437}
438
439/// [`parse_record_at`] with owned text, as [`parse`] returns it.
440pub(crate) fn decode_owned(
441    input: &[u8],
442    span: Span,
443    options: ParseOptions,
444) -> Result<(DataRecord, Vec<Diagnostic>), StepError> {
445    parse_record_at(input, span, options)
446}
447
448/// [`parse_record_at`] with text borrowed where it can be, as
449/// [`parse_events_borrowed`] returns it.
450pub(crate) fn decode_borrowed(
451    input: &[u8],
452    span: Span,
453    options: ParseOptions,
454) -> Result<(DataRecord<Cow<'_, str>>, Vec<Diagnostic>), StepError> {
455    parse_record_at(input, span, options)
456}
457
458impl<'a, S: Text<'a>> Parser<'a, S> {
459    fn new(input: &'a [u8]) -> Self {
460        Self {
461            input,
462            lexer: Lexer::new(input),
463            lookahead: None,
464            last_end: 0,
465            phase: Phase::BeforeStart,
466            header_records_seen: 0,
467            options: ParseOptions::strict(),
468            diagnostics: Vec::new(),
469            references: None,
470            stop: Stop::Never,
471            stopped: None,
472            record_spans: None,
473            scratch: Vec::new(),
474            names: S::names(),
475        }
476    }
477
478    /// Applies the data-section-only options to the lexer. Called wherever
479    /// the parser starts reading data records, so the header stays strict.
480    fn enter_data(&mut self) {
481        self.lexer
482            .accept_real_without_point(self.options.accept_real_without_point);
483    }
484
485    /// Reports the reals the lexer read without a decimal point since the
486    /// last call, at the end of the record that holds them.
487    #[inline]
488    fn take_reals_without_point(&mut self) {
489        if self.lexer.has_reals_without_point() {
490            self.report_reals_without_point();
491        }
492    }
493
494    #[cold]
495    fn report_reals_without_point(&mut self) {
496        for span in self.lexer.take_reals_without_point() {
497            let token = String::from_utf8_lossy(&self.input[span.start..span.end]);
498            self.diagnostics
499                .push(Diagnostic::real_without_point(span, &token));
500        }
501    }
502
503    // Keeping section dispatch together makes the state-machine transitions auditable.
504    #[allow(clippy::too_many_lines)]
505    fn parse(&mut self, sink: &mut impl EventSink<S>) -> Result<(), StepError> {
506        loop {
507            if let Stop::AtOffset(end) = self.stop {
508                // Between records there is no lookahead, and the lexer sits
509                // just past the last `;`. Landing exactly on `end` is the
510                // only success; passing it means the split point was inside
511                // a record or literal, and the slice is not usable.
512                if self.lookahead.is_none() {
513                    let offset = self.lexer.offset();
514                    if offset == end {
515                        self.stopped = Some(end);
516                        return Ok(());
517                    }
518                    if offset > end {
519                        return Ok(());
520                    }
521                }
522            }
523            let token = match self.next() {
524                Ok(Some(token)) => token,
525                Ok(None) => break,
526                Err(error) => {
527                    // A lexical defect inside DATA is recoverable: the damaged
528                    // bytes belong to one record, not to the file structure.
529                    self.recover_or_fail(error, None)?;
530                    continue;
531                }
532            };
533            if self.phase == Phase::Done {
534                return Err(StepError::syntax(
535                    token.span,
536                    "content after END-ISO-10303-21",
537                ));
538            }
539            match token.value {
540                Token::Name(name) if name.eq_ignore_ascii_case(b"ISO-10303-21") => {
541                    if self.phase != Phase::BeforeStart {
542                        return Err(StepError::syntax(
543                            token.span,
544                            "unexpected ISO-10303-21 marker",
545                        ));
546                    }
547                    self.expect_semicolon("after ISO-10303-21")?;
548                    self.phase = Phase::BeforeHeader;
549                }
550                Token::Name(name) if name.eq_ignore_ascii_case(b"HEADER") => {
551                    if self.phase != Phase::BeforeHeader {
552                        return Err(StepError::syntax(token.span, "unexpected HEADER section"));
553                    }
554                    self.expect_semicolon("after HEADER")?;
555                    self.phase = Phase::Header;
556                    sink.event(Event::StartHeader);
557                }
558                Token::Name(name) if name.eq_ignore_ascii_case(b"DATA") => {
559                    if self.phase != Phase::BeforeData {
560                        return Err(StepError::syntax(token.span, "unexpected DATA section"));
561                    }
562                    self.expect_semicolon("after DATA")?;
563                    self.phase = Phase::Data;
564                    self.enter_data();
565                    sink.event(Event::StartData);
566                    if self.stop == Stop::AfterDataStart {
567                        self.stopped = Some(self.lexer.offset());
568                        return Ok(());
569                    }
570                }
571                Token::Name(name) if name.eq_ignore_ascii_case(b"ENDSEC") => {
572                    self.expect_semicolon("after ENDSEC")?;
573                    match self.phase {
574                        Phase::Header => {
575                            if self.header_records_seen < 3 {
576                                return Err(StepError::syntax(
577                                    token.span,
578                                    "missing mandatory STEP header record",
579                                ));
580                            }
581                            sink.event(Event::EndHeader);
582                            self.phase = Phase::BeforeData;
583                        }
584                        Phase::Data => {
585                            if let Some(check) = self.references.take() {
586                                check.finish(&mut self.diagnostics);
587                            }
588                            sink.event(Event::EndData);
589                            self.phase = Phase::BeforeEnd;
590                        }
591                        _ => {
592                            return Err(StepError::syntax(token.span, "ENDSEC outside a section"));
593                        }
594                    }
595                }
596                Token::Name(name) if name.eq_ignore_ascii_case(b"END-ISO-10303-21") => {
597                    if self.phase != Phase::BeforeEnd {
598                        return Err(StepError::syntax(
599                            token.span,
600                            "unexpected END-ISO-10303-21 marker",
601                        ));
602                    }
603                    self.expect_semicolon("after END-ISO-10303-21")?;
604                    self.phase = Phase::Done;
605                }
606                Token::Name(name) if self.phase == Phase::Header => {
607                    self.validate_header_record(&name, token.span)?;
608                    let parameters = self.parse_arguments()?;
609                    self.expect_semicolon("after header record")?;
610                    sink.event(Event::HeaderRecord(HeaderRecord {
611                        name: S::name(name, &mut self.names),
612                        parameters,
613                    }));
614                }
615                Token::Id(id) if self.phase == Phase::Data => {
616                    let start = token.span.start;
617                    match self.parse_data_record(&id, token.span) {
618                        Ok(record) => {
619                            self.take_reals_without_point();
620                            if let Some(check) = &mut self.references {
621                                let span = Span::new(start, self.last_end);
622                                check.record(&record, span, &mut self.diagnostics);
623                            }
624                            if let Some(spans) = &mut self.record_spans {
625                                spans.push(Span::new(start, self.last_end));
626                            }
627                            sink.event(Event::DataRecord(record));
628                        }
629                        Err(error) => self.recover_or_fail(error, Some(start))?,
630                    }
631                }
632                _ => {
633                    let error = StepError::syntax(
634                        token.span,
635                        format!("unexpected token {:?} in {:?}", token.value, self.phase),
636                    );
637                    self.recover_or_fail(error, Some(token.span.start))?;
638                }
639            }
640        }
641        if self.phase != Phase::Done {
642            let detail = if matches!(self.phase, Phase::Header | Phase::Data) {
643                "unterminated section (expected ENDSEC)"
644            } else {
645                "physical file requires start, HEADER, DATA, and end markers"
646            };
647            return Err(StepError::syntax(self.eof_span(), detail));
648        }
649        Ok(())
650    }
651
652    /// Parses one `#id = ...;` data record, assuming the id token was consumed.
653    fn parse_data_record(&mut self, id: &[u8], id_span: Span) -> Result<DataRecord<S>, StepError> {
654        self.expect_equals()?;
655        let record_token = self.next()?.ok_or_else(|| {
656            StepError::syntax(Span::new(id_span.end, id_span.end), "missing record body")
657        })?;
658        let instance = match record_token.value {
659            Token::Name(name) => Instance::Simple(self.parse_named_record(name)?),
660            Token::OpenParen => {
661                let mut records = Vec::new();
662                loop {
663                    if !self
664                        .peek()?
665                        .is_some_and(|next| next.value != Token::CloseParen)
666                    {
667                        break;
668                    }
669                    let component = self.next()?.ok_or_else(|| {
670                        StepError::syntax(self.eof_span(), "missing complex record")
671                    })?;
672                    let Token::Name(name) = component.value else {
673                        return Err(StepError::syntax(
674                            component.span,
675                            "expected complex record name",
676                        ));
677                    };
678                    records.push(self.parse_named_record(name)?);
679                }
680                let close = self.next()?.ok_or_else(|| {
681                    StepError::syntax(self.eof_span(), "unterminated complex instance")
682                })?;
683                if close.value != Token::CloseParen {
684                    return Err(StepError::syntax(
685                        close.span,
686                        "expected ')' after complex instance",
687                    ));
688                }
689                if records.is_empty() {
690                    return Err(StepError::syntax(
691                        record_token.span,
692                        "complex instance must contain a record",
693                    ));
694                }
695                Instance::Complex(records.into_boxed_slice())
696            }
697            _ => {
698                return Err(StepError::syntax(
699                    record_token.span,
700                    "expected record name or complex instance",
701                ));
702            }
703        };
704        self.expect_semicolon("after data record")?;
705        Ok(DataRecord {
706            id: InstanceId::from_ascii_digits(id).expect("lexer validates instance ids"),
707            instance,
708        })
709    }
710
711    /// Applies the malformed-record policy.
712    ///
713    /// Recovery is deliberately narrow. It applies only inside `DATA`, only
714    /// when the caller opted in, and it always advances the cursor, so a
715    /// damaged file cannot loop. Header and section-structure defects stay
716    /// fatal under every policy: they describe the file, not one payload
717    /// record, and silently continuing past them would produce a model whose
718    /// provenance is unknown.
719    fn recover_or_fail(
720        &mut self,
721        error: StepError,
722        record_start: Option<usize>,
723    ) -> Result<(), StepError> {
724        if self.options.on_malformed_record != OnMalformed::Skip || self.phase != Phase::Data {
725            return Err(error);
726        }
727        let start = record_start.unwrap_or_else(|| error.span().start);
728        // Reals repaired in the record being dropped are not reported: the
729        // skipped-record diagnostic covers their bytes.
730        self.lexer.discard_reals_without_point();
731        // Resynchronize from just past the record's first byte, NOT from the
732        // end of the error span. A diagnostic can legitimately span the token
733        // that follows the damage -- including `ENDSEC` -- and resuming past
734        // it would swallow the section terminator.
735        let resume = self.resync_from(start.saturating_add(1));
736        self.lookahead = None;
737        self.lexer.resume_at(resume);
738        self.last_end = resume;
739        self.diagnostics.push(Diagnostic::skipped_record(
740            Span::new(start, resume),
741            format!("skipped malformed data record: {}", error.detail()),
742        ));
743        Ok(())
744    }
745
746    /// Finds the next byte offset at which parsing can safely restart.
747    ///
748    /// Scans raw bytes rather than tokens because the tokenizer is what
749    /// failed. All three literal kinds -- quoted strings, binary literals, and
750    /// comments -- are tracked, so a `;` or `ENDSEC` inside a literal is not
751    /// mistaken for a record boundary. Scanning a literal's payload as code
752    /// would let recovery fabricate records that were never in the source.
753    /// A section terminator stops the scan *before* it is consumed, so
754    /// recovery can never swallow the end of `DATA`.
755    fn resync_from(&self, from: usize) -> usize {
756        // Which literal the scanner is currently inside. STEP has three, and
757        // all of them can contain bytes that look like record syntax.
758        enum Literal {
759            None,
760            // `'...'`, where `''` is an escaped apostrophe rather than a close.
761            Text,
762            // `"...."`, with no doubling rule: the first `"` closes it.
763            Binary,
764        }
765
766        let mut position = from.min(self.input.len());
767        let mut literal = Literal::None;
768        while position < self.input.len() {
769            let byte = self.input[position];
770            match literal {
771                Literal::Text => {
772                    // Mirror `Lexer::lex_text`: `\\` is one escaped
773                    // backslash, and `\S\` escapes the next byte even when it
774                    // is an apostrophe. Upper case only, as in the lexer.
775                    if self.input[position..].starts_with(br"\\") {
776                        position += 2;
777                        continue;
778                    }
779                    if self.input[position..].starts_with(br"\S\") {
780                        position = (position + 4).min(self.input.len());
781                        continue;
782                    }
783                    if byte == b'\'' {
784                        if self.input.get(position + 1) == Some(&b'\'') {
785                            position += 2;
786                            continue;
787                        }
788                        literal = Literal::None;
789                    }
790                    position += 1;
791                }
792                Literal::Binary => {
793                    if byte == b'"' {
794                        literal = Literal::None;
795                    }
796                    position += 1;
797                }
798                Literal::None => match byte {
799                    b'\'' => {
800                        literal = Literal::Text;
801                        position += 1;
802                    }
803                    b'"' => {
804                        literal = Literal::Binary;
805                        position += 1;
806                    }
807                    b'/' if self.input.get(position + 1) == Some(&b'*') => {
808                        position = self.input[position + 2..]
809                            .windows(2)
810                            .position(|window| window == b"*/")
811                            .map_or(self.input.len(), |offset| position + 2 + offset + 2);
812                    }
813                    b';' => return position + 1,
814                    _ if self.section_end_at(position) => return position,
815                    _ => position += 1,
816                },
817            }
818        }
819        self.input.len()
820    }
821
822    fn section_end_at(&self, position: usize) -> bool {
823        let preceded_by_word = position
824            .checked_sub(1)
825            .and_then(|previous| self.input.get(previous))
826            .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_');
827        !preceded_by_word
828            && self
829                .input
830                .get(position..position + 6)
831                .is_some_and(|bytes| bytes.eq_ignore_ascii_case(b"ENDSEC"))
832    }
833
834    fn validate_header_record(&mut self, name: &[u8], span: Span) -> Result<(), StepError> {
835        const REQUIRED: [&[u8]; 3] = [b"FILE_DESCRIPTION", b"FILE_NAME", b"FILE_SCHEMA"];
836        if let Some(expected) = REQUIRED.get(self.header_records_seen) {
837            if !name.eq_ignore_ascii_case(expected) {
838                return Err(StepError::syntax(
839                    span,
840                    format!(
841                        "expected mandatory {} header record",
842                        String::from_utf8_lossy(expected)
843                    ),
844                ));
845            }
846        } else if REQUIRED
847            .iter()
848            .any(|required| name.eq_ignore_ascii_case(required))
849        {
850            return Err(StepError::syntax(span, "duplicate mandatory header record"));
851        }
852        self.header_records_seen += 1;
853        Ok(())
854    }
855
856    fn parse_named_record(&mut self, name: Cow<'a, [u8]>) -> Result<Record<S>, StepError> {
857        Ok(Record {
858            name: S::name(name, &mut self.names),
859            parameters: self.parse_arguments()?,
860        })
861    }
862
863    fn parse_arguments(&mut self) -> Result<Box<[Parameter<S>]>, StepError> {
864        let token = self
865            .next()?
866            .ok_or_else(|| StepError::syntax(self.eof_span(), "expected '(' after record name"))?;
867        if token.value != Token::OpenParen {
868            return Err(StepError::syntax(
869                token.span,
870                "expected '(' after record name",
871            ));
872        }
873        self.parse_parameter_list(0)
874    }
875
876    /// Parses a list after its `(` into a slice of exactly its length.
877    fn parse_parameter_list(&mut self, depth: usize) -> Result<Box<[Parameter<S>]>, StepError> {
878        let base = self.parse_list_onto_scratch(depth)?;
879        // `Drain` knows its exact length, so this allocates once, exactly,
880        // and the boxed slice keeps that allocation as is.
881        Ok(self.scratch.drain(base..).collect())
882    }
883
884    /// Parses a list's parameters onto the scratch stack and returns the
885    /// list's base there: the list is `scratch[base..]`. Nested lists are
886    /// drained before their parent pushes again, so the stack discipline
887    /// holds. On error the scratch is cut back to `base`, so a recovering
888    /// parse resumes with it clean.
889    fn parse_list_onto_scratch(&mut self, depth: usize) -> Result<usize, StepError> {
890        let base = self.scratch.len();
891        let result = self.parse_list_items(depth);
892        if result.is_err() {
893            self.scratch.truncate(base);
894        }
895        result.map(|()| base)
896    }
897
898    fn parse_list_items(&mut self, depth: usize) -> Result<(), StepError> {
899        if depth > crate::MAX_PARAMETER_NESTING {
900            let span = match self.peek()? {
901                Some(token) => token.span,
902                None => self.eof_span(),
903            };
904            return Err(StepError::syntax(span, "parameter nesting limit exceeded"));
905        }
906        if self
907            .peek()?
908            .is_some_and(|token| token.value == Token::CloseParen)
909        {
910            let _ = self.next()?;
911            return Ok(());
912        }
913        loop {
914            let parameter = self.parse_parameter(depth)?;
915            self.scratch.push(parameter);
916            let separator = self
917                .next()?
918                .ok_or_else(|| StepError::syntax(self.eof_span(), "unterminated parameter list"))?;
919            match separator.value {
920                Token::Comma => {}
921                Token::CloseParen => return Ok(()),
922                _ => {
923                    return Err(StepError::syntax(
924                        separator.span,
925                        "expected ',' or ')' after parameter",
926                    ));
927                }
928            }
929        }
930    }
931
932    fn parse_parameter(&mut self, depth: usize) -> Result<Parameter<S>, StepError> {
933        let token = self
934            .next()?
935            .ok_or_else(|| StepError::syntax(self.eof_span(), "expected parameter"))?;
936        match token.value {
937            Token::Dollar => Ok(Parameter::Null),
938            Token::Star => Ok(Parameter::Derived),
939            Token::Id(id) => Ok(Parameter::Ref(
940                InstanceId::from_ascii_digits(&id).expect("lexer validates instance ids"),
941            )),
942            Token::Integer(value) => Ok(Parameter::Integer(S::lexeme(value))),
943            Token::Real(value) => Ok(Parameter::Real(S::lexeme(value))),
944            Token::Text(raw) => Ok(Parameter::Text(S::text(raw))),
945            Token::Binary(raw) => Ok(Parameter::Binary(S::lexeme(raw))),
946            Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"T") => {
947                Ok(Parameter::Bool(true))
948            }
949            Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"F") => {
950                Ok(Parameter::Bool(false))
951            }
952            Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"U") => {
953                Ok(Parameter::LogicalUnknown)
954            }
955            Token::Keyword(keyword) => Ok(Parameter::Enum(S::name(keyword, &mut self.names))),
956            Token::OpenParen => Ok(Parameter::List(self.parse_parameter_list(depth + 1)?)),
957            Token::Name(name) => {
958                let Some(next) = self.peek()? else {
959                    return Err(StepError::syntax(
960                        self.eof_span(),
961                        "expected '(' after typed parameter name",
962                    ));
963                };
964                if next.value != Token::OpenParen {
965                    return Err(StepError::syntax(
966                        next.span,
967                        "expected '(' after typed parameter name",
968                    ));
969                }
970                let _ = self.next()?;
971                let base = self.parse_list_onto_scratch(depth + 1)?;
972                // A single value is boxed straight off the stack, without
973                // a one-element `Vec` built only to be taken apart again.
974                let value = if self.scratch.len() == base + 1 {
975                    Box::new(self.scratch.pop().expect("the list holds one value"))
976                } else {
977                    Box::new(Parameter::List(self.scratch.drain(base..).collect()))
978                };
979                Ok(Parameter::Typed {
980                    type_name: S::name(name, &mut self.names),
981                    value,
982                })
983            }
984            value => Err(StepError::syntax(
985                token.span,
986                format!("unexpected parameter token {value:?}"),
987            )),
988        }
989    }
990
991    fn expect_equals(&mut self) -> Result<(), StepError> {
992        let token = self
993            .next()?
994            .ok_or_else(|| StepError::syntax(self.eof_span(), "expected '=' after instance id"))?;
995        if token.value == Token::Equals {
996            Ok(())
997        } else {
998            Err(StepError::syntax(
999                token.span,
1000                "expected '=' after instance id",
1001            ))
1002        }
1003    }
1004
1005    fn expect_semicolon(&mut self, context: &str) -> Result<(), StepError> {
1006        let token = self
1007            .next()?
1008            .ok_or_else(|| StepError::syntax(self.eof_span(), format!("expected ';' {context}")))?;
1009        if token.value == Token::Semicolon {
1010            Ok(())
1011        } else {
1012            Err(StepError::syntax(
1013                token.span,
1014                format!("expected ';' {context}"),
1015            ))
1016        }
1017    }
1018
1019    fn next(&mut self) -> Result<Option<Spanned<Token<'a>>>, StepError> {
1020        let token = match self.lookahead.take() {
1021            Some(token) => Some(token),
1022            None => self.lexer.next_spanned()?,
1023        };
1024        if let Some(token) = &token {
1025            self.last_end = token.span.end;
1026        }
1027        Ok(token)
1028    }
1029
1030    fn peek(&mut self) -> Result<Option<&Spanned<Token<'a>>>, StepError> {
1031        if self.lookahead.is_none() {
1032            self.lookahead = self.lexer.next_spanned()?;
1033        }
1034        Ok(self.lookahead.as_ref())
1035    }
1036
1037    fn eof_span(&self) -> Span {
1038        let offset = self.last_end.max(self.lexer.offset());
1039        Span::new(offset, offset)
1040    }
1041}
1042
1043/// An upper-cased name, built without an intermediate `String`. Names reach
1044/// here from the lexer, which admits only ASCII, so the lossy conversion
1045/// never replaces anything.
1046fn upper_str(bytes: &[u8]) -> Str {
1047    let mut buffer = [0u8; 128];
1048    if let Some(stack) = buffer.get_mut(..bytes.len()) {
1049        stack.copy_from_slice(bytes);
1050        stack.make_ascii_uppercase();
1051        if let Ok(text) = std::str::from_utf8(stack) {
1052            return Str::from(text);
1053        }
1054    }
1055    Str::from(String::from_utf8_lossy(bytes).to_ascii_uppercase())
1056}
1057
1058/// A lexeme as an owned `String`, reusing an owned buffer instead of copying.
1059/// Numbers and binaries are ASCII, so the lossy fallback never fires on them.
1060fn lexeme_string(bytes: Cow<'_, [u8]>) -> String {
1061    match bytes {
1062        Cow::Borrowed(bytes) => match std::str::from_utf8(bytes) {
1063            Ok(text) => text.to_owned(),
1064            Err(_) => String::from_utf8_lossy(bytes).into_owned(),
1065        },
1066        Cow::Owned(bytes) => String::from_utf8(bytes)
1067            .unwrap_or_else(|error| String::from_utf8_lossy(error.as_bytes()).into_owned()),
1068    }
1069}