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