1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{
11 Token, TokenKind, comment_body, doc_block_content, doc_line_text, has_blank_line_between,
12};
13use crate::span::Span;
14mod declarations;
15mod expressions;
16mod statements;
17mod types;
18
19#[derive(Debug, Default)]
28struct TriviaTable {
29 leading: Vec<Vec<Comment>>,
35 trailing: Vec<Option<String>>,
40 epilogue: Vec<Comment>,
44}
45
46impl TriviaTable {
47 fn take_leading(&mut self, index: usize) -> Vec<Comment> {
48 match self.leading.get_mut(index) {
49 Some(v) => std::mem::take(v),
50 None => Vec::new(),
51 }
52 }
53
54 fn take_trailing(&mut self, index: usize) -> Option<String> {
55 self.trailing.get_mut(index).and_then(|s| s.take())
56 }
57
58 fn take_epilogue(&mut self) -> Vec<Comment> {
59 std::mem::take(&mut self.epilogue)
60 }
61
62 fn is_fully_drained(&self) -> bool {
79 self.leading.iter().all(Vec::is_empty)
80 && self.trailing.iter().all(Option::is_none)
81 && self.epilogue.is_empty()
82 }
83
84 fn epilogue_is_empty(&self) -> bool {
92 self.epilogue.is_empty()
93 }
94}
95
96fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
107 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
108 let mut table = TriviaTable::default();
109 let mut pending_leading: Vec<Comment> = Vec::new();
110 let mut last_content_end: Option<usize> = None;
111 let mut last_leading_end: Option<usize> = None;
118 for tok in tokens {
119 if !pending_leading.is_empty()
120 && let Some(end) = last_leading_end
121 && source[end..tok.span.start].matches('\n').count() >= 2
122 {
123 pending_leading.push(Comment::Blank);
124 }
125 if tok.kind == TokenKind::Comment {
126 let body = comment_body(source, tok.span).to_string();
127 if pending_leading.is_empty()
138 && filtered.last().is_some_and(|t| {
139 !matches!(
140 t.kind,
141 TokenKind::DocBlock | TokenKind::DocLine | TokenKind::LBrace
142 )
143 })
144 && let Some(prev_end) = last_content_end
145 && !source[prev_end..tok.span.start].contains('\n')
146 {
147 let last_idx = filtered.len() - 1;
148 if table.trailing[last_idx].is_none() {
151 table.trailing[last_idx] = Some(body);
152 continue;
153 }
154 }
155 let own_line = !pending_leading.is_empty()
159 || filtered
160 .last()
161 .is_none_or(|t| matches!(t.kind, TokenKind::DocBlock | TokenKind::DocLine))
162 || last_content_end.is_some_and(|end| source[end..tok.span.start].contains('\n'));
163 pending_leading.push(Comment::Line(body));
164 last_leading_end = own_line.then_some(tok.span.end);
165 continue;
166 }
167 filtered.push(*tok);
168 table.leading.push(std::mem::take(&mut pending_leading));
169 table.trailing.push(None);
170 last_content_end = Some(tok.span.end);
171 last_leading_end = None;
172 }
173 table.epilogue = pending_leading;
174 (filtered, table)
175}
176
177pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
183 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
184}
185
186pub fn parse_with_warnings(
189 tokens: &[Token],
190 source: &str,
191) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
192 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
193 match unit {
194 SourceUnit::Commons(c) => Ok((c, warnings)),
195 SourceUnit::Context(ctx) => Err(vec![
196 CompileError::new(
197 "bynk.parse.unexpected_context",
198 ctx.span,
199 "expected a `commons` declaration but found a `context` declaration",
200 )
201 .with_note(
202 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
203 ),
204 ]),
205 SourceUnit::Suite(t) => Err(vec![
206 CompileError::new(
207 "bynk.parse.unexpected_suite",
208 t.span,
209 "expected a `commons` declaration but found a `suite` declaration",
210 )
211 .with_note(
212 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
213 ),
214 ]),
215 SourceUnit::Adapter(a) => Err(vec![
216 CompileError::new(
217 "bynk.parse.unexpected_adapter",
218 a.span,
219 "expected a `commons` declaration but found an `adapter` declaration",
220 )
221 .with_note(
222 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
223 ),
224 ]),
225 }
226}
227
228pub fn parse_unit_with_recovery(
243 tokens: &[Token],
244 source: &str,
245) -> (Option<SourceUnit>, Vec<CompileError>) {
246 let (units, errors) = parse_units_with_recovery(tokens, source);
247 (units.into_iter().next(), errors)
248}
249
250pub fn parse_units_with_recovery(
255 tokens: &[Token],
256 source: &str,
257) -> (Vec<SourceUnit>, Vec<CompileError>) {
258 let recovered = parse_units_recovering(tokens, source);
259 (recovered.units, recovered.errors)
260}
261
262#[derive(Debug)]
265pub struct Recovered {
266 pub units: Vec<SourceUnit>,
267 pub errors: Vec<CompileError>,
268 pub broken_decl_names: Vec<String>,
271}
272
273pub fn merge_syntax_errors(
280 strict: Vec<CompileError>,
281 recovered: Vec<CompileError>,
282) -> Vec<CompileError> {
283 let mut out = strict;
284 for e in recovered {
285 if !out.iter().any(|o| o.span.start == e.span.start) {
286 out.push(e);
287 }
288 }
289 out
290}
291
292pub fn parse_units_recovering(tokens: &[Token], source: &str) -> Recovered {
295 parse_units_recovering_from(tokens, source, &mut 0)
296}
297
298pub fn parse_units_recovering_from(tokens: &[Token], source: &str, next_id: &mut u32) -> Recovered {
304 let (filtered, trivia) = split_trivia(tokens, source);
305 let mut warnings = Vec::new();
306 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
307 p.recover_mode = true;
308 p.next_expr_id = *next_id;
309 let mut units = Vec::new();
310 loop {
311 match p.parse_unit() {
312 Ok(u) => units.push(u),
313 Err(e) => {
314 p.recovered_errors.push(e);
315 break;
316 }
317 }
318 if p.peek().is_none() {
324 break;
325 }
326 }
327 let broken_decl_names = std::mem::take(&mut p.broken_decl_names);
328 *next_id = p.next_expr_id;
329 let mut all_errors = p.recovered_errors;
330 all_errors.append(&mut p.doc_errors);
331 all_errors.append(&mut warnings);
332 Recovered {
333 units,
334 errors: all_errors,
335 broken_decl_names,
336 }
337}
338
339pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
343 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
344}
345
346pub fn parse_unit_with_warnings(
349 tokens: &[Token],
350 source: &str,
351) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
352 parse_unit_with_warnings_from(tokens, source, &mut 0)
353}
354
355pub fn parse_unit_with_warnings_from(
369 tokens: &[Token],
370 source: &str,
371 next_id: &mut u32,
372) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
373 let (filtered, trivia) = split_trivia(tokens, source);
374 let mut warnings = Vec::new();
375 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
376 p.next_expr_id = *next_id;
377 let result = match p.parse_unit() {
378 Ok(u) => {
379 if let Some(extra) = p.peek() {
380 Err(vec![
381 CompileError::new(
382 "bynk.parse.extra_tokens",
383 extra.span,
384 "unexpected token after top-level declaration",
385 )
386 .with_note(
387 "a `.bynk` file contains exactly one `commons` or `context` declaration",
388 ),
389 ])
390 } else {
391 Ok(u)
392 }
393 }
394 Err(e) => Err(vec![e]),
395 };
396 let result = match (result, std::mem::take(&mut p.doc_errors)) {
399 (r, docs) if docs.is_empty() => r,
400 (Ok(_), docs) => Err(docs),
401 (Err(mut errs), mut docs) => {
402 errs.append(&mut docs);
403 Err(errs)
404 }
405 };
406 *next_id = p.next_expr_id;
407 match result {
410 Ok(u) => {
411 debug_assert!(
414 p.trivia.epilogue_is_empty(),
415 "a file-trailing comment was left undrained after a successful parse"
416 );
417 Ok((u, warnings))
418 }
419 Err(mut errs) => {
420 errs.append(&mut warnings);
421 Err(errs)
422 }
423 }
424}
425
426pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
435 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
436}
437
438pub fn parse_units_with_warnings(
444 tokens: &[Token],
445 source: &str,
446) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
447 parse_units_with_drain_check(tokens, source)
448 .map(|(units, warnings, _drained)| (units, warnings))
449}
450
451pub fn parse_units_with_warnings_from(
458 tokens: &[Token],
459 source: &str,
460 next_id: &mut u32,
461) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
462 parse_units_with_drain_check_from(tokens, source, next_id)
463 .map(|(units, warnings, _drained)| (units, warnings))
464}
465
466pub fn parse_units_with_drain_check(
477 tokens: &[Token],
478 source: &str,
479) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
480 parse_units_with_drain_check_from(tokens, source, &mut 0)
481}
482
483pub fn parse_units_with_drain_check_from(
486 tokens: &[Token],
487 source: &str,
488 next_id: &mut u32,
489) -> Result<(Vec<SourceUnit>, Vec<CompileError>, bool), Vec<CompileError>> {
490 let (filtered, trivia) = split_trivia(tokens, source);
491 let mut warnings = Vec::new();
492 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
493 p.next_expr_id = *next_id;
494 let mut units = Vec::new();
495 let mut errors: Vec<CompileError> = Vec::new();
496 while p.peek().is_some() {
497 match p.parse_unit() {
498 Ok(u) => units.push(u),
499 Err(e) => {
500 errors.push(e);
501 break;
502 }
503 }
504 }
505 errors.append(&mut p.doc_errors);
507 *next_id = p.next_expr_id;
508 let eof = p.eof_span();
509 let fully_drained = p.trivia.is_fully_drained();
510 if !errors.is_empty() {
513 errors.append(&mut warnings);
514 return Err(errors);
515 }
516 if units.is_empty() {
517 return Err(vec![CompileError::new(
518 "bynk.parse.unexpected_eof",
519 eof,
520 "expected `commons`, `context`, or `suite` to start the file, found end of file",
521 )]);
522 }
523 debug_assert!(
531 p.trivia.epilogue_is_empty(),
532 "a file-trailing comment was left undrained after a successful parse"
533 );
534 Ok((units, warnings, fully_drained))
535}
536
537enum SignedNumLit {
540 Int(IntBound),
541 Float(FloatBound),
542}
543
544struct Parser<'a> {
545 tokens: &'a [Token],
546 source: &'a str,
547 pos: usize,
548 warnings: &'a mut Vec<CompileError>,
551 recover_mode: bool,
557 recovered_errors: Vec<CompileError>,
560 pub(crate) item_start: Option<usize>,
564 broken_decl_names: Vec<String>,
569 trivia: TriviaTable,
572 depth: usize,
579 no_record_literal: bool,
588 brace_depth: usize,
599 item_loop_baseline: Vec<usize>,
606 next_expr_id: u32,
610 doc_errors: Vec<CompileError>,
615 doc_probe: bool,
619}
620
621impl<'a> Parser<'a> {
622 fn new(
623 tokens: &'a [Token],
624 source: &'a str,
625 trivia: TriviaTable,
626 warnings: &'a mut Vec<CompileError>,
627 ) -> Self {
628 Self {
629 tokens,
630 source,
631 pos: 0,
632 warnings,
633 recover_mode: false,
634 recovered_errors: Vec::new(),
635 item_start: None,
636 broken_decl_names: Vec::new(),
637 trivia,
638 depth: 0,
639 no_record_literal: false,
640 brace_depth: 0,
641 item_loop_baseline: Vec::new(),
642 next_expr_id: 0,
643 doc_errors: Vec::new(),
644 doc_probe: false,
645 }
646 }
647
648 fn alloc_expr_id(&mut self) -> ExprId {
653 let id = ExprId(self.next_expr_id);
654 self.next_expr_id += 1;
655 id
656 }
657
658 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
666 self.depth += 1;
667 if self.depth > crate::MAX_NESTING_DEPTH {
668 self.depth -= 1;
669 let span = self
670 .peek()
671 .map(|t| t.span)
672 .unwrap_or_else(|| self.eof_span());
673 return Err(self.nesting_too_deep(span, what));
674 }
675 Ok(())
676 }
677
678 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
681 CompileError::new(
682 "bynk.parse.nesting_too_deep",
683 span,
684 format!(
685 "{what} nests more than {} levels deep",
686 crate::MAX_NESTING_DEPTH
687 ),
688 )
689 .with_note(
690 "deeply nested source is rejected to keep the parser from overflowing its \
691 stack and aborting; flatten or split the construct",
692 )
693 }
694
695 fn expression_too_long(&self, span: Span) -> CompileError {
701 CompileError::new(
702 "bynk.parse.nesting_too_deep",
703 span,
704 format!(
705 "this expression is more than {} levels deep",
706 crate::MAX_NESTING_DEPTH
707 ),
708 )
709 .with_note(
710 "a long operator or member chain is rejected to keep the compiler from overflowing \
711 its stack; split it across `let` bindings, or reduce a sequence with \
712 `.sum()`/`.fold(...)`",
713 )
714 }
715
716 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
736 self.depth += 1;
737 *folds += 1;
738 if self.depth > crate::MAX_NESTING_DEPTH {
739 self.depth -= *folds;
740 *folds = 0;
741 return Err(self.expression_too_long(span));
742 }
743 Ok(())
744 }
745
746 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
755 self.depth += 1;
756 if self.depth > crate::MAX_NESTING_DEPTH {
757 return Err(self.expression_too_long(span));
758 }
759 Ok(())
760 }
761
762 fn take_leading_trivia(&mut self) -> Vec<Comment> {
772 let mut comments = self.take_unit_leading_trivia();
773 comments.retain(|c| *c != Comment::Blank);
774 comments
775 }
776
777 fn take_unit_leading_trivia(&mut self) -> Vec<Comment> {
780 self.trivia.take_leading(self.pos)
781 }
782
783 fn take_epilogue_trivia(&mut self) -> Vec<Comment> {
786 self.trivia.take_epilogue()
787 }
788
789 fn take_trailing_trivia(&mut self) -> Option<String> {
793 if self.pos == 0 {
794 return None;
795 }
796 self.trivia.take_trailing(self.pos - 1)
797 }
798
799 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
803 if self.recover_mode {
804 self.recovered_errors.push(e);
805 if let Some(start) = self.item_start.take() {
806 self.broken_decl_names.extend(self.decl_names_at(start));
807 }
808 let before = self.pos;
809 self.recover_to_top_item();
810 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
823 let closes_body = baseline > 0
824 && self.brace_depth == baseline
825 && self.peek_kind() == Some(TokenKind::RBrace);
826 if self.pos == before && !closes_body {
827 self.bump();
828 self.recover_to_top_item();
833 }
834 Ok(())
835 } else {
836 Err(e)
837 }
838 }
839
840 fn decl_names_at(&self, start: usize) -> Vec<String> {
845 let kind_at = |i: usize| self.tokens.get(i).map(|t| t.kind);
846 let ident_at = |i: usize| {
847 self.tokens
848 .get(i)
849 .filter(|t| t.kind == TokenKind::Ident)
850 .map(|t| self.slice(t.span).to_string())
851 };
852 match kind_at(start) {
853 Some(
854 TokenKind::Type
855 | TokenKind::Event
856 | TokenKind::Capability
857 | TokenKind::Service
858 | TokenKind::Agent
859 | TokenKind::Actor,
860 ) => ident_at(start + 1).into_iter().collect(),
861 Some(TokenKind::Fn) => match (ident_at(start + 1), kind_at(start + 2)) {
862 (Some(owner), Some(TokenKind::Dot)) => match ident_at(start + 3) {
865 Some(method) => vec![format!("{owner}.{method}")],
866 None => Vec::new(),
867 },
868 (Some(name), _) => vec![name],
869 (None, _) => Vec::new(),
870 },
871 _ => Vec::new(),
872 }
873 }
874
875 fn recover_to_top_item(&mut self) {
888 let baseline = self.item_loop_baseline.last().copied().unwrap_or(0);
889 while let Some(t) = self.peek() {
890 match t.kind {
891 TokenKind::RBrace if self.brace_depth == baseline => return,
892 _ if self.brace_depth == baseline && is_item_start(t.kind) => return,
893 _ => {
894 self.bump();
895 }
896 }
897 }
898 }
899
900 fn enter_item_loop(&mut self) {
907 self.item_loop_baseline.push(self.brace_depth);
908 }
909
910 fn exit_item_loop(&mut self) {
912 self.item_loop_baseline.pop();
913 }
914
915 fn peek(&self) -> Option<Token> {
916 self.tokens.get(self.pos).copied()
917 }
918
919 fn peek_kind(&self) -> Option<TokenKind> {
920 self.peek().map(|t| t.kind)
921 }
922
923 fn nth(&self, n: usize) -> Option<Token> {
925 self.tokens.get(self.pos + n).copied()
926 }
927
928 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
929 self.nth(n).map(|t| t.kind)
930 }
931
932 fn nth_text(&self, n: usize) -> &'a str {
934 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
935 }
936
937 fn prev_span(&self) -> Span {
940 self.tokens
941 .get(self.pos.wrapping_sub(1))
942 .or_else(|| self.peek_ref())
943 .map(|t| t.span)
944 .unwrap_or_default()
945 }
946
947 fn peek_ref(&self) -> Option<&Token> {
948 self.tokens.get(self.pos)
949 }
950
951 fn bump(&mut self) -> Option<Token> {
952 let t = self.peek();
953 if let Some(t) = t {
954 match t.kind {
955 TokenKind::LBrace => self.brace_depth += 1,
956 TokenKind::RBrace => self.brace_depth = self.brace_depth.saturating_sub(1),
957 _ => {}
958 }
959 self.pos += 1;
960 }
961 t
962 }
963
964 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
965 if self.peek_kind() == Some(kind) {
966 self.bump()
967 } else {
968 None
969 }
970 }
971
972 fn slice(&self, span: Span) -> &'a str {
973 &self.source[span.range()]
974 }
975
976 fn next_token_on_new_line(&self, prev: Span) -> bool {
981 match self.peek() {
982 Some(t) if prev.end <= t.span.start => {
983 self.source[prev.end..t.span.start].contains('\n')
984 }
985 _ => false,
986 }
987 }
988
989 fn eof_span(&self) -> Span {
994 let end = self.source.len();
995 let start = (0..end)
996 .rev()
997 .find(|&i| self.source.is_char_boundary(i))
998 .unwrap_or(0);
999 Span::new(start, end)
1000 }
1001
1002 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
1003 match self.peek() {
1004 Some(t) if t.kind == kind => {
1005 self.bump();
1006 Ok(t)
1007 }
1008 Some(t) => Err(CompileError::new(
1009 "bynk.parse.expected_token",
1010 t.span,
1011 format!(
1012 "expected {} {ctx}, found {}",
1013 kind.describe(),
1014 t.kind.describe()
1015 ),
1016 )),
1017 None => Err(CompileError::new(
1018 "bynk.parse.unexpected_eof",
1019 self.eof_span(),
1020 format!("expected {} {ctx}, found end of file", kind.describe()),
1021 )),
1022 }
1023 }
1024
1025 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
1026 match self.peek() {
1027 Some(t) if t.kind == TokenKind::Ident => {
1028 self.bump();
1029 Ok(Ident {
1030 name: self.slice(t.span).to_string(),
1031 span: t.span,
1032 })
1033 }
1034 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
1049 self.bump();
1050 Ok(Ident {
1051 name: self.slice(t.span).to_string(),
1052 span: t.span,
1053 })
1054 }
1055 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
1056 "bynk.parse.reserved_keyword",
1057 t.span,
1058 format!(
1059 "expected identifier {ctx}, but `{}` is a reserved keyword",
1060 self.slice(t.span)
1061 ),
1062 )
1063 .with_note("rename the identifier to something that is not a keyword")),
1064 Some(t) => Err(CompileError::new(
1065 "bynk.parse.expected_token",
1066 t.span,
1067 format!("expected identifier {ctx}, found {}", t.kind.describe()),
1068 )),
1069 None => Err(CompileError::new(
1070 "bynk.parse.unexpected_eof",
1071 self.eof_span(),
1072 format!("expected identifier {ctx}, found end of file"),
1073 )),
1074 }
1075 }
1076
1077 fn take_doc_block(&mut self) -> Option<(Doc, Span)> {
1093 let (doc, span) = self.take_one_doc()?;
1094 if let Some(next) = self.peek()
1095 && next.kind != doc_token_kind(doc.form)
1096 && matches!(next.kind, TokenKind::DocBlock | TokenKind::DocLine)
1097 && !has_blank_line_between(self.source, span.end, next.span.start)
1098 {
1099 let (_, other) = self.take_one_doc().expect("a doc token was peeked");
1100 if !self.doc_probe {
1101 self.doc_errors.push(doc_forms_mixed(span, other));
1102 }
1103 }
1104 Some((doc, span))
1105 }
1106
1107 fn take_one_doc(&mut self) -> Option<(Doc, Span)> {
1111 match self.peek_kind()? {
1112 TokenKind::DocBlock => {
1113 let t = self.bump().unwrap();
1114 let body = doc_block_content(self.source, t.span);
1115 if !self.doc_probe && doc_content_is_code(&body) {
1116 self.doc_errors.push(doc_block_contains_code(t.span));
1117 }
1118 Some((Doc::new(body, DocForm::Block), t.span))
1119 }
1120 TokenKind::DocLine => {
1121 let first = self.bump().unwrap();
1122 let mut span = first.span;
1123 let mut lines = vec![doc_line_text(self.source, first.span)];
1124 while let Some(next) = self.peek()
1127 && next.kind == TokenKind::DocLine
1128 && self.source[span.end..next.span.start]
1129 .bytes()
1130 .all(|b| matches!(b, b' ' | b'\t' | b'\r'))
1131 {
1132 let t = self.bump().unwrap();
1133 lines.push(doc_line_text(self.source, t.span));
1134 span = span.merge(t.span);
1135 }
1136 Some((Doc::new(lines.join("\n"), DocForm::Lines), span))
1137 }
1138 _ => None,
1139 }
1140 }
1141
1142 fn collect_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1155 self.collect_lead(Self::take_unit_leading_trivia)
1156 }
1157
1158 fn collect_body_item_lead(&mut self) -> (Vec<Comment>, Option<DocLead>) {
1161 self.collect_lead(Self::take_leading_trivia)
1162 }
1163
1164 fn collect_lead(
1165 &mut self,
1166 take: fn(&mut Self) -> Vec<Comment>,
1167 ) -> (Vec<Comment>, Option<DocLead>) {
1168 let mut leading = take(self);
1169 let doc = self.take_doc_block().map(|(content, span)| DocLead {
1170 content,
1171 span,
1172 at: leading.len(),
1173 });
1174 if doc.is_some() {
1175 leading.extend(take(self));
1176 }
1177 (leading, doc)
1178 }
1179
1180 fn collect_member_lead(&mut self, close: TokenKind) -> (Vec<Comment>, Option<Doc>) {
1189 let (mut leading, doc) = self.collect_body_item_lead();
1190 let Some(doc) = doc else {
1191 return (leading, None);
1192 };
1193 match self.peek() {
1194 Some(t) if t.kind != close => {
1195 let doc = self.finalize_doc(Some(doc), t.span, &mut leading);
1196 (leading, doc)
1197 }
1198 _ => {
1199 self.warnings.push(CompileError::new(
1200 "bynk.parse.orphan_doc_block",
1201 doc.span,
1202 "documentation has no following member to attach to",
1203 ));
1204 keep_orphan(&mut leading, doc);
1205 (leading, None)
1206 }
1207 }
1208 }
1209
1210 fn peek_past_docs(&self) -> Option<TokenKind> {
1214 self.tokens[self.pos.min(self.tokens.len())..]
1215 .iter()
1216 .map(|t| t.kind)
1217 .find(|k| !matches!(k, TokenKind::DocBlock | TokenKind::DocLine))
1218 }
1219
1220 fn finalize_doc(
1224 &mut self,
1225 doc: Option<DocLead>,
1226 next_span: Span,
1227 leading: &mut Vec<Comment>,
1228 ) -> Option<Doc> {
1229 let doc = doc?;
1230 let doc_span = doc.span;
1231 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
1233 self.warnings.push(
1234 CompileError::new(
1235 "bynk.parse.orphan_doc_block",
1236 doc_span,
1237 "documentation block is separated from the following declaration by a blank line; it will not be attached",
1238 )
1239 .with_note(
1240 "remove the blank line to attach the doc to the next declaration, \
1241 or remove the doc block if it is not meant to document anything",
1242 ),
1243 );
1244 keep_orphan(leading, doc);
1245 return None;
1246 }
1247 Some(doc.content)
1248 }
1249}
1250
1251pub(crate) struct DocLead {
1255 pub(crate) content: Doc,
1256 pub(crate) span: Span,
1257 pub(crate) at: usize,
1258}
1259
1260fn keep_orphan(leading: &mut Vec<Comment>, doc: DocLead) {
1264 let at = doc.at.min(leading.len());
1265 leading.insert(at, Comment::OrphanDoc(doc.content));
1266}
1267
1268fn doc_token_kind(form: DocForm) -> TokenKind {
1270 match form {
1271 DocForm::Block => TokenKind::DocBlock,
1272 DocForm::Lines => TokenKind::DocLine,
1273 }
1274}
1275
1276fn doc_forms_mixed(first: Span, second: Span) -> CompileError {
1279 CompileError::new(
1280 "bynk.parse.doc_forms_mixed",
1281 second,
1282 "this declaration or member has a doc in both forms, a `---` block and `--|` lines",
1283 )
1284 .with_label(first, "the other doc is here")
1285 .with_note(
1286 "write a doc in one form: `--|` lines for a sentence or two, a `---` block for \
1287 anything with structure (lists, code, headings)",
1288 )
1289}
1290
1291fn doc_block_contains_code(span: Span) -> CompileError {
1294 CompileError::new(
1295 "bynk.parse.doc_block_contains_code",
1296 span,
1297 "this documentation block contains declarations, so they are not compiled",
1298 )
1299 .with_note(
1300 "a line of three or more dashes opens or closes a doc block, so two dash \
1301 dividers turn the code between them into documentation; for a divider use a \
1302 line comment with text, such as `-- Helpers --`, and to show code in a doc \
1303 block put it in a Markdown code fence (```)",
1304 )
1305}
1306
1307const DECLARATION_KEYWORDS: &[TokenKind] = &[
1311 TokenKind::Type,
1312 TokenKind::Fn,
1313 TokenKind::Capability,
1314 TokenKind::Provides,
1315 TokenKind::Service,
1316 TokenKind::Agent,
1317 TokenKind::Actor,
1318 TokenKind::Messages,
1319 TokenKind::Event,
1320 TokenKind::Asset,
1321 TokenKind::Uses,
1322 TokenKind::Consumes,
1323 TokenKind::Exports,
1324 TokenKind::Binding,
1325];
1326
1327fn doc_content_is_code(content: &str) -> bool {
1342 doc_content_is_code_at(content, 0)
1343}
1344
1345const DOC_PROBE_MAX_DEPTH: usize = 4;
1349
1350fn doc_content_is_code_at(content: &str, depth: usize) -> bool {
1351 let stripped = strip_code_fences(content);
1352 let tokens = match crate::lexer::tokenize(&stripped) {
1353 Ok(tokens) => tokens,
1354 Err(_) => {
1355 let unmarked = blank_marker_lines(&stripped);
1358 if unmarked == stripped {
1359 return false;
1360 }
1361 return doc_content_is_code_at(&unmarked, depth);
1362 }
1363 };
1364 if depth < DOC_PROBE_MAX_DEPTH
1365 && tokens.iter().any(|t| {
1366 t.kind == TokenKind::DocBlock
1367 && doc_content_is_code_at(&doc_block_content(&stripped, t.span), depth + 1)
1368 })
1369 {
1370 return true;
1371 }
1372 if !tokens
1373 .iter()
1374 .any(|t| DECLARATION_KEYWORDS.contains(&t.kind))
1375 {
1376 return false;
1377 }
1378 let (filtered, trivia) = split_trivia(&tokens, &stripped);
1379 let mut warnings = Vec::new();
1380 let mut p = Parser::new(&filtered, &stripped, trivia, &mut warnings);
1381 p.doc_probe = true;
1382 let at = Span::new(0, 0);
1383 let name = QualifiedName {
1384 parts: Vec::new(),
1385 span: at,
1386 };
1387 match p.parse_adapter_body(at, name, None, false) {
1388 Ok(a) => {
1389 p.peek().is_none()
1390 && (!a.items.is_empty()
1391 || !a.uses.is_empty()
1392 || !a.consumes.is_empty()
1393 || !a.exports.is_empty()
1394 || a.binding.is_some())
1395 }
1396 Err(_) => false,
1397 }
1398}
1399
1400fn blank_marker_lines(content: &str) -> String {
1403 let mut out = String::with_capacity(content.len());
1404 for line in content.lines() {
1405 let trimmed = line.trim();
1406 if !(trimmed.len() >= 3 && trimmed.bytes().all(|b| b == b'-')) {
1407 out.push_str(line);
1408 }
1409 out.push('\n');
1410 }
1411 out
1412}
1413
1414fn strip_code_fences(content: &str) -> String {
1419 let mut out = String::with_capacity(content.len());
1420 let mut open: Option<(char, usize)> = None;
1421 for line in content.lines() {
1422 let trimmed = line.trim();
1423 let fence = ['`', '~'].into_iter().find_map(|c| {
1424 let n = trimmed.chars().take_while(|&x| x == c).count();
1425 (n >= 3).then_some((c, n))
1426 });
1427 match (open, fence) {
1428 (None, Some(f)) => open = Some(f),
1429 (Some((c, n)), Some((fc, fnum)))
1430 if fc == c && fnum >= n && trimmed.len() == fnum * c.len_utf8() =>
1431 {
1432 open = None;
1433 }
1434 (None, None) => out.push_str(line),
1435 (Some(_), _) => {}
1436 }
1437 out.push('\n');
1438 }
1439 out
1440}
1441
1442fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
1445 let bytes = lexeme.as_bytes();
1446 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
1447 let inner = &lexeme[1..lexeme.len() - 1];
1448 let mut out = String::with_capacity(inner.len());
1449 let mut chars = inner.chars();
1450 while let Some(c) = chars.next() {
1451 if c == '\\' {
1452 match chars.next() {
1453 Some('n') => out.push('\n'),
1454 Some('t') => out.push('\t'),
1455 Some('"') => out.push('"'),
1456 Some('\\') => out.push('\\'),
1457 other => {
1458 return Err(CompileError::new(
1459 "bynk.lex.bad_escape",
1460 span,
1461 format!(
1462 "invalid escape sequence `\\{}` in string literal",
1463 other.map(|c| c.to_string()).unwrap_or_default()
1464 ),
1465 )
1466 .with_note("supported escapes: \\n \\t \\\" \\\\"));
1467 }
1468 }
1469 } else {
1470 out.push(c);
1471 }
1472 }
1473 Ok(out)
1474}
1475
1476fn is_reserved_keyword(kind: TokenKind) -> bool {
1477 use TokenKind::*;
1478 matches!(
1479 kind,
1480 Commons
1481 | Type
1482 | Fn
1483 | Where
1484 | True
1485 | False
1486 | Int
1487 | String
1488 | Bool
1489 | Let
1490 | If
1491 | Else
1492 | Ok
1493 | Err
1494 | Result
1495 | ValidationError
1496 | Enum
1497 | Match
1498 | Option
1499 | Record
1500 | Self_
1501 | Some
1502 | None
1503 | Is
1504 | Opaque
1505 | Uses
1506 | Context
1507 | Consumes
1508 | Exports
1509 | Transparent
1510 | Agent
1511 | As
1512 | Capability
1513 | Effect
1514 | Do
1515 | Given
1516 | On
1517 | Http
1518 | Provides
1519 | Stub
1520 | Service
1521 | Actor
1522 | By
1523 | Expect
1524 | Suite
1525 | Case
1526 | Float
1527 | Duration
1528 | Instant
1529 | Bytes
1530 | JsonError
1531 | Property
1532 | Adapter
1533 | Binding
1534 | Cron
1535 | Queue
1536 | From
1537 | Protocol
1538 | Invariant
1539 | Implies
1540 | Requires
1541 | Ensures
1542 | Transition
1543 )
1544}
1545
1546fn is_item_start(kind: TokenKind) -> bool {
1557 use TokenKind::*;
1558 matches!(
1559 kind,
1560 Commons | Context | Adapter | Suite
1562 | Type | Fn | Messages | Event | Asset | Uses
1564 | Consumes | Exports | Capability | Provides | Service | Agent | Actor
1566 | Binding
1568 | Stub | Case | Property
1570 )
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575 use super::*;
1576 use crate::lexer::tokenize;
1577
1578 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
1579 let toks = tokenize(src).map_err(|e| vec![e])?;
1580 parse(&toks, src)
1581 }
1582
1583 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
1584 let toks = match tokenize(src) {
1585 Ok(t) => t,
1586 Err(e) => return (None, vec![e]),
1587 };
1588 parse_unit_with_recovery(&toks, src)
1589 }
1590
1591 #[test]
1597 fn parse_units_with_recovery_keeps_every_top_level_unit() {
1598 let src =
1599 "commons m {\n fn f() -> Int { 1 }\n}\n\nsuite m\n\ncase \"c\" {\n expect true\n}\n";
1600 let toks = tokenize(src).unwrap();
1601 let (units, errors) = parse_units_with_recovery(&toks, src);
1602 assert!(errors.is_empty(), "{errors:?}");
1603 assert_eq!(units.len(), 2, "expected both units, got {units:?}");
1604 assert!(matches!(units[0], SourceUnit::Commons(_)));
1605 assert!(matches!(units[1], SourceUnit::Suite(_)));
1606
1607 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1609 assert!(errors.is_empty(), "{errors:?}");
1610 assert!(matches!(unit, Some(SourceUnit::Commons(_))));
1611 }
1612
1613 #[test]
1620 fn empty_input_still_reports_an_error_through_the_plural_entry_point() {
1621 let toks = tokenize("").unwrap();
1622 let (units, errors) = parse_units_with_recovery(&toks, "");
1623 assert!(units.is_empty());
1624 assert!(
1625 !errors.is_empty(),
1626 "an empty file must still produce a diagnostic, not silently no units and no error"
1627 );
1628
1629 let (unit, unit_errors) = parse_unit_with_recovery(&toks, "");
1630 assert!(unit.is_none());
1631 assert_eq!(
1632 errors.len(),
1633 unit_errors.len(),
1634 "the singular wrapper must see the same error(s) as the plural entry point"
1635 );
1636 }
1637
1638 #[test]
1645 fn drain_check_reports_expression_interior_comments_as_undrained() {
1646 let ordinary = "commons x {\n-- note\ntype T = Int where Positive\n}\n";
1647 let toks = tokenize(ordinary).unwrap();
1648 let (_, _, drained) = parse_units_with_drain_check(&toks, ordinary).unwrap();
1649 assert!(
1650 drained,
1651 "a declaration-leading comment must be fully drained"
1652 );
1653
1654 let lossy = "commons x {\n fn f() -> Int {\n 1 + -- note\n 2\n }\n}\n";
1655 let toks = tokenize(lossy).unwrap();
1656 let (_, _, drained) = parse_units_with_drain_check(&toks, lossy).unwrap();
1657 assert!(
1658 !drained,
1659 "a comment inside a binop expression must be reported as undrained"
1660 );
1661 }
1662
1663 #[test]
1664 fn eof_span_never_splits_a_multibyte_codepoint() {
1665 for src in [
1670 "commons x {\n -- ends with an arrow →",
1671 "agent A {\n key k: String\n -- note 🦀",
1672 "commons y {\n type T = é",
1673 ] {
1674 let (_unit, errors) = parse_recover_str(src);
1675 for e in &errors {
1676 assert!(
1677 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1678 "span {:?} splits a codepoint in {src:?}",
1679 e.span,
1680 );
1681 }
1682 }
1683 }
1684
1685 #[test]
1686 fn reserved_contextual_keywords_readable_in_expression_position() {
1687 for kw in ["asset", "case", "event", "messages", "on", "suite"] {
1697 let src = format!("commons x\n\nfn f({kw}: Int) -> Int {{\n {kw}\n}}\n");
1698 let result = parse_str(&src);
1699 assert!(
1700 result.is_ok(),
1701 "a parameter named `{kw}` must be readable in expression position: {:?}",
1702 result.err()
1703 );
1704 }
1705 }
1706
1707 #[test]
1708 fn recovery_skips_garbage_between_decls() {
1709 let src = "commons x {\n\
1712 type A = Int where NonNegative\n\
1713 ??? !!!\n\
1714 type B = String where NonEmpty\n\
1715 }";
1716 let (unit, errors) = parse_recover_str(src);
1717 let unit = unit.expect("recovery should produce a partial AST");
1718 let SourceUnit::Commons(c) = unit else {
1719 panic!("expected commons")
1720 };
1721 let names: Vec<_> = c
1723 .items
1724 .iter()
1725 .map(|i| match i {
1726 CommonsItem::Type(t) => t.name.name.clone(),
1727 _ => panic!("expected only types"),
1728 })
1729 .collect();
1730 assert!(
1731 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
1732 "expected both A and B; got {names:?}",
1733 );
1734 assert!(!errors.is_empty(), "expected at least one parse error");
1735 }
1736
1737 #[test]
1738 fn recovery_handles_bad_first_decl_then_good_second() {
1739 let src = "commons x {\n\
1741 type A Int where NonNegative\n\
1742 type B = String where NonEmpty\n\
1743 }";
1744 let (unit, errors) = parse_recover_str(src);
1745 let unit = unit.expect("recovery should produce a partial AST");
1746 let SourceUnit::Commons(c) = unit else {
1747 panic!("expected commons")
1748 };
1749 let names: Vec<_> = c
1750 .items
1751 .iter()
1752 .filter_map(|i| match i {
1753 CommonsItem::Type(t) => Some(t.name.name.clone()),
1754 _ => None,
1755 })
1756 .collect();
1757 assert!(
1758 names.contains(&"B".to_string()),
1759 "B should be parsed after A's failure; got {names:?}"
1760 );
1761 assert!(!errors.is_empty(), "expected at least one parse error");
1762 }
1763
1764 #[test]
1779 fn recovery_keeps_the_bodys_closing_brace_after_a_bodiless_item() {
1780 let src = "commons m {\n fn f() -> Int\n}\n";
1781 let (unit, errors) = parse_recover_str(src);
1782 assert!(unit.is_some(), "recovery should produce a partial AST");
1783 let categories: Vec<_> = errors.iter().map(|e| e.category).collect();
1784 assert_eq!(
1785 categories.len(),
1786 1,
1787 "one syntax error, no follow-on: {categories:?}"
1788 );
1789 assert!(
1790 !categories.contains(&"bynk.parse.unexpected_eof"),
1791 "the commons' closing brace must survive: {categories:?}"
1792 );
1793 }
1794
1795 #[test]
1800 fn recovery_skips_a_rejected_item_whole() {
1801 let src = "commons m\n\nagent Counter {\n key id: String\n}\n\nfn g() -> Int { 2 }\n";
1802 let recovered = parse_units_recovering(&crate::lexer::tokenize(src).unwrap(), src);
1803 assert_eq!(
1804 recovered.errors.len(),
1805 1,
1806 "one syntax error, no follow-on: {:?}",
1807 recovered
1808 .errors
1809 .iter()
1810 .map(|e| e.category)
1811 .collect::<Vec<_>>()
1812 );
1813 let Some(SourceUnit::Commons(c)) = recovered.units.first() else {
1814 panic!("expected commons")
1815 };
1816 assert!(
1817 c.items.iter().any(|i| matches!(i, CommonsItem::Fn(_))),
1818 "the `fn g` after the skipped agent must still parse"
1819 );
1820 assert_eq!(recovered.broken_decl_names, ["Counter"]);
1821 }
1822
1823 #[test]
1824 fn recovery_skips_a_nested_blocks_own_closing_brace() {
1825 let src = "commons m {\n \
1826 fn f() -> Int {\n \
1827 match 1 {\n \
1828 is 1 -> { let z = }\n \
1829 is _ -> 2\n \
1830 }\n \
1831 }\n \
1832 fn g() -> Int { 2 }\n\
1833 }\n";
1834 let (unit, errors) = parse_recover_str(src);
1835 let unit = unit.expect("recovery should produce a partial AST");
1836 let SourceUnit::Commons(c) = unit else {
1837 panic!("expected commons")
1838 };
1839 let names: Vec<_> = c
1840 .items
1841 .iter()
1842 .filter_map(|i| match i {
1843 CommonsItem::Fn(f) => match &f.name {
1844 FnName::Free(id) => Some(id.name.clone()),
1845 _ => None,
1846 },
1847 _ => None,
1848 })
1849 .collect();
1850 assert_eq!(
1851 names,
1852 vec!["g".to_string()],
1853 "g must still be recovered as an item; got {names:?}"
1854 );
1855 assert!(
1856 !errors
1857 .iter()
1858 .any(|e| e.category == "bynk.parse.expected_unit_header"),
1859 "the outer body's own closing brace must not be mistaken for \
1860 end-of-file: {errors:?}"
1861 );
1862 }
1863
1864 #[test]
1865 fn doc_block_attaches_to_type() {
1866 let c =
1867 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
1868 .unwrap();
1869 let CommonsItem::Type(t) = &c.items[0] else {
1870 panic!()
1871 };
1872 assert!(t.documentation.is_some());
1873 assert!(
1874 t.documentation
1875 .as_ref()
1876 .unwrap()
1877 .contains("A descriptive doc.")
1878 );
1879 }
1880
1881 #[test]
1882 fn interpolated_string_parses_into_parts() {
1883 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
1885 .unwrap();
1886 let CommonsItem::Fn(f) = &c.items[0] else {
1887 panic!("expected fn")
1888 };
1889 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1890 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
1891 };
1892 assert_eq!(parts.len(), 3);
1893 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
1894 assert!(
1895 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
1896 );
1897 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
1898 }
1899
1900 #[test]
1901 fn interpolated_hole_parses_a_full_expression() {
1902 let c =
1904 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
1905 .unwrap();
1906 let CommonsItem::Fn(f) = &c.items[0] else {
1907 panic!("expected fn")
1908 };
1909 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
1910 panic!("expected InterpStr")
1911 };
1912 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
1913 }
1914
1915 #[test]
1916 fn empty_interpolation_hole_is_rejected() {
1917 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
1918 assert!(
1919 errs.iter()
1920 .any(|e| e.category == "bynk.parse.empty_interpolation"),
1921 "expected empty_interpolation; got {errs:?}"
1922 );
1923 }
1924
1925 #[test]
1926 fn interpolation_hole_lex_error_span_is_rebased() {
1927 let cases = [
1933 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1935 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1937 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1940 ];
1941 for src in cases {
1942 let errs = parse_str(src).unwrap_err();
1943 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1944 for e in &errs {
1945 assert!(
1946 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1947 "span {:?} splits a codepoint in {src:?}",
1948 e.span,
1949 );
1950 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1953 assert!(
1954 e.span.start >= hole_start,
1955 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1956 e.span,
1957 );
1958 }
1959 }
1960 }
1961
1962 #[test]
1963 fn fragment_form_parses() {
1964 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1965 assert_eq!(c.form, CommonsForm::Fragment);
1966 assert_eq!(c.items.len(), 1);
1967 }
1968
1969 #[test]
1970 fn uses_parses() {
1971 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1972 assert_eq!(c.uses.len(), 1);
1973 assert_eq!(c.uses[0].target.joined(), "other.lib");
1974 }
1975
1976 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1977 let toks = tokenize(src).map_err(|e| vec![e])?;
1978 parse_unit(&toks, src)
1979 }
1980
1981 #[test]
1982 fn minimal_context_parses() {
1983 let u = parse_unit_str("context commerce.orders {}").unwrap();
1984 let SourceUnit::Context(c) = u else {
1985 panic!("expected context");
1986 };
1987 assert_eq!(c.name.joined(), "commerce.orders");
1988 assert!(c.items.is_empty());
1989 }
1990
1991 #[test]
1992 fn context_consumes_and_exports_parse() {
1993 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}";
1994 let u = parse_unit_str(src).unwrap();
1995 let SourceUnit::Context(c) = u else { panic!() };
1996 assert_eq!(c.uses.len(), 1);
1997 assert_eq!(c.consumes.len(), 1);
1998 assert_eq!(c.exports.len(), 2);
1999 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
2000 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
2001 }
2002
2003 #[test]
2004 fn context_fragment_form_parses() {
2005 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
2006 let u = parse_unit_str(src).unwrap();
2007 let SourceUnit::Context(c) = u else { panic!() };
2008 assert_eq!(c.form, CommonsForm::Fragment);
2009 assert_eq!(c.uses.len(), 1);
2010 assert_eq!(c.consumes.len(), 1);
2011 assert_eq!(c.exports.len(), 1);
2012 }
2013
2014 #[test]
2015 fn opaque_type_parses() {
2016 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
2017 let CommonsItem::Type(t) = &c.items[0] else {
2018 panic!()
2019 };
2020 assert!(matches!(t.body, TypeBody::Opaque { .. }));
2021 }
2022
2023 #[test]
2024 fn empty_commons() {
2025 let c = parse_str("commons fitness.units {}").unwrap();
2026 assert_eq!(c.name.joined(), "fitness.units");
2027 assert!(c.items.is_empty());
2028 }
2029
2030 #[test]
2031 fn one_type_decl() {
2032 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
2033 assert_eq!(c.items.len(), 1);
2034 let CommonsItem::Type(t) = &c.items[0] else {
2035 panic!()
2036 };
2037 assert_eq!(t.name.name, "Metres");
2038 match &t.body {
2039 TypeBody::Refined {
2040 base, refinement, ..
2041 } => {
2042 assert_eq!(*base, BaseType::Int);
2043 assert!(refinement.is_some());
2044 }
2045 _ => panic!("expected refined body"),
2046 }
2047 }
2048
2049 #[test]
2050 fn function_decl() {
2051 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
2052 let CommonsItem::Fn(f) = &c.items[0] else {
2053 panic!()
2054 };
2055 assert_eq!(f.name.ident().name, "add");
2056 assert_eq!(f.params.len(), 2);
2057 }
2058
2059 #[test]
2060 fn chained_comparison_is_error() {
2061 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
2062 .unwrap_err();
2063 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2064 }
2065
2066 #[test]
2067 fn chained_equality_is_error() {
2068 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
2069 .unwrap_err();
2070 assert_eq!(errs[0].category, "bynk.parse.non_associative");
2071 }
2072
2073 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
2082 std::thread::Builder::new()
2083 .stack_size(64 * 1024 * 1024)
2084 .spawn(f)
2085 .unwrap()
2086 .join()
2087 .unwrap()
2088 }
2089
2090 #[test]
2091 fn deeply_nested_parens_are_bounded_not_overflowed() {
2092 let errs = on_big_stack(|| {
2098 let depth = crate::MAX_NESTING_DEPTH + 8;
2099 let src = format!(
2100 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
2101 "(".repeat(depth),
2102 ")".repeat(depth),
2103 );
2104 parse_str(&src).unwrap_err()
2105 });
2106 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2107 }
2108
2109 #[test]
2110 fn deeply_nested_types_are_bounded_not_overflowed() {
2111 let errs = on_big_stack(|| {
2116 let depth = crate::MAX_NESTING_DEPTH + 8;
2117 let src = format!(
2118 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
2119 "Result[Int, ".repeat(depth),
2120 "]".repeat(depth),
2121 );
2122 parse_str(&src).unwrap_err()
2123 });
2124 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2125 }
2126
2127 #[test]
2128 fn deeply_nested_patterns_are_bounded_not_overflowed() {
2129 let errs = on_big_stack(|| {
2134 let depth = crate::MAX_NESTING_DEPTH + 8;
2135 let src = format!(
2136 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
2137 "Ok(".repeat(depth),
2138 ")".repeat(depth),
2139 );
2140 parse_str(&src).unwrap_err()
2141 });
2142 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
2143 }
2144
2145 #[test]
2146 fn nesting_below_the_limit_still_parses() {
2147 let ok = 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).is_ok()
2157 });
2158 assert!(ok, "well-nested source under the limit should parse");
2159 }
2160
2161 #[test]
2162 fn let_statement_parses() {
2163 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
2164 let CommonsItem::Fn(f) = &c.items[0] else {
2165 panic!()
2166 };
2167 assert_eq!(f.body.statements.len(), 1);
2168 match &f.body.statements[0] {
2169 Statement::Let(l) => {
2170 assert_eq!(l.name.name, "y");
2171 assert!(l.type_annot.is_none());
2172 }
2173 _ => panic!("expected a pure `let` statement"),
2174 }
2175 }
2176
2177 #[test]
2178 fn let_with_annotation() {
2179 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
2180 let CommonsItem::Fn(f) = &c.items[0] else {
2181 panic!()
2182 };
2183 match &f.body.statements[0] {
2184 Statement::Let(l) => assert!(l.type_annot.is_some()),
2185 _ => panic!("expected a pure `let` statement"),
2186 }
2187 }
2188
2189 #[test]
2190 fn if_else_parses_as_expression() {
2191 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
2192 let CommonsItem::Fn(f) = &c.items[0] else {
2193 panic!()
2194 };
2195 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
2196 }
2197
2198 #[test]
2199 fn else_if_chain_parses() {
2200 let c = parse_str(
2201 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
2202 )
2203 .unwrap();
2204 let CommonsItem::Fn(f) = &c.items[0] else {
2205 panic!()
2206 };
2207 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
2208 panic!()
2209 };
2210 assert!(else_block.statements.is_empty());
2212 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
2213 }
2214
2215 #[test]
2216 fn ok_and_err_parse_as_expressions() {
2217 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2218 let CommonsItem::Fn(f) = &c.items[0] else {
2219 panic!()
2220 };
2221 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
2222
2223 let c =
2224 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
2225 let CommonsItem::Fn(f) = &c.items[0] else {
2226 panic!()
2227 };
2228 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
2229 }
2230
2231 #[test]
2232 fn question_postfix_parses() {
2233 let c = parse_str(
2234 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
2235 )
2236 .unwrap();
2237 let CommonsItem::Fn(f) = &c.items[1] else {
2238 panic!()
2239 };
2240 let Statement::Let(l) = &f.body.statements[0] else {
2241 panic!("expected a pure `let` statement");
2242 };
2243 assert!(matches!(l.value.kind, ExprKind::Question(_)));
2244 }
2245
2246 #[test]
2247 fn constructor_call_parses() {
2248 let c = parse_str(
2249 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
2250 )
2251 .unwrap();
2252 let CommonsItem::Fn(f) = &c.items[1] else {
2253 panic!()
2254 };
2255 let ExprKind::MethodCall {
2258 receiver, method, ..
2259 } = &f.body.tail.kind
2260 else {
2261 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
2262 };
2263 let ExprKind::Ident(id) = &receiver.kind else {
2264 panic!("expected receiver Ident");
2265 };
2266 assert_eq!(id.name, "T");
2267 assert_eq!(method.name, "of");
2268 }
2269
2270 #[test]
2271 fn result_type_ref_parses() {
2272 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
2273 let CommonsItem::Fn(f) = &c.items[0] else {
2274 panic!()
2275 };
2276 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
2277 }
2278
2279 #[test]
2280 fn result_missing_arg_count_errors() {
2281 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
2282 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
2283 }
2284
2285 #[test]
2286 fn field_access_parses_in_v0_2() {
2287 let c =
2290 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
2291 let CommonsItem::Fn(f) = &c.items[1] else {
2292 panic!()
2293 };
2294 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
2295 }
2296
2297 #[test]
2300 fn leading_line_comment_attaches_to_next_decl() {
2301 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
2302 let c = parse_str(src).unwrap();
2303 let CommonsItem::Type(t) = &c.items[0] else {
2304 panic!()
2305 };
2306 assert_eq!(
2307 t.trivia.leading,
2308 vec![Comment::Line(" explain the type".to_string())]
2309 );
2310 assert!(t.trivia.trailing.is_none());
2311 }
2312
2313 #[test]
2314 fn trailing_line_comment_attaches_to_prev_decl() {
2315 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
2316 let c = parse_str(src).unwrap();
2317 let CommonsItem::Type(t) = &c.items[0] else {
2318 panic!()
2319 };
2320 assert!(t.trivia.leading.is_empty());
2321 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
2322 }
2323
2324 #[test]
2325 fn grouped_leading_comments_attach_together() {
2326 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
2327 let c = parse_str(src).unwrap();
2328 let CommonsItem::Type(t) = &c.items[0] else {
2329 panic!()
2330 };
2331 assert_eq!(
2332 t.trivia.leading,
2333 vec![
2334 Comment::Line(" one".to_string()),
2335 Comment::Line(" two".to_string()),
2336 Comment::Line(" three".to_string())
2337 ],
2338 );
2339 }
2340
2341 #[test]
2342 fn comment_with_doc_block_keeps_both() {
2343 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
2345 let c = parse_str(src).unwrap();
2346 let CommonsItem::Type(t) = &c.items[0] else {
2347 panic!()
2348 };
2349 assert_eq!(t.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2350 assert_eq!(t.documentation.as_deref(), Some("docs"));
2351 }
2352
2353 #[test]
2354 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
2355 let src = "commons app.messages {\ntype T = Int where Positive\n}";
2361 let c = parse_str(src).unwrap();
2362 assert_eq!(c.name.joined(), "app.messages");
2363 }
2364
2365 #[test]
2366 fn messages_decl_parses_tag_annotation_and_entries() {
2367 let src = "commons app.messages {\n\
2369 -- intro\n\
2370 ---\n\
2371 docs\n\
2372 ---\n\
2373 messages \"en\" @reference {\n\
2374 \"greeting\" => \"Hello, {name}!\"\n\
2375 \"farewell\" => \"Bye\"\n\
2376 } -- trailing\n\
2377 }";
2378 let c = parse_str(src).unwrap();
2379 let CommonsItem::Messages(m) = &c.items[0] else {
2380 panic!("expected a messages item, got {:?}", c.items[0]);
2381 };
2382 assert_eq!(m.tag, "en");
2383 assert_eq!(m.annotations.len(), 1);
2384 assert_eq!(m.annotations[0].name.name, "reference");
2385 assert!(m.annotations[0].args.is_empty());
2386 assert_eq!(m.entries.len(), 2);
2387 assert_eq!(m.entries[0].code, "greeting");
2388 assert_eq!(m.entries[0].template, "Hello, {name}!");
2389 assert_eq!(m.entries[1].code, "farewell");
2390 assert_eq!(m.entries[1].template, "Bye");
2391 assert_eq!(m.trivia.leading, vec![Comment::Line(" intro".to_string())]);
2392 assert_eq!(m.documentation.as_deref(), Some("docs"));
2393 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
2394 }
2395
2396 #[test]
2397 fn messages_decl_parses_with_no_annotation_and_no_entries() {
2398 let src = "commons app.messages {\nmessages \"en\" {\n}\n}";
2402 let c = parse_str(src).unwrap();
2403 let CommonsItem::Messages(m) = &c.items[0] else {
2404 panic!("expected a messages item, got {:?}", c.items[0]);
2405 };
2406 assert_eq!(m.tag, "en");
2407 assert!(m.annotations.is_empty());
2408 assert!(m.entries.is_empty());
2409 }
2410
2411 #[test]
2412 fn messages_decl_parses_syntactically_inside_a_context_too() {
2413 let src = "context app.svc {\nmessages \"en\" @reference {\n\"a\" => \"b\"\n}\n}";
2418 let toks = tokenize(src).unwrap();
2419 let (unit, errors) = parse_unit_with_recovery(&toks, src);
2420 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
2421 let Some(SourceUnit::Context(ctx)) = unit else {
2422 panic!("expected a context")
2423 };
2424 let CommonsItem::Messages(m) = &ctx.items[0] else {
2425 panic!("expected a messages item, got {:?}", ctx.items[0]);
2426 };
2427 assert_eq!(m.tag, "en");
2428 }
2429
2430 #[test]
2431 fn comment_before_let_statement_attaches() {
2432 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
2433 let c = parse_str(src).unwrap();
2434 let CommonsItem::Fn(f) = &c.items[0] else {
2435 panic!()
2436 };
2437 let Statement::Let(l) = &f.body.statements[0] else {
2438 panic!()
2439 };
2440 assert_eq!(
2441 l.trivia.leading,
2442 vec![Comment::Line(" pick a value".to_string())]
2443 );
2444 }
2445
2446 #[test]
2447 fn comment_before_tail_attaches_to_block_tail() {
2448 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
2449 let c = parse_str(src).unwrap();
2450 let CommonsItem::Fn(f) = &c.items[0] else {
2451 panic!()
2452 };
2453 assert_eq!(
2454 f.body.tail_leading_comments,
2455 vec![Comment::Line(" result".to_string())],
2456 );
2457 }
2458
2459 #[test]
2465 fn contextual_keywords_are_valid_identifiers() {
2466 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
2468 .expect("`on`/`suite`/`case` are valid field names");
2469 let CommonsItem::Type(_) = &c.items[0] else {
2470 panic!("expected a type decl")
2471 };
2472
2473 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
2475 .expect("`on`/`case` are valid parameter names");
2476
2477 parse_str("commons demo {\n type R = { suite: Int }\n}")
2479 .expect("`suite` is a valid field name");
2480 }
2481
2482 #[test]
2490 fn is_reserved_keyword_covers_every_lexer_keyword() {
2491 let lexer_src = include_str!("lexer.rs");
2492 let mut words = Vec::new();
2493 for line in lexer_src.lines() {
2494 let t = line.trim();
2495 if let Some(rest) = t.strip_prefix("#[token(\"")
2496 && let Some(word) = rest.split('"').next()
2497 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
2498 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
2499 {
2500 words.push(word.to_string());
2501 }
2502 }
2503 assert!(
2504 words.len() > 30,
2505 "keyword extraction looks broken: only {} words",
2506 words.len()
2507 );
2508 use crate::keywords::RESERVED_CONTEXTUAL;
2511 let mut unclassified = Vec::new();
2512 for word in &words {
2513 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
2514 let kind = tokens.first().expect("keyword yields a token").kind;
2515 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
2516 unclassified.push(word.clone());
2517 }
2518 }
2519 assert!(
2520 unclassified.is_empty(),
2521 "keywords missing from is_reserved_keyword (add them, or document \
2522 them as contextual): {unclassified:?}"
2523 );
2524 }
2525
2526 #[test]
2537 fn is_item_start_matches_the_pinned_keyword_set() {
2538 use TokenKind::*;
2539 let expected_true = [
2540 Commons, Context, Adapter, Suite, Type, Fn, Messages, Event, Asset, Uses, Consumes,
2541 Exports, Capability, Provides, Service, Agent, Actor, Binding, Stub, Case, Property,
2542 ];
2543 for kind in expected_true {
2544 assert!(is_item_start(kind), "{kind:?} must be an item start");
2545 }
2546 let expected_false = [
2547 Ident, Plus, Minus, Colon, Dot, Eq, LBrace, RBrace, LParen, RParen, If, Else, Let,
2548 Where, True, False, Match, Is, On, Given,
2549 ];
2550 for kind in expected_false {
2551 assert!(!is_item_start(kind), "{kind:?} must not be an item start");
2552 }
2553 }
2554
2555 #[test]
2560 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
2561 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
2562 let tokens = crate::lexer::tokenize(src).unwrap();
2563 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
2564 assert!(unit.is_some(), "the commons header still parses");
2565 assert!(
2566 errors
2567 .iter()
2568 .any(|e| e.category == "bynk.capability.outside_context"),
2569 "the misplaced capability is reported: {errors:?}"
2570 );
2571 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
2574 }
2575
2576 #[test]
2577 fn trailing_file_comment_becomes_unit_trailing() {
2578 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
2582 let c = parse_str(src).unwrap();
2583 assert_eq!(
2584 c.trailing_comments,
2585 vec![Comment::Line(" afterword".to_string())]
2586 );
2587 }
2588
2589 #[test]
2590 fn trailing_file_comment_after_a_brace_form_commons_is_not_dropped() {
2591 let src = "commons x {\n type T = Int where Positive\n}\n-- afterword\n";
2598 let c = parse_str(src).unwrap();
2599 assert_eq!(
2600 c.trailing_comments,
2601 vec![Comment::Line(" afterword".to_string())]
2602 );
2603 }
2604
2605 #[test]
2606 fn trailing_file_comment_after_a_brace_form_context_is_not_dropped() {
2607 let src = "context x {\n type T = Int where Positive\n}\n-- afterword\n";
2609 let SourceUnit::Context(c) = parse_unit_str(src).unwrap() else {
2610 panic!("expected context");
2611 };
2612 assert_eq!(
2613 c.trailing_comments,
2614 vec![Comment::Line(" afterword".to_string())]
2615 );
2616 }
2617
2618 #[test]
2619 fn trailing_file_comment_after_a_brace_form_suite_is_not_dropped() {
2620 let src = "suite x {\n case \"c\" {\n expect 1 == 1\n }\n}\n-- afterword\n";
2622 let SourceUnit::Suite(s) = parse_unit_str(src).unwrap() else {
2623 panic!("expected suite");
2624 };
2625 assert_eq!(
2626 s.trailing_comments,
2627 vec![Comment::Line(" afterword".to_string())]
2628 );
2629 }
2630
2631 #[test]
2638 fn trailing_file_comment_after_a_brace_form_adapter_is_not_dropped() {
2639 let src = "adapter x {\n binding \"./x.ts\"\n}\n-- afterword\n";
2640 let SourceUnit::Adapter(a) = parse_unit_str(src).unwrap() else {
2641 panic!("expected adapter");
2642 };
2643 assert_eq!(
2644 a.trailing_comments,
2645 vec![Comment::Line(" afterword".to_string())]
2646 );
2647 }
2648
2649 #[test]
2656 fn commons_fragment_rejects_uses_after_a_decl() {
2657 let src = "commons x\n\ntype T = Int where Positive\nuses bynk.list\n";
2658 let errs = parse_str(src).unwrap_err();
2659 assert!(
2660 errs.iter()
2661 .any(|e| e.category == "bynk.parse.uses_after_decls"),
2662 "{errs:?}"
2663 );
2664 }
2665
2666 #[test]
2669 fn commons_brace_allows_uses_after_a_decl() {
2670 let src = "commons x {\n type T = Int where Positive\n uses bynk.list\n}\n";
2671 parse_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2672 }
2673
2674 #[test]
2676 fn context_fragment_rejects_consumes_after_a_decl() {
2677 let src = "context x\n\ntype T = Int where Positive\nconsumes bynk\n";
2678 let errs = parse_unit_str(src).unwrap_err();
2679 assert!(
2680 errs.iter()
2681 .any(|e| e.category == "bynk.parse.consumes_after_decls"),
2682 "{errs:?}"
2683 );
2684 }
2685
2686 #[test]
2688 fn context_fragment_rejects_exports_after_a_decl() {
2689 let src = "context x\n\ntype T = Int where Positive\nexports opaque { T }\n";
2690 let errs = parse_unit_str(src).unwrap_err();
2691 assert!(
2692 errs.iter()
2693 .any(|e| e.category == "bynk.parse.exports_after_decls"),
2694 "{errs:?}"
2695 );
2696 }
2697
2698 #[test]
2700 fn context_brace_allows_consumes_and_exports_after_a_decl() {
2701 let src = "context x {\n type T = Int where Positive\n consumes bynk\n exports opaque { T }\n}\n";
2702 parse_unit_str(src)
2703 .expect("brace form must not enforce fragment's consumes/exports-ordering rules");
2704 }
2705
2706 #[test]
2708 fn test_fragment_rejects_uses_after_a_decl() {
2709 let src = "suite m\n\ncase \"c\" {\n expect true\n}\nuses bynk.list\n";
2710 let errs = parse_unit_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]
2720 fn test_brace_allows_uses_after_a_decl() {
2721 let src = "suite m {\n case \"c\" {\n expect true\n }\n uses bynk.list\n}\n";
2722 parse_unit_str(src).expect("brace form must not enforce fragment's uses-ordering rule");
2723 }
2724
2725 fn body_tail(body: &str) -> ExprKind {
2729 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2730 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
2731 let CommonsItem::Fn(f) = &c.items[0] else {
2732 panic!("expected fn, got {:?}", c.items[0]);
2733 };
2734 f.body.tail.kind.clone()
2735 }
2736
2737 fn body_err(body: &str) -> Vec<CompileError> {
2738 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
2739 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
2740 }
2741
2742 #[test]
2743 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
2744 for src in [
2747 "if ready { result } else { fallback }",
2748 "if ready { fallback } else { result }",
2749 "if !ready { result } else { fallback }",
2750 "if a == b { result } else { fallback }",
2751 "if a && b { result } else { fallback }",
2752 ] {
2753 let ExprKind::If {
2754 then_block,
2755 else_block,
2756 ..
2757 } = body_tail(src)
2758 else {
2759 panic!("expected If for {src:?}, got {:?}", body_tail(src));
2760 };
2761 assert!(
2764 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
2765 "then-branch tail not an ident for {src:?}: {:?}",
2766 then_block.tail.kind,
2767 );
2768 assert!(
2769 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
2770 "else-branch tail not an ident for {src:?}: {:?}",
2771 else_block.tail.kind,
2772 );
2773 }
2774 }
2775
2776 #[test]
2777 fn else_less_if_with_single_ident_branch_parses() {
2778 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
2780 panic!("expected If");
2781 };
2782 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
2783 }
2784
2785 #[test]
2786 fn record_construction_still_parses_in_value_position() {
2787 assert!(matches!(
2790 body_tail("Point { x }"),
2791 ExprKind::RecordConstruction { .. }
2792 ));
2793 assert!(matches!(
2794 body_tail("Point { x: 1, y: 2 }"),
2795 ExprKind::RecordConstruction { .. }
2796 ));
2797 assert!(matches!(
2798 body_tail("Empty {}"),
2799 ExprKind::RecordConstruction { .. }
2800 ));
2801 }
2802
2803 #[test]
2804 fn parenthesised_record_is_allowed_in_condition_head() {
2805 let ExprKind::If { cond, .. } =
2808 body_tail("if (ready { result }) { branch } else { other }")
2809 else {
2810 panic!("expected If");
2811 };
2812 let ExprKind::Paren(inner) = &cond.kind else {
2813 panic!("expected a parenthesised condition, got {:?}", cond.kind);
2814 };
2815 assert!(
2816 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
2817 "parenthesised record in condition head should still construct: {:?}",
2818 inner.kind,
2819 );
2820 }
2821
2822 #[test]
2823 fn record_in_call_arg_within_condition_still_constructs() {
2824 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
2827 else {
2828 panic!("expected If");
2829 };
2830 let ExprKind::Call { args, .. } = &cond.kind else {
2831 panic!("expected Call in condition, got {:?}", cond.kind);
2832 };
2833 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
2834 }
2835
2836 #[test]
2837 fn safe_condition_shapes_are_unaffected() {
2838 assert!(matches!(
2840 body_tail("if ready == true { result } else { fallback }"),
2841 ExprKind::If { .. }
2842 ));
2843 assert!(matches!(
2844 body_tail("if (ready) { result } else { fallback }"),
2845 ExprKind::If { .. }
2846 ));
2847 assert!(matches!(
2848 body_tail("if ready { \"a\" } else { \"b\" }"),
2849 ExprKind::If { .. }
2850 ));
2851 }
2852
2853 #[test]
2854 fn empty_match_reports_its_own_diagnostic() {
2855 let errs = body_err("match result {}");
2859 assert!(
2860 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
2861 "expected empty_match; got {errs:?}",
2862 );
2863 }
2864
2865 #[test]
2866 fn match_discriminant_ending_in_ident_parses() {
2867 assert!(matches!(
2869 body_tail("match ready { x => x }"),
2870 ExprKind::Match { .. }
2871 ));
2872 }
2873
2874 #[test]
2883 fn identifier_statement_followed_by_unit_tail_does_not_merge_into_a_call() {
2884 let src = "commons c\n\nfn f() -> Int {\n status := Paid\n ()\n}\n";
2885 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2886 let CommonsItem::Fn(f) = &c.items[0] else {
2887 panic!("expected fn, got {:?}", c.items[0]);
2888 };
2889 assert_eq!(
2890 f.body.statements.len(),
2891 1,
2892 "expected exactly one Assign statement, got {:?}",
2893 f.body.statements
2894 );
2895 let Statement::Assign(a) = &f.body.statements[0] else {
2896 panic!(
2897 "expected an Assign statement, got {:?}",
2898 f.body.statements[0]
2899 );
2900 };
2901 assert!(
2902 matches!(a.value.kind, ExprKind::Ident(_)),
2903 "assign value must stay the bare identifier `Paid`, got {:?}",
2904 a.value.kind
2905 );
2906 assert!(
2907 matches!(f.body.tail.kind, ExprKind::UnitLit),
2908 "the `()` must remain the block's own tail, got {:?}",
2909 f.body.tail.kind
2910 );
2911 }
2912
2913 #[test]
2918 fn variant_pattern_followed_by_unit_tail_does_not_take_it_as_bindings() {
2919 for (name, pat) in [("user variant", "Paid"), ("built-in variant", "None")] {
2920 let src = format!("commons c\n\nfn f() -> () {{\n let b = r is {pat}\n ()\n}}\n");
2921 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
2922 let CommonsItem::Fn(f) = &c.items[0] else {
2923 panic!("expected fn, got {:?}", c.items[0]);
2924 };
2925 let Statement::Let(l) = &f.body.statements[0] else {
2926 panic!("{name}: expected a Let, got {:?}", f.body.statements[0]);
2927 };
2928 let ExprKind::Is { pattern, .. } = &l.value.kind else {
2929 panic!("{name}: expected an `is` test, got {:?}", l.value.kind);
2930 };
2931 let Pattern::Variant { variant, span, .. } = pattern.as_ref() else {
2932 panic!("{name}: expected a variant pattern, got {pattern:?}");
2933 };
2934 assert_eq!(
2935 span.end, variant.span.end,
2936 "{name}: the pattern must end at `{pat}`, not take the `()` below it"
2937 );
2938 assert!(
2939 matches!(f.body.tail.kind, ExprKind::UnitLit),
2940 "{name}: the `()` must remain the block's own tail, got {:?}",
2941 f.body.tail.kind
2942 );
2943 }
2944 }
2945
2946 #[test]
2949 fn variant_pattern_payload_on_the_same_line_still_binds() {
2950 for (name, pat) in [
2951 ("user variant", "Paid(\n x\n )"),
2952 ("built-in variant", "Some(x)"),
2953 ] {
2954 let src = format!("commons c\n\nfn f() -> Bool {{\n r is {pat}\n}}\n");
2955 let c = parse_str(&src).unwrap_or_else(|e| panic!("{name}: parse failed: {e:?}"));
2956 let CommonsItem::Fn(f) = &c.items[0] else {
2957 panic!("expected fn, got {:?}", c.items[0]);
2958 };
2959 let ExprKind::Is { pattern, .. } = &f.body.tail.kind else {
2960 panic!("{name}: expected an `is` tail, got {:?}", f.body.tail.kind);
2961 };
2962 assert!(
2963 matches!(pattern.as_ref(), Pattern::Variant { bindings, .. } if bindings.len() == 1),
2964 "{name}: expected one binding, got {pattern:?}"
2965 );
2966 }
2967 }
2968
2969 #[test]
2975 fn match_arm_pattern_payload_on_a_later_line_is_rejected() {
2976 let src = "commons c\n\nfn f() -> Int {\n match m {\n Settled\n (amount) => 1,\n _ => 0,\n }\n}\n";
2977 assert!(
2978 parse_str(src).is_err(),
2979 "a next-line `(` after an arm's variant must not open its payload list"
2980 );
2981 let same_line = src.replace("Settled\n (amount)", "Settled(amount)");
2982 parse_str(&same_line).unwrap_or_else(|e| panic!("same-line form must parse: {e:?}"));
2983 }
2984
2985 #[test]
2989 fn method_reference_followed_by_unit_tail_does_not_merge_into_a_call() {
2990 let src = "commons c\n\nfn f() -> Int {\n let y = x.field\n ()\n}\n";
2991 let c = parse_str(src).unwrap_or_else(|e| panic!("parse failed: {e:?}"));
2992 let CommonsItem::Fn(f) = &c.items[0] else {
2993 panic!("expected fn, got {:?}", c.items[0]);
2994 };
2995 let Statement::Let(l) = &f.body.statements[0] else {
2996 panic!("expected a Let statement, got {:?}", f.body.statements[0]);
2997 };
2998 assert!(
2999 matches!(l.value.kind, ExprKind::FieldAccess { .. }),
3000 "let value must stay a field access, got {:?}",
3001 l.value.kind
3002 );
3003 assert!(
3004 matches!(f.body.tail.kind, ExprKind::UnitLit),
3005 "the `()` must remain the block's own tail, got {:?}",
3006 f.body.tail.kind
3007 );
3008 }
3009
3010 #[test]
3011 fn unparenthesised_record_in_condition_head_now_errors() {
3012 assert!(
3018 !body_err("match Point { x: 1 } { p => p }").is_empty(),
3019 "unparenthesised record discriminant should not parse",
3020 );
3021 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
3023 else {
3024 panic!("expected Match for the parenthesised form");
3025 };
3026 let ExprKind::Paren(inner) = &discriminant.kind else {
3027 panic!(
3028 "expected a parenthesised discriminant, got {:?}",
3029 discriminant.kind
3030 );
3031 };
3032 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
3033 }
3034
3035 fn strict_codes(src: &str) -> Vec<&'static str> {
3037 let toks = tokenize(src).unwrap();
3038 match parse_units_with_warnings(&toks, src) {
3039 Ok((_, warnings)) => warnings.iter().map(|e| e.category).collect(),
3040 Err(errs) => errs.iter().map(|e| e.category).collect(),
3041 }
3042 }
3043
3044 #[test]
3048 fn a_divider_pair_around_code_is_doc_block_contains_code() {
3049 let src =
3050 "commons m\n\n--------\nfn helper() -> Int { 1 }\n--------\nfn f() -> Int { 2 }\n";
3051 assert_eq!(strict_codes(src), ["bynk.parse.doc_block_contains_code"]);
3052 let errs = parse_str(src).unwrap_err();
3053 assert_eq!(errs[0].category, "bynk.parse.doc_block_contains_code");
3054 assert_eq!(
3056 &src[errs[0].span.range()],
3057 "--------\nfn helper() -> Int { 1 }\n--------\n"
3058 );
3059 let (unit, errs) = parse_recover_str(src);
3060 assert!(unit.is_some(), "recovery keeps the unit");
3061 assert!(
3062 errs.iter()
3063 .any(|e| e.category == "bynk.parse.doc_block_contains_code"),
3064 "{errs:?}"
3065 );
3066 }
3067
3068 #[test]
3071 fn an_orphaned_or_trailing_divider_pair_is_doc_block_contains_code() {
3072 let orphan = "commons m\n\n---\nfn helper() -> Int { 1 }\n---\n\nfn f() -> Int { 2 }\n";
3073 let codes = strict_codes(orphan);
3074 assert!(
3075 codes.contains(&"bynk.parse.doc_block_contains_code"),
3076 "{codes:?}"
3077 );
3078 let trailing = "commons m\n\nfn f() -> Int { 2 }\n\n---\ntype Id = Int\n---\n";
3079 let codes = strict_codes(trailing);
3080 assert!(
3081 codes.contains(&"bynk.parse.doc_block_contains_code"),
3082 "{codes:?}"
3083 );
3084 }
3085
3086 #[test]
3090 fn near_miss_prose_and_fenced_code_are_not_code() {
3091 for doc in [
3092 "type of the thing",
3093 "fn is used here",
3094 "The type Id is an Int.",
3095 "uses the clock",
3096 "Event handler.\nfn helper() -> Int { 1 }",
3097 "Example:\n```bynk\nfn helper() -> Int { 1 }\n```",
3098 "~~~~\ntype Id = Int\n~~~~",
3099 "---\nA nested doc.\n---",
3100 "",
3101 ] {
3102 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3103 }
3104 for doc in [
3105 "fn helper() -> Int { 1 }",
3106 "type Id = Int\nfn f() -> Int { 1 }",
3107 "```\nan example\n```\nfn helper() -> Int { 1 }",
3108 "---\nA nested doc.\n---\nfn helper() -> Int { 1 }",
3109 ] {
3110 assert!(doc_content_is_code(doc), "{doc:?} not read as code");
3111 }
3112 }
3113
3114 #[test]
3118 fn code_inside_a_nested_doc_or_beside_a_rule_is_code() {
3119 let nested =
3120 "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n---\n----\nfn f() -> Int { 2 }\n";
3121 assert_eq!(strict_codes(nested), ["bynk.parse.doc_block_contains_code"]);
3122 let ruled = "commons m\n\n----\n---\nfn helper() -> Int { 1 }\n----\nfn f() -> Int { 2 }\n";
3123 assert_eq!(strict_codes(ruled), ["bynk.parse.doc_block_contains_code"]);
3124 assert!(doc_content_is_code(
3126 "----\n---\nfn helper() -> Int { 1 }\n---\n----"
3127 ));
3128 for doc in [
3129 "---\nAn inner doc.\n---",
3130 "Intro.\n\n---\n\nfn is used here",
3131 "---\n```\nfn helper() -> Int { 1 }\n```\n---",
3132 ] {
3133 assert!(!doc_content_is_code(doc), "{doc:?} read as code");
3134 }
3135 let mut deep = "fn helper() -> Int { 1 }".to_string();
3137 for n in (3..3 + DOC_PROBE_MAX_DEPTH + 1).rev() {
3138 let fence = "-".repeat(n);
3139 deep = format!("{fence}\n{deep}\n{fence}");
3140 }
3141 assert!(!doc_content_is_code(&deep));
3142 }
3143
3144 #[test]
3147 fn a_fenced_nested_doc_example_parses_clean() {
3148 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";
3149 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3150 let c = parse_str(src).unwrap();
3151 let CommonsItem::Fn(f) = &c.items[0] else {
3152 panic!("expected fn")
3153 };
3154 assert!(
3155 f.documentation
3156 .as_deref()
3157 .is_some_and(|d| d.contains("---\nInner.\n---")),
3158 "{:?}",
3159 f.documentation
3160 );
3161 }
3162
3163 fn type_at(c: &Commons, i: usize) -> &TypeDecl {
3165 let CommonsItem::Type(t) = &c.items[i] else {
3166 panic!("expected a type at {i}")
3167 };
3168 t
3169 }
3170
3171 fn doc_of(d: &Option<Doc>) -> Option<(&str, DocForm)> {
3172 d.as_ref().map(|d| (d.as_str().trim_end(), d.form))
3175 }
3176
3177 #[test]
3180 fn doc_lines_document_a_declaration() {
3181 let src = "commons m\n\n--| A commit SHA.\ntype Commit = String\n\n--| First.\n--|\n--| Indented.\nfn f() -> Int { 1 }\n";
3182 let c = parse_str(src).unwrap();
3183 assert_eq!(
3184 doc_of(&type_at(&c, 0).documentation),
3185 Some(("A commit SHA.", DocForm::Lines))
3186 );
3187 let CommonsItem::Fn(f) = &c.items[1] else {
3188 panic!("expected fn")
3189 };
3190 assert_eq!(
3191 doc_of(&f.documentation),
3192 Some(("First.\n\n Indented.", DocForm::Lines))
3193 );
3194 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3195 }
3196
3197 #[test]
3202 fn members_take_docs_in_either_form() {
3203 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";
3204 assert!(strict_codes(src).is_empty(), "{:?}", strict_codes(src));
3205 let c = parse_str(src).unwrap();
3206 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3207 panic!("record")
3208 };
3209 assert_eq!(
3210 doc_of(&r.fields[0].documentation),
3211 Some(("The repository.", DocForm::Lines))
3212 );
3213 assert_eq!(
3214 doc_of(&r.fields[1].documentation),
3215 Some(("The version.", DocForm::Block))
3216 );
3217 assert_eq!(r.fields[2].documentation, None);
3218 assert_eq!(
3219 r.fields[2].trivia.leading,
3220 vec![Comment::Line(" a note".into())]
3221 );
3222 assert_eq!(r.fields[2].trivia.trailing.as_deref(), Some("| trailing"));
3223 let TypeBody::Sum(e) = &type_at(&c, 1).body else {
3224 panic!("enum")
3225 };
3226 assert_eq!(
3227 doc_of(&e.variants[0].documentation),
3228 Some(("Passed.", DocForm::Lines))
3229 );
3230 assert_eq!(e.variants[1].documentation, None);
3231 let TypeBody::Sum(p) = &type_at(&c, 2).body else {
3232 panic!("sum")
3233 };
3234 assert_eq!(
3235 doc_of(&p.variants[0].documentation),
3236 Some(("Round.", DocForm::Lines))
3237 );
3238 assert_eq!(
3239 doc_of(&p.variants[0].payload[0].documentation),
3240 Some(("The radius.", DocForm::Lines))
3241 );
3242 assert_eq!(p.variants[1].documentation, None);
3243 let CommonsItem::Fn(f) = &c.items[3] else {
3245 panic!("expected fn")
3246 };
3247 assert_eq!(
3248 doc_of(&f.documentation),
3249 Some(("After the sum.", DocForm::Lines))
3250 );
3251 let CommonsItem::Messages(m) = &c.items[4] else {
3252 panic!("expected messages")
3253 };
3254 assert_eq!(
3255 doc_of(&m.entries[0].documentation),
3256 Some(("Greets.", DocForm::Lines))
3257 );
3258 }
3259
3260 #[test]
3264 fn both_doc_forms_on_one_target_is_doc_forms_mixed() {
3265 for src in [
3266 "commons m\n\n---\nBlock.\n---\n--| Lines.\nfn f() -> Int { 1 }\n",
3267 "commons m\n\n--| Lines.\n---\nBlock.\n---\nfn f() -> Int { 1 }\n",
3268 "commons m\n\ntype R = {\n --| Lines.\n ---\n Block.\n ---\n a: Int,\n}\n",
3269 ] {
3270 assert_eq!(strict_codes(src), ["bynk.parse.doc_forms_mixed"], "{src}");
3271 let (unit, errs) = parse_recover_str(src);
3272 assert!(unit.is_some());
3273 assert!(
3274 errs.iter()
3275 .any(|e| e.category == "bynk.parse.doc_forms_mixed")
3276 );
3277 }
3278 }
3279
3280 #[test]
3283 fn a_dangling_doc_before_a_payload_close_is_only_an_orphan() {
3284 let src =
3285 "commons m\n\ntype S =\n | Circle(\n r: Int,\n --| dangling\n )\n | Dot\n";
3286 assert_eq!(strict_codes(src), ["bynk.parse.orphan_doc_block"]);
3287 let c = parse_str(src).unwrap();
3288 let TypeBody::Sum(s) = &type_at(&c, 0).body else {
3289 panic!("sum")
3290 };
3291 assert_eq!(s.variants[0].payload.len(), 1);
3292 assert_eq!(s.variants[0].payload[0].documentation, None);
3293 }
3294
3295 #[test]
3298 fn an_unattached_member_doc_is_an_orphan() {
3299 let src = "commons m\n\ntype R = {\n --| Spaced.\n\n a: Int,\n --| Dangling.\n}\n";
3300 assert_eq!(
3301 strict_codes(src),
3302 ["bynk.parse.orphan_doc_block", "bynk.parse.orphan_doc_block"]
3303 );
3304 let c = parse_str(src).unwrap();
3305 let TypeBody::Record(r) = &type_at(&c, 0).body else {
3306 panic!("record")
3307 };
3308 assert_eq!(r.fields[0].documentation, None);
3309 assert!(matches!(
3310 &r.fields[0].trivia.leading[..],
3311 [Comment::OrphanDoc(d)] if d.as_str() == "Spaced." && d.form == DocForm::Lines
3312 ));
3313 assert!(matches!(
3314 &r.trailing_comments[..],
3315 [Comment::OrphanDoc(d)] if d.as_str() == "Dangling."
3316 ));
3317 }
3318}