1use crate::ast::*;
9use crate::error::CompileError;
10use crate::lexer::{Token, TokenKind, comment_body, doc_block_content, has_blank_line_between};
11use crate::span::Span;
12mod declarations;
13mod expressions;
14mod statements;
15mod types;
16
17#[derive(Debug, Default)]
26struct TriviaTable {
27 leading: Vec<Vec<String>>,
31 trailing: Vec<Option<String>>,
35 epilogue: Vec<String>,
38}
39
40impl TriviaTable {
41 fn take_leading(&mut self, index: usize) -> Vec<String> {
42 match self.leading.get_mut(index) {
43 Some(v) => std::mem::take(v),
44 None => Vec::new(),
45 }
46 }
47
48 fn take_trailing(&mut self, index: usize) -> Option<String> {
49 self.trailing.get_mut(index).and_then(|s| s.take())
50 }
51
52 fn take_epilogue(&mut self) -> Vec<String> {
53 std::mem::take(&mut self.epilogue)
54 }
55}
56
57fn split_trivia(tokens: &[Token], source: &str) -> (Vec<Token>, TriviaTable) {
63 let mut filtered: Vec<Token> = Vec::with_capacity(tokens.len());
64 let mut table = TriviaTable::default();
65 let mut pending_leading: Vec<String> = Vec::new();
66 let mut last_content_end: Option<usize> = None;
67 for tok in tokens {
68 if tok.kind == TokenKind::Comment {
69 let body = comment_body(source, tok.span).to_string();
70 if pending_leading.is_empty()
74 && let Some(prev_end) = last_content_end
75 && !source[prev_end..tok.span.start].contains('\n')
76 {
77 let last_idx = filtered.len() - 1;
78 if table.trailing[last_idx].is_none() {
81 table.trailing[last_idx] = Some(body);
82 continue;
83 }
84 }
85 pending_leading.push(body);
86 continue;
87 }
88 filtered.push(*tok);
89 table.leading.push(std::mem::take(&mut pending_leading));
90 table.trailing.push(None);
91 last_content_end = Some(tok.span.end);
92 }
93 table.epilogue = pending_leading;
94 (filtered, table)
95}
96
97pub fn parse(tokens: &[Token], source: &str) -> Result<Commons, Vec<CompileError>> {
103 parse_with_warnings(tokens, source).map(|(c, _warnings)| c)
104}
105
106pub fn parse_with_warnings(
109 tokens: &[Token],
110 source: &str,
111) -> Result<(Commons, Vec<CompileError>), Vec<CompileError>> {
112 let (unit, warnings) = parse_unit_with_warnings(tokens, source)?;
113 match unit {
114 SourceUnit::Commons(c) => Ok((c, warnings)),
115 SourceUnit::Context(ctx) => Err(vec![
116 CompileError::new(
117 "bynk.parse.unexpected_context",
118 ctx.span,
119 "expected a `commons` declaration but found a `context` declaration",
120 )
121 .with_note(
122 "contexts must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
123 ),
124 ]),
125 SourceUnit::Suite(t) => Err(vec![
126 CompileError::new(
127 "bynk.parse.unexpected_suite",
128 t.span,
129 "expected a `commons` declaration but found a `suite` declaration",
130 )
131 .with_note(
132 "tests must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
133 ),
134 ]),
135 SourceUnit::Adapter(a) => Err(vec![
136 CompileError::new(
137 "bynk.parse.unexpected_adapter",
138 a.span,
139 "expected a `commons` declaration but found an `adapter` declaration",
140 )
141 .with_note(
142 "adapters must be compiled as part of a project — pass the source directory, e.g. `bynkc compile --target bundle --output out src`",
143 ),
144 ]),
145 }
146}
147
148pub fn parse_unit_with_recovery(
157 tokens: &[Token],
158 source: &str,
159) -> (Option<SourceUnit>, Vec<CompileError>) {
160 let (filtered, trivia) = split_trivia(tokens, source);
161 let mut warnings = Vec::new();
162 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
163 p.recover_mode = true;
164 let unit_opt = match p.parse_unit() {
165 Ok(u) => {
166 while p.peek().is_some() {
172 match p.parse_unit() {
173 Ok(_) => {}
174 Err(e) => {
175 p.recovered_errors.push(e);
176 break;
177 }
178 }
179 }
180 Some(u)
181 }
182 Err(e) => {
183 p.recovered_errors.push(e);
184 None
185 }
186 };
187 let mut all_errors = p.recovered_errors;
188 all_errors.append(&mut warnings);
189 (unit_opt, all_errors)
190}
191
192pub fn parse_unit(tokens: &[Token], source: &str) -> Result<SourceUnit, Vec<CompileError>> {
196 parse_unit_with_warnings(tokens, source).map(|(unit, _warnings)| unit)
197}
198
199pub fn parse_unit_with_warnings(
202 tokens: &[Token],
203 source: &str,
204) -> Result<(SourceUnit, Vec<CompileError>), Vec<CompileError>> {
205 let (filtered, trivia) = split_trivia(tokens, source);
206 let mut warnings = Vec::new();
207 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
208 let result = match p.parse_unit() {
209 Ok(u) => {
210 if let Some(extra) = p.peek() {
211 Err(vec![
212 CompileError::new(
213 "bynk.parse.extra_tokens",
214 extra.span,
215 "unexpected token after top-level declaration",
216 )
217 .with_note(
218 "a `.bynk` file contains exactly one `commons` or `context` declaration",
219 ),
220 ])
221 } else {
222 Ok(u)
223 }
224 }
225 Err(e) => Err(vec![e]),
226 };
227 match result {
230 Ok(u) => Ok((u, warnings)),
231 Err(mut errs) => {
232 errs.append(&mut warnings);
233 Err(errs)
234 }
235 }
236}
237
238pub fn parse_units(tokens: &[Token], source: &str) -> Result<Vec<SourceUnit>, Vec<CompileError>> {
247 parse_units_with_warnings(tokens, source).map(|(units, _warnings)| units)
248}
249
250pub fn parse_units_with_warnings(
256 tokens: &[Token],
257 source: &str,
258) -> Result<(Vec<SourceUnit>, Vec<CompileError>), Vec<CompileError>> {
259 let (filtered, trivia) = split_trivia(tokens, source);
260 let mut warnings = Vec::new();
261 let mut p = Parser::new(&filtered, source, trivia, &mut warnings);
262 let mut units = Vec::new();
263 let mut errors: Vec<CompileError> = Vec::new();
264 while p.peek().is_some() {
265 match p.parse_unit() {
266 Ok(u) => units.push(u),
267 Err(e) => {
268 errors.push(e);
269 break;
270 }
271 }
272 }
273 let eof = p.eof_span();
274 if !errors.is_empty() {
277 errors.append(&mut warnings);
278 return Err(errors);
279 }
280 if units.is_empty() {
281 return Err(vec![CompileError::new(
282 "bynk.parse.unexpected_eof",
283 eof,
284 "expected `commons`, `context`, or `suite` to start the file, found end of file",
285 )]);
286 }
287 Ok((units, warnings))
288}
289
290enum SignedNumLit {
293 Int(IntBound),
294 Float(FloatBound),
295}
296
297struct Parser<'a> {
298 tokens: &'a [Token],
299 source: &'a str,
300 pos: usize,
301 warnings: &'a mut Vec<CompileError>,
304 recover_mode: bool,
310 recovered_errors: Vec<CompileError>,
313 trivia: TriviaTable,
316 depth: usize,
323 no_record_literal: bool,
332}
333
334impl<'a> Parser<'a> {
335 fn new(
336 tokens: &'a [Token],
337 source: &'a str,
338 trivia: TriviaTable,
339 warnings: &'a mut Vec<CompileError>,
340 ) -> Self {
341 Self {
342 tokens,
343 source,
344 pos: 0,
345 warnings,
346 recover_mode: false,
347 recovered_errors: Vec::new(),
348 trivia,
349 depth: 0,
350 no_record_literal: false,
351 }
352 }
353
354 fn enter_recursion(&mut self, what: &str) -> Result<(), CompileError> {
362 self.depth += 1;
363 if self.depth > crate::MAX_NESTING_DEPTH {
364 self.depth -= 1;
365 let span = self
366 .peek()
367 .map(|t| t.span)
368 .unwrap_or_else(|| self.eof_span());
369 return Err(self.nesting_too_deep(span, what));
370 }
371 Ok(())
372 }
373
374 fn nesting_too_deep(&self, span: Span, what: &str) -> CompileError {
377 CompileError::new(
378 "bynk.parse.nesting_too_deep",
379 span,
380 format!(
381 "{what} nests more than {} levels deep",
382 crate::MAX_NESTING_DEPTH
383 ),
384 )
385 .with_note(
386 "deeply nested source is rejected to keep the parser from overflowing its \
387 stack and aborting; flatten or split the construct",
388 )
389 }
390
391 fn expression_too_long(&self, span: Span) -> CompileError {
397 CompileError::new(
398 "bynk.parse.nesting_too_deep",
399 span,
400 format!(
401 "this expression is more than {} levels deep",
402 crate::MAX_NESTING_DEPTH
403 ),
404 )
405 .with_note(
406 "a long operator or member chain is rejected to keep the compiler from overflowing \
407 its stack; split it across `let` bindings, or reduce a sequence with \
408 `.sum()`/`.fold(...)`",
409 )
410 }
411
412 fn enter_chain_fold(&mut self, folds: &mut usize, span: Span) -> Result<(), CompileError> {
432 self.depth += 1;
433 *folds += 1;
434 if self.depth > crate::MAX_NESTING_DEPTH {
435 self.depth -= *folds;
436 *folds = 0;
437 return Err(self.expression_too_long(span));
438 }
439 Ok(())
440 }
441
442 fn deepen_spine(&mut self, span: Span) -> Result<(), CompileError> {
451 self.depth += 1;
452 if self.depth > crate::MAX_NESTING_DEPTH {
453 return Err(self.expression_too_long(span));
454 }
455 Ok(())
456 }
457
458 fn take_leading_trivia(&mut self) -> Vec<String> {
462 self.trivia.take_leading(self.pos)
463 }
464
465 fn take_trailing_trivia(&mut self) -> Option<String> {
469 if self.pos == 0 {
470 return None;
471 }
472 self.trivia.take_trailing(self.pos - 1)
473 }
474
475 fn handle_item_err(&mut self, e: CompileError) -> Result<(), CompileError> {
479 if self.recover_mode {
480 self.recovered_errors.push(e);
481 let before = self.pos;
482 self.recover_to_top_item();
483 if self.pos == before {
490 self.bump();
491 }
492 Ok(())
493 } else {
494 Err(e)
495 }
496 }
497
498 fn recover_to_top_item(&mut self) {
503 while let Some(t) = self.peek() {
504 match t.kind {
505 TokenKind::Type
506 | TokenKind::Fn
507 | TokenKind::Messages
508 | TokenKind::Uses
509 | TokenKind::Consumes
510 | TokenKind::Exports
511 | TokenKind::Capability
512 | TokenKind::Provides
513 | TokenKind::Stub
514 | TokenKind::Service
515 | TokenKind::Agent
516 | TokenKind::Suite
517 | TokenKind::Case
518 | TokenKind::RBrace
519 | TokenKind::Commons
520 | TokenKind::Context => return,
521 _ => {
522 self.bump();
523 }
524 }
525 }
526 }
527
528 fn peek(&self) -> Option<Token> {
529 self.tokens.get(self.pos).copied()
530 }
531
532 fn peek_kind(&self) -> Option<TokenKind> {
533 self.peek().map(|t| t.kind)
534 }
535
536 fn nth(&self, n: usize) -> Option<Token> {
538 self.tokens.get(self.pos + n).copied()
539 }
540
541 fn nth_kind(&self, n: usize) -> Option<TokenKind> {
542 self.nth(n).map(|t| t.kind)
543 }
544
545 fn nth_text(&self, n: usize) -> &'a str {
547 self.nth(n).map(|t| self.slice(t.span)).unwrap_or("")
548 }
549
550 fn prev_span(&self) -> Span {
553 self.tokens
554 .get(self.pos.wrapping_sub(1))
555 .or_else(|| self.peek_ref())
556 .map(|t| t.span)
557 .unwrap_or_default()
558 }
559
560 fn peek_ref(&self) -> Option<&Token> {
561 self.tokens.get(self.pos)
562 }
563
564 fn bump(&mut self) -> Option<Token> {
565 let t = self.peek();
566 if t.is_some() {
567 self.pos += 1;
568 }
569 t
570 }
571
572 fn eat(&mut self, kind: TokenKind) -> Option<Token> {
573 if self.peek_kind() == Some(kind) {
574 self.bump()
575 } else {
576 None
577 }
578 }
579
580 fn slice(&self, span: Span) -> &'a str {
581 &self.source[span.range()]
582 }
583
584 fn next_token_on_new_line(&self, prev: Span) -> bool {
589 match self.peek() {
590 Some(t) if prev.end <= t.span.start => {
591 self.source[prev.end..t.span.start].contains('\n')
592 }
593 _ => false,
594 }
595 }
596
597 fn eof_span(&self) -> Span {
602 let end = self.source.len();
603 let start = (0..end)
604 .rev()
605 .find(|&i| self.source.is_char_boundary(i))
606 .unwrap_or(0);
607 Span::new(start, end)
608 }
609
610 fn expect(&mut self, kind: TokenKind, ctx: &str) -> Result<Token, CompileError> {
611 match self.peek() {
612 Some(t) if t.kind == kind => {
613 self.bump();
614 Ok(t)
615 }
616 Some(t) => Err(CompileError::new(
617 "bynk.parse.expected_token",
618 t.span,
619 format!(
620 "expected {} {ctx}, found {}",
621 kind.describe(),
622 t.kind.describe()
623 ),
624 )),
625 None => Err(CompileError::new(
626 "bynk.parse.unexpected_eof",
627 self.eof_span(),
628 format!("expected {} {ctx}, found end of file", kind.describe()),
629 )),
630 }
631 }
632
633 fn expect_ident(&mut self, ctx: &str) -> Result<Ident, CompileError> {
634 match self.peek() {
635 Some(t) if t.kind == TokenKind::Ident => {
636 self.bump();
637 Ok(Ident {
638 name: self.slice(t.span).to_string(),
639 span: t.span,
640 })
641 }
642 Some(t) if crate::keywords::is_reserved_contextual(self.slice(t.span)) => {
657 self.bump();
658 Ok(Ident {
659 name: self.slice(t.span).to_string(),
660 span: t.span,
661 })
662 }
663 Some(t) if is_reserved_keyword(t.kind) => Err(CompileError::new(
664 "bynk.parse.reserved_keyword",
665 t.span,
666 format!(
667 "expected identifier {ctx}, but `{}` is a reserved keyword",
668 self.slice(t.span)
669 ),
670 )
671 .with_note("rename the identifier to something that is not a keyword")),
672 Some(t) => Err(CompileError::new(
673 "bynk.parse.expected_token",
674 t.span,
675 format!("expected identifier {ctx}, found {}", t.kind.describe()),
676 )),
677 None => Err(CompileError::new(
678 "bynk.parse.unexpected_eof",
679 self.eof_span(),
680 format!("expected identifier {ctx}, found end of file"),
681 )),
682 }
683 }
684
685 fn take_doc_block(&mut self) -> Option<(String, Span)> {
691 if self.peek_kind() == Some(TokenKind::DocBlock) {
692 let t = self.bump().unwrap();
693 let body = doc_block_content(self.source, t.span);
694 return Some((body, t.span));
695 }
696 None
697 }
698
699 fn collect_item_lead(&mut self) -> (Vec<String>, Option<(String, Span)>) {
704 let mut leading = self.take_leading_trivia();
705 let doc = self.take_doc_block();
706 if doc.is_some() {
707 leading.extend(self.take_leading_trivia());
708 }
709 (leading, doc)
710 }
711
712 fn finalize_doc(&mut self, doc: Option<(String, Span)>, next_span: Span) -> Option<String> {
715 let (content, doc_span) = doc?;
716 if has_blank_line_between(self.source, doc_span.end, next_span.start) {
718 self.warnings.push(
719 CompileError::new(
720 "bynk.parse.orphan_doc_block",
721 doc_span,
722 "documentation block is separated from the following declaration by a blank line; it will not be attached",
723 )
724 .with_note(
725 "remove the blank line to attach the doc to the next declaration, \
726 or remove the doc block if it is not meant to document anything",
727 ),
728 );
729 return None;
730 }
731 Some(content)
732 }
733}
734
735fn parse_string_literal(lexeme: &str, span: Span) -> Result<String, CompileError> {
738 let bytes = lexeme.as_bytes();
739 debug_assert!(bytes.first() == Some(&b'"') && bytes.last() == Some(&b'"'));
740 let inner = &lexeme[1..lexeme.len() - 1];
741 let mut out = String::with_capacity(inner.len());
742 let mut chars = inner.chars();
743 while let Some(c) = chars.next() {
744 if c == '\\' {
745 match chars.next() {
746 Some('n') => out.push('\n'),
747 Some('t') => out.push('\t'),
748 Some('"') => out.push('"'),
749 Some('\\') => out.push('\\'),
750 other => {
751 return Err(CompileError::new(
752 "bynk.lex.bad_escape",
753 span,
754 format!(
755 "invalid escape sequence `\\{}` in string literal",
756 other.map(|c| c.to_string()).unwrap_or_default()
757 ),
758 )
759 .with_note("supported escapes: \\n \\t \\\" \\\\"));
760 }
761 }
762 } else {
763 out.push(c);
764 }
765 }
766 Ok(out)
767}
768
769fn is_reserved_keyword(kind: TokenKind) -> bool {
770 use TokenKind::*;
771 matches!(
772 kind,
773 Commons
774 | Type
775 | Fn
776 | Where
777 | True
778 | False
779 | Int
780 | String
781 | Bool
782 | Let
783 | If
784 | Else
785 | Ok
786 | Err
787 | Result
788 | ValidationError
789 | Enum
790 | Match
791 | Option
792 | Record
793 | Self_
794 | Some
795 | None
796 | Is
797 | Opaque
798 | Uses
799 | Context
800 | Consumes
801 | Exports
802 | Transparent
803 | Agent
804 | As
805 | Capability
806 | Effect
807 | Do
808 | Given
809 | On
810 | Http
811 | Provides
812 | Stub
813 | Service
814 | Actor
815 | By
816 | Expect
817 | Suite
818 | Case
819 | Float
820 | Duration
821 | Instant
822 | Bytes
823 | JsonError
824 | Property
825 | Adapter
826 | Binding
827 | Cron
828 | Queue
829 | From
830 | Protocol
831 | Invariant
832 | Implies
833 | Requires
834 | Ensures
835 | Transition
836 )
837}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842 use crate::lexer::tokenize;
843
844 fn parse_str(src: &str) -> Result<Commons, Vec<CompileError>> {
845 let toks = tokenize(src).map_err(|e| vec![e])?;
846 parse(&toks, src)
847 }
848
849 fn parse_recover_str(src: &str) -> (Option<SourceUnit>, Vec<CompileError>) {
850 let toks = match tokenize(src) {
851 Ok(t) => t,
852 Err(e) => return (None, vec![e]),
853 };
854 parse_unit_with_recovery(&toks, src)
855 }
856
857 #[test]
858 fn eof_span_never_splits_a_multibyte_codepoint() {
859 for src in [
864 "commons x {\n -- ends with an arrow →",
865 "agent A {\n key k: String\n -- note 🦀",
866 "commons y {\n type T = é",
867 ] {
868 let (_unit, errors) = parse_recover_str(src);
869 for e in &errors {
870 assert!(
871 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
872 "span {:?} splits a codepoint in {src:?}",
873 e.span,
874 );
875 }
876 }
877 }
878
879 #[test]
880 fn recovery_skips_garbage_between_decls() {
881 let src = "commons x {\n\
884 type A = Int where NonNegative\n\
885 ??? !!!\n\
886 type B = String where NonEmpty\n\
887 }";
888 let (unit, errors) = parse_recover_str(src);
889 let unit = unit.expect("recovery should produce a partial AST");
890 let SourceUnit::Commons(c) = unit else {
891 panic!("expected commons")
892 };
893 let names: Vec<_> = c
895 .items
896 .iter()
897 .map(|i| match i {
898 CommonsItem::Type(t) => t.name.name.clone(),
899 _ => panic!("expected only types"),
900 })
901 .collect();
902 assert!(
903 names.contains(&"A".to_string()) && names.contains(&"B".to_string()),
904 "expected both A and B; got {names:?}",
905 );
906 assert!(!errors.is_empty(), "expected at least one parse error");
907 }
908
909 #[test]
910 fn recovery_handles_bad_first_decl_then_good_second() {
911 let src = "commons x {\n\
913 type A Int where NonNegative\n\
914 type B = String where NonEmpty\n\
915 }";
916 let (unit, errors) = parse_recover_str(src);
917 let unit = unit.expect("recovery should produce a partial AST");
918 let SourceUnit::Commons(c) = unit else {
919 panic!("expected commons")
920 };
921 let names: Vec<_> = c
922 .items
923 .iter()
924 .filter_map(|i| match i {
925 CommonsItem::Type(t) => Some(t.name.name.clone()),
926 _ => None,
927 })
928 .collect();
929 assert!(
930 names.contains(&"B".to_string()),
931 "B should be parsed after A's failure; got {names:?}"
932 );
933 assert!(!errors.is_empty(), "expected at least one parse error");
934 }
935
936 #[test]
937 fn doc_block_attaches_to_type() {
938 let c =
939 parse_str("commons x {\n---\nA descriptive doc.\n---\ntype T = Int where Positive\n}")
940 .unwrap();
941 let CommonsItem::Type(t) = &c.items[0] else {
942 panic!()
943 };
944 assert!(t.documentation.is_some());
945 assert!(
946 t.documentation
947 .as_ref()
948 .unwrap()
949 .contains("A descriptive doc.")
950 );
951 }
952
953 #[test]
954 fn interpolated_string_parses_into_parts() {
955 let c = parse_str("commons x\n\nfn f(name: String) -> String {\n \"Hi, \\(name)!\"\n}\n")
957 .unwrap();
958 let CommonsItem::Fn(f) = &c.items[0] else {
959 panic!("expected fn")
960 };
961 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
962 panic!("expected InterpStr, got {:?}", f.body.tail.kind)
963 };
964 assert_eq!(parts.len(), 3);
965 assert!(matches!(&parts[0], InterpPart::Chunk(s) if s == "Hi, "));
966 assert!(
967 matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::Ident(id) if id.name == "name"))
968 );
969 assert!(matches!(&parts[2], InterpPart::Chunk(s) if s == "!"));
970 }
971
972 #[test]
973 fn interpolated_hole_parses_a_full_expression() {
974 let c =
976 parse_str("commons x\n\nfn f(a: Int, b: Int) -> String {\n \"sum = \\(a + b)\"\n}\n")
977 .unwrap();
978 let CommonsItem::Fn(f) = &c.items[0] else {
979 panic!("expected fn")
980 };
981 let ExprKind::InterpStr(parts) = &f.body.tail.kind else {
982 panic!("expected InterpStr")
983 };
984 assert!(matches!(&parts[1], InterpPart::Hole(h) if matches!(&h.kind, ExprKind::BinOp(..))));
985 }
986
987 #[test]
988 fn empty_interpolation_hole_is_rejected() {
989 let errs = parse_str("commons x\n\nfn f() -> String {\n \"\\()\"\n}\n").unwrap_err();
990 assert!(
991 errs.iter()
992 .any(|e| e.category == "bynk.parse.empty_interpolation"),
993 "expected empty_interpolation; got {errs:?}"
994 );
995 }
996
997 #[test]
998 fn interpolation_hole_lex_error_span_is_rebased() {
999 let cases = [
1005 "commons x\n\nfn f() -> String {\n \"a \\($)\"\n}\n",
1007 "commons x\n\nfn f() -> String {\n \"n = \\(99999999999999999999)\"\n}\n",
1009 "commons x\n\nfn f() -> String {\n \"é \\($)\"\n}\n",
1012 ];
1013 for src in cases {
1014 let errs = parse_str(src).unwrap_err();
1015 assert!(!errs.is_empty(), "expected a lex error for {src:?}");
1016 for e in &errs {
1017 assert!(
1018 src.is_char_boundary(e.span.start) && src.is_char_boundary(e.span.end),
1019 "span {:?} splits a codepoint in {src:?}",
1020 e.span,
1021 );
1022 let hole_start = src.find("\\(").expect("case has a hole") + 2;
1025 assert!(
1026 e.span.start >= hole_start,
1027 "span {:?} precedes the hole (starts at {hole_start}) in {src:?}",
1028 e.span,
1029 );
1030 }
1031 }
1032 }
1033
1034 #[test]
1035 fn fragment_form_parses() {
1036 let c = parse_str("commons x.y\n\ntype T = Int where NonNegative\n").unwrap();
1037 assert_eq!(c.form, CommonsForm::Fragment);
1038 assert_eq!(c.items.len(), 1);
1039 }
1040
1041 #[test]
1042 fn uses_parses() {
1043 let c = parse_str("commons x\n\nuses other.lib\n").unwrap();
1044 assert_eq!(c.uses.len(), 1);
1045 assert_eq!(c.uses[0].target.joined(), "other.lib");
1046 }
1047
1048 fn parse_unit_str(src: &str) -> Result<SourceUnit, Vec<CompileError>> {
1049 let toks = tokenize(src).map_err(|e| vec![e])?;
1050 parse_unit(&toks, src)
1051 }
1052
1053 #[test]
1054 fn minimal_context_parses() {
1055 let u = parse_unit_str("context commerce.orders {}").unwrap();
1056 let SourceUnit::Context(c) = u else {
1057 panic!("expected context");
1058 };
1059 assert_eq!(c.name.joined(), "commerce.orders");
1060 assert!(c.items.is_empty());
1061 }
1062
1063 #[test]
1064 fn context_consumes_and_exports_parse() {
1065 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}";
1066 let u = parse_unit_str(src).unwrap();
1067 let SourceUnit::Context(c) = u else { panic!() };
1068 assert_eq!(c.uses.len(), 1);
1069 assert_eq!(c.consumes.len(), 1);
1070 assert_eq!(c.exports.len(), 2);
1071 assert_eq!(c.exports[0].kind, ExportKind::Type(Visibility::Opaque));
1072 assert_eq!(c.exports[1].kind, ExportKind::Type(Visibility::Transparent));
1073 }
1074
1075 #[test]
1076 fn context_fragment_form_parses() {
1077 let src = "context x.y\n\nuses other.lib\nconsumes other.ctx\nexports opaque { T }\n\ntype T = Int where NonNegative\n";
1078 let u = parse_unit_str(src).unwrap();
1079 let SourceUnit::Context(c) = u else { panic!() };
1080 assert_eq!(c.form, CommonsForm::Fragment);
1081 assert_eq!(c.uses.len(), 1);
1082 assert_eq!(c.consumes.len(), 1);
1083 assert_eq!(c.exports.len(), 1);
1084 }
1085
1086 #[test]
1087 fn opaque_type_parses() {
1088 let c = parse_str("commons x { type T = opaque Int where NonNegative }").unwrap();
1089 let CommonsItem::Type(t) = &c.items[0] else {
1090 panic!()
1091 };
1092 assert!(matches!(t.body, TypeBody::Opaque { .. }));
1093 }
1094
1095 #[test]
1096 fn empty_commons() {
1097 let c = parse_str("commons fitness.units {}").unwrap();
1098 assert_eq!(c.name.joined(), "fitness.units");
1099 assert!(c.items.is_empty());
1100 }
1101
1102 #[test]
1103 fn one_type_decl() {
1104 let c = parse_str("commons x { type Metres = Int where NonNegative }").unwrap();
1105 assert_eq!(c.items.len(), 1);
1106 let CommonsItem::Type(t) = &c.items[0] else {
1107 panic!()
1108 };
1109 assert_eq!(t.name.name, "Metres");
1110 match &t.body {
1111 TypeBody::Refined {
1112 base, refinement, ..
1113 } => {
1114 assert_eq!(*base, BaseType::Int);
1115 assert!(refinement.is_some());
1116 }
1117 _ => panic!("expected refined body"),
1118 }
1119 }
1120
1121 #[test]
1122 fn function_decl() {
1123 let c = parse_str("commons x { fn add(a: Int, b: Int) -> Int { a + b } }").unwrap();
1124 let CommonsItem::Fn(f) = &c.items[0] else {
1125 panic!()
1126 };
1127 assert_eq!(f.name.ident().name, "add");
1128 assert_eq!(f.params.len(), 2);
1129 }
1130
1131 #[test]
1132 fn chained_comparison_is_error() {
1133 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a < b < c } }")
1134 .unwrap_err();
1135 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1136 }
1137
1138 #[test]
1139 fn chained_equality_is_error() {
1140 let errs = parse_str("commons x { fn f(a: Int, b: Int, c: Int) -> Bool { a == b == c } }")
1141 .unwrap_err();
1142 assert_eq!(errs[0].category, "bynk.parse.non_associative");
1143 }
1144
1145 fn on_big_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
1154 std::thread::Builder::new()
1155 .stack_size(64 * 1024 * 1024)
1156 .spawn(f)
1157 .unwrap()
1158 .join()
1159 .unwrap()
1160 }
1161
1162 #[test]
1163 fn deeply_nested_parens_are_bounded_not_overflowed() {
1164 let errs = on_big_stack(|| {
1170 let depth = crate::MAX_NESTING_DEPTH + 8;
1171 let src = format!(
1172 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1173 "(".repeat(depth),
1174 ")".repeat(depth),
1175 );
1176 parse_str(&src).unwrap_err()
1177 });
1178 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1179 }
1180
1181 #[test]
1182 fn deeply_nested_types_are_bounded_not_overflowed() {
1183 let errs = on_big_stack(|| {
1188 let depth = crate::MAX_NESTING_DEPTH + 8;
1189 let src = format!(
1190 "commons x {{ fn f(x: {}Int{}) -> Int {{ 0 }} }}",
1191 "Result[Int, ".repeat(depth),
1192 "]".repeat(depth),
1193 );
1194 parse_str(&src).unwrap_err()
1195 });
1196 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1197 }
1198
1199 #[test]
1200 fn deeply_nested_patterns_are_bounded_not_overflowed() {
1201 let errs = on_big_stack(|| {
1206 let depth = crate::MAX_NESTING_DEPTH + 8;
1207 let src = format!(
1208 "commons x {{ fn f(n: Int) -> Int {{ match n {{ {}n{} => 0 }} }} }}",
1209 "Ok(".repeat(depth),
1210 ")".repeat(depth),
1211 );
1212 parse_str(&src).unwrap_err()
1213 });
1214 assert_eq!(errs[0].category, "bynk.parse.nesting_too_deep");
1215 }
1216
1217 #[test]
1218 fn nesting_below_the_limit_still_parses() {
1219 let ok = on_big_stack(|| {
1222 let depth = crate::MAX_NESTING_DEPTH - 8;
1223 let src = format!(
1224 "commons x {{ fn f() -> Int {{ {}0{} }} }}",
1225 "(".repeat(depth),
1226 ")".repeat(depth),
1227 );
1228 parse_str(&src).is_ok()
1229 });
1230 assert!(ok, "well-nested source under the limit should parse");
1231 }
1232
1233 #[test]
1234 fn let_statement_parses() {
1235 let c = parse_str("commons x { fn f(n: Int) -> Int { let y = n + 1\n y } }").unwrap();
1236 let CommonsItem::Fn(f) = &c.items[0] else {
1237 panic!()
1238 };
1239 assert_eq!(f.body.statements.len(), 1);
1240 match &f.body.statements[0] {
1241 Statement::Let(l) => {
1242 assert_eq!(l.name.name, "y");
1243 assert!(l.type_annot.is_none());
1244 }
1245 _ => panic!("expected a pure `let` statement"),
1246 }
1247 }
1248
1249 #[test]
1250 fn let_with_annotation() {
1251 let c = parse_str("commons x { fn f(n: Int) -> Int { let y: Int = n\n y } }").unwrap();
1252 let CommonsItem::Fn(f) = &c.items[0] else {
1253 panic!()
1254 };
1255 match &f.body.statements[0] {
1256 Statement::Let(l) => assert!(l.type_annot.is_some()),
1257 _ => panic!("expected a pure `let` statement"),
1258 }
1259 }
1260
1261 #[test]
1262 fn if_else_parses_as_expression() {
1263 let c = parse_str("commons x { fn f(b: Bool) -> Int { if b { 1 } else { 0 } } }").unwrap();
1264 let CommonsItem::Fn(f) = &c.items[0] else {
1265 panic!()
1266 };
1267 assert!(matches!(f.body.tail.kind, ExprKind::If { .. }));
1268 }
1269
1270 #[test]
1271 fn else_if_chain_parses() {
1272 let c = parse_str(
1273 "commons x { fn f(n: Int) -> Int { if n < 0 { -1 } else if n == 0 { 0 } else { 1 } } }",
1274 )
1275 .unwrap();
1276 let CommonsItem::Fn(f) = &c.items[0] else {
1277 panic!()
1278 };
1279 let ExprKind::If { else_block, .. } = &f.body.tail.kind else {
1280 panic!()
1281 };
1282 assert!(else_block.statements.is_empty());
1284 assert!(matches!(else_block.tail.kind, ExprKind::If { .. }));
1285 }
1286
1287 #[test]
1288 fn ok_and_err_parse_as_expressions() {
1289 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1290 let CommonsItem::Fn(f) = &c.items[0] else {
1291 panic!()
1292 };
1293 assert!(matches!(f.body.tail.kind, ExprKind::Ok(_)));
1294
1295 let c =
1296 parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Err(\"x\") } }").unwrap();
1297 let CommonsItem::Fn(f) = &c.items[0] else {
1298 panic!()
1299 };
1300 assert!(matches!(f.body.tail.kind, ExprKind::Err(_)));
1301 }
1302
1303 #[test]
1304 fn question_postfix_parses() {
1305 let c = parse_str(
1306 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { let x = T.of(n)?\n Ok(x) } }",
1307 )
1308 .unwrap();
1309 let CommonsItem::Fn(f) = &c.items[1] else {
1310 panic!()
1311 };
1312 let Statement::Let(l) = &f.body.statements[0] else {
1313 panic!("expected a pure `let` statement");
1314 };
1315 assert!(matches!(l.value.kind, ExprKind::Question(_)));
1316 }
1317
1318 #[test]
1319 fn constructor_call_parses() {
1320 let c = parse_str(
1321 "commons x { type T = Int where Positive\n fn f(n: Int) -> Result[T, ValidationError] { T.of(n) } }",
1322 )
1323 .unwrap();
1324 let CommonsItem::Fn(f) = &c.items[1] else {
1325 panic!()
1326 };
1327 let ExprKind::MethodCall {
1330 receiver, method, ..
1331 } = &f.body.tail.kind
1332 else {
1333 panic!("expected MethodCall, got {:?}", f.body.tail.kind)
1334 };
1335 let ExprKind::Ident(id) = &receiver.kind else {
1336 panic!("expected receiver Ident");
1337 };
1338 assert_eq!(id.name, "T");
1339 assert_eq!(method.name, "of");
1340 }
1341
1342 #[test]
1343 fn result_type_ref_parses() {
1344 let c = parse_str("commons x { fn f(n: Int) -> Result[Int, String] { Ok(n) } }").unwrap();
1345 let CommonsItem::Fn(f) = &c.items[0] else {
1346 panic!()
1347 };
1348 assert!(matches!(f.return_type, TypeRef::Result(_, _, _)));
1349 }
1350
1351 #[test]
1352 fn result_missing_arg_count_errors() {
1353 let errs = parse_str("commons x { fn f(n: Int) -> Result[Int] { Ok(n) } }").unwrap_err();
1354 assert_eq!(errs[0].category, "bynk.parse.generic_arg_count");
1355 }
1356
1357 #[test]
1358 fn field_access_parses_in_v0_2() {
1359 let c =
1362 parse_str("commons x { type R = { foo: Int }\n fn f(r: R) -> Int { r.foo } }").unwrap();
1363 let CommonsItem::Fn(f) = &c.items[1] else {
1364 panic!()
1365 };
1366 assert!(matches!(f.body.tail.kind, ExprKind::FieldAccess { .. }));
1367 }
1368
1369 #[test]
1372 fn leading_line_comment_attaches_to_next_decl() {
1373 let src = "commons x {\n-- explain the type\ntype T = Int where NonNegative\n}";
1374 let c = parse_str(src).unwrap();
1375 let CommonsItem::Type(t) = &c.items[0] else {
1376 panic!()
1377 };
1378 assert_eq!(t.trivia.leading, vec![" explain the type".to_string()]);
1379 assert!(t.trivia.trailing.is_none());
1380 }
1381
1382 #[test]
1383 fn trailing_line_comment_attaches_to_prev_decl() {
1384 let src = "commons x {\ntype T = Int where NonNegative -- trailing note\n}";
1385 let c = parse_str(src).unwrap();
1386 let CommonsItem::Type(t) = &c.items[0] else {
1387 panic!()
1388 };
1389 assert!(t.trivia.leading.is_empty());
1390 assert_eq!(t.trivia.trailing.as_deref(), Some(" trailing note"));
1391 }
1392
1393 #[test]
1394 fn grouped_leading_comments_attach_together() {
1395 let src = "commons x {\n-- one\n-- two\n-- three\ntype T = Int where Positive\n}";
1396 let c = parse_str(src).unwrap();
1397 let CommonsItem::Type(t) = &c.items[0] else {
1398 panic!()
1399 };
1400 assert_eq!(
1401 t.trivia.leading,
1402 vec![" one".to_string(), " two".to_string(), " three".to_string()],
1403 );
1404 }
1405
1406 #[test]
1407 fn comment_with_doc_block_keeps_both() {
1408 let src = "commons x {\n-- intro\n---\ndocs\n---\ntype T = Int where Positive\n}";
1410 let c = parse_str(src).unwrap();
1411 let CommonsItem::Type(t) = &c.items[0] else {
1412 panic!()
1413 };
1414 assert_eq!(t.trivia.leading, vec![" intro".to_string()]);
1415 assert_eq!(t.documentation.as_deref(), Some("docs"));
1416 }
1417
1418 #[test]
1419 fn messages_keyword_does_not_collide_with_a_commons_name_segment() {
1420 let src = "commons app.messages {\ntype T = Int where Positive\n}";
1426 let c = parse_str(src).unwrap();
1427 assert_eq!(c.name.joined(), "app.messages");
1428 }
1429
1430 #[test]
1431 fn messages_decl_parses_tag_annotation_and_entries() {
1432 let src = "commons app.messages {\n\
1434 -- intro\n\
1435 ---\n\
1436 docs\n\
1437 ---\n\
1438 messages en @reference {\n\
1439 \"greeting\" => \"Hello, {name}!\"\n\
1440 \"farewell\" => \"Bye\"\n\
1441 } -- trailing\n\
1442 }";
1443 let c = parse_str(src).unwrap();
1444 let CommonsItem::Messages(m) = &c.items[0] else {
1445 panic!("expected a messages item, got {:?}", c.items[0]);
1446 };
1447 assert_eq!(m.tag.name, "en");
1448 assert_eq!(m.annotations.len(), 1);
1449 assert_eq!(m.annotations[0].name.name, "reference");
1450 assert!(m.annotations[0].args.is_empty());
1451 assert_eq!(m.entries.len(), 2);
1452 assert_eq!(m.entries[0].code, "greeting");
1453 assert_eq!(m.entries[0].template, "Hello, {name}!");
1454 assert_eq!(m.entries[1].code, "farewell");
1455 assert_eq!(m.entries[1].template, "Bye");
1456 assert_eq!(m.trivia.leading, vec![" intro".to_string()]);
1457 assert_eq!(m.documentation.as_deref(), Some("docs"));
1458 assert_eq!(m.trivia.trailing.as_deref(), Some(" trailing"));
1459 }
1460
1461 #[test]
1462 fn messages_decl_parses_with_no_annotation_and_no_entries() {
1463 let src = "commons app.messages {\nmessages en {\n}\n}";
1467 let c = parse_str(src).unwrap();
1468 let CommonsItem::Messages(m) = &c.items[0] else {
1469 panic!("expected a messages item, got {:?}", c.items[0]);
1470 };
1471 assert_eq!(m.tag.name, "en");
1472 assert!(m.annotations.is_empty());
1473 assert!(m.entries.is_empty());
1474 }
1475
1476 #[test]
1477 fn messages_decl_parses_syntactically_inside_a_context_too() {
1478 let src = "context app.svc {\nmessages en @reference {\n\"a\" => \"b\"\n}\n}";
1483 let toks = tokenize(src).unwrap();
1484 let (unit, errors) = parse_unit_with_recovery(&toks, src);
1485 assert!(errors.is_empty(), "unexpected parse errors: {errors:?}");
1486 let Some(SourceUnit::Context(ctx)) = unit else {
1487 panic!("expected a context")
1488 };
1489 let CommonsItem::Messages(m) = &ctx.items[0] else {
1490 panic!("expected a messages item, got {:?}", ctx.items[0]);
1491 };
1492 assert_eq!(m.tag.name, "en");
1493 }
1494
1495 #[test]
1496 fn comment_before_let_statement_attaches() {
1497 let src = "commons x {\nfn f(n: Int) -> Int {\n-- pick a value\nlet y = n + 1\ny\n}\n}";
1498 let c = parse_str(src).unwrap();
1499 let CommonsItem::Fn(f) = &c.items[0] else {
1500 panic!()
1501 };
1502 let Statement::Let(l) = &f.body.statements[0] else {
1503 panic!()
1504 };
1505 assert_eq!(l.trivia.leading, vec![" pick a value".to_string()]);
1506 }
1507
1508 #[test]
1509 fn comment_before_tail_attaches_to_block_tail() {
1510 let src = "commons x {\nfn f(n: Int) -> Int {\nlet y = n + 1\n-- result\ny\n}\n}";
1511 let c = parse_str(src).unwrap();
1512 let CommonsItem::Fn(f) = &c.items[0] else {
1513 panic!()
1514 };
1515 assert_eq!(f.body.tail_leading_comments, vec![" result".to_string()],);
1516 }
1517
1518 #[test]
1524 fn contextual_keywords_are_valid_identifiers() {
1525 let c = parse_str("commons demo {\n type R = { on: Int, suite: String, case: Bool }\n}")
1527 .expect("`on`/`suite`/`case` are valid field names");
1528 let CommonsItem::Type(_) = &c.items[0] else {
1529 panic!("expected a type decl")
1530 };
1531
1532 parse_str("commons demo {\n fn f(on: Int, case: Int) -> Int { 0 }\n}")
1534 .expect("`on`/`case` are valid parameter names");
1535
1536 parse_str("commons demo {\n type R = { suite: Int }\n}")
1538 .expect("`suite` is a valid field name");
1539 }
1540
1541 #[test]
1549 fn is_reserved_keyword_covers_every_lexer_keyword() {
1550 let lexer_src = include_str!("lexer.rs");
1551 let mut words = Vec::new();
1552 for line in lexer_src.lines() {
1553 let t = line.trim();
1554 if let Some(rest) = t.strip_prefix("#[token(\"")
1555 && let Some(word) = rest.split('"').next()
1556 && word.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
1557 && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
1558 {
1559 words.push(word.to_string());
1560 }
1561 }
1562 assert!(
1563 words.len() > 30,
1564 "keyword extraction looks broken: only {} words",
1565 words.len()
1566 );
1567 use crate::keywords::RESERVED_CONTEXTUAL;
1570 let mut unclassified = Vec::new();
1571 for word in &words {
1572 let tokens = crate::lexer::tokenize(word).expect("keyword lexes");
1573 let kind = tokens.first().expect("keyword yields a token").kind;
1574 if !is_reserved_keyword(kind) && !RESERVED_CONTEXTUAL.contains(&word.as_str()) {
1575 unclassified.push(word.clone());
1576 }
1577 }
1578 assert!(
1579 unclassified.is_empty(),
1580 "keywords missing from is_reserved_keyword (add them, or document \
1581 them as contextual): {unclassified:?}"
1582 );
1583 }
1584
1585 #[test]
1590 fn recovery_makes_progress_on_context_only_keyword_in_commons() {
1591 let src = "commons demo\n\ncapability Logger {\n fn log(m: String) -> Effect[()]\n}\n";
1592 let tokens = crate::lexer::tokenize(src).unwrap();
1593 let (unit, errors) = parse_unit_with_recovery(&tokens, src);
1594 assert!(unit.is_some(), "the commons header still parses");
1595 assert!(
1596 errors
1597 .iter()
1598 .any(|e| e.category == "bynk.capability.outside_context"),
1599 "the misplaced capability is reported: {errors:?}"
1600 );
1601 assert!(errors.len() < 10, "recovery repeated itself: {errors:?}");
1604 }
1605
1606 #[test]
1607 fn trailing_file_comment_becomes_unit_trailing() {
1608 let src = "commons x\n\ntype T = Int where Positive\n-- afterword\n";
1612 let c = parse_str(src).unwrap();
1613 assert_eq!(c.trailing_comments, vec![" afterword".to_string()]);
1614 }
1615
1616 fn body_tail(body: &str) -> ExprKind {
1620 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
1621 let c = parse_str(&src).unwrap_or_else(|e| panic!("parse failed for {body:?}: {e:?}"));
1622 let CommonsItem::Fn(f) = &c.items[0] else {
1623 panic!("expected fn, got {:?}", c.items[0]);
1624 };
1625 f.body.tail.kind.clone()
1626 }
1627
1628 fn body_err(body: &str) -> Vec<CompileError> {
1629 let src = format!("commons x\n\nfn f() -> Int {{\n {body}\n}}\n");
1630 parse_str(&src).expect_err(&format!("expected a parse error for {body:?}"))
1631 }
1632
1633 #[test]
1634 fn if_condition_ending_in_ident_does_not_swallow_a_single_ident_branch() {
1635 for src in [
1638 "if ready { result } else { fallback }",
1639 "if ready { fallback } else { result }",
1640 "if !ready { result } else { fallback }",
1641 "if a == b { result } else { fallback }",
1642 "if a && b { result } else { fallback }",
1643 ] {
1644 let ExprKind::If {
1645 then_block,
1646 else_block,
1647 ..
1648 } = body_tail(src)
1649 else {
1650 panic!("expected If for {src:?}, got {:?}", body_tail(src));
1651 };
1652 assert!(
1655 matches!(&then_block.tail.kind, ExprKind::Ident(_)),
1656 "then-branch tail not an ident for {src:?}: {:?}",
1657 then_block.tail.kind,
1658 );
1659 assert!(
1660 matches!(&else_block.tail.kind, ExprKind::Ident(_)),
1661 "else-branch tail not an ident for {src:?}: {:?}",
1662 else_block.tail.kind,
1663 );
1664 }
1665 }
1666
1667 #[test]
1668 fn else_less_if_with_single_ident_branch_parses() {
1669 let ExprKind::If { then_block, .. } = body_tail("if ready { result }") else {
1671 panic!("expected If");
1672 };
1673 assert!(matches!(&then_block.tail.kind, ExprKind::Ident(_)));
1674 }
1675
1676 #[test]
1677 fn record_construction_still_parses_in_value_position() {
1678 assert!(matches!(
1681 body_tail("Point { x }"),
1682 ExprKind::RecordConstruction { .. }
1683 ));
1684 assert!(matches!(
1685 body_tail("Point { x: 1, y: 2 }"),
1686 ExprKind::RecordConstruction { .. }
1687 ));
1688 assert!(matches!(
1689 body_tail("Empty {}"),
1690 ExprKind::RecordConstruction { .. }
1691 ));
1692 }
1693
1694 #[test]
1695 fn parenthesised_record_is_allowed_in_condition_head() {
1696 let ExprKind::If { cond, .. } =
1699 body_tail("if (ready { result }) { branch } else { other }")
1700 else {
1701 panic!("expected If");
1702 };
1703 let ExprKind::Paren(inner) = &cond.kind else {
1704 panic!("expected a parenthesised condition, got {:?}", cond.kind);
1705 };
1706 assert!(
1707 matches!(&inner.kind, ExprKind::RecordConstruction { .. }),
1708 "parenthesised record in condition head should still construct: {:?}",
1709 inner.kind,
1710 );
1711 }
1712
1713 #[test]
1714 fn record_in_call_arg_within_condition_still_constructs() {
1715 let ExprKind::If { cond, .. } = body_tail("if check(Point { x: 1 }) { a } else { b }")
1718 else {
1719 panic!("expected If");
1720 };
1721 let ExprKind::Call { args, .. } = &cond.kind else {
1722 panic!("expected Call in condition, got {:?}", cond.kind);
1723 };
1724 assert!(matches!(&args[0].kind, ExprKind::RecordConstruction { .. }));
1725 }
1726
1727 #[test]
1728 fn safe_condition_shapes_are_unaffected() {
1729 assert!(matches!(
1731 body_tail("if ready == true { result } else { fallback }"),
1732 ExprKind::If { .. }
1733 ));
1734 assert!(matches!(
1735 body_tail("if (ready) { result } else { fallback }"),
1736 ExprKind::If { .. }
1737 ));
1738 assert!(matches!(
1739 body_tail("if ready { \"a\" } else { \"b\" }"),
1740 ExprKind::If { .. }
1741 ));
1742 }
1743
1744 #[test]
1745 fn empty_match_reports_its_own_diagnostic() {
1746 let errs = body_err("match result {}");
1750 assert!(
1751 errs.iter().any(|e| e.category == "bynk.parse.empty_match"),
1752 "expected empty_match; got {errs:?}",
1753 );
1754 }
1755
1756 #[test]
1757 fn match_discriminant_ending_in_ident_parses() {
1758 assert!(matches!(
1760 body_tail("match ready { x => x }"),
1761 ExprKind::Match { .. }
1762 ));
1763 }
1764
1765 #[test]
1766 fn unparenthesised_record_in_condition_head_now_errors() {
1767 assert!(
1773 !body_err("match Point { x: 1 } { p => p }").is_empty(),
1774 "unparenthesised record discriminant should not parse",
1775 );
1776 let ExprKind::Match { discriminant, .. } = body_tail("match (Point { x: 1 }) { p => p }")
1778 else {
1779 panic!("expected Match for the parenthesised form");
1780 };
1781 let ExprKind::Paren(inner) = &discriminant.kind else {
1782 panic!(
1783 "expected a parenthesised discriminant, got {:?}",
1784 discriminant.kind
1785 );
1786 };
1787 assert!(matches!(&inner.kind, ExprKind::RecordConstruction { .. }));
1788 }
1789}