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 parse_unit_with_warnings_from(tokens, source, &mut 0)
252}
253
254pub fn parse_unit_with_warnings_from(
268 tokens: &[Token],
269 source: &str,
270 next_id: &mut u32,
271) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
272 let (filtered, trivia) = split_trivia(tokens, source);
273 let mut warnings = Vec::new();
274 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
275 p.next_expr_id = *next_id;
276 let result = match p.parse_unit() {
277 Ok(u) => {
278 if let Some(extra) = p.peek() {
279 Err(vec![
280 CompileError::new(
281 "bynk.parse.extra_tokens",
282 extra.span,
283 "unexpected token after top-level declaration",
284 )
285 .with_note(
286 "a `.bynk` file contains exactly one `commons` or `context` declaration",
287 ),
288 ])
289 } else {
290 Ok(u)
291 }
292 }
293 Err(e) => Err(vec![e]),
294 };
295 *next_id = p.next_expr_id;
296 match result {
299 Ok(u) => {
300 debug_assert!(
303 p.trivia.epilogue_is_empty(),
304 "a file-trailing comment was left undrained after a successful parse"
305 );
306 Ok((u, warnings))
307 }
308 Err(mut errs) => {
309 errs.append(&mut warnings);
310 Err(errs)
311 }
312 }
313}
314
315pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
324 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
325}
326
327pub fn parse_units_with_warnings(
333 tokens: &[Token],
334 source: &str,
335) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
336 parse_units_with_drain_check(tokens, source)
337 .map(|(units, warnings, _drained)| (units, warnings))
338}
339
340pub fn parse_units_with_warnings_from(
347 tokens: &[Token],
348 source: &str,
349 next_id: &mut u32,
350) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
351 parse_units_with_drain_check_from(tokens, source, next_id)
352 .map(|(units, warnings, _drained)| (units, warnings))
353}
354
355pub fn parse_units_with_drain_check(
366 tokens: &[Token],
367 source: &str,
368) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
369 parse_units_with_drain_check_from(tokens, source, &mut 0)
370}
371
372pub fn parse_units_with_drain_check_from(
375 tokens: &[Token],
376 source: &str,
377 next_id: &mut u32,
378) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
379 let (filtered, trivia) = split_trivia(tokens, source);
380 let mut warnings = Vec::new();
381 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
382 p.next_expr_id = *next_id;
383 let mut units = Vec::new();
384 let mut errors: Vec<CompileError> = Vec::new();
385 while p.peek().is_some() {
386 match p.parse_unit() {
387 Ok(u) => units.push(u),
388 Err(e) => {
389 errors.push(e);
390 break;
391 }
392 }
393 }
394 *next_id = p.next_expr_id;
395 let eof = p.eof_span();
396 let fully_drained = p.trivia.is_fully_drained();
397 if !errors.is_empty() {
400 errors.append(&mut warnings);
401 return Err(errors);
402 }
403 if units.is_empty() {
404 return Err(vec![CompileError::new(
405 "bynk.parse.unexpected_eof",
406 eof,
407 "expected `commons`, `context`, or `suite` to start the file, found end of file",
408 )]);
409 }
410 debug_assert!(
418 p.trivia.epilogue_is_empty(),
419 "a file-trailing comment was left undrained after a successful parse"
420 );
421 Ok((units, warnings, fully_drained))
422}
423
424enum SignedNumLit {
427 Int(IntBound),
428 Float(FloatBound),
429}
430
431struct Parser<'a> {
432 tokens: &'a [Token],
433 source: &'a str,
434 pos: usize,
435 warnings: &'a mut Vec<CompileError>,
438 recover_mode: bool,
444 recovered_errors: Vec<CompileError>,
447 trivia: TriviaTable,
450 depth: usize,
457 no_record_literal: bool,
466 brace_depth: usize,
477 item_loop_baseline: Vec<usize>,
484 next_expr_id: u32,
488}
489
490impl<'a> Parser<'a> {
491 fn new(
492 tokens: &'a [Token],
493 source: &'a str,
494 trivia: TriviaTable,
495 warnings: &'a mut Vec<CompileError>,
496 ) -> Self {
497 Self {
498 tokens,
499 source,
500 pos: 0,
501 warnings,
502 recover_mode: false,
503 recovered_errors: Vec::new(),
504 trivia,
505 depth: 0,
506 no_record_literal: false,
507 brace_depth: 0,
508 item_loop_baseline: Vec::new(),
509 next_expr_id: 0,
510 }
511 }
512
513 fn alloc_expr_id(&mut self) -> ExprId {
518 let id = ExprId(self.next_expr_id);
519 self.next_expr_id += 1;
520 id
521 }
522
523 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
531 self.depth += 1;
532 if self.depth > crate::MAX_NESTING_DEPTH {
533 self.depth -= 1;
534 let span = self
535 .peek()
536 .map(|t| t.span)
537 .unwrap_or_else(|| self.eof_span());
538 return Err(self.nesting_too_deep(span, what));
539 }
540 Ok(())
541 }
542
543 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
546 CompileError::new(
547 "bynk.parse.nesting_too_deep",
548 span,
549 format!(
550 "{what} nests more than {} levels deep",
551 crate::MAX_NESTING_DEPTH
552 ),
553 )
554 .with_note(
555 "deeply nested source is rejected to keep the parser from overflowing its \
556 stack and aborting; flatten or split the construct",
557 )
558 }
559
560 fn expression_too_long(&self, span: Span) -> CompileError {
566 CompileError::new(
567 "bynk.parse.nesting_too_deep",
568 span,
569 format!(
570 "this expression is more than {} levels deep",
571 crate::MAX_NESTING_DEPTH
572 ),
573 )
574 .with_note(
575 "a long operator or member chain is rejected to keep the compiler from overflowing \
576 its stack; split it across `let` bindings, or reduce a sequence with \
577 `.sum()`/`.fold(...)`",
578 )
579 }
580
581 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
601 self.depth += 1;
602 *folds += 1;
603 if self.depth > crate::MAX_NESTING_DEPTH {
604 self.depth -= *folds;
605 *folds = 0;
606 return Err(self.expression_too_long(span));
607 }
608 Ok(())
609 }
610
611 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
620 self.depth += 1;
621 if self.depth > crate::MAX_NESTING_DEPTH {
622 return Err(self.expression_too_long(span));
623 }
624 Ok(())
625 }
626
627 fn take_leading_trivia(&mut self) -> Vec<String> {
631 self.trivia.take_leading(self.pos)
632 }
633
634 fn take_trailing_trivia(&mut self) -> Option<String> {
638 if self.pos == 0 {
639 return None;
640 }
641 self.trivia.take_trailing(self.pos - 1)
642 }
643
644 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
648 if self.recover_mode {
649 self.recovered_errors.push(e);
650 let before = self.pos;
651 self.recover_to_top_item();
652 if self.pos == before {
659 self.bump();
660 }
661 Ok(())
662 } else {
663 Err(e)
664 }
665 }
666
667 fn recover_to_top_item(&mut self) {
680 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
681 while let Some(t) = self.peek() {
682 match t.kind {
683 TokenKind::RBrace if self.brace_depth == baseline => return,
684 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
685 _ => {
686 self.bump();
687 }
688 }
689 }
690 }
691
692 fn enter_item_loop(&mut self) {
699 self.item_loop_baseline.push(self.brace_depth);
700 }
701
702 fn exit_item_loop(&mut self) {
704 self.item_loop_baseline.pop();
705 }
706
707 fn peek(&self) -> Option<Token> {
708 self.tokens.get(self.pos).copied()
709 }
710
711 fn peek_kind(&self) -> Option<TokenKind> {
712 self.peek().map(|t| t.kind)
713 }
714
715 fn nth(&self, n: usize) -> Option<Token> {
717 self.tokens.get(self.pos + n).copied()
718 }
719
720 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
721 self.nth(n).map(|t| t.kind)
722 }
723
724 fn nth_text(&self, n: usize) -> &'a str {
726 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
727 }
728
729 fn prev_span(&self) -> Span {
732 self.tokens
733 .get(self.pos.wrapping_sub(1))
734 .or_else(|| self.peek_ref())
735 .map(|t| t.span)
736 .unwrap_or_default()
737 }
738
739 fn peek_ref(&self) -> Option<&Token> {
740 self.tokens.get(self.pos)
741 }
742
743 fn bump(&mut self) -> Option<Token> {
744 let t = self.peek();
745 if let Some(t) = t {
746 match t.kind {
747 TokenKind::LBrace => self.brace_depth += 1,
748 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
749 _ => {}
750 }
751 self.pos += 1;
752 }
753 t
754 }
755
756 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
757 if self.peek_kind() == Some(kind) {
758 self.bump()
759 } else {
760 None
761 }
762 }
763
764 fn slice(&self, span: Span) -> &'a str {
765 &self.source[span.range()]
766 }
767
768 fn next_token_on_new_line(&self, prev: Span) -> bool {
773 match self.peek() {
774 Some(t) if prev.end <= t.span.start => {
775 self.source[prev.end..t.span.start].contains('\n')
776 }
777 _ => false,
778 }
779 }
780
781 fn eof_span(&self) -> Span {
786 let end = self.source.len();
787 let start = (0..end)
788 .rev()
789 .find(|&i| self.source.is_char_boundary(i))
790 .unwrap_or(0);
791 Span::new(start, end)
792 }
793
794 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
795 match self.peek() {
796 Some(t) if t.kind == kind => {
797 self.bump();
798 Ok(t)
799 }
800 Some(t) => Err(CompileError::new(
801 "bynk.parse.expected_token",
802 t.span,
803 format!(
804 "expected {} {ctx}, found {}",
805 kind.describe(),
806 t.kind.describe()
807 ),
808 )),
809 None => Err(CompileError::new(
810 "bynk.parse.unexpected_eof",
811 self.eof_span(),
812 format!("expected {} {ctx}, found end of file", kind.describe()),
813 )),
814 }
815 }
816
817 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
818 match self.peek() {
819 Some(t) if t.kind == TokenKind::Ident => {
820 self.bump();
821 Ok(Ident {
822 name: self.slice(t.span).to_string(),
823 span: t.span,
824 })
825 }
826 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
841 self.bump();
842 Ok(Ident {
843 name: self.slice(t.span).to_string(),
844 span: t.span,
845 })
846 }
847 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
848 "bynk.parse.reserved_keyword",
849 t.span,
850 format!(
851 "expected identifier {ctx}, but `{}` is a reserved keyword",
852 self.slice(t.span)
853 ),
854 )
855 .with_note("rename the identifier to something that is not a keyword")),
856 Some(t) => Err(CompileError::new(
857 "bynk.parse.expected_token",
858 t.span,
859 format!("expected identifier {ctx}, found {}", t.kind.describe()),
860 )),
861 None => Err(CompileError::new(
862 "bynk.parse.unexpected_eof",
863 self.eof_span(),
864 format!("expected identifier {ctx}, found end of file"),
865 )),
866 }
867 }
868
869 fn take_doc_block(&mut self) -> Option<(String, Span)> {
875 if self.peek_kind() == Some(TokenKind::DocBlock) {
876 let t = self.bump().unwrap();
877 let body = doc_block_content(self.source, t.span);
878 return Some((body, t.span));
879 }
880 None
881 }
882
883 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
888 let mut leading = self.take_leading_trivia();
889 let doc = self.take_doc_block();
890 if doc.is_some() {
891 leading.extend(self.take_leading_trivia());
892 }
893 (leading, doc)
894 }
895
896 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
899 let (content, doc_span) = doc?;
900 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
902 self.warnings.push(
903 CompileError::new(
904 "bynk.parse.orphan_doc_block",
905 doc_span,
906 "documentation block is separated from the following declaration by a blank line; it will not be attached",
907 )
908 .with_note(
909 "remove the blank line to attach the doc to the next declaration, \
910 or remove the doc block if it is not meant to document anything",
911 ),
912 );
913 return None;
914 }
915 Some(content)
916 }
917}
918
919fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
922 let bytes = lexeme.as_bytes();
923 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
924 let inner = &lexeme[1..lexeme.len() - 1];
925 let mut out = String::with_capacity(inner.len());
926 let mut chars = inner.chars();
927 while let Some(c) = chars.next() {
928 if c == '\\' {
929 match chars.next() {
930 Some('n') => out.push('\n'),
931 Some('t') => out.push('\t'),
932 Some('"') => out.push('"'),
933 Some('\\') => out.push('\\'),
934 other => {
935 return Err(CompileError::new(
936 "bynk.lex.bad_escape",
937 span,
938 format!(
939 "invalid escape sequence `\\{}` in string literal",
940 other.map(|c| c.to_string()).unwrap_or_default()
941 ),
942 )
943 .with_note("supported escapes: \\n \\t \\\" \\\\"));
944 }
945 }
946 } else {
947 out.push(c);
948 }
949 }
950 Ok(out)
951}
952
953fn is_reserved_keyword(kind: TokenKind) -> bool {
954 use TokenKind::*;
955 matches!(
956 kind,
957 Commons
958 | Type
959 | Fn
960 | Where
961 | True
962 | False
963 | Int
964 | String
965 | Bool
966 | Let
967 | If
968 | Else
969 | Ok
970 | Err
971 | Result
972 | ValidationError
973 | Enum
974 | Match
975 | Option
976 | Record
977 | Self_
978 | Some
979 | None
980 | Is
981 | Opaque
982 | Uses
983 | Context
984 | Consumes
985 | Exports
986 | Transparent
987 | Agent
988 | As
989 | Capability
990 | Effect
991 | Do
992 | Given
993 | On
994 | Http
995 | Provides
996 | Stub
997 | Service
998 | Actor
999 | By
1000 | Expect
1001 | Suite
1002 | Case
1003 | Float
1004 | Duration
1005 | Instant
1006 | Bytes
1007 | JsonError
1008 | Property
1009 | Adapter
1010 | Binding
1011 | Cron
1012 | Queue
1013 | From
1014 | Protocol
1015 | Invariant
1016 | Implies
1017 | Requires
1018 | Ensures
1019 | Transition
1020 )
1021}
1022
1023fn is_item_start(kind: TokenKind) -> bool {
1034 use TokenKind::*;
1035 matches!(
1036 kind,
1037 Commons | Context | Adapter | Suite
1039 | Type | Fn | Messages | Event | Uses
1041 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1043 | Binding
1045 | Stub | Case | Property
1047 )
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053 use crate::lexer::tokenize;
1054
1055 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1056 let toks = tokenize(src).map_err(|e| vec![e])?;
1057 parse(&toks, src)
1058 }
1059
1060 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1061 let toks = match tokenize(src) {
1062 Ok(t) => t,
1063 Err(e) => return (None, vec![e]),
1064 };
1065 parse_unit_with_recovery(&toks, src)
1066 }
1067
1068 #[test]
1074 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1075 let src =
1076 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1077 let toks = tokenize(src).unwrap();
1078 let (units, errors) = parse_units_with_recovery(&toks, src);
1079 assert!(errors.is_empty(), "{errors:?}");
1080 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1081 assert!(matches!(units[0], SourceUnit::Commons(_)));
1082 assert!(matches!(units[1], SourceUnit::Suite(_)));
1083
1084 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1086 assert!(errors.is_empty(), "{errors:?}");
1087 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1088 }
1089
1090 #[test]
1097 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1098 let toks = tokenize("").unwrap();
1099 let (units, errors) = parse_units_with_recovery(&toks, "");
1100 assert!(units.is_empty());
1101 assert!(
1102 !errors.is_empty(),
1103 "an empty file must still produce a diagnostic, not silently no units and no error"
1104 );
1105
1106 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1107 assert!(unit.is_none());
1108 assert_eq!(
1109 errors.len(),
1110 unit_errors.len(),
1111 "the singular wrapper must see the same error(s) as the plural entry point"
1112 );
1113 }
1114
1115 #[test]
1122 fn drain_check_reports_expression_interior_comments_as_undrained() {
1123 let ordinary = "commons x {\n-- note\ntype T = Int where Positive\n}\n";
1124 let toks = tokenize(ordinary).unwrap();
1125 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1126 assert!(
1127 drained,
1128 "a declaration-leading comment must be fully drained"
1129 );
1130
1131 let lossy = "commons x {\n fn f() -> Int {\n 1 + -- note\n 2\n }\n}\n";
1132 let toks = tokenize(lossy).unwrap();
1133 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1134 assert!(
1135 !drained,
1136 "a comment inside a binop expression must be reported as undrained"
1137 );
1138 }
1139
1140 #[test]
1141 fn eof_span_never_splits_a_multibyte_codepoint() {
1142 for src in [
1147 "commons x {\n -- ends with an arrow →",
1148 "agent A {\n key k: String\n -- note 🦀",
1149 "commons y {\n type T = é",
1150 ] {
1151 let (_unit, errors) = parse_recover_str(src);
1152 for e in &errors {
1153 assert!(
1154 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1155 "span {:?} splits a codepoint in {src:?}",
1156 e.span,
1157 );
1158 }
1159 }
1160 }
1161
1162 #[test]
1163 fn reserved_contextual_keywords_readable_in_expression_position() {
1164 for kw in ["case", "event", "messages", "on", "suite"] {
1174 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1175 let result = parse_str(&src);
1176 assert!(
1177 result.is_ok(),
1178 "a parameter named `{kw}` must be readable in expression position: {:?}",
1179 result.err()
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn recovery_skips_garbage_between_decls() {
1186 let src = "commons x {\n\
1189 type A = Int where NonNegative\n\
1190 ??? !!!\n\
1191 type B = String where NonEmpty\n\
1192 }";
1193 let (unit, errors) = parse_recover_str(src);
1194 let unit = unit.expect("recovery should produce a partial AST");
1195 let SourceUnit::Commons(c) = unit else {
1196 panic!("expected commons")
1197 };
1198 let names: Vec<_> = c
1200 .items
1201 .iter()
1202 .map(|i| match i {
1203 CommonsItem::Type(t) => t.name.name.clone(),
1204 _ => panic!("expected only types"),
1205 })
1206 .collect();
1207 assert!(
1208 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1209 "expected both A and B; got {names:?}",
1210 );
1211 assert!(!errors.is_empty(), "expected at least one parse error");
1212 }
1213
1214 #[test]
1215 fn recovery_handles_bad_first_decl_then_good_second() {
1216 let src = "commons x {\n\
1218 type A Int where NonNegative\n\
1219 type B = String where NonEmpty\n\
1220 }";
1221 let (unit, errors) = parse_recover_str(src);
1222 let unit = unit.expect("recovery should produce a partial AST");
1223 let SourceUnit::Commons(c) = unit else {
1224 panic!("expected commons")
1225 };
1226 let names: Vec<_> = c
1227 .items
1228 .iter()
1229 .filter_map(|i| match i {
1230 CommonsItem::Type(t) => Some(t.name.name.clone()),
1231 _ => None,
1232 })
1233 .collect();
1234 assert!(
1235 names.contains(&"B".to_string()),
1236 "B should be parsed after A's failure; got {names:?}"
1237 );
1238 assert!(!errors.is_empty(), "expected at least one parse error");
1239 }
1240
1241 #[test]
1251 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1252 let src = "commons m {\n \
1253 fn f() -> Int {\n \
1254 match 1 {\n \
1255 is 1 -> { let z = }\n \
1256 is _ -> 2\n \
1257 }\n \
1258 }\n \
1259 fn g() -> Int { 2 }\n\
1260 }\n";
1261 let (unit, errors) = parse_recover_str(src);
1262 let unit = unit.expect("recovery should produce a partial AST");
1263 let SourceUnit::Commons(c) = unit else {
1264 panic!("expected commons")
1265 };
1266 let names: Vec<_> = c
1267 .items
1268 .iter()
1269 .filter_map(|i| match i {
1270 CommonsItem::Fn(f) => match &f.name {
1271 FnName::Free(id) => Some(id.name.clone()),
1272 _ => None,
1273 },
1274 _ => None,
1275 })
1276 .collect();
1277 assert_eq!(
1278 names,
1279 vec!["g".to_string()],
1280 "g must still be recovered as an item; got {names:?}"
1281 );
1282 assert!(
1283 !errors
1284 .iter()
1285 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1286 "the outer body's own closing brace must not be mistaken for \
1287 end-of-file: {errors:?}"
1288 );
1289 }
1290
1291 #[test]
1292 fn doc_block_attaches_to_type() {
1293 let c =
1294 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1295 .unwrap();
1296 let CommonsItem::Type(t) = &c.items[0] else {
1297 panic!()
1298 };
1299 assert!(t.documentation.is_some());
1300 assert!(
1301 t.documentation
1302 .as_ref()
1303 .unwrap()
1304 .contains("A descriptive doc.")
1305 );
1306 }
1307
1308 #[test]
1309 fn interpolated_string_parses_into_parts() {
1310 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1312 .unwrap();
1313 let CommonsItem::Fn(f) = &c.items[0] else {
1314 panic!("expected fn")
1315 };
1316 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1317 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1318 };
1319 assert_eq!(parts.len(), 3);
1320 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1321 assert!(
1322 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1323 );
1324 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1325 }
1326
1327 #[test]
1328 fn interpolated_hole_parses_a_full_expression() {
1329 let c =
1331 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1332 .unwrap();
1333 let CommonsItem::Fn(f) = &c.items[0] else {
1334 panic!("expected fn")
1335 };
1336 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1337 panic!("expected InterpStr")
1338 };
1339 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1340 }
1341
1342 #[test]
1343 fn empty_interpolation_hole_is_rejected() {
1344 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1345 assert!(
1346 errs.iter()
1347 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1348 "expected empty_interpolation; got {errs:?}"
1349 );
1350 }
1351
1352 #[test]
1353 fn interpolation_hole_lex_error_span_is_rebased() {
1354 let cases = [
1360 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1362 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1364 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1367 ];
1368 for src in cases {
1369 let errs = parse_str(src).unwrap_err();
1370 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1371 for e in &errs {
1372 assert!(
1373 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1374 "span {:?} splits a codepoint in {src:?}",
1375 e.span,
1376 );
1377 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1380 assert!(
1381 e.span.start >= hole_start,
1382 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1383 e.span,
1384 );
1385 }
1386 }
1387 }
1388
1389 #[test]
1390 fn fragment_form_parses() {
1391 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1392 assert_eq!(c.form, CommonsForm::Fragment);
1393 assert_eq!(c.items.len(), 1);
1394 }
1395
1396 #[test]
1397 fn uses_parses() {
1398 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1399 assert_eq!(c.uses.len(), 1);
1400 assert_eq!(c.uses[0].target.joined(), "other.lib");
1401 }
1402
1403 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1404 let toks = tokenize(src).map_err(|e| vec![e])?;
1405 parse_unit(&toks, src)
1406 }
1407
1408 #[test]
1409 fn minimal_context_parses() {
1410 let u = parse_unit_str("context commerce.orders {}").unwrap();
1411 let SourceUnit::Context(c) = u else {
1412 panic!("expected context");
1413 };
1414 assert_eq!(c.name.joined(), "commerce.orders");
1415 assert!(c.items.is_empty());
1416 }
1417
1418 #[test]
1419 fn context_consumes_and_exports_parse() {
1420 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}";
1421 let u = parse_unit_str(src).unwrap();
1422 let SourceUnit::Context(c) = u else { panic!() };
1423 assert_eq!(c.uses.len(), 1);
1424 assert_eq!(c.consumes.len(), 1);
1425 assert_eq!(c.exports.len(), 2);
1426 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
1427 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
1428 }
1429
1430 #[test]
1431 fn context_fragment_form_parses() {
1432 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
1433 let u = parse_unit_str(src).unwrap();
1434 let SourceUnit::Context(c) = u else { panic!() };
1435 assert_eq!(c.form, CommonsForm::Fragment);
1436 assert_eq!(c.uses.len(), 1);
1437 assert_eq!(c.consumes.len(), 1);
1438 assert_eq!(c.exports.len(), 1);
1439 }
1440
1441 #[test]
1442 fn opaque_type_parses() {
1443 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
1444 let CommonsItem::Type(t) = &c.items[0] else {
1445 panic!()
1446 };
1447 assert!(matches!(t.body, TypeBody::Opaque { .. }));
1448 }
1449
1450 #[test]
1451 fn empty_commons() {
1452 let c = parse_str("commons fitness.units {}").unwrap();
1453 assert_eq!(c.name.joined(), "fitness.units");
1454 assert!(c.items.is_empty());
1455 }
1456
1457 #[test]
1458 fn one_type_decl() {
1459 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
1460 assert_eq!(c.items.len(), 1);
1461 let CommonsItem::Type(t) = &c.items[0] else {
1462 panic!()
1463 };
1464 assert_eq!(t.name.name, "Metres");
1465 match &t.body {
1466 TypeBody::Refined {
1467 base, refinement, ..
1468 } => {
1469 assert_eq!(*base, BaseType::Int);
1470 assert!(refinement.is_some());
1471 }
1472 _ => panic!("expected refined body"),
1473 }
1474 }
1475
1476 #[test]
1477 fn function_decl() {
1478 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
1479 let CommonsItem::Fn(f) = &c.items[0] else {
1480 panic!()
1481 };
1482 assert_eq!(f.name.ident().name, "add");
1483 assert_eq!(f.params.len(), 2);
1484 }
1485
1486 #[test]
1487 fn chained_comparison_is_error() {
1488 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
1489 .unwrap_err();
1490 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1491 }
1492
1493 #[test]
1494 fn chained_equality_is_error() {
1495 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
1496 .unwrap_err();
1497 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1498 }
1499
1500 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1509 std::thread::Builder::new()
1510 .stack_size(64 * 1024 * 1024)
1511 .spawn(f)
1512 .unwrap()
1513 .join()
1514 .unwrap()
1515 }
1516
1517 #[test]
1518 fn deeply_nested_parens_are_bounded_not_overflowed() {
1519 let errs = on_big_stack(|| {
1525 let depth = crate::MAX_NESTING_DEPTH + 8;
1526 let src = format!(
1527 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1528 "(".repeat(depth),
1529 ")".repeat(depth),
1530 );
1531 parse_str(&src).unwrap_err()
1532 });
1533 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1534 }
1535
1536 #[test]
1537 fn deeply_nested_types_are_bounded_not_overflowed() {
1538 let errs = on_big_stack(|| {
1543 let depth = crate::MAX_NESTING_DEPTH + 8;
1544 let src = format!(
1545 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
1546 "Result[Int, ".repeat(depth),
1547 "]".repeat(depth),
1548 );
1549 parse_str(&src).unwrap_err()
1550 });
1551 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1552 }
1553
1554 #[test]
1555 fn deeply_nested_patterns_are_bounded_not_overflowed() {
1556 let errs = on_big_stack(|| {
1561 let depth = crate::MAX_NESTING_DEPTH + 8;
1562 let src = format!(
1563 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
1564 "Ok(".repeat(depth),
1565 ")".repeat(depth),
1566 );
1567 parse_str(&src).unwrap_err()
1568 });
1569 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1570 }
1571
1572 #[test]
1573 fn nesting_below_the_limit_still_parses() {
1574 let ok = on_big_stack(|| {
1577 let depth = crate::MAX_NESTING_DEPTH - 8;
1578 let src = format!(
1579 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1580 "(".repeat(depth),
1581 ")".repeat(depth),
1582 );
1583 parse_str(&src).is_ok()
1584 });
1585 assert!(ok, "well-nested source under the limit should parse");
1586 }
1587
1588 #[test]
1589 fn let_statement_parses() {
1590 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
1591 let CommonsItem::Fn(f) = &c.items[0] else {
1592 panic!()
1593 };
1594 assert_eq!(f.body.statements.len(), 1);
1595 match &f.body.statements[0] {
1596 Statement::Let(l) => {
1597 assert_eq!(l.name.name, "y");
1598 assert!(l.type_annot.is_none());
1599 }
1600 _ => panic!("expected a pure `let` statement"),
1601 }
1602 }
1603
1604 #[test]
1605 fn let_with_annotation() {
1606 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
1607 let CommonsItem::Fn(f) = &c.items[0] else {
1608 panic!()
1609 };
1610 match &f.body.statements[0] {
1611 Statement::Let(l) => assert!(l.type_annot.is_some()),
1612 _ => panic!("expected a pure `let` statement"),
1613 }
1614 }
1615
1616 #[test]
1617 fn if_else_parses_as_expression() {
1618 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1619 let CommonsItem::Fn(f) = &c.items[0] else {
1620 panic!()
1621 };
1622 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1623 }
1624
1625 #[test]
1626 fn else_if_chain_parses() {
1627 let c = parse_str(
1628 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1629 )
1630 .unwrap();
1631 let CommonsItem::Fn(f) = &c.items[0] else {
1632 panic!()
1633 };
1634 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1635 panic!()
1636 };
1637 assert!(else_block.statements.is_empty());
1639 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1640 }
1641
1642 #[test]
1643 fn ok_and_err_parse_as_expressions() {
1644 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1645 let CommonsItem::Fn(f) = &c.items[0] else {
1646 panic!()
1647 };
1648 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1649
1650 let c =
1651 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1652 let CommonsItem::Fn(f) = &c.items[0] else {
1653 panic!()
1654 };
1655 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1656 }
1657
1658 #[test]
1659 fn question_postfix_parses() {
1660 let c = parse_str(
1661 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1662 )
1663 .unwrap();
1664 let CommonsItem::Fn(f) = &c.items[1] else {
1665 panic!()
1666 };
1667 let Statement::Let(l) = &f.body.statements[0] else {
1668 panic!("expected a pure `let` statement");
1669 };
1670 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1671 }
1672
1673 #[test]
1674 fn constructor_call_parses() {
1675 let c = parse_str(
1676 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1677 )
1678 .unwrap();
1679 let CommonsItem::Fn(f) = &c.items[1] else {
1680 panic!()
1681 };
1682 let ExprKind::MethodCall {
1685 receiver, method, ..
1686 } = &f.body.tail.kind
1687 else {
1688 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1689 };
1690 let ExprKind::Ident(id) = &receiver.kind else {
1691 panic!("expected receiver Ident");
1692 };
1693 assert_eq!(id.name, "T");
1694 assert_eq!(method.name, "of");
1695 }
1696
1697 #[test]
1698 fn result_type_ref_parses() {
1699 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1700 let CommonsItem::Fn(f) = &c.items[0] else {
1701 panic!()
1702 };
1703 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1704 }
1705
1706 #[test]
1707 fn result_missing_arg_count_errors() {
1708 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1709 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1710 }
1711
1712 #[test]
1713 fn field_access_parses_in_v0_2() {
1714 let c =
1717 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1718 let CommonsItem::Fn(f) = &c.items[1] else {
1719 panic!()
1720 };
1721 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1722 }
1723
1724 #[test]
1727 fn leading_line_comment_attaches_to_next_decl() {
1728 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1729 let c = parse_str(src).unwrap();
1730 let CommonsItem::Type(t) = &c.items[0] else {
1731 panic!()
1732 };
1733 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1734 assert!(t.trivia.trailing.is_none());
1735 }
1736
1737 #[test]
1738 fn trailing_line_comment_attaches_to_prev_decl() {
1739 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1740 let c = parse_str(src).unwrap();
1741 let CommonsItem::Type(t) = &c.items[0] else {
1742 panic!()
1743 };
1744 assert!(t.trivia.leading.is_empty());
1745 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1746 }
1747
1748 #[test]
1749 fn grouped_leading_comments_attach_together() {
1750 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1751 let c = parse_str(src).unwrap();
1752 let CommonsItem::Type(t) = &c.items[0] else {
1753 panic!()
1754 };
1755 assert_eq!(
1756 t.trivia.leading,
1757 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1758 );
1759 }
1760
1761 #[test]
1762 fn comment_with_doc_block_keeps_both() {
1763 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1765 let c = parse_str(src).unwrap();
1766 let CommonsItem::Type(t) = &c.items[0] else {
1767 panic!()
1768 };
1769 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1770 assert_eq!(t.documentation.as_deref(), Some("docs"));
1771 }
1772
1773 #[test]
1774 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
1775 let src = "commons app.messages {\ntype T = Int where Positive\n}";
1781 let c = parse_str(src).unwrap();
1782 assert_eq!(c.name.joined(), "app.messages");
1783 }
1784
1785 #[test]
1786 fn messages_decl_parses_tag_annotation_and_entries() {
1787 let src = "commons app.messages {\n\
1789 -- intro\n\
1790 ---\n\
1791 docs\n\
1792 ---\n\
1793 messages \"en\" @reference {\n\
1794 \"greeting\" => \"Hello, {name}!\"\n\
1795 \"farewell\" => \"Bye\"\n\
1796 } -- trailing\n\
1797 }";
1798 let c = parse_str(src).unwrap();
1799 let CommonsItem::Messages(m) = &c.items[0] else {
1800 panic!("expected a messages item, got {:?}", c.items[0]);
1801 };
1802 assert_eq!(m.tag, "en");
1803 assert_eq!(m.annotations.len(), 1);
1804 assert_eq!(m.annotations[0].name.name, "reference");
1805 assert!(m.annotations[0].args.is_empty());
1806 assert_eq!(m.entries.len(), 2);
1807 assert_eq!(m.entries[0].code, "greeting");
1808 assert_eq!(m.entries[0].template, "Hello, {name}!");
1809 assert_eq!(m.entries[1].code, "farewell");
1810 assert_eq!(m.entries[1].template, "Bye");
1811 assert_eq!(m.trivia.leading, vec![" intro".to_string()]);
1812 assert_eq!(m.documentation.as_deref(), Some("docs"));
1813 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
1814 }
1815
1816 #[test]
1817 fn messages_decl_parses_with_no_annotation_and_no_entries() {
1818 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
1822 let c = parse_str(src).unwrap();
1823 let CommonsItem::Messages(m) = &c.items[0] else {
1824 panic!("expected a messages item, got {:?}", c.items[0]);
1825 };
1826 assert_eq!(m.tag, "en");
1827 assert!(m.annotations.is_empty());
1828 assert!(m.entries.is_empty());
1829 }
1830
1831 #[test]
1832 fn messages_decl_parses_syntactically_inside_a_context_too() {
1833 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
1838 let toks = tokenize(src).unwrap();
1839 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1840 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
1841 let Some(SourceUnit::Context(ctx)) = unit else {
1842 panic!("expected a context")
1843 };
1844 let CommonsItem::Messages(m) = &ctx.items[0] else {
1845 panic!("expected a messages item, got {:?}", ctx.items[0]);
1846 };
1847 assert_eq!(m.tag, "en");
1848 }
1849
1850 #[test]
1851 fn comment_before_let_statement_attaches() {
1852 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1853 let c = parse_str(src).unwrap();
1854 let CommonsItem::Fn(f) = &c.items[0] else {
1855 panic!()
1856 };
1857 let Statement::Let(l) = &f.body.statements[0] else {
1858 panic!()
1859 };
1860 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1861 }
1862
1863 #[test]
1864 fn comment_before_tail_attaches_to_block_tail() {
1865 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1866 let c = parse_str(src).unwrap();
1867 let CommonsItem::Fn(f) = &c.items[0] else {
1868 panic!()
1869 };
1870 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1871 }
1872
1873 #[test]
1879 fn contextual_keywords_are_valid_identifiers() {
1880 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
1882 .expect("`on`/`suite`/`case` are valid field names");
1883 let CommonsItem::Type(_) = &c.items[0] else {
1884 panic!("expected a type decl")
1885 };
1886
1887 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
1889 .expect("`on`/`case` are valid parameter names");
1890
1891 parse_str("commons demo {\n type R = { suite: Int }\n}")
1893 .expect("`suite` is a valid field name");
1894 }
1895
1896 #[test]
1904 fn is_reserved_keyword_covers_every_lexer_keyword() {
1905 let lexer_src = include_str!("lexer.rs");
1906 let mut words = Vec::new();
1907 for line in lexer_src.lines() {
1908 let t = line.trim();
1909 if let Some(rest) = t.strip_prefix("#[token(\"")
1910 && let Some(word) = rest.split('"').next()
1911 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
1912 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1913 {
1914 words.push(word.to_string());
1915 }
1916 }
1917 assert!(
1918 words.len() > 30,
1919 "keyword extraction looks broken: only {} words",
1920 words.len()
1921 );
1922 use crate::keywords::RESERVED_CONTEXTUAL;
1925 let mut unclassified = Vec::new();
1926 for word in &words {
1927 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
1928 let kind = tokens.first().expect("keyword yields a token").kind;
1929 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
1930 unclassified.push(word.clone());
1931 }
1932 }
1933 assert!(
1934 unclassified.is_empty(),
1935 "keywords missing from is_reserved_keyword (add them, or document \
1936 them as contextual): {unclassified:?}"
1937 );
1938 }
1939
1940 #[test]
1951 fn is_item_start_matches_the_pinned_keyword_set() {
1952 use TokenKind::*;
1953 let expected_true = [
1954 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Uses, Consumes, Exports,
1955 Capability, Provides, Service, Agent, Actor, Binding, Stub, Case, Property,
1956 ];
1957 for kind in expected_true {
1958 assert!(is_item_start(kind), "{kind:?} must be an item start");
1959 }
1960 let expected_false = [
1961 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
1962 Where, True, False, Match, Is, On, Given,
1963 ];
1964 for kind in expected_false {
1965 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
1966 }
1967 }
1968
1969 #[test]
1974 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
1975 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
1976 let tokens = crate::lexer::tokenize(src).unwrap();
1977 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
1978 assert!(unit.is_some(), "the commons header still parses");
1979 assert!(
1980 errors
1981 .iter()
1982 .any(|e| e.category == "bynk.capability.outside_context"),
1983 "the misplaced capability is reported: {errors:?}"
1984 );
1985 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
1988 }
1989
1990 #[test]
1991 fn trailing_file_comment_becomes_unit_trailing() {
1992 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1996 let c = parse_str(src).unwrap();
1997 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1998 }
1999
2000 #[test]
2001 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2002 let src = "commons x {\n type T = Int where Positive\n}\n-- afterword\n";
2009 let c = parse_str(src).unwrap();
2010 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
2011 }
2012
2013 #[test]
2014 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2015 let src = "context x {\n type T = Int where Positive\n}\n-- afterword\n";
2017 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2018 panic!("expected context");
2019 };
2020 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
2021 }
2022
2023 #[test]
2024 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2025 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n-- afterword\n";
2027 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2028 panic!("expected suite");
2029 };
2030 assert_eq!(s.trailing_comments, vec![" afterword".to_string()]);
2031 }
2032
2033 #[test]
2040 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2041 let src = "adapter x {\n binding \"./x.ts\"\n}\n-- afterword\n";
2042 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2043 panic!("expected adapter");
2044 };
2045 assert_eq!(a.trailing_comments, vec![" afterword".to_string()]);
2046 }
2047
2048 #[test]
2055 fn commons_fragment_rejects_uses_after_a_decl() {
2056 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
2057 let errs = parse_str(src).unwrap_err();
2058 assert!(
2059 errs.iter()
2060 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2061 "{errs:?}"
2062 );
2063 }
2064
2065 #[test]
2068 fn commons_brace_allows_uses_after_a_decl() {
2069 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2070 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2071 }
2072
2073 #[test]
2075 fn context_fragment_rejects_consumes_after_a_decl() {
2076 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2077 let errs = parse_unit_str(src).unwrap_err();
2078 assert!(
2079 errs.iter()
2080 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2081 "{errs:?}"
2082 );
2083 }
2084
2085 #[test]
2087 fn context_fragment_rejects_exports_after_a_decl() {
2088 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2089 let errs = parse_unit_str(src).unwrap_err();
2090 assert!(
2091 errs.iter()
2092 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2093 "{errs:?}"
2094 );
2095 }
2096
2097 #[test]
2099 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2100 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2101 parse_unit_str(src)
2102 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2103 }
2104
2105 #[test]
2107 fn test_fragment_rejects_uses_after_a_decl() {
2108 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2109 let errs = parse_unit_str(src).unwrap_err();
2110 assert!(
2111 errs.iter()
2112 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2113 "{errs:?}"
2114 );
2115 }
2116
2117 #[test]
2119 fn test_brace_allows_uses_after_a_decl() {
2120 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2121 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2122 }
2123
2124 fn body_tail(body: &str) -> ExprKind {
2128 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2129 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2130 let CommonsItem::Fn(f) = &c.items[0] else {
2131 panic!("expected fn, got {:?}", c.items[0]);
2132 };
2133 f.body.tail.kind.clone()
2134 }
2135
2136 fn body_err(body: &str) -> Vec<CompileError> {
2137 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2138 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2139 }
2140
2141 #[test]
2142 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2143 for src in [
2146 "if ready { result } else { fallback }",
2147 "if ready { fallback } else { result }",
2148 "if !ready { result } else { fallback }",
2149 "if a == b { result } else { fallback }",
2150 "if a && b { result } else { fallback }",
2151 ] {
2152 let ExprKind::If {
2153 then_block,
2154 else_block,
2155 ..
2156 } = body_tail(src)
2157 else {
2158 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2159 };
2160 assert!(
2163 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2164 "then-branch tail not an ident for {src:?}: {:?}",
2165 then_block.tail.kind,
2166 );
2167 assert!(
2168 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2169 "else-branch tail not an ident for {src:?}: {:?}",
2170 else_block.tail.kind,
2171 );
2172 }
2173 }
2174
2175 #[test]
2176 fn else_less_if_with_single_ident_branch_parses() {
2177 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2179 panic!("expected If");
2180 };
2181 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2182 }
2183
2184 #[test]
2185 fn record_construction_still_parses_in_value_position() {
2186 assert!(matches!(
2189 body_tail("Point { x }"),
2190 ExprKind::RecordConstruction { .. }
2191 ));
2192 assert!(matches!(
2193 body_tail("Point { x: 1, y: 2 }"),
2194 ExprKind::RecordConstruction { .. }
2195 ));
2196 assert!(matches!(
2197 body_tail("Empty {}"),
2198 ExprKind::RecordConstruction { .. }
2199 ));
2200 }
2201
2202 #[test]
2203 fn parenthesised_record_is_allowed_in_condition_head() {
2204 let ExprKind::If { cond, .. } =
2207 body_tail("if (ready { result }) { branch } else { other }")
2208 else {
2209 panic!("expected If");
2210 };
2211 let ExprKind::Paren(inner) = &cond.kind else {
2212 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2213 };
2214 assert!(
2215 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2216 "parenthesised record in condition head should still construct: {:?}",
2217 inner.kind,
2218 );
2219 }
2220
2221 #[test]
2222 fn record_in_call_arg_within_condition_still_constructs() {
2223 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2226 else {
2227 panic!("expected If");
2228 };
2229 let ExprKind::Call { args, .. } = &cond.kind else {
2230 panic!("expected Call in condition, got {:?}", cond.kind);
2231 };
2232 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2233 }
2234
2235 #[test]
2236 fn safe_condition_shapes_are_unaffected() {
2237 assert!(matches!(
2239 body_tail("if ready == true { result } else { fallback }"),
2240 ExprKind::If { .. }
2241 ));
2242 assert!(matches!(
2243 body_tail("if (ready) { result } else { fallback }"),
2244 ExprKind::If { .. }
2245 ));
2246 assert!(matches!(
2247 body_tail("if ready { \"a\" } else { \"b\" }"),
2248 ExprKind::If { .. }
2249 ));
2250 }
2251
2252 #[test]
2253 fn empty_match_reports_its_own_diagnostic() {
2254 let errs = body_err("match result {}");
2258 assert!(
2259 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
2260 "expected empty_match; got {errs:?}",
2261 );
2262 }
2263
2264 #[test]
2265 fn match_discriminant_ending_in_ident_parses() {
2266 assert!(matches!(
2268 body_tail("match ready { x => x }"),
2269 ExprKind::Match { .. }
2270 ));
2271 }
2272
2273 #[test]
2282 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
2283 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
2284 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2285 let CommonsItem::Fn(f) = &c.items[0] else {
2286 panic!("expected fn, got {:?}", c.items[0]);
2287 };
2288 assert_eq!(
2289 f.body.statements.len(),
2290 1,
2291 "expected exactly one Assign statement, got {:?}",
2292 f.body.statements
2293 );
2294 let Statement::Assign(a) = &f.body.statements[0] else {
2295 panic!(
2296 "expected an Assign statement, got {:?}",
2297 f.body.statements[0]
2298 );
2299 };
2300 assert!(
2301 matches!(a.value.kind, ExprKind::Ident(_)),
2302 "assign value must stay the bare identifier `Paid`, got {:?}",
2303 a.value.kind
2304 );
2305 assert!(
2306 matches!(f.body.tail.kind, ExprKind::UnitLit),
2307 "the `()` must remain the block's own tail, got {:?}",
2308 f.body.tail.kind
2309 );
2310 }
2311
2312 #[test]
2316 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
2317 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
2318 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2319 let CommonsItem::Fn(f) = &c.items[0] else {
2320 panic!("expected fn, got {:?}", c.items[0]);
2321 };
2322 let Statement::Let(l) = &f.body.statements[0] else {
2323 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
2324 };
2325 assert!(
2326 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
2327 "let value must stay a field access, got {:?}",
2328 l.value.kind
2329 );
2330 assert!(
2331 matches!(f.body.tail.kind, ExprKind::UnitLit),
2332 "the `()` must remain the block's own tail, got {:?}",
2333 f.body.tail.kind
2334 );
2335 }
2336
2337 #[test]
2338 fn unparenthesised_record_in_condition_head_now_errors() {
2339 assert!(
2345 !body_err("match Point { x: 1 } { p => p }").is_empty(),
2346 "unparenthesised record discriminant should not parse",
2347 );
2348 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
2350 else {
2351 panic!("expected Match for the parenthesised form");
2352 };
2353 let ExprKind::Paren(inner) = &discriminant.kind else {
2354 panic!(
2355 "expected a parenthesised discriminant, got {:?}",
2356 discriminant.kind
2357 );
2358 };
2359 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
2360 }
2361}