Skip to main content

molframe_cif/parser/
document.rs

1//! Assembling tokens into a document.
2//!
3//! Two shapes carry data: a tag followed by one value, and a loop header
4//! followed by tags and then rows of values. Everything else is structure.
5//!
6//! A row count that is not a whole multiple of the item count is the single most
7//! common way a hand-edited file breaks, almost always because an unquoted value
8//! contained a space and parsed as two. The finding says so rather than leaving
9//! the reader to work it out.
10
11use crate::document::{Category, CifValue, CifValueRef, DataBlock, Document};
12use crate::lexer::{LexError, Lexer, Quoting};
13use molframe_core::diagnostic::{Code, Diagnostic};
14use molframe_core::io::InputBuffer;
15use molframe_core::span::{ByteSpan, Position};
16use std::borrow::Cow;
17use std::collections::HashSet;
18use std::sync::Arc;
19
20mod state;
21use self::state::ParseState;
22
23/// A document and everything that was wrong with the file it came from.
24pub type ParseResult = Result<(Document, Vec<Diagnostic>), Vec<Diagnostic>>;
25
26/// One parsed CIF scalar borrowing text directly from the input buffer.
27///
28/// This hidden integration type lets domain projections consume parser events
29/// without building a lossless [`Document`] or allocating text per cell.
30#[doc(hidden)]
31#[derive(Clone, Copy, Debug, PartialEq)]
32pub enum CifScalar<'input> {
33    /// Written `.`.
34    Inapplicable,
35    /// Written `?`.
36    Unknown,
37    /// Text borrowing the input buffer.
38    Text(&'input str),
39    /// An exact whole number.
40    Integer(i64),
41    /// A floating-point number.
42    Float(f64),
43}
44
45/// Compact identity of one declared column within the active loop.
46///
47/// Hot sinks resolve this ordinal once from the loop header instead of
48/// comparing category and item strings for every value.
49#[doc(hidden)]
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51#[repr(transparent)]
52pub(crate) struct ColumnId(u32);
53
54impl ColumnId {
55    pub(crate) fn from_position(position: usize) -> Option<Self> {
56        u32::try_from(position).ok().map(Self)
57    }
58
59    /// Zero-based position within the active declaration.
60    #[must_use]
61    pub(crate) const fn position(self) -> usize {
62        self.0 as usize
63    }
64}
65
66impl<'input> From<CifValueRef<'input>> for CifScalar<'input> {
67    fn from(value: CifValueRef<'input>) -> Self {
68        match value {
69            CifValueRef::Inapplicable => Self::Inapplicable,
70            CifValueRef::Unknown => Self::Unknown,
71            CifValueRef::Text(value) => Self::Text(value),
72            CifValueRef::Integer(value) => Self::Integer(value),
73            CifValueRef::Float(value) => Self::Float(value),
74        }
75    }
76}
77
78/// Event consumer for allocation-bounded projections over CIF input.
79///
80/// Category and item names are allocated once per declaration by the parser;
81/// each cell text borrow is valid only for its [`Self::value`] callback. This
82/// deliberately prevents a projection from retaining an input or decoder
83/// buffer and allows the same sink to consume text and binary CIF streams.
84///
85/// ```compile_fail
86/// use molframe_cif::{CifEventSink, CifScalar};
87/// use molframe_core::span::ByteSpan;
88///
89/// struct RetainsText<'a>(Option<&'a str>);
90///
91/// impl CifEventSink for RetainsText<'_> {
92///     type Output = ();
93///
94///     fn block(&mut self, _name: &str) {}
95///
96///     fn value(&mut self, _category: &str, _item: &str, value: CifScalar<'_>, _span: ByteSpan) {
97///         if let CifScalar::Text(text) = value {
98///             self.0 = Some(text);
99///         }
100///     }
101///
102///     fn finish(self) {}
103/// }
104/// ```
105#[doc(hidden)]
106pub trait CifEventSink {
107    /// Completed projection.
108    type Output;
109
110    /// Starts a data block.
111    fn block(&mut self, name: &str);
112
113    /// Whether values from one category need scalar decoding and delivery.
114    fn accepts_category(&self, _category: &str) -> bool {
115        true
116    }
117
118    /// Starts a loop after all of its tags have been collected.
119    fn begin_loop(&mut self, _tags: &[(Box<str>, Box<str>)]) {}
120
121    /// Consumes one typed scalar.
122    fn value(&mut self, category: &str, item: &str, value: CifScalar<'_>, span: ByteSpan);
123
124    /// Completes one scalar or loop row.
125    fn end_row(&mut self) {}
126
127    /// Completes an active loop.
128    fn end_loop(&mut self) {}
129
130    /// Completes the projection.
131    fn finish(self) -> Self::Output;
132}
133
134/// Reads a document.
135///
136/// # Errors
137///
138/// Returns the findings that stopped the read: text that is not valid, a value
139/// or quote that never closed, or a file with no block header.
140pub fn parse(input: &InputBuffer) -> ParseResult {
141    parse_into(input, DocumentSink::all())
142}
143
144/// Parses CIF into a caller-provided event projection.
145///
146/// The same lexer, row state machine, and diagnostics as [`parse`] are used;
147/// only the destination representation differs.
148///
149/// # Errors
150///
151/// Returns the syntax findings that stopped parsing.
152#[doc(hidden)]
153pub fn parse_events<S>(
154    input: &InputBuffer,
155    sink: S,
156) -> Result<(S::Output, Vec<Diagnostic>), Vec<Diagnostic>>
157where
158    S: CifEventSink,
159{
160    parse_into(input, EventAdapter(sink))
161}
162
163struct EventAdapter<S>(S);
164
165impl<'input, S> ValueSink<'input> for EventAdapter<S>
166where
167    S: CifEventSink,
168{
169    type Output = S::Output;
170
171    fn block(&mut self, name: &str) {
172        self.0.block(name);
173    }
174
175    fn begin_loop(&mut self, tags: &[(Box<str>, Box<str>)]) {
176        self.0.begin_loop(tags);
177    }
178
179    fn value(
180        &mut self,
181        _column: ColumnId,
182        category: &str,
183        item: &str,
184        text: &'input str,
185        quoting: Quoting,
186        span: ByteSpan,
187    ) {
188        if !self.0.accepts_category(category) {
189            return;
190        }
191        self.0.value(
192            category,
193            item,
194            CifScalar::from(CifValueRef::parse(text, quoting)),
195            span,
196        );
197    }
198
199    fn end_row(&mut self) {
200        self.0.end_row();
201    }
202
203    fn end_loop(&mut self) {
204        self.0.end_loop();
205    }
206
207    fn finish(self) -> Self::Output {
208        self.0.finish()
209    }
210}
211
212pub(crate) trait ValueSink<'input> {
213    type Output;
214
215    fn block(&mut self, name: &str);
216
217    fn begin_loop(&mut self, _tags: &[(Box<str>, Box<str>)]) {}
218
219    fn value(
220        &mut self,
221        column: ColumnId,
222        category: &str,
223        item: &str,
224        text: &'input str,
225        quoting: Quoting,
226        span: ByteSpan,
227    );
228
229    fn end_row(&mut self) {}
230
231    fn end_loop(&mut self) {}
232
233    /// Offers the text after the first row of a loop for bulk consumption.
234    ///
235    /// `rest` starts at a row boundary. A sink that takes rows from it returns
236    /// how many bytes it consumed, which must end on a row boundary too; the
237    /// parser resumes there as if it had read those rows value by value.
238    /// Declining is always correct.
239    fn bulk_rows(&mut self, _tags: &[(Box<str>, Box<str>)], _rest: &'input str) -> Option<usize> {
240        None
241    }
242
243    fn finish(self) -> Self::Output;
244}
245
246pub(crate) fn parse_into<'input, S>(
247    input: &'input InputBuffer,
248    sink: S,
249) -> Result<(S::Output, Vec<Diagnostic>), Vec<Diagnostic>>
250where
251    S: ValueSink<'input>,
252{
253    let mut lexer = match Lexer::new(input.as_bytes()) {
254        Ok(lexer) => lexer,
255        Err(error) => return Err(vec![lex_finding(error)]),
256    };
257    let mut state = ParseState::new(sink);
258
259    loop {
260        match lexer.next_token() {
261            Ok(Some(spanned)) => {
262                state.token(spanned);
263                if state.offer_bulk {
264                    state.offer_bulk = false;
265                    let consumed = state.sink.bulk_rows(&state.loop_tags, lexer.rest());
266                    if let Some(consumed) = consumed {
267                        lexer.skip(consumed);
268                    }
269                }
270            }
271            Ok(None) => break,
272            Err(error) => {
273                state.findings.push(lex_finding(error));
274                return Err(state.findings.finish());
275            }
276        }
277    }
278    state.finish()
279}
280
281pub(crate) struct DocumentSink {
282    document: Document,
283    texts: TextInterner,
284    keep: fn(&str) -> bool,
285}
286
287impl DocumentSink {
288    fn all() -> Self {
289        Self::with_filter(|_| true)
290    }
291
292    pub(crate) fn with_filter(keep: fn(&str) -> bool) -> Self {
293        Self {
294            document: Document::new(),
295            texts: TextInterner::default(),
296            keep,
297        }
298    }
299
300    pub(crate) fn block(&mut self, name: &str) {
301        self.document.push(DataBlock::new(name));
302    }
303
304    pub(crate) fn value(
305        &mut self,
306        category: &str,
307        item: &str,
308        text: &str,
309        quoting: Quoting,
310        span: ByteSpan,
311    ) {
312        if !(self.keep)(category) {
313            return;
314        }
315        let Some(block) = self.document.last_block_mut() else {
316            return;
317        };
318        block.category_mut(category, span).column_mut(item).push(
319            CifValue::parse_with_text(text, quoting, |text| self.texts.intern(text)),
320            quoting,
321        );
322    }
323
324    pub(crate) fn finish(self) -> Document {
325        self.document
326    }
327
328    pub(crate) const fn document(&self) -> &Document {
329        &self.document
330    }
331}
332
333impl<'input> ValueSink<'input> for DocumentSink {
334    type Output = Document;
335
336    fn block(&mut self, name: &str) {
337        Self::block(self, name);
338    }
339
340    fn value(
341        &mut self,
342        _column: ColumnId,
343        category: &str,
344        item: &str,
345        text: &'input str,
346        quoting: Quoting,
347        span: ByteSpan,
348    ) {
349        Self::value(self, category, item, text, quoting, span);
350    }
351
352    fn finish(self) -> Self::Output {
353        Self::finish(self)
354    }
355}
356
357/// Splits an item name into its category and its item.
358///
359/// A name without a dot belongs to no category, which older files use and which
360/// is kept under its own name rather than discarded.
361#[must_use]
362pub fn split_tag(tag: &str) -> (&str, &str) {
363    let body = match tag.strip_prefix('_') {
364        Some(body) => body,
365        None => tag,
366    };
367    match body.split_once('.') {
368        Some((category, item)) => (category, item),
369        None => (body, body),
370    }
371}
372
373#[derive(Default)]
374struct TextInterner {
375    values: HashSet<Arc<str>>,
376}
377
378impl TextInterner {
379    fn intern(&mut self, text: &str) -> Arc<str> {
380        if let Some(value) = self.values.get(text) {
381            return Arc::clone(value);
382        }
383        let value: Arc<str> = text.into();
384        self.values.insert(Arc::clone(&value));
385        value
386    }
387}
388
389/// Turns a lexer refusal into a finding.
390fn lex_finding(error: LexError) -> Diagnostic {
391    match error {
392        LexError::UnterminatedQuote(at) => Diagnostic::new(Code::E1101).at(ByteSpan::empty(at)),
393        LexError::UnterminatedText(at) => Diagnostic::new(Code::E1102).at(ByteSpan::empty(at)),
394        LexError::NotText => Diagnostic::new(Code::E1201)
395            .with_message("input is not valid text")
396            .at(ByteSpan::empty(Position::START)),
397        LexError::PositionOverflow(at) => Diagnostic::new(Code::E1201)
398            .with_message("input exceeds the supported source-position range")
399            .at(ByteSpan::empty(at)),
400    }
401}
402
403/// The rows of one category, as a cursor a caller can walk.
404///
405/// Reading a category row by row rather than item by item is what the lowering
406/// stage wants, and doing it through one type keeps the absent-item handling in
407/// one place.
408#[derive(Clone, Copy, Debug)]
409pub struct Rows<'a> {
410    category: &'a Category,
411    row: usize,
412}
413
414impl<'a> Rows<'a> {
415    /// Walks the rows of a category.
416    #[must_use]
417    pub const fn new(category: &'a Category) -> Self {
418        Self { category, row: 0 }
419    }
420
421    /// The category being walked.
422    #[must_use]
423    pub const fn category(&self) -> &'a Category {
424        self.category
425    }
426
427    /// The current row number.
428    #[must_use]
429    pub const fn row(&self) -> usize {
430        self.row
431    }
432
433    /// Moves to the next row, returning false at the end.
434    pub fn advance(&mut self) -> bool {
435        self.row += 1;
436        self.row < self.category.row_count()
437    }
438
439    /// One value of the current row, as text.
440    #[must_use]
441    pub fn text(&self, item: &str) -> Option<&'a str> {
442        self.category.value(item, self.row)?.as_str()
443    }
444
445    /// One value of the current row, as an identifier.
446    ///
447    /// Use this wherever the value names something rather than measures it: the
448    /// format writes entity numbers and many chain labels as bare numerals, and
449    /// reading those only as text loses them.
450    #[must_use]
451    pub fn identifier(&self, item: &str) -> Option<Cow<'a, str>> {
452        self.category.value(item, self.row)?.as_identifier()
453    }
454
455    /// One value of the current row, as a whole number.
456    #[must_use]
457    pub fn integer(&self, item: &str) -> Option<i64> {
458        self.category.value(item, self.row)?.as_integer()
459    }
460
461    /// One value of the current row, as a number.
462    #[must_use]
463    pub fn float(&self, item: &str) -> Option<f64> {
464        self.category.value(item, self.row)?.as_float()
465    }
466
467    /// Whether one value of the current row was recorded at all.
468    #[must_use]
469    pub fn is_recorded(&self, item: &str) -> bool {
470        self.category
471            .value(item, self.row)
472            .is_some_and(CifValue::is_recorded)
473    }
474}
475
476#[cfg(test)]
477#[path = "document_tests.rs"]
478mod tests;