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}