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 match parse_unit(tokens, source)? {
104 SourceUnit::Commons(c) => Ok(c),
105 SourceUnit::Context(ctx) => Err(vec![
106 CompileError::new(
107 "bynk.parse.unexpected_context",
108 ctx.span,
109 "expected a `commons` declaration but found a `context` declaration",
110 )
111 .with_note(
112 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
113 ),
114 ]),
115 SourceUnit::Suite(t) => Err(vec![
116 CompileError::new(
117 "bynk.parse.unexpected_suite",
118 t.span,
119 "expected a `commons` declaration but found a `suite` declaration",
120 )
121 .with_note(
122 "tests 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::Adapter(a) => Err(vec![
126 CompileError::new(
127 "bynk.parse.unexpected_adapter",
128 a.span,
129 "expected a `commons` declaration but found an `adapter` declaration",
130 )
131 .with_note(
132 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
133 ),
134 ]),
135 }
136}
137
138pub fn parse_unit_with_recovery(
147 tokens: &[Token],
148 source: &str,
149) -> (Option<SourceUnit>, Vec<CompileError>) {
150 let (filtered, trivia) = split_trivia(tokens, source);
151 let mut warnings = Vec::new();
152 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
153 p.recover_mode = true;
154 let unit_opt = match p.parse_unit() {
155 Ok(u) => {
156 while p.peek().is_some() {
162 match p.parse_unit() {
163 Ok(_) => {}
164 Err(e) => {
165 p.recovered_errors.push(e);
166 break;
167 }
168 }
169 }
170 Some(u)
171 }
172 Err(e) => {
173 p.recovered_errors.push(e);
174 None
175 }
176 };
177 let mut all_errors = p.recovered_errors;
178 all_errors.append(&mut warnings);
179 (unit_opt, all_errors)
180}
181
182pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
186 let (filtered, trivia) = split_trivia(tokens, source);
187 let mut warnings = Vec::new();
188 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
189 let result = match p.parse_unit() {
190 Ok(u) => {
191 if let Some(extra) = p.peek() {
192 Err(vec![
193 CompileError::new(
194 "bynk.parse.extra_tokens",
195 extra.span,
196 "unexpected token after top-level declaration",
197 )
198 .with_note(
199 "a `.bynk` file contains exactly one `commons` or `context` declaration",
200 ),
201 ])
202 } else {
203 Ok(u)
204 }
205 }
206 Err(e) => Err(vec![e]),
207 };
208 if !warnings.is_empty() {
211 match result {
212 Ok(_) => return Err(warnings),
213 Err(mut errs) => {
214 errs.append(&mut warnings);
215 return Err(errs);
216 }
217 }
218 }
219 result
220}
221
222pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
231 let (filtered, trivia) = split_trivia(tokens, source);
232 let mut warnings = Vec::new();
233 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
234 let mut units = Vec::new();
235 let mut errors: Vec<CompileError> = Vec::new();
236 while p.peek().is_some() {
237 match p.parse_unit() {
238 Ok(u) => units.push(u),
239 Err(e) => {
240 errors.push(e);
241 break;
242 }
243 }
244 }
245 let eof = p.eof_span();
246 errors.append(&mut warnings);
249 if !errors.is_empty() {
250 return Err(errors);
251 }
252 if units.is_empty() {
253 return Err(vec![CompileError::new(
254 "bynk.parse.unexpected_eof",
255 eof,
256 "expected `commons`, `context`, or `suite` to start the file, found end of file",
257 )]);
258 }
259 Ok(units)
260}
261
262enum SignedNumLit {
265 Int(IntBound),
266 Float(FloatBound),
267}
268
269struct Parser<'a> {
270 tokens: &'a [Token],
271 source: &'a str,
272 pos: usize,
273 warnings: &'a mut Vec<CompileError>,
276 recover_mode: bool,
282 recovered_errors: Vec<CompileError>,
285 trivia: TriviaTable,
288}
289
290impl<'a> Parser<'a> {
291 fn new(
292 tokens: &'a [Token],
293 source: &'a str,
294 trivia: TriviaTable,
295 warnings: &'a mut Vec<CompileError>,
296 ) -> Self {
297 Self {
298 tokens,
299 source,
300 pos: 0,
301 warnings,
302 recover_mode: false,
303 recovered_errors: Vec::new(),
304 trivia,
305 }
306 }
307
308 fn take_leading_trivia(&mut self) -> Vec<String> {
312 self.trivia.take_leading(self.pos)
313 }
314
315 fn take_trailing_trivia(&mut self) -> Option<String> {
319 if self.pos == 0 {
320 return None;
321 }
322 self.trivia.take_trailing(self.pos - 1)
323 }
324
325 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
329 if self.recover_mode {
330 self.recovered_errors.push(e);
331 self.recover_to_top_item();
332 Ok(())
333 } else {
334 Err(e)
335 }
336 }
337
338 fn recover_to_top_item(&mut self) {
343 while let Some(t) = self.peek() {
344 match t.kind {
345 TokenKind::Type
346 | TokenKind::Fn
347 | TokenKind::Uses
348 | TokenKind::Consumes
349 | TokenKind::Exports
350 | TokenKind::Capability
351 | TokenKind::Provides
352 | TokenKind::Service
353 | TokenKind::Agent
354 | TokenKind::Suite
355 | TokenKind::Case
356 | TokenKind::RBrace
357 | TokenKind::Commons
358 | TokenKind::Context => return,
359 _ => {
360 self.bump();
361 }
362 }
363 }
364 }
365
366 fn peek(&self) -> Option<Token> {
367 self.tokens.get(self.pos).copied()
368 }
369
370 fn peek_kind(&self) -> Option<TokenKind> {
371 self.peek().map(|t| t.kind)
372 }
373
374 fn nth(&self, n: usize) -> Option<Token> {
376 self.tokens.get(self.pos + n).copied()
377 }
378
379 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
380 self.nth(n).map(|t| t.kind)
381 }
382
383 fn nth_text(&self, n: usize) -> &'a str {
385 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
386 }
387
388 fn prev_span(&self) -> Span {
391 self.tokens
392 .get(self.pos.wrapping_sub(1))
393 .or_else(|| self.peek_ref())
394 .map(|t| t.span)
395 .unwrap_or_default()
396 }
397
398 fn peek_ref(&self) -> Option<&Token> {
399 self.tokens.get(self.pos)
400 }
401
402 fn bump(&mut self) -> Option<Token> {
403 let t = self.peek();
404 if t.is_some() {
405 self.pos += 1;
406 }
407 t
408 }
409
410 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
411 if self.peek_kind() == Some(kind) {
412 self.bump()
413 } else {
414 None
415 }
416 }
417
418 fn slice(&self, span: Span) -> &'a str {
419 &self.source[span.range()]
420 }
421
422 fn next_token_on_new_line(&self, prev: Span) -> bool {
427 match self.peek() {
428 Some(t) if prev.end <= t.span.start => {
429 self.source[prev.end..t.span.start].contains('\n')
430 }
431 _ => false,
432 }
433 }
434
435 fn eof_span(&self) -> Span {
437 let end = self.source.len();
438 Span::new(end.saturating_sub(1), end)
439 }
440
441 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
442 match self.peek() {
443 Some(t) if t.kind == kind => {
444 self.bump();
445 Ok(t)
446 }
447 Some(t) => Err(CompileError::new(
448 "bynk.parse.expected_token",
449 t.span,
450 format!(
451 "expected {} {ctx}, found {}",
452 kind.describe(),
453 t.kind.describe()
454 ),
455 )),
456 None => Err(CompileError::new(
457 "bynk.parse.unexpected_eof",
458 self.eof_span(),
459 format!("expected {} {ctx}, found end of file", kind.describe()),
460 )),
461 }
462 }
463
464 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
465 match self.peek() {
466 Some(t) if t.kind == TokenKind::Ident => {
467 self.bump();
468 Ok(Ident {
469 name: self.slice(t.span).to_string(),
470 span: t.span,
471 })
472 }
473 Some(t) if matches!(t.kind, TokenKind::On | TokenKind::Suite | TokenKind::Case) => {
482 self.bump();
483 Ok(Ident {
484 name: self.slice(t.span).to_string(),
485 span: t.span,
486 })
487 }
488 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
489 "bynk.parse.reserved_keyword",
490 t.span,
491 format!(
492 "expected identifier {ctx}, but `{}` is a reserved keyword",
493 self.slice(t.span)
494 ),
495 )
496 .with_note("rename the identifier to something that is not a keyword")),
497 Some(t) => Err(CompileError::new(
498 "bynk.parse.expected_token",
499 t.span,
500 format!("expected identifier {ctx}, found {}", t.kind.describe()),
501 )),
502 None => Err(CompileError::new(
503 "bynk.parse.unexpected_eof",
504 self.eof_span(),
505 format!("expected identifier {ctx}, found end of file"),
506 )),
507 }
508 }
509
510 fn take_doc_block(&mut self) -> Option<(String, Span)> {
516 if self.peek_kind() == Some(TokenKind::DocBlock) {
517 let t = self.bump().unwrap();
518 let body = doc_block_content(self.source, t.span);
519 return Some((body, t.span));
520 }
521 None
522 }
523
524 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
529 let mut leading = self.take_leading_trivia();
530 let doc = self.take_doc_block();
531 if doc.is_some() {
532 leading.extend(self.take_leading_trivia());
533 }
534 (leading, doc)
535 }
536
537 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
540 let (content, doc_span) = doc?;
541 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
543 self.warnings.push(
544 CompileError::new(
545 "bynk.parse.orphan_doc_block",
546 doc_span,
547 "documentation block is separated from the following declaration by a blank line; it will not be attached",
548 )
549 .with_note(
550 "remove the blank line to attach the doc to the next declaration, \
551 or remove the doc block if it is not meant to document anything",
552 ),
553 );
554 return None;
555 }
556 Some(content)
557 }
558}
559
560fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
563 let bytes = lexeme.as_bytes();
564 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
565 let inner = &lexeme[1..lexeme.len() - 1];
566 let mut out = String::with_capacity(inner.len());
567 let mut chars = inner.chars();
568 while let Some(c) = chars.next() {
569 if c == '\\' {
570 match chars.next() {
571 Some('n') => out.push('\n'),
572 Some('t') => out.push('\t'),
573 Some('"') => out.push('"'),
574 Some('\\') => out.push('\\'),
575 other => {
576 return Err(CompileError::new(
577 "bynk.lex.bad_escape",
578 span,
579 format!(
580 "invalid escape sequence `\\{}` in string literal",
581 other.map(|c| c.to_string()).unwrap_or_default()
582 ),
583 )
584 .with_note("supported escapes: \\n \\t \\\" \\\\"));
585 }
586 }
587 } else {
588 out.push(c);
589 }
590 }
591 Ok(out)
592}
593
594fn is_reserved_keyword(kind: TokenKind) -> bool {
595 use TokenKind::*;
596 matches!(
597 kind,
598 Commons
599 | Type
600 | Fn
601 | Where
602 | And
603 | True
604 | False
605 | Int
606 | String
607 | Bool
608 | Let
609 | If
610 | Else
611 | Ok
612 | Err
613 | Result
614 | ValidationError
615 | Enum
616 | Match
617 | Option
618 | Record
619 | Self_
620 | Some
621 | None
622 | Is
623 | Opaque
624 | Uses
625 | Context
626 | Consumes
627 | Exports
628 | Transparent
629 | Agent
630 | As
631 | Capability
632 | Effect
633 | Given
634 | On
635 | Http
636 | Provides
637 | Service
638 | Actor
639 | By
640 | Expect
641 | Suite
642 | Case
643 )
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use crate::lexer::tokenize;
650
651 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
652 let toks = tokenize(src).map_err(|e| vec![e])?;
653 parse(&toks, src)
654 }
655
656 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
657 let toks = match tokenize(src) {
658 Ok(t) => t,
659 Err(e) => return (None, vec![e]),
660 };
661 parse_unit_with_recovery(&toks, src)
662 }
663
664 #[test]
665 fn recovery_skips_garbage_between_decls() {
666 let src = "commons x {\n\
669 type A = Int where NonNegative\n\
670 ??? !!!\n\
671 type B = String where NonEmpty\n\
672 }";
673 let (unit, errors) = parse_recover_str(src);
674 let unit = unit.expect("recovery should produce a partial AST");
675 let SourceUnit::Commons(c) = unit else {
676 panic!("expected commons")
677 };
678 let names: Vec<_> = c
680 .items
681 .iter()
682 .map(|i| match i {
683 CommonsItem::Type(t) => t.name.name.clone(),
684 _ => panic!("expected only types"),
685 })
686 .collect();
687 assert!(
688 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
689 "expected both A and B; got {names:?}",
690 );
691 assert!(!errors.is_empty(), "expected at least one parse error");
692 }
693
694 #[test]
695 fn recovery_handles_bad_first_decl_then_good_second() {
696 let src = "commons x {\n\
698 type A Int where NonNegative\n\
699 type B = String where NonEmpty\n\
700 }";
701 let (unit, errors) = parse_recover_str(src);
702 let unit = unit.expect("recovery should produce a partial AST");
703 let SourceUnit::Commons(c) = unit else {
704 panic!("expected commons")
705 };
706 let names: Vec<_> = c
707 .items
708 .iter()
709 .filter_map(|i| match i {
710 CommonsItem::Type(t) => Some(t.name.name.clone()),
711 _ => None,
712 })
713 .collect();
714 assert!(
715 names.contains(&"B".to_string()),
716 "B should be parsed after A's failure; got {names:?}"
717 );
718 assert!(!errors.is_empty(), "expected at least one parse error");
719 }
720
721 #[test]
722 fn doc_block_attaches_to_type() {
723 let c =
724 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
725 .unwrap();
726 let CommonsItem::Type(t) = &c.items[0] else {
727 panic!()
728 };
729 assert!(t.documentation.is_some());
730 assert!(
731 t.documentation
732 .as_ref()
733 .unwrap()
734 .contains("A descriptive doc.")
735 );
736 }
737
738 #[test]
739 fn interpolated_string_parses_into_parts() {
740 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
742 .unwrap();
743 let CommonsItem::Fn(f) = &c.items[0] else {
744 panic!("expected fn")
745 };
746 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
747 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
748 };
749 assert_eq!(parts.len(), 3);
750 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
751 assert!(
752 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
753 );
754 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
755 }
756
757 #[test]
758 fn interpolated_hole_parses_a_full_expression() {
759 let c =
761 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
762 .unwrap();
763 let CommonsItem::Fn(f) = &c.items[0] else {
764 panic!("expected fn")
765 };
766 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
767 panic!("expected InterpStr")
768 };
769 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
770 }
771
772 #[test]
773 fn empty_interpolation_hole_is_rejected() {
774 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
775 assert!(
776 errs.iter()
777 .any(|e| e.category == "bynk.parse.empty_interpolation"),
778 "expected empty_interpolation; got {errs:?}"
779 );
780 }
781
782 #[test]
783 fn fragment_form_parses() {
784 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
785 assert_eq!(c.form, CommonsForm::Fragment);
786 assert_eq!(c.items.len(), 1);
787 }
788
789 #[test]
790 fn uses_parses() {
791 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
792 assert_eq!(c.uses.len(), 1);
793 assert_eq!(c.uses[0].target.joined(), "other.lib");
794 }
795
796 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
797 let toks = tokenize(src).map_err(|e| vec![e])?;
798 parse_unit(&toks, src)
799 }
800
801 #[test]
802 fn minimal_context_parses() {
803 let u = parse_unit_str("context commerce.orders {}").unwrap();
804 let SourceUnit::Context(c) = u else {
805 panic!("expected context");
806 };
807 assert_eq!(c.name.joined(), "commerce.orders");
808 assert!(c.items.is_empty());
809 }
810
811 #[test]
812 fn context_consumes_and_exports_parse() {
813 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}";
814 let u = parse_unit_str(src).unwrap();
815 let SourceUnit::Context(c) = u else { panic!() };
816 assert_eq!(c.uses.len(), 1);
817 assert_eq!(c.consumes.len(), 1);
818 assert_eq!(c.exports.len(), 2);
819 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
820 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
821 }
822
823 #[test]
824 fn context_fragment_form_parses() {
825 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
826 let u = parse_unit_str(src).unwrap();
827 let SourceUnit::Context(c) = u else { panic!() };
828 assert_eq!(c.form, CommonsForm::Fragment);
829 assert_eq!(c.uses.len(), 1);
830 assert_eq!(c.consumes.len(), 1);
831 assert_eq!(c.exports.len(), 1);
832 }
833
834 #[test]
835 fn opaque_type_parses() {
836 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
837 let CommonsItem::Type(t) = &c.items[0] else {
838 panic!()
839 };
840 assert!(matches!(t.body, TypeBody::Opaque { .. }));
841 }
842
843 #[test]
844 fn empty_commons() {
845 let c = parse_str("commons fitness.units {}").unwrap();
846 assert_eq!(c.name.joined(), "fitness.units");
847 assert!(c.items.is_empty());
848 }
849
850 #[test]
851 fn one_type_decl() {
852 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
853 assert_eq!(c.items.len(), 1);
854 let CommonsItem::Type(t) = &c.items[0] else {
855 panic!()
856 };
857 assert_eq!(t.name.name, "Metres");
858 match &t.body {
859 TypeBody::Refined {
860 base, refinement, ..
861 } => {
862 assert_eq!(*base, BaseType::Int);
863 assert!(refinement.is_some());
864 }
865 _ => panic!("expected refined body"),
866 }
867 }
868
869 #[test]
870 fn function_decl() {
871 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
872 let CommonsItem::Fn(f) = &c.items[0] else {
873 panic!()
874 };
875 assert_eq!(f.name.ident().name, "add");
876 assert_eq!(f.params.len(), 2);
877 }
878
879 #[test]
880 fn chained_comparison_is_error() {
881 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
882 .unwrap_err();
883 assert_eq!(errs[0].category, "bynk.parse.non_associative");
884 }
885
886 #[test]
887 fn chained_equality_is_error() {
888 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
889 .unwrap_err();
890 assert_eq!(errs[0].category, "bynk.parse.non_associative");
891 }
892
893 #[test]
894 fn let_statement_parses() {
895 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
896 let CommonsItem::Fn(f) = &c.items[0] else {
897 panic!()
898 };
899 assert_eq!(f.body.statements.len(), 1);
900 match &f.body.statements[0] {
901 Statement::Let(l) => {
902 assert_eq!(l.name.name, "y");
903 assert!(l.type_annot.is_none());
904 }
905 _ => panic!("expected a pure `let` statement"),
906 }
907 }
908
909 #[test]
910 fn let_with_annotation() {
911 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
912 let CommonsItem::Fn(f) = &c.items[0] else {
913 panic!()
914 };
915 match &f.body.statements[0] {
916 Statement::Let(l) => assert!(l.type_annot.is_some()),
917 _ => panic!("expected a pure `let` statement"),
918 }
919 }
920
921 #[test]
922 fn if_else_parses_as_expression() {
923 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
924 let CommonsItem::Fn(f) = &c.items[0] else {
925 panic!()
926 };
927 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
928 }
929
930 #[test]
931 fn else_if_chain_parses() {
932 let c = parse_str(
933 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
934 )
935 .unwrap();
936 let CommonsItem::Fn(f) = &c.items[0] else {
937 panic!()
938 };
939 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
940 panic!()
941 };
942 assert!(else_block.statements.is_empty());
944 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
945 }
946
947 #[test]
948 fn ok_and_err_parse_as_expressions() {
949 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
950 let CommonsItem::Fn(f) = &c.items[0] else {
951 panic!()
952 };
953 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
954
955 let c =
956 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
957 let CommonsItem::Fn(f) = &c.items[0] else {
958 panic!()
959 };
960 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
961 }
962
963 #[test]
964 fn question_postfix_parses() {
965 let c = parse_str(
966 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
967 )
968 .unwrap();
969 let CommonsItem::Fn(f) = &c.items[1] else {
970 panic!()
971 };
972 let Statement::Let(l) = &f.body.statements[0] else {
973 panic!("expected a pure `let` statement");
974 };
975 assert!(matches!(l.value.kind, ExprKind::Question(_)));
976 }
977
978 #[test]
979 fn constructor_call_parses() {
980 let c = parse_str(
981 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
982 )
983 .unwrap();
984 let CommonsItem::Fn(f) = &c.items[1] else {
985 panic!()
986 };
987 let ExprKind::MethodCall {
990 receiver, method, ..
991 } = &f.body.tail.kind
992 else {
993 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
994 };
995 let ExprKind::Ident(id) = &receiver.kind else {
996 panic!("expected receiver Ident");
997 };
998 assert_eq!(id.name, "T");
999 assert_eq!(method.name, "of");
1000 }
1001
1002 #[test]
1003 fn result_type_ref_parses() {
1004 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1005 let CommonsItem::Fn(f) = &c.items[0] else {
1006 panic!()
1007 };
1008 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1009 }
1010
1011 #[test]
1012 fn result_missing_arg_count_errors() {
1013 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1014 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1015 }
1016
1017 #[test]
1018 fn field_access_parses_in_v0_2() {
1019 let c =
1022 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1023 let CommonsItem::Fn(f) = &c.items[1] else {
1024 panic!()
1025 };
1026 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1027 }
1028
1029 #[test]
1032 fn leading_line_comment_attaches_to_next_decl() {
1033 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1034 let c = parse_str(src).unwrap();
1035 let CommonsItem::Type(t) = &c.items[0] else {
1036 panic!()
1037 };
1038 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1039 assert!(t.trivia.trailing.is_none());
1040 }
1041
1042 #[test]
1043 fn trailing_line_comment_attaches_to_prev_decl() {
1044 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1045 let c = parse_str(src).unwrap();
1046 let CommonsItem::Type(t) = &c.items[0] else {
1047 panic!()
1048 };
1049 assert!(t.trivia.leading.is_empty());
1050 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1051 }
1052
1053 #[test]
1054 fn grouped_leading_comments_attach_together() {
1055 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1056 let c = parse_str(src).unwrap();
1057 let CommonsItem::Type(t) = &c.items[0] else {
1058 panic!()
1059 };
1060 assert_eq!(
1061 t.trivia.leading,
1062 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1063 );
1064 }
1065
1066 #[test]
1067 fn comment_with_doc_block_keeps_both() {
1068 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1070 let c = parse_str(src).unwrap();
1071 let CommonsItem::Type(t) = &c.items[0] else {
1072 panic!()
1073 };
1074 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1075 assert_eq!(t.documentation.as_deref(), Some("docs"));
1076 }
1077
1078 #[test]
1079 fn comment_before_let_statement_attaches() {
1080 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1081 let c = parse_str(src).unwrap();
1082 let CommonsItem::Fn(f) = &c.items[0] else {
1083 panic!()
1084 };
1085 let Statement::Let(l) = &f.body.statements[0] else {
1086 panic!()
1087 };
1088 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1089 }
1090
1091 #[test]
1092 fn comment_before_tail_attaches_to_block_tail() {
1093 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1094 let c = parse_str(src).unwrap();
1095 let CommonsItem::Fn(f) = &c.items[0] else {
1096 panic!()
1097 };
1098 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1099 }
1100
1101 #[test]
1102 fn trailing_file_comment_becomes_unit_trailing() {
1103 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1107 let c = parse_str(src).unwrap();
1108 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1109 }
1110}