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("let")]
49 Let,
50 #[token("if")]
51 If,
52 #[token("else")]
53 Else,
54 #[token("Ok")]
55 Ok,
56 #[token("Err")]
57 Err,
58 #[token("Result")]
59 Result,
60 #[token("ValidationError")]
61 ValidationError,
62 #[token("JsonError")]
64 JsonError,
65 #[token("enum")]
67 Enum,
68 #[token("match")]
69 Match,
70 #[token("Option")]
71 Option,
72 #[token("record")]
73 Record,
74 #[token("self")]
75 Self_,
76 #[token("Some")]
77 Some,
78 #[token("None")]
79 None,
80 #[token("is")]
81 Is,
82 #[token("opaque")]
84 Opaque,
85 #[token("uses")]
86 Uses,
87 #[token("context")]
89 Context,
90 #[token("consumes")]
91 Consumes,
92 #[token("exports")]
93 Exports,
94 #[token("transparent")]
95 Transparent,
96 #[token("as")]
98 As,
99 #[token("assert")]
101 Assert,
102 #[token("expect")]
103 Expect,
104 #[token("mocks")]
105 Mocks,
106 #[token("test")]
107 Test,
108 #[token("wires")]
110 Wires,
111 #[token("adapter")]
113 Adapter,
114 #[token("binding")]
115 Binding,
116 #[token("agent")]
118 Agent,
119 #[token("capability")]
120 Capability,
121 #[token("commit")]
122 Commit,
123 #[token("Effect")]
124 Effect,
125 #[token("given")]
126 Given,
127 #[token("on")]
128 On,
129 #[token("http")]
131 Http,
132 #[token("cron")]
134 Cron,
135 #[token("queue")]
137 Queue,
138 #[token("from")]
141 From,
142 #[token("protocol")]
143 Protocol,
144 #[token("provides")]
145 Provides,
146 #[token("service")]
147 Service,
148 #[token("state")]
149 State,
150 #[token("actor")]
153 Actor,
154 #[token("by")]
155 By,
156 #[token("...")]
158 DotDotDot,
159 #[token("<-")]
161 LArrow,
162 #[token("~>")]
166 TildeArrow,
167
168 DocBlock,
173
174 Comment,
181
182 #[regex(r"[A-Za-z][A-Za-z0-9_]*")]
184 Ident,
185
186 #[regex(r"[0-9]+")]
188 IntLit,
189 #[regex(r"[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+")]
194 FloatLit,
195 #[regex(r#""([^"\\\n]|\\[nt"\\])*""#)]
199 StrLit,
200 InterpStr,
205
206 #[token("->")]
208 Arrow,
209 #[token("==")]
210 EqEq,
211 #[token("!=")]
212 BangEq,
213 #[token("<=")]
214 LtEq,
215 #[token(">=")]
216 GtEq,
217 #[token("&&")]
218 AmpAmp,
219 #[token("||")]
220 PipePipe,
221
222 #[token("+")]
224 Plus,
225 #[token("-")]
226 Minus,
227 #[token("*")]
228 Star,
229 #[token("/")]
230 Slash,
231 #[token("!")]
232 Bang,
233 #[token("=")]
234 Eq,
235 #[token("<")]
236 Lt,
237 #[token(">")]
238 Gt,
239 #[token("?")]
241 Question,
242 #[token("=>")]
244 FatArrow,
245 #[token("_")]
248 Underscore,
249 #[token("|")]
252 Pipe,
253
254 #[token("(")]
256 LParen,
257 #[token(")")]
258 RParen,
259 #[token("{")]
260 LBrace,
261 #[token("}")]
262 RBrace,
263 #[token("[")]
264 LBracket,
265 #[token("]")]
266 RBracket,
267 #[token(",")]
268 Comma,
269 #[token(":")]
270 Colon,
271 #[token(".")]
272 Dot,
273}
274
275impl TokenKind {
276 pub fn describe(self) -> &'static str {
278 use TokenKind::*;
279 match self {
280 Commons => "`commons`",
281 Type => "`type`",
282 Fn => "`fn`",
283 Where => "`where`",
284 And => "`and`",
285 True => "`true`",
286 False => "`false`",
287 Int => "`Int`",
288 String => "`String`",
289 Bool => "`Bool`",
290 Float => "`Float`",
291 Let => "`let`",
292 If => "`if`",
293 Else => "`else`",
294 Ok => "`Ok`",
295 Err => "`Err`",
296 Result => "`Result`",
297 ValidationError => "`ValidationError`",
298 JsonError => "`JsonError`",
299 Enum => "`enum`",
300 Match => "`match`",
301 Option => "`Option`",
302 Record => "`record`",
303 Self_ => "`self`",
304 Some => "`Some`",
305 None => "`None`",
306 Is => "`is`",
307 Opaque => "`opaque`",
308 Uses => "`uses`",
309 Context => "`context`",
310 Consumes => "`consumes`",
311 Exports => "`exports`",
312 Transparent => "`transparent`",
313 As => "`as`",
314 Assert => "`assert`",
315 Expect => "`expect`",
316 Mocks => "`mocks`",
317 Test => "`test`",
318 Wires => "`wires`",
319 Adapter => "`adapter`",
320 Binding => "`binding`",
321 Agent => "`agent`",
322 Capability => "`capability`",
323 Commit => "`commit`",
324 Effect => "`Effect`",
325 Given => "`given`",
326 On => "`on`",
327 Http => "`http`",
328 Cron => "`cron`",
329 Queue => "`queue`",
330 From => "`from`",
331 Protocol => "`protocol`",
332 Provides => "`provides`",
333 Service => "`service`",
334 State => "`state`",
335 Actor => "`actor`",
336 By => "`by`",
337 DotDotDot => "`...`",
338 LArrow => "`<-`",
339 TildeArrow => "`~>`",
340 DocBlock => "documentation block",
341 Comment => "line comment",
342 Ident => "identifier",
343 IntLit => "integer literal",
344 FloatLit => "float literal",
345 StrLit => "string literal",
346 InterpStr => "interpolated string",
347 Arrow => "`->`",
348 EqEq => "`==`",
349 BangEq => "`!=`",
350 LtEq => "`<=`",
351 GtEq => "`>=`",
352 AmpAmp => "`&&`",
353 PipePipe => "`||`",
354 Plus => "`+`",
355 Minus => "`-`",
356 Star => "`*`",
357 Slash => "`/`",
358 Bang => "`!`",
359 Eq => "`=`",
360 Lt => "`<`",
361 Gt => "`>`",
362 Question => "`?`",
363 FatArrow => "`=>`",
364 Underscore => "`_`",
365 Pipe => "`|`",
366 LParen => "`(`",
367 RParen => "`)`",
368 LBrace => "`{`",
369 RBrace => "`}`",
370 LBracket => "`[`",
371 RBracket => "`]`",
372 Comma => "`,`",
373 Colon => "`:`",
374 Dot => "`.`",
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy)]
381pub struct Token {
382 pub kind: TokenKind,
383 pub span: Span,
384}
385
386pub fn tokenize(source: &str) -> Result<Vec<Token>, CompileError> {
393 let mut tokens = Vec::new();
394 let bytes = source.as_bytes();
395 let mut pos = 0;
396 while pos < bytes.len() {
397 if let Some(open_end) = doc_block_open_at(source, pos) {
401 match doc_block_close(source, open_end) {
403 Some((close_start, close_end)) => {
404 let span = Span::new(pos, close_end);
405 tokens.push(Token {
406 kind: TokenKind::DocBlock,
407 span,
408 });
409 let _ = close_start;
410 pos = close_end;
411 continue;
412 }
413 None => {
414 return Err(CompileError::new(
415 "bynk.lex.unclosed_doc_block",
416 Span::new(pos, open_end),
417 "documentation block opened but never closed",
418 )
419 .with_note(
420 "a doc block must be terminated by another `---` on a line by itself",
421 ));
422 }
423 }
424 }
425 if pos + 1 < bytes.len() && bytes[pos] == b'-' && bytes[pos + 1] == b'-' {
433 let start = pos;
434 while pos < bytes.len() && bytes[pos] != b'\n' {
435 pos += 1;
436 }
437 tokens.push(Token {
438 kind: TokenKind::Comment,
439 span: Span::new(start, pos),
440 });
441 continue;
442 }
443 if matches!(bytes[pos], b' ' | b'\t' | b'\r' | b'\n') {
446 pos += 1;
447 continue;
448 }
449 if bytes[pos] == b'"' && has_interp_hole(bytes, pos) {
455 let end = scan_str(bytes, source, pos)?;
456 tokens.push(Token {
457 kind: TokenKind::InterpStr,
458 span: Span::new(pos, end),
459 });
460 pos = end;
461 continue;
462 }
463 let mut lex = TokenKind::lexer(&source[pos..]);
465 let Some(result) = lex.next() else {
466 let ch = source[pos..].chars().next().unwrap_or('\0');
469 let span = Span::new(pos, pos + ch.len_utf8());
470 return Err(CompileError::new(
471 "bynk.lex.unexpected_character",
472 span,
473 format!("unexpected character `{ch}`"),
474 ));
475 };
476 let local = lex.span();
477 let span: Span = Span::new(pos + local.start, pos + local.end);
478 match result {
479 Ok(kind) => {
480 if kind == TokenKind::IntLit {
481 let slice = &source[span.range()];
482 if slice.parse::<i64>().is_err() {
483 return Err(CompileError::new(
484 "bynk.lex.integer_overflow",
485 span,
486 format!(
487 "integer literal `{slice}` is out of range for a 64-bit signed integer"
488 ),
489 )
490 .with_note("the range is -2^63 to 2^63 - 1"));
491 }
492 }
493 if kind == TokenKind::FloatLit {
494 let slice = &source[span.range()];
495 match slice.parse::<f64>() {
496 Ok(v) if v.is_finite() => {}
497 _ => {
498 return Err(CompileError::new(
499 "bynk.lex.float_literal_overflow",
500 span,
501 format!(
502 "float literal `{slice}` is out of range for a 64-bit float"
503 ),
504 )
505 .with_note(
506 "the literal does not fit a finite IEEE 754 double; \
507 the largest finite value is ~1.8e308",
508 ));
509 }
510 }
511 }
512 tokens.push(Token { kind, span });
513 pos = span.end;
514 }
515 Err(()) => {
516 let slice = &source[span.range()];
517 let ch = slice.chars().next().unwrap_or('\0');
518 let err = if ch == '"' {
519 CompileError::new(
520 "bynk.lex.unterminated_string",
521 span,
522 "unterminated string literal",
523 )
524 .with_note(
525 "string literals must close with `\"` on the same line; \
526 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`",
527 )
528 } else {
529 CompileError::new(
530 "bynk.lex.unexpected_character",
531 span,
532 format!("unexpected character `{ch}`"),
533 )
534 };
535 return Err(err);
536 }
537 }
538 }
539 Ok(tokens)
540}
541
542fn has_interp_hole(bytes: &[u8], start: usize) -> bool {
548 let mut i = start + 1;
549 while i < bytes.len() {
550 match bytes[i] {
551 b'\n' | b'"' => return false,
552 b'\\' => {
553 if bytes.get(i + 1) == Some(&b'(') {
554 return true;
555 }
556 i += 2;
557 }
558 _ => i += 1,
559 }
560 }
561 false
562}
563
564fn scan_str(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
569 debug_assert_eq!(bytes[start], b'"');
570 let mut i = start + 1;
571 loop {
572 if i >= bytes.len() || bytes[i] == b'\n' {
573 return Err(CompileError::new(
574 "bynk.lex.unterminated_string",
575 Span::new(start, i.min(bytes.len())),
576 "unterminated string literal",
577 )
578 .with_note(
579 "string literals must close with `\"` on the same line; \
580 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`, and `\\(…)` interpolation",
581 ));
582 }
583 match bytes[i] {
584 b'"' => return Ok(i + 1),
585 b'\\' => match bytes.get(i + 1) {
586 Some(b'n' | b't' | b'"' | b'\\') => i += 2,
587 Some(b'(') => i = scan_hole(bytes, source, i + 2)?,
588 other => {
589 let shown = other.map(|b| (*b as char).to_string()).unwrap_or_default();
590 return Err(CompileError::new(
591 "bynk.lex.bad_escape",
592 Span::new(i, (i + 2).min(bytes.len())),
593 format!("invalid escape sequence `\\{shown}` in string literal"),
594 )
595 .with_note("supported escapes: \\n \\t \\\" \\\\ \\(…)"));
596 }
597 },
598 _ => i += 1,
601 }
602 }
603}
604
605fn scan_hole(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
610 let mut i = start;
611 let mut depth = 1usize;
612 loop {
613 if i >= bytes.len() || bytes[i] == b'\n' {
614 return Err(CompileError::new(
615 "bynk.lex.unterminated_interpolation",
616 Span::new(start.saturating_sub(2), i.min(bytes.len())),
617 "unterminated interpolation hole",
618 )
619 .with_note(
620 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
621 ));
622 }
623 match bytes[i] {
624 b'(' => {
625 depth += 1;
626 i += 1;
627 }
628 b')' => {
629 depth -= 1;
630 i += 1;
631 if depth == 0 {
632 return Ok(i);
633 }
634 }
635 b'"' => i = scan_str(bytes, source, i)?,
636 _ => i += 1,
637 }
638 }
639}
640
641pub(crate) enum InterpSegment {
646 Chunk(String),
647 Hole(Span),
648}
649
650pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
655 let bytes = source.as_bytes();
656 let inner_end = span.end - 1; let mut segments = Vec::new();
658 let mut chunk = String::new();
659 let mut i = span.start + 1; while i < inner_end {
661 match bytes[i] {
662 b'\\' => match bytes[i + 1] {
663 b'n' => {
664 chunk.push('\n');
665 i += 2;
666 }
667 b't' => {
668 chunk.push('\t');
669 i += 2;
670 }
671 b'"' => {
672 chunk.push('"');
673 i += 2;
674 }
675 b'\\' => {
676 chunk.push('\\');
677 i += 2;
678 }
679 b'(' => {
680 if !chunk.is_empty() {
681 segments.push(InterpSegment::Chunk(std::mem::take(&mut chunk)));
682 }
683 let hole_start = i + 2;
684 let after = scan_hole(bytes, source, hole_start)?;
685 segments.push(InterpSegment::Hole(Span::new(hole_start, after - 1)));
688 i = after;
689 }
690 other => unreachable!("unvalidated escape `\\{}` in InterpStr", other as char),
693 },
694 _ => {
695 let ch = source[i..].chars().next().unwrap();
696 chunk.push(ch);
697 i += ch.len_utf8();
698 }
699 }
700 }
701 if !chunk.is_empty() {
702 segments.push(InterpSegment::Chunk(chunk));
703 }
704 Ok(segments)
705}
706
707fn doc_block_open_at(source: &str, pos: usize) -> Option<usize> {
713 let bytes = source.as_bytes();
714 if !at_line_start(source, pos) {
715 return None;
716 }
717 let mut i = pos;
719 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
720 i += 1;
721 }
722 if i + 3 > bytes.len() {
723 return None;
724 }
725 if &bytes[i..i + 3] != b"---" {
726 return None;
727 }
728 i += 3;
729 while i < bytes.len() && bytes[i] == b'-' {
732 i += 1;
733 }
734 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
736 i += 1;
737 }
738 if i == bytes.len() {
739 return Some(i);
740 }
741 if bytes[i] == b'\n' {
742 return Some(i + 1);
743 }
744 None
745}
746
747fn doc_block_close(source: &str, mut pos: usize) -> Option<(usize, usize)> {
751 let bytes = source.as_bytes();
752 while pos < bytes.len() {
753 let line_start = pos;
755 let mut line_end = line_start;
757 while line_end < bytes.len() && bytes[line_end] != b'\n' {
758 line_end += 1;
759 }
760 if let Some(end) = doc_block_open_at(source, line_start) {
762 return Some((line_start, end));
763 }
764 pos = if line_end < bytes.len() {
766 line_end + 1
767 } else {
768 line_end
769 };
770 }
771 None
772}
773
774fn at_line_start(source: &str, pos: usize) -> bool {
776 if pos == 0 {
777 return true;
778 }
779 let bytes = source.as_bytes();
780 bytes[pos - 1] == b'\n'
781}
782
783pub fn doc_block_content(source: &str, span: Span) -> String {
790 let slice = &source[span.range()];
791 let after_open = match slice.find('\n') {
793 Some(i) => &slice[i + 1..],
794 None => return String::new(),
795 };
796 let bytes = after_open.as_bytes();
797 let mut i = bytes.len();
799 if i > 0 && bytes[i - 1] == b'\n' {
800 i -= 1;
801 }
802 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
803 i -= 1;
804 }
805 while i > 0 && bytes[i - 1] == b'-' {
806 i -= 1;
807 }
808 if i > 0 && bytes[i - 1] == b'\n' {
809 i -= 1;
810 }
811 let body = &after_open[..i];
812
813 let common: Option<usize> = body
817 .lines()
818 .filter(|l| !l.trim().is_empty())
819 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
820 .min();
821 let strip = common.unwrap_or(0);
822 if strip == 0 {
823 return body.to_string();
824 }
825 let mut out = String::with_capacity(body.len());
826 let mut first = true;
827 for line in body.lines() {
828 if !first {
829 out.push('\n');
830 }
831 first = false;
832 if line.trim().is_empty() {
833 continue;
835 }
836 let leading: usize = line
837 .bytes()
838 .take_while(|&b| b == b' ' || b == b'\t')
839 .count();
840 let drop = strip.min(leading);
841 out.push_str(&line[drop..]);
842 }
843 out
844}
845
846pub fn comment_body(source: &str, span: Span) -> &str {
850 let slice = &source[span.range()];
851 slice.strip_prefix("--").unwrap_or(slice)
854}
855
856pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
866 if to <= from {
867 return false;
868 }
869 let bytes = source.as_bytes();
870 let mut i = from;
871 while i < to {
872 if bytes[i] == b'\n' {
873 return true;
874 }
875 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
876 return false;
877 }
878 i += 1;
879 }
880 false
881}
882
883#[cfg(test)]
884mod tests {
885 use super::*;
886
887 fn kinds(source: &str) -> Vec<TokenKind> {
888 tokenize(source)
889 .unwrap()
890 .into_iter()
891 .map(|t| t.kind)
892 .collect()
893 }
894
895 #[test]
896 fn keywords_and_idents() {
897 use TokenKind::*;
898 assert_eq!(
899 kinds("commons type fn where and true false Int String Bool foo bar"),
900 vec![
901 Commons, Type, Fn, Where, And, True, False, Int, String, Bool, Ident, Ident
902 ],
903 );
904 }
905
906 #[test]
907 fn integer_and_string_literals() {
908 use TokenKind::*;
909 assert_eq!(
910 kinds(r#"0 42 "hello" "with\nescape""#),
911 vec![IntLit, IntLit, StrLit, StrLit]
912 );
913 }
914
915 #[test]
916 fn operators() {
917 use TokenKind::*;
918 assert_eq!(
919 kinds("-> == != <= >= && || + - * / ! = < > ( ) { } [ ] , : ."),
920 vec![
921 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash, Bang,
922 Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot,
923 ],
924 );
925 }
926
927 #[test]
928 fn line_comments_emitted_as_trivia() {
929 use TokenKind::*;
932 let src = "-- a comment\ntype X = Int -- trailing\n";
933 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
934 }
935
936 #[test]
937 fn comment_body_extracts_text_after_marker() {
938 let toks = tokenize("-- hello world\n").unwrap();
939 assert_eq!(toks.len(), 1);
940 assert_eq!(toks[0].kind, TokenKind::Comment);
941 assert_eq!(
942 comment_body("-- hello world\n", toks[0].span),
943 " hello world"
944 );
945 }
946
947 #[test]
948 fn comment_does_not_consume_newline() {
949 let toks = tokenize("-- one\n-- two\n").unwrap();
952 assert_eq!(toks.len(), 2);
953 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
954 }
955
956 #[test]
957 fn unterminated_string_is_error() {
958 let err = tokenize("\"oops\n").unwrap_err();
959 assert_eq!(err.category, "bynk.lex.unterminated_string");
960 }
961
962 #[test]
963 fn integer_overflow_is_error() {
964 let err = tokenize("99999999999999999999").unwrap_err();
965 assert_eq!(err.category, "bynk.lex.integer_overflow");
966 }
967
968 #[test]
969 fn unexpected_character_is_error() {
970 let err = tokenize("type X = Int $").unwrap_err();
971 assert_eq!(err.category, "bynk.lex.unexpected_character");
972 }
973
974 #[test]
975 fn v0_1_keywords() {
976 use TokenKind::*;
977 assert_eq!(
978 kinds("let if else Ok Err Result ValidationError"),
979 vec![Let, If, Else, Ok, Err, Result, ValidationError],
980 );
981 }
982
983 #[test]
984 fn question_token() {
985 use TokenKind::*;
986 assert_eq!(kinds("x?"), vec![Ident, Question]);
987 }
988
989 #[test]
990 fn v0_2_keywords() {
991 use TokenKind::*;
992 assert_eq!(
993 kinds("enum match Option record self Some None is"),
994 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
995 );
996 }
997
998 #[test]
999 fn pipe_and_pipe_pipe_disambiguated() {
1000 use TokenKind::*;
1001 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
1002 }
1003
1004 #[test]
1005 fn v0_7_keywords() {
1006 use TokenKind::*;
1007 assert_eq!(
1008 kinds("assert expect mocks test"),
1009 vec![Assert, Expect, Mocks, Test],
1010 );
1011 }
1012
1013 #[test]
1014 fn fat_arrow_and_underscore() {
1015 use TokenKind::*;
1016 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
1017 }
1018
1019 #[test]
1022 fn interp_string_is_one_token() {
1023 use TokenKind::*;
1024 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
1025 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
1027 }
1028
1029 #[test]
1030 fn interp_balances_nested_parens_and_strings() {
1031 use TokenKind::*;
1032 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
1034 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
1036 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
1038 }
1039
1040 #[test]
1041 fn escaped_open_paren_is_not_a_hole() {
1042 use TokenKind::*;
1043 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
1046 }
1047
1048 #[test]
1049 fn unterminated_hole_is_an_error() {
1050 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
1052 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
1053 }
1054
1055 #[test]
1056 fn unterminated_interp_string_is_an_error() {
1057 let err = tokenize("\"value \\(x) more\n").unwrap_err();
1059 assert_eq!(err.category, "bynk.lex.unterminated_string");
1060 }
1061
1062 #[test]
1063 fn bad_escape_in_interp_string_is_an_error() {
1064 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
1065 assert_eq!(err.category, "bynk.lex.bad_escape");
1066 }
1067}