1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{
11 Token, TokenKind, comment_body, comment_is_legacy, doc_block_content, doc_fence_len,
12 doc_line_is_legacy, doc_line_text, has_blank_line_between, legacy_comment_error,
13 legacy_doc_line_error,
14};
15use crate::span::Span;
16mod declarations;
17mod expressions;
18mod staircase;
19mod statements;
20mod types;
21
22#[derive(Debug, Default)]
31struct TriviaTable {
32 leading: Vec<Vec<Comment>>,
38 trailing: Vec<Option<String>>,
43 blank_before: Vec<bool>,
47 epilogue: Vec<Comment>,
51 legacy: Vec<CompileError>,
57}
58
59impl TriviaTable {
60 fn take_leading(&mut self, index: usize) -> Vec<Comment> {
61 match self.leading.get_mut(index) {
62 Some(v) => std::mem::take(v),
63 None => Vec::new(),
64 }
65 }
66
67 fn blank_before(&self, index: usize) -> bool {
68 self.blank_before.get(index).copied().unwrap_or(false)
69 }
70
71 fn take_trailing(&mut self, index: usize) -> Option<String> {
72 self.trailing.get_mut(index).and_then(|s| s.take())
73 }
74
75 fn take_epilogue(&mut self) -> Vec<Comment> {
76 std::mem::take(&mut self.epilogue)
77 }
78
79 fn is_fully_drained(&self) -> bool {
96 self.leading.iter().all(Vec::is_empty)
97 && self.trailing.iter().all(Option::is_none)
98 && self.epilogue.is_empty()
99 }
100
101 fn epilogue_is_empty(&self) -> bool {
109 self.epilogue.is_empty()
110 }
111}
112
113fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
124 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
125 let mut table = TriviaTable::default();
126 let mut pending_leading: Vec<Comment> = Vec::new();
127 let mut last_content_end: Option<usize> = None;
128 let mut last_leading_end: Option<usize> = None;
135 let mut pending_blank_before = false;
137 for tok in tokens {
138 if !pending_leading.is_empty()
139 && let Some(end) = last_leading_end
140 && source[end..tok.span.start].matches('\n').count() >= 2
141 {
142 pending_leading.push(Comment::Blank);
143 }
144 if tok.kind == TokenKind::DocLine && doc_line_is_legacy(source, tok.span) {
145 table.legacy.push(legacy_doc_line_error(source, tok.span));
146 }
147 if tok.kind == TokenKind::Comment {
148 if comment_is_legacy(source, tok.span) {
149 table.legacy.push(legacy_comment_error(tok.span));
150 }
151 let body = comment_body(source, tok.span).into_owned();
152 if pending_leading.is_empty()
164 && filtered.last().is_some_and(|t| {
165 !matches!(
166 t.kind,
167 TokenKind::DocBlock
168 | TokenKind::DocLine
169 | TokenKind::LBrace
170 | TokenKind::LBracket
171 | TokenKind::LParen
172 )
173 })
174 && let Some(prev_end) = last_content_end
175 && !source[prev_end..tok.span.start].contains('\n')
176 {
177 let last_idx = filtered.len() - 1;
178 if table.trailing[last_idx].is_none() {
181 table.trailing[last_idx] = Some(body);
182 continue;
183 }
184 }
185 let own_line = !pending_leading.is_empty()
189 || filtered
190 .last()
191 .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
192 || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
193 if pending_leading.is_empty() {
194 pending_blank_before = own_line
195 && last_content_end
196 .is_some_and(|end| source[end..tok.span.start].matches('\n').count() >= 2);
197 }
198 pending_leading.push(Comment::Line(body));
199 last_leading_end = own_line.then_some(tok.span.end);
200 continue;
201 }
202 filtered.push(*tok);
203 table.leading.push(std::mem::take(&mut pending_leading));
204 table.trailing.push(None);
205 table
206 .blank_before
207 .push(std::mem::take(&mut pending_blank_before));
208 last_content_end = Some(tok.span.end);
209 last_leading_end = None;
210 }
211 table.epilogue = pending_leading;
212 (filtered, table)
213}
214
215pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
221 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
222}
223
224pub fn parse_with_warnings(
227 tokens: &[Token],
228 source: &str,
229) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
230 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
231 match unit {
232 SourceUnit::Commons(c) => Ok((c, warnings)),
233 SourceUnit::Context(ctx) => Err(vec![
234 CompileError::new(
235 "bynk.parse.unexpected_context",
236 ctx.span,
237 "expected a `commons` declaration but found a `context` declaration",
238 )
239 .with_note(
240 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
241 ),
242 ]),
243 SourceUnit::Suite(t) => Err(vec![
244 CompileError::new(
245 "bynk.parse.unexpected_suite",
246 t.span,
247 "expected a `commons` declaration but found a `suite` declaration",
248 )
249 .with_note(
250 "tests must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
251 ),
252 ]),
253 SourceUnit::Adapter(a) => Err(vec![
254 CompileError::new(
255 "bynk.parse.unexpected_adapter",
256 a.span,
257 "expected a `commons` declaration but found an `adapter` declaration",
258 )
259 .with_note(
260 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
261 ),
262 ]),
263 }
264}
265
266pub fn parse_unit_with_recovery(
281 tokens: &[Token],
282 source: &str,
283) -> (Option<SourceUnit>, Vec<CompileError>) {
284 let (units, errors) = parse_units_with_recovery(tokens, source);
285 (units.into_iter().next(), errors)
286}
287
288pub fn parse_units_with_recovery(
293 tokens: &[Token],
294 source: &str,
295) -> (Vec<SourceUnit>, Vec<CompileError>) {
296 let recovered = parse_units_recovering(tokens, source);
297 (recovered.units, recovered.errors)
298}
299
300#[derive(Debug)]
303pub struct Recovered {
304 pub units: Vec<SourceUnit>,
305 pub errors: Vec<CompileError>,
306 pub broken_decl_names: Vec<String>,
309}
310
311pub fn merge_syntax_errors(
318 strict: Vec<CompileError>,
319 recovered: Vec<CompileError>,
320) -> Vec<CompileError> {
321 let mut out = strict;
322 for e in recovered {
323 if !out.iter().any(|o| o.span.start == e.span.start) {
324 out.push(e);
325 }
326 }
327 out
328}
329
330pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
333 parse_units_recovering_from(tokens, source, &mut 0)
334}
335
336pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
342 let (filtered, trivia) = split_trivia(tokens, source);
343 let mut warnings = Vec::new();
344 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
345 p.recover_mode = true;
346 p.next_expr_id = *next_id;
347 let mut units = Vec::new();
348 loop {
349 match p.parse_unit() {
350 Ok(u) => units.push(u),
351 Err(e) => {
352 p.recovered_errors.push(e);
353 break;
354 }
355 }
356 if p.peek().is_none() {
362 break;
363 }
364 }
365 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
366 *next_id = p.next_expr_id;
367 let mut all_errors = p.recovered_errors;
368 all_errors.append(&mut p.doc_errors);
369 all_errors.append(&mut warnings);
370 Recovered {
371 units,
372 errors: all_errors,
373 broken_decl_names,
374 }
375}
376
377pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
381 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
382}
383
384pub fn parse_unit_with_warnings(
387 tokens: &[Token],
388 source: &str,
389) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
390 parse_unit_with_warnings_from(tokens, source, &mut 0)
391}
392
393pub fn parse_unit_with_warnings_from(
407 tokens: &[Token],
408 source: &str,
409 next_id: &mut u32,
410) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
411 let (filtered, trivia) = split_trivia(tokens, source);
412 let mut warnings = Vec::new();
413 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
414 p.next_expr_id = *next_id;
415 let result = match p.parse_unit() {
416 Ok(u) => {
417 if let Some(extra) = p.peek() {
418 Err(vec![
419 CompileError::new(
420 "bynk.parse.extra_tokens",
421 extra.span,
422 "unexpected token after top-level declaration",
423 )
424 .with_note(
425 "a `.bynk` file contains exactly one `commons` or `context` declaration",
426 ),
427 ])
428 } else {
429 Ok(u)
430 }
431 }
432 Err(e) => Err(vec![e]),
433 };
434 let result = match (result, std::mem::take(&mut p.doc_errors)) {
437 (r, docs) if docs.is_empty() => r,
438 (Ok(_), docs) => Err(docs),
439 (Err(mut errs), mut docs) => {
440 errs.append(&mut docs);
441 Err(errs)
442 }
443 };
444 *next_id = p.next_expr_id;
445 match result {
448 Ok(u) => {
449 debug_assert!(
452 p.trivia.epilogue_is_empty(),
453 "a file-trailing comment was left undrained after a successful parse"
454 );
455 Ok((u, warnings))
456 }
457 Err(mut errs) => {
458 errs.append(&mut warnings);
459 Err(errs)
460 }
461 }
462}
463
464pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
473 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
474}
475
476pub fn parse_units_with_warnings(
482 tokens: &[Token],
483 source: &str,
484) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
485 parse_units_with_drain_check(tokens, source)
486 .map(|(units, warnings, _drained)| (units, warnings))
487}
488
489pub fn parse_units_with_warnings_from(
496 tokens: &[Token],
497 source: &str,
498 next_id: &mut u32,
499) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
500 parse_units_with_drain_check_from(tokens, source, next_id)
501 .map(|(units, warnings, _drained)| (units, warnings))
502}
503
504pub fn parse_units_with_drain_check(
515 tokens: &[Token],
516 source: &str,
517) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
518 parse_units_with_drain_check_from(tokens, source, &mut 0)
519}
520
521pub fn parse_units_with_drain_check_from(
524 tokens: &[Token],
525 source: &str,
526 next_id: &mut u32,
527) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
528 parse_units_drain(tokens, source, next_id, false)
529}
530
531pub fn parse_units_migrating(
536 tokens: &[Token],
537 source: &str,
538) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
539 parse_units_drain(tokens, source, &mut 0, true)
540}
541
542fn parse_units_drain(
543 tokens: &[Token],
544 source: &str,
545 next_id: &mut u32,
546 migrate: bool,
547) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
548 let (filtered, trivia) = split_trivia(tokens, source);
549 let mut warnings = Vec::new();
550 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
551 if migrate {
552 p.doc_errors.clear();
553 }
554 p.next_expr_id = *next_id;
555 let mut units = Vec::new();
556 let mut errors: Vec<CompileError> = Vec::new();
557 while p.peek().is_some() {
558 match p.parse_unit() {
559 Ok(u) => units.push(u),
560 Err(e) => {
561 errors.push(e);
562 break;
563 }
564 }
565 }
566 errors.append(&mut p.doc_errors);
568 *next_id = p.next_expr_id;
569 let eof = p.eof_span();
570 let fully_drained = p.trivia.is_fully_drained();
571 if !errors.is_empty() {
574 errors.append(&mut warnings);
575 return Err(errors);
576 }
577 if units.is_empty() {
578 return Err(vec![CompileError::new(
579 "bynk.parse.unexpected_eof",
580 eof,
581 "expected `commons`, `context`, or `suite` to start the file, found end of file",
582 )]);
583 }
584 debug_assert!(
592 p.trivia.epilogue_is_empty(),
593 "a file-trailing comment was left undrained after a successful parse"
594 );
595 Ok((units, warnings, fully_drained))
596}
597
598enum SignedNumLit {
601 Int(IntBound),
602 Float(FloatBound),
603 Decimal(DecimalBound),
604}
605
606struct Parser<'a> {
607 tokens: &'a [Token],
608 source: &'a str,
609 pos: usize,
610 warnings: &'a mut Vec<CompileError>,
613 recover_mode: bool,
619 recovered_errors: Vec<CompileError>,
622 pub(crate) item_start: Option<usize>,
626 broken_decl_names: Vec<String>,
631 trivia: TriviaTable,
634 depth: usize,
641 no_record_literal: bool,
650 in_guard: bool,
654 is_pattern_tail: bool,
662 brace_depth: usize,
673 item_loop_baseline: Vec<usize>,
680 next_expr_id: u32,
684 doc_errors: Vec<CompileError>,
690 doc_probe: bool,
694}
695
696impl<'a> Parser<'a> {
697 fn new(
698 tokens: &'a [Token],
699 source: &'a str,
700 mut trivia: TriviaTable,
701 warnings: &'a mut Vec<CompileError>,
702 ) -> Self {
703 let doc_errors = std::mem::take(&mut trivia.legacy);
704 Self {
705 tokens,
706 source,
707 pos: 0,
708 warnings,
709 recover_mode: false,
710 recovered_errors: Vec::new(),
711 item_start: None,
712 broken_decl_names: Vec::new(),
713 trivia,
714 depth: 0,
715 no_record_literal: false,
716 in_guard: false,
717 is_pattern_tail: false,
718 brace_depth: 0,
719 item_loop_baseline: Vec::new(),
720 next_expr_id: 0,
721 doc_errors,
722 doc_probe: false,
723 }
724 }
725
726 fn alloc_expr_id(&mut self) -> ExprId {
731 let id = ExprId(self.next_expr_id);
732 self.next_expr_id += 1;
733 id
734 }
735
736 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
744 self.depth += 1;
745 if self.depth > crate::MAX_NESTING_DEPTH {
746 self.depth -= 1;
747 let span = self
748 .peek()
749 .map(|t| t.span)
750 .unwrap_or_else(|| self.eof_span());
751 return Err(self.nesting_too_deep(span, what));
752 }
753 Ok(())
754 }
755
756 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
759 CompileError::new(
760 "bynk.parse.nesting_too_deep",
761 span,
762 format!(
763 "{what} nests more than {} levels deep",
764 crate::MAX_NESTING_DEPTH
765 ),
766 )
767 .with_note(
768 "deeply nested source is rejected to keep the parser from overflowing its \
769 stack and aborting; flatten or split the construct",
770 )
771 }
772
773 fn expression_too_long(&self, span: Span) -> CompileError {
779 CompileError::new(
780 "bynk.parse.nesting_too_deep",
781 span,
782 format!(
783 "this expression is more than {} levels deep",
784 crate::MAX_NESTING_DEPTH
785 ),
786 )
787 .with_note(
788 "a long operator or member chain is rejected to keep the compiler from overflowing \
789 its stack; split it across `let` bindings, or reduce a sequence with \
790 `.sum()`/`.fold(...)`",
791 )
792 }
793
794 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
814 self.depth += 1;
815 *folds += 1;
816 if self.depth > crate::MAX_NESTING_DEPTH {
817 self.depth -= *folds;
818 *folds = 0;
819 return Err(self.expression_too_long(span));
820 }
821 Ok(())
822 }
823
824 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
833 self.depth += 1;
834 if self.depth > crate::MAX_NESTING_DEPTH {
835 return Err(self.expression_too_long(span));
836 }
837 Ok(())
838 }
839
840 fn take_leading_trivia(&mut self) -> Vec<Comment> {
850 let mut comments = self.take_unit_leading_trivia();
851 comments.retain(|c| *c != Comment::Blank);
852 comments
853 }
854
855 fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
858 self.trivia.take_leading(self.pos)
859 }
860
861 fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
864 self.trivia.take_epilogue()
865 }
866
867 fn take_trailing_trivia(&mut self) -> Option<String> {
871 if self.pos == 0 {
872 return None;
873 }
874 self.trivia.take_trailing(self.pos - 1)
875 }
876
877 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
881 if self.recover_mode {
882 self.recovered_errors.push(e);
883 if let Some(start) = self.item_start.take() {
884 self.broken_decl_names.extend(self.decl_names_at(start));
885 }
886 let before = self.pos;
887 self.recover_to_top_item();
888 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
901 let closes_body = baseline > 0
902 && self.brace_depth == baseline
903 && self.peek_kind() == Some(TokenKind::RBrace);
904 if self.pos == before && !closes_body {
905 self.bump();
906 self.recover_to_top_item();
911 }
912 Ok(())
913 } else {
914 Err(e)
915 }
916 }
917
918 fn decl_names_at(&self, start: usize) -> Vec<String> {
923 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
924 let ident_at = |i: usize| {
925 self.tokens
926 .get(i)
927 .filter(|t| t.kind == TokenKind::Ident)
928 .map(|t| self.slice(t.span).to_string())
929 };
930 match kind_at(start) {
931 Some(
932 TokenKind::Type
933 | TokenKind::Event
934 | TokenKind::Capability
935 | TokenKind::Service
936 | TokenKind::Agent
937 | TokenKind::Actor,
938 ) => ident_at(start + 1).into_iter().collect(),
939 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
940 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
943 Some(method) => vec![format!("{owner}.{method}")],
944 None => Vec::new(),
945 },
946 (Some(name), _) => vec![name],
947 (None, _) => Vec::new(),
948 },
949 _ => Vec::new(),
950 }
951 }
952
953 fn recover_to_top_item(&mut self) {
966 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
967 while let Some(t) = self.peek() {
968 match t.kind {
969 TokenKind::RBrace if self.brace_depth == baseline => return,
970 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
971 _ => {
972 self.bump();
973 }
974 }
975 }
976 }
977
978 fn enter_item_loop(&mut self) {
985 self.item_loop_baseline.push(self.brace_depth);
986 }
987
988 fn exit_item_loop(&mut self) {
990 self.item_loop_baseline.pop();
991 }
992
993 fn peek(&self) -> Option<Token> {
994 self.tokens.get(self.pos).copied()
995 }
996
997 fn peek_kind(&self) -> Option<TokenKind> {
998 self.peek().map(|t| t.kind)
999 }
1000
1001 fn take_private_modifier(&mut self, allowed: bool) -> Result<Option<Span>, CompileError> {
1013 let Some(t) = self.peek() else {
1014 return Ok(None);
1015 };
1016 if t.kind != TokenKind::Ident || self.slice(t.span) != "private" {
1017 return Ok(None);
1018 }
1019 let Some(next) = self.nth(1) else {
1020 return Ok(None);
1021 };
1022 let modifiable = matches!(
1023 next.kind,
1024 TokenKind::Fn
1025 | TokenKind::Type
1026 | TokenKind::Messages
1027 | TokenKind::Asset
1028 | TokenKind::Embed
1029 | TokenKind::Const
1030 );
1031 let declaration = modifiable
1032 || matches!(
1033 next.kind,
1034 TokenKind::Service
1035 | TokenKind::Agent
1036 | TokenKind::Capability
1037 | TokenKind::Provides
1038 | TokenKind::Actor
1039 | TokenKind::Event
1040 );
1041 if !declaration {
1042 return Ok(None);
1043 }
1044 self.bump();
1045 if modifiable && allowed {
1046 return Ok(Some(t.span));
1047 }
1048 let what = self.slice(next.span);
1049 let err = if allowed {
1050 CompileError::new(
1051 "bynk.visibility.private_not_allowed",
1052 t.span,
1053 format!("`private` does not apply to `{what}` declarations"),
1054 )
1055 .with_note(
1056 "`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",
1057 )
1058 } else {
1059 CompileError::new(
1060 "bynk.visibility.private_not_allowed",
1061 t.span,
1062 "`private` applies only to declarations in a commons or context, not in an adapter",
1063 )
1064 .with_note("an adapter's `exports` governs what its consumers see")
1065 };
1066 if self.recover_mode {
1067 self.recovered_errors.push(err);
1068 Ok(None)
1069 } else {
1070 Err(err)
1071 }
1072 }
1073
1074 fn nth(&self, n: usize) -> Option<Token> {
1076 self.tokens.get(self.pos + n).copied()
1077 }
1078
1079 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
1080 self.nth(n).map(|t| t.kind)
1081 }
1082
1083 fn nth_text(&self, n: usize) -> &'a str {
1085 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
1086 }
1087
1088 fn prev_span(&self) -> Span {
1091 self.tokens
1092 .get(self.pos.wrapping_sub(1))
1093 .or_else(|| self.peek_ref())
1094 .map(|t| t.span)
1095 .unwrap_or_default()
1096 }
1097
1098 fn peek_ref(&self) -> Option<&Token> {
1099 self.tokens.get(self.pos)
1100 }
1101
1102 fn bump(&mut self) -> Option<Token> {
1103 let t = self.peek();
1104 if let Some(t) = t {
1105 match t.kind {
1106 TokenKind::LBrace => self.brace_depth += 1,
1107 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
1108 _ => {}
1109 }
1110 self.pos += 1;
1111 }
1112 t
1113 }
1114
1115 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
1116 if self.peek_kind() == Some(kind) {
1117 self.bump()
1118 } else {
1119 None
1120 }
1121 }
1122
1123 fn slice(&self, span: Span) -> &'a str {
1124 &self.source[span.range()]
1125 }
1126
1127 fn next_token_on_new_line(&self, prev: Span) -> bool {
1132 match self.peek() {
1133 Some(t) if prev.end <= t.span.start => {
1134 self.source[prev.end..t.span.start].contains('\n')
1135 }
1136 _ => false,
1137 }
1138 }
1139
1140 fn eof_span(&self) -> Span {
1145 let end = self.source.len();
1146 let start = (0..end)
1147 .rev()
1148 .find(|&i| self.source.is_char_boundary(i))
1149 .unwrap_or(0);
1150 Span::new(start, end)
1151 }
1152
1153 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1154 match self.peek() {
1155 Some(t) if t.kind == kind => {
1156 self.bump();
1157 Ok(t)
1158 }
1159 Some(t) => Err(CompileError::new(
1160 "bynk.parse.expected_token",
1161 t.span,
1162 format!(
1163 "expected {} {ctx}, found {}",
1164 kind.describe(),
1165 t.kind.describe()
1166 ),
1167 )),
1168 None => Err(CompileError::new(
1169 "bynk.parse.unexpected_eof",
1170 self.eof_span(),
1171 format!("expected {} {ctx}, found end of file", kind.describe()),
1172 )),
1173 }
1174 }
1175
1176 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1177 match self.peek() {
1178 Some(t) if t.kind == TokenKind::Ident => {
1179 self.bump();
1180 Ok(Ident {
1181 name: self.slice(t.span).to_string(),
1182 span: t.span,
1183 })
1184 }
1185 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1200 self.bump();
1201 Ok(Ident {
1202 name: self.slice(t.span).to_string(),
1203 span: t.span,
1204 })
1205 }
1206 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1207 "bynk.parse.reserved_keyword",
1208 t.span,
1209 format!(
1210 "expected identifier {ctx}, but `{}` is a reserved keyword",
1211 self.slice(t.span)
1212 ),
1213 )
1214 .with_note("rename the identifier to something that is not a keyword")),
1215 Some(t) => Err(CompileError::new(
1216 "bynk.parse.expected_token",
1217 t.span,
1218 format!("expected identifier {ctx}, found {}", t.kind.describe()),
1219 )),
1220 None => Err(CompileError::new(
1221 "bynk.parse.unexpected_eof",
1222 self.eof_span(),
1223 format!("expected identifier {ctx}, found end of file"),
1224 )),
1225 }
1226 }
1227
1228 fn at_keyword_label(&self) -> bool {
1232 matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1233 && self.tokens.get(self.pos + 1).map(|t| t.kind) == Some(TokenKind::Colon)
1234 }
1235
1236 pub(crate) fn expect_label(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1242 if self.at_keyword_label() {
1243 let t = self.bump().expect("peeked a keyword");
1244 return Ok(Ident {
1245 name: self.slice(t.span).to_string(),
1246 span: t.span,
1247 });
1248 }
1249 self.expect_ident(ctx)
1250 }
1251
1252 pub(crate) fn keyword_shorthand_error(&self, pattern: bool) -> Option<CompileError> {
1264 let t = self.peek()?;
1265 if !is_reserved_keyword(t.kind) {
1266 return None;
1267 }
1268 let next = self.tokens.get(self.pos + 1).map(|t| t.kind);
1269 let shorthand = match next {
1270 Some(TokenKind::Comma | TokenKind::RBrace) => true,
1271 Some(TokenKind::DotDot) => pattern,
1272 _ => false,
1273 };
1274 if !shorthand {
1275 return None;
1276 }
1277 let word = self.slice(t.span);
1278 let mut err = CompileError::new(
1279 "bynk.parse.keyword_field_shorthand",
1280 t.span,
1281 format!("`{word}` is a reserved keyword, so it cannot be a field shorthand"),
1282 );
1283 if pattern {
1284 let taken = |name: &str| {
1285 self.tokens
1286 .iter()
1287 .any(|k| k.kind == TokenKind::Ident && self.slice(k.span) == name)
1288 };
1289 let mut binding = format!("{word}_");
1290 let mut n = 2;
1291 while taken(&binding) {
1292 binding = format!("{word}_{n}");
1293 n += 1;
1294 }
1295 err = err
1296 .with_note(format!(
1297 "the shorthand binds a variable named `{word}`; name the binding instead"
1298 ))
1299 .with_suggestion(
1300 format!("bind the `{word}` field as `{binding}`"),
1301 vec![(t.span, format!("{word}: {binding}"))],
1302 crate::error::Applicability::MaybeIncorrect,
1303 );
1304 } else {
1305 err = err.with_note(format!(
1306 "write `{word}: <value>`; a keyword names the field but cannot name a variable"
1307 ));
1308 }
1309 Some(err)
1310 }
1311
1312 pub(crate) fn expect_member_name(
1317 &mut self,
1318 ctx: &str,
1319 dot: Span,
1320 ) -> Result<Ident, CompileError> {
1321 if matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1322 && !self.next_token_on_new_line(dot)
1323 {
1324 let t = self.bump().expect("peeked a keyword");
1325 return Ok(Ident {
1326 name: self.slice(t.span).to_string(),
1327 span: t.span,
1328 });
1329 }
1330 self.expect_ident(ctx)
1331 }
1332
1333 fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1351 let first = self.take_one_doc()?;
1352 let mut docs = vec![first.clone()];
1353 while let Some(next) = self.peek()
1354 && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1355 && !has_blank_line_between(self.source, docs[docs.len() - 1].1.end, next.span.start)
1356 {
1357 docs.push(self.take_one_doc().expect("a doc token was peeked"));
1358 }
1359 if docs.len() > 1 && !self.doc_probe {
1360 self.doc_errors.push(multiple_docs(self.source, &docs));
1361 }
1362 Some(first)
1363 }
1364
1365 fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1367 match self.peek_kind()? {
1368 TokenKind::DocBlock => {
1369 let t = self.bump().unwrap();
1370 let body = doc_block_content(self.source, t.span);
1371 if !self.doc_probe && doc_content_is_code(&body) {
1372 self.doc_errors.push(doc_block_contains_code(t.span));
1373 }
1374 Some((Doc::new(body, DocForm::Block), t.span))
1375 }
1376 TokenKind::DocLine => {
1377 let t = self.bump().unwrap();
1378 let text = doc_line_text(self.source, t.span);
1379 Some((Doc::new(text, DocForm::Lines), t.span))
1380 }
1381 _ => None,
1382 }
1383 }
1384
1385 fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1398 self.collect_lead(Self::take_unit_leading_trivia)
1399 }
1400
1401 fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1404 self.collect_lead(Self::take_leading_trivia)
1405 }
1406
1407 fn collect_lead(
1408 &mut self,
1409 take: fn(&mut Self) -> Vec<Comment>,
1410 ) -> (Vec<Comment>, Option<DocLead>) {
1411 let mut leading = take(self);
1412 let doc = self.take_doc_block().map(|(content, span)| DocLead {
1413 content,
1414 span,
1415 at: leading.len(),
1416 });
1417 if doc.is_some() {
1418 leading.extend(take(self));
1419 }
1420 (leading, doc)
1421 }
1422
1423 fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1432 let (mut leading, doc) = self.collect_body_item_lead();
1433 let Some(doc) = doc else {
1434 return (leading, None);
1435 };
1436 match self.peek() {
1437 Some(t) if t.kind != close => {
1438 let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1439 (leading, doc)
1440 }
1441 _ => {
1442 self.warnings.push(CompileError::new(
1443 "bynk.parse.orphan_doc_block",
1444 doc.span,
1445 "documentation has no following member to attach to",
1446 ));
1447 keep_orphan(&mut leading, doc);
1448 (leading, None)
1449 }
1450 }
1451 }
1452
1453 fn peek_past_docs(&self) -> Option<TokenKind> {
1457 self.tokens[self.pos.min(self.tokens.len())..]
1458 .iter()
1459 .map(|t| t.kind)
1460 .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1461 }
1462
1463 fn finalize_doc(
1467 &mut self,
1468 doc: Option<DocLead>,
1469 next_span: Span,
1470 leading: &mut Vec<Comment>,
1471 ) -> Option<Doc> {
1472 let doc = doc?;
1473 let doc_span = doc.span;
1474 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1476 self.warnings.push(
1477 CompileError::new(
1478 "bynk.parse.orphan_doc_block",
1479 doc_span,
1480 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1481 )
1482 .with_note(
1483 "remove the blank line to attach the doc to the next declaration, \
1484 or remove the doc block if it is not meant to document anything",
1485 ),
1486 );
1487 keep_orphan(leading, doc);
1488 return None;
1489 }
1490 Some(doc.content)
1491 }
1492}
1493
1494pub(crate) struct DocLead {
1498 pub(crate) content: Doc,
1499 pub(crate) span: Span,
1500 pub(crate) at: usize,
1501}
1502
1503fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1507 let at = doc.at.min(leading.len());
1508 leading.insert(at, Comment::OrphanDoc(doc.content));
1509}
1510
1511fn multiple_docs(source: &str, docs: &[(Doc, Span)]) -> CompileError {
1516 let (_, first) = docs[0];
1517 let (_, last) = docs[docs.len() - 1];
1518 let mut err = CompileError::new(
1519 "bynk.parse.multiple_docs",
1520 docs[1].1,
1521 "this declaration or member already has a doc; a target has one doc",
1522 )
1523 .with_label(first, "the first doc is here")
1524 .with_note(
1525 "a doc of more than one line is a block: a `---` line, the text, and another `---` line",
1526 );
1527 let one_liners = docs
1528 .iter()
1529 .all(|(d, span)| d.form == DocForm::Lines && !doc_line_is_legacy(source, *span));
1530 if one_liners {
1531 let line_start = source[..first.start].rfind('\n').map_or(0, |i| i + 1);
1532 let indent = &source[line_start..first.start];
1533 let text = docs
1534 .iter()
1535 .map(|(d, _)| d.as_str())
1536 .collect::<Vec<_>>()
1537 .join("\n");
1538 let fence = "-".repeat(doc_fence_len(&text));
1539 let mut block = fence.clone();
1540 for line in text.lines() {
1541 block.push('\n');
1542 if !line.is_empty() {
1543 block.push_str(indent);
1544 block.push_str(line);
1545 }
1546 }
1547 block.push('\n');
1548 block.push_str(indent);
1549 block.push_str(&fence);
1550 let end = source[..last.end].trim_end_matches(['\n', '\r']).len();
1551 err = err.with_suggestion(
1552 "join the docs into one `---` block",
1553 vec![(Span::new_in(first.file, first.start, end), block)],
1554 crate::error::Applicability::MachineApplicable,
1555 );
1556 }
1557 err
1558}
1559
1560fn doc_block_contains_code(span: Span) -> CompileError {
1563 CompileError::new(
1564 "bynk.parse.doc_block_contains_code",
1565 span,
1566 "this documentation block contains declarations, so they are not compiled",
1567 )
1568 .with_note(
1569 "a line of three or more dashes opens or closes a doc block, so two dash \
1570 dividers turn the code between them into documentation; for a divider use a \
1571 line comment with text, such as `-- Helpers --`, and to show code in a doc \
1572 block put it in a Markdown code fence (```)",
1573 )
1574}
1575
1576const DECLARATION_KEYWORDS: &[TokenKind] = &[
1580 TokenKind::Type,
1581 TokenKind::Fn,
1582 TokenKind::Capability,
1583 TokenKind::Provides,
1584 TokenKind::Service,
1585 TokenKind::Agent,
1586 TokenKind::Actor,
1587 TokenKind::Messages,
1588 TokenKind::Event,
1589 TokenKind::Asset,
1590 TokenKind::Embed,
1591 TokenKind::Const,
1592 TokenKind::Uses,
1593 TokenKind::Consumes,
1594 TokenKind::Exports,
1595 TokenKind::Binding,
1596];
1597
1598fn doc_content_is_code(content: &str) -> bool {
1613 doc_content_is_code_at(content, 0)
1614}
1615
1616const DOC_PROBE_MAX_DEPTH: usize = 4;
1620
1621fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1622 let stripped = strip_code_fences(content);
1623 let tokens = match crate::lexer::tokenize(&stripped) {
1624 Ok(tokens) => tokens,
1625 Err(_) => {
1626 let unmarked = blank_marker_lines(&stripped);
1629 if unmarked == stripped {
1630 return false;
1631 }
1632 return doc_content_is_code_at(&unmarked, depth);
1633 }
1634 };
1635 if depth < DOC_PROBE_MAX_DEPTH
1636 && tokens.iter().any(|t| {
1637 t.kind == TokenKind::DocBlock
1638 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1639 })
1640 {
1641 return true;
1642 }
1643 if !tokens
1644 .iter()
1645 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1646 {
1647 return false;
1648 }
1649 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1650 let mut warnings = Vec::new();
1651 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1652 p.doc_probe = true;
1653 let at = Span::new(0, 0);
1654 let name = QualifiedName {
1655 parts: Vec::new(),
1656 span: at,
1657 };
1658 match p.parse_adapter_body(at, name, None, false) {
1659 Ok(a) => {
1660 p.peek().is_none()
1661 && (!a.items.is_empty()
1662 || !a.uses.is_empty()
1663 || !a.consumes.is_empty()
1664 || !a.exports.is_empty()
1665 || a.binding.is_some())
1666 }
1667 Err(_) => false,
1668 }
1669}
1670
1671fn blank_marker_lines(content: &str) -> String {
1674 let mut out = String::with_capacity(content.len());
1675 for line in content.lines() {
1676 let trimmed = line.trim();
1677 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1678 out.push_str(line);
1679 }
1680 out.push('\n');
1681 }
1682 out
1683}
1684
1685fn strip_code_fences(content: &str) -> String {
1690 let mut out = String::with_capacity(content.len());
1691 let mut open: Option<(char, usize)> = None;
1692 for line in content.lines() {
1693 let trimmed = line.trim();
1694 let fence = ['`', '~'].into_iter().find_map(|c| {
1695 let n = trimmed.chars().take_while(|&x| x == c).count();
1696 (n >= 3).then_some((c, n))
1697 });
1698 match (open, fence) {
1699 (None, Some(f)) => open = Some(f),
1700 (Some((c, n)), Some((fc, fnum)))
1701 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1702 {
1703 open = None;
1704 }
1705 (None, None) => out.push_str(line),
1706 (Some(_), _) => {}
1707 }
1708 out.push('\n');
1709 }
1710 out
1711}
1712
1713fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1716 crate::lexer::decode_string_literal(lexeme, span)
1717}
1718
1719pub fn is_reserved_keyword(kind: TokenKind) -> bool {
1720 use TokenKind::*;
1721 matches!(
1722 kind,
1723 Commons
1724 | Type
1725 | Fn
1726 | Where
1727 | True
1728 | False
1729 | Int
1730 | String
1731 | Bool
1732 | Let
1733 | If
1734 | Else
1735 | Ok
1736 | Err
1737 | Result
1738 | ValidationError
1739 | Enum
1740 | Match
1741 | Option
1742 | Record
1743 | Self_
1744 | Some
1745 | None
1746 | Is
1747 | Opaque
1748 | Uses
1749 | Context
1750 | Consumes
1751 | Exports
1752 | Transparent
1753 | Agent
1754 | As
1755 | Capability
1756 | Effect
1757 | Do
1758 | Given
1759 | On
1760 | Http
1761 | Provides
1762 | Stub
1763 | Service
1764 | Actor
1765 | By
1766 | Expect
1767 | Suite
1768 | Case
1769 | Float
1770 | Duration
1771 | Instant
1772 | Bytes
1773 | Decimal
1774 | JsonError
1775 | Property
1776 | Adapter
1777 | Binding
1778 | Cron
1779 | Queue
1780 | From
1781 | Protocol
1782 | Invariant
1783 | Implies
1784 | Requires
1785 | Ensures
1786 | Transition
1787 )
1788}
1789
1790fn is_item_start(kind: TokenKind) -> bool {
1801 use TokenKind::*;
1802 matches!(
1803 kind,
1804 Commons | Context | Adapter | Suite
1806 | Type | Fn | Messages | Event | Asset | Embed | Const | Uses
1808 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1810 | Binding
1812 | Stub | Case | Property
1814 )
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819 use super::*;
1820 use crate::lexer::tokenize;
1821
1822 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1823 let toks = tokenize(src).map_err(|e| vec![e])?;
1824 parse(&toks, src)
1825 }
1826
1827 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1828 let toks = match tokenize(src) {
1829 Ok(t) => t,
1830 Err(e) => return (None, vec![e]),
1831 };
1832 parse_unit_with_recovery(&toks, src)
1833 }
1834
1835 #[test]
1839 fn contract_clause_name_before_next_line_brace_is_not_a_record() {
1840 for src in [
1841 "commons m\n\nconst Limit: Int = 10\n\nfn small(n: Int) -> Int\n requires small: n <= Limit\n{\n n\n}\n",
1842 "commons m\n\nfn f(n: Int) -> Int\n ensures same: result == n\n{\n n\n}\n",
1843 "commons m\n\nfn f(n: Int) -> Int\n requires r: n <= Limit {\n n\n}\n",
1844 ] {
1845 assert!(parse_str(src).is_ok(), "{src}\n{:?}", parse_str(src).err());
1846 }
1847 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";
1849 assert!(parse_str(src).is_ok(), "{:?}", parse_str(src).err());
1850 }
1851
1852 #[test]
1855 fn contract_clause_unparenthesised_record_literal_is_rejected() {
1856 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";
1857 assert!(parse_str(src).is_err());
1858 }
1859
1860 #[test]
1866 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1867 let src =
1868 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1869 let toks = tokenize(src).unwrap();
1870 let (units, errors) = parse_units_with_recovery(&toks, src);
1871 assert!(errors.is_empty(), "{errors:?}");
1872 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1873 assert!(matches!(units[0], SourceUnit::Commons(_)));
1874 assert!(matches!(units[1], SourceUnit::Suite(_)));
1875
1876 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1878 assert!(errors.is_empty(), "{errors:?}");
1879 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1880 }
1881
1882 #[test]
1889 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1890 let toks = tokenize("").unwrap();
1891 let (units, errors) = parse_units_with_recovery(&toks, "");
1892 assert!(units.is_empty());
1893 assert!(
1894 !errors.is_empty(),
1895 "an empty file must still produce a diagnostic, not silently no units and no error"
1896 );
1897
1898 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1899 assert!(unit.is_none());
1900 assert_eq!(
1901 errors.len(),
1902 unit_errors.len(),
1903 "the singular wrapper must see the same error(s) as the plural entry point"
1904 );
1905 }
1906
1907 #[test]
1914 fn drain_check_reports_expression_interior_comments_as_undrained() {
1915 let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1916 let toks = tokenize(ordinary).unwrap();
1917 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1918 assert!(
1919 drained,
1920 "a declaration-leading comment must be fully drained"
1921 );
1922
1923 let lossy = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
1924 let toks = tokenize(lossy).unwrap();
1925 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1926 assert!(
1927 !drained,
1928 "a comment inside a binop expression must be reported as undrained"
1929 );
1930 }
1931
1932 #[test]
1933 fn eof_span_never_splits_a_multibyte_codepoint() {
1934 for src in [
1939 "commons x {\n // ends with an arrow →",
1940 "agent A {\n key k: String\n // note 🦀",
1941 "commons y {\n type T = é",
1942 ] {
1943 let (_unit, errors) = parse_recover_str(src);
1944 for e in &errors {
1945 assert!(
1946 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1947 "span {:?} splits a codepoint in {src:?}",
1948 e.span,
1949 );
1950 }
1951 }
1952 }
1953
1954 #[test]
1955 fn reserved_contextual_keywords_readable_in_expression_position() {
1956 for kw in [
1966 "asset", "case", "const", "embed", "event", "messages", "on", "suite",
1967 ] {
1968 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1969 let result = parse_str(&src);
1970 assert!(
1971 result.is_ok(),
1972 "a parameter named `{kw}` must be readable in expression position: {:?}",
1973 result.err()
1974 );
1975 }
1976 }
1977
1978 #[test]
1979 fn recovery_skips_garbage_between_decls() {
1980 let src = "commons x {\n\
1983 type A = Int where NonNegative\n\
1984 ??? !!!\n\
1985 type B = String where NonEmpty\n\
1986 }";
1987 let (unit, errors) = parse_recover_str(src);
1988 let unit = unit.expect("recovery should produce a partial AST");
1989 let SourceUnit::Commons(c) = unit else {
1990 panic!("expected commons")
1991 };
1992 let names: Vec<_> = c
1994 .items
1995 .iter()
1996 .map(|i| match i {
1997 CommonsItem::Type(t) => t.name.name.clone(),
1998 _ => panic!("expected only types"),
1999 })
2000 .collect();
2001 assert!(
2002 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
2003 "expected both A and B; got {names:?}",
2004 );
2005 assert!(!errors.is_empty(), "expected at least one parse error");
2006 }
2007
2008 #[test]
2009 fn recovery_handles_bad_first_decl_then_good_second() {
2010 let src = "commons x {\n\
2012 type A Int where NonNegative\n\
2013 type B = String where NonEmpty\n\
2014 }";
2015 let (unit, errors) = parse_recover_str(src);
2016 let unit = unit.expect("recovery should produce a partial AST");
2017 let SourceUnit::Commons(c) = unit else {
2018 panic!("expected commons")
2019 };
2020 let names: Vec<_> = c
2021 .items
2022 .iter()
2023 .filter_map(|i| match i {
2024 CommonsItem::Type(t) => Some(t.name.name.clone()),
2025 _ => None,
2026 })
2027 .collect();
2028 assert!(
2029 names.contains(&"B".to_string()),
2030 "B should be parsed after A's failure; got {names:?}"
2031 );
2032 assert!(!errors.is_empty(), "expected at least one parse error");
2033 }
2034
2035 #[test]
2050 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
2051 let src = "commons m {\n fn f() -> Int\n}\n";
2052 let (unit, errors) = parse_recover_str(src);
2053 assert!(unit.is_some(), "recovery should produce a partial AST");
2054 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
2055 assert_eq!(
2056 categories.len(),
2057 1,
2058 "one syntax error, no follow-on: {categories:?}"
2059 );
2060 assert!(
2061 !categories.contains(&"bynk.parse.unexpected_eof"),
2062 "the commons' closing brace must survive: {categories:?}"
2063 );
2064 }
2065
2066 #[test]
2071 fn recovery_skips_a_rejected_item_whole() {
2072 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
2073 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
2074 assert_eq!(
2075 recovered.errors.len(),
2076 1,
2077 "one syntax error, no follow-on: {:?}",
2078 recovered
2079 .errors
2080 .iter()
2081 .map(|e| e.category)
2082 .collect::<Vec<_>>()
2083 );
2084 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
2085 panic!("expected commons")
2086 };
2087 assert!(
2088 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
2089 "the `fn g` after the skipped agent must still parse"
2090 );
2091 assert_eq!(recovered.broken_decl_names, ["Counter"]);
2092 }
2093
2094 #[test]
2095 fn recovery_skips_a_nested_blocks_own_closing_brace() {
2096 let src = "commons m {\n \
2097 fn f() -> Int {\n \
2098 match 1 {\n \
2099 is 1 -> { let z = }\n \
2100 is _ -> 2\n \
2101 }\n \
2102 }\n \
2103 fn g() -> Int { 2 }\n\
2104 }\n";
2105 let (unit, errors) = parse_recover_str(src);
2106 let unit = unit.expect("recovery should produce a partial AST");
2107 let SourceUnit::Commons(c) = unit else {
2108 panic!("expected commons")
2109 };
2110 let names: Vec<_> = c
2111 .items
2112 .iter()
2113 .filter_map(|i| match i {
2114 CommonsItem::Fn(f) => match &f.name {
2115 FnName::Free(id) => Some(id.name.clone()),
2116 _ => None,
2117 },
2118 _ => None,
2119 })
2120 .collect();
2121 assert_eq!(
2122 names,
2123 vec!["g".to_string()],
2124 "g must still be recovered as an item; got {names:?}"
2125 );
2126 assert!(
2127 !errors
2128 .iter()
2129 .any(|e| e.category == "bynk.parse.expected_unit_header"),
2130 "the outer body's own closing brace must not be mistaken for \
2131 end-of-file: {errors:?}"
2132 );
2133 }
2134
2135 #[test]
2136 fn doc_block_attaches_to_type() {
2137 let c =
2138 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
2139 .unwrap();
2140 let CommonsItem::Type(t) = &c.items[0] else {
2141 panic!()
2142 };
2143 assert!(t.documentation.is_some());
2144 assert!(
2145 t.documentation
2146 .as_ref()
2147 .unwrap()
2148 .contains("A descriptive doc.")
2149 );
2150 }
2151
2152 #[test]
2153 fn interpolated_string_parses_into_parts() {
2154 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
2156 .unwrap();
2157 let CommonsItem::Fn(f) = &c.items[0] else {
2158 panic!("expected fn")
2159 };
2160 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2161 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
2162 };
2163 assert_eq!(parts.len(), 3);
2164 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
2165 assert!(
2166 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
2167 );
2168 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
2169 }
2170
2171 #[test]
2172 fn interpolated_hole_parses_a_full_expression() {
2173 let c =
2175 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
2176 .unwrap();
2177 let CommonsItem::Fn(f) = &c.items[0] else {
2178 panic!("expected fn")
2179 };
2180 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2181 panic!("expected InterpStr")
2182 };
2183 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
2184 }
2185
2186 #[test]
2187 fn empty_interpolation_hole_is_rejected() {
2188 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
2189 assert!(
2190 errs.iter()
2191 .any(|e| e.category == "bynk.parse.empty_interpolation"),
2192 "expected empty_interpolation; got {errs:?}"
2193 );
2194 }
2195
2196 #[test]
2197 fn interpolation_hole_lex_error_span_is_rebased() {
2198 let cases = [
2204 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
2206 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
2208 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
2211 ];
2212 for src in cases {
2213 let errs = parse_str(src).unwrap_err();
2214 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
2215 for e in &errs {
2216 assert!(
2217 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
2218 "span {:?} splits a codepoint in {src:?}",
2219 e.span,
2220 );
2221 let hole_start = src.find("\\(").expect("case has a hole") + 2;
2224 assert!(
2225 e.span.start >= hole_start,
2226 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2227 e.span,
2228 );
2229 }
2230 }
2231 }
2232
2233 #[test]
2234 fn fragment_form_parses() {
2235 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2236 assert_eq!(c.form, CommonsForm::Fragment);
2237 assert_eq!(c.items.len(), 1);
2238 }
2239
2240 #[test]
2241 fn uses_parses() {
2242 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2243 assert_eq!(c.uses.len(), 1);
2244 assert_eq!(c.uses[0].target.joined(), "other.lib");
2245 }
2246
2247 #[test]
2249 fn percent_is_a_targeted_error_with_a_fix_for_simple_operands() {
2250 let err = |expr: &str| {
2251 let src = format!("commons m\n\nfn f(n: Int, k: Int) -> Int {{ {expr} }}\n");
2252 let errs = parse_str(&src).unwrap_err();
2253 assert_eq!(
2254 errs[0].category, "bynk.parse.no_remainder_operator",
2255 "{expr}"
2256 );
2257 errs.into_iter().next().unwrap()
2258 };
2259 let fix = err("n % 2");
2260 assert_eq!(fix.suggestions.len(), 1);
2261 assert_eq!(
2262 fix.notes,
2263 [
2264 "use `a.rem(b)` for the C-family remainder, which takes the dividend's sign, \
2265 or `a.mod(b)` for the modulo, which takes the divisor's sign"
2266 ]
2267 );
2268 assert_eq!(fix.suggestions[0].edits[0].1, "n.rem(2)");
2269 assert_eq!(
2270 fix.suggestions[0].applicability,
2271 crate::error::Applicability::MaybeIncorrect
2272 );
2273 assert_eq!(err("n % k").suggestions[0].edits[0].1, "n.rem(k)");
2274 for complex in [
2275 "(n + 1) % k",
2276 "n * 2 % k",
2277 "n % (k + 1)",
2278 "-n % k",
2279 "n % -k",
2280 ] {
2281 assert!(err(complex).suggestions.is_empty(), "{complex}");
2282 }
2283 }
2284
2285 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2286 let toks = tokenize(src).map_err(|e| vec![e])?;
2287 parse_unit(&toks, src)
2288 }
2289
2290 #[test]
2291 fn minimal_context_parses() {
2292 let u = parse_unit_str("context commerce.orders {}").unwrap();
2293 let SourceUnit::Context(c) = u else {
2294 panic!("expected context");
2295 };
2296 assert_eq!(c.name.joined(), "commerce.orders");
2297 assert!(c.items.is_empty());
2298 }
2299
2300 #[test]
2301 fn context_consumes_and_exports_parse() {
2302 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}";
2303 let u = parse_unit_str(src).unwrap();
2304 let SourceUnit::Context(c) = u else { panic!() };
2305 assert_eq!(c.uses.len(), 1);
2306 assert_eq!(c.consumes.len(), 1);
2307 assert_eq!(c.exports.len(), 2);
2308 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2309 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2310 }
2311
2312 #[test]
2313 fn context_fragment_form_parses() {
2314 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2315 let u = parse_unit_str(src).unwrap();
2316 let SourceUnit::Context(c) = u else { panic!() };
2317 assert_eq!(c.form, CommonsForm::Fragment);
2318 assert_eq!(c.uses.len(), 1);
2319 assert_eq!(c.consumes.len(), 1);
2320 assert_eq!(c.exports.len(), 1);
2321 }
2322
2323 #[test]
2324 fn opaque_type_parses() {
2325 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2326 let CommonsItem::Type(t) = &c.items[0] else {
2327 panic!()
2328 };
2329 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2330 }
2331
2332 #[test]
2333 fn empty_commons() {
2334 let c = parse_str("commons fitness.units {}").unwrap();
2335 assert_eq!(c.name.joined(), "fitness.units");
2336 assert!(c.items.is_empty());
2337 }
2338
2339 #[test]
2340 fn one_type_decl() {
2341 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2342 assert_eq!(c.items.len(), 1);
2343 let CommonsItem::Type(t) = &c.items[0] else {
2344 panic!()
2345 };
2346 assert_eq!(t.name.name, "Metres");
2347 match &t.body {
2348 TypeBody::Refined {
2349 base, refinement, ..
2350 } => {
2351 assert_eq!(*base, BaseType::Int);
2352 assert!(refinement.is_some());
2353 }
2354 _ => panic!("expected refined body"),
2355 }
2356 }
2357
2358 #[test]
2359 fn function_decl() {
2360 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2361 let CommonsItem::Fn(f) = &c.items[0] else {
2362 panic!()
2363 };
2364 assert_eq!(f.name.ident().name, "add");
2365 assert_eq!(f.params.len(), 2);
2366 }
2367
2368 #[test]
2369 fn chained_comparison_is_error() {
2370 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2371 .unwrap_err();
2372 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2373 }
2374
2375 #[test]
2376 fn chained_equality_is_error() {
2377 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2378 .unwrap_err();
2379 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2380 }
2381
2382 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2391 std::thread::Builder::new()
2392 .stack_size(64 * 1024 * 1024)
2393 .spawn(f)
2394 .unwrap()
2395 .join()
2396 .unwrap()
2397 }
2398
2399 #[test]
2400 fn deeply_nested_parens_are_bounded_not_overflowed() {
2401 let errs = on_big_stack(|| {
2407 let depth = crate::MAX_NESTING_DEPTH + 8;
2408 let src = format!(
2409 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2410 "(".repeat(depth),
2411 ")".repeat(depth),
2412 );
2413 parse_str(&src).unwrap_err()
2414 });
2415 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2416 }
2417
2418 #[test]
2419 fn deeply_nested_types_are_bounded_not_overflowed() {
2420 let errs = on_big_stack(|| {
2425 let depth = crate::MAX_NESTING_DEPTH + 8;
2426 let src = format!(
2427 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2428 "Result[Int, ".repeat(depth),
2429 "]".repeat(depth),
2430 );
2431 parse_str(&src).unwrap_err()
2432 });
2433 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2434 }
2435
2436 #[test]
2437 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2438 let errs = on_big_stack(|| {
2443 let depth = crate::MAX_NESTING_DEPTH + 8;
2444 let src = format!(
2445 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2446 "Ok(".repeat(depth),
2447 ")".repeat(depth),
2448 );
2449 parse_str(&src).unwrap_err()
2450 });
2451 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2452 }
2453
2454 #[test]
2455 fn nesting_below_the_limit_still_parses() {
2456 let ok = on_big_stack(|| {
2459 let depth = crate::MAX_NESTING_DEPTH - 8;
2460 let src = format!(
2461 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2462 "(".repeat(depth),
2463 ")".repeat(depth),
2464 );
2465 parse_str(&src).is_ok()
2466 });
2467 assert!(ok, "well-nested source under the limit should parse");
2468 }
2469
2470 #[test]
2471 fn let_statement_parses() {
2472 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2473 let CommonsItem::Fn(f) = &c.items[0] else {
2474 panic!()
2475 };
2476 assert_eq!(f.body.statements.len(), 1);
2477 match &f.body.statements[0] {
2478 Statement::Let(l) => {
2479 assert_eq!(l.name.name, "y");
2480 assert!(l.type_annot.is_none());
2481 }
2482 _ => panic!("expected a pure `let` statement"),
2483 }
2484 }
2485
2486 #[test]
2487 fn let_with_annotation() {
2488 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2489 let CommonsItem::Fn(f) = &c.items[0] else {
2490 panic!()
2491 };
2492 match &f.body.statements[0] {
2493 Statement::Let(l) => assert!(l.type_annot.is_some()),
2494 _ => panic!("expected a pure `let` statement"),
2495 }
2496 }
2497
2498 #[test]
2499 fn if_else_parses_as_expression() {
2500 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2501 let CommonsItem::Fn(f) = &c.items[0] else {
2502 panic!()
2503 };
2504 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2505 }
2506
2507 #[test]
2508 fn else_if_chain_parses() {
2509 let c = parse_str(
2510 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2511 )
2512 .unwrap();
2513 let CommonsItem::Fn(f) = &c.items[0] else {
2514 panic!()
2515 };
2516 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2517 panic!()
2518 };
2519 assert!(else_block.statements.is_empty());
2521 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2522 }
2523
2524 #[test]
2525 fn ok_and_err_parse_as_expressions() {
2526 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2527 let CommonsItem::Fn(f) = &c.items[0] else {
2528 panic!()
2529 };
2530 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2531
2532 let c =
2533 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2534 let CommonsItem::Fn(f) = &c.items[0] else {
2535 panic!()
2536 };
2537 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2538 }
2539
2540 #[test]
2541 fn question_postfix_parses() {
2542 let c = parse_str(
2543 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2544 )
2545 .unwrap();
2546 let CommonsItem::Fn(f) = &c.items[1] else {
2547 panic!()
2548 };
2549 let Statement::Let(l) = &f.body.statements[0] else {
2550 panic!("expected a pure `let` statement");
2551 };
2552 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2553 }
2554
2555 #[test]
2556 fn constructor_call_parses() {
2557 let c = parse_str(
2558 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2559 )
2560 .unwrap();
2561 let CommonsItem::Fn(f) = &c.items[1] else {
2562 panic!()
2563 };
2564 let ExprKind::MethodCall {
2567 receiver, method, ..
2568 } = &f.body.tail.kind
2569 else {
2570 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2571 };
2572 let ExprKind::Ident(id) = &receiver.kind else {
2573 panic!("expected receiver Ident");
2574 };
2575 assert_eq!(id.name, "T");
2576 assert_eq!(method.name, "of");
2577 }
2578
2579 #[test]
2580 fn result_type_ref_parses() {
2581 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2582 let CommonsItem::Fn(f) = &c.items[0] else {
2583 panic!()
2584 };
2585 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2586 }
2587
2588 #[test]
2589 fn result_missing_arg_count_errors() {
2590 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2591 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2592 }
2593
2594 #[test]
2595 fn field_access_parses_in_v0_2() {
2596 let c =
2599 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2600 let CommonsItem::Fn(f) = &c.items[1] else {
2601 panic!()
2602 };
2603 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2604 }
2605
2606 #[test]
2609 fn leading_line_comment_attaches_to_next_decl() {
2610 let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2611 let c = parse_str(src).unwrap();
2612 let CommonsItem::Type(t) = &c.items[0] else {
2613 panic!()
2614 };
2615 assert_eq!(
2616 t.trivia.leading,
2617 vec![Comment::Line(" explain the type".to_string())]
2618 );
2619 assert!(t.trivia.trailing.is_none());
2620 }
2621
2622 #[test]
2623 fn trailing_line_comment_attaches_to_prev_decl() {
2624 let src = "commons x {\ntype T = Int where NonNegative // trailing note\n}";
2625 let c = parse_str(src).unwrap();
2626 let CommonsItem::Type(t) = &c.items[0] else {
2627 panic!()
2628 };
2629 assert!(t.trivia.leading.is_empty());
2630 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2631 }
2632
2633 #[test]
2634 fn grouped_leading_comments_attach_together() {
2635 let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2636 let c = parse_str(src).unwrap();
2637 let CommonsItem::Type(t) = &c.items[0] else {
2638 panic!()
2639 };
2640 assert_eq!(
2641 t.trivia.leading,
2642 vec![
2643 Comment::Line(" one".to_string()),
2644 Comment::Line(" two".to_string()),
2645 Comment::Line(" three".to_string())
2646 ],
2647 );
2648 }
2649
2650 #[test]
2651 fn comment_with_doc_block_keeps_both() {
2652 let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2654 let c = parse_str(src).unwrap();
2655 let CommonsItem::Type(t) = &c.items[0] else {
2656 panic!()
2657 };
2658 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2659 assert_eq!(t.documentation.as_deref(), Some("docs"));
2660 }
2661
2662 #[test]
2663 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2664 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2670 let c = parse_str(src).unwrap();
2671 assert_eq!(c.name.joined(), "app.messages");
2672 }
2673
2674 #[test]
2675 fn messages_decl_parses_tag_annotation_and_entries() {
2676 let src = "commons app.messages {\n\
2678 // intro\n\
2679 ---\n\
2680 docs\n\
2681 ---\n\
2682 messages \"en\" @reference {\n\
2683 \"greeting\" => \"Hello, {name}!\"\n\
2684 \"farewell\" => \"Bye\"\n\
2685 } // trailing\n\
2686 }";
2687 let c = parse_str(src).unwrap();
2688 let CommonsItem::Messages(m) = &c.items[0] else {
2689 panic!("expected a messages item, got {:?}", c.items[0]);
2690 };
2691 assert_eq!(m.tag, "en");
2692 assert_eq!(m.annotations.len(), 1);
2693 assert_eq!(m.annotations[0].name.name, "reference");
2694 assert!(m.annotations[0].args.is_empty());
2695 assert_eq!(m.entries.len(), 2);
2696 assert_eq!(m.entries[0].code, "greeting");
2697 assert_eq!(m.entries[0].template, "Hello, {name}!");
2698 assert_eq!(m.entries[1].code, "farewell");
2699 assert_eq!(m.entries[1].template, "Bye");
2700 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2701 assert_eq!(m.documentation.as_deref(), Some("docs"));
2702 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2703 }
2704
2705 #[test]
2706 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2707 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2711 let c = parse_str(src).unwrap();
2712 let CommonsItem::Messages(m) = &c.items[0] else {
2713 panic!("expected a messages item, got {:?}", c.items[0]);
2714 };
2715 assert_eq!(m.tag, "en");
2716 assert!(m.annotations.is_empty());
2717 assert!(m.entries.is_empty());
2718 }
2719
2720 #[test]
2721 fn messages_decl_parses_syntactically_inside_a_context_too() {
2722 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2727 let toks = tokenize(src).unwrap();
2728 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2729 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2730 let Some(SourceUnit::Context(ctx)) = unit else {
2731 panic!("expected a context")
2732 };
2733 let CommonsItem::Messages(m) = &ctx.items[0] else {
2734 panic!("expected a messages item, got {:?}", ctx.items[0]);
2735 };
2736 assert_eq!(m.tag, "en");
2737 }
2738
2739 #[test]
2740 fn comment_before_let_statement_attaches() {
2741 let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2742 let c = parse_str(src).unwrap();
2743 let CommonsItem::Fn(f) = &c.items[0] else {
2744 panic!()
2745 };
2746 let Statement::Let(l) = &f.body.statements[0] else {
2747 panic!()
2748 };
2749 assert_eq!(
2750 l.trivia.leading,
2751 vec![Comment::Line(" pick a value".to_string())]
2752 );
2753 }
2754
2755 #[test]
2756 fn comment_before_tail_attaches_to_block_tail() {
2757 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2758 let c = parse_str(src).unwrap();
2759 let CommonsItem::Fn(f) = &c.items[0] else {
2760 panic!()
2761 };
2762 assert_eq!(
2763 f.body.tail_leading_comments,
2764 vec![Comment::Line(" result".to_string())],
2765 );
2766 }
2767
2768 #[test]
2774 fn contextual_keywords_are_valid_identifiers() {
2775 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2777 .expect("`on`/`suite`/`case` are valid field names");
2778 let CommonsItem::Type(_) = &c.items[0] else {
2779 panic!("expected a type decl")
2780 };
2781
2782 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2784 .expect("`on`/`case` are valid parameter names");
2785
2786 parse_str("commons demo {\n type R = { suite: Int }\n}")
2788 .expect("`suite` is a valid field name");
2789 }
2790
2791 #[test]
2794 fn keywords_are_labels_and_members_not_bindings() {
2795 for src in [
2796 "commons demo {\n type A = { type: String, from: Int, self: Bool }\n}",
2797 "commons demo {\n type S = | P(from: String, type: String)\n}",
2798 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { a.type }\n}",
2799 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type: \"x\" } }\n}",
2800 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { match a { A { type: t, .. } => t } }\n}",
2801 "commons demo {\n type S = | P(from: String)\n fn f(s: S) -> String { match s { P(from: x) => x } }\n}",
2802 "commons demo {\n type S = { n: Int }\n fn S.from(n: Int) -> S { S { n: n } }\n fn g() -> S { S.from(1) }\n}",
2803 ] {
2804 parse_str(src).unwrap_or_else(|e| panic!("should parse: {src}\n{e:?}"));
2805 }
2806 for src in [
2807 "commons demo {\n fn f(type: Int) -> Int { 1 }\n}",
2808 "commons demo {\n fn f() -> Int { let from = 1\n from }\n}",
2809 "commons demo {\n type A = { type String }\n}",
2810 "commons demo {\n fn f(a: Int) -> Int {\n a.\n let x = 1\n x\n }\n}",
2812 ] {
2813 let errs = parse_str(src).expect_err(src);
2814 assert_eq!(errs[0].category, "bynk.parse.reserved_keyword", "{src}");
2815 }
2816 }
2817
2818 #[test]
2822 fn keyword_shorthand_is_a_targeted_error() {
2823 let pat = |extra: &str| {
2824 format!(
2825 "commons demo {{\n type A = {{ type: String }}\n fn f(a: A) -> String {{ {extra} match a {{ A {{ type, .. }} => \"x\" }} }}\n}}"
2826 )
2827 };
2828 let errs = parse_str(&pat("")).expect_err("pattern shorthand");
2829 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2830 let fix = &errs[0].suggestions[0];
2831 assert_eq!(fix.edits[0].1, "type: type_");
2832 assert!(matches!(
2833 fix.applicability,
2834 crate::error::Applicability::MaybeIncorrect
2835 ));
2836
2837 let errs = parse_str(&pat("let type_ = 1")).expect_err("pattern shorthand");
2838 assert_eq!(errs[0].suggestions[0].edits[0].1, "type: type_2");
2839
2840 let errs = parse_str(
2841 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type } }\n}",
2842 )
2843 .expect_err("construction shorthand");
2844 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2845 assert!(errs[0].suggestions.is_empty());
2846 }
2847
2848 #[test]
2856 fn is_reserved_keyword_covers_every_lexer_keyword() {
2857 let lexer_src = include_str!("lexer.rs");
2858 let mut words = Vec::new();
2859 for line in lexer_src.lines() {
2860 let t = line.trim();
2861 if let Some(rest) = t.strip_prefix("#[token(\"")
2862 && let Some(word) = rest.split('"').next()
2863 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2864 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2865 {
2866 words.push(word.to_string());
2867 }
2868 }
2869 assert!(
2870 words.len() > 30,
2871 "keyword extraction looks broken: only {} words",
2872 words.len()
2873 );
2874 use crate::keywords::RESERVED_CONTEXTUAL;
2877 let mut unclassified = Vec::new();
2878 for word in &words {
2879 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2880 let kind = tokens.first().expect("keyword yields a token").kind;
2881 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2882 unclassified.push(word.clone());
2883 }
2884 }
2885 assert!(
2886 unclassified.is_empty(),
2887 "keywords missing from is_reserved_keyword (add them, or document \
2888 them as contextual): {unclassified:?}"
2889 );
2890 }
2891
2892 #[test]
2903 fn is_item_start_matches_the_pinned_keyword_set() {
2904 use TokenKind::*;
2905 let expected_true = [
2906 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Embed, Const, Uses,
2907 Consumes, Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case,
2908 Property,
2909 ];
2910 for kind in expected_true {
2911 assert!(is_item_start(kind), "{kind:?} must be an item start");
2912 }
2913 let expected_false = [
2914 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2915 Where, True, False, Match, Is, On, Given,
2916 ];
2917 for kind in expected_false {
2918 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2919 }
2920 }
2921
2922 #[test]
2927 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2928 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2929 let tokens = crate::lexer::tokenize(src).unwrap();
2930 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2931 assert!(unit.is_some(), "the commons header still parses");
2932 assert!(
2933 errors
2934 .iter()
2935 .any(|e| e.category == "bynk.capability.outside_context"),
2936 "the misplaced capability is reported: {errors:?}"
2937 );
2938 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2941 }
2942
2943 #[test]
2944 fn trailing_file_comment_becomes_unit_trailing() {
2945 let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2949 let c = parse_str(src).unwrap();
2950 assert_eq!(
2951 c.trailing_comments,
2952 vec![Comment::Line(" afterword".to_string())]
2953 );
2954 }
2955
2956 #[test]
2957 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2958 let src = "commons x {\n type T = Int where Positive\n}\n// afterword\n";
2965 let c = parse_str(src).unwrap();
2966 assert_eq!(
2967 c.trailing_comments,
2968 vec![Comment::Line(" afterword".to_string())]
2969 );
2970 }
2971
2972 #[test]
2973 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2974 let src = "context x {\n type T = Int where Positive\n}\n// afterword\n";
2976 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2977 panic!("expected context");
2978 };
2979 assert_eq!(
2980 c.trailing_comments,
2981 vec![Comment::Line(" afterword".to_string())]
2982 );
2983 }
2984
2985 #[test]
2986 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2987 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n// afterword\n";
2989 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2990 panic!("expected suite");
2991 };
2992 assert_eq!(
2993 s.trailing_comments,
2994 vec![Comment::Line(" afterword".to_string())]
2995 );
2996 }
2997
2998 #[test]
3005 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
3006 let src = "adapter x {\n binding \"./x.ts\"\n}\n// afterword\n";
3007 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3008 panic!("expected adapter");
3009 };
3010 assert_eq!(
3011 a.trailing_comments,
3012 vec![Comment::Line(" afterword".to_string())]
3013 );
3014 }
3015
3016 #[test]
3019 fn binding_includes_list_parses_before_requires() {
3020 let src = "adapter x {\n binding \"./x.ts\" includes [\"./a.wasm\", \"b/c.wasm\"] requires { \"p\": \"^1\" }\n}\n";
3021 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3022 panic!("expected adapter");
3023 };
3024 let b = a.binding.unwrap();
3025 let paths: Vec<_> = b.includes.iter().map(|i| i.path.as_str()).collect();
3026 assert_eq!(paths, ["./a.wasm", "b/c.wasm"]);
3027 assert_eq!(b.requires.len(), 1);
3028 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [\"a.wasm\"").is_err());
3029 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [a]\n}\n").is_err());
3030 }
3031
3032 #[test]
3039 fn commons_fragment_rejects_uses_after_a_decl() {
3040 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
3041 let errs = parse_str(src).unwrap_err();
3042 assert!(
3043 errs.iter()
3044 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3045 "{errs:?}"
3046 );
3047 }
3048
3049 #[test]
3052 fn commons_brace_allows_uses_after_a_decl() {
3053 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
3054 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3055 }
3056
3057 #[test]
3059 fn context_fragment_rejects_consumes_after_a_decl() {
3060 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
3061 let errs = parse_unit_str(src).unwrap_err();
3062 assert!(
3063 errs.iter()
3064 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
3065 "{errs:?}"
3066 );
3067 }
3068
3069 #[test]
3071 fn context_fragment_rejects_exports_after_a_decl() {
3072 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
3073 let errs = parse_unit_str(src).unwrap_err();
3074 assert!(
3075 errs.iter()
3076 .any(|e| e.category == "bynk.parse.exports_after_decls"),
3077 "{errs:?}"
3078 );
3079 }
3080
3081 #[test]
3083 fn context_brace_allows_consumes_and_exports_after_a_decl() {
3084 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
3085 parse_unit_str(src)
3086 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
3087 }
3088
3089 #[test]
3091 fn test_fragment_rejects_uses_after_a_decl() {
3092 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
3093 let errs = parse_unit_str(src).unwrap_err();
3094 assert!(
3095 errs.iter()
3096 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3097 "{errs:?}"
3098 );
3099 }
3100
3101 #[test]
3103 fn test_brace_allows_uses_after_a_decl() {
3104 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
3105 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3106 }
3107
3108 fn body_tail(body: &str) -> ExprKind {
3112 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3113 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3114 let CommonsItem::Fn(f) = &c.items[0] else {
3115 panic!("expected fn, got {:?}", c.items[0]);
3116 };
3117 f.body.tail.kind.clone()
3118 }
3119
3120 fn body_err(body: &str) -> Vec<CompileError> {
3121 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3122 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
3123 }
3124
3125 fn apply_fix(src: &str, err: &CompileError) -> String {
3129 let s = &err.suggestions[0];
3130 assert_eq!(
3131 s.applicability,
3132 crate::error::Applicability::MachineApplicable
3133 );
3134 let mut edits = s.edits.clone();
3135 edits.sort_by_key(|(span, _)| std::cmp::Reverse(span.start));
3136 let mut out = src.to_string();
3137 for (span, text) in edits {
3138 out.replace_range(span.start..span.end, &text);
3139 }
3140 out
3141 }
3142
3143 #[test]
3146 fn list_patterns_parse_in_every_shape() {
3147 let ExprKind::Match { arms, .. } = body_tail(
3148 "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 }",
3149 ) else {
3150 panic!("expected Match");
3151 };
3152 let shape = |i: usize| match &arms[i].pattern {
3153 Pattern::List { elems, rest, .. } => (
3154 elems.len(),
3155 rest.as_ref()
3156 .map(|r| r.binder.as_ref().map(|b| b.name.clone())),
3157 ),
3158 other => panic!("arm {i}: expected a list pattern, got {other:?}"),
3159 };
3160 assert_eq!(shape(0), (0, None));
3161 assert_eq!(shape(1), (1, None));
3162 assert_eq!(shape(2), (2, Some(None)));
3163 assert_eq!(shape(3), (1, Some(Some("rest".to_string()))));
3164 assert_eq!(shape(4), (0, Some(Some("all".to_string()))));
3165 assert_eq!(shape(5), (0, Some(None)));
3166 assert_eq!(shape(6), (1, Some(None)));
3167 assert!(matches!(&arms[7].pattern, Pattern::Variant { bindings, .. }
3168 if matches!(bindings[0].pattern(), Pattern::List { .. })));
3169 assert!(!arms[0].pattern.is_irrefutable());
3171 assert!(!arms[2].pattern.is_irrefutable());
3172 assert!(arms[4].pattern.is_irrefutable());
3173 assert!(arms[5].pattern.is_irrefutable());
3174 }
3175
3176 #[test]
3177 fn list_pattern_after_is_closes_at_the_bracket() {
3178 for body in [
3179 "if xs is [_, ..] { 1 } else { 2 }",
3180 "if xs is [] { 1 } else { 2 }",
3181 "if xs is [a, ..rest] { 1 } else { 2 }",
3182 "if xs is [S { n: 1, .. }, ..] { 1 } else { 2 }",
3183 "if xs is [] | [_] { 1 } else { 2 }",
3184 ] {
3185 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3186 parse_str(&src).unwrap_or_else(|e| panic!("{body:?}: {e:?}"));
3187 }
3188 }
3189
3190 #[test]
3191 fn list_rest_must_be_last() {
3192 for body in [
3193 "match xs { [.., last] => 1 }",
3194 "match xs { [..r, last] => 1 }",
3195 "match xs { [.., ..] => 1 }",
3196 "match xs { [a, .., b, c] => 1 }",
3197 ] {
3198 let errs = body_err(body);
3199 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last", "{body}");
3200 assert!(
3201 errs[0].suggestions.is_empty(),
3202 "{body}: only trailing wildcards earn a fix"
3203 );
3204 }
3205 }
3206
3207 #[test]
3208 fn list_rest_followed_by_wildcards_has_a_fix() {
3209 for (body, fixed) in [
3210 ("match xs { [.., _] => 1 }", "match xs { [_, ..] => 1 }"),
3211 (
3212 "match xs { [a, .., _, _] => 1 }",
3213 "match xs { [a, _, _, ..] => 1 }",
3214 ),
3215 ] {
3216 let errs = body_err(body);
3217 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last");
3218 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3219 let got = apply_fix(&src, &errs[0]);
3220 assert!(got.contains(fixed), "{body}: fixed to {got:?}");
3221 parse_str(&got).unwrap_or_else(|e| panic!("{body:?} fixed to {got:?}: {e:?}"));
3222 }
3223 }
3224
3225 #[test]
3226 fn list_rest_followed_by_an_integer_is_a_range_element() {
3227 for body in [
3229 "match xs { [..9] => 1 _ => 0 }",
3230 "match xs { [a, ..-1] => 1 _ => 0 }",
3231 "match xs { [0..9, ..rest] => 1 _ => 0 }",
3232 "match xs { [18.., ..] => 1 _ => 0 }",
3233 ] {
3234 let ExprKind::Match { arms, .. } = body_tail(body) else {
3235 panic!("expected Match: {body}");
3236 };
3237 let Pattern::List { elems, .. } = &arms[0].pattern else {
3238 panic!("expected a list pattern: {body}");
3239 };
3240 assert!(
3241 elems.iter().any(|e| matches!(e, Pattern::Range(_))),
3242 "{body}"
3243 );
3244 }
3245 for body in ["match xs { [..\"a\"] => 1 }", "match xs { [..true] => 1 }"] {
3247 let errs = body_err(body);
3248 assert_eq!(errs[0].category, "bynk.parse.expected_token", "{body}");
3249 }
3250 let errs = body_err("match xs { [a, .._] => 1 }");
3252 assert_eq!(errs[0].category, "bynk.parse.expected_token");
3253 }
3254
3255 #[test]
3256 fn int_range_patterns_parse_in_every_form() {
3257 let range = |body: &str| {
3258 let ExprKind::Match { arms, .. } = body_tail(body) else {
3259 panic!("expected Match: {body}");
3260 };
3261 match &arms[0].pattern {
3262 Pattern::Range(r) => (r.lo, r.hi),
3263 other => panic!("expected a range: {body}: {other:?}"),
3264 }
3265 };
3266 assert_eq!(range("match n { 0..12 => 1 _ => 0 }"), (Some(0), Some(12)));
3267 assert_eq!(range("match n { 18.. => 1 _ => 0 }"), (Some(18), None));
3268 assert_eq!(range("match n { ..-1 => 1 _ => 0 }"), (None, Some(-1)));
3269 assert_eq!(
3270 range("match n { -5..-1 => 1 _ => 0 }"),
3271 (Some(-5), Some(-1))
3272 );
3273 assert_eq!(
3274 range("match n { 1_000..2_000 => 1 _ => 0 }"),
3275 (Some(1000), Some(2000))
3276 );
3277 let ExprKind::Match { arms, .. } = body_tail("match n { 18.. => 1 5 => 2 _ => 0 }") else {
3279 panic!("expected Match");
3280 };
3281 assert!(matches!(arms[0].pattern, Pattern::Range(_)));
3282 assert!(matches!(arms[1].pattern, Pattern::Literal { .. }));
3283 }
3284
3285 #[test]
3286 fn a_range_is_not_an_event_pattern_value() {
3287 let src = "service S from Events(E { n: 1..5 }) { }";
3289 assert!(parse_str(src).is_err());
3290 }
3291
3292 #[test]
3293 fn record_pattern_parses_in_a_match_arm_and_nested() {
3294 let ExprKind::Match { arms, .. } = body_tail(
3295 "match o { Order { status: Paid, total, .. } => 1 Some(Order { .. }) => 2 _ => 3 }",
3296 ) else {
3297 panic!("expected Match");
3298 };
3299 let Pattern::Record {
3300 type_name, fields, ..
3301 } = &arms[0].pattern
3302 else {
3303 panic!("expected a record pattern, got {:?}", arms[0].pattern);
3304 };
3305 assert_eq!(type_name.name, "Order");
3306 assert_eq!(fields.len(), 2);
3307 assert!(fields[0].1.is_some(), "`status: Paid` has a sub-pattern");
3308 assert!(fields[1].1.is_none(), "`total` is shorthand");
3309 let Pattern::Variant { bindings, .. } = &arms[1].pattern else {
3310 panic!("expected Some(..)");
3311 };
3312 assert!(matches!(bindings[0].pattern(), Pattern::Record { .. }));
3313 }
3314
3315 #[test]
3316 fn record_pattern_without_rest_is_an_error_with_a_fix() {
3317 for body in [
3318 "match o { Order { total } => 1 }",
3319 "match o { Order { total, } => 1 }",
3320 "match o { Order { status: Paid, total } => 1 }",
3321 "match o { Order { } => 1 }",
3322 ] {
3323 let errs = body_err(body);
3324 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3325 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3326 let fixed = apply_fix(&src, &errs[0]);
3327 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3328 }
3329 }
3330
3331 #[test]
3332 fn record_pattern_after_is_must_be_parenthesised() {
3333 for body in [
3334 "if o is Order { status: Paid, .. } { 1 } else { 2 }",
3335 "if o is None | Order { .. } { 1 } else { 2 }",
3336 ] {
3337 let errs = body_err(body);
3338 assert_eq!(
3339 errs[0].category,
3340 "bynk.parse.record_pattern_in_is_unparenthesised"
3341 );
3342 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3343 if body.contains("None |") {
3344 continue; }
3346 let fixed = apply_fix(&src, &errs[0]);
3347 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3348 }
3349 for body in [
3351 "if o is (Order { status: Paid, .. }) { 1 } else { 2 }",
3352 "if o is Some(Order { .. }) { 1 } else { 2 }",
3353 "if o is (None | Order { .. }) { 1 } else { 2 }",
3354 "if o is Paid { 1 } else { 2 }",
3355 ] {
3356 body_tail(body);
3357 }
3358 }
3359
3360 #[test]
3361 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
3362 for src in [
3365 "if ready { result } else { fallback }",
3366 "if ready { fallback } else { result }",
3367 "if !ready { result } else { fallback }",
3368 "if a == b { result } else { fallback }",
3369 "if a && b { result } else { fallback }",
3370 ] {
3371 let ExprKind::If {
3372 then_block,
3373 else_block,
3374 ..
3375 } = body_tail(src)
3376 else {
3377 panic!("expected If for {src:?}, got {:?}", body_tail(src));
3378 };
3379 assert!(
3382 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
3383 "then-branch tail not an ident for {src:?}: {:?}",
3384 then_block.tail.kind,
3385 );
3386 assert!(
3387 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
3388 "else-branch tail not an ident for {src:?}: {:?}",
3389 else_block.tail.kind,
3390 );
3391 }
3392 }
3393
3394 #[test]
3395 fn else_less_if_with_single_ident_branch_parses() {
3396 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
3398 panic!("expected If");
3399 };
3400 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
3401 }
3402
3403 #[test]
3404 fn record_construction_still_parses_in_value_position() {
3405 assert!(matches!(
3408 body_tail("Point { x }"),
3409 ExprKind::RecordConstruction { .. }
3410 ));
3411 assert!(matches!(
3412 body_tail("Point { x: 1, y: 2 }"),
3413 ExprKind::RecordConstruction { .. }
3414 ));
3415 assert!(matches!(
3416 body_tail("Empty {}"),
3417 ExprKind::RecordConstruction { .. }
3418 ));
3419 }
3420
3421 #[test]
3422 fn parenthesised_record_is_allowed_in_condition_head() {
3423 let ExprKind::If { cond, .. } =
3426 body_tail("if (ready { result }) { branch } else { other }")
3427 else {
3428 panic!("expected If");
3429 };
3430 let ExprKind::Paren(inner) = &cond.kind else {
3431 panic!("expected a parenthesised condition, got {:?}", cond.kind);
3432 };
3433 assert!(
3434 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
3435 "parenthesised record in condition head should still construct: {:?}",
3436 inner.kind,
3437 );
3438 }
3439
3440 #[test]
3441 fn record_in_call_arg_within_condition_still_constructs() {
3442 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
3445 else {
3446 panic!("expected If");
3447 };
3448 let ExprKind::Call { args, .. } = &cond.kind else {
3449 panic!("expected Call in condition, got {:?}", cond.kind);
3450 };
3451 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
3452 }
3453
3454 #[test]
3455 fn safe_condition_shapes_are_unaffected() {
3456 assert!(matches!(
3458 body_tail("if ready == true { result } else { fallback }"),
3459 ExprKind::If { .. }
3460 ));
3461 assert!(matches!(
3462 body_tail("if (ready) { result } else { fallback }"),
3463 ExprKind::If { .. }
3464 ));
3465 assert!(matches!(
3466 body_tail("if ready { \"a\" } else { \"b\" }"),
3467 ExprKind::If { .. }
3468 ));
3469 }
3470
3471 fn body_block(body: &str) -> Block {
3472 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3473 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3474 let CommonsItem::Fn(f) = &c.items[0] else {
3475 panic!("expected fn, got {:?}", c.items[0]);
3476 };
3477 f.body.clone()
3478 }
3479
3480 fn has_error(errs: &[CompileError], code: &str) -> bool {
3481 errs.iter().any(|e| e.category == code)
3482 }
3483
3484 fn lambda_of(body: &str) -> LambdaExpr {
3487 let b = body_block(body);
3488 let ExprKind::Lambda(l) = &b.tail.kind else {
3489 panic!("expected a lambda tail, got {:?}", b.tail.kind);
3490 };
3491 l.clone()
3492 }
3493
3494 #[test]
3495 fn a_record_pattern_parameter_desugars_to_a_hidden_binder_and_a_let() {
3496 let l = lambda_of("(Item { qty, .. }) => qty");
3498 assert_eq!(l.params.len(), 1);
3499 let p = &l.params[0];
3500 assert!(p.name.name.starts_with(LAMBDA_ARG_BINDER_PREFIX));
3501 assert!(is_generated_binding(&p.name.name));
3502 assert_eq!(
3503 p.name.span.start, p.name.span.end,
3504 "the binder has no extent"
3505 );
3506 assert!(p.type_ref.is_none());
3507 assert!(matches!(p.pattern, Some(Pattern::Record { .. })));
3508 assert!(!l.block_body);
3509 let ExprKind::Block(b) = &l.body.kind else {
3510 panic!("expected a block body, got {:?}", l.body.kind);
3511 };
3512 let Statement::Let(hidden) = &b.statements[0] else {
3513 panic!("expected the hidden `let`, got {:?}", b.statements[0]);
3514 };
3515 assert_eq!(
3516 hidden.let_else.as_ref().map(|le| le.form),
3517 Some(LetElseForm::Plain)
3518 );
3519 assert!(b.tail.is_let_else_match());
3520 assert!(
3521 matches!(&hidden.value.kind, ExprKind::Ident(i) if i.name == p.name.name),
3522 "the `let` destructures the hidden parameter"
3523 );
3524 }
3525
3526 #[test]
3527 fn name_and_pattern_parameters_mix_and_each_binder_is_distinct() {
3528 let l = lambda_of("(a, Item { qty, .. }, b: Int, Box { v, .. }) => a");
3529 let names: Vec<_> = l.params.iter().map(|p| p.name.name.as_str()).collect();
3530 assert_eq!(names[0], "a");
3531 assert_eq!(names[2], "b");
3532 assert!(names[1].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3533 assert!(names[3].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3534 assert_ne!(names[1], names[3]);
3535 assert!(l.params[0].pattern.is_none() && l.params[2].type_ref.is_some());
3536 let ExprKind::Block(outer) = &l.body.kind else {
3538 panic!("expected a block body");
3539 };
3540 let ExprKind::Match { arms, .. } = &outer.tail.kind else {
3541 panic!("expected the first pattern's match");
3542 };
3543 let MatchBody::Block(inner) = &arms[0].body else {
3544 panic!("expected the arm's block");
3545 };
3546 assert!(inner.tail.is_let_else_match(), "the second pattern nests");
3547 }
3548
3549 #[test]
3550 fn a_block_body_is_kept_as_the_pattern_arms_block() {
3551 let l = lambda_of("(Item { qty, .. }) => { let x = qty\n x }");
3552 assert!(l.block_body);
3553 }
3554
3555 #[test]
3556 fn a_lambda_without_pattern_parameters_is_unchanged() {
3557 let l = lambda_of("(a, b: Int) => a");
3558 assert!(l.params.iter().all(|p| p.pattern.is_none()));
3559 assert!(matches!(l.body.kind, ExprKind::Ident(_)));
3560 }
3561
3562 #[test]
3563 fn a_lambda_pattern_parameter_that_is_refutable_names_its_first_failing_field() {
3564 let errs = body_err("(Item { status: Paid, qty, .. }) => qty");
3565 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_refutable");
3566 assert!(
3567 errs[0].notes.iter().any(|n| n.contains("status: Paid")),
3568 "{:?}",
3569 errs[0].notes
3570 );
3571 }
3572
3573 #[test]
3574 fn a_lambda_pattern_parameter_must_be_a_record_pattern() {
3575 for body in [
3576 "(Some(x)) => x",
3577 "(a, [x, ..]) => x",
3578 "(0..9) => 1",
3579 "(1) => 1",
3580 "(None) => 1",
3581 "(Status.Paid) => 1",
3582 ] {
3583 let errs = body_err(body);
3584 assert_eq!(
3585 errs[0].category, "bynk.parse.lambda_pattern_not_record",
3586 "{body}"
3587 );
3588 }
3589 }
3590
3591 #[test]
3592 fn an_annotated_lambda_pattern_parameter_offers_to_remove_the_annotation() {
3593 let body = "(Item { qty, .. }: Item) => qty";
3594 let errs = body_err(body);
3595 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_annotation");
3596 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3597 let fixed = apply_fix(&src, &errs[0]);
3598 assert!(fixed.contains("(Item { qty, .. }) => qty"), "{fixed}");
3599 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3600 }
3601
3602 #[test]
3603 fn a_missing_rest_in_a_lambda_pattern_parameter_has_slice_ones_fix() {
3604 let body = "(a, Item { qty }) => qty";
3605 let errs = body_err(body);
3606 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3607 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3608 let fixed = apply_fix(&src, &errs[0]);
3609 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3610 }
3611
3612 #[test]
3613 fn let_else_desugars_to_a_hidden_binding_and_a_match() {
3614 let b = body_block("let x = 1\n let Some(n) = find(x) else { 0 }\n n + x");
3617 assert_eq!(
3618 b.statements.len(),
3619 2,
3620 "the plain let and the hidden binding"
3621 );
3622 let Statement::Let(hidden) = &b.statements[1] else {
3623 panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
3624 };
3625 assert!(
3626 is_generated_binding(&hidden.name.name),
3627 "{}",
3628 hidden.name.name
3629 );
3630 assert_eq!(
3631 hidden.name.span.start, hidden.name.span.end,
3632 "the binder has no extent"
3633 );
3634 let le = hidden
3635 .let_else
3636 .as_ref()
3637 .expect("the binding records the sugar");
3638 assert_eq!(le.form, LetElseForm::Block);
3639 let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
3640 panic!("expected a match tail, got {:?}", b.tail.kind);
3641 };
3642 assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
3643 assert_eq!(arms.len(), 2);
3644 assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
3645 let MatchBody::Block(rest) = &arms[0].body else {
3646 panic!("the pattern's arm runs the rest of the block");
3647 };
3648 assert!(rest.statements.is_empty());
3649 assert!(
3650 !matches!(rest.tail.kind, ExprKind::UnitLit),
3651 "{:?}",
3652 rest.tail.kind
3653 );
3654 assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
3655 }
3656
3657 #[test]
3659 fn par_block_holds_its_statements_in_source_order() {
3660 let b = body_block(
3661 "par {\n let a <- one()\n do two()\n let b: Int <- three()\n }\n a",
3662 );
3663 let Statement::Par(p) = &b.statements[0] else {
3664 panic!("expected a par block, got {:?}", b.statements[0]);
3665 };
3666 assert_eq!(p.statements.len(), 3);
3667 assert!(matches!(&p.statements[0], Statement::EffectLet(l) if l.name.name == "a"));
3668 assert!(matches!(&p.statements[1], Statement::Do(_)));
3669 assert!(
3670 matches!(&p.statements[2], Statement::EffectLet(l) if l.name.name == "b" && l.type_annot.is_some())
3671 );
3672 }
3673
3674 #[test]
3676 fn par_is_an_identifier_outside_its_statement_shape() {
3677 let b = body_block("let par = 1\n let x = par + 1\n x");
3678 assert_eq!(b.statements.len(), 2);
3679 assert!(matches!(&b.statements[0], Statement::Let(l) if l.name.name == "par"));
3680 }
3681
3682 #[test]
3685 fn par_block_admits_statements_for_the_checker_to_refuse() {
3686 let b = body_block(
3687 "par {\n let a = 1\n ~> notify()\n par {\n do x()\n }\n }\n 1",
3688 );
3689 let Statement::Par(p) = &b.statements[0] else {
3690 panic!("expected a par block");
3691 };
3692 assert!(matches!(&p.statements[0], Statement::Let(_)));
3693 assert!(matches!(&p.statements[1], Statement::Send(_)));
3694 assert!(matches!(&p.statements[2], Statement::Par(_)));
3695 }
3696
3697 #[test]
3698 fn par_block_refuses_a_refutable_let_and_a_stray_expression() {
3699 assert!(has_error(
3700 &body_err("par {\n let Ok(v) <- load() else { 0 }\n }\n 1"),
3701 "bynk.par.invalid_statement"
3702 ));
3703 assert!(has_error(
3704 &body_err("par {\n load()\n }\n 1"),
3705 "bynk.par.invalid_statement"
3706 ));
3707 }
3708
3709 #[test]
3710 fn effectful_let_else_binds_with_an_arrow() {
3711 let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n v");
3712 assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
3713 }
3714
3715 #[test]
3716 fn let_else_chain_nests_each_step_in_the_previous_arm() {
3717 let b = body_block(
3720 "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",
3721 );
3722 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3723 panic!("expected a match tail");
3724 };
3725 let MatchBody::Block(second) = &arms[0].body else {
3726 panic!("expected a block arm");
3727 };
3728 assert_eq!(
3729 second.statements.len(),
3730 2,
3731 "`let y` and the second hidden binding"
3732 );
3733 assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
3734 let ExprKind::Match { arms, .. } = &second.tail.kind else {
3735 panic!("expected the second step's match");
3736 };
3737 let MatchBody::Block(third) = &arms[0].body else {
3738 panic!("expected a block arm");
3739 };
3740 let ExprKind::Match { arms, .. } = &third.tail.kind else {
3741 panic!("expected the third step's match");
3742 };
3743 let MatchBody::Block(last) = &arms[0].body else {
3744 panic!("expected a block arm");
3745 };
3746 assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3747 }
3748
3749 #[test]
3750 fn else_match_arms_follow_the_patterns_arm() {
3751 let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n v");
3752 let Statement::Let(l) = &b.statements[0] else {
3753 panic!("expected the hidden binding");
3754 };
3755 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3756 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3757 panic!("expected a match tail");
3758 };
3759 assert_eq!(arms.len(), 2);
3760 assert!(
3761 matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3762 );
3763 }
3764
3765 #[test]
3766 fn an_irrefutable_record_let_needs_no_else() {
3767 for body in [
3770 "let Item { sku, qty: n, .. } = item\n n",
3771 "let (Item { sku, .. }) = item\n sku",
3772 "let Item { sku: _, inner: Inner { x, .. }, .. } <- load(1)\n x",
3773 "let Item { sku, .. } <- load(1) by Admin(\"a\")\n sku",
3774 ] {
3775 let b = body_block(body);
3776 let (Statement::Let(l) | Statement::EffectLet(l)) = &b.statements[0] else {
3777 panic!("{body:?}: expected the hidden binding");
3778 };
3779 let le = l.let_else.as_ref().expect("the binding records the sugar");
3780 assert_eq!(le.form, LetElseForm::Plain, "{body:?}");
3781 assert_eq!(le.else_span.start, le.else_span.end, "no `else` extent");
3782 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3783 panic!("{body:?}: expected a match tail");
3784 };
3785 assert_eq!(arms.len(), 1, "{body:?}: only the pattern's arm");
3786 assert!(matches!(arms[0].pattern, Pattern::Record { .. }));
3787 }
3788 }
3789
3790 #[test]
3791 fn a_refutable_record_let_without_else_names_the_refutable_field() {
3792 for (body, field) in [
3793 (
3794 "let Order { status: Paid, total, .. } = o\n total",
3795 "status: Paid",
3796 ),
3797 (
3798 "let Order { total, c: Cust { kind: Gold, .. }, .. } = o\n total",
3799 "kind: Gold",
3800 ),
3801 ("let Order { id: 1, .. } <- load()\n 1", "id: 1"),
3802 ] {
3803 let errs = body_err(body);
3804 assert_eq!(errs[0].category, "bynk.parse.let_pattern_without_else");
3805 assert!(
3806 errs[0]
3807 .notes
3808 .iter()
3809 .any(|n| n.contains(&format!("`{field}` can fail to match"))),
3810 "{body:?}: {:?}",
3811 errs[0].notes
3812 );
3813 }
3814 for body in [
3816 "let Ok(n) = r\n n",
3817 "let (Ok(n)) = r\n n",
3818 "let Item { sku, .. } | Other = r\n 1",
3819 ] {
3820 let errs = body_err(body);
3821 assert_eq!(
3822 errs[0].category, "bynk.parse.let_pattern_without_else",
3823 "{body:?}"
3824 );
3825 }
3826 }
3827
3828 #[test]
3829 fn a_record_let_keeps_an_else_when_one_is_written() {
3830 let b = body_block("let Item { sku, .. } = item else { 0 }\n 1");
3831 let Statement::Let(l) = &b.statements[0] else {
3832 panic!("expected the hidden binding");
3833 };
3834 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Block);
3835 }
3836
3837 #[test]
3838 fn a_capitalised_binding_is_not_a_record_pattern() {
3839 for body in ["let Total = 1\n Total", "let Total: Int = 1\n Total"] {
3841 let b = body_block(body);
3842 let Statement::Let(l) = &b.statements[0] else {
3843 panic!("{body:?}: expected a plain let");
3844 };
3845 assert!(l.let_else.is_none(), "{body:?}");
3846 }
3847 let errs = body_err("let Item { sku } = item\n 1");
3849 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3850 }
3851
3852 #[test]
3853 fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3854 let b = body_block("let Some(n) = find(1) else { () }");
3855 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3856 panic!("expected a match tail");
3857 };
3858 let MatchBody::Block(rest) = &arms[0].body else {
3859 panic!("expected a block arm");
3860 };
3861 assert!(rest.implicit_tail && rest.statements.is_empty());
3862 }
3863
3864 #[test]
3865 fn let_pattern_forms_are_recognised() {
3866 for src in [
3867 "let None = o else { 0 }\n 1",
3868 "let Status.Done = s else { 0 }\n 1",
3869 "let Shipped(at) = s else { 0 }\n at",
3870 "let 0 = n else { 1 }\n 0",
3871 "let -1 = n else { 1 }\n 0",
3872 "let \"a\" = s else { 1 }\n 0",
3873 "let (Ok(x) | Err(x)) = r else { 1 }\n x",
3874 ] {
3875 let b = body_block(src);
3876 assert!(
3877 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3878 "{src:?} should parse as a let-else"
3879 );
3880 }
3881 let b = body_block("let Total = 1\n Total");
3883 assert!(
3884 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3885 );
3886 }
3887
3888 #[test]
3889 fn let_pattern_without_else_is_refused() {
3890 assert!(has_error(
3891 &body_err("let Ok(v) = r\n v"),
3892 "bynk.parse.let_pattern_without_else"
3893 ));
3894 assert!(has_error(
3895 &body_err("let Ok(v) <- r()\n v"),
3896 "bynk.parse.let_pattern_without_else"
3897 ));
3898 }
3899
3900 #[test]
3901 fn else_on_a_plain_binding_is_refused() {
3902 assert!(has_error(
3903 &body_err("let v = r else { 0 }\n v"),
3904 "bynk.parse.let_else_on_binding"
3905 ));
3906 assert!(has_error(
3907 &body_err("let v <- r() else { 0 }\n v"),
3908 "bynk.parse.let_else_on_binding"
3909 ));
3910 }
3911
3912 #[test]
3913 fn annotated_let_pattern_is_refused() {
3914 assert!(has_error(
3915 &body_err("let Ok(v): Int = r else { 0 }\n v"),
3916 "bynk.parse.let_pattern_annotation"
3917 ));
3918 }
3919
3920 #[test]
3921 fn empty_else_match_is_refused() {
3922 assert!(has_error(
3923 &body_err("let Ok(v) = r else match { }\n v"),
3924 "bynk.parse.empty_match"
3925 ));
3926 }
3927
3928 #[test]
3929 fn let_else_steps_count_against_the_nesting_budget() {
3930 let chain = |n: usize| -> String {
3934 let steps: String = (0..n)
3935 .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n "))
3936 .collect();
3937 format!("{steps}0")
3938 };
3939 body_block(&chain(40));
3940 let errs = body_err(&chain(400));
3941 assert!(
3942 errs.iter()
3943 .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3944 "{errs:?}"
3945 );
3946 }
3947
3948 #[test]
3949 fn empty_match_reports_its_own_diagnostic() {
3950 let errs = body_err("match result {}");
3954 assert!(
3955 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3956 "expected empty_match; got {errs:?}",
3957 );
3958 }
3959
3960 #[test]
3961 fn match_discriminant_ending_in_ident_parses() {
3962 assert!(matches!(
3964 body_tail("match ready { x => x }"),
3965 ExprKind::Match { .. }
3966 ));
3967 }
3968
3969 #[test]
3978 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3979 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
3980 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3981 let CommonsItem::Fn(f) = &c.items[0] else {
3982 panic!("expected fn, got {:?}", c.items[0]);
3983 };
3984 assert_eq!(
3985 f.body.statements.len(),
3986 1,
3987 "expected exactly one Assign statement, got {:?}",
3988 f.body.statements
3989 );
3990 let Statement::Assign(a) = &f.body.statements[0] else {
3991 panic!(
3992 "expected an Assign statement, got {:?}",
3993 f.body.statements[0]
3994 );
3995 };
3996 assert!(
3997 matches!(a.value.kind, ExprKind::Ident(_)),
3998 "assign value must stay the bare identifier `Paid`, got {:?}",
3999 a.value.kind
4000 );
4001 assert!(
4002 matches!(f.body.tail.kind, ExprKind::UnitLit),
4003 "the `()` must remain the block's own tail, got {:?}",
4004 f.body.tail.kind
4005 );
4006 }
4007
4008 #[test]
4013 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
4014 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
4015 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
4016 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4017 let CommonsItem::Fn(f) = &c.items[0] else {
4018 panic!("expected fn, got {:?}", c.items[0]);
4019 };
4020 let Statement::Let(l) = &f.body.statements[0] else {
4021 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
4022 };
4023 let ExprKind::Is { pattern, .. } = &l.value.kind else {
4024 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
4025 };
4026 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
4027 panic!("{name}: expected a variant pattern, got {pattern:?}");
4028 };
4029 assert_eq!(
4030 span.end, variant.span.end,
4031 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
4032 );
4033 assert!(
4034 matches!(f.body.tail.kind, ExprKind::UnitLit),
4035 "{name}: the `()` must remain the block's own tail, got {:?}",
4036 f.body.tail.kind
4037 );
4038 }
4039 }
4040
4041 #[test]
4044 fn variant_pattern_payload_on_the_same_line_still_binds() {
4045 for (name, pat) in [
4046 ("user variant", "Paid(\n x\n )"),
4047 ("built-in variant", "Some(x)"),
4048 ] {
4049 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
4050 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4051 let CommonsItem::Fn(f) = &c.items[0] else {
4052 panic!("expected fn, got {:?}", c.items[0]);
4053 };
4054 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
4055 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
4056 };
4057 assert!(
4058 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
4059 "{name}: expected one binding, got {pattern:?}"
4060 );
4061 }
4062 }
4063
4064 #[test]
4070 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
4071 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
4072 assert!(
4073 parse_str(src).is_err(),
4074 "a next-line `(` after an arm's variant must not open its payload list"
4075 );
4076 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
4077 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
4078 }
4079
4080 #[test]
4084 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
4085 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
4086 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
4087 let CommonsItem::Fn(f) = &c.items[0] else {
4088 panic!("expected fn, got {:?}", c.items[0]);
4089 };
4090 let Statement::Let(l) = &f.body.statements[0] else {
4091 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
4092 };
4093 assert!(
4094 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
4095 "let value must stay a field access, got {:?}",
4096 l.value.kind
4097 );
4098 assert!(
4099 matches!(f.body.tail.kind, ExprKind::UnitLit),
4100 "the `()` must remain the block's own tail, got {:?}",
4101 f.body.tail.kind
4102 );
4103 }
4104
4105 #[test]
4106 fn unparenthesised_record_in_condition_head_now_errors() {
4107 assert!(
4113 !body_err("match Point { x: 1 } { p => p }").is_empty(),
4114 "unparenthesised record discriminant should not parse",
4115 );
4116 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
4118 else {
4119 panic!("expected Match for the parenthesised form");
4120 };
4121 let ExprKind::Paren(inner) = &discriminant.kind else {
4122 panic!(
4123 "expected a parenthesised discriminant, got {:?}",
4124 discriminant.kind
4125 );
4126 };
4127 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
4128 }
4129
4130 fn strict_codes(src: &str) -> Vec<&'static str> {
4132 let toks = tokenize(src).unwrap();
4133 match parse_units_with_warnings(&toks, src) {
4134 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
4135 Err(errs) => errs.iter().map(|e| e.category).collect(),
4136 }
4137 }
4138
4139 #[test]
4143 fn a_divider_pair_around_code_is_doc_block_contains_code() {
4144 let src =
4145 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
4146 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
4147 let errs = parse_str(src).unwrap_err();
4148 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
4149 assert_eq!(
4151 &src[errs[0].span.range()],
4152 "--------\nfn helper() -> Int { 1 }\n--------\n"
4153 );
4154 let (unit, errs) = parse_recover_str(src);
4155 assert!(unit.is_some(), "recovery keeps the unit");
4156 assert!(
4157 errs.iter()
4158 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
4159 "{errs:?}"
4160 );
4161 }
4162
4163 #[test]
4166 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
4167 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
4168 let codes = strict_codes(orphan);
4169 assert!(
4170 codes.contains(&"bynk.parse.doc_block_contains_code"),
4171 "{codes:?}"
4172 );
4173 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
4174 let codes = strict_codes(trailing);
4175 assert!(
4176 codes.contains(&"bynk.parse.doc_block_contains_code"),
4177 "{codes:?}"
4178 );
4179 }
4180
4181 #[test]
4185 fn near_miss_prose_and_fenced_code_are_not_code() {
4186 for doc in [
4187 "type of the thing",
4188 "fn is used here",
4189 "The type Id is an Int.",
4190 "uses the clock",
4191 "Event handler.\nfn helper() -> Int { 1 }",
4192 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
4193 "~~~~\ntype Id = Int\n~~~~",
4194 "---\nA nested doc.\n---",
4195 "",
4196 ] {
4197 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4198 }
4199 for doc in [
4200 "fn helper() -> Int { 1 }",
4201 "type Id = Int\nfn f() -> Int { 1 }",
4202 "```\nan example\n```\nfn helper() -> Int { 1 }",
4203 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
4204 ] {
4205 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
4206 }
4207 }
4208
4209 #[test]
4213 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
4214 let nested =
4215 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
4216 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
4217 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
4218 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
4219 assert!(doc_content_is_code(
4221 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
4222 ));
4223 for doc in [
4224 "---\nAn inner doc.\n---",
4225 "Intro.\n\n---\n\nfn is used here",
4226 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
4227 ] {
4228 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4229 }
4230 let mut deep = "fn helper() -> Int { 1 }".to_string();
4232 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
4233 let fence = "-".repeat(n);
4234 deep = format!("{fence}\n{deep}\n{fence}");
4235 }
4236 assert!(!doc_content_is_code(&deep));
4237 }
4238
4239 #[test]
4242 fn a_fenced_nested_doc_example_parses_clean() {
4243 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";
4244 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4245 let c = parse_str(src).unwrap();
4246 let CommonsItem::Fn(f) = &c.items[0] else {
4247 panic!("expected fn")
4248 };
4249 assert!(
4250 f.documentation
4251 .as_deref()
4252 .is_some_and(|d| d.contains("---\nInner.\n---")),
4253 "{:?}",
4254 f.documentation
4255 );
4256 }
4257
4258 #[test]
4262 fn private_modifier_is_contextual() {
4263 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";
4264 let c = parse_str(src).unwrap();
4265 let t = type_at(&c, 0);
4266 let at = src.find("private type").unwrap();
4267 assert_eq!(t.private.map(|s| s.start), Some(at));
4268 assert_eq!(t.span.start, at);
4269 assert_eq!(doc_of(&t.documentation), Some(("Hidden.", DocForm::Lines)));
4270 assert!(c.items[1].is_private());
4271 assert!(!c.items[2].is_private());
4272 }
4273
4274 #[test]
4277 fn private_modifier_on_a_constant() {
4278 let src = "commons m\n\nprivate const Max: Int = 3\n\nconst Min: Int = 1\n";
4279 let c = parse_str(src).unwrap();
4280 let CommonsItem::Const(k) = &c.items[0] else {
4281 panic!("a constant");
4282 };
4283 assert_eq!(
4284 k.private.map(|s| s.start),
4285 Some(src.find("private").unwrap())
4286 );
4287 assert_eq!(k.span.start, src.find("private").unwrap());
4288 assert!(c.items[0].is_private());
4289 assert!(!c.items[1].is_private());
4290 }
4291
4292 #[test]
4294 fn private_on_a_service_is_rejected() {
4295 let src = "context m\n\nprivate service s {\n on call() -> Effect[Int] { 1 }\n}\n";
4296 let tokens = crate::lexer::tokenize(src).unwrap();
4297 let errs = parse_units(&tokens, src).unwrap_err();
4298 assert_eq!(errs[0].category, "bynk.visibility.private_not_allowed");
4299 }
4300
4301 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
4303 let CommonsItem::Type(t) = &c.items[i] else {
4304 panic!("expected a type at {i}")
4305 };
4306 t
4307 }
4308
4309 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
4310 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
4313 }
4314
4315 #[test]
4318 fn doc_lines_document_a_declaration() {
4319 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
4320 let c = parse_str(src).unwrap();
4321 assert_eq!(
4322 doc_of(&type_at(&c, 0).documentation),
4323 Some(("A commit SHA.", DocForm::Lines))
4324 );
4325 let CommonsItem::Fn(f) = &c.items[1] else {
4326 panic!("expected fn")
4327 };
4328 assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
4329 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4330 }
4331
4332 #[test]
4336 fn legacy_spellings_parse_and_are_each_reported() {
4337 let src =
4338 "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n repo: String, -- trailing\n}\n";
4339 assert_eq!(
4340 strict_codes(src),
4341 [
4342 "bynk.lex.legacy_comment",
4343 "bynk.lex.legacy_doc_line",
4344 "bynk.lex.legacy_comment"
4345 ]
4346 );
4347 let tokens = tokenize(src).unwrap();
4348 let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
4349 let SourceUnit::Commons(c) = &units[0] else {
4350 panic!("commons")
4351 };
4352 assert_eq!(
4353 doc_of(&type_at(c, 0).documentation),
4354 Some(("A run.", DocForm::Lines))
4355 );
4356 }
4357
4358 #[test]
4361 fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
4362 let src = "commons m\n\ntype R = {\n --- First.\n --- Second.\n a: Int,\n}\n";
4363 let tokens = tokenize(src).unwrap();
4364 let errs = parse_units(&tokens, src).unwrap_err();
4365 assert_eq!(errs.len(), 1, "{errs:?}");
4366 assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
4367 let (span, block) = &errs[0].suggestions[0].edits[0];
4368 assert_eq!(&src[span.range()], "--- First.\n --- Second.");
4369 assert_eq!(block, "---\n First.\n Second.\n ---");
4370 }
4371
4372 #[test]
4376 fn members_take_docs_in_either_form() {
4377 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";
4378 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4379 let c = parse_str(src).unwrap();
4380 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4381 panic!("record")
4382 };
4383 assert_eq!(
4384 doc_of(&r.fields[0].documentation),
4385 Some(("The repository.", DocForm::Lines))
4386 );
4387 assert_eq!(
4388 doc_of(&r.fields[1].documentation),
4389 Some(("The version.", DocForm::Block))
4390 );
4391 assert_eq!(r.fields[2].documentation, None);
4392 assert_eq!(
4393 r.fields[2].trivia.leading,
4394 vec![Comment::Line(" a note".into())]
4395 );
4396 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
4397 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
4398 panic!("enum")
4399 };
4400 assert_eq!(
4401 doc_of(&e.variants[0].documentation),
4402 Some(("Passed.", DocForm::Lines))
4403 );
4404 assert_eq!(e.variants[1].documentation, None);
4405 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
4406 panic!("sum")
4407 };
4408 assert_eq!(
4409 doc_of(&p.variants[0].documentation),
4410 Some(("Round.", DocForm::Lines))
4411 );
4412 assert_eq!(
4413 doc_of(&p.variants[0].payload[0].documentation),
4414 Some(("The radius.", DocForm::Lines))
4415 );
4416 assert_eq!(p.variants[1].documentation, None);
4417 let CommonsItem::Fn(f) = &c.items[3] else {
4419 panic!("expected fn")
4420 };
4421 assert_eq!(
4422 doc_of(&f.documentation),
4423 Some(("After the sum.", DocForm::Lines))
4424 );
4425 let CommonsItem::Messages(m) = &c.items[4] else {
4426 panic!("expected messages")
4427 };
4428 assert_eq!(
4429 doc_of(&m.entries[0].documentation),
4430 Some(("Greets.", DocForm::Lines))
4431 );
4432 }
4433
4434 #[test]
4438 fn two_docs_on_one_target_is_multiple_docs() {
4439 for src in [
4440 "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
4441 "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
4442 "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
4443 "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
4444 "commons m\n\ntype R = {\n --- Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
4445 ] {
4446 assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
4447 let (unit, errs) = parse_recover_str(src);
4448 assert!(unit.is_some());
4449 assert!(
4450 errs.iter()
4451 .any(|e| e.category == "bynk.parse.multiple_docs")
4452 );
4453 }
4454 }
4455
4456 #[test]
4459 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
4460 let src =
4461 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --- dangling\n )\n | Dot\n";
4462 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
4463 let c = parse_str(src).unwrap();
4464 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
4465 panic!("sum")
4466 };
4467 assert_eq!(s.variants[0].payload.len(), 1);
4468 assert_eq!(s.variants[0].payload[0].documentation, None);
4469 }
4470
4471 #[test]
4474 fn an_unattached_member_doc_is_an_orphan() {
4475 let src = "commons m\n\ntype R = {\n --- Spaced.\n\n a: Int,\n --- Dangling.\n}\n";
4476 assert_eq!(
4477 strict_codes(src),
4478 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
4479 );
4480 let c = parse_str(src).unwrap();
4481 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4482 panic!("record")
4483 };
4484 assert_eq!(r.fields[0].documentation, None);
4485 assert!(matches!(
4486 &r.fields[0].trivia.leading[..],
4487 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
4488 ));
4489 assert!(matches!(
4490 &r.trailing_comments[..],
4491 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
4492 ));
4493 }
4494}
4495
4496#[cfg(test)]
4497mod paren_guard_2721 {
4498 use super::*;
4499 use crate::lexer::tokenize;
4500
4501 fn ok(src: &str) {
4502 let toks = tokenize(src).unwrap();
4503 parse(&toks, src).unwrap_or_else(|e| panic!("{src}\n{e:?}"));
4504 }
4505
4506 fn wrap(guard: &str) -> String {
4507 format!(
4508 "commons t {{\n fn f(n: Int, flag: Bool) -> Int {{\n match n {{\n n if {guard} => n,\n _ => 0,\n }}\n }}\n}}\n"
4509 )
4510 }
4511
4512 #[test]
4513 fn parenthesised_guards_parse() {
4514 ok(&wrap("(flag)"));
4515 ok(&wrap("(n > 0 && flag)"));
4516 ok(&wrap("(n > 0) && flag"));
4517 ok(&wrap("!(flag)"));
4518 ok(&wrap("()"));
4519 }
4520
4521 #[test]
4522 fn lambda_still_parses_when_parenthesised_in_guard() {
4523 ok(&wrap("pred(((x: Int) => x > 0))"));
4524 }
4525}