1use super::*;
7pub fn parser_stage() -> &'static str {
9 whipplescript_core::IMPLEMENTATION_STAGE
10}
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub(crate) struct Lexed {
14 pub(crate) tokens: Vec<Token>,
15 diagnostics: Vec<Diagnostic>,
16 comments: Vec<Comment>,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub(crate) struct Token {
21 kind: TokenKind,
22 span: SourceSpan,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub(crate) enum TokenKind {
27 Ident(String),
28 String(String),
29 Number(String),
30 Arrow,
31 ThinArrow,
32 Symbol(char),
33}
34
35impl TokenKind {
36 fn label(&self) -> String {
37 match self {
38 Self::Ident(value) => format!("identifier `{value}`"),
39 Self::String(_) => "string literal".to_owned(),
40 Self::Number(_) => "number literal".to_owned(),
41 Self::Arrow => "`=>`".to_owned(),
42 Self::ThinArrow => "`->`".to_owned(),
43 Self::Symbol(value) => format!("`{value}`"),
44 }
45 }
46}
47
48pub(crate) fn lex(source: &str) -> Lexed {
49 let bytes = source.as_bytes();
50 let mut tokens = Vec::new();
51 let mut diagnostics = Vec::new();
52 let mut comments = Vec::new();
53 let mut index = 0;
54
55 while index < bytes.len() {
56 let byte = bytes[index];
57 if byte.is_ascii_whitespace() {
58 index += 1;
59 continue;
60 }
61
62 if byte == b'#' {
63 let end = skip_line(bytes, index + 1);
64 comments.push(Comment {
65 marker: CommentMarker::Hash,
66 text: source[index + 1..end].trim().to_owned(),
67 span: SourceSpan { start: index, end },
68 });
69 index = end;
70 continue;
71 }
72
73 if byte == b'/' && bytes.get(index + 1) == Some(&b'/') {
74 let end = skip_line(bytes, index + 2);
75 comments.push(Comment {
76 marker: CommentMarker::Slash,
77 text: source[index + 2..end].trim().to_owned(),
78 span: SourceSpan { start: index, end },
79 });
80 index = end;
81 continue;
82 }
83
84 if is_ident_start(byte) {
85 let start = index;
86 index += 1;
87 while index < bytes.len() && is_ident_continue(bytes[index]) {
88 index += 1;
89 }
90 tokens.push(Token {
91 kind: TokenKind::Ident(source[start..index].to_owned()),
92 span: SourceSpan { start, end: index },
93 });
94 continue;
95 }
96
97 if byte.is_ascii_digit() {
98 let start = index;
99 index += 1;
100 while index < bytes.len() && bytes[index].is_ascii_digit() {
101 index += 1;
102 }
103 tokens.push(Token {
104 kind: TokenKind::Number(source[start..index].to_owned()),
105 span: SourceSpan { start, end: index },
106 });
107 continue;
108 }
109
110 if byte == b'"' {
111 let (token, next, diagnostic) = lex_string(source, index);
112 tokens.push(token);
113 if let Some(diagnostic) = diagnostic {
114 diagnostics.push(diagnostic);
115 }
116 index = next;
117 continue;
118 }
119
120 if byte == b'=' && bytes.get(index + 1) == Some(&b'>') {
121 tokens.push(Token {
122 kind: TokenKind::Arrow,
123 span: SourceSpan {
124 start: index,
125 end: index + 2,
126 },
127 });
128 index += 2;
129 continue;
130 }
131
132 if byte == b'=' && bytes.get(index + 1) == Some(&b'=') {
133 index += 2;
134 continue;
135 }
136
137 if byte == b'!' && bytes.get(index + 1) == Some(&b'=') {
138 index += 2;
139 continue;
140 }
141
142 if matches!(byte, b'<' | b'>') && bytes.get(index + 1) == Some(&b'=') {
143 index += 2;
144 continue;
145 }
146
147 if matches!(byte, b'&' | b'|') && bytes.get(index + 1) == Some(&byte) {
148 index += 2;
149 continue;
150 }
151
152 if byte == b'-' && bytes.get(index + 1) == Some(&b'>') {
153 tokens.push(Token {
154 kind: TokenKind::ThinArrow,
155 span: SourceSpan {
156 start: index,
157 end: index + 2,
158 },
159 });
160 index += 2;
161 continue;
162 }
163
164 if matches!(byte, b'*' | b'/' | b'-') {
168 index += 1;
169 continue;
170 }
171
172 if b"{}[]()<>,?|.+!:@".contains(&byte) {
173 tokens.push(Token {
174 kind: TokenKind::Symbol(byte as char),
175 span: SourceSpan {
176 start: index,
177 end: index + 1,
178 },
179 });
180 index += 1;
181 continue;
182 }
183
184 diagnostics.push(Diagnostic {
185 related: Vec::new(),
186 span: SourceSpan {
187 start: index,
188 end: index + 1,
189 },
190 message: format!("unexpected character `{}`", byte as char),
191 suggestion: None,
192 });
193 index += 1;
194 }
195
196 Lexed {
197 tokens,
198 diagnostics,
199 comments,
200 }
201}
202
203pub fn lex_comments(source: &str) -> Vec<Comment> {
207 lex(source).comments
208}
209
210pub fn string_and_comment_spans(source: &str) -> Vec<SourceSpan> {
215 let lexed = lex(source);
216 let mut spans: Vec<SourceSpan> = lexed
217 .tokens
218 .iter()
219 .filter(|token| matches!(token.kind, TokenKind::String(_)))
220 .map(|token| token.span)
221 .collect();
222 spans.extend(lexed.comments.iter().map(|comment| comment.span));
223 spans
224}
225
226pub(crate) fn skip_line(bytes: &[u8], mut index: usize) -> usize {
227 while index < bytes.len() && bytes[index] != b'\n' {
228 index += 1;
229 }
230 index
231}
232
233pub(crate) fn is_ident_start(byte: u8) -> bool {
234 byte.is_ascii_alphabetic() || byte == b'_'
235}
236
237pub(crate) fn is_ident_continue(byte: u8) -> bool {
238 is_ident_start(byte) || byte.is_ascii_digit() || byte == b'-'
239}
240
241pub(crate) fn lex_string(source: &str, start: usize) -> (Token, usize, Option<Diagnostic>) {
242 let bytes = source.as_bytes();
243 let triple = bytes.get(start..start + 3) == Some(b"\"\"\"");
244 let content_start = if triple { start + 3 } else { start + 1 };
245 let mut index = content_start;
246
247 while index < bytes.len() {
248 if triple && bytes.get(index..index + 3) == Some(b"\"\"\"") {
249 let end = index + 3;
250 return (
251 Token {
252 kind: TokenKind::String(source[content_start..index].to_owned()),
253 span: SourceSpan { start, end },
254 },
255 end,
256 None,
257 );
258 }
259
260 if !triple && bytes[index] == b'"' {
261 let end = index + 1;
262 return (
263 Token {
264 kind: TokenKind::String(source[content_start..index].to_owned()),
265 span: SourceSpan { start, end },
266 },
267 end,
268 None,
269 );
270 }
271
272 if !triple && bytes[index] == b'\\' && index + 1 < bytes.len() {
273 index += 2;
274 } else {
275 index += 1;
276 }
277 }
278
279 (
280 Token {
281 kind: TokenKind::String(source[content_start..].to_owned()),
282 span: SourceSpan {
283 start,
284 end: source.len(),
285 },
286 },
287 source.len(),
288 Some(Diagnostic {
289 related: Vec::new(),
290 span: SourceSpan {
291 start,
292 end: source.len(),
293 },
294 message: "unterminated string literal".to_owned(),
295 suggestion: Some("close the string literal".to_owned()),
296 }),
297 )
298}
299
300#[derive(Clone, Copy, Debug)]
305#[allow(dead_code)]
306pub(crate) enum ClauseKind {
307 Identifier,
308 Expression,
309 Duration,
310 Glob,
311 Schema,
312 Scalar,
313 Flag,
314}
315
316#[derive(Clone, Copy, Debug)]
322#[allow(dead_code)]
323pub(crate) enum DeclAstKind {
324 Tracker,
325 Channel,
326 Counter,
327 Lease,
328 Ledger,
329 MemoryPool,
330 FileStore,
331 Stream,
332 Credential,
333}
334
335#[derive(Clone, Copy, Debug)]
350pub(crate) struct ClauseSpec {
351 pub(crate) name: &'static str,
352 pub(crate) words: &'static [&'static str],
353 pub(crate) connective: Option<&'static str>,
354 pub(crate) kind: ClauseKind,
355 pub(crate) list: bool,
356 unknown_hint: &'static str,
357}
358
359#[derive(Clone, Copy, Debug)]
364#[allow(dead_code)]
365pub(crate) struct DeclarationBlockSpec {
366 pub(crate) keyword: &'static str,
367 pub(crate) keyword_words: &'static [&'static str],
368 ast_kind: DeclAstKind,
369 pub(crate) clauses: &'static [ClauseSpec],
370}
371
372include!(concat!(env!("OUT_DIR"), "/declaration_block_grammar.rs"));
379
380#[derive(Clone, Debug)]
388pub(crate) enum ClauseValue {
389 Ident(Ident),
390 Idents(Vec<Ident>),
391 Duration(u64),
392 Number(u32),
393 Str(StringLiteral),
394 Globs(Vec<String>),
395 Flag,
396 Missing,
397}
398
399pub(crate) struct ClauseBag {
406 records: Vec<(&'static str, SourceSpan, ClauseValue)>,
407}
408
409impl ClauseBag {
410 fn new() -> Self {
411 ClauseBag {
412 records: Vec::new(),
413 }
414 }
415
416 fn record(&mut self, name: &'static str, first_word_span: SourceSpan, value: ClauseValue) {
417 self.records.push((name, first_word_span, value));
418 }
419
420 fn get(&self, name: &str) -> Option<&(&'static str, SourceSpan, ClauseValue)> {
421 self.records
422 .iter()
423 .rev()
424 .find(|(clause, _, _)| *clause == name)
425 }
426
427 fn ident(&self, name: &str) -> Option<Ident> {
428 match self.get(name) {
429 Some((_, _, ClauseValue::Ident(ident))) => Some(ident.clone()),
430 _ => None,
431 }
432 }
433
434 fn idents(&self, name: &str) -> Option<Vec<Ident>> {
435 match self.get(name) {
436 Some((_, _, ClauseValue::Idents(idents))) => Some(idents.clone()),
437 _ => None,
438 }
439 }
440
441 fn duration(&self, name: &str) -> Option<u64> {
442 match self.get(name) {
443 Some((_, _, ClauseValue::Duration(seconds))) => Some(*seconds),
444 _ => None,
445 }
446 }
447
448 fn number(&self, name: &str) -> Option<u32> {
449 match self.get(name) {
450 Some((_, _, ClauseValue::Number(value))) => Some(*value),
451 _ => None,
452 }
453 }
454
455 fn text(&self, name: &str) -> Option<String> {
456 self.text_literal(name).map(|literal| literal.value)
457 }
458
459 fn text_literal(&self, name: &str) -> Option<StringLiteral> {
460 match self.get(name) {
461 Some((_, _, ClauseValue::Str(literal))) => Some(literal.clone()),
462 _ => None,
463 }
464 }
465
466 fn globs(&self, name: &str) -> Vec<String> {
467 match self.get(name) {
468 Some((_, _, ClauseValue::Globs(values))) => values.clone(),
469 _ => Vec::new(),
470 }
471 }
472
473 fn flag(&self, name: &str) -> bool {
474 matches!(self.get(name), Some((_, _, ClauseValue::Flag)))
475 }
476
477 fn span(&self, name: &str) -> Option<SourceSpan> {
478 self.get(name).map(|(_, span, _)| *span)
479 }
480}
481
482pub(crate) struct Parser<'a> {
483 pub(crate) source: &'a str,
484 pub(crate) tokens: Vec<Token>,
485 pub(crate) pos: usize,
486 pub(crate) diagnostics: Vec<Diagnostic>,
487 pub(crate) pending_contract_classes: Vec<ClassDecl>,
490}
491
492pub(crate) struct ParsedWorkflow {
493 decl: WorkflowDecl,
494 explicit_body: bool,
495}
496
497impl Parser<'_> {
498 fn parse_program(&mut self) -> Program {
499 let mut workflow = None;
500 let mut workflow_tags = Vec::new();
501 let mut workflow_description = None;
502 let mut explicit_workflow_body = false;
503 let mut workflows = Vec::new();
504 let mut patterns = Vec::new();
505 let mut items = Vec::new();
506 let mut pending_tags = Vec::new();
507 let mut pending_description = None;
508
509 while !self.is_at_end() {
510 if self.at_symbol('@') {
511 if let Some(tag) = self.parse_tag() {
512 pending_tags.push(tag);
513 }
514 } else if self.at_ident("description") {
515 self.parse_pending_description(&mut pending_description);
516 } else if self.at_ident("workflow") {
517 if let Some(parsed_workflow) = self.parse_workflow(
518 std::mem::take(&mut pending_tags),
519 pending_description.take(),
520 ) {
521 if parsed_workflow.explicit_body {
522 workflows.push(parsed_workflow.decl);
523 } else {
524 if workflow.is_some() {
525 self.diagnostics.push(Diagnostic { related: Vec::new(),
526 span: parsed_workflow.decl.name.span,
527 message: "multiple implicit workflow headers are not supported"
528 .to_owned(),
529 suggestion: Some(
530 "use explicit `workflow Name { ... }` declarations with `--root`"
531 .to_owned(),
532 ),
533 });
534 }
535 workflow_tags = parsed_workflow.decl.tags;
536 workflow_description = parsed_workflow.decl.description;
537 items.extend(parsed_workflow.decl.items);
541 workflow = Some(parsed_workflow.decl.name);
542 explicit_workflow_body = false;
543 }
544 }
545 } else if self.at_ident("pattern") {
546 self.reject_pending_tags(&mut pending_tags, "pattern");
547 self.reject_pending_description(&mut pending_description, "pattern");
548 if let Some(pattern) = self.parse_pattern() {
549 patterns.push(pattern);
550 }
551 } else if let Some(item) =
552 self.parse_declaration_item(&mut pending_tags, &mut pending_description)
553 {
554 items.push(item);
555 } else if self.reject_gherkin_misuse() {
556 continue;
557 } else {
558 if self.is_at_end() {
559 break;
560 }
561 self.unexpected("top-level declaration");
562 if !self.is_at_end() {
563 self.advance();
564 }
565 }
566 }
567
568 items.extend(
571 std::mem::take(&mut self.pending_contract_classes)
572 .into_iter()
573 .map(Item::Class),
574 );
575
576 Program {
577 workflow,
578 workflow_tags,
579 workflow_description,
580 explicit_workflow_body,
581 workflows,
582 patterns,
583 items,
584 }
585 }
586
587 fn parse_workflow(
588 &mut self,
589 tags: Vec<TagDecl>,
590 description: Option<StringLiteral>,
591 ) -> Option<ParsedWorkflow> {
592 let start = self.expect_keyword("workflow")?.span.start;
593 let name = self.expect_ident("workflow name")?;
594 let mut explicit_body = false;
595 let mut items = Vec::new();
596 let mut end = name.span.end;
597 if self.at_symbol('(') {
603 if let Some((contracts, signature_end)) = self.parse_compact_contract_signature() {
604 end = signature_end;
605 items.extend(contracts.into_iter().map(Item::WorkflowContract));
606 }
607 }
608 if self.at_symbol('{') {
609 explicit_body = true;
610 self.expect_symbol('{')?;
611 let mut pending_tags = Vec::new();
612 let mut pending_description = None;
613 while !self.is_at_end() && !self.at_symbol('}') {
614 if self.at_symbol('@') {
615 if let Some(tag) = self.parse_tag() {
616 pending_tags.push(tag);
617 }
618 continue;
619 }
620 if self.at_ident("description") {
621 self.parse_pending_description(&mut pending_description);
622 continue;
623 }
624 if self.at_ident("workflow") || self.at_ident("pattern") {
625 self.reject_pending_tags(&mut pending_tags, "workflow body declaration");
626 self.reject_pending_description(
627 &mut pending_description,
628 "workflow body declaration",
629 );
630 self.unexpected("workflow body declaration");
631 self.advance();
632 continue;
633 }
634 if let Some(item) =
635 self.parse_declaration_item(&mut pending_tags, &mut pending_description)
636 {
637 items.push(item);
638 } else if self.reject_gherkin_misuse() {
639 continue;
640 } else {
641 if self.is_at_end() {
642 break;
643 }
644 self.reject_pending_tags(&mut pending_tags, "workflow body declaration");
645 self.reject_pending_description(
646 &mut pending_description,
647 "workflow body declaration",
648 );
649 self.unexpected("workflow body declaration");
650 if !self.is_at_end() {
651 self.advance();
652 }
653 }
654 }
655 if let Some(close) = self.expect_symbol('}') {
656 end = close.span.end;
657 }
658 }
659 items.extend(
664 std::mem::take(&mut self.pending_contract_classes)
665 .into_iter()
666 .map(Item::Class),
667 );
668 Some(ParsedWorkflow {
669 decl: WorkflowDecl {
670 name,
671 tags,
672 description,
673 items,
674 span: SourceSpan { start, end },
675 },
676 explicit_body,
677 })
678 }
679
680 fn parse_compact_contract_signature(&mut self) -> Option<(Vec<WorkflowContractDecl>, usize)> {
686 self.expect_symbol('(')?;
687 let mut contracts = Vec::new();
688 while !self.is_at_end() && !self.at_symbol(')') {
689 let name = self.expect_ident("workflow input name")?;
690 self.expect_symbol(':')?;
691 let ty = self.parse_type()?;
692 let span = name.span.join(ty.span());
693 contracts.push(WorkflowContractDecl {
694 kind: WorkflowContractKind::Input,
695 name,
696 ty,
697 span,
698 });
699 if self.at_symbol(',') {
700 self.advance();
701 } else if !self.at_symbol(')') {
702 self.unexpected("`,` or `)`");
703 while !self.is_at_end() && !self.at_symbol(')') && !self.at_symbol(',') {
704 self.advance();
705 }
706 }
707 }
708 self.expect_symbol(')')?;
709 self.expect_thin_arrow()?;
710 let output_ty = self.parse_type()?;
711 let output_span = output_ty.span();
712 let mut end = output_span.end;
713 contracts.push(WorkflowContractDecl {
714 kind: WorkflowContractKind::Output,
715 name: Ident {
716 name: "result".to_owned(),
717 span: output_span,
718 },
719 ty: output_ty,
720 span: output_span,
721 });
722 if self.at_symbol('!') {
723 self.advance();
724 let failure_ty = self.parse_type()?;
725 let failure_span = failure_ty.span();
726 end = failure_span.end;
727 contracts.push(WorkflowContractDecl {
728 kind: WorkflowContractKind::Failure,
729 name: Ident {
730 name: "error".to_owned(),
731 span: failure_span,
732 },
733 ty: failure_ty,
734 span: failure_span,
735 });
736 }
737 Some((contracts, end))
738 }
739
740 fn parse_tag(&mut self) -> Option<TagDecl> {
741 let at = self.expect_symbol('@')?;
742 let name_start = at.span.end;
743 let mut name_end = name_start;
744 for (offset, ch) in self.source[name_start..].char_indices() {
745 if ch.is_whitespace() {
746 break;
747 }
748 name_end = name_start + offset + ch.len_utf8();
749 }
750 let name = self.source[name_start..name_end].to_owned();
751 while !self.is_at_end() && self.peek().is_some_and(|token| token.span.start < name_end) {
752 self.advance();
753 }
754 let span = SourceSpan {
755 start: at.span.start,
756 end: name_end,
757 };
758 if name.is_empty() {
759 self.diagnostics.push(Diagnostic {
760 related: Vec::new(),
761 span,
762 message: "tag is missing a name".to_owned(),
763 suggestion: Some("write a tag such as `@fixture`".to_owned()),
764 });
765 return None;
766 }
767 if !name
768 .chars()
769 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | ':' | '.'))
770 {
771 self.diagnostics.push(Diagnostic {
772 related: Vec::new(),
773 span,
774 message: format!("tag `@{name}` contains unsupported characters"),
775 suggestion: Some(
776 "use letters, digits, `_`, `-`, `.`, or `:` in tag names".to_owned(),
777 ),
778 });
779 return None;
780 }
781 Some(TagDecl { name, span })
782 }
783
784 fn reject_pending_tags(&mut self, pending_tags: &mut Vec<TagDecl>, target: &str) {
785 for tag in pending_tags.drain(..) {
786 self.diagnostics.push(Diagnostic {
787 related: Vec::new(),
788 span: tag.span,
789 message: format!("tag `@{}` cannot be attached to {target}", tag.name),
790 suggestion: Some(
791 "place tags on workflows, matrices, assertions, or rules".to_owned(),
792 ),
793 });
794 }
795 }
796
797 fn parse_pending_description(&mut self, pending_description: &mut Option<StringLiteral>) {
798 let Some(description) = self.parse_description() else {
799 return;
800 };
801 if let Some(previous) = pending_description.replace(description) {
802 self.diagnostics.push(Diagnostic { related: Vec::new(),
803 span: previous.span,
804 message: "description is not attached to a declaration".to_owned(),
805 suggestion: Some(
806 "place only one `description \"...\"` immediately before the target declaration"
807 .to_owned(),
808 ),
809 });
810 }
811 }
812
813 fn parse_description(&mut self) -> Option<StringLiteral> {
814 let description = self.expect_keyword("description")?;
815 let Some(value) = self.expect_string("description string") else {
816 return Some(StringLiteral {
817 value: String::new(),
818 span: description.span,
819 });
820 };
821 Some(value)
822 }
823
824 fn reject_pending_description(
825 &mut self,
826 pending_description: &mut Option<StringLiteral>,
827 target: &str,
828 ) {
829 if let Some(description) = pending_description.take() {
830 self.diagnostics.push(Diagnostic {
831 related: Vec::new(),
832 span: description.span,
833 message: format!("description cannot be attached to {target}"),
834 suggestion: Some(
835 "place descriptions on workflows, matrices, assertions, or rules".to_owned(),
836 ),
837 });
838 }
839 }
840
841 fn reject_gherkin_misuse(&mut self) -> bool {
842 let Some(token) = self.peek() else {
843 return false;
844 };
845 let TokenKind::Ident(keyword) = &token.kind else {
846 return false;
847 };
848 if !is_gherkin_keyword(keyword) {
849 return false;
850 }
851 let span = token.span;
852 self.diagnostics.push(Diagnostic { related: Vec::new(),
853 span,
854 message: format!(
855 "Gherkin keyword `{keyword}` is not WhippleScript workflow syntax"
856 ),
857 suggestion: Some(
858 "use `workflow`, `table`, `rule ... when ... => { ... }`, and `assert` instead of free-text Given/When/Then steps"
859 .to_owned(),
860 ),
861 });
862 self.advance_to_line_end(span.start);
863 true
864 }
865
866 fn advance_to_line_end(&mut self, line_start: usize) {
867 let line_end = self.source[line_start..]
868 .find('\n')
869 .map(|offset| line_start + offset)
870 .unwrap_or(self.source.len());
871 while self.peek().is_some_and(|token| token.span.start < line_end) {
872 self.advance();
873 }
874 }
875
876 fn parse_declaration_item(
877 &mut self,
878 pending_tags: &mut Vec<TagDecl>,
879 pending_description: &mut Option<StringLiteral>,
880 ) -> Option<Item> {
881 if let Some(spec) = self.declaration_block_spec_at() {
888 self.reject_pending_tags(pending_tags, spec.keyword);
889 self.reject_pending_description(pending_description, spec.keyword);
890 return self.parse_declaration_block(spec);
891 }
892 if self.at_ident("include") {
893 self.reject_pending_tags(pending_tags, "include");
894 self.reject_pending_description(pending_description, "include");
895 self.parse_include().map(Item::Include)
896 } else if self.at_ident("use") {
897 self.reject_pending_tags(pending_tags, "use");
898 self.reject_pending_description(pending_description, "use");
899 self.parse_use().map(Item::Use)
900 } else if self.at_ident("pattern") {
901 self.reject_pending_tags(pending_tags, "pattern");
902 self.reject_pending_description(pending_description, "pattern");
903 self.parse_pattern().map(Item::Pattern)
904 } else if self.at_ident("apply") {
905 self.reject_pending_tags(pending_tags, "apply");
906 self.reject_pending_description(pending_description, "apply");
907 self.parse_apply().map(Item::Apply)
908 } else if self.at_ident("input") || self.at_ident("output") || self.at_ident("failure") {
909 self.reject_pending_tags(pending_tags, "workflow contract");
910 self.reject_pending_description(pending_description, "workflow contract");
911 self.parse_workflow_contract().map(Item::WorkflowContract)
912 } else if self.at_ident("flow") {
913 let span = self
917 .peek()
918 .map(|token| token.span)
919 .unwrap_or(SourceSpan { start: 0, end: 0 });
920 self.diagnostics.push(Diagnostic {
921 related: Vec::new(),
922 span,
923 message: "the `flow` declaration was removed".to_owned(),
924 suggestion: Some(
925 "write a `rule` and chain sequential steps with `then <binding> <- <effect>`"
926 .to_owned(),
927 ),
928 });
929 let mut depth = 0usize;
933 while !self.is_at_end() {
934 let token = self.advance();
935 match &token.kind {
936 TokenKind::Symbol('{') => depth += 1,
937 TokenKind::Symbol('}') => {
938 depth = depth.saturating_sub(1);
939 if depth == 0 {
940 break;
941 }
942 }
943 _ => {}
944 }
945 }
946 None
947 } else if self.at_ident("action") {
948 self.reject_pending_tags(pending_tags, "action");
949 self.reject_pending_description(pending_description, "action");
950 self.parse_action().map(Item::Action)
951 } else if self.at_ident("harness") {
952 self.reject_pending_tags(pending_tags, "harness");
953 self.reject_pending_description(pending_description, "harness");
954 self.parse_harness().map(Item::Harness)
955 } else if self.at_ident("agent") {
956 self.reject_pending_tags(pending_tags, "agent");
957 self.reject_pending_description(pending_description, "agent");
958 self.parse_agent().map(Item::Agent)
959 } else if self.at_ident("enum") {
960 self.reject_pending_tags(pending_tags, "enum");
961 self.reject_pending_description(pending_description, "enum");
962 self.parse_enum().map(Item::Enum)
963 } else if self.at_ident("signal") {
964 self.reject_pending_tags(pending_tags, "signal");
965 self.reject_pending_description(pending_description, "signal");
966 self.parse_event().map(Item::Event)
967 } else if self.at_ident("gauge") {
968 self.reject_pending_tags(pending_tags, "gauge");
969 self.reject_pending_description(pending_description, "gauge");
970 self.parse_gauge().map(Item::Gauge)
971 } else if self.at_ident("campaign") {
972 self.reject_pending_tags(pending_tags, "campaign");
973 self.reject_pending_description(pending_description, "campaign");
974 self.parse_campaign().map(Item::Campaign)
975 } else if self.at_ident("mark") {
976 self.reject_pending_tags(pending_tags, "mark");
977 self.reject_pending_description(pending_description, "mark");
978 self.parse_mark().map(Item::Mark)
979 } else if self.at_ident("source") {
980 self.reject_pending_tags(pending_tags, "source");
981 self.reject_pending_description(pending_description, "source");
982 self.parse_source()
983 .map(|source| Item::Source(Box::new(source)))
984 } else if self.at_ident("test") {
985 self.reject_pending_tags(pending_tags, "test");
986 self.reject_pending_description(pending_description, "test");
987 self.parse_test().map(Item::Test)
988 } else if self.at_ident("class") {
989 self.reject_pending_tags(pending_tags, "class");
990 self.reject_pending_description(pending_description, "class");
991 self.parse_class().map(Item::Class)
992 } else if self.at_ident("table") {
993 self.parse_table(std::mem::take(pending_tags), pending_description.take())
994 .map(Item::Table)
995 } else if self.at_ident("coerce") {
996 self.reject_pending_tags(pending_tags, "coerce");
997 self.reject_pending_description(pending_description, "coerce");
998 self.parse_coerce().map(Item::Coerce)
999 } else if self.at_ident("assert") {
1000 self.parse_assert(std::mem::take(pending_tags), pending_description.take())
1001 .map(Item::Assert)
1002 } else if self.at_ident("rule") {
1003 self.parse_rule(std::mem::take(pending_tags), pending_description.take())
1004 .map(Item::Rule)
1005 } else {
1006 None
1007 }
1008 }
1009
1010 pub(crate) fn declaration_block_spec_at(&self) -> Option<&'static DeclarationBlockSpec> {
1016 let head = match self.peek().map(|token| &token.kind) {
1017 Some(TokenKind::Ident(value)) => value.as_str(),
1018 _ => return None,
1019 };
1020 DECLARATION_BLOCK_GRAMMAR
1021 .iter()
1022 .find(|spec| spec.keyword_words.first() == Some(&head))
1023 }
1024
1025 fn parse_declaration_block(&mut self, spec: &'static DeclarationBlockSpec) -> Option<Item> {
1033 let head = *spec.keyword_words.first()?;
1034 let start = self.expect_keyword(head)?.span.start;
1035 for tail in &spec.keyword_words[1..] {
1036 if !self.consume_ident(tail) {
1037 self.expected(format!("`{tail}` after `{head}`"));
1038 return None;
1039 }
1040 }
1041 let name = self.expect_ident(&format!("{} name", spec.keyword))?;
1042 if !self.at_symbol('{') {
1048 let span = SourceSpan {
1049 start,
1050 end: name.span.end,
1051 };
1052 let bag = ClauseBag::new();
1053 return self.item_from_decl_ast(spec.ast_kind, &bag, name, span);
1054 }
1055 self.expect_symbol('{')?;
1056 let field_label = format!("{} field", spec.keyword);
1057 let mut bag = ClauseBag::new();
1058 while !self.is_at_end() && !self.at_symbol('}') {
1059 let Some(field) = self.expect_ident(&field_label) else {
1060 self.synchronize_to_block_item();
1061 continue;
1062 };
1063 let first_word_span = field.span;
1067 let mut words = vec![field.name];
1068 let matched = loop {
1069 let depth = words.len();
1070 if let Some(clause) = spec.clauses.iter().find(|clause| {
1071 clause.words.len() == depth
1072 && clause
1073 .words
1074 .iter()
1075 .zip(&words)
1076 .all(|(a, b)| *a == b.as_str())
1077 }) {
1078 break Some(clause);
1079 }
1080 let extendable = spec.clauses.iter().any(|clause| {
1081 clause.words.len() > depth
1082 && clause
1083 .words
1084 .iter()
1085 .zip(&words)
1086 .all(|(a, b)| *a == b.as_str())
1087 });
1088 if !extendable {
1089 break None;
1090 }
1091 let Some(next) = self.expect_ident("clause name") else {
1092 break None;
1093 };
1094 words.push(next.name);
1095 };
1096 let Some(clause) = matched else {
1097 let hint = spec.clauses.first().map(|clause| clause.unknown_hint);
1098 self.diagnostics.push(Diagnostic {
1099 related: Vec::new(),
1100 span: first_word_span,
1101 message: format!("unknown {} field `{}`", spec.keyword, words.join(" ")),
1102 suggestion: hint.map(str::to_owned),
1103 });
1104 self.synchronize_to_block_item();
1105 continue;
1106 };
1107 if let Some(connective) = clause.connective {
1110 if !self.consume_ident(connective) {
1111 self.diagnostics.push(Diagnostic {
1112 related: Vec::new(),
1113 span: first_word_span,
1114 message: format!("expected `{connective}` after `{}`", clause.name),
1115 suggestion: Some(format!("write `{} {connective} <field>`", clause.name)),
1116 });
1117 self.synchronize_to_block_item();
1118 continue;
1119 }
1120 }
1121 let value = self.parse_clause_value(clause);
1122 bag.record(clause.name, first_word_span, value);
1123 }
1124 let close = self.expect_symbol('}')?;
1125 let span = SourceSpan {
1126 start,
1127 end: close.span.end,
1128 };
1129 self.item_from_decl_ast(spec.ast_kind, &bag, name, span)
1130 }
1131
1132 fn parse_clause_value(&mut self, clause: &ClauseSpec) -> ClauseValue {
1136 match clause.kind {
1137 ClauseKind::Identifier if clause.list => self
1142 .parse_ident_list()
1143 .map_or(ClauseValue::Missing, |(idents, _)| {
1144 ClauseValue::Idents(idents)
1145 }),
1146 ClauseKind::Identifier | ClauseKind::Schema => self
1147 .expect_ident(&format!("{} value", clause.name))
1148 .map_or(ClauseValue::Missing, ClauseValue::Ident),
1149 ClauseKind::Duration => self
1150 .parse_decl_duration_seconds(&format!("{} duration", clause.name))
1151 .map_or(ClauseValue::Missing, ClauseValue::Duration),
1152 ClauseKind::Scalar => match self.peek().map(|token| &token.kind) {
1153 Some(TokenKind::String(_)) => self
1154 .expect_string(&format!("{} value", clause.name))
1155 .map_or(ClauseValue::Missing, ClauseValue::Str),
1156 _ => self
1157 .expect_u32(&format!("{} value", clause.name))
1158 .map_or(ClauseValue::Missing, |(value, _)| {
1159 ClauseValue::Number(value)
1160 }),
1161 },
1162 ClauseKind::Glob if clause.list => {
1163 let globs = self
1167 .parse_string_list()
1168 .map(|(literals, _)| literals.into_iter().map(|l| l.value).collect())
1169 .unwrap_or_default();
1170 ClauseValue::Globs(globs)
1171 }
1172 ClauseKind::Glob => self
1173 .expect_string(&format!("{} value", clause.name))
1174 .map_or(ClauseValue::Missing, ClauseValue::Str),
1175 ClauseKind::Flag => ClauseValue::Flag,
1176 ClauseKind::Expression => ClauseValue::Missing,
1178 }
1179 }
1180
1181 fn item_from_decl_ast(
1187 &mut self,
1188 ast_kind: DeclAstKind,
1189 bag: &ClauseBag,
1190 name: Ident,
1191 span: SourceSpan,
1192 ) -> Option<Item> {
1193 match ast_kind {
1194 DeclAstKind::Tracker => {
1195 let provider = bag.ident("provider").unwrap_or_else(|| Ident {
1198 name: "builtin".to_owned(),
1199 span,
1200 });
1201 Some(Item::Tracker(TrackerDecl {
1202 name,
1203 provider,
1204 span,
1205 }))
1206 }
1207 DeclAstKind::Channel => {
1208 let provider = bag.ident("provider").unwrap_or_else(|| Ident {
1210 name: "local".to_owned(),
1211 span,
1212 });
1213 Some(Item::Channel(ChannelDecl {
1214 name,
1215 provider,
1216 workspace: bag.ident("workspace"),
1217 destination: bag.text_literal("destination"),
1218 span,
1219 }))
1220 }
1221 DeclAstKind::Credential => {
1222 let Some(kind) = bag.ident("kind") else {
1223 self.diagnostics.push(Diagnostic {
1224 related: Vec::new(),
1225 span,
1226 message: format!("credential `{}` must declare its kind", name.name),
1227 suggestion: Some(
1228 "add `kind <kind>` inside the credential block (bearer | basic | \
1229 raw | hmac_sha256 | ed25519 | aws_sigv4 | jwt_rs256)"
1230 .to_owned(),
1231 ),
1232 });
1233 return None;
1234 };
1235 Some(Item::Credential(CredentialDecl { name, kind, span }))
1236 }
1237 DeclAstKind::Stream => {
1238 let Some(members) = bag.idents("members").filter(|idents| !idents.is_empty())
1239 else {
1240 self.diagnostics.push(Diagnostic {
1241 related: Vec::new(),
1242 span,
1243 message: format!("stream `{}` must declare its members", name.name),
1244 suggestion: Some(
1245 "a stream is a declared collaboration: name its member \
1246 agents with `members [<agent>, ...]`"
1247 .to_owned(),
1248 ),
1249 });
1250 return None;
1251 };
1252 Some(Item::Stream(StreamDecl {
1253 name,
1254 members,
1255 staleness_seconds: bag.duration("staleness"),
1256 span,
1257 }))
1258 }
1259 DeclAstKind::Counter => {
1260 let key_type = bag.ident("key");
1261 let cap = bag.number("cap").map(i64::from);
1262 let reset = bag.ident("reset").map(|period| {
1265 if !matches!(
1266 period.name.as_str(),
1267 "hourly" | "daily" | "weekly" | "monthly"
1268 ) {
1269 self.diagnostics.push(Diagnostic {
1270 related: Vec::new(),
1271 span: period.span,
1272 message: format!("unknown reset period `{}`", period.name),
1273 suggestion: Some(
1274 "use `hourly`, `daily`, `weekly`, or `monthly`".to_owned(),
1275 ),
1276 });
1277 }
1278 period.name
1279 });
1280 let shared = bag.flag("shared");
1281 let (Some(key_type), Some(cap), Some(reset)) = (key_type, cap, reset) else {
1282 self.diagnostics.push(Diagnostic {
1283 related: Vec::new(),
1284 span,
1285 message: format!(
1286 "counter `{}` must declare `key`, `cap`, and `reset`",
1287 name.name
1288 ),
1289 suggestion: Some(
1290 "every counter is bounded: declare all three fields".to_owned(),
1291 ),
1292 });
1293 return None;
1294 };
1295 Some(Item::Counter(CounterDecl {
1296 name,
1297 key_type,
1298 cap,
1299 reset,
1300 timezone: bag.text("timezone"),
1301 shared,
1302 span,
1303 }))
1304 }
1305 DeclAstKind::Lease => {
1306 let key_type = bag.ident("key");
1307 let slots = bag.number("slots").unwrap_or(1);
1308 let ttl_seconds = bag.duration("ttl");
1309 let shared = bag.flag("shared");
1310 let (Some(key_type), Some(ttl_seconds)) = (key_type, ttl_seconds) else {
1311 self.diagnostics.push(Diagnostic {
1312 related: Vec::new(),
1313 span,
1314 message: format!(
1315 "lease `{}` must declare a `key` type and a `ttl` backstop",
1316 name.name
1317 ),
1318 suggestion: Some(
1319 "every lease is bounded: declare `key <Type>` and `ttl <duration>`"
1320 .to_owned(),
1321 ),
1322 });
1323 return None;
1324 };
1325 Some(Item::Lease(LeaseDecl {
1326 name,
1327 key_type,
1328 slots,
1329 ttl_seconds,
1330 shared,
1331 span,
1332 }))
1333 }
1334 DeclAstKind::Ledger => {
1335 let entry_schema = bag.ident("entry");
1336 let partition_field = bag.ident("partition");
1337 let retain_seconds = bag.duration("retain");
1338 let shared = bag.flag("shared");
1339 let (Some(entry_schema), Some(partition_field), Some(retain_seconds)) =
1340 (entry_schema, partition_field, retain_seconds)
1341 else {
1342 self.diagnostics.push(Diagnostic {
1343 related: Vec::new(),
1344 span,
1345 message: format!(
1346 "ledger `{}` must declare `entry`, `partition by`, and `retain`",
1347 name.name
1348 ),
1349 suggestion: Some(
1350 "every ledger is bounded and partitioned: declare all three fields"
1351 .to_owned(),
1352 ),
1353 });
1354 return None;
1355 };
1356 Some(Item::Ledger(LedgerDecl {
1357 name,
1358 entry_schema,
1359 partition_field,
1360 retain_seconds,
1361 shared,
1362 span,
1363 }))
1364 }
1365 DeclAstKind::FileStore => {
1366 let root_span = bag.span("root");
1367 let read_span = bag.span("allow read");
1368 let write_span = bag.span("allow write");
1369 let provider_span = bag.span("provider");
1370 let read_globs = bag.globs("allow read");
1371 let write_globs = bag.globs("allow write");
1372 let provider = bag.ident("provider");
1373 let Some(root) = bag.text("root") else {
1374 self.diagnostics.push(Diagnostic {
1375 related: Vec::new(),
1376 span,
1377 message: format!("file store `{}` is missing a root", name.name),
1378 suggestion: Some(
1379 "add `root \"<dir>\"` inside the file store block".to_owned(),
1380 ),
1381 });
1382 return None;
1383 };
1384 Some(Item::FileStore(FileStoreDecl {
1385 name,
1386 root,
1387 read_globs,
1388 write_globs,
1389 provider,
1390 root_span,
1391 read_span,
1392 write_span,
1393 provider_span,
1394 span,
1395 }))
1396 }
1397 DeclAstKind::MemoryPool => {
1398 let context_limit = bag.number("context limit").map(u64::from);
1399 let context_limit_span = context_limit.and(bag.span("context limit"));
1402 Some(Item::MemoryPool(MemoryPoolDecl {
1403 name,
1404 context_limit,
1405 context_limit_span,
1406 span,
1407 }))
1408 }
1409 }
1410 }
1411
1412 fn parse_pattern(&mut self) -> Option<PatternDecl> {
1413 let start = self.expect_keyword("pattern")?.span.start;
1414 let name = self.expect_ident("pattern name")?;
1415 let type_params = self.parse_type_param_list().unwrap_or_default();
1416 let open = self.expect_symbol('{')?;
1417 let mut items = Vec::new();
1418 let mut pending_tags = Vec::new();
1419 let mut pending_description = None;
1420 while !self.is_at_end() && !self.at_symbol('}') {
1421 if self.at_symbol('@') {
1422 if let Some(tag) = self.parse_tag() {
1423 pending_tags.push(tag);
1424 }
1425 continue;
1426 }
1427 if self.at_ident("description") {
1428 self.parse_pending_description(&mut pending_description);
1429 continue;
1430 }
1431 if self.at_ident("workflow") || self.at_ident("pattern") {
1432 self.reject_pending_tags(&mut pending_tags, "pattern body declaration");
1433 self.reject_pending_description(
1434 &mut pending_description,
1435 "pattern body declaration",
1436 );
1437 self.unexpected("pattern body declaration");
1438 self.advance();
1439 continue;
1440 }
1441 if let Some(item) =
1442 self.parse_declaration_item(&mut pending_tags, &mut pending_description)
1443 {
1444 items.push(item);
1445 } else if self.reject_gherkin_misuse() {
1446 continue;
1447 } else {
1448 if self.is_at_end() {
1449 break;
1450 }
1451 self.reject_pending_tags(&mut pending_tags, "pattern body declaration");
1452 self.reject_pending_description(
1453 &mut pending_description,
1454 "pattern body declaration",
1455 );
1456 self.unexpected("pattern body declaration");
1457 self.advance();
1458 }
1459 }
1460 let end = self
1461 .expect_symbol('}')
1462 .map(|token| token.span.end)
1463 .unwrap_or(open.span.end);
1464 Some(PatternDecl {
1465 name,
1466 type_params,
1467 items,
1468 span: SourceSpan { start, end },
1469 })
1470 }
1471
1472 fn parse_type_param_list(&mut self) -> Option<Vec<Ident>> {
1473 if !self.at_symbol('<') {
1474 return Some(Vec::new());
1475 }
1476 self.expect_symbol('<')?;
1477 let mut params = Vec::new();
1478 while !self.is_at_end() && !self.at_symbol('>') {
1479 params.push(self.expect_ident("type parameter")?);
1480 if self.at_symbol(',') {
1481 self.advance();
1482 } else if !self.at_symbol('>') {
1483 self.unexpected("`,` or `>`");
1484 while !self.is_at_end() && !self.at_symbol('>') && !self.at_symbol(',') {
1485 self.advance();
1486 }
1487 }
1488 }
1489 self.expect_symbol('>')?;
1490 Some(params)
1491 }
1492
1493 fn parse_type_arg_list(&mut self) -> Option<Vec<TypeSyntax>> {
1494 if !self.at_symbol('<') {
1495 return Some(Vec::new());
1496 }
1497 self.expect_symbol('<')?;
1498 let mut args = Vec::new();
1499 while !self.is_at_end() && !self.at_symbol('>') {
1500 args.push(self.parse_type()?);
1501 if self.at_symbol(',') {
1502 self.advance();
1503 } else if !self.at_symbol('>') {
1504 self.unexpected("`,` or `>`");
1505 while !self.is_at_end() && !self.at_symbol('>') && !self.at_symbol(',') {
1506 self.advance();
1507 }
1508 }
1509 }
1510 self.expect_symbol('>')?;
1511 Some(args)
1512 }
1513
1514 fn parse_apply(&mut self) -> Option<ApplyDecl> {
1515 let start = self.expect_keyword("apply")?.span.start;
1516 let pattern = self.expect_ident("pattern name")?;
1517 let type_args = self.parse_type_arg_list().unwrap_or_default();
1518 self.expect_keyword("as")?;
1519 let alias = self.expect_ident("pattern application alias")?;
1520 let body = self.parse_block_source()?;
1521 let span = SourceSpan {
1522 start,
1523 end: body.span.end,
1524 };
1525 Some(ApplyDecl {
1526 pattern,
1527 type_args,
1528 alias,
1529 body,
1530 span,
1531 })
1532 }
1533
1534 fn parse_include(&mut self) -> Option<IncludeDecl> {
1535 self.expect_keyword("include")?;
1536 Some(IncludeDecl {
1537 path: self.expect_string("include path")?,
1538 })
1539 }
1540
1541 fn parse_workflow_contract(&mut self) -> Option<WorkflowContractDecl> {
1542 let keyword = self.advance().clone();
1543 let kind = match &keyword.kind {
1544 TokenKind::Ident(value) if value == "input" => WorkflowContractKind::Input,
1545 TokenKind::Ident(value) if value == "output" => WorkflowContractKind::Output,
1546 TokenKind::Ident(value) if value == "failure" => WorkflowContractKind::Failure,
1547 _ => return None,
1548 };
1549 let name = self.expect_ident("workflow contract name")?;
1550 if self.at_symbol('{') {
1556 self.advance();
1557 let mut fields = Vec::new();
1558 while !self.is_at_end() && !self.at_symbol('}') {
1559 let Some(field_name) = self.expect_ident("contract payload field name") else {
1560 self.synchronize_to_block_item();
1561 continue;
1562 };
1563 let Some(field_ty) = self.parse_type() else {
1564 self.synchronize_to_block_item();
1565 continue;
1566 };
1567 let field_span = field_name.span.join(field_ty.span());
1568 fields.push(ClassField {
1569 name: field_name,
1570 ty: field_ty,
1571 is_key: false,
1572 presence_condition: None,
1573 span: field_span,
1574 });
1575 }
1576 let close_span = self.peek().map(|token| token.span);
1577 self.expect_symbol('}')?;
1578 let contract_keyword = match kind {
1579 WorkflowContractKind::Input => "input",
1580 WorkflowContractKind::Output => "output",
1581 WorkflowContractKind::Failure => "failure",
1582 };
1583 let class_name = format!("{contract_keyword}.{}", name.name);
1584 let end = close_span.map(|span| span.end).unwrap_or(name.span.end);
1585 let span = SourceSpan {
1586 start: keyword.span.start,
1587 end,
1588 };
1589 self.pending_contract_classes.push(ClassDecl {
1590 name: Ident {
1591 name: class_name.clone(),
1592 span,
1593 },
1594 fields,
1595 span,
1596 });
1597 return Some(WorkflowContractDecl {
1598 kind,
1599 name,
1600 ty: TypeSyntax::Ref {
1601 name: Ident {
1602 name: class_name,
1603 span,
1604 },
1605 },
1606 span,
1607 });
1608 }
1609 let ty = self.parse_type()?;
1610 let span = keyword.span.join(ty.span());
1611 Some(WorkflowContractDecl {
1612 kind,
1613 name,
1614 ty,
1615 span,
1616 })
1617 }
1618
1619 fn parse_use(&mut self) -> Option<UseDecl> {
1620 self.expect_keyword("use")?;
1621 if self.at_ident("plugin") || self.at_ident("skill") {
1622 let removed_kind = self.advance().clone();
1623 let removed_label = match &removed_kind.kind {
1624 TokenKind::Ident(value) => value.as_str(),
1625 _ => "",
1626 };
1627 self.diagnostics.push(Diagnostic { related: Vec::new(),
1628 span: removed_kind.span,
1629 message: format!("`use {removed_label}` is no longer supported"),
1630 suggestion: Some(
1631 "write `use std.memory` for package libraries; attach skills with `agent { skills [...] }`"
1632 .to_owned(),
1633 ),
1634 });
1635 }
1636 Some(UseDecl {
1637 name: self.expect_use_name("package library name")?,
1638 })
1639 }
1640
1641 fn parse_decl_duration_seconds(&mut self, label: &str) -> Option<u64> {
1644 let (value, span) = self.expect_u32(label)?;
1645 let unit = self.expect_ident(label)?;
1646 match body::parse_short_duration_seconds(&format!("{value}{}", unit.name)) {
1647 Some(seconds) if seconds > 0 => Some(seconds),
1648 _ => {
1649 self.diagnostics.push(Diagnostic {
1650 related: Vec::new(),
1651 span: span.join(unit.span),
1652 message: format!("invalid duration `{value}{}`", unit.name),
1653 suggestion: Some("use `<n><unit>` with unit s, m, h, or d".to_owned()),
1654 });
1655 None
1656 }
1657 }
1658 }
1659
1660 fn parse_harness(&mut self) -> Option<HarnessDecl> {
1661 let start = self.expect_keyword("harness")?.span.start;
1662 let name = self.expect_ident("harness name")?;
1663 self.expect_symbol(':')?;
1664 let kind = self.expect_ident("harness kind")?;
1665 let span = SourceSpan {
1666 start,
1667 end: kind.span.end,
1668 };
1669 Some(HarnessDecl { name, kind, span })
1670 }
1671
1672 fn parse_agent(&mut self) -> Option<AgentDecl> {
1673 let start = self.expect_keyword("agent")?.span.start;
1674 let name = self.expect_ident("agent name")?;
1675 let harness = if self.at_ident("using") {
1676 self.advance();
1677 Some(self.expect_ident("harness name")?)
1678 } else {
1679 None
1680 };
1681 let delegated_to = if harness.is_none() && self.at_ident("delegated") {
1682 self.advance();
1683 self.expect_keyword("to")?;
1684 Some(self.expect_ident("delegate provider")?)
1685 } else {
1686 None
1687 };
1688 if !self.at_symbol('{') {
1691 let end = delegated_to
1692 .as_ref()
1693 .map(|ident| ident.span.end)
1694 .or_else(|| harness.as_ref().map(|ident| ident.span.end))
1695 .unwrap_or(name.span.end);
1696 return Some(AgentDecl {
1697 name,
1698 harness,
1699 delegated_to,
1700 fields: Vec::new(),
1701 span: SourceSpan { start, end },
1702 });
1703 }
1704 let open = self.expect_symbol('{')?;
1705 let mut fields = Vec::new();
1706
1707 while !self.is_at_end() && !self.at_symbol('}') {
1708 let Some(field_name) = self.expect_ident("agent field") else {
1709 self.synchronize_to_block_item();
1710 continue;
1711 };
1712
1713 match field_name.name.as_str() {
1714 "provider" => {
1715 if let Some(provider) = self.expect_ident("provider name") {
1716 fields.push(AgentField::Provider(provider));
1717 } else {
1718 self.synchronize_to_block_item();
1719 }
1720 }
1721 "profile" => {
1722 if let Some(value) = self.expect_string("profile string") {
1723 fields.push(AgentField::Profile(value));
1724 } else {
1725 self.synchronize_to_block_item();
1726 }
1727 }
1728 "capacity" => {
1729 if let Some((value, span)) = self.expect_u32("capacity value") {
1730 fields.push(AgentField::Capacity(value, span));
1731 } else {
1732 self.synchronize_to_block_item();
1733 }
1734 }
1735 "skills" => {
1736 if let Some((skills, span)) = self.parse_string_list() {
1737 fields.push(AgentField::Skills(skills, span));
1738 } else {
1739 self.synchronize_to_block_item();
1740 }
1741 }
1742 "capabilities" => {
1743 if let Some((capabilities, span)) = self.parse_string_list() {
1744 fields.push(AgentField::Capabilities(capabilities, span));
1745 } else {
1746 self.synchronize_to_block_item();
1747 }
1748 }
1749 "requires" => {
1750 if let Some((classes, span)) = self.parse_feature_class_list() {
1751 fields.push(AgentField::Requires(classes, span));
1752 } else {
1753 self.synchronize_to_block_item();
1754 }
1755 }
1756 "tools" => {
1757 if let Some((tools, span)) = self.parse_ident_list() {
1758 fields.push(AgentField::Tools(tools, span));
1759 } else {
1760 self.synchronize_to_block_item();
1761 }
1762 }
1763 "compaction" => {
1764 if let Some(strategy) = self.expect_ident("compaction strategy") {
1765 fields.push(AgentField::Compaction(strategy));
1766 } else {
1767 self.synchronize_to_block_item();
1768 }
1769 }
1770 "thread" => {
1771 if let Some(mode) = self.expect_ident("thread mode") {
1772 fields.push(AgentField::Thread(mode));
1773 } else {
1774 self.synchronize_to_block_item();
1775 }
1776 }
1777 "settings" => {
1778 if let Some(sources) = self.expect_ident("settings source") {
1779 fields.push(AgentField::Settings(sources));
1780 } else {
1781 self.synchronize_to_block_item();
1782 }
1783 }
1784 _ => {
1785 let span = field_name.span;
1786 fields.push(AgentField::Unknown {
1787 name: field_name,
1788 span,
1789 });
1790 self.synchronize_to_block_item();
1791 }
1792 }
1793 }
1794
1795 let end = self
1796 .expect_symbol('}')
1797 .map(|token| token.span.end)
1798 .unwrap_or(open.span.end);
1799
1800 Some(AgentDecl {
1801 name,
1802 harness,
1803 delegated_to,
1804 fields,
1805 span: SourceSpan { start, end },
1806 })
1807 }
1808
1809 fn parse_enum(&mut self) -> Option<EnumDecl> {
1810 let start = self.expect_keyword("enum")?.span.start;
1811 let name = self.expect_ident("enum name")?;
1812 let open = self.expect_symbol('{')?;
1813 let mut variants = Vec::new();
1814
1815 let mut previous_variant_end: Option<usize> = None;
1816 while !self.is_at_end() && !self.at_symbol('}') {
1817 let Some(variant) = self.expect_ident("enum variant") else {
1818 self.synchronize_to_block_item();
1819 continue;
1820 };
1821 if let Some(previous_end) = previous_variant_end {
1826 let line_of = |offset: usize| {
1827 self.source[..offset.min(self.source.len())]
1828 .bytes()
1829 .filter(|byte| *byte == b'\n')
1830 .count()
1831 };
1832 if line_of(previous_end) == line_of(variant.span.start) {
1833 self.diagnostics.push(Diagnostic {
1834 related: Vec::new(),
1835 span: variant.span,
1836 message: format!(
1837 "enum `{}` declares variant `{}` on the same line as the previous variant",
1838 name.name, variant.name
1839 ),
1840 suggestion: Some("write one enum variant per line".to_owned()),
1841 });
1842 }
1843 }
1844 let mut fields = Vec::new();
1847 let mut end = variant.span.end;
1848 if self.at_symbol('{') {
1849 self.expect_symbol('{');
1850 while !self.is_at_end() && !self.at_symbol('}') {
1851 let Some(field_name) = self.expect_ident("variant field name") else {
1852 self.synchronize_to_block_item();
1853 continue;
1854 };
1855 let Some(ty) = self.parse_type() else {
1856 self.synchronize_to_block_item();
1857 continue;
1858 };
1859 fields.push(ClassField {
1860 span: field_name.span.join(ty.span()),
1861 name: field_name,
1862 ty,
1863 is_key: false,
1864 presence_condition: None,
1865 });
1866 }
1867 if let Some(close) = self.expect_symbol('}') {
1868 end = close.span.end;
1869 }
1870 }
1871 let span = SourceSpan {
1872 start: variant.span.start,
1873 end,
1874 };
1875 previous_variant_end = Some(end);
1876 variants.push(EnumVariantDecl {
1877 name: variant,
1878 fields,
1879 span,
1880 });
1881 }
1882
1883 let end = self
1884 .expect_symbol('}')
1885 .map(|token| token.span.end)
1886 .unwrap_or(open.span.end);
1887
1888 Some(EnumDecl {
1889 name,
1890 variants,
1891 span: SourceSpan { start, end },
1892 })
1893 }
1894
1895 fn parse_event(&mut self) -> Option<EventDecl> {
1896 let start = self.expect_keyword("signal")?.span.start;
1897 let first = self.expect_ident("signal name")?;
1900 let mut name = first.name.clone();
1901 let mut name_span = first.span;
1902 while self.at_symbol('.') {
1903 self.expect_symbol('.');
1904 let segment = self.expect_ident("signal name segment")?;
1905 name.push('.');
1906 name.push_str(&segment.name);
1907 name_span = name_span.join(segment.span);
1908 }
1909 if !name.contains('.')
1910 || name
1911 .split('.')
1912 .any(|segment| segment.chars().next().is_some_and(char::is_uppercase))
1913 {
1914 self.diagnostics.push(Diagnostic {
1915 related: Vec::new(),
1916 span: name_span,
1917 message: format!("signal name `{name}` must be dotted lowercase"),
1918 suggestion: Some(
1919 "use a dotted lowercase name such as `deploy.finished`".to_owned(),
1920 ),
1921 });
1922 }
1923 let open = self.expect_symbol('{')?;
1924 let mut fields = Vec::new();
1925 while !self.is_at_end() && !self.at_symbol('}') {
1926 let Some(field_name) = self.expect_ident("signal field name") else {
1927 self.synchronize_to_block_item();
1928 continue;
1929 };
1930 let Some(ty) = self.parse_type() else {
1931 self.synchronize_to_block_item();
1932 continue;
1933 };
1934 let presence_condition = self.parse_field_presence_condition();
1935 fields.push(ClassField {
1936 span: field_name.span.join(ty.span()),
1937 name: field_name,
1938 ty,
1939 is_key: false,
1940 presence_condition,
1941 });
1942 }
1943 let end = self
1944 .expect_symbol('}')
1945 .map(|token| token.span.end)
1946 .unwrap_or(open.span.end);
1947 Some(EventDecl {
1948 name,
1949 name_span,
1950 fields,
1951 span: SourceSpan { start, end },
1952 })
1953 }
1954
1955 fn parse_dotted_name_spanned(&mut self, label: &str) -> Option<(String, SourceSpan)> {
1957 let first = self.expect_ident(label)?;
1958 let mut name = first.name.clone();
1959 let mut span = first.span;
1960 while self.at_symbol('.') {
1961 self.expect_symbol('.');
1962 let segment = self.expect_ident(label)?;
1963 name.push('.');
1964 name.push_str(&segment.name);
1965 span = span.join(segment.span);
1966 }
1967 Some((name, span))
1968 }
1969
1970 fn parse_gauge_ref(&mut self, label: &str) -> Option<GaugeRef> {
1971 let (name, span) = self.parse_dotted_name_spanned(label)?;
1972 Some(GaugeRef { name, span })
1973 }
1974
1975 fn parse_gauge_ref_list(&mut self, label: &str, into: &mut Vec<GaugeRef>) -> Option<()> {
1977 into.push(self.parse_gauge_ref(label)?);
1978 while self.at_symbol(',') {
1979 self.expect_symbol(',');
1980 into.push(self.parse_gauge_ref(label)?);
1981 }
1982 Some(())
1983 }
1984
1985 fn parse_decl_number_text(&mut self, label: &str) -> Option<(String, SourceSpan)> {
1989 let token = self.peek()?;
1990 let TokenKind::Number(whole) = token.kind.clone() else {
1991 self.expected(label);
1992 return None;
1993 };
1994 let mut span = token.span;
1995 let mut text = whole;
1996 self.advance();
1997 if self.at_symbol('.') {
1998 self.expect_symbol('.');
1999 let token = self.peek()?;
2000 let TokenKind::Number(fraction) = token.kind.clone() else {
2001 self.expected(format!("{label} fraction digits"));
2002 return None;
2003 };
2004 text.push('.');
2005 text.push_str(&fraction);
2006 span = span.join(token.span);
2007 self.advance();
2008 }
2009 Some((text, span))
2010 }
2011
2012 fn parse_bar_direction(&mut self, label: &str) -> Option<bool> {
2018 if self.consume_ident("at") {
2019 if self.consume_ident("least") {
2020 return Some(true);
2021 }
2022 if self.consume_ident("most") {
2023 return Some(false);
2024 }
2025 self.expected(format!("`least` or `most` after `at` in {label}"));
2026 return None;
2027 }
2028 let gap_start = self.last_span_end();
2029 let gap_end = self
2030 .peek()
2031 .map(|token| token.span.start)
2032 .unwrap_or(gap_start);
2033 let gap = &self.source[gap_start..gap_end.max(gap_start)];
2034 let suggestion = if gap.contains(">=") {
2035 Some("write `at least` (the declaration grammar uses words, not `>=`)".to_owned())
2036 } else if gap.contains("<=") {
2037 Some("write `at most` (the declaration grammar uses words, not `<=`)".to_owned())
2038 } else {
2039 Some("write `at least <n>` or `at most <n>`".to_owned())
2040 };
2041 self.diagnostics.push(Diagnostic {
2042 related: Vec::new(),
2043 span: SourceSpan {
2044 start: gap_start,
2045 end: gap_end.max(gap_start),
2046 },
2047 message: format!("expected `at least` or `at most` in {label}"),
2048 suggestion,
2049 });
2050 None
2051 }
2052
2053 fn parse_mark(&mut self) -> Option<MarkDecl> {
2055 let start = self.expect_keyword("mark")?.span.start;
2056 let name = self.expect_string("mark name")?;
2057 if !self.consume_ident("after") {
2058 self.expected("`after` and a committing site in the mark declaration");
2059 return None;
2060 }
2061 let (site, site_span) = self.parse_dotted_name_spanned("mark site")?;
2062 Some(MarkDecl {
2063 name,
2064 site,
2065 site_span,
2066 span: SourceSpan {
2067 start,
2068 end: site_span.end,
2069 },
2070 })
2071 }
2072
2073 fn parse_gauge(&mut self) -> Option<GaugeDecl> {
2076 let start = self.expect_keyword("gauge")?.span.start;
2077 let name = self.expect_ident("gauge name")?;
2078 let (site, site_span) = if self.consume_ident("on") {
2079 let (site, span) = self.parse_dotted_name_spanned("gauge site")?;
2080 (Some(site), Some(span))
2081 } else {
2082 (None, None)
2083 };
2084 let open = self.expect_symbol('{')?;
2085 let mut judge: Option<GaugeJudge> = None;
2086 let mut expect: Option<GaugeBar> = None;
2087 let mut inputs: Vec<GaugeRef> = Vec::new();
2088 while !self.is_at_end() && !self.at_symbol('}') {
2089 if self.at_ident("judge") {
2090 let keyword = self.advance().clone();
2091 if !self.consume_ident("via") {
2092 self.expected("`via` after `judge`");
2093 self.synchronize_to_block_item();
2094 continue;
2095 }
2096 let form = if self.consume_ident("coerce") {
2097 self.expect_ident("coerce judge name").and_then(|name| {
2098 let mut args = Vec::new();
2099 if self.at_symbol('(') {
2100 self.expect_symbol('(');
2101 loop {
2102 let (path, _) = self.parse_dotted_name_spanned("judge argument")?;
2103 args.push(path);
2104 if !self.at_symbol(',') {
2105 break;
2106 }
2107 self.expect_symbol(',')?;
2108 }
2109 self.expect_symbol(')')?;
2110 }
2111 Some(GaugeJudge::Coerce(name, args))
2112 })
2113 } else if self.consume_ident("prompt") {
2114 self.expect_string("prompt judge template")
2115 .map(GaugeJudge::Prompt)
2116 } else if self.consume_ident("exec") {
2117 self.expect_string("exec judge command")
2118 .map(GaugeJudge::Exec)
2119 } else if self.consume_ident("labels") {
2120 self.expect_string("labels source").map(GaugeJudge::Labels)
2121 } else {
2122 self.diagnostics.push(Diagnostic {
2123 related: Vec::new(),
2124 span: keyword.span,
2125 message: "unknown judge form".to_owned(),
2126 suggestion: Some(
2127 "judge forms are `coerce <Name>`, `prompt \"<template>\"`, \
2128 `exec \"<command>\"`, and `labels \"<source>\"`"
2129 .to_owned(),
2130 ),
2131 });
2132 None
2133 };
2134 let Some(form) = form else {
2135 self.synchronize_to_block_item();
2136 continue;
2137 };
2138 if judge.is_some() {
2139 self.diagnostics.push(Diagnostic {
2140 related: Vec::new(),
2141 span: keyword.span,
2142 message: "gauge declares more than one judge".to_owned(),
2143 suggestion: Some("a gauge has exactly one judge".to_owned()),
2144 });
2145 } else {
2146 judge = Some(form);
2147 }
2148 } else if self.at_ident("expect") {
2149 let keyword = self.advance().clone();
2150 let subject_ident = match self.expect_ident("bar subject") {
2151 Some(ident) => ident,
2152 None => {
2153 self.synchronize_to_block_item();
2154 continue;
2155 }
2156 };
2157 let subject = if subject_ident.name == "P" && self.at_symbol('(') {
2158 self.expect_symbol('(');
2159 let Some(field) = self.expect_ident("chance bar field") else {
2160 self.synchronize_to_block_item();
2161 continue;
2162 };
2163 if self.expect_symbol(')').is_none() {
2164 self.synchronize_to_block_item();
2165 continue;
2166 }
2167 GaugeBarSubject::Chance { field }
2168 } else {
2169 let stat = &subject_ident.name;
2170 let is_quantile = stat.len() > 1
2171 && stat.starts_with('p')
2172 && stat[1..].chars().all(|ch| ch.is_ascii_digit());
2173 if stat != "mean" && !is_quantile {
2174 self.diagnostics.push(Diagnostic {
2175 related: Vec::new(),
2176 span: subject_ident.span,
2177 message: format!("unknown bar statistic `{stat}`"),
2178 suggestion: Some(
2179 "bars are chance-shaped (`P(<field>)`) or stat-shaped \
2180 (`mean`, `p10`, `p90`, ...)"
2181 .to_owned(),
2182 ),
2183 });
2184 }
2185 GaugeBarSubject::Stat {
2186 stat: subject_ident,
2187 }
2188 };
2189 let Some(at_least) = self.parse_bar_direction("the gauge bar") else {
2190 self.synchronize_to_block_item();
2191 continue;
2192 };
2193 let Some((threshold, threshold_span)) =
2194 self.parse_decl_number_text("bar threshold")
2195 else {
2196 self.synchronize_to_block_item();
2197 continue;
2198 };
2199 if expect.is_some() {
2200 self.diagnostics.push(Diagnostic {
2201 related: Vec::new(),
2202 span: keyword.span,
2203 message: "gauge declares more than one bar".to_owned(),
2204 suggestion: Some("a gauge has at most one `expect` bar".to_owned()),
2205 });
2206 } else {
2207 expect = Some(GaugeBar {
2208 subject,
2209 at_least,
2210 threshold,
2211 span: keyword.span.join(threshold_span),
2212 });
2213 }
2214 } else if self.at_ident("inputs") {
2215 self.advance();
2216 if self
2217 .parse_gauge_ref_list("input gauge name", &mut inputs)
2218 .is_none()
2219 {
2220 self.synchronize_to_block_item();
2221 }
2222 } else {
2223 let span = self.peek().map(|token| token.span).unwrap_or(open.span);
2224 self.diagnostics.push(Diagnostic {
2225 related: Vec::new(),
2226 span,
2227 message: "unknown gauge clause".to_owned(),
2228 suggestion: Some(
2229 "gauge clauses are `judge via`, `expect`, and `inputs`".to_owned(),
2230 ),
2231 });
2232 self.synchronize_to_block_item();
2233 }
2234 }
2235 let end = self
2236 .expect_symbol('}')
2237 .map(|token| token.span.end)
2238 .unwrap_or(open.span.end);
2239 let Some(judge) = judge else {
2240 self.diagnostics.push(Diagnostic {
2241 related: Vec::new(),
2242 span: name.span,
2243 message: format!("gauge `{}` declares no judge", name.name),
2244 suggestion: Some(
2245 "add `judge via coerce <Name>`, `judge via prompt \"<template>\"`, \
2246 `judge via exec \"<command>\"`, or `judge via labels \"<source>\"`"
2247 .to_owned(),
2248 ),
2249 });
2250 return None;
2251 };
2252 Some(GaugeDecl {
2253 name,
2254 site,
2255 site_span,
2256 judge,
2257 expect,
2258 inputs,
2259 span: SourceSpan { start, end },
2260 })
2261 }
2262
2263 fn parse_campaign(&mut self) -> Option<CampaignDecl> {
2265 let start = self.expect_keyword("campaign")?.span.start;
2266 let name = self.expect_ident("campaign name")?;
2267 let open = self.expect_symbol('{')?;
2268 let mut ascend: Vec<GaugeRef> = Vec::new();
2269 let mut reach: Vec<CampaignReach> = Vec::new();
2270 let mut guard: Vec<CampaignGuard> = Vec::new();
2271 let mut sacrifice: Vec<GaugeRef> = Vec::new();
2272 let mut proposer_redacted = false;
2273 while !self.is_at_end() && !self.at_symbol('}') {
2274 if self.at_ident("ascend") {
2275 self.advance();
2276 if self
2277 .parse_gauge_ref_list("ascend gauge name", &mut ascend)
2278 .is_none()
2279 {
2280 self.synchronize_to_block_item();
2281 }
2282 } else if self.at_ident("reach") {
2283 let keyword = self.advance().clone();
2284 let Some(gauge) = self.parse_gauge_ref("reach gauge name") else {
2285 self.synchronize_to_block_item();
2286 continue;
2287 };
2288 let Some(at_least) = self.parse_bar_direction("the reach target") else {
2289 self.synchronize_to_block_item();
2290 continue;
2291 };
2292 let Some((threshold, threshold_span)) =
2293 self.parse_decl_number_text("reach threshold")
2294 else {
2295 self.synchronize_to_block_item();
2296 continue;
2297 };
2298 let unit = if self
2301 .peek()
2302 .map(|token| {
2303 matches!(&token.kind, TokenKind::Ident(name)
2304 if matches!(name.as_str(), "ms" | "s" | "m" | "h" | "d"))
2305 })
2306 .unwrap_or(false)
2307 {
2308 self.expect_ident("unit").map(|ident| ident.name)
2309 } else {
2310 None
2311 };
2312 reach.push(CampaignReach {
2313 gauge,
2314 at_least,
2315 threshold,
2316 unit,
2317 span: keyword.span.join(threshold_span),
2318 });
2319 } else if self.at_ident("guard") {
2320 let keyword = self.advance().clone();
2321 let Some(gauge) = self.parse_gauge_ref("guard gauge name") else {
2322 self.synchronize_to_block_item();
2323 continue;
2324 };
2325 if !self.consume_ident("within") {
2326 self.expected("`within` after the guarded gauge");
2327 self.synchronize_to_block_item();
2328 continue;
2329 }
2330 let Some((band_percent, band_span)) = self.parse_decl_number_text("guard band")
2331 else {
2332 self.synchronize_to_block_item();
2333 continue;
2334 };
2335 if !self.consume_ident("percent") {
2336 self.expected("`percent` after the guard band");
2337 self.synchronize_to_block_item();
2338 continue;
2339 }
2340 guard.push(CampaignGuard {
2341 gauge,
2342 band_percent,
2343 span: keyword.span.join(band_span),
2344 });
2345 } else if self.at_ident("sacrifice") {
2346 self.advance();
2347 if self
2348 .parse_gauge_ref_list("sacrifice gauge name", &mut sacrifice)
2349 .is_none()
2350 {
2351 self.synchronize_to_block_item();
2352 }
2353 } else if self.at_ident("proposer") {
2354 self.advance();
2355 if self.consume_ident("redacted") {
2356 proposer_redacted = true;
2357 } else {
2358 self.expected("`redacted` after `proposer`");
2359 self.synchronize_to_block_item();
2360 }
2361 } else {
2362 let span = self.peek().map(|token| token.span).unwrap_or(open.span);
2363 self.diagnostics.push(Diagnostic {
2364 related: Vec::new(),
2365 span,
2366 message: "unknown campaign clause".to_owned(),
2367 suggestion: Some(
2368 "campaign clauses are `ascend`, `reach`, `guard`, `sacrifice`, \
2369 and `proposer redacted`"
2370 .to_owned(),
2371 ),
2372 });
2373 self.synchronize_to_block_item();
2374 }
2375 }
2376 let end = self
2377 .expect_symbol('}')
2378 .map(|token| token.span.end)
2379 .unwrap_or(open.span.end);
2380 if ascend.is_empty() && reach.is_empty() {
2381 self.diagnostics.push(Diagnostic {
2382 related: Vec::new(),
2383 span: name.span,
2384 message: format!("campaign `{}` names nothing to improve", name.name),
2385 suggestion: Some("add an `ascend` or `reach` clause".to_owned()),
2386 });
2387 }
2388 Some(CampaignDecl {
2389 name,
2390 ascend,
2391 reach,
2392 guard,
2393 sacrifice,
2394 proposer_redacted,
2395 span: SourceSpan { start, end },
2396 })
2397 }
2398
2399 fn last_span_end(&self) -> usize {
2400 self.pos
2401 .checked_sub(1)
2402 .and_then(|index| self.tokens.get(index))
2403 .map(|token| token.span.end)
2404 .unwrap_or(0)
2405 }
2406
2407 fn parse_dotted_name(&mut self, label: &str) -> Option<String> {
2408 let first = self.expect_ident(label)?;
2409 let mut name = first.name.clone();
2410 while self.at_symbol('.') {
2411 self.advance();
2412 let segment = self.expect_ident(label)?;
2413 name.push('.');
2414 name.push_str(&segment.name);
2415 }
2416 Some(name)
2417 }
2418
2419 fn capture_expr_to_line_end(&mut self) -> (String, SourceSpan) {
2422 let start = self
2423 .peek()
2424 .map(|token| token.span.start)
2425 .unwrap_or(self.source.len());
2426 let line_end = self.source[start..]
2427 .find('\n')
2428 .map(|offset| start + offset)
2429 .unwrap_or(self.source.len());
2430 let mut end = start;
2431 while !self.is_at_end() {
2432 let Some(token) = self.peek() else { break };
2433 if token.span.start >= line_end {
2434 break;
2435 }
2436 let token_end = token.span.end.min(line_end);
2437 self.advance();
2438 end = token_end;
2439 }
2440 let span = SourceSpan { start, end };
2441 trimmed_source_text(self.source_text(span), span)
2442 }
2443
2444 fn capture_expr_until_ident(&mut self, terminator: &str) -> (String, SourceSpan) {
2447 let start = self
2448 .peek()
2449 .map(|token| token.span.start)
2450 .unwrap_or(self.source.len());
2451 let mut end = start;
2452 while !self.is_at_end() && !self.at_ident(terminator) && !self.at_symbol('}') {
2453 let Some(token) = self.peek() else { break };
2454 let token_end = token.span.end;
2455 self.advance();
2456 end = token_end;
2457 }
2458 let span = SourceSpan { start, end };
2459 trimmed_source_text(self.source_text(span), span)
2460 }
2461
2462 fn parse_test(&mut self) -> Option<TestDecl> {
2463 let start = self.expect_keyword("test")?.span.start;
2464 let name = self.expect_string("test name")?;
2465 let open = self.expect_symbol('{')?;
2466 let mut workflow = None;
2467 let mut clauses = Vec::new();
2468 while !self.is_at_end() && !self.at_symbol('}') {
2469 if self.at_ident("workflow") {
2470 self.advance();
2471 match self.expect_ident("workflow name") {
2472 Some(name) => {
2473 if workflow.is_some() {
2474 self.diagnostics.push(Diagnostic {
2475 related: Vec::new(),
2476 span: name.span,
2477 message: "a test scenario binds at most one `workflow`".to_owned(),
2478 suggestion: Some(
2479 "remove the extra `workflow <Name>` header".to_owned(),
2480 ),
2481 });
2482 }
2483 workflow = Some(name);
2484 }
2485 None => self.synchronize_to_block_item(),
2486 }
2487 } else if self.at_ident("given") {
2488 match self.parse_given() {
2489 Some(clause) => clauses.push(TestClause::Given(clause)),
2490 None => self.synchronize_to_block_item(),
2491 }
2492 } else if self.at_ident("stub") {
2493 match self.parse_stub() {
2494 Some(clause) => clauses.push(TestClause::Stub(clause)),
2495 None => self.synchronize_to_block_item(),
2496 }
2497 } else if self.at_ident("run") {
2498 match self.parse_run() {
2499 Some(clause) => clauses.push(TestClause::Run(clause)),
2500 None => self.synchronize_to_block_item(),
2501 }
2502 } else if self.at_ident("expect") {
2503 match self.parse_expect() {
2504 Some(clause) => clauses.push(TestClause::Expect(clause)),
2505 None => self.synchronize_to_block_item(),
2506 }
2507 } else {
2508 self.unexpected("a test clause (`workflow`, `given`, `stub`, `run`, or `expect`)");
2509 self.synchronize_to_block_item();
2510 }
2511 }
2512 let end = self
2513 .expect_symbol('}')
2514 .map(|token| token.span.end)
2515 .unwrap_or(open.span.end);
2516 Some(TestDecl {
2517 name,
2518 workflow,
2519 clauses,
2520 span: SourceSpan { start, end },
2521 })
2522 }
2523
2524 fn parse_test_record(&mut self) -> Option<(Vec<TestField>, usize)> {
2525 let open = self.expect_symbol('{')?;
2526 let mut fields = Vec::new();
2527 while !self.is_at_end() && !self.at_symbol('}') {
2528 let Some(name) = self.expect_ident("test field name") else {
2529 self.synchronize_to_block_item();
2530 continue;
2531 };
2532 let (value, value_span) = self.capture_expr_to_line_end();
2533 fields.push(TestField {
2534 span: name.span.join(value_span),
2535 name,
2536 value,
2537 });
2538 }
2539 let end = self
2540 .expect_symbol('}')
2541 .map(|token| token.span.end)
2542 .unwrap_or(open.span.end);
2543 Some((fields, end))
2544 }
2545
2546 fn parse_given(&mut self) -> Option<GivenClause> {
2547 let start = self.expect_keyword("given")?.span.start;
2548 if self.consume_ident("input") {
2549 let (fields, end) = self.parse_test_record()?;
2550 Some(GivenClause::Input {
2551 fields,
2552 span: SourceSpan { start, end },
2553 })
2554 } else if self.consume_ident("fact") {
2555 let ty = self.expect_ident("fact type")?;
2556 let (fields, end) = self.parse_test_record()?;
2557 Some(GivenClause::Fact {
2558 ty,
2559 fields,
2560 span: SourceSpan { start, end },
2561 })
2562 } else if self.consume_ident("signal") {
2563 let name = self.parse_dotted_name("signal name")?;
2564 let (fields, end) = self.parse_test_record()?;
2565 Some(GivenClause::Signal {
2566 name,
2567 fields,
2568 span: SourceSpan { start, end },
2569 })
2570 } else if self.consume_ident("clock") {
2571 if !self.consume_ident("at") {
2572 self.expected("`at <timestamp>` after `given clock`");
2573 }
2574 let at = self.expect_string("clock timestamp")?;
2575 let end = at.span.end;
2576 Some(GivenClause::Clock {
2577 at,
2578 span: SourceSpan { start, end },
2579 })
2580 } else if self.consume_ident("tracker") {
2581 let tracker = self.parse_dotted_name("tracker name")?;
2582 if !self.consume_ident("issue") {
2583 self.expected("`issue { … }` after `given tracker <name>`");
2584 }
2585 let (fields, end) = self.parse_test_record()?;
2586 Some(GivenClause::Tracker {
2587 tracker,
2588 fields,
2589 span: SourceSpan { start, end },
2590 })
2591 } else if self.consume_ident("file") {
2592 let store = self.parse_dotted_name("file store name")?;
2593 if !self.consume_ident("at") {
2594 self.expected("`at <path> \"<content>\"` after `given file <store>`");
2595 }
2596 let path = self.expect_string("file path")?;
2597 let content = self.expect_string("file content")?;
2598 let end = content.span.end;
2599 Some(GivenClause::File {
2600 store,
2601 path,
2602 content,
2603 span: SourceSpan { start, end },
2604 })
2605 } else {
2606 self.unexpected(
2607 "`input`, `fact`, `signal`, `clock`, `tracker`, or `file` after `given`",
2608 );
2609 None
2610 }
2611 }
2612
2613 fn parse_stub(&mut self) -> Option<StubClause> {
2614 let start = self.expect_keyword("stub")?.span.start;
2615 let line_end = self.source[start..]
2620 .find('\n')
2621 .map(|offset| start + offset)
2622 .unwrap_or(self.source.len());
2623 let mut segments = Vec::new();
2624 while matches!(
2625 self.peek().map(|token| &token.kind),
2626 Some(TokenKind::Ident(_))
2627 ) && self.peek().is_some_and(|token| token.span.start < line_end)
2628 {
2629 match self.parse_dotted_name("stub surface") {
2630 Some(segment) => segments.push(segment),
2631 None => break,
2632 }
2633 }
2634 if segments.len() < 2 {
2635 self.diagnostics.push(Diagnostic {
2636 related: Vec::new(),
2637 span: SourceSpan {
2638 start,
2639 end: self.last_span_end(),
2640 },
2641 message: "stub needs a surface and an outcome (e.g. `stub agent triager succeeds`)"
2642 .to_owned(),
2643 suggestion: Some("write `stub <surface...> <outcome> [payload]`".to_owned()),
2644 });
2645 return None;
2646 }
2647 let outcome = segments.pop().expect("outcome present");
2648 let surface = segments;
2649 let payload = if self.at_symbol('{') {
2650 let (fields, _) = self.parse_test_record()?;
2651 Some(StubPayload::Record(fields))
2652 } else if matches!(
2653 self.peek().map(|token| &token.kind),
2654 Some(TokenKind::String(_))
2655 ) {
2656 Some(StubPayload::Message(self.expect_string("stub message")?))
2657 } else {
2658 None
2659 };
2660 let end = self.last_span_end();
2661 Some(StubClause {
2662 surface,
2663 outcome,
2664 payload,
2665 span: SourceSpan { start, end },
2666 })
2667 }
2668
2669 fn parse_run(&mut self) -> Option<RunClause> {
2670 let start = self.expect_keyword("run")?.span.start;
2671 let kind = if self.consume_ident("until") {
2672 if self.consume_ident("idle") {
2673 RunKind::UntilIdle
2674 } else if self.consume_ident("workflow") {
2675 if self.consume_ident("completed") {
2676 RunKind::UntilWorkflowCompleted
2677 } else if self.consume_ident("failed") {
2678 RunKind::UntilWorkflowFailed
2679 } else {
2680 self.expected("`completed` or `failed` after `workflow`");
2681 return None;
2682 }
2683 } else {
2684 self.expected("`idle` or `workflow completed|failed` after `until`");
2685 return None;
2686 }
2687 } else if self.consume_ident("for") {
2688 let (steps, _) = self.expect_u32("step count")?;
2689 if !self.consume_ident("steps") {
2690 self.expected("`steps` after the step count");
2691 }
2692 RunKind::ForSteps(steps)
2693 } else {
2694 self.expected("`until ...` or `for <N> steps` after `run`");
2695 return None;
2696 };
2697 let end = self.last_span_end();
2698 Some(RunClause {
2699 kind,
2700 span: SourceSpan { start, end },
2701 })
2702 }
2703
2704 fn parse_expect(&mut self) -> Option<ExpectClause> {
2705 let start = self.expect_keyword("expect")?.span.start;
2706 let target = if self.consume_ident("workflow") {
2707 if self.consume_ident("completed") {
2708 ExpectTarget::WorkflowCompleted
2709 } else if self.consume_ident("failed") {
2710 let failure = if self.consume_ident("with") {
2711 self.expect_ident("failure type")
2712 } else {
2713 None
2714 };
2715 ExpectTarget::WorkflowFailed { failure }
2716 } else {
2717 self.expected("`completed` or `failed` after `workflow`");
2718 return None;
2719 }
2720 } else if self.consume_ident("rule") {
2721 let name = self.expect_ident("rule name")?;
2722 let status = if self.consume_ident("fired") {
2723 if matches!(
2724 self.peek().map(|token| &token.kind),
2725 Some(TokenKind::Number(_))
2726 ) {
2727 let (count, _) = self.expect_u32("fired count")?;
2728 if !self.consume_ident("times") {
2729 self.expected("`times` after the fired count");
2730 }
2731 RuleStatus::FiredTimes(count)
2732 } else {
2733 RuleStatus::Fired
2734 }
2735 } else if self.consume_ident("did") {
2736 if !self.consume_ident("not") {
2737 self.expected("`not` in `did not fire`");
2738 }
2739 if !self.consume_ident("fire") {
2740 self.expected("`fire` in `did not fire`");
2741 }
2742 RuleStatus::DidNotFire
2743 } else {
2744 self.expected("`fired`, `fired <N> times`, or `did not fire`");
2745 return None;
2746 };
2747 ExpectTarget::Rule { name, status }
2748 } else if self.consume_ident("effect") {
2749 let name = self.parse_dotted_name("effect name")?;
2750 let status = if self.consume_ident("requested") {
2751 EffectStatus::Requested
2752 } else if self.consume_ident("completed") {
2753 EffectStatus::Completed
2754 } else if self.consume_ident("failed") {
2755 EffectStatus::Failed
2756 } else {
2757 self.expected("`requested`, `completed`, or `failed` after the effect name");
2758 return None;
2759 };
2760 ExpectTarget::Effect { name, status }
2761 } else if self.consume_ident("diagnostic") {
2762 let code = self.parse_dotted_name("diagnostic code")?;
2763 ExpectTarget::Diagnostic { code }
2764 } else if self.consume_ident("no") {
2765 let name = self.parse_dotted_name("forbidden effect name")?;
2766 ExpectTarget::NoEffect { name }
2767 } else {
2768 let noun = self.parse_dotted_name("projection noun")?;
2769 let kind = self.parse_proj_query_kind()?;
2770 let end = self.last_span_end();
2771 ExpectTarget::Projection(ProjQuery {
2772 noun,
2773 kind,
2774 span: SourceSpan { start, end },
2775 })
2776 };
2777 let end = self.last_span_end();
2778 Some(ExpectClause {
2779 target,
2780 span: SourceSpan { start, end },
2781 })
2782 }
2783
2784 fn parse_proj_query_kind(&mut self) -> Option<ProjQueryKind> {
2785 if self.consume_ident("exists") {
2786 return Some(ProjQueryKind::Exists);
2787 }
2788 if self.consume_ident("count") {
2789 if !self.consume_ident("where") {
2790 self.expected("`where <predicate> is <N>` after `count`");
2791 return None;
2792 }
2793 let (predicate, _) = self.capture_expr_until_ident("is");
2794 if !self.consume_ident("is") {
2795 self.expected("`is <N>` after the count predicate");
2796 return None;
2797 }
2798 let (count, _) = self.expect_u32("count value")?;
2799 return Some(ProjQueryKind::Count { predicate, count });
2800 }
2801 if self.consume_ident("where") {
2802 let (predicate, _) = self.capture_expr_to_line_end();
2803 return Some(ProjQueryKind::Where { predicate });
2804 }
2805 self.expected("`exists`, `count where ... is <N>`, or `where ...`");
2806 None
2807 }
2808
2809 fn parse_source(&mut self) -> Option<SourceDecl> {
2810 let start = self.expect_keyword("source")?.span.start;
2811 let provider = self.expect_ident("source provider")?;
2812 let is_clock = provider.name == "clock";
2813 if !self.consume_ident("as") {
2814 self.expected("`as <name>` after the source provider");
2815 return None;
2816 }
2817 let name = self.expect_ident("source name")?;
2818 let open = self.expect_symbol('{')?;
2819
2820 let mut recurrence: Option<Recurrence> = None;
2821 let mut timezone: Option<StringLiteral> = None;
2822 let mut missed: Option<MissedPolicy> = None;
2823 let mut path: Option<StringLiteral> = None;
2824 let mut watch: Option<StringLiteral> = None;
2825 let mut url: Option<StringLiteral> = None;
2826 let mut dedup: Option<SourceValue> = None;
2827 let mut observe_binding: Option<Ident> = None;
2828 let mut emit: Option<SourceEmit> = None;
2829
2830 while !self.is_at_end() && !self.at_symbol('}') {
2831 if self.at_ident("every") || self.at_ident("at") {
2832 if let Some(parsed) = self.parse_recurrence() {
2833 recurrence = Some(parsed);
2834 } else {
2835 self.synchronize_to_block_item();
2836 }
2837 } else if self.at_ident("timezone") {
2838 self.advance();
2839 timezone = self.expect_string("timezone string");
2840 } else if self.at_ident("path") {
2841 self.advance();
2842 path = self.expect_string("path string");
2843 } else if self.at_ident("watch") {
2844 self.advance();
2845 watch = self.expect_string("watch glob string");
2846 } else if self.at_ident("url") {
2847 self.advance();
2848 url = self.expect_string("url string");
2849 } else if self.at_ident("dedup") {
2850 self.advance();
2851 dedup = self.parse_source_value();
2852 } else if self.at_ident("missed") {
2853 missed = self.parse_missed_policy();
2854 } else if self.at_ident("observe") {
2855 self.advance();
2856 if !self.consume_ident("as") {
2857 self.expected("`as <binding>` after `observe`");
2858 }
2859 observe_binding = self.expect_ident("observe binding");
2860 } else if self.at_ident("emit") {
2861 emit = self.parse_source_emit();
2862 } else {
2863 self.unexpected(
2864 "a source clause (`every`/`at`, `timezone`, `path`, `watch`, `url`, `dedup`, `missed`, `observe`, `emit`)",
2865 );
2866 self.synchronize_to_block_item();
2867 }
2868 }
2869 let end = self
2870 .expect_symbol('}')
2871 .map(|token| token.span.end)
2872 .unwrap_or(open.span.end);
2873 let span = SourceSpan { start, end };
2874
2875 let observe_binding = match observe_binding {
2876 Some(binding) => binding,
2877 None => {
2878 self.diagnostics.push(Diagnostic {
2879 related: Vec::new(),
2880 span,
2881 message: format!("source `{}` must declare `observe as <binding>`", name.name),
2882 suggestion: Some("add `observe as tick`".to_owned()),
2883 });
2884 return None;
2885 }
2886 };
2887 let emit = match emit {
2888 Some(emit) => emit,
2889 None => {
2890 self.diagnostics.push(Diagnostic {
2891 related: Vec::new(),
2892 span,
2893 message: format!(
2894 "source `{}` must declare `emit <signal> {{ ... }}`",
2895 name.name
2896 ),
2897 suggestion: Some("add `emit triage.tick { ... }`".to_owned()),
2898 });
2899 return None;
2900 }
2901 };
2902
2903 let clock = if is_clock {
2904 let recurrence = match recurrence {
2905 Some(recurrence) => recurrence,
2906 None => {
2907 self.diagnostics.push(Diagnostic {
2908 related: Vec::new(),
2909 span,
2910 message: format!("clock source `{}` must declare a recurrence", name.name),
2911 suggestion: Some(
2912 "add `every weekday at 09:00`, `every 5m`, or `at 09:00`".to_owned(),
2913 ),
2914 });
2915 return None;
2916 }
2917 };
2918 Some(ClockPolicy {
2919 recurrence,
2920 timezone,
2921 missed,
2922 span,
2923 })
2924 } else {
2925 if recurrence.is_some() || timezone.is_some() || missed.is_some() {
2926 self.diagnostics.push(Diagnostic {
2927 related: Vec::new(),
2928 span,
2929 message: format!(
2930 "source `{}` uses clock-only clauses but its provider is `{}`, not `clock`",
2931 name.name, provider.name
2932 ),
2933 suggestion: Some(
2934 "use `source clock as ...` for recurrence, timezone, or missed clauses"
2935 .to_owned(),
2936 ),
2937 });
2938 }
2939 None
2940 };
2941
2942 Some(SourceDecl {
2943 name,
2944 provider,
2945 clock,
2946 path,
2947 watch,
2948 url,
2949 dedup,
2950 observe_binding,
2951 emit,
2952 span,
2953 })
2954 }
2955
2956 fn parse_recurrence(&mut self) -> Option<Recurrence> {
2957 if self.at_ident("at") {
2958 let at = self.expect_keyword("at")?;
2959 let time = self.parse_time_of_day()?;
2960 return Some(Recurrence::At {
2961 span: at.span.join(time.span),
2962 time,
2963 });
2964 }
2965 let every = self.expect_keyword("every")?;
2966 if matches!(
2967 self.peek().map(|token| &token.kind),
2968 Some(TokenKind::Number(_))
2969 ) {
2970 let (value, _) = self.expect_u32("recurrence interval")?;
2971 let unit = self.expect_ident("duration unit (`s`, `m`, `h`, or `d`)")?;
2972 let seconds = match unit.name.as_str() {
2973 "s" => value as u64,
2974 "m" => value as u64 * 60,
2975 "h" => value as u64 * 3_600,
2976 "d" => value as u64 * 86_400,
2977 other => {
2978 self.diagnostics.push(Diagnostic {
2979 related: Vec::new(),
2980 span: unit.span,
2981 message: format!("unknown duration unit `{other}`"),
2982 suggestion: Some("use `s`, `m`, `h`, or `d`".to_owned()),
2983 });
2984 return None;
2985 }
2986 };
2987 return Some(Recurrence::EveryDuration {
2988 seconds,
2989 source: format!("{value}{}", unit.name),
2990 span: every.span.join(unit.span),
2991 });
2992 }
2993 let pattern_ident =
2994 self.expect_ident("calendar pattern (`day`, `weekday`, or a weekday)")?;
2995 let pattern = match pattern_ident.name.as_str() {
2996 "day" => CalendarPattern::Day,
2997 "weekday" => CalendarPattern::Weekday,
2998 "monday" => CalendarPattern::Weekly(Weekday::Monday),
2999 "tuesday" => CalendarPattern::Weekly(Weekday::Tuesday),
3000 "wednesday" => CalendarPattern::Weekly(Weekday::Wednesday),
3001 "thursday" => CalendarPattern::Weekly(Weekday::Thursday),
3002 "friday" => CalendarPattern::Weekly(Weekday::Friday),
3003 "saturday" => CalendarPattern::Weekly(Weekday::Saturday),
3004 "sunday" => CalendarPattern::Weekly(Weekday::Sunday),
3005 other => {
3006 self.diagnostics.push(Diagnostic {
3007 related: Vec::new(),
3008 span: pattern_ident.span,
3009 message: format!("unknown calendar pattern `{other}`"),
3010 suggestion: Some(
3011 "use `day`, `weekday`, or a weekday such as `monday`".to_owned(),
3012 ),
3013 });
3014 return None;
3015 }
3016 };
3017 if !self.consume_ident("at") {
3018 self.expected("`at <hh:mm>` after the calendar pattern");
3019 return None;
3020 }
3021 let time = self.parse_time_of_day()?;
3022 Some(Recurrence::EveryCalendar {
3023 pattern,
3024 span: every.span.join(time.span),
3025 time,
3026 })
3027 }
3028
3029 fn parse_time_of_day(&mut self) -> Option<TimeOfDay> {
3030 let (hour, hour_span) = self.expect_u32("hour")?;
3031 self.expect_symbol(':')?;
3032 let (minute, minute_span) = self.expect_u32("minute")?;
3033 if hour > 23 || minute > 59 {
3034 self.diagnostics.push(Diagnostic {
3035 related: Vec::new(),
3036 span: hour_span.join(minute_span),
3037 message: format!("invalid time of day `{hour:02}:{minute:02}`"),
3038 suggestion: Some("use a 24-hour `hh:mm` such as `09:00`".to_owned()),
3039 });
3040 return None;
3041 }
3042 Some(TimeOfDay {
3043 hour: hour as u8,
3044 minute: minute as u8,
3045 span: hour_span.join(minute_span),
3046 })
3047 }
3048
3049 fn parse_missed_policy(&mut self) -> Option<MissedPolicy> {
3050 self.expect_keyword("missed")?;
3051 if self.consume_ident("skip") {
3052 return Some(MissedPolicy::Skip);
3053 }
3054 if self.consume_ident("coalesce") {
3055 return Some(MissedPolicy::Coalesce);
3056 }
3057 if self.consume_ident("catch_up") {
3058 if !self.consume_ident("limit") {
3059 self.expected("`limit <N>` after `catch_up`");
3060 return None;
3061 }
3062 let (limit, _) = self.expect_u32("catch_up limit")?;
3063 return Some(MissedPolicy::CatchUp { limit });
3064 }
3065 self.expected("`skip`, `coalesce`, or `catch_up limit <N>`");
3066 None
3067 }
3068
3069 fn parse_source_emit(&mut self) -> Option<SourceEmit> {
3070 let emit = self.expect_keyword("emit")?;
3071 let first = self.expect_ident("emit signal name")?;
3072 let mut signal = first.name.clone();
3073 let mut signal_span = first.span;
3074 while self.at_symbol('.') {
3075 self.advance();
3076 let segment = self.expect_ident("signal name segment")?;
3077 signal.push('.');
3078 signal.push_str(&segment.name);
3079 signal_span = signal_span.join(segment.span);
3080 }
3081 let from = if self.consume_ident("from") {
3082 Some(self.expect_ident("binding name after `from`")?)
3083 } else {
3084 None
3085 };
3086 if from.is_some() && !self.at_symbol('{') {
3087 let end = from.as_ref().map(|ident| ident.span.end).unwrap_or(0);
3088 return Some(SourceEmit {
3089 signal,
3090 signal_span,
3091 from,
3092 fields: Vec::new(),
3093 span: SourceSpan {
3094 start: emit.span.start,
3095 end,
3096 },
3097 });
3098 }
3099 let open = self.expect_symbol('{')?;
3100 let mut fields = Vec::new();
3101 while !self.is_at_end() && !self.at_symbol('}') {
3102 let Some(field_name) = self.expect_ident("emit field name") else {
3103 self.synchronize_to_block_item();
3104 continue;
3105 };
3106 let Some(value) = self.parse_source_value() else {
3107 self.synchronize_to_block_item();
3108 continue;
3109 };
3110 let value_span = match &value {
3111 SourceValue::Path { span, .. } => *span,
3112 SourceValue::String(literal) => literal.span,
3113 SourceValue::Number(_, span) => *span,
3114 };
3115 fields.push(SourceEmitField {
3116 span: field_name.span.join(value_span),
3117 name: field_name,
3118 value,
3119 });
3120 }
3121 let end = self
3122 .expect_symbol('}')
3123 .map(|token| token.span.end)
3124 .unwrap_or(open.span.end);
3125 Some(SourceEmit {
3126 signal,
3127 signal_span,
3128 from,
3129 fields,
3130 span: SourceSpan {
3131 start: emit.span.start,
3132 end,
3133 },
3134 })
3135 }
3136
3137 fn parse_source_value(&mut self) -> Option<SourceValue> {
3138 match self.peek().map(|token| &token.kind) {
3139 Some(TokenKind::String(_)) => self.expect_string("value").map(SourceValue::String),
3140 Some(TokenKind::Number(_)) => {
3141 let token = self.advance().clone();
3142 if let TokenKind::Number(value) = token.kind {
3143 Some(SourceValue::Number(value, token.span))
3144 } else {
3145 None
3146 }
3147 }
3148 Some(TokenKind::Ident(_)) => {
3149 let binding = self.expect_ident("value path")?;
3150 let mut segments = Vec::new();
3151 let mut span = binding.span;
3152 while self.at_symbol('.') {
3153 self.advance();
3154 let segment = self.expect_ident("path segment")?;
3155 span = span.join(segment.span);
3156 segments.push(segment);
3157 }
3158 Some(SourceValue::Path {
3159 binding,
3160 segments,
3161 span,
3162 })
3163 }
3164 _ => {
3165 self.expected("a value (observation path, string, or number)");
3166 None
3167 }
3168 }
3169 }
3170
3171 fn parse_class(&mut self) -> Option<ClassDecl> {
3172 let start = self.expect_keyword("class")?.span.start;
3173 let name = self.expect_ident("class name")?;
3174 let open = self.expect_symbol('{')?;
3175 let mut fields = Vec::new();
3176
3177 while !self.is_at_end() && !self.at_symbol('}') {
3178 let Some(field_name) = self.expect_ident("class field name") else {
3179 self.synchronize_to_block_item();
3180 continue;
3181 };
3182 let Some(ty) = self.parse_type() else {
3183 self.synchronize_to_block_item();
3184 continue;
3185 };
3186 let mut is_key = false;
3189 if self.at_symbol('@') {
3190 if let Some(tag) = self.parse_tag() {
3191 if tag.name == "key" {
3192 is_key = true;
3193 } else {
3194 self.diagnostics.push(Diagnostic {
3195 related: Vec::new(),
3196 span: tag.span,
3197 message: format!("unknown field tag `@{}`", tag.name),
3198 suggestion: Some(
3199 "the only field tag is `@key` (the class natural key)".to_owned(),
3200 ),
3201 });
3202 }
3203 }
3204 }
3205 let presence_condition = self.parse_field_presence_condition();
3206 let span = field_name.span.join(ty.span());
3207 fields.push(ClassField {
3208 span,
3209 name: field_name,
3210 ty,
3211 is_key,
3212 presence_condition,
3213 });
3214 }
3215
3216 let end = self
3217 .expect_symbol('}')
3218 .map(|token| token.span.end)
3219 .unwrap_or(open.span.end);
3220
3221 Some(ClassDecl {
3222 name,
3223 fields,
3224 span: SourceSpan { start, end },
3225 })
3226 }
3227
3228 fn parse_table(
3229 &mut self,
3230 tags: Vec<TagDecl>,
3231 description: Option<StringLiteral>,
3232 ) -> Option<TableDecl> {
3233 let start = self.expect_keyword("table")?.span.start;
3234 let name = self.expect_ident("table name")?;
3235 self.expect_keyword("as")?;
3236 let schema = self.expect_ident("table row class")?;
3237 let open = self.expect_symbol('[')?;
3238 let mut rows = Vec::new();
3239
3240 while !self.is_at_end() && !self.at_symbol(']') {
3241 if self.at_symbol(',') {
3242 self.advance();
3243 continue;
3244 }
3245 if !self.at_symbol('{') {
3246 self.unexpected("table row `{ ... }`");
3247 self.synchronize_to_table_row();
3248 continue;
3249 }
3250 if let Some(row) = self.parse_table_row() {
3251 rows.push(row);
3252 }
3253 if self.at_symbol(',') {
3254 self.advance();
3255 }
3256 }
3257
3258 let end = self
3259 .expect_symbol(']')
3260 .map(|token| token.span.end)
3261 .unwrap_or(open.span.end);
3262 Some(TableDecl {
3263 name,
3264 tags,
3265 description,
3266 schema,
3267 rows,
3268 span: SourceSpan { start, end },
3269 })
3270 }
3271
3272 fn parse_table_row(&mut self) -> Option<TableRow> {
3273 let open = self.expect_symbol('{')?;
3274 let body_start = open.span.end;
3275 let mut depth = 1usize;
3276 let mut body_end = body_start;
3277 let mut close_end = open.span.end;
3278
3279 while !self.is_at_end() {
3280 let token = self.advance().clone();
3281 match token.kind {
3282 TokenKind::Symbol('{') => {
3283 depth += 1;
3284 body_end = token.span.end;
3285 }
3286 TokenKind::Symbol('}') => {
3287 depth -= 1;
3288 if depth == 0 {
3289 body_end = token.span.start;
3290 close_end = token.span.end;
3291 break;
3292 }
3293 body_end = token.span.end;
3294 }
3295 _ => body_end = token.span.end,
3296 }
3297 }
3298
3299 if depth != 0 {
3300 self.diagnostics.push(Diagnostic {
3301 related: Vec::new(),
3302 span: SourceSpan {
3303 start: open.span.start,
3304 end: body_end,
3305 },
3306 message: "unterminated table row".to_owned(),
3307 suggestion: Some("close the table row with `}`".to_owned()),
3308 });
3309 return None;
3310 }
3311
3312 let body_span = SourceSpan {
3313 start: body_start,
3314 end: body_end,
3315 };
3316 let (text, span) = trimmed_source_text(self.source_text(body_span), body_span);
3317 Some(TableRow {
3318 body: BlockSource { text, span },
3319 span: SourceSpan {
3320 start: open.span.start,
3321 end: close_end,
3322 },
3323 })
3324 }
3325
3326 fn parse_coerce(&mut self) -> Option<CoerceDecl> {
3327 let start = self.expect_keyword("coerce")?.span.start;
3328 let name = self.expect_ident("coerce name")?;
3329 let params = self.parse_param_list()?;
3330 self.expect_thin_arrow()?;
3331 let output = self.parse_type()?;
3332 if !self.at_symbol('{') {
3337 if let Some(TokenKind::String(_)) = self.peek().map(|token| &token.kind) {
3338 let token = self.advance().clone();
3339 let raw = self
3340 .source_text(SourceSpan {
3341 start: token.span.start,
3342 end: token.span.end,
3343 })
3344 .to_owned();
3345 let body = BlockSource {
3346 text: format!("prompt {raw}"),
3347 span: token.span,
3348 };
3349 let span = SourceSpan {
3350 start,
3351 end: body.span.end,
3352 };
3353 return Some(CoerceDecl {
3354 name,
3355 params,
3356 output,
3357 body,
3358 span,
3359 });
3360 }
3361 }
3362 let body = self.parse_block_source()?;
3363 let span = SourceSpan {
3364 start,
3365 end: body.span.end,
3366 };
3367 Some(CoerceDecl {
3368 name,
3369 params,
3370 output,
3371 body,
3372 span,
3373 })
3374 }
3375
3376 fn parse_param_list(&mut self) -> Option<Vec<ParamDecl>> {
3377 self.expect_symbol('(')?;
3378 let mut params = Vec::new();
3379
3380 while !self.is_at_end() && !self.at_symbol(')') {
3381 let name = self.expect_ident("parameter name")?;
3382 let ty = self.parse_type()?;
3383 params.push(ParamDecl {
3384 span: name.span.join(ty.span()),
3385 name,
3386 ty,
3387 });
3388
3389 if self.at_symbol(',') {
3390 self.advance();
3391 } else if !self.at_symbol(')') {
3392 self.unexpected("`,` or `)`");
3393 while !self.is_at_end() && !self.at_symbol(')') && !self.at_symbol(',') {
3394 self.advance();
3395 }
3396 }
3397 }
3398
3399 self.expect_symbol(')')?;
3400 Some(params)
3401 }
3402
3403 fn parse_action(&mut self) -> Option<ActionDecl> {
3406 let start = self.expect_keyword("action")?.span.start;
3407 let name = self.expect_ident("action name")?;
3408 self.expect_symbol('(')?;
3409 let mut params = Vec::new();
3410 while !self.is_at_end() && !self.at_symbol(')') {
3411 let param_name = self.expect_ident("action parameter name")?;
3412 let ty = self.parse_type()?;
3413 let span = param_name.span.join(ty.span());
3414 params.push(ActionParam {
3415 name: param_name,
3416 ty,
3417 span,
3418 });
3419 if self.at_symbol(',') {
3420 self.advance();
3421 }
3422 }
3423 self.expect_symbol(')')?;
3424 let body = self.parse_block_source()?;
3425 let span = SourceSpan {
3426 start,
3427 end: body.span.end,
3428 };
3429 Some(ActionDecl {
3430 name,
3431 params,
3432 body,
3433 span,
3434 })
3435 }
3436
3437 fn parse_rule(
3438 &mut self,
3439 tags: Vec<TagDecl>,
3440 description: Option<StringLiteral>,
3441 ) -> Option<RuleDecl> {
3442 let start = self.expect_keyword("rule")?.span.start;
3443 let name = self.expect_ident("rule name")?;
3444 let mut whens = Vec::new();
3445
3446 while !self.is_at_end() && !self.at_arrow() {
3447 if self.at_ident("when") {
3448 whens.extend(self.parse_when_clauses()?);
3449 } else if self.at_ident("with") {
3450 let span = self
3451 .peek()
3452 .map(|token| token.span)
3453 .unwrap_or(SourceSpan { start, end: start });
3454 self.diagnostics.push(Diagnostic {
3455 related: Vec::new(),
3456 span,
3457 message: "`with` is not a rule readiness clause".to_owned(),
3458 suggestion: Some("use `when` for rule conditions".to_owned()),
3459 });
3460 self.advance();
3461 } else {
3462 self.unexpected("`when` clause or `=>`");
3463 self.advance();
3464 }
3465 }
3466
3467 self.expect_arrow()?;
3468 let body = self.parse_block_source()?;
3469 let span = SourceSpan {
3470 start,
3471 end: body.span.end,
3472 };
3473 Some(RuleDecl {
3474 name,
3475 tags,
3476 description,
3477 whens,
3478 body,
3479 span,
3480 })
3481 }
3482
3483 fn parse_when_clauses(&mut self) -> Option<Vec<WhenClause>> {
3484 let when = self.expect_keyword("when")?;
3485 if self.at_symbol('{') {
3486 return self.parse_grouped_when_clauses(when.span);
3487 }
3488
3489 Some(vec![self.parse_when_clause_after_keyword(when.span)?])
3490 }
3491
3492 fn parse_assert(
3493 &mut self,
3494 tags: Vec<TagDecl>,
3495 description: Option<StringLiteral>,
3496 ) -> Option<AssertDecl> {
3497 let assert = self.expect_keyword("assert")?;
3498 let expr_start = assert.span.end;
3499 let line_end = self.source[expr_start..]
3500 .find('\n')
3501 .map(|offset| expr_start + offset)
3502 .unwrap_or(self.source.len());
3503 let mut expr_end = line_end;
3504
3505 while !self.is_at_end() && self.peek()?.span.start < line_end {
3506 expr_end = self.peek()?.span.end.min(line_end);
3507 self.advance();
3508 }
3509 expr_end = Self::extend_span_over_skipped_operators(self.source, expr_end, line_end);
3510
3511 let span = SourceSpan {
3512 start: expr_start,
3513 end: expr_end,
3514 };
3515 let (expr, span) = trimmed_source_text(self.source_text(span), span);
3516 Some(AssertDecl {
3517 tags,
3518 description,
3519 expr,
3520 span,
3521 })
3522 }
3523
3524 fn parse_when_clause_after_keyword(&mut self, when: SourceSpan) -> Option<WhenClause> {
3525 self.parse_when_clause_with_stop(when, false)
3526 }
3527
3528 fn parse_when_clause_with_stop(
3530 &mut self,
3531 when: SourceSpan,
3532 stop_at_brace: bool,
3533 ) -> Option<WhenClause> {
3534 let text_start = when.end;
3535 let mut text_end = text_start;
3536
3537 while !(self.is_at_end()
3538 || self.at_arrow()
3539 || self.at_ident("when")
3540 || self.at_ident("rule")
3541 || stop_at_brace && self.at_symbol('{'))
3542 {
3543 text_end = self.peek()?.span.end;
3544 self.advance();
3545 }
3546 let limit = self
3547 .peek()
3548 .map(|token| token.span.start)
3549 .unwrap_or(self.source.len());
3550 text_end = Self::extend_span_over_skipped_operators(self.source, text_end, limit);
3551
3552 let span = SourceSpan {
3553 start: text_start,
3554 end: text_end,
3555 };
3556 let (text, span) = trimmed_source_text(self.source_text(span), span);
3557 Some(WhenClause { text, span })
3558 }
3559
3560 fn extend_span_over_skipped_operators(source: &str, mut end: usize, limit: usize) -> usize {
3570 let bytes = source.as_bytes();
3571 loop {
3572 let mut cursor = end;
3573 while cursor < limit && bytes[cursor].is_ascii_whitespace() && bytes[cursor] != b'\n' {
3574 cursor += 1;
3575 }
3576 let width = match (bytes.get(cursor), bytes.get(cursor + 1)) {
3577 _ if cursor >= limit => break,
3578 (Some(b'='), Some(b'='))
3579 | (Some(b'!'), Some(b'='))
3580 | (Some(b'<'), Some(b'='))
3581 | (Some(b'>'), Some(b'='))
3582 | (Some(b'&'), Some(b'&'))
3583 | (Some(b'|'), Some(b'|')) => 2,
3584 (Some(b'/'), Some(b'/')) => break,
3585 (Some(b'-'), Some(b'>')) => break,
3586 (Some(b'*' | b'/' | b'-'), _) => 1,
3587 _ => break,
3588 };
3589 if cursor + width > limit {
3590 break;
3591 }
3592 end = cursor + width;
3593 }
3594 end
3595 }
3596
3597 fn parse_grouped_when_clauses(&mut self, when: SourceSpan) -> Option<Vec<WhenClause>> {
3598 let open = self.expect_symbol('{')?;
3599 let body_start = open.span.end;
3600 let mut depth = 1usize;
3601 let mut body_end = body_start;
3602 let mut close_end = open.span.end;
3603
3604 while !self.is_at_end() {
3605 let token = self.advance().clone();
3606 match token.kind {
3607 TokenKind::Symbol('{') => {
3608 depth += 1;
3609 body_end = token.span.end;
3610 }
3611 TokenKind::Symbol('}') => {
3612 depth -= 1;
3613 if depth == 0 {
3614 body_end = token.span.start;
3615 close_end = token.span.end;
3616 break;
3617 }
3618 body_end = token.span.end;
3619 }
3620 _ => body_end = token.span.end,
3621 }
3622 }
3623
3624 if depth != 0 {
3625 self.diagnostics.push(Diagnostic {
3626 related: Vec::new(),
3627 span: SourceSpan {
3628 start: when.start,
3629 end: body_end,
3630 },
3631 message: "unterminated grouped `when` block".to_owned(),
3632 suggestion: Some("close the grouped readiness block with `}`".to_owned()),
3633 });
3634 return Some(Vec::new());
3635 }
3636
3637 let body_span = SourceSpan {
3638 start: body_start,
3639 end: body_end,
3640 };
3641 let mut clauses = Vec::new();
3642 let mut offset = 0usize;
3643 for line in self.source_text(body_span).split_inclusive('\n') {
3644 let line_without_newline = line.trim_end_matches('\n');
3645 let line_start = body_span.start + offset;
3646 offset += line.len();
3647 let leading = line_without_newline.len() - line_without_newline.trim_start().len();
3648 let trailing = line_without_newline.len() - line_without_newline.trim_end().len();
3649 let trimmed_start = line_start + leading;
3650 let trimmed_end = line_start + line_without_newline.len().saturating_sub(trailing);
3651 if trimmed_start >= trimmed_end {
3652 continue;
3653 }
3654 clauses.push(WhenClause {
3655 text: self.source[trimmed_start..trimmed_end].to_owned(),
3656 span: SourceSpan {
3657 start: trimmed_start,
3658 end: trimmed_end,
3659 },
3660 });
3661 }
3662
3663 if clauses.is_empty() {
3664 self.diagnostics.push(Diagnostic {
3665 related: Vec::new(),
3666 span: SourceSpan {
3667 start: when.start,
3668 end: close_end,
3669 },
3670 message: "grouped `when` block has no readiness clauses".to_owned(),
3671 suggestion: Some(
3672 "add one condition per line, such as `started` or `Class as binding`"
3673 .to_owned(),
3674 ),
3675 });
3676 }
3677
3678 Some(clauses)
3679 }
3680
3681 fn parse_block_source(&mut self) -> Option<BlockSource> {
3682 let open = self.expect_symbol('{')?;
3683 let body_start = open.span.end;
3684 let mut depth = 1usize;
3685 let mut body_end = body_start;
3686
3687 while !self.is_at_end() {
3688 let token = self.advance().clone();
3689 match token.kind {
3690 TokenKind::Symbol('{') => {
3691 depth += 1;
3692 body_end = token.span.end;
3693 }
3694 TokenKind::Symbol('}') => {
3695 depth -= 1;
3696 if depth == 0 {
3697 body_end = token.span.start;
3698 return Some(BlockSource {
3699 text: self
3700 .source_text(SourceSpan {
3701 start: body_start,
3702 end: body_end,
3703 })
3704 .trim()
3705 .to_owned(),
3706 span: SourceSpan {
3707 start: open.span.start,
3708 end: token.span.end,
3709 },
3710 });
3711 }
3712 body_end = token.span.end;
3713 }
3714 _ => {
3715 body_end = token.span.end;
3716 }
3717 }
3718 }
3719
3720 self.diagnostics.push(Diagnostic {
3721 related: Vec::new(),
3722 span: SourceSpan {
3723 start: open.span.start,
3724 end: body_end,
3725 },
3726 message: "unterminated block".to_owned(),
3727 suggestion: Some("add a closing `}`".to_owned()),
3728 });
3729 Some(BlockSource {
3730 text: self
3731 .source_text(SourceSpan {
3732 start: body_start,
3733 end: body_end,
3734 })
3735 .trim()
3736 .to_owned(),
3737 span: SourceSpan {
3738 start: open.span.start,
3739 end: body_end,
3740 },
3741 })
3742 }
3743
3744 fn parse_type(&mut self) -> Option<TypeSyntax> {
3745 let first = self.parse_type_atom()?;
3746 let first = self.parse_type_suffixes(first);
3747
3748 if !self.at_symbol('|') {
3749 return Some(first);
3750 }
3751
3752 let start = first.span().start;
3753 let mut end = first.span().end;
3754 let mut variants = vec![first];
3755
3756 while self.at_symbol('|') {
3757 self.advance();
3758 let variant = self.parse_type_atom()?;
3759 let variant = self.parse_type_suffixes(variant);
3760 end = variant.span().end;
3761 variants.push(variant);
3762 }
3763
3764 Some(TypeSyntax::Union {
3765 variants,
3766 span: SourceSpan { start, end },
3767 })
3768 }
3769
3770 fn parse_type_atom(&mut self) -> Option<TypeSyntax> {
3771 Some(if self.at_ident("AgentRef") {
3772 let agent_ref = self.advance().clone();
3773 self.expect_symbol('<')?;
3774 let mut agents = Vec::new();
3775 while !self.is_at_end() && !self.at_symbol('>') {
3776 if self.at_symbol('|') {
3777 self.advance();
3778 continue;
3779 }
3780 let Some(agent) = self.expect_ident("agent reference") else {
3781 break;
3782 };
3783 agents.push(agent);
3784 }
3785 let close = self.expect_symbol('>')?;
3786 TypeSyntax::AgentRef {
3787 agents,
3788 span: agent_ref.span.join(close.span),
3789 }
3790 } else if self.at_ident("map") {
3791 let map = self.advance().clone();
3792 self.expect_symbol('<')?;
3793 let inner = self.parse_type()?;
3794 let close = self.expect_symbol('>')?;
3795 TypeSyntax::Map {
3796 span: map.span.join(close.span),
3797 inner: Box::new(inner),
3798 }
3799 } else if matches!(
3800 self.peek().map(|token| &token.kind),
3801 Some(TokenKind::String(_))
3802 ) {
3803 let literal = self.expect_string("literal type")?;
3804 TypeSyntax::LiteralString {
3805 value: literal.value,
3806 span: literal.span,
3807 }
3808 } else {
3809 let ident = self.expect_ident("type name")?;
3810 if is_primitive_type(&ident.name) {
3811 TypeSyntax::Primitive {
3812 name: ident.name,
3813 span: ident.span,
3814 }
3815 } else {
3816 TypeSyntax::Ref { name: ident }
3817 }
3818 })
3819 }
3820
3821 fn parse_type_suffixes(&mut self, mut ty: TypeSyntax) -> TypeSyntax {
3822 loop {
3823 if self.at_symbol('?') {
3824 let question = self.advance().clone();
3825 ty = TypeSyntax::Optional {
3826 span: ty.span().join(question.span),
3827 inner: Box::new(ty),
3828 };
3829 } else if self.at_symbol('[') {
3830 self.advance();
3831 let Some(close) = self.expect_symbol(']') else {
3832 return ty;
3833 };
3834 ty = TypeSyntax::Array {
3835 span: ty.span().join(close.span),
3836 inner: Box::new(ty),
3837 };
3838 } else {
3839 return ty;
3840 }
3841 }
3842 }
3843
3844 fn parse_string_list(&mut self) -> Option<(Vec<StringLiteral>, SourceSpan)> {
3845 let open = self.expect_symbol('[')?;
3846 let mut values = Vec::new();
3847
3848 while !self.is_at_end() && !self.at_symbol(']') {
3849 values.push(self.expect_string("skill string")?);
3850 if self.at_symbol(',') {
3851 self.advance();
3852 } else if !self.at_symbol(']') {
3853 self.unexpected("`,` or `]`");
3854 self.synchronize_to_block_item();
3855 break;
3856 }
3857 }
3858
3859 let close = self.expect_symbol(']')?;
3860 Some((values, open.span.join(close.span)))
3861 }
3862
3863 fn parse_ident_list(&mut self) -> Option<(Vec<Ident>, SourceSpan)> {
3866 let open = self.expect_symbol('[')?;
3867 let mut values = Vec::new();
3868
3869 while !self.is_at_end() && !self.at_symbol(']') {
3870 values.push(self.expect_ident("tool workflow name")?);
3871 if self.at_symbol(',') {
3872 self.advance();
3873 } else if !self.at_symbol(']') {
3874 self.unexpected("`,` or `]`");
3875 self.synchronize_to_block_item();
3876 break;
3877 }
3878 }
3879
3880 let close = self.expect_symbol(']')?;
3881 Some((values, open.span.join(close.span)))
3882 }
3883
3884 fn parse_feature_class_list(&mut self) -> Option<(Vec<Ident>, SourceSpan)> {
3889 let open = self.expect_symbol('[')?;
3890 let mut values = Vec::new();
3891
3892 while !self.is_at_end() && !self.at_symbol(']') {
3893 let head = self.expect_ident("feature class")?;
3894 let mut name = head.name.clone();
3895 let mut span = head.span;
3896 while self.at_symbol('.') {
3897 self.advance();
3898 let part = self.expect_ident("feature class segment")?;
3899 name.push('.');
3900 name.push_str(&part.name);
3901 span = span.join(part.span);
3902 }
3903 values.push(Ident { name, span });
3904 if self.at_symbol(',') {
3905 self.advance();
3906 } else if !self.at_symbol(']') {
3907 self.unexpected("`,` or `]`");
3908 self.synchronize_to_block_item();
3909 break;
3910 }
3911 }
3912
3913 let close = self.expect_symbol(']')?;
3914 Some((values, open.span.join(close.span)))
3915 }
3916
3917 fn expect_keyword(&mut self, keyword: &str) -> Option<Token> {
3918 if self.at_ident(keyword) {
3919 Some(self.advance().clone())
3920 } else {
3921 self.expected(format!("`{keyword}`"));
3922 None
3923 }
3924 }
3925
3926 fn expect_ident(&mut self, label: &str) -> Option<Ident> {
3927 let token = self.peek()?;
3928 if let TokenKind::Ident(name) = &token.kind {
3929 let ident = Ident {
3930 name: name.clone(),
3931 span: token.span,
3932 };
3933 self.advance();
3934 Some(ident)
3935 } else {
3936 self.expected(label);
3937 None
3938 }
3939 }
3940
3941 fn parse_field_presence_condition(&mut self) -> Option<(String, String)> {
3947 if !self.at_ident("when") {
3948 return None;
3949 }
3950 self.advance(); let disc = self.expect_ident("discriminant field name after `when`")?;
3952 if self.at_ident("is") {
3953 self.advance();
3954 } else {
3955 self.expected("`is` after the discriminant field");
3956 return None;
3957 }
3958 let literal = self.expect_string("discriminant literal value")?;
3959 Some((disc.name, literal.value))
3960 }
3961
3962 fn expect_string(&mut self, label: &str) -> Option<StringLiteral> {
3963 let token = self.peek()?;
3964 if let TokenKind::String(value) = &token.kind {
3965 let literal = StringLiteral {
3966 value: value.clone(),
3967 span: token.span,
3968 };
3969 self.advance();
3970 Some(literal)
3971 } else {
3972 self.expected(label);
3973 None
3974 }
3975 }
3976
3977 fn expect_use_name(&mut self, label: &str) -> Option<StringLiteral> {
3978 let token = self.peek()?;
3979 match &token.kind {
3980 TokenKind::Ident(value) => {
3983 let mut name = value.clone();
3984 let mut span = token.span;
3985 self.advance();
3986 while self.at_symbol('.') {
3987 self.expect_symbol('.');
3988 let Some(segment) = self.expect_ident("package name segment") else {
3989 break;
3990 };
3991 name.push('.');
3992 name.push_str(&segment.name);
3993 span = span.join(segment.span);
3994 }
3995 Some(StringLiteral { value: name, span })
3996 }
3997 TokenKind::String(value) => {
3998 let literal = StringLiteral {
3999 value: value.clone(),
4000 span: token.span,
4001 };
4002 self.advance();
4003 Some(literal)
4004 }
4005 _ => {
4006 self.expected(label);
4007 None
4008 }
4009 }
4010 }
4011
4012 fn expect_u32(&mut self, label: &str) -> Option<(u32, SourceSpan)> {
4013 let token = self.peek()?;
4014 if let TokenKind::Number(value) = &token.kind {
4015 let span = token.span;
4016 let parsed = value.parse::<u32>();
4017 self.advance();
4018 match parsed {
4019 Ok(value) => Some((value, span)),
4020 Err(_) => {
4021 self.diagnostics.push(Diagnostic {
4022 related: Vec::new(),
4023 span,
4024 message: format!("{label} must fit in u32"),
4025 suggestion: Some("use a non-negative integer such as `1`".to_owned()),
4026 });
4027 None
4028 }
4029 }
4030 } else {
4031 self.expected(label);
4032 None
4033 }
4034 }
4035
4036 fn expect_symbol(&mut self, symbol: char) -> Option<Token> {
4037 if self.at_symbol(symbol) {
4038 Some(self.advance().clone())
4039 } else {
4040 self.expected(format!("`{symbol}`"));
4041 None
4042 }
4043 }
4044
4045 fn expect_arrow(&mut self) -> Option<Token> {
4046 if self.at_arrow() {
4047 Some(self.advance().clone())
4048 } else {
4049 self.expected("`=>`");
4050 None
4051 }
4052 }
4053
4054 fn expect_thin_arrow(&mut self) -> Option<Token> {
4055 if self.at_thin_arrow() {
4056 Some(self.advance().clone())
4057 } else {
4058 self.expected("`->`");
4059 None
4060 }
4061 }
4062
4063 fn at_ident(&self, expected: &str) -> bool {
4064 matches!(self.peek().map(|token| &token.kind), Some(TokenKind::Ident(value)) if value == expected)
4065 }
4066
4067 fn consume_ident(&mut self, expected: &str) -> bool {
4068 if self.at_ident(expected) {
4069 self.advance();
4070 true
4071 } else {
4072 false
4073 }
4074 }
4075
4076 fn at_symbol(&self, expected: char) -> bool {
4077 matches!(self.peek().map(|token| &token.kind), Some(TokenKind::Symbol(value)) if *value == expected)
4078 }
4079
4080 fn at_arrow(&self) -> bool {
4081 matches!(self.peek().map(|token| &token.kind), Some(TokenKind::Arrow))
4082 }
4083
4084 fn at_thin_arrow(&self) -> bool {
4085 matches!(
4086 self.peek().map(|token| &token.kind),
4087 Some(TokenKind::ThinArrow)
4088 )
4089 }
4090
4091 fn peek(&self) -> Option<&Token> {
4092 self.tokens.get(self.pos)
4093 }
4094
4095 fn advance(&mut self) -> &Token {
4096 let index = self.pos;
4097 self.pos += 1;
4098 &self.tokens[index]
4099 }
4100
4101 fn is_at_end(&self) -> bool {
4102 self.pos >= self.tokens.len()
4103 }
4104
4105 fn expected(&mut self, expected: impl fmt::Display) {
4106 let expected = expected.to_string();
4107 let (span, found) = match self.peek() {
4108 Some(token) => (token.span, token.kind.label()),
4109 None => (
4110 SourceSpan {
4111 start: self.source.len(),
4112 end: self.source.len(),
4113 },
4114 "end of file".to_owned(),
4115 ),
4116 };
4117 self.diagnostics.push(Diagnostic {
4118 related: Vec::new(),
4119 span,
4120 message: format!("expected {expected}, found {found}"),
4121 suggestion: suggestion_for_expected(&expected),
4122 });
4123 }
4124
4125 fn unexpected(&mut self, expected: impl fmt::Display) {
4126 let Some(token) = self.peek() else {
4127 self.expected(expected);
4128 return;
4129 };
4130 let expected = expected.to_string();
4131 self.diagnostics.push(Diagnostic {
4132 related: Vec::new(),
4133 span: token.span,
4134 message: format!("expected {expected}, found {}", token.kind.label()),
4135 suggestion: suggestion_for_expected(&expected),
4136 });
4137 }
4138
4139 fn synchronize_to_block_item(&mut self) {
4140 while !self.is_at_end() {
4141 if self.at_symbol('}')
4142 || self.at_ident("profile")
4143 || self.at_ident("provider")
4144 || self.at_ident("capacity")
4145 || self.at_ident("skills")
4146 || self.at_ident("capabilities")
4147 || self.at_ident("tools")
4148 || self.at_ident("compaction")
4149 || self.at_ident("settings")
4150 {
4151 return;
4152 }
4153 self.advance();
4154 }
4155 }
4156
4157 fn synchronize_to_table_row(&mut self) {
4158 while !self.is_at_end() {
4159 if self.at_symbol('{') || self.at_symbol(']') {
4160 return;
4161 }
4162 self.advance();
4163 }
4164 }
4165
4166 fn source_text(&self, span: SourceSpan) -> &str {
4167 &self.source[span.start..span.end]
4168 }
4169}
4170
4171pub(crate) fn trimmed_source_text(source: &str, span: SourceSpan) -> (String, SourceSpan) {
4172 let leading = source.len() - source.trim_start().len();
4173 let trailing = source.len() - source.trim_end().len();
4174 let end = source.len().saturating_sub(trailing);
4175 if leading > end {
4176 return (
4177 String::new(),
4178 SourceSpan {
4179 start: span.end,
4180 end: span.end,
4181 },
4182 );
4183 }
4184 (
4185 source[leading..end].to_owned(),
4186 SourceSpan {
4187 start: span.start + leading,
4188 end: span.start + end,
4189 },
4190 )
4191}
4192
4193pub(crate) fn is_primitive_type(name: &str) -> bool {
4194 matches!(
4195 name,
4196 "string"
4197 | "int"
4198 | "float"
4199 | "bool"
4200 | "null"
4201 | "duration"
4202 | "time"
4203 | "image"
4204 | "audio"
4205 | "pdf"
4206 | "video"
4207 | "secret"
4208 )
4209}
4210
4211pub(crate) fn is_gherkin_keyword(keyword: &str) -> bool {
4212 matches!(
4213 keyword,
4214 "Feature"
4215 | "Rule"
4216 | "Background"
4217 | "Scenario"
4218 | "ScenarioOutline"
4219 | "Scenario-Outline"
4220 | "Examples"
4221 | "Given"
4222 | "When"
4223 | "Then"
4224 | "And"
4225 | "But"
4226 )
4227}
4228
4229pub(crate) fn suggestion_for_expected(expected: &str) -> Option<String> {
4230 match expected {
4231 "`{`" => Some("add a `{ ... }` block".to_owned()),
4232 "`=>`" => Some("add `=> { ... }` after the rule conditions".to_owned()),
4233 "`->`" => Some("add `-> OutputType` before the coerce prompt block".to_owned()),
4234 "profile string" => Some("write `profile \"profile-name\"`".to_owned()),
4235 "capacity value" => Some("write `capacity 1`".to_owned()),
4236 "package library name" => Some("write a package library name, such as `memory`".to_owned()),
4237 "type name" => Some("write a primitive type or schema name".to_owned()),
4238 _ => None,
4239 }
4240}
4241
4242pub fn parse_program(source: &str) -> ParseOutput {
4244 let lexed = lex(source);
4245 let mut parser = Parser {
4246 source,
4247 tokens: lexed.tokens,
4248 pos: 0,
4249 diagnostics: lexed.diagnostics,
4250 pending_contract_classes: Vec::new(),
4251 };
4252
4253 let program = parser.parse_program();
4254 ParseOutput {
4255 program,
4256 diagnostics: parser.diagnostics,
4257 }
4258}