Skip to main content

acdc_parser/
parsed.rs

1//! Self-referential wrappers returned by the public `parse_*` entry points.
2//!
3//! `ParseResult` pins the preprocessed source, the `bumpalo::Bump` arena
4//! that backs parser-allocated strings, the `Document<'_>` AST that borrows
5//! from both, and any warnings the parser produced — all bound together so
6//! callers can hold the AST past the caller's input slice without any
7//! `into_static()` copy. Drop releases the arena, source, and warnings in
8//! one shot. `ParseInlineResult` plays the same role for the inline-only
9//! entry point used by TCK tests and the HTML converter's quotes-only
10//! fallback.
11//!
12//! Consumers reach the AST via `.document()` (or `.inlines()`); bumpalo
13//! never appears in a public signature.
14//!
15//! `OwnedSource` covers the parse-failure case (we have the text but no
16//! AST) without paying for an empty arena.
17
18use std::{cell::RefCell, collections::HashMap, path::PathBuf, rc::Rc};
19
20use bumpalo::Bump;
21
22use crate::{
23    Document, InlineNode, Location, SourceLocation, Warning, WarningKind, model::SourceRange,
24};
25
26#[derive(Debug, Default)]
27pub(crate) struct SourceFiles {
28    primary: Option<PathBuf>,
29    included: HashMap<Vec<String>, PathBuf>,
30}
31
32impl SourceFiles {
33    pub(crate) fn new(primary: Option<PathBuf>, ranges: &[SourceRange]) -> Self {
34        let mut included = HashMap::new();
35        for range in ranges {
36            if !range.file_chain.is_empty()
37                && let Some(file) = &range.file
38                && !included.contains_key(range.file_chain.as_slice())
39            {
40                included.insert(range.file_chain.clone(), file.clone());
41            }
42        }
43        Self { primary, included }
44    }
45
46    fn location(&self, location: &Location) -> SourceLocation {
47        let file = match location
48            .start
49            .file
50            .as_deref()
51            .filter(|chain| !chain.is_empty())
52        {
53            Some(chain) => self.included.get(chain.as_slice()),
54            None => self.primary.as_ref(),
55        };
56        SourceLocation::at_location(file.cloned(), location.clone())
57    }
58}
59
60/// Owner-side of the self-referential parse cell: holds the preprocessed
61/// source text and the arena that parser-allocated strings live in. The
62/// AST dependent borrows from both fields simultaneously via their shared
63/// owner lifetime.
64#[derive(Debug)]
65pub(crate) struct OwnedInput {
66    pub(crate) source: Box<str>,
67    pub(crate) arena: Bump,
68}
69
70impl OwnedInput {
71    pub(crate) fn new(source: Box<str>) -> Self {
72        // Seed the arena with one chunk sized to the input. Most arena
73        // memory ends up holding interned strings + AST nodes whose total
74        // footprint correlates with source length, so this avoids the
75        // first ~10 chunk-grow round-trips through the global allocator
76        // on documents larger than a few KB.
77        let arena = Bump::with_capacity(source.len());
78        Self { source, arena }
79    }
80}
81
82// `self_cell!`'s `dependent:` slot takes a bare identifier and expands it
83// internally as `$Dependent<'a>`. `Document` already fits, so it goes in
84// directly. `Vec<InlineNode<'a>>` doesn't — hence the `InlineAst` alias.
85type InlineAst<'a> = Vec<InlineNode<'a>>;
86
87self_cell::self_cell! {
88    struct ParsedDocumentCell {
89        owner: OwnedInput,
90        #[covariant]
91        dependent: Document,
92    }
93
94    impl {Debug}
95}
96
97self_cell::self_cell! {
98    struct ParsedInlineCell {
99        owner: OwnedInput,
100        #[covariant]
101        dependent: InlineAst,
102    }
103
104    impl {Debug}
105}
106
107/// Owns a parsed document, its backing text, and non-fatal warnings.
108///
109/// A successful parse can contain recovered content. Check [`Self::source_recovery`]
110/// when the application requires complete input, and [`Self::warnings`] for all diagnostics.
111#[derive(Debug)]
112#[must_use = "ignoring a ParseResult drops any warnings the parser produced"]
113pub struct ParseResult {
114    cell: ParsedDocumentCell,
115    warnings: Vec<Warning>,
116    source_recovery: Option<Box<Warning>>,
117    source_files: SourceFiles,
118}
119
120impl ParseResult {
121    /// Build the document with its source map and collect warnings after construction.
122    pub(crate) fn try_new<E>(
123        owner: OwnedInput,
124        warnings_handle: Rc<RefCell<Vec<Warning>>>,
125        source_files: SourceFiles,
126        builder: impl for<'a> FnOnce(&'a OwnedInput) -> Result<Document<'a>, E>,
127    ) -> Result<Self, E> {
128        let cell = ParsedDocumentCell::try_new(owner, builder)?;
129        let warnings = recover_warnings(warnings_handle);
130        let source_recovery = warnings
131            .iter()
132            .find(|warning| {
133                matches!(
134                    warning.kind,
135                    WarningKind::ContentRecovery { .. }
136                        | WarningKind::UnterminatedDelimitedBlock { .. }
137                        | WarningKind::UnterminatedTable { .. }
138                        | WarningKind::TableUnknownFormat { .. }
139                        | WarningKind::TableIncompleteRow
140                        | WarningKind::TableCellOverflow { .. }
141                        | WarningKind::TableColumnCount { .. }
142                )
143            })
144            .map(|warning| Box::new(Warning::new(warning.kind.clone(), warning.location.clone())));
145        Ok(Self {
146            cell,
147            warnings,
148            source_recovery,
149            source_files,
150        })
151    }
152
153    /// Borrow the document AST.
154    #[must_use]
155    pub fn document(&self) -> &Document<'_> {
156        self.cell.borrow_dependent()
157    }
158
159    /// Borrow the preprocessed source the AST was parsed from.
160    ///
161    /// Note: this is the text as seen by the grammar, after include
162    /// resolution and other preprocessor transforms — not the original
163    /// caller input.
164    #[must_use]
165    pub fn source(&self) -> &str {
166        &self.cell.borrow_owner().source
167    }
168
169    /// Resolve a location from this result's AST to its original source file and position.
170    ///
171    /// Includes use the file resolved during preprocessing, even when selection
172    /// or indentation changed the parsed text. A span crossing files is anchored
173    /// in the file containing its start. Unknown include chains have no file.
174    #[must_use]
175    pub fn source_location(&self, location: &Location) -> SourceLocation {
176        self.source_files.location(location)
177    }
178
179    /// Borrow the collected warnings.
180    #[must_use]
181    pub fn warnings(&self) -> &[Warning] {
182        &self.warnings
183    }
184
185    /// The first warning for recovered source content or structure, when present.
186    ///
187    /// This includes omitted content, ignored substitutions, and unmatched block
188    /// boundaries, but excludes presentation warnings. It remains available after
189    /// [`Self::take_warnings`].
190    #[must_use]
191    pub fn source_recovery(&self) -> Option<&Warning> {
192        self.source_recovery.as_deref()
193    }
194
195    /// Take the warnings, leaving [`Self::warnings`] empty.
196    ///
197    /// The document and [`Self::source_recovery`] remain available.
198    pub fn take_warnings(&mut self) -> Vec<Warning> {
199        std::mem::take(&mut self.warnings)
200    }
201}
202
203/// Successful inline-only parse output: the inline-node slice plus the
204/// buffers it borrows from, plus any non-fatal warnings. Counterpart to
205/// [`ParseResult`] for the inline-only entry point.
206#[derive(Debug)]
207#[must_use = "ignoring a ParseInlineResult drops any warnings the parser produced"]
208pub struct ParseInlineResult {
209    cell: ParsedInlineCell,
210    warnings: Vec<Warning>,
211}
212
213impl ParseInlineResult {
214    /// Internal constructor. Counterpart to [`ParseResult::try_new`].
215    pub(crate) fn try_new<E>(
216        owner: OwnedInput,
217        warnings_handle: Rc<RefCell<Vec<Warning>>>,
218        builder: impl for<'a> FnOnce(&'a OwnedInput) -> Result<Vec<InlineNode<'a>>, E>,
219    ) -> Result<Self, E> {
220        let cell = ParsedInlineCell::try_new(owner, builder)?;
221        Ok(Self {
222            cell,
223            warnings: recover_warnings(warnings_handle),
224        })
225    }
226
227    /// Infallible variant for callers that don't need warnings (e.g. the
228    /// HTML converter's quotes-only fallback via `parse_text_for_quotes`).
229    /// `warnings` is always empty.
230    pub(crate) fn from_infallible(
231        owner: OwnedInput,
232        builder: impl for<'a> FnOnce(&'a OwnedInput) -> Vec<InlineNode<'a>>,
233    ) -> Self {
234        let cell = ParsedInlineCell::new(owner, builder);
235        Self {
236            cell,
237            warnings: Vec::new(),
238        }
239    }
240
241    /// Borrow the inline-node slice.
242    #[must_use]
243    pub fn inlines(&self) -> &[InlineNode<'_>] {
244        self.cell.borrow_dependent()
245    }
246
247    /// Borrow the preprocessed source the nodes were parsed from.
248    #[must_use]
249    pub fn source(&self) -> &str {
250        &self.cell.borrow_owner().source
251    }
252
253    /// Borrow the collected warnings.
254    #[must_use]
255    pub fn warnings(&self) -> &[Warning] {
256        &self.warnings
257    }
258
259    /// Take the warnings out of this result, leaving an empty slice
260    /// behind. See [`ParseResult::take_warnings`].
261    pub fn take_warnings(&mut self) -> Vec<Warning> {
262        std::mem::take(&mut self.warnings)
263    }
264}
265
266/// Unwrap the `Rc` the `ParserState` shared with the outer scope. The
267/// state is dropped before `try_new`'s builder returns, so the outer
268/// clone is normally unique and `try_unwrap` succeeds. If any other clone
269/// lingers (e.g. a future code path keeps one alive), we fall back to
270/// draining through the `RefCell` rather than losing the warnings.
271fn recover_warnings(handle: Rc<RefCell<Vec<Warning>>>) -> Vec<Warning> {
272    Rc::try_unwrap(handle).map_or_else(
273        |shared| std::mem::take(&mut *shared.borrow_mut()),
274        RefCell::into_inner,
275    )
276}
277
278/// Source text with no AST — returned by consumers (e.g. the LSP) when the
279/// input is available but parsing failed.
280#[derive(Debug, Clone)]
281pub struct OwnedSource(Box<str>);
282
283impl OwnedSource {
284    /// Wrap owned source text.
285    #[must_use]
286    pub fn new(source: impl Into<Box<str>>) -> Self {
287        Self(source.into())
288    }
289
290    /// Borrow the source text.
291    #[must_use]
292    pub fn source(&self) -> &str {
293        &self.0
294    }
295}