1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{Token, TokenKind, comment_body, doc_block_content, has_blank_line_between};
11use crate::span::Span;
12mod declarations;
13mod expressions;
14mod statements;
15mod types;
16
17#[derive(Debug, Default)]
26struct TriviaTable {
27 leading: Vec<Vec<String>>,
31 trailing: Vec<Option<String>>,
35 epilogue: Vec<String>,
38}
39
40impl TriviaTable {
41 fn take_leading(&mut self, index: usize) -> Vec<String> {
42 match self.leading.get_mut(index) {
43 Some(v) => std::mem::take(v),
44 None => Vec::new(),
45 }
46 }
47
48 fn take_trailing(&mut self, index: usize) -> Option<String> {
49 self.trailing.get_mut(index).and_then(|s| s.take())
50 }
51
52 fn take_epilogue(&mut self) -> Vec<String> {
53 std::mem::take(&mut self.epilogue)
54 }
55}
56
57fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
63 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
64 let mut table = TriviaTable::default();
65 let mut pending_leading: Vec<String> = Vec::new();
66 let mut last_content_end: Option<usize> = None;
67 for tok in tokens {
68 if tok.kind == TokenKind::Comment {
69 let body = comment_body(source, tok.span).to_string();
70 if pending_leading.is_empty()
74 && let Some(prev_end) = last_content_end
75 && !source[prev_end..tok.span.start].contains('\n')
76 {
77 let last_idx = filtered.len() - 1;
78 if table.trailing[last_idx].is_none() {
81 table.trailing[last_idx] = Some(body);
82 continue;
83 }
84 }
85 pending_leading.push(body);
86 continue;
87 }
88 filtered.push(*tok);
89 table.leading.push(std::mem::take(&mut pending_leading));
90 table.trailing.push(None);
91 last_content_end = Some(tok.span.end);
92 }
93 table.epilogue = pending_leading;
94 (filtered, table)
95}
96
97pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
103 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
104}
105
106pub fn parse_with_warnings(
109 tokens: &[Token],
110 source: &str,
111) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
112 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
113 match unit {
114 SourceUnit::Commons(c) => Ok((c, warnings)),
115 SourceUnit::Context(ctx) => Err(vec![
116 CompileError::new(
117 "bynk.parse.unexpected_context",
118 ctx.span,
119 "expected a `commons` declaration but found a `context` declaration",
120 )
121 .with_note(
122 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
123 ),
124 ]),
125 SourceUnit::Suite(t) => Err(vec![
126 CompileError::new(
127 "bynk.parse.unexpected_suite",
128 t.span,
129 "expected a `commons` declaration but found a `suite` declaration",
130 )
131 .with_note(
132 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
133 ),
134 ]),
135 SourceUnit::Adapter(a) => Err(vec![
136 CompileError::new(
137 "bynk.parse.unexpected_adapter",
138 a.span,
139 "expected a `commons` declaration but found an `adapter` declaration",
140 )
141 .with_note(
142 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
143 ),
144 ]),
145 }
146}
147
148pub fn parse_unit_with_recovery(
157 tokens: &[Token],
158 source: &str,
159) -> (Option<SourceUnit>, Vec<CompileError>) {
160 let (filtered, trivia) = split_trivia(tokens, source);
161 let mut warnings = Vec::new();
162 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
163 p.recover_mode = true;
164 let unit_opt = match p.parse_unit() {
165 Ok(u) => {
166 while p.peek().is_some() {
172 match p.parse_unit() {
173 Ok(_) => {}
174 Err(e) => {
175 p.recovered_errors.push(e);
176 break;
177 }
178 }
179 }
180 Some(u)
181 }
182 Err(e) => {
183 p.recovered_errors.push(e);
184 None
185 }
186 };
187 let mut all_errors = p.recovered_errors;
188 all_errors.append(&mut warnings);
189 (unit_opt, all_errors)
190}
191
192pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
196 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
197}
198
199pub fn parse_unit_with_warnings(
202 tokens: &[Token],
203 source: &str,
204) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
205 let (filtered, trivia) = split_trivia(tokens, source);
206 let mut warnings = Vec::new();
207 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
208 let result = match p.parse_unit() {
209 Ok(u) => {
210 if let Some(extra) = p.peek() {
211 Err(vec![
212 CompileError::new(
213 "bynk.parse.extra_tokens",
214 extra.span,
215 "unexpected token after top-level declaration",
216 )
217 .with_note(
218 "a `.bynk` file contains exactly one `commons` or `context` declaration",
219 ),
220 ])
221 } else {
222 Ok(u)
223 }
224 }
225 Err(e) => Err(vec![e]),
226 };
227 match result {
230 Ok(u) => Ok((u, warnings)),
231 Err(mut errs) => {
232 errs.append(&mut warnings);
233 Err(errs)
234 }
235 }
236}
237
238pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
247 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
248}
249
250pub fn parse_units_with_warnings(
256 tokens: &[Token],
257 source: &str,
258) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
259 let (filtered, trivia) = split_trivia(tokens, source);
260 let mut warnings = Vec::new();
261 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
262 let mut units = Vec::new();
263 let mut errors: Vec<CompileError> = Vec::new();
264 while p.peek().is_some() {
265 match p.parse_unit() {
266 Ok(u) => units.push(u),
267 Err(e) => {
268 errors.push(e);
269 break;
270 }
271 }
272 }
273 let eof = p.eof_span();
274 if !errors.is_empty() {
277 errors.append(&mut warnings);
278 return Err(errors);
279 }
280 if units.is_empty() {
281 return Err(vec![CompileError::new(
282 "bynk.parse.unexpected_eof",
283 eof,
284 "expected `commons`, `context`, or `suite` to start the file, found end of file",
285 )]);
286 }
287 Ok((units, warnings))
288}
289
290enum SignedNumLit {
293 Int(IntBound),
294 Float(FloatBound),
295}
296
297struct Parser<'a> {
298 tokens: &'a [Token],
299 source: &'a str,
300 pos: usize,
301 warnings: &'a mut Vec<CompileError>,
304 recover_mode: bool,
310 recovered_errors: Vec<CompileError>,
313 trivia: TriviaTable,
316 depth: usize,
323 no_record_literal: bool,
332}
333
334impl<'a> Parser<'a> {
335 fn new(
336 tokens: &'a [Token],
337 source: &'a str,
338 trivia: TriviaTable,
339 warnings: &'a mut Vec<CompileError>,
340 ) -> Self {
341 Self {
342 tokens,
343 source,
344 pos: 0,
345 warnings,
346 recover_mode: false,
347 recovered_errors: Vec::new(),
348 trivia,
349 depth: 0,
350 no_record_literal: false,
351 }
352 }
353
354 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
362 self.depth += 1;
363 if self.depth > crate::MAX_NESTING_DEPTH {
364 self.depth -= 1;
365 let span = self
366 .peek()
367 .map(|t| t.span)
368 .unwrap_or_else(|| self.eof_span());
369 return Err(self.nesting_too_deep(span, what));
370 }
371 Ok(())
372 }
373
374 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
377 CompileError::new(
378 "bynk.parse.nesting_too_deep",
379 span,
380 format!(
381 "{what} nests more than {} levels deep",
382 crate::MAX_NESTING_DEPTH
383 ),
384 )
385 .with_note(
386 "deeply nested source is rejected to keep the parser from overflowing its \
387 stack and aborting; flatten or split the construct",
388 )
389 }
390
391 fn expression_too_long(&self, span: Span) -> CompileError {
397 CompileError::new(
398 "bynk.parse.nesting_too_deep",
399 span,
400 format!(
401 "this expression is more than {} levels deep",
402 crate::MAX_NESTING_DEPTH
403 ),
404 )
405 .with_note(
406 "a long operator or member chain is rejected to keep the compiler from overflowing \
407 its stack; split it across `let` bindings, or reduce a sequence with \
408 `.sum()`/`.fold(...)`",
409 )
410 }
411
412 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
432 self.depth += 1;
433 *folds += 1;
434 if self.depth > crate::MAX_NESTING_DEPTH {
435 self.depth -= *folds;
436 *folds = 0;
437 return Err(self.expression_too_long(span));
438 }
439 Ok(())
440 }
441
442 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
451 self.depth += 1;
452 if self.depth > crate::MAX_NESTING_DEPTH {
453 return Err(self.expression_too_long(span));
454 }
455 Ok(())
456 }
457
458 fn take_leading_trivia(&mut self) -> Vec<String> {
462 self.trivia.take_leading(self.pos)
463 }
464
465 fn take_trailing_trivia(&mut self) -> Option<String> {
469 if self.pos == 0 {
470 return None;
471 }
472 self.trivia.take_trailing(self.pos - 1)
473 }
474
475 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
479 if self.recover_mode {
480 self.recovered_errors.push(e);
481 let before = self.pos;
482 self.recover_to_top_item();
483 if self.pos == before {
490 self.bump();
491 }
492 Ok(())
493 } else {
494 Err(e)
495 }
496 }
497
498 fn recover_to_top_item(&mut self) {
503 while let Some(t) = self.peek() {
504 match t.kind {
505 TokenKind::Type
506 | TokenKind::Fn
507 | TokenKind::Uses
508 | TokenKind::Consumes
509 | TokenKind::Exports
510 | TokenKind::Capability
511 | TokenKind::Provides
512 | TokenKind::Stub
513 | TokenKind::Service
514 | TokenKind::Agent
515 | TokenKind::Suite
516 | TokenKind::Case
517 | TokenKind::RBrace
518 | TokenKind::Commons
519 | TokenKind::Context => return,
520 _ => {
521 self.bump();
522 }
523 }
524 }
525 }
526
527 fn peek(&self) -> Option<Token> {
528 self.tokens.get(self.pos).copied()
529 }
530
531 fn peek_kind(&self) -> Option<TokenKind> {
532 self.peek().map(|t| t.kind)
533 }
534
535 fn nth(&self, n: usize) -> Option<Token> {
537 self.tokens.get(self.pos + n).copied()
538 }
539
540 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
541 self.nth(n).map(|t| t.kind)
542 }
543
544 fn nth_text(&self, n: usize) -> &'a str {
546 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
547 }
548
549 fn prev_span(&self) -> Span {
552 self.tokens
553 .get(self.pos.wrapping_sub(1))
554 .or_else(|| self.peek_ref())
555 .map(|t| t.span)
556 .unwrap_or_default()
557 }
558
559 fn peek_ref(&self) -> Option<&Token> {
560 self.tokens.get(self.pos)
561 }
562
563 fn bump(&mut self) -> Option<Token> {
564 let t = self.peek();
565 if t.is_some() {
566 self.pos += 1;
567 }
568 t
569 }
570
571 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
572 if self.peek_kind() == Some(kind) {
573 self.bump()
574 } else {
575 None
576 }
577 }
578
579 fn slice(&self, span: Span) -> &'a str {
580 &self.source[span.range()]
581 }
582
583 fn next_token_on_new_line(&self, prev: Span) -> bool {
588 match self.peek() {
589 Some(t) if prev.end <= t.span.start => {
590 self.source[prev.end..t.span.start].contains('\n')
591 }
592 _ => false,
593 }
594 }
595
596 fn eof_span(&self) -> Span {
601 let end = self.source.len();
602 let start = (0..end)
603 .rev()
604 .find(|&i| self.source.is_char_boundary(i))
605 .unwrap_or(0);
606 Span::new(start, end)
607 }
608
609 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
610 match self.peek() {
611 Some(t) if t.kind == kind => {
612 self.bump();
613 Ok(t)
614 }
615 Some(t) => Err(CompileError::new(
616 "bynk.parse.expected_token",
617 t.span,
618 format!(
619 "expected {} {ctx}, found {}",
620 kind.describe(),
621 t.kind.describe()
622 ),
623 )),
624 None => Err(CompileError::new(
625 "bynk.parse.unexpected_eof",
626 self.eof_span(),
627 format!("expected {} {ctx}, found end of file", kind.describe()),
628 )),
629 }
630 }
631
632 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
633 match self.peek() {
634 Some(t) if t.kind == TokenKind::Ident => {
635 self.bump();
636 Ok(Ident {
637 name: self.slice(t.span).to_string(),
638 span: t.span,
639 })
640 }
641 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
656 self.bump();
657 Ok(Ident {
658 name: self.slice(t.span).to_string(),
659 span: t.span,
660 })
661 }
662 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
663 "bynk.parse.reserved_keyword",
664 t.span,
665 format!(
666 "expected identifier {ctx}, but `{}` is a reserved keyword",
667 self.slice(t.span)
668 ),
669 )
670 .with_note("rename the identifier to something that is not a keyword")),
671 Some(t) => Err(CompileError::new(
672 "bynk.parse.expected_token",
673 t.span,
674 format!("expected identifier {ctx}, found {}", t.kind.describe()),
675 )),
676 None => Err(CompileError::new(
677 "bynk.parse.unexpected_eof",
678 self.eof_span(),
679 format!("expected identifier {ctx}, found end of file"),
680 )),
681 }
682 }
683
684 fn take_doc_block(&mut self) -> Option<(String, Span)> {
690 if self.peek_kind() == Some(TokenKind::DocBlock) {
691 let t = self.bump().unwrap();
692 let body = doc_block_content(self.source, t.span);
693 return Some((body, t.span));
694 }
695 None
696 }
697
698 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
703 let mut leading = self.take_leading_trivia();
704 let doc = self.take_doc_block();
705 if doc.is_some() {
706 leading.extend(self.take_leading_trivia());
707 }
708 (leading, doc)
709 }
710
711 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
714 let (content, doc_span) = doc?;
715 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
717 self.warnings.push(
718 CompileError::new(
719 "bynk.parse.orphan_doc_block",
720 doc_span,
721 "documentation block is separated from the following declaration by a blank line; it will not be attached",
722 )
723 .with_note(
724 "remove the blank line to attach the doc to the next declaration, \
725 or remove the doc block if it is not meant to document anything",
726 ),
727 );
728 return None;
729 }
730 Some(content)
731 }
732}
733
734fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
737 let bytes = lexeme.as_bytes();
738 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
739 let inner = &lexeme[1..lexeme.len() - 1];
740 let mut out = String::with_capacity(inner.len());
741 let mut chars = inner.chars();
742 while let Some(c) = chars.next() {
743 if c == '\\' {
744 match chars.next() {
745 Some('n') => out.push('\n'),
746 Some('t') => out.push('\t'),
747 Some('"') => out.push('"'),
748 Some('\\') => out.push('\\'),
749 other => {
750 return Err(CompileError::new(
751 "bynk.lex.bad_escape",
752 span,
753 format!(
754 "invalid escape sequence `\\{}` in string literal",
755 other.map(|c| c.to_string()).unwrap_or_default()
756 ),
757 )
758 .with_note("supported escapes: \\n \\t \\\" \\\\"));
759 }
760 }
761 } else {
762 out.push(c);
763 }
764 }
765 Ok(out)
766}
767
768fn is_reserved_keyword(kind: TokenKind) -> bool {
769 use TokenKind::*;
770 matches!(
771 kind,
772 Commons
773 | Type
774 | Fn
775 | Where
776 | True
777 | False
778 | Int
779 | String
780 | Bool
781 | Let
782 | If
783 | Else
784 | Ok
785 | Err
786 | Result
787 | ValidationError
788 | Enum
789 | Match
790 | Option
791 | Record
792 | Self_
793 | Some
794 | None
795 | Is
796 | Opaque
797 | Uses
798 | Context
799 | Consumes
800 | Exports
801 | Transparent
802 | Agent
803 | As
804 | Capability
805 | Effect
806 | Do
807 | Given
808 | On
809 | Http
810 | Provides
811 | Stub
812 | Service
813 | Actor
814 | By
815 | Expect
816 | Suite
817 | Case
818 | Float
819 | Duration
820 | Instant
821 | Bytes
822 | JsonError
823 | Property
824 | Adapter
825 | Binding
826 | Cron
827 | Queue
828 | From
829 | Protocol
830 | Invariant
831 | Implies
832 | Requires
833 | Ensures
834 | Transition
835 )
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use crate::lexer::tokenize;
842
843 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
844 let toks = tokenize(src).map_err(|e| vec![e])?;
845 parse(&toks, src)
846 }
847
848 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
849 let toks = match tokenize(src) {
850 Ok(t) => t,
851 Err(e) => return (None, vec![e]),
852 };
853 parse_unit_with_recovery(&toks, src)
854 }
855
856 #[test]
857 fn eof_span_never_splits_a_multibyte_codepoint() {
858 for src in [
863 "commons x {\n -- ends with an arrow →",
864 "agent A {\n key k: String\n -- note 🦀",
865 "commons y {\n type T = é",
866 ] {
867 let (_unit, errors) = parse_recover_str(src);
868 for e in &errors {
869 assert!(
870 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
871 "span {:?} splits a codepoint in {src:?}",
872 e.span,
873 );
874 }
875 }
876 }
877
878 #[test]
879 fn recovery_skips_garbage_between_decls() {
880 let src = "commons x {\n\
883 type A = Int where NonNegative\n\
884 ??? !!!\n\
885 type B = String where NonEmpty\n\
886 }";
887 let (unit, errors) = parse_recover_str(src);
888 let unit = unit.expect("recovery should produce a partial AST");
889 let SourceUnit::Commons(c) = unit else {
890 panic!("expected commons")
891 };
892 let names: Vec<_> = c
894 .items
895 .iter()
896 .map(|i| match i {
897 CommonsItem::Type(t) => t.name.name.clone(),
898 _ => panic!("expected only types"),
899 })
900 .collect();
901 assert!(
902 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
903 "expected both A and B; got {names:?}",
904 );
905 assert!(!errors.is_empty(), "expected at least one parse error");
906 }
907
908 #[test]
909 fn recovery_handles_bad_first_decl_then_good_second() {
910 let src = "commons x {\n\
912 type A Int where NonNegative\n\
913 type B = String where NonEmpty\n\
914 }";
915 let (unit, errors) = parse_recover_str(src);
916 let unit = unit.expect("recovery should produce a partial AST");
917 let SourceUnit::Commons(c) = unit else {
918 panic!("expected commons")
919 };
920 let names: Vec<_> = c
921 .items
922 .iter()
923 .filter_map(|i| match i {
924 CommonsItem::Type(t) => Some(t.name.name.clone()),
925 _ => None,
926 })
927 .collect();
928 assert!(
929 names.contains(&"B".to_string()),
930 "B should be parsed after A's failure; got {names:?}"
931 );
932 assert!(!errors.is_empty(), "expected at least one parse error");
933 }
934
935 #[test]
936 fn doc_block_attaches_to_type() {
937 let c =
938 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
939 .unwrap();
940 let CommonsItem::Type(t) = &c.items[0] else {
941 panic!()
942 };
943 assert!(t.documentation.is_some());
944 assert!(
945 t.documentation
946 .as_ref()
947 .unwrap()
948 .contains("A descriptive doc.")
949 );
950 }
951
952 #[test]
953 fn interpolated_string_parses_into_parts() {
954 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
956 .unwrap();
957 let CommonsItem::Fn(f) = &c.items[0] else {
958 panic!("expected fn")
959 };
960 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
961 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
962 };
963 assert_eq!(parts.len(), 3);
964 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
965 assert!(
966 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
967 );
968 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
969 }
970
971 #[test]
972 fn interpolated_hole_parses_a_full_expression() {
973 let c =
975 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
976 .unwrap();
977 let CommonsItem::Fn(f) = &c.items[0] else {
978 panic!("expected fn")
979 };
980 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
981 panic!("expected InterpStr")
982 };
983 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
984 }
985
986 #[test]
987 fn empty_interpolation_hole_is_rejected() {
988 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
989 assert!(
990 errs.iter()
991 .any(|e| e.category == "bynk.parse.empty_interpolation"),
992 "expected empty_interpolation; got {errs:?}"
993 );
994 }
995
996 #[test]
997 fn interpolation_hole_lex_error_span_is_rebased() {
998 let cases = [
1004 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1006 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1008 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1011 ];
1012 for src in cases {
1013 let errs = parse_str(src).unwrap_err();
1014 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1015 for e in &errs {
1016 assert!(
1017 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1018 "span {:?} splits a codepoint in {src:?}",
1019 e.span,
1020 );
1021 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1024 assert!(
1025 e.span.start >= hole_start,
1026 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1027 e.span,
1028 );
1029 }
1030 }
1031 }
1032
1033 #[test]
1034 fn fragment_form_parses() {
1035 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1036 assert_eq!(c.form, CommonsForm::Fragment);
1037 assert_eq!(c.items.len(), 1);
1038 }
1039
1040 #[test]
1041 fn uses_parses() {
1042 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1043 assert_eq!(c.uses.len(), 1);
1044 assert_eq!(c.uses[0].target.joined(), "other.lib");
1045 }
1046
1047 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1048 let toks = tokenize(src).map_err(|e| vec![e])?;
1049 parse_unit(&toks, src)
1050 }
1051
1052 #[test]
1053 fn minimal_context_parses() {
1054 let u = parse_unit_str("context commerce.orders {}").unwrap();
1055 let SourceUnit::Context(c) = u else {
1056 panic!("expected context");
1057 };
1058 assert_eq!(c.name.joined(), "commerce.orders");
1059 assert!(c.items.is_empty());
1060 }
1061
1062 #[test]
1063 fn context_consumes_and_exports_parse() {
1064 let src = "context commerce.orders {\n uses commerce.money\n consumes commerce.payment\n exports opaque { OrderId }\n exports transparent { OrderError }\n type OrderId = String where Matches(\"ORD-[0-9]+\")\n type OrderError = enum { CartEmpty, BadInput }\n}";
1065 let u = parse_unit_str(src).unwrap();
1066 let SourceUnit::Context(c) = u else { panic!() };
1067 assert_eq!(c.uses.len(), 1);
1068 assert_eq!(c.consumes.len(), 1);
1069 assert_eq!(c.exports.len(), 2);
1070 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
1071 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
1072 }
1073
1074 #[test]
1075 fn context_fragment_form_parses() {
1076 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
1077 let u = parse_unit_str(src).unwrap();
1078 let SourceUnit::Context(c) = u else { panic!() };
1079 assert_eq!(c.form, CommonsForm::Fragment);
1080 assert_eq!(c.uses.len(), 1);
1081 assert_eq!(c.consumes.len(), 1);
1082 assert_eq!(c.exports.len(), 1);
1083 }
1084
1085 #[test]
1086 fn opaque_type_parses() {
1087 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
1088 let CommonsItem::Type(t) = &c.items[0] else {
1089 panic!()
1090 };
1091 assert!(matches!(t.body, TypeBody::Opaque { .. }));
1092 }
1093
1094 #[test]
1095 fn empty_commons() {
1096 let c = parse_str("commons fitness.units {}").unwrap();
1097 assert_eq!(c.name.joined(), "fitness.units");
1098 assert!(c.items.is_empty());
1099 }
1100
1101 #[test]
1102 fn one_type_decl() {
1103 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
1104 assert_eq!(c.items.len(), 1);
1105 let CommonsItem::Type(t) = &c.items[0] else {
1106 panic!()
1107 };
1108 assert_eq!(t.name.name, "Metres");
1109 match &t.body {
1110 TypeBody::Refined {
1111 base, refinement, ..
1112 } => {
1113 assert_eq!(*base, BaseType::Int);
1114 assert!(refinement.is_some());
1115 }
1116 _ => panic!("expected refined body"),
1117 }
1118 }
1119
1120 #[test]
1121 fn function_decl() {
1122 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
1123 let CommonsItem::Fn(f) = &c.items[0] else {
1124 panic!()
1125 };
1126 assert_eq!(f.name.ident().name, "add");
1127 assert_eq!(f.params.len(), 2);
1128 }
1129
1130 #[test]
1131 fn chained_comparison_is_error() {
1132 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
1133 .unwrap_err();
1134 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1135 }
1136
1137 #[test]
1138 fn chained_equality_is_error() {
1139 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
1140 .unwrap_err();
1141 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1142 }
1143
1144 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1153 std::thread::Builder::new()
1154 .stack_size(64 * 1024 * 1024)
1155 .spawn(f)
1156 .unwrap()
1157 .join()
1158 .unwrap()
1159 }
1160
1161 #[test]
1162 fn deeply_nested_parens_are_bounded_not_overflowed() {
1163 let errs = on_big_stack(|| {
1169 let depth = crate::MAX_NESTING_DEPTH + 8;
1170 let src = format!(
1171 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1172 "(".repeat(depth),
1173 ")".repeat(depth),
1174 );
1175 parse_str(&src).unwrap_err()
1176 });
1177 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1178 }
1179
1180 #[test]
1181 fn deeply_nested_types_are_bounded_not_overflowed() {
1182 let errs = on_big_stack(|| {
1187 let depth = crate::MAX_NESTING_DEPTH + 8;
1188 let src = format!(
1189 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
1190 "Result[Int, ".repeat(depth),
1191 "]".repeat(depth),
1192 );
1193 parse_str(&src).unwrap_err()
1194 });
1195 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1196 }
1197
1198 #[test]
1199 fn deeply_nested_patterns_are_bounded_not_overflowed() {
1200 let errs = on_big_stack(|| {
1205 let depth = crate::MAX_NESTING_DEPTH + 8;
1206 let src = format!(
1207 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
1208 "Ok(".repeat(depth),
1209 ")".repeat(depth),
1210 );
1211 parse_str(&src).unwrap_err()
1212 });
1213 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1214 }
1215
1216 #[test]
1217 fn nesting_below_the_limit_still_parses() {
1218 let ok = on_big_stack(|| {
1221 let depth = crate::MAX_NESTING_DEPTH - 8;
1222 let src = format!(
1223 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1224 "(".repeat(depth),
1225 ")".repeat(depth),
1226 );
1227 parse_str(&src).is_ok()
1228 });
1229 assert!(ok, "well-nested source under the limit should parse");
1230 }
1231
1232 #[test]
1233 fn let_statement_parses() {
1234 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
1235 let CommonsItem::Fn(f) = &c.items[0] else {
1236 panic!()
1237 };
1238 assert_eq!(f.body.statements.len(), 1);
1239 match &f.body.statements[0] {
1240 Statement::Let(l) => {
1241 assert_eq!(l.name.name, "y");
1242 assert!(l.type_annot.is_none());
1243 }
1244 _ => panic!("expected a pure `let` statement"),
1245 }
1246 }
1247
1248 #[test]
1249 fn let_with_annotation() {
1250 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
1251 let CommonsItem::Fn(f) = &c.items[0] else {
1252 panic!()
1253 };
1254 match &f.body.statements[0] {
1255 Statement::Let(l) => assert!(l.type_annot.is_some()),
1256 _ => panic!("expected a pure `let` statement"),
1257 }
1258 }
1259
1260 #[test]
1261 fn if_else_parses_as_expression() {
1262 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1263 let CommonsItem::Fn(f) = &c.items[0] else {
1264 panic!()
1265 };
1266 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1267 }
1268
1269 #[test]
1270 fn else_if_chain_parses() {
1271 let c = parse_str(
1272 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1273 )
1274 .unwrap();
1275 let CommonsItem::Fn(f) = &c.items[0] else {
1276 panic!()
1277 };
1278 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1279 panic!()
1280 };
1281 assert!(else_block.statements.is_empty());
1283 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1284 }
1285
1286 #[test]
1287 fn ok_and_err_parse_as_expressions() {
1288 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1289 let CommonsItem::Fn(f) = &c.items[0] else {
1290 panic!()
1291 };
1292 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1293
1294 let c =
1295 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1296 let CommonsItem::Fn(f) = &c.items[0] else {
1297 panic!()
1298 };
1299 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1300 }
1301
1302 #[test]
1303 fn question_postfix_parses() {
1304 let c = parse_str(
1305 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1306 )
1307 .unwrap();
1308 let CommonsItem::Fn(f) = &c.items[1] else {
1309 panic!()
1310 };
1311 let Statement::Let(l) = &f.body.statements[0] else {
1312 panic!("expected a pure `let` statement");
1313 };
1314 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1315 }
1316
1317 #[test]
1318 fn constructor_call_parses() {
1319 let c = parse_str(
1320 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1321 )
1322 .unwrap();
1323 let CommonsItem::Fn(f) = &c.items[1] else {
1324 panic!()
1325 };
1326 let ExprKind::MethodCall {
1329 receiver, method, ..
1330 } = &f.body.tail.kind
1331 else {
1332 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1333 };
1334 let ExprKind::Ident(id) = &receiver.kind else {
1335 panic!("expected receiver Ident");
1336 };
1337 assert_eq!(id.name, "T");
1338 assert_eq!(method.name, "of");
1339 }
1340
1341 #[test]
1342 fn result_type_ref_parses() {
1343 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1344 let CommonsItem::Fn(f) = &c.items[0] else {
1345 panic!()
1346 };
1347 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1348 }
1349
1350 #[test]
1351 fn result_missing_arg_count_errors() {
1352 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1353 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1354 }
1355
1356 #[test]
1357 fn field_access_parses_in_v0_2() {
1358 let c =
1361 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1362 let CommonsItem::Fn(f) = &c.items[1] else {
1363 panic!()
1364 };
1365 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1366 }
1367
1368 #[test]
1371 fn leading_line_comment_attaches_to_next_decl() {
1372 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1373 let c = parse_str(src).unwrap();
1374 let CommonsItem::Type(t) = &c.items[0] else {
1375 panic!()
1376 };
1377 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1378 assert!(t.trivia.trailing.is_none());
1379 }
1380
1381 #[test]
1382 fn trailing_line_comment_attaches_to_prev_decl() {
1383 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1384 let c = parse_str(src).unwrap();
1385 let CommonsItem::Type(t) = &c.items[0] else {
1386 panic!()
1387 };
1388 assert!(t.trivia.leading.is_empty());
1389 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1390 }
1391
1392 #[test]
1393 fn grouped_leading_comments_attach_together() {
1394 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1395 let c = parse_str(src).unwrap();
1396 let CommonsItem::Type(t) = &c.items[0] else {
1397 panic!()
1398 };
1399 assert_eq!(
1400 t.trivia.leading,
1401 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1402 );
1403 }
1404
1405 #[test]
1406 fn comment_with_doc_block_keeps_both() {
1407 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1409 let c = parse_str(src).unwrap();
1410 let CommonsItem::Type(t) = &c.items[0] else {
1411 panic!()
1412 };
1413 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1414 assert_eq!(t.documentation.as_deref(), Some("docs"));
1415 }
1416
1417 #[test]
1418 fn comment_before_let_statement_attaches() {
1419 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1420 let c = parse_str(src).unwrap();
1421 let CommonsItem::Fn(f) = &c.items[0] else {
1422 panic!()
1423 };
1424 let Statement::Let(l) = &f.body.statements[0] else {
1425 panic!()
1426 };
1427 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1428 }
1429
1430 #[test]
1431 fn comment_before_tail_attaches_to_block_tail() {
1432 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1433 let c = parse_str(src).unwrap();
1434 let CommonsItem::Fn(f) = &c.items[0] else {
1435 panic!()
1436 };
1437 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1438 }
1439
1440 #[test]
1446 fn contextual_keywords_are_valid_identifiers() {
1447 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
1449 .expect("`on`/`suite`/`case` are valid field names");
1450 let CommonsItem::Type(_) = &c.items[0] else {
1451 panic!("expected a type decl")
1452 };
1453
1454 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
1456 .expect("`on`/`case` are valid parameter names");
1457
1458 parse_str("commons demo {\n type R = { suite: Int }\n}")
1460 .expect("`suite` is a valid field name");
1461 }
1462
1463 #[test]
1471 fn is_reserved_keyword_covers_every_lexer_keyword() {
1472 let lexer_src = include_str!("lexer.rs");
1473 let mut words = Vec::new();
1474 for line in lexer_src.lines() {
1475 let t = line.trim();
1476 if let Some(rest) = t.strip_prefix("#[token(\"")
1477 && let Some(word) = rest.split('"').next()
1478 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
1479 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1480 {
1481 words.push(word.to_string());
1482 }
1483 }
1484 assert!(
1485 words.len() > 30,
1486 "keyword extraction looks broken: only {} words",
1487 words.len()
1488 );
1489 use crate::keywords::RESERVED_CONTEXTUAL;
1492 let mut unclassified = Vec::new();
1493 for word in &words {
1494 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
1495 let kind = tokens.first().expect("keyword yields a token").kind;
1496 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
1497 unclassified.push(word.clone());
1498 }
1499 }
1500 assert!(
1501 unclassified.is_empty(),
1502 "keywords missing from is_reserved_keyword (add them, or document \
1503 them as contextual): {unclassified:?}"
1504 );
1505 }
1506
1507 #[test]
1512 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
1513 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
1514 let tokens = crate::lexer::tokenize(src).unwrap();
1515 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
1516 assert!(unit.is_some(), "the commons header still parses");
1517 assert!(
1518 errors
1519 .iter()
1520 .any(|e| e.category == "bynk.capability.outside_context"),
1521 "the misplaced capability is reported: {errors:?}"
1522 );
1523 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
1526 }
1527
1528 #[test]
1529 fn trailing_file_comment_becomes_unit_trailing() {
1530 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1534 let c = parse_str(src).unwrap();
1535 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1536 }
1537
1538 fn body_tail(body: &str) -> ExprKind {
1542 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
1543 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
1544 let CommonsItem::Fn(f) = &c.items[0] else {
1545 panic!("expected fn, got {:?}", c.items[0]);
1546 };
1547 f.body.tail.kind.clone()
1548 }
1549
1550 fn body_err(body: &str) -> Vec<CompileError> {
1551 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
1552 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
1553 }
1554
1555 #[test]
1556 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
1557 for src in [
1560 "if ready { result } else { fallback }",
1561 "if ready { fallback } else { result }",
1562 "if !ready { result } else { fallback }",
1563 "if a == b { result } else { fallback }",
1564 "if a && b { result } else { fallback }",
1565 ] {
1566 let ExprKind::If {
1567 then_block,
1568 else_block,
1569 ..
1570 } = body_tail(src)
1571 else {
1572 panic!("expected If for {src:?}, got {:?}", body_tail(src));
1573 };
1574 assert!(
1577 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
1578 "then-branch tail not an ident for {src:?}: {:?}",
1579 then_block.tail.kind,
1580 );
1581 assert!(
1582 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
1583 "else-branch tail not an ident for {src:?}: {:?}",
1584 else_block.tail.kind,
1585 );
1586 }
1587 }
1588
1589 #[test]
1590 fn else_less_if_with_single_ident_branch_parses() {
1591 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
1593 panic!("expected If");
1594 };
1595 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
1596 }
1597
1598 #[test]
1599 fn record_construction_still_parses_in_value_position() {
1600 assert!(matches!(
1603 body_tail("Point { x }"),
1604 ExprKind::RecordConstruction { .. }
1605 ));
1606 assert!(matches!(
1607 body_tail("Point { x: 1, y: 2 }"),
1608 ExprKind::RecordConstruction { .. }
1609 ));
1610 assert!(matches!(
1611 body_tail("Empty {}"),
1612 ExprKind::RecordConstruction { .. }
1613 ));
1614 }
1615
1616 #[test]
1617 fn parenthesised_record_is_allowed_in_condition_head() {
1618 let ExprKind::If { cond, .. } =
1621 body_tail("if (ready { result }) { branch } else { other }")
1622 else {
1623 panic!("expected If");
1624 };
1625 let ExprKind::Paren(inner) = &cond.kind else {
1626 panic!("expected a parenthesised condition, got {:?}", cond.kind);
1627 };
1628 assert!(
1629 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
1630 "parenthesised record in condition head should still construct: {:?}",
1631 inner.kind,
1632 );
1633 }
1634
1635 #[test]
1636 fn record_in_call_arg_within_condition_still_constructs() {
1637 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
1640 else {
1641 panic!("expected If");
1642 };
1643 let ExprKind::Call { args, .. } = &cond.kind else {
1644 panic!("expected Call in condition, got {:?}", cond.kind);
1645 };
1646 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
1647 }
1648
1649 #[test]
1650 fn safe_condition_shapes_are_unaffected() {
1651 assert!(matches!(
1653 body_tail("if ready == true { result } else { fallback }"),
1654 ExprKind::If { .. }
1655 ));
1656 assert!(matches!(
1657 body_tail("if (ready) { result } else { fallback }"),
1658 ExprKind::If { .. }
1659 ));
1660 assert!(matches!(
1661 body_tail("if ready { \"a\" } else { \"b\" }"),
1662 ExprKind::If { .. }
1663 ));
1664 }
1665
1666 #[test]
1667 fn empty_match_reports_its_own_diagnostic() {
1668 let errs = body_err("match result {}");
1672 assert!(
1673 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
1674 "expected empty_match; got {errs:?}",
1675 );
1676 }
1677
1678 #[test]
1679 fn match_discriminant_ending_in_ident_parses() {
1680 assert!(matches!(
1682 body_tail("match ready { x => x }"),
1683 ExprKind::Match { .. }
1684 ));
1685 }
1686
1687 #[test]
1688 fn unparenthesised_record_in_condition_head_now_errors() {
1689 assert!(
1695 !body_err("match Point { x: 1 } { p => p }").is_empty(),
1696 "unparenthesised record discriminant should not parse",
1697 );
1698 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
1700 else {
1701 panic!("expected Match for the parenthesised form");
1702 };
1703 let ExprKind::Paren(inner) = &discriminant.kind else {
1704 panic!(
1705 "expected a parenthesised discriminant, got {:?}",
1706 discriminant.kind
1707 );
1708 };
1709 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
1710 }
1711}