1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{Token, TokenKind, comment_body, doc_block_content, has_blank_line_between};
11use crate::span::Span;
12mod declarations;
13mod expressions;
14mod statements;
15mod types;
16
17#[derive(Debug, Default)]
26struct TriviaTable {
27 leading: Vec<Vec<String>>,
31 trailing: Vec<Option<String>>,
35 epilogue: Vec<String>,
38}
39
40impl TriviaTable {
41 fn take_leading(&mut self, index: usize) -> Vec<String> {
42 match self.leading.get_mut(index) {
43 Some(v) => std::mem::take(v),
44 None => Vec::new(),
45 }
46 }
47
48 fn take_trailing(&mut self, index: usize) -> Option<String> {
49 self.trailing.get_mut(index).and_then(|s| s.take())
50 }
51
52 fn take_epilogue(&mut self) -> Vec<String> {
53 std::mem::take(&mut self.epilogue)
54 }
55}
56
57fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
63 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
64 let mut table = TriviaTable::default();
65 let mut pending_leading: Vec<String> = Vec::new();
66 let mut last_content_end: Option<usize> = None;
67 for tok in tokens {
68 if tok.kind == TokenKind::Comment {
69 let body = comment_body(source, tok.span).to_string();
70 if pending_leading.is_empty()
74 && let Some(prev_end) = last_content_end
75 && !source[prev_end..tok.span.start].contains('\n')
76 {
77 let last_idx = filtered.len() - 1;
78 if table.trailing[last_idx].is_none() {
81 table.trailing[last_idx] = Some(body);
82 continue;
83 }
84 }
85 pending_leading.push(body);
86 continue;
87 }
88 filtered.push(*tok);
89 table.leading.push(std::mem::take(&mut pending_leading));
90 table.trailing.push(None);
91 last_content_end = Some(tok.span.end);
92 }
93 table.epilogue = pending_leading;
94 (filtered, table)
95}
96
97pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
103 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
104}
105
106pub fn parse_with_warnings(
109 tokens: &[Token],
110 source: &str,
111) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
112 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
113 match unit {
114 SourceUnit::Commons(c) => Ok((c, warnings)),
115 SourceUnit::Context(ctx) => Err(vec![
116 CompileError::new(
117 "bynk.parse.unexpected_context",
118 ctx.span,
119 "expected a `commons` declaration but found a `context` declaration",
120 )
121 .with_note(
122 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
123 ),
124 ]),
125 SourceUnit::Suite(t) => Err(vec![
126 CompileError::new(
127 "bynk.parse.unexpected_suite",
128 t.span,
129 "expected a `commons` declaration but found a `suite` declaration",
130 )
131 .with_note(
132 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
133 ),
134 ]),
135 SourceUnit::Adapter(a) => Err(vec![
136 CompileError::new(
137 "bynk.parse.unexpected_adapter",
138 a.span,
139 "expected a `commons` declaration but found an `adapter` declaration",
140 )
141 .with_note(
142 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
143 ),
144 ]),
145 }
146}
147
148pub fn parse_unit_with_recovery(
157 tokens: &[Token],
158 source: &str,
159) -> (Option<SourceUnit>, Vec<CompileError>) {
160 let (filtered, trivia) = split_trivia(tokens, source);
161 let mut warnings = Vec::new();
162 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
163 p.recover_mode = true;
164 let unit_opt = match p.parse_unit() {
165 Ok(u) => {
166 while p.peek().is_some() {
172 match p.parse_unit() {
173 Ok(_) => {}
174 Err(e) => {
175 p.recovered_errors.push(e);
176 break;
177 }
178 }
179 }
180 Some(u)
181 }
182 Err(e) => {
183 p.recovered_errors.push(e);
184 None
185 }
186 };
187 let mut all_errors = p.recovered_errors;
188 all_errors.append(&mut warnings);
189 (unit_opt, all_errors)
190}
191
192pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
196 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
197}
198
199pub fn parse_unit_with_warnings(
202 tokens: &[Token],
203 source: &str,
204) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
205 let (filtered, trivia) = split_trivia(tokens, source);
206 let mut warnings = Vec::new();
207 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
208 let result = match p.parse_unit() {
209 Ok(u) => {
210 if let Some(extra) = p.peek() {
211 Err(vec![
212 CompileError::new(
213 "bynk.parse.extra_tokens",
214 extra.span,
215 "unexpected token after top-level declaration",
216 )
217 .with_note(
218 "a `.bynk` file contains exactly one `commons` or `context` declaration",
219 ),
220 ])
221 } else {
222 Ok(u)
223 }
224 }
225 Err(e) => Err(vec![e]),
226 };
227 match result {
230 Ok(u) => Ok((u, warnings)),
231 Err(mut errs) => {
232 errs.append(&mut warnings);
233 Err(errs)
234 }
235 }
236}
237
238pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
247 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
248}
249
250pub fn parse_units_with_warnings(
256 tokens: &[Token],
257 source: &str,
258) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
259 let (filtered, trivia) = split_trivia(tokens, source);
260 let mut warnings = Vec::new();
261 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
262 let mut units = Vec::new();
263 let mut errors: Vec<CompileError> = Vec::new();
264 while p.peek().is_some() {
265 match p.parse_unit() {
266 Ok(u) => units.push(u),
267 Err(e) => {
268 errors.push(e);
269 break;
270 }
271 }
272 }
273 let eof = p.eof_span();
274 if !errors.is_empty() {
277 errors.append(&mut warnings);
278 return Err(errors);
279 }
280 if units.is_empty() {
281 return Err(vec![CompileError::new(
282 "bynk.parse.unexpected_eof",
283 eof,
284 "expected `commons`, `context`, or `suite` to start the file, found end of file",
285 )]);
286 }
287 Ok((units, warnings))
288}
289
290enum SignedNumLit {
293 Int(IntBound),
294 Float(FloatBound),
295}
296
297struct Parser<'a> {
298 tokens: &'a [Token],
299 source: &'a str,
300 pos: usize,
301 warnings: &'a mut Vec<CompileError>,
304 recover_mode: bool,
310 recovered_errors: Vec<CompileError>,
313 trivia: TriviaTable,
316}
317
318impl<'a> Parser<'a> {
319 fn new(
320 tokens: &'a [Token],
321 source: &'a str,
322 trivia: TriviaTable,
323 warnings: &'a mut Vec<CompileError>,
324 ) -> Self {
325 Self {
326 tokens,
327 source,
328 pos: 0,
329 warnings,
330 recover_mode: false,
331 recovered_errors: Vec::new(),
332 trivia,
333 }
334 }
335
336 fn take_leading_trivia(&mut self) -> Vec<String> {
340 self.trivia.take_leading(self.pos)
341 }
342
343 fn take_trailing_trivia(&mut self) -> Option<String> {
347 if self.pos == 0 {
348 return None;
349 }
350 self.trivia.take_trailing(self.pos - 1)
351 }
352
353 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
357 if self.recover_mode {
358 self.recovered_errors.push(e);
359 let before = self.pos;
360 self.recover_to_top_item();
361 if self.pos == before {
368 self.bump();
369 }
370 Ok(())
371 } else {
372 Err(e)
373 }
374 }
375
376 fn recover_to_top_item(&mut self) {
381 while let Some(t) = self.peek() {
382 match t.kind {
383 TokenKind::Type
384 | TokenKind::Fn
385 | TokenKind::Uses
386 | TokenKind::Consumes
387 | TokenKind::Exports
388 | TokenKind::Capability
389 | TokenKind::Provides
390 | TokenKind::Service
391 | TokenKind::Agent
392 | TokenKind::Suite
393 | TokenKind::Case
394 | TokenKind::RBrace
395 | TokenKind::Commons
396 | TokenKind::Context => return,
397 _ => {
398 self.bump();
399 }
400 }
401 }
402 }
403
404 fn peek(&self) -> Option<Token> {
405 self.tokens.get(self.pos).copied()
406 }
407
408 fn peek_kind(&self) -> Option<TokenKind> {
409 self.peek().map(|t| t.kind)
410 }
411
412 fn nth(&self, n: usize) -> Option<Token> {
414 self.tokens.get(self.pos + n).copied()
415 }
416
417 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
418 self.nth(n).map(|t| t.kind)
419 }
420
421 fn nth_text(&self, n: usize) -> &'a str {
423 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
424 }
425
426 fn prev_span(&self) -> Span {
429 self.tokens
430 .get(self.pos.wrapping_sub(1))
431 .or_else(|| self.peek_ref())
432 .map(|t| t.span)
433 .unwrap_or_default()
434 }
435
436 fn peek_ref(&self) -> Option<&Token> {
437 self.tokens.get(self.pos)
438 }
439
440 fn bump(&mut self) -> Option<Token> {
441 let t = self.peek();
442 if t.is_some() {
443 self.pos += 1;
444 }
445 t
446 }
447
448 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
449 if self.peek_kind() == Some(kind) {
450 self.bump()
451 } else {
452 None
453 }
454 }
455
456 fn slice(&self, span: Span) -> &'a str {
457 &self.source[span.range()]
458 }
459
460 fn next_token_on_new_line(&self, prev: Span) -> bool {
465 match self.peek() {
466 Some(t) if prev.end <= t.span.start => {
467 self.source[prev.end..t.span.start].contains('\n')
468 }
469 _ => false,
470 }
471 }
472
473 fn eof_span(&self) -> Span {
478 let end = self.source.len();
479 let start = (0..end)
480 .rev()
481 .find(|&i| self.source.is_char_boundary(i))
482 .unwrap_or(0);
483 Span::new(start, end)
484 }
485
486 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
487 match self.peek() {
488 Some(t) if t.kind == kind => {
489 self.bump();
490 Ok(t)
491 }
492 Some(t) => Err(CompileError::new(
493 "bynk.parse.expected_token",
494 t.span,
495 format!(
496 "expected {} {ctx}, found {}",
497 kind.describe(),
498 t.kind.describe()
499 ),
500 )),
501 None => Err(CompileError::new(
502 "bynk.parse.unexpected_eof",
503 self.eof_span(),
504 format!("expected {} {ctx}, found end of file", kind.describe()),
505 )),
506 }
507 }
508
509 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
510 match self.peek() {
511 Some(t) if t.kind == TokenKind::Ident => {
512 self.bump();
513 Ok(Ident {
514 name: self.slice(t.span).to_string(),
515 span: t.span,
516 })
517 }
518 Some(t) if matches!(t.kind, TokenKind::On | TokenKind::Suite | TokenKind::Case) => {
527 self.bump();
528 Ok(Ident {
529 name: self.slice(t.span).to_string(),
530 span: t.span,
531 })
532 }
533 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
534 "bynk.parse.reserved_keyword",
535 t.span,
536 format!(
537 "expected identifier {ctx}, but `{}` is a reserved keyword",
538 self.slice(t.span)
539 ),
540 )
541 .with_note("rename the identifier to something that is not a keyword")),
542 Some(t) => Err(CompileError::new(
543 "bynk.parse.expected_token",
544 t.span,
545 format!("expected identifier {ctx}, found {}", t.kind.describe()),
546 )),
547 None => Err(CompileError::new(
548 "bynk.parse.unexpected_eof",
549 self.eof_span(),
550 format!("expected identifier {ctx}, found end of file"),
551 )),
552 }
553 }
554
555 fn take_doc_block(&mut self) -> Option<(String, Span)> {
561 if self.peek_kind() == Some(TokenKind::DocBlock) {
562 let t = self.bump().unwrap();
563 let body = doc_block_content(self.source, t.span);
564 return Some((body, t.span));
565 }
566 None
567 }
568
569 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
574 let mut leading = self.take_leading_trivia();
575 let doc = self.take_doc_block();
576 if doc.is_some() {
577 leading.extend(self.take_leading_trivia());
578 }
579 (leading, doc)
580 }
581
582 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
585 let (content, doc_span) = doc?;
586 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
588 self.warnings.push(
589 CompileError::new(
590 "bynk.parse.orphan_doc_block",
591 doc_span,
592 "documentation block is separated from the following declaration by a blank line; it will not be attached",
593 )
594 .with_note(
595 "remove the blank line to attach the doc to the next declaration, \
596 or remove the doc block if it is not meant to document anything",
597 ),
598 );
599 return None;
600 }
601 Some(content)
602 }
603}
604
605fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
608 let bytes = lexeme.as_bytes();
609 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
610 let inner = &lexeme[1..lexeme.len() - 1];
611 let mut out = String::with_capacity(inner.len());
612 let mut chars = inner.chars();
613 while let Some(c) = chars.next() {
614 if c == '\\' {
615 match chars.next() {
616 Some('n') => out.push('\n'),
617 Some('t') => out.push('\t'),
618 Some('"') => out.push('"'),
619 Some('\\') => out.push('\\'),
620 other => {
621 return Err(CompileError::new(
622 "bynk.lex.bad_escape",
623 span,
624 format!(
625 "invalid escape sequence `\\{}` in string literal",
626 other.map(|c| c.to_string()).unwrap_or_default()
627 ),
628 )
629 .with_note("supported escapes: \\n \\t \\\" \\\\"));
630 }
631 }
632 } else {
633 out.push(c);
634 }
635 }
636 Ok(out)
637}
638
639fn is_reserved_keyword(kind: TokenKind) -> bool {
640 use TokenKind::*;
641 matches!(
642 kind,
643 Commons
644 | Type
645 | Fn
646 | Where
647 | And
648 | True
649 | False
650 | Int
651 | String
652 | Bool
653 | Let
654 | If
655 | Else
656 | Ok
657 | Err
658 | Result
659 | ValidationError
660 | Enum
661 | Match
662 | Option
663 | Record
664 | Self_
665 | Some
666 | None
667 | Is
668 | Opaque
669 | Uses
670 | Context
671 | Consumes
672 | Exports
673 | Transparent
674 | Agent
675 | As
676 | Capability
677 | Effect
678 | Do
679 | Given
680 | On
681 | Http
682 | Provides
683 | Service
684 | Actor
685 | By
686 | Expect
687 | Suite
688 | Case
689 | Float
690 | Duration
691 | Instant
692 | Bytes
693 | JsonError
694 | Property
695 | Adapter
696 | Binding
697 | Cron
698 | Queue
699 | From
700 | Protocol
701 | Invariant
702 | Implies
703 | Requires
704 | Ensures
705 | Transition
706 )
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::lexer::tokenize;
713
714 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
715 let toks = tokenize(src).map_err(|e| vec![e])?;
716 parse(&toks, src)
717 }
718
719 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
720 let toks = match tokenize(src) {
721 Ok(t) => t,
722 Err(e) => return (None, vec![e]),
723 };
724 parse_unit_with_recovery(&toks, src)
725 }
726
727 #[test]
728 fn eof_span_never_splits_a_multibyte_codepoint() {
729 for src in [
734 "commons x {\n -- ends with an arrow →",
735 "agent A {\n key k: String\n -- note 🦀",
736 "commons y {\n type T = é",
737 ] {
738 let (_unit, errors) = parse_recover_str(src);
739 for e in &errors {
740 assert!(
741 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
742 "span {:?} splits a codepoint in {src:?}",
743 e.span,
744 );
745 }
746 }
747 }
748
749 #[test]
750 fn recovery_skips_garbage_between_decls() {
751 let src = "commons x {\n\
754 type A = Int where NonNegative\n\
755 ??? !!!\n\
756 type B = String where NonEmpty\n\
757 }";
758 let (unit, errors) = parse_recover_str(src);
759 let unit = unit.expect("recovery should produce a partial AST");
760 let SourceUnit::Commons(c) = unit else {
761 panic!("expected commons")
762 };
763 let names: Vec<_> = c
765 .items
766 .iter()
767 .map(|i| match i {
768 CommonsItem::Type(t) => t.name.name.clone(),
769 _ => panic!("expected only types"),
770 })
771 .collect();
772 assert!(
773 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
774 "expected both A and B; got {names:?}",
775 );
776 assert!(!errors.is_empty(), "expected at least one parse error");
777 }
778
779 #[test]
780 fn recovery_handles_bad_first_decl_then_good_second() {
781 let src = "commons x {\n\
783 type A Int where NonNegative\n\
784 type B = String where NonEmpty\n\
785 }";
786 let (unit, errors) = parse_recover_str(src);
787 let unit = unit.expect("recovery should produce a partial AST");
788 let SourceUnit::Commons(c) = unit else {
789 panic!("expected commons")
790 };
791 let names: Vec<_> = c
792 .items
793 .iter()
794 .filter_map(|i| match i {
795 CommonsItem::Type(t) => Some(t.name.name.clone()),
796 _ => None,
797 })
798 .collect();
799 assert!(
800 names.contains(&"B".to_string()),
801 "B should be parsed after A's failure; got {names:?}"
802 );
803 assert!(!errors.is_empty(), "expected at least one parse error");
804 }
805
806 #[test]
807 fn doc_block_attaches_to_type() {
808 let c =
809 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
810 .unwrap();
811 let CommonsItem::Type(t) = &c.items[0] else {
812 panic!()
813 };
814 assert!(t.documentation.is_some());
815 assert!(
816 t.documentation
817 .as_ref()
818 .unwrap()
819 .contains("A descriptive doc.")
820 );
821 }
822
823 #[test]
824 fn interpolated_string_parses_into_parts() {
825 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
827 .unwrap();
828 let CommonsItem::Fn(f) = &c.items[0] else {
829 panic!("expected fn")
830 };
831 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
832 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
833 };
834 assert_eq!(parts.len(), 3);
835 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
836 assert!(
837 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
838 );
839 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
840 }
841
842 #[test]
843 fn interpolated_hole_parses_a_full_expression() {
844 let c =
846 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
847 .unwrap();
848 let CommonsItem::Fn(f) = &c.items[0] else {
849 panic!("expected fn")
850 };
851 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
852 panic!("expected InterpStr")
853 };
854 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
855 }
856
857 #[test]
858 fn empty_interpolation_hole_is_rejected() {
859 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
860 assert!(
861 errs.iter()
862 .any(|e| e.category == "bynk.parse.empty_interpolation"),
863 "expected empty_interpolation; got {errs:?}"
864 );
865 }
866
867 #[test]
868 fn fragment_form_parses() {
869 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
870 assert_eq!(c.form, CommonsForm::Fragment);
871 assert_eq!(c.items.len(), 1);
872 }
873
874 #[test]
875 fn uses_parses() {
876 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
877 assert_eq!(c.uses.len(), 1);
878 assert_eq!(c.uses[0].target.joined(), "other.lib");
879 }
880
881 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
882 let toks = tokenize(src).map_err(|e| vec![e])?;
883 parse_unit(&toks, src)
884 }
885
886 #[test]
887 fn minimal_context_parses() {
888 let u = parse_unit_str("context commerce.orders {}").unwrap();
889 let SourceUnit::Context(c) = u else {
890 panic!("expected context");
891 };
892 assert_eq!(c.name.joined(), "commerce.orders");
893 assert!(c.items.is_empty());
894 }
895
896 #[test]
897 fn context_consumes_and_exports_parse() {
898 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}";
899 let u = parse_unit_str(src).unwrap();
900 let SourceUnit::Context(c) = u else { panic!() };
901 assert_eq!(c.uses.len(), 1);
902 assert_eq!(c.consumes.len(), 1);
903 assert_eq!(c.exports.len(), 2);
904 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
905 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
906 }
907
908 #[test]
909 fn context_fragment_form_parses() {
910 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
911 let u = parse_unit_str(src).unwrap();
912 let SourceUnit::Context(c) = u else { panic!() };
913 assert_eq!(c.form, CommonsForm::Fragment);
914 assert_eq!(c.uses.len(), 1);
915 assert_eq!(c.consumes.len(), 1);
916 assert_eq!(c.exports.len(), 1);
917 }
918
919 #[test]
920 fn opaque_type_parses() {
921 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
922 let CommonsItem::Type(t) = &c.items[0] else {
923 panic!()
924 };
925 assert!(matches!(t.body, TypeBody::Opaque { .. }));
926 }
927
928 #[test]
929 fn empty_commons() {
930 let c = parse_str("commons fitness.units {}").unwrap();
931 assert_eq!(c.name.joined(), "fitness.units");
932 assert!(c.items.is_empty());
933 }
934
935 #[test]
936 fn one_type_decl() {
937 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
938 assert_eq!(c.items.len(), 1);
939 let CommonsItem::Type(t) = &c.items[0] else {
940 panic!()
941 };
942 assert_eq!(t.name.name, "Metres");
943 match &t.body {
944 TypeBody::Refined {
945 base, refinement, ..
946 } => {
947 assert_eq!(*base, BaseType::Int);
948 assert!(refinement.is_some());
949 }
950 _ => panic!("expected refined body"),
951 }
952 }
953
954 #[test]
955 fn function_decl() {
956 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
957 let CommonsItem::Fn(f) = &c.items[0] else {
958 panic!()
959 };
960 assert_eq!(f.name.ident().name, "add");
961 assert_eq!(f.params.len(), 2);
962 }
963
964 #[test]
965 fn chained_comparison_is_error() {
966 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
967 .unwrap_err();
968 assert_eq!(errs[0].category, "bynk.parse.non_associative");
969 }
970
971 #[test]
972 fn chained_equality_is_error() {
973 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
974 .unwrap_err();
975 assert_eq!(errs[0].category, "bynk.parse.non_associative");
976 }
977
978 #[test]
979 fn let_statement_parses() {
980 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
981 let CommonsItem::Fn(f) = &c.items[0] else {
982 panic!()
983 };
984 assert_eq!(f.body.statements.len(), 1);
985 match &f.body.statements[0] {
986 Statement::Let(l) => {
987 assert_eq!(l.name.name, "y");
988 assert!(l.type_annot.is_none());
989 }
990 _ => panic!("expected a pure `let` statement"),
991 }
992 }
993
994 #[test]
995 fn let_with_annotation() {
996 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
997 let CommonsItem::Fn(f) = &c.items[0] else {
998 panic!()
999 };
1000 match &f.body.statements[0] {
1001 Statement::Let(l) => assert!(l.type_annot.is_some()),
1002 _ => panic!("expected a pure `let` statement"),
1003 }
1004 }
1005
1006 #[test]
1007 fn if_else_parses_as_expression() {
1008 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1009 let CommonsItem::Fn(f) = &c.items[0] else {
1010 panic!()
1011 };
1012 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1013 }
1014
1015 #[test]
1016 fn else_if_chain_parses() {
1017 let c = parse_str(
1018 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1019 )
1020 .unwrap();
1021 let CommonsItem::Fn(f) = &c.items[0] else {
1022 panic!()
1023 };
1024 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1025 panic!()
1026 };
1027 assert!(else_block.statements.is_empty());
1029 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1030 }
1031
1032 #[test]
1033 fn ok_and_err_parse_as_expressions() {
1034 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1035 let CommonsItem::Fn(f) = &c.items[0] else {
1036 panic!()
1037 };
1038 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1039
1040 let c =
1041 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1042 let CommonsItem::Fn(f) = &c.items[0] else {
1043 panic!()
1044 };
1045 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1046 }
1047
1048 #[test]
1049 fn question_postfix_parses() {
1050 let c = parse_str(
1051 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1052 )
1053 .unwrap();
1054 let CommonsItem::Fn(f) = &c.items[1] else {
1055 panic!()
1056 };
1057 let Statement::Let(l) = &f.body.statements[0] else {
1058 panic!("expected a pure `let` statement");
1059 };
1060 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1061 }
1062
1063 #[test]
1064 fn constructor_call_parses() {
1065 let c = parse_str(
1066 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1067 )
1068 .unwrap();
1069 let CommonsItem::Fn(f) = &c.items[1] else {
1070 panic!()
1071 };
1072 let ExprKind::MethodCall {
1075 receiver, method, ..
1076 } = &f.body.tail.kind
1077 else {
1078 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1079 };
1080 let ExprKind::Ident(id) = &receiver.kind else {
1081 panic!("expected receiver Ident");
1082 };
1083 assert_eq!(id.name, "T");
1084 assert_eq!(method.name, "of");
1085 }
1086
1087 #[test]
1088 fn result_type_ref_parses() {
1089 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1090 let CommonsItem::Fn(f) = &c.items[0] else {
1091 panic!()
1092 };
1093 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1094 }
1095
1096 #[test]
1097 fn result_missing_arg_count_errors() {
1098 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1099 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1100 }
1101
1102 #[test]
1103 fn field_access_parses_in_v0_2() {
1104 let c =
1107 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1108 let CommonsItem::Fn(f) = &c.items[1] else {
1109 panic!()
1110 };
1111 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1112 }
1113
1114 #[test]
1117 fn leading_line_comment_attaches_to_next_decl() {
1118 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1119 let c = parse_str(src).unwrap();
1120 let CommonsItem::Type(t) = &c.items[0] else {
1121 panic!()
1122 };
1123 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1124 assert!(t.trivia.trailing.is_none());
1125 }
1126
1127 #[test]
1128 fn trailing_line_comment_attaches_to_prev_decl() {
1129 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1130 let c = parse_str(src).unwrap();
1131 let CommonsItem::Type(t) = &c.items[0] else {
1132 panic!()
1133 };
1134 assert!(t.trivia.leading.is_empty());
1135 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1136 }
1137
1138 #[test]
1139 fn grouped_leading_comments_attach_together() {
1140 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1141 let c = parse_str(src).unwrap();
1142 let CommonsItem::Type(t) = &c.items[0] else {
1143 panic!()
1144 };
1145 assert_eq!(
1146 t.trivia.leading,
1147 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1148 );
1149 }
1150
1151 #[test]
1152 fn comment_with_doc_block_keeps_both() {
1153 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1155 let c = parse_str(src).unwrap();
1156 let CommonsItem::Type(t) = &c.items[0] else {
1157 panic!()
1158 };
1159 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1160 assert_eq!(t.documentation.as_deref(), Some("docs"));
1161 }
1162
1163 #[test]
1164 fn comment_before_let_statement_attaches() {
1165 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1166 let c = parse_str(src).unwrap();
1167 let CommonsItem::Fn(f) = &c.items[0] else {
1168 panic!()
1169 };
1170 let Statement::Let(l) = &f.body.statements[0] else {
1171 panic!()
1172 };
1173 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1174 }
1175
1176 #[test]
1177 fn comment_before_tail_attaches_to_block_tail() {
1178 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1179 let c = parse_str(src).unwrap();
1180 let CommonsItem::Fn(f) = &c.items[0] else {
1181 panic!()
1182 };
1183 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1184 }
1185
1186 #[test]
1194 fn is_reserved_keyword_covers_every_lexer_keyword() {
1195 let lexer_src = include_str!("lexer.rs");
1196 let mut words = Vec::new();
1197 for line in lexer_src.lines() {
1198 let t = line.trim();
1199 if let Some(rest) = t.strip_prefix("#[token(\"")
1200 && let Some(word) = rest.split('"').next()
1201 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
1202 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1203 {
1204 words.push(word.to_string());
1205 }
1206 }
1207 assert!(
1208 words.len() > 30,
1209 "keyword extraction looks broken: only {} words",
1210 words.len()
1211 );
1212 const CONTEXTUAL: &[&str] = &["on", "suite", "case"];
1214 let mut unclassified = Vec::new();
1215 for word in &words {
1216 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
1217 let kind = tokens.first().expect("keyword yields a token").kind;
1218 if !is_reserved_keyword(kind) && !CONTEXTUAL.contains(&word.as_str()) {
1219 unclassified.push(word.clone());
1220 }
1221 }
1222 assert!(
1223 unclassified.is_empty(),
1224 "keywords missing from is_reserved_keyword (add them, or document \
1225 them as contextual): {unclassified:?}"
1226 );
1227 }
1228
1229 #[test]
1234 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
1235 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
1236 let tokens = crate::lexer::tokenize(src).unwrap();
1237 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
1238 assert!(unit.is_some(), "the commons header still parses");
1239 assert!(
1240 errors
1241 .iter()
1242 .any(|e| e.category == "bynk.capability.outside_context"),
1243 "the misplaced capability is reported: {errors:?}"
1244 );
1245 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
1248 }
1249
1250 #[test]
1251 fn trailing_file_comment_becomes_unit_trailing() {
1252 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1256 let c = parse_str(src).unwrap();
1257 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1258 }
1259}