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 recovered = parse_units_recovering(tokens, source);
212 (recovered.units, recovered.errors)
213}
214
215#[derive(Debug)]
218pub struct Recovered {
219 pub units: Vec<SourceUnit>,
220 pub errors: Vec<CompileError>,
221 pub broken_decl_names: Vec<String>,
224}
225
226pub fn merge_syntax_errors(
233 strict: Vec<CompileError>,
234 recovered: Vec<CompileError>,
235) -> Vec<CompileError> {
236 let mut out = strict;
237 for e in recovered {
238 if !out.iter().any(|o| o.span.start == e.span.start) {
239 out.push(e);
240 }
241 }
242 out
243}
244
245pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
248 let (filtered, trivia) = split_trivia(tokens, source);
249 let mut warnings = Vec::new();
250 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
251 p.recover_mode = true;
252 let mut units = Vec::new();
253 loop {
254 match p.parse_unit() {
255 Ok(u) => units.push(u),
256 Err(e) => {
257 p.recovered_errors.push(e);
258 break;
259 }
260 }
261 if p.peek().is_none() {
267 break;
268 }
269 }
270 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
271 let mut all_errors = p.recovered_errors;
272 all_errors.append(&mut warnings);
273 Recovered {
274 units,
275 errors: all_errors,
276 broken_decl_names,
277 }
278}
279
280pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
284 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
285}
286
287pub fn parse_unit_with_warnings(
290 tokens: &[Token],
291 source: &str,
292) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
293 parse_unit_with_warnings_from(tokens, source, &mut 0)
294}
295
296pub fn parse_unit_with_warnings_from(
310 tokens: &[Token],
311 source: &str,
312 next_id: &mut u32,
313) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
314 let (filtered, trivia) = split_trivia(tokens, source);
315 let mut warnings = Vec::new();
316 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
317 p.next_expr_id = *next_id;
318 let result = match p.parse_unit() {
319 Ok(u) => {
320 if let Some(extra) = p.peek() {
321 Err(vec![
322 CompileError::new(
323 "bynk.parse.extra_tokens",
324 extra.span,
325 "unexpected token after top-level declaration",
326 )
327 .with_note(
328 "a `.bynk` file contains exactly one `commons` or `context` declaration",
329 ),
330 ])
331 } else {
332 Ok(u)
333 }
334 }
335 Err(e) => Err(vec![e]),
336 };
337 *next_id = p.next_expr_id;
338 match result {
341 Ok(u) => {
342 debug_assert!(
345 p.trivia.epilogue_is_empty(),
346 "a file-trailing comment was left undrained after a successful parse"
347 );
348 Ok((u, warnings))
349 }
350 Err(mut errs) => {
351 errs.append(&mut warnings);
352 Err(errs)
353 }
354 }
355}
356
357pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
366 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
367}
368
369pub fn parse_units_with_warnings(
375 tokens: &[Token],
376 source: &str,
377) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
378 parse_units_with_drain_check(tokens, source)
379 .map(|(units, warnings, _drained)| (units, warnings))
380}
381
382pub fn parse_units_with_warnings_from(
389 tokens: &[Token],
390 source: &str,
391 next_id: &mut u32,
392) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
393 parse_units_with_drain_check_from(tokens, source, next_id)
394 .map(|(units, warnings, _drained)| (units, warnings))
395}
396
397pub fn parse_units_with_drain_check(
408 tokens: &[Token],
409 source: &str,
410) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
411 parse_units_with_drain_check_from(tokens, source, &mut 0)
412}
413
414pub fn parse_units_with_drain_check_from(
417 tokens: &[Token],
418 source: &str,
419 next_id: &mut u32,
420) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
421 let (filtered, trivia) = split_trivia(tokens, source);
422 let mut warnings = Vec::new();
423 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
424 p.next_expr_id = *next_id;
425 let mut units = Vec::new();
426 let mut errors: Vec<CompileError> = Vec::new();
427 while p.peek().is_some() {
428 match p.parse_unit() {
429 Ok(u) => units.push(u),
430 Err(e) => {
431 errors.push(e);
432 break;
433 }
434 }
435 }
436 *next_id = p.next_expr_id;
437 let eof = p.eof_span();
438 let fully_drained = p.trivia.is_fully_drained();
439 if !errors.is_empty() {
442 errors.append(&mut warnings);
443 return Err(errors);
444 }
445 if units.is_empty() {
446 return Err(vec![CompileError::new(
447 "bynk.parse.unexpected_eof",
448 eof,
449 "expected `commons`, `context`, or `suite` to start the file, found end of file",
450 )]);
451 }
452 debug_assert!(
460 p.trivia.epilogue_is_empty(),
461 "a file-trailing comment was left undrained after a successful parse"
462 );
463 Ok((units, warnings, fully_drained))
464}
465
466enum SignedNumLit {
469 Int(IntBound),
470 Float(FloatBound),
471}
472
473struct Parser<'a> {
474 tokens: &'a [Token],
475 source: &'a str,
476 pos: usize,
477 warnings: &'a mut Vec<CompileError>,
480 recover_mode: bool,
486 recovered_errors: Vec<CompileError>,
489 pub(crate) item_start: Option<usize>,
493 broken_decl_names: Vec<String>,
498 trivia: TriviaTable,
501 depth: usize,
508 no_record_literal: bool,
517 brace_depth: usize,
528 item_loop_baseline: Vec<usize>,
535 next_expr_id: u32,
539}
540
541impl<'a> Parser<'a> {
542 fn new(
543 tokens: &'a [Token],
544 source: &'a str,
545 trivia: TriviaTable,
546 warnings: &'a mut Vec<CompileError>,
547 ) -> Self {
548 Self {
549 tokens,
550 source,
551 pos: 0,
552 warnings,
553 recover_mode: false,
554 recovered_errors: Vec::new(),
555 item_start: None,
556 broken_decl_names: Vec::new(),
557 trivia,
558 depth: 0,
559 no_record_literal: false,
560 brace_depth: 0,
561 item_loop_baseline: Vec::new(),
562 next_expr_id: 0,
563 }
564 }
565
566 fn alloc_expr_id(&mut self) -> ExprId {
571 let id = ExprId(self.next_expr_id);
572 self.next_expr_id += 1;
573 id
574 }
575
576 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
584 self.depth += 1;
585 if self.depth > crate::MAX_NESTING_DEPTH {
586 self.depth -= 1;
587 let span = self
588 .peek()
589 .map(|t| t.span)
590 .unwrap_or_else(|| self.eof_span());
591 return Err(self.nesting_too_deep(span, what));
592 }
593 Ok(())
594 }
595
596 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
599 CompileError::new(
600 "bynk.parse.nesting_too_deep",
601 span,
602 format!(
603 "{what} nests more than {} levels deep",
604 crate::MAX_NESTING_DEPTH
605 ),
606 )
607 .with_note(
608 "deeply nested source is rejected to keep the parser from overflowing its \
609 stack and aborting; flatten or split the construct",
610 )
611 }
612
613 fn expression_too_long(&self, span: Span) -> CompileError {
619 CompileError::new(
620 "bynk.parse.nesting_too_deep",
621 span,
622 format!(
623 "this expression is more than {} levels deep",
624 crate::MAX_NESTING_DEPTH
625 ),
626 )
627 .with_note(
628 "a long operator or member chain is rejected to keep the compiler from overflowing \
629 its stack; split it across `let` bindings, or reduce a sequence with \
630 `.sum()`/`.fold(...)`",
631 )
632 }
633
634 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
654 self.depth += 1;
655 *folds += 1;
656 if self.depth > crate::MAX_NESTING_DEPTH {
657 self.depth -= *folds;
658 *folds = 0;
659 return Err(self.expression_too_long(span));
660 }
661 Ok(())
662 }
663
664 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
673 self.depth += 1;
674 if self.depth > crate::MAX_NESTING_DEPTH {
675 return Err(self.expression_too_long(span));
676 }
677 Ok(())
678 }
679
680 fn take_leading_trivia(&mut self) -> Vec<String> {
684 self.trivia.take_leading(self.pos)
685 }
686
687 fn take_trailing_trivia(&mut self) -> Option<String> {
691 if self.pos == 0 {
692 return None;
693 }
694 self.trivia.take_trailing(self.pos - 1)
695 }
696
697 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
701 if self.recover_mode {
702 self.recovered_errors.push(e);
703 if let Some(start) = self.item_start.take() {
704 self.broken_decl_names.extend(self.decl_names_at(start));
705 }
706 let before = self.pos;
707 self.recover_to_top_item();
708 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
721 let closes_body = baseline > 0
722 && self.brace_depth == baseline
723 && self.peek_kind() == Some(TokenKind::RBrace);
724 if self.pos == before && !closes_body {
725 self.bump();
726 self.recover_to_top_item();
731 }
732 Ok(())
733 } else {
734 Err(e)
735 }
736 }
737
738 fn decl_names_at(&self, start: usize) -> Vec<String> {
743 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
744 let ident_at = |i: usize| {
745 self.tokens
746 .get(i)
747 .filter(|t| t.kind == TokenKind::Ident)
748 .map(|t| self.slice(t.span).to_string())
749 };
750 match kind_at(start) {
751 Some(
752 TokenKind::Type
753 | TokenKind::Event
754 | TokenKind::Capability
755 | TokenKind::Service
756 | TokenKind::Agent
757 | TokenKind::Actor,
758 ) => ident_at(start + 1).into_iter().collect(),
759 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
760 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
763 Some(method) => vec![format!("{owner}.{method}")],
764 None => Vec::new(),
765 },
766 (Some(name), _) => vec![name],
767 (None, _) => Vec::new(),
768 },
769 _ => Vec::new(),
770 }
771 }
772
773 fn recover_to_top_item(&mut self) {
786 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
787 while let Some(t) = self.peek() {
788 match t.kind {
789 TokenKind::RBrace if self.brace_depth == baseline => return,
790 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
791 _ => {
792 self.bump();
793 }
794 }
795 }
796 }
797
798 fn enter_item_loop(&mut self) {
805 self.item_loop_baseline.push(self.brace_depth);
806 }
807
808 fn exit_item_loop(&mut self) {
810 self.item_loop_baseline.pop();
811 }
812
813 fn peek(&self) -> Option<Token> {
814 self.tokens.get(self.pos).copied()
815 }
816
817 fn peek_kind(&self) -> Option<TokenKind> {
818 self.peek().map(|t| t.kind)
819 }
820
821 fn nth(&self, n: usize) -> Option<Token> {
823 self.tokens.get(self.pos + n).copied()
824 }
825
826 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
827 self.nth(n).map(|t| t.kind)
828 }
829
830 fn nth_text(&self, n: usize) -> &'a str {
832 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
833 }
834
835 fn prev_span(&self) -> Span {
838 self.tokens
839 .get(self.pos.wrapping_sub(1))
840 .or_else(|| self.peek_ref())
841 .map(|t| t.span)
842 .unwrap_or_default()
843 }
844
845 fn peek_ref(&self) -> Option<&Token> {
846 self.tokens.get(self.pos)
847 }
848
849 fn bump(&mut self) -> Option<Token> {
850 let t = self.peek();
851 if let Some(t) = t {
852 match t.kind {
853 TokenKind::LBrace => self.brace_depth += 1,
854 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
855 _ => {}
856 }
857 self.pos += 1;
858 }
859 t
860 }
861
862 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
863 if self.peek_kind() == Some(kind) {
864 self.bump()
865 } else {
866 None
867 }
868 }
869
870 fn slice(&self, span: Span) -> &'a str {
871 &self.source[span.range()]
872 }
873
874 fn next_token_on_new_line(&self, prev: Span) -> bool {
879 match self.peek() {
880 Some(t) if prev.end <= t.span.start => {
881 self.source[prev.end..t.span.start].contains('\n')
882 }
883 _ => false,
884 }
885 }
886
887 fn eof_span(&self) -> Span {
892 let end = self.source.len();
893 let start = (0..end)
894 .rev()
895 .find(|&i| self.source.is_char_boundary(i))
896 .unwrap_or(0);
897 Span::new(start, end)
898 }
899
900 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
901 match self.peek() {
902 Some(t) if t.kind == kind => {
903 self.bump();
904 Ok(t)
905 }
906 Some(t) => Err(CompileError::new(
907 "bynk.parse.expected_token",
908 t.span,
909 format!(
910 "expected {} {ctx}, found {}",
911 kind.describe(),
912 t.kind.describe()
913 ),
914 )),
915 None => Err(CompileError::new(
916 "bynk.parse.unexpected_eof",
917 self.eof_span(),
918 format!("expected {} {ctx}, found end of file", kind.describe()),
919 )),
920 }
921 }
922
923 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
924 match self.peek() {
925 Some(t) if t.kind == TokenKind::Ident => {
926 self.bump();
927 Ok(Ident {
928 name: self.slice(t.span).to_string(),
929 span: t.span,
930 })
931 }
932 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
947 self.bump();
948 Ok(Ident {
949 name: self.slice(t.span).to_string(),
950 span: t.span,
951 })
952 }
953 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
954 "bynk.parse.reserved_keyword",
955 t.span,
956 format!(
957 "expected identifier {ctx}, but `{}` is a reserved keyword",
958 self.slice(t.span)
959 ),
960 )
961 .with_note("rename the identifier to something that is not a keyword")),
962 Some(t) => Err(CompileError::new(
963 "bynk.parse.expected_token",
964 t.span,
965 format!("expected identifier {ctx}, found {}", t.kind.describe()),
966 )),
967 None => Err(CompileError::new(
968 "bynk.parse.unexpected_eof",
969 self.eof_span(),
970 format!("expected identifier {ctx}, found end of file"),
971 )),
972 }
973 }
974
975 fn take_doc_block(&mut self) -> Option<(String, Span)> {
981 if self.peek_kind() == Some(TokenKind::DocBlock) {
982 let t = self.bump().unwrap();
983 let body = doc_block_content(self.source, t.span);
984 return Some((body, t.span));
985 }
986 None
987 }
988
989 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
994 let mut leading = self.take_leading_trivia();
995 let doc = self.take_doc_block();
996 if doc.is_some() {
997 leading.extend(self.take_leading_trivia());
998 }
999 (leading, doc)
1000 }
1001
1002 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
1005 let (content, doc_span) = doc?;
1006 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1008 self.warnings.push(
1009 CompileError::new(
1010 "bynk.parse.orphan_doc_block",
1011 doc_span,
1012 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1013 )
1014 .with_note(
1015 "remove the blank line to attach the doc to the next declaration, \
1016 or remove the doc block if it is not meant to document anything",
1017 ),
1018 );
1019 return None;
1020 }
1021 Some(content)
1022 }
1023}
1024
1025fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1028 let bytes = lexeme.as_bytes();
1029 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
1030 let inner = &lexeme[1..lexeme.len() - 1];
1031 let mut out = String::with_capacity(inner.len());
1032 let mut chars = inner.chars();
1033 while let Some(c) = chars.next() {
1034 if c == '\\' {
1035 match chars.next() {
1036 Some('n') => out.push('\n'),
1037 Some('t') => out.push('\t'),
1038 Some('"') => out.push('"'),
1039 Some('\\') => out.push('\\'),
1040 other => {
1041 return Err(CompileError::new(
1042 "bynk.lex.bad_escape",
1043 span,
1044 format!(
1045 "invalid escape sequence `\\{}` in string literal",
1046 other.map(|c| c.to_string()).unwrap_or_default()
1047 ),
1048 )
1049 .with_note("supported escapes: \\n \\t \\\" \\\\"));
1050 }
1051 }
1052 } else {
1053 out.push(c);
1054 }
1055 }
1056 Ok(out)
1057}
1058
1059fn is_reserved_keyword(kind: TokenKind) -> bool {
1060 use TokenKind::*;
1061 matches!(
1062 kind,
1063 Commons
1064 | Type
1065 | Fn
1066 | Where
1067 | True
1068 | False
1069 | Int
1070 | String
1071 | Bool
1072 | Let
1073 | If
1074 | Else
1075 | Ok
1076 | Err
1077 | Result
1078 | ValidationError
1079 | Enum
1080 | Match
1081 | Option
1082 | Record
1083 | Self_
1084 | Some
1085 | None
1086 | Is
1087 | Opaque
1088 | Uses
1089 | Context
1090 | Consumes
1091 | Exports
1092 | Transparent
1093 | Agent
1094 | As
1095 | Capability
1096 | Effect
1097 | Do
1098 | Given
1099 | On
1100 | Http
1101 | Provides
1102 | Stub
1103 | Service
1104 | Actor
1105 | By
1106 | Expect
1107 | Suite
1108 | Case
1109 | Float
1110 | Duration
1111 | Instant
1112 | Bytes
1113 | JsonError
1114 | Property
1115 | Adapter
1116 | Binding
1117 | Cron
1118 | Queue
1119 | From
1120 | Protocol
1121 | Invariant
1122 | Implies
1123 | Requires
1124 | Ensures
1125 | Transition
1126 )
1127}
1128
1129fn is_item_start(kind: TokenKind) -> bool {
1140 use TokenKind::*;
1141 matches!(
1142 kind,
1143 Commons | Context | Adapter | Suite
1145 | Type | Fn | Messages | Event | Uses
1147 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1149 | Binding
1151 | Stub | Case | Property
1153 )
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use crate::lexer::tokenize;
1160
1161 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1162 let toks = tokenize(src).map_err(|e| vec![e])?;
1163 parse(&toks, src)
1164 }
1165
1166 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1167 let toks = match tokenize(src) {
1168 Ok(t) => t,
1169 Err(e) => return (None, vec![e]),
1170 };
1171 parse_unit_with_recovery(&toks, src)
1172 }
1173
1174 #[test]
1180 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1181 let src =
1182 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1183 let toks = tokenize(src).unwrap();
1184 let (units, errors) = parse_units_with_recovery(&toks, src);
1185 assert!(errors.is_empty(), "{errors:?}");
1186 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1187 assert!(matches!(units[0], SourceUnit::Commons(_)));
1188 assert!(matches!(units[1], SourceUnit::Suite(_)));
1189
1190 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1192 assert!(errors.is_empty(), "{errors:?}");
1193 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1194 }
1195
1196 #[test]
1203 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1204 let toks = tokenize("").unwrap();
1205 let (units, errors) = parse_units_with_recovery(&toks, "");
1206 assert!(units.is_empty());
1207 assert!(
1208 !errors.is_empty(),
1209 "an empty file must still produce a diagnostic, not silently no units and no error"
1210 );
1211
1212 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1213 assert!(unit.is_none());
1214 assert_eq!(
1215 errors.len(),
1216 unit_errors.len(),
1217 "the singular wrapper must see the same error(s) as the plural entry point"
1218 );
1219 }
1220
1221 #[test]
1228 fn drain_check_reports_expression_interior_comments_as_undrained() {
1229 let ordinary = "commons x {\n-- note\ntype T = Int where Positive\n}\n";
1230 let toks = tokenize(ordinary).unwrap();
1231 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1232 assert!(
1233 drained,
1234 "a declaration-leading comment must be fully drained"
1235 );
1236
1237 let lossy = "commons x {\n fn f() -> Int {\n 1 + -- note\n 2\n }\n}\n";
1238 let toks = tokenize(lossy).unwrap();
1239 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1240 assert!(
1241 !drained,
1242 "a comment inside a binop expression must be reported as undrained"
1243 );
1244 }
1245
1246 #[test]
1247 fn eof_span_never_splits_a_multibyte_codepoint() {
1248 for src in [
1253 "commons x {\n -- ends with an arrow →",
1254 "agent A {\n key k: String\n -- note 🦀",
1255 "commons y {\n type T = é",
1256 ] {
1257 let (_unit, errors) = parse_recover_str(src);
1258 for e in &errors {
1259 assert!(
1260 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1261 "span {:?} splits a codepoint in {src:?}",
1262 e.span,
1263 );
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 fn reserved_contextual_keywords_readable_in_expression_position() {
1270 for kw in ["case", "event", "messages", "on", "suite"] {
1280 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1281 let result = parse_str(&src);
1282 assert!(
1283 result.is_ok(),
1284 "a parameter named `{kw}` must be readable in expression position: {:?}",
1285 result.err()
1286 );
1287 }
1288 }
1289
1290 #[test]
1291 fn recovery_skips_garbage_between_decls() {
1292 let src = "commons x {\n\
1295 type A = Int where NonNegative\n\
1296 ??? !!!\n\
1297 type B = String where NonEmpty\n\
1298 }";
1299 let (unit, errors) = parse_recover_str(src);
1300 let unit = unit.expect("recovery should produce a partial AST");
1301 let SourceUnit::Commons(c) = unit else {
1302 panic!("expected commons")
1303 };
1304 let names: Vec<_> = c
1306 .items
1307 .iter()
1308 .map(|i| match i {
1309 CommonsItem::Type(t) => t.name.name.clone(),
1310 _ => panic!("expected only types"),
1311 })
1312 .collect();
1313 assert!(
1314 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1315 "expected both A and B; got {names:?}",
1316 );
1317 assert!(!errors.is_empty(), "expected at least one parse error");
1318 }
1319
1320 #[test]
1321 fn recovery_handles_bad_first_decl_then_good_second() {
1322 let src = "commons x {\n\
1324 type A Int where NonNegative\n\
1325 type B = String where NonEmpty\n\
1326 }";
1327 let (unit, errors) = parse_recover_str(src);
1328 let unit = unit.expect("recovery should produce a partial AST");
1329 let SourceUnit::Commons(c) = unit else {
1330 panic!("expected commons")
1331 };
1332 let names: Vec<_> = c
1333 .items
1334 .iter()
1335 .filter_map(|i| match i {
1336 CommonsItem::Type(t) => Some(t.name.name.clone()),
1337 _ => None,
1338 })
1339 .collect();
1340 assert!(
1341 names.contains(&"B".to_string()),
1342 "B should be parsed after A's failure; got {names:?}"
1343 );
1344 assert!(!errors.is_empty(), "expected at least one parse error");
1345 }
1346
1347 #[test]
1362 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
1363 let src = "commons m {\n fn f() -> Int\n}\n";
1364 let (unit, errors) = parse_recover_str(src);
1365 assert!(unit.is_some(), "recovery should produce a partial AST");
1366 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
1367 assert_eq!(
1368 categories.len(),
1369 1,
1370 "one syntax error, no follow-on: {categories:?}"
1371 );
1372 assert!(
1373 !categories.contains(&"bynk.parse.unexpected_eof"),
1374 "the commons' closing brace must survive: {categories:?}"
1375 );
1376 }
1377
1378 #[test]
1383 fn recovery_skips_a_rejected_item_whole() {
1384 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
1385 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
1386 assert_eq!(
1387 recovered.errors.len(),
1388 1,
1389 "one syntax error, no follow-on: {:?}",
1390 recovered
1391 .errors
1392 .iter()
1393 .map(|e| e.category)
1394 .collect::<Vec<_>>()
1395 );
1396 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
1397 panic!("expected commons")
1398 };
1399 assert!(
1400 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
1401 "the `fn g` after the skipped agent must still parse"
1402 );
1403 assert_eq!(recovered.broken_decl_names, ["Counter"]);
1404 }
1405
1406 #[test]
1407 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1408 let src = "commons m {\n \
1409 fn f() -> Int {\n \
1410 match 1 {\n \
1411 is 1 -> { let z = }\n \
1412 is _ -> 2\n \
1413 }\n \
1414 }\n \
1415 fn g() -> Int { 2 }\n\
1416 }\n";
1417 let (unit, errors) = parse_recover_str(src);
1418 let unit = unit.expect("recovery should produce a partial AST");
1419 let SourceUnit::Commons(c) = unit else {
1420 panic!("expected commons")
1421 };
1422 let names: Vec<_> = c
1423 .items
1424 .iter()
1425 .filter_map(|i| match i {
1426 CommonsItem::Fn(f) => match &f.name {
1427 FnName::Free(id) => Some(id.name.clone()),
1428 _ => None,
1429 },
1430 _ => None,
1431 })
1432 .collect();
1433 assert_eq!(
1434 names,
1435 vec!["g".to_string()],
1436 "g must still be recovered as an item; got {names:?}"
1437 );
1438 assert!(
1439 !errors
1440 .iter()
1441 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1442 "the outer body's own closing brace must not be mistaken for \
1443 end-of-file: {errors:?}"
1444 );
1445 }
1446
1447 #[test]
1448 fn doc_block_attaches_to_type() {
1449 let c =
1450 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1451 .unwrap();
1452 let CommonsItem::Type(t) = &c.items[0] else {
1453 panic!()
1454 };
1455 assert!(t.documentation.is_some());
1456 assert!(
1457 t.documentation
1458 .as_ref()
1459 .unwrap()
1460 .contains("A descriptive doc.")
1461 );
1462 }
1463
1464 #[test]
1465 fn interpolated_string_parses_into_parts() {
1466 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1468 .unwrap();
1469 let CommonsItem::Fn(f) = &c.items[0] else {
1470 panic!("expected fn")
1471 };
1472 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1473 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1474 };
1475 assert_eq!(parts.len(), 3);
1476 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1477 assert!(
1478 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1479 );
1480 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1481 }
1482
1483 #[test]
1484 fn interpolated_hole_parses_a_full_expression() {
1485 let c =
1487 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1488 .unwrap();
1489 let CommonsItem::Fn(f) = &c.items[0] else {
1490 panic!("expected fn")
1491 };
1492 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1493 panic!("expected InterpStr")
1494 };
1495 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1496 }
1497
1498 #[test]
1499 fn empty_interpolation_hole_is_rejected() {
1500 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1501 assert!(
1502 errs.iter()
1503 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1504 "expected empty_interpolation; got {errs:?}"
1505 );
1506 }
1507
1508 #[test]
1509 fn interpolation_hole_lex_error_span_is_rebased() {
1510 let cases = [
1516 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1518 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1520 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1523 ];
1524 for src in cases {
1525 let errs = parse_str(src).unwrap_err();
1526 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1527 for e in &errs {
1528 assert!(
1529 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1530 "span {:?} splits a codepoint in {src:?}",
1531 e.span,
1532 );
1533 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1536 assert!(
1537 e.span.start >= hole_start,
1538 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1539 e.span,
1540 );
1541 }
1542 }
1543 }
1544
1545 #[test]
1546 fn fragment_form_parses() {
1547 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1548 assert_eq!(c.form, CommonsForm::Fragment);
1549 assert_eq!(c.items.len(), 1);
1550 }
1551
1552 #[test]
1553 fn uses_parses() {
1554 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1555 assert_eq!(c.uses.len(), 1);
1556 assert_eq!(c.uses[0].target.joined(), "other.lib");
1557 }
1558
1559 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1560 let toks = tokenize(src).map_err(|e| vec![e])?;
1561 parse_unit(&toks, src)
1562 }
1563
1564 #[test]
1565 fn minimal_context_parses() {
1566 let u = parse_unit_str("context commerce.orders {}").unwrap();
1567 let SourceUnit::Context(c) = u else {
1568 panic!("expected context");
1569 };
1570 assert_eq!(c.name.joined(), "commerce.orders");
1571 assert!(c.items.is_empty());
1572 }
1573
1574 #[test]
1575 fn context_consumes_and_exports_parse() {
1576 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}";
1577 let u = parse_unit_str(src).unwrap();
1578 let SourceUnit::Context(c) = u else { panic!() };
1579 assert_eq!(c.uses.len(), 1);
1580 assert_eq!(c.consumes.len(), 1);
1581 assert_eq!(c.exports.len(), 2);
1582 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
1583 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
1584 }
1585
1586 #[test]
1587 fn context_fragment_form_parses() {
1588 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
1589 let u = parse_unit_str(src).unwrap();
1590 let SourceUnit::Context(c) = u else { panic!() };
1591 assert_eq!(c.form, CommonsForm::Fragment);
1592 assert_eq!(c.uses.len(), 1);
1593 assert_eq!(c.consumes.len(), 1);
1594 assert_eq!(c.exports.len(), 1);
1595 }
1596
1597 #[test]
1598 fn opaque_type_parses() {
1599 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
1600 let CommonsItem::Type(t) = &c.items[0] else {
1601 panic!()
1602 };
1603 assert!(matches!(t.body, TypeBody::Opaque { .. }));
1604 }
1605
1606 #[test]
1607 fn empty_commons() {
1608 let c = parse_str("commons fitness.units {}").unwrap();
1609 assert_eq!(c.name.joined(), "fitness.units");
1610 assert!(c.items.is_empty());
1611 }
1612
1613 #[test]
1614 fn one_type_decl() {
1615 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
1616 assert_eq!(c.items.len(), 1);
1617 let CommonsItem::Type(t) = &c.items[0] else {
1618 panic!()
1619 };
1620 assert_eq!(t.name.name, "Metres");
1621 match &t.body {
1622 TypeBody::Refined {
1623 base, refinement, ..
1624 } => {
1625 assert_eq!(*base, BaseType::Int);
1626 assert!(refinement.is_some());
1627 }
1628 _ => panic!("expected refined body"),
1629 }
1630 }
1631
1632 #[test]
1633 fn function_decl() {
1634 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
1635 let CommonsItem::Fn(f) = &c.items[0] else {
1636 panic!()
1637 };
1638 assert_eq!(f.name.ident().name, "add");
1639 assert_eq!(f.params.len(), 2);
1640 }
1641
1642 #[test]
1643 fn chained_comparison_is_error() {
1644 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
1645 .unwrap_err();
1646 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1647 }
1648
1649 #[test]
1650 fn chained_equality_is_error() {
1651 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
1652 .unwrap_err();
1653 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1654 }
1655
1656 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1665 std::thread::Builder::new()
1666 .stack_size(64 * 1024 * 1024)
1667 .spawn(f)
1668 .unwrap()
1669 .join()
1670 .unwrap()
1671 }
1672
1673 #[test]
1674 fn deeply_nested_parens_are_bounded_not_overflowed() {
1675 let errs = on_big_stack(|| {
1681 let depth = crate::MAX_NESTING_DEPTH + 8;
1682 let src = format!(
1683 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1684 "(".repeat(depth),
1685 ")".repeat(depth),
1686 );
1687 parse_str(&src).unwrap_err()
1688 });
1689 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1690 }
1691
1692 #[test]
1693 fn deeply_nested_types_are_bounded_not_overflowed() {
1694 let errs = on_big_stack(|| {
1699 let depth = crate::MAX_NESTING_DEPTH + 8;
1700 let src = format!(
1701 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
1702 "Result[Int, ".repeat(depth),
1703 "]".repeat(depth),
1704 );
1705 parse_str(&src).unwrap_err()
1706 });
1707 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1708 }
1709
1710 #[test]
1711 fn deeply_nested_patterns_are_bounded_not_overflowed() {
1712 let errs = on_big_stack(|| {
1717 let depth = crate::MAX_NESTING_DEPTH + 8;
1718 let src = format!(
1719 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
1720 "Ok(".repeat(depth),
1721 ")".repeat(depth),
1722 );
1723 parse_str(&src).unwrap_err()
1724 });
1725 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1726 }
1727
1728 #[test]
1729 fn nesting_below_the_limit_still_parses() {
1730 let ok = on_big_stack(|| {
1733 let depth = crate::MAX_NESTING_DEPTH - 8;
1734 let src = format!(
1735 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1736 "(".repeat(depth),
1737 ")".repeat(depth),
1738 );
1739 parse_str(&src).is_ok()
1740 });
1741 assert!(ok, "well-nested source under the limit should parse");
1742 }
1743
1744 #[test]
1745 fn let_statement_parses() {
1746 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
1747 let CommonsItem::Fn(f) = &c.items[0] else {
1748 panic!()
1749 };
1750 assert_eq!(f.body.statements.len(), 1);
1751 match &f.body.statements[0] {
1752 Statement::Let(l) => {
1753 assert_eq!(l.name.name, "y");
1754 assert!(l.type_annot.is_none());
1755 }
1756 _ => panic!("expected a pure `let` statement"),
1757 }
1758 }
1759
1760 #[test]
1761 fn let_with_annotation() {
1762 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
1763 let CommonsItem::Fn(f) = &c.items[0] else {
1764 panic!()
1765 };
1766 match &f.body.statements[0] {
1767 Statement::Let(l) => assert!(l.type_annot.is_some()),
1768 _ => panic!("expected a pure `let` statement"),
1769 }
1770 }
1771
1772 #[test]
1773 fn if_else_parses_as_expression() {
1774 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1775 let CommonsItem::Fn(f) = &c.items[0] else {
1776 panic!()
1777 };
1778 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1779 }
1780
1781 #[test]
1782 fn else_if_chain_parses() {
1783 let c = parse_str(
1784 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1785 )
1786 .unwrap();
1787 let CommonsItem::Fn(f) = &c.items[0] else {
1788 panic!()
1789 };
1790 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1791 panic!()
1792 };
1793 assert!(else_block.statements.is_empty());
1795 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1796 }
1797
1798 #[test]
1799 fn ok_and_err_parse_as_expressions() {
1800 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1801 let CommonsItem::Fn(f) = &c.items[0] else {
1802 panic!()
1803 };
1804 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1805
1806 let c =
1807 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1808 let CommonsItem::Fn(f) = &c.items[0] else {
1809 panic!()
1810 };
1811 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1812 }
1813
1814 #[test]
1815 fn question_postfix_parses() {
1816 let c = parse_str(
1817 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1818 )
1819 .unwrap();
1820 let CommonsItem::Fn(f) = &c.items[1] else {
1821 panic!()
1822 };
1823 let Statement::Let(l) = &f.body.statements[0] else {
1824 panic!("expected a pure `let` statement");
1825 };
1826 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1827 }
1828
1829 #[test]
1830 fn constructor_call_parses() {
1831 let c = parse_str(
1832 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1833 )
1834 .unwrap();
1835 let CommonsItem::Fn(f) = &c.items[1] else {
1836 panic!()
1837 };
1838 let ExprKind::MethodCall {
1841 receiver, method, ..
1842 } = &f.body.tail.kind
1843 else {
1844 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1845 };
1846 let ExprKind::Ident(id) = &receiver.kind else {
1847 panic!("expected receiver Ident");
1848 };
1849 assert_eq!(id.name, "T");
1850 assert_eq!(method.name, "of");
1851 }
1852
1853 #[test]
1854 fn result_type_ref_parses() {
1855 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1856 let CommonsItem::Fn(f) = &c.items[0] else {
1857 panic!()
1858 };
1859 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1860 }
1861
1862 #[test]
1863 fn result_missing_arg_count_errors() {
1864 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1865 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1866 }
1867
1868 #[test]
1869 fn field_access_parses_in_v0_2() {
1870 let c =
1873 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1874 let CommonsItem::Fn(f) = &c.items[1] else {
1875 panic!()
1876 };
1877 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1878 }
1879
1880 #[test]
1883 fn leading_line_comment_attaches_to_next_decl() {
1884 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1885 let c = parse_str(src).unwrap();
1886 let CommonsItem::Type(t) = &c.items[0] else {
1887 panic!()
1888 };
1889 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1890 assert!(t.trivia.trailing.is_none());
1891 }
1892
1893 #[test]
1894 fn trailing_line_comment_attaches_to_prev_decl() {
1895 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1896 let c = parse_str(src).unwrap();
1897 let CommonsItem::Type(t) = &c.items[0] else {
1898 panic!()
1899 };
1900 assert!(t.trivia.leading.is_empty());
1901 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1902 }
1903
1904 #[test]
1905 fn grouped_leading_comments_attach_together() {
1906 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1907 let c = parse_str(src).unwrap();
1908 let CommonsItem::Type(t) = &c.items[0] else {
1909 panic!()
1910 };
1911 assert_eq!(
1912 t.trivia.leading,
1913 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1914 );
1915 }
1916
1917 #[test]
1918 fn comment_with_doc_block_keeps_both() {
1919 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1921 let c = parse_str(src).unwrap();
1922 let CommonsItem::Type(t) = &c.items[0] else {
1923 panic!()
1924 };
1925 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1926 assert_eq!(t.documentation.as_deref(), Some("docs"));
1927 }
1928
1929 #[test]
1930 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
1931 let src = "commons app.messages {\ntype T = Int where Positive\n}";
1937 let c = parse_str(src).unwrap();
1938 assert_eq!(c.name.joined(), "app.messages");
1939 }
1940
1941 #[test]
1942 fn messages_decl_parses_tag_annotation_and_entries() {
1943 let src = "commons app.messages {\n\
1945 -- intro\n\
1946 ---\n\
1947 docs\n\
1948 ---\n\
1949 messages \"en\" @reference {\n\
1950 \"greeting\" => \"Hello, {name}!\"\n\
1951 \"farewell\" => \"Bye\"\n\
1952 } -- trailing\n\
1953 }";
1954 let c = parse_str(src).unwrap();
1955 let CommonsItem::Messages(m) = &c.items[0] else {
1956 panic!("expected a messages item, got {:?}", c.items[0]);
1957 };
1958 assert_eq!(m.tag, "en");
1959 assert_eq!(m.annotations.len(), 1);
1960 assert_eq!(m.annotations[0].name.name, "reference");
1961 assert!(m.annotations[0].args.is_empty());
1962 assert_eq!(m.entries.len(), 2);
1963 assert_eq!(m.entries[0].code, "greeting");
1964 assert_eq!(m.entries[0].template, "Hello, {name}!");
1965 assert_eq!(m.entries[1].code, "farewell");
1966 assert_eq!(m.entries[1].template, "Bye");
1967 assert_eq!(m.trivia.leading, vec![" intro".to_string()]);
1968 assert_eq!(m.documentation.as_deref(), Some("docs"));
1969 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
1970 }
1971
1972 #[test]
1973 fn messages_decl_parses_with_no_annotation_and_no_entries() {
1974 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
1978 let c = parse_str(src).unwrap();
1979 let CommonsItem::Messages(m) = &c.items[0] else {
1980 panic!("expected a messages item, got {:?}", c.items[0]);
1981 };
1982 assert_eq!(m.tag, "en");
1983 assert!(m.annotations.is_empty());
1984 assert!(m.entries.is_empty());
1985 }
1986
1987 #[test]
1988 fn messages_decl_parses_syntactically_inside_a_context_too() {
1989 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
1994 let toks = tokenize(src).unwrap();
1995 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1996 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
1997 let Some(SourceUnit::Context(ctx)) = unit else {
1998 panic!("expected a context")
1999 };
2000 let CommonsItem::Messages(m) = &ctx.items[0] else {
2001 panic!("expected a messages item, got {:?}", ctx.items[0]);
2002 };
2003 assert_eq!(m.tag, "en");
2004 }
2005
2006 #[test]
2007 fn comment_before_let_statement_attaches() {
2008 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
2009 let c = parse_str(src).unwrap();
2010 let CommonsItem::Fn(f) = &c.items[0] else {
2011 panic!()
2012 };
2013 let Statement::Let(l) = &f.body.statements[0] else {
2014 panic!()
2015 };
2016 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
2017 }
2018
2019 #[test]
2020 fn comment_before_tail_attaches_to_block_tail() {
2021 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
2022 let c = parse_str(src).unwrap();
2023 let CommonsItem::Fn(f) = &c.items[0] else {
2024 panic!()
2025 };
2026 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
2027 }
2028
2029 #[test]
2035 fn contextual_keywords_are_valid_identifiers() {
2036 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2038 .expect("`on`/`suite`/`case` are valid field names");
2039 let CommonsItem::Type(_) = &c.items[0] else {
2040 panic!("expected a type decl")
2041 };
2042
2043 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2045 .expect("`on`/`case` are valid parameter names");
2046
2047 parse_str("commons demo {\n type R = { suite: Int }\n}")
2049 .expect("`suite` is a valid field name");
2050 }
2051
2052 #[test]
2060 fn is_reserved_keyword_covers_every_lexer_keyword() {
2061 let lexer_src = include_str!("lexer.rs");
2062 let mut words = Vec::new();
2063 for line in lexer_src.lines() {
2064 let t = line.trim();
2065 if let Some(rest) = t.strip_prefix("#[token(\"")
2066 && let Some(word) = rest.split('"').next()
2067 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2068 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2069 {
2070 words.push(word.to_string());
2071 }
2072 }
2073 assert!(
2074 words.len() > 30,
2075 "keyword extraction looks broken: only {} words",
2076 words.len()
2077 );
2078 use crate::keywords::RESERVED_CONTEXTUAL;
2081 let mut unclassified = Vec::new();
2082 for word in &words {
2083 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2084 let kind = tokens.first().expect("keyword yields a token").kind;
2085 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2086 unclassified.push(word.clone());
2087 }
2088 }
2089 assert!(
2090 unclassified.is_empty(),
2091 "keywords missing from is_reserved_keyword (add them, or document \
2092 them as contextual): {unclassified:?}"
2093 );
2094 }
2095
2096 #[test]
2107 fn is_item_start_matches_the_pinned_keyword_set() {
2108 use TokenKind::*;
2109 let expected_true = [
2110 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Uses, Consumes, Exports,
2111 Capability, Provides, Service, Agent, Actor, Binding, Stub, Case, Property,
2112 ];
2113 for kind in expected_true {
2114 assert!(is_item_start(kind), "{kind:?} must be an item start");
2115 }
2116 let expected_false = [
2117 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2118 Where, True, False, Match, Is, On, Given,
2119 ];
2120 for kind in expected_false {
2121 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2122 }
2123 }
2124
2125 #[test]
2130 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2131 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2132 let tokens = crate::lexer::tokenize(src).unwrap();
2133 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2134 assert!(unit.is_some(), "the commons header still parses");
2135 assert!(
2136 errors
2137 .iter()
2138 .any(|e| e.category == "bynk.capability.outside_context"),
2139 "the misplaced capability is reported: {errors:?}"
2140 );
2141 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2144 }
2145
2146 #[test]
2147 fn trailing_file_comment_becomes_unit_trailing() {
2148 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
2152 let c = parse_str(src).unwrap();
2153 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
2154 }
2155
2156 #[test]
2157 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2158 let src = "commons x {\n type T = Int where Positive\n}\n-- afterword\n";
2165 let c = parse_str(src).unwrap();
2166 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
2167 }
2168
2169 #[test]
2170 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2171 let src = "context x {\n type T = Int where Positive\n}\n-- afterword\n";
2173 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2174 panic!("expected context");
2175 };
2176 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
2177 }
2178
2179 #[test]
2180 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2181 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n-- afterword\n";
2183 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2184 panic!("expected suite");
2185 };
2186 assert_eq!(s.trailing_comments, vec![" afterword".to_string()]);
2187 }
2188
2189 #[test]
2196 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2197 let src = "adapter x {\n binding \"./x.ts\"\n}\n-- afterword\n";
2198 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2199 panic!("expected adapter");
2200 };
2201 assert_eq!(a.trailing_comments, vec![" afterword".to_string()]);
2202 }
2203
2204 #[test]
2211 fn commons_fragment_rejects_uses_after_a_decl() {
2212 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
2213 let errs = parse_str(src).unwrap_err();
2214 assert!(
2215 errs.iter()
2216 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2217 "{errs:?}"
2218 );
2219 }
2220
2221 #[test]
2224 fn commons_brace_allows_uses_after_a_decl() {
2225 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2226 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2227 }
2228
2229 #[test]
2231 fn context_fragment_rejects_consumes_after_a_decl() {
2232 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2233 let errs = parse_unit_str(src).unwrap_err();
2234 assert!(
2235 errs.iter()
2236 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2237 "{errs:?}"
2238 );
2239 }
2240
2241 #[test]
2243 fn context_fragment_rejects_exports_after_a_decl() {
2244 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2245 let errs = parse_unit_str(src).unwrap_err();
2246 assert!(
2247 errs.iter()
2248 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2249 "{errs:?}"
2250 );
2251 }
2252
2253 #[test]
2255 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2256 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2257 parse_unit_str(src)
2258 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2259 }
2260
2261 #[test]
2263 fn test_fragment_rejects_uses_after_a_decl() {
2264 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2265 let errs = parse_unit_str(src).unwrap_err();
2266 assert!(
2267 errs.iter()
2268 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2269 "{errs:?}"
2270 );
2271 }
2272
2273 #[test]
2275 fn test_brace_allows_uses_after_a_decl() {
2276 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2277 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2278 }
2279
2280 fn body_tail(body: &str) -> ExprKind {
2284 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2285 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2286 let CommonsItem::Fn(f) = &c.items[0] else {
2287 panic!("expected fn, got {:?}", c.items[0]);
2288 };
2289 f.body.tail.kind.clone()
2290 }
2291
2292 fn body_err(body: &str) -> Vec<CompileError> {
2293 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2294 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2295 }
2296
2297 #[test]
2298 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2299 for src in [
2302 "if ready { result } else { fallback }",
2303 "if ready { fallback } else { result }",
2304 "if !ready { result } else { fallback }",
2305 "if a == b { result } else { fallback }",
2306 "if a && b { result } else { fallback }",
2307 ] {
2308 let ExprKind::If {
2309 then_block,
2310 else_block,
2311 ..
2312 } = body_tail(src)
2313 else {
2314 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2315 };
2316 assert!(
2319 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2320 "then-branch tail not an ident for {src:?}: {:?}",
2321 then_block.tail.kind,
2322 );
2323 assert!(
2324 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2325 "else-branch tail not an ident for {src:?}: {:?}",
2326 else_block.tail.kind,
2327 );
2328 }
2329 }
2330
2331 #[test]
2332 fn else_less_if_with_single_ident_branch_parses() {
2333 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2335 panic!("expected If");
2336 };
2337 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2338 }
2339
2340 #[test]
2341 fn record_construction_still_parses_in_value_position() {
2342 assert!(matches!(
2345 body_tail("Point { x }"),
2346 ExprKind::RecordConstruction { .. }
2347 ));
2348 assert!(matches!(
2349 body_tail("Point { x: 1, y: 2 }"),
2350 ExprKind::RecordConstruction { .. }
2351 ));
2352 assert!(matches!(
2353 body_tail("Empty {}"),
2354 ExprKind::RecordConstruction { .. }
2355 ));
2356 }
2357
2358 #[test]
2359 fn parenthesised_record_is_allowed_in_condition_head() {
2360 let ExprKind::If { cond, .. } =
2363 body_tail("if (ready { result }) { branch } else { other }")
2364 else {
2365 panic!("expected If");
2366 };
2367 let ExprKind::Paren(inner) = &cond.kind else {
2368 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2369 };
2370 assert!(
2371 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2372 "parenthesised record in condition head should still construct: {:?}",
2373 inner.kind,
2374 );
2375 }
2376
2377 #[test]
2378 fn record_in_call_arg_within_condition_still_constructs() {
2379 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2382 else {
2383 panic!("expected If");
2384 };
2385 let ExprKind::Call { args, .. } = &cond.kind else {
2386 panic!("expected Call in condition, got {:?}", cond.kind);
2387 };
2388 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2389 }
2390
2391 #[test]
2392 fn safe_condition_shapes_are_unaffected() {
2393 assert!(matches!(
2395 body_tail("if ready == true { result } else { fallback }"),
2396 ExprKind::If { .. }
2397 ));
2398 assert!(matches!(
2399 body_tail("if (ready) { result } else { fallback }"),
2400 ExprKind::If { .. }
2401 ));
2402 assert!(matches!(
2403 body_tail("if ready { \"a\" } else { \"b\" }"),
2404 ExprKind::If { .. }
2405 ));
2406 }
2407
2408 #[test]
2409 fn empty_match_reports_its_own_diagnostic() {
2410 let errs = body_err("match result {}");
2414 assert!(
2415 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
2416 "expected empty_match; got {errs:?}",
2417 );
2418 }
2419
2420 #[test]
2421 fn match_discriminant_ending_in_ident_parses() {
2422 assert!(matches!(
2424 body_tail("match ready { x => x }"),
2425 ExprKind::Match { .. }
2426 ));
2427 }
2428
2429 #[test]
2438 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
2439 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
2440 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2441 let CommonsItem::Fn(f) = &c.items[0] else {
2442 panic!("expected fn, got {:?}", c.items[0]);
2443 };
2444 assert_eq!(
2445 f.body.statements.len(),
2446 1,
2447 "expected exactly one Assign statement, got {:?}",
2448 f.body.statements
2449 );
2450 let Statement::Assign(a) = &f.body.statements[0] else {
2451 panic!(
2452 "expected an Assign statement, got {:?}",
2453 f.body.statements[0]
2454 );
2455 };
2456 assert!(
2457 matches!(a.value.kind, ExprKind::Ident(_)),
2458 "assign value must stay the bare identifier `Paid`, got {:?}",
2459 a.value.kind
2460 );
2461 assert!(
2462 matches!(f.body.tail.kind, ExprKind::UnitLit),
2463 "the `()` must remain the block's own tail, got {:?}",
2464 f.body.tail.kind
2465 );
2466 }
2467
2468 #[test]
2472 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
2473 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
2474 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2475 let CommonsItem::Fn(f) = &c.items[0] else {
2476 panic!("expected fn, got {:?}", c.items[0]);
2477 };
2478 let Statement::Let(l) = &f.body.statements[0] else {
2479 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
2480 };
2481 assert!(
2482 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
2483 "let value must stay a field access, got {:?}",
2484 l.value.kind
2485 );
2486 assert!(
2487 matches!(f.body.tail.kind, ExprKind::UnitLit),
2488 "the `()` must remain the block's own tail, got {:?}",
2489 f.body.tail.kind
2490 );
2491 }
2492
2493 #[test]
2494 fn unparenthesised_record_in_condition_head_now_errors() {
2495 assert!(
2501 !body_err("match Point { x: 1 } { p => p }").is_empty(),
2502 "unparenthesised record discriminant should not parse",
2503 );
2504 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
2506 else {
2507 panic!("expected Match for the parenthesised form");
2508 };
2509 let ExprKind::Paren(inner) = &discriminant.kind else {
2510 panic!(
2511 "expected a parenthesised discriminant, got {:?}",
2512 discriminant.kind
2513 );
2514 };
2515 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
2516 }
2517}