Skip to main content

marco_core/parser/
shared.rs

1// Canonical span conversion helpers for the parser layer
2// Centralized to ensure blocks and inlines use the same logic.
3
4use crate::parser::position::{Position, Span as ParserSpan};
5use nom_locate::LocatedSpan;
6use std::cell::Cell;
7
8// ---------------------------------------------------------------------------
9// Per-parse runtime option flags (thread-local for zero call-site overhead)
10// ---------------------------------------------------------------------------
11
12thread_local! {
13    static TRACK_POSITIONS: Cell<bool> = const { Cell::new(true) };
14    static PARSE_MATH: Cell<bool>      = const { Cell::new(true) };
15    static PARSE_DIAGRAMS: Cell<bool>  = const { Cell::new(true) };
16}
17
18/// RAII guard that sets per-thread parse options and restores them on drop.
19///
20/// Created by [`parse_with_options`] before calling the parse pipeline; dropped
21/// (and thus restored) when the parse call returns or panics.
22pub(crate) struct ParseOptionsGuard {
23    prev_track: bool,
24    prev_math: bool,
25    prev_diagrams: bool,
26}
27
28impl ParseOptionsGuard {
29    pub(crate) fn new(track: bool, math: bool, diagrams: bool) -> Self {
30        let prev_track = TRACK_POSITIONS.with(|c| c.replace(track));
31        let prev_math = PARSE_MATH.with(|c| c.replace(math));
32        let prev_diagrams = PARSE_DIAGRAMS.with(|c| c.replace(diagrams));
33        Self {
34            prev_track,
35            prev_math,
36            prev_diagrams,
37        }
38    }
39}
40
41impl Drop for ParseOptionsGuard {
42    fn drop(&mut self) {
43        TRACK_POSITIONS.with(|c| c.set(self.prev_track));
44        PARSE_MATH.with(|c| c.set(self.prev_math));
45        PARSE_DIAGRAMS.with(|c| c.set(self.prev_diagrams));
46    }
47}
48
49/// Returns whether position tracking is enabled in the current parse context.
50///
51/// Exists mainly so callers that need to *capture* the current thread's
52/// option values (e.g. before dispatching work onto other threads, where
53/// `ParseOptionsGuard` must be re-installed per closure — thread-locals
54/// don't propagate to new threads) don't have to reach into `opt_span`'s
55/// private `TRACK_POSITIONS` read. Only such a caller exists today
56/// (`parallel_inline`, behind `parallel-parse`), hence the `cfg`.
57#[cfg(feature = "parallel-parse")]
58#[inline]
59pub(crate) fn track_positions_enabled() -> bool {
60    TRACK_POSITIONS.with(|c| c.get())
61}
62
63/// Returns whether math parsing is enabled in the current parse context.
64#[inline]
65pub(crate) fn parse_math_enabled() -> bool {
66    PARSE_MATH.with(|c| c.get())
67}
68
69/// Returns whether diagram parsing is enabled in the current parse context.
70#[inline]
71pub(crate) fn parse_diagrams_enabled() -> bool {
72    PARSE_DIAGRAMS.with(|c| c.get())
73}
74
75// ---------------------------------------------------------------------------
76// opt_span helpers — use these in parser code instead of `Some(to_parser_span(...))`
77// ---------------------------------------------------------------------------
78
79/// Returns `None` (skipping O(n) string scans) when position tracking is disabled,
80/// or `Some(span)` with real line/column data when enabled.
81///
82/// Replace `span: Some(to_parser_span(x))` with `span: opt_span(x)`.
83#[inline]
84pub fn opt_span(span: GrammarSpan) -> Option<ParserSpan> {
85    if !TRACK_POSITIONS.with(|c| c.get()) {
86        return None;
87    }
88    Some(to_parser_span(span))
89}
90
91/// Like [`opt_span`] but takes a start/end range using exclusive end semantics.
92///
93/// Replace `span: Some(to_parser_span_range(start, end))` with
94/// `span: opt_span_range(start, end)`.
95#[inline]
96pub fn opt_span_range(start: GrammarSpan, end: GrammarSpan) -> Option<ParserSpan> {
97    if !TRACK_POSITIONS.with(|c| c.get()) {
98        return None;
99    }
100    Some(to_parser_span_range(start, end))
101}
102
103/// Like [`opt_span`] but takes a start/end range using inclusive end semantics.
104///
105/// Replace `span: Some(to_parser_span_range_inclusive(start, end))` with
106/// `span: opt_span_range_inclusive(start, end)`.
107#[inline]
108pub fn opt_span_range_inclusive(start: GrammarSpan, end: GrammarSpan) -> Option<ParserSpan> {
109    if !TRACK_POSITIONS.with(|c| c.get()) {
110        return None;
111    }
112    Some(to_parser_span_range_inclusive(start, end))
113}
114
115/// Grammar span type (nom_locate::LocatedSpan)
116pub type GrammarSpan<'a> = LocatedSpan<&'a str>;
117
118/// Convert grammar span (LocatedSpan) to parser span (line/column)
119///
120/// This handles multi-line fragments by computing end line/column
121/// based on newline count and last-line length. Columns are byte-based
122/// 1-based offsets to match `Position` semantics.
123pub fn to_parser_span(span: GrammarSpan) -> ParserSpan {
124    let start_line = span.location_line() as usize; // 1-based
125    let frag = span.fragment().as_bytes();
126
127    // Single O(n) pass: count newlines and record the last newline byte position.
128    let mut newline_count = 0usize;
129    let mut last_nl: Option<usize> = None;
130    for (i, &b) in frag.iter().enumerate() {
131        if b == b'\n' {
132            newline_count += 1;
133            last_nl = Some(i);
134        }
135    }
136    let end_line = start_line + newline_count;
137
138    let end_column = match last_nl {
139        Some(pos) if pos == frag.len() - 1 => {
140            // Fragment ends with '\n' — end column is column 1 of the next line.
141            1
142        }
143        Some(pos) => {
144            // Multi-line: bytes after last newline + 1 (1-based).
145            frag.len() - pos - 1 + 1
146        }
147        None => {
148            // Single-line: start column (byte-based) + fragment byte length.
149            span.get_column() + frag.len()
150        }
151    };
152
153    let start = Position::new(start_line, span.get_column(), span.location_offset());
154    let end = Position::new(
155        end_line,
156        end_column,
157        span.location_offset() + span.fragment().len(),
158    );
159    ParserSpan::new(start, end)
160}
161
162/// Convert grammar span range (start, end) to parser span
163/// Convert a grammar span range where `end` is the remainder span
164/// (i.e. the nom `rest` after a match). This sets the end position to the
165/// `end.location_offset()` (start of the remainder), matching inline parser
166/// usage patterns like `to_parser_span_range(start, rest)`.
167pub fn to_parser_span_range(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
168    let start_pos = Position::new(
169        start.location_line() as usize,
170        start.get_column(),
171        start.location_offset(),
172    );
173    let end_pos = Position::new(
174        end.location_line() as usize,
175        end.get_column(),
176        end.location_offset(),
177    );
178    ParserSpan::new(start_pos, end_pos)
179}
180
181/// Convert a grammar span range where `end` is the final fragment of the
182/// matched range (i.e. inclusive). This preserves the previous block-level
183/// semantics where callers pass the last fragment and expect the end to be
184/// at `end.location_offset() + end.fragment().len()`.
185pub fn to_parser_span_range_inclusive(start: GrammarSpan, end: GrammarSpan) -> ParserSpan {
186    let start_pos = Position::new(
187        start.location_line() as usize,
188        start.get_column(),
189        start.location_offset(),
190    );
191    let end_pos = Position::new(
192        end.location_line() as usize,
193        end.get_column() + end.fragment().len(),
194        end.location_offset() + end.fragment().len(),
195    );
196    ParserSpan::new(start_pos, end_pos)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn test_to_parser_span_ascii() {
205        let input = GrammarSpan::new("hello");
206        let span = to_parser_span(input);
207        assert_eq!(span.start.line, 1);
208        assert_eq!(span.start.column, 1);
209        assert_eq!(span.end.column, 6); // 5 bytes + 1-based
210    }
211
212    #[test]
213    fn test_to_parser_span_utf8_and_emoji() {
214        let input = GrammarSpan::new("Tëst");
215        let span = to_parser_span(input);
216        assert_eq!(span.start.column, 1);
217        // 'Tëst' is 5 bytes; end.column should be 6
218        assert_eq!(span.end.column, 6);
219
220        let input2 = GrammarSpan::new("🎨");
221        let span2 = to_parser_span(input2);
222        assert_eq!(span2.start.column, 1);
223        // emoji 4 bytes -> end column 5
224        assert_eq!(span2.end.column, 5);
225    }
226}