1use logos::Logos;
18
19use crate::error::{Applicability, CompileError};
20use crate::span::{FileId, Span};
21
22pub const MAX_SAFE_INT: i64 = 9_007_199_254_740_991;
25
26pub(crate) fn strip_digit_separators(lexeme: &str) -> std::borrow::Cow<'_, str> {
32 if lexeme.as_bytes().contains(&b'_') {
33 std::borrow::Cow::Owned(lexeme.replace('_', ""))
34 } else {
35 std::borrow::Cow::Borrowed(lexeme)
36 }
37}
38
39#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)]
46#[logos(skip r"[ \t\r\n]+")]
47pub enum TokenKind {
48 #[token("commons")]
50 Commons,
51 #[token("type")]
52 Type,
53 #[token("event")]
59 Event,
60 #[token("asset")]
65 Asset,
66 #[token("embed")]
70 Embed,
71 #[token("const")]
76 Const,
77 #[token("fn")]
78 Fn,
79 #[token("where")]
80 Where,
81 #[token("true")]
84 True,
85 #[token("false")]
86 False,
87 #[token("Int")]
88 Int,
89 #[token("String")]
90 String,
91 #[token("Bool")]
92 Bool,
93 #[token("Float")]
95 Float,
96 #[token("Duration")]
98 Duration,
99 #[token("Instant")]
101 Instant,
102 #[token("Bytes")]
104 Bytes,
105 #[token("Decimal")]
107 Decimal,
108 #[token("let")]
110 Let,
111 #[token("if")]
112 If,
113 #[token("else")]
114 Else,
115 #[token("Ok")]
116 Ok,
117 #[token("Err")]
118 Err,
119 #[token("Result")]
120 Result,
121 #[token("ValidationError")]
122 ValidationError,
123 #[token("JsonError")]
125 JsonError,
126 #[token("enum")]
128 Enum,
129 #[token("match")]
130 Match,
131 #[token("Option")]
132 Option,
133 #[token("record")]
134 Record,
135 #[token("self")]
136 Self_,
137 #[token("Some")]
138 Some,
139 #[token("None")]
140 None,
141 #[token("is")]
142 Is,
143 #[token("opaque")]
145 Opaque,
146 #[token("uses")]
147 Uses,
148 #[token("context")]
150 Context,
151 #[token("consumes")]
152 Consumes,
153 #[token("exports")]
154 Exports,
155 #[token("transparent")]
156 Transparent,
157 #[token("as")]
159 As,
160 #[token("expect")]
165 Expect,
166 #[token("suite")]
167 Suite,
168 #[token("case")]
169 Case,
170 #[token("stub")]
174 Stub,
175 #[token("property")]
180 Property,
181 #[token("adapter")]
183 Adapter,
184 #[token("binding")]
185 Binding,
186 #[token("agent")]
188 Agent,
189 #[token("capability")]
190 Capability,
191 #[token("Effect")]
192 Effect,
193 #[token("do")]
196 Do,
197 #[token("given")]
198 Given,
199 #[token("on")]
200 On,
201 #[token("http")]
203 Http,
204 #[token("cron")]
206 Cron,
207 #[token("queue")]
209 Queue,
210 #[token("from")]
213 From,
214 #[token("protocol")]
215 Protocol,
216 #[token("provides")]
217 Provides,
218 #[token("service")]
219 Service,
220 #[token("actor")]
223 Actor,
224 #[token("by")]
225 By,
226 #[token("invariant")]
229 Invariant,
230 #[token("implies")]
231 Implies,
232 #[token("requires")]
240 Requires,
241 #[token("ensures")]
242 Ensures,
243 #[token("transition")]
250 Transition,
251 #[token("messages")]
254 Messages,
255 #[token("...")]
257 DotDotDot,
258 #[token("..")]
267 DotDot,
268 #[token("<-")]
270 LArrow,
271 #[token("~>")]
275 TildeArrow,
276 #[token(":=")]
280 ColonEq,
281
282 DocBlock,
287
288 DocLine,
299
300 Comment,
308
309 #[regex(r"[A-Za-z][A-Za-z0-9_]*")]
311 Ident,
312
313 #[regex(r"[0-9]+(_[0-9]+)*")]
318 IntLit,
319 #[regex(
325 r"[0-9]+(_[0-9]+)*\.[0-9]+(_[0-9]+)*([eE][+-]?[0-9]+(_[0-9]+)*)?|[0-9]+(_[0-9]+)*[eE][+-]?[0-9]+(_[0-9]+)*"
326 )]
327 FloatLit,
328 #[regex(r"[0-9]+(_[0-9]+)*(\.[0-9]+(_[0-9]+)*)?d")]
332 DecimalLit,
333 StrLit,
339 InterpStr,
344
345 #[token("->")]
347 Arrow,
348 #[token("==")]
349 EqEq,
350 #[token("!=")]
351 BangEq,
352 #[token("<=")]
353 LtEq,
354 #[token(">=")]
355 GtEq,
356 #[token("&&")]
357 AmpAmp,
358 #[token("||")]
359 PipePipe,
360
361 #[token("+")]
363 Plus,
364 #[token("-")]
365 Minus,
366 #[token("*")]
367 Star,
368 #[token("/")]
369 Slash,
370 #[token("%")]
373 Percent,
374 #[token("!")]
375 Bang,
376 #[token("=")]
377 Eq,
378 #[token("<")]
379 Lt,
380 #[token(">")]
381 Gt,
382 #[token("?")]
384 Question,
385 #[token("=>")]
387 FatArrow,
388 #[token("_")]
391 Underscore,
392 #[token("|")]
395 Pipe,
396 #[token("@")]
401 At,
402
403 #[token("(")]
405 LParen,
406 #[token(")")]
407 RParen,
408 #[token("{")]
409 LBrace,
410 #[token("}")]
411 RBrace,
412 #[token("[")]
413 LBracket,
414 #[token("]")]
415 RBracket,
416 #[token(",")]
417 Comma,
418 #[token(":")]
419 Colon,
420 #[token(".")]
421 Dot,
422}
423
424impl TokenKind {
425 pub fn describe(self) -> &'static str {
427 use TokenKind::*;
428 match self {
429 Commons => "`commons`",
430 Type => "`type`",
431 Event => "`event`",
432 Asset => "`asset`",
433 Embed => "`embed`",
434 Const => "`const`",
435 Fn => "`fn`",
436 Where => "`where`",
437 True => "`true`",
438 False => "`false`",
439 Int => "`Int`",
440 String => "`String`",
441 Bool => "`Bool`",
442 Float => "`Float`",
443 Duration => "`Duration`",
444 Instant => "`Instant`",
445 Bytes => "`Bytes`",
446 Decimal => "`Decimal`",
447 Let => "`let`",
448 If => "`if`",
449 Else => "`else`",
450 Ok => "`Ok`",
451 Err => "`Err`",
452 Result => "`Result`",
453 ValidationError => "`ValidationError`",
454 JsonError => "`JsonError`",
455 Enum => "`enum`",
456 Match => "`match`",
457 Option => "`Option`",
458 Record => "`record`",
459 Self_ => "`self`",
460 Some => "`Some`",
461 None => "`None`",
462 Is => "`is`",
463 Opaque => "`opaque`",
464 Uses => "`uses`",
465 Context => "`context`",
466 Consumes => "`consumes`",
467 Exports => "`exports`",
468 Transparent => "`transparent`",
469 As => "`as`",
470 Expect => "`expect`",
471 Suite => "`suite`",
472 Case => "`case`",
473 Property => "`property`",
474 Adapter => "`adapter`",
475 Binding => "`binding`",
476 Agent => "`agent`",
477 Capability => "`capability`",
478 Effect => "`Effect`",
479 Do => "`do`",
480 Given => "`given`",
481 On => "`on`",
482 Http => "`http`",
483 Cron => "`cron`",
484 Queue => "`queue`",
485 From => "`from`",
486 Protocol => "`protocol`",
487 Provides => "`provides`",
488 Stub => "`stub`",
489 Service => "`service`",
490 Actor => "`actor`",
491 By => "`by`",
492 Invariant => "`invariant`",
493 Implies => "`implies`",
494 Requires => "`requires`",
495 Ensures => "`ensures`",
496 Transition => "`transition`",
497 Messages => "`messages`",
498 ColonEq => "`:=`",
499 DotDotDot => "`...`",
500 DotDot => "`..`",
501 LArrow => "`<-`",
502 TildeArrow => "`~>`",
503 DocBlock => "documentation block",
504 DocLine => "one-line documentation (`--- text`)",
505 Comment => "line comment",
506 Ident => "identifier",
507 IntLit => "integer literal",
508 FloatLit => "float literal",
509 DecimalLit => "decimal literal",
510 StrLit => "string literal",
511 InterpStr => "interpolated string",
512 Arrow => "`->`",
513 EqEq => "`==`",
514 BangEq => "`!=`",
515 LtEq => "`<=`",
516 GtEq => "`>=`",
517 AmpAmp => "`&&`",
518 PipePipe => "`||`",
519 Plus => "`+`",
520 Minus => "`-`",
521 Star => "`*`",
522 Slash => "`/`",
523 Percent => "`%`",
524 Bang => "`!`",
525 Eq => "`=`",
526 Lt => "`<`",
527 Gt => "`>`",
528 Question => "`?`",
529 FatArrow => "`=>`",
530 Underscore => "`_`",
531 Pipe => "`|`",
532 At => "`@`",
533 LParen => "`(`",
534 RParen => "`)`",
535 LBrace => "`{`",
536 RBrace => "`}`",
537 LBracket => "`[`",
538 RBracket => "`]`",
539 Comma => "`,`",
540 Colon => "`:`",
541 Dot => "`.`",
542 }
543 }
544}
545
546#[derive(Debug, Clone, Copy)]
548pub struct Token {
549 pub kind: TokenKind,
550 pub span: Span,
551}
552
553pub fn tokenize(source: &str) -> Result<Vec<Token>, CompileError> {
557 tokenize_in(source, FileId::UNKNOWN)
558}
559
560pub fn tokenize_in(source: &str, file: FileId) -> Result<Vec<Token>, CompileError> {
569 tokenize_mode(source, file, false)
570}
571
572pub fn tokenize_legacy(source: &str) -> Result<Vec<Token>, CompileError> {
581 tokenize_mode(source, FileId::UNKNOWN, true)
582}
583
584fn tokenize_mode(source: &str, file: FileId, legacy: bool) -> Result<Vec<Token>, CompileError> {
585 let (tokens, errors) = tokenize_mode_recovering(source, file, legacy);
586 match errors.into_iter().next() {
587 Some(first) => Err(first),
588 None => Ok(tokens),
589 }
590}
591
592pub fn tokenize_recovering_in(source: &str, file: FileId) -> (Vec<Token>, Vec<CompileError>) {
606 tokenize_mode_recovering(source, file, false)
607}
608
609pub fn tokenize_recovering(source: &str) -> (Vec<Token>, Vec<CompileError>) {
611 tokenize_recovering_in(source, FileId::UNKNOWN)
612}
613
614fn skip_line(bytes: &[u8], from: usize) -> usize {
617 match bytes[from.min(bytes.len())..]
618 .iter()
619 .position(|&b| b == b'\n')
620 {
621 Some(i) => from + i + 1,
622 None => bytes.len(),
623 }
624}
625
626fn string_error_resume(bytes: &[u8], pos: usize, err: &CompileError) -> usize {
634 let fallback = skip_line(bytes, err.span.end.max(pos + 1));
635 if err.category == "bynk.lex.unterminated_string" {
636 return fallback;
637 }
638 let Some(open) = string_open(bytes, pos) else {
639 return fallback;
640 };
641 let mut p = open.body;
642 while p < bytes.len() {
643 match bytes[p] {
644 b'\\' if !open.raw && bytes.get(p + 1) == Some(&b'(') => {
645 match hole_end(bytes, p + 2) {
648 Some(end) => p = end,
649 None => return fallback,
650 }
651 }
652 b'\\' if !open.raw => p += 2,
653 b'\n' if !open.multi => return fallback,
654 b'"' if open.multi => {
655 if bytes[p..].starts_with(b"\"\"\"") {
656 return p + 3;
657 }
658 p += 1;
659 }
660 b'"' => {
661 let hashes = &bytes[p + 1..];
662 if hashes.len() >= open.hashes && hashes[..open.hashes].iter().all(|&b| b == b'#') {
663 return p + 1 + open.hashes;
664 }
665 p += 1;
666 }
667 _ => p += 1,
668 }
669 }
670 fallback
671}
672
673fn hole_end(bytes: &[u8], from: usize) -> Option<usize> {
677 let (mut p, mut depth) = (from, 1usize);
678 while p < bytes.len() {
679 match bytes[p] {
680 b'\n' => return None,
681 b'(' => depth += 1,
682 b')' => {
683 depth -= 1;
684 if depth == 0 {
685 return Some(p + 1);
686 }
687 }
688 b'"' => {
689 p += 1;
690 while p < bytes.len() && bytes[p] != b'"' {
691 match bytes[p] {
692 b'\n' => return None,
693 b'\\' => p += 1,
694 _ => {}
695 }
696 p += 1;
697 }
698 }
699 _ => {}
700 }
701 p += 1;
702 }
703 None
704}
705
706fn push_lex_error(errors: &mut Vec<CompileError>, err: CompileError) {
709 const UNEXPECTED: &str = "bynk.lex.unexpected_character";
710 if err.category == UNEXPECTED
711 && let Some(prev) = errors.last_mut()
712 && prev.category == UNEXPECTED
713 && prev.span.end == err.span.start
714 {
715 prev.span.end = err.span.end;
716 prev.message = "unexpected characters".to_string();
717 return;
718 }
719 errors.push(err);
720}
721
722fn tokenize_mode_recovering(
723 source: &str,
724 file: FileId,
725 legacy: bool,
726) -> (Vec<Token>, Vec<CompileError>) {
727 let mut tokens = Vec::new();
728 let mut errors: Vec<CompileError> = Vec::new();
729 let bytes = source.as_bytes();
730 let mut pos = 0;
731 while pos < bytes.len() {
732 if let Some((open_end, dashes)) = doc_block_open_at(source, pos) {
736 match doc_block_close(source, open_end, dashes) {
739 Some((close_start, close_end)) => {
740 let span = Span::new_in(file, pos, close_end);
741 tokens.push(Token {
742 kind: TokenKind::DocBlock,
743 span,
744 });
745 let _ = close_start;
746 pos = close_end;
747 continue;
748 }
749 None => {
750 errors.push(CompileError::new(
751 "bynk.lex.unclosed_doc_block",
752 Span::new_in(file, pos, open_end),
753 "documentation block opened but never closed",
754 )
755 .with_note(format!(
756 "a doc block is closed by a line of exactly {dashes} dashes on its own; \
757 a line of three or more dashes is a doc-block marker, not a divider, so \
758 for a divider use a line comment, such as `// ---------`"
759 )));
760 pos = bytes.len();
763 continue;
764 }
765 }
766 }
767 let line_first = only_indent_before(bytes, pos);
768 if line_first && doc_line_marker(bytes, pos) {
772 if legacy {
773 pos = push_comment(&mut tokens, bytes, pos, file);
774 } else {
775 pos = push_doc_line(&mut tokens, bytes, pos, file);
776 }
777 continue;
778 }
779 if line_first && bytes[pos..].starts_with(b"///") && bytes.get(pos + 3) != Some(&b'/') {
784 errors.push(slash_doc_error(bytes, pos, file));
785 pos = skip_line(bytes, pos);
786 continue;
787 }
788 if line_first && bytes[pos..].starts_with(b"--|") {
791 pos = push_doc_line(&mut tokens, bytes, pos, file);
792 continue;
793 }
794 if bytes[pos..].starts_with(b"//") {
801 pos = push_comment(&mut tokens, bytes, pos, file);
802 continue;
803 }
804 let after_space = pos == 0 || matches!(bytes[pos - 1], b' ' | b'\t' | b'\r' | b'\n');
812 if after_space
813 && bytes[pos..].starts_with(b"--")
814 && (legacy
815 || matches!(
816 bytes.get(pos + 2),
817 None | Some(b' ' | b'\t' | b'\r' | b'\n' | b'|')
818 ))
819 {
820 pos = push_comment(&mut tokens, bytes, pos, file);
821 continue;
822 }
823 if matches!(bytes[pos], b' ' | b'\t' | b'\r' | b'\n') {
826 pos += 1;
827 continue;
828 }
829 if string_open(bytes, pos).is_some() {
834 let (end, segments) = match lex_string(source, pos, 0, file) {
835 Ok(lexed) => lexed,
836 Err(e) => {
837 pos = string_error_resume(bytes, pos, &e);
840 errors.push(e);
841 continue;
842 }
843 };
844 let kind = if segments.iter().any(|s| matches!(s, InterpSegment::Hole(_))) {
845 TokenKind::InterpStr
846 } else {
847 TokenKind::StrLit
848 };
849 tokens.push(Token {
850 kind,
851 span: Span::new_in(file, pos, end),
852 });
853 pos = end;
854 continue;
855 }
856 let mut lex = TokenKind::lexer(&source[pos..]);
858 let Some(result) = lex.next() else {
859 let ch = source[pos..].chars().next().unwrap_or('\0');
862 let span = Span::new_in(file, pos, pos + ch.len_utf8());
863 push_lex_error(
864 &mut errors,
865 CompileError::new(
866 "bynk.lex.unexpected_character",
867 span,
868 format!("unexpected character `{ch}`"),
869 ),
870 );
871 pos += ch.len_utf8();
872 continue;
873 };
874 let local = lex.span();
875 let span: Span = Span::new_in(file, pos + local.start, pos + local.end);
876 match result {
877 Ok(kind) => {
878 if kind == TokenKind::IntLit {
885 let slice = &source[span.range()];
886 if !strip_digit_separators(slice)
887 .parse::<i64>()
888 .is_ok_and(|n| n <= MAX_SAFE_INT)
889 {
890 errors.push(
891 CompileError::new(
892 "bynk.lex.integer_overflow",
893 span,
894 format!("integer literal `{slice}` is out of range for an `Int`"),
895 )
896 .with_note(
897 "an `Int` is a safe integer, -(2^53 - 1) to 2^53 - 1 \
898 (±9007199254740991); use a `Float` or a `String` for a larger value",
899 ),
900 );
901 pos = span.end;
902 continue;
903 }
904 }
905 if kind == TokenKind::DecimalLit {
906 let slice = &source[span.range()];
909 let body = strip_digit_separators(&slice[..slice.len() - 1]).into_owned();
910 let frac = body.split_once('.').map_or(0, |(_, f)| f.len());
911 if frac > 100 || body.len() > 1000 {
912 errors.push(
913 CompileError::new(
914 "bynk.lex.decimal_literal_too_large",
915 span,
916 "decimal literal is too large",
917 )
918 .with_note(
919 "a `Decimal` has at most 100 fraction digits and 1000 characters",
920 ),
921 );
922 pos = span.end;
923 continue;
924 }
925 }
926 if kind == TokenKind::FloatLit {
927 let slice = &source[span.range()];
928 match strip_digit_separators(slice).parse::<f64>() {
929 Ok(v) if v.is_finite() => {}
930 _ => {
931 errors.push(
932 CompileError::new(
933 "bynk.lex.float_literal_overflow",
934 span,
935 format!(
936 "float literal `{slice}` is out of range for a 64-bit float"
937 ),
938 )
939 .with_note(
940 "the literal does not fit a finite IEEE 754 double; \
941 the largest finite value is ~1.8e308",
942 ),
943 );
944 pos = span.end;
945 continue;
946 }
947 }
948 }
949 tokens.push(Token { kind, span });
950 pos = span.end;
951 }
952 Err(()) => {
953 let slice = &source[span.range()];
954 let ch = slice.chars().next().unwrap_or('\0');
955 let err = if ch == '"' {
956 CompileError::new(
957 "bynk.lex.unterminated_string",
958 span,
959 "unterminated string literal",
960 )
961 .with_note(
962 "string literals must close with `\"` on the same line; \
963 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`",
964 )
965 } else {
966 CompileError::new(
967 "bynk.lex.unexpected_character",
968 span,
969 format!("unexpected character `{ch}`"),
970 )
971 };
972 push_lex_error(&mut errors, err);
973 pos = if ch == '"' {
974 skip_line(bytes, span.end.max(pos + 1))
975 } else {
976 span.end.max(pos + ch.len_utf8())
977 };
978 }
979 }
980 }
981 (tokens, errors)
982}
983
984pub fn without_lex_echoes(
993 lex_errors: &[CompileError],
994 tokens: &[Token],
995 parse_errors: Vec<CompileError>,
996) -> Vec<CompileError> {
997 let echo_ranges: Vec<(usize, usize)> = lex_errors
998 .iter()
999 .map(|le| {
1000 let first = tokens.partition_point(|t| t.span.start < le.span.end);
1004 let next_end = tokens[first..]
1005 .iter()
1006 .find(|t| !matches!(t.kind, TokenKind::Comment | TokenKind::DocLine))
1007 .map_or(usize::MAX, |t| t.span.end);
1008 (le.span.start, next_end)
1009 })
1010 .collect();
1011 parse_errors
1012 .into_iter()
1013 .filter(|e| {
1014 !echo_ranges
1015 .iter()
1016 .any(|&(lo, hi)| e.span.start >= lo && e.span.start <= hi)
1017 })
1018 .collect()
1019}
1020
1021pub fn tokenize_expanding_holes(source: &str) -> Result<Vec<Token>, CompileError> {
1038 tokenize_expanding_holes_in(source, FileId::UNKNOWN)
1039}
1040
1041pub fn tokenize_expanding_holes_in(source: &str, file: FileId) -> Result<Vec<Token>, CompileError> {
1044 let mut out = Vec::new();
1045 for tok in tokenize_in(source, file)? {
1046 expand_hole_token(source, file, tok, &mut out);
1047 }
1048 Ok(out)
1049}
1050
1051fn expand_hole_token(source: &str, file: FileId, tok: Token, out: &mut Vec<Token>) {
1054 if tok.kind != TokenKind::InterpStr {
1055 out.push(tok);
1056 return;
1057 }
1058 let Ok(segments) = split_interp(source, tok.span) else {
1059 out.push(tok); return;
1061 };
1062 for segment in segments {
1063 let InterpSegment::Hole(hole) = segment else {
1064 continue; };
1066 let Ok(hole_tokens) = tokenize_in(&source[hole.range()], file) else {
1067 continue;
1068 };
1069 for mut t in hole_tokens {
1070 t.span = Span::new_in(file, t.span.start + hole.start, t.span.end + hole.start);
1072 expand_hole_token(source, file, t, out); }
1074 }
1075}
1076
1077#[derive(Clone, Copy)]
1081struct StringOpen {
1082 raw: bool,
1084 multi: bool,
1086 hashes: usize,
1088 body: usize,
1090}
1091
1092fn is_ident_byte(b: u8) -> bool {
1093 b.is_ascii_alphanumeric() || b == b'_'
1094}
1095
1096fn string_open(bytes: &[u8], start: usize) -> Option<StringOpen> {
1100 let raw = bytes.get(start) == Some(&b'r');
1101 let mut p = start + usize::from(raw);
1102 let mut hashes = 0;
1103 if raw {
1104 while bytes.get(p) == Some(&b'#') {
1105 hashes += 1;
1106 p += 1;
1107 }
1108 }
1109 if bytes.get(p) != Some(&b'"') {
1110 return None;
1111 }
1112 let multi = hashes == 0 && bytes[p..].starts_with(b"\"\"\"");
1113 Some(StringOpen {
1114 raw,
1115 multi,
1116 hashes,
1117 body: if multi { p + 3 } else { p + 1 },
1118 })
1119}
1120
1121fn unterminated_string(file: FileId, start: usize, end: usize, multi: bool) -> CompileError {
1122 let note = if multi {
1123 "a multi-line string closes with `\"\"\"` on a line of its own"
1124 } else {
1125 "string literals must close with `\"` on the same line (use `\"\"\"` for a multi-line string); \
1126 supported escapes: \\n \\r \\t \\0 \\\" \\\\ \\u{…} and `\\(…)` interpolation"
1127 };
1128 CompileError::new(
1129 "bynk.lex.unterminated_string",
1130 Span::new_in(file, start, end),
1131 "unterminated string literal",
1132 )
1133 .with_note(note)
1134}
1135
1136fn boundary_span(source: &str, file: FileId, from: usize, to: usize) -> Span {
1139 let mut end = to.min(source.len());
1140 while end < source.len() && !source.is_char_boundary(end) {
1141 end += 1;
1142 }
1143 Span::new_in(file, from, end)
1144}
1145
1146const ESCAPE_NOTE: &str = "supported escapes: \\n \\r \\t \\0 \\\" \\\\ \\u{…} \\(…)";
1147
1148fn lex_string(
1152 source: &str,
1153 start: usize,
1154 depth: usize,
1155 file: FileId,
1156) -> Result<(usize, Vec<InterpSegment>), CompileError> {
1157 let bytes = source.as_bytes();
1158 let open = string_open(bytes, start).expect("caller checked string_open");
1159 if depth > crate::MAX_NESTING_DEPTH {
1160 return Err(too_deeply_nested_interpolation(Span::new_in(
1162 file,
1163 start,
1164 start + 1,
1165 )));
1166 }
1167 let mut segments = Vec::new();
1168 let mut chunk = String::new();
1169 if open.multi {
1170 let end = lex_multiline(source, start, open, depth, file, &mut segments, &mut chunk)?;
1171 if !chunk.is_empty() {
1172 segments.push(InterpSegment::Chunk(chunk));
1173 }
1174 return Ok((end, segments));
1175 }
1176 if open.raw {
1177 let mut i = open.body;
1179 loop {
1180 if i >= bytes.len() || bytes[i] == b'\n' {
1181 return Err(unterminated_string(file, start, i.min(bytes.len()), false));
1182 }
1183 if bytes[i] == b'"' && (1..=open.hashes).all(|k| bytes.get(i + k) == Some(&b'#')) {
1184 break;
1185 }
1186 i += 1;
1187 }
1188 chunk.push_str(&source[open.body..i]);
1189 if !chunk.is_empty() {
1190 segments.push(InterpSegment::Chunk(chunk));
1191 }
1192 return Ok((i + 1 + open.hashes, segments));
1193 }
1194 let end = decode_run(
1195 source,
1196 open.body,
1197 Stop::Quote(start),
1198 depth,
1199 file,
1200 &mut segments,
1201 &mut chunk,
1202 )?;
1203 if !chunk.is_empty() {
1204 segments.push(InterpSegment::Chunk(chunk));
1205 }
1206 Ok((end + 1, segments))
1207}
1208
1209#[derive(Clone, Copy)]
1211enum Stop {
1212 Quote(usize),
1216 End(usize),
1219}
1220
1221fn decode_run(
1224 source: &str,
1225 from: usize,
1226 stop: Stop,
1227 depth: usize,
1228 file: FileId,
1229 segments: &mut Vec<InterpSegment>,
1230 chunk: &mut String,
1231) -> Result<usize, CompileError> {
1232 let bytes = source.as_bytes();
1233 let limit = match stop {
1234 Stop::Quote(_) => bytes.len(),
1235 Stop::End(e) => e,
1236 };
1237 let mut i = from;
1238 loop {
1239 if i >= limit {
1240 match stop {
1241 Stop::End(_) => return Ok(i),
1242 Stop::Quote(start) => {
1243 return Err(unterminated_string(file, start, i.min(bytes.len()), false));
1244 }
1245 }
1246 }
1247 match bytes[i] {
1248 b'\n' if matches!(stop, Stop::Quote(_)) => {
1249 let Stop::Quote(start) = stop else {
1250 unreachable!()
1251 };
1252 return Err(unterminated_string(file, start, i, false));
1253 }
1254 b'"' => match stop {
1255 Stop::Quote(_) => return Ok(i),
1256 Stop::End(e) => {
1257 if bytes[i..e].starts_with(b"\"\"\"") {
1258 return Err(CompileError::new(
1259 "bynk.lex.multiline_close_not_own_line",
1260 Span::new_in(file, i, i + 3),
1261 "a `\"\"\"` inside a multi-line string closes it, but it is not on a line of its own",
1262 )
1263 .with_note(
1264 "put the closing `\"\"\"` on its own line, or escape a quote as `\\\"`",
1265 ));
1266 }
1267 chunk.push('"');
1268 i += 1;
1269 }
1270 },
1271 b'\\' => {
1272 let next = if i + 1 < limit {
1273 Some(bytes[i + 1])
1274 } else {
1275 None
1276 };
1277 match next {
1278 Some(b'n') => {
1279 chunk.push('\n');
1280 i += 2;
1281 }
1282 Some(b't') => {
1283 chunk.push('\t');
1284 i += 2;
1285 }
1286 Some(b'r') => {
1287 chunk.push('\r');
1288 i += 2;
1289 }
1290 Some(b'0') => {
1291 chunk.push('\0');
1292 i += 2;
1293 }
1294 Some(b'"') => {
1295 chunk.push('"');
1296 i += 2;
1297 }
1298 Some(b'\\') => {
1299 chunk.push('\\');
1300 i += 2;
1301 }
1302 Some(b'u') => {
1303 let (ch, after) = decode_unicode_escape(source, i, limit, file)?;
1304 chunk.push(ch);
1305 i = after;
1306 }
1307 Some(b'(') => {
1308 if !chunk.is_empty() {
1309 segments.push(InterpSegment::Chunk(std::mem::take(chunk)));
1310 }
1311 let hole_start = i + 2;
1312 let after = scan_hole(bytes, source, hole_start, depth + 1, file)?;
1313 segments.push(InterpSegment::Hole(Span::new_in(
1316 file,
1317 hole_start,
1318 after - 1,
1319 )));
1320 i = after;
1321 }
1322 other => {
1323 let shown = other.map(|b| (b as char).to_string()).unwrap_or_default();
1324 return Err(CompileError::new(
1327 "bynk.lex.bad_escape",
1328 boundary_span(source, file, i, i + 2),
1329 format!("invalid escape sequence `\\{shown}` in string literal"),
1330 )
1331 .with_note(ESCAPE_NOTE));
1332 }
1333 }
1334 }
1335 _ => {
1338 let ch = source[i..].chars().next().unwrap();
1339 chunk.push(ch);
1340 i += ch.len_utf8();
1341 }
1342 }
1343 }
1344}
1345
1346fn decode_unicode_escape(
1351 source: &str,
1352 i: usize,
1353 limit: usize,
1354 file: FileId,
1355) -> Result<(char, usize), CompileError> {
1356 let bytes = source.as_bytes();
1357 let malformed = |end: usize| {
1358 CompileError::new(
1359 "bynk.lex.bad_escape",
1360 Span::new_in(file, i, end),
1361 "invalid `\\u{…}` escape in string literal",
1362 )
1363 .with_note(
1364 "a Unicode escape is `\\u{` followed by 1 to 6 hex digits and `}`, e.g. `\\u{1F600}`",
1365 )
1366 };
1367 let mut p = i + 2; if p >= limit || bytes[p] != b'{' {
1369 return Err(malformed(p));
1370 }
1371 p += 1;
1372 let digits_start = p;
1373 while p < limit && bytes[p].is_ascii_hexdigit() {
1374 p += 1;
1375 }
1376 let digits = p - digits_start;
1377 if digits == 0 || digits > 6 || p >= limit || bytes[p] != b'}' {
1378 return Err(malformed(p.min(limit)));
1379 }
1380 let value = u32::from_str_radix(&source[digits_start..p], 16).expect("hex digits");
1381 p += 1; match char::from_u32(value) {
1383 Some(ch) => Ok((ch, p)),
1384 None => Err(CompileError::new(
1385 "bynk.lex.bad_unicode_escape",
1386 Span::new_in(file, i, p),
1387 format!("`\\u{{{value:X}}}` is not a Unicode scalar value"),
1388 )
1389 .with_note(
1390 "surrogate code points (D800 to DFFF) and values above 10FFFF cannot appear in a string",
1391 )),
1392 }
1393}
1394
1395fn lex_multiline(
1401 source: &str,
1402 start: usize,
1403 open: StringOpen,
1404 depth: usize,
1405 file: FileId,
1406 segments: &mut Vec<InterpSegment>,
1407 chunk: &mut String,
1408) -> Result<usize, CompileError> {
1409 let bytes = source.as_bytes();
1410 let mut pos = match (bytes.get(open.body), bytes.get(open.body + 1)) {
1412 (Some(b'\n'), _) => open.body + 1,
1413 (Some(b'\r'), Some(b'\n')) => open.body + 2,
1414 _ => {
1415 return Err(CompileError::new(
1416 "bynk.lex.multiline_open",
1417 Span::new_in(file, start, open.body),
1418 "the opening `\"\"\"` of a multi-line string must be followed by a newline",
1419 )
1420 .with_note("the text starts on the next line; write `\"\"\"` then press enter"));
1421 }
1422 };
1423 let mut lines: Vec<(usize, usize)> = Vec::new(); let (indent_len, end) = loop {
1426 if pos >= bytes.len() {
1427 return Err(unterminated_string(file, start, bytes.len(), true));
1428 }
1429 let line_end = bytes[pos..]
1430 .iter()
1431 .position(|&b| b == b'\n')
1432 .map_or(bytes.len(), |n| pos + n);
1433 let mut ws = pos;
1434 while ws < line_end && matches!(bytes[ws], b' ' | b'\t') {
1435 ws += 1;
1436 }
1437 if bytes[ws..line_end].starts_with(b"\"\"\"")
1440 && !bytes
1441 .get(ws + 3)
1442 .is_some_and(|&b| is_ident_byte(b) || b == b'"')
1443 {
1444 break (ws - pos, ws + 3);
1445 }
1446 let trimmed_end = if line_end > pos && bytes[line_end - 1] == b'\r' {
1447 line_end - 1
1448 } else {
1449 line_end
1450 };
1451 lines.push((pos, trimmed_end));
1452 if line_end >= bytes.len() {
1453 return Err(unterminated_string(file, start, bytes.len(), true));
1454 }
1455 pos = line_end + 1;
1456 };
1457 let closing_line_start = end - 3 - indent_len;
1458 let indent = &bytes[closing_line_start..closing_line_start + indent_len];
1459 for (n, &(ls, le)) in lines.iter().enumerate() {
1461 if n > 0 {
1462 chunk.push('\n');
1463 }
1464 let line = &bytes[ls..le];
1465 let blank = line.iter().all(|b| matches!(b, b' ' | b'\t'));
1466 let content = if line.starts_with(indent) {
1467 ls + indent_len
1468 } else if blank {
1469 le } else {
1471 return Err(CompileError::new(
1472 "bynk.lex.multiline_indent",
1473 boundary_span(source, file, ls, le),
1474 "a line of this multi-line string is indented less than its closing `\"\"\"`",
1475 )
1476 .with_note(if line.len() >= indent.len() {
1477 "the indentation must match the closing delimiter's character for character: \
1478 a tab does not match a space, so mixed tabs and spaces fail here"
1479 } else {
1480 "every non-blank line must start with the closing delimiter's indentation, \
1481 which is stripped from the value"
1482 }));
1483 };
1484 if open.raw {
1485 let text = &source[content..le];
1486 if let Some(at) = text.find("\"\"\"") {
1487 return Err(CompileError::new(
1488 "bynk.lex.multiline_close_not_own_line",
1489 Span::new_in(file, content + at, content + at + 3),
1490 "a `\"\"\"` inside a multi-line string closes it, but it is not on a line of its own",
1491 )
1492 .with_note("put the closing `\"\"\"` on its own line"));
1493 }
1494 chunk.push_str(text);
1495 } else {
1496 decode_run(source, content, Stop::End(le), depth, file, segments, chunk)?;
1497 }
1498 }
1499 Ok(end)
1500}
1501
1502fn scan_hole(
1507 bytes: &[u8],
1508 source: &str,
1509 start: usize,
1510 nesting: usize,
1511 file: FileId,
1512) -> Result<usize, CompileError> {
1513 if nesting > crate::MAX_NESTING_DEPTH {
1514 return Err(too_deeply_nested_interpolation(Span::new_in(
1518 file,
1519 start.saturating_sub(2),
1520 start,
1521 )));
1522 }
1523 let mut i = start;
1524 let mut depth = 1usize;
1525 loop {
1526 if i >= bytes.len() || bytes[i] == b'\n' {
1527 return Err(CompileError::new(
1528 "bynk.lex.unterminated_interpolation",
1529 Span::new_in(file, start.saturating_sub(2), i.min(bytes.len())),
1530 "unterminated interpolation hole",
1531 )
1532 .with_note(
1533 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
1534 ));
1535 }
1536 match bytes[i] {
1537 b'(' => {
1538 depth += 1;
1539 i += 1;
1540 }
1541 b')' => {
1542 depth -= 1;
1543 i += 1;
1544 if depth == 0 {
1545 return Ok(i);
1546 }
1547 }
1548 b'"' => i = nested_string(source, i, nesting, file)?,
1549 b'r' if (i == start || !is_ident_byte(bytes[i - 1]))
1551 && string_open(bytes, i).is_some() =>
1552 {
1553 i = nested_string(source, i, nesting, file)?
1554 }
1555 _ => i += 1,
1556 }
1557 }
1558}
1559
1560fn nested_string(
1563 source: &str,
1564 at: usize,
1565 nesting: usize,
1566 file: FileId,
1567) -> Result<usize, CompileError> {
1568 if string_open(source.as_bytes(), at).is_some_and(|o| o.multi) {
1569 return Err(CompileError::new(
1570 "bynk.lex.multiline_in_hole",
1571 Span::new_in(file, at, at + 3),
1572 "a multi-line string cannot appear inside an interpolation hole",
1573 )
1574 .with_note(
1575 "an interpolation hole `\\(…)` closes on the line it opens on; \
1576 bind the multi-line text to a name and interpolate that",
1577 ));
1578 }
1579 Ok(lex_string(source, at, nesting + 1, file)?.0)
1580}
1581
1582fn too_deeply_nested_interpolation(span: Span) -> CompileError {
1588 CompileError::new(
1589 "bynk.lex.interpolation_too_deep",
1590 span,
1591 format!(
1592 "string interpolation nests more than {} levels deep",
1593 crate::MAX_NESTING_DEPTH
1594 ),
1595 )
1596 .with_note(
1597 "deeply nested `\\(…)` interpolation is rejected to keep the lexer from \
1598 overflowing its stack and aborting; flatten or split the string",
1599 )
1600}
1601
1602pub(crate) enum InterpSegment {
1607 Chunk(String),
1608 Hole(Span),
1609}
1610
1611pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
1616 lex_string(source, span.start, 0, span.file).map(|(_, segments)| segments)
1617}
1618
1619#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1621pub enum StringPart {
1622 Escape,
1624 RawDelimiter,
1627}
1628
1629pub fn string_parts(source: &str) -> Vec<(Span, StringPart)> {
1633 let mut out = Vec::new();
1634 if let Ok(tokens) = tokenize(source) {
1635 for tok in tokens {
1636 string_parts_of(source, tok, &mut out);
1637 }
1638 }
1639 out
1640}
1641
1642fn string_parts_of(source: &str, tok: Token, out: &mut Vec<(Span, StringPart)>) {
1645 if !matches!(tok.kind, TokenKind::StrLit | TokenKind::InterpStr) {
1646 return;
1647 }
1648 let bytes = source.as_bytes();
1649 let file = tok.span.file;
1650 let Some(open) = string_open(bytes, tok.span.start) else {
1651 return;
1652 };
1653 if open.raw {
1654 let close = if open.multi { 3 } else { 1 + open.hashes };
1657 out.push((
1658 Span::new_in(file, tok.span.start, open.body),
1659 StringPart::RawDelimiter,
1660 ));
1661 out.push((
1662 Span::new_in(file, tok.span.end - close, tok.span.end),
1663 StringPart::RawDelimiter,
1664 ));
1665 return;
1666 }
1667 let Ok(segments) = split_interp(source, tok.span) else {
1668 return;
1669 };
1670 let holes: Vec<Span> = segments
1671 .iter()
1672 .filter_map(|s| match s {
1673 InterpSegment::Hole(h) => Some(*h),
1674 InterpSegment::Chunk(_) => None,
1675 })
1676 .collect();
1677 let mut i = open.body;
1678 let end = tok.span.end - if open.multi { 3 } else { 1 };
1679 while i < end {
1680 if bytes[i] != b'\\' {
1681 i += 1;
1682 continue;
1683 }
1684 if let Some(hole) = holes.iter().find(|h| h.start == i + 2) {
1685 i = hole.end + 1; continue;
1687 }
1688 let len = if bytes.get(i + 1) == Some(&b'u') {
1689 bytes[i..end]
1690 .iter()
1691 .position(|&b| b == b'}')
1692 .map_or(2, |n| n + 1)
1693 } else {
1694 2
1695 };
1696 out.push((Span::new_in(file, i, i + len), StringPart::Escape));
1697 i += len;
1698 }
1699 for hole in holes {
1700 let Ok(hole_tokens) = tokenize_in(&source[hole.range()], file) else {
1701 continue;
1702 };
1703 for mut t in hole_tokens {
1704 t.span = Span::new_in(file, t.span.start + hole.start, t.span.end + hole.start);
1705 string_parts_of(source, t, out);
1706 }
1707 }
1708}
1709
1710pub fn decode_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1715 let (_, segments) =
1716 lex_string(lexeme, 0, 0, span.file).map_err(|e| e.offset_spans(span.start))?;
1717 let mut out = String::with_capacity(lexeme.len());
1718 for segment in segments {
1719 if let InterpSegment::Chunk(text) = segment {
1720 out.push_str(&text);
1721 }
1722 }
1723 Ok(out)
1724}
1725
1726fn doc_block_open_at(source: &str, pos: usize) -> Option<(usize, usize)> {
1732 let bytes = source.as_bytes();
1733 if !at_line_start(source, pos) {
1734 return None;
1735 }
1736 let mut i = pos;
1738 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
1739 i += 1;
1740 }
1741 if i + 3 > bytes.len() {
1742 return None;
1743 }
1744 if &bytes[i..i + 3] != b"---" {
1745 return None;
1746 }
1747 let dash_start = i;
1748 i += 3;
1749 while i < bytes.len() && bytes[i] == b'-' {
1752 i += 1;
1753 }
1754 let dashes = i - dash_start;
1755 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
1757 i += 1;
1758 }
1759 if i == bytes.len() {
1760 return Some((i, dashes));
1761 }
1762 if bytes[i] == b'\n' {
1763 return Some((i + 1, dashes));
1764 }
1765 None
1766}
1767
1768fn doc_block_close(source: &str, mut pos: usize, dashes: usize) -> Option<(usize, usize)> {
1774 let bytes = source.as_bytes();
1775 while pos < bytes.len() {
1776 let line_start = pos;
1778 let mut line_end = line_start;
1780 while line_end < bytes.len() && bytes[line_end] != b'\n' {
1781 line_end += 1;
1782 }
1783 if let Some((end, n)) = doc_block_open_at(source, line_start)
1785 && n == dashes
1786 {
1787 return Some((line_start, end));
1788 }
1789 pos = if line_end < bytes.len() {
1791 line_end + 1
1792 } else {
1793 line_end
1794 };
1795 }
1796 None
1797}
1798
1799fn only_indent_before(bytes: &[u8], pos: usize) -> bool {
1802 bytes[..pos]
1803 .iter()
1804 .rev()
1805 .take_while(|&&b| b != b'\n')
1806 .all(|&b| matches!(b, b' ' | b'\t' | b'\r'))
1807}
1808
1809fn doc_line_marker(bytes: &[u8], pos: usize) -> bool {
1814 let dashes = bytes[pos..].iter().take_while(|&&b| b == b'-').count();
1815 if dashes < 3 || !matches!(bytes.get(pos + dashes), Some(b' ' | b'\t')) {
1816 return false;
1817 }
1818 bytes[pos + dashes..]
1819 .iter()
1820 .take_while(|&&b| b != b'\n')
1821 .any(|b| !b.is_ascii_whitespace())
1822}
1823
1824fn push_doc_line(tokens: &mut Vec<Token>, bytes: &[u8], start: usize, file: FileId) -> usize {
1827 let mut pos = start;
1828 while pos < bytes.len() && bytes[pos] != b'\n' {
1829 pos += 1;
1830 }
1831 if pos < bytes.len() {
1832 pos += 1;
1833 }
1834 tokens.push(Token {
1835 kind: TokenKind::DocLine,
1836 span: Span::new_in(file, start, pos),
1837 });
1838 pos
1839}
1840
1841fn push_comment(tokens: &mut Vec<Token>, bytes: &[u8], start: usize, file: FileId) -> usize {
1844 let mut pos = start;
1845 while pos < bytes.len() && bytes[pos] != b'\n' {
1846 pos += 1;
1847 }
1848 tokens.push(Token {
1849 kind: TokenKind::Comment,
1850 span: Span::new_in(file, start, pos),
1851 });
1852 pos
1853}
1854
1855fn slash_doc_error(bytes: &[u8], pos: usize, file: FileId) -> CompileError {
1858 let marker = Span::new_in(file, pos, pos + 3);
1859 let fix = match bytes.get(pos + 3) {
1860 None | Some(b' ' | b'\t' | b'\r' | b'\n') => "---",
1861 Some(_) => "--- ",
1862 };
1863 CompileError::new(
1864 "bynk.lex.slash_doc",
1865 marker,
1866 "`///` is not a doc comment in Bynk; a doc is written with dashes",
1867 )
1868 .with_note(
1869 "a one-line doc is `--- text`, and a longer doc is a block between two `---` lines; \
1870 a comment is `//`",
1871 )
1872 .with_suggestion(
1873 "write `---`",
1874 vec![(marker, fix.to_string())],
1875 Applicability::MachineApplicable,
1876 )
1877}
1878
1879pub fn doc_line_text(source: &str, span: Span) -> &str {
1885 let slice = source[span.range()].trim_end_matches(['\n', '\r']);
1886 if let Some(rest) = slice.strip_prefix("--|") {
1887 return rest.strip_prefix(' ').unwrap_or(rest);
1888 }
1889 let dashes = slice.bytes().take_while(|&b| b == b'-').count();
1890 let rest = &slice[dashes..];
1891 let rest = rest.strip_prefix([' ', '\t']).unwrap_or(rest);
1892 let body = rest.trim_end_matches([' ', '\t']);
1893 let run = body.len() - body.trim_end_matches('-').len();
1894 if run == dashes {
1895 let before = &body[..body.len() - run];
1896 if before.is_empty() || before.ends_with([' ', '\t']) {
1897 return before.trim_end_matches([' ', '\t']);
1898 }
1899 }
1900 rest
1901}
1902
1903pub fn one_line_doc(text: &str) -> String {
1908 let body = text.trim_end_matches([' ', '\t']);
1909 if body.trim().is_empty() {
1910 return "--- ---".to_string();
1911 }
1912 let run = body.len() - body.trim_end_matches('-').len();
1913 let before = &body[..body.len() - run];
1914 let closer_shaped = run == 3 && (before.is_empty() || before.ends_with([' ', '\t']));
1915 let marker = if closer_shaped { "----" } else { "---" };
1916 format!("{marker} {text}")
1917}
1918
1919pub fn doc_fence_len(doc: &str) -> usize {
1924 doc.lines()
1925 .map(str::trim)
1926 .filter(|l| l.len() >= 3 && l.bytes().all(|b| b == b'-'))
1927 .map(|l| l.len() + 1)
1928 .fold(3, usize::max)
1929}
1930
1931pub fn doc_line_is_legacy(source: &str, span: Span) -> bool {
1934 source[span.range()].starts_with("--|")
1935}
1936
1937pub fn comment_is_legacy(source: &str, span: Span) -> bool {
1940 source[span.range()].starts_with("--")
1941}
1942
1943pub fn legacy_comment_error(span: Span) -> CompileError {
1946 let marker = Span::new_in(span.file, span.start, span.start + 2);
1947 CompileError::new(
1948 "bynk.lex.legacy_comment",
1949 marker,
1950 "`--` no longer starts a comment; a line comment is `//`",
1951 )
1952 .with_note(
1953 "`bynk fmt` rewrites every legacy comment; for subtraction of a negation, write `a - -b`",
1954 )
1955 .with_suggestion(
1956 "write `//`",
1957 vec![(marker, "//".to_string())],
1958 Applicability::MachineApplicable,
1959 )
1960}
1961
1962pub fn legacy_doc_line_error(source: &str, span: Span) -> CompileError {
1967 let marker = Span::new_in(span.file, span.start, span.start + 3);
1968 let err = CompileError::new(
1969 "bynk.lex.legacy_doc_line",
1970 marker,
1971 "`--|` no longer starts a doc; a one-line doc is `--- text`",
1972 );
1973 let text = doc_line_text(source, span);
1974 if text.trim().is_empty() {
1975 return err.with_note(
1976 "a one-line doc holds text; a doc of more than one paragraph is a block between \
1977 two `---` lines",
1978 );
1979 }
1980 let line = Span::new_in(span.file, span.start, doc_line_text_range(source, span).end);
1981 err.with_note("`bynk fmt` rewrites every legacy doc line")
1982 .with_suggestion(
1983 "write `--- `",
1984 vec![(line, one_line_doc(text))],
1985 Applicability::MachineApplicable,
1986 )
1987}
1988
1989pub fn doc_line_text_range(source: &str, span: Span) -> std::ops::Range<usize> {
1993 let text = doc_line_text(source, span);
1994 let start = text.as_ptr() as usize - source.as_ptr() as usize;
1995 start..start + text.len()
1996}
1997
1998fn at_line_start(source: &str, pos: usize) -> bool {
1999 if pos == 0 {
2000 return true;
2001 }
2002 let bytes = source.as_bytes();
2003 bytes[pos - 1] == b'\n'
2004}
2005
2006pub fn doc_block_body_range(source: &str, span: Span) -> Option<std::ops::Range<usize>> {
2013 let slice = &source[span.range()];
2014 let after_open_rel = slice.find('\n')? + 1;
2016 let after_open = &slice[after_open_rel..];
2017 let bytes = after_open.as_bytes();
2018 let mut i = bytes.len();
2020 if i > 0 && bytes[i - 1] == b'\n' {
2021 i -= 1;
2022 }
2023 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
2024 i -= 1;
2025 }
2026 while i > 0 && bytes[i - 1] == b'-' {
2027 i -= 1;
2028 }
2029 if i > 0 && bytes[i - 1] == b'\n' {
2030 i -= 1;
2031 }
2032 let start = span.range().start + after_open_rel;
2033 Some(start..start + i)
2034}
2035
2036pub fn doc_block_content(source: &str, span: Span) -> String {
2043 let Some(range) = doc_block_body_range(source, span) else {
2044 return String::new();
2045 };
2046 let body = &source[range];
2047
2048 let common: Option<usize> = body
2052 .lines()
2053 .filter(|l| !l.trim().is_empty())
2054 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
2055 .min();
2056 let strip = common.unwrap_or(0);
2057 if strip == 0 {
2058 return body.to_string();
2059 }
2060 let mut out = String::with_capacity(body.len());
2061 let mut first = true;
2062 for line in body.lines() {
2063 if !first {
2064 out.push('\n');
2065 }
2066 first = false;
2067 if line.trim().is_empty() {
2068 continue;
2070 }
2071 let leading: usize = line
2072 .bytes()
2073 .take_while(|&b| b == b' ' || b == b'\t')
2074 .count();
2075 let drop = strip.min(leading);
2076 out.push_str(&line[drop..]);
2077 }
2078 out
2079}
2080
2081pub fn comment_body(source: &str, span: Span) -> std::borrow::Cow<'_, str> {
2091 let slice = &source[span.range()];
2092 if let Some(rest) = slice.strip_prefix("//") {
2093 return rest.into();
2094 }
2095 match slice.strip_prefix("--") {
2096 Some(rest) if rest.starts_with('-') => format!(" {slice}").into(),
2097 Some(rest) => rest.strip_prefix('|').unwrap_or(rest).into(),
2099 None => slice.into(),
2100 }
2101}
2102
2103pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
2113 if to <= from {
2114 return false;
2115 }
2116 let bytes = source.as_bytes();
2117 let mut i = from;
2118 while i < to {
2119 if bytes[i] == b'\n' {
2120 return true;
2121 }
2122 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
2123 return false;
2124 }
2125 i += 1;
2126 }
2127 false
2128}
2129
2130#[cfg(test)]
2131mod tests {
2132 use super::*;
2133
2134 fn kinds(source: &str) -> Vec<TokenKind> {
2135 tokenize(source)
2136 .unwrap()
2137 .into_iter()
2138 .map(|t| t.kind)
2139 .collect()
2140 }
2141
2142 #[test]
2143 fn keywords_and_idents() {
2144 use TokenKind::*;
2145 assert_eq!(
2146 kinds("commons type fn where true false Int String Bool foo bar"),
2147 vec![
2148 Commons, Type, Fn, Where, True, False, Int, String, Bool, Ident, Ident
2149 ],
2150 );
2151 assert_eq!(kinds("and"), vec![Ident]);
2153 }
2154
2155 #[test]
2156 fn deeply_nested_interpolation_is_bounded_not_overflowed() {
2157 let depth = crate::MAX_NESTING_DEPTH + 8;
2163 let src = format!("\"{}", "\\(\"".repeat(depth));
2164 let err = tokenize(&src).unwrap_err();
2165 assert_eq!(err.category, "bynk.lex.interpolation_too_deep");
2166 }
2167
2168 #[test]
2169 fn integer_and_string_literals() {
2170 use TokenKind::*;
2171 assert_eq!(
2172 kinds(r#"0 42 "hello" "with\nescape""#),
2173 vec![IntLit, IntLit, StrLit, StrLit]
2174 );
2175 }
2176
2177 #[test]
2178 fn operators() {
2179 use TokenKind::*;
2180 assert_eq!(
2181 kinds("-> == != <= >= && || + - * / % ! = < > ( ) { } [ ] , : . @"),
2182 vec![
2183 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash,
2184 Percent, Bang, Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket,
2185 Comma, Colon, Dot, At,
2186 ],
2187 );
2188 }
2189
2190 #[test]
2191 fn dot_family_maximal_munch() {
2192 use TokenKind::*;
2199 assert_eq!(
2200 kinds("a .. b ... c . d . ."),
2201 vec![Ident, DotDot, Ident, DotDotDot, Ident, Dot, Ident, Dot, Dot,],
2202 );
2203 }
2204
2205 #[test]
2206 fn line_comments_emitted_as_trivia() {
2207 use TokenKind::*;
2210 let src = "// a comment\ntype X = Int // trailing\n";
2211 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
2212 }
2213
2214 #[test]
2215 fn comment_body_extracts_text_after_marker() {
2216 let toks = tokenize("// hello world\n").unwrap();
2217 assert_eq!(toks.len(), 1);
2218 assert_eq!(toks[0].kind, TokenKind::Comment);
2219 assert_eq!(
2220 comment_body("// hello world\n", toks[0].span),
2221 " hello world"
2222 );
2223 }
2224
2225 #[test]
2226 fn comment_does_not_consume_newline() {
2227 let toks = tokenize("// one\n// two\n").unwrap();
2230 assert_eq!(toks.len(), 2);
2231 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
2232 }
2233
2234 #[test]
2237 fn slash_slash_is_a_comment_and_dash_dash_is_subtraction() {
2238 use TokenKind::*;
2239 assert_eq!(kinds("a//b"), vec![Ident, Comment]);
2240 assert_eq!(kinds("a--b"), vec![Ident, Minus, Minus, Ident]);
2241 assert_eq!(kinds("x--\ny"), vec![Ident, Minus, Minus, Ident]);
2242 assert_eq!(kinds("x--"), vec![Ident, Minus, Minus]);
2243 assert_eq!(kinds("a - -b"), vec![Ident, Minus, Minus, Ident]);
2244 assert_eq!(kinds("a --b"), vec![Ident, Minus, Minus, Ident]);
2245 assert_eq!(kinds("////////\n"), vec![Comment]);
2247 assert_eq!(kinds("// -------\n"), vec![Comment]);
2248 }
2249
2250 #[test]
2254 fn a_legacy_dash_dash_comment_still_lexes_as_a_comment() {
2255 use TokenKind::*;
2256 assert_eq!(kinds("a -- c"), vec![Ident, Comment]);
2257 assert_eq!(kinds("-- c"), vec![Comment]);
2258 assert_eq!(kinds("a\n--\nb"), vec![Ident, Comment, Ident]);
2259 assert_eq!(kinds("x --"), vec![Ident, Comment]);
2260 assert_eq!(
2261 kinds("a: Int, --| note"),
2262 vec![Ident, Colon, Int, Comma, Comment]
2263 );
2264 let src = "x = 1 -- note\n";
2265 let toks = tokenize(src).unwrap();
2266 let c = toks.last().unwrap().span;
2267 assert!(comment_is_legacy(src, c));
2268 assert_eq!(comment_body(src, c), " note");
2269 let err = legacy_comment_error(c);
2270 assert_eq!(err.category, "bynk.lex.legacy_comment");
2271 assert_eq!(err.suggestions[0].edits[0].1, "//");
2272 assert_eq!(&src[err.suggestions[0].edits[0].0.range()], "--");
2273 }
2274
2275 #[test]
2278 fn a_dash_marker_with_text_is_a_doc_line() {
2279 use TokenKind::*;
2280 assert_eq!(kinds("--- doc\ntype"), vec![DocLine, Type]);
2281 assert_eq!(kinds(" ---- doc\n\t--- x"), vec![DocLine, DocLine]);
2282 let text = |src: &str| {
2283 let toks = tokenize(src).unwrap();
2284 assert_eq!(toks[0].kind, DocLine, "{src}");
2285 doc_line_text(src, toks[0].span).to_string()
2286 };
2287 assert_eq!(text("--- The repo.\n"), "The repo.");
2288 assert_eq!(text("--- The repo. ---\n"), "The repo.");
2289 assert_eq!(text("---- The repo. ----\r\n"), "The repo.");
2290 assert_eq!(text("--- a -- b ----\n"), "a -- b ----");
2292 assert_eq!(text("---- a ---\n"), "a ---");
2293 assert_eq!(text("--- a---\n"), "a---");
2294 assert_eq!(text("--- indented\n"), " indented");
2296 assert_eq!(kinds("---\nx\n---\n"), vec![DocBlock]);
2299 assert_eq!(kinds("---x\n"), vec![Minus, Minus, Minus, Ident]);
2300 assert_eq!(kinds("a --- b"), vec![Ident, Minus, Minus, Minus, Ident]);
2302 let src = " --- A doc. \nx";
2303 let toks = tokenize(src).unwrap();
2304 assert_eq!(&src[doc_line_text_range(src, toks[0].span)], "A doc. ");
2305 }
2306
2307 #[test]
2310 fn one_line_doc_spelling_round_trips() {
2311 for text in [
2312 "The repo.",
2313 "a ---",
2314 "---",
2315 "----",
2316 "a -- b",
2317 " indented",
2318 "",
2319 ] {
2320 let src = format!("{}\n", one_line_doc(text));
2321 let toks = tokenize(&src).unwrap();
2322 assert_eq!(toks[0].kind, TokenKind::DocLine, "{src}");
2323 assert_eq!(doc_line_text(&src, toks[0].span), text, "{src}");
2324 }
2325 }
2326
2327 #[test]
2330 fn a_legacy_bar_doc_line_still_lexes_as_a_doc_line() {
2331 use TokenKind::{DocLine, Type};
2332 assert_eq!(kinds("--| doc\ntype"), vec![DocLine, Type]);
2333 assert_eq!(kinds(" --| doc\n\t--|\n"), vec![DocLine, DocLine]);
2334 let src = " --| indented \r\n--|bare\n--|";
2335 let toks = tokenize(src).unwrap();
2336 assert!(doc_line_is_legacy(src, toks[0].span));
2337 assert_eq!(&src[toks[0].span.range()], "--| indented \r\n");
2338 assert_eq!(doc_line_text(src, toks[0].span), " indented ");
2339 assert_eq!(doc_line_text(src, toks[1].span), "bare");
2340 assert_eq!(doc_line_text(src, toks[2].span), "");
2341 let fix = |i: usize| {
2342 let err = legacy_doc_line_error(src, toks[i].span);
2343 assert_eq!(err.category, "bynk.lex.legacy_doc_line");
2344 err.suggestions
2345 .first()
2346 .map(|s| (src[s.edits[0].0.range()].to_string(), s.edits[0].1.clone()))
2347 };
2348 assert_eq!(
2349 fix(1),
2350 Some(("--|bare".to_string(), "--- bare".to_string()))
2351 );
2352 assert_eq!(fix(2), None, "an empty `--|` has no one-line spelling");
2353 }
2354
2355 #[test]
2357 fn a_slash_doc_is_an_error_with_a_fix() {
2358 for (src, fix) in [
2359 ("/// A doc.\n", "---"),
2360 ("///A doc.\n", "--- "),
2361 (" ///", "---"),
2362 ] {
2363 let err = tokenize(src).unwrap_err();
2364 assert_eq!(err.category, "bynk.lex.slash_doc");
2365 let (span, text) = &err.suggestions[0].edits[0];
2366 assert_eq!(&src[span.range()], "///");
2367 assert_eq!(text, fix);
2368 }
2369 }
2370
2371 #[test]
2374 fn a_legacy_read_keeps_the_old_comment_rule() {
2375 let src = "--- Helpers ---\nx --note\na--b\n--| doc\n// new\n";
2376 let toks = tokenize_legacy(src).unwrap();
2377 let kinds: Vec<_> = toks.iter().map(|t| t.kind).collect();
2378 use TokenKind::*;
2379 assert_eq!(
2380 kinds,
2381 vec![
2382 Comment, Ident, Comment, Ident, Minus, Minus, Ident, DocLine, Comment
2383 ]
2384 );
2385 assert_eq!(comment_body(src, toks[0].span), " --- Helpers ---");
2386 assert_eq!(comment_body(src, toks[2].span), "note");
2387 }
2388
2389 #[test]
2390 fn unterminated_string_is_error() {
2391 let err = tokenize("\"oops\n").unwrap_err();
2392 assert_eq!(err.category, "bynk.lex.unterminated_string");
2393 }
2394
2395 #[test]
2396 fn integer_overflow_is_error() {
2397 let err = tokenize("99999999999999999999").unwrap_err();
2398 assert_eq!(err.category, "bynk.lex.integer_overflow");
2399 }
2400
2401 #[test]
2402 fn an_int_literal_must_be_a_safe_integer() {
2403 assert!(tokenize("9007199254740991").is_ok());
2406 assert!(tokenize("9_007_199_254_740_991").is_ok());
2407 for over in [
2408 "9007199254740992",
2409 "9007199254740993",
2410 "9223372036854775807",
2411 ] {
2412 let err = tokenize(over).unwrap_err();
2413 assert_eq!(err.category, "bynk.lex.integer_overflow", "{over}");
2414 }
2415 }
2416
2417 #[test]
2418 fn range_bounds_never_lex_as_a_float() {
2419 use TokenKind::*;
2420 assert_eq!(kinds("0..9"), vec![IntLit, DotDot, IntLit]);
2423 assert_eq!(kinds("18.."), vec![IntLit, DotDot]);
2424 assert_eq!(kinds("..-1"), vec![DotDot, Minus, IntLit]);
2425 assert_eq!(kinds("-5..-1"), vec![Minus, IntLit, DotDot, Minus, IntLit]);
2426 assert_eq!(kinds("1_000..2_000"), vec![IntLit, DotDot, IntLit]);
2427 }
2428
2429 #[test]
2430 fn a_decimal_literal_is_one_token_with_a_d_suffix() {
2431 use TokenKind::*;
2432 assert_eq!(kinds("1.20d"), vec![DecimalLit]);
2434 assert_eq!(kinds("5d"), vec![DecimalLit]);
2435 assert_eq!(kinds("1_000.50d"), vec![DecimalLit]);
2436 assert_eq!(kinds("1.20"), vec![FloatLit]);
2439 assert_eq!(kinds("5.days"), vec![IntLit, Dot, Ident]);
2440 assert_eq!(
2441 kinds("2.5.round()"),
2442 vec![FloatLit, Dot, Ident, LParen, RParen]
2443 );
2444 assert_eq!(kinds("0..9"), vec![IntLit, DotDot, IntLit]);
2445 assert_eq!(kinds("-1.5d"), vec![Minus, DecimalLit]);
2448 assert_eq!(kinds("1e5d"), vec![FloatLit, Ident]);
2449 assert_eq!(kinds("Decimal"), vec![Decimal]);
2451 }
2452
2453 #[test]
2454 fn a_decimal_literal_obeys_the_decimal_limits() {
2455 let frac = |n: usize| format!("0.{}d", "1".repeat(n));
2456 assert!(tokenize(&frac(100)).is_ok());
2457 assert!(tokenize(&frac(101)).is_err());
2458 assert!(tokenize(&format!("{}d", "9".repeat(1000))).is_ok());
2459 assert!(tokenize(&format!("{}d", "9".repeat(1001))).is_err());
2460 }
2461
2462 #[test]
2463 fn digit_separators_lex_as_one_number() {
2464 use TokenKind::*;
2465 assert_eq!(kinds("1_048_576"), vec![IntLit]);
2468 assert_eq!(kinds("1_000.500_5"), vec![FloatLit]);
2469 assert_eq!(kinds("1_000e1_0"), vec![FloatLit]);
2470 assert!(tokenize("9_007_199_254_740_991").is_ok());
2473 let err = tokenize("9_999_999_999_999_999_999_9").unwrap_err();
2475 assert_eq!(err.category, "bynk.lex.integer_overflow");
2476 }
2477
2478 #[test]
2479 fn strip_digit_separators_removes_underscores() {
2480 assert_eq!(strip_digit_separators("1_048_576"), "1048576");
2481 assert_eq!(strip_digit_separators("42"), "42");
2482 }
2483
2484 #[test]
2485 fn unexpected_character_is_error() {
2486 let err = tokenize("type X = Int $").unwrap_err();
2487 assert_eq!(err.category, "bynk.lex.unexpected_character");
2488 }
2489
2490 #[test]
2491 fn v0_1_keywords() {
2492 use TokenKind::*;
2493 assert_eq!(
2494 kinds("let if else Ok Err Result ValidationError"),
2495 vec![Let, If, Else, Ok, Err, Result, ValidationError],
2496 );
2497 }
2498
2499 #[test]
2500 fn question_token() {
2501 use TokenKind::*;
2502 assert_eq!(kinds("x?"), vec![Ident, Question]);
2503 }
2504
2505 #[test]
2506 fn v0_2_keywords() {
2507 use TokenKind::*;
2508 assert_eq!(
2509 kinds("enum match Option record self Some None is"),
2510 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
2511 );
2512 }
2513
2514 #[test]
2515 fn pipe_and_pipe_pipe_disambiguated() {
2516 use TokenKind::*;
2517 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
2518 }
2519
2520 #[test]
2521 fn v0_7_keywords() {
2522 use TokenKind::*;
2523 assert_eq!(kinds("expect suite case"), vec![Expect, Suite, Case],);
2524 assert_eq!(kinds("mocks wires"), vec![Ident, Ident]);
2526 }
2527
2528 #[test]
2529 fn fat_arrow_and_underscore() {
2530 use TokenKind::*;
2531 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
2532 }
2533
2534 #[test]
2537 fn interp_string_is_one_token() {
2538 use TokenKind::*;
2539 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
2540 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
2542 }
2543
2544 #[test]
2545 fn interp_balances_nested_parens_and_strings() {
2546 use TokenKind::*;
2547 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
2549 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
2551 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
2553 }
2554
2555 #[test]
2558 fn expanding_holes_exposes_hole_identifiers() {
2559 use TokenKind::*;
2560 let expand = |src: &str| {
2561 tokenize_expanding_holes(src)
2562 .unwrap()
2563 .into_iter()
2564 .map(|t| t.kind)
2565 .collect::<Vec<_>>()
2566 };
2567 assert_eq!(expand(r#""Hello, \(name)!""#), vec![Ident]);
2570 assert_eq!(expand(r#""= \(f(x))""#), vec![Ident, LParen, Ident, RParen]);
2572 assert_eq!(expand(r#""out \("in \(x)")""#), vec![Ident]);
2574 assert_eq!(expand(r#""Hello, world""#), vec![StrLit]);
2576 }
2577
2578 #[test]
2579 fn expanding_holes_rebases_spans_to_absolute() {
2580 let src = r#""Hello, \(name)!""#;
2581 let toks = tokenize_expanding_holes(src).unwrap();
2582 let ident = toks
2583 .iter()
2584 .find(|t| t.kind == TokenKind::Ident)
2585 .expect("the hole identifier is exposed");
2586 assert_eq!(&src[ident.span.range()], "name");
2588 assert_eq!(ident.span.start, src.find("name").unwrap());
2589 }
2590
2591 #[test]
2592 fn escaped_open_paren_is_not_a_hole() {
2593 use TokenKind::*;
2594 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
2597 }
2598
2599 #[test]
2600 fn unterminated_hole_is_an_error() {
2601 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
2603 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
2604 }
2605
2606 #[test]
2607 fn unterminated_interp_string_is_an_error() {
2608 let err = tokenize("\"value \\(x) more\n").unwrap_err();
2610 assert_eq!(err.category, "bynk.lex.unterminated_string");
2611 }
2612
2613 #[test]
2614 fn bad_escape_in_interp_string_is_an_error() {
2615 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
2616 assert_eq!(err.category, "bynk.lex.bad_escape");
2617 }
2618
2619 fn string_value(src: &str) -> String {
2621 let toks = tokenize(src).unwrap();
2622 assert_eq!(toks.len(), 1, "{src:?}");
2623 assert_eq!(toks[0].kind, TokenKind::StrLit, "{src:?}");
2624 decode_string_literal(&src[toks[0].span.range()], toks[0].span).unwrap()
2625 }
2626
2627 #[test]
2629 fn new_escapes_decode() {
2630 assert_eq!(string_value(r#""a\r\nb""#), "a\r\nb");
2631 assert_eq!(string_value(r#""\0""#), "\0");
2632 assert_eq!(string_value(r#""\u{1F600}""#), "\u{1F600}");
2633 assert_eq!(string_value(r#""a\u{00A0}b""#), "a\u{A0}b");
2634 assert_eq!(string_value(r#""\u{41}\u{000041}""#), "AA");
2635 assert_eq!(string_value(r#""\u{10FFFF}""#), "\u{10FFFF}");
2636 }
2637
2638 #[test]
2639 fn bad_unicode_escapes_are_lexical_errors() {
2640 for src in [r#""\u{D800}""#, r#""\u{DFFF}""#, r#""\u{110000}""#] {
2641 assert_eq!(
2642 tokenize(src).unwrap_err().category,
2643 "bynk.lex.bad_unicode_escape",
2644 "{src}"
2645 );
2646 }
2647 for src in [
2648 r#""\u""#,
2649 r#""\u{}""#,
2650 r#""\u{1234567}""#,
2651 r#""\u{12""#,
2652 r#""\u{zz}""#,
2653 ] {
2654 assert_eq!(
2655 tokenize(src).unwrap_err().category,
2656 "bynk.lex.bad_escape",
2657 "{src}"
2658 );
2659 }
2660 }
2661
2662 #[test]
2663 fn raw_strings_keep_backslashes_and_do_not_interpolate() {
2664 assert_eq!(string_value(r#"r"\d+\.\d+""#), "\\d+\\.\\d+");
2665 assert_eq!(string_value(r#"r"a\(b)""#), "a\\(b)");
2666 assert_eq!(string_value(r###"r#"say "hi""#"###), "say \"hi\"");
2667 assert_eq!(string_value(r#"r"""#), "");
2668 use TokenKind::*;
2670 assert_eq!(kinds(r#"r "x""#), vec![Ident, StrLit]);
2671 assert_eq!(kinds(r#"bar"x""#), vec![Ident, StrLit]);
2672 assert_eq!(
2674 tokenize("r\"abc\n\"").unwrap_err().category,
2675 "bynk.lex.unterminated_string"
2676 );
2677 }
2678
2679 #[test]
2680 fn multiline_strings_strip_the_closing_indentation() {
2681 let src = "\"\"\"\n SELECT id\n FROM t\n\n \"\"\"";
2682 assert_eq!(string_value(src), "SELECT id\n FROM t\n");
2683 assert_eq!(string_value("\"\"\"\na\\tb\n\"\"\""), "a\tb");
2685 assert_eq!(string_value("\"\"\"\n\"\"\""), "");
2687 assert_eq!(
2689 string_value("\"\"\"\n a \"q\" \"\"b\n \"\"\""),
2690 "a \"q\" \"\"b"
2691 );
2692 assert_eq!(string_value("r\"\"\"\n \\d+\n \"\"\""), "\\d+");
2693 }
2694
2695 #[test]
2697 fn multiline_newlines_are_normalised() {
2698 assert_eq!(string_value("\"\"\"\r\n a\r\n b\r\n \"\"\""), "a\nb");
2699 }
2700
2701 #[test]
2702 fn multiline_with_a_hole_is_an_interpolated_string() {
2703 use TokenKind::*;
2704 assert_eq!(kinds("\"\"\"\n hi \\(name)\n \"\"\""), vec![InterpStr]);
2705 }
2706
2707 #[test]
2709 fn multiline_edge_cases() {
2710 let src = "\"\"\"\n \\(a)\n \\(b) x\n \"\"\"";
2713 let toks = tokenize(src).unwrap();
2714 assert_eq!(toks[0].kind, TokenKind::InterpStr);
2715 let segs = split_interp(src, toks[0].span).unwrap();
2716 let shape: Vec<String> = segs
2717 .iter()
2718 .map(|s| match s {
2719 InterpSegment::Chunk(c) => format!("{c:?}"),
2720 InterpSegment::Hole(h) => src[h.range()].to_string(),
2721 })
2722 .collect();
2723 assert_eq!(shape, ["a", "\"\\n \"", "b", "\" x\""]);
2724 assert_eq!(
2726 string_value("\"\"\"\n a\n \n b\n \"\"\""),
2727 "a\n\nb"
2728 );
2729 let err = tokenize("\"\"\"\n\t a\n \"\"\"").unwrap_err();
2731 assert_eq!(err.category, "bynk.lex.multiline_indent");
2732 assert!(err.notes[0].contains("character for character"));
2733 assert_eq!(
2735 string_value("r\"\"\"\r\n a\\\r\n b\r\n \"\"\""),
2736 "a\\\nb"
2737 );
2738 assert_eq!(string_value("\"\"\"\n a\rb\n \"\"\""), "a\rb");
2739 assert_eq!(string_value("r\"\"\"\n \"a\"\" b\n \"\"\""), "\"a\"\" b");
2741 assert!(tokenize(r#""\u{}""#).is_err());
2743 assert!(tokenize(r#""\u{00000041}""#).is_err());
2744 }
2745
2746 #[test]
2749 fn multiline_in_hole_and_stray_delimiters() {
2750 let cat = |src: &str| tokenize(src).unwrap_err().category;
2751 assert_eq!(
2752 cat("\"a \\(f(\"\"\"\n x\n \"\"\")) b\""),
2753 "bynk.lex.multiline_in_hole"
2754 );
2755 assert_eq!(
2756 cat("\"\"\"\n \\(g(\"\"\"\n x\n \"\"\"))\n \"\"\""),
2757 "bynk.lex.multiline_in_hole"
2758 );
2759 assert_eq!(
2761 cat("\"\"\"\n \"\"\"quoted\"\"\"\n \"\"\""),
2762 "bynk.lex.multiline_close_not_own_line"
2763 );
2764 let toks = tokenize("\"\"\"\n a\n \"\"\", x)").unwrap();
2766 assert_eq!(toks.len(), 4);
2767 }
2768
2769 #[test]
2770 fn multiline_errors() {
2771 let cat = |src: &str| tokenize(src).unwrap_err().category;
2772 assert_eq!(
2774 cat("\"\"\"\n a\n b\n \"\"\""),
2775 "bynk.lex.multiline_indent"
2776 );
2777 assert_eq!(cat("\"\"\"abc\n\"\"\""), "bynk.lex.multiline_open");
2779 assert_eq!(cat("\"\"\"\n a\n"), "bynk.lex.unterminated_string");
2781 assert_eq!(
2783 cat("\"\"\"\n a \"\"\" b\n \"\"\""),
2784 "bynk.lex.multiline_close_not_own_line"
2785 );
2786 }
2787
2788 #[test]
2790 fn string_parts_cover_escapes_and_raw_delimiters() {
2791 let src = "\"a\\n\\u{41}\" r#\"x\\n\"# \"\"\"\n \\t \\(\"\\r\")\n \"\"\"";
2792 let texts: Vec<(&str, StringPart)> = string_parts(src)
2793 .into_iter()
2794 .map(|(s, p)| (&src[s.range()], p))
2795 .collect();
2796 use StringPart::*;
2797 assert_eq!(
2798 texts,
2799 vec![
2800 ("\\n", Escape),
2801 ("\\u{41}", Escape),
2802 ("r#\"", RawDelimiter),
2803 ("\"#", RawDelimiter),
2804 ("\\t", Escape),
2805 ("\\r", Escape),
2806 ]
2807 );
2808 }
2809
2810 fn doc_block_span(source: &str) -> Span {
2811 tokenize(source)
2812 .unwrap()
2813 .into_iter()
2814 .find(|t| t.kind == TokenKind::DocBlock)
2815 .expect("a DocBlock token")
2816 .span
2817 }
2818
2819 #[test]
2820 fn doc_block_body_range_slices_to_the_same_bytes_doc_block_content_would_strip() {
2821 let src = "---\nHello there.\n---\nfn f() -> Int = 1\n";
2822 let span = doc_block_span(src);
2823 let range = doc_block_body_range(src, span).unwrap();
2824 assert_eq!(&src[range], "Hello there.");
2825 }
2826
2827 #[test]
2828 fn doc_block_body_range_is_offset_preserving_unlike_doc_block_content() {
2829 let src = "---\n See [Foo].\n---\nfn f() -> Int = 1\n";
2835 let span = doc_block_span(src);
2836 let range = doc_block_body_range(src, span).unwrap();
2837 assert_eq!(&src[range.clone()], " See [Foo].");
2838 assert_eq!(doc_block_content(src, span), "See [Foo].");
2839 let bracket_rel = src[range.clone()].find('[').unwrap();
2841 assert_eq!(
2842 &src[range.start + bracket_rel..range.start + bracket_rel + 5],
2843 "[Foo]"
2844 );
2845 }
2846
2847 #[test]
2848 fn doc_block_content_and_body_range_agree_on_empty_body() {
2849 let src = "---\n---\nfn f() -> Int = 1\n";
2850 let span = doc_block_span(src);
2851 let range = doc_block_body_range(src, span).unwrap();
2852 assert_eq!(&src[range], "");
2853 assert_eq!(doc_block_content(src, span), "");
2854 }
2855
2856 #[test]
2859 fn a_longer_fence_holds_shorter_marker_lines() {
2860 let src = "----\nIntro.\n---\nInner doc.\n---\nTail.\n----\nfn f() -> Int = 1\n";
2861 let tokens = tokenize(src).unwrap();
2862 let docs: Vec<_> = tokens
2863 .iter()
2864 .filter(|t| t.kind == TokenKind::DocBlock)
2865 .collect();
2866 assert_eq!(docs.len(), 1, "one block, not three");
2867 assert_eq!(
2868 doc_block_content(src, docs[0].span),
2869 "Intro.\n---\nInner doc.\n---\nTail."
2870 );
2871 }
2872
2873 #[test]
2876 fn a_body_ending_in_a_shorter_marker_line_keeps_it() {
2877 let src = "----\nAbove.\n---\n----\nfn f() -> Int = 1\n";
2878 let span = doc_block_span(src);
2879 assert_eq!(doc_block_content(src, span), "Above.\n---");
2880 }
2881
2882 #[test]
2886 fn a_marker_of_another_length_does_not_close_the_block() {
2887 let err = tokenize("----\nText.\n---\nfn f() -> Int = 1\n").unwrap_err();
2888 assert_eq!(err.category, "bynk.lex.unclosed_doc_block");
2889 let note = err.notes.join(" ");
2890 assert!(note.contains("exactly 4 dashes"), "{note}");
2891 assert!(note.contains("not a divider"), "{note}");
2892 let err = tokenize("---\nText.\n----\n").unwrap_err();
2894 assert_eq!(err.category, "bynk.lex.unclosed_doc_block");
2895 }
2896
2897 #[test]
2900 fn recovering_lex_collects_every_error_and_keeps_the_rest() {
2901 let src = "fn a() $ 1\nlet n = 99999999999999999999\n\"oops\nfn b() ~ 2\n";
2902 let (tokens, errors) = tokenize_recovering(src);
2903 let cats: Vec<_> = errors.iter().map(|e| e.category).collect();
2904 assert_eq!(
2905 cats,
2906 [
2907 "bynk.lex.unexpected_character",
2908 "bynk.lex.integer_overflow",
2909 "bynk.lex.unterminated_string",
2910 "bynk.lex.unexpected_character",
2911 ]
2912 );
2913 assert!(errors.windows(2).all(|w| w[0].span.start < w[1].span.start));
2914 let fns = tokens.iter().filter(|t| t.kind == TokenKind::Fn).count();
2916 assert_eq!(fns, 2, "{tokens:?}");
2917 assert_eq!(tokenize(src).unwrap_err().span.start, errors[0].span.start);
2919 }
2920
2921 #[test]
2924 fn recovering_lex_terminates_on_hostile_input() {
2925 for src in [
2926 "é",
2927 "\"é",
2928 "\"\"\"é",
2929 "$é$",
2930 "///é",
2931 "----\né",
2932 "r\"é",
2933 "\"\\(é",
2934 "9é99999999999999999999é",
2935 ] {
2936 let (_, errors) = tokenize_recovering(src);
2937 assert!(!errors.is_empty(), "{src:?}");
2938 }
2939 }
2940
2941 #[test]
2944 fn string_recovery_skips_a_hole_holding_a_string() {
2945 let src = "fn f() { show(\"a\\q \\(g(\"x\")) b\", 1) }\nfn k() $";
2946 let (tokens, errors) = tokenize_recovering(src);
2947 let cats: Vec<_> = errors.iter().map(|e| e.category).collect();
2948 assert_eq!(
2949 cats,
2950 ["bynk.lex.bad_escape", "bynk.lex.unexpected_character"]
2951 );
2952 assert!(!tokens.iter().any(|t| t.kind == TokenKind::StrLit));
2954 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
2955 assert_eq!(
2956 &kinds[kinds.len() - 8..kinds.len() - 4],
2957 [
2958 TokenKind::Comma,
2959 TokenKind::IntLit,
2960 TokenKind::RParen,
2961 TokenKind::RBrace
2962 ]
2963 );
2964 }
2965
2966 #[test]
2967 fn adjacent_stray_characters_are_one_error() {
2968 let (_, errors) = tokenize_recovering("fn $$$ f é$");
2969 assert_eq!(errors.len(), 2, "{errors:?}");
2970 assert_eq!(errors[0].span.end - errors[0].span.start, 3);
2971 }
2972
2973 #[test]
2974 fn an_unclosed_doc_block_swallows_the_rest_without_more_errors() {
2975 let (tokens, errors) =
2976 tokenize_recovering("fn a() = 1\n---\nit's prose; don't lex #this\nfn b() = 2\n");
2977 assert_eq!(errors.len(), 1, "{errors:?}");
2978 assert_eq!(errors[0].category, "bynk.lex.unclosed_doc_block");
2979 assert_eq!(tokens.iter().filter(|t| t.kind == TokenKind::Fn).count(), 1);
2980 }
2981
2982 #[test]
2985 fn lex_echoes_are_dropped() {
2986 let src = "commons d\n\nfn f(a: Int, b: Int) -> Int { a $ b }\n";
2987 let (tokens, lex_errors) = tokenize_recovering(src);
2988 let parse_errors = crate::parser::parse_units_recovering(&tokens, src).errors;
2989 assert!(!parse_errors.is_empty(), "`a b` is a syntax error");
2990 assert!(without_lex_echoes(&lex_errors, &tokens, parse_errors).is_empty());
2991 }
2992
2993 fn surviving(src: &str) -> usize {
2994 let (tokens, lex_errors) = tokenize_recovering(src);
2995 let parse_errors = crate::parser::parse_units_recovering(&tokens, src).errors;
2996 without_lex_echoes(&lex_errors, &tokens, parse_errors).len()
2997 }
2998
2999 #[test]
3000 fn lex_echo_window_skips_comments_and_runs_to_eof() {
3001 assert_eq!(
3002 surviving("commons d\n\nfn f(a: Int, b: Int) -> Int { a $ // why\n b }\n"),
3003 0
3004 );
3005 assert_eq!(
3007 surviving("commons d\n\nfn f() -> Int {\n 1\n---\nprose\n"),
3008 0
3009 );
3010 assert_eq!(
3012 surviving("commons d\n\nfn f() -> Int { 1 $ }\n\nfn g( -> Int { 1 }\n"),
3013 1
3014 );
3015 }
3016}