1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{
11 Token, TokenKind, comment_body, comment_is_legacy, doc_block_content, doc_fence_len,
12 doc_line_is_legacy, doc_line_text, has_blank_line_between, legacy_comment_error,
13 legacy_doc_line_error,
14};
15use crate::span::Span;
16mod declarations;
17mod expressions;
18mod statements;
19mod types;
20
21#[derive(Debug, Default)]
30struct TriviaTable {
31 leading: Vec<Vec<Comment>>,
37 trailing: Vec<Option<String>>,
42 epilogue: Vec<Comment>,
46 legacy: Vec<CompileError>,
52}
53
54impl TriviaTable {
55 fn take_leading(&mut self, index: usize) -> Vec<Comment> {
56 match self.leading.get_mut(index) {
57 Some(v) => std::mem::take(v),
58 None => Vec::new(),
59 }
60 }
61
62 fn take_trailing(&mut self, index: usize) -> Option<String> {
63 self.trailing.get_mut(index).and_then(|s| s.take())
64 }
65
66 fn take_epilogue(&mut self) -> Vec<Comment> {
67 std::mem::take(&mut self.epilogue)
68 }
69
70 fn is_fully_drained(&self) -> bool {
87 self.leading.iter().all(Vec::is_empty)
88 && self.trailing.iter().all(Option::is_none)
89 && self.epilogue.is_empty()
90 }
91
92 fn epilogue_is_empty(&self) -> bool {
100 self.epilogue.is_empty()
101 }
102}
103
104fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
115 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
116 let mut table = TriviaTable::default();
117 let mut pending_leading: Vec<Comment> = Vec::new();
118 let mut last_content_end: Option<usize> = None;
119 let mut last_leading_end: Option<usize> = None;
126 for tok in tokens {
127 if !pending_leading.is_empty()
128 && let Some(end) = last_leading_end
129 && source[end..tok.span.start].matches('\n').count() >= 2
130 {
131 pending_leading.push(Comment::Blank);
132 }
133 if tok.kind == TokenKind::DocLine && doc_line_is_legacy(source, tok.span) {
134 table.legacy.push(legacy_doc_line_error(source, tok.span));
135 }
136 if tok.kind == TokenKind::Comment {
137 if comment_is_legacy(source, tok.span) {
138 table.legacy.push(legacy_comment_error(tok.span));
139 }
140 let body = comment_body(source, tok.span).into_owned();
141 if pending_leading.is_empty()
152 && filtered.last().is_some_and(|t| {
153 !matches!(
154 t.kind,
155 TokenKind::DocBlock | TokenKind::DocLine | TokenKind::LBrace
156 )
157 })
158 && let Some(prev_end) = last_content_end
159 && !source[prev_end..tok.span.start].contains('\n')
160 {
161 let last_idx = filtered.len() - 1;
162 if table.trailing[last_idx].is_none() {
165 table.trailing[last_idx] = Some(body);
166 continue;
167 }
168 }
169 let own_line = !pending_leading.is_empty()
173 || filtered
174 .last()
175 .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
176 || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
177 pending_leading.push(Comment::Line(body));
178 last_leading_end = own_line.then_some(tok.span.end);
179 continue;
180 }
181 filtered.push(*tok);
182 table.leading.push(std::mem::take(&mut pending_leading));
183 table.trailing.push(None);
184 last_content_end = Some(tok.span.end);
185 last_leading_end = None;
186 }
187 table.epilogue = pending_leading;
188 (filtered, table)
189}
190
191pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
197 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
198}
199
200pub fn parse_with_warnings(
203 tokens: &[Token],
204 source: &str,
205) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
206 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
207 match unit {
208 SourceUnit::Commons(c) => Ok((c, warnings)),
209 SourceUnit::Context(ctx) => Err(vec![
210 CompileError::new(
211 "bynk.parse.unexpected_context",
212 ctx.span,
213 "expected a `commons` declaration but found a `context` declaration",
214 )
215 .with_note(
216 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
217 ),
218 ]),
219 SourceUnit::Suite(t) => Err(vec![
220 CompileError::new(
221 "bynk.parse.unexpected_suite",
222 t.span,
223 "expected a `commons` declaration but found a `suite` declaration",
224 )
225 .with_note(
226 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
227 ),
228 ]),
229 SourceUnit::Adapter(a) => Err(vec![
230 CompileError::new(
231 "bynk.parse.unexpected_adapter",
232 a.span,
233 "expected a `commons` declaration but found an `adapter` declaration",
234 )
235 .with_note(
236 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
237 ),
238 ]),
239 }
240}
241
242pub fn parse_unit_with_recovery(
257 tokens: &[Token],
258 source: &str,
259) -> (Option<SourceUnit>, Vec<CompileError>) {
260 let (units, errors) = parse_units_with_recovery(tokens, source);
261 (units.into_iter().next(), errors)
262}
263
264pub fn parse_units_with_recovery(
269 tokens: &[Token],
270 source: &str,
271) -> (Vec<SourceUnit>, Vec<CompileError>) {
272 let recovered = parse_units_recovering(tokens, source);
273 (recovered.units, recovered.errors)
274}
275
276#[derive(Debug)]
279pub struct Recovered {
280 pub units: Vec<SourceUnit>,
281 pub errors: Vec<CompileError>,
282 pub broken_decl_names: Vec<String>,
285}
286
287pub fn merge_syntax_errors(
294 strict: Vec<CompileError>,
295 recovered: Vec<CompileError>,
296) -> Vec<CompileError> {
297 let mut out = strict;
298 for e in recovered {
299 if !out.iter().any(|o| o.span.start == e.span.start) {
300 out.push(e);
301 }
302 }
303 out
304}
305
306pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
309 parse_units_recovering_from(tokens, source, &mut 0)
310}
311
312pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
318 let (filtered, trivia) = split_trivia(tokens, source);
319 let mut warnings = Vec::new();
320 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
321 p.recover_mode = true;
322 p.next_expr_id = *next_id;
323 let mut units = Vec::new();
324 loop {
325 match p.parse_unit() {
326 Ok(u) => units.push(u),
327 Err(e) => {
328 p.recovered_errors.push(e);
329 break;
330 }
331 }
332 if p.peek().is_none() {
338 break;
339 }
340 }
341 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
342 *next_id = p.next_expr_id;
343 let mut all_errors = p.recovered_errors;
344 all_errors.append(&mut p.doc_errors);
345 all_errors.append(&mut warnings);
346 Recovered {
347 units,
348 errors: all_errors,
349 broken_decl_names,
350 }
351}
352
353pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
357 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
358}
359
360pub fn parse_unit_with_warnings(
363 tokens: &[Token],
364 source: &str,
365) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
366 parse_unit_with_warnings_from(tokens, source, &mut 0)
367}
368
369pub fn parse_unit_with_warnings_from(
383 tokens: &[Token],
384 source: &str,
385 next_id: &mut u32,
386) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
387 let (filtered, trivia) = split_trivia(tokens, source);
388 let mut warnings = Vec::new();
389 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
390 p.next_expr_id = *next_id;
391 let result = match p.parse_unit() {
392 Ok(u) => {
393 if let Some(extra) = p.peek() {
394 Err(vec![
395 CompileError::new(
396 "bynk.parse.extra_tokens",
397 extra.span,
398 "unexpected token after top-level declaration",
399 )
400 .with_note(
401 "a `.bynk` file contains exactly one `commons` or `context` declaration",
402 ),
403 ])
404 } else {
405 Ok(u)
406 }
407 }
408 Err(e) => Err(vec![e]),
409 };
410 let result = match (result, std::mem::take(&mut p.doc_errors)) {
413 (r, docs) if docs.is_empty() => r,
414 (Ok(_), docs) => Err(docs),
415 (Err(mut errs), mut docs) => {
416 errs.append(&mut docs);
417 Err(errs)
418 }
419 };
420 *next_id = p.next_expr_id;
421 match result {
424 Ok(u) => {
425 debug_assert!(
428 p.trivia.epilogue_is_empty(),
429 "a file-trailing comment was left undrained after a successful parse"
430 );
431 Ok((u, warnings))
432 }
433 Err(mut errs) => {
434 errs.append(&mut warnings);
435 Err(errs)
436 }
437 }
438}
439
440pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
449 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
450}
451
452pub fn parse_units_with_warnings(
458 tokens: &[Token],
459 source: &str,
460) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
461 parse_units_with_drain_check(tokens, source)
462 .map(|(units, warnings, _drained)| (units, warnings))
463}
464
465pub fn parse_units_with_warnings_from(
472 tokens: &[Token],
473 source: &str,
474 next_id: &mut u32,
475) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
476 parse_units_with_drain_check_from(tokens, source, next_id)
477 .map(|(units, warnings, _drained)| (units, warnings))
478}
479
480pub fn parse_units_with_drain_check(
491 tokens: &[Token],
492 source: &str,
493) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
494 parse_units_with_drain_check_from(tokens, source, &mut 0)
495}
496
497pub fn parse_units_with_drain_check_from(
500 tokens: &[Token],
501 source: &str,
502 next_id: &mut u32,
503) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
504 parse_units_drain(tokens, source, next_id, false)
505}
506
507pub fn parse_units_migrating(
512 tokens: &[Token],
513 source: &str,
514) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
515 parse_units_drain(tokens, source, &mut 0, true)
516}
517
518fn parse_units_drain(
519 tokens: &[Token],
520 source: &str,
521 next_id: &mut u32,
522 migrate: bool,
523) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
524 let (filtered, trivia) = split_trivia(tokens, source);
525 let mut warnings = Vec::new();
526 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
527 if migrate {
528 p.doc_errors.clear();
529 }
530 p.next_expr_id = *next_id;
531 let mut units = Vec::new();
532 let mut errors: Vec<CompileError> = Vec::new();
533 while p.peek().is_some() {
534 match p.parse_unit() {
535 Ok(u) => units.push(u),
536 Err(e) => {
537 errors.push(e);
538 break;
539 }
540 }
541 }
542 errors.append(&mut p.doc_errors);
544 *next_id = p.next_expr_id;
545 let eof = p.eof_span();
546 let fully_drained = p.trivia.is_fully_drained();
547 if !errors.is_empty() {
550 errors.append(&mut warnings);
551 return Err(errors);
552 }
553 if units.is_empty() {
554 return Err(vec![CompileError::new(
555 "bynk.parse.unexpected_eof",
556 eof,
557 "expected `commons`, `context`, or `suite` to start the file, found end of file",
558 )]);
559 }
560 debug_assert!(
568 p.trivia.epilogue_is_empty(),
569 "a file-trailing comment was left undrained after a successful parse"
570 );
571 Ok((units, warnings, fully_drained))
572}
573
574enum SignedNumLit {
577 Int(IntBound),
578 Float(FloatBound),
579}
580
581struct Parser<'a> {
582 tokens: &'a [Token],
583 source: &'a str,
584 pos: usize,
585 warnings: &'a mut Vec<CompileError>,
588 recover_mode: bool,
594 recovered_errors: Vec<CompileError>,
597 pub(crate) item_start: Option<usize>,
601 broken_decl_names: Vec<String>,
606 trivia: TriviaTable,
609 depth: usize,
616 no_record_literal: bool,
625 brace_depth: usize,
636 item_loop_baseline: Vec<usize>,
643 next_expr_id: u32,
647 doc_errors: Vec<CompileError>,
653 doc_probe: bool,
657}
658
659impl<'a> Parser<'a> {
660 fn new(
661 tokens: &'a [Token],
662 source: &'a str,
663 mut trivia: TriviaTable,
664 warnings: &'a mut Vec<CompileError>,
665 ) -> Self {
666 let doc_errors = std::mem::take(&mut trivia.legacy);
667 Self {
668 tokens,
669 source,
670 pos: 0,
671 warnings,
672 recover_mode: false,
673 recovered_errors: Vec::new(),
674 item_start: None,
675 broken_decl_names: Vec::new(),
676 trivia,
677 depth: 0,
678 no_record_literal: false,
679 brace_depth: 0,
680 item_loop_baseline: Vec::new(),
681 next_expr_id: 0,
682 doc_errors,
683 doc_probe: false,
684 }
685 }
686
687 fn alloc_expr_id(&mut self) -> ExprId {
692 let id = ExprId(self.next_expr_id);
693 self.next_expr_id += 1;
694 id
695 }
696
697 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
705 self.depth += 1;
706 if self.depth > crate::MAX_NESTING_DEPTH {
707 self.depth -= 1;
708 let span = self
709 .peek()
710 .map(|t| t.span)
711 .unwrap_or_else(|| self.eof_span());
712 return Err(self.nesting_too_deep(span, what));
713 }
714 Ok(())
715 }
716
717 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
720 CompileError::new(
721 "bynk.parse.nesting_too_deep",
722 span,
723 format!(
724 "{what} nests more than {} levels deep",
725 crate::MAX_NESTING_DEPTH
726 ),
727 )
728 .with_note(
729 "deeply nested source is rejected to keep the parser from overflowing its \
730 stack and aborting; flatten or split the construct",
731 )
732 }
733
734 fn expression_too_long(&self, span: Span) -> CompileError {
740 CompileError::new(
741 "bynk.parse.nesting_too_deep",
742 span,
743 format!(
744 "this expression is more than {} levels deep",
745 crate::MAX_NESTING_DEPTH
746 ),
747 )
748 .with_note(
749 "a long operator or member chain is rejected to keep the compiler from overflowing \
750 its stack; split it across `let` bindings, or reduce a sequence with \
751 `.sum()`/`.fold(...)`",
752 )
753 }
754
755 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
775 self.depth += 1;
776 *folds += 1;
777 if self.depth > crate::MAX_NESTING_DEPTH {
778 self.depth -= *folds;
779 *folds = 0;
780 return Err(self.expression_too_long(span));
781 }
782 Ok(())
783 }
784
785 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
794 self.depth += 1;
795 if self.depth > crate::MAX_NESTING_DEPTH {
796 return Err(self.expression_too_long(span));
797 }
798 Ok(())
799 }
800
801 fn take_leading_trivia(&mut self) -> Vec<Comment> {
811 let mut comments = self.take_unit_leading_trivia();
812 comments.retain(|c| *c != Comment::Blank);
813 comments
814 }
815
816 fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
819 self.trivia.take_leading(self.pos)
820 }
821
822 fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
825 self.trivia.take_epilogue()
826 }
827
828 fn take_trailing_trivia(&mut self) -> Option<String> {
832 if self.pos == 0 {
833 return None;
834 }
835 self.trivia.take_trailing(self.pos - 1)
836 }
837
838 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
842 if self.recover_mode {
843 self.recovered_errors.push(e);
844 if let Some(start) = self.item_start.take() {
845 self.broken_decl_names.extend(self.decl_names_at(start));
846 }
847 let before = self.pos;
848 self.recover_to_top_item();
849 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
862 let closes_body = baseline > 0
863 && self.brace_depth == baseline
864 && self.peek_kind() == Some(TokenKind::RBrace);
865 if self.pos == before && !closes_body {
866 self.bump();
867 self.recover_to_top_item();
872 }
873 Ok(())
874 } else {
875 Err(e)
876 }
877 }
878
879 fn decl_names_at(&self, start: usize) -> Vec<String> {
884 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
885 let ident_at = |i: usize| {
886 self.tokens
887 .get(i)
888 .filter(|t| t.kind == TokenKind::Ident)
889 .map(|t| self.slice(t.span).to_string())
890 };
891 match kind_at(start) {
892 Some(
893 TokenKind::Type
894 | TokenKind::Event
895 | TokenKind::Capability
896 | TokenKind::Service
897 | TokenKind::Agent
898 | TokenKind::Actor,
899 ) => ident_at(start + 1).into_iter().collect(),
900 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
901 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
904 Some(method) => vec![format!("{owner}.{method}")],
905 None => Vec::new(),
906 },
907 (Some(name), _) => vec![name],
908 (None, _) => Vec::new(),
909 },
910 _ => Vec::new(),
911 }
912 }
913
914 fn recover_to_top_item(&mut self) {
927 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
928 while let Some(t) = self.peek() {
929 match t.kind {
930 TokenKind::RBrace if self.brace_depth == baseline => return,
931 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
932 _ => {
933 self.bump();
934 }
935 }
936 }
937 }
938
939 fn enter_item_loop(&mut self) {
946 self.item_loop_baseline.push(self.brace_depth);
947 }
948
949 fn exit_item_loop(&mut self) {
951 self.item_loop_baseline.pop();
952 }
953
954 fn peek(&self) -> Option<Token> {
955 self.tokens.get(self.pos).copied()
956 }
957
958 fn peek_kind(&self) -> Option<TokenKind> {
959 self.peek().map(|t| t.kind)
960 }
961
962 fn nth(&self, n: usize) -> Option<Token> {
964 self.tokens.get(self.pos + n).copied()
965 }
966
967 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
968 self.nth(n).map(|t| t.kind)
969 }
970
971 fn nth_text(&self, n: usize) -> &'a str {
973 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
974 }
975
976 fn prev_span(&self) -> Span {
979 self.tokens
980 .get(self.pos.wrapping_sub(1))
981 .or_else(|| self.peek_ref())
982 .map(|t| t.span)
983 .unwrap_or_default()
984 }
985
986 fn peek_ref(&self) -> Option<&Token> {
987 self.tokens.get(self.pos)
988 }
989
990 fn bump(&mut self) -> Option<Token> {
991 let t = self.peek();
992 if let Some(t) = t {
993 match t.kind {
994 TokenKind::LBrace => self.brace_depth += 1,
995 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
996 _ => {}
997 }
998 self.pos += 1;
999 }
1000 t
1001 }
1002
1003 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
1004 if self.peek_kind() == Some(kind) {
1005 self.bump()
1006 } else {
1007 None
1008 }
1009 }
1010
1011 fn slice(&self, span: Span) -> &'a str {
1012 &self.source[span.range()]
1013 }
1014
1015 fn next_token_on_new_line(&self, prev: Span) -> bool {
1020 match self.peek() {
1021 Some(t) if prev.end <= t.span.start => {
1022 self.source[prev.end..t.span.start].contains('\n')
1023 }
1024 _ => false,
1025 }
1026 }
1027
1028 fn eof_span(&self) -> Span {
1033 let end = self.source.len();
1034 let start = (0..end)
1035 .rev()
1036 .find(|&i| self.source.is_char_boundary(i))
1037 .unwrap_or(0);
1038 Span::new(start, end)
1039 }
1040
1041 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1042 match self.peek() {
1043 Some(t) if t.kind == kind => {
1044 self.bump();
1045 Ok(t)
1046 }
1047 Some(t) => Err(CompileError::new(
1048 "bynk.parse.expected_token",
1049 t.span,
1050 format!(
1051 "expected {} {ctx}, found {}",
1052 kind.describe(),
1053 t.kind.describe()
1054 ),
1055 )),
1056 None => Err(CompileError::new(
1057 "bynk.parse.unexpected_eof",
1058 self.eof_span(),
1059 format!("expected {} {ctx}, found end of file", kind.describe()),
1060 )),
1061 }
1062 }
1063
1064 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1065 match self.peek() {
1066 Some(t) if t.kind == TokenKind::Ident => {
1067 self.bump();
1068 Ok(Ident {
1069 name: self.slice(t.span).to_string(),
1070 span: t.span,
1071 })
1072 }
1073 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1088 self.bump();
1089 Ok(Ident {
1090 name: self.slice(t.span).to_string(),
1091 span: t.span,
1092 })
1093 }
1094 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1095 "bynk.parse.reserved_keyword",
1096 t.span,
1097 format!(
1098 "expected identifier {ctx}, but `{}` is a reserved keyword",
1099 self.slice(t.span)
1100 ),
1101 )
1102 .with_note("rename the identifier to something that is not a keyword")),
1103 Some(t) => Err(CompileError::new(
1104 "bynk.parse.expected_token",
1105 t.span,
1106 format!("expected identifier {ctx}, found {}", t.kind.describe()),
1107 )),
1108 None => Err(CompileError::new(
1109 "bynk.parse.unexpected_eof",
1110 self.eof_span(),
1111 format!("expected identifier {ctx}, found end of file"),
1112 )),
1113 }
1114 }
1115
1116 fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1134 let first = self.take_one_doc()?;
1135 let mut docs = vec![first.clone()];
1136 while let Some(next) = self.peek()
1137 && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1138 && !has_blank_line_between(self.source, docs[docs.len() - 1].1.end, next.span.start)
1139 {
1140 docs.push(self.take_one_doc().expect("a doc token was peeked"));
1141 }
1142 if docs.len() > 1 && !self.doc_probe {
1143 self.doc_errors.push(multiple_docs(self.source, &docs));
1144 }
1145 Some(first)
1146 }
1147
1148 fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1150 match self.peek_kind()? {
1151 TokenKind::DocBlock => {
1152 let t = self.bump().unwrap();
1153 let body = doc_block_content(self.source, t.span);
1154 if !self.doc_probe && doc_content_is_code(&body) {
1155 self.doc_errors.push(doc_block_contains_code(t.span));
1156 }
1157 Some((Doc::new(body, DocForm::Block), t.span))
1158 }
1159 TokenKind::DocLine => {
1160 let t = self.bump().unwrap();
1161 let text = doc_line_text(self.source, t.span);
1162 Some((Doc::new(text, DocForm::Lines), t.span))
1163 }
1164 _ => None,
1165 }
1166 }
1167
1168 fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1181 self.collect_lead(Self::take_unit_leading_trivia)
1182 }
1183
1184 fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1187 self.collect_lead(Self::take_leading_trivia)
1188 }
1189
1190 fn collect_lead(
1191 &mut self,
1192 take: fn(&mut Self) -> Vec<Comment>,
1193 ) -> (Vec<Comment>, Option<DocLead>) {
1194 let mut leading = take(self);
1195 let doc = self.take_doc_block().map(|(content, span)| DocLead {
1196 content,
1197 span,
1198 at: leading.len(),
1199 });
1200 if doc.is_some() {
1201 leading.extend(take(self));
1202 }
1203 (leading, doc)
1204 }
1205
1206 fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1215 let (mut leading, doc) = self.collect_body_item_lead();
1216 let Some(doc) = doc else {
1217 return (leading, None);
1218 };
1219 match self.peek() {
1220 Some(t) if t.kind != close => {
1221 let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1222 (leading, doc)
1223 }
1224 _ => {
1225 self.warnings.push(CompileError::new(
1226 "bynk.parse.orphan_doc_block",
1227 doc.span,
1228 "documentation has no following member to attach to",
1229 ));
1230 keep_orphan(&mut leading, doc);
1231 (leading, None)
1232 }
1233 }
1234 }
1235
1236 fn peek_past_docs(&self) -> Option<TokenKind> {
1240 self.tokens[self.pos.min(self.tokens.len())..]
1241 .iter()
1242 .map(|t| t.kind)
1243 .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1244 }
1245
1246 fn finalize_doc(
1250 &mut self,
1251 doc: Option<DocLead>,
1252 next_span: Span,
1253 leading: &mut Vec<Comment>,
1254 ) -> Option<Doc> {
1255 let doc = doc?;
1256 let doc_span = doc.span;
1257 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1259 self.warnings.push(
1260 CompileError::new(
1261 "bynk.parse.orphan_doc_block",
1262 doc_span,
1263 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1264 )
1265 .with_note(
1266 "remove the blank line to attach the doc to the next declaration, \
1267 or remove the doc block if it is not meant to document anything",
1268 ),
1269 );
1270 keep_orphan(leading, doc);
1271 return None;
1272 }
1273 Some(doc.content)
1274 }
1275}
1276
1277pub(crate) struct DocLead {
1281 pub(crate) content: Doc,
1282 pub(crate) span: Span,
1283 pub(crate) at: usize,
1284}
1285
1286fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1290 let at = doc.at.min(leading.len());
1291 leading.insert(at, Comment::OrphanDoc(doc.content));
1292}
1293
1294fn multiple_docs(source: &str, docs: &[(Doc, Span)]) -> CompileError {
1299 let (_, first) = docs[0];
1300 let (_, last) = docs[docs.len() - 1];
1301 let mut err = CompileError::new(
1302 "bynk.parse.multiple_docs",
1303 docs[1].1,
1304 "this declaration or member already has a doc; a target has one doc",
1305 )
1306 .with_label(first, "the first doc is here")
1307 .with_note(
1308 "a doc of more than one line is a block: a `---` line, the text, and another `---` line",
1309 );
1310 let one_liners = docs
1311 .iter()
1312 .all(|(d, span)| d.form == DocForm::Lines && !doc_line_is_legacy(source, *span));
1313 if one_liners {
1314 let line_start = source[..first.start].rfind('\n').map_or(0, |i| i + 1);
1315 let indent = &source[line_start..first.start];
1316 let text = docs
1317 .iter()
1318 .map(|(d, _)| d.as_str())
1319 .collect::<Vec<_>>()
1320 .join("\n");
1321 let fence = "-".repeat(doc_fence_len(&text));
1322 let mut block = fence.clone();
1323 for line in text.lines() {
1324 block.push('\n');
1325 if !line.is_empty() {
1326 block.push_str(indent);
1327 block.push_str(line);
1328 }
1329 }
1330 block.push('\n');
1331 block.push_str(indent);
1332 block.push_str(&fence);
1333 let end = source[..last.end].trim_end_matches(['\n', '\r']).len();
1334 err = err.with_suggestion(
1335 "join the docs into one `---` block",
1336 vec![(Span::new_in(first.file, first.start, end), block)],
1337 crate::error::Applicability::MachineApplicable,
1338 );
1339 }
1340 err
1341}
1342
1343fn doc_block_contains_code(span: Span) -> CompileError {
1346 CompileError::new(
1347 "bynk.parse.doc_block_contains_code",
1348 span,
1349 "this documentation block contains declarations, so they are not compiled",
1350 )
1351 .with_note(
1352 "a line of three or more dashes opens or closes a doc block, so two dash \
1353 dividers turn the code between them into documentation; for a divider use a \
1354 line comment with text, such as `-- Helpers --`, and to show code in a doc \
1355 block put it in a Markdown code fence (```)",
1356 )
1357}
1358
1359const DECLARATION_KEYWORDS: &[TokenKind] = &[
1363 TokenKind::Type,
1364 TokenKind::Fn,
1365 TokenKind::Capability,
1366 TokenKind::Provides,
1367 TokenKind::Service,
1368 TokenKind::Agent,
1369 TokenKind::Actor,
1370 TokenKind::Messages,
1371 TokenKind::Event,
1372 TokenKind::Asset,
1373 TokenKind::Embed,
1374 TokenKind::Uses,
1375 TokenKind::Consumes,
1376 TokenKind::Exports,
1377 TokenKind::Binding,
1378];
1379
1380fn doc_content_is_code(content: &str) -> bool {
1395 doc_content_is_code_at(content, 0)
1396}
1397
1398const DOC_PROBE_MAX_DEPTH: usize = 4;
1402
1403fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1404 let stripped = strip_code_fences(content);
1405 let tokens = match crate::lexer::tokenize(&stripped) {
1406 Ok(tokens) => tokens,
1407 Err(_) => {
1408 let unmarked = blank_marker_lines(&stripped);
1411 if unmarked == stripped {
1412 return false;
1413 }
1414 return doc_content_is_code_at(&unmarked, depth);
1415 }
1416 };
1417 if depth < DOC_PROBE_MAX_DEPTH
1418 && tokens.iter().any(|t| {
1419 t.kind == TokenKind::DocBlock
1420 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1421 })
1422 {
1423 return true;
1424 }
1425 if !tokens
1426 .iter()
1427 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1428 {
1429 return false;
1430 }
1431 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1432 let mut warnings = Vec::new();
1433 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1434 p.doc_probe = true;
1435 let at = Span::new(0, 0);
1436 let name = QualifiedName {
1437 parts: Vec::new(),
1438 span: at,
1439 };
1440 match p.parse_adapter_body(at, name, None, false) {
1441 Ok(a) => {
1442 p.peek().is_none()
1443 && (!a.items.is_empty()
1444 || !a.uses.is_empty()
1445 || !a.consumes.is_empty()
1446 || !a.exports.is_empty()
1447 || a.binding.is_some())
1448 }
1449 Err(_) => false,
1450 }
1451}
1452
1453fn blank_marker_lines(content: &str) -> String {
1456 let mut out = String::with_capacity(content.len());
1457 for line in content.lines() {
1458 let trimmed = line.trim();
1459 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1460 out.push_str(line);
1461 }
1462 out.push('\n');
1463 }
1464 out
1465}
1466
1467fn strip_code_fences(content: &str) -> String {
1472 let mut out = String::with_capacity(content.len());
1473 let mut open: Option<(char, usize)> = None;
1474 for line in content.lines() {
1475 let trimmed = line.trim();
1476 let fence = ['`', '~'].into_iter().find_map(|c| {
1477 let n = trimmed.chars().take_while(|&x| x == c).count();
1478 (n >= 3).then_some((c, n))
1479 });
1480 match (open, fence) {
1481 (None, Some(f)) => open = Some(f),
1482 (Some((c, n)), Some((fc, fnum)))
1483 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1484 {
1485 open = None;
1486 }
1487 (None, None) => out.push_str(line),
1488 (Some(_), _) => {}
1489 }
1490 out.push('\n');
1491 }
1492 out
1493}
1494
1495fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1498 let bytes = lexeme.as_bytes();
1499 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
1500 let inner = &lexeme[1..lexeme.len() - 1];
1501 let mut out = String::with_capacity(inner.len());
1502 let mut chars = inner.chars();
1503 while let Some(c) = chars.next() {
1504 if c == '\\' {
1505 match chars.next() {
1506 Some('n') => out.push('\n'),
1507 Some('t') => out.push('\t'),
1508 Some('"') => out.push('"'),
1509 Some('\\') => out.push('\\'),
1510 other => {
1511 return Err(CompileError::new(
1512 "bynk.lex.bad_escape",
1513 span,
1514 format!(
1515 "invalid escape sequence `\\{}` in string literal",
1516 other.map(|c| c.to_string()).unwrap_or_default()
1517 ),
1518 )
1519 .with_note("supported escapes: \\n \\t \\\" \\\\"));
1520 }
1521 }
1522 } else {
1523 out.push(c);
1524 }
1525 }
1526 Ok(out)
1527}
1528
1529fn is_reserved_keyword(kind: TokenKind) -> bool {
1530 use TokenKind::*;
1531 matches!(
1532 kind,
1533 Commons
1534 | Type
1535 | Fn
1536 | Where
1537 | True
1538 | False
1539 | Int
1540 | String
1541 | Bool
1542 | Let
1543 | If
1544 | Else
1545 | Ok
1546 | Err
1547 | Result
1548 | ValidationError
1549 | Enum
1550 | Match
1551 | Option
1552 | Record
1553 | Self_
1554 | Some
1555 | None
1556 | Is
1557 | Opaque
1558 | Uses
1559 | Context
1560 | Consumes
1561 | Exports
1562 | Transparent
1563 | Agent
1564 | As
1565 | Capability
1566 | Effect
1567 | Do
1568 | Given
1569 | On
1570 | Http
1571 | Provides
1572 | Stub
1573 | Service
1574 | Actor
1575 | By
1576 | Expect
1577 | Suite
1578 | Case
1579 | Float
1580 | Duration
1581 | Instant
1582 | Bytes
1583 | JsonError
1584 | Property
1585 | Adapter
1586 | Binding
1587 | Cron
1588 | Queue
1589 | From
1590 | Protocol
1591 | Invariant
1592 | Implies
1593 | Requires
1594 | Ensures
1595 | Transition
1596 )
1597}
1598
1599fn is_item_start(kind: TokenKind) -> bool {
1610 use TokenKind::*;
1611 matches!(
1612 kind,
1613 Commons | Context | Adapter | Suite
1615 | Type | Fn | Messages | Event | Asset | Embed | Uses
1617 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1619 | Binding
1621 | Stub | Case | Property
1623 )
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628 use super::*;
1629 use crate::lexer::tokenize;
1630
1631 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1632 let toks = tokenize(src).map_err(|e| vec![e])?;
1633 parse(&toks, src)
1634 }
1635
1636 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1637 let toks = match tokenize(src) {
1638 Ok(t) => t,
1639 Err(e) => return (None, vec![e]),
1640 };
1641 parse_unit_with_recovery(&toks, src)
1642 }
1643
1644 #[test]
1650 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1651 let src =
1652 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1653 let toks = tokenize(src).unwrap();
1654 let (units, errors) = parse_units_with_recovery(&toks, src);
1655 assert!(errors.is_empty(), "{errors:?}");
1656 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1657 assert!(matches!(units[0], SourceUnit::Commons(_)));
1658 assert!(matches!(units[1], SourceUnit::Suite(_)));
1659
1660 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1662 assert!(errors.is_empty(), "{errors:?}");
1663 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1664 }
1665
1666 #[test]
1673 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1674 let toks = tokenize("").unwrap();
1675 let (units, errors) = parse_units_with_recovery(&toks, "");
1676 assert!(units.is_empty());
1677 assert!(
1678 !errors.is_empty(),
1679 "an empty file must still produce a diagnostic, not silently no units and no error"
1680 );
1681
1682 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1683 assert!(unit.is_none());
1684 assert_eq!(
1685 errors.len(),
1686 unit_errors.len(),
1687 "the singular wrapper must see the same error(s) as the plural entry point"
1688 );
1689 }
1690
1691 #[test]
1698 fn drain_check_reports_expression_interior_comments_as_undrained() {
1699 let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1700 let toks = tokenize(ordinary).unwrap();
1701 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1702 assert!(
1703 drained,
1704 "a declaration-leading comment must be fully drained"
1705 );
1706
1707 let lossy = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
1708 let toks = tokenize(lossy).unwrap();
1709 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1710 assert!(
1711 !drained,
1712 "a comment inside a binop expression must be reported as undrained"
1713 );
1714 }
1715
1716 #[test]
1717 fn eof_span_never_splits_a_multibyte_codepoint() {
1718 for src in [
1723 "commons x {\n // ends with an arrow →",
1724 "agent A {\n key k: String\n // note 🦀",
1725 "commons y {\n type T = é",
1726 ] {
1727 let (_unit, errors) = parse_recover_str(src);
1728 for e in &errors {
1729 assert!(
1730 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1731 "span {:?} splits a codepoint in {src:?}",
1732 e.span,
1733 );
1734 }
1735 }
1736 }
1737
1738 #[test]
1739 fn reserved_contextual_keywords_readable_in_expression_position() {
1740 for kw in ["asset", "case", "embed", "event", "messages", "on", "suite"] {
1750 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1751 let result = parse_str(&src);
1752 assert!(
1753 result.is_ok(),
1754 "a parameter named `{kw}` must be readable in expression position: {:?}",
1755 result.err()
1756 );
1757 }
1758 }
1759
1760 #[test]
1761 fn recovery_skips_garbage_between_decls() {
1762 let src = "commons x {\n\
1765 type A = Int where NonNegative\n\
1766 ??? !!!\n\
1767 type B = String where NonEmpty\n\
1768 }";
1769 let (unit, errors) = parse_recover_str(src);
1770 let unit = unit.expect("recovery should produce a partial AST");
1771 let SourceUnit::Commons(c) = unit else {
1772 panic!("expected commons")
1773 };
1774 let names: Vec<_> = c
1776 .items
1777 .iter()
1778 .map(|i| match i {
1779 CommonsItem::Type(t) => t.name.name.clone(),
1780 _ => panic!("expected only types"),
1781 })
1782 .collect();
1783 assert!(
1784 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1785 "expected both A and B; got {names:?}",
1786 );
1787 assert!(!errors.is_empty(), "expected at least one parse error");
1788 }
1789
1790 #[test]
1791 fn recovery_handles_bad_first_decl_then_good_second() {
1792 let src = "commons x {\n\
1794 type A Int where NonNegative\n\
1795 type B = String where NonEmpty\n\
1796 }";
1797 let (unit, errors) = parse_recover_str(src);
1798 let unit = unit.expect("recovery should produce a partial AST");
1799 let SourceUnit::Commons(c) = unit else {
1800 panic!("expected commons")
1801 };
1802 let names: Vec<_> = c
1803 .items
1804 .iter()
1805 .filter_map(|i| match i {
1806 CommonsItem::Type(t) => Some(t.name.name.clone()),
1807 _ => None,
1808 })
1809 .collect();
1810 assert!(
1811 names.contains(&"B".to_string()),
1812 "B should be parsed after A's failure; got {names:?}"
1813 );
1814 assert!(!errors.is_empty(), "expected at least one parse error");
1815 }
1816
1817 #[test]
1832 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
1833 let src = "commons m {\n fn f() -> Int\n}\n";
1834 let (unit, errors) = parse_recover_str(src);
1835 assert!(unit.is_some(), "recovery should produce a partial AST");
1836 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
1837 assert_eq!(
1838 categories.len(),
1839 1,
1840 "one syntax error, no follow-on: {categories:?}"
1841 );
1842 assert!(
1843 !categories.contains(&"bynk.parse.unexpected_eof"),
1844 "the commons' closing brace must survive: {categories:?}"
1845 );
1846 }
1847
1848 #[test]
1853 fn recovery_skips_a_rejected_item_whole() {
1854 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
1855 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
1856 assert_eq!(
1857 recovered.errors.len(),
1858 1,
1859 "one syntax error, no follow-on: {:?}",
1860 recovered
1861 .errors
1862 .iter()
1863 .map(|e| e.category)
1864 .collect::<Vec<_>>()
1865 );
1866 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
1867 panic!("expected commons")
1868 };
1869 assert!(
1870 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
1871 "the `fn g` after the skipped agent must still parse"
1872 );
1873 assert_eq!(recovered.broken_decl_names, ["Counter"]);
1874 }
1875
1876 #[test]
1877 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1878 let src = "commons m {\n \
1879 fn f() -> Int {\n \
1880 match 1 {\n \
1881 is 1 -> { let z = }\n \
1882 is _ -> 2\n \
1883 }\n \
1884 }\n \
1885 fn g() -> Int { 2 }\n\
1886 }\n";
1887 let (unit, errors) = parse_recover_str(src);
1888 let unit = unit.expect("recovery should produce a partial AST");
1889 let SourceUnit::Commons(c) = unit else {
1890 panic!("expected commons")
1891 };
1892 let names: Vec<_> = c
1893 .items
1894 .iter()
1895 .filter_map(|i| match i {
1896 CommonsItem::Fn(f) => match &f.name {
1897 FnName::Free(id) => Some(id.name.clone()),
1898 _ => None,
1899 },
1900 _ => None,
1901 })
1902 .collect();
1903 assert_eq!(
1904 names,
1905 vec!["g".to_string()],
1906 "g must still be recovered as an item; got {names:?}"
1907 );
1908 assert!(
1909 !errors
1910 .iter()
1911 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1912 "the outer body's own closing brace must not be mistaken for \
1913 end-of-file: {errors:?}"
1914 );
1915 }
1916
1917 #[test]
1918 fn doc_block_attaches_to_type() {
1919 let c =
1920 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1921 .unwrap();
1922 let CommonsItem::Type(t) = &c.items[0] else {
1923 panic!()
1924 };
1925 assert!(t.documentation.is_some());
1926 assert!(
1927 t.documentation
1928 .as_ref()
1929 .unwrap()
1930 .contains("A descriptive doc.")
1931 );
1932 }
1933
1934 #[test]
1935 fn interpolated_string_parses_into_parts() {
1936 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1938 .unwrap();
1939 let CommonsItem::Fn(f) = &c.items[0] else {
1940 panic!("expected fn")
1941 };
1942 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1943 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1944 };
1945 assert_eq!(parts.len(), 3);
1946 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1947 assert!(
1948 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1949 );
1950 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1951 }
1952
1953 #[test]
1954 fn interpolated_hole_parses_a_full_expression() {
1955 let c =
1957 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1958 .unwrap();
1959 let CommonsItem::Fn(f) = &c.items[0] else {
1960 panic!("expected fn")
1961 };
1962 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1963 panic!("expected InterpStr")
1964 };
1965 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1966 }
1967
1968 #[test]
1969 fn empty_interpolation_hole_is_rejected() {
1970 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1971 assert!(
1972 errs.iter()
1973 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1974 "expected empty_interpolation; got {errs:?}"
1975 );
1976 }
1977
1978 #[test]
1979 fn interpolation_hole_lex_error_span_is_rebased() {
1980 let cases = [
1986 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1988 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1990 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1993 ];
1994 for src in cases {
1995 let errs = parse_str(src).unwrap_err();
1996 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1997 for e in &errs {
1998 assert!(
1999 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
2000 "span {:?} splits a codepoint in {src:?}",
2001 e.span,
2002 );
2003 let hole_start = src.find("\\(").expect("case has a hole") + 2;
2006 assert!(
2007 e.span.start >= hole_start,
2008 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2009 e.span,
2010 );
2011 }
2012 }
2013 }
2014
2015 #[test]
2016 fn fragment_form_parses() {
2017 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2018 assert_eq!(c.form, CommonsForm::Fragment);
2019 assert_eq!(c.items.len(), 1);
2020 }
2021
2022 #[test]
2023 fn uses_parses() {
2024 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2025 assert_eq!(c.uses.len(), 1);
2026 assert_eq!(c.uses[0].target.joined(), "other.lib");
2027 }
2028
2029 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2030 let toks = tokenize(src).map_err(|e| vec![e])?;
2031 parse_unit(&toks, src)
2032 }
2033
2034 #[test]
2035 fn minimal_context_parses() {
2036 let u = parse_unit_str("context commerce.orders {}").unwrap();
2037 let SourceUnit::Context(c) = u else {
2038 panic!("expected context");
2039 };
2040 assert_eq!(c.name.joined(), "commerce.orders");
2041 assert!(c.items.is_empty());
2042 }
2043
2044 #[test]
2045 fn context_consumes_and_exports_parse() {
2046 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}";
2047 let u = parse_unit_str(src).unwrap();
2048 let SourceUnit::Context(c) = u else { panic!() };
2049 assert_eq!(c.uses.len(), 1);
2050 assert_eq!(c.consumes.len(), 1);
2051 assert_eq!(c.exports.len(), 2);
2052 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2053 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2054 }
2055
2056 #[test]
2057 fn context_fragment_form_parses() {
2058 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2059 let u = parse_unit_str(src).unwrap();
2060 let SourceUnit::Context(c) = u else { panic!() };
2061 assert_eq!(c.form, CommonsForm::Fragment);
2062 assert_eq!(c.uses.len(), 1);
2063 assert_eq!(c.consumes.len(), 1);
2064 assert_eq!(c.exports.len(), 1);
2065 }
2066
2067 #[test]
2068 fn opaque_type_parses() {
2069 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2070 let CommonsItem::Type(t) = &c.items[0] else {
2071 panic!()
2072 };
2073 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2074 }
2075
2076 #[test]
2077 fn empty_commons() {
2078 let c = parse_str("commons fitness.units {}").unwrap();
2079 assert_eq!(c.name.joined(), "fitness.units");
2080 assert!(c.items.is_empty());
2081 }
2082
2083 #[test]
2084 fn one_type_decl() {
2085 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2086 assert_eq!(c.items.len(), 1);
2087 let CommonsItem::Type(t) = &c.items[0] else {
2088 panic!()
2089 };
2090 assert_eq!(t.name.name, "Metres");
2091 match &t.body {
2092 TypeBody::Refined {
2093 base, refinement, ..
2094 } => {
2095 assert_eq!(*base, BaseType::Int);
2096 assert!(refinement.is_some());
2097 }
2098 _ => panic!("expected refined body"),
2099 }
2100 }
2101
2102 #[test]
2103 fn function_decl() {
2104 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2105 let CommonsItem::Fn(f) = &c.items[0] else {
2106 panic!()
2107 };
2108 assert_eq!(f.name.ident().name, "add");
2109 assert_eq!(f.params.len(), 2);
2110 }
2111
2112 #[test]
2113 fn chained_comparison_is_error() {
2114 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2115 .unwrap_err();
2116 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2117 }
2118
2119 #[test]
2120 fn chained_equality_is_error() {
2121 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2122 .unwrap_err();
2123 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2124 }
2125
2126 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2135 std::thread::Builder::new()
2136 .stack_size(64 * 1024 * 1024)
2137 .spawn(f)
2138 .unwrap()
2139 .join()
2140 .unwrap()
2141 }
2142
2143 #[test]
2144 fn deeply_nested_parens_are_bounded_not_overflowed() {
2145 let errs = on_big_stack(|| {
2151 let depth = crate::MAX_NESTING_DEPTH + 8;
2152 let src = format!(
2153 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2154 "(".repeat(depth),
2155 ")".repeat(depth),
2156 );
2157 parse_str(&src).unwrap_err()
2158 });
2159 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2160 }
2161
2162 #[test]
2163 fn deeply_nested_types_are_bounded_not_overflowed() {
2164 let errs = on_big_stack(|| {
2169 let depth = crate::MAX_NESTING_DEPTH + 8;
2170 let src = format!(
2171 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2172 "Result[Int, ".repeat(depth),
2173 "]".repeat(depth),
2174 );
2175 parse_str(&src).unwrap_err()
2176 });
2177 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2178 }
2179
2180 #[test]
2181 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2182 let errs = on_big_stack(|| {
2187 let depth = crate::MAX_NESTING_DEPTH + 8;
2188 let src = format!(
2189 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2190 "Ok(".repeat(depth),
2191 ")".repeat(depth),
2192 );
2193 parse_str(&src).unwrap_err()
2194 });
2195 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2196 }
2197
2198 #[test]
2199 fn nesting_below_the_limit_still_parses() {
2200 let ok = on_big_stack(|| {
2203 let depth = crate::MAX_NESTING_DEPTH - 8;
2204 let src = format!(
2205 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2206 "(".repeat(depth),
2207 ")".repeat(depth),
2208 );
2209 parse_str(&src).is_ok()
2210 });
2211 assert!(ok, "well-nested source under the limit should parse");
2212 }
2213
2214 #[test]
2215 fn let_statement_parses() {
2216 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2217 let CommonsItem::Fn(f) = &c.items[0] else {
2218 panic!()
2219 };
2220 assert_eq!(f.body.statements.len(), 1);
2221 match &f.body.statements[0] {
2222 Statement::Let(l) => {
2223 assert_eq!(l.name.name, "y");
2224 assert!(l.type_annot.is_none());
2225 }
2226 _ => panic!("expected a pure `let` statement"),
2227 }
2228 }
2229
2230 #[test]
2231 fn let_with_annotation() {
2232 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2233 let CommonsItem::Fn(f) = &c.items[0] else {
2234 panic!()
2235 };
2236 match &f.body.statements[0] {
2237 Statement::Let(l) => assert!(l.type_annot.is_some()),
2238 _ => panic!("expected a pure `let` statement"),
2239 }
2240 }
2241
2242 #[test]
2243 fn if_else_parses_as_expression() {
2244 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2245 let CommonsItem::Fn(f) = &c.items[0] else {
2246 panic!()
2247 };
2248 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2249 }
2250
2251 #[test]
2252 fn else_if_chain_parses() {
2253 let c = parse_str(
2254 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2255 )
2256 .unwrap();
2257 let CommonsItem::Fn(f) = &c.items[0] else {
2258 panic!()
2259 };
2260 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2261 panic!()
2262 };
2263 assert!(else_block.statements.is_empty());
2265 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2266 }
2267
2268 #[test]
2269 fn ok_and_err_parse_as_expressions() {
2270 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2271 let CommonsItem::Fn(f) = &c.items[0] else {
2272 panic!()
2273 };
2274 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2275
2276 let c =
2277 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2278 let CommonsItem::Fn(f) = &c.items[0] else {
2279 panic!()
2280 };
2281 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2282 }
2283
2284 #[test]
2285 fn question_postfix_parses() {
2286 let c = parse_str(
2287 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2288 )
2289 .unwrap();
2290 let CommonsItem::Fn(f) = &c.items[1] else {
2291 panic!()
2292 };
2293 let Statement::Let(l) = &f.body.statements[0] else {
2294 panic!("expected a pure `let` statement");
2295 };
2296 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2297 }
2298
2299 #[test]
2300 fn constructor_call_parses() {
2301 let c = parse_str(
2302 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2303 )
2304 .unwrap();
2305 let CommonsItem::Fn(f) = &c.items[1] else {
2306 panic!()
2307 };
2308 let ExprKind::MethodCall {
2311 receiver, method, ..
2312 } = &f.body.tail.kind
2313 else {
2314 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2315 };
2316 let ExprKind::Ident(id) = &receiver.kind else {
2317 panic!("expected receiver Ident");
2318 };
2319 assert_eq!(id.name, "T");
2320 assert_eq!(method.name, "of");
2321 }
2322
2323 #[test]
2324 fn result_type_ref_parses() {
2325 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2326 let CommonsItem::Fn(f) = &c.items[0] else {
2327 panic!()
2328 };
2329 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2330 }
2331
2332 #[test]
2333 fn result_missing_arg_count_errors() {
2334 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2335 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2336 }
2337
2338 #[test]
2339 fn field_access_parses_in_v0_2() {
2340 let c =
2343 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2344 let CommonsItem::Fn(f) = &c.items[1] else {
2345 panic!()
2346 };
2347 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2348 }
2349
2350 #[test]
2353 fn leading_line_comment_attaches_to_next_decl() {
2354 let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2355 let c = parse_str(src).unwrap();
2356 let CommonsItem::Type(t) = &c.items[0] else {
2357 panic!()
2358 };
2359 assert_eq!(
2360 t.trivia.leading,
2361 vec![Comment::Line(" explain the type".to_string())]
2362 );
2363 assert!(t.trivia.trailing.is_none());
2364 }
2365
2366 #[test]
2367 fn trailing_line_comment_attaches_to_prev_decl() {
2368 let src = "commons x {\ntype T = Int where NonNegative // trailing note\n}";
2369 let c = parse_str(src).unwrap();
2370 let CommonsItem::Type(t) = &c.items[0] else {
2371 panic!()
2372 };
2373 assert!(t.trivia.leading.is_empty());
2374 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2375 }
2376
2377 #[test]
2378 fn grouped_leading_comments_attach_together() {
2379 let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2380 let c = parse_str(src).unwrap();
2381 let CommonsItem::Type(t) = &c.items[0] else {
2382 panic!()
2383 };
2384 assert_eq!(
2385 t.trivia.leading,
2386 vec![
2387 Comment::Line(" one".to_string()),
2388 Comment::Line(" two".to_string()),
2389 Comment::Line(" three".to_string())
2390 ],
2391 );
2392 }
2393
2394 #[test]
2395 fn comment_with_doc_block_keeps_both() {
2396 let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2398 let c = parse_str(src).unwrap();
2399 let CommonsItem::Type(t) = &c.items[0] else {
2400 panic!()
2401 };
2402 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2403 assert_eq!(t.documentation.as_deref(), Some("docs"));
2404 }
2405
2406 #[test]
2407 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2408 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2414 let c = parse_str(src).unwrap();
2415 assert_eq!(c.name.joined(), "app.messages");
2416 }
2417
2418 #[test]
2419 fn messages_decl_parses_tag_annotation_and_entries() {
2420 let src = "commons app.messages {\n\
2422 // intro\n\
2423 ---\n\
2424 docs\n\
2425 ---\n\
2426 messages \"en\" @reference {\n\
2427 \"greeting\" => \"Hello, {name}!\"\n\
2428 \"farewell\" => \"Bye\"\n\
2429 } // trailing\n\
2430 }";
2431 let c = parse_str(src).unwrap();
2432 let CommonsItem::Messages(m) = &c.items[0] else {
2433 panic!("expected a messages item, got {:?}", c.items[0]);
2434 };
2435 assert_eq!(m.tag, "en");
2436 assert_eq!(m.annotations.len(), 1);
2437 assert_eq!(m.annotations[0].name.name, "reference");
2438 assert!(m.annotations[0].args.is_empty());
2439 assert_eq!(m.entries.len(), 2);
2440 assert_eq!(m.entries[0].code, "greeting");
2441 assert_eq!(m.entries[0].template, "Hello, {name}!");
2442 assert_eq!(m.entries[1].code, "farewell");
2443 assert_eq!(m.entries[1].template, "Bye");
2444 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2445 assert_eq!(m.documentation.as_deref(), Some("docs"));
2446 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2447 }
2448
2449 #[test]
2450 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2451 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2455 let c = parse_str(src).unwrap();
2456 let CommonsItem::Messages(m) = &c.items[0] else {
2457 panic!("expected a messages item, got {:?}", c.items[0]);
2458 };
2459 assert_eq!(m.tag, "en");
2460 assert!(m.annotations.is_empty());
2461 assert!(m.entries.is_empty());
2462 }
2463
2464 #[test]
2465 fn messages_decl_parses_syntactically_inside_a_context_too() {
2466 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2471 let toks = tokenize(src).unwrap();
2472 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2473 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2474 let Some(SourceUnit::Context(ctx)) = unit else {
2475 panic!("expected a context")
2476 };
2477 let CommonsItem::Messages(m) = &ctx.items[0] else {
2478 panic!("expected a messages item, got {:?}", ctx.items[0]);
2479 };
2480 assert_eq!(m.tag, "en");
2481 }
2482
2483 #[test]
2484 fn comment_before_let_statement_attaches() {
2485 let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2486 let c = parse_str(src).unwrap();
2487 let CommonsItem::Fn(f) = &c.items[0] else {
2488 panic!()
2489 };
2490 let Statement::Let(l) = &f.body.statements[0] else {
2491 panic!()
2492 };
2493 assert_eq!(
2494 l.trivia.leading,
2495 vec![Comment::Line(" pick a value".to_string())]
2496 );
2497 }
2498
2499 #[test]
2500 fn comment_before_tail_attaches_to_block_tail() {
2501 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2502 let c = parse_str(src).unwrap();
2503 let CommonsItem::Fn(f) = &c.items[0] else {
2504 panic!()
2505 };
2506 assert_eq!(
2507 f.body.tail_leading_comments,
2508 vec![Comment::Line(" result".to_string())],
2509 );
2510 }
2511
2512 #[test]
2518 fn contextual_keywords_are_valid_identifiers() {
2519 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2521 .expect("`on`/`suite`/`case` are valid field names");
2522 let CommonsItem::Type(_) = &c.items[0] else {
2523 panic!("expected a type decl")
2524 };
2525
2526 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2528 .expect("`on`/`case` are valid parameter names");
2529
2530 parse_str("commons demo {\n type R = { suite: Int }\n}")
2532 .expect("`suite` is a valid field name");
2533 }
2534
2535 #[test]
2543 fn is_reserved_keyword_covers_every_lexer_keyword() {
2544 let lexer_src = include_str!("lexer.rs");
2545 let mut words = Vec::new();
2546 for line in lexer_src.lines() {
2547 let t = line.trim();
2548 if let Some(rest) = t.strip_prefix("#[token(\"")
2549 && let Some(word) = rest.split('"').next()
2550 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2551 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2552 {
2553 words.push(word.to_string());
2554 }
2555 }
2556 assert!(
2557 words.len() > 30,
2558 "keyword extraction looks broken: only {} words",
2559 words.len()
2560 );
2561 use crate::keywords::RESERVED_CONTEXTUAL;
2564 let mut unclassified = Vec::new();
2565 for word in &words {
2566 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2567 let kind = tokens.first().expect("keyword yields a token").kind;
2568 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2569 unclassified.push(word.clone());
2570 }
2571 }
2572 assert!(
2573 unclassified.is_empty(),
2574 "keywords missing from is_reserved_keyword (add them, or document \
2575 them as contextual): {unclassified:?}"
2576 );
2577 }
2578
2579 #[test]
2590 fn is_item_start_matches_the_pinned_keyword_set() {
2591 use TokenKind::*;
2592 let expected_true = [
2593 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Embed, Uses,
2594 Consumes, Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case,
2595 Property,
2596 ];
2597 for kind in expected_true {
2598 assert!(is_item_start(kind), "{kind:?} must be an item start");
2599 }
2600 let expected_false = [
2601 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2602 Where, True, False, Match, Is, On, Given,
2603 ];
2604 for kind in expected_false {
2605 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2606 }
2607 }
2608
2609 #[test]
2614 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2615 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2616 let tokens = crate::lexer::tokenize(src).unwrap();
2617 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2618 assert!(unit.is_some(), "the commons header still parses");
2619 assert!(
2620 errors
2621 .iter()
2622 .any(|e| e.category == "bynk.capability.outside_context"),
2623 "the misplaced capability is reported: {errors:?}"
2624 );
2625 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2628 }
2629
2630 #[test]
2631 fn trailing_file_comment_becomes_unit_trailing() {
2632 let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2636 let c = parse_str(src).unwrap();
2637 assert_eq!(
2638 c.trailing_comments,
2639 vec![Comment::Line(" afterword".to_string())]
2640 );
2641 }
2642
2643 #[test]
2644 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2645 let src = "commons x {\n type T = Int where Positive\n}\n// afterword\n";
2652 let c = parse_str(src).unwrap();
2653 assert_eq!(
2654 c.trailing_comments,
2655 vec![Comment::Line(" afterword".to_string())]
2656 );
2657 }
2658
2659 #[test]
2660 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2661 let src = "context x {\n type T = Int where Positive\n}\n// afterword\n";
2663 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2664 panic!("expected context");
2665 };
2666 assert_eq!(
2667 c.trailing_comments,
2668 vec![Comment::Line(" afterword".to_string())]
2669 );
2670 }
2671
2672 #[test]
2673 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2674 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n// afterword\n";
2676 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2677 panic!("expected suite");
2678 };
2679 assert_eq!(
2680 s.trailing_comments,
2681 vec![Comment::Line(" afterword".to_string())]
2682 );
2683 }
2684
2685 #[test]
2692 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2693 let src = "adapter x {\n binding \"./x.ts\"\n}\n// afterword\n";
2694 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2695 panic!("expected adapter");
2696 };
2697 assert_eq!(
2698 a.trailing_comments,
2699 vec![Comment::Line(" afterword".to_string())]
2700 );
2701 }
2702
2703 #[test]
2706 fn binding_includes_list_parses_before_requires() {
2707 let src = "adapter x {\n binding \"./x.ts\" includes [\"./a.wasm\", \"b/c.wasm\"] requires { \"p\": \"^1\" }\n}\n";
2708 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2709 panic!("expected adapter");
2710 };
2711 let b = a.binding.unwrap();
2712 let paths: Vec<_> = b.includes.iter().map(|i| i.path.as_str()).collect();
2713 assert_eq!(paths, ["./a.wasm", "b/c.wasm"]);
2714 assert_eq!(b.requires.len(), 1);
2715 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [\"a.wasm\"").is_err());
2716 assert!(parse_unit_str("adapter x {\n binding \"./x.ts\" includes [a]\n}\n").is_err());
2717 }
2718
2719 #[test]
2726 fn commons_fragment_rejects_uses_after_a_decl() {
2727 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
2728 let errs = parse_str(src).unwrap_err();
2729 assert!(
2730 errs.iter()
2731 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2732 "{errs:?}"
2733 );
2734 }
2735
2736 #[test]
2739 fn commons_brace_allows_uses_after_a_decl() {
2740 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2741 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2742 }
2743
2744 #[test]
2746 fn context_fragment_rejects_consumes_after_a_decl() {
2747 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2748 let errs = parse_unit_str(src).unwrap_err();
2749 assert!(
2750 errs.iter()
2751 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2752 "{errs:?}"
2753 );
2754 }
2755
2756 #[test]
2758 fn context_fragment_rejects_exports_after_a_decl() {
2759 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2760 let errs = parse_unit_str(src).unwrap_err();
2761 assert!(
2762 errs.iter()
2763 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2764 "{errs:?}"
2765 );
2766 }
2767
2768 #[test]
2770 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2771 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2772 parse_unit_str(src)
2773 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2774 }
2775
2776 #[test]
2778 fn test_fragment_rejects_uses_after_a_decl() {
2779 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2780 let errs = parse_unit_str(src).unwrap_err();
2781 assert!(
2782 errs.iter()
2783 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2784 "{errs:?}"
2785 );
2786 }
2787
2788 #[test]
2790 fn test_brace_allows_uses_after_a_decl() {
2791 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2792 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2793 }
2794
2795 fn body_tail(body: &str) -> ExprKind {
2799 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2800 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2801 let CommonsItem::Fn(f) = &c.items[0] else {
2802 panic!("expected fn, got {:?}", c.items[0]);
2803 };
2804 f.body.tail.kind.clone()
2805 }
2806
2807 fn body_err(body: &str) -> Vec<CompileError> {
2808 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2809 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2810 }
2811
2812 #[test]
2813 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2814 for src in [
2817 "if ready { result } else { fallback }",
2818 "if ready { fallback } else { result }",
2819 "if !ready { result } else { fallback }",
2820 "if a == b { result } else { fallback }",
2821 "if a && b { result } else { fallback }",
2822 ] {
2823 let ExprKind::If {
2824 then_block,
2825 else_block,
2826 ..
2827 } = body_tail(src)
2828 else {
2829 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2830 };
2831 assert!(
2834 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2835 "then-branch tail not an ident for {src:?}: {:?}",
2836 then_block.tail.kind,
2837 );
2838 assert!(
2839 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2840 "else-branch tail not an ident for {src:?}: {:?}",
2841 else_block.tail.kind,
2842 );
2843 }
2844 }
2845
2846 #[test]
2847 fn else_less_if_with_single_ident_branch_parses() {
2848 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2850 panic!("expected If");
2851 };
2852 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2853 }
2854
2855 #[test]
2856 fn record_construction_still_parses_in_value_position() {
2857 assert!(matches!(
2860 body_tail("Point { x }"),
2861 ExprKind::RecordConstruction { .. }
2862 ));
2863 assert!(matches!(
2864 body_tail("Point { x: 1, y: 2 }"),
2865 ExprKind::RecordConstruction { .. }
2866 ));
2867 assert!(matches!(
2868 body_tail("Empty {}"),
2869 ExprKind::RecordConstruction { .. }
2870 ));
2871 }
2872
2873 #[test]
2874 fn parenthesised_record_is_allowed_in_condition_head() {
2875 let ExprKind::If { cond, .. } =
2878 body_tail("if (ready { result }) { branch } else { other }")
2879 else {
2880 panic!("expected If");
2881 };
2882 let ExprKind::Paren(inner) = &cond.kind else {
2883 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2884 };
2885 assert!(
2886 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2887 "parenthesised record in condition head should still construct: {:?}",
2888 inner.kind,
2889 );
2890 }
2891
2892 #[test]
2893 fn record_in_call_arg_within_condition_still_constructs() {
2894 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2897 else {
2898 panic!("expected If");
2899 };
2900 let ExprKind::Call { args, .. } = &cond.kind else {
2901 panic!("expected Call in condition, got {:?}", cond.kind);
2902 };
2903 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2904 }
2905
2906 #[test]
2907 fn safe_condition_shapes_are_unaffected() {
2908 assert!(matches!(
2910 body_tail("if ready == true { result } else { fallback }"),
2911 ExprKind::If { .. }
2912 ));
2913 assert!(matches!(
2914 body_tail("if (ready) { result } else { fallback }"),
2915 ExprKind::If { .. }
2916 ));
2917 assert!(matches!(
2918 body_tail("if ready { \"a\" } else { \"b\" }"),
2919 ExprKind::If { .. }
2920 ));
2921 }
2922
2923 fn body_block(body: &str) -> Block {
2924 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2925 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2926 let CommonsItem::Fn(f) = &c.items[0] else {
2927 panic!("expected fn, got {:?}", c.items[0]);
2928 };
2929 f.body.clone()
2930 }
2931
2932 fn has_error(errs: &[CompileError], code: &str) -> bool {
2933 errs.iter().any(|e| e.category == code)
2934 }
2935
2936 #[test]
2937 fn let_else_desugars_to_a_hidden_binding_and_a_match() {
2938 let b = body_block("let x = 1\n let Some(n) = find(x) else { 0 }\n n + x");
2941 assert_eq!(
2942 b.statements.len(),
2943 2,
2944 "the plain let and the hidden binding"
2945 );
2946 let Statement::Let(hidden) = &b.statements[1] else {
2947 panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
2948 };
2949 assert!(
2950 is_generated_binding(&hidden.name.name),
2951 "{}",
2952 hidden.name.name
2953 );
2954 assert_eq!(
2955 hidden.name.span.start, hidden.name.span.end,
2956 "the binder has no extent"
2957 );
2958 let le = hidden
2959 .let_else
2960 .as_ref()
2961 .expect("the binding records the sugar");
2962 assert_eq!(le.form, LetElseForm::Block);
2963 let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
2964 panic!("expected a match tail, got {:?}", b.tail.kind);
2965 };
2966 assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
2967 assert_eq!(arms.len(), 2);
2968 assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
2969 let MatchBody::Block(rest) = &arms[0].body else {
2970 panic!("the pattern's arm runs the rest of the block");
2971 };
2972 assert!(rest.statements.is_empty());
2973 assert!(
2974 !matches!(rest.tail.kind, ExprKind::UnitLit),
2975 "{:?}",
2976 rest.tail.kind
2977 );
2978 assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
2979 }
2980
2981 #[test]
2982 fn effectful_let_else_binds_with_an_arrow() {
2983 let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n v");
2984 assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
2985 }
2986
2987 #[test]
2988 fn let_else_chain_nests_each_step_in_the_previous_arm() {
2989 let b = body_block(
2992 "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",
2993 );
2994 let ExprKind::Match { arms, .. } = &b.tail.kind else {
2995 panic!("expected a match tail");
2996 };
2997 let MatchBody::Block(second) = &arms[0].body else {
2998 panic!("expected a block arm");
2999 };
3000 assert_eq!(
3001 second.statements.len(),
3002 2,
3003 "`let y` and the second hidden binding"
3004 );
3005 assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
3006 let ExprKind::Match { arms, .. } = &second.tail.kind else {
3007 panic!("expected the second step's match");
3008 };
3009 let MatchBody::Block(third) = &arms[0].body else {
3010 panic!("expected a block arm");
3011 };
3012 let ExprKind::Match { arms, .. } = &third.tail.kind else {
3013 panic!("expected the third step's match");
3014 };
3015 let MatchBody::Block(last) = &arms[0].body else {
3016 panic!("expected a block arm");
3017 };
3018 assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3019 }
3020
3021 #[test]
3022 fn else_match_arms_follow_the_patterns_arm() {
3023 let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n v");
3024 let Statement::Let(l) = &b.statements[0] else {
3025 panic!("expected the hidden binding");
3026 };
3027 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3028 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3029 panic!("expected a match tail");
3030 };
3031 assert_eq!(arms.len(), 2);
3032 assert!(
3033 matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3034 );
3035 }
3036
3037 #[test]
3038 fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3039 let b = body_block("let Some(n) = find(1) else { () }");
3040 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3041 panic!("expected a match tail");
3042 };
3043 let MatchBody::Block(rest) = &arms[0].body else {
3044 panic!("expected a block arm");
3045 };
3046 assert!(rest.implicit_tail && rest.statements.is_empty());
3047 }
3048
3049 #[test]
3050 fn let_pattern_forms_are_recognised() {
3051 for src in [
3052 "let None = o else { 0 }\n 1",
3053 "let Status.Done = s else { 0 }\n 1",
3054 "let Shipped(at) = s else { 0 }\n at",
3055 "let 0 = n else { 1 }\n 0",
3056 "let -1 = n else { 1 }\n 0",
3057 "let \"a\" = s else { 1 }\n 0",
3058 "let (Ok(x) | Err(x)) = r else { 1 }\n x",
3059 ] {
3060 let b = body_block(src);
3061 assert!(
3062 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3063 "{src:?} should parse as a let-else"
3064 );
3065 }
3066 let b = body_block("let Total = 1\n Total");
3068 assert!(
3069 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3070 );
3071 }
3072
3073 #[test]
3074 fn let_pattern_without_else_is_refused() {
3075 assert!(has_error(
3076 &body_err("let Ok(v) = r\n v"),
3077 "bynk.parse.let_pattern_without_else"
3078 ));
3079 assert!(has_error(
3080 &body_err("let Ok(v) <- r()\n v"),
3081 "bynk.parse.let_pattern_without_else"
3082 ));
3083 }
3084
3085 #[test]
3086 fn else_on_a_plain_binding_is_refused() {
3087 assert!(has_error(
3088 &body_err("let v = r else { 0 }\n v"),
3089 "bynk.parse.let_else_on_binding"
3090 ));
3091 assert!(has_error(
3092 &body_err("let v <- r() else { 0 }\n v"),
3093 "bynk.parse.let_else_on_binding"
3094 ));
3095 }
3096
3097 #[test]
3098 fn annotated_let_pattern_is_refused() {
3099 assert!(has_error(
3100 &body_err("let Ok(v): Int = r else { 0 }\n v"),
3101 "bynk.parse.let_pattern_annotation"
3102 ));
3103 }
3104
3105 #[test]
3106 fn empty_else_match_is_refused() {
3107 assert!(has_error(
3108 &body_err("let Ok(v) = r else match { }\n v"),
3109 "bynk.parse.empty_match"
3110 ));
3111 }
3112
3113 #[test]
3114 fn let_else_steps_count_against_the_nesting_budget() {
3115 let chain = |n: usize| -> String {
3119 let steps: String = (0..n)
3120 .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n "))
3121 .collect();
3122 format!("{steps}0")
3123 };
3124 body_block(&chain(40));
3125 let errs = body_err(&chain(400));
3126 assert!(
3127 errs.iter()
3128 .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3129 "{errs:?}"
3130 );
3131 }
3132
3133 #[test]
3134 fn empty_match_reports_its_own_diagnostic() {
3135 let errs = body_err("match result {}");
3139 assert!(
3140 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3141 "expected empty_match; got {errs:?}",
3142 );
3143 }
3144
3145 #[test]
3146 fn match_discriminant_ending_in_ident_parses() {
3147 assert!(matches!(
3149 body_tail("match ready { x => x }"),
3150 ExprKind::Match { .. }
3151 ));
3152 }
3153
3154 #[test]
3163 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3164 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
3165 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3166 let CommonsItem::Fn(f) = &c.items[0] else {
3167 panic!("expected fn, got {:?}", c.items[0]);
3168 };
3169 assert_eq!(
3170 f.body.statements.len(),
3171 1,
3172 "expected exactly one Assign statement, got {:?}",
3173 f.body.statements
3174 );
3175 let Statement::Assign(a) = &f.body.statements[0] else {
3176 panic!(
3177 "expected an Assign statement, got {:?}",
3178 f.body.statements[0]
3179 );
3180 };
3181 assert!(
3182 matches!(a.value.kind, ExprKind::Ident(_)),
3183 "assign value must stay the bare identifier `Paid`, got {:?}",
3184 a.value.kind
3185 );
3186 assert!(
3187 matches!(f.body.tail.kind, ExprKind::UnitLit),
3188 "the `()` must remain the block's own tail, got {:?}",
3189 f.body.tail.kind
3190 );
3191 }
3192
3193 #[test]
3198 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
3199 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
3200 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
3201 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
3202 let CommonsItem::Fn(f) = &c.items[0] else {
3203 panic!("expected fn, got {:?}", c.items[0]);
3204 };
3205 let Statement::Let(l) = &f.body.statements[0] else {
3206 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
3207 };
3208 let ExprKind::Is { pattern, .. } = &l.value.kind else {
3209 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
3210 };
3211 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
3212 panic!("{name}: expected a variant pattern, got {pattern:?}");
3213 };
3214 assert_eq!(
3215 span.end, variant.span.end,
3216 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
3217 );
3218 assert!(
3219 matches!(f.body.tail.kind, ExprKind::UnitLit),
3220 "{name}: the `()` must remain the block's own tail, got {:?}",
3221 f.body.tail.kind
3222 );
3223 }
3224 }
3225
3226 #[test]
3229 fn variant_pattern_payload_on_the_same_line_still_binds() {
3230 for (name, pat) in [
3231 ("user variant", "Paid(\n x\n )"),
3232 ("built-in variant", "Some(x)"),
3233 ] {
3234 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
3235 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
3236 let CommonsItem::Fn(f) = &c.items[0] else {
3237 panic!("expected fn, got {:?}", c.items[0]);
3238 };
3239 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
3240 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
3241 };
3242 assert!(
3243 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
3244 "{name}: expected one binding, got {pattern:?}"
3245 );
3246 }
3247 }
3248
3249 #[test]
3255 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
3256 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
3257 assert!(
3258 parse_str(src).is_err(),
3259 "a next-line `(` after an arm's variant must not open its payload list"
3260 );
3261 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
3262 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
3263 }
3264
3265 #[test]
3269 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
3270 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
3271 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3272 let CommonsItem::Fn(f) = &c.items[0] else {
3273 panic!("expected fn, got {:?}", c.items[0]);
3274 };
3275 let Statement::Let(l) = &f.body.statements[0] else {
3276 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
3277 };
3278 assert!(
3279 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
3280 "let value must stay a field access, got {:?}",
3281 l.value.kind
3282 );
3283 assert!(
3284 matches!(f.body.tail.kind, ExprKind::UnitLit),
3285 "the `()` must remain the block's own tail, got {:?}",
3286 f.body.tail.kind
3287 );
3288 }
3289
3290 #[test]
3291 fn unparenthesised_record_in_condition_head_now_errors() {
3292 assert!(
3298 !body_err("match Point { x: 1 } { p => p }").is_empty(),
3299 "unparenthesised record discriminant should not parse",
3300 );
3301 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
3303 else {
3304 panic!("expected Match for the parenthesised form");
3305 };
3306 let ExprKind::Paren(inner) = &discriminant.kind else {
3307 panic!(
3308 "expected a parenthesised discriminant, got {:?}",
3309 discriminant.kind
3310 );
3311 };
3312 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
3313 }
3314
3315 fn strict_codes(src: &str) -> Vec<&'static str> {
3317 let toks = tokenize(src).unwrap();
3318 match parse_units_with_warnings(&toks, src) {
3319 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
3320 Err(errs) => errs.iter().map(|e| e.category).collect(),
3321 }
3322 }
3323
3324 #[test]
3328 fn a_divider_pair_around_code_is_doc_block_contains_code() {
3329 let src =
3330 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
3331 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
3332 let errs = parse_str(src).unwrap_err();
3333 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
3334 assert_eq!(
3336 &src[errs[0].span.range()],
3337 "--------\nfn helper() -> Int { 1 }\n--------\n"
3338 );
3339 let (unit, errs) = parse_recover_str(src);
3340 assert!(unit.is_some(), "recovery keeps the unit");
3341 assert!(
3342 errs.iter()
3343 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
3344 "{errs:?}"
3345 );
3346 }
3347
3348 #[test]
3351 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
3352 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
3353 let codes = strict_codes(orphan);
3354 assert!(
3355 codes.contains(&"bynk.parse.doc_block_contains_code"),
3356 "{codes:?}"
3357 );
3358 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
3359 let codes = strict_codes(trailing);
3360 assert!(
3361 codes.contains(&"bynk.parse.doc_block_contains_code"),
3362 "{codes:?}"
3363 );
3364 }
3365
3366 #[test]
3370 fn near_miss_prose_and_fenced_code_are_not_code() {
3371 for doc in [
3372 "type of the thing",
3373 "fn is used here",
3374 "The type Id is an Int.",
3375 "uses the clock",
3376 "Event handler.\nfn helper() -> Int { 1 }",
3377 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
3378 "~~~~\ntype Id = Int\n~~~~",
3379 "---\nA nested doc.\n---",
3380 "",
3381 ] {
3382 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3383 }
3384 for doc in [
3385 "fn helper() -> Int { 1 }",
3386 "type Id = Int\nfn f() -> Int { 1 }",
3387 "```\nan example\n```\nfn helper() -> Int { 1 }",
3388 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
3389 ] {
3390 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
3391 }
3392 }
3393
3394 #[test]
3398 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
3399 let nested =
3400 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
3401 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
3402 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
3403 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
3404 assert!(doc_content_is_code(
3406 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
3407 ));
3408 for doc in [
3409 "---\nAn inner doc.\n---",
3410 "Intro.\n\n---\n\nfn is used here",
3411 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
3412 ] {
3413 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3414 }
3415 let mut deep = "fn helper() -> Int { 1 }".to_string();
3417 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
3418 let fence = "-".repeat(n);
3419 deep = format!("{fence}\n{deep}\n{fence}");
3420 }
3421 assert!(!doc_content_is_code(&deep));
3422 }
3423
3424 #[test]
3427 fn a_fenced_nested_doc_example_parses_clean() {
3428 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";
3429 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3430 let c = parse_str(src).unwrap();
3431 let CommonsItem::Fn(f) = &c.items[0] else {
3432 panic!("expected fn")
3433 };
3434 assert!(
3435 f.documentation
3436 .as_deref()
3437 .is_some_and(|d| d.contains("---\nInner.\n---")),
3438 "{:?}",
3439 f.documentation
3440 );
3441 }
3442
3443 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
3445 let CommonsItem::Type(t) = &c.items[i] else {
3446 panic!("expected a type at {i}")
3447 };
3448 t
3449 }
3450
3451 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
3452 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
3455 }
3456
3457 #[test]
3460 fn doc_lines_document_a_declaration() {
3461 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
3462 let c = parse_str(src).unwrap();
3463 assert_eq!(
3464 doc_of(&type_at(&c, 0).documentation),
3465 Some(("A commit SHA.", DocForm::Lines))
3466 );
3467 let CommonsItem::Fn(f) = &c.items[1] else {
3468 panic!("expected fn")
3469 };
3470 assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
3471 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3472 }
3473
3474 #[test]
3478 fn legacy_spellings_parse_and_are_each_reported() {
3479 let src =
3480 "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n repo: String, -- trailing\n}\n";
3481 assert_eq!(
3482 strict_codes(src),
3483 [
3484 "bynk.lex.legacy_comment",
3485 "bynk.lex.legacy_doc_line",
3486 "bynk.lex.legacy_comment"
3487 ]
3488 );
3489 let tokens = tokenize(src).unwrap();
3490 let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
3491 let SourceUnit::Commons(c) = &units[0] else {
3492 panic!("commons")
3493 };
3494 assert_eq!(
3495 doc_of(&type_at(c, 0).documentation),
3496 Some(("A run.", DocForm::Lines))
3497 );
3498 }
3499
3500 #[test]
3503 fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
3504 let src = "commons m\n\ntype R = {\n --- First.\n --- Second.\n a: Int,\n}\n";
3505 let tokens = tokenize(src).unwrap();
3506 let errs = parse_units(&tokens, src).unwrap_err();
3507 assert_eq!(errs.len(), 1, "{errs:?}");
3508 assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
3509 let (span, block) = &errs[0].suggestions[0].edits[0];
3510 assert_eq!(&src[span.range()], "--- First.\n --- Second.");
3511 assert_eq!(block, "---\n First.\n Second.\n ---");
3512 }
3513
3514 #[test]
3518 fn members_take_docs_in_either_form() {
3519 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";
3520 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3521 let c = parse_str(src).unwrap();
3522 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3523 panic!("record")
3524 };
3525 assert_eq!(
3526 doc_of(&r.fields[0].documentation),
3527 Some(("The repository.", DocForm::Lines))
3528 );
3529 assert_eq!(
3530 doc_of(&r.fields[1].documentation),
3531 Some(("The version.", DocForm::Block))
3532 );
3533 assert_eq!(r.fields[2].documentation, None);
3534 assert_eq!(
3535 r.fields[2].trivia.leading,
3536 vec![Comment::Line(" a note".into())]
3537 );
3538 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
3539 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
3540 panic!("enum")
3541 };
3542 assert_eq!(
3543 doc_of(&e.variants[0].documentation),
3544 Some(("Passed.", DocForm::Lines))
3545 );
3546 assert_eq!(e.variants[1].documentation, None);
3547 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
3548 panic!("sum")
3549 };
3550 assert_eq!(
3551 doc_of(&p.variants[0].documentation),
3552 Some(("Round.", DocForm::Lines))
3553 );
3554 assert_eq!(
3555 doc_of(&p.variants[0].payload[0].documentation),
3556 Some(("The radius.", DocForm::Lines))
3557 );
3558 assert_eq!(p.variants[1].documentation, None);
3559 let CommonsItem::Fn(f) = &c.items[3] else {
3561 panic!("expected fn")
3562 };
3563 assert_eq!(
3564 doc_of(&f.documentation),
3565 Some(("After the sum.", DocForm::Lines))
3566 );
3567 let CommonsItem::Messages(m) = &c.items[4] else {
3568 panic!("expected messages")
3569 };
3570 assert_eq!(
3571 doc_of(&m.entries[0].documentation),
3572 Some(("Greets.", DocForm::Lines))
3573 );
3574 }
3575
3576 #[test]
3580 fn two_docs_on_one_target_is_multiple_docs() {
3581 for src in [
3582 "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
3583 "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
3584 "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
3585 "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
3586 "commons m\n\ntype R = {\n --- Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
3587 ] {
3588 assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
3589 let (unit, errs) = parse_recover_str(src);
3590 assert!(unit.is_some());
3591 assert!(
3592 errs.iter()
3593 .any(|e| e.category == "bynk.parse.multiple_docs")
3594 );
3595 }
3596 }
3597
3598 #[test]
3601 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
3602 let src =
3603 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --- dangling\n )\n | Dot\n";
3604 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
3605 let c = parse_str(src).unwrap();
3606 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
3607 panic!("sum")
3608 };
3609 assert_eq!(s.variants[0].payload.len(), 1);
3610 assert_eq!(s.variants[0].payload[0].documentation, None);
3611 }
3612
3613 #[test]
3616 fn an_unattached_member_doc_is_an_orphan() {
3617 let src = "commons m\n\ntype R = {\n --- Spaced.\n\n a: Int,\n --- Dangling.\n}\n";
3618 assert_eq!(
3619 strict_codes(src),
3620 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
3621 );
3622 let c = parse_str(src).unwrap();
3623 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3624 panic!("record")
3625 };
3626 assert_eq!(r.fields[0].documentation, None);
3627 assert!(matches!(
3628 &r.fields[0].trivia.leading[..],
3629 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
3630 ));
3631 assert!(matches!(
3632 &r.trailing_comments[..],
3633 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
3634 ));
3635 }
3636}