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::Uses,
1374 TokenKind::Consumes,
1375 TokenKind::Exports,
1376 TokenKind::Binding,
1377];
1378
1379fn doc_content_is_code(content: &str) -> bool {
1394 doc_content_is_code_at(content, 0)
1395}
1396
1397const DOC_PROBE_MAX_DEPTH: usize = 4;
1401
1402fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1403 let stripped = strip_code_fences(content);
1404 let tokens = match crate::lexer::tokenize(&stripped) {
1405 Ok(tokens) => tokens,
1406 Err(_) => {
1407 let unmarked = blank_marker_lines(&stripped);
1410 if unmarked == stripped {
1411 return false;
1412 }
1413 return doc_content_is_code_at(&unmarked, depth);
1414 }
1415 };
1416 if depth < DOC_PROBE_MAX_DEPTH
1417 && tokens.iter().any(|t| {
1418 t.kind == TokenKind::DocBlock
1419 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1420 })
1421 {
1422 return true;
1423 }
1424 if !tokens
1425 .iter()
1426 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1427 {
1428 return false;
1429 }
1430 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1431 let mut warnings = Vec::new();
1432 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1433 p.doc_probe = true;
1434 let at = Span::new(0, 0);
1435 let name = QualifiedName {
1436 parts: Vec::new(),
1437 span: at,
1438 };
1439 match p.parse_adapter_body(at, name, None, false) {
1440 Ok(a) => {
1441 p.peek().is_none()
1442 && (!a.items.is_empty()
1443 || !a.uses.is_empty()
1444 || !a.consumes.is_empty()
1445 || !a.exports.is_empty()
1446 || a.binding.is_some())
1447 }
1448 Err(_) => false,
1449 }
1450}
1451
1452fn blank_marker_lines(content: &str) -> String {
1455 let mut out = String::with_capacity(content.len());
1456 for line in content.lines() {
1457 let trimmed = line.trim();
1458 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1459 out.push_str(line);
1460 }
1461 out.push('\n');
1462 }
1463 out
1464}
1465
1466fn strip_code_fences(content: &str) -> String {
1471 let mut out = String::with_capacity(content.len());
1472 let mut open: Option<(char, usize)> = None;
1473 for line in content.lines() {
1474 let trimmed = line.trim();
1475 let fence = ['`', '~'].into_iter().find_map(|c| {
1476 let n = trimmed.chars().take_while(|&x| x == c).count();
1477 (n >= 3).then_some((c, n))
1478 });
1479 match (open, fence) {
1480 (None, Some(f)) => open = Some(f),
1481 (Some((c, n)), Some((fc, fnum)))
1482 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1483 {
1484 open = None;
1485 }
1486 (None, None) => out.push_str(line),
1487 (Some(_), _) => {}
1488 }
1489 out.push('\n');
1490 }
1491 out
1492}
1493
1494fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1497 let bytes = lexeme.as_bytes();
1498 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
1499 let inner = &lexeme[1..lexeme.len() - 1];
1500 let mut out = String::with_capacity(inner.len());
1501 let mut chars = inner.chars();
1502 while let Some(c) = chars.next() {
1503 if c == '\\' {
1504 match chars.next() {
1505 Some('n') => out.push('\n'),
1506 Some('t') => out.push('\t'),
1507 Some('"') => out.push('"'),
1508 Some('\\') => out.push('\\'),
1509 other => {
1510 return Err(CompileError::new(
1511 "bynk.lex.bad_escape",
1512 span,
1513 format!(
1514 "invalid escape sequence `\\{}` in string literal",
1515 other.map(|c| c.to_string()).unwrap_or_default()
1516 ),
1517 )
1518 .with_note("supported escapes: \\n \\t \\\" \\\\"));
1519 }
1520 }
1521 } else {
1522 out.push(c);
1523 }
1524 }
1525 Ok(out)
1526}
1527
1528fn is_reserved_keyword(kind: TokenKind) -> bool {
1529 use TokenKind::*;
1530 matches!(
1531 kind,
1532 Commons
1533 | Type
1534 | Fn
1535 | Where
1536 | True
1537 | False
1538 | Int
1539 | String
1540 | Bool
1541 | Let
1542 | If
1543 | Else
1544 | Ok
1545 | Err
1546 | Result
1547 | ValidationError
1548 | Enum
1549 | Match
1550 | Option
1551 | Record
1552 | Self_
1553 | Some
1554 | None
1555 | Is
1556 | Opaque
1557 | Uses
1558 | Context
1559 | Consumes
1560 | Exports
1561 | Transparent
1562 | Agent
1563 | As
1564 | Capability
1565 | Effect
1566 | Do
1567 | Given
1568 | On
1569 | Http
1570 | Provides
1571 | Stub
1572 | Service
1573 | Actor
1574 | By
1575 | Expect
1576 | Suite
1577 | Case
1578 | Float
1579 | Duration
1580 | Instant
1581 | Bytes
1582 | JsonError
1583 | Property
1584 | Adapter
1585 | Binding
1586 | Cron
1587 | Queue
1588 | From
1589 | Protocol
1590 | Invariant
1591 | Implies
1592 | Requires
1593 | Ensures
1594 | Transition
1595 )
1596}
1597
1598fn is_item_start(kind: TokenKind) -> bool {
1609 use TokenKind::*;
1610 matches!(
1611 kind,
1612 Commons | Context | Adapter | Suite
1614 | Type | Fn | Messages | Event | Asset | Uses
1616 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1618 | Binding
1620 | Stub | Case | Property
1622 )
1623}
1624
1625#[cfg(test)]
1626mod tests {
1627 use super::*;
1628 use crate::lexer::tokenize;
1629
1630 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1631 let toks = tokenize(src).map_err(|e| vec![e])?;
1632 parse(&toks, src)
1633 }
1634
1635 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1636 let toks = match tokenize(src) {
1637 Ok(t) => t,
1638 Err(e) => return (None, vec![e]),
1639 };
1640 parse_unit_with_recovery(&toks, src)
1641 }
1642
1643 #[test]
1649 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1650 let src =
1651 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1652 let toks = tokenize(src).unwrap();
1653 let (units, errors) = parse_units_with_recovery(&toks, src);
1654 assert!(errors.is_empty(), "{errors:?}");
1655 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1656 assert!(matches!(units[0], SourceUnit::Commons(_)));
1657 assert!(matches!(units[1], SourceUnit::Suite(_)));
1658
1659 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1661 assert!(errors.is_empty(), "{errors:?}");
1662 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1663 }
1664
1665 #[test]
1672 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1673 let toks = tokenize("").unwrap();
1674 let (units, errors) = parse_units_with_recovery(&toks, "");
1675 assert!(units.is_empty());
1676 assert!(
1677 !errors.is_empty(),
1678 "an empty file must still produce a diagnostic, not silently no units and no error"
1679 );
1680
1681 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1682 assert!(unit.is_none());
1683 assert_eq!(
1684 errors.len(),
1685 unit_errors.len(),
1686 "the singular wrapper must see the same error(s) as the plural entry point"
1687 );
1688 }
1689
1690 #[test]
1697 fn drain_check_reports_expression_interior_comments_as_undrained() {
1698 let ordinary = "commons x {\n// note\ntype T = Int where Positive\n}\n";
1699 let toks = tokenize(ordinary).unwrap();
1700 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1701 assert!(
1702 drained,
1703 "a declaration-leading comment must be fully drained"
1704 );
1705
1706 let lossy = "commons x {\n fn f() -> Int {\n 1 + // note\n 2\n }\n}\n";
1707 let toks = tokenize(lossy).unwrap();
1708 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1709 assert!(
1710 !drained,
1711 "a comment inside a binop expression must be reported as undrained"
1712 );
1713 }
1714
1715 #[test]
1716 fn eof_span_never_splits_a_multibyte_codepoint() {
1717 for src in [
1722 "commons x {\n // ends with an arrow →",
1723 "agent A {\n key k: String\n // note 🦀",
1724 "commons y {\n type T = é",
1725 ] {
1726 let (_unit, errors) = parse_recover_str(src);
1727 for e in &errors {
1728 assert!(
1729 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1730 "span {:?} splits a codepoint in {src:?}",
1731 e.span,
1732 );
1733 }
1734 }
1735 }
1736
1737 #[test]
1738 fn reserved_contextual_keywords_readable_in_expression_position() {
1739 for kw in ["asset", "case", "event", "messages", "on", "suite"] {
1749 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1750 let result = parse_str(&src);
1751 assert!(
1752 result.is_ok(),
1753 "a parameter named `{kw}` must be readable in expression position: {:?}",
1754 result.err()
1755 );
1756 }
1757 }
1758
1759 #[test]
1760 fn recovery_skips_garbage_between_decls() {
1761 let src = "commons x {\n\
1764 type A = Int where NonNegative\n\
1765 ??? !!!\n\
1766 type B = String where NonEmpty\n\
1767 }";
1768 let (unit, errors) = parse_recover_str(src);
1769 let unit = unit.expect("recovery should produce a partial AST");
1770 let SourceUnit::Commons(c) = unit else {
1771 panic!("expected commons")
1772 };
1773 let names: Vec<_> = c
1775 .items
1776 .iter()
1777 .map(|i| match i {
1778 CommonsItem::Type(t) => t.name.name.clone(),
1779 _ => panic!("expected only types"),
1780 })
1781 .collect();
1782 assert!(
1783 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1784 "expected both A and B; got {names:?}",
1785 );
1786 assert!(!errors.is_empty(), "expected at least one parse error");
1787 }
1788
1789 #[test]
1790 fn recovery_handles_bad_first_decl_then_good_second() {
1791 let src = "commons x {\n\
1793 type A Int where NonNegative\n\
1794 type B = String where NonEmpty\n\
1795 }";
1796 let (unit, errors) = parse_recover_str(src);
1797 let unit = unit.expect("recovery should produce a partial AST");
1798 let SourceUnit::Commons(c) = unit else {
1799 panic!("expected commons")
1800 };
1801 let names: Vec<_> = c
1802 .items
1803 .iter()
1804 .filter_map(|i| match i {
1805 CommonsItem::Type(t) => Some(t.name.name.clone()),
1806 _ => None,
1807 })
1808 .collect();
1809 assert!(
1810 names.contains(&"B".to_string()),
1811 "B should be parsed after A's failure; got {names:?}"
1812 );
1813 assert!(!errors.is_empty(), "expected at least one parse error");
1814 }
1815
1816 #[test]
1831 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
1832 let src = "commons m {\n fn f() -> Int\n}\n";
1833 let (unit, errors) = parse_recover_str(src);
1834 assert!(unit.is_some(), "recovery should produce a partial AST");
1835 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
1836 assert_eq!(
1837 categories.len(),
1838 1,
1839 "one syntax error, no follow-on: {categories:?}"
1840 );
1841 assert!(
1842 !categories.contains(&"bynk.parse.unexpected_eof"),
1843 "the commons' closing brace must survive: {categories:?}"
1844 );
1845 }
1846
1847 #[test]
1852 fn recovery_skips_a_rejected_item_whole() {
1853 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
1854 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
1855 assert_eq!(
1856 recovered.errors.len(),
1857 1,
1858 "one syntax error, no follow-on: {:?}",
1859 recovered
1860 .errors
1861 .iter()
1862 .map(|e| e.category)
1863 .collect::<Vec<_>>()
1864 );
1865 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
1866 panic!("expected commons")
1867 };
1868 assert!(
1869 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
1870 "the `fn g` after the skipped agent must still parse"
1871 );
1872 assert_eq!(recovered.broken_decl_names, ["Counter"]);
1873 }
1874
1875 #[test]
1876 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1877 let src = "commons m {\n \
1878 fn f() -> Int {\n \
1879 match 1 {\n \
1880 is 1 -> { let z = }\n \
1881 is _ -> 2\n \
1882 }\n \
1883 }\n \
1884 fn g() -> Int { 2 }\n\
1885 }\n";
1886 let (unit, errors) = parse_recover_str(src);
1887 let unit = unit.expect("recovery should produce a partial AST");
1888 let SourceUnit::Commons(c) = unit else {
1889 panic!("expected commons")
1890 };
1891 let names: Vec<_> = c
1892 .items
1893 .iter()
1894 .filter_map(|i| match i {
1895 CommonsItem::Fn(f) => match &f.name {
1896 FnName::Free(id) => Some(id.name.clone()),
1897 _ => None,
1898 },
1899 _ => None,
1900 })
1901 .collect();
1902 assert_eq!(
1903 names,
1904 vec!["g".to_string()],
1905 "g must still be recovered as an item; got {names:?}"
1906 );
1907 assert!(
1908 !errors
1909 .iter()
1910 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1911 "the outer body's own closing brace must not be mistaken for \
1912 end-of-file: {errors:?}"
1913 );
1914 }
1915
1916 #[test]
1917 fn doc_block_attaches_to_type() {
1918 let c =
1919 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1920 .unwrap();
1921 let CommonsItem::Type(t) = &c.items[0] else {
1922 panic!()
1923 };
1924 assert!(t.documentation.is_some());
1925 assert!(
1926 t.documentation
1927 .as_ref()
1928 .unwrap()
1929 .contains("A descriptive doc.")
1930 );
1931 }
1932
1933 #[test]
1934 fn interpolated_string_parses_into_parts() {
1935 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1937 .unwrap();
1938 let CommonsItem::Fn(f) = &c.items[0] else {
1939 panic!("expected fn")
1940 };
1941 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1942 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1943 };
1944 assert_eq!(parts.len(), 3);
1945 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1946 assert!(
1947 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1948 );
1949 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1950 }
1951
1952 #[test]
1953 fn interpolated_hole_parses_a_full_expression() {
1954 let c =
1956 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1957 .unwrap();
1958 let CommonsItem::Fn(f) = &c.items[0] else {
1959 panic!("expected fn")
1960 };
1961 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1962 panic!("expected InterpStr")
1963 };
1964 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1965 }
1966
1967 #[test]
1968 fn empty_interpolation_hole_is_rejected() {
1969 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1970 assert!(
1971 errs.iter()
1972 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1973 "expected empty_interpolation; got {errs:?}"
1974 );
1975 }
1976
1977 #[test]
1978 fn interpolation_hole_lex_error_span_is_rebased() {
1979 let cases = [
1985 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1987 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1989 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1992 ];
1993 for src in cases {
1994 let errs = parse_str(src).unwrap_err();
1995 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1996 for e in &errs {
1997 assert!(
1998 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1999 "span {:?} splits a codepoint in {src:?}",
2000 e.span,
2001 );
2002 let hole_start = src.find("\\(").expect("case has a hole") + 2;
2005 assert!(
2006 e.span.start >= hole_start,
2007 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
2008 e.span,
2009 );
2010 }
2011 }
2012 }
2013
2014 #[test]
2015 fn fragment_form_parses() {
2016 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
2017 assert_eq!(c.form, CommonsForm::Fragment);
2018 assert_eq!(c.items.len(), 1);
2019 }
2020
2021 #[test]
2022 fn uses_parses() {
2023 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
2024 assert_eq!(c.uses.len(), 1);
2025 assert_eq!(c.uses[0].target.joined(), "other.lib");
2026 }
2027
2028 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
2029 let toks = tokenize(src).map_err(|e| vec![e])?;
2030 parse_unit(&toks, src)
2031 }
2032
2033 #[test]
2034 fn minimal_context_parses() {
2035 let u = parse_unit_str("context commerce.orders {}").unwrap();
2036 let SourceUnit::Context(c) = u else {
2037 panic!("expected context");
2038 };
2039 assert_eq!(c.name.joined(), "commerce.orders");
2040 assert!(c.items.is_empty());
2041 }
2042
2043 #[test]
2044 fn context_consumes_and_exports_parse() {
2045 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}";
2046 let u = parse_unit_str(src).unwrap();
2047 let SourceUnit::Context(c) = u else { panic!() };
2048 assert_eq!(c.uses.len(), 1);
2049 assert_eq!(c.consumes.len(), 1);
2050 assert_eq!(c.exports.len(), 2);
2051 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2052 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2053 }
2054
2055 #[test]
2056 fn context_fragment_form_parses() {
2057 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2058 let u = parse_unit_str(src).unwrap();
2059 let SourceUnit::Context(c) = u else { panic!() };
2060 assert_eq!(c.form, CommonsForm::Fragment);
2061 assert_eq!(c.uses.len(), 1);
2062 assert_eq!(c.consumes.len(), 1);
2063 assert_eq!(c.exports.len(), 1);
2064 }
2065
2066 #[test]
2067 fn opaque_type_parses() {
2068 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2069 let CommonsItem::Type(t) = &c.items[0] else {
2070 panic!()
2071 };
2072 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2073 }
2074
2075 #[test]
2076 fn empty_commons() {
2077 let c = parse_str("commons fitness.units {}").unwrap();
2078 assert_eq!(c.name.joined(), "fitness.units");
2079 assert!(c.items.is_empty());
2080 }
2081
2082 #[test]
2083 fn one_type_decl() {
2084 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2085 assert_eq!(c.items.len(), 1);
2086 let CommonsItem::Type(t) = &c.items[0] else {
2087 panic!()
2088 };
2089 assert_eq!(t.name.name, "Metres");
2090 match &t.body {
2091 TypeBody::Refined {
2092 base, refinement, ..
2093 } => {
2094 assert_eq!(*base, BaseType::Int);
2095 assert!(refinement.is_some());
2096 }
2097 _ => panic!("expected refined body"),
2098 }
2099 }
2100
2101 #[test]
2102 fn function_decl() {
2103 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2104 let CommonsItem::Fn(f) = &c.items[0] else {
2105 panic!()
2106 };
2107 assert_eq!(f.name.ident().name, "add");
2108 assert_eq!(f.params.len(), 2);
2109 }
2110
2111 #[test]
2112 fn chained_comparison_is_error() {
2113 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2114 .unwrap_err();
2115 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2116 }
2117
2118 #[test]
2119 fn chained_equality_is_error() {
2120 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2121 .unwrap_err();
2122 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2123 }
2124
2125 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2134 std::thread::Builder::new()
2135 .stack_size(64 * 1024 * 1024)
2136 .spawn(f)
2137 .unwrap()
2138 .join()
2139 .unwrap()
2140 }
2141
2142 #[test]
2143 fn deeply_nested_parens_are_bounded_not_overflowed() {
2144 let errs = on_big_stack(|| {
2150 let depth = crate::MAX_NESTING_DEPTH + 8;
2151 let src = format!(
2152 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2153 "(".repeat(depth),
2154 ")".repeat(depth),
2155 );
2156 parse_str(&src).unwrap_err()
2157 });
2158 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2159 }
2160
2161 #[test]
2162 fn deeply_nested_types_are_bounded_not_overflowed() {
2163 let errs = on_big_stack(|| {
2168 let depth = crate::MAX_NESTING_DEPTH + 8;
2169 let src = format!(
2170 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2171 "Result[Int, ".repeat(depth),
2172 "]".repeat(depth),
2173 );
2174 parse_str(&src).unwrap_err()
2175 });
2176 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2177 }
2178
2179 #[test]
2180 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2181 let errs = on_big_stack(|| {
2186 let depth = crate::MAX_NESTING_DEPTH + 8;
2187 let src = format!(
2188 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2189 "Ok(".repeat(depth),
2190 ")".repeat(depth),
2191 );
2192 parse_str(&src).unwrap_err()
2193 });
2194 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2195 }
2196
2197 #[test]
2198 fn nesting_below_the_limit_still_parses() {
2199 let ok = on_big_stack(|| {
2202 let depth = crate::MAX_NESTING_DEPTH - 8;
2203 let src = format!(
2204 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2205 "(".repeat(depth),
2206 ")".repeat(depth),
2207 );
2208 parse_str(&src).is_ok()
2209 });
2210 assert!(ok, "well-nested source under the limit should parse");
2211 }
2212
2213 #[test]
2214 fn let_statement_parses() {
2215 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2216 let CommonsItem::Fn(f) = &c.items[0] else {
2217 panic!()
2218 };
2219 assert_eq!(f.body.statements.len(), 1);
2220 match &f.body.statements[0] {
2221 Statement::Let(l) => {
2222 assert_eq!(l.name.name, "y");
2223 assert!(l.type_annot.is_none());
2224 }
2225 _ => panic!("expected a pure `let` statement"),
2226 }
2227 }
2228
2229 #[test]
2230 fn let_with_annotation() {
2231 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2232 let CommonsItem::Fn(f) = &c.items[0] else {
2233 panic!()
2234 };
2235 match &f.body.statements[0] {
2236 Statement::Let(l) => assert!(l.type_annot.is_some()),
2237 _ => panic!("expected a pure `let` statement"),
2238 }
2239 }
2240
2241 #[test]
2242 fn if_else_parses_as_expression() {
2243 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2244 let CommonsItem::Fn(f) = &c.items[0] else {
2245 panic!()
2246 };
2247 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2248 }
2249
2250 #[test]
2251 fn else_if_chain_parses() {
2252 let c = parse_str(
2253 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2254 )
2255 .unwrap();
2256 let CommonsItem::Fn(f) = &c.items[0] else {
2257 panic!()
2258 };
2259 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2260 panic!()
2261 };
2262 assert!(else_block.statements.is_empty());
2264 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2265 }
2266
2267 #[test]
2268 fn ok_and_err_parse_as_expressions() {
2269 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2270 let CommonsItem::Fn(f) = &c.items[0] else {
2271 panic!()
2272 };
2273 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2274
2275 let c =
2276 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2277 let CommonsItem::Fn(f) = &c.items[0] else {
2278 panic!()
2279 };
2280 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2281 }
2282
2283 #[test]
2284 fn question_postfix_parses() {
2285 let c = parse_str(
2286 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2287 )
2288 .unwrap();
2289 let CommonsItem::Fn(f) = &c.items[1] else {
2290 panic!()
2291 };
2292 let Statement::Let(l) = &f.body.statements[0] else {
2293 panic!("expected a pure `let` statement");
2294 };
2295 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2296 }
2297
2298 #[test]
2299 fn constructor_call_parses() {
2300 let c = parse_str(
2301 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2302 )
2303 .unwrap();
2304 let CommonsItem::Fn(f) = &c.items[1] else {
2305 panic!()
2306 };
2307 let ExprKind::MethodCall {
2310 receiver, method, ..
2311 } = &f.body.tail.kind
2312 else {
2313 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2314 };
2315 let ExprKind::Ident(id) = &receiver.kind else {
2316 panic!("expected receiver Ident");
2317 };
2318 assert_eq!(id.name, "T");
2319 assert_eq!(method.name, "of");
2320 }
2321
2322 #[test]
2323 fn result_type_ref_parses() {
2324 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2325 let CommonsItem::Fn(f) = &c.items[0] else {
2326 panic!()
2327 };
2328 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2329 }
2330
2331 #[test]
2332 fn result_missing_arg_count_errors() {
2333 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2334 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2335 }
2336
2337 #[test]
2338 fn field_access_parses_in_v0_2() {
2339 let c =
2342 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2343 let CommonsItem::Fn(f) = &c.items[1] else {
2344 panic!()
2345 };
2346 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2347 }
2348
2349 #[test]
2352 fn leading_line_comment_attaches_to_next_decl() {
2353 let src = "commons x {\n// explain the type\ntype T = Int where NonNegative\n}";
2354 let c = parse_str(src).unwrap();
2355 let CommonsItem::Type(t) = &c.items[0] else {
2356 panic!()
2357 };
2358 assert_eq!(
2359 t.trivia.leading,
2360 vec![Comment::Line(" explain the type".to_string())]
2361 );
2362 assert!(t.trivia.trailing.is_none());
2363 }
2364
2365 #[test]
2366 fn trailing_line_comment_attaches_to_prev_decl() {
2367 let src = "commons x {\ntype T = Int where NonNegative // trailing note\n}";
2368 let c = parse_str(src).unwrap();
2369 let CommonsItem::Type(t) = &c.items[0] else {
2370 panic!()
2371 };
2372 assert!(t.trivia.leading.is_empty());
2373 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2374 }
2375
2376 #[test]
2377 fn grouped_leading_comments_attach_together() {
2378 let src = "commons x {\n// one\n// two\n// three\ntype T = Int where Positive\n}";
2379 let c = parse_str(src).unwrap();
2380 let CommonsItem::Type(t) = &c.items[0] else {
2381 panic!()
2382 };
2383 assert_eq!(
2384 t.trivia.leading,
2385 vec![
2386 Comment::Line(" one".to_string()),
2387 Comment::Line(" two".to_string()),
2388 Comment::Line(" three".to_string())
2389 ],
2390 );
2391 }
2392
2393 #[test]
2394 fn comment_with_doc_block_keeps_both() {
2395 let src = "commons x {\n// intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2397 let c = parse_str(src).unwrap();
2398 let CommonsItem::Type(t) = &c.items[0] else {
2399 panic!()
2400 };
2401 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2402 assert_eq!(t.documentation.as_deref(), Some("docs"));
2403 }
2404
2405 #[test]
2406 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2407 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2413 let c = parse_str(src).unwrap();
2414 assert_eq!(c.name.joined(), "app.messages");
2415 }
2416
2417 #[test]
2418 fn messages_decl_parses_tag_annotation_and_entries() {
2419 let src = "commons app.messages {\n\
2421 // intro\n\
2422 ---\n\
2423 docs\n\
2424 ---\n\
2425 messages \"en\" @reference {\n\
2426 \"greeting\" => \"Hello, {name}!\"\n\
2427 \"farewell\" => \"Bye\"\n\
2428 } // trailing\n\
2429 }";
2430 let c = parse_str(src).unwrap();
2431 let CommonsItem::Messages(m) = &c.items[0] else {
2432 panic!("expected a messages item, got {:?}", c.items[0]);
2433 };
2434 assert_eq!(m.tag, "en");
2435 assert_eq!(m.annotations.len(), 1);
2436 assert_eq!(m.annotations[0].name.name, "reference");
2437 assert!(m.annotations[0].args.is_empty());
2438 assert_eq!(m.entries.len(), 2);
2439 assert_eq!(m.entries[0].code, "greeting");
2440 assert_eq!(m.entries[0].template, "Hello, {name}!");
2441 assert_eq!(m.entries[1].code, "farewell");
2442 assert_eq!(m.entries[1].template, "Bye");
2443 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2444 assert_eq!(m.documentation.as_deref(), Some("docs"));
2445 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2446 }
2447
2448 #[test]
2449 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2450 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2454 let c = parse_str(src).unwrap();
2455 let CommonsItem::Messages(m) = &c.items[0] else {
2456 panic!("expected a messages item, got {:?}", c.items[0]);
2457 };
2458 assert_eq!(m.tag, "en");
2459 assert!(m.annotations.is_empty());
2460 assert!(m.entries.is_empty());
2461 }
2462
2463 #[test]
2464 fn messages_decl_parses_syntactically_inside_a_context_too() {
2465 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2470 let toks = tokenize(src).unwrap();
2471 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2472 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2473 let Some(SourceUnit::Context(ctx)) = unit else {
2474 panic!("expected a context")
2475 };
2476 let CommonsItem::Messages(m) = &ctx.items[0] else {
2477 panic!("expected a messages item, got {:?}", ctx.items[0]);
2478 };
2479 assert_eq!(m.tag, "en");
2480 }
2481
2482 #[test]
2483 fn comment_before_let_statement_attaches() {
2484 let src = "commons x {\nfn f(n: Int) -> Int {\n// pick a value\nlet y = n + 1\ny\n}\n}";
2485 let c = parse_str(src).unwrap();
2486 let CommonsItem::Fn(f) = &c.items[0] else {
2487 panic!()
2488 };
2489 let Statement::Let(l) = &f.body.statements[0] else {
2490 panic!()
2491 };
2492 assert_eq!(
2493 l.trivia.leading,
2494 vec![Comment::Line(" pick a value".to_string())]
2495 );
2496 }
2497
2498 #[test]
2499 fn comment_before_tail_attaches_to_block_tail() {
2500 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n// result\ny\n}\n}";
2501 let c = parse_str(src).unwrap();
2502 let CommonsItem::Fn(f) = &c.items[0] else {
2503 panic!()
2504 };
2505 assert_eq!(
2506 f.body.tail_leading_comments,
2507 vec![Comment::Line(" result".to_string())],
2508 );
2509 }
2510
2511 #[test]
2517 fn contextual_keywords_are_valid_identifiers() {
2518 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2520 .expect("`on`/`suite`/`case` are valid field names");
2521 let CommonsItem::Type(_) = &c.items[0] else {
2522 panic!("expected a type decl")
2523 };
2524
2525 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2527 .expect("`on`/`case` are valid parameter names");
2528
2529 parse_str("commons demo {\n type R = { suite: Int }\n}")
2531 .expect("`suite` is a valid field name");
2532 }
2533
2534 #[test]
2542 fn is_reserved_keyword_covers_every_lexer_keyword() {
2543 let lexer_src = include_str!("lexer.rs");
2544 let mut words = Vec::new();
2545 for line in lexer_src.lines() {
2546 let t = line.trim();
2547 if let Some(rest) = t.strip_prefix("#[token(\"")
2548 && let Some(word) = rest.split('"').next()
2549 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2550 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2551 {
2552 words.push(word.to_string());
2553 }
2554 }
2555 assert!(
2556 words.len() > 30,
2557 "keyword extraction looks broken: only {} words",
2558 words.len()
2559 );
2560 use crate::keywords::RESERVED_CONTEXTUAL;
2563 let mut unclassified = Vec::new();
2564 for word in &words {
2565 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2566 let kind = tokens.first().expect("keyword yields a token").kind;
2567 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2568 unclassified.push(word.clone());
2569 }
2570 }
2571 assert!(
2572 unclassified.is_empty(),
2573 "keywords missing from is_reserved_keyword (add them, or document \
2574 them as contextual): {unclassified:?}"
2575 );
2576 }
2577
2578 #[test]
2589 fn is_item_start_matches_the_pinned_keyword_set() {
2590 use TokenKind::*;
2591 let expected_true = [
2592 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Uses, Consumes,
2593 Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case, Property,
2594 ];
2595 for kind in expected_true {
2596 assert!(is_item_start(kind), "{kind:?} must be an item start");
2597 }
2598 let expected_false = [
2599 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2600 Where, True, False, Match, Is, On, Given,
2601 ];
2602 for kind in expected_false {
2603 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2604 }
2605 }
2606
2607 #[test]
2612 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2613 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2614 let tokens = crate::lexer::tokenize(src).unwrap();
2615 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2616 assert!(unit.is_some(), "the commons header still parses");
2617 assert!(
2618 errors
2619 .iter()
2620 .any(|e| e.category == "bynk.capability.outside_context"),
2621 "the misplaced capability is reported: {errors:?}"
2622 );
2623 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2626 }
2627
2628 #[test]
2629 fn trailing_file_comment_becomes_unit_trailing() {
2630 let src = "commons x\n\ntype T = Int where Positive\n// afterword\n";
2634 let c = parse_str(src).unwrap();
2635 assert_eq!(
2636 c.trailing_comments,
2637 vec![Comment::Line(" afterword".to_string())]
2638 );
2639 }
2640
2641 #[test]
2642 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2643 let src = "commons x {\n type T = Int where Positive\n}\n// afterword\n";
2650 let c = parse_str(src).unwrap();
2651 assert_eq!(
2652 c.trailing_comments,
2653 vec![Comment::Line(" afterword".to_string())]
2654 );
2655 }
2656
2657 #[test]
2658 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2659 let src = "context x {\n type T = Int where Positive\n}\n// afterword\n";
2661 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2662 panic!("expected context");
2663 };
2664 assert_eq!(
2665 c.trailing_comments,
2666 vec![Comment::Line(" afterword".to_string())]
2667 );
2668 }
2669
2670 #[test]
2671 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2672 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n// afterword\n";
2674 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2675 panic!("expected suite");
2676 };
2677 assert_eq!(
2678 s.trailing_comments,
2679 vec![Comment::Line(" afterword".to_string())]
2680 );
2681 }
2682
2683 #[test]
2690 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2691 let src = "adapter x {\n binding \"./x.ts\"\n}\n// afterword\n";
2692 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2693 panic!("expected adapter");
2694 };
2695 assert_eq!(
2696 a.trailing_comments,
2697 vec![Comment::Line(" afterword".to_string())]
2698 );
2699 }
2700
2701 #[test]
2708 fn commons_fragment_rejects_uses_after_a_decl() {
2709 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
2710 let errs = parse_str(src).unwrap_err();
2711 assert!(
2712 errs.iter()
2713 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2714 "{errs:?}"
2715 );
2716 }
2717
2718 #[test]
2721 fn commons_brace_allows_uses_after_a_decl() {
2722 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2723 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2724 }
2725
2726 #[test]
2728 fn context_fragment_rejects_consumes_after_a_decl() {
2729 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2730 let errs = parse_unit_str(src).unwrap_err();
2731 assert!(
2732 errs.iter()
2733 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2734 "{errs:?}"
2735 );
2736 }
2737
2738 #[test]
2740 fn context_fragment_rejects_exports_after_a_decl() {
2741 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2742 let errs = parse_unit_str(src).unwrap_err();
2743 assert!(
2744 errs.iter()
2745 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2746 "{errs:?}"
2747 );
2748 }
2749
2750 #[test]
2752 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2753 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2754 parse_unit_str(src)
2755 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2756 }
2757
2758 #[test]
2760 fn test_fragment_rejects_uses_after_a_decl() {
2761 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2762 let errs = parse_unit_str(src).unwrap_err();
2763 assert!(
2764 errs.iter()
2765 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2766 "{errs:?}"
2767 );
2768 }
2769
2770 #[test]
2772 fn test_brace_allows_uses_after_a_decl() {
2773 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2774 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2775 }
2776
2777 fn body_tail(body: &str) -> ExprKind {
2781 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2782 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2783 let CommonsItem::Fn(f) = &c.items[0] else {
2784 panic!("expected fn, got {:?}", c.items[0]);
2785 };
2786 f.body.tail.kind.clone()
2787 }
2788
2789 fn body_err(body: &str) -> Vec<CompileError> {
2790 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2791 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2792 }
2793
2794 #[test]
2795 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2796 for src in [
2799 "if ready { result } else { fallback }",
2800 "if ready { fallback } else { result }",
2801 "if !ready { result } else { fallback }",
2802 "if a == b { result } else { fallback }",
2803 "if a && b { result } else { fallback }",
2804 ] {
2805 let ExprKind::If {
2806 then_block,
2807 else_block,
2808 ..
2809 } = body_tail(src)
2810 else {
2811 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2812 };
2813 assert!(
2816 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2817 "then-branch tail not an ident for {src:?}: {:?}",
2818 then_block.tail.kind,
2819 );
2820 assert!(
2821 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2822 "else-branch tail not an ident for {src:?}: {:?}",
2823 else_block.tail.kind,
2824 );
2825 }
2826 }
2827
2828 #[test]
2829 fn else_less_if_with_single_ident_branch_parses() {
2830 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2832 panic!("expected If");
2833 };
2834 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2835 }
2836
2837 #[test]
2838 fn record_construction_still_parses_in_value_position() {
2839 assert!(matches!(
2842 body_tail("Point { x }"),
2843 ExprKind::RecordConstruction { .. }
2844 ));
2845 assert!(matches!(
2846 body_tail("Point { x: 1, y: 2 }"),
2847 ExprKind::RecordConstruction { .. }
2848 ));
2849 assert!(matches!(
2850 body_tail("Empty {}"),
2851 ExprKind::RecordConstruction { .. }
2852 ));
2853 }
2854
2855 #[test]
2856 fn parenthesised_record_is_allowed_in_condition_head() {
2857 let ExprKind::If { cond, .. } =
2860 body_tail("if (ready { result }) { branch } else { other }")
2861 else {
2862 panic!("expected If");
2863 };
2864 let ExprKind::Paren(inner) = &cond.kind else {
2865 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2866 };
2867 assert!(
2868 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2869 "parenthesised record in condition head should still construct: {:?}",
2870 inner.kind,
2871 );
2872 }
2873
2874 #[test]
2875 fn record_in_call_arg_within_condition_still_constructs() {
2876 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2879 else {
2880 panic!("expected If");
2881 };
2882 let ExprKind::Call { args, .. } = &cond.kind else {
2883 panic!("expected Call in condition, got {:?}", cond.kind);
2884 };
2885 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2886 }
2887
2888 #[test]
2889 fn safe_condition_shapes_are_unaffected() {
2890 assert!(matches!(
2892 body_tail("if ready == true { result } else { fallback }"),
2893 ExprKind::If { .. }
2894 ));
2895 assert!(matches!(
2896 body_tail("if (ready) { result } else { fallback }"),
2897 ExprKind::If { .. }
2898 ));
2899 assert!(matches!(
2900 body_tail("if ready { \"a\" } else { \"b\" }"),
2901 ExprKind::If { .. }
2902 ));
2903 }
2904
2905 fn body_block(body: &str) -> Block {
2906 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2907 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2908 let CommonsItem::Fn(f) = &c.items[0] else {
2909 panic!("expected fn, got {:?}", c.items[0]);
2910 };
2911 f.body.clone()
2912 }
2913
2914 fn has_error(errs: &[CompileError], code: &str) -> bool {
2915 errs.iter().any(|e| e.category == code)
2916 }
2917
2918 #[test]
2919 fn let_else_desugars_to_a_hidden_binding_and_a_match() {
2920 let b = body_block("let x = 1\n let Some(n) = find(x) else { 0 }\n n + x");
2923 assert_eq!(
2924 b.statements.len(),
2925 2,
2926 "the plain let and the hidden binding"
2927 );
2928 let Statement::Let(hidden) = &b.statements[1] else {
2929 panic!("expected a pure hidden binding, got {:?}", b.statements[1]);
2930 };
2931 assert!(
2932 is_generated_binding(&hidden.name.name),
2933 "{}",
2934 hidden.name.name
2935 );
2936 assert_eq!(
2937 hidden.name.span.start, hidden.name.span.end,
2938 "the binder has no extent"
2939 );
2940 let le = hidden
2941 .let_else
2942 .as_ref()
2943 .expect("the binding records the sugar");
2944 assert_eq!(le.form, LetElseForm::Block);
2945 let ExprKind::Match { discriminant, arms } = &b.tail.kind else {
2946 panic!("expected a match tail, got {:?}", b.tail.kind);
2947 };
2948 assert!(matches!(&discriminant.kind, ExprKind::Ident(i) if i.name == hidden.name.name));
2949 assert_eq!(arms.len(), 2);
2950 assert!(matches!(arms[0].pattern, Pattern::Variant { .. }));
2951 let MatchBody::Block(rest) = &arms[0].body else {
2952 panic!("the pattern's arm runs the rest of the block");
2953 };
2954 assert!(rest.statements.is_empty());
2955 assert!(
2956 !matches!(rest.tail.kind, ExprKind::UnitLit),
2957 "{:?}",
2958 rest.tail.kind
2959 );
2960 assert!(matches!(arms[1].pattern, Pattern::Wildcard(_)));
2961 }
2962
2963 #[test]
2964 fn effectful_let_else_binds_with_an_arrow() {
2965 let b = body_block("let Ok(v) <- load() else { Effect.pure(0) }\n v");
2966 assert!(matches!(&b.statements[0], Statement::EffectLet(l) if l.let_else.is_some()));
2967 }
2968
2969 #[test]
2970 fn let_else_chain_nests_each_step_in_the_previous_arm() {
2971 let b = body_block(
2974 "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",
2975 );
2976 let ExprKind::Match { arms, .. } = &b.tail.kind else {
2977 panic!("expected a match tail");
2978 };
2979 let MatchBody::Block(second) = &arms[0].body else {
2980 panic!("expected a block arm");
2981 };
2982 assert_eq!(
2983 second.statements.len(),
2984 2,
2985 "`let y` and the second hidden binding"
2986 );
2987 assert!(matches!(&second.statements[0], Statement::Let(l) if l.name.name == "y"));
2988 let ExprKind::Match { arms, .. } = &second.tail.kind else {
2989 panic!("expected the second step's match");
2990 };
2991 let MatchBody::Block(third) = &arms[0].body else {
2992 panic!("expected a block arm");
2993 };
2994 let ExprKind::Match { arms, .. } = &third.tail.kind else {
2995 panic!("expected the third step's match");
2996 };
2997 let MatchBody::Block(last) = &arms[0].body else {
2998 panic!("expected a block arm");
2999 };
3000 assert!(matches!(&last.tail.kind, ExprKind::Ident(i) if i.name == "c"));
3001 }
3002
3003 #[test]
3004 fn else_match_arms_follow_the_patterns_arm() {
3005 let b = body_block("let Ok(v) = r else match { Err(e) => 0 }\n v");
3006 let Statement::Let(l) = &b.statements[0] else {
3007 panic!("expected the hidden binding");
3008 };
3009 assert_eq!(l.let_else.as_ref().unwrap().form, LetElseForm::Match);
3010 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3011 panic!("expected a match tail");
3012 };
3013 assert_eq!(arms.len(), 2);
3014 assert!(
3015 matches!(&arms[1].pattern, Pattern::Variant { variant, .. } if variant.name == "Err")
3016 );
3017 }
3018
3019 #[test]
3020 fn let_else_as_the_last_statement_leaves_a_unit_rest() {
3021 let b = body_block("let Some(n) = find(1) else { () }");
3022 let ExprKind::Match { arms, .. } = &b.tail.kind else {
3023 panic!("expected a match tail");
3024 };
3025 let MatchBody::Block(rest) = &arms[0].body else {
3026 panic!("expected a block arm");
3027 };
3028 assert!(rest.implicit_tail && rest.statements.is_empty());
3029 }
3030
3031 #[test]
3032 fn let_pattern_forms_are_recognised() {
3033 for src in [
3034 "let None = o else { 0 }\n 1",
3035 "let Status.Done = s else { 0 }\n 1",
3036 "let Shipped(at) = s else { 0 }\n at",
3037 "let 0 = n else { 1 }\n 0",
3038 "let -1 = n else { 1 }\n 0",
3039 "let \"a\" = s else { 1 }\n 0",
3040 "let (Ok(x) | Err(x)) = r else { 1 }\n x",
3041 ] {
3042 let b = body_block(src);
3043 assert!(
3044 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_some()),
3045 "{src:?} should parse as a let-else"
3046 );
3047 }
3048 let b = body_block("let Total = 1\n Total");
3050 assert!(
3051 matches!(&b.statements[0], Statement::Let(l) if l.let_else.is_none() && l.name.name == "Total")
3052 );
3053 }
3054
3055 #[test]
3056 fn let_pattern_without_else_is_refused() {
3057 assert!(has_error(
3058 &body_err("let Ok(v) = r\n v"),
3059 "bynk.parse.let_pattern_without_else"
3060 ));
3061 assert!(has_error(
3062 &body_err("let Ok(v) <- r()\n v"),
3063 "bynk.parse.let_pattern_without_else"
3064 ));
3065 }
3066
3067 #[test]
3068 fn else_on_a_plain_binding_is_refused() {
3069 assert!(has_error(
3070 &body_err("let v = r else { 0 }\n v"),
3071 "bynk.parse.let_else_on_binding"
3072 ));
3073 assert!(has_error(
3074 &body_err("let v <- r() else { 0 }\n v"),
3075 "bynk.parse.let_else_on_binding"
3076 ));
3077 }
3078
3079 #[test]
3080 fn annotated_let_pattern_is_refused() {
3081 assert!(has_error(
3082 &body_err("let Ok(v): Int = r else { 0 }\n v"),
3083 "bynk.parse.let_pattern_annotation"
3084 ));
3085 }
3086
3087 #[test]
3088 fn empty_else_match_is_refused() {
3089 assert!(has_error(
3090 &body_err("let Ok(v) = r else match { }\n v"),
3091 "bynk.parse.empty_match"
3092 ));
3093 }
3094
3095 #[test]
3096 fn let_else_steps_count_against_the_nesting_budget() {
3097 let chain = |n: usize| -> String {
3101 let steps: String = (0..n)
3102 .map(|i| format!("let Ok(v{i}) = step() else {{ {i} }}\n "))
3103 .collect();
3104 format!("{steps}0")
3105 };
3106 body_block(&chain(40));
3107 let errs = body_err(&chain(400));
3108 assert!(
3109 errs.iter()
3110 .any(|e| e.category == "bynk.parse.nesting_too_deep"),
3111 "{errs:?}"
3112 );
3113 }
3114
3115 #[test]
3116 fn empty_match_reports_its_own_diagnostic() {
3117 let errs = body_err("match result {}");
3121 assert!(
3122 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
3123 "expected empty_match; got {errs:?}",
3124 );
3125 }
3126
3127 #[test]
3128 fn match_discriminant_ending_in_ident_parses() {
3129 assert!(matches!(
3131 body_tail("match ready { x => x }"),
3132 ExprKind::Match { .. }
3133 ));
3134 }
3135
3136 #[test]
3145 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
3146 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
3147 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3148 let CommonsItem::Fn(f) = &c.items[0] else {
3149 panic!("expected fn, got {:?}", c.items[0]);
3150 };
3151 assert_eq!(
3152 f.body.statements.len(),
3153 1,
3154 "expected exactly one Assign statement, got {:?}",
3155 f.body.statements
3156 );
3157 let Statement::Assign(a) = &f.body.statements[0] else {
3158 panic!(
3159 "expected an Assign statement, got {:?}",
3160 f.body.statements[0]
3161 );
3162 };
3163 assert!(
3164 matches!(a.value.kind, ExprKind::Ident(_)),
3165 "assign value must stay the bare identifier `Paid`, got {:?}",
3166 a.value.kind
3167 );
3168 assert!(
3169 matches!(f.body.tail.kind, ExprKind::UnitLit),
3170 "the `()` must remain the block's own tail, got {:?}",
3171 f.body.tail.kind
3172 );
3173 }
3174
3175 #[test]
3180 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
3181 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
3182 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
3183 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
3184 let CommonsItem::Fn(f) = &c.items[0] else {
3185 panic!("expected fn, got {:?}", c.items[0]);
3186 };
3187 let Statement::Let(l) = &f.body.statements[0] else {
3188 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
3189 };
3190 let ExprKind::Is { pattern, .. } = &l.value.kind else {
3191 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
3192 };
3193 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
3194 panic!("{name}: expected a variant pattern, got {pattern:?}");
3195 };
3196 assert_eq!(
3197 span.end, variant.span.end,
3198 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
3199 );
3200 assert!(
3201 matches!(f.body.tail.kind, ExprKind::UnitLit),
3202 "{name}: the `()` must remain the block's own tail, got {:?}",
3203 f.body.tail.kind
3204 );
3205 }
3206 }
3207
3208 #[test]
3211 fn variant_pattern_payload_on_the_same_line_still_binds() {
3212 for (name, pat) in [
3213 ("user variant", "Paid(\n x\n )"),
3214 ("built-in variant", "Some(x)"),
3215 ] {
3216 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
3217 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
3218 let CommonsItem::Fn(f) = &c.items[0] else {
3219 panic!("expected fn, got {:?}", c.items[0]);
3220 };
3221 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
3222 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
3223 };
3224 assert!(
3225 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
3226 "{name}: expected one binding, got {pattern:?}"
3227 );
3228 }
3229 }
3230
3231 #[test]
3237 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
3238 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
3239 assert!(
3240 parse_str(src).is_err(),
3241 "a next-line `(` after an arm's variant must not open its payload list"
3242 );
3243 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
3244 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
3245 }
3246
3247 #[test]
3251 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
3252 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
3253 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
3254 let CommonsItem::Fn(f) = &c.items[0] else {
3255 panic!("expected fn, got {:?}", c.items[0]);
3256 };
3257 let Statement::Let(l) = &f.body.statements[0] else {
3258 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
3259 };
3260 assert!(
3261 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
3262 "let value must stay a field access, got {:?}",
3263 l.value.kind
3264 );
3265 assert!(
3266 matches!(f.body.tail.kind, ExprKind::UnitLit),
3267 "the `()` must remain the block's own tail, got {:?}",
3268 f.body.tail.kind
3269 );
3270 }
3271
3272 #[test]
3273 fn unparenthesised_record_in_condition_head_now_errors() {
3274 assert!(
3280 !body_err("match Point { x: 1 } { p => p }").is_empty(),
3281 "unparenthesised record discriminant should not parse",
3282 );
3283 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
3285 else {
3286 panic!("expected Match for the parenthesised form");
3287 };
3288 let ExprKind::Paren(inner) = &discriminant.kind else {
3289 panic!(
3290 "expected a parenthesised discriminant, got {:?}",
3291 discriminant.kind
3292 );
3293 };
3294 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
3295 }
3296
3297 fn strict_codes(src: &str) -> Vec<&'static str> {
3299 let toks = tokenize(src).unwrap();
3300 match parse_units_with_warnings(&toks, src) {
3301 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
3302 Err(errs) => errs.iter().map(|e| e.category).collect(),
3303 }
3304 }
3305
3306 #[test]
3310 fn a_divider_pair_around_code_is_doc_block_contains_code() {
3311 let src =
3312 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
3313 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
3314 let errs = parse_str(src).unwrap_err();
3315 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
3316 assert_eq!(
3318 &src[errs[0].span.range()],
3319 "--------\nfn helper() -> Int { 1 }\n--------\n"
3320 );
3321 let (unit, errs) = parse_recover_str(src);
3322 assert!(unit.is_some(), "recovery keeps the unit");
3323 assert!(
3324 errs.iter()
3325 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
3326 "{errs:?}"
3327 );
3328 }
3329
3330 #[test]
3333 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
3334 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
3335 let codes = strict_codes(orphan);
3336 assert!(
3337 codes.contains(&"bynk.parse.doc_block_contains_code"),
3338 "{codes:?}"
3339 );
3340 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
3341 let codes = strict_codes(trailing);
3342 assert!(
3343 codes.contains(&"bynk.parse.doc_block_contains_code"),
3344 "{codes:?}"
3345 );
3346 }
3347
3348 #[test]
3352 fn near_miss_prose_and_fenced_code_are_not_code() {
3353 for doc in [
3354 "type of the thing",
3355 "fn is used here",
3356 "The type Id is an Int.",
3357 "uses the clock",
3358 "Event handler.\nfn helper() -> Int { 1 }",
3359 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
3360 "~~~~\ntype Id = Int\n~~~~",
3361 "---\nA nested doc.\n---",
3362 "",
3363 ] {
3364 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3365 }
3366 for doc in [
3367 "fn helper() -> Int { 1 }",
3368 "type Id = Int\nfn f() -> Int { 1 }",
3369 "```\nan example\n```\nfn helper() -> Int { 1 }",
3370 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
3371 ] {
3372 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
3373 }
3374 }
3375
3376 #[test]
3380 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
3381 let nested =
3382 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
3383 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
3384 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
3385 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
3386 assert!(doc_content_is_code(
3388 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
3389 ));
3390 for doc in [
3391 "---\nAn inner doc.\n---",
3392 "Intro.\n\n---\n\nfn is used here",
3393 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
3394 ] {
3395 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3396 }
3397 let mut deep = "fn helper() -> Int { 1 }".to_string();
3399 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
3400 let fence = "-".repeat(n);
3401 deep = format!("{fence}\n{deep}\n{fence}");
3402 }
3403 assert!(!doc_content_is_code(&deep));
3404 }
3405
3406 #[test]
3409 fn a_fenced_nested_doc_example_parses_clean() {
3410 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";
3411 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3412 let c = parse_str(src).unwrap();
3413 let CommonsItem::Fn(f) = &c.items[0] else {
3414 panic!("expected fn")
3415 };
3416 assert!(
3417 f.documentation
3418 .as_deref()
3419 .is_some_and(|d| d.contains("---\nInner.\n---")),
3420 "{:?}",
3421 f.documentation
3422 );
3423 }
3424
3425 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
3427 let CommonsItem::Type(t) = &c.items[i] else {
3428 panic!("expected a type at {i}")
3429 };
3430 t
3431 }
3432
3433 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
3434 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
3437 }
3438
3439 #[test]
3442 fn doc_lines_document_a_declaration() {
3443 let src = "commons m\n\n--- A commit SHA.\ntype Commit = String\n\n---- First. ----\nfn f() -> Int { 1 }\n";
3444 let c = parse_str(src).unwrap();
3445 assert_eq!(
3446 doc_of(&type_at(&c, 0).documentation),
3447 Some(("A commit SHA.", DocForm::Lines))
3448 );
3449 let CommonsItem::Fn(f) = &c.items[1] else {
3450 panic!("expected fn")
3451 };
3452 assert_eq!(doc_of(&f.documentation), Some(("First.", DocForm::Lines)));
3453 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3454 }
3455
3456 #[test]
3460 fn legacy_spellings_parse_and_are_each_reported() {
3461 let src =
3462 "-- a header\ncommons m\n\n--| A run.\ntype Run = {\n repo: String, -- trailing\n}\n";
3463 assert_eq!(
3464 strict_codes(src),
3465 [
3466 "bynk.lex.legacy_comment",
3467 "bynk.lex.legacy_doc_line",
3468 "bynk.lex.legacy_comment"
3469 ]
3470 );
3471 let tokens = tokenize(src).unwrap();
3472 let (units, _, _) = parse_units_migrating(&tokens, src).unwrap();
3473 let SourceUnit::Commons(c) = &units[0] else {
3474 panic!("commons")
3475 };
3476 assert_eq!(
3477 doc_of(&type_at(c, 0).documentation),
3478 Some(("A run.", DocForm::Lines))
3479 );
3480 }
3481
3482 #[test]
3485 fn consecutive_doc_lines_are_multiple_docs_with_a_block_fix() {
3486 let src = "commons m\n\ntype R = {\n --- First.\n --- Second.\n a: Int,\n}\n";
3487 let tokens = tokenize(src).unwrap();
3488 let errs = parse_units(&tokens, src).unwrap_err();
3489 assert_eq!(errs.len(), 1, "{errs:?}");
3490 assert_eq!(errs[0].category, "bynk.parse.multiple_docs");
3491 let (span, block) = &errs[0].suggestions[0].edits[0];
3492 assert_eq!(&src[span.range()], "--- First.\n --- Second.");
3493 assert_eq!(block, "---\n First.\n Second.\n ---");
3494 }
3495
3496 #[test]
3500 fn members_take_docs_in_either_form() {
3501 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";
3502 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3503 let c = parse_str(src).unwrap();
3504 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3505 panic!("record")
3506 };
3507 assert_eq!(
3508 doc_of(&r.fields[0].documentation),
3509 Some(("The repository.", DocForm::Lines))
3510 );
3511 assert_eq!(
3512 doc_of(&r.fields[1].documentation),
3513 Some(("The version.", DocForm::Block))
3514 );
3515 assert_eq!(r.fields[2].documentation, None);
3516 assert_eq!(
3517 r.fields[2].trivia.leading,
3518 vec![Comment::Line(" a note".into())]
3519 );
3520 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some(" trailing"));
3521 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
3522 panic!("enum")
3523 };
3524 assert_eq!(
3525 doc_of(&e.variants[0].documentation),
3526 Some(("Passed.", DocForm::Lines))
3527 );
3528 assert_eq!(e.variants[1].documentation, None);
3529 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
3530 panic!("sum")
3531 };
3532 assert_eq!(
3533 doc_of(&p.variants[0].documentation),
3534 Some(("Round.", DocForm::Lines))
3535 );
3536 assert_eq!(
3537 doc_of(&p.variants[0].payload[0].documentation),
3538 Some(("The radius.", DocForm::Lines))
3539 );
3540 assert_eq!(p.variants[1].documentation, None);
3541 let CommonsItem::Fn(f) = &c.items[3] else {
3543 panic!("expected fn")
3544 };
3545 assert_eq!(
3546 doc_of(&f.documentation),
3547 Some(("After the sum.", DocForm::Lines))
3548 );
3549 let CommonsItem::Messages(m) = &c.items[4] else {
3550 panic!("expected messages")
3551 };
3552 assert_eq!(
3553 doc_of(&m.entries[0].documentation),
3554 Some(("Greets.", DocForm::Lines))
3555 );
3556 }
3557
3558 #[test]
3562 fn two_docs_on_one_target_is_multiple_docs() {
3563 for src in [
3564 "commons m\n\n---\nBlock.\n---\n--- Lines.\nfn f() -> Int { 1 }\n",
3565 "commons m\n\n--- Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
3566 "commons m\n\n---\nOne.\n---\n---\nTwo.\n---\nfn f() -> Int { 1 }\n",
3567 "commons m\n\n--- One.\n--- Two.\nfn f() -> Int { 1 }\n",
3568 "commons m\n\ntype R = {\n --- Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
3569 ] {
3570 assert_eq!(strict_codes(src), ["bynk.parse.multiple_docs"], "{src}");
3571 let (unit, errs) = parse_recover_str(src);
3572 assert!(unit.is_some());
3573 assert!(
3574 errs.iter()
3575 .any(|e| e.category == "bynk.parse.multiple_docs")
3576 );
3577 }
3578 }
3579
3580 #[test]
3583 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
3584 let src =
3585 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --- dangling\n )\n | Dot\n";
3586 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
3587 let c = parse_str(src).unwrap();
3588 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
3589 panic!("sum")
3590 };
3591 assert_eq!(s.variants[0].payload.len(), 1);
3592 assert_eq!(s.variants[0].payload[0].documentation, None);
3593 }
3594
3595 #[test]
3598 fn an_unattached_member_doc_is_an_orphan() {
3599 let src = "commons m\n\ntype R = {\n --- Spaced.\n\n a: Int,\n --- Dangling.\n}\n";
3600 assert_eq!(
3601 strict_codes(src),
3602 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
3603 );
3604 let c = parse_str(src).unwrap();
3605 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3606 panic!("record")
3607 };
3608 assert_eq!(r.fields[0].documentation, None);
3609 assert!(matches!(
3610 &r.fields[0].trivia.leading[..],
3611 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
3612 ));
3613 assert!(matches!(
3614 &r.trailing_comments[..],
3615 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
3616 ));
3617 }
3618}