Skip to main content

ox_mf2_parser/
api.rs

1// @license MIT
2// @author kazuya kawaguchi (a.k.a. kazupon)
3
4//! Public parser API.
5//!
6//! `parse_message` and `parse_source` are owned-result entry points. They
7//! materialise a [`ParseResult`] that the caller owns. `parse_source_session`
8//! reuses a borrowed [`crate::ParseWorkspace`] and returns a
9//! [`ParseSessionResult`] tied to the workspace lifetime.
10//!
11//! Concrete parsing behaviour is filled in by Milestones 5, 6, 7, 8, and 9.
12
13use crate::diagnostic::{Diagnostic, DiagnosticView};
14use crate::parser::{run_parse, run_parse_text};
15use crate::semantic::{lower_into as lower_semantic_into, SemanticModel, SemanticView};
16use crate::source::{SourceFileInput, SourceStore};
17use crate::span::SourceId;
18use crate::tables::CstTables;
19use crate::view::CstView;
20use crate::workspace::ParseWorkspace;
21
22/// Parser knobs. See `design/002` for the rationale.
23#[derive(Debug, Clone, Copy)]
24pub struct ParseOptions {
25    /// Recover when input is malformed instead of bailing out at the first
26    /// syntax error. Defaults to `true`.
27    pub recovery: bool,
28    /// Build the optional [`SemanticModel`]. Defaults to `false`.
29    pub parse_semantic: bool,
30    /// Preserve `ws` / `bidi` trivia. Defaults to `true`.
31    pub collect_trivia: bool,
32}
33
34impl Default for ParseOptions {
35    fn default() -> Self {
36        Self {
37            recovery: true,
38            parse_semantic: false,
39            collect_trivia: true,
40        }
41    }
42}
43
44/// Owned parse result. Detached from any workspace.
45#[derive(Debug, Default, Clone)]
46pub struct ParseResult {
47    pub source: SourceId,
48    pub cst: CstTables,
49    pub semantic: Option<SemanticModel>,
50    pub diagnostics: Vec<Diagnostic>,
51}
52
53/// Borrowed parse result. Lives until the next workspace `clear()` / `reset()`.
54#[derive(Debug, Clone, Copy)]
55pub struct ParseSessionResult<'a> {
56    pub source: SourceId,
57    pub cst: CstView<'a>,
58    pub semantic: Option<SemanticView<'a>>,
59    pub diagnostics: DiagnosticView<'a>,
60}
61
62/// Single-source batch input.
63#[derive(Debug, Default, Clone)]
64pub struct ParseInput<'a> {
65    pub source: &'a str,
66    pub path: Option<&'a str>,
67    pub locale: Option<&'a str>,
68    pub message_id: Option<&'a str>,
69    pub base_offset: Option<u32>,
70}
71
72/// Batch execution mode. Phase 1 only implements [`BatchExecution::Sequential`];
73/// requesting any other mode returns a [`BatchParseResult`] whose
74/// [`BatchParseResult::execution`] is `Sequential` and whose
75/// [`BatchParseResult::degraded`] flag is set so callers can observe the
76/// fallback. The `Parallel` variant is reserved for a future milestone.
77#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum BatchExecution {
80    #[default]
81    Sequential,
82    /// Reserved for Phase 2. Today this falls back to [`Self::Sequential`]
83    /// at run time and the result is marked `degraded`.
84    Parallel,
85}
86
87#[derive(Debug, Clone, Copy)]
88pub struct BatchParseOptions {
89    pub execution: BatchExecution,
90    /// Reserved for Phase 2 parallel execution. Currently ignored.
91    pub max_threads: Option<usize>,
92    /// Reserved for Phase 2 parallel execution. Currently ignored — the
93    /// sequential path always preserves input order.
94    pub preserve_order: bool,
95    pub parse: ParseOptions,
96}
97
98impl Default for BatchParseOptions {
99    fn default() -> Self {
100        Self {
101            execution: BatchExecution::Sequential,
102            max_threads: None,
103            preserve_order: true,
104            parse: ParseOptions::default(),
105        }
106    }
107}
108
109#[derive(Debug, Default, Clone)]
110pub struct BatchParseResult {
111    pub sources: SourceStore,
112    pub items: Vec<BatchParseItem>,
113    /// The execution mode that actually ran. Phase 1 always returns
114    /// [`BatchExecution::Sequential`] regardless of the request.
115    pub execution: BatchExecution,
116    /// `true` when the requested execution mode was not honoured (Phase 1
117    /// downgrades [`BatchExecution::Parallel`] to sequential). Inspect this
118    /// before relying on parallel-only assumptions.
119    pub degraded: bool,
120}
121
122#[derive(Debug, Default, Clone)]
123pub struct BatchParseItem {
124    pub source: SourceId,
125    pub result: ParseResult,
126}
127
128/// Parse `source_id` from `sources` and return an owned [`ParseResult`].
129pub fn parse_source(
130    sources: &SourceStore,
131    source_id: SourceId,
132    options: ParseOptions,
133) -> ParseResult {
134    let mut workspace = ParseWorkspace::new();
135    let source_len = sources.get(source_id).map_or(0, |f| f.text.len());
136    workspace.reserve_for_source_len(source_len);
137
138    run_parse(sources, source_id, &mut workspace, options);
139
140    materialise_owned_workspace(sources, source_id, workspace, options)
141}
142
143/// One-shot convenience parser. Parses `source` directly and returns an owned
144/// [`ParseResult`]. The success path does not allocate a temporary
145/// [`SourceStore`]; malformed inputs build one only when diagnostics need
146/// line/column materialisation.
147pub fn parse_message(source: &str) -> ParseResult {
148    assert!(
149        u32::try_from(source.len()).is_ok(),
150        "source length fits in u32"
151    );
152    let source_id = SourceId::new(0);
153    let mut workspace = ParseWorkspace::new();
154    workspace.reserve_for_source_len(source.len());
155    run_parse_text(source, source_id, &mut workspace, ParseOptions::default());
156    materialise_one_shot_message(source, source_id, workspace)
157}
158
159/// Reuse `workspace` to parse `source_id`. Returns a [`ParseSessionResult`]
160/// borrowed from the workspace.
161pub fn parse_source_session<'a>(
162    sources: &'a SourceStore,
163    source_id: SourceId,
164    workspace: &'a mut ParseWorkspace,
165    options: ParseOptions,
166) -> ParseSessionResult<'a> {
167    workspace.clear();
168    run_parse(sources, source_id, workspace, options);
169
170    if options.parse_semantic {
171        // Reuse the workspace-held SemanticModel's capacity instead of
172        // allocating a fresh model every session.
173        let model = workspace
174            .semantic
175            .model
176            .get_or_insert_with(SemanticModel::default);
177        lower_semantic_into(sources, source_id, &workspace.parser.tables, model);
178    } else {
179        workspace.semantic.model = None;
180    }
181
182    let cst = CstView::new(sources, source_id, &workspace.parser.tables);
183    let diagnostics = DiagnosticView {
184        sources,
185        records: &workspace.parser.diagnostics,
186    };
187    let semantic = workspace
188        .semantic
189        .model
190        .as_ref()
191        .map(|m| SemanticView::new(m, &workspace.parser.tables));
192    ParseSessionResult {
193        source: source_id,
194        cst,
195        semantic,
196        diagnostics,
197    }
198}
199
200/// Parse `inputs` sequentially and return owned results in input order.
201///
202/// Phase 1 only supports [`BatchExecution::Sequential`]. Requesting
203/// [`BatchExecution::Parallel`] falls back to the sequential path and sets
204/// [`BatchParseResult::degraded`] so callers can detect the downgrade.
205/// `max_threads` and `preserve_order` are reserved for Phase 2 and
206/// currently ignored.
207pub fn parse_batch(inputs: &[ParseInput<'_>], options: BatchParseOptions) -> BatchParseResult {
208    let mut sources = SourceStore::with_capacity(inputs.len());
209    let mut items = Vec::with_capacity(inputs.len());
210    let mut workspace = ParseWorkspace::new();
211
212    for input in inputs {
213        let source_id = sources.add(SourceFileInput {
214            source: input.source,
215            path: input.path,
216            locale: input.locale,
217            message_id: input.message_id,
218            base_offset: input.base_offset,
219        });
220        workspace.clear();
221        workspace.reserve_for_source_len(input.source.len());
222        run_parse(&sources, source_id, &mut workspace, options.parse);
223        let result = materialise(&sources, source_id, &workspace, options.parse);
224        items.push(BatchParseItem {
225            source: source_id,
226            result,
227        });
228    }
229
230    let degraded = !matches!(options.execution, BatchExecution::Sequential);
231
232    BatchParseResult {
233        sources,
234        items,
235        execution: BatchExecution::Sequential,
236        degraded,
237    }
238}
239
240fn materialise(
241    sources: &SourceStore,
242    source_id: SourceId,
243    workspace: &ParseWorkspace,
244    options: ParseOptions,
245) -> ParseResult {
246    let cst = workspace.parser.tables.clone();
247    let semantic = if options.parse_semantic {
248        let mut model = SemanticModel::default();
249        lower_semantic_into(sources, source_id, &cst, &mut model);
250        Some(model)
251    } else {
252        None
253    };
254    let diagnostics = DiagnosticView {
255        sources,
256        records: &workspace.parser.diagnostics,
257    }
258    .iter()
259    .collect();
260    ParseResult {
261        source: source_id,
262        cst,
263        semantic,
264        diagnostics,
265    }
266}
267
268fn materialise_owned_workspace(
269    sources: &SourceStore,
270    source_id: SourceId,
271    mut workspace: ParseWorkspace,
272    options: ParseOptions,
273) -> ParseResult {
274    let cst = core::mem::take(&mut workspace.parser.tables);
275    let semantic = if options.parse_semantic {
276        let mut model = SemanticModel::default();
277        lower_semantic_into(sources, source_id, &cst, &mut model);
278        Some(model)
279    } else {
280        None
281    };
282    let diagnostics = DiagnosticView {
283        sources,
284        records: &workspace.parser.diagnostics,
285    }
286    .iter()
287    .collect();
288    ParseResult {
289        source: source_id,
290        cst,
291        semantic,
292        diagnostics,
293    }
294}
295
296fn materialise_one_shot_message(
297    source: &str,
298    source_id: SourceId,
299    mut workspace: ParseWorkspace,
300) -> ParseResult {
301    let cst = core::mem::take(&mut workspace.parser.tables);
302    let diagnostics = if workspace.parser.diagnostics.is_empty() {
303        Vec::new()
304    } else {
305        let mut sources = SourceStore::with_capacity(1);
306        let actual_source_id = sources.add(SourceFileInput {
307            source,
308            ..Default::default()
309        });
310        debug_assert_eq!(actual_source_id, source_id);
311        DiagnosticView {
312            sources: &sources,
313            records: &workspace.parser.diagnostics,
314        }
315        .iter()
316        .collect()
317    };
318    ParseResult {
319        source: source_id,
320        cst,
321        semantic: None,
322        diagnostics,
323    }
324}