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 std::num::NonZeroUsize;
9use text_size::TextSize;
10
11macro_rules! define_zero_based_coordinate {
12    ($(#[$meta:meta])* $name:ident) => {
13        $(#[$meta])*
14        #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
15        pub struct $name(usize);
16
17        impl $name {
18            /// Creates a 0-based coordinate.
19            #[must_use]
20            pub const fn new(value: usize) -> Self {
21                Self(value)
22            }
23        }
24
25        impl Coordinate for $name {
26            fn get(self) -> usize {
27                self.0
28            }
29        }
30
31        impl fmt::Debug for $name {
32            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33                write!(f, "{}({})", stringify!($name), self.0)
34            }
35        }
36
37        impl fmt::Display for $name {
38            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39                self.0.fmt(f)
40            }
41        }
42    };
43}
44
45macro_rules! define_one_based_coordinate {
46    ($(#[$meta:meta])* $name:ident) => {
47        $(#[$meta])*
48        #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
49        pub struct $name(NonZeroUsize);
50
51        impl $name {
52            /// Creates a 1-based coordinate when `value` is non-zero.
53            #[must_use]
54            pub const fn try_new(value: usize) -> Option<Self> {
55                match NonZeroUsize::new(value) {
56                    Some(v) => Some(Self(v)),
57                    None => None,
58                }
59            }
60
61            /// Creates a 1-based coordinate.
62            ///
63            /// # Panics
64            ///
65            /// Panics when `value` is zero.
66            #[must_use]
67            pub const fn new(value: usize) -> Self {
68                match NonZeroUsize::new(value) {
69                    Some(v) => Self(v),
70                    None => panic!("1-based coordinates must be non-zero"),
71                }
72            }
73        }
74
75        impl Coordinate for $name {
76            fn get(self) -> usize {
77                self.0.get()
78            }
79        }
80
81        impl fmt::Debug for $name {
82            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83                write!(f, "{}({})", stringify!($name), self.0)
84            }
85        }
86
87        impl fmt::Display for $name {
88            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89                self.0.fmt(f)
90            }
91        }
92    };
93}
94
95/// Explicit conversion from a typed coordinate to a raw `usize`.
96pub trait Coordinate {
97    /// Returns the underlying numeric value for this coordinate.
98    fn get(self) -> usize;
99}
100
101define_one_based_coordinate!(
102    /// 1-based line number in a source file.
103    ///
104    /// Zero is invalid; use [`LineNumber::try_new`] to reject it explicitly.
105    LineNumber
106);
107define_one_based_coordinate!(
108    /// 1-based byte column offset from the start of a line.
109    ///
110    /// Zero is invalid; use [`ByteColumn::try_new`] to reject it explicitly.
111    ByteColumn
112);
113define_one_based_coordinate!(
114    /// 1-based Unicode scalar column offset from the start of a line.
115    ///
116    /// Zero is invalid; use [`CharColumn::try_new`] to reject it explicitly.
117    CharColumn
118);
119define_zero_based_coordinate!(
120    /// 0-based UTF-8 byte offset into source text.
121    ///
122    /// Prefer [`TextSize`] for [`text_size::TextRange`] construction; this
123    /// newtype marks raw parser byte offsets at crate boundaries.
124    ByteOffset
125);
126define_zero_based_coordinate!(
127    /// 0-based UTF-16 code-unit offset into source text.
128    Utf16Offset
129);
130
131impl From<TextSize> for ByteOffset {
132    fn from(offset: TextSize) -> Self {
133        Self::new(usize::from(offset))
134    }
135}
136
137impl TryFrom<ByteOffset> for TextSize {
138    type Error = std::num::TryFromIntError;
139
140    fn try_from(offset: ByteOffset) -> Result<Self, Self::Error> {
141        Self::try_from(offset.get())
142    }
143}
144
145/// 1-based line and byte-column position returned by [`super::LineIndex::line_col`].
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub struct ByteLineCol {
148    /// 1-based line number.
149    pub line: LineNumber,
150    /// 1-based byte column on that line.
151    pub column: ByteColumn,
152}
153
154/// 1-based line and character-column position returned by
155/// [`super::LineIndex::char_line_col`].
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct CharLineCol {
158    /// 1-based line number.
159    pub line: LineNumber,
160    /// 1-based Unicode scalar column on that line.
161    pub column: CharColumn,
162}
163
164#[cfg(test)]
165#[allow(clippy::unwrap_used)]
166mod tests {
167    use super::*;
168
169    fn assert_distinct_coordinate_types() {
170        let line = LineNumber::new(1);
171        let byte_col = ByteColumn::new(4);
172        let char_col = CharColumn::new(4);
173        let byte = ByteOffset::new(10);
174        let utf16 = Utf16Offset::new(10);
175
176        assert_eq!(line.get(), 1);
177        assert_eq!(byte_col.get(), char_col.get());
178        assert_ne!(byte.get(), line.get());
179
180        let _: ByteColumn = byte_col;
181        let _: CharColumn = char_col;
182        let _: ByteOffset = byte;
183        let _: Utf16Offset = utf16;
184        // `let _mixed: ByteColumn = char_col;` must not compile.
185    }
186
187    #[test]
188    fn coordinate_newtypes_are_not_interchangeable() {
189        assert_distinct_coordinate_types();
190    }
191
192    #[test]
193    fn one_based_coordinates_reject_zero() {
194        assert!(LineNumber::try_new(0).is_none());
195        assert!(ByteColumn::try_new(0).is_none());
196        assert!(CharColumn::try_new(0).is_none());
197    }
198
199    #[test]
200    fn one_based_coordinates_accept_one() {
201        assert_eq!(LineNumber::try_new(1), Some(LineNumber::new(1)));
202        assert_eq!(ByteColumn::try_new(1), Some(ByteColumn::new(1)));
203        assert_eq!(CharColumn::try_new(1), Some(CharColumn::new(1)));
204    }
205
206    #[test]
207    fn byte_offset_converts_to_text_size() {
208        let offset = ByteOffset::new(5);
209        assert_eq!(TextSize::try_from(offset).unwrap(), TextSize::from(5));
210        assert_eq!(ByteOffset::from(TextSize::from(5)), ByteOffset::new(5));
211    }
212}