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    /// line number
29    pub line: usize,
30    /// 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    /// The source.
286    source: Arc<Source>,
287    /// The range.
288    range: Option<Range<Offset>>,
289}
290
291impl Span {
292    /// Builds a span in `source` with an optional byte range.
293    pub fn new(source: Arc<Source>, range: Option<Range<Offset>>) -> Self {
294        Self {
295            source,
296            range: range.filter(|range| range.start <= range.end),
297        }
298    }
299
300    /// Builds a span covering `range` in `source`.
301    pub fn ranged(source: Arc<Source>, range: Range<Offset>) -> Self {
302        Self::new(source, Some(range))
303    }
304
305    /// Builds a span whose source is known but whose byte range is not.
306    pub fn unlocated(source: Arc<Source>) -> Self {
307        Self {
308            source,
309            range: None,
310        }
311    }
312
313    /// Creates a span covering one complete synthetic source buffer.
314    pub fn for_text(text: impl Into<Arc<str>>) -> Self {
315        let source = Source::synthetic(text);
316        Self::ranged(Arc::clone(&source), 0..source.text().len())
317    }
318
319    /// Returns the source file identity.
320    pub fn file(&self) -> File {
321        self.source.file()
322    }
323
324    /// Borrows the shared source snapshot.
325    pub fn source(&self) -> &Arc<Source> {
326        &self.source
327    }
328
329    /// Returns the source identity.
330    pub fn source_id(&self) -> File {
331        self.file()
332    }
333
334    /// Returns a copy of the indexed byte range.
335    pub fn range(&self) -> Option<Range<Offset>> {
336        self.range.clone()
337    }
338
339    /// Changes the source snapshot while preserving this span's range.
340    pub fn with_source(mut self, source: Arc<Source>) -> Self {
341        self.source = source;
342        self
343    }
344
345    /// Ensures that this span resolves to exactly `text`.
346    ///
347    /// Parsed syntax retains its original source allocation. Programmatically
348    /// constructed syntax receives a compact synthetic source allocation
349    /// rather than storing a second copy of its spelling in every token.
350    pub fn with_text(self, text: impl Into<Arc<str>>) -> Self {
351        let text = text.into();
352        if self.text() == Some(text.as_ref()) {
353            return self;
354        }
355        let source = Source::with_file(self.file(), text);
356        Self::ranged(Arc::clone(&source), 0..source.text().len())
357    }
358
359    /// Returns the exact source text covered by this span.
360    pub fn text(&self) -> Option<&str> {
361        self.source.text().get(self.range.as_ref()?.clone())
362    }
363    /// Creates a zero-width span at `offset` with this span's source identity.
364    pub fn at(&self, offset: Offset) -> Self {
365        if self.range.is_none() {
366            return Self::unlocated(Arc::clone(&self.source));
367        }
368        Self {
369            source: Arc::clone(&self.source),
370            range: Some(offset..offset),
371        }
372    }
373
374    /// Returns a subspan at byte offsets relative to this span's opening.
375    ///
376    /// The returned span shares the same source identity. Invalid or unavailable
377    /// ranges become unlocated spans in the same file.
378    pub fn subspan(&self, start: Offset, end: Offset) -> Self {
379        let Some(outer) = &self.range else {
380            return Self::unlocated(Arc::clone(&self.source));
381        };
382        let Some(start) = outer.start.checked_add(start) else {
383            return Self::unlocated(Arc::clone(&self.source));
384        };
385        let Some(end) = outer.start.checked_add(end) else {
386            return Self::unlocated(Arc::clone(&self.source));
387        };
388        if start > end || end > outer.end {
389            return Self::unlocated(Arc::clone(&self.source));
390        }
391        Self {
392            source: Arc::clone(&self.source),
393            range: Some(start..end),
394        }
395    }
396
397    /// The boundary this span opens at, or `None` when it is unlocated.
398    pub fn start(&self) -> Option<Offset> {
399        self.bounds().map(|(start, _)| start)
400    }
401
402    /// The boundary this span closes at, past its last byte.
403    pub fn end(&self) -> Option<Offset> {
404        self.bounds().map(|(_, end)| end)
405    }
406
407    /// The boundaries this span lies between.
408    pub fn bounds(&self) -> Option<(Offset, Offset)> {
409        self.range.as_ref().map(|range| (range.start, range.end))
410    }
411
412    /// Returns this span as one-based line and character-column positions.
413    ///
414    /// Returns `None` for an unlocated span, a range outside its source
415    /// snapshot, or a boundary inside a multi-byte UTF-8 character.
416    pub fn line_column_range(&self) -> Option<LineColumnRange> {
417        let (start, end) = self.bounds()?;
418        Some(LineColumnRange {
419            start: self.source.line_column(start)?,
420            end: self.source.line_column(end)?,
421        })
422    }
423
424    /// How many bytes this span covers, or `None` when it is unlocated.
425    pub fn len(&self) -> Option<usize> {
426        self.bounds().map(|(start, end)| end - start)
427    }
428
429    /// Reports whether this span covers no bytes, as the virtual marker tokens
430    /// do. Unlocated syntax covers no bytes either.
431    pub fn is_empty(&self) -> bool {
432        self.len().unwrap_or(0) == 0
433    }
434
435    /// Reports whether both spans identify the same source site.
436    pub fn shares_source(&self, other: &Self) -> bool {
437        Arc::ptr_eq(&self.source, &other.source)
438    }
439
440    /// Returns whether `offset` lies inside this span, excluding its closing
441    /// boundary.
442    ///
443    /// Boundaries sit between bytes, so the closing one belongs to whatever
444    /// follows: in `a+b` the boundary after `a` is contained by `+`, not by `a`.
445    /// This is the predicate to use when exactly one span must own a position. A
446    /// zero-width span contains nothing, having no interior.
447    pub fn contains(&self, offset: Offset) -> bool {
448        self.bounds()
449            .is_some_and(|(start, end)| start <= offset && offset < end)
450    }
451
452    /// Returns whether `offset` lies inside this span or on either boundary.
453    ///
454    /// Adjacent spans both touch the boundary between them, so this answers
455    /// "which syntax is the cursor at" rather than "which syntax owns this
456    /// position" — a cursor just past an identifier still touches it, and a
457    /// zero-width marker is touched at its own position.
458    pub fn touches(&self, offset: Offset) -> bool {
459        self.bounds()
460            .is_some_and(|(start, end)| start <= offset && offset <= end)
461    }
462
463    /// Returns this span's position in the buffer produced by `edit`.
464    ///
465    /// Both boundaries move under the same rule, so nothing here decides which
466    /// side of an insertion a boundary belongs to. Because that rule is monotone
467    /// the pair can never invert, and a zero-width span needs no special case:
468    /// its two equal boundaries stay equal.
469    ///
470    /// Text inserted at a span's *closing* boundary therefore extends the span,
471    /// which is what an unclosed group wants — it keeps reaching whatever now
472    /// follows it. A span wholly removed by a deletion collapses to a zero-width
473    /// span at the deletion point rather than losing its location,
474    /// because it still records where the syntax used to be.
475    pub fn shifted(&self, edit: &TextEdit) -> Self {
476        match self.bounds() {
477            None => Self::unlocated(Arc::clone(&self.source)),
478            Some((start, end)) => Self {
479                source: Arc::clone(&self.source),
480                range: Some(edit.shift(start)..edit.shift(end)),
481            },
482        }
483    }
484
485    /// Returns this span with the same opening boundary and source identity but
486    /// a new byte length.
487    pub fn resized(&self, len: usize) -> Self {
488        let Some(start) = self.start() else {
489            return Self::unlocated(Arc::clone(&self.source));
490        };
491        Self {
492            source: Arc::clone(&self.source),
493            range: Some(start..start.saturating_add(len)),
494        }
495    }
496}
497
498impl Span {
499    /// The smallest span covering both, or `None` when they belong to
500    /// different files.
501    pub fn join(&self, other: Self) -> Option<Self> {
502        if !self.shares_source(&other) {
503            return None;
504        }
505        match (self.bounds(), other.bounds()) {
506            (None, _) => Some(other),
507            (_, None) => Some(self.clone()),
508            (Some((start, end)), Some((start2, end2))) => {
509                let start = start.min(start2);
510                let end = end.max(end2);
511                Some(Self {
512                    source: Arc::clone(&self.source),
513                    range: Some(start..end),
514                })
515            }
516        }
517    }
518
519    /// The overlap of both, or `None` when the files differ or the ranges do
520    /// not overlap.
521    pub fn meet(&self, other: Self) -> Option<Self> {
522        if !self.shares_source(&other) {
523            return None;
524        }
525        match (self.bounds(), other.bounds()) {
526            (Some((start, end)), Some((start2, end2))) => {
527                let start = start.max(start2);
528                let end = end.min(end2);
529                if start < end {
530                    Some(Self {
531                        source: Arc::clone(&self.source),
532                        range: Some(start..end),
533                    })
534                } else {
535                    None
536                }
537            }
538            _ => None,
539        }
540    }
541
542    /// Joins every span in an iterator, returning `None` for an empty iterator
543    /// or when any two spans belong to different files.
544    pub fn join_all<I, S>(spans: I) -> Option<Self>
545    where
546        I: IntoIterator<Item = S>,
547        S: Into<Option<Self>>,
548    {
549        let mut spans = spans.into_iter().filter_map(Into::into);
550        let first = spans.next()?;
551        spans.try_fold(first, |left, right| left.join(right))
552    }
553}
554
555impl PartialEq for Span {
556    fn eq(&self, other: &Self) -> bool {
557        self.shares_source(other) && self.range == other.range
558    }
559}
560
561impl Eq for Span {}
562
563impl Hash for Span {
564    fn hash<H: Hasher>(&self, state: &mut H) {
565        Arc::as_ptr(&self.source).hash(state);
566        self.range.hash(state);
567    }
568}
569
570impl Debug for Span {
571    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
572        let mut span = formatter.debug_struct("Span");
573        span.field("file", &self.file())
574            .field("range", &self.line_column_range())
575            .field("bytes", &self.range);
576        if let Some(text) = self.text() {
577            span.field("text", &text);
578        }
579        span.finish()
580    }
581}
582
583impl Display for Span {
584    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
585        match self.line_column_range() {
586            Some(range) => Display::fmt(&range, formatter),
587            None => formatter.write_str("unlocated"),
588        }
589    }
590}
591
592/// A syntax value that reports its source extent.
593pub trait Spanned {
594    /// Returns the smallest span covering this value.
595    ///
596    /// # Panics
597    ///
598    /// Panics when the value is absent or assembled from different source
599    /// snapshots. Use [`Self::try_span`] when either case is expected.
600    fn span(&self) -> Span {
601        self.try_span()
602            .expect("syntax must have one shared source snapshot")
603    }
604
605    /// Returns the smallest span covering this value, when its constituents
606    /// share one source snapshot.
607    fn try_span(&self) -> Option<Span> {
608        Some(self.span())
609    }
610
611    /// Returns this value's span when one source snapshot covers it.
612    ///
613    /// # Panics
614    ///
615    /// Panics for absent syntax or syntax assembled from different sources.
616    fn expect_span(&self) -> Span {
617        self.span()
618    }
619}
620
621impl Spanned for Span {
622    fn span(&self) -> Span {
623        self.clone()
624    }
625}
626
627impl<T: Spanned + ?Sized> Spanned for &T {
628    fn span(&self) -> Span {
629        (*self).span()
630    }
631
632    fn try_span(&self) -> Option<Span> {
633        (*self).try_span()
634    }
635}
636
637impl<T: Spanned + ?Sized> Spanned for Box<T> {
638    fn span(&self) -> Span {
639        self.as_ref().span()
640    }
641
642    fn try_span(&self) -> Option<Span> {
643        self.as_ref().try_span()
644    }
645}
646
647impl<T: Spanned> Spanned for Option<T> {
648    fn try_span(&self) -> Option<Span> {
649        self.as_ref().and_then(Spanned::try_span)
650    }
651}
652
653impl<T: Spanned> Spanned for Vec<T> {
654    fn try_span(&self) -> Option<Span> {
655        Span::join_all(self.iter().map(Spanned::try_span))
656    }
657}
658
659impl<A: Spanned, B: Spanned> Spanned for (A, B) {
660    fn try_span(&self) -> Option<Span> {
661        self.0.try_span()?.join(self.1.try_span()?)
662    }
663}
664
665impl<T: Spanned> Spanned for &[T] {
666    fn try_span(&self) -> Option<Span> {
667        Span::join_all(self.iter().map(Spanned::try_span))
668    }
669}
670
671#[cfg(test)]
672mod source_tests {
673    use std::sync::Arc;
674
675    use super::*;
676
677    #[test]
678    fn spans_resolve_text_from_one_shared_source() {
679        let source = Source::new("quotient ring");
680        let quotient = Span::ranged(Arc::clone(&source), 0..8);
681        let ring = Span::ranged(Arc::clone(&source), 9..13);
682
683        assert_eq!(quotient.text(), Some("quotient"));
684        assert_eq!(ring.text(), Some("ring"));
685        assert!(Arc::ptr_eq(quotient.source(), ring.source()));
686    }
687
688    #[test]
689    fn joining_different_sources_returns_none() {
690        let left = Span::for_text("x");
691        let right = Span::for_text("y");
692
693        assert_eq!(left.join(right), None);
694    }
695
696    #[test]
697    fn an_unlocated_span_still_knows_its_source() {
698        let source = Source::new("x + y");
699        let span = Span::unlocated(Arc::clone(&source));
700
701        assert_eq!(span.file(), source.file());
702        assert_eq!(span.range(), None);
703        assert_eq!(span.text(), None);
704    }
705
706    #[test]
707    fn source_snapshots_with_the_same_file_do_not_alias() {
708        let file = File::from_raw(7);
709        let before = Span::ranged(Source::with_file(file, "x"), 0..1);
710        let after = Span::ranged(Source::with_file(file, "y"), 0..1);
711
712        assert_ne!(before, after);
713        assert_eq!(before.join(after), None);
714    }
715
716    #[test]
717    fn shifting_preserves_source_identity() {
718        let source = Source::new("ring");
719        let span = Span::ranged(Arc::clone(&source), 0..4);
720        let shifted = span.shifted(&TextEdit::insertion(0, "graded "));
721
722        assert!(Arc::ptr_eq(&source, shifted.source()));
723        assert_eq!(shifted.range(), Some(7..11));
724    }
725
726    #[test]
727    fn line_columns_are_one_based_and_count_characters() {
728        let source = Source::new("aα\nβc");
729
730        assert_eq!(
731            source.line_column(0),
732            Some(LineColumn { line: 1, column: 1 }),
733        );
734        assert_eq!(
735            source.line_column("aα\n".len()),
736            Some(LineColumn { line: 2, column: 1 }),
737        );
738        assert_eq!(
739            source.line_column("aα\nβ".len()),
740            Some(LineColumn { line: 2, column: 2 }),
741        );
742        assert_eq!(source.line_column(2), None);
743    }
744
745    #[test]
746    fn spans_format_as_half_open_line_column_ranges() {
747        let source = Source::new("one\ntwo");
748        let range = Span::ranged(Arc::clone(&source), 2..6);
749        let point = range.at(4);
750
751        assert_eq!(range.to_string(), "1:3-2:3");
752        assert_eq!(point.to_string(), "2:1");
753    }
754}