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
163 DocBlock,
168
169 Comment,
176
177 #[regex(r"[A-Za-z][A-Za-z0-9_]*")]
179 Ident,
180
181 #[regex(r"[0-9]+")]
183 IntLit,
184 #[regex(r"[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+")]
189 FloatLit,
190 #[regex(r#""([^"\\\n]|\\[nt"\\])*""#)]
194 StrLit,
195 InterpStr,
200
201 #[token("->")]
203 Arrow,
204 #[token("==")]
205 EqEq,
206 #[token("!=")]
207 BangEq,
208 #[token("<=")]
209 LtEq,
210 #[token(">=")]
211 GtEq,
212 #[token("&&")]
213 AmpAmp,
214 #[token("||")]
215 PipePipe,
216
217 #[token("+")]
219 Plus,
220 #[token("-")]
221 Minus,
222 #[token("*")]
223 Star,
224 #[token("/")]
225 Slash,
226 #[token("!")]
227 Bang,
228 #[token("=")]
229 Eq,
230 #[token("<")]
231 Lt,
232 #[token(">")]
233 Gt,
234 #[token("?")]
236 Question,
237 #[token("=>")]
239 FatArrow,
240 #[token("_")]
243 Underscore,
244 #[token("|")]
247 Pipe,
248
249 #[token("(")]
251 LParen,
252 #[token(")")]
253 RParen,
254 #[token("{")]
255 LBrace,
256 #[token("}")]
257 RBrace,
258 #[token("[")]
259 LBracket,
260 #[token("]")]
261 RBracket,
262 #[token(",")]
263 Comma,
264 #[token(":")]
265 Colon,
266 #[token(".")]
267 Dot,
268}
269
270impl TokenKind {
271 pub fn describe(self) -> &'static str {
273 use TokenKind::*;
274 match self {
275 Commons => "`commons`",
276 Type => "`type`",
277 Fn => "`fn`",
278 Where => "`where`",
279 And => "`and`",
280 True => "`true`",
281 False => "`false`",
282 Int => "`Int`",
283 String => "`String`",
284 Bool => "`Bool`",
285 Float => "`Float`",
286 Let => "`let`",
287 If => "`if`",
288 Else => "`else`",
289 Ok => "`Ok`",
290 Err => "`Err`",
291 Result => "`Result`",
292 ValidationError => "`ValidationError`",
293 JsonError => "`JsonError`",
294 Enum => "`enum`",
295 Match => "`match`",
296 Option => "`Option`",
297 Record => "`record`",
298 Self_ => "`self`",
299 Some => "`Some`",
300 None => "`None`",
301 Is => "`is`",
302 Opaque => "`opaque`",
303 Uses => "`uses`",
304 Context => "`context`",
305 Consumes => "`consumes`",
306 Exports => "`exports`",
307 Transparent => "`transparent`",
308 As => "`as`",
309 Assert => "`assert`",
310 Expect => "`expect`",
311 Mocks => "`mocks`",
312 Test => "`test`",
313 Wires => "`wires`",
314 Adapter => "`adapter`",
315 Binding => "`binding`",
316 Agent => "`agent`",
317 Capability => "`capability`",
318 Commit => "`commit`",
319 Effect => "`Effect`",
320 Given => "`given`",
321 On => "`on`",
322 Http => "`http`",
323 Cron => "`cron`",
324 Queue => "`queue`",
325 From => "`from`",
326 Protocol => "`protocol`",
327 Provides => "`provides`",
328 Service => "`service`",
329 State => "`state`",
330 Actor => "`actor`",
331 By => "`by`",
332 DotDotDot => "`...`",
333 LArrow => "`<-`",
334 DocBlock => "documentation block",
335 Comment => "line comment",
336 Ident => "identifier",
337 IntLit => "integer literal",
338 FloatLit => "float literal",
339 StrLit => "string literal",
340 InterpStr => "interpolated string",
341 Arrow => "`->`",
342 EqEq => "`==`",
343 BangEq => "`!=`",
344 LtEq => "`<=`",
345 GtEq => "`>=`",
346 AmpAmp => "`&&`",
347 PipePipe => "`||`",
348 Plus => "`+`",
349 Minus => "`-`",
350 Star => "`*`",
351 Slash => "`/`",
352 Bang => "`!`",
353 Eq => "`=`",
354 Lt => "`<`",
355 Gt => "`>`",
356 Question => "`?`",
357 FatArrow => "`=>`",
358 Underscore => "`_`",
359 Pipe => "`|`",
360 LParen => "`(`",
361 RParen => "`)`",
362 LBrace => "`{`",
363 RBrace => "`}`",
364 LBracket => "`[`",
365 RBracket => "`]`",
366 Comma => "`,`",
367 Colon => "`:`",
368 Dot => "`.`",
369 }
370 }
371}
372
373#[derive(Debug, Clone, Copy)]
375pub struct Token {
376 pub kind: TokenKind,
377 pub span: Span,
378}
379
380pub fn tokenize(source: &str) -> Result<Vec<Token>, CompileError> {
387 let mut tokens = Vec::new();
388 let bytes = source.as_bytes();
389 let mut pos = 0;
390 while pos < bytes.len() {
391 if let Some(open_end) = doc_block_open_at(source, pos) {
395 match doc_block_close(source, open_end) {
397 Some((close_start, close_end)) => {
398 let span = Span::new(pos, close_end);
399 tokens.push(Token {
400 kind: TokenKind::DocBlock,
401 span,
402 });
403 let _ = close_start;
404 pos = close_end;
405 continue;
406 }
407 None => {
408 return Err(CompileError::new(
409 "bynk.lex.unclosed_doc_block",
410 Span::new(pos, open_end),
411 "documentation block opened but never closed",
412 )
413 .with_note(
414 "a doc block must be terminated by another `---` on a line by itself",
415 ));
416 }
417 }
418 }
419 if pos + 1 < bytes.len() && bytes[pos] == b'-' && bytes[pos + 1] == b'-' {
427 let start = pos;
428 while pos < bytes.len() && bytes[pos] != b'\n' {
429 pos += 1;
430 }
431 tokens.push(Token {
432 kind: TokenKind::Comment,
433 span: Span::new(start, pos),
434 });
435 continue;
436 }
437 if matches!(bytes[pos], b' ' | b'\t' | b'\r' | b'\n') {
440 pos += 1;
441 continue;
442 }
443 if bytes[pos] == b'"' && has_interp_hole(bytes, pos) {
449 let end = scan_str(bytes, source, pos)?;
450 tokens.push(Token {
451 kind: TokenKind::InterpStr,
452 span: Span::new(pos, end),
453 });
454 pos = end;
455 continue;
456 }
457 let mut lex = TokenKind::lexer(&source[pos..]);
459 let Some(result) = lex.next() else {
460 let ch = source[pos..].chars().next().unwrap_or('\0');
463 let span = Span::new(pos, pos + ch.len_utf8());
464 return Err(CompileError::new(
465 "bynk.lex.unexpected_character",
466 span,
467 format!("unexpected character `{ch}`"),
468 ));
469 };
470 let local = lex.span();
471 let span: Span = Span::new(pos + local.start, pos + local.end);
472 match result {
473 Ok(kind) => {
474 if kind == TokenKind::IntLit {
475 let slice = &source[span.range()];
476 if slice.parse::<i64>().is_err() {
477 return Err(CompileError::new(
478 "bynk.lex.integer_overflow",
479 span,
480 format!(
481 "integer literal `{slice}` is out of range for a 64-bit signed integer"
482 ),
483 )
484 .with_note("the range is -2^63 to 2^63 - 1"));
485 }
486 }
487 if kind == TokenKind::FloatLit {
488 let slice = &source[span.range()];
489 match slice.parse::<f64>() {
490 Ok(v) if v.is_finite() => {}
491 _ => {
492 return Err(CompileError::new(
493 "bynk.lex.float_literal_overflow",
494 span,
495 format!(
496 "float literal `{slice}` is out of range for a 64-bit float"
497 ),
498 )
499 .with_note(
500 "the literal does not fit a finite IEEE 754 double; \
501 the largest finite value is ~1.8e308",
502 ));
503 }
504 }
505 }
506 tokens.push(Token { kind, span });
507 pos = span.end;
508 }
509 Err(()) => {
510 let slice = &source[span.range()];
511 let ch = slice.chars().next().unwrap_or('\0');
512 let err = if ch == '"' {
513 CompileError::new(
514 "bynk.lex.unterminated_string",
515 span,
516 "unterminated string literal",
517 )
518 .with_note(
519 "string literals must close with `\"` on the same line; \
520 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`",
521 )
522 } else {
523 CompileError::new(
524 "bynk.lex.unexpected_character",
525 span,
526 format!("unexpected character `{ch}`"),
527 )
528 };
529 return Err(err);
530 }
531 }
532 }
533 Ok(tokens)
534}
535
536fn has_interp_hole(bytes: &[u8], start: usize) -> bool {
542 let mut i = start + 1;
543 while i < bytes.len() {
544 match bytes[i] {
545 b'\n' | b'"' => return false,
546 b'\\' => {
547 if bytes.get(i + 1) == Some(&b'(') {
548 return true;
549 }
550 i += 2;
551 }
552 _ => i += 1,
553 }
554 }
555 false
556}
557
558fn scan_str(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
563 debug_assert_eq!(bytes[start], b'"');
564 let mut i = start + 1;
565 loop {
566 if i >= bytes.len() || bytes[i] == b'\n' {
567 return Err(CompileError::new(
568 "bynk.lex.unterminated_string",
569 Span::new(start, i.min(bytes.len())),
570 "unterminated string literal",
571 )
572 .with_note(
573 "string literals must close with `\"` on the same line; \
574 supported escapes are `\\n`, `\\t`, `\\\"`, `\\\\`, and `\\(…)` interpolation",
575 ));
576 }
577 match bytes[i] {
578 b'"' => return Ok(i + 1),
579 b'\\' => match bytes.get(i + 1) {
580 Some(b'n' | b't' | b'"' | b'\\') => i += 2,
581 Some(b'(') => i = scan_hole(bytes, source, i + 2)?,
582 other => {
583 let shown = other.map(|b| (*b as char).to_string()).unwrap_or_default();
584 return Err(CompileError::new(
585 "bynk.lex.bad_escape",
586 Span::new(i, (i + 2).min(bytes.len())),
587 format!("invalid escape sequence `\\{shown}` in string literal"),
588 )
589 .with_note("supported escapes: \\n \\t \\\" \\\\ \\(…)"));
590 }
591 },
592 _ => i += 1,
595 }
596 }
597}
598
599fn scan_hole(bytes: &[u8], source: &str, start: usize) -> Result<usize, CompileError> {
604 let mut i = start;
605 let mut depth = 1usize;
606 loop {
607 if i >= bytes.len() || bytes[i] == b'\n' {
608 return Err(CompileError::new(
609 "bynk.lex.unterminated_interpolation",
610 Span::new(start.saturating_sub(2), i.min(bytes.len())),
611 "unterminated interpolation hole",
612 )
613 .with_note(
614 "an interpolation hole `\\(…)` must close with a matching `)` on the same line",
615 ));
616 }
617 match bytes[i] {
618 b'(' => {
619 depth += 1;
620 i += 1;
621 }
622 b')' => {
623 depth -= 1;
624 i += 1;
625 if depth == 0 {
626 return Ok(i);
627 }
628 }
629 b'"' => i = scan_str(bytes, source, i)?,
630 _ => i += 1,
631 }
632 }
633}
634
635pub(crate) enum InterpSegment {
640 Chunk(String),
641 Hole(Span),
642}
643
644pub(crate) fn split_interp(source: &str, span: Span) -> Result<Vec<InterpSegment>, CompileError> {
649 let bytes = source.as_bytes();
650 let inner_end = span.end - 1; let mut segments = Vec::new();
652 let mut chunk = String::new();
653 let mut i = span.start + 1; while i < inner_end {
655 match bytes[i] {
656 b'\\' => match bytes[i + 1] {
657 b'n' => {
658 chunk.push('\n');
659 i += 2;
660 }
661 b't' => {
662 chunk.push('\t');
663 i += 2;
664 }
665 b'"' => {
666 chunk.push('"');
667 i += 2;
668 }
669 b'\\' => {
670 chunk.push('\\');
671 i += 2;
672 }
673 b'(' => {
674 if !chunk.is_empty() {
675 segments.push(InterpSegment::Chunk(std::mem::take(&mut chunk)));
676 }
677 let hole_start = i + 2;
678 let after = scan_hole(bytes, source, hole_start)?;
679 segments.push(InterpSegment::Hole(Span::new(hole_start, after - 1)));
682 i = after;
683 }
684 other => unreachable!("unvalidated escape `\\{}` in InterpStr", other as char),
687 },
688 _ => {
689 let ch = source[i..].chars().next().unwrap();
690 chunk.push(ch);
691 i += ch.len_utf8();
692 }
693 }
694 }
695 if !chunk.is_empty() {
696 segments.push(InterpSegment::Chunk(chunk));
697 }
698 Ok(segments)
699}
700
701fn doc_block_open_at(source: &str, pos: usize) -> Option<usize> {
707 let bytes = source.as_bytes();
708 if !at_line_start(source, pos) {
709 return None;
710 }
711 let mut i = pos;
713 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
714 i += 1;
715 }
716 if i + 3 > bytes.len() {
717 return None;
718 }
719 if &bytes[i..i + 3] != b"---" {
720 return None;
721 }
722 i += 3;
723 while i < bytes.len() && bytes[i] == b'-' {
726 i += 1;
727 }
728 while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\r') {
730 i += 1;
731 }
732 if i == bytes.len() {
733 return Some(i);
734 }
735 if bytes[i] == b'\n' {
736 return Some(i + 1);
737 }
738 None
739}
740
741fn doc_block_close(source: &str, mut pos: usize) -> Option<(usize, usize)> {
745 let bytes = source.as_bytes();
746 while pos < bytes.len() {
747 let line_start = pos;
749 let mut line_end = line_start;
751 while line_end < bytes.len() && bytes[line_end] != b'\n' {
752 line_end += 1;
753 }
754 if let Some(end) = doc_block_open_at(source, line_start) {
756 return Some((line_start, end));
757 }
758 pos = if line_end < bytes.len() {
760 line_end + 1
761 } else {
762 line_end
763 };
764 }
765 None
766}
767
768fn at_line_start(source: &str, pos: usize) -> bool {
770 if pos == 0 {
771 return true;
772 }
773 let bytes = source.as_bytes();
774 bytes[pos - 1] == b'\n'
775}
776
777pub fn doc_block_content(source: &str, span: Span) -> String {
784 let slice = &source[span.range()];
785 let after_open = match slice.find('\n') {
787 Some(i) => &slice[i + 1..],
788 None => return String::new(),
789 };
790 let bytes = after_open.as_bytes();
791 let mut i = bytes.len();
793 if i > 0 && bytes[i - 1] == b'\n' {
794 i -= 1;
795 }
796 while i > 0 && matches!(bytes[i - 1], b' ' | b'\t' | b'\r') {
797 i -= 1;
798 }
799 while i > 0 && bytes[i - 1] == b'-' {
800 i -= 1;
801 }
802 if i > 0 && bytes[i - 1] == b'\n' {
803 i -= 1;
804 }
805 let body = &after_open[..i];
806
807 let common: Option<usize> = body
811 .lines()
812 .filter(|l| !l.trim().is_empty())
813 .map(|l| l.bytes().take_while(|&b| b == b' ' || b == b'\t').count())
814 .min();
815 let strip = common.unwrap_or(0);
816 if strip == 0 {
817 return body.to_string();
818 }
819 let mut out = String::with_capacity(body.len());
820 let mut first = true;
821 for line in body.lines() {
822 if !first {
823 out.push('\n');
824 }
825 first = false;
826 if line.trim().is_empty() {
827 continue;
829 }
830 let leading: usize = line
831 .bytes()
832 .take_while(|&b| b == b' ' || b == b'\t')
833 .count();
834 let drop = strip.min(leading);
835 out.push_str(&line[drop..]);
836 }
837 out
838}
839
840pub fn comment_body(source: &str, span: Span) -> &str {
844 let slice = &source[span.range()];
845 slice.strip_prefix("--").unwrap_or(slice)
848}
849
850pub fn has_blank_line_between(source: &str, from: usize, to: usize) -> bool {
860 if to <= from {
861 return false;
862 }
863 let bytes = source.as_bytes();
864 let mut i = from;
865 while i < to {
866 if bytes[i] == b'\n' {
867 return true;
868 }
869 if !matches!(bytes[i], b' ' | b'\t' | b'\r') {
870 return false;
871 }
872 i += 1;
873 }
874 false
875}
876
877#[cfg(test)]
878mod tests {
879 use super::*;
880
881 fn kinds(source: &str) -> Vec<TokenKind> {
882 tokenize(source)
883 .unwrap()
884 .into_iter()
885 .map(|t| t.kind)
886 .collect()
887 }
888
889 #[test]
890 fn keywords_and_idents() {
891 use TokenKind::*;
892 assert_eq!(
893 kinds("commons type fn where and true false Int String Bool foo bar"),
894 vec![
895 Commons, Type, Fn, Where, And, True, False, Int, String, Bool, Ident, Ident
896 ],
897 );
898 }
899
900 #[test]
901 fn integer_and_string_literals() {
902 use TokenKind::*;
903 assert_eq!(
904 kinds(r#"0 42 "hello" "with\nescape""#),
905 vec![IntLit, IntLit, StrLit, StrLit]
906 );
907 }
908
909 #[test]
910 fn operators() {
911 use TokenKind::*;
912 assert_eq!(
913 kinds("-> == != <= >= && || + - * / ! = < > ( ) { } [ ] , : ."),
914 vec![
915 Arrow, EqEq, BangEq, LtEq, GtEq, AmpAmp, PipePipe, Plus, Minus, Star, Slash, Bang,
916 Eq, Lt, Gt, LParen, RParen, LBrace, RBrace, LBracket, RBracket, Comma, Colon, Dot,
917 ],
918 );
919 }
920
921 #[test]
922 fn line_comments_emitted_as_trivia() {
923 use TokenKind::*;
926 let src = "-- a comment\ntype X = Int -- trailing\n";
927 assert_eq!(kinds(src), vec![Comment, Type, Ident, Eq, Int, Comment],);
928 }
929
930 #[test]
931 fn comment_body_extracts_text_after_marker() {
932 let toks = tokenize("-- hello world\n").unwrap();
933 assert_eq!(toks.len(), 1);
934 assert_eq!(toks[0].kind, TokenKind::Comment);
935 assert_eq!(
936 comment_body("-- hello world\n", toks[0].span),
937 " hello world"
938 );
939 }
940
941 #[test]
942 fn comment_does_not_consume_newline() {
943 let toks = tokenize("-- one\n-- two\n").unwrap();
946 assert_eq!(toks.len(), 2);
947 assert!(toks.iter().all(|t| t.kind == TokenKind::Comment));
948 }
949
950 #[test]
951 fn unterminated_string_is_error() {
952 let err = tokenize("\"oops\n").unwrap_err();
953 assert_eq!(err.category, "bynk.lex.unterminated_string");
954 }
955
956 #[test]
957 fn integer_overflow_is_error() {
958 let err = tokenize("99999999999999999999").unwrap_err();
959 assert_eq!(err.category, "bynk.lex.integer_overflow");
960 }
961
962 #[test]
963 fn unexpected_character_is_error() {
964 let err = tokenize("type X = Int $").unwrap_err();
965 assert_eq!(err.category, "bynk.lex.unexpected_character");
966 }
967
968 #[test]
969 fn v0_1_keywords() {
970 use TokenKind::*;
971 assert_eq!(
972 kinds("let if else Ok Err Result ValidationError"),
973 vec![Let, If, Else, Ok, Err, Result, ValidationError],
974 );
975 }
976
977 #[test]
978 fn question_token() {
979 use TokenKind::*;
980 assert_eq!(kinds("x?"), vec![Ident, Question]);
981 }
982
983 #[test]
984 fn v0_2_keywords() {
985 use TokenKind::*;
986 assert_eq!(
987 kinds("enum match Option record self Some None is"),
988 vec![Enum, Match, Option, Record, Self_, Some, None, Is],
989 );
990 }
991
992 #[test]
993 fn pipe_and_pipe_pipe_disambiguated() {
994 use TokenKind::*;
995 assert_eq!(kinds("| || |"), vec![Pipe, PipePipe, Pipe]);
996 }
997
998 #[test]
999 fn v0_7_keywords() {
1000 use TokenKind::*;
1001 assert_eq!(
1002 kinds("assert expect mocks test"),
1003 vec![Assert, Expect, Mocks, Test],
1004 );
1005 }
1006
1007 #[test]
1008 fn fat_arrow_and_underscore() {
1009 use TokenKind::*;
1010 assert_eq!(kinds("_ =>"), vec![Underscore, FatArrow]);
1011 }
1012
1013 #[test]
1016 fn interp_string_is_one_token() {
1017 use TokenKind::*;
1018 assert_eq!(kinds(r#""Hello, \(name)!""#), vec![InterpStr]);
1019 assert_eq!(kinds(r#""Hello, world""#), vec![StrLit]);
1021 }
1022
1023 #[test]
1024 fn interp_balances_nested_parens_and_strings() {
1025 use TokenKind::*;
1026 assert_eq!(kinds(r#""= \(f(x))""#), vec![InterpStr]);
1028 assert_eq!(kinds(r#""= \(label(")"))""#), vec![InterpStr]);
1030 assert_eq!(kinds(r#""out \("in \(x)")""#), vec![InterpStr]);
1032 }
1033
1034 #[test]
1035 fn escaped_open_paren_is_not_a_hole() {
1036 use TokenKind::*;
1037 assert_eq!(kinds(r#""a \\(b) c""#), vec![StrLit]);
1040 }
1041
1042 #[test]
1043 fn unterminated_hole_is_an_error() {
1044 let err = tokenize("\"value \\(x + 1\n\"").unwrap_err();
1046 assert_eq!(err.category, "bynk.lex.unterminated_interpolation");
1047 }
1048
1049 #[test]
1050 fn unterminated_interp_string_is_an_error() {
1051 let err = tokenize("\"value \\(x) more\n").unwrap_err();
1053 assert_eq!(err.category, "bynk.lex.unterminated_string");
1054 }
1055
1056 #[test]
1057 fn bad_escape_in_interp_string_is_an_error() {
1058 let err = tokenize(r#""a \q \(x)""#).unwrap_err();
1059 assert_eq!(err.category, "bynk.lex.bad_escape");
1060 }
1061}