1use std::collections::HashMap;
28
29use super::ast::{
30 AboutClause, AccumulateStmt, AccumulateTarget, AddStmt, AddWithOption, AddWorkflowStmt,
31 AliasedFormat, AssembleStmt, AssembleWithOption, BatchEntry, BatchStmt, BetweenClause,
32 BindClause, BudgetSpec, BudgetUnit, CalQuery, CalStatement, CalVersion, CoalesceBranch,
33 CoalesceStmt, Comparator, Condition, ContradictionsClause, DefineTemplateStmt, DeltaOp,
34 DescribeStmt, DescribeTarget, ExistsStmt, ExplainStmt, Extractor, FieldAssignment,
35 FormatClause, FormatSpec, GrainTypePlural, GrainTypeSingular, GraphEdge, HistoryStmt,
36 LetBinding, LikeClause, NamedSource, PipelineStage, PrioritySpec, ProjectField, RecallStmt,
37 RecentClause, RevertStmt, SetOp, SetOpStmt, Source, SupersedeStmt, SupersedeWorkflowStmt,
38 UntilClause, Value, WhereClause, WithOption,
39};
40use super::ast::{
41 DefineQueryStmt, DerivedFromStmt, DropQueryStmt, EntityAtStmt, ForgetStmt, ForgetTarget,
42 GovernanceStmt, GrantStmt, MergeStmt, NoveltyStmt, PurgeStmt, QueryParam, RelatedStmt,
43 RememberStmt, ReportSubjectStmt, RevokeStmt, RunLoopStmt, RunQueryStmt, RunTraceStmt,
44 RunsTouchingStmt, ShowForksStmt, ShowGrantsStmt, TemplateSectionSources,
45};
46use super::errors::{CalError, CalResult, CalWarning, Span};
47use super::lexer::{is_destructive_keyword, Lexer, SectionBody, SpannedToken, Token};
48
49type DclBody = (Vec<String>, Vec<String>, String, Option<String>);
52
53fn parse_stale_duration_ms(s: &str) -> Option<i64> {
56 let s = s.trim();
57 if s.len() < 2 {
58 return None;
59 }
60 let (num, unit) = s.split_at(s.len() - 1);
61 let n: f64 = num.parse().ok()?;
62 if n < 0.0 || !n.is_finite() {
63 return None;
64 }
65 let ms = match unit {
66 "s" => n * 1000.0,
67 "m" => n * 60_000.0,
68 "h" => n * 3_600_000.0,
69 "d" => n * 86_400_000.0,
70 _ => return None,
71 };
72 Some(ms as i64)
73}
74
75pub(crate) const MAX_QUERY_LENGTH: usize = 65_536;
84
85pub(crate) const MAX_NESTING_DEPTH: usize = 8;
90
91const MAX_LIMIT_VALUE: u64 = 1_000;
93
94const MAX_IN_SET_SIZE: usize = 100;
96
97const MAX_PIPELINE_STAGES: usize = 5;
99
100const MAX_SET_OPERANDS: usize = 4;
102
103const MAX_BATCH_ENTRIES: usize = 10;
105
106const MAX_REASON_LENGTH: usize = 500;
108
109pub fn parse(input: &str) -> CalResult<CalQuery> {
123 parse_with_params(input, &HashMap::new())
124}
125
126pub fn parse_with_params(input: &str, _params: &HashMap<String, Value>) -> CalResult<CalQuery> {
134 if input.len() > MAX_QUERY_LENGTH {
136 return Err(CalError::QueryTooLong {
137 length: input.len(),
138 max: MAX_QUERY_LENGTH,
139 span: None,
140 });
141 }
142
143 let trimmed = input.trim();
144 if trimmed.is_empty() {
145 return Err(CalError::EmptyQuery { span: None });
146 }
147
148 let tokens = Lexer::tokenize(input)?;
150
151 if tokens.is_empty() {
152 return Err(CalError::EmptyQuery {
153 span: Some(Span::zero()),
154 });
155 }
156
157 let mut parser = Parser::new(tokens, input);
158 parser.parse_query()
159}
160
161fn params_as_literals(body: &str) -> String {
169 let mut out = String::with_capacity(body.len());
170 let mut chars = body.chars().peekable();
171 let mut quote: Option<char> = None;
172 while let Some(c) = chars.next() {
173 match quote {
174 Some(q) => {
175 out.push(c);
176 if c == '\\' {
177 if let Some(n) = chars.next() {
179 out.push(n);
180 }
181 } else if c == q {
182 quote = None;
183 }
184 }
185 None if c == '"' || c == '\'' => {
186 quote = Some(c);
187 out.push(c);
188 }
189 None if c == '$' => {
190 while chars.peek().is_some_and(|n| n.is_ascii_alphanumeric() || *n == '_') {
191 chars.next();
192 }
193 out.push('1');
197 }
198 None => out.push(c),
199 }
200 }
201 out
202}
203
204pub(crate) fn check_read_only_statement(stmt: &CalStatement, span: &Span) -> CalResult<()> {
211 let write = |s: &str| {
212 Err(CalError::WriteInQueryBody {
213 stmt: s.into(),
214 span: Some(*span),
215 })
216 };
217 match stmt {
218 CalStatement::Recall(_)
219 | CalStatement::SetOp(_)
220 | CalStatement::Exists(_)
221 | CalStatement::Assemble(_)
222 | CalStatement::History(_)
223 | CalStatement::Explain(_)
224 | CalStatement::Describe(_)
225 | CalStatement::Coalesce(_) => Ok(()),
226 CalStatement::Batch(batch) => {
227 for entry in &batch.statements {
228 check_read_only_statement(&entry.statement, span)?;
229 }
230 if let Some(labeled) = &batch.labeled {
231 for (_, entry) in labeled {
232 check_read_only_statement(&entry.statement, span)?;
233 }
234 }
235 Ok(())
236 }
237 CalStatement::Add(_) | CalStatement::AddWorkflow(_) => write("ADD"),
238 CalStatement::Supersede(_) | CalStatement::SupersedeWorkflow(_) => write("SUPERSEDE"),
239 CalStatement::Accumulate(_) => write("ACCUMULATE"),
240 CalStatement::Revert(_) => write("REVERT"),
241 CalStatement::Remember(_) => write("REMEMBER"),
242 CalStatement::EntityAt(_)
243 | CalStatement::RunTrace(_)
244 | CalStatement::RunsTouching(_)
245 | CalStatement::DerivedFrom(_)
246 | CalStatement::ShowForks(_)
247 | CalStatement::Related(_)
248 | CalStatement::Novelty(_)
249 | CalStatement::ReportSubject(_) => Ok(()),
250 CalStatement::Merge(_) => write("MERGE"),
251 CalStatement::Forget(_) => write("FORGET"),
252 CalStatement::Purge(_) => write("PURGE"),
253 CalStatement::Grant(_) => write("GRANT"),
257 CalStatement::Revoke(_) => write("REVOKE"),
258 CalStatement::ShowGrants(_) => Ok(()),
259 CalStatement::Approve(_) => write("APPROVE"),
262 CalStatement::Reject(_) => write("REJECT"),
263 CalStatement::ApplyRec(_) => write("APPLY"),
264 CalStatement::RollbackRec(_) => write("ROLLBACK"),
265 CalStatement::RunLoop(_) => write("RUN LOOP"),
266 CalStatement::DefineTemplate(_) => write("DEFINE TEMPLATE"),
267 CalStatement::DropTemplate(_) => write("DROP TEMPLATE"),
268 CalStatement::DefineQuery(_) => write("DEFINE QUERY"),
269 CalStatement::DropQuery(_) => write("DROP QUERY"),
270 CalStatement::RunQuery(_) => Err(CalError::RecursiveQuery { span: Some(*span) }),
271 }
272}
273
274struct Parser {
280 tokens: Vec<SpannedToken>,
281 pos: usize,
282 warnings: Vec<CalWarning>,
283 nesting_depth: usize,
284 input: String,
286}
287
288impl Parser {
289 fn new(tokens: Vec<SpannedToken>, input: &str) -> Self {
290 Self {
291 tokens,
292 pos: 0,
293 warnings: vec![],
294 nesting_depth: 0,
295 input: input.to_string(),
296 }
297 }
298
299 fn peek(&self) -> Option<&SpannedToken> {
303 self.tokens.get(self.pos)
304 }
305
306 fn peek_ahead(&self, offset: usize) -> Option<&SpannedToken> {
308 self.tokens.get(self.pos + offset)
309 }
310
311 fn advance(&mut self) -> Option<&SpannedToken> {
313 if self.pos < self.tokens.len() {
314 let tok = &self.tokens[self.pos];
315 self.pos += 1;
316 Some(tok)
317 } else {
318 None
319 }
320 }
321
322 fn at_end(&self) -> bool {
323 self.pos >= self.tokens.len()
324 }
325
326 fn current_span(&self) -> Span {
328 self.peek().map(|st| st.span).unwrap_or_else(Span::zero)
329 }
330
331 fn prev_span(&self) -> Span {
333 if self.pos == 0 {
334 Span::zero()
335 } else {
336 self.tokens
337 .get(self.pos - 1)
338 .map(|st| st.span)
339 .unwrap_or_else(Span::zero)
340 }
341 }
342
343 #[allow(dead_code)] fn eat_if_token(&mut self, token: &Token) -> bool {
347 if let Some(st) = self.peek() {
348 if std::mem::discriminant(&st.token) == std::mem::discriminant(token) {
349 self.advance();
350 return true;
351 }
352 }
353 false
354 }
355
356 fn eat_exact(&mut self, token: &Token) -> bool {
358 if let Some(st) = self.peek() {
359 if &st.token == token {
360 self.advance();
361 return true;
362 }
363 }
364 false
365 }
366
367 fn expect_exact(&mut self, expected: &Token) -> CalResult<SpannedToken> {
370 match self.peek() {
371 Some(st) if &st.token == expected => {
372 let st = st.clone();
373 self.advance();
374 Ok(st)
375 }
376 Some(st) => {
377 let found = st.token.description();
378 let span = st.span;
379 Err(CalError::UnexpectedToken {
380 expected: expected.description(),
381 found,
382 span: Some(span),
383 suggestion: None,
384 })
385 }
386 None => Err(CalError::UnexpectedToken {
387 expected: expected.description(),
388 found: "<end of query>".into(),
389 span: None,
390 suggestion: None,
391 }),
392 }
393 }
394
395 #[allow(dead_code)] fn expect_token(&mut self, expected: &Token) -> CalResult<SpannedToken> {
398 match self.peek() {
399 Some(st) if std::mem::discriminant(&st.token) == std::mem::discriminant(expected) => {
400 let st = st.clone();
401 self.advance();
402 Ok(st)
403 }
404 Some(st) => {
405 let found = st.token.description();
406 let span = st.span;
407 Err(CalError::UnexpectedToken {
408 expected: expected.description(),
409 found,
410 span: Some(span),
411 suggestion: None,
412 })
413 }
414 None => Err(CalError::UnexpectedToken {
415 expected: expected.description(),
416 found: "<end of query>".into(),
417 span: None,
418 suggestion: None,
419 }),
420 }
421 }
422
423 fn at(&self, expected: &Token) -> bool {
426 match self.peek() {
427 Some(st) => std::mem::discriminant(&st.token) == std::mem::discriminant(expected),
428 None => false,
429 }
430 }
431
432 fn at_exact(&self, expected: &Token) -> bool {
434 self.peek().map(|st| &st.token == expected).unwrap_or(false)
435 }
436
437 fn enter_nesting(&mut self) -> CalResult<()> {
440 self.nesting_depth += 1;
441 if self.nesting_depth > MAX_NESTING_DEPTH {
442 return Err(CalError::NestingTooDeep {
443 depth: self.nesting_depth,
444 max: MAX_NESTING_DEPTH,
445 span: Some(self.current_span()),
446 });
447 }
448 Ok(())
449 }
450
451 fn leave_nesting(&mut self) {
452 self.nesting_depth = self.nesting_depth.saturating_sub(1);
453 }
454
455 fn parse_string_literal(&mut self) -> CalResult<String> {
458 match self.peek() {
459 Some(SpannedToken {
460 token: Token::StringLiteral(_),
461 ..
462 }) => {
463 let st = self.advance().unwrap();
464 if let Token::StringLiteral(s) = &st.token {
465 Ok(s.clone())
466 } else {
467 unreachable!()
468 }
469 }
470 Some(st) => {
471 let found = st.token.description();
472 let span = st.span;
473 Err(CalError::UnexpectedToken {
474 expected: "string literal".into(),
475 found,
476 span: Some(span),
477 suggestion: Some("string values must be enclosed in double quotes".into()),
478 })
479 }
480 None => Err(CalError::UnexpectedToken {
481 expected: "string literal".into(),
482 found: "<end of query>".into(),
483 span: None,
484 suggestion: None,
485 }),
486 }
487 }
488
489 fn parse_number(&mut self) -> CalResult<f64> {
490 match self.peek() {
491 Some(SpannedToken {
492 token: Token::NumberLiteral(_),
493 ..
494 }) => {
495 let st = self.advance().unwrap();
496 if let Token::NumberLiteral(n) = &st.token {
497 Ok(*n)
498 } else {
499 unreachable!()
500 }
501 }
502 Some(st) => {
503 let found = st.token.description();
504 let span = st.span;
505 Err(CalError::UnexpectedToken {
506 expected: "number literal".into(),
507 found,
508 span: Some(span),
509 suggestion: None,
510 })
511 }
512 None => Err(CalError::UnexpectedToken {
513 expected: "number literal".into(),
514 found: "<end of query>".into(),
515 span: None,
516 suggestion: None,
517 }),
518 }
519 }
520
521 fn parse_u64(&mut self) -> CalResult<u64> {
522 let span = self.current_span();
523 let n = self.parse_number()?;
524 if n < 0.0 || n.fract() != 0.0 {
525 return Err(CalError::InvalidNumber {
526 found: n.to_string(),
527 span: Some(span),
528 });
529 }
530 Ok(n as u64)
531 }
532
533 fn parse_hash_literal(&mut self) -> CalResult<String> {
534 match self.peek() {
535 Some(SpannedToken {
536 token: Token::HashLiteral(_),
537 ..
538 }) => {
539 let st = self.advance().unwrap();
540 if let Token::HashLiteral(h) = &st.token {
541 Ok(h.clone())
542 } else {
543 unreachable!()
544 }
545 }
546 Some(st) => {
547 let found = st.token.description();
548 let span = st.span;
549 Err(CalError::UnexpectedToken {
550 expected: "hash literal (sha256:...)".into(),
551 found,
552 span: Some(span),
553 suggestion: Some("hash literals use the format sha256:<hex digits>".into()),
554 })
555 }
556 None => Err(CalError::UnexpectedToken {
557 expected: "hash literal (sha256:...)".into(),
558 found: "<end of query>".into(),
559 span: None,
560 suggestion: None,
561 }),
562 }
563 }
564
565 fn parse_parameter(&mut self) -> CalResult<String> {
567 match self.peek() {
568 Some(SpannedToken {
569 token: Token::Parameter(_),
570 ..
571 }) => {
572 let st = self.advance().unwrap();
573 if let Token::Parameter(p) = &st.token {
574 Ok(p.clone())
575 } else {
576 unreachable!()
577 }
578 }
579 Some(st) => {
580 let found = st.token.description();
581 let span = st.span;
582 Err(CalError::UnexpectedToken {
583 expected: "$parameter".into(),
584 found,
585 span: Some(span),
586 suggestion: None,
587 })
588 }
589 None => Err(CalError::UnexpectedToken {
590 expected: "$parameter".into(),
591 found: "<end of query>".into(),
592 span: None,
593 suggestion: None,
594 }),
595 }
596 }
597
598 fn parse_value(&mut self) -> CalResult<Value> {
600 match self.peek() {
601 Some(SpannedToken {
602 token: Token::StringLiteral(_),
603 ..
604 }) => Ok(Value::String {
605 value: self.parse_string_literal()?,
606 }),
607 Some(SpannedToken {
608 token: Token::NumberLiteral(_),
609 ..
610 }) => Ok(Value::Number {
611 value: self.parse_number()?,
612 }),
613 Some(SpannedToken {
614 token: Token::True, ..
615 }) => {
616 self.advance();
617 Ok(Value::Boolean { value: true })
618 }
619 Some(SpannedToken {
620 token: Token::False,
621 ..
622 }) => {
623 self.advance();
624 Ok(Value::Boolean { value: false })
625 }
626 Some(SpannedToken {
627 token: Token::HashLiteral(_),
628 ..
629 }) => Ok(Value::Hash {
630 value: self.parse_hash_literal()?,
631 }),
632 Some(SpannedToken {
633 token: Token::Parameter(_),
634 ..
635 }) => Ok(Value::Parameter {
636 name: self.parse_parameter()?,
637 }),
638 Some(SpannedToken {
640 token: Token::LBracket,
641 ..
642 }) => {
643 self.advance(); self.enter_nesting()?;
645 let mut values = vec![];
646 while !self.at_exact(&Token::RBracket) && !self.at_end() {
647 values.push(self.parse_value()?);
648 if !self.eat_exact(&Token::Comma) {
649 break;
650 }
651 }
652 self.leave_nesting();
653 self.expect_exact(&Token::RBracket)?;
654 Ok(Value::Array { values })
655 }
656 Some(st) => {
657 let found = st.token.description();
658 let span = st.span;
659 Err(CalError::UnexpectedToken {
660 expected: "a value (string, number, boolean, hash, $parameter, or [array])"
661 .into(),
662 found,
663 span: Some(span),
664 suggestion: None,
665 })
666 }
667 None => Err(CalError::UnexpectedToken {
668 expected: "a value".into(),
669 found: "<end of query>".into(),
670 span: None,
671 suggestion: None,
672 }),
673 }
674 }
675
676 fn parse_value_list(&mut self) -> CalResult<Vec<Value>> {
678 self.expect_exact(&Token::LParen)?;
679 self.enter_nesting()?;
680 let mut values = vec![];
681 while !self.at_exact(&Token::RParen) && !self.at_end() {
682 if self.at(&Token::Parameter("".into())) {
684 let name = self.parse_parameter()?;
686 values.push(Value::Parameter { name });
687 } else {
688 values.push(self.parse_value()?);
689 }
690 if !self.eat_exact(&Token::Comma) {
691 break;
692 }
693 }
694 self.leave_nesting();
695 self.expect_exact(&Token::RParen)?;
696 if values.is_empty() {
698 return Err(CalError::UnexpectedToken {
699 expected: "at least one value inside IN ( ... )".into(),
700 found: "empty IN list".into(),
701 span: Some(self.prev_span()),
702 suggestion: Some("IN requires one or more values.".into()),
703 });
704 }
705 if values.len() > MAX_IN_SET_SIZE {
706 return Err(CalError::InSetTooLarge {
707 count: values.len(),
708 max: MAX_IN_SET_SIZE,
709 span: Some(self.prev_span()),
710 });
711 }
712 Ok(values)
713 }
714
715 fn parse_identifier(&mut self) -> CalResult<String> {
721 match self.peek() {
722 Some(SpannedToken {
723 token: Token::Ident(_),
724 ..
725 }) => {
726 let st = self.advance().unwrap();
727 if let Token::Ident(s) = &st.token {
728 if is_destructive_keyword(s) {
730 return Err(CalError::UnexpectedToken {
731 expected: "a field name or grain type".into(),
732 found: s.clone(),
733 span: Some(self.prev_span()),
734 suggestion: Some(
735 "CAL cannot destroy data in bulk. The only \
736 destructive CAL statement is FORGET <hash>; \
737 erasure, key rotation, and schema changes are \
738 host-level operations."
739 .into(),
740 ),
741 });
742 }
743 Ok(s.clone())
744 } else {
745 unreachable!()
746 }
747 }
748 Some(SpannedToken {
753 token: Token::On | Token::When | Token::Bind | Token::Priority | Token::Scope,
754 ..
755 }) => {
756 let st = self.advance().unwrap();
757 Ok(st.token.description().to_ascii_lowercase())
758 }
759 Some(st) => {
760 let found = st.token.description();
761 let span = st.span;
762 Err(CalError::UnexpectedToken {
763 expected: "identifier".into(),
764 found,
765 span: Some(span),
766 suggestion: None,
767 })
768 }
769 None => Err(CalError::UnexpectedToken {
770 expected: "identifier".into(),
771 found: "<end of query>".into(),
772 span: None,
773 suggestion: None,
774 }),
775 }
776 }
777
778 fn parse_label(&mut self) -> CalResult<String> {
782 match self.peek() {
783 Some(SpannedToken {
784 token: Token::Ident(_),
785 ..
786 }) => {
787 let st = self.advance().unwrap();
788 if let Token::Ident(s) = &st.token {
789 Ok(s.clone())
790 } else {
791 unreachable!()
792 }
793 }
794 Some(st) if Self::is_word_token(&st.token) => {
796 let label = st.token.description().to_ascii_lowercase();
797 self.advance();
798 Ok(label)
799 }
800 Some(st) => {
801 let found = st.token.description();
802 let span = st.span;
803 Err(CalError::UnexpectedToken {
804 expected: "label name".into(),
805 found,
806 span: Some(span),
807 suggestion: None,
808 })
809 }
810 None => Err(CalError::UnexpectedToken {
811 expected: "label name".into(),
812 found: "<end of query>".into(),
813 span: None,
814 suggestion: None,
815 }),
816 }
817 }
818
819 fn is_word_token(token: &Token) -> bool {
822 !matches!(
823 token,
824 Token::Eq
825 | Token::NotEq
826 | Token::Gte
827 | Token::Lte
828 | Token::Gt
829 | Token::Lt
830 | Token::LParen
831 | Token::RParen
832 | Token::LBracket
833 | Token::RBracket
834 | Token::LBrace
835 | Token::RBrace
836 | Token::Pipe
837 | Token::Comma
838 | Token::Colon
839 | Token::Dot
840 | Token::Slash
841 | Token::NumberLiteral(_)
842 | Token::StringLiteral(_)
843 | Token::HashLiteral(_)
844 | Token::Parameter(_)
845 )
846 }
847
848 fn parse_grain_type_plural_opt(&mut self) -> CalResult<Option<GrainTypePlural>> {
853 if let Some(SpannedToken {
855 token: Token::Ident(s),
856 span,
857 ..
858 }) = self.peek()
859 {
860 let s = s.clone();
861 let span = *span;
862 if let Some(gt) = GrainTypePlural::parse(&s) {
863 self.advance();
864 return Ok(Some(gt));
865 }
866 let suggestion = suggest_grain_type_plural(&s);
868 if suggestion.is_some() {
870 return Err(CalError::UnknownGrainType {
871 found: s,
872 span: Some(span),
873 suggestion,
874 });
875 }
876 }
877 if self.at_exact(&Token::All) {
879 self.advance();
880 return Ok(Some(GrainTypePlural::All));
881 }
882 Ok(None)
883 }
884
885 fn parse_grain_type_plural(&mut self) -> CalResult<GrainTypePlural> {
887 let span = self.current_span();
888 match self.peek() {
890 Some(SpannedToken {
891 token: Token::Ident(s),
892 ..
893 }) => {
894 let s = s.clone();
895 let lower = s.to_ascii_lowercase();
897 if matches!(
898 lower.as_str(),
899 "fact"
900 | "event"
901 | "state"
902 | "workflow"
903 | "tool"
904 | "observation"
905 | "goal"
906 | "reasoning"
907 | "consensus"
908 | "consent"
909 ) {
910 let plural = match lower.as_str() {
911 "fact" => "facts",
912 "event" => "events",
913 "state" => "states",
914 "workflow" => "workflows",
915 "tool" => "tools",
916 "observation" => "observations",
917 "goal" => "goals",
918 "reasoning" => "reasonings",
919 "consensus" => "consensuses",
920 "consent" => "consents",
921 _ => unreachable!(),
922 };
923 return Err(CalError::UnknownGrainType {
924 found: s,
925 span: Some(span),
926 suggestion: Some(format!(
927 "did you mean \"{}\"? RECALL requires the plural form.",
928 plural
929 )),
930 });
931 }
932 if let Some(gt) = GrainTypePlural::parse(&s) {
933 self.advance();
934 return Ok(gt);
935 }
936 let suggestion = suggest_grain_type_plural(&s);
937 Err(CalError::UnknownGrainType {
938 found: s,
939 span: Some(span),
940 suggestion,
941 })
942 }
943 Some(SpannedToken {
944 token: Token::All, ..
945 }) => {
946 self.advance();
947 Ok(GrainTypePlural::All)
948 }
949 Some(st) => {
950 let found = st.token.description();
951 Err(CalError::UnexpectedToken {
952 expected: "grain type (facts, events, states, ...)".into(),
953 found,
954 span: Some(span),
955 suggestion: Some(
956 "valid grain types: facts, events, states, workflows, tools, \
957 observations, goals, reasonings, consensuses, consents, skills, *"
958 .into(),
959 ),
960 })
961 }
962 None => Err(CalError::UnexpectedToken {
963 expected: "grain type".into(),
964 found: "<end of query>".into(),
965 span: None,
966 suggestion: None,
967 }),
968 }
969 }
970
971 fn parse_grain_type_singular(&mut self) -> CalResult<GrainTypeSingular> {
973 let span = self.current_span();
974 match self.peek() {
975 Some(SpannedToken {
976 token: Token::Ident(s),
977 ..
978 }) => {
979 let s = s.clone();
980 if let Some(gt) = GrainTypeSingular::parse(&s) {
981 self.advance();
982 return Ok(gt);
983 }
984 let suggestion = suggest_grain_type_singular(&s);
985 Err(CalError::UnknownGrainType {
986 found: s,
987 span: Some(span),
988 suggestion,
989 })
990 }
991 Some(SpannedToken {
995 token: Token::Observation,
996 ..
997 }) => {
998 self.advance();
999 Ok(GrainTypeSingular::Observation)
1000 }
1001 Some(st) => {
1002 let found = st.token.description();
1003 Err(CalError::UnexpectedToken {
1004 expected: "grain type (fact, event, state, ...)".into(),
1005 found,
1006 span: Some(span),
1007 suggestion: None,
1008 })
1009 }
1010 None => Err(CalError::UnexpectedToken {
1011 expected: "grain type".into(),
1012 found: "<end of query>".into(),
1013 span: None,
1014 suggestion: None,
1015 }),
1016 }
1017 }
1018
1019 fn parse_query(&mut self) -> CalResult<CalQuery> {
1028 let version = self.parse_version_prefix()?;
1030
1031 let let_bindings = self.parse_let_block()?;
1033
1034 let (statement, inline_pipeline, inline_with, inline_format, user_vars) =
1036 self.parse_statement_full()?;
1037
1038 while self.at_exact(&Token::Semicolon) {
1043 self.advance();
1044 }
1045 if let Some(st) = self.peek() {
1046 let found = st.token.description();
1047 let span = st.span;
1048 return Err(CalError::UnexpectedToken {
1049 expected: "end of query".into(),
1050 found,
1051 span: Some(span),
1052 suggestion: Some(
1053 "a query is one statement; use BATCH { stmt1 ; stmt2 } to run several".into(),
1054 ),
1055 });
1056 }
1057
1058 Ok(CalQuery {
1059 let_values: Default::default(),
1061 version,
1062 statement,
1063 pipeline: inline_pipeline,
1064 with_options: inline_with,
1065 format: inline_format,
1066 let_bindings,
1067 user_vars,
1068 warnings: self.warnings.clone(),
1069 })
1070 }
1071
1072 fn parse_version_prefix(&mut self) -> CalResult<CalVersion> {
1074 if !self.at_exact(&Token::Cal) {
1075 return Ok(CalVersion::default());
1076 }
1077 let span = self.current_span();
1078 self.advance(); self.expect_exact(&Token::Slash)?;
1081 let n = self.parse_u64()?;
1083 if n != 1 {
1084 return Err(CalError::UnsupportedVersion {
1085 version: n as u32,
1086 span: Some(span),
1087 });
1088 }
1089 Ok(CalVersion(n as u32))
1090 }
1091
1092 fn parse_let_block(&mut self) -> CalResult<Vec<LetBinding>> {
1094 let mut bindings = vec![];
1095 while self.at_exact(&Token::Let) {
1096 let span_start = self.current_span();
1097 self.advance(); let name = self.parse_parameter()?;
1099 self.expect_exact(&Token::Eq)?;
1100
1101 let (source_stmt, extractor) = if self.at_extractor() {
1105 let extractor = self.parse_extractor()?;
1106 if matches!(
1108 self.peek(),
1109 Some(SpannedToken {
1110 token: Token::Of,
1111 ..
1112 })
1113 ) {
1114 self.advance();
1115 } else if let Some(SpannedToken {
1116 token: Token::Ident(id),
1117 ..
1118 }) = self.peek()
1119 {
1120 if id.eq_ignore_ascii_case("OF") {
1121 self.advance();
1122 }
1123 }
1124 let source_stmt = if self.at_exact(&Token::LParen) {
1126 self.advance();
1127 self.enter_nesting()?;
1128 let stmt = self.parse_recall_stmt()?;
1129 self.leave_nesting();
1130 self.expect_exact(&Token::RParen)?;
1131 stmt
1132 } else {
1133 self.parse_recall_stmt()?
1134 };
1135 (source_stmt, extractor)
1136 } else {
1137 let source_stmt = self.parse_recall_stmt()?;
1139
1140 let extractor = if self.at_exact(&Token::Pipe) || self.at_extractor() {
1142 if self.eat_exact(&Token::Pipe) {
1143 self.warnings.push(CalWarning::DeprecatedPipeOperator {
1144 span: Some(self.current_span()),
1145 });
1146 }
1147 self.parse_extractor()?
1148 } else {
1149 Extractor::Hashes };
1151 (source_stmt, extractor)
1152 };
1153
1154 self.expect_exact(&Token::Semicolon)?;
1155
1156 let span_end = self.prev_span();
1157 let span = Span::new(
1158 span_start.start,
1159 span_end.end,
1160 span_start.line,
1161 span_start.col,
1162 );
1163
1164 bindings.push(LetBinding {
1165 name,
1166 extractor,
1167 source: Box::new(CalStatement::Recall(source_stmt)),
1168 span: Some(span),
1169 });
1170 }
1171 Ok(bindings)
1172 }
1173
1174 fn at_extractor(&self) -> bool {
1176 matches!(
1177 self.peek(),
1178 Some(SpannedToken {
1179 token: Token::Subjects | Token::Objects | Token::Hashes,
1180 ..
1181 })
1182 )
1183 }
1184
1185 fn parse_extractor(&mut self) -> CalResult<Extractor> {
1186 match self.peek() {
1187 Some(SpannedToken {
1188 token: Token::Subjects,
1189 ..
1190 }) => {
1191 self.advance();
1192 Ok(Extractor::Subjects)
1193 }
1194 Some(SpannedToken {
1195 token: Token::Objects,
1196 ..
1197 }) => {
1198 self.advance();
1199 Ok(Extractor::Objects)
1200 }
1201 Some(SpannedToken {
1202 token: Token::Hashes,
1203 ..
1204 }) => {
1205 self.advance();
1206 Ok(Extractor::Hashes)
1207 }
1208 Some(st) => {
1209 let found = st.token.description();
1210 let span = st.span;
1211 Err(CalError::UnexpectedToken {
1212 expected: "SUBJECTS, OBJECTS, or HASHES".into(),
1213 found,
1214 span: Some(span),
1215 suggestion: None,
1216 })
1217 }
1218 None => Err(CalError::UnexpectedToken {
1219 expected: "SUBJECTS, OBJECTS, or HASHES".into(),
1220 found: "<end of query>".into(),
1221 span: None,
1222 suggestion: None,
1223 }),
1224 }
1225 }
1226
1227 #[allow(clippy::type_complexity)]
1232 fn parse_statement_full(
1233 &mut self,
1234 ) -> CalResult<(
1235 CalStatement,
1236 Vec<PipelineStage>,
1237 Vec<WithOption>,
1238 Option<FormatClause>,
1239 HashMap<String, String>,
1240 )> {
1241 let stmt = self.parse_statement()?;
1242
1243 let mut pipeline = self.parse_pipeline()?;
1245
1246 let mut with_options: Vec<WithOption> = Vec::new();
1249 let mut format = None;
1250 let mut user_vars = HashMap::new();
1251
1252 loop {
1253 if self.at_exact(&Token::Format) && format.is_none() {
1254 format = Some(self.parse_format()?);
1255 } else if self.at_exact(&Token::As) && format.is_none() && !self.peek_next_is_of() {
1256 format = Some(self.parse_as_format()?);
1257 } else if self.at_exact(&Token::With) {
1258 if self.peek_next_is_vars() {
1259 if user_vars.is_empty() {
1260 user_vars = self.parse_user_vars()?;
1261 } else {
1262 break;
1263 }
1264 } else if with_options.is_empty() {
1265 with_options = self.parse_with_clause()?;
1266 } else {
1267 let extra = self.parse_with_clause()?;
1269 with_options.extend(extra);
1270 }
1271 } else if self.at_exact(&Token::Where) {
1272 let wc = self.parse_where_clause()?;
1274 if let Some(clause) = wc {
1275 pipeline.push(PipelineStage::Filter {
1276 condition: clause.condition,
1277 span: clause.span,
1278 });
1279 }
1280 } else {
1281 break;
1282 }
1283 }
1284
1285 Ok((stmt, pipeline, with_options, format, user_vars))
1286 }
1287
1288 fn parse_statement(&mut self) -> CalResult<CalStatement> {
1291 if let Some(SpannedToken {
1293 token: Token::Ident(word),
1294 span,
1295 ..
1296 }) = self.peek()
1297 {
1298 if is_destructive_keyword(word) {
1299 let word = word.clone();
1300 let span = *span;
1301 return Err(CalError::UnexpectedToken {
1302 expected: "RECALL, EXISTS, ASSEMBLE, HISTORY, EXPLAIN, DESCRIBE, BATCH, COALESCE, ADD, SUPERSEDE, REVERT, FORGET, DEFINE, DROP, or STREAM".into(),
1303 found: word,
1304 span: Some(span),
1305 suggestion: Some(
1306 "CAL cannot destroy data in bulk. The only destructive \
1307 CAL statement is FORGET <hash>; erasure, key rotation, \
1308 and schema changes are host-level operations."
1309 .into(),
1310 ),
1311 });
1312 }
1313 }
1314
1315 match self.peek() {
1316 Some(SpannedToken {
1317 token: Token::Explain,
1318 ..
1319 }) => self.parse_explain(),
1320 Some(SpannedToken {
1321 token: Token::Recall,
1322 ..
1323 }) => {
1324 let stmt = self.parse_recall_stmt()?;
1325 self.parse_set_op_tail(CalStatement::Recall(stmt))
1327 }
1328 Some(SpannedToken {
1329 token: Token::Assemble,
1330 ..
1331 }) => self.parse_assemble(),
1332 Some(SpannedToken {
1333 token: Token::Exists,
1334 ..
1335 }) => self.parse_exists(),
1336 Some(SpannedToken {
1337 token: Token::History,
1338 ..
1339 }) => self.parse_history(),
1340 Some(SpannedToken {
1341 token: Token::Describe,
1342 ..
1343 }) => self.parse_describe(),
1344 Some(SpannedToken {
1345 token: Token::Batch,
1346 ..
1347 }) => self.parse_batch(),
1348 Some(SpannedToken {
1349 token: Token::Coalesce,
1350 ..
1351 }) => self.parse_coalesce(),
1352 Some(SpannedToken {
1353 token: Token::Add, ..
1354 }) => self.parse_add(),
1355 Some(SpannedToken {
1356 token: Token::Accumulate,
1357 ..
1358 }) => self.parse_accumulate(),
1359 Some(SpannedToken {
1360 token: Token::Supersede,
1361 ..
1362 }) => self.parse_supersede(),
1363 Some(SpannedToken {
1364 token: Token::Revert,
1365 ..
1366 }) => self.parse_revert(),
1367 Some(SpannedToken {
1371 token: Token::Forget,
1372 ..
1373 }) => self.parse_forget(),
1374 Some(SpannedToken {
1377 token: Token::Purge,
1378 ..
1379 }) => self.parse_purge(),
1380 Some(SpannedToken {
1383 token: Token::Report,
1384 ..
1385 }) => self.parse_report_subject(),
1386 Some(SpannedToken {
1389 token: Token::Approve,
1390 ..
1391 }) => Ok(CalStatement::Approve(
1392 self.parse_governance_body(&Token::Approve)?,
1393 )),
1394 Some(SpannedToken {
1395 token: Token::Reject,
1396 ..
1397 }) => Ok(CalStatement::Reject(
1398 self.parse_governance_body(&Token::Reject)?,
1399 )),
1400 Some(SpannedToken {
1401 token: Token::Apply,
1402 ..
1403 }) => Ok(CalStatement::ApplyRec(
1404 self.parse_governance_body(&Token::Apply)?,
1405 )),
1406 Some(SpannedToken {
1407 token: Token::Rollback,
1408 ..
1409 }) => Ok(CalStatement::RollbackRec(
1410 self.parse_governance_body(&Token::Rollback)?,
1411 )),
1412 Some(SpannedToken {
1414 token: Token::Grant,
1415 ..
1416 }) => self.parse_grant(),
1417 Some(SpannedToken {
1418 token: Token::Revoke,
1419 ..
1420 }) => self.parse_revoke(),
1421 Some(SpannedToken {
1422 token: Token::Show,
1423 ..
1424 }) => self.parse_show_grants(),
1425 Some(SpannedToken {
1426 token: Token::Remember,
1427 ..
1428 }) => self.parse_remember(),
1429 Some(SpannedToken {
1431 token: Token::Entity,
1432 ..
1433 }) => self.parse_entity_at(),
1434 Some(SpannedToken {
1435 token: Token::Runs,
1436 ..
1437 }) => self.parse_runs_touching(),
1438 Some(SpannedToken {
1439 token: Token::Derived,
1440 ..
1441 }) => self.parse_derived_from(),
1442 Some(SpannedToken {
1443 token: Token::Merge,
1444 ..
1445 }) => self.parse_merge(),
1446 Some(SpannedToken {
1447 token: Token::Related,
1448 ..
1449 }) => self.parse_related(),
1450 Some(SpannedToken {
1451 token: Token::Novelty,
1452 ..
1453 }) => self.parse_novelty(),
1454 Some(SpannedToken {
1455 token: Token::Define,
1456 ..
1457 }) => self.parse_define(),
1458 Some(SpannedToken {
1459 token: Token::Drop, ..
1460 }) => self.parse_drop(),
1461 Some(SpannedToken {
1462 token: Token::Run, ..
1463 }) => self.parse_run_query(),
1464 Some(SpannedToken {
1465 token: Token::Stream,
1466 ..
1467 }) => self.parse_stream_assemble(),
1468 Some(SpannedToken {
1470 token: Token::LParen,
1471 ..
1472 }) => {
1473 let stmt = self.parse_paren_statement()?;
1474 self.parse_set_op_tail(stmt)
1475 }
1476 Some(st) => {
1477 let found = st.token.description();
1478 let span = st.span;
1479 Err(CalError::UnexpectedToken {
1480 expected: "RECALL, EXISTS, ASSEMBLE, HISTORY, EXPLAIN, DESCRIBE, BATCH, \
1481 COALESCE, ADD, SUPERSEDE, REVERT, FORGET, DEFINE, DROP, or RUN"
1482 .into(),
1483 found,
1484 span: Some(span),
1485 suggestion: None,
1486 })
1487 }
1488 None => Err(CalError::EmptyQuery { span: None }),
1489 }
1490 }
1491
1492 fn parse_paren_statement(&mut self) -> CalResult<CalStatement> {
1494 self.expect_exact(&Token::LParen)?;
1495 self.enter_nesting()?;
1496 let stmt = self.parse_statement()?;
1497 self.leave_nesting();
1498 self.expect_exact(&Token::RParen)?;
1499 Ok(stmt)
1500 }
1501
1502 fn parse_assemble_source_query(&mut self) -> CalResult<(CalStatement, Vec<WithOption>)> {
1509 if self.at_exact(&Token::LParen) {
1510 self.expect_exact(&Token::LParen)?;
1511 self.enter_nesting()?;
1512 let stmt = self.parse_statement()?;
1514 self.leave_nesting();
1515 let inside_with = if self.at_exact(&Token::With) && !self.peek_next_is_vars() {
1517 self.parse_with_clause()?
1518 } else {
1519 vec![]
1520 };
1521 self.expect_exact(&Token::RParen)?;
1522 Ok((stmt, inside_with))
1523 } else {
1524 let stmt = self.parse_recall_stmt()?;
1526 Ok((CalStatement::Recall(stmt), vec![]))
1527 }
1528 }
1529
1530 fn parse_set_op_tail(&mut self, first: CalStatement) -> CalResult<CalStatement> {
1532 let op = match self.peek() {
1533 Some(SpannedToken {
1534 token: Token::Union,
1535 ..
1536 }) => SetOp::Union,
1537 Some(SpannedToken {
1538 token: Token::Intersect,
1539 ..
1540 }) => SetOp::Intersect,
1541 Some(SpannedToken {
1542 token: Token::Except,
1543 ..
1544 }) => SetOp::Except,
1545 _ => return Ok(first),
1546 };
1547
1548 let span_start = self.current_span();
1549 self.advance(); let mut operands = vec![first];
1552 let rhs = if self.at_exact(&Token::LParen) {
1554 self.parse_paren_statement()?
1555 } else {
1556 let stmt = self.parse_recall_stmt()?;
1557 CalStatement::Recall(stmt)
1558 };
1559 operands.push(rhs);
1560
1561 while matches!(self.peek(), Some(st) if std::mem::discriminant(&st.token) == std::mem::discriminant(&Token::Union)
1563 || std::mem::discriminant(&st.token) == std::mem::discriminant(&Token::Intersect)
1564 || std::mem::discriminant(&st.token) == std::mem::discriminant(&Token::Except))
1565 {
1566 if operands.len() >= MAX_SET_OPERANDS {
1567 return Err(CalError::TooManySetOperands {
1568 count: operands.len() + 1,
1569 max: MAX_SET_OPERANDS,
1570 span: Some(self.current_span()),
1571 });
1572 }
1573 self.advance(); let next = if self.at_exact(&Token::LParen) {
1575 self.parse_paren_statement()?
1576 } else {
1577 let stmt = self.parse_recall_stmt()?;
1578 CalStatement::Recall(stmt)
1579 };
1580 operands.push(next);
1581 }
1582
1583 let span_end = self.prev_span();
1584 Ok(CalStatement::SetOp(SetOpStmt {
1585 op,
1586 operands,
1587 span: Some(Span::new(
1588 span_start.start,
1589 span_end.end,
1590 span_start.line,
1591 span_start.col,
1592 )),
1593 }))
1594 }
1595
1596 fn parse_recall_stmt(&mut self) -> CalResult<RecallStmt> {
1599 let span_start = self.current_span();
1600 self.expect_exact(&Token::Recall)?;
1601
1602 let has_my = self.eat_exact(&Token::My);
1604 if has_my {
1605 self.warnings.push(CalWarning::UnknownRelation {
1606 relation: "MY (shorthand — will be desugared at execution time)".into(),
1607 span: Some(self.prev_span()),
1608 });
1609 }
1610
1611 let grain_type = if self.eat_exact(&Token::Asterisk) {
1616 GrainTypePlural::All
1619 } else if self.at_exact(&Token::Where)
1620 || self.at_exact(&Token::Recent)
1621 || self.at_exact(&Token::Since)
1622 || self.at_exact(&Token::Until)
1623 || self.at_exact(&Token::Between)
1624 || self.at_exact(&Token::About)
1625 || self.at_exact(&Token::Like)
1626 || self.at_exact(&Token::Limit)
1627 || self.at_exact(&Token::Pipe)
1628 {
1629 GrainTypePlural::All
1630 } else {
1631 self.parse_grain_type_plural()?
1632 };
1633
1634 let about = self.parse_about_clause()?;
1638 let like = self.parse_like_clause()?;
1639 let since = self.parse_since_clause()?;
1640 let until = self.parse_until_clause()?;
1641 let between = self.parse_between_clause()?;
1642 let mut where_clause = self.parse_where_clause()?;
1643
1644 if where_clause.is_none() && self.at_exact(&Token::And) {
1648 let wspan = self.current_span();
1649 self.advance(); let cond = self.parse_condition_or()?;
1651 where_clause = Some(WhereClause {
1652 condition: cond,
1653 span: Some(wspan),
1654 });
1655 }
1656
1657 let since = if since.is_none() {
1660 self.parse_since_clause()?
1661 } else {
1662 since
1663 };
1664 let until = if until.is_none() {
1665 self.parse_until_clause()?
1666 } else {
1667 until
1668 };
1669
1670 let recent = self.parse_recent_clause()?;
1671
1672 let limit = if self.at_exact(&Token::Limit) {
1674 self.advance();
1675 let span = self.current_span();
1676 let v = self.parse_u64()?;
1677 if v == 0 {
1678 return Err(CalError::LimitExceeded {
1679 value: 0,
1680 max: MAX_LIMIT_VALUE,
1681 span: Some(span),
1682 });
1683 }
1684 if v > MAX_LIMIT_VALUE {
1685 return Err(CalError::LimitExceeded {
1686 value: v,
1687 max: MAX_LIMIT_VALUE,
1688 span: Some(span),
1689 });
1690 }
1691 Some(v)
1692 } else {
1693 None
1694 };
1695
1696 let contradictions = self.parse_contradictions_clause()?;
1698
1699 let where_refers_to = |field: &str| -> bool {
1702 fn cond_refs(root: &Condition, f: &str) -> bool {
1706 let mut stack: Vec<&Condition> = vec![root];
1707 while let Some(c) = stack.pop() {
1708 match c {
1709 Condition::Comparison { field, .. }
1710 | Condition::In { field, .. }
1711 | Condition::NotIn { field, .. }
1712 if field == f =>
1713 {
1714 return true;
1715 }
1716 Condition::And { left, right, .. } | Condition::Or { left, right, .. } => {
1717 stack.push(left);
1718 stack.push(right);
1719 }
1720 Condition::Not { inner, .. } => stack.push(inner),
1721 _ => {}
1722 }
1723 }
1724 false
1725 }
1726 where_clause
1727 .as_ref()
1728 .map(|w| cond_refs(&w.condition, field))
1729 .unwrap_or(false)
1730 };
1731
1732 let report_conflict = |a: &str, b: &str, hint: &str| -> CalError {
1733 CalError::FieldNotOnGrainType {
1734 field: format!("{} + {}", a, b),
1735 grain_type: "RECALL".into(),
1736 span: Some(span_start),
1737 suggestion: Some(format!(
1738 "Combination ambiguous per spec §9.8: {} with {}. {}",
1739 a, b, hint
1740 )),
1741 }
1742 };
1743
1744 if about.is_some() && like.is_some() {
1745 return Err(report_conflict(
1746 "ABOUT",
1747 "LIKE",
1748 "use ABOUT for semantic search or LIKE for textual similarity, not both.",
1749 ));
1750 }
1751 if recent.is_some() && limit.is_some() {
1752 return Err(report_conflict(
1753 "RECENT",
1754 "LIMIT",
1755 "RECENT N already caps the result count; remove LIMIT.",
1756 ));
1757 }
1758 if since.is_some() && where_refers_to("time") {
1759 return Err(report_conflict(
1760 "SINCE",
1761 "WHERE time",
1762 "SINCE is shorthand for WHERE time — pick one.",
1763 ));
1764 }
1765 if since.is_some() && between.is_some() {
1766 return Err(report_conflict(
1767 "SINCE",
1768 "BETWEEN",
1769 "SINCE sets a lower bound; BETWEEN gives an explicit range — pick one.",
1770 ));
1771 }
1772 if has_my && where_refers_to("user_id") {
1773 return Err(report_conflict(
1774 "MY",
1775 "WHERE user_id",
1776 "MY desugars to `WHERE user_id = $current_user_id` — remove the explicit clause.",
1777 ));
1778 }
1779
1780 let span_end = self.prev_span();
1781 Ok(RecallStmt {
1782 grain_type,
1783 about,
1784 where_clause,
1785 recent,
1786 since,
1787 until,
1788 like,
1789 between,
1790 contradictions,
1791 limit,
1792 as_format: None,
1793 span: Some(Span::new(
1794 span_start.start,
1795 span_end.end,
1796 span_start.line,
1797 span_start.col,
1798 )),
1799 })
1800 }
1801
1802 fn parse_about_clause(&mut self) -> CalResult<Option<AboutClause>> {
1803 if !self.at_exact(&Token::About) {
1804 return Ok(None);
1805 }
1806 let span = self.current_span();
1807 self.advance();
1808 let text = self.parse_string_literal()?;
1809 Ok(Some(AboutClause {
1810 text,
1811 span: Some(span),
1812 }))
1813 }
1814
1815 fn parse_like_clause(&mut self) -> CalResult<Option<LikeClause>> {
1816 if !self.at_exact(&Token::Like) {
1817 return Ok(None);
1818 }
1819 let span = self.current_span();
1820 self.advance();
1821 let text = self.parse_string_literal()?;
1822 Ok(Some(LikeClause {
1823 text,
1824 span: Some(span),
1825 }))
1826 }
1827
1828 fn parse_since_clause(&mut self) -> CalResult<Option<SinceClause>> {
1829 if !self.at_exact(&Token::Since) {
1830 return Ok(None);
1831 }
1832 let span = self.current_span();
1833 self.advance();
1834 let expression = self.parse_string_literal()?;
1835 Ok(Some(SinceClause {
1836 expression,
1837 span: Some(span),
1838 }))
1839 }
1840
1841 fn parse_until_clause(&mut self) -> CalResult<Option<UntilClause>> {
1842 if !self.at_exact(&Token::Until) {
1843 return Ok(None);
1844 }
1845 let span = self.current_span();
1846 self.advance();
1847 let expression = self.parse_string_literal()?;
1848 Ok(Some(UntilClause {
1849 expression,
1850 span: Some(span),
1851 }))
1852 }
1853
1854 fn parse_between_clause(&mut self) -> CalResult<Option<BetweenClause>> {
1855 if !self.at_exact(&Token::Between) {
1856 return Ok(None);
1857 }
1858 let span = self.current_span();
1859 self.advance();
1860 let start = self.parse_string_literal()?;
1861 self.expect_exact(&Token::And)?;
1862 let end = self.parse_string_literal()?;
1863 Ok(Some(BetweenClause {
1864 start,
1865 end,
1866 span: Some(span),
1867 }))
1868 }
1869
1870 fn parse_recent_clause(&mut self) -> CalResult<Option<RecentClause>> {
1871 if !self.at_exact(&Token::Recent) {
1872 return Ok(None);
1873 }
1874 let span = self.current_span();
1875 self.advance();
1876 let count = self.parse_u64()?;
1877 Ok(Some(RecentClause {
1878 count,
1879 span: Some(span),
1880 }))
1881 }
1882
1883 fn parse_contradictions_clause(&mut self) -> CalResult<Option<ContradictionsClause>> {
1884 if !self.at_exact(&Token::Contradictions) {
1885 return Ok(None);
1886 }
1887 let span = self.current_span();
1888 self.advance();
1889 self.eat_exact(&Token::Of);
1892 let inner = if self.at_exact(&Token::LParen) {
1893 Some(Box::new(self.parse_paren_statement()?))
1894 } else {
1895 None
1896 };
1897 Ok(Some(ContradictionsClause {
1898 inner,
1899 span: Some(span),
1900 }))
1901 }
1902
1903 fn parse_where_clause(&mut self) -> CalResult<Option<WhereClause>> {
1906 if !self.at_exact(&Token::Where) {
1907 return Ok(None);
1908 }
1909 let span = self.current_span();
1910 self.advance();
1911 let condition = self.parse_condition_or()?;
1912 Ok(Some(WhereClause {
1913 condition,
1914 span: Some(span),
1915 }))
1916 }
1917
1918 fn parse_condition_or(&mut self) -> CalResult<Condition> {
1920 let span_start = self.current_span();
1921 let mut left = self.parse_condition_and()?;
1922 while self.at_exact(&Token::Or) {
1923 self.advance();
1924 let right = self.parse_condition_and()?;
1925 let span_end = self.prev_span();
1926 left = Condition::Or {
1927 left: Box::new(left),
1928 right: Box::new(right),
1929 span: Some(Span::new(
1930 span_start.start,
1931 span_end.end,
1932 span_start.line,
1933 span_start.col,
1934 )),
1935 };
1936 }
1937 Ok(left)
1938 }
1939
1940 fn parse_condition_and(&mut self) -> CalResult<Condition> {
1942 let span_start = self.current_span();
1943 let mut left = self.parse_condition_unary()?;
1944 while self.at_exact(&Token::And) {
1945 self.advance();
1946 let right = self.parse_condition_unary()?;
1947 let span_end = self.prev_span();
1948 left = Condition::And {
1949 left: Box::new(left),
1950 right: Box::new(right),
1951 span: Some(Span::new(
1952 span_start.start,
1953 span_end.end,
1954 span_start.line,
1955 span_start.col,
1956 )),
1957 };
1958 }
1959 Ok(left)
1960 }
1961
1962 fn parse_condition_unary(&mut self) -> CalResult<Condition> {
1964 if self.at_exact(&Token::Not) {
1965 let span = self.current_span();
1966 self.advance();
1967 let inner = self.parse_condition_primary()?;
1968 return Ok(Condition::Not {
1969 inner: Box::new(inner),
1970 span: Some(span),
1971 });
1972 }
1973 self.parse_condition_primary()
1974 }
1975
1976 fn parse_condition_primary(&mut self) -> CalResult<Condition> {
1979 if self.at_exact(&Token::LParen) {
1981 self.advance();
1982 self.enter_nesting()?;
1983 let cond = self.parse_condition_or()?;
1984 self.leave_nesting();
1985 self.expect_exact(&Token::RParen)?;
1986 return Ok(cond);
1987 }
1988
1989 let span_start = self.current_span();
1990
1991 let field = self.parse_field_name()?;
1993
1994 match self.peek() {
1996 Some(SpannedToken {
1998 token: Token::Eq | Token::NotEq | Token::Gte | Token::Lte | Token::Gt | Token::Lt,
1999 ..
2000 }) => {
2001 let st = self.advance().unwrap();
2002 let comparator = match &st.token {
2003 Token::Eq => Comparator::Eq,
2004 Token::NotEq => Comparator::NotEq,
2005 Token::Gte => Comparator::Gte,
2006 Token::Lte => Comparator::Lte,
2007 Token::Gt => Comparator::Gt,
2008 Token::Lt => Comparator::Lt,
2009 _ => unreachable!(),
2010 };
2011 let value = self.parse_value()?;
2012 if field == "relation" {
2015 if let Value::String { value: ref s } = value {
2016 if let Some(w) = super::relations::validate_relation(s) {
2017 self.warnings.push(w);
2018 }
2019 }
2020 }
2021 let span_end = self.prev_span();
2022 Ok(Condition::Comparison {
2023 field,
2024 comparator,
2025 value,
2026 span: Some(Span::new(
2027 span_start.start,
2028 span_end.end,
2029 span_start.line,
2030 span_start.col,
2031 )),
2032 })
2033 }
2034
2035 Some(SpannedToken {
2037 token: Token::In, ..
2038 }) => {
2039 self.advance();
2040 if self.at(&Token::Parameter("".into())) {
2042 let name = self.parse_parameter()?;
2044 let values = vec![Value::Parameter { name }];
2045 let span_end = self.prev_span();
2046 return Ok(Condition::In {
2047 field,
2048 values,
2049 span: Some(Span::new(
2050 span_start.start,
2051 span_end.end,
2052 span_start.line,
2053 span_start.col,
2054 )),
2055 });
2056 }
2057 let values = self.parse_value_list()?;
2058 let span_end = self.prev_span();
2059 Ok(Condition::In {
2060 field,
2061 values,
2062 span: Some(Span::new(
2063 span_start.start,
2064 span_end.end,
2065 span_start.line,
2066 span_start.col,
2067 )),
2068 })
2069 }
2070
2071 Some(SpannedToken {
2073 token: Token::Not, ..
2074 }) if self.peek_ahead(1).map(|t| &t.token) == Some(&Token::In) => {
2075 self.advance(); self.advance(); let values = self.parse_value_list()?;
2078 let span_end = self.prev_span();
2079 Ok(Condition::NotIn {
2080 field,
2081 values,
2082 span: Some(Span::new(
2083 span_start.start,
2084 span_end.end,
2085 span_start.line,
2086 span_start.col,
2087 )),
2088 })
2089 }
2090
2091 Some(SpannedToken {
2093 token: Token::Is, ..
2094 }) => {
2095 self.advance(); let not_null = self.eat_exact(&Token::Not);
2097
2098 if !not_null {
2100 if let Some(category) = self.try_parse_relation_category() {
2101 let span_end = self.prev_span();
2102 let span = Some(Span::new(
2103 span_start.start,
2104 span_end.end,
2105 span_start.line,
2106 span_start.col,
2107 ));
2108 return Ok(Condition::IsCategory {
2109 field,
2110 category,
2111 span,
2112 });
2113 }
2114 }
2115
2116 self.expect_exact(&Token::Null)?;
2117 let span_end = self.prev_span();
2118 let span = Some(Span::new(
2119 span_start.start,
2120 span_end.end,
2121 span_start.line,
2122 span_start.col,
2123 ));
2124 if not_null {
2125 Ok(Condition::IsNotNull { field, span })
2126 } else {
2127 Ok(Condition::IsNull { field, span })
2128 }
2129 }
2130
2131 Some(SpannedToken {
2138 token: Token::Include,
2139 ..
2140 }) => {
2141 self.advance();
2142 let include_span = self.current_span();
2143 let val = self.parse_value()?;
2144 let values = match val {
2146 Value::Array { values } => values,
2147 Value::Parameter { .. } => vec![val],
2148 other => {
2149 return Err(CalError::UnexpectedToken {
2150 expected: "array literal `[...]` after INCLUDE".into(),
2151 found: other.type_name().to_string(),
2152 span: Some(include_span),
2153 suggestion: Some(
2154 "tags INCLUDE requires an array, e.g. tags INCLUDE [\"tag1\", \"tag2\"]".into(),
2155 ),
2156 });
2157 }
2158 };
2159 let span_end = self.prev_span();
2160 Ok(Condition::In {
2161 field,
2162 values,
2163 span: Some(Span::new(
2164 span_start.start,
2165 span_end.end,
2166 span_start.line,
2167 span_start.col,
2168 )),
2169 })
2170 }
2171
2172 Some(SpannedToken {
2174 token: Token::Exclude,
2175 ..
2176 }) => {
2177 self.advance();
2178 let exclude_span = self.current_span();
2179 let val = self.parse_value()?;
2180 let values = match val {
2182 Value::Array { values } => values,
2183 Value::Parameter { .. } => vec![val],
2184 other => {
2185 return Err(CalError::UnexpectedToken {
2186 expected: "array literal `[...]` after EXCLUDE".into(),
2187 found: other.type_name().to_string(),
2188 span: Some(exclude_span),
2189 suggestion: Some(
2190 "tags EXCLUDE requires an array, e.g. tags EXCLUDE [\"tag1\"]"
2191 .into(),
2192 ),
2193 });
2194 }
2195 };
2196 let span_end = self.prev_span();
2197 Ok(Condition::NotIn {
2198 field,
2199 values,
2200 span: Some(Span::new(
2201 span_start.start,
2202 span_end.end,
2203 span_start.line,
2204 span_start.col,
2205 )),
2206 })
2207 }
2208
2209 Some(SpannedToken {
2211 token: Token::Ident(id),
2212 ..
2213 }) if id.eq_ignore_ascii_case("CONTAINS") => {
2214 self.advance();
2215 let value = self.parse_string_literal()?;
2216 let span_end = self.prev_span();
2217 Ok(Condition::Contains {
2218 field,
2219 value,
2220 span: Some(Span::new(
2221 span_start.start,
2222 span_end.end,
2223 span_start.line,
2224 span_start.col,
2225 )),
2226 })
2227 }
2228
2229 Some(SpannedToken {
2231 token: Token::Ident(id),
2232 ..
2233 }) if id.eq_ignore_ascii_case("STARTS") => {
2234 self.advance(); let has_with_keyword = self.at_exact(&Token::With)
2237 || matches!(self.peek(), Some(SpannedToken { token: Token::Ident(id), .. }) if id.eq_ignore_ascii_case("WITH"));
2238 if has_with_keyword {
2239 self.advance();
2240 }
2241 let value = self.parse_string_literal()?;
2242 let span_end = self.prev_span();
2243 Ok(Condition::StartsWith {
2244 field,
2245 value,
2246 span: Some(Span::new(
2247 span_start.start,
2248 span_end.end,
2249 span_start.line,
2250 span_start.col,
2251 )),
2252 })
2253 }
2254
2255 Some(st) => {
2256 let found = st.token.description();
2257 let span = st.span;
2258 Err(CalError::UnexpectedToken {
2259 expected:
2260 "comparison operator (=, !=, >=, <=, >, <), IN, IS, CONTAINS, STARTS WITH, INCLUDE, or EXCLUDE"
2261 .into(),
2262 found,
2263 span: Some(span),
2264 suggestion: None,
2265 })
2266 }
2267 None => Err(CalError::UnexpectedToken {
2268 expected: "comparison operator".into(),
2269 found: "<end of query>".into(),
2270 span: None,
2271 suggestion: None,
2272 }),
2273 }
2274 }
2275
2276 fn try_parse_relation_category(&mut self) -> Option<String> {
2280 match self.peek() {
2281 Some(SpannedToken {
2282 token: Token::Preference,
2283 ..
2284 }) => {
2285 self.advance();
2286 Some("preference".to_string())
2287 }
2288 Some(SpannedToken {
2289 token: Token::Knowledge,
2290 ..
2291 }) => {
2292 self.advance();
2293 Some("knowledge".to_string())
2294 }
2295 Some(SpannedToken {
2296 token: Token::Permission,
2297 ..
2298 }) => {
2299 self.advance();
2300 Some("permission".to_string())
2301 }
2302 Some(SpannedToken {
2303 token: Token::Interaction,
2304 ..
2305 }) => {
2306 self.advance();
2307 Some("interaction".to_string())
2308 }
2309 Some(SpannedToken {
2310 token: Token::Agency,
2311 ..
2312 }) => {
2313 self.advance();
2314 Some("agency".to_string())
2315 }
2316 Some(SpannedToken {
2317 token: Token::Lifecycle,
2318 ..
2319 }) => {
2320 self.advance();
2321 Some("lifecycle".to_string())
2322 }
2323 Some(SpannedToken {
2324 token: Token::Observation,
2325 ..
2326 }) => {
2327 self.advance();
2328 Some("observation".to_string())
2329 }
2330 Some(SpannedToken {
2332 token: Token::Ident(id),
2333 ..
2334 }) if id.eq_ignore_ascii_case("WORKFLOW") => {
2335 self.advance();
2336 Some("workflow".to_string())
2337 }
2338 Some(SpannedToken {
2339 token: Token::Ident(id),
2340 ..
2341 }) if id.eq_ignore_ascii_case("CONSENSUS") => {
2342 self.advance();
2343 Some("consensus".to_string())
2344 }
2345 Some(SpannedToken {
2346 token: Token::Ident(id),
2347 ..
2348 }) if id.eq_ignore_ascii_case("GOVERNANCE") => {
2349 self.advance();
2350 Some("governance".to_string())
2351 }
2352 _ => None,
2353 }
2354 }
2355
2356 fn parse_field_name(&mut self) -> CalResult<String> {
2360 let first = self.parse_identifier()?;
2361 if self.at_exact(&Token::Dot) {
2362 self.advance();
2363 let second = self.parse_identifier()?;
2364 Ok(format!("{}.{}", first, second))
2365 } else {
2366 Ok(first)
2367 }
2368 }
2369
2370 fn at_pipeline_stage(&self) -> bool {
2374 if matches!(
2375 self.peek(),
2376 Some(SpannedToken {
2377 token: Token::Select
2378 | Token::Order
2379 | Token::Limit
2380 | Token::Offset
2381 | Token::Count
2382 | Token::First
2383 | Token::Subjects
2384 | Token::Objects
2385 | Token::Hashes
2386 | Token::Group
2387 | Token::Project,
2388 ..
2389 })
2390 ) {
2391 return true;
2392 }
2393 matches!(
2395 self.peek(),
2396 Some(SpannedToken {
2397 token: Token::Ident(id),
2398 ..
2399 }) if id.eq_ignore_ascii_case("SORT")
2400 )
2401 }
2402
2403 fn parse_pipeline(&mut self) -> CalResult<Vec<PipelineStage>> {
2404 let mut stages = vec![];
2405 while self.at_exact(&Token::Pipe) || self.at_pipeline_stage() {
2407 if stages.len() >= MAX_PIPELINE_STAGES {
2408 return Err(CalError::TooManyPipelineStages {
2409 count: stages.len() + 1,
2410 max: MAX_PIPELINE_STAGES,
2411 span: Some(self.current_span()),
2412 });
2413 }
2414 if self.eat_exact(&Token::Pipe) {
2416 self.warnings.push(CalWarning::DeprecatedPipeOperator {
2417 span: Some(self.current_span()),
2418 });
2419 }
2420 let stage = self.parse_pipeline_stage()?;
2421 stages.push(stage);
2422 }
2423 Ok(stages)
2424 }
2425
2426 fn parse_pipeline_stage(&mut self) -> CalResult<PipelineStage> {
2427 let span = self.current_span();
2428 match self.peek() {
2429 Some(SpannedToken {
2430 token: Token::Select,
2431 ..
2432 }) => {
2433 self.advance();
2434 let mut fields = vec![];
2435 loop {
2436 fields.push(self.parse_identifier()?);
2437 if !self.eat_exact(&Token::Comma) {
2438 break;
2439 }
2440 }
2441 Ok(PipelineStage::Select {
2442 fields,
2443 span: Some(span),
2444 })
2445 }
2446 Some(SpannedToken {
2447 token: Token::Order,
2448 ..
2449 }) => {
2450 self.advance();
2451 self.expect_exact(&Token::By)?;
2452 let field = self.parse_identifier()?;
2453 let descending = if self.at_exact(&Token::Desc) {
2454 self.advance();
2455 true
2456 } else {
2457 self.eat_exact(&Token::Asc);
2458 false
2459 };
2460 Ok(PipelineStage::OrderBy {
2461 field,
2462 descending,
2463 span: Some(span),
2464 })
2465 }
2466 Some(SpannedToken {
2467 token: Token::Limit,
2468 ..
2469 }) => {
2470 self.advance();
2471 let lspan = self.current_span();
2472 let value = self.parse_u64()?;
2473 if value == 0 {
2474 return Err(CalError::LimitExceeded {
2475 value: 0,
2476 max: MAX_LIMIT_VALUE,
2477 span: Some(lspan),
2478 });
2479 }
2480 if value > MAX_LIMIT_VALUE {
2481 return Err(CalError::LimitExceeded {
2482 value,
2483 max: MAX_LIMIT_VALUE,
2484 span: Some(lspan),
2485 });
2486 }
2487 Ok(PipelineStage::Limit {
2488 value,
2489 span: Some(span),
2490 })
2491 }
2492 Some(SpannedToken {
2493 token: Token::Offset,
2494 ..
2495 }) => {
2496 self.advance();
2497 let value = self.parse_u64()?;
2498 Ok(PipelineStage::Offset {
2499 value,
2500 span: Some(span),
2501 })
2502 }
2503 Some(SpannedToken {
2504 token: Token::Count,
2505 ..
2506 }) => {
2507 self.advance();
2508 Ok(PipelineStage::Count { span: Some(span) })
2509 }
2510 Some(SpannedToken {
2511 token: Token::First,
2512 ..
2513 }) => {
2514 self.advance();
2515 Ok(PipelineStage::First { span: Some(span) })
2516 }
2517 Some(SpannedToken {
2518 token: Token::Subjects,
2519 ..
2520 }) => {
2521 self.advance();
2522 Ok(PipelineStage::Subjects { span: Some(span) })
2523 }
2524 Some(SpannedToken {
2525 token: Token::Objects,
2526 ..
2527 }) => {
2528 self.advance();
2529 Ok(PipelineStage::Objects { span: Some(span) })
2530 }
2531 Some(SpannedToken {
2532 token: Token::Hashes,
2533 ..
2534 }) => {
2535 self.advance();
2536 Ok(PipelineStage::Hashes { span: Some(span) })
2537 }
2538 Some(SpannedToken {
2539 token: Token::Group,
2540 ..
2541 }) => {
2542 self.advance();
2543 self.expect_exact(&Token::By)?;
2544 let field = self.parse_identifier()?;
2545 Ok(PipelineStage::GroupBy {
2546 field,
2547 span: Some(span),
2548 })
2549 }
2550 Some(SpannedToken {
2551 token: Token::Project,
2552 ..
2553 }) => {
2554 self.advance();
2555 let mut fields = vec![];
2556 loop {
2557 let field = self.parse_identifier()?;
2558 let alias = if self.eat_exact(&Token::As) {
2559 Some(self.parse_identifier()?)
2560 } else {
2561 None
2562 };
2563 fields.push(ProjectField { field, alias });
2564 if !self.eat_exact(&Token::Comma) {
2565 break;
2566 }
2567 }
2568 Ok(PipelineStage::Project {
2569 fields,
2570 span: Some(span),
2571 })
2572 }
2573 Some(SpannedToken {
2575 token: Token::Ident(id),
2576 ..
2577 }) if id.eq_ignore_ascii_case("SORT") => {
2578 self.advance(); let field = self.parse_identifier()?;
2580 let descending = if self.at_exact(&Token::Desc) {
2581 self.advance();
2582 true
2583 } else {
2584 self.eat_exact(&Token::Asc);
2585 false
2586 };
2587 Ok(PipelineStage::OrderBy {
2588 field,
2589 descending,
2590 span: Some(span),
2591 })
2592 }
2593 Some(st) => {
2594 let found = st.token.description();
2595 let sp = st.span;
2596 Err(CalError::UnexpectedToken {
2597 expected: "pipeline stage (SELECT, ORDER BY, LIMIT, OFFSET, COUNT, FIRST, \
2598 SUBJECTS, OBJECTS, HASHES, GROUP BY, PROJECT)"
2599 .into(),
2600 found,
2601 span: Some(sp),
2602 suggestion: None,
2603 })
2604 }
2605 None => Err(CalError::UnexpectedToken {
2606 expected: "pipeline stage".into(),
2607 found: "<end of query>".into(),
2608 span: None,
2609 suggestion: None,
2610 }),
2611 }
2612 }
2613
2614 fn parse_with_clause(&mut self) -> CalResult<Vec<WithOption>> {
2617 self.expect_exact(&Token::With)?;
2618 let mut options = vec![];
2619 loop {
2620 if let Some(opt) = self.parse_with_option()? {
2623 options.push(opt);
2624 }
2625 if !self.eat_exact(&Token::Comma) {
2626 break;
2627 }
2628 match self.peek() {
2632 None => break,
2633 Some(SpannedToken {
2634 token: Token::Pipe, ..
2635 }) => break,
2636 Some(SpannedToken {
2637 token: Token::Format,
2638 ..
2639 }) => break,
2640 _ => {
2641 }
2643 }
2644 }
2645 Ok(options)
2646 }
2647
2648 fn parse_with_option(&mut self) -> CalResult<Option<WithOption>> {
2654 match self.peek() {
2655 Some(SpannedToken {
2656 token: Token::Superseded,
2657 ..
2658 }) => {
2659 self.advance();
2660 Ok(Some(WithOption::Superseded))
2661 }
2662 Some(SpannedToken {
2663 token: Token::ScoreBreakdown,
2664 ..
2665 }) => {
2666 self.advance();
2667 Ok(Some(WithOption::ScoreBreakdown))
2668 }
2669 Some(SpannedToken {
2670 token: Token::Explanation,
2671 ..
2672 }) => {
2673 self.advance();
2674 Ok(Some(WithOption::Explanation))
2675 }
2676 Some(SpannedToken {
2677 token: Token::Provenance,
2678 ..
2679 }) => {
2680 self.advance();
2681 Ok(Some(WithOption::Provenance))
2682 }
2683 Some(SpannedToken {
2684 token: Token::ContradictionDetection,
2685 ..
2686 }) => {
2687 self.advance();
2688 Ok(Some(WithOption::ContradictionDetection))
2689 }
2690 Some(SpannedToken {
2691 token: Token::Diversity,
2692 ..
2693 }) => {
2694 self.advance();
2695 let lambda = if self.at_exact(&Token::LParen) {
2696 self.advance();
2697 let n = self.parse_number()?;
2698 self.expect_exact(&Token::RParen)?;
2699 Some(n)
2700 } else {
2701 None
2702 };
2703 Ok(Some(WithOption::Diversity { lambda }))
2704 }
2705 Some(SpannedToken {
2706 token: Token::Dedup,
2707 ..
2708 }) => {
2709 self.advance();
2710 let field = if self.at_exact(&Token::LParen) {
2712 self.advance();
2713 let f = self.parse_field_name()?;
2714 self.expect_exact(&Token::RParen)?;
2715 Some(f)
2716 } else {
2717 None
2718 };
2719 Ok(Some(WithOption::Dedup { field }))
2720 }
2721 Some(SpannedToken {
2723 token: Token::ProgressiveDisclosure,
2724 span,
2725 ..
2726 }) => {
2727 let span = *span;
2728 self.advance();
2729 let level = if self.at_exact(&Token::LParen) {
2730 self.advance();
2731 let lvl = self.parse_identifier()?;
2732 let lvl_lc = lvl.to_ascii_lowercase();
2733 if !matches!(lvl_lc.as_str(), "summary" | "headlines" | "full") {
2734 return Err(CalError::UnexpectedToken {
2735 expected: "one of: summary, headlines, full".into(),
2736 found: lvl,
2737 span: Some(self.prev_span()),
2738 suggestion: Some(
2739 "progressive_disclosure level must be summary | headlines | full (spec §4).".into(),
2740 ),
2741 });
2742 }
2743 self.expect_exact(&Token::RParen)?;
2744 Some(lvl_lc)
2745 } else {
2746 None
2747 };
2748 let _ = span;
2749 Ok(Some(WithOption::ProgressiveDisclosure { level }))
2750 }
2751 Some(SpannedToken {
2753 token: Token::Consistency,
2754 span,
2755 ..
2756 }) => {
2757 let span = *span;
2758 self.advance();
2759 self.expect_exact(&Token::LParen)?;
2760 let lvl = self.parse_identifier()?;
2761 let lvl_lc = lvl.to_ascii_lowercase();
2762 if !matches!(lvl_lc.as_str(), "eventual" | "bounded" | "linearizable") {
2763 return Err(CalError::UnexpectedToken {
2764 expected: "one of: eventual, bounded, linearizable".into(),
2765 found: lvl,
2766 span: Some(self.prev_span()),
2767 suggestion: Some(
2768 "consistency level must be eventual | bounded | linearizable (spec §4).".into(),
2769 ),
2770 });
2771 }
2772 self.expect_exact(&Token::RParen)?;
2773 self.warnings.push(CalWarning::UnknownExtensionOption {
2774 option: "consistency (parsed but executor no-op)".into(),
2775 span: Some(span),
2776 });
2777 Ok(Some(WithOption::Consistency {
2778 level: Some(lvl_lc),
2779 }))
2780 }
2781 Some(SpannedToken {
2783 token: Token::Locale,
2784 span,
2785 ..
2786 }) => {
2787 let span = *span;
2788 self.advance();
2789 self.expect_exact(&Token::LParen)?;
2790 let tag = self.parse_string_literal()?;
2791 self.expect_exact(&Token::RParen)?;
2792 self.warnings.push(CalWarning::UnknownExtensionOption {
2793 option: "locale (parsed but executor no-op)".into(),
2794 span: Some(span),
2795 });
2796 Ok(Some(WithOption::Locale { tag }))
2797 }
2798 Some(SpannedToken {
2800 token: Token::Cache,
2801 span,
2802 ..
2803 }) => {
2804 let span = *span;
2805 self.advance();
2806 self.expect_exact(&Token::LParen)?;
2807 if self.at_exact(&Token::Ttl) {
2809 self.advance();
2810 self.expect_exact(&Token::Eq)?;
2811 }
2812 let ttl_seconds = self.parse_u64()?;
2813 self.expect_exact(&Token::RParen)?;
2814 self.warnings.push(CalWarning::UnknownExtensionOption {
2815 option: "cache (parsed but executor no-op)".into(),
2816 span: Some(span),
2817 });
2818 Ok(Some(WithOption::Cache { ttl_seconds }))
2819 }
2820 Some(SpannedToken {
2822 token: Token::Rerank,
2823 ..
2824 }) => {
2825 self.advance();
2826 let model = if self.at_exact(&Token::LParen) {
2827 self.advance();
2828 let m = self.parse_string_literal()?;
2829 self.expect_exact(&Token::RParen)?;
2830 Some(m)
2831 } else {
2832 None
2833 };
2834 Ok(Some(WithOption::Rerank { model }))
2835 }
2836 Some(SpannedToken {
2837 token: Token::LlmRerank,
2838 ..
2839 }) => {
2840 self.advance();
2841 let model = if self.at_exact(&Token::LParen) {
2842 self.advance();
2843 let m = self.parse_string_literal()?;
2844 self.expect_exact(&Token::RParen)?;
2845 Some(m)
2846 } else {
2847 None
2848 };
2849 Ok(Some(WithOption::LlmRerank { model }))
2850 }
2851 Some(SpannedToken {
2852 token: Token::QueryExpansion,
2853 ..
2854 }) => {
2855 self.advance();
2856 Ok(Some(WithOption::QueryExpansion))
2857 }
2858 Some(SpannedToken {
2859 token: Token::QueryDecompose,
2860 ..
2861 }) => {
2862 self.advance();
2863 Ok(Some(WithOption::QueryDecompose))
2864 }
2865 Some(SpannedToken {
2866 token: Token::Hyde, ..
2867 }) => {
2868 self.advance();
2869 Ok(Some(WithOption::Hyde))
2870 }
2871 Some(SpannedToken {
2872 token: Token::ConflictResolution,
2873 ..
2874 }) => {
2875 self.advance();
2876 Ok(Some(WithOption::ConflictResolution))
2877 }
2878 Some(SpannedToken {
2879 token: Token::IncludeSources,
2880 ..
2881 }) => {
2882 self.advance();
2883 Ok(Some(WithOption::IncludeSources))
2884 }
2885 Some(SpannedToken {
2886 token: Token::AnnotateRelativeTime,
2887 ..
2888 }) => {
2889 self.advance();
2890 Ok(Some(WithOption::AnnotateRelativeTime))
2891 }
2892 Some(SpannedToken {
2893 token: Token::RecencyWeight,
2894 ..
2895 }) => {
2896 self.advance();
2897 self.expect_exact(&Token::LParen)?;
2898 let weight = self.parse_number()?;
2899 self.expect_exact(&Token::RParen)?;
2900 Ok(Some(WithOption::RecencyWeight { weight }))
2901 }
2902 Some(SpannedToken {
2903 token: Token::MinScore,
2904 ..
2905 }) => {
2906 self.advance();
2907 self.expect_exact(&Token::LParen)?;
2908 let score = self.parse_number()?;
2909 self.expect_exact(&Token::RParen)?;
2910 Ok(Some(WithOption::MinScore { score }))
2911 }
2912 Some(SpannedToken {
2913 token: Token::MultiHop,
2914 ..
2915 }) => {
2916 self.advance();
2917 self.expect_exact(&Token::LParen)?;
2918 let hops = self.parse_u64()?;
2919 self.expect_exact(&Token::RParen)?;
2920 Ok(Some(WithOption::MultiHop { hops }))
2921 }
2922
2923 Some(SpannedToken {
2924 token: Token::SessionAffinity,
2925 ..
2926 }) => {
2927 self.advance();
2928 self.expect_exact(&Token::LParen)?;
2929 let boost = self.parse_number()?;
2930 self.expect_exact(&Token::RParen)?;
2931 Ok(Some(WithOption::SessionAffinity { boost }))
2932 }
2933 Some(SpannedToken {
2934 token: Token::SubjectAffinity,
2935 ..
2936 }) => {
2937 self.advance();
2938 self.expect_exact(&Token::LParen)?;
2939 let boost = self.parse_number()?;
2940 self.expect_exact(&Token::RParen)?;
2941 Ok(Some(WithOption::SubjectAffinity { boost }))
2942 }
2943 Some(SpannedToken {
2944 token: Token::SessionCoverage,
2945 ..
2946 }) => {
2947 self.advance();
2948 self.expect_exact(&Token::LParen)?;
2949 let min_per_ns = self.parse_u64()?;
2950 self.expect_exact(&Token::RParen)?;
2951 Ok(Some(WithOption::SessionCoverage { min_per_ns }))
2952 }
2953 Some(SpannedToken {
2954 token: Token::MaxNamespaces,
2955 ..
2956 }) => {
2957 self.advance();
2958 self.expect_exact(&Token::LParen)?;
2959 let max = self.parse_u64()?;
2960 self.expect_exact(&Token::RParen)?;
2961 Ok(Some(WithOption::MaxNamespaces { max }))
2962 }
2963 Some(SpannedToken {
2964 token: Token::Exhaustive,
2965 ..
2966 }) => {
2967 self.advance();
2968 let max_rounds = if self.peek().map(|st| &st.token) == Some(&Token::LParen) {
2970 self.expect_exact(&Token::LParen)?;
2971 let rounds = self.parse_u64()?;
2972 self.expect_exact(&Token::RParen)?;
2973 Some(rounds)
2974 } else {
2975 None
2976 };
2977 Ok(Some(WithOption::Exhaustive { max_rounds }))
2978 }
2979 Some(SpannedToken {
2980 token: Token::SessionCensus,
2981 ..
2982 }) => {
2983 self.advance();
2984 let (mut min_per_session, mut min_score) = (None, None);
2987 if self.peek().map(|st| &st.token) == Some(&Token::LParen) {
2988 self.expect_exact(&Token::LParen)?;
2989 min_per_session = Some(self.parse_u64()?);
2990 if self.peek().map(|st| &st.token) == Some(&Token::Comma) {
2991 self.advance();
2992 min_score = Some(self.parse_number()?);
2993 }
2994 self.expect_exact(&Token::RParen)?;
2995 }
2996 Ok(Some(WithOption::SessionCensus {
2997 min_per_session,
2998 min_score,
2999 }))
3000 }
3001 Some(SpannedToken {
3002 token: Token::AggregationIntent,
3003 ..
3004 }) => {
3005 self.advance();
3006 Ok(Some(WithOption::AggregationIntent))
3007 }
3008
3009 Some(SpannedToken {
3010 token: Token::PreferenceEnrichment,
3011 ..
3012 }) => {
3013 self.advance();
3014 Ok(Some(WithOption::PreferenceEnrichment))
3015 }
3016
3017 Some(st) => {
3019 let found = st.token.description();
3020 let span = st.span;
3021 self.warnings.push(CalWarning::UnknownExtensionOption {
3022 option: found,
3023 span: Some(span),
3024 });
3025 self.advance();
3029 Ok(None)
3030 }
3031 None => Err(CalError::UnexpectedToken {
3032 expected: "WITH option".into(),
3033 found: "<end of query>".into(),
3034 span: None,
3035 suggestion: None,
3036 }),
3037 }
3038 }
3039
3040 const MAX_MULTI_FORMATS: usize = 5;
3044
3045 fn parse_format(&mut self) -> CalResult<FormatClause> {
3046 self.expect_exact(&Token::Format)?;
3047 self.parse_format_value()
3048 }
3049
3050 fn parse_as_format(&mut self) -> CalResult<FormatClause> {
3055 self.expect_exact(&Token::As)?;
3056 self.parse_format_value()
3057 }
3058
3059 fn parse_format_value(&mut self) -> CalResult<FormatClause> {
3060 if self.at_exact(&Token::LBracket) {
3062 return self.parse_format_list();
3063 }
3064
3065 let spec = self.parse_single_format_spec()?;
3067
3068 if self.at_exact(&Token::Comma) {
3071 let mut entries = vec![AliasedFormat { spec, alias: None }];
3072 while self.eat_exact(&Token::Comma) {
3073 let next = self.parse_single_format_spec()?;
3074 let entry = AliasedFormat {
3075 spec: next,
3076 alias: None,
3077 };
3078 if !entries.iter().any(|e| e.spec == entry.spec) {
3079 entries.push(entry);
3080 }
3081 }
3082 if entries.len() > Self::MAX_MULTI_FORMATS {
3083 return Err(CalError::TooManyFormats {
3084 count: entries.len(),
3085 max: Self::MAX_MULTI_FORMATS,
3086 span: Some(self.current_span()),
3087 });
3088 }
3089 return Ok(FormatClause::Multi(entries));
3090 }
3091
3092 Ok(FormatClause::Single(spec))
3093 }
3094
3095 fn parse_format_list(&mut self) -> CalResult<FormatClause> {
3098 let span_start = self.current_span();
3099 self.expect_exact(&Token::LBracket)?;
3100
3101 if self.at_exact(&Token::RBracket) {
3103 return Err(CalError::UnexpectedToken {
3104 expected: "at least one format type in format list".into(),
3105 found: "]".into(),
3106 span: Some(span_start),
3107 suggestion: Some("FORMAT [json] or FORMAT [markdown, json]".into()),
3108 });
3109 }
3110
3111 let mut entries = Vec::new();
3112 let mut seen_keys = std::collections::HashSet::new();
3113 loop {
3114 let spec = self.parse_single_format_spec()?;
3115
3116 let alias = if self.eat_exact(&Token::As) {
3122 let span = self.current_span();
3123 let name = self.parse_label()?;
3124 if is_destructive_keyword(&name) {
3128 return Err(CalError::UnexpectedToken {
3129 expected: "an output name".into(),
3130 found: name,
3131 span: Some(span),
3132 suggestion: Some(
3133 "Choose a different alias — CAL keeps destructive \
3134 words out of its grammar entirely."
3135 .into(),
3136 ),
3137 });
3138 }
3139 Some(name)
3140 } else {
3141 None
3142 };
3143
3144 let key = alias.as_deref().unwrap_or(spec.canonical_key()).to_string();
3146 if !seen_keys.insert(key.clone()) {
3147 if alias.is_some() {
3150 return Err(CalError::DuplicateFormatKey {
3151 key,
3152 span: Some(self.current_span()),
3153 });
3154 }
3155 } else {
3157 entries.push(AliasedFormat { spec, alias });
3158 }
3159
3160 if self.eat_exact(&Token::Comma) {
3161 continue;
3162 }
3163 break;
3164 }
3165
3166 self.expect_exact(&Token::RBracket)?;
3167
3168 if entries.len() > Self::MAX_MULTI_FORMATS {
3170 return Err(CalError::TooManyFormats {
3171 count: entries.len(),
3172 max: Self::MAX_MULTI_FORMATS,
3173 span: Some(span_start),
3174 });
3175 }
3176
3177 Ok(FormatClause::Multi(entries))
3178 }
3179
3180 fn parse_single_format_spec(&mut self) -> CalResult<FormatSpec> {
3182 match self.peek() {
3183 Some(SpannedToken {
3184 token: Token::Json, ..
3185 }) => {
3186 self.advance();
3187 Ok(FormatSpec::Json)
3188 }
3189 Some(SpannedToken {
3190 token: Token::Yaml, ..
3191 }) => {
3192 self.advance();
3193 Ok(FormatSpec::Yaml)
3194 }
3195 Some(SpannedToken {
3196 token: Token::Markdown,
3197 ..
3198 }) => {
3199 self.advance();
3200 Ok(FormatSpec::Markdown)
3201 }
3202 Some(SpannedToken {
3203 token: Token::Text, ..
3204 }) => {
3205 self.advance();
3206 Ok(FormatSpec::Text)
3207 }
3208 Some(SpannedToken {
3209 token: Token::Sml, ..
3210 }) => {
3211 self.advance();
3212 Ok(FormatSpec::Sml)
3213 }
3214 Some(SpannedToken {
3215 token: Token::Toon, ..
3216 }) => {
3217 self.advance();
3218 Ok(FormatSpec::Toon)
3219 }
3220 Some(SpannedToken {
3221 token: Token::Triples,
3222 ..
3223 }) => {
3224 self.advance();
3225 Ok(FormatSpec::Triples)
3226 }
3227 Some(SpannedToken {
3231 token: Token::Structured,
3232 ..
3233 }) => {
3234 self.advance();
3235 Ok(FormatSpec::Sml)
3236 }
3237 Some(SpannedToken {
3238 token: Token::Readable,
3239 ..
3240 }) => {
3241 self.advance();
3242 Ok(FormatSpec::Markdown)
3243 }
3244 Some(SpannedToken {
3245 token: Token::Compact,
3246 ..
3247 }) => {
3248 self.advance();
3249 Ok(FormatSpec::Text)
3250 }
3251 Some(SpannedToken {
3252 token: Token::Data, ..
3253 }) => {
3254 self.advance();
3255 Ok(FormatSpec::Json)
3256 }
3257 Some(SpannedToken {
3258 token: Token::Template,
3259 ..
3260 }) => {
3261 self.advance();
3262 match self.peek() {
3268 Some(st) if Self::is_word_token(&st.token) => {
3274 Ok(FormatSpec::TemplateRef {
3275 name: self.parse_template_name()?,
3276 })
3277 }
3278 Some(SpannedToken {
3279 token: Token::LBrace,
3280 ..
3281 }) => {
3282 self.advance();
3283 let sections = self.parse_template_sections()?;
3284 self.expect_exact(&Token::RBrace)?;
3285 if sections.is_empty() {
3286 return Err(CalError::TemplateSyntaxError {
3287 detail: "inline TEMPLATE must define at least one section"
3288 .to_string(),
3289 span: Some(self.current_span()),
3290 });
3291 }
3292 Ok(FormatSpec::TemplateInline { sections })
3293 }
3294 _ => {
3295 let template = self.parse_string_literal()?;
3296 Ok(FormatSpec::Template { template })
3297 }
3298 }
3299 }
3300 Some(SpannedToken {
3301 token: Token::Ident(name),
3302 ..
3303 }) if name.eq_ignore_ascii_case("preset") => {
3304 self.advance();
3305 let preset_name = if self.at_exact(&Token::LParen) {
3307 self.advance(); let n = self.parse_string_literal()?;
3309 self.expect_exact(&Token::RParen)?;
3310 n
3311 } else {
3312 self.parse_string_literal()?
3313 };
3314 Ok(FormatSpec::Preset { name: preset_name })
3315 }
3316 Some(SpannedToken {
3317 token: Token::Ident(name),
3318 ..
3319 }) if name.eq_ignore_ascii_case("csv") => {
3320 self.advance();
3321 Ok(FormatSpec::Csv)
3322 }
3323 Some(SpannedToken {
3324 token: Token::Ident(name),
3325 ..
3326 }) if name.eq_ignore_ascii_case("table") => {
3327 self.advance();
3328 Ok(FormatSpec::Table)
3329 }
3330 Some(SpannedToken {
3331 token: Token::Ident(name),
3332 ..
3333 }) => {
3334 let name = name.clone();
3335 let span = self.current_span();
3336 Err(CalError::UnexpectedToken {
3337 expected: "format type (json, yaml, markdown, text, sml, toon, triples, csv, table, template, or preset)".into(),
3338 found: name,
3339 span: Some(span),
3340 suggestion: Some("Valid formats: json, yaml, markdown, text, sml, toon, triples, csv, table, template \"...\", preset(\"...\")".into()),
3341 })
3342 }
3343 Some(st) => {
3344 let found = st.token.description();
3345 let span = st.span;
3346 Err(CalError::UnexpectedToken {
3347 expected: "format type (json, yaml, markdown, text, sml, toon, triples, or preset name)".into(),
3348 found,
3349 span: Some(span),
3350 suggestion: None,
3351 })
3352 }
3353 None => Err(CalError::UnexpectedToken {
3354 expected: "format type".into(),
3355 found: "<end of query>".into(),
3356 span: None,
3357 suggestion: None,
3358 }),
3359 }
3360 }
3361
3362 const MAX_USER_VARS: usize = 10;
3366
3367 const MAX_USER_VAR_SIZE: usize = 1024;
3369
3370 fn peek_next_is_of(&self) -> bool {
3377 matches!(
3378 self.tokens.get(self.pos + 1),
3379 Some(SpannedToken {
3380 token: Token::Of,
3381 ..
3382 })
3383 )
3384 }
3385
3386 fn peek_next_is_vars(&self) -> bool {
3387 if !self.at_exact(&Token::With) {
3388 return false;
3389 }
3390 matches!(
3392 self.tokens.get(self.pos + 1),
3393 Some(SpannedToken {
3394 token: Token::Vars,
3395 ..
3396 })
3397 )
3398 }
3399
3400 fn parse_user_vars(&mut self) -> CalResult<HashMap<String, String>> {
3405 let span_start = self.current_span();
3406 self.expect_exact(&Token::With)?;
3407 self.expect_exact(&Token::Vars)?;
3408 self.expect_exact(&Token::LBrace)?;
3409
3410 let mut vars = HashMap::new();
3411
3412 if self.at_exact(&Token::RBrace) {
3414 self.advance();
3415 return Ok(vars);
3416 }
3417
3418 loop {
3419 let key = self.parse_string_literal()?;
3421
3422 if key.is_empty()
3424 || (!key.as_bytes()[0].is_ascii_alphabetic() && key.as_bytes()[0] != b'_')
3425 {
3426 return Err(CalError::UnexpectedToken {
3427 expected: "valid variable name (must start with a letter or underscore)".into(),
3428 found: key,
3429 span: Some(span_start),
3430 suggestion: Some("variable names must match [a-zA-Z_][a-zA-Z0-9_]*".into()),
3431 });
3432 }
3433 if !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
3434 return Err(CalError::UnexpectedToken {
3435 expected: "valid variable name (alphanumeric and underscores only)".into(),
3436 found: key,
3437 span: Some(span_start),
3438 suggestion: Some("variable names must match [a-zA-Z_][a-zA-Z0-9_]*".into()),
3439 });
3440 }
3441
3442 self.expect_exact(&Token::Colon)?;
3444
3445 let value = self.parse_string_literal()?;
3447
3448 if value.len() > Self::MAX_USER_VAR_SIZE {
3450 return Err(CalError::UserVarTooLarge {
3451 key,
3452 size: value.len(),
3453 max: Self::MAX_USER_VAR_SIZE,
3454 span: Some(span_start),
3455 });
3456 }
3457
3458 vars.insert(key, value);
3459
3460 if vars.len() > Self::MAX_USER_VARS {
3462 return Err(CalError::TooManyUserVars {
3463 count: vars.len(),
3464 max: Self::MAX_USER_VARS,
3465 span: Some(span_start),
3466 });
3467 }
3468
3469 if !self.eat_exact(&Token::Comma) {
3471 break;
3472 }
3473 if self.at_exact(&Token::RBrace) {
3475 break;
3476 }
3477 }
3478
3479 self.expect_exact(&Token::RBrace)?;
3480
3481 Ok(vars)
3482 }
3483
3484 fn parse_assemble(&mut self) -> CalResult<CalStatement> {
3487 let span_start = self.current_span();
3488 self.expect_exact(&Token::Assemble)?;
3489
3490 let topic = if self.at(&Token::StringLiteral("".into())) {
3493 self.parse_string_literal()?
3494 } else if let Some(SpannedToken {
3495 token: Token::Ident(word),
3496 ..
3497 }) = self.peek()
3498 {
3499 let word = word.clone();
3500 self.advance();
3501 word
3502 } else {
3503 String::new()
3504 };
3505 const MAX_CONTEXT_NAME_LEN: usize = 64;
3507 if topic.chars().count() > MAX_CONTEXT_NAME_LEN {
3508 return Err(CalError::UnexpectedToken {
3509 expected: format!("context name ≤ {} characters", MAX_CONTEXT_NAME_LEN),
3510 found: format!("{}-character context name", topic.chars().count()),
3511 span: Some(span_start),
3512 suggestion: Some(format!(
3513 "Shorten the ASSEMBLE context name (spec §8.2: max {} chars).",
3514 MAX_CONTEXT_NAME_LEN
3515 )),
3516 });
3517 }
3518 let context_name = if !topic.is_empty() {
3519 Some(topic.clone())
3520 } else {
3521 None
3522 };
3523
3524 const MAX_FOR_LEN: usize = 256;
3526 let for_whom = if self.at_exact(&Token::For) {
3527 self.advance();
3528 let s = self.parse_string_literal()?;
3529 if s.chars().count() > MAX_FOR_LEN {
3530 return Err(CalError::UnexpectedToken {
3531 expected: format!("FOR string ≤ {} characters", MAX_FOR_LEN),
3532 found: format!("{}-character FOR clause", s.chars().count()),
3533 span: Some(span_start),
3534 suggestion: Some(format!(
3535 "Shorten the ASSEMBLE FOR string (spec §8.2: max {} chars).",
3536 MAX_FOR_LEN
3537 )),
3538 });
3539 }
3540 Some(s)
3541 } else {
3542 None
3543 };
3544
3545 self.expect_exact(&Token::From)?;
3547 let (from, sources) = self.parse_assemble_from_clause()?;
3548
3549 if let Some(ref srcs) = sources {
3551 if srcs.len() > 8 {
3553 return Err(CalError::AssembleTooManySources {
3554 count: srcs.len(),
3555 max: 8,
3556 span: Some(span_start),
3557 });
3558 }
3559 let mut seen_labels = std::collections::HashSet::new();
3561 for src in srcs {
3562 if !seen_labels.insert(&src.label) {
3563 return Err(CalError::AssembleDuplicateLabel {
3564 label: src.label.clone(),
3565 span: src.span,
3566 });
3567 }
3568 }
3569 }
3570
3571 let where_clause = self.parse_where_clause()?;
3573
3574 if sources.is_some() && where_clause.is_some() {
3576 return Err(CalError::UnexpectedToken {
3577 expected: "BUDGET, PRIORITY, FORMAT, or WITH".into(),
3578 found: "WHERE".into(),
3579 span: where_clause.as_ref().and_then(|wc| wc.span),
3580 suggestion: Some(
3581 "WHERE is not supported with multi-source ASSEMBLE — apply WHERE inside each source's RECALL instead".into(),
3582 ),
3583 });
3584 }
3585
3586 let budget = if self.at_exact(&Token::Budget) {
3588 self.advance();
3589 let bspan = self.current_span();
3590 let raw = self.parse_u64()?;
3591 if raw > u32::MAX as u64 {
3593 return Err(CalError::AssembleBudgetExceeded {
3594 value: raw,
3595 max: 16000,
3596 unit: "tokens".into(),
3597 span: Some(bspan),
3598 });
3599 }
3600 let tokens = raw as u32;
3601 let unit = if let Some(SpannedToken {
3603 token: Token::Ident(id),
3604 ..
3605 }) = self.peek()
3606 {
3607 match id.to_ascii_lowercase().as_str() {
3608 "tokens" => {
3609 self.advance();
3610 BudgetUnit::Tokens
3611 }
3612 "grains" => {
3613 self.advance();
3614 BudgetUnit::Grains
3615 }
3616 _ => BudgetUnit::Tokens,
3617 }
3618 } else {
3619 BudgetUnit::Tokens
3620 };
3621 if tokens == 0 || tokens > 16000 {
3623 return Err(CalError::AssembleBudgetExceeded {
3624 value: tokens as u64,
3625 max: 16000,
3626 unit: match unit {
3627 BudgetUnit::Tokens => "tokens",
3628 BudgetUnit::Grains => "grains",
3629 }
3630 .into(),
3631 span: Some(bspan),
3632 });
3633 }
3634 Some(BudgetSpec {
3635 tokens,
3636 unit,
3637 span: Some(bspan),
3638 })
3639 } else {
3640 None
3641 };
3642
3643 let priority = if self.at_exact(&Token::Priority) {
3647 self.advance();
3648 let is_weighted = matches!(
3654 (self.peek_ahead(0), self.peek_ahead(1)),
3655 (
3656 Some(_),
3657 Some(SpannedToken {
3658 token: Token::Colon,
3659 ..
3660 })
3661 )
3662 );
3663 if !is_weighted {
3664 let mut labels = vec![];
3666 loop {
3667 let pspan = self.current_span();
3668 let label = self.parse_label()?;
3669 labels.push((label, pspan));
3670 if !self.eat_exact(&Token::Gt) {
3671 break;
3672 }
3673 }
3674 let n = labels.len() as f64;
3676 let specs = labels
3677 .into_iter()
3678 .enumerate()
3679 .map(|(i, (label, pspan))| PrioritySpec {
3680 label,
3681 weight: (n - i as f64) / n,
3682 span: Some(pspan),
3683 })
3684 .collect();
3685 Some(specs)
3686 } else {
3687 let mut specs = vec![];
3689 loop {
3690 let pspan = self.current_span();
3691 let label = self.parse_label()?;
3692 self.expect_exact(&Token::Colon)?;
3693 let weight = self.parse_number()?;
3694 if !(0.0..=1.0).contains(&weight) {
3696 return Err(CalError::UnexpectedToken {
3697 expected: "weight between 0.0 and 1.0".into(),
3698 found: format!("{}", weight),
3699 span: Some(pspan),
3700 suggestion: Some(
3701 "PRIORITY weights must be in the range 0.0 to 1.0".into(),
3702 ),
3703 });
3704 }
3705 specs.push(PrioritySpec {
3706 label,
3707 weight,
3708 span: Some(pspan),
3709 });
3710 if !self.eat_exact(&Token::Comma) {
3711 break;
3712 }
3713 }
3714 Some(specs)
3715 }
3716 } else {
3717 None
3718 };
3719
3720 if let (Some(ref prio_specs), Some(ref srcs)) = (&priority, &sources) {
3722 let source_labels: std::collections::HashSet<&str> =
3723 srcs.iter().map(|s| s.label.as_str()).collect();
3724 for ps in prio_specs {
3725 if !source_labels.contains(ps.label.as_str()) {
3726 return Err(CalError::AssemblePriorityMismatch {
3727 label: ps.label.clone(),
3728 span: ps.span,
3729 });
3730 }
3731 }
3732 }
3733
3734 let format = if self.at_exact(&Token::Format) {
3736 Some(self.parse_format()?)
3737 } else {
3738 None
3739 };
3740
3741 let (assemble_with, assemble_with_options) = if self.at_exact(&Token::With)
3755 && matches!(
3756 self.peek_ahead(1),
3757 Some(SpannedToken {
3758 token: Token::Dedup,
3759 ..
3760 })
3761 ) {
3762 let mut aw = Vec::new();
3763 let mut opts = Vec::new();
3764 for opt in self.parse_with_clause()? {
3765 match opt {
3766 WithOption::Dedup { field } => aw.push(AssembleWithOption::Dedup { field }),
3767 other => opts.push(other),
3768 }
3769 }
3770 (aw, opts)
3771 } else {
3772 (Vec::new(), Vec::new())
3773 };
3774
3775 if let Some(st) = self.peek() {
3787 let late = match st.token {
3788 Token::Budget => Some("BUDGET"),
3789 Token::Priority => Some("PRIORITY"),
3790 Token::For => Some("FOR"),
3791 Token::Where => Some("WHERE"),
3792 Token::Format => Some("FORMAT"),
3793 Token::From => Some("FROM"),
3794 _ => None,
3795 };
3796 if let Some(clause) = late {
3797 return Err(CalError::UnexpectedToken {
3798 expected: "end of the ASSEMBLE statement".into(),
3799 found: format!("{clause} clause out of order"),
3800 span: Some(st.span),
3801 suggestion: Some(
3802 "ASSEMBLE clauses are ordered: ASSEMBLE \"name\" FOR \"…\" FROM … \
3803 [WHERE …] BUDGET n PRIORITY … FORMAT … WITH dedup. A clause written \
3804 out of order used to be dropped silently — put it in this order."
3805 .into(),
3806 ),
3807 });
3808 }
3809 }
3810
3811 let span_end = self.prev_span();
3812 Ok(CalStatement::Assemble(AssembleStmt {
3813 topic,
3814 from,
3815 where_clause,
3816 context_name,
3817 sources,
3818 budget,
3819 priority,
3820 format,
3821 for_whom,
3822 assemble_with,
3823 with_options: assemble_with_options,
3824 streaming: false,
3825 span: Some(Span::new(
3826 span_start.start,
3827 span_end.end,
3828 span_start.line,
3829 span_start.col,
3830 )),
3831 }))
3832 }
3833
3834 fn parse_assemble_from_clause(&mut self) -> CalResult<(Source, Option<Vec<NamedSource>>)> {
3837 let is_multi_source = matches!(
3840 (self.peek(), self.peek_ahead(1)),
3841 (
3842 Some(st),
3843 Some(SpannedToken {
3844 token: Token::Colon,
3845 ..
3846 }),
3847 ) if matches!(st.token, Token::Ident(_)) || Self::is_word_token(&st.token)
3848 );
3849
3850 if is_multi_source {
3851 let mut sources = vec![];
3852 loop {
3853 let sspan = self.current_span();
3854 let label = self.parse_label()?;
3855 self.expect_exact(&Token::Colon)?;
3856 let pinned = self.eat_exact(&Token::Pin);
3861 if self.at_exact(&Token::Literal) {
3863 self.advance();
3864 let text = self.parse_string_literal()?;
3865 sources.push(NamedSource {
3866 label,
3867 query: Box::new(CalStatement::Assemble(AssembleStmt {
3870 topic: String::new(),
3871 from: Source::Parameter {
3872 name: "_literal".to_string(),
3873 },
3874 where_clause: None,
3875 context_name: None,
3876 sources: None,
3877 budget: None,
3878 priority: None,
3879 format: None,
3880 for_whom: None,
3881 assemble_with: Vec::new(),
3882 with_options: Vec::new(),
3883 streaming: false,
3884 span: Some(sspan),
3885 })),
3886 literal: Some(text),
3887 pinned,
3888 with_options: Vec::new(),
3889 span: Some(sspan),
3890 });
3891 if !self.eat_exact(&Token::Comma) {
3892 break;
3893 }
3894 continue;
3895 }
3896 let (query, inside_with) = self.parse_assemble_source_query()?;
3899 let mut with_options = inside_with;
3901 let outside_with = if self.at_exact(&Token::With) && !self.peek_next_is_vars() {
3902 self.parse_with_clause()?
3903 } else {
3904 vec![]
3905 };
3906 with_options.extend(outside_with); sources.push(NamedSource {
3908 label,
3909 query: Box::new(query),
3910 literal: None,
3911 pinned,
3912 with_options,
3913 span: Some(sspan),
3914 });
3915 if !self.eat_exact(&Token::Comma) {
3916 break;
3917 }
3918 }
3919 let default_from = Source::Parameter {
3921 name: "_multi_source".to_string(),
3922 };
3923 Ok((default_from, Some(sources)))
3924 } else {
3925 let from = self.parse_assemble_source()?;
3926 Ok((from, None))
3927 }
3928 }
3929
3930 fn parse_assemble_source(&mut self) -> CalResult<Source> {
3932 match self.peek() {
3933 Some(SpannedToken {
3934 token: Token::Parameter(_),
3935 ..
3936 }) => {
3937 let name = self.parse_parameter()?;
3938 Ok(Source::Parameter { name })
3939 }
3940 Some(SpannedToken {
3941 token: Token::LParen,
3942 ..
3943 }) => {
3944 self.advance();
3946 self.enter_nesting()?;
3947 let inner = self.parse_recall_stmt()?;
3949 self.leave_nesting();
3950 self.expect_exact(&Token::RParen)?;
3951 Ok(Source::Query(Box::new(inner)))
3952 }
3953 Some(SpannedToken {
3954 token: Token::HashLiteral(_),
3955 ..
3956 }) => {
3957 let mut hashes = vec![];
3958 while self.at(&Token::HashLiteral("".into())) {
3959 hashes.push(self.parse_hash_literal()?);
3960 if !self.eat_exact(&Token::Comma) {
3961 break;
3962 }
3963 }
3964 Ok(Source::Hashes(hashes))
3965 }
3966 Some(SpannedToken {
3967 token: Token::Recall,
3968 ..
3969 }) => {
3970 let inner = self.parse_recall_stmt()?;
3972 Ok(Source::Query(Box::new(inner)))
3973 }
3974 Some(SpannedToken {
3977 token: Token::Ident(id),
3978 ..
3979 }) if GrainTypePlural::parse(id).is_some() => {
3980 let span = self.current_span();
3981 let grain_type_str = if let Some(SpannedToken {
3983 token: Token::Ident(id),
3984 ..
3985 }) = self.advance()
3986 {
3987 id.clone()
3988 } else {
3989 "facts".to_string()
3990 };
3991 let grain_type =
3992 GrainTypePlural::parse(&grain_type_str).unwrap_or(GrainTypePlural::Facts);
3993 let where_clause = self.parse_where_clause()?;
3994 let recent = self.parse_recent_clause()?;
3995 let inner = RecallStmt {
3996 grain_type,
3997 about: None,
3998 where_clause,
3999 recent,
4000 since: None,
4001 until: None,
4002 like: None,
4003 between: None,
4004 contradictions: None,
4005 limit: None,
4006 as_format: None,
4007 span: Some(span),
4008 };
4009 Ok(Source::Query(Box::new(inner)))
4010 }
4011 Some(st) => {
4012 let found = st.token.description();
4013 let span = st.span;
4014 Err(CalError::UnexpectedToken {
4015 expected:
4016 "source (RECALL ..., $parameter, sha256:..., grain type, or (RECALL ...))"
4017 .into(),
4018 found,
4019 span: Some(span),
4020 suggestion: None,
4021 })
4022 }
4023 None => Err(CalError::UnexpectedToken {
4024 expected: "FROM source".into(),
4025 found: "<end of query>".into(),
4026 span: None,
4027 suggestion: None,
4028 }),
4029 }
4030 }
4031
4032 fn parse_exists(&mut self) -> CalResult<CalStatement> {
4035 let span_start = self.current_span();
4036 self.expect_exact(&Token::Exists)?;
4037
4038 match self.peek() {
4043 Some(SpannedToken {
4044 token: Token::HashLiteral(_),
4045 ..
4046 }) => {
4047 let hash = self.parse_hash_literal()?;
4048 let span_end = self.prev_span();
4050 Ok(CalStatement::Exists(ExistsStmt {
4051 grain_type: GrainTypePlural::All,
4052 where_clause: Some(WhereClause {
4053 condition: Condition::Comparison {
4054 field: "hash".into(),
4055 comparator: Comparator::Eq,
4056 value: Value::Hash { value: hash },
4057 span: None,
4058 },
4059 span: None,
4060 }),
4061 about: None,
4062 span: Some(Span::new(
4063 span_start.start,
4064 span_end.end,
4065 span_start.line,
4066 span_start.col,
4067 )),
4068 }))
4069 }
4070 Some(SpannedToken {
4071 token: Token::Parameter(_),
4072 ..
4073 }) => {
4074 let name = self.parse_parameter()?;
4075 let span_end = self.prev_span();
4076 Ok(CalStatement::Exists(ExistsStmt {
4077 grain_type: GrainTypePlural::All,
4078 where_clause: Some(WhereClause {
4079 condition: Condition::Comparison {
4080 field: "hash".into(),
4081 comparator: Comparator::Eq,
4082 value: Value::Parameter { name },
4083 span: None,
4084 },
4085 span: None,
4086 }),
4087 about: None,
4088 span: Some(Span::new(
4089 span_start.start,
4090 span_end.end,
4091 span_start.line,
4092 span_start.col,
4093 )),
4094 }))
4095 }
4096 _ => {
4097 let grain_type = self.parse_grain_type_plural()?;
4099 let about = self.parse_about_clause()?;
4100 let where_clause = self.parse_where_clause()?;
4101 let span_end = self.prev_span();
4102 Ok(CalStatement::Exists(ExistsStmt {
4103 grain_type,
4104 where_clause,
4105 about,
4106 span: Some(Span::new(
4107 span_start.start,
4108 span_end.end,
4109 span_start.line,
4110 span_start.col,
4111 )),
4112 }))
4113 }
4114 }
4115 }
4116
4117 fn parse_history(&mut self) -> CalResult<CalStatement> {
4120 let span_start = self.current_span();
4121 self.expect_exact(&Token::History)?;
4122
4123 self.eat_exact(&Token::Of);
4125
4126 if self.at_exact(&Token::Where) {
4130 let where_clause = self.parse_where_clause()?;
4132
4133 let diff_target: Option<String> = if self.at_exact(&Token::Diff) {
4134 self.advance();
4135 Some(self.parse_hash_literal()?)
4136 } else {
4137 None
4138 };
4139
4140 let span_end = self.prev_span();
4141 return Ok(CalStatement::History(HistoryStmt {
4142 hash: String::new(),
4143 where_clause,
4144 diff_target,
4145 span: Some(Span::new(
4146 span_start.start,
4147 span_end.end,
4148 span_start.line,
4149 span_start.col,
4150 )),
4151 }));
4152 }
4153
4154 let hash = self.parse_hash_literal()?;
4156
4157 let diff_target: Option<String> = if self.at_exact(&Token::Diff) {
4159 self.advance();
4160 Some(self.parse_hash_literal()?)
4161 } else {
4162 None
4163 };
4164
4165 let span_end = self.prev_span();
4166 Ok(CalStatement::History(HistoryStmt {
4167 hash,
4168 where_clause: None,
4169 diff_target,
4170 span: Some(Span::new(
4171 span_start.start,
4172 span_end.end,
4173 span_start.line,
4174 span_start.col,
4175 )),
4176 }))
4177 }
4178
4179 fn parse_explain(&mut self) -> CalResult<CalStatement> {
4182 let span_start = self.current_span();
4183 self.expect_exact(&Token::Explain)?;
4184 let inner = self.parse_statement()?;
4185 let span_end = self.prev_span();
4186
4187 match &inner {
4191 CalStatement::Describe(_)
4192 | CalStatement::DefineTemplate(_)
4193 | CalStatement::DropTemplate(_)
4194 | CalStatement::DefineQuery(_)
4195 | CalStatement::DropQuery(_) => {
4196 return Err(CalError::UnexpectedToken {
4197 expected: "RECALL, ASSEMBLE, set operation, ADD, SUPERSEDE, REVERT, BATCH, or COALESCE".into(),
4198 found: format!("{} statement", match &inner {
4199 CalStatement::Describe(_) => "DESCRIBE",
4200 CalStatement::DefineTemplate(_) => "DEFINE TEMPLATE",
4201 CalStatement::DropTemplate(_) => "DROP TEMPLATE",
4202 CalStatement::DefineQuery(_) => "DEFINE QUERY",
4203 CalStatement::DropQuery(_) => "DROP QUERY",
4204 _ => unreachable!(),
4205 }),
4206 span: Some(span_start),
4207 suggestion: Some(
4208 "EXPLAIN may only wrap statements that produce an execution plan (spec §8.5).".into(),
4209 ),
4210 });
4211 }
4212 _ => {}
4213 }
4214
4215 Ok(CalStatement::Explain(ExplainStmt {
4216 inner: Box::new(inner),
4217 span: Some(Span::new(
4218 span_start.start,
4219 span_end.end,
4220 span_start.line,
4221 span_start.col,
4222 )),
4223 }))
4224 }
4225
4226 fn parse_describe(&mut self) -> CalResult<CalStatement> {
4229 let span_start = self.current_span();
4230 self.expect_exact(&Token::Describe)?;
4231
4232 let target = match self.peek() {
4233 Some(SpannedToken {
4234 token: Token::Ident(s),
4235 ..
4236 }) => {
4237 let s = s.clone();
4238 if let Some(gt) = GrainTypePlural::parse(&s) {
4240 self.advance();
4241 DescribeTarget::GrainType(gt)
4242 } else if s.eq_ignore_ascii_case("schema") {
4243 self.advance();
4244 DescribeTarget::Schema
4245 } else if s.eq_ignore_ascii_case("capabilities") {
4246 self.advance();
4247 DescribeTarget::Capabilities
4248 } else if s.eq_ignore_ascii_case("server") {
4249 self.advance();
4250 DescribeTarget::Server
4251 } else if s.eq_ignore_ascii_case("fields") {
4252 self.advance();
4253 let gt = self.parse_grain_type_plural_opt()?;
4255 DescribeTarget::Fields(gt)
4256 } else if s.eq_ignore_ascii_case("templates") {
4257 self.advance();
4258 DescribeTarget::Templates
4259 } else if s.eq_ignore_ascii_case("grammar") {
4260 self.advance();
4261 DescribeTarget::Grammar
4262 } else if s.eq_ignore_ascii_case("queries") {
4263 self.advance();
4264 DescribeTarget::Queries
4265 } else if s.eq_ignore_ascii_case("principal") {
4266 self.advance();
4267 let name = self.parse_string_literal()?;
4268 DescribeTarget::Principal(name)
4269 } else if s.eq_ignore_ascii_case("loop") {
4270 self.advance();
4271 DescribeTarget::Loop
4272 } else if s.eq_ignore_ascii_case("analyzers") {
4273 self.advance();
4274 DescribeTarget::Analyzers
4275 } else if s.eq_ignore_ascii_case("outcomes") {
4276 self.advance();
4277 DescribeTarget::Outcomes
4278 } else if s.eq_ignore_ascii_case("policy") {
4279 self.advance();
4280 DescribeTarget::LoopPolicy
4281 } else if s.eq_ignore_ascii_case("stats") {
4282 self.advance();
4283 DescribeTarget::Stats
4284 } else if s.eq_ignore_ascii_case("integrity") {
4285 self.advance();
4286 DescribeTarget::Integrity
4287 } else {
4288 self.advance();
4289 DescribeTarget::Schema
4290 }
4291 }
4292 Some(SpannedToken {
4294 token: Token::Query,
4295 ..
4296 }) => {
4297 self.advance(); let name = self.parse_string_literal()?;
4299 DescribeTarget::Query(name)
4300 }
4301 _ => DescribeTarget::Schema,
4302 };
4303
4304 let span_end = self.prev_span();
4305 Ok(CalStatement::Describe(DescribeStmt {
4306 target,
4307 span: Some(Span::new(
4308 span_start.start,
4309 span_end.end,
4310 span_start.line,
4311 span_start.col,
4312 )),
4313 }))
4314 }
4315
4316 fn parse_batch(&mut self) -> CalResult<CalStatement> {
4319 let span_start = self.current_span();
4320 self.expect_exact(&Token::Batch)?;
4321 self.expect_exact(&Token::LBrace)?;
4322 self.enter_nesting()?;
4323
4324 let mut entries = vec![];
4325 while !self.at_exact(&Token::RBrace) && !self.at_end() {
4326 if entries.len() >= MAX_BATCH_ENTRIES {
4327 return Err(CalError::TooManyPipelineStages {
4328 count: entries.len() + 1,
4329 max: MAX_BATCH_ENTRIES,
4330 span: Some(self.current_span()),
4331 });
4332 }
4333 let is_label = match self.peek() {
4339 Some(st) if !st.token.is_statement_starter() => {
4340 self.peek_ahead(1).map(|t| &t.token) == Some(&Token::Colon)
4341 }
4342 _ => false,
4343 };
4344 if is_label {
4345 self.advance(); self.advance(); }
4348 let (stmt, pipeline, with_options, format, user_vars) = self.parse_statement_full()?;
4350 entries.push(BatchEntry {
4351 statement: stmt,
4352 pipeline,
4353 with_options,
4354 format,
4355 user_vars,
4356 });
4357 self.eat_exact(&Token::Comma);
4359 self.eat_exact(&Token::Semicolon);
4360 }
4361
4362 self.leave_nesting();
4363 self.expect_exact(&Token::RBrace)?;
4364
4365 if entries.is_empty() {
4367 return Err(CalError::UnexpectedToken {
4368 expected: "at least one statement inside BATCH { ... }".into(),
4369 found: "empty batch".into(),
4370 span: Some(span_start),
4371 suggestion: Some("BATCH requires one or more statements (spec §8.7).".into()),
4372 });
4373 }
4374
4375 let span_end = self.prev_span();
4376 Ok(CalStatement::Batch(BatchStmt {
4377 statements: entries,
4378 labeled: None,
4379 span: Some(Span::new(
4380 span_start.start,
4381 span_end.end,
4382 span_start.line,
4383 span_start.col,
4384 )),
4385 }))
4386 }
4387
4388 fn parse_coalesce(&mut self) -> CalResult<CalStatement> {
4391 let span_start = self.current_span();
4392 self.expect_exact(&Token::Coalesce)?;
4393
4394 if self.at_exact(&Token::LBrace) {
4398 return self.parse_coalesce_braces(span_start);
4400 }
4401
4402 self.expect_exact(&Token::LParen)?;
4404 self.enter_nesting()?;
4405
4406 let mut statements = vec![];
4407 while !self.at_exact(&Token::RParen) && !self.at_end() {
4408 let stmt = self.parse_statement()?;
4409 statements.push(stmt);
4410 if !self.eat_exact(&Token::Comma) {
4411 break;
4412 }
4413 }
4414
4415 self.leave_nesting();
4416 self.expect_exact(&Token::RParen)?;
4417
4418 const COALESCE_MIN: usize = 2;
4420 const COALESCE_MAX: usize = 5;
4421 if statements.len() < COALESCE_MIN {
4422 return Err(CalError::UnexpectedToken {
4423 expected: format!("at least {} branches in COALESCE(...)", COALESCE_MIN),
4424 found: format!("{} branches", statements.len()),
4425 span: Some(span_start),
4426 suggestion: Some("COALESCE requires 2-5 RECALL branches (spec §8.13).".into()),
4427 });
4428 }
4429 if statements.len() > COALESCE_MAX {
4430 return Err(CalError::CoalesceTooManyBranches {
4431 count: statements.len(),
4432 max: COALESCE_MAX,
4433 span: Some(span_start),
4434 });
4435 }
4436
4437 let grain_type = GrainTypePlural::All;
4439 let span_end = self.prev_span();
4440
4441 Ok(CalStatement::Coalesce(CoalesceStmt {
4442 grain_type,
4443 where_clause: None,
4444 branches: statements
4445 .into_iter()
4446 .map(|q| CoalesceBranch {
4447 query: q,
4448 span: None,
4449 })
4450 .collect(),
4451 else_branch: None,
4452 span: Some(Span::new(
4453 span_start.start,
4454 span_end.end,
4455 span_start.line,
4456 span_start.col,
4457 )),
4458 }))
4459 }
4460
4461 fn parse_coalesce_braces(&mut self, span_start: Span) -> CalResult<CalStatement> {
4464 let mut branches = vec![];
4465
4466 let branch = self.parse_brace_statement()?;
4468 branches.push(CoalesceBranch {
4469 span: Some(self.prev_span()),
4470 query: branch,
4471 });
4472
4473 while self.at_exact(&Token::Or) {
4475 self.advance(); let branch = self.parse_brace_statement()?;
4477 branches.push(CoalesceBranch {
4478 span: Some(self.prev_span()),
4479 query: branch,
4480 });
4481 }
4482
4483 let else_branch = if self.at_exact(&Token::Ident("ELSE".into()))
4485 || self
4486 .peek()
4487 .map(|st| st.token.description().eq_ignore_ascii_case("ELSE"))
4488 .unwrap_or(false)
4489 {
4490 if let Some(SpannedToken {
4492 token: Token::Ident(s),
4493 ..
4494 }) = self.peek()
4495 {
4496 if s.eq_ignore_ascii_case("else") {
4497 self.advance();
4498 let stmt = self.parse_brace_statement()?;
4499 Some(Box::new(stmt))
4500 } else {
4501 None
4502 }
4503 } else {
4504 None
4505 }
4506 } else {
4507 None
4508 };
4509
4510 let span_end = self.prev_span();
4511 Ok(CalStatement::Coalesce(CoalesceStmt {
4512 grain_type: GrainTypePlural::All,
4513 where_clause: None,
4514 branches,
4515 else_branch,
4516 span: Some(Span::new(
4517 span_start.start,
4518 span_end.end,
4519 span_start.line,
4520 span_start.col,
4521 )),
4522 }))
4523 }
4524
4525 fn parse_brace_statement(&mut self) -> CalResult<CalStatement> {
4527 self.expect_exact(&Token::LBrace)?;
4528 self.enter_nesting()?;
4529 let stmt = self.parse_statement()?;
4530 self.leave_nesting();
4531 self.expect_exact(&Token::RBrace)?;
4532 Ok(stmt)
4533 }
4534
4535 fn parse_add(&mut self) -> CalResult<CalStatement> {
4538 let span_start = self.current_span();
4539 self.expect_exact(&Token::Add)?;
4540
4541 if let Some(SpannedToken {
4543 token: Token::Ident(s),
4544 ..
4545 }) = self.peek()
4546 {
4547 if s.eq_ignore_ascii_case("workflow") {
4548 self.advance(); return self.parse_add_workflow(span_start);
4550 }
4551 }
4552
4553 let grain_type = self.parse_grain_type_singular()?;
4554
4555 let mut fields = vec![];
4557 let mut seen_fields = std::collections::HashSet::new();
4558 while self.at_exact(&Token::Set) {
4559 self.advance();
4560 let fspan = self.current_span();
4561 let field = self.parse_identifier()?;
4562 self.expect_exact(&Token::Eq)?;
4563 let value = self.parse_value()?;
4564 if !seen_fields.insert(field.clone()) {
4565 self.warnings.push(CalWarning::DuplicateSetField {
4566 field: field.clone(),
4567 span: Some(fspan),
4568 });
4569 }
4570 fields.push(FieldAssignment {
4571 field,
4572 value,
4573 span: Some(fspan),
4574 });
4575 }
4576
4577 if fields.is_empty() {
4578 return Err(CalError::MissingSetClause {
4579 span: Some(self.current_span()),
4580 });
4581 }
4582
4583 let with_options = if self.at_exact(&Token::With) {
4585 self.parse_add_with_clause()?
4586 } else {
4587 vec![]
4588 };
4589
4590 let reason = self.parse_reason_clause()?;
4592
4593 let span_end = self.prev_span();
4594 Ok(CalStatement::Add(AddStmt {
4595 grain_type,
4596 fields,
4597 reason,
4598 with_options,
4599 span: Some(Span::new(
4600 span_start.start,
4601 span_end.end,
4602 span_start.line,
4603 span_start.col,
4604 )),
4605 }))
4606 }
4607
4608 fn parse_add_workflow(&mut self, span_start: Span) -> CalResult<CalStatement> {
4612 let name = self.parse_string_literal()?;
4614
4615 if self.at_exact(&Token::On) {
4621 return Err(CalError::UnexpectedToken {
4622 expected: "the plan's graph — `ON \"...\"` was removed in 1.3; a trigger is now \
4623 its own grain that points at this plan (see `areev trigger add`)"
4624 .into(),
4625 found: "ON".into(),
4626 span: Some(self.current_span()),
4627 suggestion: Some(
4628 "declare a trigger with `areev trigger add --workflow <this plan's hash>`"
4629 .into(),
4630 ),
4631 });
4632 }
4633
4634 let (nodes, edges) = self.parse_workflow_graph()?;
4636
4637 let mut bindings = vec![];
4639 while self.at_exact(&Token::Bind) {
4640 bindings.push(self.parse_bind_clause()?);
4641 }
4642
4643 let with_options = if self.at_exact(&Token::With) {
4645 self.parse_add_with_clause()?
4646 } else {
4647 vec![]
4648 };
4649
4650 let reason = self.parse_reason_clause()?;
4652
4653 let span_end = self.prev_span();
4654 Ok(CalStatement::AddWorkflow(AddWorkflowStmt {
4655 name,
4656 nodes,
4657 edges,
4658 bindings,
4659 reason,
4660 with_options,
4661 span: Some(Span::new(
4662 span_start.start,
4663 span_end.end,
4664 span_start.line,
4665 span_start.col,
4666 )),
4667 }))
4668 }
4669
4670 fn parse_workflow_graph(&mut self) -> CalResult<(Vec<String>, Vec<GraphEdge>)> {
4680 let mut nodes: Vec<String> = Vec::new();
4681 let mut edges: Vec<GraphEdge> = Vec::new();
4682
4683 let ensure_node = |name: &str, nodes: &mut Vec<String>| {
4685 if !nodes.contains(&name.to_string()) {
4686 nodes.push(name.to_string());
4687 }
4688 };
4689
4690 let mut parsed_any = false;
4692 while self.is_graph_line_start() {
4693 parsed_any = true;
4694
4695 let mut sources = self.parse_node_or_group()?;
4697 for s in &sources {
4698 ensure_node(s, &mut nodes);
4699 }
4700
4701 while self.eat_exact(&Token::Arrow) {
4703 if self.at_exact(&Token::Arrow) {
4705 return Err(CalError::UnexpectedToken {
4706 expected: "node name after '->'".into(),
4707 found: "another '->'".into(),
4708 span: Some(self.current_span()),
4709 suggestion: Some(
4710 "remove the extra '->' — chain nodes with single arrows: a -> b -> c"
4711 .into(),
4712 ),
4713 });
4714 }
4715 let targets = self.parse_node_or_group()?;
4716 for t in &targets {
4717 ensure_node(t, &mut nodes);
4718 }
4719
4720 let cond = if self.eat_exact(&Token::When) {
4722 Some(self.parse_string_literal()?)
4723 } else {
4724 None
4725 };
4726
4727 let repeat = if self.eat_exact(&Token::Asterisk) {
4729 if !matches!(
4731 self.peek(),
4732 Some(SpannedToken {
4733 token: Token::NumberLiteral(_),
4734 ..
4735 })
4736 ) {
4737 let found = self
4738 .peek()
4739 .map(|st| st.token.description())
4740 .unwrap_or_else(|| "<end of query>".into());
4741 return Err(CalError::UnexpectedToken {
4742 expected: "number after '*' for retry count".into(),
4743 found,
4744 span: Some(self.current_span()),
4745 suggestion: Some("* N sets the retry count — e.g.: a -> b * 3".into()),
4746 });
4747 }
4748 let n = self.parse_number()? as u32;
4749 if n == 0 {
4750 return Err(CalError::UnexpectedToken {
4751 expected: "repeat count >= 1".into(),
4752 found: "0".into(),
4753 span: Some(self.prev_span()),
4754 suggestion: Some(
4755 "repeat count must be at least 1 — e.g.: a -> b * 1".into(),
4756 ),
4757 });
4758 }
4759 Some(n)
4760 } else {
4761 None
4762 };
4763
4764 for s in &sources {
4766 for t in &targets {
4767 edges.push(GraphEdge {
4768 src: s.clone(),
4769 dst: t.clone(),
4770 cond: cond.clone(),
4771 repeat,
4772 });
4773 }
4774 }
4775
4776 sources = targets;
4779 }
4780 }
4781
4782 if self.at_exact(&Token::When) {
4784 return Err(CalError::UnexpectedToken {
4785 expected: "'->' edge before WHEN condition".into(),
4786 found: "WHEN".into(),
4787 span: Some(self.current_span()),
4788 suggestion: Some(
4789 "WHEN must follow a -> edge, not a node — e.g.: a -> b WHEN \"condition\""
4790 .into(),
4791 ),
4792 });
4793 }
4794 if self.at_exact(&Token::Arrow) && parsed_any {
4797 return Err(CalError::UnexpectedToken {
4798 expected: "node name after '->'".into(),
4799 found: self
4800 .peek()
4801 .map(|st| st.token.description())
4802 .unwrap_or_else(|| "<end of query>".into()),
4803 span: Some(self.current_span()),
4804 suggestion: Some("expected a node name or parallel group after '->'".into()),
4805 });
4806 }
4807
4808 if !parsed_any {
4809 return Err(CalError::UnexpectedToken {
4810 expected: "at least one node in workflow graph".into(),
4811 found: self
4812 .peek()
4813 .map(|st| st.token.description())
4814 .unwrap_or_else(|| "<end of query>".into()),
4815 span: Some(self.current_span()),
4816 suggestion: Some(
4817 "workflow body must contain node names and arrows, e.g.: build -> test -> deploy"
4818 .into(),
4819 ),
4820 });
4821 }
4822
4823 Ok((nodes, edges))
4824 }
4825
4826 fn is_graph_line_start(&self) -> bool {
4829 matches!(
4830 self.peek(),
4831 Some(SpannedToken {
4832 token: Token::Ident(_)
4833 | Token::StringLiteral(_)
4834 | Token::LParen
4835 | Token::Run
4836 | Token::Query,
4837 ..
4838 })
4839 )
4840 }
4841
4842 fn parse_node_or_group(&mut self) -> CalResult<Vec<String>> {
4844 if self.eat_exact(&Token::LParen) {
4845 self.enter_nesting()?;
4846 let mut group = vec![self.parse_node_name()?];
4847 while self.eat_exact(&Token::Comma) {
4848 group.push(self.parse_node_name()?);
4849 }
4850 self.leave_nesting();
4851 if !self.eat_exact(&Token::RParen) {
4852 let found = self
4853 .peek()
4854 .map(|st| st.token.description())
4855 .unwrap_or_else(|| "<end of query>".into());
4856 return Err(CalError::UnexpectedToken {
4857 expected: "')' to close parallel group".into(),
4858 found,
4859 span: Some(self.current_span()),
4860 suggestion: Some(
4861 "close the parallel group with ')' — e.g.: (build, test) -> deploy".into(),
4862 ),
4863 });
4864 }
4865 Ok(group)
4866 } else {
4867 Ok(vec![self.parse_node_name()?])
4868 }
4869 }
4870
4871 fn parse_node_name(&mut self) -> CalResult<String> {
4873 match self.peek() {
4874 Some(SpannedToken {
4875 token: Token::Ident(s),
4876 ..
4877 }) => {
4878 let s = s.clone();
4879 self.advance();
4880 Ok(s)
4881 }
4882 Some(SpannedToken {
4884 token: Token::Run, ..
4885 }) => {
4886 self.advance();
4887 Ok("run".to_string())
4888 }
4889 Some(SpannedToken {
4890 token: Token::Query,
4891 ..
4892 }) => {
4893 self.advance();
4894 Ok("query".to_string())
4895 }
4896 Some(SpannedToken {
4899 token:
4900 t @ (Token::Approve
4901 | Token::Reject
4902 | Token::Apply
4903 | Token::Rollback
4904 | Token::Grant
4905 | Token::Revoke
4906 | Token::Show
4907 | Token::Merge
4908 | Token::Related
4909 | Token::Novelty
4910 | Token::Remember
4911 | Token::Entity),
4912 ..
4913 }) => {
4914 let name = t.description().to_ascii_lowercase();
4915 self.advance();
4916 Ok(name)
4917 }
4918 Some(SpannedToken {
4919 token: Token::StringLiteral(_),
4920 ..
4921 }) => self.parse_string_literal(),
4922 Some(st) => {
4923 let found = st.token.description();
4924 let span = st.span;
4925 let suggestion = match &st.token {
4927 Token::Reason | Token::Because => Some(
4928 "expected node name after '->' — the arrow is dangling. \
4929 Add a target node: a -> b REASON \"...\""
4930 .into(),
4931 ),
4932 Token::RParen => Some(
4933 "parallel group must contain at least one node — \
4934 e.g.: (build, test)"
4935 .into(),
4936 ),
4937 _ => Some(
4938 "reserved words (ON, WHEN, BIND, REASON) must be quoted to use as node names"
4939 .into(),
4940 ),
4941 };
4942 Err(CalError::UnexpectedToken {
4943 expected: "node name (identifier or quoted string)".into(),
4944 found,
4945 span: Some(span),
4946 suggestion,
4947 })
4948 }
4949 None => Err(CalError::UnexpectedToken {
4950 expected: "node name after '->'".into(),
4951 found: "<end of query>".into(),
4952 span: None,
4953 suggestion: Some("the arrow is dangling — add a target node: a -> b".into()),
4954 }),
4955 }
4956 }
4957
4958 fn parse_bind_clause(&mut self) -> CalResult<BindClause> {
4960 self.expect_exact(&Token::Bind)?;
4961 let node = self.parse_node_name()?;
4962 self.expect_exact(&Token::Eq)?;
4963 let hash = self.parse_hash_literal()?;
4964 Ok(BindClause { node, hash })
4965 }
4966
4967 fn parse_reason_clause(&mut self) -> CalResult<String> {
4969 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
4970 return Err(CalError::MissingReason {
4971 span: Some(self.current_span()),
4972 });
4973 }
4974 self.advance();
4975 let rspan = self.current_span();
4976 let reason = self.parse_string_literal()?;
4977 if reason.len() > MAX_REASON_LENGTH {
4978 return Err(CalError::ReasonTooLong {
4979 length: reason.len(),
4980 max: MAX_REASON_LENGTH,
4981 span: Some(rspan),
4982 });
4983 }
4984 Ok(reason)
4985 }
4986
4987 fn parse_add_with_clause(&mut self) -> CalResult<Vec<AddWithOption>> {
4990 self.expect_exact(&Token::With)?;
4991 let mut options = vec![];
4992 loop {
4993 if let Some(opt) = self.parse_add_with_option()? {
4994 options.push(opt);
4995 }
4996 if !self.eat_exact(&Token::Comma) {
4997 break;
4998 }
4999 }
5000 Ok(options)
5001 }
5002
5003 fn parse_add_with_option(&mut self) -> CalResult<Option<AddWithOption>> {
5004 match self.peek() {
5005 Some(SpannedToken {
5006 token: Token::ExtractEventDate,
5007 ..
5008 }) => {
5009 self.advance();
5010 Ok(Some(AddWithOption::ExtractEventDate))
5011 }
5012 Some(SpannedToken {
5013 token: Token::AutoRelate,
5014 ..
5015 }) => {
5016 self.advance();
5017 Ok(Some(AddWithOption::AutoRelate))
5018 }
5019 Some(SpannedToken {
5020 token: Token::ExtractMemories,
5021 ..
5022 }) => {
5023 self.advance();
5024 Ok(Some(AddWithOption::ExtractMemories))
5025 }
5026 Some(SpannedToken {
5027 token: Token::SyncOption,
5028 ..
5029 }) => {
5030 self.advance();
5031 Ok(Some(AddWithOption::Sync))
5032 }
5033 Some(SpannedToken { token: Token::Ident(w), .. })
5034 if w.eq_ignore_ascii_case("occurrence") =>
5035 {
5036 self.advance();
5037 Ok(Some(AddWithOption::Occurrence))
5038 }
5039 Some(st) => {
5040 let found = st.token.description();
5041 let span = st.span;
5042 self.warnings.push(CalWarning::UnknownExtensionOption {
5043 option: found,
5044 span: Some(span),
5045 });
5046 self.advance();
5047 Ok(None)
5048 }
5049 None => Err(CalError::UnexpectedToken {
5050 expected:
5051 "ADD WITH option (extract_event_date, auto_relate, extract_memories, sync)"
5052 .into(),
5053 found: "<end of query>".into(),
5054 span: None,
5055 suggestion: None,
5056 }),
5057 }
5058 }
5059
5060 fn parse_accumulate(&mut self) -> CalResult<CalStatement> {
5063 let span_start = self.current_span();
5064 self.expect_exact(&Token::Accumulate)?;
5065
5066 let grain_type = self.parse_grain_type_singular()?;
5068
5069 let target = if let Some(SpannedToken {
5071 token: Token::HashLiteral(_),
5072 ..
5073 }) = self.peek()
5074 {
5075 let hash = self.parse_hash_literal()?;
5076 AccumulateTarget::Hash { hash }
5077 } else if self.at_exact(&Token::Where) {
5078 self.advance();
5079 let mut subject: Option<String> = None;
5081 let mut relation: Option<String> = None;
5082 let mut namespace: Option<String> = None;
5083 loop {
5084 self.eat_exact(&Token::And);
5086 match self.peek() {
5087 Some(SpannedToken {
5088 token: Token::Ident(id),
5089 ..
5090 }) if id.eq_ignore_ascii_case("subject") => {
5091 self.advance();
5092 self.expect_exact(&Token::Eq)?;
5093 subject = Some(self.parse_string_literal()?);
5094 }
5095 Some(SpannedToken {
5096 token: Token::Ident(id),
5097 ..
5098 }) if id.eq_ignore_ascii_case("relation") => {
5099 self.advance();
5100 self.expect_exact(&Token::Eq)?;
5101 relation = Some(self.parse_string_literal()?);
5102 }
5103 Some(SpannedToken {
5104 token: Token::Ident(id),
5105 ..
5106 }) if id.eq_ignore_ascii_case("namespace") => {
5107 self.advance();
5108 self.expect_exact(&Token::Eq)?;
5109 namespace = Some(self.parse_string_literal()?);
5110 }
5111 _ => break,
5112 }
5113 }
5114 let subject = subject.ok_or_else(|| CalError::UnexpectedToken {
5115 expected: "subject = \"...\" in WHERE clause".into(),
5116 found: self
5117 .peek()
5118 .map(|st| format!("{:?}", st.token))
5119 .unwrap_or_else(|| "<end of query>".into()),
5120 span: Some(self.current_span()),
5121 suggestion: Some("ACCUMULATE WHERE requires subject = \"...\"".into()),
5122 })?;
5123 let relation = relation.ok_or_else(|| CalError::UnexpectedToken {
5124 expected: "relation = \"...\" in WHERE clause".into(),
5125 found: self
5126 .peek()
5127 .map(|st| format!("{:?}", st.token))
5128 .unwrap_or_else(|| "<end of query>".into()),
5129 span: Some(self.current_span()),
5130 suggestion: Some("ACCUMULATE WHERE requires relation = \"...\"".into()),
5131 })?;
5132 AccumulateTarget::TipResolved {
5133 subject,
5134 relation,
5135 namespace,
5136 }
5137 } else {
5138 return Err(CalError::UnexpectedToken {
5139 expected: "hash literal or WHERE clause".into(),
5140 found: self
5141 .peek()
5142 .map(|st| format!("{:?}", st.token))
5143 .unwrap_or_else(|| "<end of query>".into()),
5144 span: Some(self.current_span()),
5145 suggestion: Some(
5146 "ACCUMULATE requires either a hash or WHERE subject = ... relation = ..."
5147 .into(),
5148 ),
5149 });
5150 };
5151
5152 let mut add_ops = Vec::new();
5154 let mut set_ops = Vec::new();
5155 loop {
5156 if self.at_exact(&Token::Add) {
5157 self.advance();
5158 let fspan = self.current_span();
5159 let field = self.parse_identifier()?;
5160 self.expect_exact(&Token::Eq)?;
5161 let value = self.parse_number()?;
5162 add_ops.push(DeltaOp {
5163 field,
5164 delta: value,
5165 span: Some(fspan),
5166 });
5167 } else if self.at_exact(&Token::Set) {
5168 self.advance();
5169 let fspan = self.current_span();
5170 let field = self.parse_identifier()?;
5171 self.expect_exact(&Token::Eq)?;
5172 let value = self.parse_value()?;
5173 set_ops.push(FieldAssignment {
5174 field,
5175 value,
5176 span: Some(fspan),
5177 });
5178 } else {
5179 break;
5180 }
5181 }
5182
5183 if add_ops.is_empty() {
5184 return Err(CalError::MissingAccumulateOps {
5185 span: Some(self.current_span()),
5186 });
5187 }
5188
5189 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
5191 return Err(CalError::MissingReason {
5192 span: Some(self.current_span()),
5193 });
5194 }
5195 self.advance();
5196 let rspan = self.current_span();
5197 let reason = self.parse_string_literal()?;
5198 if reason.len() > MAX_REASON_LENGTH {
5199 return Err(CalError::ReasonTooLong {
5200 length: reason.len(),
5201 max: MAX_REASON_LENGTH,
5202 span: Some(rspan),
5203 });
5204 }
5205
5206 let span_end = self.prev_span();
5207 Ok(CalStatement::Accumulate(AccumulateStmt {
5208 grain_type,
5209 target,
5210 add_ops,
5211 set_ops,
5212 reason,
5213 span: Some(Span::new(
5214 span_start.start,
5215 span_end.end,
5216 span_start.line,
5217 span_start.col,
5218 )),
5219 }))
5220 }
5221
5222 fn parse_supersede(&mut self) -> CalResult<CalStatement> {
5225 let span_start = self.current_span();
5226 self.expect_exact(&Token::Supersede)?;
5227
5228 let hash = self.parse_hash_literal()?;
5229
5230 if self.is_graph_line_start() {
5235 return self.parse_supersede_workflow(span_start, hash);
5236 }
5237
5238 let mut set_clauses = vec![];
5240 while self.at_exact(&Token::Set) {
5241 self.advance();
5242 let fspan = self.current_span();
5243 let field = self.parse_identifier()?;
5244 self.expect_exact(&Token::Eq)?;
5245 let value = self.parse_value()?;
5246 set_clauses.push(FieldAssignment {
5247 field,
5248 value,
5249 span: Some(fspan),
5250 });
5251 }
5252
5253 if set_clauses.is_empty() {
5254 return Err(CalError::MissingSetClause {
5255 span: Some(self.current_span()),
5256 });
5257 }
5258
5259 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
5261 return Err(CalError::MissingReason {
5262 span: Some(self.current_span()),
5263 });
5264 }
5265 self.advance();
5266 let rspan = self.current_span();
5267 let reason = self.parse_string_literal()?;
5268 if reason.len() > MAX_REASON_LENGTH {
5269 return Err(CalError::ReasonTooLong {
5270 length: reason.len(),
5271 max: MAX_REASON_LENGTH,
5272 span: Some(rspan),
5273 });
5274 }
5275
5276 let span_end = self.prev_span();
5277 Ok(CalStatement::Supersede(SupersedeStmt {
5278 hash,
5279 set_clauses,
5280 reason,
5281 span: Some(Span::new(
5282 span_start.start,
5283 span_end.end,
5284 span_start.line,
5285 span_start.col,
5286 )),
5287 }))
5288 }
5289
5290 fn parse_supersede_workflow(
5292 &mut self,
5293 span_start: Span,
5294 hash: String,
5295 ) -> CalResult<CalStatement> {
5296 let (nodes, edges) = self.parse_workflow_graph()?;
5297
5298 let mut bindings = vec![];
5299 while self.at_exact(&Token::Bind) {
5300 bindings.push(self.parse_bind_clause()?);
5301 }
5302
5303 let reason = self.parse_reason_clause()?;
5304
5305 let span_end = self.prev_span();
5306 Ok(CalStatement::SupersedeWorkflow(SupersedeWorkflowStmt {
5307 hash,
5308 nodes,
5309 edges,
5310 bindings,
5311 reason,
5312 span: Some(Span::new(
5313 span_start.start,
5314 span_end.end,
5315 span_start.line,
5316 span_start.col,
5317 )),
5318 }))
5319 }
5320
5321 fn parse_forget(&mut self) -> CalResult<CalStatement> {
5330 let span_start = self.current_span();
5331 self.expect_exact(&Token::Forget)?;
5332
5333 if let Some(SpannedToken { token: Token::Ident(word), span, .. }) = self.peek() {
5337 let word_up = word.to_ascii_uppercase();
5338 let span = *span;
5339 match word_up.as_str() {
5340 "SUBJECT" => {
5341 self.advance();
5342 let user_id = self.parse_string_literal()?;
5343 let mut text_mentions = false;
5345 if self.at_exact(&Token::With) {
5346 self.advance();
5347 match self.peek() {
5348 Some(SpannedToken { token: Token::Ident(opt), .. })
5349 if opt.eq_ignore_ascii_case("text_mentions") =>
5350 {
5351 self.advance();
5352 text_mentions = true;
5353 }
5354 other => {
5355 return Err(CalError::UnexpectedToken {
5356 expected: "text_mentions".into(),
5357 found: other
5358 .map(|t| t.token.description())
5359 .unwrap_or_else(|| "end of input".into()),
5360 span: other.map(|t| t.span),
5361 suggestion: Some(
5362 "FORGET SUBJECT supports exactly one option: \
5363 WITH text_mentions"
5364 .into(),
5365 ),
5366 });
5367 }
5368 }
5369 }
5370 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
5373 return Err(CalError::MissingReason {
5374 span: Some(self.current_span()),
5375 });
5376 }
5377 self.advance();
5378 let reason = self.parse_string_literal()?;
5379 let span_end = self.prev_span();
5380 return Ok(CalStatement::Forget(ForgetStmt {
5381 target: ForgetTarget::User { user_id },
5382 reason: Some(reason),
5383 text_mentions,
5384 span: Some(Span::new(
5385 span_start.start,
5386 span_end.end,
5387 span_start.line,
5388 span_start.col,
5389 )),
5390 }));
5391 }
5392 "USER" | "SCOPE" => {
5393 return Err(CalError::UnexpectedToken {
5394 expected: "a grain hash or SUBJECT".into(),
5395 found: word_up,
5396 span: Some(span),
5397 suggestion: Some(
5398 "identity erasure is spelled FORGET SUBJECT \"<id>\" \
5399 BECAUSE \"<why>\"; scope erasure is not part of the \
5400 text grammar"
5401 .into(),
5402 ),
5403 });
5404 }
5405 _ => {}
5406 }
5407 }
5408
5409 let hash = self.parse_hash_literal()?;
5410 let reason = if self.at_exact(&Token::Reason) || self.at_exact(&Token::Because) {
5413 self.advance();
5414 Some(self.parse_string_literal()?)
5415 } else {
5416 None
5417 };
5418 let span_end = self.prev_span();
5419 Ok(CalStatement::Forget(ForgetStmt {
5420 target: ForgetTarget::Hash { hash },
5421 reason,
5422 text_mentions: false,
5423 span: Some(Span::new(
5424 span_start.start,
5425 span_end.end,
5426 span_start.line,
5427 span_start.col,
5428 )),
5429 }))
5430 }
5431
5432 fn parse_report_subject(&mut self) -> CalResult<CalStatement> {
5436 let span_start = self.current_span();
5437 self.expect_exact(&Token::Report)?;
5438 match self.peek() {
5439 Some(SpannedToken { token: Token::Ident(word), .. })
5440 if word.eq_ignore_ascii_case("SUBJECT") =>
5441 {
5442 self.advance();
5443 }
5444 other => {
5445 return Err(CalError::UnexpectedToken {
5446 expected: "SUBJECT".into(),
5447 found: other
5448 .map(|t| t.token.description())
5449 .unwrap_or_else(|| "end of input".into()),
5450 span: other.map(|t| t.span),
5451 suggestion: Some(
5452 "the DSAR read is spelled REPORT SUBJECT \"<id>\" \
5453 [WITH text_mentions]"
5454 .into(),
5455 ),
5456 });
5457 }
5458 }
5459 let subject_id = self.parse_string_literal()?;
5460 let mut text_mentions = false;
5461 if self.at_exact(&Token::With) {
5462 self.advance();
5463 match self.peek() {
5464 Some(SpannedToken { token: Token::Ident(opt), .. })
5465 if opt.eq_ignore_ascii_case("text_mentions") =>
5466 {
5467 self.advance();
5468 text_mentions = true;
5469 }
5470 other => {
5471 return Err(CalError::UnexpectedToken {
5472 expected: "text_mentions".into(),
5473 found: other
5474 .map(|t| t.token.description())
5475 .unwrap_or_else(|| "end of input".into()),
5476 span: other.map(|t| t.span),
5477 suggestion: Some(
5478 "REPORT SUBJECT supports exactly one option: \
5479 WITH text_mentions"
5480 .into(),
5481 ),
5482 });
5483 }
5484 }
5485 }
5486 let span_end = self.prev_span();
5487 Ok(CalStatement::ReportSubject(ReportSubjectStmt {
5488 subject_id,
5489 text_mentions,
5490 span: Some(Span::new(
5491 span_start.start,
5492 span_end.end,
5493 span_start.line,
5494 span_start.col,
5495 )),
5496 }))
5497 }
5498
5499 fn parse_purge(&mut self) -> CalResult<CalStatement> {
5502 let span_start = self.current_span();
5503 self.expect_exact(&Token::Purge)?;
5504
5505 let expect_word = |me: &mut Self, word: &str| -> CalResult<()> {
5506 match me.peek() {
5507 Some(SpannedToken { token: Token::Ident(w), .. })
5508 if w.eq_ignore_ascii_case(word) =>
5509 {
5510 me.advance();
5511 Ok(())
5512 }
5513 other => Err(CalError::UnexpectedToken {
5514 expected: word.into(),
5515 found: other
5516 .map(|t| t.token.description())
5517 .unwrap_or_else(|| "end of input".into()),
5518 span: other.map(|t| t.span),
5519 suggestion: Some(
5520 "the retention sweep is spelled PURGE OLDER THAN <n><d|h|m> \
5521 [TYPE <t>] [IN \"<ns>\"] BECAUSE \"<why>\""
5522 .into(),
5523 ),
5524 }),
5525 }
5526 };
5527 expect_word(self, "OLDER")?;
5528 expect_word(self, "THAN")?;
5529
5530 let n = match self.peek() {
5532 Some(SpannedToken { token: Token::NumberLiteral(n), .. }) if *n > 0.0 => {
5533 let n = *n;
5534 self.advance();
5535 n
5536 }
5537 other => {
5538 return Err(CalError::UnexpectedToken {
5539 expected: "a positive age like 90d, 6h, or 30m".into(),
5540 found: other
5541 .map(|t| t.token.description())
5542 .unwrap_or_else(|| "end of input".into()),
5543 span: other.map(|t| t.span),
5544 suggestion: None,
5545 });
5546 }
5547 };
5548 let min_age_days = match self.peek() {
5549 Some(SpannedToken { token: Token::Ident(u), .. }) => {
5550 let unit = u.to_ascii_lowercase();
5551 let days = match unit.as_str() {
5552 "d" => n,
5553 "h" => n / 24.0,
5554 "m" => n / 1440.0,
5555 _ => {
5556 return Err(CalError::UnexpectedToken {
5557 expected: "an age unit: d, h, or m".into(),
5558 found: u.clone(),
5559 span: self.peek().map(|t| t.span),
5560 suggestion: None,
5561 });
5562 }
5563 };
5564 self.advance();
5565 days
5566 }
5567 other => {
5568 return Err(CalError::UnexpectedToken {
5569 expected: "an age unit: d, h, or m".into(),
5570 found: other
5571 .map(|t| t.token.description())
5572 .unwrap_or_else(|| "end of input".into()),
5573 span: other.map(|t| t.span),
5574 suggestion: Some("write the age as one word, e.g. 90d".into()),
5575 });
5576 }
5577 };
5578
5579 let mut grain_type: Option<String> = None;
5581 let mut namespace: Option<String> = None;
5582 let mut limit: Option<usize> = None;
5583 loop {
5584 match self.peek() {
5585 Some(SpannedToken { token: Token::Ident(w), .. })
5586 if w.eq_ignore_ascii_case("TYPE") && grain_type.is_none() =>
5587 {
5588 self.advance();
5589 match self.peek() {
5590 Some(SpannedToken { token: Token::Ident(t), .. }) => {
5591 grain_type = Some(t.to_ascii_lowercase());
5592 self.advance();
5593 }
5594 other => {
5595 return Err(CalError::UnexpectedToken {
5596 expected: "a grain type, e.g. event".into(),
5597 found: other
5598 .map(|t| t.token.description())
5599 .unwrap_or_else(|| "end of input".into()),
5600 span: other.map(|t| t.span),
5601 suggestion: None,
5602 });
5603 }
5604 }
5605 }
5606 Some(SpannedToken { token: Token::In, .. }) if namespace.is_none() => {
5607 self.advance();
5608 namespace = Some(self.parse_string_literal()?);
5609 }
5610 Some(SpannedToken { token: Token::Limit, .. }) if limit.is_none() => {
5611 self.advance();
5612 match self.peek() {
5613 Some(SpannedToken { token: Token::NumberLiteral(n), .. })
5614 if *n >= 1.0 =>
5615 {
5616 limit = Some(*n as usize);
5617 self.advance();
5618 }
5619 other => {
5620 return Err(CalError::UnexpectedToken {
5621 expected: "a positive LIMIT".into(),
5622 found: other
5623 .map(|t| t.token.description())
5624 .unwrap_or_else(|| "end of input".into()),
5625 span: other.map(|t| t.span),
5626 suggestion: None,
5627 });
5628 }
5629 }
5630 }
5631 _ => break,
5632 }
5633 }
5634
5635 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
5638 return Err(CalError::MissingReason {
5639 span: Some(self.current_span()),
5640 });
5641 }
5642 self.advance();
5643 let reason = self.parse_string_literal()?;
5644
5645 let span_end = self.prev_span();
5646 Ok(CalStatement::Purge(PurgeStmt {
5647 min_age_days: Some(min_age_days),
5648 namespace,
5649 limit,
5650 grain_type,
5651 reason: Some(reason),
5652 span: Some(Span::new(
5653 span_start.start,
5654 span_end.end,
5655 span_start.line,
5656 span_start.col,
5657 )),
5658 }))
5659 }
5660
5661 fn parse_dcl_verb(&mut self) -> CalResult<String> {
5665 let head = match self.peek() {
5666 Some(SpannedToken { token: Token::Ident(w), .. }) => {
5667 let w = w.to_ascii_lowercase();
5668 self.advance();
5669 w
5670 }
5671 Some(SpannedToken { token: Token::Run, .. }) => {
5676 self.advance();
5677 "run".to_string()
5678 }
5679 other => {
5680 return Err(CalError::UnexpectedToken {
5681 expected: "a verb (read, write, supersede, delete, erase, \
5682 loop.run, loop.review, loop.apply, run.execute, \
5683 run.respond, run.cancel, admin)"
5684 .into(),
5685 found: other
5686 .map(|t| t.token.description())
5687 .unwrap_or_else(|| "end of input".into()),
5688 span: other.map(|t| t.span),
5689 suggestion: None,
5690 });
5691 }
5692 };
5693 if self.at_exact(&Token::Dot) {
5694 self.advance();
5695 match self.peek() {
5696 Some(SpannedToken { token: Token::Ident(tail), .. }) => {
5697 let tail = tail.to_ascii_lowercase();
5698 self.advance();
5699 Ok(format!("{head}.{tail}"))
5700 }
5701 other => Err(CalError::UnexpectedToken {
5702 expected: "the dotted verb tail (run, review, apply, \
5703 execute, respond, cancel)"
5704 .into(),
5705 found: other
5706 .map(|t| t.token.description())
5707 .unwrap_or_else(|| "end of input".into()),
5708 span: other.map(|t| t.span),
5709 suggestion: None,
5710 }),
5711 }
5712 } else {
5713 Ok(head)
5714 }
5715 }
5716
5717 fn parse_dcl_body(&mut self, recipient_token: &Token) -> CalResult<DclBody> {
5721 let mut verbs = vec![self.parse_dcl_verb()?];
5722 while self.at_exact(&Token::Comma) {
5723 self.advance();
5724 verbs.push(self.parse_dcl_verb()?);
5725 }
5726
5727 self.expect_exact(&Token::On)?;
5728 let mut namespaces = Vec::new();
5729 loop {
5730 match self.peek() {
5731 Some(SpannedToken { token: Token::Asterisk, .. }) => {
5732 namespaces.push("*".to_string());
5733 self.advance();
5734 }
5735 Some(SpannedToken { token: Token::Ident(ns), .. }) => {
5736 namespaces.push(ns.clone());
5737 self.advance();
5738 }
5739 Some(SpannedToken { token: Token::StringLiteral(ns), .. }) => {
5740 namespaces.push(ns.clone());
5741 self.advance();
5742 }
5743 other => {
5744 return Err(CalError::UnexpectedToken {
5745 expected: "a namespace or *".into(),
5746 found: other
5747 .map(|t| t.token.description())
5748 .unwrap_or_else(|| "end of input".into()),
5749 span: other.map(|t| t.span),
5750 suggestion: None,
5751 });
5752 }
5753 }
5754 if self.at_exact(&Token::Comma) {
5755 self.advance();
5756 } else {
5757 break;
5758 }
5759 }
5760
5761 self.expect_exact(recipient_token)?;
5762 let principal = self.parse_string_literal()?;
5765
5766 let mut reason = None;
5769 if self.at_exact(&Token::With) {
5770 self.advance();
5771 if self.at_exact(&Token::Because) || self.at_exact(&Token::Reason) {
5772 self.advance();
5773 self.expect_exact(&Token::LParen)?;
5774 reason = Some(self.parse_string_literal()?);
5775 self.expect_exact(&Token::RParen)?;
5776 } else {
5777 let other = self.peek();
5778 return Err(CalError::UnexpectedToken {
5779 expected: "because(\"<why>\")".into(),
5780 found: other
5781 .map(|t| t.token.description())
5782 .unwrap_or_else(|| "end of input".into()),
5783 span: other.map(|t| t.span),
5784 suggestion: Some(
5785 "GRANT/REVOKE support exactly one option: \
5786 WITH because(\"<why>\")"
5787 .into(),
5788 ),
5789 });
5790 }
5791 }
5792 Ok((verbs, namespaces, principal, reason))
5793 }
5794
5795 fn parse_grant(&mut self) -> CalResult<CalStatement> {
5797 let span_start = self.current_span();
5798 self.expect_exact(&Token::Grant)?;
5799 let (verbs, namespaces, principal, reason) = self.parse_dcl_body(&Token::To)?;
5800 let span_end = self.prev_span();
5801 Ok(CalStatement::Grant(GrantStmt {
5802 verbs,
5803 namespaces,
5804 principal,
5805 reason,
5806 span: Some(Span::new(
5807 span_start.start,
5808 span_end.end,
5809 span_start.line,
5810 span_start.col,
5811 )),
5812 }))
5813 }
5814
5815 fn parse_revoke(&mut self) -> CalResult<CalStatement> {
5817 let span_start = self.current_span();
5818 self.expect_exact(&Token::Revoke)?;
5819 let (verbs, namespaces, principal, reason) = self.parse_dcl_body(&Token::From)?;
5820 let span_end = self.prev_span();
5821 Ok(CalStatement::Revoke(RevokeStmt {
5822 verbs,
5823 namespaces,
5824 principal,
5825 reason,
5826 span: Some(Span::new(
5827 span_start.start,
5828 span_end.end,
5829 span_start.line,
5830 span_start.col,
5831 )),
5832 }))
5833 }
5834
5835 fn parse_governance_body(&mut self, keyword: &Token) -> CalResult<GovernanceStmt> {
5839 let span_start = self.current_span();
5840 self.expect_exact(keyword)?;
5841 let hash = self.parse_hash_literal()?;
5842 if !self.at_exact(&Token::Because) && !self.at_exact(&Token::Reason) {
5843 return Err(CalError::MissingReason {
5844 span: Some(self.current_span()),
5845 });
5846 }
5847 self.advance();
5848 let reason = self.parse_string_literal()?;
5849 let span_end = self.prev_span();
5850 Ok(GovernanceStmt {
5851 hash,
5852 reason,
5853 span: Some(Span::new(
5854 span_start.start,
5855 span_end.end,
5856 span_start.line,
5857 span_start.col,
5858 )),
5859 })
5860 }
5861
5862 fn parse_run_loop(&mut self, span_start: Span) -> CalResult<CalStatement> {
5865 self.advance();
5867 let mut full_sweep = false;
5868 if let Some(SpannedToken { token: Token::Ident(w), .. }) = self.peek() {
5869 if w.eq_ignore_ascii_case("FULL") {
5870 full_sweep = true;
5871 self.advance();
5872 }
5873 }
5874 let mut min_new: Option<u64> = None;
5875 let mut if_stale_ms: Option<i64> = None;
5876 if self.at_exact(&Token::With) {
5877 self.advance();
5878 loop {
5879 match self.peek() {
5880 Some(SpannedToken { token: Token::Ident(opt), .. })
5881 if opt.eq_ignore_ascii_case("min_new") =>
5882 {
5883 self.advance();
5884 self.expect_exact(&Token::LParen)?;
5885 match self.peek() {
5886 Some(SpannedToken { token: Token::NumberLiteral(n), .. })
5887 if *n >= 0.0 =>
5888 {
5889 min_new = Some(*n as u64);
5890 self.advance();
5891 }
5892 other => {
5893 return Err(CalError::UnexpectedToken {
5894 expected: "a number".into(),
5895 found: other
5896 .map(|t| t.token.description())
5897 .unwrap_or_else(|| "end of input".into()),
5898 span: other.map(|t| t.span),
5899 suggestion: None,
5900 });
5901 }
5902 }
5903 self.expect_exact(&Token::RParen)?;
5904 }
5905 Some(SpannedToken { token: Token::Ident(opt), .. })
5906 if opt.eq_ignore_ascii_case("if_stale") =>
5907 {
5908 self.advance();
5909 self.expect_exact(&Token::LParen)?;
5910 let dur = self.parse_string_literal()?;
5911 if_stale_ms = Some(parse_stale_duration_ms(&dur).ok_or_else(|| {
5912 CalError::UnexpectedToken {
5913 expected: "a duration like \"6h\", \"90m\", or \"2d\"".into(),
5914 found: dur.clone(),
5915 span: Some(self.prev_span()),
5916 suggestion: None,
5917 }
5918 })?);
5919 self.expect_exact(&Token::RParen)?;
5920 }
5921 other => {
5922 return Err(CalError::UnexpectedToken {
5923 expected: "min_new(N) or if_stale(\"<dur>\")".into(),
5924 found: other
5925 .map(|t| t.token.description())
5926 .unwrap_or_else(|| "end of input".into()),
5927 span: other.map(|t| t.span),
5928 suggestion: Some(
5929 "RUN LOOP supports WITH min_new(N), if_stale(\"6h\") — model \
5930 and policy configuration is host-side, never statement text"
5931 .into(),
5932 ),
5933 });
5934 }
5935 }
5936 if self.at_exact(&Token::Comma) {
5937 self.advance();
5938 } else {
5939 break;
5940 }
5941 }
5942 }
5943 let span_end = self.prev_span();
5944 Ok(CalStatement::RunLoop(RunLoopStmt {
5945 full_sweep,
5946 min_new,
5947 if_stale_ms,
5948 span: Some(Span::new(
5949 span_start.start,
5950 span_end.end,
5951 span_start.line,
5952 span_start.col,
5953 )),
5954 }))
5955 }
5956
5957 fn parse_remember(&mut self) -> CalResult<CalStatement> {
5960 let span_start = self.current_span();
5961 self.expect_exact(&Token::Remember)?;
5962 let content = self.parse_string_literal()?;
5963 let mut session_id = None;
5964 let mut role = None;
5965 let mut run_id = None;
5966 if self.at_exact(&Token::With) {
5967 self.advance();
5968 loop {
5969 let opt = match self.peek() {
5970 Some(SpannedToken { token: Token::Ident(w), .. }) => w.to_ascii_lowercase(),
5971 Some(SpannedToken { token: Token::Run, .. }) => "run".to_string(),
5973 other => {
5974 return Err(CalError::UnexpectedToken {
5975 expected: "session(\"<id>\"), role(\"<r>\"), or run(\"<id>\")".into(),
5976 found: other
5977 .map(|t| t.token.description())
5978 .unwrap_or_else(|| "end of input".into()),
5979 span: other.map(|t| t.span),
5980 suggestion: None,
5981 });
5982 }
5983 };
5984 match opt.as_str() {
5985 "session" | "role" | "run" => {
5986 self.advance();
5987 self.expect_exact(&Token::LParen)?;
5988 let value = self.parse_string_literal()?;
5989 self.expect_exact(&Token::RParen)?;
5990 match opt.as_str() {
5991 "session" => session_id = Some(value),
5992 "role" => role = Some(value),
5993 _ => run_id = Some(value),
5994 }
5995 }
5996 other => {
5997 return Err(CalError::UnexpectedToken {
5998 expected: "session(\"<id>\"), role(\"<r>\"), or run(\"<id>\")".into(),
5999 found: other.to_string(),
6000 span: self.peek().map(|t| t.span),
6001 suggestion: Some(
6002 "REMEMBER carries capture metadata only — fact extraction \
6003 is host configuration (areev remember --model …)"
6004 .into(),
6005 ),
6006 });
6007 }
6008 }
6009 if self.at_exact(&Token::Comma) {
6010 self.advance();
6011 } else {
6012 break;
6013 }
6014 }
6015 }
6016 let span_end = self.prev_span();
6017 Ok(CalStatement::Remember(RememberStmt {
6018 content,
6019 session_id,
6020 role,
6021 run_id,
6022 span: Some(Span::new(
6023 span_start.start,
6024 span_end.end,
6025 span_start.line,
6026 span_start.col,
6027 )),
6028 }))
6029 }
6030
6031 fn parse_word_number(&mut self, word: &str) -> CalResult<Option<usize>> {
6034 let matches_word = match self.peek() {
6035 Some(SpannedToken { token: Token::Ident(w), .. }) => w.eq_ignore_ascii_case(word),
6036 Some(SpannedToken { token: Token::Limit, .. }) => word.eq_ignore_ascii_case("LIMIT"),
6037 _ => false,
6038 };
6039 if !matches_word {
6040 return Ok(None);
6041 }
6042 self.advance();
6043 match self.peek() {
6044 Some(SpannedToken { token: Token::NumberLiteral(n), .. }) if *n >= 1.0 => {
6045 let n = *n as usize;
6046 self.advance();
6047 Ok(Some(n))
6048 }
6049 other => Err(CalError::UnexpectedToken {
6050 expected: format!("a positive number after {word}"),
6051 found: other
6052 .map(|t| t.token.description())
6053 .unwrap_or_else(|| "end of input".into()),
6054 span: other.map(|t| t.span),
6055 suggestion: None,
6056 }),
6057 }
6058 }
6059
6060 fn expect_word(&mut self, word: &str, form: &str) -> CalResult<()> {
6062 match self.peek() {
6063 Some(SpannedToken { token: Token::Ident(w), .. })
6064 if w.eq_ignore_ascii_case(word) =>
6065 {
6066 self.advance();
6067 Ok(())
6068 }
6069 other => Err(CalError::UnexpectedToken {
6070 expected: word.into(),
6071 found: other
6072 .map(|t| t.token.description())
6073 .unwrap_or_else(|| "end of input".into()),
6074 span: other.map(|t| t.span),
6075 suggestion: Some(form.into()),
6076 }),
6077 }
6078 }
6079
6080 fn parse_merge(&mut self) -> CalResult<CalStatement> {
6083 let span_start = self.current_span();
6084 self.expect_exact(&Token::Merge)?;
6085 const FORM: &str = "a merge is spelled MERGE \"<subject>\" RELATION \"<relation>\" \
6086 TO \"<object>\" [CONFIDENCE <n>] BECAUSE \"<why>\"";
6087 let subject = self.parse_string_literal()?;
6088 self.expect_word("RELATION", FORM)?;
6089 let relation = self.parse_string_literal()?;
6090 self.expect_exact(&Token::To)?;
6091 let object = self.parse_string_literal()?;
6092 let mut confidence = None;
6093 if let Some(SpannedToken { token: Token::Ident(w), .. }) = self.peek() {
6094 if w.eq_ignore_ascii_case("CONFIDENCE") {
6095 self.advance();
6096 match self.peek() {
6097 Some(SpannedToken { token: Token::NumberLiteral(n), .. })
6098 if (0.0..=1.0).contains(n) =>
6099 {
6100 confidence = Some(*n);
6101 self.advance();
6102 }
6103 other => {
6104 return Err(CalError::UnexpectedToken {
6105 expected: "a confidence between 0 and 1".into(),
6106 found: other
6107 .map(|t| t.token.description())
6108 .unwrap_or_else(|| "end of input".into()),
6109 span: other.map(|t| t.span),
6110 suggestion: None,
6111 });
6112 }
6113 }
6114 }
6115 }
6116 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
6118 return Err(CalError::MissingReason { span: Some(self.current_span()) });
6119 }
6120 self.advance();
6121 let reason = self.parse_string_literal()?;
6122 let span_end = self.prev_span();
6123 Ok(CalStatement::Merge(MergeStmt {
6124 subject,
6125 relation,
6126 object,
6127 confidence,
6128 reason,
6129 span: Some(Span::new(span_start.start, span_end.end, span_start.line, span_start.col)),
6130 }))
6131 }
6132
6133 fn parse_related(&mut self) -> CalResult<CalStatement> {
6136 let span_start = self.current_span();
6137 self.expect_exact(&Token::Related)?;
6138 const FORM: &str = "the graph walk is spelled RELATED \"<start>\" VIA \
6139 \"<r1,r2>\" [DIRECTION out|in|both] [DEPTH <n>] [LIMIT <n>]";
6140 let start = self.parse_string_literal()?;
6141 self.expect_word("VIA", FORM)?;
6142 let relations = self.parse_string_literal()?;
6143 let mut direction = None;
6144 if let Some(SpannedToken { token: Token::Ident(w), .. }) = self.peek() {
6145 if w.eq_ignore_ascii_case("DIRECTION") {
6146 self.advance();
6147 match self.peek() {
6148 Some(SpannedToken { token: Token::Ident(d), .. })
6149 if ["out", "in", "both"].iter().any(|x| d.eq_ignore_ascii_case(x)) =>
6150 {
6151 direction = Some(d.to_ascii_lowercase());
6152 self.advance();
6153 }
6154 Some(SpannedToken { token: Token::In, .. }) => {
6156 direction = Some("in".to_string());
6157 self.advance();
6158 }
6159 other => {
6160 return Err(CalError::UnexpectedToken {
6161 expected: "out, in, or both".into(),
6162 found: other
6163 .map(|t| t.token.description())
6164 .unwrap_or_else(|| "end of input".into()),
6165 span: other.map(|t| t.span),
6166 suggestion: None,
6167 });
6168 }
6169 }
6170 }
6171 }
6172 let depth = self.parse_word_number("DEPTH")?;
6173 let limit = self.parse_word_number("LIMIT")?;
6174 let span_end = self.prev_span();
6175 Ok(CalStatement::Related(RelatedStmt {
6176 start,
6177 relations,
6178 direction,
6179 depth,
6180 limit,
6181 span: Some(Span::new(span_start.start, span_end.end, span_start.line, span_start.col)),
6182 }))
6183 }
6184
6185 fn parse_novelty(&mut self) -> CalResult<CalStatement> {
6188 let span_start = self.current_span();
6189 self.expect_exact(&Token::Novelty)?;
6190 let text = self.parse_string_literal()?;
6191 let mut subject = None;
6192 let mut relation = None;
6193 loop {
6194 match self.peek() {
6195 Some(SpannedToken { token: Token::Ident(w), .. })
6196 if w.eq_ignore_ascii_case("SUBJECT") && subject.is_none() =>
6197 {
6198 self.advance();
6199 subject = Some(self.parse_string_literal()?);
6200 }
6201 Some(SpannedToken { token: Token::Ident(w), .. })
6202 if w.eq_ignore_ascii_case("RELATION") && relation.is_none() =>
6203 {
6204 self.advance();
6205 relation = Some(self.parse_string_literal()?);
6206 }
6207 _ => break,
6208 }
6209 }
6210 let limit = self.parse_word_number("LIMIT")?;
6211 let span_end = self.prev_span();
6212 Ok(CalStatement::Novelty(NoveltyStmt {
6213 text,
6214 subject,
6215 relation,
6216 limit,
6217 span: Some(Span::new(span_start.start, span_end.end, span_start.line, span_start.col)),
6218 }))
6219 }
6220
6221 fn parse_entity_at(&mut self) -> CalResult<CalStatement> {
6224 let span_start = self.current_span();
6225 self.expect_exact(&Token::Entity)?;
6226 let subject = self.parse_string_literal()?;
6227 match self.peek() {
6228 Some(SpannedToken { token: Token::Ident(w), .. })
6229 if w.eq_ignore_ascii_case("RELATION") =>
6230 {
6231 self.advance();
6232 }
6233 other => {
6234 return Err(CalError::UnexpectedToken {
6235 expected: "RELATION".into(),
6236 found: other
6237 .map(|t| t.token.description())
6238 .unwrap_or_else(|| "end of input".into()),
6239 span: other.map(|t| t.span),
6240 suggestion: Some(
6241 "the as-of read is spelled ENTITY \"<subject>\" RELATION \
6242 \"<relation>\" AT <epoch-ms> [AXIS world|knowledge]"
6243 .into(),
6244 ),
6245 });
6246 }
6247 }
6248 let relation = self.parse_string_literal()?;
6249 match self.peek() {
6250 Some(SpannedToken { token: Token::Ident(w), .. }) if w.eq_ignore_ascii_case("AT") => {
6251 self.advance();
6252 }
6253 other => {
6254 return Err(CalError::UnexpectedToken {
6255 expected: "AT".into(),
6256 found: other
6257 .map(|t| t.token.description())
6258 .unwrap_or_else(|| "end of input".into()),
6259 span: other.map(|t| t.span),
6260 suggestion: None,
6261 });
6262 }
6263 }
6264 let at_ms = match self.peek() {
6265 Some(SpannedToken { token: Token::NumberLiteral(n), .. }) if *n >= 0.0 => {
6266 let n = *n as i64;
6267 self.advance();
6268 n
6269 }
6270 other => {
6271 return Err(CalError::UnexpectedToken {
6272 expected: "a point in time (epoch milliseconds)".into(),
6273 found: other
6274 .map(|t| t.token.description())
6275 .unwrap_or_else(|| "end of input".into()),
6276 span: other.map(|t| t.span),
6277 suggestion: None,
6278 });
6279 }
6280 };
6281 let mut axis = None;
6282 if let Some(SpannedToken { token: Token::Ident(w), .. }) = self.peek() {
6283 if w.eq_ignore_ascii_case("AXIS") {
6284 self.advance();
6285 match self.peek() {
6286 Some(SpannedToken { token: Token::Ident(a), .. })
6287 if a.eq_ignore_ascii_case("world") || a.eq_ignore_ascii_case("knowledge") =>
6288 {
6289 axis = Some(a.to_ascii_lowercase());
6290 self.advance();
6291 }
6292 Some(SpannedToken { token: Token::Knowledge, .. }) => {
6295 axis = Some("knowledge".to_string());
6296 self.advance();
6297 }
6298 other => {
6299 return Err(CalError::UnexpectedToken {
6300 expected: "world or knowledge".into(),
6301 found: other
6302 .map(|t| t.token.description())
6303 .unwrap_or_else(|| "end of input".into()),
6304 span: other.map(|t| t.span),
6305 suggestion: None,
6306 });
6307 }
6308 }
6309 }
6310 }
6311 let span_end = self.prev_span();
6312 Ok(CalStatement::EntityAt(EntityAtStmt {
6313 subject,
6314 relation,
6315 at_ms,
6316 axis,
6317 span: Some(Span::new(
6318 span_start.start,
6319 span_end.end,
6320 span_start.line,
6321 span_start.col,
6322 )),
6323 }))
6324 }
6325
6326 fn parse_run_trace(&mut self, span_start: Span) -> CalResult<CalStatement> {
6329 self.advance(); let run_id = self.parse_string_literal()?;
6331 let limit = self.parse_word_number("LIMIT")?;
6332 let span_end = self.prev_span();
6333 Ok(CalStatement::RunTrace(RunTraceStmt {
6334 run_id,
6335 limit,
6336 span: Some(Span::new(
6337 span_start.start,
6338 span_end.end,
6339 span_start.line,
6340 span_start.col,
6341 )),
6342 }))
6343 }
6344
6345 fn parse_runs_touching(&mut self) -> CalResult<CalStatement> {
6347 let span_start = self.current_span();
6348 self.expect_exact(&Token::Runs)?;
6349 match self.peek() {
6350 Some(SpannedToken { token: Token::Ident(w), .. })
6351 if w.eq_ignore_ascii_case("TOUCHING") =>
6352 {
6353 self.advance();
6354 }
6355 other => {
6356 return Err(CalError::UnexpectedToken {
6357 expected: "TOUCHING".into(),
6358 found: other
6359 .map(|t| t.token.description())
6360 .unwrap_or_else(|| "end of input".into()),
6361 span: other.map(|t| t.span),
6362 suggestion: Some("the reverse join is spelled RUNS TOUCHING <hash> [DEPTH <n>]".into()),
6363 });
6364 }
6365 }
6366 let hash = self.parse_hash_literal()?;
6367 let depth = self.parse_word_number("DEPTH")?;
6368 let span_end = self.prev_span();
6369 Ok(CalStatement::RunsTouching(RunsTouchingStmt {
6370 hash,
6371 depth,
6372 span: Some(Span::new(
6373 span_start.start,
6374 span_end.end,
6375 span_start.line,
6376 span_start.col,
6377 )),
6378 }))
6379 }
6380
6381 fn parse_derived_from(&mut self) -> CalResult<CalStatement> {
6383 let span_start = self.current_span();
6384 self.expect_exact(&Token::Derived)?;
6385 self.expect_exact(&Token::From)?;
6386 let hash = self.parse_hash_literal()?;
6387 let span_end = self.prev_span();
6388 Ok(CalStatement::DerivedFrom(DerivedFromStmt {
6389 hash,
6390 span: Some(Span::new(
6391 span_start.start,
6392 span_end.end,
6393 span_start.line,
6394 span_start.col,
6395 )),
6396 }))
6397 }
6398
6399 fn parse_show_grants(&mut self) -> CalResult<CalStatement> {
6401 let span_start = self.current_span();
6402 self.expect_exact(&Token::Show)?;
6403 match self.peek() {
6404 Some(SpannedToken { token: Token::Ident(w), .. })
6405 if w.eq_ignore_ascii_case("GRANTS") =>
6406 {
6407 self.advance();
6408 }
6409 Some(SpannedToken { token: Token::Ident(w), .. })
6410 if w.eq_ignore_ascii_case("FORKS") =>
6411 {
6412 self.advance();
6413 let span_end = self.prev_span();
6414 return Ok(CalStatement::ShowForks(ShowForksStmt {
6415 span: Some(Span::new(
6416 span_start.start,
6417 span_end.end,
6418 span_start.line,
6419 span_start.col,
6420 )),
6421 }));
6422 }
6423 other => {
6424 return Err(CalError::UnexpectedToken {
6425 expected: "GRANTS or FORKS".into(),
6426 found: other
6427 .map(|t| t.token.description())
6428 .unwrap_or_else(|| "end of input".into()),
6429 span: other.map(|t| t.span),
6430 suggestion: Some("SHOW GRANTS [FOR \"<principal>\"] | SHOW FORKS".into()),
6431 });
6432 }
6433 }
6434 let principal = if self.at_exact(&Token::For) {
6435 self.advance();
6436 Some(self.parse_string_literal()?)
6437 } else {
6438 None
6439 };
6440 let span_end = self.prev_span();
6441 Ok(CalStatement::ShowGrants(ShowGrantsStmt {
6442 principal,
6443 span: Some(Span::new(
6444 span_start.start,
6445 span_end.end,
6446 span_start.line,
6447 span_start.col,
6448 )),
6449 }))
6450 }
6451
6452 fn parse_revert(&mut self) -> CalResult<CalStatement> {
6453 let span_start = self.current_span();
6454 self.expect_exact(&Token::Revert)?;
6455
6456 let hash = self.parse_hash_literal()?;
6457
6458 if !self.at_exact(&Token::Reason) && !self.at_exact(&Token::Because) {
6460 return Err(CalError::MissingReason {
6461 span: Some(self.current_span()),
6462 });
6463 }
6464 self.advance();
6465 let rspan = self.current_span();
6466 let reason = self.parse_string_literal()?;
6467 if reason.len() > MAX_REASON_LENGTH {
6468 return Err(CalError::ReasonTooLong {
6469 length: reason.len(),
6470 max: MAX_REASON_LENGTH,
6471 span: Some(rspan),
6472 });
6473 }
6474
6475 let span_end = self.prev_span();
6476 Ok(CalStatement::Revert(RevertStmt {
6477 hash,
6478 reason,
6479 span: Some(Span::new(
6480 span_start.start,
6481 span_end.end,
6482 span_start.line,
6483 span_start.col,
6484 )),
6485 }))
6486 }
6487
6488 fn parse_drop(&mut self) -> CalResult<CalStatement> {
6491 let span_start = self.current_span();
6492 self.expect_exact(&Token::Drop)?;
6493
6494 if self.at_exact(&Token::Template) {
6495 self.advance(); let name = self.parse_string_literal()?;
6497 let span_end = self.prev_span();
6498 Ok(CalStatement::DropTemplate(super::ast::DropTemplateStmt {
6499 name,
6500 span: Some(Span::new(
6501 span_start.start,
6502 span_end.end,
6503 span_start.line,
6504 span_start.col,
6505 )),
6506 }))
6507 } else if self.at_exact(&Token::Query) {
6508 self.advance(); let name = self.parse_string_literal()?;
6510 let span_end = self.prev_span();
6511 Ok(CalStatement::DropQuery(DropQueryStmt {
6512 name,
6513 span: Some(Span::new(
6514 span_start.start,
6515 span_end.end,
6516 span_start.line,
6517 span_start.col,
6518 )),
6519 }))
6520 } else {
6521 Err(CalError::UnexpectedToken {
6522 expected: "TEMPLATE or QUERY after DROP".into(),
6523 found: self
6524 .peek()
6525 .map(|t| t.token.description())
6526 .unwrap_or("end of input".into()),
6527 span: Some(self.current_span()),
6528 suggestion: Some("Use DROP TEMPLATE \"name\" or DROP QUERY \"name\".".into()),
6529 })
6530 }
6531 }
6532
6533 fn parse_define(&mut self) -> CalResult<CalStatement> {
6537 let next = self.tokens.get(self.pos + 1);
6539 match next {
6540 Some(SpannedToken {
6541 token: Token::Template,
6542 ..
6543 }) => self.parse_define_template(),
6544 Some(SpannedToken {
6545 token: Token::Query,
6546 ..
6547 }) => self.parse_define_query(),
6548 _ => Err(CalError::UnexpectedToken {
6549 expected: "TEMPLATE or QUERY after DEFINE".into(),
6550 found: next
6551 .map(|t| t.token.description())
6552 .unwrap_or("end of input".into()),
6553 span: Some(self.current_span()),
6554 suggestion: Some("Use DEFINE TEMPLATE or DEFINE QUERY.".into()),
6555 }),
6556 }
6557 }
6558
6559 fn parse_define_template(&mut self) -> CalResult<CalStatement> {
6563 let span_start = self.current_span();
6564 self.expect_exact(&Token::Define)?;
6565 self.expect_exact(&Token::Template)?;
6566
6567 let name = self.parse_template_name()?;
6571
6572 let mut description = None;
6573 let mut parent = None;
6574 let mut grain_types = Vec::new();
6575
6576 loop {
6578 match self.peek() {
6579 Some(SpannedToken {
6580 token: Token::Ident(s),
6581 ..
6582 }) if s.eq_ignore_ascii_case("DESCRIPTION") => {
6583 self.advance();
6584 description = Some(self.parse_string_literal()?);
6585 }
6586 Some(SpannedToken {
6587 token: Token::Extends,
6588 ..
6589 }) => {
6590 self.advance();
6591 parent = Some(self.parse_template_name()?);
6592 }
6593 Some(SpannedToken {
6594 token: Token::For, ..
6595 }) => {
6596 self.advance();
6597 loop {
6599 let gt = match self.peek() {
6600 Some(SpannedToken {
6601 token: Token::Ident(s),
6602 ..
6603 }) => {
6604 let s = s.clone();
6605 self.advance();
6606 s
6607 }
6608 Some(st) => {
6609 let desc = st.token.description();
6610 self.advance();
6612 desc
6613 }
6614 None => break,
6615 };
6616 grain_types.push(gt.to_lowercase());
6617 if self.at_exact(&Token::Comma) {
6618 self.advance();
6619 } else {
6620 break;
6621 }
6622 }
6623 }
6624 _ => break,
6625 }
6626 }
6627
6628 let (source, sections) = if self.at_exact(&Token::As) {
6631 self.advance();
6632 let source_span = self.current_span();
6633 let source = self.parse_string_literal()?;
6634 if source.is_empty() {
6635 return Err(CalError::TemplateSyntaxError {
6636 detail: "template source cannot be empty".to_string(),
6637 span: Some(source_span),
6638 });
6639 }
6640 if source.contains("{{#each") {
6651 (source, None)
6652 } else {
6653 let sections = TemplateSectionSources {
6654 element: Some(source),
6655 ..Default::default()
6656 };
6657 (sections.to_source(), Some(sections))
6658 }
6659 } else {
6660 let sections = self.parse_template_sections()?;
6661 if sections.is_empty() {
6662 return Err(CalError::TemplateSyntaxError {
6663 detail: "template body must be AS \"...\" or at least one \
6664 section (HEADER, ELEMENT, ELEMENT_SUMMARY, \
6665 ELEMENT_OMIT, SOURCE_BREAK, FOOTER)"
6666 .to_string(),
6667 span: Some(self.current_span()),
6668 });
6669 }
6670 (sections.to_source(), Some(sections))
6671 };
6672
6673 let span_end = self.prev_span();
6674 Ok(CalStatement::DefineTemplate(DefineTemplateStmt {
6675 name,
6676 description,
6677 parent,
6678 grain_types,
6679 source,
6680 sections,
6681 span: Some(Span::new(
6682 span_start.start,
6683 span_end.end,
6684 span_start.line,
6685 span_start.col,
6686 )),
6687 }))
6688 }
6689
6690 fn parse_template_sections(&mut self) -> CalResult<TemplateSectionSources> {
6696 let mut out = TemplateSectionSources::default();
6697
6698 loop {
6699 let span = self.current_span();
6700 let Some(st) = self.peek() else { break };
6701 let (keyword, body) = match &st.token {
6702 Token::Header(b) => ("HEADER", b),
6703 Token::Element(b) => ("ELEMENT", b),
6704 Token::ElementSummary(b) => ("ELEMENT_SUMMARY", b),
6705 Token::ElementOmit(b) => ("ELEMENT_OMIT", b),
6706 Token::SourceBreak(b) => ("SOURCE_BREAK", b),
6707 Token::Footer(b) => ("FOOTER", b),
6708 _ => break,
6709 };
6710 let SectionBody::Body(text) = body else { break };
6712 let text = text.clone();
6713 self.advance();
6714
6715 let slot = match keyword {
6716 "HEADER" => &mut out.header,
6717 "ELEMENT" => &mut out.element,
6718 "ELEMENT_SUMMARY" => &mut out.element_summary,
6719 "ELEMENT_OMIT" => &mut out.element_omit,
6720 "SOURCE_BREAK" => &mut out.source_break,
6721 _ => &mut out.footer,
6722 };
6723 Self::set_section(slot, text, keyword, span)?;
6724 }
6725
6726 Ok(out)
6727 }
6728
6729 fn parse_template_name(&mut self) -> CalResult<String> {
6737 if matches!(
6738 self.peek(),
6739 Some(SpannedToken {
6740 token: Token::StringLiteral(_),
6741 ..
6742 })
6743 ) {
6744 return self.parse_string_literal();
6745 }
6746
6747 let span = self.current_span();
6748 if self.at_exact(&Token::As) {
6751 return Err(CalError::UnexpectedToken {
6752 expected: "a template name".into(),
6753 found: "AS".into(),
6754 span: Some(span),
6755 suggestion: Some("Name the template: DEFINE TEMPLATE <name> AS \"...\"".into()),
6756 });
6757 }
6758
6759 let name = self.parse_label()?;
6763 if is_destructive_keyword(&name) {
6767 return Err(CalError::UnexpectedToken {
6768 expected: "a template name".into(),
6769 found: name,
6770 span: Some(span),
6771 suggestion: Some(
6772 "Choose a different name — CAL keeps destructive words out \
6773 of its grammar entirely."
6774 .into(),
6775 ),
6776 });
6777 }
6778 Ok(name)
6779 }
6780
6781 fn set_section(
6783 slot: &mut Option<String>,
6784 text: String,
6785 keyword: &str,
6786 span: Span,
6787 ) -> CalResult<()> {
6788 if slot.is_some() {
6789 return Err(CalError::TemplateSyntaxError {
6790 detail: format!("section {keyword} is defined twice"),
6791 span: Some(span),
6792 });
6793 }
6794 *slot = Some(text);
6795 Ok(())
6796 }
6797
6798 fn parse_define_query(&mut self) -> CalResult<CalStatement> {
6802 let span_start = self.current_span();
6803 self.expect_exact(&Token::Define)?;
6804 self.expect_exact(&Token::Query)?;
6805
6806 let name = self.parse_string_literal()?;
6808
6809 if !crate::queries::QueryRegistry::is_valid_name(&name) {
6811 return Err(CalError::TemplateInvalidName {
6812 name,
6813 span: Some(self.current_span()),
6814 });
6815 }
6816
6817 let params = if self.at_exact(&Token::LParen) {
6819 self.parse_query_param_decls()?
6820 } else {
6821 Vec::new()
6822 };
6823
6824 if params.len() > crate::queries::MAX_QUERY_PARAMS {
6826 return Err(CalError::TooManyQueryParams {
6827 count: params.len(),
6828 max: crate::queries::MAX_QUERY_PARAMS,
6829 span: Some(self.current_span()),
6830 });
6831 }
6832
6833 let description = if matches!(
6835 self.peek(),
6836 Some(SpannedToken {
6837 token: Token::Ident(s),
6838 ..
6839 }) if s.eq_ignore_ascii_case("DESCRIPTION")
6840 ) {
6841 self.advance();
6842 Some(self.parse_string_literal()?)
6843 } else {
6844 None
6845 };
6846
6847 self.expect_exact(&Token::As)?;
6849
6850 self.expect_exact(&Token::LBrace)?;
6852 let body_start = self.pos;
6853
6854 let mut brace_depth = 1u32;
6856 while brace_depth > 0 {
6857 match self.peek() {
6858 Some(SpannedToken {
6859 token: Token::LBrace,
6860 ..
6861 }) => {
6862 brace_depth += 1;
6863 self.advance();
6864 }
6865 Some(SpannedToken {
6866 token: Token::RBrace,
6867 ..
6868 }) => {
6869 brace_depth -= 1;
6870 if brace_depth > 0 {
6871 self.advance();
6872 }
6873 }
6874 None => {
6875 return Err(CalError::UnexpectedToken {
6876 expected: "closing '}' for DEFINE QUERY body".into(),
6877 found: "end of input".into(),
6878 span: Some(self.current_span()),
6879 suggestion: None,
6880 });
6881 }
6882 _ => {
6883 self.advance();
6884 }
6885 }
6886 }
6887
6888 let body_text = self.reconstruct_body_text(body_start);
6890
6891 if body_text.len() > crate::queries::MAX_QUERY_BODY_SIZE {
6893 return Err(CalError::QueryBodyTooLarge {
6894 size: body_text.len(),
6895 max: crate::queries::MAX_QUERY_BODY_SIZE,
6896 span: Some(span_start),
6897 });
6898 }
6899
6900 self.validate_query_body(&body_text, &span_start)?;
6903
6904 self.advance(); let span_end = self.prev_span();
6907 Ok(CalStatement::DefineQuery(DefineQueryStmt {
6908 name,
6909 description,
6910 params,
6911 body: body_text,
6912 span: Some(Span::new(
6913 span_start.start,
6914 span_end.end,
6915 span_start.line,
6916 span_start.col,
6917 )),
6918 }))
6919 }
6920
6921 fn parse_query_param_decls(&mut self) -> CalResult<Vec<QueryParam>> {
6923 self.expect_exact(&Token::LParen)?;
6924 let mut params = Vec::new();
6925
6926 loop {
6927 if self.at_exact(&Token::RParen) {
6928 self.advance();
6929 break;
6930 }
6931 if !params.is_empty() {
6932 self.expect_exact(&Token::Comma)?;
6933 }
6934
6935 let name = match self.peek() {
6937 Some(SpannedToken {
6938 token: Token::Parameter(n),
6939 ..
6940 }) => {
6941 let n = n.clone();
6942 self.advance();
6943 n
6944 }
6945 _ => {
6946 return Err(CalError::UnexpectedToken {
6947 expected: "parameter name ($name)".into(),
6948 found: self
6949 .peek()
6950 .map(|t| t.token.description())
6951 .unwrap_or("end of input".into()),
6952 span: Some(self.current_span()),
6953 suggestion: Some(
6954 "Parameter declarations use $name syntax, e.g. ($user, $limit = 10)"
6955 .into(),
6956 ),
6957 });
6958 }
6959 };
6960
6961 let default = if self.at_exact(&Token::Eq) {
6963 self.advance();
6964 Some(self.parse_value()?)
6965 } else {
6966 None
6967 };
6968
6969 params.push(QueryParam { name, default });
6970 }
6971
6972 Ok(params)
6973 }
6974
6975 fn reconstruct_body_text(&self, body_start: usize) -> String {
6977 if body_start >= self.pos {
6979 return String::new();
6980 }
6981 let start_byte = self.tokens[body_start].span.start;
6982 let end_byte = self.tokens[self.pos - 1].span.end;
6983 self.input[start_byte..end_byte].trim().to_string()
6984 }
6985
6986 fn validate_query_body(&self, body: &str, span: &Span) -> CalResult<()> {
6989 if body.contains('$') {
6996 let upper = body.to_ascii_uppercase();
7005 for word in upper.split(|c: char| !c.is_ascii_alphanumeric() && c != '_') {
7006 if word == "RUN" {
7007 return Err(CalError::RecursiveQuery { span: Some(*span) });
7008 }
7009 if matches!(
7010 word,
7011 "ADD" | "SUPERSEDE" | "ACCUMULATE" | "REVERT" | "FORGET" | "PURGE" | "DROP"
7012 | "DEFINE" | "DELETE" | "ERASE" | "TRUNCATE" | "INSERT" | "UPDATE"
7013 | "CREATE" | "GRANT"
7014 ) {
7015 return Err(CalError::WriteInQueryBody {
7016 stmt: word.to_string(),
7017 span: Some(*span),
7018 });
7019 }
7020 }
7021 match super::parser::parse(body) {
7029 Ok(q) => return self.check_statement_read_only(&q.statement, span),
7030 Err(strict) => {
7031 let filled = params_as_literals(body);
7032 match super::parser::parse(&filled) {
7033 Ok(q) => return self.check_statement_read_only(&q.statement, span),
7034 Err(_) => {
7038 return Err(CalError::InvalidQueryBody {
7039 detail: strict.to_string(),
7040 span: Some(*span),
7041 })
7042 }
7043 }
7044 }
7045 }
7046 }
7047
7048 let parsed = match super::parser::parse(body) {
7050 Ok(q) => q,
7051 Err(e) => {
7052 return Err(CalError::InvalidQueryBody {
7053 detail: e.to_string(),
7054 span: Some(*span),
7055 });
7056 }
7057 };
7058
7059 self.check_statement_read_only(&parsed.statement, span)?;
7061 Ok(())
7062 }
7063
7064 fn check_statement_read_only(&self, stmt: &CalStatement, span: &Span) -> CalResult<()> {
7068 check_read_only_statement(stmt, span)
7069 }
7070
7071 fn parse_run_query(&mut self) -> CalResult<CalStatement> {
7075 let span_start = self.current_span();
7076 self.expect_exact(&Token::Run)?;
7077
7078 if let Some(SpannedToken { token: Token::Ident(w), .. }) = self.peek() {
7081 if w.eq_ignore_ascii_case("LOOP") {
7082 return self.parse_run_loop(span_start);
7083 }
7084 if w.eq_ignore_ascii_case("TRACE") {
7085 return self.parse_run_trace(span_start);
7086 }
7087 }
7088
7089 let name = self.parse_string_literal()?;
7090
7091 let bindings = if self.at_exact(&Token::LParen) {
7093 self.parse_query_param_bindings()?
7094 } else {
7095 Vec::new()
7096 };
7097
7098 let span_end = self.prev_span();
7099 Ok(CalStatement::RunQuery(RunQueryStmt {
7100 name,
7101 bindings,
7102 span: Some(Span::new(
7103 span_start.start,
7104 span_end.end,
7105 span_start.line,
7106 span_start.col,
7107 )),
7108 }))
7109 }
7110
7111 fn parse_query_param_bindings(&mut self) -> CalResult<Vec<(String, Value)>> {
7113 self.expect_exact(&Token::LParen)?;
7114 let mut bindings = Vec::new();
7115
7116 loop {
7117 if self.at_exact(&Token::RParen) {
7118 self.advance();
7119 break;
7120 }
7121 if !bindings.is_empty() {
7122 self.expect_exact(&Token::Comma)?;
7123 }
7124
7125 let name = match self.peek() {
7127 Some(SpannedToken {
7128 token: Token::Parameter(n),
7129 ..
7130 }) => {
7131 let n = n.clone();
7132 self.advance();
7133 n
7134 }
7135 _ => {
7136 return Err(CalError::UnexpectedToken {
7137 expected: "parameter binding ($name = value)".into(),
7138 found: self
7139 .peek()
7140 .map(|t| t.token.description())
7141 .unwrap_or("end of input".into()),
7142 span: Some(self.current_span()),
7143 suggestion: Some(
7144 "Parameter bindings use $name = value syntax, e.g. ($user = \"john\")"
7145 .into(),
7146 ),
7147 });
7148 }
7149 };
7150 self.expect_exact(&Token::Eq)?;
7151 let value = self.parse_value()?;
7152 bindings.push((name, value));
7153 }
7154
7155 Ok(bindings)
7156 }
7157
7158 fn parse_stream_assemble(&mut self) -> CalResult<CalStatement> {
7162 self.expect_exact(&Token::Stream)?;
7163 let stmt = self.parse_assemble()?;
7165 match stmt {
7166 CalStatement::Assemble(mut asm) => {
7167 asm.streaming = true;
7168 Ok(CalStatement::Assemble(asm))
7169 }
7170 other => Ok(other), }
7172 }
7173}
7174
7175fn suggest_grain_type_plural(s: &str) -> Option<String> {
7181 let lower = s.to_ascii_lowercase();
7182 match lower.as_str() {
7184 "beliefs" | "belief" => return Some("did you mean \"facts\"?".into()),
7185 "episodes" | "episode" => return Some("did you mean \"events\"?".into()),
7186 "checkpoints" | "checkpoint" => return Some("did you mean \"states\"?".into()),
7187 "actions" | "action" | "toolcalls" | "tool_calls" | "tool_call" => {
7188 return Some("did you mean \"tools\"?".into())
7189 }
7190 _ => {}
7191 }
7192 None
7195}
7196
7197fn suggest_grain_type_singular(s: &str) -> Option<String> {
7199 let lower = s.to_ascii_lowercase();
7200 match lower.as_str() {
7201 "belief" => return Some("did you mean \"fact\"?".into()),
7202 "episode" => return Some("did you mean \"event\"?".into()),
7203 "checkpoint" => return Some("did you mean \"state\"?".into()),
7204 "toolcall" | "tool_call" => return Some("did you mean \"tool\"?".into()),
7205 _ => {}
7206 }
7207 None
7208}
7209
7210use super::ast::SinceClause;
7215
7216#[cfg(test)]
7221mod tests {
7222 use super::*;
7223
7224 fn p(input: &str) -> CalQuery {
7225 parse(input).expect(input)
7226 }
7227
7228 fn pe(input: &str) -> CalError {
7229 parse(input).expect_err(input)
7230 }
7231
7232 fn af(spec: FormatSpec) -> AliasedFormat {
7234 AliasedFormat { spec, alias: None }
7235 }
7236
7237 fn af_as(spec: FormatSpec, alias: &str) -> AliasedFormat {
7239 AliasedFormat {
7240 spec,
7241 alias: Some(alias.to_string()),
7242 }
7243 }
7244
7245 #[test]
7248 fn test_trailing_garbage_rejected() {
7249 let e = pe(r#"RECALL facts WHERE subject = "john" banana banana"#);
7250 assert!(e.to_string().contains("end of query"), "{e}");
7251 }
7252
7253 #[test]
7254 fn test_trailing_second_statement_rejected() {
7255 let e = pe(r#"RECALL facts WHERE subject = "john"; RECALL facts WHERE subject = "mary""#);
7257 assert!(e.to_string().contains("end of query"), "{e}");
7258 let e = pe(r#"RECALL facts WHERE subject = "john"; DELETE FROM facts"#);
7259 assert!(e.to_string().contains("end of query"), "{e}");
7260 }
7261
7262 #[test]
7263 fn test_trailing_semicolons_allowed() {
7264 p(r#"RECALL facts WHERE subject = "john";"#);
7265 p(r#"RECALL facts WHERE subject = "john";;"#);
7266 }
7267
7268 #[test]
7269 fn test_assemble_with_mixed_dedup_list_keeps_all_options() {
7270 let q = p("ASSEMBLE \"ctx\" FROM a: (RECALL facts), b: (RECALL events) \
7275 BUDGET 400 tokens FORMAT sml \
7276 WITH dedup, recency_weight(0.7), provenance");
7277 let CalStatement::Assemble(a) = &q.statement else {
7278 panic!("expected Assemble, got {:?}", q.statement);
7279 };
7280 assert_eq!(a.assemble_with.len(), 1, "dedup stays assemble-scoped");
7281 assert!(
7282 a.with_options.iter().any(|o| matches!(o, WithOption::RecencyWeight { .. })),
7283 "recency_weight kept on the assemble: {:?}",
7284 a.with_options
7285 );
7286 assert!(
7287 a.with_options.iter().any(|o| matches!(o, WithOption::Provenance)),
7288 "provenance kept on the assemble: {:?}",
7289 a.with_options
7290 );
7291 assert!(
7292 q.with_options.is_empty(),
7293 "options stay assemble-scoped, not on the enclosing query: {:?}",
7294 q.with_options
7295 );
7296 }
7297
7298 #[test]
7303 fn test_nested_assemble_with_options_do_not_leak() {
7304 fn inner_assemble(stmt: &CalStatement) -> &AssembleStmt {
7305 match stmt {
7306 CalStatement::Assemble(a) => a,
7307 CalStatement::Explain(e) => inner_assemble(&e.inner),
7308 CalStatement::Coalesce(c) => inner_assemble(&c.branches[0].query),
7309 other => panic!("expected an ASSEMBLE (possibly wrapped), got {:?}", other),
7310 }
7311 }
7312 let src = "ASSEMBLE \"ctx\" FROM a: (RECALL facts), b: (RECALL events) \
7315 FORMAT sml WITH dedup, query_expansion";
7316 for wrapped in [format!("EXPLAIN {src}"), format!("COALESCE {{ {src} }} OR {{ RECALL facts }}")] {
7317 let q = p(&wrapped);
7318 assert!(
7319 q.with_options.is_empty(),
7320 "{wrapped:?}: options leaked to the wrapper query: {:?}",
7321 q.with_options
7322 );
7323 let a = inner_assemble(&q.statement);
7324 assert!(
7325 a.with_options.iter().any(|o| matches!(o, WithOption::QueryExpansion)),
7326 "{wrapped:?}: query_expansion must scope to the inner assemble: {:?}",
7327 a.with_options
7328 );
7329 }
7330 }
7331
7332 #[test]
7335 fn test_recall_beliefs() {
7336 let q = p("RECALL facts");
7337 match &q.statement {
7338 CalStatement::Recall(r) => {
7339 assert_eq!(r.grain_type, GrainTypePlural::Facts);
7340 assert!(r.where_clause.is_none());
7341 assert!(r.about.is_none());
7342 }
7343 other => panic!("expected Recall, got {:?}", other),
7344 }
7345 }
7346
7347 #[test]
7350 fn test_recall_where_subject_eq() {
7351 let q = p(r#"RECALL facts WHERE subject = "john""#);
7352 match &q.statement {
7353 CalStatement::Recall(r) => {
7354 assert!(r.where_clause.is_some());
7355 let wc = r.where_clause.as_ref().unwrap();
7356 match &wc.condition {
7357 Condition::Comparison {
7358 field,
7359 comparator,
7360 value,
7361 ..
7362 } => {
7363 assert_eq!(field, "subject");
7364 assert_eq!(*comparator, Comparator::Eq);
7365 assert_eq!(
7366 *value,
7367 Value::String {
7368 value: "john".into()
7369 }
7370 );
7371 }
7372 other => panic!("expected Comparison, got {:?}", other),
7373 }
7374 }
7375 _ => panic!("expected Recall"),
7376 }
7377 }
7378
7379 #[test]
7382 fn test_recall_about() {
7383 let q = p(r#"RECALL facts ABOUT "john preferences""#);
7384 match &q.statement {
7385 CalStatement::Recall(r) => {
7386 assert_eq!(r.about.as_ref().unwrap().text, "john preferences");
7387 }
7388 _ => panic!("expected Recall"),
7389 }
7390 }
7391
7392 #[test]
7395 fn test_recall_recent() {
7396 let q = p("RECALL events RECENT 5");
7397 match &q.statement {
7398 CalStatement::Recall(r) => {
7399 assert_eq!(r.grain_type, GrainTypePlural::Events);
7400 assert_eq!(r.recent.as_ref().unwrap().count, 5);
7401 }
7402 _ => panic!("expected Recall"),
7403 }
7404 }
7405
7406 #[test]
7409 fn test_recall_since() {
7410 let q = p(r#"RECALL facts SINCE "3 days ago""#);
7411 match &q.statement {
7412 CalStatement::Recall(r) => {
7413 assert_eq!(r.since.as_ref().unwrap().expression, "3 days ago");
7414 }
7415 _ => panic!("expected Recall"),
7416 }
7417 }
7418
7419 #[test]
7422 fn test_recall_pipeline() {
7423 let q = p(r#"RECALL facts WHERE subject = "john" ORDER BY confidence DESC LIMIT 10"#);
7425 assert_eq!(q.pipeline.len(), 2);
7426 match &q.pipeline[0] {
7427 PipelineStage::OrderBy {
7428 field, descending, ..
7429 } => {
7430 assert_eq!(field, "confidence");
7431 assert!(*descending);
7432 }
7433 other => panic!("expected OrderBy, got {:?}", other),
7434 }
7435 match &q.pipeline[1] {
7436 PipelineStage::Limit { value, .. } => assert_eq!(*value, 10),
7437 other => panic!("expected Limit, got {:?}", other),
7438 }
7439 }
7440
7441 #[test]
7442 fn test_recall_pipeline_with_pipe_backward_compat() {
7443 let q = p(r#"RECALL facts WHERE subject = "john" | ORDER BY confidence DESC | LIMIT 10"#);
7445 assert_eq!(q.pipeline.len(), 2);
7446 match &q.pipeline[0] {
7447 PipelineStage::OrderBy {
7448 field, descending, ..
7449 } => {
7450 assert_eq!(field, "confidence");
7451 assert!(*descending);
7452 }
7453 other => panic!("expected OrderBy, got {:?}", other),
7454 }
7455 match &q.pipeline[1] {
7456 PipelineStage::Limit { value, .. } => assert_eq!(*value, 10),
7457 other => panic!("expected Limit, got {:?}", other),
7458 }
7459 }
7460
7461 #[test]
7464 fn test_recall_with_superseded_and_score_breakdown() {
7465 let q = p("RECALL facts WITH superseded, score_breakdown");
7466 assert!(q.with_options.contains(&WithOption::Superseded));
7467 assert!(q.with_options.contains(&WithOption::ScoreBreakdown));
7468 }
7469
7470 #[test]
7473 fn test_recall_format_json() {
7474 let q = p("RECALL facts FORMAT json");
7475 assert_eq!(q.format, Some(FormatClause::Single(FormatSpec::Json)));
7476 }
7477
7478 #[test]
7481 fn test_exists_hash() {
7482 let q = p("EXISTS sha256:abc123def456");
7483 match &q.statement {
7484 CalStatement::Exists(e) => {
7485 let wc = e.where_clause.as_ref().unwrap();
7486 match &wc.condition {
7487 Condition::Comparison { field, value, .. } => {
7488 assert_eq!(field, "hash");
7489 assert_eq!(
7490 *value,
7491 Value::Hash {
7492 value: "abc123def456".into()
7493 }
7494 );
7495 }
7496 other => panic!("expected Comparison, got {:?}", other),
7497 }
7498 }
7499 other => panic!("expected Exists, got {:?}", other),
7500 }
7501 }
7502
7503 #[test]
7506 fn test_assemble() {
7507 let q = p(r#"ASSEMBLE "daily_summary" FROM (RECALL facts RECENT 10)"#);
7508 match &q.statement {
7509 CalStatement::Assemble(a) => {
7510 assert_eq!(a.topic, "daily_summary");
7511 match &a.from {
7512 Source::Query(_) => {}
7513 other => panic!("expected Query source, got {:?}", other),
7514 }
7515 }
7516 other => panic!("expected Assemble, got {:?}", other),
7517 }
7518 }
7519
7520 #[test]
7523 fn test_explain() {
7524 let q = p(r#"EXPLAIN RECALL facts WHERE subject = "john""#);
7525 match &q.statement {
7526 CalStatement::Explain(e) => {
7527 matches!(*e.inner, CalStatement::Recall(_));
7528 }
7529 other => panic!("expected Explain, got {:?}", other),
7530 }
7531 }
7532
7533 #[test]
7536 fn test_describe_grain_types() {
7537 let q = p("DESCRIBE grain_types");
7538 match &q.statement {
7539 CalStatement::Describe(_) => {}
7540 other => panic!("expected Describe, got {:?}", other),
7541 }
7542 }
7543
7544 #[test]
7547 fn test_batch() {
7548 let q = p("BATCH { recent: RECALL facts RECENT 5, ex: EXISTS sha256:abc123def456 }");
7549 match &q.statement {
7550 CalStatement::Batch(b) => {
7551 assert_eq!(b.statements.len(), 2);
7552 assert!(matches!(
7553 &b.statements[0].statement,
7554 CalStatement::Recall(_)
7555 ));
7556 assert!(matches!(
7557 &b.statements[1].statement,
7558 CalStatement::Exists(_)
7559 ));
7560 }
7561 other => panic!("expected Batch, got {:?}", other),
7562 }
7563 }
7564
7565 #[test]
7568 fn test_coalesce() {
7569 let q = p(
7570 r#"COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
7571 );
7572 assert!(matches!(q.statement, CalStatement::Coalesce(_)));
7573 }
7574
7575 #[test]
7578 fn test_union() {
7579 let q = p(
7580 r#"(RECALL facts WHERE subject = "john") UNION (RECALL facts WHERE subject = "bob")"#,
7581 );
7582 match &q.statement {
7583 CalStatement::SetOp(s) => {
7584 assert_eq!(s.op, SetOp::Union);
7585 assert_eq!(s.operands.len(), 2);
7586 }
7587 other => panic!("expected SetOp, got {:?}", other),
7588 }
7589 }
7590
7591 #[test]
7594 fn test_add_fact() {
7595 let q = p(
7596 r#"ADD fact SET subject = "john" SET relation = "likes" SET object = "coffee" REASON "user preference""#,
7597 );
7598 match &q.statement {
7599 CalStatement::Add(a) => {
7600 assert_eq!(a.grain_type, GrainTypeSingular::Fact);
7601 assert_eq!(a.fields.len(), 3);
7602 assert_eq!(a.fields[0].field, "subject");
7603 }
7604 other => panic!("expected Add, got {:?}", other),
7605 }
7606 }
7607
7608 #[test]
7609 fn test_add_fact_reason_stored() {
7610 let q = p(
7611 r#"ADD fact SET subject = "john" SET relation = "likes" SET object = "coffee" REASON "user preference""#,
7612 );
7613 match &q.statement {
7614 CalStatement::Add(a) => {
7615 assert_eq!(a.reason, "user preference");
7616 }
7617 other => panic!("expected Add, got {:?}", other),
7618 }
7619 }
7620
7621 #[test]
7622 fn test_add_fact_because_accepted() {
7623 let q = p(
7624 r#"ADD fact SET subject = "bob" SET relation = "likes" SET object = "rust" BECAUSE "test reason""#,
7625 );
7626 match &q.statement {
7627 CalStatement::Add(a) => {
7628 assert_eq!(a.grain_type, GrainTypeSingular::Fact);
7629 assert_eq!(a.reason, "test reason");
7630 }
7631 other => panic!("expected Add, got {:?}", other),
7632 }
7633 }
7634
7635 #[test]
7636 fn test_add_fact_missing_reason_fails() {
7637 let input = r#"ADD fact SET subject = "john" SET relation = "likes" SET object = "coffee""#;
7638 let err = parse(input).unwrap_err();
7639 assert!(
7640 matches!(err, CalError::MissingReason { .. }),
7641 "expected MissingReason, got: {:?}",
7642 err
7643 );
7644 }
7645
7646 #[test]
7647 fn test_add_duplicate_set_field_warning() {
7648 let q = p(
7649 r#"ADD fact SET subject = "a" SET subject = "b" SET relation = "r" SET object = "o" REASON "test""#,
7650 );
7651 match &q.statement {
7652 CalStatement::Add(a) => {
7653 assert_eq!(a.fields.len(), 4);
7654 assert_eq!(a.fields[0].field, "subject");
7656 assert_eq!(a.fields[1].field, "subject");
7657 }
7658 other => panic!("expected Add, got {:?}", other),
7659 }
7660 assert_eq!(q.warnings.len(), 1);
7662 assert!(
7663 matches!(&q.warnings[0], CalWarning::DuplicateSetField { field, .. } if field == "subject"),
7664 "expected DuplicateSetField warning, got: {:?}",
7665 q.warnings
7666 );
7667 }
7668
7669 #[test]
7672 fn test_add_workflow_simple_linear() {
7673 let q = p(
7674 r#"ADD workflow "CI pipeline" build -> test -> deploy REASON "standard pipeline""#,
7675 );
7676 match &q.statement {
7677 CalStatement::AddWorkflow(wf) => {
7678 assert_eq!(wf.name, "CI pipeline");
7679 assert_eq!(wf.nodes, vec!["build", "test", "deploy"]);
7680 assert_eq!(wf.edges.len(), 2);
7681 assert_eq!(wf.edges[0].src, "build");
7682 assert_eq!(wf.edges[0].dst, "test");
7683 assert_eq!(wf.edges[1].src, "test");
7684 assert_eq!(wf.edges[1].dst, "deploy");
7685 assert_eq!(wf.reason, "standard pipeline");
7686 }
7687 other => panic!("expected AddWorkflow, got {:?}", other),
7688 }
7689 }
7690
7691 #[test]
7692 fn test_add_workflow_parallel() {
7693 let q = p(
7694 r#"ADD workflow "review" lint -> (security, compliance) -> evaluate REASON "parallel review""#,
7695 );
7696 match &q.statement {
7697 CalStatement::AddWorkflow(wf) => {
7698 assert_eq!(wf.nodes, vec!["lint", "security", "compliance", "evaluate"]);
7699 assert_eq!(wf.edges.len(), 4);
7702 assert_eq!(wf.edges[0].src, "lint");
7703 assert_eq!(wf.edges[0].dst, "security");
7704 assert_eq!(wf.edges[1].src, "lint");
7705 assert_eq!(wf.edges[1].dst, "compliance");
7706 assert_eq!(wf.edges[2].src, "security");
7707 assert_eq!(wf.edges[2].dst, "evaluate");
7708 assert_eq!(wf.edges[3].src, "compliance");
7709 assert_eq!(wf.edges[3].dst, "evaluate");
7710 }
7711 other => panic!("expected AddWorkflow, got {:?}", other),
7712 }
7713 }
7714
7715 #[test]
7716 fn test_add_workflow_conditional() {
7717 let q = p(
7718 r#"ADD workflow "gate" evaluate -> implement WHEN "approved" evaluate -> reject WHEN "rejected" REASON "approval routing""#,
7719 );
7720 match &q.statement {
7721 CalStatement::AddWorkflow(wf) => {
7722 assert_eq!(wf.nodes, vec!["evaluate", "implement", "reject"]);
7723 assert_eq!(wf.edges.len(), 2);
7724 assert_eq!(wf.edges[0].cond, Some("approved".into()));
7725 assert_eq!(wf.edges[1].cond, Some("rejected".into()));
7726 }
7727 other => panic!("expected AddWorkflow, got {:?}", other),
7728 }
7729 }
7730
7731 #[test]
7732 fn test_add_workflow_retry() {
7733 let q = p(
7734 r#"ADD workflow "deploy" build -> deploy * 3 -> notify REASON "retry deploy""#,
7735 );
7736 match &q.statement {
7737 CalStatement::AddWorkflow(wf) => {
7738 assert_eq!(wf.nodes, vec!["build", "deploy", "notify"]);
7739 assert_eq!(wf.edges.len(), 2);
7740 assert_eq!(wf.edges[0].src, "build");
7742 assert_eq!(wf.edges[0].dst, "deploy");
7743 assert_eq!(wf.edges[0].repeat, Some(3));
7744 assert_eq!(wf.edges[1].src, "deploy");
7746 assert_eq!(wf.edges[1].dst, "notify");
7747 }
7748 other => panic!("expected AddWorkflow, got {:?}", other),
7749 }
7750 }
7751
7752 #[test]
7753 fn test_add_workflow_with_bindings() {
7754 let q = p(
7755 r#"ADD workflow "pipeline" build -> test BIND build = sha256:def11111 BIND test = sha256:def22222 REASON "bound pipeline""#,
7756 );
7757 match &q.statement {
7758 CalStatement::AddWorkflow(wf) => {
7759 assert_eq!(wf.bindings.len(), 2);
7760 assert_eq!(wf.bindings[0].node, "build");
7761 assert_eq!(wf.bindings[0].hash, "def11111");
7762 assert_eq!(wf.bindings[1].node, "test");
7763 assert_eq!(wf.bindings[1].hash, "def22222");
7764 }
7765 other => panic!("expected AddWorkflow, got {:?}", other),
7766 }
7767 }
7768
7769 #[test]
7770 fn test_add_workflow_no_trigger() {
7771 let q = p(r#"ADD workflow "checklist" review -> test -> merge REASON "template""#);
7772 match &q.statement {
7773 CalStatement::AddWorkflow(wf) => {
7774 assert_eq!(wf.name, "checklist");
7775 assert_eq!(wf.nodes, vec!["review", "test", "merge"]);
7776 }
7777 other => panic!("expected AddWorkflow, got {:?}", other),
7778 }
7779 }
7780
7781 #[test]
7782 fn test_add_workflow_quoted_node_names() {
7783 let q = p(
7784 r#"ADD workflow "onboarding" "send welcome email" -> "schedule orientation" REASON "onboard""#,
7785 );
7786 match &q.statement {
7787 CalStatement::AddWorkflow(wf) => {
7788 assert_eq!(wf.nodes, vec!["send welcome email", "schedule orientation"]);
7789 }
7790 other => panic!("expected AddWorkflow, got {:?}", other),
7791 }
7792 }
7793
7794 #[test]
7795 fn test_add_workflow_mixed_pipeline() {
7796 let q = p(
7797 r#"ADD workflow "release" build -> (unit_test, lint) -> integration_test integration_test -> stage_deploy * 3 stage_deploy -> approval approval -> prod_deploy WHEN "approved" approval -> rollback WHEN "rejected" REASON "release pipeline""#,
7798 );
7799 match &q.statement {
7800 CalStatement::AddWorkflow(wf) => {
7801 assert_eq!(
7802 wf.nodes,
7803 vec![
7804 "build",
7805 "unit_test",
7806 "lint",
7807 "integration_test",
7808 "stage_deploy",
7809 "approval",
7810 "prod_deploy",
7811 "rollback"
7812 ]
7813 );
7814 assert_eq!(wf.edges.len(), 8);
7818 }
7819 other => panic!("expected AddWorkflow, got {:?}", other),
7820 }
7821 }
7822
7823 #[test]
7829 fn test_add_workflow_single_node_no_edges() {
7830 let q = p(r#"ADD workflow "x" build REASON "y""#);
7831 match &q.statement {
7832 CalStatement::AddWorkflow(wf) => {
7833 assert_eq!(wf.name, "x");
7834 assert_eq!(wf.nodes, vec!["build"]);
7835 assert!(wf.edges.is_empty());
7836 assert_eq!(wf.reason, "y");
7837 }
7838 other => panic!("expected AddWorkflow, got {:?}", other),
7839 }
7840 }
7841
7842 #[test]
7844 fn test_add_workflow_long_chain() {
7845 let q = p(r#"ADD workflow "long" a -> b -> c -> d -> e -> f REASON "chain""#);
7846 match &q.statement {
7847 CalStatement::AddWorkflow(wf) => {
7848 assert_eq!(wf.nodes, vec!["a", "b", "c", "d", "e", "f"]);
7849 assert_eq!(wf.edges.len(), 5);
7850 assert_eq!(wf.edges[0].src, "a");
7851 assert_eq!(wf.edges[0].dst, "b");
7852 assert_eq!(wf.edges[1].src, "b");
7853 assert_eq!(wf.edges[1].dst, "c");
7854 assert_eq!(wf.edges[2].src, "c");
7855 assert_eq!(wf.edges[2].dst, "d");
7856 assert_eq!(wf.edges[3].src, "d");
7857 assert_eq!(wf.edges[3].dst, "e");
7858 assert_eq!(wf.edges[4].src, "e");
7859 assert_eq!(wf.edges[4].dst, "f");
7860 }
7861 other => panic!("expected AddWorkflow, got {:?}", other),
7862 }
7863 }
7864
7865 #[test]
7868 fn test_add_workflow_multiple_separate_chains() {
7869 let q = p(r#"ADD workflow "multi" a -> b c -> d REASON "two chains""#);
7870 match &q.statement {
7871 CalStatement::AddWorkflow(wf) => {
7872 assert_eq!(wf.nodes, vec!["a", "b", "c", "d"]);
7873 assert_eq!(wf.edges.len(), 2);
7874 assert_eq!(wf.edges[0].src, "a");
7875 assert_eq!(wf.edges[0].dst, "b");
7876 assert_eq!(wf.edges[1].src, "c");
7877 assert_eq!(wf.edges[1].dst, "d");
7878 }
7879 other => panic!("expected AddWorkflow, got {:?}", other),
7880 }
7881 }
7882
7883 #[test]
7889 fn test_add_workflow_nested_parallel_fails() {
7890 let err = pe(r#"ADD workflow "x" a -> (b, (c, d)) -> e REASON "y""#);
7894 assert!(matches!(err, CalError::UnexpectedToken { .. }));
7895 }
7896
7897 #[test]
7899 fn test_add_workflow_when_and_repeat_combined() {
7900 let q = p(r#"ADD workflow "x" a -> b WHEN "cond" * 3 REASON "y""#);
7901 match &q.statement {
7902 CalStatement::AddWorkflow(wf) => {
7903 assert_eq!(wf.edges.len(), 1);
7904 assert_eq!(wf.edges[0].src, "a");
7905 assert_eq!(wf.edges[0].dst, "b");
7906 assert_eq!(wf.edges[0].cond, Some("cond".into()));
7907 assert_eq!(wf.edges[0].repeat, Some(3));
7908 }
7909 other => panic!("expected AddWorkflow, got {:?}", other),
7910 }
7911 }
7912
7913 #[test]
7915 fn test_add_workflow_alphanumeric_node_names() {
7916 let q = p(r#"ADD workflow "x" step1 -> step2 REASON "y""#);
7917 match &q.statement {
7918 CalStatement::AddWorkflow(wf) => {
7919 assert_eq!(wf.nodes, vec!["step1", "step2"]);
7920 assert_eq!(wf.edges.len(), 1);
7921 }
7922 other => panic!("expected AddWorkflow, got {:?}", other),
7923 }
7924 }
7925
7926 #[test]
7928 fn test_add_workflow_underscore_names() {
7929 let q = p(r#"ADD workflow "x" __init__ -> _cleanup_ REASON "y""#);
7930 match &q.statement {
7931 CalStatement::AddWorkflow(wf) => {
7932 assert_eq!(wf.nodes, vec!["__init__", "_cleanup_"]);
7933 assert_eq!(wf.edges.len(), 1);
7934 }
7935 other => panic!("expected AddWorkflow, got {:?}", other),
7936 }
7937 }
7938
7939 #[test]
7941 fn test_add_workflow_single_char_names() {
7942 let q = p(r#"ADD workflow "x" a -> b -> c REASON "y""#);
7943 match &q.statement {
7944 CalStatement::AddWorkflow(wf) => {
7945 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
7946 assert_eq!(wf.edges.len(), 2);
7947 }
7948 other => panic!("expected AddWorkflow, got {:?}", other),
7949 }
7950 }
7951
7952 #[test]
7954 fn test_add_workflow_because_keyword() {
7955 let q = p(r#"ADD workflow "x" build -> test BECAUSE "alt keyword""#);
7956 match &q.statement {
7957 CalStatement::AddWorkflow(wf) => {
7958 assert_eq!(wf.reason, "alt keyword");
7959 assert_eq!(wf.nodes, vec!["build", "test"]);
7960 }
7961 other => panic!("expected AddWorkflow, got {:?}", other),
7962 }
7963 }
7964
7965 #[test]
7967 fn test_add_workflow_mixed_bare_and_quoted() {
7968 let q = p(r#"ADD workflow "x" build -> "run tests" -> deploy REASON "y""#);
7969 match &q.statement {
7970 CalStatement::AddWorkflow(wf) => {
7971 assert_eq!(wf.nodes, vec!["build", "run tests", "deploy"]);
7972 assert_eq!(wf.edges.len(), 2);
7973 }
7974 other => panic!("expected AddWorkflow, got {:?}", other),
7975 }
7976 }
7977
7978 #[test]
7980 fn test_add_workflow_single_element_parallel() {
7981 let q = p(r#"ADD workflow "x" a -> (b) -> c REASON "y""#);
7982 match &q.statement {
7983 CalStatement::AddWorkflow(wf) => {
7984 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
7985 assert_eq!(wf.edges.len(), 2);
7986 assert_eq!(wf.edges[0].src, "a");
7987 assert_eq!(wf.edges[0].dst, "b");
7988 assert_eq!(wf.edges[1].src, "b");
7989 assert_eq!(wf.edges[1].dst, "c");
7990 }
7991 other => panic!("expected AddWorkflow, got {:?}", other),
7992 }
7993 }
7994
7995 #[test]
7997 fn test_add_workflow_convergence_node() {
7998 let q = p(r#"ADD workflow "x" a -> c b -> c REASON "converge""#);
7999 match &q.statement {
8000 CalStatement::AddWorkflow(wf) => {
8001 assert_eq!(wf.nodes, vec!["a", "c", "b"]);
8003 assert_eq!(wf.edges.len(), 2);
8004 assert_eq!(wf.edges[0].src, "a");
8005 assert_eq!(wf.edges[0].dst, "c");
8006 assert_eq!(wf.edges[1].src, "b");
8007 assert_eq!(wf.edges[1].dst, "c");
8008 }
8009 other => panic!("expected AddWorkflow, got {:?}", other),
8010 }
8011 }
8012
8013 #[test]
8016 fn test_add_workflow_no_bind_clauses() {
8017 let q = p(r#"ADD workflow "x" a -> b REASON "no bindings""#);
8018 match &q.statement {
8019 CalStatement::AddWorkflow(wf) => {
8020 assert!(wf.bindings.is_empty());
8021 }
8022 other => panic!("expected AddWorkflow, got {:?}", other),
8023 }
8024 }
8025
8026 #[test]
8028 fn test_add_workflow_many_bind_clauses() {
8029 let q = p(concat!(
8030 r#"ADD workflow "x" a -> b -> c "#,
8031 "BIND a = sha256:aaa11111 ",
8032 "BIND b = sha256:bbb22222 ",
8033 "BIND c = sha256:ccc33333 ",
8034 "BIND a = sha256:aaa44444 ",
8035 "BIND b = sha256:bbb55555 ",
8036 "BIND c = sha256:ccc66666 ",
8037 "BIND a = sha256:aaa77777 ",
8038 "BIND b = sha256:bbb88888 ",
8039 "BIND c = sha256:ccc99999 ",
8040 "BIND a = sha256:aaa00000 ",
8041 r#"REASON "many bindings""#,
8042 ));
8043 match &q.statement {
8044 CalStatement::AddWorkflow(wf) => {
8045 assert_eq!(wf.bindings.len(), 10);
8046 assert_eq!(wf.bindings[0].node, "a");
8047 assert_eq!(wf.bindings[0].hash, "aaa11111");
8048 assert_eq!(wf.bindings[9].node, "a");
8049 assert_eq!(wf.bindings[9].hash, "aaa00000");
8050 }
8051 other => panic!("expected AddWorkflow, got {:?}", other),
8052 }
8053 }
8054
8055 #[test]
8062 fn add_workflow_refuses_the_removed_on_clause_and_says_where_triggers_live() {
8063 let e = parse(r#"ADD workflow "cron" ON "cron * * * * *" run_job REASON "scheduled""#)
8064 .expect_err("ON must be refused, not ignored");
8065 let msg = e.to_string();
8066 assert!(msg.contains("removed in 1.3"), "{msg}");
8067 assert!(msg.contains("trigger"), "the message must point at the replacement: {msg}");
8068 }
8069
8070 #[test]
8072 fn test_add_workflow_self_loop() {
8073 let q = p(r#"ADD workflow "x" a -> a REASON "self loop""#);
8074 match &q.statement {
8075 CalStatement::AddWorkflow(wf) => {
8076 assert_eq!(wf.nodes, vec!["a"]);
8077 assert_eq!(wf.edges.len(), 1);
8078 assert_eq!(wf.edges[0].src, "a");
8079 assert_eq!(wf.edges[0].dst, "a");
8080 }
8081 other => panic!("expected AddWorkflow, got {:?}", other),
8082 }
8083 }
8084
8085 #[test]
8087 fn test_add_workflow_repeat_one() {
8088 let q = p(r#"ADD workflow "x" a -> b * 1 REASON "y""#);
8089 match &q.statement {
8090 CalStatement::AddWorkflow(wf) => {
8091 assert_eq!(wf.edges[0].repeat, Some(1));
8092 }
8093 other => panic!("expected AddWorkflow, got {:?}", other),
8094 }
8095 }
8096
8097 #[test]
8099 fn test_add_workflow_repeat_large() {
8100 let q = p(r#"ADD workflow "x" a -> b * 100 REASON "y""#);
8101 match &q.statement {
8102 CalStatement::AddWorkflow(wf) => {
8103 assert_eq!(wf.edges[0].repeat, Some(100));
8104 }
8105 other => panic!("expected AddWorkflow, got {:?}", other),
8106 }
8107 }
8108
8109 #[test]
8111 fn test_add_workflow_multiple_when_conditions() {
8112 let q = p(
8113 r#"ADD workflow "x" a -> b WHEN "yes" a -> c WHEN "no" a -> d WHEN "maybe" REASON "branching""#,
8114 );
8115 match &q.statement {
8116 CalStatement::AddWorkflow(wf) => {
8117 assert_eq!(wf.nodes, vec!["a", "b", "c", "d"]);
8118 assert_eq!(wf.edges.len(), 3);
8119 assert_eq!(wf.edges[0].cond, Some("yes".into()));
8120 assert_eq!(wf.edges[1].cond, Some("no".into()));
8121 assert_eq!(wf.edges[2].cond, Some("maybe".into()));
8122 }
8123 other => panic!("expected AddWorkflow, got {:?}", other),
8124 }
8125 }
8126
8127 #[test]
8129 fn test_add_workflow_parallel_at_start() {
8130 let q = p(r#"ADD workflow "x" (a, b) -> c REASON "y""#);
8131 match &q.statement {
8132 CalStatement::AddWorkflow(wf) => {
8133 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
8134 assert_eq!(wf.edges.len(), 2);
8135 assert_eq!(wf.edges[0].src, "a");
8136 assert_eq!(wf.edges[0].dst, "c");
8137 assert_eq!(wf.edges[1].src, "b");
8138 assert_eq!(wf.edges[1].dst, "c");
8139 }
8140 other => panic!("expected AddWorkflow, got {:?}", other),
8141 }
8142 }
8143
8144 #[test]
8146 fn test_add_workflow_parallel_at_end() {
8147 let q = p(r#"ADD workflow "x" a -> (b, c) REASON "y""#);
8148 match &q.statement {
8149 CalStatement::AddWorkflow(wf) => {
8150 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
8151 assert_eq!(wf.edges.len(), 2);
8152 assert_eq!(wf.edges[0].src, "a");
8153 assert_eq!(wf.edges[0].dst, "b");
8154 assert_eq!(wf.edges[1].src, "a");
8155 assert_eq!(wf.edges[1].dst, "c");
8156 }
8157 other => panic!("expected AddWorkflow, got {:?}", other),
8158 }
8159 }
8160
8161 #[test]
8163 fn test_add_workflow_parallel_to_parallel() {
8164 let q = p(r#"ADD workflow "x" (a, b) -> (c, d) REASON "y""#);
8165 match &q.statement {
8166 CalStatement::AddWorkflow(wf) => {
8167 assert_eq!(wf.nodes, vec!["a", "b", "c", "d"]);
8168 assert_eq!(wf.edges.len(), 4);
8170 assert_eq!(wf.edges[0].src, "a");
8171 assert_eq!(wf.edges[0].dst, "c");
8172 assert_eq!(wf.edges[1].src, "a");
8173 assert_eq!(wf.edges[1].dst, "d");
8174 assert_eq!(wf.edges[2].src, "b");
8175 assert_eq!(wf.edges[2].dst, "c");
8176 assert_eq!(wf.edges[3].src, "b");
8177 assert_eq!(wf.edges[3].dst, "d");
8178 }
8179 other => panic!("expected AddWorkflow, got {:?}", other),
8180 }
8181 }
8182
8183 #[test]
8185 fn test_add_workflow_when_empty_string() {
8186 let q = p(r#"ADD workflow "x" a -> b WHEN "" REASON "y""#);
8187 match &q.statement {
8188 CalStatement::AddWorkflow(wf) => {
8189 assert_eq!(wf.edges[0].cond, Some("".into()));
8190 }
8191 other => panic!("expected AddWorkflow, got {:?}", other),
8192 }
8193 }
8194
8195 #[test]
8197 fn test_add_workflow_repeat_on_multiple_edges() {
8198 let q = p(r#"ADD workflow "x" a -> b * 2 -> c * 5 REASON "y""#);
8199 match &q.statement {
8200 CalStatement::AddWorkflow(wf) => {
8201 assert_eq!(wf.edges.len(), 2);
8202 assert_eq!(wf.edges[0].repeat, Some(2));
8203 assert_eq!(wf.edges[1].repeat, Some(5));
8204 }
8205 other => panic!("expected AddWorkflow, got {:?}", other),
8206 }
8207 }
8208
8209 #[test]
8211 fn test_add_workflow_large_parallel_group() {
8212 let q = p(r#"ADD workflow "x" init -> (a, b, c, d, e) -> done REASON "y""#);
8213 match &q.statement {
8214 CalStatement::AddWorkflow(wf) => {
8215 assert_eq!(wf.nodes, vec!["init", "a", "b", "c", "d", "e", "done"]);
8216 assert_eq!(wf.edges.len(), 10);
8218 }
8219 other => panic!("expected AddWorkflow, got {:?}", other),
8220 }
8221 }
8222
8223 #[test]
8225 fn test_add_workflow_diamond_pattern() {
8226 let q = p(r#"ADD workflow "diamond" a -> (b, c) -> d REASON "fork-join""#);
8227 match &q.statement {
8228 CalStatement::AddWorkflow(wf) => {
8229 assert_eq!(wf.nodes, vec!["a", "b", "c", "d"]);
8230 assert_eq!(wf.edges.len(), 4);
8231 assert_eq!(wf.edges[0].src, "a");
8233 assert_eq!(wf.edges[0].dst, "b");
8234 assert_eq!(wf.edges[1].src, "a");
8235 assert_eq!(wf.edges[1].dst, "c");
8236 assert_eq!(wf.edges[2].src, "b");
8238 assert_eq!(wf.edges[2].dst, "d");
8239 assert_eq!(wf.edges[3].src, "c");
8240 assert_eq!(wf.edges[3].dst, "d");
8241 }
8242 other => panic!("expected AddWorkflow, got {:?}", other),
8243 }
8244 }
8245
8246 #[test]
8248 fn test_add_workflow_with_sync() {
8249 let q = p(r#"ADD workflow "x" a -> b WITH sync REASON "y""#);
8250 match &q.statement {
8251 CalStatement::AddWorkflow(wf) => {
8252 assert_eq!(wf.with_options.len(), 1);
8253 assert_eq!(wf.with_options[0], AddWithOption::Sync);
8254 assert_eq!(wf.reason, "y");
8255 }
8256 other => panic!("expected AddWorkflow, got {:?}", other),
8257 }
8258 }
8259
8260 #[test]
8262 fn test_add_workflow_bind_and_with() {
8263 let q = p(concat!(
8264 r#"ADD workflow "x" a -> b "#,
8265 "BIND a = sha256:abc12345 ",
8266 "WITH sync ",
8267 r#"REASON "y""#,
8268 ));
8269 match &q.statement {
8270 CalStatement::AddWorkflow(wf) => {
8271 assert_eq!(wf.bindings.len(), 1);
8272 assert_eq!(wf.with_options.len(), 1);
8273 }
8274 other => panic!("expected AddWorkflow, got {:?}", other),
8275 }
8276 }
8277
8278 #[test]
8280 fn test_add_workflow_node_name_equals_workflow_name() {
8281 let q = p(r#"ADD workflow "build" build -> test REASON "y""#);
8282 match &q.statement {
8283 CalStatement::AddWorkflow(wf) => {
8284 assert_eq!(wf.name, "build");
8285 assert_eq!(wf.nodes, vec!["build", "test"]);
8286 }
8287 other => panic!("expected AddWorkflow, got {:?}", other),
8288 }
8289 }
8290
8291 #[test]
8293 fn test_add_workflow_when_not_inherited() {
8294 let q = p(r#"ADD workflow "x" a -> b WHEN "ok" -> c REASON "y""#);
8295 match &q.statement {
8296 CalStatement::AddWorkflow(wf) => {
8297 assert_eq!(wf.edges.len(), 2);
8298 assert_eq!(wf.edges[0].cond, Some("ok".into()));
8300 assert_eq!(wf.edges[1].cond, None);
8302 }
8303 other => panic!("expected AddWorkflow, got {:?}", other),
8304 }
8305 }
8306
8307 #[test]
8309 fn test_add_workflow_repeat_not_inherited() {
8310 let q = p(r#"ADD workflow "x" a -> b * 3 -> c REASON "y""#);
8311 match &q.statement {
8312 CalStatement::AddWorkflow(wf) => {
8313 assert_eq!(wf.edges.len(), 2);
8314 assert_eq!(wf.edges[0].repeat, Some(3));
8315 assert_eq!(wf.edges[1].repeat, None);
8316 }
8317 other => panic!("expected AddWorkflow, got {:?}", other),
8318 }
8319 }
8320
8321 #[test]
8325 fn test_add_workflow_missing_reason() {
8326 let err = pe(r#"ADD workflow "x" a -> b"#);
8327 assert!(matches!(err, CalError::MissingReason { .. }));
8328 }
8329
8330 #[test]
8332 fn test_add_workflow_missing_name() {
8333 let err = pe(r#"ADD workflow ON "trigger" a -> b REASON "y""#);
8334 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8335 }
8336
8337 #[test]
8339 fn test_add_workflow_empty_graph() {
8340 let err = pe(r#"ADD workflow "x" REASON "y""#);
8341 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8342 }
8343
8344 #[test]
8346 fn test_add_workflow_reserved_word_as_node() {
8347 let err = pe(r#"ADD workflow "x" ON -> WHEN REASON "y""#);
8353 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8354 }
8355
8356 #[test]
8358 fn test_add_workflow_repeat_zero() {
8359 let err = pe(r#"ADD workflow "x" a -> b * 0 REASON "y""#);
8360 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8361 }
8362
8363 #[test]
8367 fn test_add_workflow_repeat_negative() {
8368 let err = pe(r#"ADD workflow "x" a -> b * -1 REASON "y""#);
8369 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8370 }
8371
8372 #[test]
8374 fn test_add_workflow_dangling_arrow() {
8375 let err = pe(r#"ADD workflow "x" a -> REASON "y""#);
8376 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8379 }
8380
8381 #[test]
8383 fn test_add_workflow_double_arrow() {
8384 let err = pe(r#"ADD workflow "x" a -> -> b REASON "y""#);
8385 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8386 }
8387
8388 #[test]
8390 fn test_add_workflow_empty_parallel_group() {
8391 let err = pe(r#"ADD workflow "x" a -> () -> b REASON "y""#);
8392 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8393 }
8394
8395 #[test]
8397 fn test_add_workflow_unclosed_paren() {
8398 let err = pe(r#"ADD workflow "x" a -> (b, c REASON "y""#);
8399 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8401 }
8402
8403 #[test]
8405 fn test_add_workflow_bind_non_hash() {
8406 let err = pe(r#"ADD workflow "x" a -> b BIND a = "not_a_hash" REASON "y""#);
8407 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8408 }
8409
8410 #[test]
8412 fn test_add_workflow_when_without_string() {
8413 let err = pe(r#"ADD workflow "x" a -> b WHEN 42 REASON "y""#);
8414 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8415 }
8416
8417 #[test]
8421 fn test_add_workflow_arrow_at_start() {
8422 let err = pe(r#"ADD workflow "x" -> a -> b REASON "y""#);
8423 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8424 }
8425
8426 #[test]
8428 fn test_add_workflow_trailing_comma_in_group() {
8429 let err = pe(r#"ADD workflow "x" (a, b,) -> c REASON "y""#);
8430 assert!(matches!(err, CalError::UnexpectedToken { .. }));
8432 }
8433
8434 #[test]
8435 fn test_workflow_error_messages_quality() {
8436 let cases: Vec<(&str, &str, &str)> = vec![
8438 (r#"ADD workflow "x" a -> b"#, "missing_reason", "REASON"),
8439 (
8440 r#"ADD workflow ON "t" a -> b REASON "y""#,
8441 "missing_name",
8442 "string",
8443 ),
8444 (r#"ADD workflow "x" REASON "y""#, "empty_graph", "node"),
8445 (
8446 r#"ADD workflow "x" a -> b * 0 REASON "y""#,
8447 "repeat_zero",
8448 ">= 1",
8449 ),
8450 (
8451 r#"ADD workflow "x" a -> REASON "y""#,
8452 "dangling_arrow",
8453 "node name",
8454 ),
8455 (
8456 r#"ADD workflow "x" a -> -> b REASON "y""#,
8457 "double_arrow",
8458 "node name",
8459 ),
8460 (
8461 r#"ADD workflow "x" a -> () -> b REASON "y""#,
8462 "empty_parens",
8463 "node name",
8464 ),
8465 (
8466 r#"ADD workflow "x" a -> (b, c REASON "y""#,
8467 "unclosed_paren",
8468 ")",
8469 ),
8470 (
8471 r#"ADD workflow "x" a -> b WHEN 42 REASON "y""#,
8472 "when_no_string",
8473 "string",
8474 ),
8475 ];
8476 for (input, label, expected_substring) in &cases {
8477 let err = pe(input);
8478 let msg = format!("{}", err);
8479 assert!(
8480 msg.to_lowercase()
8481 .contains(&expected_substring.to_lowercase()),
8482 "Error for '{}' should mention '{}', got: {}",
8483 label,
8484 expected_substring,
8485 msg
8486 );
8487 }
8488 }
8489
8490 #[test]
8492 fn test_add_workflow_reserved_word_quoted() {
8493 let q = p(r#"ADD workflow "x" "ON" -> "WHEN" -> "BIND" REASON "quoted keywords""#);
8494 match &q.statement {
8495 CalStatement::AddWorkflow(wf) => {
8496 assert_eq!(wf.nodes, vec!["ON", "WHEN", "BIND"]);
8497 assert_eq!(wf.edges.len(), 2);
8498 }
8499 other => panic!("expected AddWorkflow, got {:?}", other),
8500 }
8501 }
8502
8503 #[test]
8505 fn test_add_workflow_repeat_fractional() {
8506 let q = p(r#"ADD workflow "x" a -> b * 2.5 REASON "y""#);
8507 match &q.statement {
8508 CalStatement::AddWorkflow(wf) => {
8509 assert_eq!(wf.edges[0].repeat, Some(2));
8511 }
8512 other => panic!("expected AddWorkflow, got {:?}", other),
8513 }
8514 }
8515
8516 #[test]
8518 fn test_add_workflow_deduplicates_nodes() {
8519 let q = p(r#"ADD workflow "x" a -> b -> a -> c REASON "cycle""#);
8520 match &q.statement {
8521 CalStatement::AddWorkflow(wf) => {
8522 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
8524 assert_eq!(wf.edges.len(), 3);
8525 assert_eq!(wf.edges[0].src, "a");
8526 assert_eq!(wf.edges[0].dst, "b");
8527 assert_eq!(wf.edges[1].src, "b");
8528 assert_eq!(wf.edges[1].dst, "a");
8529 assert_eq!(wf.edges[2].src, "a");
8530 assert_eq!(wf.edges[2].dst, "c");
8531 }
8532 other => panic!("expected AddWorkflow, got {:?}", other),
8533 }
8534 }
8535
8536 #[test]
8538 fn test_add_workflow_disconnected_subgraphs() {
8539 let q = p(r#"ADD workflow "x" a -> b c -> d REASON "disconnected""#);
8540 match &q.statement {
8541 CalStatement::AddWorkflow(wf) => {
8542 assert_eq!(wf.nodes, vec!["a", "b", "c", "d"]);
8543 assert_eq!(wf.edges.len(), 2);
8544 assert_eq!(wf.edges[0].src, "a");
8546 assert_eq!(wf.edges[0].dst, "b");
8547 assert_eq!(wf.edges[1].src, "c");
8548 assert_eq!(wf.edges[1].dst, "d");
8549 }
8550 other => panic!("expected AddWorkflow, got {:?}", other),
8551 }
8552 }
8553
8554 #[test]
8556 fn test_add_workflow_isolated_nodes_multiple_chains() {
8557 let q = p(r#"ADD workflow "x" a b c REASON "isolated""#);
8558 match &q.statement {
8559 CalStatement::AddWorkflow(wf) => {
8560 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
8561 assert!(wf.edges.is_empty());
8562 }
8563 other => panic!("expected AddWorkflow, got {:?}", other),
8564 }
8565 }
8566
8567 #[test]
8571 fn test_supersede_workflow_basic() {
8572 let q = p(r#"SUPERSEDE sha256:abc12345 a -> b REASON "updated""#);
8573 match &q.statement {
8574 CalStatement::SupersedeWorkflow(wf) => {
8575 assert_eq!(wf.hash, "abc12345");
8576 assert_eq!(wf.nodes, vec!["a", "b"]);
8577 assert_eq!(wf.edges.len(), 1);
8578 assert_eq!(wf.reason, "updated");
8579 }
8580 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8581 }
8582 }
8583
8584 #[test]
8586 fn test_supersede_workflow_no_trigger() {
8587 let q = p(r#"SUPERSEDE sha256:def45678 a -> b REASON "y""#);
8588 match &q.statement {
8589 CalStatement::SupersedeWorkflow(wf) => {
8590 assert_eq!(wf.hash, "def45678");
8591 assert_eq!(wf.nodes, vec!["a", "b"]);
8592 }
8593 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8594 }
8595 }
8596
8597 #[test]
8599 fn test_supersede_workflow_with_bindings() {
8600 let q = p(concat!(
8601 "SUPERSEDE sha256:abc12345 a -> b -> c ",
8602 "BIND a = sha256:def11111 ",
8603 "BIND b = sha256:def22222 ",
8604 r#"REASON "bound""#,
8605 ));
8606 match &q.statement {
8607 CalStatement::SupersedeWorkflow(wf) => {
8608 assert_eq!(wf.bindings.len(), 2);
8609 assert_eq!(wf.bindings[0].node, "a");
8610 assert_eq!(wf.bindings[0].hash, "def11111");
8611 assert_eq!(wf.bindings[1].node, "b");
8612 assert_eq!(wf.bindings[1].hash, "def22222");
8613 }
8614 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8615 }
8616 }
8617
8618 #[test]
8620 fn test_supersede_workflow_complex_graph() {
8621 let q = p(concat!(
8622 r#"SUPERSEDE sha256:aaaa1111 "#,
8623 r#"build -> (test, lint) -> deploy WHEN "pass" * 3 "#,
8624 r#"REASON "complex supersede""#,
8625 ));
8626 match &q.statement {
8627 CalStatement::SupersedeWorkflow(wf) => {
8628 assert_eq!(wf.nodes, vec!["build", "test", "lint", "deploy"]);
8629 assert_eq!(wf.edges.len(), 4);
8631 assert_eq!(wf.edges[2].cond, Some("pass".into()));
8632 assert_eq!(wf.edges[2].repeat, Some(3));
8633 assert_eq!(wf.edges[3].cond, Some("pass".into()));
8634 assert_eq!(wf.edges[3].repeat, Some(3));
8635 }
8636 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8637 }
8638 }
8639
8640 #[test]
8642 fn test_supersede_workflow_missing_reason() {
8643 let err = pe("SUPERSEDE sha256:abc12345 a -> b");
8644 assert!(matches!(err, CalError::MissingReason { .. }));
8645 }
8646
8647 #[test]
8651 fn test_add_workflow_in_batch() {
8652 let q = p("BATCH { ADD workflow \"x\" a -> b REASON \"y\" }");
8653 match &q.statement {
8654 CalStatement::Batch(b) => {
8655 assert_eq!(b.statements.len(), 1);
8656 assert!(matches!(
8657 &b.statements[0].statement,
8658 CalStatement::AddWorkflow(_)
8659 ));
8660 }
8661 other => panic!("expected Batch, got {:?}", other),
8662 }
8663 }
8664
8665 #[test]
8667 fn test_explain_add_workflow() {
8668 let q = p(r#"EXPLAIN ADD workflow "x" a -> b REASON "y""#);
8669 match &q.statement {
8670 CalStatement::Explain(e) => {
8671 assert!(matches!(*e.inner, CalStatement::AddWorkflow(_)));
8672 }
8673 other => panic!("expected Explain, got {:?}", other),
8674 }
8675 }
8676
8677 #[test]
8679 fn test_multiple_workflows_in_batch() {
8680 let q = p(concat!(
8681 "BATCH { ",
8682 r#"ADD workflow "first" a -> b REASON "r1" ; "#,
8683 r#"ADD workflow "second" c -> d REASON "r2" ; "#,
8684 "}",
8685 ));
8686 match &q.statement {
8687 CalStatement::Batch(b) => {
8688 assert_eq!(b.statements.len(), 2);
8689 match &b.statements[0].statement {
8690 CalStatement::AddWorkflow(wf) => assert_eq!(wf.name, "first"),
8691 other => panic!("expected AddWorkflow, got {:?}", other),
8692 }
8693 match &b.statements[1].statement {
8694 CalStatement::AddWorkflow(wf) => assert_eq!(wf.name, "second"),
8695 other => panic!("expected AddWorkflow, got {:?}", other),
8696 }
8697 }
8698 other => panic!("expected Batch, got {:?}", other),
8699 }
8700 }
8701
8702 #[test]
8704 fn test_supersede_workflow_in_batch() {
8705 let q = p(concat!(
8706 "BATCH { ",
8707 r#"SUPERSEDE sha256:abc12345 a -> b REASON "y" ; "#,
8708 "}",
8709 ));
8710 match &q.statement {
8711 CalStatement::Batch(b) => {
8712 assert_eq!(b.statements.len(), 1);
8713 assert!(matches!(
8714 &b.statements[0].statement,
8715 CalStatement::SupersedeWorkflow(_)
8716 ));
8717 }
8718 other => panic!("expected Batch, got {:?}", other),
8719 }
8720 }
8721
8722 #[test]
8724 fn test_explain_supersede_workflow() {
8725 let q = p(r#"EXPLAIN SUPERSEDE sha256:abc12345 a -> b REASON "y""#);
8726 match &q.statement {
8727 CalStatement::Explain(e) => {
8728 assert!(matches!(*e.inner, CalStatement::SupersedeWorkflow(_)));
8729 }
8730 other => panic!("expected Explain, got {:?}", other),
8731 }
8732 }
8733
8734 #[test]
8736 fn test_add_workflow_all_quoted_names() {
8737 let q =
8738 p(r#"ADD workflow "proc" "step one" -> "step two" -> "step three" REASON "quoted""#);
8739 match &q.statement {
8740 CalStatement::AddWorkflow(wf) => {
8741 assert_eq!(wf.nodes, vec!["step one", "step two", "step three"]);
8742 assert_eq!(wf.edges.len(), 2);
8743 }
8744 other => panic!("expected AddWorkflow, got {:?}", other),
8745 }
8746 }
8747
8748 #[test]
8750 fn test_add_workflow_parallel_all_quoted() {
8751 let q = p(
8752 r#"ADD workflow "x" start -> ("unit tests", "integration tests") -> deploy REASON "y""#,
8753 );
8754 match &q.statement {
8755 CalStatement::AddWorkflow(wf) => {
8756 assert_eq!(
8757 wf.nodes,
8758 vec!["start", "unit tests", "integration tests", "deploy"]
8759 );
8760 assert_eq!(wf.edges.len(), 4);
8761 }
8762 other => panic!("expected AddWorkflow, got {:?}", other),
8763 }
8764 }
8765
8766 #[test]
8768 fn test_add_workflow_three_parallel_groups() {
8769 let q = p(r#"ADD workflow "x" (a, b) -> (c, d) -> (e, f) REASON "y""#);
8770 match &q.statement {
8771 CalStatement::AddWorkflow(wf) => {
8772 assert_eq!(wf.nodes, vec!["a", "b", "c", "d", "e", "f"]);
8773 assert_eq!(wf.edges.len(), 8);
8775 }
8776 other => panic!("expected AddWorkflow, got {:?}", other),
8777 }
8778 }
8779
8780 #[test]
8783 fn test_add_workflow_when_on_parallel_to_single() {
8784 let q = p(r#"ADD workflow "x" (a, b) -> c WHEN "ok" REASON "y""#);
8785 match &q.statement {
8786 CalStatement::AddWorkflow(wf) => {
8787 assert_eq!(wf.edges.len(), 2);
8788 assert_eq!(wf.edges[0].cond, Some("ok".into()));
8789 assert_eq!(wf.edges[1].cond, Some("ok".into()));
8790 }
8791 other => panic!("expected AddWorkflow, got {:?}", other),
8792 }
8793 }
8794
8795 #[test]
8798 fn test_add_workflow_repeat_on_single_to_parallel() {
8799 let q = p(r#"ADD workflow "x" a -> (b, c) * 3 REASON "y""#);
8800 match &q.statement {
8801 CalStatement::AddWorkflow(wf) => {
8802 assert_eq!(wf.edges.len(), 2);
8803 assert_eq!(wf.edges[0].repeat, Some(3));
8804 assert_eq!(wf.edges[1].repeat, Some(3));
8805 }
8806 other => panic!("expected AddWorkflow, got {:?}", other),
8807 }
8808 }
8809
8810 #[test]
8813 fn test_add_workflow_bind_nonexistent_node() {
8814 let q = p(r#"ADD workflow "x" a -> b BIND nonexistent = sha256:abc12345 REASON "y""#);
8815 match &q.statement {
8816 CalStatement::AddWorkflow(wf) => {
8817 assert_eq!(wf.bindings.len(), 1);
8818 assert_eq!(wf.bindings[0].node, "nonexistent");
8819 }
8820 other => panic!("expected AddWorkflow, got {:?}", other),
8821 }
8822 }
8823
8824 #[test]
8827 fn supersede_workflow_refuses_the_removed_on_clause() {
8828 let e = parse(
8829 r#"SUPERSEDE sha256:1111111111111111111111111111111111111111111111111111111111111111 ON "x" a -> b REASON "y""#,
8830 )
8831 .expect_err("ON must not be accepted on a supersede either");
8832 assert!(!e.to_string().is_empty());
8833 }
8834
8835 #[test]
8837 fn test_add_workflow_unicode_name() {
8838 let q = p("ADD workflow \"\u{1F680} deploy\" a -> b REASON \"y\"");
8839 match &q.statement {
8840 CalStatement::AddWorkflow(wf) => {
8841 assert_eq!(wf.name, "\u{1F680} deploy");
8842 }
8843 other => panic!("expected AddWorkflow, got {:?}", other),
8844 }
8845 }
8846
8847 #[test]
8849 fn test_supersede_workflow_single_node() {
8850 let q = p(r#"SUPERSEDE sha256:abc12345 run REASON "minimal""#);
8851 match &q.statement {
8852 CalStatement::SupersedeWorkflow(wf) => {
8853 assert_eq!(wf.nodes, vec!["run"]);
8854 assert!(wf.edges.is_empty());
8855 }
8856 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8857 }
8858 }
8859
8860 #[test]
8862 fn test_supersede_workflow_because() {
8863 let q = p(r#"SUPERSEDE sha256:abc12345 a -> b BECAUSE "alt""#);
8864 match &q.statement {
8865 CalStatement::SupersedeWorkflow(wf) => {
8866 assert_eq!(wf.reason, "alt");
8867 }
8868 other => panic!("expected SupersedeWorkflow, got {:?}", other),
8869 }
8870 }
8871
8872 #[test]
8875 fn test_add_workflow_edge_order() {
8876 let q = p(concat!(
8877 r#"ADD workflow "x" "#,
8878 "a -> b -> c ",
8879 "d -> e ",
8880 r#"REASON "y""#,
8881 ));
8882 match &q.statement {
8883 CalStatement::AddWorkflow(wf) => {
8884 assert_eq!(wf.edges.len(), 3);
8885 assert_eq!(wf.edges[0].src, "a");
8887 assert_eq!(wf.edges[0].dst, "b");
8888 assert_eq!(wf.edges[1].src, "b");
8889 assert_eq!(wf.edges[1].dst, "c");
8890 assert_eq!(wf.edges[2].src, "d");
8892 assert_eq!(wf.edges[2].dst, "e");
8893 }
8894 other => panic!("expected AddWorkflow, got {:?}", other),
8895 }
8896 }
8897
8898 #[test]
8900 fn test_add_workflow_complex_ident_names() {
8901 let q = p(r#"ADD workflow "x" a1_b2_c3 -> x99_y00 REASON "y""#);
8902 match &q.statement {
8903 CalStatement::AddWorkflow(wf) => {
8904 assert_eq!(wf.nodes, vec!["a1_b2_c3", "x99_y00"]);
8905 }
8906 other => panic!("expected AddWorkflow, got {:?}", other),
8907 }
8908 }
8909
8910 #[test]
8912 fn test_add_workflow_parallel_group_only() {
8913 let q = p(r#"ADD workflow "x" (a, b, c) REASON "y""#);
8914 match &q.statement {
8915 CalStatement::AddWorkflow(wf) => {
8916 assert_eq!(wf.nodes, vec!["a", "b", "c"]);
8917 assert!(wf.edges.is_empty());
8918 }
8919 other => panic!("expected AddWorkflow, got {:?}", other),
8920 }
8921 }
8922
8923 #[test]
8928 fn test_add_workflow_when_on_non_edge_context() {
8929 let err = pe(r#"ADD workflow "x" a WHEN "cond" -> b REASON "y""#);
8930 let msg = format!("{}", err);
8931 assert!(
8932 msg.to_lowercase().contains("when") && msg.to_lowercase().contains("edge"),
8933 "expected error about WHEN placement, got: {}",
8934 msg
8935 );
8936 }
8937
8938 #[test]
8940 fn test_add_workflow_dangling_arrow_message() {
8941 let err = pe(r#"ADD workflow "x" a -> REASON "y""#);
8942 let msg = format!("{}", err);
8943 assert!(
8944 msg.to_lowercase().contains("node name"),
8945 "expected error mentioning 'node name', got: {}",
8946 msg
8947 );
8948 }
8949
8950 #[test]
8952 fn test_add_workflow_double_arrow_message() {
8953 let err = pe(r#"ADD workflow "x" a -> -> b REASON "y""#);
8954 let msg = format!("{}", err);
8955 assert!(
8956 msg.to_lowercase().contains("node name") && msg.to_lowercase().contains("->"),
8957 "expected error about node name after ->, got: {}",
8958 msg
8959 );
8960 }
8961
8962 #[test]
8964 fn test_add_workflow_empty_group_message() {
8965 let err = pe(r#"ADD workflow "x" a -> () -> b REASON "y""#);
8966 let msg = format!("{}", err);
8967 assert!(
8968 msg.to_lowercase().contains("node name"),
8969 "expected error about empty group needing a node, got: {}",
8970 msg
8971 );
8972 }
8973
8974 #[test]
8976 fn test_add_workflow_unclosed_paren_message() {
8977 let err = pe(r#"ADD workflow "x" a -> (b, c REASON "y""#);
8978 let msg = format!("{}", err);
8979 assert!(
8980 msg.contains(")") || msg.to_lowercase().contains("close"),
8981 "expected error mentioning ')' to close group, got: {}",
8982 msg
8983 );
8984 }
8985
8986 #[test]
8988 fn test_add_workflow_no_graph_message() {
8989 let err = pe(r#"ADD workflow "x" REASON "y""#);
8990 let msg = format!("{}", err);
8991 assert!(
8992 msg.to_lowercase().contains("node"),
8993 "expected error mentioning nodes needed, got: {}",
8994 msg
8995 );
8996 }
8997
8998 #[test]
9000 fn test_add_workflow_repeat_zero_message() {
9001 let err = pe(r#"ADD workflow "x" a -> b * 0 REASON "y""#);
9002 let msg = format!("{}", err);
9003 assert!(
9004 msg.contains(">= 1") || msg.to_lowercase().contains("at least"),
9005 "expected error about repeat count >= 1, got: {}",
9006 msg
9007 );
9008 }
9009
9010 #[test]
9012 fn test_add_workflow_repeat_non_number() {
9013 let err = pe(r#"ADD workflow "x" a -> b * abc REASON "y""#);
9014 let msg = format!("{}", err);
9015 assert!(
9016 msg.to_lowercase().contains("number"),
9017 "expected error about expecting number after *, got: {}",
9018 msg
9019 );
9020 }
9021
9022 #[test]
9025 fn test_on_as_field_name_in_where() {
9026 let q = p(r#"RECALL facts WHERE on = "something""#);
9028 match &q.statement {
9029 CalStatement::Recall(r) => {
9030 assert!(r.where_clause.is_some());
9031 }
9032 other => panic!("expected Recall, got {:?}", other),
9033 }
9034 }
9035
9036 #[test]
9037 fn test_when_as_field_name_in_where() {
9038 let q = p(r#"RECALL events WHERE when = "2025-01-01""#);
9039 match &q.statement {
9040 CalStatement::Recall(r) => {
9041 assert!(r.where_clause.is_some());
9042 }
9043 other => panic!("expected Recall, got {:?}", other),
9044 }
9045 }
9046
9047 #[test]
9048 fn test_bind_as_field_name_in_where() {
9049 let q = p(r#"RECALL facts WHERE bind = "test""#);
9050 match &q.statement {
9051 CalStatement::Recall(r) => {
9052 assert!(r.where_clause.is_some());
9053 }
9054 other => panic!("expected Recall, got {:?}", other),
9055 }
9056 }
9057
9058 #[test]
9059 fn test_on_as_field_name_in_add() {
9060 let q = p(r#"ADD fact SET on = "value" REASON "y""#);
9062 match &q.statement {
9063 CalStatement::Add(a) => {
9064 assert!(a.fields.iter().any(|f| f.field == "on"));
9065 }
9066 other => panic!("expected Add, got {:?}", other),
9067 }
9068 }
9069
9070 #[test]
9074 fn test_add_workflow_repeat_attaches_to_target() {
9075 let q = p(r#"ADD workflow "x" a -> b * 3 -> c REASON "y""#);
9076 match &q.statement {
9077 CalStatement::AddWorkflow(wf) => {
9078 assert_eq!(wf.edges.len(), 2);
9080 assert_eq!(wf.edges[0].src, "a");
9081 assert_eq!(wf.edges[0].dst, "b");
9082 assert_eq!(wf.edges[0].repeat, Some(3));
9083 assert_eq!(wf.edges[1].src, "b");
9084 assert_eq!(wf.edges[1].dst, "c");
9085 assert_eq!(wf.edges[1].repeat, None);
9086 }
9087 other => panic!("expected AddWorkflow, got {:?}", other),
9088 }
9089 }
9090
9091 #[test]
9094 fn test_supersede_set_not_confused_with_workflow() {
9095 let q = p(r#"SUPERSEDE sha256:abc12345 SET object = "new" REASON "updated""#);
9096 match &q.statement {
9097 CalStatement::Supersede(s) => {
9098 assert_eq!(s.hash, "abc12345");
9099 assert_eq!(s.set_clauses.len(), 1);
9100 assert_eq!(s.set_clauses[0].field, "object");
9101 assert_eq!(s.reason, "updated");
9102 }
9103 other => panic!(
9104 "expected Supersede (not SupersedeWorkflow), got {:?}",
9105 other
9106 ),
9107 }
9108 }
9109
9110 #[test]
9113 fn test_add_workflow_repeat_on_parallel_targets() {
9114 let q = p(r#"ADD workflow "x" a -> (b, c) * 2 REASON "y""#);
9115 match &q.statement {
9116 CalStatement::AddWorkflow(wf) => {
9117 assert_eq!(wf.edges.len(), 2);
9119 assert_eq!(wf.edges[0].repeat, Some(2));
9120 assert_eq!(wf.edges[1].repeat, Some(2));
9121 }
9122 other => panic!("expected AddWorkflow, got {:?}", other),
9123 }
9124 }
9125
9126 #[test]
9128 fn test_keyword_as_select_field() {
9129 let q = p(r#"RECALL facts WHERE subject = "x" | SELECT on, when, bind"#);
9130 match &q.pipeline.first() {
9131 Some(PipelineStage::Select { fields, .. }) => {
9132 assert_eq!(fields, &["on", "when", "bind"]);
9133 }
9134 other => panic!("expected Select pipeline stage, got {:?}", other),
9135 }
9136 }
9137
9138 #[test]
9139 fn test_priority_as_field_name_in_where() {
9140 let q = p(r#"RECALL goals WHERE priority = "high""#);
9141 match &q.statement {
9142 CalStatement::Recall(r) => {
9143 assert!(r.where_clause.is_some());
9144 }
9145 other => panic!("expected Recall, got {:?}", other),
9146 }
9147 }
9148
9149 #[test]
9150 fn test_scope_as_field_name_in_where() {
9151 let q = p(r#"RECALL consents WHERE scope = "read""#);
9152 match &q.statement {
9153 CalStatement::Recall(r) => {
9154 assert!(r.where_clause.is_some());
9155 }
9156 other => panic!("expected Recall, got {:?}", other),
9157 }
9158 }
9159
9160 #[test]
9161 fn test_priority_scope_as_select_fields() {
9162 let q = p(r#"RECALL goals WHERE subject = "x" | SELECT priority, scope"#);
9163 match &q.pipeline.first() {
9164 Some(PipelineStage::Select { fields, .. }) => {
9165 assert!(fields.contains(&"priority".to_string()));
9166 assert!(fields.contains(&"scope".to_string()));
9167 }
9168 other => panic!("expected Select pipeline stage, got {:?}", other),
9169 }
9170 }
9171
9172 #[test]
9173 fn test_multiple_with_clauses_merged() {
9174 let q = p("RECALL facts WITH superseded WITH score_breakdown WITH explanation");
9175 assert!(q.with_options.contains(&WithOption::Superseded));
9176 assert!(q.with_options.contains(&WithOption::ScoreBreakdown));
9177 assert!(q.with_options.contains(&WithOption::Explanation));
9178 assert_eq!(q.with_options.len(), 3);
9179 }
9180
9181 #[test]
9182 fn test_multiple_with_clauses_mixed_with_comma() {
9183 let q = p("RECALL facts WITH superseded, score_breakdown WITH explanation");
9184 assert!(q.with_options.contains(&WithOption::Superseded));
9185 assert!(q.with_options.contains(&WithOption::ScoreBreakdown));
9186 assert!(q.with_options.contains(&WithOption::Explanation));
9187 assert_eq!(q.with_options.len(), 3);
9188 }
9189
9190 #[test]
9193 fn test_supersede() {
9194 let q = p(
9195 r#"SUPERSEDE sha256:abc123def456 SET object = "light mode" REASON "changed preference""#,
9196 );
9197 match &q.statement {
9198 CalStatement::Supersede(s) => {
9199 assert_eq!(s.hash, "abc123def456");
9200 assert_eq!(s.reason, "changed preference");
9201 assert_eq!(s.set_clauses.len(), 1);
9202 }
9203 other => panic!("expected Supersede, got {:?}", other),
9204 }
9205 }
9206
9207 #[test]
9210 fn test_revert() {
9211 let q = p(r#"REVERT sha256:abc123def456 REASON "mistake""#);
9212 match &q.statement {
9213 CalStatement::Revert(r) => {
9214 assert_eq!(r.hash, "abc123def456");
9215 assert_eq!(r.reason, "mistake");
9216 }
9217 other => panic!("expected Revert, got {:?}", other),
9218 }
9219 }
9220
9221 #[test]
9224 fn test_let_binding() {
9225 let q = p("LET $users = RECALL facts SUBJECTS; RECALL events");
9226 assert_eq!(q.let_bindings.len(), 1);
9227 assert_eq!(q.let_bindings[0].name, "users");
9228 assert_eq!(q.let_bindings[0].extractor, Extractor::Subjects);
9229 }
9230
9231 #[test]
9232 fn test_let_binding_pipe_backward_compat() {
9233 let q = p("LET $users = RECALL facts | SUBJECTS; RECALL events");
9234 assert_eq!(q.let_bindings.len(), 1);
9235 assert_eq!(q.let_bindings[0].name, "users");
9236 assert_eq!(q.let_bindings[0].extractor, Extractor::Subjects);
9237 }
9238
9239 #[test]
9242 fn test_where_and() {
9243 let q = p(r#"RECALL facts WHERE subject = "john" AND confidence >= 0.8"#);
9244 match &q.statement {
9245 CalStatement::Recall(r) => {
9246 let cond = &r.where_clause.as_ref().unwrap().condition;
9247 assert!(matches!(cond, Condition::And { .. }));
9248 }
9249 _ => panic!("expected Recall"),
9250 }
9251 }
9252
9253 #[test]
9254 fn test_where_or() {
9255 let q = p(r#"RECALL facts WHERE subject = "john" OR subject = "bob""#);
9256 match &q.statement {
9257 CalStatement::Recall(r) => {
9258 let cond = &r.where_clause.as_ref().unwrap().condition;
9259 assert!(matches!(cond, Condition::Or { .. }));
9260 }
9261 _ => panic!("expected Recall"),
9262 }
9263 }
9264
9265 #[test]
9268 fn test_complex_nested() {
9269 let q = p(
9270 r#"CAL/1 RECALL facts ABOUT "preferences" WHERE confidence >= 0.8 ORDER BY confidence DESC LIMIT 5 WITH score_breakdown FORMAT json"#,
9271 );
9272 assert_eq!(q.version, CalVersion(1));
9273 assert!(matches!(q.statement, CalStatement::Recall(_)));
9274 assert_eq!(q.pipeline.len(), 2);
9275 assert!(q.with_options.contains(&WithOption::ScoreBreakdown));
9276 assert_eq!(q.format, Some(FormatClause::Single(FormatSpec::Json)));
9277 }
9278
9279 #[test]
9282 fn test_error_destructive_keyword() {
9283 let err = pe("ERASE facts WHERE subject = \"john\"");
9285 assert!(matches!(err, CalError::UnexpectedToken { .. }));
9286 assert!(err.suggestion().is_some());
9287 let sug = err.suggestion().unwrap();
9288 assert!(sug.contains("CAL cannot destroy data in bulk"));
9289 let msg = err.to_string();
9292 assert!(!msg.contains("PURGE"), "{msg}");
9293 assert!(!msg.contains("DELETE"), "{msg}");
9294 }
9295
9296 #[test]
9299 fn test_error_unknown_grain_type_beliefs() {
9300 let err = pe("RECALL beliefs");
9301 match err {
9302 CalError::UnknownGrainType {
9303 found, suggestion, ..
9304 } => {
9305 assert_eq!(found, "beliefs");
9306 assert!(suggestion.unwrap().contains("facts"));
9307 }
9308 other => panic!("expected UnknownGrainType, got {:?}", other),
9309 }
9310 }
9311
9312 #[test]
9315 fn test_version_prefix() {
9316 let q = p("CAL/1 RECALL facts");
9317 assert_eq!(q.version, CalVersion(1));
9318 }
9319
9320 #[test]
9321 fn test_unsupported_version() {
9322 let err = pe("CAL/2 RECALL facts");
9323 assert!(matches!(
9324 err,
9325 CalError::UnsupportedVersion { version: 2, .. }
9326 ));
9327 }
9328
9329 #[test]
9332 fn test_history() {
9333 let q = p("HISTORY sha256:abc123def456");
9334 match &q.statement {
9335 CalStatement::History(h) => {
9336 assert_eq!(h.hash, "abc123def456");
9337 }
9338 other => panic!("expected History, got {:?}", other),
9339 }
9340 }
9341
9342 #[test]
9345 fn test_where_in_list() {
9346 let q = p(r#"RECALL facts WHERE subject IN ("john", "bob")"#);
9347 match &q.statement {
9348 CalStatement::Recall(r) => {
9349 let cond = &r.where_clause.as_ref().unwrap().condition;
9350 match cond {
9351 Condition::In { field, values, .. } => {
9352 assert_eq!(field, "subject");
9353 assert_eq!(values.len(), 2);
9354 }
9355 other => panic!("expected In, got {:?}", other),
9356 }
9357 }
9358 _ => panic!("expected Recall"),
9359 }
9360 }
9361
9362 #[test]
9365 fn test_where_multiple_and() {
9366 let q = p(
9367 r#"RECALL facts WHERE subject = "john" AND confidence >= 0.8 AND tags INCLUDE ["preferences"]"#,
9368 );
9369 match &q.statement {
9370 CalStatement::Recall(r) => {
9371 let cond = &r.where_clause.as_ref().unwrap().condition;
9372 assert!(matches!(cond, Condition::And { .. }));
9374 }
9375 _ => panic!("expected Recall"),
9376 }
9377 }
9378
9379 #[test]
9382 fn test_error_empty_query() {
9383 let err = pe("");
9384 assert!(matches!(err, CalError::EmptyQuery { .. }));
9385 }
9386
9387 #[test]
9390 fn test_error_query_too_long() {
9391 let huge = "RECALL facts WHERE subject = \"".to_string() + &"a".repeat(66_000) + "\"";
9393 let err = pe(&huge);
9394 assert!(matches!(err, CalError::QueryTooLong { .. }));
9395 }
9396
9397 #[test]
9400 fn test_where_is_null() {
9401 let q = p("RECALL facts WHERE object IS NULL");
9402 match &q.statement {
9403 CalStatement::Recall(r) => {
9404 let cond = &r.where_clause.as_ref().unwrap().condition;
9405 assert!(matches!(cond, Condition::IsNull { field, .. } if field == "object"));
9406 }
9407 _ => panic!("expected Recall"),
9408 }
9409 }
9410
9411 #[test]
9412 fn test_where_is_not_null() {
9413 let q = p("RECALL facts WHERE object IS NOT NULL");
9414 match &q.statement {
9415 CalStatement::Recall(r) => {
9416 let cond = &r.where_clause.as_ref().unwrap().condition;
9417 assert!(matches!(cond, Condition::IsNotNull { field, .. } if field == "object"));
9418 }
9419 _ => panic!("expected Recall"),
9420 }
9421 }
9422
9423 #[test]
9426 fn test_format_template() {
9427 let q = p(r#"RECALL facts FORMAT TEMPLATE "{{subject}}: {{object}}""#);
9428 match &q.format {
9429 Some(FormatClause::Single(FormatSpec::Template { template })) => {
9430 assert!(template.contains("subject"));
9431 }
9432 other => panic!("expected Template format, got {:?}", other),
9433 }
9434 }
9435
9436 #[test]
9439 fn test_intersect() {
9440 let q = p(
9441 r#"(RECALL facts WHERE subject = "john") INTERSECT (RECALL facts WHERE confidence >= 0.9)"#,
9442 );
9443 match &q.statement {
9444 CalStatement::SetOp(s) => {
9445 assert_eq!(s.op, SetOp::Intersect);
9446 }
9447 other => panic!("expected SetOp, got {:?}", other),
9448 }
9449 }
9450
9451 #[test]
9454 fn test_except() {
9455 let q = p(
9456 r#"(RECALL facts WHERE subject = "john") EXCEPT (RECALL facts WHERE confidence < 0.5)"#,
9457 );
9458 match &q.statement {
9459 CalStatement::SetOp(s) => {
9460 assert_eq!(s.op, SetOp::Except);
9461 }
9462 other => panic!("expected SetOp, got {:?}", other),
9463 }
9464 }
9465
9466 #[test]
9469 fn test_where_not() {
9470 let q = p(r#"RECALL facts WHERE NOT subject = "john""#);
9471 match &q.statement {
9472 CalStatement::Recall(r) => {
9473 let cond = &r.where_clause.as_ref().unwrap().condition;
9474 assert!(matches!(cond, Condition::Not { .. }));
9475 }
9476 _ => panic!("expected Recall"),
9477 }
9478 }
9479
9480 #[test]
9483 fn test_between_clause() {
9484 let q = p(r#"RECALL events BETWEEN "2024-01-01" AND "2024-12-31""#);
9485 match &q.statement {
9486 CalStatement::Recall(r) => {
9487 let b = r.between.as_ref().unwrap();
9488 assert_eq!(b.start, "2024-01-01");
9489 assert_eq!(b.end, "2024-12-31");
9490 }
9491 _ => panic!("expected Recall"),
9492 }
9493 }
9494
9495 #[test]
9498 fn test_error_whitespace_only_input() {
9499 let err = pe(" \t\n ");
9500 assert!(matches!(err, CalError::EmptyQuery { .. }));
9501 }
9502
9503 #[test]
9506 fn test_error_all_destructive_keywords() {
9507 let keywords = [
9508 "DELETE", "DROP", "FORGET", "ERASE", "DESTROY", "PURGE", "TRUNCATE", "INSERT",
9509 "CREATE", "WRITE", "STORE",
9510 ];
9511 for kw in &keywords {
9512 let input = format!("{} facts WHERE subject = \"john\"", kw);
9513 let result = parse(&input);
9514 assert!(result.is_err(), "'{}' should produce a parse error", kw);
9515 let err = result.unwrap_err();
9516 assert!(
9517 err.suggestion().is_some(),
9518 "'{}' error should have a suggestion",
9519 kw
9520 );
9521 }
9522 }
9523
9524 #[test]
9527 fn test_where_or_produces_warning() {
9528 let q = p(r#"RECALL facts WHERE subject = "john" OR subject = "bob""#);
9529 assert!(matches!(q.statement, CalStatement::Recall(_)));
9532 }
9535
9536 #[test]
9539 fn test_query_well_under_max_length_succeeds() {
9540 let input = format!("RECALL facts ABOUT \"{}\"", "a".repeat(100));
9542 assert!(input.len() < MAX_QUERY_LENGTH);
9543 let result = parse(&input);
9544 assert!(result.is_ok(), "query under MAX_QUERY_LENGTH should parse");
9545 }
9546
9547 #[test]
9548 fn test_query_over_max_length_rejected() {
9549 let input = format!("RECALL facts ABOUT \"{}\"", "a".repeat(66_000));
9551 assert!(input.len() > MAX_QUERY_LENGTH);
9552 let result = parse(&input);
9553 assert!(result.is_err(), "query over MAX_QUERY_LENGTH should fail");
9554 assert!(matches!(result.unwrap_err(), CalError::QueryTooLong { .. }));
9555 }
9556
9557 #[test]
9558 fn test_max_query_length_constant_is_65536() {
9559 assert_eq!(MAX_QUERY_LENGTH, 65_536);
9560 }
9561
9562 #[test]
9568 fn test_nesting_depth_overflow_is_rejected() {
9569 let mut input = String::from("RECALL facts WHERE ");
9570 for _ in 0..20 {
9571 input.push('(');
9572 }
9573 input.push_str("confidence > 0.5");
9574 for _ in 0..20 {
9575 input.push(')');
9576 }
9577 let result = parse(&input);
9578 assert!(
9579 matches!(result, Err(CalError::NestingTooDeep { .. })),
9580 "deeply nested parens must return NestingTooDeep, got {:?}",
9581 result
9582 );
9583 }
9584
9585 #[test]
9586 fn test_nesting_depth_constant_matches_spec() {
9587 const _: () = assert!(
9590 MAX_NESTING_DEPTH == 8,
9591 "MAX_NESTING_DEPTH must remain 8 to stay in sync with json.rs"
9592 );
9593 }
9594
9595 #[test]
9596 fn test_query_32kb_parses_successfully() {
9597 let result = std::thread::Builder::new()
9601 .stack_size(16 * 1024 * 1024) .spawn(|| {
9603 let query_text = format!("RECALL facts ABOUT \"{}\"", "a".repeat(32_000));
9604 assert!(query_text.len() > 32_000);
9605 assert!(query_text.len() < MAX_QUERY_LENGTH);
9606 let result = parse(&query_text);
9607 assert!(result.is_ok(), "32KB query should parse successfully");
9608 })
9609 .expect("spawn thread")
9610 .join();
9611 result.expect("32KB parse thread panicked");
9612 }
9613
9614 #[test]
9617 fn test_all_grain_type_plurals_parse() {
9618 let types = [
9619 "facts",
9620 "events",
9621 "states",
9622 "workflows",
9623 "tools",
9624 "observations",
9625 "goals",
9626 "reasonings",
9627 "consensuses",
9628 "consents",
9629 ];
9630 for gt in &types {
9631 let input = format!("RECALL {}", gt);
9632 let result = parse(&input);
9633 assert!(
9634 result.is_ok(),
9635 "RECALL {} should parse successfully, got: {:?}",
9636 gt,
9637 result.unwrap_err()
9638 );
9639 }
9640 }
9641
9642 #[test]
9645 fn test_parser_case_insensitive_keywords() {
9646 let inputs = [
9648 "recall facts",
9649 "Recall Facts",
9650 "RECALL FACTS",
9651 "rEcAlL fAcTs",
9652 ];
9653 for input in &inputs {
9654 let result = parse(input);
9655 assert!(
9656 result.is_ok(),
9657 "'{}' should parse case-insensitively",
9658 input
9659 );
9660 }
9661 }
9662
9663 #[test]
9666 fn test_error_oms_1_1_renamed_types() {
9667 let err = pe("RECALL beliefs");
9669 match err {
9670 CalError::UnknownGrainType {
9671 found, suggestion, ..
9672 } => {
9673 assert_eq!(found, "beliefs");
9674 assert!(
9675 suggestion.as_ref().unwrap().contains("facts"),
9676 "suggestion for 'beliefs' should mention 'facts'"
9677 );
9678 }
9679 other => panic!("expected UnknownGrainType, got {:?}", other),
9680 }
9681 }
9682
9683 #[test]
9690 fn test_assemble_multi_source() {
9691 let q = p(
9692 r#"ASSEMBLE "context" FROM recent: (RECALL facts RECENT 5), background: (RECALL events RECENT 10)"#,
9693 );
9694 match &q.statement {
9695 CalStatement::Assemble(a) => {
9696 assert_eq!(a.topic, "context");
9697 assert_eq!(a.context_name.as_deref(), Some("context"));
9698 let sources = a.sources.as_ref().expect("should have named sources");
9699 assert_eq!(sources.len(), 2);
9700 assert_eq!(sources[0].label, "recent");
9701 assert_eq!(sources[1].label, "background");
9702 assert!(matches!(*sources[0].query, CalStatement::Recall(_)));
9703 assert!(matches!(*sources[1].query, CalStatement::Recall(_)));
9704 }
9705 other => panic!("expected Assemble, got {:?}", other),
9706 }
9707 }
9708
9709 fn assemble_sources(q: &CalQuery) -> Vec<NamedSource> {
9713 match &q.statement {
9714 CalStatement::Assemble(a) => a
9715 .sources
9716 .as_ref()
9717 .expect("should have named sources")
9718 .clone(),
9719 other => panic!("expected Assemble, got {:?}", other),
9720 }
9721 }
9722
9723 #[test]
9724 fn test_assemble_per_source_with_annotate_relative_time() {
9725 let q = p(
9726 r#"ASSEMBLE "ctx" FROM messages: (RECALL events RECENT 5 WITH annotate_relative_time)"#,
9727 );
9728 let sources = assemble_sources(&q);
9729 assert_eq!(sources.len(), 1);
9730 assert_eq!(sources[0].with_options.len(), 1);
9731 assert!(matches!(
9732 sources[0].with_options[0],
9733 WithOption::AnnotateRelativeTime
9734 ));
9735 }
9736
9737 #[test]
9738 fn test_assemble_per_source_with_rerank() {
9739 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH rerank)"#);
9740 let sources = assemble_sources(&q);
9741 assert!(matches!(
9742 sources[0].with_options.as_slice(),
9743 [WithOption::Rerank { .. }]
9744 ));
9745 }
9746
9747 #[test]
9748 fn test_assemble_per_source_with_conflict_resolution() {
9749 let q =
9750 p(r#"ASSEMBLE "ctx" FROM u: (RECALL facts ABOUT "alice" WITH conflict_resolution)"#);
9751 let sources = assemble_sources(&q);
9752 assert!(matches!(
9753 sources[0].with_options.as_slice(),
9754 [WithOption::ConflictResolution]
9755 ));
9756 }
9757
9758 #[test]
9759 fn test_assemble_per_source_with_dedup() {
9760 let q = p(r#"ASSEMBLE "ctx" FROM h: (RECALL tools RECENT 5 WITH dedup)"#);
9761 let sources = assemble_sources(&q);
9762 assert!(matches!(
9763 sources[0].with_options.as_slice(),
9764 [WithOption::Dedup { .. }]
9765 ));
9766 }
9767
9768 #[test]
9769 fn test_assemble_per_source_with_min_score() {
9770 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH min_score(0.5))"#);
9771 let sources = assemble_sources(&q);
9772 assert!(matches!(
9773 sources[0].with_options.as_slice(),
9774 [WithOption::MinScore { .. }]
9775 ));
9776 }
9777
9778 #[test]
9779 fn test_assemble_per_source_with_query_expansion() {
9780 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH query_expansion)"#);
9781 let sources = assemble_sources(&q);
9782 assert!(matches!(
9783 sources[0].with_options.as_slice(),
9784 [WithOption::QueryExpansion]
9785 ));
9786 }
9787
9788 #[test]
9789 fn test_assemble_per_source_with_recency_weight() {
9790 let q = p(r#"ASSEMBLE "ctx" FROM m: (RECALL events RECENT 5 WITH recency_weight(0.7))"#);
9791 let sources = assemble_sources(&q);
9792 assert!(matches!(
9793 sources[0].with_options.as_slice(),
9794 [WithOption::RecencyWeight { .. }]
9795 ));
9796 }
9797
9798 #[test]
9799 fn test_assemble_per_source_with_hyde() {
9800 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH hyde)"#);
9801 let sources = assemble_sources(&q);
9802 assert!(matches!(
9803 sources[0].with_options.as_slice(),
9804 [WithOption::Hyde]
9805 ));
9806 }
9807
9808 #[test]
9810 fn test_assemble_per_source_with_inside_and_outside_merge_order() {
9811 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH rerank) WITH dedup"#);
9812 let sources = assemble_sources(&q);
9813 assert_eq!(sources[0].with_options.len(), 2);
9814 assert!(matches!(
9816 sources[0].with_options[0],
9817 WithOption::Rerank { .. }
9818 ));
9819 assert!(matches!(
9820 sources[0].with_options[1],
9821 WithOption::Dedup { .. }
9822 ));
9823 }
9824
9825 #[test]
9828 fn test_assemble_per_source_with_on_set_op_source() {
9829 let q =
9830 p(r#"ASSEMBLE "ctx" FROM combined: (RECALL facts UNION RECALL events WITH rerank)"#);
9831 let sources = assemble_sources(&q);
9832 assert_eq!(sources.len(), 1);
9833 assert!(
9834 matches!(*sources[0].query, CalStatement::SetOp(_)),
9835 "set-op tail should be consumed into the query, got {:?}",
9836 sources[0].query
9837 );
9838 assert!(matches!(
9839 sources[0].with_options.as_slice(),
9840 [WithOption::Rerank { .. }]
9841 ));
9842 }
9843
9844 #[test]
9847 fn test_assemble_per_source_with_unknown_option_warns() {
9848 let q = p(r#"ASSEMBLE "ctx" FROM k: (RECALL facts LIMIT 10 WITH bogus_option)"#);
9849 let sources = assemble_sources(&q);
9850 assert!(
9851 sources[0].with_options.is_empty(),
9852 "unknown option must be skipped, not pushed"
9853 );
9854 assert!(
9855 q.warnings.iter().any(|w| w.code() == "CAL-W004"),
9856 "expected CAL-W004 warning, got {:?}",
9857 q.warnings
9858 );
9859 }
9860
9861 #[test]
9868 fn test_assemble_issue_609_ticket_context_example() {
9869 let q = p(r#"ASSEMBLE "ticket_context"
9870 FROM
9871 task: (RECALL goals ABOUT "session-1" WHERE status = "open" LIMIT 1),
9872 messages: (RECALL events ABOUT "alice" RECENT 5 WITH annotate_relative_time),
9873 knowledge: (RECALL facts WHERE tags INCLUDE ["product","support"] LIMIT 10 WITH rerank),
9874 user: (RECALL facts ABOUT "alice" WHERE relation IS PREFERENCE WITH conflict_resolution),
9875 history: (RECALL tools ABOUT "alice" RECENT 5 WITH dedup)
9876 BUDGET 2500 tokens
9877 WITH provenance
9878 FORMAT TEMPLATE "ticket_brief""#);
9879 let sources = assemble_sources(&q);
9880 assert_eq!(sources.len(), 5);
9881 assert_eq!(sources[0].label, "task");
9883 assert!(sources[0].with_options.is_empty());
9884 assert_eq!(sources[1].label, "messages");
9886 assert!(matches!(
9887 sources[1].with_options.as_slice(),
9888 [WithOption::AnnotateRelativeTime]
9889 ));
9890 assert_eq!(sources[2].label, "knowledge");
9892 assert!(matches!(
9893 sources[2].with_options.as_slice(),
9894 [WithOption::Rerank { .. }]
9895 ));
9896 assert_eq!(sources[3].label, "user");
9898 assert!(matches!(
9899 sources[3].with_options.as_slice(),
9900 [WithOption::ConflictResolution]
9901 ));
9902 assert_eq!(sources[4].label, "history");
9904 assert!(matches!(
9905 sources[4].with_options.as_slice(),
9906 [WithOption::Dedup { .. }]
9907 ));
9908 assert!(
9910 q.with_options
9911 .iter()
9912 .any(|w| matches!(w, WithOption::Provenance)),
9913 "top-level provenance should be in query with_options: {:?}",
9914 q.with_options
9915 );
9916 }
9917
9918 #[test]
9921 fn test_assemble_budget() {
9922 let q = p(r#"ASSEMBLE "summary" FROM (RECALL facts RECENT 10) BUDGET 2000"#);
9923 match &q.statement {
9924 CalStatement::Assemble(a) => {
9925 assert_eq!(a.topic, "summary");
9926 let budget = a.budget.as_ref().expect("should have budget");
9927 assert_eq!(budget.tokens, 2000);
9928 }
9929 other => panic!("expected Assemble, got {:?}", other),
9930 }
9931 }
9932
9933 #[test]
9936 fn test_assemble_priority() {
9937 let q = p(
9938 r#"ASSEMBLE "ctx" FROM recent: (RECALL facts RECENT 5), bg: (RECALL events RECENT 10) PRIORITY recent: 0.8, bg: 0.2"#,
9939 );
9940 match &q.statement {
9941 CalStatement::Assemble(a) => {
9942 let priority = a.priority.as_ref().expect("should have priority");
9943 assert_eq!(priority.len(), 2);
9944 assert_eq!(priority[0].label, "recent");
9945 assert!((priority[0].weight - 0.8).abs() < f64::EPSILON);
9946 assert_eq!(priority[1].label, "bg");
9947 assert!((priority[1].weight - 0.2).abs() < f64::EPSILON);
9948 }
9949 other => panic!("expected Assemble, got {:?}", other),
9950 }
9951 }
9952
9953 #[test]
9956 fn test_assemble_format() {
9957 let q = p(r#"ASSEMBLE "summary" FROM (RECALL facts RECENT 10) FORMAT markdown"#);
9958 match &q.statement {
9959 CalStatement::Assemble(a) => {
9960 assert_eq!(a.format, Some(FormatClause::Single(FormatSpec::Markdown)));
9961 }
9962 other => panic!("expected Assemble, got {:?}", other),
9963 }
9964 }
9965
9966 #[test]
9969 fn test_assemble_with_options() {
9970 let q = p(r#"ASSEMBLE "summary" FROM (RECALL facts RECENT 10) WITH dedup(subject)"#);
9971 match &q.statement {
9972 CalStatement::Assemble(a) => {
9973 assert_eq!(a.assemble_with.len(), 1);
9974 let AssembleWithOption::Dedup { field } = &a.assemble_with[0];
9975 assert_eq!(field.as_deref(), Some("subject"));
9976 }
9977 other => panic!("expected Assemble, got {:?}", other),
9978 }
9979 }
9980
9981 #[test]
9984 fn test_assemble_with_dedup_no_field() {
9985 let q = p(r#"ASSEMBLE "summary" FROM (RECALL facts RECENT 10) WITH dedup"#);
9986 match &q.statement {
9987 CalStatement::Assemble(a) => {
9988 assert_eq!(a.assemble_with.len(), 1);
9989 let AssembleWithOption::Dedup { field } = &a.assemble_with[0];
9990 assert_eq!(*field, None);
9991 }
9992 other => panic!("expected Assemble, got {:?}", other),
9993 }
9994 }
9995
9996 #[test]
9999 fn test_history_where() {
10000 let q = p(r#"HISTORY WHERE subject = "john" AND relation = "likes""#);
10001 match &q.statement {
10002 CalStatement::History(h) => {
10003 assert!(h.hash.is_empty(), "hash should be empty for WHERE-based");
10004 assert!(h.where_clause.is_some(), "should have where_clause");
10005 }
10006 other => panic!("expected History, got {:?}", other),
10007 }
10008 }
10009
10010 #[test]
10013 fn test_history_diff() {
10014 let q = p("HISTORY sha256:abc123def456 DIFF sha256:def789abc012");
10015 match &q.statement {
10016 CalStatement::History(h) => {
10017 assert_eq!(h.hash, "abc123def456");
10018 assert_eq!(h.diff_target.as_deref(), Some("def789abc012"));
10019 }
10020 other => panic!("expected History, got {:?}", other),
10021 }
10022 }
10023
10024 #[test]
10027 fn test_history_where_with_diff() {
10028 let q =
10029 p(r#"HISTORY WHERE subject = "john" AND relation = "likes" DIFF sha256:abc123def456"#);
10030 match &q.statement {
10031 CalStatement::History(h) => {
10032 assert!(h.hash.is_empty());
10033 assert!(h.where_clause.is_some());
10034 assert_eq!(h.diff_target.as_deref(), Some("abc123def456"));
10035 }
10036 other => panic!("expected History, got {:?}", other),
10037 }
10038 }
10039
10040 #[test]
10043 fn test_describe_capabilities() {
10044 let q = p("DESCRIBE capabilities");
10045 match &q.statement {
10046 CalStatement::Describe(d) => {
10047 assert!(matches!(d.target, DescribeTarget::Capabilities));
10048 }
10049 other => panic!("expected Describe, got {:?}", other),
10050 }
10051 }
10052
10053 #[test]
10056 fn test_describe_server() {
10057 let q = p("DESCRIBE server");
10058 match &q.statement {
10059 CalStatement::Describe(d) => {
10060 assert!(matches!(d.target, DescribeTarget::Server));
10061 }
10062 other => panic!("expected Describe, got {:?}", other),
10063 }
10064 }
10065
10066 #[test]
10069 fn test_describe_fields() {
10070 let q = p("DESCRIBE fields");
10071 match &q.statement {
10072 CalStatement::Describe(d) => match &d.target {
10073 DescribeTarget::Fields(gt) => {
10074 assert!(gt.is_none(), "bare FIELDS should have no grain type");
10075 }
10076 other => panic!("expected Fields, got {:?}", other),
10077 },
10078 other => panic!("expected Describe, got {:?}", other),
10079 }
10080 }
10081
10082 #[test]
10085 fn test_describe_fields_facts() {
10086 let q = p("DESCRIBE fields facts");
10087 match &q.statement {
10088 CalStatement::Describe(d) => match &d.target {
10089 DescribeTarget::Fields(gt) => {
10090 assert_eq!(*gt, Some(GrainTypePlural::Facts));
10091 }
10092 other => panic!("expected Fields, got {:?}", other),
10093 },
10094 other => panic!("expected Describe, got {:?}", other),
10095 }
10096 }
10097
10098 #[test]
10101 fn test_describe_templates() {
10102 let q = p("DESCRIBE templates");
10103 match &q.statement {
10104 CalStatement::Describe(d) => {
10105 assert!(matches!(d.target, DescribeTarget::Templates));
10106 }
10107 other => panic!("expected Describe, got {:?}", other),
10108 }
10109 }
10110
10111 #[test]
10114 fn test_describe_grammar() {
10115 let q = p("DESCRIBE grammar");
10116 match &q.statement {
10117 CalStatement::Describe(d) => {
10118 assert!(matches!(d.target, DescribeTarget::Grammar));
10119 }
10120 other => panic!("expected Describe, got {:?}", other),
10121 }
10122 }
10123
10124 #[test]
10127 fn test_coalesce_braces() {
10128 let q = p(
10129 r#"COALESCE { RECALL facts WHERE subject = "john" } OR { RECALL facts WHERE subject = "bob" }"#,
10130 );
10131 match &q.statement {
10132 CalStatement::Coalesce(c) => {
10133 assert_eq!(c.branches.len(), 2);
10134 assert!(matches!(c.branches[0].query, CalStatement::Recall(_)));
10135 assert!(matches!(c.branches[1].query, CalStatement::Recall(_)));
10136 assert!(c.else_branch.is_none());
10137 }
10138 other => panic!("expected Coalesce, got {:?}", other),
10139 }
10140 }
10141
10142 #[test]
10145 fn test_coalesce_braces_with_else() {
10146 let q = p(
10147 r#"COALESCE { RECALL facts WHERE subject = "john" } OR { RECALL facts WHERE subject = "bob" } ELSE { RECALL facts RECENT 5 }"#,
10148 );
10149 match &q.statement {
10150 CalStatement::Coalesce(c) => {
10151 assert_eq!(c.branches.len(), 2);
10152 assert!(c.else_branch.is_some());
10153 let else_stmt = c.else_branch.as_ref().unwrap();
10154 assert!(matches!(**else_stmt, CalStatement::Recall(_)));
10155 }
10156 other => panic!("expected Coalesce, got {:?}", other),
10157 }
10158 }
10159
10160 #[test]
10163 fn test_coalesce_phase1_stores_branches() {
10164 let q = p(
10165 r#"COALESCE(RECALL facts WHERE subject = "john", RECALL facts WHERE subject = "bob")"#,
10166 );
10167 match &q.statement {
10168 CalStatement::Coalesce(c) => {
10169 assert_eq!(
10170 c.branches.len(),
10171 2,
10172 "Phase 1 COALESCE should store branches"
10173 );
10174 assert!(matches!(c.branches[0].query, CalStatement::Recall(_)));
10175 assert!(matches!(c.branches[1].query, CalStatement::Recall(_)));
10176 assert!(c.else_branch.is_none());
10177 }
10178 other => panic!("expected Coalesce, got {:?}", other),
10179 }
10180 }
10181
10182 #[test]
10185 fn test_is_category_preference() {
10186 let q = p("RECALL facts WHERE relation IS PREFERENCE");
10187 match &q.statement {
10188 CalStatement::Recall(r) => {
10189 let cond = &r.where_clause.as_ref().unwrap().condition;
10190 match cond {
10191 Condition::IsCategory {
10192 field, category, ..
10193 } => {
10194 assert_eq!(field, "relation");
10195 assert_eq!(category, "preference");
10196 }
10197 other => panic!("expected IsCategory, got {:?}", other),
10198 }
10199 }
10200 _ => panic!("expected Recall"),
10201 }
10202 }
10203
10204 #[test]
10207 fn test_is_category_knowledge() {
10208 let q = p("RECALL facts WHERE relation IS KNOWLEDGE");
10209 match &q.statement {
10210 CalStatement::Recall(r) => {
10211 let cond = &r.where_clause.as_ref().unwrap().condition;
10212 match cond {
10213 Condition::IsCategory {
10214 field, category, ..
10215 } => {
10216 assert_eq!(field, "relation");
10217 assert_eq!(category, "knowledge");
10218 }
10219 other => panic!("expected IsCategory, got {:?}", other),
10220 }
10221 }
10222 _ => panic!("expected Recall"),
10223 }
10224 }
10225
10226 #[test]
10229 fn test_is_category_all_variants() {
10230 let categories = [
10231 "PREFERENCE",
10232 "KNOWLEDGE",
10233 "PERMISSION",
10234 "INTERACTION",
10235 "AGENCY",
10236 "LIFECYCLE",
10237 "OBSERVATION",
10238 ];
10239 for cat in &categories {
10240 let input = format!("RECALL facts WHERE relation IS {}", cat);
10241 let result = parse(&input);
10242 assert!(
10243 result.is_ok(),
10244 "IS {} should parse successfully, got: {:?}",
10245 cat,
10246 result.unwrap_err()
10247 );
10248 let q = result.unwrap();
10249 match &q.statement {
10250 CalStatement::Recall(r) => {
10251 let cond = &r.where_clause.as_ref().unwrap().condition;
10252 assert!(
10253 matches!(cond, Condition::IsCategory { .. }),
10254 "IS {} should produce IsCategory condition, got {:?}",
10255 cat,
10256 cond
10257 );
10258 }
10259 _ => panic!("expected Recall for IS {}", cat),
10260 }
10261 }
10262 }
10263
10264 #[test]
10267 fn test_assemble_single_source_still_works() {
10268 let q = p(r#"ASSEMBLE "daily_summary" FROM (RECALL facts RECENT 10)"#);
10270 match &q.statement {
10271 CalStatement::Assemble(a) => {
10272 assert_eq!(a.topic, "daily_summary");
10273 assert!(
10274 a.sources.is_none(),
10275 "single source should have no named sources"
10276 );
10277 assert!(a.budget.is_none());
10278 assert!(a.priority.is_none());
10279 assert!(a.format.is_none());
10280 assert!(a.assemble_with.is_empty());
10281 match &a.from {
10282 Source::Query(_) => {}
10283 other => panic!("expected Query source, got {:?}", other),
10284 }
10285 }
10286 other => panic!("expected Assemble, got {:?}", other),
10287 }
10288 }
10289
10290 #[test]
10293 fn test_assemble_full_phase2() {
10294 let q = p(
10295 r#"ASSEMBLE "brief" FROM recent: (RECALL facts RECENT 5), bg: (RECALL events RECENT 10) BUDGET 1500 PRIORITY recent: 0.7, bg: 0.3 FORMAT json WITH dedup"#,
10296 );
10297 match &q.statement {
10298 CalStatement::Assemble(a) => {
10299 assert_eq!(a.topic, "brief");
10300 assert_eq!(a.sources.as_ref().unwrap().len(), 2);
10301 assert_eq!(a.budget.as_ref().unwrap().tokens, 1500);
10302 assert_eq!(a.priority.as_ref().unwrap().len(), 2);
10303 assert_eq!(a.format, Some(FormatClause::Single(FormatSpec::Json)));
10304 assert_eq!(a.assemble_with.len(), 1);
10305 }
10306 other => panic!("expected Assemble, got {:?}", other),
10307 }
10308 }
10309
10310 #[test]
10316 fn test_assemble_with_rerank_not_stolen() {
10317 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts RECENT 10) WITH rerank FORMAT json"#);
10318 match &q.statement {
10319 CalStatement::Assemble(a) => {
10320 assert!(
10322 a.assemble_with.is_empty(),
10323 "rerank should not be in assemble_with"
10324 );
10325 }
10326 other => panic!("expected Assemble, got {:?}", other),
10327 }
10328 assert!(
10330 q.with_options
10331 .iter()
10332 .any(|w| matches!(w, WithOption::Rerank { .. })),
10333 "rerank should be in query with_options: {:?}",
10334 q.with_options
10335 );
10336 }
10337
10338 #[test]
10341 fn test_assemble_with_dedup_still_works() {
10342 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts RECENT 10) WITH dedup(subject)"#);
10343 match &q.statement {
10344 CalStatement::Assemble(a) => {
10345 assert_eq!(a.assemble_with.len(), 1);
10346 let AssembleWithOption::Dedup { field } = &a.assemble_with[0];
10347 assert_eq!(field.as_deref(), Some("subject"));
10348 }
10349 other => panic!("expected Assemble, got {:?}", other),
10350 }
10351 }
10352
10353 #[test]
10355 fn test_assemble_too_many_sources() {
10356 let input = r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL facts), c: (RECALL facts), d: (RECALL facts), e: (RECALL facts), f: (RECALL facts), g: (RECALL facts), h: (RECALL facts), i: (RECALL facts)"#;
10357 let err = pe(input);
10358 assert_eq!(err.code(), "CAL-E032");
10359 }
10360
10361 #[test]
10363 fn test_assemble_duplicate_labels() {
10364 let input = r#"ASSEMBLE "ctx" FROM a: (RECALL facts), a: (RECALL events)"#;
10365 let err = pe(input);
10366 assert_eq!(err.code(), "CAL-E034");
10367 }
10368
10369 #[test]
10371 fn test_assemble_priority_unknown_label() {
10372 let input =
10373 r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY typo: 0.5"#;
10374 let err = pe(input);
10375 assert_eq!(err.code(), "CAL-E035");
10376 }
10377
10378 #[test]
10380 fn test_assemble_budget_zero() {
10381 let input = r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 0"#;
10382 let err = pe(input);
10383 assert_eq!(err.code(), "CAL-E033");
10384 }
10385
10386 #[test]
10388 fn test_assemble_budget_exceeds_max() {
10389 let input = r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 20000"#;
10390 let err = pe(input);
10391 assert_eq!(err.code(), "CAL-E033");
10392 }
10393
10394 #[test]
10397 fn test_assemble_unquoted_topic_accepted() {
10398 let q = parse(r#"ASSEMBLE user_context FROM (RECALL facts)"#)
10399 .expect("bare identifier topic must parse");
10400 match q.statement {
10401 CalStatement::Assemble(a) => {
10402 assert_eq!(a.context_name.as_deref(), Some("user_context"));
10403 }
10404 other => panic!("expected Assemble, got {:?}", other),
10405 }
10406 let q2 = parse(r#"ASSEMBLE "quoted ctx" FROM (RECALL facts)"#)
10408 .expect("quoted topic must still parse");
10409 match q2.statement {
10410 CalStatement::Assemble(a) => {
10411 assert_eq!(a.context_name.as_deref(), Some("quoted ctx"));
10412 }
10413 other => panic!("expected Assemble, got {:?}", other),
10414 }
10415 }
10416
10417 #[test]
10419 fn test_assemble_priority_weight_too_high() {
10420 let input = r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY a: 1.5"#;
10421 let err = pe(input);
10422 assert_eq!(err.code(), "CAL-E002");
10423 assert!(err.to_string().contains("0.0 and 1.0") || err.to_string().contains("weight"));
10424 }
10425
10426 #[test]
10428 fn test_assemble_priority_weight_negative() {
10429 let input = r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY a: -0.3"#;
10430 let err = pe(input);
10433 assert!(err.to_string().contains("CAL-E"));
10435 }
10436
10437 #[test]
10439 fn test_assemble_for_clause_stored() {
10440 let q = p(r#"ASSEMBLE "ctx" FOR "john" FROM (RECALL facts)"#);
10441 match &q.statement {
10442 CalStatement::Assemble(a) => {
10443 assert_eq!(a.for_whom, Some("john".to_string()));
10444 }
10445 other => panic!("expected Assemble, got {:?}", other),
10446 }
10447 }
10448
10449 #[test]
10451 fn test_assemble_no_for_clause() {
10452 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts)"#);
10453 match &q.statement {
10454 CalStatement::Assemble(a) => {
10455 assert_eq!(a.for_whom, None);
10456 }
10457 other => panic!("expected Assemble, got {:?}", other),
10458 }
10459 }
10460
10461 #[test]
10463 fn test_assemble_where_multi_source_rejected() {
10464 let input =
10465 r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) WHERE confidence >= 0.8"#;
10466 let err = pe(input);
10467 assert_eq!(err.code(), "CAL-E002");
10468 let suggestion = err.suggestion().unwrap_or("");
10469 assert!(
10470 suggestion.contains("not supported with multi-source"),
10471 "suggestion should explain WHERE is not supported: {}",
10472 suggestion
10473 );
10474 }
10475
10476 #[test]
10478 fn test_assemble_where_single_source_still_works() {
10479 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts) WHERE confidence >= 0.8"#);
10480 match &q.statement {
10481 CalStatement::Assemble(a) => {
10482 assert!(a.where_clause.is_some(), "single-source WHERE should work");
10483 assert!(a.sources.is_none());
10484 }
10485 other => panic!("expected Assemble, got {:?}", other),
10486 }
10487 }
10488
10489 #[test]
10492 fn test_assemble_budget_with_tokens_unit() {
10493 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 2000 tokens"#);
10494 match &q.statement {
10495 CalStatement::Assemble(a) => {
10496 let budget = a.budget.as_ref().expect("should have budget");
10497 assert_eq!(budget.tokens, 2000);
10498 assert_eq!(budget.unit, BudgetUnit::Tokens);
10499 }
10500 other => panic!("expected Assemble, got {:?}", other),
10501 }
10502 }
10503
10504 #[test]
10505 fn test_assemble_budget_with_grains_unit() {
10506 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 50 grains"#);
10507 match &q.statement {
10508 CalStatement::Assemble(a) => {
10509 let budget = a.budget.as_ref().expect("should have budget");
10510 assert_eq!(budget.tokens, 50);
10511 assert_eq!(budget.unit, BudgetUnit::Grains);
10512 }
10513 other => panic!("expected Assemble, got {:?}", other),
10514 }
10515 }
10516
10517 #[test]
10518 fn test_assemble_budget_no_unit_defaults_to_tokens() {
10519 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 100"#);
10520 match &q.statement {
10521 CalStatement::Assemble(a) => {
10522 let budget = a.budget.as_ref().expect("should have budget");
10523 assert_eq!(budget.tokens, 100);
10524 assert_eq!(budget.unit, BudgetUnit::Tokens);
10525 }
10526 other => panic!("expected Assemble, got {:?}", other),
10527 }
10528 }
10529
10530 #[test]
10533 fn test_assemble_priority_ordering_syntax() {
10534 let q = p(
10535 r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events), c: (RECALL goals) PRIORITY a > b > c"#,
10536 );
10537 match &q.statement {
10538 CalStatement::Assemble(a) => {
10539 let priority = a.priority.as_ref().expect("should have priority");
10540 assert_eq!(priority.len(), 3);
10541 assert_eq!(priority[0].label, "a");
10542 assert!((priority[0].weight - 1.0).abs() < f64::EPSILON);
10543 assert_eq!(priority[1].label, "b");
10544 assert!((priority[1].weight - 2.0 / 3.0).abs() < 0.001);
10546 assert_eq!(priority[2].label, "c");
10547 assert!((priority[2].weight - 1.0 / 3.0).abs() < 0.001);
10549 }
10550 other => panic!("expected Assemble, got {:?}", other),
10551 }
10552 }
10553
10554 #[test]
10555 fn test_assemble_priority_weighted_syntax_still_works() {
10556 let q = p(
10557 r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY a: 0.7, b: 0.3"#,
10558 );
10559 match &q.statement {
10560 CalStatement::Assemble(a) => {
10561 let priority = a.priority.as_ref().expect("should have priority");
10562 assert_eq!(priority.len(), 2);
10563 assert_eq!(priority[0].label, "a");
10564 assert!((priority[0].weight - 0.7).abs() < f64::EPSILON);
10565 assert_eq!(priority[1].label, "b");
10566 assert!((priority[1].weight - 0.3).abs() < f64::EPSILON);
10567 }
10568 other => panic!("expected Assemble, got {:?}", other),
10569 }
10570 }
10571
10572 #[test]
10573 fn test_assemble_priority_ordering_two_labels() {
10574 let q = p(r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY a > b"#);
10575 match &q.statement {
10576 CalStatement::Assemble(a) => {
10577 let priority = a.priority.as_ref().expect("should have priority");
10578 assert_eq!(priority.len(), 2);
10579 assert_eq!(priority[0].label, "a");
10580 assert!((priority[0].weight - 1.0).abs() < f64::EPSILON);
10581 assert_eq!(priority[1].label, "b");
10582 assert!((priority[1].weight - 0.5).abs() < f64::EPSILON);
10583 }
10584 other => panic!("expected Assemble, got {:?}", other),
10585 }
10586 }
10587
10588 #[test]
10592 fn test_assemble_priority_single_label_no_budget_parses() {
10593 let q = p(
10594 r#"ASSEMBLE "ctx" FOR "topic" FROM s1: (RECALL facts RECENT 5) PRIORITY s1 FORMAT markdown"#,
10595 );
10596 match &q.statement {
10597 CalStatement::Assemble(a) => {
10598 let priority = a.priority.as_ref().expect("should have priority");
10599 assert_eq!(priority.len(), 1);
10600 assert_eq!(priority[0].label, "s1");
10601 assert!((priority[0].weight - 1.0).abs() < f64::EPSILON);
10602 assert!(a.format.is_some(), "FORMAT clause should also parse");
10603 }
10604 other => panic!("expected Assemble, got {:?}", other),
10605 }
10606 }
10607
10608 #[test]
10610 fn test_assemble_priority_ordering_then_format_no_budget() {
10611 let q = p(
10612 r#"ASSEMBLE "ctx" FROM a: (RECALL facts), b: (RECALL events) PRIORITY a > b FORMAT markdown"#,
10613 );
10614 match &q.statement {
10615 CalStatement::Assemble(a) => {
10616 let priority = a.priority.as_ref().expect("should have priority");
10617 assert_eq!(priority.len(), 2);
10618 assert_eq!(priority[0].label, "a");
10619 assert_eq!(priority[1].label, "b");
10620 assert!(a.format.is_some(), "FORMAT clause should parse");
10621 }
10622 other => panic!("expected Assemble, got {:?}", other),
10623 }
10624 }
10625
10626 #[test]
10631 fn test_where_tags_include_desugars_to_in() {
10632 let q = p(r#"RECALL facts WHERE tags INCLUDE ["strategy", "platform"]"#);
10633 match &q.statement {
10634 CalStatement::Recall(r) => {
10635 let cond = &r.where_clause.as_ref().unwrap().condition;
10636 match cond {
10637 Condition::In { field, values, .. } => {
10638 assert_eq!(field, "tags");
10639 assert_eq!(values.len(), 2);
10640 }
10641 other => panic!("expected Condition::In, got {:?}", other),
10642 }
10643 }
10644 _ => panic!("expected Recall"),
10645 }
10646 }
10647
10648 #[test]
10651 fn test_assemble_budget_u32_overflow() {
10652 let input = r#"ASSEMBLE "ctx" FROM (RECALL facts) BUDGET 5000000000"#;
10654 let err = pe(input);
10655 assert_eq!(err.code(), "CAL-E033");
10656 }
10657
10658 #[test]
10661 fn test_format_multi_two_formats() {
10662 let q = p("RECALL facts FORMAT [markdown, json]");
10663 assert_eq!(
10664 q.format,
10665 Some(FormatClause::Multi(vec![
10666 af(FormatSpec::Markdown),
10667 af(FormatSpec::Json)
10668 ]))
10669 );
10670 }
10671
10672 #[test]
10673 fn test_format_multi_single_element_list() {
10674 let q = p("RECALL facts FORMAT [json]");
10676 assert_eq!(
10677 q.format,
10678 Some(FormatClause::Multi(vec![af(FormatSpec::Json)]))
10679 );
10680 }
10681
10682 #[test]
10683 fn test_format_multi_all_types() {
10684 let q = p("RECALL facts FORMAT [json, markdown, yaml, text, sml]");
10685 assert_eq!(
10686 q.format,
10687 Some(FormatClause::Multi(vec![
10688 af(FormatSpec::Json),
10689 af(FormatSpec::Markdown),
10690 af(FormatSpec::Yaml),
10691 af(FormatSpec::Text),
10692 af(FormatSpec::Sml),
10693 ]))
10694 );
10695 }
10696
10697 #[test]
10698 fn test_format_multi_deduplicates() {
10699 let q = p("RECALL facts FORMAT [json, json, markdown]");
10700 assert_eq!(
10701 q.format,
10702 Some(FormatClause::Multi(vec![
10703 af(FormatSpec::Json),
10704 af(FormatSpec::Markdown)
10705 ]))
10706 );
10707 }
10708
10709 #[test]
10710 fn test_format_multi_empty_list_errors() {
10711 let result = crate::parser::parse("RECALL facts FORMAT []");
10712 assert!(result.is_err());
10713 let err = result.unwrap_err();
10714 assert!(err.to_string().contains("at least one format type"));
10715 }
10716
10717 #[test]
10718 fn test_format_multi_too_many_errors() {
10719 let result = crate::parser::parse(
10720 "RECALL facts FORMAT [json, markdown, yaml, text, sml, toon]",
10721 );
10722 assert!(result.is_err());
10723 let err = result.unwrap_err();
10724 assert_eq!(err.code(), "CAL-E110");
10725 }
10726
10727 #[test]
10728 fn test_format_single_unchanged() {
10729 let q = p("RECALL facts FORMAT markdown");
10731 assert_eq!(q.format, Some(FormatClause::Single(FormatSpec::Markdown)));
10732 }
10733
10734 #[test]
10735 fn test_assemble_format_multi() {
10736 let q = p(r#"ASSEMBLE "ctx" FROM (RECALL facts RECENT 5) FORMAT [json, toon]"#);
10737 match &q.statement {
10738 CalStatement::Assemble(a) => {
10739 assert_eq!(
10740 a.format,
10741 Some(FormatClause::Multi(vec![
10742 af(FormatSpec::Json),
10743 af(FormatSpec::Toon)
10744 ]))
10745 );
10746 }
10747 other => panic!("expected Assemble, got {:?}", other),
10748 }
10749 }
10750
10751 #[test]
10754 fn test_format_alias_single_alias() {
10755 let q = p("RECALL facts FORMAT [json AS customers]");
10756 assert_eq!(
10757 q.format,
10758 Some(FormatClause::Multi(vec![af_as(
10759 FormatSpec::Json,
10760 "customers"
10761 )]))
10762 );
10763 }
10764
10765 #[test]
10769 fn test_format_alias_may_be_a_reserved_word() {
10770 let q = p("RECALL facts FORMAT [json AS data, markdown AS readable]");
10771 assert_eq!(
10772 q.format,
10773 Some(FormatClause::Multi(vec![
10774 af_as(FormatSpec::Json, "data"),
10775 af_as(FormatSpec::Markdown, "readable"),
10776 ]))
10777 );
10778 let q = p("RECALL facts FORMAT [sml AS compact, json AS raw]");
10779 assert_eq!(
10780 q.format,
10781 Some(FormatClause::Multi(vec![
10782 af_as(FormatSpec::Sml, "compact"),
10783 af_as(FormatSpec::Json, "raw"),
10784 ]))
10785 );
10786 }
10787
10788 #[test]
10791 fn test_format_alias_rejects_destructive_words() {
10792 for word in ["delete", "erase", "truncate", "insert", "create"] {
10795 let q = format!("RECALL facts FORMAT [json AS {word}]");
10796 parse(&q).expect_err(&format!("{word} must not be usable as an alias"));
10797 }
10798 }
10799
10800 #[test]
10802 fn test_format_alias_reserved_word_duplicate_still_rejected() {
10803 let e = pe("RECALL facts FORMAT [json AS data, markdown AS data]");
10804 assert_eq!(e.code(), "CAL-E113", "expected duplicate-key error, got {e:?}");
10805 }
10806
10807 #[test]
10812 fn test_recall_star_is_all_grain_types() {
10813 let starred = p(r#"RECALL * WHERE namespace = "caller""#);
10814 let omitted = p(r#"RECALL WHERE namespace = "caller""#);
10815 match (&starred.statement, &omitted.statement) {
10816 (CalStatement::Recall(a), CalStatement::Recall(b)) => {
10817 assert_eq!(a.grain_type, GrainTypePlural::All);
10818 assert_eq!(a.grain_type, b.grain_type);
10819 }
10820 other => panic!("expected two Recall statements, got {other:?}"),
10821 }
10822 }
10823
10824 #[test]
10825 fn test_recall_star_through_a_pipeline() {
10826 let q = p(r#"RECALL * WHERE namespace = "caller" | COUNT"#);
10827 match &q.statement {
10828 CalStatement::Recall(r) => assert_eq!(r.grain_type, GrainTypePlural::All),
10829 other => panic!("expected Recall, got {other:?}"),
10830 }
10831 assert_eq!(q.pipeline.len(), 1);
10832 }
10833
10834 #[test]
10835 fn test_format_alias_mixed() {
10836 let q = p("RECALL facts FORMAT [json AS customers, markdown]");
10837 assert_eq!(
10838 q.format,
10839 Some(FormatClause::Multi(vec![
10840 af_as(FormatSpec::Json, "customers"),
10841 af(FormatSpec::Markdown),
10842 ]))
10843 );
10844 }
10845
10846 #[test]
10847 fn test_format_alias_template() {
10848 let q = p(
10849 r#"RECALL facts FORMAT [json AS customers, TEMPLATE "{{subject}}: {{object}}" AS summary]"#,
10850 );
10851 assert_eq!(
10852 q.format,
10853 Some(FormatClause::Multi(vec![
10854 af_as(FormatSpec::Json, "customers"),
10855 af_as(
10856 FormatSpec::Template {
10857 template: "{{subject}}: {{object}}".into()
10858 },
10859 "summary"
10860 ),
10861 ]))
10862 );
10863 }
10864
10865 #[test]
10866 fn test_format_alias_two_templates_different_aliases() {
10867 let q = p(
10868 r#"RECALL facts FORMAT [TEMPLATE "{{subject}}" AS names, TEMPLATE "{{object}}" AS values]"#,
10869 );
10870 assert_eq!(
10871 q.format,
10872 Some(FormatClause::Multi(vec![
10873 af_as(
10874 FormatSpec::Template {
10875 template: "{{subject}}".into()
10876 },
10877 "names"
10878 ),
10879 af_as(
10880 FormatSpec::Template {
10881 template: "{{object}}".into()
10882 },
10883 "values"
10884 ),
10885 ]))
10886 );
10887 }
10888
10889 #[test]
10890 fn test_format_alias_duplicate_alias_errors() {
10891 let err = pe("RECALL facts FORMAT [json AS customers, markdown AS customers]");
10892 assert_eq!(err.code(), "CAL-E113");
10893 }
10894
10895 #[test]
10896 fn test_format_alias_not_in_unbracketed() {
10897 let q = p("RECALL facts FORMAT json, markdown");
10900 assert_eq!(
10901 q.format,
10902 Some(FormatClause::Multi(vec![
10903 af(FormatSpec::Json),
10904 af(FormatSpec::Markdown)
10905 ]))
10906 );
10907 }
10908
10909 #[test]
10912 fn test_with_vars_basic() {
10913 let q = p(r#"RECALL facts WITH VARS { "name": "John", "theme": "dark" }"#);
10914 assert_eq!(q.user_vars.len(), 2);
10915 assert_eq!(q.user_vars.get("name").unwrap(), "John");
10916 assert_eq!(q.user_vars.get("theme").unwrap(), "dark");
10917 }
10918
10919 #[test]
10920 fn test_with_vars_empty() {
10921 let q = p(r#"RECALL facts WITH VARS { }"#);
10922 assert!(q.user_vars.is_empty());
10923 }
10924
10925 #[test]
10926 fn test_with_vars_single() {
10927 let q = p(r#"RECALL facts WITH VARS { "key": "value" }"#);
10928 assert_eq!(q.user_vars.len(), 1);
10929 assert_eq!(q.user_vars.get("key").unwrap(), "value");
10930 }
10931
10932 #[test]
10933 fn test_with_vars_trailing_comma() {
10934 let q = p(r#"RECALL facts WITH VARS { "a": "1", "b": "2", }"#);
10935 assert_eq!(q.user_vars.len(), 2);
10936 }
10937
10938 #[test]
10939 fn test_with_vars_after_format() {
10940 let q = p(r#"RECALL facts FORMAT json WITH VARS { "x": "y" }"#);
10941 assert!(q.format.is_some());
10942 assert_eq!(q.user_vars.get("x").unwrap(), "y");
10943 }
10944
10945 #[test]
10946 fn test_with_vars_after_with_options_and_format() {
10947 let q = p(r#"RECALL facts WITH superseded FORMAT json WITH VARS { "a": "b" }"#);
10948 assert!(q.with_options.contains(&WithOption::Superseded));
10949 assert!(q.format.is_some());
10950 assert_eq!(q.user_vars.get("a").unwrap(), "b");
10951 }
10952
10953 #[test]
10954 fn test_with_vars_without_format() {
10955 let q = p(r#"RECALL facts WITH VARS { "key": "val" }"#);
10956 assert!(q.format.is_none());
10957 assert_eq!(q.user_vars.get("key").unwrap(), "val");
10958 }
10959
10960 #[test]
10961 fn test_with_vars_too_many() {
10962 let input = format!(
10963 r#"RECALL facts WITH VARS {{ {} }}"#,
10964 (0..11)
10965 .map(|i| format!(r#""k{}": "v{}""#, i, i))
10966 .collect::<Vec<_>>()
10967 .join(", ")
10968 );
10969 let err = pe(&input);
10970 assert_eq!(err.code(), "CAL-E111");
10971 }
10972
10973 #[test]
10974 fn test_with_vars_value_too_large() {
10975 let big_val = "x".repeat(1025);
10976 let input = format!(r#"RECALL facts WITH VARS {{ "k": "{}" }}"#, big_val);
10977 let err = pe(&input);
10978 assert_eq!(err.code(), "CAL-E112");
10979 }
10980
10981 #[test]
10982 fn test_with_vars_invalid_key_starts_with_digit() {
10983 let err = pe(r#"RECALL facts WITH VARS { "1bad": "val" }"#);
10984 assert_eq!(err.code(), "CAL-E002"); }
10986
10987 #[test]
10988 fn test_with_vars_coexists_with_pipeline() {
10989 let q = p(r#"RECALL facts | LIMIT 5 WITH VARS { "x": "1" }"#);
10990 assert!(!q.pipeline.is_empty());
10991 assert_eq!(q.user_vars.get("x").unwrap(), "1");
10992 }
10993
10994 #[test]
10995 fn test_with_vars_exactly_at_limit() {
10996 let input = format!(
10997 r#"RECALL facts WITH VARS {{ {} }}"#,
10998 (0..10)
10999 .map(|i| format!(r#""k{}": "v{}""#, i, i))
11000 .collect::<Vec<_>>()
11001 .join(", ")
11002 );
11003 let q = p(&input);
11004 assert_eq!(q.user_vars.len(), 10, "exactly 10 vars should be allowed");
11005 }
11006
11007 #[test]
11008 fn test_with_vars_value_exactly_at_size_limit() {
11009 let val = "x".repeat(1024);
11010 let input = format!(r#"RECALL facts WITH VARS {{ "k": "{}" }}"#, val);
11011 let q = p(&input);
11012 assert_eq!(q.user_vars.get("k").unwrap().len(), 1024);
11013 }
11014
11015 #[test]
11016 fn test_with_vars_underscore_prefixed_key() {
11017 let q = p(r#"RECALL facts WITH VARS { "_private": "value" }"#);
11018 assert_eq!(q.user_vars.get("_private").unwrap(), "value");
11019 }
11020
11021 #[test]
11022 fn test_with_vars_empty_value() {
11023 let q = p(r#"RECALL facts WITH VARS { "key": "" }"#);
11024 assert_eq!(q.user_vars.get("key").unwrap(), "");
11025 }
11026
11027 #[test]
11028 fn test_with_vars_duplicate_key_last_wins() {
11029 let q = p(r#"RECALL facts WITH VARS { "k": "first", "k": "second" }"#);
11030 assert_eq!(q.user_vars.len(), 1);
11031 assert_eq!(q.user_vars.get("k").unwrap(), "second");
11032 }
11033
11034 #[test]
11035 fn test_with_vars_invalid_key_with_hyphen() {
11036 let err = pe(r#"RECALL facts WITH VARS { "my-key": "val" }"#);
11037 assert_eq!(err.code(), "CAL-E002");
11038 }
11039
11040 #[test]
11041 fn test_with_vars_invalid_key_empty_string() {
11042 let err = pe(r#"RECALL facts WITH VARS { "": "val" }"#);
11043 assert_eq!(err.code(), "CAL-E002");
11044 }
11045
11046 #[test]
11049 fn test_accumulate_tip_resolved_basic() {
11050 let q = p(
11051 r#"ACCUMULATE fact WHERE subject = "john" relation = "score" ADD importance = 0.5 REASON "bump""#,
11052 );
11053 match &q.statement {
11054 CalStatement::Accumulate(acc) => {
11055 assert_eq!(acc.grain_type, GrainTypeSingular::Fact);
11056 match &acc.target {
11057 AccumulateTarget::TipResolved {
11058 subject,
11059 relation,
11060 namespace,
11061 } => {
11062 assert_eq!(subject, "john");
11063 assert_eq!(relation, "score");
11064 assert!(namespace.is_none());
11065 }
11066 other => panic!("expected TipResolved, got {:?}", other),
11067 }
11068 assert_eq!(acc.add_ops.len(), 1);
11069 assert_eq!(acc.add_ops[0].field, "importance");
11070 assert!((acc.add_ops[0].delta - 0.5).abs() < f64::EPSILON);
11071 assert!(acc.set_ops.is_empty());
11072 assert_eq!(acc.reason, "bump");
11073 }
11074 other => panic!("expected Accumulate, got {:?}", other),
11075 }
11076 }
11077
11078 #[test]
11079 fn test_accumulate_tip_resolved_with_namespace() {
11080 let q = p(
11081 r#"ACCUMULATE fact WHERE subject = "john" relation = "score" namespace = "team1" ADD confidence = 0.1 REASON "ns test""#,
11082 );
11083 match &q.statement {
11084 CalStatement::Accumulate(acc) => match &acc.target {
11085 AccumulateTarget::TipResolved {
11086 subject,
11087 relation,
11088 namespace,
11089 } => {
11090 assert_eq!(subject, "john");
11091 assert_eq!(relation, "score");
11092 assert_eq!(namespace.as_deref(), Some("team1"));
11093 }
11094 other => panic!("expected TipResolved, got {:?}", other),
11095 },
11096 other => panic!("expected Accumulate, got {:?}", other),
11097 }
11098 }
11099
11100 #[test]
11101 fn test_accumulate_hash_targeted() {
11102 let q = p(
11103 r#"ACCUMULATE fact sha256:0000000000000000000000000000000000000000000000000000000000000001 ADD importance = 0.25 REASON "hash test""#,
11104 );
11105 match &q.statement {
11106 CalStatement::Accumulate(acc) => {
11107 assert_eq!(acc.grain_type, GrainTypeSingular::Fact);
11108 match &acc.target {
11109 AccumulateTarget::Hash { hash } => {
11110 assert!(hash.contains(
11111 "0000000000000000000000000000000000000000000000000000000000000001"
11112 ));
11113 }
11114 other => panic!("expected Hash target, got {:?}", other),
11115 }
11116 assert_eq!(acc.add_ops.len(), 1);
11117 assert_eq!(acc.reason, "hash test");
11118 }
11119 other => panic!("expected Accumulate, got {:?}", other),
11120 }
11121 }
11122
11123 #[test]
11124 fn test_accumulate_multiple_add_ops() {
11125 let q = p(
11126 r#"ACCUMULATE fact WHERE subject = "x" relation = "y" ADD importance = 0.1 ADD confidence = 0.2 REASON "multi""#,
11127 );
11128 match &q.statement {
11129 CalStatement::Accumulate(acc) => {
11130 assert_eq!(acc.add_ops.len(), 2);
11131 assert_eq!(acc.add_ops[0].field, "importance");
11132 assert!((acc.add_ops[0].delta - 0.1).abs() < f64::EPSILON);
11133 assert_eq!(acc.add_ops[1].field, "confidence");
11134 assert!((acc.add_ops[1].delta - 0.2).abs() < f64::EPSILON);
11135 }
11136 other => panic!("expected Accumulate, got {:?}", other),
11137 }
11138 }
11139
11140 #[test]
11141 fn test_accumulate_with_set_ops() {
11142 let q = p(
11143 r#"ACCUMULATE fact WHERE subject = "x" relation = "y" ADD importance = 0.1 SET object = "new val" REASON "set test""#,
11144 );
11145 match &q.statement {
11146 CalStatement::Accumulate(acc) => {
11147 assert_eq!(acc.add_ops.len(), 1);
11148 assert_eq!(acc.set_ops.len(), 1);
11149 assert_eq!(acc.set_ops[0].field, "object");
11150 assert_eq!(
11151 acc.set_ops[0].value,
11152 Value::String {
11153 value: "new val".into()
11154 }
11155 );
11156 }
11157 other => panic!("expected Accumulate, got {:?}", other),
11158 }
11159 }
11160
11161 #[test]
11162 fn test_accumulate_because_alias() {
11163 let q = p(
11164 r#"ACCUMULATE fact WHERE subject = "x" relation = "y" ADD importance = 1 BECAUSE "alias""#,
11165 );
11166 match &q.statement {
11167 CalStatement::Accumulate(acc) => {
11168 assert_eq!(acc.reason, "alias");
11169 }
11170 other => panic!("expected Accumulate, got {:?}", other),
11171 }
11172 }
11173
11174 #[test]
11175 fn test_accumulate_negative_delta() {
11176 let q = p(
11177 r#"ACCUMULATE fact WHERE subject = "x" relation = "y" ADD importance = -0.3 REASON "decay""#,
11178 );
11179 match &q.statement {
11180 CalStatement::Accumulate(acc) => {
11181 assert_eq!(acc.add_ops.len(), 1);
11182 assert!((acc.add_ops[0].delta - (-0.3)).abs() < f64::EPSILON);
11183 }
11184 other => panic!("expected Accumulate, got {:?}", other),
11185 }
11186 }
11187
11188 #[test]
11189 fn test_accumulate_missing_add_ops_error() {
11190 let err = pe(
11191 r#"ACCUMULATE fact WHERE subject = "x" relation = "y" SET foo = "bar" REASON "no adds""#,
11192 );
11193 assert_eq!(err.code(), "CAL-E080");
11194 }
11195
11196 #[test]
11197 fn test_accumulate_missing_reason_error() {
11198 let err = pe(r#"ACCUMULATE fact WHERE subject = "x" relation = "y" ADD importance = 0.1"#);
11199 assert_eq!(err.code(), "CAL-E018");
11200 }
11201
11202 #[test]
11203 fn test_accumulate_missing_target_error() {
11204 let err = pe(r#"ACCUMULATE fact ADD importance = 0.1 REASON "no target""#);
11205 assert_eq!(err.code(), "CAL-E002");
11206 }
11207
11208 #[test]
11209 fn test_accumulate_case_insensitive() {
11210 let q = p(
11211 r#"accumulate fact WHERE subject = "x" relation = "y" ADD importance = 1 REASON "ci""#,
11212 );
11213 assert!(matches!(&q.statement, CalStatement::Accumulate(_)));
11214 }
11215}