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 fn is_fully_drained(&self) -> bool {
73 self.leading.iter().all(Vec::is_empty)
74 && self.trailing.iter().all(Option::is_none)
75 && self.epilogue.is_empty()
76 }
77
78 fn epilogue_is_empty(&self) -> bool {
86 self.epilogue.is_empty()
87 }
88}
89
90fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
96 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
97 let mut table = TriviaTable::default();
98 let mut pending_leading: Vec<String> = Vec::new();
99 let mut last_content_end: Option<usize> = None;
100 for tok in tokens {
101 if tok.kind == TokenKind::Comment {
102 let body = comment_body(source, tok.span).to_string();
103 if pending_leading.is_empty()
107 && let Some(prev_end) = last_content_end
108 && !source[prev_end..tok.span.start].contains('\n')
109 {
110 let last_idx = filtered.len() - 1;
111 if table.trailing[last_idx].is_none() {
114 table.trailing[last_idx] = Some(body);
115 continue;
116 }
117 }
118 pending_leading.push(body);
119 continue;
120 }
121 filtered.push(*tok);
122 table.leading.push(std::mem::take(&mut pending_leading));
123 table.trailing.push(None);
124 last_content_end = Some(tok.span.end);
125 }
126 table.epilogue = pending_leading;
127 (filtered, table)
128}
129
130pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
136 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
137}
138
139pub fn parse_with_warnings(
142 tokens: &[Token],
143 source: &str,
144) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
145 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
146 match unit {
147 SourceUnit::Commons(c) => Ok((c, warnings)),
148 SourceUnit::Context(ctx) => Err(vec![
149 CompileError::new(
150 "bynk.parse.unexpected_context",
151 ctx.span,
152 "expected a `commons` declaration but found a `context` declaration",
153 )
154 .with_note(
155 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
156 ),
157 ]),
158 SourceUnit::Suite(t) => Err(vec![
159 CompileError::new(
160 "bynk.parse.unexpected_suite",
161 t.span,
162 "expected a `commons` declaration but found a `suite` declaration",
163 )
164 .with_note(
165 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
166 ),
167 ]),
168 SourceUnit::Adapter(a) => Err(vec![
169 CompileError::new(
170 "bynk.parse.unexpected_adapter",
171 a.span,
172 "expected a `commons` declaration but found an `adapter` declaration",
173 )
174 .with_note(
175 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
176 ),
177 ]),
178 }
179}
180
181pub fn parse_unit_with_recovery(
196 tokens: &[Token],
197 source: &str,
198) -> (Option<SourceUnit>, Vec<CompileError>) {
199 let (units, errors) = parse_units_with_recovery(tokens, source);
200 (units.into_iter().next(), errors)
201}
202
203pub fn parse_units_with_recovery(
208 tokens: &[Token],
209 source: &str,
210) -> (Vec<SourceUnit>, Vec<CompileError>) {
211 let (filtered, trivia) = split_trivia(tokens, source);
212 let mut warnings = Vec::new();
213 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
214 p.recover_mode = true;
215 let mut units = Vec::new();
216 loop {
217 match p.parse_unit() {
218 Ok(u) => units.push(u),
219 Err(e) => {
220 p.recovered_errors.push(e);
221 break;
222 }
223 }
224 if p.peek().is_none() {
230 break;
231 }
232 }
233 let mut all_errors = p.recovered_errors;
234 all_errors.append(&mut warnings);
235 (units, all_errors)
236}
237
238pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
242 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
243}
244
245pub fn parse_unit_with_warnings(
248 tokens: &[Token],
249 source: &str,
250) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
251 let (filtered, trivia) = split_trivia(tokens, source);
252 let mut warnings = Vec::new();
253 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
254 let result = match p.parse_unit() {
255 Ok(u) => {
256 if let Some(extra) = p.peek() {
257 Err(vec![
258 CompileError::new(
259 "bynk.parse.extra_tokens",
260 extra.span,
261 "unexpected token after top-level declaration",
262 )
263 .with_note(
264 "a `.bynk` file contains exactly one `commons` or `context` declaration",
265 ),
266 ])
267 } else {
268 Ok(u)
269 }
270 }
271 Err(e) => Err(vec![e]),
272 };
273 match result {
276 Ok(u) => {
277 debug_assert!(
280 p.trivia.epilogue_is_empty(),
281 "a file-trailing comment was left undrained after a successful parse"
282 );
283 Ok((u, warnings))
284 }
285 Err(mut errs) => {
286 errs.append(&mut warnings);
287 Err(errs)
288 }
289 }
290}
291
292pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
301 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
302}
303
304pub fn parse_units_with_warnings(
310 tokens: &[Token],
311 source: &str,
312) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
313 parse_units_with_drain_check(tokens, source)
314 .map(|(units, warnings, _drained)| (units, warnings))
315}
316
317pub fn parse_units_with_drain_check(
328 tokens: &[Token],
329 source: &str,
330) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
331 let (filtered, trivia) = split_trivia(tokens, source);
332 let mut warnings = Vec::new();
333 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
334 let mut units = Vec::new();
335 let mut errors: Vec<CompileError> = Vec::new();
336 while p.peek().is_some() {
337 match p.parse_unit() {
338 Ok(u) => units.push(u),
339 Err(e) => {
340 errors.push(e);
341 break;
342 }
343 }
344 }
345 let eof = p.eof_span();
346 let fully_drained = p.trivia.is_fully_drained();
347 if !errors.is_empty() {
350 errors.append(&mut warnings);
351 return Err(errors);
352 }
353 if units.is_empty() {
354 return Err(vec![CompileError::new(
355 "bynk.parse.unexpected_eof",
356 eof,
357 "expected `commons`, `context`, or `suite` to start the file, found end of file",
358 )]);
359 }
360 debug_assert!(
368 p.trivia.epilogue_is_empty(),
369 "a file-trailing comment was left undrained after a successful parse"
370 );
371 Ok((units, warnings, fully_drained))
372}
373
374enum SignedNumLit {
377 Int(IntBound),
378 Float(FloatBound),
379}
380
381struct Parser<'a> {
382 tokens: &'a [Token],
383 source: &'a str,
384 pos: usize,
385 warnings: &'a mut Vec<CompileError>,
388 recover_mode: bool,
394 recovered_errors: Vec<CompileError>,
397 trivia: TriviaTable,
400 depth: usize,
407 no_record_literal: bool,
416 brace_depth: usize,
427 item_loop_baseline: Vec<usize>,
434}
435
436impl<'a> Parser<'a> {
437 fn new(
438 tokens: &'a [Token],
439 source: &'a str,
440 trivia: TriviaTable,
441 warnings: &'a mut Vec<CompileError>,
442 ) -> Self {
443 Self {
444 tokens,
445 source,
446 pos: 0,
447 warnings,
448 recover_mode: false,
449 recovered_errors: Vec::new(),
450 trivia,
451 depth: 0,
452 no_record_literal: false,
453 brace_depth: 0,
454 item_loop_baseline: Vec::new(),
455 }
456 }
457
458 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
466 self.depth += 1;
467 if self.depth > crate::MAX_NESTING_DEPTH {
468 self.depth -= 1;
469 let span = self
470 .peek()
471 .map(|t| t.span)
472 .unwrap_or_else(|| self.eof_span());
473 return Err(self.nesting_too_deep(span, what));
474 }
475 Ok(())
476 }
477
478 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
481 CompileError::new(
482 "bynk.parse.nesting_too_deep",
483 span,
484 format!(
485 "{what} nests more than {} levels deep",
486 crate::MAX_NESTING_DEPTH
487 ),
488 )
489 .with_note(
490 "deeply nested source is rejected to keep the parser from overflowing its \
491 stack and aborting; flatten or split the construct",
492 )
493 }
494
495 fn expression_too_long(&self, span: Span) -> CompileError {
501 CompileError::new(
502 "bynk.parse.nesting_too_deep",
503 span,
504 format!(
505 "this expression is more than {} levels deep",
506 crate::MAX_NESTING_DEPTH
507 ),
508 )
509 .with_note(
510 "a long operator or member chain is rejected to keep the compiler from overflowing \
511 its stack; split it across `let` bindings, or reduce a sequence with \
512 `.sum()`/`.fold(...)`",
513 )
514 }
515
516 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
536 self.depth += 1;
537 *folds += 1;
538 if self.depth > crate::MAX_NESTING_DEPTH {
539 self.depth -= *folds;
540 *folds = 0;
541 return Err(self.expression_too_long(span));
542 }
543 Ok(())
544 }
545
546 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
555 self.depth += 1;
556 if self.depth > crate::MAX_NESTING_DEPTH {
557 return Err(self.expression_too_long(span));
558 }
559 Ok(())
560 }
561
562 fn take_leading_trivia(&mut self) -> Vec<String> {
566 self.trivia.take_leading(self.pos)
567 }
568
569 fn take_trailing_trivia(&mut self) -> Option<String> {
573 if self.pos == 0 {
574 return None;
575 }
576 self.trivia.take_trailing(self.pos - 1)
577 }
578
579 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
583 if self.recover_mode {
584 self.recovered_errors.push(e);
585 let before = self.pos;
586 self.recover_to_top_item();
587 if self.pos == before {
594 self.bump();
595 }
596 Ok(())
597 } else {
598 Err(e)
599 }
600 }
601
602 fn recover_to_top_item(&mut self) {
615 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
616 while let Some(t) = self.peek() {
617 match t.kind {
618 TokenKind::RBrace if self.brace_depth == baseline => return,
619 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
620 _ => {
621 self.bump();
622 }
623 }
624 }
625 }
626
627 fn enter_item_loop(&mut self) {
634 self.item_loop_baseline.push(self.brace_depth);
635 }
636
637 fn exit_item_loop(&mut self) {
639 self.item_loop_baseline.pop();
640 }
641
642 fn peek(&self) -> Option<Token> {
643 self.tokens.get(self.pos).copied()
644 }
645
646 fn peek_kind(&self) -> Option<TokenKind> {
647 self.peek().map(|t| t.kind)
648 }
649
650 fn nth(&self, n: usize) -> Option<Token> {
652 self.tokens.get(self.pos + n).copied()
653 }
654
655 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
656 self.nth(n).map(|t| t.kind)
657 }
658
659 fn nth_text(&self, n: usize) -> &'a str {
661 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
662 }
663
664 fn prev_span(&self) -> Span {
667 self.tokens
668 .get(self.pos.wrapping_sub(1))
669 .or_else(|| self.peek_ref())
670 .map(|t| t.span)
671 .unwrap_or_default()
672 }
673
674 fn peek_ref(&self) -> Option<&Token> {
675 self.tokens.get(self.pos)
676 }
677
678 fn bump(&mut self) -> Option<Token> {
679 let t = self.peek();
680 if let Some(t) = t {
681 match t.kind {
682 TokenKind::LBrace => self.brace_depth += 1,
683 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
684 _ => {}
685 }
686 self.pos += 1;
687 }
688 t
689 }
690
691 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
692 if self.peek_kind() == Some(kind) {
693 self.bump()
694 } else {
695 None
696 }
697 }
698
699 fn slice(&self, span: Span) -> &'a str {
700 &self.source[span.range()]
701 }
702
703 fn next_token_on_new_line(&self, prev: Span) -> bool {
708 match self.peek() {
709 Some(t) if prev.end <= t.span.start => {
710 self.source[prev.end..t.span.start].contains('\n')
711 }
712 _ => false,
713 }
714 }
715
716 fn eof_span(&self) -> Span {
721 let end = self.source.len();
722 let start = (0..end)
723 .rev()
724 .find(|&i| self.source.is_char_boundary(i))
725 .unwrap_or(0);
726 Span::new(start, end)
727 }
728
729 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
730 match self.peek() {
731 Some(t) if t.kind == kind => {
732 self.bump();
733 Ok(t)
734 }
735 Some(t) => Err(CompileError::new(
736 "bynk.parse.expected_token",
737 t.span,
738 format!(
739 "expected {} {ctx}, found {}",
740 kind.describe(),
741 t.kind.describe()
742 ),
743 )),
744 None => Err(CompileError::new(
745 "bynk.parse.unexpected_eof",
746 self.eof_span(),
747 format!("expected {} {ctx}, found end of file", kind.describe()),
748 )),
749 }
750 }
751
752 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
753 match self.peek() {
754 Some(t) if t.kind == TokenKind::Ident => {
755 self.bump();
756 Ok(Ident {
757 name: self.slice(t.span).to_string(),
758 span: t.span,
759 })
760 }
761 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
776 self.bump();
777 Ok(Ident {
778 name: self.slice(t.span).to_string(),
779 span: t.span,
780 })
781 }
782 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
783 "bynk.parse.reserved_keyword",
784 t.span,
785 format!(
786 "expected identifier {ctx}, but `{}` is a reserved keyword",
787 self.slice(t.span)
788 ),
789 )
790 .with_note("rename the identifier to something that is not a keyword")),
791 Some(t) => Err(CompileError::new(
792 "bynk.parse.expected_token",
793 t.span,
794 format!("expected identifier {ctx}, found {}", t.kind.describe()),
795 )),
796 None => Err(CompileError::new(
797 "bynk.parse.unexpected_eof",
798 self.eof_span(),
799 format!("expected identifier {ctx}, found end of file"),
800 )),
801 }
802 }
803
804 fn take_doc_block(&mut self) -> Option<(String, Span)> {
810 if self.peek_kind() == Some(TokenKind::DocBlock) {
811 let t = self.bump().unwrap();
812 let body = doc_block_content(self.source, t.span);
813 return Some((body, t.span));
814 }
815 None
816 }
817
818 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
823 let mut leading = self.take_leading_trivia();
824 let doc = self.take_doc_block();
825 if doc.is_some() {
826 leading.extend(self.take_leading_trivia());
827 }
828 (leading, doc)
829 }
830
831 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
834 let (content, doc_span) = doc?;
835 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
837 self.warnings.push(
838 CompileError::new(
839 "bynk.parse.orphan_doc_block",
840 doc_span,
841 "documentation block is separated from the following declaration by a blank line; it will not be attached",
842 )
843 .with_note(
844 "remove the blank line to attach the doc to the next declaration, \
845 or remove the doc block if it is not meant to document anything",
846 ),
847 );
848 return None;
849 }
850 Some(content)
851 }
852}
853
854fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
857 let bytes = lexeme.as_bytes();
858 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
859 let inner = &lexeme[1..lexeme.len() - 1];
860 let mut out = String::with_capacity(inner.len());
861 let mut chars = inner.chars();
862 while let Some(c) = chars.next() {
863 if c == '\\' {
864 match chars.next() {
865 Some('n') => out.push('\n'),
866 Some('t') => out.push('\t'),
867 Some('"') => out.push('"'),
868 Some('\\') => out.push('\\'),
869 other => {
870 return Err(CompileError::new(
871 "bynk.lex.bad_escape",
872 span,
873 format!(
874 "invalid escape sequence `\\{}` in string literal",
875 other.map(|c| c.to_string()).unwrap_or_default()
876 ),
877 )
878 .with_note("supported escapes: \\n \\t \\\" \\\\"));
879 }
880 }
881 } else {
882 out.push(c);
883 }
884 }
885 Ok(out)
886}
887
888fn is_reserved_keyword(kind: TokenKind) -> bool {
889 use TokenKind::*;
890 matches!(
891 kind,
892 Commons
893 | Type
894 | Fn
895 | Where
896 | True
897 | False
898 | Int
899 | String
900 | Bool
901 | Let
902 | If
903 | Else
904 | Ok
905 | Err
906 | Result
907 | ValidationError
908 | Enum
909 | Match
910 | Option
911 | Record
912 | Self_
913 | Some
914 | None
915 | Is
916 | Opaque
917 | Uses
918 | Context
919 | Consumes
920 | Exports
921 | Transparent
922 | Agent
923 | As
924 | Capability
925 | Effect
926 | Do
927 | Given
928 | On
929 | Http
930 | Provides
931 | Stub
932 | Service
933 | Actor
934 | By
935 | Expect
936 | Suite
937 | Case
938 | Float
939 | Duration
940 | Instant
941 | Bytes
942 | JsonError
943 | Property
944 | Adapter
945 | Binding
946 | Cron
947 | Queue
948 | From
949 | Protocol
950 | Invariant
951 | Implies
952 | Requires
953 | Ensures
954 | Transition
955 )
956}
957
958fn is_item_start(kind: TokenKind) -> bool {
969 use TokenKind::*;
970 matches!(
971 kind,
972 Commons | Context | Adapter | Suite
974 | Type | Fn | Messages | Event | Uses
976 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
978 | Binding
980 | Stub | Case | Property
982 )
983}
984
985#[cfg(test)]
986mod tests {
987 use super::*;
988 use crate::lexer::tokenize;
989
990 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
991 let toks = tokenize(src).map_err(|e| vec![e])?;
992 parse(&toks, src)
993 }
994
995 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
996 let toks = match tokenize(src) {
997 Ok(t) => t,
998 Err(e) => return (None, vec![e]),
999 };
1000 parse_unit_with_recovery(&toks, src)
1001 }
1002
1003 #[test]
1009 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1010 let src =
1011 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1012 let toks = tokenize(src).unwrap();
1013 let (units, errors) = parse_units_with_recovery(&toks, src);
1014 assert!(errors.is_empty(), "{errors:?}");
1015 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1016 assert!(matches!(units[0], SourceUnit::Commons(_)));
1017 assert!(matches!(units[1], SourceUnit::Suite(_)));
1018
1019 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1021 assert!(errors.is_empty(), "{errors:?}");
1022 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1023 }
1024
1025 #[test]
1032 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1033 let toks = tokenize("").unwrap();
1034 let (units, errors) = parse_units_with_recovery(&toks, "");
1035 assert!(units.is_empty());
1036 assert!(
1037 !errors.is_empty(),
1038 "an empty file must still produce a diagnostic, not silently no units and no error"
1039 );
1040
1041 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1042 assert!(unit.is_none());
1043 assert_eq!(
1044 errors.len(),
1045 unit_errors.len(),
1046 "the singular wrapper must see the same error(s) as the plural entry point"
1047 );
1048 }
1049
1050 #[test]
1057 fn drain_check_reports_expression_interior_comments_as_undrained() {
1058 let ordinary = "commons x {\n-- note\ntype T = Int where Positive\n}\n";
1059 let toks = tokenize(ordinary).unwrap();
1060 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1061 assert!(
1062 drained,
1063 "a declaration-leading comment must be fully drained"
1064 );
1065
1066 let lossy = "commons x {\n fn f() -> Int {\n 1 + -- note\n 2\n }\n}\n";
1067 let toks = tokenize(lossy).unwrap();
1068 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1069 assert!(
1070 !drained,
1071 "a comment inside a binop expression must be reported as undrained"
1072 );
1073 }
1074
1075 #[test]
1076 fn eof_span_never_splits_a_multibyte_codepoint() {
1077 for src in [
1082 "commons x {\n -- ends with an arrow →",
1083 "agent A {\n key k: String\n -- note 🦀",
1084 "commons y {\n type T = é",
1085 ] {
1086 let (_unit, errors) = parse_recover_str(src);
1087 for e in &errors {
1088 assert!(
1089 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1090 "span {:?} splits a codepoint in {src:?}",
1091 e.span,
1092 );
1093 }
1094 }
1095 }
1096
1097 #[test]
1098 fn reserved_contextual_keywords_readable_in_expression_position() {
1099 for kw in ["case", "event", "messages", "on", "suite"] {
1109 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1110 let result = parse_str(&src);
1111 assert!(
1112 result.is_ok(),
1113 "a parameter named `{kw}` must be readable in expression position: {:?}",
1114 result.err()
1115 );
1116 }
1117 }
1118
1119 #[test]
1120 fn recovery_skips_garbage_between_decls() {
1121 let src = "commons x {\n\
1124 type A = Int where NonNegative\n\
1125 ??? !!!\n\
1126 type B = String where NonEmpty\n\
1127 }";
1128 let (unit, errors) = parse_recover_str(src);
1129 let unit = unit.expect("recovery should produce a partial AST");
1130 let SourceUnit::Commons(c) = unit else {
1131 panic!("expected commons")
1132 };
1133 let names: Vec<_> = c
1135 .items
1136 .iter()
1137 .map(|i| match i {
1138 CommonsItem::Type(t) => t.name.name.clone(),
1139 _ => panic!("expected only types"),
1140 })
1141 .collect();
1142 assert!(
1143 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1144 "expected both A and B; got {names:?}",
1145 );
1146 assert!(!errors.is_empty(), "expected at least one parse error");
1147 }
1148
1149 #[test]
1150 fn recovery_handles_bad_first_decl_then_good_second() {
1151 let src = "commons x {\n\
1153 type A Int where NonNegative\n\
1154 type B = String where NonEmpty\n\
1155 }";
1156 let (unit, errors) = parse_recover_str(src);
1157 let unit = unit.expect("recovery should produce a partial AST");
1158 let SourceUnit::Commons(c) = unit else {
1159 panic!("expected commons")
1160 };
1161 let names: Vec<_> = c
1162 .items
1163 .iter()
1164 .filter_map(|i| match i {
1165 CommonsItem::Type(t) => Some(t.name.name.clone()),
1166 _ => None,
1167 })
1168 .collect();
1169 assert!(
1170 names.contains(&"B".to_string()),
1171 "B should be parsed after A's failure; got {names:?}"
1172 );
1173 assert!(!errors.is_empty(), "expected at least one parse error");
1174 }
1175
1176 #[test]
1186 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1187 let src = "commons m {\n \
1188 fn f() -> Int {\n \
1189 match 1 {\n \
1190 is 1 -> { let z = }\n \
1191 is _ -> 2\n \
1192 }\n \
1193 }\n \
1194 fn g() -> Int { 2 }\n\
1195 }\n";
1196 let (unit, errors) = parse_recover_str(src);
1197 let unit = unit.expect("recovery should produce a partial AST");
1198 let SourceUnit::Commons(c) = unit else {
1199 panic!("expected commons")
1200 };
1201 let names: Vec<_> = c
1202 .items
1203 .iter()
1204 .filter_map(|i| match i {
1205 CommonsItem::Fn(f) => match &f.name {
1206 FnName::Free(id) => Some(id.name.clone()),
1207 _ => None,
1208 },
1209 _ => None,
1210 })
1211 .collect();
1212 assert_eq!(
1213 names,
1214 vec!["g".to_string()],
1215 "g must still be recovered as an item; got {names:?}"
1216 );
1217 assert!(
1218 !errors
1219 .iter()
1220 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1221 "the outer body's own closing brace must not be mistaken for \
1222 end-of-file: {errors:?}"
1223 );
1224 }
1225
1226 #[test]
1227 fn doc_block_attaches_to_type() {
1228 let c =
1229 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1230 .unwrap();
1231 let CommonsItem::Type(t) = &c.items[0] else {
1232 panic!()
1233 };
1234 assert!(t.documentation.is_some());
1235 assert!(
1236 t.documentation
1237 .as_ref()
1238 .unwrap()
1239 .contains("A descriptive doc.")
1240 );
1241 }
1242
1243 #[test]
1244 fn interpolated_string_parses_into_parts() {
1245 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1247 .unwrap();
1248 let CommonsItem::Fn(f) = &c.items[0] else {
1249 panic!("expected fn")
1250 };
1251 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1252 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1253 };
1254 assert_eq!(parts.len(), 3);
1255 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1256 assert!(
1257 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1258 );
1259 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1260 }
1261
1262 #[test]
1263 fn interpolated_hole_parses_a_full_expression() {
1264 let c =
1266 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1267 .unwrap();
1268 let CommonsItem::Fn(f) = &c.items[0] else {
1269 panic!("expected fn")
1270 };
1271 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1272 panic!("expected InterpStr")
1273 };
1274 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1275 }
1276
1277 #[test]
1278 fn empty_interpolation_hole_is_rejected() {
1279 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1280 assert!(
1281 errs.iter()
1282 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1283 "expected empty_interpolation; got {errs:?}"
1284 );
1285 }
1286
1287 #[test]
1288 fn interpolation_hole_lex_error_span_is_rebased() {
1289 let cases = [
1295 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1297 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1299 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1302 ];
1303 for src in cases {
1304 let errs = parse_str(src).unwrap_err();
1305 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1306 for e in &errs {
1307 assert!(
1308 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1309 "span {:?} splits a codepoint in {src:?}",
1310 e.span,
1311 );
1312 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1315 assert!(
1316 e.span.start >= hole_start,
1317 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1318 e.span,
1319 );
1320 }
1321 }
1322 }
1323
1324 #[test]
1325 fn fragment_form_parses() {
1326 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1327 assert_eq!(c.form, CommonsForm::Fragment);
1328 assert_eq!(c.items.len(), 1);
1329 }
1330
1331 #[test]
1332 fn uses_parses() {
1333 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1334 assert_eq!(c.uses.len(), 1);
1335 assert_eq!(c.uses[0].target.joined(), "other.lib");
1336 }
1337
1338 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1339 let toks = tokenize(src).map_err(|e| vec![e])?;
1340 parse_unit(&toks, src)
1341 }
1342
1343 #[test]
1344 fn minimal_context_parses() {
1345 let u = parse_unit_str("context commerce.orders {}").unwrap();
1346 let SourceUnit::Context(c) = u else {
1347 panic!("expected context");
1348 };
1349 assert_eq!(c.name.joined(), "commerce.orders");
1350 assert!(c.items.is_empty());
1351 }
1352
1353 #[test]
1354 fn context_consumes_and_exports_parse() {
1355 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}";
1356 let u = parse_unit_str(src).unwrap();
1357 let SourceUnit::Context(c) = u else { panic!() };
1358 assert_eq!(c.uses.len(), 1);
1359 assert_eq!(c.consumes.len(), 1);
1360 assert_eq!(c.exports.len(), 2);
1361 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
1362 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
1363 }
1364
1365 #[test]
1366 fn context_fragment_form_parses() {
1367 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
1368 let u = parse_unit_str(src).unwrap();
1369 let SourceUnit::Context(c) = u else { panic!() };
1370 assert_eq!(c.form, CommonsForm::Fragment);
1371 assert_eq!(c.uses.len(), 1);
1372 assert_eq!(c.consumes.len(), 1);
1373 assert_eq!(c.exports.len(), 1);
1374 }
1375
1376 #[test]
1377 fn opaque_type_parses() {
1378 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
1379 let CommonsItem::Type(t) = &c.items[0] else {
1380 panic!()
1381 };
1382 assert!(matches!(t.body, TypeBody::Opaque { .. }));
1383 }
1384
1385 #[test]
1386 fn empty_commons() {
1387 let c = parse_str("commons fitness.units {}").unwrap();
1388 assert_eq!(c.name.joined(), "fitness.units");
1389 assert!(c.items.is_empty());
1390 }
1391
1392 #[test]
1393 fn one_type_decl() {
1394 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
1395 assert_eq!(c.items.len(), 1);
1396 let CommonsItem::Type(t) = &c.items[0] else {
1397 panic!()
1398 };
1399 assert_eq!(t.name.name, "Metres");
1400 match &t.body {
1401 TypeBody::Refined {
1402 base, refinement, ..
1403 } => {
1404 assert_eq!(*base, BaseType::Int);
1405 assert!(refinement.is_some());
1406 }
1407 _ => panic!("expected refined body"),
1408 }
1409 }
1410
1411 #[test]
1412 fn function_decl() {
1413 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
1414 let CommonsItem::Fn(f) = &c.items[0] else {
1415 panic!()
1416 };
1417 assert_eq!(f.name.ident().name, "add");
1418 assert_eq!(f.params.len(), 2);
1419 }
1420
1421 #[test]
1422 fn chained_comparison_is_error() {
1423 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
1424 .unwrap_err();
1425 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1426 }
1427
1428 #[test]
1429 fn chained_equality_is_error() {
1430 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
1431 .unwrap_err();
1432 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1433 }
1434
1435 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1444 std::thread::Builder::new()
1445 .stack_size(64 * 1024 * 1024)
1446 .spawn(f)
1447 .unwrap()
1448 .join()
1449 .unwrap()
1450 }
1451
1452 #[test]
1453 fn deeply_nested_parens_are_bounded_not_overflowed() {
1454 let errs = on_big_stack(|| {
1460 let depth = crate::MAX_NESTING_DEPTH + 8;
1461 let src = format!(
1462 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1463 "(".repeat(depth),
1464 ")".repeat(depth),
1465 );
1466 parse_str(&src).unwrap_err()
1467 });
1468 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1469 }
1470
1471 #[test]
1472 fn deeply_nested_types_are_bounded_not_overflowed() {
1473 let errs = on_big_stack(|| {
1478 let depth = crate::MAX_NESTING_DEPTH + 8;
1479 let src = format!(
1480 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
1481 "Result[Int, ".repeat(depth),
1482 "]".repeat(depth),
1483 );
1484 parse_str(&src).unwrap_err()
1485 });
1486 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1487 }
1488
1489 #[test]
1490 fn deeply_nested_patterns_are_bounded_not_overflowed() {
1491 let errs = on_big_stack(|| {
1496 let depth = crate::MAX_NESTING_DEPTH + 8;
1497 let src = format!(
1498 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
1499 "Ok(".repeat(depth),
1500 ")".repeat(depth),
1501 );
1502 parse_str(&src).unwrap_err()
1503 });
1504 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1505 }
1506
1507 #[test]
1508 fn nesting_below_the_limit_still_parses() {
1509 let ok = on_big_stack(|| {
1512 let depth = crate::MAX_NESTING_DEPTH - 8;
1513 let src = format!(
1514 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1515 "(".repeat(depth),
1516 ")".repeat(depth),
1517 );
1518 parse_str(&src).is_ok()
1519 });
1520 assert!(ok, "well-nested source under the limit should parse");
1521 }
1522
1523 #[test]
1524 fn let_statement_parses() {
1525 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
1526 let CommonsItem::Fn(f) = &c.items[0] else {
1527 panic!()
1528 };
1529 assert_eq!(f.body.statements.len(), 1);
1530 match &f.body.statements[0] {
1531 Statement::Let(l) => {
1532 assert_eq!(l.name.name, "y");
1533 assert!(l.type_annot.is_none());
1534 }
1535 _ => panic!("expected a pure `let` statement"),
1536 }
1537 }
1538
1539 #[test]
1540 fn let_with_annotation() {
1541 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
1542 let CommonsItem::Fn(f) = &c.items[0] else {
1543 panic!()
1544 };
1545 match &f.body.statements[0] {
1546 Statement::Let(l) => assert!(l.type_annot.is_some()),
1547 _ => panic!("expected a pure `let` statement"),
1548 }
1549 }
1550
1551 #[test]
1552 fn if_else_parses_as_expression() {
1553 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1554 let CommonsItem::Fn(f) = &c.items[0] else {
1555 panic!()
1556 };
1557 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1558 }
1559
1560 #[test]
1561 fn else_if_chain_parses() {
1562 let c = parse_str(
1563 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1564 )
1565 .unwrap();
1566 let CommonsItem::Fn(f) = &c.items[0] else {
1567 panic!()
1568 };
1569 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1570 panic!()
1571 };
1572 assert!(else_block.statements.is_empty());
1574 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1575 }
1576
1577 #[test]
1578 fn ok_and_err_parse_as_expressions() {
1579 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1580 let CommonsItem::Fn(f) = &c.items[0] else {
1581 panic!()
1582 };
1583 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1584
1585 let c =
1586 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1587 let CommonsItem::Fn(f) = &c.items[0] else {
1588 panic!()
1589 };
1590 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1591 }
1592
1593 #[test]
1594 fn question_postfix_parses() {
1595 let c = parse_str(
1596 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1597 )
1598 .unwrap();
1599 let CommonsItem::Fn(f) = &c.items[1] else {
1600 panic!()
1601 };
1602 let Statement::Let(l) = &f.body.statements[0] else {
1603 panic!("expected a pure `let` statement");
1604 };
1605 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1606 }
1607
1608 #[test]
1609 fn constructor_call_parses() {
1610 let c = parse_str(
1611 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1612 )
1613 .unwrap();
1614 let CommonsItem::Fn(f) = &c.items[1] else {
1615 panic!()
1616 };
1617 let ExprKind::MethodCall {
1620 receiver, method, ..
1621 } = &f.body.tail.kind
1622 else {
1623 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1624 };
1625 let ExprKind::Ident(id) = &receiver.kind else {
1626 panic!("expected receiver Ident");
1627 };
1628 assert_eq!(id.name, "T");
1629 assert_eq!(method.name, "of");
1630 }
1631
1632 #[test]
1633 fn result_type_ref_parses() {
1634 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1635 let CommonsItem::Fn(f) = &c.items[0] else {
1636 panic!()
1637 };
1638 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1639 }
1640
1641 #[test]
1642 fn result_missing_arg_count_errors() {
1643 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1644 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1645 }
1646
1647 #[test]
1648 fn field_access_parses_in_v0_2() {
1649 let c =
1652 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1653 let CommonsItem::Fn(f) = &c.items[1] else {
1654 panic!()
1655 };
1656 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1657 }
1658
1659 #[test]
1662 fn leading_line_comment_attaches_to_next_decl() {
1663 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1664 let c = parse_str(src).unwrap();
1665 let CommonsItem::Type(t) = &c.items[0] else {
1666 panic!()
1667 };
1668 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1669 assert!(t.trivia.trailing.is_none());
1670 }
1671
1672 #[test]
1673 fn trailing_line_comment_attaches_to_prev_decl() {
1674 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1675 let c = parse_str(src).unwrap();
1676 let CommonsItem::Type(t) = &c.items[0] else {
1677 panic!()
1678 };
1679 assert!(t.trivia.leading.is_empty());
1680 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1681 }
1682
1683 #[test]
1684 fn grouped_leading_comments_attach_together() {
1685 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1686 let c = parse_str(src).unwrap();
1687 let CommonsItem::Type(t) = &c.items[0] else {
1688 panic!()
1689 };
1690 assert_eq!(
1691 t.trivia.leading,
1692 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1693 );
1694 }
1695
1696 #[test]
1697 fn comment_with_doc_block_keeps_both() {
1698 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1700 let c = parse_str(src).unwrap();
1701 let CommonsItem::Type(t) = &c.items[0] else {
1702 panic!()
1703 };
1704 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1705 assert_eq!(t.documentation.as_deref(), Some("docs"));
1706 }
1707
1708 #[test]
1709 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
1710 let src = "commons app.messages {\ntype T = Int where Positive\n}";
1716 let c = parse_str(src).unwrap();
1717 assert_eq!(c.name.joined(), "app.messages");
1718 }
1719
1720 #[test]
1721 fn messages_decl_parses_tag_annotation_and_entries() {
1722 let src = "commons app.messages {\n\
1724 -- intro\n\
1725 ---\n\
1726 docs\n\
1727 ---\n\
1728 messages \"en\" @reference {\n\
1729 \"greeting\" => \"Hello, {name}!\"\n\
1730 \"farewell\" => \"Bye\"\n\
1731 } -- trailing\n\
1732 }";
1733 let c = parse_str(src).unwrap();
1734 let CommonsItem::Messages(m) = &c.items[0] else {
1735 panic!("expected a messages item, got {:?}", c.items[0]);
1736 };
1737 assert_eq!(m.tag, "en");
1738 assert_eq!(m.annotations.len(), 1);
1739 assert_eq!(m.annotations[0].name.name, "reference");
1740 assert!(m.annotations[0].args.is_empty());
1741 assert_eq!(m.entries.len(), 2);
1742 assert_eq!(m.entries[0].code, "greeting");
1743 assert_eq!(m.entries[0].template, "Hello, {name}!");
1744 assert_eq!(m.entries[1].code, "farewell");
1745 assert_eq!(m.entries[1].template, "Bye");
1746 assert_eq!(m.trivia.leading, vec![" intro".to_string()]);
1747 assert_eq!(m.documentation.as_deref(), Some("docs"));
1748 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
1749 }
1750
1751 #[test]
1752 fn messages_decl_parses_with_no_annotation_and_no_entries() {
1753 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
1757 let c = parse_str(src).unwrap();
1758 let CommonsItem::Messages(m) = &c.items[0] else {
1759 panic!("expected a messages item, got {:?}", c.items[0]);
1760 };
1761 assert_eq!(m.tag, "en");
1762 assert!(m.annotations.is_empty());
1763 assert!(m.entries.is_empty());
1764 }
1765
1766 #[test]
1767 fn messages_decl_parses_syntactically_inside_a_context_too() {
1768 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
1773 let toks = tokenize(src).unwrap();
1774 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1775 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
1776 let Some(SourceUnit::Context(ctx)) = unit else {
1777 panic!("expected a context")
1778 };
1779 let CommonsItem::Messages(m) = &ctx.items[0] else {
1780 panic!("expected a messages item, got {:?}", ctx.items[0]);
1781 };
1782 assert_eq!(m.tag, "en");
1783 }
1784
1785 #[test]
1786 fn comment_before_let_statement_attaches() {
1787 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1788 let c = parse_str(src).unwrap();
1789 let CommonsItem::Fn(f) = &c.items[0] else {
1790 panic!()
1791 };
1792 let Statement::Let(l) = &f.body.statements[0] else {
1793 panic!()
1794 };
1795 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1796 }
1797
1798 #[test]
1799 fn comment_before_tail_attaches_to_block_tail() {
1800 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1801 let c = parse_str(src).unwrap();
1802 let CommonsItem::Fn(f) = &c.items[0] else {
1803 panic!()
1804 };
1805 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1806 }
1807
1808 #[test]
1814 fn contextual_keywords_are_valid_identifiers() {
1815 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
1817 .expect("`on`/`suite`/`case` are valid field names");
1818 let CommonsItem::Type(_) = &c.items[0] else {
1819 panic!("expected a type decl")
1820 };
1821
1822 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
1824 .expect("`on`/`case` are valid parameter names");
1825
1826 parse_str("commons demo {\n type R = { suite: Int }\n}")
1828 .expect("`suite` is a valid field name");
1829 }
1830
1831 #[test]
1839 fn is_reserved_keyword_covers_every_lexer_keyword() {
1840 let lexer_src = include_str!("lexer.rs");
1841 let mut words = Vec::new();
1842 for line in lexer_src.lines() {
1843 let t = line.trim();
1844 if let Some(rest) = t.strip_prefix("#[token(\"")
1845 && let Some(word) = rest.split('"').next()
1846 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
1847 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1848 {
1849 words.push(word.to_string());
1850 }
1851 }
1852 assert!(
1853 words.len() > 30,
1854 "keyword extraction looks broken: only {} words",
1855 words.len()
1856 );
1857 use crate::keywords::RESERVED_CONTEXTUAL;
1860 let mut unclassified = Vec::new();
1861 for word in &words {
1862 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
1863 let kind = tokens.first().expect("keyword yields a token").kind;
1864 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
1865 unclassified.push(word.clone());
1866 }
1867 }
1868 assert!(
1869 unclassified.is_empty(),
1870 "keywords missing from is_reserved_keyword (add them, or document \
1871 them as contextual): {unclassified:?}"
1872 );
1873 }
1874
1875 #[test]
1886 fn is_item_start_matches_the_pinned_keyword_set() {
1887 use TokenKind::*;
1888 let expected_true = [
1889 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Uses, Consumes, Exports,
1890 Capability, Provides, Service, Agent, Actor, Binding, Stub, Case, Property,
1891 ];
1892 for kind in expected_true {
1893 assert!(is_item_start(kind), "{kind:?} must be an item start");
1894 }
1895 let expected_false = [
1896 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
1897 Where, True, False, Match, Is, On, Given,
1898 ];
1899 for kind in expected_false {
1900 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
1901 }
1902 }
1903
1904 #[test]
1909 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
1910 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
1911 let tokens = crate::lexer::tokenize(src).unwrap();
1912 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
1913 assert!(unit.is_some(), "the commons header still parses");
1914 assert!(
1915 errors
1916 .iter()
1917 .any(|e| e.category == "bynk.capability.outside_context"),
1918 "the misplaced capability is reported: {errors:?}"
1919 );
1920 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
1923 }
1924
1925 #[test]
1926 fn trailing_file_comment_becomes_unit_trailing() {
1927 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1931 let c = parse_str(src).unwrap();
1932 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1933 }
1934
1935 #[test]
1936 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
1937 let src = "commons x {\n type T = Int where Positive\n}\n-- afterword\n";
1944 let c = parse_str(src).unwrap();
1945 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1946 }
1947
1948 #[test]
1949 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
1950 let src = "context x {\n type T = Int where Positive\n}\n-- afterword\n";
1952 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
1953 panic!("expected context");
1954 };
1955 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1956 }
1957
1958 #[test]
1959 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
1960 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n-- afterword\n";
1962 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
1963 panic!("expected suite");
1964 };
1965 assert_eq!(s.trailing_comments, vec![" afterword".to_string()]);
1966 }
1967
1968 #[test]
1975 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
1976 let src = "adapter x {\n binding \"./x.ts\"\n}\n-- afterword\n";
1977 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
1978 panic!("expected adapter");
1979 };
1980 assert_eq!(a.trailing_comments, vec![" afterword".to_string()]);
1981 }
1982
1983 #[test]
1990 fn commons_fragment_rejects_uses_after_a_decl() {
1991 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
1992 let errs = parse_str(src).unwrap_err();
1993 assert!(
1994 errs.iter()
1995 .any(|e| e.category == "bynk.parse.uses_after_decls"),
1996 "{errs:?}"
1997 );
1998 }
1999
2000 #[test]
2003 fn commons_brace_allows_uses_after_a_decl() {
2004 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2005 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2006 }
2007
2008 #[test]
2010 fn context_fragment_rejects_consumes_after_a_decl() {
2011 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2012 let errs = parse_unit_str(src).unwrap_err();
2013 assert!(
2014 errs.iter()
2015 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2016 "{errs:?}"
2017 );
2018 }
2019
2020 #[test]
2022 fn context_fragment_rejects_exports_after_a_decl() {
2023 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2024 let errs = parse_unit_str(src).unwrap_err();
2025 assert!(
2026 errs.iter()
2027 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2028 "{errs:?}"
2029 );
2030 }
2031
2032 #[test]
2034 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2035 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2036 parse_unit_str(src)
2037 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2038 }
2039
2040 #[test]
2042 fn test_fragment_rejects_uses_after_a_decl() {
2043 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2044 let errs = parse_unit_str(src).unwrap_err();
2045 assert!(
2046 errs.iter()
2047 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2048 "{errs:?}"
2049 );
2050 }
2051
2052 #[test]
2054 fn test_brace_allows_uses_after_a_decl() {
2055 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2056 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2057 }
2058
2059 fn body_tail(body: &str) -> ExprKind {
2063 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2064 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2065 let CommonsItem::Fn(f) = &c.items[0] else {
2066 panic!("expected fn, got {:?}", c.items[0]);
2067 };
2068 f.body.tail.kind.clone()
2069 }
2070
2071 fn body_err(body: &str) -> Vec<CompileError> {
2072 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2073 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2074 }
2075
2076 #[test]
2077 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2078 for src in [
2081 "if ready { result } else { fallback }",
2082 "if ready { fallback } else { result }",
2083 "if !ready { result } else { fallback }",
2084 "if a == b { result } else { fallback }",
2085 "if a && b { result } else { fallback }",
2086 ] {
2087 let ExprKind::If {
2088 then_block,
2089 else_block,
2090 ..
2091 } = body_tail(src)
2092 else {
2093 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2094 };
2095 assert!(
2098 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2099 "then-branch tail not an ident for {src:?}: {:?}",
2100 then_block.tail.kind,
2101 );
2102 assert!(
2103 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2104 "else-branch tail not an ident for {src:?}: {:?}",
2105 else_block.tail.kind,
2106 );
2107 }
2108 }
2109
2110 #[test]
2111 fn else_less_if_with_single_ident_branch_parses() {
2112 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2114 panic!("expected If");
2115 };
2116 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2117 }
2118
2119 #[test]
2120 fn record_construction_still_parses_in_value_position() {
2121 assert!(matches!(
2124 body_tail("Point { x }"),
2125 ExprKind::RecordConstruction { .. }
2126 ));
2127 assert!(matches!(
2128 body_tail("Point { x: 1, y: 2 }"),
2129 ExprKind::RecordConstruction { .. }
2130 ));
2131 assert!(matches!(
2132 body_tail("Empty {}"),
2133 ExprKind::RecordConstruction { .. }
2134 ));
2135 }
2136
2137 #[test]
2138 fn parenthesised_record_is_allowed_in_condition_head() {
2139 let ExprKind::If { cond, .. } =
2142 body_tail("if (ready { result }) { branch } else { other }")
2143 else {
2144 panic!("expected If");
2145 };
2146 let ExprKind::Paren(inner) = &cond.kind else {
2147 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2148 };
2149 assert!(
2150 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2151 "parenthesised record in condition head should still construct: {:?}",
2152 inner.kind,
2153 );
2154 }
2155
2156 #[test]
2157 fn record_in_call_arg_within_condition_still_constructs() {
2158 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2161 else {
2162 panic!("expected If");
2163 };
2164 let ExprKind::Call { args, .. } = &cond.kind else {
2165 panic!("expected Call in condition, got {:?}", cond.kind);
2166 };
2167 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2168 }
2169
2170 #[test]
2171 fn safe_condition_shapes_are_unaffected() {
2172 assert!(matches!(
2174 body_tail("if ready == true { result } else { fallback }"),
2175 ExprKind::If { .. }
2176 ));
2177 assert!(matches!(
2178 body_tail("if (ready) { result } else { fallback }"),
2179 ExprKind::If { .. }
2180 ));
2181 assert!(matches!(
2182 body_tail("if ready { \"a\" } else { \"b\" }"),
2183 ExprKind::If { .. }
2184 ));
2185 }
2186
2187 #[test]
2188 fn empty_match_reports_its_own_diagnostic() {
2189 let errs = body_err("match result {}");
2193 assert!(
2194 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
2195 "expected empty_match; got {errs:?}",
2196 );
2197 }
2198
2199 #[test]
2200 fn match_discriminant_ending_in_ident_parses() {
2201 assert!(matches!(
2203 body_tail("match ready { x => x }"),
2204 ExprKind::Match { .. }
2205 ));
2206 }
2207
2208 #[test]
2217 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
2218 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
2219 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2220 let CommonsItem::Fn(f) = &c.items[0] else {
2221 panic!("expected fn, got {:?}", c.items[0]);
2222 };
2223 assert_eq!(
2224 f.body.statements.len(),
2225 1,
2226 "expected exactly one Assign statement, got {:?}",
2227 f.body.statements
2228 );
2229 let Statement::Assign(a) = &f.body.statements[0] else {
2230 panic!(
2231 "expected an Assign statement, got {:?}",
2232 f.body.statements[0]
2233 );
2234 };
2235 assert!(
2236 matches!(a.value.kind, ExprKind::Ident(_)),
2237 "assign value must stay the bare identifier `Paid`, got {:?}",
2238 a.value.kind
2239 );
2240 assert!(
2241 matches!(f.body.tail.kind, ExprKind::UnitLit),
2242 "the `()` must remain the block's own tail, got {:?}",
2243 f.body.tail.kind
2244 );
2245 }
2246
2247 #[test]
2251 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
2252 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
2253 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2254 let CommonsItem::Fn(f) = &c.items[0] else {
2255 panic!("expected fn, got {:?}", c.items[0]);
2256 };
2257 let Statement::Let(l) = &f.body.statements[0] else {
2258 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
2259 };
2260 assert!(
2261 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
2262 "let value must stay a field access, got {:?}",
2263 l.value.kind
2264 );
2265 assert!(
2266 matches!(f.body.tail.kind, ExprKind::UnitLit),
2267 "the `()` must remain the block's own tail, got {:?}",
2268 f.body.tail.kind
2269 );
2270 }
2271
2272 #[test]
2273 fn unparenthesised_record_in_condition_head_now_errors() {
2274 assert!(
2280 !body_err("match Point { x: 1 } { p => p }").is_empty(),
2281 "unparenthesised record discriminant should not parse",
2282 );
2283 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
2285 else {
2286 panic!("expected Match for the parenthesised form");
2287 };
2288 let ExprKind::Paren(inner) = &discriminant.kind else {
2289 panic!(
2290 "expected a parenthesised discriminant, got {:?}",
2291 discriminant.kind
2292 );
2293 };
2294 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
2295 }
2296}