1use crate::ast::*;
13use crate::tokens::{is_declaration_keyword, Token, TokenType, Trivia, TriviaKind};
14
15const fn is_comment_token(tt: &TokenType) -> bool {
21 matches!(
22 tt,
23 TokenType::LineComment
24 | TokenType::BlockComment
25 | TokenType::DocLineComment
26 | TokenType::DocBlockComment
27 | TokenType::InnerDocLineComment
28 | TokenType::InnerDocBlockComment
29 )
30}
31
32const fn token_to_trivia_kind(tt: &TokenType) -> Option<TriviaKind> {
33 match tt {
34 TokenType::LineComment => Some(TriviaKind::Line),
35 TokenType::BlockComment => Some(TriviaKind::Block),
36 TokenType::DocLineComment => Some(TriviaKind::DocLine),
37 TokenType::DocBlockComment => Some(TriviaKind::DocBlock),
38 TokenType::InnerDocLineComment => Some(TriviaKind::InnerDocLine),
39 TokenType::InnerDocBlockComment => Some(TriviaKind::InnerDocBlock),
40 _ => None,
41 }
42}
43
44fn attach_trivia_to_decl(decl: &mut Declaration, leading: Vec<Trivia>, trailing: Vec<Trivia>) {
52 match decl {
53 Declaration::Budget(n) => {
56 n.leading_trivia = leading;
57 n.trailing_trivia = trailing;
58 }
59 Declaration::Import(n) => {
60 n.leading_trivia = leading;
61 n.trailing_trivia = trailing;
62 }
63 Declaration::Persona(n) => {
64 n.leading_trivia = leading;
65 n.trailing_trivia = trailing;
66 }
67 Declaration::Context(n) => {
68 n.leading_trivia = leading;
69 n.trailing_trivia = trailing;
70 }
71 Declaration::Anchor(n) => {
72 n.leading_trivia = leading;
73 n.trailing_trivia = trailing;
74 }
75 Declaration::Memory(n) => {
76 n.leading_trivia = leading;
77 n.trailing_trivia = trailing;
78 }
79 Declaration::Effect(n) => {
81 n.leading_trivia = leading;
82 n.trailing_trivia = trailing;
83 }
84 Declaration::Tool(n) => {
85 n.leading_trivia = leading;
86 n.trailing_trivia = trailing;
87 }
88 Declaration::Type(n) => {
89 n.leading_trivia = leading;
90 n.trailing_trivia = trailing;
91 }
92 Declaration::Flow(n) => {
93 n.leading_trivia = leading;
94 n.trailing_trivia = trailing;
95 }
96 Declaration::Intent(n) => {
97 n.leading_trivia = leading;
98 n.trailing_trivia = trailing;
99 }
100 Declaration::Run(n) => {
101 n.leading_trivia = leading;
102 n.trailing_trivia = trailing;
103 }
104 Declaration::Epistemic(n) => {
105 n.leading_trivia = leading;
106 n.trailing_trivia = trailing;
107 }
108 Declaration::Let(n) => {
109 n.leading_trivia = leading;
110 n.trailing_trivia = trailing;
111 }
112 Declaration::LambdaData(n) => {
113 n.leading_trivia = leading;
114 n.trailing_trivia = trailing;
115 }
116 Declaration::Agent(n) => {
117 n.leading_trivia = leading;
118 n.trailing_trivia = trailing;
119 }
120 Declaration::Shield(n) => {
121 n.leading_trivia = leading;
122 n.trailing_trivia = trailing;
123 }
124 Declaration::Window(n) => {
125 n.leading_trivia = leading;
126 n.trailing_trivia = trailing;
127 }
128 Declaration::Pix(n) => {
129 n.leading_trivia = leading;
130 n.trailing_trivia = trailing;
131 }
132 Declaration::Ledger(n) => {
133 n.leading_trivia = leading;
134 n.trailing_trivia = trailing;
135 }
136 Declaration::Psyche(n) => {
137 n.leading_trivia = leading;
138 n.trailing_trivia = trailing;
139 }
140 Declaration::Corpus(n) => {
141 n.leading_trivia = leading;
142 n.trailing_trivia = trailing;
143 }
144 Declaration::Dataspace(n) => {
145 n.leading_trivia = leading;
146 n.trailing_trivia = trailing;
147 }
148 Declaration::Ots(n) => {
149 n.leading_trivia = leading;
150 n.trailing_trivia = trailing;
151 }
152 Declaration::Mandate(n) => {
153 n.leading_trivia = leading;
154 n.trailing_trivia = trailing;
155 }
156 Declaration::Compute(n) => {
157 n.leading_trivia = leading;
158 n.trailing_trivia = trailing;
159 }
160 Declaration::Daemon(n) => {
161 n.leading_trivia = leading;
162 n.trailing_trivia = trailing;
163 }
164 Declaration::Extension(n) => {
165 n.leading_trivia = leading;
166 n.trailing_trivia = trailing;
167 }
168 Declaration::AxonStore(n) => {
169 n.leading_trivia = leading;
170 n.trailing_trivia = trailing;
171 }
172 Declaration::AxonEndpoint(n) => {
173 n.leading_trivia = leading;
174 n.trailing_trivia = trailing;
175 }
176 Declaration::Resource(n) => {
177 n.leading_trivia = leading;
178 n.trailing_trivia = trailing;
179 }
180 Declaration::Fabric(n) => {
181 n.leading_trivia = leading;
182 n.trailing_trivia = trailing;
183 }
184 Declaration::Manifest(n) => {
185 n.leading_trivia = leading;
186 n.trailing_trivia = trailing;
187 }
188 Declaration::Observe(n) => {
189 n.leading_trivia = leading;
190 n.trailing_trivia = trailing;
191 }
192 Declaration::Reconcile(n) => {
193 n.leading_trivia = leading;
194 n.trailing_trivia = trailing;
195 }
196 Declaration::Lease(n) => {
197 n.leading_trivia = leading;
198 n.trailing_trivia = trailing;
199 }
200 Declaration::Ensemble(n) => {
201 n.leading_trivia = leading;
202 n.trailing_trivia = trailing;
203 }
204 Declaration::Session(n) => {
205 n.leading_trivia = leading;
206 n.trailing_trivia = trailing;
207 }
208 Declaration::Topology(n) => {
209 n.leading_trivia = leading;
210 n.trailing_trivia = trailing;
211 }
212 Declaration::Immune(n) => {
213 n.leading_trivia = leading;
214 n.trailing_trivia = trailing;
215 }
216 Declaration::Reflex(n) => {
217 n.leading_trivia = leading;
218 n.trailing_trivia = trailing;
219 }
220 Declaration::Heal(n) => {
221 n.leading_trivia = leading;
222 n.trailing_trivia = trailing;
223 }
224 Declaration::Component(n) => {
225 n.leading_trivia = leading;
226 n.trailing_trivia = trailing;
227 }
228 Declaration::View(n) => {
229 n.leading_trivia = leading;
230 n.trailing_trivia = trailing;
231 }
232 Declaration::Channel(n) => {
233 n.leading_trivia = leading;
234 n.trailing_trivia = trailing;
235 }
236 Declaration::Socket(n) => {
237 n.leading_trivia = leading;
238 n.trailing_trivia = trailing;
239 }
240 Declaration::Upstream(n) => {
241 n.leading_trivia = leading;
242 n.trailing_trivia = trailing;
243 }
244 Declaration::Voice(n) => {
245 n.leading_trivia = leading;
246 n.trailing_trivia = trailing;
247 }
248 Declaration::Cors(n) => {
249 n.leading_trivia = leading;
250 n.trailing_trivia = trailing;
251 }
252 Declaration::Credential(n) => {
253 n.leading_trivia = leading;
254 n.trailing_trivia = trailing;
255 }
256 Declaration::Cache(n) => {
257 n.leading_trivia = leading;
258 n.trailing_trivia = trailing;
259 }
260 Declaration::Savant(n) => {
261 n.leading_trivia = leading;
262 n.trailing_trivia = trailing;
263 }
264 Declaration::Synth(n) => {
265 n.leading_trivia = leading;
266 n.trailing_trivia = trailing;
267 }
268 Declaration::Scope(n) => {
269 n.leading_trivia = leading;
270 n.trailing_trivia = trailing;
271 }
272 Declaration::Observable(n) => {
273 n.leading_trivia = leading;
274 n.trailing_trivia = trailing;
275 }
276 Declaration::Witness(n) => {
277 n.leading_trivia = leading;
278 n.trailing_trivia = trailing;
279 }
280 Declaration::Document(n) => {
281 n.leading_trivia = leading;
282 n.trailing_trivia = trailing;
283 }
284 Declaration::Deliver(n) => {
285 n.leading_trivia = leading;
286 n.trailing_trivia = trailing;
287 }
288 Declaration::Notify(n) => {
289 n.leading_trivia = leading;
290 n.trailing_trivia = trailing;
291 }
292 Declaration::Generic(n) => {
293 n.leading_trivia = leading;
294 n.trailing_trivia = trailing;
295 }
296 }
297}
298
299pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
306pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
307
308#[derive(Debug, Clone)]
319pub struct SourceSnippet {
320 pub source: String,
321 pub line: u32,
322 pub column: u32,
323 pub filename: String,
324 pub context_before: usize,
325 pub context_after: usize,
326}
327
328impl SourceSnippet {
329 pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
331 Self {
332 source,
333 line,
334 column,
335 filename,
336 context_before: SOURCE_CONTEXT_LINES_BEFORE,
337 context_after: SOURCE_CONTEXT_LINES_AFTER,
338 }
339 }
340
341 #[must_use]
348 pub fn render(&self) -> String {
349 if self.source.is_empty() || self.line < 1 {
350 return String::new();
351 }
352 let raw: Vec<&str> = self.source.split('\n').collect();
353 let lines: Vec<&str> = if raw.last() == Some(&"") {
356 raw[..raw.len() - 1].to_vec()
357 } else {
358 raw
359 };
360 if lines.is_empty() || self.line as usize > lines.len() {
361 return String::new();
362 }
363
364 let line_idx = self.line as usize;
365 let start = line_idx.saturating_sub(self.context_before).max(1);
366 let end = (line_idx + self.context_after).min(lines.len());
367
368 let gutter = end.to_string().len();
369 let empty_gutter = " ".repeat(gutter);
370
371 let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
372 out.push(format!(
373 "{empty_gutter} --> {}:{}:{}",
374 self.filename, self.line, self.column
375 ));
376 out.push(format!("{empty_gutter} |"));
377 for n in start..=end {
378 let line_text = lines[n - 1];
379 out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
380 if n == line_idx {
381 let line_len = line_text.chars().count();
382 let col = (self.column as usize).clamp(1, line_len + 1);
383 out.push(format!(
384 "{empty_gutter} | {pad}^",
385 pad = " ".repeat(col - 1)
386 ));
387 }
388 }
389 out.join("\n")
390 }
391}
392
393#[derive(Debug, Clone, Default)]
394pub struct ParseError {
395 pub message: String,
396 pub line: u32,
397 pub column: u32,
398 pub source_snippet: Option<SourceSnippet>,
405}
406
407impl ParseError {
408 #[must_use]
412 pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
413 if self.line >= 1 {
414 self.source_snippet = Some(SourceSnippet::new(
415 source.to_string(),
416 self.line,
417 self.column,
418 filename.to_string(),
419 ));
420 }
421 self
422 }
423}
424
425impl std::fmt::Display for ParseError {
426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
428 if let Some(snippet) = &self.source_snippet {
429 let block = snippet.render();
430 if !block.is_empty() {
431 write!(f, "\n{block}")?;
432 }
433 }
434 Ok(())
435 }
436}
437
438impl std::error::Error for ParseError {}
439
440#[derive(Debug)]
458pub struct ParseResult {
459 pub program: Program,
460 pub errors: Vec<ParseError>,
461}
462
463impl ParseResult {
464 #[inline]
469 #[must_use]
470 pub fn has_errors(&self) -> bool {
471 !self.errors.is_empty()
472 }
473
474 #[inline]
477 #[must_use]
478 pub fn is_clean(&self) -> bool {
479 self.errors.is_empty()
480 }
481}
482
483#[inline]
497const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
498 matches!(
499 tt,
500 TokenType::Import
501 | TokenType::Persona
502 | TokenType::Context
503 | TokenType::Anchor
504 | TokenType::Memory
505 | TokenType::Tool
506 | TokenType::Type
507 | TokenType::Flow
508 | TokenType::Intent
509 | TokenType::Run
510 | TokenType::Let
511 | TokenType::Know
512 | TokenType::Believe
513 | TokenType::Speculate
514 | TokenType::Doubt
515 | TokenType::Lambda
516 | TokenType::Agent
517 | TokenType::Shield
518 | TokenType::Pix
519 | TokenType::Ledger
520 | TokenType::Psyche
521 | TokenType::Corpus
522 | TokenType::Dataspace
523 | TokenType::Ots
524 | TokenType::Mandate
525 | TokenType::Compute
526 | TokenType::Daemon
527 | TokenType::Savant
529 | TokenType::Synth
530 | TokenType::Scope
532 | TokenType::AxonStore
533 | TokenType::AxonEndpoint
534 | TokenType::Resource
535 | TokenType::Fabric
536 | TokenType::Manifest
537 | TokenType::Observe
538 | TokenType::Reconcile
539 | TokenType::Lease
540 | TokenType::Ensemble
541 | TokenType::Session
542 | TokenType::Topology
543 | TokenType::Immune
544 | TokenType::Reflex
545 | TokenType::Heal
546 | TokenType::Component
547 | TokenType::View
548 | TokenType::Channel
549 | TokenType::Ingest
550 | TokenType::Persist
551 | TokenType::Retrieve
552 | TokenType::Mutate
553 | TokenType::Purge
554 | TokenType::Transact
555 | TokenType::Mcp
556 )
557}
558
559pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
572
573pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
617 &["axon", "openai", "kimi", "glm", "anthropic"];
618
619pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
621
622pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
636 &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
637
638pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
659 "anthropic",
660 "auto",
661 "gemini",
662 "glm",
663 "kimi",
664 "ollama",
665 "openai",
666 "openrouter",
667 "stub",
668];
669
670#[inline]
671fn axonendpoint_is_valid_transport(s: &str) -> bool {
672 AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
673}
674
675#[inline]
676fn axonendpoint_is_valid_method(s: &str) -> bool {
677 AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
678}
679
680#[inline]
681fn axonendpoint_is_valid_backend(s: &str) -> bool {
682 AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
683}
684
685#[inline]
686fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
687 AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
688}
689
690pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
705 &["Text", "Int", "Float", "Bool", "Uuid"];
706
707#[inline]
710pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
711 AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
712}
713
714pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
741 let mut out: Vec<String> = Vec::new();
742 let bytes = path.as_bytes();
743 let mut i = 0;
744 while i < bytes.len() {
745 if bytes[i] != b'{' {
746 i += 1;
747 continue;
748 }
749 let start = i + 1;
752 let mut end = start;
753 while end < bytes.len() && bytes[end] != b'}' {
754 end += 1;
755 }
756 if end == bytes.len() {
757 break;
760 }
761 let raw = &path[start..end];
762 let valid = !raw.is_empty()
764 && raw.bytes().enumerate().all(|(idx, b)| {
765 if idx == 0 {
766 b.is_ascii_alphabetic() || b == b'_'
767 } else {
768 b.is_ascii_alphanumeric() || b == b'_'
769 }
770 });
771 if valid {
772 let name = raw.to_string();
773 if out.iter().any(|existing| existing == &name) {
774 return Err(name);
775 }
776 out.push(name);
777 }
778 i = end + 1;
779 }
780 Ok(out)
781}
782
783pub fn is_valid_capability_slug(slug: &str) -> bool {
797 if slug.is_empty() {
798 return false;
799 }
800 for segment in slug.split('.') {
801 if !is_valid_slug_segment(segment) {
802 return false;
803 }
804 }
805 true
806}
807
808fn is_valid_slug_segment(seg: &str) -> bool {
809 let mut chars = seg.chars();
810 let first = match chars.next() {
811 Some(c) => c,
812 None => return false,
813 };
814 if !first.is_ascii_lowercase() {
815 return false;
816 }
817 chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
818}
819
820#[cfg(test)]
830mod query_param_catalog_tests {
831 use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
832
833 #[test]
834 fn accepts_every_catalog_entry() {
835 for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
836 assert!(
837 axonendpoint_is_valid_query_param_type(ty),
838 "catalog entry `{ty}` must validate"
839 );
840 }
841 }
842
843 #[test]
844 fn rejects_off_catalog_types() {
845 for off in &[
846 "Timestamp", "Date",
848 "DateTime",
849 "List<Text>", "Jsonb", "Bytea",
852 "text", "TEXT",
854 "Number", "", " ", ] {
858 assert!(
859 !axonendpoint_is_valid_query_param_type(off),
860 "off-catalog `{off}` must reject"
861 );
862 }
863 }
864
865 #[test]
866 fn catalog_size_matches_design() {
867 assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
871 }
872}
873
874#[cfg(test)]
875mod query_param_parser_tests {
876 use crate::lexer::Lexer;
877 use crate::parser::Parser;
878
879 fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
880 let tokens = Lexer::new(src, "test.axon")
881 .tokenize()
882 .map_err(|e| format!("lex: {}", e.message))?;
883 let mut parser = Parser::new(tokens);
884 let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
885 program
886 .declarations
887 .into_iter()
888 .find_map(|d| match d {
889 crate::ast::Declaration::AxonEndpoint(e) => Some(e),
890 _ => None,
891 })
892 .ok_or_else(|| "no axonendpoint in program".to_string())
893 }
894
895 #[test]
896 fn endpoint_with_no_query_block_keeps_empty_vec() {
897 let src = r#"
898 axonendpoint write_secret {
899 method: POST
900 path: "/api/users"
901 body: SecretWriteRequest
902 execute: WriteSecret
903 }
904 "#;
905 let ep = parse_endpoint_source(src).expect("parses");
906 assert!(
907 ep.query_params.is_empty(),
908 "D5 — no `query:` block ⇒ empty query_params"
909 );
910 }
911
912 #[test]
913 fn single_query_param_required() {
914 let src = r#"
915 axonendpoint list_users {
916 method: GET
917 path: "/api/users"
918 query: { status: Text }
919 execute: ListUsers
920 }
921 "#;
922 let ep = parse_endpoint_source(src).expect("parses");
923 assert_eq!(ep.query_params.len(), 1);
924 assert_eq!(ep.query_params[0].name, "status");
925 assert_eq!(ep.query_params[0].type_expr.name, "Text");
926 assert!(!ep.query_params[0].type_expr.optional);
927 }
928
929 #[test]
930 fn optional_query_param_via_question_suffix() {
931 let src = r#"
932 axonendpoint list_users {
933 method: GET
934 path: "/api/users"
935 query: { limit: Int? }
936 execute: ListUsers
937 }
938 "#;
939 let ep = parse_endpoint_source(src).expect("parses");
940 assert_eq!(ep.query_params.len(), 1);
941 assert_eq!(ep.query_params[0].name, "limit");
942 assert_eq!(ep.query_params[0].type_expr.name, "Int");
943 assert!(
944 ep.query_params[0].type_expr.optional,
945 "`?` suffix sets optional"
946 );
947 }
948
949 #[test]
950 fn multiple_query_params_preserve_declaration_order() {
951 let src = r#"
952 axonendpoint search {
953 method: GET
954 path: "/api/search"
955 query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
956 execute: Search
957 }
958 "#;
959 let ep = parse_endpoint_source(src).expect("parses");
960 let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
961 assert_eq!(names, vec!["q", "page", "limit", "exact"]);
962 let types: Vec<&str> = ep
963 .query_params
964 .iter()
965 .map(|f| f.type_expr.name.as_str())
966 .collect();
967 assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
968 let optionals: Vec<bool> = ep
969 .query_params
970 .iter()
971 .map(|f| f.type_expr.optional)
972 .collect();
973 assert_eq!(optionals, vec![false, true, true, true]);
974 }
975
976 #[test]
977 fn duplicate_query_param_is_parse_error() {
978 let src = r#"
979 axonendpoint bad {
980 method: GET
981 path: "/api/x"
982 query: { name: Text, name: Int? }
983 execute: Bad
984 }
985 "#;
986 let err = parse_endpoint_source(src).expect_err("must fail");
987 assert!(
988 err.contains("duplicate query param 'name'"),
989 "error must name the duplicate. Got: {err}"
990 );
991 }
992
993 #[test]
994 fn off_catalog_type_with_smart_suggest_hint() {
995 let src = r#"
1000 axonendpoint bad {
1001 method: GET
1002 path: "/api/x"
1003 query: { value: Strng }
1004 execute: Bad
1005 }
1006 "#;
1007 let err = parse_endpoint_source(src).expect_err("must fail");
1008 assert!(
1009 err.contains("unsupported type 'Strng'"),
1010 "error must name the bad type. Got: {err}"
1011 );
1012 assert!(
1013 err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1014 "error must list the closed catalog. Got: {err}"
1015 );
1016 }
1017
1018 #[test]
1019 fn close_typo_gets_did_you_mean_hint() {
1020 let src = r#"
1023 axonendpoint bad {
1024 method: GET
1025 path: "/api/x"
1026 query: { value: Txt }
1027 execute: Bad
1028 }
1029 "#;
1030 let err = parse_endpoint_source(src).expect_err("must fail");
1031 assert!(
1032 err.contains("Did you mean") && err.contains("`Text`"),
1033 "smart-suggest must hint `Text`. Got: {err}"
1034 );
1035 }
1036
1037 #[test]
1038 fn every_catalog_type_parses_cleanly() {
1039 for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1041 let src = format!(
1042 r#"
1043 axonendpoint x {{
1044 method: GET
1045 path: "/api/x"
1046 query: {{ v: {ty} }}
1047 execute: X
1048 }}
1049 "#
1050 );
1051 let ep = parse_endpoint_source(&src)
1052 .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1053 assert_eq!(ep.query_params[0].type_expr.name, *ty);
1054 }
1055 }
1056
1057 #[test]
1058 fn comma_optional_between_params() {
1059 let src = r#"
1063 axonendpoint x {
1064 method: GET
1065 path: "/api/x"
1066 query: { a: Text b: Int? }
1067 execute: X
1068 }
1069 "#;
1070 let ep = parse_endpoint_source(src).expect("parses without commas");
1071 assert_eq!(ep.query_params.len(), 2);
1072 }
1073
1074 #[test]
1077 fn double_query_block_is_parse_error() {
1078 let src = r#"
1082 axonendpoint x {
1083 method: GET
1084 path: "/api/x"
1085 query: { a: Text }
1086 query: { b: Int? }
1087 execute: X
1088 }
1089 "#;
1090 let err = parse_endpoint_source(src).expect_err("must fail");
1091 assert!(
1092 err.contains("declares `query: { … }` more than once"),
1093 "error must call out the duplicate block. Got: {err}"
1094 );
1095 assert!(
1096 err.contains("combine all params into a single block"),
1097 "error must hint the canonical fix. Got: {err}"
1098 );
1099 }
1100
1101 #[test]
1102 fn optional_generic_type_is_parse_error_with_canonical_hint() {
1103 let src = r#"
1107 axonendpoint x {
1108 method: GET
1109 path: "/api/x"
1110 query: { value: Optional<Text> }
1111 execute: X
1112 }
1113 "#;
1114 let err = parse_endpoint_source(src).expect_err("must fail");
1115 assert!(
1116 err.contains("generic type `Optional<Text>`"),
1117 "error must name the generic type literally. Got: {err}"
1118 );
1119 assert!(
1120 err.contains("Use `Text?` (the `?` suffix)"),
1121 "error must hint the canonical `Text?` syntax. Got: {err}"
1122 );
1123 }
1124
1125 #[test]
1126 fn list_generic_type_is_parse_error_with_deferral_hint() {
1127 let src = r#"
1132 axonendpoint x {
1133 method: GET
1134 path: "/api/x"
1135 query: { tags: List<Text> }
1136 execute: X
1137 }
1138 "#;
1139 let err = parse_endpoint_source(src).expect_err("must fail");
1140 assert!(
1141 err.contains("generic type `List<Text>`"),
1142 "error must name the generic type. Got: {err}"
1143 );
1144 assert!(
1145 err.contains("Multi-value query params")
1146 && err.contains("honest-deferred"),
1147 "error must mention the multi-value deferral. Got: {err}"
1148 );
1149 }
1150
1151 #[test]
1152 fn other_generic_types_caught_generically() {
1153 let src = r#"
1157 axonendpoint x {
1158 method: GET
1159 path: "/api/x"
1160 query: { value: Stream<Int> }
1161 execute: X
1162 }
1163 "#;
1164 let err = parse_endpoint_source(src).expect_err("must fail");
1165 assert!(
1166 err.contains("generic type `Stream<Int>`"),
1167 "error must name the generic type. Got: {err}"
1168 );
1169 assert!(
1170 err.contains("Text | Int | Float | Bool | Uuid"),
1171 "error must list the closed catalog. Got: {err}"
1172 );
1173 }
1174
1175 #[test]
1176 fn uuid_optional_parses_cleanly() {
1177 let src = r#"
1180 axonendpoint find {
1181 method: GET
1182 path: "/api/x"
1183 query: { after: Uuid? }
1184 execute: Find
1185 }
1186 "#;
1187 let ep = parse_endpoint_source(src).expect("parses");
1188 assert_eq!(ep.query_params.len(), 1);
1189 assert_eq!(ep.query_params[0].name, "after");
1190 assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1191 assert!(ep.query_params[0].type_expr.optional);
1192 assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1193 }
1194
1195 #[test]
1196 fn empty_query_block_yields_empty_vec() {
1197 let src = r#"
1201 axonendpoint x {
1202 method: GET
1203 path: "/api/x"
1204 query: { }
1205 execute: X
1206 }
1207 "#;
1208 let ep = parse_endpoint_source(src).expect("empty block parses");
1209 assert!(ep.query_params.is_empty());
1210 }
1211
1212 #[test]
1213 fn kivi_secret_write_path_plus_query() {
1214 let src = r#"
1218 axonendpoint write_secret {
1219 method: POST
1220 path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1221 query: { dry_run: Bool?, overwrite: Bool? }
1222 body: SecretWriteRequest
1223 execute: WriteSecret
1224 }
1225 "#;
1226 let ep = parse_endpoint_source(src).expect("parses");
1227 assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1229 assert_eq!(ep.query_params.len(), 2);
1231 assert_eq!(ep.query_params[0].name, "dry_run");
1232 assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1233 assert!(ep.query_params[0].type_expr.optional);
1234 assert_eq!(ep.query_params[1].name, "overwrite");
1235 assert_eq!(ep.body_type, "SecretWriteRequest");
1237 }
1238}
1239
1240#[cfg(test)]
1241mod path_param_extraction_tests {
1242 use super::extract_path_param_names;
1243
1244 #[test]
1245 fn empty_path_no_placeholders() {
1246 assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1247 assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1248 assert_eq!(extract_path_param_names(""), Ok(vec![]));
1249 }
1250
1251 #[test]
1252 fn single_placeholder() {
1253 assert_eq!(
1254 extract_path_param_names("/api/users/{id}"),
1255 Ok(vec!["id".to_string()])
1256 );
1257 }
1258
1259 #[test]
1260 fn multiple_placeholders_in_declaration_order() {
1261 assert_eq!(
1262 extract_path_param_names(
1263 "/api/tenants/{tenant_id}/secrets/{secret_name}"
1264 ),
1265 Ok(vec![
1266 "tenant_id".to_string(),
1267 "secret_name".to_string(),
1268 ])
1269 );
1270 }
1271
1272 #[test]
1273 fn kivi_chat_history_path_pattern() {
1274 let names = extract_path_param_names(
1278 "/api/tenants/{tenant_id}/secrets/{secret_name}",
1279 );
1280 assert_eq!(
1281 names,
1282 Ok(vec![
1283 "tenant_id".to_string(),
1284 "secret_name".to_string(),
1285 ])
1286 );
1287 }
1288
1289 #[test]
1290 fn duplicate_placeholder_returns_err() {
1291 assert_eq!(
1292 extract_path_param_names("/api/users/{id}/posts/{id}"),
1293 Err("id".to_string())
1294 );
1295 }
1296
1297 #[test]
1298 fn underscore_and_numeric_in_name() {
1299 assert_eq!(
1300 extract_path_param_names("/api/{user_id}/items/{item_2}"),
1301 Ok(vec!["user_id".to_string(), "item_2".to_string()])
1302 );
1303 }
1304
1305 #[test]
1306 fn leading_underscore_accepted() {
1307 assert_eq!(
1313 extract_path_param_names("/api/{_internal}"),
1314 Ok(vec!["_internal".to_string()])
1315 );
1316 }
1317
1318 #[test]
1319 fn malformed_placeholder_silently_ignored() {
1320 assert_eq!(
1324 extract_path_param_names("/api/{not valid}"),
1325 Ok(vec![])
1326 );
1327 assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1329 assert_eq!(
1331 extract_path_param_names("/api/{tenant id}/users/{id}"),
1332 Ok(vec!["id".to_string()])
1333 );
1334 }
1335
1336 #[test]
1337 fn unterminated_brace_returns_clean() {
1338 assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1341 }
1342
1343 #[test]
1344 fn placeholders_at_path_boundaries() {
1345 assert_eq!(
1348 extract_path_param_names("{prefix}/api/users/{id}"),
1349 Ok(vec!["prefix".to_string(), "id".to_string()])
1350 );
1351 assert_eq!(
1352 extract_path_param_names("/api/{id}"),
1353 Ok(vec!["id".to_string()])
1354 );
1355 }
1356
1357 #[test]
1358 fn deduplication_detects_non_adjacent_duplicates() {
1359 assert_eq!(
1361 extract_path_param_names(
1362 "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1363 ),
1364 Err("org_id".to_string())
1365 );
1366 }
1367
1368 #[test]
1369 fn never_panics_on_arbitrary_input() {
1370 for input in &[
1372 "{",
1373 "}",
1374 "{}",
1375 "{{}}",
1376 "{{{",
1377 "/api/{}/{id}",
1378 "////",
1379 "\u{1F4A1}", "\u{0000}", ] {
1382 let _ = extract_path_param_names(input); }
1384 }
1385}
1386
1387#[cfg(test)]
1388mod capability_slug_tests {
1389 use super::is_valid_capability_slug;
1390
1391 #[test]
1392 fn accepts_canonical_examples() {
1393 assert!(is_valid_capability_slug("admin"));
1394 assert!(is_valid_capability_slug("legal.read"));
1395 assert!(is_valid_capability_slug("hipaa.phi.read"));
1396 assert!(is_valid_capability_slug("bank.officer.senior"));
1397 assert!(is_valid_capability_slug("a"));
1398 assert!(is_valid_capability_slug("a_b"));
1399 assert!(is_valid_capability_slug("a1"));
1400 assert!(is_valid_capability_slug("a.b1_c"));
1401 }
1402
1403 #[test]
1404 fn rejects_empty_string() {
1405 assert!(!is_valid_capability_slug(""));
1406 }
1407
1408 #[test]
1409 fn rejects_uppercase() {
1410 assert!(!is_valid_capability_slug("Admin"));
1411 assert!(!is_valid_capability_slug("admin.READ"));
1412 }
1413
1414 #[test]
1415 fn rejects_digit_first() {
1416 assert!(!is_valid_capability_slug("1admin"));
1417 assert!(!is_valid_capability_slug("admin.1read"));
1418 }
1419
1420 #[test]
1421 fn rejects_hyphen() {
1422 assert!(!is_valid_capability_slug("bank-officer"));
1423 }
1424
1425 #[test]
1426 fn rejects_empty_segments() {
1427 assert!(!is_valid_capability_slug("bank..a"));
1428 assert!(!is_valid_capability_slug(".admin"));
1429 assert!(!is_valid_capability_slug("admin."));
1430 }
1431
1432 #[test]
1433 fn rejects_special_chars() {
1434 assert!(!is_valid_capability_slug("admin@read"));
1435 assert!(!is_valid_capability_slug("admin/read"));
1436 assert!(!is_valid_capability_slug("admin read"));
1437 }
1438}
1439
1440pub struct Parser {
1443 tokens: Vec<Token>,
1444 pos: usize,
1445 hoisted: Vec<Declaration>,
1465 leading_trivia: Vec<Vec<Trivia>>,
1470 trailing_trivia: Vec<Vec<Trivia>>,
1474 last_let_value_kind: String,
1478 loop_depth: u32,
1483 source: Option<String>,
1489 filename: String,
1490}
1491
1492impl Parser {
1493 pub fn new(raw_tokens: Vec<Token>) -> Self {
1494 let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1504 let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1505 let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1506
1507 let mut pending_leading: Vec<Trivia> = Vec::new();
1508 let mut last_effective_line: i64 = -1;
1509 for tok in raw_tokens {
1510 if is_comment_token(&tok.ttype) {
1511 let kind = token_to_trivia_kind(&tok.ttype)
1512 .expect("comment token must map to a trivia kind");
1513 let triv = Trivia {
1514 kind,
1515 text: tok.value,
1516 line: tok.line,
1517 column: tok.column,
1518 };
1519 if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1520 trailing.last_mut().unwrap().push(triv);
1521 } else {
1522 pending_leading.push(triv);
1523 }
1524 } else {
1525 last_effective_line = tok.line as i64;
1526 effective.push(tok);
1527 leading.push(std::mem::take(&mut pending_leading));
1528 trailing.push(Vec::new());
1529 }
1530 }
1531
1532 Parser {
1533 hoisted: Vec::new(),
1534 tokens: effective,
1535 pos: 0,
1536 leading_trivia: leading,
1537 trailing_trivia: trailing,
1538 last_let_value_kind: "literal".to_string(),
1539 loop_depth: 0,
1540 source: None,
1541 filename: "<source>".to_string(),
1542 }
1543 }
1544
1545 #[must_use]
1557 pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1558 self.source = Some(source.to_string());
1559 self.filename = filename.to_string();
1560 self
1561 }
1562
1563 pub fn parse(&mut self) -> Result<Program, ParseError> {
1566 let mut program = Program {
1567 declarations: Vec::new(),
1568 declaration_trivia: Vec::new(),
1569 loc: Loc { line: 1, column: 1 },
1570 };
1571 while !self.check(TokenType::Eof) {
1572 let start_pos = self.pos;
1578 let mut decl = match self.parse_declaration() {
1579 Ok(d) => d,
1580 Err(e) => return Err(self.attach_source_to_error(e)),
1581 };
1582 let end_pos = self.pos.saturating_sub(1);
1583 let leading = self
1584 .leading_trivia
1585 .get(start_pos)
1586 .cloned()
1587 .unwrap_or_default();
1588 let trailing = self
1589 .trailing_trivia
1590 .get(end_pos)
1591 .cloned()
1592 .unwrap_or_default();
1593 attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1599 program.declarations.push(decl);
1600 program
1601 .declaration_trivia
1602 .push(DeclarationTrivia { leading, trailing });
1603 for hoisted in std::mem::take(&mut self.hoisted) {
1607 program.declarations.push(hoisted);
1608 program.declaration_trivia.push(DeclarationTrivia {
1609 leading: Vec::new(),
1610 trailing: Vec::new(),
1611 });
1612 }
1613 }
1614 crate::voice_desugar::expand(&mut program);
1621 crate::upstream_presets::expand(&mut program);
1622 Ok(program)
1623 }
1624
1625 pub fn parse_with_recovery(&mut self) -> ParseResult {
1658 let mut program = Program {
1659 declarations: Vec::new(),
1660 declaration_trivia: Vec::new(),
1661 loc: Loc { line: 1, column: 1 },
1662 };
1663 let mut errors: Vec<ParseError> = Vec::new();
1664
1665 while !self.check(TokenType::Eof) {
1666 let start_pos = self.pos;
1667 match self.parse_declaration() {
1668 Ok(mut decl) => {
1669 let end_pos = self.pos.saturating_sub(1);
1670 let leading = self
1671 .leading_trivia
1672 .get(start_pos)
1673 .cloned()
1674 .unwrap_or_default();
1675 let trailing = self
1676 .trailing_trivia
1677 .get(end_pos)
1678 .cloned()
1679 .unwrap_or_default();
1680 attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1681 program.declarations.push(decl);
1682 program
1683 .declaration_trivia
1684 .push(DeclarationTrivia { leading, trailing });
1685 }
1686 Err(err) => {
1687 errors.push(self.attach_source_to_error(err));
1691 if self.pos == start_pos && !self.check(TokenType::Eof) {
1696 self.advance();
1697 }
1698 self.advance_to_sync_point();
1699 }
1700 }
1701 }
1702
1703 ParseResult { program, errors }
1704 }
1705
1706 fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1711 match &self.source {
1712 Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1713 _ => err,
1714 }
1715 }
1716
1717 fn advance_to_sync_point(&mut self) {
1722 let mut depth: i32 = 0;
1723 while !self.check(TokenType::Eof) {
1724 let tt = self.current().ttype.clone();
1725 if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1729 return;
1730 }
1731 if matches!(tt, TokenType::LBrace) {
1732 depth += 1;
1733 } else if matches!(tt, TokenType::RBrace) {
1734 depth -= 1;
1735 }
1736 self.advance();
1737 }
1738 }
1739
1740 fn current(&self) -> &Token {
1743 if self.pos >= self.tokens.len() {
1744 self.tokens.last().unwrap() } else {
1746 &self.tokens[self.pos]
1747 }
1748 }
1749
1750 fn advance(&mut self) -> &Token {
1751 let idx = self.pos;
1752 if self.pos < self.tokens.len() {
1753 self.pos += 1;
1754 }
1755 &self.tokens[idx]
1756 }
1757
1758 fn check(&self, tt: TokenType) -> bool {
1759 self.current().ttype == tt
1760 }
1761
1762 fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1763 let tok = self.current().clone();
1764 if tok.ttype != expected {
1765 return Err(ParseError {
1766 message: format!(
1767 "Expected {:?}, found {:?}('{}')",
1768 expected, tok.ttype, tok.value
1769 ),
1770 line: tok.line,
1771 column: tok.column,
1772 ..Default::default()
1773 });
1774 }
1775 self.pos += 1;
1776 Ok(tok)
1777 }
1778
1779 fn error(&self, message: &str) -> ParseError {
1781 let tok = self.current();
1782 ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1783 }
1784
1785 fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1787 let tok = self.current().clone();
1788 match tok.ttype {
1789 TokenType::Identifier
1790 | TokenType::Bool
1791 | TokenType::StringLit
1792 | TokenType::Integer
1793 | TokenType::Float => {
1794 self.pos += 1;
1795 Ok(tok)
1796 }
1797 _ => {
1798 if !tok.value.is_empty()
1800 && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1801 && tok.ttype != TokenType::Eof
1802 {
1803 self.pos += 1;
1804 Ok(tok)
1805 } else {
1806 Err(ParseError {
1807 message: format!(
1808 "Expected identifier or keyword value, found {:?}('{}')",
1809 tok.ttype, tok.value
1810 ),
1811 line: tok.line,
1812 column: tok.column,
1813 ..Default::default()
1814 })
1815 }
1816 }
1817 }
1818 }
1819
1820 fn consume_number(&mut self) -> Result<f64, ParseError> {
1821 let tok = self.current().clone();
1822 match tok.ttype {
1823 TokenType::Float | TokenType::Integer => {
1824 self.pos += 1;
1825 tok.value.parse::<f64>().map_err(|_| ParseError {
1826 message: format!("Invalid number '{}'", tok.value),
1827 line: tok.line,
1828 column: tok.column,
1829 ..Default::default()
1830 })
1831 }
1832 _ => Err(ParseError {
1833 message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1834 line: tok.line,
1835 column: tok.column,
1836 ..Default::default()
1837 }),
1838 }
1839 }
1840
1841 fn parse_bool(&mut self) -> Result<bool, ParseError> {
1842 let tok = self.consume(TokenType::Bool)?;
1843 Ok(tok.value == "true")
1844 }
1845
1846 fn loc_of(&self, tok: &Token) -> Loc {
1847 Loc {
1848 line: tok.line,
1849 column: tok.column,
1850 }
1851 }
1852
1853 fn check_run_modifier(&self) -> bool {
1854 if self.current().value == "with" {
1861 return true;
1862 }
1863 matches!(
1864 self.current().ttype,
1865 TokenType::As
1866 | TokenType::Within
1867 | TokenType::ConstrainedBy
1868 | TokenType::OnFailure
1869 | TokenType::OutputTo
1870 | TokenType::Effort
1871 )
1872 }
1873
1874 fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1877 self.consume(TokenType::LBracket)?;
1878 let mut items = Vec::new();
1879 items.push(self.consume(TokenType::StringLit)?.value);
1880 while self.check(TokenType::Comma) {
1881 self.advance();
1882 items.push(self.consume(TokenType::StringLit)?.value);
1883 }
1884 self.consume(TokenType::RBracket)?;
1885 Ok(items)
1886 }
1887
1888 fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1897 self.consume(TokenType::LBracket)?;
1898 let mut items = Vec::new();
1899 if !self.check(TokenType::RBracket) {
1900 items.push(self.consume(TokenType::StringLit)?.value);
1901 while self.check(TokenType::Comma) {
1902 self.advance();
1903 if self.check(TokenType::RBracket) {
1904 break; }
1906 items.push(self.consume(TokenType::StringLit)?.value);
1907 }
1908 }
1909 self.consume(TokenType::RBracket)?;
1910 Ok(items)
1911 }
1912
1913 fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1914 let mut names = Vec::new();
1915 names.push(self.consume(TokenType::Identifier)?.value);
1916 while self.check(TokenType::Comma) {
1917 self.advance();
1918 names.push(self.consume(TokenType::Identifier)?.value);
1919 }
1920 Ok(names)
1921 }
1922
1923 fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1924 self.consume(TokenType::LBracket)?;
1925 let items = self.parse_extended_identifier_list()?;
1926 self.consume(TokenType::RBracket)?;
1927 Ok(items)
1928 }
1929
1930 fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1931 let mut items = Vec::new();
1932 items.push(self.consume_any_ident_or_kw()?.value);
1933 while self.check(TokenType::Comma) {
1934 self.advance();
1935 items.push(self.consume_any_ident_or_kw()?.value);
1936 }
1937 Ok(items)
1938 }
1939
1940 fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1941 let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1942 while self.check(TokenType::Dot) {
1943 self.advance();
1944 parts.push(self.consume_any_ident_or_kw()?.value);
1945 }
1946 Ok(parts.join("."))
1947 }
1948
1949 fn parse_subject(&mut self) -> Result<String, ParseError> {
1973 let t = self.current().clone();
1974 match t.ttype {
1975 TokenType::StringLit => {
1976 self.advance();
1977 Ok(format!("\"{}\"", t.value))
1978 }
1979 TokenType::Integer | TokenType::Float => {
1980 self.advance();
1981 Ok(t.value)
1982 }
1983 _ => self.parse_dotted_identifier(),
1984 }
1985 }
1986
1987 fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1988 if self.check(TokenType::LBracket) {
1989 let items = self.parse_bracketed_dot_identifiers()?;
1990 return Ok(format!("[{}]", items.join(", ")));
1991 }
1992 self.parse_dotted_identifier()
1993 }
1994
1995 fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1996 self.consume(TokenType::LBracket)?;
1997 let mut items = vec![self.parse_dotted_identifier()?];
1998 while self.check(TokenType::Comma) {
1999 self.advance();
2000 items.push(self.parse_dotted_identifier()?);
2001 }
2002 self.consume(TokenType::RBracket)?;
2003 Ok(items)
2004 }
2005
2006 fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
2007 let mut args = Vec::new();
2008 while !self.check(TokenType::RParen) {
2009 let tok = self.current().clone();
2010 match tok.ttype {
2011 TokenType::StringLit | TokenType::Integer | TokenType::Float => {
2012 self.advance();
2013 args.push(tok.value);
2014 }
2015 TokenType::Identifier => {
2016 self.advance();
2017 let mut val = tok.value;
2018 if self.check(TokenType::Dot) {
2019 self.advance();
2020 val.push('.');
2021 val.push_str(&self.consume_any_ident_or_kw()?.value);
2022 }
2023 args.push(val);
2024 }
2025 _ => {
2026 self.advance();
2027 let key = tok.value;
2028 if self.check(TokenType::Colon) {
2029 self.advance();
2030 let v = self.advance().value.clone();
2031 args.push(format!("{key}:{v}"));
2032 } else {
2033 args.push(key);
2034 }
2035 }
2036 }
2037 if self.check(TokenType::Comma) {
2038 self.advance();
2039 }
2040 }
2041 Ok(args)
2042 }
2043
2044 fn skip_value(&mut self) {
2046 match self.current().ttype {
2047 TokenType::LBracket => {
2048 self.advance();
2049 let mut depth = 1u32;
2050 while depth > 0 && !self.check(TokenType::Eof) {
2051 if self.check(TokenType::LBracket) {
2052 depth += 1;
2053 } else if self.check(TokenType::RBracket) {
2054 depth -= 1;
2055 }
2056 self.advance();
2057 }
2058 }
2059 TokenType::LBrace => {
2060 self.advance();
2061 let mut depth = 1u32;
2062 while depth > 0 && !self.check(TokenType::Eof) {
2063 if self.check(TokenType::LBrace) {
2064 depth += 1;
2065 } else if self.check(TokenType::RBrace) {
2066 depth -= 1;
2067 }
2068 self.advance();
2069 }
2070 }
2071 TokenType::Lt => {
2072 self.advance();
2074 let mut depth = 1u32;
2075 while depth > 0 && !self.check(TokenType::Eof) {
2076 if self.check(TokenType::Lt) {
2077 depth += 1;
2078 } else if self.check(TokenType::Gt) {
2079 depth -= 1;
2080 }
2081 self.advance();
2082 }
2083 }
2084 _ => {
2085 self.advance();
2086 while self.check(TokenType::Dot) {
2087 self.advance();
2088 self.advance();
2089 }
2090 }
2091 }
2092 }
2093
2094 fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2096 self.consume(TokenType::LBrace)?;
2097 let mut depth = 1u32;
2098 while depth > 0 {
2099 if self.check(TokenType::Eof) {
2100 let tok = self.current();
2101 return Err(ParseError {
2102 message: "Unterminated block — expected '}'".to_string(),
2103 line: tok.line,
2104 column: tok.column,
2105 ..Default::default()
2106 });
2107 }
2108 if self.check(TokenType::LBrace) {
2109 depth += 1;
2110 } else if self.check(TokenType::RBrace) {
2111 depth -= 1;
2112 }
2113 self.advance();
2114 }
2115 Ok(())
2116 }
2117
2118 fn at_declaration_start(&self) -> bool {
2119 is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2120 }
2121
2122 fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2125 let tok = self.current().clone();
2126
2127 if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2135 return self.parse_top_level_budget().map(Declaration::Budget);
2136 }
2137
2138 match tok.ttype {
2139 TokenType::Import => self.parse_import().map(Declaration::Import),
2140 TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2141 TokenType::Context => self.parse_context().map(Declaration::Context),
2142 TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2143 TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2144 TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2145 TokenType::Type => self.parse_type_def().map(Declaration::Type),
2146 TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2147 TokenType::Effect => self.parse_effect().map(Declaration::Effect),
2150 TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2151 TokenType::Run => self.parse_run().map(Declaration::Run),
2152 TokenType::Let => self.parse_let().map(Declaration::Let),
2153 TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2154 self.parse_epistemic_block().map(Declaration::Epistemic)
2155 }
2156 TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2157
2158 TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2160 TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2161 TokenType::Window => self.parse_window().map(Declaration::Window),
2163 TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2164 TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2165 TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2166 TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2167 TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2168 TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2169 TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2170 TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2171 TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2172 TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2173 TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2174 TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2175
2176 TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2178 TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2179 TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2180 TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2181
2182 TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2184 TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2185 TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2186
2187 TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2189 TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2190
2191 TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2193
2194 TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2196
2197 TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2199
2200 TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2202
2203 TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2205 TokenType::Document => self.parse_document().map(Declaration::Document),
2206
2207 TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2209 TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2210
2211 TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2213
2214 TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2216
2217 TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2219
2220 TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2222
2223 TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2225
2226 TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2228
2229 TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2231 TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2232 TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2233
2234 TokenType::Component => self.parse_component().map(Declaration::Component),
2236 TokenType::View => self.parse_view().map(Declaration::View),
2237
2238 TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2240
2241 TokenType::Ingest
2244 | TokenType::Persist
2245 | TokenType::Retrieve
2246 | TokenType::Mutate
2247 | TokenType::Purge
2248 | TokenType::Transact => self.parse_generic_declaration(),
2249
2250 TokenType::Mcp => self.parse_generic_declaration(),
2252
2253 _ => {
2254 let hint = crate::smart_suggest::suggest_for(
2258 &tok.value,
2259 crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2260 );
2261 let base = format!(
2262 "Unexpected token at top level: '{}' — expected declaration \
2263 (persona, context, anchor, flow, run, ...)",
2264 tok.value
2265 );
2266 let message = if hint.is_empty() {
2267 base
2268 } else {
2269 format!("{base}. {hint}")
2270 };
2271 Err(ParseError {
2272 message,
2273 line: tok.line,
2274 column: tok.column,
2275 ..Default::default()
2276 })
2277 }
2278 }
2279 }
2280
2281 fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2284 let tok = self.consume(TokenType::Import)?;
2285 let loc = self.loc_of(&tok);
2286
2287 let mut path_parts = Vec::new();
2288
2289 if self.check(TokenType::At) {
2291 self.advance();
2292 let first = self.consume(TokenType::Identifier)?;
2293 path_parts.push(format!("@{}", first.value));
2294 } else {
2295 let first = self.consume(TokenType::Identifier)?;
2296 path_parts.push(first.value);
2297 }
2298
2299 while self.check(TokenType::Dot) {
2300 self.advance();
2301 if self.check(TokenType::LBrace) {
2302 break;
2303 }
2304 let part = self.consume(TokenType::Identifier)?;
2305 path_parts.push(part.value);
2306 }
2307
2308 let mut names = Vec::new();
2309 if self.check(TokenType::LBrace) {
2310 self.advance();
2311 names = self.parse_identifier_list()?;
2312 self.consume(TokenType::RBrace)?;
2313 }
2314
2315 let mut allow_downgrade = false;
2324 if self.check(TokenType::At) {
2325 let at_tok = self.current().clone();
2326 self.advance();
2327 let ident = self.consume(TokenType::Identifier)?;
2328 if ident.value == "allow_downgrade" {
2329 allow_downgrade = true;
2330 } else {
2331 return Err(ParseError {
2332 message: format!(
2333 "unknown import annotation '@{}' — the only import annotation is \
2334 `@allow_downgrade` (the epistemic-downgrade acknowledgment).",
2335 ident.value
2336 ),
2337 line: at_tok.line,
2338 column: at_tok.column,
2339 ..Default::default()
2340 });
2341 }
2342 }
2343
2344 let next_is_apx = self
2359 .tokens
2360 .get(self.pos + 1)
2361 .map(|t| t.value == "apx")
2362 .unwrap_or(false);
2363 if self.current().value == "with" && next_is_apx {
2364 let tok = self.current().clone();
2365 return Err(ParseError {
2366 message: "`import … with apx { … }` is RETRACTED (v2.67.0). The apx policy block was \
2367 parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2368 dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2369 quarantine, no compliance gate. Declaring it verified nothing while \
2370 implying your supply chain was checked. Remove the `with apx { … }` \
2371 clause; the plain `import` resolves through the Epistemic Module \
2372 System."
2373 .to_string(),
2374 line: tok.line,
2375 column: tok.column,
2376 ..Default::default()
2377 });
2378 }
2379
2380 Ok(ImportNode {
2381 module_path: path_parts,
2382 names,
2383 allow_downgrade,
2384 loc,
2385 leading_trivia: Vec::new(),
2386 trailing_trivia: Vec::new(),
2387 })
2388 }
2389
2390 fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2393 let tok = self.consume(TokenType::Persona)?;
2394 let loc = self.loc_of(&tok);
2395 let name = self.consume(TokenType::Identifier)?.value;
2396 self.consume(TokenType::LBrace)?;
2397
2398 let mut node = PersonaDefinition {
2399 name,
2400 domain: Vec::new(),
2401 tone: String::new(),
2402 confidence_threshold: None,
2403 cite_sources: None,
2404 refuse_if: Vec::new(),
2405 language: String::new(),
2406 description: String::new(),
2407 loc,
2408 leading_trivia: Vec::new(),
2409 trailing_trivia: Vec::new(),
2410 };
2411
2412 while !self.check(TokenType::RBrace) {
2413 let field_name = self.current().value.clone();
2414 self.advance();
2415 self.consume(TokenType::Colon)?;
2416
2417 match field_name.as_str() {
2418 "domain" => node.domain = self.parse_string_list()?,
2419 "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2420 "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2421 "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2422 "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2423 "language" => node.language = self.consume(TokenType::StringLit)?.value,
2424 "description" => node.description = self.consume(TokenType::StringLit)?.value,
2425 _ => self.skip_value(),
2426 }
2427 }
2428 self.consume(TokenType::RBrace)?;
2429 Ok(node)
2430 }
2431
2432 fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2435 let tok = self.consume(TokenType::Context)?;
2436 let loc = self.loc_of(&tok);
2437 let name = self.consume(TokenType::Identifier)?.value;
2438 self.consume(TokenType::LBrace)?;
2439
2440 let mut node = ContextDefinition {
2441 name,
2442 memory_scope: String::new(),
2443 language: String::new(),
2444 depth: String::new(),
2445 max_tokens: None,
2446 temperature: None,
2447 cite_sources: None,
2448 now_tz: None,
2449 loc,
2450 leading_trivia: Vec::new(),
2451 trailing_trivia: Vec::new(),
2452 };
2453
2454 while !self.check(TokenType::RBrace) {
2455 let field_name = self.current().value.clone();
2456 self.advance();
2457 self.consume(TokenType::Colon)?;
2458
2459 match field_name.as_str() {
2460 "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2461 "language" => node.language = self.consume(TokenType::StringLit)?.value,
2462 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2463 "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2465 "max_tokens" => {
2466 node.max_tokens = Some(
2467 self.consume(TokenType::Integer)?
2468 .value
2469 .parse::<i64>()
2470 .unwrap_or(0),
2471 )
2472 }
2473 "temperature" => node.temperature = Some(self.consume_number()?),
2474 "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2475 _ => self.skip_value(),
2476 }
2477 }
2478 self.consume(TokenType::RBrace)?;
2479 Ok(node)
2480 }
2481
2482 fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2485 let tok = self.consume(TokenType::Anchor)?;
2486 let loc = self.loc_of(&tok);
2487 let name = self.consume(TokenType::Identifier)?.value;
2488 self.consume(TokenType::LBrace)?;
2489
2490 let mut node = AnchorConstraint {
2491 name,
2492 require: String::new(),
2493 reject: Vec::new(),
2494 enforce: String::new(),
2495 description: String::new(),
2496 confidence_floor: None,
2497 unknown_response: String::new(),
2498 on_violation: String::new(),
2499 on_violation_target: String::new(),
2500 loc,
2501 leading_trivia: Vec::new(),
2502 trailing_trivia: Vec::new(),
2503 };
2504
2505 while !self.check(TokenType::RBrace) {
2506 let field_name = self.current().value.clone();
2507 self.advance();
2508 self.consume(TokenType::Colon)?;
2509
2510 match field_name.as_str() {
2511 "require" => node.require = self.consume_any_ident_or_kw()?.value,
2512 "description" => node.description = self.consume(TokenType::StringLit)?.value,
2513 "reject" => node.reject = self.parse_bracketed_identifiers()?,
2514 "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2515 "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2516 "unknown_response" => {
2517 node.unknown_response = self.consume(TokenType::StringLit)?.value
2518 }
2519 "on_violation" => {
2520 let action = self.consume_any_ident_or_kw()?.value;
2522 node.on_violation = action.clone();
2523 if action == "raise" || action == "fallback" {
2524 node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2525 }
2526 }
2527 _ => self.skip_value(),
2528 }
2529 }
2530 self.consume(TokenType::RBrace)?;
2531 Ok(node)
2532 }
2533
2534 fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2537 let tok = self.consume(TokenType::Memory)?;
2538 let loc = self.loc_of(&tok);
2539 let name = self.consume(TokenType::Identifier)?.value;
2540 self.consume(TokenType::LBrace)?;
2541
2542 let mut node = MemoryDefinition {
2543 name,
2544 store: String::new(),
2545 backend: String::new(),
2546 retrieval: String::new(),
2547 decay: String::new(),
2548 loc,
2549 leading_trivia: Vec::new(),
2550 trailing_trivia: Vec::new(),
2551 };
2552
2553 while !self.check(TokenType::RBrace) {
2554 let field_name = self.current().value.clone();
2555 self.advance();
2556 self.consume(TokenType::Colon)?;
2557
2558 match field_name.as_str() {
2559 "store" => node.store = self.consume_any_ident_or_kw()?.value,
2560 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2561 "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2562 "decay" => {
2563 if self.check(TokenType::Duration) {
2564 node.decay = self.advance().value.clone();
2565 } else {
2566 node.decay = self.consume_any_ident_or_kw()?.value;
2567 }
2568 }
2569 _ => self.skip_value(),
2570 }
2571 }
2572 self.consume(TokenType::RBrace)?;
2573 Ok(node)
2574 }
2575
2576 fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2579 let tok = self.consume(TokenType::Tool)?;
2580 let loc = self.loc_of(&tok);
2581 let name = self.consume(TokenType::Identifier)?.value;
2582 self.consume(TokenType::LBrace)?;
2583
2584 let mut node = ToolDefinition {
2585 shield_ref: String::new(),
2586 name,
2587 provider: String::new(),
2588 max_results: None,
2589 filter_expr: String::new(),
2590 timeout: String::new(),
2591 runtime: String::new(),
2592 resource_ref: String::new(),
2593 sandbox: None,
2594 effects: None,
2595 parameters: Vec::new(),
2596 output_type: None,
2597 requires: Vec::new(),
2598 secret: String::new(),
2599 secret_partition: String::new(),
2600 target: None,
2601 risk: None,
2602 argv: Vec::new(),
2603 cache: String::new(),
2604 scrape: None,
2605 loc,
2606 leading_trivia: Vec::new(),
2607 trailing_trivia: Vec::new(),
2608 };
2609
2610 let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2617
2618 while !self.check(TokenType::RBrace) {
2619 let field_tok = self.current().clone();
2620 let field_name = field_tok.value.clone();
2621 self.advance();
2622 self.consume(TokenType::Colon)?;
2623
2624 match field_name.as_str() {
2625 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2626 "max_results" => {
2627 node.max_results = Some(
2628 self.consume(TokenType::Integer)?
2629 .value
2630 .parse::<i64>()
2631 .unwrap_or(0),
2632 )
2633 }
2634 "filter" => node.filter_expr = self.parse_filter_expression()?,
2635 "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2636 "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2637 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2641 "sandbox" => node.sandbox = Some(self.parse_bool()?),
2642 "effects" => node.effects = Some(self.parse_effect_row()?),
2643 "parameters" => node.parameters = self.parse_tool_param_schema()?,
2645 "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2646 "requires" => {
2652 let bracket_tok = self.current().clone();
2653 let items = self.parse_bracketed_dot_identifiers()?;
2654 for slug in &items {
2655 if !is_valid_capability_slug(slug) {
2656 return Err(ParseError {
2657 message: format!(
2658 "Invalid capability slug '{slug}' in tool '{}' \
2659 `requires:`. Scope slugs must match \
2660 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2661 grammar as `credential.grants`. Examples: \
2662 `w_organization_social`, `video.publish`.",
2663 node.name
2664 ),
2665 line: bracket_tok.line,
2666 column: bracket_tok.column,
2667 ..Default::default()
2668 });
2669 }
2670 }
2671 node.requires = items;
2672 }
2673 "secret" => node.secret = self.parse_dotted_identifier()?,
2677 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
2679 "secret_partition" => {
2685 node.secret_partition = self.consume_any_ident_or_kw()?.value
2686 }
2687 "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2689 "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2690 "argv" => node.argv = self.parse_bracketed_strings()?,
2694 "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2697 "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2700 _ => {
2701 unknown_fields.push((field_name, field_tok.line, field_tok.column));
2702 self.skip_value();
2703 }
2704 }
2705 }
2706 self.consume(TokenType::RBrace)?;
2707
2708 if node.target.is_some() || node.scrape.is_some() {
2716 if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2717 let (surface, valid) = if node.target.is_some() {
2718 (
2719 "technician tool",
2720 "provider, parameters, output_type, timeout, effects, target, risk, argv",
2721 )
2722 } else {
2723 (
2724 "web-acquisition tool",
2725 "provider, parameters, output_type, timeout, effects, secret, \
2726 secret_partition, cache, scrape",
2727 )
2728 };
2729 return Err(ParseError {
2730 message: format!(
2731 "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2732 strict field checking; valid fields: {valid}",
2733 node.name
2734 ),
2735 line,
2736 column,
2737 ..Default::default()
2738 });
2739 }
2740 }
2741 Ok(node)
2742 }
2743
2744 fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2749 let open = self.consume(TokenType::LBrace)?;
2750 let loc = self.loc_of(&open);
2751 let mut spec = crate::ast::ScrapeSpec {
2752 loc,
2753 ..Default::default()
2754 };
2755 while !self.check(TokenType::RBrace) {
2756 let field_tok = self.current().clone();
2757 let field_name = field_tok.value.clone();
2758 self.advance();
2759 self.consume(TokenType::Colon)?;
2760 match field_name.as_str() {
2761 "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2762 "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2763 "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2764 "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2765 "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2766 "extract" => spec.extract = self.parse_bracketed_strings()?,
2767 "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2768 "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2769 "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2770 "max_depth" => {
2771 spec.max_depth =
2772 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2773 }
2774 "max_pages" => {
2775 spec.max_pages =
2776 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2777 }
2778 "concurrency" => {
2779 spec.concurrency =
2780 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2781 }
2782 "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2783 "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2784 other => {
2785 return Err(self.error(&format!(
2786 "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2787 closed catalog; valid fields: engine, impersonate, \
2788 render_wait, proxy, respect_robots, extract, adaptive, \
2789 similarity_floor, follow, max_depth, max_pages, concurrency, \
2790 politeness, checkpoint"
2791 )));
2792 }
2793 }
2794 }
2795 self.consume(TokenType::RBrace)?;
2796 Ok(spec)
2797 }
2798
2799 fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2805 self.consume(TokenType::LBrace)?;
2806 let mut params = Vec::new();
2807 while !self.check(TokenType::RBrace) {
2808 let name = self.consume_any_ident_or_kw()?;
2812 let ploc = self.loc_of(&name);
2813 self.consume(TokenType::Colon)?;
2814 let type_expr = self.parse_type_expr()?;
2815 params.push(Parameter {
2816 name: name.value,
2817 type_expr,
2818 loc: ploc,
2819 });
2820 if self.check(TokenType::Comma) {
2821 self.advance();
2822 } else {
2823 break;
2824 }
2825 }
2826 self.consume(TokenType::RBrace)?;
2827 Ok(params)
2828 }
2829
2830 fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2831 let name = self.consume_any_ident_or_kw()?.value;
2832 if self.check(TokenType::LParen) {
2833 self.advance();
2834 let mut parts = vec![name, "(".to_string()];
2835 while !self.check(TokenType::RParen) {
2836 parts.push(self.advance().value.clone());
2837 }
2838 self.consume(TokenType::RParen)?;
2839 parts.push(")".to_string());
2840 Ok(parts.join(""))
2841 } else {
2842 Ok(name)
2843 }
2844 }
2845
2846 fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2847 let tok = self.consume(TokenType::Lt)?;
2848 let loc = self.loc_of(&tok);
2849 let mut effects = Vec::new();
2850 let mut epistemic_level = String::new();
2851
2852 while !self.check(TokenType::Gt) {
2853 let name = self.consume_any_ident_or_kw()?.value;
2854 if self.check(TokenType::Colon) {
2855 self.advance();
2856 let level = self.parse_qualifier_value()?;
2873 if name == "epistemic" {
2874 epistemic_level = level;
2875 } else {
2876 effects.push(format!("{name}:{level}"));
2877 }
2878 } else {
2879 effects.push(name);
2880 }
2881 if self.check(TokenType::Comma) {
2882 self.advance();
2883 }
2884 }
2885 self.consume(TokenType::Gt)?;
2886
2887 Ok(EffectRow {
2888 effects,
2889 epistemic_level,
2890 loc,
2891 })
2892 }
2893
2894 fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2903 let mut buf = self.consume_dotted_slug_segment()?;
2904 loop {
2905 let sep = if self.check(TokenType::Dot) {
2906 '.'
2907 } else if self.check(TokenType::Colon) {
2908 ':'
2909 } else {
2910 break;
2911 };
2912 self.advance();
2913 let part = self.consume_dotted_slug_segment()?;
2914 buf.push(sep);
2915 buf.push_str(&part);
2916 }
2917 Ok(buf)
2918 }
2919
2920 fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2932 let first = self.consume_any_ident_or_kw()?;
2933 let mut buf = first.value.clone();
2934 let mut next_line = first.line;
2935 let mut next_col = first.column + first.value.chars().count() as u32;
2936 loop {
2937 let cur = self.current();
2938 let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2939 if !is_segment_token {
2940 break;
2941 }
2942 if cur.line != next_line || cur.column != next_col {
2943 break;
2944 }
2945 buf.push_str(&cur.value);
2946 next_col = cur.column + cur.value.chars().count() as u32;
2947 next_line = cur.line;
2948 self.pos += 1;
2949 }
2950 Ok(buf)
2951 }
2952
2953 fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2956 let tok = self.consume(TokenType::Type)?;
2957 let loc = self.loc_of(&tok);
2958 let name = self.consume(TokenType::Identifier)?.value;
2959
2960 let mut node = TypeDefinition {
2961 name,
2962 fields: Vec::new(),
2963 range_constraint: None,
2964 where_clause: None,
2965 compliance: Vec::new(),
2966 loc: loc.clone(),
2967 leading_trivia: Vec::new(),
2968 trailing_trivia: Vec::new(),
2969 };
2970
2971 if self.check(TokenType::LParen) {
2973 self.advance();
2974 let min_val = self.consume_number()?;
2975 self.consume(TokenType::DotDot)?;
2976 let max_val = self.consume_number()?;
2977 self.consume(TokenType::RParen)?;
2978 node.range_constraint = Some(RangeConstraint {
2979 min_value: min_val,
2980 max_value: max_val,
2981 loc: loc.clone(),
2982 });
2983 }
2984
2985 if self.check(TokenType::Where) {
2987 self.advance();
2988 let mut expr_parts = Vec::new();
2989 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2990 if self.check(TokenType::Eof) {
2991 break;
2992 }
2993 expr_parts.push(self.advance().value.clone());
2994 }
2995 node.where_clause = Some(WhereClause {
2996 expression: expr_parts.join(" "),
2997 loc: loc.clone(),
2998 });
2999 }
3000
3001 if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3004 self.advance();
3005 node.compliance = self.parse_bracketed_identifiers()?;
3006 }
3007
3008 if self.check(TokenType::LBrace) {
3010 self.advance();
3011 while !self.check(TokenType::RBrace) {
3012 let field_name = self.consume(TokenType::Identifier)?;
3013 let field_loc = self.loc_of(&field_name);
3014 self.consume(TokenType::Colon)?;
3015 let type_expr = self.parse_type_expr()?;
3016 node.fields.push(TypeField {
3017 name: field_name.value,
3018 type_expr,
3019 loc: field_loc,
3020 });
3021 if self.check(TokenType::Comma) {
3022 self.advance();
3023 }
3024 }
3025 self.consume(TokenType::RBrace)?;
3026 }
3027
3028 Ok(node)
3029 }
3030
3031 fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3032 if self.check(TokenType::LBracket) {
3038 let open = self.current().clone();
3039 self.advance();
3040 let inner = self.parse_type_expr()?;
3041 self.consume(TokenType::RBracket)?;
3042 let mut optional = false;
3043 if self.check(TokenType::Question) {
3044 self.advance();
3045 optional = true;
3046 }
3047 return Ok(TypeExpr {
3048 name: "List".to_string(),
3049 generic_param: if inner.generic_param.is_empty() {
3050 inner.name
3051 } else {
3052 format!("{}<{}>", inner.name, inner.generic_param)
3053 },
3054 optional,
3055 loc: self.loc_of(&open),
3056 });
3057 }
3058 let name_tok = self.consume(TokenType::Identifier)?;
3059 let loc = self.loc_of(&name_tok);
3060 let mut generic_param = String::new();
3061 let mut optional = false;
3062
3063 if self.check(TokenType::Lt) {
3064 self.advance();
3065 let inner = self.parse_type_expr()?;
3075 generic_param = if inner.generic_param.is_empty() {
3076 inner.name
3077 } else {
3078 format!("{}<{}>", inner.name, inner.generic_param)
3079 };
3080 self.consume(TokenType::Gt)?;
3081 }
3082 if self.check(TokenType::LBracket) {
3086 self.advance();
3087 if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3088 generic_param = self.advance().value.clone();
3089 } else {
3090 let inner = self.parse_type_expr()?;
3091 generic_param = if inner.generic_param.is_empty() {
3092 inner.name
3093 } else {
3094 format!("{}[{}]", inner.name, inner.generic_param)
3095 };
3096 }
3097 self.consume(TokenType::RBracket)?;
3098 }
3099 if self.check(TokenType::Question) {
3100 self.advance();
3101 optional = true;
3102 }
3103
3104 Ok(TypeExpr {
3105 name: name_tok.value,
3106 generic_param,
3107 optional,
3108 loc,
3109 })
3110 }
3111
3112 fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3137 let expr = self.parse_type_expr()?;
3138 let mut s = expr.name;
3139 if !expr.generic_param.is_empty() {
3140 s.push('<');
3141 s.push_str(&expr.generic_param);
3142 s.push('>');
3143 }
3144 if expr.optional {
3145 s.push('?');
3146 }
3147 Ok(s)
3148 }
3149
3150 fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3153 let tok = self.consume(TokenType::Flow)?;
3154 let loc = self.loc_of(&tok);
3155 let name = self.consume(TokenType::Identifier)?.value;
3156
3157 self.consume(TokenType::LParen)?;
3158 let mut parameters = Vec::new();
3159 if !self.check(TokenType::RParen) {
3160 parameters = self.parse_param_list()?;
3161 }
3162 self.consume(TokenType::RParen)?;
3163
3164 let mut return_type = None;
3165 if self.check(TokenType::Arrow) {
3166 self.advance();
3167 return_type = Some(self.parse_type_expr()?);
3168 }
3169
3170 self.consume(TokenType::LBrace)?;
3171 let mut body = Vec::new();
3172 while !self.check(TokenType::RBrace) {
3173 body.push(self.parse_flow_step()?);
3174 }
3175 self.consume(TokenType::RBrace)?;
3176
3177 Ok(FlowDefinition {
3178 name,
3179 parameters,
3180 return_type,
3181 body,
3182 loc,
3183 leading_trivia: Vec::new(),
3184 trailing_trivia: Vec::new(),
3185 })
3186 }
3187
3188 fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3201 let tok = self.consume(TokenType::Effect)?;
3202 let loc = self.loc_of(&tok);
3203 let name = self.consume_any_ident_or_kw()?.value;
3204 self.consume(TokenType::LBrace)?;
3205
3206 let mut operations: Vec<EffectOperation> = Vec::new();
3207 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3208 let op_tok = self.current().clone();
3209 let op_name = self.consume_any_ident_or_kw()?.value;
3210
3211 self.consume(TokenType::LParen)?;
3212 let parameters = if self.check(TokenType::RParen) {
3213 Vec::new()
3214 } else {
3215 self.parse_param_list()?
3216 };
3217 self.consume(TokenType::RParen)?;
3218
3219 let mut return_type = String::new();
3222 if self.check(TokenType::Arrow) {
3223 self.advance();
3224 return_type = self.parse_type_expr()?.name;
3225 }
3226
3227 if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3232 return Err(ParseError {
3233 message: format!(
3234 "effect `{name}` declares operation `{op_name}` twice (first at \
3235 line {}); handler dispatch is by operation NAME, so a second \
3236 declaration would silently shadow the first",
3237 prior.loc.line
3238 ),
3239 line: op_tok.line,
3240 column: op_tok.column,
3241 ..Default::default()
3242 });
3243 }
3244
3245 operations.push(EffectOperation {
3246 name: op_name,
3247 parameters,
3248 return_type,
3249 loc: self.loc_of(&op_tok),
3250 });
3251 }
3252 self.consume(TokenType::RBrace)?;
3253
3254 Ok(EffectDefinition {
3255 name,
3256 operations,
3257 loc,
3258 leading_trivia: Vec::new(),
3259 trailing_trivia: Vec::new(),
3260 })
3261 }
3262
3263 fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3273 let tok = self.consume(TokenType::Handle)?;
3274 let loc = self.loc_of(&tok);
3275
3276 let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3278 while self.check(TokenType::Comma) {
3279 self.advance();
3280 effect_names.push(self.consume_any_ident_or_kw()?.value);
3281 }
3282
3283 self.consume(TokenType::LBrace)?;
3284 let mut clauses: Vec<HandlerClause> = Vec::new();
3285 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3286 let clause_tok = self.current().clone();
3287 let operation_name = self.consume_any_ident_or_kw()?.value;
3288
3289 self.consume(TokenType::LParen)?;
3293 let mut parameter_names = Vec::new();
3294 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3295 parameter_names.push(self.consume_any_ident_or_kw()?.value);
3296 if self.check(TokenType::Comma) {
3297 self.advance();
3298 }
3299 }
3300 self.consume(TokenType::RParen)?;
3301 self.consume(TokenType::Arrow)?;
3302 self.consume(TokenType::LBrace)?;
3303
3304 let mut body = Vec::new();
3305 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3306 body.push(self.parse_flow_step()?);
3307 }
3308 self.consume(TokenType::RBrace)?;
3309
3310 if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3311 return Err(ParseError {
3312 message: format!(
3313 "handler declares clause `{operation_name}` twice (first at line {}); \
3314 dispatch finds a clause by operation NAME and would always run the \
3315 first, leaving the second dead",
3316 prior.loc.line
3317 ),
3318 line: clause_tok.line,
3319 column: clause_tok.column,
3320 ..Default::default()
3321 });
3322 }
3323
3324 clauses.push(HandlerClause {
3325 operation_name,
3326 parameter_names,
3327 body,
3328 loc: self.loc_of(&clause_tok),
3329 });
3330 }
3331 self.consume(TokenType::RBrace)?;
3332
3333 let in_tok = self.current().clone();
3338 if !self.check(TokenType::In) {
3339 return Err(ParseError {
3340 message: format!(
3341 "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3342 DELIMITED (the design plan D3). Without the `in` block the frame has no \
3343 extent, so no `perform` could ever reach these clauses (got '{}')",
3344 effect_names.join(", "),
3345 in_tok.value
3346 ),
3347 line: in_tok.line,
3348 column: in_tok.column,
3349 ..Default::default()
3350 });
3351 }
3352 self.advance();
3353 self.consume(TokenType::LBrace)?;
3354 let mut body = Vec::new();
3355 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3356 body.push(self.parse_flow_step()?);
3357 }
3358 self.consume(TokenType::RBrace)?;
3359
3360 Ok(HandleBlock {
3361 effect_names,
3362 clauses,
3363 body,
3364 loc,
3365 })
3366 }
3367
3368 fn parse_effect_op_ref(
3377 &mut self,
3378 ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3379 let first = self.consume_any_ident_or_kw()?.value;
3380 let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3381 self.advance();
3382 (Some(first), self.consume_any_ident_or_kw()?.value)
3383 } else {
3384 (None, first)
3385 };
3386
3387 self.consume(TokenType::LParen)?;
3388 let mut arguments = Vec::new();
3389 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3390 arguments.push(self.parse_subject()?);
3393 if self.check(TokenType::Comma) {
3394 self.advance();
3395 }
3396 }
3397 self.consume(TokenType::RParen)?;
3398 Ok((effect_name, operation_name, arguments))
3399 }
3400
3401 fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3403 let tok = self.consume(TokenType::Perform)?;
3404 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3405 Ok(PerformStep {
3406 effect_name,
3407 operation_name,
3408 arguments,
3409 loc: self.loc_of(&tok),
3410 })
3411 }
3412
3413 fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3415 let tok = self.consume(TokenType::Forward)?;
3416 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3417 Ok(ForwardStep {
3418 effect_name,
3419 operation_name,
3420 arguments,
3421 loc: self.loc_of(&tok),
3422 })
3423 }
3424
3425 fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3427 self.consume(TokenType::LParen)?;
3428 let value = if self.check(TokenType::RParen) {
3429 String::new()
3430 } else {
3431 self.parse_subject()?
3432 };
3433 self.consume(TokenType::RParen)?;
3434 Ok(value)
3435 }
3436
3437 fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3438 let mut params = Vec::new();
3439
3440 let name = self.consume(TokenType::Identifier)?;
3441 let ploc = self.loc_of(&name);
3442 self.consume(TokenType::Colon)?;
3443 let type_expr = self.parse_type_expr()?;
3444 params.push(Parameter {
3445 name: name.value,
3446 type_expr,
3447 loc: ploc,
3448 });
3449
3450 while self.check(TokenType::Comma) {
3451 self.advance();
3452 let name = self.consume(TokenType::Identifier)?;
3453 let ploc = self.loc_of(&name);
3454 self.consume(TokenType::Colon)?;
3455 let type_expr = self.parse_type_expr()?;
3456 params.push(Parameter {
3457 name: name.value,
3458 type_expr,
3459 loc: ploc,
3460 });
3461 }
3462 Ok(params)
3463 }
3464
3465 fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3468 let tok = self.current().clone();
3469
3470 match tok.ttype {
3471 TokenType::Know | TokenType::Believe | TokenType::Speculate
3477 if self
3478 .tokens
3479 .get(self.pos + 1)
3480 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3481 {
3482 let block = self.parse_epistemic_block()?;
3483 self.hoisted.push(Declaration::Epistemic(block));
3484 self.parse_flow_step()
3485 }
3486 TokenType::Doubt
3487 if self
3488 .tokens
3489 .get(self.pos + 1)
3490 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3491 {
3492 let block = self.parse_epistemic_block()?;
3493 self.hoisted.push(Declaration::Epistemic(block));
3494 self.parse_flow_step()
3495 }
3496 TokenType::Step => self.parse_step().map(FlowStep::Step),
3497 TokenType::If => self.parse_if().map(FlowStep::If),
3498 TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3499 TokenType::Let => self.parse_let().map(FlowStep::Let),
3500 TokenType::Return => self.parse_return().map(FlowStep::Return),
3501 TokenType::Break => self.parse_break().map(FlowStep::Break),
3502 TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3503 TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3504
3505 TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3507 TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3511 TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3512 TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3513 TokenType::Weave => self.parse_weave_step(),
3514 TokenType::Use => self.parse_use_step(),
3515 TokenType::Remember => self.parse_remember_step(),
3516 TokenType::Recall => self.parse_recall_step(),
3517 TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3518 TokenType::Hibernate => self.parse_hibernate_step(),
3519 TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3520 TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3521 TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3522 TokenType::Focus => self.parse_focus_step(),
3523 TokenType::Grad => self.parse_grad_step(),
3524 TokenType::Associate => self.parse_associate_step(),
3525 TokenType::Aggregate => self.parse_aggregate_step(),
3526 TokenType::Explore => self.parse_explore_step(),
3527 TokenType::Ingest => self.parse_ingest_step(),
3528 TokenType::Shield => self.parse_apply_step("shield").map(|l| FlowStep::ShieldApply(ShieldApplyStep { shield_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3529 TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3535 TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3543 TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3544 TokenType::Resume => {
3545 let tok = self.consume(TokenType::Resume)?;
3546 let value_expr = self.parse_discharge_value()?;
3547 Ok(FlowStep::Resume(ResumeStep {
3548 value_expr,
3549 loc: self.loc_of(&tok),
3550 }))
3551 }
3552 TokenType::Abort => {
3553 let tok = self.consume(TokenType::Abort)?;
3554 let value_expr = self.parse_discharge_value()?;
3555 Ok(FlowStep::Abort(AbortStep {
3556 value_expr,
3557 loc: self.loc_of(&tok),
3558 }))
3559 }
3560 TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3561 TokenType::Navigate => self.parse_navigate_step(),
3562 TokenType::Drill => self.parse_drill_step(),
3563 TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3564 TokenType::Corroborate => self.parse_corroborate_step(),
3565 TokenType::Ots => self.parse_apply_step("ots").map(|l| FlowStep::OtsApply(OtsApplyStep { ots_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3566 TokenType::Mandate => self.parse_apply_step("mandate").map(|l| FlowStep::MandateApply(MandateApplyStep { mandate_name: l.1, target: l.2, output_type: l.3, loc: l.0 })),
3567 TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3572 TokenType::Listen => self.parse_listen_step(),
3573 TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3574 TokenType::Emit => self.parse_emit_step(),
3576 TokenType::Mint => self.parse_mint_step(),
3578 TokenType::Rotate => self.parse_rotate_step(),
3581 TokenType::Publish => self.parse_publish_step(),
3582 TokenType::Discover => self.parse_discover_step(),
3583 TokenType::Persist => self.parse_persist_step(),
3584 TokenType::Retrieve => self.parse_retrieve_step(),
3585 TokenType::Mutate => self.parse_mutate_step(),
3586 TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3587 TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3588 TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3590 TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3592 TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3594 TokenType::Run => self.parse_run().map(FlowStep::Run),
3598
3599 _ => {
3600 let hint = crate::smart_suggest::suggest_for(
3604 &tok.value,
3605 crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3606 );
3607 let base = format!(
3608 "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3609 tok.value
3610 );
3611 let message = if hint.is_empty() {
3612 base
3613 } else {
3614 format!("{base}. {hint}")
3615 };
3616 Err(ParseError {
3617 message,
3618 line: tok.line,
3619 column: tok.column,
3620 ..Default::default()
3621 })
3622 }
3623 }
3624 }
3625
3626 fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3629 let tok = self.consume(TokenType::Step)?;
3630 let loc = self.loc_of(&tok);
3631 let name = self.consume(TokenType::Identifier)?.value;
3632
3633 let mut persona_ref = String::new();
3634 if self.check(TokenType::Use) {
3635 self.advance();
3636 persona_ref = self.consume_any_ident_or_kw()?.value;
3637 }
3638
3639 self.consume(TokenType::LBrace)?;
3640
3641 let mut node = StepNode {
3642 name,
3643 persona_ref,
3644 given: String::new(),
3645 ask: String::new(),
3646 output_type: String::new(),
3647 confidence_floor: None,
3648 navigate_ref: String::new(),
3649 apply_ref: String::new(),
3650 requires_context: None,
3651 now_tz: None,
3652 guards: Vec::new(),
3653 pix_ops: Vec::new(),
3654 stream: None,
3655 performs: Vec::new(),
3656 loc,
3657 };
3658
3659 self.parse_step_body_into(&mut node)?;
3660 self.consume(TokenType::RBrace)?;
3661 Ok(node)
3662 }
3663
3664 fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3677 while !self.check(TokenType::RBrace) {
3678 let inner = self.current().clone();
3679
3680 match inner.ttype {
3681 TokenType::Given => {
3682 self.advance();
3683 self.consume(TokenType::Colon)?;
3684 node.given = self.parse_expression_string()?;
3685 }
3686 TokenType::Ask => {
3687 self.advance();
3688 self.consume(TokenType::Colon)?;
3689 node.ask = self.consume(TokenType::StringLit)?.value;
3690 }
3691 TokenType::Output => {
3692 self.advance();
3702 self.consume(TokenType::Colon)?;
3703 node.output_type = self.parse_output_type_string()?;
3704 }
3705 TokenType::Navigate
3712 if self
3713 .tokens
3714 .get(self.pos + 1)
3715 .is_some_and(|t| t.ttype != TokenType::Colon) =>
3716 {
3717 let op = self.parse_navigate_step()?;
3718 node.pix_ops.push(op);
3719 }
3720 TokenType::Drill => {
3721 let op = self.parse_drill_step()?;
3722 node.pix_ops.push(op);
3723 }
3724 TokenType::Trail => {
3725 let op = self
3726 .parse_flow_step_simple("trail")
3727 .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3728 node.pix_ops.push(op);
3729 }
3730 TokenType::Validate => {
3735 let tok = self.current().clone();
3736 self.advance();
3737 let target = self.parse_subject()?;
3739 let mut rule = String::new();
3740 if self.current().value == "against" {
3741 self.advance();
3742 self.consume(TokenType::Colon)?;
3743 rule = self.consume_any_ident_or_kw()?.value.clone();
3744 }
3745 node.pix_ops.push(FlowStep::Validate(ValidateStep {
3746 target,
3747 rule,
3748 guard: None,
3749 loc: Loc { line: tok.line, column: tok.column },
3750 }));
3751 }
3752 TokenType::If => {
3764 let tok = self.current().clone();
3765 self.advance();
3766
3767 let metric = self.consume_any_ident_or_kw()?;
3768 if metric.value != "confidence" {
3769 return Err(ParseError {
3770 message: format!(
3771 "step-body `if` is the confidence guard — `if confidence < \
3772 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3773 is its only metric (the CSR the preceding `validate … \
3774 against:` computes). Got '{}'. General branching belongs at \
3775 flow level: `if <cond> {{ … }}`.",
3776 metric.value
3777 ),
3778 line: metric.line,
3779 column: metric.column,
3780 ..Default::default()
3781 });
3782 }
3783
3784 let op = self.current().clone();
3785 if op.ttype != TokenType::Lt {
3786 return Err(ParseError {
3787 message: format!(
3788 "a confidence guard declares a FLOOR: `if confidence < \
3789 <threshold>`. `<` is the only comparison — the guard fires on \
3790 DEFICIENCY, and an inverted form would refine the outputs \
3791 that already conform. Got '{}'.",
3792 op.value
3793 ),
3794 line: op.line,
3795 column: op.column,
3796 ..Default::default()
3797 });
3798 }
3799 self.advance();
3800 let threshold = self.consume_number()?;
3801
3802 self.consume(TokenType::Arrow)?;
3803
3804 if !self.check(TokenType::Refine) {
3805 let bad = self.current().clone();
3806 return Err(ParseError {
3807 message: format!(
3808 "the guard's action catalog is CLOSED and `refine` is its only \
3809 member — the recovery the runtime actually performs (re-derive \
3810 the validated value with the violations as feedback, then \
3811 re-score). Got '{}'. An action name outside the catalog would \
3812 advertise a recovery nothing dispatches.",
3813 bad.value
3814 ),
3815 line: bad.line,
3816 column: bad.column,
3817 ..Default::default()
3818 });
3819 }
3820 self.advance();
3821 self.consume(TokenType::LParen)?;
3822 let key = self.consume_any_ident_or_kw()?;
3823 if key.value != "max_attempts" {
3824 return Err(ParseError {
3825 message: format!(
3826 "`refine` takes exactly `max_attempts: <n>` — the bound that \
3827 makes the recovery loop TERMINATE by construction. Got '{}'.",
3828 key.value
3829 ),
3830 line: key.line,
3831 column: key.column,
3832 ..Default::default()
3833 });
3834 }
3835 self.consume(TokenType::Colon)?;
3836 let attempts_tok = self.current().clone();
3837 if attempts_tok.ttype != TokenType::Integer {
3838 return Err(ParseError {
3839 message: format!(
3840 "`max_attempts:` must be a positive integer literal (got '{}')",
3841 attempts_tok.value
3842 ),
3843 line: attempts_tok.line,
3844 column: attempts_tok.column,
3845 ..Default::default()
3846 });
3847 }
3848 let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3849 message: format!("Invalid attempt count '{}'", attempts_tok.value),
3850 line: attempts_tok.line,
3851 column: attempts_tok.column,
3852 ..Default::default()
3853 })?;
3854 self.advance();
3855 self.consume(TokenType::RParen)?;
3856
3857 let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3864 FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3865 _ => None,
3866 });
3867 match attached {
3868 Some(v) => {
3869 if v.guard.is_some() {
3870 return Err(ParseError {
3871 message: "this validation already carries a confidence \
3872 guard; a second one would race the first over \
3873 the same score. One validation, one floor, one \
3874 recovery."
3875 .to_string(),
3876 line: tok.line,
3877 column: tok.column,
3878 ..Default::default()
3879 });
3880 }
3881 v.guard = Some(ConfidenceGuard {
3882 threshold,
3883 max_attempts,
3884 loc: Loc { line: tok.line, column: tok.column },
3885 });
3886 }
3887 None => {
3888 return Err(ParseError {
3889 message: "`if confidence` reads the CSR of a preceding \
3890 `validate … against: <Schema>` in this step body, \
3891 and none exists. A `validate` without `against:` \
3892 computes no score (there is no schema to score \
3893 with), so it cannot carry a guard either."
3894 .to_string(),
3895 line: tok.line,
3896 column: tok.column,
3897 ..Default::default()
3898 });
3899 }
3900 }
3901 }
3902 TokenType::Navigate => {
3903 self.advance();
3904 self.consume(TokenType::Colon)?;
3905 node.navigate_ref = self.parse_dotted_identifier()?;
3906 }
3907 TokenType::Identifier if inner.value == "confidence_floor" => {
3908 self.advance();
3909 self.consume(TokenType::Colon)?;
3910 node.confidence_floor = Some(self.consume_number()?);
3911 }
3912 TokenType::Identifier if inner.value == "apply" => {
3913 self.advance();
3914 self.consume(TokenType::Colon)?;
3915 node.apply_ref = self.consume_any_ident_or_kw()?.value;
3916 }
3917 TokenType::Identifier if inner.value == "requires_context" => {
3925 self.advance();
3926 self.consume(TokenType::Colon)?;
3927 let num = self.current().clone();
3928 let bad = |tok: &crate::tokens::Token| ParseError {
3929 message: format!(
3930 "`requires_context:` must be a positive integer token count \
3931 (got '{}')",
3932 tok.value
3933 ),
3934 line: tok.line,
3935 column: tok.column,
3936 ..Default::default()
3937 };
3938 if num.ttype != TokenType::Integer {
3939 return Err(bad(&num));
3940 }
3941 let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3942 self.advance();
3943 node.requires_context = Some(value);
3944 }
3945 TokenType::Identifier if inner.value == "now" => {
3950 self.advance();
3951 self.consume(TokenType::Colon)?;
3952 let tz = self.current().clone();
3953 if tz.ttype != TokenType::StringLit {
3954 return Err(ParseError {
3955 message: format!(
3956 "`now:` must be an IANA timezone string literal like \
3957 \"America/Bogota\" or \"UTC\" (got '{}')",
3958 tz.value
3959 ),
3960 line: tz.line,
3961 column: tz.column,
3962 ..Default::default()
3963 });
3964 }
3965 self.advance();
3966 node.now_tz = Some(tz.value);
3967 }
3968 TokenType::Use => {
3978 let tool = self
3979 .tokens
3980 .get(self.pos + 1)
3981 .map(|t| t.value.as_str())
3982 .filter(|v| !v.is_empty())
3983 .unwrap_or("<Tool>");
3984 return Err(ParseError {
3985 message: format!(
3986 "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3987 would be silently dropped. To invoke a tool, either write the \
3988 flow-level step `use {tool} on <arg>` (outside this block), or bind \
3989 it inside this step with `apply: {tool}`. To attach a persona, put \
3990 it in the step header: `step <name> use <Persona> {{ … }}`."
3991 ),
3992 line: inner.line,
3993 column: inner.column,
3994 ..Default::default()
3995 });
3996 }
3997 TokenType::Mandate => {
4008 let g = self.parse_step_guard("mandate")?;
4009 node.guards.push(g);
4010 }
4011 TokenType::Shield => {
4012 let g = self.parse_step_guard("shield")?;
4013 node.guards.push(g);
4014 }
4015 TokenType::Ots => {
4016 let g = self.parse_step_guard("ots")?;
4017 node.guards.push(g);
4018 }
4019 TokenType::Lambda => {
4026 let g = self.parse_step_guard("lambda")?;
4027 node.guards.push(g);
4028 }
4029 TokenType::Probe
4037 if self
4038 .tokens
4039 .get(self.pos + 1)
4040 .is_some_and(|t| t.ttype != TokenType::Colon) =>
4041 {
4042 let tok = self.current().clone();
4043 self.advance();
4044 let target = self.parse_subject()?;
4047 let mut fields = Vec::new();
4048 if self.check(TokenType::For) {
4049 self.advance();
4050 self.consume(TokenType::LBracket)?;
4051 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4052 fields.push(self.consume_any_ident_or_kw()?.value.clone());
4053 if self.check(TokenType::Comma) {
4054 self.advance();
4055 }
4056 }
4057 self.consume(TokenType::RBracket)?;
4058 }
4059 node.pix_ops.push(FlowStep::Probe(ProbeStep {
4060 target,
4061 fields,
4062 loc: Loc { line: tok.line, column: tok.column },
4063 }));
4064 }
4065 TokenType::Identifier if inner.value == "use_tool" => {
4072 let tok = self.current().clone();
4073 self.advance();
4074 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4075 let args = if self.current().value == "with" {
4076 self.advance();
4077 let mut named: Vec<(String, String, String)> = Vec::new();
4078 loop {
4079 let k = self.consume_any_ident_or_kw()?.value.clone();
4080 self.consume(TokenType::Colon)?;
4081 let kind = if self.check(TokenType::StringLit) {
4085 "literal"
4086 } else {
4087 "reference"
4088 };
4089 let v = self.parse_expression_string()?;
4090 named.push((k, v, kind.to_string()));
4091 if self.check(TokenType::Comma) {
4092 self.advance();
4093 } else {
4094 break;
4095 }
4096 }
4097 UseArgs::Named(named)
4098 } else if self.current().value == "on" {
4099 self.advance();
4100 UseArgs::LegacyPositional(
4101 self.consume_any_ident_or_kw()?.value.clone(),
4102 )
4103 } else {
4104 UseArgs::LegacyPositional(String::new())
4105 };
4106 node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4107 tool_name,
4108 args,
4109 loc: Loc { line: tok.line, column: tok.column },
4110 }));
4111 }
4112 TokenType::Par => {
4114 let block = self.parse_par_block()?;
4115 node.pix_ops.push(FlowStep::Par(block));
4116 }
4117 TokenType::Reason
4139 if self
4140 .tokens
4141 .get(self.pos + 1)
4142 .is_some_and(|t| t.ttype == TokenType::Colon) =>
4143 {
4144 let tok = self.current().clone();
4145 self.advance();
4146 self.consume(TokenType::Colon)?;
4147 let mut r = ReasonStep {
4148 strategy: String::new(),
4149 target: String::new(),
4150 given: String::new(),
4151 ask: String::new(),
4152 depth: None,
4153 loc: self.loc_of(&tok),
4154 };
4155 if self.check(TokenType::StringLit) {
4156 r.ask = self.consume(TokenType::StringLit)?.value;
4157 } else {
4158 r.target = self.parse_dotted_identifier()?;
4159 }
4160 node.pix_ops.push(FlowStep::Reason(r));
4161 }
4162 TokenType::Reason => {
4163 let r = self.parse_reason_step()?;
4164 node.pix_ops.push(FlowStep::Reason(r));
4165 }
4166 TokenType::Weave => {
4175 let w = self.parse_weave_step()?;
4176 node.pix_ops.push(w);
4177 }
4178 TokenType::Identifier
4188 if self
4189 .tokens
4190 .get(self.pos + 1)
4191 .is_some_and(|t| t.ttype == TokenType::LParen) =>
4192 {
4193 let tok = self.current().clone();
4194 let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4195 self.consume(TokenType::LParen)?;
4196 let mut arguments = Vec::new();
4197 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4198 arguments.push(self.parse_subject()?);
4199 if self.check(TokenType::Comma) {
4200 self.advance();
4201 }
4202 }
4203 self.consume(TokenType::RParen)?;
4204 node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4205 agent_name,
4206 arguments,
4207 loc: self.loc_of(&tok),
4208 }));
4209 }
4210 TokenType::Retrieve => {
4220 let r = self.parse_retrieve_step()?;
4221 node.pix_ops.push(r);
4222 }
4223 TokenType::Stream => {
4241 let sb = self.parse_stream_block()?;
4242 if node.stream.is_some() {
4243 return Err(ParseError {
4244 message:
4245 "step declares two `stream` blocks; a step has one output stream, \
4246 and composing two has no defined meaning (which one is the \
4247 step's output?). Refused rather than silently keeping the last."
4248 .to_string(),
4249 line: inner.line,
4250 column: inner.column,
4251 ..Default::default()
4252 });
4253 }
4254 node.stream = Some(Box::new(sb));
4255 }
4256 TokenType::Perform => {
4273 let p = self.parse_perform_step()?;
4274 node.performs.push(p);
4275 }
4276 TokenType::Probe => {
4280 self.skip_flow_step_structural()?;
4281 }
4282 _ => {
4283 return Err(ParseError {
4284 message: format!(
4285 "Unexpected token in step body: '{}' — expected given, ask, \
4286 probe, reason, weave, stream, perform, output, confidence_floor, \
4287 navigate, apply, requires_context, now",
4288 inner.value
4289 ),
4290 line: inner.line,
4291 column: inner.column,
4292 ..Default::default()
4293 });
4294 }
4295 }
4296 }
4297 Ok(())
4298 }
4299
4300 fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4302 self.advance();
4304 while !self.check(TokenType::LBrace)
4306 && !self.check(TokenType::RBrace)
4307 && !self.check(TokenType::Eof)
4308 {
4309 let tt = &self.current().ttype;
4311 if matches!(
4312 tt,
4313 TokenType::Step
4314 | TokenType::Given
4315 | TokenType::Ask
4316 | TokenType::Output
4317 | TokenType::Navigate
4318 | TokenType::Use
4319 | TokenType::Probe
4320 | TokenType::Reason
4321 | TokenType::Weave
4322 | TokenType::Stream
4323 | TokenType::If
4324 | TokenType::For
4325 | TokenType::Let
4326 | TokenType::Return
4327 ) {
4328 return Ok(());
4329 }
4330 self.advance();
4331 }
4332 if self.check(TokenType::LBrace) {
4334 self.skip_braced_block()?;
4335 }
4336 Ok(())
4337 }
4338
4339 fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4342 let tok = self.consume(TokenType::Intent)?;
4343 let loc = self.loc_of(&tok);
4344 let name = self.consume(TokenType::Identifier)?.value;
4345 self.consume(TokenType::LBrace)?;
4346
4347 let mut node = IntentNode {
4348 name,
4349 given: String::new(),
4350 ask: String::new(),
4351 output_type: None,
4352 confidence_floor: None,
4353 loc,
4354 leading_trivia: Vec::new(),
4355 trailing_trivia: Vec::new(),
4356 };
4357
4358 while !self.check(TokenType::RBrace) {
4359 let field_name = self.current().value.clone();
4360 self.advance();
4361 self.consume(TokenType::Colon)?;
4362
4363 match field_name.as_str() {
4364 "given" => node.given = self.consume(TokenType::Identifier)?.value,
4365 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4366 "output" => node.output_type = Some(self.parse_type_expr()?),
4367 "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4368 _ => self.skip_value(),
4369 }
4370 }
4371 self.consume(TokenType::RBrace)?;
4372 Ok(node)
4373 }
4374
4375 fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4378 let tok = self.consume(TokenType::Run)?;
4379 let loc = self.loc_of(&tok);
4380 let flow_name = self.consume(TokenType::Identifier)?.value;
4381
4382 self.consume(TokenType::LParen)?;
4383 let mut arguments = Vec::new();
4384 if !self.check(TokenType::RParen) {
4385 arguments = self.parse_argument_list()?;
4386 }
4387 self.consume(TokenType::RParen)?;
4388
4389 let mut node = RunStatement {
4390 flow_name,
4391 arguments,
4392 persona: String::new(),
4393 context: String::new(),
4394 anchors: Vec::new(),
4395 on_failure: String::new(),
4396 on_failure_params: Vec::new(),
4397 output_to: String::new(),
4398 effort: String::new(),
4399 loc,
4400 leading_trivia: Vec::new(),
4401 trailing_trivia: Vec::new(),
4402 };
4403
4404 while self.check_run_modifier() {
4405 let mod_tok = self.current().clone();
4406 if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4408 self.advance();
4409 node.persona = self.consume(TokenType::Identifier)?.value;
4410 continue;
4411 }
4412 match mod_tok.ttype {
4413 TokenType::As => {
4414 self.advance();
4415 node.persona = self.consume(TokenType::Identifier)?.value;
4416 }
4417 TokenType::Within => {
4418 self.advance();
4419 node.context = self.consume(TokenType::Identifier)?.value;
4420 }
4421 TokenType::ConstrainedBy => {
4422 self.advance();
4423 node.anchors = self.parse_bracketed_identifiers()?;
4424 }
4425 TokenType::OnFailure => {
4426 self.advance();
4427 self.consume(TokenType::Colon)?;
4428 node.on_failure = self.consume_any_ident_or_kw()?.value;
4429 if self.check(TokenType::LParen) {
4431 self.advance();
4432 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4433 let key = self.consume_any_ident_or_kw()?.value;
4434 self.consume(TokenType::Colon)?;
4435 let val = self.consume_any_ident_or_kw()?.value;
4436 node.on_failure_params.push((key, val));
4437 if self.check(TokenType::Comma) {
4438 self.advance();
4439 }
4440 }
4441 if self.check(TokenType::RParen) {
4442 self.advance();
4443 }
4444 }
4445 }
4446 TokenType::OutputTo => {
4447 self.advance();
4448 self.consume(TokenType::Colon)?;
4449 node.output_to = self.consume(TokenType::StringLit)?.value;
4450 }
4451 TokenType::Effort => {
4452 self.advance();
4453 self.consume(TokenType::Colon)?;
4454 node.effort = self.consume_any_ident_or_kw()?.value;
4455 }
4456 _ => break,
4457 }
4458 }
4459
4460 Ok(node)
4461 }
4462
4463 fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4466 let tok = self.current().clone();
4467 let mode = match tok.ttype {
4468 TokenType::Know => "know",
4469 TokenType::Believe => "believe",
4470 TokenType::Speculate => "speculate",
4471 TokenType::Doubt => "doubt",
4472 _ => unreachable!(),
4473 };
4474 self.advance();
4475 let loc = self.loc_of(&tok);
4476
4477 self.consume(TokenType::LBrace)?;
4478 let mut body = Vec::new();
4479 while !self.check(TokenType::RBrace) {
4480 body.push(self.parse_declaration()?);
4481 }
4482 self.consume(TokenType::RBrace)?;
4483
4484 Ok(EpistemicBlock {
4485 mode: mode.to_string(),
4486 body,
4487 loc,
4488 leading_trivia: Vec::new(),
4489 trailing_trivia: Vec::new(),
4490 })
4491 }
4492
4493 fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4501 self.parse_expr_bp(0)
4502 }
4503
4504 fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4505 let mut lhs = match self.current().ttype {
4508 TokenType::Minus => {
4509 self.advance();
4510 Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4511 }
4512 TokenType::Not => {
4513 self.advance();
4514 Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4515 }
4516 _ => self.parse_postfix()?,
4517 };
4518 while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4520 if lbp < min_bp {
4521 break;
4522 }
4523 self.advance();
4524 let rhs = self.parse_expr_bp(lbp + 1)?;
4525 lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4526 }
4527 Ok(lhs)
4528 }
4529
4530 fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4533 Some(match t {
4534 TokenType::Or => (BinOp::Or, 1),
4535 TokenType::And => (BinOp::And, 2),
4536 TokenType::Eq => (BinOp::Eq, 3),
4537 TokenType::Neq => (BinOp::Ne, 3),
4538 TokenType::Lt => (BinOp::Lt, 3),
4539 TokenType::Lte => (BinOp::Le, 3),
4540 TokenType::Gt => (BinOp::Gt, 3),
4541 TokenType::Gte => (BinOp::Ge, 3),
4542 TokenType::Plus => (BinOp::Add, 4),
4543 TokenType::Minus => (BinOp::Sub, 4),
4544 TokenType::Star => (BinOp::Mul, 5),
4545 TokenType::Slash => (BinOp::Div, 5),
4546 TokenType::Percent => (BinOp::Mod, 5),
4547 _ => return None,
4548 })
4549 }
4550
4551 fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4557 let mut expr = self.parse_expr_atom()?;
4558 loop {
4559 if self.check(TokenType::Dot) {
4560 self.advance();
4561 let name = self.consume_any_ident_or_kw()?.value;
4562 if let Some(builtin) = Builtin::from_name(&name) {
4563 let mut args = vec![expr];
4564 if self.check(TokenType::LParen) {
4565 self.advance();
4566 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4567 args.push(self.parse_expr_bp(0)?);
4568 if self.check(TokenType::Comma) {
4569 self.advance();
4570 } else {
4571 break;
4572 }
4573 }
4574 self.consume(TokenType::RParen)?;
4575 }
4576 expr = Expr::Call(builtin, args);
4577 } else {
4578 expr = match expr {
4582 Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4583 other => Expr::Field(Box::new(other), name),
4584 };
4585 }
4586 } else if self.check(TokenType::LBracket) {
4587 self.advance();
4589 let index = self.parse_expr_bp(0)?;
4590 self.consume(TokenType::RBracket)?;
4591 expr = Expr::Index(Box::new(expr), Box::new(index));
4592 } else {
4593 break;
4594 }
4595 }
4596 Ok(expr)
4597 }
4598
4599 fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4600 let tok = self.current().clone();
4601 match tok.ttype {
4602 TokenType::Integer => {
4603 self.advance();
4604 let lit = tok
4605 .value
4606 .parse::<i64>()
4607 .map(ExprLit::Int)
4608 .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4609 .map_err(|_| ParseError {
4610 message: format!("invalid integer literal '{}'", tok.value),
4611 line: tok.line,
4612 column: tok.column,
4613 ..Default::default()
4614 })?;
4615 Ok(Expr::Lit(lit))
4616 }
4617 TokenType::Float => {
4618 self.advance();
4619 let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4620 message: format!("invalid float literal '{}'", tok.value),
4621 line: tok.line,
4622 column: tok.column,
4623 ..Default::default()
4624 })?;
4625 Ok(Expr::Lit(ExprLit::Float(f)))
4626 }
4627 TokenType::Bool => {
4628 self.advance();
4629 Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4630 }
4631 TokenType::StringLit => {
4632 self.advance();
4633 Ok(Expr::Lit(ExprLit::Str(tok.value)))
4634 }
4635 TokenType::LParen => {
4636 self.advance();
4637 let inner = self.parse_expr_bp(0)?;
4638 self.consume(TokenType::RParen)?;
4639 Ok(inner)
4640 }
4641 _ => {
4642 Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4646 }
4647 }
4648 }
4649
4650 fn expr_lit_surface(lit: &ExprLit) -> String {
4655 match lit {
4656 ExprLit::Int(i) => i.to_string(),
4657 ExprLit::Float(f) => f.to_string(),
4658 ExprLit::Bool(b) => b.to_string(),
4659 ExprLit::Str(s) => s.clone(),
4660 }
4661 }
4662
4663 fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4664 match expr {
4665 Expr::Ref(p) => Some(p.clone()),
4666 Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4667 _ => None,
4668 }
4669 }
4670
4671 fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4675 match expr {
4676 Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4677 Expr::Binary(op, l, r) => {
4678 let op_s = match op {
4679 BinOp::Eq => "==",
4680 BinOp::Ne => "!=",
4681 BinOp::Lt => "<",
4682 BinOp::Le => "<=",
4683 BinOp::Gt => ">",
4684 BinOp::Ge => ">=",
4685 _ => return None,
4686 };
4687 let lhs = match &**l {
4688 Expr::Ref(p) => p.clone(),
4689 _ => return None,
4690 };
4691 let rhs = Self::expr_leaf_surface(r)?;
4692 Some((lhs, op_s.to_string(), rhs))
4693 }
4694 _ => None,
4695 }
4696 }
4697
4698 fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4701 match expr {
4702 Expr::Binary(BinOp::Or, l, r) => {
4703 Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4704 }
4705 _ => match Self::expr_legacy_leaf(expr) {
4706 Some(t) => {
4707 out.push(t);
4708 true
4709 }
4710 None => false,
4711 },
4712 }
4713 }
4714
4715 #[allow(clippy::type_complexity)]
4721 fn cond_as_legacy(
4722 expr: &Expr,
4723 ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4724 let mut leaves = Vec::new();
4725 if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4726 return None;
4727 }
4728 let (c0, o0, v0) = leaves[0].clone();
4729 let rest = leaves[1..].to_vec();
4730 let conjunctor = if rest.is_empty() {
4731 String::new()
4732 } else {
4733 "or".to_string()
4734 };
4735 Some((c0, o0, v0, rest, conjunctor))
4736 }
4737
4738 fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4739 let tok = self.consume(TokenType::If)?;
4740 let loc = self.loc_of(&tok);
4741
4742 let expr = self.parse_expr()?;
4747 let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4748 match Self::cond_as_legacy(&expr) {
4749 Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4750 None => (
4751 String::new(),
4752 String::new(),
4753 String::new(),
4754 Vec::new(),
4755 String::new(),
4756 Some(expr),
4757 ),
4758 };
4759
4760 let mut then_body = Vec::new();
4761 let mut else_body = Vec::new();
4762
4763 if self.check(TokenType::Arrow) {
4765 self.advance();
4766 then_body.push(self.parse_flow_step()?);
4767 } else if self.check(TokenType::LBrace) {
4768 self.advance();
4769 while !self.check(TokenType::RBrace) {
4770 then_body.push(self.parse_flow_step()?);
4771 }
4772 self.consume(TokenType::RBrace)?;
4773 }
4774
4775 if self.check(TokenType::Else) {
4777 self.advance();
4778 if self.check(TokenType::Arrow) {
4779 self.advance();
4780 else_body.push(self.parse_flow_step()?);
4781 } else if self.check(TokenType::LBrace) {
4782 self.advance();
4783 while !self.check(TokenType::RBrace) {
4784 else_body.push(self.parse_flow_step()?);
4785 }
4786 self.consume(TokenType::RBrace)?;
4787 }
4788 }
4789
4790 Ok(ConditionalNode {
4791 condition,
4792 comparison_op,
4793 comparison_value,
4794 then_body,
4795 else_body,
4796 conditions,
4797 conjunctor,
4798 cond,
4799 loc,
4800 })
4801 }
4802
4803 fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4806 let tok = self.consume(TokenType::For)?;
4807 let loc = self.loc_of(&tok);
4808 let variable = self.consume(TokenType::Identifier)?.value;
4809 self.consume(TokenType::In)?;
4810 let iterable = self.parse_dotted_identifier()?;
4811
4812 self.consume(TokenType::LBrace)?;
4813 self.loop_depth += 1;
4820 let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4821 let mut body = Vec::new();
4822 while !self.check(TokenType::RBrace) {
4823 body.push(self.parse_flow_step()?);
4824 }
4825 Ok(body)
4826 })();
4827 self.loop_depth -= 1;
4828 let body = body_result?;
4829 self.consume(TokenType::RBrace)?;
4830
4831 Ok(ForInStatement {
4832 variable,
4833 iterable,
4834 body,
4835 loc,
4836 })
4837 }
4838
4839 fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4842 let tok = self.consume(TokenType::Break)?;
4843 let loc = self.loc_of(&tok);
4844 if self.loop_depth == 0 {
4845 return Err(ParseError {
4846 message: "'break' outside of a for-in loop body".to_string(),
4847 line: tok.line,
4848 column: tok.column,
4849 ..Default::default()
4850 });
4851 }
4852 Ok(BreakStatement { loc })
4853 }
4854
4855 fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4858 let tok = self.consume(TokenType::Continue)?;
4859 let loc = self.loc_of(&tok);
4860 if self.loop_depth == 0 {
4861 return Err(ParseError {
4862 message: "'continue' outside of a for-in loop body".to_string(),
4863 line: tok.line,
4864 column: tok.column,
4865 ..Default::default()
4866 });
4867 }
4868 Ok(ContinueStatement { loc })
4869 }
4870
4871 fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4874 let tok = self.consume(TokenType::Let)?;
4875 let loc = self.loc_of(&tok);
4876
4877 let name = self.consume_any_ident_or_kw()?.value;
4879 let type_annotation = if self.check(TokenType::Colon) {
4881 self.advance();
4882 Some(self.parse_type_expr()?)
4883 } else {
4884 None
4885 };
4886 self.consume(TokenType::Assign)?;
4887 self.last_let_value_kind = "literal".to_string();
4890 let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4891
4892 Ok(LetStatement {
4893 identifier: name,
4894 value_expr: value,
4895 value_kind: self.last_let_value_kind.clone(),
4896 type_annotation,
4897 value_ast,
4898 loc,
4899 leading_trivia: Vec::new(),
4900 trailing_trivia: Vec::new(),
4901 })
4902 }
4903
4904 fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4905 let atom = self.parse_let_atom()?;
4906
4907 if matches!(
4909 self.current().ttype,
4910 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4911 ) {
4912 let mut parts = vec![atom];
4913 while matches!(
4914 self.current().ttype,
4915 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4916 ) {
4917 parts.push(self.advance().value.clone());
4918 parts.push(self.parse_let_atom()?);
4919 }
4920 self.last_let_value_kind = "expression".to_string();
4921 return Ok(parts.join(" "));
4922 }
4923 Ok(atom)
4924 }
4925
4926 fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4936 if self.check(TokenType::LBracket) {
4937 self.last_let_value_kind = "literal".to_string();
4938 return Ok((self.parse_let_list_literal()?, None));
4939 }
4940 let expr = self.parse_expr()?;
4941 Ok(match expr {
4942 Expr::Lit(lit) => {
4943 self.last_let_value_kind = "literal".to_string();
4944 (Self::expr_lit_surface(&lit), None)
4945 }
4946 Expr::Ref(p) => {
4947 self.last_let_value_kind = "reference".to_string();
4948 (p, None)
4949 }
4950 other => {
4951 self.last_let_value_kind = "expression".to_string();
4952 (Self::render_expr(&other), Some(other))
4953 }
4954 })
4955 }
4956
4957 fn render_expr(e: &Expr) -> String {
4960 match e {
4961 Expr::Lit(l) => Self::expr_lit_surface(l),
4962 Expr::Ref(p) => p.clone(),
4963 Expr::Let { name, value, body } => format!(
4967 "let {name} = {} in {}",
4968 Self::render_expr(value),
4969 Self::render_expr(body)
4970 ),
4971 Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4972 Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4973 Expr::Binary(op, l, r) => {
4974 let sym = match op {
4975 BinOp::Add => "+",
4976 BinOp::Sub => "-",
4977 BinOp::Mul => "*",
4978 BinOp::Div => "/",
4979 BinOp::Mod => "%",
4980 BinOp::Eq => "==",
4981 BinOp::Ne => "!=",
4982 BinOp::Lt => "<",
4983 BinOp::Le => "<=",
4984 BinOp::Gt => ">",
4985 BinOp::Ge => ">=",
4986 BinOp::And => "and",
4987 BinOp::Or => "or",
4988 };
4989 format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4990 }
4991 Expr::Call(b, args) => {
4992 let recv = args.first().map(Self::render_expr).unwrap_or_default();
4993 let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4994 if rest.is_empty() {
4995 format!("{recv}.{}", b.surface())
4996 } else {
4997 format!("{recv}.{}({})", b.surface(), rest.join(", "))
4998 }
4999 }
5000 Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
5001 Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
5002 }
5003 }
5004
5005 fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5006 let tok = self.current().clone();
5007
5008 match tok.ttype {
5009 TokenType::StringLit => {
5010 self.last_let_value_kind = "literal".to_string();
5011 self.advance();
5012 Ok(tok.value)
5013 }
5014 TokenType::Integer | TokenType::Float => {
5015 self.last_let_value_kind = "literal".to_string();
5016 self.advance();
5017 Ok(tok.value)
5018 }
5019 TokenType::Bool => {
5020 self.last_let_value_kind = "literal".to_string();
5021 self.advance();
5022 Ok(tok.value)
5023 }
5024 TokenType::Identifier => {
5025 self.last_let_value_kind = "reference".to_string();
5026 self.parse_dotted_identifier()
5027 }
5028 TokenType::LBracket => {
5029 self.last_let_value_kind = "literal".to_string();
5030 self.parse_let_list_literal()
5031 }
5032 _ => {
5033 if self.pos + 1 < self.tokens.len()
5035 && self.tokens[self.pos + 1].ttype == TokenType::Dot
5036 {
5037 self.last_let_value_kind = "reference".to_string();
5038 return self.parse_dotted_identifier();
5039 }
5040 Err(ParseError {
5041 message: format!(
5042 "Expected value expression, found {:?}('{}')",
5043 tok.ttype, tok.value
5044 ),
5045 line: tok.line,
5046 column: tok.column,
5047 ..Default::default()
5048 })
5049 }
5050 }
5051 }
5052
5053 fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5054 self.consume(TokenType::LBracket)?;
5055 let mut items = Vec::new();
5056 if !self.check(TokenType::RBracket) {
5057 items.push(self.parse_let_value_expr()?);
5058 while self.check(TokenType::Comma) {
5059 self.advance();
5060 if self.check(TokenType::RBracket) {
5061 break; }
5063 items.push(self.parse_let_value_expr()?);
5064 }
5065 }
5066 self.consume(TokenType::RBracket)?;
5067 Ok(format!("[{}]", items.join(", ")))
5068 }
5069
5070 fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5073 let tok = self.consume(TokenType::Return)?;
5074 let loc = self.loc_of(&tok);
5075 let value = self.parse_let_value_expr()?;
5076 Ok(ReturnStatement {
5077 value_expr: value,
5078 loc,
5079 })
5080 }
5081
5082 fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5086 let tok = self.current().clone();
5087 self.advance(); let target = if self.at_declaration_start()
5089 || self.check(TokenType::RBrace)
5090 || self.check(TokenType::Eof)
5091 {
5092 String::new()
5093 } else {
5094 self.consume_any_ident_or_kw()?.value.clone()
5095 };
5096 if self.check(TokenType::LBrace) {
5098 self.skip_braced_block()?;
5099 }
5100 Ok((
5101 Loc {
5102 line: tok.line,
5103 column: tok.column,
5104 },
5105 target,
5106 ))
5107 }
5108
5109 fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5120 let tok = self.current().clone();
5121 let loc = self.loc_of(&tok);
5122 self.advance(); let mut chunk_type = String::new();
5130 if self.check(TokenType::Lt) {
5131 self.advance();
5132 let inner = self.parse_type_expr()?;
5133 chunk_type = if inner.generic_param.is_empty() {
5134 inner.name
5135 } else {
5136 format!("{}<{}>", inner.name, inner.generic_param)
5137 };
5138 self.consume(TokenType::Gt)?;
5139 }
5140
5141 while !self.check(TokenType::LBrace)
5145 && !self.check(TokenType::RBrace)
5146 && !self.check(TokenType::Eof)
5147 && !self.at_declaration_start()
5148 {
5149 self.advance();
5150 }
5151
5152 let mut block = StreamBlock {
5153 chunk_type,
5154 on_chunk: None,
5155 on_complete: None,
5156 on_error: None,
5157 body: Vec::new(),
5158 loc,
5159 };
5160
5161 if self.check(TokenType::LBrace) {
5162 self.advance();
5163 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5164 let name = self.current().value.clone();
5170 let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5171 && self
5172 .tokens
5173 .get(self.pos + 1)
5174 .is_some_and(|t| t.ttype == TokenType::Colon);
5175 if is_arm {
5176 let arm_tok = self.current().clone();
5177 self.advance(); self.advance(); let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5180 let slot = match name.as_str() {
5181 "on_chunk" => &mut block.on_chunk,
5182 "on_complete" => &mut block.on_complete,
5183 _ => &mut block.on_error,
5184 };
5185 if slot.is_some() {
5186 return Err(ParseError {
5187 message: format!(
5188 "`{name}` is declared twice in this `stream` block. Two handlers \
5189 for one edge have no defined composition (whose output is the \
5190 stream's?), so the duplicate is refused rather than silently \
5191 overwriting the first."
5192 ),
5193 line: arm_tok.line,
5194 column: arm_tok.column,
5195 ..Default::default()
5196 });
5197 }
5198 *slot = Some(arm);
5199 continue;
5200 }
5201
5202 let next_two_are_block = self
5209 .tokens
5210 .get(self.pos + 1)
5211 .is_some_and(|t| t.ttype == TokenType::Colon)
5212 && self
5213 .tokens
5214 .get(self.pos + 2)
5215 .is_some_and(|t| t.ttype == TokenType::LBrace);
5216 if next_two_are_block {
5217 let bad = self.current().clone();
5218 return Err(ParseError {
5219 message: format!(
5220 "unknown `stream` handler `{name}` — this block accepts only \
5221 `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5222 `on_complete:` (run once, after the source closes, with the \
5223 accumulation bound as `complete`) and `on_error:` (run when the \
5224 SOURCE fails, with the failure bound as `error`). An unrecognised \
5225 handler is refused rather than skipped: a skipped handler removes \
5226 the processing the author wrote, and silence in that direction is \
5227 indistinguishable from a stream that had nothing to do."
5228 ),
5229 line: bad.line,
5230 column: bad.column,
5231 ..Default::default()
5232 });
5233 }
5234
5235 block.body.push(self.parse_flow_step()?);
5237 }
5238 self.consume(TokenType::RBrace)?;
5239 }
5240
5241 Ok(block)
5242 }
5243
5244 fn parse_stream_handler_arm(
5253 &mut self,
5254 name: &str,
5255 at: &Token,
5256 ) -> Result<StepNode, ParseError> {
5257 self.consume(TokenType::LBrace)?;
5258 let mut node = StepNode {
5259 name: name.to_string(),
5260 persona_ref: String::new(),
5261 given: String::new(),
5262 ask: String::new(),
5263 output_type: String::new(),
5264 confidence_floor: None,
5265 navigate_ref: String::new(),
5266 apply_ref: String::new(),
5267 requires_context: None,
5268 now_tz: None,
5269 guards: Vec::new(),
5270 pix_ops: Vec::new(),
5271 stream: None,
5272 performs: Vec::new(),
5273 loc: self.loc_of(at),
5274 };
5275 self.parse_step_body_into(&mut node)?;
5276 self.consume(TokenType::RBrace)?;
5277 Ok(node)
5278 }
5279
5280 fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5281 let tok = self.current().clone();
5282 self.advance();
5283 while !self.check(TokenType::LBrace)
5285 && !self.check(TokenType::RBrace)
5286 && !self.check(TokenType::Eof)
5287 && !self.at_declaration_start()
5288 {
5289 self.advance();
5290 }
5291 if self.check(TokenType::LBrace) {
5292 self.skip_braced_block()?;
5293 }
5294 Ok(Loc {
5295 line: tok.line,
5296 column: tok.column,
5297 })
5298 }
5299
5300 fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5307 let tok = self.consume(TokenType::Forge)?;
5308 let name = self.consume(TokenType::Identifier)?.value;
5309 let mut node = ForgeBlock {
5310 name,
5311 novelty: 0.5,
5312 depth: 1,
5313 branches: 1,
5314 loc: Loc { line: tok.line, column: tok.column },
5315 ..Default::default()
5316 };
5317 self.consume(TokenType::LParen)?;
5319 let arg = self.consume_any_ident_or_kw()?.value;
5320 self.consume(TokenType::Colon)?;
5321 if arg != "seed" {
5322 return Err(self.error(&format!(
5323 "forge '{}' expects `seed:` as its argument, found `{arg}`",
5324 node.name
5325 )));
5326 }
5327 node.seed = self.consume(TokenType::StringLit)?.value;
5328 self.consume(TokenType::RParen)?;
5329 self.consume(TokenType::Arrow)?;
5331 node.output_type = self.consume_any_ident_or_kw()?.value;
5332 self.consume(TokenType::LBrace)?;
5334 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5335 let field = self.consume_any_ident_or_kw()?.value;
5336 self.consume(TokenType::Colon)?;
5337 match field.as_str() {
5338 "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5339 "novelty" => node.novelty = self.consume_number()?,
5340 "depth" => {
5341 node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5342 }
5343 "branches" => {
5344 node.branches =
5345 self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5346 }
5347 "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5348 other => {
5349 return Err(self.error(&format!("unknown forge field `{other}`")))
5350 }
5351 }
5352 if self.check(TokenType::Comma) {
5353 self.consume(TokenType::Comma)?;
5354 }
5355 }
5356 self.consume(TokenType::RBrace)?;
5357 Ok(node)
5358 }
5359
5360 fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5369 let tok = self.current().clone();
5370 self.advance(); self.consume(TokenType::LBrace)?;
5372 let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5373 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5374 branches.push(vec![self.parse_flow_step()?]);
5375 }
5376 self.consume(TokenType::RBrace)?;
5377 Ok(ParBlock {
5378 branches,
5379 loc: Loc {
5380 line: tok.line,
5381 column: tok.column,
5382 },
5383 })
5384 }
5385
5386 fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5403 let tok = self.consume(TokenType::Warden)?;
5404 self.consume(TokenType::LParen)?;
5406 let target = self.consume_any_ident_or_kw()?.value;
5407 self.consume(TokenType::RParen)?;
5408 self.consume(TokenType::Within)?;
5410 let scope_ref = self.consume(TokenType::Identifier)?.value;
5411 let mut block = WardenBlock {
5412 target,
5413 scope_ref,
5414 body: Vec::new(),
5415 loc: Loc {
5416 line: tok.line,
5417 column: tok.column,
5418 },
5419 };
5420 self.consume(TokenType::LBrace)?;
5422 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5423 block.body.push(self.parse_flow_step()?);
5424 }
5425 self.consume(TokenType::RBrace)?;
5426 Ok(block)
5427 }
5428
5429 fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5434 let tok = self.consume(TokenType::Scope)?;
5435 let name = self.consume(TokenType::Identifier)?.value;
5436 let mut node = ScopeDefinition {
5437 name,
5438 loc: Loc {
5439 line: tok.line,
5440 column: tok.column,
5441 },
5442 ..Default::default()
5443 };
5444 self.consume(TokenType::LBrace)?;
5445 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5446 let key = self.consume_any_ident_or_kw()?.value;
5447 self.consume(TokenType::Colon)?;
5448 match key.as_str() {
5449 "targets" => {
5450 self.consume(TokenType::LBracket)?;
5451 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5452 let t = if self.check(TokenType::StringLit) {
5453 self.consume(TokenType::StringLit)?.value
5454 } else {
5455 self.consume_any_ident_or_kw()?.value
5456 };
5457 node.targets.push(t);
5458 if self.check(TokenType::Comma) {
5459 self.advance();
5460 }
5461 }
5462 self.consume(TokenType::RBracket)?;
5463 }
5464 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5465 "approver" => {
5466 if self.current().value == "requires" {
5468 self.advance();
5469 }
5470 node.approver = self.consume(TokenType::StringLit)?.value;
5471 }
5472 other => {
5473 return Err(self.error(&format!(
5474 "unknown scope field `{other}` in scope `{}` — expected \
5475 `targets` / `depth` / `approver`",
5476 node.name
5477 )))
5478 }
5479 }
5480 if self.check(TokenType::Comma) {
5481 self.consume(TokenType::Comma)?;
5482 }
5483 }
5484 self.consume(TokenType::RBrace)?;
5485 Ok(node)
5486 }
5487
5488 fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5489 let tok = self.current().clone();
5490 self.advance(); let mut block = QuantBlock {
5493 encoding: None,
5494 observable: None,
5495 qubits: None,
5496 depth: None,
5497 bandwidth: None,
5498 reupload: None,
5499 effect: "quant_sim".to_string(),
5502 body: Vec::new(),
5503 loc: Loc {
5504 line: tok.line,
5505 column: tok.column,
5506 },
5507 };
5508
5509 if self.check(TokenType::LParen) {
5511 self.advance();
5512 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5513 let key = self.consume_any_ident_or_kw()?.value;
5514 self.consume(TokenType::Colon)?;
5515 match key.as_str() {
5516 "encoding" => {
5517 block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5518 }
5519 "observable" => {
5520 block.observable = Some(self.parse_dotted_identifier()?)
5521 }
5522 "qubits" => block.qubits = Some(self.consume_number()? as i64),
5523 "depth" => block.depth = Some(self.consume_number()? as i64),
5524 "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5525 "reupload" => block.reupload = Some(self.consume_number()? as i64),
5527 "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5529 other => {
5530 return Err(ParseError {
5531 message: format!(
5532 "Unknown `quant` attribute `{other}` — expected one of \
5533 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5534 ),
5535 line: self.current().line,
5536 column: self.current().column,
5537 ..Default::default()
5538 });
5539 }
5540 }
5541 if self.check(TokenType::Comma) {
5543 self.advance();
5544 }
5545 }
5546 self.consume(TokenType::RParen)?;
5547 }
5548
5549 self.consume(TokenType::LBrace)?;
5551 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5552 block.body.push(self.parse_flow_step()?);
5553 }
5554 self.consume(TokenType::RBrace)?;
5555
5556 Ok(block)
5557 }
5558
5559 fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5563 let tok = self.consume(TokenType::Yield)?;
5564 let loc = self.loc_of(&tok);
5565 self.last_let_value_kind = "literal".to_string();
5566 let value_expr = self.parse_let_value_expr()?;
5567 Ok(YieldStatement {
5568 value_expr,
5569 value_kind: self.last_let_value_kind.clone(),
5570 loc,
5571 })
5572 }
5573
5574 fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5581 let tok = self.current().clone();
5582 let loc = self.loc_of(&tok);
5583 self.advance(); let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5585
5586 let mut arguments = Vec::new();
5587 if self.current().value == "on" {
5588 self.advance();
5589 loop {
5590 arguments.push(self.parse_subject()?);
5596 if self.check(TokenType::Comma) {
5597 self.advance();
5598 } else {
5599 break;
5600 }
5601 }
5602 }
5603
5604 let mut output_name = String::new();
5605 if self.check(TokenType::Arrow) {
5606 self.advance();
5607 output_name = self.consume_any_ident_or_kw()?.value.clone();
5608 }
5609
5610 Ok(ComputeApplyStep {
5611 compute_name,
5612 arguments,
5613 output_name,
5614 loc,
5615 })
5616 }
5617
5618 fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5619 let tok = self.current().clone();
5620 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5622 let mut target = String::new();
5623 let mut output_type = String::new();
5624 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5626 let next = self.current().clone();
5627 if next.value == "on" {
5628 self.advance();
5629 target = self.parse_subject()?;
5632 }
5633 }
5634 if self.check(TokenType::Arrow) {
5636 self.advance();
5637 output_type = self.consume_any_ident_or_kw()?.value.clone();
5638 }
5639 if self.check(TokenType::LBrace) {
5641 self.skip_braced_block()?;
5642 }
5643 Ok((
5644 Loc {
5645 line: tok.line,
5646 column: tok.column,
5647 },
5648 name,
5649 target,
5650 output_type,
5651 ))
5652 }
5653
5654 fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5665 let tok = self.current().clone();
5666 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5668 let mut target = String::new();
5669 let mut binding = String::new();
5670 if self.current().value == "on" {
5671 self.advance();
5672 target = self.parse_subject()?;
5675 if self.check(TokenType::LParen) {
5678 let mut depth = 0usize;
5679 loop {
5680 let t = self.current().clone();
5681 match t.ttype {
5682 TokenType::LParen => depth += 1,
5683 TokenType::RParen => depth -= 1,
5684 TokenType::Eof => {
5685 return Err(ParseError {
5686 message: format!(
5687 "unterminated argument list in `{kind} {name} on {target}(…`"
5688 ),
5689 line: t.line,
5690 column: t.column,
5691 ..Default::default()
5692 })
5693 }
5694 _ => {}
5695 }
5696 target.push_str(&t.value);
5697 self.advance();
5698 if depth == 0 {
5699 break;
5700 }
5701 }
5702 }
5703 }
5704 if self.check(TokenType::Arrow) {
5705 self.advance();
5706 binding = self.consume_any_ident_or_kw()?.value.clone();
5707 }
5708 Ok(StepGuardNode {
5709 kind: kind.to_string(),
5710 name,
5711 target,
5712 binding,
5713 loc: Loc {
5714 line: tok.line,
5715 column: tok.column,
5716 },
5717 })
5718 }
5719
5720 fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5731 let tok = self.current().clone();
5732 let loc = self.loc_of(&tok);
5733 self.advance(); let next_is_field_key = self
5746 .tokens
5747 .get(self.pos + 1)
5748 .is_some_and(|t| t.ttype == TokenType::Colon);
5749 let target = if self.check(TokenType::LBrace)
5750 || self.at_declaration_start()
5751 || self.check(TokenType::RBrace)
5752 || self.check(TokenType::Eof)
5753 || next_is_field_key
5754 {
5755 String::new()
5756 } else {
5757 self.parse_dotted_identifier()?
5758 };
5759
5760 let mut node = ReasonStep {
5761 strategy: String::new(),
5762 target,
5763 given: String::new(),
5764 ask: String::new(),
5765 depth: None,
5766 loc,
5767 };
5768
5769 if !self.check(TokenType::LBrace) {
5770 return Ok(node);
5771 }
5772 self.advance();
5773 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5774 let key = self.current().clone();
5775 self.advance();
5776 self.consume(TokenType::Colon)?;
5777 match key.value.as_str() {
5778 "given" => {
5783 let mut parts = vec![self.parse_expression_string()?];
5784 while self.check(TokenType::Comma) {
5785 self.advance();
5786 parts.push(self.parse_expression_string()?);
5787 }
5788 node.given = parts.join(", ");
5789 }
5790 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5791 "depth" => {
5792 let n = self.current().clone();
5793 if n.ttype != TokenType::Integer {
5794 return Err(ParseError {
5795 message: format!(
5796 "`depth:` in a `reason` block is a deliberation depth — a \
5797 positive integer (got '{}')",
5798 n.value
5799 ),
5800 line: n.line,
5801 column: n.column,
5802 ..Default::default()
5803 });
5804 }
5805 self.advance();
5806 node.depth = n.value.parse::<u32>().ok();
5807 }
5808 "chain_of_thought" => {
5812 let v = self.consume_any_ident_or_kw()?.value;
5813 if v == "enabled" {
5814 node.strategy = "chain_of_thought".to_string();
5815 }
5816 }
5817 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5818 "target" => {
5827 let v = if self.check(TokenType::StringLit) {
5828 self.consume(TokenType::StringLit)?.value
5829 } else {
5830 self.parse_dotted_identifier()?
5831 };
5832 if !node.target.is_empty() {
5833 return Err(ParseError {
5834 message: format!(
5835 "`reason {} {{ target: … }}` declares the subject twice — \
5836 once positionally as `{}` and once as `target: {}`. They \
5837 are the same field. Write one of them.",
5838 node.target, node.target, v
5839 ),
5840 line: key.line,
5841 column: key.column,
5842 ..Default::default()
5843 });
5844 }
5845 node.target = v;
5846 }
5847 other => {
5848 return Err(ParseError {
5849 message: format!(
5850 "unknown field '{other}' in a `reason` block. Accepted: given, \
5851 ask, depth, strategy, chain_of_thought, target. A field this \
5852 block does not recognise is REFUSED rather than skipped — a \
5853 `reason` that silently loses its `ask:` deliberates over \
5854 nothing, and that failure is quiet."
5855 ),
5856 line: key.line,
5857 column: key.column,
5858 ..Default::default()
5859 })
5860 }
5861 }
5862 }
5863 self.consume(TokenType::RBrace)?;
5864 Ok(node)
5865 }
5866
5867 fn at_weave_field(&self) -> bool {
5876 const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5877 self.field_ahead(FIELDS)
5878 }
5879
5880 fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5889 let tok = self.current().clone();
5890 self.advance();
5891 let mut node = WeaveStep {
5892 sources: Vec::new(),
5893 target: String::new(),
5894 format_type: String::new(),
5895 priority: Vec::new(),
5896 style: String::new(),
5897 include: Vec::new(),
5898 loc: Loc {
5899 line: tok.line,
5900 column: tok.column,
5901 },
5902 };
5903 if self.check(TokenType::LBracket) {
5909 node.sources = self.parse_bracketed_dot_identifiers()?;
5910 } else if self.current().ttype == TokenType::Identifier
5911 && !self
5912 .tokens
5913 .get(self.pos + 1)
5914 .is_some_and(|t| t.ttype == TokenType::Colon)
5915 {
5916 node.sources = vec![self.parse_dotted_identifier()?];
5926 }
5927 if self.check(TokenType::Into) || self.current().value == "into" {
5929 self.advance();
5930 node.target = self.parse_dotted_identifier()?;
5931 }
5932 while self.at_weave_field() {
5934 let f = self.current().value.clone();
5935 self.advance();
5936 self.consume(TokenType::Colon)?;
5937 match f.as_str() {
5938 "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
5939 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5940 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5941 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5942 other => {
5945 return Err(ParseError {
5946 message: format!(
5947 "`{other}` passed the `weave` field test but has no handler — \
5948 the braceless catalog and its parser have drifted apart."
5949 ),
5950 line: tok.line,
5951 column: tok.column,
5952 ..Default::default()
5953 })
5954 }
5955 }
5956 }
5957 if self.check(TokenType::LBrace) {
5958 self.advance();
5959 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5960 let f = self.current().value.clone();
5961 self.advance();
5962 if self.check(TokenType::Colon) {
5963 self.advance();
5964 match f.as_str() {
5965 "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
5966 "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
5967 "format" => {
5968 node.format_type = self.consume_any_ident_or_kw()?.value.clone()
5969 }
5970 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5971 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5972 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5976 _ => self.skip_value(),
5977 }
5978 }
5979 }
5980 if self.check(TokenType::RBrace) {
5981 self.advance();
5982 }
5983 }
5984 Ok(FlowStep::Weave(node))
5985 }
5986
5987 fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
5988 let tok = self.current().clone();
5989 self.advance();
5990 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
5991 let args = if self.check(TokenType::LParen) {
6002 UseArgs::Named(self.parse_named_arg_list()?)
6003 } else {
6004 let mut argument = String::new();
6005 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6006 let next = self.current().clone();
6007 if next.value == "on" {
6008 self.advance();
6009 argument = self.consume_any_ident_or_kw()?.value.clone();
6010 }
6011 }
6012 UseArgs::LegacyPositional(argument)
6013 };
6014 if self.check(TokenType::LBrace) {
6015 self.skip_braced_block()?;
6016 }
6017 Ok(FlowStep::UseTool(UseToolStep {
6018 tool_name,
6019 args,
6020 loc: Loc {
6021 line: tok.line,
6022 column: tok.column,
6023 },
6024 }))
6025 }
6026
6027 fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6033 self.consume(TokenType::LParen)?;
6034 let mut args = Vec::new();
6035 while !self.check(TokenType::RParen) {
6036 let name = self.consume_any_ident_or_kw()?.value;
6039 self.consume(TokenType::Assign)?;
6040 let value = self.parse_let_atom()?;
6041 let value_kind = self.last_let_value_kind.clone();
6045 args.push((name, value, value_kind));
6046 if self.check(TokenType::Comma) {
6047 self.advance();
6048 } else {
6049 break;
6050 }
6051 }
6052 self.consume(TokenType::RParen)?;
6053 Ok(args)
6054 }
6055
6056 fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6057 let tok = self.current().clone();
6058 self.advance();
6059 let expr = self.consume_any_ident_or_kw()?.value.clone();
6060 let mut mem = String::new();
6061 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6062 let next = self.current().clone();
6063 if next.value == "in" || next.ttype == TokenType::In {
6064 self.advance();
6065 mem = self.consume_any_ident_or_kw()?.value.clone();
6066 }
6067 }
6068 Ok(FlowStep::Remember(RememberStep {
6069 expression: expr,
6070 memory_target: mem,
6071 loc: Loc {
6072 line: tok.line,
6073 column: tok.column,
6074 },
6075 }))
6076 }
6077
6078 fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6079 let tok = self.current().clone();
6080 self.advance();
6081 let query = if self.check(TokenType::StringLit) {
6082 self.consume(TokenType::StringLit)?.value.clone()
6083 } else {
6084 self.consume_any_ident_or_kw()?.value.clone()
6085 };
6086 let mut mem = String::new();
6087 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6088 let next = self.current().clone();
6089 if next.value == "from" || next.ttype == TokenType::From {
6090 self.advance();
6091 mem = self.consume_any_ident_or_kw()?.value.clone();
6092 }
6093 }
6094 Ok(FlowStep::Recall(RecallStep {
6095 query,
6096 memory_source: mem,
6097 loc: Loc {
6098 line: tok.line,
6099 column: tok.column,
6100 },
6101 }))
6102 }
6103
6104 fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6105 let tok = self.current().clone();
6106 self.advance();
6107 let mut event = String::new();
6108 let mut timeout = String::new();
6109 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6110 let first = self.consume_any_ident_or_kw()?.value.clone();
6115 if first == "until" && self.check(TokenType::StringLit) {
6116 event = self.consume(TokenType::StringLit)?.value.clone();
6117 } else {
6118 event = first;
6119 }
6120 }
6121 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6122 let next = self.current().clone();
6123 if next.ttype == TokenType::Duration {
6124 self.advance();
6125 timeout = next.value.clone();
6126 }
6127 }
6128 Ok(FlowStep::Hibernate(HibernateStep {
6129 event_name: event,
6130 timeout,
6131 loc: Loc {
6132 line: tok.line,
6133 column: tok.column,
6134 },
6135 }))
6136 }
6137
6138 fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6147 let tok = self.current().clone();
6148 self.advance();
6149 let target = self.consume_any_ident_or_kw()?.value.clone();
6150 let mut wrt: Vec<String> = Vec::new();
6151 let mut output = String::new();
6152 if !self.at_declaration_start() && self.current().value == "wrt" {
6153 self.advance();
6154 if self.check(TokenType::LBracket) {
6155 wrt = self.parse_bracketed_identifiers()?;
6156 } else {
6157 wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6158 }
6159 }
6160 if !self.at_declaration_start() && self.current().value == "as" {
6161 self.advance();
6162 output = self.consume_any_ident_or_kw()?.value.clone();
6163 }
6164 Ok(FlowStep::Grad(GradStep {
6165 target,
6166 wrt,
6167 output,
6168 loc: Loc {
6169 line: tok.line,
6170 column: tok.column,
6171 },
6172 }))
6173 }
6174
6175 fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6176 let tok = self.current().clone();
6177 self.advance();
6178 let expression = if self.at_declaration_start()
6179 || self.check(TokenType::RBrace)
6180 || self.check(TokenType::Eof)
6181 {
6182 String::new()
6183 } else {
6184 self.consume_any_ident_or_kw()?.value.clone()
6185 };
6186 let mut where_expr = String::new();
6187 let mut select: Vec<String> = Vec::new();
6188 let mut output = String::new();
6189 if self.check(TokenType::LBrace) {
6190 self.advance();
6191 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6192 if self.check(TokenType::Comma) {
6193 self.advance();
6194 continue;
6195 }
6196 let f = self.current().value.clone();
6197 self.advance();
6198 if self.check(TokenType::Colon) {
6199 self.advance();
6200 match f.as_str() {
6201 "where" => {
6202 where_expr = self.consume(TokenType::StringLit)?.value.clone()
6203 }
6204 "select" => select = self.parse_bracketed_identifiers()?,
6205 "as" | "alias" => {
6206 output = self.consume_any_ident_or_kw()?.value.clone()
6207 }
6208 _ => self.skip_value(),
6209 }
6210 }
6211 }
6212 if self.check(TokenType::RBrace) {
6213 self.advance();
6214 }
6215 }
6216 Ok(FlowStep::Focus(FocusStep {
6217 expression,
6218 where_expr,
6219 select,
6220 output,
6221 loc: Loc {
6222 line: tok.line,
6223 column: tok.column,
6224 },
6225 }))
6226 }
6227
6228 fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6229 let tok = self.current().clone();
6230 self.advance();
6231 let left = self.consume_any_ident_or_kw()?.value.clone();
6232 let mut right = String::new();
6233 let mut using = String::new();
6234 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6235 right = self.consume_any_ident_or_kw()?.value.clone();
6236 }
6237 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6238 let next = self.current().clone();
6239 if next.value == "using" {
6240 self.advance();
6241 using = self.consume_any_ident_or_kw()?.value.clone();
6242 }
6243 }
6244 let mut output = String::new();
6245 if self.check(TokenType::LBrace) {
6246 self.advance();
6247 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6248 let f = self.current().value.clone();
6249 self.advance();
6250 if self.check(TokenType::Colon) {
6251 self.advance();
6252 match f.as_str() {
6253 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6254 _ => self.skip_value(),
6255 }
6256 }
6257 }
6258 if self.check(TokenType::RBrace) {
6259 self.advance();
6260 }
6261 }
6262 Ok(FlowStep::Associate(AssociateStep {
6263 left,
6264 right,
6265 using_field: using,
6266 output,
6267 loc: Loc {
6268 line: tok.line,
6269 column: tok.column,
6270 },
6271 }))
6272 }
6273
6274 fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6275 let tok = self.current().clone();
6276 self.advance();
6277 let target = self.consume_any_ident_or_kw()?.value.clone();
6278 let mut group_by = Vec::new();
6279 let mut alias = String::new();
6280 let mut compute: Vec<String> = Vec::new();
6281 let mut where_expr = String::new();
6282 if self.check(TokenType::LBrace) {
6283 self.advance();
6284 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6285 let f = self.current().value.clone();
6286 self.advance();
6287 if self.check(TokenType::Colon) {
6288 self.advance();
6289 match f.as_str() {
6290 "group_by" => group_by = self.parse_bracketed_identifiers()?,
6291 "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6292 "compute" => compute = self.parse_bracketed_aggregates()?,
6295 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6297 _ => self.skip_value(),
6298 }
6299 }
6300 }
6301 if self.check(TokenType::RBrace) {
6302 self.advance();
6303 }
6304 }
6305 Ok(FlowStep::Aggregate(AggregateStep {
6306 target,
6307 group_by,
6308 alias,
6309 compute,
6310 where_expr,
6311 loc: Loc {
6312 line: tok.line,
6313 column: tok.column,
6314 },
6315 }))
6316 }
6317
6318 fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6319 let tok = self.current().clone();
6320 self.advance();
6321 let target = self.consume_any_ident_or_kw()?.value.clone();
6322 let mut limit = None;
6323 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6324 if self.current().ttype == TokenType::Integer {
6325 limit = self.current().value.parse::<i64>().ok();
6326 self.advance();
6327 }
6328 }
6329 let mut output = String::new();
6330 if self.check(TokenType::LBrace) {
6331 self.advance();
6332 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6333 let f = self.current().value.clone();
6334 self.advance();
6335 if self.check(TokenType::Colon) {
6336 self.advance();
6337 match f.as_str() {
6338 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6339 _ => self.skip_value(),
6340 }
6341 }
6342 }
6343 if self.check(TokenType::RBrace) {
6344 self.advance();
6345 }
6346 }
6347 Ok(FlowStep::ExploreStep(ExploreStepNode {
6348 target,
6349 limit,
6350 output,
6351 loc: Loc {
6352 line: tok.line,
6353 column: tok.column,
6354 },
6355 }))
6356 }
6357
6358 fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6361 let mut out = Vec::new();
6362 self.consume(TokenType::LBracket)?;
6363 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6364 let name = self.consume_any_ident_or_kw()?.value.clone();
6365 if self.check(TokenType::LParen) {
6366 self.advance();
6367 let col = self.consume_any_ident_or_kw()?.value.clone();
6368 self.consume(TokenType::RParen)?;
6369 out.push(format!("{name}({col})"));
6370 } else {
6371 out.push(name);
6372 }
6373 if self.check(TokenType::Comma) {
6374 self.advance();
6375 }
6376 }
6377 self.consume(TokenType::RBracket)?;
6378 Ok(out)
6379 }
6380
6381 fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6396 let tok = self.current().clone();
6397 self.advance();
6398 let source = self.consume_any_ident_or_kw()?.value.clone();
6399 let mut target = String::new();
6400 let mut format = String::new();
6401 let mut max_bytes: Option<u64> = None;
6402 let mut max_rows: Option<u64> = None;
6403 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6404 let next = self.current().clone();
6405 if next.value == "into" || next.ttype == TokenType::Into {
6406 self.advance();
6407 target = self.consume_any_ident_or_kw()?.value.clone();
6408 }
6409 }
6410 if self.check(TokenType::LBrace) {
6411 self.consume(TokenType::LBrace)?;
6412 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6413 if self.check(TokenType::Comma) {
6415 self.advance();
6416 continue;
6417 }
6418 let entry = self.current().clone();
6419 match entry.value.as_str() {
6420 "format" => {
6421 self.advance();
6422 self.consume(TokenType::Colon)?;
6423 format = self.consume_any_ident_or_kw()?.value.clone();
6424 }
6425 "limits" => {
6426 self.advance();
6427 self.consume(TokenType::LBrace)?;
6428 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6429 let bound = self.current().clone();
6430 self.advance();
6431 self.consume(TokenType::Colon)?;
6432 let num_tok = self.consume(TokenType::Integer)?.clone();
6433 let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6434 message: format!(
6435 "ingest `limits` bound `{}` must be a non-negative \
6436 integer byte/row count, got `{}`.",
6437 bound.value, num_tok.value
6438 ),
6439 line: num_tok.line,
6440 column: num_tok.column,
6441 ..Default::default()
6442 })?;
6443 match bound.value.as_str() {
6444 "max_bytes" => max_bytes = Some(value),
6445 "max_rows" => max_rows = Some(value),
6446 other => {
6447 return Err(ParseError {
6448 message: format!(
6449 "Unknown ingest limit `{other}`. The closed \
6450 limits grammar is `max_bytes: <N>` and \
6451 `max_rows: <N>` — bounds enforced on the raw \
6452 stream BEFORE parsing.",
6453 ),
6454 line: bound.line,
6455 column: bound.column,
6456 ..Default::default()
6457 });
6458 }
6459 }
6460 if self.check(TokenType::Comma) {
6461 self.advance();
6462 }
6463 }
6464 self.consume(TokenType::RBrace)?;
6465 }
6466 other => {
6467 return Err(ParseError {
6468 message: format!(
6469 "Unknown entry `{other}` in ingest body. The closed \
6470 grammar is `format: csv|json` and \
6471 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6472 ),
6473 line: entry.line,
6474 column: entry.column,
6475 ..Default::default()
6476 });
6477 }
6478 }
6479 }
6480 self.consume(TokenType::RBrace)?;
6481 }
6482 Ok(FlowStep::Ingest(IngestStep {
6483 source,
6484 target,
6485 format,
6486 max_bytes,
6487 max_rows,
6488 loc: Loc {
6489 line: tok.line,
6490 column: tok.column,
6491 },
6492 }))
6493 }
6494
6495 fn at_navigate_field(&self) -> bool {
6502 const FIELDS: &[&str] = &[
6503 "corpus", "query", "trail", "as", "from", "budget", "where",
6511 "depth", "recall",
6512 ];
6513 self.field_ahead(FIELDS)
6514 }
6515
6516 fn at_drill_field(&self) -> bool {
6518 const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6521 self.field_ahead(FIELDS)
6522 }
6523
6524 fn field_ahead(&self, names: &[&str]) -> bool {
6526 let cur = self.current();
6527 if !names.contains(&cur.value.as_str()) {
6528 return false;
6529 }
6530 self.tokens
6531 .get(self.pos + 1)
6532 .is_some_and(|t| t.ttype == TokenType::Colon)
6533 }
6534
6535 fn parse_config_key(&mut self) -> Result<String, ParseError> {
6545 if self.check(TokenType::StringLit) {
6546 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6547 }
6548 let scheme = self.consume_any_ident_or_kw()?.value.clone();
6549 if self.check(TokenType::Colon) {
6550 self.advance();
6551 let key = self.consume_any_ident_or_kw()?.value.clone();
6552 return Ok(format!("{scheme}:{key}"));
6553 }
6554 Ok(scheme)
6555 }
6556
6557 fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6562 if self.check(TokenType::StringLit) {
6563 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6564 }
6565 Ok(self.consume_any_ident_or_kw()?.value.clone())
6566 }
6567
6568 fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6569 let tok = self.current().clone();
6570 self.advance();
6571 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6572 let mut node = NavigateStep {
6573 depth: None,
6574 pix_name,
6575 corpus_name: String::new(),
6576 query_expr: String::new(),
6577 trail_enabled: false,
6578 output_name: String::new(),
6579 seed: String::new(),
6580 budget: None,
6581 where_expr: String::new(),
6582 loc: Loc {
6583 line: tok.line,
6584 column: tok.column,
6585 },
6586 };
6587 if !self.check(TokenType::LBrace) {
6601 while self.at_navigate_field() {
6602 let f = self.current().value.clone();
6603 self.advance();
6604 self.consume(TokenType::Colon)?;
6605 match f.as_str() {
6606 "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6607 "query" => node.query_expr = self.parse_pix_value()?,
6608 "trail" => {
6609 let v = self.consume_any_ident_or_kw()?.value;
6610 node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6611 }
6612 "output" | "as" => {
6613 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6614 }
6615 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6616 "budget" => node.budget = self.parse_optional_int(),
6617 "where" => node.where_expr = self.parse_pix_value()?,
6618 "depth" => node.depth = self.parse_optional_int(),
6619 "recall" => {
6627 let mode = self.consume_any_ident_or_kw()?.value.clone();
6628 if node.seed.is_empty() {
6629 node.seed = format!("recall:{mode}");
6630 }
6631 }
6632 _ => self.skip_value(),
6633 }
6634 }
6635 }
6636 if self.check(TokenType::LBrace) {
6637 self.advance();
6638 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6639 let f = self.current().value.clone();
6640 self.advance();
6641 if self.check(TokenType::Colon) {
6642 self.advance();
6643 match f.as_str() {
6644 "corpus" => {
6645 node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6646 }
6647 "query" => node.query_expr = self.parse_pix_value()?,
6648 "trail" => {
6649 let v = self.consume_any_ident_or_kw()?.value;
6650 node.trail_enabled =
6651 matches!(v.as_str(), "true" | "enabled" | "on");
6652 }
6653 "output" | "as" => {
6654 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6655 }
6656 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6658 "budget" => node.budget = self.parse_optional_int(),
6659 "where" => {
6667 node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6668 }
6669 _ => self.skip_value(),
6670 }
6671 }
6672 }
6673 if self.check(TokenType::RBrace) {
6674 self.advance();
6675 }
6676 }
6677 Ok(FlowStep::Navigate(node))
6678 }
6679
6680 fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6681 let tok = self.current().clone();
6682 self.advance();
6683 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6684 let mut node = DrillStep {
6685 pix_name,
6686 subtree_path: String::new(),
6687 query_expr: String::new(),
6688 output_name: String::new(),
6689 loc: Loc {
6690 line: tok.line,
6691 column: tok.column,
6692 },
6693 };
6694 if self.current().value == "into" {
6698 self.advance();
6699 node.subtree_path = if self.check(TokenType::StringLit) {
6704 self.consume(TokenType::StringLit)?.value.clone()
6705 } else {
6706 self.parse_dotted_identifier()?
6707 };
6708 }
6709 if !self.check(TokenType::LBrace) {
6710 while self.at_drill_field() {
6711 let f = self.current().value.clone();
6712 self.advance();
6713 self.consume(TokenType::Colon)?;
6714 match f.as_str() {
6715 "subtree" | "path" => {
6716 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6717 }
6718 "query" => node.query_expr = self.parse_pix_value()?,
6719 "output" | "as" => {
6720 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6721 }
6722 _ => self.skip_value(),
6723 }
6724 }
6725 }
6726 if self.check(TokenType::LBrace) {
6727 self.advance();
6728 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6729 let f = self.current().value.clone();
6730 self.advance();
6731 if self.check(TokenType::Colon) {
6732 self.advance();
6733 match f.as_str() {
6734 "subtree" | "path" => {
6735 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6736 }
6737 "query" => node.query_expr = self.parse_pix_value()?,
6738 "output" | "as" => {
6739 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6740 }
6741 _ => self.skip_value(),
6742 }
6743 }
6744 }
6745 if self.check(TokenType::RBrace) {
6746 self.advance();
6747 }
6748 }
6749 Ok(FlowStep::Drill(node))
6750 }
6751
6752 fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6753 let tok = self.current().clone();
6754 self.advance();
6755 let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6756 let mut output = String::new();
6757 if self.check(TokenType::Arrow) {
6758 self.advance();
6759 output = self.consume_any_ident_or_kw()?.value.clone();
6760 }
6761 Ok(FlowStep::Corroborate(CorroborateStep {
6762 navigate_ref: nav_ref,
6763 output_name: output,
6764 loc: Loc {
6765 line: tok.line,
6766 column: tok.column,
6767 },
6768 }))
6769 }
6770
6771 fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6772 let tok = self.current().clone();
6773 self.advance();
6774 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6778 (self.consume(TokenType::StringLit)?.value.clone(), false)
6779 } else {
6780 (self.consume_any_ident_or_kw()?.value.clone(), true)
6781 };
6782 let mut alias = String::new();
6783 if !self.at_declaration_start()
6784 && !self.check(TokenType::RBrace)
6785 && !self.check(TokenType::LBrace)
6786 {
6787 let next = self.current().clone();
6788 if next.value == "as" || next.ttype == TokenType::As {
6789 self.advance();
6790 alias = self.consume_any_ident_or_kw()?.value.clone();
6791 }
6792 }
6793 let body = self.parse_listener_body()?;
6797 Ok(FlowStep::Listen(ListenStep {
6798 channel,
6799 channel_is_ref,
6800 event_alias: alias,
6801 body,
6802 loc: Loc {
6803 line: tok.line,
6804 column: tok.column,
6805 },
6806 }))
6807 }
6808
6809 fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6814 let mut body = Vec::new();
6815 if self.check(TokenType::LBrace) {
6816 self.advance(); while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6818 body.push(self.parse_flow_step()?);
6819 }
6820 self.consume(TokenType::RBrace)?;
6821 }
6822 Ok(body)
6823 }
6824
6825 fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6832 let tok = self.current().clone();
6833 self.advance();
6834 if self.check(TokenType::From) || self.current().value == "from" {
6837 self.advance();
6838 }
6839 let store = self.consume_any_ident_or_kw()?.value.clone();
6840 let mut where_expr = String::new();
6841 let mut alias = String::new();
6842 let mut order_by = String::new();
6843 let mut limit_expr = String::new();
6844 let mut aggregate = String::new();
6845 let mut group_by = String::new();
6846 let mut cache = String::new();
6847 loop {
6854 match self.current().value.as_str() {
6855 "where" if !self.check(TokenType::LBrace) => {
6856 self.advance();
6857 where_expr = self.consume(TokenType::StringLit)?.value.clone();
6858 }
6859 "as" => {
6860 self.advance();
6861 alias = self.consume_any_ident_or_kw()?.value.clone();
6862 }
6863 _ => break,
6864 }
6865 }
6866 if self.check(TokenType::LBrace) {
6867 self.advance();
6868 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6869 let f = self.current().value.clone();
6870 self.advance();
6871 if self.check(TokenType::Colon) {
6872 self.advance();
6873 match f.as_str() {
6874 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6875 "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6876 "order_by" => {
6879 order_by = self.consume(TokenType::StringLit)?.value.clone()
6880 }
6881 "limit" => {
6886 let t = self.current().clone();
6887 match t.ttype {
6888 TokenType::Integer | TokenType::StringLit => {
6889 limit_expr = t.value.clone();
6890 self.advance();
6891 }
6892 _ => self.skip_value(),
6893 }
6894 }
6895 "aggregate" => {
6902 aggregate = self.consume(TokenType::StringLit)?.value.clone()
6903 }
6904 "group_by" => {
6905 group_by = self.consume(TokenType::StringLit)?.value.clone()
6906 }
6907 "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
6913 _ => self.skip_value(),
6914 }
6915 }
6916 }
6917 if self.check(TokenType::RBrace) {
6918 self.advance();
6919 }
6920 }
6921 Ok(FlowStep::Retrieve(RetrieveStep {
6922 store_name: store,
6923 where_expr,
6924 alias,
6925 order_by,
6926 limit_expr,
6927 aggregate,
6928 group_by,
6929 cache,
6930 loc: Loc {
6931 line: tok.line,
6932 column: tok.column,
6933 },
6934 }))
6935 }
6936
6937 fn parse_store_where_step(
6950 &mut self,
6951 ) -> Result<(Loc, String, String), ParseError> {
6952 let tok = self.current().clone();
6953 self.advance(); let store = if self.at_declaration_start()
6955 || self.check(TokenType::RBrace)
6956 || self.check(TokenType::Eof)
6957 {
6958 String::new()
6959 } else {
6960 self.consume_any_ident_or_kw()?.value.clone()
6961 };
6962 let mut where_expr = String::new();
6963 if self.check(TokenType::LBrace) {
6964 self.advance();
6965 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6966 let field = self.current().value.clone();
6967 self.advance();
6968 if self.check(TokenType::Colon) {
6969 self.advance();
6970 match field.as_str() {
6971 "where" => {
6972 where_expr =
6973 self.consume(TokenType::StringLit)?.value.clone()
6974 }
6975 _ => self.skip_value(),
6976 }
6977 }
6978 }
6979 if self.check(TokenType::RBrace) {
6980 self.advance();
6981 }
6982 }
6983 Ok((
6984 Loc {
6985 line: tok.line,
6986 column: tok.column,
6987 },
6988 store,
6989 where_expr,
6990 ))
6991 }
6992
6993 fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7012 let tok = self.current().clone();
7013 self.advance(); if self.current().value == "into" && !self.check(TokenType::LBrace) {
7017 self.advance();
7018 }
7019 let store = if self.at_declaration_start()
7020 || self.check(TokenType::LBrace)
7021 || self.check(TokenType::RBrace)
7022 || self.check(TokenType::Eof)
7023 {
7024 String::new()
7025 } else {
7026 self.consume_any_ident_or_kw()?.value.clone()
7027 };
7028 let mut fields: Vec<(String, String)> = Vec::new();
7029 if self.check(TokenType::LBrace) {
7030 self.advance();
7031 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7032 let col = self.current().value.clone();
7033 self.advance();
7034 if self.check(TokenType::Colon) {
7035 self.advance();
7036 let value = if self.check(TokenType::StringLit) {
7037 self.consume(TokenType::StringLit)?.value.clone()
7038 } else if self.check(TokenType::RBrace)
7039 || self.check(TokenType::Eof)
7040 || self.check(TokenType::Colon)
7041 {
7042 String::new()
7043 } else {
7044 let v = self.current().clone();
7045 self.advance();
7046 v.value.clone()
7047 };
7048 fields.push((col, value));
7049 }
7050 }
7051 if self.check(TokenType::RBrace) {
7052 self.advance();
7053 }
7054 }
7055 Ok(FlowStep::Persist(PersistStep {
7056 store_name: store,
7057 fields,
7058 loc: Loc {
7059 line: tok.line,
7060 column: tok.column,
7061 },
7062 }))
7063 }
7064
7065 fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7079 let tok = self.current().clone();
7080 self.advance(); let store = if self.at_declaration_start()
7082 || self.check(TokenType::LBrace)
7083 || self.check(TokenType::RBrace)
7084 || self.check(TokenType::Eof)
7085 {
7086 String::new()
7087 } else {
7088 self.consume_any_ident_or_kw()?.value.clone()
7089 };
7090 let mut where_expr = String::new();
7091 let mut fields: Vec<(String, String)> = Vec::new();
7092 if self.check(TokenType::LBrace) {
7093 self.advance();
7094 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7095 let key = self.current().value.clone();
7096 self.advance();
7097 if self.check(TokenType::Colon) {
7098 self.advance();
7099 if key == "where" {
7100 where_expr =
7101 self.consume(TokenType::StringLit)?.value.clone();
7102 } else {
7103 let value = if self.check(TokenType::StringLit) {
7104 self.consume(TokenType::StringLit)?.value.clone()
7105 } else if self.check(TokenType::RBrace)
7106 || self.check(TokenType::Eof)
7107 || self.check(TokenType::Colon)
7108 {
7109 String::new()
7110 } else {
7111 let v = self.current().clone();
7112 self.advance();
7113 v.value.clone()
7114 };
7115 fields.push((key, value));
7116 }
7117 }
7118 }
7119 if self.check(TokenType::RBrace) {
7120 self.advance();
7121 }
7122 }
7123 Ok(FlowStep::Mutate(MutateStep {
7124 store_name: store,
7125 where_expr,
7126 fields,
7127 loc: Loc {
7128 line: tok.line,
7129 column: tok.column,
7130 },
7131 }))
7132 }
7133
7134 fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7137 let tok = self.consume(TokenType::Agent)?;
7138 let name = self.consume(TokenType::Identifier)?.value;
7139 let mut node = AgentDefinition {
7140 name,
7141 goal: String::new(),
7142 tools: Vec::new(),
7143 memory_ref: String::new(),
7144 strategy: String::new(),
7145 on_stuck: String::new(),
7146 shield_ref: String::new(),
7147 max_iterations: None,
7148 max_tokens: None,
7149 max_time: String::new(),
7150 max_cost: None,
7151 return_type: String::new(),
7152 body: Vec::new(),
7153 loc: Loc {
7154 line: tok.line,
7155 column: tok.column,
7156 },
7157 leading_trivia: Vec::new(),
7158 trailing_trivia: Vec::new(),
7159 };
7160 if self.check(TokenType::LParen) {
7166 let mut depth = 0usize;
7167 while !self.check(TokenType::Eof) {
7168 if self.check(TokenType::LParen) {
7169 depth += 1;
7170 } else if self.check(TokenType::RParen) {
7171 depth -= 1;
7172 if depth == 0 {
7173 self.advance();
7174 break;
7175 }
7176 }
7177 self.advance();
7178 }
7179 }
7180 if self.check(TokenType::Arrow) {
7181 self.advance();
7182 node.return_type = self.parse_output_type_string()?;
7183 }
7184 self.consume(TokenType::LBrace)?;
7185 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7191 if self.check(TokenType::Step) {
7192 let step = self.parse_step()?;
7193 node.body.push(step);
7194 continue;
7195 }
7196 let field = self.current().clone();
7197 let field_name = field.value.clone();
7198 self.advance();
7199 if !self.check(TokenType::Colon) {
7200 return Err(self.error(&format!(
7201 "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7202 `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7203 goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7204 max_tokens, max_time, max_cost, return",
7205 node.name
7206 )));
7207 }
7208 self.advance();
7209 match field_name.as_str() {
7210 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7211 "tools" => node.tools = self.parse_bracketed_identifiers()?,
7212 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7213 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7214 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7215 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7216 "max_iterations" => node.max_iterations = self.parse_optional_int(),
7217 "max_tokens" => node.max_tokens = self.parse_optional_int(),
7218 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7219 "max_cost" => node.max_cost = self.parse_optional_float(),
7220 "return" => node.return_type = self.parse_output_type_string()?,
7221 other => {
7222 return Err(self.error(&format!(
7223 "unknown agent field `{other}` in `agent {}` — the agent block is a \
7224 closed catalog; valid fields: goal, tools, memory, strategy, \
7225 on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7226 return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7227 node.name
7228 )));
7229 }
7230 }
7231 }
7232 self.consume(TokenType::RBrace)?;
7233 Ok(node)
7234 }
7235
7236 fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7241 let tok = self.consume(TokenType::Extension)?;
7242 let name = self.consume(TokenType::Identifier)?.value;
7243 let mut node = ExtensionDefinition {
7244 name,
7245 category: String::new(),
7246 members: Vec::new(),
7247 loc: Loc {
7248 line: tok.line,
7249 column: tok.column,
7250 },
7251 leading_trivia: Vec::new(),
7252 trailing_trivia: Vec::new(),
7253 };
7254 self.consume(TokenType::LBrace)?;
7255 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7256 let field_name = self.current().value.clone();
7257 self.advance();
7258 if self.check(TokenType::Colon) {
7259 self.advance();
7260 match field_name.as_str() {
7261 "category" => {
7262 node.category = self.consume_any_ident_or_kw()?.value.clone()
7263 }
7264 "members" => node.members = self.parse_extension_members()?,
7265 _ => self.skip_value(),
7266 }
7267 } else if self.check(TokenType::LBrace) {
7268 self.skip_braced_block()?;
7269 }
7270 }
7271 self.consume(TokenType::RBrace)?;
7272 Ok(node)
7273 }
7274
7275 fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7279 let mut members = Vec::new();
7280 self.consume(TokenType::LBracket)?;
7281 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7282 let name_tok = self.consume(TokenType::StringLit)?;
7283 let mut member = ExtensionMember {
7284 name: name_tok.value.clone(),
7285 semantics: None,
7286 default_confidence: None,
7287 loc: Loc {
7288 line: name_tok.line,
7289 column: name_tok.column,
7290 },
7291 };
7292 if self.check(TokenType::Colon) {
7294 self.advance();
7295 self.consume(TokenType::LBrace)?;
7296 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7297 let mkey = self.current().value.clone();
7298 self.advance();
7299 if self.check(TokenType::Colon) {
7300 self.advance();
7301 match mkey.as_str() {
7302 "semantics" => {
7303 member.semantics =
7304 Some(self.consume(TokenType::StringLit)?.value.clone())
7305 }
7306 "default_confidence" => {
7307 member.default_confidence = self.parse_optional_float()
7308 }
7309 _ => self.skip_value(),
7310 }
7311 }
7312 if self.check(TokenType::Comma) {
7313 self.advance();
7314 }
7315 }
7316 self.consume(TokenType::RBrace)?;
7317 }
7318 members.push(member);
7319 if self.check(TokenType::Comma) {
7320 self.advance();
7321 }
7322 }
7323 self.consume(TokenType::RBracket)?;
7324 Ok(members)
7325 }
7326
7327 fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7330 let tok = self.consume(TokenType::Window)?;
7331 let name = self.consume(TokenType::Identifier)?.value;
7332 let mut node = WindowDefinition {
7333 name,
7334 timezone: String::new(),
7335 allow: Vec::new(),
7336 exclude: Vec::new(),
7337 on_outside: String::new(),
7338 loc: Loc {
7339 line: tok.line,
7340 column: tok.column,
7341 },
7342 leading_trivia: Vec::new(),
7343 trailing_trivia: Vec::new(),
7344 };
7345 self.consume(TokenType::LBrace)?;
7346 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7347 let field_name = self.consume_any_ident_or_kw()?.value;
7348 self.consume(TokenType::Colon)?;
7349 match field_name.as_str() {
7350 "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7351 "allow" => node.allow = self.parse_window_allow()?,
7352 "exclude" => node.exclude = self.parse_window_exclude()?,
7353 "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7354 _ => self.skip_value(),
7355 }
7356 }
7357 self.consume(TokenType::RBrace)?;
7358 Ok(node)
7359 }
7360
7361 fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7363 self.consume(TokenType::LBracket)?;
7364 let mut spans = Vec::new();
7365 if !self.check(TokenType::RBracket) {
7366 spans.push(self.parse_window_span()?);
7367 while self.check(TokenType::Comma) {
7368 self.advance();
7369 if self.check(TokenType::RBracket) {
7370 break; }
7372 spans.push(self.parse_window_span()?);
7373 }
7374 }
7375 self.consume(TokenType::RBracket)?;
7376 Ok(spans)
7377 }
7378
7379 fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7383 self.consume(TokenType::LBracket)?;
7384 let mut dates = Vec::new();
7385 if !self.check(TokenType::RBracket) {
7386 dates.push(self.consume(TokenType::StringLit)?.value);
7387 while self.check(TokenType::Comma) {
7388 self.advance();
7389 if self.check(TokenType::RBracket) {
7390 break; }
7392 dates.push(self.consume(TokenType::StringLit)?.value);
7393 }
7394 }
7395 self.consume(TokenType::RBracket)?;
7396 Ok(dates)
7397 }
7398
7399 fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7401 let tok = self.consume(TokenType::LBrace)?;
7402 let mut span = WindowSpan {
7403 day_start: String::new(),
7404 day_end: String::new(),
7405 hour_start: 0,
7406 hour_end: 0,
7407 loc: Loc {
7408 line: tok.line,
7409 column: tok.column,
7410 },
7411 };
7412 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7413 let field = self.consume_any_ident_or_kw()?.value;
7414 self.consume(TokenType::Colon)?;
7415 match field.as_str() {
7416 "days" => {
7417 span.day_start = self.consume_any_ident_or_kw()?.value;
7418 self.consume(TokenType::DotDot)?;
7419 span.day_end = self.consume_any_ident_or_kw()?.value;
7420 }
7421 "hours" => {
7422 span.hour_start = self.consume_number()? as i64;
7423 self.consume(TokenType::DotDot)?;
7424 span.hour_end = self.consume_number()? as i64;
7425 }
7426 _ => self.skip_value(),
7427 }
7428 if self.check(TokenType::Comma) {
7429 self.advance();
7430 }
7431 }
7432 self.consume(TokenType::RBrace)?;
7433 Ok(span)
7434 }
7435
7436 fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7437 let tok = self.consume(TokenType::Shield)?;
7438 let name = self.consume(TokenType::Identifier)?.value;
7439 let mut node = ShieldDefinition {
7440 name,
7441 scan: Vec::new(),
7442 strategy: String::new(),
7443 on_breach: String::new(),
7444 severity: String::new(),
7445 quarantine: String::new(),
7446 max_retries: None,
7447 confidence_threshold: None,
7448 allow_tools: Vec::new(),
7449 deny_tools: Vec::new(),
7450 sandbox: None,
7451 redact: Vec::new(),
7452 log: String::new(),
7453 deflect_message: String::new(),
7454 taint: String::new(),
7455 compliance: Vec::new(),
7456 sign: String::new(),
7457 unknown_fields: Vec::new(),
7458 loc: Loc {
7459 line: tok.line,
7460 column: tok.column,
7461 },
7462 leading_trivia: Vec::new(),
7463 trailing_trivia: Vec::new(),
7464 };
7465 self.consume(TokenType::LBrace)?;
7466 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7467 let field_name = self.current().value.clone();
7468 let field_loc = Loc {
7469 line: self.current().line,
7470 column: self.current().column,
7471 };
7472 self.advance();
7473 if self.check(TokenType::Colon) {
7474 self.advance();
7475 match field_name.as_str() {
7476 "scan" => node.scan = self.parse_bracketed_identifiers()?,
7477 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7478 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7479 "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7480 "quarantine" => {
7481 node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7482 }
7483 "max_retries" => node.max_retries = self.parse_optional_int(),
7484 "confidence_threshold" => {
7485 node.confidence_threshold = self.parse_optional_float()
7486 }
7487 "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7488 "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7489 "sandbox" => {
7490 node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7491 }
7492 "redact" => node.redact = self.parse_bracketed_identifiers()?,
7493 "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7494 "deflect_message" => {
7495 node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7496 }
7497 "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7498 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7500 "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7503 _ => {
7507 node.unknown_fields.push((field_name.clone(), field_loc));
7508 self.skip_value()
7509 }
7510 }
7511 } else if self.check(TokenType::LBrace) {
7512 self.skip_braced_block()?;
7513 }
7514 }
7515 self.consume(TokenType::RBrace)?;
7516 Ok(node)
7517 }
7518
7519 fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7520 let tok = self.consume(TokenType::Pix)?;
7521 let name = self.consume(TokenType::Identifier)?.value;
7522 let mut node = PixDefinition {
7523 name,
7524 source: String::new(),
7525 depth: None,
7526 branching: None,
7527 model: String::new(),
7528 loc: Loc {
7529 line: tok.line,
7530 column: tok.column,
7531 },
7532 leading_trivia: Vec::new(),
7533 trailing_trivia: Vec::new(),
7534 };
7535 self.consume(TokenType::LBrace)?;
7536 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7537 let field_name = self.current().value.clone();
7538 self.advance();
7539 if self.check(TokenType::Colon) {
7540 self.advance();
7541 match field_name.as_str() {
7542 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7543 "depth" => node.depth = self.parse_optional_int(),
7544 "branching" => node.branching = self.parse_optional_int(),
7545 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7546 _ => self.skip_value(),
7547 }
7548 } else if self.check(TokenType::LBrace) {
7549 self.skip_braced_block()?;
7550 }
7551 }
7552 self.consume(TokenType::RBrace)?;
7553 Ok(node)
7554 }
7555
7556 fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7562 let tok = self.consume(TokenType::Ledger)?;
7563 let name = self.consume(TokenType::Identifier)?.value;
7564 let mut node = LedgerDefinition {
7565 name,
7566 source: String::new(),
7567 depth: None,
7568 branching: None,
7569 model: String::new(),
7570 loc: Loc {
7571 line: tok.line,
7572 column: tok.column,
7573 },
7574 leading_trivia: Vec::new(),
7575 trailing_trivia: Vec::new(),
7576 };
7577 self.consume(TokenType::LBrace)?;
7578 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7579 let field_name = self.current().value.clone();
7580 self.advance();
7581 if self.check(TokenType::Colon) {
7582 self.advance();
7583 match field_name.as_str() {
7584 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7585 "depth" => node.depth = self.parse_optional_int(),
7586 "branching" => node.branching = self.parse_optional_int(),
7587 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7588 _ => self.skip_value(),
7589 }
7590 } else if self.check(TokenType::LBrace) {
7591 self.skip_braced_block()?;
7592 }
7593 }
7594 self.consume(TokenType::RBrace)?;
7595 Ok(node)
7596 }
7597
7598 fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7599 let tok = self.consume(TokenType::Psyche)?;
7600 let name = self.consume(TokenType::Identifier)?.value;
7601 let mut node = PsycheDefinition {
7602 name,
7603 dimensions: Vec::new(),
7604 manifold_noise: None,
7605 manifold_momentum: None,
7606 safety_constraints: Vec::new(),
7607 quantum_enabled: None,
7608 inference_mode: String::new(),
7609 loc: Loc {
7610 line: tok.line,
7611 column: tok.column,
7612 },
7613 leading_trivia: Vec::new(),
7614 trailing_trivia: Vec::new(),
7615 };
7616 self.consume(TokenType::LBrace)?;
7617 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7618 let field_name = self.current().value.clone();
7619 self.advance();
7620 if self.check(TokenType::Colon) {
7621 self.advance();
7622 match field_name.as_str() {
7623 "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7624 "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7625 "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7626 "safety_constraints" | "safety" => {
7631 node.safety_constraints = self.parse_bracketed_identifiers()?
7632 }
7633 "quantum_enabled" => {
7634 node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7635 }
7636 "inference_mode" => {
7637 node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7638 }
7639 _ => self.skip_value(),
7640 }
7641 } else if self.check(TokenType::LBrace) {
7642 self.skip_braced_block()?;
7643 }
7644 }
7645 self.consume(TokenType::RBrace)?;
7646 Ok(node)
7647 }
7648
7649 fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7650 let tok = self.consume(TokenType::Corpus)?;
7651 let name = self.consume(TokenType::Identifier)?.value;
7652 let mut node = CorpusDefinition {
7653 name,
7654 documents: Vec::new(),
7655 relations: Vec::new(),
7656 adaptive: false,
7657 mcp_server: String::new(),
7658 mcp_resource_uri: String::new(),
7659 store_source: None,
7660 loc: Loc {
7661 line: tok.line,
7662 column: tok.column,
7663 },
7664 leading_trivia: Vec::new(),
7665 trailing_trivia: Vec::new(),
7666 };
7667 let mut dynamic = false;
7671 if self.check(TokenType::From) {
7672 self.advance();
7673 if self.check(TokenType::AxonStore) {
7674 self.advance();
7675 dynamic = true;
7676 } else {
7677 self.consume(TokenType::Mcp)?;
7678 self.consume(TokenType::LParen)?;
7679 node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7680 self.consume(TokenType::Comma)?;
7681 node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7682 self.consume(TokenType::RParen)?;
7683 return Ok(node);
7684 }
7685 }
7686 self.consume(TokenType::LBrace)?;
7687 let mut src = CorpusStoreSource {
7690 doc_store: String::new(),
7691 doc_id_col: String::new(),
7692 doc_title_col: String::new(),
7693 edge_store: String::new(),
7694 edge_from_col: String::new(),
7695 edge_to_col: String::new(),
7696 edge_type_col: String::new(),
7697 edge_weight_col: String::new(),
7698 loc: Loc {
7699 line: tok.line,
7700 column: tok.column,
7701 },
7702 };
7703 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7704 let field_name = self.current().value.clone();
7705 self.advance();
7706 if self.check(TokenType::Colon) {
7707 self.advance();
7708 match field_name.as_str() {
7709 "documents" if dynamic => {
7711 let (store, cols) = self.parse_corpus_store_mapping(2)?;
7712 src.doc_store = store;
7713 src.doc_id_col = cols[0].clone();
7714 src.doc_title_col = cols[1].clone();
7715 }
7716 "documents" => node.documents = self.parse_bracketed_identifiers()?,
7717 "relations" if dynamic => {
7719 let (store, cols) = self.parse_corpus_store_mapping(4)?;
7720 src.edge_store = store;
7721 src.edge_from_col = cols[0].clone();
7722 src.edge_to_col = cols[1].clone();
7723 src.edge_type_col = cols[2].clone();
7724 src.edge_weight_col = cols[3].clone();
7725 }
7726 "relations" => node.relations = self.parse_corpus_relations()?,
7728 "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7730 _ => self.skip_value(),
7731 }
7732 } else if self.check(TokenType::LBrace) {
7733 self.skip_braced_block()?;
7734 }
7735 }
7736 self.consume(TokenType::RBrace)?;
7737 if dynamic {
7738 node.store_source = Some(src);
7739 }
7740 Ok(node)
7741 }
7742
7743 fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7750 let store = self.consume(TokenType::Identifier)?.value.clone();
7751 self.consume(TokenType::LParen)?;
7752 let mut cols = Vec::with_capacity(n);
7753 for i in 0..n {
7754 if i > 0 {
7755 self.consume(TokenType::Comma)?;
7756 }
7757 cols.push(self.consume_any_ident_or_kw()?.value.clone());
7758 }
7759 self.consume(TokenType::RParen)?;
7760 Ok((store, cols))
7761 }
7762
7763 fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7769 let mut out = Vec::new();
7770 self.consume(TokenType::LBracket)?;
7771 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7772 if self.check(TokenType::Comma) {
7773 self.advance();
7774 continue;
7775 }
7776 let tok = self.current().clone();
7777 let etype = self.consume_any_ident_or_kw()?.value.clone();
7778 self.consume(TokenType::LParen)?;
7779 let from = self.consume_any_ident_or_kw()?.value.clone();
7780 self.consume(TokenType::Comma)?;
7781 let to = self.consume_any_ident_or_kw()?.value.clone();
7782 self.consume(TokenType::Comma)?;
7783 let weight = self.consume_number()?;
7784 self.consume(TokenType::RParen)?;
7785 out.push(CorpusRelation {
7786 etype,
7787 from,
7788 to,
7789 weight,
7790 loc: Loc { line: tok.line, column: tok.column },
7791 });
7792 }
7793 self.consume(TokenType::RBracket)?;
7794 Ok(out)
7795 }
7796
7797 fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7814 let tok = self.consume(TokenType::Dataspace)?;
7815 let name = self.consume(TokenType::Identifier)?.value;
7816 let mut node = DataspaceDefinition {
7817 name,
7818 columns: Vec::new(),
7819 loc: Loc {
7820 line: tok.line,
7821 column: tok.column,
7822 },
7823 leading_trivia: Vec::new(),
7824 trailing_trivia: Vec::new(),
7825 };
7826 if self.check(TokenType::LBrace) {
7827 self.consume(TokenType::LBrace)?;
7828 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7829 let entry = self.current().clone();
7830 if entry.value != "column" {
7831 return Err(ParseError {
7832 message: format!(
7833 "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7834 declares its columnar schema: `column <name>: <Type>` \
7835 (one per line, over the closed type catalog — \
7836 Text, Int, Float, Bool, Timestamp, Json).",
7837 entry.value, node.name
7838 ),
7839 line: entry.line,
7840 column: entry.column,
7841 ..Default::default()
7842 });
7843 }
7844 self.advance(); let col_tok = self.current().clone();
7846 let col_name = self.consume_any_ident_or_kw()?.value.clone();
7847 self.consume(TokenType::Colon)?;
7848 let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7849 node.columns.push(crate::ast::DataspaceColumn {
7850 name: col_name,
7851 declared_type,
7852 loc: Loc {
7853 line: col_tok.line,
7854 column: col_tok.column,
7855 },
7856 });
7857 }
7858 self.consume(TokenType::RBrace)?;
7859 }
7860 Ok(node)
7861 }
7862
7863 fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
7864 let tok = self.consume(TokenType::Ots)?;
7865 let name = self.consume(TokenType::Identifier)?.value;
7866 let mut node = OtsDefinition {
7867 name,
7868 teleology: String::new(),
7869 homotopy_search: String::new(),
7870 loss_function: String::new(),
7871 loc: Loc {
7872 line: tok.line,
7873 column: tok.column,
7874 },
7875 leading_trivia: Vec::new(),
7876 trailing_trivia: Vec::new(),
7877 };
7878 if self.check(TokenType::Lt) {
7880 while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
7881 self.advance();
7882 }
7883 if self.check(TokenType::Gt) {
7884 self.advance();
7885 }
7886 }
7887 self.consume(TokenType::LBrace)?;
7888 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7889 let field_name = self.current().value.clone();
7890 self.advance();
7891 if self.check(TokenType::Colon) {
7892 self.advance();
7893 match field_name.as_str() {
7894 "teleology" => {
7895 node.teleology = self.consume(TokenType::StringLit)?.value.clone()
7896 }
7897 "homotopy_search" => {
7898 node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
7899 }
7900 "loss_function" => {
7906 node.loss_function = if self.check(TokenType::StringLit) {
7907 self.consume(TokenType::StringLit)?.value.clone()
7908 } else {
7909 self.consume_any_ident_or_kw()?.value.clone()
7910 }
7911 }
7912 _ => self.skip_value(),
7913 }
7914 } else if self.check(TokenType::LBrace) {
7915 self.skip_braced_block()?;
7916 }
7917 }
7918 self.consume(TokenType::RBrace)?;
7919 Ok(node)
7920 }
7921
7922 fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
7923 let tok = self.consume(TokenType::Mandate)?;
7924 let name = self.consume(TokenType::Identifier)?.value;
7925 let mut node = MandateDefinition {
7926 name,
7927 constraint: String::new(),
7928 kp: None,
7929 ki: None,
7930 kd: None,
7931 tolerance: None,
7932 max_steps: None,
7933 drift_bound: None,
7934 lipschitz: None,
7935 on_violation: String::new(),
7936 loc: Loc {
7937 line: tok.line,
7938 column: tok.column,
7939 },
7940 leading_trivia: Vec::new(),
7941 trailing_trivia: Vec::new(),
7942 };
7943 self.consume(TokenType::LBrace)?;
7944 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7945 let field_name = self.current().value.clone();
7946 self.advance();
7947 if self.check(TokenType::Colon) {
7948 self.advance();
7949 match field_name.as_str() {
7950 "constraint" => {
7951 node.constraint = self.consume(TokenType::StringLit)?.value.clone()
7952 }
7953 "kp" | "Kp" => node.kp = self.parse_optional_float(),
7954 "ki" | "Ki" => node.ki = self.parse_optional_float(),
7955 "kd" | "Kd" => node.kd = self.parse_optional_float(),
7956 "max_steps" => node.max_steps = self.parse_optional_int(),
7957 "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
7962 "on_violation" => {
7963 node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
7964 }
7965 _ => self.skip_value(),
7966 }
7967 } else if self.check(TokenType::LBrace) {
7968 if field_name == "pid" {
7978 self.parse_pid_block(&mut node)?;
7979 } else if field_name == "stability" {
7980 self.parse_stability_block(&mut node)?;
7981 } else {
7982 self.skip_braced_block()?;
7983 }
7984 }
7985 }
7986 self.consume(TokenType::RBrace)?;
7987 Ok(node)
7988 }
7989
7990 fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8013 self.consume(TokenType::LBrace)?;
8014 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8015 let key_token = self.current().clone();
8016 let key = key_token.value.clone();
8017 self.advance();
8018 if self.check(TokenType::Colon) {
8019 self.advance();
8020 match key.as_str() {
8021 "kp" | "Kp" => node.kp = self.parse_optional_float(),
8022 "ki" | "Ki" => node.ki = self.parse_optional_float(),
8023 "kd" | "Kd" => node.kd = self.parse_optional_float(),
8024 _ => {
8025 return Err(ParseError {
8026 message: format!(
8027 "`{key}` is not a gain of the PID controller. The block accepts \
8028 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8029 Skipping what it does not recognise would let a typo drop a \
8030 gain, and the stability band is computed from all three."
8031 ),
8032 line: key_token.line,
8033 column: key_token.column,
8034 ..Default::default()
8035 });
8036 }
8037 }
8038 }
8039 if self.check(TokenType::Comma) {
8040 self.advance();
8041 }
8042 }
8043 self.consume(TokenType::RBrace)?;
8044 Ok(())
8045 }
8046
8047 fn parse_stability_block(
8063 &mut self,
8064 node: &mut MandateDefinition,
8065 ) -> Result<(), ParseError> {
8066 let open = self.consume(TokenType::LBrace)?;
8067 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8068 let key_token = self.current().clone();
8069 let key = key_token.value.clone();
8070 self.advance();
8071 if self.check(TokenType::Colon) {
8072 self.advance();
8073 match key.as_str() {
8074 "D" | "d" | "drift_bound" => {
8075 node.drift_bound = self.parse_optional_float()
8076 }
8077 "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8078 _ => {
8082 return Err(ParseError {
8083 message: format!(
8084 "`{key}` is not a hypothesis of the stability theorem. The block \
8085 accepts exactly `D` (the drift bound, also spelled `d` or \
8086 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8087 `lipschitz`). This is an error rather than a skipped key \
8088 because a bound that fails to parse is a bound that is not \
8089 declared, and the compiler would then verify the band it can \
8090 see — the sign conditions — and admit the mandate as if the \
8091 rest had been checked."
8092 ),
8093 line: key_token.line,
8094 column: key_token.column,
8095 ..Default::default()
8096 });
8097 }
8098 }
8099 }
8100 if self.check(TokenType::Comma) {
8101 self.advance();
8102 }
8103 }
8104 self.consume(TokenType::RBrace)?;
8105 if node.drift_bound.is_none() && node.lipschitz.is_none() {
8106 return Err(ParseError {
8107 message: "the `stability { }` block declares neither `D` nor `L` — it asserts nothing and can discharge nothing. Declare the drift bound (`D:`), the Lipschitz constant (`L:`), or both; or remove the block."
8108 .to_string(),
8109 line: open.line,
8110 column: open.column,
8111 ..Default::default()
8112 });
8113 }
8114 Ok(())
8115 }
8116
8117 fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8146 let tok = self.consume(TokenType::Compute)?;
8147 let name = self.consume(TokenType::Identifier)?.value;
8148 let mut node = ComputeDefinition {
8149 name,
8150 shield_ref: String::new(),
8151 parameters: Vec::new(),
8152 return_type: String::new(),
8153 body: None,
8154 loc: Loc {
8155 line: tok.line,
8156 column: tok.column,
8157 },
8158 leading_trivia: Vec::new(),
8159 trailing_trivia: Vec::new(),
8160 };
8161
8162 if self.check(TokenType::LParen) {
8164 self.advance();
8165 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8166 let ptok = self.current().clone();
8167 let pname = self.consume_any_ident_or_kw()?.value.clone();
8168 self.consume(TokenType::Colon)?;
8169 let ptype = self.parse_type_expr()?;
8170 node.parameters.push(Parameter {
8171 name: pname,
8172 type_expr: ptype,
8173 loc: self.loc_of(&ptok),
8174 });
8175 if self.check(TokenType::Comma) {
8176 self.advance();
8177 }
8178 }
8179 self.consume(TokenType::RParen)?;
8180 }
8181
8182 if self.check(TokenType::Arrow) {
8184 self.advance();
8185 node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8186 }
8187
8188 self.consume(TokenType::LBrace)?;
8189 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8190 let is_field = self
8200 .tokens
8201 .get(self.pos + 1)
8202 .map(|t| t.ttype == TokenType::Colon)
8203 .unwrap_or(false);
8204 if is_field {
8205 let field_tok = self.current().clone();
8206 let field_name = self.current().value.clone();
8207 self.advance();
8208 self.consume(TokenType::Colon)?;
8209 match field_name.as_str() {
8210 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8211 "input" => self.parse_compute_input_list(&mut node)?,
8221 "output" => {
8224 node.return_type = self.parse_output_type_string()?;
8225 }
8226 _ => self.skip_value(),
8227 }
8228 let _ = field_tok;
8229 } else if self.current().value == "logic"
8230 && self
8231 .tokens
8232 .get(self.pos + 1)
8233 .is_some_and(|t| t.ttype == TokenType::LBrace)
8234 {
8235 if node.body.is_some() {
8240 return Err(ParseError {
8241 message: "compute declares two bodies; a pure function has one result, \
8242 and keeping the last silently would discard the first"
8243 .to_string(),
8244 line: self.current().line,
8245 column: self.current().column,
8246 ..Default::default()
8247 });
8248 }
8249 node.body = Some(self.parse_logic_block()?);
8250 } else {
8251 node.body = Some(self.parse_expr()?);
8252 }
8253 }
8254 self.consume(TokenType::RBrace)?;
8255 Ok(node)
8256 }
8257
8258 fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8264 loop {
8265 let ptok = self.current().clone();
8266 let pname = self.consume_any_ident_or_kw()?.value.clone();
8267 let type_expr = if self.check(TokenType::LParen) {
8271 self.advance();
8272 let t = self.parse_type_expr()?;
8273 self.consume(TokenType::RParen)?;
8274 t
8275 } else {
8276 TypeExpr {
8277 name: String::new(),
8278 generic_param: String::new(),
8279 optional: false,
8280 loc: self.loc_of(&ptok),
8281 }
8282 };
8283 node.parameters.push(Parameter {
8284 name: pname,
8285 type_expr,
8286 loc: self.loc_of(&ptok),
8287 });
8288 if self.check(TokenType::Comma) {
8289 self.advance();
8290 } else {
8291 break;
8292 }
8293 }
8294 Ok(())
8295 }
8296
8297 fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8310 let open = self.current().clone();
8311 self.advance(); self.consume(TokenType::LBrace)?;
8313
8314 let mut bindings: Vec<(String, Expr)> = Vec::new();
8315 let mut result: Option<Expr> = None;
8316
8317 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8318 if self.check(TokenType::Let) {
8319 if result.is_some() {
8320 return Err(ParseError {
8321 message: "a `let` after the `return` in a `logic { }` block is \
8322 unreachable — the block's value is already decided. Move it \
8323 above the `return`."
8324 .to_string(),
8325 line: self.current().line,
8326 column: self.current().column,
8327 ..Default::default()
8328 });
8329 }
8330 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
8332 self.consume(TokenType::Assign)?;
8333 bindings.push((name, self.parse_expr()?));
8334 } else if self.check(TokenType::Return) {
8335 self.advance();
8336 result = Some(self.parse_expr()?);
8337 } else {
8338 let bad = self.current().clone();
8339 return Err(ParseError {
8340 message: format!(
8341 "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8342 <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8343 function (its own paper: \"pureza categórica de los morfismos \
8344 funcionales\"), so a statement that could have an effect is refused \
8345 rather than parsed and dropped.",
8346 bad.value
8347 ),
8348 line: bad.line,
8349 column: bad.column,
8350 ..Default::default()
8351 });
8352 }
8353 }
8354 self.consume(TokenType::RBrace)?;
8355
8356 let mut expr = result.ok_or_else(|| ParseError {
8357 message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8358 names and yields nothing, and the compute would have to invent a result — \
8359 which is precisely what a deterministic primitive must never do."
8360 .to_string(),
8361 line: open.line,
8362 column: open.column,
8363 ..Default::default()
8364 })?;
8365
8366 for (name, value) in bindings.into_iter().rev() {
8368 expr = Expr::Let {
8369 name,
8370 value: Box::new(value),
8371 body: Box::new(expr),
8372 };
8373 }
8374 Ok(expr)
8375 }
8376
8377 fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8378 let tok = self.consume(TokenType::Daemon)?;
8379 let name = self.consume(TokenType::Identifier)?.value;
8380 let mut node = DaemonDefinition {
8381 name,
8382 goal: String::new(),
8383 tools: Vec::new(),
8384 memory_ref: String::new(),
8385 strategy: String::new(),
8386 on_stuck: String::new(),
8387 shield_ref: String::new(),
8388 window_ref: String::new(),
8389 budget: None,
8390 max_tokens: None,
8391 max_time: String::new(),
8392 max_cost: None,
8393 listeners: Vec::new(),
8394 requires_capabilities: Vec::new(),
8395 loc: Loc {
8396 line: tok.line,
8397 column: tok.column,
8398 },
8399 leading_trivia: Vec::new(),
8400 trailing_trivia: Vec::new(),
8401 };
8402 while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8404 self.advance();
8405 }
8406 self.consume(TokenType::LBrace)?;
8407 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8408 let field = self.current().clone();
8409 let field_name = field.value.clone();
8410 self.advance();
8411 if self.check(TokenType::Colon) {
8412 self.advance();
8413 match field_name.as_str() {
8414 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8415 "tools" => node.tools = self.parse_bracketed_identifiers()?,
8416 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8417 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8418 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8419 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8420 "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8422 "max_tokens" => node.max_tokens = self.parse_optional_int(),
8423 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8424 "max_cost" => node.max_cost = self.parse_optional_float(),
8425 "requires" => {
8430 let bracket_tok = self.current().clone();
8431 let items = self.parse_bracketed_dot_identifiers()?;
8432 for slug in &items {
8433 if !is_valid_capability_slug(slug) {
8434 return Err(ParseError {
8435 message: format!(
8436 "Invalid capability slug '{slug}' in daemon '{}' \
8437 `requires:`. Capability slugs must match \
8438 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8439 lowercase identifiers. Examples: `daemon.run`, \
8440 `memory.write`, `flow.execute`.",
8441 node.name
8442 ),
8443 line: bracket_tok.line,
8444 column: bracket_tok.column,
8445 ..Default::default()
8446 });
8447 }
8448 }
8449 node.requires_capabilities = items;
8450 }
8451 _ => self.skip_value(),
8452 }
8453 } else if field.ttype == TokenType::Listen {
8454 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8460 (self.consume(TokenType::StringLit)?.value.clone(), false)
8461 } else {
8462 (self.consume_any_ident_or_kw()?.value.clone(), true)
8463 };
8464 let mut alias = String::new();
8465 if !self.at_declaration_start()
8466 && !self.check(TokenType::RBrace)
8467 && !self.check(TokenType::LBrace)
8468 {
8469 let next = self.current().clone();
8470 if next.value == "as" || next.ttype == TokenType::As {
8471 self.advance();
8472 alias = self.consume_any_ident_or_kw()?.value.clone();
8473 }
8474 }
8475 let listen_loc = Loc {
8476 line: field.line,
8477 column: field.column,
8478 };
8479 let body = self.parse_listener_body()?;
8483 node.listeners.push(ListenStep {
8484 channel,
8485 channel_is_ref,
8486 event_alias: alias,
8487 body,
8488 loc: listen_loc,
8489 });
8490 } else if field_name == "budget" && self.check(TokenType::LBrace) {
8491 node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8493 } else if self.check(TokenType::LBrace) {
8494 self.skip_braced_block()?;
8495 }
8496 }
8497 self.consume(TokenType::RBrace)?;
8498 Ok(node)
8499 }
8500
8501 fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8510 let kw = self.consume(TokenType::Budget)?; let name = self.consume(TokenType::Identifier)?.value;
8512 let mut block = self.parse_budget_block(kw.line, kw.column)?;
8513 block.name = name;
8514 Ok(block)
8515 }
8516
8517 fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8519 self.consume(TokenType::LBrace)?;
8520 let mut quotas = Vec::new();
8521 let mut on_exhausted = String::new();
8522 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8523 let field = self.current().clone();
8524 let field_name = self.consume_any_ident_or_kw()?.value;
8525 match field_name.as_str() {
8526 "rate" | "max" => {
8527 quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8528 }
8529 "on_exhausted" => {
8530 self.consume(TokenType::Colon)?;
8531 on_exhausted = self.consume_any_ident_or_kw()?.value;
8532 }
8533 _ => self.skip_value(),
8534 }
8535 }
8536 self.consume(TokenType::RBrace)?;
8537 Ok(BudgetBlock {
8538 name: String::new(),
8539 quotas,
8540 on_exhausted,
8541 loc: Loc { line, column },
8542 leading_trivia: Vec::new(),
8543 trailing_trivia: Vec::new(),
8544 })
8545 }
8546
8547 fn parse_budget_quota(
8550 &mut self,
8551 kind: String,
8552 line: u32,
8553 column: u32,
8554 ) -> Result<BudgetQuota, ParseError> {
8555 self.consume(TokenType::Colon)?;
8556 let limit = self.consume_number()? as i64;
8557 let _per = self.consume_any_ident_or_kw()?; let period = self.consume_any_ident_or_kw()?.value;
8560 let _on = self.consume_any_ident_or_kw()?; let _tool = self.consume_any_ident_or_kw()?; self.consume(TokenType::LParen)?;
8564 let effect = self.consume_any_ident_or_kw()?.value;
8565 self.consume(TokenType::RParen)?;
8566 Ok(BudgetQuota {
8567 kind,
8568 limit,
8569 period,
8570 effect,
8571 loc: Loc { line, column },
8572 })
8573 }
8574
8575 fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8576 let tok = self.consume(TokenType::AxonStore)?;
8577 let name = self.consume(TokenType::Identifier)?.value;
8578 let mut node = AxonStoreDefinition {
8579 name,
8580 backend: String::new(),
8581 connection: String::new(),
8582 resource_ref: String::new(),
8583 confidence_floor: None,
8584 isolation: String::new(),
8585 on_breach: String::new(),
8586 capability: String::new(),
8587 class: String::new(),
8588 column_schema: None,
8589 loc: Loc {
8590 line: tok.line,
8591 column: tok.column,
8592 },
8593 leading_trivia: Vec::new(),
8594 trailing_trivia: Vec::new(),
8595 };
8596 self.consume(TokenType::LBrace)?;
8597 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8598 let field = self.current().clone();
8599 let field_name = field.value.clone();
8600 if field.ttype == TokenType::Schema {
8606 self.advance();
8607 let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8608 node.column_schema = Some(parsed);
8609 continue;
8610 }
8611 self.advance();
8612 if self.check(TokenType::Colon) {
8613 self.advance();
8614 match field_name.as_str() {
8615 "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8616 "class" => node.class = self.parse_dotted_identifier()?,
8624 "connection" => node.connection = self.parse_config_key()?,
8625 "resource" => {
8631 node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8632 }
8633 "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8634 "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8635 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8636 "capability" => {
8640 let slug_tok = self.consume(TokenType::StringLit)?.clone();
8641 if !is_valid_capability_slug(&slug_tok.value) {
8642 return Err(ParseError {
8643 message: format!(
8644 "Invalid capability slug '{}' in axonstore '{}' \
8645 `capability:`. Capability slugs must match \
8646 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8647 lowercase identifiers starting with a letter. Examples: \
8648 `admin`, `tenant.read`, `hipaa.phi.read`.",
8649 slug_tok.value, node.name
8650 ),
8651 line: slug_tok.line,
8652 column: slug_tok.column,
8653 ..Default::default()
8654 });
8655 }
8656 node.capability = slug_tok.value.clone();
8657 }
8658 _ => self.skip_value(),
8659 }
8660 } else if self.check(TokenType::LBrace) {
8661 self.skip_braced_block()?;
8662 }
8663 }
8664 self.consume(TokenType::RBrace)?;
8665 Ok(node)
8666 }
8667
8668 fn parse_store_schema_declaration(
8680 &mut self,
8681 store_name: &str,
8682 sch_line: u32,
8683 sch_col: u32,
8684 ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8685 use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8686
8687 if self.check(TokenType::LBrace) {
8689 self.consume(TokenType::LBrace)?;
8690 let mut columns: Vec<StoreColumn> = Vec::new();
8691 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8692 let col_tok = self.current().clone();
8693 let col_name = self.consume_any_ident_or_kw()?.value.clone();
8694 self.consume(TokenType::Colon)?;
8695 let type_tok = self.consume_any_ident_or_kw()?.clone();
8696 let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8697 let names = StoreColumnType::all_canonical_names();
8698 let suggestion =
8699 crate::smart_suggest::suggest_for(&type_tok.value, &names);
8700 let suggest_suffix = if suggestion.is_empty() {
8701 String::new()
8702 } else {
8703 format!(" {suggestion}")
8704 };
8705 let known = names.join(", ");
8706 ParseError {
8707 message: format!(
8708 "Unknown column type `{}` for column `{}` in \
8709 axonstore `{}` `schema:` block. The closed \
8710 v1.38.0 column-type catalog \
8711 is {{{known}}} (plus common lowercase \
8712 aliases — `int`/`integer`/`int4` for \
8713 `Int`, `bool`/`boolean` for `Bool`, etc.).\
8714 {suggest_suffix}",
8715 type_tok.value, col_name, store_name
8716 ),
8717 line: type_tok.line,
8718 column: type_tok.column,
8719 ..Default::default()
8720 }
8721 })?;
8722
8723 let mut json_shape: Option<String> = None;
8733 if self.check(TokenType::Lt) {
8734 self.advance();
8735 let shape_tok = self.consume_any_ident_or_kw()?.clone();
8736 self.consume(TokenType::Gt)?;
8737 if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8738 json_shape = Some(shape_tok.value.clone());
8739 } else {
8740 return Err(ParseError {
8741 message: format!(
8742 "axon-T841 a shape lens `<{shape}>` may refine \
8743 only a `Json` / `Jsonb` column, but column \
8744 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8745 the `<{shape}>` (a rigid column already has a \
8746 fixed shape), or change the column type to \
8747 `Json<{shape}>` if it carries open documents.",
8748 shape = shape_tok.value,
8749 col = col_name,
8750 store = store_name,
8751 ty = col_type.canonical_name(),
8752 ),
8753 line: shape_tok.line,
8754 column: shape_tok.column,
8755 ..Default::default()
8756 });
8757 }
8758 }
8759
8760 let mut col = StoreColumn {
8761 name: col_name,
8762 col_type,
8763 json_shape,
8764 primary_key: false,
8765 auto_increment: false,
8766 not_null: false,
8767 unique: false,
8768 indexed: false,
8769 default_value: String::new(),
8770 identity: false,
8775 line: col_tok.line,
8776 column: col_tok.column,
8777 };
8778
8779 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8782 if self.current().ttype != TokenType::Identifier {
8783 break;
8786 }
8787 let constraint = self.current().value.clone();
8788 match constraint.as_str() {
8789 "primary_key" => {
8790 col.primary_key = true;
8791 self.advance();
8792 }
8793 "auto_increment" => {
8794 col.auto_increment = true;
8795 self.advance();
8796 }
8797 "not_null" => {
8798 col.not_null = true;
8799 self.advance();
8800 }
8801 "unique" => {
8802 col.unique = true;
8803 self.advance();
8804 }
8805 "index" => {
8811 col.indexed = true;
8812 self.advance();
8813 }
8814 "identity" => {
8825 col.identity = true;
8826 self.advance();
8827 }
8828 "default" => {
8829 self.advance();
8830 let dv = self.current().clone();
8831 if matches!(
8832 dv.ttype,
8833 TokenType::StringLit
8834 | TokenType::Integer
8835 | TokenType::Float
8836 ) {
8837 col.default_value = dv.value.clone();
8838 self.advance();
8839 } else {
8840 col.default_value =
8841 self.consume_any_ident_or_kw()?.value.clone();
8842 }
8843 }
8844 _ => break,
8845 }
8846 }
8847
8848 columns.push(col);
8849 }
8850 self.consume(TokenType::RBrace)?;
8851 return Ok(StoreColumnSchema::Inline {
8852 columns,
8853 leading_trivia: Vec::new(),
8854 line: sch_line,
8855 column: sch_col,
8856 });
8857 }
8858
8859 if !self.check(TokenType::Colon) {
8861 let cur = self.current().clone();
8862 return Err(ParseError {
8863 message: format!(
8864 "axonstore `{store_name}` `schema:` declaration expects \
8865 `{{ … }}` (inline columns), `: \"manifest.ref\"` \
8866 (manifest reference), or `: env:VAR` (per-tenant schema \
8867 namespace). Got `{}` instead.",
8868 cur.value
8869 ),
8870 line: cur.line,
8871 column: cur.column,
8872 ..Default::default()
8873 });
8874 }
8875 self.consume(TokenType::Colon)?;
8876
8877 if self.check(TokenType::StringLit) {
8879 let lit = self.consume(TokenType::StringLit)?.clone();
8880 let value = lit.value.clone();
8881 if let Some(var) = value.strip_prefix("env:") {
8882 let var = var.trim();
8883 if var.is_empty() {
8884 return Err(ParseError {
8885 message: format!(
8886 "axonstore `{store_name}` `schema: \"env:\"` is \
8887 missing the variable name after the `env:` \
8888 prefix."
8889 ),
8890 line: lit.line,
8891 column: lit.column,
8892 ..Default::default()
8893 });
8894 }
8895 return Ok(StoreColumnSchema::EnvVar {
8896 var_name: var.to_string(),
8897 line: sch_line,
8898 column: sch_col,
8899 });
8900 }
8901 if value.trim().is_empty() {
8903 return Err(ParseError {
8904 message: format!(
8905 "axonstore `{store_name}` `schema:` manifest reference \
8906 is empty. Expected `\"qualified.name\"` — e.g. \
8907 `\"public.tenants\"`."
8908 ),
8909 line: lit.line,
8910 column: lit.column,
8911 ..Default::default()
8912 });
8913 }
8914 return Ok(StoreColumnSchema::ManifestRef {
8915 qualified_name: value,
8916 line: sch_line,
8917 column: sch_col,
8918 });
8919 }
8920
8921 let env_tok = self.current().clone();
8924 if env_tok.value == "env" {
8925 self.advance();
8926 if !self.check(TokenType::Colon) {
8927 return Err(ParseError {
8928 message: format!(
8929 "axonstore `{store_name}` `schema: env` is missing the \
8930 `:` separator. Expected `schema: env:VAR`."
8931 ),
8932 line: env_tok.line,
8933 column: env_tok.column,
8934 ..Default::default()
8935 });
8936 }
8937 self.advance(); let var_tok = self.consume_any_ident_or_kw()?.clone();
8939 if var_tok.value.trim().is_empty() {
8940 return Err(ParseError {
8941 message: format!(
8942 "axonstore `{store_name}` `schema: env:` is missing \
8943 the variable name."
8944 ),
8945 line: var_tok.line,
8946 column: var_tok.column,
8947 ..Default::default()
8948 });
8949 }
8950 return Ok(StoreColumnSchema::EnvVar {
8951 var_name: var_tok.value.clone(),
8952 line: sch_line,
8953 column: sch_col,
8954 });
8955 }
8956
8957 Err(ParseError {
8958 message: format!(
8959 "axonstore `{store_name}` `schema:` declaration expects \
8960 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
8961 reference), or `env:VAR` (per-tenant schema namespace). \
8962 Got `{}` instead.",
8963 env_tok.value
8964 ),
8965 line: env_tok.line,
8966 column: env_tok.column,
8967 ..Default::default()
8968 })
8969 }
8970
8971 fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
8978 let tok = self.consume(TokenType::Resource)?;
8979 let name = self.consume(TokenType::Identifier)?.value;
8980 let mut node = ResourceDefinition {
8981 name,
8982 kind: String::new(),
8983 endpoint: String::new(),
8984 capacity: None,
8985 lifetime: "affine".to_string(),
8986 certainty_floor: None,
8987 shield_ref: String::new(),
8988 within: String::new(),
8989 loc: Loc {
8990 line: tok.line,
8991 column: tok.column,
8992 },
8993 leading_trivia: Vec::new(),
8994 trailing_trivia: Vec::new(),
8995 };
8996 self.consume(TokenType::LBrace)?;
8997 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8998 let field_tok = self.current().clone();
8999 let field_name = field_tok.value.clone();
9000 self.advance();
9001 if !self.check(TokenType::Colon) {
9002 if self.check(TokenType::LBrace) {
9004 self.skip_braced_block()?;
9005 }
9006 continue;
9007 }
9008 self.advance(); match field_name.as_str() {
9010 "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9011 "endpoint" => {
9024 node.endpoint = if self.check(TokenType::StringLit) {
9025 self.consume(TokenType::StringLit)?.value
9026 } else {
9027 self.parse_dotted_identifier()?
9028 };
9029 }
9030 "capacity" => {
9031 node.capacity = self.parse_optional_int();
9032 }
9033 "lifetime" => {
9034 let lt_tok = self.consume_any_ident_or_kw()?;
9035 let lt = lt_tok.value;
9036 if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9037 return Err(ParseError {
9038 message: format!(
9039 "Invalid lifetime '{lt}' in resource '{}' — \
9040 expected linear | affine | persistent",
9041 node.name
9042 ),
9043 line: lt_tok.line,
9044 column: lt_tok.column,
9045 ..Default::default()
9046 });
9047 }
9048 node.lifetime = lt;
9049 }
9050 "certainty_floor" => {
9051 node.certainty_floor = self.parse_optional_float();
9052 }
9053 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9054 "within" => node.within = self.consume_any_ident_or_kw()?.value,
9058 unknown => {
9068 return Err(ParseError {
9069 message: format!(
9070 "Unknown field '{unknown}' in resource '{}' — expected one of: \
9071 kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9072 node.name
9073 ),
9074 line: field_tok.line,
9075 column: field_tok.column,
9076 ..Default::default()
9077 });
9078 }
9079 }
9080 }
9081 self.consume(TokenType::RBrace)?;
9082 Ok(node)
9083 }
9084
9085 fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9087 let tok = self.consume(TokenType::Fabric)?;
9088 let name = self.consume(TokenType::Identifier)?.value;
9089 let mut node = FabricDefinition {
9090 name,
9091 provider: String::new(),
9092 region: String::new(),
9093 zones: None,
9094 ephemeral: None,
9095 shield_ref: String::new(),
9096 loc: Loc {
9097 line: tok.line,
9098 column: tok.column,
9099 },
9100 leading_trivia: Vec::new(),
9101 trailing_trivia: Vec::new(),
9102 };
9103 self.consume(TokenType::LBrace)?;
9104 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9105 let field_name = self.current().value.clone();
9106 self.advance();
9107 if !self.check(TokenType::Colon) {
9108 if self.check(TokenType::LBrace) {
9109 self.skip_braced_block()?;
9110 }
9111 continue;
9112 }
9113 self.advance(); match field_name.as_str() {
9115 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9116 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9117 "zones" => node.zones = self.parse_optional_int(),
9118 "ephemeral" => {
9119 let b = self.parse_bool()?;
9120 node.ephemeral = Some(b);
9121 }
9122 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9123 _ => self.skip_value(),
9124 }
9125 }
9126 self.consume(TokenType::RBrace)?;
9127 Ok(node)
9128 }
9129
9130 fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9132 let tok = self.consume(TokenType::Manifest)?;
9133 let name = self.consume(TokenType::Identifier)?.value;
9134 let mut node = ManifestDefinition {
9135 name,
9136 resources: Vec::new(),
9137 fabric_ref: String::new(),
9138 region: String::new(),
9139 zones: None,
9140 compliance: Vec::new(),
9141 loc: Loc {
9142 line: tok.line,
9143 column: tok.column,
9144 },
9145 leading_trivia: Vec::new(),
9146 trailing_trivia: Vec::new(),
9147 };
9148 self.consume(TokenType::LBrace)?;
9149 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9150 let field_name = self.current().value.clone();
9151 self.advance();
9152 if !self.check(TokenType::Colon) {
9153 if self.check(TokenType::LBrace) {
9154 self.skip_braced_block()?;
9155 }
9156 continue;
9157 }
9158 self.advance();
9159 match field_name.as_str() {
9160 "resources" => node.resources = self.parse_bracketed_identifiers()?,
9161 "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9162 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9163 "zones" => node.zones = self.parse_optional_int(),
9164 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9165 _ => self.skip_value(),
9166 }
9167 }
9168 self.consume(TokenType::RBrace)?;
9169 Ok(node)
9170 }
9171
9172 fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9174 let tok = self.consume(TokenType::Observe)?;
9175 let name = self.consume(TokenType::Identifier)?.value;
9176 self.consume(TokenType::From)?;
9178 let target = self.consume(TokenType::Identifier)?.value;
9179 let mut node = ObserveDefinition {
9180 name,
9181 target,
9182 sources: Vec::new(),
9183 quorum: None,
9184 timeout: String::new(),
9185 on_partition: "fail".to_string(),
9186 certainty_floor: None,
9187 loc: Loc {
9188 line: tok.line,
9189 column: tok.column,
9190 },
9191 leading_trivia: Vec::new(),
9192 trailing_trivia: Vec::new(),
9193 };
9194 self.consume(TokenType::LBrace)?;
9195 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9196 let field_name = self.current().value.clone();
9197 self.advance();
9198 if !self.check(TokenType::Colon) {
9199 if self.check(TokenType::LBrace) {
9200 self.skip_braced_block()?;
9201 }
9202 continue;
9203 }
9204 self.advance();
9205 match field_name.as_str() {
9206 "sources" => node.sources = self.parse_bracketed_identifiers()?,
9207 "quorum" => node.quorum = self.parse_optional_int(),
9208 "timeout" => {
9209 let t = self.current().clone();
9210 match t.ttype {
9211 TokenType::Duration | TokenType::StringLit => {
9212 self.advance();
9213 node.timeout = t.value;
9214 }
9215 _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9216 }
9217 }
9218 "on_partition" => {
9219 let p_tok = self.consume_any_ident_or_kw()?;
9220 let p = p_tok.value;
9221 if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9222 return Err(ParseError {
9223 message: format!(
9224 "Invalid on_partition '{p}' in observe '{}' — \
9225 expected fail | shield_quarantine",
9226 node.name
9227 ),
9228 line: p_tok.line,
9229 column: p_tok.column,
9230 ..Default::default()
9231 });
9232 }
9233 node.on_partition = p;
9234 }
9235 "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9236 _ => self.skip_value(),
9237 }
9238 }
9239 self.consume(TokenType::RBrace)?;
9240 Ok(node)
9241 }
9242
9243 fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9247 let tok = self.consume(TokenType::Reconcile)?;
9248 let name = self.consume(TokenType::Identifier)?.value;
9249 let mut node = ReconcileDefinition {
9250 name,
9251 observe_ref: String::new(),
9252 threshold: None,
9253 tolerance: None,
9254 on_drift: "provision".to_string(),
9255 shield_ref: String::new(),
9256 mandate_ref: String::new(),
9257 max_retries: 3,
9258 loc: Loc {
9259 line: tok.line,
9260 column: tok.column,
9261 },
9262 leading_trivia: Vec::new(),
9263 trailing_trivia: Vec::new(),
9264 };
9265 self.consume(TokenType::LBrace)?;
9266 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9267 let field_name = self.current().value.clone();
9268 self.advance();
9269 if !self.check(TokenType::Colon) {
9270 if self.check(TokenType::LBrace) {
9271 self.skip_braced_block()?;
9272 }
9273 continue;
9274 }
9275 self.advance();
9276 match field_name.as_str() {
9277 "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9278 "threshold" => node.threshold = self.parse_optional_float(),
9279 "tolerance" => node.tolerance = self.parse_optional_float(),
9280 "on_drift" => {
9281 let d_tok = self.consume_any_ident_or_kw()?;
9282 let d = d_tok.value;
9283 if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9284 return Err(ParseError {
9285 message: format!(
9286 "Invalid on_drift '{d}' in reconcile '{}' — \
9287 expected provision | alert | refine",
9288 node.name
9289 ),
9290 line: d_tok.line,
9291 column: d_tok.column,
9292 ..Default::default()
9293 });
9294 }
9295 node.on_drift = d;
9296 }
9297 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9298 "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9299 "max_retries" => {
9300 if let Some(v) = self.parse_optional_int() {
9301 node.max_retries = v;
9302 }
9303 }
9304 _ => self.skip_value(),
9305 }
9306 }
9307 self.consume(TokenType::RBrace)?;
9308 Ok(node)
9309 }
9310
9311 fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9313 let tok = self.consume(TokenType::Lease)?;
9314 let name = self.consume(TokenType::Identifier)?.value;
9315 let mut node = LeaseDefinition {
9316 name,
9317 resource_ref: String::new(),
9318 duration: String::new(),
9319 acquire: "on_start".to_string(),
9320 on_expire: "anchor_breach".to_string(),
9321 loc: Loc {
9322 line: tok.line,
9323 column: tok.column,
9324 },
9325 leading_trivia: Vec::new(),
9326 trailing_trivia: Vec::new(),
9327 };
9328 self.consume(TokenType::LBrace)?;
9329 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9330 let field_name = self.current().value.clone();
9331 self.advance();
9332 if !self.check(TokenType::Colon) {
9333 if self.check(TokenType::LBrace) {
9334 self.skip_braced_block()?;
9335 }
9336 continue;
9337 }
9338 self.advance();
9339 match field_name.as_str() {
9340 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9341 "duration" => {
9342 let t = self.current().clone();
9343 match t.ttype {
9344 TokenType::Duration | TokenType::StringLit => {
9345 self.advance();
9346 node.duration = t.value;
9347 }
9348 _ => node.duration = self.consume_any_ident_or_kw()?.value,
9349 }
9350 }
9351 "acquire" => {
9352 let a_tok = self.consume_any_ident_or_kw()?;
9353 let a = a_tok.value;
9354 if !matches!(a.as_str(), "on_start" | "on_demand") {
9355 return Err(ParseError {
9356 message: format!(
9357 "Invalid acquire '{a}' in lease '{}' — \
9358 expected on_start | on_demand",
9359 node.name
9360 ),
9361 line: a_tok.line,
9362 column: a_tok.column,
9363 ..Default::default()
9364 });
9365 }
9366 node.acquire = a;
9367 }
9368 "on_expire" => {
9369 let e_tok = self.consume_any_ident_or_kw()?;
9370 let e = e_tok.value;
9371 if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9372 return Err(ParseError {
9373 message: format!(
9374 "Invalid on_expire '{e}' in lease '{}' — \
9375 expected anchor_breach | release | extend",
9376 node.name
9377 ),
9378 line: e_tok.line,
9379 column: e_tok.column,
9380 ..Default::default()
9381 });
9382 }
9383 node.on_expire = e;
9384 }
9385 _ => self.skip_value(),
9386 }
9387 }
9388 self.consume(TokenType::RBrace)?;
9389 Ok(node)
9390 }
9391
9392 fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9394 let tok = self.consume(TokenType::Ensemble)?;
9395 let name = self.consume(TokenType::Identifier)?.value;
9396 let mut node = EnsembleDefinition {
9397 name,
9398 observations: Vec::new(),
9399 quorum: None,
9400 aggregation: "majority".to_string(),
9401 certainty_mode: "min".to_string(),
9402 loc: Loc {
9403 line: tok.line,
9404 column: tok.column,
9405 },
9406 leading_trivia: Vec::new(),
9407 trailing_trivia: Vec::new(),
9408 };
9409 self.consume(TokenType::LBrace)?;
9410 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9411 let field_name = self.current().value.clone();
9412 self.advance();
9413 if !self.check(TokenType::Colon) {
9414 if self.check(TokenType::LBrace) {
9415 self.skip_braced_block()?;
9416 }
9417 continue;
9418 }
9419 self.advance();
9420 match field_name.as_str() {
9421 "observations" => node.observations = self.parse_bracketed_identifiers()?,
9422 "quorum" => node.quorum = self.parse_optional_int(),
9423 "aggregation" => {
9424 let a_tok = self.consume_any_ident_or_kw()?;
9425 let a = a_tok.value;
9426 if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9427 return Err(ParseError {
9428 message: format!(
9429 "Invalid aggregation '{a}' in ensemble '{}' — \
9430 expected majority | weighted | byzantine",
9431 node.name
9432 ),
9433 line: a_tok.line,
9434 column: a_tok.column,
9435 ..Default::default()
9436 });
9437 }
9438 node.aggregation = a;
9439 }
9440 "certainty_mode" => {
9441 let c_tok = self.consume_any_ident_or_kw()?;
9442 let c = c_tok.value;
9443 if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9444 return Err(ParseError {
9445 message: format!(
9446 "Invalid certainty_mode '{c}' in ensemble '{}' — \
9447 expected min | weighted | harmonic",
9448 node.name
9449 ),
9450 line: c_tok.line,
9451 column: c_tok.column,
9452 ..Default::default()
9453 });
9454 }
9455 node.certainty_mode = c;
9456 }
9457 _ => self.skip_value(),
9458 }
9459 }
9460 self.consume(TokenType::RBrace)?;
9461 Ok(node)
9462 }
9463
9464 fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9472 let tok = self.consume(TokenType::Session)?;
9473 let name = self.consume(TokenType::Identifier)?.value;
9474 let mut node = SessionDefinition {
9475 name,
9476 roles: Vec::new(),
9477 loc: Loc {
9478 line: tok.line,
9479 column: tok.column,
9480 },
9481 leading_trivia: Vec::new(),
9482 trailing_trivia: Vec::new(),
9483 };
9484 self.consume(TokenType::LBrace)?;
9485 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9486 let role_tok = self.consume_any_ident_or_kw()?;
9487 self.consume(TokenType::Colon)?;
9488 let steps = self.parse_session_steps()?;
9489 node.roles.push(SessionRole {
9490 name: role_tok.value,
9491 steps,
9492 loc: Loc {
9493 line: role_tok.line,
9494 column: role_tok.column,
9495 },
9496 });
9497 }
9498 self.consume(TokenType::RBrace)?;
9499 Ok(node)
9500 }
9501
9502 fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9515 let tok = self.consume(TokenType::Observable)?;
9516 let name = self.consume(TokenType::Identifier)?.value;
9517 let mut node = ObservableDefinition {
9518 name,
9519 qubits: None,
9520 terms: Vec::new(),
9521 loc: Loc {
9522 line: tok.line,
9523 column: tok.column,
9524 },
9525 leading_trivia: Vec::new(),
9526 trailing_trivia: Vec::new(),
9527 };
9528 self.consume(TokenType::LBrace)?;
9529 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9530 let key_tok = self.consume_any_ident_or_kw()?;
9531 self.consume(TokenType::Colon)?;
9532 match key_tok.value.as_str() {
9533 "qubits" => node.qubits = Some(self.consume_number()? as i64),
9534 "term" => {
9535 let term_loc = Loc {
9536 line: key_tok.line,
9537 column: key_tok.column,
9538 };
9539 let mut negative = false;
9541 if self.check(TokenType::Minus) {
9542 self.advance();
9543 negative = true;
9544 } else if self.check(TokenType::Plus) {
9545 self.advance();
9546 }
9547 let mag = self.consume_number()?;
9548 let coefficient = if negative { -mag } else { mag };
9549 self.consume(TokenType::Star)?;
9551 let pauli = self.consume(TokenType::StringLit)?.value;
9552 node.terms.push(PauliTerm {
9553 coefficient,
9554 pauli,
9555 loc: term_loc,
9556 });
9557 }
9558 _ => self.skip_value(),
9559 }
9560 }
9561 self.consume(TokenType::RBrace)?;
9562 Ok(node)
9563 }
9564
9565 fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9573 let tok = self.consume(TokenType::Witness)?;
9574 let name = self.consume(TokenType::Identifier)?.value;
9575 let mut node = WitnessDefinition {
9576 name,
9577 claim: String::new(),
9578 baseline: String::new(),
9579 metric: String::new(),
9580 threshold: 0.0,
9581 data: String::new(),
9582 loc: Loc {
9583 line: tok.line,
9584 column: tok.column,
9585 },
9586 leading_trivia: Vec::new(),
9587 trailing_trivia: Vec::new(),
9588 };
9589 self.consume(TokenType::LBrace)?;
9590 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9591 let key_tok = self.consume_any_ident_or_kw()?;
9592 self.consume(TokenType::Colon)?;
9593 match key_tok.value.as_str() {
9594 "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9595 "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9598 "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9599 "threshold" => node.threshold = self.consume_number()?,
9600 "data" => node.data = self.consume_any_ident_or_kw()?.value,
9601 _ => self.skip_value(),
9602 }
9603 }
9604 self.consume(TokenType::RBrace)?;
9605 Ok(node)
9606 }
9607
9608 fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9613 let tok = self.consume(TokenType::Socket)?;
9614 let name = self.consume(TokenType::Identifier)?.value;
9615 let mut node = SocketDefinition {
9616 name,
9617 loc: Loc { line: tok.line, column: tok.column },
9618 ..Default::default()
9619 };
9620 self.consume(TokenType::LBrace)?;
9621 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9622 let key = self.consume_any_ident_or_kw()?.value;
9623 self.consume(TokenType::Colon)?;
9624 match key.as_str() {
9625 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9626 "backpressure" => {
9627 let kind = self.consume_any_ident_or_kw()?.value;
9629 if kind != "credit" {
9630 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9631 }
9632 self.consume(TokenType::LParen)?;
9633 let n = self
9634 .consume(TokenType::Integer)?
9635 .value
9636 .parse::<i64>()
9637 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9638 self.consume(TokenType::RParen)?;
9639 node.backpressure_credit = Some(n);
9640 }
9641 "reconnect" => {
9642 let mode = self.consume_any_ident_or_kw()?.value;
9643 node.reconnect = mode == "cognitive_state";
9644 }
9645 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9646 other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9647 }
9648 if self.check(TokenType::Comma) {
9650 self.consume(TokenType::Comma)?;
9651 }
9652 }
9653 self.consume(TokenType::RBrace)?;
9654 Ok(node)
9655 }
9656
9657 fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9665 let tok = self.consume(TokenType::Upstream)?;
9666 let name = self.consume(TokenType::Identifier)?.value;
9667 let mut node = UpstreamDefinition {
9668 name,
9669 loc: Loc { line: tok.line, column: tok.column },
9670 ..Default::default()
9671 };
9672 if self.check(TokenType::From) {
9674 self.advance();
9675 let base = self.consume(TokenType::Identifier)?.value;
9676 self.consume(TokenType::At)?;
9677 let version = self.consume_any_ident_or_kw()?.value;
9678 node.preset = Some(format!("{base}@{version}"));
9679 }
9680 self.consume(TokenType::LBrace)?;
9681 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9682 let key = self.consume_any_ident_or_kw()?.value;
9683 self.consume(TokenType::Colon)?;
9684 match key.as_str() {
9685 "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9686 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9687 "role" => node.role = self.consume_any_ident_or_kw()?.value,
9688 "resolve" => node.resolve = self.parse_dotted_identifier()?,
9689 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9693 "secret" => node.secret = self.parse_dotted_identifier()?,
9694 "auth" => {
9695 node.auth_kind = self.consume_any_ident_or_kw()?.value;
9698 if self.check(TokenType::LParen) {
9699 self.consume(TokenType::LParen)?;
9700 node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9701 if self.check(TokenType::Comma) {
9702 self.consume(TokenType::Comma)?;
9703 node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9704 }
9705 self.consume(TokenType::RParen)?;
9706 }
9707 }
9708 "map" => node.map = self.parse_upstream_map()?,
9709 "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9710 "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9711 "backpressure" => {
9712 let kind = self.consume_any_ident_or_kw()?.value;
9714 if kind != "credit" {
9715 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9716 }
9717 self.consume(TokenType::LParen)?;
9718 let n = self
9719 .consume(TokenType::Integer)?
9720 .value
9721 .parse::<i64>()
9722 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9723 self.consume(TokenType::RParen)?;
9724 node.backpressure_credit = Some(n);
9725 }
9726 other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9727 }
9728 if self.check(TokenType::Comma) {
9730 self.consume(TokenType::Comma)?;
9731 }
9732 }
9733 self.consume(TokenType::RBrace)?;
9734 Ok(node)
9735 }
9736
9737 fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9746 let tok = self.consume(TokenType::Cors)?;
9747 let name = self.consume(TokenType::Identifier)?.value;
9748 let mut node = CorsDefinition {
9749 name,
9750 loc: Loc { line: tok.line, column: tok.column },
9751 ..Default::default()
9752 };
9753 self.consume(TokenType::LBrace)?;
9754 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9755 let key = self.consume_any_ident_or_kw()?.value;
9756 self.consume(TokenType::Colon)?;
9757 match key.as_str() {
9758 "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9759 "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9760 "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9761 "allow_credentials" => {
9762 node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9763 }
9764 "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9765 "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9766 other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9767 }
9768 if self.check(TokenType::Comma) {
9770 self.consume(TokenType::Comma)?;
9771 }
9772 }
9773 self.consume(TokenType::RBrace)?;
9774 Ok(node)
9775 }
9776
9777 fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9785 let tok = self.consume(TokenType::Credential)?;
9786 let name = self.consume(TokenType::Identifier)?.value;
9787 let mut node = CredentialDefinition {
9788 name,
9789 loc: Loc { line: tok.line, column: tok.column },
9790 ..Default::default()
9791 };
9792 self.consume(TokenType::LBrace)?;
9793 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9794 let key = self.consume_any_ident_or_kw()?.value;
9795 self.consume(TokenType::Colon)?;
9796 match key.as_str() {
9797 "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9798 "grants" => {
9799 let bracket_tok = self.current().clone();
9800 let items = self.parse_bracketed_dot_identifiers()?;
9801 for slug in &items {
9802 if !is_valid_capability_slug(slug) {
9803 return Err(ParseError {
9804 message: format!(
9805 "Invalid capability slug '{slug}' in credential '{}' \
9806 `grants:`. Capability slugs must match \
9807 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9808 lowercase identifiers starting with a letter. Examples: \
9809 `chat.invoke`, `flow.execute`.",
9810 node.name
9811 ),
9812 line: bracket_tok.line,
9813 column: bracket_tok.column,
9814 ..Default::default()
9815 });
9816 }
9817 }
9818 node.grants = items;
9819 }
9820 other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9821 }
9822 if self.check(TokenType::Comma) {
9824 self.consume(TokenType::Comma)?;
9825 }
9826 }
9827 self.consume(TokenType::RBrace)?;
9828 Ok(node)
9829 }
9830
9831 fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9838 let tok = self.consume(TokenType::Cache)?;
9839 let name = self.consume(TokenType::Identifier)?.value;
9840 let mut node = CacheDefinition {
9841 name,
9842 loc: Loc { line: tok.line, column: tok.column },
9843 ..Default::default()
9844 };
9845 self.consume(TokenType::LBrace)?;
9846 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9847 let key = self.consume_any_ident_or_kw()?.value;
9848 self.consume(TokenType::Colon)?;
9849 match key.as_str() {
9850 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9851 "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
9852 "key" => node.key_params = self.parse_bracketed_identifiers()?,
9853 "default" => {
9854 node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
9855 }
9856 "apply_to_effects" => {
9857 node.apply_to_effects = self.parse_bracketed_identifiers()?
9858 }
9859 "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
9860 other => return Err(self.error(&format!("unknown cache field `{other}`"))),
9861 }
9862 if self.check(TokenType::Comma) {
9863 self.consume(TokenType::Comma)?;
9864 }
9865 }
9866 self.consume(TokenType::RBrace)?;
9867 Ok(node)
9868 }
9869
9870 fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
9878 let tok = self.consume(TokenType::Document)?;
9879 let name = self.consume(TokenType::Identifier)?.value;
9880 let mut node = crate::ast::DocumentDefinition {
9881 name,
9882 loc: Loc {
9883 line: tok.line,
9884 column: tok.column,
9885 },
9886 ..Default::default()
9887 };
9888 self.consume(TokenType::LBrace)?;
9889 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9890 let field = self.current().clone();
9891 let field_name = field.value.clone();
9892 self.advance();
9893 if self.check(TokenType::Colon) {
9894 self.advance();
9895 match field_name.as_str() {
9896 "target" => node.target = self.consume_any_ident_or_kw()?.value,
9897 "template" => node.template = self.parse_dotted_identifier()?,
9898 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9899 "effects" => node.effects = Some(self.parse_effect_row()?),
9900 other => {
9901 return Err(self.error(&format!(
9902 "unknown document field `{other}` in document `{}` — expected \
9903 `target:` / `template:` / `provenance:` / `effects:`, or a body \
9904 block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
9905 node.name
9906 )))
9907 }
9908 }
9909 } else if self.check(TokenType::LBrace) {
9910 node.blocks
9911 .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
9912 } else {
9913 return Err(self.error(&format!(
9914 "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
9915 body block `{field_name} {{ … }}`",
9916 node.name
9917 )));
9918 }
9919 if self.check(TokenType::Comma) {
9920 self.advance();
9921 }
9922 }
9923 self.consume(TokenType::RBrace)?;
9924 Ok(node)
9925 }
9926
9927 fn parse_doc_block_body(
9933 &mut self,
9934 kind: String,
9935 line: u32,
9936 column: u32,
9937 ) -> Result<crate::ast::DocBlock, ParseError> {
9938 let mut block = crate::ast::DocBlock {
9939 kind,
9940 loc: Loc { line, column },
9941 ..Default::default()
9942 };
9943 self.consume(TokenType::LBrace)?;
9944 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9945 let name_tok = self.current().clone();
9946 let name = self.consume_any_ident_or_kw()?.value;
9947 if self.check(TokenType::Colon) {
9948 self.advance();
9949 let value = self.parse_doc_scalar()?;
9950 block.fields.push((name, value));
9951 } else if self.check(TokenType::LBrace) {
9952 let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
9953 block.children.push(child);
9954 } else {
9955 return Err(self.error(&format!(
9956 "in document block `{}`: `{name}` must be a `field:` value or open a nested \
9957 block `{name} {{ … }}`",
9958 block.kind
9959 )));
9960 }
9961 if self.check(TokenType::Comma) {
9962 self.advance();
9963 }
9964 }
9965 self.consume(TokenType::RBrace)?;
9966 Ok(block)
9967 }
9968
9969 fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
9974 let tok = self.current().clone();
9975 match tok.ttype {
9976 TokenType::StringLit => {
9977 self.advance();
9978 Ok(crate::ast::DocScalar::Text(tok.value))
9979 }
9980 TokenType::Integer => {
9981 self.advance();
9982 Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
9983 }
9984 TokenType::Bool => {
9985 self.advance();
9986 Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
9987 }
9988 TokenType::LBracket => {
9989 let items = self.parse_bracketed_strings()?;
9990 Ok(crate::ast::DocScalar::List(items))
9991 }
9992 _ => {
9993 let name = self.consume_any_ident_or_kw()?.value;
9994 Ok(crate::ast::DocScalar::Ref(name))
9995 }
9996 }
9997 }
9998
9999 fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10024 let tok = self.consume(TokenType::Notify)?;
10025 let name = self.consume(TokenType::Identifier)?.value;
10026 let mut node = crate::ast::NotifyDefinition {
10027 name,
10028 loc: Loc {
10029 line: tok.line,
10030 column: tok.column,
10031 },
10032 ..Default::default()
10033 };
10034 self.consume(TokenType::LBrace)?;
10035 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10036 let field = self.current().clone();
10037 let field_name = field.value.clone();
10038 self.advance();
10039 if self.check(TokenType::Colon) {
10040 self.advance();
10041 match field_name.as_str() {
10042 "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10043 "to" => {
10044 if self.current().value == "secret" && self.peek_is_lparen() {
10048 self.advance(); self.consume(TokenType::LParen)?;
10050 node.to_secret = self.parse_dotted_identifier()?;
10051 self.consume(TokenType::RParen)?;
10052 node.to_is_secret = true;
10053 } else if self.check(TokenType::StringLit) {
10054 node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10055 node.to_is_secret = false;
10056 } else {
10057 node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10058 node.to_is_secret = false;
10059 }
10060 }
10061 "template" => {
10062 node.template = self.consume(TokenType::StringLit)?.value.clone()
10063 }
10064 "window" => {
10065 if self.check(TokenType::Integer) {
10068 let n = self.consume(TokenType::Integer)?.value.clone();
10069 let unit = self.consume_any_ident_or_kw()?.value.clone();
10070 node.window = format!("{n}{unit}");
10071 } else {
10072 node.window = self.consume_any_ident_or_kw()?.value.clone();
10073 }
10074 }
10075 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10076 "effects" => node.effects = Some(self.parse_effect_row()?),
10077 other => {
10078 return Err(self.error(&format!(
10079 "unknown notify field `{other}` in notify `{}` — expected \
10080 `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10081 `effects:`",
10082 node.name
10083 )))
10084 }
10085 }
10086 }
10087 }
10088 self.consume(TokenType::RBrace)?;
10089 Ok(node)
10090 }
10091
10092 fn peek_is_identifier(&self) -> bool {
10096 self.tokens
10097 .get(self.pos + 1)
10098 .map(|t| t.ttype == TokenType::Identifier)
10099 .unwrap_or(false)
10100 }
10101
10102 fn peek_is_lparen(&self) -> bool {
10103 self.tokens
10104 .get(self.pos + 1)
10105 .map(|t| t.ttype == TokenType::LParen)
10106 .unwrap_or(false)
10107 }
10108
10109 fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10111 let tok = self.consume(TokenType::Deliver)?;
10112 let name = self.consume(TokenType::Identifier)?.value;
10113 let mut node = crate::ast::DeliverDefinition {
10114 name,
10115 loc: Loc {
10116 line: tok.line,
10117 column: tok.column,
10118 },
10119 ..Default::default()
10120 };
10121 self.consume(TokenType::LBrace)?;
10122 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10123 let field = self.current().clone();
10124 let field_name = field.value.clone();
10125 self.advance();
10126 if self.check(TokenType::Colon) {
10127 self.advance();
10128 match field_name.as_str() {
10129 "target" => node.target = self.consume_any_ident_or_kw()?.value,
10130 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10131 "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10132 "effects" => node.effects = Some(self.parse_effect_row()?),
10133 other => {
10134 return Err(self.error(&format!(
10135 "unknown deliver field `{other}` in deliver `{}` — expected \
10136 `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10137 block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10138 `add_note {{ … }}`)",
10139 node.name
10140 )))
10141 }
10142 }
10143 } else if self.check(TokenType::LBrace) {
10144 node.ops
10145 .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10146 } else {
10147 return Err(self.error(&format!(
10148 "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10149 operation block `{field_name} {{ … }}`",
10150 node.name
10151 )));
10152 }
10153 if self.check(TokenType::Comma) {
10154 self.advance();
10155 }
10156 }
10157 self.consume(TokenType::RBrace)?;
10158 Ok(node)
10159 }
10160
10161 fn parse_deliver_op(
10165 &mut self,
10166 kind: String,
10167 line: u32,
10168 column: u32,
10169 ) -> Result<crate::ast::DeliverOp, ParseError> {
10170 let mut op = crate::ast::DeliverOp {
10171 kind,
10172 loc: Loc { line, column },
10173 ..Default::default()
10174 };
10175 self.consume(TokenType::LBrace)?;
10176 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10177 let name = self.consume_any_ident_or_kw()?.value;
10178 self.consume(TokenType::Colon).map_err(|_| {
10179 self.error(&format!(
10180 "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10181 operation binds scalar fields, it takes no nested blocks",
10182 op.kind
10183 ))
10184 })?;
10185 let value = self.parse_doc_scalar()?;
10186 op.fields.push((name, value));
10187 if self.check(TokenType::Comma) {
10188 self.advance();
10189 }
10190 }
10191 self.consume(TokenType::RBrace)?;
10192 Ok(op)
10193 }
10194
10195 fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10201 let tok = self.consume(TokenType::Savant)?;
10202 let name = self.consume(TokenType::Identifier)?.value;
10203 let mut node = SavantDefinition {
10204 name,
10205 loc: Loc {
10206 line: tok.line,
10207 column: tok.column,
10208 },
10209 ..Default::default()
10210 };
10211 self.consume(TokenType::LBrace)?;
10212 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10213 let field = self.current().clone();
10214 let field_name = field.value.clone();
10215 self.advance();
10216 if self.check(TokenType::Colon) {
10217 self.advance();
10218 match field_name.as_str() {
10219 "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10220 other => {
10221 return Err(self.error(&format!(
10222 "unknown savant field `{other}` in savant `{}` — expected \
10223 `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10224 node.name
10225 )))
10226 }
10227 }
10228 } else if field_name == "cognition" {
10229 node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10230 } else if field_name == "memory" {
10231 node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10232 } else if field_name == "budget" {
10233 node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10234 } else if field_name == "mandate" {
10235 node.mandates
10236 .push(self.parse_savant_mandate(field.line, field.column)?);
10237 } else {
10238 return Err(self.error(&format!(
10239 "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10240 `cognition` / `memory` / `budget` / `mandate` block",
10241 node.name
10242 )));
10243 }
10244 if self.check(TokenType::Comma) {
10245 self.advance();
10246 }
10247 }
10248 self.consume(TokenType::RBrace)?;
10249 Ok(node)
10250 }
10251
10252 fn parse_savant_cognition(
10255 &mut self,
10256 line: u32,
10257 column: u32,
10258 ) -> Result<SavantCognition, ParseError> {
10259 self.consume(TokenType::LBrace)?;
10260 let mut node = SavantCognition {
10261 loc: Loc { line, column },
10262 ..Default::default()
10263 };
10264 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10265 let key = self.consume_any_ident_or_kw()?.value;
10266 self.consume(TokenType::Colon)?;
10267 match key.as_str() {
10268 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10269 "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10270 "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10271 other => {
10272 return Err(self.error(&format!(
10273 "unknown savant `cognition` field `{other}` — expected \
10274 `depth` / `entropic_threshold` / `divergence`"
10275 )))
10276 }
10277 }
10278 if self.check(TokenType::Comma) {
10279 self.advance();
10280 }
10281 }
10282 self.consume(TokenType::RBrace)?;
10283 Ok(node)
10284 }
10285
10286 fn parse_savant_memory(
10289 &mut self,
10290 line: u32,
10291 column: u32,
10292 ) -> Result<SavantMemory, ParseError> {
10293 self.consume(TokenType::LBrace)?;
10294 let mut node = SavantMemory {
10295 loc: Loc { line, column },
10296 ..Default::default()
10297 };
10298 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10299 let key = self.consume_any_ident_or_kw()?.value;
10300 self.consume(TokenType::Colon)?;
10301 match key.as_str() {
10302 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10303 "corpus_graph" => {
10304 node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10305 }
10306 "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10307 other => {
10308 return Err(self.error(&format!(
10309 "unknown savant `memory` field `{other}` — expected \
10310 `backend` / `corpus_graph` / `isolation_level`"
10311 )))
10312 }
10313 }
10314 if self.check(TokenType::Comma) {
10315 self.advance();
10316 }
10317 }
10318 self.consume(TokenType::RBrace)?;
10319 Ok(node)
10320 }
10321
10322 fn parse_savant_budget(
10325 &mut self,
10326 line: u32,
10327 column: u32,
10328 ) -> Result<SavantBudget, ParseError> {
10329 self.consume(TokenType::LBrace)?;
10330 let mut node = SavantBudget {
10331 loc: Loc { line, column },
10332 ..Default::default()
10333 };
10334 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10335 let key = self.consume_any_ident_or_kw()?.value;
10336 self.consume(TokenType::Colon)?;
10337 match key.as_str() {
10338 "max_iterations" => node.max_iterations = self.parse_optional_int(),
10339 "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10340 other => {
10341 return Err(self.error(&format!(
10342 "unknown savant `budget` field `{other}` — expected \
10343 `max_iterations` / `max_tool_synth`"
10344 )))
10345 }
10346 }
10347 if self.check(TokenType::Comma) {
10348 self.advance();
10349 }
10350 }
10351 self.consume(TokenType::RBrace)?;
10352 Ok(node)
10353 }
10354
10355 fn parse_savant_mandate(
10358 &mut self,
10359 line: u32,
10360 column: u32,
10361 ) -> Result<SavantMandate, ParseError> {
10362 let name = self.consume(TokenType::Identifier)?.value;
10363 let mut node = SavantMandate {
10364 name,
10365 loc: Loc { line, column },
10366 ..Default::default()
10367 };
10368 self.consume(TokenType::LBrace)?;
10369 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10370 let key = self.consume_any_ident_or_kw()?.value;
10371 self.consume(TokenType::Colon)?;
10372 match key.as_str() {
10373 "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10374 "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10375 other => {
10376 return Err(self.error(&format!(
10377 "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10378 )))
10379 }
10380 }
10381 if self.check(TokenType::Comma) {
10382 self.advance();
10383 }
10384 }
10385 self.consume(TokenType::RBrace)?;
10386 Ok(node)
10387 }
10388
10389 fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10394 let tok = self.consume(TokenType::Synth)?;
10395 let name = self.consume(TokenType::Identifier)?.value;
10396 let mut node = SynthDefinition {
10397 name,
10398 loc: Loc {
10399 line: tok.line,
10400 column: tok.column,
10401 },
10402 ..Default::default()
10403 };
10404 self.consume(TokenType::LBrace)?;
10405 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10406 let key = self.consume_any_ident_or_kw()?.value;
10407 self.consume(TokenType::Colon)?;
10408 match key.as_str() {
10409 "target" => node.target = self.consume(TokenType::StringLit)?.value,
10410 "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10411 "language" => node.language = self.consume_any_ident_or_kw()?.value,
10412 "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10413 "review" => node.review = self.consume_any_ident_or_kw()?.value,
10414 "max_lines" => node.max_lines = self.parse_optional_int(),
10415 other => {
10416 return Err(self.error(&format!(
10417 "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10418 / `language` / `sandbox` / `review` / `max_lines`",
10419 node.name
10420 )))
10421 }
10422 }
10423 if self.check(TokenType::Comma) {
10424 self.consume(TokenType::Comma)?;
10425 }
10426 }
10427 self.consume(TokenType::RBrace)?;
10428 Ok(node)
10429 }
10430
10431 fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10436 let tok = self.consume(TokenType::Voice)?;
10437 let name = self.consume(TokenType::Identifier)?.value;
10438 let mut node = VoiceDefinition {
10439 name,
10440 loc: Loc { line: tok.line, column: tok.column },
10441 ..Default::default()
10442 };
10443 self.consume(TokenType::LBrace)?;
10444 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10445 let key = self.consume_any_ident_or_kw()?.value;
10446 self.consume(TokenType::Colon)?;
10447 match key.as_str() {
10448 "stt" => node.stt = Some(self.parse_upstream_ref()?),
10450 "tts" => node.tts = Some(self.parse_upstream_ref()?),
10451 "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10452 "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10453 "interruptible" => {
10454 let v = self.consume_any_ident_or_kw()?.value;
10455 node.interruptible = v == "true";
10456 }
10457 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10458 "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10459 "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10460 other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10461 }
10462 if self.check(TokenType::Comma) {
10463 self.consume(TokenType::Comma)?;
10464 }
10465 }
10466 self.consume(TokenType::RBrace)?;
10467 Ok(node)
10468 }
10469
10470 fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10473 let base = self.consume(TokenType::Identifier)?.value;
10474 if self.check(TokenType::At) {
10475 self.advance();
10476 let version = self.consume_any_ident_or_kw()?.value;
10477 Ok(format!("{base}@{version}"))
10478 } else {
10479 Ok(base)
10480 }
10481 }
10482
10483 fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10489 self.consume(TokenType::LBracket)?;
10490 let mut rules = Vec::new();
10491 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10492 let dir_tok = self.current().clone();
10493 let direction = match dir_tok.ttype {
10494 TokenType::Send => "send",
10495 TokenType::Receive => "receive",
10496 _ => {
10497 return Err(self.error(&format!(
10498 "upstream map rule must start with `send` or `receive`, got `{}`",
10499 dir_tok.value
10500 )))
10501 }
10502 };
10503 self.advance();
10504 let message = self.consume(TokenType::Identifier)?.value;
10505 self.consume(TokenType::As)?;
10506 let framing = self.consume_any_ident_or_kw()?.value;
10507 let mut rule = UpstreamMapRule {
10508 direction: direction.to_string(),
10509 message,
10510 framing,
10511 loc: Loc { line: dir_tok.line, column: dir_tok.column },
10512 ..Default::default()
10513 };
10514 if self.current().value == "tag" {
10516 self.advance();
10517 rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10518 } else if self.current().value == "when" {
10519 self.advance();
10524 rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10525 if self.check(TokenType::Assign) {
10526 self.advance();
10527 rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10528 }
10529 }
10530 rules.push(rule);
10531 if self.check(TokenType::Comma) {
10532 self.advance();
10533 }
10534 }
10535 self.consume(TokenType::RBracket)?;
10536 Ok(rules)
10537 }
10538
10539 fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10543 self.consume(TokenType::LBrace)?;
10544 let mut backoff_ms: Option<i64> = None;
10545 let mut max_attempts: Option<i64> = None;
10546 let mut on_exhausted: Option<String> = None;
10547 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10548 let key = self.consume_any_ident_or_kw()?.value;
10549 self.consume(TokenType::Colon)?;
10550 match key.as_str() {
10551 "backoff_ms" => {
10552 backoff_ms = Some(
10553 self.consume(TokenType::Integer)?
10554 .value
10555 .parse::<i64>()
10556 .map_err(|_| self.error("backoff_ms must be an integer"))?,
10557 )
10558 }
10559 "max_attempts" => {
10560 max_attempts = Some(
10561 self.consume(TokenType::Integer)?
10562 .value
10563 .parse::<i64>()
10564 .map_err(|_| self.error("max_attempts must be an integer"))?,
10565 )
10566 }
10567 "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10568 other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10569 }
10570 if self.check(TokenType::Comma) {
10571 self.consume(TokenType::Comma)?;
10572 }
10573 }
10574 self.consume(TokenType::RBrace)?;
10575 match (backoff_ms, max_attempts, on_exhausted) {
10576 (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10577 _ => Err(self.error(
10578 "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10579 )),
10580 }
10581 }
10582
10583 fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10585 self.consume(TokenType::LBracket)?;
10586 let mut steps = Vec::new();
10587 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10588 steps.push(self.parse_session_step()?);
10589 if self.check(TokenType::Comma) {
10590 self.advance();
10591 }
10592 }
10593 self.consume(TokenType::RBracket)?;
10594 Ok(steps)
10595 }
10596
10597 fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10601 self.consume(TokenType::LBrace)?;
10602 let mut steps = Vec::new();
10603 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10604 steps.push(self.parse_session_step()?);
10605 if self.check(TokenType::Comma) {
10606 self.advance();
10607 }
10608 }
10609 self.consume(TokenType::RBrace)?;
10610 Ok(steps)
10611 }
10612
10613 fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10614 let tok = self.current().clone();
10615 let loc = Loc { line: tok.line, column: tok.column };
10616 match tok.ttype {
10617 TokenType::Send => {
10618 self.advance();
10619 let msg = self.consume_any_ident_or_kw()?;
10620 Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10621 }
10622 TokenType::Receive => {
10623 self.advance();
10624 let msg = self.consume_any_ident_or_kw()?;
10625 Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10626 }
10627 TokenType::Loop => {
10628 self.advance();
10629 Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10630 }
10631 TokenType::End => {
10632 self.advance();
10633 Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10634 }
10635 TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10638 self.parse_session_choice(&tok.value, loc)
10639 }
10640 TokenType::Identifier if tok.value == "interrupt" => {
10643 self.parse_session_interrupt(loc)
10644 }
10645 _ if tok.value == "resume" => {
10663 self.advance();
10664 Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10665 }
10666 _ => Err(ParseError {
10667 message: format!(
10668 "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10669 tok.value
10670 ),
10671 line: tok.line,
10672 column: tok.column,
10673 ..Default::default()
10674 }),
10675 }
10676 }
10677
10678 fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10683 let t = self.current().clone();
10684 if t.value != kw {
10685 return Err(ParseError {
10686 message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10687 line: t.line,
10688 column: t.column,
10689 ..Default::default()
10690 });
10691 }
10692 self.advance();
10693 Ok(())
10694 }
10695
10696 fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10704 self.advance(); let body = self.parse_session_step_block()?;
10708 self.consume_contextual("on")?;
10710 let signal = self.consume_any_ident_or_kw()?;
10711 self.consume_contextual("as")?;
10713 let binder = self.consume_any_ident_or_kw()?;
10714 self.consume_contextual("resumable")?;
10716 let handler = self.parse_session_step_block()?;
10717 Ok(SessionStep {
10718 op: "interrupt".into(),
10719 message_type: signal.value,
10720 branches: vec![
10721 SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10722 SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10723 ],
10724 binder: binder.value,
10725 resumable: true,
10726 loc,
10727 })
10728 }
10729
10730 fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10733 self.advance(); self.consume(TokenType::LBrace)?;
10735 let mut branches = Vec::new();
10736 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10737 let label_tok = self.consume_any_ident_or_kw()?;
10738 self.consume(TokenType::Colon)?;
10739 let steps = self.parse_session_steps()?;
10740 branches.push(SessionBranch {
10741 label: label_tok.value,
10742 steps,
10743 loc: Loc { line: label_tok.line, column: label_tok.column },
10744 });
10745 if self.check(TokenType::Comma) {
10746 self.advance();
10747 }
10748 }
10749 self.consume(TokenType::RBrace)?;
10750 Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10751 }
10752
10753 fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10755 let tok = self.consume(TokenType::Topology)?;
10756 let name = self.consume(TokenType::Identifier)?.value;
10757 let mut node = TopologyDefinition {
10758 name,
10759 nodes: Vec::new(),
10760 edges: Vec::new(),
10761 loc: Loc {
10762 line: tok.line,
10763 column: tok.column,
10764 },
10765 leading_trivia: Vec::new(),
10766 trailing_trivia: Vec::new(),
10767 };
10768 self.consume(TokenType::LBrace)?;
10769 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10770 let field_name = self.current().value.clone();
10771 self.advance();
10772 if !self.check(TokenType::Colon) {
10773 if self.check(TokenType::LBrace) {
10774 self.skip_braced_block()?;
10775 }
10776 continue;
10777 }
10778 self.advance();
10779 match field_name.as_str() {
10780 "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10781 "edges" => node.edges = self.parse_topology_edges()?,
10782 _ => self.skip_value(),
10783 }
10784 }
10785 self.consume(TokenType::RBrace)?;
10786 Ok(node)
10787 }
10788
10789 fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10790 self.consume(TokenType::LBracket)?;
10791 let mut edges = Vec::new();
10792 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10793 edges.push(self.parse_topology_edge()?);
10794 if self.check(TokenType::Comma) {
10795 self.advance();
10796 }
10797 }
10798 self.consume(TokenType::RBracket)?;
10799 Ok(edges)
10800 }
10801
10802 fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10803 let src_tok = self.consume_any_ident_or_kw()?;
10804 self.consume(TokenType::Arrow)?;
10805 let tgt_tok = self.consume_any_ident_or_kw()?;
10806 self.consume(TokenType::Colon)?;
10807 let sess_tok = self.consume_any_ident_or_kw()?;
10808 Ok(TopologyEdge {
10809 source: src_tok.value,
10810 target: tgt_tok.value,
10811 session_ref: sess_tok.value,
10812 loc: Loc {
10813 line: src_tok.line,
10814 column: src_tok.column,
10815 },
10816 })
10817 }
10818
10819 fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10823 let tok = self.consume(TokenType::Immune)?;
10824 let name = self.consume(TokenType::Identifier)?.value;
10825 let mut node = ImmuneDefinition {
10826 name,
10827 watch: Vec::new(),
10828 sensitivity: None,
10829 baseline: "learned".to_string(),
10830 window: 100,
10831 scope: String::new(),
10832 tau: String::new(),
10833 decay: "exponential".to_string(),
10834 loc: Loc {
10835 line: tok.line,
10836 column: tok.column,
10837 },
10838 leading_trivia: Vec::new(),
10839 trailing_trivia: Vec::new(),
10840 };
10841 self.consume(TokenType::LBrace)?;
10842 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10843 let field_name = self.current().value.clone();
10844 self.advance();
10845 if !self.check(TokenType::Colon) {
10846 if self.check(TokenType::LBrace) {
10847 self.skip_braced_block()?;
10848 }
10849 continue;
10850 }
10851 self.advance();
10852 match field_name.as_str() {
10853 "watch" => node.watch = self.parse_bracketed_identifiers()?,
10854 "sensitivity" => node.sensitivity = self.parse_optional_float(),
10855 "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
10856 "window" => {
10857 if let Some(v) = self.parse_optional_int() {
10858 node.window = v;
10859 }
10860 }
10861 "scope" => {
10862 let s_tok = self.consume_any_ident_or_kw()?;
10863 let s = s_tok.value;
10864 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10865 return Err(ParseError {
10866 message: format!(
10867 "Invalid scope '{s}' in immune '{}' — \
10868 expected tenant | flow | global",
10869 node.name
10870 ),
10871 line: s_tok.line,
10872 column: s_tok.column,
10873 ..Default::default()
10874 });
10875 }
10876 node.scope = s;
10877 }
10878 "tau" => {
10879 let t = self.current().clone();
10880 match t.ttype {
10881 TokenType::Duration | TokenType::StringLit => {
10882 self.advance();
10883 node.tau = t.value;
10884 }
10885 _ => node.tau = self.consume_any_ident_or_kw()?.value,
10886 }
10887 }
10888 "decay" => {
10889 let d_tok = self.consume_any_ident_or_kw()?;
10890 let d = d_tok.value;
10891 if !matches!(d.as_str(), "exponential" | "linear" | "none") {
10892 return Err(ParseError {
10893 message: format!(
10894 "Invalid decay '{d}' in immune '{}' — \
10895 expected exponential | linear | none",
10896 node.name
10897 ),
10898 line: d_tok.line,
10899 column: d_tok.column,
10900 ..Default::default()
10901 });
10902 }
10903 node.decay = d;
10904 }
10905 _ => self.skip_value(),
10906 }
10907 }
10908 self.consume(TokenType::RBrace)?;
10909 Ok(node)
10910 }
10911
10912 fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
10914 let tok = self.consume(TokenType::Reflex)?;
10915 let name = self.consume(TokenType::Identifier)?.value;
10916 let mut node = ReflexDefinition {
10917 name,
10918 trigger: String::new(),
10919 on_level: "doubt".to_string(),
10920 action: String::new(),
10921 scope: String::new(),
10922 sla: String::new(),
10923 loc: Loc {
10924 line: tok.line,
10925 column: tok.column,
10926 },
10927 leading_trivia: Vec::new(),
10928 trailing_trivia: Vec::new(),
10929 };
10930 self.consume(TokenType::LBrace)?;
10931 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10932 let field_name = self.current().value.clone();
10933 self.advance();
10934 if !self.check(TokenType::Colon) {
10935 if self.check(TokenType::LBrace) {
10936 self.skip_braced_block()?;
10937 }
10938 continue;
10939 }
10940 self.advance();
10941 match field_name.as_str() {
10942 "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
10943 "on_level" => {
10944 let l_tok = self.consume_any_ident_or_kw()?;
10945 let l = l_tok.value;
10946 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10947 return Err(ParseError {
10948 message: format!(
10949 "Invalid on_level '{l}' in reflex '{}' — \
10950 expected know | believe | speculate | doubt",
10951 node.name
10952 ),
10953 line: l_tok.line,
10954 column: l_tok.column,
10955 ..Default::default()
10956 });
10957 }
10958 node.on_level = l;
10959 }
10960 "action" => {
10961 let a_tok = self.consume_any_ident_or_kw()?;
10962 let a = a_tok.value;
10963 if !matches!(
10964 a.as_str(),
10965 "drop"
10966 | "revoke"
10967 | "emit"
10968 | "redact"
10969 | "quarantine"
10970 | "terminate"
10971 | "alert"
10972 ) {
10973 return Err(ParseError {
10974 message: format!(
10975 "Invalid action '{a}' in reflex '{}' — \
10976 expected drop | revoke | emit | redact | \
10977 quarantine | terminate | alert",
10978 node.name
10979 ),
10980 line: a_tok.line,
10981 column: a_tok.column,
10982 ..Default::default()
10983 });
10984 }
10985 node.action = a;
10986 }
10987 "scope" => {
10988 let s_tok = self.consume_any_ident_or_kw()?;
10989 let s = s_tok.value;
10990 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10991 return Err(ParseError {
10992 message: format!(
10993 "Invalid scope '{s}' in reflex '{}' — \
10994 expected tenant | flow | global",
10995 node.name
10996 ),
10997 line: s_tok.line,
10998 column: s_tok.column,
10999 ..Default::default()
11000 });
11001 }
11002 node.scope = s;
11003 }
11004 "sla" => {
11005 let t = self.current().clone();
11006 match t.ttype {
11007 TokenType::Duration | TokenType::StringLit => {
11008 self.advance();
11009 node.sla = t.value;
11010 }
11011 _ => node.sla = self.consume_any_ident_or_kw()?.value,
11012 }
11013 }
11014 _ => self.skip_value(),
11015 }
11016 }
11017 self.consume(TokenType::RBrace)?;
11018 Ok(node)
11019 }
11020
11021 fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11023 let tok = self.consume(TokenType::Heal)?;
11024 let name = self.consume(TokenType::Identifier)?.value;
11025 let mut node = HealDefinition {
11026 name,
11027 source: String::new(),
11028 on_level: "doubt".to_string(),
11029 mode: "human_in_loop".to_string(),
11030 scope: String::new(),
11031 review_sla: String::new(),
11032 shield_ref: String::new(),
11033 max_patches: 3,
11034 loc: Loc {
11035 line: tok.line,
11036 column: tok.column,
11037 },
11038 leading_trivia: Vec::new(),
11039 trailing_trivia: Vec::new(),
11040 };
11041 self.consume(TokenType::LBrace)?;
11042 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11043 let field_name = self.current().value.clone();
11044 self.advance();
11045 if !self.check(TokenType::Colon) {
11046 if self.check(TokenType::LBrace) {
11047 self.skip_braced_block()?;
11048 }
11049 continue;
11050 }
11051 self.advance();
11052 match field_name.as_str() {
11053 "source" => node.source = self.consume_any_ident_or_kw()?.value,
11054 "on_level" => {
11055 let l_tok = self.consume_any_ident_or_kw()?;
11056 let l = l_tok.value;
11057 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11058 return Err(ParseError {
11059 message: format!(
11060 "Invalid on_level '{l}' in heal '{}' — \
11061 expected know | believe | speculate | doubt",
11062 node.name
11063 ),
11064 line: l_tok.line,
11065 column: l_tok.column,
11066 ..Default::default()
11067 });
11068 }
11069 node.on_level = l;
11070 }
11071 "mode" => {
11072 let m_tok = self.consume_any_ident_or_kw()?;
11073 let m = m_tok.value;
11074 if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11075 return Err(ParseError {
11076 message: format!(
11077 "Invalid mode '{m}' in heal '{}' — \
11078 expected audit_only | human_in_loop | adversarial",
11079 node.name
11080 ),
11081 line: m_tok.line,
11082 column: m_tok.column,
11083 ..Default::default()
11084 });
11085 }
11086 node.mode = m;
11087 }
11088 "scope" => {
11089 let s_tok = self.consume_any_ident_or_kw()?;
11090 let s = s_tok.value;
11091 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11092 return Err(ParseError {
11093 message: format!(
11094 "Invalid scope '{s}' in heal '{}' — \
11095 expected tenant | flow | global",
11096 node.name
11097 ),
11098 line: s_tok.line,
11099 column: s_tok.column,
11100 ..Default::default()
11101 });
11102 }
11103 node.scope = s;
11104 }
11105 "review_sla" => {
11106 let t = self.current().clone();
11107 match t.ttype {
11108 TokenType::Duration | TokenType::StringLit => {
11109 self.advance();
11110 node.review_sla = t.value;
11111 }
11112 _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11113 }
11114 }
11115 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11116 "max_patches" => {
11117 if let Some(v) = self.parse_optional_int() {
11118 node.max_patches = v;
11119 }
11120 }
11121 _ => self.skip_value(),
11122 }
11123 }
11124 self.consume(TokenType::RBrace)?;
11125 Ok(node)
11126 }
11127
11128 fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11132 let tok = self.consume(TokenType::Component)?;
11133 let name = self.consume(TokenType::Identifier)?.value;
11134 let mut node = ComponentDefinition {
11135 name,
11136 renders: String::new(),
11137 via_shield: String::new(),
11138 on_interact: String::new(),
11139 render_hint: "custom".to_string(),
11140 loc: Loc {
11141 line: tok.line,
11142 column: tok.column,
11143 },
11144 leading_trivia: Vec::new(),
11145 trailing_trivia: Vec::new(),
11146 };
11147 self.consume(TokenType::LBrace)?;
11148 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11149 let field_name = self.current().value.clone();
11150 self.advance();
11151 if !self.check(TokenType::Colon) {
11152 if self.check(TokenType::LBrace) {
11153 self.skip_braced_block()?;
11154 }
11155 continue;
11156 }
11157 self.advance();
11158 match field_name.as_str() {
11159 "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11160 "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11161 "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11162 "render_hint" => {
11163 let h_tok = self.consume_any_ident_or_kw()?;
11164 let h = h_tok.value;
11165 if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11166 return Err(ParseError {
11167 message: format!(
11168 "Invalid render_hint '{h}' in component '{}' — \
11169 expected card | list | form | chart | custom",
11170 node.name
11171 ),
11172 line: h_tok.line,
11173 column: h_tok.column,
11174 ..Default::default()
11175 });
11176 }
11177 node.render_hint = h;
11178 }
11179 _ => self.skip_value(),
11180 }
11181 }
11182 self.consume(TokenType::RBrace)?;
11183 Ok(node)
11184 }
11185
11186 fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11188 let tok = self.consume(TokenType::View)?;
11189 let name = self.consume(TokenType::Identifier)?.value;
11190 let mut node = ViewDefinition {
11191 name,
11192 title: String::new(),
11193 components: Vec::new(),
11194 route: String::new(),
11195 loc: Loc {
11196 line: tok.line,
11197 column: tok.column,
11198 },
11199 leading_trivia: Vec::new(),
11200 trailing_trivia: Vec::new(),
11201 };
11202 self.consume(TokenType::LBrace)?;
11203 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11204 let field_name = self.current().value.clone();
11205 self.advance();
11206 if !self.check(TokenType::Colon) {
11207 if self.check(TokenType::LBrace) {
11208 self.skip_braced_block()?;
11209 }
11210 continue;
11211 }
11212 self.advance();
11213 match field_name.as_str() {
11214 "title" => node.title = self.consume(TokenType::StringLit)?.value,
11215 "components" => node.components = self.parse_bracketed_identifiers()?,
11216 "route" => node.route = self.consume(TokenType::StringLit)?.value,
11217 _ => self.skip_value(),
11218 }
11219 }
11220 self.consume(TokenType::RBrace)?;
11221 Ok(node)
11222 }
11223
11224 fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11225 let tok = self.consume(TokenType::AxonEndpoint)?;
11226 let name = self.consume(TokenType::Identifier)?.value;
11227 let mut node = AxonEndpointDefinition {
11228 name,
11229 method: String::new(),
11230 path: String::new(),
11231 body_type: String::new(),
11232 execute_flow: String::new(),
11233 output_type: String::new(),
11234 shield_ref: String::new(),
11235 cors_ref: String::new(),
11238 retries: None,
11239 timeout: String::new(),
11240 compliance: Vec::new(),
11241 transport: "json".to_string(),
11243 keepalive: String::new(),
11244 transport_explicit: false,
11250 implicit_transport: String::new(),
11251 requires_capabilities: Vec::new(),
11253 public: false,
11256 replay_explicit: false,
11260 replay: false,
11261 transport_dialect: String::new(),
11266 has_algebraic_stream_effect: false,
11272 backend: String::new(),
11277 path_params: Vec::new(),
11282 query_params: Vec::new(),
11287 loc: Loc {
11288 line: tok.line,
11289 column: tok.column,
11290 },
11291 leading_trivia: Vec::new(),
11292 trailing_trivia: Vec::new(),
11293 };
11294 self.consume(TokenType::LBrace)?;
11295 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11296 let field_name = self.current().value.clone();
11297 self.advance();
11298 if self.check(TokenType::Colon) {
11299 self.advance();
11300 match field_name.as_str() {
11301 "method" => {
11302 let value_tok = self.consume_any_ident_or_kw()?;
11309 let value_upper = value_tok.value.to_uppercase();
11310 if !axonendpoint_is_valid_method(&value_upper) {
11311 let hint = crate::smart_suggest::suggest_for(
11312 &value_upper,
11313 AXONENDPOINT_METHOD_VALUES,
11314 );
11315 let base = format!(
11316 "Invalid method '{}' in axonendpoint '{}'.",
11317 value_tok.value, node.name
11318 );
11319 let message = if hint.is_empty() {
11320 format!(
11321 "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11322 value_tok.value
11323 )
11324 } else {
11325 format!(
11326 "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11327 value_tok.value
11328 )
11329 };
11330 return Err(ParseError {
11331 message,
11332 line: value_tok.line,
11333 column: value_tok.column,
11334 ..Default::default()
11335 });
11336 }
11337 node.method = value_upper;
11338 }
11339 "path" => {
11340 node.path = self.consume(TokenType::StringLit)?.value.clone();
11341 match extract_path_param_names(&node.path) {
11349 Ok(names) => node.path_params = names,
11350 Err(dup) => {
11351 let cur = self.current().clone();
11352 return Err(ParseError {
11353 message: format!(
11354 "axonendpoint '{}' declares path '{}' \
11355 containing duplicate placeholder '{{{}}}'. \
11356 Each `{{name}}` in a `path:` must be \
11357 unique — the runtime cannot bind two \
11358 path segments to the same name.",
11359 node.name, node.path, dup,
11360 ),
11361 line: cur.line,
11362 column: cur.column,
11363 ..Default::default()
11364 });
11365 }
11366 }
11367 },
11368 "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11369 "query" => {
11370 let lbrace_tok = self.consume(TokenType::LBrace)?;
11389 let block_line = lbrace_tok.line;
11390 if !node.query_params.is_empty() {
11391 return Err(ParseError {
11392 message: format!(
11393 "axonendpoint '{}' declares `query: {{ … }}` \
11394 more than once. The query-parameter block \
11395 is unique per endpoint; combine all params \
11396 into a single block.",
11397 node.name,
11398 ),
11399 line: lbrace_tok.line,
11400 column: lbrace_tok.column,
11401 ..Default::default()
11402 });
11403 }
11404 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11405 let name_tok = self.consume(TokenType::Identifier)?;
11406 let field_name = name_tok.value.clone();
11407 if node
11409 .query_params
11410 .iter()
11411 .any(|f| f.name == field_name)
11412 {
11413 return Err(ParseError {
11414 message: format!(
11415 "axonendpoint '{}' declares duplicate \
11416 query param '{}' inside `query: {{ … }}`. \
11417 Each name must appear at most once \
11418 .",
11419 node.name, field_name,
11420 ),
11421 line: name_tok.line,
11422 column: name_tok.column,
11423 ..Default::default()
11424 });
11425 }
11426 self.consume(TokenType::Colon)?;
11427 let type_expr = self.parse_type_expr()?;
11428 if !type_expr.generic_param.is_empty() {
11440 let canonical_hint = if type_expr.name == "Optional" {
11441 format!(
11442 " Use `{}?` (the `?` suffix) for an \
11443 optional query param instead of \
11444 `Optional<{}>`.",
11445 type_expr.generic_param,
11446 type_expr.generic_param,
11447 )
11448 } else if type_expr.name == "List" {
11449 " Multi-value query params (e.g. `?tag=a&tag=b`) \
11450 are honest-deferred from v1.38.5; bind a \
11451 single-value `Text` query param and parse \
11452 the value inside the flow."
11453 .to_string()
11454 } else {
11455 String::new()
11456 };
11457 return Err(ParseError {
11458 message: format!(
11459 "axonendpoint '{}' query param '{}' uses \
11460 a generic type `{}<{}>`. Query params \
11461 take a primitive type from the closed \
11462 catalog ({}); the `?` suffix marks \
11463 optional.{} .",
11464 node.name,
11465 field_name,
11466 type_expr.name,
11467 type_expr.generic_param,
11468 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11469 canonical_hint,
11470 ),
11471 line: type_expr.loc.line,
11472 column: type_expr.loc.column,
11473 ..Default::default()
11474 });
11475 }
11476 if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11480 let hint = crate::smart_suggest::suggest_for(
11487 &type_expr.name,
11488 AXONENDPOINT_QUERY_PARAM_TYPES,
11489 );
11490 let hint_text = if hint.is_empty() {
11491 format!(
11492 " Expected one of: {}.",
11493 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11494 )
11495 } else {
11496 format!(
11497 " {} Expected one of: {}.",
11498 hint,
11499 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11500 )
11501 };
11502 return Err(ParseError {
11503 message: format!(
11504 "axonendpoint '{}' query param '{}' has \
11505 unsupported type '{}'.{} .",
11506 node.name, field_name, type_expr.name,
11507 hint_text,
11508 ),
11509 line: type_expr.loc.line,
11510 column: type_expr.loc.column,
11511 ..Default::default()
11512 });
11513 }
11514 node.query_params.push(TypeField {
11515 name: field_name,
11516 type_expr,
11517 loc: Loc {
11518 line: name_tok.line,
11519 column: name_tok.column,
11520 },
11521 });
11522 if self.check(TokenType::Comma) {
11528 self.advance();
11529 }
11530 let _ = block_line; }
11532 self.consume(TokenType::RBrace)?;
11533 },
11534 "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11535 "output" => {
11536 node.output_type = self.parse_output_type_string()?;
11554 }
11555 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11556 "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11558 "retries" => node.retries = self.parse_optional_int(),
11559 "timeout" => {
11560 let t = self.current().clone();
11561 self.advance();
11562 node.timeout = t.value.clone();
11563 }
11564 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11565 "replay" => {
11566 let value_tok = self.consume(TokenType::Bool)?;
11573 node.replay = value_tok.value.eq_ignore_ascii_case("true");
11574 node.replay_explicit = true;
11575 }
11576 "public" => {
11582 let value_tok = self.consume(TokenType::Bool)?;
11583 node.public = value_tok.value.eq_ignore_ascii_case("true");
11584 }
11585 "requires" => {
11586 let bracket_tok = self.current().clone();
11593 let items = self.parse_bracketed_dot_identifiers()?;
11594 for slug in &items {
11595 if !is_valid_capability_slug(slug) {
11596 return Err(ParseError {
11597 message: format!(
11598 "Invalid capability slug '{slug}' in axonendpoint '{}' \
11599 `requires:`. Capability slugs must match \
11600 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11601 lowercase identifiers starting with a letter. Examples: \
11602 `admin`, `legal.read`, `hipaa.phi.read`.",
11603 node.name
11604 ),
11605 line: bracket_tok.line,
11606 column: bracket_tok.column,
11607 ..Default::default()
11608 });
11609 }
11610 }
11611 node.requires_capabilities = items;
11612 }
11613 "transport" => {
11617 let value_tok = self.consume_any_ident_or_kw()?;
11618 let value = &value_tok.value;
11619 if !axonendpoint_is_valid_transport(value) {
11620 let hint = crate::smart_suggest::suggest_for(
11621 value,
11622 AXONENDPOINT_TRANSPORT_VALUES,
11623 );
11624 let base = format!(
11625 "Invalid transport '{}' in axonendpoint '{}'.",
11626 value, node.name
11627 );
11628 let message = if hint.is_empty() {
11629 format!("{base} expected json | sse | ndjson, found {value}")
11630 } else {
11631 format!(
11632 "{base} {hint} (expected json | sse | ndjson, found {value})"
11633 )
11634 };
11635 return Err(ParseError {
11636 message,
11637 line: value_tok.line,
11638 column: value_tok.column,
11639 ..Default::default()
11640 });
11641 }
11642 node.transport = value.clone();
11643 node.transport_explicit = true;
11648 if self.check(TokenType::LParen) {
11655 if value != "sse" {
11656 let tok = self.current().clone();
11657 return Err(ParseError {
11658 message: format!(
11659 "Dialect parametrization \
11660 `transport: {value}(<dialect>)` is \
11661 only valid for `sse`; got \
11662 `{value}` in axonendpoint '{}'.",
11663 node.name
11664 ),
11665 line: tok.line,
11666 column: tok.column,
11667 ..Default::default()
11668 });
11669 }
11670 self.advance(); let dialect_tok = self.consume_any_ident_or_kw()?;
11672 let dialect = dialect_tok.value.clone();
11673 if !AXONENDPOINT_TRANSPORT_DIALECTS
11674 .iter()
11675 .any(|&d| d == dialect)
11676 {
11677 let hint = crate::smart_suggest::suggest_for(
11678 &dialect,
11679 AXONENDPOINT_TRANSPORT_DIALECTS,
11680 );
11681 let base = format!(
11682 "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11683 node.name
11684 );
11685 let message = if hint.is_empty() {
11686 format!(
11687 "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11688 )
11689 } else {
11690 format!(
11691 "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11692 )
11693 };
11694 return Err(ParseError {
11695 message,
11696 line: dialect_tok.line,
11697 column: dialect_tok.column,
11698 ..Default::default()
11699 });
11700 }
11701 let rparen_tok = self.current().clone();
11703 if !self.check(TokenType::RParen) {
11704 return Err(ParseError {
11705 message: format!(
11706 "Expected `)` after dialect name \
11707 in axonendpoint '{}' \
11708 (transport: sse(<dialect>) grammar).",
11709 node.name
11710 ),
11711 line: rparen_tok.line,
11712 column: rparen_tok.column,
11713 ..Default::default()
11714 });
11715 }
11716 self.advance(); node.transport_dialect = dialect;
11718 }
11719 }
11720 "keepalive" => {
11721 let value_tok = self.current().clone();
11725 self.advance();
11726 let value = &value_tok.value;
11727 if !axonendpoint_is_valid_keepalive(value) {
11728 let hint = crate::smart_suggest::suggest_for(
11729 value,
11730 AXONENDPOINT_KEEPALIVE_VALUES,
11731 );
11732 let base = format!(
11733 "Invalid keepalive '{}' in axonendpoint '{}'.",
11734 value, node.name
11735 );
11736 let message = if hint.is_empty() {
11737 format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11738 } else {
11739 format!(
11740 "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11741 )
11742 };
11743 return Err(ParseError {
11744 message,
11745 line: value_tok.line,
11746 column: value_tok.column,
11747 ..Default::default()
11748 });
11749 }
11750 node.keepalive = value.clone();
11751 }
11752 "backend" => {
11753 let value_tok = self.consume_any_ident_or_kw()?;
11761 let value = &value_tok.value;
11762 if !axonendpoint_is_valid_backend(value) {
11763 let hint = crate::smart_suggest::suggest_for(
11764 value,
11765 AXONENDPOINT_BACKEND_VALUES,
11766 );
11767 let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11768 let base = format!(
11769 "Invalid backend '{}' in axonendpoint '{}'.",
11770 value, node.name
11771 );
11772 let message = if hint.is_empty() {
11773 format!("{base} expected {expected}, found {value}")
11774 } else {
11775 format!(
11776 "{base} {hint} (expected {expected}, found {value})"
11777 )
11778 };
11779 return Err(ParseError {
11780 message,
11781 line: value_tok.line,
11782 column: value_tok.column,
11783 ..Default::default()
11784 });
11785 }
11786 node.backend = value.clone();
11787 }
11788 _ => self.skip_value(),
11789 }
11790 } else if self.check(TokenType::LBrace) {
11791 self.skip_braced_block()?;
11792 }
11793 }
11794 self.consume(TokenType::RBrace)?;
11795 Ok(node)
11796 }
11797
11798 fn parse_optional_int(&mut self) -> Option<i64> {
11801 let tok = self.current().clone();
11802 match tok.ttype {
11803 TokenType::Integer => {
11804 self.advance();
11805 tok.value.parse::<i64>().ok()
11806 }
11807 _ => {
11808 self.advance();
11809 None
11810 }
11811 }
11812 }
11813
11814 fn parse_optional_float(&mut self) -> Option<f64> {
11815 let tok = self.current().clone();
11816 match tok.ttype {
11817 TokenType::Float | TokenType::Integer => {
11818 self.advance();
11819 tok.value.parse::<f64>().ok()
11820 }
11821 _ => {
11822 self.advance();
11823 None
11824 }
11825 }
11826 }
11827
11828 fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11831 let tok = self.consume(TokenType::Lambda)?;
11832 let name = self.consume(TokenType::Identifier)?;
11833 self.consume(TokenType::LBrace)?;
11834
11835 let mut node = LambdaDataDefinition {
11836 name: name.value.clone(),
11837 ontology: String::new(),
11838 certainty: 1.0,
11839 temporal_frame_start: String::new(),
11840 temporal_frame_end: String::new(),
11841 provenance: String::new(),
11842 derivation: String::new(),
11843 loc: Loc {
11844 line: tok.line,
11845 column: tok.column,
11846 },
11847 leading_trivia: Vec::new(),
11848 trailing_trivia: Vec::new(),
11849 };
11850
11851 while !self.check(TokenType::RBrace) {
11852 let field = self.current().clone();
11853 match field.ttype {
11854 TokenType::Ontology => {
11855 self.advance();
11856 self.consume(TokenType::Colon)?;
11857 node.ontology = self.consume(TokenType::StringLit)?.value.clone();
11858 }
11859 TokenType::Certainty => {
11860 self.advance();
11861 self.consume(TokenType::Colon)?;
11862 let val = self.current().clone();
11863 match val.ttype {
11864 TokenType::Float => {
11865 self.advance();
11866 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11867 }
11868 TokenType::Integer => {
11869 self.advance();
11870 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11871 }
11872 _ => {
11873 return Err(ParseError {
11874 message: format!(
11875 "Expected number for certainty, got '{}'",
11876 val.value
11877 ),
11878 line: val.line,
11879 column: val.column,
11880 ..Default::default()
11881 });
11882 }
11883 }
11884 }
11885 TokenType::TemporalFrame => {
11886 self.advance();
11887 self.consume(TokenType::Colon)?;
11888 node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
11889 if self.check(TokenType::StringLit) {
11891 node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
11892 }
11893 }
11894 TokenType::Provenance => {
11895 self.advance();
11896 self.consume(TokenType::Colon)?;
11897 node.provenance = self.consume(TokenType::StringLit)?.value.clone();
11898 }
11899 TokenType::Derivation => {
11900 self.advance();
11901 self.consume(TokenType::Colon)?;
11902 let d = self.current().clone();
11903 self.advance();
11904 node.derivation = d.value.clone();
11905 }
11906 _ => {
11907 self.advance();
11909 if self.check(TokenType::Colon) {
11910 self.advance();
11911 self.skip_value();
11912 }
11913 }
11914 }
11915 }
11916
11917 self.consume(TokenType::RBrace)?;
11918 Ok(node)
11919 }
11920
11921 fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
11922 let tok = self.consume(TokenType::Lambda)?;
11923 let lambda_name = self.consume(TokenType::Identifier)?;
11924
11925 let on_tok = self.current().clone();
11927 self.advance();
11928 if on_tok.value != "on" {
11929 return Err(ParseError {
11930 message: format!(
11931 "Expected 'on' after lambda data name in flow step, got '{}'",
11932 on_tok.value
11933 ),
11934 line: on_tok.line,
11935 column: on_tok.column,
11936 ..Default::default()
11937 });
11938 }
11939
11940 let target = self.current().clone();
11941 self.advance();
11942
11943 let mut output_type = String::new();
11944 if self.check(TokenType::Arrow) {
11945 self.advance();
11946 output_type = self.consume(TokenType::Identifier)?.value.clone();
11947 }
11948
11949 Ok(LambdaDataApplyNode {
11950 lambda_data_name: lambda_name.value.clone(),
11951 target: target.value.clone(),
11952 output_type,
11953 loc: Loc {
11954 line: tok.line,
11955 column: tok.column,
11956 },
11957 })
11958 }
11959
11960 fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
11963 let kw_tok = self.current().clone();
11964 self.advance(); let name = if self.current().ttype == TokenType::Identifier {
11968 let n = self.current().value.clone();
11969 self.advance();
11970 n
11971 } else if !self.check(TokenType::LBrace)
11972 && !self.check(TokenType::LParen)
11973 && !self.check(TokenType::Eof)
11974 && self
11975 .current()
11976 .value
11977 .chars()
11978 .all(|c| c.is_alphanumeric() || c == '_')
11979 {
11980 let n = self.current().value.clone();
11981 self.advance();
11982 n
11983 } else {
11984 String::new()
11985 };
11986
11987 if self.check(TokenType::LParen) {
11989 self.advance();
11990 let mut depth = 1u32;
11991 while depth > 0 && !self.check(TokenType::Eof) {
11992 if self.check(TokenType::LParen) {
11993 depth += 1;
11994 } else if self.check(TokenType::RParen) {
11995 depth -= 1;
11996 }
11997 self.advance();
11998 }
11999 }
12000
12001 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12003 if self.check(TokenType::Eof) {
12004 break;
12005 }
12006 self.advance();
12007 }
12008
12009 if self.check(TokenType::LBrace) {
12011 self.skip_braced_block()?;
12012 }
12013
12014 Ok(Declaration::Generic(GenericDeclaration {
12015 keyword: kw_tok.value,
12016 name,
12017 loc: Loc {
12018 line: kw_tok.line,
12019 column: kw_tok.column,
12020 },
12021 leading_trivia: Vec::new(),
12022 trailing_trivia: Vec::new(),
12023 }))
12024 }
12025
12026 fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12034 let tok = self.consume(TokenType::Channel)?;
12035 let name = self.consume(TokenType::Identifier)?.value;
12036 let mut node = ChannelDefinition {
12037 name: name.clone(),
12038 message: String::new(),
12039 qos: "at_least_once".to_string(),
12040 lifetime: "affine".to_string(),
12041 persistence: "ephemeral".to_string(),
12042 shield_ref: String::new(),
12043 loc: Loc {
12044 line: tok.line,
12045 column: tok.column,
12046 },
12047 leading_trivia: Vec::new(),
12048 trailing_trivia: Vec::new(),
12049 };
12050 self.consume(TokenType::LBrace)?;
12051 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12052 let field_tok = self.current().clone();
12053 let field_name = field_tok.value.clone();
12054 self.advance();
12055 if !self.check(TokenType::Colon) {
12056 if self.check(TokenType::LBrace) {
12057 self.skip_braced_block()?;
12058 }
12059 continue;
12060 }
12061 self.advance();
12062 match field_name.as_str() {
12063 "message" => node.message = self.parse_channel_message_type()?,
12064 "qos" => {
12065 let q_tok = self.consume_any_ident_or_kw()?;
12066 if !matches!(
12067 q_tok.value.as_str(),
12068 "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12069 ) {
12070 return Err(ParseError {
12071 message: format!(
12072 "Invalid qos '{}' in channel '{}' — \
12073 expected at_most_once | at_least_once | \
12074 exactly_once | broadcast | queue",
12075 q_tok.value, name
12076 ),
12077 line: q_tok.line,
12078 column: q_tok.column,
12079 ..Default::default()
12080 });
12081 }
12082 node.qos = q_tok.value;
12083 }
12084 "lifetime" => {
12085 let lt_tok = self.consume_any_ident_or_kw()?;
12086 if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12087 return Err(ParseError {
12088 message: format!(
12089 "Invalid lifetime '{}' in channel '{}' — \
12090 expected linear | affine | persistent",
12091 lt_tok.value, name
12092 ),
12093 line: lt_tok.line,
12094 column: lt_tok.column,
12095 ..Default::default()
12096 });
12097 }
12098 node.lifetime = lt_tok.value;
12099 }
12100 "persistence" => {
12101 let p_tok = self.consume_any_ident_or_kw()?;
12102 if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12103 return Err(ParseError {
12104 message: format!(
12105 "Invalid persistence '{}' in channel '{}' — \
12106 expected ephemeral | persistent_axonstore",
12107 p_tok.value, name
12108 ),
12109 line: p_tok.line,
12110 column: p_tok.column,
12111 ..Default::default()
12112 });
12113 }
12114 node.persistence = p_tok.value;
12115 }
12116 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12117 _ => self.skip_value(),
12118 }
12119 }
12120 self.consume(TokenType::RBrace)?;
12121 Ok(node)
12122 }
12123
12124 fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12127 let head = self.consume(TokenType::Identifier)?;
12128 let mut spelling = head.value;
12129 if self.check(TokenType::Lt) {
12130 self.advance();
12131 let inner = self.parse_channel_message_type()?;
12132 self.consume(TokenType::Gt)?;
12133 spelling = format!("{}<{}>", spelling, inner);
12134 }
12135 Ok(spelling)
12136 }
12137
12138 fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12144 let tok = self.consume(TokenType::Emit)?;
12145 let channel = self.consume(TokenType::Identifier)?.value;
12146 self.consume(TokenType::LParen)?;
12147 let value = self.parse_emit_value_ref()?;
12148 self.consume(TokenType::RParen)?;
12149 Ok(FlowStep::Emit(EmitStatement {
12150 channel_ref: channel,
12151 value_ref: value,
12152 loc: Loc {
12153 line: tok.line,
12154 column: tok.column,
12155 },
12156 }))
12157 }
12158
12159 fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12165 let tok = self.consume(TokenType::Mint)?;
12166 let credential_ref = self.consume(TokenType::Identifier)?.value;
12167 self.consume(TokenType::As)?;
12168 let binding = self.consume(TokenType::Identifier)?.value;
12169 Ok(FlowStep::Mint(MintStep {
12170 credential_ref,
12171 binding,
12172 loc: Loc {
12173 line: tok.line,
12174 column: tok.column,
12175 },
12176 }))
12177 }
12178
12179 fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12194 let tok = self.consume(TokenType::Rotate)?;
12195 let store_ref = self.consume(TokenType::Identifier)?.value;
12196 let mut where_expr = String::new();
12197 if self.check(TokenType::Where) {
12198 self.advance();
12199 where_expr = self.consume(TokenType::StringLit)?.value.clone();
12200 }
12201 let with_tok = self.current().clone();
12202 if with_tok.value != "with" {
12203 return Err(ParseError {
12204 message: format!(
12205 "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12206 A rotation names the tool that performs the renewal exchange: \
12207 `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12208 if where_expr.is_empty() { "" } else { " where …" },
12209 with_tok.value
12210 ),
12211 line: with_tok.line,
12212 column: with_tok.column,
12213 ..Default::default()
12214 });
12215 }
12216 self.advance();
12217 let tool_ref = self.consume(TokenType::Identifier)?.value;
12218 self.consume(TokenType::As)?;
12219 let binding = self.consume(TokenType::Identifier)?.value;
12220 Ok(FlowStep::Rotate(RotateStep {
12221 store_ref,
12222 where_expr,
12223 tool_ref,
12224 binding,
12225 loc: Loc {
12226 line: tok.line,
12227 column: tok.column,
12228 },
12229 }))
12230 }
12231
12232 fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12248 let head = self.consume(TokenType::Identifier)?.value;
12249 let mut parts = vec![head];
12250 while self.check(TokenType::Dot) {
12251 self.advance(); let next_tok = self.current().clone();
12253 let valid = !next_tok.value.is_empty()
12254 && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12255 || next_tok.value.starts_with('_');
12256 if !valid {
12257 return Err(ParseError {
12258 message: format!(
12259 "Expected identifier or keyword after '.' in dotted \
12260 access, found {:?}",
12261 next_tok.value
12262 ),
12263 line: next_tok.line,
12264 column: next_tok.column,
12265 ..Default::default()
12266 });
12267 }
12268 self.advance();
12269 parts.push(next_tok.value);
12270 }
12271 Ok(parts.join("."))
12272 }
12273
12274 fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12276 let tok = self.consume(TokenType::Publish)?;
12277 let channel = self.consume(TokenType::Identifier)?.value;
12278 self.consume(TokenType::Within)?;
12279 let shield = self.consume(TokenType::Identifier)?.value;
12280 Ok(FlowStep::Publish(PublishStatement {
12281 channel_ref: channel,
12282 shield_ref: shield,
12283 loc: Loc {
12284 line: tok.line,
12285 column: tok.column,
12286 },
12287 }))
12288 }
12289
12290 fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12292 let tok = self.consume(TokenType::Discover)?;
12293 let cap = self.consume(TokenType::Identifier)?.value;
12294 self.consume(TokenType::As)?;
12295 let alias = self.consume(TokenType::Identifier)?.value;
12296 Ok(FlowStep::Discover(DiscoverStatement {
12297 capability_ref: cap,
12298 alias,
12299 loc: Loc {
12300 line: tok.line,
12301 column: tok.column,
12302 },
12303 }))
12304 }
12305}
12306
12307#[cfg(test)]
12310mod parser_tests {
12311 use super::*;
12312 use crate::lexer::Lexer;
12313
12314 fn parse(src: &str) -> Result<Program, ParseError> {
12315 let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12316 Parser::new(tokens).parse()
12317 }
12318
12319 #[test]
12320 fn channel_full_parses() {
12321 let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12322 let prog = parse(src).expect("parse");
12323 match &prog.declarations[0] {
12324 Declaration::Channel(c) => {
12325 assert_eq!(c.name, "C");
12326 assert_eq!(c.message, "Order");
12327 assert_eq!(c.qos, "at_least_once");
12328 assert_eq!(c.lifetime, "affine");
12329 assert_eq!(c.persistence, "ephemeral");
12330 assert_eq!(c.shield_ref, "Gate");
12331 }
12332 _ => panic!("expected ChannelDefinition"),
12333 }
12334 }
12335
12336 #[test]
12337 fn channel_defaults_match_paper_d1() {
12338 let prog = parse("channel C { message: Order }").expect("parse");
12339 if let Declaration::Channel(c) = &prog.declarations[0] {
12340 assert_eq!(c.qos, "at_least_once"); assert_eq!(c.lifetime, "affine"); assert_eq!(c.persistence, "ephemeral");
12343 assert_eq!(c.shield_ref, "");
12344 } else {
12345 panic!("expected ChannelDefinition");
12346 }
12347 }
12348
12349 #[test]
12350 fn channel_second_order_message_type_parses() {
12351 let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12352 if let Declaration::Channel(c) = &prog.declarations[0] {
12353 assert_eq!(c.message, "Channel<Order>");
12354 } else {
12355 panic!("expected ChannelDefinition");
12356 }
12357 }
12358
12359 #[test]
12360 fn channel_nested_channel_message_type_parses() {
12361 let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12362 if let Declaration::Channel(c) = &prog.declarations[0] {
12363 assert_eq!(c.message, "Channel<Channel<Order>>");
12364 } else {
12365 panic!("expected ChannelDefinition");
12366 }
12367 }
12368
12369 #[test]
12370 fn channel_invalid_qos_rejected() {
12371 let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12372 assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12373 }
12374
12375 #[test]
12376 fn channel_invalid_lifetime_rejected() {
12377 let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12378 assert!(
12379 err.message.contains("Invalid lifetime"),
12380 "got {}",
12381 err.message
12382 );
12383 }
12384
12385 #[test]
12386 fn channel_invalid_persistence_rejected() {
12387 let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12388 assert!(
12389 err.message.contains("Invalid persistence"),
12390 "got {}",
12391 err.message
12392 );
12393 }
12394
12395 #[test]
12396 fn emit_value_parses() {
12397 let src = "flow f() -> Out { emit C(payload) }";
12398 let prog = parse(src).expect("parse");
12399 if let Declaration::Flow(f) = &prog.declarations[0] {
12400 match &f.body[0] {
12401 FlowStep::Emit(e) => {
12402 assert_eq!(e.channel_ref, "C");
12403 assert_eq!(e.value_ref, "payload");
12404 }
12405 other => panic!("expected Emit, got {:?}", other),
12406 }
12407 } else {
12408 panic!("expected Flow");
12409 }
12410 }
12411
12412 #[test]
12413 fn publish_within_shield_parses() {
12414 let src = "flow f() -> Cap { publish C within Gate }";
12415 let prog = parse(src).expect("parse");
12416 if let Declaration::Flow(f) = &prog.declarations[0] {
12417 match &f.body[0] {
12418 FlowStep::Publish(p) => {
12419 assert_eq!(p.channel_ref, "C");
12420 assert_eq!(p.shield_ref, "Gate");
12421 }
12422 other => panic!("expected Publish, got {:?}", other),
12423 }
12424 } else {
12425 panic!("expected Flow");
12426 }
12427 }
12428
12429 #[test]
12430 fn discover_with_alias_parses() {
12431 let src = "flow f() -> Out { discover C as ch }";
12432 let prog = parse(src).expect("parse");
12433 if let Declaration::Flow(f) = &prog.declarations[0] {
12434 match &f.body[0] {
12435 FlowStep::Discover(d) => {
12436 assert_eq!(d.capability_ref, "C");
12437 assert_eq!(d.alias, "ch");
12438 }
12439 other => panic!("expected Discover, got {:?}", other),
12440 }
12441 } else {
12442 panic!("expected Flow");
12443 }
12444 }
12445
12446 #[test]
12447 fn listen_typed_ref_sets_flag_true() {
12448 let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12449 let prog = parse(src).expect("parse");
12450 if let Declaration::Daemon(d) = &prog.declarations[0] {
12451 assert_eq!(d.listeners.len(), 1);
12452 assert_eq!(d.listeners[0].channel, "C");
12453 assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12454 } else {
12455 panic!("expected Daemon");
12456 }
12457 }
12458
12459 #[test]
12460 fn listen_string_topic_legacy_flag_false() {
12461 let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12462 let prog = parse(src).expect("parse");
12463 if let Declaration::Daemon(d) = &prog.declarations[0] {
12464 assert_eq!(d.listeners.len(), 1);
12465 assert_eq!(d.listeners[0].channel, "orders");
12466 assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12467 } else {
12468 panic!("expected Daemon");
12469 }
12470 }
12471
12472 fn extract_first_emit(prog: &Program) -> &EmitStatement {
12475 if let Declaration::Flow(f) = &prog.declarations[0] {
12476 if let FlowStep::Emit(e) = &f.body[0] {
12477 return e;
12478 }
12479 }
12480 panic!("expected emit statement at flow body[0]");
12481 }
12482
12483 #[test]
12484 fn emit_accepts_bare_identifier_value_ref() {
12485 let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12487 let emit = extract_first_emit(&prog);
12488 assert_eq!(emit.channel_ref, "Hello");
12489 assert_eq!(emit.value_ref, "payload");
12490 }
12491
12492 #[test]
12493 fn emit_accepts_two_segment_dotted_value_ref() {
12494 let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12496 let emit = extract_first_emit(&prog);
12497 assert_eq!(emit.value_ref, "Build.output");
12498 }
12499
12500 #[test]
12501 fn emit_accepts_three_segment_nested_dotted_value_ref() {
12502 let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12503 let emit = extract_first_emit(&prog);
12504 assert_eq!(emit.value_ref, "Analyze.result.score");
12505 }
12506
12507 #[test]
12508 fn emit_dotted_with_trailing_dot_fails() {
12509 let result = parse("flow f() -> Out { emit Hello(Build.) }");
12511 assert!(result.is_err(), "expected parse error for trailing dot");
12512 }
12513}
12514
12515#[cfg(test)]
12518mod declaration_trivia_tests {
12519 use super::*;
12520 use crate::lexer::Lexer;
12521 use crate::tokens::TriviaKind;
12522
12523 fn parse(src: &str) -> Program {
12524 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12525 Parser::new(toks).parse().expect("parse")
12526 }
12527
12528 #[test]
12529 fn no_comments_means_empty_trivia_per_decl() {
12530 let prog = parse("flow F() -> Out { }");
12531 assert_eq!(prog.declarations.len(), 1);
12532 assert_eq!(prog.declaration_trivia.len(), 1);
12533 assert!(prog.declaration_trivia[0].leading.is_empty());
12534 assert!(prog.declaration_trivia[0].trailing.is_empty());
12535 }
12536
12537 #[test]
12538 fn doc_line_comment_attaches_as_leading() {
12539 let prog = parse("/// Documents F\nflow F() -> Out { }");
12540 let triv = &prog.declaration_trivia[0];
12541 assert_eq!(triv.leading.len(), 1);
12542 assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12543 assert!(triv.leading[0].is_doc());
12544 assert_eq!(triv.leading[0].text, "/// Documents F");
12545 }
12546
12547 #[test]
12548 fn regular_line_comment_attaches_as_leading() {
12549 let prog = parse("// header\nflow F() -> Out { }");
12550 let triv = &prog.declaration_trivia[0];
12551 assert_eq!(triv.leading.len(), 1);
12552 assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12553 assert!(!triv.leading[0].is_doc());
12554 }
12555
12556 #[test]
12557 fn block_doc_comment_attaches_as_leading() {
12558 let prog = parse("/** Doc block */\nflow F() -> Out { }");
12559 let triv = &prog.declaration_trivia[0];
12560 assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12561 assert!(triv.leading[0].is_doc());
12562 }
12563
12564 #[test]
12565 fn multiple_comments_collected_in_source_order() {
12566 let src = "/// First\n/// Second\nflow F() -> Out { }";
12567 let prog = parse(src);
12568 let triv = &prog.declaration_trivia[0];
12569 assert_eq!(triv.leading.len(), 2);
12570 assert_eq!(triv.leading[0].text, "/// First");
12571 assert_eq!(triv.leading[1].text, "/// Second");
12572 }
12573
12574 #[test]
12575 fn three_decls_each_get_own_leading() {
12576 let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12577 let prog = parse(src);
12578 assert_eq!(prog.declarations.len(), 3);
12579 assert_eq!(prog.declaration_trivia.len(), 3);
12580 for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12581 let triv = &prog.declaration_trivia[idx];
12582 assert_eq!(triv.leading.len(), 1);
12583 assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12584 }
12585 }
12586
12587 #[test]
12588 fn trailing_comment_attaches_to_last_token_of_decl() {
12589 let prog = parse("flow F() -> Out { } // tail");
12591 let triv = &prog.declaration_trivia[0];
12592 assert_eq!(triv.trailing.len(), 1);
12593 assert_eq!(triv.trailing[0].text, "// tail");
12594 }
12595
12596 #[test]
12597 fn mixed_doc_and_regular_preserve_order_between_decls() {
12598 let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12599 let prog = parse(src);
12600 assert_eq!(prog.declarations.len(), 2);
12601 assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12603 assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12605 assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12606 assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12607 }
12608
12609 #[test]
12610 fn parser_unaffected_by_comments_in_grammar_path() {
12611 let src =
12616 "// before flow\nflow /* between flow and name */ F() -> Out {\n // body comment\n}";
12617 let prog = parse(src);
12618 assert_eq!(prog.declarations.len(), 1);
12619 if let Declaration::Flow(f) = &prog.declarations[0] {
12620 assert_eq!(f.name, "F");
12621 } else {
12622 panic!("expected Flow declaration");
12623 }
12624 }
12625}
12626
12627#[cfg(test)]
12636mod per_struct_trivia_tests {
12637 use super::*;
12638 use crate::lexer::Lexer;
12639 use crate::tokens::TriviaKind;
12640
12641 fn parse(src: &str) -> Program {
12642 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12643 Parser::new(toks).parse().expect("parse")
12644 }
12645
12646 #[test]
12647 fn flow_definition_carries_leading_trivia_directly() {
12648 let prog = parse("/// documents F\nflow F() -> Out { }");
12649 if let Declaration::Flow(f) = &prog.declarations[0] {
12650 assert_eq!(f.leading_trivia.len(), 1);
12651 assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12652 assert_eq!(f.leading_trivia[0].text, "/// documents F");
12653 assert!(f.trailing_trivia.is_empty());
12654 } else {
12655 panic!("expected Flow declaration");
12656 }
12657 }
12658
12659 #[test]
12660 fn flow_definition_carries_trailing_trivia_directly() {
12661 let prog = parse("flow F() -> Out { } // tail comment");
12662 if let Declaration::Flow(f) = &prog.declarations[0] {
12663 assert_eq!(f.trailing_trivia.len(), 1);
12664 assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12665 } else {
12666 panic!("expected Flow declaration");
12667 }
12668 }
12669
12670 #[test]
12671 fn channel_definition_carries_trivia_directly() {
12672 let src = concat!(
12675 "/// inbound order events\n",
12676 "channel Orders {\n",
12677 " message: Order\n",
12678 " qos: at_least_once\n",
12679 " lifetime: affine\n",
12680 " persistence: ephemeral\n",
12681 " shield: Broker\n",
12682 "}",
12683 );
12684 let prog = parse(src);
12685 if let Declaration::Channel(ch) = &prog.declarations[0] {
12686 assert_eq!(ch.leading_trivia.len(), 1);
12687 assert!(ch.leading_trivia[0].is_doc());
12688 assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12689 } else {
12690 panic!("expected Channel declaration");
12691 }
12692 }
12693
12694 #[test]
12695 fn per_struct_fields_match_side_channel() {
12696 let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12699 let prog = parse(src);
12700 for (idx, decl) in prog.declarations.iter().enumerate() {
12701 let side = &prog.declaration_trivia[idx];
12702 let (per_lead, per_trail) = match decl {
12703 Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12704 _ => panic!("unexpected variant"),
12705 };
12706 assert_eq!(per_lead.len(), side.leading.len());
12707 assert_eq!(per_trail.len(), side.trailing.len());
12708 for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12709 assert_eq!(a.text, b.text);
12710 assert_eq!(a.kind, b.kind);
12711 }
12712 }
12713 }
12714
12715 #[test]
12716 fn comment_free_program_yields_empty_per_struct_fields() {
12717 let prog = parse("flow F() -> Out { }");
12718 if let Declaration::Flow(f) = &prog.declarations[0] {
12719 assert!(f.leading_trivia.is_empty());
12720 assert!(f.trailing_trivia.is_empty());
12721 } else {
12722 panic!("expected Flow declaration");
12723 }
12724 }
12725}
12726
12727#[cfg(test)]
12736mod inner_doc_tests {
12737 use super::*;
12738 use crate::lexer::Lexer;
12739 use crate::tokens::TriviaKind;
12740
12741 fn parse(src: &str) -> Program {
12742 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12743 Parser::new(toks).parse().expect("parse")
12744 }
12745
12746 #[test]
12747 fn inner_doc_line_reaches_leading_trivia() {
12748 let src = "//! file-level docs\nflow F() -> Out { }";
12749 let prog = parse(src);
12750 let triv = &prog.declaration_trivia[0];
12751 assert_eq!(triv.leading.len(), 1);
12752 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12753 assert!(triv.leading[0].is_doc());
12754 assert!(triv.leading[0].is_inner_doc());
12755 assert_eq!(triv.leading[0].text, "//! file-level docs");
12756 assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12757 }
12758
12759 #[test]
12760 fn inner_doc_block_reaches_leading_trivia() {
12761 let src = "/*! module-level docs */\nflow F() -> Out { }";
12762 let prog = parse(src);
12763 let triv = &prog.declaration_trivia[0];
12764 assert_eq!(triv.leading.len(), 1);
12765 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12766 assert!(triv.leading[0].is_inner_doc());
12767 assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12768 }
12769
12770 #[test]
12771 fn outer_and_inner_doc_can_coexist() {
12772 let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12776 let prog = parse(src);
12777 let triv = &prog.declaration_trivia[0];
12778 assert_eq!(triv.leading.len(), 2);
12779 assert!(triv.leading[0].is_inner_doc());
12780 assert!(triv.leading[1].is_doc());
12781 assert!(!triv.leading[1].is_inner_doc());
12782 }
12783
12784 #[test]
12785 fn inner_doc_reaches_per_struct_fields() {
12786 let src = "//! intro\nflow F() -> Out { }";
12788 let prog = parse(src);
12789 if let Declaration::Flow(f) = &prog.declarations[0] {
12790 assert_eq!(f.leading_trivia.len(), 1);
12791 assert!(f.leading_trivia[0].is_inner_doc());
12792 } else {
12793 panic!("expected Flow declaration");
12794 }
12795 }
12796}
12797
12798#[cfg(test)]
12816mod recovery_tests {
12817 use super::*;
12818 use crate::lexer::Lexer;
12819
12820 fn lex(src: &str) -> Vec<Token> {
12823 Lexer::new(src, "<test>").tokenize().expect("lex")
12824 }
12825
12826 fn recover(src: &str) -> ParseResult {
12829 Parser::new(lex(src)).parse_with_recovery()
12830 }
12831
12832 fn strict(src: &str) -> Result<Program, ParseError> {
12834 Parser::new(lex(src)).parse()
12835 }
12836
12837 #[test]
12840 fn strict_parse_unchanged_for_clean_source() {
12841 let src = "intent I {}";
12844 let prog = strict(src).expect("clean parse");
12845 assert_eq!(prog.declarations.len(), 1);
12846 }
12847
12848 #[test]
12849 fn strict_parse_still_raises_on_first_error() {
12850 let src = "flow F() { } not_a_keyword flow G() { }";
12855 let _ = strict(src).expect_err("must error fast in strict mode");
12856 }
12857
12858 #[test]
12859 fn recovery_clean_source_yields_no_errors() {
12860 let src = "flow F() { } flow G() { }";
12861 let pr = recover(src);
12862 assert!(pr.is_clean(), "errors: {:?}", pr.errors);
12863 assert_eq!(pr.program.declarations.len(), 2);
12864 }
12865
12866 #[test]
12869 fn single_unknown_top_level_token_recovers() {
12870 let src = "garbage_token flow F() { } flow G() { }";
12872 let pr = recover(src);
12873 assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
12874 assert_eq!(pr.program.declarations.len(), 2);
12875 }
12876
12877 #[test]
12878 fn error_in_first_decl_does_not_block_second() {
12879 let src = "flow F() { not_a_step nope } flow G() { }";
12882 let pr = recover(src);
12883 assert!(pr.has_errors(), "expected at least one error");
12884 let names: Vec<&str> = pr
12886 .program
12887 .declarations
12888 .iter()
12889 .filter_map(|d| match d {
12890 Declaration::Flow(f) => Some(f.name.as_str()),
12891 _ => None,
12892 })
12893 .collect();
12894 assert!(names.contains(&"G"), "G not found among {names:?}");
12895 }
12896
12897 #[test]
12898 fn malformed_declaration_then_clean_intent_recovers() {
12899 let src = "flow @ () { } intent I {}";
12900 let pr = recover(src);
12901 assert!(pr.has_errors());
12902 let kinds: Vec<&str> = pr
12903 .program
12904 .declarations
12905 .iter()
12906 .map(|d| match d {
12907 Declaration::Intent(_) => "intent",
12908 Declaration::Flow(_) => "flow",
12909 _ => "other",
12910 })
12911 .collect();
12912 assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
12913 }
12914
12915 #[test]
12916 fn recovery_does_not_double_count_a_single_error() {
12917 let src = "flow F() { not_a_step }";
12925 let pr = recover(src);
12926 let outer_ghosts = pr
12927 .errors
12928 .iter()
12929 .filter(|e| e.message.contains("at top level"))
12930 .count();
12931 assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
12932 }
12933
12934 #[test]
12937 fn three_independent_errors_yield_three_entries() {
12938 let src =
12939 "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
12940 let pr = recover(src);
12941 assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
12942 assert_eq!(pr.program.declarations.len(), 3);
12943 }
12944
12945 #[test]
12946 fn all_errors_no_valid_declarations() {
12947 let src = "foo bar baz qux";
12948 let pr = recover(src);
12949 assert!(pr.has_errors());
12950 assert!(pr.program.declarations.is_empty());
12951 }
12952
12953 #[test]
12954 fn errors_recorded_in_source_order() {
12955 let src = "x flow A() { } y flow B() { } z flow C() { }";
12956 let pr = recover(src);
12957 assert_eq!(pr.errors.len(), 3);
12958 let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
12959 assert!(
12962 lines.windows(2).all(|w| w[0] <= w[1]),
12963 "errors out of order: {lines:?}"
12964 );
12965 }
12966
12967 #[test]
12970 fn sync_to_flow_keyword() {
12971 let src = "garbage flow F() { }";
12972 let pr = recover(src);
12973 assert_eq!(pr.program.declarations.len(), 1);
12974 }
12975
12976 #[test]
12977 fn sync_to_intent_keyword() {
12978 let src = "garbage intent I {}";
12979 let pr = recover(src);
12980 assert_eq!(pr.program.declarations.len(), 1);
12981 }
12982
12983 #[test]
12984 fn sync_to_persona_keyword() {
12985 let src = "garbage persona P { name: \"P\" role: \"R\" }";
12986 let pr = recover(src);
12987 assert!(
12988 pr.program
12989 .declarations
12990 .iter()
12991 .any(|d| matches!(d, Declaration::Persona(_))),
12992 "persona not recovered: decls = {:?}",
12993 pr.program.declarations.len()
12994 );
12995 }
12996
12997 #[test]
12998 fn sync_to_run_keyword() {
12999 let src = "garbage run R { input: { user_message: \"hi\" } }";
13000 let pr = recover(src);
13001 assert!(pr.has_errors());
13003 }
13004
13005 #[test]
13008 fn parse_result_has_errors_and_is_clean_invert() {
13009 let pr_clean = recover("flow F() { }");
13010 assert!(pr_clean.is_clean());
13011 assert!(!pr_clean.has_errors());
13012
13013 let pr_err = recover("garbage");
13014 assert!(!pr_err.is_clean());
13015 assert!(pr_err.has_errors());
13016 }
13017
13018 #[test]
13019 fn parse_result_program_field_holds_partial_program() {
13020 let pr = recover("garbage flow F() { }");
13021 assert!(!pr.program.declarations.is_empty());
13022 }
13023
13024 #[test]
13025 fn parse_result_errors_carry_line_and_column() {
13026 let pr = recover("garbage");
13027 assert!(!pr.errors.is_empty());
13028 let e = &pr.errors[0];
13029 assert!(e.line >= 1);
13030 let _ = e.column;
13033 assert!(!e.message.is_empty());
13034 }
13035
13036 #[test]
13037 fn parse_result_debug_renders() {
13038 let pr = recover("flow F() { }");
13039 let s = format!("{pr:?}");
13040 assert!(s.contains("ParseResult"));
13041 }
13042
13043 #[test]
13046 fn empty_source_is_clean() {
13047 let pr = recover("");
13048 assert!(pr.is_clean());
13049 assert!(pr.program.declarations.is_empty());
13050 }
13051
13052 #[test]
13053 fn whitespace_only_source_is_clean() {
13054 let pr = recover(" \n\n\t \n");
13055 assert!(pr.is_clean());
13056 assert!(pr.program.declarations.is_empty());
13057 }
13058
13059 #[test]
13060 fn only_garbage_does_not_crash() {
13061 let pr = recover("foo bar baz { qux quux } corge { grault }");
13063 assert!(pr.has_errors());
13064 }
13065
13066 #[test]
13067 fn unbalanced_close_brace_does_not_crash() {
13068 let pr = recover("} flow F() { }");
13069 let names: Vec<&str> = pr
13071 .program
13072 .declarations
13073 .iter()
13074 .filter_map(|d| match d {
13075 Declaration::Flow(f) => Some(f.name.as_str()),
13076 _ => None,
13077 })
13078 .collect();
13079 assert!(names.contains(&"F"), "F not recovered: {names:?}");
13080 }
13081
13082 #[test]
13083 fn error_at_eof_does_not_loop() {
13084 let pr = recover("flow F() { ");
13086 let _ = pr.errors.len();
13088 }
13089
13090 #[test]
13091 fn nested_braces_inside_error_still_balance() {
13092 let src = "flow F() { not_a_step { inner } } flow G() { }";
13095 let pr = recover(src);
13096 let names: Vec<&str> = pr
13097 .program
13098 .declarations
13099 .iter()
13100 .filter_map(|d| match d {
13101 Declaration::Flow(f) => Some(f.name.as_str()),
13102 _ => None,
13103 })
13104 .collect();
13105 assert!(names.contains(&"G"), "G not recovered: {names:?}");
13106 }
13107
13108 #[derive(Clone, Copy)]
13117 struct Xorshift(u64);
13118 impl Xorshift {
13119 fn next(&mut self) -> u64 {
13120 let mut x = self.0;
13121 x ^= x << 13;
13122 x ^= x >> 7;
13123 x ^= x << 17;
13124 self.0 = x;
13125 x
13126 }
13127 fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13128 slice[(self.next() as usize) % slice.len()]
13129 }
13130 }
13131
13132 fn mutate(src: &str, rng: &mut Xorshift) -> String {
13133 let mut bytes: Vec<u8> = src.bytes().collect();
13134 if bytes.is_empty() {
13135 return src.to_string();
13136 }
13137 let op = rng.next() % 4;
13138 let pos = (rng.next() as usize) % bytes.len();
13139 let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13143 match op {
13144 0 => {
13145 bytes.remove(pos);
13146 }
13147 1 => {
13148 let b = rng.pick(safe);
13149 bytes.insert(pos, b);
13150 }
13151 2 if pos + 1 < bytes.len() => {
13152 bytes.swap(pos, pos + 1);
13153 }
13154 _ => {
13155 let b = rng.pick(safe);
13156 bytes[pos] = b;
13157 }
13158 }
13159 bytes.retain(|b| b.is_ascii());
13162 String::from_utf8_lossy(&bytes).into_owned()
13163 }
13164
13165 #[test]
13166 fn fuzz_recovery_never_crashes() {
13167 let seed_bases = [
13168 "flow F() { }",
13169 "intent I { }",
13170 "persona P { name: \"P\" role: \"R\" }",
13171 "intent J { ask: \"a\" }",
13172 "type T = String",
13173 ];
13174 for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13176 let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13177 let mut current = (*base).to_string();
13178 for _ in 0..10 {
13179 current = mutate(¤t, &mut rng);
13180 let toks = match Lexer::new(¤t, "<fuzz>").tokenize() {
13183 Ok(t) => t,
13184 Err(_) => continue,
13185 };
13186 let _pr = Parser::new(toks).parse_with_recovery();
13188 }
13189 }
13190 }
13191
13192 #[test]
13195 fn missing_colon_hint_preserved_under_recovery() {
13196 let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13200 let pr = recover(src);
13201 if !pr.errors.is_empty() {
13205 let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13206 assert!(any_msg);
13207 }
13208 }
13209
13210 #[test]
13213 fn recovered_declarations_appear_in_source_order() {
13214 let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13215 let pr = recover(src);
13216 let names: Vec<&str> = pr
13217 .program
13218 .declarations
13219 .iter()
13220 .filter_map(|d| match d {
13221 Declaration::Flow(f) => Some(f.name.as_str()),
13222 _ => None,
13223 })
13224 .collect();
13225 assert_eq!(names, vec!["A", "B", "C"]);
13226 }
13227}
13228
13229#[cfg(test)]
13237mod source_context_tests {
13238 use super::*;
13239 use crate::lexer::Lexer;
13240
13241 fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13242 SourceSnippet::new(
13243 source.to_string(),
13244 line,
13245 column,
13246 filename.to_string(),
13247 )
13248 .render()
13249 }
13250
13251 #[test]
13254 fn rustc_style_block_for_middle_line() {
13255 let src = "line one\nline two\nline three\nline four\nline five";
13256 let out = snippet(src, 3, 6, "x.axon");
13257 assert!(out.contains("--> x.axon:3:6"));
13258 assert!(out.contains("1 | line one"));
13259 assert!(out.contains("2 | line two"));
13260 assert!(out.contains("3 | line three"));
13261 assert!(out.contains("4 | line four"));
13262 assert!(out.contains("5 | line five"));
13263 assert!(out.contains("\n | ^"), "out:\n{out}");
13265 }
13266
13267 #[test]
13268 fn caret_column_one_renders_correctly() {
13269 let out = snippet("abc\n", 1, 1, "<source>");
13270 assert!(out.contains("\n | ^"));
13271 }
13272
13273 #[test]
13274 fn first_line_clamps_context_before_to_zero() {
13275 let src = "first\nsecond\nthird\nfourth\nfifth";
13276 let out = snippet(src, 1, 1, "<source>");
13277 assert!(out.contains("1 | first"));
13278 assert!(out.contains("2 | second"));
13279 assert!(out.contains("3 | third"));
13280 assert!(!out.contains("4 | fourth"));
13281 }
13282
13283 #[test]
13284 fn last_line_clamps_context_after_to_eof() {
13285 let src = "first\nsecond\nthird\nfourth\nfifth";
13286 let out = snippet(src, 5, 2, "<source>");
13287 assert!(out.contains("5 | fifth"));
13288 assert!(out.contains("3 | third"));
13289 assert!(out.contains("4 | fourth"));
13290 assert!(!out.contains("2 | second"));
13291 }
13292
13293 #[test]
13294 fn gutter_width_grows_with_line_count() {
13295 let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13296 let out = snippet(&src, 12, 1, "<source>");
13297 assert!(out.contains("12 | line12"));
13298 assert!(out.contains("10 | line10"));
13299 }
13300
13301 #[test]
13304 fn empty_source_returns_empty() {
13305 assert_eq!(snippet("", 1, 1, "<source>"), "");
13306 }
13307
13308 #[test]
13309 fn zero_line_returns_empty() {
13310 assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13311 }
13312
13313 #[test]
13314 fn out_of_range_line_returns_empty() {
13315 assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13316 }
13317
13318 #[test]
13319 fn caret_clamps_past_eol() {
13320 let out = snippet("hello", 1, 50, "<source>");
13321 assert!(out.contains("\n | ^"), "out:\n{out}");
13322 }
13323
13324 #[test]
13325 fn unicode_codepoint_count_for_caret_clamp() {
13326 let out = snippet("héllo", 1, 99, "<source>");
13328 assert!(out.contains("\n | ^"), "out:\n{out}");
13329 }
13330
13331 #[test]
13332 fn trailing_newline_does_not_create_phantom_last_line() {
13333 let out = snippet("first\nsecond\n", 2, 1, "<source>");
13334 assert!(!out.contains("3 |"));
13335 assert!(out.contains("2 | second"));
13336 }
13337
13338 fn lex(src: &str) -> Vec<Token> {
13341 Lexer::new(src, "<test>").tokenize().expect("lex")
13342 }
13343
13344 #[test]
13345 fn strict_parse_attaches_snippet_when_source_given() {
13346 let src = "garbage_token\nflow F() { }";
13347 let err = Parser::new(lex(src))
13348 .with_source(src, "x.axon")
13349 .parse()
13350 .expect_err("must error");
13351 assert!(err.source_snippet.is_some());
13352 let display = format!("{err}");
13353 assert!(display.contains("--> x.axon:"), "display: {display}");
13354 }
13355
13356 #[test]
13357 fn strict_parse_no_snippet_when_no_source() {
13358 let src = "garbage_token";
13359 let err = Parser::new(lex(src)).parse().expect_err("must error");
13360 assert!(err.source_snippet.is_none());
13361 let display = format!("{err}");
13362 assert!(!display.contains("\n -->"));
13363 }
13364
13365 #[test]
13366 fn every_recovered_error_has_snippet() {
13367 let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13368 let result = Parser::new(lex(src))
13369 .with_source(src, "multi.axon")
13370 .parse_with_recovery();
13371 assert!(!result.errors.is_empty());
13372 for err in &result.errors {
13373 assert!(err.source_snippet.is_some());
13374 let display = format!("{err}");
13375 assert!(
13376 display.contains("--> multi.axon:"),
13377 "display: {display}"
13378 );
13379 }
13380 }
13381
13382 #[test]
13383 fn recovery_no_snippet_when_no_source() {
13384 let src = "garbage1 garbage2";
13385 let result = Parser::new(lex(src)).parse_with_recovery();
13386 for err in &result.errors {
13387 assert!(err.source_snippet.is_none());
13388 }
13389 }
13390
13391 #[test]
13392 fn snippet_points_at_correct_line_for_each_error() {
13393 let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13394 let result = Parser::new(lex(src))
13395 .with_source(src, "x")
13396 .parse_with_recovery();
13397 for err in &result.errors {
13398 let sn = err.source_snippet.as_ref().expect("snippet");
13399 assert_eq!(sn.line, err.line);
13400 }
13401 }
13402
13403 #[test]
13406 fn legacy_constructor_still_works() {
13407 let src = "flow F() { }";
13408 let prog = Parser::new(lex(src)).parse().expect("clean");
13409 assert_eq!(prog.declarations.len(), 1);
13410 }
13411
13412 #[test]
13413 fn attach_source_idempotent() {
13414 let err = ParseError {
13415 message: "bad".to_string(),
13416 line: 2,
13417 column: 3,
13418 ..Default::default()
13419 };
13420 let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13421 let first = format!("{err2}");
13422 let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13423 let second = format!("{err3}");
13424 assert_eq!(first, second);
13425 }
13426
13427 #[test]
13428 fn attach_source_noop_when_line_zero() {
13429 let err = ParseError {
13430 message: "bad".to_string(),
13431 line: 0,
13432 column: 0,
13433 ..Default::default()
13434 };
13435 let err = err.attach_source("a\nb\nc\n", "f.axon");
13436 assert!(err.source_snippet.is_none());
13437 }
13438
13439 #[test]
13445 fn golden_simple_three_line_block() {
13446 let src = "alpha\nbeta\ngamma";
13447 let out = snippet(src, 2, 3, "g.axon");
13448 let expected = concat!(
13452 " --> g.axon:2:3\n",
13453 " |\n",
13454 "1 | alpha\n",
13455 "2 | beta\n",
13456 " | ^\n",
13457 "3 | gamma",
13458 );
13459 assert_eq!(out, expected);
13460 }
13461
13462 #[test]
13463 fn golden_first_line_caret() {
13464 let src = "abc\ndef\n";
13465 let out = snippet(src, 1, 1, "x");
13466 let expected = concat!(
13467 " --> x:1:1\n",
13468 " |\n",
13469 "1 | abc\n",
13470 " | ^\n",
13471 "2 | def",
13472 );
13473 assert_eq!(out, expected);
13474 }
13475
13476 #[test]
13477 fn golden_two_digit_gutter() {
13478 let src: String = (1..=11)
13479 .map(|i| format!("L{i}"))
13480 .collect::<Vec<_>>()
13481 .join("\n");
13482 let out = snippet(&src, 10, 2, "big");
13483 let expected = concat!(
13484 " --> big:10:2\n",
13485 " |\n",
13486 " 8 | L8\n",
13487 " 9 | L9\n",
13488 "10 | L10\n",
13489 " | ^\n",
13490 "11 | L11",
13491 );
13492 assert_eq!(out, expected);
13493 }
13494}
13495
13496#[cfg(test)]
13503mod smart_suggest_parser_tests {
13504 use super::*;
13505 use crate::lexer::Lexer;
13506
13507 fn lex(src: &str) -> Vec<Token> {
13508 Lexer::new(src, "<test>").tokenize().expect("lex")
13509 }
13510
13511 #[test]
13512 fn top_level_typo_suggests_flow() {
13513 let src = "flwo F() { }";
13514 let err = Parser::new(lex(src)).parse().expect_err("must error");
13515 assert!(
13516 err.message.contains("Did you mean `flow`?"),
13517 "msg: {}",
13518 err.message
13519 );
13520 }
13521
13522 #[test]
13523 fn top_level_unknown_far_no_suggestion() {
13524 let src = "qwerty F() { }";
13525 let err = Parser::new(lex(src)).parse().expect_err("must error");
13526 assert!(
13527 !err.message.contains("Did you mean"),
13528 "msg: {}",
13529 err.message
13530 );
13531 }
13532
13533 #[test]
13534 fn flow_body_typo_suggests_step() {
13535 let src = "flow F() { stepp S {} }";
13536 let err = Parser::new(lex(src)).parse().expect_err("must error");
13537 assert!(
13538 err.message.contains("Did you mean `step`"),
13539 "msg: {}",
13540 err.message
13541 );
13542 }
13543
13544 #[test]
13545 fn flow_body_typo_suggests_reason() {
13546 let src = "flow F() { reasn R {} }";
13547 let err = Parser::new(lex(src)).parse().expect_err("must error");
13548 assert!(
13549 err.message.contains("Did you mean `reason`?"),
13550 "msg: {}",
13551 err.message
13552 );
13553 }
13554
13555 #[test]
13556 fn recovery_mode_carries_hint() {
13557 let src = "flwo F() { }";
13558 let result = Parser::new(lex(src)).parse_with_recovery();
13559 assert!(
13560 result
13561 .errors
13562 .iter()
13563 .any(|e| e.message.contains("Did you mean `flow`?")),
13564 "errors: {:?}",
13565 result.errors
13566 );
13567 }
13568}
13569
13570#[cfg(test)]
13573mod mutate_purge_where_tests {
13574 use super::*;
13575
13576 fn parse(src: &str) -> Program {
13577 let tokens = crate::lexer::Lexer::new(src, "<test>")
13578 .tokenize()
13579 .expect("lex");
13580 Parser::new(tokens).parse().expect("parse")
13581 }
13582
13583 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13584 for d in &prog.declarations {
13585 if let Declaration::Flow(f) = d {
13586 if f.name == flow {
13587 return f.body.first().expect("flow has at least one step");
13588 }
13589 }
13590 }
13591 panic!("flow `{flow}` not found");
13592 }
13593
13594 #[test]
13595 fn mutate_captures_its_where_clause() {
13596 let prog =
13599 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13600 match first_step(&prog, "F") {
13601 FlowStep::Mutate(m) => {
13602 assert_eq!(m.store_name, "accounts");
13603 assert_eq!(m.where_expr, "id = 1");
13604 }
13605 other => panic!("expected Mutate, got {other:?}"),
13606 }
13607 }
13608
13609 #[test]
13610 fn purge_captures_its_where_clause() {
13611 let prog =
13612 parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13613 match first_step(&prog, "F") {
13614 FlowStep::Purge(p) => {
13615 assert_eq!(p.store_name, "logs");
13616 assert_eq!(p.where_expr, "ts < 100");
13617 }
13618 other => panic!("expected Purge, got {other:?}"),
13619 }
13620 }
13621
13622 #[test]
13623 fn mutate_without_a_where_block_is_a_whole_store_op() {
13624 let prog = parse("flow F() -> Unit { mutate accounts }");
13627 match first_step(&prog, "F") {
13628 FlowStep::Mutate(m) => {
13629 assert_eq!(m.store_name, "accounts");
13630 assert_eq!(m.where_expr, "");
13631 }
13632 other => panic!("expected Mutate, got {other:?}"),
13633 }
13634 }
13635}
13636
13637#[cfg(test)]
13640mod persist_fields_tests {
13641 use super::*;
13642
13643 fn parse(src: &str) -> Program {
13644 let tokens = crate::lexer::Lexer::new(src, "<test>")
13645 .tokenize()
13646 .expect("lex");
13647 Parser::new(tokens).parse().expect("parse")
13648 }
13649
13650 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13651 for d in &prog.declarations {
13652 if let Declaration::Flow(f) = d {
13653 if f.name == flow {
13654 return f.body.first().expect("flow has at least one step");
13655 }
13656 }
13657 }
13658 panic!("flow `{flow}` not found");
13659 }
13660
13661 #[test]
13662 fn persist_captures_its_field_block() {
13663 let prog = parse(
13667 "flow F() -> Unit { persist into chat_history { \
13668 session_id: \"${session_id}\" sender: \"user\" \
13669 content: \"${message}\" } }",
13670 );
13671 match first_step(&prog, "F") {
13672 FlowStep::Persist(p) => {
13673 assert_eq!(p.store_name, "chat_history");
13674 assert_eq!(
13675 p.fields,
13676 vec![
13677 ("session_id".to_string(), "${session_id}".to_string()),
13678 ("sender".to_string(), "user".to_string()),
13679 ("content".to_string(), "${message}".to_string()),
13680 ]
13681 );
13682 }
13683 other => panic!("expected Persist, got {other:?}"),
13684 }
13685 }
13686
13687 #[test]
13688 fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13689 let prog = parse("flow F() -> Unit { persist events }");
13692 match first_step(&prog, "F") {
13693 FlowStep::Persist(p) => {
13694 assert_eq!(p.store_name, "events");
13695 assert!(p.fields.is_empty());
13696 }
13697 other => panic!("expected Persist, got {other:?}"),
13698 }
13699 }
13700
13701 #[test]
13702 fn persist_accepts_the_optional_into_connector() {
13703 let with =
13706 parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13707 let without =
13708 parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13709 for prog in [&with, &without] {
13710 match first_step(prog, "F") {
13711 FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13712 other => panic!("expected Persist, got {other:?}"),
13713 }
13714 }
13715 }
13716
13717 #[test]
13718 fn persist_into_without_a_block_resolves_the_store_name() {
13719 let prog = parse("flow F() -> Unit { persist into events }");
13722 match first_step(&prog, "F") {
13723 FlowStep::Persist(p) => {
13724 assert_eq!(p.store_name, "events");
13725 assert!(p.fields.is_empty());
13726 }
13727 other => panic!("expected Persist, got {other:?}"),
13728 }
13729 }
13730
13731 #[test]
13732 fn persist_fields_lower_into_the_ir() {
13733 let prog = parse(
13736 "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13737 );
13738 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13739 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13740 match flow.steps.first().expect("one step") {
13741 crate::ir_nodes::IRFlowNode::Persist(p) => {
13742 assert_eq!(p.store_name, "chat");
13743 assert_eq!(
13744 p.fields,
13745 vec![("content".to_string(), "${msg}".to_string())]
13746 );
13747 }
13748 other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13749 }
13750 }
13751}
13752
13753#[cfg(test)]
13756mod mutate_fields_tests {
13757 use super::*;
13758
13759 fn parse(src: &str) -> Program {
13760 let tokens = crate::lexer::Lexer::new(src, "<test>")
13761 .tokenize()
13762 .expect("lex");
13763 Parser::new(tokens).parse().expect("parse")
13764 }
13765
13766 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13767 for d in &prog.declarations {
13768 if let Declaration::Flow(f) = d {
13769 if f.name == flow {
13770 return f.body.first().expect("flow has at least one step");
13771 }
13772 }
13773 }
13774 panic!("flow `{flow}` not found");
13775 }
13776
13777 #[test]
13778 fn mutate_captures_its_set_field_block() {
13779 let prog = parse(
13783 "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13784 balance: \"${new_balance}\" status: \"active\" } }",
13785 );
13786 match first_step(&prog, "F") {
13787 FlowStep::Mutate(m) => {
13788 assert_eq!(m.store_name, "accounts");
13789 assert_eq!(m.where_expr, "id = ${id}");
13790 assert_eq!(
13791 m.fields,
13792 vec![
13793 ("balance".to_string(), "${new_balance}".to_string()),
13794 ("status".to_string(), "active".to_string()),
13795 ]
13796 );
13797 }
13798 other => panic!("expected Mutate, got {other:?}"),
13799 }
13800 }
13801
13802 #[test]
13803 fn mutate_where_only_block_has_no_set_fields() {
13804 let prog =
13807 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13808 match first_step(&prog, "F") {
13809 FlowStep::Mutate(m) => {
13810 assert_eq!(m.where_expr, "id = 1");
13811 assert!(m.fields.is_empty());
13812 }
13813 other => panic!("expected Mutate, got {other:?}"),
13814 }
13815 }
13816
13817 #[test]
13818 fn mutate_with_no_block_is_a_whole_store_op() {
13819 let prog = parse("flow F() -> Unit { mutate accounts }");
13822 match first_step(&prog, "F") {
13823 FlowStep::Mutate(m) => {
13824 assert_eq!(m.store_name, "accounts");
13825 assert_eq!(m.where_expr, "");
13826 assert!(m.fields.is_empty());
13827 }
13828 other => panic!("expected Mutate, got {other:?}"),
13829 }
13830 }
13831
13832 #[test]
13833 fn mutate_fields_lower_into_the_ir() {
13834 let prog = parse(
13835 "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
13836 );
13837 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13838 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13839 match flow.steps.first().expect("one step") {
13840 crate::ir_nodes::IRFlowNode::Mutate(m) => {
13841 assert_eq!(m.where_expr, "id = 1");
13842 assert_eq!(
13843 m.fields,
13844 vec![("v".to_string(), "${x}".to_string())]
13845 );
13846 }
13847 other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
13848 }
13849 }
13850}
13851