Skip to main content

nickel_lang_parser/
position.rs

1//! Define types of positions and position spans.
2//!
3//! The positions defined in this module are represented by the id of the corresponding source and
4//! raw byte indices.  They are prefixed with Raw to differentiate them from codespan's types and
5//! indicate that they do not store human friendly data like lines and columns.
6use crate::files::FileId;
7use codespan::{self, ByteIndex};
8use std::{
9    cmp::{Ordering, max, min},
10    ops::Range,
11};
12
13/// A simple wrapper trait for numeric types that define a `MAX` constant. This is useful to make
14/// interfaces more generic.
15pub trait Max {
16    const MAX: Self;
17}
18
19impl Max for u32 {
20    const MAX: Self = Self::MAX;
21}
22
23impl Max for usize {
24    const MAX: Self = Self::MAX;
25}
26
27/// A position identified by a byte offset in a file.
28#[derive(Debug, Clone, Copy, Eq, PartialEq)]
29pub struct RawPos {
30    pub src_id: FileId,
31    pub index: ByteIndex,
32}
33
34impl RawPos {
35    pub fn new(src_id: FileId, index: ByteIndex) -> Self {
36        Self { src_id, index }
37    }
38}
39
40/// A position span identified by a starting byte offset and an ending byte offset in a file.
41///
42/// `end` is the offset of the last character plus one.
43#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
44pub struct RawSpan {
45    pub src_id: FileId,
46    pub start: ByteIndex,
47    pub end: ByteIndex,
48}
49
50impl RawSpan {
51    /// Fuse two spans if they are from the same source file. The resulting span is the smallest
52    /// span that contain both `self` and `other`.
53    pub fn fuse(self, other: RawSpan) -> Option<RawSpan> {
54        if self.src_id == other.src_id {
55            Some(RawSpan {
56                src_id: self.src_id,
57                start: min(self.start, other.start),
58                end: max(self.end, other.end),
59            })
60        } else {
61            None
62        }
63    }
64
65    /// Create a `RawSpan` from a span as represented by the codespan library.
66    pub fn from_codespan(src_id: FileId, span: codespan::Span) -> Self {
67        RawSpan {
68            src_id,
69            start: span.start(),
70            end: span.end(),
71        }
72    }
73
74    /// Create a span from a numeric range. If either start or end is too large to be represented,
75    /// `u32::MAX` is used instead.
76    pub fn from_range<T>(src_id: FileId, range: Range<T>) -> Self
77    where
78        u32: TryFrom<T>,
79    {
80        RawSpan {
81            src_id,
82            start: ByteIndex(u32::try_from(range.start).unwrap_or(u32::MAX)),
83            end: ByteIndex(u32::try_from(range.end).unwrap_or(u32::MAX)),
84        }
85    }
86
87    /// Convert this span to a numeric index range. If either start or end is too large to be
88    /// represented, `T::MAX` is used instead.
89    pub fn to_range<T>(self) -> Range<T>
90    where
91        T: TryFrom<u32> + Max,
92    {
93        T::try_from(self.start.0).unwrap_or(T::MAX)..T::try_from(self.end.0).unwrap_or(T::MAX)
94    }
95
96    /// Return the start of this range.
97    pub fn start_pos(&self) -> RawPos {
98        RawPos {
99            src_id: self.src_id,
100            index: self.start,
101        }
102    }
103
104    /// Check whether this span contains a position.
105    pub fn contains(&self, pos: RawPos) -> bool {
106        self.src_id == pos.src_id && (self.start..self.end).contains(&pos.index)
107    }
108
109    /// Check whether this span contains another span.
110    pub fn contains_span(&self, other: RawSpan) -> bool {
111        self.src_id == other.src_id && self.start <= other.start && self.end >= other.end
112    }
113}
114
115impl From<RawSpan> for codespan::Span {
116    fn from(span: RawSpan) -> Self {
117        codespan::Span::new(span.start, span.end)
118    }
119}
120
121/// The position span of a term.
122#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
123pub enum TermPos {
124    /// The term exactly corresponds to an original expression in the source, or is a construct
125    /// introduced by program transformation that corresponds to an original span in the source.
126    Original(RawSpan),
127    /// The term is the result of the evaluation of an original expression in the source.
128    Inherited(RawSpan),
129    /// The term couldn't be assigned a position (usually generated during execution or program
130    /// transformations).
131    #[default]
132    None,
133}
134
135impl TermPos {
136    /// Apply a transformation to the inner position, if any.
137    pub fn map<F: FnOnce(RawSpan) -> RawSpan>(self, f: F) -> Self {
138        match self {
139            TermPos::Original(x) => TermPos::Original(f(x)),
140            TermPos::Inherited(x) => TermPos::Inherited(f(x)),
141            TermPos::None => TermPos::None,
142        }
143    }
144
145    pub fn as_opt_ref(&self) -> Option<&RawSpan> {
146        match self {
147            TermPos::Original(pos) | TermPos::Inherited(pos) => Some(pos),
148            TermPos::None => None,
149        }
150    }
151
152    // In principle, this should rather be implemented in a trait impl `impl Into<Option<RawSpan>>
153    // for TermPos`, but the type inference was working too badly.
154    pub fn into_opt(self) -> Option<RawSpan> {
155        match self {
156            TermPos::Original(pos) | TermPos::Inherited(pos) => Some(pos),
157            TermPos::None => None,
158        }
159    }
160
161    /// Returns the file id associated to this position, if the position is defined, or `None`
162    /// otherwise.
163    pub fn src_id(&self) -> Option<FileId> {
164        match self {
165            TermPos::Original(raw_span) | TermPos::Inherited(raw_span) => Some(raw_span.src_id),
166            TermPos::None => None,
167        }
168    }
169
170    /// Return `self` if `self` not [Self::None], or `other` otherwise.
171    pub fn or(self, other: Self) -> Self {
172        if let TermPos::None = self {
173            other
174        } else {
175            self
176        }
177    }
178
179    /// Return either `self` or `other` if and only if exactly one of them is defined. If both are
180    /// `None` or both are defined, `None` is returned.
181    pub fn xor(self, other: Self) -> Self {
182        match (self, other) {
183            (defn, TermPos::None) | (TermPos::None, defn) => defn,
184            _ => TermPos::None,
185        }
186    }
187
188    /// Determine is the position is defined. Return `false` if it is `None`, and `true` otherwise.
189    pub fn is_def(&self) -> bool {
190        matches!(self, TermPos::Original(_) | TermPos::Inherited(_))
191    }
192
193    /// Try to unwrap the underlying span. Panic if `self` is `None`.
194    #[track_caller]
195    pub fn unwrap(self) -> RawSpan {
196        match self {
197            TermPos::Original(x) | TermPos::Inherited(x) => x,
198            TermPos::None => panic!("TermPos::unwrap"),
199        }
200    }
201
202    /// Try to set the position to inherited, if it wasn't already. If `self` was `None`, `None` is
203    /// returned.
204    pub fn into_inherited(self) -> Self {
205        match self {
206            TermPos::Original(pos) => TermPos::Inherited(pos),
207            p => p,
208        }
209    }
210
211    /// Check whether this span contains a position.
212    pub fn contains(&self, pos: RawPos) -> bool {
213        self.as_opt_ref().is_some_and(|sp| sp.contains(pos))
214    }
215
216    /// Fuse two positions if they are from the same source file.
217    ///
218    /// - If both positions are defined and from the same file, the resulting position is the
219    ///   smallest span that contain both.
220    /// - If both positions are defined but aren't from the same file, this returns `TermPos::None`
221    /// - If at most one position is defined, the other is returned (whether defined or not).
222    pub fn fuse(self, other: Self) -> Self {
223        match (self, other) {
224            (TermPos::Original(sp1), TermPos::Original(sp2)) => {
225                if let Some(span) = sp1.fuse(sp2) {
226                    TermPos::Original(span)
227                } else {
228                    TermPos::None
229                }
230            }
231            (TermPos::Inherited(sp1), TermPos::Inherited(sp2))
232            | (TermPos::Original(sp1), TermPos::Inherited(sp2))
233            | (TermPos::Inherited(sp1), TermPos::Original(sp2)) => {
234                if let Some(span) = sp1.fuse(sp2) {
235                    TermPos::Inherited(span)
236                } else {
237                    TermPos::None
238                }
239            }
240            (TermPos::None, maybe_def) | (maybe_def, TermPos::None) => maybe_def,
241        }
242    }
243}
244
245/// A natural ordering for positions: `p1` is smaller than `p2` if they are located in the same
246/// file and `p1.offset` is smaller than `p2.offset`.
247impl PartialOrd for RawPos {
248    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
249        if self.src_id == other.src_id {
250            Some(self.index.cmp(&other.index))
251        } else {
252            None
253        }
254    }
255}
256
257/// An inclusion ordering for spans: `s1` is smaller than `s2` if they are located in the same file
258/// and `s1` is included in `s2` when seen as position intervals.
259impl PartialOrd for RawSpan {
260    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
261        if self.src_id != other.src_id {
262            None
263        } else if self.start == other.start && self.end == other.end {
264            Some(Ordering::Equal)
265        } else if self.start >= other.start && self.end <= other.end {
266            Some(Ordering::Less)
267        } else if self.start <= other.start && self.end >= other.end {
268            Some(Ordering::Greater)
269        } else {
270            None
271        }
272    }
273}
274
275impl From<RawSpan> for TermPos {
276    fn from(span: RawSpan) -> Self {
277        TermPos::Original(span)
278    }
279}
280
281impl From<Option<RawSpan>> for TermPos {
282    fn from(value: Option<RawSpan>) -> Self {
283        match value {
284            Some(span) => TermPos::Original(span),
285            None => TermPos::None,
286        }
287    }
288}