1use std::str;
11
12use backhopper_erlang_scan::{
13 for_each_top_level_byte, is_name_byte, number_span, skip_char_literal_span,
14};
15
16use crate::model::spec_ast::SpecType;
17
18pub fn parse_signature_return(signature: &str) -> SpecType {
19 let Some(idx) = find_top_level_arrow(signature) else {
20 return SpecType::Unknown;
21 };
22 let rest = &signature[idx + 2..];
23 let body = strip_when_guard(rest);
24 parse(body)
25}
26
27pub fn parse(text: &str) -> SpecType {
28 let mut p = Parser::new(text);
29 let t = p.parse_union();
30 p.skip_ws();
31 t.canonicalise()
32}
33
34fn find_top_level_arrow(s: &str) -> Option<usize> {
35 let bytes = s.as_bytes();
36 for_each_top_level_byte(s, |i| bytes[i] == b'-' && bytes.get(i + 1) == Some(&b'>'))
37}
38
39fn strip_when_guard(rest: &str) -> &str {
40 let bytes = rest.as_bytes();
41 let found = for_each_top_level_byte(rest, |i| {
42 bytes[i..].starts_with(b"when") && is_word_boundary(bytes, i, i + 4)
43 });
44 match found {
45 Some(i) => rest[..i].trim_end(),
46 None => rest.trim(),
47 }
48}
49
50fn is_word_boundary(bytes: &[u8], start: usize, end: usize) -> bool {
51 let before_ok = start == 0 || !is_name_byte(bytes[start - 1]);
52 let after_ok = end >= bytes.len() || !is_name_byte(bytes[end]);
53 before_ok && after_ok
54}
55
56struct Parser<'a> {
57 bytes: &'a [u8],
58 cursor: usize,
59}
60
61const RECURSION_BUDGET: usize = 32;
62
63impl<'a> Parser<'a> {
64 fn new(text: &'a str) -> Self {
65 Self {
66 bytes: text.as_bytes(),
67 cursor: 0,
68 }
69 }
70
71 fn skip_ws(&mut self) {
72 while self.cursor < self.bytes.len() {
73 let b = self.bytes[self.cursor];
74 if b.is_ascii_whitespace() {
75 self.cursor += 1;
76 } else {
77 break;
78 }
79 }
80 }
81
82 fn peek(&self) -> Option<u8> {
83 self.bytes.get(self.cursor).copied()
84 }
85
86 fn peek_at(&self, offset: usize) -> Option<u8> {
87 self.bytes.get(self.cursor + offset).copied()
88 }
89
90 fn eat(&mut self, expected: u8) -> bool {
91 self.skip_ws();
92 if self.peek() == Some(expected) {
93 self.cursor += 1;
94 true
95 } else {
96 false
97 }
98 }
99
100 fn parse_union(&mut self) -> SpecType {
101 self.parse_union_with_budget(RECURSION_BUDGET)
102 }
103
104 fn parse_union_with_budget(&mut self, budget: usize) -> SpecType {
105 if budget == 0 {
106 return SpecType::Unknown;
107 }
108 let first = self.parse_single(budget - 1);
109 self.skip_ws();
110 if self.peek() != Some(b'|') {
111 return first;
112 }
113 let mut variants = vec![first];
114 while self.eat(b'|') {
115 let v = self.parse_single(budget - 1);
116 variants.push(v);
117 }
118 SpecType::Union { variants }
119 }
120
121 fn parse_single(&mut self, budget: usize) -> SpecType {
122 if budget == 0 {
123 return SpecType::Unknown;
124 }
125 self.skip_ws();
126 let Some(b) = self.peek() else {
127 return SpecType::Unknown;
128 };
129 match b {
130 b'{' => self.parse_tuple(budget),
131 b'[' => self.parse_list(budget),
132 b'#' => self.parse_hash(budget),
133 b'\'' => self.parse_quoted_atom(),
134 b'_' => self.parse_underscore(),
135 b'-' | b'0'..=b'9' | b'$' => self.parse_integer(),
136 b'<' if self.peek_at(1) == Some(b'<') => self.parse_binary(),
137 b if b.is_ascii_uppercase() => self.parse_var_or_unknown(budget),
138 b if b.is_ascii_lowercase() => self.parse_lowercase_form(budget),
139 _ => {
140 self.skip_unknown();
141 SpecType::Unknown
142 }
143 }
144 }
145
146 fn parse_tuple(&mut self, budget: usize) -> SpecType {
147 debug_assert_eq!(self.peek(), Some(b'{'));
148 self.cursor += 1;
149 self.skip_ws();
150 if self.peek() == Some(b'}') {
151 self.cursor += 1;
152 return SpecType::Tuple { arity: 0 };
153 }
154 let mut elements: Vec<SpecType> = Vec::new();
155 loop {
156 let elem = self.parse_union_with_budget(budget - 1);
157 elements.push(elem);
158 self.skip_ws();
159 match self.peek() {
160 Some(b',') => {
161 self.cursor += 1;
162 }
163 Some(b'}') => {
164 self.cursor += 1;
165 break;
166 }
167 _ => {
168 self.skip_until(b'}');
169 return SpecType::Unknown;
170 }
171 }
172 }
173 let arity = elements.len().min(u8::MAX as usize) as u8;
174 match elements.first() {
175 Some(SpecType::Atom { name }) => SpecType::TaggedTuple {
176 tag: name.clone(),
177 arity,
178 },
179 _ => SpecType::Tuple { arity },
180 }
181 }
182
183 fn parse_list(&mut self, budget: usize) -> SpecType {
184 debug_assert_eq!(self.peek(), Some(b'['));
185 self.cursor += 1;
186 self.skip_ws();
187 if self.peek() == Some(b']') {
188 self.cursor += 1;
189 return SpecType::List {
190 element: Box::new(SpecType::Unknown),
191 };
192 }
193 let elem = self.parse_union_with_budget(budget - 1);
194 self.skip_until(b']');
195 SpecType::List {
196 element: Box::new(elem),
197 }
198 }
199
200 fn parse_hash(&mut self, budget: usize) -> SpecType {
201 debug_assert_eq!(self.peek(), Some(b'#'));
202 self.cursor += 1;
203 self.skip_ws();
204 match self.peek() {
205 Some(b'{') => {
206 self.cursor += 1;
207 self.skip_until_matching(b'{', b'}');
208 SpecType::Map
209 }
210 Some(b) if b.is_ascii_lowercase() || b == b'\'' => {
211 let name = self.read_atom_name();
212 self.skip_ws();
213 if self.peek() == Some(b'{') {
214 self.cursor += 1;
215 self.skip_until_matching(b'{', b'}');
216 }
217 SpecType::Record { name }
218 }
219 _ => {
220 let _ = budget;
221 self.skip_unknown();
222 SpecType::Unknown
223 }
224 }
225 }
226
227 fn parse_quoted_atom(&mut self) -> SpecType {
228 let name = self.read_quoted_atom();
229 if name.is_empty() {
230 return SpecType::Unknown;
231 }
232 SpecType::Atom { name }
233 }
234
235 fn parse_underscore(&mut self) -> SpecType {
236 let next = self.peek_at(1);
237 let is_word = match next {
238 Some(c) => is_name_byte(c),
239 None => false,
240 };
241 if !is_word {
242 self.cursor += 1;
243 return SpecType::Var {
244 name: "_".to_owned(),
245 };
246 }
247 let name = self.read_identifier();
248 SpecType::Var { name }
249 }
250
251 fn parse_integer(&mut self) -> SpecType {
252 let Some(low) = self.read_integer_value() else {
253 self.skip_unknown();
254 return SpecType::Unknown;
255 };
256 let save = self.cursor;
257 self.skip_ws();
258 if self.peek() == Some(b'.') && self.peek_at(1) == Some(b'.') {
259 self.cursor += 2;
260 self.skip_ws();
261 let Some(high) = self.read_integer_value() else {
262 self.skip_unknown();
263 return SpecType::Unknown;
264 };
265 return SpecType::Range { low, high };
266 }
267 self.cursor = save;
268 SpecType::Integer { value: low }
269 }
270
271 fn read_integer_value(&mut self) -> Option<i64> {
275 let negative = self.peek() == Some(b'-');
276 if negative {
277 self.cursor += 1;
278 }
279 if self.peek() == Some(b'$') {
280 let span = skip_char_literal_span(self.bytes, self.cursor);
281 let raw = str::from_utf8(&self.bytes[self.cursor..self.cursor + span]).ok()?;
282 let value = char_literal_value(raw)?;
283 self.cursor += span;
284 return Some(if negative { -value } else { value });
285 }
286 let span = number_span(self.bytes, self.cursor)?;
287 let raw = str::from_utf8(&self.bytes[self.cursor..self.cursor + span]).ok()?;
288 let value = integer_literal_value(raw)?;
289 self.cursor += span;
290 Some(if negative { -value } else { value })
291 }
292
293 fn parse_binary(&mut self) -> SpecType {
294 debug_assert_eq!(self.peek(), Some(b'<'));
295 self.cursor += 2;
296 let mut depth = 1i32;
297 while self.cursor < self.bytes.len() {
298 if self.peek() == Some(b'<') && self.peek_at(1) == Some(b'<') {
299 depth += 1;
300 self.cursor += 2;
301 } else if self.peek() == Some(b'>') && self.peek_at(1) == Some(b'>') {
302 depth -= 1;
303 self.cursor += 2;
304 if depth == 0 {
305 return SpecType::Binary;
306 }
307 } else {
308 self.cursor += 1;
309 }
310 }
311 SpecType::Binary
312 }
313
314 fn parse_var_or_unknown(&mut self, budget: usize) -> SpecType {
315 let name = self.read_identifier();
316 self.skip_ws();
317 match self.peek() {
318 Some(b':') if self.peek_at(1) == Some(b':') => {
320 self.cursor += 2;
321 self.parse_union_with_budget(budget)
322 }
323 Some(b':') => {
324 self.cursor += 1;
325 let sub = self.read_identifier();
326 self.skip_ws();
327 if self.peek() == Some(b'(') {
328 let arity = self.read_call_args(budget);
329 SpecType::TypeCall {
330 module: Some(name),
331 name: sub,
332 arity,
333 }
334 } else {
335 SpecType::Unknown
336 }
337 }
338 _ => SpecType::Var { name },
339 }
340 }
341
342 fn parse_lowercase_form(&mut self, budget: usize) -> SpecType {
343 if self.is_at_keyword(b"fun") {
344 self.cursor += 3;
345 self.skip_balanced(b'(', b')');
346 return SpecType::Fun;
347 }
348 let name = self.read_identifier();
349 self.skip_ws();
350 match self.peek() {
351 Some(b':') => {
352 self.cursor += 1;
353 let sub = self.read_identifier();
354 self.skip_ws();
355 if self.peek() == Some(b'(') {
356 let arity = self.read_call_args(budget);
357 SpecType::TypeCall {
358 module: Some(name),
359 name: sub,
360 arity,
361 }
362 } else {
363 SpecType::Unknown
364 }
365 }
366 Some(b'(') => {
367 let arity = self.read_call_args(budget);
368 if is_builtin_type(&name) {
369 SpecType::Builtin { name }
370 } else {
371 SpecType::TypeCall {
372 module: None,
373 name,
374 arity,
375 }
376 }
377 }
378 _ => SpecType::Atom { name },
379 }
380 }
381
382 fn is_at_keyword(&self, word: &[u8]) -> bool {
383 if self.cursor + word.len() > self.bytes.len() {
384 return false;
385 }
386 if &self.bytes[self.cursor..self.cursor + word.len()] != word {
387 return false;
388 }
389 is_word_boundary(self.bytes, self.cursor, self.cursor + word.len())
390 }
391
392 fn read_identifier(&mut self) -> String {
393 let start = self.cursor;
394 while let Some(b) = self.peek() {
395 if is_name_byte(b) {
396 self.cursor += 1;
397 } else {
398 break;
399 }
400 }
401 str::from_utf8(&self.bytes[start..self.cursor])
402 .unwrap_or("")
403 .to_owned()
404 }
405
406 fn read_atom_name(&mut self) -> String {
407 if self.peek() == Some(b'\'') {
408 self.read_quoted_atom()
409 } else {
410 self.read_identifier()
411 }
412 }
413
414 fn read_quoted_atom(&mut self) -> String {
415 debug_assert_eq!(self.peek(), Some(b'\''));
416 self.cursor += 1;
417 let start = self.cursor;
418 let mut prev_escape = false;
419 while let Some(b) = self.peek() {
420 if prev_escape {
421 prev_escape = false;
422 self.cursor += 1;
423 continue;
424 }
425 if b == b'\\' {
426 prev_escape = true;
427 self.cursor += 1;
428 continue;
429 }
430 if b == b'\'' {
431 let raw = &self.bytes[start..self.cursor];
432 self.cursor += 1;
433 return str::from_utf8(raw).unwrap_or("").to_owned();
434 }
435 self.cursor += 1;
436 }
437 String::new()
438 }
439
440 fn read_call_args(&mut self, budget: usize) -> u8 {
441 debug_assert_eq!(self.peek(), Some(b'('));
442 self.cursor += 1;
443 self.skip_ws();
444 if self.peek() == Some(b')') {
445 self.cursor += 1;
446 return 0;
447 }
448 let mut count = 0u32;
449 loop {
450 let _ = self.parse_union_with_budget(budget.saturating_sub(1));
451 count += 1;
452 self.skip_ws();
453 match self.peek() {
454 Some(b',') => self.cursor += 1,
455 Some(b')') => {
456 self.cursor += 1;
457 break;
458 }
459 _ => {
460 self.skip_until(b')');
461 break;
462 }
463 }
464 }
465 count.min(u32::from(u8::MAX)) as u8
466 }
467
468 fn skip_unknown(&mut self) {
469 while let Some(b) = self.peek() {
470 if matches!(b, b',' | b'|' | b')' | b']' | b'}') {
471 break;
472 }
473 self.cursor += 1;
474 }
475 }
476
477 fn skip_until(&mut self, target: u8) {
478 let mut depth_paren = 0i32;
479 let mut depth_brace = 0i32;
480 let mut depth_brack = 0i32;
481 while let Some(b) = self.peek() {
482 match b {
483 b'(' => depth_paren += 1,
484 b')' => {
485 if depth_paren == 0 && target == b')' {
486 self.cursor += 1;
487 return;
488 }
489 depth_paren -= 1;
490 }
491 b'{' => depth_brace += 1,
492 b'}' => {
493 if depth_brace == 0 && target == b'}' {
494 self.cursor += 1;
495 return;
496 }
497 depth_brace -= 1;
498 }
499 b'[' => depth_brack += 1,
500 b']' => {
501 if depth_brack == 0 && target == b']' {
502 self.cursor += 1;
503 return;
504 }
505 depth_brack -= 1;
506 }
507 _ => {}
508 }
509 self.cursor += 1;
510 }
511 }
512
513 fn skip_until_matching(&mut self, open: u8, close: u8) {
514 let mut depth = 1i32;
515 while let Some(b) = self.peek() {
516 if b == open {
517 depth += 1;
518 } else if b == close {
519 depth -= 1;
520 if depth == 0 {
521 self.cursor += 1;
522 return;
523 }
524 }
525 self.cursor += 1;
526 }
527 }
528
529 fn skip_balanced(&mut self, open: u8, close: u8) {
530 self.skip_ws();
531 if self.peek() != Some(open) {
532 return;
533 }
534 self.cursor += 1;
535 self.skip_until_matching(open, close);
536 }
537}
538
539fn integer_literal_value(raw: &str) -> Option<i64> {
542 let s = raw.replace('_', "");
543 if s.contains('.') {
544 return None;
545 }
546 match s.split_once('#') {
547 Some((base, digits)) => {
548 if digits.contains('#') {
549 return None;
550 }
551 let base: u32 = base.parse().ok()?;
552 if !(2..=36).contains(&base) {
553 return None;
554 }
555 i64::from_str_radix(digits, base).ok()
556 }
557 None => s.parse().ok(),
558 }
559}
560
561fn char_literal_value(raw: &str) -> Option<i64> {
564 let body = raw.strip_prefix('$')?;
565 let mut chars = body.chars();
566 let first = chars.next()?;
567 if first != '\\' {
568 return Some(i64::from(u32::from(first)));
569 }
570 let esc = chars.next()?;
571 let value = match esc {
572 'n' => 10,
573 'r' => 13,
574 't' => 9,
575 'b' => 8,
576 'f' => 12,
577 'e' => 27,
578 's' => 32,
579 'd' => 127,
580 'v' => 11,
581 '^' => u32::from(chars.next()?) & 31,
582 'x' => {
583 let rest = chars.as_str();
584 let hex = rest
585 .strip_prefix('{')
586 .and_then(|r| r.strip_suffix('}'))
587 .unwrap_or(rest);
588 u32::from_str_radix(hex, 16).ok()?
589 }
590 '0'..='7' => {
591 let mut v = u32::from(esc) - u32::from('0');
592 for c in chars {
593 v = v * 8 + c.to_digit(8)?;
594 }
595 v
596 }
597 other => u32::from(other),
598 };
599 Some(i64::from(value))
600}
601
602fn is_builtin_type(name: &str) -> bool {
603 matches!(
604 name,
605 "any"
606 | "arity"
607 | "atom"
608 | "binary"
609 | "bitstring"
610 | "boolean"
611 | "byte"
612 | "char"
613 | "dynamic"
614 | "float"
615 | "function"
616 | "identifier"
617 | "integer"
618 | "iodata"
619 | "iolist"
620 | "list"
621 | "map"
622 | "maybe_improper_list"
623 | "mfa"
624 | "module"
625 | "neg_integer"
626 | "nil"
627 | "no_return"
628 | "node"
629 | "non_neg_integer"
630 | "none"
631 | "nonempty_binary"
632 | "nonempty_bitstring"
633 | "nonempty_improper_list"
634 | "nonempty_list"
635 | "nonempty_maybe_improper_list"
636 | "nonempty_string"
637 | "number"
638 | "pid"
639 | "port"
640 | "pos_integer"
641 | "reference"
642 | "string"
643 | "term"
644 | "timeout"
645 | "tuple"
646 )
647}