1use logos::Logos;
10
11use crate::error::CompileError;
12use crate::span::Span;
13
14#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)]
21#[logos(skip r"[ \t\r\n]+")]
22pub enum TokenKind {
23 #[token("commons")]
25 Commons,
26 #[token("type")]
27 Type,
28 #[token("fn")]
29 Fn,
30 #[token("where")]
31 Where,
32 #[token("and")]
33 And,
34 #[token("true")]
35 True,
36 #[token("false")]
37 False,
38 #[token("Int")]
39 Int,
40 #[token("String")]
41 String,
42 #[token("Bool")]
43 Bool,
44 #[token("Float")]
46 Float,
47 #[token("Duration")]
49 Duration,
50 #[token("Instant")]
52 Instant,
53 #[token("Bytes")]
55 Bytes,
56 #[token("let")]
58 Let,
59 #[token("if")]
60 If,
61 #[token("else")]
62 Else,
63 #[token("Ok")]
64 Ok,
65 #[token("Err")]
66 Err,
67 #[token("Result")]
68 Result,
69 #[token("ValidationError")]
70 ValidationError,
71 #[token("JsonError")]
73 JsonError,
74 #[token("enum")]
76 Enum,
77 #[token("match")]
78 Match,
79 #[token("Option")]
80 Option,
81 #[token("record")]
82 Record,
83 #[token("self")]
84 Self_,
85 #[token("Some")]
86 Some,
87 #[token("None")]
88 None,
89 #[token("is")]
90 Is,
91 #[token("opaque")]
93 Opaque,
94 #[token("uses")]
95 Uses,
96 #[token("context")]
98 Context,
99 #[token("consumes")]
100 Consumes,
101 #[token("exports")]
102 Exports,
103 #[token("transparent")]
104 Transparent,
105 #[token("as")]
107 As,
108 #[token("expect")]
111 Expect,
112 #[token("suite")]
113 Suite,
114 #[token("case")]
115 Case,
116 #[token("property")]
121 Property,
122 #[token("adapter")]
124 Adapter,
125 #[token("binding")]
126 Binding,
127 #[token("agent")]
129 Agent,
130 #[token("capability")]
131 Capability,
132 #[token("Effect")]
133 Effect,
134 #[token("given")]
135 Given,
136 #[token("on")]
137 On,
138 #[token("http")]
140 Http,
141 #[token("cron")]
143 Cron,
144 #[token("queue")]
146 Queue,
147 #[token("from")]
150 From,
151 #[token("protocol")]
152 Protocol,
153 #[token("provides")]
154 Provides,
155 #[token("service")]
156 Service,
157 #[token("actor")]
160 Actor,
161 #[token("by")]
162 By,
163 #[token("invariant")]
166 Invariant,
167 #[token("implies")]
168 Implies,
169 #[token("requires")]
177 Requires,
178 #[token("ensures")]
179 Ensures,
180 #[token("transition")]
187 Transition,
188 #[token("...")]
190 DotDotDot,
191 #[token("<-")]
193 LArrow,
194 #[token("~>")]
198 TildeArrow,
199 #[token(":=")]
203 ColonEq,
204
205 DocBlock,
210
211 Comment,
218
219 #[regex(r"[A-Za-z][A-Za-z0-9_]*")]
221 Ident,
222
223 #[regex(r"[0-9]+")]
225 IntLit,
226 #[regex(r"[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+")]
231 FloatLit,
232 #[regex(r#""([^"\\\n]|\\[nt"\\])*""#)]
236 StrLit,
237 InterpStr,
242
243 #[token("->")]
245 Arrow,
246 #[token("==")]
247 EqEq,
248 #[token("!=")]
249 BangEq,
250 #[token("<=")]
251 LtEq,
252 #[token(">=")]
253 GtEq,
254 #[token("&&")]
255 AmpAmp,
256 #[token("||")]
257 PipePipe,
258
259 #[token("+")]
261 Plus,
262 #[token("-")]
263 Minus,
264 #[token("*")]
265 Star,
266 #[token("/")]
267 Slash,
268 #[token("!")]
269 Bang,
270 #[token("=")]
271 Eq,
272 #[token("<")]
273 Lt,
274 #[token(">")]
275 Gt,
276 #[token("?")]
278 Question,
279 #[token("=>")]
281 FatArrow,
282 #[token("_")]
285 Underscore,
286 #[token("|")]
289 Pipe,
290 #[token("@")]
295 At,
296
297 #[token("(")]
299 LParen,
300 #[token(")")]
301 RParen,
302 #[token("{")]
303 LBrace,
304 #[token("}")]
305 RBrace,
306 #[token("[")]
307 LBracket,
308 #[token("]")]
309 RBracket,
310 #[token(",")]
311 Comma,
312 #[token(":")]
313 Colon,
314 #[token(".")]
315 Dot,
316}
317
318impl TokenKind {
319 pub fn describe(self) -> &'static str {
321 use TokenKind::*;
322 match self {
323 Commons => "`commons`",
324 Type => "`type`",
325 Fn => "`fn`",
326 Where => "`where`",
327 And => "`and`",
328 True => "`true`",
329 False => "`false`",
330 Int => "`Int`",
331 String => "`String`",
332 Bool => "`Bool`",
333 Float => "`Float`",
334 Duration => "`Duration`",
335 Instant => "`Instant`",
336 Bytes => "`Bytes`",
337 Let => "`let`",
338 If => "`if`",
339 Else => "`else`",
340 Ok => "`Ok`",
341 Err => "`Err`",
342 Result => "`Result`",
343 ValidationError => "`ValidationError`",
344 JsonError => "`JsonError`",
345 Enum => "`enum`",
346 Match => "`match`",
347 Option => "`Option`",
348 Record => "`record`",
349 Self_ => "`self`",
350 Some => "`Some`",
351 None => "`None`",
352 Is => "`is`",
353 Opaque => "`opaque`",
354 Uses => "`uses`",
355 Context => "`context`",
356 Consumes => "`consumes`",
357 Exports => "`exports`",
358 Transparent => "`transparent`",
359 As => "`as`",
360 Expect => "`expect`",
361 Suite => "`suite`",
362 Case => "`case`",
363 Property => "`property`",
364 Adapter => "`adapter`",
365 Binding => "`binding`",
366 Agent => "`agent`",
367 Capability => "`capability`",
368 Effect => "`Effect`",
369 Given => "`given`",
370 On => "`on`",
371 Http => "`http`",
372 Cron => "`cron`",
373 Queue => "`queue`",
374 From => "`from`",
375 Protocol => "`protocol`",
376 Provides => "`provides`",
377 Service => "`service`",
378 Actor => "`actor`",
379 By => "`by`",
380 Invariant => "`invariant`",
381 Implies => "`implies`",
382 Requires => "`requires`",
383 Ensures => "`ensures`",
384 Transition => "`transition`",
385 ColonEq => "`:=`",
386 DotDotDot => "`...`",
387 LArrow => "`<-`",
388 TildeArrow => "`~>`",
389 DocBlock => "documentation block",
390 Comment => "line comment",
391 Ident => "identifier",
392 IntLit => "integer literal",
393 FloatLit => "float literal",
394 StrLit => "string literal",
395 InterpStr => "interpolated string",
396 Arrow => "`->`",
397 EqEq => "`==`",
398 BangEq => "`!=`",
399 LtEq => "`<=`",
400 GtEq => "`>=`",
401 AmpAmp => "`&&`",
402 PipePipe => "`||`",
403 Plus => "`+`",
404 Minus => "`-`",
405 Star => "`*`",
406 Slash => "`/`",
407 Bang => "`!`",
408 Eq => "`=`",
409 Lt => "`<`",
410 Gt => "`>`",
411 Question => "`?`",
412 FatArrow => "`=>`",
413 Underscore => "`_`",
414 Pipe => "`|`",
415 At => "`@`",
416 LParen => "`(`",
417 RParen => "`)`",
418 LBrace => "`{`",
419 RBrace => "`}`",
420 LBracket => "`[`",
421 RBracket => "`]`",
422 Comma => "`,`",
423 Colon => "`:`",
424 Dot => "`.`",
425 }
426 }
427}
428
429#[derive(Debug, Clone, Copy)]
431pub struct Token {
432 pub kind: TokenKind,
433 pub span: Span,
434}
435
436pub fn tokenize(source: &str) -> Result<Vec<Token>, CompileError> {
443 let mut tokens = Vec::new();
444 let bytes = source.as_bytes();
445 let mut pos = 0;
446 while pos < bytes.len() {
447 if let Some(open_end) = doc_block_open_at(source, pos) {
451 match doc_block_close(source, open_end) {
453 Some((close_start, close_end)) => {
454 let span = Span::new(pos, close_end);
455 tokens.push(Token {
456 kind: TokenKind::DocBlock,
457 span,
458 });
459 let _ = close_start;
460 pos = close_end;
461 continue;
462 }
463 None => {
464 return Err(CompileError::new(
465 "bynk.lex.unclosed_doc_block",
466 Span::new(pos, open_end),
467 "documentation block opened but never closed",
468 )
469 .with_note(
470 "a doc block must be terminated by another `---` on a line by itself",
471 ));
472 }
473 }
474 }
475 if pos + 1 < bytes.len() && bytes[pos] == b'-' && bytes[pos + 1] == b'-' {
483 let start = pos;
484 while pos < bytes.len() && bytes[pos] != b'\n' {
485 pos += 1;
486 }
487 tokens.push(Token {
488 kind: TokenKind::Comment,
489 span: Span::new(start, pos),
490 });
491 continue;
492 }
493 if matches!(bytes[pos], b' ' | b'\t' | b'\r' | b'\n') {
496 pos += 1;
497 continue;
498 }
499 if bytes[pos] == b'"' && has_interp_hole(bytes, pos) {
505 let end = scan_str(bytes, source, pos)?;
506 tokens.push(Token {
507 kind: TokenKind::InterpStr,
508 span: Span::new(pos, end),
509 });
510 pos = end;
511 continue;
512 }
513 let mut lex = TokenKind::lexer(&source[pos..]);
515 let Some(result) = lex.next() else {
516 let ch = source[pos..].chars().next().unwrap_or('\0');
519 let span = Span::new(pos, pos + ch.len_utf8());
520 return Err(CompileError::new(
521 "bynk.lex.unexpected_character",
522 span,
523 format!("unexpected character `{ch}`"),
524 ));
525 };
526 let local = lex.span();
527 let span: Span = Span::new(pos + local.start, pos + local.end);
528 match result {
529 Ok(kind) => {
530 if kind == TokenKind::IntLit {
531 let slice = &source[span.range()];
532 if slice.parse::<i64>().is_err() {
533 return Err(CompileError::new(
534 "bynk.lex.integer_overflow",
535 span,
536 format!(
537 "integer literal `{slice}` is out of range for a 64-bit signed integer"
538 ),
539 )
540 .with_note("the range is -2^63 to 2^63 - 1"));
541 }
542 }
543 if kind == TokenKind::FloatLit {
544 let slice = &source[span.range()];
545 match slice.parse::<f64>() {
546 Ok(v) if v.is_finite() => {}
547 _ => {
548 return Err(CompileError::new(
549 "bynk.lex.float_literal_overflow",
550 span,
551 format!(
552 "float literal `{slice}` is out of range for a 64-bit float"
553 ),
554 )
555 .with_note(
556 "the literal does not fit a finite IEEE 754 double; \
557 the largest finite value is ~1.8e308",
558 ));
559 }
560 }
561 }
562 tokens.push(Token { kind, span });
563 pos = span.end;
564 }
565 Err(()) => {
566 let slice = &source[span.range()];
567 let ch = slice.chars().next().unwrap_or('\0');
568 let err = if ch == '"' {
569 CompileError::new(
570 "bynk.lex.unterminated_string",
571 span,
572 "unterminated string literal",
573 )
574 .with_note(
575 "string literals must close with `\"` on the same line; \
576 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`",
577 )
578 } else {
579 CompileError::new(
580 "bynk.lex.unexpected_character",
581 span,
582 format!("unexpected character `{ch}`"),
583 )
584 };
585 return Err(err);
586 }
587 }
588 }
589 Ok(tokens)
590}
591
592fn has_interp_hole(bytes: &[u8], start: usize) -> bool {
598 let mut i = start + 1;
599 while i < bytes.len() {
600 match bytes[i] {
601 b'\n' | b'"' => return false,
602 b'\\' => {
603 if bytes.get(i + 1) == Some(&b'(') {
604 return true;
605 }
606 i += 2;
607 }
608 _ => i += 1,
609 }
610 }
611 false
612}
613
614fn scan_str(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
619 debug_assert_eq!(bytes[start], b'"');
620 let mut i = start + 1;
621 loop {
622 if i >= bytes.len() || bytes[i] == b'\n' {
623 return Err(CompileError::new(
624 "bynk.lex.unterminated_string",
625 Span::new(start, i.min(bytes.len())),
626 "unterminated string literal",
627 )
628 .with_note(
629 "string literals must close with `\"` on the same line; \
630 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`, and `\\(…)` interpolation",
631 ));
632 }
633 match bytes[i] {
634 b'"' => return Ok(i + 1),
635 b'\\' => match bytes.get(i + 1) {
636 Some(b'n' | b't' | b'"' | b'\\') => i += 2,
637 Some(b'(') => i = scan_hole(bytes, source, i + 2)?,
638 other => {
639 let shown = other.map(|b| (*b as char).to_string()).unwrap_or_default();
640 return Err(CompileError::new(
641 "bynk.lex.bad_escape",
642 Span::new(i, (i + 2).min(bytes.len())),
643 format!("invalid escape sequence `\\{shown}` in string literal"),
644 )
645 .with_note("supported escapes: \\n \\t \\\" \\\\ \\(…)"));
646 }
647 },
648 _ => i += 1,
651 }
652 }
653}
654
655fn scan_hole(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
660 let mut i = start;
661 let mut depth = 1usize;
662 loop {
663 if i >= bytes.len() || bytes[i] == b'\n' {
664 return Err(CompileError::new(
665 "bynk.lex.unterminated_interpolation",
666 Span::new(start.saturating_sub(2), i.min(bytes.len())),
667 "unterminated interpolation hole",
668 )
669 .with_note(
670 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
671 ));
672 }
673 match bytes[i] {
674 b'(' => {
675 depth += 1;
676 i += 1;
677 }
678 b')' => {
679 depth -= 1;
680 i += 1;
681 if depth == 0 {
682 return Ok(i);
683 }
684 }
685 b'"' => i = scan_str(bytes, source, i)?,
686 _ => i += 1,
687 }
688 }
689}
690
691pub(crate) enum InterpSegment {
696 Chunk(String),
697 Hole(Span),
698}
699
700pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
705 let bytes = source.as_bytes();
706 let inner_end = span.end - 1; let mut segments = Vec::new();
708 let mut chunk = String::new();
709 let mut i = span.start + 1; while i < inner_end {
711 match bytes[i] {
712 b'\\' => match bytes[i + 1] {
713 b'n' => {
714 chunk.push('\n');
715 i += 2;
716 }
717 b't' => {
718 chunk.push('\t');
719 i += 2;
720 }
721 b'"' => {
722 chunk.push('"');
723 i += 2;
724 }
725 b'\\' => {
726 chunk.push('\\');
727 i += 2;
728 }
729 b'(' => {
730 if !chunk.is_empty() {
731 segments.push(InterpSegment::Chunk(std::mem::take(&mut chunk)));
732 }
733 let hole_start = i + 2;
734 let after = scan_hole(bytes, source, hole_start)?;
735 segments.push(InterpSegment::Hole(Span::new(hole_start, after - 1)));
738 i = after;
739 }
740 other => unreachable!("unvalidated escape `\\{}` in InterpStr", other as char),
743 },
744 _ => {
745 let ch = source[i..].chars().next().unwrap();
746 chunk.push(ch);
747 i += ch.len_utf8();
748 }
749 }
750 }
751 if !chunk.is_empty() {
752 segments.push(InterpSegment::Chunk(chunk));
753 }
754 Ok(segments)
755}
756
757fn doc_block_open_at(source: &str, pos: usize) -> Option<usize> {
763 let bytes = source.as_bytes();
764 if !at_line_start(source, pos) {
765 return None;
766 }
767 let mut i = pos;
769 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
770 i += 1;
771 }
772 if i + 3 > bytes.len() {
773 return None;
774 }
775 if &bytes[i..i + 3] != b"---" {
776 return None;
777 }
778 i += 3;
779 while i < bytes.len() && bytes[i] == b'-' {
782 i += 1;
783 }
784 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
786 i += 1;
787 }
788 if i == bytes.len() {
789 return Some(i);
790 }
791 if bytes[i] == b'\n' {
792 return Some(i + 1);
793 }
794 None
795}
796
797fn doc_block_close(source: &str, mut pos: usize) -> Option<(usize, usize)> {
801 let bytes = source.as_bytes();
802 while pos < bytes.len() {
803 let line_start = pos;
805 let mut line_end = line_start;
807 while line_end < bytes.len() && bytes[line_end] != b'\n' {
808 line_end += 1;
809 }
810 if let Some(end) = doc_block_open_at(source, line_start) {
812 return Some((line_start, end));
813 }
814 pos = if line_end < bytes.len() {
816 line_end + 1
817 } else {
818 line_end
819 };
820 }
821 None
822}
823
824fn at_line_start(source: &str, pos: usize) -> bool {
826 if pos == 0 {
827 return true;
828 }
829 let bytes = source.as_bytes();
830 bytes[pos - 1] == b'\n'
831}
832
833pub fn doc_block_content(source: &str, span: Span) -> String {
840 let slice = &source[span.range()];
841 let after_open = match slice.find('\n') {
843 Some(i) => &slice[i + 1..],
844 None => return String::new(),
845 };
846 let bytes = after_open.as_bytes();
847 let mut i = bytes.len();
849 if i > 0 && bytes[i - 1] == b'\n' {
850 i -= 1;
851 }
852 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
853 i -= 1;
854 }
855 while i > 0 && bytes[i - 1] == b'-' {
856 i -= 1;
857 }
858 if i > 0 && bytes[i - 1] == b'\n' {
859 i -= 1;
860 }
861 let body = &after_open[..i];
862
863 let common: Option<usize> = body
867 .lines()
868 .filter(|l| !l.trim().is_empty())
869 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
870 .min();
871 let strip = common.unwrap_or(0);
872 if strip == 0 {
873 return body.to_string();
874 }
875 let mut out = String::with_capacity(body.len());
876 let mut first = true;
877 for line in body.lines() {
878 if !first {
879 out.push('\n');
880 }
881 first = false;
882 if line.trim().is_empty() {
883 continue;
885 }
886 let leading: usize = line
887 .bytes()
888 .take_while(|&b| b == b' ' || b == b'\t')
889 .count();
890 let drop = strip.min(leading);
891 out.push_str(&line[drop..]);
892 }
893 out
894}
895
896pub fn comment_body(source: &str, span: Span) -> &str {
900 let slice = &source[span.range()];
901 slice.strip_prefix("--").unwrap_or(slice)
904}
905
906pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
916 if to <= from {
917 return false;
918 }
919 let bytes = source.as_bytes();
920 let mut i = from;
921 while i < to {
922 if bytes[i] == b'\n' {
923 return true;
924 }
925 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
926 return false;
927 }
928 i += 1;
929 }
930 false
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 fn kinds(source: &str) -> Vec<TokenKind> {
938 tokenize(source)
939 .unwrap()
940 .into_iter()
941 .map(|t| t.kind)
942 .collect()
943 }
944
945 #[test]
946 fn keywords_and_idents() {
947 use TokenKind::*;
948 assert_eq!(
949 kinds("commons type fn where and true false Int String Bool foo bar"),
950 vec![
951 Commons, Type, Fn, Where, And, True, False, Int, String, Bool, Ident, Ident
952 ],
953 );
954 }
955
956 #[test]
957 fn integer_and_string_literals() {
958 use TokenKind::*;
959 assert_eq!(
960 kinds(r#"0 42 "hello" "with\nescape""#),
961 vec![IntLit, IntLit, StrLit, StrLit]
962 );
963 }
964
965 #[test]
966 fn operators() {
967 use TokenKind::*;
968 assert_eq!(
969 kinds("-> == != <= >= && || + - * / ! = < > ( ) { } [ ] , : . @"),
970 vec![
971 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash, Bang,
972 Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot,
973 At,
974 ],
975 );
976 }
977
978 #[test]
979 fn line_comments_emitted_as_trivia() {
980 use TokenKind::*;
983 let src = "-- a comment\ntype X = Int -- trailing\n";
984 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
985 }
986
987 #[test]
988 fn comment_body_extracts_text_after_marker() {
989 let toks = tokenize("-- hello world\n").unwrap();
990 assert_eq!(toks.len(), 1);
991 assert_eq!(toks[0].kind, TokenKind::Comment);
992 assert_eq!(
993 comment_body("-- hello world\n", toks[0].span),
994 " hello world"
995 );
996 }
997
998 #[test]
999 fn comment_does_not_consume_newline() {
1000 let toks = tokenize("-- one\n-- two\n").unwrap();
1003 assert_eq!(toks.len(), 2);
1004 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
1005 }
1006
1007 #[test]
1008 fn unterminated_string_is_error() {
1009 let err = tokenize("\"oops\n").unwrap_err();
1010 assert_eq!(err.category, "bynk.lex.unterminated_string");
1011 }
1012
1013 #[test]
1014 fn integer_overflow_is_error() {
1015 let err = tokenize("99999999999999999999").unwrap_err();
1016 assert_eq!(err.category, "bynk.lex.integer_overflow");
1017 }
1018
1019 #[test]
1020 fn unexpected_character_is_error() {
1021 let err = tokenize("type X = Int $").unwrap_err();
1022 assert_eq!(err.category, "bynk.lex.unexpected_character");
1023 }
1024
1025 #[test]
1026 fn v0_1_keywords() {
1027 use TokenKind::*;
1028 assert_eq!(
1029 kinds("let if else Ok Err Result ValidationError"),
1030 vec![Let, If, Else, Ok, Err, Result, ValidationError],
1031 );
1032 }
1033
1034 #[test]
1035 fn question_token() {
1036 use TokenKind::*;
1037 assert_eq!(kinds("x?"), vec![Ident, Question]);
1038 }
1039
1040 #[test]
1041 fn v0_2_keywords() {
1042 use TokenKind::*;
1043 assert_eq!(
1044 kinds("enum match Option record self Some None is"),
1045 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
1046 );
1047 }
1048
1049 #[test]
1050 fn pipe_and_pipe_pipe_disambiguated() {
1051 use TokenKind::*;
1052 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
1053 }
1054
1055 #[test]
1056 fn v0_7_keywords() {
1057 use TokenKind::*;
1058 assert_eq!(kinds("expect suite case"), vec![Expect, Suite, Case],);
1059 assert_eq!(kinds("mocks wires"), vec![Ident, Ident]);
1061 }
1062
1063 #[test]
1064 fn fat_arrow_and_underscore() {
1065 use TokenKind::*;
1066 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
1067 }
1068
1069 #[test]
1072 fn interp_string_is_one_token() {
1073 use TokenKind::*;
1074 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
1075 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
1077 }
1078
1079 #[test]
1080 fn interp_balances_nested_parens_and_strings() {
1081 use TokenKind::*;
1082 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
1084 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
1086 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
1088 }
1089
1090 #[test]
1091 fn escaped_open_paren_is_not_a_hole() {
1092 use TokenKind::*;
1093 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
1096 }
1097
1098 #[test]
1099 fn unterminated_hole_is_an_error() {
1100 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
1102 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
1103 }
1104
1105 #[test]
1106 fn unterminated_interp_string_is_an_error() {
1107 let err = tokenize("\"value \\(x) more\n").unwrap_err();
1109 assert_eq!(err.category, "bynk.lex.unterminated_string");
1110 }
1111
1112 #[test]
1113 fn bad_escape_in_interp_string_is_an_error() {
1114 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
1115 assert_eq!(err.category, "bynk.lex.bad_escape");
1116 }
1117}