Skip to main content

llvmkit_asmparser/
file_loc.rs

1//! Line / column source locations for parser diagnostics.
2//!
3//! Direct port of `llvm/include/llvm/AsmParser/FileLoc.h`. [`FileLoc`] is a
4//! `(line, col)` pair with 0-based components; [`FileLocRange`] is the
5//! half-open range `[start, end)` produced by `LLParser` while it consumes
6//! tokens.
7//!
8//! These are the *external* diagnostic surface — the lexer keeps working in
9//! byte offsets via [`llvmkit_support::Span`], and the parser projects to
10//! [`FileLoc`] only when it needs to expose a location to callers. Mirrors
11//! upstream's split between `SMLoc` (byte pointer) and `FileLoc` (line/col).
12
13use core::cmp::Ordering;
14
15/// Line / column pair. Both components are **0-based** to match upstream
16/// (`FileLoc.h`).
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
18pub struct FileLoc {
19    /// 0-based line number.
20    pub line: u32,
21    /// 0-based column number.
22    pub col: u32,
23}
24
25impl FileLoc {
26    /// Construct a [`FileLoc`]. Mirrors the two-argument upstream constructor.
27    #[inline]
28    pub const fn new(line: u32, col: u32) -> Self {
29        Self { line, col }
30    }
31}
32
33impl PartialOrd for FileLoc {
34    #[inline]
35    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
36        Some(self.cmp(other))
37    }
38}
39
40/// Lexicographic ordering on `(line, col)`. Mirrors the explicit
41/// `operator<` / `operator<=` overloads in upstream `FileLoc.h`.
42impl Ord for FileLoc {
43    #[inline]
44    fn cmp(&self, other: &Self) -> Ordering {
45        match self.line.cmp(&other.line) {
46            Ordering::Equal => self.col.cmp(&other.col),
47            ord => ord,
48        }
49    }
50}
51
52impl From<(u32, u32)> for FileLoc {
53    #[inline]
54    fn from((line, col): (u32, u32)) -> Self {
55        Self::new(line, col)
56    }
57}
58
59/// Half-open range `[start, end)` of [`FileLoc`] positions.
60///
61/// Direct port of upstream `FileLocRange`; constructors enforce
62/// `start <= end` via the typed [`FileLocRange::new`] / [`FileLocRange::try_new`]
63/// pair (upstream uses `assert(Start <= End)` in the constructor — the
64/// fallible `try_new` makes that contract surface-visible without panicking
65/// production code).
66#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
67pub struct FileLocRange {
68    pub start: FileLoc,
69    pub end: FileLoc,
70}
71
72impl FileLocRange {
73    /// Construct a [`FileLocRange`] from sorted endpoints.
74    ///
75    /// Returns `None` if `start > end`. The upstream `FileLocRange(Start, End)`
76    /// constructor `assert`s the same invariant; we surface it as an
77    /// `Option`-returning constructor so callers must acknowledge the contract
78    /// without crashing on malformed inputs.
79    #[inline]
80    pub const fn try_new(start: FileLoc, end: FileLoc) -> Option<Self> {
81        // Manual `<=` because const trait dispatch is unstable.
82        let ord = if start.line < end.line {
83            true
84        } else if start.line == end.line {
85            start.col <= end.col
86        } else {
87            false
88        };
89        if ord { Some(Self { start, end }) } else { None }
90    }
91
92    /// Construct a [`FileLocRange`] when the caller has already validated
93    /// `start <= end`. Mirrors upstream's debug-asserted constructor; in
94    /// release builds the upstream form silently constructs an inverted range,
95    /// so this Rust analogue clamps to an empty range at `start` for the
96    /// inverted case to keep downstream invariants intact.
97    #[inline]
98    pub fn new(start: FileLoc, end: FileLoc) -> Self {
99        match Self::try_new(start, end) {
100            Some(r) => r,
101            None => Self { start, end: start },
102        }
103    }
104
105    /// Empty range `[start, start)`.
106    #[inline]
107    pub const fn empty_at(start: FileLoc) -> Self {
108        Self { start, end: start }
109    }
110
111    /// `true` iff `loc` falls inside the half-open range. Mirrors upstream
112    /// `bool contains(FileLoc) const`.
113    #[inline]
114    pub fn contains_loc(&self, loc: FileLoc) -> bool {
115        self.start <= loc && loc < self.end
116    }
117
118    /// `true` iff `other` is fully contained. Mirrors upstream
119    /// `bool contains(FileLocRange) const`.
120    #[inline]
121    pub fn contains_range(&self, other: FileLocRange) -> bool {
122        self.start <= other.start && other.end <= self.end
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    /// Ports the `operator<` / `operator<=` / `operator==` semantics declared
131    /// in `llvm/include/llvm/AsmParser/FileLoc.h`. Upstream has no dedicated
132    /// unit test for these inline operators; the header itself is the
133    /// canonical source.
134    #[test]
135    fn file_loc_orderings() {
136        let a = FileLoc::new(1, 4);
137        let b = FileLoc::new(1, 5);
138        let c = FileLoc::new(2, 0);
139
140        assert!(a < b);
141        assert!(b < c);
142        assert!(a <= a);
143        assert!(a == FileLoc::new(1, 4));
144        assert!(b > a && c > b);
145    }
146
147    /// Ports the `FileLocRange::contains(FileLoc)` arm of `FileLoc.h`.
148    #[test]
149    fn range_contains_loc_is_half_open() {
150        let r = FileLocRange::new(FileLoc::new(1, 0), FileLoc::new(1, 5));
151        assert!(r.contains_loc(FileLoc::new(1, 0)));
152        assert!(r.contains_loc(FileLoc::new(1, 4)));
153        // Half-open: end is *not* included.
154        assert!(!r.contains_loc(FileLoc::new(1, 5)));
155        assert!(!r.contains_loc(FileLoc::new(0, 9)));
156    }
157
158    /// Ports the `FileLocRange::contains(FileLocRange)` arm.
159    #[test]
160    fn range_contains_range() {
161        let outer = FileLocRange::new(FileLoc::new(1, 0), FileLoc::new(3, 0));
162        let inner = FileLocRange::new(FileLoc::new(1, 2), FileLoc::new(2, 8));
163        assert!(outer.contains_range(inner));
164        assert!(outer.contains_range(outer));
165
166        let crossing = FileLocRange::new(FileLoc::new(2, 0), FileLoc::new(4, 0));
167        assert!(!outer.contains_range(crossing));
168    }
169
170    /// llvmkit-specific: surfaces the upstream `assert(Start <= End)` as an
171    /// `Option`-returning constructor. Closest upstream anchor:
172    /// `FileLocRange::FileLocRange(FileLoc, FileLoc)` in `FileLoc.h`.
173    #[test]
174    fn try_new_rejects_inverted_range() {
175        let s = FileLoc::new(2, 0);
176        let e = FileLoc::new(1, 0);
177        assert!(FileLocRange::try_new(s, e).is_none());
178        // Equal endpoints are an empty range, not an inverted one.
179        assert_eq!(FileLocRange::try_new(s, s), Some(FileLocRange::empty_at(s)));
180    }
181}