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::State | TokenKind::On | TokenKind::Test) => {
423 self.bump();
424 Ok(Ident {
425 name: self.slice(t.span).to_string(),
426 span: t.span,
427 })
428 }
429 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
430 "bynk.parse.reserved_keyword",
431 t.span,
432 format!(
433 "expected identifier {ctx}, but `{}` is a reserved keyword",
434 self.slice(t.span)
435 ),
436 )
437 .with_note("rename the identifier to something that is not a keyword")),
438 Some(t) => Err(CompileError::new(
439 "bynk.parse.expected_token",
440 t.span,
441 format!("expected identifier {ctx}, found {}", t.kind.describe()),
442 )),
443 None => Err(CompileError::new(
444 "bynk.parse.unexpected_eof",
445 self.eof_span(),
446 format!("expected identifier {ctx}, found end of file"),
447 )),
448 }
449 }
450
451 fn take_doc_block(&mut self) -> Option<(String, Span)> {
457 if self.peek_kind() == Some(TokenKind::DocBlock) {
458 let t = self.bump().unwrap();
459 let body = doc_block_content(self.source, t.span);
460 return Some((body, t.span));
461 }
462 None
463 }
464
465 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
470 let mut leading = self.take_leading_trivia();
471 let doc = self.take_doc_block();
472 if doc.is_some() {
473 leading.extend(self.take_leading_trivia());
474 }
475 (leading, doc)
476 }
477
478 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
481 let (content, doc_span) = doc?;
482 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
484 self.warnings.push(
485 CompileError::new(
486 "bynk.parse.orphan_doc_block",
487 doc_span,
488 "documentation block is separated from the following declaration by a blank line; it will not be attached",
489 )
490 .with_note(
491 "remove the blank line to attach the doc to the next declaration, \
492 or remove the doc block if it is not meant to document anything",
493 ),
494 );
495 return None;
496 }
497 Some(content)
498 }
499}
500
501fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
504 let bytes = lexeme.as_bytes();
505 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
506 let inner = &lexeme[1..lexeme.len() - 1];
507 let mut out = String::with_capacity(inner.len());
508 let mut chars = inner.chars();
509 while let Some(c) = chars.next() {
510 if c == '\\' {
511 match chars.next() {
512 Some('n') => out.push('\n'),
513 Some('t') => out.push('\t'),
514 Some('"') => out.push('"'),
515 Some('\\') => out.push('\\'),
516 other => {
517 return Err(CompileError::new(
518 "bynk.lex.bad_escape",
519 span,
520 format!(
521 "invalid escape sequence `\\{}` in string literal",
522 other.map(|c| c.to_string()).unwrap_or_default()
523 ),
524 )
525 .with_note("supported escapes: \\n \\t \\\" \\\\"));
526 }
527 }
528 } else {
529 out.push(c);
530 }
531 }
532 Ok(out)
533}
534
535fn is_reserved_keyword(kind: TokenKind) -> bool {
536 use TokenKind::*;
537 matches!(
538 kind,
539 Commons
540 | Type
541 | Fn
542 | Where
543 | And
544 | True
545 | False
546 | Int
547 | String
548 | Bool
549 | Let
550 | If
551 | Else
552 | Ok
553 | Err
554 | Result
555 | ValidationError
556 | Enum
557 | Match
558 | Option
559 | Record
560 | Self_
561 | Some
562 | None
563 | Is
564 | Opaque
565 | Uses
566 | Context
567 | Consumes
568 | Exports
569 | Transparent
570 | Agent
571 | As
572 | Capability
573 | Commit
574 | Effect
575 | Given
576 | On
577 | Http
578 | Provides
579 | Service
580 | State
581 | Actor
582 | By
583 | Assert
584 | Expect
585 | Mocks
586 )
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use crate::lexer::tokenize;
593
594 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
595 let toks = tokenize(src).map_err(|e| vec![e])?;
596 parse(&toks, src)
597 }
598
599 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
600 let toks = match tokenize(src) {
601 Ok(t) => t,
602 Err(e) => return (None, vec![e]),
603 };
604 parse_unit_with_recovery(&toks, src)
605 }
606
607 #[test]
608 fn recovery_skips_garbage_between_decls() {
609 let src = "commons x {\n\
612 type A = Int where NonNegative\n\
613 ??? !!!\n\
614 type B = String where NonEmpty\n\
615 }";
616 let (unit, errors) = parse_recover_str(src);
617 let unit = unit.expect("recovery should produce a partial AST");
618 let SourceUnit::Commons(c) = unit else {
619 panic!("expected commons")
620 };
621 let names: Vec<_> = c
623 .items
624 .iter()
625 .map(|i| match i {
626 CommonsItem::Type(t) => t.name.name.clone(),
627 _ => panic!("expected only types"),
628 })
629 .collect();
630 assert!(
631 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
632 "expected both A and B; got {names:?}",
633 );
634 assert!(!errors.is_empty(), "expected at least one parse error");
635 }
636
637 #[test]
638 fn recovery_handles_bad_first_decl_then_good_second() {
639 let src = "commons x {\n\
641 type A Int where NonNegative\n\
642 type B = String where NonEmpty\n\
643 }";
644 let (unit, errors) = parse_recover_str(src);
645 let unit = unit.expect("recovery should produce a partial AST");
646 let SourceUnit::Commons(c) = unit else {
647 panic!("expected commons")
648 };
649 let names: Vec<_> = c
650 .items
651 .iter()
652 .filter_map(|i| match i {
653 CommonsItem::Type(t) => Some(t.name.name.clone()),
654 _ => None,
655 })
656 .collect();
657 assert!(
658 names.contains(&"B".to_string()),
659 "B should be parsed after A's failure; got {names:?}"
660 );
661 assert!(!errors.is_empty(), "expected at least one parse error");
662 }
663
664 #[test]
665 fn doc_block_attaches_to_type() {
666 let c =
667 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
668 .unwrap();
669 let CommonsItem::Type(t) = &c.items[0] else {
670 panic!()
671 };
672 assert!(t.documentation.is_some());
673 assert!(
674 t.documentation
675 .as_ref()
676 .unwrap()
677 .contains("A descriptive doc.")
678 );
679 }
680
681 #[test]
682 fn interpolated_string_parses_into_parts() {
683 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
685 .unwrap();
686 let CommonsItem::Fn(f) = &c.items[0] else {
687 panic!("expected fn")
688 };
689 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
690 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
691 };
692 assert_eq!(parts.len(), 3);
693 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
694 assert!(
695 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
696 );
697 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
698 }
699
700 #[test]
701 fn interpolated_hole_parses_a_full_expression() {
702 let c =
704 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
705 .unwrap();
706 let CommonsItem::Fn(f) = &c.items[0] else {
707 panic!("expected fn")
708 };
709 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
710 panic!("expected InterpStr")
711 };
712 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
713 }
714
715 #[test]
716 fn empty_interpolation_hole_is_rejected() {
717 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
718 assert!(
719 errs.iter()
720 .any(|e| e.category == "bynk.parse.empty_interpolation"),
721 "expected empty_interpolation; got {errs:?}"
722 );
723 }
724
725 #[test]
726 fn fragment_form_parses() {
727 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
728 assert_eq!(c.form, CommonsForm::Fragment);
729 assert_eq!(c.items.len(), 1);
730 }
731
732 #[test]
733 fn uses_parses() {
734 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
735 assert_eq!(c.uses.len(), 1);
736 assert_eq!(c.uses[0].target.joined(), "other.lib");
737 }
738
739 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
740 let toks = tokenize(src).map_err(|e| vec![e])?;
741 parse_unit(&toks, src)
742 }
743
744 #[test]
745 fn minimal_context_parses() {
746 let u = parse_unit_str("context commerce.orders {}").unwrap();
747 let SourceUnit::Context(c) = u else {
748 panic!("expected context");
749 };
750 assert_eq!(c.name.joined(), "commerce.orders");
751 assert!(c.items.is_empty());
752 }
753
754 #[test]
755 fn context_consumes_and_exports_parse() {
756 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}";
757 let u = parse_unit_str(src).unwrap();
758 let SourceUnit::Context(c) = u else { panic!() };
759 assert_eq!(c.uses.len(), 1);
760 assert_eq!(c.consumes.len(), 1);
761 assert_eq!(c.exports.len(), 2);
762 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
763 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
764 }
765
766 #[test]
767 fn context_fragment_form_parses() {
768 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
769 let u = parse_unit_str(src).unwrap();
770 let SourceUnit::Context(c) = u else { panic!() };
771 assert_eq!(c.form, CommonsForm::Fragment);
772 assert_eq!(c.uses.len(), 1);
773 assert_eq!(c.consumes.len(), 1);
774 assert_eq!(c.exports.len(), 1);
775 }
776
777 #[test]
778 fn opaque_type_parses() {
779 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
780 let CommonsItem::Type(t) = &c.items[0] else {
781 panic!()
782 };
783 assert!(matches!(t.body, TypeBody::Opaque { .. }));
784 }
785
786 #[test]
787 fn empty_commons() {
788 let c = parse_str("commons fitness.units {}").unwrap();
789 assert_eq!(c.name.joined(), "fitness.units");
790 assert!(c.items.is_empty());
791 }
792
793 #[test]
794 fn one_type_decl() {
795 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
796 assert_eq!(c.items.len(), 1);
797 let CommonsItem::Type(t) = &c.items[0] else {
798 panic!()
799 };
800 assert_eq!(t.name.name, "Metres");
801 match &t.body {
802 TypeBody::Refined {
803 base, refinement, ..
804 } => {
805 assert_eq!(*base, BaseType::Int);
806 assert!(refinement.is_some());
807 }
808 _ => panic!("expected refined body"),
809 }
810 }
811
812 #[test]
813 fn function_decl() {
814 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
815 let CommonsItem::Fn(f) = &c.items[0] else {
816 panic!()
817 };
818 assert_eq!(f.name.ident().name, "add");
819 assert_eq!(f.params.len(), 2);
820 }
821
822 #[test]
823 fn chained_comparison_is_error() {
824 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
825 .unwrap_err();
826 assert_eq!(errs[0].category, "bynk.parse.non_associative");
827 }
828
829 #[test]
830 fn chained_equality_is_error() {
831 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
832 .unwrap_err();
833 assert_eq!(errs[0].category, "bynk.parse.non_associative");
834 }
835
836 #[test]
837 fn let_statement_parses() {
838 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
839 let CommonsItem::Fn(f) = &c.items[0] else {
840 panic!()
841 };
842 assert_eq!(f.body.statements.len(), 1);
843 match &f.body.statements[0] {
844 Statement::Let(l) => {
845 assert_eq!(l.name.name, "y");
846 assert!(l.type_annot.is_none());
847 }
848 _ => panic!("expected a pure `let` statement"),
849 }
850 }
851
852 #[test]
853 fn let_with_annotation() {
854 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
855 let CommonsItem::Fn(f) = &c.items[0] else {
856 panic!()
857 };
858 match &f.body.statements[0] {
859 Statement::Let(l) => assert!(l.type_annot.is_some()),
860 _ => panic!("expected a pure `let` statement"),
861 }
862 }
863
864 #[test]
865 fn if_else_parses_as_expression() {
866 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
867 let CommonsItem::Fn(f) = &c.items[0] else {
868 panic!()
869 };
870 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
871 }
872
873 #[test]
874 fn else_if_chain_parses() {
875 let c = parse_str(
876 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
877 )
878 .unwrap();
879 let CommonsItem::Fn(f) = &c.items[0] else {
880 panic!()
881 };
882 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
883 panic!()
884 };
885 assert!(else_block.statements.is_empty());
887 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
888 }
889
890 #[test]
891 fn ok_and_err_parse_as_expressions() {
892 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
893 let CommonsItem::Fn(f) = &c.items[0] else {
894 panic!()
895 };
896 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
897
898 let c =
899 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
900 let CommonsItem::Fn(f) = &c.items[0] else {
901 panic!()
902 };
903 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
904 }
905
906 #[test]
907 fn question_postfix_parses() {
908 let c = parse_str(
909 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
910 )
911 .unwrap();
912 let CommonsItem::Fn(f) = &c.items[1] else {
913 panic!()
914 };
915 let Statement::Let(l) = &f.body.statements[0] else {
916 panic!("expected a pure `let` statement");
917 };
918 assert!(matches!(l.value.kind, ExprKind::Question(_)));
919 }
920
921 #[test]
922 fn constructor_call_parses() {
923 let c = parse_str(
924 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
925 )
926 .unwrap();
927 let CommonsItem::Fn(f) = &c.items[1] else {
928 panic!()
929 };
930 let ExprKind::MethodCall {
933 receiver, method, ..
934 } = &f.body.tail.kind
935 else {
936 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
937 };
938 let ExprKind::Ident(id) = &receiver.kind else {
939 panic!("expected receiver Ident");
940 };
941 assert_eq!(id.name, "T");
942 assert_eq!(method.name, "of");
943 }
944
945 #[test]
946 fn result_type_ref_parses() {
947 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
948 let CommonsItem::Fn(f) = &c.items[0] else {
949 panic!()
950 };
951 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
952 }
953
954 #[test]
955 fn result_missing_arg_count_errors() {
956 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
957 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
958 }
959
960 #[test]
961 fn field_access_parses_in_v0_2() {
962 let c =
965 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
966 let CommonsItem::Fn(f) = &c.items[1] else {
967 panic!()
968 };
969 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
970 }
971
972 #[test]
975 fn leading_line_comment_attaches_to_next_decl() {
976 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
977 let c = parse_str(src).unwrap();
978 let CommonsItem::Type(t) = &c.items[0] else {
979 panic!()
980 };
981 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
982 assert!(t.trivia.trailing.is_none());
983 }
984
985 #[test]
986 fn trailing_line_comment_attaches_to_prev_decl() {
987 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
988 let c = parse_str(src).unwrap();
989 let CommonsItem::Type(t) = &c.items[0] else {
990 panic!()
991 };
992 assert!(t.trivia.leading.is_empty());
993 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
994 }
995
996 #[test]
997 fn grouped_leading_comments_attach_together() {
998 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
999 let c = parse_str(src).unwrap();
1000 let CommonsItem::Type(t) = &c.items[0] else {
1001 panic!()
1002 };
1003 assert_eq!(
1004 t.trivia.leading,
1005 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1006 );
1007 }
1008
1009 #[test]
1010 fn comment_with_doc_block_keeps_both() {
1011 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1013 let c = parse_str(src).unwrap();
1014 let CommonsItem::Type(t) = &c.items[0] else {
1015 panic!()
1016 };
1017 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1018 assert_eq!(t.documentation.as_deref(), Some("docs"));
1019 }
1020
1021 #[test]
1022 fn comment_before_let_statement_attaches() {
1023 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1024 let c = parse_str(src).unwrap();
1025 let CommonsItem::Fn(f) = &c.items[0] else {
1026 panic!()
1027 };
1028 let Statement::Let(l) = &f.body.statements[0] else {
1029 panic!()
1030 };
1031 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1032 }
1033
1034 #[test]
1035 fn comment_before_tail_attaches_to_block_tail() {
1036 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1037 let c = parse_str(src).unwrap();
1038 let CommonsItem::Fn(f) = &c.items[0] else {
1039 panic!()
1040 };
1041 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1042 }
1043
1044 #[test]
1045 fn trailing_file_comment_becomes_unit_trailing() {
1046 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1050 let c = parse_str(src).unwrap();
1051 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1052 }
1053}