1use std::borrow::Cow;
14
15pub fn is_type_declaration_keyword(name: &str) -> bool {
19 matches!(name, "type" | "opaque" | "nominal")
20}
21
22pub fn opens_type_ref_context(name: &str) -> bool {
25 is_type_declaration_keyword(name) || matches!(name, "spec" | "callback")
26}
27
28pub fn skip_char_literal_span(bytes: &[u8], at: usize) -> usize {
32 debug_assert_eq!(bytes[at], b'$');
33 let next = at + 1;
34 if next >= bytes.len() {
35 return 1;
36 }
37 if bytes[next] != b'\\' {
38 return 2;
39 }
40 let esc = next + 1;
41 if esc >= bytes.len() {
42 return 2;
43 }
44 match bytes[esc] {
45 b'^' => {
46 if esc + 1 < bytes.len() {
47 4
48 } else {
49 3
50 }
51 }
52 b'x' if bytes.get(esc + 1) == Some(&b'{') => {
53 let mut i = esc + 2;
54 while i < bytes.len() && bytes[i] != b'}' {
55 i += 1;
56 }
57 if i < bytes.len() { i + 1 - at } else { i - at }
58 }
59 b'x' => {
60 let mut i = esc + 1;
61 let stop = (esc + 3).min(bytes.len());
62 while i < stop && bytes[i].is_ascii_hexdigit() {
63 i += 1;
64 }
65 i - at
66 }
67 b'0'..=b'7' => {
68 let mut i = esc + 1;
69 let stop = (esc + 3).min(bytes.len());
70 while i < stop && (b'0'..=b'7').contains(&bytes[i]) {
71 i += 1;
72 }
73 i - at
74 }
75 _ => 3,
76 }
77}
78
79pub fn string_span(bytes: &[u8], at: usize) -> Option<usize> {
84 match bytes.get(at)? {
85 b'"' => {
86 if let Some(span) = triple_quoted_span(bytes, at) {
87 return Some(span);
88 }
89 Some(quoted_span(bytes, at, b'"'))
90 }
91 b'~' => sigil_span(bytes, at),
92 _ => None,
93 }
94}
95
96pub fn quoted_atom_span(bytes: &[u8], at: usize) -> Option<usize> {
100 if bytes.get(at) != Some(&b'\'') {
101 return None;
102 }
103 Some(quoted_span(bytes, at, b'\''))
104}
105
106pub fn literal_span(bytes: &[u8], at: usize) -> Option<usize> {
111 match *bytes.get(at)? {
112 b'"' | b'~' => string_span(bytes, at),
113 b'\'' => quoted_atom_span(bytes, at),
114 b'$' => Some(skip_char_literal_span(bytes, at)),
115 _ => None,
116 }
117}
118
119fn quoted_span(bytes: &[u8], at: usize, quote: u8) -> usize {
120 let mut i = at + 1;
121 while i < bytes.len() {
122 match bytes[i] {
123 b'\\' => i = (i + 2).min(bytes.len()),
124 b if b == quote => return i + 1 - at,
125 _ => i += 1,
126 }
127 }
128 i - at
129}
130
131pub fn dot_terminates(bytes: &[u8], i: usize) -> bool {
135 if bytes.get(i) != Some(&b'.') {
136 return false;
137 }
138 match bytes.get(i + 1) {
139 None => true,
140 Some(b) => b.is_ascii_whitespace() || *b == b'%',
141 }
142}
143
144fn based_digit_value(b: u8) -> Option<u32> {
147 match b {
148 b'0'..=b'9' => Some(u32::from(b - b'0')),
149 b'a'..=b'z' => Some(u32::from(b - b'a') + 10),
150 b'A'..=b'Z' => Some(u32::from(b - b'A') + 10),
151 _ => None,
152 }
153}
154
155fn based_digit_run(bytes: &[u8], from: usize, base: u32) -> usize {
158 let is_digit = |b: u8| based_digit_value(b).is_some_and(|v| v < base);
159 let mut i = from;
160 while i < bytes.len() {
161 if is_digit(bytes[i]) {
162 i += 1;
163 } else if bytes[i] == b'_' && i > from && bytes.get(i + 1).copied().is_some_and(is_digit) {
164 i += 1;
165 } else {
166 break;
167 }
168 }
169 i
170}
171
172pub fn number_span(bytes: &[u8], at: usize) -> Option<usize> {
179 if !bytes.get(at)?.is_ascii_digit() {
180 return None;
181 }
182 let int_end = based_digit_run(bytes, at, 10);
183 if bytes.get(int_end) == Some(&b'#') {
184 let base: Option<u32> = std::str::from_utf8(&bytes[at..int_end])
185 .ok()
186 .and_then(|s| s.replace('_', "").parse().ok());
187 let Some(base) = base.filter(|b| (2..=36).contains(b)) else {
188 return Some(int_end - at);
189 };
190 let digits_end = based_digit_run(bytes, int_end + 1, base);
191 if digits_end == int_end + 1 {
192 return Some(int_end - at);
193 }
194 let mut end = digits_end;
195 if bytes.get(end) == Some(&b'.')
196 && bytes
197 .get(end + 1)
198 .copied()
199 .is_some_and(|b| based_digit_value(b).is_some_and(|v| v < base))
200 {
201 end = based_digit_run(bytes, end + 1, base);
202 if bytes.get(end) == Some(&b'#')
203 && let Some(exp) = exponent_run(bytes, end + 1)
204 {
205 end = exp;
206 }
207 }
208 return Some(end - at);
209 }
210 let mut end = int_end;
211 if bytes.get(end) == Some(&b'.')
212 && bytes
213 .get(end + 1)
214 .copied()
215 .is_some_and(|b| b.is_ascii_digit())
216 {
217 end = based_digit_run(bytes, end + 1, 10);
218 if let Some(exp) = exponent_run(bytes, end) {
219 end = exp;
220 }
221 }
222 Some(end - at)
223}
224
225fn exponent_run(bytes: &[u8], from: usize) -> Option<usize> {
228 if !matches!(bytes.get(from), Some(b'e') | Some(b'E')) {
229 return None;
230 }
231 let mut i = from + 1;
232 if matches!(bytes.get(i), Some(b'+') | Some(b'-')) {
233 i += 1;
234 }
235 if !bytes.get(i).copied().is_some_and(|b| b.is_ascii_digit()) {
236 return None;
237 }
238 Some(based_digit_run(bytes, i, 10))
239}
240
241pub fn hash_inside_number(bytes: &[u8], at: usize) -> bool {
247 debug_assert_eq!(bytes.get(at), Some(&b'#'));
248 let mut start = at;
249 while start > 0 {
250 let prev = bytes[start - 1];
251 if is_name_byte(prev) {
252 start -= 1;
253 } else if (prev == b'.' || prev == b'#')
254 && start >= 2
255 && is_name_byte(bytes[start - 2])
256 && bytes.get(start).copied().is_some_and(is_name_byte)
257 {
258 start -= 1;
259 } else {
260 break;
261 }
262 }
263 if start == at || !bytes[start].is_ascii_digit() {
264 return false;
265 }
266 number_span(bytes, start).is_some_and(|n| start + n > at)
267}
268
269fn quote_run_len(bytes: &[u8], at: usize) -> usize {
272 let mut i = at;
273 while i < bytes.len() && bytes[i] == b'"' {
274 i += 1;
275 }
276 i - at
277}
278
279fn skip_horizontal_ws(bytes: &[u8], mut i: usize) -> usize {
280 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
281 i += 1;
282 }
283 i
284}
285
286pub fn triple_quoted_span(bytes: &[u8], at: usize) -> Option<usize> {
297 let open = quote_run_len(bytes, at);
298 if open < 3 {
299 return None;
300 }
301 let mut i = at + open;
302 while i < bytes.len() {
303 while i < bytes.len() && bytes[i] != b'\n' {
304 i += 1;
305 }
306 if i == bytes.len() {
307 break;
308 }
309 i += 1;
310 let content = skip_horizontal_ws(bytes, i);
311 let close = quote_run_len(bytes, content);
312 if close >= open {
313 return Some(content + close - at);
314 }
315 }
316 Some(bytes.len() - at)
317}
318
319fn sigil_closer(open: u8) -> Option<u8> {
322 match open {
323 b'(' => Some(b')'),
324 b'[' => Some(b']'),
325 b'{' => Some(b'}'),
326 b'<' => Some(b'>'),
327 b'/' | b'|' | b'#' | b'`' | b'\'' | b'"' => Some(open),
328 _ => None,
329 }
330}
331
332pub fn sigil_span(bytes: &[u8], at: usize) -> Option<usize> {
337 if bytes.get(at) != Some(&b'~') {
338 return None;
339 }
340 let mut i = at + 1;
341 while i < bytes.len() && is_name_byte(bytes[i]) {
342 i += 1;
343 }
344 let open = *bytes.get(i)?;
345 let closer = sigil_closer(open)?;
346 let prefix = &bytes[at + 1..i];
347 let escaped = matches!(prefix, b"" | b"b" | b"s");
348 if open == b'"'
349 && let Some(span) = triple_quoted_span(bytes, i)
350 {
351 i += span;
352 } else {
353 i += 1;
354 loop {
355 let Some(&b) = bytes.get(i) else {
356 return Some(bytes.len() - at);
357 };
358 i += 1;
359 if b == b'\\' && escaped {
360 i += 1;
361 } else if b == closer {
362 break;
363 }
364 }
365 }
366 while i < bytes.len() && is_name_byte(bytes[i]) {
367 i += 1;
368 }
369 Some(i - at)
370}
371
372#[derive(Debug, Clone, Copy, Default)]
377pub struct BlockDepth(i32);
378
379impl BlockDepth {
380 pub fn in_block(self) -> bool {
381 self.0 > 0
382 }
383
384 pub fn observe(&mut self, bytes: &[u8], at: usize) -> usize {
388 let mut j = at;
389 while j < bytes.len() && is_name_byte(bytes[j]) {
390 j += 1;
391 }
392 if preceded_by_hash(bytes, at) {
394 return j - at;
395 }
396 match &bytes[at..j] {
397 b"case" | b"if" | b"begin" | b"receive" | b"try" => self.0 += 1,
398 b"end" => self.0 = (self.0 - 1).max(0),
399 b"fun" if fun_opens_block(bytes, j) => self.0 += 1,
400 b"maybe" if maybe_opens_block(bytes, j) => self.0 += 1,
401 _ => {}
402 }
403 j - at
404 }
405}
406
407fn preceded_by_hash(bytes: &[u8], at: usize) -> bool {
408 let mut i = at;
409 while i > 0 && (bytes[i - 1] == b' ' || bytes[i - 1] == b'\t') {
410 i -= 1;
411 }
412 i > 0 && bytes[i - 1] == b'#'
413}
414
415const EXPRESSION_CONTINUERS: [&[u8]; 19] = [
418 b"of", b"when", b"andalso", b"orelse", b"end", b"after", b"else", b"div", b"rem", b"band",
419 b"bor", b"bxor", b"bsl", b"bsr", b"and", b"or", b"xor", b"not", b"bnot",
420];
421
422fn maybe_opens_block(bytes: &[u8], after_maybe: usize) -> bool {
428 let k = skip_ws(bytes, after_maybe);
429 let Some(&b) = bytes.get(k) else {
430 return false;
431 };
432 if b == b'<' {
433 return bytes.get(k + 1) == Some(&b'<');
434 }
435 match b {
436 b'[' | b'{' | b'"' | b'\'' | b'$' | b'~' => true,
437 _ if b.is_ascii_uppercase() || b == b'_' || b.is_ascii_digit() => true,
438 _ if b.is_ascii_lowercase() => {
439 let mut m = k;
440 while m < bytes.len() && is_name_byte(bytes[m]) {
441 m += 1;
442 }
443 !EXPRESSION_CONTINUERS.contains(&&bytes[k..m])
444 }
445 _ => false,
446 }
447}
448
449pub fn is_name_byte(b: u8) -> bool {
450 b.is_ascii_alphanumeric() || b == b'_' || b == b'@'
451}
452
453pub fn is_bare_atom(s: &str) -> bool {
456 let bytes = s.as_bytes();
457 match bytes.first() {
458 Some(b) if b.is_ascii_lowercase() => {}
459 _ => return false,
460 }
461 bytes.iter().all(|&b| is_name_byte(b))
462}
463
464pub fn for_each_top_level_byte(s: &str, mut visit: impl FnMut(usize) -> bool) -> Option<usize> {
470 let bytes = s.as_bytes();
471 let mut depth = 0i32;
472 let mut depth_angle = 0i32;
473 let mut i = 0usize;
474 while i < bytes.len() {
475 match bytes[i] {
476 b'"' | b'~' => {
477 if let Some(span) = string_span(bytes, i) {
478 i += span;
479 continue;
480 }
481 }
482 b'\'' => {
483 if let Some(span) = quoted_atom_span(bytes, i) {
484 i += span;
485 continue;
486 }
487 }
488 b'$' => {
489 i += skip_char_literal_span(bytes, i);
490 continue;
491 }
492 b'%' => {
493 i += line_comment_span(bytes, i);
494 continue;
495 }
496 _ => {}
497 }
498 match bytes[i] {
499 b'<' if bytes.get(i + 1) == Some(&b'<') => {
500 depth_angle += 1;
501 i += 2;
502 continue;
503 }
504 b'>' if bytes.get(i + 1) == Some(&b'>') && depth_angle > 0 => {
505 depth_angle -= 1;
506 i += 2;
507 continue;
508 }
509 b'(' | b'[' | b'{' => depth += 1,
510 b')' | b']' | b'}' => depth -= 1,
511 _ if depth == 0 && depth_angle == 0 => {
512 if visit(i) {
513 return Some(i);
514 }
515 }
516 _ => {}
517 }
518 i += 1;
519 }
520 None
521}
522
523pub fn remove_line_comments(s: &str) -> Cow<'_, str> {
527 let bytes = s.as_bytes();
528 let mut out: Option<String> = None;
529 let mut i = 0usize;
530 while i < bytes.len() {
531 let span = match bytes[i] {
532 b'%' => {
533 out.get_or_insert_with(|| s[..i].to_string());
534 i += line_comment_span(bytes, i);
535 continue;
536 }
537 b'"' | b'~' => string_span(bytes, i).unwrap_or(1),
538 b'\'' => quoted_atom_span(bytes, i).unwrap_or(1),
539 b'$' => skip_char_literal_span(bytes, i),
540 _ => 1,
541 };
542 let mut end = (i + span).min(bytes.len());
544 while !s.is_char_boundary(end) {
545 end += 1;
546 }
547 if let Some(acc) = out.as_mut() {
548 acc.push_str(&s[i..end]);
549 }
550 i = end;
551 }
552 match out {
553 Some(acc) => Cow::Owned(acc),
554 None => Cow::Borrowed(s),
555 }
556}
557
558fn line_comment_span(bytes: &[u8], at: usize) -> usize {
562 let mut i = at;
563 while i < bytes.len() && bytes[i] != b'\n' {
564 i += 1;
565 }
566 i - at
567}
568
569fn skip_ws(bytes: &[u8], mut i: usize) -> usize {
570 loop {
571 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
572 i += 1;
573 }
574 if i < bytes.len() && bytes[i] == b'%' {
575 i += line_comment_span(bytes, i);
576 continue;
577 }
578 return i;
579 }
580}
581
582fn fun_opens_block(bytes: &[u8], after_fun: usize) -> bool {
589 let mut j = skip_ws(bytes, after_fun);
590 if bytes
591 .get(j)
592 .is_some_and(|b| b.is_ascii_uppercase() || *b == b'_')
593 {
594 while j < bytes.len() && is_name_byte(bytes[j]) {
595 j += 1;
596 }
597 j = skip_ws(bytes, j);
598 }
599 if bytes.get(j) != Some(&b'(') {
600 return false;
601 }
602 let Some(close) = matching_bracket(bytes, j) else {
603 return false;
604 };
605 let j = skip_ws(bytes, close + 1);
606 bytes[j..].starts_with(b"->")
607 || (bytes[j..].starts_with(b"when") && !bytes.get(j + 4).copied().is_some_and(is_name_byte))
608}
609
610pub fn matching_bracket(bytes: &[u8], open: usize) -> Option<usize> {
615 let opener = *bytes.get(open)?;
616 let closer = match opener {
617 b'(' => b')',
618 b'[' => b']',
619 b'{' => b'}',
620 _ => return None,
621 };
622 let mut depth = 0i32;
623 let mut in_str = false;
624 let mut in_atom = false;
625 let mut i = open;
626 while i < bytes.len() {
627 let b = bytes[i];
628 if in_str || in_atom {
629 match b {
630 b'\\' => i += 1,
631 b'"' if in_str => in_str = false,
632 b'\'' if in_atom => in_atom = false,
633 _ => {}
634 }
635 i += 1;
636 continue;
637 }
638 match b {
639 b'"' => {
640 if let Some(span) = triple_quoted_span(bytes, i) {
641 i += span;
642 continue;
643 }
644 in_str = true;
645 }
646 b'\'' => in_atom = true,
647 b'$' => {
648 i += skip_char_literal_span(bytes, i);
649 continue;
650 }
651 b'%' => {
652 i += line_comment_span(bytes, i);
653 continue;
654 }
655 b'~' => {
656 if let Some(span) = sigil_span(bytes, i) {
657 i += span;
658 continue;
659 }
660 }
661 _ if b == opener => depth += 1,
662 _ if b == closer => {
663 depth -= 1;
664 if depth == 0 {
665 return Some(i);
666 }
667 }
668 _ => {}
669 }
670 i += 1;
671 }
672 None
673}
674
675#[derive(Debug, Clone, PartialEq, Eq)]
679pub enum ScannedArgs<'a> {
680 Terminated { args: Vec<&'a str>, consumed: usize },
681 Unterminated { args: Vec<&'a str> },
682}
683
684#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum ScanArity {
687 Exact(u8),
688 Unterminated,
689}
690
691pub fn scan_top_level_args(after_open_paren: &str) -> ScannedArgs<'_> {
695 let bytes = after_open_paren.as_bytes();
696 let mut args: Vec<&str> = Vec::new();
697 let mut depth = 1i32;
698 let mut blocks = BlockDepth::default();
699 let mut in_str = false;
700 let mut in_atom = false;
701 let mut start = 0usize;
702 let mut i = 0usize;
703 while i < bytes.len() {
704 let c = bytes[i];
705 if in_str || in_atom {
706 match c {
707 b'\\' => i += 1,
708 b'"' if in_str => in_str = false,
709 b'\'' if in_atom => in_atom = false,
710 _ => {}
711 }
712 i += 1;
713 continue;
714 }
715 if c.is_ascii_lowercase() && (i == 0 || !is_name_byte(bytes[i - 1])) {
716 i += blocks.observe(bytes, i);
717 continue;
718 }
719 match c {
720 b'"' => {
721 if let Some(span) = triple_quoted_span(bytes, i) {
722 i += span;
723 continue;
724 }
725 in_str = true;
726 }
727 b'\'' => in_atom = true,
728 b'$' => {
729 i += skip_char_literal_span(bytes, i);
730 continue;
731 }
732 b'%' => {
733 i += line_comment_span(bytes, i);
734 continue;
735 }
736 b'~' => {
737 if let Some(span) = sigil_span(bytes, i) {
738 i += span;
739 continue;
740 }
741 }
742 b'<' if bytes.get(i + 1) == Some(&b'<') => {
743 depth += 1;
744 i += 1;
745 }
746 b'>' if bytes.get(i + 1) == Some(&b'>') && depth > 1 => {
747 depth -= 1;
748 i += 1;
749 }
750 b'(' | b'[' | b'{' => depth += 1,
751 b']' | b'}' => depth -= 1,
752 b')' => {
753 depth -= 1;
754 if depth == 0 && !blocks.in_block() {
757 let slice = &after_open_paren[start..i];
758 if !slice.trim().is_empty() || !args.is_empty() {
759 args.push(slice);
760 }
761 return ScannedArgs::Terminated {
762 args,
763 consumed: i + 1,
764 };
765 }
766 }
767 b',' if depth == 1 && !blocks.in_block() => {
768 args.push(&after_open_paren[start..i]);
769 start = i + 1;
770 }
771 _ => {}
772 }
773 i += 1;
774 }
775 if start < bytes.len() {
776 let slice = &after_open_paren[start..];
777 if !slice.trim().is_empty() {
778 args.push(slice);
779 }
780 }
781 ScannedArgs::Unterminated { args }
782}
783
784pub fn scan_arity(after_open_paren: &str) -> ScanArity {
785 match scan_top_level_args(after_open_paren) {
786 ScannedArgs::Terminated { args, .. } => {
787 ScanArity::Exact(u8::try_from(args.len()).unwrap_or(u8::MAX))
788 }
789 ScannedArgs::Unterminated { .. } => ScanArity::Unterminated,
790 }
791}
792
793pub fn split_top_level_args(after_open_paren: &str) -> Vec<&str> {
797 match scan_top_level_args(after_open_paren) {
798 ScannedArgs::Terminated { args, .. } | ScannedArgs::Unterminated { args } => args,
799 }
800}
801
802pub fn count_top_level_items(s: &str, open: char, close: char) -> usize {
804 let body = match (s.find(open), s.rfind(close)) {
805 (Some(a), Some(b)) if b > a => &s[a + 1..b],
806 _ => return 0,
807 };
808 if body.trim().is_empty() {
809 return 0;
810 }
811 match scan_top_level_args(body) {
812 ScannedArgs::Terminated { args, .. } | ScannedArgs::Unterminated { args } => args.len(),
813 }
814}
815
816fn for_each_top_level_comma(s: &str, mut on_comma: impl FnMut(usize)) {
821 let bytes = s.as_bytes();
822 let mut depth_paren = 0i32;
823 let mut depth_brace = 0i32;
824 let mut depth_brack = 0i32;
825 let mut depth_angle = 0i32;
826 let mut blocks = BlockDepth::default();
827 let mut in_string = false;
828 let mut in_atom_quote = false;
829 let mut prev_back = false;
830 let mut i = 0usize;
831 while i < bytes.len() {
832 let ch = bytes[i] as char;
833 if prev_back {
834 prev_back = false;
835 i += 1;
836 continue;
837 }
838 if !in_string && !in_atom_quote && ch == '$' {
839 i += skip_char_literal_span(bytes, i);
840 continue;
841 }
842 if !in_string && !in_atom_quote && ch == '%' {
843 i += line_comment_span(bytes, i);
844 continue;
845 }
846 if !in_string
847 && !in_atom_quote
848 && ch == '"'
849 && let Some(span) = triple_quoted_span(bytes, i)
850 {
851 i += span;
852 continue;
853 }
854 if !in_string
855 && !in_atom_quote
856 && ch == '~'
857 && let Some(span) = sigil_span(bytes, i)
858 {
859 i += span;
860 continue;
861 }
862 if !in_string
863 && !in_atom_quote
864 && ch.is_ascii_lowercase()
865 && (i == 0 || !is_name_byte(bytes[i - 1]))
866 {
867 i += blocks.observe(bytes, i);
868 continue;
869 }
870 if !in_string && !in_atom_quote && ch == '<' && bytes.get(i + 1) == Some(&b'<') {
871 depth_angle += 1;
872 i += 2;
873 continue;
874 }
875 if !in_string && !in_atom_quote && ch == '>' && bytes.get(i + 1) == Some(&b'>') {
876 depth_angle -= 1;
877 i += 2;
878 continue;
879 }
880 match ch {
881 '\\' if in_string || in_atom_quote => prev_back = true,
882 '"' if !in_atom_quote => in_string = !in_string,
883 '\'' if !in_string => in_atom_quote = !in_atom_quote,
884 '(' if !in_string && !in_atom_quote => depth_paren += 1,
885 ')' if !in_string && !in_atom_quote => depth_paren -= 1,
886 '{' if !in_string && !in_atom_quote => depth_brace += 1,
887 '}' if !in_string && !in_atom_quote => depth_brace -= 1,
888 '[' if !in_string && !in_atom_quote => depth_brack += 1,
889 ']' if !in_string && !in_atom_quote => depth_brack -= 1,
890 ',' if !in_string
891 && !in_atom_quote
892 && depth_paren == 0
893 && depth_brace == 0
894 && depth_brack == 0
895 && depth_angle == 0
896 && !blocks.in_block() =>
897 {
898 on_comma(i);
899 }
900 _ => {}
901 }
902 i += 1;
903 }
904}
905
906pub fn split_top_level_commas(s: &str) -> Vec<&str> {
910 let mut out: Vec<&str> = Vec::new();
911 let mut start = 0usize;
912 for_each_top_level_comma(s, |i| {
913 out.push(s[start..i].trim());
914 start = i + 1;
915 });
916 if start < s.len() {
917 out.push(s[start..].trim());
918 }
919 out.into_iter().filter(|x| !x.is_empty()).collect()
920}
921
922pub fn count_top_level_commas(s: &str) -> usize {
925 let mut n = 0usize;
926 for_each_top_level_comma(s, |_| n += 1);
927 n
928}
929
930#[derive(Debug, Clone, PartialEq, Eq)]
935pub enum ScannedList<'a> {
936 Terminated {
937 elements: Vec<&'a str>,
938 consumed: usize,
939 },
940 ImproperTail,
941 Unterminated,
942}
943
944pub fn scan_list_elements(after_open_bracket: &str) -> ScannedList<'_> {
948 let bytes = after_open_bracket.as_bytes();
949 let mut elements: Vec<&str> = Vec::new();
950 let mut depth = 1i32;
951 let mut in_str = false;
952 let mut in_atom = false;
953 let mut start = 0usize;
954 let mut i = 0usize;
955 while i < bytes.len() {
956 let c = bytes[i];
957 if in_str || in_atom {
958 match c {
959 b'\\' => i += 1,
960 b'"' if in_str => in_str = false,
961 b'\'' if in_atom => in_atom = false,
962 _ => {}
963 }
964 i += 1;
965 continue;
966 }
967 match c {
968 b'"' => {
969 if let Some(span) = triple_quoted_span(bytes, i) {
970 i += span;
971 continue;
972 }
973 in_str = true;
974 }
975 b'\'' => in_atom = true,
976 b'$' => {
977 i += skip_char_literal_span(bytes, i);
978 continue;
979 }
980 b'%' => {
981 i += line_comment_span(bytes, i);
982 continue;
983 }
984 b'~' => {
985 if let Some(span) = sigil_span(bytes, i) {
986 i += span;
987 continue;
988 }
989 }
990 b'<' if bytes.get(i + 1) == Some(&b'<') => {
991 depth += 1;
992 i += 1;
993 }
994 b'>' if bytes.get(i + 1) == Some(&b'>') && depth > 1 => {
995 depth -= 1;
996 i += 1;
997 }
998 b'(' | b'[' | b'{' => depth += 1,
999 b')' | b'}' => depth -= 1,
1000 b']' => {
1001 depth -= 1;
1002 if depth == 0 {
1003 let slice = &after_open_bracket[start..i];
1004 if !slice.trim().is_empty() || !elements.is_empty() {
1005 elements.push(slice);
1006 }
1007 return ScannedList::Terminated {
1008 elements,
1009 consumed: i + 1,
1010 };
1011 }
1012 }
1013 b'|' if depth == 1 => return ScannedList::ImproperTail,
1015 b',' if depth == 1 => {
1016 elements.push(&after_open_bracket[start..i]);
1017 start = i + 1;
1018 }
1019 _ => {}
1020 }
1021 i += 1;
1022 }
1023 ScannedList::Unterminated
1024}
1025
1026#[derive(Debug, Clone, PartialEq, Eq)]
1029pub struct ParsedSignature {
1030 pub name: String,
1031 pub arity: u8,
1032 pub signature: String,
1033}
1034
1035pub fn split_leading_name(s: &str) -> (&str, &str) {
1044 let trimmed = s.trim_start();
1045 let name_end = trimmed
1046 .char_indices()
1047 .find(|(_, c)| !c.is_ascii_alphanumeric() && *c != '_' && *c != '@' && *c != '\'')
1048 .map(|(i, _)| i)
1049 .unwrap_or(trimmed.len());
1050 (&trimmed[..name_end], trimmed[name_end..].trim_start())
1051}
1052
1053pub fn split_name_and_args(body: &str) -> Option<(&str, &str, &str)> {
1054 let (name, after_name) = split_leading_name(body);
1055 if name.is_empty() || !after_name.starts_with('(') {
1056 return None;
1057 }
1058 let (args, rest) = take_balanced_parens(after_name)?;
1059 Some((name, args, rest.trim_start()))
1060}
1061
1062pub fn arity_of_args(args: &str) -> usize {
1064 count_top_level_commas(args) + if args.trim().is_empty() { 0 } else { 1 }
1065}
1066
1067pub fn parse_callable_signature(body: &str) -> Option<ParsedSignature> {
1071 let (name, args, rest) = split_name_and_args(body)?;
1072 let after_arrow = rest.strip_prefix("->")?.trim_start();
1073 let signature = format!("{}({}) -> {}", name, args.trim(), after_arrow.trim());
1074 Some(ParsedSignature {
1075 name: name.to_string(),
1076 arity: arity_of_args(args).min(255) as u8,
1077 signature,
1078 })
1079}
1080
1081pub fn take_balanced_parens(s: &str) -> Option<(&str, &str)> {
1084 if !s.starts_with('(') {
1085 return None;
1086 }
1087 let bytes = s.as_bytes();
1088 let mut depth = 0i32;
1089 let mut in_string = false;
1090 let mut in_atom_quote = false;
1091 let mut i = 0usize;
1092 while i < bytes.len() {
1093 let ch = bytes[i] as char;
1094 if !in_string && !in_atom_quote && ch == '$' {
1095 i += skip_char_literal_span(bytes, i);
1096 continue;
1097 }
1098 if !in_string && !in_atom_quote && ch == '%' {
1099 i += line_comment_span(bytes, i);
1100 continue;
1101 }
1102 if !in_string
1103 && !in_atom_quote
1104 && ch == '"'
1105 && let Some(span) = triple_quoted_span(bytes, i)
1106 {
1107 i += span;
1108 continue;
1109 }
1110 if !in_string
1111 && !in_atom_quote
1112 && ch == '~'
1113 && let Some(span) = sigil_span(bytes, i)
1114 {
1115 i += span;
1116 continue;
1117 }
1118 match ch {
1119 '\\' if in_string || in_atom_quote => {
1120 i += 2;
1121 continue;
1122 }
1123 '"' if !in_atom_quote => in_string = !in_string,
1124 '\'' if !in_string => in_atom_quote = !in_atom_quote,
1125 '(' if !in_string && !in_atom_quote => depth += 1,
1126 ')' if !in_string && !in_atom_quote => {
1127 depth -= 1;
1128 if depth == 0 {
1129 return Some((&s[1..i], &s[i + 1..]));
1130 }
1131 }
1132 _ => {}
1133 }
1134 i += 1;
1135 }
1136 None
1137}