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}