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