Skip to main content

daml_syntax/
coordinate.rs

1//! Domain-specific source coordinates.
2//!
3//! Byte offsets, line numbers, and column positions use distinct newtypes so
4//! they cannot be passed to the wrong API by accident. Conversions to raw
5//! `usize` are explicit via [`Coordinate::get`].
6
7use std::fmt;
8use text_size::TextSize;
9
10macro_rules! define_coordinate {
11    ($(#[$meta:meta])* $name:ident) => {
12        $(#[$meta])*
13        #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14        pub struct $name(usize);
15
16        impl $name {
17            #[must_use]
18            pub const fn new(value: usize) -> Self {
19                Self(value)
20            }
21        }
22
23        impl Coordinate for $name {
24            fn get(self) -> usize {
25                self.0
26            }
27        }
28
29        impl fmt::Debug for $name {
30            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31                write!(f, "{}({})", stringify!($name), self.0)
32            }
33        }
34
35        impl fmt::Display for $name {
36            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37                self.0.fmt(f)
38            }
39        }
40    };
41}
42
43/// Explicit conversion from a typed coordinate to a raw `usize`.
44pub trait Coordinate {
45    fn get(self) -> usize;
46}
47
48define_coordinate!(/// 1-based line number in a source file.
49    LineNumber);
50define_coordinate!(/// 1-based byte column offset from the start of a line.
51    ByteColumn);
52define_coordinate!(/// 1-based Unicode scalar column offset from the start of a line.
53    CharColumn);
54define_coordinate!(/// 0-based UTF-8 byte offset into source text.
55    ///
56    /// Prefer [`TextSize`] for [`text_size::TextRange`] construction; this
57    /// newtype marks raw parser byte offsets at crate boundaries.
58    ByteOffset);
59define_coordinate!(/// 0-based UTF-16 code-unit offset into source text.
60    Utf16Offset);
61
62impl From<TextSize> for ByteOffset {
63    fn from(offset: TextSize) -> Self {
64        Self::new(usize::from(offset))
65    }
66}
67
68impl TryFrom<ByteOffset> for TextSize {
69    type Error = std::num::TryFromIntError;
70
71    fn try_from(offset: ByteOffset) -> Result<Self, Self::Error> {
72        Self::try_from(offset.get())
73    }
74}
75
76/// 1-based line and byte-column position returned by [`super::LineIndex::line_col`].
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct ByteLineCol {
79    pub line: LineNumber,
80    pub column: ByteColumn,
81}
82
83/// 1-based line and character-column position returned by
84/// [`super::LineIndex::char_line_col`].
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct CharLineCol {
87    pub line: LineNumber,
88    pub column: CharColumn,
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used)]
93mod tests {
94    use super::*;
95
96    fn assert_distinct_coordinate_types() {
97        let line = LineNumber::new(1);
98        let byte_col = ByteColumn::new(4);
99        let char_col = CharColumn::new(4);
100        let byte = ByteOffset::new(10);
101        let utf16 = Utf16Offset::new(10);
102
103        assert_eq!(line.get(), 1);
104        assert_eq!(byte_col.get(), char_col.get());
105        assert_ne!(byte.get(), line.get());
106
107        let _: ByteColumn = byte_col;
108        let _: CharColumn = char_col;
109        let _: ByteOffset = byte;
110        let _: Utf16Offset = utf16;
111        // `let _mixed: ByteColumn = char_col;` must not compile.
112    }
113
114    #[test]
115    fn coordinate_newtypes_are_not_interchangeable() {
116        assert_distinct_coordinate_types();
117    }
118
119    #[test]
120    fn byte_offset_converts_to_text_size() {
121        let offset = ByteOffset::new(5);
122        assert_eq!(TextSize::try_from(offset).unwrap(), TextSize::from(5));
123        assert_eq!(ByteOffset::from(TextSize::from(5)), ByteOffset::new(5));
124    }
125}