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("assert")]
110 Assert,
111 #[token("expect")]
112 Expect,
113 #[token("mocks")]
114 Mocks,
115 #[token("test")]
116 Test,
117 #[token("wires")]
119 Wires,
120 #[token("adapter")]
122 Adapter,
123 #[token("binding")]
124 Binding,
125 #[token("agent")]
127 Agent,
128 #[token("capability")]
129 Capability,
130 #[token("Effect")]
131 Effect,
132 #[token("given")]
133 Given,
134 #[token("on")]
135 On,
136 #[token("http")]
138 Http,
139 #[token("cron")]
141 Cron,
142 #[token("queue")]
144 Queue,
145 #[token("from")]
148 From,
149 #[token("protocol")]
150 Protocol,
151 #[token("provides")]
152 Provides,
153 #[token("service")]
154 Service,
155 #[token("actor")]
158 Actor,
159 #[token("by")]
160 By,
161 #[token("invariant")]
164 Invariant,
165 #[token("implies")]
166 Implies,
167 #[token("...")]
169 DotDotDot,
170 #[token("<-")]
172 LArrow,
173 #[token("~>")]
177 TildeArrow,
178 #[token(":=")]
182 ColonEq,
183
184 DocBlock,
189
190 Comment,
197
198 #[regex(r"[A-Za-z][A-Za-z0-9_]*")]
200 Ident,
201
202 #[regex(r"[0-9]+")]
204 IntLit,
205 #[regex(r"[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+")]
210 FloatLit,
211 #[regex(r#""([^"\\\n]|\\[nt"\\])*""#)]
215 StrLit,
216 InterpStr,
221
222 #[token("->")]
224 Arrow,
225 #[token("==")]
226 EqEq,
227 #[token("!=")]
228 BangEq,
229 #[token("<=")]
230 LtEq,
231 #[token(">=")]
232 GtEq,
233 #[token("&&")]
234 AmpAmp,
235 #[token("||")]
236 PipePipe,
237
238 #[token("+")]
240 Plus,
241 #[token("-")]
242 Minus,
243 #[token("*")]
244 Star,
245 #[token("/")]
246 Slash,
247 #[token("!")]
248 Bang,
249 #[token("=")]
250 Eq,
251 #[token("<")]
252 Lt,
253 #[token(">")]
254 Gt,
255 #[token("?")]
257 Question,
258 #[token("=>")]
260 FatArrow,
261 #[token("_")]
264 Underscore,
265 #[token("|")]
268 Pipe,
269 #[token("@")]
274 At,
275
276 #[token("(")]
278 LParen,
279 #[token(")")]
280 RParen,
281 #[token("{")]
282 LBrace,
283 #[token("}")]
284 RBrace,
285 #[token("[")]
286 LBracket,
287 #[token("]")]
288 RBracket,
289 #[token(",")]
290 Comma,
291 #[token(":")]
292 Colon,
293 #[token(".")]
294 Dot,
295}
296
297impl TokenKind {
298 pub fn describe(self) -> &'static str {
300 use TokenKind::*;
301 match self {
302 Commons => "`commons`",
303 Type => "`type`",
304 Fn => "`fn`",
305 Where => "`where`",
306 And => "`and`",
307 True => "`true`",
308 False => "`false`",
309 Int => "`Int`",
310 String => "`String`",
311 Bool => "`Bool`",
312 Float => "`Float`",
313 Duration => "`Duration`",
314 Instant => "`Instant`",
315 Bytes => "`Bytes`",
316 Let => "`let`",
317 If => "`if`",
318 Else => "`else`",
319 Ok => "`Ok`",
320 Err => "`Err`",
321 Result => "`Result`",
322 ValidationError => "`ValidationError`",
323 JsonError => "`JsonError`",
324 Enum => "`enum`",
325 Match => "`match`",
326 Option => "`Option`",
327 Record => "`record`",
328 Self_ => "`self`",
329 Some => "`Some`",
330 None => "`None`",
331 Is => "`is`",
332 Opaque => "`opaque`",
333 Uses => "`uses`",
334 Context => "`context`",
335 Consumes => "`consumes`",
336 Exports => "`exports`",
337 Transparent => "`transparent`",
338 As => "`as`",
339 Assert => "`assert`",
340 Expect => "`expect`",
341 Mocks => "`mocks`",
342 Test => "`test`",
343 Wires => "`wires`",
344 Adapter => "`adapter`",
345 Binding => "`binding`",
346 Agent => "`agent`",
347 Capability => "`capability`",
348 Effect => "`Effect`",
349 Given => "`given`",
350 On => "`on`",
351 Http => "`http`",
352 Cron => "`cron`",
353 Queue => "`queue`",
354 From => "`from`",
355 Protocol => "`protocol`",
356 Provides => "`provides`",
357 Service => "`service`",
358 Actor => "`actor`",
359 By => "`by`",
360 Invariant => "`invariant`",
361 Implies => "`implies`",
362 ColonEq => "`:=`",
363 DotDotDot => "`...`",
364 LArrow => "`<-`",
365 TildeArrow => "`~>`",
366 DocBlock => "documentation block",
367 Comment => "line comment",
368 Ident => "identifier",
369 IntLit => "integer literal",
370 FloatLit => "float literal",
371 StrLit => "string literal",
372 InterpStr => "interpolated string",
373 Arrow => "`->`",
374 EqEq => "`==`",
375 BangEq => "`!=`",
376 LtEq => "`<=`",
377 GtEq => "`>=`",
378 AmpAmp => "`&&`",
379 PipePipe => "`||`",
380 Plus => "`+`",
381 Minus => "`-`",
382 Star => "`*`",
383 Slash => "`/`",
384 Bang => "`!`",
385 Eq => "`=`",
386 Lt => "`<`",
387 Gt => "`>`",
388 Question => "`?`",
389 FatArrow => "`=>`",
390 Underscore => "`_`",
391 Pipe => "`|`",
392 At => "`@`",
393 LParen => "`(`",
394 RParen => "`)`",
395 LBrace => "`{`",
396 RBrace => "`}`",
397 LBracket => "`[`",
398 RBracket => "`]`",
399 Comma => "`,`",
400 Colon => "`:`",
401 Dot => "`.`",
402 }
403 }
404}
405
406#[derive(Debug, Clone, Copy)]
408pub struct Token {
409 pub kind: TokenKind,
410 pub span: Span,
411}
412
413pub fn tokenize(source: &str) -> Result<Vec<Token>, CompileError> {
420 let mut tokens = Vec::new();
421 let bytes = source.as_bytes();
422 let mut pos = 0;
423 while pos < bytes.len() {
424 if let Some(open_end) = doc_block_open_at(source, pos) {
428 match doc_block_close(source, open_end) {
430 Some((close_start, close_end)) => {
431 let span = Span::new(pos, close_end);
432 tokens.push(Token {
433 kind: TokenKind::DocBlock,
434 span,
435 });
436 let _ = close_start;
437 pos = close_end;
438 continue;
439 }
440 None => {
441 return Err(CompileError::new(
442 "bynk.lex.unclosed_doc_block",
443 Span::new(pos, open_end),
444 "documentation block opened but never closed",
445 )
446 .with_note(
447 "a doc block must be terminated by another `---` on a line by itself",
448 ));
449 }
450 }
451 }
452 if pos + 1 < bytes.len() && bytes[pos] == b'-' && bytes[pos + 1] == b'-' {
460 let start = pos;
461 while pos < bytes.len() && bytes[pos] != b'\n' {
462 pos += 1;
463 }
464 tokens.push(Token {
465 kind: TokenKind::Comment,
466 span: Span::new(start, pos),
467 });
468 continue;
469 }
470 if matches!(bytes[pos], b' ' | b'\t' | b'\r' | b'\n') {
473 pos += 1;
474 continue;
475 }
476 if bytes[pos] == b'"' && has_interp_hole(bytes, pos) {
482 let end = scan_str(bytes, source, pos)?;
483 tokens.push(Token {
484 kind: TokenKind::InterpStr,
485 span: Span::new(pos, end),
486 });
487 pos = end;
488 continue;
489 }
490 let mut lex = TokenKind::lexer(&source[pos..]);
492 let Some(result) = lex.next() else {
493 let ch = source[pos..].chars().next().unwrap_or('\0');
496 let span = Span::new(pos, pos + ch.len_utf8());
497 return Err(CompileError::new(
498 "bynk.lex.unexpected_character",
499 span,
500 format!("unexpected character `{ch}`"),
501 ));
502 };
503 let local = lex.span();
504 let span: Span = Span::new(pos + local.start, pos + local.end);
505 match result {
506 Ok(kind) => {
507 if kind == TokenKind::IntLit {
508 let slice = &source[span.range()];
509 if slice.parse::<i64>().is_err() {
510 return Err(CompileError::new(
511 "bynk.lex.integer_overflow",
512 span,
513 format!(
514 "integer literal `{slice}` is out of range for a 64-bit signed integer"
515 ),
516 )
517 .with_note("the range is -2^63 to 2^63 - 1"));
518 }
519 }
520 if kind == TokenKind::FloatLit {
521 let slice = &source[span.range()];
522 match slice.parse::<f64>() {
523 Ok(v) if v.is_finite() => {}
524 _ => {
525 return Err(CompileError::new(
526 "bynk.lex.float_literal_overflow",
527 span,
528 format!(
529 "float literal `{slice}` is out of range for a 64-bit float"
530 ),
531 )
532 .with_note(
533 "the literal does not fit a finite IEEE 754 double; \
534 the largest finite value is ~1.8e308",
535 ));
536 }
537 }
538 }
539 tokens.push(Token { kind, span });
540 pos = span.end;
541 }
542 Err(()) => {
543 let slice = &source[span.range()];
544 let ch = slice.chars().next().unwrap_or('\0');
545 let err = if ch == '"' {
546 CompileError::new(
547 "bynk.lex.unterminated_string",
548 span,
549 "unterminated string literal",
550 )
551 .with_note(
552 "string literals must close with `\"` on the same line; \
553 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`",
554 )
555 } else {
556 CompileError::new(
557 "bynk.lex.unexpected_character",
558 span,
559 format!("unexpected character `{ch}`"),
560 )
561 };
562 return Err(err);
563 }
564 }
565 }
566 Ok(tokens)
567}
568
569fn has_interp_hole(bytes: &[u8], start: usize) -> bool {
575 let mut i = start + 1;
576 while i < bytes.len() {
577 match bytes[i] {
578 b'\n' | b'"' => return false,
579 b'\\' => {
580 if bytes.get(i + 1) == Some(&b'(') {
581 return true;
582 }
583 i += 2;
584 }
585 _ => i += 1,
586 }
587 }
588 false
589}
590
591fn scan_str(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
596 debug_assert_eq!(bytes[start], b'"');
597 let mut i = start + 1;
598 loop {
599 if i >= bytes.len() || bytes[i] == b'\n' {
600 return Err(CompileError::new(
601 "bynk.lex.unterminated_string",
602 Span::new(start, i.min(bytes.len())),
603 "unterminated string literal",
604 )
605 .with_note(
606 "string literals must close with `\"` on the same line; \
607 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`, and `\\(…)` interpolation",
608 ));
609 }
610 match bytes[i] {
611 b'"' => return Ok(i + 1),
612 b'\\' => match bytes.get(i + 1) {
613 Some(b'n' | b't' | b'"' | b'\\') => i += 2,
614 Some(b'(') => i = scan_hole(bytes, source, i + 2)?,
615 other => {
616 let shown = other.map(|b| (*b as char).to_string()).unwrap_or_default();
617 return Err(CompileError::new(
618 "bynk.lex.bad_escape",
619 Span::new(i, (i + 2).min(bytes.len())),
620 format!("invalid escape sequence `\\{shown}` in string literal"),
621 )
622 .with_note("supported escapes: \\n \\t \\\" \\\\ \\(…)"));
623 }
624 },
625 _ => i += 1,
628 }
629 }
630}
631
632fn scan_hole(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
637 let mut i = start;
638 let mut depth = 1usize;
639 loop {
640 if i >= bytes.len() || bytes[i] == b'\n' {
641 return Err(CompileError::new(
642 "bynk.lex.unterminated_interpolation",
643 Span::new(start.saturating_sub(2), i.min(bytes.len())),
644 "unterminated interpolation hole",
645 )
646 .with_note(
647 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
648 ));
649 }
650 match bytes[i] {
651 b'(' => {
652 depth += 1;
653 i += 1;
654 }
655 b')' => {
656 depth -= 1;
657 i += 1;
658 if depth == 0 {
659 return Ok(i);
660 }
661 }
662 b'"' => i = scan_str(bytes, source, i)?,
663 _ => i += 1,
664 }
665 }
666}
667
668pub(crate) enum InterpSegment {
673 Chunk(String),
674 Hole(Span),
675}
676
677pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
682 let bytes = source.as_bytes();
683 let inner_end = span.end - 1; let mut segments = Vec::new();
685 let mut chunk = String::new();
686 let mut i = span.start + 1; while i < inner_end {
688 match bytes[i] {
689 b'\\' => match bytes[i + 1] {
690 b'n' => {
691 chunk.push('\n');
692 i += 2;
693 }
694 b't' => {
695 chunk.push('\t');
696 i += 2;
697 }
698 b'"' => {
699 chunk.push('"');
700 i += 2;
701 }
702 b'\\' => {
703 chunk.push('\\');
704 i += 2;
705 }
706 b'(' => {
707 if !chunk.is_empty() {
708 segments.push(InterpSegment::Chunk(std::mem::take(&mut chunk)));
709 }
710 let hole_start = i + 2;
711 let after = scan_hole(bytes, source, hole_start)?;
712 segments.push(InterpSegment::Hole(Span::new(hole_start, after - 1)));
715 i = after;
716 }
717 other => unreachable!("unvalidated escape `\\{}` in InterpStr", other as char),
720 },
721 _ => {
722 let ch = source[i..].chars().next().unwrap();
723 chunk.push(ch);
724 i += ch.len_utf8();
725 }
726 }
727 }
728 if !chunk.is_empty() {
729 segments.push(InterpSegment::Chunk(chunk));
730 }
731 Ok(segments)
732}
733
734fn doc_block_open_at(source: &str, pos: usize) -> Option<usize> {
740 let bytes = source.as_bytes();
741 if !at_line_start(source, pos) {
742 return None;
743 }
744 let mut i = pos;
746 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
747 i += 1;
748 }
749 if i + 3 > bytes.len() {
750 return None;
751 }
752 if &bytes[i..i + 3] != b"---" {
753 return None;
754 }
755 i += 3;
756 while i < bytes.len() && bytes[i] == b'-' {
759 i += 1;
760 }
761 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
763 i += 1;
764 }
765 if i == bytes.len() {
766 return Some(i);
767 }
768 if bytes[i] == b'\n' {
769 return Some(i + 1);
770 }
771 None
772}
773
774fn doc_block_close(source: &str, mut pos: usize) -> Option<(usize, usize)> {
778 let bytes = source.as_bytes();
779 while pos < bytes.len() {
780 let line_start = pos;
782 let mut line_end = line_start;
784 while line_end < bytes.len() && bytes[line_end] != b'\n' {
785 line_end += 1;
786 }
787 if let Some(end) = doc_block_open_at(source, line_start) {
789 return Some((line_start, end));
790 }
791 pos = if line_end < bytes.len() {
793 line_end + 1
794 } else {
795 line_end
796 };
797 }
798 None
799}
800
801fn at_line_start(source: &str, pos: usize) -> bool {
803 if pos == 0 {
804 return true;
805 }
806 let bytes = source.as_bytes();
807 bytes[pos - 1] == b'\n'
808}
809
810pub fn doc_block_content(source: &str, span: Span) -> String {
817 let slice = &source[span.range()];
818 let after_open = match slice.find('\n') {
820 Some(i) => &slice[i + 1..],
821 None => return String::new(),
822 };
823 let bytes = after_open.as_bytes();
824 let mut i = bytes.len();
826 if i > 0 && bytes[i - 1] == b'\n' {
827 i -= 1;
828 }
829 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
830 i -= 1;
831 }
832 while i > 0 && bytes[i - 1] == b'-' {
833 i -= 1;
834 }
835 if i > 0 && bytes[i - 1] == b'\n' {
836 i -= 1;
837 }
838 let body = &after_open[..i];
839
840 let common: Option<usize> = body
844 .lines()
845 .filter(|l| !l.trim().is_empty())
846 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
847 .min();
848 let strip = common.unwrap_or(0);
849 if strip == 0 {
850 return body.to_string();
851 }
852 let mut out = String::with_capacity(body.len());
853 let mut first = true;
854 for line in body.lines() {
855 if !first {
856 out.push('\n');
857 }
858 first = false;
859 if line.trim().is_empty() {
860 continue;
862 }
863 let leading: usize = line
864 .bytes()
865 .take_while(|&b| b == b' ' || b == b'\t')
866 .count();
867 let drop = strip.min(leading);
868 out.push_str(&line[drop..]);
869 }
870 out
871}
872
873pub fn comment_body(source: &str, span: Span) -> &str {
877 let slice = &source[span.range()];
878 slice.strip_prefix("--").unwrap_or(slice)
881}
882
883pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
893 if to <= from {
894 return false;
895 }
896 let bytes = source.as_bytes();
897 let mut i = from;
898 while i < to {
899 if bytes[i] == b'\n' {
900 return true;
901 }
902 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
903 return false;
904 }
905 i += 1;
906 }
907 false
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 fn kinds(source: &str) -> Vec<TokenKind> {
915 tokenize(source)
916 .unwrap()
917 .into_iter()
918 .map(|t| t.kind)
919 .collect()
920 }
921
922 #[test]
923 fn keywords_and_idents() {
924 use TokenKind::*;
925 assert_eq!(
926 kinds("commons type fn where and true false Int String Bool foo bar"),
927 vec![
928 Commons, Type, Fn, Where, And, True, False, Int, String, Bool, Ident, Ident
929 ],
930 );
931 }
932
933 #[test]
934 fn integer_and_string_literals() {
935 use TokenKind::*;
936 assert_eq!(
937 kinds(r#"0 42 "hello" "with\nescape""#),
938 vec![IntLit, IntLit, StrLit, StrLit]
939 );
940 }
941
942 #[test]
943 fn operators() {
944 use TokenKind::*;
945 assert_eq!(
946 kinds("-> == != <= >= && || + - * / ! = < > ( ) { } [ ] , : . @"),
947 vec![
948 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash, Bang,
949 Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot,
950 At,
951 ],
952 );
953 }
954
955 #[test]
956 fn line_comments_emitted_as_trivia() {
957 use TokenKind::*;
960 let src = "-- a comment\ntype X = Int -- trailing\n";
961 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
962 }
963
964 #[test]
965 fn comment_body_extracts_text_after_marker() {
966 let toks = tokenize("-- hello world\n").unwrap();
967 assert_eq!(toks.len(), 1);
968 assert_eq!(toks[0].kind, TokenKind::Comment);
969 assert_eq!(
970 comment_body("-- hello world\n", toks[0].span),
971 " hello world"
972 );
973 }
974
975 #[test]
976 fn comment_does_not_consume_newline() {
977 let toks = tokenize("-- one\n-- two\n").unwrap();
980 assert_eq!(toks.len(), 2);
981 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
982 }
983
984 #[test]
985 fn unterminated_string_is_error() {
986 let err = tokenize("\"oops\n").unwrap_err();
987 assert_eq!(err.category, "bynk.lex.unterminated_string");
988 }
989
990 #[test]
991 fn integer_overflow_is_error() {
992 let err = tokenize("99999999999999999999").unwrap_err();
993 assert_eq!(err.category, "bynk.lex.integer_overflow");
994 }
995
996 #[test]
997 fn unexpected_character_is_error() {
998 let err = tokenize("type X = Int $").unwrap_err();
999 assert_eq!(err.category, "bynk.lex.unexpected_character");
1000 }
1001
1002 #[test]
1003 fn v0_1_keywords() {
1004 use TokenKind::*;
1005 assert_eq!(
1006 kinds("let if else Ok Err Result ValidationError"),
1007 vec![Let, If, Else, Ok, Err, Result, ValidationError],
1008 );
1009 }
1010
1011 #[test]
1012 fn question_token() {
1013 use TokenKind::*;
1014 assert_eq!(kinds("x?"), vec![Ident, Question]);
1015 }
1016
1017 #[test]
1018 fn v0_2_keywords() {
1019 use TokenKind::*;
1020 assert_eq!(
1021 kinds("enum match Option record self Some None is"),
1022 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
1023 );
1024 }
1025
1026 #[test]
1027 fn pipe_and_pipe_pipe_disambiguated() {
1028 use TokenKind::*;
1029 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
1030 }
1031
1032 #[test]
1033 fn v0_7_keywords() {
1034 use TokenKind::*;
1035 assert_eq!(
1036 kinds("assert expect mocks test"),
1037 vec![Assert, Expect, Mocks, Test],
1038 );
1039 }
1040
1041 #[test]
1042 fn fat_arrow_and_underscore() {
1043 use TokenKind::*;
1044 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
1045 }
1046
1047 #[test]
1050 fn interp_string_is_one_token() {
1051 use TokenKind::*;
1052 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
1053 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
1055 }
1056
1057 #[test]
1058 fn interp_balances_nested_parens_and_strings() {
1059 use TokenKind::*;
1060 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
1062 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
1064 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
1066 }
1067
1068 #[test]
1069 fn escaped_open_paren_is_not_a_hole() {
1070 use TokenKind::*;
1071 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
1074 }
1075
1076 #[test]
1077 fn unterminated_hole_is_an_error() {
1078 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
1080 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
1081 }
1082
1083 #[test]
1084 fn unterminated_interp_string_is_an_error() {
1085 let err = tokenize("\"value \\(x) more\n").unwrap_err();
1087 assert_eq!(err.category, "bynk.lex.unterminated_string");
1088 }
1089
1090 #[test]
1091 fn bad_escape_in_interp_string_is_an_error() {
1092 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
1093 assert_eq!(err.category, "bynk.lex.bad_escape");
1094 }
1095}