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