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