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