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()
163 && filtered.last().is_some_and(|t| {
164 !matches!(
165 t.kind,
166 TokenKind::DocBlock | TokenKind::DocLine | TokenKind::LBrace
167 )
168 })
169 && let Some(prev_end) = last_content_end
170 && !source[prev_end..tok.span.start].contains('\n')
171 {
172 let last_idx = filtered.len() - 1;
173 if table.trailing[last_idx].is_none() {
176 table.trailing[last_idx] = Some(body);
177 continue;
178 }
179 }
180 let own_line = !pending_leading.is_empty()
184 || filtered
185 .last()
186 .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
187 || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
188 if pending_leading.is_empty() {
189 pending_blank_before = own_line
190 && last_content_end
191 .is_some_and(|end| source[end..tok.span.start].matches('\n').count() >= 2);
192 }
193 pending_leading.push(Comment::Line(body));
194 last_leading_end = own_line.then_some(tok.span.end);
195 continue;
196 }
197 filtered.push(*tok);
198 table.leading.push(std::mem::take(&mut pending_leading));
199 table.trailing.push(None);
200 table
201 .blank_before
202 .push(std::mem::take(&mut pending_blank_before));
203 last_content_end = Some(tok.span.end);
204 last_leading_end = None;
205 }
206 table.epilogue = pending_leading;
207 (filtered, table)
208}
209
210pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
216 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
217}
218
219pub fn parse_with_warnings(
222 tokens: &[Token],
223 source: &str,
224) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
225 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
226 match unit {
227 SourceUnit::Commons(c) => Ok((c, warnings)),
228 SourceUnit::Context(ctx) => Err(vec![
229 CompileError::new(
230 "bynk.parse.unexpected_context",
231 ctx.span,
232 "expected a `commons` declaration but found a `context` declaration",
233 )
234 .with_note(
235 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
236 ),
237 ]),
238 SourceUnit::Suite(t) => Err(vec![
239 CompileError::new(
240 "bynk.parse.unexpected_suite",
241 t.span,
242 "expected a `commons` declaration but found a `suite` declaration",
243 )
244 .with_note(
245 "tests must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
246 ),
247 ]),
248 SourceUnit::Adapter(a) => Err(vec![
249 CompileError::new(
250 "bynk.parse.unexpected_adapter",
251 a.span,
252 "expected a `commons` declaration but found an `adapter` declaration",
253 )
254 .with_note(
255 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynk compile --target bundle --output out src`",
256 ),
257 ]),
258 }
259}
260
261pub fn parse_unit_with_recovery(
276 tokens: &[Token],
277 source: &str,
278) -> (Option<SourceUnit>, Vec<CompileError>) {
279 let (units, errors) = parse_units_with_recovery(tokens, source);
280 (units.into_iter().next(), errors)
281}
282
283pub fn parse_units_with_recovery(
288 tokens: &[Token],
289 source: &str,
290) -> (Vec<SourceUnit>, Vec<CompileError>) {
291 let recovered = parse_units_recovering(tokens, source);
292 (recovered.units, recovered.errors)
293}
294
295#[derive(Debug)]
298pub struct Recovered {
299 pub units: Vec<SourceUnit>,
300 pub errors: Vec<CompileError>,
301 pub broken_decl_names: Vec<String>,
304}
305
306pub fn merge_syntax_errors(
313 strict: Vec<CompileError>,
314 recovered: Vec<CompileError>,
315) -> Vec<CompileError> {
316 let mut out = strict;
317 for e in recovered {
318 if !out.iter().any(|o| o.span.start == e.span.start) {
319 out.push(e);
320 }
321 }
322 out
323}
324
325pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
328 parse_units_recovering_from(tokens, source, &mut 0)
329}
330
331pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
337 let (filtered, trivia) = split_trivia(tokens, source);
338 let mut warnings = Vec::new();
339 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
340 p.recover_mode = true;
341 p.next_expr_id = *next_id;
342 let mut units = Vec::new();
343 loop {
344 match p.parse_unit() {
345 Ok(u) => units.push(u),
346 Err(e) => {
347 p.recovered_errors.push(e);
348 break;
349 }
350 }
351 if p.peek().is_none() {
357 break;
358 }
359 }
360 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
361 *next_id = p.next_expr_id;
362 let mut all_errors = p.recovered_errors;
363 all_errors.append(&mut p.doc_errors);
364 all_errors.append(&mut warnings);
365 Recovered {
366 units,
367 errors: all_errors,
368 broken_decl_names,
369 }
370}
371
372pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
376 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
377}
378
379pub fn parse_unit_with_warnings(
382 tokens: &[Token],
383 source: &str,
384) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
385 parse_unit_with_warnings_from(tokens, source, &mut 0)
386}
387
388pub fn parse_unit_with_warnings_from(
402 tokens: &[Token],
403 source: &str,
404 next_id: &mut u32,
405) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
406 let (filtered, trivia) = split_trivia(tokens, source);
407 let mut warnings = Vec::new();
408 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
409 p.next_expr_id = *next_id;
410 let result = match p.parse_unit() {
411 Ok(u) => {
412 if let Some(extra) = p.peek() {
413 Err(vec![
414 CompileError::new(
415 "bynk.parse.extra_tokens",
416 extra.span,
417 "unexpected token after top-level declaration",
418 )
419 .with_note(
420 "a `.bynk` file contains exactly one `commons` or `context` declaration",
421 ),
422 ])
423 } else {
424 Ok(u)
425 }
426 }
427 Err(e) => Err(vec![e]),
428 };
429 let result = match (result, std::mem::take(&mut p.doc_errors)) {
432 (r, docs) if docs.is_empty() => r,
433 (Ok(_), docs) => Err(docs),
434 (Err(mut errs), mut docs) => {
435 errs.append(&mut docs);
436 Err(errs)
437 }
438 };
439 *next_id = p.next_expr_id;
440 match result {
443 Ok(u) => {
444 debug_assert!(
447 p.trivia.epilogue_is_empty(),
448 "a file-trailing comment was left undrained after a successful parse"
449 );
450 Ok((u, warnings))
451 }
452 Err(mut errs) => {
453 errs.append(&mut warnings);
454 Err(errs)
455 }
456 }
457}
458
459pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
468 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
469}
470
471pub fn parse_units_with_warnings(
477 tokens: &[Token],
478 source: &str,
479) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
480 parse_units_with_drain_check(tokens, source)
481 .map(|(units, warnings, _drained)| (units, warnings))
482}
483
484pub fn parse_units_with_warnings_from(
491 tokens: &[Token],
492 source: &str,
493 next_id: &mut u32,
494) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
495 parse_units_with_drain_check_from(tokens, source, next_id)
496 .map(|(units, warnings, _drained)| (units, warnings))
497}
498
499pub fn parse_units_with_drain_check(
510 tokens: &[Token],
511 source: &str,
512) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
513 parse_units_with_drain_check_from(tokens, source, &mut 0)
514}
515
516pub fn parse_units_with_drain_check_from(
519 tokens: &[Token],
520 source: &str,
521 next_id: &mut u32,
522) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
523 parse_units_drain(tokens, source, next_id, false)
524}
525
526pub fn parse_units_migrating(
531 tokens: &[Token],
532 source: &str,
533) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
534 parse_units_drain(tokens, source, &mut 0, true)
535}
536
537fn parse_units_drain(
538 tokens: &[Token],
539 source: &str,
540 next_id: &mut u32,
541 migrate: bool,
542) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
543 let (filtered, trivia) = split_trivia(tokens, source);
544 let mut warnings = Vec::new();
545 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
546 if migrate {
547 p.doc_errors.clear();
548 }
549 p.next_expr_id = *next_id;
550 let mut units = Vec::new();
551 let mut errors: Vec<CompileError> = Vec::new();
552 while p.peek().is_some() {
553 match p.parse_unit() {
554 Ok(u) => units.push(u),
555 Err(e) => {
556 errors.push(e);
557 break;
558 }
559 }
560 }
561 errors.append(&mut p.doc_errors);
563 *next_id = p.next_expr_id;
564 let eof = p.eof_span();
565 let fully_drained = p.trivia.is_fully_drained();
566 if !errors.is_empty() {
569 errors.append(&mut warnings);
570 return Err(errors);
571 }
572 if units.is_empty() {
573 return Err(vec![CompileError::new(
574 "bynk.parse.unexpected_eof",
575 eof,
576 "expected `commons`, `context`, or `suite` to start the file, found end of file",
577 )]);
578 }
579 debug_assert!(
587 p.trivia.epilogue_is_empty(),
588 "a file-trailing comment was left undrained after a successful parse"
589 );
590 Ok((units, warnings, fully_drained))
591}
592
593enum SignedNumLit {
596 Int(IntBound),
597 Float(FloatBound),
598 Decimal(DecimalBound),
599}
600
601struct Parser<'a> {
602 tokens: &'a [Token],
603 source: &'a str,
604 pos: usize,
605 warnings: &'a mut Vec<CompileError>,
608 recover_mode: bool,
614 recovered_errors: Vec<CompileError>,
617 pub(crate) item_start: Option<usize>,
621 broken_decl_names: Vec<String>,
626 trivia: TriviaTable,
629 depth: usize,
636 no_record_literal: bool,
645 in_guard: bool,
649 is_pattern_tail: bool,
657 brace_depth: usize,
668 item_loop_baseline: Vec<usize>,
675 next_expr_id: u32,
679 doc_errors: Vec<CompileError>,
685 doc_probe: bool,
689}
690
691impl<'a> Parser<'a> {
692 fn new(
693 tokens: &'a [Token],
694 source: &'a str,
695 mut trivia: TriviaTable,
696 warnings: &'a mut Vec<CompileError>,
697 ) -> Self {
698 let doc_errors = std::mem::take(&mut trivia.legacy);
699 Self {
700 tokens,
701 source,
702 pos: 0,
703 warnings,
704 recover_mode: false,
705 recovered_errors: Vec::new(),
706 item_start: None,
707 broken_decl_names: Vec::new(),
708 trivia,
709 depth: 0,
710 no_record_literal: false,
711 in_guard: false,
712 is_pattern_tail: false,
713 brace_depth: 0,
714 item_loop_baseline: Vec::new(),
715 next_expr_id: 0,
716 doc_errors,
717 doc_probe: false,
718 }
719 }
720
721 fn alloc_expr_id(&mut self) -> ExprId {
726 let id = ExprId(self.next_expr_id);
727 self.next_expr_id += 1;
728 id
729 }
730
731 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
739 self.depth += 1;
740 if self.depth > crate::MAX_NESTING_DEPTH {
741 self.depth -= 1;
742 let span = self
743 .peek()
744 .map(|t| t.span)
745 .unwrap_or_else(|| self.eof_span());
746 return Err(self.nesting_too_deep(span, what));
747 }
748 Ok(())
749 }
750
751 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
754 CompileError::new(
755 "bynk.parse.nesting_too_deep",
756 span,
757 format!(
758 "{what} nests more than {} levels deep",
759 crate::MAX_NESTING_DEPTH
760 ),
761 )
762 .with_note(
763 "deeply nested source is rejected to keep the parser from overflowing its \
764 stack and aborting; flatten or split the construct",
765 )
766 }
767
768 fn expression_too_long(&self, span: Span) -> CompileError {
774 CompileError::new(
775 "bynk.parse.nesting_too_deep",
776 span,
777 format!(
778 "this expression is more than {} levels deep",
779 crate::MAX_NESTING_DEPTH
780 ),
781 )
782 .with_note(
783 "a long operator or member chain is rejected to keep the compiler from overflowing \
784 its stack; split it across `let` bindings, or reduce a sequence with \
785 `.sum()`/`.fold(...)`",
786 )
787 }
788
789 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
809 self.depth += 1;
810 *folds += 1;
811 if self.depth > crate::MAX_NESTING_DEPTH {
812 self.depth -= *folds;
813 *folds = 0;
814 return Err(self.expression_too_long(span));
815 }
816 Ok(())
817 }
818
819 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
828 self.depth += 1;
829 if self.depth > crate::MAX_NESTING_DEPTH {
830 return Err(self.expression_too_long(span));
831 }
832 Ok(())
833 }
834
835 fn take_leading_trivia(&mut self) -> Vec<Comment> {
845 let mut comments = self.take_unit_leading_trivia();
846 comments.retain(|c| *c != Comment::Blank);
847 comments
848 }
849
850 fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
853 self.trivia.take_leading(self.pos)
854 }
855
856 fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
859 self.trivia.take_epilogue()
860 }
861
862 fn take_trailing_trivia(&mut self) -> Option<String> {
866 if self.pos == 0 {
867 return None;
868 }
869 self.trivia.take_trailing(self.pos - 1)
870 }
871
872 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
876 if self.recover_mode {
877 self.recovered_errors.push(e);
878 if let Some(start) = self.item_start.take() {
879 self.broken_decl_names.extend(self.decl_names_at(start));
880 }
881 let before = self.pos;
882 self.recover_to_top_item();
883 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
896 let closes_body = baseline > 0
897 && self.brace_depth == baseline
898 && self.peek_kind() == Some(TokenKind::RBrace);
899 if self.pos == before && !closes_body {
900 self.bump();
901 self.recover_to_top_item();
906 }
907 Ok(())
908 } else {
909 Err(e)
910 }
911 }
912
913 fn decl_names_at(&self, start: usize) -> Vec<String> {
918 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
919 let ident_at = |i: usize| {
920 self.tokens
921 .get(i)
922 .filter(|t| t.kind == TokenKind::Ident)
923 .map(|t| self.slice(t.span).to_string())
924 };
925 match kind_at(start) {
926 Some(
927 TokenKind::Type
928 | TokenKind::Event
929 | TokenKind::Capability
930 | TokenKind::Service
931 | TokenKind::Agent
932 | TokenKind::Actor,
933 ) => ident_at(start + 1).into_iter().collect(),
934 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
935 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
938 Some(method) => vec![format!("{owner}.{method}")],
939 None => Vec::new(),
940 },
941 (Some(name), _) => vec![name],
942 (None, _) => Vec::new(),
943 },
944 _ => Vec::new(),
945 }
946 }
947
948 fn recover_to_top_item(&mut self) {
961 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
962 while let Some(t) = self.peek() {
963 match t.kind {
964 TokenKind::RBrace if self.brace_depth == baseline => return,
965 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
966 _ => {
967 self.bump();
968 }
969 }
970 }
971 }
972
973 fn enter_item_loop(&mut self) {
980 self.item_loop_baseline.push(self.brace_depth);
981 }
982
983 fn exit_item_loop(&mut self) {
985 self.item_loop_baseline.pop();
986 }
987
988 fn peek(&self) -> Option<Token> {
989 self.tokens.get(self.pos).copied()
990 }
991
992 fn peek_kind(&self) -> Option<TokenKind> {
993 self.peek().map(|t| t.kind)
994 }
995
996 fn take_private_modifier(&mut self, allowed: bool) -> Result<Option<Span>, CompileError> {
1008 let Some(t) = self.peek() else {
1009 return Ok(None);
1010 };
1011 if t.kind != TokenKind::Ident || self.slice(t.span) != "private" {
1012 return Ok(None);
1013 }
1014 let Some(next) = self.nth(1) else {
1015 return Ok(None);
1016 };
1017 let modifiable = matches!(
1018 next.kind,
1019 TokenKind::Fn
1020 | TokenKind::Type
1021 | TokenKind::Messages
1022 | TokenKind::Asset
1023 | TokenKind::Embed
1024 | TokenKind::Const
1025 );
1026 let declaration = modifiable
1027 || matches!(
1028 next.kind,
1029 TokenKind::Service
1030 | TokenKind::Agent
1031 | TokenKind::Capability
1032 | TokenKind::Provides
1033 | TokenKind::Actor
1034 | TokenKind::Event
1035 );
1036 if !declaration {
1037 return Ok(None);
1038 }
1039 self.bump();
1040 if modifiable && allowed {
1041 return Ok(Some(t.span));
1042 }
1043 let what = self.slice(next.span);
1044 let err = if allowed {
1045 CompileError::new(
1046 "bynk.visibility.private_not_allowed",
1047 t.span,
1048 format!("`private` does not apply to `{what}` declarations"),
1049 )
1050 .with_note(
1051 "`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",
1052 )
1053 } else {
1054 CompileError::new(
1055 "bynk.visibility.private_not_allowed",
1056 t.span,
1057 "`private` applies only to declarations in a commons or context, not in an adapter",
1058 )
1059 .with_note("an adapter's `exports` governs what its consumers see")
1060 };
1061 if self.recover_mode {
1062 self.recovered_errors.push(err);
1063 Ok(None)
1064 } else {
1065 Err(err)
1066 }
1067 }
1068
1069 fn nth(&self, n: usize) -> Option<Token> {
1071 self.tokens.get(self.pos + n).copied()
1072 }
1073
1074 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
1075 self.nth(n).map(|t| t.kind)
1076 }
1077
1078 fn nth_text(&self, n: usize) -> &'a str {
1080 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
1081 }
1082
1083 fn prev_span(&self) -> Span {
1086 self.tokens
1087 .get(self.pos.wrapping_sub(1))
1088 .or_else(|| self.peek_ref())
1089 .map(|t| t.span)
1090 .unwrap_or_default()
1091 }
1092
1093 fn peek_ref(&self) -> Option<&Token> {
1094 self.tokens.get(self.pos)
1095 }
1096
1097 fn bump(&mut self) -> Option<Token> {
1098 let t = self.peek();
1099 if let Some(t) = t {
1100 match t.kind {
1101 TokenKind::LBrace => self.brace_depth += 1,
1102 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
1103 _ => {}
1104 }
1105 self.pos += 1;
1106 }
1107 t
1108 }
1109
1110 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
1111 if self.peek_kind() == Some(kind) {
1112 self.bump()
1113 } else {
1114 None
1115 }
1116 }
1117
1118 fn slice(&self, span: Span) -> &'a str {
1119 &self.source[span.range()]
1120 }
1121
1122 fn next_token_on_new_line(&self, prev: Span) -> bool {
1127 match self.peek() {
1128 Some(t) if prev.end <= t.span.start => {
1129 self.source[prev.end..t.span.start].contains('\n')
1130 }
1131 _ => false,
1132 }
1133 }
1134
1135 fn eof_span(&self) -> Span {
1140 let end = self.source.len();
1141 let start = (0..end)
1142 .rev()
1143 .find(|&i| self.source.is_char_boundary(i))
1144 .unwrap_or(0);
1145 Span::new(start, end)
1146 }
1147
1148 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1149 match self.peek() {
1150 Some(t) if t.kind == kind => {
1151 self.bump();
1152 Ok(t)
1153 }
1154 Some(t) => Err(CompileError::new(
1155 "bynk.parse.expected_token",
1156 t.span,
1157 format!(
1158 "expected {} {ctx}, found {}",
1159 kind.describe(),
1160 t.kind.describe()
1161 ),
1162 )),
1163 None => Err(CompileError::new(
1164 "bynk.parse.unexpected_eof",
1165 self.eof_span(),
1166 format!("expected {} {ctx}, found end of file", kind.describe()),
1167 )),
1168 }
1169 }
1170
1171 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1172 match self.peek() {
1173 Some(t) if t.kind == TokenKind::Ident => {
1174 self.bump();
1175 Ok(Ident {
1176 name: self.slice(t.span).to_string(),
1177 span: t.span,
1178 })
1179 }
1180 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1195 self.bump();
1196 Ok(Ident {
1197 name: self.slice(t.span).to_string(),
1198 span: t.span,
1199 })
1200 }
1201 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1202 "bynk.parse.reserved_keyword",
1203 t.span,
1204 format!(
1205 "expected identifier {ctx}, but `{}` is a reserved keyword",
1206 self.slice(t.span)
1207 ),
1208 )
1209 .with_note("rename the identifier to something that is not a keyword")),
1210 Some(t) => Err(CompileError::new(
1211 "bynk.parse.expected_token",
1212 t.span,
1213 format!("expected identifier {ctx}, found {}", t.kind.describe()),
1214 )),
1215 None => Err(CompileError::new(
1216 "bynk.parse.unexpected_eof",
1217 self.eof_span(),
1218 format!("expected identifier {ctx}, found end of file"),
1219 )),
1220 }
1221 }
1222
1223 fn at_keyword_label(&self) -> bool {
1227 matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1228 && self.tokens.get(self.pos + 1).map(|t| t.kind) == Some(TokenKind::Colon)
1229 }
1230
1231 pub(crate) fn expect_label(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1237 if self.at_keyword_label() {
1238 let t = self.bump().expect("peeked a keyword");
1239 return Ok(Ident {
1240 name: self.slice(t.span).to_string(),
1241 span: t.span,
1242 });
1243 }
1244 self.expect_ident(ctx)
1245 }
1246
1247 pub(crate) fn keyword_shorthand_error(&self, pattern: bool) -> Option<CompileError> {
1259 let t = self.peek()?;
1260 if !is_reserved_keyword(t.kind) {
1261 return None;
1262 }
1263 let next = self.tokens.get(self.pos + 1).map(|t| t.kind);
1264 let shorthand = match next {
1265 Some(TokenKind::Comma | TokenKind::RBrace) => true,
1266 Some(TokenKind::DotDot) => pattern,
1267 _ => false,
1268 };
1269 if !shorthand {
1270 return None;
1271 }
1272 let word = self.slice(t.span);
1273 let mut err = CompileError::new(
1274 "bynk.parse.keyword_field_shorthand",
1275 t.span,
1276 format!("`{word}` is a reserved keyword, so it cannot be a field shorthand"),
1277 );
1278 if pattern {
1279 let taken = |name: &str| {
1280 self.tokens
1281 .iter()
1282 .any(|k| k.kind == TokenKind::Ident && self.slice(k.span) == name)
1283 };
1284 let mut binding = format!("{word}_");
1285 let mut n = 2;
1286 while taken(&binding) {
1287 binding = format!("{word}_{n}");
1288 n += 1;
1289 }
1290 err = err
1291 .with_note(format!(
1292 "the shorthand binds a variable named `{word}`; name the binding instead"
1293 ))
1294 .with_suggestion(
1295 format!("bind the `{word}` field as `{binding}`"),
1296 vec![(t.span, format!("{word}: {binding}"))],
1297 crate::error::Applicability::MaybeIncorrect,
1298 );
1299 } else {
1300 err = err.with_note(format!(
1301 "write `{word}: <value>`; a keyword names the field but cannot name a variable"
1302 ));
1303 }
1304 Some(err)
1305 }
1306
1307 pub(crate) fn expect_member_name(
1312 &mut self,
1313 ctx: &str,
1314 dot: Span,
1315 ) -> Result<Ident, CompileError> {
1316 if matches!(self.peek(), Some(t) if is_reserved_keyword(t.kind))
1317 && !self.next_token_on_new_line(dot)
1318 {
1319 let t = self.bump().expect("peeked a keyword");
1320 return Ok(Ident {
1321 name: self.slice(t.span).to_string(),
1322 span: t.span,
1323 });
1324 }
1325 self.expect_ident(ctx)
1326 }
1327
1328 fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1346 let first = self.take_one_doc()?;
1347 let mut docs = vec![first.clone()];
1348 while let Some(next) = self.peek()
1349 && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1350 && !has_blank_line_between(self.source, docs[docs.len() - 1].1.end, next.span.start)
1351 {
1352 docs.push(self.take_one_doc().expect("a doc token was peeked"));
1353 }
1354 if docs.len() > 1 && !self.doc_probe {
1355 self.doc_errors.push(multiple_docs(self.source, &docs));
1356 }
1357 Some(first)
1358 }
1359
1360 fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1362 match self.peek_kind()? {
1363 TokenKind::DocBlock => {
1364 let t = self.bump().unwrap();
1365 let body = doc_block_content(self.source, t.span);
1366 if !self.doc_probe && doc_content_is_code(&body) {
1367 self.doc_errors.push(doc_block_contains_code(t.span));
1368 }
1369 Some((Doc::new(body, DocForm::Block), t.span))
1370 }
1371 TokenKind::DocLine => {
1372 let t = self.bump().unwrap();
1373 let text = doc_line_text(self.source, t.span);
1374 Some((Doc::new(text, DocForm::Lines), t.span))
1375 }
1376 _ => None,
1377 }
1378 }
1379
1380 fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1393 self.collect_lead(Self::take_unit_leading_trivia)
1394 }
1395
1396 fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1399 self.collect_lead(Self::take_leading_trivia)
1400 }
1401
1402 fn collect_lead(
1403 &mut self,
1404 take: fn(&mut Self) -> Vec<Comment>,
1405 ) -> (Vec<Comment>, Option<DocLead>) {
1406 let mut leading = take(self);
1407 let doc = self.take_doc_block().map(|(content, span)| DocLead {
1408 content,
1409 span,
1410 at: leading.len(),
1411 });
1412 if doc.is_some() {
1413 leading.extend(take(self));
1414 }
1415 (leading, doc)
1416 }
1417
1418 fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1427 let (mut leading, doc) = self.collect_body_item_lead();
1428 let Some(doc) = doc else {
1429 return (leading, None);
1430 };
1431 match self.peek() {
1432 Some(t) if t.kind != close => {
1433 let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1434 (leading, doc)
1435 }
1436 _ => {
1437 self.warnings.push(CompileError::new(
1438 "bynk.parse.orphan_doc_block",
1439 doc.span,
1440 "documentation has no following member to attach to",
1441 ));
1442 keep_orphan(&mut leading, doc);
1443 (leading, None)
1444 }
1445 }
1446 }
1447
1448 fn peek_past_docs(&self) -> Option<TokenKind> {
1452 self.tokens[self.pos.min(self.tokens.len())..]
1453 .iter()
1454 .map(|t| t.kind)
1455 .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1456 }
1457
1458 fn finalize_doc(
1462 &mut self,
1463 doc: Option<DocLead>,
1464 next_span: Span,
1465 leading: &mut Vec<Comment>,
1466 ) -> Option<Doc> {
1467 let doc = doc?;
1468 let doc_span = doc.span;
1469 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1471 self.warnings.push(
1472 CompileError::new(
1473 "bynk.parse.orphan_doc_block",
1474 doc_span,
1475 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1476 )
1477 .with_note(
1478 "remove the blank line to attach the doc to the next declaration, \
1479 or remove the doc block if it is not meant to document anything",
1480 ),
1481 );
1482 keep_orphan(leading, doc);
1483 return None;
1484 }
1485 Some(doc.content)
1486 }
1487}
1488
1489pub(crate) struct DocLead {
1493 pub(crate) content: Doc,
1494 pub(crate) span: Span,
1495 pub(crate) at: usize,
1496}
1497
1498fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1502 let at = doc.at.min(leading.len());
1503 leading.insert(at, Comment::OrphanDoc(doc.content));
1504}
1505
1506fn multiple_docs(source: &str, docs: &[(Doc, Span)]) -> CompileError {
1511 let (_, first) = docs[0];
1512 let (_, last) = docs[docs.len() - 1];
1513 let mut err = CompileError::new(
1514 "bynk.parse.multiple_docs",
1515 docs[1].1,
1516 "this declaration or member already has a doc; a target has one doc",
1517 )
1518 .with_label(first, "the first doc is here")
1519 .with_note(
1520 "a doc of more than one line is a block: a `---` line, the text, and another `---` line",
1521 );
1522 let one_liners = docs
1523 .iter()
1524 .all(|(d, span)| d.form == DocForm::Lines && !doc_line_is_legacy(source, *span));
1525 if one_liners {
1526 let line_start = source[..first.start].rfind('\n').map_or(0, |i| i + 1);
1527 let indent = &source[line_start..first.start];
1528 let text = docs
1529 .iter()
1530 .map(|(d, _)| d.as_str())
1531 .collect::<Vec<_>>()
1532 .join("\n");
1533 let fence = "-".repeat(doc_fence_len(&text));
1534 let mut block = fence.clone();
1535 for line in text.lines() {
1536 block.push('\n');
1537 if !line.is_empty() {
1538 block.push_str(indent);
1539 block.push_str(line);
1540 }
1541 }
1542 block.push('\n');
1543 block.push_str(indent);
1544 block.push_str(&fence);
1545 let end = source[..last.end].trim_end_matches(['\n', '\r']).len();
1546 err = err.with_suggestion(
1547 "join the docs into one `---` block",
1548 vec![(Span::new_in(first.file, first.start, end), block)],
1549 crate::error::Applicability::MachineApplicable,
1550 );
1551 }
1552 err
1553}
1554
1555fn doc_block_contains_code(span: Span) -> CompileError {
1558 CompileError::new(
1559 "bynk.parse.doc_block_contains_code",
1560 span,
1561 "this documentation block contains declarations, so they are not compiled",
1562 )
1563 .with_note(
1564 "a line of three or more dashes opens or closes a doc block, so two dash \
1565 dividers turn the code between them into documentation; for a divider use a \
1566 line comment with text, such as `-- Helpers --`, and to show code in a doc \
1567 block put it in a Markdown code fence (```)",
1568 )
1569}
1570
1571const DECLARATION_KEYWORDS: &[TokenKind] = &[
1575 TokenKind::Type,
1576 TokenKind::Fn,
1577 TokenKind::Capability,
1578 TokenKind::Provides,
1579 TokenKind::Service,
1580 TokenKind::Agent,
1581 TokenKind::Actor,
1582 TokenKind::Messages,
1583 TokenKind::Event,
1584 TokenKind::Asset,
1585 TokenKind::Embed,
1586 TokenKind::Const,
1587 TokenKind::Uses,
1588 TokenKind::Consumes,
1589 TokenKind::Exports,
1590 TokenKind::Binding,
1591];
1592
1593fn doc_content_is_code(content: &str) -> bool {
1608 doc_content_is_code_at(content, 0)
1609}
1610
1611const DOC_PROBE_MAX_DEPTH: usize = 4;
1615
1616fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1617 let stripped = strip_code_fences(content);
1618 let tokens = match crate::lexer::tokenize(&stripped) {
1619 Ok(tokens) => tokens,
1620 Err(_) => {
1621 let unmarked = blank_marker_lines(&stripped);
1624 if unmarked == stripped {
1625 return false;
1626 }
1627 return doc_content_is_code_at(&unmarked, depth);
1628 }
1629 };
1630 if depth < DOC_PROBE_MAX_DEPTH
1631 && tokens.iter().any(|t| {
1632 t.kind == TokenKind::DocBlock
1633 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1634 })
1635 {
1636 return true;
1637 }
1638 if !tokens
1639 .iter()
1640 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1641 {
1642 return false;
1643 }
1644 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1645 let mut warnings = Vec::new();
1646 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1647 p.doc_probe = true;
1648 let at = Span::new(0, 0);
1649 let name = QualifiedName {
1650 parts: Vec::new(),
1651 span: at,
1652 };
1653 match p.parse_adapter_body(at, name, None, false) {
1654 Ok(a) => {
1655 p.peek().is_none()
1656 && (!a.items.is_empty()
1657 || !a.uses.is_empty()
1658 || !a.consumes.is_empty()
1659 || !a.exports.is_empty()
1660 || a.binding.is_some())
1661 }
1662 Err(_) => false,
1663 }
1664}
1665
1666fn blank_marker_lines(content: &str) -> String {
1669 let mut out = String::with_capacity(content.len());
1670 for line in content.lines() {
1671 let trimmed = line.trim();
1672 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1673 out.push_str(line);
1674 }
1675 out.push('\n');
1676 }
1677 out
1678}
1679
1680fn strip_code_fences(content: &str) -> String {
1685 let mut out = String::with_capacity(content.len());
1686 let mut open: Option<(char, usize)> = None;
1687 for line in content.lines() {
1688 let trimmed = line.trim();
1689 let fence = ['`', '~'].into_iter().find_map(|c| {
1690 let n = trimmed.chars().take_while(|&x| x == c).count();
1691 (n >= 3).then_some((c, n))
1692 });
1693 match (open, fence) {
1694 (None, Some(f)) => open = Some(f),
1695 (Some((c, n)), Some((fc, fnum)))
1696 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1697 {
1698 open = None;
1699 }
1700 (None, None) => out.push_str(line),
1701 (Some(_), _) => {}
1702 }
1703 out.push('\n');
1704 }
1705 out
1706}
1707
1708fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1711 crate::lexer::decode_string_literal(lexeme, span)
1712}
1713
1714pub fn is_reserved_keyword(kind: TokenKind) -> bool {
1715 use TokenKind::*;
1716 matches!(
1717 kind,
1718 Commons
1719 | Type
1720 | Fn
1721 | Where
1722 | True
1723 | False
1724 | Int
1725 | String
1726 | Bool
1727 | Let
1728 | If
1729 | Else
1730 | Ok
1731 | Err
1732 | Result
1733 | ValidationError
1734 | Enum
1735 | Match
1736 | Option
1737 | Record
1738 | Self_
1739 | Some
1740 | None
1741 | Is
1742 | Opaque
1743 | Uses
1744 | Context
1745 | Consumes
1746 | Exports
1747 | Transparent
1748 | Agent
1749 | As
1750 | Capability
1751 | Effect
1752 | Do
1753 | Given
1754 | On
1755 | Http
1756 | Provides
1757 | Stub
1758 | Service
1759 | Actor
1760 | By
1761 | Expect
1762 | Suite
1763 | Case
1764 | Float
1765 | Duration
1766 | Instant
1767 | Bytes
1768 | Decimal
1769 | JsonError
1770 | Property
1771 | Adapter
1772 | Binding
1773 | Cron
1774 | Queue
1775 | From
1776 | Protocol
1777 | Invariant
1778 | Implies
1779 | Requires
1780 | Ensures
1781 | Transition
1782 )
1783}
1784
1785fn is_item_start(kind: TokenKind) -> bool {
1796 use TokenKind::*;
1797 matches!(
1798 kind,
1799 Commons | Context | Adapter | Suite
1801 | Type | Fn | Messages | Event | Asset | Embed | Const | Uses
1803 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1805 | Binding
1807 | Stub | Case | Property
1809 )
1810}
1811
1812#[cfg(test)]
1813mod tests {
1814 use super::*;
1815 use crate::lexer::tokenize;
1816
1817 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1818 let toks = tokenize(src).map_err(|e| vec![e])?;
1819 parse(&toks, src)
1820 }
1821
1822 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1823 let toks = match tokenize(src) {
1824 Ok(t) => t,
1825 Err(e) => return (None, vec![e]),
1826 };
1827 parse_unit_with_recovery(&toks, src)
1828 }
1829
1830 #[test]
1834 fn contract_clause_name_before_next_line_brace_is_not_a_record() {
1835 for src in [
1836 "commons m\n\nconst Limit: Int = 10\n\nfn small(n: Int) -> Int\n requires small: n <= Limit\n{\n n\n}\n",
1837 "commons m\n\nfn f(n: Int) -> Int\n ensures same: result == n\n{\n n\n}\n",
1838 "commons m\n\nfn f(n: Int) -> Int\n requires r: n <= Limit {\n n\n}\n",
1839 ] {
1840 assert!(parse_str(src).is_ok(), "{src}\n{:?}", parse_str(src).err());
1841 }
1842 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";
1844 assert!(parse_str(src).is_ok(), "{:?}", parse_str(src).err());
1845 }
1846
1847 #[test]
1850 fn contract_clause_unparenthesised_record_literal_is_rejected() {
1851 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";
1852 assert!(parse_str(src).is_err());
1853 }
1854
1855 #[test]
1861 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1862 let src =
1863 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1864 let toks = tokenize(src).unwrap();
1865 let (units, errors) = parse_units_with_recovery(&toks, src);
1866 assert!(errors.is_empty(), "{errors:?}");
1867 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1868 assert!(matches!(units[0], SourceUnit::Commons(_)));
1869 assert!(matches!(units[1], SourceUnit::Suite(_)));
1870
1871 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1873 assert!(errors.is_empty(), "{errors:?}");
1874 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1875 }
1876
1877 #[test]
1884 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1885 let toks = tokenize("").unwrap();
1886 let (units, errors) = parse_units_with_recovery(&toks, "");
1887 assert!(units.is_empty());
1888 assert!(
1889 !errors.is_empty(),
1890 "an empty file must still produce a diagnostic, not silently no units and no error"
1891 );
1892
1893 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1894 assert!(unit.is_none());
1895 assert_eq!(
1896 errors.len(),
1897 unit_errors.len(),
1898 "the singular wrapper must see the same error(s) as the plural entry point"
1899 );
1900 }
1901
1902 #[test]
1909 fn drain_check_reports_expression_interior_comments_as_undrained() {
1910 let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1911 let toks = tokenize(ordinary).unwrap();
1912 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1913 assert!(
1914 drained,
1915 "a declaration-leading comment must be fully drained"
1916 );
1917
1918 let lossy = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
1919 let toks = tokenize(lossy).unwrap();
1920 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1921 assert!(
1922 !drained,
1923 "a comment inside a binop expression must be reported as undrained"
1924 );
1925 }
1926
1927 #[test]
1928 fn eof_span_never_splits_a_multibyte_codepoint() {
1929 for src in [
1934 "commons x {\n // ends with an arrow →",
1935 "agent A {\n key k: String\n // note 🦀",
1936 "commons y {\n type T = é",
1937 ] {
1938 let (_unit, errors) = parse_recover_str(src);
1939 for e in &errors {
1940 assert!(
1941 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1942 "span {:?} splits a codepoint in {src:?}",
1943 e.span,
1944 );
1945 }
1946 }
1947 }
1948
1949 #[test]
1950 fn reserved_contextual_keywords_readable_in_expression_position() {
1951 for kw in [
1961 "asset", "case", "const", "embed", "event", "messages", "on", "suite",
1962 ] {
1963 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1964 let result = parse_str(&src);
1965 assert!(
1966 result.is_ok(),
1967 "a parameter named `{kw}` must be readable in expression position: {:?}",
1968 result.err()
1969 );
1970 }
1971 }
1972
1973 #[test]
1974 fn recovery_skips_garbage_between_decls() {
1975 let src = "commons x {\n\
1978 type A = Int where NonNegative\n\
1979 ??? !!!\n\
1980 type B = String where NonEmpty\n\
1981 }";
1982 let (unit, errors) = parse_recover_str(src);
1983 let unit = unit.expect("recovery should produce a partial AST");
1984 let SourceUnit::Commons(c) = unit else {
1985 panic!("expected commons")
1986 };
1987 let names: Vec<_> = c
1989 .items
1990 .iter()
1991 .map(|i| match i {
1992 CommonsItem::Type(t) => t.name.name.clone(),
1993 _ => panic!("expected only types"),
1994 })
1995 .collect();
1996 assert!(
1997 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1998 "expected both A and B; got {names:?}",
1999 );
2000 assert!(!errors.is_empty(), "expected at least one parse error");
2001 }
2002
2003 #[test]
2004 fn recovery_handles_bad_first_decl_then_good_second() {
2005 let src = "commons x {\n\
2007 type A Int where NonNegative\n\
2008 type B = String where NonEmpty\n\
2009 }";
2010 let (unit, errors) = parse_recover_str(src);
2011 let unit = unit.expect("recovery should produce a partial AST");
2012 let SourceUnit::Commons(c) = unit else {
2013 panic!("expected commons")
2014 };
2015 let names: Vec<_> = c
2016 .items
2017 .iter()
2018 .filter_map(|i| match i {
2019 CommonsItem::Type(t) => Some(t.name.name.clone()),
2020 _ => None,
2021 })
2022 .collect();
2023 assert!(
2024 names.contains(&"B".to_string()),
2025 "B should be parsed after A's failure; got {names:?}"
2026 );
2027 assert!(!errors.is_empty(), "expected at least one parse error");
2028 }
2029
2030 #[test]
2045 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
2046 let src = "commons m {\n fn f() -> Int\n}\n";
2047 let (unit, errors) = parse_recover_str(src);
2048 assert!(unit.is_some(), "recovery should produce a partial AST");
2049 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
2050 assert_eq!(
2051 categories.len(),
2052 1,
2053 "one syntax error, no follow-on: {categories:?}"
2054 );
2055 assert!(
2056 !categories.contains(&"bynk.parse.unexpected_eof"),
2057 "the commons' closing brace must survive: {categories:?}"
2058 );
2059 }
2060
2061 #[test]
2066 fn recovery_skips_a_rejected_item_whole() {
2067 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
2068 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
2069 assert_eq!(
2070 recovered.errors.len(),
2071 1,
2072 "one syntax error, no follow-on: {:?}",
2073 recovered
2074 .errors
2075 .iter()
2076 .map(|e| e.category)
2077 .collect::<Vec<_>>()
2078 );
2079 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
2080 panic!("expected commons")
2081 };
2082 assert!(
2083 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
2084 "the `fn g` after the skipped agent must still parse"
2085 );
2086 assert_eq!(recovered.broken_decl_names, ["Counter"]);
2087 }
2088
2089 #[test]
2090 fn recovery_skips_a_nested_blocks_own_closing_brace() {
2091 let src = "commons m {\n \
2092 fn f() -> Int {\n \
2093 match 1 {\n \
2094 is 1 -> { let z = }\n \
2095 is _ -> 2\n \
2096 }\n \
2097 }\n \
2098 fn g() -> Int { 2 }\n\
2099 }\n";
2100 let (unit, errors) = parse_recover_str(src);
2101 let unit = unit.expect("recovery should produce a partial AST");
2102 let SourceUnit::Commons(c) = unit else {
2103 panic!("expected commons")
2104 };
2105 let names: Vec<_> = c
2106 .items
2107 .iter()
2108 .filter_map(|i| match i {
2109 CommonsItem::Fn(f) => match &f.name {
2110 FnName::Free(id) => Some(id.name.clone()),
2111 _ => None,
2112 },
2113 _ => None,
2114 })
2115 .collect();
2116 assert_eq!(
2117 names,
2118 vec!["g".to_string()],
2119 "g must still be recovered as an item; got {names:?}"
2120 );
2121 assert!(
2122 !errors
2123 .iter()
2124 .any(|e| e.category == "bynk.parse.expected_unit_header"),
2125 "the outer body's own closing brace must not be mistaken for \
2126 end-of-file: {errors:?}"
2127 );
2128 }
2129
2130 #[test]
2131 fn doc_block_attaches_to_type() {
2132 let c =
2133 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
2134 .unwrap();
2135 let CommonsItem::Type(t) = &c.items[0] else {
2136 panic!()
2137 };
2138 assert!(t.documentation.is_some());
2139 assert!(
2140 t.documentation
2141 .as_ref()
2142 .unwrap()
2143 .contains("A descriptive doc.")
2144 );
2145 }
2146
2147 #[test]
2148 fn interpolated_string_parses_into_parts() {
2149 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
2151 .unwrap();
2152 let CommonsItem::Fn(f) = &c.items[0] else {
2153 panic!("expected fn")
2154 };
2155 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2156 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
2157 };
2158 assert_eq!(parts.len(), 3);
2159 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
2160 assert!(
2161 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
2162 );
2163 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
2164 }
2165
2166 #[test]
2167 fn interpolated_hole_parses_a_full_expression() {
2168 let c =
2170 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
2171 .unwrap();
2172 let CommonsItem::Fn(f) = &c.items[0] else {
2173 panic!("expected fn")
2174 };
2175 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
2176 panic!("expected InterpStr")
2177 };
2178 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
2179 }
2180
2181 #[test]
2182 fn empty_interpolation_hole_is_rejected() {
2183 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
2184 assert!(
2185 errs.iter()
2186 .any(|e| e.category == "bynk.parse.empty_interpolation"),
2187 "expected empty_interpolation; got {errs:?}"
2188 );
2189 }
2190
2191 #[test]
2192 fn interpolation_hole_lex_error_span_is_rebased() {
2193 let cases = [
2199 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
2201 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
2203 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
2206 ];
2207 for src in cases {
2208 let errs = parse_str(src).unwrap_err();
2209 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
2210 for e in &errs {
2211 assert!(
2212 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
2213 "span {:?} splits a codepoint in {src:?}",
2214 e.span,
2215 );
2216 let hole_start = src.find("\\(").expect("case has a hole") + 2;
2219 assert!(
2220 e.span.start >= hole_start,
2221 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2222 e.span,
2223 );
2224 }
2225 }
2226 }
2227
2228 #[test]
2229 fn fragment_form_parses() {
2230 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2231 assert_eq!(c.form, CommonsForm::Fragment);
2232 assert_eq!(c.items.len(), 1);
2233 }
2234
2235 #[test]
2236 fn uses_parses() {
2237 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2238 assert_eq!(c.uses.len(), 1);
2239 assert_eq!(c.uses[0].target.joined(), "other.lib");
2240 }
2241
2242 #[test]
2244 fn percent_is_a_targeted_error_with_a_fix_for_simple_operands() {
2245 let err = |expr: &str| {
2246 let src = format!("commons m\n\nfn f(n: Int, k: Int) -> Int {{ {expr} }}\n");
2247 let errs = parse_str(&src).unwrap_err();
2248 assert_eq!(
2249 errs[0].category, "bynk.parse.no_remainder_operator",
2250 "{expr}"
2251 );
2252 errs.into_iter().next().unwrap()
2253 };
2254 let fix = err("n % 2");
2255 assert_eq!(fix.suggestions.len(), 1);
2256 assert_eq!(
2257 fix.notes,
2258 [
2259 "use `a.rem(b)` for the C-family remainder, which takes the dividend's sign, \
2260 or `a.mod(b)` for the modulo, which takes the divisor's sign"
2261 ]
2262 );
2263 assert_eq!(fix.suggestions[0].edits[0].1, "n.rem(2)");
2264 assert_eq!(
2265 fix.suggestions[0].applicability,
2266 crate::error::Applicability::MaybeIncorrect
2267 );
2268 assert_eq!(err("n % k").suggestions[0].edits[0].1, "n.rem(k)");
2269 for complex in [
2270 "(n + 1) % k",
2271 "n * 2 % k",
2272 "n % (k + 1)",
2273 "-n % k",
2274 "n % -k",
2275 ] {
2276 assert!(err(complex).suggestions.is_empty(), "{complex}");
2277 }
2278 }
2279
2280 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2281 let toks = tokenize(src).map_err(|e| vec![e])?;
2282 parse_unit(&toks, src)
2283 }
2284
2285 #[test]
2286 fn minimal_context_parses() {
2287 let u = parse_unit_str("context commerce.orders {}").unwrap();
2288 let SourceUnit::Context(c) = u else {
2289 panic!("expected context");
2290 };
2291 assert_eq!(c.name.joined(), "commerce.orders");
2292 assert!(c.items.is_empty());
2293 }
2294
2295 #[test]
2296 fn context_consumes_and_exports_parse() {
2297 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}";
2298 let u = parse_unit_str(src).unwrap();
2299 let SourceUnit::Context(c) = u else { panic!() };
2300 assert_eq!(c.uses.len(), 1);
2301 assert_eq!(c.consumes.len(), 1);
2302 assert_eq!(c.exports.len(), 2);
2303 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2304 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2305 }
2306
2307 #[test]
2308 fn context_fragment_form_parses() {
2309 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2310 let u = parse_unit_str(src).unwrap();
2311 let SourceUnit::Context(c) = u else { panic!() };
2312 assert_eq!(c.form, CommonsForm::Fragment);
2313 assert_eq!(c.uses.len(), 1);
2314 assert_eq!(c.consumes.len(), 1);
2315 assert_eq!(c.exports.len(), 1);
2316 }
2317
2318 #[test]
2319 fn opaque_type_parses() {
2320 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2321 let CommonsItem::Type(t) = &c.items[0] else {
2322 panic!()
2323 };
2324 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2325 }
2326
2327 #[test]
2328 fn empty_commons() {
2329 let c = parse_str("commons fitness.units {}").unwrap();
2330 assert_eq!(c.name.joined(), "fitness.units");
2331 assert!(c.items.is_empty());
2332 }
2333
2334 #[test]
2335 fn one_type_decl() {
2336 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2337 assert_eq!(c.items.len(), 1);
2338 let CommonsItem::Type(t) = &c.items[0] else {
2339 panic!()
2340 };
2341 assert_eq!(t.name.name, "Metres");
2342 match &t.body {
2343 TypeBody::Refined {
2344 base, refinement, ..
2345 } => {
2346 assert_eq!(*base, BaseType::Int);
2347 assert!(refinement.is_some());
2348 }
2349 _ => panic!("expected refined body"),
2350 }
2351 }
2352
2353 #[test]
2354 fn function_decl() {
2355 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2356 let CommonsItem::Fn(f) = &c.items[0] else {
2357 panic!()
2358 };
2359 assert_eq!(f.name.ident().name, "add");
2360 assert_eq!(f.params.len(), 2);
2361 }
2362
2363 #[test]
2364 fn chained_comparison_is_error() {
2365 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2366 .unwrap_err();
2367 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2368 }
2369
2370 #[test]
2371 fn chained_equality_is_error() {
2372 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2373 .unwrap_err();
2374 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2375 }
2376
2377 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2386 std::thread::Builder::new()
2387 .stack_size(64 * 1024 * 1024)
2388 .spawn(f)
2389 .unwrap()
2390 .join()
2391 .unwrap()
2392 }
2393
2394 #[test]
2395 fn deeply_nested_parens_are_bounded_not_overflowed() {
2396 let errs = on_big_stack(|| {
2402 let depth = crate::MAX_NESTING_DEPTH + 8;
2403 let src = format!(
2404 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2405 "(".repeat(depth),
2406 ")".repeat(depth),
2407 );
2408 parse_str(&src).unwrap_err()
2409 });
2410 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2411 }
2412
2413 #[test]
2414 fn deeply_nested_types_are_bounded_not_overflowed() {
2415 let errs = on_big_stack(|| {
2420 let depth = crate::MAX_NESTING_DEPTH + 8;
2421 let src = format!(
2422 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2423 "Result[Int, ".repeat(depth),
2424 "]".repeat(depth),
2425 );
2426 parse_str(&src).unwrap_err()
2427 });
2428 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2429 }
2430
2431 #[test]
2432 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2433 let errs = on_big_stack(|| {
2438 let depth = crate::MAX_NESTING_DEPTH + 8;
2439 let src = format!(
2440 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2441 "Ok(".repeat(depth),
2442 ")".repeat(depth),
2443 );
2444 parse_str(&src).unwrap_err()
2445 });
2446 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2447 }
2448
2449 #[test]
2450 fn nesting_below_the_limit_still_parses() {
2451 let ok = on_big_stack(|| {
2454 let depth = crate::MAX_NESTING_DEPTH - 8;
2455 let src = format!(
2456 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2457 "(".repeat(depth),
2458 ")".repeat(depth),
2459 );
2460 parse_str(&src).is_ok()
2461 });
2462 assert!(ok, "well-nested source under the limit should parse");
2463 }
2464
2465 #[test]
2466 fn let_statement_parses() {
2467 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2468 let CommonsItem::Fn(f) = &c.items[0] else {
2469 panic!()
2470 };
2471 assert_eq!(f.body.statements.len(), 1);
2472 match &f.body.statements[0] {
2473 Statement::Let(l) => {
2474 assert_eq!(l.name.name, "y");
2475 assert!(l.type_annot.is_none());
2476 }
2477 _ => panic!("expected a pure `let` statement"),
2478 }
2479 }
2480
2481 #[test]
2482 fn let_with_annotation() {
2483 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2484 let CommonsItem::Fn(f) = &c.items[0] else {
2485 panic!()
2486 };
2487 match &f.body.statements[0] {
2488 Statement::Let(l) => assert!(l.type_annot.is_some()),
2489 _ => panic!("expected a pure `let` statement"),
2490 }
2491 }
2492
2493 #[test]
2494 fn if_else_parses_as_expression() {
2495 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2496 let CommonsItem::Fn(f) = &c.items[0] else {
2497 panic!()
2498 };
2499 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2500 }
2501
2502 #[test]
2503 fn else_if_chain_parses() {
2504 let c = parse_str(
2505 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2506 )
2507 .unwrap();
2508 let CommonsItem::Fn(f) = &c.items[0] else {
2509 panic!()
2510 };
2511 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2512 panic!()
2513 };
2514 assert!(else_block.statements.is_empty());
2516 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2517 }
2518
2519 #[test]
2520 fn ok_and_err_parse_as_expressions() {
2521 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2522 let CommonsItem::Fn(f) = &c.items[0] else {
2523 panic!()
2524 };
2525 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2526
2527 let c =
2528 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2529 let CommonsItem::Fn(f) = &c.items[0] else {
2530 panic!()
2531 };
2532 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2533 }
2534
2535 #[test]
2536 fn question_postfix_parses() {
2537 let c = parse_str(
2538 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2539 )
2540 .unwrap();
2541 let CommonsItem::Fn(f) = &c.items[1] else {
2542 panic!()
2543 };
2544 let Statement::Let(l) = &f.body.statements[0] else {
2545 panic!("expected a pure `let` statement");
2546 };
2547 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2548 }
2549
2550 #[test]
2551 fn constructor_call_parses() {
2552 let c = parse_str(
2553 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2554 )
2555 .unwrap();
2556 let CommonsItem::Fn(f) = &c.items[1] else {
2557 panic!()
2558 };
2559 let ExprKind::MethodCall {
2562 receiver, method, ..
2563 } = &f.body.tail.kind
2564 else {
2565 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2566 };
2567 let ExprKind::Ident(id) = &receiver.kind else {
2568 panic!("expected receiver Ident");
2569 };
2570 assert_eq!(id.name, "T");
2571 assert_eq!(method.name, "of");
2572 }
2573
2574 #[test]
2575 fn result_type_ref_parses() {
2576 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2577 let CommonsItem::Fn(f) = &c.items[0] else {
2578 panic!()
2579 };
2580 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2581 }
2582
2583 #[test]
2584 fn result_missing_arg_count_errors() {
2585 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2586 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2587 }
2588
2589 #[test]
2590 fn field_access_parses_in_v0_2() {
2591 let c =
2594 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2595 let CommonsItem::Fn(f) = &c.items[1] else {
2596 panic!()
2597 };
2598 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2599 }
2600
2601 #[test]
2604 fn leading_line_comment_attaches_to_next_decl() {
2605 let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2606 let c = parse_str(src).unwrap();
2607 let CommonsItem::Type(t) = &c.items[0] else {
2608 panic!()
2609 };
2610 assert_eq!(
2611 t.trivia.leading,
2612 vec![Comment::Line(" explain the type".to_string())]
2613 );
2614 assert!(t.trivia.trailing.is_none());
2615 }
2616
2617 #[test]
2618 fn trailing_line_comment_attaches_to_prev_decl() {
2619 let src = "commons x {\ntype T = Int where NonNegative // trailing note\n}";
2620 let c = parse_str(src).unwrap();
2621 let CommonsItem::Type(t) = &c.items[0] else {
2622 panic!()
2623 };
2624 assert!(t.trivia.leading.is_empty());
2625 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2626 }
2627
2628 #[test]
2629 fn grouped_leading_comments_attach_together() {
2630 let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2631 let c = parse_str(src).unwrap();
2632 let CommonsItem::Type(t) = &c.items[0] else {
2633 panic!()
2634 };
2635 assert_eq!(
2636 t.trivia.leading,
2637 vec![
2638 Comment::Line(" one".to_string()),
2639 Comment::Line(" two".to_string()),
2640 Comment::Line(" three".to_string())
2641 ],
2642 );
2643 }
2644
2645 #[test]
2646 fn comment_with_doc_block_keeps_both() {
2647 let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2649 let c = parse_str(src).unwrap();
2650 let CommonsItem::Type(t) = &c.items[0] else {
2651 panic!()
2652 };
2653 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2654 assert_eq!(t.documentation.as_deref(), Some("docs"));
2655 }
2656
2657 #[test]
2658 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2659 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2665 let c = parse_str(src).unwrap();
2666 assert_eq!(c.name.joined(), "app.messages");
2667 }
2668
2669 #[test]
2670 fn messages_decl_parses_tag_annotation_and_entries() {
2671 let src = "commons app.messages {\n\
2673 // intro\n\
2674 ---\n\
2675 docs\n\
2676 ---\n\
2677 messages \"en\" @reference {\n\
2678 \"greeting\" => \"Hello, {name}!\"\n\
2679 \"farewell\" => \"Bye\"\n\
2680 } // trailing\n\
2681 }";
2682 let c = parse_str(src).unwrap();
2683 let CommonsItem::Messages(m) = &c.items[0] else {
2684 panic!("expected a messages item, got {:?}", c.items[0]);
2685 };
2686 assert_eq!(m.tag, "en");
2687 assert_eq!(m.annotations.len(), 1);
2688 assert_eq!(m.annotations[0].name.name, "reference");
2689 assert!(m.annotations[0].args.is_empty());
2690 assert_eq!(m.entries.len(), 2);
2691 assert_eq!(m.entries[0].code, "greeting");
2692 assert_eq!(m.entries[0].template, "Hello, {name}!");
2693 assert_eq!(m.entries[1].code, "farewell");
2694 assert_eq!(m.entries[1].template, "Bye");
2695 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2696 assert_eq!(m.documentation.as_deref(), Some("docs"));
2697 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2698 }
2699
2700 #[test]
2701 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2702 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2706 let c = parse_str(src).unwrap();
2707 let CommonsItem::Messages(m) = &c.items[0] else {
2708 panic!("expected a messages item, got {:?}", c.items[0]);
2709 };
2710 assert_eq!(m.tag, "en");
2711 assert!(m.annotations.is_empty());
2712 assert!(m.entries.is_empty());
2713 }
2714
2715 #[test]
2716 fn messages_decl_parses_syntactically_inside_a_context_too() {
2717 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2722 let toks = tokenize(src).unwrap();
2723 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2724 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2725 let Some(SourceUnit::Context(ctx)) = unit else {
2726 panic!("expected a context")
2727 };
2728 let CommonsItem::Messages(m) = &ctx.items[0] else {
2729 panic!("expected a messages item, got {:?}", ctx.items[0]);
2730 };
2731 assert_eq!(m.tag, "en");
2732 }
2733
2734 #[test]
2735 fn comment_before_let_statement_attaches() {
2736 let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2737 let c = parse_str(src).unwrap();
2738 let CommonsItem::Fn(f) = &c.items[0] else {
2739 panic!()
2740 };
2741 let Statement::Let(l) = &f.body.statements[0] else {
2742 panic!()
2743 };
2744 assert_eq!(
2745 l.trivia.leading,
2746 vec![Comment::Line(" pick a value".to_string())]
2747 );
2748 }
2749
2750 #[test]
2751 fn comment_before_tail_attaches_to_block_tail() {
2752 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2753 let c = parse_str(src).unwrap();
2754 let CommonsItem::Fn(f) = &c.items[0] else {
2755 panic!()
2756 };
2757 assert_eq!(
2758 f.body.tail_leading_comments,
2759 vec![Comment::Line(" result".to_string())],
2760 );
2761 }
2762
2763 #[test]
2769 fn contextual_keywords_are_valid_identifiers() {
2770 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2772 .expect("`on`/`suite`/`case` are valid field names");
2773 let CommonsItem::Type(_) = &c.items[0] else {
2774 panic!("expected a type decl")
2775 };
2776
2777 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2779 .expect("`on`/`case` are valid parameter names");
2780
2781 parse_str("commons demo {\n type R = { suite: Int }\n}")
2783 .expect("`suite` is a valid field name");
2784 }
2785
2786 #[test]
2789 fn keywords_are_labels_and_members_not_bindings() {
2790 for src in [
2791 "commons demo {\n type A = { type: String, from: Int, self: Bool }\n}",
2792 "commons demo {\n type S = | P(from: String, type: String)\n}",
2793 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { a.type }\n}",
2794 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type: \"x\" } }\n}",
2795 "commons demo {\n type A = { type: String }\n fn f(a: A) -> String { match a { A { type: t, .. } => t } }\n}",
2796 "commons demo {\n type S = | P(from: String)\n fn f(s: S) -> String { match s { P(from: x) => x } }\n}",
2797 "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}",
2798 ] {
2799 parse_str(src).unwrap_or_else(|e| panic!("should parse: {src}\n{e:?}"));
2800 }
2801 for src in [
2802 "commons demo {\n fn f(type: Int) -> Int { 1 }\n}",
2803 "commons demo {\n fn f() -> Int { let from = 1\n from }\n}",
2804 "commons demo {\n type A = { type String }\n}",
2805 "commons demo {\n fn f(a: Int) -> Int {\n a.\n let x = 1\n x\n }\n}",
2807 ] {
2808 let errs = parse_str(src).expect_err(src);
2809 assert_eq!(errs[0].category, "bynk.parse.reserved_keyword", "{src}");
2810 }
2811 }
2812
2813 #[test]
2817 fn keyword_shorthand_is_a_targeted_error() {
2818 let pat = |extra: &str| {
2819 format!(
2820 "commons demo {{\n type A = {{ type: String }}\n fn f(a: A) -> String {{ {extra} match a {{ A {{ type, .. }} => \"x\" }} }}\n}}"
2821 )
2822 };
2823 let errs = parse_str(&pat("")).expect_err("pattern shorthand");
2824 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2825 let fix = &errs[0].suggestions[0];
2826 assert_eq!(fix.edits[0].1, "type: type_");
2827 assert!(matches!(
2828 fix.applicability,
2829 crate::error::Applicability::MaybeIncorrect
2830 ));
2831
2832 let errs = parse_str(&pat("let type_ = 1")).expect_err("pattern shorthand");
2833 assert_eq!(errs[0].suggestions[0].edits[0].1, "type: type_2");
2834
2835 let errs = parse_str(
2836 "commons demo {\n type A = { type: String }\n fn f() -> A { A { type } }\n}",
2837 )
2838 .expect_err("construction shorthand");
2839 assert_eq!(errs[0].category, "bynk.parse.keyword_field_shorthand");
2840 assert!(errs[0].suggestions.is_empty());
2841 }
2842
2843 #[test]
2851 fn is_reserved_keyword_covers_every_lexer_keyword() {
2852 let lexer_src = include_str!("lexer.rs");
2853 let mut words = Vec::new();
2854 for line in lexer_src.lines() {
2855 let t = line.trim();
2856 if let Some(rest) = t.strip_prefix("#[token(\"")
2857 && let Some(word) = rest.split('"').next()
2858 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2859 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2860 {
2861 words.push(word.to_string());
2862 }
2863 }
2864 assert!(
2865 words.len() > 30,
2866 "keyword extraction looks broken: only {} words",
2867 words.len()
2868 );
2869 use crate::keywords::RESERVED_CONTEXTUAL;
2872 let mut unclassified = Vec::new();
2873 for word in &words {
2874 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2875 let kind = tokens.first().expect("keyword yields a token").kind;
2876 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2877 unclassified.push(word.clone());
2878 }
2879 }
2880 assert!(
2881 unclassified.is_empty(),
2882 "keywords missing from is_reserved_keyword (add them, or document \
2883 them as contextual): {unclassified:?}"
2884 );
2885 }
2886
2887 #[test]
2898 fn is_item_start_matches_the_pinned_keyword_set() {
2899 use TokenKind::*;
2900 let expected_true = [
2901 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Embed, Const, Uses,
2902 Consumes, Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case,
2903 Property,
2904 ];
2905 for kind in expected_true {
2906 assert!(is_item_start(kind), "{kind:?} must be an item start");
2907 }
2908 let expected_false = [
2909 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2910 Where, True, False, Match, Is, On, Given,
2911 ];
2912 for kind in expected_false {
2913 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2914 }
2915 }
2916
2917 #[test]
2922 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2923 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2924 let tokens = crate::lexer::tokenize(src).unwrap();
2925 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2926 assert!(unit.is_some(), "the commons header still parses");
2927 assert!(
2928 errors
2929 .iter()
2930 .any(|e| e.category == "bynk.capability.outside_context"),
2931 "the misplaced capability is reported: {errors:?}"
2932 );
2933 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2936 }
2937
2938 #[test]
2939 fn trailing_file_comment_becomes_unit_trailing() {
2940 let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2944 let c = parse_str(src).unwrap();
2945 assert_eq!(
2946 c.trailing_comments,
2947 vec![Comment::Line(" afterword".to_string())]
2948 );
2949 }
2950
2951 #[test]
2952 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2953 let src = "commons x {\n type T = Int where Positive\n}\n// afterword\n";
2960 let c = parse_str(src).unwrap();
2961 assert_eq!(
2962 c.trailing_comments,
2963 vec![Comment::Line(" afterword".to_string())]
2964 );
2965 }
2966
2967 #[test]
2968 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2969 let src = "context x {\n type T = Int where Positive\n}\n// afterword\n";
2971 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2972 panic!("expected context");
2973 };
2974 assert_eq!(
2975 c.trailing_comments,
2976 vec![Comment::Line(" afterword".to_string())]
2977 );
2978 }
2979
2980 #[test]
2981 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2982 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n// afterword\n";
2984 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2985 panic!("expected suite");
2986 };
2987 assert_eq!(
2988 s.trailing_comments,
2989 vec![Comment::Line(" afterword".to_string())]
2990 );
2991 }
2992
2993 #[test]
3000 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
3001 let src = "adapter x {\n binding \"./x.ts\"\n}\n// afterword\n";
3002 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3003 panic!("expected adapter");
3004 };
3005 assert_eq!(
3006 a.trailing_comments,
3007 vec![Comment::Line(" afterword".to_string())]
3008 );
3009 }
3010
3011 #[test]
3014 fn binding_includes_list_parses_before_requires() {
3015 let src = "adapter x {\n binding \"./x.ts\" includes [\"./a.wasm\", \"b/c.wasm\"] requires { \"p\": \"^1\" }\n}\n";
3016 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
3017 panic!("expected adapter");
3018 };
3019 let b = a.binding.unwrap();
3020 let paths: Vec<_> = b.includes.iter().map(|i| i.path.as_str()).collect();
3021 assert_eq!(paths, ["./a.wasm", "b/c.wasm"]);
3022 assert_eq!(b.requires.len(), 1);
3023 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [\"a.wasm\"").is_err());
3024 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [a]\n}\n").is_err());
3025 }
3026
3027 #[test]
3034 fn commons_fragment_rejects_uses_after_a_decl() {
3035 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
3036 let errs = parse_str(src).unwrap_err();
3037 assert!(
3038 errs.iter()
3039 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3040 "{errs:?}"
3041 );
3042 }
3043
3044 #[test]
3047 fn commons_brace_allows_uses_after_a_decl() {
3048 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
3049 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3050 }
3051
3052 #[test]
3054 fn context_fragment_rejects_consumes_after_a_decl() {
3055 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
3056 let errs = parse_unit_str(src).unwrap_err();
3057 assert!(
3058 errs.iter()
3059 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
3060 "{errs:?}"
3061 );
3062 }
3063
3064 #[test]
3066 fn context_fragment_rejects_exports_after_a_decl() {
3067 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
3068 let errs = parse_unit_str(src).unwrap_err();
3069 assert!(
3070 errs.iter()
3071 .any(|e| e.category == "bynk.parse.exports_after_decls"),
3072 "{errs:?}"
3073 );
3074 }
3075
3076 #[test]
3078 fn context_brace_allows_consumes_and_exports_after_a_decl() {
3079 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
3080 parse_unit_str(src)
3081 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
3082 }
3083
3084 #[test]
3086 fn test_fragment_rejects_uses_after_a_decl() {
3087 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
3088 let errs = parse_unit_str(src).unwrap_err();
3089 assert!(
3090 errs.iter()
3091 .any(|e| e.category == "bynk.parse.uses_after_decls"),
3092 "{errs:?}"
3093 );
3094 }
3095
3096 #[test]
3098 fn test_brace_allows_uses_after_a_decl() {
3099 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
3100 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
3101 }
3102
3103 fn body_tail(body: &str) -> ExprKind {
3107 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3108 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3109 let CommonsItem::Fn(f) = &c.items[0] else {
3110 panic!("expected fn, got {:?}", c.items[0]);
3111 };
3112 f.body.tail.kind.clone()
3113 }
3114
3115 fn body_err(body: &str) -> Vec<CompileError> {
3116 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3117 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
3118 }
3119
3120 fn apply_fix(src: &str, err: &CompileError) -> String {
3124 let s = &err.suggestions[0];
3125 assert_eq!(
3126 s.applicability,
3127 crate::error::Applicability::MachineApplicable
3128 );
3129 let mut edits = s.edits.clone();
3130 edits.sort_by_key(|(span, _)| std::cmp::Reverse(span.start));
3131 let mut out = src.to_string();
3132 for (span, text) in edits {
3133 out.replace_range(span.start..span.end, &text);
3134 }
3135 out
3136 }
3137
3138 #[test]
3141 fn list_patterns_parse_in_every_shape() {
3142 let ExprKind::Match { arms, .. } = body_tail(
3143 "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 }",
3144 ) else {
3145 panic!("expected Match");
3146 };
3147 let shape = |i: usize| match &arms[i].pattern {
3148 Pattern::List { elems, rest, .. } => (
3149 elems.len(),
3150 rest.as_ref()
3151 .map(|r| r.binder.as_ref().map(|b| b.name.clone())),
3152 ),
3153 other => panic!("arm {i}: expected a list pattern, got {other:?}"),
3154 };
3155 assert_eq!(shape(0), (0, None));
3156 assert_eq!(shape(1), (1, None));
3157 assert_eq!(shape(2), (2, Some(None)));
3158 assert_eq!(shape(3), (1, Some(Some("rest".to_string()))));
3159 assert_eq!(shape(4), (0, Some(Some("all".to_string()))));
3160 assert_eq!(shape(5), (0, Some(None)));
3161 assert_eq!(shape(6), (1, Some(None)));
3162 assert!(matches!(&arms[7].pattern, Pattern::Variant { bindings, .. }
3163 if matches!(bindings[0].pattern(), Pattern::List { .. })));
3164 assert!(!arms[0].pattern.is_irrefutable());
3166 assert!(!arms[2].pattern.is_irrefutable());
3167 assert!(arms[4].pattern.is_irrefutable());
3168 assert!(arms[5].pattern.is_irrefutable());
3169 }
3170
3171 #[test]
3172 fn list_pattern_after_is_closes_at_the_bracket() {
3173 for body in [
3174 "if xs is [_, ..] { 1 } else { 2 }",
3175 "if xs is [] { 1 } else { 2 }",
3176 "if xs is [a, ..rest] { 1 } else { 2 }",
3177 "if xs is [S { n: 1, .. }, ..] { 1 } else { 2 }",
3178 "if xs is [] | [_] { 1 } else { 2 }",
3179 ] {
3180 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3181 parse_str(&src).unwrap_or_else(|e| panic!("{body:?}: {e:?}"));
3182 }
3183 }
3184
3185 #[test]
3186 fn list_rest_must_be_last() {
3187 for body in [
3188 "match xs { [.., last] => 1 }",
3189 "match xs { [..r, last] => 1 }",
3190 "match xs { [.., ..] => 1 }",
3191 "match xs { [a, .., b, c] => 1 }",
3192 ] {
3193 let errs = body_err(body);
3194 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last", "{body}");
3195 assert!(
3196 errs[0].suggestions.is_empty(),
3197 "{body}: only trailing wildcards earn a fix"
3198 );
3199 }
3200 }
3201
3202 #[test]
3203 fn list_rest_followed_by_wildcards_has_a_fix() {
3204 for (body, fixed) in [
3205 ("match xs { [.., _] => 1 }", "match xs { [_, ..] => 1 }"),
3206 (
3207 "match xs { [a, .., _, _] => 1 }",
3208 "match xs { [a, _, _, ..] => 1 }",
3209 ),
3210 ] {
3211 let errs = body_err(body);
3212 assert_eq!(errs[0].category, "bynk.parse.list_rest_not_last");
3213 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3214 let got = apply_fix(&src, &errs[0]);
3215 assert!(got.contains(fixed), "{body}: fixed to {got:?}");
3216 parse_str(&got).unwrap_or_else(|e| panic!("{body:?} fixed to {got:?}: {e:?}"));
3217 }
3218 }
3219
3220 #[test]
3221 fn list_rest_followed_by_an_integer_is_a_range_element() {
3222 for body in [
3224 "match xs { [..9] => 1 _ => 0 }",
3225 "match xs { [a, ..-1] => 1 _ => 0 }",
3226 "match xs { [0..9, ..rest] => 1 _ => 0 }",
3227 "match xs { [18.., ..] => 1 _ => 0 }",
3228 ] {
3229 let ExprKind::Match { arms, .. } = body_tail(body) else {
3230 panic!("expected Match: {body}");
3231 };
3232 let Pattern::List { elems, .. } = &arms[0].pattern else {
3233 panic!("expected a list pattern: {body}");
3234 };
3235 assert!(
3236 elems.iter().any(|e| matches!(e, Pattern::Range(_))),
3237 "{body}"
3238 );
3239 }
3240 for body in ["match xs { [..\"a\"] => 1 }", "match xs { [..true] => 1 }"] {
3242 let errs = body_err(body);
3243 assert_eq!(errs[0].category, "bynk.parse.expected_token", "{body}");
3244 }
3245 let errs = body_err("match xs { [a, .._] => 1 }");
3247 assert_eq!(errs[0].category, "bynk.parse.expected_token");
3248 }
3249
3250 #[test]
3251 fn int_range_patterns_parse_in_every_form() {
3252 let range = |body: &str| {
3253 let ExprKind::Match { arms, .. } = body_tail(body) else {
3254 panic!("expected Match: {body}");
3255 };
3256 match &arms[0].pattern {
3257 Pattern::Range(r) => (r.lo, r.hi),
3258 other => panic!("expected a range: {body}: {other:?}"),
3259 }
3260 };
3261 assert_eq!(range("match n { 0..12 => 1 _ => 0 }"), (Some(0), Some(12)));
3262 assert_eq!(range("match n { 18.. => 1 _ => 0 }"), (Some(18), None));
3263 assert_eq!(range("match n { ..-1 => 1 _ => 0 }"), (None, Some(-1)));
3264 assert_eq!(
3265 range("match n { -5..-1 => 1 _ => 0 }"),
3266 (Some(-5), Some(-1))
3267 );
3268 assert_eq!(
3269 range("match n { 1_000..2_000 => 1 _ => 0 }"),
3270 (Some(1000), Some(2000))
3271 );
3272 let ExprKind::Match { arms, .. } = body_tail("match n { 18.. => 1 5 => 2 _ => 0 }") else {
3274 panic!("expected Match");
3275 };
3276 assert!(matches!(arms[0].pattern, Pattern::Range(_)));
3277 assert!(matches!(arms[1].pattern, Pattern::Literal { .. }));
3278 }
3279
3280 #[test]
3281 fn a_range_is_not_an_event_pattern_value() {
3282 let src = "service S from Events(E { n: 1..5 }) { }";
3284 assert!(parse_str(src).is_err());
3285 }
3286
3287 #[test]
3288 fn record_pattern_parses_in_a_match_arm_and_nested() {
3289 let ExprKind::Match { arms, .. } = body_tail(
3290 "match o { Order { status: Paid, total, .. } => 1 Some(Order { .. }) => 2 _ => 3 }",
3291 ) else {
3292 panic!("expected Match");
3293 };
3294 let Pattern::Record {
3295 type_name, fields, ..
3296 } = &arms[0].pattern
3297 else {
3298 panic!("expected a record pattern, got {:?}", arms[0].pattern);
3299 };
3300 assert_eq!(type_name.name, "Order");
3301 assert_eq!(fields.len(), 2);
3302 assert!(fields[0].1.is_some(), "`status: Paid` has a sub-pattern");
3303 assert!(fields[1].1.is_none(), "`total` is shorthand");
3304 let Pattern::Variant { bindings, .. } = &arms[1].pattern else {
3305 panic!("expected Some(..)");
3306 };
3307 assert!(matches!(bindings[0].pattern(), Pattern::Record { .. }));
3308 }
3309
3310 #[test]
3311 fn record_pattern_without_rest_is_an_error_with_a_fix() {
3312 for body in [
3313 "match o { Order { total } => 1 }",
3314 "match o { Order { total, } => 1 }",
3315 "match o { Order { status: Paid, total } => 1 }",
3316 "match o { Order { } => 1 }",
3317 ] {
3318 let errs = body_err(body);
3319 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3320 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3321 let fixed = apply_fix(&src, &errs[0]);
3322 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3323 }
3324 }
3325
3326 #[test]
3327 fn record_pattern_after_is_must_be_parenthesised() {
3328 for body in [
3329 "if o is Order { status: Paid, .. } { 1 } else { 2 }",
3330 "if o is None | Order { .. } { 1 } else { 2 }",
3331 ] {
3332 let errs = body_err(body);
3333 assert_eq!(
3334 errs[0].category,
3335 "bynk.parse.record_pattern_in_is_unparenthesised"
3336 );
3337 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3338 if body.contains("None |") {
3339 continue; }
3341 let fixed = apply_fix(&src, &errs[0]);
3342 parse_str(&fixed).unwrap_or_else(|e| panic!("{body:?} fixed to {fixed:?}: {e:?}"));
3343 }
3344 for body in [
3346 "if o is (Order { status: Paid, .. }) { 1 } else { 2 }",
3347 "if o is Some(Order { .. }) { 1 } else { 2 }",
3348 "if o is (None | Order { .. }) { 1 } else { 2 }",
3349 "if o is Paid { 1 } else { 2 }",
3350 ] {
3351 body_tail(body);
3352 }
3353 }
3354
3355 #[test]
3356 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
3357 for src in [
3360 "if ready { result } else { fallback }",
3361 "if ready { fallback } else { result }",
3362 "if !ready { result } else { fallback }",
3363 "if a == b { result } else { fallback }",
3364 "if a && b { result } else { fallback }",
3365 ] {
3366 let ExprKind::If {
3367 then_block,
3368 else_block,
3369 ..
3370 } = body_tail(src)
3371 else {
3372 panic!("expected If for {src:?}, got {:?}", body_tail(src));
3373 };
3374 assert!(
3377 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
3378 "then-branch tail not an ident for {src:?}: {:?}",
3379 then_block.tail.kind,
3380 );
3381 assert!(
3382 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
3383 "else-branch tail not an ident for {src:?}: {:?}",
3384 else_block.tail.kind,
3385 );
3386 }
3387 }
3388
3389 #[test]
3390 fn else_less_if_with_single_ident_branch_parses() {
3391 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
3393 panic!("expected If");
3394 };
3395 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
3396 }
3397
3398 #[test]
3399 fn record_construction_still_parses_in_value_position() {
3400 assert!(matches!(
3403 body_tail("Point { x }"),
3404 ExprKind::RecordConstruction { .. }
3405 ));
3406 assert!(matches!(
3407 body_tail("Point { x: 1, y: 2 }"),
3408 ExprKind::RecordConstruction { .. }
3409 ));
3410 assert!(matches!(
3411 body_tail("Empty {}"),
3412 ExprKind::RecordConstruction { .. }
3413 ));
3414 }
3415
3416 #[test]
3417 fn parenthesised_record_is_allowed_in_condition_head() {
3418 let ExprKind::If { cond, .. } =
3421 body_tail("if (ready { result }) { branch } else { other }")
3422 else {
3423 panic!("expected If");
3424 };
3425 let ExprKind::Paren(inner) = &cond.kind else {
3426 panic!("expected a parenthesised condition, got {:?}", cond.kind);
3427 };
3428 assert!(
3429 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
3430 "parenthesised record in condition head should still construct: {:?}",
3431 inner.kind,
3432 );
3433 }
3434
3435 #[test]
3436 fn record_in_call_arg_within_condition_still_constructs() {
3437 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
3440 else {
3441 panic!("expected If");
3442 };
3443 let ExprKind::Call { args, .. } = &cond.kind else {
3444 panic!("expected Call in condition, got {:?}", cond.kind);
3445 };
3446 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
3447 }
3448
3449 #[test]
3450 fn safe_condition_shapes_are_unaffected() {
3451 assert!(matches!(
3453 body_tail("if ready == true { result } else { fallback }"),
3454 ExprKind::If { .. }
3455 ));
3456 assert!(matches!(
3457 body_tail("if (ready) { result } else { fallback }"),
3458 ExprKind::If { .. }
3459 ));
3460 assert!(matches!(
3461 body_tail("if ready { \"a\" } else { \"b\" }"),
3462 ExprKind::If { .. }
3463 ));
3464 }
3465
3466 fn body_block(body: &str) -> Block {
3467 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3468 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
3469 let CommonsItem::Fn(f) = &c.items[0] else {
3470 panic!("expected fn, got {:?}", c.items[0]);
3471 };
3472 f.body.clone()
3473 }
3474
3475 fn has_error(errs: &[CompileError], code: &str) -> bool {
3476 errs.iter().any(|e| e.category == code)
3477 }
3478
3479 fn lambda_of(body: &str) -> LambdaExpr {
3482 let b = body_block(body);
3483 let ExprKind::Lambda(l) = &b.tail.kind else {
3484 panic!("expected a lambda tail, got {:?}", b.tail.kind);
3485 };
3486 l.clone()
3487 }
3488
3489 #[test]
3490 fn a_record_pattern_parameter_desugars_to_a_hidden_binder_and_a_let() {
3491 let l = lambda_of("(Item { qty, .. }) => qty");
3493 assert_eq!(l.params.len(), 1);
3494 let p = &l.params[0];
3495 assert!(p.name.name.starts_with(LAMBDA_ARG_BINDER_PREFIX));
3496 assert!(is_generated_binding(&p.name.name));
3497 assert_eq!(
3498 p.name.span.start, p.name.span.end,
3499 "the binder has no extent"
3500 );
3501 assert!(p.type_ref.is_none());
3502 assert!(matches!(p.pattern, Some(Pattern::Record { .. })));
3503 assert!(!l.block_body);
3504 let ExprKind::Block(b) = &l.body.kind else {
3505 panic!("expected a block body, got {:?}", l.body.kind);
3506 };
3507 let Statement::Let(hidden) = &b.statements[0] else {
3508 panic!("expected the hidden `let`, got {:?}", b.statements[0]);
3509 };
3510 assert_eq!(
3511 hidden.let_else.as_ref().map(|le| le.form),
3512 Some(LetElseForm::Plain)
3513 );
3514 assert!(b.tail.is_let_else_match());
3515 assert!(
3516 matches!(&hidden.value.kind, ExprKind::Ident(i) if i.name == p.name.name),
3517 "the `let` destructures the hidden parameter"
3518 );
3519 }
3520
3521 #[test]
3522 fn name_and_pattern_parameters_mix_and_each_binder_is_distinct() {
3523 let l = lambda_of("(a, Item { qty, .. }, b: Int, Box { v, .. }) => a");
3524 let names: Vec<_> = l.params.iter().map(|p| p.name.name.as_str()).collect();
3525 assert_eq!(names[0], "a");
3526 assert_eq!(names[2], "b");
3527 assert!(names[1].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3528 assert!(names[3].starts_with(LAMBDA_ARG_BINDER_PREFIX));
3529 assert_ne!(names[1], names[3]);
3530 assert!(l.params[0].pattern.is_none() && l.params[2].type_ref.is_some());
3531 let ExprKind::Block(outer) = &l.body.kind else {
3533 panic!("expected a block body");
3534 };
3535 let ExprKind::Match { arms, .. } = &outer.tail.kind else {
3536 panic!("expected the first pattern's match");
3537 };
3538 let MatchBody::Block(inner) = &arms[0].body else {
3539 panic!("expected the arm's block");
3540 };
3541 assert!(inner.tail.is_let_else_match(), "the second pattern nests");
3542 }
3543
3544 #[test]
3545 fn a_block_body_is_kept_as_the_pattern_arms_block() {
3546 let l = lambda_of("(Item { qty, .. }) => { let x = qty\n x }");
3547 assert!(l.block_body);
3548 }
3549
3550 #[test]
3551 fn a_lambda_without_pattern_parameters_is_unchanged() {
3552 let l = lambda_of("(a, b: Int) => a");
3553 assert!(l.params.iter().all(|p| p.pattern.is_none()));
3554 assert!(matches!(l.body.kind, ExprKind::Ident(_)));
3555 }
3556
3557 #[test]
3558 fn a_lambda_pattern_parameter_that_is_refutable_names_its_first_failing_field() {
3559 let errs = body_err("(Item { status: Paid, qty, .. }) => qty");
3560 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_refutable");
3561 assert!(
3562 errs[0].notes.iter().any(|n| n.contains("status: Paid")),
3563 "{:?}",
3564 errs[0].notes
3565 );
3566 }
3567
3568 #[test]
3569 fn a_lambda_pattern_parameter_must_be_a_record_pattern() {
3570 for body in [
3571 "(Some(x)) => x",
3572 "(a, [x, ..]) => x",
3573 "(0..9) => 1",
3574 "(1) => 1",
3575 "(None) => 1",
3576 "(Status.Paid) => 1",
3577 ] {
3578 let errs = body_err(body);
3579 assert_eq!(
3580 errs[0].category, "bynk.parse.lambda_pattern_not_record",
3581 "{body}"
3582 );
3583 }
3584 }
3585
3586 #[test]
3587 fn an_annotated_lambda_pattern_parameter_offers_to_remove_the_annotation() {
3588 let body = "(Item { qty, .. }: Item) => qty";
3589 let errs = body_err(body);
3590 assert_eq!(errs[0].category, "bynk.parse.lambda_pattern_annotation");
3591 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3592 let fixed = apply_fix(&src, &errs[0]);
3593 assert!(fixed.contains("(Item { qty, .. }) => qty"), "{fixed}");
3594 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3595 }
3596
3597 #[test]
3598 fn a_missing_rest_in_a_lambda_pattern_parameter_has_slice_ones_fix() {
3599 let body = "(a, Item { qty }) => qty";
3600 let errs = body_err(body);
3601 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3602 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
3603 let fixed = apply_fix(&src, &errs[0]);
3604 parse_str(&fixed).unwrap_or_else(|e| panic!("fixed to {fixed:?}: {e:?}"));
3605 }
3606
3607 #[test]
3608 fn let_else_desugars_to_a_hidden_binding_and_a_match() {
3609 let b = body_block("let x = 1\n let Some(n) = find(x) else { 0 }\n n + x");
3612 assert_eq!(
3613 b.statements.len(),
3614 2,
3615 "the plain let and the hidden binding"
3616 );
3617 let Statement::Let(hidden) = &b.statements[1] else {
3618 panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
3619 };
3620 assert!(
3621 is_generated_binding(&hidden.name.name),
3622 "{}",
3623 hidden.name.name
3624 );
3625 assert_eq!(
3626 hidden.name.span.start, hidden.name.span.end,
3627 "the binder has no extent"
3628 );
3629 let le = hidden
3630 .let_else
3631 .as_ref()
3632 .expect("the binding records the sugar");
3633 assert_eq!(le.form, LetElseForm::Block);
3634 let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
3635 panic!("expected a match tail, got {:?}", b.tail.kind);
3636 };
3637 assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
3638 assert_eq!(arms.len(), 2);
3639 assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
3640 let MatchBody::Block(rest) = &arms[0].body else {
3641 panic!("the pattern's arm runs the rest of the block");
3642 };
3643 assert!(rest.statements.is_empty());
3644 assert!(
3645 !matches!(rest.tail.kind, ExprKind::UnitLit),
3646 "{:?}",
3647 rest.tail.kind
3648 );
3649 assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
3650 }
3651
3652 #[test]
3654 fn par_block_holds_its_statements_in_source_order() {
3655 let b = body_block(
3656 "par {\n let a <- one()\n do two()\n let b: Int <- three()\n }\n a",
3657 );
3658 let Statement::Par(p) = &b.statements[0] else {
3659 panic!("expected a par block, got {:?}", b.statements[0]);
3660 };
3661 assert_eq!(p.statements.len(), 3);
3662 assert!(matches!(&p.statements[0], Statement::EffectLet(l) if l.name.name == "a"));
3663 assert!(matches!(&p.statements[1], Statement::Do(_)));
3664 assert!(
3665 matches!(&p.statements[2], Statement::EffectLet(l) if l.name.name == "b" && l.type_annot.is_some())
3666 );
3667 }
3668
3669 #[test]
3671 fn par_is_an_identifier_outside_its_statement_shape() {
3672 let b = body_block("let par = 1\n let x = par + 1\n x");
3673 assert_eq!(b.statements.len(), 2);
3674 assert!(matches!(&b.statements[0], Statement::Let(l) if l.name.name == "par"));
3675 }
3676
3677 #[test]
3680 fn par_block_admits_statements_for_the_checker_to_refuse() {
3681 let b = body_block(
3682 "par {\n let a = 1\n ~> notify()\n par {\n do x()\n }\n }\n 1",
3683 );
3684 let Statement::Par(p) = &b.statements[0] else {
3685 panic!("expected a par block");
3686 };
3687 assert!(matches!(&p.statements[0], Statement::Let(_)));
3688 assert!(matches!(&p.statements[1], Statement::Send(_)));
3689 assert!(matches!(&p.statements[2], Statement::Par(_)));
3690 }
3691
3692 #[test]
3693 fn par_block_refuses_a_refutable_let_and_a_stray_expression() {
3694 assert!(has_error(
3695 &body_err("par {\n let Ok(v) <- load() else { 0 }\n }\n 1"),
3696 "bynk.par.invalid_statement"
3697 ));
3698 assert!(has_error(
3699 &body_err("par {\n load()\n }\n 1"),
3700 "bynk.par.invalid_statement"
3701 ));
3702 }
3703
3704 #[test]
3705 fn effectful_let_else_binds_with_an_arrow() {
3706 let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n v");
3707 assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
3708 }
3709
3710 #[test]
3711 fn let_else_chain_nests_each_step_in_the_previous_arm() {
3712 let b = body_block(
3715 "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",
3716 );
3717 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3718 panic!("expected a match tail");
3719 };
3720 let MatchBody::Block(second) = &arms[0].body else {
3721 panic!("expected a block arm");
3722 };
3723 assert_eq!(
3724 second.statements.len(),
3725 2,
3726 "`let y` and the second hidden binding"
3727 );
3728 assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
3729 let ExprKind::Match { arms, .. } = &second.tail.kind else {
3730 panic!("expected the second step's match");
3731 };
3732 let MatchBody::Block(third) = &arms[0].body else {
3733 panic!("expected a block arm");
3734 };
3735 let ExprKind::Match { arms, .. } = &third.tail.kind else {
3736 panic!("expected the third step's match");
3737 };
3738 let MatchBody::Block(last) = &arms[0].body else {
3739 panic!("expected a block arm");
3740 };
3741 assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3742 }
3743
3744 #[test]
3745 fn else_match_arms_follow_the_patterns_arm() {
3746 let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n v");
3747 let Statement::Let(l) = &b.statements[0] else {
3748 panic!("expected the hidden binding");
3749 };
3750 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3751 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3752 panic!("expected a match tail");
3753 };
3754 assert_eq!(arms.len(), 2);
3755 assert!(
3756 matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3757 );
3758 }
3759
3760 #[test]
3761 fn an_irrefutable_record_let_needs_no_else() {
3762 for body in [
3765 "let Item { sku, qty: n, .. } = item\n n",
3766 "let (Item { sku, .. }) = item\n sku",
3767 "let Item { sku: _, inner: Inner { x, .. }, .. } <- load(1)\n x",
3768 "let Item { sku, .. } <- load(1) by Admin(\"a\")\n sku",
3769 ] {
3770 let b = body_block(body);
3771 let (Statement::Let(l) | Statement::EffectLet(l)) = &b.statements[0] else {
3772 panic!("{body:?}: expected the hidden binding");
3773 };
3774 let le = l.let_else.as_ref().expect("the binding records the sugar");
3775 assert_eq!(le.form, LetElseForm::Plain, "{body:?}");
3776 assert_eq!(le.else_span.start, le.else_span.end, "no `else` extent");
3777 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3778 panic!("{body:?}: expected a match tail");
3779 };
3780 assert_eq!(arms.len(), 1, "{body:?}: only the pattern's arm");
3781 assert!(matches!(arms[0].pattern, Pattern::Record { .. }));
3782 }
3783 }
3784
3785 #[test]
3786 fn a_refutable_record_let_without_else_names_the_refutable_field() {
3787 for (body, field) in [
3788 (
3789 "let Order { status: Paid, total, .. } = o\n total",
3790 "status: Paid",
3791 ),
3792 (
3793 "let Order { total, c: Cust { kind: Gold, .. }, .. } = o\n total",
3794 "kind: Gold",
3795 ),
3796 ("let Order { id: 1, .. } <- load()\n 1", "id: 1"),
3797 ] {
3798 let errs = body_err(body);
3799 assert_eq!(errs[0].category, "bynk.parse.let_pattern_without_else");
3800 assert!(
3801 errs[0]
3802 .notes
3803 .iter()
3804 .any(|n| n.contains(&format!("`{field}` can fail to match"))),
3805 "{body:?}: {:?}",
3806 errs[0].notes
3807 );
3808 }
3809 for body in [
3811 "let Ok(n) = r\n n",
3812 "let (Ok(n)) = r\n n",
3813 "let Item { sku, .. } | Other = r\n 1",
3814 ] {
3815 let errs = body_err(body);
3816 assert_eq!(
3817 errs[0].category, "bynk.parse.let_pattern_without_else",
3818 "{body:?}"
3819 );
3820 }
3821 }
3822
3823 #[test]
3824 fn a_record_let_keeps_an_else_when_one_is_written() {
3825 let b = body_block("let Item { sku, .. } = item else { 0 }\n 1");
3826 let Statement::Let(l) = &b.statements[0] else {
3827 panic!("expected the hidden binding");
3828 };
3829 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Block);
3830 }
3831
3832 #[test]
3833 fn a_capitalised_binding_is_not_a_record_pattern() {
3834 for body in ["let Total = 1\n Total", "let Total: Int = 1\n Total"] {
3836 let b = body_block(body);
3837 let Statement::Let(l) = &b.statements[0] else {
3838 panic!("{body:?}: expected a plain let");
3839 };
3840 assert!(l.let_else.is_none(), "{body:?}");
3841 }
3842 let errs = body_err("let Item { sku } = item\n 1");
3844 assert_eq!(errs[0].category, "bynk.parse.record_pattern_without_rest");
3845 }
3846
3847 #[test]
3848 fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3849 let b = body_block("let Some(n) = find(1) else { () }");
3850 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3851 panic!("expected a match tail");
3852 };
3853 let MatchBody::Block(rest) = &arms[0].body else {
3854 panic!("expected a block arm");
3855 };
3856 assert!(rest.implicit_tail && rest.statements.is_empty());
3857 }
3858
3859 #[test]
3860 fn let_pattern_forms_are_recognised() {
3861 for src in [
3862 "let None = o else { 0 }\n 1",
3863 "let Status.Done = s else { 0 }\n 1",
3864 "let Shipped(at) = s else { 0 }\n at",
3865 "let 0 = n else { 1 }\n 0",
3866 "let -1 = n else { 1 }\n 0",
3867 "let \"a\" = s else { 1 }\n 0",
3868 "let (Ok(x) | Err(x)) = r else { 1 }\n x",
3869 ] {
3870 let b = body_block(src);
3871 assert!(
3872 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3873 "{src:?} should parse as a let-else"
3874 );
3875 }
3876 let b = body_block("let Total = 1\n Total");
3878 assert!(
3879 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3880 );
3881 }
3882
3883 #[test]
3884 fn let_pattern_without_else_is_refused() {
3885 assert!(has_error(
3886 &body_err("let Ok(v) = r\n v"),
3887 "bynk.parse.let_pattern_without_else"
3888 ));
3889 assert!(has_error(
3890 &body_err("let Ok(v) <- r()\n v"),
3891 "bynk.parse.let_pattern_without_else"
3892 ));
3893 }
3894
3895 #[test]
3896 fn else_on_a_plain_binding_is_refused() {
3897 assert!(has_error(
3898 &body_err("let v = r else { 0 }\n v"),
3899 "bynk.parse.let_else_on_binding"
3900 ));
3901 assert!(has_error(
3902 &body_err("let v <- r() else { 0 }\n v"),
3903 "bynk.parse.let_else_on_binding"
3904 ));
3905 }
3906
3907 #[test]
3908 fn annotated_let_pattern_is_refused() {
3909 assert!(has_error(
3910 &body_err("let Ok(v): Int = r else { 0 }\n v"),
3911 "bynk.parse.let_pattern_annotation"
3912 ));
3913 }
3914
3915 #[test]
3916 fn empty_else_match_is_refused() {
3917 assert!(has_error(
3918 &body_err("let Ok(v) = r else match { }\n v"),
3919 "bynk.parse.empty_match"
3920 ));
3921 }
3922
3923 #[test]
3924 fn let_else_steps_count_against_the_nesting_budget() {
3925 let chain = |n: usize| -> String {
3929 let steps: String = (0..n)
3930 .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n "))
3931 .collect();
3932 format!("{steps}0")
3933 };
3934 body_block(&chain(40));
3935 let errs = body_err(&chain(400));
3936 assert!(
3937 errs.iter()
3938 .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3939 "{errs:?}"
3940 );
3941 }
3942
3943 #[test]
3944 fn empty_match_reports_its_own_diagnostic() {
3945 let errs = body_err("match result {}");
3949 assert!(
3950 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3951 "expected empty_match; got {errs:?}",
3952 );
3953 }
3954
3955 #[test]
3956 fn match_discriminant_ending_in_ident_parses() {
3957 assert!(matches!(
3959 body_tail("match ready { x => x }"),
3960 ExprKind::Match { .. }
3961 ));
3962 }
3963
3964 #[test]
3973 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3974 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
3975 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3976 let CommonsItem::Fn(f) = &c.items[0] else {
3977 panic!("expected fn, got {:?}", c.items[0]);
3978 };
3979 assert_eq!(
3980 f.body.statements.len(),
3981 1,
3982 "expected exactly one Assign statement, got {:?}",
3983 f.body.statements
3984 );
3985 let Statement::Assign(a) = &f.body.statements[0] else {
3986 panic!(
3987 "expected an Assign statement, got {:?}",
3988 f.body.statements[0]
3989 );
3990 };
3991 assert!(
3992 matches!(a.value.kind, ExprKind::Ident(_)),
3993 "assign value must stay the bare identifier `Paid`, got {:?}",
3994 a.value.kind
3995 );
3996 assert!(
3997 matches!(f.body.tail.kind, ExprKind::UnitLit),
3998 "the `()` must remain the block's own tail, got {:?}",
3999 f.body.tail.kind
4000 );
4001 }
4002
4003 #[test]
4008 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
4009 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
4010 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
4011 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4012 let CommonsItem::Fn(f) = &c.items[0] else {
4013 panic!("expected fn, got {:?}", c.items[0]);
4014 };
4015 let Statement::Let(l) = &f.body.statements[0] else {
4016 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
4017 };
4018 let ExprKind::Is { pattern, .. } = &l.value.kind else {
4019 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
4020 };
4021 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
4022 panic!("{name}: expected a variant pattern, got {pattern:?}");
4023 };
4024 assert_eq!(
4025 span.end, variant.span.end,
4026 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
4027 );
4028 assert!(
4029 matches!(f.body.tail.kind, ExprKind::UnitLit),
4030 "{name}: the `()` must remain the block's own tail, got {:?}",
4031 f.body.tail.kind
4032 );
4033 }
4034 }
4035
4036 #[test]
4039 fn variant_pattern_payload_on_the_same_line_still_binds() {
4040 for (name, pat) in [
4041 ("user variant", "Paid(\n x\n )"),
4042 ("built-in variant", "Some(x)"),
4043 ] {
4044 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
4045 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
4046 let CommonsItem::Fn(f) = &c.items[0] else {
4047 panic!("expected fn, got {:?}", c.items[0]);
4048 };
4049 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
4050 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
4051 };
4052 assert!(
4053 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
4054 "{name}: expected one binding, got {pattern:?}"
4055 );
4056 }
4057 }
4058
4059 #[test]
4065 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
4066 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
4067 assert!(
4068 parse_str(src).is_err(),
4069 "a next-line `(` after an arm's variant must not open its payload list"
4070 );
4071 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
4072 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
4073 }
4074
4075 #[test]
4079 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
4080 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
4081 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
4082 let CommonsItem::Fn(f) = &c.items[0] else {
4083 panic!("expected fn, got {:?}", c.items[0]);
4084 };
4085 let Statement::Let(l) = &f.body.statements[0] else {
4086 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
4087 };
4088 assert!(
4089 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
4090 "let value must stay a field access, got {:?}",
4091 l.value.kind
4092 );
4093 assert!(
4094 matches!(f.body.tail.kind, ExprKind::UnitLit),
4095 "the `()` must remain the block's own tail, got {:?}",
4096 f.body.tail.kind
4097 );
4098 }
4099
4100 #[test]
4101 fn unparenthesised_record_in_condition_head_now_errors() {
4102 assert!(
4108 !body_err("match Point { x: 1 } { p => p }").is_empty(),
4109 "unparenthesised record discriminant should not parse",
4110 );
4111 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
4113 else {
4114 panic!("expected Match for the parenthesised form");
4115 };
4116 let ExprKind::Paren(inner) = &discriminant.kind else {
4117 panic!(
4118 "expected a parenthesised discriminant, got {:?}",
4119 discriminant.kind
4120 );
4121 };
4122 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
4123 }
4124
4125 fn strict_codes(src: &str) -> Vec<&'static str> {
4127 let toks = tokenize(src).unwrap();
4128 match parse_units_with_warnings(&toks, src) {
4129 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
4130 Err(errs) => errs.iter().map(|e| e.category).collect(),
4131 }
4132 }
4133
4134 #[test]
4138 fn a_divider_pair_around_code_is_doc_block_contains_code() {
4139 let src =
4140 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
4141 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
4142 let errs = parse_str(src).unwrap_err();
4143 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
4144 assert_eq!(
4146 &src[errs[0].span.range()],
4147 "--------\nfn helper() -> Int { 1 }\n--------\n"
4148 );
4149 let (unit, errs) = parse_recover_str(src);
4150 assert!(unit.is_some(), "recovery keeps the unit");
4151 assert!(
4152 errs.iter()
4153 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
4154 "{errs:?}"
4155 );
4156 }
4157
4158 #[test]
4161 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
4162 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
4163 let codes = strict_codes(orphan);
4164 assert!(
4165 codes.contains(&"bynk.parse.doc_block_contains_code"),
4166 "{codes:?}"
4167 );
4168 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
4169 let codes = strict_codes(trailing);
4170 assert!(
4171 codes.contains(&"bynk.parse.doc_block_contains_code"),
4172 "{codes:?}"
4173 );
4174 }
4175
4176 #[test]
4180 fn near_miss_prose_and_fenced_code_are_not_code() {
4181 for doc in [
4182 "type of the thing",
4183 "fn is used here",
4184 "The type Id is an Int.",
4185 "uses the clock",
4186 "Event handler.\nfn helper() -> Int { 1 }",
4187 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
4188 "~~~~\ntype Id = Int\n~~~~",
4189 "---\nA nested doc.\n---",
4190 "",
4191 ] {
4192 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4193 }
4194 for doc in [
4195 "fn helper() -> Int { 1 }",
4196 "type Id = Int\nfn f() -> Int { 1 }",
4197 "```\nan example\n```\nfn helper() -> Int { 1 }",
4198 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
4199 ] {
4200 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
4201 }
4202 }
4203
4204 #[test]
4208 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
4209 let nested =
4210 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
4211 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
4212 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
4213 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
4214 assert!(doc_content_is_code(
4216 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
4217 ));
4218 for doc in [
4219 "---\nAn inner doc.\n---",
4220 "Intro.\n\n---\n\nfn is used here",
4221 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
4222 ] {
4223 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
4224 }
4225 let mut deep = "fn helper() -> Int { 1 }".to_string();
4227 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
4228 let fence = "-".repeat(n);
4229 deep = format!("{fence}\n{deep}\n{fence}");
4230 }
4231 assert!(!doc_content_is_code(&deep));
4232 }
4233
4234 #[test]
4237 fn a_fenced_nested_doc_example_parses_clean() {
4238 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";
4239 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4240 let c = parse_str(src).unwrap();
4241 let CommonsItem::Fn(f) = &c.items[0] else {
4242 panic!("expected fn")
4243 };
4244 assert!(
4245 f.documentation
4246 .as_deref()
4247 .is_some_and(|d| d.contains("---\nInner.\n---")),
4248 "{:?}",
4249 f.documentation
4250 );
4251 }
4252
4253 #[test]
4257 fn private_modifier_is_contextual() {
4258 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";
4259 let c = parse_str(src).unwrap();
4260 let t = type_at(&c, 0);
4261 let at = src.find("private type").unwrap();
4262 assert_eq!(t.private.map(|s| s.start), Some(at));
4263 assert_eq!(t.span.start, at);
4264 assert_eq!(doc_of(&t.documentation), Some(("Hidden.", DocForm::Lines)));
4265 assert!(c.items[1].is_private());
4266 assert!(!c.items[2].is_private());
4267 }
4268
4269 #[test]
4272 fn private_modifier_on_a_constant() {
4273 let src = "commons m\n\nprivate const Max: Int = 3\n\nconst Min: Int = 1\n";
4274 let c = parse_str(src).unwrap();
4275 let CommonsItem::Const(k) = &c.items[0] else {
4276 panic!("a constant");
4277 };
4278 assert_eq!(
4279 k.private.map(|s| s.start),
4280 Some(src.find("private").unwrap())
4281 );
4282 assert_eq!(k.span.start, src.find("private").unwrap());
4283 assert!(c.items[0].is_private());
4284 assert!(!c.items[1].is_private());
4285 }
4286
4287 #[test]
4289 fn private_on_a_service_is_rejected() {
4290 let src = "context m\n\nprivate service s {\n on call() -> Effect[Int] { 1 }\n}\n";
4291 let tokens = crate::lexer::tokenize(src).unwrap();
4292 let errs = parse_units(&tokens, src).unwrap_err();
4293 assert_eq!(errs[0].category, "bynk.visibility.private_not_allowed");
4294 }
4295
4296 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
4298 let CommonsItem::Type(t) = &c.items[i] else {
4299 panic!("expected a type at {i}")
4300 };
4301 t
4302 }
4303
4304 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
4305 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
4308 }
4309
4310 #[test]
4313 fn doc_lines_document_a_declaration() {
4314 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
4315 let c = parse_str(src).unwrap();
4316 assert_eq!(
4317 doc_of(&type_at(&c, 0).documentation),
4318 Some(("A commit SHA.", DocForm::Lines))
4319 );
4320 let CommonsItem::Fn(f) = &c.items[1] else {
4321 panic!("expected fn")
4322 };
4323 assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
4324 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4325 }
4326
4327 #[test]
4331 fn legacy_spellings_parse_and_are_each_reported() {
4332 let src =
4333 "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n repo: String, -- trailing\n}\n";
4334 assert_eq!(
4335 strict_codes(src),
4336 [
4337 "bynk.lex.legacy_comment",
4338 "bynk.lex.legacy_doc_line",
4339 "bynk.lex.legacy_comment"
4340 ]
4341 );
4342 let tokens = tokenize(src).unwrap();
4343 let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
4344 let SourceUnit::Commons(c) = &units[0] else {
4345 panic!("commons")
4346 };
4347 assert_eq!(
4348 doc_of(&type_at(c, 0).documentation),
4349 Some(("A run.", DocForm::Lines))
4350 );
4351 }
4352
4353 #[test]
4356 fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
4357 let src = "commons m\n\ntype R = {\n --- First.\n --- Second.\n a: Int,\n}\n";
4358 let tokens = tokenize(src).unwrap();
4359 let errs = parse_units(&tokens, src).unwrap_err();
4360 assert_eq!(errs.len(), 1, "{errs:?}");
4361 assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
4362 let (span, block) = &errs[0].suggestions[0].edits[0];
4363 assert_eq!(&src[span.range()], "--- First.\n --- Second.");
4364 assert_eq!(block, "---\n First.\n Second.\n ---");
4365 }
4366
4367 #[test]
4371 fn members_take_docs_in_either_form() {
4372 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";
4373 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
4374 let c = parse_str(src).unwrap();
4375 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4376 panic!("record")
4377 };
4378 assert_eq!(
4379 doc_of(&r.fields[0].documentation),
4380 Some(("The repository.", DocForm::Lines))
4381 );
4382 assert_eq!(
4383 doc_of(&r.fields[1].documentation),
4384 Some(("The version.", DocForm::Block))
4385 );
4386 assert_eq!(r.fields[2].documentation, None);
4387 assert_eq!(
4388 r.fields[2].trivia.leading,
4389 vec![Comment::Line(" a note".into())]
4390 );
4391 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
4392 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
4393 panic!("enum")
4394 };
4395 assert_eq!(
4396 doc_of(&e.variants[0].documentation),
4397 Some(("Passed.", DocForm::Lines))
4398 );
4399 assert_eq!(e.variants[1].documentation, None);
4400 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
4401 panic!("sum")
4402 };
4403 assert_eq!(
4404 doc_of(&p.variants[0].documentation),
4405 Some(("Round.", DocForm::Lines))
4406 );
4407 assert_eq!(
4408 doc_of(&p.variants[0].payload[0].documentation),
4409 Some(("The radius.", DocForm::Lines))
4410 );
4411 assert_eq!(p.variants[1].documentation, None);
4412 let CommonsItem::Fn(f) = &c.items[3] else {
4414 panic!("expected fn")
4415 };
4416 assert_eq!(
4417 doc_of(&f.documentation),
4418 Some(("After the sum.", DocForm::Lines))
4419 );
4420 let CommonsItem::Messages(m) = &c.items[4] else {
4421 panic!("expected messages")
4422 };
4423 assert_eq!(
4424 doc_of(&m.entries[0].documentation),
4425 Some(("Greets.", DocForm::Lines))
4426 );
4427 }
4428
4429 #[test]
4433 fn two_docs_on_one_target_is_multiple_docs() {
4434 for src in [
4435 "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
4436 "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
4437 "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
4438 "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
4439 "commons m\n\ntype R = {\n --- Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
4440 ] {
4441 assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
4442 let (unit, errs) = parse_recover_str(src);
4443 assert!(unit.is_some());
4444 assert!(
4445 errs.iter()
4446 .any(|e| e.category == "bynk.parse.multiple_docs")
4447 );
4448 }
4449 }
4450
4451 #[test]
4454 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
4455 let src =
4456 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --- dangling\n )\n | Dot\n";
4457 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
4458 let c = parse_str(src).unwrap();
4459 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
4460 panic!("sum")
4461 };
4462 assert_eq!(s.variants[0].payload.len(), 1);
4463 assert_eq!(s.variants[0].payload[0].documentation, None);
4464 }
4465
4466 #[test]
4469 fn an_unattached_member_doc_is_an_orphan() {
4470 let src = "commons m\n\ntype R = {\n --- Spaced.\n\n a: Int,\n --- Dangling.\n}\n";
4471 assert_eq!(
4472 strict_codes(src),
4473 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
4474 );
4475 let c = parse_str(src).unwrap();
4476 let TypeBody::Record(r) = &type_at(&c, 0).body else {
4477 panic!("record")
4478 };
4479 assert_eq!(r.fields[0].documentation, None);
4480 assert!(matches!(
4481 &r.fields[0].trivia.leading[..],
4482 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
4483 ));
4484 assert!(matches!(
4485 &r.trailing_comments[..],
4486 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
4487 ));
4488 }
4489}
4490
4491#[cfg(test)]
4492mod paren_guard_2721 {
4493 use super::*;
4494 use crate::lexer::tokenize;
4495
4496 fn ok(src: &str) {
4497 let toks = tokenize(src).unwrap();
4498 parse(&toks, src).unwrap_or_else(|e| panic!("{src}\n{e:?}"));
4499 }
4500
4501 fn wrap(guard: &str) -> String {
4502 format!(
4503 "commons t {{\n fn f(n: Int, flag: Bool) -> Int {{\n match n {{\n n if {guard} => n,\n _ => 0,\n }}\n }}\n}}\n"
4504 )
4505 }
4506
4507 #[test]
4508 fn parenthesised_guards_parse() {
4509 ok(&wrap("(flag)"));
4510 ok(&wrap("(n > 0 && flag)"));
4511 ok(&wrap("(n > 0) && flag"));
4512 ok(&wrap("!(flag)"));
4513 ok(&wrap("()"));
4514 }
4515
4516 #[test]
4517 fn lambda_still_parses_when_parenthesised_in_guard() {
4518 ok(&wrap("pred(((x: Int) => x > 0))"));
4519 }
4520}