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 multiline_opener(bytes: &[u8], body: usize) -> Option<(std::ops::Range<usize>, usize)> {
1404 let mut p = body;
1405 if bytes.get(p).is_some_and(u8::is_ascii_lowercase) {
1406 p += 1;
1407 while bytes.get(p).is_some_and(|&b| {
1408 b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-'
1409 }) {
1410 p += 1;
1411 }
1412 }
1413 let tag = body..p;
1414 if !tag.is_empty() {
1415 while matches!(bytes.get(p), Some(b' ' | b'\t')) {
1416 p += 1;
1417 }
1418 }
1419 match (bytes.get(p), bytes.get(p + 1)) {
1420 (Some(b'\n'), _) => Some((tag, p + 1)),
1421 (Some(b'\r'), Some(b'\n')) => Some((tag, p + 2)),
1422 _ => None,
1423 }
1424}
1425
1426fn lex_multiline(
1432 source: &str,
1433 start: usize,
1434 open: StringOpen,
1435 depth: usize,
1436 file: FileId,
1437 segments: &mut Vec<InterpSegment>,
1438 chunk: &mut String,
1439) -> Result<usize, CompileError> {
1440 let bytes = source.as_bytes();
1441 let Some((_, mut pos)) = multiline_opener(bytes, open.body) else {
1444 return Err(CompileError::new(
1445 "bynk.lex.multiline_open",
1446 Span::new_in(file, start, open.body),
1447 "the opening `\"\"\"` of a multi-line string must be followed by a newline",
1448 )
1449 .with_note(
1450 "the text starts on the next line; write `\"\"\"` then press enter. \
1451 An optional language tag may sit directly after the delimiter \
1452 (`\"\"\"sql`): a lowercase letter, then lowercase letters, digits, `_` or `-`",
1453 ));
1454 };
1455 let mut lines: Vec<(usize, usize)> = Vec::new(); let (indent_len, end) = loop {
1458 if pos >= bytes.len() {
1459 return Err(unterminated_string(file, start, bytes.len(), true));
1460 }
1461 let line_end = bytes[pos..]
1462 .iter()
1463 .position(|&b| b == b'\n')
1464 .map_or(bytes.len(), |n| pos + n);
1465 let mut ws = pos;
1466 while ws < line_end && matches!(bytes[ws], b' ' | b'\t') {
1467 ws += 1;
1468 }
1469 if bytes[ws..line_end].starts_with(b"\"\"\"")
1472 && !bytes
1473 .get(ws + 3)
1474 .is_some_and(|&b| is_ident_byte(b) || b == b'"')
1475 {
1476 break (ws - pos, ws + 3);
1477 }
1478 let trimmed_end = if line_end > pos && bytes[line_end - 1] == b'\r' {
1479 line_end - 1
1480 } else {
1481 line_end
1482 };
1483 lines.push((pos, trimmed_end));
1484 if line_end >= bytes.len() {
1485 return Err(unterminated_string(file, start, bytes.len(), true));
1486 }
1487 pos = line_end + 1;
1488 };
1489 let closing_line_start = end - 3 - indent_len;
1490 let indent = &bytes[closing_line_start..closing_line_start + indent_len];
1491 for (n, &(ls, le)) in lines.iter().enumerate() {
1493 if n > 0 {
1494 chunk.push('\n');
1495 }
1496 let line = &bytes[ls..le];
1497 let blank = line.iter().all(|b| matches!(b, b' ' | b'\t'));
1498 let content = if line.starts_with(indent) {
1499 ls + indent_len
1500 } else if blank {
1501 le } else {
1503 return Err(CompileError::new(
1504 "bynk.lex.multiline_indent",
1505 boundary_span(source, file, ls, le),
1506 "a line of this multi-line string is indented less than its closing `\"\"\"`",
1507 )
1508 .with_note(if line.len() >= indent.len() {
1509 "the indentation must match the closing delimiter's character for character: \
1510 a tab does not match a space, so mixed tabs and spaces fail here"
1511 } else {
1512 "every non-blank line must start with the closing delimiter's indentation, \
1513 which is stripped from the value"
1514 }));
1515 };
1516 if open.raw {
1517 let text = &source[content..le];
1518 if let Some(at) = text.find("\"\"\"") {
1519 return Err(CompileError::new(
1520 "bynk.lex.multiline_close_not_own_line",
1521 Span::new_in(file, content + at, content + at + 3),
1522 "a `\"\"\"` inside a multi-line string closes it, but it is not on a line of its own",
1523 )
1524 .with_note("put the closing `\"\"\"` on its own line"));
1525 }
1526 chunk.push_str(text);
1527 } else {
1528 decode_run(source, content, Stop::End(le), depth, file, segments, chunk)?;
1529 }
1530 }
1531 Ok(end)
1532}
1533
1534fn scan_hole(
1539 bytes: &[u8],
1540 source: &str,
1541 start: usize,
1542 nesting: usize,
1543 file: FileId,
1544) -> Result<usize, CompileError> {
1545 if nesting > crate::MAX_NESTING_DEPTH {
1546 return Err(too_deeply_nested_interpolation(Span::new_in(
1550 file,
1551 start.saturating_sub(2),
1552 start,
1553 )));
1554 }
1555 let mut i = start;
1556 let mut depth = 1usize;
1557 loop {
1558 if i >= bytes.len() || bytes[i] == b'\n' {
1559 return Err(CompileError::new(
1560 "bynk.lex.unterminated_interpolation",
1561 Span::new_in(file, start.saturating_sub(2), i.min(bytes.len())),
1562 "unterminated interpolation hole",
1563 )
1564 .with_note(
1565 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
1566 ));
1567 }
1568 match bytes[i] {
1569 b'(' => {
1570 depth += 1;
1571 i += 1;
1572 }
1573 b')' => {
1574 depth -= 1;
1575 i += 1;
1576 if depth == 0 {
1577 return Ok(i);
1578 }
1579 }
1580 b'"' => i = nested_string(source, i, nesting, file)?,
1581 b'r' if (i == start || !is_ident_byte(bytes[i - 1]))
1583 && string_open(bytes, i).is_some() =>
1584 {
1585 i = nested_string(source, i, nesting, file)?
1586 }
1587 _ => i += 1,
1588 }
1589 }
1590}
1591
1592fn nested_string(
1595 source: &str,
1596 at: usize,
1597 nesting: usize,
1598 file: FileId,
1599) -> Result<usize, CompileError> {
1600 if string_open(source.as_bytes(), at).is_some_and(|o| o.multi) {
1601 return Err(CompileError::new(
1602 "bynk.lex.multiline_in_hole",
1603 Span::new_in(file, at, at + 3),
1604 "a multi-line string cannot appear inside an interpolation hole",
1605 )
1606 .with_note(
1607 "an interpolation hole `\\(…)` closes on the line it opens on; \
1608 bind the multi-line text to a name and interpolate that",
1609 ));
1610 }
1611 Ok(lex_string(source, at, nesting + 1, file)?.0)
1612}
1613
1614fn too_deeply_nested_interpolation(span: Span) -> CompileError {
1620 CompileError::new(
1621 "bynk.lex.interpolation_too_deep",
1622 span,
1623 format!(
1624 "string interpolation nests more than {} levels deep",
1625 crate::MAX_NESTING_DEPTH
1626 ),
1627 )
1628 .with_note(
1629 "deeply nested `\\(…)` interpolation is rejected to keep the lexer from \
1630 overflowing its stack and aborting; flatten or split the string",
1631 )
1632}
1633
1634pub(crate) enum InterpSegment {
1639 Chunk(String),
1640 Hole(Span),
1641}
1642
1643pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
1648 lex_string(source, span.start, 0, span.file).map(|(_, segments)| segments)
1649}
1650
1651#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1653pub enum StringPart {
1654 Escape,
1656 RawDelimiter,
1659 Tag,
1662}
1663
1664pub fn string_parts(source: &str) -> Vec<(Span, StringPart)> {
1668 let mut out = Vec::new();
1669 if let Ok(tokens) = tokenize(source) {
1670 for tok in tokens {
1671 string_parts_of(source, tok, &mut out);
1672 }
1673 }
1674 out
1675}
1676
1677fn string_parts_of(source: &str, tok: Token, out: &mut Vec<(Span, StringPart)>) {
1680 if !matches!(tok.kind, TokenKind::StrLit | TokenKind::InterpStr) {
1681 return;
1682 }
1683 let bytes = source.as_bytes();
1684 let file = tok.span.file;
1685 let Some(open) = string_open(bytes, tok.span.start) else {
1686 return;
1687 };
1688 if open.multi
1689 && let Some((tag, _)) = multiline_opener(bytes, open.body)
1690 && !tag.is_empty()
1691 {
1692 out.push((Span::new_in(file, tag.start, tag.end), StringPart::Tag));
1693 }
1694 if open.raw {
1695 let close = if open.multi { 3 } else { 1 + open.hashes };
1698 out.push((
1699 Span::new_in(file, tok.span.start, open.body),
1700 StringPart::RawDelimiter,
1701 ));
1702 out.push((
1703 Span::new_in(file, tok.span.end - close, tok.span.end),
1704 StringPart::RawDelimiter,
1705 ));
1706 return;
1707 }
1708 let Ok(segments) = split_interp(source, tok.span) else {
1709 return;
1710 };
1711 let holes: Vec<Span> = segments
1712 .iter()
1713 .filter_map(|s| match s {
1714 InterpSegment::Hole(h) => Some(*h),
1715 InterpSegment::Chunk(_) => None,
1716 })
1717 .collect();
1718 let mut i = open.body;
1719 let end = tok.span.end - if open.multi { 3 } else { 1 };
1720 while i < end {
1721 if bytes[i] != b'\\' {
1722 i += 1;
1723 continue;
1724 }
1725 if let Some(hole) = holes.iter().find(|h| h.start == i + 2) {
1726 i = hole.end + 1; continue;
1728 }
1729 let len = if bytes.get(i + 1) == Some(&b'u') {
1730 bytes[i..end]
1731 .iter()
1732 .position(|&b| b == b'}')
1733 .map_or(2, |n| n + 1)
1734 } else {
1735 2
1736 };
1737 out.push((Span::new_in(file, i, i + len), StringPart::Escape));
1738 i += len;
1739 }
1740 for hole in holes {
1741 let Ok(hole_tokens) = tokenize_in(&source[hole.range()], file) else {
1742 continue;
1743 };
1744 for mut t in hole_tokens {
1745 t.span = Span::new_in(file, t.span.start + hole.start, t.span.end + hole.start);
1746 string_parts_of(source, t, out);
1747 }
1748 }
1749}
1750
1751pub fn decode_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1756 let (_, segments) =
1757 lex_string(lexeme, 0, 0, span.file).map_err(|e| e.offset_spans(span.start))?;
1758 let mut out = String::with_capacity(lexeme.len());
1759 for segment in segments {
1760 if let InterpSegment::Chunk(text) = segment {
1761 out.push_str(&text);
1762 }
1763 }
1764 Ok(out)
1765}
1766
1767fn doc_block_open_at(source: &str, pos: usize) -> Option<(usize, usize)> {
1773 let bytes = source.as_bytes();
1774 if !at_line_start(source, pos) {
1775 return None;
1776 }
1777 let mut i = pos;
1779 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
1780 i += 1;
1781 }
1782 if i + 3 > bytes.len() {
1783 return None;
1784 }
1785 if &bytes[i..i + 3] != b"---" {
1786 return None;
1787 }
1788 let dash_start = i;
1789 i += 3;
1790 while i < bytes.len() && bytes[i] == b'-' {
1793 i += 1;
1794 }
1795 let dashes = i - dash_start;
1796 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
1798 i += 1;
1799 }
1800 if i == bytes.len() {
1801 return Some((i, dashes));
1802 }
1803 if bytes[i] == b'\n' {
1804 return Some((i + 1, dashes));
1805 }
1806 None
1807}
1808
1809fn doc_block_close(source: &str, mut pos: usize, dashes: usize) -> Option<(usize, usize)> {
1815 let bytes = source.as_bytes();
1816 while pos < bytes.len() {
1817 let line_start = pos;
1819 let mut line_end = line_start;
1821 while line_end < bytes.len() && bytes[line_end] != b'\n' {
1822 line_end += 1;
1823 }
1824 if let Some((end, n)) = doc_block_open_at(source, line_start)
1826 && n == dashes
1827 {
1828 return Some((line_start, end));
1829 }
1830 pos = if line_end < bytes.len() {
1832 line_end + 1
1833 } else {
1834 line_end
1835 };
1836 }
1837 None
1838}
1839
1840fn only_indent_before(bytes: &[u8], pos: usize) -> bool {
1843 bytes[..pos]
1844 .iter()
1845 .rev()
1846 .take_while(|&&b| b != b'\n')
1847 .all(|&b| matches!(b, b' ' | b'\t' | b'\r'))
1848}
1849
1850fn doc_line_marker(bytes: &[u8], pos: usize) -> bool {
1855 let dashes = bytes[pos..].iter().take_while(|&&b| b == b'-').count();
1856 if dashes < 3 || !matches!(bytes.get(pos + dashes), Some(b' ' | b'\t')) {
1857 return false;
1858 }
1859 bytes[pos + dashes..]
1860 .iter()
1861 .take_while(|&&b| b != b'\n')
1862 .any(|b| !b.is_ascii_whitespace())
1863}
1864
1865fn push_doc_line(tokens: &mut Vec<Token>, bytes: &[u8], start: usize, file: FileId) -> usize {
1868 let mut pos = start;
1869 while pos < bytes.len() && bytes[pos] != b'\n' {
1870 pos += 1;
1871 }
1872 if pos < bytes.len() {
1873 pos += 1;
1874 }
1875 tokens.push(Token {
1876 kind: TokenKind::DocLine,
1877 span: Span::new_in(file, start, pos),
1878 });
1879 pos
1880}
1881
1882fn push_comment(tokens: &mut Vec<Token>, bytes: &[u8], start: usize, file: FileId) -> usize {
1885 let mut pos = start;
1886 while pos < bytes.len() && bytes[pos] != b'\n' {
1887 pos += 1;
1888 }
1889 tokens.push(Token {
1890 kind: TokenKind::Comment,
1891 span: Span::new_in(file, start, pos),
1892 });
1893 pos
1894}
1895
1896fn slash_doc_error(bytes: &[u8], pos: usize, file: FileId) -> CompileError {
1899 let marker = Span::new_in(file, pos, pos + 3);
1900 let fix = match bytes.get(pos + 3) {
1901 None | Some(b' ' | b'\t' | b'\r' | b'\n') => "---",
1902 Some(_) => "--- ",
1903 };
1904 CompileError::new(
1905 "bynk.lex.slash_doc",
1906 marker,
1907 "`///` is not a doc comment in Bynk; a doc is written with dashes",
1908 )
1909 .with_note(
1910 "a one-line doc is `--- text`, and a longer doc is a block between two `---` lines; \
1911 a comment is `//`",
1912 )
1913 .with_suggestion(
1914 "write `---`",
1915 vec![(marker, fix.to_string())],
1916 Applicability::MachineApplicable,
1917 )
1918}
1919
1920pub fn doc_line_text(source: &str, span: Span) -> &str {
1926 let slice = source[span.range()].trim_end_matches(['\n', '\r']);
1927 if let Some(rest) = slice.strip_prefix("--|") {
1928 return rest.strip_prefix(' ').unwrap_or(rest);
1929 }
1930 let dashes = slice.bytes().take_while(|&b| b == b'-').count();
1931 let rest = &slice[dashes..];
1932 let rest = rest.strip_prefix([' ', '\t']).unwrap_or(rest);
1933 let body = rest.trim_end_matches([' ', '\t']);
1934 let run = body.len() - body.trim_end_matches('-').len();
1935 if run == dashes {
1936 let before = &body[..body.len() - run];
1937 if before.is_empty() || before.ends_with([' ', '\t']) {
1938 return before.trim_end_matches([' ', '\t']);
1939 }
1940 }
1941 rest
1942}
1943
1944pub fn one_line_doc(text: &str) -> String {
1949 let body = text.trim_end_matches([' ', '\t']);
1950 if body.trim().is_empty() {
1951 return "--- ---".to_string();
1952 }
1953 let run = body.len() - body.trim_end_matches('-').len();
1954 let before = &body[..body.len() - run];
1955 let closer_shaped = run == 3 && (before.is_empty() || before.ends_with([' ', '\t']));
1956 let marker = if closer_shaped { "----" } else { "---" };
1957 format!("{marker} {text}")
1958}
1959
1960pub fn doc_fence_len(doc: &str) -> usize {
1965 doc.lines()
1966 .map(str::trim)
1967 .filter(|l| l.len() >= 3 && l.bytes().all(|b| b == b'-'))
1968 .map(|l| l.len() + 1)
1969 .fold(3, usize::max)
1970}
1971
1972pub fn doc_line_is_legacy(source: &str, span: Span) -> bool {
1975 source[span.range()].starts_with("--|")
1976}
1977
1978pub fn comment_is_legacy(source: &str, span: Span) -> bool {
1981 source[span.range()].starts_with("--")
1982}
1983
1984pub fn legacy_comment_error(span: Span) -> CompileError {
1987 let marker = Span::new_in(span.file, span.start, span.start + 2);
1988 CompileError::new(
1989 "bynk.lex.legacy_comment",
1990 marker,
1991 "`--` no longer starts a comment; a line comment is `//`",
1992 )
1993 .with_note(
1994 "`bynk fmt` rewrites every legacy comment; for subtraction of a negation, write `a - -b`",
1995 )
1996 .with_suggestion(
1997 "write `//`",
1998 vec![(marker, "//".to_string())],
1999 Applicability::MachineApplicable,
2000 )
2001}
2002
2003pub fn legacy_doc_line_error(source: &str, span: Span) -> CompileError {
2008 let marker = Span::new_in(span.file, span.start, span.start + 3);
2009 let err = CompileError::new(
2010 "bynk.lex.legacy_doc_line",
2011 marker,
2012 "`--|` no longer starts a doc; a one-line doc is `--- text`",
2013 );
2014 let text = doc_line_text(source, span);
2015 if text.trim().is_empty() {
2016 return err.with_note(
2017 "a one-line doc holds text; a doc of more than one paragraph is a block between \
2018 two `---` lines",
2019 );
2020 }
2021 let line = Span::new_in(span.file, span.start, doc_line_text_range(source, span).end);
2022 err.with_note("`bynk fmt` rewrites every legacy doc line")
2023 .with_suggestion(
2024 "write `--- `",
2025 vec![(line, one_line_doc(text))],
2026 Applicability::MachineApplicable,
2027 )
2028}
2029
2030pub fn doc_line_text_range(source: &str, span: Span) -> std::ops::Range<usize> {
2034 let text = doc_line_text(source, span);
2035 let start = text.as_ptr() as usize - source.as_ptr() as usize;
2036 start..start + text.len()
2037}
2038
2039fn at_line_start(source: &str, pos: usize) -> bool {
2040 if pos == 0 {
2041 return true;
2042 }
2043 let bytes = source.as_bytes();
2044 bytes[pos - 1] == b'\n'
2045}
2046
2047pub fn doc_block_body_range(source: &str, span: Span) -> Option<std::ops::Range<usize>> {
2054 let slice = &source[span.range()];
2055 let after_open_rel = slice.find('\n')? + 1;
2057 let after_open = &slice[after_open_rel..];
2058 let bytes = after_open.as_bytes();
2059 let mut i = bytes.len();
2061 if i > 0 && bytes[i - 1] == b'\n' {
2062 i -= 1;
2063 }
2064 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
2065 i -= 1;
2066 }
2067 while i > 0 && bytes[i - 1] == b'-' {
2068 i -= 1;
2069 }
2070 if i > 0 && bytes[i - 1] == b'\n' {
2071 i -= 1;
2072 }
2073 let start = span.range().start + after_open_rel;
2074 Some(start..start + i)
2075}
2076
2077pub fn doc_block_content(source: &str, span: Span) -> String {
2084 let Some(range) = doc_block_body_range(source, span) else {
2085 return String::new();
2086 };
2087 let body = &source[range];
2088
2089 let common: Option<usize> = body
2093 .lines()
2094 .filter(|l| !l.trim().is_empty())
2095 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
2096 .min();
2097 let strip = common.unwrap_or(0);
2098 if strip == 0 {
2099 return body.to_string();
2100 }
2101 let mut out = String::with_capacity(body.len());
2102 let mut first = true;
2103 for line in body.lines() {
2104 if !first {
2105 out.push('\n');
2106 }
2107 first = false;
2108 if line.trim().is_empty() {
2109 continue;
2111 }
2112 let leading: usize = line
2113 .bytes()
2114 .take_while(|&b| b == b' ' || b == b'\t')
2115 .count();
2116 let drop = strip.min(leading);
2117 out.push_str(&line[drop..]);
2118 }
2119 out
2120}
2121
2122pub fn comment_body(source: &str, span: Span) -> std::borrow::Cow<'_, str> {
2132 let slice = &source[span.range()];
2133 if let Some(rest) = slice.strip_prefix("//") {
2134 return rest.into();
2135 }
2136 match slice.strip_prefix("--") {
2137 Some(rest) if rest.starts_with('-') => format!(" {slice}").into(),
2138 Some(rest) => rest.strip_prefix('|').unwrap_or(rest).into(),
2140 None => slice.into(),
2141 }
2142}
2143
2144pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
2154 if to <= from {
2155 return false;
2156 }
2157 let bytes = source.as_bytes();
2158 let mut i = from;
2159 while i < to {
2160 if bytes[i] == b'\n' {
2161 return true;
2162 }
2163 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
2164 return false;
2165 }
2166 i += 1;
2167 }
2168 false
2169}
2170
2171#[cfg(test)]
2172mod tests {
2173 use super::*;
2174
2175 fn kinds(source: &str) -> Vec<TokenKind> {
2176 tokenize(source)
2177 .unwrap()
2178 .into_iter()
2179 .map(|t| t.kind)
2180 .collect()
2181 }
2182
2183 #[test]
2184 fn keywords_and_idents() {
2185 use TokenKind::*;
2186 assert_eq!(
2187 kinds("commons type fn where true false Int String Bool foo bar"),
2188 vec![
2189 Commons, Type, Fn, Where, True, False, Int, String, Bool, Ident, Ident
2190 ],
2191 );
2192 assert_eq!(kinds("and"), vec![Ident]);
2194 }
2195
2196 #[test]
2197 fn deeply_nested_interpolation_is_bounded_not_overflowed() {
2198 let depth = crate::MAX_NESTING_DEPTH + 8;
2204 let src = format!("\"{}", "\\(\"".repeat(depth));
2205 let err = tokenize(&src).unwrap_err();
2206 assert_eq!(err.category, "bynk.lex.interpolation_too_deep");
2207 }
2208
2209 #[test]
2210 fn integer_and_string_literals() {
2211 use TokenKind::*;
2212 assert_eq!(
2213 kinds(r#"0 42 "hello" "with\nescape""#),
2214 vec![IntLit, IntLit, StrLit, StrLit]
2215 );
2216 }
2217
2218 #[test]
2219 fn operators() {
2220 use TokenKind::*;
2221 assert_eq!(
2222 kinds("-> == != <= >= && || + - * / % ! = < > ( ) { } [ ] , : . @"),
2223 vec![
2224 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash,
2225 Percent, Bang, Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket,
2226 Comma, Colon, Dot, At,
2227 ],
2228 );
2229 }
2230
2231 #[test]
2232 fn dot_family_maximal_munch() {
2233 use TokenKind::*;
2240 assert_eq!(
2241 kinds("a .. b ... c . d . ."),
2242 vec![Ident, DotDot, Ident, DotDotDot, Ident, Dot, Ident, Dot, Dot,],
2243 );
2244 }
2245
2246 #[test]
2247 fn line_comments_emitted_as_trivia() {
2248 use TokenKind::*;
2251 let src = "// a comment\ntype X = Int // trailing\n";
2252 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
2253 }
2254
2255 #[test]
2256 fn comment_body_extracts_text_after_marker() {
2257 let toks = tokenize("// hello world\n").unwrap();
2258 assert_eq!(toks.len(), 1);
2259 assert_eq!(toks[0].kind, TokenKind::Comment);
2260 assert_eq!(
2261 comment_body("// hello world\n", toks[0].span),
2262 " hello world"
2263 );
2264 }
2265
2266 #[test]
2267 fn comment_does_not_consume_newline() {
2268 let toks = tokenize("// one\n// two\n").unwrap();
2271 assert_eq!(toks.len(), 2);
2272 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
2273 }
2274
2275 #[test]
2278 fn slash_slash_is_a_comment_and_dash_dash_is_subtraction() {
2279 use TokenKind::*;
2280 assert_eq!(kinds("a//b"), vec![Ident, Comment]);
2281 assert_eq!(kinds("a--b"), vec![Ident, Minus, Minus, Ident]);
2282 assert_eq!(kinds("x--\ny"), vec![Ident, Minus, Minus, Ident]);
2283 assert_eq!(kinds("x--"), vec![Ident, Minus, Minus]);
2284 assert_eq!(kinds("a - -b"), vec![Ident, Minus, Minus, Ident]);
2285 assert_eq!(kinds("a --b"), vec![Ident, Minus, Minus, Ident]);
2286 assert_eq!(kinds("////////\n"), vec![Comment]);
2288 assert_eq!(kinds("// -------\n"), vec![Comment]);
2289 }
2290
2291 #[test]
2295 fn a_legacy_dash_dash_comment_still_lexes_as_a_comment() {
2296 use TokenKind::*;
2297 assert_eq!(kinds("a -- c"), vec![Ident, Comment]);
2298 assert_eq!(kinds("-- c"), vec![Comment]);
2299 assert_eq!(kinds("a\n--\nb"), vec![Ident, Comment, Ident]);
2300 assert_eq!(kinds("x --"), vec![Ident, Comment]);
2301 assert_eq!(
2302 kinds("a: Int, --| note"),
2303 vec![Ident, Colon, Int, Comma, Comment]
2304 );
2305 let src = "x = 1 -- note\n";
2306 let toks = tokenize(src).unwrap();
2307 let c = toks.last().unwrap().span;
2308 assert!(comment_is_legacy(src, c));
2309 assert_eq!(comment_body(src, c), " note");
2310 let err = legacy_comment_error(c);
2311 assert_eq!(err.category, "bynk.lex.legacy_comment");
2312 assert_eq!(err.suggestions[0].edits[0].1, "//");
2313 assert_eq!(&src[err.suggestions[0].edits[0].0.range()], "--");
2314 }
2315
2316 #[test]
2319 fn a_dash_marker_with_text_is_a_doc_line() {
2320 use TokenKind::*;
2321 assert_eq!(kinds("--- doc\ntype"), vec![DocLine, Type]);
2322 assert_eq!(kinds(" ---- doc\n\t--- x"), vec![DocLine, DocLine]);
2323 let text = |src: &str| {
2324 let toks = tokenize(src).unwrap();
2325 assert_eq!(toks[0].kind, DocLine, "{src}");
2326 doc_line_text(src, toks[0].span).to_string()
2327 };
2328 assert_eq!(text("--- The repo.\n"), "The repo.");
2329 assert_eq!(text("--- The repo. ---\n"), "The repo.");
2330 assert_eq!(text("---- The repo. ----\r\n"), "The repo.");
2331 assert_eq!(text("--- a -- b ----\n"), "a -- b ----");
2333 assert_eq!(text("---- a ---\n"), "a ---");
2334 assert_eq!(text("--- a---\n"), "a---");
2335 assert_eq!(text("--- indented\n"), " indented");
2337 assert_eq!(kinds("---\nx\n---\n"), vec![DocBlock]);
2340 assert_eq!(kinds("---x\n"), vec![Minus, Minus, Minus, Ident]);
2341 assert_eq!(kinds("a --- b"), vec![Ident, Minus, Minus, Minus, Ident]);
2343 let src = " --- A doc. \nx";
2344 let toks = tokenize(src).unwrap();
2345 assert_eq!(&src[doc_line_text_range(src, toks[0].span)], "A doc. ");
2346 }
2347
2348 #[test]
2351 fn one_line_doc_spelling_round_trips() {
2352 for text in [
2353 "The repo.",
2354 "a ---",
2355 "---",
2356 "----",
2357 "a -- b",
2358 " indented",
2359 "",
2360 ] {
2361 let src = format!("{}\n", one_line_doc(text));
2362 let toks = tokenize(&src).unwrap();
2363 assert_eq!(toks[0].kind, TokenKind::DocLine, "{src}");
2364 assert_eq!(doc_line_text(&src, toks[0].span), text, "{src}");
2365 }
2366 }
2367
2368 #[test]
2371 fn a_legacy_bar_doc_line_still_lexes_as_a_doc_line() {
2372 use TokenKind::{DocLine, Type};
2373 assert_eq!(kinds("--| doc\ntype"), vec![DocLine, Type]);
2374 assert_eq!(kinds(" --| doc\n\t--|\n"), vec![DocLine, DocLine]);
2375 let src = " --| indented \r\n--|bare\n--|";
2376 let toks = tokenize(src).unwrap();
2377 assert!(doc_line_is_legacy(src, toks[0].span));
2378 assert_eq!(&src[toks[0].span.range()], "--| indented \r\n");
2379 assert_eq!(doc_line_text(src, toks[0].span), " indented ");
2380 assert_eq!(doc_line_text(src, toks[1].span), "bare");
2381 assert_eq!(doc_line_text(src, toks[2].span), "");
2382 let fix = |i: usize| {
2383 let err = legacy_doc_line_error(src, toks[i].span);
2384 assert_eq!(err.category, "bynk.lex.legacy_doc_line");
2385 err.suggestions
2386 .first()
2387 .map(|s| (src[s.edits[0].0.range()].to_string(), s.edits[0].1.clone()))
2388 };
2389 assert_eq!(
2390 fix(1),
2391 Some(("--|bare".to_string(), "--- bare".to_string()))
2392 );
2393 assert_eq!(fix(2), None, "an empty `--|` has no one-line spelling");
2394 }
2395
2396 #[test]
2398 fn a_slash_doc_is_an_error_with_a_fix() {
2399 for (src, fix) in [
2400 ("/// A doc.\n", "---"),
2401 ("///A doc.\n", "--- "),
2402 (" ///", "---"),
2403 ] {
2404 let err = tokenize(src).unwrap_err();
2405 assert_eq!(err.category, "bynk.lex.slash_doc");
2406 let (span, text) = &err.suggestions[0].edits[0];
2407 assert_eq!(&src[span.range()], "///");
2408 assert_eq!(text, fix);
2409 }
2410 }
2411
2412 #[test]
2415 fn a_legacy_read_keeps_the_old_comment_rule() {
2416 let src = "--- Helpers ---\nx --note\na--b\n--| doc\n// new\n";
2417 let toks = tokenize_legacy(src).unwrap();
2418 let kinds: Vec<_> = toks.iter().map(|t| t.kind).collect();
2419 use TokenKind::*;
2420 assert_eq!(
2421 kinds,
2422 vec![
2423 Comment, Ident, Comment, Ident, Minus, Minus, Ident, DocLine, Comment
2424 ]
2425 );
2426 assert_eq!(comment_body(src, toks[0].span), " --- Helpers ---");
2427 assert_eq!(comment_body(src, toks[2].span), "note");
2428 }
2429
2430 #[test]
2431 fn unterminated_string_is_error() {
2432 let err = tokenize("\"oops\n").unwrap_err();
2433 assert_eq!(err.category, "bynk.lex.unterminated_string");
2434 }
2435
2436 #[test]
2437 fn integer_overflow_is_error() {
2438 let err = tokenize("99999999999999999999").unwrap_err();
2439 assert_eq!(err.category, "bynk.lex.integer_overflow");
2440 }
2441
2442 #[test]
2443 fn an_int_literal_must_be_a_safe_integer() {
2444 assert!(tokenize("9007199254740991").is_ok());
2447 assert!(tokenize("9_007_199_254_740_991").is_ok());
2448 for over in [
2449 "9007199254740992",
2450 "9007199254740993",
2451 "9223372036854775807",
2452 ] {
2453 let err = tokenize(over).unwrap_err();
2454 assert_eq!(err.category, "bynk.lex.integer_overflow", "{over}");
2455 }
2456 }
2457
2458 #[test]
2459 fn range_bounds_never_lex_as_a_float() {
2460 use TokenKind::*;
2461 assert_eq!(kinds("0..9"), vec![IntLit, DotDot, IntLit]);
2464 assert_eq!(kinds("18.."), vec![IntLit, DotDot]);
2465 assert_eq!(kinds("..-1"), vec![DotDot, Minus, IntLit]);
2466 assert_eq!(kinds("-5..-1"), vec![Minus, IntLit, DotDot, Minus, IntLit]);
2467 assert_eq!(kinds("1_000..2_000"), vec![IntLit, DotDot, IntLit]);
2468 }
2469
2470 #[test]
2471 fn a_decimal_literal_is_one_token_with_a_d_suffix() {
2472 use TokenKind::*;
2473 assert_eq!(kinds("1.20d"), vec![DecimalLit]);
2475 assert_eq!(kinds("5d"), vec![DecimalLit]);
2476 assert_eq!(kinds("1_000.50d"), vec![DecimalLit]);
2477 assert_eq!(kinds("1.20"), vec![FloatLit]);
2480 assert_eq!(kinds("5.days"), vec![IntLit, Dot, Ident]);
2481 assert_eq!(
2482 kinds("2.5.round()"),
2483 vec![FloatLit, Dot, Ident, LParen, RParen]
2484 );
2485 assert_eq!(kinds("0..9"), vec![IntLit, DotDot, IntLit]);
2486 assert_eq!(kinds("-1.5d"), vec![Minus, DecimalLit]);
2489 assert_eq!(kinds("1e5d"), vec![FloatLit, Ident]);
2490 assert_eq!(kinds("Decimal"), vec![Decimal]);
2492 }
2493
2494 #[test]
2495 fn a_decimal_literal_obeys_the_decimal_limits() {
2496 let frac = |n: usize| format!("0.{}d", "1".repeat(n));
2497 assert!(tokenize(&frac(100)).is_ok());
2498 assert!(tokenize(&frac(101)).is_err());
2499 assert!(tokenize(&format!("{}d", "9".repeat(1000))).is_ok());
2500 assert!(tokenize(&format!("{}d", "9".repeat(1001))).is_err());
2501 }
2502
2503 #[test]
2504 fn digit_separators_lex_as_one_number() {
2505 use TokenKind::*;
2506 assert_eq!(kinds("1_048_576"), vec![IntLit]);
2509 assert_eq!(kinds("1_000.500_5"), vec![FloatLit]);
2510 assert_eq!(kinds("1_000e1_0"), vec![FloatLit]);
2511 assert!(tokenize("9_007_199_254_740_991").is_ok());
2514 let err = tokenize("9_999_999_999_999_999_999_9").unwrap_err();
2516 assert_eq!(err.category, "bynk.lex.integer_overflow");
2517 }
2518
2519 #[test]
2520 fn strip_digit_separators_removes_underscores() {
2521 assert_eq!(strip_digit_separators("1_048_576"), "1048576");
2522 assert_eq!(strip_digit_separators("42"), "42");
2523 }
2524
2525 #[test]
2526 fn unexpected_character_is_error() {
2527 let err = tokenize("type X = Int $").unwrap_err();
2528 assert_eq!(err.category, "bynk.lex.unexpected_character");
2529 }
2530
2531 #[test]
2532 fn v0_1_keywords() {
2533 use TokenKind::*;
2534 assert_eq!(
2535 kinds("let if else Ok Err Result ValidationError"),
2536 vec![Let, If, Else, Ok, Err, Result, ValidationError],
2537 );
2538 }
2539
2540 #[test]
2541 fn question_token() {
2542 use TokenKind::*;
2543 assert_eq!(kinds("x?"), vec![Ident, Question]);
2544 }
2545
2546 #[test]
2547 fn v0_2_keywords() {
2548 use TokenKind::*;
2549 assert_eq!(
2550 kinds("enum match Option record self Some None is"),
2551 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
2552 );
2553 }
2554
2555 #[test]
2556 fn pipe_and_pipe_pipe_disambiguated() {
2557 use TokenKind::*;
2558 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
2559 }
2560
2561 #[test]
2562 fn v0_7_keywords() {
2563 use TokenKind::*;
2564 assert_eq!(kinds("expect suite case"), vec![Expect, Suite, Case],);
2565 assert_eq!(kinds("mocks wires"), vec![Ident, Ident]);
2567 }
2568
2569 #[test]
2570 fn fat_arrow_and_underscore() {
2571 use TokenKind::*;
2572 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
2573 }
2574
2575 #[test]
2578 fn interp_string_is_one_token() {
2579 use TokenKind::*;
2580 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
2581 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
2583 }
2584
2585 #[test]
2586 fn interp_balances_nested_parens_and_strings() {
2587 use TokenKind::*;
2588 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
2590 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
2592 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
2594 }
2595
2596 #[test]
2599 fn expanding_holes_exposes_hole_identifiers() {
2600 use TokenKind::*;
2601 let expand = |src: &str| {
2602 tokenize_expanding_holes(src)
2603 .unwrap()
2604 .into_iter()
2605 .map(|t| t.kind)
2606 .collect::<Vec<_>>()
2607 };
2608 assert_eq!(expand(r#""Hello, \(name)!""#), vec![Ident]);
2611 assert_eq!(expand(r#""= \(f(x))""#), vec![Ident, LParen, Ident, RParen]);
2613 assert_eq!(expand(r#""out \("in \(x)")""#), vec![Ident]);
2615 assert_eq!(expand(r#""Hello, world""#), vec![StrLit]);
2617 }
2618
2619 #[test]
2620 fn expanding_holes_rebases_spans_to_absolute() {
2621 let src = r#""Hello, \(name)!""#;
2622 let toks = tokenize_expanding_holes(src).unwrap();
2623 let ident = toks
2624 .iter()
2625 .find(|t| t.kind == TokenKind::Ident)
2626 .expect("the hole identifier is exposed");
2627 assert_eq!(&src[ident.span.range()], "name");
2629 assert_eq!(ident.span.start, src.find("name").unwrap());
2630 }
2631
2632 #[test]
2633 fn escaped_open_paren_is_not_a_hole() {
2634 use TokenKind::*;
2635 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
2638 }
2639
2640 #[test]
2641 fn unterminated_hole_is_an_error() {
2642 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
2644 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
2645 }
2646
2647 #[test]
2648 fn unterminated_interp_string_is_an_error() {
2649 let err = tokenize("\"value \\(x) more\n").unwrap_err();
2651 assert_eq!(err.category, "bynk.lex.unterminated_string");
2652 }
2653
2654 #[test]
2655 fn bad_escape_in_interp_string_is_an_error() {
2656 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
2657 assert_eq!(err.category, "bynk.lex.bad_escape");
2658 }
2659
2660 fn string_value(src: &str) -> String {
2662 let toks = tokenize(src).unwrap();
2663 assert_eq!(toks.len(), 1, "{src:?}");
2664 assert_eq!(toks[0].kind, TokenKind::StrLit, "{src:?}");
2665 decode_string_literal(&src[toks[0].span.range()], toks[0].span).unwrap()
2666 }
2667
2668 #[test]
2670 fn new_escapes_decode() {
2671 assert_eq!(string_value(r#""a\r\nb""#), "a\r\nb");
2672 assert_eq!(string_value(r#""\0""#), "\0");
2673 assert_eq!(string_value(r#""\u{1F600}""#), "\u{1F600}");
2674 assert_eq!(string_value(r#""a\u{00A0}b""#), "a\u{A0}b");
2675 assert_eq!(string_value(r#""\u{41}\u{000041}""#), "AA");
2676 assert_eq!(string_value(r#""\u{10FFFF}""#), "\u{10FFFF}");
2677 }
2678
2679 #[test]
2680 fn bad_unicode_escapes_are_lexical_errors() {
2681 for src in [r#""\u{D800}""#, r#""\u{DFFF}""#, r#""\u{110000}""#] {
2682 assert_eq!(
2683 tokenize(src).unwrap_err().category,
2684 "bynk.lex.bad_unicode_escape",
2685 "{src}"
2686 );
2687 }
2688 for src in [
2689 r#""\u""#,
2690 r#""\u{}""#,
2691 r#""\u{1234567}""#,
2692 r#""\u{12""#,
2693 r#""\u{zz}""#,
2694 ] {
2695 assert_eq!(
2696 tokenize(src).unwrap_err().category,
2697 "bynk.lex.bad_escape",
2698 "{src}"
2699 );
2700 }
2701 }
2702
2703 #[test]
2704 fn raw_strings_keep_backslashes_and_do_not_interpolate() {
2705 assert_eq!(string_value(r#"r"\d+\.\d+""#), "\\d+\\.\\d+");
2706 assert_eq!(string_value(r#"r"a\(b)""#), "a\\(b)");
2707 assert_eq!(string_value(r###"r#"say "hi""#"###), "say \"hi\"");
2708 assert_eq!(string_value(r#"r"""#), "");
2709 use TokenKind::*;
2711 assert_eq!(kinds(r#"r "x""#), vec![Ident, StrLit]);
2712 assert_eq!(kinds(r#"bar"x""#), vec![Ident, StrLit]);
2713 assert_eq!(
2715 tokenize("r\"abc\n\"").unwrap_err().category,
2716 "bynk.lex.unterminated_string"
2717 );
2718 }
2719
2720 #[test]
2721 fn multiline_strings_strip_the_closing_indentation() {
2722 let src = "\"\"\"\n SELECT id\n FROM t\n\n \"\"\"";
2723 assert_eq!(string_value(src), "SELECT id\n FROM t\n");
2724 assert_eq!(string_value("\"\"\"\na\\tb\n\"\"\""), "a\tb");
2726 assert_eq!(string_value("\"\"\"\n\"\"\""), "");
2728 assert_eq!(
2730 string_value("\"\"\"\n a \"q\" \"\"b\n \"\"\""),
2731 "a \"q\" \"\"b"
2732 );
2733 assert_eq!(string_value("r\"\"\"\n \\d+\n \"\"\""), "\\d+");
2734 }
2735
2736 #[test]
2738 fn multiline_newlines_are_normalised() {
2739 assert_eq!(string_value("\"\"\"\r\n a\r\n b\r\n \"\"\""), "a\nb");
2740 }
2741
2742 #[test]
2743 fn multiline_with_a_hole_is_an_interpolated_string() {
2744 use TokenKind::*;
2745 assert_eq!(kinds("\"\"\"\n hi \\(name)\n \"\"\""), vec![InterpStr]);
2746 }
2747
2748 #[test]
2750 fn multiline_edge_cases() {
2751 let src = "\"\"\"\n \\(a)\n \\(b) x\n \"\"\"";
2754 let toks = tokenize(src).unwrap();
2755 assert_eq!(toks[0].kind, TokenKind::InterpStr);
2756 let segs = split_interp(src, toks[0].span).unwrap();
2757 let shape: Vec<String> = segs
2758 .iter()
2759 .map(|s| match s {
2760 InterpSegment::Chunk(c) => format!("{c:?}"),
2761 InterpSegment::Hole(h) => src[h.range()].to_string(),
2762 })
2763 .collect();
2764 assert_eq!(shape, ["a", "\"\\n \"", "b", "\" x\""]);
2765 assert_eq!(
2767 string_value("\"\"\"\n a\n \n b\n \"\"\""),
2768 "a\n\nb"
2769 );
2770 let err = tokenize("\"\"\"\n\t a\n \"\"\"").unwrap_err();
2772 assert_eq!(err.category, "bynk.lex.multiline_indent");
2773 assert!(err.notes[0].contains("character for character"));
2774 assert_eq!(
2776 string_value("r\"\"\"\r\n a\\\r\n b\r\n \"\"\""),
2777 "a\\\nb"
2778 );
2779 assert_eq!(string_value("\"\"\"\n a\rb\n \"\"\""), "a\rb");
2780 assert_eq!(string_value("r\"\"\"\n \"a\"\" b\n \"\"\""), "\"a\"\" b");
2782 assert!(tokenize(r#""\u{}""#).is_err());
2784 assert!(tokenize(r#""\u{00000041}""#).is_err());
2785 }
2786
2787 #[test]
2790 fn multiline_in_hole_and_stray_delimiters() {
2791 let cat = |src: &str| tokenize(src).unwrap_err().category;
2792 assert_eq!(
2793 cat("\"a \\(f(\"\"\"\n x\n \"\"\")) b\""),
2794 "bynk.lex.multiline_in_hole"
2795 );
2796 assert_eq!(
2797 cat("\"\"\"\n \\(g(\"\"\"\n x\n \"\"\"))\n \"\"\""),
2798 "bynk.lex.multiline_in_hole"
2799 );
2800 assert_eq!(
2802 cat("\"\"\"\n \"\"\"quoted\"\"\"\n \"\"\""),
2803 "bynk.lex.multiline_close_not_own_line"
2804 );
2805 let toks = tokenize("\"\"\"\n a\n \"\"\", x)").unwrap();
2807 assert_eq!(toks.len(), 4);
2808 }
2809
2810 #[test]
2811 fn multiline_errors() {
2812 let cat = |src: &str| tokenize(src).unwrap_err().category;
2813 assert_eq!(
2815 cat("\"\"\"\n a\n b\n \"\"\""),
2816 "bynk.lex.multiline_indent"
2817 );
2818 assert_eq!(cat("\"\"\" abc\n\"\"\""), "bynk.lex.multiline_open");
2820 assert_eq!(cat("\"\"\"ABC\n\"\"\""), "bynk.lex.multiline_open");
2821 assert_eq!(cat("\"\"\"\n a\n"), "bynk.lex.unterminated_string");
2823 assert_eq!(
2825 cat("\"\"\"\n a \"\"\" b\n \"\"\""),
2826 "bynk.lex.multiline_close_not_own_line"
2827 );
2828 }
2829
2830 #[test]
2833 fn multiline_tag_is_value_neutral() {
2834 let body = "\n SELECT 1\n FROM t\n \"\"\"";
2835 let plain = string_value(&format!("\"\"\"{body}"));
2836 for opener in [
2837 "\"\"\"sql",
2838 "\"\"\"sql ",
2839 "\"\"\"sql\t",
2840 "\"\"\"x",
2841 "\"\"\"my-lang_2",
2842 ] {
2843 assert_eq!(
2844 string_value(&format!("{opener}{body}")),
2845 plain,
2846 "{opener:?}"
2847 );
2848 }
2849 assert_eq!(
2851 string_value("\"\"\"sql\r\n a\r\n \"\"\""),
2852 string_value("\"\"\"\r\n a\r\n \"\"\"")
2853 );
2854 assert_eq!(string_value("r\"\"\"regex\n ^\\d+$\n \"\"\""), "^\\d+$");
2855 assert_eq!(
2857 kinds("\"\"\"sql\n hi \\(name)\n \"\"\""),
2858 vec![TokenKind::InterpStr]
2859 );
2860 }
2861
2862 #[test]
2864 fn multiline_malformed_tags() {
2865 let cat = |src: &str| tokenize(src).unwrap_err().category;
2866 for opener in [
2867 "\"\"\" sql", "\"\"\"sql x", "\"\"\"SQL", "\"\"\"1sql", "\"\"\"-sql", "\"\"\"sql.x", "\"\"\" ", "r\"\"\" sql",
2875 ] {
2876 assert_eq!(
2877 cat(&format!("{opener}\n a\n \"\"\"")),
2878 "bynk.lex.multiline_open",
2879 "{opener:?}"
2880 );
2881 }
2882 let err = tokenize("\"\"\"SQL\n a\n \"\"\"").unwrap_err();
2884 assert!(err.notes[0].contains("language tag"));
2885 }
2886
2887 #[test]
2889 fn string_parts_cover_escapes_and_raw_delimiters() {
2890 let src = "\"a\\n\\u{41}\" r#\"x\\n\"# \"\"\"\n \\t \\(\"\\r\")\n \"\"\" \
2891 \"\"\"sql\n a\\n\n \"\"\" r\"\"\"regex\n \\d\n \"\"\"";
2892 let texts: Vec<(&str, StringPart)> = string_parts(src)
2893 .into_iter()
2894 .map(|(s, p)| (&src[s.range()], p))
2895 .collect();
2896 use StringPart::*;
2897 assert_eq!(
2898 texts,
2899 vec![
2900 ("\\n", Escape),
2901 ("\\u{41}", Escape),
2902 ("r#\"", RawDelimiter),
2903 ("\"#", RawDelimiter),
2904 ("\\t", Escape),
2905 ("\\r", Escape),
2906 ("sql", Tag),
2907 ("\\n", Escape),
2908 ("regex", Tag),
2909 ("r\"\"\"", RawDelimiter),
2910 ("\"\"\"", RawDelimiter),
2911 ]
2912 );
2913 }
2914
2915 fn doc_block_span(source: &str) -> Span {
2916 tokenize(source)
2917 .unwrap()
2918 .into_iter()
2919 .find(|t| t.kind == TokenKind::DocBlock)
2920 .expect("a DocBlock token")
2921 .span
2922 }
2923
2924 #[test]
2925 fn doc_block_body_range_slices_to_the_same_bytes_doc_block_content_would_strip() {
2926 let src = "---\nHello there.\n---\nfn f() -> Int = 1\n";
2927 let span = doc_block_span(src);
2928 let range = doc_block_body_range(src, span).unwrap();
2929 assert_eq!(&src[range], "Hello there.");
2930 }
2931
2932 #[test]
2933 fn doc_block_body_range_is_offset_preserving_unlike_doc_block_content() {
2934 let src = "---\n See [Foo].\n---\nfn f() -> Int = 1\n";
2940 let span = doc_block_span(src);
2941 let range = doc_block_body_range(src, span).unwrap();
2942 assert_eq!(&src[range.clone()], " See [Foo].");
2943 assert_eq!(doc_block_content(src, span), "See [Foo].");
2944 let bracket_rel = src[range.clone()].find('[').unwrap();
2946 assert_eq!(
2947 &src[range.start + bracket_rel..range.start + bracket_rel + 5],
2948 "[Foo]"
2949 );
2950 }
2951
2952 #[test]
2953 fn doc_block_content_and_body_range_agree_on_empty_body() {
2954 let src = "---\n---\nfn f() -> Int = 1\n";
2955 let span = doc_block_span(src);
2956 let range = doc_block_body_range(src, span).unwrap();
2957 assert_eq!(&src[range], "");
2958 assert_eq!(doc_block_content(src, span), "");
2959 }
2960
2961 #[test]
2964 fn a_longer_fence_holds_shorter_marker_lines() {
2965 let src = "----\nIntro.\n---\nInner doc.\n---\nTail.\n----\nfn f() -> Int = 1\n";
2966 let tokens = tokenize(src).unwrap();
2967 let docs: Vec<_> = tokens
2968 .iter()
2969 .filter(|t| t.kind == TokenKind::DocBlock)
2970 .collect();
2971 assert_eq!(docs.len(), 1, "one block, not three");
2972 assert_eq!(
2973 doc_block_content(src, docs[0].span),
2974 "Intro.\n---\nInner doc.\n---\nTail."
2975 );
2976 }
2977
2978 #[test]
2981 fn a_body_ending_in_a_shorter_marker_line_keeps_it() {
2982 let src = "----\nAbove.\n---\n----\nfn f() -> Int = 1\n";
2983 let span = doc_block_span(src);
2984 assert_eq!(doc_block_content(src, span), "Above.\n---");
2985 }
2986
2987 #[test]
2991 fn a_marker_of_another_length_does_not_close_the_block() {
2992 let err = tokenize("----\nText.\n---\nfn f() -> Int = 1\n").unwrap_err();
2993 assert_eq!(err.category, "bynk.lex.unclosed_doc_block");
2994 let note = err.notes.join(" ");
2995 assert!(note.contains("exactly 4 dashes"), "{note}");
2996 assert!(note.contains("not a divider"), "{note}");
2997 let err = tokenize("---\nText.\n----\n").unwrap_err();
2999 assert_eq!(err.category, "bynk.lex.unclosed_doc_block");
3000 }
3001
3002 #[test]
3005 fn recovering_lex_collects_every_error_and_keeps_the_rest() {
3006 let src = "fn a() $ 1\nlet n = 99999999999999999999\n\"oops\nfn b() ~ 2\n";
3007 let (tokens, errors) = tokenize_recovering(src);
3008 let cats: Vec<_> = errors.iter().map(|e| e.category).collect();
3009 assert_eq!(
3010 cats,
3011 [
3012 "bynk.lex.unexpected_character",
3013 "bynk.lex.integer_overflow",
3014 "bynk.lex.unterminated_string",
3015 "bynk.lex.unexpected_character",
3016 ]
3017 );
3018 assert!(errors.windows(2).all(|w| w[0].span.start < w[1].span.start));
3019 let fns = tokens.iter().filter(|t| t.kind == TokenKind::Fn).count();
3021 assert_eq!(fns, 2, "{tokens:?}");
3022 assert_eq!(tokenize(src).unwrap_err().span.start, errors[0].span.start);
3024 }
3025
3026 #[test]
3029 fn recovering_lex_terminates_on_hostile_input() {
3030 for src in [
3031 "é",
3032 "\"é",
3033 "\"\"\"é",
3034 "$é$",
3035 "///é",
3036 "----\né",
3037 "r\"é",
3038 "\"\\(é",
3039 "9é99999999999999999999é",
3040 ] {
3041 let (_, errors) = tokenize_recovering(src);
3042 assert!(!errors.is_empty(), "{src:?}");
3043 }
3044 }
3045
3046 #[test]
3049 fn string_recovery_skips_a_hole_holding_a_string() {
3050 let src = "fn f() { show(\"a\\q \\(g(\"x\")) b\", 1) }\nfn k() $";
3051 let (tokens, errors) = tokenize_recovering(src);
3052 let cats: Vec<_> = errors.iter().map(|e| e.category).collect();
3053 assert_eq!(
3054 cats,
3055 ["bynk.lex.bad_escape", "bynk.lex.unexpected_character"]
3056 );
3057 assert!(!tokens.iter().any(|t| t.kind == TokenKind::StrLit));
3059 let kinds: Vec<_> = tokens.iter().map(|t| t.kind).collect();
3060 assert_eq!(
3061 &kinds[kinds.len() - 8..kinds.len() - 4],
3062 [
3063 TokenKind::Comma,
3064 TokenKind::IntLit,
3065 TokenKind::RParen,
3066 TokenKind::RBrace
3067 ]
3068 );
3069 }
3070
3071 #[test]
3072 fn adjacent_stray_characters_are_one_error() {
3073 let (_, errors) = tokenize_recovering("fn $$$ f é$");
3074 assert_eq!(errors.len(), 2, "{errors:?}");
3075 assert_eq!(errors[0].span.end - errors[0].span.start, 3);
3076 }
3077
3078 #[test]
3079 fn an_unclosed_doc_block_swallows_the_rest_without_more_errors() {
3080 let (tokens, errors) =
3081 tokenize_recovering("fn a() = 1\n---\nit's prose; don't lex #this\nfn b() = 2\n");
3082 assert_eq!(errors.len(), 1, "{errors:?}");
3083 assert_eq!(errors[0].category, "bynk.lex.unclosed_doc_block");
3084 assert_eq!(tokens.iter().filter(|t| t.kind == TokenKind::Fn).count(), 1);
3085 }
3086
3087 #[test]
3090 fn lex_echoes_are_dropped() {
3091 let src = "commons d\n\nfn f(a: Int, b: Int) -> Int { a $ b }\n";
3092 let (tokens, lex_errors) = tokenize_recovering(src);
3093 let parse_errors = crate::parser::parse_units_recovering(&tokens, src).errors;
3094 assert!(!parse_errors.is_empty(), "`a b` is a syntax error");
3095 assert!(without_lex_echoes(&lex_errors, &tokens, parse_errors).is_empty());
3096 }
3097
3098 fn surviving(src: &str) -> usize {
3099 let (tokens, lex_errors) = tokenize_recovering(src);
3100 let parse_errors = crate::parser::parse_units_recovering(&tokens, src).errors;
3101 without_lex_echoes(&lex_errors, &tokens, parse_errors).len()
3102 }
3103
3104 #[test]
3105 fn lex_echo_window_skips_comments_and_runs_to_eof() {
3106 assert_eq!(
3107 surviving("commons d\n\nfn f(a: Int, b: Int) -> Int { a $ // why\n b }\n"),
3108 0
3109 );
3110 assert_eq!(
3112 surviving("commons d\n\nfn f() -> Int {\n 1\n---\nprose\n"),
3113 0
3114 );
3115 assert_eq!(
3117 surviving("commons d\n\nfn f() -> Int { 1 $ }\n\nfn g( -> Int { 1 }\n"),
3118 1
3119 );
3120 }
3121}