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