backhopper_xref_reader/
scanner.rs1use backhopper_xref_graph::Position;
15
16#[derive(Debug, Clone)]
17pub struct Scanner<'a> {
18 src: &'a str,
19 bytes: &'a [u8],
20 pos: Position,
21}
22
23impl<'a> Scanner<'a> {
24 pub fn new(src: &'a str) -> Self {
25 Self {
26 src,
27 bytes: src.as_bytes(),
28 pos: Position::zero(),
29 }
30 }
31
32 pub fn pos(&self) -> Position {
33 self.pos
34 }
35
36 pub fn src(&self) -> &'a str {
37 self.src
38 }
39
40 pub fn src_slice(&self, start: usize, end: usize) -> &'a str {
41 &self.src[start..end]
42 }
43
44 pub fn set_pos(&mut self, pos: Position) {
46 self.pos = pos;
47 }
48
49 pub fn is_eof(&self) -> bool {
50 self.pos.byte_offset as usize >= self.bytes.len()
51 }
52
53 pub fn peek(&self) -> Option<u8> {
54 self.bytes.get(self.pos.byte_offset as usize).copied()
55 }
56
57 pub fn peek_at(&self, offset: usize) -> Option<u8> {
58 self.bytes
59 .get(self.pos.byte_offset as usize + offset)
60 .copied()
61 }
62
63 pub fn advance(&mut self) -> Option<u8> {
64 let p = self.pos.byte_offset as usize;
65 if p >= self.bytes.len() {
66 return None;
67 }
68 let b = self.bytes[p];
69 self.pos.byte_offset += 1;
70 if b == b'\n' {
71 self.pos.line += 1;
72 self.pos.column = 1;
73 } else {
74 self.pos.column += 1;
75 }
76 Some(b)
77 }
78
79 pub fn skip_line(&mut self) {
80 while let Some(b) = self.peek() {
81 self.advance();
82 if b == b'\n' {
83 break;
84 }
85 }
86 }
87
88 pub fn skip_trivia(&mut self) {
91 loop {
92 match self.peek() {
93 Some(b) if b.is_ascii_whitespace() => {
94 self.advance();
95 }
96 Some(b'%') => self.skip_line(),
97 _ => break,
98 }
99 }
100 }
101
102 pub fn consume_string(&mut self) {
105 debug_assert_eq!(self.peek(), Some(b'"'));
106 self.advance();
107 while let Some(b) = self.peek() {
108 self.advance();
109 if b == b'\\' {
110 self.advance();
111 continue;
112 }
113 if b == b'"' {
114 break;
115 }
116 }
117 }
118
119 pub fn consume_quoted_atom(&mut self) {
122 debug_assert_eq!(self.peek(), Some(b'\''));
123 self.advance();
124 while let Some(b) = self.peek() {
125 self.advance();
126 if b == b'\\' {
127 self.advance();
128 continue;
129 }
130 if b == b'\'' {
131 break;
132 }
133 }
134 }
135
136 pub fn consume_char_literal(&mut self) {
138 debug_assert_eq!(self.peek(), Some(b'$'));
139 self.advance();
140 if self.peek() == Some(b'\\') {
141 self.advance();
142 if self.peek() == Some(b'^') {
143 self.advance();
144 }
145 self.advance();
146 } else {
147 self.advance();
148 }
149 }
150
151 pub fn consume_identifier(&mut self) -> &'a str {
154 let start = self.pos.byte_offset as usize;
155 while let Some(b) = self.peek() {
156 if b.is_ascii_alphanumeric() || b == b'_' || b == b'@' {
157 self.advance();
158 } else {
159 break;
160 }
161 }
162 let end = self.pos.byte_offset as usize;
163 &self.src[start..end]
164 }
165
166 pub fn consume_macro_ref(&mut self) -> &'a str {
168 debug_assert_eq!(self.peek(), Some(b'?'));
169 self.advance();
170 let start = self.pos.byte_offset as usize;
171 while let Some(b) = self.peek() {
172 if b.is_ascii_alphanumeric() || b == b'_' {
173 self.advance();
174 } else {
175 break;
176 }
177 }
178 let end = self.pos.byte_offset as usize;
179 &self.src[start..end]
180 }
181
182 pub fn skip_balanced_parens(&mut self) -> u32 {
185 debug_assert_eq!(self.peek(), Some(b'('));
186 self.advance();
187 let mut depth: i32 = 1;
188 let mut top_level_commas: u32 = 0;
189 while let Some(b) = self.peek() {
190 match b {
191 b'(' => {
192 depth += 1;
193 self.advance();
194 }
195 b')' => {
196 depth -= 1;
197 self.advance();
198 if depth == 0 {
199 return top_level_commas;
200 }
201 }
202 b'[' | b'{' => {
203 let opener = b;
205 let closer = if opener == b'[' { b']' } else { b'}' };
206 self.advance();
207 let mut inner = 1i32;
208 while let Some(b2) = self.peek() {
209 match b2 {
210 x if x == opener => {
211 inner += 1;
212 self.advance();
213 }
214 x if x == closer => {
215 inner -= 1;
216 self.advance();
217 if inner == 0 {
218 break;
219 }
220 }
221 b'"' => self.consume_string(),
222 b'\'' => self.consume_quoted_atom(),
223 b'$' => self.consume_char_literal(),
224 b'%' => self.skip_line(),
225 _ => {
226 self.advance();
227 }
228 }
229 }
230 }
231 b',' if depth == 1 => {
232 top_level_commas += 1;
233 self.advance();
234 }
235 b'"' => self.consume_string(),
236 b'\'' => self.consume_quoted_atom(),
237 b'$' => self.consume_char_literal(),
238 b'%' => self.skip_line(),
239 _ => {
240 self.advance();
241 }
242 }
243 }
244 top_level_commas
245 }
246}
247
248#[derive(Debug, Clone, Copy)]
250pub struct ParenWalk {
251 pub commas: u32,
252 pub had_content: bool,
253 pub closed: bool,
254}
255
256pub fn walk_paren_group(
265 sc: &mut Scanner<'_>,
266 mut on_ident: impl FnMut(&mut Scanner<'_>) -> bool,
267) -> ParenWalk {
268 debug_assert_eq!(sc.peek(), Some(b'('));
269 sc.advance();
270 let mut depth = 1i32;
271 let mut commas: u32 = 0;
272 let mut had_content = false;
273 let mut closed = false;
274 while let Some(byte) = sc.peek() {
275 match byte {
276 b'(' => {
277 depth += 1;
278 had_content = true;
279 sc.advance();
280 }
281 b')' => {
282 depth -= 1;
283 sc.advance();
284 if depth == 0 {
285 closed = true;
286 break;
287 }
288 }
289 b'[' | b'{' => {
290 depth += 1;
291 had_content = true;
292 sc.advance();
293 }
294 b']' | b'}' => {
295 depth -= 1;
296 sc.advance();
297 }
298 b'<' if sc.peek_at(1) == Some(b'<') => {
301 depth += 1;
302 had_content = true;
303 sc.advance();
304 sc.advance();
305 }
306 b'>' if sc.peek_at(1) == Some(b'>') => {
307 depth -= 1;
308 sc.advance();
309 sc.advance();
310 }
311 b',' if depth == 1 => {
312 commas += 1;
313 sc.advance();
314 }
315 b'"' => {
316 sc.consume_string();
317 had_content = true;
318 }
319 b'\'' => {
320 sc.consume_quoted_atom();
321 had_content = true;
322 }
323 b'$' => {
324 sc.consume_char_literal();
325 had_content = true;
326 }
327 b'%' => sc.skip_line(),
328 b'?' | b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
329 had_content = true;
330 if !on_ident(sc) {
331 sc.advance();
332 }
333 }
334 _ => {
335 if !byte.is_ascii_whitespace() {
336 had_content = true;
337 }
338 sc.advance();
339 }
340 }
341 }
342 ParenWalk {
343 commas,
344 had_content,
345 closed,
346 }
347}
348
349pub fn arity_from_commas(top_level_commas: u32, had_any_content: bool) -> u32 {
352 if had_any_content {
353 top_level_commas + 1
354 } else {
355 0
356 }
357}