1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{
11 Token, TokenKind, comment_body, comment_is_legacy, doc_block_content, doc_fence_len,
12 doc_line_is_legacy, doc_line_text, has_blank_line_between, legacy_comment_error,
13 legacy_doc_line_error,
14};
15use crate::span::Span;
16mod declarations;
17mod expressions;
18mod staircase;
19mod statements;
20mod types;
21
22#[derive(Debug, Default)]
31struct TriviaTable {
32 leading: Vec<Vec<Comment>>,
38 trailing: Vec<Option<String>>,
43 epilogue: Vec<Comment>,
47 legacy: Vec<CompileError>,
53}
54
55impl TriviaTable {
56 fn take_leading(&mut self, index: usize) -> Vec<Comment> {
57 match self.leading.get_mut(index) {
58 Some(v) => std::mem::take(v),
59 None => Vec::new(),
60 }
61 }
62
63 fn take_trailing(&mut self, index: usize) -> Option<String> {
64 self.trailing.get_mut(index).and_then(|s| s.take())
65 }
66
67 fn take_epilogue(&mut self) -> Vec<Comment> {
68 std::mem::take(&mut self.epilogue)
69 }
70
71 fn is_fully_drained(&self) -> bool {
88 self.leading.iter().all(Vec::is_empty)
89 && self.trailing.iter().all(Option::is_none)
90 && self.epilogue.is_empty()
91 }
92
93 fn epilogue_is_empty(&self) -> bool {
101 self.epilogue.is_empty()
102 }
103}
104
105fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
116 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
117 let mut table = TriviaTable::default();
118 let mut pending_leading: Vec<Comment> = Vec::new();
119 let mut last_content_end: Option<usize> = None;
120 let mut last_leading_end: Option<usize> = None;
127 for tok in tokens {
128 if !pending_leading.is_empty()
129 && let Some(end) = last_leading_end
130 && source[end..tok.span.start].matches('\n').count() >= 2
131 {
132 pending_leading.push(Comment::Blank);
133 }
134 if tok.kind == TokenKind::DocLine && doc_line_is_legacy(source, tok.span) {
135 table.legacy.push(legacy_doc_line_error(source, tok.span));
136 }
137 if tok.kind == TokenKind::Comment {
138 if comment_is_legacy(source, tok.span) {
139 table.legacy.push(legacy_comment_error(tok.span));
140 }
141 let body = comment_body(source, tok.span).into_owned();
142 if pending_leading.is_empty()
153 && filtered.last().is_some_and(|t| {
154 !matches!(
155 t.kind,
156 TokenKind::DocBlock | TokenKind::DocLine | TokenKind::LBrace
157 )
158 })
159 && let Some(prev_end) = last_content_end
160 && !source[prev_end..tok.span.start].contains('\n')
161 {
162 let last_idx = filtered.len() - 1;
163 if table.trailing[last_idx].is_none() {
166 table.trailing[last_idx] = Some(body);
167 continue;
168 }
169 }
170 let own_line = !pending_leading.is_empty()
174 || filtered
175 .last()
176 .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
177 || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
178 pending_leading.push(Comment::Line(body));
179 last_leading_end = own_line.then_some(tok.span.end);
180 continue;
181 }
182 filtered.push(*tok);
183 table.leading.push(std::mem::take(&mut pending_leading));
184 table.trailing.push(None);
185 last_content_end = Some(tok.span.end);
186 last_leading_end = None;
187 }
188 table.epilogue = pending_leading;
189 (filtered, table)
190}
191
192pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
198 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
199}
200
201pub fn parse_with_warnings(
204 tokens: &[Token],
205 source: &str,
206) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
207 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
208 match unit {
209 SourceUnit::Commons(c) => Ok((c, warnings)),
210 SourceUnit::Context(ctx) => Err(vec![
211 CompileError::new(
212 "bynk.parse.unexpected_context",
213 ctx.span,
214 "expected a `commons` declaration but found a `context` declaration",
215 )
216 .with_note(
217 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
218 ),
219 ]),
220 SourceUnit::Suite(t) => Err(vec![
221 CompileError::new(
222 "bynk.parse.unexpected_suite",
223 t.span,
224 "expected a `commons` declaration but found a `suite` declaration",
225 )
226 .with_note(
227 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
228 ),
229 ]),
230 SourceUnit::Adapter(a) => Err(vec![
231 CompileError::new(
232 "bynk.parse.unexpected_adapter",
233 a.span,
234 "expected a `commons` declaration but found an `adapter` declaration",
235 )
236 .with_note(
237 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
238 ),
239 ]),
240 }
241}
242
243pub fn parse_unit_with_recovery(
258 tokens: &[Token],
259 source: &str,
260) -> (Option<SourceUnit>, Vec<CompileError>) {
261 let (units, errors) = parse_units_with_recovery(tokens, source);
262 (units.into_iter().next(), errors)
263}
264
265pub fn parse_units_with_recovery(
270 tokens: &[Token],
271 source: &str,
272) -> (Vec<SourceUnit>, Vec<CompileError>) {
273 let recovered = parse_units_recovering(tokens, source);
274 (recovered.units, recovered.errors)
275}
276
277#[derive(Debug)]
280pub struct Recovered {
281 pub units: Vec<SourceUnit>,
282 pub errors: Vec<CompileError>,
283 pub broken_decl_names: Vec<String>,
286}
287
288pub fn merge_syntax_errors(
295 strict: Vec<CompileError>,
296 recovered: Vec<CompileError>,
297) -> Vec<CompileError> {
298 let mut out = strict;
299 for e in recovered {
300 if !out.iter().any(|o| o.span.start == e.span.start) {
301 out.push(e);
302 }
303 }
304 out
305}
306
307pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
310 parse_units_recovering_from(tokens, source, &mut 0)
311}
312
313pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
319 let (filtered, trivia) = split_trivia(tokens, source);
320 let mut warnings = Vec::new();
321 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
322 p.recover_mode = true;
323 p.next_expr_id = *next_id;
324 let mut units = Vec::new();
325 loop {
326 match p.parse_unit() {
327 Ok(u) => units.push(u),
328 Err(e) => {
329 p.recovered_errors.push(e);
330 break;
331 }
332 }
333 if p.peek().is_none() {
339 break;
340 }
341 }
342 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
343 *next_id = p.next_expr_id;
344 let mut all_errors = p.recovered_errors;
345 all_errors.append(&mut p.doc_errors);
346 all_errors.append(&mut warnings);
347 Recovered {
348 units,
349 errors: all_errors,
350 broken_decl_names,
351 }
352}
353
354pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
358 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
359}
360
361pub fn parse_unit_with_warnings(
364 tokens: &[Token],
365 source: &str,
366) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
367 parse_unit_with_warnings_from(tokens, source, &mut 0)
368}
369
370pub fn parse_unit_with_warnings_from(
384 tokens: &[Token],
385 source: &str,
386 next_id: &mut u32,
387) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
388 let (filtered, trivia) = split_trivia(tokens, source);
389 let mut warnings = Vec::new();
390 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
391 p.next_expr_id = *next_id;
392 let result = match p.parse_unit() {
393 Ok(u) => {
394 if let Some(extra) = p.peek() {
395 Err(vec![
396 CompileError::new(
397 "bynk.parse.extra_tokens",
398 extra.span,
399 "unexpected token after top-level declaration",
400 )
401 .with_note(
402 "a `.bynk` file contains exactly one `commons` or `context` declaration",
403 ),
404 ])
405 } else {
406 Ok(u)
407 }
408 }
409 Err(e) => Err(vec![e]),
410 };
411 let result = match (result, std::mem::take(&mut p.doc_errors)) {
414 (r, docs) if docs.is_empty() => r,
415 (Ok(_), docs) => Err(docs),
416 (Err(mut errs), mut docs) => {
417 errs.append(&mut docs);
418 Err(errs)
419 }
420 };
421 *next_id = p.next_expr_id;
422 match result {
425 Ok(u) => {
426 debug_assert!(
429 p.trivia.epilogue_is_empty(),
430 "a file-trailing comment was left undrained after a successful parse"
431 );
432 Ok((u, warnings))
433 }
434 Err(mut errs) => {
435 errs.append(&mut warnings);
436 Err(errs)
437 }
438 }
439}
440
441pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
450 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
451}
452
453pub fn parse_units_with_warnings(
459 tokens: &[Token],
460 source: &str,
461) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
462 parse_units_with_drain_check(tokens, source)
463 .map(|(units, warnings, _drained)| (units, warnings))
464}
465
466pub fn parse_units_with_warnings_from(
473 tokens: &[Token],
474 source: &str,
475 next_id: &mut u32,
476) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
477 parse_units_with_drain_check_from(tokens, source, next_id)
478 .map(|(units, warnings, _drained)| (units, warnings))
479}
480
481pub fn parse_units_with_drain_check(
492 tokens: &[Token],
493 source: &str,
494) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
495 parse_units_with_drain_check_from(tokens, source, &mut 0)
496}
497
498pub fn parse_units_with_drain_check_from(
501 tokens: &[Token],
502 source: &str,
503 next_id: &mut u32,
504) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
505 parse_units_drain(tokens, source, next_id, false)
506}
507
508pub fn parse_units_migrating(
513 tokens: &[Token],
514 source: &str,
515) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
516 parse_units_drain(tokens, source, &mut 0, true)
517}
518
519fn parse_units_drain(
520 tokens: &[Token],
521 source: &str,
522 next_id: &mut u32,
523 migrate: bool,
524) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
525 let (filtered, trivia) = split_trivia(tokens, source);
526 let mut warnings = Vec::new();
527 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
528 if migrate {
529 p.doc_errors.clear();
530 }
531 p.next_expr_id = *next_id;
532 let mut units = Vec::new();
533 let mut errors: Vec<CompileError> = Vec::new();
534 while p.peek().is_some() {
535 match p.parse_unit() {
536 Ok(u) => units.push(u),
537 Err(e) => {
538 errors.push(e);
539 break;
540 }
541 }
542 }
543 errors.append(&mut p.doc_errors);
545 *next_id = p.next_expr_id;
546 let eof = p.eof_span();
547 let fully_drained = p.trivia.is_fully_drained();
548 if !errors.is_empty() {
551 errors.append(&mut warnings);
552 return Err(errors);
553 }
554 if units.is_empty() {
555 return Err(vec![CompileError::new(
556 "bynk.parse.unexpected_eof",
557 eof,
558 "expected `commons`, `context`, or `suite` to start the file, found end of file",
559 )]);
560 }
561 debug_assert!(
569 p.trivia.epilogue_is_empty(),
570 "a file-trailing comment was left undrained after a successful parse"
571 );
572 Ok((units, warnings, fully_drained))
573}
574
575enum SignedNumLit {
578 Int(IntBound),
579 Float(FloatBound),
580 Decimal(DecimalBound),
581}
582
583struct Parser<'a> {
584 tokens: &'a [Token],
585 source: &'a str,
586 pos: usize,
587 warnings: &'a mut Vec<CompileError>,
590 recover_mode: bool,
596 recovered_errors: Vec<CompileError>,
599 pub(crate) item_start: Option<usize>,
603 broken_decl_names: Vec<String>,
608 trivia: TriviaTable,
611 depth: usize,
618 no_record_literal: bool,
627 in_guard: bool,
631 is_pattern_tail: bool,
639 brace_depth: usize,
650 item_loop_baseline: Vec<usize>,
657 next_expr_id: u32,
661 doc_errors: Vec<CompileError>,
667 doc_probe: bool,
671}
672
673impl<'a> Parser<'a> {
674 fn new(
675 tokens: &'a [Token],
676 source: &'a str,
677 mut trivia: TriviaTable,
678 warnings: &'a mut Vec<CompileError>,
679 ) -> Self {
680 let doc_errors = std::mem::take(&mut trivia.legacy);
681 Self {
682 tokens,
683 source,
684 pos: 0,
685 warnings,
686 recover_mode: false,
687 recovered_errors: Vec::new(),
688 item_start: None,
689 broken_decl_names: Vec::new(),
690 trivia,
691 depth: 0,
692 no_record_literal: false,
693 in_guard: false,
694 is_pattern_tail: false,
695 brace_depth: 0,
696 item_loop_baseline: Vec::new(),
697 next_expr_id: 0,
698 doc_errors,
699 doc_probe: false,
700 }
701 }
702
703 fn alloc_expr_id(&mut self) -> ExprId {
708 let id = ExprId(self.next_expr_id);
709 self.next_expr_id += 1;
710 id
711 }
712
713 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
721 self.depth += 1;
722 if self.depth > crate::MAX_NESTING_DEPTH {
723 self.depth -= 1;
724 let span = self
725 .peek()
726 .map(|t| t.span)
727 .unwrap_or_else(|| self.eof_span());
728 return Err(self.nesting_too_deep(span, what));
729 }
730 Ok(())
731 }
732
733 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
736 CompileError::new(
737 "bynk.parse.nesting_too_deep",
738 span,
739 format!(
740 "{what} nests more than {} levels deep",
741 crate::MAX_NESTING_DEPTH
742 ),
743 )
744 .with_note(
745 "deeply nested source is rejected to keep the parser from overflowing its \
746 stack and aborting; flatten or split the construct",
747 )
748 }
749
750 fn expression_too_long(&self, span: Span) -> CompileError {
756 CompileError::new(
757 "bynk.parse.nesting_too_deep",
758 span,
759 format!(
760 "this expression is more than {} levels deep",
761 crate::MAX_NESTING_DEPTH
762 ),
763 )
764 .with_note(
765 "a long operator or member chain is rejected to keep the compiler from overflowing \
766 its stack; split it across `let` bindings, or reduce a sequence with \
767 `.sum()`/`.fold(...)`",
768 )
769 }
770
771 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
791 self.depth += 1;
792 *folds += 1;
793 if self.depth > crate::MAX_NESTING_DEPTH {
794 self.depth -= *folds;
795 *folds = 0;
796 return Err(self.expression_too_long(span));
797 }
798 Ok(())
799 }
800
801 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
810 self.depth += 1;
811 if self.depth > crate::MAX_NESTING_DEPTH {
812 return Err(self.expression_too_long(span));
813 }
814 Ok(())
815 }
816
817 fn take_leading_trivia(&mut self) -> Vec<Comment> {
827 let mut comments = self.take_unit_leading_trivia();
828 comments.retain(|c| *c != Comment::Blank);
829 comments
830 }
831
832 fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
835 self.trivia.take_leading(self.pos)
836 }
837
838 fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
841 self.trivia.take_epilogue()
842 }
843
844 fn take_trailing_trivia(&mut self) -> Option<String> {
848 if self.pos == 0 {
849 return None;
850 }
851 self.trivia.take_trailing(self.pos - 1)
852 }
853
854 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
858 if self.recover_mode {
859 self.recovered_errors.push(e);
860 if let Some(start) = self.item_start.take() {
861 self.broken_decl_names.extend(self.decl_names_at(start));
862 }
863 let before = self.pos;
864 self.recover_to_top_item();
865 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
878 let closes_body = baseline > 0
879 && self.brace_depth == baseline
880 && self.peek_kind() == Some(TokenKind::RBrace);
881 if self.pos == before && !closes_body {
882 self.bump();
883 self.recover_to_top_item();
888 }
889 Ok(())
890 } else {
891 Err(e)
892 }
893 }
894
895 fn decl_names_at(&self, start: usize) -> Vec<String> {
900 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
901 let ident_at = |i: usize| {
902 self.tokens
903 .get(i)
904 .filter(|t| t.kind == TokenKind::Ident)
905 .map(|t| self.slice(t.span).to_string())
906 };
907 match kind_at(start) {
908 Some(
909 TokenKind::Type
910 | TokenKind::Event
911 | TokenKind::Capability
912 | TokenKind::Service
913 | TokenKind::Agent
914 | TokenKind::Actor,
915 ) => ident_at(start + 1).into_iter().collect(),
916 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
917 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
920 Some(method) => vec![format!("{owner}.{method}")],
921 None => Vec::new(),
922 },
923 (Some(name), _) => vec![name],
924 (None, _) => Vec::new(),
925 },
926 _ => Vec::new(),
927 }
928 }
929
930 fn recover_to_top_item(&mut self) {
943 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
944 while let Some(t) = self.peek() {
945 match t.kind {
946 TokenKind::RBrace if self.brace_depth == baseline => return,
947 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
948 _ => {
949 self.bump();
950 }
951 }
952 }
953 }
954
955 fn enter_item_loop(&mut self) {
962 self.item_loop_baseline.push(self.brace_depth);
963 }
964
965 fn exit_item_loop(&mut self) {
967 self.item_loop_baseline.pop();
968 }
969
970 fn peek(&self) -> Option<Token> {
971 self.tokens.get(self.pos).copied()
972 }
973
974 fn peek_kind(&self) -> Option<TokenKind> {
975 self.peek().map(|t| t.kind)
976 }
977
978 fn take_private_modifier(&mut self, allowed: bool) -> Result<Option<Span>, CompileError> {
990 let Some(t) = self.peek() else {
991 return Ok(None);
992 };
993 if t.kind != TokenKind::Ident || self.slice(t.span) != "private" {
994 return Ok(None);
995 }
996 let Some(next) = self.nth(1) else {
997 return Ok(None);
998 };
999 let modifiable = matches!(
1000 next.kind,
1001 TokenKind::Fn
1002 | TokenKind::Type
1003 | TokenKind::Messages
1004 | TokenKind::Asset
1005 | TokenKind::Embed
1006 | TokenKind::Const
1007 );
1008 let declaration = modifiable
1009 || matches!(
1010 next.kind,
1011 TokenKind::Service
1012 | TokenKind::Agent
1013 | TokenKind::Capability
1014 | TokenKind::Provides
1015 | TokenKind::Actor
1016 | TokenKind::Event
1017 );
1018 if !declaration {
1019 return Ok(None);
1020 }
1021 self.bump();
1022 if modifiable && allowed {
1023 return Ok(Some(t.span));
1024 }
1025 let what = self.slice(next.span);
1026 let err = if allowed {
1027 CompileError::new(
1028 "bynk.visibility.private_not_allowed",
1029 t.span,
1030 format!("`private` does not apply to `{what}` declarations"),
1031 )
1032 .with_note(
1033 "`private` applies to `fn`, `type`, `messages`, `asset`, `embed` and `const`; a context's `exports` and `consumes` govern how far its services, agents, capabilities, providers, actors and events reach",
1034 )
1035 } else {
1036 CompileError::new(
1037 "bynk.visibility.private_not_allowed",
1038 t.span,
1039 "`private` applies only to declarations in a commons or context, not in an adapter",
1040 )
1041 .with_note("an adapter's `exports` governs what its consumers see")
1042 };
1043 if self.recover_mode {
1044 self.recovered_errors.push(err);
1045 Ok(None)
1046 } else {
1047 Err(err)
1048 }
1049 }
1050
1051 fn nth(&self, n: usize) -> Option<Token> {
1053 self.tokens.get(self.pos + n).copied()
1054 }
1055
1056 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
1057 self.nth(n).map(|t| t.kind)
1058 }
1059
1060 fn nth_text(&self, n: usize) -> &'a str {
1062 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
1063 }
1064
1065 fn prev_span(&self) -> Span {
1068 self.tokens
1069 .get(self.pos.wrapping_sub(1))
1070 .or_else(|| self.peek_ref())
1071 .map(|t| t.span)
1072 .unwrap_or_default()
1073 }
1074
1075 fn peek_ref(&self) -> Option<&Token> {
1076 self.tokens.get(self.pos)
1077 }
1078
1079 fn bump(&mut self) -> Option<Token> {
1080 let t = self.peek();
1081 if let Some(t) = t {
1082 match t.kind {
1083 TokenKind::LBrace => self.brace_depth += 1,
1084 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
1085 _ => {}
1086 }
1087 self.pos += 1;
1088 }
1089 t
1090 }
1091
1092 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
1093 if self.peek_kind() == Some(kind) {
1094 self.bump()
1095 } else {
1096 None
1097 }
1098 }
1099
1100 fn slice(&self, span: Span) -> &'a str {
1101 &self.source[span.range()]
1102 }
1103
1104 fn next_token_on_new_line(&self, prev: Span) -> bool {
1109 match self.peek() {
1110 Some(t) if prev.end <= t.span.start => {
1111 self.source[prev.end..t.span.start].contains('\n')
1112 }
1113 _ => false,
1114 }
1115 }
1116
1117 fn eof_span(&self) -> Span {
1122 let end = self.source.len();
1123 let start = (0..end)
1124 .rev()
1125 .find(|&i| self.source.is_char_boundary(i))
1126 .unwrap_or(0);
1127 Span::new(start, end)
1128 }
1129
1130 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1131 match self.peek() {
1132 Some(t) if t.kind == kind => {
1133 self.bump();
1134 Ok(t)
1135 }
1136 Some(t) => Err(CompileError::new(
1137 "bynk.parse.expected_token",
1138 t.span,
1139 format!(
1140 "expected {} {ctx}, found {}",
1141 kind.describe(),
1142 t.kind.describe()
1143 ),
1144 )),
1145 None => Err(CompileError::new(
1146 "bynk.parse.unexpected_eof",
1147 self.eof_span(),
1148 format!("expected {} {ctx}, found end of file", kind.describe()),
1149 )),
1150 }
1151 }
1152
1153 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1154 match self.peek() {
1155 Some(t) if t.kind == TokenKind::Ident => {
1156 self.bump();
1157 Ok(Ident {
1158 name: self.slice(t.span).to_string(),
1159 span: t.span,
1160 })
1161 }
1162 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1177 self.bump();
1178 Ok(Ident {
1179 name: self.slice(t.span).to_string(),
1180 span: t.span,
1181 })
1182 }
1183 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1184 "bynk.parse.reserved_keyword",
1185 t.span,
1186 format!(
1187 "expected identifier {ctx}, but `{}` is a reserved keyword",
1188 self.slice(t.span)
1189 ),
1190 )
1191 .with_note("rename the identifier to something that is not a keyword")),
1192 Some(t) => Err(CompileError::new(
1193 "bynk.parse.expected_token",
1194 t.span,
1195 format!("expected identifier {ctx}, found {}", t.kind.describe()),
1196 )),
1197 None => Err(CompileError::new(
1198 "bynk.parse.unexpected_eof",
1199 self.eof_span(),
1200 format!("expected identifier {ctx}, found end of file"),
1201 )),
1202 }
1203 }
1204
1205 fn at_keyword_label(&self) -> bool {
1209 matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1210 && self.tokens.get(self.pos + 1).map(|t| t.kind) == Some(TokenKind::Colon)
1211 }
1212
1213 pub(crate) fn expect_label(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1219 if self.at_keyword_label() {
1220 let t = self.bump().expect("peeked a keyword");
1221 return Ok(Ident {
1222 name: self.slice(t.span).to_string(),
1223 span: t.span,
1224 });
1225 }
1226 self.expect_ident(ctx)
1227 }
1228
1229 pub(crate) fn keyword_shorthand_error(&self, pattern: bool) -> Option<CompileError> {
1241 let t = self.peek()?;
1242 if !is_reserved_keyword(t.kind) {
1243 return None;
1244 }
1245 let next = self.tokens.get(self.pos + 1).map(|t| t.kind);
1246 let shorthand = match next {
1247 Some(TokenKind::Comma | TokenKind::RBrace) => true,
1248 Some(TokenKind::DotDot) => pattern,
1249 _ => false,
1250 };
1251 if !shorthand {
1252 return None;
1253 }
1254 let word = self.slice(t.span);
1255 let mut err = CompileError::new(
1256 "bynk.parse.keyword_field_shorthand",
1257 t.span,
1258 format!("`{word}` is a reserved keyword, so it cannot be a field shorthand"),
1259 );
1260 if pattern {
1261 let taken = |name: &str| {
1262 self.tokens
1263 .iter()
1264 .any(|k| k.kind == TokenKind::Ident && self.slice(k.span) == name)
1265 };
1266 let mut binding = format!("{word}_");
1267 let mut n = 2;
1268 while taken(&binding) {
1269 binding = format!("{word}_{n}");
1270 n += 1;
1271 }
1272 err = err
1273 .with_note(format!(
1274 "the shorthand binds a variable named `{word}`; name the binding instead"
1275 ))
1276 .with_suggestion(
1277 format!("bind the `{word}` field as `{binding}`"),
1278 vec![(t.span, format!("{word}: {binding}"))],
1279 crate::error::Applicability::MaybeIncorrect,
1280 );
1281 } else {
1282 err = err.with_note(format!(
1283 "write `{word}: <value>`; a keyword names the field but cannot name a variable"
1284 ));
1285 }
1286 Some(err)
1287 }
1288
1289 pub(crate) fn expect_member_name(
1294 &mut self,
1295 ctx: &str,
1296 dot: Span,
1297 ) -> Result<Ident, CompileError> {
1298 if matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1299 && !self.next_token_on_new_line(dot)
1300 {
1301 let t = self.bump().expect("peeked a keyword");
1302 return Ok(Ident {
1303 name: self.slice(t.span).to_string(),
1304 span: t.span,
1305 });
1306 }
1307 self.expect_ident(ctx)
1308 }
1309
1310 fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1328 let first = self.take_one_doc()?;
1329 let mut docs = vec![first.clone()];
1330 while let Some(next) = self.peek()
1331 && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1332 && !has_blank_line_between(self.source, docs[docs.len() - 1].1.end, next.span.start)
1333 {
1334 docs.push(self.take_one_doc().expect("a doc token was peeked"));
1335 }
1336 if docs.len() > 1 && !self.doc_probe {
1337 self.doc_errors.push(multiple_docs(self.source, &docs));
1338 }
1339 Some(first)
1340 }
1341
1342 fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1344 match self.peek_kind()? {
1345 TokenKind::DocBlock => {
1346 let t = self.bump().unwrap();
1347 let body = doc_block_content(self.source, t.span);
1348 if !self.doc_probe && doc_content_is_code(&body) {
1349 self.doc_errors.push(doc_block_contains_code(t.span));
1350 }
1351 Some((Doc::new(body, DocForm::Block), t.span))
1352 }
1353 TokenKind::DocLine => {
1354 let t = self.bump().unwrap();
1355 let text = doc_line_text(self.source, t.span);
1356 Some((Doc::new(text, DocForm::Lines), t.span))
1357 }
1358 _ => None,
1359 }
1360 }
1361
1362 fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1375 self.collect_lead(Self::take_unit_leading_trivia)
1376 }
1377
1378 fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1381 self.collect_lead(Self::take_leading_trivia)
1382 }
1383
1384 fn collect_lead(
1385 &mut self,
1386 take: fn(&mut Self) -> Vec<Comment>,
1387 ) -> (Vec<Comment>, Option<DocLead>) {
1388 let mut leading = take(self);
1389 let doc = self.take_doc_block().map(|(content, span)| DocLead {
1390 content,
1391 span,
1392 at: leading.len(),
1393 });
1394 if doc.is_some() {
1395 leading.extend(take(self));
1396 }
1397 (leading, doc)
1398 }
1399
1400 fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1409 let (mut leading, doc) = self.collect_body_item_lead();
1410 let Some(doc) = doc else {
1411 return (leading, None);
1412 };
1413 match self.peek() {
1414 Some(t) if t.kind != close => {
1415 let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1416 (leading, doc)
1417 }
1418 _ => {
1419 self.warnings.push(CompileError::new(
1420 "bynk.parse.orphan_doc_block",
1421 doc.span,
1422 "documentation has no following member to attach to",
1423 ));
1424 keep_orphan(&mut leading, doc);
1425 (leading, None)
1426 }
1427 }
1428 }
1429
1430 fn peek_past_docs(&self) -> Option<TokenKind> {
1434 self.tokens[self.pos.min(self.tokens.len())..]
1435 .iter()
1436 .map(|t| t.kind)
1437 .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1438 }
1439
1440 fn finalize_doc(
1444 &mut self,
1445 doc: Option<DocLead>,
1446 next_span: Span,
1447 leading: &mut Vec<Comment>,
1448 ) -> Option<Doc> {
1449 let doc = doc?;
1450 let doc_span = doc.span;
1451 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1453 self.warnings.push(
1454 CompileError::new(
1455 "bynk.parse.orphan_doc_block",
1456 doc_span,
1457 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1458 )
1459 .with_note(
1460 "remove the blank line to attach the doc to the next declaration, \
1461 or remove the doc block if it is not meant to document anything",
1462 ),
1463 );
1464 keep_orphan(leading, doc);
1465 return None;
1466 }
1467 Some(doc.content)
1468 }
1469}
1470
1471pub(crate) struct DocLead {
1475 pub(crate) content: Doc,
1476 pub(crate) span: Span,
1477 pub(crate) at: usize,
1478}
1479
1480fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1484 let at = doc.at.min(leading.len());
1485 leading.insert(at, Comment::OrphanDoc(doc.content));
1486}
1487
1488fn multiple_docs(source: &str, docs: &[(Doc, Span)]) -> CompileError {
1493 let (_, first) = docs[0];
1494 let (_, last) = docs[docs.len() - 1];
1495 let mut err = CompileError::new(
1496 "bynk.parse.multiple_docs",
1497 docs[1].1,
1498 "this declaration or member already has a doc; a target has one doc",
1499 )
1500 .with_label(first, "the first doc is here")
1501 .with_note(
1502 "a doc of more than one line is a block: a `---` line, the text, and another `---` line",
1503 );
1504 let one_liners = docs
1505 .iter()
1506 .all(|(d, span)| d.form == DocForm::Lines && !doc_line_is_legacy(source, *span));
1507 if one_liners {
1508 let line_start = source[..first.start].rfind('\n').map_or(0, |i| i + 1);
1509 let indent = &source[line_start..first.start];
1510 let text = docs
1511 .iter()
1512 .map(|(d, _)| d.as_str())
1513 .collect::<Vec<_>>()
1514 .join("\n");
1515 let fence = "-".repeat(doc_fence_len(&text));
1516 let mut block = fence.clone();
1517 for line in text.lines() {
1518 block.push('\n');
1519 if !line.is_empty() {
1520 block.push_str(indent);
1521 block.push_str(line);
1522 }
1523 }
1524 block.push('\n');
1525 block.push_str(indent);
1526 block.push_str(&fence);
1527 let end = source[..last.end].trim_end_matches(['\n', '\r']).len();
1528 err = err.with_suggestion(
1529 "join the docs into one `---` block",
1530 vec![(Span::new_in(first.file, first.start, end), block)],
1531 crate::error::Applicability::MachineApplicable,
1532 );
1533 }
1534 err
1535}
1536
1537fn doc_block_contains_code(span: Span) -> CompileError {
1540 CompileError::new(
1541 "bynk.parse.doc_block_contains_code",
1542 span,
1543 "this documentation block contains declarations, so they are not compiled",
1544 )
1545 .with_note(
1546 "a line of three or more dashes opens or closes a doc block, so two dash \
1547 dividers turn the code between them into documentation; for a divider use a \
1548 line comment with text, such as `-- Helpers --`, and to show code in a doc \
1549 block put it in a Markdown code fence (```)",
1550 )
1551}
1552
1553const DECLARATION_KEYWORDS: &[TokenKind] = &[
1557 TokenKind::Type,
1558 TokenKind::Fn,
1559 TokenKind::Capability,
1560 TokenKind::Provides,
1561 TokenKind::Service,
1562 TokenKind::Agent,
1563 TokenKind::Actor,
1564 TokenKind::Messages,
1565 TokenKind::Event,
1566 TokenKind::Asset,
1567 TokenKind::Embed,
1568 TokenKind::Const,
1569 TokenKind::Uses,
1570 TokenKind::Consumes,
1571 TokenKind::Exports,
1572 TokenKind::Binding,
1573];
1574
1575fn doc_content_is_code(content: &str) -> bool {
1590 doc_content_is_code_at(content, 0)
1591}
1592
1593const DOC_PROBE_MAX_DEPTH: usize = 4;
1597
1598fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1599 let stripped = strip_code_fences(content);
1600 let tokens = match crate::lexer::tokenize(&stripped) {
1601 Ok(tokens) => tokens,
1602 Err(_) => {
1603 let unmarked = blank_marker_lines(&stripped);
1606 if unmarked == stripped {
1607 return false;
1608 }
1609 return doc_content_is_code_at(&unmarked, depth);
1610 }
1611 };
1612 if depth < DOC_PROBE_MAX_DEPTH
1613 && tokens.iter().any(|t| {
1614 t.kind == TokenKind::DocBlock
1615 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1616 })
1617 {
1618 return true;
1619 }
1620 if !tokens
1621 .iter()
1622 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1623 {
1624 return false;
1625 }
1626 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1627 let mut warnings = Vec::new();
1628 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1629 p.doc_probe = true;
1630 let at = Span::new(0, 0);
1631 let name = QualifiedName {
1632 parts: Vec::new(),
1633 span: at,
1634 };
1635 match p.parse_adapter_body(at, name, None, false) {
1636 Ok(a) => {
1637 p.peek().is_none()
1638 && (!a.items.is_empty()
1639 || !a.uses.is_empty()
1640 || !a.consumes.is_empty()
1641 || !a.exports.is_empty()
1642 || a.binding.is_some())
1643 }
1644 Err(_) => false,
1645 }
1646}
1647
1648fn blank_marker_lines(content: &str) -> String {
1651 let mut out = String::with_capacity(content.len());
1652 for line in content.lines() {
1653 let trimmed = line.trim();
1654 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1655 out.push_str(line);
1656 }
1657 out.push('\n');
1658 }
1659 out
1660}
1661
1662fn strip_code_fences(content: &str) -> String {
1667 let mut out = String::with_capacity(content.len());
1668 let mut open: Option<(char, usize)> = None;
1669 for line in content.lines() {
1670 let trimmed = line.trim();
1671 let fence = ['`', '~'].into_iter().find_map(|c| {
1672 let n = trimmed.chars().take_while(|&x| x == c).count();
1673 (n >= 3).then_some((c, n))
1674 });
1675 match (open, fence) {
1676 (None, Some(f)) => open = Some(f),
1677 (Some((c, n)), Some((fc, fnum)))
1678 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1679 {
1680 open = None;
1681 }
1682 (None, None) => out.push_str(line),
1683 (Some(_), _) => {}
1684 }
1685 out.push('\n');
1686 }
1687 out
1688}
1689
1690fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1693 crate::lexer::decode_string_literal(lexeme, span)
1694}
1695
1696pub fn is_reserved_keyword(kind: TokenKind) -> bool {
1697 use TokenKind::*;
1698 matches!(
1699 kind,
1700 Commons
1701 | Type
1702 | Fn
1703 | Where
1704 | True
1705 | False
1706 | Int
1707 | String
1708 | Bool
1709 | Let
1710 | If
1711 | Else
1712 | Ok
1713 | Err
1714 | Result
1715 | ValidationError
1716 | Enum
1717 | Match
1718 | Option
1719 | Record
1720 | Self_
1721 | Some
1722 | None
1723 | Is
1724 | Opaque
1725 | Uses
1726 | Context
1727 | Consumes
1728 | Exports
1729 | Transparent
1730 | Agent
1731 | As
1732 | Capability
1733 | Effect
1734 | Do
1735 | Given
1736 | On
1737 | Http
1738 | Provides
1739 | Stub
1740 | Service
1741 | Actor
1742 | By
1743 | Expect
1744 | Suite
1745 | Case
1746 | Float
1747 | Duration
1748 | Instant
1749 | Bytes
1750 | Decimal
1751 | JsonError
1752 | Property
1753 | Adapter
1754 | Binding
1755 | Cron
1756 | Queue
1757 | From
1758 | Protocol
1759 | Invariant
1760 | Implies
1761 | Requires
1762 | Ensures
1763 | Transition
1764 )
1765}
1766
1767fn is_item_start(kind: TokenKind) -> bool {
1778 use TokenKind::*;
1779 matches!(
1780 kind,
1781 Commons | Context | Adapter | Suite
1783 | Type | Fn | Messages | Event | Asset | Embed | Const | Uses
1785 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1787 | Binding
1789 | Stub | Case | Property
1791 )
1792}
1793
1794#[cfg(test)]
1795mod tests {
1796 use super::*;
1797 use crate::lexer::tokenize;
1798
1799 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1800 let toks = tokenize(src).map_err(|e| vec![e])?;
1801 parse(&toks, src)
1802 }
1803
1804 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1805 let toks = match tokenize(src) {
1806 Ok(t) => t,
1807 Err(e) => return (None, vec![e]),
1808 };
1809 parse_unit_with_recovery(&toks, src)
1810 }
1811
1812 #[test]
1816 fn contract_clause_name_before_next_line_brace_is_not_a_record() {
1817 for src in [
1818 "commons m\n\nconst Limit: Int = 10\n\nfn small(n: Int) -> Int\n requires small: n <= Limit\n{\n n\n}\n",
1819 "commons m\n\nfn f(n: Int) -> Int\n ensures same: result == n\n{\n n\n}\n",
1820 "commons m\n\nfn f(n: Int) -> Int\n requires r: n <= Limit {\n n\n}\n",
1821 ] {
1822 assert!(parse_str(src).is_ok(), "{src}\n{:?}", parse_str(src).err());
1823 }
1824 let src = "commons m\n\ntype P = { x: Int }\n\nfn f(p: P) -> Int\n requires r: (p == P { x: 1 })\n{\n 1\n}\n";
1826 assert!(parse_str(src).is_ok(), "{:?}", parse_str(src).err());
1827 }
1828
1829 #[test]
1832 fn contract_clause_unparenthesised_record_literal_is_rejected() {
1833 let src = "commons m\n\ntype P = { x: Int }\n\nfn f(p: P) -> Int\n requires r: p == P { x: 1 }\n{\n 1\n}\n";
1834 assert!(parse_str(src).is_err());
1835 }
1836
1837 #[test]
1843 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1844 let src =
1845 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1846 let toks = tokenize(src).unwrap();
1847 let (units, errors) = parse_units_with_recovery(&toks, src);
1848 assert!(errors.is_empty(), "{errors:?}");
1849 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1850 assert!(matches!(units[0], SourceUnit::Commons(_)));
1851 assert!(matches!(units[1], SourceUnit::Suite(_)));
1852
1853 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1855 assert!(errors.is_empty(), "{errors:?}");
1856 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1857 }
1858
1859 #[test]
1866 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1867 let toks = tokenize("").unwrap();
1868 let (units, errors) = parse_units_with_recovery(&toks, "");
1869 assert!(units.is_empty());
1870 assert!(
1871 !errors.is_empty(),
1872 "an empty file must still produce a diagnostic, not silently no units and no error"
1873 );
1874
1875 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1876 assert!(unit.is_none());
1877 assert_eq!(
1878 errors.len(),
1879 unit_errors.len(),
1880 "the singular wrapper must see the same error(s) as the plural entry point"
1881 );
1882 }
1883
1884 #[test]
1891 fn drain_check_reports_expression_interior_comments_as_undrained() {
1892 let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1893 let toks = tokenize(ordinary).unwrap();
1894 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1895 assert!(
1896 drained,
1897 "a declaration-leading comment must be fully drained"
1898 );
1899
1900 let lossy = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
1901 let toks = tokenize(lossy).unwrap();
1902 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1903 assert!(
1904 !drained,
1905 "a comment inside a binop expression must be reported as undrained"
1906 );
1907 }
1908
1909 #[test]
1910 fn eof_span_never_splits_a_multibyte_codepoint() {
1911 for src in [
1916 "commons x {\n // ends with an arrow →",
1917 "agent A {\n key k: String\n // note 🦀",
1918 "commons y {\n type T = é",
1919 ] {
1920 let (_unit, errors) = parse_recover_str(src);
1921 for e in &errors {
1922 assert!(
1923 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1924 "span {:?} splits a codepoint in {src:?}",
1925 e.span,
1926 );
1927 }
1928 }
1929 }
1930
1931 #[test]
1932 fn reserved_contextual_keywords_readable_in_expression_position() {
1933 for kw in [
1943 "asset", "case", "const", "embed", "event", "messages", "on", "suite",
1944 ] {
1945 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1946 let result = parse_str(&src);
1947 assert!(
1948 result.is_ok(),
1949 "a parameter named `{kw}` must be readable in expression position: {:?}",
1950 result.err()
1951 );
1952 }
1953 }
1954
1955 #[test]
1956 fn recovery_skips_garbage_between_decls() {
1957 let src = "commons x {\n\
1960 type A = Int where NonNegative\n\
1961 ??? !!!\n\
1962 type B = String where NonEmpty\n\
1963 }";
1964 let (unit, errors) = parse_recover_str(src);
1965 let unit = unit.expect("recovery should produce a partial AST");
1966 let SourceUnit::Commons(c) = unit else {
1967 panic!("expected commons")
1968 };
1969 let names: Vec<_> = c
1971 .items
1972 .iter()
1973 .map(|i| match i {
1974 CommonsItem::Type(t) => t.name.name.clone(),
1975 _ => panic!("expected only types"),
1976 })
1977 .collect();
1978 assert!(
1979 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1980 "expected both A and B; got {names:?}",
1981 );
1982 assert!(!errors.is_empty(), "expected at least one parse error");
1983 }
1984
1985 #[test]
1986 fn recovery_handles_bad_first_decl_then_good_second() {
1987 let src = "commons x {\n\
1989 type A Int where NonNegative\n\
1990 type B = String where NonEmpty\n\
1991 }";
1992 let (unit, errors) = parse_recover_str(src);
1993 let unit = unit.expect("recovery should produce a partial AST");
1994 let SourceUnit::Commons(c) = unit else {
1995 panic!("expected commons")
1996 };
1997 let names: Vec<_> = c
1998 .items
1999 .iter()
2000 .filter_map(|i| match i {
2001 CommonsItem::Type(t) => Some(t.name.name.clone()),
2002 _ => None,
2003 })
2004 .collect();
2005 assert!(
2006 names.contains(&"B".to_string()),
2007 "B should be parsed after A's failure; got {names:?}"
2008 );
2009 assert!(!errors.is_empty(), "expected at least one parse error");
2010 }
2011
2012 #[test]
2027 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
2028 let src = "commons m {\n fn f() -> Int\n}\n";
2029 let (unit, errors) = parse_recover_str(src);
2030 assert!(unit.is_some(), "recovery should produce a partial AST");
2031 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
2032 assert_eq!(
2033 categories.len(),
2034 1,
2035 "one syntax error, no follow-on: {categories:?}"
2036 );
2037 assert!(
2038 !categories.contains(&"bynk.parse.unexpected_eof"),
2039 "the commons' closing brace must survive: {categories:?}"
2040 );
2041 }
2042
2043 #[test]
2048 fn recovery_skips_a_rejected_item_whole() {
2049 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
2050 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
2051 assert_eq!(
2052 recovered.errors.len(),
2053 1,
2054 "one syntax error, no follow-on: {:?}",
2055 recovered
2056 .errors
2057 .iter()
2058 .map(|e| e.category)
2059 .collect::<Vec<_>>()
2060 );
2061 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
2062 panic!("expected commons")
2063 };
2064 assert!(
2065 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
2066 "the `fn g` after the skipped agent must still parse"
2067 );
2068 assert_eq!(recovered.broken_decl_names, ["Counter"]);
2069 }
2070
2071 #[test]
2072 fn recovery_skips_a_nested_blocks_own_closing_brace() {
2073 let src = "commons m {\n \
2074 fn f() -> Int {\n \
2075 match 1 {\n \
2076 is 1 -> { let z = }\n \
2077 is _ -> 2\n \
2078 }\n \
2079 }\n \
2080 fn g() -> Int { 2 }\n\
2081 }\n";
2082 let (unit, errors) = parse_recover_str(src);
2083 let unit = unit.expect("recovery should produce a partial AST");
2084 let SourceUnit::Commons(c) = unit else {
2085 panic!("expected commons")
2086 };
2087 let names: Vec<_> = c
2088 .items
2089 .iter()
2090 .filter_map(|i| match i {
2091 CommonsItem::Fn(f) => match &f.name {
2092 FnName::Free(id) => Some(id.name.clone()),
2093 _ => None,
2094 },
2095 _ => None,
2096 })
2097 .collect();
2098 assert_eq!(
2099 names,
2100 vec!["g".to_string()],
2101 "g must still be recovered as an item; got {names:?}"
2102 );
2103 assert!(
2104 !errors
2105 .iter()
2106 .any(|e| e.category == "bynk.parse.expected_unit_header"),
2107 "the outer body's own closing brace must not be mistaken for \
2108 end-of-file: {errors:?}"
2109 );
2110 }
2111
2112 #[test]
2113 fn doc_block_attaches_to_type() {
2114 let c =
2115 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
2116 .unwrap();
2117 let CommonsItem::Type(t) = &c.items[0] else {
2118 panic!()
2119 };
2120 assert!(t.documentation.is_some());
2121 assert!(
2122 t.documentation
2123 .as_ref()
2124 .unwrap()
2125 .contains("A descriptive doc.")
2126 );
2127 }
2128
2129 #[test]
2130 fn interpolated_string_parses_into_parts() {
2131 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
2133 .unwrap();
2134 let CommonsItem::Fn(f) = &c.items[0] else {
2135 panic!("expected fn")
2136 };
2137 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2138 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
2139 };
2140 assert_eq!(parts.len(), 3);
2141 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
2142 assert!(
2143 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
2144 );
2145 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
2146 }
2147
2148 #[test]
2149 fn interpolated_hole_parses_a_full_expression() {
2150 let c =
2152 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
2153 .unwrap();
2154 let CommonsItem::Fn(f) = &c.items[0] else {
2155 panic!("expected fn")
2156 };
2157 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2158 panic!("expected InterpStr")
2159 };
2160 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
2161 }
2162
2163 #[test]
2164 fn empty_interpolation_hole_is_rejected() {
2165 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
2166 assert!(
2167 errs.iter()
2168 .any(|e| e.category == "bynk.parse.empty_interpolation"),
2169 "expected empty_interpolation; got {errs:?}"
2170 );
2171 }
2172
2173 #[test]
2174 fn interpolation_hole_lex_error_span_is_rebased() {
2175 let cases = [
2181 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
2183 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
2185 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
2188 ];
2189 for src in cases {
2190 let errs = parse_str(src).unwrap_err();
2191 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
2192 for e in &errs {
2193 assert!(
2194 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
2195 "span {:?} splits a codepoint in {src:?}",
2196 e.span,
2197 );
2198 let hole_start = src.find("\\(").expect("case has a hole") + 2;
2201 assert!(
2202 e.span.start >= hole_start,
2203 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2204 e.span,
2205 );
2206 }
2207 }
2208 }
2209
2210 #[test]
2211 fn fragment_form_parses() {
2212 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2213 assert_eq!(c.form, CommonsForm::Fragment);
2214 assert_eq!(c.items.len(), 1);
2215 }
2216
2217 #[test]
2218 fn uses_parses() {
2219 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2220 assert_eq!(c.uses.len(), 1);
2221 assert_eq!(c.uses[0].target.joined(), "other.lib");
2222 }
2223
2224 #[test]
2226 fn percent_is_a_targeted_error_with_a_fix_for_simple_operands() {
2227 let err = |expr: &str| {
2228 let src = format!("commons m\n\nfn f(n: Int, k: Int) -> Int {{ {expr} }}\n");
2229 let errs = parse_str(&src).unwrap_err();
2230 assert_eq!(
2231 errs[0].category, "bynk.parse.no_remainder_operator",
2232 "{expr}"
2233 );
2234 errs.into_iter().next().unwrap()
2235 };
2236 let fix = err("n % 2");
2237 assert_eq!(fix.suggestions.len(), 1);
2238 assert_eq!(
2239 fix.notes,
2240 [
2241 "use `a.rem(b)` for the C-family remainder, which takes the dividend's sign, \
2242 or `a.mod(b)` for the modulo, which takes the divisor's sign"
2243 ]
2244 );
2245 assert_eq!(fix.suggestions[0].edits[0].1, "n.rem(2)");
2246 assert_eq!(
2247 fix.suggestions[0].applicability,
2248 crate::error::Applicability::MaybeIncorrect
2249 );
2250 assert_eq!(err("n % k").suggestions[0].edits[0].1, "n.rem(k)");
2251 for complex in [
2252 "(n + 1) % k",
2253 "n * 2 % k",
2254 "n % (k + 1)",
2255 "-n % k",
2256 "n % -k",
2257 ] {
2258 assert!(err(complex).suggestions.is_empty(), "{complex}");
2259 }
2260 }
2261
2262 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2263 let toks = tokenize(src).map_err(|e| vec![e])?;
2264 parse_unit(&toks, src)
2265 }
2266
2267 #[test]
2268 fn minimal_context_parses() {
2269 let u = parse_unit_str("context commerce.orders {}").unwrap();
2270 let SourceUnit::Context(c) = u else {
2271 panic!("expected context");
2272 };
2273 assert_eq!(c.name.joined(), "commerce.orders");
2274 assert!(c.items.is_empty());
2275 }
2276
2277 #[test]
2278 fn context_consumes_and_exports_parse() {
2279 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}";
2280 let u = parse_unit_str(src).unwrap();
2281 let SourceUnit::Context(c) = u else { panic!() };
2282 assert_eq!(c.uses.len(), 1);
2283 assert_eq!(c.consumes.len(), 1);
2284 assert_eq!(c.exports.len(), 2);
2285 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2286 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2287 }
2288
2289 #[test]
2290 fn context_fragment_form_parses() {
2291 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2292 let u = parse_unit_str(src).unwrap();
2293 let SourceUnit::Context(c) = u else { panic!() };
2294 assert_eq!(c.form, CommonsForm::Fragment);
2295 assert_eq!(c.uses.len(), 1);
2296 assert_eq!(c.consumes.len(), 1);
2297 assert_eq!(c.exports.len(), 1);
2298 }
2299
2300 #[test]
2301 fn opaque_type_parses() {
2302 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2303 let CommonsItem::Type(t) = &c.items[0] else {
2304 panic!()
2305 };
2306 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2307 }
2308
2309 #[test]
2310 fn empty_commons() {
2311 let c = parse_str("commons fitness.units {}").unwrap();
2312 assert_eq!(c.name.joined(), "fitness.units");
2313 assert!(c.items.is_empty());
2314 }
2315
2316 #[test]
2317 fn one_type_decl() {
2318 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2319 assert_eq!(c.items.len(), 1);
2320 let CommonsItem::Type(t) = &c.items[0] else {
2321 panic!()
2322 };
2323 assert_eq!(t.name.name, "Metres");
2324 match &t.body {
2325 TypeBody::Refined {
2326 base, refinement, ..
2327 } => {
2328 assert_eq!(*base, BaseType::Int);
2329 assert!(refinement.is_some());
2330 }
2331 _ => panic!("expected refined body"),
2332 }
2333 }
2334
2335 #[test]
2336 fn function_decl() {
2337 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2338 let CommonsItem::Fn(f) = &c.items[0] else {
2339 panic!()
2340 };
2341 assert_eq!(f.name.ident().name, "add");
2342 assert_eq!(f.params.len(), 2);
2343 }
2344
2345 #[test]
2346 fn chained_comparison_is_error() {
2347 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2348 .unwrap_err();
2349 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2350 }
2351
2352 #[test]
2353 fn chained_equality_is_error() {
2354 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2355 .unwrap_err();
2356 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2357 }
2358
2359 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2368 std::thread::Builder::new()
2369 .stack_size(64 * 1024 * 1024)
2370 .spawn(f)
2371 .unwrap()
2372 .join()
2373 .unwrap()
2374 }
2375
2376 #[test]
2377 fn deeply_nested_parens_are_bounded_not_overflowed() {
2378 let errs = on_big_stack(|| {
2384 let depth = crate::MAX_NESTING_DEPTH + 8;
2385 let src = format!(
2386 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2387 "(".repeat(depth),
2388 ")".repeat(depth),
2389 );
2390 parse_str(&src).unwrap_err()
2391 });
2392 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2393 }
2394
2395 #[test]
2396 fn deeply_nested_types_are_bounded_not_overflowed() {
2397 let errs = on_big_stack(|| {
2402 let depth = crate::MAX_NESTING_DEPTH + 8;
2403 let src = format!(
2404 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2405 "Result[Int, ".repeat(depth),
2406 "]".repeat(depth),
2407 );
2408 parse_str(&src).unwrap_err()
2409 });
2410 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2411 }
2412
2413 #[test]
2414 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2415 let errs = on_big_stack(|| {
2420 let depth = crate::MAX_NESTING_DEPTH + 8;
2421 let src = format!(
2422 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2423 "Ok(".repeat(depth),
2424 ")".repeat(depth),
2425 );
2426 parse_str(&src).unwrap_err()
2427 });
2428 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2429 }
2430
2431 #[test]
2432 fn nesting_below_the_limit_still_parses() {
2433 let ok = on_big_stack(|| {
2436 let depth = crate::MAX_NESTING_DEPTH - 8;
2437 let src = format!(
2438 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2439 "(".repeat(depth),
2440 ")".repeat(depth),
2441 );
2442 parse_str(&src).is_ok()
2443 });
2444 assert!(ok, "well-nested source under the limit should parse");
2445 }
2446
2447 #[test]
2448 fn let_statement_parses() {
2449 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2450 let CommonsItem::Fn(f) = &c.items[0] else {
2451 panic!()
2452 };
2453 assert_eq!(f.body.statements.len(), 1);
2454 match &f.body.statements[0] {
2455 Statement::Let(l) => {
2456 assert_eq!(l.name.name, "y");
2457 assert!(l.type_annot.is_none());
2458 }
2459 _ => panic!("expected a pure `let` statement"),
2460 }
2461 }
2462
2463 #[test]
2464 fn let_with_annotation() {
2465 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2466 let CommonsItem::Fn(f) = &c.items[0] else {
2467 panic!()
2468 };
2469 match &f.body.statements[0] {
2470 Statement::Let(l) => assert!(l.type_annot.is_some()),
2471 _ => panic!("expected a pure `let` statement"),
2472 }
2473 }
2474
2475 #[test]
2476 fn if_else_parses_as_expression() {
2477 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2478 let CommonsItem::Fn(f) = &c.items[0] else {
2479 panic!()
2480 };
2481 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2482 }
2483
2484 #[test]
2485 fn else_if_chain_parses() {
2486 let c = parse_str(
2487 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2488 )
2489 .unwrap();
2490 let CommonsItem::Fn(f) = &c.items[0] else {
2491 panic!()
2492 };
2493 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2494 panic!()
2495 };
2496 assert!(else_block.statements.is_empty());
2498 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2499 }
2500
2501 #[test]
2502 fn ok_and_err_parse_as_expressions() {
2503 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2504 let CommonsItem::Fn(f) = &c.items[0] else {
2505 panic!()
2506 };
2507 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2508
2509 let c =
2510 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2511 let CommonsItem::Fn(f) = &c.items[0] else {
2512 panic!()
2513 };
2514 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2515 }
2516
2517 #[test]
2518 fn question_postfix_parses() {
2519 let c = parse_str(
2520 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2521 )
2522 .unwrap();
2523 let CommonsItem::Fn(f) = &c.items[1] else {
2524 panic!()
2525 };
2526 let Statement::Let(l) = &f.body.statements[0] else {
2527 panic!("expected a pure `let` statement");
2528 };
2529 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2530 }
2531
2532 #[test]
2533 fn constructor_call_parses() {
2534 let c = parse_str(
2535 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2536 )
2537 .unwrap();
2538 let CommonsItem::Fn(f) = &c.items[1] else {
2539 panic!()
2540 };
2541 let ExprKind::MethodCall {
2544 receiver, method, ..
2545 } = &f.body.tail.kind
2546 else {
2547 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2548 };
2549 let ExprKind::Ident(id) = &receiver.kind else {
2550 panic!("expected receiver Ident");
2551 };
2552 assert_eq!(id.name, "T");
2553 assert_eq!(method.name, "of");
2554 }
2555
2556 #[test]
2557 fn result_type_ref_parses() {
2558 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2559 let CommonsItem::Fn(f) = &c.items[0] else {
2560 panic!()
2561 };
2562 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2563 }
2564
2565 #[test]
2566 fn result_missing_arg_count_errors() {
2567 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2568 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2569 }
2570
2571 #[test]
2572 fn field_access_parses_in_v0_2() {
2573 let c =
2576 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2577 let CommonsItem::Fn(f) = &c.items[1] else {
2578 panic!()
2579 };
2580 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2581 }
2582
2583 #[test]
2586 fn leading_line_comment_attaches_to_next_decl() {
2587 let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2588 let c = parse_str(src).unwrap();
2589 let CommonsItem::Type(t) = &c.items[0] else {
2590 panic!()
2591 };
2592 assert_eq!(
2593 t.trivia.leading,
2594 vec![Comment::Line(" explain the type".to_string())]
2595 );
2596 assert!(t.trivia.trailing.is_none());
2597 }
2598
2599 #[test]
2600 fn trailing_line_comment_attaches_to_prev_decl() {
2601 let src = "commons x {\ntype T = Int where NonNegative // trailing note\n}";
2602 let c = parse_str(src).unwrap();
2603 let CommonsItem::Type(t) = &c.items[0] else {
2604 panic!()
2605 };
2606 assert!(t.trivia.leading.is_empty());
2607 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2608 }
2609
2610 #[test]
2611 fn grouped_leading_comments_attach_together() {
2612 let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2613 let c = parse_str(src).unwrap();
2614 let CommonsItem::Type(t) = &c.items[0] else {
2615 panic!()
2616 };
2617 assert_eq!(
2618 t.trivia.leading,
2619 vec![
2620 Comment::Line(" one".to_string()),
2621 Comment::Line(" two".to_string()),
2622 Comment::Line(" three".to_string())
2623 ],
2624 );
2625 }
2626
2627 #[test]
2628 fn comment_with_doc_block_keeps_both() {
2629 let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2631 let c = parse_str(src).unwrap();
2632 let CommonsItem::Type(t) = &c.items[0] else {
2633 panic!()
2634 };
2635 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2636 assert_eq!(t.documentation.as_deref(), Some("docs"));
2637 }
2638
2639 #[test]
2640 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2641 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2647 let c = parse_str(src).unwrap();
2648 assert_eq!(c.name.joined(), "app.messages");
2649 }
2650
2651 #[test]
2652 fn messages_decl_parses_tag_annotation_and_entries() {
2653 let src = "commons app.messages {\n\
2655 // intro\n\
2656 ---\n\
2657 docs\n\
2658 ---\n\
2659 messages \"en\" @reference {\n\
2660 \"greeting\" => \"Hello, {name}!\"\n\
2661 \"farewell\" => \"Bye\"\n\
2662 } // trailing\n\
2663 }";
2664 let c = parse_str(src).unwrap();
2665 let CommonsItem::Messages(m) = &c.items[0] else {
2666 panic!("expected a messages item, got {:?}", c.items[0]);
2667 };
2668 assert_eq!(m.tag, "en");
2669 assert_eq!(m.annotations.len(), 1);
2670 assert_eq!(m.annotations[0].name.name, "reference");
2671 assert!(m.annotations[0].args.is_empty());
2672 assert_eq!(m.entries.len(), 2);
2673 assert_eq!(m.entries[0].code, "greeting");
2674 assert_eq!(m.entries[0].template, "Hello, {name}!");
2675 assert_eq!(m.entries[1].code, "farewell");
2676 assert_eq!(m.entries[1].template, "Bye");
2677 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2678 assert_eq!(m.documentation.as_deref(), Some("docs"));
2679 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2680 }
2681
2682 #[test]
2683 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2684 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2688 let c = parse_str(src).unwrap();
2689 let CommonsItem::Messages(m) = &c.items[0] else {
2690 panic!("expected a messages item, got {:?}", c.items[0]);
2691 };
2692 assert_eq!(m.tag, "en");
2693 assert!(m.annotations.is_empty());
2694 assert!(m.entries.is_empty());
2695 }
2696
2697 #[test]
2698 fn messages_decl_parses_syntactically_inside_a_context_too() {
2699 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2704 let toks = tokenize(src).unwrap();
2705 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2706 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2707 let Some(SourceUnit::Context(ctx)) = unit else {
2708 panic!("expected a context")
2709 };
2710 let CommonsItem::Messages(m) = &ctx.items[0] else {
2711 panic!("expected a messages item, got {:?}", ctx.items[0]);
2712 };
2713 assert_eq!(m.tag, "en");
2714 }
2715
2716 #[test]
2717 fn comment_before_let_statement_attaches() {
2718 let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2719 let c = parse_str(src).unwrap();
2720 let CommonsItem::Fn(f) = &c.items[0] else {
2721 panic!()
2722 };
2723 let Statement::Let(l) = &f.body.statements[0] else {
2724 panic!()
2725 };
2726 assert_eq!(
2727 l.trivia.leading,
2728 vec![Comment::Line(" pick a value".to_string())]
2729 );
2730 }
2731
2732 #[test]
2733 fn comment_before_tail_attaches_to_block_tail() {
2734 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2735 let c = parse_str(src).unwrap();
2736 let CommonsItem::Fn(f) = &c.items[0] else {
2737 panic!()
2738 };
2739 assert_eq!(
2740 f.body.tail_leading_comments,
2741 vec![Comment::Line(" result".to_string())],
2742 );
2743 }
2744
2745 #[test]
2751 fn contextual_keywords_are_valid_identifiers() {
2752 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2754 .expect("`on`/`suite`/`case` are valid field names");
2755 let CommonsItem::Type(_) = &c.items[0] else {
2756 panic!("expected a type decl")
2757 };
2758
2759 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2761 .expect("`on`/`case` are valid parameter names");
2762
2763 parse_str("commons demo {\n type R = { suite: Int }\n}")
2765 .expect("`suite` is a valid field name");
2766 }
2767
2768 #[test]
2771 fn keywords_are_labels_and_members_not_bindings() {
2772 for src in [
2773 "commons demo {\n type A = { type: String, from: Int, self: Bool }\n}",
2774 "commons demo {\n type S = | P(from: String, type: String)\n}",
2775 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { a.type }\n}",
2776 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type: \"x\" } }\n}",
2777 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { match a { A { type: t, .. } => t } }\n}",
2778 "commons demo {\n type S = | P(from: String)\n fn f(s: S) -> String { match s { P(from: x) => x } }\n}",
2779 "commons demo {\n type S = { n: Int }\n fn S.from(n: Int) -> S { S { n: n } }\n fn g() -> S { S.from(1) }\n}",
2780 ] {
2781 parse_str(src).unwrap_or_else(|e| panic!("should parse: {src}\n{e:?}"));
2782 }
2783 for src in [
2784 "commons demo {\n fn f(type: Int) -> Int { 1 }\n}",
2785 "commons demo {\n fn f() -> Int { let from = 1\n from }\n}",
2786 "commons demo {\n type A = { type String }\n}",
2787 "commons demo {\n fn f(a: Int) -> Int {\n a.\n let x = 1\n x\n }\n}",
2789 ] {
2790 let errs = parse_str(src).expect_err(src);
2791 assert_eq!(errs[0].category, "bynk.parse.reserved_keyword", "{src}");
2792 }
2793 }
2794
2795 #[test]
2799 fn keyword_shorthand_is_a_targeted_error() {
2800 let pat = |extra: &str| {
2801 format!(
2802 "commons demo {{\n type A = {{ type: String }}\n fn f(a: A) -> String {{ {extra} match a {{ A {{ type, .. }} => \"x\" }} }}\n}}"
2803 )
2804 };
2805 let errs = parse_str(&pat("")).expect_err("pattern shorthand");
2806 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2807 let fix = &errs[0].suggestions[0];
2808 assert_eq!(fix.edits[0].1, "type: type_");
2809 assert!(matches!(
2810 fix.applicability,
2811 crate::error::Applicability::MaybeIncorrect
2812 ));
2813
2814 let errs = parse_str(&pat("let type_ = 1")).expect_err("pattern shorthand");
2815 assert_eq!(errs[0].suggestions[0].edits[0].1, "type: type_2");
2816
2817 let errs = parse_str(
2818 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type } }\n}",
2819 )
2820 .expect_err("construction shorthand");
2821 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2822 assert!(errs[0].suggestions.is_empty());
2823 }
2824
2825 #[test]
2833 fn is_reserved_keyword_covers_every_lexer_keyword() {
2834 let lexer_src = include_str!("lexer.rs");
2835 let mut words = Vec::new();
2836 for line in lexer_src.lines() {
2837 let t = line.trim();
2838 if let Some(rest) = t.strip_prefix("#[token(\"")
2839 && let Some(word) = rest.split('"').next()
2840 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2841 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2842 {
2843 words.push(word.to_string());
2844 }
2845 }
2846 assert!(
2847 words.len() > 30,
2848 "keyword extraction looks broken: only {} words",
2849 words.len()
2850 );
2851 use crate::keywords::RESERVED_CONTEXTUAL;
2854 let mut unclassified = Vec::new();
2855 for word in &words {
2856 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2857 let kind = tokens.first().expect("keyword yields a token").kind;
2858 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2859 unclassified.push(word.clone());
2860 }
2861 }
2862 assert!(
2863 unclassified.is_empty(),
2864 "keywords missing from is_reserved_keyword (add them, or document \
2865 them as contextual): {unclassified:?}"
2866 );
2867 }
2868
2869 #[test]
2880 fn is_item_start_matches_the_pinned_keyword_set() {
2881 use TokenKind::*;
2882 let expected_true = [
2883 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Embed, Const, Uses,
2884 Consumes, Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case,
2885 Property,
2886 ];
2887 for kind in expected_true {
2888 assert!(is_item_start(kind), "{kind:?} must be an item start");
2889 }
2890 let expected_false = [
2891 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2892 Where, True, False, Match, Is, On, Given,
2893 ];
2894 for kind in expected_false {
2895 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2896 }
2897 }
2898
2899 #[test]
2904 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2905 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2906 let tokens = crate::lexer::tokenize(src).unwrap();
2907 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2908 assert!(unit.is_some(), "the commons header still parses");
2909 assert!(
2910 errors
2911 .iter()
2912 .any(|e| e.category == "bynk.capability.outside_context"),
2913 "the misplaced capability is reported: {errors:?}"
2914 );
2915 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2918 }
2919
2920 #[test]
2921 fn trailing_file_comment_becomes_unit_trailing() {
2922 let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2926 let c = parse_str(src).unwrap();
2927 assert_eq!(
2928 c.trailing_comments,
2929 vec![Comment::Line(" afterword".to_string())]
2930 );
2931 }
2932
2933 #[test]
2934 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2935 let src = "commons x {\n type T = Int where Positive\n}\n// afterword\n";
2942 let c = parse_str(src).unwrap();
2943 assert_eq!(
2944 c.trailing_comments,
2945 vec![Comment::Line(" afterword".to_string())]
2946 );
2947 }
2948
2949 #[test]
2950 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2951 let src = "context x {\n type T = Int where Positive\n}\n// afterword\n";
2953 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2954 panic!("expected context");
2955 };
2956 assert_eq!(
2957 c.trailing_comments,
2958 vec![Comment::Line(" afterword".to_string())]
2959 );
2960 }
2961
2962 #[test]
2963 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2964 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n// afterword\n";
2966 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2967 panic!("expected suite");
2968 };
2969 assert_eq!(
2970 s.trailing_comments,
2971 vec![Comment::Line(" afterword".to_string())]
2972 );
2973 }
2974
2975 #[test]
2982 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2983 let src = "adapter x {\n binding \"./x.ts\"\n}\n// afterword\n";
2984 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2985 panic!("expected adapter");
2986 };
2987 assert_eq!(
2988 a.trailing_comments,
2989 vec![Comment::Line(" afterword".to_string())]
2990 );
2991 }
2992
2993 #[test]
2996 fn binding_includes_list_parses_before_requires() {
2997 let src = "adapter x {\n binding \"./x.ts\" includes [\"./a.wasm\", \"b/c.wasm\"] requires { \"p\": \"^1\" }\n}\n";
2998 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2999 panic!("expected adapter");
3000 };
3001 let b = a.binding.unwrap();
3002 let paths: Vec<_> = b.includes.iter().map(|i| i.path.as_str()).collect();
3003 assert_eq!(paths, ["./a.wasm", "b/c.wasm"]);
3004 assert_eq!(b.requires.len(), 1);
3005 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [\"a.wasm\"").is_err());
3006 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [a]\n}\n").is_err());
3007 }
3008
3009 #[test]
3016 fn commons_fragment_rejects_uses_after_a_decl() {
3017 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
3018 let errs = parse_str(src).unwrap_err();
3019 assert!(
3020 errs.iter()
3021 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3022 "{errs:?}"
3023 );
3024 }
3025
3026 #[test]
3029 fn commons_brace_allows_uses_after_a_decl() {
3030 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
3031 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3032 }
3033
3034 #[test]
3036 fn context_fragment_rejects_consumes_after_a_decl() {
3037 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
3038 let errs = parse_unit_str(src).unwrap_err();
3039 assert!(
3040 errs.iter()
3041 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
3042 "{errs:?}"
3043 );
3044 }
3045
3046 #[test]
3048 fn context_fragment_rejects_exports_after_a_decl() {
3049 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
3050 let errs = parse_unit_str(src).unwrap_err();
3051 assert!(
3052 errs.iter()
3053 .any(|e| e.category == "bynk.parse.exports_after_decls"),
3054 "{errs:?}"
3055 );
3056 }
3057
3058 #[test]
3060 fn context_brace_allows_consumes_and_exports_after_a_decl() {
3061 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
3062 parse_unit_str(src)
3063 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
3064 }
3065
3066 #[test]
3068 fn test_fragment_rejects_uses_after_a_decl() {
3069 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
3070 let errs = parse_unit_str(src).unwrap_err();
3071 assert!(
3072 errs.iter()
3073 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3074 "{errs:?}"
3075 );
3076 }
3077
3078 #[test]
3080 fn test_brace_allows_uses_after_a_decl() {
3081 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
3082 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3083 }
3084
3085 fn body_tail(body: &str) -> ExprKind {
3089 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3090 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3091 let CommonsItem::Fn(f) = &c.items[0] else {
3092 panic!("expected fn, got {:?}", c.items[0]);
3093 };
3094 f.body.tail.kind.clone()
3095 }
3096
3097 fn body_err(body: &str) -> Vec<CompileError> {
3098 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3099 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
3100 }
3101
3102 fn apply_fix(src: &str, err: &CompileError) -> String {
3106 let s = &err.suggestions[0];
3107 assert_eq!(
3108 s.applicability,
3109 crate::error::Applicability::MachineApplicable
3110 );
3111 let mut edits = s.edits.clone();
3112 edits.sort_by_key(|(span, _)| std::cmp::Reverse(span.start));
3113 let mut out = src.to_string();
3114 for (span, text) in edits {
3115 out.replace_range(span.start..span.end, &text);
3116 }
3117 out
3118 }
3119
3120 #[test]
3123 fn list_patterns_parse_in_every_shape() {
3124 let ExprKind::Match { arms, .. } = body_tail(
3125 "match xs { [] => 0\n [a] => 1\n [a, b, ..] => 2\n [a, ..rest] => 3\n [..all] => 4\n [..] => 5\n [a, ..,] => 6\n Some([x, ..]) => 7 }",
3126 ) else {
3127 panic!("expected Match");
3128 };
3129 let shape = |i: usize| match &arms[i].pattern {
3130 Pattern::List { elems, rest, .. } => (
3131 elems.len(),
3132 rest.as_ref()
3133 .map(|r| r.binder.as_ref().map(|b| b.name.clone())),
3134 ),
3135 other => panic!("arm {i}: expected a list pattern, got {other:?}"),
3136 };
3137 assert_eq!(shape(0), (0, None));
3138 assert_eq!(shape(1), (1, None));
3139 assert_eq!(shape(2), (2, Some(None)));
3140 assert_eq!(shape(3), (1, Some(Some("rest".to_string()))));
3141 assert_eq!(shape(4), (0, Some(Some("all".to_string()))));
3142 assert_eq!(shape(5), (0, Some(None)));
3143 assert_eq!(shape(6), (1, Some(None)));
3144 assert!(matches!(&arms[7].pattern, Pattern::Variant { bindings, .. }
3145 if matches!(bindings[0].pattern(), Pattern::List { .. })));
3146 assert!(!arms[0].pattern.is_irrefutable());
3148 assert!(!arms[2].pattern.is_irrefutable());
3149 assert!(arms[4].pattern.is_irrefutable());
3150 assert!(arms[5].pattern.is_irrefutable());
3151 }
3152
3153 #[test]
3154 fn list_pattern_after_is_closes_at_the_bracket() {
3155 for body in [
3156 "if xs is [_, ..] { 1 } else { 2 }",
3157 "if xs is [] { 1 } else { 2 }",
3158 "if xs is [a, ..rest] { 1 } else { 2 }",
3159 "if xs is [S { n: 1, .. }, ..] { 1 } else { 2 }",
3160 "if xs is [] | [_] { 1 } else { 2 }",
3161 ] {
3162 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3163 parse_str(&src).unwrap_or_else(|e| panic!("{body:?}: {e:?}"));
3164 }
3165 }
3166
3167 #[test]
3168 fn list_rest_must_be_last() {
3169 for body in [
3170 "match xs { [.., last] => 1 }",
3171 "match xs { [..r, last] => 1 }",
3172 "match xs { [.., ..] => 1 }",
3173 "match xs { [a, .., b, c] => 1 }",
3174 ] {
3175 let errs = body_err(body);
3176 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last", "{body}");
3177 assert!(
3178 errs[0].suggestions.is_empty(),
3179 "{body}: only trailing wildcards earn a fix"
3180 );
3181 }
3182 }
3183
3184 #[test]
3185 fn list_rest_followed_by_wildcards_has_a_fix() {
3186 for (body, fixed) in [
3187 ("match xs { [.., _] => 1 }", "match xs { [_, ..] => 1 }"),
3188 (
3189 "match xs { [a, .., _, _] => 1 }",
3190 "match xs { [a, _, _, ..] => 1 }",
3191 ),
3192 ] {
3193 let errs = body_err(body);
3194 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last");
3195 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3196 let got = apply_fix(&src, &errs[0]);
3197 assert!(got.contains(fixed), "{body}: fixed to {got:?}");
3198 parse_str(&got).unwrap_or_else(|e| panic!("{body:?} fixed to {got:?}: {e:?}"));
3199 }
3200 }
3201
3202 #[test]
3203 fn list_rest_followed_by_an_integer_is_a_range_element() {
3204 for body in [
3206 "match xs { [..9] => 1 _ => 0 }",
3207 "match xs { [a, ..-1] => 1 _ => 0 }",
3208 "match xs { [0..9, ..rest] => 1 _ => 0 }",
3209 "match xs { [18.., ..] => 1 _ => 0 }",
3210 ] {
3211 let ExprKind::Match { arms, .. } = body_tail(body) else {
3212 panic!("expected Match: {body}");
3213 };
3214 let Pattern::List { elems, .. } = &arms[0].pattern else {
3215 panic!("expected a list pattern: {body}");
3216 };
3217 assert!(
3218 elems.iter().any(|e| matches!(e, Pattern::Range(_))),
3219 "{body}"
3220 );
3221 }
3222 for body in ["match xs { [..\"a\"] => 1 }", "match xs { [..true] => 1 }"] {
3224 let errs = body_err(body);
3225 assert_eq!(errs[0].category, "bynk.parse.expected_token", "{body}");
3226 }
3227 let errs = body_err("match xs { [a, .._] => 1 }");
3229 assert_eq!(errs[0].category, "bynk.parse.expected_token");
3230 }
3231
3232 #[test]
3233 fn int_range_patterns_parse_in_every_form() {
3234 let range = |body: &str| {
3235 let ExprKind::Match { arms, .. } = body_tail(body) else {
3236 panic!("expected Match: {body}");
3237 };
3238 match &arms[0].pattern {
3239 Pattern::Range(r) => (r.lo, r.hi),
3240 other => panic!("expected a range: {body}: {other:?}"),
3241 }
3242 };
3243 assert_eq!(range("match n { 0..12 => 1 _ => 0 }"), (Some(0), Some(12)));
3244 assert_eq!(range("match n { 18.. => 1 _ => 0 }"), (Some(18), None));
3245 assert_eq!(range("match n { ..-1 => 1 _ => 0 }"), (None, Some(-1)));
3246 assert_eq!(
3247 range("match n { -5..-1 => 1 _ => 0 }"),
3248 (Some(-5), Some(-1))
3249 );
3250 assert_eq!(
3251 range("match n { 1_000..2_000 => 1 _ => 0 }"),
3252 (Some(1000), Some(2000))
3253 );
3254 let ExprKind::Match { arms, .. } = body_tail("match n { 18.. => 1 5 => 2 _ => 0 }") else {
3256 panic!("expected Match");
3257 };
3258 assert!(matches!(arms[0].pattern, Pattern::Range(_)));
3259 assert!(matches!(arms[1].pattern, Pattern::Literal { .. }));
3260 }
3261
3262 #[test]
3263 fn a_range_is_not_an_event_pattern_value() {
3264 let src = "service S from Events(E { n: 1..5 }) { }";
3266 assert!(parse_str(src).is_err());
3267 }
3268
3269 #[test]
3270 fn record_pattern_parses_in_a_match_arm_and_nested() {
3271 let ExprKind::Match { arms, .. } = body_tail(
3272 "match o { Order { status: Paid, total, .. } => 1 Some(Order { .. }) => 2 _ => 3 }",
3273 ) else {
3274 panic!("expected Match");
3275 };
3276 let Pattern::Record {
3277 type_name, fields, ..
3278 } = &arms[0].pattern
3279 else {
3280 panic!("expected a record pattern, got {:?}", arms[0].pattern);
3281 };
3282 assert_eq!(type_name.name, "Order");
3283 assert_eq!(fields.len(), 2);
3284 assert!(fields[0].1.is_some(), "`status: Paid` has a sub-pattern");
3285 assert!(fields[1].1.is_none(), "`total` is shorthand");
3286 let Pattern::Variant { bindings, .. } = &arms[1].pattern else {
3287 panic!("expected Some(..)");
3288 };
3289 assert!(matches!(bindings[0].pattern(), Pattern::Record { .. }));
3290 }
3291
3292 #[test]
3293 fn record_pattern_without_rest_is_an_error_with_a_fix() {
3294 for body in [
3295 "match o { Order { total } => 1 }",
3296 "match o { Order { total, } => 1 }",
3297 "match o { Order { status: Paid, total } => 1 }",
3298 "match o { Order { } => 1 }",
3299 ] {
3300 let errs = body_err(body);
3301 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3302 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3303 let fixed = apply_fix(&src, &errs[0]);
3304 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3305 }
3306 }
3307
3308 #[test]
3309 fn record_pattern_after_is_must_be_parenthesised() {
3310 for body in [
3311 "if o is Order { status: Paid, .. } { 1 } else { 2 }",
3312 "if o is None | Order { .. } { 1 } else { 2 }",
3313 ] {
3314 let errs = body_err(body);
3315 assert_eq!(
3316 errs[0].category,
3317 "bynk.parse.record_pattern_in_is_unparenthesised"
3318 );
3319 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3320 if body.contains("None |") {
3321 continue; }
3323 let fixed = apply_fix(&src, &errs[0]);
3324 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3325 }
3326 for body in [
3328 "if o is (Order { status: Paid, .. }) { 1 } else { 2 }",
3329 "if o is Some(Order { .. }) { 1 } else { 2 }",
3330 "if o is (None | Order { .. }) { 1 } else { 2 }",
3331 "if o is Paid { 1 } else { 2 }",
3332 ] {
3333 body_tail(body);
3334 }
3335 }
3336
3337 #[test]
3338 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
3339 for src in [
3342 "if ready { result } else { fallback }",
3343 "if ready { fallback } else { result }",
3344 "if !ready { result } else { fallback }",
3345 "if a == b { result } else { fallback }",
3346 "if a && b { result } else { fallback }",
3347 ] {
3348 let ExprKind::If {
3349 then_block,
3350 else_block,
3351 ..
3352 } = body_tail(src)
3353 else {
3354 panic!("expected If for {src:?}, got {:?}", body_tail(src));
3355 };
3356 assert!(
3359 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
3360 "then-branch tail not an ident for {src:?}: {:?}",
3361 then_block.tail.kind,
3362 );
3363 assert!(
3364 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
3365 "else-branch tail not an ident for {src:?}: {:?}",
3366 else_block.tail.kind,
3367 );
3368 }
3369 }
3370
3371 #[test]
3372 fn else_less_if_with_single_ident_branch_parses() {
3373 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
3375 panic!("expected If");
3376 };
3377 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
3378 }
3379
3380 #[test]
3381 fn record_construction_still_parses_in_value_position() {
3382 assert!(matches!(
3385 body_tail("Point { x }"),
3386 ExprKind::RecordConstruction { .. }
3387 ));
3388 assert!(matches!(
3389 body_tail("Point { x: 1, y: 2 }"),
3390 ExprKind::RecordConstruction { .. }
3391 ));
3392 assert!(matches!(
3393 body_tail("Empty {}"),
3394 ExprKind::RecordConstruction { .. }
3395 ));
3396 }
3397
3398 #[test]
3399 fn parenthesised_record_is_allowed_in_condition_head() {
3400 let ExprKind::If { cond, .. } =
3403 body_tail("if (ready { result }) { branch } else { other }")
3404 else {
3405 panic!("expected If");
3406 };
3407 let ExprKind::Paren(inner) = &cond.kind else {
3408 panic!("expected a parenthesised condition, got {:?}", cond.kind);
3409 };
3410 assert!(
3411 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
3412 "parenthesised record in condition head should still construct: {:?}",
3413 inner.kind,
3414 );
3415 }
3416
3417 #[test]
3418 fn record_in_call_arg_within_condition_still_constructs() {
3419 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
3422 else {
3423 panic!("expected If");
3424 };
3425 let ExprKind::Call { args, .. } = &cond.kind else {
3426 panic!("expected Call in condition, got {:?}", cond.kind);
3427 };
3428 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
3429 }
3430
3431 #[test]
3432 fn safe_condition_shapes_are_unaffected() {
3433 assert!(matches!(
3435 body_tail("if ready == true { result } else { fallback }"),
3436 ExprKind::If { .. }
3437 ));
3438 assert!(matches!(
3439 body_tail("if (ready) { result } else { fallback }"),
3440 ExprKind::If { .. }
3441 ));
3442 assert!(matches!(
3443 body_tail("if ready { \"a\" } else { \"b\" }"),
3444 ExprKind::If { .. }
3445 ));
3446 }
3447
3448 fn body_block(body: &str) -> Block {
3449 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3450 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3451 let CommonsItem::Fn(f) = &c.items[0] else {
3452 panic!("expected fn, got {:?}", c.items[0]);
3453 };
3454 f.body.clone()
3455 }
3456
3457 fn has_error(errs: &[CompileError], code: &str) -> bool {
3458 errs.iter().any(|e| e.category == code)
3459 }
3460
3461 fn lambda_of(body: &str) -> LambdaExpr {
3464 let b = body_block(body);
3465 let ExprKind::Lambda(l) = &b.tail.kind else {
3466 panic!("expected a lambda tail, got {:?}", b.tail.kind);
3467 };
3468 l.clone()
3469 }
3470
3471 #[test]
3472 fn a_record_pattern_parameter_desugars_to_a_hidden_binder_and_a_let() {
3473 let l = lambda_of("(Item { qty, .. }) => qty");
3475 assert_eq!(l.params.len(), 1);
3476 let p = &l.params[0];
3477 assert!(p.name.name.starts_with(LAMBDA_ARG_BINDER_PREFIX));
3478 assert!(is_generated_binding(&p.name.name));
3479 assert_eq!(
3480 p.name.span.start, p.name.span.end,
3481 "the binder has no extent"
3482 );
3483 assert!(p.type_ref.is_none());
3484 assert!(matches!(p.pattern, Some(Pattern::Record { .. })));
3485 assert!(!l.block_body);
3486 let ExprKind::Block(b) = &l.body.kind else {
3487 panic!("expected a block body, got {:?}", l.body.kind);
3488 };
3489 let Statement::Let(hidden) = &b.statements[0] else {
3490 panic!("expected the hidden `let`, got {:?}", b.statements[0]);
3491 };
3492 assert_eq!(
3493 hidden.let_else.as_ref().map(|le| le.form),
3494 Some(LetElseForm::Plain)
3495 );
3496 assert!(b.tail.is_let_else_match());
3497 assert!(
3498 matches!(&hidden.value.kind, ExprKind::Ident(i) if i.name == p.name.name),
3499 "the `let` destructures the hidden parameter"
3500 );
3501 }
3502
3503 #[test]
3504 fn name_and_pattern_parameters_mix_and_each_binder_is_distinct() {
3505 let l = lambda_of("(a, Item { qty, .. }, b: Int, Box { v, .. }) => a");
3506 let names: Vec<_> = l.params.iter().map(|p| p.name.name.as_str()).collect();
3507 assert_eq!(names[0], "a");
3508 assert_eq!(names[2], "b");
3509 assert!(names[1].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3510 assert!(names[3].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3511 assert_ne!(names[1], names[3]);
3512 assert!(l.params[0].pattern.is_none() && l.params[2].type_ref.is_some());
3513 let ExprKind::Block(outer) = &l.body.kind else {
3515 panic!("expected a block body");
3516 };
3517 let ExprKind::Match { arms, .. } = &outer.tail.kind else {
3518 panic!("expected the first pattern's match");
3519 };
3520 let MatchBody::Block(inner) = &arms[0].body else {
3521 panic!("expected the arm's block");
3522 };
3523 assert!(inner.tail.is_let_else_match(), "the second pattern nests");
3524 }
3525
3526 #[test]
3527 fn a_block_body_is_kept_as_the_pattern_arms_block() {
3528 let l = lambda_of("(Item { qty, .. }) => { let x = qty\n x }");
3529 assert!(l.block_body);
3530 }
3531
3532 #[test]
3533 fn a_lambda_without_pattern_parameters_is_unchanged() {
3534 let l = lambda_of("(a, b: Int) => a");
3535 assert!(l.params.iter().all(|p| p.pattern.is_none()));
3536 assert!(matches!(l.body.kind, ExprKind::Ident(_)));
3537 }
3538
3539 #[test]
3540 fn a_lambda_pattern_parameter_that_is_refutable_names_its_first_failing_field() {
3541 let errs = body_err("(Item { status: Paid, qty, .. }) => qty");
3542 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_refutable");
3543 assert!(
3544 errs[0].notes.iter().any(|n| n.contains("status: Paid")),
3545 "{:?}",
3546 errs[0].notes
3547 );
3548 }
3549
3550 #[test]
3551 fn a_lambda_pattern_parameter_must_be_a_record_pattern() {
3552 for body in [
3553 "(Some(x)) => x",
3554 "(a, [x, ..]) => x",
3555 "(0..9) => 1",
3556 "(1) => 1",
3557 "(None) => 1",
3558 "(Status.Paid) => 1",
3559 ] {
3560 let errs = body_err(body);
3561 assert_eq!(
3562 errs[0].category, "bynk.parse.lambda_pattern_not_record",
3563 "{body}"
3564 );
3565 }
3566 }
3567
3568 #[test]
3569 fn an_annotated_lambda_pattern_parameter_offers_to_remove_the_annotation() {
3570 let body = "(Item { qty, .. }: Item) => qty";
3571 let errs = body_err(body);
3572 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_annotation");
3573 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3574 let fixed = apply_fix(&src, &errs[0]);
3575 assert!(fixed.contains("(Item { qty, .. }) => qty"), "{fixed}");
3576 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3577 }
3578
3579 #[test]
3580 fn a_missing_rest_in_a_lambda_pattern_parameter_has_slice_ones_fix() {
3581 let body = "(a, Item { qty }) => qty";
3582 let errs = body_err(body);
3583 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3584 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3585 let fixed = apply_fix(&src, &errs[0]);
3586 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3587 }
3588
3589 #[test]
3590 fn let_else_desugars_to_a_hidden_binding_and_a_match() {
3591 let b = body_block("let x = 1\n let Some(n) = find(x) else { 0 }\n n + x");
3594 assert_eq!(
3595 b.statements.len(),
3596 2,
3597 "the plain let and the hidden binding"
3598 );
3599 let Statement::Let(hidden) = &b.statements[1] else {
3600 panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
3601 };
3602 assert!(
3603 is_generated_binding(&hidden.name.name),
3604 "{}",
3605 hidden.name.name
3606 );
3607 assert_eq!(
3608 hidden.name.span.start, hidden.name.span.end,
3609 "the binder has no extent"
3610 );
3611 let le = hidden
3612 .let_else
3613 .as_ref()
3614 .expect("the binding records the sugar");
3615 assert_eq!(le.form, LetElseForm::Block);
3616 let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
3617 panic!("expected a match tail, got {:?}", b.tail.kind);
3618 };
3619 assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
3620 assert_eq!(arms.len(), 2);
3621 assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
3622 let MatchBody::Block(rest) = &arms[0].body else {
3623 panic!("the pattern's arm runs the rest of the block");
3624 };
3625 assert!(rest.statements.is_empty());
3626 assert!(
3627 !matches!(rest.tail.kind, ExprKind::UnitLit),
3628 "{:?}",
3629 rest.tail.kind
3630 );
3631 assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
3632 }
3633
3634 #[test]
3636 fn par_block_holds_its_statements_in_source_order() {
3637 let b = body_block(
3638 "par {\n let a <- one()\n do two()\n let b: Int <- three()\n }\n a",
3639 );
3640 let Statement::Par(p) = &b.statements[0] else {
3641 panic!("expected a par block, got {:?}", b.statements[0]);
3642 };
3643 assert_eq!(p.statements.len(), 3);
3644 assert!(matches!(&p.statements[0], Statement::EffectLet(l) if l.name.name == "a"));
3645 assert!(matches!(&p.statements[1], Statement::Do(_)));
3646 assert!(
3647 matches!(&p.statements[2], Statement::EffectLet(l) if l.name.name == "b" && l.type_annot.is_some())
3648 );
3649 }
3650
3651 #[test]
3653 fn par_is_an_identifier_outside_its_statement_shape() {
3654 let b = body_block("let par = 1\n let x = par + 1\n x");
3655 assert_eq!(b.statements.len(), 2);
3656 assert!(matches!(&b.statements[0], Statement::Let(l) if l.name.name == "par"));
3657 }
3658
3659 #[test]
3662 fn par_block_admits_statements_for_the_checker_to_refuse() {
3663 let b = body_block(
3664 "par {\n let a = 1\n ~> notify()\n par {\n do x()\n }\n }\n 1",
3665 );
3666 let Statement::Par(p) = &b.statements[0] else {
3667 panic!("expected a par block");
3668 };
3669 assert!(matches!(&p.statements[0], Statement::Let(_)));
3670 assert!(matches!(&p.statements[1], Statement::Send(_)));
3671 assert!(matches!(&p.statements[2], Statement::Par(_)));
3672 }
3673
3674 #[test]
3675 fn par_block_refuses_a_refutable_let_and_a_stray_expression() {
3676 assert!(has_error(
3677 &body_err("par {\n let Ok(v) <- load() else { 0 }\n }\n 1"),
3678 "bynk.par.invalid_statement"
3679 ));
3680 assert!(has_error(
3681 &body_err("par {\n load()\n }\n 1"),
3682 "bynk.par.invalid_statement"
3683 ));
3684 }
3685
3686 #[test]
3687 fn effectful_let_else_binds_with_an_arrow() {
3688 let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n v");
3689 assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
3690 }
3691
3692 #[test]
3693 fn let_else_chain_nests_each_step_in_the_previous_arm() {
3694 let b = body_block(
3697 "let Ok(a) = one() else { 1 }\n let y = a\n let Ok(b) = two(y) else { 2 }\n let Ok(c) = three(b) else { 3 }\n c",
3698 );
3699 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3700 panic!("expected a match tail");
3701 };
3702 let MatchBody::Block(second) = &arms[0].body else {
3703 panic!("expected a block arm");
3704 };
3705 assert_eq!(
3706 second.statements.len(),
3707 2,
3708 "`let y` and the second hidden binding"
3709 );
3710 assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
3711 let ExprKind::Match { arms, .. } = &second.tail.kind else {
3712 panic!("expected the second step's match");
3713 };
3714 let MatchBody::Block(third) = &arms[0].body else {
3715 panic!("expected a block arm");
3716 };
3717 let ExprKind::Match { arms, .. } = &third.tail.kind else {
3718 panic!("expected the third step's match");
3719 };
3720 let MatchBody::Block(last) = &arms[0].body else {
3721 panic!("expected a block arm");
3722 };
3723 assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3724 }
3725
3726 #[test]
3727 fn else_match_arms_follow_the_patterns_arm() {
3728 let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n v");
3729 let Statement::Let(l) = &b.statements[0] else {
3730 panic!("expected the hidden binding");
3731 };
3732 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3733 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3734 panic!("expected a match tail");
3735 };
3736 assert_eq!(arms.len(), 2);
3737 assert!(
3738 matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3739 );
3740 }
3741
3742 #[test]
3743 fn an_irrefutable_record_let_needs_no_else() {
3744 for body in [
3747 "let Item { sku, qty: n, .. } = item\n n",
3748 "let (Item { sku, .. }) = item\n sku",
3749 "let Item { sku: _, inner: Inner { x, .. }, .. } <- load(1)\n x",
3750 "let Item { sku, .. } <- load(1) by Admin(\"a\")\n sku",
3751 ] {
3752 let b = body_block(body);
3753 let (Statement::Let(l) | Statement::EffectLet(l)) = &b.statements[0] else {
3754 panic!("{body:?}: expected the hidden binding");
3755 };
3756 let le = l.let_else.as_ref().expect("the binding records the sugar");
3757 assert_eq!(le.form, LetElseForm::Plain, "{body:?}");
3758 assert_eq!(le.else_span.start, le.else_span.end, "no `else` extent");
3759 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3760 panic!("{body:?}: expected a match tail");
3761 };
3762 assert_eq!(arms.len(), 1, "{body:?}: only the pattern's arm");
3763 assert!(matches!(arms[0].pattern, Pattern::Record { .. }));
3764 }
3765 }
3766
3767 #[test]
3768 fn a_refutable_record_let_without_else_names_the_refutable_field() {
3769 for (body, field) in [
3770 (
3771 "let Order { status: Paid, total, .. } = o\n total",
3772 "status: Paid",
3773 ),
3774 (
3775 "let Order { total, c: Cust { kind: Gold, .. }, .. } = o\n total",
3776 "kind: Gold",
3777 ),
3778 ("let Order { id: 1, .. } <- load()\n 1", "id: 1"),
3779 ] {
3780 let errs = body_err(body);
3781 assert_eq!(errs[0].category, "bynk.parse.let_pattern_without_else");
3782 assert!(
3783 errs[0]
3784 .notes
3785 .iter()
3786 .any(|n| n.contains(&format!("`{field}` can fail to match"))),
3787 "{body:?}: {:?}",
3788 errs[0].notes
3789 );
3790 }
3791 for body in [
3793 "let Ok(n) = r\n n",
3794 "let (Ok(n)) = r\n n",
3795 "let Item { sku, .. } | Other = r\n 1",
3796 ] {
3797 let errs = body_err(body);
3798 assert_eq!(
3799 errs[0].category, "bynk.parse.let_pattern_without_else",
3800 "{body:?}"
3801 );
3802 }
3803 }
3804
3805 #[test]
3806 fn a_record_let_keeps_an_else_when_one_is_written() {
3807 let b = body_block("let Item { sku, .. } = item else { 0 }\n 1");
3808 let Statement::Let(l) = &b.statements[0] else {
3809 panic!("expected the hidden binding");
3810 };
3811 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Block);
3812 }
3813
3814 #[test]
3815 fn a_capitalised_binding_is_not_a_record_pattern() {
3816 for body in ["let Total = 1\n Total", "let Total: Int = 1\n Total"] {
3818 let b = body_block(body);
3819 let Statement::Let(l) = &b.statements[0] else {
3820 panic!("{body:?}: expected a plain let");
3821 };
3822 assert!(l.let_else.is_none(), "{body:?}");
3823 }
3824 let errs = body_err("let Item { sku } = item\n 1");
3826 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3827 }
3828
3829 #[test]
3830 fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3831 let b = body_block("let Some(n) = find(1) else { () }");
3832 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3833 panic!("expected a match tail");
3834 };
3835 let MatchBody::Block(rest) = &arms[0].body else {
3836 panic!("expected a block arm");
3837 };
3838 assert!(rest.implicit_tail && rest.statements.is_empty());
3839 }
3840
3841 #[test]
3842 fn let_pattern_forms_are_recognised() {
3843 for src in [
3844 "let None = o else { 0 }\n 1",
3845 "let Status.Done = s else { 0 }\n 1",
3846 "let Shipped(at) = s else { 0 }\n at",
3847 "let 0 = n else { 1 }\n 0",
3848 "let -1 = n else { 1 }\n 0",
3849 "let \"a\" = s else { 1 }\n 0",
3850 "let (Ok(x) | Err(x)) = r else { 1 }\n x",
3851 ] {
3852 let b = body_block(src);
3853 assert!(
3854 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3855 "{src:?} should parse as a let-else"
3856 );
3857 }
3858 let b = body_block("let Total = 1\n Total");
3860 assert!(
3861 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3862 );
3863 }
3864
3865 #[test]
3866 fn let_pattern_without_else_is_refused() {
3867 assert!(has_error(
3868 &body_err("let Ok(v) = r\n v"),
3869 "bynk.parse.let_pattern_without_else"
3870 ));
3871 assert!(has_error(
3872 &body_err("let Ok(v) <- r()\n v"),
3873 "bynk.parse.let_pattern_without_else"
3874 ));
3875 }
3876
3877 #[test]
3878 fn else_on_a_plain_binding_is_refused() {
3879 assert!(has_error(
3880 &body_err("let v = r else { 0 }\n v"),
3881 "bynk.parse.let_else_on_binding"
3882 ));
3883 assert!(has_error(
3884 &body_err("let v <- r() else { 0 }\n v"),
3885 "bynk.parse.let_else_on_binding"
3886 ));
3887 }
3888
3889 #[test]
3890 fn annotated_let_pattern_is_refused() {
3891 assert!(has_error(
3892 &body_err("let Ok(v): Int = r else { 0 }\n v"),
3893 "bynk.parse.let_pattern_annotation"
3894 ));
3895 }
3896
3897 #[test]
3898 fn empty_else_match_is_refused() {
3899 assert!(has_error(
3900 &body_err("let Ok(v) = r else match { }\n v"),
3901 "bynk.parse.empty_match"
3902 ));
3903 }
3904
3905 #[test]
3906 fn let_else_steps_count_against_the_nesting_budget() {
3907 let chain = |n: usize| -> String {
3911 let steps: String = (0..n)
3912 .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n "))
3913 .collect();
3914 format!("{steps}0")
3915 };
3916 body_block(&chain(40));
3917 let errs = body_err(&chain(400));
3918 assert!(
3919 errs.iter()
3920 .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3921 "{errs:?}"
3922 );
3923 }
3924
3925 #[test]
3926 fn empty_match_reports_its_own_diagnostic() {
3927 let errs = body_err("match result {}");
3931 assert!(
3932 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3933 "expected empty_match; got {errs:?}",
3934 );
3935 }
3936
3937 #[test]
3938 fn match_discriminant_ending_in_ident_parses() {
3939 assert!(matches!(
3941 body_tail("match ready { x => x }"),
3942 ExprKind::Match { .. }
3943 ));
3944 }
3945
3946 #[test]
3955 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3956 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
3957 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3958 let CommonsItem::Fn(f) = &c.items[0] else {
3959 panic!("expected fn, got {:?}", c.items[0]);
3960 };
3961 assert_eq!(
3962 f.body.statements.len(),
3963 1,
3964 "expected exactly one Assign statement, got {:?}",
3965 f.body.statements
3966 );
3967 let Statement::Assign(a) = &f.body.statements[0] else {
3968 panic!(
3969 "expected an Assign statement, got {:?}",
3970 f.body.statements[0]
3971 );
3972 };
3973 assert!(
3974 matches!(a.value.kind, ExprKind::Ident(_)),
3975 "assign value must stay the bare identifier `Paid`, got {:?}",
3976 a.value.kind
3977 );
3978 assert!(
3979 matches!(f.body.tail.kind, ExprKind::UnitLit),
3980 "the `()` must remain the block's own tail, got {:?}",
3981 f.body.tail.kind
3982 );
3983 }
3984
3985 #[test]
3990 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
3991 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
3992 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
3993 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
3994 let CommonsItem::Fn(f) = &c.items[0] else {
3995 panic!("expected fn, got {:?}", c.items[0]);
3996 };
3997 let Statement::Let(l) = &f.body.statements[0] else {
3998 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
3999 };
4000 let ExprKind::Is { pattern, .. } = &l.value.kind else {
4001 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
4002 };
4003 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
4004 panic!("{name}: expected a variant pattern, got {pattern:?}");
4005 };
4006 assert_eq!(
4007 span.end, variant.span.end,
4008 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
4009 );
4010 assert!(
4011 matches!(f.body.tail.kind, ExprKind::UnitLit),
4012 "{name}: the `()` must remain the block's own tail, got {:?}",
4013 f.body.tail.kind
4014 );
4015 }
4016 }
4017
4018 #[test]
4021 fn variant_pattern_payload_on_the_same_line_still_binds() {
4022 for (name, pat) in [
4023 ("user variant", "Paid(\n x\n )"),
4024 ("built-in variant", "Some(x)"),
4025 ] {
4026 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
4027 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4028 let CommonsItem::Fn(f) = &c.items[0] else {
4029 panic!("expected fn, got {:?}", c.items[0]);
4030 };
4031 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
4032 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
4033 };
4034 assert!(
4035 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
4036 "{name}: expected one binding, got {pattern:?}"
4037 );
4038 }
4039 }
4040
4041 #[test]
4047 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
4048 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
4049 assert!(
4050 parse_str(src).is_err(),
4051 "a next-line `(` after an arm's variant must not open its payload list"
4052 );
4053 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
4054 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
4055 }
4056
4057 #[test]
4061 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
4062 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
4063 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
4064 let CommonsItem::Fn(f) = &c.items[0] else {
4065 panic!("expected fn, got {:?}", c.items[0]);
4066 };
4067 let Statement::Let(l) = &f.body.statements[0] else {
4068 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
4069 };
4070 assert!(
4071 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
4072 "let value must stay a field access, got {:?}",
4073 l.value.kind
4074 );
4075 assert!(
4076 matches!(f.body.tail.kind, ExprKind::UnitLit),
4077 "the `()` must remain the block's own tail, got {:?}",
4078 f.body.tail.kind
4079 );
4080 }
4081
4082 #[test]
4083 fn unparenthesised_record_in_condition_head_now_errors() {
4084 assert!(
4090 !body_err("match Point { x: 1 } { p => p }").is_empty(),
4091 "unparenthesised record discriminant should not parse",
4092 );
4093 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
4095 else {
4096 panic!("expected Match for the parenthesised form");
4097 };
4098 let ExprKind::Paren(inner) = &discriminant.kind else {
4099 panic!(
4100 "expected a parenthesised discriminant, got {:?}",
4101 discriminant.kind
4102 );
4103 };
4104 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
4105 }
4106
4107 fn strict_codes(src: &str) -> Vec<&'static str> {
4109 let toks = tokenize(src).unwrap();
4110 match parse_units_with_warnings(&toks, src) {
4111 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
4112 Err(errs) => errs.iter().map(|e| e.category).collect(),
4113 }
4114 }
4115
4116 #[test]
4120 fn a_divider_pair_around_code_is_doc_block_contains_code() {
4121 let src =
4122 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
4123 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
4124 let errs = parse_str(src).unwrap_err();
4125 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
4126 assert_eq!(
4128 &src[errs[0].span.range()],
4129 "--------\nfn helper() -> Int { 1 }\n--------\n"
4130 );
4131 let (unit, errs) = parse_recover_str(src);
4132 assert!(unit.is_some(), "recovery keeps the unit");
4133 assert!(
4134 errs.iter()
4135 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
4136 "{errs:?}"
4137 );
4138 }
4139
4140 #[test]
4143 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
4144 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
4145 let codes = strict_codes(orphan);
4146 assert!(
4147 codes.contains(&"bynk.parse.doc_block_contains_code"),
4148 "{codes:?}"
4149 );
4150 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
4151 let codes = strict_codes(trailing);
4152 assert!(
4153 codes.contains(&"bynk.parse.doc_block_contains_code"),
4154 "{codes:?}"
4155 );
4156 }
4157
4158 #[test]
4162 fn near_miss_prose_and_fenced_code_are_not_code() {
4163 for doc in [
4164 "type of the thing",
4165 "fn is used here",
4166 "The type Id is an Int.",
4167 "uses the clock",
4168 "Event handler.\nfn helper() -> Int { 1 }",
4169 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
4170 "~~~~\ntype Id = Int\n~~~~",
4171 "---\nA nested doc.\n---",
4172 "",
4173 ] {
4174 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4175 }
4176 for doc in [
4177 "fn helper() -> Int { 1 }",
4178 "type Id = Int\nfn f() -> Int { 1 }",
4179 "```\nan example\n```\nfn helper() -> Int { 1 }",
4180 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
4181 ] {
4182 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
4183 }
4184 }
4185
4186 #[test]
4190 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
4191 let nested =
4192 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
4193 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
4194 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
4195 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
4196 assert!(doc_content_is_code(
4198 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
4199 ));
4200 for doc in [
4201 "---\nAn inner doc.\n---",
4202 "Intro.\n\n---\n\nfn is used here",
4203 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
4204 ] {
4205 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4206 }
4207 let mut deep = "fn helper() -> Int { 1 }".to_string();
4209 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
4210 let fence = "-".repeat(n);
4211 deep = format!("{fence}\n{deep}\n{fence}");
4212 }
4213 assert!(!doc_content_is_code(&deep));
4214 }
4215
4216 #[test]
4219 fn a_fenced_nested_doc_example_parses_clean() {
4220 let src = "commons m\n\n----\nShows a doc.\n\n```bynk\n---\nInner.\n---\nfn example() -> Int { 1 }\n```\n----\nfn f() -> Int { 2 }\n";
4221 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4222 let c = parse_str(src).unwrap();
4223 let CommonsItem::Fn(f) = &c.items[0] else {
4224 panic!("expected fn")
4225 };
4226 assert!(
4227 f.documentation
4228 .as_deref()
4229 .is_some_and(|d| d.contains("---\nInner.\n---")),
4230 "{:?}",
4231 f.documentation
4232 );
4233 }
4234
4235 #[test]
4239 fn private_modifier_is_contextual() {
4240 let src = "commons m\n\n--- Hidden.\nprivate type T = Int\n\nprivate fn T.get(self) -> Int { 1 }\n\nfn f(private: Int) -> Int { private }\n";
4241 let c = parse_str(src).unwrap();
4242 let t = type_at(&c, 0);
4243 let at = src.find("private type").unwrap();
4244 assert_eq!(t.private.map(|s| s.start), Some(at));
4245 assert_eq!(t.span.start, at);
4246 assert_eq!(doc_of(&t.documentation), Some(("Hidden.", DocForm::Lines)));
4247 assert!(c.items[1].is_private());
4248 assert!(!c.items[2].is_private());
4249 }
4250
4251 #[test]
4254 fn private_modifier_on_a_constant() {
4255 let src = "commons m\n\nprivate const Max: Int = 3\n\nconst Min: Int = 1\n";
4256 let c = parse_str(src).unwrap();
4257 let CommonsItem::Const(k) = &c.items[0] else {
4258 panic!("a constant");
4259 };
4260 assert_eq!(
4261 k.private.map(|s| s.start),
4262 Some(src.find("private").unwrap())
4263 );
4264 assert_eq!(k.span.start, src.find("private").unwrap());
4265 assert!(c.items[0].is_private());
4266 assert!(!c.items[1].is_private());
4267 }
4268
4269 #[test]
4271 fn private_on_a_service_is_rejected() {
4272 let src = "context m\n\nprivate service s {\n on call() -> Effect[Int] { 1 }\n}\n";
4273 let tokens = crate::lexer::tokenize(src).unwrap();
4274 let errs = parse_units(&tokens, src).unwrap_err();
4275 assert_eq!(errs[0].category, "bynk.visibility.private_not_allowed");
4276 }
4277
4278 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
4280 let CommonsItem::Type(t) = &c.items[i] else {
4281 panic!("expected a type at {i}")
4282 };
4283 t
4284 }
4285
4286 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
4287 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
4290 }
4291
4292 #[test]
4295 fn doc_lines_document_a_declaration() {
4296 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
4297 let c = parse_str(src).unwrap();
4298 assert_eq!(
4299 doc_of(&type_at(&c, 0).documentation),
4300 Some(("A commit SHA.", DocForm::Lines))
4301 );
4302 let CommonsItem::Fn(f) = &c.items[1] else {
4303 panic!("expected fn")
4304 };
4305 assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
4306 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4307 }
4308
4309 #[test]
4313 fn legacy_spellings_parse_and_are_each_reported() {
4314 let src =
4315 "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n repo: String, -- trailing\n}\n";
4316 assert_eq!(
4317 strict_codes(src),
4318 [
4319 "bynk.lex.legacy_comment",
4320 "bynk.lex.legacy_doc_line",
4321 "bynk.lex.legacy_comment"
4322 ]
4323 );
4324 let tokens = tokenize(src).unwrap();
4325 let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
4326 let SourceUnit::Commons(c) = &units[0] else {
4327 panic!("commons")
4328 };
4329 assert_eq!(
4330 doc_of(&type_at(c, 0).documentation),
4331 Some(("A run.", DocForm::Lines))
4332 );
4333 }
4334
4335 #[test]
4338 fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
4339 let src = "commons m\n\ntype R = {\n --- First.\n --- Second.\n a: Int,\n}\n";
4340 let tokens = tokenize(src).unwrap();
4341 let errs = parse_units(&tokens, src).unwrap_err();
4342 assert_eq!(errs.len(), 1, "{errs:?}");
4343 assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
4344 let (span, block) = &errs[0].suggestions[0].edits[0];
4345 assert_eq!(&src[span.range()], "--- First.\n --- Second.");
4346 assert_eq!(block, "---\n First.\n Second.\n ---");
4347 }
4348
4349 #[test]
4353 fn members_take_docs_in_either_form() {
4354 let src = "commons m\n\ntype Run = {\n --- The repository.\n repo: String,\n ---\n The version.\n ---\n version: String,\n // a note\n sha: String, // trailing\n}\n\ntype Outcome = enum {\n --- Passed.\n Pass,\n Fail,\n}\n\ntype Shape =\n --- Round.\n | Circle(\n --- The radius.\n r: Int,\n )\n | Dot\n\n--- After the sum.\nfn f() -> Int { 1 }\n\nmessages \"en\" @reference {\n --- Greets.\n \"hi\" => \"Hello\"\n}\n";
4355 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4356 let c = parse_str(src).unwrap();
4357 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4358 panic!("record")
4359 };
4360 assert_eq!(
4361 doc_of(&r.fields[0].documentation),
4362 Some(("The repository.", DocForm::Lines))
4363 );
4364 assert_eq!(
4365 doc_of(&r.fields[1].documentation),
4366 Some(("The version.", DocForm::Block))
4367 );
4368 assert_eq!(r.fields[2].documentation, None);
4369 assert_eq!(
4370 r.fields[2].trivia.leading,
4371 vec![Comment::Line(" a note".into())]
4372 );
4373 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
4374 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
4375 panic!("enum")
4376 };
4377 assert_eq!(
4378 doc_of(&e.variants[0].documentation),
4379 Some(("Passed.", DocForm::Lines))
4380 );
4381 assert_eq!(e.variants[1].documentation, None);
4382 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
4383 panic!("sum")
4384 };
4385 assert_eq!(
4386 doc_of(&p.variants[0].documentation),
4387 Some(("Round.", DocForm::Lines))
4388 );
4389 assert_eq!(
4390 doc_of(&p.variants[0].payload[0].documentation),
4391 Some(("The radius.", DocForm::Lines))
4392 );
4393 assert_eq!(p.variants[1].documentation, None);
4394 let CommonsItem::Fn(f) = &c.items[3] else {
4396 panic!("expected fn")
4397 };
4398 assert_eq!(
4399 doc_of(&f.documentation),
4400 Some(("After the sum.", DocForm::Lines))
4401 );
4402 let CommonsItem::Messages(m) = &c.items[4] else {
4403 panic!("expected messages")
4404 };
4405 assert_eq!(
4406 doc_of(&m.entries[0].documentation),
4407 Some(("Greets.", DocForm::Lines))
4408 );
4409 }
4410
4411 #[test]
4415 fn two_docs_on_one_target_is_multiple_docs() {
4416 for src in [
4417 "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
4418 "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
4419 "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
4420 "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
4421 "commons m\n\ntype R = {\n --- Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
4422 ] {
4423 assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
4424 let (unit, errs) = parse_recover_str(src);
4425 assert!(unit.is_some());
4426 assert!(
4427 errs.iter()
4428 .any(|e| e.category == "bynk.parse.multiple_docs")
4429 );
4430 }
4431 }
4432
4433 #[test]
4436 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
4437 let src =
4438 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --- dangling\n )\n | Dot\n";
4439 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
4440 let c = parse_str(src).unwrap();
4441 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
4442 panic!("sum")
4443 };
4444 assert_eq!(s.variants[0].payload.len(), 1);
4445 assert_eq!(s.variants[0].payload[0].documentation, None);
4446 }
4447
4448 #[test]
4451 fn an_unattached_member_doc_is_an_orphan() {
4452 let src = "commons m\n\ntype R = {\n --- Spaced.\n\n a: Int,\n --- Dangling.\n}\n";
4453 assert_eq!(
4454 strict_codes(src),
4455 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
4456 );
4457 let c = parse_str(src).unwrap();
4458 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4459 panic!("record")
4460 };
4461 assert_eq!(r.fields[0].documentation, None);
4462 assert!(matches!(
4463 &r.fields[0].trivia.leading[..],
4464 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
4465 ));
4466 assert!(matches!(
4467 &r.trailing_comments[..],
4468 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
4469 ));
4470 }
4471}
4472
4473#[cfg(test)]
4474mod paren_guard_2721 {
4475 use super::*;
4476 use crate::lexer::tokenize;
4477
4478 fn ok(src: &str) {
4479 let toks = tokenize(src).unwrap();
4480 parse(&toks, src).unwrap_or_else(|e| panic!("{src}\n{e:?}"));
4481 }
4482
4483 fn wrap(guard: &str) -> String {
4484 format!(
4485 "commons t {{\n fn f(n: Int, flag: Bool) -> Int {{\n match n {{\n n if {guard} => n,\n _ => 0,\n }}\n }}\n}}\n"
4486 )
4487 }
4488
4489 #[test]
4490 fn parenthesised_guards_parse() {
4491 ok(&wrap("(flag)"));
4492 ok(&wrap("(n > 0 && flag)"));
4493 ok(&wrap("(n > 0) && flag"));
4494 ok(&wrap("!(flag)"));
4495 ok(&wrap("()"));
4496 }
4497
4498 #[test]
4499 fn lambda_still_parses_when_parenthesised_in_guard() {
4500 ok(&wrap("pred(((x: Int) => x > 0))"));
4501 }
4502}