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 name,
2586 provider: String::new(),
2587 max_results: None,
2588 filter_expr: String::new(),
2589 timeout: String::new(),
2590 runtime: String::new(),
2591 resource_ref: String::new(),
2592 sandbox: None,
2593 effects: None,
2594 parameters: Vec::new(),
2595 output_type: None,
2596 requires: Vec::new(),
2597 secret: String::new(),
2598 secret_partition: String::new(),
2599 target: None,
2600 risk: None,
2601 argv: Vec::new(),
2602 cache: String::new(),
2603 scrape: None,
2604 loc,
2605 leading_trivia: Vec::new(),
2606 trailing_trivia: Vec::new(),
2607 };
2608
2609 let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2616
2617 while !self.check(TokenType::RBrace) {
2618 let field_tok = self.current().clone();
2619 let field_name = field_tok.value.clone();
2620 self.advance();
2621 self.consume(TokenType::Colon)?;
2622
2623 match field_name.as_str() {
2624 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2625 "max_results" => {
2626 node.max_results = Some(
2627 self.consume(TokenType::Integer)?
2628 .value
2629 .parse::<i64>()
2630 .unwrap_or(0),
2631 )
2632 }
2633 "filter" => node.filter_expr = self.parse_filter_expression()?,
2634 "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2635 "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2636 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2640 "sandbox" => node.sandbox = Some(self.parse_bool()?),
2641 "effects" => node.effects = Some(self.parse_effect_row()?),
2642 "parameters" => node.parameters = self.parse_tool_param_schema()?,
2644 "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2645 "requires" => {
2651 let bracket_tok = self.current().clone();
2652 let items = self.parse_bracketed_dot_identifiers()?;
2653 for slug in &items {
2654 if !is_valid_capability_slug(slug) {
2655 return Err(ParseError {
2656 message: format!(
2657 "Invalid capability slug '{slug}' in tool '{}' \
2658 `requires:`. Scope slugs must match \
2659 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2660 grammar as `credential.grants`. Examples: \
2661 `w_organization_social`, `video.publish`.",
2662 node.name
2663 ),
2664 line: bracket_tok.line,
2665 column: bracket_tok.column,
2666 ..Default::default()
2667 });
2668 }
2669 }
2670 node.requires = items;
2671 }
2672 "secret" => node.secret = self.parse_dotted_identifier()?,
2676 "secret_partition" => {
2682 node.secret_partition = self.consume_any_ident_or_kw()?.value
2683 }
2684 "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2686 "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2687 "argv" => node.argv = self.parse_bracketed_strings()?,
2691 "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2694 "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2697 _ => {
2698 unknown_fields.push((field_name, field_tok.line, field_tok.column));
2699 self.skip_value();
2700 }
2701 }
2702 }
2703 self.consume(TokenType::RBrace)?;
2704
2705 if node.target.is_some() || node.scrape.is_some() {
2713 if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2714 let (surface, valid) = if node.target.is_some() {
2715 (
2716 "technician tool",
2717 "provider, parameters, output_type, timeout, effects, target, risk, argv",
2718 )
2719 } else {
2720 (
2721 "web-acquisition tool",
2722 "provider, parameters, output_type, timeout, effects, secret, \
2723 secret_partition, cache, scrape",
2724 )
2725 };
2726 return Err(ParseError {
2727 message: format!(
2728 "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2729 strict field checking; valid fields: {valid}",
2730 node.name
2731 ),
2732 line,
2733 column,
2734 ..Default::default()
2735 });
2736 }
2737 }
2738 Ok(node)
2739 }
2740
2741 fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2746 let open = self.consume(TokenType::LBrace)?;
2747 let loc = self.loc_of(&open);
2748 let mut spec = crate::ast::ScrapeSpec {
2749 loc,
2750 ..Default::default()
2751 };
2752 while !self.check(TokenType::RBrace) {
2753 let field_tok = self.current().clone();
2754 let field_name = field_tok.value.clone();
2755 self.advance();
2756 self.consume(TokenType::Colon)?;
2757 match field_name.as_str() {
2758 "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2759 "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2760 "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2761 "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2762 "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2763 "extract" => spec.extract = self.parse_bracketed_strings()?,
2764 "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2765 "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2766 "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2767 "max_depth" => {
2768 spec.max_depth =
2769 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2770 }
2771 "max_pages" => {
2772 spec.max_pages =
2773 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2774 }
2775 "concurrency" => {
2776 spec.concurrency =
2777 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2778 }
2779 "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2780 "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2781 other => {
2782 return Err(self.error(&format!(
2783 "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2784 closed catalog; valid fields: engine, impersonate, \
2785 render_wait, proxy, respect_robots, extract, adaptive, \
2786 similarity_floor, follow, max_depth, max_pages, concurrency, \
2787 politeness, checkpoint"
2788 )));
2789 }
2790 }
2791 }
2792 self.consume(TokenType::RBrace)?;
2793 Ok(spec)
2794 }
2795
2796 fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2802 self.consume(TokenType::LBrace)?;
2803 let mut params = Vec::new();
2804 while !self.check(TokenType::RBrace) {
2805 let name = self.consume_any_ident_or_kw()?;
2809 let ploc = self.loc_of(&name);
2810 self.consume(TokenType::Colon)?;
2811 let type_expr = self.parse_type_expr()?;
2812 params.push(Parameter {
2813 name: name.value,
2814 type_expr,
2815 loc: ploc,
2816 });
2817 if self.check(TokenType::Comma) {
2818 self.advance();
2819 } else {
2820 break;
2821 }
2822 }
2823 self.consume(TokenType::RBrace)?;
2824 Ok(params)
2825 }
2826
2827 fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2828 let name = self.consume_any_ident_or_kw()?.value;
2829 if self.check(TokenType::LParen) {
2830 self.advance();
2831 let mut parts = vec![name, "(".to_string()];
2832 while !self.check(TokenType::RParen) {
2833 parts.push(self.advance().value.clone());
2834 }
2835 self.consume(TokenType::RParen)?;
2836 parts.push(")".to_string());
2837 Ok(parts.join(""))
2838 } else {
2839 Ok(name)
2840 }
2841 }
2842
2843 fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2844 let tok = self.consume(TokenType::Lt)?;
2845 let loc = self.loc_of(&tok);
2846 let mut effects = Vec::new();
2847 let mut epistemic_level = String::new();
2848
2849 while !self.check(TokenType::Gt) {
2850 let name = self.consume_any_ident_or_kw()?.value;
2851 if self.check(TokenType::Colon) {
2852 self.advance();
2853 let level = self.parse_qualifier_value()?;
2870 if name == "epistemic" {
2871 epistemic_level = level;
2872 } else {
2873 effects.push(format!("{name}:{level}"));
2874 }
2875 } else {
2876 effects.push(name);
2877 }
2878 if self.check(TokenType::Comma) {
2879 self.advance();
2880 }
2881 }
2882 self.consume(TokenType::Gt)?;
2883
2884 Ok(EffectRow {
2885 effects,
2886 epistemic_level,
2887 loc,
2888 })
2889 }
2890
2891 fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2900 let mut buf = self.consume_dotted_slug_segment()?;
2901 loop {
2902 let sep = if self.check(TokenType::Dot) {
2903 '.'
2904 } else if self.check(TokenType::Colon) {
2905 ':'
2906 } else {
2907 break;
2908 };
2909 self.advance();
2910 let part = self.consume_dotted_slug_segment()?;
2911 buf.push(sep);
2912 buf.push_str(&part);
2913 }
2914 Ok(buf)
2915 }
2916
2917 fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2929 let first = self.consume_any_ident_or_kw()?;
2930 let mut buf = first.value.clone();
2931 let mut next_line = first.line;
2932 let mut next_col = first.column + first.value.chars().count() as u32;
2933 loop {
2934 let cur = self.current();
2935 let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2936 if !is_segment_token {
2937 break;
2938 }
2939 if cur.line != next_line || cur.column != next_col {
2940 break;
2941 }
2942 buf.push_str(&cur.value);
2943 next_col = cur.column + cur.value.chars().count() as u32;
2944 next_line = cur.line;
2945 self.pos += 1;
2946 }
2947 Ok(buf)
2948 }
2949
2950 fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2953 let tok = self.consume(TokenType::Type)?;
2954 let loc = self.loc_of(&tok);
2955 let name = self.consume(TokenType::Identifier)?.value;
2956
2957 let mut node = TypeDefinition {
2958 name,
2959 fields: Vec::new(),
2960 range_constraint: None,
2961 where_clause: None,
2962 compliance: Vec::new(),
2963 loc: loc.clone(),
2964 leading_trivia: Vec::new(),
2965 trailing_trivia: Vec::new(),
2966 };
2967
2968 if self.check(TokenType::LParen) {
2970 self.advance();
2971 let min_val = self.consume_number()?;
2972 self.consume(TokenType::DotDot)?;
2973 let max_val = self.consume_number()?;
2974 self.consume(TokenType::RParen)?;
2975 node.range_constraint = Some(RangeConstraint {
2976 min_value: min_val,
2977 max_value: max_val,
2978 loc: loc.clone(),
2979 });
2980 }
2981
2982 if self.check(TokenType::Where) {
2984 self.advance();
2985 let mut expr_parts = Vec::new();
2986 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2987 if self.check(TokenType::Eof) {
2988 break;
2989 }
2990 expr_parts.push(self.advance().value.clone());
2991 }
2992 node.where_clause = Some(WhereClause {
2993 expression: expr_parts.join(" "),
2994 loc: loc.clone(),
2995 });
2996 }
2997
2998 if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3001 self.advance();
3002 node.compliance = self.parse_bracketed_identifiers()?;
3003 }
3004
3005 if self.check(TokenType::LBrace) {
3007 self.advance();
3008 while !self.check(TokenType::RBrace) {
3009 let field_name = self.consume(TokenType::Identifier)?;
3010 let field_loc = self.loc_of(&field_name);
3011 self.consume(TokenType::Colon)?;
3012 let type_expr = self.parse_type_expr()?;
3013 node.fields.push(TypeField {
3014 name: field_name.value,
3015 type_expr,
3016 loc: field_loc,
3017 });
3018 if self.check(TokenType::Comma) {
3019 self.advance();
3020 }
3021 }
3022 self.consume(TokenType::RBrace)?;
3023 }
3024
3025 Ok(node)
3026 }
3027
3028 fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3029 if self.check(TokenType::LBracket) {
3035 let open = self.current().clone();
3036 self.advance();
3037 let inner = self.parse_type_expr()?;
3038 self.consume(TokenType::RBracket)?;
3039 let mut optional = false;
3040 if self.check(TokenType::Question) {
3041 self.advance();
3042 optional = true;
3043 }
3044 return Ok(TypeExpr {
3045 name: "List".to_string(),
3046 generic_param: if inner.generic_param.is_empty() {
3047 inner.name
3048 } else {
3049 format!("{}<{}>", inner.name, inner.generic_param)
3050 },
3051 optional,
3052 loc: self.loc_of(&open),
3053 });
3054 }
3055 let name_tok = self.consume(TokenType::Identifier)?;
3056 let loc = self.loc_of(&name_tok);
3057 let mut generic_param = String::new();
3058 let mut optional = false;
3059
3060 if self.check(TokenType::Lt) {
3061 self.advance();
3062 let inner = self.parse_type_expr()?;
3072 generic_param = if inner.generic_param.is_empty() {
3073 inner.name
3074 } else {
3075 format!("{}<{}>", inner.name, inner.generic_param)
3076 };
3077 self.consume(TokenType::Gt)?;
3078 }
3079 if self.check(TokenType::LBracket) {
3083 self.advance();
3084 if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3085 generic_param = self.advance().value.clone();
3086 } else {
3087 let inner = self.parse_type_expr()?;
3088 generic_param = if inner.generic_param.is_empty() {
3089 inner.name
3090 } else {
3091 format!("{}[{}]", inner.name, inner.generic_param)
3092 };
3093 }
3094 self.consume(TokenType::RBracket)?;
3095 }
3096 if self.check(TokenType::Question) {
3097 self.advance();
3098 optional = true;
3099 }
3100
3101 Ok(TypeExpr {
3102 name: name_tok.value,
3103 generic_param,
3104 optional,
3105 loc,
3106 })
3107 }
3108
3109 fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3134 let expr = self.parse_type_expr()?;
3135 let mut s = expr.name;
3136 if !expr.generic_param.is_empty() {
3137 s.push('<');
3138 s.push_str(&expr.generic_param);
3139 s.push('>');
3140 }
3141 if expr.optional {
3142 s.push('?');
3143 }
3144 Ok(s)
3145 }
3146
3147 fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3150 let tok = self.consume(TokenType::Flow)?;
3151 let loc = self.loc_of(&tok);
3152 let name = self.consume(TokenType::Identifier)?.value;
3153
3154 self.consume(TokenType::LParen)?;
3155 let mut parameters = Vec::new();
3156 if !self.check(TokenType::RParen) {
3157 parameters = self.parse_param_list()?;
3158 }
3159 self.consume(TokenType::RParen)?;
3160
3161 let mut return_type = None;
3162 if self.check(TokenType::Arrow) {
3163 self.advance();
3164 return_type = Some(self.parse_type_expr()?);
3165 }
3166
3167 self.consume(TokenType::LBrace)?;
3168 let mut body = Vec::new();
3169 while !self.check(TokenType::RBrace) {
3170 body.push(self.parse_flow_step()?);
3171 }
3172 self.consume(TokenType::RBrace)?;
3173
3174 Ok(FlowDefinition {
3175 name,
3176 parameters,
3177 return_type,
3178 body,
3179 loc,
3180 leading_trivia: Vec::new(),
3181 trailing_trivia: Vec::new(),
3182 })
3183 }
3184
3185 fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3198 let tok = self.consume(TokenType::Effect)?;
3199 let loc = self.loc_of(&tok);
3200 let name = self.consume_any_ident_or_kw()?.value;
3201 self.consume(TokenType::LBrace)?;
3202
3203 let mut operations: Vec<EffectOperation> = Vec::new();
3204 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3205 let op_tok = self.current().clone();
3206 let op_name = self.consume_any_ident_or_kw()?.value;
3207
3208 self.consume(TokenType::LParen)?;
3209 let parameters = if self.check(TokenType::RParen) {
3210 Vec::new()
3211 } else {
3212 self.parse_param_list()?
3213 };
3214 self.consume(TokenType::RParen)?;
3215
3216 let mut return_type = String::new();
3219 if self.check(TokenType::Arrow) {
3220 self.advance();
3221 return_type = self.parse_type_expr()?.name;
3222 }
3223
3224 if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3229 return Err(ParseError {
3230 message: format!(
3231 "effect `{name}` declares operation `{op_name}` twice (first at \
3232 line {}); handler dispatch is by operation NAME, so a second \
3233 declaration would silently shadow the first",
3234 prior.loc.line
3235 ),
3236 line: op_tok.line,
3237 column: op_tok.column,
3238 ..Default::default()
3239 });
3240 }
3241
3242 operations.push(EffectOperation {
3243 name: op_name,
3244 parameters,
3245 return_type,
3246 loc: self.loc_of(&op_tok),
3247 });
3248 }
3249 self.consume(TokenType::RBrace)?;
3250
3251 Ok(EffectDefinition {
3252 name,
3253 operations,
3254 loc,
3255 leading_trivia: Vec::new(),
3256 trailing_trivia: Vec::new(),
3257 })
3258 }
3259
3260 fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3270 let tok = self.consume(TokenType::Handle)?;
3271 let loc = self.loc_of(&tok);
3272
3273 let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3275 while self.check(TokenType::Comma) {
3276 self.advance();
3277 effect_names.push(self.consume_any_ident_or_kw()?.value);
3278 }
3279
3280 self.consume(TokenType::LBrace)?;
3281 let mut clauses: Vec<HandlerClause> = Vec::new();
3282 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3283 let clause_tok = self.current().clone();
3284 let operation_name = self.consume_any_ident_or_kw()?.value;
3285
3286 self.consume(TokenType::LParen)?;
3290 let mut parameter_names = Vec::new();
3291 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3292 parameter_names.push(self.consume_any_ident_or_kw()?.value);
3293 if self.check(TokenType::Comma) {
3294 self.advance();
3295 }
3296 }
3297 self.consume(TokenType::RParen)?;
3298 self.consume(TokenType::Arrow)?;
3299 self.consume(TokenType::LBrace)?;
3300
3301 let mut body = Vec::new();
3302 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3303 body.push(self.parse_flow_step()?);
3304 }
3305 self.consume(TokenType::RBrace)?;
3306
3307 if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3308 return Err(ParseError {
3309 message: format!(
3310 "handler declares clause `{operation_name}` twice (first at line {}); \
3311 dispatch finds a clause by operation NAME and would always run the \
3312 first, leaving the second dead",
3313 prior.loc.line
3314 ),
3315 line: clause_tok.line,
3316 column: clause_tok.column,
3317 ..Default::default()
3318 });
3319 }
3320
3321 clauses.push(HandlerClause {
3322 operation_name,
3323 parameter_names,
3324 body,
3325 loc: self.loc_of(&clause_tok),
3326 });
3327 }
3328 self.consume(TokenType::RBrace)?;
3329
3330 let in_tok = self.current().clone();
3335 if !self.check(TokenType::In) {
3336 return Err(ParseError {
3337 message: format!(
3338 "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3339 DELIMITED (the design plan D3). Without the `in` block the frame has no \
3340 extent, so no `perform` could ever reach these clauses (got '{}')",
3341 effect_names.join(", "),
3342 in_tok.value
3343 ),
3344 line: in_tok.line,
3345 column: in_tok.column,
3346 ..Default::default()
3347 });
3348 }
3349 self.advance();
3350 self.consume(TokenType::LBrace)?;
3351 let mut body = Vec::new();
3352 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3353 body.push(self.parse_flow_step()?);
3354 }
3355 self.consume(TokenType::RBrace)?;
3356
3357 Ok(HandleBlock {
3358 effect_names,
3359 clauses,
3360 body,
3361 loc,
3362 })
3363 }
3364
3365 fn parse_effect_op_ref(
3374 &mut self,
3375 ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3376 let first = self.consume_any_ident_or_kw()?.value;
3377 let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3378 self.advance();
3379 (Some(first), self.consume_any_ident_or_kw()?.value)
3380 } else {
3381 (None, first)
3382 };
3383
3384 self.consume(TokenType::LParen)?;
3385 let mut arguments = Vec::new();
3386 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3387 arguments.push(self.parse_subject()?);
3390 if self.check(TokenType::Comma) {
3391 self.advance();
3392 }
3393 }
3394 self.consume(TokenType::RParen)?;
3395 Ok((effect_name, operation_name, arguments))
3396 }
3397
3398 fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3400 let tok = self.consume(TokenType::Perform)?;
3401 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3402 Ok(PerformStep {
3403 effect_name,
3404 operation_name,
3405 arguments,
3406 loc: self.loc_of(&tok),
3407 })
3408 }
3409
3410 fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3412 let tok = self.consume(TokenType::Forward)?;
3413 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3414 Ok(ForwardStep {
3415 effect_name,
3416 operation_name,
3417 arguments,
3418 loc: self.loc_of(&tok),
3419 })
3420 }
3421
3422 fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3424 self.consume(TokenType::LParen)?;
3425 let value = if self.check(TokenType::RParen) {
3426 String::new()
3427 } else {
3428 self.parse_subject()?
3429 };
3430 self.consume(TokenType::RParen)?;
3431 Ok(value)
3432 }
3433
3434 fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3435 let mut params = Vec::new();
3436
3437 let name = self.consume(TokenType::Identifier)?;
3438 let ploc = self.loc_of(&name);
3439 self.consume(TokenType::Colon)?;
3440 let type_expr = self.parse_type_expr()?;
3441 params.push(Parameter {
3442 name: name.value,
3443 type_expr,
3444 loc: ploc,
3445 });
3446
3447 while self.check(TokenType::Comma) {
3448 self.advance();
3449 let name = self.consume(TokenType::Identifier)?;
3450 let ploc = self.loc_of(&name);
3451 self.consume(TokenType::Colon)?;
3452 let type_expr = self.parse_type_expr()?;
3453 params.push(Parameter {
3454 name: name.value,
3455 type_expr,
3456 loc: ploc,
3457 });
3458 }
3459 Ok(params)
3460 }
3461
3462 fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3465 let tok = self.current().clone();
3466
3467 match tok.ttype {
3468 TokenType::Know | TokenType::Believe | TokenType::Speculate
3474 if self
3475 .tokens
3476 .get(self.pos + 1)
3477 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3478 {
3479 let block = self.parse_epistemic_block()?;
3480 self.hoisted.push(Declaration::Epistemic(block));
3481 self.parse_flow_step()
3482 }
3483 TokenType::Doubt
3484 if self
3485 .tokens
3486 .get(self.pos + 1)
3487 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3488 {
3489 let block = self.parse_epistemic_block()?;
3490 self.hoisted.push(Declaration::Epistemic(block));
3491 self.parse_flow_step()
3492 }
3493 TokenType::Step => self.parse_step().map(FlowStep::Step),
3494 TokenType::If => self.parse_if().map(FlowStep::If),
3495 TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3496 TokenType::Let => self.parse_let().map(FlowStep::Let),
3497 TokenType::Return => self.parse_return().map(FlowStep::Return),
3498 TokenType::Break => self.parse_break().map(FlowStep::Break),
3499 TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3500 TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3501
3502 TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3504 TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3508 TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3509 TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3510 TokenType::Weave => self.parse_weave_step(),
3511 TokenType::Use => self.parse_use_step(),
3512 TokenType::Remember => self.parse_remember_step(),
3513 TokenType::Recall => self.parse_recall_step(),
3514 TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3515 TokenType::Hibernate => self.parse_hibernate_step(),
3516 TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3517 TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3518 TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3519 TokenType::Focus => self.parse_focus_step(),
3520 TokenType::Grad => self.parse_grad_step(),
3521 TokenType::Associate => self.parse_associate_step(),
3522 TokenType::Aggregate => self.parse_aggregate_step(),
3523 TokenType::Explore => self.parse_explore_step(),
3524 TokenType::Ingest => self.parse_ingest_step(),
3525 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 })),
3526 TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3532 TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3540 TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3541 TokenType::Resume => {
3542 let tok = self.consume(TokenType::Resume)?;
3543 let value_expr = self.parse_discharge_value()?;
3544 Ok(FlowStep::Resume(ResumeStep {
3545 value_expr,
3546 loc: self.loc_of(&tok),
3547 }))
3548 }
3549 TokenType::Abort => {
3550 let tok = self.consume(TokenType::Abort)?;
3551 let value_expr = self.parse_discharge_value()?;
3552 Ok(FlowStep::Abort(AbortStep {
3553 value_expr,
3554 loc: self.loc_of(&tok),
3555 }))
3556 }
3557 TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3558 TokenType::Navigate => self.parse_navigate_step(),
3559 TokenType::Drill => self.parse_drill_step(),
3560 TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3561 TokenType::Corroborate => self.parse_corroborate_step(),
3562 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 })),
3563 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 })),
3564 TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3569 TokenType::Listen => self.parse_listen_step(),
3570 TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3571 TokenType::Emit => self.parse_emit_step(),
3573 TokenType::Mint => self.parse_mint_step(),
3575 TokenType::Rotate => self.parse_rotate_step(),
3578 TokenType::Publish => self.parse_publish_step(),
3579 TokenType::Discover => self.parse_discover_step(),
3580 TokenType::Persist => self.parse_persist_step(),
3581 TokenType::Retrieve => self.parse_retrieve_step(),
3582 TokenType::Mutate => self.parse_mutate_step(),
3583 TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3584 TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3585 TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3587 TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3589 TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3591 TokenType::Run => self.parse_run().map(FlowStep::Run),
3595
3596 _ => {
3597 let hint = crate::smart_suggest::suggest_for(
3601 &tok.value,
3602 crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3603 );
3604 let base = format!(
3605 "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3606 tok.value
3607 );
3608 let message = if hint.is_empty() {
3609 base
3610 } else {
3611 format!("{base}. {hint}")
3612 };
3613 Err(ParseError {
3614 message,
3615 line: tok.line,
3616 column: tok.column,
3617 ..Default::default()
3618 })
3619 }
3620 }
3621 }
3622
3623 fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3626 let tok = self.consume(TokenType::Step)?;
3627 let loc = self.loc_of(&tok);
3628 let name = self.consume(TokenType::Identifier)?.value;
3629
3630 let mut persona_ref = String::new();
3631 if self.check(TokenType::Use) {
3632 self.advance();
3633 persona_ref = self.consume_any_ident_or_kw()?.value;
3634 }
3635
3636 self.consume(TokenType::LBrace)?;
3637
3638 let mut node = StepNode {
3639 name,
3640 persona_ref,
3641 given: String::new(),
3642 ask: String::new(),
3643 output_type: String::new(),
3644 confidence_floor: None,
3645 navigate_ref: String::new(),
3646 apply_ref: String::new(),
3647 requires_context: None,
3648 now_tz: None,
3649 guards: Vec::new(),
3650 pix_ops: Vec::new(),
3651 stream: None,
3652 performs: Vec::new(),
3653 loc,
3654 };
3655
3656 self.parse_step_body_into(&mut node)?;
3657 self.consume(TokenType::RBrace)?;
3658 Ok(node)
3659 }
3660
3661 fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3674 while !self.check(TokenType::RBrace) {
3675 let inner = self.current().clone();
3676
3677 match inner.ttype {
3678 TokenType::Given => {
3679 self.advance();
3680 self.consume(TokenType::Colon)?;
3681 node.given = self.parse_expression_string()?;
3682 }
3683 TokenType::Ask => {
3684 self.advance();
3685 self.consume(TokenType::Colon)?;
3686 node.ask = self.consume(TokenType::StringLit)?.value;
3687 }
3688 TokenType::Output => {
3689 self.advance();
3699 self.consume(TokenType::Colon)?;
3700 node.output_type = self.parse_output_type_string()?;
3701 }
3702 TokenType::Navigate
3709 if self
3710 .tokens
3711 .get(self.pos + 1)
3712 .is_some_and(|t| t.ttype != TokenType::Colon) =>
3713 {
3714 let op = self.parse_navigate_step()?;
3715 node.pix_ops.push(op);
3716 }
3717 TokenType::Drill => {
3718 let op = self.parse_drill_step()?;
3719 node.pix_ops.push(op);
3720 }
3721 TokenType::Trail => {
3722 let op = self
3723 .parse_flow_step_simple("trail")
3724 .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3725 node.pix_ops.push(op);
3726 }
3727 TokenType::Validate => {
3732 let tok = self.current().clone();
3733 self.advance();
3734 let target = self.parse_subject()?;
3736 let mut rule = String::new();
3737 if self.current().value == "against" {
3738 self.advance();
3739 self.consume(TokenType::Colon)?;
3740 rule = self.consume_any_ident_or_kw()?.value.clone();
3741 }
3742 node.pix_ops.push(FlowStep::Validate(ValidateStep {
3743 target,
3744 rule,
3745 guard: None,
3746 loc: Loc { line: tok.line, column: tok.column },
3747 }));
3748 }
3749 TokenType::If => {
3761 let tok = self.current().clone();
3762 self.advance();
3763
3764 let metric = self.consume_any_ident_or_kw()?;
3765 if metric.value != "confidence" {
3766 return Err(ParseError {
3767 message: format!(
3768 "step-body `if` is the confidence guard — `if confidence < \
3769 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3770 is its only metric (the CSR the preceding `validate … \
3771 against:` computes). Got '{}'. General branching belongs at \
3772 flow level: `if <cond> {{ … }}`.",
3773 metric.value
3774 ),
3775 line: metric.line,
3776 column: metric.column,
3777 ..Default::default()
3778 });
3779 }
3780
3781 let op = self.current().clone();
3782 if op.ttype != TokenType::Lt {
3783 return Err(ParseError {
3784 message: format!(
3785 "a confidence guard declares a FLOOR: `if confidence < \
3786 <threshold>`. `<` is the only comparison — the guard fires on \
3787 DEFICIENCY, and an inverted form would refine the outputs \
3788 that already conform. Got '{}'.",
3789 op.value
3790 ),
3791 line: op.line,
3792 column: op.column,
3793 ..Default::default()
3794 });
3795 }
3796 self.advance();
3797 let threshold = self.consume_number()?;
3798
3799 self.consume(TokenType::Arrow)?;
3800
3801 if !self.check(TokenType::Refine) {
3802 let bad = self.current().clone();
3803 return Err(ParseError {
3804 message: format!(
3805 "the guard's action catalog is CLOSED and `refine` is its only \
3806 member — the recovery the runtime actually performs (re-derive \
3807 the validated value with the violations as feedback, then \
3808 re-score). Got '{}'. An action name outside the catalog would \
3809 advertise a recovery nothing dispatches.",
3810 bad.value
3811 ),
3812 line: bad.line,
3813 column: bad.column,
3814 ..Default::default()
3815 });
3816 }
3817 self.advance();
3818 self.consume(TokenType::LParen)?;
3819 let key = self.consume_any_ident_or_kw()?;
3820 if key.value != "max_attempts" {
3821 return Err(ParseError {
3822 message: format!(
3823 "`refine` takes exactly `max_attempts: <n>` — the bound that \
3824 makes the recovery loop TERMINATE by construction. Got '{}'.",
3825 key.value
3826 ),
3827 line: key.line,
3828 column: key.column,
3829 ..Default::default()
3830 });
3831 }
3832 self.consume(TokenType::Colon)?;
3833 let attempts_tok = self.current().clone();
3834 if attempts_tok.ttype != TokenType::Integer {
3835 return Err(ParseError {
3836 message: format!(
3837 "`max_attempts:` must be a positive integer literal (got '{}')",
3838 attempts_tok.value
3839 ),
3840 line: attempts_tok.line,
3841 column: attempts_tok.column,
3842 ..Default::default()
3843 });
3844 }
3845 let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3846 message: format!("Invalid attempt count '{}'", attempts_tok.value),
3847 line: attempts_tok.line,
3848 column: attempts_tok.column,
3849 ..Default::default()
3850 })?;
3851 self.advance();
3852 self.consume(TokenType::RParen)?;
3853
3854 let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3861 FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3862 _ => None,
3863 });
3864 match attached {
3865 Some(v) => {
3866 if v.guard.is_some() {
3867 return Err(ParseError {
3868 message: "this validation already carries a confidence \
3869 guard; a second one would race the first over \
3870 the same score. One validation, one floor, one \
3871 recovery."
3872 .to_string(),
3873 line: tok.line,
3874 column: tok.column,
3875 ..Default::default()
3876 });
3877 }
3878 v.guard = Some(ConfidenceGuard {
3879 threshold,
3880 max_attempts,
3881 loc: Loc { line: tok.line, column: tok.column },
3882 });
3883 }
3884 None => {
3885 return Err(ParseError {
3886 message: "`if confidence` reads the CSR of a preceding \
3887 `validate … against: <Schema>` in this step body, \
3888 and none exists. A `validate` without `against:` \
3889 computes no score (there is no schema to score \
3890 with), so it cannot carry a guard either."
3891 .to_string(),
3892 line: tok.line,
3893 column: tok.column,
3894 ..Default::default()
3895 });
3896 }
3897 }
3898 }
3899 TokenType::Navigate => {
3900 self.advance();
3901 self.consume(TokenType::Colon)?;
3902 node.navigate_ref = self.parse_dotted_identifier()?;
3903 }
3904 TokenType::Identifier if inner.value == "confidence_floor" => {
3905 self.advance();
3906 self.consume(TokenType::Colon)?;
3907 node.confidence_floor = Some(self.consume_number()?);
3908 }
3909 TokenType::Identifier if inner.value == "apply" => {
3910 self.advance();
3911 self.consume(TokenType::Colon)?;
3912 node.apply_ref = self.consume_any_ident_or_kw()?.value;
3913 }
3914 TokenType::Identifier if inner.value == "requires_context" => {
3922 self.advance();
3923 self.consume(TokenType::Colon)?;
3924 let num = self.current().clone();
3925 let bad = |tok: &crate::tokens::Token| ParseError {
3926 message: format!(
3927 "`requires_context:` must be a positive integer token count \
3928 (got '{}')",
3929 tok.value
3930 ),
3931 line: tok.line,
3932 column: tok.column,
3933 ..Default::default()
3934 };
3935 if num.ttype != TokenType::Integer {
3936 return Err(bad(&num));
3937 }
3938 let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3939 self.advance();
3940 node.requires_context = Some(value);
3941 }
3942 TokenType::Identifier if inner.value == "now" => {
3947 self.advance();
3948 self.consume(TokenType::Colon)?;
3949 let tz = self.current().clone();
3950 if tz.ttype != TokenType::StringLit {
3951 return Err(ParseError {
3952 message: format!(
3953 "`now:` must be an IANA timezone string literal like \
3954 \"America/Bogota\" or \"UTC\" (got '{}')",
3955 tz.value
3956 ),
3957 line: tz.line,
3958 column: tz.column,
3959 ..Default::default()
3960 });
3961 }
3962 self.advance();
3963 node.now_tz = Some(tz.value);
3964 }
3965 TokenType::Use => {
3975 let tool = self
3976 .tokens
3977 .get(self.pos + 1)
3978 .map(|t| t.value.as_str())
3979 .filter(|v| !v.is_empty())
3980 .unwrap_or("<Tool>");
3981 return Err(ParseError {
3982 message: format!(
3983 "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
3984 would be silently dropped. To invoke a tool, either write the \
3985 flow-level step `use {tool} on <arg>` (outside this block), or bind \
3986 it inside this step with `apply: {tool}`. To attach a persona, put \
3987 it in the step header: `step <name> use <Persona> {{ … }}`."
3988 ),
3989 line: inner.line,
3990 column: inner.column,
3991 ..Default::default()
3992 });
3993 }
3994 TokenType::Mandate => {
4005 let g = self.parse_step_guard("mandate")?;
4006 node.guards.push(g);
4007 }
4008 TokenType::Shield => {
4009 let g = self.parse_step_guard("shield")?;
4010 node.guards.push(g);
4011 }
4012 TokenType::Ots => {
4013 let g = self.parse_step_guard("ots")?;
4014 node.guards.push(g);
4015 }
4016 TokenType::Lambda => {
4023 let g = self.parse_step_guard("lambda")?;
4024 node.guards.push(g);
4025 }
4026 TokenType::Probe
4034 if self
4035 .tokens
4036 .get(self.pos + 1)
4037 .is_some_and(|t| t.ttype != TokenType::Colon) =>
4038 {
4039 let tok = self.current().clone();
4040 self.advance();
4041 let target = self.parse_subject()?;
4044 let mut fields = Vec::new();
4045 if self.check(TokenType::For) {
4046 self.advance();
4047 self.consume(TokenType::LBracket)?;
4048 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4049 fields.push(self.consume_any_ident_or_kw()?.value.clone());
4050 if self.check(TokenType::Comma) {
4051 self.advance();
4052 }
4053 }
4054 self.consume(TokenType::RBracket)?;
4055 }
4056 node.pix_ops.push(FlowStep::Probe(ProbeStep {
4057 target,
4058 fields,
4059 loc: Loc { line: tok.line, column: tok.column },
4060 }));
4061 }
4062 TokenType::Identifier if inner.value == "use_tool" => {
4069 let tok = self.current().clone();
4070 self.advance();
4071 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4072 let args = if self.current().value == "with" {
4073 self.advance();
4074 let mut named: Vec<(String, String, String)> = Vec::new();
4075 loop {
4076 let k = self.consume_any_ident_or_kw()?.value.clone();
4077 self.consume(TokenType::Colon)?;
4078 let kind = if self.check(TokenType::StringLit) {
4082 "literal"
4083 } else {
4084 "reference"
4085 };
4086 let v = self.parse_expression_string()?;
4087 named.push((k, v, kind.to_string()));
4088 if self.check(TokenType::Comma) {
4089 self.advance();
4090 } else {
4091 break;
4092 }
4093 }
4094 UseArgs::Named(named)
4095 } else if self.current().value == "on" {
4096 self.advance();
4097 UseArgs::LegacyPositional(
4098 self.consume_any_ident_or_kw()?.value.clone(),
4099 )
4100 } else {
4101 UseArgs::LegacyPositional(String::new())
4102 };
4103 node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4104 tool_name,
4105 args,
4106 loc: Loc { line: tok.line, column: tok.column },
4107 }));
4108 }
4109 TokenType::Par => {
4111 let block = self.parse_par_block()?;
4112 node.pix_ops.push(FlowStep::Par(block));
4113 }
4114 TokenType::Reason
4136 if self
4137 .tokens
4138 .get(self.pos + 1)
4139 .is_some_and(|t| t.ttype == TokenType::Colon) =>
4140 {
4141 let tok = self.current().clone();
4142 self.advance();
4143 self.consume(TokenType::Colon)?;
4144 let mut r = ReasonStep {
4145 strategy: String::new(),
4146 target: String::new(),
4147 given: String::new(),
4148 ask: String::new(),
4149 depth: None,
4150 loc: self.loc_of(&tok),
4151 };
4152 if self.check(TokenType::StringLit) {
4153 r.ask = self.consume(TokenType::StringLit)?.value;
4154 } else {
4155 r.target = self.parse_dotted_identifier()?;
4156 }
4157 node.pix_ops.push(FlowStep::Reason(r));
4158 }
4159 TokenType::Reason => {
4160 let r = self.parse_reason_step()?;
4161 node.pix_ops.push(FlowStep::Reason(r));
4162 }
4163 TokenType::Weave => {
4172 let w = self.parse_weave_step()?;
4173 node.pix_ops.push(w);
4174 }
4175 TokenType::Identifier
4185 if self
4186 .tokens
4187 .get(self.pos + 1)
4188 .is_some_and(|t| t.ttype == TokenType::LParen) =>
4189 {
4190 let tok = self.current().clone();
4191 let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4192 self.consume(TokenType::LParen)?;
4193 let mut arguments = Vec::new();
4194 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4195 arguments.push(self.parse_subject()?);
4196 if self.check(TokenType::Comma) {
4197 self.advance();
4198 }
4199 }
4200 self.consume(TokenType::RParen)?;
4201 node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4202 agent_name,
4203 arguments,
4204 loc: self.loc_of(&tok),
4205 }));
4206 }
4207 TokenType::Retrieve => {
4217 let r = self.parse_retrieve_step()?;
4218 node.pix_ops.push(r);
4219 }
4220 TokenType::Stream => {
4238 let sb = self.parse_stream_block()?;
4239 if node.stream.is_some() {
4240 return Err(ParseError {
4241 message:
4242 "step declares two `stream` blocks; a step has one output stream, \
4243 and composing two has no defined meaning (which one is the \
4244 step's output?). Refused rather than silently keeping the last."
4245 .to_string(),
4246 line: inner.line,
4247 column: inner.column,
4248 ..Default::default()
4249 });
4250 }
4251 node.stream = Some(Box::new(sb));
4252 }
4253 TokenType::Perform => {
4270 let p = self.parse_perform_step()?;
4271 node.performs.push(p);
4272 }
4273 TokenType::Probe => {
4277 self.skip_flow_step_structural()?;
4278 }
4279 _ => {
4280 return Err(ParseError {
4281 message: format!(
4282 "Unexpected token in step body: '{}' — expected given, ask, \
4283 probe, reason, weave, stream, perform, output, confidence_floor, \
4284 navigate, apply, requires_context, now",
4285 inner.value
4286 ),
4287 line: inner.line,
4288 column: inner.column,
4289 ..Default::default()
4290 });
4291 }
4292 }
4293 }
4294 Ok(())
4295 }
4296
4297 fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4299 self.advance();
4301 while !self.check(TokenType::LBrace)
4303 && !self.check(TokenType::RBrace)
4304 && !self.check(TokenType::Eof)
4305 {
4306 let tt = &self.current().ttype;
4308 if matches!(
4309 tt,
4310 TokenType::Step
4311 | TokenType::Given
4312 | TokenType::Ask
4313 | TokenType::Output
4314 | TokenType::Navigate
4315 | TokenType::Use
4316 | TokenType::Probe
4317 | TokenType::Reason
4318 | TokenType::Weave
4319 | TokenType::Stream
4320 | TokenType::If
4321 | TokenType::For
4322 | TokenType::Let
4323 | TokenType::Return
4324 ) {
4325 return Ok(());
4326 }
4327 self.advance();
4328 }
4329 if self.check(TokenType::LBrace) {
4331 self.skip_braced_block()?;
4332 }
4333 Ok(())
4334 }
4335
4336 fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4339 let tok = self.consume(TokenType::Intent)?;
4340 let loc = self.loc_of(&tok);
4341 let name = self.consume(TokenType::Identifier)?.value;
4342 self.consume(TokenType::LBrace)?;
4343
4344 let mut node = IntentNode {
4345 name,
4346 given: String::new(),
4347 ask: String::new(),
4348 output_type: None,
4349 confidence_floor: None,
4350 loc,
4351 leading_trivia: Vec::new(),
4352 trailing_trivia: Vec::new(),
4353 };
4354
4355 while !self.check(TokenType::RBrace) {
4356 let field_name = self.current().value.clone();
4357 self.advance();
4358 self.consume(TokenType::Colon)?;
4359
4360 match field_name.as_str() {
4361 "given" => node.given = self.consume(TokenType::Identifier)?.value,
4362 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4363 "output" => node.output_type = Some(self.parse_type_expr()?),
4364 "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4365 _ => self.skip_value(),
4366 }
4367 }
4368 self.consume(TokenType::RBrace)?;
4369 Ok(node)
4370 }
4371
4372 fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4375 let tok = self.consume(TokenType::Run)?;
4376 let loc = self.loc_of(&tok);
4377 let flow_name = self.consume(TokenType::Identifier)?.value;
4378
4379 self.consume(TokenType::LParen)?;
4380 let mut arguments = Vec::new();
4381 if !self.check(TokenType::RParen) {
4382 arguments = self.parse_argument_list()?;
4383 }
4384 self.consume(TokenType::RParen)?;
4385
4386 let mut node = RunStatement {
4387 flow_name,
4388 arguments,
4389 persona: String::new(),
4390 context: String::new(),
4391 anchors: Vec::new(),
4392 on_failure: String::new(),
4393 on_failure_params: Vec::new(),
4394 output_to: String::new(),
4395 effort: String::new(),
4396 loc,
4397 leading_trivia: Vec::new(),
4398 trailing_trivia: Vec::new(),
4399 };
4400
4401 while self.check_run_modifier() {
4402 let mod_tok = self.current().clone();
4403 if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4405 self.advance();
4406 node.persona = self.consume(TokenType::Identifier)?.value;
4407 continue;
4408 }
4409 match mod_tok.ttype {
4410 TokenType::As => {
4411 self.advance();
4412 node.persona = self.consume(TokenType::Identifier)?.value;
4413 }
4414 TokenType::Within => {
4415 self.advance();
4416 node.context = self.consume(TokenType::Identifier)?.value;
4417 }
4418 TokenType::ConstrainedBy => {
4419 self.advance();
4420 node.anchors = self.parse_bracketed_identifiers()?;
4421 }
4422 TokenType::OnFailure => {
4423 self.advance();
4424 self.consume(TokenType::Colon)?;
4425 node.on_failure = self.consume_any_ident_or_kw()?.value;
4426 if self.check(TokenType::LParen) {
4428 self.advance();
4429 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4430 let key = self.consume_any_ident_or_kw()?.value;
4431 self.consume(TokenType::Colon)?;
4432 let val = self.consume_any_ident_or_kw()?.value;
4433 node.on_failure_params.push((key, val));
4434 if self.check(TokenType::Comma) {
4435 self.advance();
4436 }
4437 }
4438 if self.check(TokenType::RParen) {
4439 self.advance();
4440 }
4441 }
4442 }
4443 TokenType::OutputTo => {
4444 self.advance();
4445 self.consume(TokenType::Colon)?;
4446 node.output_to = self.consume(TokenType::StringLit)?.value;
4447 }
4448 TokenType::Effort => {
4449 self.advance();
4450 self.consume(TokenType::Colon)?;
4451 node.effort = self.consume_any_ident_or_kw()?.value;
4452 }
4453 _ => break,
4454 }
4455 }
4456
4457 Ok(node)
4458 }
4459
4460 fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4463 let tok = self.current().clone();
4464 let mode = match tok.ttype {
4465 TokenType::Know => "know",
4466 TokenType::Believe => "believe",
4467 TokenType::Speculate => "speculate",
4468 TokenType::Doubt => "doubt",
4469 _ => unreachable!(),
4470 };
4471 self.advance();
4472 let loc = self.loc_of(&tok);
4473
4474 self.consume(TokenType::LBrace)?;
4475 let mut body = Vec::new();
4476 while !self.check(TokenType::RBrace) {
4477 body.push(self.parse_declaration()?);
4478 }
4479 self.consume(TokenType::RBrace)?;
4480
4481 Ok(EpistemicBlock {
4482 mode: mode.to_string(),
4483 body,
4484 loc,
4485 leading_trivia: Vec::new(),
4486 trailing_trivia: Vec::new(),
4487 })
4488 }
4489
4490 fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4498 self.parse_expr_bp(0)
4499 }
4500
4501 fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4502 let mut lhs = match self.current().ttype {
4505 TokenType::Minus => {
4506 self.advance();
4507 Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4508 }
4509 TokenType::Not => {
4510 self.advance();
4511 Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4512 }
4513 _ => self.parse_postfix()?,
4514 };
4515 while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4517 if lbp < min_bp {
4518 break;
4519 }
4520 self.advance();
4521 let rhs = self.parse_expr_bp(lbp + 1)?;
4522 lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4523 }
4524 Ok(lhs)
4525 }
4526
4527 fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4530 Some(match t {
4531 TokenType::Or => (BinOp::Or, 1),
4532 TokenType::And => (BinOp::And, 2),
4533 TokenType::Eq => (BinOp::Eq, 3),
4534 TokenType::Neq => (BinOp::Ne, 3),
4535 TokenType::Lt => (BinOp::Lt, 3),
4536 TokenType::Lte => (BinOp::Le, 3),
4537 TokenType::Gt => (BinOp::Gt, 3),
4538 TokenType::Gte => (BinOp::Ge, 3),
4539 TokenType::Plus => (BinOp::Add, 4),
4540 TokenType::Minus => (BinOp::Sub, 4),
4541 TokenType::Star => (BinOp::Mul, 5),
4542 TokenType::Slash => (BinOp::Div, 5),
4543 TokenType::Percent => (BinOp::Mod, 5),
4544 _ => return None,
4545 })
4546 }
4547
4548 fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4554 let mut expr = self.parse_expr_atom()?;
4555 loop {
4556 if self.check(TokenType::Dot) {
4557 self.advance();
4558 let name = self.consume_any_ident_or_kw()?.value;
4559 if let Some(builtin) = Builtin::from_name(&name) {
4560 let mut args = vec![expr];
4561 if self.check(TokenType::LParen) {
4562 self.advance();
4563 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4564 args.push(self.parse_expr_bp(0)?);
4565 if self.check(TokenType::Comma) {
4566 self.advance();
4567 } else {
4568 break;
4569 }
4570 }
4571 self.consume(TokenType::RParen)?;
4572 }
4573 expr = Expr::Call(builtin, args);
4574 } else {
4575 expr = match expr {
4579 Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4580 other => Expr::Field(Box::new(other), name),
4581 };
4582 }
4583 } else if self.check(TokenType::LBracket) {
4584 self.advance();
4586 let index = self.parse_expr_bp(0)?;
4587 self.consume(TokenType::RBracket)?;
4588 expr = Expr::Index(Box::new(expr), Box::new(index));
4589 } else {
4590 break;
4591 }
4592 }
4593 Ok(expr)
4594 }
4595
4596 fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4597 let tok = self.current().clone();
4598 match tok.ttype {
4599 TokenType::Integer => {
4600 self.advance();
4601 let lit = tok
4602 .value
4603 .parse::<i64>()
4604 .map(ExprLit::Int)
4605 .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4606 .map_err(|_| ParseError {
4607 message: format!("invalid integer literal '{}'", tok.value),
4608 line: tok.line,
4609 column: tok.column,
4610 ..Default::default()
4611 })?;
4612 Ok(Expr::Lit(lit))
4613 }
4614 TokenType::Float => {
4615 self.advance();
4616 let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4617 message: format!("invalid float literal '{}'", tok.value),
4618 line: tok.line,
4619 column: tok.column,
4620 ..Default::default()
4621 })?;
4622 Ok(Expr::Lit(ExprLit::Float(f)))
4623 }
4624 TokenType::Bool => {
4625 self.advance();
4626 Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4627 }
4628 TokenType::StringLit => {
4629 self.advance();
4630 Ok(Expr::Lit(ExprLit::Str(tok.value)))
4631 }
4632 TokenType::LParen => {
4633 self.advance();
4634 let inner = self.parse_expr_bp(0)?;
4635 self.consume(TokenType::RParen)?;
4636 Ok(inner)
4637 }
4638 _ => {
4639 Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4643 }
4644 }
4645 }
4646
4647 fn expr_lit_surface(lit: &ExprLit) -> String {
4652 match lit {
4653 ExprLit::Int(i) => i.to_string(),
4654 ExprLit::Float(f) => f.to_string(),
4655 ExprLit::Bool(b) => b.to_string(),
4656 ExprLit::Str(s) => s.clone(),
4657 }
4658 }
4659
4660 fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4661 match expr {
4662 Expr::Ref(p) => Some(p.clone()),
4663 Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4664 _ => None,
4665 }
4666 }
4667
4668 fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4672 match expr {
4673 Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4674 Expr::Binary(op, l, r) => {
4675 let op_s = match op {
4676 BinOp::Eq => "==",
4677 BinOp::Ne => "!=",
4678 BinOp::Lt => "<",
4679 BinOp::Le => "<=",
4680 BinOp::Gt => ">",
4681 BinOp::Ge => ">=",
4682 _ => return None,
4683 };
4684 let lhs = match &**l {
4685 Expr::Ref(p) => p.clone(),
4686 _ => return None,
4687 };
4688 let rhs = Self::expr_leaf_surface(r)?;
4689 Some((lhs, op_s.to_string(), rhs))
4690 }
4691 _ => None,
4692 }
4693 }
4694
4695 fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4698 match expr {
4699 Expr::Binary(BinOp::Or, l, r) => {
4700 Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4701 }
4702 _ => match Self::expr_legacy_leaf(expr) {
4703 Some(t) => {
4704 out.push(t);
4705 true
4706 }
4707 None => false,
4708 },
4709 }
4710 }
4711
4712 #[allow(clippy::type_complexity)]
4718 fn cond_as_legacy(
4719 expr: &Expr,
4720 ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4721 let mut leaves = Vec::new();
4722 if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4723 return None;
4724 }
4725 let (c0, o0, v0) = leaves[0].clone();
4726 let rest = leaves[1..].to_vec();
4727 let conjunctor = if rest.is_empty() {
4728 String::new()
4729 } else {
4730 "or".to_string()
4731 };
4732 Some((c0, o0, v0, rest, conjunctor))
4733 }
4734
4735 fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4736 let tok = self.consume(TokenType::If)?;
4737 let loc = self.loc_of(&tok);
4738
4739 let expr = self.parse_expr()?;
4744 let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4745 match Self::cond_as_legacy(&expr) {
4746 Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4747 None => (
4748 String::new(),
4749 String::new(),
4750 String::new(),
4751 Vec::new(),
4752 String::new(),
4753 Some(expr),
4754 ),
4755 };
4756
4757 let mut then_body = Vec::new();
4758 let mut else_body = Vec::new();
4759
4760 if self.check(TokenType::Arrow) {
4762 self.advance();
4763 then_body.push(self.parse_flow_step()?);
4764 } else if self.check(TokenType::LBrace) {
4765 self.advance();
4766 while !self.check(TokenType::RBrace) {
4767 then_body.push(self.parse_flow_step()?);
4768 }
4769 self.consume(TokenType::RBrace)?;
4770 }
4771
4772 if self.check(TokenType::Else) {
4774 self.advance();
4775 if self.check(TokenType::Arrow) {
4776 self.advance();
4777 else_body.push(self.parse_flow_step()?);
4778 } else if self.check(TokenType::LBrace) {
4779 self.advance();
4780 while !self.check(TokenType::RBrace) {
4781 else_body.push(self.parse_flow_step()?);
4782 }
4783 self.consume(TokenType::RBrace)?;
4784 }
4785 }
4786
4787 Ok(ConditionalNode {
4788 condition,
4789 comparison_op,
4790 comparison_value,
4791 then_body,
4792 else_body,
4793 conditions,
4794 conjunctor,
4795 cond,
4796 loc,
4797 })
4798 }
4799
4800 fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4803 let tok = self.consume(TokenType::For)?;
4804 let loc = self.loc_of(&tok);
4805 let variable = self.consume(TokenType::Identifier)?.value;
4806 self.consume(TokenType::In)?;
4807 let iterable = self.parse_dotted_identifier()?;
4808
4809 self.consume(TokenType::LBrace)?;
4810 self.loop_depth += 1;
4817 let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4818 let mut body = Vec::new();
4819 while !self.check(TokenType::RBrace) {
4820 body.push(self.parse_flow_step()?);
4821 }
4822 Ok(body)
4823 })();
4824 self.loop_depth -= 1;
4825 let body = body_result?;
4826 self.consume(TokenType::RBrace)?;
4827
4828 Ok(ForInStatement {
4829 variable,
4830 iterable,
4831 body,
4832 loc,
4833 })
4834 }
4835
4836 fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4839 let tok = self.consume(TokenType::Break)?;
4840 let loc = self.loc_of(&tok);
4841 if self.loop_depth == 0 {
4842 return Err(ParseError {
4843 message: "'break' outside of a for-in loop body".to_string(),
4844 line: tok.line,
4845 column: tok.column,
4846 ..Default::default()
4847 });
4848 }
4849 Ok(BreakStatement { loc })
4850 }
4851
4852 fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4855 let tok = self.consume(TokenType::Continue)?;
4856 let loc = self.loc_of(&tok);
4857 if self.loop_depth == 0 {
4858 return Err(ParseError {
4859 message: "'continue' outside of a for-in loop body".to_string(),
4860 line: tok.line,
4861 column: tok.column,
4862 ..Default::default()
4863 });
4864 }
4865 Ok(ContinueStatement { loc })
4866 }
4867
4868 fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4871 let tok = self.consume(TokenType::Let)?;
4872 let loc = self.loc_of(&tok);
4873
4874 let name = self.consume_any_ident_or_kw()?.value;
4876 let type_annotation = if self.check(TokenType::Colon) {
4878 self.advance();
4879 Some(self.parse_type_expr()?)
4880 } else {
4881 None
4882 };
4883 self.consume(TokenType::Assign)?;
4884 self.last_let_value_kind = "literal".to_string();
4887 let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4888
4889 Ok(LetStatement {
4890 identifier: name,
4891 value_expr: value,
4892 value_kind: self.last_let_value_kind.clone(),
4893 type_annotation,
4894 value_ast,
4895 loc,
4896 leading_trivia: Vec::new(),
4897 trailing_trivia: Vec::new(),
4898 })
4899 }
4900
4901 fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4902 let atom = self.parse_let_atom()?;
4903
4904 if matches!(
4906 self.current().ttype,
4907 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4908 ) {
4909 let mut parts = vec![atom];
4910 while matches!(
4911 self.current().ttype,
4912 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4913 ) {
4914 parts.push(self.advance().value.clone());
4915 parts.push(self.parse_let_atom()?);
4916 }
4917 self.last_let_value_kind = "expression".to_string();
4918 return Ok(parts.join(" "));
4919 }
4920 Ok(atom)
4921 }
4922
4923 fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4933 if self.check(TokenType::LBracket) {
4934 self.last_let_value_kind = "literal".to_string();
4935 return Ok((self.parse_let_list_literal()?, None));
4936 }
4937 let expr = self.parse_expr()?;
4938 Ok(match expr {
4939 Expr::Lit(lit) => {
4940 self.last_let_value_kind = "literal".to_string();
4941 (Self::expr_lit_surface(&lit), None)
4942 }
4943 Expr::Ref(p) => {
4944 self.last_let_value_kind = "reference".to_string();
4945 (p, None)
4946 }
4947 other => {
4948 self.last_let_value_kind = "expression".to_string();
4949 (Self::render_expr(&other), Some(other))
4950 }
4951 })
4952 }
4953
4954 fn render_expr(e: &Expr) -> String {
4957 match e {
4958 Expr::Lit(l) => Self::expr_lit_surface(l),
4959 Expr::Ref(p) => p.clone(),
4960 Expr::Let { name, value, body } => format!(
4964 "let {name} = {} in {}",
4965 Self::render_expr(value),
4966 Self::render_expr(body)
4967 ),
4968 Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4969 Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4970 Expr::Binary(op, l, r) => {
4971 let sym = match op {
4972 BinOp::Add => "+",
4973 BinOp::Sub => "-",
4974 BinOp::Mul => "*",
4975 BinOp::Div => "/",
4976 BinOp::Mod => "%",
4977 BinOp::Eq => "==",
4978 BinOp::Ne => "!=",
4979 BinOp::Lt => "<",
4980 BinOp::Le => "<=",
4981 BinOp::Gt => ">",
4982 BinOp::Ge => ">=",
4983 BinOp::And => "and",
4984 BinOp::Or => "or",
4985 };
4986 format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
4987 }
4988 Expr::Call(b, args) => {
4989 let recv = args.first().map(Self::render_expr).unwrap_or_default();
4990 let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
4991 if rest.is_empty() {
4992 format!("{recv}.{}", b.surface())
4993 } else {
4994 format!("{recv}.{}({})", b.surface(), rest.join(", "))
4995 }
4996 }
4997 Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
4998 Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
4999 }
5000 }
5001
5002 fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5003 let tok = self.current().clone();
5004
5005 match tok.ttype {
5006 TokenType::StringLit => {
5007 self.last_let_value_kind = "literal".to_string();
5008 self.advance();
5009 Ok(tok.value)
5010 }
5011 TokenType::Integer | TokenType::Float => {
5012 self.last_let_value_kind = "literal".to_string();
5013 self.advance();
5014 Ok(tok.value)
5015 }
5016 TokenType::Bool => {
5017 self.last_let_value_kind = "literal".to_string();
5018 self.advance();
5019 Ok(tok.value)
5020 }
5021 TokenType::Identifier => {
5022 self.last_let_value_kind = "reference".to_string();
5023 self.parse_dotted_identifier()
5024 }
5025 TokenType::LBracket => {
5026 self.last_let_value_kind = "literal".to_string();
5027 self.parse_let_list_literal()
5028 }
5029 _ => {
5030 if self.pos + 1 < self.tokens.len()
5032 && self.tokens[self.pos + 1].ttype == TokenType::Dot
5033 {
5034 self.last_let_value_kind = "reference".to_string();
5035 return self.parse_dotted_identifier();
5036 }
5037 Err(ParseError {
5038 message: format!(
5039 "Expected value expression, found {:?}('{}')",
5040 tok.ttype, tok.value
5041 ),
5042 line: tok.line,
5043 column: tok.column,
5044 ..Default::default()
5045 })
5046 }
5047 }
5048 }
5049
5050 fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5051 self.consume(TokenType::LBracket)?;
5052 let mut items = Vec::new();
5053 if !self.check(TokenType::RBracket) {
5054 items.push(self.parse_let_value_expr()?);
5055 while self.check(TokenType::Comma) {
5056 self.advance();
5057 if self.check(TokenType::RBracket) {
5058 break; }
5060 items.push(self.parse_let_value_expr()?);
5061 }
5062 }
5063 self.consume(TokenType::RBracket)?;
5064 Ok(format!("[{}]", items.join(", ")))
5065 }
5066
5067 fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5070 let tok = self.consume(TokenType::Return)?;
5071 let loc = self.loc_of(&tok);
5072 let value = self.parse_let_value_expr()?;
5073 Ok(ReturnStatement {
5074 value_expr: value,
5075 loc,
5076 })
5077 }
5078
5079 fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5083 let tok = self.current().clone();
5084 self.advance(); let target = if self.at_declaration_start()
5086 || self.check(TokenType::RBrace)
5087 || self.check(TokenType::Eof)
5088 {
5089 String::new()
5090 } else {
5091 self.consume_any_ident_or_kw()?.value.clone()
5092 };
5093 if self.check(TokenType::LBrace) {
5095 self.skip_braced_block()?;
5096 }
5097 Ok((
5098 Loc {
5099 line: tok.line,
5100 column: tok.column,
5101 },
5102 target,
5103 ))
5104 }
5105
5106 fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5117 let tok = self.current().clone();
5118 let loc = self.loc_of(&tok);
5119 self.advance(); let mut chunk_type = String::new();
5127 if self.check(TokenType::Lt) {
5128 self.advance();
5129 let inner = self.parse_type_expr()?;
5130 chunk_type = if inner.generic_param.is_empty() {
5131 inner.name
5132 } else {
5133 format!("{}<{}>", inner.name, inner.generic_param)
5134 };
5135 self.consume(TokenType::Gt)?;
5136 }
5137
5138 while !self.check(TokenType::LBrace)
5142 && !self.check(TokenType::RBrace)
5143 && !self.check(TokenType::Eof)
5144 && !self.at_declaration_start()
5145 {
5146 self.advance();
5147 }
5148
5149 let mut block = StreamBlock {
5150 chunk_type,
5151 on_chunk: None,
5152 on_complete: None,
5153 on_error: None,
5154 body: Vec::new(),
5155 loc,
5156 };
5157
5158 if self.check(TokenType::LBrace) {
5159 self.advance();
5160 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5161 let name = self.current().value.clone();
5167 let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5168 && self
5169 .tokens
5170 .get(self.pos + 1)
5171 .is_some_and(|t| t.ttype == TokenType::Colon);
5172 if is_arm {
5173 let arm_tok = self.current().clone();
5174 self.advance(); self.advance(); let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5177 let slot = match name.as_str() {
5178 "on_chunk" => &mut block.on_chunk,
5179 "on_complete" => &mut block.on_complete,
5180 _ => &mut block.on_error,
5181 };
5182 if slot.is_some() {
5183 return Err(ParseError {
5184 message: format!(
5185 "`{name}` is declared twice in this `stream` block. Two handlers \
5186 for one edge have no defined composition (whose output is the \
5187 stream's?), so the duplicate is refused rather than silently \
5188 overwriting the first."
5189 ),
5190 line: arm_tok.line,
5191 column: arm_tok.column,
5192 ..Default::default()
5193 });
5194 }
5195 *slot = Some(arm);
5196 continue;
5197 }
5198
5199 let next_two_are_block = self
5206 .tokens
5207 .get(self.pos + 1)
5208 .is_some_and(|t| t.ttype == TokenType::Colon)
5209 && self
5210 .tokens
5211 .get(self.pos + 2)
5212 .is_some_and(|t| t.ttype == TokenType::LBrace);
5213 if next_two_are_block {
5214 let bad = self.current().clone();
5215 return Err(ParseError {
5216 message: format!(
5217 "unknown `stream` handler `{name}` — this block accepts only \
5218 `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5219 `on_complete:` (run once, after the source closes, with the \
5220 accumulation bound as `complete`) and `on_error:` (run when the \
5221 SOURCE fails, with the failure bound as `error`). An unrecognised \
5222 handler is refused rather than skipped: a skipped handler removes \
5223 the processing the author wrote, and silence in that direction is \
5224 indistinguishable from a stream that had nothing to do."
5225 ),
5226 line: bad.line,
5227 column: bad.column,
5228 ..Default::default()
5229 });
5230 }
5231
5232 block.body.push(self.parse_flow_step()?);
5234 }
5235 self.consume(TokenType::RBrace)?;
5236 }
5237
5238 Ok(block)
5239 }
5240
5241 fn parse_stream_handler_arm(
5250 &mut self,
5251 name: &str,
5252 at: &Token,
5253 ) -> Result<StepNode, ParseError> {
5254 self.consume(TokenType::LBrace)?;
5255 let mut node = StepNode {
5256 name: name.to_string(),
5257 persona_ref: String::new(),
5258 given: String::new(),
5259 ask: String::new(),
5260 output_type: String::new(),
5261 confidence_floor: None,
5262 navigate_ref: String::new(),
5263 apply_ref: String::new(),
5264 requires_context: None,
5265 now_tz: None,
5266 guards: Vec::new(),
5267 pix_ops: Vec::new(),
5268 stream: None,
5269 performs: Vec::new(),
5270 loc: self.loc_of(at),
5271 };
5272 self.parse_step_body_into(&mut node)?;
5273 self.consume(TokenType::RBrace)?;
5274 Ok(node)
5275 }
5276
5277 fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5278 let tok = self.current().clone();
5279 self.advance();
5280 while !self.check(TokenType::LBrace)
5282 && !self.check(TokenType::RBrace)
5283 && !self.check(TokenType::Eof)
5284 && !self.at_declaration_start()
5285 {
5286 self.advance();
5287 }
5288 if self.check(TokenType::LBrace) {
5289 self.skip_braced_block()?;
5290 }
5291 Ok(Loc {
5292 line: tok.line,
5293 column: tok.column,
5294 })
5295 }
5296
5297 fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5304 let tok = self.consume(TokenType::Forge)?;
5305 let name = self.consume(TokenType::Identifier)?.value;
5306 let mut node = ForgeBlock {
5307 name,
5308 novelty: 0.5,
5309 depth: 1,
5310 branches: 1,
5311 loc: Loc { line: tok.line, column: tok.column },
5312 ..Default::default()
5313 };
5314 self.consume(TokenType::LParen)?;
5316 let arg = self.consume_any_ident_or_kw()?.value;
5317 self.consume(TokenType::Colon)?;
5318 if arg != "seed" {
5319 return Err(self.error(&format!(
5320 "forge '{}' expects `seed:` as its argument, found `{arg}`",
5321 node.name
5322 )));
5323 }
5324 node.seed = self.consume(TokenType::StringLit)?.value;
5325 self.consume(TokenType::RParen)?;
5326 self.consume(TokenType::Arrow)?;
5328 node.output_type = self.consume_any_ident_or_kw()?.value;
5329 self.consume(TokenType::LBrace)?;
5331 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5332 let field = self.consume_any_ident_or_kw()?.value;
5333 self.consume(TokenType::Colon)?;
5334 match field.as_str() {
5335 "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5336 "novelty" => node.novelty = self.consume_number()?,
5337 "depth" => {
5338 node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5339 }
5340 "branches" => {
5341 node.branches =
5342 self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5343 }
5344 "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5345 other => {
5346 return Err(self.error(&format!("unknown forge field `{other}`")))
5347 }
5348 }
5349 if self.check(TokenType::Comma) {
5350 self.consume(TokenType::Comma)?;
5351 }
5352 }
5353 self.consume(TokenType::RBrace)?;
5354 Ok(node)
5355 }
5356
5357 fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5366 let tok = self.current().clone();
5367 self.advance(); self.consume(TokenType::LBrace)?;
5369 let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5370 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5371 branches.push(vec![self.parse_flow_step()?]);
5372 }
5373 self.consume(TokenType::RBrace)?;
5374 Ok(ParBlock {
5375 branches,
5376 loc: Loc {
5377 line: tok.line,
5378 column: tok.column,
5379 },
5380 })
5381 }
5382
5383 fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5400 let tok = self.consume(TokenType::Warden)?;
5401 self.consume(TokenType::LParen)?;
5403 let target = self.consume_any_ident_or_kw()?.value;
5404 self.consume(TokenType::RParen)?;
5405 self.consume(TokenType::Within)?;
5407 let scope_ref = self.consume(TokenType::Identifier)?.value;
5408 let mut block = WardenBlock {
5409 target,
5410 scope_ref,
5411 body: Vec::new(),
5412 loc: Loc {
5413 line: tok.line,
5414 column: tok.column,
5415 },
5416 };
5417 self.consume(TokenType::LBrace)?;
5419 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5420 block.body.push(self.parse_flow_step()?);
5421 }
5422 self.consume(TokenType::RBrace)?;
5423 Ok(block)
5424 }
5425
5426 fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5431 let tok = self.consume(TokenType::Scope)?;
5432 let name = self.consume(TokenType::Identifier)?.value;
5433 let mut node = ScopeDefinition {
5434 name,
5435 loc: Loc {
5436 line: tok.line,
5437 column: tok.column,
5438 },
5439 ..Default::default()
5440 };
5441 self.consume(TokenType::LBrace)?;
5442 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5443 let key = self.consume_any_ident_or_kw()?.value;
5444 self.consume(TokenType::Colon)?;
5445 match key.as_str() {
5446 "targets" => {
5447 self.consume(TokenType::LBracket)?;
5448 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5449 let t = if self.check(TokenType::StringLit) {
5450 self.consume(TokenType::StringLit)?.value
5451 } else {
5452 self.consume_any_ident_or_kw()?.value
5453 };
5454 node.targets.push(t);
5455 if self.check(TokenType::Comma) {
5456 self.advance();
5457 }
5458 }
5459 self.consume(TokenType::RBracket)?;
5460 }
5461 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5462 "approver" => {
5463 if self.current().value == "requires" {
5465 self.advance();
5466 }
5467 node.approver = self.consume(TokenType::StringLit)?.value;
5468 }
5469 other => {
5470 return Err(self.error(&format!(
5471 "unknown scope field `{other}` in scope `{}` — expected \
5472 `targets` / `depth` / `approver`",
5473 node.name
5474 )))
5475 }
5476 }
5477 if self.check(TokenType::Comma) {
5478 self.consume(TokenType::Comma)?;
5479 }
5480 }
5481 self.consume(TokenType::RBrace)?;
5482 Ok(node)
5483 }
5484
5485 fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5486 let tok = self.current().clone();
5487 self.advance(); let mut block = QuantBlock {
5490 encoding: None,
5491 observable: None,
5492 qubits: None,
5493 depth: None,
5494 bandwidth: None,
5495 reupload: None,
5496 effect: "quant_sim".to_string(),
5499 body: Vec::new(),
5500 loc: Loc {
5501 line: tok.line,
5502 column: tok.column,
5503 },
5504 };
5505
5506 if self.check(TokenType::LParen) {
5508 self.advance();
5509 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5510 let key = self.consume_any_ident_or_kw()?.value;
5511 self.consume(TokenType::Colon)?;
5512 match key.as_str() {
5513 "encoding" => {
5514 block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5515 }
5516 "observable" => {
5517 block.observable = Some(self.parse_dotted_identifier()?)
5518 }
5519 "qubits" => block.qubits = Some(self.consume_number()? as i64),
5520 "depth" => block.depth = Some(self.consume_number()? as i64),
5521 "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5522 "reupload" => block.reupload = Some(self.consume_number()? as i64),
5524 "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5526 other => {
5527 return Err(ParseError {
5528 message: format!(
5529 "Unknown `quant` attribute `{other}` — expected one of \
5530 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5531 ),
5532 line: self.current().line,
5533 column: self.current().column,
5534 ..Default::default()
5535 });
5536 }
5537 }
5538 if self.check(TokenType::Comma) {
5540 self.advance();
5541 }
5542 }
5543 self.consume(TokenType::RParen)?;
5544 }
5545
5546 self.consume(TokenType::LBrace)?;
5548 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5549 block.body.push(self.parse_flow_step()?);
5550 }
5551 self.consume(TokenType::RBrace)?;
5552
5553 Ok(block)
5554 }
5555
5556 fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5560 let tok = self.consume(TokenType::Yield)?;
5561 let loc = self.loc_of(&tok);
5562 self.last_let_value_kind = "literal".to_string();
5563 let value_expr = self.parse_let_value_expr()?;
5564 Ok(YieldStatement {
5565 value_expr,
5566 value_kind: self.last_let_value_kind.clone(),
5567 loc,
5568 })
5569 }
5570
5571 fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5578 let tok = self.current().clone();
5579 let loc = self.loc_of(&tok);
5580 self.advance(); let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5582
5583 let mut arguments = Vec::new();
5584 if self.current().value == "on" {
5585 self.advance();
5586 loop {
5587 arguments.push(self.parse_subject()?);
5593 if self.check(TokenType::Comma) {
5594 self.advance();
5595 } else {
5596 break;
5597 }
5598 }
5599 }
5600
5601 let mut output_name = String::new();
5602 if self.check(TokenType::Arrow) {
5603 self.advance();
5604 output_name = self.consume_any_ident_or_kw()?.value.clone();
5605 }
5606
5607 Ok(ComputeApplyStep {
5608 compute_name,
5609 arguments,
5610 output_name,
5611 loc,
5612 })
5613 }
5614
5615 fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5616 let tok = self.current().clone();
5617 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5619 let mut target = String::new();
5620 let mut output_type = String::new();
5621 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5623 let next = self.current().clone();
5624 if next.value == "on" {
5625 self.advance();
5626 target = self.parse_subject()?;
5629 }
5630 }
5631 if self.check(TokenType::Arrow) {
5633 self.advance();
5634 output_type = self.consume_any_ident_or_kw()?.value.clone();
5635 }
5636 if self.check(TokenType::LBrace) {
5638 self.skip_braced_block()?;
5639 }
5640 Ok((
5641 Loc {
5642 line: tok.line,
5643 column: tok.column,
5644 },
5645 name,
5646 target,
5647 output_type,
5648 ))
5649 }
5650
5651 fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5662 let tok = self.current().clone();
5663 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5665 let mut target = String::new();
5666 let mut binding = String::new();
5667 if self.current().value == "on" {
5668 self.advance();
5669 target = self.parse_subject()?;
5672 if self.check(TokenType::LParen) {
5675 let mut depth = 0usize;
5676 loop {
5677 let t = self.current().clone();
5678 match t.ttype {
5679 TokenType::LParen => depth += 1,
5680 TokenType::RParen => depth -= 1,
5681 TokenType::Eof => {
5682 return Err(ParseError {
5683 message: format!(
5684 "unterminated argument list in `{kind} {name} on {target}(…`"
5685 ),
5686 line: t.line,
5687 column: t.column,
5688 ..Default::default()
5689 })
5690 }
5691 _ => {}
5692 }
5693 target.push_str(&t.value);
5694 self.advance();
5695 if depth == 0 {
5696 break;
5697 }
5698 }
5699 }
5700 }
5701 if self.check(TokenType::Arrow) {
5702 self.advance();
5703 binding = self.consume_any_ident_or_kw()?.value.clone();
5704 }
5705 Ok(StepGuardNode {
5706 kind: kind.to_string(),
5707 name,
5708 target,
5709 binding,
5710 loc: Loc {
5711 line: tok.line,
5712 column: tok.column,
5713 },
5714 })
5715 }
5716
5717 fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5728 let tok = self.current().clone();
5729 let loc = self.loc_of(&tok);
5730 self.advance(); let next_is_field_key = self
5743 .tokens
5744 .get(self.pos + 1)
5745 .is_some_and(|t| t.ttype == TokenType::Colon);
5746 let target = if self.check(TokenType::LBrace)
5747 || self.at_declaration_start()
5748 || self.check(TokenType::RBrace)
5749 || self.check(TokenType::Eof)
5750 || next_is_field_key
5751 {
5752 String::new()
5753 } else {
5754 self.parse_dotted_identifier()?
5755 };
5756
5757 let mut node = ReasonStep {
5758 strategy: String::new(),
5759 target,
5760 given: String::new(),
5761 ask: String::new(),
5762 depth: None,
5763 loc,
5764 };
5765
5766 if !self.check(TokenType::LBrace) {
5767 return Ok(node);
5768 }
5769 self.advance();
5770 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5771 let key = self.current().clone();
5772 self.advance();
5773 self.consume(TokenType::Colon)?;
5774 match key.value.as_str() {
5775 "given" => {
5780 let mut parts = vec![self.parse_expression_string()?];
5781 while self.check(TokenType::Comma) {
5782 self.advance();
5783 parts.push(self.parse_expression_string()?);
5784 }
5785 node.given = parts.join(", ");
5786 }
5787 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5788 "depth" => {
5789 let n = self.current().clone();
5790 if n.ttype != TokenType::Integer {
5791 return Err(ParseError {
5792 message: format!(
5793 "`depth:` in a `reason` block is a deliberation depth — a \
5794 positive integer (got '{}')",
5795 n.value
5796 ),
5797 line: n.line,
5798 column: n.column,
5799 ..Default::default()
5800 });
5801 }
5802 self.advance();
5803 node.depth = n.value.parse::<u32>().ok();
5804 }
5805 "chain_of_thought" => {
5809 let v = self.consume_any_ident_or_kw()?.value;
5810 if v == "enabled" {
5811 node.strategy = "chain_of_thought".to_string();
5812 }
5813 }
5814 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5815 "target" => {
5824 let v = if self.check(TokenType::StringLit) {
5825 self.consume(TokenType::StringLit)?.value
5826 } else {
5827 self.parse_dotted_identifier()?
5828 };
5829 if !node.target.is_empty() {
5830 return Err(ParseError {
5831 message: format!(
5832 "`reason {} {{ target: … }}` declares the subject twice — \
5833 once positionally as `{}` and once as `target: {}`. They \
5834 are the same field. Write one of them.",
5835 node.target, node.target, v
5836 ),
5837 line: key.line,
5838 column: key.column,
5839 ..Default::default()
5840 });
5841 }
5842 node.target = v;
5843 }
5844 other => {
5845 return Err(ParseError {
5846 message: format!(
5847 "unknown field '{other}' in a `reason` block. Accepted: given, \
5848 ask, depth, strategy, chain_of_thought, target. A field this \
5849 block does not recognise is REFUSED rather than skipped — a \
5850 `reason` that silently loses its `ask:` deliberates over \
5851 nothing, and that failure is quiet."
5852 ),
5853 line: key.line,
5854 column: key.column,
5855 ..Default::default()
5856 })
5857 }
5858 }
5859 }
5860 self.consume(TokenType::RBrace)?;
5861 Ok(node)
5862 }
5863
5864 fn at_weave_field(&self) -> bool {
5873 const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5874 self.field_ahead(FIELDS)
5875 }
5876
5877 fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5886 let tok = self.current().clone();
5887 self.advance();
5888 let mut node = WeaveStep {
5889 sources: Vec::new(),
5890 target: String::new(),
5891 format_type: String::new(),
5892 priority: Vec::new(),
5893 style: String::new(),
5894 include: Vec::new(),
5895 loc: Loc {
5896 line: tok.line,
5897 column: tok.column,
5898 },
5899 };
5900 if self.check(TokenType::LBracket) {
5906 node.sources = self.parse_bracketed_dot_identifiers()?;
5907 } else if self.current().ttype == TokenType::Identifier
5908 && !self
5909 .tokens
5910 .get(self.pos + 1)
5911 .is_some_and(|t| t.ttype == TokenType::Colon)
5912 {
5913 node.sources = vec![self.parse_dotted_identifier()?];
5923 }
5924 if self.check(TokenType::Into) || self.current().value == "into" {
5926 self.advance();
5927 node.target = self.parse_dotted_identifier()?;
5928 }
5929 while self.at_weave_field() {
5931 let f = self.current().value.clone();
5932 self.advance();
5933 self.consume(TokenType::Colon)?;
5934 match f.as_str() {
5935 "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
5936 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5937 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5938 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5939 other => {
5942 return Err(ParseError {
5943 message: format!(
5944 "`{other}` passed the `weave` field test but has no handler — \
5945 the braceless catalog and its parser have drifted apart."
5946 ),
5947 line: tok.line,
5948 column: tok.column,
5949 ..Default::default()
5950 })
5951 }
5952 }
5953 }
5954 if self.check(TokenType::LBrace) {
5955 self.advance();
5956 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5957 let f = self.current().value.clone();
5958 self.advance();
5959 if self.check(TokenType::Colon) {
5960 self.advance();
5961 match f.as_str() {
5962 "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
5963 "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
5964 "format" => {
5965 node.format_type = self.consume_any_ident_or_kw()?.value.clone()
5966 }
5967 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
5968 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
5969 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
5973 _ => self.skip_value(),
5974 }
5975 }
5976 }
5977 if self.check(TokenType::RBrace) {
5978 self.advance();
5979 }
5980 }
5981 Ok(FlowStep::Weave(node))
5982 }
5983
5984 fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
5985 let tok = self.current().clone();
5986 self.advance();
5987 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
5988 let args = if self.check(TokenType::LParen) {
5999 UseArgs::Named(self.parse_named_arg_list()?)
6000 } else {
6001 let mut argument = String::new();
6002 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6003 let next = self.current().clone();
6004 if next.value == "on" {
6005 self.advance();
6006 argument = self.consume_any_ident_or_kw()?.value.clone();
6007 }
6008 }
6009 UseArgs::LegacyPositional(argument)
6010 };
6011 if self.check(TokenType::LBrace) {
6012 self.skip_braced_block()?;
6013 }
6014 Ok(FlowStep::UseTool(UseToolStep {
6015 tool_name,
6016 args,
6017 loc: Loc {
6018 line: tok.line,
6019 column: tok.column,
6020 },
6021 }))
6022 }
6023
6024 fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6030 self.consume(TokenType::LParen)?;
6031 let mut args = Vec::new();
6032 while !self.check(TokenType::RParen) {
6033 let name = self.consume_any_ident_or_kw()?.value;
6036 self.consume(TokenType::Assign)?;
6037 let value = self.parse_let_atom()?;
6038 let value_kind = self.last_let_value_kind.clone();
6042 args.push((name, value, value_kind));
6043 if self.check(TokenType::Comma) {
6044 self.advance();
6045 } else {
6046 break;
6047 }
6048 }
6049 self.consume(TokenType::RParen)?;
6050 Ok(args)
6051 }
6052
6053 fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6054 let tok = self.current().clone();
6055 self.advance();
6056 let expr = self.consume_any_ident_or_kw()?.value.clone();
6057 let mut mem = String::new();
6058 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6059 let next = self.current().clone();
6060 if next.value == "in" || next.ttype == TokenType::In {
6061 self.advance();
6062 mem = self.consume_any_ident_or_kw()?.value.clone();
6063 }
6064 }
6065 Ok(FlowStep::Remember(RememberStep {
6066 expression: expr,
6067 memory_target: mem,
6068 loc: Loc {
6069 line: tok.line,
6070 column: tok.column,
6071 },
6072 }))
6073 }
6074
6075 fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6076 let tok = self.current().clone();
6077 self.advance();
6078 let query = if self.check(TokenType::StringLit) {
6079 self.consume(TokenType::StringLit)?.value.clone()
6080 } else {
6081 self.consume_any_ident_or_kw()?.value.clone()
6082 };
6083 let mut mem = String::new();
6084 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6085 let next = self.current().clone();
6086 if next.value == "from" || next.ttype == TokenType::From {
6087 self.advance();
6088 mem = self.consume_any_ident_or_kw()?.value.clone();
6089 }
6090 }
6091 Ok(FlowStep::Recall(RecallStep {
6092 query,
6093 memory_source: mem,
6094 loc: Loc {
6095 line: tok.line,
6096 column: tok.column,
6097 },
6098 }))
6099 }
6100
6101 fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6102 let tok = self.current().clone();
6103 self.advance();
6104 let mut event = String::new();
6105 let mut timeout = String::new();
6106 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6107 let first = self.consume_any_ident_or_kw()?.value.clone();
6112 if first == "until" && self.check(TokenType::StringLit) {
6113 event = self.consume(TokenType::StringLit)?.value.clone();
6114 } else {
6115 event = first;
6116 }
6117 }
6118 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6119 let next = self.current().clone();
6120 if next.ttype == TokenType::Duration {
6121 self.advance();
6122 timeout = next.value.clone();
6123 }
6124 }
6125 Ok(FlowStep::Hibernate(HibernateStep {
6126 event_name: event,
6127 timeout,
6128 loc: Loc {
6129 line: tok.line,
6130 column: tok.column,
6131 },
6132 }))
6133 }
6134
6135 fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6144 let tok = self.current().clone();
6145 self.advance();
6146 let target = self.consume_any_ident_or_kw()?.value.clone();
6147 let mut wrt: Vec<String> = Vec::new();
6148 let mut output = String::new();
6149 if !self.at_declaration_start() && self.current().value == "wrt" {
6150 self.advance();
6151 if self.check(TokenType::LBracket) {
6152 wrt = self.parse_bracketed_identifiers()?;
6153 } else {
6154 wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6155 }
6156 }
6157 if !self.at_declaration_start() && self.current().value == "as" {
6158 self.advance();
6159 output = self.consume_any_ident_or_kw()?.value.clone();
6160 }
6161 Ok(FlowStep::Grad(GradStep {
6162 target,
6163 wrt,
6164 output,
6165 loc: Loc {
6166 line: tok.line,
6167 column: tok.column,
6168 },
6169 }))
6170 }
6171
6172 fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6173 let tok = self.current().clone();
6174 self.advance();
6175 let expression = if self.at_declaration_start()
6176 || self.check(TokenType::RBrace)
6177 || self.check(TokenType::Eof)
6178 {
6179 String::new()
6180 } else {
6181 self.consume_any_ident_or_kw()?.value.clone()
6182 };
6183 let mut where_expr = String::new();
6184 let mut select: Vec<String> = Vec::new();
6185 let mut output = String::new();
6186 if self.check(TokenType::LBrace) {
6187 self.advance();
6188 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6189 if self.check(TokenType::Comma) {
6190 self.advance();
6191 continue;
6192 }
6193 let f = self.current().value.clone();
6194 self.advance();
6195 if self.check(TokenType::Colon) {
6196 self.advance();
6197 match f.as_str() {
6198 "where" => {
6199 where_expr = self.consume(TokenType::StringLit)?.value.clone()
6200 }
6201 "select" => select = self.parse_bracketed_identifiers()?,
6202 "as" | "alias" => {
6203 output = self.consume_any_ident_or_kw()?.value.clone()
6204 }
6205 _ => self.skip_value(),
6206 }
6207 }
6208 }
6209 if self.check(TokenType::RBrace) {
6210 self.advance();
6211 }
6212 }
6213 Ok(FlowStep::Focus(FocusStep {
6214 expression,
6215 where_expr,
6216 select,
6217 output,
6218 loc: Loc {
6219 line: tok.line,
6220 column: tok.column,
6221 },
6222 }))
6223 }
6224
6225 fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6226 let tok = self.current().clone();
6227 self.advance();
6228 let left = self.consume_any_ident_or_kw()?.value.clone();
6229 let mut right = String::new();
6230 let mut using = String::new();
6231 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6232 right = self.consume_any_ident_or_kw()?.value.clone();
6233 }
6234 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6235 let next = self.current().clone();
6236 if next.value == "using" {
6237 self.advance();
6238 using = self.consume_any_ident_or_kw()?.value.clone();
6239 }
6240 }
6241 let mut output = String::new();
6242 if self.check(TokenType::LBrace) {
6243 self.advance();
6244 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6245 let f = self.current().value.clone();
6246 self.advance();
6247 if self.check(TokenType::Colon) {
6248 self.advance();
6249 match f.as_str() {
6250 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6251 _ => self.skip_value(),
6252 }
6253 }
6254 }
6255 if self.check(TokenType::RBrace) {
6256 self.advance();
6257 }
6258 }
6259 Ok(FlowStep::Associate(AssociateStep {
6260 left,
6261 right,
6262 using_field: using,
6263 output,
6264 loc: Loc {
6265 line: tok.line,
6266 column: tok.column,
6267 },
6268 }))
6269 }
6270
6271 fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6272 let tok = self.current().clone();
6273 self.advance();
6274 let target = self.consume_any_ident_or_kw()?.value.clone();
6275 let mut group_by = Vec::new();
6276 let mut alias = String::new();
6277 let mut compute: Vec<String> = Vec::new();
6278 let mut where_expr = String::new();
6279 if self.check(TokenType::LBrace) {
6280 self.advance();
6281 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6282 let f = self.current().value.clone();
6283 self.advance();
6284 if self.check(TokenType::Colon) {
6285 self.advance();
6286 match f.as_str() {
6287 "group_by" => group_by = self.parse_bracketed_identifiers()?,
6288 "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6289 "compute" => compute = self.parse_bracketed_aggregates()?,
6292 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6294 _ => self.skip_value(),
6295 }
6296 }
6297 }
6298 if self.check(TokenType::RBrace) {
6299 self.advance();
6300 }
6301 }
6302 Ok(FlowStep::Aggregate(AggregateStep {
6303 target,
6304 group_by,
6305 alias,
6306 compute,
6307 where_expr,
6308 loc: Loc {
6309 line: tok.line,
6310 column: tok.column,
6311 },
6312 }))
6313 }
6314
6315 fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6316 let tok = self.current().clone();
6317 self.advance();
6318 let target = self.consume_any_ident_or_kw()?.value.clone();
6319 let mut limit = None;
6320 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6321 if self.current().ttype == TokenType::Integer {
6322 limit = self.current().value.parse::<i64>().ok();
6323 self.advance();
6324 }
6325 }
6326 let mut output = String::new();
6327 if self.check(TokenType::LBrace) {
6328 self.advance();
6329 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6330 let f = self.current().value.clone();
6331 self.advance();
6332 if self.check(TokenType::Colon) {
6333 self.advance();
6334 match f.as_str() {
6335 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6336 _ => self.skip_value(),
6337 }
6338 }
6339 }
6340 if self.check(TokenType::RBrace) {
6341 self.advance();
6342 }
6343 }
6344 Ok(FlowStep::ExploreStep(ExploreStepNode {
6345 target,
6346 limit,
6347 output,
6348 loc: Loc {
6349 line: tok.line,
6350 column: tok.column,
6351 },
6352 }))
6353 }
6354
6355 fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6358 let mut out = Vec::new();
6359 self.consume(TokenType::LBracket)?;
6360 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6361 let name = self.consume_any_ident_or_kw()?.value.clone();
6362 if self.check(TokenType::LParen) {
6363 self.advance();
6364 let col = self.consume_any_ident_or_kw()?.value.clone();
6365 self.consume(TokenType::RParen)?;
6366 out.push(format!("{name}({col})"));
6367 } else {
6368 out.push(name);
6369 }
6370 if self.check(TokenType::Comma) {
6371 self.advance();
6372 }
6373 }
6374 self.consume(TokenType::RBracket)?;
6375 Ok(out)
6376 }
6377
6378 fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6393 let tok = self.current().clone();
6394 self.advance();
6395 let source = self.consume_any_ident_or_kw()?.value.clone();
6396 let mut target = String::new();
6397 let mut format = String::new();
6398 let mut max_bytes: Option<u64> = None;
6399 let mut max_rows: Option<u64> = None;
6400 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6401 let next = self.current().clone();
6402 if next.value == "into" || next.ttype == TokenType::Into {
6403 self.advance();
6404 target = self.consume_any_ident_or_kw()?.value.clone();
6405 }
6406 }
6407 if self.check(TokenType::LBrace) {
6408 self.consume(TokenType::LBrace)?;
6409 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6410 if self.check(TokenType::Comma) {
6412 self.advance();
6413 continue;
6414 }
6415 let entry = self.current().clone();
6416 match entry.value.as_str() {
6417 "format" => {
6418 self.advance();
6419 self.consume(TokenType::Colon)?;
6420 format = self.consume_any_ident_or_kw()?.value.clone();
6421 }
6422 "limits" => {
6423 self.advance();
6424 self.consume(TokenType::LBrace)?;
6425 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6426 let bound = self.current().clone();
6427 self.advance();
6428 self.consume(TokenType::Colon)?;
6429 let num_tok = self.consume(TokenType::Integer)?.clone();
6430 let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6431 message: format!(
6432 "ingest `limits` bound `{}` must be a non-negative \
6433 integer byte/row count, got `{}`.",
6434 bound.value, num_tok.value
6435 ),
6436 line: num_tok.line,
6437 column: num_tok.column,
6438 ..Default::default()
6439 })?;
6440 match bound.value.as_str() {
6441 "max_bytes" => max_bytes = Some(value),
6442 "max_rows" => max_rows = Some(value),
6443 other => {
6444 return Err(ParseError {
6445 message: format!(
6446 "Unknown ingest limit `{other}`. The closed \
6447 limits grammar is `max_bytes: <N>` and \
6448 `max_rows: <N>` — bounds enforced on the raw \
6449 stream BEFORE parsing.",
6450 ),
6451 line: bound.line,
6452 column: bound.column,
6453 ..Default::default()
6454 });
6455 }
6456 }
6457 if self.check(TokenType::Comma) {
6458 self.advance();
6459 }
6460 }
6461 self.consume(TokenType::RBrace)?;
6462 }
6463 other => {
6464 return Err(ParseError {
6465 message: format!(
6466 "Unknown entry `{other}` in ingest body. The closed \
6467 grammar is `format: csv|json` and \
6468 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6469 ),
6470 line: entry.line,
6471 column: entry.column,
6472 ..Default::default()
6473 });
6474 }
6475 }
6476 }
6477 self.consume(TokenType::RBrace)?;
6478 }
6479 Ok(FlowStep::Ingest(IngestStep {
6480 source,
6481 target,
6482 format,
6483 max_bytes,
6484 max_rows,
6485 loc: Loc {
6486 line: tok.line,
6487 column: tok.column,
6488 },
6489 }))
6490 }
6491
6492 fn at_navigate_field(&self) -> bool {
6499 const FIELDS: &[&str] = &[
6500 "corpus", "query", "trail", "as", "from", "budget", "where",
6508 "depth", "recall",
6509 ];
6510 self.field_ahead(FIELDS)
6511 }
6512
6513 fn at_drill_field(&self) -> bool {
6515 const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6518 self.field_ahead(FIELDS)
6519 }
6520
6521 fn field_ahead(&self, names: &[&str]) -> bool {
6523 let cur = self.current();
6524 if !names.contains(&cur.value.as_str()) {
6525 return false;
6526 }
6527 self.tokens
6528 .get(self.pos + 1)
6529 .is_some_and(|t| t.ttype == TokenType::Colon)
6530 }
6531
6532 fn parse_config_key(&mut self) -> Result<String, ParseError> {
6542 if self.check(TokenType::StringLit) {
6543 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6544 }
6545 let scheme = self.consume_any_ident_or_kw()?.value.clone();
6546 if self.check(TokenType::Colon) {
6547 self.advance();
6548 let key = self.consume_any_ident_or_kw()?.value.clone();
6549 return Ok(format!("{scheme}:{key}"));
6550 }
6551 Ok(scheme)
6552 }
6553
6554 fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6559 if self.check(TokenType::StringLit) {
6560 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6561 }
6562 Ok(self.consume_any_ident_or_kw()?.value.clone())
6563 }
6564
6565 fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6566 let tok = self.current().clone();
6567 self.advance();
6568 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6569 let mut node = NavigateStep {
6570 depth: None,
6571 pix_name,
6572 corpus_name: String::new(),
6573 query_expr: String::new(),
6574 trail_enabled: false,
6575 output_name: String::new(),
6576 seed: String::new(),
6577 budget: None,
6578 where_expr: String::new(),
6579 loc: Loc {
6580 line: tok.line,
6581 column: tok.column,
6582 },
6583 };
6584 if !self.check(TokenType::LBrace) {
6598 while self.at_navigate_field() {
6599 let f = self.current().value.clone();
6600 self.advance();
6601 self.consume(TokenType::Colon)?;
6602 match f.as_str() {
6603 "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6604 "query" => node.query_expr = self.parse_pix_value()?,
6605 "trail" => {
6606 let v = self.consume_any_ident_or_kw()?.value;
6607 node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6608 }
6609 "output" | "as" => {
6610 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6611 }
6612 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6613 "budget" => node.budget = self.parse_optional_int(),
6614 "where" => node.where_expr = self.parse_pix_value()?,
6615 "depth" => node.depth = self.parse_optional_int(),
6616 "recall" => {
6624 let mode = self.consume_any_ident_or_kw()?.value.clone();
6625 if node.seed.is_empty() {
6626 node.seed = format!("recall:{mode}");
6627 }
6628 }
6629 _ => self.skip_value(),
6630 }
6631 }
6632 }
6633 if self.check(TokenType::LBrace) {
6634 self.advance();
6635 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6636 let f = self.current().value.clone();
6637 self.advance();
6638 if self.check(TokenType::Colon) {
6639 self.advance();
6640 match f.as_str() {
6641 "corpus" => {
6642 node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6643 }
6644 "query" => node.query_expr = self.parse_pix_value()?,
6645 "trail" => {
6646 let v = self.consume_any_ident_or_kw()?.value;
6647 node.trail_enabled =
6648 matches!(v.as_str(), "true" | "enabled" | "on");
6649 }
6650 "output" | "as" => {
6651 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6652 }
6653 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6655 "budget" => node.budget = self.parse_optional_int(),
6656 "where" => {
6664 node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6665 }
6666 _ => self.skip_value(),
6667 }
6668 }
6669 }
6670 if self.check(TokenType::RBrace) {
6671 self.advance();
6672 }
6673 }
6674 Ok(FlowStep::Navigate(node))
6675 }
6676
6677 fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6678 let tok = self.current().clone();
6679 self.advance();
6680 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6681 let mut node = DrillStep {
6682 pix_name,
6683 subtree_path: String::new(),
6684 query_expr: String::new(),
6685 output_name: String::new(),
6686 loc: Loc {
6687 line: tok.line,
6688 column: tok.column,
6689 },
6690 };
6691 if self.current().value == "into" {
6695 self.advance();
6696 node.subtree_path = if self.check(TokenType::StringLit) {
6701 self.consume(TokenType::StringLit)?.value.clone()
6702 } else {
6703 self.parse_dotted_identifier()?
6704 };
6705 }
6706 if !self.check(TokenType::LBrace) {
6707 while self.at_drill_field() {
6708 let f = self.current().value.clone();
6709 self.advance();
6710 self.consume(TokenType::Colon)?;
6711 match f.as_str() {
6712 "subtree" | "path" => {
6713 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6714 }
6715 "query" => node.query_expr = self.parse_pix_value()?,
6716 "output" | "as" => {
6717 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6718 }
6719 _ => self.skip_value(),
6720 }
6721 }
6722 }
6723 if self.check(TokenType::LBrace) {
6724 self.advance();
6725 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6726 let f = self.current().value.clone();
6727 self.advance();
6728 if self.check(TokenType::Colon) {
6729 self.advance();
6730 match f.as_str() {
6731 "subtree" | "path" => {
6732 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6733 }
6734 "query" => node.query_expr = self.parse_pix_value()?,
6735 "output" | "as" => {
6736 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6737 }
6738 _ => self.skip_value(),
6739 }
6740 }
6741 }
6742 if self.check(TokenType::RBrace) {
6743 self.advance();
6744 }
6745 }
6746 Ok(FlowStep::Drill(node))
6747 }
6748
6749 fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6750 let tok = self.current().clone();
6751 self.advance();
6752 let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6753 let mut output = String::new();
6754 if self.check(TokenType::Arrow) {
6755 self.advance();
6756 output = self.consume_any_ident_or_kw()?.value.clone();
6757 }
6758 Ok(FlowStep::Corroborate(CorroborateStep {
6759 navigate_ref: nav_ref,
6760 output_name: output,
6761 loc: Loc {
6762 line: tok.line,
6763 column: tok.column,
6764 },
6765 }))
6766 }
6767
6768 fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6769 let tok = self.current().clone();
6770 self.advance();
6771 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6775 (self.consume(TokenType::StringLit)?.value.clone(), false)
6776 } else {
6777 (self.consume_any_ident_or_kw()?.value.clone(), true)
6778 };
6779 let mut alias = String::new();
6780 if !self.at_declaration_start()
6781 && !self.check(TokenType::RBrace)
6782 && !self.check(TokenType::LBrace)
6783 {
6784 let next = self.current().clone();
6785 if next.value == "as" || next.ttype == TokenType::As {
6786 self.advance();
6787 alias = self.consume_any_ident_or_kw()?.value.clone();
6788 }
6789 }
6790 let body = self.parse_listener_body()?;
6794 Ok(FlowStep::Listen(ListenStep {
6795 channel,
6796 channel_is_ref,
6797 event_alias: alias,
6798 body,
6799 loc: Loc {
6800 line: tok.line,
6801 column: tok.column,
6802 },
6803 }))
6804 }
6805
6806 fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6811 let mut body = Vec::new();
6812 if self.check(TokenType::LBrace) {
6813 self.advance(); while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6815 body.push(self.parse_flow_step()?);
6816 }
6817 self.consume(TokenType::RBrace)?;
6818 }
6819 Ok(body)
6820 }
6821
6822 fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6829 let tok = self.current().clone();
6830 self.advance();
6831 if self.check(TokenType::From) || self.current().value == "from" {
6834 self.advance();
6835 }
6836 let store = self.consume_any_ident_or_kw()?.value.clone();
6837 let mut where_expr = String::new();
6838 let mut alias = String::new();
6839 let mut order_by = String::new();
6840 let mut limit_expr = String::new();
6841 let mut aggregate = String::new();
6842 let mut group_by = String::new();
6843 let mut cache = String::new();
6844 loop {
6851 match self.current().value.as_str() {
6852 "where" if !self.check(TokenType::LBrace) => {
6853 self.advance();
6854 where_expr = self.consume(TokenType::StringLit)?.value.clone();
6855 }
6856 "as" => {
6857 self.advance();
6858 alias = self.consume_any_ident_or_kw()?.value.clone();
6859 }
6860 _ => break,
6861 }
6862 }
6863 if self.check(TokenType::LBrace) {
6864 self.advance();
6865 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6866 let f = self.current().value.clone();
6867 self.advance();
6868 if self.check(TokenType::Colon) {
6869 self.advance();
6870 match f.as_str() {
6871 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6872 "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6873 "order_by" => {
6876 order_by = self.consume(TokenType::StringLit)?.value.clone()
6877 }
6878 "limit" => {
6883 let t = self.current().clone();
6884 match t.ttype {
6885 TokenType::Integer | TokenType::StringLit => {
6886 limit_expr = t.value.clone();
6887 self.advance();
6888 }
6889 _ => self.skip_value(),
6890 }
6891 }
6892 "aggregate" => {
6899 aggregate = self.consume(TokenType::StringLit)?.value.clone()
6900 }
6901 "group_by" => {
6902 group_by = self.consume(TokenType::StringLit)?.value.clone()
6903 }
6904 "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
6910 _ => self.skip_value(),
6911 }
6912 }
6913 }
6914 if self.check(TokenType::RBrace) {
6915 self.advance();
6916 }
6917 }
6918 Ok(FlowStep::Retrieve(RetrieveStep {
6919 store_name: store,
6920 where_expr,
6921 alias,
6922 order_by,
6923 limit_expr,
6924 aggregate,
6925 group_by,
6926 cache,
6927 loc: Loc {
6928 line: tok.line,
6929 column: tok.column,
6930 },
6931 }))
6932 }
6933
6934 fn parse_store_where_step(
6947 &mut self,
6948 ) -> Result<(Loc, String, String), ParseError> {
6949 let tok = self.current().clone();
6950 self.advance(); let store = if self.at_declaration_start()
6952 || self.check(TokenType::RBrace)
6953 || self.check(TokenType::Eof)
6954 {
6955 String::new()
6956 } else {
6957 self.consume_any_ident_or_kw()?.value.clone()
6958 };
6959 let mut where_expr = String::new();
6960 if self.check(TokenType::LBrace) {
6961 self.advance();
6962 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6963 let field = self.current().value.clone();
6964 self.advance();
6965 if self.check(TokenType::Colon) {
6966 self.advance();
6967 match field.as_str() {
6968 "where" => {
6969 where_expr =
6970 self.consume(TokenType::StringLit)?.value.clone()
6971 }
6972 _ => self.skip_value(),
6973 }
6974 }
6975 }
6976 if self.check(TokenType::RBrace) {
6977 self.advance();
6978 }
6979 }
6980 Ok((
6981 Loc {
6982 line: tok.line,
6983 column: tok.column,
6984 },
6985 store,
6986 where_expr,
6987 ))
6988 }
6989
6990 fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7009 let tok = self.current().clone();
7010 self.advance(); if self.current().value == "into" && !self.check(TokenType::LBrace) {
7014 self.advance();
7015 }
7016 let store = if self.at_declaration_start()
7017 || self.check(TokenType::LBrace)
7018 || self.check(TokenType::RBrace)
7019 || self.check(TokenType::Eof)
7020 {
7021 String::new()
7022 } else {
7023 self.consume_any_ident_or_kw()?.value.clone()
7024 };
7025 let mut fields: Vec<(String, String)> = Vec::new();
7026 if self.check(TokenType::LBrace) {
7027 self.advance();
7028 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7029 let col = self.current().value.clone();
7030 self.advance();
7031 if self.check(TokenType::Colon) {
7032 self.advance();
7033 let value = if self.check(TokenType::StringLit) {
7034 self.consume(TokenType::StringLit)?.value.clone()
7035 } else if self.check(TokenType::RBrace)
7036 || self.check(TokenType::Eof)
7037 || self.check(TokenType::Colon)
7038 {
7039 String::new()
7040 } else {
7041 let v = self.current().clone();
7042 self.advance();
7043 v.value.clone()
7044 };
7045 fields.push((col, value));
7046 }
7047 }
7048 if self.check(TokenType::RBrace) {
7049 self.advance();
7050 }
7051 }
7052 Ok(FlowStep::Persist(PersistStep {
7053 store_name: store,
7054 fields,
7055 loc: Loc {
7056 line: tok.line,
7057 column: tok.column,
7058 },
7059 }))
7060 }
7061
7062 fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7076 let tok = self.current().clone();
7077 self.advance(); let store = if self.at_declaration_start()
7079 || self.check(TokenType::LBrace)
7080 || self.check(TokenType::RBrace)
7081 || self.check(TokenType::Eof)
7082 {
7083 String::new()
7084 } else {
7085 self.consume_any_ident_or_kw()?.value.clone()
7086 };
7087 let mut where_expr = String::new();
7088 let mut fields: Vec<(String, String)> = Vec::new();
7089 if self.check(TokenType::LBrace) {
7090 self.advance();
7091 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7092 let key = self.current().value.clone();
7093 self.advance();
7094 if self.check(TokenType::Colon) {
7095 self.advance();
7096 if key == "where" {
7097 where_expr =
7098 self.consume(TokenType::StringLit)?.value.clone();
7099 } else {
7100 let value = if self.check(TokenType::StringLit) {
7101 self.consume(TokenType::StringLit)?.value.clone()
7102 } else if self.check(TokenType::RBrace)
7103 || self.check(TokenType::Eof)
7104 || self.check(TokenType::Colon)
7105 {
7106 String::new()
7107 } else {
7108 let v = self.current().clone();
7109 self.advance();
7110 v.value.clone()
7111 };
7112 fields.push((key, value));
7113 }
7114 }
7115 }
7116 if self.check(TokenType::RBrace) {
7117 self.advance();
7118 }
7119 }
7120 Ok(FlowStep::Mutate(MutateStep {
7121 store_name: store,
7122 where_expr,
7123 fields,
7124 loc: Loc {
7125 line: tok.line,
7126 column: tok.column,
7127 },
7128 }))
7129 }
7130
7131 fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7134 let tok = self.consume(TokenType::Agent)?;
7135 let name = self.consume(TokenType::Identifier)?.value;
7136 let mut node = AgentDefinition {
7137 name,
7138 goal: String::new(),
7139 tools: Vec::new(),
7140 memory_ref: String::new(),
7141 strategy: String::new(),
7142 on_stuck: String::new(),
7143 shield_ref: String::new(),
7144 max_iterations: None,
7145 max_tokens: None,
7146 max_time: String::new(),
7147 max_cost: None,
7148 return_type: String::new(),
7149 body: Vec::new(),
7150 loc: Loc {
7151 line: tok.line,
7152 column: tok.column,
7153 },
7154 leading_trivia: Vec::new(),
7155 trailing_trivia: Vec::new(),
7156 };
7157 if self.check(TokenType::LParen) {
7163 let mut depth = 0usize;
7164 while !self.check(TokenType::Eof) {
7165 if self.check(TokenType::LParen) {
7166 depth += 1;
7167 } else if self.check(TokenType::RParen) {
7168 depth -= 1;
7169 if depth == 0 {
7170 self.advance();
7171 break;
7172 }
7173 }
7174 self.advance();
7175 }
7176 }
7177 if self.check(TokenType::Arrow) {
7178 self.advance();
7179 node.return_type = self.parse_output_type_string()?;
7180 }
7181 self.consume(TokenType::LBrace)?;
7182 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7188 if self.check(TokenType::Step) {
7189 let step = self.parse_step()?;
7190 node.body.push(step);
7191 continue;
7192 }
7193 let field = self.current().clone();
7194 let field_name = field.value.clone();
7195 self.advance();
7196 if !self.check(TokenType::Colon) {
7197 return Err(self.error(&format!(
7198 "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7199 `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7200 goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7201 max_tokens, max_time, max_cost, return",
7202 node.name
7203 )));
7204 }
7205 self.advance();
7206 match field_name.as_str() {
7207 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7208 "tools" => node.tools = self.parse_bracketed_identifiers()?,
7209 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7210 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7211 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7212 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7213 "max_iterations" => node.max_iterations = self.parse_optional_int(),
7214 "max_tokens" => node.max_tokens = self.parse_optional_int(),
7215 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7216 "max_cost" => node.max_cost = self.parse_optional_float(),
7217 "return" => node.return_type = self.parse_output_type_string()?,
7218 other => {
7219 return Err(self.error(&format!(
7220 "unknown agent field `{other}` in `agent {}` — the agent block is a \
7221 closed catalog; valid fields: goal, tools, memory, strategy, \
7222 on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7223 return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7224 node.name
7225 )));
7226 }
7227 }
7228 }
7229 self.consume(TokenType::RBrace)?;
7230 Ok(node)
7231 }
7232
7233 fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7238 let tok = self.consume(TokenType::Extension)?;
7239 let name = self.consume(TokenType::Identifier)?.value;
7240 let mut node = ExtensionDefinition {
7241 name,
7242 category: String::new(),
7243 members: Vec::new(),
7244 loc: Loc {
7245 line: tok.line,
7246 column: tok.column,
7247 },
7248 leading_trivia: Vec::new(),
7249 trailing_trivia: Vec::new(),
7250 };
7251 self.consume(TokenType::LBrace)?;
7252 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7253 let field_name = self.current().value.clone();
7254 self.advance();
7255 if self.check(TokenType::Colon) {
7256 self.advance();
7257 match field_name.as_str() {
7258 "category" => {
7259 node.category = self.consume_any_ident_or_kw()?.value.clone()
7260 }
7261 "members" => node.members = self.parse_extension_members()?,
7262 _ => self.skip_value(),
7263 }
7264 } else if self.check(TokenType::LBrace) {
7265 self.skip_braced_block()?;
7266 }
7267 }
7268 self.consume(TokenType::RBrace)?;
7269 Ok(node)
7270 }
7271
7272 fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7276 let mut members = Vec::new();
7277 self.consume(TokenType::LBracket)?;
7278 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7279 let name_tok = self.consume(TokenType::StringLit)?;
7280 let mut member = ExtensionMember {
7281 name: name_tok.value.clone(),
7282 semantics: None,
7283 default_confidence: None,
7284 loc: Loc {
7285 line: name_tok.line,
7286 column: name_tok.column,
7287 },
7288 };
7289 if self.check(TokenType::Colon) {
7291 self.advance();
7292 self.consume(TokenType::LBrace)?;
7293 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7294 let mkey = self.current().value.clone();
7295 self.advance();
7296 if self.check(TokenType::Colon) {
7297 self.advance();
7298 match mkey.as_str() {
7299 "semantics" => {
7300 member.semantics =
7301 Some(self.consume(TokenType::StringLit)?.value.clone())
7302 }
7303 "default_confidence" => {
7304 member.default_confidence = self.parse_optional_float()
7305 }
7306 _ => self.skip_value(),
7307 }
7308 }
7309 if self.check(TokenType::Comma) {
7310 self.advance();
7311 }
7312 }
7313 self.consume(TokenType::RBrace)?;
7314 }
7315 members.push(member);
7316 if self.check(TokenType::Comma) {
7317 self.advance();
7318 }
7319 }
7320 self.consume(TokenType::RBracket)?;
7321 Ok(members)
7322 }
7323
7324 fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7327 let tok = self.consume(TokenType::Window)?;
7328 let name = self.consume(TokenType::Identifier)?.value;
7329 let mut node = WindowDefinition {
7330 name,
7331 timezone: String::new(),
7332 allow: Vec::new(),
7333 exclude: Vec::new(),
7334 on_outside: String::new(),
7335 loc: Loc {
7336 line: tok.line,
7337 column: tok.column,
7338 },
7339 leading_trivia: Vec::new(),
7340 trailing_trivia: Vec::new(),
7341 };
7342 self.consume(TokenType::LBrace)?;
7343 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7344 let field_name = self.consume_any_ident_or_kw()?.value;
7345 self.consume(TokenType::Colon)?;
7346 match field_name.as_str() {
7347 "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7348 "allow" => node.allow = self.parse_window_allow()?,
7349 "exclude" => node.exclude = self.parse_window_exclude()?,
7350 "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7351 _ => self.skip_value(),
7352 }
7353 }
7354 self.consume(TokenType::RBrace)?;
7355 Ok(node)
7356 }
7357
7358 fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7360 self.consume(TokenType::LBracket)?;
7361 let mut spans = Vec::new();
7362 if !self.check(TokenType::RBracket) {
7363 spans.push(self.parse_window_span()?);
7364 while self.check(TokenType::Comma) {
7365 self.advance();
7366 if self.check(TokenType::RBracket) {
7367 break; }
7369 spans.push(self.parse_window_span()?);
7370 }
7371 }
7372 self.consume(TokenType::RBracket)?;
7373 Ok(spans)
7374 }
7375
7376 fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7380 self.consume(TokenType::LBracket)?;
7381 let mut dates = Vec::new();
7382 if !self.check(TokenType::RBracket) {
7383 dates.push(self.consume(TokenType::StringLit)?.value);
7384 while self.check(TokenType::Comma) {
7385 self.advance();
7386 if self.check(TokenType::RBracket) {
7387 break; }
7389 dates.push(self.consume(TokenType::StringLit)?.value);
7390 }
7391 }
7392 self.consume(TokenType::RBracket)?;
7393 Ok(dates)
7394 }
7395
7396 fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7398 let tok = self.consume(TokenType::LBrace)?;
7399 let mut span = WindowSpan {
7400 day_start: String::new(),
7401 day_end: String::new(),
7402 hour_start: 0,
7403 hour_end: 0,
7404 loc: Loc {
7405 line: tok.line,
7406 column: tok.column,
7407 },
7408 };
7409 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7410 let field = self.consume_any_ident_or_kw()?.value;
7411 self.consume(TokenType::Colon)?;
7412 match field.as_str() {
7413 "days" => {
7414 span.day_start = self.consume_any_ident_or_kw()?.value;
7415 self.consume(TokenType::DotDot)?;
7416 span.day_end = self.consume_any_ident_or_kw()?.value;
7417 }
7418 "hours" => {
7419 span.hour_start = self.consume_number()? as i64;
7420 self.consume(TokenType::DotDot)?;
7421 span.hour_end = self.consume_number()? as i64;
7422 }
7423 _ => self.skip_value(),
7424 }
7425 if self.check(TokenType::Comma) {
7426 self.advance();
7427 }
7428 }
7429 self.consume(TokenType::RBrace)?;
7430 Ok(span)
7431 }
7432
7433 fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7434 let tok = self.consume(TokenType::Shield)?;
7435 let name = self.consume(TokenType::Identifier)?.value;
7436 let mut node = ShieldDefinition {
7437 name,
7438 scan: Vec::new(),
7439 strategy: String::new(),
7440 on_breach: String::new(),
7441 severity: String::new(),
7442 quarantine: String::new(),
7443 max_retries: None,
7444 confidence_threshold: None,
7445 allow_tools: Vec::new(),
7446 deny_tools: Vec::new(),
7447 sandbox: None,
7448 redact: Vec::new(),
7449 log: String::new(),
7450 deflect_message: String::new(),
7451 taint: String::new(),
7452 compliance: Vec::new(),
7453 sign: String::new(),
7454 unknown_fields: Vec::new(),
7455 loc: Loc {
7456 line: tok.line,
7457 column: tok.column,
7458 },
7459 leading_trivia: Vec::new(),
7460 trailing_trivia: Vec::new(),
7461 };
7462 self.consume(TokenType::LBrace)?;
7463 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7464 let field_name = self.current().value.clone();
7465 let field_loc = Loc {
7466 line: self.current().line,
7467 column: self.current().column,
7468 };
7469 self.advance();
7470 if self.check(TokenType::Colon) {
7471 self.advance();
7472 match field_name.as_str() {
7473 "scan" => node.scan = self.parse_bracketed_identifiers()?,
7474 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7475 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7476 "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7477 "quarantine" => {
7478 node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7479 }
7480 "max_retries" => node.max_retries = self.parse_optional_int(),
7481 "confidence_threshold" => {
7482 node.confidence_threshold = self.parse_optional_float()
7483 }
7484 "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7485 "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7486 "sandbox" => {
7487 node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7488 }
7489 "redact" => node.redact = self.parse_bracketed_identifiers()?,
7490 "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7491 "deflect_message" => {
7492 node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7493 }
7494 "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7495 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7497 "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7500 _ => {
7504 node.unknown_fields.push((field_name.clone(), field_loc));
7505 self.skip_value()
7506 }
7507 }
7508 } else if self.check(TokenType::LBrace) {
7509 self.skip_braced_block()?;
7510 }
7511 }
7512 self.consume(TokenType::RBrace)?;
7513 Ok(node)
7514 }
7515
7516 fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7517 let tok = self.consume(TokenType::Pix)?;
7518 let name = self.consume(TokenType::Identifier)?.value;
7519 let mut node = PixDefinition {
7520 name,
7521 source: String::new(),
7522 depth: None,
7523 branching: None,
7524 model: String::new(),
7525 loc: Loc {
7526 line: tok.line,
7527 column: tok.column,
7528 },
7529 leading_trivia: Vec::new(),
7530 trailing_trivia: Vec::new(),
7531 };
7532 self.consume(TokenType::LBrace)?;
7533 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7534 let field_name = self.current().value.clone();
7535 self.advance();
7536 if self.check(TokenType::Colon) {
7537 self.advance();
7538 match field_name.as_str() {
7539 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7540 "depth" => node.depth = self.parse_optional_int(),
7541 "branching" => node.branching = self.parse_optional_int(),
7542 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7543 _ => self.skip_value(),
7544 }
7545 } else if self.check(TokenType::LBrace) {
7546 self.skip_braced_block()?;
7547 }
7548 }
7549 self.consume(TokenType::RBrace)?;
7550 Ok(node)
7551 }
7552
7553 fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7559 let tok = self.consume(TokenType::Ledger)?;
7560 let name = self.consume(TokenType::Identifier)?.value;
7561 let mut node = LedgerDefinition {
7562 name,
7563 source: String::new(),
7564 depth: None,
7565 branching: None,
7566 model: String::new(),
7567 loc: Loc {
7568 line: tok.line,
7569 column: tok.column,
7570 },
7571 leading_trivia: Vec::new(),
7572 trailing_trivia: Vec::new(),
7573 };
7574 self.consume(TokenType::LBrace)?;
7575 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7576 let field_name = self.current().value.clone();
7577 self.advance();
7578 if self.check(TokenType::Colon) {
7579 self.advance();
7580 match field_name.as_str() {
7581 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7582 "depth" => node.depth = self.parse_optional_int(),
7583 "branching" => node.branching = self.parse_optional_int(),
7584 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7585 _ => self.skip_value(),
7586 }
7587 } else if self.check(TokenType::LBrace) {
7588 self.skip_braced_block()?;
7589 }
7590 }
7591 self.consume(TokenType::RBrace)?;
7592 Ok(node)
7593 }
7594
7595 fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7596 let tok = self.consume(TokenType::Psyche)?;
7597 let name = self.consume(TokenType::Identifier)?.value;
7598 let mut node = PsycheDefinition {
7599 name,
7600 dimensions: Vec::new(),
7601 manifold_noise: None,
7602 manifold_momentum: None,
7603 safety_constraints: Vec::new(),
7604 quantum_enabled: None,
7605 inference_mode: String::new(),
7606 loc: Loc {
7607 line: tok.line,
7608 column: tok.column,
7609 },
7610 leading_trivia: Vec::new(),
7611 trailing_trivia: Vec::new(),
7612 };
7613 self.consume(TokenType::LBrace)?;
7614 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7615 let field_name = self.current().value.clone();
7616 self.advance();
7617 if self.check(TokenType::Colon) {
7618 self.advance();
7619 match field_name.as_str() {
7620 "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7621 "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7622 "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7623 "safety_constraints" | "safety" => {
7628 node.safety_constraints = self.parse_bracketed_identifiers()?
7629 }
7630 "quantum_enabled" => {
7631 node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7632 }
7633 "inference_mode" => {
7634 node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7635 }
7636 _ => self.skip_value(),
7637 }
7638 } else if self.check(TokenType::LBrace) {
7639 self.skip_braced_block()?;
7640 }
7641 }
7642 self.consume(TokenType::RBrace)?;
7643 Ok(node)
7644 }
7645
7646 fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7647 let tok = self.consume(TokenType::Corpus)?;
7648 let name = self.consume(TokenType::Identifier)?.value;
7649 let mut node = CorpusDefinition {
7650 name,
7651 documents: Vec::new(),
7652 relations: Vec::new(),
7653 adaptive: false,
7654 mcp_server: String::new(),
7655 mcp_resource_uri: String::new(),
7656 store_source: None,
7657 loc: Loc {
7658 line: tok.line,
7659 column: tok.column,
7660 },
7661 leading_trivia: Vec::new(),
7662 trailing_trivia: Vec::new(),
7663 };
7664 let mut dynamic = false;
7668 if self.check(TokenType::From) {
7669 self.advance();
7670 if self.check(TokenType::AxonStore) {
7671 self.advance();
7672 dynamic = true;
7673 } else {
7674 self.consume(TokenType::Mcp)?;
7675 self.consume(TokenType::LParen)?;
7676 node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7677 self.consume(TokenType::Comma)?;
7678 node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7679 self.consume(TokenType::RParen)?;
7680 return Ok(node);
7681 }
7682 }
7683 self.consume(TokenType::LBrace)?;
7684 let mut src = CorpusStoreSource {
7687 doc_store: String::new(),
7688 doc_id_col: String::new(),
7689 doc_title_col: String::new(),
7690 edge_store: String::new(),
7691 edge_from_col: String::new(),
7692 edge_to_col: String::new(),
7693 edge_type_col: String::new(),
7694 edge_weight_col: String::new(),
7695 loc: Loc {
7696 line: tok.line,
7697 column: tok.column,
7698 },
7699 };
7700 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7701 let field_name = self.current().value.clone();
7702 self.advance();
7703 if self.check(TokenType::Colon) {
7704 self.advance();
7705 match field_name.as_str() {
7706 "documents" if dynamic => {
7708 let (store, cols) = self.parse_corpus_store_mapping(2)?;
7709 src.doc_store = store;
7710 src.doc_id_col = cols[0].clone();
7711 src.doc_title_col = cols[1].clone();
7712 }
7713 "documents" => node.documents = self.parse_bracketed_identifiers()?,
7714 "relations" if dynamic => {
7716 let (store, cols) = self.parse_corpus_store_mapping(4)?;
7717 src.edge_store = store;
7718 src.edge_from_col = cols[0].clone();
7719 src.edge_to_col = cols[1].clone();
7720 src.edge_type_col = cols[2].clone();
7721 src.edge_weight_col = cols[3].clone();
7722 }
7723 "relations" => node.relations = self.parse_corpus_relations()?,
7725 "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7727 _ => self.skip_value(),
7728 }
7729 } else if self.check(TokenType::LBrace) {
7730 self.skip_braced_block()?;
7731 }
7732 }
7733 self.consume(TokenType::RBrace)?;
7734 if dynamic {
7735 node.store_source = Some(src);
7736 }
7737 Ok(node)
7738 }
7739
7740 fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7747 let store = self.consume(TokenType::Identifier)?.value.clone();
7748 self.consume(TokenType::LParen)?;
7749 let mut cols = Vec::with_capacity(n);
7750 for i in 0..n {
7751 if i > 0 {
7752 self.consume(TokenType::Comma)?;
7753 }
7754 cols.push(self.consume_any_ident_or_kw()?.value.clone());
7755 }
7756 self.consume(TokenType::RParen)?;
7757 Ok((store, cols))
7758 }
7759
7760 fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7766 let mut out = Vec::new();
7767 self.consume(TokenType::LBracket)?;
7768 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7769 if self.check(TokenType::Comma) {
7770 self.advance();
7771 continue;
7772 }
7773 let tok = self.current().clone();
7774 let etype = self.consume_any_ident_or_kw()?.value.clone();
7775 self.consume(TokenType::LParen)?;
7776 let from = self.consume_any_ident_or_kw()?.value.clone();
7777 self.consume(TokenType::Comma)?;
7778 let to = self.consume_any_ident_or_kw()?.value.clone();
7779 self.consume(TokenType::Comma)?;
7780 let weight = self.consume_number()?;
7781 self.consume(TokenType::RParen)?;
7782 out.push(CorpusRelation {
7783 etype,
7784 from,
7785 to,
7786 weight,
7787 loc: Loc { line: tok.line, column: tok.column },
7788 });
7789 }
7790 self.consume(TokenType::RBracket)?;
7791 Ok(out)
7792 }
7793
7794 fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7811 let tok = self.consume(TokenType::Dataspace)?;
7812 let name = self.consume(TokenType::Identifier)?.value;
7813 let mut node = DataspaceDefinition {
7814 name,
7815 columns: Vec::new(),
7816 loc: Loc {
7817 line: tok.line,
7818 column: tok.column,
7819 },
7820 leading_trivia: Vec::new(),
7821 trailing_trivia: Vec::new(),
7822 };
7823 if self.check(TokenType::LBrace) {
7824 self.consume(TokenType::LBrace)?;
7825 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7826 let entry = self.current().clone();
7827 if entry.value != "column" {
7828 return Err(ParseError {
7829 message: format!(
7830 "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7831 declares its columnar schema: `column <name>: <Type>` \
7832 (one per line, over the closed type catalog — \
7833 Text, Int, Float, Bool, Timestamp, Json).",
7834 entry.value, node.name
7835 ),
7836 line: entry.line,
7837 column: entry.column,
7838 ..Default::default()
7839 });
7840 }
7841 self.advance(); let col_tok = self.current().clone();
7843 let col_name = self.consume_any_ident_or_kw()?.value.clone();
7844 self.consume(TokenType::Colon)?;
7845 let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7846 node.columns.push(crate::ast::DataspaceColumn {
7847 name: col_name,
7848 declared_type,
7849 loc: Loc {
7850 line: col_tok.line,
7851 column: col_tok.column,
7852 },
7853 });
7854 }
7855 self.consume(TokenType::RBrace)?;
7856 }
7857 Ok(node)
7858 }
7859
7860 fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
7861 let tok = self.consume(TokenType::Ots)?;
7862 let name = self.consume(TokenType::Identifier)?.value;
7863 let mut node = OtsDefinition {
7864 name,
7865 teleology: String::new(),
7866 homotopy_search: String::new(),
7867 loss_function: String::new(),
7868 loc: Loc {
7869 line: tok.line,
7870 column: tok.column,
7871 },
7872 leading_trivia: Vec::new(),
7873 trailing_trivia: Vec::new(),
7874 };
7875 if self.check(TokenType::Lt) {
7877 while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
7878 self.advance();
7879 }
7880 if self.check(TokenType::Gt) {
7881 self.advance();
7882 }
7883 }
7884 self.consume(TokenType::LBrace)?;
7885 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7886 let field_name = self.current().value.clone();
7887 self.advance();
7888 if self.check(TokenType::Colon) {
7889 self.advance();
7890 match field_name.as_str() {
7891 "teleology" => {
7892 node.teleology = self.consume(TokenType::StringLit)?.value.clone()
7893 }
7894 "homotopy_search" => {
7895 node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
7896 }
7897 "loss_function" => {
7903 node.loss_function = if self.check(TokenType::StringLit) {
7904 self.consume(TokenType::StringLit)?.value.clone()
7905 } else {
7906 self.consume_any_ident_or_kw()?.value.clone()
7907 }
7908 }
7909 _ => self.skip_value(),
7910 }
7911 } else if self.check(TokenType::LBrace) {
7912 self.skip_braced_block()?;
7913 }
7914 }
7915 self.consume(TokenType::RBrace)?;
7916 Ok(node)
7917 }
7918
7919 fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
7920 let tok = self.consume(TokenType::Mandate)?;
7921 let name = self.consume(TokenType::Identifier)?.value;
7922 let mut node = MandateDefinition {
7923 name,
7924 constraint: String::new(),
7925 kp: None,
7926 ki: None,
7927 kd: None,
7928 tolerance: None,
7929 max_steps: None,
7930 drift_bound: None,
7931 lipschitz: None,
7932 on_violation: String::new(),
7933 loc: Loc {
7934 line: tok.line,
7935 column: tok.column,
7936 },
7937 leading_trivia: Vec::new(),
7938 trailing_trivia: Vec::new(),
7939 };
7940 self.consume(TokenType::LBrace)?;
7941 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7942 let field_name = self.current().value.clone();
7943 self.advance();
7944 if self.check(TokenType::Colon) {
7945 self.advance();
7946 match field_name.as_str() {
7947 "constraint" => {
7948 node.constraint = self.consume(TokenType::StringLit)?.value.clone()
7949 }
7950 "kp" | "Kp" => node.kp = self.parse_optional_float(),
7951 "ki" | "Ki" => node.ki = self.parse_optional_float(),
7952 "kd" | "Kd" => node.kd = self.parse_optional_float(),
7953 "max_steps" => node.max_steps = self.parse_optional_int(),
7954 "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
7959 "on_violation" => {
7960 node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
7961 }
7962 _ => self.skip_value(),
7963 }
7964 } else if self.check(TokenType::LBrace) {
7965 if field_name == "pid" {
7975 self.parse_pid_block(&mut node)?;
7976 } else if field_name == "stability" {
7977 self.parse_stability_block(&mut node)?;
7978 } else {
7979 self.skip_braced_block()?;
7980 }
7981 }
7982 }
7983 self.consume(TokenType::RBrace)?;
7984 Ok(node)
7985 }
7986
7987 fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8010 self.consume(TokenType::LBrace)?;
8011 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8012 let key_token = self.current().clone();
8013 let key = key_token.value.clone();
8014 self.advance();
8015 if self.check(TokenType::Colon) {
8016 self.advance();
8017 match key.as_str() {
8018 "kp" | "Kp" => node.kp = self.parse_optional_float(),
8019 "ki" | "Ki" => node.ki = self.parse_optional_float(),
8020 "kd" | "Kd" => node.kd = self.parse_optional_float(),
8021 _ => {
8022 return Err(ParseError {
8023 message: format!(
8024 "`{key}` is not a gain of the PID controller. The block accepts \
8025 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8026 Skipping what it does not recognise would let a typo drop a \
8027 gain, and the stability band is computed from all three."
8028 ),
8029 line: key_token.line,
8030 column: key_token.column,
8031 ..Default::default()
8032 });
8033 }
8034 }
8035 }
8036 if self.check(TokenType::Comma) {
8037 self.advance();
8038 }
8039 }
8040 self.consume(TokenType::RBrace)?;
8041 Ok(())
8042 }
8043
8044 fn parse_stability_block(
8060 &mut self,
8061 node: &mut MandateDefinition,
8062 ) -> Result<(), ParseError> {
8063 let open = self.consume(TokenType::LBrace)?;
8064 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8065 let key_token = self.current().clone();
8066 let key = key_token.value.clone();
8067 self.advance();
8068 if self.check(TokenType::Colon) {
8069 self.advance();
8070 match key.as_str() {
8071 "D" | "d" | "drift_bound" => {
8072 node.drift_bound = self.parse_optional_float()
8073 }
8074 "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8075 _ => {
8079 return Err(ParseError {
8080 message: format!(
8081 "`{key}` is not a hypothesis of the stability theorem. The block \
8082 accepts exactly `D` (the drift bound, also spelled `d` or \
8083 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8084 `lipschitz`). This is an error rather than a skipped key \
8085 because a bound that fails to parse is a bound that is not \
8086 declared, and the compiler would then verify the band it can \
8087 see — the sign conditions — and admit the mandate as if the \
8088 rest had been checked."
8089 ),
8090 line: key_token.line,
8091 column: key_token.column,
8092 ..Default::default()
8093 });
8094 }
8095 }
8096 }
8097 if self.check(TokenType::Comma) {
8098 self.advance();
8099 }
8100 }
8101 self.consume(TokenType::RBrace)?;
8102 if node.drift_bound.is_none() && node.lipschitz.is_none() {
8103 return Err(ParseError {
8104 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."
8105 .to_string(),
8106 line: open.line,
8107 column: open.column,
8108 ..Default::default()
8109 });
8110 }
8111 Ok(())
8112 }
8113
8114 fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8143 let tok = self.consume(TokenType::Compute)?;
8144 let name = self.consume(TokenType::Identifier)?.value;
8145 let mut node = ComputeDefinition {
8146 name,
8147 shield_ref: String::new(),
8148 parameters: Vec::new(),
8149 return_type: String::new(),
8150 body: None,
8151 loc: Loc {
8152 line: tok.line,
8153 column: tok.column,
8154 },
8155 leading_trivia: Vec::new(),
8156 trailing_trivia: Vec::new(),
8157 };
8158
8159 if self.check(TokenType::LParen) {
8161 self.advance();
8162 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8163 let ptok = self.current().clone();
8164 let pname = self.consume_any_ident_or_kw()?.value.clone();
8165 self.consume(TokenType::Colon)?;
8166 let ptype = self.parse_type_expr()?;
8167 node.parameters.push(Parameter {
8168 name: pname,
8169 type_expr: ptype,
8170 loc: self.loc_of(&ptok),
8171 });
8172 if self.check(TokenType::Comma) {
8173 self.advance();
8174 }
8175 }
8176 self.consume(TokenType::RParen)?;
8177 }
8178
8179 if self.check(TokenType::Arrow) {
8181 self.advance();
8182 node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8183 }
8184
8185 self.consume(TokenType::LBrace)?;
8186 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8187 let is_field = self
8197 .tokens
8198 .get(self.pos + 1)
8199 .map(|t| t.ttype == TokenType::Colon)
8200 .unwrap_or(false);
8201 if is_field {
8202 let field_tok = self.current().clone();
8203 let field_name = self.current().value.clone();
8204 self.advance();
8205 self.consume(TokenType::Colon)?;
8206 match field_name.as_str() {
8207 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8208 "input" => self.parse_compute_input_list(&mut node)?,
8218 "output" => {
8221 node.return_type = self.parse_output_type_string()?;
8222 }
8223 _ => self.skip_value(),
8224 }
8225 let _ = field_tok;
8226 } else if self.current().value == "logic"
8227 && self
8228 .tokens
8229 .get(self.pos + 1)
8230 .is_some_and(|t| t.ttype == TokenType::LBrace)
8231 {
8232 if node.body.is_some() {
8237 return Err(ParseError {
8238 message: "compute declares two bodies; a pure function has one result, \
8239 and keeping the last silently would discard the first"
8240 .to_string(),
8241 line: self.current().line,
8242 column: self.current().column,
8243 ..Default::default()
8244 });
8245 }
8246 node.body = Some(self.parse_logic_block()?);
8247 } else {
8248 node.body = Some(self.parse_expr()?);
8249 }
8250 }
8251 self.consume(TokenType::RBrace)?;
8252 Ok(node)
8253 }
8254
8255 fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8261 loop {
8262 let ptok = self.current().clone();
8263 let pname = self.consume_any_ident_or_kw()?.value.clone();
8264 let type_expr = if self.check(TokenType::LParen) {
8268 self.advance();
8269 let t = self.parse_type_expr()?;
8270 self.consume(TokenType::RParen)?;
8271 t
8272 } else {
8273 TypeExpr {
8274 name: String::new(),
8275 generic_param: String::new(),
8276 optional: false,
8277 loc: self.loc_of(&ptok),
8278 }
8279 };
8280 node.parameters.push(Parameter {
8281 name: pname,
8282 type_expr,
8283 loc: self.loc_of(&ptok),
8284 });
8285 if self.check(TokenType::Comma) {
8286 self.advance();
8287 } else {
8288 break;
8289 }
8290 }
8291 Ok(())
8292 }
8293
8294 fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8307 let open = self.current().clone();
8308 self.advance(); self.consume(TokenType::LBrace)?;
8310
8311 let mut bindings: Vec<(String, Expr)> = Vec::new();
8312 let mut result: Option<Expr> = None;
8313
8314 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8315 if self.check(TokenType::Let) {
8316 if result.is_some() {
8317 return Err(ParseError {
8318 message: "a `let` after the `return` in a `logic { }` block is \
8319 unreachable — the block's value is already decided. Move it \
8320 above the `return`."
8321 .to_string(),
8322 line: self.current().line,
8323 column: self.current().column,
8324 ..Default::default()
8325 });
8326 }
8327 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
8329 self.consume(TokenType::Assign)?;
8330 bindings.push((name, self.parse_expr()?));
8331 } else if self.check(TokenType::Return) {
8332 self.advance();
8333 result = Some(self.parse_expr()?);
8334 } else {
8335 let bad = self.current().clone();
8336 return Err(ParseError {
8337 message: format!(
8338 "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8339 <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8340 function (its own paper: \"pureza categórica de los morfismos \
8341 funcionales\"), so a statement that could have an effect is refused \
8342 rather than parsed and dropped.",
8343 bad.value
8344 ),
8345 line: bad.line,
8346 column: bad.column,
8347 ..Default::default()
8348 });
8349 }
8350 }
8351 self.consume(TokenType::RBrace)?;
8352
8353 let mut expr = result.ok_or_else(|| ParseError {
8354 message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8355 names and yields nothing, and the compute would have to invent a result — \
8356 which is precisely what a deterministic primitive must never do."
8357 .to_string(),
8358 line: open.line,
8359 column: open.column,
8360 ..Default::default()
8361 })?;
8362
8363 for (name, value) in bindings.into_iter().rev() {
8365 expr = Expr::Let {
8366 name,
8367 value: Box::new(value),
8368 body: Box::new(expr),
8369 };
8370 }
8371 Ok(expr)
8372 }
8373
8374 fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8375 let tok = self.consume(TokenType::Daemon)?;
8376 let name = self.consume(TokenType::Identifier)?.value;
8377 let mut node = DaemonDefinition {
8378 name,
8379 goal: String::new(),
8380 tools: Vec::new(),
8381 memory_ref: String::new(),
8382 strategy: String::new(),
8383 on_stuck: String::new(),
8384 shield_ref: String::new(),
8385 window_ref: String::new(),
8386 budget: None,
8387 max_tokens: None,
8388 max_time: String::new(),
8389 max_cost: None,
8390 listeners: Vec::new(),
8391 requires_capabilities: Vec::new(),
8392 loc: Loc {
8393 line: tok.line,
8394 column: tok.column,
8395 },
8396 leading_trivia: Vec::new(),
8397 trailing_trivia: Vec::new(),
8398 };
8399 while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8401 self.advance();
8402 }
8403 self.consume(TokenType::LBrace)?;
8404 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8405 let field = self.current().clone();
8406 let field_name = field.value.clone();
8407 self.advance();
8408 if self.check(TokenType::Colon) {
8409 self.advance();
8410 match field_name.as_str() {
8411 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8412 "tools" => node.tools = self.parse_bracketed_identifiers()?,
8413 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8414 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8415 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8416 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8417 "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8419 "max_tokens" => node.max_tokens = self.parse_optional_int(),
8420 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8421 "max_cost" => node.max_cost = self.parse_optional_float(),
8422 "requires" => {
8427 let bracket_tok = self.current().clone();
8428 let items = self.parse_bracketed_dot_identifiers()?;
8429 for slug in &items {
8430 if !is_valid_capability_slug(slug) {
8431 return Err(ParseError {
8432 message: format!(
8433 "Invalid capability slug '{slug}' in daemon '{}' \
8434 `requires:`. Capability slugs must match \
8435 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8436 lowercase identifiers. Examples: `daemon.run`, \
8437 `memory.write`, `flow.execute`.",
8438 node.name
8439 ),
8440 line: bracket_tok.line,
8441 column: bracket_tok.column,
8442 ..Default::default()
8443 });
8444 }
8445 }
8446 node.requires_capabilities = items;
8447 }
8448 _ => self.skip_value(),
8449 }
8450 } else if field.ttype == TokenType::Listen {
8451 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8457 (self.consume(TokenType::StringLit)?.value.clone(), false)
8458 } else {
8459 (self.consume_any_ident_or_kw()?.value.clone(), true)
8460 };
8461 let mut alias = String::new();
8462 if !self.at_declaration_start()
8463 && !self.check(TokenType::RBrace)
8464 && !self.check(TokenType::LBrace)
8465 {
8466 let next = self.current().clone();
8467 if next.value == "as" || next.ttype == TokenType::As {
8468 self.advance();
8469 alias = self.consume_any_ident_or_kw()?.value.clone();
8470 }
8471 }
8472 let listen_loc = Loc {
8473 line: field.line,
8474 column: field.column,
8475 };
8476 let body = self.parse_listener_body()?;
8480 node.listeners.push(ListenStep {
8481 channel,
8482 channel_is_ref,
8483 event_alias: alias,
8484 body,
8485 loc: listen_loc,
8486 });
8487 } else if field_name == "budget" && self.check(TokenType::LBrace) {
8488 node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8490 } else if self.check(TokenType::LBrace) {
8491 self.skip_braced_block()?;
8492 }
8493 }
8494 self.consume(TokenType::RBrace)?;
8495 Ok(node)
8496 }
8497
8498 fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8507 let kw = self.consume(TokenType::Budget)?; let name = self.consume(TokenType::Identifier)?.value;
8509 let mut block = self.parse_budget_block(kw.line, kw.column)?;
8510 block.name = name;
8511 Ok(block)
8512 }
8513
8514 fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8516 self.consume(TokenType::LBrace)?;
8517 let mut quotas = Vec::new();
8518 let mut on_exhausted = String::new();
8519 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8520 let field = self.current().clone();
8521 let field_name = self.consume_any_ident_or_kw()?.value;
8522 match field_name.as_str() {
8523 "rate" | "max" => {
8524 quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8525 }
8526 "on_exhausted" => {
8527 self.consume(TokenType::Colon)?;
8528 on_exhausted = self.consume_any_ident_or_kw()?.value;
8529 }
8530 _ => self.skip_value(),
8531 }
8532 }
8533 self.consume(TokenType::RBrace)?;
8534 Ok(BudgetBlock {
8535 name: String::new(),
8536 quotas,
8537 on_exhausted,
8538 loc: Loc { line, column },
8539 leading_trivia: Vec::new(),
8540 trailing_trivia: Vec::new(),
8541 })
8542 }
8543
8544 fn parse_budget_quota(
8547 &mut self,
8548 kind: String,
8549 line: u32,
8550 column: u32,
8551 ) -> Result<BudgetQuota, ParseError> {
8552 self.consume(TokenType::Colon)?;
8553 let limit = self.consume_number()? as i64;
8554 let _per = self.consume_any_ident_or_kw()?; let period = self.consume_any_ident_or_kw()?.value;
8557 let _on = self.consume_any_ident_or_kw()?; let _tool = self.consume_any_ident_or_kw()?; self.consume(TokenType::LParen)?;
8561 let effect = self.consume_any_ident_or_kw()?.value;
8562 self.consume(TokenType::RParen)?;
8563 Ok(BudgetQuota {
8564 kind,
8565 limit,
8566 period,
8567 effect,
8568 loc: Loc { line, column },
8569 })
8570 }
8571
8572 fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8573 let tok = self.consume(TokenType::AxonStore)?;
8574 let name = self.consume(TokenType::Identifier)?.value;
8575 let mut node = AxonStoreDefinition {
8576 name,
8577 backend: String::new(),
8578 connection: String::new(),
8579 resource_ref: String::new(),
8580 confidence_floor: None,
8581 isolation: String::new(),
8582 on_breach: String::new(),
8583 capability: String::new(),
8584 class: String::new(),
8585 column_schema: None,
8586 loc: Loc {
8587 line: tok.line,
8588 column: tok.column,
8589 },
8590 leading_trivia: Vec::new(),
8591 trailing_trivia: Vec::new(),
8592 };
8593 self.consume(TokenType::LBrace)?;
8594 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8595 let field = self.current().clone();
8596 let field_name = field.value.clone();
8597 if field.ttype == TokenType::Schema {
8603 self.advance();
8604 let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8605 node.column_schema = Some(parsed);
8606 continue;
8607 }
8608 self.advance();
8609 if self.check(TokenType::Colon) {
8610 self.advance();
8611 match field_name.as_str() {
8612 "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8613 "class" => node.class = self.parse_dotted_identifier()?,
8621 "connection" => node.connection = self.parse_config_key()?,
8622 "resource" => {
8628 node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8629 }
8630 "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8631 "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8632 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8633 "capability" => {
8637 let slug_tok = self.consume(TokenType::StringLit)?.clone();
8638 if !is_valid_capability_slug(&slug_tok.value) {
8639 return Err(ParseError {
8640 message: format!(
8641 "Invalid capability slug '{}' in axonstore '{}' \
8642 `capability:`. Capability slugs must match \
8643 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8644 lowercase identifiers starting with a letter. Examples: \
8645 `admin`, `tenant.read`, `hipaa.phi.read`.",
8646 slug_tok.value, node.name
8647 ),
8648 line: slug_tok.line,
8649 column: slug_tok.column,
8650 ..Default::default()
8651 });
8652 }
8653 node.capability = slug_tok.value.clone();
8654 }
8655 _ => self.skip_value(),
8656 }
8657 } else if self.check(TokenType::LBrace) {
8658 self.skip_braced_block()?;
8659 }
8660 }
8661 self.consume(TokenType::RBrace)?;
8662 Ok(node)
8663 }
8664
8665 fn parse_store_schema_declaration(
8677 &mut self,
8678 store_name: &str,
8679 sch_line: u32,
8680 sch_col: u32,
8681 ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8682 use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8683
8684 if self.check(TokenType::LBrace) {
8686 self.consume(TokenType::LBrace)?;
8687 let mut columns: Vec<StoreColumn> = Vec::new();
8688 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8689 let col_tok = self.current().clone();
8690 let col_name = self.consume_any_ident_or_kw()?.value.clone();
8691 self.consume(TokenType::Colon)?;
8692 let type_tok = self.consume_any_ident_or_kw()?.clone();
8693 let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8694 let names = StoreColumnType::all_canonical_names();
8695 let suggestion =
8696 crate::smart_suggest::suggest_for(&type_tok.value, &names);
8697 let suggest_suffix = if suggestion.is_empty() {
8698 String::new()
8699 } else {
8700 format!(" {suggestion}")
8701 };
8702 let known = names.join(", ");
8703 ParseError {
8704 message: format!(
8705 "Unknown column type `{}` for column `{}` in \
8706 axonstore `{}` `schema:` block. The closed \
8707 v1.38.0 column-type catalog \
8708 is {{{known}}} (plus common lowercase \
8709 aliases — `int`/`integer`/`int4` for \
8710 `Int`, `bool`/`boolean` for `Bool`, etc.).\
8711 {suggest_suffix}",
8712 type_tok.value, col_name, store_name
8713 ),
8714 line: type_tok.line,
8715 column: type_tok.column,
8716 ..Default::default()
8717 }
8718 })?;
8719
8720 let mut json_shape: Option<String> = None;
8730 if self.check(TokenType::Lt) {
8731 self.advance();
8732 let shape_tok = self.consume_any_ident_or_kw()?.clone();
8733 self.consume(TokenType::Gt)?;
8734 if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8735 json_shape = Some(shape_tok.value.clone());
8736 } else {
8737 return Err(ParseError {
8738 message: format!(
8739 "axon-T841 a shape lens `<{shape}>` may refine \
8740 only a `Json` / `Jsonb` column, but column \
8741 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8742 the `<{shape}>` (a rigid column already has a \
8743 fixed shape), or change the column type to \
8744 `Json<{shape}>` if it carries open documents.",
8745 shape = shape_tok.value,
8746 col = col_name,
8747 store = store_name,
8748 ty = col_type.canonical_name(),
8749 ),
8750 line: shape_tok.line,
8751 column: shape_tok.column,
8752 ..Default::default()
8753 });
8754 }
8755 }
8756
8757 let mut col = StoreColumn {
8758 name: col_name,
8759 col_type,
8760 json_shape,
8761 primary_key: false,
8762 auto_increment: false,
8763 not_null: false,
8764 unique: false,
8765 indexed: false,
8766 default_value: String::new(),
8767 identity: false,
8772 line: col_tok.line,
8773 column: col_tok.column,
8774 };
8775
8776 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8779 if self.current().ttype != TokenType::Identifier {
8780 break;
8783 }
8784 let constraint = self.current().value.clone();
8785 match constraint.as_str() {
8786 "primary_key" => {
8787 col.primary_key = true;
8788 self.advance();
8789 }
8790 "auto_increment" => {
8791 col.auto_increment = true;
8792 self.advance();
8793 }
8794 "not_null" => {
8795 col.not_null = true;
8796 self.advance();
8797 }
8798 "unique" => {
8799 col.unique = true;
8800 self.advance();
8801 }
8802 "index" => {
8808 col.indexed = true;
8809 self.advance();
8810 }
8811 "identity" => {
8822 col.identity = true;
8823 self.advance();
8824 }
8825 "default" => {
8826 self.advance();
8827 let dv = self.current().clone();
8828 if matches!(
8829 dv.ttype,
8830 TokenType::StringLit
8831 | TokenType::Integer
8832 | TokenType::Float
8833 ) {
8834 col.default_value = dv.value.clone();
8835 self.advance();
8836 } else {
8837 col.default_value =
8838 self.consume_any_ident_or_kw()?.value.clone();
8839 }
8840 }
8841 _ => break,
8842 }
8843 }
8844
8845 columns.push(col);
8846 }
8847 self.consume(TokenType::RBrace)?;
8848 return Ok(StoreColumnSchema::Inline {
8849 columns,
8850 leading_trivia: Vec::new(),
8851 line: sch_line,
8852 column: sch_col,
8853 });
8854 }
8855
8856 if !self.check(TokenType::Colon) {
8858 let cur = self.current().clone();
8859 return Err(ParseError {
8860 message: format!(
8861 "axonstore `{store_name}` `schema:` declaration expects \
8862 `{{ … }}` (inline columns), `: \"manifest.ref\"` \
8863 (manifest reference), or `: env:VAR` (per-tenant schema \
8864 namespace). Got `{}` instead.",
8865 cur.value
8866 ),
8867 line: cur.line,
8868 column: cur.column,
8869 ..Default::default()
8870 });
8871 }
8872 self.consume(TokenType::Colon)?;
8873
8874 if self.check(TokenType::StringLit) {
8876 let lit = self.consume(TokenType::StringLit)?.clone();
8877 let value = lit.value.clone();
8878 if let Some(var) = value.strip_prefix("env:") {
8879 let var = var.trim();
8880 if var.is_empty() {
8881 return Err(ParseError {
8882 message: format!(
8883 "axonstore `{store_name}` `schema: \"env:\"` is \
8884 missing the variable name after the `env:` \
8885 prefix."
8886 ),
8887 line: lit.line,
8888 column: lit.column,
8889 ..Default::default()
8890 });
8891 }
8892 return Ok(StoreColumnSchema::EnvVar {
8893 var_name: var.to_string(),
8894 line: sch_line,
8895 column: sch_col,
8896 });
8897 }
8898 if value.trim().is_empty() {
8900 return Err(ParseError {
8901 message: format!(
8902 "axonstore `{store_name}` `schema:` manifest reference \
8903 is empty. Expected `\"qualified.name\"` — e.g. \
8904 `\"public.tenants\"`."
8905 ),
8906 line: lit.line,
8907 column: lit.column,
8908 ..Default::default()
8909 });
8910 }
8911 return Ok(StoreColumnSchema::ManifestRef {
8912 qualified_name: value,
8913 line: sch_line,
8914 column: sch_col,
8915 });
8916 }
8917
8918 let env_tok = self.current().clone();
8921 if env_tok.value == "env" {
8922 self.advance();
8923 if !self.check(TokenType::Colon) {
8924 return Err(ParseError {
8925 message: format!(
8926 "axonstore `{store_name}` `schema: env` is missing the \
8927 `:` separator. Expected `schema: env:VAR`."
8928 ),
8929 line: env_tok.line,
8930 column: env_tok.column,
8931 ..Default::default()
8932 });
8933 }
8934 self.advance(); let var_tok = self.consume_any_ident_or_kw()?.clone();
8936 if var_tok.value.trim().is_empty() {
8937 return Err(ParseError {
8938 message: format!(
8939 "axonstore `{store_name}` `schema: env:` is missing \
8940 the variable name."
8941 ),
8942 line: var_tok.line,
8943 column: var_tok.column,
8944 ..Default::default()
8945 });
8946 }
8947 return Ok(StoreColumnSchema::EnvVar {
8948 var_name: var_tok.value.clone(),
8949 line: sch_line,
8950 column: sch_col,
8951 });
8952 }
8953
8954 Err(ParseError {
8955 message: format!(
8956 "axonstore `{store_name}` `schema:` declaration expects \
8957 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
8958 reference), or `env:VAR` (per-tenant schema namespace). \
8959 Got `{}` instead.",
8960 env_tok.value
8961 ),
8962 line: env_tok.line,
8963 column: env_tok.column,
8964 ..Default::default()
8965 })
8966 }
8967
8968 fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
8975 let tok = self.consume(TokenType::Resource)?;
8976 let name = self.consume(TokenType::Identifier)?.value;
8977 let mut node = ResourceDefinition {
8978 name,
8979 kind: String::new(),
8980 endpoint: String::new(),
8981 capacity: None,
8982 lifetime: "affine".to_string(),
8983 certainty_floor: None,
8984 shield_ref: String::new(),
8985 within: String::new(),
8986 loc: Loc {
8987 line: tok.line,
8988 column: tok.column,
8989 },
8990 leading_trivia: Vec::new(),
8991 trailing_trivia: Vec::new(),
8992 };
8993 self.consume(TokenType::LBrace)?;
8994 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8995 let field_tok = self.current().clone();
8996 let field_name = field_tok.value.clone();
8997 self.advance();
8998 if !self.check(TokenType::Colon) {
8999 if self.check(TokenType::LBrace) {
9001 self.skip_braced_block()?;
9002 }
9003 continue;
9004 }
9005 self.advance(); match field_name.as_str() {
9007 "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9008 "endpoint" => {
9021 node.endpoint = if self.check(TokenType::StringLit) {
9022 self.consume(TokenType::StringLit)?.value
9023 } else {
9024 self.parse_dotted_identifier()?
9025 };
9026 }
9027 "capacity" => {
9028 node.capacity = self.parse_optional_int();
9029 }
9030 "lifetime" => {
9031 let lt_tok = self.consume_any_ident_or_kw()?;
9032 let lt = lt_tok.value;
9033 if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9034 return Err(ParseError {
9035 message: format!(
9036 "Invalid lifetime '{lt}' in resource '{}' — \
9037 expected linear | affine | persistent",
9038 node.name
9039 ),
9040 line: lt_tok.line,
9041 column: lt_tok.column,
9042 ..Default::default()
9043 });
9044 }
9045 node.lifetime = lt;
9046 }
9047 "certainty_floor" => {
9048 node.certainty_floor = self.parse_optional_float();
9049 }
9050 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9051 "within" => node.within = self.consume_any_ident_or_kw()?.value,
9055 unknown => {
9065 return Err(ParseError {
9066 message: format!(
9067 "Unknown field '{unknown}' in resource '{}' — expected one of: \
9068 kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9069 node.name
9070 ),
9071 line: field_tok.line,
9072 column: field_tok.column,
9073 ..Default::default()
9074 });
9075 }
9076 }
9077 }
9078 self.consume(TokenType::RBrace)?;
9079 Ok(node)
9080 }
9081
9082 fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9084 let tok = self.consume(TokenType::Fabric)?;
9085 let name = self.consume(TokenType::Identifier)?.value;
9086 let mut node = FabricDefinition {
9087 name,
9088 provider: String::new(),
9089 region: String::new(),
9090 zones: None,
9091 ephemeral: None,
9092 shield_ref: String::new(),
9093 loc: Loc {
9094 line: tok.line,
9095 column: tok.column,
9096 },
9097 leading_trivia: Vec::new(),
9098 trailing_trivia: Vec::new(),
9099 };
9100 self.consume(TokenType::LBrace)?;
9101 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9102 let field_name = self.current().value.clone();
9103 self.advance();
9104 if !self.check(TokenType::Colon) {
9105 if self.check(TokenType::LBrace) {
9106 self.skip_braced_block()?;
9107 }
9108 continue;
9109 }
9110 self.advance(); match field_name.as_str() {
9112 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9113 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9114 "zones" => node.zones = self.parse_optional_int(),
9115 "ephemeral" => {
9116 let b = self.parse_bool()?;
9117 node.ephemeral = Some(b);
9118 }
9119 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9120 _ => self.skip_value(),
9121 }
9122 }
9123 self.consume(TokenType::RBrace)?;
9124 Ok(node)
9125 }
9126
9127 fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9129 let tok = self.consume(TokenType::Manifest)?;
9130 let name = self.consume(TokenType::Identifier)?.value;
9131 let mut node = ManifestDefinition {
9132 name,
9133 resources: Vec::new(),
9134 fabric_ref: String::new(),
9135 region: String::new(),
9136 zones: None,
9137 compliance: Vec::new(),
9138 loc: Loc {
9139 line: tok.line,
9140 column: tok.column,
9141 },
9142 leading_trivia: Vec::new(),
9143 trailing_trivia: Vec::new(),
9144 };
9145 self.consume(TokenType::LBrace)?;
9146 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9147 let field_name = self.current().value.clone();
9148 self.advance();
9149 if !self.check(TokenType::Colon) {
9150 if self.check(TokenType::LBrace) {
9151 self.skip_braced_block()?;
9152 }
9153 continue;
9154 }
9155 self.advance();
9156 match field_name.as_str() {
9157 "resources" => node.resources = self.parse_bracketed_identifiers()?,
9158 "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9159 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9160 "zones" => node.zones = self.parse_optional_int(),
9161 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9162 _ => self.skip_value(),
9163 }
9164 }
9165 self.consume(TokenType::RBrace)?;
9166 Ok(node)
9167 }
9168
9169 fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9171 let tok = self.consume(TokenType::Observe)?;
9172 let name = self.consume(TokenType::Identifier)?.value;
9173 self.consume(TokenType::From)?;
9175 let target = self.consume(TokenType::Identifier)?.value;
9176 let mut node = ObserveDefinition {
9177 name,
9178 target,
9179 sources: Vec::new(),
9180 quorum: None,
9181 timeout: String::new(),
9182 on_partition: "fail".to_string(),
9183 certainty_floor: None,
9184 loc: Loc {
9185 line: tok.line,
9186 column: tok.column,
9187 },
9188 leading_trivia: Vec::new(),
9189 trailing_trivia: Vec::new(),
9190 };
9191 self.consume(TokenType::LBrace)?;
9192 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9193 let field_name = self.current().value.clone();
9194 self.advance();
9195 if !self.check(TokenType::Colon) {
9196 if self.check(TokenType::LBrace) {
9197 self.skip_braced_block()?;
9198 }
9199 continue;
9200 }
9201 self.advance();
9202 match field_name.as_str() {
9203 "sources" => node.sources = self.parse_bracketed_identifiers()?,
9204 "quorum" => node.quorum = self.parse_optional_int(),
9205 "timeout" => {
9206 let t = self.current().clone();
9207 match t.ttype {
9208 TokenType::Duration | TokenType::StringLit => {
9209 self.advance();
9210 node.timeout = t.value;
9211 }
9212 _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9213 }
9214 }
9215 "on_partition" => {
9216 let p_tok = self.consume_any_ident_or_kw()?;
9217 let p = p_tok.value;
9218 if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9219 return Err(ParseError {
9220 message: format!(
9221 "Invalid on_partition '{p}' in observe '{}' — \
9222 expected fail | shield_quarantine",
9223 node.name
9224 ),
9225 line: p_tok.line,
9226 column: p_tok.column,
9227 ..Default::default()
9228 });
9229 }
9230 node.on_partition = p;
9231 }
9232 "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9233 _ => self.skip_value(),
9234 }
9235 }
9236 self.consume(TokenType::RBrace)?;
9237 Ok(node)
9238 }
9239
9240 fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9244 let tok = self.consume(TokenType::Reconcile)?;
9245 let name = self.consume(TokenType::Identifier)?.value;
9246 let mut node = ReconcileDefinition {
9247 name,
9248 observe_ref: String::new(),
9249 threshold: None,
9250 tolerance: None,
9251 on_drift: "provision".to_string(),
9252 shield_ref: String::new(),
9253 mandate_ref: String::new(),
9254 max_retries: 3,
9255 loc: Loc {
9256 line: tok.line,
9257 column: tok.column,
9258 },
9259 leading_trivia: Vec::new(),
9260 trailing_trivia: Vec::new(),
9261 };
9262 self.consume(TokenType::LBrace)?;
9263 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9264 let field_name = self.current().value.clone();
9265 self.advance();
9266 if !self.check(TokenType::Colon) {
9267 if self.check(TokenType::LBrace) {
9268 self.skip_braced_block()?;
9269 }
9270 continue;
9271 }
9272 self.advance();
9273 match field_name.as_str() {
9274 "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9275 "threshold" => node.threshold = self.parse_optional_float(),
9276 "tolerance" => node.tolerance = self.parse_optional_float(),
9277 "on_drift" => {
9278 let d_tok = self.consume_any_ident_or_kw()?;
9279 let d = d_tok.value;
9280 if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9281 return Err(ParseError {
9282 message: format!(
9283 "Invalid on_drift '{d}' in reconcile '{}' — \
9284 expected provision | alert | refine",
9285 node.name
9286 ),
9287 line: d_tok.line,
9288 column: d_tok.column,
9289 ..Default::default()
9290 });
9291 }
9292 node.on_drift = d;
9293 }
9294 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9295 "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9296 "max_retries" => {
9297 if let Some(v) = self.parse_optional_int() {
9298 node.max_retries = v;
9299 }
9300 }
9301 _ => self.skip_value(),
9302 }
9303 }
9304 self.consume(TokenType::RBrace)?;
9305 Ok(node)
9306 }
9307
9308 fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9310 let tok = self.consume(TokenType::Lease)?;
9311 let name = self.consume(TokenType::Identifier)?.value;
9312 let mut node = LeaseDefinition {
9313 name,
9314 resource_ref: String::new(),
9315 duration: String::new(),
9316 acquire: "on_start".to_string(),
9317 on_expire: "anchor_breach".to_string(),
9318 loc: Loc {
9319 line: tok.line,
9320 column: tok.column,
9321 },
9322 leading_trivia: Vec::new(),
9323 trailing_trivia: Vec::new(),
9324 };
9325 self.consume(TokenType::LBrace)?;
9326 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9327 let field_name = self.current().value.clone();
9328 self.advance();
9329 if !self.check(TokenType::Colon) {
9330 if self.check(TokenType::LBrace) {
9331 self.skip_braced_block()?;
9332 }
9333 continue;
9334 }
9335 self.advance();
9336 match field_name.as_str() {
9337 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9338 "duration" => {
9339 let t = self.current().clone();
9340 match t.ttype {
9341 TokenType::Duration | TokenType::StringLit => {
9342 self.advance();
9343 node.duration = t.value;
9344 }
9345 _ => node.duration = self.consume_any_ident_or_kw()?.value,
9346 }
9347 }
9348 "acquire" => {
9349 let a_tok = self.consume_any_ident_or_kw()?;
9350 let a = a_tok.value;
9351 if !matches!(a.as_str(), "on_start" | "on_demand") {
9352 return Err(ParseError {
9353 message: format!(
9354 "Invalid acquire '{a}' in lease '{}' — \
9355 expected on_start | on_demand",
9356 node.name
9357 ),
9358 line: a_tok.line,
9359 column: a_tok.column,
9360 ..Default::default()
9361 });
9362 }
9363 node.acquire = a;
9364 }
9365 "on_expire" => {
9366 let e_tok = self.consume_any_ident_or_kw()?;
9367 let e = e_tok.value;
9368 if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9369 return Err(ParseError {
9370 message: format!(
9371 "Invalid on_expire '{e}' in lease '{}' — \
9372 expected anchor_breach | release | extend",
9373 node.name
9374 ),
9375 line: e_tok.line,
9376 column: e_tok.column,
9377 ..Default::default()
9378 });
9379 }
9380 node.on_expire = e;
9381 }
9382 _ => self.skip_value(),
9383 }
9384 }
9385 self.consume(TokenType::RBrace)?;
9386 Ok(node)
9387 }
9388
9389 fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9391 let tok = self.consume(TokenType::Ensemble)?;
9392 let name = self.consume(TokenType::Identifier)?.value;
9393 let mut node = EnsembleDefinition {
9394 name,
9395 observations: Vec::new(),
9396 quorum: None,
9397 aggregation: "majority".to_string(),
9398 certainty_mode: "min".to_string(),
9399 loc: Loc {
9400 line: tok.line,
9401 column: tok.column,
9402 },
9403 leading_trivia: Vec::new(),
9404 trailing_trivia: Vec::new(),
9405 };
9406 self.consume(TokenType::LBrace)?;
9407 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9408 let field_name = self.current().value.clone();
9409 self.advance();
9410 if !self.check(TokenType::Colon) {
9411 if self.check(TokenType::LBrace) {
9412 self.skip_braced_block()?;
9413 }
9414 continue;
9415 }
9416 self.advance();
9417 match field_name.as_str() {
9418 "observations" => node.observations = self.parse_bracketed_identifiers()?,
9419 "quorum" => node.quorum = self.parse_optional_int(),
9420 "aggregation" => {
9421 let a_tok = self.consume_any_ident_or_kw()?;
9422 let a = a_tok.value;
9423 if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9424 return Err(ParseError {
9425 message: format!(
9426 "Invalid aggregation '{a}' in ensemble '{}' — \
9427 expected majority | weighted | byzantine",
9428 node.name
9429 ),
9430 line: a_tok.line,
9431 column: a_tok.column,
9432 ..Default::default()
9433 });
9434 }
9435 node.aggregation = a;
9436 }
9437 "certainty_mode" => {
9438 let c_tok = self.consume_any_ident_or_kw()?;
9439 let c = c_tok.value;
9440 if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9441 return Err(ParseError {
9442 message: format!(
9443 "Invalid certainty_mode '{c}' in ensemble '{}' — \
9444 expected min | weighted | harmonic",
9445 node.name
9446 ),
9447 line: c_tok.line,
9448 column: c_tok.column,
9449 ..Default::default()
9450 });
9451 }
9452 node.certainty_mode = c;
9453 }
9454 _ => self.skip_value(),
9455 }
9456 }
9457 self.consume(TokenType::RBrace)?;
9458 Ok(node)
9459 }
9460
9461 fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9469 let tok = self.consume(TokenType::Session)?;
9470 let name = self.consume(TokenType::Identifier)?.value;
9471 let mut node = SessionDefinition {
9472 name,
9473 roles: Vec::new(),
9474 loc: Loc {
9475 line: tok.line,
9476 column: tok.column,
9477 },
9478 leading_trivia: Vec::new(),
9479 trailing_trivia: Vec::new(),
9480 };
9481 self.consume(TokenType::LBrace)?;
9482 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9483 let role_tok = self.consume_any_ident_or_kw()?;
9484 self.consume(TokenType::Colon)?;
9485 let steps = self.parse_session_steps()?;
9486 node.roles.push(SessionRole {
9487 name: role_tok.value,
9488 steps,
9489 loc: Loc {
9490 line: role_tok.line,
9491 column: role_tok.column,
9492 },
9493 });
9494 }
9495 self.consume(TokenType::RBrace)?;
9496 Ok(node)
9497 }
9498
9499 fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9512 let tok = self.consume(TokenType::Observable)?;
9513 let name = self.consume(TokenType::Identifier)?.value;
9514 let mut node = ObservableDefinition {
9515 name,
9516 qubits: None,
9517 terms: Vec::new(),
9518 loc: Loc {
9519 line: tok.line,
9520 column: tok.column,
9521 },
9522 leading_trivia: Vec::new(),
9523 trailing_trivia: Vec::new(),
9524 };
9525 self.consume(TokenType::LBrace)?;
9526 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9527 let key_tok = self.consume_any_ident_or_kw()?;
9528 self.consume(TokenType::Colon)?;
9529 match key_tok.value.as_str() {
9530 "qubits" => node.qubits = Some(self.consume_number()? as i64),
9531 "term" => {
9532 let term_loc = Loc {
9533 line: key_tok.line,
9534 column: key_tok.column,
9535 };
9536 let mut negative = false;
9538 if self.check(TokenType::Minus) {
9539 self.advance();
9540 negative = true;
9541 } else if self.check(TokenType::Plus) {
9542 self.advance();
9543 }
9544 let mag = self.consume_number()?;
9545 let coefficient = if negative { -mag } else { mag };
9546 self.consume(TokenType::Star)?;
9548 let pauli = self.consume(TokenType::StringLit)?.value;
9549 node.terms.push(PauliTerm {
9550 coefficient,
9551 pauli,
9552 loc: term_loc,
9553 });
9554 }
9555 _ => self.skip_value(),
9556 }
9557 }
9558 self.consume(TokenType::RBrace)?;
9559 Ok(node)
9560 }
9561
9562 fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9570 let tok = self.consume(TokenType::Witness)?;
9571 let name = self.consume(TokenType::Identifier)?.value;
9572 let mut node = WitnessDefinition {
9573 name,
9574 claim: String::new(),
9575 baseline: String::new(),
9576 metric: String::new(),
9577 threshold: 0.0,
9578 data: String::new(),
9579 loc: Loc {
9580 line: tok.line,
9581 column: tok.column,
9582 },
9583 leading_trivia: Vec::new(),
9584 trailing_trivia: Vec::new(),
9585 };
9586 self.consume(TokenType::LBrace)?;
9587 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9588 let key_tok = self.consume_any_ident_or_kw()?;
9589 self.consume(TokenType::Colon)?;
9590 match key_tok.value.as_str() {
9591 "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9592 "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9595 "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9596 "threshold" => node.threshold = self.consume_number()?,
9597 "data" => node.data = self.consume_any_ident_or_kw()?.value,
9598 _ => self.skip_value(),
9599 }
9600 }
9601 self.consume(TokenType::RBrace)?;
9602 Ok(node)
9603 }
9604
9605 fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9610 let tok = self.consume(TokenType::Socket)?;
9611 let name = self.consume(TokenType::Identifier)?.value;
9612 let mut node = SocketDefinition {
9613 name,
9614 loc: Loc { line: tok.line, column: tok.column },
9615 ..Default::default()
9616 };
9617 self.consume(TokenType::LBrace)?;
9618 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9619 let key = self.consume_any_ident_or_kw()?.value;
9620 self.consume(TokenType::Colon)?;
9621 match key.as_str() {
9622 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9623 "backpressure" => {
9624 let kind = self.consume_any_ident_or_kw()?.value;
9626 if kind != "credit" {
9627 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9628 }
9629 self.consume(TokenType::LParen)?;
9630 let n = self
9631 .consume(TokenType::Integer)?
9632 .value
9633 .parse::<i64>()
9634 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9635 self.consume(TokenType::RParen)?;
9636 node.backpressure_credit = Some(n);
9637 }
9638 "reconnect" => {
9639 let mode = self.consume_any_ident_or_kw()?.value;
9640 node.reconnect = mode == "cognitive_state";
9641 }
9642 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9643 other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9644 }
9645 if self.check(TokenType::Comma) {
9647 self.consume(TokenType::Comma)?;
9648 }
9649 }
9650 self.consume(TokenType::RBrace)?;
9651 Ok(node)
9652 }
9653
9654 fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9662 let tok = self.consume(TokenType::Upstream)?;
9663 let name = self.consume(TokenType::Identifier)?.value;
9664 let mut node = UpstreamDefinition {
9665 name,
9666 loc: Loc { line: tok.line, column: tok.column },
9667 ..Default::default()
9668 };
9669 if self.check(TokenType::From) {
9671 self.advance();
9672 let base = self.consume(TokenType::Identifier)?.value;
9673 self.consume(TokenType::At)?;
9674 let version = self.consume_any_ident_or_kw()?.value;
9675 node.preset = Some(format!("{base}@{version}"));
9676 }
9677 self.consume(TokenType::LBrace)?;
9678 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9679 let key = self.consume_any_ident_or_kw()?.value;
9680 self.consume(TokenType::Colon)?;
9681 match key.as_str() {
9682 "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9683 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9684 "role" => node.role = self.consume_any_ident_or_kw()?.value,
9685 "resolve" => node.resolve = self.parse_dotted_identifier()?,
9686 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9690 "secret" => node.secret = self.parse_dotted_identifier()?,
9691 "auth" => {
9692 node.auth_kind = self.consume_any_ident_or_kw()?.value;
9695 if self.check(TokenType::LParen) {
9696 self.consume(TokenType::LParen)?;
9697 node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9698 if self.check(TokenType::Comma) {
9699 self.consume(TokenType::Comma)?;
9700 node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9701 }
9702 self.consume(TokenType::RParen)?;
9703 }
9704 }
9705 "map" => node.map = self.parse_upstream_map()?,
9706 "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9707 "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9708 "backpressure" => {
9709 let kind = self.consume_any_ident_or_kw()?.value;
9711 if kind != "credit" {
9712 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9713 }
9714 self.consume(TokenType::LParen)?;
9715 let n = self
9716 .consume(TokenType::Integer)?
9717 .value
9718 .parse::<i64>()
9719 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9720 self.consume(TokenType::RParen)?;
9721 node.backpressure_credit = Some(n);
9722 }
9723 other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9724 }
9725 if self.check(TokenType::Comma) {
9727 self.consume(TokenType::Comma)?;
9728 }
9729 }
9730 self.consume(TokenType::RBrace)?;
9731 Ok(node)
9732 }
9733
9734 fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9743 let tok = self.consume(TokenType::Cors)?;
9744 let name = self.consume(TokenType::Identifier)?.value;
9745 let mut node = CorsDefinition {
9746 name,
9747 loc: Loc { line: tok.line, column: tok.column },
9748 ..Default::default()
9749 };
9750 self.consume(TokenType::LBrace)?;
9751 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9752 let key = self.consume_any_ident_or_kw()?.value;
9753 self.consume(TokenType::Colon)?;
9754 match key.as_str() {
9755 "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9756 "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9757 "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9758 "allow_credentials" => {
9759 node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9760 }
9761 "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9762 "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9763 other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9764 }
9765 if self.check(TokenType::Comma) {
9767 self.consume(TokenType::Comma)?;
9768 }
9769 }
9770 self.consume(TokenType::RBrace)?;
9771 Ok(node)
9772 }
9773
9774 fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9782 let tok = self.consume(TokenType::Credential)?;
9783 let name = self.consume(TokenType::Identifier)?.value;
9784 let mut node = CredentialDefinition {
9785 name,
9786 loc: Loc { line: tok.line, column: tok.column },
9787 ..Default::default()
9788 };
9789 self.consume(TokenType::LBrace)?;
9790 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9791 let key = self.consume_any_ident_or_kw()?.value;
9792 self.consume(TokenType::Colon)?;
9793 match key.as_str() {
9794 "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9795 "grants" => {
9796 let bracket_tok = self.current().clone();
9797 let items = self.parse_bracketed_dot_identifiers()?;
9798 for slug in &items {
9799 if !is_valid_capability_slug(slug) {
9800 return Err(ParseError {
9801 message: format!(
9802 "Invalid capability slug '{slug}' in credential '{}' \
9803 `grants:`. Capability slugs must match \
9804 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9805 lowercase identifiers starting with a letter. Examples: \
9806 `chat.invoke`, `flow.execute`.",
9807 node.name
9808 ),
9809 line: bracket_tok.line,
9810 column: bracket_tok.column,
9811 ..Default::default()
9812 });
9813 }
9814 }
9815 node.grants = items;
9816 }
9817 other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9818 }
9819 if self.check(TokenType::Comma) {
9821 self.consume(TokenType::Comma)?;
9822 }
9823 }
9824 self.consume(TokenType::RBrace)?;
9825 Ok(node)
9826 }
9827
9828 fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9835 let tok = self.consume(TokenType::Cache)?;
9836 let name = self.consume(TokenType::Identifier)?.value;
9837 let mut node = CacheDefinition {
9838 name,
9839 loc: Loc { line: tok.line, column: tok.column },
9840 ..Default::default()
9841 };
9842 self.consume(TokenType::LBrace)?;
9843 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9844 let key = self.consume_any_ident_or_kw()?.value;
9845 self.consume(TokenType::Colon)?;
9846 match key.as_str() {
9847 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
9848 "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
9849 "key" => node.key_params = self.parse_bracketed_identifiers()?,
9850 "default" => {
9851 node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
9852 }
9853 "apply_to_effects" => {
9854 node.apply_to_effects = self.parse_bracketed_identifiers()?
9855 }
9856 "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
9857 other => return Err(self.error(&format!("unknown cache field `{other}`"))),
9858 }
9859 if self.check(TokenType::Comma) {
9860 self.consume(TokenType::Comma)?;
9861 }
9862 }
9863 self.consume(TokenType::RBrace)?;
9864 Ok(node)
9865 }
9866
9867 fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
9875 let tok = self.consume(TokenType::Document)?;
9876 let name = self.consume(TokenType::Identifier)?.value;
9877 let mut node = crate::ast::DocumentDefinition {
9878 name,
9879 loc: Loc {
9880 line: tok.line,
9881 column: tok.column,
9882 },
9883 ..Default::default()
9884 };
9885 self.consume(TokenType::LBrace)?;
9886 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9887 let field = self.current().clone();
9888 let field_name = field.value.clone();
9889 self.advance();
9890 if self.check(TokenType::Colon) {
9891 self.advance();
9892 match field_name.as_str() {
9893 "target" => node.target = self.consume_any_ident_or_kw()?.value,
9894 "template" => node.template = self.parse_dotted_identifier()?,
9895 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
9896 "effects" => node.effects = Some(self.parse_effect_row()?),
9897 other => {
9898 return Err(self.error(&format!(
9899 "unknown document field `{other}` in document `{}` — expected \
9900 `target:` / `template:` / `provenance:` / `effects:`, or a body \
9901 block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
9902 node.name
9903 )))
9904 }
9905 }
9906 } else if self.check(TokenType::LBrace) {
9907 node.blocks
9908 .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
9909 } else {
9910 return Err(self.error(&format!(
9911 "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
9912 body block `{field_name} {{ … }}`",
9913 node.name
9914 )));
9915 }
9916 if self.check(TokenType::Comma) {
9917 self.advance();
9918 }
9919 }
9920 self.consume(TokenType::RBrace)?;
9921 Ok(node)
9922 }
9923
9924 fn parse_doc_block_body(
9930 &mut self,
9931 kind: String,
9932 line: u32,
9933 column: u32,
9934 ) -> Result<crate::ast::DocBlock, ParseError> {
9935 let mut block = crate::ast::DocBlock {
9936 kind,
9937 loc: Loc { line, column },
9938 ..Default::default()
9939 };
9940 self.consume(TokenType::LBrace)?;
9941 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9942 let name_tok = self.current().clone();
9943 let name = self.consume_any_ident_or_kw()?.value;
9944 if self.check(TokenType::Colon) {
9945 self.advance();
9946 let value = self.parse_doc_scalar()?;
9947 block.fields.push((name, value));
9948 } else if self.check(TokenType::LBrace) {
9949 let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
9950 block.children.push(child);
9951 } else {
9952 return Err(self.error(&format!(
9953 "in document block `{}`: `{name}` must be a `field:` value or open a nested \
9954 block `{name} {{ … }}`",
9955 block.kind
9956 )));
9957 }
9958 if self.check(TokenType::Comma) {
9959 self.advance();
9960 }
9961 }
9962 self.consume(TokenType::RBrace)?;
9963 Ok(block)
9964 }
9965
9966 fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
9971 let tok = self.current().clone();
9972 match tok.ttype {
9973 TokenType::StringLit => {
9974 self.advance();
9975 Ok(crate::ast::DocScalar::Text(tok.value))
9976 }
9977 TokenType::Integer => {
9978 self.advance();
9979 Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
9980 }
9981 TokenType::Bool => {
9982 self.advance();
9983 Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
9984 }
9985 TokenType::LBracket => {
9986 let items = self.parse_bracketed_strings()?;
9987 Ok(crate::ast::DocScalar::List(items))
9988 }
9989 _ => {
9990 let name = self.consume_any_ident_or_kw()?.value;
9991 Ok(crate::ast::DocScalar::Ref(name))
9992 }
9993 }
9994 }
9995
9996 fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10021 let tok = self.consume(TokenType::Notify)?;
10022 let name = self.consume(TokenType::Identifier)?.value;
10023 let mut node = crate::ast::NotifyDefinition {
10024 name,
10025 loc: Loc {
10026 line: tok.line,
10027 column: tok.column,
10028 },
10029 ..Default::default()
10030 };
10031 self.consume(TokenType::LBrace)?;
10032 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10033 let field = self.current().clone();
10034 let field_name = field.value.clone();
10035 self.advance();
10036 if self.check(TokenType::Colon) {
10037 self.advance();
10038 match field_name.as_str() {
10039 "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10040 "to" => {
10041 if self.current().value == "secret" && self.peek_is_lparen() {
10045 self.advance(); self.consume(TokenType::LParen)?;
10047 node.to_secret = self.parse_dotted_identifier()?;
10048 self.consume(TokenType::RParen)?;
10049 node.to_is_secret = true;
10050 } else if self.check(TokenType::StringLit) {
10051 node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10052 node.to_is_secret = false;
10053 } else {
10054 node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10055 node.to_is_secret = false;
10056 }
10057 }
10058 "template" => {
10059 node.template = self.consume(TokenType::StringLit)?.value.clone()
10060 }
10061 "window" => {
10062 if self.check(TokenType::Integer) {
10065 let n = self.consume(TokenType::Integer)?.value.clone();
10066 let unit = self.consume_any_ident_or_kw()?.value.clone();
10067 node.window = format!("{n}{unit}");
10068 } else {
10069 node.window = self.consume_any_ident_or_kw()?.value.clone();
10070 }
10071 }
10072 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10073 "effects" => node.effects = Some(self.parse_effect_row()?),
10074 other => {
10075 return Err(self.error(&format!(
10076 "unknown notify field `{other}` in notify `{}` — expected \
10077 `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10078 `effects:`",
10079 node.name
10080 )))
10081 }
10082 }
10083 }
10084 }
10085 self.consume(TokenType::RBrace)?;
10086 Ok(node)
10087 }
10088
10089 fn peek_is_identifier(&self) -> bool {
10093 self.tokens
10094 .get(self.pos + 1)
10095 .map(|t| t.ttype == TokenType::Identifier)
10096 .unwrap_or(false)
10097 }
10098
10099 fn peek_is_lparen(&self) -> bool {
10100 self.tokens
10101 .get(self.pos + 1)
10102 .map(|t| t.ttype == TokenType::LParen)
10103 .unwrap_or(false)
10104 }
10105
10106 fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10108 let tok = self.consume(TokenType::Deliver)?;
10109 let name = self.consume(TokenType::Identifier)?.value;
10110 let mut node = crate::ast::DeliverDefinition {
10111 name,
10112 loc: Loc {
10113 line: tok.line,
10114 column: tok.column,
10115 },
10116 ..Default::default()
10117 };
10118 self.consume(TokenType::LBrace)?;
10119 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10120 let field = self.current().clone();
10121 let field_name = field.value.clone();
10122 self.advance();
10123 if self.check(TokenType::Colon) {
10124 self.advance();
10125 match field_name.as_str() {
10126 "target" => node.target = self.consume_any_ident_or_kw()?.value,
10127 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10128 "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10129 "effects" => node.effects = Some(self.parse_effect_row()?),
10130 other => {
10131 return Err(self.error(&format!(
10132 "unknown deliver field `{other}` in deliver `{}` — expected \
10133 `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10134 block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10135 `add_note {{ … }}`)",
10136 node.name
10137 )))
10138 }
10139 }
10140 } else if self.check(TokenType::LBrace) {
10141 node.ops
10142 .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10143 } else {
10144 return Err(self.error(&format!(
10145 "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10146 operation block `{field_name} {{ … }}`",
10147 node.name
10148 )));
10149 }
10150 if self.check(TokenType::Comma) {
10151 self.advance();
10152 }
10153 }
10154 self.consume(TokenType::RBrace)?;
10155 Ok(node)
10156 }
10157
10158 fn parse_deliver_op(
10162 &mut self,
10163 kind: String,
10164 line: u32,
10165 column: u32,
10166 ) -> Result<crate::ast::DeliverOp, ParseError> {
10167 let mut op = crate::ast::DeliverOp {
10168 kind,
10169 loc: Loc { line, column },
10170 ..Default::default()
10171 };
10172 self.consume(TokenType::LBrace)?;
10173 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10174 let name = self.consume_any_ident_or_kw()?.value;
10175 self.consume(TokenType::Colon).map_err(|_| {
10176 self.error(&format!(
10177 "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10178 operation binds scalar fields, it takes no nested blocks",
10179 op.kind
10180 ))
10181 })?;
10182 let value = self.parse_doc_scalar()?;
10183 op.fields.push((name, value));
10184 if self.check(TokenType::Comma) {
10185 self.advance();
10186 }
10187 }
10188 self.consume(TokenType::RBrace)?;
10189 Ok(op)
10190 }
10191
10192 fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10198 let tok = self.consume(TokenType::Savant)?;
10199 let name = self.consume(TokenType::Identifier)?.value;
10200 let mut node = SavantDefinition {
10201 name,
10202 loc: Loc {
10203 line: tok.line,
10204 column: tok.column,
10205 },
10206 ..Default::default()
10207 };
10208 self.consume(TokenType::LBrace)?;
10209 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10210 let field = self.current().clone();
10211 let field_name = field.value.clone();
10212 self.advance();
10213 if self.check(TokenType::Colon) {
10214 self.advance();
10215 match field_name.as_str() {
10216 "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10217 other => {
10218 return Err(self.error(&format!(
10219 "unknown savant field `{other}` in savant `{}` — expected \
10220 `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10221 node.name
10222 )))
10223 }
10224 }
10225 } else if field_name == "cognition" {
10226 node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10227 } else if field_name == "memory" {
10228 node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10229 } else if field_name == "budget" {
10230 node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10231 } else if field_name == "mandate" {
10232 node.mandates
10233 .push(self.parse_savant_mandate(field.line, field.column)?);
10234 } else {
10235 return Err(self.error(&format!(
10236 "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10237 `cognition` / `memory` / `budget` / `mandate` block",
10238 node.name
10239 )));
10240 }
10241 if self.check(TokenType::Comma) {
10242 self.advance();
10243 }
10244 }
10245 self.consume(TokenType::RBrace)?;
10246 Ok(node)
10247 }
10248
10249 fn parse_savant_cognition(
10252 &mut self,
10253 line: u32,
10254 column: u32,
10255 ) -> Result<SavantCognition, ParseError> {
10256 self.consume(TokenType::LBrace)?;
10257 let mut node = SavantCognition {
10258 loc: Loc { line, column },
10259 ..Default::default()
10260 };
10261 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10262 let key = self.consume_any_ident_or_kw()?.value;
10263 self.consume(TokenType::Colon)?;
10264 match key.as_str() {
10265 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10266 "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10267 "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10268 other => {
10269 return Err(self.error(&format!(
10270 "unknown savant `cognition` field `{other}` — expected \
10271 `depth` / `entropic_threshold` / `divergence`"
10272 )))
10273 }
10274 }
10275 if self.check(TokenType::Comma) {
10276 self.advance();
10277 }
10278 }
10279 self.consume(TokenType::RBrace)?;
10280 Ok(node)
10281 }
10282
10283 fn parse_savant_memory(
10286 &mut self,
10287 line: u32,
10288 column: u32,
10289 ) -> Result<SavantMemory, ParseError> {
10290 self.consume(TokenType::LBrace)?;
10291 let mut node = SavantMemory {
10292 loc: Loc { line, column },
10293 ..Default::default()
10294 };
10295 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10296 let key = self.consume_any_ident_or_kw()?.value;
10297 self.consume(TokenType::Colon)?;
10298 match key.as_str() {
10299 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10300 "corpus_graph" => {
10301 node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10302 }
10303 "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10304 other => {
10305 return Err(self.error(&format!(
10306 "unknown savant `memory` field `{other}` — expected \
10307 `backend` / `corpus_graph` / `isolation_level`"
10308 )))
10309 }
10310 }
10311 if self.check(TokenType::Comma) {
10312 self.advance();
10313 }
10314 }
10315 self.consume(TokenType::RBrace)?;
10316 Ok(node)
10317 }
10318
10319 fn parse_savant_budget(
10322 &mut self,
10323 line: u32,
10324 column: u32,
10325 ) -> Result<SavantBudget, ParseError> {
10326 self.consume(TokenType::LBrace)?;
10327 let mut node = SavantBudget {
10328 loc: Loc { line, column },
10329 ..Default::default()
10330 };
10331 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10332 let key = self.consume_any_ident_or_kw()?.value;
10333 self.consume(TokenType::Colon)?;
10334 match key.as_str() {
10335 "max_iterations" => node.max_iterations = self.parse_optional_int(),
10336 "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10337 other => {
10338 return Err(self.error(&format!(
10339 "unknown savant `budget` field `{other}` — expected \
10340 `max_iterations` / `max_tool_synth`"
10341 )))
10342 }
10343 }
10344 if self.check(TokenType::Comma) {
10345 self.advance();
10346 }
10347 }
10348 self.consume(TokenType::RBrace)?;
10349 Ok(node)
10350 }
10351
10352 fn parse_savant_mandate(
10355 &mut self,
10356 line: u32,
10357 column: u32,
10358 ) -> Result<SavantMandate, ParseError> {
10359 let name = self.consume(TokenType::Identifier)?.value;
10360 let mut node = SavantMandate {
10361 name,
10362 loc: Loc { line, column },
10363 ..Default::default()
10364 };
10365 self.consume(TokenType::LBrace)?;
10366 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10367 let key = self.consume_any_ident_or_kw()?.value;
10368 self.consume(TokenType::Colon)?;
10369 match key.as_str() {
10370 "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10371 "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10372 other => {
10373 return Err(self.error(&format!(
10374 "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10375 )))
10376 }
10377 }
10378 if self.check(TokenType::Comma) {
10379 self.advance();
10380 }
10381 }
10382 self.consume(TokenType::RBrace)?;
10383 Ok(node)
10384 }
10385
10386 fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10391 let tok = self.consume(TokenType::Synth)?;
10392 let name = self.consume(TokenType::Identifier)?.value;
10393 let mut node = SynthDefinition {
10394 name,
10395 loc: Loc {
10396 line: tok.line,
10397 column: tok.column,
10398 },
10399 ..Default::default()
10400 };
10401 self.consume(TokenType::LBrace)?;
10402 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10403 let key = self.consume_any_ident_or_kw()?.value;
10404 self.consume(TokenType::Colon)?;
10405 match key.as_str() {
10406 "target" => node.target = self.consume(TokenType::StringLit)?.value,
10407 "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10408 "language" => node.language = self.consume_any_ident_or_kw()?.value,
10409 "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10410 "review" => node.review = self.consume_any_ident_or_kw()?.value,
10411 "max_lines" => node.max_lines = self.parse_optional_int(),
10412 other => {
10413 return Err(self.error(&format!(
10414 "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10415 / `language` / `sandbox` / `review` / `max_lines`",
10416 node.name
10417 )))
10418 }
10419 }
10420 if self.check(TokenType::Comma) {
10421 self.consume(TokenType::Comma)?;
10422 }
10423 }
10424 self.consume(TokenType::RBrace)?;
10425 Ok(node)
10426 }
10427
10428 fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10433 let tok = self.consume(TokenType::Voice)?;
10434 let name = self.consume(TokenType::Identifier)?.value;
10435 let mut node = VoiceDefinition {
10436 name,
10437 loc: Loc { line: tok.line, column: tok.column },
10438 ..Default::default()
10439 };
10440 self.consume(TokenType::LBrace)?;
10441 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10442 let key = self.consume_any_ident_or_kw()?.value;
10443 self.consume(TokenType::Colon)?;
10444 match key.as_str() {
10445 "stt" => node.stt = Some(self.parse_upstream_ref()?),
10447 "tts" => node.tts = Some(self.parse_upstream_ref()?),
10448 "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10449 "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10450 "interruptible" => {
10451 let v = self.consume_any_ident_or_kw()?.value;
10452 node.interruptible = v == "true";
10453 }
10454 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10455 "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10456 "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10457 other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10458 }
10459 if self.check(TokenType::Comma) {
10460 self.consume(TokenType::Comma)?;
10461 }
10462 }
10463 self.consume(TokenType::RBrace)?;
10464 Ok(node)
10465 }
10466
10467 fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10470 let base = self.consume(TokenType::Identifier)?.value;
10471 if self.check(TokenType::At) {
10472 self.advance();
10473 let version = self.consume_any_ident_or_kw()?.value;
10474 Ok(format!("{base}@{version}"))
10475 } else {
10476 Ok(base)
10477 }
10478 }
10479
10480 fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10486 self.consume(TokenType::LBracket)?;
10487 let mut rules = Vec::new();
10488 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10489 let dir_tok = self.current().clone();
10490 let direction = match dir_tok.ttype {
10491 TokenType::Send => "send",
10492 TokenType::Receive => "receive",
10493 _ => {
10494 return Err(self.error(&format!(
10495 "upstream map rule must start with `send` or `receive`, got `{}`",
10496 dir_tok.value
10497 )))
10498 }
10499 };
10500 self.advance();
10501 let message = self.consume(TokenType::Identifier)?.value;
10502 self.consume(TokenType::As)?;
10503 let framing = self.consume_any_ident_or_kw()?.value;
10504 let mut rule = UpstreamMapRule {
10505 direction: direction.to_string(),
10506 message,
10507 framing,
10508 loc: Loc { line: dir_tok.line, column: dir_tok.column },
10509 ..Default::default()
10510 };
10511 if self.current().value == "tag" {
10513 self.advance();
10514 rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10515 } else if self.current().value == "when" {
10516 self.advance();
10521 rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10522 if self.check(TokenType::Assign) {
10523 self.advance();
10524 rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10525 }
10526 }
10527 rules.push(rule);
10528 if self.check(TokenType::Comma) {
10529 self.advance();
10530 }
10531 }
10532 self.consume(TokenType::RBracket)?;
10533 Ok(rules)
10534 }
10535
10536 fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10540 self.consume(TokenType::LBrace)?;
10541 let mut backoff_ms: Option<i64> = None;
10542 let mut max_attempts: Option<i64> = None;
10543 let mut on_exhausted: Option<String> = None;
10544 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10545 let key = self.consume_any_ident_or_kw()?.value;
10546 self.consume(TokenType::Colon)?;
10547 match key.as_str() {
10548 "backoff_ms" => {
10549 backoff_ms = Some(
10550 self.consume(TokenType::Integer)?
10551 .value
10552 .parse::<i64>()
10553 .map_err(|_| self.error("backoff_ms must be an integer"))?,
10554 )
10555 }
10556 "max_attempts" => {
10557 max_attempts = Some(
10558 self.consume(TokenType::Integer)?
10559 .value
10560 .parse::<i64>()
10561 .map_err(|_| self.error("max_attempts must be an integer"))?,
10562 )
10563 }
10564 "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10565 other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10566 }
10567 if self.check(TokenType::Comma) {
10568 self.consume(TokenType::Comma)?;
10569 }
10570 }
10571 self.consume(TokenType::RBrace)?;
10572 match (backoff_ms, max_attempts, on_exhausted) {
10573 (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10574 _ => Err(self.error(
10575 "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10576 )),
10577 }
10578 }
10579
10580 fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10582 self.consume(TokenType::LBracket)?;
10583 let mut steps = Vec::new();
10584 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10585 steps.push(self.parse_session_step()?);
10586 if self.check(TokenType::Comma) {
10587 self.advance();
10588 }
10589 }
10590 self.consume(TokenType::RBracket)?;
10591 Ok(steps)
10592 }
10593
10594 fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10598 self.consume(TokenType::LBrace)?;
10599 let mut steps = Vec::new();
10600 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10601 steps.push(self.parse_session_step()?);
10602 if self.check(TokenType::Comma) {
10603 self.advance();
10604 }
10605 }
10606 self.consume(TokenType::RBrace)?;
10607 Ok(steps)
10608 }
10609
10610 fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10611 let tok = self.current().clone();
10612 let loc = Loc { line: tok.line, column: tok.column };
10613 match tok.ttype {
10614 TokenType::Send => {
10615 self.advance();
10616 let msg = self.consume_any_ident_or_kw()?;
10617 Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10618 }
10619 TokenType::Receive => {
10620 self.advance();
10621 let msg = self.consume_any_ident_or_kw()?;
10622 Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10623 }
10624 TokenType::Loop => {
10625 self.advance();
10626 Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10627 }
10628 TokenType::End => {
10629 self.advance();
10630 Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10631 }
10632 TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10635 self.parse_session_choice(&tok.value, loc)
10636 }
10637 TokenType::Identifier if tok.value == "interrupt" => {
10640 self.parse_session_interrupt(loc)
10641 }
10642 _ if tok.value == "resume" => {
10660 self.advance();
10661 Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10662 }
10663 _ => Err(ParseError {
10664 message: format!(
10665 "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10666 tok.value
10667 ),
10668 line: tok.line,
10669 column: tok.column,
10670 ..Default::default()
10671 }),
10672 }
10673 }
10674
10675 fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10680 let t = self.current().clone();
10681 if t.value != kw {
10682 return Err(ParseError {
10683 message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10684 line: t.line,
10685 column: t.column,
10686 ..Default::default()
10687 });
10688 }
10689 self.advance();
10690 Ok(())
10691 }
10692
10693 fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10701 self.advance(); let body = self.parse_session_step_block()?;
10705 self.consume_contextual("on")?;
10707 let signal = self.consume_any_ident_or_kw()?;
10708 self.consume_contextual("as")?;
10710 let binder = self.consume_any_ident_or_kw()?;
10711 self.consume_contextual("resumable")?;
10713 let handler = self.parse_session_step_block()?;
10714 Ok(SessionStep {
10715 op: "interrupt".into(),
10716 message_type: signal.value,
10717 branches: vec![
10718 SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10719 SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10720 ],
10721 binder: binder.value,
10722 resumable: true,
10723 loc,
10724 })
10725 }
10726
10727 fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10730 self.advance(); self.consume(TokenType::LBrace)?;
10732 let mut branches = Vec::new();
10733 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10734 let label_tok = self.consume_any_ident_or_kw()?;
10735 self.consume(TokenType::Colon)?;
10736 let steps = self.parse_session_steps()?;
10737 branches.push(SessionBranch {
10738 label: label_tok.value,
10739 steps,
10740 loc: Loc { line: label_tok.line, column: label_tok.column },
10741 });
10742 if self.check(TokenType::Comma) {
10743 self.advance();
10744 }
10745 }
10746 self.consume(TokenType::RBrace)?;
10747 Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10748 }
10749
10750 fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10752 let tok = self.consume(TokenType::Topology)?;
10753 let name = self.consume(TokenType::Identifier)?.value;
10754 let mut node = TopologyDefinition {
10755 name,
10756 nodes: Vec::new(),
10757 edges: Vec::new(),
10758 loc: Loc {
10759 line: tok.line,
10760 column: tok.column,
10761 },
10762 leading_trivia: Vec::new(),
10763 trailing_trivia: Vec::new(),
10764 };
10765 self.consume(TokenType::LBrace)?;
10766 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10767 let field_name = self.current().value.clone();
10768 self.advance();
10769 if !self.check(TokenType::Colon) {
10770 if self.check(TokenType::LBrace) {
10771 self.skip_braced_block()?;
10772 }
10773 continue;
10774 }
10775 self.advance();
10776 match field_name.as_str() {
10777 "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10778 "edges" => node.edges = self.parse_topology_edges()?,
10779 _ => self.skip_value(),
10780 }
10781 }
10782 self.consume(TokenType::RBrace)?;
10783 Ok(node)
10784 }
10785
10786 fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10787 self.consume(TokenType::LBracket)?;
10788 let mut edges = Vec::new();
10789 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10790 edges.push(self.parse_topology_edge()?);
10791 if self.check(TokenType::Comma) {
10792 self.advance();
10793 }
10794 }
10795 self.consume(TokenType::RBracket)?;
10796 Ok(edges)
10797 }
10798
10799 fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10800 let src_tok = self.consume_any_ident_or_kw()?;
10801 self.consume(TokenType::Arrow)?;
10802 let tgt_tok = self.consume_any_ident_or_kw()?;
10803 self.consume(TokenType::Colon)?;
10804 let sess_tok = self.consume_any_ident_or_kw()?;
10805 Ok(TopologyEdge {
10806 source: src_tok.value,
10807 target: tgt_tok.value,
10808 session_ref: sess_tok.value,
10809 loc: Loc {
10810 line: src_tok.line,
10811 column: src_tok.column,
10812 },
10813 })
10814 }
10815
10816 fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10820 let tok = self.consume(TokenType::Immune)?;
10821 let name = self.consume(TokenType::Identifier)?.value;
10822 let mut node = ImmuneDefinition {
10823 name,
10824 watch: Vec::new(),
10825 sensitivity: None,
10826 baseline: "learned".to_string(),
10827 window: 100,
10828 scope: String::new(),
10829 tau: String::new(),
10830 decay: "exponential".to_string(),
10831 loc: Loc {
10832 line: tok.line,
10833 column: tok.column,
10834 },
10835 leading_trivia: Vec::new(),
10836 trailing_trivia: Vec::new(),
10837 };
10838 self.consume(TokenType::LBrace)?;
10839 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10840 let field_name = self.current().value.clone();
10841 self.advance();
10842 if !self.check(TokenType::Colon) {
10843 if self.check(TokenType::LBrace) {
10844 self.skip_braced_block()?;
10845 }
10846 continue;
10847 }
10848 self.advance();
10849 match field_name.as_str() {
10850 "watch" => node.watch = self.parse_bracketed_identifiers()?,
10851 "sensitivity" => node.sensitivity = self.parse_optional_float(),
10852 "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
10853 "window" => {
10854 if let Some(v) = self.parse_optional_int() {
10855 node.window = v;
10856 }
10857 }
10858 "scope" => {
10859 let s_tok = self.consume_any_ident_or_kw()?;
10860 let s = s_tok.value;
10861 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10862 return Err(ParseError {
10863 message: format!(
10864 "Invalid scope '{s}' in immune '{}' — \
10865 expected tenant | flow | global",
10866 node.name
10867 ),
10868 line: s_tok.line,
10869 column: s_tok.column,
10870 ..Default::default()
10871 });
10872 }
10873 node.scope = s;
10874 }
10875 "tau" => {
10876 let t = self.current().clone();
10877 match t.ttype {
10878 TokenType::Duration | TokenType::StringLit => {
10879 self.advance();
10880 node.tau = t.value;
10881 }
10882 _ => node.tau = self.consume_any_ident_or_kw()?.value,
10883 }
10884 }
10885 "decay" => {
10886 let d_tok = self.consume_any_ident_or_kw()?;
10887 let d = d_tok.value;
10888 if !matches!(d.as_str(), "exponential" | "linear" | "none") {
10889 return Err(ParseError {
10890 message: format!(
10891 "Invalid decay '{d}' in immune '{}' — \
10892 expected exponential | linear | none",
10893 node.name
10894 ),
10895 line: d_tok.line,
10896 column: d_tok.column,
10897 ..Default::default()
10898 });
10899 }
10900 node.decay = d;
10901 }
10902 _ => self.skip_value(),
10903 }
10904 }
10905 self.consume(TokenType::RBrace)?;
10906 Ok(node)
10907 }
10908
10909 fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
10911 let tok = self.consume(TokenType::Reflex)?;
10912 let name = self.consume(TokenType::Identifier)?.value;
10913 let mut node = ReflexDefinition {
10914 name,
10915 trigger: String::new(),
10916 on_level: "doubt".to_string(),
10917 action: String::new(),
10918 scope: String::new(),
10919 sla: String::new(),
10920 loc: Loc {
10921 line: tok.line,
10922 column: tok.column,
10923 },
10924 leading_trivia: Vec::new(),
10925 trailing_trivia: Vec::new(),
10926 };
10927 self.consume(TokenType::LBrace)?;
10928 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10929 let field_name = self.current().value.clone();
10930 self.advance();
10931 if !self.check(TokenType::Colon) {
10932 if self.check(TokenType::LBrace) {
10933 self.skip_braced_block()?;
10934 }
10935 continue;
10936 }
10937 self.advance();
10938 match field_name.as_str() {
10939 "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
10940 "on_level" => {
10941 let l_tok = self.consume_any_ident_or_kw()?;
10942 let l = l_tok.value;
10943 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
10944 return Err(ParseError {
10945 message: format!(
10946 "Invalid on_level '{l}' in reflex '{}' — \
10947 expected know | believe | speculate | doubt",
10948 node.name
10949 ),
10950 line: l_tok.line,
10951 column: l_tok.column,
10952 ..Default::default()
10953 });
10954 }
10955 node.on_level = l;
10956 }
10957 "action" => {
10958 let a_tok = self.consume_any_ident_or_kw()?;
10959 let a = a_tok.value;
10960 if !matches!(
10961 a.as_str(),
10962 "drop"
10963 | "revoke"
10964 | "emit"
10965 | "redact"
10966 | "quarantine"
10967 | "terminate"
10968 | "alert"
10969 ) {
10970 return Err(ParseError {
10971 message: format!(
10972 "Invalid action '{a}' in reflex '{}' — \
10973 expected drop | revoke | emit | redact | \
10974 quarantine | terminate | alert",
10975 node.name
10976 ),
10977 line: a_tok.line,
10978 column: a_tok.column,
10979 ..Default::default()
10980 });
10981 }
10982 node.action = a;
10983 }
10984 "scope" => {
10985 let s_tok = self.consume_any_ident_or_kw()?;
10986 let s = s_tok.value;
10987 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
10988 return Err(ParseError {
10989 message: format!(
10990 "Invalid scope '{s}' in reflex '{}' — \
10991 expected tenant | flow | global",
10992 node.name
10993 ),
10994 line: s_tok.line,
10995 column: s_tok.column,
10996 ..Default::default()
10997 });
10998 }
10999 node.scope = s;
11000 }
11001 "sla" => {
11002 let t = self.current().clone();
11003 match t.ttype {
11004 TokenType::Duration | TokenType::StringLit => {
11005 self.advance();
11006 node.sla = t.value;
11007 }
11008 _ => node.sla = self.consume_any_ident_or_kw()?.value,
11009 }
11010 }
11011 _ => self.skip_value(),
11012 }
11013 }
11014 self.consume(TokenType::RBrace)?;
11015 Ok(node)
11016 }
11017
11018 fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11020 let tok = self.consume(TokenType::Heal)?;
11021 let name = self.consume(TokenType::Identifier)?.value;
11022 let mut node = HealDefinition {
11023 name,
11024 source: String::new(),
11025 on_level: "doubt".to_string(),
11026 mode: "human_in_loop".to_string(),
11027 scope: String::new(),
11028 review_sla: String::new(),
11029 shield_ref: String::new(),
11030 max_patches: 3,
11031 loc: Loc {
11032 line: tok.line,
11033 column: tok.column,
11034 },
11035 leading_trivia: Vec::new(),
11036 trailing_trivia: Vec::new(),
11037 };
11038 self.consume(TokenType::LBrace)?;
11039 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11040 let field_name = self.current().value.clone();
11041 self.advance();
11042 if !self.check(TokenType::Colon) {
11043 if self.check(TokenType::LBrace) {
11044 self.skip_braced_block()?;
11045 }
11046 continue;
11047 }
11048 self.advance();
11049 match field_name.as_str() {
11050 "source" => node.source = self.consume_any_ident_or_kw()?.value,
11051 "on_level" => {
11052 let l_tok = self.consume_any_ident_or_kw()?;
11053 let l = l_tok.value;
11054 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11055 return Err(ParseError {
11056 message: format!(
11057 "Invalid on_level '{l}' in heal '{}' — \
11058 expected know | believe | speculate | doubt",
11059 node.name
11060 ),
11061 line: l_tok.line,
11062 column: l_tok.column,
11063 ..Default::default()
11064 });
11065 }
11066 node.on_level = l;
11067 }
11068 "mode" => {
11069 let m_tok = self.consume_any_ident_or_kw()?;
11070 let m = m_tok.value;
11071 if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11072 return Err(ParseError {
11073 message: format!(
11074 "Invalid mode '{m}' in heal '{}' — \
11075 expected audit_only | human_in_loop | adversarial",
11076 node.name
11077 ),
11078 line: m_tok.line,
11079 column: m_tok.column,
11080 ..Default::default()
11081 });
11082 }
11083 node.mode = m;
11084 }
11085 "scope" => {
11086 let s_tok = self.consume_any_ident_or_kw()?;
11087 let s = s_tok.value;
11088 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11089 return Err(ParseError {
11090 message: format!(
11091 "Invalid scope '{s}' in heal '{}' — \
11092 expected tenant | flow | global",
11093 node.name
11094 ),
11095 line: s_tok.line,
11096 column: s_tok.column,
11097 ..Default::default()
11098 });
11099 }
11100 node.scope = s;
11101 }
11102 "review_sla" => {
11103 let t = self.current().clone();
11104 match t.ttype {
11105 TokenType::Duration | TokenType::StringLit => {
11106 self.advance();
11107 node.review_sla = t.value;
11108 }
11109 _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11110 }
11111 }
11112 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11113 "max_patches" => {
11114 if let Some(v) = self.parse_optional_int() {
11115 node.max_patches = v;
11116 }
11117 }
11118 _ => self.skip_value(),
11119 }
11120 }
11121 self.consume(TokenType::RBrace)?;
11122 Ok(node)
11123 }
11124
11125 fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11129 let tok = self.consume(TokenType::Component)?;
11130 let name = self.consume(TokenType::Identifier)?.value;
11131 let mut node = ComponentDefinition {
11132 name,
11133 renders: String::new(),
11134 via_shield: String::new(),
11135 on_interact: String::new(),
11136 render_hint: "custom".to_string(),
11137 loc: Loc {
11138 line: tok.line,
11139 column: tok.column,
11140 },
11141 leading_trivia: Vec::new(),
11142 trailing_trivia: Vec::new(),
11143 };
11144 self.consume(TokenType::LBrace)?;
11145 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11146 let field_name = self.current().value.clone();
11147 self.advance();
11148 if !self.check(TokenType::Colon) {
11149 if self.check(TokenType::LBrace) {
11150 self.skip_braced_block()?;
11151 }
11152 continue;
11153 }
11154 self.advance();
11155 match field_name.as_str() {
11156 "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11157 "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11158 "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11159 "render_hint" => {
11160 let h_tok = self.consume_any_ident_or_kw()?;
11161 let h = h_tok.value;
11162 if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11163 return Err(ParseError {
11164 message: format!(
11165 "Invalid render_hint '{h}' in component '{}' — \
11166 expected card | list | form | chart | custom",
11167 node.name
11168 ),
11169 line: h_tok.line,
11170 column: h_tok.column,
11171 ..Default::default()
11172 });
11173 }
11174 node.render_hint = h;
11175 }
11176 _ => self.skip_value(),
11177 }
11178 }
11179 self.consume(TokenType::RBrace)?;
11180 Ok(node)
11181 }
11182
11183 fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11185 let tok = self.consume(TokenType::View)?;
11186 let name = self.consume(TokenType::Identifier)?.value;
11187 let mut node = ViewDefinition {
11188 name,
11189 title: String::new(),
11190 components: Vec::new(),
11191 route: String::new(),
11192 loc: Loc {
11193 line: tok.line,
11194 column: tok.column,
11195 },
11196 leading_trivia: Vec::new(),
11197 trailing_trivia: Vec::new(),
11198 };
11199 self.consume(TokenType::LBrace)?;
11200 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11201 let field_name = self.current().value.clone();
11202 self.advance();
11203 if !self.check(TokenType::Colon) {
11204 if self.check(TokenType::LBrace) {
11205 self.skip_braced_block()?;
11206 }
11207 continue;
11208 }
11209 self.advance();
11210 match field_name.as_str() {
11211 "title" => node.title = self.consume(TokenType::StringLit)?.value,
11212 "components" => node.components = self.parse_bracketed_identifiers()?,
11213 "route" => node.route = self.consume(TokenType::StringLit)?.value,
11214 _ => self.skip_value(),
11215 }
11216 }
11217 self.consume(TokenType::RBrace)?;
11218 Ok(node)
11219 }
11220
11221 fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11222 let tok = self.consume(TokenType::AxonEndpoint)?;
11223 let name = self.consume(TokenType::Identifier)?.value;
11224 let mut node = AxonEndpointDefinition {
11225 name,
11226 method: String::new(),
11227 path: String::new(),
11228 body_type: String::new(),
11229 execute_flow: String::new(),
11230 output_type: String::new(),
11231 shield_ref: String::new(),
11232 cors_ref: String::new(),
11235 retries: None,
11236 timeout: String::new(),
11237 compliance: Vec::new(),
11238 transport: "json".to_string(),
11240 keepalive: String::new(),
11241 transport_explicit: false,
11247 implicit_transport: String::new(),
11248 requires_capabilities: Vec::new(),
11250 public: false,
11253 replay_explicit: false,
11257 replay: false,
11258 transport_dialect: String::new(),
11263 has_algebraic_stream_effect: false,
11269 backend: String::new(),
11274 path_params: Vec::new(),
11279 query_params: Vec::new(),
11284 loc: Loc {
11285 line: tok.line,
11286 column: tok.column,
11287 },
11288 leading_trivia: Vec::new(),
11289 trailing_trivia: Vec::new(),
11290 };
11291 self.consume(TokenType::LBrace)?;
11292 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11293 let field_name = self.current().value.clone();
11294 self.advance();
11295 if self.check(TokenType::Colon) {
11296 self.advance();
11297 match field_name.as_str() {
11298 "method" => {
11299 let value_tok = self.consume_any_ident_or_kw()?;
11306 let value_upper = value_tok.value.to_uppercase();
11307 if !axonendpoint_is_valid_method(&value_upper) {
11308 let hint = crate::smart_suggest::suggest_for(
11309 &value_upper,
11310 AXONENDPOINT_METHOD_VALUES,
11311 );
11312 let base = format!(
11313 "Invalid method '{}' in axonendpoint '{}'.",
11314 value_tok.value, node.name
11315 );
11316 let message = if hint.is_empty() {
11317 format!(
11318 "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11319 value_tok.value
11320 )
11321 } else {
11322 format!(
11323 "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11324 value_tok.value
11325 )
11326 };
11327 return Err(ParseError {
11328 message,
11329 line: value_tok.line,
11330 column: value_tok.column,
11331 ..Default::default()
11332 });
11333 }
11334 node.method = value_upper;
11335 }
11336 "path" => {
11337 node.path = self.consume(TokenType::StringLit)?.value.clone();
11338 match extract_path_param_names(&node.path) {
11346 Ok(names) => node.path_params = names,
11347 Err(dup) => {
11348 let cur = self.current().clone();
11349 return Err(ParseError {
11350 message: format!(
11351 "axonendpoint '{}' declares path '{}' \
11352 containing duplicate placeholder '{{{}}}'. \
11353 Each `{{name}}` in a `path:` must be \
11354 unique — the runtime cannot bind two \
11355 path segments to the same name.",
11356 node.name, node.path, dup,
11357 ),
11358 line: cur.line,
11359 column: cur.column,
11360 ..Default::default()
11361 });
11362 }
11363 }
11364 },
11365 "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11366 "query" => {
11367 let lbrace_tok = self.consume(TokenType::LBrace)?;
11386 let block_line = lbrace_tok.line;
11387 if !node.query_params.is_empty() {
11388 return Err(ParseError {
11389 message: format!(
11390 "axonendpoint '{}' declares `query: {{ … }}` \
11391 more than once. The query-parameter block \
11392 is unique per endpoint; combine all params \
11393 into a single block.",
11394 node.name,
11395 ),
11396 line: lbrace_tok.line,
11397 column: lbrace_tok.column,
11398 ..Default::default()
11399 });
11400 }
11401 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11402 let name_tok = self.consume(TokenType::Identifier)?;
11403 let field_name = name_tok.value.clone();
11404 if node
11406 .query_params
11407 .iter()
11408 .any(|f| f.name == field_name)
11409 {
11410 return Err(ParseError {
11411 message: format!(
11412 "axonendpoint '{}' declares duplicate \
11413 query param '{}' inside `query: {{ … }}`. \
11414 Each name must appear at most once \
11415 .",
11416 node.name, field_name,
11417 ),
11418 line: name_tok.line,
11419 column: name_tok.column,
11420 ..Default::default()
11421 });
11422 }
11423 self.consume(TokenType::Colon)?;
11424 let type_expr = self.parse_type_expr()?;
11425 if !type_expr.generic_param.is_empty() {
11437 let canonical_hint = if type_expr.name == "Optional" {
11438 format!(
11439 " Use `{}?` (the `?` suffix) for an \
11440 optional query param instead of \
11441 `Optional<{}>`.",
11442 type_expr.generic_param,
11443 type_expr.generic_param,
11444 )
11445 } else if type_expr.name == "List" {
11446 " Multi-value query params (e.g. `?tag=a&tag=b`) \
11447 are honest-deferred from v1.38.5; bind a \
11448 single-value `Text` query param and parse \
11449 the value inside the flow."
11450 .to_string()
11451 } else {
11452 String::new()
11453 };
11454 return Err(ParseError {
11455 message: format!(
11456 "axonendpoint '{}' query param '{}' uses \
11457 a generic type `{}<{}>`. Query params \
11458 take a primitive type from the closed \
11459 catalog ({}); the `?` suffix marks \
11460 optional.{} .",
11461 node.name,
11462 field_name,
11463 type_expr.name,
11464 type_expr.generic_param,
11465 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11466 canonical_hint,
11467 ),
11468 line: type_expr.loc.line,
11469 column: type_expr.loc.column,
11470 ..Default::default()
11471 });
11472 }
11473 if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11477 let hint = crate::smart_suggest::suggest_for(
11484 &type_expr.name,
11485 AXONENDPOINT_QUERY_PARAM_TYPES,
11486 );
11487 let hint_text = if hint.is_empty() {
11488 format!(
11489 " Expected one of: {}.",
11490 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11491 )
11492 } else {
11493 format!(
11494 " {} Expected one of: {}.",
11495 hint,
11496 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11497 )
11498 };
11499 return Err(ParseError {
11500 message: format!(
11501 "axonendpoint '{}' query param '{}' has \
11502 unsupported type '{}'.{} .",
11503 node.name, field_name, type_expr.name,
11504 hint_text,
11505 ),
11506 line: type_expr.loc.line,
11507 column: type_expr.loc.column,
11508 ..Default::default()
11509 });
11510 }
11511 node.query_params.push(TypeField {
11512 name: field_name,
11513 type_expr,
11514 loc: Loc {
11515 line: name_tok.line,
11516 column: name_tok.column,
11517 },
11518 });
11519 if self.check(TokenType::Comma) {
11525 self.advance();
11526 }
11527 let _ = block_line; }
11529 self.consume(TokenType::RBrace)?;
11530 },
11531 "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11532 "output" => {
11533 node.output_type = self.parse_output_type_string()?;
11551 }
11552 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11553 "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11555 "retries" => node.retries = self.parse_optional_int(),
11556 "timeout" => {
11557 let t = self.current().clone();
11558 self.advance();
11559 node.timeout = t.value.clone();
11560 }
11561 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11562 "replay" => {
11563 let value_tok = self.consume(TokenType::Bool)?;
11570 node.replay = value_tok.value.eq_ignore_ascii_case("true");
11571 node.replay_explicit = true;
11572 }
11573 "public" => {
11579 let value_tok = self.consume(TokenType::Bool)?;
11580 node.public = value_tok.value.eq_ignore_ascii_case("true");
11581 }
11582 "requires" => {
11583 let bracket_tok = self.current().clone();
11590 let items = self.parse_bracketed_dot_identifiers()?;
11591 for slug in &items {
11592 if !is_valid_capability_slug(slug) {
11593 return Err(ParseError {
11594 message: format!(
11595 "Invalid capability slug '{slug}' in axonendpoint '{}' \
11596 `requires:`. Capability slugs must match \
11597 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11598 lowercase identifiers starting with a letter. Examples: \
11599 `admin`, `legal.read`, `hipaa.phi.read`.",
11600 node.name
11601 ),
11602 line: bracket_tok.line,
11603 column: bracket_tok.column,
11604 ..Default::default()
11605 });
11606 }
11607 }
11608 node.requires_capabilities = items;
11609 }
11610 "transport" => {
11614 let value_tok = self.consume_any_ident_or_kw()?;
11615 let value = &value_tok.value;
11616 if !axonendpoint_is_valid_transport(value) {
11617 let hint = crate::smart_suggest::suggest_for(
11618 value,
11619 AXONENDPOINT_TRANSPORT_VALUES,
11620 );
11621 let base = format!(
11622 "Invalid transport '{}' in axonendpoint '{}'.",
11623 value, node.name
11624 );
11625 let message = if hint.is_empty() {
11626 format!("{base} expected json | sse | ndjson, found {value}")
11627 } else {
11628 format!(
11629 "{base} {hint} (expected json | sse | ndjson, found {value})"
11630 )
11631 };
11632 return Err(ParseError {
11633 message,
11634 line: value_tok.line,
11635 column: value_tok.column,
11636 ..Default::default()
11637 });
11638 }
11639 node.transport = value.clone();
11640 node.transport_explicit = true;
11645 if self.check(TokenType::LParen) {
11652 if value != "sse" {
11653 let tok = self.current().clone();
11654 return Err(ParseError {
11655 message: format!(
11656 "Dialect parametrization \
11657 `transport: {value}(<dialect>)` is \
11658 only valid for `sse`; got \
11659 `{value}` in axonendpoint '{}'.",
11660 node.name
11661 ),
11662 line: tok.line,
11663 column: tok.column,
11664 ..Default::default()
11665 });
11666 }
11667 self.advance(); let dialect_tok = self.consume_any_ident_or_kw()?;
11669 let dialect = dialect_tok.value.clone();
11670 if !AXONENDPOINT_TRANSPORT_DIALECTS
11671 .iter()
11672 .any(|&d| d == dialect)
11673 {
11674 let hint = crate::smart_suggest::suggest_for(
11675 &dialect,
11676 AXONENDPOINT_TRANSPORT_DIALECTS,
11677 );
11678 let base = format!(
11679 "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11680 node.name
11681 );
11682 let message = if hint.is_empty() {
11683 format!(
11684 "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11685 )
11686 } else {
11687 format!(
11688 "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11689 )
11690 };
11691 return Err(ParseError {
11692 message,
11693 line: dialect_tok.line,
11694 column: dialect_tok.column,
11695 ..Default::default()
11696 });
11697 }
11698 let rparen_tok = self.current().clone();
11700 if !self.check(TokenType::RParen) {
11701 return Err(ParseError {
11702 message: format!(
11703 "Expected `)` after dialect name \
11704 in axonendpoint '{}' \
11705 (transport: sse(<dialect>) grammar).",
11706 node.name
11707 ),
11708 line: rparen_tok.line,
11709 column: rparen_tok.column,
11710 ..Default::default()
11711 });
11712 }
11713 self.advance(); node.transport_dialect = dialect;
11715 }
11716 }
11717 "keepalive" => {
11718 let value_tok = self.current().clone();
11722 self.advance();
11723 let value = &value_tok.value;
11724 if !axonendpoint_is_valid_keepalive(value) {
11725 let hint = crate::smart_suggest::suggest_for(
11726 value,
11727 AXONENDPOINT_KEEPALIVE_VALUES,
11728 );
11729 let base = format!(
11730 "Invalid keepalive '{}' in axonendpoint '{}'.",
11731 value, node.name
11732 );
11733 let message = if hint.is_empty() {
11734 format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11735 } else {
11736 format!(
11737 "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11738 )
11739 };
11740 return Err(ParseError {
11741 message,
11742 line: value_tok.line,
11743 column: value_tok.column,
11744 ..Default::default()
11745 });
11746 }
11747 node.keepalive = value.clone();
11748 }
11749 "backend" => {
11750 let value_tok = self.consume_any_ident_or_kw()?;
11758 let value = &value_tok.value;
11759 if !axonendpoint_is_valid_backend(value) {
11760 let hint = crate::smart_suggest::suggest_for(
11761 value,
11762 AXONENDPOINT_BACKEND_VALUES,
11763 );
11764 let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11765 let base = format!(
11766 "Invalid backend '{}' in axonendpoint '{}'.",
11767 value, node.name
11768 );
11769 let message = if hint.is_empty() {
11770 format!("{base} expected {expected}, found {value}")
11771 } else {
11772 format!(
11773 "{base} {hint} (expected {expected}, found {value})"
11774 )
11775 };
11776 return Err(ParseError {
11777 message,
11778 line: value_tok.line,
11779 column: value_tok.column,
11780 ..Default::default()
11781 });
11782 }
11783 node.backend = value.clone();
11784 }
11785 _ => self.skip_value(),
11786 }
11787 } else if self.check(TokenType::LBrace) {
11788 self.skip_braced_block()?;
11789 }
11790 }
11791 self.consume(TokenType::RBrace)?;
11792 Ok(node)
11793 }
11794
11795 fn parse_optional_int(&mut self) -> Option<i64> {
11798 let tok = self.current().clone();
11799 match tok.ttype {
11800 TokenType::Integer => {
11801 self.advance();
11802 tok.value.parse::<i64>().ok()
11803 }
11804 _ => {
11805 self.advance();
11806 None
11807 }
11808 }
11809 }
11810
11811 fn parse_optional_float(&mut self) -> Option<f64> {
11812 let tok = self.current().clone();
11813 match tok.ttype {
11814 TokenType::Float | TokenType::Integer => {
11815 self.advance();
11816 tok.value.parse::<f64>().ok()
11817 }
11818 _ => {
11819 self.advance();
11820 None
11821 }
11822 }
11823 }
11824
11825 fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11828 let tok = self.consume(TokenType::Lambda)?;
11829 let name = self.consume(TokenType::Identifier)?;
11830 self.consume(TokenType::LBrace)?;
11831
11832 let mut node = LambdaDataDefinition {
11833 name: name.value.clone(),
11834 ontology: String::new(),
11835 certainty: 1.0,
11836 temporal_frame_start: String::new(),
11837 temporal_frame_end: String::new(),
11838 provenance: String::new(),
11839 derivation: String::new(),
11840 loc: Loc {
11841 line: tok.line,
11842 column: tok.column,
11843 },
11844 leading_trivia: Vec::new(),
11845 trailing_trivia: Vec::new(),
11846 };
11847
11848 while !self.check(TokenType::RBrace) {
11849 let field = self.current().clone();
11850 match field.ttype {
11851 TokenType::Ontology => {
11852 self.advance();
11853 self.consume(TokenType::Colon)?;
11854 node.ontology = self.consume(TokenType::StringLit)?.value.clone();
11855 }
11856 TokenType::Certainty => {
11857 self.advance();
11858 self.consume(TokenType::Colon)?;
11859 let val = self.current().clone();
11860 match val.ttype {
11861 TokenType::Float => {
11862 self.advance();
11863 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11864 }
11865 TokenType::Integer => {
11866 self.advance();
11867 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
11868 }
11869 _ => {
11870 return Err(ParseError {
11871 message: format!(
11872 "Expected number for certainty, got '{}'",
11873 val.value
11874 ),
11875 line: val.line,
11876 column: val.column,
11877 ..Default::default()
11878 });
11879 }
11880 }
11881 }
11882 TokenType::TemporalFrame => {
11883 self.advance();
11884 self.consume(TokenType::Colon)?;
11885 node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
11886 if self.check(TokenType::StringLit) {
11888 node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
11889 }
11890 }
11891 TokenType::Provenance => {
11892 self.advance();
11893 self.consume(TokenType::Colon)?;
11894 node.provenance = self.consume(TokenType::StringLit)?.value.clone();
11895 }
11896 TokenType::Derivation => {
11897 self.advance();
11898 self.consume(TokenType::Colon)?;
11899 let d = self.current().clone();
11900 self.advance();
11901 node.derivation = d.value.clone();
11902 }
11903 _ => {
11904 self.advance();
11906 if self.check(TokenType::Colon) {
11907 self.advance();
11908 self.skip_value();
11909 }
11910 }
11911 }
11912 }
11913
11914 self.consume(TokenType::RBrace)?;
11915 Ok(node)
11916 }
11917
11918 fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
11919 let tok = self.consume(TokenType::Lambda)?;
11920 let lambda_name = self.consume(TokenType::Identifier)?;
11921
11922 let on_tok = self.current().clone();
11924 self.advance();
11925 if on_tok.value != "on" {
11926 return Err(ParseError {
11927 message: format!(
11928 "Expected 'on' after lambda data name in flow step, got '{}'",
11929 on_tok.value
11930 ),
11931 line: on_tok.line,
11932 column: on_tok.column,
11933 ..Default::default()
11934 });
11935 }
11936
11937 let target = self.current().clone();
11938 self.advance();
11939
11940 let mut output_type = String::new();
11941 if self.check(TokenType::Arrow) {
11942 self.advance();
11943 output_type = self.consume(TokenType::Identifier)?.value.clone();
11944 }
11945
11946 Ok(LambdaDataApplyNode {
11947 lambda_data_name: lambda_name.value.clone(),
11948 target: target.value.clone(),
11949 output_type,
11950 loc: Loc {
11951 line: tok.line,
11952 column: tok.column,
11953 },
11954 })
11955 }
11956
11957 fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
11960 let kw_tok = self.current().clone();
11961 self.advance(); let name = if self.current().ttype == TokenType::Identifier {
11965 let n = self.current().value.clone();
11966 self.advance();
11967 n
11968 } else if !self.check(TokenType::LBrace)
11969 && !self.check(TokenType::LParen)
11970 && !self.check(TokenType::Eof)
11971 && self
11972 .current()
11973 .value
11974 .chars()
11975 .all(|c| c.is_alphanumeric() || c == '_')
11976 {
11977 let n = self.current().value.clone();
11978 self.advance();
11979 n
11980 } else {
11981 String::new()
11982 };
11983
11984 if self.check(TokenType::LParen) {
11986 self.advance();
11987 let mut depth = 1u32;
11988 while depth > 0 && !self.check(TokenType::Eof) {
11989 if self.check(TokenType::LParen) {
11990 depth += 1;
11991 } else if self.check(TokenType::RParen) {
11992 depth -= 1;
11993 }
11994 self.advance();
11995 }
11996 }
11997
11998 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12000 if self.check(TokenType::Eof) {
12001 break;
12002 }
12003 self.advance();
12004 }
12005
12006 if self.check(TokenType::LBrace) {
12008 self.skip_braced_block()?;
12009 }
12010
12011 Ok(Declaration::Generic(GenericDeclaration {
12012 keyword: kw_tok.value,
12013 name,
12014 loc: Loc {
12015 line: kw_tok.line,
12016 column: kw_tok.column,
12017 },
12018 leading_trivia: Vec::new(),
12019 trailing_trivia: Vec::new(),
12020 }))
12021 }
12022
12023 fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12031 let tok = self.consume(TokenType::Channel)?;
12032 let name = self.consume(TokenType::Identifier)?.value;
12033 let mut node = ChannelDefinition {
12034 name: name.clone(),
12035 message: String::new(),
12036 qos: "at_least_once".to_string(),
12037 lifetime: "affine".to_string(),
12038 persistence: "ephemeral".to_string(),
12039 shield_ref: String::new(),
12040 loc: Loc {
12041 line: tok.line,
12042 column: tok.column,
12043 },
12044 leading_trivia: Vec::new(),
12045 trailing_trivia: Vec::new(),
12046 };
12047 self.consume(TokenType::LBrace)?;
12048 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12049 let field_tok = self.current().clone();
12050 let field_name = field_tok.value.clone();
12051 self.advance();
12052 if !self.check(TokenType::Colon) {
12053 if self.check(TokenType::LBrace) {
12054 self.skip_braced_block()?;
12055 }
12056 continue;
12057 }
12058 self.advance();
12059 match field_name.as_str() {
12060 "message" => node.message = self.parse_channel_message_type()?,
12061 "qos" => {
12062 let q_tok = self.consume_any_ident_or_kw()?;
12063 if !matches!(
12064 q_tok.value.as_str(),
12065 "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12066 ) {
12067 return Err(ParseError {
12068 message: format!(
12069 "Invalid qos '{}' in channel '{}' — \
12070 expected at_most_once | at_least_once | \
12071 exactly_once | broadcast | queue",
12072 q_tok.value, name
12073 ),
12074 line: q_tok.line,
12075 column: q_tok.column,
12076 ..Default::default()
12077 });
12078 }
12079 node.qos = q_tok.value;
12080 }
12081 "lifetime" => {
12082 let lt_tok = self.consume_any_ident_or_kw()?;
12083 if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12084 return Err(ParseError {
12085 message: format!(
12086 "Invalid lifetime '{}' in channel '{}' — \
12087 expected linear | affine | persistent",
12088 lt_tok.value, name
12089 ),
12090 line: lt_tok.line,
12091 column: lt_tok.column,
12092 ..Default::default()
12093 });
12094 }
12095 node.lifetime = lt_tok.value;
12096 }
12097 "persistence" => {
12098 let p_tok = self.consume_any_ident_or_kw()?;
12099 if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12100 return Err(ParseError {
12101 message: format!(
12102 "Invalid persistence '{}' in channel '{}' — \
12103 expected ephemeral | persistent_axonstore",
12104 p_tok.value, name
12105 ),
12106 line: p_tok.line,
12107 column: p_tok.column,
12108 ..Default::default()
12109 });
12110 }
12111 node.persistence = p_tok.value;
12112 }
12113 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12114 _ => self.skip_value(),
12115 }
12116 }
12117 self.consume(TokenType::RBrace)?;
12118 Ok(node)
12119 }
12120
12121 fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12124 let head = self.consume(TokenType::Identifier)?;
12125 let mut spelling = head.value;
12126 if self.check(TokenType::Lt) {
12127 self.advance();
12128 let inner = self.parse_channel_message_type()?;
12129 self.consume(TokenType::Gt)?;
12130 spelling = format!("{}<{}>", spelling, inner);
12131 }
12132 Ok(spelling)
12133 }
12134
12135 fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12141 let tok = self.consume(TokenType::Emit)?;
12142 let channel = self.consume(TokenType::Identifier)?.value;
12143 self.consume(TokenType::LParen)?;
12144 let value = self.parse_emit_value_ref()?;
12145 self.consume(TokenType::RParen)?;
12146 Ok(FlowStep::Emit(EmitStatement {
12147 channel_ref: channel,
12148 value_ref: value,
12149 loc: Loc {
12150 line: tok.line,
12151 column: tok.column,
12152 },
12153 }))
12154 }
12155
12156 fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12162 let tok = self.consume(TokenType::Mint)?;
12163 let credential_ref = self.consume(TokenType::Identifier)?.value;
12164 self.consume(TokenType::As)?;
12165 let binding = self.consume(TokenType::Identifier)?.value;
12166 Ok(FlowStep::Mint(MintStep {
12167 credential_ref,
12168 binding,
12169 loc: Loc {
12170 line: tok.line,
12171 column: tok.column,
12172 },
12173 }))
12174 }
12175
12176 fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12191 let tok = self.consume(TokenType::Rotate)?;
12192 let store_ref = self.consume(TokenType::Identifier)?.value;
12193 let mut where_expr = String::new();
12194 if self.check(TokenType::Where) {
12195 self.advance();
12196 where_expr = self.consume(TokenType::StringLit)?.value.clone();
12197 }
12198 let with_tok = self.current().clone();
12199 if with_tok.value != "with" {
12200 return Err(ParseError {
12201 message: format!(
12202 "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12203 A rotation names the tool that performs the renewal exchange: \
12204 `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12205 if where_expr.is_empty() { "" } else { " where …" },
12206 with_tok.value
12207 ),
12208 line: with_tok.line,
12209 column: with_tok.column,
12210 ..Default::default()
12211 });
12212 }
12213 self.advance();
12214 let tool_ref = self.consume(TokenType::Identifier)?.value;
12215 self.consume(TokenType::As)?;
12216 let binding = self.consume(TokenType::Identifier)?.value;
12217 Ok(FlowStep::Rotate(RotateStep {
12218 store_ref,
12219 where_expr,
12220 tool_ref,
12221 binding,
12222 loc: Loc {
12223 line: tok.line,
12224 column: tok.column,
12225 },
12226 }))
12227 }
12228
12229 fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12245 let head = self.consume(TokenType::Identifier)?.value;
12246 let mut parts = vec![head];
12247 while self.check(TokenType::Dot) {
12248 self.advance(); let next_tok = self.current().clone();
12250 let valid = !next_tok.value.is_empty()
12251 && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12252 || next_tok.value.starts_with('_');
12253 if !valid {
12254 return Err(ParseError {
12255 message: format!(
12256 "Expected identifier or keyword after '.' in dotted \
12257 access, found {:?}",
12258 next_tok.value
12259 ),
12260 line: next_tok.line,
12261 column: next_tok.column,
12262 ..Default::default()
12263 });
12264 }
12265 self.advance();
12266 parts.push(next_tok.value);
12267 }
12268 Ok(parts.join("."))
12269 }
12270
12271 fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12273 let tok = self.consume(TokenType::Publish)?;
12274 let channel = self.consume(TokenType::Identifier)?.value;
12275 self.consume(TokenType::Within)?;
12276 let shield = self.consume(TokenType::Identifier)?.value;
12277 Ok(FlowStep::Publish(PublishStatement {
12278 channel_ref: channel,
12279 shield_ref: shield,
12280 loc: Loc {
12281 line: tok.line,
12282 column: tok.column,
12283 },
12284 }))
12285 }
12286
12287 fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12289 let tok = self.consume(TokenType::Discover)?;
12290 let cap = self.consume(TokenType::Identifier)?.value;
12291 self.consume(TokenType::As)?;
12292 let alias = self.consume(TokenType::Identifier)?.value;
12293 Ok(FlowStep::Discover(DiscoverStatement {
12294 capability_ref: cap,
12295 alias,
12296 loc: Loc {
12297 line: tok.line,
12298 column: tok.column,
12299 },
12300 }))
12301 }
12302}
12303
12304#[cfg(test)]
12307mod parser_tests {
12308 use super::*;
12309 use crate::lexer::Lexer;
12310
12311 fn parse(src: &str) -> Result<Program, ParseError> {
12312 let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12313 Parser::new(tokens).parse()
12314 }
12315
12316 #[test]
12317 fn channel_full_parses() {
12318 let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12319 let prog = parse(src).expect("parse");
12320 match &prog.declarations[0] {
12321 Declaration::Channel(c) => {
12322 assert_eq!(c.name, "C");
12323 assert_eq!(c.message, "Order");
12324 assert_eq!(c.qos, "at_least_once");
12325 assert_eq!(c.lifetime, "affine");
12326 assert_eq!(c.persistence, "ephemeral");
12327 assert_eq!(c.shield_ref, "Gate");
12328 }
12329 _ => panic!("expected ChannelDefinition"),
12330 }
12331 }
12332
12333 #[test]
12334 fn channel_defaults_match_paper_d1() {
12335 let prog = parse("channel C { message: Order }").expect("parse");
12336 if let Declaration::Channel(c) = &prog.declarations[0] {
12337 assert_eq!(c.qos, "at_least_once"); assert_eq!(c.lifetime, "affine"); assert_eq!(c.persistence, "ephemeral");
12340 assert_eq!(c.shield_ref, "");
12341 } else {
12342 panic!("expected ChannelDefinition");
12343 }
12344 }
12345
12346 #[test]
12347 fn channel_second_order_message_type_parses() {
12348 let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12349 if let Declaration::Channel(c) = &prog.declarations[0] {
12350 assert_eq!(c.message, "Channel<Order>");
12351 } else {
12352 panic!("expected ChannelDefinition");
12353 }
12354 }
12355
12356 #[test]
12357 fn channel_nested_channel_message_type_parses() {
12358 let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12359 if let Declaration::Channel(c) = &prog.declarations[0] {
12360 assert_eq!(c.message, "Channel<Channel<Order>>");
12361 } else {
12362 panic!("expected ChannelDefinition");
12363 }
12364 }
12365
12366 #[test]
12367 fn channel_invalid_qos_rejected() {
12368 let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12369 assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12370 }
12371
12372 #[test]
12373 fn channel_invalid_lifetime_rejected() {
12374 let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12375 assert!(
12376 err.message.contains("Invalid lifetime"),
12377 "got {}",
12378 err.message
12379 );
12380 }
12381
12382 #[test]
12383 fn channel_invalid_persistence_rejected() {
12384 let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12385 assert!(
12386 err.message.contains("Invalid persistence"),
12387 "got {}",
12388 err.message
12389 );
12390 }
12391
12392 #[test]
12393 fn emit_value_parses() {
12394 let src = "flow f() -> Out { emit C(payload) }";
12395 let prog = parse(src).expect("parse");
12396 if let Declaration::Flow(f) = &prog.declarations[0] {
12397 match &f.body[0] {
12398 FlowStep::Emit(e) => {
12399 assert_eq!(e.channel_ref, "C");
12400 assert_eq!(e.value_ref, "payload");
12401 }
12402 other => panic!("expected Emit, got {:?}", other),
12403 }
12404 } else {
12405 panic!("expected Flow");
12406 }
12407 }
12408
12409 #[test]
12410 fn publish_within_shield_parses() {
12411 let src = "flow f() -> Cap { publish C within Gate }";
12412 let prog = parse(src).expect("parse");
12413 if let Declaration::Flow(f) = &prog.declarations[0] {
12414 match &f.body[0] {
12415 FlowStep::Publish(p) => {
12416 assert_eq!(p.channel_ref, "C");
12417 assert_eq!(p.shield_ref, "Gate");
12418 }
12419 other => panic!("expected Publish, got {:?}", other),
12420 }
12421 } else {
12422 panic!("expected Flow");
12423 }
12424 }
12425
12426 #[test]
12427 fn discover_with_alias_parses() {
12428 let src = "flow f() -> Out { discover C as ch }";
12429 let prog = parse(src).expect("parse");
12430 if let Declaration::Flow(f) = &prog.declarations[0] {
12431 match &f.body[0] {
12432 FlowStep::Discover(d) => {
12433 assert_eq!(d.capability_ref, "C");
12434 assert_eq!(d.alias, "ch");
12435 }
12436 other => panic!("expected Discover, got {:?}", other),
12437 }
12438 } else {
12439 panic!("expected Flow");
12440 }
12441 }
12442
12443 #[test]
12444 fn listen_typed_ref_sets_flag_true() {
12445 let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12446 let prog = parse(src).expect("parse");
12447 if let Declaration::Daemon(d) = &prog.declarations[0] {
12448 assert_eq!(d.listeners.len(), 1);
12449 assert_eq!(d.listeners[0].channel, "C");
12450 assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12451 } else {
12452 panic!("expected Daemon");
12453 }
12454 }
12455
12456 #[test]
12457 fn listen_string_topic_legacy_flag_false() {
12458 let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12459 let prog = parse(src).expect("parse");
12460 if let Declaration::Daemon(d) = &prog.declarations[0] {
12461 assert_eq!(d.listeners.len(), 1);
12462 assert_eq!(d.listeners[0].channel, "orders");
12463 assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12464 } else {
12465 panic!("expected Daemon");
12466 }
12467 }
12468
12469 fn extract_first_emit(prog: &Program) -> &EmitStatement {
12472 if let Declaration::Flow(f) = &prog.declarations[0] {
12473 if let FlowStep::Emit(e) = &f.body[0] {
12474 return e;
12475 }
12476 }
12477 panic!("expected emit statement at flow body[0]");
12478 }
12479
12480 #[test]
12481 fn emit_accepts_bare_identifier_value_ref() {
12482 let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12484 let emit = extract_first_emit(&prog);
12485 assert_eq!(emit.channel_ref, "Hello");
12486 assert_eq!(emit.value_ref, "payload");
12487 }
12488
12489 #[test]
12490 fn emit_accepts_two_segment_dotted_value_ref() {
12491 let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12493 let emit = extract_first_emit(&prog);
12494 assert_eq!(emit.value_ref, "Build.output");
12495 }
12496
12497 #[test]
12498 fn emit_accepts_three_segment_nested_dotted_value_ref() {
12499 let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12500 let emit = extract_first_emit(&prog);
12501 assert_eq!(emit.value_ref, "Analyze.result.score");
12502 }
12503
12504 #[test]
12505 fn emit_dotted_with_trailing_dot_fails() {
12506 let result = parse("flow f() -> Out { emit Hello(Build.) }");
12508 assert!(result.is_err(), "expected parse error for trailing dot");
12509 }
12510}
12511
12512#[cfg(test)]
12515mod declaration_trivia_tests {
12516 use super::*;
12517 use crate::lexer::Lexer;
12518 use crate::tokens::TriviaKind;
12519
12520 fn parse(src: &str) -> Program {
12521 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12522 Parser::new(toks).parse().expect("parse")
12523 }
12524
12525 #[test]
12526 fn no_comments_means_empty_trivia_per_decl() {
12527 let prog = parse("flow F() -> Out { }");
12528 assert_eq!(prog.declarations.len(), 1);
12529 assert_eq!(prog.declaration_trivia.len(), 1);
12530 assert!(prog.declaration_trivia[0].leading.is_empty());
12531 assert!(prog.declaration_trivia[0].trailing.is_empty());
12532 }
12533
12534 #[test]
12535 fn doc_line_comment_attaches_as_leading() {
12536 let prog = parse("/// Documents F\nflow F() -> Out { }");
12537 let triv = &prog.declaration_trivia[0];
12538 assert_eq!(triv.leading.len(), 1);
12539 assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12540 assert!(triv.leading[0].is_doc());
12541 assert_eq!(triv.leading[0].text, "/// Documents F");
12542 }
12543
12544 #[test]
12545 fn regular_line_comment_attaches_as_leading() {
12546 let prog = parse("// header\nflow F() -> Out { }");
12547 let triv = &prog.declaration_trivia[0];
12548 assert_eq!(triv.leading.len(), 1);
12549 assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12550 assert!(!triv.leading[0].is_doc());
12551 }
12552
12553 #[test]
12554 fn block_doc_comment_attaches_as_leading() {
12555 let prog = parse("/** Doc block */\nflow F() -> Out { }");
12556 let triv = &prog.declaration_trivia[0];
12557 assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12558 assert!(triv.leading[0].is_doc());
12559 }
12560
12561 #[test]
12562 fn multiple_comments_collected_in_source_order() {
12563 let src = "/// First\n/// Second\nflow F() -> Out { }";
12564 let prog = parse(src);
12565 let triv = &prog.declaration_trivia[0];
12566 assert_eq!(triv.leading.len(), 2);
12567 assert_eq!(triv.leading[0].text, "/// First");
12568 assert_eq!(triv.leading[1].text, "/// Second");
12569 }
12570
12571 #[test]
12572 fn three_decls_each_get_own_leading() {
12573 let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12574 let prog = parse(src);
12575 assert_eq!(prog.declarations.len(), 3);
12576 assert_eq!(prog.declaration_trivia.len(), 3);
12577 for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12578 let triv = &prog.declaration_trivia[idx];
12579 assert_eq!(triv.leading.len(), 1);
12580 assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12581 }
12582 }
12583
12584 #[test]
12585 fn trailing_comment_attaches_to_last_token_of_decl() {
12586 let prog = parse("flow F() -> Out { } // tail");
12588 let triv = &prog.declaration_trivia[0];
12589 assert_eq!(triv.trailing.len(), 1);
12590 assert_eq!(triv.trailing[0].text, "// tail");
12591 }
12592
12593 #[test]
12594 fn mixed_doc_and_regular_preserve_order_between_decls() {
12595 let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12596 let prog = parse(src);
12597 assert_eq!(prog.declarations.len(), 2);
12598 assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12600 assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12602 assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12603 assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12604 }
12605
12606 #[test]
12607 fn parser_unaffected_by_comments_in_grammar_path() {
12608 let src =
12613 "// before flow\nflow /* between flow and name */ F() -> Out {\n // body comment\n}";
12614 let prog = parse(src);
12615 assert_eq!(prog.declarations.len(), 1);
12616 if let Declaration::Flow(f) = &prog.declarations[0] {
12617 assert_eq!(f.name, "F");
12618 } else {
12619 panic!("expected Flow declaration");
12620 }
12621 }
12622}
12623
12624#[cfg(test)]
12633mod per_struct_trivia_tests {
12634 use super::*;
12635 use crate::lexer::Lexer;
12636 use crate::tokens::TriviaKind;
12637
12638 fn parse(src: &str) -> Program {
12639 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12640 Parser::new(toks).parse().expect("parse")
12641 }
12642
12643 #[test]
12644 fn flow_definition_carries_leading_trivia_directly() {
12645 let prog = parse("/// documents F\nflow F() -> Out { }");
12646 if let Declaration::Flow(f) = &prog.declarations[0] {
12647 assert_eq!(f.leading_trivia.len(), 1);
12648 assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12649 assert_eq!(f.leading_trivia[0].text, "/// documents F");
12650 assert!(f.trailing_trivia.is_empty());
12651 } else {
12652 panic!("expected Flow declaration");
12653 }
12654 }
12655
12656 #[test]
12657 fn flow_definition_carries_trailing_trivia_directly() {
12658 let prog = parse("flow F() -> Out { } // tail comment");
12659 if let Declaration::Flow(f) = &prog.declarations[0] {
12660 assert_eq!(f.trailing_trivia.len(), 1);
12661 assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12662 } else {
12663 panic!("expected Flow declaration");
12664 }
12665 }
12666
12667 #[test]
12668 fn channel_definition_carries_trivia_directly() {
12669 let src = concat!(
12672 "/// inbound order events\n",
12673 "channel Orders {\n",
12674 " message: Order\n",
12675 " qos: at_least_once\n",
12676 " lifetime: affine\n",
12677 " persistence: ephemeral\n",
12678 " shield: Broker\n",
12679 "}",
12680 );
12681 let prog = parse(src);
12682 if let Declaration::Channel(ch) = &prog.declarations[0] {
12683 assert_eq!(ch.leading_trivia.len(), 1);
12684 assert!(ch.leading_trivia[0].is_doc());
12685 assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12686 } else {
12687 panic!("expected Channel declaration");
12688 }
12689 }
12690
12691 #[test]
12692 fn per_struct_fields_match_side_channel() {
12693 let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12696 let prog = parse(src);
12697 for (idx, decl) in prog.declarations.iter().enumerate() {
12698 let side = &prog.declaration_trivia[idx];
12699 let (per_lead, per_trail) = match decl {
12700 Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12701 _ => panic!("unexpected variant"),
12702 };
12703 assert_eq!(per_lead.len(), side.leading.len());
12704 assert_eq!(per_trail.len(), side.trailing.len());
12705 for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12706 assert_eq!(a.text, b.text);
12707 assert_eq!(a.kind, b.kind);
12708 }
12709 }
12710 }
12711
12712 #[test]
12713 fn comment_free_program_yields_empty_per_struct_fields() {
12714 let prog = parse("flow F() -> Out { }");
12715 if let Declaration::Flow(f) = &prog.declarations[0] {
12716 assert!(f.leading_trivia.is_empty());
12717 assert!(f.trailing_trivia.is_empty());
12718 } else {
12719 panic!("expected Flow declaration");
12720 }
12721 }
12722}
12723
12724#[cfg(test)]
12733mod inner_doc_tests {
12734 use super::*;
12735 use crate::lexer::Lexer;
12736 use crate::tokens::TriviaKind;
12737
12738 fn parse(src: &str) -> Program {
12739 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12740 Parser::new(toks).parse().expect("parse")
12741 }
12742
12743 #[test]
12744 fn inner_doc_line_reaches_leading_trivia() {
12745 let src = "//! file-level docs\nflow F() -> Out { }";
12746 let prog = parse(src);
12747 let triv = &prog.declaration_trivia[0];
12748 assert_eq!(triv.leading.len(), 1);
12749 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12750 assert!(triv.leading[0].is_doc());
12751 assert!(triv.leading[0].is_inner_doc());
12752 assert_eq!(triv.leading[0].text, "//! file-level docs");
12753 assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12754 }
12755
12756 #[test]
12757 fn inner_doc_block_reaches_leading_trivia() {
12758 let src = "/*! module-level docs */\nflow F() -> Out { }";
12759 let prog = parse(src);
12760 let triv = &prog.declaration_trivia[0];
12761 assert_eq!(triv.leading.len(), 1);
12762 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12763 assert!(triv.leading[0].is_inner_doc());
12764 assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12765 }
12766
12767 #[test]
12768 fn outer_and_inner_doc_can_coexist() {
12769 let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12773 let prog = parse(src);
12774 let triv = &prog.declaration_trivia[0];
12775 assert_eq!(triv.leading.len(), 2);
12776 assert!(triv.leading[0].is_inner_doc());
12777 assert!(triv.leading[1].is_doc());
12778 assert!(!triv.leading[1].is_inner_doc());
12779 }
12780
12781 #[test]
12782 fn inner_doc_reaches_per_struct_fields() {
12783 let src = "//! intro\nflow F() -> Out { }";
12785 let prog = parse(src);
12786 if let Declaration::Flow(f) = &prog.declarations[0] {
12787 assert_eq!(f.leading_trivia.len(), 1);
12788 assert!(f.leading_trivia[0].is_inner_doc());
12789 } else {
12790 panic!("expected Flow declaration");
12791 }
12792 }
12793}
12794
12795#[cfg(test)]
12813mod recovery_tests {
12814 use super::*;
12815 use crate::lexer::Lexer;
12816
12817 fn lex(src: &str) -> Vec<Token> {
12820 Lexer::new(src, "<test>").tokenize().expect("lex")
12821 }
12822
12823 fn recover(src: &str) -> ParseResult {
12826 Parser::new(lex(src)).parse_with_recovery()
12827 }
12828
12829 fn strict(src: &str) -> Result<Program, ParseError> {
12831 Parser::new(lex(src)).parse()
12832 }
12833
12834 #[test]
12837 fn strict_parse_unchanged_for_clean_source() {
12838 let src = "intent I {}";
12841 let prog = strict(src).expect("clean parse");
12842 assert_eq!(prog.declarations.len(), 1);
12843 }
12844
12845 #[test]
12846 fn strict_parse_still_raises_on_first_error() {
12847 let src = "flow F() { } not_a_keyword flow G() { }";
12852 let _ = strict(src).expect_err("must error fast in strict mode");
12853 }
12854
12855 #[test]
12856 fn recovery_clean_source_yields_no_errors() {
12857 let src = "flow F() { } flow G() { }";
12858 let pr = recover(src);
12859 assert!(pr.is_clean(), "errors: {:?}", pr.errors);
12860 assert_eq!(pr.program.declarations.len(), 2);
12861 }
12862
12863 #[test]
12866 fn single_unknown_top_level_token_recovers() {
12867 let src = "garbage_token flow F() { } flow G() { }";
12869 let pr = recover(src);
12870 assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
12871 assert_eq!(pr.program.declarations.len(), 2);
12872 }
12873
12874 #[test]
12875 fn error_in_first_decl_does_not_block_second() {
12876 let src = "flow F() { not_a_step nope } flow G() { }";
12879 let pr = recover(src);
12880 assert!(pr.has_errors(), "expected at least one error");
12881 let names: Vec<&str> = pr
12883 .program
12884 .declarations
12885 .iter()
12886 .filter_map(|d| match d {
12887 Declaration::Flow(f) => Some(f.name.as_str()),
12888 _ => None,
12889 })
12890 .collect();
12891 assert!(names.contains(&"G"), "G not found among {names:?}");
12892 }
12893
12894 #[test]
12895 fn malformed_declaration_then_clean_intent_recovers() {
12896 let src = "flow @ () { } intent I {}";
12897 let pr = recover(src);
12898 assert!(pr.has_errors());
12899 let kinds: Vec<&str> = pr
12900 .program
12901 .declarations
12902 .iter()
12903 .map(|d| match d {
12904 Declaration::Intent(_) => "intent",
12905 Declaration::Flow(_) => "flow",
12906 _ => "other",
12907 })
12908 .collect();
12909 assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
12910 }
12911
12912 #[test]
12913 fn recovery_does_not_double_count_a_single_error() {
12914 let src = "flow F() { not_a_step }";
12922 let pr = recover(src);
12923 let outer_ghosts = pr
12924 .errors
12925 .iter()
12926 .filter(|e| e.message.contains("at top level"))
12927 .count();
12928 assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
12929 }
12930
12931 #[test]
12934 fn three_independent_errors_yield_three_entries() {
12935 let src =
12936 "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
12937 let pr = recover(src);
12938 assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
12939 assert_eq!(pr.program.declarations.len(), 3);
12940 }
12941
12942 #[test]
12943 fn all_errors_no_valid_declarations() {
12944 let src = "foo bar baz qux";
12945 let pr = recover(src);
12946 assert!(pr.has_errors());
12947 assert!(pr.program.declarations.is_empty());
12948 }
12949
12950 #[test]
12951 fn errors_recorded_in_source_order() {
12952 let src = "x flow A() { } y flow B() { } z flow C() { }";
12953 let pr = recover(src);
12954 assert_eq!(pr.errors.len(), 3);
12955 let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
12956 assert!(
12959 lines.windows(2).all(|w| w[0] <= w[1]),
12960 "errors out of order: {lines:?}"
12961 );
12962 }
12963
12964 #[test]
12967 fn sync_to_flow_keyword() {
12968 let src = "garbage flow F() { }";
12969 let pr = recover(src);
12970 assert_eq!(pr.program.declarations.len(), 1);
12971 }
12972
12973 #[test]
12974 fn sync_to_intent_keyword() {
12975 let src = "garbage intent I {}";
12976 let pr = recover(src);
12977 assert_eq!(pr.program.declarations.len(), 1);
12978 }
12979
12980 #[test]
12981 fn sync_to_persona_keyword() {
12982 let src = "garbage persona P { name: \"P\" role: \"R\" }";
12983 let pr = recover(src);
12984 assert!(
12985 pr.program
12986 .declarations
12987 .iter()
12988 .any(|d| matches!(d, Declaration::Persona(_))),
12989 "persona not recovered: decls = {:?}",
12990 pr.program.declarations.len()
12991 );
12992 }
12993
12994 #[test]
12995 fn sync_to_run_keyword() {
12996 let src = "garbage run R { input: { user_message: \"hi\" } }";
12997 let pr = recover(src);
12998 assert!(pr.has_errors());
13000 }
13001
13002 #[test]
13005 fn parse_result_has_errors_and_is_clean_invert() {
13006 let pr_clean = recover("flow F() { }");
13007 assert!(pr_clean.is_clean());
13008 assert!(!pr_clean.has_errors());
13009
13010 let pr_err = recover("garbage");
13011 assert!(!pr_err.is_clean());
13012 assert!(pr_err.has_errors());
13013 }
13014
13015 #[test]
13016 fn parse_result_program_field_holds_partial_program() {
13017 let pr = recover("garbage flow F() { }");
13018 assert!(!pr.program.declarations.is_empty());
13019 }
13020
13021 #[test]
13022 fn parse_result_errors_carry_line_and_column() {
13023 let pr = recover("garbage");
13024 assert!(!pr.errors.is_empty());
13025 let e = &pr.errors[0];
13026 assert!(e.line >= 1);
13027 let _ = e.column;
13030 assert!(!e.message.is_empty());
13031 }
13032
13033 #[test]
13034 fn parse_result_debug_renders() {
13035 let pr = recover("flow F() { }");
13036 let s = format!("{pr:?}");
13037 assert!(s.contains("ParseResult"));
13038 }
13039
13040 #[test]
13043 fn empty_source_is_clean() {
13044 let pr = recover("");
13045 assert!(pr.is_clean());
13046 assert!(pr.program.declarations.is_empty());
13047 }
13048
13049 #[test]
13050 fn whitespace_only_source_is_clean() {
13051 let pr = recover(" \n\n\t \n");
13052 assert!(pr.is_clean());
13053 assert!(pr.program.declarations.is_empty());
13054 }
13055
13056 #[test]
13057 fn only_garbage_does_not_crash() {
13058 let pr = recover("foo bar baz { qux quux } corge { grault }");
13060 assert!(pr.has_errors());
13061 }
13062
13063 #[test]
13064 fn unbalanced_close_brace_does_not_crash() {
13065 let pr = recover("} flow F() { }");
13066 let names: Vec<&str> = pr
13068 .program
13069 .declarations
13070 .iter()
13071 .filter_map(|d| match d {
13072 Declaration::Flow(f) => Some(f.name.as_str()),
13073 _ => None,
13074 })
13075 .collect();
13076 assert!(names.contains(&"F"), "F not recovered: {names:?}");
13077 }
13078
13079 #[test]
13080 fn error_at_eof_does_not_loop() {
13081 let pr = recover("flow F() { ");
13083 let _ = pr.errors.len();
13085 }
13086
13087 #[test]
13088 fn nested_braces_inside_error_still_balance() {
13089 let src = "flow F() { not_a_step { inner } } flow G() { }";
13092 let pr = recover(src);
13093 let names: Vec<&str> = pr
13094 .program
13095 .declarations
13096 .iter()
13097 .filter_map(|d| match d {
13098 Declaration::Flow(f) => Some(f.name.as_str()),
13099 _ => None,
13100 })
13101 .collect();
13102 assert!(names.contains(&"G"), "G not recovered: {names:?}");
13103 }
13104
13105 #[derive(Clone, Copy)]
13114 struct Xorshift(u64);
13115 impl Xorshift {
13116 fn next(&mut self) -> u64 {
13117 let mut x = self.0;
13118 x ^= x << 13;
13119 x ^= x >> 7;
13120 x ^= x << 17;
13121 self.0 = x;
13122 x
13123 }
13124 fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13125 slice[(self.next() as usize) % slice.len()]
13126 }
13127 }
13128
13129 fn mutate(src: &str, rng: &mut Xorshift) -> String {
13130 let mut bytes: Vec<u8> = src.bytes().collect();
13131 if bytes.is_empty() {
13132 return src.to_string();
13133 }
13134 let op = rng.next() % 4;
13135 let pos = (rng.next() as usize) % bytes.len();
13136 let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13140 match op {
13141 0 => {
13142 bytes.remove(pos);
13143 }
13144 1 => {
13145 let b = rng.pick(safe);
13146 bytes.insert(pos, b);
13147 }
13148 2 if pos + 1 < bytes.len() => {
13149 bytes.swap(pos, pos + 1);
13150 }
13151 _ => {
13152 let b = rng.pick(safe);
13153 bytes[pos] = b;
13154 }
13155 }
13156 bytes.retain(|b| b.is_ascii());
13159 String::from_utf8_lossy(&bytes).into_owned()
13160 }
13161
13162 #[test]
13163 fn fuzz_recovery_never_crashes() {
13164 let seed_bases = [
13165 "flow F() { }",
13166 "intent I { }",
13167 "persona P { name: \"P\" role: \"R\" }",
13168 "intent J { ask: \"a\" }",
13169 "type T = String",
13170 ];
13171 for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13173 let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13174 let mut current = (*base).to_string();
13175 for _ in 0..10 {
13176 current = mutate(¤t, &mut rng);
13177 let toks = match Lexer::new(¤t, "<fuzz>").tokenize() {
13180 Ok(t) => t,
13181 Err(_) => continue,
13182 };
13183 let _pr = Parser::new(toks).parse_with_recovery();
13185 }
13186 }
13187 }
13188
13189 #[test]
13192 fn missing_colon_hint_preserved_under_recovery() {
13193 let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13197 let pr = recover(src);
13198 if !pr.errors.is_empty() {
13202 let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13203 assert!(any_msg);
13204 }
13205 }
13206
13207 #[test]
13210 fn recovered_declarations_appear_in_source_order() {
13211 let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13212 let pr = recover(src);
13213 let names: Vec<&str> = pr
13214 .program
13215 .declarations
13216 .iter()
13217 .filter_map(|d| match d {
13218 Declaration::Flow(f) => Some(f.name.as_str()),
13219 _ => None,
13220 })
13221 .collect();
13222 assert_eq!(names, vec!["A", "B", "C"]);
13223 }
13224}
13225
13226#[cfg(test)]
13234mod source_context_tests {
13235 use super::*;
13236 use crate::lexer::Lexer;
13237
13238 fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13239 SourceSnippet::new(
13240 source.to_string(),
13241 line,
13242 column,
13243 filename.to_string(),
13244 )
13245 .render()
13246 }
13247
13248 #[test]
13251 fn rustc_style_block_for_middle_line() {
13252 let src = "line one\nline two\nline three\nline four\nline five";
13253 let out = snippet(src, 3, 6, "x.axon");
13254 assert!(out.contains("--> x.axon:3:6"));
13255 assert!(out.contains("1 | line one"));
13256 assert!(out.contains("2 | line two"));
13257 assert!(out.contains("3 | line three"));
13258 assert!(out.contains("4 | line four"));
13259 assert!(out.contains("5 | line five"));
13260 assert!(out.contains("\n | ^"), "out:\n{out}");
13262 }
13263
13264 #[test]
13265 fn caret_column_one_renders_correctly() {
13266 let out = snippet("abc\n", 1, 1, "<source>");
13267 assert!(out.contains("\n | ^"));
13268 }
13269
13270 #[test]
13271 fn first_line_clamps_context_before_to_zero() {
13272 let src = "first\nsecond\nthird\nfourth\nfifth";
13273 let out = snippet(src, 1, 1, "<source>");
13274 assert!(out.contains("1 | first"));
13275 assert!(out.contains("2 | second"));
13276 assert!(out.contains("3 | third"));
13277 assert!(!out.contains("4 | fourth"));
13278 }
13279
13280 #[test]
13281 fn last_line_clamps_context_after_to_eof() {
13282 let src = "first\nsecond\nthird\nfourth\nfifth";
13283 let out = snippet(src, 5, 2, "<source>");
13284 assert!(out.contains("5 | fifth"));
13285 assert!(out.contains("3 | third"));
13286 assert!(out.contains("4 | fourth"));
13287 assert!(!out.contains("2 | second"));
13288 }
13289
13290 #[test]
13291 fn gutter_width_grows_with_line_count() {
13292 let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13293 let out = snippet(&src, 12, 1, "<source>");
13294 assert!(out.contains("12 | line12"));
13295 assert!(out.contains("10 | line10"));
13296 }
13297
13298 #[test]
13301 fn empty_source_returns_empty() {
13302 assert_eq!(snippet("", 1, 1, "<source>"), "");
13303 }
13304
13305 #[test]
13306 fn zero_line_returns_empty() {
13307 assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13308 }
13309
13310 #[test]
13311 fn out_of_range_line_returns_empty() {
13312 assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13313 }
13314
13315 #[test]
13316 fn caret_clamps_past_eol() {
13317 let out = snippet("hello", 1, 50, "<source>");
13318 assert!(out.contains("\n | ^"), "out:\n{out}");
13319 }
13320
13321 #[test]
13322 fn unicode_codepoint_count_for_caret_clamp() {
13323 let out = snippet("héllo", 1, 99, "<source>");
13325 assert!(out.contains("\n | ^"), "out:\n{out}");
13326 }
13327
13328 #[test]
13329 fn trailing_newline_does_not_create_phantom_last_line() {
13330 let out = snippet("first\nsecond\n", 2, 1, "<source>");
13331 assert!(!out.contains("3 |"));
13332 assert!(out.contains("2 | second"));
13333 }
13334
13335 fn lex(src: &str) -> Vec<Token> {
13338 Lexer::new(src, "<test>").tokenize().expect("lex")
13339 }
13340
13341 #[test]
13342 fn strict_parse_attaches_snippet_when_source_given() {
13343 let src = "garbage_token\nflow F() { }";
13344 let err = Parser::new(lex(src))
13345 .with_source(src, "x.axon")
13346 .parse()
13347 .expect_err("must error");
13348 assert!(err.source_snippet.is_some());
13349 let display = format!("{err}");
13350 assert!(display.contains("--> x.axon:"), "display: {display}");
13351 }
13352
13353 #[test]
13354 fn strict_parse_no_snippet_when_no_source() {
13355 let src = "garbage_token";
13356 let err = Parser::new(lex(src)).parse().expect_err("must error");
13357 assert!(err.source_snippet.is_none());
13358 let display = format!("{err}");
13359 assert!(!display.contains("\n -->"));
13360 }
13361
13362 #[test]
13363 fn every_recovered_error_has_snippet() {
13364 let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13365 let result = Parser::new(lex(src))
13366 .with_source(src, "multi.axon")
13367 .parse_with_recovery();
13368 assert!(!result.errors.is_empty());
13369 for err in &result.errors {
13370 assert!(err.source_snippet.is_some());
13371 let display = format!("{err}");
13372 assert!(
13373 display.contains("--> multi.axon:"),
13374 "display: {display}"
13375 );
13376 }
13377 }
13378
13379 #[test]
13380 fn recovery_no_snippet_when_no_source() {
13381 let src = "garbage1 garbage2";
13382 let result = Parser::new(lex(src)).parse_with_recovery();
13383 for err in &result.errors {
13384 assert!(err.source_snippet.is_none());
13385 }
13386 }
13387
13388 #[test]
13389 fn snippet_points_at_correct_line_for_each_error() {
13390 let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13391 let result = Parser::new(lex(src))
13392 .with_source(src, "x")
13393 .parse_with_recovery();
13394 for err in &result.errors {
13395 let sn = err.source_snippet.as_ref().expect("snippet");
13396 assert_eq!(sn.line, err.line);
13397 }
13398 }
13399
13400 #[test]
13403 fn legacy_constructor_still_works() {
13404 let src = "flow F() { }";
13405 let prog = Parser::new(lex(src)).parse().expect("clean");
13406 assert_eq!(prog.declarations.len(), 1);
13407 }
13408
13409 #[test]
13410 fn attach_source_idempotent() {
13411 let err = ParseError {
13412 message: "bad".to_string(),
13413 line: 2,
13414 column: 3,
13415 ..Default::default()
13416 };
13417 let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13418 let first = format!("{err2}");
13419 let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13420 let second = format!("{err3}");
13421 assert_eq!(first, second);
13422 }
13423
13424 #[test]
13425 fn attach_source_noop_when_line_zero() {
13426 let err = ParseError {
13427 message: "bad".to_string(),
13428 line: 0,
13429 column: 0,
13430 ..Default::default()
13431 };
13432 let err = err.attach_source("a\nb\nc\n", "f.axon");
13433 assert!(err.source_snippet.is_none());
13434 }
13435
13436 #[test]
13442 fn golden_simple_three_line_block() {
13443 let src = "alpha\nbeta\ngamma";
13444 let out = snippet(src, 2, 3, "g.axon");
13445 let expected = concat!(
13449 " --> g.axon:2:3\n",
13450 " |\n",
13451 "1 | alpha\n",
13452 "2 | beta\n",
13453 " | ^\n",
13454 "3 | gamma",
13455 );
13456 assert_eq!(out, expected);
13457 }
13458
13459 #[test]
13460 fn golden_first_line_caret() {
13461 let src = "abc\ndef\n";
13462 let out = snippet(src, 1, 1, "x");
13463 let expected = concat!(
13464 " --> x:1:1\n",
13465 " |\n",
13466 "1 | abc\n",
13467 " | ^\n",
13468 "2 | def",
13469 );
13470 assert_eq!(out, expected);
13471 }
13472
13473 #[test]
13474 fn golden_two_digit_gutter() {
13475 let src: String = (1..=11)
13476 .map(|i| format!("L{i}"))
13477 .collect::<Vec<_>>()
13478 .join("\n");
13479 let out = snippet(&src, 10, 2, "big");
13480 let expected = concat!(
13481 " --> big:10:2\n",
13482 " |\n",
13483 " 8 | L8\n",
13484 " 9 | L9\n",
13485 "10 | L10\n",
13486 " | ^\n",
13487 "11 | L11",
13488 );
13489 assert_eq!(out, expected);
13490 }
13491}
13492
13493#[cfg(test)]
13500mod smart_suggest_parser_tests {
13501 use super::*;
13502 use crate::lexer::Lexer;
13503
13504 fn lex(src: &str) -> Vec<Token> {
13505 Lexer::new(src, "<test>").tokenize().expect("lex")
13506 }
13507
13508 #[test]
13509 fn top_level_typo_suggests_flow() {
13510 let src = "flwo F() { }";
13511 let err = Parser::new(lex(src)).parse().expect_err("must error");
13512 assert!(
13513 err.message.contains("Did you mean `flow`?"),
13514 "msg: {}",
13515 err.message
13516 );
13517 }
13518
13519 #[test]
13520 fn top_level_unknown_far_no_suggestion() {
13521 let src = "qwerty F() { }";
13522 let err = Parser::new(lex(src)).parse().expect_err("must error");
13523 assert!(
13524 !err.message.contains("Did you mean"),
13525 "msg: {}",
13526 err.message
13527 );
13528 }
13529
13530 #[test]
13531 fn flow_body_typo_suggests_step() {
13532 let src = "flow F() { stepp S {} }";
13533 let err = Parser::new(lex(src)).parse().expect_err("must error");
13534 assert!(
13535 err.message.contains("Did you mean `step`"),
13536 "msg: {}",
13537 err.message
13538 );
13539 }
13540
13541 #[test]
13542 fn flow_body_typo_suggests_reason() {
13543 let src = "flow F() { reasn R {} }";
13544 let err = Parser::new(lex(src)).parse().expect_err("must error");
13545 assert!(
13546 err.message.contains("Did you mean `reason`?"),
13547 "msg: {}",
13548 err.message
13549 );
13550 }
13551
13552 #[test]
13553 fn recovery_mode_carries_hint() {
13554 let src = "flwo F() { }";
13555 let result = Parser::new(lex(src)).parse_with_recovery();
13556 assert!(
13557 result
13558 .errors
13559 .iter()
13560 .any(|e| e.message.contains("Did you mean `flow`?")),
13561 "errors: {:?}",
13562 result.errors
13563 );
13564 }
13565}
13566
13567#[cfg(test)]
13570mod mutate_purge_where_tests {
13571 use super::*;
13572
13573 fn parse(src: &str) -> Program {
13574 let tokens = crate::lexer::Lexer::new(src, "<test>")
13575 .tokenize()
13576 .expect("lex");
13577 Parser::new(tokens).parse().expect("parse")
13578 }
13579
13580 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13581 for d in &prog.declarations {
13582 if let Declaration::Flow(f) = d {
13583 if f.name == flow {
13584 return f.body.first().expect("flow has at least one step");
13585 }
13586 }
13587 }
13588 panic!("flow `{flow}` not found");
13589 }
13590
13591 #[test]
13592 fn mutate_captures_its_where_clause() {
13593 let prog =
13596 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13597 match first_step(&prog, "F") {
13598 FlowStep::Mutate(m) => {
13599 assert_eq!(m.store_name, "accounts");
13600 assert_eq!(m.where_expr, "id = 1");
13601 }
13602 other => panic!("expected Mutate, got {other:?}"),
13603 }
13604 }
13605
13606 #[test]
13607 fn purge_captures_its_where_clause() {
13608 let prog =
13609 parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13610 match first_step(&prog, "F") {
13611 FlowStep::Purge(p) => {
13612 assert_eq!(p.store_name, "logs");
13613 assert_eq!(p.where_expr, "ts < 100");
13614 }
13615 other => panic!("expected Purge, got {other:?}"),
13616 }
13617 }
13618
13619 #[test]
13620 fn mutate_without_a_where_block_is_a_whole_store_op() {
13621 let prog = parse("flow F() -> Unit { mutate accounts }");
13624 match first_step(&prog, "F") {
13625 FlowStep::Mutate(m) => {
13626 assert_eq!(m.store_name, "accounts");
13627 assert_eq!(m.where_expr, "");
13628 }
13629 other => panic!("expected Mutate, got {other:?}"),
13630 }
13631 }
13632}
13633
13634#[cfg(test)]
13637mod persist_fields_tests {
13638 use super::*;
13639
13640 fn parse(src: &str) -> Program {
13641 let tokens = crate::lexer::Lexer::new(src, "<test>")
13642 .tokenize()
13643 .expect("lex");
13644 Parser::new(tokens).parse().expect("parse")
13645 }
13646
13647 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13648 for d in &prog.declarations {
13649 if let Declaration::Flow(f) = d {
13650 if f.name == flow {
13651 return f.body.first().expect("flow has at least one step");
13652 }
13653 }
13654 }
13655 panic!("flow `{flow}` not found");
13656 }
13657
13658 #[test]
13659 fn persist_captures_its_field_block() {
13660 let prog = parse(
13664 "flow F() -> Unit { persist into chat_history { \
13665 session_id: \"${session_id}\" sender: \"user\" \
13666 content: \"${message}\" } }",
13667 );
13668 match first_step(&prog, "F") {
13669 FlowStep::Persist(p) => {
13670 assert_eq!(p.store_name, "chat_history");
13671 assert_eq!(
13672 p.fields,
13673 vec![
13674 ("session_id".to_string(), "${session_id}".to_string()),
13675 ("sender".to_string(), "user".to_string()),
13676 ("content".to_string(), "${message}".to_string()),
13677 ]
13678 );
13679 }
13680 other => panic!("expected Persist, got {other:?}"),
13681 }
13682 }
13683
13684 #[test]
13685 fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13686 let prog = parse("flow F() -> Unit { persist events }");
13689 match first_step(&prog, "F") {
13690 FlowStep::Persist(p) => {
13691 assert_eq!(p.store_name, "events");
13692 assert!(p.fields.is_empty());
13693 }
13694 other => panic!("expected Persist, got {other:?}"),
13695 }
13696 }
13697
13698 #[test]
13699 fn persist_accepts_the_optional_into_connector() {
13700 let with =
13703 parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13704 let without =
13705 parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13706 for prog in [&with, &without] {
13707 match first_step(prog, "F") {
13708 FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13709 other => panic!("expected Persist, got {other:?}"),
13710 }
13711 }
13712 }
13713
13714 #[test]
13715 fn persist_into_without_a_block_resolves_the_store_name() {
13716 let prog = parse("flow F() -> Unit { persist into events }");
13719 match first_step(&prog, "F") {
13720 FlowStep::Persist(p) => {
13721 assert_eq!(p.store_name, "events");
13722 assert!(p.fields.is_empty());
13723 }
13724 other => panic!("expected Persist, got {other:?}"),
13725 }
13726 }
13727
13728 #[test]
13729 fn persist_fields_lower_into_the_ir() {
13730 let prog = parse(
13733 "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13734 );
13735 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13736 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13737 match flow.steps.first().expect("one step") {
13738 crate::ir_nodes::IRFlowNode::Persist(p) => {
13739 assert_eq!(p.store_name, "chat");
13740 assert_eq!(
13741 p.fields,
13742 vec![("content".to_string(), "${msg}".to_string())]
13743 );
13744 }
13745 other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13746 }
13747 }
13748}
13749
13750#[cfg(test)]
13753mod mutate_fields_tests {
13754 use super::*;
13755
13756 fn parse(src: &str) -> Program {
13757 let tokens = crate::lexer::Lexer::new(src, "<test>")
13758 .tokenize()
13759 .expect("lex");
13760 Parser::new(tokens).parse().expect("parse")
13761 }
13762
13763 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13764 for d in &prog.declarations {
13765 if let Declaration::Flow(f) = d {
13766 if f.name == flow {
13767 return f.body.first().expect("flow has at least one step");
13768 }
13769 }
13770 }
13771 panic!("flow `{flow}` not found");
13772 }
13773
13774 #[test]
13775 fn mutate_captures_its_set_field_block() {
13776 let prog = parse(
13780 "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13781 balance: \"${new_balance}\" status: \"active\" } }",
13782 );
13783 match first_step(&prog, "F") {
13784 FlowStep::Mutate(m) => {
13785 assert_eq!(m.store_name, "accounts");
13786 assert_eq!(m.where_expr, "id = ${id}");
13787 assert_eq!(
13788 m.fields,
13789 vec![
13790 ("balance".to_string(), "${new_balance}".to_string()),
13791 ("status".to_string(), "active".to_string()),
13792 ]
13793 );
13794 }
13795 other => panic!("expected Mutate, got {other:?}"),
13796 }
13797 }
13798
13799 #[test]
13800 fn mutate_where_only_block_has_no_set_fields() {
13801 let prog =
13804 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13805 match first_step(&prog, "F") {
13806 FlowStep::Mutate(m) => {
13807 assert_eq!(m.where_expr, "id = 1");
13808 assert!(m.fields.is_empty());
13809 }
13810 other => panic!("expected Mutate, got {other:?}"),
13811 }
13812 }
13813
13814 #[test]
13815 fn mutate_with_no_block_is_a_whole_store_op() {
13816 let prog = parse("flow F() -> Unit { mutate accounts }");
13819 match first_step(&prog, "F") {
13820 FlowStep::Mutate(m) => {
13821 assert_eq!(m.store_name, "accounts");
13822 assert_eq!(m.where_expr, "");
13823 assert!(m.fields.is_empty());
13824 }
13825 other => panic!("expected Mutate, got {other:?}"),
13826 }
13827 }
13828
13829 #[test]
13830 fn mutate_fields_lower_into_the_ir() {
13831 let prog = parse(
13832 "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
13833 );
13834 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13835 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13836 match flow.steps.first().expect("one step") {
13837 crate::ir_nodes::IRFlowNode::Mutate(m) => {
13838 assert_eq!(m.where_expr, "id = 1");
13839 assert_eq!(
13840 m.fields,
13841 vec![("v".to_string(), "${x}".to_string())]
13842 );
13843 }
13844 other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
13845 }
13846 }
13847}
13848