Skip to main content

m2_syn/
span.rs

1use std::fmt::{Debug, Display, Formatter};
2use std::hash::{Hash, Hasher};
3use std::ops::Range;
4use std::sync::{
5    Arc, OnceLock,
6    atomic::{AtomicU16, Ordering},
7};
8
9/// A boundary in a source buffer, as a byte offset.
10///
11/// Boundaries sit between bytes rather than on them, so a span's `end` is the
12/// offset past its last byte and `end - start` is its length. One offset is
13/// enough because a span's only job is to index a buffer: line and column are
14/// facts about a particular buffer's contents, recovered from it when something
15/// needs to *display* a location, and so they belong with the buffer rather
16/// than with the span. This is what `rustc` does too — its `Span` stores `lo`
17/// and `hi` byte positions and nothing else, and `SourceMap` binary-searches a
18/// line table on demand.
19pub type Offset = usize;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22/// A human-facing position in a source snapshot.
23///
24/// Lines and columns are one-based. Columns count Unicode scalar values rather
25/// than bytes or terminal cells, so the first character on every line is at
26/// column one regardless of its UTF-8 width.
27pub struct LineColumn {
28    /// The one-based line number.
29    pub line: usize,
30    /// The one-based character column.
31    pub column: usize,
32}
33
34impl Display for LineColumn {
35    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
36        write!(formatter, "{}:{}", self.line, self.column)
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41/// The human-facing opening and closing positions of a source span.
42///
43/// Like the underlying byte range, this range is half-open: `end` is the
44/// position immediately after the final character.
45pub struct LineColumnRange {
46    /// The position at which the span opens.
47    pub start: LineColumn,
48    /// The position immediately after the span closes.
49    pub end: LineColumn,
50}
51
52impl Display for LineColumnRange {
53    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
54        if self.start == self.end {
55            Display::fmt(&self.start, formatter)
56        } else {
57            write!(formatter, "{}-{}", self.start, self.end)
58        }
59    }
60}
61
62/// One text change: the text a buffer gains at a point, or the stretch it
63/// loses.
64///
65/// An edit acts on *both* halves of the indexing relation — on source text
66/// through [`Self::apply`] and on a [`Span`] through [`Span::shifted`] — and
67/// those two actions are designed to agree.
68///
69/// An insertion carries the text it inserts rather than a pair of boundaries,
70/// because the boundaries are recoverable (`at` and `at + text.len()`) while the
71/// text is not, and without the text an edit cannot act on a buffer at all.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum TextEdit {
74    /// Inserts `text` at one byte boundary.
75    Insertion {
76        /// The insertion boundary.
77        at: Offset,
78        /// The inserted text.
79        text: String,
80    },
81    /// Deletes one half-open byte range.
82    Deletion {
83        /// The first deleted byte.
84        start: Offset,
85        /// The boundary after the last deleted byte.
86        end: Offset,
87    },
88}
89
90impl TextEdit {
91    /// Creates an insertion of `text` landing at `at`.
92    pub fn insertion(at: Offset, text: impl Into<String>) -> Self {
93        Self::Insertion {
94            at,
95            text: text.into(),
96        }
97    }
98
99    /// Creates a deletion of the text occupying `start..end`.
100    ///
101    /// # Panics
102    ///
103    /// Panics when `start` is greater than `end`.
104    pub fn deletion(start: Offset, end: Offset) -> Self {
105        assert!(start <= end, "deletion start must not exceed its end");
106        Self::Deletion { start, end }
107    }
108
109    /// Returns `offset` in the buffer this edit produces.
110    ///
111    /// For an insertion of width `d` at `s` this is `offset + (offset >= s) * d`
112    /// — everything at or after the insertion point moves by the width, and
113    /// nothing else moves at all. A deletion is the mirror image, with the text
114    /// it covered collapsing onto its start.
115    ///
116    /// The rule is monotone in `offset`, so applying it to both ends of a range
117    /// can never invert them, and only the edit's *width* enters the arithmetic.
118    /// Reaching the same result by summing two absolute offsets would
119    /// manufacture an overflow the buffer never causes.
120    fn shift(&self, offset: Offset) -> Offset {
121        match self {
122            Self::Insertion { at, text } => {
123                if offset >= *at {
124                    offset + text.len()
125                } else {
126                    offset
127                }
128            }
129            Self::Deletion { start, end } => {
130                if offset <= *start {
131                    offset
132                } else if offset <= *end {
133                    // Everything the deletion covered now sits at its start.
134                    *start
135                } else {
136                    offset - (end - start)
137                }
138            }
139        }
140    }
141
142    /// Returns `source` with this edit applied, or `None` when the edit falls
143    /// outside the buffer or across a character boundary.
144    pub fn apply(&self, source: &str) -> Option<String> {
145        let (cut, resumes) = match self {
146            Self::Insertion { at, .. } => (*at, *at),
147            Self::Deletion { start, end } => (*start, *end),
148        };
149        let mut edited = String::from(source.get(..cut)?);
150        if let Self::Insertion { text, .. } = self {
151            edited.push_str(text);
152        }
153        edited.push_str(source.get(resumes..)?);
154        Some(edited)
155    }
156}
157
158static NEXT_FILE_ID: AtomicU16 = AtomicU16::new(1);
159
160/// The source site containing syntax.
161///
162/// `FileId` is the ordinary source-file identity. Keeping the identity behind
163/// an enum lets the syntax model add distinct embedded or macro hygiene sites
164/// without changing every span and token field.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub enum File {
167    /// An ordinary source file registered under a compact process-local id.
168    FileId(u16),
169    /// Syntax constructed without a corresponding on-disk source file.
170    Synthetic(u16),
171}
172
173impl File {
174    /// Allocates a compact process-local file label.
175    ///
176    /// The counter may recycle after all `u16` values have been used. Source
177    /// snapshot identity is carried by the shared [`Source`] allocation, so a
178    /// recycled display label cannot make unrelated spans equal or joinable.
179    pub fn fresh() -> Self {
180        let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
181        Self::FileId(id)
182    }
183
184    /// Creates an identity from an application-defined integer.
185    pub const fn from_raw(raw: u16) -> Self {
186        Self::FileId(raw)
187    }
188
189    /// Returns the application-defined integer stored by this identity.
190    pub const fn into_raw(self) -> u16 {
191        match self {
192            Self::FileId(id) | Self::Synthetic(id) => id,
193        }
194    }
195}
196
197/// Compatibility name for the source identity stored by [`Span`].
198pub type SourceId = File;
199
200/// One immutable source snapshot shared by every span into it.
201pub struct Source {
202    file: File,
203    text: Arc<str>,
204    line_starts: OnceLock<Arc<[Offset]>>,
205}
206
207impl Source {
208    /// Creates an ordinary source file with a fresh compact identity.
209    pub fn new(text: impl Into<Arc<str>>) -> Arc<Self> {
210        Self::with_file(File::fresh(), text)
211    }
212
213    /// Creates source text for syntax constructed outside an input file.
214    pub fn synthetic(text: impl Into<Arc<str>>) -> Arc<Self> {
215        let id = NEXT_FILE_ID.fetch_add(1, Ordering::Relaxed);
216        Self::with_file(File::Synthetic(id), text)
217    }
218
219    /// Creates a source snapshot with an explicitly selected source kind.
220    pub fn with_file(file: File, text: impl Into<Arc<str>>) -> Arc<Self> {
221        let text = text.into();
222        Arc::new(Self {
223            file,
224            text,
225            line_starts: OnceLock::new(),
226        })
227    }
228
229    /// Returns this source's semantic identity.
230    pub fn file(&self) -> File {
231        self.file
232    }
233
234    /// Returns the complete immutable source snapshot.
235    pub fn text(&self) -> &str {
236        &self.text
237    }
238
239    /// Converts a byte boundary into a one-based line and character column.
240    ///
241    /// Returns `None` when `offset` lies outside this snapshot or inside a
242    /// multi-byte UTF-8 character.
243    pub fn line_column(&self, offset: Offset) -> Option<LineColumn> {
244        self.text.get(..offset)?;
245        let line_starts = self.line_starts.get_or_init(|| {
246            std::iter::once(0)
247                .chain(
248                    self.text
249                        .bytes()
250                        .enumerate()
251                        .filter_map(|(offset, byte)| (byte == b'\n').then_some(offset + 1)),
252                )
253                .collect::<Vec<_>>()
254                .into()
255        });
256        let line_index = line_starts
257            .partition_point(|line_start| *line_start <= offset)
258            .checked_sub(1)?;
259        let line_start = line_starts[line_index];
260        let column = self.text.get(line_start..offset)?.chars().count() + 1;
261        Some(LineColumn {
262            line: line_index + 1,
263            column,
264        })
265    }
266}
267
268impl Debug for Source {
269    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
270        formatter
271            .debug_struct("Source")
272            .field("file", &self.file)
273            .field("bytes", &self.text.len())
274            .finish_non_exhaustive()
275    }
276}
277
278/// A shared source snapshot and optional byte range within it.
279#[derive(Clone)]
280pub struct Span {
281    source: Arc<Source>,
282    range: Option<Range<Offset>>,
283}
284
285impl Span {
286    /// Builds a span in `source` with an optional byte range.
287    pub fn new(source: Arc<Source>, range: Option<Range<Offset>>) -> Self {
288        Self {
289            source,
290            range: range.filter(|range| range.start <= range.end),
291        }
292    }
293
294    /// Builds a span covering `range` in `source`.
295    pub fn ranged(source: Arc<Source>, range: Range<Offset>) -> Self {
296        Self::new(source, Some(range))
297    }
298
299    /// Builds a span whose source is known but whose byte range is not.
300    pub fn unlocated(source: Arc<Source>) -> Self {
301        Self {
302            source,
303            range: None,
304        }
305    }
306
307    /// Creates a span covering one complete synthetic source buffer.
308    pub fn for_text(text: impl Into<Arc<str>>) -> Self {
309        let source = Source::synthetic(text);
310        Self::ranged(Arc::clone(&source), 0..source.text().len())
311    }
312
313    /// Returns the source file identity.
314    pub fn file(&self) -> File {
315        self.source.file()
316    }
317
318    /// Borrows the shared source snapshot.
319    pub fn source(&self) -> &Arc<Source> {
320        &self.source
321    }
322
323    /// Returns the source identity.
324    pub fn source_id(&self) -> File {
325        self.file()
326    }
327
328    /// Returns a copy of the indexed byte range.
329    pub fn range(&self) -> Option<Range<Offset>> {
330        self.range.clone()
331    }
332
333    /// Changes the source snapshot while preserving this span's range.
334    pub fn with_source(mut self, source: Arc<Source>) -> Self {
335        self.source = source;
336        self
337    }
338
339    /// Ensures that this span resolves to exactly `text`.
340    ///
341    /// Parsed syntax retains its original source allocation. Programmatically
342    /// constructed syntax receives a compact synthetic source allocation
343    /// rather than storing a second copy of its spelling in every token.
344    pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
345        let text = text.into();
346        if self.text() == Some(text.as_ref()) {
347            return self;
348        }
349        let source = Source::with_file(self.file(), text);
350        Self::ranged(Arc::clone(&source), 0..source.text().len())
351    }
352
353    /// Returns the exact source text covered by this span.
354    pub fn text(&self) -> Option<&str> {
355        self.source.text().get(self.range.as_ref()?.clone())
356    }
357    /// Creates a zero-width span at `offset` with this span's source identity.
358    pub fn at(&self, offset: Offset) -> Self {
359        if self.range.is_none() {
360            return Self::unlocated(Arc::clone(&self.source));
361        }
362        Self {
363            source: Arc::clone(&self.source),
364            range: Some(offset..offset),
365        }
366    }
367
368    /// Returns a subspan at byte offsets relative to this span's opening.
369    ///
370    /// The returned span shares the same source identity. Invalid or unavailable
371    /// ranges become unlocated spans in the same file.
372    pub fn subspan(&self, start: Offset, end: Offset) -> Self {
373        let Some(outer) = &self.range else {
374            return Self::unlocated(Arc::clone(&self.source));
375        };
376        let Some(start) = outer.start.checked_add(start) else {
377            return Self::unlocated(Arc::clone(&self.source));
378        };
379        let Some(end) = outer.start.checked_add(end) else {
380            return Self::unlocated(Arc::clone(&self.source));
381        };
382        if start > end || end > outer.end {
383            return Self::unlocated(Arc::clone(&self.source));
384        }
385        Self {
386            source: Arc::clone(&self.source),
387            range: Some(start..end),
388        }
389    }
390
391    /// The boundary this span opens at, or `None` when it is unlocated.
392    pub fn start(&self) -> Option<Offset> {
393        self.bounds().map(|(start, _)| start)
394    }
395
396    /// The boundary this span closes at, past its last byte.
397    pub fn end(&self) -> Option<Offset> {
398        self.bounds().map(|(_, end)| end)
399    }
400
401    /// The boundaries this span lies between.
402    pub fn bounds(&self) -> Option<(Offset, Offset)> {
403        self.range.as_ref().map(|range| (range.start, range.end))
404    }
405
406    /// Returns this span as one-based line and character-column positions.
407    ///
408    /// Returns `None` for an unlocated span, a range outside its source
409    /// snapshot, or a boundary inside a multi-byte UTF-8 character.
410    pub fn line_column_range(&self) -> Option<LineColumnRange> {
411        let (start, end) = self.bounds()?;
412        Some(LineColumnRange {
413            start: self.source.line_column(start)?,
414            end: self.source.line_column(end)?,
415        })
416    }
417
418    /// How many bytes this span covers, or `None` when it is unlocated.
419    pub fn len(&self) -> Option<usize> {
420        self.bounds().map(|(start, end)| end - start)
421    }
422
423    /// Reports whether this span covers no bytes, as the virtual marker tokens
424    /// do. Unlocated syntax covers no bytes either.
425    pub fn is_empty(&self) -> bool {
426        self.len().unwrap_or(0) == 0
427    }
428
429    /// Reports whether both spans identify the same source site.
430    pub fn shares_source(&self, other: &Self) -> bool {
431        Arc::ptr_eq(&self.source, &other.source)
432    }
433
434    /// Returns whether `offset` lies inside this span, excluding its closing
435    /// boundary.
436    ///
437    /// Boundaries sit between bytes, so the closing one belongs to whatever
438    /// follows: in `a+b` the boundary after `a` is contained by `+`, not by `a`.
439    /// This is the predicate to use when exactly one span must own a position. A
440    /// zero-width span contains nothing, having no interior.
441    pub fn contains(&self, offset: Offset) -> bool {
442        self.bounds()
443            .is_some_and(|(start, end)| start <= offset && offset < end)
444    }
445
446    /// Returns whether `offset` lies inside this span or on either boundary.
447    ///
448    /// Adjacent spans both touch the boundary between them, so this answers
449    /// "which syntax is the cursor at" rather than "which syntax owns this
450    /// position" — a cursor just past an identifier still touches it, and a
451    /// zero-width marker is touched at its own position.
452    pub fn touches(&self, offset: Offset) -> bool {
453        self.bounds()
454            .is_some_and(|(start, end)| start <= offset && offset <= end)
455    }
456
457    /// Returns this span's position in the buffer produced by `edit`.
458    ///
459    /// Both boundaries move under the same rule, so nothing here decides which
460    /// side of an insertion a boundary belongs to. Because that rule is monotone
461    /// the pair can never invert, and a zero-width span needs no special case:
462    /// its two equal boundaries stay equal.
463    ///
464    /// Text inserted at a span's *closing* boundary therefore extends the span,
465    /// which is what an unclosed group wants — it keeps reaching whatever now
466    /// follows it. A span wholly removed by a deletion collapses to a zero-width
467    /// span at the deletion point rather than losing its location,
468    /// because it still records where the syntax used to be.
469    pub fn shifted(&self, edit: &TextEdit) -> Self {
470        match self.bounds() {
471            None => Self::unlocated(Arc::clone(&self.source)),
472            Some((start, end)) => Self {
473                source: Arc::clone(&self.source),
474                range: Some(edit.shift(start)..edit.shift(end)),
475            },
476        }
477    }
478
479    /// Returns this span with the same opening boundary and source identity but
480    /// a new byte length.
481    pub fn resized(&self, len: usize) -> Self {
482        let Some(start) = self.start() else {
483            return Self::unlocated(Arc::clone(&self.source));
484        };
485        Self {
486            source: Arc::clone(&self.source),
487            range: Some(start..start.saturating_add(len)),
488        }
489    }
490}
491
492impl Span {
493    /// The smallest span covering both, or `None` when they belong to
494    /// different files.
495    pub fn join(&self, other: Self) -> Option<Self> {
496        if !self.shares_source(&other) {
497            return None;
498        }
499        match (self.bounds(), other.bounds()) {
500            (None, _) => Some(other),
501            (_, None) => Some(self.clone()),
502            (Some((start, end)), Some((start2, end2))) => {
503                let start = start.min(start2);
504                let end = end.max(end2);
505                Some(Self {
506                    source: Arc::clone(&self.source),
507                    range: Some(start..end),
508                })
509            }
510        }
511    }
512
513    /// The overlap of both, or `None` when the files differ or the ranges do
514    /// not overlap.
515    pub fn meet(&self, other: Self) -> Option<Self> {
516        if !self.shares_source(&other) {
517            return None;
518        }
519        match (self.bounds(), other.bounds()) {
520            (Some((start, end)), Some((start2, end2))) => {
521                let start = start.max(start2);
522                let end = end.min(end2);
523                if start < end {
524                    Some(Self {
525                        source: Arc::clone(&self.source),
526                        range: Some(start..end),
527                    })
528                } else {
529                    None
530                }
531            }
532            _ => None,
533        }
534    }
535
536    /// Joins every span in an iterator, returning `None` for an empty iterator
537    /// or when any two spans belong to different files.
538    pub fn join_all<I, S>(spans: I) -> Option<Self>
539    where
540        I: IntoIterator<Item = S>,
541        S: Into<Option<Self>>,
542    {
543        let mut spans = spans.into_iter().filter_map(Into::into);
544        let first = spans.next()?;
545        spans.try_fold(first, |left, right| left.join(right))
546    }
547}
548
549impl PartialEq for Span {
550    fn eq(&self, other: &Self) -> bool {
551        self.shares_source(other) && self.range == other.range
552    }
553}
554
555impl Eq for Span {}
556
557impl Hash for Span {
558    fn hash<H: Hasher>(&self, state: &mut H) {
559        Arc::as_ptr(&self.source).hash(state);
560        self.range.hash(state);
561    }
562}
563
564impl Debug for Span {
565    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
566        let mut span = formatter.debug_struct("Span");
567        span.field("file", &self.file())
568            .field("range", &self.line_column_range())
569            .field("bytes", &self.range);
570        if let Some(text) = self.text() {
571            span.field("text", &text);
572        }
573        span.finish()
574    }
575}
576
577impl Display for Span {
578    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
579        match self.line_column_range() {
580            Some(range) => Display::fmt(&range, formatter),
581            None => formatter.write_str("unlocated"),
582        }
583    }
584}
585
586/// A syntax value that reports its source extent.
587pub trait Spanned {
588    /// Returns the smallest span covering this value.
589    ///
590    /// # Panics
591    ///
592    /// Panics when the value is absent or assembled from different source
593    /// snapshots. Use [`Self::try_span`] when either case is expected.
594    fn span(&self) -> Span {
595        self.try_span()
596            .expect("syntax must have one shared source snapshot")
597    }
598
599    /// Returns the smallest span covering this value, when its constituents
600    /// share one source snapshot.
601    fn try_span(&self) -> Option<Span> {
602        Some(self.span())
603    }
604
605    /// Returns this value's span when one source snapshot covers it.
606    ///
607    /// # Panics
608    ///
609    /// Panics for absent syntax or syntax assembled from different sources.
610    fn expect_span(&self) -> Span {
611        self.span()
612    }
613}
614
615impl Spanned for Span {
616    fn span(&self) -> Span {
617        self.clone()
618    }
619}
620
621impl<T: Spanned + ?Sized> Spanned for &T {
622    fn span(&self) -> Span {
623        (*self).span()
624    }
625
626    fn try_span(&self) -> Option<Span> {
627        (*self).try_span()
628    }
629}
630
631impl<T: Spanned + ?Sized> Spanned for Box<T> {
632    fn span(&self) -> Span {
633        self.as_ref().span()
634    }
635
636    fn try_span(&self) -> Option<Span> {
637        self.as_ref().try_span()
638    }
639}
640
641impl<T: Spanned> Spanned for Option<T> {
642    fn try_span(&self) -> Option<Span> {
643        self.as_ref().and_then(Spanned::try_span)
644    }
645}
646
647impl<T: Spanned> Spanned for Vec<T> {
648    fn try_span(&self) -> Option<Span> {
649        Span::join_all(self.iter().map(Spanned::try_span))
650    }
651}
652
653impl<A: Spanned, B: Spanned> Spanned for (A, B) {
654    fn try_span(&self) -> Option<Span> {
655        self.0.try_span()?.join(self.1.try_span()?)
656    }
657}
658
659impl<T: Spanned> Spanned for &[T] {
660    fn try_span(&self) -> Option<Span> {
661        Span::join_all(self.iter().map(Spanned::try_span))
662    }
663}
664
665#[cfg(test)]
666mod source_tests {
667    use std::sync::Arc;
668
669    use super::*;
670
671    #[test]
672    fn spans_resolve_text_from_one_shared_source() {
673        let source = Source::new("quotient ring");
674        let quotient = Span::ranged(Arc::clone(&source), 0..8);
675        let ring = Span::ranged(Arc::clone(&source), 9..13);
676
677        assert_eq!(quotient.text(), Some("quotient"));
678        assert_eq!(ring.text(), Some("ring"));
679        assert!(Arc::ptr_eq(quotient.source(), ring.source()));
680    }
681
682    #[test]
683    fn joining_different_sources_returns_none() {
684        let left = Span::for_text("x");
685        let right = Span::for_text("y");
686
687        assert_eq!(left.join(right), None);
688    }
689
690    #[test]
691    fn an_unlocated_span_still_knows_its_source() {
692        let source = Source::new("x + y");
693        let span = Span::unlocated(Arc::clone(&source));
694
695        assert_eq!(span.file(), source.file());
696        assert_eq!(span.range(), None);
697        assert_eq!(span.text(), None);
698    }
699
700    #[test]
701    fn source_snapshots_with_the_same_file_do_not_alias() {
702        let file = File::from_raw(7);
703        let before = Span::ranged(Source::with_file(file, "x"), 0..1);
704        let after = Span::ranged(Source::with_file(file, "y"), 0..1);
705
706        assert_ne!(before, after);
707        assert_eq!(before.join(after), None);
708    }
709
710    #[test]
711    fn shifting_preserves_source_identity() {
712        let source = Source::new("ring");
713        let span = Span::ranged(Arc::clone(&source), 0..4);
714        let shifted = span.shifted(&TextEdit::insertion(0, "graded "));
715
716        assert!(Arc::ptr_eq(&source, shifted.source()));
717        assert_eq!(shifted.range(), Some(7..11));
718    }
719
720    #[test]
721    fn line_columns_are_one_based_and_count_characters() {
722        let source = Source::new("aα\nβc");
723
724        assert_eq!(
725            source.line_column(0),
726            Some(LineColumn { line: 1, column: 1 }),
727        );
728        assert_eq!(
729            source.line_column("aα\n".len()),
730            Some(LineColumn { line: 2, column: 1 }),
731        );
732        assert_eq!(
733            source.line_column("aα\nβ".len()),
734            Some(LineColumn { line: 2, column: 2 }),
735        );
736        assert_eq!(source.line_column(2), None);
737    }
738
739    #[test]
740    fn spans_format_as_half_open_line_column_ranges() {
741        let source = Source::new("one\ntwo");
742        let range = Span::ranged(Arc::clone(&source), 2..6);
743        let point = range.at(4);
744
745        assert_eq!(range.to_string(), "1:3-2:3");
746        assert_eq!(point.to_string(), "2:1");
747    }
748}