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::Attest(n) => {
125 n.leading_trivia = leading;
126 n.trailing_trivia = trailing;
127 }
128 Declaration::Window(n) => {
129 n.leading_trivia = leading;
130 n.trailing_trivia = trailing;
131 }
132 Declaration::Pix(n) => {
133 n.leading_trivia = leading;
134 n.trailing_trivia = trailing;
135 }
136 Declaration::Ledger(n) => {
137 n.leading_trivia = leading;
138 n.trailing_trivia = trailing;
139 }
140 Declaration::Psyche(n) => {
141 n.leading_trivia = leading;
142 n.trailing_trivia = trailing;
143 }
144 Declaration::Corpus(n) => {
145 n.leading_trivia = leading;
146 n.trailing_trivia = trailing;
147 }
148 Declaration::Dataspace(n) => {
149 n.leading_trivia = leading;
150 n.trailing_trivia = trailing;
151 }
152 Declaration::Ots(n) => {
153 n.leading_trivia = leading;
154 n.trailing_trivia = trailing;
155 }
156 Declaration::Mandate(n) => {
157 n.leading_trivia = leading;
158 n.trailing_trivia = trailing;
159 }
160 Declaration::Compute(n) => {
161 n.leading_trivia = leading;
162 n.trailing_trivia = trailing;
163 }
164 Declaration::Daemon(n) => {
165 n.leading_trivia = leading;
166 n.trailing_trivia = trailing;
167 }
168 Declaration::Extension(n) => {
169 n.leading_trivia = leading;
170 n.trailing_trivia = trailing;
171 }
172 Declaration::AxonStore(n) => {
173 n.leading_trivia = leading;
174 n.trailing_trivia = trailing;
175 }
176 Declaration::AxonEndpoint(n) => {
177 n.leading_trivia = leading;
178 n.trailing_trivia = trailing;
179 }
180 Declaration::Resource(n) => {
181 n.leading_trivia = leading;
182 n.trailing_trivia = trailing;
183 }
184 Declaration::Fabric(n) => {
185 n.leading_trivia = leading;
186 n.trailing_trivia = trailing;
187 }
188 Declaration::Manifest(n) => {
189 n.leading_trivia = leading;
190 n.trailing_trivia = trailing;
191 }
192 Declaration::Observe(n) => {
193 n.leading_trivia = leading;
194 n.trailing_trivia = trailing;
195 }
196 Declaration::Reconcile(n) => {
197 n.leading_trivia = leading;
198 n.trailing_trivia = trailing;
199 }
200 Declaration::Lease(n) => {
201 n.leading_trivia = leading;
202 n.trailing_trivia = trailing;
203 }
204 Declaration::Ensemble(n) => {
205 n.leading_trivia = leading;
206 n.trailing_trivia = trailing;
207 }
208 Declaration::Session(n) => {
209 n.leading_trivia = leading;
210 n.trailing_trivia = trailing;
211 }
212 Declaration::Topology(n) => {
213 n.leading_trivia = leading;
214 n.trailing_trivia = trailing;
215 }
216 Declaration::Immune(n) => {
217 n.leading_trivia = leading;
218 n.trailing_trivia = trailing;
219 }
220 Declaration::Reflex(n) => {
221 n.leading_trivia = leading;
222 n.trailing_trivia = trailing;
223 }
224 Declaration::Heal(n) => {
225 n.leading_trivia = leading;
226 n.trailing_trivia = trailing;
227 }
228 Declaration::Component(n) => {
229 n.leading_trivia = leading;
230 n.trailing_trivia = trailing;
231 }
232 Declaration::View(n) => {
233 n.leading_trivia = leading;
234 n.trailing_trivia = trailing;
235 }
236 Declaration::Channel(n) => {
237 n.leading_trivia = leading;
238 n.trailing_trivia = trailing;
239 }
240 Declaration::Socket(n) => {
241 n.leading_trivia = leading;
242 n.trailing_trivia = trailing;
243 }
244 Declaration::Upstream(n) => {
245 n.leading_trivia = leading;
246 n.trailing_trivia = trailing;
247 }
248 Declaration::Voice(n) => {
249 n.leading_trivia = leading;
250 n.trailing_trivia = trailing;
251 }
252 Declaration::Cors(n) => {
253 n.leading_trivia = leading;
254 n.trailing_trivia = trailing;
255 }
256 Declaration::Credential(n) => {
257 n.leading_trivia = leading;
258 n.trailing_trivia = trailing;
259 }
260 Declaration::Cache(n) => {
261 n.leading_trivia = leading;
262 n.trailing_trivia = trailing;
263 }
264 Declaration::Savant(n) => {
265 n.leading_trivia = leading;
266 n.trailing_trivia = trailing;
267 }
268 Declaration::Synth(n) => {
269 n.leading_trivia = leading;
270 n.trailing_trivia = trailing;
271 }
272 Declaration::Scope(n) => {
273 n.leading_trivia = leading;
274 n.trailing_trivia = trailing;
275 }
276 Declaration::Observable(n) => {
277 n.leading_trivia = leading;
278 n.trailing_trivia = trailing;
279 }
280 Declaration::Witness(n) => {
281 n.leading_trivia = leading;
282 n.trailing_trivia = trailing;
283 }
284 Declaration::Document(n) => {
285 n.leading_trivia = leading;
286 n.trailing_trivia = trailing;
287 }
288 Declaration::Deliver(n) => {
289 n.leading_trivia = leading;
290 n.trailing_trivia = trailing;
291 }
292 Declaration::Notify(n) => {
293 n.leading_trivia = leading;
294 n.trailing_trivia = trailing;
295 }
296 Declaration::Generic(n) => {
297 n.leading_trivia = leading;
298 n.trailing_trivia = trailing;
299 }
300 }
301}
302
303pub const SOURCE_CONTEXT_LINES_BEFORE: usize = 2;
310pub const SOURCE_CONTEXT_LINES_AFTER: usize = 2;
311
312#[derive(Debug, Clone)]
323pub struct SourceSnippet {
324 pub source: String,
325 pub line: u32,
326 pub column: u32,
327 pub filename: String,
328 pub context_before: usize,
329 pub context_after: usize,
330}
331
332impl SourceSnippet {
333 pub fn new(source: String, line: u32, column: u32, filename: String) -> Self {
335 Self {
336 source,
337 line,
338 column,
339 filename,
340 context_before: SOURCE_CONTEXT_LINES_BEFORE,
341 context_after: SOURCE_CONTEXT_LINES_AFTER,
342 }
343 }
344
345 #[must_use]
352 pub fn render(&self) -> String {
353 if self.source.is_empty() || self.line < 1 {
354 return String::new();
355 }
356 let raw: Vec<&str> = self.source.split('\n').collect();
357 let lines: Vec<&str> = if raw.last() == Some(&"") {
360 raw[..raw.len() - 1].to_vec()
361 } else {
362 raw
363 };
364 if lines.is_empty() || self.line as usize > lines.len() {
365 return String::new();
366 }
367
368 let line_idx = self.line as usize;
369 let start = line_idx.saturating_sub(self.context_before).max(1);
370 let end = (line_idx + self.context_after).min(lines.len());
371
372 let gutter = end.to_string().len();
373 let empty_gutter = " ".repeat(gutter);
374
375 let mut out: Vec<String> = Vec::with_capacity(end - start + 4);
376 out.push(format!(
377 "{empty_gutter} --> {}:{}:{}",
378 self.filename, self.line, self.column
379 ));
380 out.push(format!("{empty_gutter} |"));
381 for n in start..=end {
382 let line_text = lines[n - 1];
383 out.push(format!("{n:>gutter$} | {line_text}", gutter = gutter));
384 if n == line_idx {
385 let line_len = line_text.chars().count();
386 let col = (self.column as usize).clamp(1, line_len + 1);
387 out.push(format!(
388 "{empty_gutter} | {pad}^",
389 pad = " ".repeat(col - 1)
390 ));
391 }
392 }
393 out.join("\n")
394 }
395}
396
397#[derive(Debug, Clone, Default)]
398pub struct ParseError {
399 pub message: String,
400 pub line: u32,
401 pub column: u32,
402 pub source_snippet: Option<SourceSnippet>,
409}
410
411impl ParseError {
412 #[must_use]
416 pub fn attach_source(mut self, source: &str, filename: &str) -> Self {
417 if self.line >= 1 {
418 self.source_snippet = Some(SourceSnippet::new(
419 source.to_string(),
420 self.line,
421 self.column,
422 filename.to_string(),
423 ));
424 }
425 self
426 }
427}
428
429impl std::fmt::Display for ParseError {
430 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431 write!(f, "[line {}:{}] {}", self.line, self.column, self.message)?;
432 if let Some(snippet) = &self.source_snippet {
433 let block = snippet.render();
434 if !block.is_empty() {
435 write!(f, "\n{block}")?;
436 }
437 }
438 Ok(())
439 }
440}
441
442impl std::error::Error for ParseError {}
443
444#[derive(Debug)]
462pub struct ParseResult {
463 pub program: Program,
464 pub errors: Vec<ParseError>,
465}
466
467impl ParseResult {
468 #[inline]
473 #[must_use]
474 pub fn has_errors(&self) -> bool {
475 !self.errors.is_empty()
476 }
477
478 #[inline]
481 #[must_use]
482 pub fn is_clean(&self) -> bool {
483 self.errors.is_empty()
484 }
485}
486
487#[inline]
501const fn is_top_level_decl_kw_for_recovery(tt: &TokenType) -> bool {
502 matches!(
503 tt,
504 TokenType::Import
505 | TokenType::Persona
506 | TokenType::Context
507 | TokenType::Anchor
508 | TokenType::Memory
509 | TokenType::Tool
510 | TokenType::Type
511 | TokenType::Flow
512 | TokenType::Intent
513 | TokenType::Run
514 | TokenType::Let
515 | TokenType::Know
516 | TokenType::Believe
517 | TokenType::Speculate
518 | TokenType::Doubt
519 | TokenType::Lambda
520 | TokenType::Agent
521 | TokenType::Shield
522 | TokenType::Pix
523 | TokenType::Ledger
524 | TokenType::Psyche
525 | TokenType::Corpus
526 | TokenType::Dataspace
527 | TokenType::Ots
528 | TokenType::Mandate
529 | TokenType::Compute
530 | TokenType::Daemon
531 | TokenType::Savant
533 | TokenType::Synth
534 | TokenType::Scope
536 | TokenType::AxonStore
537 | TokenType::AxonEndpoint
538 | TokenType::Resource
539 | TokenType::Fabric
540 | TokenType::Manifest
541 | TokenType::Observe
542 | TokenType::Reconcile
543 | TokenType::Lease
544 | TokenType::Ensemble
545 | TokenType::Session
546 | TokenType::Topology
547 | TokenType::Immune
548 | TokenType::Reflex
549 | TokenType::Heal
550 | TokenType::Component
551 | TokenType::View
552 | TokenType::Channel
553 | TokenType::Ingest
554 | TokenType::Persist
555 | TokenType::Retrieve
556 | TokenType::Mutate
557 | TokenType::Purge
558 | TokenType::Transact
559 | TokenType::Mcp
560 )
561}
562
563pub const AXONENDPOINT_TRANSPORT_VALUES: &[&str] = &["json", "sse", "ndjson"];
576
577pub const AXONENDPOINT_TRANSPORT_DIALECTS: &[&str] =
621 &["axon", "openai", "kimi", "glm", "anthropic"];
622
623pub const AXONENDPOINT_KEEPALIVE_VALUES: &[&str] = &["5s", "15s", "30s", "60s"];
625
626pub const AXONENDPOINT_METHOD_VALUES: &[&str] =
640 &["GET", "POST", "PUT", "DELETE", "PATCH", "QUERY"];
641
642pub const AXONENDPOINT_BACKEND_VALUES: &[&str] = &[
663 "anthropic",
664 "auto",
665 "gemini",
666 "glm",
667 "kimi",
668 "ollama",
669 "openai",
670 "openrouter",
671 "stub",
672];
673
674#[inline]
675fn axonendpoint_is_valid_transport(s: &str) -> bool {
676 AXONENDPOINT_TRANSPORT_VALUES.iter().any(|&v| v == s)
677}
678
679#[inline]
680fn axonendpoint_is_valid_method(s: &str) -> bool {
681 AXONENDPOINT_METHOD_VALUES.iter().any(|&v| v == s)
682}
683
684#[inline]
685fn axonendpoint_is_valid_backend(s: &str) -> bool {
686 AXONENDPOINT_BACKEND_VALUES.iter().any(|&v| v == s)
687}
688
689#[inline]
690fn axonendpoint_is_valid_keepalive(s: &str) -> bool {
691 AXONENDPOINT_KEEPALIVE_VALUES.iter().any(|&v| v == s)
692}
693
694pub const AXONENDPOINT_QUERY_PARAM_TYPES: &[&str] =
709 &["Text", "Int", "Float", "Bool", "Uuid"];
710
711#[inline]
714pub(crate) fn axonendpoint_is_valid_query_param_type(s: &str) -> bool {
715 AXONENDPOINT_QUERY_PARAM_TYPES.iter().any(|&v| v == s)
716}
717
718pub(crate) fn extract_path_param_names(path: &str) -> Result<Vec<String>, String> {
745 let mut out: Vec<String> = Vec::new();
746 let bytes = path.as_bytes();
747 let mut i = 0;
748 while i < bytes.len() {
749 if bytes[i] != b'{' {
750 i += 1;
751 continue;
752 }
753 let start = i + 1;
756 let mut end = start;
757 while end < bytes.len() && bytes[end] != b'}' {
758 end += 1;
759 }
760 if end == bytes.len() {
761 break;
764 }
765 let raw = &path[start..end];
766 let valid = !raw.is_empty()
768 && raw.bytes().enumerate().all(|(idx, b)| {
769 if idx == 0 {
770 b.is_ascii_alphabetic() || b == b'_'
771 } else {
772 b.is_ascii_alphanumeric() || b == b'_'
773 }
774 });
775 if valid {
776 let name = raw.to_string();
777 if out.iter().any(|existing| existing == &name) {
778 return Err(name);
779 }
780 out.push(name);
781 }
782 i = end + 1;
783 }
784 Ok(out)
785}
786
787pub fn is_valid_capability_slug(slug: &str) -> bool {
801 if slug.is_empty() {
802 return false;
803 }
804 for segment in slug.split('.') {
805 if !is_valid_slug_segment(segment) {
806 return false;
807 }
808 }
809 true
810}
811
812fn is_valid_slug_segment(seg: &str) -> bool {
813 let mut chars = seg.chars();
814 let first = match chars.next() {
815 Some(c) => c,
816 None => return false,
817 };
818 if !first.is_ascii_lowercase() {
819 return false;
820 }
821 chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
822}
823
824#[cfg(test)]
834mod query_param_catalog_tests {
835 use super::{axonendpoint_is_valid_query_param_type, AXONENDPOINT_QUERY_PARAM_TYPES};
836
837 #[test]
838 fn accepts_every_catalog_entry() {
839 for ty in AXONENDPOINT_QUERY_PARAM_TYPES {
840 assert!(
841 axonendpoint_is_valid_query_param_type(ty),
842 "catalog entry `{ty}` must validate"
843 );
844 }
845 }
846
847 #[test]
848 fn rejects_off_catalog_types() {
849 for off in &[
850 "Timestamp", "Date",
852 "DateTime",
853 "List<Text>", "Jsonb", "Bytea",
856 "text", "TEXT",
858 "Number", "", " ", ] {
862 assert!(
863 !axonendpoint_is_valid_query_param_type(off),
864 "off-catalog `{off}` must reject"
865 );
866 }
867 }
868
869 #[test]
870 fn catalog_size_matches_design() {
871 assert_eq!(AXONENDPOINT_QUERY_PARAM_TYPES.len(), 5);
875 }
876}
877
878#[cfg(test)]
879mod query_param_parser_tests {
880 use crate::lexer::Lexer;
881 use crate::parser::Parser;
882
883 fn parse_endpoint_source(src: &str) -> Result<crate::ast::AxonEndpointDefinition, String> {
884 let tokens = Lexer::new(src, "test.axon")
885 .tokenize()
886 .map_err(|e| format!("lex: {}", e.message))?;
887 let mut parser = Parser::new(tokens);
888 let program = parser.parse().map_err(|e| format!("parse: {}", e.message))?;
889 program
890 .declarations
891 .into_iter()
892 .find_map(|d| match d {
893 crate::ast::Declaration::AxonEndpoint(e) => Some(e),
894 _ => None,
895 })
896 .ok_or_else(|| "no axonendpoint in program".to_string())
897 }
898
899 #[test]
900 fn endpoint_with_no_query_block_keeps_empty_vec() {
901 let src = r#"
902 axonendpoint write_secret {
903 method: POST
904 path: "/api/users"
905 body: SecretWriteRequest
906 execute: WriteSecret
907 }
908 "#;
909 let ep = parse_endpoint_source(src).expect("parses");
910 assert!(
911 ep.query_params.is_empty(),
912 "D5 — no `query:` block ⇒ empty query_params"
913 );
914 }
915
916 #[test]
917 fn single_query_param_required() {
918 let src = r#"
919 axonendpoint list_users {
920 method: GET
921 path: "/api/users"
922 query: { status: Text }
923 execute: ListUsers
924 }
925 "#;
926 let ep = parse_endpoint_source(src).expect("parses");
927 assert_eq!(ep.query_params.len(), 1);
928 assert_eq!(ep.query_params[0].name, "status");
929 assert_eq!(ep.query_params[0].type_expr.name, "Text");
930 assert!(!ep.query_params[0].type_expr.optional);
931 }
932
933 #[test]
934 fn optional_query_param_via_question_suffix() {
935 let src = r#"
936 axonendpoint list_users {
937 method: GET
938 path: "/api/users"
939 query: { limit: Int? }
940 execute: ListUsers
941 }
942 "#;
943 let ep = parse_endpoint_source(src).expect("parses");
944 assert_eq!(ep.query_params.len(), 1);
945 assert_eq!(ep.query_params[0].name, "limit");
946 assert_eq!(ep.query_params[0].type_expr.name, "Int");
947 assert!(
948 ep.query_params[0].type_expr.optional,
949 "`?` suffix sets optional"
950 );
951 }
952
953 #[test]
954 fn multiple_query_params_preserve_declaration_order() {
955 let src = r#"
956 axonendpoint search {
957 method: GET
958 path: "/api/search"
959 query: { q: Text, page: Int?, limit: Int?, exact: Bool? }
960 execute: Search
961 }
962 "#;
963 let ep = parse_endpoint_source(src).expect("parses");
964 let names: Vec<&str> = ep.query_params.iter().map(|f| f.name.as_str()).collect();
965 assert_eq!(names, vec!["q", "page", "limit", "exact"]);
966 let types: Vec<&str> = ep
967 .query_params
968 .iter()
969 .map(|f| f.type_expr.name.as_str())
970 .collect();
971 assert_eq!(types, vec!["Text", "Int", "Int", "Bool"]);
972 let optionals: Vec<bool> = ep
973 .query_params
974 .iter()
975 .map(|f| f.type_expr.optional)
976 .collect();
977 assert_eq!(optionals, vec![false, true, true, true]);
978 }
979
980 #[test]
981 fn duplicate_query_param_is_parse_error() {
982 let src = r#"
983 axonendpoint bad {
984 method: GET
985 path: "/api/x"
986 query: { name: Text, name: Int? }
987 execute: Bad
988 }
989 "#;
990 let err = parse_endpoint_source(src).expect_err("must fail");
991 assert!(
992 err.contains("duplicate query param 'name'"),
993 "error must name the duplicate. Got: {err}"
994 );
995 }
996
997 #[test]
998 fn off_catalog_type_with_smart_suggest_hint() {
999 let src = r#"
1004 axonendpoint bad {
1005 method: GET
1006 path: "/api/x"
1007 query: { value: Strng }
1008 execute: Bad
1009 }
1010 "#;
1011 let err = parse_endpoint_source(src).expect_err("must fail");
1012 assert!(
1013 err.contains("unsupported type 'Strng'"),
1014 "error must name the bad type. Got: {err}"
1015 );
1016 assert!(
1017 err.contains("Expected one of: Text | Int | Float | Bool | Uuid"),
1018 "error must list the closed catalog. Got: {err}"
1019 );
1020 }
1021
1022 #[test]
1023 fn close_typo_gets_did_you_mean_hint() {
1024 let src = r#"
1027 axonendpoint bad {
1028 method: GET
1029 path: "/api/x"
1030 query: { value: Txt }
1031 execute: Bad
1032 }
1033 "#;
1034 let err = parse_endpoint_source(src).expect_err("must fail");
1035 assert!(
1036 err.contains("Did you mean") && err.contains("`Text`"),
1037 "smart-suggest must hint `Text`. Got: {err}"
1038 );
1039 }
1040
1041 #[test]
1042 fn every_catalog_type_parses_cleanly() {
1043 for ty in &["Text", "Int", "Float", "Bool", "Uuid"] {
1045 let src = format!(
1046 r#"
1047 axonendpoint x {{
1048 method: GET
1049 path: "/api/x"
1050 query: {{ v: {ty} }}
1051 execute: X
1052 }}
1053 "#
1054 );
1055 let ep = parse_endpoint_source(&src)
1056 .unwrap_or_else(|e| panic!("`{ty}` should parse: {e}"));
1057 assert_eq!(ep.query_params[0].type_expr.name, *ty);
1058 }
1059 }
1060
1061 #[test]
1062 fn comma_optional_between_params() {
1063 let src = r#"
1067 axonendpoint x {
1068 method: GET
1069 path: "/api/x"
1070 query: { a: Text b: Int? }
1071 execute: X
1072 }
1073 "#;
1074 let ep = parse_endpoint_source(src).expect("parses without commas");
1075 assert_eq!(ep.query_params.len(), 2);
1076 }
1077
1078 #[test]
1081 fn double_query_block_is_parse_error() {
1082 let src = r#"
1086 axonendpoint x {
1087 method: GET
1088 path: "/api/x"
1089 query: { a: Text }
1090 query: { b: Int? }
1091 execute: X
1092 }
1093 "#;
1094 let err = parse_endpoint_source(src).expect_err("must fail");
1095 assert!(
1096 err.contains("declares `query: { … }` more than once"),
1097 "error must call out the duplicate block. Got: {err}"
1098 );
1099 assert!(
1100 err.contains("combine all params into a single block"),
1101 "error must hint the canonical fix. Got: {err}"
1102 );
1103 }
1104
1105 #[test]
1106 fn optional_generic_type_is_parse_error_with_canonical_hint() {
1107 let src = r#"
1111 axonendpoint x {
1112 method: GET
1113 path: "/api/x"
1114 query: { value: Optional<Text> }
1115 execute: X
1116 }
1117 "#;
1118 let err = parse_endpoint_source(src).expect_err("must fail");
1119 assert!(
1120 err.contains("generic type `Optional<Text>`"),
1121 "error must name the generic type literally. Got: {err}"
1122 );
1123 assert!(
1124 err.contains("Use `Text?` (the `?` suffix)"),
1125 "error must hint the canonical `Text?` syntax. Got: {err}"
1126 );
1127 }
1128
1129 #[test]
1130 fn list_generic_type_is_parse_error_with_deferral_hint() {
1131 let src = r#"
1136 axonendpoint x {
1137 method: GET
1138 path: "/api/x"
1139 query: { tags: List<Text> }
1140 execute: X
1141 }
1142 "#;
1143 let err = parse_endpoint_source(src).expect_err("must fail");
1144 assert!(
1145 err.contains("generic type `List<Text>`"),
1146 "error must name the generic type. Got: {err}"
1147 );
1148 assert!(
1149 err.contains("Multi-value query params")
1150 && err.contains("honest-deferred"),
1151 "error must mention the multi-value deferral. Got: {err}"
1152 );
1153 }
1154
1155 #[test]
1156 fn other_generic_types_caught_generically() {
1157 let src = r#"
1161 axonendpoint x {
1162 method: GET
1163 path: "/api/x"
1164 query: { value: Stream<Int> }
1165 execute: X
1166 }
1167 "#;
1168 let err = parse_endpoint_source(src).expect_err("must fail");
1169 assert!(
1170 err.contains("generic type `Stream<Int>`"),
1171 "error must name the generic type. Got: {err}"
1172 );
1173 assert!(
1174 err.contains("Text | Int | Float | Bool | Uuid"),
1175 "error must list the closed catalog. Got: {err}"
1176 );
1177 }
1178
1179 #[test]
1180 fn uuid_optional_parses_cleanly() {
1181 let src = r#"
1184 axonendpoint find {
1185 method: GET
1186 path: "/api/x"
1187 query: { after: Uuid? }
1188 execute: Find
1189 }
1190 "#;
1191 let ep = parse_endpoint_source(src).expect("parses");
1192 assert_eq!(ep.query_params.len(), 1);
1193 assert_eq!(ep.query_params[0].name, "after");
1194 assert_eq!(ep.query_params[0].type_expr.name, "Uuid");
1195 assert!(ep.query_params[0].type_expr.optional);
1196 assert_eq!(ep.query_params[0].type_expr.generic_param, "");
1197 }
1198
1199 #[test]
1200 fn empty_query_block_yields_empty_vec() {
1201 let src = r#"
1205 axonendpoint x {
1206 method: GET
1207 path: "/api/x"
1208 query: { }
1209 execute: X
1210 }
1211 "#;
1212 let ep = parse_endpoint_source(src).expect("empty block parses");
1213 assert!(ep.query_params.is_empty());
1214 }
1215
1216 #[test]
1217 fn kivi_secret_write_path_plus_query() {
1218 let src = r#"
1222 axonendpoint write_secret {
1223 method: POST
1224 path: "/api/tenants/{tenant_id}/secrets/{secret_name}"
1225 query: { dry_run: Bool?, overwrite: Bool? }
1226 body: SecretWriteRequest
1227 execute: WriteSecret
1228 }
1229 "#;
1230 let ep = parse_endpoint_source(src).expect("parses");
1231 assert_eq!(ep.path_params, vec!["tenant_id", "secret_name"]);
1233 assert_eq!(ep.query_params.len(), 2);
1235 assert_eq!(ep.query_params[0].name, "dry_run");
1236 assert_eq!(ep.query_params[0].type_expr.name, "Bool");
1237 assert!(ep.query_params[0].type_expr.optional);
1238 assert_eq!(ep.query_params[1].name, "overwrite");
1239 assert_eq!(ep.body_type, "SecretWriteRequest");
1241 }
1242}
1243
1244#[cfg(test)]
1245mod path_param_extraction_tests {
1246 use super::extract_path_param_names;
1247
1248 #[test]
1249 fn empty_path_no_placeholders() {
1250 assert_eq!(extract_path_param_names("/api/users"), Ok(vec![]));
1251 assert_eq!(extract_path_param_names("/"), Ok(vec![]));
1252 assert_eq!(extract_path_param_names(""), Ok(vec![]));
1253 }
1254
1255 #[test]
1256 fn single_placeholder() {
1257 assert_eq!(
1258 extract_path_param_names("/api/users/{id}"),
1259 Ok(vec!["id".to_string()])
1260 );
1261 }
1262
1263 #[test]
1264 fn multiple_placeholders_in_declaration_order() {
1265 assert_eq!(
1266 extract_path_param_names(
1267 "/api/tenants/{tenant_id}/secrets/{secret_name}"
1268 ),
1269 Ok(vec![
1270 "tenant_id".to_string(),
1271 "secret_name".to_string(),
1272 ])
1273 );
1274 }
1275
1276 #[test]
1277 fn kivi_chat_history_path_pattern() {
1278 let names = extract_path_param_names(
1282 "/api/tenants/{tenant_id}/secrets/{secret_name}",
1283 );
1284 assert_eq!(
1285 names,
1286 Ok(vec![
1287 "tenant_id".to_string(),
1288 "secret_name".to_string(),
1289 ])
1290 );
1291 }
1292
1293 #[test]
1294 fn duplicate_placeholder_returns_err() {
1295 assert_eq!(
1296 extract_path_param_names("/api/users/{id}/posts/{id}"),
1297 Err("id".to_string())
1298 );
1299 }
1300
1301 #[test]
1302 fn underscore_and_numeric_in_name() {
1303 assert_eq!(
1304 extract_path_param_names("/api/{user_id}/items/{item_2}"),
1305 Ok(vec!["user_id".to_string(), "item_2".to_string()])
1306 );
1307 }
1308
1309 #[test]
1310 fn leading_underscore_accepted() {
1311 assert_eq!(
1317 extract_path_param_names("/api/{_internal}"),
1318 Ok(vec!["_internal".to_string()])
1319 );
1320 }
1321
1322 #[test]
1323 fn malformed_placeholder_silently_ignored() {
1324 assert_eq!(
1328 extract_path_param_names("/api/{not valid}"),
1329 Ok(vec![])
1330 );
1331 assert_eq!(extract_path_param_names("/api/{}"), Ok(vec![]));
1333 assert_eq!(
1335 extract_path_param_names("/api/{tenant id}/users/{id}"),
1336 Ok(vec!["id".to_string()])
1337 );
1338 }
1339
1340 #[test]
1341 fn unterminated_brace_returns_clean() {
1342 assert_eq!(extract_path_param_names("/api/{id"), Ok(vec![]));
1345 }
1346
1347 #[test]
1348 fn placeholders_at_path_boundaries() {
1349 assert_eq!(
1352 extract_path_param_names("{prefix}/api/users/{id}"),
1353 Ok(vec!["prefix".to_string(), "id".to_string()])
1354 );
1355 assert_eq!(
1356 extract_path_param_names("/api/{id}"),
1357 Ok(vec!["id".to_string()])
1358 );
1359 }
1360
1361 #[test]
1362 fn deduplication_detects_non_adjacent_duplicates() {
1363 assert_eq!(
1365 extract_path_param_names(
1366 "/api/orgs/{org_id}/teams/{team_id}/repos/{org_id}"
1367 ),
1368 Err("org_id".to_string())
1369 );
1370 }
1371
1372 #[test]
1373 fn never_panics_on_arbitrary_input() {
1374 for input in &[
1376 "{",
1377 "}",
1378 "{}",
1379 "{{}}",
1380 "{{{",
1381 "/api/{}/{id}",
1382 "////",
1383 "\u{1F4A1}", "\u{0000}", ] {
1386 let _ = extract_path_param_names(input); }
1388 }
1389}
1390
1391#[cfg(test)]
1392mod capability_slug_tests {
1393 use super::is_valid_capability_slug;
1394
1395 #[test]
1396 fn accepts_canonical_examples() {
1397 assert!(is_valid_capability_slug("admin"));
1398 assert!(is_valid_capability_slug("legal.read"));
1399 assert!(is_valid_capability_slug("hipaa.phi.read"));
1400 assert!(is_valid_capability_slug("bank.officer.senior"));
1401 assert!(is_valid_capability_slug("a"));
1402 assert!(is_valid_capability_slug("a_b"));
1403 assert!(is_valid_capability_slug("a1"));
1404 assert!(is_valid_capability_slug("a.b1_c"));
1405 }
1406
1407 #[test]
1408 fn rejects_empty_string() {
1409 assert!(!is_valid_capability_slug(""));
1410 }
1411
1412 #[test]
1413 fn rejects_uppercase() {
1414 assert!(!is_valid_capability_slug("Admin"));
1415 assert!(!is_valid_capability_slug("admin.READ"));
1416 }
1417
1418 #[test]
1419 fn rejects_digit_first() {
1420 assert!(!is_valid_capability_slug("1admin"));
1421 assert!(!is_valid_capability_slug("admin.1read"));
1422 }
1423
1424 #[test]
1425 fn rejects_hyphen() {
1426 assert!(!is_valid_capability_slug("bank-officer"));
1427 }
1428
1429 #[test]
1430 fn rejects_empty_segments() {
1431 assert!(!is_valid_capability_slug("bank..a"));
1432 assert!(!is_valid_capability_slug(".admin"));
1433 assert!(!is_valid_capability_slug("admin."));
1434 }
1435
1436 #[test]
1437 fn rejects_special_chars() {
1438 assert!(!is_valid_capability_slug("admin@read"));
1439 assert!(!is_valid_capability_slug("admin/read"));
1440 assert!(!is_valid_capability_slug("admin read"));
1441 }
1442}
1443
1444pub struct Parser {
1447 tokens: Vec<Token>,
1448 pos: usize,
1449 hoisted: Vec<Declaration>,
1469 leading_trivia: Vec<Vec<Trivia>>,
1474 trailing_trivia: Vec<Vec<Trivia>>,
1478 last_let_value_kind: String,
1482 loop_depth: u32,
1487 source: Option<String>,
1493 filename: String,
1494}
1495
1496impl Parser {
1497 pub fn new(raw_tokens: Vec<Token>) -> Self {
1498 let mut effective: Vec<Token> = Vec::with_capacity(raw_tokens.len());
1508 let mut leading: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1509 let mut trailing: Vec<Vec<Trivia>> = Vec::with_capacity(raw_tokens.len());
1510
1511 let mut pending_leading: Vec<Trivia> = Vec::new();
1512 let mut last_effective_line: i64 = -1;
1513 for tok in raw_tokens {
1514 if is_comment_token(&tok.ttype) {
1515 let kind = token_to_trivia_kind(&tok.ttype)
1516 .expect("comment token must map to a trivia kind");
1517 let triv = Trivia {
1518 kind,
1519 text: tok.value,
1520 line: tok.line,
1521 column: tok.column,
1522 };
1523 if !effective.is_empty() && (tok.line as i64) == last_effective_line {
1524 trailing.last_mut().unwrap().push(triv);
1525 } else {
1526 pending_leading.push(triv);
1527 }
1528 } else {
1529 last_effective_line = tok.line as i64;
1530 effective.push(tok);
1531 leading.push(std::mem::take(&mut pending_leading));
1532 trailing.push(Vec::new());
1533 }
1534 }
1535
1536 Parser {
1537 hoisted: Vec::new(),
1538 tokens: effective,
1539 pos: 0,
1540 leading_trivia: leading,
1541 trailing_trivia: trailing,
1542 last_let_value_kind: "literal".to_string(),
1543 loop_depth: 0,
1544 source: None,
1545 filename: "<source>".to_string(),
1546 }
1547 }
1548
1549 #[must_use]
1561 pub fn with_source(mut self, source: &str, filename: &str) -> Self {
1562 self.source = Some(source.to_string());
1563 self.filename = filename.to_string();
1564 self
1565 }
1566
1567 pub fn parse(&mut self) -> Result<Program, ParseError> {
1570 let mut program = Program {
1571 declarations: Vec::new(),
1572 declaration_trivia: Vec::new(),
1573 loc: Loc { line: 1, column: 1 },
1574 };
1575 while !self.check(TokenType::Eof) {
1576 let start_pos = self.pos;
1582 let mut decl = match self.parse_declaration() {
1583 Ok(d) => d,
1584 Err(e) => return Err(self.attach_source_to_error(e)),
1585 };
1586 let end_pos = self.pos.saturating_sub(1);
1587 let leading = self
1588 .leading_trivia
1589 .get(start_pos)
1590 .cloned()
1591 .unwrap_or_default();
1592 let trailing = self
1593 .trailing_trivia
1594 .get(end_pos)
1595 .cloned()
1596 .unwrap_or_default();
1597 attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1603 program.declarations.push(decl);
1604 program
1605 .declaration_trivia
1606 .push(DeclarationTrivia { leading, trailing });
1607 for hoisted in std::mem::take(&mut self.hoisted) {
1611 program.declarations.push(hoisted);
1612 program.declaration_trivia.push(DeclarationTrivia {
1613 leading: Vec::new(),
1614 trailing: Vec::new(),
1615 });
1616 }
1617 }
1618 crate::voice_desugar::expand(&mut program);
1625 crate::upstream_presets::expand(&mut program);
1626 Ok(program)
1627 }
1628
1629 pub fn parse_with_recovery(&mut self) -> ParseResult {
1662 let mut program = Program {
1663 declarations: Vec::new(),
1664 declaration_trivia: Vec::new(),
1665 loc: Loc { line: 1, column: 1 },
1666 };
1667 let mut errors: Vec<ParseError> = Vec::new();
1668
1669 while !self.check(TokenType::Eof) {
1670 let start_pos = self.pos;
1671 match self.parse_declaration() {
1672 Ok(mut decl) => {
1673 let end_pos = self.pos.saturating_sub(1);
1674 let leading = self
1675 .leading_trivia
1676 .get(start_pos)
1677 .cloned()
1678 .unwrap_or_default();
1679 let trailing = self
1680 .trailing_trivia
1681 .get(end_pos)
1682 .cloned()
1683 .unwrap_or_default();
1684 attach_trivia_to_decl(&mut decl, leading.clone(), trailing.clone());
1685 program.declarations.push(decl);
1686 program
1687 .declaration_trivia
1688 .push(DeclarationTrivia { leading, trailing });
1689 }
1690 Err(err) => {
1691 errors.push(self.attach_source_to_error(err));
1695 if self.pos == start_pos && !self.check(TokenType::Eof) {
1700 self.advance();
1701 }
1702 self.advance_to_sync_point();
1703 }
1704 }
1705 }
1706
1707 ParseResult { program, errors }
1708 }
1709
1710 fn attach_source_to_error(&self, err: ParseError) -> ParseError {
1715 match &self.source {
1716 Some(src) if err.line >= 1 => err.attach_source(src, &self.filename),
1717 _ => err,
1718 }
1719 }
1720
1721 fn advance_to_sync_point(&mut self) {
1726 let mut depth: i32 = 0;
1727 while !self.check(TokenType::Eof) {
1728 let tt = self.current().ttype.clone();
1729 if is_top_level_decl_kw_for_recovery(&tt) && depth <= 0 {
1733 return;
1734 }
1735 if matches!(tt, TokenType::LBrace) {
1736 depth += 1;
1737 } else if matches!(tt, TokenType::RBrace) {
1738 depth -= 1;
1739 }
1740 self.advance();
1741 }
1742 }
1743
1744 fn current(&self) -> &Token {
1747 if self.pos >= self.tokens.len() {
1748 self.tokens.last().unwrap() } else {
1750 &self.tokens[self.pos]
1751 }
1752 }
1753
1754 fn advance(&mut self) -> &Token {
1755 let idx = self.pos;
1756 if self.pos < self.tokens.len() {
1757 self.pos += 1;
1758 }
1759 &self.tokens[idx]
1760 }
1761
1762 fn check(&self, tt: TokenType) -> bool {
1763 self.current().ttype == tt
1764 }
1765
1766 fn consume(&mut self, expected: TokenType) -> Result<Token, ParseError> {
1767 let tok = self.current().clone();
1768 if tok.ttype != expected {
1769 return Err(ParseError {
1770 message: format!(
1771 "Expected {:?}, found {:?}('{}')",
1772 expected, tok.ttype, tok.value
1773 ),
1774 line: tok.line,
1775 column: tok.column,
1776 ..Default::default()
1777 });
1778 }
1779 self.pos += 1;
1780 Ok(tok)
1781 }
1782
1783 fn error(&self, message: &str) -> ParseError {
1785 let tok = self.current();
1786 ParseError { message: message.to_string(), line: tok.line, column: tok.column, ..Default::default() }
1787 }
1788
1789 fn consume_any_ident_or_kw(&mut self) -> Result<Token, ParseError> {
1791 let tok = self.current().clone();
1792 match tok.ttype {
1793 TokenType::Identifier
1794 | TokenType::Bool
1795 | TokenType::StringLit
1796 | TokenType::Integer
1797 | TokenType::Float => {
1798 self.pos += 1;
1799 Ok(tok)
1800 }
1801 _ => {
1802 if !tok.value.is_empty()
1804 && tok.value.chars().all(|c| c.is_alphanumeric() || c == '_')
1805 && tok.ttype != TokenType::Eof
1806 {
1807 self.pos += 1;
1808 Ok(tok)
1809 } else {
1810 Err(ParseError {
1811 message: format!(
1812 "Expected identifier or keyword value, found {:?}('{}')",
1813 tok.ttype, tok.value
1814 ),
1815 line: tok.line,
1816 column: tok.column,
1817 ..Default::default()
1818 })
1819 }
1820 }
1821 }
1822 }
1823
1824 fn consume_number(&mut self) -> Result<f64, ParseError> {
1825 let tok = self.current().clone();
1826 match tok.ttype {
1827 TokenType::Float | TokenType::Integer => {
1828 self.pos += 1;
1829 tok.value.parse::<f64>().map_err(|_| ParseError {
1830 message: format!("Invalid number '{}'", tok.value),
1831 line: tok.line,
1832 column: tok.column,
1833 ..Default::default()
1834 })
1835 }
1836 _ => Err(ParseError {
1837 message: format!("Expected number, found {:?}('{}')", tok.ttype, tok.value),
1838 line: tok.line,
1839 column: tok.column,
1840 ..Default::default()
1841 }),
1842 }
1843 }
1844
1845 fn parse_bool(&mut self) -> Result<bool, ParseError> {
1846 let tok = self.consume(TokenType::Bool)?;
1847 Ok(tok.value == "true")
1848 }
1849
1850 fn loc_of(&self, tok: &Token) -> Loc {
1851 Loc {
1852 line: tok.line,
1853 column: tok.column,
1854 }
1855 }
1856
1857 fn check_run_modifier(&self) -> bool {
1858 if self.current().value == "with" {
1865 return true;
1866 }
1867 matches!(
1868 self.current().ttype,
1869 TokenType::As
1870 | TokenType::Within
1871 | TokenType::ConstrainedBy
1872 | TokenType::OnFailure
1873 | TokenType::OutputTo
1874 | TokenType::Effort
1875 )
1876 }
1877
1878 fn parse_string_list(&mut self) -> Result<Vec<String>, ParseError> {
1881 self.consume(TokenType::LBracket)?;
1882 let mut items = Vec::new();
1883 items.push(self.consume(TokenType::StringLit)?.value);
1884 while self.check(TokenType::Comma) {
1885 self.advance();
1886 items.push(self.consume(TokenType::StringLit)?.value);
1887 }
1888 self.consume(TokenType::RBracket)?;
1889 Ok(items)
1890 }
1891
1892 fn parse_bracketed_strings(&mut self) -> Result<Vec<String>, ParseError> {
1901 self.consume(TokenType::LBracket)?;
1902 let mut items = Vec::new();
1903 if !self.check(TokenType::RBracket) {
1904 items.push(self.consume(TokenType::StringLit)?.value);
1905 while self.check(TokenType::Comma) {
1906 self.advance();
1907 if self.check(TokenType::RBracket) {
1908 break; }
1910 items.push(self.consume(TokenType::StringLit)?.value);
1911 }
1912 }
1913 self.consume(TokenType::RBracket)?;
1914 Ok(items)
1915 }
1916
1917 fn parse_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1918 let mut names = Vec::new();
1919 names.push(self.consume(TokenType::Identifier)?.value);
1920 while self.check(TokenType::Comma) {
1921 self.advance();
1922 names.push(self.consume(TokenType::Identifier)?.value);
1923 }
1924 Ok(names)
1925 }
1926
1927 fn parse_bracketed_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
1928 self.consume(TokenType::LBracket)?;
1929 let items = self.parse_extended_identifier_list()?;
1930 self.consume(TokenType::RBracket)?;
1931 Ok(items)
1932 }
1933
1934 fn parse_extended_identifier_list(&mut self) -> Result<Vec<String>, ParseError> {
1935 let mut items = Vec::new();
1936 items.push(self.consume_any_ident_or_kw()?.value);
1937 while self.check(TokenType::Comma) {
1938 self.advance();
1939 items.push(self.consume_any_ident_or_kw()?.value);
1940 }
1941 Ok(items)
1942 }
1943
1944 fn parse_dotted_identifier(&mut self) -> Result<String, ParseError> {
1945 let mut parts = vec![self.consume_any_ident_or_kw()?.value];
1946 while self.check(TokenType::Dot) {
1947 self.advance();
1948 parts.push(self.consume_any_ident_or_kw()?.value);
1949 }
1950 Ok(parts.join("."))
1951 }
1952
1953 fn parse_subject(&mut self) -> Result<String, ParseError> {
1977 let t = self.current().clone();
1978 match t.ttype {
1979 TokenType::StringLit => {
1980 self.advance();
1981 Ok(format!("\"{}\"", t.value))
1982 }
1983 TokenType::Integer | TokenType::Float => {
1984 self.advance();
1985 Ok(t.value)
1986 }
1987 _ => self.parse_dotted_identifier(),
1988 }
1989 }
1990
1991 fn parse_expression_string(&mut self) -> Result<String, ParseError> {
1992 if self.check(TokenType::LBracket) {
1993 let items = self.parse_bracketed_dot_identifiers()?;
1994 return Ok(format!("[{}]", items.join(", ")));
1995 }
1996 self.parse_dotted_identifier()
1997 }
1998
1999 fn parse_bracketed_dot_identifiers(&mut self) -> Result<Vec<String>, ParseError> {
2000 self.consume(TokenType::LBracket)?;
2001 let mut items = vec![self.parse_dotted_identifier()?];
2002 while self.check(TokenType::Comma) {
2003 self.advance();
2004 items.push(self.parse_dotted_identifier()?);
2005 }
2006 self.consume(TokenType::RBracket)?;
2007 Ok(items)
2008 }
2009
2010 fn parse_argument_list(&mut self) -> Result<Vec<String>, ParseError> {
2011 let mut args = Vec::new();
2012 while !self.check(TokenType::RParen) {
2013 let tok = self.current().clone();
2014 match tok.ttype {
2015 TokenType::StringLit | TokenType::Integer | TokenType::Float => {
2016 self.advance();
2017 args.push(tok.value);
2018 }
2019 TokenType::Identifier => {
2020 self.advance();
2021 let mut val = tok.value;
2022 if self.check(TokenType::Dot) {
2023 self.advance();
2024 val.push('.');
2025 val.push_str(&self.consume_any_ident_or_kw()?.value);
2026 }
2027 args.push(val);
2028 }
2029 _ => {
2030 self.advance();
2031 let key = tok.value;
2032 if self.check(TokenType::Colon) {
2033 self.advance();
2034 let v = self.advance().value.clone();
2035 args.push(format!("{key}:{v}"));
2036 } else {
2037 args.push(key);
2038 }
2039 }
2040 }
2041 if self.check(TokenType::Comma) {
2042 self.advance();
2043 }
2044 }
2045 Ok(args)
2046 }
2047
2048 fn skip_value(&mut self) {
2050 match self.current().ttype {
2051 TokenType::LBracket => {
2052 self.advance();
2053 let mut depth = 1u32;
2054 while depth > 0 && !self.check(TokenType::Eof) {
2055 if self.check(TokenType::LBracket) {
2056 depth += 1;
2057 } else if self.check(TokenType::RBracket) {
2058 depth -= 1;
2059 }
2060 self.advance();
2061 }
2062 }
2063 TokenType::LBrace => {
2064 self.advance();
2065 let mut depth = 1u32;
2066 while depth > 0 && !self.check(TokenType::Eof) {
2067 if self.check(TokenType::LBrace) {
2068 depth += 1;
2069 } else if self.check(TokenType::RBrace) {
2070 depth -= 1;
2071 }
2072 self.advance();
2073 }
2074 }
2075 TokenType::Lt => {
2076 self.advance();
2078 let mut depth = 1u32;
2079 while depth > 0 && !self.check(TokenType::Eof) {
2080 if self.check(TokenType::Lt) {
2081 depth += 1;
2082 } else if self.check(TokenType::Gt) {
2083 depth -= 1;
2084 }
2085 self.advance();
2086 }
2087 }
2088 _ => {
2089 self.advance();
2090 while self.check(TokenType::Dot) {
2091 self.advance();
2092 self.advance();
2093 }
2094 }
2095 }
2096 }
2097
2098 fn skip_braced_block(&mut self) -> Result<(), ParseError> {
2100 self.consume(TokenType::LBrace)?;
2101 let mut depth = 1u32;
2102 while depth > 0 {
2103 if self.check(TokenType::Eof) {
2104 let tok = self.current();
2105 return Err(ParseError {
2106 message: "Unterminated block — expected '}'".to_string(),
2107 line: tok.line,
2108 column: tok.column,
2109 ..Default::default()
2110 });
2111 }
2112 if self.check(TokenType::LBrace) {
2113 depth += 1;
2114 } else if self.check(TokenType::RBrace) {
2115 depth -= 1;
2116 }
2117 self.advance();
2118 }
2119 Ok(())
2120 }
2121
2122 fn at_declaration_start(&self) -> bool {
2123 is_declaration_keyword(&self.current().ttype) || self.check(TokenType::Eof)
2124 }
2125
2126 fn parse_declaration(&mut self) -> Result<Declaration, ParseError> {
2129 let tok = self.current().clone();
2130
2131 if tok.ttype == TokenType::Budget && self.peek_is_identifier() {
2139 return self.parse_top_level_budget().map(Declaration::Budget);
2140 }
2141
2142 match tok.ttype {
2143 TokenType::Import => self.parse_import().map(Declaration::Import),
2144 TokenType::Persona => self.parse_persona().map(Declaration::Persona),
2145 TokenType::Context => self.parse_context().map(Declaration::Context),
2146 TokenType::Anchor => self.parse_anchor().map(Declaration::Anchor),
2147 TokenType::Memory => self.parse_memory().map(Declaration::Memory),
2148 TokenType::Tool => self.parse_tool().map(Declaration::Tool),
2149 TokenType::Type => self.parse_type_def().map(Declaration::Type),
2150 TokenType::Flow => self.parse_flow().map(Declaration::Flow),
2151 TokenType::Effect => self.parse_effect().map(Declaration::Effect),
2154 TokenType::Intent => self.parse_intent().map(Declaration::Intent),
2155 TokenType::Run => self.parse_run().map(Declaration::Run),
2156 TokenType::Let => self.parse_let().map(Declaration::Let),
2157 TokenType::Know | TokenType::Believe | TokenType::Speculate | TokenType::Doubt => {
2158 self.parse_epistemic_block().map(Declaration::Epistemic)
2159 }
2160 TokenType::Lambda => self.parse_lambda_data().map(Declaration::LambdaData),
2161
2162 TokenType::Agent => self.parse_agent().map(Declaration::Agent),
2164 TokenType::Shield => self.parse_shield().map(Declaration::Shield),
2165 TokenType::Attest => self.parse_attest().map(Declaration::Attest),
2166 TokenType::Window => self.parse_window().map(Declaration::Window),
2168 TokenType::Pix => self.parse_pix().map(Declaration::Pix),
2169 TokenType::Ledger => self.parse_ledger().map(Declaration::Ledger),
2170 TokenType::Psyche => self.parse_psyche().map(Declaration::Psyche),
2171 TokenType::Corpus => self.parse_corpus().map(Declaration::Corpus),
2172 TokenType::Dataspace => self.parse_dataspace().map(Declaration::Dataspace),
2173 TokenType::Ots => self.parse_ots().map(Declaration::Ots),
2174 TokenType::Mandate => self.parse_mandate().map(Declaration::Mandate),
2175 TokenType::Compute => self.parse_compute().map(Declaration::Compute),
2176 TokenType::Daemon => self.parse_daemon().map(Declaration::Daemon),
2177 TokenType::Extension => self.parse_extension().map(Declaration::Extension),
2178 TokenType::AxonStore => self.parse_axonstore().map(Declaration::AxonStore),
2179 TokenType::AxonEndpoint => self.parse_axonendpoint().map(Declaration::AxonEndpoint),
2180
2181 TokenType::Resource => self.parse_resource().map(Declaration::Resource),
2183 TokenType::Fabric => self.parse_fabric().map(Declaration::Fabric),
2184 TokenType::Manifest => self.parse_manifest().map(Declaration::Manifest),
2185 TokenType::Observe => self.parse_observe().map(Declaration::Observe),
2186
2187 TokenType::Reconcile => self.parse_reconcile().map(Declaration::Reconcile),
2189 TokenType::Lease => self.parse_lease().map(Declaration::Lease),
2190 TokenType::Ensemble => self.parse_ensemble().map(Declaration::Ensemble),
2191
2192 TokenType::Session => self.parse_session_definition().map(Declaration::Session),
2194 TokenType::Topology => self.parse_topology().map(Declaration::Topology),
2195
2196 TokenType::Socket => self.parse_socket().map(Declaration::Socket),
2198
2199 TokenType::Upstream => self.parse_upstream().map(Declaration::Upstream),
2201
2202 TokenType::Voice => self.parse_voice().map(Declaration::Voice),
2204
2205 TokenType::Cors => self.parse_cors().map(Declaration::Cors),
2207
2208 TokenType::Cache => self.parse_cache().map(Declaration::Cache),
2210 TokenType::Document => self.parse_document().map(Declaration::Document),
2211
2212 TokenType::Deliver => self.parse_deliver().map(Declaration::Deliver),
2214 TokenType::Notify => self.parse_notify().map(Declaration::Notify),
2215
2216 TokenType::Savant => self.parse_savant().map(Declaration::Savant),
2218
2219 TokenType::Synth => self.parse_synth().map(Declaration::Synth),
2221
2222 TokenType::Scope => self.parse_scope().map(Declaration::Scope),
2224
2225 TokenType::Credential => self.parse_credential().map(Declaration::Credential),
2227
2228 TokenType::Observable => self.parse_observable().map(Declaration::Observable),
2230
2231 TokenType::Witness => self.parse_witness().map(Declaration::Witness),
2233
2234 TokenType::Immune => self.parse_immune().map(Declaration::Immune),
2236 TokenType::Reflex => self.parse_reflex().map(Declaration::Reflex),
2237 TokenType::Heal => self.parse_heal().map(Declaration::Heal),
2238
2239 TokenType::Component => self.parse_component().map(Declaration::Component),
2241 TokenType::View => self.parse_view().map(Declaration::View),
2242
2243 TokenType::Channel => self.parse_channel().map(Declaration::Channel),
2245
2246 TokenType::Ingest
2249 | TokenType::Persist
2250 | TokenType::Retrieve
2251 | TokenType::Mutate
2252 | TokenType::Purge
2253 | TokenType::Transact => self.parse_generic_declaration(),
2254
2255 TokenType::Mcp => self.parse_generic_declaration(),
2257
2258 _ => {
2259 let hint = crate::smart_suggest::suggest_for(
2263 &tok.value,
2264 crate::smart_suggest::TOP_LEVEL_KEYWORD_NAMES,
2265 );
2266 let base = format!(
2267 "Unexpected token at top level: '{}' — expected declaration \
2268 (persona, context, anchor, flow, run, ...)",
2269 tok.value
2270 );
2271 let message = if hint.is_empty() {
2272 base
2273 } else {
2274 format!("{base}. {hint}")
2275 };
2276 Err(ParseError {
2277 message,
2278 line: tok.line,
2279 column: tok.column,
2280 ..Default::default()
2281 })
2282 }
2283 }
2284 }
2285
2286 fn parse_import(&mut self) -> Result<ImportNode, ParseError> {
2289 let tok = self.consume(TokenType::Import)?;
2290 let loc = self.loc_of(&tok);
2291
2292 let mut path_parts = Vec::new();
2293
2294 if self.check(TokenType::At) {
2296 self.advance();
2297 let first = self.consume(TokenType::Identifier)?;
2298 path_parts.push(format!("@{}", first.value));
2299 } else {
2300 let first = self.consume(TokenType::Identifier)?;
2301 path_parts.push(first.value);
2302 }
2303
2304 while self.check(TokenType::Dot) {
2305 self.advance();
2306 if self.check(TokenType::LBrace) {
2307 break;
2308 }
2309 let part = self.consume(TokenType::Identifier)?;
2310 path_parts.push(part.value);
2311 }
2312
2313 let mut names = Vec::new();
2314 if self.check(TokenType::LBrace) {
2315 self.advance();
2316 names = self.parse_identifier_list()?;
2317 self.consume(TokenType::RBrace)?;
2318 }
2319
2320 let mut allow_downgrade = false;
2329 if self.check(TokenType::At) {
2330 let at_tok = self.current().clone();
2331 self.advance();
2332 let ident = self.consume(TokenType::Identifier)?;
2333 if ident.value == "allow_downgrade" {
2334 allow_downgrade = true;
2335 } else {
2336 return Err(ParseError {
2337 message: format!(
2338 "unknown import annotation '@{}' — the only import annotation is \
2339 `@allow_downgrade` (the epistemic-downgrade acknowledgment).",
2340 ident.value
2341 ),
2342 line: at_tok.line,
2343 column: at_tok.column,
2344 ..Default::default()
2345 });
2346 }
2347 }
2348
2349 let next_is_apx = self
2364 .tokens
2365 .get(self.pos + 1)
2366 .map(|t| t.value == "apx")
2367 .unwrap_or(false);
2368 if self.current().value == "with" && next_is_apx {
2369 let tok = self.current().clone();
2370 return Err(ParseError {
2371 message: "`import … with apx { … }` is RETRACTED (v2.67.0). The apx policy block was \
2372 parsed and silently DISCARDED — it never reached the IR, and no epistemic \
2373 dependency manager exists: no MEC/PCC verification, no EPR ranking, no \
2374 quarantine, no compliance gate. Declaring it verified nothing while \
2375 implying your supply chain was checked. Remove the `with apx { … }` \
2376 clause; the plain `import` resolves through the Epistemic Module \
2377 System."
2378 .to_string(),
2379 line: tok.line,
2380 column: tok.column,
2381 ..Default::default()
2382 });
2383 }
2384
2385 Ok(ImportNode {
2386 module_path: path_parts,
2387 names,
2388 allow_downgrade,
2389 loc,
2390 leading_trivia: Vec::new(),
2391 trailing_trivia: Vec::new(),
2392 })
2393 }
2394
2395 fn parse_persona(&mut self) -> Result<PersonaDefinition, ParseError> {
2398 let tok = self.consume(TokenType::Persona)?;
2399 let loc = self.loc_of(&tok);
2400 let name = self.consume(TokenType::Identifier)?.value;
2401 self.consume(TokenType::LBrace)?;
2402
2403 let mut node = PersonaDefinition {
2404 name,
2405 domain: Vec::new(),
2406 tone: String::new(),
2407 confidence_threshold: None,
2408 cite_sources: None,
2409 refuse_if: Vec::new(),
2410 language: String::new(),
2411 description: String::new(),
2412 loc,
2413 leading_trivia: Vec::new(),
2414 trailing_trivia: Vec::new(),
2415 };
2416
2417 while !self.check(TokenType::RBrace) {
2418 let field_name = self.current().value.clone();
2419 self.advance();
2420 self.consume(TokenType::Colon)?;
2421
2422 match field_name.as_str() {
2423 "domain" => node.domain = self.parse_string_list()?,
2424 "tone" => node.tone = self.consume_any_ident_or_kw()?.value,
2425 "confidence_threshold" => node.confidence_threshold = Some(self.consume_number()?),
2426 "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2427 "refuse_if" => node.refuse_if = self.parse_bracketed_identifiers()?,
2428 "language" => node.language = self.consume(TokenType::StringLit)?.value,
2429 "description" => node.description = self.consume(TokenType::StringLit)?.value,
2430 _ => self.skip_value(),
2431 }
2432 }
2433 self.consume(TokenType::RBrace)?;
2434 Ok(node)
2435 }
2436
2437 fn parse_context(&mut self) -> Result<ContextDefinition, ParseError> {
2440 let tok = self.consume(TokenType::Context)?;
2441 let loc = self.loc_of(&tok);
2442 let name = self.consume(TokenType::Identifier)?.value;
2443 self.consume(TokenType::LBrace)?;
2444
2445 let mut node = ContextDefinition {
2446 name,
2447 memory_scope: String::new(),
2448 language: String::new(),
2449 depth: String::new(),
2450 max_tokens: None,
2451 temperature: None,
2452 cite_sources: None,
2453 now_tz: None,
2454 loc,
2455 leading_trivia: Vec::new(),
2456 trailing_trivia: Vec::new(),
2457 };
2458
2459 while !self.check(TokenType::RBrace) {
2460 let field_name = self.current().value.clone();
2461 self.advance();
2462 self.consume(TokenType::Colon)?;
2463
2464 match field_name.as_str() {
2465 "memory" => node.memory_scope = self.consume_any_ident_or_kw()?.value,
2466 "language" => node.language = self.consume(TokenType::StringLit)?.value,
2467 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
2468 "now" => node.now_tz = Some(self.consume(TokenType::StringLit)?.value),
2470 "max_tokens" => {
2471 node.max_tokens = Some(
2472 self.consume(TokenType::Integer)?
2473 .value
2474 .parse::<i64>()
2475 .unwrap_or(0),
2476 )
2477 }
2478 "temperature" => node.temperature = Some(self.consume_number()?),
2479 "cite_sources" => node.cite_sources = Some(self.parse_bool()?),
2480 _ => self.skip_value(),
2481 }
2482 }
2483 self.consume(TokenType::RBrace)?;
2484 Ok(node)
2485 }
2486
2487 fn parse_anchor(&mut self) -> Result<AnchorConstraint, ParseError> {
2490 let tok = self.consume(TokenType::Anchor)?;
2491 let loc = self.loc_of(&tok);
2492 let name = self.consume(TokenType::Identifier)?.value;
2493 self.consume(TokenType::LBrace)?;
2494
2495 let mut node = AnchorConstraint {
2496 name,
2497 require: String::new(),
2498 reject: Vec::new(),
2499 enforce: String::new(),
2500 description: String::new(),
2501 confidence_floor: None,
2502 unknown_response: String::new(),
2503 on_violation: String::new(),
2504 on_violation_target: String::new(),
2505 loc,
2506 leading_trivia: Vec::new(),
2507 trailing_trivia: Vec::new(),
2508 };
2509
2510 while !self.check(TokenType::RBrace) {
2511 let field_name = self.current().value.clone();
2512 self.advance();
2513 self.consume(TokenType::Colon)?;
2514
2515 match field_name.as_str() {
2516 "require" => node.require = self.consume_any_ident_or_kw()?.value,
2517 "description" => node.description = self.consume(TokenType::StringLit)?.value,
2518 "reject" => node.reject = self.parse_bracketed_identifiers()?,
2519 "enforce" => node.enforce = self.consume_any_ident_or_kw()?.value,
2520 "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
2521 "unknown_response" => {
2522 node.unknown_response = self.consume(TokenType::StringLit)?.value
2523 }
2524 "on_violation" => {
2525 let action = self.consume_any_ident_or_kw()?.value;
2527 node.on_violation = action.clone();
2528 if action == "raise" || action == "fallback" {
2529 node.on_violation_target = self.consume_any_ident_or_kw()?.value;
2530 }
2531 }
2532 _ => self.skip_value(),
2533 }
2534 }
2535 self.consume(TokenType::RBrace)?;
2536 Ok(node)
2537 }
2538
2539 fn parse_memory(&mut self) -> Result<MemoryDefinition, ParseError> {
2542 let tok = self.consume(TokenType::Memory)?;
2543 let loc = self.loc_of(&tok);
2544 let name = self.consume(TokenType::Identifier)?.value;
2545 self.consume(TokenType::LBrace)?;
2546
2547 let mut node = MemoryDefinition {
2548 name,
2549 store: String::new(),
2550 backend: String::new(),
2551 retrieval: String::new(),
2552 decay: String::new(),
2553 loc,
2554 leading_trivia: Vec::new(),
2555 trailing_trivia: Vec::new(),
2556 };
2557
2558 while !self.check(TokenType::RBrace) {
2559 let field_name = self.current().value.clone();
2560 self.advance();
2561 self.consume(TokenType::Colon)?;
2562
2563 match field_name.as_str() {
2564 "store" => node.store = self.consume_any_ident_or_kw()?.value,
2565 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
2566 "retrieval" => node.retrieval = self.consume_any_ident_or_kw()?.value,
2567 "decay" => {
2568 if self.check(TokenType::Duration) {
2569 node.decay = self.advance().value.clone();
2570 } else {
2571 node.decay = self.consume_any_ident_or_kw()?.value;
2572 }
2573 }
2574 _ => self.skip_value(),
2575 }
2576 }
2577 self.consume(TokenType::RBrace)?;
2578 Ok(node)
2579 }
2580
2581 fn parse_tool(&mut self) -> Result<ToolDefinition, ParseError> {
2584 let tok = self.consume(TokenType::Tool)?;
2585 let loc = self.loc_of(&tok);
2586 let name = self.consume(TokenType::Identifier)?.value;
2587 self.consume(TokenType::LBrace)?;
2588
2589 let mut node = ToolDefinition {
2590 shield_ref: String::new(),
2591 name,
2592 provider: String::new(),
2593 max_results: None,
2594 filter_expr: String::new(),
2595 timeout: String::new(),
2596 runtime: String::new(),
2597 resource_ref: String::new(),
2598 sandbox: None,
2599 effects: None,
2600 parameters: Vec::new(),
2601 output_type: None,
2602 requires: Vec::new(),
2603 secret: String::new(),
2604 secret_partition: String::new(),
2605 target: None,
2606 risk: None,
2607 argv: Vec::new(),
2608 cache: String::new(),
2609 scrape: None,
2610 loc,
2611 leading_trivia: Vec::new(),
2612 trailing_trivia: Vec::new(),
2613 };
2614
2615 let mut unknown_fields: Vec<(String, u32, u32)> = Vec::new();
2622
2623 while !self.check(TokenType::RBrace) {
2624 let field_tok = self.current().clone();
2625 let field_name = field_tok.value.clone();
2626 self.advance();
2627 self.consume(TokenType::Colon)?;
2628
2629 match field_name.as_str() {
2630 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
2631 "max_results" => {
2632 node.max_results = Some(
2633 self.consume(TokenType::Integer)?
2634 .value
2635 .parse::<i64>()
2636 .unwrap_or(0),
2637 )
2638 }
2639 "filter" => node.filter_expr = self.parse_filter_expression()?,
2640 "timeout" => node.timeout = self.consume(TokenType::Duration)?.value,
2641 "runtime" => node.runtime = self.consume_any_ident_or_kw()?.value,
2642 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
2646 "sandbox" => node.sandbox = Some(self.parse_bool()?),
2647 "effects" => node.effects = Some(self.parse_effect_row()?),
2648 "parameters" => node.parameters = self.parse_tool_param_schema()?,
2650 "output_type" => node.output_type = Some(self.parse_output_type_string()?),
2651 "requires" => {
2657 let bracket_tok = self.current().clone();
2658 let items = self.parse_bracketed_dot_identifiers()?;
2659 for slug in &items {
2660 if !is_valid_capability_slug(slug) {
2661 return Err(ParseError {
2662 message: format!(
2663 "Invalid capability slug '{slug}' in tool '{}' \
2664 `requires:`. Scope slugs must match \
2665 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — the same \
2666 grammar as `credential.grants`. Examples: \
2667 `w_organization_social`, `video.publish`.",
2668 node.name
2669 ),
2670 line: bracket_tok.line,
2671 column: bracket_tok.column,
2672 ..Default::default()
2673 });
2674 }
2675 }
2676 node.requires = items;
2677 }
2678 "secret" => node.secret = self.parse_dotted_identifier()?,
2682 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
2684 "secret_partition" => {
2690 node.secret_partition = self.consume_any_ident_or_kw()?.value
2691 }
2692 "target" => node.target = Some(self.consume_any_ident_or_kw()?.value),
2694 "risk" => node.risk = Some(self.consume_any_ident_or_kw()?.value),
2695 "argv" => node.argv = self.parse_bracketed_strings()?,
2699 "cache" => node.cache = self.consume_any_ident_or_kw()?.value,
2702 "scrape" => node.scrape = Some(self.parse_scrape_spec()?),
2705 _ => {
2706 unknown_fields.push((field_name, field_tok.line, field_tok.column));
2707 self.skip_value();
2708 }
2709 }
2710 }
2711 self.consume(TokenType::RBrace)?;
2712
2713 if node.target.is_some() || node.scrape.is_some() {
2721 if let Some((field_name, line, column)) = unknown_fields.into_iter().next() {
2722 let (surface, valid) = if node.target.is_some() {
2723 (
2724 "technician tool",
2725 "provider, parameters, output_type, timeout, effects, target, risk, argv",
2726 )
2727 } else {
2728 (
2729 "web-acquisition tool",
2730 "provider, parameters, output_type, timeout, effects, secret, \
2731 secret_partition, cache, scrape",
2732 )
2733 };
2734 return Err(ParseError {
2735 message: format!(
2736 "unknown field `{field_name}` in {surface} `{}` — this tool uses \
2737 strict field checking; valid fields: {valid}",
2738 node.name
2739 ),
2740 line,
2741 column,
2742 ..Default::default()
2743 });
2744 }
2745 }
2746 Ok(node)
2747 }
2748
2749 fn parse_scrape_spec(&mut self) -> Result<crate::ast::ScrapeSpec, ParseError> {
2754 let open = self.consume(TokenType::LBrace)?;
2755 let loc = self.loc_of(&open);
2756 let mut spec = crate::ast::ScrapeSpec {
2757 loc,
2758 ..Default::default()
2759 };
2760 while !self.check(TokenType::RBrace) {
2761 let field_tok = self.current().clone();
2762 let field_name = field_tok.value.clone();
2763 self.advance();
2764 self.consume(TokenType::Colon)?;
2765 match field_name.as_str() {
2766 "engine" => spec.engine = Some(self.consume_any_ident_or_kw()?.value),
2767 "impersonate" => spec.impersonate = Some(self.consume_any_ident_or_kw()?.value),
2768 "render_wait" => spec.render_wait = Some(self.consume(TokenType::Duration)?.value),
2769 "proxy" => spec.proxy = self.parse_dotted_identifier()?,
2770 "respect_robots" => spec.respect_robots = Some(self.parse_bool()?),
2771 "extract" => spec.extract = self.parse_bracketed_strings()?,
2772 "adaptive" => spec.adaptive = Some(self.parse_bool()?),
2773 "similarity_floor" => spec.similarity_floor = self.parse_optional_float(),
2774 "follow" => spec.follow = self.consume(TokenType::StringLit)?.value,
2775 "max_depth" => {
2776 spec.max_depth =
2777 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2778 }
2779 "max_pages" => {
2780 spec.max_pages =
2781 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2782 }
2783 "concurrency" => {
2784 spec.concurrency =
2785 Some(self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0))
2786 }
2787 "politeness" => spec.politeness = self.consume_any_ident_or_kw()?.value,
2788 "checkpoint" => spec.checkpoint = self.consume_any_ident_or_kw()?.value,
2789 other => {
2790 return Err(self.error(&format!(
2791 "unknown scrape field `{other}` — the `scrape: {{ … }}` block is a \
2792 closed catalog; valid fields: engine, impersonate, \
2793 render_wait, proxy, respect_robots, extract, adaptive, \
2794 similarity_floor, follow, max_depth, max_pages, concurrency, \
2795 politeness, checkpoint"
2796 )));
2797 }
2798 }
2799 }
2800 self.consume(TokenType::RBrace)?;
2801 Ok(spec)
2802 }
2803
2804 fn parse_tool_param_schema(&mut self) -> Result<Vec<Parameter>, ParseError> {
2810 self.consume(TokenType::LBrace)?;
2811 let mut params = Vec::new();
2812 while !self.check(TokenType::RBrace) {
2813 let name = self.consume_any_ident_or_kw()?;
2817 let ploc = self.loc_of(&name);
2818 self.consume(TokenType::Colon)?;
2819 let type_expr = self.parse_type_expr()?;
2820 params.push(Parameter {
2821 name: name.value,
2822 type_expr,
2823 loc: ploc,
2824 });
2825 if self.check(TokenType::Comma) {
2826 self.advance();
2827 } else {
2828 break;
2829 }
2830 }
2831 self.consume(TokenType::RBrace)?;
2832 Ok(params)
2833 }
2834
2835 fn parse_filter_expression(&mut self) -> Result<String, ParseError> {
2836 let name = self.consume_any_ident_or_kw()?.value;
2837 if self.check(TokenType::LParen) {
2838 self.advance();
2839 let mut parts = vec![name, "(".to_string()];
2840 while !self.check(TokenType::RParen) {
2841 parts.push(self.advance().value.clone());
2842 }
2843 self.consume(TokenType::RParen)?;
2844 parts.push(")".to_string());
2845 Ok(parts.join(""))
2846 } else {
2847 Ok(name)
2848 }
2849 }
2850
2851 fn parse_effect_row(&mut self) -> Result<EffectRow, ParseError> {
2852 let tok = self.consume(TokenType::Lt)?;
2853 let loc = self.loc_of(&tok);
2854 let mut effects = Vec::new();
2855 let mut epistemic_level = String::new();
2856
2857 while !self.check(TokenType::Gt) {
2858 let name = self.consume_any_ident_or_kw()?.value;
2859 if self.check(TokenType::Colon) {
2860 self.advance();
2861 let level = self.parse_qualifier_value()?;
2878 if name == "epistemic" {
2879 epistemic_level = level;
2880 } else {
2881 effects.push(format!("{name}:{level}"));
2882 }
2883 } else {
2884 effects.push(name);
2885 }
2886 if self.check(TokenType::Comma) {
2887 self.advance();
2888 }
2889 }
2890 self.consume(TokenType::Gt)?;
2891
2892 Ok(EffectRow {
2893 effects,
2894 epistemic_level,
2895 loc,
2896 })
2897 }
2898
2899 fn parse_qualifier_value(&mut self) -> Result<String, ParseError> {
2908 let mut buf = self.consume_dotted_slug_segment()?;
2909 loop {
2910 let sep = if self.check(TokenType::Dot) {
2911 '.'
2912 } else if self.check(TokenType::Colon) {
2913 ':'
2914 } else {
2915 break;
2916 };
2917 self.advance();
2918 let part = self.consume_dotted_slug_segment()?;
2919 buf.push(sep);
2920 buf.push_str(&part);
2921 }
2922 Ok(buf)
2923 }
2924
2925 fn consume_dotted_slug_segment(&mut self) -> Result<String, ParseError> {
2937 let first = self.consume_any_ident_or_kw()?;
2938 let mut buf = first.value.clone();
2939 let mut next_line = first.line;
2940 let mut next_col = first.column + first.value.chars().count() as u32;
2941 loop {
2942 let cur = self.current();
2943 let is_segment_token = matches!(cur.ttype, TokenType::Identifier | TokenType::Integer,);
2944 if !is_segment_token {
2945 break;
2946 }
2947 if cur.line != next_line || cur.column != next_col {
2948 break;
2949 }
2950 buf.push_str(&cur.value);
2951 next_col = cur.column + cur.value.chars().count() as u32;
2952 next_line = cur.line;
2953 self.pos += 1;
2954 }
2955 Ok(buf)
2956 }
2957
2958 fn parse_type_def(&mut self) -> Result<TypeDefinition, ParseError> {
2961 let tok = self.consume(TokenType::Type)?;
2962 let loc = self.loc_of(&tok);
2963 let name = self.consume(TokenType::Identifier)?.value;
2964
2965 let mut node = TypeDefinition {
2966 identifier: String::new(),
2967 name,
2968 fields: Vec::new(),
2969 range_constraint: None,
2970 where_clause: None,
2971 compliance: Vec::new(),
2972 loc: loc.clone(),
2973 leading_trivia: Vec::new(),
2974 trailing_trivia: Vec::new(),
2975 };
2976
2977 if self.check(TokenType::LParen) {
2979 self.advance();
2980 let min_val = self.consume_number()?;
2981 self.consume(TokenType::DotDot)?;
2982 let max_val = self.consume_number()?;
2983 self.consume(TokenType::RParen)?;
2984 node.range_constraint = Some(RangeConstraint {
2985 min_value: min_val,
2986 max_value: max_val,
2987 loc: loc.clone(),
2988 });
2989 }
2990
2991 if self.check(TokenType::Where) {
2993 self.advance();
2994 let mut expr_parts = Vec::new();
2995 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
2996 if self.check(TokenType::Eof) {
2997 break;
2998 }
2999 expr_parts.push(self.advance().value.clone());
3000 }
3001 node.where_clause = Some(WhereClause {
3002 expression: expr_parts.join(" "),
3003 loc: loc.clone(),
3004 });
3005 }
3006
3007 if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3010 self.advance();
3011 node.compliance = self.parse_bracketed_identifiers()?;
3012 }
3013
3014 if self.check(TokenType::Identifier) && self.current().value == "identifier" {
3022 self.advance();
3023 node.identifier = self.consume_any_ident_or_kw()?.value.clone();
3024 if self.check(TokenType::Identifier) && self.current().value == "compliance" {
3025 self.advance();
3026 node.compliance = self.parse_bracketed_identifiers()?;
3027 }
3028 }
3029
3030 if self.check(TokenType::LBrace) {
3032 self.advance();
3033 while !self.check(TokenType::RBrace) {
3034 let field_name = self.consume(TokenType::Identifier)?;
3035 let field_loc = self.loc_of(&field_name);
3036 self.consume(TokenType::Colon)?;
3037 let type_expr = self.parse_type_expr()?;
3038 node.fields.push(TypeField {
3039 name: field_name.value,
3040 type_expr,
3041 loc: field_loc,
3042 });
3043 if self.check(TokenType::Comma) {
3044 self.advance();
3045 }
3046 }
3047 self.consume(TokenType::RBrace)?;
3048 }
3049
3050 Ok(node)
3051 }
3052
3053 fn parse_type_expr(&mut self) -> Result<TypeExpr, ParseError> {
3054 if self.check(TokenType::LBracket) {
3060 let open = self.current().clone();
3061 self.advance();
3062 let inner = self.parse_type_expr()?;
3063 self.consume(TokenType::RBracket)?;
3064 let mut optional = false;
3065 if self.check(TokenType::Question) {
3066 self.advance();
3067 optional = true;
3068 }
3069 return Ok(TypeExpr {
3070 name: "List".to_string(),
3071 generic_param: if inner.generic_param.is_empty() {
3072 inner.name
3073 } else {
3074 format!("{}<{}>", inner.name, inner.generic_param)
3075 },
3076 optional,
3077 loc: self.loc_of(&open),
3078 });
3079 }
3080 let name_tok = self.consume(TokenType::Identifier)?;
3081 let loc = self.loc_of(&name_tok);
3082 let mut generic_param = String::new();
3083 let mut optional = false;
3084
3085 if self.check(TokenType::Lt) {
3086 self.advance();
3087 let inner = self.parse_type_expr()?;
3097 generic_param = if inner.generic_param.is_empty() {
3098 inner.name
3099 } else {
3100 format!("{}<{}>", inner.name, inner.generic_param)
3101 };
3102 self.consume(TokenType::Gt)?;
3103 }
3104 if self.check(TokenType::LBracket) {
3108 self.advance();
3109 if matches!(self.current().ttype, TokenType::Integer | TokenType::Float) {
3110 generic_param = self.advance().value.clone();
3111 } else {
3112 let inner = self.parse_type_expr()?;
3113 generic_param = if inner.generic_param.is_empty() {
3114 inner.name
3115 } else {
3116 format!("{}[{}]", inner.name, inner.generic_param)
3117 };
3118 }
3119 self.consume(TokenType::RBracket)?;
3120 }
3121 if self.check(TokenType::Question) {
3122 self.advance();
3123 optional = true;
3124 }
3125
3126 Ok(TypeExpr {
3127 name: name_tok.value,
3128 generic_param,
3129 optional,
3130 loc,
3131 })
3132 }
3133
3134 fn parse_output_type_string(&mut self) -> Result<String, ParseError> {
3159 let expr = self.parse_type_expr()?;
3160 let mut s = expr.name;
3161 if !expr.generic_param.is_empty() {
3162 s.push('<');
3163 s.push_str(&expr.generic_param);
3164 s.push('>');
3165 }
3166 if expr.optional {
3167 s.push('?');
3168 }
3169 Ok(s)
3170 }
3171
3172 fn parse_flow(&mut self) -> Result<FlowDefinition, ParseError> {
3175 let tok = self.consume(TokenType::Flow)?;
3176 let loc = self.loc_of(&tok);
3177 let name = self.consume(TokenType::Identifier)?.value;
3178
3179 self.consume(TokenType::LParen)?;
3180 let mut parameters = Vec::new();
3181 if !self.check(TokenType::RParen) {
3182 parameters = self.parse_param_list()?;
3183 }
3184 self.consume(TokenType::RParen)?;
3185
3186 let mut return_type = None;
3187 if self.check(TokenType::Arrow) {
3188 self.advance();
3189 return_type = Some(self.parse_type_expr()?);
3190 }
3191
3192 self.consume(TokenType::LBrace)?;
3193 let mut body = Vec::new();
3194 while !self.check(TokenType::RBrace) {
3195 body.push(self.parse_flow_step()?);
3196 }
3197 self.consume(TokenType::RBrace)?;
3198
3199 Ok(FlowDefinition {
3200 name,
3201 parameters,
3202 return_type,
3203 body,
3204 loc,
3205 leading_trivia: Vec::new(),
3206 trailing_trivia: Vec::new(),
3207 })
3208 }
3209
3210 fn parse_effect(&mut self) -> Result<EffectDefinition, ParseError> {
3223 let tok = self.consume(TokenType::Effect)?;
3224 let loc = self.loc_of(&tok);
3225 let name = self.consume_any_ident_or_kw()?.value;
3226 self.consume(TokenType::LBrace)?;
3227
3228 let mut operations: Vec<EffectOperation> = Vec::new();
3229 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3230 let op_tok = self.current().clone();
3231 let op_name = self.consume_any_ident_or_kw()?.value;
3232
3233 self.consume(TokenType::LParen)?;
3234 let parameters = if self.check(TokenType::RParen) {
3235 Vec::new()
3236 } else {
3237 self.parse_param_list()?
3238 };
3239 self.consume(TokenType::RParen)?;
3240
3241 let mut return_type = String::new();
3244 if self.check(TokenType::Arrow) {
3245 self.advance();
3246 return_type = self.parse_type_expr()?.name;
3247 }
3248
3249 if let Some(prior) = operations.iter().find(|o| o.name == op_name) {
3254 return Err(ParseError {
3255 message: format!(
3256 "effect `{name}` declares operation `{op_name}` twice (first at \
3257 line {}); handler dispatch is by operation NAME, so a second \
3258 declaration would silently shadow the first",
3259 prior.loc.line
3260 ),
3261 line: op_tok.line,
3262 column: op_tok.column,
3263 ..Default::default()
3264 });
3265 }
3266
3267 operations.push(EffectOperation {
3268 name: op_name,
3269 parameters,
3270 return_type,
3271 loc: self.loc_of(&op_tok),
3272 });
3273 }
3274 self.consume(TokenType::RBrace)?;
3275
3276 Ok(EffectDefinition {
3277 name,
3278 operations,
3279 loc,
3280 leading_trivia: Vec::new(),
3281 trailing_trivia: Vec::new(),
3282 })
3283 }
3284
3285 fn parse_handle_block(&mut self) -> Result<HandleBlock, ParseError> {
3295 let tok = self.consume(TokenType::Handle)?;
3296 let loc = self.loc_of(&tok);
3297
3298 let mut effect_names = vec![self.consume_any_ident_or_kw()?.value];
3300 while self.check(TokenType::Comma) {
3301 self.advance();
3302 effect_names.push(self.consume_any_ident_or_kw()?.value);
3303 }
3304
3305 self.consume(TokenType::LBrace)?;
3306 let mut clauses: Vec<HandlerClause> = Vec::new();
3307 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3308 let clause_tok = self.current().clone();
3309 let operation_name = self.consume_any_ident_or_kw()?.value;
3310
3311 self.consume(TokenType::LParen)?;
3315 let mut parameter_names = Vec::new();
3316 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3317 parameter_names.push(self.consume_any_ident_or_kw()?.value);
3318 if self.check(TokenType::Comma) {
3319 self.advance();
3320 }
3321 }
3322 self.consume(TokenType::RParen)?;
3323 self.consume(TokenType::Arrow)?;
3324 self.consume(TokenType::LBrace)?;
3325
3326 let mut body = Vec::new();
3327 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3328 body.push(self.parse_flow_step()?);
3329 }
3330 self.consume(TokenType::RBrace)?;
3331
3332 if let Some(prior) = clauses.iter().find(|c| c.operation_name == operation_name) {
3333 return Err(ParseError {
3334 message: format!(
3335 "handler declares clause `{operation_name}` twice (first at line {}); \
3336 dispatch finds a clause by operation NAME and would always run the \
3337 first, leaving the second dead",
3338 prior.loc.line
3339 ),
3340 line: clause_tok.line,
3341 column: clause_tok.column,
3342 ..Default::default()
3343 });
3344 }
3345
3346 clauses.push(HandlerClause {
3347 operation_name,
3348 parameter_names,
3349 body,
3350 loc: self.loc_of(&clause_tok),
3351 });
3352 }
3353 self.consume(TokenType::RBrace)?;
3354
3355 let in_tok = self.current().clone();
3360 if !self.check(TokenType::In) {
3361 return Err(ParseError {
3362 message: format!(
3363 "`handle {}` must be followed by `in {{ … }}` — a handler scope is \
3364 DELIMITED (the design plan D3). Without the `in` block the frame has no \
3365 extent, so no `perform` could ever reach these clauses (got '{}')",
3366 effect_names.join(", "),
3367 in_tok.value
3368 ),
3369 line: in_tok.line,
3370 column: in_tok.column,
3371 ..Default::default()
3372 });
3373 }
3374 self.advance();
3375 self.consume(TokenType::LBrace)?;
3376 let mut body = Vec::new();
3377 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
3378 body.push(self.parse_flow_step()?);
3379 }
3380 self.consume(TokenType::RBrace)?;
3381
3382 Ok(HandleBlock {
3383 effect_names,
3384 clauses,
3385 body,
3386 loc,
3387 })
3388 }
3389
3390 fn parse_effect_op_ref(
3399 &mut self,
3400 ) -> Result<(Option<String>, String, Vec<String>), ParseError> {
3401 let first = self.consume_any_ident_or_kw()?.value;
3402 let (effect_name, operation_name) = if self.check(TokenType::Dot) {
3403 self.advance();
3404 (Some(first), self.consume_any_ident_or_kw()?.value)
3405 } else {
3406 (None, first)
3407 };
3408
3409 self.consume(TokenType::LParen)?;
3410 let mut arguments = Vec::new();
3411 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
3412 arguments.push(self.parse_subject()?);
3415 if self.check(TokenType::Comma) {
3416 self.advance();
3417 }
3418 }
3419 self.consume(TokenType::RParen)?;
3420 Ok((effect_name, operation_name, arguments))
3421 }
3422
3423 fn parse_perform_step(&mut self) -> Result<PerformStep, ParseError> {
3425 let tok = self.consume(TokenType::Perform)?;
3426 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3427 Ok(PerformStep {
3428 effect_name,
3429 operation_name,
3430 arguments,
3431 loc: self.loc_of(&tok),
3432 })
3433 }
3434
3435 fn parse_forward_step(&mut self) -> Result<ForwardStep, ParseError> {
3437 let tok = self.consume(TokenType::Forward)?;
3438 let (effect_name, operation_name, arguments) = self.parse_effect_op_ref()?;
3439 Ok(ForwardStep {
3440 effect_name,
3441 operation_name,
3442 arguments,
3443 loc: self.loc_of(&tok),
3444 })
3445 }
3446
3447 fn parse_discharge_value(&mut self) -> Result<String, ParseError> {
3449 self.consume(TokenType::LParen)?;
3450 let value = if self.check(TokenType::RParen) {
3451 String::new()
3452 } else {
3453 self.parse_subject()?
3454 };
3455 self.consume(TokenType::RParen)?;
3456 Ok(value)
3457 }
3458
3459 fn parse_param_list(&mut self) -> Result<Vec<Parameter>, ParseError> {
3460 let mut params = Vec::new();
3461
3462 let name = self.consume(TokenType::Identifier)?;
3463 let ploc = self.loc_of(&name);
3464 self.consume(TokenType::Colon)?;
3465 let type_expr = self.parse_type_expr()?;
3466 params.push(Parameter {
3467 name: name.value,
3468 type_expr,
3469 loc: ploc,
3470 });
3471
3472 while self.check(TokenType::Comma) {
3473 self.advance();
3474 let name = self.consume(TokenType::Identifier)?;
3475 let ploc = self.loc_of(&name);
3476 self.consume(TokenType::Colon)?;
3477 let type_expr = self.parse_type_expr()?;
3478 params.push(Parameter {
3479 name: name.value,
3480 type_expr,
3481 loc: ploc,
3482 });
3483 }
3484 Ok(params)
3485 }
3486
3487 fn parse_flow_step(&mut self) -> Result<FlowStep, ParseError> {
3490 let tok = self.current().clone();
3491
3492 match tok.ttype {
3493 TokenType::Know | TokenType::Believe | TokenType::Speculate
3499 if self
3500 .tokens
3501 .get(self.pos + 1)
3502 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3503 {
3504 let block = self.parse_epistemic_block()?;
3505 self.hoisted.push(Declaration::Epistemic(block));
3506 self.parse_flow_step()
3507 }
3508 TokenType::Doubt
3509 if self
3510 .tokens
3511 .get(self.pos + 1)
3512 .is_some_and(|t| t.ttype == TokenType::LBrace) =>
3513 {
3514 let block = self.parse_epistemic_block()?;
3515 self.hoisted.push(Declaration::Epistemic(block));
3516 self.parse_flow_step()
3517 }
3518 TokenType::Step => self.parse_step().map(FlowStep::Step),
3519 TokenType::If => self.parse_if().map(FlowStep::If),
3520 TokenType::For => self.parse_for_in().map(FlowStep::ForIn),
3521 TokenType::Let => self.parse_let().map(FlowStep::Let),
3522 TokenType::Return => self.parse_return().map(FlowStep::Return),
3523 TokenType::Break => self.parse_break().map(FlowStep::Break),
3524 TokenType::Continue => self.parse_continue().map(FlowStep::Continue),
3525 TokenType::Lambda => self.parse_lambda_data_apply().map(FlowStep::LambdaDataApply),
3526
3527 TokenType::Probe => self.parse_flow_step_simple("probe").map(|l| FlowStep::Probe(ProbeStep { target: l.1, fields: Vec::new(), loc: l.0 })),
3529 TokenType::Reason => self.parse_reason_step().map(FlowStep::Reason),
3533 TokenType::Validate => self.parse_flow_step_simple("validate").map(|l| FlowStep::Validate(ValidateStep { target: l.1, rule: String::new(), guard: None, loc: l.0 })),
3534 TokenType::Refine => self.parse_flow_step_simple("refine").map(|l| FlowStep::Refine(RefineStep { target: l.1, strategy: String::new(), loc: l.0 })),
3535 TokenType::Weave => self.parse_weave_step(),
3536 TokenType::Use => self.parse_use_step(),
3537 TokenType::Remember => self.parse_remember_step(),
3538 TokenType::Recall => self.parse_recall_step(),
3539 TokenType::Par => self.parse_par_block().map(FlowStep::Par),
3540 TokenType::Hibernate => self.parse_hibernate_step(),
3541 TokenType::Deliberate => self.parse_block_step("deliberate").map(|l| FlowStep::Deliberate(DeliberateBlock { loc: l })),
3542 TokenType::Consensus => self.parse_block_step("consensus").map(|l| FlowStep::Consensus(ConsensusBlock { loc: l })),
3543 TokenType::Forge => self.parse_forge_step().map(FlowStep::Forge),
3544 TokenType::Focus => self.parse_focus_step(),
3545 TokenType::Grad => self.parse_grad_step(),
3546 TokenType::Associate => self.parse_associate_step(),
3547 TokenType::Aggregate => self.parse_aggregate_step(),
3548 TokenType::Explore => self.parse_explore_step(),
3549 TokenType::Ingest => self.parse_ingest_step(),
3550 TokenType::Declassify => self.parse_declassify_step().map(FlowStep::Declassify),
3551 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 })),
3552 TokenType::Stream => self.parse_stream_block().map(FlowStep::Stream),
3558 TokenType::Handle => self.parse_handle_block().map(FlowStep::Handle),
3566 TokenType::Perform => self.parse_perform_step().map(FlowStep::Perform),
3567 TokenType::Resume => {
3568 let tok = self.consume(TokenType::Resume)?;
3569 let value_expr = self.parse_discharge_value()?;
3570 Ok(FlowStep::Resume(ResumeStep {
3571 value_expr,
3572 loc: self.loc_of(&tok),
3573 }))
3574 }
3575 TokenType::Abort => {
3576 let tok = self.consume(TokenType::Abort)?;
3577 let value_expr = self.parse_discharge_value()?;
3578 Ok(FlowStep::Abort(AbortStep {
3579 value_expr,
3580 loc: self.loc_of(&tok),
3581 }))
3582 }
3583 TokenType::Forward => self.parse_forward_step().map(FlowStep::Forward),
3584 TokenType::Navigate => self.parse_navigate_step(),
3585 TokenType::Drill => self.parse_drill_step(),
3586 TokenType::Trail => self.parse_flow_step_simple("trail").map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 })),
3587 TokenType::Corroborate => self.parse_corroborate_step(),
3588 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 })),
3589 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 })),
3590 TokenType::Compute => self.parse_compute_apply().map(FlowStep::ComputeApply),
3595 TokenType::Listen => self.parse_listen_step(),
3596 TokenType::Daemon => self.parse_flow_step_simple("daemon").map(|l| FlowStep::DaemonStep(DaemonStepNode { daemon_ref: l.1, loc: l.0 })),
3597 TokenType::Emit => self.parse_emit_step(),
3599 TokenType::Mint => self.parse_mint_step(),
3601 TokenType::Rotate => self.parse_rotate_step(),
3604 TokenType::Publish => self.parse_publish_step(),
3605 TokenType::Discover => self.parse_discover_step(),
3606 TokenType::Persist => self.parse_persist_step(),
3607 TokenType::Retrieve => self.parse_retrieve_step(),
3608 TokenType::Mutate => self.parse_mutate_step(),
3609 TokenType::Purge => self.parse_store_where_step().map(|(loc, store_name, where_expr)| FlowStep::Purge(PurgeStep { store_name, where_expr, loc })),
3610 TokenType::Transact => self.parse_block_step("transact").map(|l| FlowStep::Transact(TransactBlock { loc: l })),
3611 TokenType::Warden => self.parse_warden().map(FlowStep::Warden),
3613 TokenType::Quant => self.parse_quant().map(FlowStep::Quant),
3615 TokenType::Yield => self.parse_yield().map(FlowStep::Yield),
3617 TokenType::Run => self.parse_run().map(FlowStep::Run),
3621
3622 _ => {
3623 let hint = crate::smart_suggest::suggest_for(
3627 &tok.value,
3628 crate::smart_suggest::FLOW_BODY_KEYWORD_NAMES,
3629 );
3630 let base = format!(
3631 "Unexpected token in flow body: '{}' — expected step, if, for, let, return, ...",
3632 tok.value
3633 );
3634 let message = if hint.is_empty() {
3635 base
3636 } else {
3637 format!("{base}. {hint}")
3638 };
3639 Err(ParseError {
3640 message,
3641 line: tok.line,
3642 column: tok.column,
3643 ..Default::default()
3644 })
3645 }
3646 }
3647 }
3648
3649 fn parse_step(&mut self) -> Result<StepNode, ParseError> {
3652 let tok = self.consume(TokenType::Step)?;
3653 let loc = self.loc_of(&tok);
3654 let name = self.consume(TokenType::Identifier)?.value;
3655
3656 let mut persona_ref = String::new();
3657 if self.check(TokenType::Use) {
3658 self.advance();
3659 persona_ref = self.consume_any_ident_or_kw()?.value;
3660 }
3661
3662 self.consume(TokenType::LBrace)?;
3663
3664 let mut node = StepNode {
3665 name,
3666 persona_ref,
3667 given: String::new(),
3668 ask: String::new(),
3669 output_type: String::new(),
3670 confidence_floor: None,
3671 navigate_ref: String::new(),
3672 apply_ref: String::new(),
3673 requires_context: None,
3674 now_tz: None,
3675 guards: Vec::new(),
3676 pix_ops: Vec::new(),
3677 stream: None,
3678 performs: Vec::new(),
3679 loc,
3680 };
3681
3682 self.parse_step_body_into(&mut node)?;
3683 self.consume(TokenType::RBrace)?;
3684 Ok(node)
3685 }
3686
3687 fn parse_step_body_into(&mut self, node: &mut StepNode) -> Result<(), ParseError> {
3700 while !self.check(TokenType::RBrace) {
3701 let inner = self.current().clone();
3702
3703 match inner.ttype {
3704 TokenType::Given => {
3705 self.advance();
3706 self.consume(TokenType::Colon)?;
3707 node.given = self.parse_expression_string()?;
3708 }
3709 TokenType::Ask => {
3710 self.advance();
3711 self.consume(TokenType::Colon)?;
3712 node.ask = self.consume(TokenType::StringLit)?.value;
3713 }
3714 TokenType::Output => {
3715 self.advance();
3725 self.consume(TokenType::Colon)?;
3726 node.output_type = self.parse_output_type_string()?;
3727 }
3728 TokenType::Navigate
3735 if self
3736 .tokens
3737 .get(self.pos + 1)
3738 .is_some_and(|t| t.ttype != TokenType::Colon) =>
3739 {
3740 let op = self.parse_navigate_step()?;
3741 node.pix_ops.push(op);
3742 }
3743 TokenType::Drill => {
3744 let op = self.parse_drill_step()?;
3745 node.pix_ops.push(op);
3746 }
3747 TokenType::Trail => {
3748 let op = self
3749 .parse_flow_step_simple("trail")
3750 .map(|l| FlowStep::Trail(TrailStep { navigate_ref: l.1, loc: l.0 }))?;
3751 node.pix_ops.push(op);
3752 }
3753 TokenType::Validate => {
3758 let tok = self.current().clone();
3759 self.advance();
3760 let target = self.parse_subject()?;
3762 let mut rule = String::new();
3763 if self.current().value == "against" {
3764 self.advance();
3765 self.consume(TokenType::Colon)?;
3766 rule = self.consume_any_ident_or_kw()?.value.clone();
3767 }
3768 node.pix_ops.push(FlowStep::Validate(ValidateStep {
3769 target,
3770 rule,
3771 guard: None,
3772 loc: Loc { line: tok.line, column: tok.column },
3773 }));
3774 }
3775 TokenType::If => {
3787 let tok = self.current().clone();
3788 self.advance();
3789
3790 let metric = self.consume_any_ident_or_kw()?;
3791 if metric.value != "confidence" {
3792 return Err(ParseError {
3793 message: format!(
3794 "step-body `if` is the confidence guard — `if confidence < \
3795 <threshold> -> refine(max_attempts: <n>)` — and `confidence` \
3796 is its only metric (the CSR the preceding `validate … \
3797 against:` computes). Got '{}'. General branching belongs at \
3798 flow level: `if <cond> {{ … }}`.",
3799 metric.value
3800 ),
3801 line: metric.line,
3802 column: metric.column,
3803 ..Default::default()
3804 });
3805 }
3806
3807 let op = self.current().clone();
3808 if op.ttype != TokenType::Lt {
3809 return Err(ParseError {
3810 message: format!(
3811 "a confidence guard declares a FLOOR: `if confidence < \
3812 <threshold>`. `<` is the only comparison — the guard fires on \
3813 DEFICIENCY, and an inverted form would refine the outputs \
3814 that already conform. Got '{}'.",
3815 op.value
3816 ),
3817 line: op.line,
3818 column: op.column,
3819 ..Default::default()
3820 });
3821 }
3822 self.advance();
3823 let threshold = self.consume_number()?;
3824
3825 self.consume(TokenType::Arrow)?;
3826
3827 if !self.check(TokenType::Refine) {
3828 let bad = self.current().clone();
3829 return Err(ParseError {
3830 message: format!(
3831 "the guard's action catalog is CLOSED and `refine` is its only \
3832 member — the recovery the runtime actually performs (re-derive \
3833 the validated value with the violations as feedback, then \
3834 re-score). Got '{}'. An action name outside the catalog would \
3835 advertise a recovery nothing dispatches.",
3836 bad.value
3837 ),
3838 line: bad.line,
3839 column: bad.column,
3840 ..Default::default()
3841 });
3842 }
3843 self.advance();
3844 self.consume(TokenType::LParen)?;
3845 let key = self.consume_any_ident_or_kw()?;
3846 if key.value != "max_attempts" {
3847 return Err(ParseError {
3848 message: format!(
3849 "`refine` takes exactly `max_attempts: <n>` — the bound that \
3850 makes the recovery loop TERMINATE by construction. Got '{}'.",
3851 key.value
3852 ),
3853 line: key.line,
3854 column: key.column,
3855 ..Default::default()
3856 });
3857 }
3858 self.consume(TokenType::Colon)?;
3859 let attempts_tok = self.current().clone();
3860 if attempts_tok.ttype != TokenType::Integer {
3861 return Err(ParseError {
3862 message: format!(
3863 "`max_attempts:` must be a positive integer literal (got '{}')",
3864 attempts_tok.value
3865 ),
3866 line: attempts_tok.line,
3867 column: attempts_tok.column,
3868 ..Default::default()
3869 });
3870 }
3871 let max_attempts = attempts_tok.value.parse::<u32>().map_err(|_| ParseError {
3872 message: format!("Invalid attempt count '{}'", attempts_tok.value),
3873 line: attempts_tok.line,
3874 column: attempts_tok.column,
3875 ..Default::default()
3876 })?;
3877 self.advance();
3878 self.consume(TokenType::RParen)?;
3879
3880 let attached = node.pix_ops.iter_mut().rev().find_map(|op| match op {
3887 FlowStep::Validate(v) if !v.rule.is_empty() => Some(v),
3888 _ => None,
3889 });
3890 match attached {
3891 Some(v) => {
3892 if v.guard.is_some() {
3893 return Err(ParseError {
3894 message: "this validation already carries a confidence \
3895 guard; a second one would race the first over \
3896 the same score. One validation, one floor, one \
3897 recovery."
3898 .to_string(),
3899 line: tok.line,
3900 column: tok.column,
3901 ..Default::default()
3902 });
3903 }
3904 v.guard = Some(ConfidenceGuard {
3905 threshold,
3906 max_attempts,
3907 loc: Loc { line: tok.line, column: tok.column },
3908 });
3909 }
3910 None => {
3911 return Err(ParseError {
3912 message: "`if confidence` reads the CSR of a preceding \
3913 `validate … against: <Schema>` in this step body, \
3914 and none exists. A `validate` without `against:` \
3915 computes no score (there is no schema to score \
3916 with), so it cannot carry a guard either."
3917 .to_string(),
3918 line: tok.line,
3919 column: tok.column,
3920 ..Default::default()
3921 });
3922 }
3923 }
3924 }
3925 TokenType::Navigate => {
3926 self.advance();
3927 self.consume(TokenType::Colon)?;
3928 node.navigate_ref = self.parse_dotted_identifier()?;
3929 }
3930 TokenType::Identifier if inner.value == "confidence_floor" => {
3931 self.advance();
3932 self.consume(TokenType::Colon)?;
3933 node.confidence_floor = Some(self.consume_number()?);
3934 }
3935 TokenType::Identifier if inner.value == "apply" => {
3936 self.advance();
3937 self.consume(TokenType::Colon)?;
3938 node.apply_ref = self.consume_any_ident_or_kw()?.value;
3939 }
3940 TokenType::Identifier if inner.value == "requires_context" => {
3948 self.advance();
3949 self.consume(TokenType::Colon)?;
3950 let num = self.current().clone();
3951 let bad = |tok: &crate::tokens::Token| ParseError {
3952 message: format!(
3953 "`requires_context:` must be a positive integer token count \
3954 (got '{}')",
3955 tok.value
3956 ),
3957 line: tok.line,
3958 column: tok.column,
3959 ..Default::default()
3960 };
3961 if num.ttype != TokenType::Integer {
3962 return Err(bad(&num));
3963 }
3964 let value = num.value.parse::<u32>().map_err(|_| bad(&num))?;
3965 self.advance();
3966 node.requires_context = Some(value);
3967 }
3968 TokenType::Identifier if inner.value == "now" => {
3973 self.advance();
3974 self.consume(TokenType::Colon)?;
3975 let tz = self.current().clone();
3976 if tz.ttype != TokenType::StringLit {
3977 return Err(ParseError {
3978 message: format!(
3979 "`now:` must be an IANA timezone string literal like \
3980 \"America/Bogota\" or \"UTC\" (got '{}')",
3981 tz.value
3982 ),
3983 line: tz.line,
3984 column: tz.column,
3985 ..Default::default()
3986 });
3987 }
3988 self.advance();
3989 node.now_tz = Some(tz.value);
3990 }
3991 TokenType::Use => {
4001 let tool = self
4002 .tokens
4003 .get(self.pos + 1)
4004 .map(|t| t.value.as_str())
4005 .filter(|v| !v.is_empty())
4006 .unwrap_or("<Tool>");
4007 return Err(ParseError {
4008 message: format!(
4009 "`use` is not valid inside a `step {{ }}` body — the tool dispatch \
4010 would be silently dropped. To invoke a tool, either write the \
4011 flow-level step `use {tool} on <arg>` (outside this block), or bind \
4012 it inside this step with `apply: {tool}`. To attach a persona, put \
4013 it in the step header: `step <name> use <Persona> {{ … }}`."
4014 ),
4015 line: inner.line,
4016 column: inner.column,
4017 ..Default::default()
4018 });
4019 }
4020 TokenType::Mandate => {
4031 let g = self.parse_step_guard("mandate")?;
4032 node.guards.push(g);
4033 }
4034 TokenType::Shield => {
4035 let g = self.parse_step_guard("shield")?;
4036 node.guards.push(g);
4037 }
4038 TokenType::Ots => {
4039 let g = self.parse_step_guard("ots")?;
4040 node.guards.push(g);
4041 }
4042 TokenType::Lambda => {
4049 let g = self.parse_step_guard("lambda")?;
4050 node.guards.push(g);
4051 }
4052 TokenType::Probe
4060 if self
4061 .tokens
4062 .get(self.pos + 1)
4063 .is_some_and(|t| t.ttype != TokenType::Colon) =>
4064 {
4065 let tok = self.current().clone();
4066 self.advance();
4067 let target = self.parse_subject()?;
4070 let mut fields = Vec::new();
4071 if self.check(TokenType::For) {
4072 self.advance();
4073 self.consume(TokenType::LBracket)?;
4074 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
4075 fields.push(self.consume_any_ident_or_kw()?.value.clone());
4076 if self.check(TokenType::Comma) {
4077 self.advance();
4078 }
4079 }
4080 self.consume(TokenType::RBracket)?;
4081 }
4082 node.pix_ops.push(FlowStep::Probe(ProbeStep {
4083 target,
4084 fields,
4085 loc: Loc { line: tok.line, column: tok.column },
4086 }));
4087 }
4088 TokenType::Identifier if inner.value == "use_tool" => {
4095 let tok = self.current().clone();
4096 self.advance();
4097 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
4098 let args = if self.current().value == "with" {
4099 self.advance();
4100 let mut named: Vec<(String, String, String)> = Vec::new();
4101 loop {
4102 let k = self.consume_any_ident_or_kw()?.value.clone();
4103 self.consume(TokenType::Colon)?;
4104 let kind = if self.check(TokenType::StringLit) {
4108 "literal"
4109 } else {
4110 "reference"
4111 };
4112 let v = self.parse_expression_string()?;
4113 named.push((k, v, kind.to_string()));
4114 if self.check(TokenType::Comma) {
4115 self.advance();
4116 } else {
4117 break;
4118 }
4119 }
4120 UseArgs::Named(named)
4121 } else if self.current().value == "on" {
4122 self.advance();
4123 UseArgs::LegacyPositional(
4124 self.consume_any_ident_or_kw()?.value.clone(),
4125 )
4126 } else {
4127 UseArgs::LegacyPositional(String::new())
4128 };
4129 node.pix_ops.push(FlowStep::UseTool(UseToolStep {
4130 tool_name,
4131 args,
4132 loc: Loc { line: tok.line, column: tok.column },
4133 }));
4134 }
4135 TokenType::Par => {
4137 let block = self.parse_par_block()?;
4138 node.pix_ops.push(FlowStep::Par(block));
4139 }
4140 TokenType::Reason
4162 if self
4163 .tokens
4164 .get(self.pos + 1)
4165 .is_some_and(|t| t.ttype == TokenType::Colon) =>
4166 {
4167 let tok = self.current().clone();
4168 self.advance();
4169 self.consume(TokenType::Colon)?;
4170 let mut r = ReasonStep {
4171 strategy: String::new(),
4172 target: String::new(),
4173 given: String::new(),
4174 ask: String::new(),
4175 depth: None,
4176 loc: self.loc_of(&tok),
4177 };
4178 if self.check(TokenType::StringLit) {
4179 r.ask = self.consume(TokenType::StringLit)?.value;
4180 } else {
4181 r.target = self.parse_dotted_identifier()?;
4182 }
4183 node.pix_ops.push(FlowStep::Reason(r));
4184 }
4185 TokenType::Reason => {
4186 let r = self.parse_reason_step()?;
4187 node.pix_ops.push(FlowStep::Reason(r));
4188 }
4189 TokenType::Weave => {
4198 let w = self.parse_weave_step()?;
4199 node.pix_ops.push(w);
4200 }
4201 TokenType::Identifier
4211 if self
4212 .tokens
4213 .get(self.pos + 1)
4214 .is_some_and(|t| t.ttype == TokenType::LParen) =>
4215 {
4216 let tok = self.current().clone();
4217 let agent_name = self.consume_any_ident_or_kw()?.value.clone();
4218 self.consume(TokenType::LParen)?;
4219 let mut arguments = Vec::new();
4220 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4221 arguments.push(self.parse_subject()?);
4222 if self.check(TokenType::Comma) {
4223 self.advance();
4224 }
4225 }
4226 self.consume(TokenType::RParen)?;
4227 node.pix_ops.push(FlowStep::AgentCall(AgentCallStep {
4228 agent_name,
4229 arguments,
4230 loc: self.loc_of(&tok),
4231 }));
4232 }
4233 TokenType::Retrieve => {
4243 let r = self.parse_retrieve_step()?;
4244 node.pix_ops.push(r);
4245 }
4246 TokenType::Stream => {
4264 let sb = self.parse_stream_block()?;
4265 if node.stream.is_some() {
4266 return Err(ParseError {
4267 message:
4268 "step declares two `stream` blocks; a step has one output stream, \
4269 and composing two has no defined meaning (which one is the \
4270 step's output?). Refused rather than silently keeping the last."
4271 .to_string(),
4272 line: inner.line,
4273 column: inner.column,
4274 ..Default::default()
4275 });
4276 }
4277 node.stream = Some(Box::new(sb));
4278 }
4279 TokenType::Perform => {
4296 let p = self.parse_perform_step()?;
4297 node.performs.push(p);
4298 }
4299 TokenType::Probe => {
4303 self.skip_flow_step_structural()?;
4304 }
4305 _ => {
4306 return Err(ParseError {
4307 message: format!(
4308 "Unexpected token in step body: '{}' — expected given, ask, \
4309 probe, reason, weave, stream, perform, output, confidence_floor, \
4310 navigate, apply, requires_context, now",
4311 inner.value
4312 ),
4313 line: inner.line,
4314 column: inner.column,
4315 ..Default::default()
4316 });
4317 }
4318 }
4319 }
4320 Ok(())
4321 }
4322
4323 fn skip_flow_step_structural(&mut self) -> Result<(), ParseError> {
4325 self.advance();
4327 while !self.check(TokenType::LBrace)
4329 && !self.check(TokenType::RBrace)
4330 && !self.check(TokenType::Eof)
4331 {
4332 let tt = &self.current().ttype;
4334 if matches!(
4335 tt,
4336 TokenType::Step
4337 | TokenType::Given
4338 | TokenType::Ask
4339 | TokenType::Output
4340 | TokenType::Navigate
4341 | TokenType::Use
4342 | TokenType::Probe
4343 | TokenType::Reason
4344 | TokenType::Weave
4345 | TokenType::Stream
4346 | TokenType::If
4347 | TokenType::For
4348 | TokenType::Let
4349 | TokenType::Return
4350 ) {
4351 return Ok(());
4352 }
4353 self.advance();
4354 }
4355 if self.check(TokenType::LBrace) {
4357 self.skip_braced_block()?;
4358 }
4359 Ok(())
4360 }
4361
4362 fn parse_intent(&mut self) -> Result<IntentNode, ParseError> {
4365 let tok = self.consume(TokenType::Intent)?;
4366 let loc = self.loc_of(&tok);
4367 let name = self.consume(TokenType::Identifier)?.value;
4368 self.consume(TokenType::LBrace)?;
4369
4370 let mut node = IntentNode {
4371 name,
4372 given: String::new(),
4373 ask: String::new(),
4374 output_type: None,
4375 confidence_floor: None,
4376 loc,
4377 leading_trivia: Vec::new(),
4378 trailing_trivia: Vec::new(),
4379 };
4380
4381 while !self.check(TokenType::RBrace) {
4382 let field_name = self.current().value.clone();
4383 self.advance();
4384 self.consume(TokenType::Colon)?;
4385
4386 match field_name.as_str() {
4387 "given" => node.given = self.consume(TokenType::Identifier)?.value,
4388 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
4389 "output" => node.output_type = Some(self.parse_type_expr()?),
4390 "confidence_floor" => node.confidence_floor = Some(self.consume_number()?),
4391 _ => self.skip_value(),
4392 }
4393 }
4394 self.consume(TokenType::RBrace)?;
4395 Ok(node)
4396 }
4397
4398 fn parse_run(&mut self) -> Result<RunStatement, ParseError> {
4401 let tok = self.consume(TokenType::Run)?;
4402 let loc = self.loc_of(&tok);
4403 let flow_name = self.consume(TokenType::Identifier)?.value;
4404
4405 self.consume(TokenType::LParen)?;
4406 let mut arguments = Vec::new();
4407 if !self.check(TokenType::RParen) {
4408 arguments = self.parse_argument_list()?;
4409 }
4410 self.consume(TokenType::RParen)?;
4411
4412 let mut node = RunStatement {
4413 flow_name,
4414 arguments,
4415 persona: String::new(),
4416 context: String::new(),
4417 anchors: Vec::new(),
4418 on_failure: String::new(),
4419 on_failure_params: Vec::new(),
4420 output_to: String::new(),
4421 effort: String::new(),
4422 loc,
4423 leading_trivia: Vec::new(),
4424 trailing_trivia: Vec::new(),
4425 };
4426
4427 while self.check_run_modifier() {
4428 let mod_tok = self.current().clone();
4429 if mod_tok.value == "with" && mod_tok.ttype != TokenType::As {
4431 self.advance();
4432 node.persona = self.consume(TokenType::Identifier)?.value;
4433 continue;
4434 }
4435 match mod_tok.ttype {
4436 TokenType::As => {
4437 self.advance();
4438 node.persona = self.consume(TokenType::Identifier)?.value;
4439 }
4440 TokenType::Within => {
4441 self.advance();
4442 node.context = self.consume(TokenType::Identifier)?.value;
4443 }
4444 TokenType::ConstrainedBy => {
4445 self.advance();
4446 node.anchors = self.parse_bracketed_identifiers()?;
4447 }
4448 TokenType::OnFailure => {
4449 self.advance();
4450 self.consume(TokenType::Colon)?;
4451 node.on_failure = self.consume_any_ident_or_kw()?.value;
4452 if self.check(TokenType::LParen) {
4454 self.advance();
4455 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4456 let key = self.consume_any_ident_or_kw()?.value;
4457 self.consume(TokenType::Colon)?;
4458 let val = self.consume_any_ident_or_kw()?.value;
4459 node.on_failure_params.push((key, val));
4460 if self.check(TokenType::Comma) {
4461 self.advance();
4462 }
4463 }
4464 if self.check(TokenType::RParen) {
4465 self.advance();
4466 }
4467 }
4468 }
4469 TokenType::OutputTo => {
4470 self.advance();
4471 self.consume(TokenType::Colon)?;
4472 node.output_to = self.consume(TokenType::StringLit)?.value;
4473 }
4474 TokenType::Effort => {
4475 self.advance();
4476 self.consume(TokenType::Colon)?;
4477 node.effort = self.consume_any_ident_or_kw()?.value;
4478 }
4479 _ => break,
4480 }
4481 }
4482
4483 Ok(node)
4484 }
4485
4486 fn parse_epistemic_block(&mut self) -> Result<EpistemicBlock, ParseError> {
4489 let tok = self.current().clone();
4490 let mode = match tok.ttype {
4491 TokenType::Know => "know",
4492 TokenType::Believe => "believe",
4493 TokenType::Speculate => "speculate",
4494 TokenType::Doubt => "doubt",
4495 _ => unreachable!(),
4496 };
4497 self.advance();
4498 let loc = self.loc_of(&tok);
4499
4500 self.consume(TokenType::LBrace)?;
4501 let mut body = Vec::new();
4502 while !self.check(TokenType::RBrace) {
4503 body.push(self.parse_declaration()?);
4504 }
4505 self.consume(TokenType::RBrace)?;
4506
4507 Ok(EpistemicBlock {
4508 mode: mode.to_string(),
4509 body,
4510 loc,
4511 leading_trivia: Vec::new(),
4512 trailing_trivia: Vec::new(),
4513 })
4514 }
4515
4516 fn parse_expr(&mut self) -> Result<Expr, ParseError> {
4524 self.parse_expr_bp(0)
4525 }
4526
4527 fn parse_expr_bp(&mut self, min_bp: u8) -> Result<Expr, ParseError> {
4528 let mut lhs = match self.current().ttype {
4531 TokenType::Minus => {
4532 self.advance();
4533 Expr::Unary(UnOp::Neg, Box::new(self.parse_expr_bp(6)?))
4534 }
4535 TokenType::Not => {
4536 self.advance();
4537 Expr::Unary(UnOp::Not, Box::new(self.parse_expr_bp(6)?))
4538 }
4539 _ => self.parse_postfix()?,
4540 };
4541 while let Some((op, lbp)) = Self::binop_of(self.current().ttype.clone()) {
4543 if lbp < min_bp {
4544 break;
4545 }
4546 self.advance();
4547 let rhs = self.parse_expr_bp(lbp + 1)?;
4548 lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
4549 }
4550 Ok(lhs)
4551 }
4552
4553 fn binop_of(t: TokenType) -> Option<(BinOp, u8)> {
4556 Some(match t {
4557 TokenType::Or => (BinOp::Or, 1),
4558 TokenType::And => (BinOp::And, 2),
4559 TokenType::Eq => (BinOp::Eq, 3),
4560 TokenType::Neq => (BinOp::Ne, 3),
4561 TokenType::Lt => (BinOp::Lt, 3),
4562 TokenType::Lte => (BinOp::Le, 3),
4563 TokenType::Gt => (BinOp::Gt, 3),
4564 TokenType::Gte => (BinOp::Ge, 3),
4565 TokenType::Plus => (BinOp::Add, 4),
4566 TokenType::Minus => (BinOp::Sub, 4),
4567 TokenType::Star => (BinOp::Mul, 5),
4568 TokenType::Slash => (BinOp::Div, 5),
4569 TokenType::Percent => (BinOp::Mod, 5),
4570 _ => return None,
4571 })
4572 }
4573
4574 fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
4580 let mut expr = self.parse_expr_atom()?;
4581 loop {
4582 if self.check(TokenType::Dot) {
4583 self.advance();
4584 let name = self.consume_any_ident_or_kw()?.value;
4585 if let Some(builtin) = Builtin::from_name(&name) {
4586 let mut args = vec![expr];
4587 if self.check(TokenType::LParen) {
4588 self.advance();
4589 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
4590 args.push(self.parse_expr_bp(0)?);
4591 if self.check(TokenType::Comma) {
4592 self.advance();
4593 } else {
4594 break;
4595 }
4596 }
4597 self.consume(TokenType::RParen)?;
4598 }
4599 expr = Expr::Call(builtin, args);
4600 } else {
4601 expr = match expr {
4605 Expr::Ref(p) => Expr::Ref(format!("{p}.{name}")),
4606 other => Expr::Field(Box::new(other), name),
4607 };
4608 }
4609 } else if self.check(TokenType::LBracket) {
4610 self.advance();
4612 let index = self.parse_expr_bp(0)?;
4613 self.consume(TokenType::RBracket)?;
4614 expr = Expr::Index(Box::new(expr), Box::new(index));
4615 } else {
4616 break;
4617 }
4618 }
4619 Ok(expr)
4620 }
4621
4622 fn parse_expr_atom(&mut self) -> Result<Expr, ParseError> {
4623 let tok = self.current().clone();
4624 match tok.ttype {
4625 TokenType::Integer => {
4626 self.advance();
4627 let lit = tok
4628 .value
4629 .parse::<i64>()
4630 .map(ExprLit::Int)
4631 .or_else(|_| tok.value.parse::<f64>().map(ExprLit::Float))
4632 .map_err(|_| ParseError {
4633 message: format!("invalid integer literal '{}'", tok.value),
4634 line: tok.line,
4635 column: tok.column,
4636 ..Default::default()
4637 })?;
4638 Ok(Expr::Lit(lit))
4639 }
4640 TokenType::Float => {
4641 self.advance();
4642 let f = tok.value.parse::<f64>().map_err(|_| ParseError {
4643 message: format!("invalid float literal '{}'", tok.value),
4644 line: tok.line,
4645 column: tok.column,
4646 ..Default::default()
4647 })?;
4648 Ok(Expr::Lit(ExprLit::Float(f)))
4649 }
4650 TokenType::Bool => {
4651 self.advance();
4652 Ok(Expr::Lit(ExprLit::Bool(tok.value == "true")))
4653 }
4654 TokenType::StringLit => {
4655 self.advance();
4656 Ok(Expr::Lit(ExprLit::Str(tok.value)))
4657 }
4658 TokenType::LParen => {
4659 self.advance();
4660 let inner = self.parse_expr_bp(0)?;
4661 self.consume(TokenType::RParen)?;
4662 Ok(inner)
4663 }
4664 _ => {
4665 Ok(Expr::Ref(self.consume_any_ident_or_kw()?.value))
4669 }
4670 }
4671 }
4672
4673 fn expr_lit_surface(lit: &ExprLit) -> String {
4678 match lit {
4679 ExprLit::Int(i) => i.to_string(),
4680 ExprLit::Float(f) => f.to_string(),
4681 ExprLit::Bool(b) => b.to_string(),
4682 ExprLit::Str(s) => s.clone(),
4683 }
4684 }
4685
4686 fn expr_leaf_surface(expr: &Expr) -> Option<String> {
4687 match expr {
4688 Expr::Ref(p) => Some(p.clone()),
4689 Expr::Lit(l) => Some(Self::expr_lit_surface(l)),
4690 _ => None,
4691 }
4692 }
4693
4694 fn expr_legacy_leaf(expr: &Expr) -> Option<(String, String, String)> {
4698 match expr {
4699 Expr::Ref(p) => Some((p.clone(), String::new(), String::new())),
4700 Expr::Binary(op, l, r) => {
4701 let op_s = match op {
4702 BinOp::Eq => "==",
4703 BinOp::Ne => "!=",
4704 BinOp::Lt => "<",
4705 BinOp::Le => "<=",
4706 BinOp::Gt => ">",
4707 BinOp::Ge => ">=",
4708 _ => return None,
4709 };
4710 let lhs = match &**l {
4711 Expr::Ref(p) => p.clone(),
4712 _ => return None,
4713 };
4714 let rhs = Self::expr_leaf_surface(r)?;
4715 Some((lhs, op_s.to_string(), rhs))
4716 }
4717 _ => None,
4718 }
4719 }
4720
4721 fn collect_or_leaves(expr: &Expr, out: &mut Vec<(String, String, String)>) -> bool {
4724 match expr {
4725 Expr::Binary(BinOp::Or, l, r) => {
4726 Self::collect_or_leaves(l, out) && Self::collect_or_leaves(r, out)
4727 }
4728 _ => match Self::expr_legacy_leaf(expr) {
4729 Some(t) => {
4730 out.push(t);
4731 true
4732 }
4733 None => false,
4734 },
4735 }
4736 }
4737
4738 #[allow(clippy::type_complexity)]
4744 fn cond_as_legacy(
4745 expr: &Expr,
4746 ) -> Option<(String, String, String, Vec<(String, String, String)>, String)> {
4747 let mut leaves = Vec::new();
4748 if !Self::collect_or_leaves(expr, &mut leaves) || leaves.is_empty() {
4749 return None;
4750 }
4751 let (c0, o0, v0) = leaves[0].clone();
4752 let rest = leaves[1..].to_vec();
4753 let conjunctor = if rest.is_empty() {
4754 String::new()
4755 } else {
4756 "or".to_string()
4757 };
4758 Some((c0, o0, v0, rest, conjunctor))
4759 }
4760
4761 fn parse_if(&mut self) -> Result<ConditionalNode, ParseError> {
4762 let tok = self.consume(TokenType::If)?;
4763 let loc = self.loc_of(&tok);
4764
4765 let expr = self.parse_expr()?;
4770 let (condition, comparison_op, comparison_value, conditions, conjunctor, cond) =
4771 match Self::cond_as_legacy(&expr) {
4772 Some((c, o, v, more, conj)) => (c, o, v, more, conj, None),
4773 None => (
4774 String::new(),
4775 String::new(),
4776 String::new(),
4777 Vec::new(),
4778 String::new(),
4779 Some(expr),
4780 ),
4781 };
4782
4783 let mut then_body = Vec::new();
4784 let mut else_body = Vec::new();
4785
4786 if self.check(TokenType::Arrow) {
4788 self.advance();
4789 then_body.push(self.parse_flow_step()?);
4790 } else if self.check(TokenType::LBrace) {
4791 self.advance();
4792 while !self.check(TokenType::RBrace) {
4793 then_body.push(self.parse_flow_step()?);
4794 }
4795 self.consume(TokenType::RBrace)?;
4796 }
4797
4798 if self.check(TokenType::Else) {
4800 self.advance();
4801 if self.check(TokenType::Arrow) {
4802 self.advance();
4803 else_body.push(self.parse_flow_step()?);
4804 } else if self.check(TokenType::LBrace) {
4805 self.advance();
4806 while !self.check(TokenType::RBrace) {
4807 else_body.push(self.parse_flow_step()?);
4808 }
4809 self.consume(TokenType::RBrace)?;
4810 }
4811 }
4812
4813 Ok(ConditionalNode {
4814 condition,
4815 comparison_op,
4816 comparison_value,
4817 then_body,
4818 else_body,
4819 conditions,
4820 conjunctor,
4821 cond,
4822 loc,
4823 })
4824 }
4825
4826 fn parse_for_in(&mut self) -> Result<ForInStatement, ParseError> {
4829 let tok = self.consume(TokenType::For)?;
4830 let loc = self.loc_of(&tok);
4831 let variable = self.consume(TokenType::Identifier)?.value;
4832 self.consume(TokenType::In)?;
4833 let iterable = self.parse_dotted_identifier()?;
4834
4835 self.consume(TokenType::LBrace)?;
4836 self.loop_depth += 1;
4843 let body_result = (|| -> Result<Vec<FlowStep>, ParseError> {
4844 let mut body = Vec::new();
4845 while !self.check(TokenType::RBrace) {
4846 body.push(self.parse_flow_step()?);
4847 }
4848 Ok(body)
4849 })();
4850 self.loop_depth -= 1;
4851 let body = body_result?;
4852 self.consume(TokenType::RBrace)?;
4853
4854 Ok(ForInStatement {
4855 variable,
4856 iterable,
4857 body,
4858 loc,
4859 })
4860 }
4861
4862 fn parse_break(&mut self) -> Result<BreakStatement, ParseError> {
4865 let tok = self.consume(TokenType::Break)?;
4866 let loc = self.loc_of(&tok);
4867 if self.loop_depth == 0 {
4868 return Err(ParseError {
4869 message: "'break' outside of a for-in loop body".to_string(),
4870 line: tok.line,
4871 column: tok.column,
4872 ..Default::default()
4873 });
4874 }
4875 Ok(BreakStatement { loc })
4876 }
4877
4878 fn parse_continue(&mut self) -> Result<ContinueStatement, ParseError> {
4881 let tok = self.consume(TokenType::Continue)?;
4882 let loc = self.loc_of(&tok);
4883 if self.loop_depth == 0 {
4884 return Err(ParseError {
4885 message: "'continue' outside of a for-in loop body".to_string(),
4886 line: tok.line,
4887 column: tok.column,
4888 ..Default::default()
4889 });
4890 }
4891 Ok(ContinueStatement { loc })
4892 }
4893
4894 fn parse_let(&mut self) -> Result<LetStatement, ParseError> {
4897 let tok = self.consume(TokenType::Let)?;
4898 let loc = self.loc_of(&tok);
4899
4900 let name = self.consume_any_ident_or_kw()?.value;
4902 let type_annotation = if self.check(TokenType::Colon) {
4904 self.advance();
4905 Some(self.parse_type_expr()?)
4906 } else {
4907 None
4908 };
4909 self.consume(TokenType::Assign)?;
4910 self.last_let_value_kind = "literal".to_string();
4913 let (value, value_ast) = self.parse_let_value_expr_with_ast()?;
4914
4915 Ok(LetStatement {
4916 identifier: name,
4917 value_expr: value,
4918 value_kind: self.last_let_value_kind.clone(),
4919 type_annotation,
4920 value_ast,
4921 loc,
4922 leading_trivia: Vec::new(),
4923 trailing_trivia: Vec::new(),
4924 })
4925 }
4926
4927 fn parse_let_value_expr(&mut self) -> Result<String, ParseError> {
4928 let atom = self.parse_let_atom()?;
4929
4930 if matches!(
4932 self.current().ttype,
4933 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4934 ) {
4935 let mut parts = vec![atom];
4936 while matches!(
4937 self.current().ttype,
4938 TokenType::Plus | TokenType::Minus | TokenType::Star | TokenType::Slash
4939 ) {
4940 parts.push(self.advance().value.clone());
4941 parts.push(self.parse_let_atom()?);
4942 }
4943 self.last_let_value_kind = "expression".to_string();
4944 return Ok(parts.join(" "));
4945 }
4946 Ok(atom)
4947 }
4948
4949 fn parse_let_value_expr_with_ast(&mut self) -> Result<(String, Option<Expr>), ParseError> {
4959 if self.check(TokenType::LBracket) {
4960 self.last_let_value_kind = "literal".to_string();
4961 return Ok((self.parse_let_list_literal()?, None));
4962 }
4963 let expr = self.parse_expr()?;
4964 Ok(match expr {
4965 Expr::Lit(lit) => {
4966 self.last_let_value_kind = "literal".to_string();
4967 (Self::expr_lit_surface(&lit), None)
4968 }
4969 Expr::Ref(p) => {
4970 self.last_let_value_kind = "reference".to_string();
4971 (p, None)
4972 }
4973 other => {
4974 self.last_let_value_kind = "expression".to_string();
4975 (Self::render_expr(&other), Some(other))
4976 }
4977 })
4978 }
4979
4980 fn render_expr(e: &Expr) -> String {
4983 match e {
4984 Expr::Lit(l) => Self::expr_lit_surface(l),
4985 Expr::Ref(p) => p.clone(),
4986 Expr::Let { name, value, body } => format!(
4990 "let {name} = {} in {}",
4991 Self::render_expr(value),
4992 Self::render_expr(body)
4993 ),
4994 Expr::Unary(UnOp::Neg, x) => format!("-{}", Self::render_expr(x)),
4995 Expr::Unary(UnOp::Not, x) => format!("not {}", Self::render_expr(x)),
4996 Expr::Binary(op, l, r) => {
4997 let sym = match op {
4998 BinOp::Add => "+",
4999 BinOp::Sub => "-",
5000 BinOp::Mul => "*",
5001 BinOp::Div => "/",
5002 BinOp::Mod => "%",
5003 BinOp::Eq => "==",
5004 BinOp::Ne => "!=",
5005 BinOp::Lt => "<",
5006 BinOp::Le => "<=",
5007 BinOp::Gt => ">",
5008 BinOp::Ge => ">=",
5009 BinOp::And => "and",
5010 BinOp::Or => "or",
5011 };
5012 format!("({} {sym} {})", Self::render_expr(l), Self::render_expr(r))
5013 }
5014 Expr::Call(b, args) => {
5015 let recv = args.first().map(Self::render_expr).unwrap_or_default();
5016 let rest: Vec<String> = args.iter().skip(1).map(Self::render_expr).collect();
5017 if rest.is_empty() {
5018 format!("{recv}.{}", b.surface())
5019 } else {
5020 format!("{recv}.{}({})", b.surface(), rest.join(", "))
5021 }
5022 }
5023 Expr::Field(b, f) => format!("{}.{f}", Self::render_expr(b)),
5024 Expr::Index(b, i) => format!("{}[{}]", Self::render_expr(b), Self::render_expr(i)),
5025 }
5026 }
5027
5028 fn parse_let_atom(&mut self) -> Result<String, ParseError> {
5029 let tok = self.current().clone();
5030
5031 match tok.ttype {
5032 TokenType::StringLit => {
5033 self.last_let_value_kind = "literal".to_string();
5034 self.advance();
5035 Ok(tok.value)
5036 }
5037 TokenType::Integer | TokenType::Float => {
5038 self.last_let_value_kind = "literal".to_string();
5039 self.advance();
5040 Ok(tok.value)
5041 }
5042 TokenType::Bool => {
5043 self.last_let_value_kind = "literal".to_string();
5044 self.advance();
5045 Ok(tok.value)
5046 }
5047 TokenType::Identifier => {
5048 self.last_let_value_kind = "reference".to_string();
5049 self.parse_dotted_identifier()
5050 }
5051 TokenType::LBracket => {
5052 self.last_let_value_kind = "literal".to_string();
5053 self.parse_let_list_literal()
5054 }
5055 _ => {
5056 if self.pos + 1 < self.tokens.len()
5058 && self.tokens[self.pos + 1].ttype == TokenType::Dot
5059 {
5060 self.last_let_value_kind = "reference".to_string();
5061 return self.parse_dotted_identifier();
5062 }
5063 Err(ParseError {
5064 message: format!(
5065 "Expected value expression, found {:?}('{}')",
5066 tok.ttype, tok.value
5067 ),
5068 line: tok.line,
5069 column: tok.column,
5070 ..Default::default()
5071 })
5072 }
5073 }
5074 }
5075
5076 fn parse_let_list_literal(&mut self) -> Result<String, ParseError> {
5077 self.consume(TokenType::LBracket)?;
5078 let mut items = Vec::new();
5079 if !self.check(TokenType::RBracket) {
5080 items.push(self.parse_let_value_expr()?);
5081 while self.check(TokenType::Comma) {
5082 self.advance();
5083 if self.check(TokenType::RBracket) {
5084 break; }
5086 items.push(self.parse_let_value_expr()?);
5087 }
5088 }
5089 self.consume(TokenType::RBracket)?;
5090 Ok(format!("[{}]", items.join(", ")))
5091 }
5092
5093 fn parse_return(&mut self) -> Result<ReturnStatement, ParseError> {
5096 let tok = self.consume(TokenType::Return)?;
5097 let loc = self.loc_of(&tok);
5098 let value = self.parse_let_value_expr()?;
5099 Ok(ReturnStatement {
5100 value_expr: value,
5101 loc,
5102 })
5103 }
5104
5105 fn parse_flow_step_simple(&mut self, _kw: &str) -> Result<(Loc, String), ParseError> {
5109 let tok = self.current().clone();
5110 self.advance(); let target = if self.at_declaration_start()
5112 || self.check(TokenType::RBrace)
5113 || self.check(TokenType::Eof)
5114 {
5115 String::new()
5116 } else {
5117 self.consume_any_ident_or_kw()?.value.clone()
5118 };
5119 if self.check(TokenType::LBrace) {
5121 self.skip_braced_block()?;
5122 }
5123 Ok((
5124 Loc {
5125 line: tok.line,
5126 column: tok.column,
5127 },
5128 target,
5129 ))
5130 }
5131
5132 fn parse_stream_block(&mut self) -> Result<StreamBlock, ParseError> {
5143 let tok = self.current().clone();
5144 let loc = self.loc_of(&tok);
5145 self.advance(); let mut chunk_type = String::new();
5153 if self.check(TokenType::Lt) {
5154 self.advance();
5155 let inner = self.parse_type_expr()?;
5156 chunk_type = if inner.generic_param.is_empty() {
5157 inner.name
5158 } else {
5159 format!("{}<{}>", inner.name, inner.generic_param)
5160 };
5161 self.consume(TokenType::Gt)?;
5162 }
5163
5164 while !self.check(TokenType::LBrace)
5168 && !self.check(TokenType::RBrace)
5169 && !self.check(TokenType::Eof)
5170 && !self.at_declaration_start()
5171 {
5172 self.advance();
5173 }
5174
5175 let mut block = StreamBlock {
5176 chunk_type,
5177 on_chunk: None,
5178 on_complete: None,
5179 on_error: None,
5180 body: Vec::new(),
5181 loc,
5182 };
5183
5184 if self.check(TokenType::LBrace) {
5185 self.advance();
5186 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5187 let name = self.current().value.clone();
5193 let is_arm = matches!(name.as_str(), "on_chunk" | "on_complete" | "on_error")
5194 && self
5195 .tokens
5196 .get(self.pos + 1)
5197 .is_some_and(|t| t.ttype == TokenType::Colon);
5198 if is_arm {
5199 let arm_tok = self.current().clone();
5200 self.advance(); self.advance(); let arm = self.parse_stream_handler_arm(&name, &arm_tok)?;
5203 let slot = match name.as_str() {
5204 "on_chunk" => &mut block.on_chunk,
5205 "on_complete" => &mut block.on_complete,
5206 _ => &mut block.on_error,
5207 };
5208 if slot.is_some() {
5209 return Err(ParseError {
5210 message: format!(
5211 "`{name}` is declared twice in this `stream` block. Two handlers \
5212 for one edge have no defined composition (whose output is the \
5213 stream's?), so the duplicate is refused rather than silently \
5214 overwriting the first."
5215 ),
5216 line: arm_tok.line,
5217 column: arm_tok.column,
5218 ..Default::default()
5219 });
5220 }
5221 *slot = Some(arm);
5222 continue;
5223 }
5224
5225 let next_two_are_block = self
5232 .tokens
5233 .get(self.pos + 1)
5234 .is_some_and(|t| t.ttype == TokenType::Colon)
5235 && self
5236 .tokens
5237 .get(self.pos + 2)
5238 .is_some_and(|t| t.ttype == TokenType::LBrace);
5239 if next_two_are_block {
5240 let bad = self.current().clone();
5241 return Err(ParseError {
5242 message: format!(
5243 "unknown `stream` handler `{name}` — this block accepts only \
5244 `on_chunk:` (run once per chunk, with the chunk bound as `chunk`), \
5245 `on_complete:` (run once, after the source closes, with the \
5246 accumulation bound as `complete`) and `on_error:` (run when the \
5247 SOURCE fails, with the failure bound as `error`). An unrecognised \
5248 handler is refused rather than skipped: a skipped handler removes \
5249 the processing the author wrote, and silence in that direction is \
5250 indistinguishable from a stream that had nothing to do."
5251 ),
5252 line: bad.line,
5253 column: bad.column,
5254 ..Default::default()
5255 });
5256 }
5257
5258 block.body.push(self.parse_flow_step()?);
5260 }
5261 self.consume(TokenType::RBrace)?;
5262 }
5263
5264 Ok(block)
5265 }
5266
5267 fn parse_stream_handler_arm(
5276 &mut self,
5277 name: &str,
5278 at: &Token,
5279 ) -> Result<StepNode, ParseError> {
5280 self.consume(TokenType::LBrace)?;
5281 let mut node = StepNode {
5282 name: name.to_string(),
5283 persona_ref: String::new(),
5284 given: String::new(),
5285 ask: String::new(),
5286 output_type: String::new(),
5287 confidence_floor: None,
5288 navigate_ref: String::new(),
5289 apply_ref: String::new(),
5290 requires_context: None,
5291 now_tz: None,
5292 guards: Vec::new(),
5293 pix_ops: Vec::new(),
5294 stream: None,
5295 performs: Vec::new(),
5296 loc: self.loc_of(at),
5297 };
5298 self.parse_step_body_into(&mut node)?;
5299 self.consume(TokenType::RBrace)?;
5300 Ok(node)
5301 }
5302
5303 fn parse_block_step(&mut self, _kw: &str) -> Result<Loc, ParseError> {
5304 let tok = self.current().clone();
5305 self.advance();
5306 while !self.check(TokenType::LBrace)
5308 && !self.check(TokenType::RBrace)
5309 && !self.check(TokenType::Eof)
5310 && !self.at_declaration_start()
5311 {
5312 self.advance();
5313 }
5314 if self.check(TokenType::LBrace) {
5315 self.skip_braced_block()?;
5316 }
5317 Ok(Loc {
5318 line: tok.line,
5319 column: tok.column,
5320 })
5321 }
5322
5323 fn parse_forge_step(&mut self) -> Result<ForgeBlock, ParseError> {
5330 let tok = self.consume(TokenType::Forge)?;
5331 let name = self.consume(TokenType::Identifier)?.value;
5332 let mut node = ForgeBlock {
5333 name,
5334 novelty: 0.5,
5335 depth: 1,
5336 branches: 1,
5337 loc: Loc { line: tok.line, column: tok.column },
5338 ..Default::default()
5339 };
5340 self.consume(TokenType::LParen)?;
5342 let arg = self.consume_any_ident_or_kw()?.value;
5343 self.consume(TokenType::Colon)?;
5344 if arg != "seed" {
5345 return Err(self.error(&format!(
5346 "forge '{}' expects `seed:` as its argument, found `{arg}`",
5347 node.name
5348 )));
5349 }
5350 node.seed = self.consume(TokenType::StringLit)?.value;
5351 self.consume(TokenType::RParen)?;
5352 self.consume(TokenType::Arrow)?;
5354 node.output_type = self.consume_any_ident_or_kw()?.value;
5355 self.consume(TokenType::LBrace)?;
5357 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5358 let field = self.consume_any_ident_or_kw()?.value;
5359 self.consume(TokenType::Colon)?;
5360 match field.as_str() {
5361 "mode" => node.mode = self.consume_any_ident_or_kw()?.value,
5362 "novelty" => node.novelty = self.consume_number()?,
5363 "depth" => {
5364 node.depth = self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5365 }
5366 "branches" => {
5367 node.branches =
5368 self.consume(TokenType::Integer)?.value.parse::<i64>().unwrap_or(0)
5369 }
5370 "constraints" => node.constraints_ref = self.consume_any_ident_or_kw()?.value,
5371 other => {
5372 return Err(self.error(&format!("unknown forge field `{other}`")))
5373 }
5374 }
5375 if self.check(TokenType::Comma) {
5376 self.consume(TokenType::Comma)?;
5377 }
5378 }
5379 self.consume(TokenType::RBrace)?;
5380 Ok(node)
5381 }
5382
5383 fn parse_par_block(&mut self) -> Result<ParBlock, ParseError> {
5392 let tok = self.current().clone();
5393 self.advance(); self.consume(TokenType::LBrace)?;
5395 let mut branches: Vec<Vec<FlowStep>> = Vec::new();
5396 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5397 branches.push(vec![self.parse_flow_step()?]);
5398 }
5399 self.consume(TokenType::RBrace)?;
5400 Ok(ParBlock {
5401 branches,
5402 loc: Loc {
5403 line: tok.line,
5404 column: tok.column,
5405 },
5406 })
5407 }
5408
5409 fn parse_warden(&mut self) -> Result<WardenBlock, ParseError> {
5426 let tok = self.consume(TokenType::Warden)?;
5427 self.consume(TokenType::LParen)?;
5429 let target = self.consume_any_ident_or_kw()?.value;
5430 self.consume(TokenType::RParen)?;
5431 self.consume(TokenType::Within)?;
5433 let scope_ref = self.consume(TokenType::Identifier)?.value;
5434 let mut block = WardenBlock {
5435 target,
5436 scope_ref,
5437 body: Vec::new(),
5438 loc: Loc {
5439 line: tok.line,
5440 column: tok.column,
5441 },
5442 };
5443 self.consume(TokenType::LBrace)?;
5445 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5446 block.body.push(self.parse_flow_step()?);
5447 }
5448 self.consume(TokenType::RBrace)?;
5449 Ok(block)
5450 }
5451
5452 fn parse_scope(&mut self) -> Result<ScopeDefinition, ParseError> {
5457 let tok = self.consume(TokenType::Scope)?;
5458 let name = self.consume(TokenType::Identifier)?.value;
5459 let mut node = ScopeDefinition {
5460 name,
5461 loc: Loc {
5462 line: tok.line,
5463 column: tok.column,
5464 },
5465 ..Default::default()
5466 };
5467 self.consume(TokenType::LBrace)?;
5468 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5469 let key = self.consume_any_ident_or_kw()?.value;
5470 self.consume(TokenType::Colon)?;
5471 match key.as_str() {
5472 "targets" => {
5473 self.consume(TokenType::LBracket)?;
5474 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
5475 let t = if self.check(TokenType::StringLit) {
5476 self.consume(TokenType::StringLit)?.value
5477 } else {
5478 self.consume_any_ident_or_kw()?.value
5479 };
5480 node.targets.push(t);
5481 if self.check(TokenType::Comma) {
5482 self.advance();
5483 }
5484 }
5485 self.consume(TokenType::RBracket)?;
5486 }
5487 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
5488 "approver" => {
5489 if self.current().value == "requires" {
5491 self.advance();
5492 }
5493 node.approver = self.consume(TokenType::StringLit)?.value;
5494 }
5495 other => {
5496 return Err(self.error(&format!(
5497 "unknown scope field `{other}` in scope `{}` — expected \
5498 `targets` / `depth` / `approver`",
5499 node.name
5500 )))
5501 }
5502 }
5503 if self.check(TokenType::Comma) {
5504 self.consume(TokenType::Comma)?;
5505 }
5506 }
5507 self.consume(TokenType::RBrace)?;
5508 Ok(node)
5509 }
5510
5511 fn parse_quant(&mut self) -> Result<QuantBlock, ParseError> {
5512 let tok = self.current().clone();
5513 self.advance(); let mut block = QuantBlock {
5516 encoding: None,
5517 observable: None,
5518 qubits: None,
5519 depth: None,
5520 bandwidth: None,
5521 reupload: None,
5522 effect: "quant_sim".to_string(),
5525 body: Vec::new(),
5526 loc: Loc {
5527 line: tok.line,
5528 column: tok.column,
5529 },
5530 };
5531
5532 if self.check(TokenType::LParen) {
5534 self.advance();
5535 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
5536 let key = self.consume_any_ident_or_kw()?.value;
5537 self.consume(TokenType::Colon)?;
5538 match key.as_str() {
5539 "encoding" => {
5540 block.encoding = Some(self.consume_any_ident_or_kw()?.value)
5541 }
5542 "observable" => {
5543 block.observable = Some(self.parse_dotted_identifier()?)
5544 }
5545 "qubits" => block.qubits = Some(self.consume_number()? as i64),
5546 "depth" => block.depth = Some(self.consume_number()? as i64),
5547 "bandwidth" => block.bandwidth = Some(self.consume_number()?),
5548 "reupload" => block.reupload = Some(self.consume_number()? as i64),
5550 "backend" => block.effect = self.consume_any_ident_or_kw()?.value,
5552 other => {
5553 return Err(ParseError {
5554 message: format!(
5555 "Unknown `quant` attribute `{other}` — expected one of \
5556 encoding, observable, qubits, depth, bandwidth, reupload, backend"
5557 ),
5558 line: self.current().line,
5559 column: self.current().column,
5560 ..Default::default()
5561 });
5562 }
5563 }
5564 if self.check(TokenType::Comma) {
5566 self.advance();
5567 }
5568 }
5569 self.consume(TokenType::RParen)?;
5570 }
5571
5572 self.consume(TokenType::LBrace)?;
5574 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5575 block.body.push(self.parse_flow_step()?);
5576 }
5577 self.consume(TokenType::RBrace)?;
5578
5579 Ok(block)
5580 }
5581
5582 fn parse_yield(&mut self) -> Result<YieldStatement, ParseError> {
5586 let tok = self.consume(TokenType::Yield)?;
5587 let loc = self.loc_of(&tok);
5588 self.last_let_value_kind = "literal".to_string();
5589 let value_expr = self.parse_let_value_expr()?;
5590 Ok(YieldStatement {
5591 value_expr,
5592 value_kind: self.last_let_value_kind.clone(),
5593 loc,
5594 })
5595 }
5596
5597 fn parse_compute_apply(&mut self) -> Result<ComputeApplyStep, ParseError> {
5604 let tok = self.current().clone();
5605 let loc = self.loc_of(&tok);
5606 self.advance(); let compute_name = self.consume_any_ident_or_kw()?.value.clone();
5608
5609 let mut arguments = Vec::new();
5610 if self.current().value == "on" {
5611 self.advance();
5612 loop {
5613 arguments.push(self.parse_subject()?);
5619 if self.check(TokenType::Comma) {
5620 self.advance();
5621 } else {
5622 break;
5623 }
5624 }
5625 }
5626
5627 let mut output_name = String::new();
5628 if self.check(TokenType::Arrow) {
5629 self.advance();
5630 output_name = self.consume_any_ident_or_kw()?.value.clone();
5631 }
5632
5633 Ok(ComputeApplyStep {
5634 compute_name,
5635 arguments,
5636 output_name,
5637 loc,
5638 })
5639 }
5640
5641 fn parse_declassify_step(&mut self) -> Result<DeclassifyStep, ParseError> {
5650 let tok = self.current().clone();
5651 self.advance();
5652
5653 let class = self.consume_any_ident_or_kw()?.value.clone();
5654
5655 if self.current().value != "from" {
5656 return Err(ParseError {
5657 message: format!(
5658 "`declassify {class}` must name the value it retires the class from: \
5659 `declassify {class} from <value> -> <Type> via <Shield>`. Found `{}`.",
5660 self.current().value
5661 ),
5662 line: self.current().line,
5663 column: self.current().column,
5664 source_snippet: None,
5665 });
5666 }
5667 self.advance();
5668 let source = self.parse_subject()?;
5669
5670 if !self.check(TokenType::Arrow) {
5671 return Err(ParseError {
5672 message: format!(
5673 "`declassify {class} from {source}` must name the TYPE the value leaves \
5674 as: `-> <Type>`. A declassification that does not change the type \
5675 changes nothing a trust boundary can read."
5676 ),
5677 line: self.current().line,
5678 column: self.current().column,
5679 source_snippet: None,
5680 });
5681 }
5682 self.advance();
5683 let output_type = self.consume_any_ident_or_kw()?.value.clone();
5684
5685 if self.current().value != "via" {
5686 return Err(ParseError {
5687 message: format!(
5688 "`declassify {class} from {source} -> {output_type}` must name the \
5689 shield that authorises it: `via <Shield>`. Retiring a regulatory \
5690 class is a capability a control declares, not something a step may \
5691 do on its own."
5692 ),
5693 line: self.current().line,
5694 column: self.current().column,
5695 source_snippet: None,
5696 });
5697 }
5698 self.advance();
5699 let shield = self.consume_any_ident_or_kw()?.value.clone();
5700
5701 Ok(DeclassifyStep {
5702 class,
5703 source,
5704 output_type,
5705 shield,
5706 loc: Loc { line: tok.line, column: tok.column },
5707 })
5708 }
5709 fn parse_apply_step(&mut self, _kw: &str) -> Result<(Loc, String, String, String), ParseError> {
5710 let tok = self.current().clone();
5711 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5713 let mut target = String::new();
5714 let mut output_type = String::new();
5715 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
5717 let next = self.current().clone();
5718 if next.value == "on" {
5719 self.advance();
5720 target = self.parse_subject()?;
5723 }
5724 }
5725 if self.check(TokenType::Arrow) {
5727 self.advance();
5728 output_type = self.consume_any_ident_or_kw()?.value.clone();
5729 }
5730 if self.check(TokenType::LBrace) {
5732 self.skip_braced_block()?;
5733 }
5734 Ok((
5735 Loc {
5736 line: tok.line,
5737 column: tok.column,
5738 },
5739 name,
5740 target,
5741 output_type,
5742 ))
5743 }
5744
5745 fn parse_step_guard(&mut self, kind: &str) -> Result<StepGuardNode, ParseError> {
5756 let tok = self.current().clone();
5757 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
5759 let mut target = String::new();
5760 let mut binding = String::new();
5761 if self.current().value == "on" {
5762 self.advance();
5763 target = self.parse_subject()?;
5766 if self.check(TokenType::LParen) {
5769 let mut depth = 0usize;
5770 loop {
5771 let t = self.current().clone();
5772 match t.ttype {
5773 TokenType::LParen => depth += 1,
5774 TokenType::RParen => depth -= 1,
5775 TokenType::Eof => {
5776 return Err(ParseError {
5777 message: format!(
5778 "unterminated argument list in `{kind} {name} on {target}(…`"
5779 ),
5780 line: t.line,
5781 column: t.column,
5782 ..Default::default()
5783 })
5784 }
5785 _ => {}
5786 }
5787 target.push_str(&t.value);
5788 self.advance();
5789 if depth == 0 {
5790 break;
5791 }
5792 }
5793 }
5794 }
5795 if self.check(TokenType::Arrow) {
5796 self.advance();
5797 binding = self.consume_any_ident_or_kw()?.value.clone();
5798 }
5799 Ok(StepGuardNode {
5800 kind: kind.to_string(),
5801 name,
5802 target,
5803 binding,
5804 loc: Loc {
5805 line: tok.line,
5806 column: tok.column,
5807 },
5808 })
5809 }
5810
5811 fn parse_reason_step(&mut self) -> Result<ReasonStep, ParseError> {
5822 let tok = self.current().clone();
5823 let loc = self.loc_of(&tok);
5824 self.advance(); let next_is_field_key = self
5837 .tokens
5838 .get(self.pos + 1)
5839 .is_some_and(|t| t.ttype == TokenType::Colon);
5840 let target = if self.check(TokenType::LBrace)
5841 || self.at_declaration_start()
5842 || self.check(TokenType::RBrace)
5843 || self.check(TokenType::Eof)
5844 || next_is_field_key
5845 {
5846 String::new()
5847 } else {
5848 self.parse_dotted_identifier()?
5849 };
5850
5851 let mut node = ReasonStep {
5852 strategy: String::new(),
5853 target,
5854 given: String::new(),
5855 ask: String::new(),
5856 depth: None,
5857 loc,
5858 };
5859
5860 if !self.check(TokenType::LBrace) {
5861 return Ok(node);
5862 }
5863 self.advance();
5864 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
5865 let key = self.current().clone();
5866 self.advance();
5867 self.consume(TokenType::Colon)?;
5868 match key.value.as_str() {
5869 "given" => {
5874 let mut parts = vec![self.parse_expression_string()?];
5875 while self.check(TokenType::Comma) {
5876 self.advance();
5877 parts.push(self.parse_expression_string()?);
5878 }
5879 node.given = parts.join(", ");
5880 }
5881 "ask" => node.ask = self.consume(TokenType::StringLit)?.value,
5882 "depth" => {
5883 let n = self.current().clone();
5884 if n.ttype != TokenType::Integer {
5885 return Err(ParseError {
5886 message: format!(
5887 "`depth:` in a `reason` block is a deliberation depth — a \
5888 positive integer (got '{}')",
5889 n.value
5890 ),
5891 line: n.line,
5892 column: n.column,
5893 ..Default::default()
5894 });
5895 }
5896 self.advance();
5897 node.depth = n.value.parse::<u32>().ok();
5898 }
5899 "chain_of_thought" => {
5903 let v = self.consume_any_ident_or_kw()?.value;
5904 if v == "enabled" {
5905 node.strategy = "chain_of_thought".to_string();
5906 }
5907 }
5908 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value,
5909 "target" => {
5918 let v = if self.check(TokenType::StringLit) {
5919 self.consume(TokenType::StringLit)?.value
5920 } else {
5921 self.parse_dotted_identifier()?
5922 };
5923 if !node.target.is_empty() {
5924 return Err(ParseError {
5925 message: format!(
5926 "`reason {} {{ target: … }}` declares the subject twice — \
5927 once positionally as `{}` and once as `target: {}`. They \
5928 are the same field. Write one of them.",
5929 node.target, node.target, v
5930 ),
5931 line: key.line,
5932 column: key.column,
5933 ..Default::default()
5934 });
5935 }
5936 node.target = v;
5937 }
5938 other => {
5939 return Err(ParseError {
5940 message: format!(
5941 "unknown field '{other}' in a `reason` block. Accepted: given, \
5942 ask, depth, strategy, chain_of_thought, target. A field this \
5943 block does not recognise is REFUSED rather than skipped — a \
5944 `reason` that silently loses its `ask:` deliberates over \
5945 nothing, and that failure is quiet."
5946 ),
5947 line: key.line,
5948 column: key.column,
5949 ..Default::default()
5950 })
5951 }
5952 }
5953 }
5954 self.consume(TokenType::RBrace)?;
5955 Ok(node)
5956 }
5957
5958 fn at_weave_field(&self) -> bool {
5967 const FIELDS: &[&str] = &["format", "include", "priority", "style"];
5968 self.field_ahead(FIELDS)
5969 }
5970
5971 fn parse_weave_step(&mut self) -> Result<FlowStep, ParseError> {
5980 let tok = self.current().clone();
5981 self.advance();
5982 let mut node = WeaveStep {
5983 sources: Vec::new(),
5984 target: String::new(),
5985 format_type: String::new(),
5986 priority: Vec::new(),
5987 style: String::new(),
5988 include: Vec::new(),
5989 loc: Loc {
5990 line: tok.line,
5991 column: tok.column,
5992 },
5993 };
5994 if self.check(TokenType::LBracket) {
6000 node.sources = self.parse_bracketed_dot_identifiers()?;
6001 } else if self.current().ttype == TokenType::Identifier
6002 && !self
6003 .tokens
6004 .get(self.pos + 1)
6005 .is_some_and(|t| t.ttype == TokenType::Colon)
6006 {
6007 node.sources = vec![self.parse_dotted_identifier()?];
6017 }
6018 if self.check(TokenType::Into) || self.current().value == "into" {
6020 self.advance();
6021 node.target = self.parse_dotted_identifier()?;
6022 }
6023 while self.at_weave_field() {
6025 let f = self.current().value.clone();
6026 self.advance();
6027 self.consume(TokenType::Colon)?;
6028 match f.as_str() {
6029 "format" => node.format_type = self.consume_any_ident_or_kw()?.value.clone(),
6030 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
6031 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
6032 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
6033 other => {
6036 return Err(ParseError {
6037 message: format!(
6038 "`{other}` passed the `weave` field test but has no handler — \
6039 the braceless catalog and its parser have drifted apart."
6040 ),
6041 line: tok.line,
6042 column: tok.column,
6043 ..Default::default()
6044 })
6045 }
6046 }
6047 }
6048 if self.check(TokenType::LBrace) {
6049 self.advance();
6050 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6051 let f = self.current().value.clone();
6052 self.advance();
6053 if self.check(TokenType::Colon) {
6054 self.advance();
6055 match f.as_str() {
6056 "sources" => node.sources = self.parse_bracketed_dot_identifiers()?,
6057 "target" => node.target = self.consume_any_ident_or_kw()?.value.clone(),
6058 "format" => {
6059 node.format_type = self.consume_any_ident_or_kw()?.value.clone()
6060 }
6061 "priority" => node.priority = self.parse_bracketed_dot_identifiers()?,
6062 "style" => node.style = self.consume_any_ident_or_kw()?.value.clone(),
6063 "include" => node.include = self.parse_bracketed_dot_identifiers()?,
6067 _ => self.skip_value(),
6068 }
6069 }
6070 }
6071 if self.check(TokenType::RBrace) {
6072 self.advance();
6073 }
6074 }
6075 Ok(FlowStep::Weave(node))
6076 }
6077
6078 fn parse_use_step(&mut self) -> Result<FlowStep, ParseError> {
6079 let tok = self.current().clone();
6080 self.advance();
6081 let tool_name = self.consume_any_ident_or_kw()?.value.clone();
6082 let args = if self.check(TokenType::LParen) {
6093 UseArgs::Named(self.parse_named_arg_list()?)
6094 } else {
6095 let mut argument = String::new();
6096 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6097 let next = self.current().clone();
6098 if next.value == "on" {
6099 self.advance();
6100 argument = self.consume_any_ident_or_kw()?.value.clone();
6101 }
6102 }
6103 UseArgs::LegacyPositional(argument)
6104 };
6105 if self.check(TokenType::LBrace) {
6106 self.skip_braced_block()?;
6107 }
6108 Ok(FlowStep::UseTool(UseToolStep {
6109 tool_name,
6110 args,
6111 loc: Loc {
6112 line: tok.line,
6113 column: tok.column,
6114 },
6115 }))
6116 }
6117
6118 fn parse_named_arg_list(&mut self) -> Result<Vec<(String, String, String)>, ParseError> {
6124 self.consume(TokenType::LParen)?;
6125 let mut args = Vec::new();
6126 while !self.check(TokenType::RParen) {
6127 let name = self.consume_any_ident_or_kw()?.value;
6130 self.consume(TokenType::Assign)?;
6131 let value = self.parse_let_atom()?;
6132 let value_kind = self.last_let_value_kind.clone();
6136 args.push((name, value, value_kind));
6137 if self.check(TokenType::Comma) {
6138 self.advance();
6139 } else {
6140 break;
6141 }
6142 }
6143 self.consume(TokenType::RParen)?;
6144 Ok(args)
6145 }
6146
6147 fn parse_remember_step(&mut self) -> Result<FlowStep, ParseError> {
6148 let tok = self.current().clone();
6149 self.advance();
6150 let expr = self.consume_any_ident_or_kw()?.value.clone();
6151 let mut mem = String::new();
6152 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6153 let next = self.current().clone();
6154 if next.value == "in" || next.ttype == TokenType::In {
6155 self.advance();
6156 mem = self.consume_any_ident_or_kw()?.value.clone();
6157 }
6158 }
6159 Ok(FlowStep::Remember(RememberStep {
6160 expression: expr,
6161 memory_target: mem,
6162 loc: Loc {
6163 line: tok.line,
6164 column: tok.column,
6165 },
6166 }))
6167 }
6168
6169 fn parse_recall_step(&mut self) -> Result<FlowStep, ParseError> {
6170 let tok = self.current().clone();
6171 self.advance();
6172 let query = if self.check(TokenType::StringLit) {
6173 self.consume(TokenType::StringLit)?.value.clone()
6174 } else {
6175 self.consume_any_ident_or_kw()?.value.clone()
6176 };
6177 let mut mem = String::new();
6178 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6179 let next = self.current().clone();
6180 if next.value == "from" || next.ttype == TokenType::From {
6181 self.advance();
6182 mem = self.consume_any_ident_or_kw()?.value.clone();
6183 }
6184 }
6185 Ok(FlowStep::Recall(RecallStep {
6186 query,
6187 memory_source: mem,
6188 loc: Loc {
6189 line: tok.line,
6190 column: tok.column,
6191 },
6192 }))
6193 }
6194
6195 fn parse_hibernate_step(&mut self) -> Result<FlowStep, ParseError> {
6196 let tok = self.current().clone();
6197 self.advance();
6198 let mut event = String::new();
6199 let mut timeout = String::new();
6200 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6201 let first = self.consume_any_ident_or_kw()?.value.clone();
6206 if first == "until" && self.check(TokenType::StringLit) {
6207 event = self.consume(TokenType::StringLit)?.value.clone();
6208 } else {
6209 event = first;
6210 }
6211 }
6212 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6213 let next = self.current().clone();
6214 if next.ttype == TokenType::Duration {
6215 self.advance();
6216 timeout = next.value.clone();
6217 }
6218 }
6219 Ok(FlowStep::Hibernate(HibernateStep {
6220 event_name: event,
6221 timeout,
6222 loc: Loc {
6223 line: tok.line,
6224 column: tok.column,
6225 },
6226 }))
6227 }
6228
6229 fn parse_grad_step(&mut self) -> Result<FlowStep, ParseError> {
6238 let tok = self.current().clone();
6239 self.advance();
6240 let target = self.consume_any_ident_or_kw()?.value.clone();
6241 let mut wrt: Vec<String> = Vec::new();
6242 let mut output = String::new();
6243 if !self.at_declaration_start() && self.current().value == "wrt" {
6244 self.advance();
6245 if self.check(TokenType::LBracket) {
6246 wrt = self.parse_bracketed_identifiers()?;
6247 } else {
6248 wrt.push(self.consume_any_ident_or_kw()?.value.clone());
6249 }
6250 }
6251 if !self.at_declaration_start() && self.current().value == "as" {
6252 self.advance();
6253 output = self.consume_any_ident_or_kw()?.value.clone();
6254 }
6255 Ok(FlowStep::Grad(GradStep {
6256 target,
6257 wrt,
6258 output,
6259 loc: Loc {
6260 line: tok.line,
6261 column: tok.column,
6262 },
6263 }))
6264 }
6265
6266 fn parse_focus_step(&mut self) -> Result<FlowStep, ParseError> {
6267 let tok = self.current().clone();
6268 self.advance();
6269 let expression = if self.at_declaration_start()
6270 || self.check(TokenType::RBrace)
6271 || self.check(TokenType::Eof)
6272 {
6273 String::new()
6274 } else {
6275 self.consume_any_ident_or_kw()?.value.clone()
6276 };
6277 let mut where_expr = String::new();
6278 let mut select: Vec<String> = Vec::new();
6279 let mut output = String::new();
6280 if self.check(TokenType::LBrace) {
6281 self.advance();
6282 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6283 if self.check(TokenType::Comma) {
6284 self.advance();
6285 continue;
6286 }
6287 let f = self.current().value.clone();
6288 self.advance();
6289 if self.check(TokenType::Colon) {
6290 self.advance();
6291 match f.as_str() {
6292 "where" => {
6293 where_expr = self.consume(TokenType::StringLit)?.value.clone()
6294 }
6295 "select" => select = self.parse_bracketed_identifiers()?,
6296 "as" | "alias" => {
6297 output = self.consume_any_ident_or_kw()?.value.clone()
6298 }
6299 _ => self.skip_value(),
6300 }
6301 }
6302 }
6303 if self.check(TokenType::RBrace) {
6304 self.advance();
6305 }
6306 }
6307 Ok(FlowStep::Focus(FocusStep {
6308 expression,
6309 where_expr,
6310 select,
6311 output,
6312 loc: Loc {
6313 line: tok.line,
6314 column: tok.column,
6315 },
6316 }))
6317 }
6318
6319 fn parse_associate_step(&mut self) -> Result<FlowStep, ParseError> {
6320 let tok = self.current().clone();
6321 self.advance();
6322 let left = self.consume_any_ident_or_kw()?.value.clone();
6323 let mut right = String::new();
6324 let mut using = String::new();
6325 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6326 right = self.consume_any_ident_or_kw()?.value.clone();
6327 }
6328 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6329 let next = self.current().clone();
6330 if next.value == "using" {
6331 self.advance();
6332 using = self.consume_any_ident_or_kw()?.value.clone();
6333 }
6334 }
6335 let mut output = String::new();
6336 if self.check(TokenType::LBrace) {
6337 self.advance();
6338 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6339 let f = self.current().value.clone();
6340 self.advance();
6341 if self.check(TokenType::Colon) {
6342 self.advance();
6343 match f.as_str() {
6344 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6345 _ => self.skip_value(),
6346 }
6347 }
6348 }
6349 if self.check(TokenType::RBrace) {
6350 self.advance();
6351 }
6352 }
6353 Ok(FlowStep::Associate(AssociateStep {
6354 left,
6355 right,
6356 using_field: using,
6357 output,
6358 loc: Loc {
6359 line: tok.line,
6360 column: tok.column,
6361 },
6362 }))
6363 }
6364
6365 fn parse_aggregate_step(&mut self) -> Result<FlowStep, ParseError> {
6366 let tok = self.current().clone();
6367 self.advance();
6368 let target = self.consume_any_ident_or_kw()?.value.clone();
6369 let mut group_by = Vec::new();
6370 let mut alias = String::new();
6371 let mut compute: Vec<String> = Vec::new();
6372 let mut where_expr = String::new();
6373 if self.check(TokenType::LBrace) {
6374 self.advance();
6375 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6376 let f = self.current().value.clone();
6377 self.advance();
6378 if self.check(TokenType::Colon) {
6379 self.advance();
6380 match f.as_str() {
6381 "group_by" => group_by = self.parse_bracketed_identifiers()?,
6382 "alias" | "as" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6383 "compute" => compute = self.parse_bracketed_aggregates()?,
6386 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6388 _ => self.skip_value(),
6389 }
6390 }
6391 }
6392 if self.check(TokenType::RBrace) {
6393 self.advance();
6394 }
6395 }
6396 Ok(FlowStep::Aggregate(AggregateStep {
6397 target,
6398 group_by,
6399 alias,
6400 compute,
6401 where_expr,
6402 loc: Loc {
6403 line: tok.line,
6404 column: tok.column,
6405 },
6406 }))
6407 }
6408
6409 fn parse_explore_step(&mut self) -> Result<FlowStep, ParseError> {
6410 let tok = self.current().clone();
6411 self.advance();
6412 let target = self.consume_any_ident_or_kw()?.value.clone();
6413 let mut limit = None;
6414 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6415 if self.current().ttype == TokenType::Integer {
6416 limit = self.current().value.parse::<i64>().ok();
6417 self.advance();
6418 }
6419 }
6420 let mut output = String::new();
6421 if self.check(TokenType::LBrace) {
6422 self.advance();
6423 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6424 let f = self.current().value.clone();
6425 self.advance();
6426 if self.check(TokenType::Colon) {
6427 self.advance();
6428 match f.as_str() {
6429 "as" | "alias" => output = self.consume_any_ident_or_kw()?.value.clone(),
6430 _ => self.skip_value(),
6431 }
6432 }
6433 }
6434 if self.check(TokenType::RBrace) {
6435 self.advance();
6436 }
6437 }
6438 Ok(FlowStep::ExploreStep(ExploreStepNode {
6439 target,
6440 limit,
6441 output,
6442 loc: Loc {
6443 line: tok.line,
6444 column: tok.column,
6445 },
6446 }))
6447 }
6448
6449 fn parse_bracketed_aggregates(&mut self) -> Result<Vec<String>, ParseError> {
6452 let mut out = Vec::new();
6453 self.consume(TokenType::LBracket)?;
6454 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
6455 let name = self.consume_any_ident_or_kw()?.value.clone();
6456 if self.check(TokenType::LParen) {
6457 self.advance();
6458 let col = self.consume_any_ident_or_kw()?.value.clone();
6459 self.consume(TokenType::RParen)?;
6460 out.push(format!("{name}({col})"));
6461 } else {
6462 out.push(name);
6463 }
6464 if self.check(TokenType::Comma) {
6465 self.advance();
6466 }
6467 }
6468 self.consume(TokenType::RBracket)?;
6469 Ok(out)
6470 }
6471
6472 fn parse_ingest_step(&mut self) -> Result<FlowStep, ParseError> {
6487 let tok = self.current().clone();
6488 self.advance();
6489 let source = self.consume_any_ident_or_kw()?.value.clone();
6490 let mut target = String::new();
6491 let mut format = String::new();
6492 let mut max_bytes: Option<u64> = None;
6493 let mut max_rows: Option<u64> = None;
6494 if !self.at_declaration_start() && !self.check(TokenType::RBrace) {
6495 let next = self.current().clone();
6496 if next.value == "into" || next.ttype == TokenType::Into {
6497 self.advance();
6498 target = self.consume_any_ident_or_kw()?.value.clone();
6499 }
6500 }
6501 if self.check(TokenType::LBrace) {
6502 self.consume(TokenType::LBrace)?;
6503 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6504 if self.check(TokenType::Comma) {
6506 self.advance();
6507 continue;
6508 }
6509 let entry = self.current().clone();
6510 match entry.value.as_str() {
6511 "format" => {
6512 self.advance();
6513 self.consume(TokenType::Colon)?;
6514 format = self.consume_any_ident_or_kw()?.value.clone();
6515 }
6516 "limits" => {
6517 self.advance();
6518 self.consume(TokenType::LBrace)?;
6519 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6520 let bound = self.current().clone();
6521 self.advance();
6522 self.consume(TokenType::Colon)?;
6523 let num_tok = self.consume(TokenType::Integer)?.clone();
6524 let value = num_tok.value.parse::<u64>().map_err(|_| ParseError {
6525 message: format!(
6526 "ingest `limits` bound `{}` must be a non-negative \
6527 integer byte/row count, got `{}`.",
6528 bound.value, num_tok.value
6529 ),
6530 line: num_tok.line,
6531 column: num_tok.column,
6532 ..Default::default()
6533 })?;
6534 match bound.value.as_str() {
6535 "max_bytes" => max_bytes = Some(value),
6536 "max_rows" => max_rows = Some(value),
6537 other => {
6538 return Err(ParseError {
6539 message: format!(
6540 "Unknown ingest limit `{other}`. The closed \
6541 limits grammar is `max_bytes: <N>` and \
6542 `max_rows: <N>` — bounds enforced on the raw \
6543 stream BEFORE parsing.",
6544 ),
6545 line: bound.line,
6546 column: bound.column,
6547 ..Default::default()
6548 });
6549 }
6550 }
6551 if self.check(TokenType::Comma) {
6552 self.advance();
6553 }
6554 }
6555 self.consume(TokenType::RBrace)?;
6556 }
6557 other => {
6558 return Err(ParseError {
6559 message: format!(
6560 "Unknown entry `{other}` in ingest body. The closed \
6561 grammar is `format: csv|json` and \
6562 `limits {{ max_bytes: <N>, max_rows: <N> }}`.",
6563 ),
6564 line: entry.line,
6565 column: entry.column,
6566 ..Default::default()
6567 });
6568 }
6569 }
6570 }
6571 self.consume(TokenType::RBrace)?;
6572 }
6573 Ok(FlowStep::Ingest(IngestStep {
6574 source,
6575 target,
6576 format,
6577 max_bytes,
6578 max_rows,
6579 loc: Loc {
6580 line: tok.line,
6581 column: tok.column,
6582 },
6583 }))
6584 }
6585
6586 fn at_navigate_field(&self) -> bool {
6593 const FIELDS: &[&str] = &[
6594 "corpus", "query", "trail", "as", "from", "budget", "where",
6602 "depth", "recall",
6603 ];
6604 self.field_ahead(FIELDS)
6605 }
6606
6607 fn at_drill_field(&self) -> bool {
6609 const FIELDS: &[&str] = &["subtree", "path", "query", "as"];
6612 self.field_ahead(FIELDS)
6613 }
6614
6615 fn field_ahead(&self, names: &[&str]) -> bool {
6617 let cur = self.current();
6618 if !names.contains(&cur.value.as_str()) {
6619 return false;
6620 }
6621 self.tokens
6622 .get(self.pos + 1)
6623 .is_some_and(|t| t.ttype == TokenType::Colon)
6624 }
6625
6626 fn parse_config_key(&mut self) -> Result<String, ParseError> {
6636 if self.check(TokenType::StringLit) {
6637 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6638 }
6639 let scheme = self.consume_any_ident_or_kw()?.value.clone();
6640 if self.check(TokenType::Colon) {
6641 self.advance();
6642 let key = self.consume_any_ident_or_kw()?.value.clone();
6643 return Ok(format!("{scheme}:{key}"));
6644 }
6645 Ok(scheme)
6646 }
6647
6648 fn parse_pix_value(&mut self) -> Result<String, ParseError> {
6653 if self.check(TokenType::StringLit) {
6654 return Ok(self.consume(TokenType::StringLit)?.value.clone());
6655 }
6656 Ok(self.consume_any_ident_or_kw()?.value.clone())
6657 }
6658
6659 fn parse_navigate_step(&mut self) -> Result<FlowStep, ParseError> {
6660 let tok = self.current().clone();
6661 self.advance();
6662 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6663 let mut node = NavigateStep {
6664 depth: None,
6665 pix_name,
6666 corpus_name: String::new(),
6667 query_expr: String::new(),
6668 trail_enabled: false,
6669 output_name: String::new(),
6670 seed: String::new(),
6671 budget: None,
6672 where_expr: String::new(),
6673 loc: Loc {
6674 line: tok.line,
6675 column: tok.column,
6676 },
6677 };
6678 if !self.check(TokenType::LBrace) {
6692 while self.at_navigate_field() {
6693 let f = self.current().value.clone();
6694 self.advance();
6695 self.consume(TokenType::Colon)?;
6696 match f.as_str() {
6697 "corpus" => node.corpus_name = self.consume_any_ident_or_kw()?.value.clone(),
6698 "query" => node.query_expr = self.parse_pix_value()?,
6699 "trail" => {
6700 let v = self.consume_any_ident_or_kw()?.value;
6701 node.trail_enabled = matches!(v.as_str(), "true" | "enabled" | "on");
6702 }
6703 "output" | "as" => {
6704 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6705 }
6706 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6707 "budget" => node.budget = self.parse_optional_int(),
6708 "where" => node.where_expr = self.parse_pix_value()?,
6709 "depth" => node.depth = self.parse_optional_int(),
6710 "recall" => {
6718 let mode = self.consume_any_ident_or_kw()?.value.clone();
6719 if node.seed.is_empty() {
6720 node.seed = format!("recall:{mode}");
6721 }
6722 }
6723 _ => self.skip_value(),
6724 }
6725 }
6726 }
6727 if self.check(TokenType::LBrace) {
6728 self.advance();
6729 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6730 let f = self.current().value.clone();
6731 self.advance();
6732 if self.check(TokenType::Colon) {
6733 self.advance();
6734 match f.as_str() {
6735 "corpus" => {
6736 node.corpus_name = self.consume_any_ident_or_kw()?.value.clone()
6737 }
6738 "query" => node.query_expr = self.parse_pix_value()?,
6739 "trail" => {
6740 let v = self.consume_any_ident_or_kw()?.value;
6741 node.trail_enabled =
6742 matches!(v.as_str(), "true" | "enabled" | "on");
6743 }
6744 "output" | "as" => {
6745 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6746 }
6747 "from" => node.seed = self.consume_any_ident_or_kw()?.value.clone(),
6749 "budget" => node.budget = self.parse_optional_int(),
6750 "where" => {
6758 node.where_expr = self.consume(TokenType::StringLit)?.value.clone()
6759 }
6760 _ => self.skip_value(),
6761 }
6762 }
6763 }
6764 if self.check(TokenType::RBrace) {
6765 self.advance();
6766 }
6767 }
6768 Ok(FlowStep::Navigate(node))
6769 }
6770
6771 fn parse_drill_step(&mut self) -> Result<FlowStep, ParseError> {
6772 let tok = self.current().clone();
6773 self.advance();
6774 let pix_name = self.consume_any_ident_or_kw()?.value.clone();
6775 let mut node = DrillStep {
6776 pix_name,
6777 subtree_path: String::new(),
6778 query_expr: String::new(),
6779 output_name: String::new(),
6780 loc: Loc {
6781 line: tok.line,
6782 column: tok.column,
6783 },
6784 };
6785 if self.current().value == "into" {
6789 self.advance();
6790 node.subtree_path = if self.check(TokenType::StringLit) {
6795 self.consume(TokenType::StringLit)?.value.clone()
6796 } else {
6797 self.parse_dotted_identifier()?
6798 };
6799 }
6800 if !self.check(TokenType::LBrace) {
6801 while self.at_drill_field() {
6802 let f = self.current().value.clone();
6803 self.advance();
6804 self.consume(TokenType::Colon)?;
6805 match f.as_str() {
6806 "subtree" | "path" => {
6807 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6808 }
6809 "query" => node.query_expr = self.parse_pix_value()?,
6810 "output" | "as" => {
6811 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6812 }
6813 _ => self.skip_value(),
6814 }
6815 }
6816 }
6817 if self.check(TokenType::LBrace) {
6818 self.advance();
6819 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6820 let f = self.current().value.clone();
6821 self.advance();
6822 if self.check(TokenType::Colon) {
6823 self.advance();
6824 match f.as_str() {
6825 "subtree" | "path" => {
6826 node.subtree_path = self.consume(TokenType::StringLit)?.value.clone()
6827 }
6828 "query" => node.query_expr = self.parse_pix_value()?,
6829 "output" | "as" => {
6830 node.output_name = self.consume_any_ident_or_kw()?.value.clone()
6831 }
6832 _ => self.skip_value(),
6833 }
6834 }
6835 }
6836 if self.check(TokenType::RBrace) {
6837 self.advance();
6838 }
6839 }
6840 Ok(FlowStep::Drill(node))
6841 }
6842
6843 fn parse_corroborate_step(&mut self) -> Result<FlowStep, ParseError> {
6844 let tok = self.current().clone();
6845 self.advance();
6846 let nav_ref = self.consume_any_ident_or_kw()?.value.clone();
6847 let mut output = String::new();
6848 if self.check(TokenType::Arrow) {
6849 self.advance();
6850 output = self.consume_any_ident_or_kw()?.value.clone();
6851 }
6852 Ok(FlowStep::Corroborate(CorroborateStep {
6853 navigate_ref: nav_ref,
6854 output_name: output,
6855 loc: Loc {
6856 line: tok.line,
6857 column: tok.column,
6858 },
6859 }))
6860 }
6861
6862 fn parse_listen_step(&mut self) -> Result<FlowStep, ParseError> {
6863 let tok = self.current().clone();
6864 self.advance();
6865 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
6869 (self.consume(TokenType::StringLit)?.value.clone(), false)
6870 } else {
6871 (self.consume_any_ident_or_kw()?.value.clone(), true)
6872 };
6873 let mut alias = String::new();
6874 if !self.at_declaration_start()
6875 && !self.check(TokenType::RBrace)
6876 && !self.check(TokenType::LBrace)
6877 {
6878 let next = self.current().clone();
6879 if next.value == "as" || next.ttype == TokenType::As {
6880 self.advance();
6881 alias = self.consume_any_ident_or_kw()?.value.clone();
6882 }
6883 }
6884 let body = self.parse_listener_body()?;
6888 Ok(FlowStep::Listen(ListenStep {
6889 channel,
6890 channel_is_ref,
6891 event_alias: alias,
6892 body,
6893 loc: Loc {
6894 line: tok.line,
6895 column: tok.column,
6896 },
6897 }))
6898 }
6899
6900 fn parse_listener_body(&mut self) -> Result<Vec<FlowStep>, ParseError> {
6905 let mut body = Vec::new();
6906 if self.check(TokenType::LBrace) {
6907 self.advance(); while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6909 body.push(self.parse_flow_step()?);
6910 }
6911 self.consume(TokenType::RBrace)?;
6912 }
6913 Ok(body)
6914 }
6915
6916 fn parse_retrieve_step(&mut self) -> Result<FlowStep, ParseError> {
6923 let tok = self.current().clone();
6924 self.advance();
6925 if self.check(TokenType::From) || self.current().value == "from" {
6928 self.advance();
6929 }
6930 let store = self.consume_any_ident_or_kw()?.value.clone();
6931 let mut where_expr = String::new();
6932 let mut alias = String::new();
6933 let mut order_by = String::new();
6934 let mut limit_expr = String::new();
6935 let mut aggregate = String::new();
6936 let mut group_by = String::new();
6937 let mut cache = String::new();
6938 loop {
6945 match self.current().value.as_str() {
6946 "where" if !self.check(TokenType::LBrace) => {
6947 self.advance();
6948 where_expr = self.consume(TokenType::StringLit)?.value.clone();
6949 }
6950 "as" => {
6951 self.advance();
6952 alias = self.consume_any_ident_or_kw()?.value.clone();
6953 }
6954 _ => break,
6955 }
6956 }
6957 if self.check(TokenType::LBrace) {
6958 self.advance();
6959 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
6960 let f = self.current().value.clone();
6961 self.advance();
6962 if self.check(TokenType::Colon) {
6963 self.advance();
6964 match f.as_str() {
6965 "where" => where_expr = self.consume(TokenType::StringLit)?.value.clone(),
6966 "as" | "alias" => alias = self.consume_any_ident_or_kw()?.value.clone(),
6967 "order_by" => {
6970 order_by = self.consume(TokenType::StringLit)?.value.clone()
6971 }
6972 "limit" => {
6977 let t = self.current().clone();
6978 match t.ttype {
6979 TokenType::Integer | TokenType::StringLit => {
6980 limit_expr = t.value.clone();
6981 self.advance();
6982 }
6983 _ => self.skip_value(),
6984 }
6985 }
6986 "aggregate" => {
6993 aggregate = self.consume(TokenType::StringLit)?.value.clone()
6994 }
6995 "group_by" => {
6996 group_by = self.consume(TokenType::StringLit)?.value.clone()
6997 }
6998 "cache" => cache = self.consume_any_ident_or_kw()?.value.clone(),
7004 _ => self.skip_value(),
7005 }
7006 }
7007 }
7008 if self.check(TokenType::RBrace) {
7009 self.advance();
7010 }
7011 }
7012 Ok(FlowStep::Retrieve(RetrieveStep {
7013 store_name: store,
7014 where_expr,
7015 alias,
7016 order_by,
7017 limit_expr,
7018 aggregate,
7019 group_by,
7020 cache,
7021 loc: Loc {
7022 line: tok.line,
7023 column: tok.column,
7024 },
7025 }))
7026 }
7027
7028 fn parse_store_where_step(
7041 &mut self,
7042 ) -> Result<(Loc, String, String), ParseError> {
7043 let tok = self.current().clone();
7044 self.advance(); let store = if self.at_declaration_start()
7046 || self.check(TokenType::RBrace)
7047 || self.check(TokenType::Eof)
7048 {
7049 String::new()
7050 } else {
7051 self.consume_any_ident_or_kw()?.value.clone()
7052 };
7053 let mut where_expr = String::new();
7054 if self.check(TokenType::LBrace) {
7055 self.advance();
7056 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7057 let field = self.current().value.clone();
7058 self.advance();
7059 if self.check(TokenType::Colon) {
7060 self.advance();
7061 match field.as_str() {
7062 "where" => {
7063 where_expr =
7064 self.consume(TokenType::StringLit)?.value.clone()
7065 }
7066 _ => self.skip_value(),
7067 }
7068 }
7069 }
7070 if self.check(TokenType::RBrace) {
7071 self.advance();
7072 }
7073 }
7074 Ok((
7075 Loc {
7076 line: tok.line,
7077 column: tok.column,
7078 },
7079 store,
7080 where_expr,
7081 ))
7082 }
7083
7084 fn parse_persist_step(&mut self) -> Result<FlowStep, ParseError> {
7103 let tok = self.current().clone();
7104 self.advance(); if self.current().value == "into" && !self.check(TokenType::LBrace) {
7108 self.advance();
7109 }
7110 let store = if self.at_declaration_start()
7111 || self.check(TokenType::LBrace)
7112 || self.check(TokenType::RBrace)
7113 || self.check(TokenType::Eof)
7114 {
7115 String::new()
7116 } else {
7117 self.consume_any_ident_or_kw()?.value.clone()
7118 };
7119 let mut fields: Vec<(String, String)> = Vec::new();
7120 if self.check(TokenType::LBrace) {
7121 self.advance();
7122 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7123 let col = self.current().value.clone();
7124 self.advance();
7125 if self.check(TokenType::Colon) {
7126 self.advance();
7127 let value = if self.check(TokenType::StringLit) {
7128 self.consume(TokenType::StringLit)?.value.clone()
7129 } else if self.check(TokenType::RBrace)
7130 || self.check(TokenType::Eof)
7131 || self.check(TokenType::Colon)
7132 {
7133 String::new()
7134 } else {
7135 let v = self.current().clone();
7136 self.advance();
7137 v.value.clone()
7138 };
7139 fields.push((col, value));
7140 }
7141 }
7142 if self.check(TokenType::RBrace) {
7143 self.advance();
7144 }
7145 }
7146 Ok(FlowStep::Persist(PersistStep {
7147 store_name: store,
7148 fields,
7149 loc: Loc {
7150 line: tok.line,
7151 column: tok.column,
7152 },
7153 }))
7154 }
7155
7156 fn parse_mutate_step(&mut self) -> Result<FlowStep, ParseError> {
7170 let tok = self.current().clone();
7171 self.advance(); let store = if self.at_declaration_start()
7173 || self.check(TokenType::LBrace)
7174 || self.check(TokenType::RBrace)
7175 || self.check(TokenType::Eof)
7176 {
7177 String::new()
7178 } else {
7179 self.consume_any_ident_or_kw()?.value.clone()
7180 };
7181 let mut where_expr = String::new();
7182 let mut fields: Vec<(String, String)> = Vec::new();
7183 if self.check(TokenType::LBrace) {
7184 self.advance();
7185 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7186 let key = self.current().value.clone();
7187 self.advance();
7188 if self.check(TokenType::Colon) {
7189 self.advance();
7190 if key == "where" {
7191 where_expr =
7192 self.consume(TokenType::StringLit)?.value.clone();
7193 } else {
7194 let value = if self.check(TokenType::StringLit) {
7195 self.consume(TokenType::StringLit)?.value.clone()
7196 } else if self.check(TokenType::RBrace)
7197 || self.check(TokenType::Eof)
7198 || self.check(TokenType::Colon)
7199 {
7200 String::new()
7201 } else {
7202 let v = self.current().clone();
7203 self.advance();
7204 v.value.clone()
7205 };
7206 fields.push((key, value));
7207 }
7208 }
7209 }
7210 if self.check(TokenType::RBrace) {
7211 self.advance();
7212 }
7213 }
7214 Ok(FlowStep::Mutate(MutateStep {
7215 store_name: store,
7216 where_expr,
7217 fields,
7218 loc: Loc {
7219 line: tok.line,
7220 column: tok.column,
7221 },
7222 }))
7223 }
7224
7225 fn parse_agent(&mut self) -> Result<AgentDefinition, ParseError> {
7228 let tok = self.consume(TokenType::Agent)?;
7229 let name = self.consume(TokenType::Identifier)?.value;
7230 let mut node = AgentDefinition {
7231 name,
7232 goal: String::new(),
7233 tools: Vec::new(),
7234 memory_ref: String::new(),
7235 strategy: String::new(),
7236 on_stuck: String::new(),
7237 shield_ref: String::new(),
7238 max_iterations: None,
7239 max_tokens: None,
7240 max_time: String::new(),
7241 max_cost: None,
7242 return_type: String::new(),
7243 body: 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 if self.check(TokenType::LParen) {
7257 let mut depth = 0usize;
7258 while !self.check(TokenType::Eof) {
7259 if self.check(TokenType::LParen) {
7260 depth += 1;
7261 } else if self.check(TokenType::RParen) {
7262 depth -= 1;
7263 if depth == 0 {
7264 self.advance();
7265 break;
7266 }
7267 }
7268 self.advance();
7269 }
7270 }
7271 if self.check(TokenType::Arrow) {
7272 self.advance();
7273 node.return_type = self.parse_output_type_string()?;
7274 }
7275 self.consume(TokenType::LBrace)?;
7276 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7282 if self.check(TokenType::Step) {
7283 let step = self.parse_step()?;
7284 node.body.push(step);
7285 continue;
7286 }
7287 let field = self.current().clone();
7288 let field_name = field.value.clone();
7289 self.advance();
7290 if !self.check(TokenType::Colon) {
7291 return Err(self.error(&format!(
7292 "unexpected `{field_name}` inside `agent {}` — an agent block holds \
7293 `field: value` pairs and `step Name {{ … }}` blocks; valid fields: \
7294 goal, tools, memory, strategy, on_stuck, shield, max_iterations, \
7295 max_tokens, max_time, max_cost, return",
7296 node.name
7297 )));
7298 }
7299 self.advance();
7300 match field_name.as_str() {
7301 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
7302 "tools" => node.tools = self.parse_bracketed_identifiers()?,
7303 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
7304 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7305 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
7306 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
7307 "max_iterations" => node.max_iterations = self.parse_optional_int(),
7308 "max_tokens" => node.max_tokens = self.parse_optional_int(),
7309 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
7310 "max_cost" => node.max_cost = self.parse_optional_float(),
7311 "return" => node.return_type = self.parse_output_type_string()?,
7312 other => {
7313 return Err(self.error(&format!(
7314 "unknown agent field `{other}` in `agent {}` — the agent block is a \
7315 closed catalog; valid fields: goal, tools, memory, strategy, \
7316 on_stuck, shield, max_iterations, max_tokens, max_time, max_cost, \
7317 return (plus `step Name {{ … }}` blocks for `strategy: custom`)",
7318 node.name
7319 )));
7320 }
7321 }
7322 }
7323 self.consume(TokenType::RBrace)?;
7324 Ok(node)
7325 }
7326
7327 fn parse_extension(&mut self) -> Result<ExtensionDefinition, ParseError> {
7332 let tok = self.consume(TokenType::Extension)?;
7333 let name = self.consume(TokenType::Identifier)?.value;
7334 let mut node = ExtensionDefinition {
7335 name,
7336 category: String::new(),
7337 members: Vec::new(),
7338 loc: Loc {
7339 line: tok.line,
7340 column: tok.column,
7341 },
7342 leading_trivia: Vec::new(),
7343 trailing_trivia: Vec::new(),
7344 };
7345 self.consume(TokenType::LBrace)?;
7346 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7347 let field_name = self.current().value.clone();
7348 self.advance();
7349 if self.check(TokenType::Colon) {
7350 self.advance();
7351 match field_name.as_str() {
7352 "category" => {
7353 node.category = self.consume_any_ident_or_kw()?.value.clone()
7354 }
7355 "members" => node.members = self.parse_extension_members()?,
7356 _ => self.skip_value(),
7357 }
7358 } else if self.check(TokenType::LBrace) {
7359 self.skip_braced_block()?;
7360 }
7361 }
7362 self.consume(TokenType::RBrace)?;
7363 Ok(node)
7364 }
7365
7366 fn parse_extension_members(&mut self) -> Result<Vec<ExtensionMember>, ParseError> {
7370 let mut members = Vec::new();
7371 self.consume(TokenType::LBracket)?;
7372 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7373 let name_tok = self.consume(TokenType::StringLit)?;
7374 let mut member = ExtensionMember {
7375 name: name_tok.value.clone(),
7376 semantics: None,
7377 default_confidence: None,
7378 loc: Loc {
7379 line: name_tok.line,
7380 column: name_tok.column,
7381 },
7382 };
7383 if self.check(TokenType::Colon) {
7385 self.advance();
7386 self.consume(TokenType::LBrace)?;
7387 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7388 let mkey = self.current().value.clone();
7389 self.advance();
7390 if self.check(TokenType::Colon) {
7391 self.advance();
7392 match mkey.as_str() {
7393 "semantics" => {
7394 member.semantics =
7395 Some(self.consume(TokenType::StringLit)?.value.clone())
7396 }
7397 "default_confidence" => {
7398 member.default_confidence = self.parse_optional_float()
7399 }
7400 _ => self.skip_value(),
7401 }
7402 }
7403 if self.check(TokenType::Comma) {
7404 self.advance();
7405 }
7406 }
7407 self.consume(TokenType::RBrace)?;
7408 }
7409 members.push(member);
7410 if self.check(TokenType::Comma) {
7411 self.advance();
7412 }
7413 }
7414 self.consume(TokenType::RBracket)?;
7415 Ok(members)
7416 }
7417
7418 fn parse_window(&mut self) -> Result<WindowDefinition, ParseError> {
7421 let tok = self.consume(TokenType::Window)?;
7422 let name = self.consume(TokenType::Identifier)?.value;
7423 let mut node = WindowDefinition {
7424 name,
7425 timezone: String::new(),
7426 allow: Vec::new(),
7427 exclude: Vec::new(),
7428 on_outside: String::new(),
7429 loc: Loc {
7430 line: tok.line,
7431 column: tok.column,
7432 },
7433 leading_trivia: Vec::new(),
7434 trailing_trivia: Vec::new(),
7435 };
7436 self.consume(TokenType::LBrace)?;
7437 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7438 let field_name = self.consume_any_ident_or_kw()?.value;
7439 self.consume(TokenType::Colon)?;
7440 match field_name.as_str() {
7441 "timezone" => node.timezone = self.consume(TokenType::StringLit)?.value,
7442 "allow" => node.allow = self.parse_window_allow()?,
7443 "exclude" => node.exclude = self.parse_window_exclude()?,
7444 "on_outside" => node.on_outside = self.consume_any_ident_or_kw()?.value,
7445 _ => self.skip_value(),
7446 }
7447 }
7448 self.consume(TokenType::RBrace)?;
7449 Ok(node)
7450 }
7451
7452 fn parse_window_allow(&mut self) -> Result<Vec<WindowSpan>, ParseError> {
7454 self.consume(TokenType::LBracket)?;
7455 let mut spans = Vec::new();
7456 if !self.check(TokenType::RBracket) {
7457 spans.push(self.parse_window_span()?);
7458 while self.check(TokenType::Comma) {
7459 self.advance();
7460 if self.check(TokenType::RBracket) {
7461 break; }
7463 spans.push(self.parse_window_span()?);
7464 }
7465 }
7466 self.consume(TokenType::RBracket)?;
7467 Ok(spans)
7468 }
7469
7470 fn parse_window_exclude(&mut self) -> Result<Vec<String>, ParseError> {
7474 self.consume(TokenType::LBracket)?;
7475 let mut dates = Vec::new();
7476 if !self.check(TokenType::RBracket) {
7477 dates.push(self.consume(TokenType::StringLit)?.value);
7478 while self.check(TokenType::Comma) {
7479 self.advance();
7480 if self.check(TokenType::RBracket) {
7481 break; }
7483 dates.push(self.consume(TokenType::StringLit)?.value);
7484 }
7485 }
7486 self.consume(TokenType::RBracket)?;
7487 Ok(dates)
7488 }
7489
7490 fn parse_window_span(&mut self) -> Result<WindowSpan, ParseError> {
7492 let tok = self.consume(TokenType::LBrace)?;
7493 let mut span = WindowSpan {
7494 day_start: String::new(),
7495 day_end: String::new(),
7496 hour_start: 0,
7497 hour_end: 0,
7498 loc: Loc {
7499 line: tok.line,
7500 column: tok.column,
7501 },
7502 };
7503 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7504 let field = self.consume_any_ident_or_kw()?.value;
7505 self.consume(TokenType::Colon)?;
7506 match field.as_str() {
7507 "days" => {
7508 span.day_start = self.consume_any_ident_or_kw()?.value;
7509 self.consume(TokenType::DotDot)?;
7510 span.day_end = self.consume_any_ident_or_kw()?.value;
7511 }
7512 "hours" => {
7513 span.hour_start = self.consume_number()? as i64;
7514 self.consume(TokenType::DotDot)?;
7515 span.hour_end = self.consume_number()? as i64;
7516 }
7517 _ => self.skip_value(),
7518 }
7519 if self.check(TokenType::Comma) {
7520 self.advance();
7521 }
7522 }
7523 self.consume(TokenType::RBrace)?;
7524 Ok(span)
7525 }
7526
7527 fn parse_attest(&mut self) -> Result<crate::ast::AttestDefinition, ParseError> {
7535 let tok = self.consume(TokenType::Attest)?;
7536 let name = self.consume(TokenType::Identifier)?.value;
7537 let mut node = crate::ast::AttestDefinition {
7538 name,
7539 loc: Loc {
7540 line: tok.line,
7541 column: tok.column,
7542 },
7543 ..Default::default()
7544 };
7545 self.consume(TokenType::LBrace)?;
7546 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7547 let field_name = self.current().value.clone();
7548 let field_loc = Loc {
7549 line: self.current().line,
7550 column: self.current().column,
7551 };
7552 self.advance();
7553 if self.check(TokenType::Colon) {
7554 self.advance();
7555 match field_name.as_str() {
7556 "for" => node.for_type = self.consume_any_ident_or_kw()?.value.clone(),
7560 "basis" => node.basis = self.consume_any_ident_or_kw()?.value.clone(),
7561 "residual" => node.residual = self.parse_bracketed_identifiers()?,
7562 "by" => node.by = self.consume(TokenType::StringLit)?.value.clone(),
7563 "on" => node.on = self.consume(TokenType::StringLit)?.value.clone(),
7564 _ => {
7565 node.unknown_fields.push((field_name.clone(), field_loc));
7566 self.skip_value()
7567 }
7568 }
7569 } else if self.check(TokenType::LBrace) {
7570 self.skip_braced_block()?;
7571 }
7572 }
7573 self.consume(TokenType::RBrace)?;
7574 Ok(node)
7575 }
7576
7577 fn parse_shield(&mut self) -> Result<ShieldDefinition, ParseError> {
7578 let tok = self.consume(TokenType::Shield)?;
7579 let name = self.consume(TokenType::Identifier)?.value;
7580 let mut node = ShieldDefinition {
7581 suppress: Vec::new(),
7582 generalise: Vec::new(),
7583 declassifies: Vec::new(),
7584 name,
7585 scan: Vec::new(),
7586 strategy: String::new(),
7587 on_breach: String::new(),
7588 severity: String::new(),
7589 quarantine: String::new(),
7590 max_retries: None,
7591 confidence_threshold: None,
7592 allow_tools: Vec::new(),
7593 deny_tools: Vec::new(),
7594 sandbox: None,
7595 redact: Vec::new(),
7596 log: String::new(),
7597 deflect_message: String::new(),
7598 taint: String::new(),
7599 compliance: Vec::new(),
7600 sign: String::new(),
7601 unknown_fields: Vec::new(),
7602 loc: Loc {
7603 line: tok.line,
7604 column: tok.column,
7605 },
7606 leading_trivia: Vec::new(),
7607 trailing_trivia: Vec::new(),
7608 };
7609 self.consume(TokenType::LBrace)?;
7610 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7611 let field_name = self.current().value.clone();
7612 let field_loc = Loc {
7613 line: self.current().line,
7614 column: self.current().column,
7615 };
7616 self.advance();
7617 if self.check(TokenType::Colon) {
7618 self.advance();
7619 match field_name.as_str() {
7620 "scan" => node.scan = self.parse_bracketed_identifiers()?,
7621 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
7622 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
7623 "severity" => node.severity = self.consume_any_ident_or_kw()?.value.clone(),
7624 "quarantine" => {
7625 node.quarantine = self.consume(TokenType::StringLit)?.value.clone()
7626 }
7627 "max_retries" => node.max_retries = self.parse_optional_int(),
7628 "confidence_threshold" => {
7629 node.confidence_threshold = self.parse_optional_float()
7630 }
7631 "allow_tools" => node.allow_tools = self.parse_bracketed_identifiers()?,
7632 "deny_tools" => node.deny_tools = self.parse_bracketed_identifiers()?,
7633 "sandbox" => {
7634 node.sandbox = Some(self.consume_any_ident_or_kw()?.value == "true")
7635 }
7636 "redact" => node.redact = self.parse_bracketed_identifiers()?,
7637 "declassifies" => node.declassifies = self.parse_bracketed_identifiers()?,
7640 "suppress" => node.suppress = self.parse_bracketed_identifiers()?,
7641 "generalise" => node.generalise = self.parse_bracketed_identifiers()?,
7642 "log" => node.log = self.consume_any_ident_or_kw()?.value.clone(),
7643 "deflect_message" => {
7644 node.deflect_message = self.consume(TokenType::StringLit)?.value.clone()
7645 }
7646 "taint" => node.taint = self.consume_any_ident_or_kw()?.value.clone(),
7647 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
7649 "sign" => node.sign = self.consume_any_ident_or_kw()?.value.clone(),
7652 _ => {
7656 node.unknown_fields.push((field_name.clone(), field_loc));
7657 self.skip_value()
7658 }
7659 }
7660 } else if self.check(TokenType::LBrace) {
7661 self.skip_braced_block()?;
7662 }
7663 }
7664 self.consume(TokenType::RBrace)?;
7665 Ok(node)
7666 }
7667
7668 fn parse_pix(&mut self) -> Result<PixDefinition, ParseError> {
7669 let tok = self.consume(TokenType::Pix)?;
7670 let name = self.consume(TokenType::Identifier)?.value;
7671 let mut node = PixDefinition {
7672 name,
7673 source: String::new(),
7674 depth: None,
7675 branching: None,
7676 model: String::new(),
7677 loc: Loc {
7678 line: tok.line,
7679 column: tok.column,
7680 },
7681 leading_trivia: Vec::new(),
7682 trailing_trivia: Vec::new(),
7683 };
7684 self.consume(TokenType::LBrace)?;
7685 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7686 let field_name = self.current().value.clone();
7687 self.advance();
7688 if self.check(TokenType::Colon) {
7689 self.advance();
7690 match field_name.as_str() {
7691 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7692 "depth" => node.depth = self.parse_optional_int(),
7693 "branching" => node.branching = self.parse_optional_int(),
7694 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7695 _ => self.skip_value(),
7696 }
7697 } else if self.check(TokenType::LBrace) {
7698 self.skip_braced_block()?;
7699 }
7700 }
7701 self.consume(TokenType::RBrace)?;
7702 Ok(node)
7703 }
7704
7705 fn parse_ledger(&mut self) -> Result<LedgerDefinition, ParseError> {
7711 let tok = self.consume(TokenType::Ledger)?;
7712 let name = self.consume(TokenType::Identifier)?.value;
7713 let mut node = LedgerDefinition {
7714 name,
7715 source: String::new(),
7716 depth: None,
7717 branching: None,
7718 model: String::new(),
7719 loc: Loc {
7720 line: tok.line,
7721 column: tok.column,
7722 },
7723 leading_trivia: Vec::new(),
7724 trailing_trivia: Vec::new(),
7725 };
7726 self.consume(TokenType::LBrace)?;
7727 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7728 let field_name = self.current().value.clone();
7729 self.advance();
7730 if self.check(TokenType::Colon) {
7731 self.advance();
7732 match field_name.as_str() {
7733 "source" => node.source = self.consume(TokenType::StringLit)?.value.clone(),
7734 "depth" => node.depth = self.parse_optional_int(),
7735 "branching" => node.branching = self.parse_optional_int(),
7736 "model" => node.model = self.consume_any_ident_or_kw()?.value.clone(),
7737 _ => self.skip_value(),
7738 }
7739 } else if self.check(TokenType::LBrace) {
7740 self.skip_braced_block()?;
7741 }
7742 }
7743 self.consume(TokenType::RBrace)?;
7744 Ok(node)
7745 }
7746
7747 fn parse_psyche(&mut self) -> Result<PsycheDefinition, ParseError> {
7748 let tok = self.consume(TokenType::Psyche)?;
7749 let name = self.consume(TokenType::Identifier)?.value;
7750 let mut node = PsycheDefinition {
7751 name,
7752 dimensions: Vec::new(),
7753 manifold_noise: None,
7754 manifold_momentum: None,
7755 safety_constraints: Vec::new(),
7756 quantum_enabled: None,
7757 inference_mode: String::new(),
7758 loc: Loc {
7759 line: tok.line,
7760 column: tok.column,
7761 },
7762 leading_trivia: Vec::new(),
7763 trailing_trivia: Vec::new(),
7764 };
7765 self.consume(TokenType::LBrace)?;
7766 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7767 let field_name = self.current().value.clone();
7768 self.advance();
7769 if self.check(TokenType::Colon) {
7770 self.advance();
7771 match field_name.as_str() {
7772 "dimensions" => node.dimensions = self.parse_bracketed_identifiers()?,
7773 "manifold_noise" => node.manifold_noise = self.parse_optional_float(),
7774 "manifold_momentum" => node.manifold_momentum = self.parse_optional_float(),
7775 "safety_constraints" | "safety" => {
7780 node.safety_constraints = self.parse_bracketed_identifiers()?
7781 }
7782 "quantum_enabled" => {
7783 node.quantum_enabled = Some(self.consume_any_ident_or_kw()?.value == "true")
7784 }
7785 "inference_mode" => {
7786 node.inference_mode = self.consume_any_ident_or_kw()?.value.clone()
7787 }
7788 _ => self.skip_value(),
7789 }
7790 } else if self.check(TokenType::LBrace) {
7791 self.skip_braced_block()?;
7792 }
7793 }
7794 self.consume(TokenType::RBrace)?;
7795 Ok(node)
7796 }
7797
7798 fn parse_corpus(&mut self) -> Result<CorpusDefinition, ParseError> {
7799 let tok = self.consume(TokenType::Corpus)?;
7800 let name = self.consume(TokenType::Identifier)?.value;
7801 let mut node = CorpusDefinition {
7802 name,
7803 documents: Vec::new(),
7804 relations: Vec::new(),
7805 adaptive: false,
7806 mcp_server: String::new(),
7807 mcp_resource_uri: String::new(),
7808 store_source: None,
7809 loc: Loc {
7810 line: tok.line,
7811 column: tok.column,
7812 },
7813 leading_trivia: Vec::new(),
7814 trailing_trivia: Vec::new(),
7815 };
7816 let mut dynamic = false;
7820 if self.check(TokenType::From) {
7821 self.advance();
7822 if self.check(TokenType::AxonStore) {
7823 self.advance();
7824 dynamic = true;
7825 } else {
7826 self.consume(TokenType::Mcp)?;
7827 self.consume(TokenType::LParen)?;
7828 node.mcp_server = self.consume(TokenType::StringLit)?.value.clone();
7829 self.consume(TokenType::Comma)?;
7830 node.mcp_resource_uri = self.consume(TokenType::StringLit)?.value.clone();
7831 self.consume(TokenType::RParen)?;
7832 return Ok(node);
7833 }
7834 }
7835 self.consume(TokenType::LBrace)?;
7836 let mut src = CorpusStoreSource {
7839 doc_store: String::new(),
7840 doc_id_col: String::new(),
7841 doc_title_col: String::new(),
7842 edge_store: String::new(),
7843 edge_from_col: String::new(),
7844 edge_to_col: String::new(),
7845 edge_type_col: String::new(),
7846 edge_weight_col: String::new(),
7847 loc: Loc {
7848 line: tok.line,
7849 column: tok.column,
7850 },
7851 };
7852 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7853 let field_name = self.current().value.clone();
7854 self.advance();
7855 if self.check(TokenType::Colon) {
7856 self.advance();
7857 match field_name.as_str() {
7858 "documents" if dynamic => {
7860 let (store, cols) = self.parse_corpus_store_mapping(2)?;
7861 src.doc_store = store;
7862 src.doc_id_col = cols[0].clone();
7863 src.doc_title_col = cols[1].clone();
7864 }
7865 "documents" => node.documents = self.parse_bracketed_identifiers()?,
7866 "relations" if dynamic => {
7868 let (store, cols) = self.parse_corpus_store_mapping(4)?;
7869 src.edge_store = store;
7870 src.edge_from_col = cols[0].clone();
7871 src.edge_to_col = cols[1].clone();
7872 src.edge_type_col = cols[2].clone();
7873 src.edge_weight_col = cols[3].clone();
7874 }
7875 "relations" => node.relations = self.parse_corpus_relations()?,
7877 "adaptive" => node.adaptive = self.consume_any_ident_or_kw()?.value == "true",
7879 _ => self.skip_value(),
7880 }
7881 } else if self.check(TokenType::LBrace) {
7882 self.skip_braced_block()?;
7883 }
7884 }
7885 self.consume(TokenType::RBrace)?;
7886 if dynamic {
7887 node.store_source = Some(src);
7888 }
7889 Ok(node)
7890 }
7891
7892 fn parse_corpus_store_mapping(&mut self, n: usize) -> Result<(String, Vec<String>), ParseError> {
7899 let store = self.consume(TokenType::Identifier)?.value.clone();
7900 self.consume(TokenType::LParen)?;
7901 let mut cols = Vec::with_capacity(n);
7902 for i in 0..n {
7903 if i > 0 {
7904 self.consume(TokenType::Comma)?;
7905 }
7906 cols.push(self.consume_any_ident_or_kw()?.value.clone());
7907 }
7908 self.consume(TokenType::RParen)?;
7909 Ok((store, cols))
7910 }
7911
7912 fn parse_corpus_relations(&mut self) -> Result<Vec<CorpusRelation>, ParseError> {
7918 let mut out = Vec::new();
7919 self.consume(TokenType::LBracket)?;
7920 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
7921 if self.check(TokenType::Comma) {
7922 self.advance();
7923 continue;
7924 }
7925 let tok = self.current().clone();
7926 let etype = self.consume_any_ident_or_kw()?.value.clone();
7927 self.consume(TokenType::LParen)?;
7928 let from = self.consume_any_ident_or_kw()?.value.clone();
7929 self.consume(TokenType::Comma)?;
7930 let to = self.consume_any_ident_or_kw()?.value.clone();
7931 self.consume(TokenType::Comma)?;
7932 let weight = self.consume_number()?;
7933 self.consume(TokenType::RParen)?;
7934 out.push(CorpusRelation {
7935 etype,
7936 from,
7937 to,
7938 weight,
7939 loc: Loc { line: tok.line, column: tok.column },
7940 });
7941 }
7942 self.consume(TokenType::RBracket)?;
7943 Ok(out)
7944 }
7945
7946 fn parse_dataspace(&mut self) -> Result<DataspaceDefinition, ParseError> {
7963 let tok = self.consume(TokenType::Dataspace)?;
7964 let name = self.consume(TokenType::Identifier)?.value;
7965 let mut node = DataspaceDefinition {
7966 name,
7967 columns: Vec::new(),
7968 loc: Loc {
7969 line: tok.line,
7970 column: tok.column,
7971 },
7972 leading_trivia: Vec::new(),
7973 trailing_trivia: Vec::new(),
7974 };
7975 if self.check(TokenType::LBrace) {
7976 self.consume(TokenType::LBrace)?;
7977 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
7978 let entry = self.current().clone();
7979 if entry.value != "column" {
7980 return Err(ParseError {
7981 message: format!(
7982 "Unknown entry `{}` in dataspace `{}`. A dataspace body \
7983 declares its columnar schema: `column <name>: <Type>` \
7984 (one per line, over the closed type catalog — \
7985 Text, Int, Float, Bool, Timestamp, Json).",
7986 entry.value, node.name
7987 ),
7988 line: entry.line,
7989 column: entry.column,
7990 ..Default::default()
7991 });
7992 }
7993 self.advance(); let col_tok = self.current().clone();
7995 let col_name = self.consume_any_ident_or_kw()?.value.clone();
7996 self.consume(TokenType::Colon)?;
7997 let declared_type = self.consume_any_ident_or_kw()?.value.clone();
7998 node.columns.push(crate::ast::DataspaceColumn {
7999 name: col_name,
8000 declared_type,
8001 loc: Loc {
8002 line: col_tok.line,
8003 column: col_tok.column,
8004 },
8005 });
8006 }
8007 self.consume(TokenType::RBrace)?;
8008 }
8009 Ok(node)
8010 }
8011
8012 fn parse_ots(&mut self) -> Result<OtsDefinition, ParseError> {
8013 let tok = self.consume(TokenType::Ots)?;
8014 let name = self.consume(TokenType::Identifier)?.value;
8015 let mut node = OtsDefinition {
8016 name,
8017 teleology: String::new(),
8018 homotopy_search: String::new(),
8019 loss_function: String::new(),
8020 loc: Loc {
8021 line: tok.line,
8022 column: tok.column,
8023 },
8024 leading_trivia: Vec::new(),
8025 trailing_trivia: Vec::new(),
8026 };
8027 if self.check(TokenType::Lt) {
8029 while !self.check(TokenType::Gt) && !self.check(TokenType::Eof) {
8030 self.advance();
8031 }
8032 if self.check(TokenType::Gt) {
8033 self.advance();
8034 }
8035 }
8036 self.consume(TokenType::LBrace)?;
8037 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8038 let field_name = self.current().value.clone();
8039 self.advance();
8040 if self.check(TokenType::Colon) {
8041 self.advance();
8042 match field_name.as_str() {
8043 "teleology" => {
8044 node.teleology = self.consume(TokenType::StringLit)?.value.clone()
8045 }
8046 "homotopy_search" => {
8047 node.homotopy_search = self.consume_any_ident_or_kw()?.value.clone()
8048 }
8049 "loss_function" => {
8055 node.loss_function = if self.check(TokenType::StringLit) {
8056 self.consume(TokenType::StringLit)?.value.clone()
8057 } else {
8058 self.consume_any_ident_or_kw()?.value.clone()
8059 }
8060 }
8061 _ => self.skip_value(),
8062 }
8063 } else if self.check(TokenType::LBrace) {
8064 self.skip_braced_block()?;
8065 }
8066 }
8067 self.consume(TokenType::RBrace)?;
8068 Ok(node)
8069 }
8070
8071 fn parse_mandate(&mut self) -> Result<MandateDefinition, ParseError> {
8072 let tok = self.consume(TokenType::Mandate)?;
8073 let name = self.consume(TokenType::Identifier)?.value;
8074 let mut node = MandateDefinition {
8075 name,
8076 constraint: String::new(),
8077 kp: None,
8078 ki: None,
8079 kd: None,
8080 tolerance: None,
8081 max_steps: None,
8082 drift_bound: None,
8083 lipschitz: None,
8084 on_violation: String::new(),
8085 loc: Loc {
8086 line: tok.line,
8087 column: tok.column,
8088 },
8089 leading_trivia: Vec::new(),
8090 trailing_trivia: Vec::new(),
8091 };
8092 self.consume(TokenType::LBrace)?;
8093 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8094 let field_name = self.current().value.clone();
8095 self.advance();
8096 if self.check(TokenType::Colon) {
8097 self.advance();
8098 match field_name.as_str() {
8099 "constraint" => {
8100 node.constraint = self.consume(TokenType::StringLit)?.value.clone()
8101 }
8102 "kp" | "Kp" => node.kp = self.parse_optional_float(),
8103 "ki" | "Ki" => node.ki = self.parse_optional_float(),
8104 "kd" | "Kd" => node.kd = self.parse_optional_float(),
8105 "max_steps" => node.max_steps = self.parse_optional_int(),
8106 "tolerance" | "epsilon" => node.tolerance = self.parse_optional_float(),
8111 "on_violation" => {
8112 node.on_violation = self.consume_any_ident_or_kw()?.value.clone()
8113 }
8114 _ => self.skip_value(),
8115 }
8116 } else if self.check(TokenType::LBrace) {
8117 if field_name == "pid" {
8127 self.parse_pid_block(&mut node)?;
8128 } else if field_name == "stability" {
8129 self.parse_stability_block(&mut node)?;
8130 } else {
8131 self.skip_braced_block()?;
8132 }
8133 }
8134 }
8135 self.consume(TokenType::RBrace)?;
8136 Ok(node)
8137 }
8138
8139 fn parse_pid_block(&mut self, node: &mut MandateDefinition) -> Result<(), ParseError> {
8162 self.consume(TokenType::LBrace)?;
8163 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8164 let key_token = self.current().clone();
8165 let key = key_token.value.clone();
8166 self.advance();
8167 if self.check(TokenType::Colon) {
8168 self.advance();
8169 match key.as_str() {
8170 "kp" | "Kp" => node.kp = self.parse_optional_float(),
8171 "ki" | "Ki" => node.ki = self.parse_optional_float(),
8172 "kd" | "Kd" => node.kd = self.parse_optional_float(),
8173 _ => {
8174 return Err(ParseError {
8175 message: format!(
8176 "`{key}` is not a gain of the PID controller. The block accepts \
8177 exactly `Kp`, `Ki` and `Kd` (lower-case spellings too). \
8178 Skipping what it does not recognise would let a typo drop a \
8179 gain, and the stability band is computed from all three."
8180 ),
8181 line: key_token.line,
8182 column: key_token.column,
8183 ..Default::default()
8184 });
8185 }
8186 }
8187 }
8188 if self.check(TokenType::Comma) {
8189 self.advance();
8190 }
8191 }
8192 self.consume(TokenType::RBrace)?;
8193 Ok(())
8194 }
8195
8196 fn parse_stability_block(
8212 &mut self,
8213 node: &mut MandateDefinition,
8214 ) -> Result<(), ParseError> {
8215 let open = self.consume(TokenType::LBrace)?;
8216 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8217 let key_token = self.current().clone();
8218 let key = key_token.value.clone();
8219 self.advance();
8220 if self.check(TokenType::Colon) {
8221 self.advance();
8222 match key.as_str() {
8223 "D" | "d" | "drift_bound" => {
8224 node.drift_bound = self.parse_optional_float()
8225 }
8226 "L" | "l" | "lipschitz" => node.lipschitz = self.parse_optional_float(),
8227 _ => {
8231 return Err(ParseError {
8232 message: format!(
8233 "`{key}` is not a hypothesis of the stability theorem. The block \
8234 accepts exactly `D` (the drift bound, also spelled `d` or \
8235 `drift_bound`) and `L` (the Lipschitz constant, also `l` or \
8236 `lipschitz`). This is an error rather than a skipped key \
8237 because a bound that fails to parse is a bound that is not \
8238 declared, and the compiler would then verify the band it can \
8239 see — the sign conditions — and admit the mandate as if the \
8240 rest had been checked."
8241 ),
8242 line: key_token.line,
8243 column: key_token.column,
8244 ..Default::default()
8245 });
8246 }
8247 }
8248 }
8249 if self.check(TokenType::Comma) {
8250 self.advance();
8251 }
8252 }
8253 self.consume(TokenType::RBrace)?;
8254 if node.drift_bound.is_none() && node.lipschitz.is_none() {
8255 return Err(ParseError {
8256 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."
8257 .to_string(),
8258 line: open.line,
8259 column: open.column,
8260 ..Default::default()
8261 });
8262 }
8263 Ok(())
8264 }
8265
8266 fn parse_compute(&mut self) -> Result<ComputeDefinition, ParseError> {
8295 let tok = self.consume(TokenType::Compute)?;
8296 let name = self.consume(TokenType::Identifier)?.value;
8297 let mut node = ComputeDefinition {
8298 name,
8299 shield_ref: String::new(),
8300 parameters: Vec::new(),
8301 return_type: String::new(),
8302 body: None,
8303 loc: Loc {
8304 line: tok.line,
8305 column: tok.column,
8306 },
8307 leading_trivia: Vec::new(),
8308 trailing_trivia: Vec::new(),
8309 };
8310
8311 if self.check(TokenType::LParen) {
8313 self.advance();
8314 while !self.check(TokenType::RParen) && !self.check(TokenType::Eof) {
8315 let ptok = self.current().clone();
8316 let pname = self.consume_any_ident_or_kw()?.value.clone();
8317 self.consume(TokenType::Colon)?;
8318 let ptype = self.parse_type_expr()?;
8319 node.parameters.push(Parameter {
8320 name: pname,
8321 type_expr: ptype,
8322 loc: self.loc_of(&ptok),
8323 });
8324 if self.check(TokenType::Comma) {
8325 self.advance();
8326 }
8327 }
8328 self.consume(TokenType::RParen)?;
8329 }
8330
8331 if self.check(TokenType::Arrow) {
8333 self.advance();
8334 node.return_type = self.consume_any_ident_or_kw()?.value.clone();
8335 }
8336
8337 self.consume(TokenType::LBrace)?;
8338 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8339 let is_field = self
8349 .tokens
8350 .get(self.pos + 1)
8351 .map(|t| t.ttype == TokenType::Colon)
8352 .unwrap_or(false);
8353 if is_field {
8354 let field_tok = self.current().clone();
8355 let field_name = self.current().value.clone();
8356 self.advance();
8357 self.consume(TokenType::Colon)?;
8358 match field_name.as_str() {
8359 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8360 "input" => self.parse_compute_input_list(&mut node)?,
8370 "output" => {
8373 node.return_type = self.parse_output_type_string()?;
8374 }
8375 _ => self.skip_value(),
8376 }
8377 let _ = field_tok;
8378 } else if self.current().value == "logic"
8379 && self
8380 .tokens
8381 .get(self.pos + 1)
8382 .is_some_and(|t| t.ttype == TokenType::LBrace)
8383 {
8384 if node.body.is_some() {
8389 return Err(ParseError {
8390 message: "compute declares two bodies; a pure function has one result, \
8391 and keeping the last silently would discard the first"
8392 .to_string(),
8393 line: self.current().line,
8394 column: self.current().column,
8395 ..Default::default()
8396 });
8397 }
8398 node.body = Some(self.parse_logic_block()?);
8399 } else {
8400 node.body = Some(self.parse_expr()?);
8401 }
8402 }
8403 self.consume(TokenType::RBrace)?;
8404 Ok(node)
8405 }
8406
8407 fn parse_compute_input_list(&mut self, node: &mut ComputeDefinition) -> Result<(), ParseError> {
8413 loop {
8414 let ptok = self.current().clone();
8415 let pname = self.consume_any_ident_or_kw()?.value.clone();
8416 let type_expr = if self.check(TokenType::LParen) {
8420 self.advance();
8421 let t = self.parse_type_expr()?;
8422 self.consume(TokenType::RParen)?;
8423 t
8424 } else {
8425 TypeExpr {
8426 name: String::new(),
8427 generic_param: String::new(),
8428 optional: false,
8429 loc: self.loc_of(&ptok),
8430 }
8431 };
8432 node.parameters.push(Parameter {
8433 name: pname,
8434 type_expr,
8435 loc: self.loc_of(&ptok),
8436 });
8437 if self.check(TokenType::Comma) {
8438 self.advance();
8439 } else {
8440 break;
8441 }
8442 }
8443 Ok(())
8444 }
8445
8446 fn parse_logic_block(&mut self) -> Result<Expr, ParseError> {
8459 let open = self.current().clone();
8460 self.advance(); self.consume(TokenType::LBrace)?;
8462
8463 let mut bindings: Vec<(String, Expr)> = Vec::new();
8464 let mut result: Option<Expr> = None;
8465
8466 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8467 if self.check(TokenType::Let) {
8468 if result.is_some() {
8469 return Err(ParseError {
8470 message: "a `let` after the `return` in a `logic { }` block is \
8471 unreachable — the block's value is already decided. Move it \
8472 above the `return`."
8473 .to_string(),
8474 line: self.current().line,
8475 column: self.current().column,
8476 ..Default::default()
8477 });
8478 }
8479 self.advance(); let name = self.consume_any_ident_or_kw()?.value.clone();
8481 self.consume(TokenType::Assign)?;
8482 bindings.push((name, self.parse_expr()?));
8483 } else if self.check(TokenType::Return) {
8484 self.advance();
8485 result = Some(self.parse_expr()?);
8486 } else {
8487 let bad = self.current().clone();
8488 return Err(ParseError {
8489 message: format!(
8490 "unexpected `{}` in a `logic {{ }}` block — it admits only `let <name> = \
8491 <expr>` bindings and a closing `return <expr>`. `compute` is a PURE \
8492 function (its own paper: \"pureza categórica de los morfismos \
8493 funcionales\"), so a statement that could have an effect is refused \
8494 rather than parsed and dropped.",
8495 bad.value
8496 ),
8497 line: bad.line,
8498 column: bad.column,
8499 ..Default::default()
8500 });
8501 }
8502 }
8503 self.consume(TokenType::RBrace)?;
8504
8505 let mut expr = result.ok_or_else(|| ParseError {
8506 message: "a `logic { }` block must end in `return <expr>`. Without it the block binds \
8507 names and yields nothing, and the compute would have to invent a result — \
8508 which is precisely what a deterministic primitive must never do."
8509 .to_string(),
8510 line: open.line,
8511 column: open.column,
8512 ..Default::default()
8513 })?;
8514
8515 for (name, value) in bindings.into_iter().rev() {
8517 expr = Expr::Let {
8518 name,
8519 value: Box::new(value),
8520 body: Box::new(expr),
8521 };
8522 }
8523 Ok(expr)
8524 }
8525
8526 fn parse_daemon(&mut self) -> Result<DaemonDefinition, ParseError> {
8527 let tok = self.consume(TokenType::Daemon)?;
8528 let name = self.consume(TokenType::Identifier)?.value;
8529 let mut node = DaemonDefinition {
8530 name,
8531 goal: String::new(),
8532 tools: Vec::new(),
8533 memory_ref: String::new(),
8534 strategy: String::new(),
8535 on_stuck: String::new(),
8536 shield_ref: String::new(),
8537 window_ref: String::new(),
8538 budget: None,
8539 max_tokens: None,
8540 max_time: String::new(),
8541 max_cost: None,
8542 listeners: Vec::new(),
8543 requires_capabilities: Vec::new(),
8544 loc: Loc {
8545 line: tok.line,
8546 column: tok.column,
8547 },
8548 leading_trivia: Vec::new(),
8549 trailing_trivia: Vec::new(),
8550 };
8551 while !self.check(TokenType::LBrace) && !self.check(TokenType::Eof) {
8553 self.advance();
8554 }
8555 self.consume(TokenType::LBrace)?;
8556 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8557 let field = self.current().clone();
8558 let field_name = field.value.clone();
8559 self.advance();
8560 if self.check(TokenType::Colon) {
8561 self.advance();
8562 match field_name.as_str() {
8563 "goal" => node.goal = self.consume(TokenType::StringLit)?.value.clone(),
8564 "tools" => node.tools = self.parse_bracketed_identifiers()?,
8565 "memory" => node.memory_ref = self.consume_any_ident_or_kw()?.value.clone(),
8566 "strategy" => node.strategy = self.consume_any_ident_or_kw()?.value.clone(),
8567 "on_stuck" => node.on_stuck = self.consume_any_ident_or_kw()?.value.clone(),
8568 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
8569 "window" => node.window_ref = self.consume_any_ident_or_kw()?.value.clone(),
8571 "max_tokens" => node.max_tokens = self.parse_optional_int(),
8572 "max_time" => node.max_time = self.consume_any_ident_or_kw()?.value.clone(),
8573 "max_cost" => node.max_cost = self.parse_optional_float(),
8574 "requires" => {
8579 let bracket_tok = self.current().clone();
8580 let items = self.parse_bracketed_dot_identifiers()?;
8581 for slug in &items {
8582 if !is_valid_capability_slug(slug) {
8583 return Err(ParseError {
8584 message: format!(
8585 "Invalid capability slug '{slug}' in daemon '{}' \
8586 `requires:`. Capability slugs must match \
8587 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8588 lowercase identifiers. Examples: `daemon.run`, \
8589 `memory.write`, `flow.execute`.",
8590 node.name
8591 ),
8592 line: bracket_tok.line,
8593 column: bracket_tok.column,
8594 ..Default::default()
8595 });
8596 }
8597 }
8598 node.requires_capabilities = items;
8599 }
8600 _ => self.skip_value(),
8601 }
8602 } else if field.ttype == TokenType::Listen {
8603 let (channel, channel_is_ref) = if self.check(TokenType::StringLit) {
8609 (self.consume(TokenType::StringLit)?.value.clone(), false)
8610 } else {
8611 (self.consume_any_ident_or_kw()?.value.clone(), true)
8612 };
8613 let mut alias = String::new();
8614 if !self.at_declaration_start()
8615 && !self.check(TokenType::RBrace)
8616 && !self.check(TokenType::LBrace)
8617 {
8618 let next = self.current().clone();
8619 if next.value == "as" || next.ttype == TokenType::As {
8620 self.advance();
8621 alias = self.consume_any_ident_or_kw()?.value.clone();
8622 }
8623 }
8624 let listen_loc = Loc {
8625 line: field.line,
8626 column: field.column,
8627 };
8628 let body = self.parse_listener_body()?;
8632 node.listeners.push(ListenStep {
8633 channel,
8634 channel_is_ref,
8635 event_alias: alias,
8636 body,
8637 loc: listen_loc,
8638 });
8639 } else if field_name == "budget" && self.check(TokenType::LBrace) {
8640 node.budget = Some(self.parse_budget_block(field.line, field.column)?);
8642 } else if self.check(TokenType::LBrace) {
8643 self.skip_braced_block()?;
8644 }
8645 }
8646 self.consume(TokenType::RBrace)?;
8647 Ok(node)
8648 }
8649
8650 fn parse_top_level_budget(&mut self) -> Result<BudgetBlock, ParseError> {
8659 let kw = self.consume(TokenType::Budget)?; let name = self.consume(TokenType::Identifier)?.value;
8661 let mut block = self.parse_budget_block(kw.line, kw.column)?;
8662 block.name = name;
8663 Ok(block)
8664 }
8665
8666 fn parse_budget_block(&mut self, line: u32, column: u32) -> Result<BudgetBlock, ParseError> {
8668 self.consume(TokenType::LBrace)?;
8669 let mut quotas = Vec::new();
8670 let mut on_exhausted = String::new();
8671 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8672 let field = self.current().clone();
8673 let field_name = self.consume_any_ident_or_kw()?.value;
8674 match field_name.as_str() {
8675 "rate" | "max" => {
8676 quotas.push(self.parse_budget_quota(field_name, field.line, field.column)?);
8677 }
8678 "on_exhausted" => {
8679 self.consume(TokenType::Colon)?;
8680 on_exhausted = self.consume_any_ident_or_kw()?.value;
8681 }
8682 _ => self.skip_value(),
8683 }
8684 }
8685 self.consume(TokenType::RBrace)?;
8686 Ok(BudgetBlock {
8687 name: String::new(),
8688 quotas,
8689 on_exhausted,
8690 loc: Loc { line, column },
8691 leading_trivia: Vec::new(),
8692 trailing_trivia: Vec::new(),
8693 })
8694 }
8695
8696 fn parse_budget_quota(
8699 &mut self,
8700 kind: String,
8701 line: u32,
8702 column: u32,
8703 ) -> Result<BudgetQuota, ParseError> {
8704 self.consume(TokenType::Colon)?;
8705 let limit = self.consume_number()? as i64;
8706 let _per = self.consume_any_ident_or_kw()?; let period = self.consume_any_ident_or_kw()?.value;
8709 let _on = self.consume_any_ident_or_kw()?; let _tool = self.consume_any_ident_or_kw()?; self.consume(TokenType::LParen)?;
8713 let effect = self.consume_any_ident_or_kw()?.value;
8714 self.consume(TokenType::RParen)?;
8715 Ok(BudgetQuota {
8716 kind,
8717 limit,
8718 period,
8719 effect,
8720 loc: Loc { line, column },
8721 })
8722 }
8723
8724 fn parse_axonstore(&mut self) -> Result<AxonStoreDefinition, ParseError> {
8725 let tok = self.consume(TokenType::AxonStore)?;
8726 let name = self.consume(TokenType::Identifier)?.value;
8727 let mut node = AxonStoreDefinition {
8728 name,
8729 backend: String::new(),
8730 connection: String::new(),
8731 resource_ref: String::new(),
8732 confidence_floor: None,
8733 isolation: String::new(),
8734 on_breach: String::new(),
8735 capability: String::new(),
8736 class: String::new(),
8737 column_schema: None,
8738 loc: Loc {
8739 line: tok.line,
8740 column: tok.column,
8741 },
8742 leading_trivia: Vec::new(),
8743 trailing_trivia: Vec::new(),
8744 };
8745 self.consume(TokenType::LBrace)?;
8746 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8747 let field = self.current().clone();
8748 let field_name = field.value.clone();
8749 if field.ttype == TokenType::Schema {
8755 self.advance();
8756 let parsed = self.parse_store_schema_declaration(&node.name, field.line, field.column)?;
8757 node.column_schema = Some(parsed);
8758 continue;
8759 }
8760 self.advance();
8761 if self.check(TokenType::Colon) {
8762 self.advance();
8763 match field_name.as_str() {
8764 "backend" => node.backend = self.consume_any_ident_or_kw()?.value.clone(),
8765 "class" => node.class = self.parse_dotted_identifier()?,
8773 "connection" => node.connection = self.parse_config_key()?,
8774 "resource" => {
8780 node.resource_ref = self.consume_any_ident_or_kw()?.value.clone()
8781 }
8782 "confidence_floor" => node.confidence_floor = self.parse_optional_float(),
8783 "isolation" => node.isolation = self.consume_any_ident_or_kw()?.value.clone(),
8784 "on_breach" => node.on_breach = self.consume_any_ident_or_kw()?.value.clone(),
8785 "capability" => {
8789 let slug_tok = self.consume(TokenType::StringLit)?.clone();
8790 if !is_valid_capability_slug(&slug_tok.value) {
8791 return Err(ParseError {
8792 message: format!(
8793 "Invalid capability slug '{}' in axonstore '{}' \
8794 `capability:`. Capability slugs must match \
8795 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
8796 lowercase identifiers starting with a letter. Examples: \
8797 `admin`, `tenant.read`, `hipaa.phi.read`.",
8798 slug_tok.value, node.name
8799 ),
8800 line: slug_tok.line,
8801 column: slug_tok.column,
8802 ..Default::default()
8803 });
8804 }
8805 node.capability = slug_tok.value.clone();
8806 }
8807 _ => self.skip_value(),
8808 }
8809 } else if self.check(TokenType::LBrace) {
8810 self.skip_braced_block()?;
8811 }
8812 }
8813 self.consume(TokenType::RBrace)?;
8814 Ok(node)
8815 }
8816
8817 fn parse_store_schema_declaration(
8829 &mut self,
8830 store_name: &str,
8831 sch_line: u32,
8832 sch_col: u32,
8833 ) -> Result<crate::store_schema::StoreColumnSchema, ParseError> {
8834 use crate::store_schema::{StoreColumn, StoreColumnSchema, StoreColumnType};
8835
8836 if self.check(TokenType::LBrace) {
8838 self.consume(TokenType::LBrace)?;
8839 let mut columns: Vec<StoreColumn> = Vec::new();
8840 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8841 let col_tok = self.current().clone();
8842 let col_name = self.consume_any_ident_or_kw()?.value.clone();
8843 self.consume(TokenType::Colon)?;
8844 let type_tok = self.consume_any_ident_or_kw()?.clone();
8845 let col_type = StoreColumnType::from_token(&type_tok.value).ok_or_else(|| {
8846 let names = StoreColumnType::all_canonical_names();
8847 let suggestion =
8848 crate::smart_suggest::suggest_for(&type_tok.value, &names);
8849 let suggest_suffix = if suggestion.is_empty() {
8850 String::new()
8851 } else {
8852 format!(" {suggestion}")
8853 };
8854 let known = names.join(", ");
8855 ParseError {
8856 message: format!(
8857 "Unknown column type `{}` for column `{}` in \
8858 axonstore `{}` `schema:` block. The closed \
8859 v1.38.0 column-type catalog \
8860 is {{{known}}} (plus common lowercase \
8861 aliases — `int`/`integer`/`int4` for \
8862 `Int`, `bool`/`boolean` for `Bool`, etc.).\
8863 {suggest_suffix}",
8864 type_tok.value, col_name, store_name
8865 ),
8866 line: type_tok.line,
8867 column: type_tok.column,
8868 ..Default::default()
8869 }
8870 })?;
8871
8872 let mut json_shape: Option<String> = None;
8882 if self.check(TokenType::Lt) {
8883 self.advance();
8884 let shape_tok = self.consume_any_ident_or_kw()?.clone();
8885 self.consume(TokenType::Gt)?;
8886 if matches!(col_type, StoreColumnType::Json | StoreColumnType::Jsonb) {
8887 json_shape = Some(shape_tok.value.clone());
8888 } else {
8889 return Err(ParseError {
8890 message: format!(
8891 "axon-T841 a shape lens `<{shape}>` may refine \
8892 only a `Json` / `Jsonb` column, but column \
8893 `{col}` in axonstore `{store}` is `{ty}`. Drop \
8894 the `<{shape}>` (a rigid column already has a \
8895 fixed shape), or change the column type to \
8896 `Json<{shape}>` if it carries open documents.",
8897 shape = shape_tok.value,
8898 col = col_name,
8899 store = store_name,
8900 ty = col_type.canonical_name(),
8901 ),
8902 line: shape_tok.line,
8903 column: shape_tok.column,
8904 ..Default::default()
8905 });
8906 }
8907 }
8908
8909 let mut col = StoreColumn {
8910 name: col_name,
8911 col_type,
8912 json_shape,
8913 primary_key: false,
8914 auto_increment: false,
8915 not_null: false,
8916 unique: false,
8917 indexed: false,
8918 default_value: String::new(),
8919 identity: false,
8924 line: col_tok.line,
8925 column: col_tok.column,
8926 };
8927
8928 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
8931 if self.current().ttype != TokenType::Identifier {
8932 break;
8935 }
8936 let constraint = self.current().value.clone();
8937 match constraint.as_str() {
8938 "primary_key" => {
8939 col.primary_key = true;
8940 self.advance();
8941 }
8942 "auto_increment" => {
8943 col.auto_increment = true;
8944 self.advance();
8945 }
8946 "not_null" => {
8947 col.not_null = true;
8948 self.advance();
8949 }
8950 "unique" => {
8951 col.unique = true;
8952 self.advance();
8953 }
8954 "index" => {
8960 col.indexed = true;
8961 self.advance();
8962 }
8963 "identity" => {
8974 col.identity = true;
8975 self.advance();
8976 }
8977 "default" => {
8978 self.advance();
8979 let dv = self.current().clone();
8980 if matches!(
8981 dv.ttype,
8982 TokenType::StringLit
8983 | TokenType::Integer
8984 | TokenType::Float
8985 ) {
8986 col.default_value = dv.value.clone();
8987 self.advance();
8988 } else {
8989 col.default_value =
8990 self.consume_any_ident_or_kw()?.value.clone();
8991 }
8992 }
8993 _ => break,
8994 }
8995 }
8996
8997 columns.push(col);
8998 }
8999 self.consume(TokenType::RBrace)?;
9000 return Ok(StoreColumnSchema::Inline {
9001 columns,
9002 leading_trivia: Vec::new(),
9003 line: sch_line,
9004 column: sch_col,
9005 });
9006 }
9007
9008 if !self.check(TokenType::Colon) {
9010 let cur = self.current().clone();
9011 return Err(ParseError {
9012 message: format!(
9013 "axonstore `{store_name}` `schema:` declaration expects \
9014 `{{ … }}` (inline columns), `: \"manifest.ref\"` \
9015 (manifest reference), or `: env:VAR` (per-tenant schema \
9016 namespace). Got `{}` instead.",
9017 cur.value
9018 ),
9019 line: cur.line,
9020 column: cur.column,
9021 ..Default::default()
9022 });
9023 }
9024 self.consume(TokenType::Colon)?;
9025
9026 if self.check(TokenType::StringLit) {
9028 let lit = self.consume(TokenType::StringLit)?.clone();
9029 let value = lit.value.clone();
9030 if let Some(var) = value.strip_prefix("env:") {
9031 let var = var.trim();
9032 if var.is_empty() {
9033 return Err(ParseError {
9034 message: format!(
9035 "axonstore `{store_name}` `schema: \"env:\"` is \
9036 missing the variable name after the `env:` \
9037 prefix."
9038 ),
9039 line: lit.line,
9040 column: lit.column,
9041 ..Default::default()
9042 });
9043 }
9044 return Ok(StoreColumnSchema::EnvVar {
9045 var_name: var.to_string(),
9046 line: sch_line,
9047 column: sch_col,
9048 });
9049 }
9050 if value.trim().is_empty() {
9052 return Err(ParseError {
9053 message: format!(
9054 "axonstore `{store_name}` `schema:` manifest reference \
9055 is empty. Expected `\"qualified.name\"` — e.g. \
9056 `\"public.tenants\"`."
9057 ),
9058 line: lit.line,
9059 column: lit.column,
9060 ..Default::default()
9061 });
9062 }
9063 return Ok(StoreColumnSchema::ManifestRef {
9064 qualified_name: value,
9065 line: sch_line,
9066 column: sch_col,
9067 });
9068 }
9069
9070 let env_tok = self.current().clone();
9073 if env_tok.value == "env" {
9074 self.advance();
9075 if !self.check(TokenType::Colon) {
9076 return Err(ParseError {
9077 message: format!(
9078 "axonstore `{store_name}` `schema: env` is missing the \
9079 `:` separator. Expected `schema: env:VAR`."
9080 ),
9081 line: env_tok.line,
9082 column: env_tok.column,
9083 ..Default::default()
9084 });
9085 }
9086 self.advance(); let var_tok = self.consume_any_ident_or_kw()?.clone();
9088 if var_tok.value.trim().is_empty() {
9089 return Err(ParseError {
9090 message: format!(
9091 "axonstore `{store_name}` `schema: env:` is missing \
9092 the variable name."
9093 ),
9094 line: var_tok.line,
9095 column: var_tok.column,
9096 ..Default::default()
9097 });
9098 }
9099 return Ok(StoreColumnSchema::EnvVar {
9100 var_name: var_tok.value.clone(),
9101 line: sch_line,
9102 column: sch_col,
9103 });
9104 }
9105
9106 Err(ParseError {
9107 message: format!(
9108 "axonstore `{store_name}` `schema:` declaration expects \
9109 `{{ … }}` (inline columns), `\"manifest.ref\"` (manifest \
9110 reference), or `env:VAR` (per-tenant schema namespace). \
9111 Got `{}` instead.",
9112 env_tok.value
9113 ),
9114 line: env_tok.line,
9115 column: env_tok.column,
9116 ..Default::default()
9117 })
9118 }
9119
9120 fn parse_resource(&mut self) -> Result<ResourceDefinition, ParseError> {
9127 let tok = self.consume(TokenType::Resource)?;
9128 let name = self.consume(TokenType::Identifier)?.value;
9129 let mut node = ResourceDefinition {
9130 name,
9131 kind: String::new(),
9132 endpoint: String::new(),
9133 capacity: None,
9134 lifetime: "affine".to_string(),
9135 certainty_floor: None,
9136 shield_ref: String::new(),
9137 within: String::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_tok = self.current().clone();
9148 let field_name = field_tok.value.clone();
9149 self.advance();
9150 if !self.check(TokenType::Colon) {
9151 if self.check(TokenType::LBrace) {
9153 self.skip_braced_block()?;
9154 }
9155 continue;
9156 }
9157 self.advance(); match field_name.as_str() {
9159 "kind" => node.kind = self.consume_any_ident_or_kw()?.value,
9160 "endpoint" => {
9173 node.endpoint = if self.check(TokenType::StringLit) {
9174 self.consume(TokenType::StringLit)?.value
9175 } else {
9176 self.parse_dotted_identifier()?
9177 };
9178 }
9179 "capacity" => {
9180 node.capacity = self.parse_optional_int();
9181 }
9182 "lifetime" => {
9183 let lt_tok = self.consume_any_ident_or_kw()?;
9184 let lt = lt_tok.value;
9185 if !matches!(lt.as_str(), "linear" | "affine" | "persistent") {
9186 return Err(ParseError {
9187 message: format!(
9188 "Invalid lifetime '{lt}' in resource '{}' — \
9189 expected linear | affine | persistent",
9190 node.name
9191 ),
9192 line: lt_tok.line,
9193 column: lt_tok.column,
9194 ..Default::default()
9195 });
9196 }
9197 node.lifetime = lt;
9198 }
9199 "certainty_floor" => {
9200 node.certainty_floor = self.parse_optional_float();
9201 }
9202 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9203 "within" => node.within = self.consume_any_ident_or_kw()?.value,
9207 unknown => {
9217 return Err(ParseError {
9218 message: format!(
9219 "Unknown field '{unknown}' in resource '{}' — expected one of: \
9220 kind, endpoint, capacity, lifetime, certainty_floor, shield, within",
9221 node.name
9222 ),
9223 line: field_tok.line,
9224 column: field_tok.column,
9225 ..Default::default()
9226 });
9227 }
9228 }
9229 }
9230 self.consume(TokenType::RBrace)?;
9231 Ok(node)
9232 }
9233
9234 fn parse_fabric(&mut self) -> Result<FabricDefinition, ParseError> {
9236 let tok = self.consume(TokenType::Fabric)?;
9237 let name = self.consume(TokenType::Identifier)?.value;
9238 let mut node = FabricDefinition {
9239 name,
9240 provider: String::new(),
9241 region: String::new(),
9242 zones: None,
9243 ephemeral: None,
9244 shield_ref: String::new(),
9245 loc: Loc {
9246 line: tok.line,
9247 column: tok.column,
9248 },
9249 leading_trivia: Vec::new(),
9250 trailing_trivia: Vec::new(),
9251 };
9252 self.consume(TokenType::LBrace)?;
9253 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9254 let field_name = self.current().value.clone();
9255 self.advance();
9256 if !self.check(TokenType::Colon) {
9257 if self.check(TokenType::LBrace) {
9258 self.skip_braced_block()?;
9259 }
9260 continue;
9261 }
9262 self.advance(); match field_name.as_str() {
9264 "provider" => node.provider = self.consume_any_ident_or_kw()?.value,
9265 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9266 "zones" => node.zones = self.parse_optional_int(),
9267 "ephemeral" => {
9268 let b = self.parse_bool()?;
9269 node.ephemeral = Some(b);
9270 }
9271 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9272 _ => self.skip_value(),
9273 }
9274 }
9275 self.consume(TokenType::RBrace)?;
9276 Ok(node)
9277 }
9278
9279 fn parse_manifest(&mut self) -> Result<ManifestDefinition, ParseError> {
9281 let tok = self.consume(TokenType::Manifest)?;
9282 let name = self.consume(TokenType::Identifier)?.value;
9283 let mut node = ManifestDefinition {
9284 name,
9285 resources: Vec::new(),
9286 fabric_ref: String::new(),
9287 region: String::new(),
9288 zones: None,
9289 census: String::new(),
9290 compliance: Vec::new(),
9291 loc: Loc {
9292 line: tok.line,
9293 column: tok.column,
9294 },
9295 leading_trivia: Vec::new(),
9296 trailing_trivia: Vec::new(),
9297 };
9298 self.consume(TokenType::LBrace)?;
9299 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9300 let field_name = self.current().value.clone();
9301 self.advance();
9302 if !self.check(TokenType::Colon) {
9303 if self.check(TokenType::LBrace) {
9304 self.skip_braced_block()?;
9305 }
9306 continue;
9307 }
9308 self.advance();
9309 match field_name.as_str() {
9310 "resources" => node.resources = self.parse_bracketed_identifiers()?,
9311 "fabric" => node.fabric_ref = self.consume_any_ident_or_kw()?.value,
9312 "region" => node.region = self.consume(TokenType::StringLit)?.value,
9313 "zones" => node.zones = self.parse_optional_int(),
9314 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
9315 "census" => node.census = self.consume_any_ident_or_kw()?.value.clone(),
9317 _ => self.skip_value(),
9318 }
9319 }
9320 self.consume(TokenType::RBrace)?;
9321 Ok(node)
9322 }
9323
9324 fn parse_observe(&mut self) -> Result<ObserveDefinition, ParseError> {
9326 let tok = self.consume(TokenType::Observe)?;
9327 let name = self.consume(TokenType::Identifier)?.value;
9328 self.consume(TokenType::From)?;
9330 let target = self.consume(TokenType::Identifier)?.value;
9331 let mut node = ObserveDefinition {
9332 name,
9333 target,
9334 sources: Vec::new(),
9335 quorum: None,
9336 timeout: String::new(),
9337 on_partition: "fail".to_string(),
9338 certainty_floor: None,
9339 loc: Loc {
9340 line: tok.line,
9341 column: tok.column,
9342 },
9343 leading_trivia: Vec::new(),
9344 trailing_trivia: Vec::new(),
9345 };
9346 self.consume(TokenType::LBrace)?;
9347 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9348 let field_name = self.current().value.clone();
9349 self.advance();
9350 if !self.check(TokenType::Colon) {
9351 if self.check(TokenType::LBrace) {
9352 self.skip_braced_block()?;
9353 }
9354 continue;
9355 }
9356 self.advance();
9357 match field_name.as_str() {
9358 "sources" => node.sources = self.parse_bracketed_identifiers()?,
9359 "quorum" => node.quorum = self.parse_optional_int(),
9360 "timeout" => {
9361 let t = self.current().clone();
9362 match t.ttype {
9363 TokenType::Duration | TokenType::StringLit => {
9364 self.advance();
9365 node.timeout = t.value;
9366 }
9367 _ => node.timeout = self.consume_any_ident_or_kw()?.value,
9368 }
9369 }
9370 "on_partition" => {
9371 let p_tok = self.consume_any_ident_or_kw()?;
9372 let p = p_tok.value;
9373 if !matches!(p.as_str(), "fail" | "shield_quarantine") {
9374 return Err(ParseError {
9375 message: format!(
9376 "Invalid on_partition '{p}' in observe '{}' — \
9377 expected fail | shield_quarantine",
9378 node.name
9379 ),
9380 line: p_tok.line,
9381 column: p_tok.column,
9382 ..Default::default()
9383 });
9384 }
9385 node.on_partition = p;
9386 }
9387 "certainty_floor" => node.certainty_floor = self.parse_optional_float(),
9388 _ => self.skip_value(),
9389 }
9390 }
9391 self.consume(TokenType::RBrace)?;
9392 Ok(node)
9393 }
9394
9395 fn parse_reconcile(&mut self) -> Result<ReconcileDefinition, ParseError> {
9399 let tok = self.consume(TokenType::Reconcile)?;
9400 let name = self.consume(TokenType::Identifier)?.value;
9401 let mut node = ReconcileDefinition {
9402 name,
9403 observe_ref: String::new(),
9404 threshold: None,
9405 tolerance: None,
9406 on_drift: "provision".to_string(),
9407 shield_ref: String::new(),
9408 mandate_ref: String::new(),
9409 max_retries: 3,
9410 loc: Loc {
9411 line: tok.line,
9412 column: tok.column,
9413 },
9414 leading_trivia: Vec::new(),
9415 trailing_trivia: Vec::new(),
9416 };
9417 self.consume(TokenType::LBrace)?;
9418 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9419 let field_name = self.current().value.clone();
9420 self.advance();
9421 if !self.check(TokenType::Colon) {
9422 if self.check(TokenType::LBrace) {
9423 self.skip_braced_block()?;
9424 }
9425 continue;
9426 }
9427 self.advance();
9428 match field_name.as_str() {
9429 "observe" => node.observe_ref = self.consume_any_ident_or_kw()?.value,
9430 "threshold" => node.threshold = self.parse_optional_float(),
9431 "tolerance" => node.tolerance = self.parse_optional_float(),
9432 "on_drift" => {
9433 let d_tok = self.consume_any_ident_or_kw()?;
9434 let d = d_tok.value;
9435 if !matches!(d.as_str(), "provision" | "alert" | "refine") {
9436 return Err(ParseError {
9437 message: format!(
9438 "Invalid on_drift '{d}' in reconcile '{}' — \
9439 expected provision | alert | refine",
9440 node.name
9441 ),
9442 line: d_tok.line,
9443 column: d_tok.column,
9444 ..Default::default()
9445 });
9446 }
9447 node.on_drift = d;
9448 }
9449 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
9450 "mandate" => node.mandate_ref = self.consume_any_ident_or_kw()?.value,
9451 "max_retries" => {
9452 if let Some(v) = self.parse_optional_int() {
9453 node.max_retries = v;
9454 }
9455 }
9456 _ => self.skip_value(),
9457 }
9458 }
9459 self.consume(TokenType::RBrace)?;
9460 Ok(node)
9461 }
9462
9463 fn parse_lease(&mut self) -> Result<LeaseDefinition, ParseError> {
9465 let tok = self.consume(TokenType::Lease)?;
9466 let name = self.consume(TokenType::Identifier)?.value;
9467 let mut node = LeaseDefinition {
9468 name,
9469 resource_ref: String::new(),
9470 duration: String::new(),
9471 acquire: "on_start".to_string(),
9472 on_expire: "anchor_breach".to_string(),
9473 loc: Loc {
9474 line: tok.line,
9475 column: tok.column,
9476 },
9477 leading_trivia: Vec::new(),
9478 trailing_trivia: Vec::new(),
9479 };
9480 self.consume(TokenType::LBrace)?;
9481 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9482 let field_name = self.current().value.clone();
9483 self.advance();
9484 if !self.check(TokenType::Colon) {
9485 if self.check(TokenType::LBrace) {
9486 self.skip_braced_block()?;
9487 }
9488 continue;
9489 }
9490 self.advance();
9491 match field_name.as_str() {
9492 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9493 "duration" => {
9494 let t = self.current().clone();
9495 match t.ttype {
9496 TokenType::Duration | TokenType::StringLit => {
9497 self.advance();
9498 node.duration = t.value;
9499 }
9500 _ => node.duration = self.consume_any_ident_or_kw()?.value,
9501 }
9502 }
9503 "acquire" => {
9504 let a_tok = self.consume_any_ident_or_kw()?;
9505 let a = a_tok.value;
9506 if !matches!(a.as_str(), "on_start" | "on_demand") {
9507 return Err(ParseError {
9508 message: format!(
9509 "Invalid acquire '{a}' in lease '{}' — \
9510 expected on_start | on_demand",
9511 node.name
9512 ),
9513 line: a_tok.line,
9514 column: a_tok.column,
9515 ..Default::default()
9516 });
9517 }
9518 node.acquire = a;
9519 }
9520 "on_expire" => {
9521 let e_tok = self.consume_any_ident_or_kw()?;
9522 let e = e_tok.value;
9523 if !matches!(e.as_str(), "anchor_breach" | "release" | "extend") {
9524 return Err(ParseError {
9525 message: format!(
9526 "Invalid on_expire '{e}' in lease '{}' — \
9527 expected anchor_breach | release | extend",
9528 node.name
9529 ),
9530 line: e_tok.line,
9531 column: e_tok.column,
9532 ..Default::default()
9533 });
9534 }
9535 node.on_expire = e;
9536 }
9537 _ => self.skip_value(),
9538 }
9539 }
9540 self.consume(TokenType::RBrace)?;
9541 Ok(node)
9542 }
9543
9544 fn parse_ensemble(&mut self) -> Result<EnsembleDefinition, ParseError> {
9546 let tok = self.consume(TokenType::Ensemble)?;
9547 let name = self.consume(TokenType::Identifier)?.value;
9548 let mut node = EnsembleDefinition {
9549 name,
9550 observations: Vec::new(),
9551 quorum: None,
9552 aggregation: "majority".to_string(),
9553 certainty_mode: "min".to_string(),
9554 loc: Loc {
9555 line: tok.line,
9556 column: tok.column,
9557 },
9558 leading_trivia: Vec::new(),
9559 trailing_trivia: Vec::new(),
9560 };
9561 self.consume(TokenType::LBrace)?;
9562 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9563 let field_name = self.current().value.clone();
9564 self.advance();
9565 if !self.check(TokenType::Colon) {
9566 if self.check(TokenType::LBrace) {
9567 self.skip_braced_block()?;
9568 }
9569 continue;
9570 }
9571 self.advance();
9572 match field_name.as_str() {
9573 "observations" => node.observations = self.parse_bracketed_identifiers()?,
9574 "quorum" => node.quorum = self.parse_optional_int(),
9575 "aggregation" => {
9576 let a_tok = self.consume_any_ident_or_kw()?;
9577 let a = a_tok.value;
9578 if !matches!(a.as_str(), "majority" | "weighted" | "byzantine") {
9579 return Err(ParseError {
9580 message: format!(
9581 "Invalid aggregation '{a}' in ensemble '{}' — \
9582 expected majority | weighted | byzantine",
9583 node.name
9584 ),
9585 line: a_tok.line,
9586 column: a_tok.column,
9587 ..Default::default()
9588 });
9589 }
9590 node.aggregation = a;
9591 }
9592 "certainty_mode" => {
9593 let c_tok = self.consume_any_ident_or_kw()?;
9594 let c = c_tok.value;
9595 if !matches!(c.as_str(), "min" | "weighted" | "harmonic") {
9596 return Err(ParseError {
9597 message: format!(
9598 "Invalid certainty_mode '{c}' in ensemble '{}' — \
9599 expected min | weighted | harmonic",
9600 node.name
9601 ),
9602 line: c_tok.line,
9603 column: c_tok.column,
9604 ..Default::default()
9605 });
9606 }
9607 node.certainty_mode = c;
9608 }
9609 _ => self.skip_value(),
9610 }
9611 }
9612 self.consume(TokenType::RBrace)?;
9613 Ok(node)
9614 }
9615
9616 fn parse_session_definition(&mut self) -> Result<SessionDefinition, ParseError> {
9624 let tok = self.consume(TokenType::Session)?;
9625 let name = self.consume(TokenType::Identifier)?.value;
9626 let mut node = SessionDefinition {
9627 name,
9628 roles: Vec::new(),
9629 loc: Loc {
9630 line: tok.line,
9631 column: tok.column,
9632 },
9633 leading_trivia: Vec::new(),
9634 trailing_trivia: Vec::new(),
9635 };
9636 self.consume(TokenType::LBrace)?;
9637 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9638 let role_tok = self.consume_any_ident_or_kw()?;
9639 self.consume(TokenType::Colon)?;
9640 let steps = self.parse_session_steps()?;
9641 node.roles.push(SessionRole {
9642 name: role_tok.value,
9643 steps,
9644 loc: Loc {
9645 line: role_tok.line,
9646 column: role_tok.column,
9647 },
9648 });
9649 }
9650 self.consume(TokenType::RBrace)?;
9651 Ok(node)
9652 }
9653
9654 fn parse_observable(&mut self) -> Result<ObservableDefinition, ParseError> {
9667 let tok = self.consume(TokenType::Observable)?;
9668 let name = self.consume(TokenType::Identifier)?.value;
9669 let mut node = ObservableDefinition {
9670 name,
9671 qubits: None,
9672 terms: Vec::new(),
9673 loc: Loc {
9674 line: tok.line,
9675 column: tok.column,
9676 },
9677 leading_trivia: Vec::new(),
9678 trailing_trivia: Vec::new(),
9679 };
9680 self.consume(TokenType::LBrace)?;
9681 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9682 let key_tok = self.consume_any_ident_or_kw()?;
9683 self.consume(TokenType::Colon)?;
9684 match key_tok.value.as_str() {
9685 "qubits" => node.qubits = Some(self.consume_number()? as i64),
9686 "term" => {
9687 let term_loc = Loc {
9688 line: key_tok.line,
9689 column: key_tok.column,
9690 };
9691 let mut negative = false;
9693 if self.check(TokenType::Minus) {
9694 self.advance();
9695 negative = true;
9696 } else if self.check(TokenType::Plus) {
9697 self.advance();
9698 }
9699 let mag = self.consume_number()?;
9700 let coefficient = if negative { -mag } else { mag };
9701 self.consume(TokenType::Star)?;
9703 let pauli = self.consume(TokenType::StringLit)?.value;
9704 node.terms.push(PauliTerm {
9705 coefficient,
9706 pauli,
9707 loc: term_loc,
9708 });
9709 }
9710 _ => self.skip_value(),
9711 }
9712 }
9713 self.consume(TokenType::RBrace)?;
9714 Ok(node)
9715 }
9716
9717 fn parse_witness(&mut self) -> Result<WitnessDefinition, ParseError> {
9725 let tok = self.consume(TokenType::Witness)?;
9726 let name = self.consume(TokenType::Identifier)?.value;
9727 let mut node = WitnessDefinition {
9728 name,
9729 claim: String::new(),
9730 baseline: String::new(),
9731 metric: String::new(),
9732 threshold: 0.0,
9733 data: String::new(),
9734 loc: Loc {
9735 line: tok.line,
9736 column: tok.column,
9737 },
9738 leading_trivia: Vec::new(),
9739 trailing_trivia: Vec::new(),
9740 };
9741 self.consume(TokenType::LBrace)?;
9742 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9743 let key_tok = self.consume_any_ident_or_kw()?;
9744 self.consume(TokenType::Colon)?;
9745 match key_tok.value.as_str() {
9746 "claim" => node.claim = self.consume_any_ident_or_kw()?.value,
9747 "against" => node.baseline = self.consume_any_ident_or_kw()?.value,
9750 "metric" => node.metric = self.consume_any_ident_or_kw()?.value,
9751 "threshold" => node.threshold = self.consume_number()?,
9752 "data" => node.data = self.consume_any_ident_or_kw()?.value,
9753 _ => self.skip_value(),
9754 }
9755 }
9756 self.consume(TokenType::RBrace)?;
9757 Ok(node)
9758 }
9759
9760 fn parse_socket(&mut self) -> Result<SocketDefinition, ParseError> {
9765 let tok = self.consume(TokenType::Socket)?;
9766 let name = self.consume(TokenType::Identifier)?.value;
9767 let mut node = SocketDefinition {
9768 name,
9769 loc: Loc { line: tok.line, column: tok.column },
9770 ..Default::default()
9771 };
9772 self.consume(TokenType::LBrace)?;
9773 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9774 let key = self.consume_any_ident_or_kw()?.value;
9775 self.consume(TokenType::Colon)?;
9776 match key.as_str() {
9777 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9778 "backpressure" => {
9779 let kind = self.consume_any_ident_or_kw()?.value;
9781 if kind != "credit" {
9782 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9783 }
9784 self.consume(TokenType::LParen)?;
9785 let n = self
9786 .consume(TokenType::Integer)?
9787 .value
9788 .parse::<i64>()
9789 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9790 self.consume(TokenType::RParen)?;
9791 node.backpressure_credit = Some(n);
9792 }
9793 "reconnect" => {
9794 let mode = self.consume_any_ident_or_kw()?.value;
9795 node.reconnect = mode == "cognitive_state";
9796 }
9797 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
9798 other => return Err(self.error(&format!("unknown socket field `{other}`"))),
9799 }
9800 if self.check(TokenType::Comma) {
9802 self.consume(TokenType::Comma)?;
9803 }
9804 }
9805 self.consume(TokenType::RBrace)?;
9806 Ok(node)
9807 }
9808
9809 fn parse_upstream(&mut self) -> Result<UpstreamDefinition, ParseError> {
9817 let tok = self.consume(TokenType::Upstream)?;
9818 let name = self.consume(TokenType::Identifier)?.value;
9819 let mut node = UpstreamDefinition {
9820 name,
9821 loc: Loc { line: tok.line, column: tok.column },
9822 ..Default::default()
9823 };
9824 if self.check(TokenType::From) {
9826 self.advance();
9827 let base = self.consume(TokenType::Identifier)?.value;
9828 self.consume(TokenType::At)?;
9829 let version = self.consume_any_ident_or_kw()?.value;
9830 node.preset = Some(format!("{base}@{version}"));
9831 }
9832 self.consume(TokenType::LBrace)?;
9833 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9834 let key = self.consume_any_ident_or_kw()?.value;
9835 self.consume(TokenType::Colon)?;
9836 match key.as_str() {
9837 "transport" => node.transport = self.consume_any_ident_or_kw()?.value,
9838 "protocol" => node.protocol = self.consume_any_ident_or_kw()?.value,
9839 "role" => node.role = self.consume_any_ident_or_kw()?.value,
9840 "resolve" => node.resolve = self.parse_dotted_identifier()?,
9841 "resource" => node.resource_ref = self.consume_any_ident_or_kw()?.value,
9845 "secret" => node.secret = self.parse_dotted_identifier()?,
9846 "auth" => {
9847 node.auth_kind = self.consume_any_ident_or_kw()?.value;
9850 if self.check(TokenType::LParen) {
9851 self.consume(TokenType::LParen)?;
9852 node.auth_name = Some(self.consume(TokenType::StringLit)?.value);
9853 if self.check(TokenType::Comma) {
9854 self.consume(TokenType::Comma)?;
9855 node.auth_prefix = Some(self.consume(TokenType::StringLit)?.value);
9856 }
9857 self.consume(TokenType::RParen)?;
9858 }
9859 }
9860 "map" => node.map = self.parse_upstream_map()?,
9861 "reconnect" => node.reconnect = Some(self.parse_upstream_reconnect()?),
9862 "overflow" => node.overflow = Some(self.consume_any_ident_or_kw()?.value),
9863 "backpressure" => {
9864 let kind = self.consume_any_ident_or_kw()?.value;
9866 if kind != "credit" {
9867 return Err(self.error(&format!("expected `credit(n)` for backpressure, got `{kind}`")));
9868 }
9869 self.consume(TokenType::LParen)?;
9870 let n = self
9871 .consume(TokenType::Integer)?
9872 .value
9873 .parse::<i64>()
9874 .map_err(|_| self.error("backpressure credit must be an integer"))?;
9875 self.consume(TokenType::RParen)?;
9876 node.backpressure_credit = Some(n);
9877 }
9878 other => return Err(self.error(&format!("unknown upstream field `{other}`"))),
9879 }
9880 if self.check(TokenType::Comma) {
9882 self.consume(TokenType::Comma)?;
9883 }
9884 }
9885 self.consume(TokenType::RBrace)?;
9886 Ok(node)
9887 }
9888
9889 fn parse_cors(&mut self) -> Result<CorsDefinition, ParseError> {
9898 let tok = self.consume(TokenType::Cors)?;
9899 let name = self.consume(TokenType::Identifier)?.value;
9900 let mut node = CorsDefinition {
9901 name,
9902 loc: Loc { line: tok.line, column: tok.column },
9903 ..Default::default()
9904 };
9905 self.consume(TokenType::LBrace)?;
9906 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9907 let key = self.consume_any_ident_or_kw()?.value;
9908 self.consume(TokenType::Colon)?;
9909 match key.as_str() {
9910 "allow_origins" => node.allow_origins = self.parse_bracketed_strings()?,
9911 "allow_methods" => node.allow_methods = self.parse_bracketed_identifiers()?,
9912 "allow_headers" => node.allow_headers = self.parse_bracketed_strings()?,
9913 "allow_credentials" => {
9914 node.allow_credentials = self.consume_any_ident_or_kw()?.value == "true"
9915 }
9916 "max_age" => node.max_age = Some(self.consume(TokenType::Duration)?.value),
9917 "expose_headers" => node.expose_headers = self.parse_bracketed_strings()?,
9918 other => return Err(self.error(&format!("unknown cors field `{other}`"))),
9919 }
9920 if self.check(TokenType::Comma) {
9922 self.consume(TokenType::Comma)?;
9923 }
9924 }
9925 self.consume(TokenType::RBrace)?;
9926 Ok(node)
9927 }
9928
9929 fn parse_credential(&mut self) -> Result<CredentialDefinition, ParseError> {
9937 let tok = self.consume(TokenType::Credential)?;
9938 let name = self.consume(TokenType::Identifier)?.value;
9939 let mut node = CredentialDefinition {
9940 name,
9941 loc: Loc { line: tok.line, column: tok.column },
9942 ..Default::default()
9943 };
9944 self.consume(TokenType::LBrace)?;
9945 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9946 let key = self.consume_any_ident_or_kw()?.value;
9947 self.consume(TokenType::Colon)?;
9948 match key.as_str() {
9949 "ttl" => node.ttl = self.consume(TokenType::Duration)?.value,
9950 "grants" => {
9951 let bracket_tok = self.current().clone();
9952 let items = self.parse_bracketed_dot_identifiers()?;
9953 for slug in &items {
9954 if !is_valid_capability_slug(slug) {
9955 return Err(ParseError {
9956 message: format!(
9957 "Invalid capability slug '{slug}' in credential '{}' \
9958 `grants:`. Capability slugs must match \
9959 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
9960 lowercase identifiers starting with a letter. Examples: \
9961 `chat.invoke`, `flow.execute`.",
9962 node.name
9963 ),
9964 line: bracket_tok.line,
9965 column: bracket_tok.column,
9966 ..Default::default()
9967 });
9968 }
9969 }
9970 node.grants = items;
9971 }
9972 other => return Err(self.error(&format!("unknown credential field `{other}`"))),
9973 }
9974 if self.check(TokenType::Comma) {
9976 self.consume(TokenType::Comma)?;
9977 }
9978 }
9979 self.consume(TokenType::RBrace)?;
9980 Ok(node)
9981 }
9982
9983 fn parse_cache(&mut self) -> Result<CacheDefinition, ParseError> {
9990 let tok = self.consume(TokenType::Cache)?;
9991 let name = self.consume(TokenType::Identifier)?.value;
9992 let mut node = CacheDefinition {
9993 name,
9994 loc: Loc { line: tok.line, column: tok.column },
9995 ..Default::default()
9996 };
9997 self.consume(TokenType::LBrace)?;
9998 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
9999 let key = self.consume_any_ident_or_kw()?.value;
10000 self.consume(TokenType::Colon)?;
10001 match key.as_str() {
10002 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10003 "ttl" => node.ttl = Some(self.consume(TokenType::Duration)?.value),
10004 "key" => node.key_params = self.parse_bracketed_identifiers()?,
10005 "default" => {
10006 node.default_policy = self.consume_any_ident_or_kw()?.value == "true"
10007 }
10008 "apply_to_effects" => {
10009 node.apply_to_effects = self.parse_bracketed_identifiers()?
10010 }
10011 "invalidate_on" => node.invalidate_on = self.parse_bracketed_identifiers()?,
10012 other => return Err(self.error(&format!("unknown cache field `{other}`"))),
10013 }
10014 if self.check(TokenType::Comma) {
10015 self.consume(TokenType::Comma)?;
10016 }
10017 }
10018 self.consume(TokenType::RBrace)?;
10019 Ok(node)
10020 }
10021
10022 fn parse_document(&mut self) -> Result<crate::ast::DocumentDefinition, ParseError> {
10030 let tok = self.consume(TokenType::Document)?;
10031 let name = self.consume(TokenType::Identifier)?.value;
10032 let mut node = crate::ast::DocumentDefinition {
10033 name,
10034 loc: Loc {
10035 line: tok.line,
10036 column: tok.column,
10037 },
10038 ..Default::default()
10039 };
10040 self.consume(TokenType::LBrace)?;
10041 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10042 let field = self.current().clone();
10043 let field_name = field.value.clone();
10044 self.advance();
10045 if self.check(TokenType::Colon) {
10046 self.advance();
10047 match field_name.as_str() {
10048 "target" => node.target = self.consume_any_ident_or_kw()?.value,
10049 "template" => node.template = self.parse_dotted_identifier()?,
10050 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10051 "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10055 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10056 "effects" => node.effects = Some(self.parse_effect_row()?),
10057 other => {
10058 return Err(self.error(&format!(
10059 "unknown document field `{other}` in document `{}` — expected \
10060 `target:` / `template:` / `provenance:` / `effects:`, or a body \
10061 block (`section {{ … }}` / `slide {{ … }}` / `sheet {{ … }}`)",
10062 node.name
10063 )))
10064 }
10065 }
10066 } else if self.check(TokenType::LBrace) {
10067 node.blocks
10068 .push(self.parse_doc_block_body(field_name, field.line, field.column)?);
10069 } else {
10070 return Err(self.error(&format!(
10071 "unexpected `{field_name}` in document `{}` body — expected a `field:` or a \
10072 body block `{field_name} {{ … }}`",
10073 node.name
10074 )));
10075 }
10076 if self.check(TokenType::Comma) {
10077 self.advance();
10078 }
10079 }
10080 self.consume(TokenType::RBrace)?;
10081 Ok(node)
10082 }
10083
10084 fn parse_doc_block_body(
10090 &mut self,
10091 kind: String,
10092 line: u32,
10093 column: u32,
10094 ) -> Result<crate::ast::DocBlock, ParseError> {
10095 let mut block = crate::ast::DocBlock {
10096 kind,
10097 loc: Loc { line, column },
10098 ..Default::default()
10099 };
10100 self.consume(TokenType::LBrace)?;
10101 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10102 let name_tok = self.current().clone();
10103 let name = self.consume_any_ident_or_kw()?.value;
10104 if self.check(TokenType::Colon) {
10105 self.advance();
10106 let value = self.parse_doc_scalar()?;
10107 block.fields.push((name, value));
10108 } else if self.check(TokenType::LBrace) {
10109 let child = self.parse_doc_block_body(name, name_tok.line, name_tok.column)?;
10110 block.children.push(child);
10111 } else {
10112 return Err(self.error(&format!(
10113 "in document block `{}`: `{name}` must be a `field:` value or open a nested \
10114 block `{name} {{ … }}`",
10115 block.kind
10116 )));
10117 }
10118 if self.check(TokenType::Comma) {
10119 self.advance();
10120 }
10121 }
10122 self.consume(TokenType::RBrace)?;
10123 Ok(block)
10124 }
10125
10126 fn parse_doc_scalar(&mut self) -> Result<crate::ast::DocScalar, ParseError> {
10131 let tok = self.current().clone();
10132 match tok.ttype {
10133 TokenType::StringLit => {
10134 self.advance();
10135 Ok(crate::ast::DocScalar::Text(tok.value))
10136 }
10137 TokenType::Integer => {
10138 self.advance();
10139 Ok(crate::ast::DocScalar::Int(tok.value.parse::<i64>().unwrap_or(0)))
10140 }
10141 TokenType::Bool => {
10142 self.advance();
10143 Ok(crate::ast::DocScalar::Bool(tok.value == "true"))
10144 }
10145 TokenType::LBracket => {
10146 let items = self.parse_bracketed_strings()?;
10147 Ok(crate::ast::DocScalar::List(items))
10148 }
10149 _ => {
10150 let name = self.consume_any_ident_or_kw()?.value;
10151 Ok(crate::ast::DocScalar::Ref(name))
10152 }
10153 }
10154 }
10155
10156 fn parse_notify(&mut self) -> Result<crate::ast::NotifyDefinition, ParseError> {
10181 let tok = self.consume(TokenType::Notify)?;
10182 let name = self.consume(TokenType::Identifier)?.value;
10183 let mut node = crate::ast::NotifyDefinition {
10184 name,
10185 loc: Loc {
10186 line: tok.line,
10187 column: tok.column,
10188 },
10189 ..Default::default()
10190 };
10191 self.consume(TokenType::LBrace)?;
10192 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10193 let field = self.current().clone();
10194 let field_name = field.value.clone();
10195 self.advance();
10196 if self.check(TokenType::Colon) {
10197 self.advance();
10198 match field_name.as_str() {
10199 "channel" => node.channel = self.consume_any_ident_or_kw()?.value,
10200 "to" => {
10201 if self.current().value == "secret" && self.peek_is_lparen() {
10205 self.advance(); self.consume(TokenType::LParen)?;
10207 node.to_secret = self.parse_dotted_identifier()?;
10208 self.consume(TokenType::RParen)?;
10209 node.to_is_secret = true;
10210 } else if self.check(TokenType::StringLit) {
10211 node.to_secret = self.consume(TokenType::StringLit)?.value.clone();
10212 node.to_is_secret = false;
10213 } else {
10214 node.to_secret = self.consume_any_ident_or_kw()?.value.clone();
10215 node.to_is_secret = false;
10216 }
10217 }
10218 "template" => {
10219 node.template = self.consume(TokenType::StringLit)?.value.clone()
10220 }
10221 "window" => {
10222 if self.check(TokenType::Integer) {
10225 let n = self.consume(TokenType::Integer)?.value.clone();
10226 let unit = self.consume_any_ident_or_kw()?.value.clone();
10227 node.window = format!("{n}{unit}");
10228 } else {
10229 node.window = self.consume_any_ident_or_kw()?.value.clone();
10230 }
10231 }
10232 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10233 "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10237 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10238 "effects" => node.effects = Some(self.parse_effect_row()?),
10239 other => {
10240 return Err(self.error(&format!(
10241 "unknown notify field `{other}` in notify `{}` — expected \
10242 `channel:` / `to:` / `template:` / `window:` / `provenance:` / \
10243 `effects:`",
10244 node.name
10245 )))
10246 }
10247 }
10248 }
10249 }
10250 self.consume(TokenType::RBrace)?;
10251 Ok(node)
10252 }
10253
10254 fn peek_is_identifier(&self) -> bool {
10258 self.tokens
10259 .get(self.pos + 1)
10260 .map(|t| t.ttype == TokenType::Identifier)
10261 .unwrap_or(false)
10262 }
10263
10264 fn peek_is_lparen(&self) -> bool {
10265 self.tokens
10266 .get(self.pos + 1)
10267 .map(|t| t.ttype == TokenType::LParen)
10268 .unwrap_or(false)
10269 }
10270
10271 fn parse_deliver(&mut self) -> Result<crate::ast::DeliverDefinition, ParseError> {
10273 let tok = self.consume(TokenType::Deliver)?;
10274 let name = self.consume(TokenType::Identifier)?.value;
10275 let mut node = crate::ast::DeliverDefinition {
10276 name,
10277 loc: Loc {
10278 line: tok.line,
10279 column: tok.column,
10280 },
10281 ..Default::default()
10282 };
10283 self.consume(TokenType::LBrace)?;
10284 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10285 let field = self.current().clone();
10286 let field_name = field.value.clone();
10287 self.advance();
10288 if self.check(TokenType::Colon) {
10289 self.advance();
10290 match field_name.as_str() {
10291 "target" => node.target = self.consume_any_ident_or_kw()?.value,
10292 "provenance" => node.provenance = self.consume_any_ident_or_kw()?.value,
10293 "payload" => node.payload = self.consume_any_ident_or_kw()?.value,
10297 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
10298 "secret" => node.secret = self.consume_any_ident_or_kw()?.value,
10299 "effects" => node.effects = Some(self.parse_effect_row()?),
10300 other => {
10301 return Err(self.error(&format!(
10302 "unknown deliver field `{other}` in deliver `{}` — expected \
10303 `target:` / `provenance:` / `secret:` / `effects:`, or an operation \
10304 block (`upsert_contact {{ … }}` / `create_deal {{ … }}` / \
10305 `add_note {{ … }}`)",
10306 node.name
10307 )))
10308 }
10309 }
10310 } else if self.check(TokenType::LBrace) {
10311 node.ops
10312 .push(self.parse_deliver_op(field_name, field.line, field.column)?);
10313 } else {
10314 return Err(self.error(&format!(
10315 "unexpected `{field_name}` in deliver `{}` body — expected a `field:` or an \
10316 operation block `{field_name} {{ … }}`",
10317 node.name
10318 )));
10319 }
10320 if self.check(TokenType::Comma) {
10321 self.advance();
10322 }
10323 }
10324 self.consume(TokenType::RBrace)?;
10325 Ok(node)
10326 }
10327
10328 fn parse_deliver_op(
10332 &mut self,
10333 kind: String,
10334 line: u32,
10335 column: u32,
10336 ) -> Result<crate::ast::DeliverOp, ParseError> {
10337 let mut op = crate::ast::DeliverOp {
10338 kind,
10339 loc: Loc { line, column },
10340 ..Default::default()
10341 };
10342 self.consume(TokenType::LBrace)?;
10343 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10344 let name = self.consume_any_ident_or_kw()?.value;
10345 self.consume(TokenType::Colon).map_err(|_| {
10346 self.error(&format!(
10347 "in deliver operation `{}`: `{name}` must be a `field: value` pair — an \
10348 operation binds scalar fields, it takes no nested blocks",
10349 op.kind
10350 ))
10351 })?;
10352 let value = self.parse_doc_scalar()?;
10353 op.fields.push((name, value));
10354 if self.check(TokenType::Comma) {
10355 self.advance();
10356 }
10357 }
10358 self.consume(TokenType::RBrace)?;
10359 Ok(op)
10360 }
10361
10362 fn parse_savant(&mut self) -> Result<SavantDefinition, ParseError> {
10368 let tok = self.consume(TokenType::Savant)?;
10369 let name = self.consume(TokenType::Identifier)?.value;
10370 let mut node = SavantDefinition {
10371 name,
10372 loc: Loc {
10373 line: tok.line,
10374 column: tok.column,
10375 },
10376 ..Default::default()
10377 };
10378 self.consume(TokenType::LBrace)?;
10379 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10380 let field = self.current().clone();
10381 let field_name = field.value.clone();
10382 self.advance();
10383 if self.check(TokenType::Colon) {
10384 self.advance();
10385 match field_name.as_str() {
10386 "domain" => node.domain = self.consume(TokenType::StringLit)?.value,
10387 other => {
10388 return Err(self.error(&format!(
10389 "unknown savant field `{other}` in savant `{}` — expected \
10390 `domain:` or a `cognition` / `memory` / `budget` / `mandate` block",
10391 node.name
10392 )))
10393 }
10394 }
10395 } else if field_name == "cognition" {
10396 node.cognition = Some(self.parse_savant_cognition(field.line, field.column)?);
10397 } else if field_name == "memory" {
10398 node.memory = Some(self.parse_savant_memory(field.line, field.column)?);
10399 } else if field_name == "budget" {
10400 node.budget = Some(self.parse_savant_budget(field.line, field.column)?);
10401 } else if field_name == "mandate" {
10402 node.mandates
10403 .push(self.parse_savant_mandate(field.line, field.column)?);
10404 } else {
10405 return Err(self.error(&format!(
10406 "unexpected `{field_name}` in savant `{}` body — expected `domain:` or a \
10407 `cognition` / `memory` / `budget` / `mandate` block",
10408 node.name
10409 )));
10410 }
10411 if self.check(TokenType::Comma) {
10412 self.advance();
10413 }
10414 }
10415 self.consume(TokenType::RBrace)?;
10416 Ok(node)
10417 }
10418
10419 fn parse_savant_cognition(
10422 &mut self,
10423 line: u32,
10424 column: u32,
10425 ) -> Result<SavantCognition, ParseError> {
10426 self.consume(TokenType::LBrace)?;
10427 let mut node = SavantCognition {
10428 loc: Loc { line, column },
10429 ..Default::default()
10430 };
10431 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10432 let key = self.consume_any_ident_or_kw()?.value;
10433 self.consume(TokenType::Colon)?;
10434 match key.as_str() {
10435 "depth" => node.depth = self.consume_any_ident_or_kw()?.value,
10436 "entropic_threshold" => node.entropic_threshold = self.parse_optional_float(),
10437 "divergence" => node.divergence = self.consume_any_ident_or_kw()?.value,
10438 other => {
10439 return Err(self.error(&format!(
10440 "unknown savant `cognition` field `{other}` — expected \
10441 `depth` / `entropic_threshold` / `divergence`"
10442 )))
10443 }
10444 }
10445 if self.check(TokenType::Comma) {
10446 self.advance();
10447 }
10448 }
10449 self.consume(TokenType::RBrace)?;
10450 Ok(node)
10451 }
10452
10453 fn parse_savant_memory(
10456 &mut self,
10457 line: u32,
10458 column: u32,
10459 ) -> Result<SavantMemory, ParseError> {
10460 self.consume(TokenType::LBrace)?;
10461 let mut node = SavantMemory {
10462 loc: Loc { line, column },
10463 ..Default::default()
10464 };
10465 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10466 let key = self.consume_any_ident_or_kw()?.value;
10467 self.consume(TokenType::Colon)?;
10468 match key.as_str() {
10469 "backend" => node.backend = self.consume_any_ident_or_kw()?.value,
10470 "corpus_graph" => {
10471 node.corpus_graph = self.consume_any_ident_or_kw()?.value == "true"
10472 }
10473 "isolation_level" => node.isolation_level = self.consume_any_ident_or_kw()?.value,
10474 other => {
10475 return Err(self.error(&format!(
10476 "unknown savant `memory` field `{other}` — expected \
10477 `backend` / `corpus_graph` / `isolation_level`"
10478 )))
10479 }
10480 }
10481 if self.check(TokenType::Comma) {
10482 self.advance();
10483 }
10484 }
10485 self.consume(TokenType::RBrace)?;
10486 Ok(node)
10487 }
10488
10489 fn parse_savant_budget(
10492 &mut self,
10493 line: u32,
10494 column: u32,
10495 ) -> Result<SavantBudget, ParseError> {
10496 self.consume(TokenType::LBrace)?;
10497 let mut node = SavantBudget {
10498 loc: Loc { line, column },
10499 ..Default::default()
10500 };
10501 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10502 let key = self.consume_any_ident_or_kw()?.value;
10503 self.consume(TokenType::Colon)?;
10504 match key.as_str() {
10505 "max_iterations" => node.max_iterations = self.parse_optional_int(),
10506 "max_tool_synth" => node.max_tool_synth = self.parse_optional_int(),
10507 other => {
10508 return Err(self.error(&format!(
10509 "unknown savant `budget` field `{other}` — expected \
10510 `max_iterations` / `max_tool_synth`"
10511 )))
10512 }
10513 }
10514 if self.check(TokenType::Comma) {
10515 self.advance();
10516 }
10517 }
10518 self.consume(TokenType::RBrace)?;
10519 Ok(node)
10520 }
10521
10522 fn parse_savant_mandate(
10525 &mut self,
10526 line: u32,
10527 column: u32,
10528 ) -> Result<SavantMandate, ParseError> {
10529 let name = self.consume(TokenType::Identifier)?.value;
10530 let mut node = SavantMandate {
10531 name,
10532 loc: Loc { line, column },
10533 ..Default::default()
10534 };
10535 self.consume(TokenType::LBrace)?;
10536 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10537 let key = self.consume_any_ident_or_kw()?.value;
10538 self.consume(TokenType::Colon)?;
10539 match key.as_str() {
10540 "objective" => node.objective = self.consume(TokenType::StringLit)?.value,
10541 "output" => node.output_type = self.consume_any_ident_or_kw()?.value,
10542 other => {
10543 return Err(self.error(&format!(
10544 "unknown savant `mandate` field `{other}` — expected `objective` / `output`"
10545 )))
10546 }
10547 }
10548 if self.check(TokenType::Comma) {
10549 self.advance();
10550 }
10551 }
10552 self.consume(TokenType::RBrace)?;
10553 Ok(node)
10554 }
10555
10556 fn parse_synth(&mut self) -> Result<SynthDefinition, ParseError> {
10561 let tok = self.consume(TokenType::Synth)?;
10562 let name = self.consume(TokenType::Identifier)?.value;
10563 let mut node = SynthDefinition {
10564 name,
10565 loc: Loc {
10566 line: tok.line,
10567 column: tok.column,
10568 },
10569 ..Default::default()
10570 };
10571 self.consume(TokenType::LBrace)?;
10572 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10573 let key = self.consume_any_ident_or_kw()?.value;
10574 self.consume(TokenType::Colon)?;
10575 match key.as_str() {
10576 "target" => node.target = self.consume(TokenType::StringLit)?.value,
10577 "risk" => node.risk = self.consume_any_ident_or_kw()?.value,
10578 "language" => node.language = self.consume_any_ident_or_kw()?.value,
10579 "sandbox" => node.sandbox = self.consume_any_ident_or_kw()?.value,
10580 "review" => node.review = self.consume_any_ident_or_kw()?.value,
10581 "max_lines" => node.max_lines = self.parse_optional_int(),
10582 other => {
10583 return Err(self.error(&format!(
10584 "unknown synth field `{other}` in synth `{}` — expected `target` / `risk` \
10585 / `language` / `sandbox` / `review` / `max_lines`",
10586 node.name
10587 )))
10588 }
10589 }
10590 if self.check(TokenType::Comma) {
10591 self.consume(TokenType::Comma)?;
10592 }
10593 }
10594 self.consume(TokenType::RBrace)?;
10595 Ok(node)
10596 }
10597
10598 fn parse_voice(&mut self) -> Result<VoiceDefinition, ParseError> {
10603 let tok = self.consume(TokenType::Voice)?;
10604 let name = self.consume(TokenType::Identifier)?.value;
10605 let mut node = VoiceDefinition {
10606 name,
10607 loc: Loc { line: tok.line, column: tok.column },
10608 ..Default::default()
10609 };
10610 self.consume(TokenType::LBrace)?;
10611 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10612 let key = self.consume_any_ident_or_kw()?.value;
10613 self.consume(TokenType::Colon)?;
10614 match key.as_str() {
10615 "stt" => node.stt = Some(self.parse_upstream_ref()?),
10617 "tts" => node.tts = Some(self.parse_upstream_ref()?),
10618 "realtime" => node.realtime = Some(self.parse_upstream_ref()?),
10619 "carrier" => node.carrier = self.consume_any_ident_or_kw()?.value,
10620 "interruptible" => {
10621 let v = self.consume_any_ident_or_kw()?.value;
10622 node.interruptible = v == "true";
10623 }
10624 "legal_basis" => node.legal_basis = Some(self.consume_any_ident_or_kw()?.value),
10625 "persona" => node.persona = Some(self.consume(TokenType::Identifier)?.value),
10626 "context" => node.context = Some(self.consume(TokenType::Identifier)?.value),
10627 other => return Err(self.error(&format!("unknown voice field `{other}`"))),
10628 }
10629 if self.check(TokenType::Comma) {
10630 self.consume(TokenType::Comma)?;
10631 }
10632 }
10633 self.consume(TokenType::RBrace)?;
10634 Ok(node)
10635 }
10636
10637 fn parse_upstream_ref(&mut self) -> Result<String, ParseError> {
10640 let base = self.consume(TokenType::Identifier)?.value;
10641 if self.check(TokenType::At) {
10642 self.advance();
10643 let version = self.consume_any_ident_or_kw()?.value;
10644 Ok(format!("{base}@{version}"))
10645 } else {
10646 Ok(base)
10647 }
10648 }
10649
10650 fn parse_upstream_map(&mut self) -> Result<Vec<UpstreamMapRule>, ParseError> {
10656 self.consume(TokenType::LBracket)?;
10657 let mut rules = Vec::new();
10658 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10659 let dir_tok = self.current().clone();
10660 let direction = match dir_tok.ttype {
10661 TokenType::Send => "send",
10662 TokenType::Receive => "receive",
10663 _ => {
10664 return Err(self.error(&format!(
10665 "upstream map rule must start with `send` or `receive`, got `{}`",
10666 dir_tok.value
10667 )))
10668 }
10669 };
10670 self.advance();
10671 let message = self.consume(TokenType::Identifier)?.value;
10672 self.consume(TokenType::As)?;
10673 let framing = self.consume_any_ident_or_kw()?.value;
10674 let mut rule = UpstreamMapRule {
10675 direction: direction.to_string(),
10676 message,
10677 framing,
10678 loc: Loc { line: dir_tok.line, column: dir_tok.column },
10679 ..Default::default()
10680 };
10681 if self.current().value == "tag" {
10683 self.advance();
10684 rule.tag = Some(self.consume(TokenType::StringLit)?.value);
10685 } else if self.current().value == "when" {
10686 self.advance();
10691 rule.when_field = Some(self.consume(TokenType::StringLit)?.value);
10692 if self.check(TokenType::Assign) {
10693 self.advance();
10694 rule.when_value = Some(self.consume(TokenType::StringLit)?.value);
10695 }
10696 }
10697 rules.push(rule);
10698 if self.check(TokenType::Comma) {
10699 self.advance();
10700 }
10701 }
10702 self.consume(TokenType::RBracket)?;
10703 Ok(rules)
10704 }
10705
10706 fn parse_upstream_reconnect(&mut self) -> Result<UpstreamReconnect, ParseError> {
10710 self.consume(TokenType::LBrace)?;
10711 let mut backoff_ms: Option<i64> = None;
10712 let mut max_attempts: Option<i64> = None;
10713 let mut on_exhausted: Option<String> = None;
10714 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10715 let key = self.consume_any_ident_or_kw()?.value;
10716 self.consume(TokenType::Colon)?;
10717 match key.as_str() {
10718 "backoff_ms" => {
10719 backoff_ms = Some(
10720 self.consume(TokenType::Integer)?
10721 .value
10722 .parse::<i64>()
10723 .map_err(|_| self.error("backoff_ms must be an integer"))?,
10724 )
10725 }
10726 "max_attempts" => {
10727 max_attempts = Some(
10728 self.consume(TokenType::Integer)?
10729 .value
10730 .parse::<i64>()
10731 .map_err(|_| self.error("max_attempts must be an integer"))?,
10732 )
10733 }
10734 "on_exhausted" => on_exhausted = Some(self.consume_any_ident_or_kw()?.value),
10735 other => return Err(self.error(&format!("unknown reconnect field `{other}`"))),
10736 }
10737 if self.check(TokenType::Comma) {
10738 self.consume(TokenType::Comma)?;
10739 }
10740 }
10741 self.consume(TokenType::RBrace)?;
10742 match (backoff_ms, max_attempts, on_exhausted) {
10743 (Some(b), Some(m), Some(o)) => Ok(UpstreamReconnect { backoff_ms: b, max_attempts: m, on_exhausted: o }),
10744 _ => Err(self.error(
10745 "reconnect requires all of `backoff_ms:`, `max_attempts:`, `on_exhausted:` — a reconnection policy with a hole is not a policy",
10746 )),
10747 }
10748 }
10749
10750 fn parse_session_steps(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10752 self.consume(TokenType::LBracket)?;
10753 let mut steps = Vec::new();
10754 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10755 steps.push(self.parse_session_step()?);
10756 if self.check(TokenType::Comma) {
10757 self.advance();
10758 }
10759 }
10760 self.consume(TokenType::RBracket)?;
10761 Ok(steps)
10762 }
10763
10764 fn parse_session_step_block(&mut self) -> Result<Vec<SessionStep>, ParseError> {
10768 self.consume(TokenType::LBrace)?;
10769 let mut steps = Vec::new();
10770 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10771 steps.push(self.parse_session_step()?);
10772 if self.check(TokenType::Comma) {
10773 self.advance();
10774 }
10775 }
10776 self.consume(TokenType::RBrace)?;
10777 Ok(steps)
10778 }
10779
10780 fn parse_session_step(&mut self) -> Result<SessionStep, ParseError> {
10781 let tok = self.current().clone();
10782 let loc = Loc { line: tok.line, column: tok.column };
10783 match tok.ttype {
10784 TokenType::Send => {
10785 self.advance();
10786 let msg = self.consume_any_ident_or_kw()?;
10787 Ok(SessionStep { op: "send".into(), message_type: msg.value, loc, ..Default::default() })
10788 }
10789 TokenType::Receive => {
10790 self.advance();
10791 let msg = self.consume_any_ident_or_kw()?;
10792 Ok(SessionStep { op: "receive".into(), message_type: msg.value, loc, ..Default::default() })
10793 }
10794 TokenType::Loop => {
10795 self.advance();
10796 Ok(SessionStep { op: "loop".into(), loc, ..Default::default() })
10797 }
10798 TokenType::End => {
10799 self.advance();
10800 Ok(SessionStep { op: "end".into(), loc, ..Default::default() })
10801 }
10802 TokenType::Identifier if tok.value == "select" || tok.value == "branch" => {
10805 self.parse_session_choice(&tok.value, loc)
10806 }
10807 TokenType::Identifier if tok.value == "interrupt" => {
10810 self.parse_session_interrupt(loc)
10811 }
10812 _ if tok.value == "resume" => {
10830 self.advance();
10831 Ok(SessionStep { op: "resume".into(), loc, ..Default::default() })
10832 }
10833 _ => Err(ParseError {
10834 message: format!(
10835 "Invalid session step '{}' — expected send | receive | loop | end | select | branch | interrupt | resume",
10836 tok.value
10837 ),
10838 line: tok.line,
10839 column: tok.column,
10840 ..Default::default()
10841 }),
10842 }
10843 }
10844
10845 fn consume_contextual(&mut self, kw: &str) -> Result<(), ParseError> {
10850 let t = self.current().clone();
10851 if t.value != kw {
10852 return Err(ParseError {
10853 message: format!("expected `{kw}` in interrupt step, got `{}`", t.value),
10854 line: t.line,
10855 column: t.column,
10856 ..Default::default()
10857 });
10858 }
10859 self.advance();
10860 Ok(())
10861 }
10862
10863 fn parse_session_interrupt(&mut self, loc: Loc) -> Result<SessionStep, ParseError> {
10871 self.advance(); let body = self.parse_session_step_block()?;
10875 self.consume_contextual("on")?;
10877 let signal = self.consume_any_ident_or_kw()?;
10878 self.consume_contextual("as")?;
10880 let binder = self.consume_any_ident_or_kw()?;
10881 self.consume_contextual("resumable")?;
10883 let handler = self.parse_session_step_block()?;
10884 Ok(SessionStep {
10885 op: "interrupt".into(),
10886 message_type: signal.value,
10887 branches: vec![
10888 SessionBranch { label: "body".into(), steps: body, loc: loc.clone() },
10889 SessionBranch { label: "handler".into(), steps: handler, loc: loc.clone() },
10890 ],
10891 binder: binder.value,
10892 resumable: true,
10893 loc,
10894 })
10895 }
10896
10897 fn parse_session_choice(&mut self, op: &str, loc: Loc) -> Result<SessionStep, ParseError> {
10900 self.advance(); self.consume(TokenType::LBrace)?;
10902 let mut branches = Vec::new();
10903 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10904 let label_tok = self.consume_any_ident_or_kw()?;
10905 self.consume(TokenType::Colon)?;
10906 let steps = self.parse_session_steps()?;
10907 branches.push(SessionBranch {
10908 label: label_tok.value,
10909 steps,
10910 loc: Loc { line: label_tok.line, column: label_tok.column },
10911 });
10912 if self.check(TokenType::Comma) {
10913 self.advance();
10914 }
10915 }
10916 self.consume(TokenType::RBrace)?;
10917 Ok(SessionStep { op: op.to_string(), branches, loc, ..Default::default() })
10918 }
10919
10920 fn parse_topology(&mut self) -> Result<TopologyDefinition, ParseError> {
10922 let tok = self.consume(TokenType::Topology)?;
10923 let name = self.consume(TokenType::Identifier)?.value;
10924 let mut node = TopologyDefinition {
10925 name,
10926 nodes: Vec::new(),
10927 edges: Vec::new(),
10928 loc: Loc {
10929 line: tok.line,
10930 column: tok.column,
10931 },
10932 leading_trivia: Vec::new(),
10933 trailing_trivia: Vec::new(),
10934 };
10935 self.consume(TokenType::LBrace)?;
10936 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
10937 let field_name = self.current().value.clone();
10938 self.advance();
10939 if !self.check(TokenType::Colon) {
10940 if self.check(TokenType::LBrace) {
10941 self.skip_braced_block()?;
10942 }
10943 continue;
10944 }
10945 self.advance();
10946 match field_name.as_str() {
10947 "nodes" => node.nodes = self.parse_bracketed_identifiers()?,
10948 "edges" => node.edges = self.parse_topology_edges()?,
10949 _ => self.skip_value(),
10950 }
10951 }
10952 self.consume(TokenType::RBrace)?;
10953 Ok(node)
10954 }
10955
10956 fn parse_topology_edges(&mut self) -> Result<Vec<TopologyEdge>, ParseError> {
10957 self.consume(TokenType::LBracket)?;
10958 let mut edges = Vec::new();
10959 while !self.check(TokenType::RBracket) && !self.check(TokenType::Eof) {
10960 edges.push(self.parse_topology_edge()?);
10961 if self.check(TokenType::Comma) {
10962 self.advance();
10963 }
10964 }
10965 self.consume(TokenType::RBracket)?;
10966 Ok(edges)
10967 }
10968
10969 fn parse_topology_edge(&mut self) -> Result<TopologyEdge, ParseError> {
10970 let src_tok = self.consume_any_ident_or_kw()?;
10971 self.consume(TokenType::Arrow)?;
10972 let tgt_tok = self.consume_any_ident_or_kw()?;
10973 self.consume(TokenType::Colon)?;
10974 let sess_tok = self.consume_any_ident_or_kw()?;
10975 Ok(TopologyEdge {
10976 source: src_tok.value,
10977 target: tgt_tok.value,
10978 session_ref: sess_tok.value,
10979 loc: Loc {
10980 line: src_tok.line,
10981 column: src_tok.column,
10982 },
10983 })
10984 }
10985
10986 fn parse_immune(&mut self) -> Result<ImmuneDefinition, ParseError> {
10990 let tok = self.consume(TokenType::Immune)?;
10991 let name = self.consume(TokenType::Identifier)?.value;
10992 let mut node = ImmuneDefinition {
10993 name,
10994 watch: Vec::new(),
10995 sensitivity: None,
10996 baseline: "learned".to_string(),
10997 window: 100,
10998 scope: String::new(),
10999 tau: String::new(),
11000 decay: "exponential".to_string(),
11001 loc: Loc {
11002 line: tok.line,
11003 column: tok.column,
11004 },
11005 leading_trivia: Vec::new(),
11006 trailing_trivia: Vec::new(),
11007 };
11008 self.consume(TokenType::LBrace)?;
11009 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11010 let field_name = self.current().value.clone();
11011 self.advance();
11012 if !self.check(TokenType::Colon) {
11013 if self.check(TokenType::LBrace) {
11014 self.skip_braced_block()?;
11015 }
11016 continue;
11017 }
11018 self.advance();
11019 match field_name.as_str() {
11020 "watch" => node.watch = self.parse_bracketed_identifiers()?,
11021 "sensitivity" => node.sensitivity = self.parse_optional_float(),
11022 "baseline" => node.baseline = self.consume_any_ident_or_kw()?.value,
11023 "window" => {
11024 if let Some(v) = self.parse_optional_int() {
11025 node.window = v;
11026 }
11027 }
11028 "scope" => {
11029 let s_tok = self.consume_any_ident_or_kw()?;
11030 let s = s_tok.value;
11031 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11032 return Err(ParseError {
11033 message: format!(
11034 "Invalid scope '{s}' in immune '{}' — \
11035 expected tenant | flow | global",
11036 node.name
11037 ),
11038 line: s_tok.line,
11039 column: s_tok.column,
11040 ..Default::default()
11041 });
11042 }
11043 node.scope = s;
11044 }
11045 "tau" => {
11046 let t = self.current().clone();
11047 match t.ttype {
11048 TokenType::Duration | TokenType::StringLit => {
11049 self.advance();
11050 node.tau = t.value;
11051 }
11052 _ => node.tau = self.consume_any_ident_or_kw()?.value,
11053 }
11054 }
11055 "decay" => {
11056 let d_tok = self.consume_any_ident_or_kw()?;
11057 let d = d_tok.value;
11058 if !matches!(d.as_str(), "exponential" | "linear" | "none") {
11059 return Err(ParseError {
11060 message: format!(
11061 "Invalid decay '{d}' in immune '{}' — \
11062 expected exponential | linear | none",
11063 node.name
11064 ),
11065 line: d_tok.line,
11066 column: d_tok.column,
11067 ..Default::default()
11068 });
11069 }
11070 node.decay = d;
11071 }
11072 _ => self.skip_value(),
11073 }
11074 }
11075 self.consume(TokenType::RBrace)?;
11076 Ok(node)
11077 }
11078
11079 fn parse_reflex(&mut self) -> Result<ReflexDefinition, ParseError> {
11081 let tok = self.consume(TokenType::Reflex)?;
11082 let name = self.consume(TokenType::Identifier)?.value;
11083 let mut node = ReflexDefinition {
11084 name,
11085 trigger: String::new(),
11086 on_level: "doubt".to_string(),
11087 action: String::new(),
11088 scope: String::new(),
11089 sla: String::new(),
11090 loc: Loc {
11091 line: tok.line,
11092 column: tok.column,
11093 },
11094 leading_trivia: Vec::new(),
11095 trailing_trivia: Vec::new(),
11096 };
11097 self.consume(TokenType::LBrace)?;
11098 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11099 let field_name = self.current().value.clone();
11100 self.advance();
11101 if !self.check(TokenType::Colon) {
11102 if self.check(TokenType::LBrace) {
11103 self.skip_braced_block()?;
11104 }
11105 continue;
11106 }
11107 self.advance();
11108 match field_name.as_str() {
11109 "trigger" => node.trigger = self.consume_any_ident_or_kw()?.value,
11110 "on_level" => {
11111 let l_tok = self.consume_any_ident_or_kw()?;
11112 let l = l_tok.value;
11113 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11114 return Err(ParseError {
11115 message: format!(
11116 "Invalid on_level '{l}' in reflex '{}' — \
11117 expected know | believe | speculate | doubt",
11118 node.name
11119 ),
11120 line: l_tok.line,
11121 column: l_tok.column,
11122 ..Default::default()
11123 });
11124 }
11125 node.on_level = l;
11126 }
11127 "action" => {
11128 let a_tok = self.consume_any_ident_or_kw()?;
11129 let a = a_tok.value;
11130 if !matches!(
11131 a.as_str(),
11132 "drop"
11133 | "revoke"
11134 | "emit"
11135 | "redact"
11136 | "quarantine"
11137 | "terminate"
11138 | "alert"
11139 ) {
11140 return Err(ParseError {
11141 message: format!(
11142 "Invalid action '{a}' in reflex '{}' — \
11143 expected drop | revoke | emit | redact | \
11144 quarantine | terminate | alert",
11145 node.name
11146 ),
11147 line: a_tok.line,
11148 column: a_tok.column,
11149 ..Default::default()
11150 });
11151 }
11152 node.action = a;
11153 }
11154 "scope" => {
11155 let s_tok = self.consume_any_ident_or_kw()?;
11156 let s = s_tok.value;
11157 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11158 return Err(ParseError {
11159 message: format!(
11160 "Invalid scope '{s}' in reflex '{}' — \
11161 expected tenant | flow | global",
11162 node.name
11163 ),
11164 line: s_tok.line,
11165 column: s_tok.column,
11166 ..Default::default()
11167 });
11168 }
11169 node.scope = s;
11170 }
11171 "sla" => {
11172 let t = self.current().clone();
11173 match t.ttype {
11174 TokenType::Duration | TokenType::StringLit => {
11175 self.advance();
11176 node.sla = t.value;
11177 }
11178 _ => node.sla = self.consume_any_ident_or_kw()?.value,
11179 }
11180 }
11181 _ => self.skip_value(),
11182 }
11183 }
11184 self.consume(TokenType::RBrace)?;
11185 Ok(node)
11186 }
11187
11188 fn parse_heal(&mut self) -> Result<HealDefinition, ParseError> {
11190 let tok = self.consume(TokenType::Heal)?;
11191 let name = self.consume(TokenType::Identifier)?.value;
11192 let mut node = HealDefinition {
11193 name,
11194 source: String::new(),
11195 on_level: "doubt".to_string(),
11196 mode: "human_in_loop".to_string(),
11197 scope: String::new(),
11198 review_sla: String::new(),
11199 shield_ref: String::new(),
11200 max_patches: 3,
11201 loc: Loc {
11202 line: tok.line,
11203 column: tok.column,
11204 },
11205 leading_trivia: Vec::new(),
11206 trailing_trivia: Vec::new(),
11207 };
11208 self.consume(TokenType::LBrace)?;
11209 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11210 let field_name = self.current().value.clone();
11211 self.advance();
11212 if !self.check(TokenType::Colon) {
11213 if self.check(TokenType::LBrace) {
11214 self.skip_braced_block()?;
11215 }
11216 continue;
11217 }
11218 self.advance();
11219 match field_name.as_str() {
11220 "source" => node.source = self.consume_any_ident_or_kw()?.value,
11221 "on_level" => {
11222 let l_tok = self.consume_any_ident_or_kw()?;
11223 let l = l_tok.value;
11224 if !matches!(l.as_str(), "know" | "believe" | "speculate" | "doubt") {
11225 return Err(ParseError {
11226 message: format!(
11227 "Invalid on_level '{l}' in heal '{}' — \
11228 expected know | believe | speculate | doubt",
11229 node.name
11230 ),
11231 line: l_tok.line,
11232 column: l_tok.column,
11233 ..Default::default()
11234 });
11235 }
11236 node.on_level = l;
11237 }
11238 "mode" => {
11239 let m_tok = self.consume_any_ident_or_kw()?;
11240 let m = m_tok.value;
11241 if !matches!(m.as_str(), "audit_only" | "human_in_loop" | "adversarial") {
11242 return Err(ParseError {
11243 message: format!(
11244 "Invalid mode '{m}' in heal '{}' — \
11245 expected audit_only | human_in_loop | adversarial",
11246 node.name
11247 ),
11248 line: m_tok.line,
11249 column: m_tok.column,
11250 ..Default::default()
11251 });
11252 }
11253 node.mode = m;
11254 }
11255 "scope" => {
11256 let s_tok = self.consume_any_ident_or_kw()?;
11257 let s = s_tok.value;
11258 if !matches!(s.as_str(), "tenant" | "flow" | "global") {
11259 return Err(ParseError {
11260 message: format!(
11261 "Invalid scope '{s}' in heal '{}' — \
11262 expected tenant | flow | global",
11263 node.name
11264 ),
11265 line: s_tok.line,
11266 column: s_tok.column,
11267 ..Default::default()
11268 });
11269 }
11270 node.scope = s;
11271 }
11272 "review_sla" => {
11273 let t = self.current().clone();
11274 match t.ttype {
11275 TokenType::Duration | TokenType::StringLit => {
11276 self.advance();
11277 node.review_sla = t.value;
11278 }
11279 _ => node.review_sla = self.consume_any_ident_or_kw()?.value,
11280 }
11281 }
11282 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
11283 "max_patches" => {
11284 if let Some(v) = self.parse_optional_int() {
11285 node.max_patches = v;
11286 }
11287 }
11288 _ => self.skip_value(),
11289 }
11290 }
11291 self.consume(TokenType::RBrace)?;
11292 Ok(node)
11293 }
11294
11295 fn parse_component(&mut self) -> Result<ComponentDefinition, ParseError> {
11299 let tok = self.consume(TokenType::Component)?;
11300 let name = self.consume(TokenType::Identifier)?.value;
11301 let mut node = ComponentDefinition {
11302 name,
11303 renders: String::new(),
11304 via_shield: String::new(),
11305 on_interact: String::new(),
11306 render_hint: "custom".to_string(),
11307 loc: Loc {
11308 line: tok.line,
11309 column: tok.column,
11310 },
11311 leading_trivia: Vec::new(),
11312 trailing_trivia: Vec::new(),
11313 };
11314 self.consume(TokenType::LBrace)?;
11315 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11316 let field_name = self.current().value.clone();
11317 self.advance();
11318 if !self.check(TokenType::Colon) {
11319 if self.check(TokenType::LBrace) {
11320 self.skip_braced_block()?;
11321 }
11322 continue;
11323 }
11324 self.advance();
11325 match field_name.as_str() {
11326 "renders" => node.renders = self.consume_any_ident_or_kw()?.value,
11327 "via_shield" => node.via_shield = self.consume_any_ident_or_kw()?.value,
11328 "on_interact" => node.on_interact = self.consume_any_ident_or_kw()?.value,
11329 "render_hint" => {
11330 let h_tok = self.consume_any_ident_or_kw()?;
11331 let h = h_tok.value;
11332 if !matches!(h.as_str(), "card" | "list" | "form" | "chart" | "custom") {
11333 return Err(ParseError {
11334 message: format!(
11335 "Invalid render_hint '{h}' in component '{}' — \
11336 expected card | list | form | chart | custom",
11337 node.name
11338 ),
11339 line: h_tok.line,
11340 column: h_tok.column,
11341 ..Default::default()
11342 });
11343 }
11344 node.render_hint = h;
11345 }
11346 _ => self.skip_value(),
11347 }
11348 }
11349 self.consume(TokenType::RBrace)?;
11350 Ok(node)
11351 }
11352
11353 fn parse_view(&mut self) -> Result<ViewDefinition, ParseError> {
11355 let tok = self.consume(TokenType::View)?;
11356 let name = self.consume(TokenType::Identifier)?.value;
11357 let mut node = ViewDefinition {
11358 name,
11359 title: String::new(),
11360 components: Vec::new(),
11361 route: String::new(),
11362 loc: Loc {
11363 line: tok.line,
11364 column: tok.column,
11365 },
11366 leading_trivia: Vec::new(),
11367 trailing_trivia: Vec::new(),
11368 };
11369 self.consume(TokenType::LBrace)?;
11370 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11371 let field_name = self.current().value.clone();
11372 self.advance();
11373 if !self.check(TokenType::Colon) {
11374 if self.check(TokenType::LBrace) {
11375 self.skip_braced_block()?;
11376 }
11377 continue;
11378 }
11379 self.advance();
11380 match field_name.as_str() {
11381 "title" => node.title = self.consume(TokenType::StringLit)?.value,
11382 "components" => node.components = self.parse_bracketed_identifiers()?,
11383 "route" => node.route = self.consume(TokenType::StringLit)?.value,
11384 _ => self.skip_value(),
11385 }
11386 }
11387 self.consume(TokenType::RBrace)?;
11388 Ok(node)
11389 }
11390
11391 fn parse_axonendpoint(&mut self) -> Result<AxonEndpointDefinition, ParseError> {
11392 let tok = self.consume(TokenType::AxonEndpoint)?;
11393 let name = self.consume(TokenType::Identifier)?.value;
11394 let mut node = AxonEndpointDefinition {
11395 name,
11396 method: String::new(),
11397 path: String::new(),
11398 body_type: String::new(),
11399 execute_flow: String::new(),
11400 output_type: String::new(),
11401 shield_ref: String::new(),
11402 cors_ref: String::new(),
11405 retries: None,
11406 timeout: String::new(),
11407 compliance: Vec::new(),
11408 transport: "json".to_string(),
11410 keepalive: String::new(),
11411 transport_explicit: false,
11417 implicit_transport: String::new(),
11418 requires_capabilities: Vec::new(),
11420 public: false,
11423 replay_explicit: false,
11427 replay: false,
11428 transport_dialect: String::new(),
11433 has_algebraic_stream_effect: false,
11439 backend: String::new(),
11444 path_params: Vec::new(),
11449 query_params: Vec::new(),
11454 loc: Loc {
11455 line: tok.line,
11456 column: tok.column,
11457 },
11458 leading_trivia: Vec::new(),
11459 trailing_trivia: Vec::new(),
11460 };
11461 self.consume(TokenType::LBrace)?;
11462 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11463 let field_name = self.current().value.clone();
11464 self.advance();
11465 if self.check(TokenType::Colon) {
11466 self.advance();
11467 match field_name.as_str() {
11468 "method" => {
11469 let value_tok = self.consume_any_ident_or_kw()?;
11476 let value_upper = value_tok.value.to_uppercase();
11477 if !axonendpoint_is_valid_method(&value_upper) {
11478 let hint = crate::smart_suggest::suggest_for(
11479 &value_upper,
11480 AXONENDPOINT_METHOD_VALUES,
11481 );
11482 let base = format!(
11483 "Invalid method '{}' in axonendpoint '{}'.",
11484 value_tok.value, node.name
11485 );
11486 let message = if hint.is_empty() {
11487 format!(
11488 "{base} expected GET | POST | PUT | DELETE | PATCH, found {}",
11489 value_tok.value
11490 )
11491 } else {
11492 format!(
11493 "{base} {hint} (expected GET | POST | PUT | DELETE | PATCH, found {})",
11494 value_tok.value
11495 )
11496 };
11497 return Err(ParseError {
11498 message,
11499 line: value_tok.line,
11500 column: value_tok.column,
11501 ..Default::default()
11502 });
11503 }
11504 node.method = value_upper;
11505 }
11506 "path" => {
11507 node.path = self.consume(TokenType::StringLit)?.value.clone();
11508 match extract_path_param_names(&node.path) {
11516 Ok(names) => node.path_params = names,
11517 Err(dup) => {
11518 let cur = self.current().clone();
11519 return Err(ParseError {
11520 message: format!(
11521 "axonendpoint '{}' declares path '{}' \
11522 containing duplicate placeholder '{{{}}}'. \
11523 Each `{{name}}` in a `path:` must be \
11524 unique — the runtime cannot bind two \
11525 path segments to the same name.",
11526 node.name, node.path, dup,
11527 ),
11528 line: cur.line,
11529 column: cur.column,
11530 ..Default::default()
11531 });
11532 }
11533 }
11534 },
11535 "body" => node.body_type = self.consume_any_ident_or_kw()?.value.clone(),
11536 "query" => {
11537 let lbrace_tok = self.consume(TokenType::LBrace)?;
11556 let block_line = lbrace_tok.line;
11557 if !node.query_params.is_empty() {
11558 return Err(ParseError {
11559 message: format!(
11560 "axonendpoint '{}' declares `query: {{ … }}` \
11561 more than once. The query-parameter block \
11562 is unique per endpoint; combine all params \
11563 into a single block.",
11564 node.name,
11565 ),
11566 line: lbrace_tok.line,
11567 column: lbrace_tok.column,
11568 ..Default::default()
11569 });
11570 }
11571 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
11572 let name_tok = self.consume(TokenType::Identifier)?;
11573 let field_name = name_tok.value.clone();
11574 if node
11576 .query_params
11577 .iter()
11578 .any(|f| f.name == field_name)
11579 {
11580 return Err(ParseError {
11581 message: format!(
11582 "axonendpoint '{}' declares duplicate \
11583 query param '{}' inside `query: {{ … }}`. \
11584 Each name must appear at most once \
11585 .",
11586 node.name, field_name,
11587 ),
11588 line: name_tok.line,
11589 column: name_tok.column,
11590 ..Default::default()
11591 });
11592 }
11593 self.consume(TokenType::Colon)?;
11594 let type_expr = self.parse_type_expr()?;
11595 if !type_expr.generic_param.is_empty() {
11607 let canonical_hint = if type_expr.name == "Optional" {
11608 format!(
11609 " Use `{}?` (the `?` suffix) for an \
11610 optional query param instead of \
11611 `Optional<{}>`.",
11612 type_expr.generic_param,
11613 type_expr.generic_param,
11614 )
11615 } else if type_expr.name == "List" {
11616 " Multi-value query params (e.g. `?tag=a&tag=b`) \
11617 are honest-deferred from v1.38.5; bind a \
11618 single-value `Text` query param and parse \
11619 the value inside the flow."
11620 .to_string()
11621 } else {
11622 String::new()
11623 };
11624 return Err(ParseError {
11625 message: format!(
11626 "axonendpoint '{}' query param '{}' uses \
11627 a generic type `{}<{}>`. Query params \
11628 take a primitive type from the closed \
11629 catalog ({}); the `?` suffix marks \
11630 optional.{} .",
11631 node.name,
11632 field_name,
11633 type_expr.name,
11634 type_expr.generic_param,
11635 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | "),
11636 canonical_hint,
11637 ),
11638 line: type_expr.loc.line,
11639 column: type_expr.loc.column,
11640 ..Default::default()
11641 });
11642 }
11643 if !axonendpoint_is_valid_query_param_type(&type_expr.name) {
11647 let hint = crate::smart_suggest::suggest_for(
11654 &type_expr.name,
11655 AXONENDPOINT_QUERY_PARAM_TYPES,
11656 );
11657 let hint_text = if hint.is_empty() {
11658 format!(
11659 " Expected one of: {}.",
11660 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11661 )
11662 } else {
11663 format!(
11664 " {} Expected one of: {}.",
11665 hint,
11666 AXONENDPOINT_QUERY_PARAM_TYPES.join(" | ")
11667 )
11668 };
11669 return Err(ParseError {
11670 message: format!(
11671 "axonendpoint '{}' query param '{}' has \
11672 unsupported type '{}'.{} .",
11673 node.name, field_name, type_expr.name,
11674 hint_text,
11675 ),
11676 line: type_expr.loc.line,
11677 column: type_expr.loc.column,
11678 ..Default::default()
11679 });
11680 }
11681 node.query_params.push(TypeField {
11682 name: field_name,
11683 type_expr,
11684 loc: Loc {
11685 line: name_tok.line,
11686 column: name_tok.column,
11687 },
11688 });
11689 if self.check(TokenType::Comma) {
11695 self.advance();
11696 }
11697 let _ = block_line; }
11699 self.consume(TokenType::RBrace)?;
11700 },
11701 "execute" => node.execute_flow = self.consume_any_ident_or_kw()?.value.clone(),
11702 "output" => {
11703 node.output_type = self.parse_output_type_string()?;
11721 }
11722 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value.clone(),
11723 "cors" => node.cors_ref = self.consume_any_ident_or_kw()?.value.clone(),
11725 "retries" => node.retries = self.parse_optional_int(),
11726 "timeout" => {
11727 let t = self.current().clone();
11728 self.advance();
11729 node.timeout = t.value.clone();
11730 }
11731 "compliance" => node.compliance = self.parse_bracketed_identifiers()?,
11732 "replay" => {
11733 let value_tok = self.consume(TokenType::Bool)?;
11740 node.replay = value_tok.value.eq_ignore_ascii_case("true");
11741 node.replay_explicit = true;
11742 }
11743 "public" => {
11749 let value_tok = self.consume(TokenType::Bool)?;
11750 node.public = value_tok.value.eq_ignore_ascii_case("true");
11751 }
11752 "requires" => {
11753 let bracket_tok = self.current().clone();
11760 let items = self.parse_bracketed_dot_identifiers()?;
11761 for slug in &items {
11762 if !is_valid_capability_slug(slug) {
11763 return Err(ParseError {
11764 message: format!(
11765 "Invalid capability slug '{slug}' in axonendpoint '{}' \
11766 `requires:`. Capability slugs must match \
11767 ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$ — dot-separated \
11768 lowercase identifiers starting with a letter. Examples: \
11769 `admin`, `legal.read`, `hipaa.phi.read`.",
11770 node.name
11771 ),
11772 line: bracket_tok.line,
11773 column: bracket_tok.column,
11774 ..Default::default()
11775 });
11776 }
11777 }
11778 node.requires_capabilities = items;
11779 }
11780 "transport" => {
11784 let value_tok = self.consume_any_ident_or_kw()?;
11785 let value = &value_tok.value;
11786 if !axonendpoint_is_valid_transport(value) {
11787 let hint = crate::smart_suggest::suggest_for(
11788 value,
11789 AXONENDPOINT_TRANSPORT_VALUES,
11790 );
11791 let base = format!(
11792 "Invalid transport '{}' in axonendpoint '{}'.",
11793 value, node.name
11794 );
11795 let message = if hint.is_empty() {
11796 format!("{base} expected json | sse | ndjson, found {value}")
11797 } else {
11798 format!(
11799 "{base} {hint} (expected json | sse | ndjson, found {value})"
11800 )
11801 };
11802 return Err(ParseError {
11803 message,
11804 line: value_tok.line,
11805 column: value_tok.column,
11806 ..Default::default()
11807 });
11808 }
11809 node.transport = value.clone();
11810 node.transport_explicit = true;
11815 if self.check(TokenType::LParen) {
11822 if value != "sse" {
11823 let tok = self.current().clone();
11824 return Err(ParseError {
11825 message: format!(
11826 "Dialect parametrization \
11827 `transport: {value}(<dialect>)` is \
11828 only valid for `sse`; got \
11829 `{value}` in axonendpoint '{}'.",
11830 node.name
11831 ),
11832 line: tok.line,
11833 column: tok.column,
11834 ..Default::default()
11835 });
11836 }
11837 self.advance(); let dialect_tok = self.consume_any_ident_or_kw()?;
11839 let dialect = dialect_tok.value.clone();
11840 if !AXONENDPOINT_TRANSPORT_DIALECTS
11841 .iter()
11842 .any(|&d| d == dialect)
11843 {
11844 let hint = crate::smart_suggest::suggest_for(
11845 &dialect,
11846 AXONENDPOINT_TRANSPORT_DIALECTS,
11847 );
11848 let base = format!(
11849 "Invalid SSE dialect '{dialect}' in axonendpoint '{}'.",
11850 node.name
11851 );
11852 let message = if hint.is_empty() {
11853 format!(
11854 "{base} expected axon | openai | kimi | glm | anthropic, found {dialect}"
11855 )
11856 } else {
11857 format!(
11858 "{base} {hint} (expected axon | openai | kimi | glm | anthropic, found {dialect})"
11859 )
11860 };
11861 return Err(ParseError {
11862 message,
11863 line: dialect_tok.line,
11864 column: dialect_tok.column,
11865 ..Default::default()
11866 });
11867 }
11868 let rparen_tok = self.current().clone();
11870 if !self.check(TokenType::RParen) {
11871 return Err(ParseError {
11872 message: format!(
11873 "Expected `)` after dialect name \
11874 in axonendpoint '{}' \
11875 (transport: sse(<dialect>) grammar).",
11876 node.name
11877 ),
11878 line: rparen_tok.line,
11879 column: rparen_tok.column,
11880 ..Default::default()
11881 });
11882 }
11883 self.advance(); node.transport_dialect = dialect;
11885 }
11886 }
11887 "keepalive" => {
11888 let value_tok = self.current().clone();
11892 self.advance();
11893 let value = &value_tok.value;
11894 if !axonendpoint_is_valid_keepalive(value) {
11895 let hint = crate::smart_suggest::suggest_for(
11896 value,
11897 AXONENDPOINT_KEEPALIVE_VALUES,
11898 );
11899 let base = format!(
11900 "Invalid keepalive '{}' in axonendpoint '{}'.",
11901 value, node.name
11902 );
11903 let message = if hint.is_empty() {
11904 format!("{base} expected 5s | 15s | 30s | 60s, found {value}")
11905 } else {
11906 format!(
11907 "{base} {hint} (expected 5s | 15s | 30s | 60s, found {value})"
11908 )
11909 };
11910 return Err(ParseError {
11911 message,
11912 line: value_tok.line,
11913 column: value_tok.column,
11914 ..Default::default()
11915 });
11916 }
11917 node.keepalive = value.clone();
11918 }
11919 "backend" => {
11920 let value_tok = self.consume_any_ident_or_kw()?;
11928 let value = &value_tok.value;
11929 if !axonendpoint_is_valid_backend(value) {
11930 let hint = crate::smart_suggest::suggest_for(
11931 value,
11932 AXONENDPOINT_BACKEND_VALUES,
11933 );
11934 let expected = AXONENDPOINT_BACKEND_VALUES.join(" | ");
11935 let base = format!(
11936 "Invalid backend '{}' in axonendpoint '{}'.",
11937 value, node.name
11938 );
11939 let message = if hint.is_empty() {
11940 format!("{base} expected {expected}, found {value}")
11941 } else {
11942 format!(
11943 "{base} {hint} (expected {expected}, found {value})"
11944 )
11945 };
11946 return Err(ParseError {
11947 message,
11948 line: value_tok.line,
11949 column: value_tok.column,
11950 ..Default::default()
11951 });
11952 }
11953 node.backend = value.clone();
11954 }
11955 _ => self.skip_value(),
11956 }
11957 } else if self.check(TokenType::LBrace) {
11958 self.skip_braced_block()?;
11959 }
11960 }
11961 self.consume(TokenType::RBrace)?;
11962 Ok(node)
11963 }
11964
11965 fn parse_optional_int(&mut self) -> Option<i64> {
11968 let tok = self.current().clone();
11969 match tok.ttype {
11970 TokenType::Integer => {
11971 self.advance();
11972 tok.value.parse::<i64>().ok()
11973 }
11974 _ => {
11975 self.advance();
11976 None
11977 }
11978 }
11979 }
11980
11981 fn parse_optional_float(&mut self) -> Option<f64> {
11982 let tok = self.current().clone();
11983 match tok.ttype {
11984 TokenType::Float | TokenType::Integer => {
11985 self.advance();
11986 tok.value.parse::<f64>().ok()
11987 }
11988 _ => {
11989 self.advance();
11990 None
11991 }
11992 }
11993 }
11994
11995 fn parse_lambda_data(&mut self) -> Result<LambdaDataDefinition, ParseError> {
11998 let tok = self.consume(TokenType::Lambda)?;
11999 let name = self.consume(TokenType::Identifier)?;
12000 self.consume(TokenType::LBrace)?;
12001
12002 let mut node = LambdaDataDefinition {
12003 name: name.value.clone(),
12004 ontology: String::new(),
12005 certainty: 1.0,
12006 temporal_frame_start: String::new(),
12007 temporal_frame_end: String::new(),
12008 provenance: String::new(),
12009 derivation: String::new(),
12010 loc: Loc {
12011 line: tok.line,
12012 column: tok.column,
12013 },
12014 leading_trivia: Vec::new(),
12015 trailing_trivia: Vec::new(),
12016 };
12017
12018 while !self.check(TokenType::RBrace) {
12019 let field = self.current().clone();
12020 match field.ttype {
12021 TokenType::Ontology => {
12022 self.advance();
12023 self.consume(TokenType::Colon)?;
12024 node.ontology = self.consume(TokenType::StringLit)?.value.clone();
12025 }
12026 TokenType::Certainty => {
12027 self.advance();
12028 self.consume(TokenType::Colon)?;
12029 let val = self.current().clone();
12030 match val.ttype {
12031 TokenType::Float => {
12032 self.advance();
12033 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
12034 }
12035 TokenType::Integer => {
12036 self.advance();
12037 node.certainty = val.value.parse::<f64>().unwrap_or(1.0);
12038 }
12039 _ => {
12040 return Err(ParseError {
12041 message: format!(
12042 "Expected number for certainty, got '{}'",
12043 val.value
12044 ),
12045 line: val.line,
12046 column: val.column,
12047 ..Default::default()
12048 });
12049 }
12050 }
12051 }
12052 TokenType::TemporalFrame => {
12053 self.advance();
12054 self.consume(TokenType::Colon)?;
12055 node.temporal_frame_start = self.consume(TokenType::StringLit)?.value.clone();
12056 if self.check(TokenType::StringLit) {
12058 node.temporal_frame_end = self.consume(TokenType::StringLit)?.value.clone();
12059 }
12060 }
12061 TokenType::Provenance => {
12062 self.advance();
12063 self.consume(TokenType::Colon)?;
12064 node.provenance = self.consume(TokenType::StringLit)?.value.clone();
12065 }
12066 TokenType::Derivation => {
12067 self.advance();
12068 self.consume(TokenType::Colon)?;
12069 let d = self.current().clone();
12070 self.advance();
12071 node.derivation = d.value.clone();
12072 }
12073 _ => {
12074 self.advance();
12076 if self.check(TokenType::Colon) {
12077 self.advance();
12078 self.skip_value();
12079 }
12080 }
12081 }
12082 }
12083
12084 self.consume(TokenType::RBrace)?;
12085 Ok(node)
12086 }
12087
12088 fn parse_lambda_data_apply(&mut self) -> Result<LambdaDataApplyNode, ParseError> {
12089 let tok = self.consume(TokenType::Lambda)?;
12090 let lambda_name = self.consume(TokenType::Identifier)?;
12091
12092 let on_tok = self.current().clone();
12094 self.advance();
12095 if on_tok.value != "on" {
12096 return Err(ParseError {
12097 message: format!(
12098 "Expected 'on' after lambda data name in flow step, got '{}'",
12099 on_tok.value
12100 ),
12101 line: on_tok.line,
12102 column: on_tok.column,
12103 ..Default::default()
12104 });
12105 }
12106
12107 let target = self.current().clone();
12108 self.advance();
12109
12110 let mut output_type = String::new();
12111 if self.check(TokenType::Arrow) {
12112 self.advance();
12113 output_type = self.consume(TokenType::Identifier)?.value.clone();
12114 }
12115
12116 Ok(LambdaDataApplyNode {
12117 lambda_data_name: lambda_name.value.clone(),
12118 target: target.value.clone(),
12119 output_type,
12120 loc: Loc {
12121 line: tok.line,
12122 column: tok.column,
12123 },
12124 })
12125 }
12126
12127 fn parse_generic_declaration(&mut self) -> Result<Declaration, ParseError> {
12130 let kw_tok = self.current().clone();
12131 self.advance(); let name = if self.current().ttype == TokenType::Identifier {
12135 let n = self.current().value.clone();
12136 self.advance();
12137 n
12138 } else if !self.check(TokenType::LBrace)
12139 && !self.check(TokenType::LParen)
12140 && !self.check(TokenType::Eof)
12141 && self
12142 .current()
12143 .value
12144 .chars()
12145 .all(|c| c.is_alphanumeric() || c == '_')
12146 {
12147 let n = self.current().value.clone();
12148 self.advance();
12149 n
12150 } else {
12151 String::new()
12152 };
12153
12154 if self.check(TokenType::LParen) {
12156 self.advance();
12157 let mut depth = 1u32;
12158 while depth > 0 && !self.check(TokenType::Eof) {
12159 if self.check(TokenType::LParen) {
12160 depth += 1;
12161 } else if self.check(TokenType::RParen) {
12162 depth -= 1;
12163 }
12164 self.advance();
12165 }
12166 }
12167
12168 while !self.check(TokenType::LBrace) && !self.at_declaration_start() {
12170 if self.check(TokenType::Eof) {
12171 break;
12172 }
12173 self.advance();
12174 }
12175
12176 if self.check(TokenType::LBrace) {
12178 self.skip_braced_block()?;
12179 }
12180
12181 Ok(Declaration::Generic(GenericDeclaration {
12182 keyword: kw_tok.value,
12183 name,
12184 loc: Loc {
12185 line: kw_tok.line,
12186 column: kw_tok.column,
12187 },
12188 leading_trivia: Vec::new(),
12189 trailing_trivia: Vec::new(),
12190 }))
12191 }
12192
12193 fn parse_channel(&mut self) -> Result<ChannelDefinition, ParseError> {
12201 let tok = self.consume(TokenType::Channel)?;
12202 let name = self.consume(TokenType::Identifier)?.value;
12203 let mut node = ChannelDefinition {
12204 name: name.clone(),
12205 message: String::new(),
12206 qos: "at_least_once".to_string(),
12207 lifetime: "affine".to_string(),
12208 persistence: "ephemeral".to_string(),
12209 shield_ref: String::new(),
12210 loc: Loc {
12211 line: tok.line,
12212 column: tok.column,
12213 },
12214 leading_trivia: Vec::new(),
12215 trailing_trivia: Vec::new(),
12216 };
12217 self.consume(TokenType::LBrace)?;
12218 while !self.check(TokenType::RBrace) && !self.check(TokenType::Eof) {
12219 let field_tok = self.current().clone();
12220 let field_name = field_tok.value.clone();
12221 self.advance();
12222 if !self.check(TokenType::Colon) {
12223 if self.check(TokenType::LBrace) {
12224 self.skip_braced_block()?;
12225 }
12226 continue;
12227 }
12228 self.advance();
12229 match field_name.as_str() {
12230 "message" => node.message = self.parse_channel_message_type()?,
12231 "qos" => {
12232 let q_tok = self.consume_any_ident_or_kw()?;
12233 if !matches!(
12234 q_tok.value.as_str(),
12235 "at_most_once" | "at_least_once" | "exactly_once" | "broadcast" | "queue"
12236 ) {
12237 return Err(ParseError {
12238 message: format!(
12239 "Invalid qos '{}' in channel '{}' — \
12240 expected at_most_once | at_least_once | \
12241 exactly_once | broadcast | queue",
12242 q_tok.value, name
12243 ),
12244 line: q_tok.line,
12245 column: q_tok.column,
12246 ..Default::default()
12247 });
12248 }
12249 node.qos = q_tok.value;
12250 }
12251 "lifetime" => {
12252 let lt_tok = self.consume_any_ident_or_kw()?;
12253 if !matches!(lt_tok.value.as_str(), "linear" | "affine" | "persistent") {
12254 return Err(ParseError {
12255 message: format!(
12256 "Invalid lifetime '{}' in channel '{}' — \
12257 expected linear | affine | persistent",
12258 lt_tok.value, name
12259 ),
12260 line: lt_tok.line,
12261 column: lt_tok.column,
12262 ..Default::default()
12263 });
12264 }
12265 node.lifetime = lt_tok.value;
12266 }
12267 "persistence" => {
12268 let p_tok = self.consume_any_ident_or_kw()?;
12269 if !matches!(p_tok.value.as_str(), "ephemeral" | "persistent_axonstore") {
12270 return Err(ParseError {
12271 message: format!(
12272 "Invalid persistence '{}' in channel '{}' — \
12273 expected ephemeral | persistent_axonstore",
12274 p_tok.value, name
12275 ),
12276 line: p_tok.line,
12277 column: p_tok.column,
12278 ..Default::default()
12279 });
12280 }
12281 node.persistence = p_tok.value;
12282 }
12283 "shield" => node.shield_ref = self.consume_any_ident_or_kw()?.value,
12284 _ => self.skip_value(),
12285 }
12286 }
12287 self.consume(TokenType::RBrace)?;
12288 Ok(node)
12289 }
12290
12291 fn parse_channel_message_type(&mut self) -> Result<String, ParseError> {
12294 let head = self.consume(TokenType::Identifier)?;
12295 let mut spelling = head.value;
12296 if self.check(TokenType::Lt) {
12297 self.advance();
12298 let inner = self.parse_channel_message_type()?;
12299 self.consume(TokenType::Gt)?;
12300 spelling = format!("{}<{}>", spelling, inner);
12301 }
12302 Ok(spelling)
12303 }
12304
12305 fn parse_emit_step(&mut self) -> Result<FlowStep, ParseError> {
12311 let tok = self.consume(TokenType::Emit)?;
12312 let channel = self.consume(TokenType::Identifier)?.value;
12313 self.consume(TokenType::LParen)?;
12314 let value = self.parse_emit_value_ref()?;
12315 self.consume(TokenType::RParen)?;
12316 Ok(FlowStep::Emit(EmitStatement {
12317 channel_ref: channel,
12318 value_ref: value,
12319 loc: Loc {
12320 line: tok.line,
12321 column: tok.column,
12322 },
12323 }))
12324 }
12325
12326 fn parse_mint_step(&mut self) -> Result<FlowStep, ParseError> {
12332 let tok = self.consume(TokenType::Mint)?;
12333 let credential_ref = self.consume(TokenType::Identifier)?.value;
12334 self.consume(TokenType::As)?;
12335 let binding = self.consume(TokenType::Identifier)?.value;
12336 Ok(FlowStep::Mint(MintStep {
12337 credential_ref,
12338 binding,
12339 loc: Loc {
12340 line: tok.line,
12341 column: tok.column,
12342 },
12343 }))
12344 }
12345
12346 fn parse_rotate_step(&mut self) -> Result<FlowStep, ParseError> {
12361 let tok = self.consume(TokenType::Rotate)?;
12362 let store_ref = self.consume(TokenType::Identifier)?.value;
12363 let mut where_expr = String::new();
12364 if self.check(TokenType::Where) {
12365 self.advance();
12366 where_expr = self.consume(TokenType::StringLit)?.value.clone();
12367 }
12368 let with_tok = self.current().clone();
12369 if with_tok.value != "with" {
12370 return Err(ParseError {
12371 message: format!(
12372 "Expected `with <Tool>` after `rotate {store_ref}{}`, found '{}'. \
12373 A rotation names the tool that performs the renewal exchange: \
12374 `rotate {store_ref} [where \"<filter>\"] with <Tool> as <binding>`.",
12375 if where_expr.is_empty() { "" } else { " where …" },
12376 with_tok.value
12377 ),
12378 line: with_tok.line,
12379 column: with_tok.column,
12380 ..Default::default()
12381 });
12382 }
12383 self.advance();
12384 let tool_ref = self.consume(TokenType::Identifier)?.value;
12385 self.consume(TokenType::As)?;
12386 let binding = self.consume(TokenType::Identifier)?.value;
12387 Ok(FlowStep::Rotate(RotateStep {
12388 store_ref,
12389 where_expr,
12390 tool_ref,
12391 binding,
12392 loc: Loc {
12393 line: tok.line,
12394 column: tok.column,
12395 },
12396 }))
12397 }
12398
12399 fn parse_emit_value_ref(&mut self) -> Result<String, ParseError> {
12415 let head = self.consume(TokenType::Identifier)?.value;
12416 let mut parts = vec![head];
12417 while self.check(TokenType::Dot) {
12418 self.advance(); let next_tok = self.current().clone();
12420 let valid = !next_tok.value.is_empty()
12421 && next_tok.value.as_bytes()[0].is_ascii_alphabetic()
12422 || next_tok.value.starts_with('_');
12423 if !valid {
12424 return Err(ParseError {
12425 message: format!(
12426 "Expected identifier or keyword after '.' in dotted \
12427 access, found {:?}",
12428 next_tok.value
12429 ),
12430 line: next_tok.line,
12431 column: next_tok.column,
12432 ..Default::default()
12433 });
12434 }
12435 self.advance();
12436 parts.push(next_tok.value);
12437 }
12438 Ok(parts.join("."))
12439 }
12440
12441 fn parse_publish_step(&mut self) -> Result<FlowStep, ParseError> {
12443 let tok = self.consume(TokenType::Publish)?;
12444 let channel = self.consume(TokenType::Identifier)?.value;
12445 self.consume(TokenType::Within)?;
12446 let shield = self.consume(TokenType::Identifier)?.value;
12447 Ok(FlowStep::Publish(PublishStatement {
12448 channel_ref: channel,
12449 shield_ref: shield,
12450 loc: Loc {
12451 line: tok.line,
12452 column: tok.column,
12453 },
12454 }))
12455 }
12456
12457 fn parse_discover_step(&mut self) -> Result<FlowStep, ParseError> {
12459 let tok = self.consume(TokenType::Discover)?;
12460 let cap = self.consume(TokenType::Identifier)?.value;
12461 self.consume(TokenType::As)?;
12462 let alias = self.consume(TokenType::Identifier)?.value;
12463 Ok(FlowStep::Discover(DiscoverStatement {
12464 capability_ref: cap,
12465 alias,
12466 loc: Loc {
12467 line: tok.line,
12468 column: tok.column,
12469 },
12470 }))
12471 }
12472}
12473
12474#[cfg(test)]
12477mod parser_tests {
12478 use super::*;
12479 use crate::lexer::Lexer;
12480
12481 fn parse(src: &str) -> Result<Program, ParseError> {
12482 let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
12483 Parser::new(tokens).parse()
12484 }
12485
12486 #[test]
12487 fn channel_full_parses() {
12488 let src = r#"channel C { message: Order qos: at_least_once lifetime: affine persistence: ephemeral shield: Gate }"#;
12489 let prog = parse(src).expect("parse");
12490 match &prog.declarations[0] {
12491 Declaration::Channel(c) => {
12492 assert_eq!(c.name, "C");
12493 assert_eq!(c.message, "Order");
12494 assert_eq!(c.qos, "at_least_once");
12495 assert_eq!(c.lifetime, "affine");
12496 assert_eq!(c.persistence, "ephemeral");
12497 assert_eq!(c.shield_ref, "Gate");
12498 }
12499 _ => panic!("expected ChannelDefinition"),
12500 }
12501 }
12502
12503 #[test]
12504 fn channel_defaults_match_paper_d1() {
12505 let prog = parse("channel C { message: Order }").expect("parse");
12506 if let Declaration::Channel(c) = &prog.declarations[0] {
12507 assert_eq!(c.qos, "at_least_once"); assert_eq!(c.lifetime, "affine"); assert_eq!(c.persistence, "ephemeral");
12510 assert_eq!(c.shield_ref, "");
12511 } else {
12512 panic!("expected ChannelDefinition");
12513 }
12514 }
12515
12516 #[test]
12517 fn channel_second_order_message_type_parses() {
12518 let prog = parse("channel C { message: Channel<Order> }").expect("parse");
12519 if let Declaration::Channel(c) = &prog.declarations[0] {
12520 assert_eq!(c.message, "Channel<Order>");
12521 } else {
12522 panic!("expected ChannelDefinition");
12523 }
12524 }
12525
12526 #[test]
12527 fn channel_nested_channel_message_type_parses() {
12528 let prog = parse("channel C { message: Channel<Channel<Order>> }").expect("parse");
12529 if let Declaration::Channel(c) = &prog.declarations[0] {
12530 assert_eq!(c.message, "Channel<Channel<Order>>");
12531 } else {
12532 panic!("expected ChannelDefinition");
12533 }
12534 }
12535
12536 #[test]
12537 fn channel_invalid_qos_rejected() {
12538 let err = parse("channel C { message: T qos: bogus }").unwrap_err();
12539 assert!(err.message.contains("Invalid qos"), "got {}", err.message);
12540 }
12541
12542 #[test]
12543 fn channel_invalid_lifetime_rejected() {
12544 let err = parse("channel C { message: T lifetime: eternal }").unwrap_err();
12545 assert!(
12546 err.message.contains("Invalid lifetime"),
12547 "got {}",
12548 err.message
12549 );
12550 }
12551
12552 #[test]
12553 fn channel_invalid_persistence_rejected() {
12554 let err = parse("channel C { message: T persistence: forever }").unwrap_err();
12555 assert!(
12556 err.message.contains("Invalid persistence"),
12557 "got {}",
12558 err.message
12559 );
12560 }
12561
12562 #[test]
12563 fn emit_value_parses() {
12564 let src = "flow f() -> Out { emit C(payload) }";
12565 let prog = parse(src).expect("parse");
12566 if let Declaration::Flow(f) = &prog.declarations[0] {
12567 match &f.body[0] {
12568 FlowStep::Emit(e) => {
12569 assert_eq!(e.channel_ref, "C");
12570 assert_eq!(e.value_ref, "payload");
12571 }
12572 other => panic!("expected Emit, got {:?}", other),
12573 }
12574 } else {
12575 panic!("expected Flow");
12576 }
12577 }
12578
12579 #[test]
12580 fn publish_within_shield_parses() {
12581 let src = "flow f() -> Cap { publish C within Gate }";
12582 let prog = parse(src).expect("parse");
12583 if let Declaration::Flow(f) = &prog.declarations[0] {
12584 match &f.body[0] {
12585 FlowStep::Publish(p) => {
12586 assert_eq!(p.channel_ref, "C");
12587 assert_eq!(p.shield_ref, "Gate");
12588 }
12589 other => panic!("expected Publish, got {:?}", other),
12590 }
12591 } else {
12592 panic!("expected Flow");
12593 }
12594 }
12595
12596 #[test]
12597 fn discover_with_alias_parses() {
12598 let src = "flow f() -> Out { discover C as ch }";
12599 let prog = parse(src).expect("parse");
12600 if let Declaration::Flow(f) = &prog.declarations[0] {
12601 match &f.body[0] {
12602 FlowStep::Discover(d) => {
12603 assert_eq!(d.capability_ref, "C");
12604 assert_eq!(d.alias, "ch");
12605 }
12606 other => panic!("expected Discover, got {:?}", other),
12607 }
12608 } else {
12609 panic!("expected Flow");
12610 }
12611 }
12612
12613 #[test]
12614 fn listen_typed_ref_sets_flag_true() {
12615 let src = "daemon D() { goal: \"x\" listen C as ev { } }";
12616 let prog = parse(src).expect("parse");
12617 if let Declaration::Daemon(d) = &prog.declarations[0] {
12618 assert_eq!(d.listeners.len(), 1);
12619 assert_eq!(d.listeners[0].channel, "C");
12620 assert!(d.listeners[0].channel_is_ref, "typed ref ⇒ true");
12621 } else {
12622 panic!("expected Daemon");
12623 }
12624 }
12625
12626 #[test]
12627 fn listen_string_topic_legacy_flag_false() {
12628 let src = "daemon D() { goal: \"x\" listen \"orders\" as ev { } }";
12629 let prog = parse(src).expect("parse");
12630 if let Declaration::Daemon(d) = &prog.declarations[0] {
12631 assert_eq!(d.listeners.len(), 1);
12632 assert_eq!(d.listeners[0].channel, "orders");
12633 assert!(!d.listeners[0].channel_is_ref, "string topic ⇒ false");
12634 } else {
12635 panic!("expected Daemon");
12636 }
12637 }
12638
12639 fn extract_first_emit(prog: &Program) -> &EmitStatement {
12642 if let Declaration::Flow(f) = &prog.declarations[0] {
12643 if let FlowStep::Emit(e) = &f.body[0] {
12644 return e;
12645 }
12646 }
12647 panic!("expected emit statement at flow body[0]");
12648 }
12649
12650 #[test]
12651 fn emit_accepts_bare_identifier_value_ref() {
12652 let prog = parse("flow f() -> Out { emit Hello(payload) }").expect("parse");
12654 let emit = extract_first_emit(&prog);
12655 assert_eq!(emit.channel_ref, "Hello");
12656 assert_eq!(emit.value_ref, "payload");
12657 }
12658
12659 #[test]
12660 fn emit_accepts_two_segment_dotted_value_ref() {
12661 let prog = parse("flow f() -> Out { emit Hello(Build.output) }").expect("parse");
12663 let emit = extract_first_emit(&prog);
12664 assert_eq!(emit.value_ref, "Build.output");
12665 }
12666
12667 #[test]
12668 fn emit_accepts_three_segment_nested_dotted_value_ref() {
12669 let prog = parse("flow f() -> Out { emit Score(Analyze.result.score) }").expect("parse");
12670 let emit = extract_first_emit(&prog);
12671 assert_eq!(emit.value_ref, "Analyze.result.score");
12672 }
12673
12674 #[test]
12675 fn emit_dotted_with_trailing_dot_fails() {
12676 let result = parse("flow f() -> Out { emit Hello(Build.) }");
12678 assert!(result.is_err(), "expected parse error for trailing dot");
12679 }
12680}
12681
12682#[cfg(test)]
12685mod declaration_trivia_tests {
12686 use super::*;
12687 use crate::lexer::Lexer;
12688 use crate::tokens::TriviaKind;
12689
12690 fn parse(src: &str) -> Program {
12691 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12692 Parser::new(toks).parse().expect("parse")
12693 }
12694
12695 #[test]
12696 fn no_comments_means_empty_trivia_per_decl() {
12697 let prog = parse("flow F() -> Out { }");
12698 assert_eq!(prog.declarations.len(), 1);
12699 assert_eq!(prog.declaration_trivia.len(), 1);
12700 assert!(prog.declaration_trivia[0].leading.is_empty());
12701 assert!(prog.declaration_trivia[0].trailing.is_empty());
12702 }
12703
12704 #[test]
12705 fn doc_line_comment_attaches_as_leading() {
12706 let prog = parse("/// Documents F\nflow F() -> Out { }");
12707 let triv = &prog.declaration_trivia[0];
12708 assert_eq!(triv.leading.len(), 1);
12709 assert_eq!(triv.leading[0].kind, TriviaKind::DocLine);
12710 assert!(triv.leading[0].is_doc());
12711 assert_eq!(triv.leading[0].text, "/// Documents F");
12712 }
12713
12714 #[test]
12715 fn regular_line_comment_attaches_as_leading() {
12716 let prog = parse("// header\nflow F() -> Out { }");
12717 let triv = &prog.declaration_trivia[0];
12718 assert_eq!(triv.leading.len(), 1);
12719 assert_eq!(triv.leading[0].kind, TriviaKind::Line);
12720 assert!(!triv.leading[0].is_doc());
12721 }
12722
12723 #[test]
12724 fn block_doc_comment_attaches_as_leading() {
12725 let prog = parse("/** Doc block */\nflow F() -> Out { }");
12726 let triv = &prog.declaration_trivia[0];
12727 assert_eq!(triv.leading[0].kind, TriviaKind::DocBlock);
12728 assert!(triv.leading[0].is_doc());
12729 }
12730
12731 #[test]
12732 fn multiple_comments_collected_in_source_order() {
12733 let src = "/// First\n/// Second\nflow F() -> Out { }";
12734 let prog = parse(src);
12735 let triv = &prog.declaration_trivia[0];
12736 assert_eq!(triv.leading.len(), 2);
12737 assert_eq!(triv.leading[0].text, "/// First");
12738 assert_eq!(triv.leading[1].text, "/// Second");
12739 }
12740
12741 #[test]
12742 fn three_decls_each_get_own_leading() {
12743 let src = "/// for A\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }\n/// for C\nflow C() -> Out { }";
12744 let prog = parse(src);
12745 assert_eq!(prog.declarations.len(), 3);
12746 assert_eq!(prog.declaration_trivia.len(), 3);
12747 for (idx, name) in ["A", "B", "C"].iter().enumerate() {
12748 let triv = &prog.declaration_trivia[idx];
12749 assert_eq!(triv.leading.len(), 1);
12750 assert_eq!(triv.leading[0].text, format!("/// for {name}"));
12751 }
12752 }
12753
12754 #[test]
12755 fn trailing_comment_attaches_to_last_token_of_decl() {
12756 let prog = parse("flow F() -> Out { } // tail");
12758 let triv = &prog.declaration_trivia[0];
12759 assert_eq!(triv.trailing.len(), 1);
12760 assert_eq!(triv.trailing[0].text, "// tail");
12761 }
12762
12763 #[test]
12764 fn mixed_doc_and_regular_preserve_order_between_decls() {
12765 let src = "/// doc for A\nflow A() -> Out { }\n\n// header line\n/// doc for B\nflow B() -> Out { }";
12766 let prog = parse(src);
12767 assert_eq!(prog.declarations.len(), 2);
12768 assert_eq!(prog.declaration_trivia[0].leading.len(), 1);
12770 assert_eq!(prog.declaration_trivia[1].leading.len(), 2);
12772 assert_eq!(prog.declaration_trivia[1].leading[0].text, "// header line");
12773 assert_eq!(prog.declaration_trivia[1].leading[1].text, "/// doc for B");
12774 }
12775
12776 #[test]
12777 fn parser_unaffected_by_comments_in_grammar_path() {
12778 let src =
12783 "// before flow\nflow /* between flow and name */ F() -> Out {\n // body comment\n}";
12784 let prog = parse(src);
12785 assert_eq!(prog.declarations.len(), 1);
12786 if let Declaration::Flow(f) = &prog.declarations[0] {
12787 assert_eq!(f.name, "F");
12788 } else {
12789 panic!("expected Flow declaration");
12790 }
12791 }
12792}
12793
12794#[cfg(test)]
12803mod per_struct_trivia_tests {
12804 use super::*;
12805 use crate::lexer::Lexer;
12806 use crate::tokens::TriviaKind;
12807
12808 fn parse(src: &str) -> Program {
12809 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12810 Parser::new(toks).parse().expect("parse")
12811 }
12812
12813 #[test]
12814 fn flow_definition_carries_leading_trivia_directly() {
12815 let prog = parse("/// documents F\nflow F() -> Out { }");
12816 if let Declaration::Flow(f) = &prog.declarations[0] {
12817 assert_eq!(f.leading_trivia.len(), 1);
12818 assert_eq!(f.leading_trivia[0].kind, TriviaKind::DocLine);
12819 assert_eq!(f.leading_trivia[0].text, "/// documents F");
12820 assert!(f.trailing_trivia.is_empty());
12821 } else {
12822 panic!("expected Flow declaration");
12823 }
12824 }
12825
12826 #[test]
12827 fn flow_definition_carries_trailing_trivia_directly() {
12828 let prog = parse("flow F() -> Out { } // tail comment");
12829 if let Declaration::Flow(f) = &prog.declarations[0] {
12830 assert_eq!(f.trailing_trivia.len(), 1);
12831 assert_eq!(f.trailing_trivia[0].text, "// tail comment");
12832 } else {
12833 panic!("expected Flow declaration");
12834 }
12835 }
12836
12837 #[test]
12838 fn channel_definition_carries_trivia_directly() {
12839 let src = concat!(
12842 "/// inbound order events\n",
12843 "channel Orders {\n",
12844 " message: Order\n",
12845 " qos: at_least_once\n",
12846 " lifetime: affine\n",
12847 " persistence: ephemeral\n",
12848 " shield: Broker\n",
12849 "}",
12850 );
12851 let prog = parse(src);
12852 if let Declaration::Channel(ch) = &prog.declarations[0] {
12853 assert_eq!(ch.leading_trivia.len(), 1);
12854 assert!(ch.leading_trivia[0].is_doc());
12855 assert_eq!(ch.leading_trivia[0].text, "/// inbound order events");
12856 } else {
12857 panic!("expected Channel declaration");
12858 }
12859 }
12860
12861 #[test]
12862 fn per_struct_fields_match_side_channel() {
12863 let src = "/// for A\n// header for B\nflow A() -> Out { }\n/// for B\nflow B() -> Out { }";
12866 let prog = parse(src);
12867 for (idx, decl) in prog.declarations.iter().enumerate() {
12868 let side = &prog.declaration_trivia[idx];
12869 let (per_lead, per_trail) = match decl {
12870 Declaration::Flow(f) => (&f.leading_trivia, &f.trailing_trivia),
12871 _ => panic!("unexpected variant"),
12872 };
12873 assert_eq!(per_lead.len(), side.leading.len());
12874 assert_eq!(per_trail.len(), side.trailing.len());
12875 for (a, b) in per_lead.iter().zip(side.leading.iter()) {
12876 assert_eq!(a.text, b.text);
12877 assert_eq!(a.kind, b.kind);
12878 }
12879 }
12880 }
12881
12882 #[test]
12883 fn comment_free_program_yields_empty_per_struct_fields() {
12884 let prog = parse("flow F() -> Out { }");
12885 if let Declaration::Flow(f) = &prog.declarations[0] {
12886 assert!(f.leading_trivia.is_empty());
12887 assert!(f.trailing_trivia.is_empty());
12888 } else {
12889 panic!("expected Flow declaration");
12890 }
12891 }
12892}
12893
12894#[cfg(test)]
12903mod inner_doc_tests {
12904 use super::*;
12905 use crate::lexer::Lexer;
12906 use crate::tokens::TriviaKind;
12907
12908 fn parse(src: &str) -> Program {
12909 let toks = Lexer::new(src, "<test>").tokenize().expect("lex");
12910 Parser::new(toks).parse().expect("parse")
12911 }
12912
12913 #[test]
12914 fn inner_doc_line_reaches_leading_trivia() {
12915 let src = "//! file-level docs\nflow F() -> Out { }";
12916 let prog = parse(src);
12917 let triv = &prog.declaration_trivia[0];
12918 assert_eq!(triv.leading.len(), 1);
12919 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocLine);
12920 assert!(triv.leading[0].is_doc());
12921 assert!(triv.leading[0].is_inner_doc());
12922 assert_eq!(triv.leading[0].text, "//! file-level docs");
12923 assert_eq!(triv.leading[0].stripped_text(), " file-level docs");
12924 }
12925
12926 #[test]
12927 fn inner_doc_block_reaches_leading_trivia() {
12928 let src = "/*! module-level docs */\nflow F() -> Out { }";
12929 let prog = parse(src);
12930 let triv = &prog.declaration_trivia[0];
12931 assert_eq!(triv.leading.len(), 1);
12932 assert_eq!(triv.leading[0].kind, TriviaKind::InnerDocBlock);
12933 assert!(triv.leading[0].is_inner_doc());
12934 assert_eq!(triv.leading[0].stripped_text(), " module-level docs ");
12935 }
12936
12937 #[test]
12938 fn outer_and_inner_doc_can_coexist() {
12939 let src = "//! file docs\n/// docs F\nflow F() -> Out { }";
12943 let prog = parse(src);
12944 let triv = &prog.declaration_trivia[0];
12945 assert_eq!(triv.leading.len(), 2);
12946 assert!(triv.leading[0].is_inner_doc());
12947 assert!(triv.leading[1].is_doc());
12948 assert!(!triv.leading[1].is_inner_doc());
12949 }
12950
12951 #[test]
12952 fn inner_doc_reaches_per_struct_fields() {
12953 let src = "//! intro\nflow F() -> Out { }";
12955 let prog = parse(src);
12956 if let Declaration::Flow(f) = &prog.declarations[0] {
12957 assert_eq!(f.leading_trivia.len(), 1);
12958 assert!(f.leading_trivia[0].is_inner_doc());
12959 } else {
12960 panic!("expected Flow declaration");
12961 }
12962 }
12963}
12964
12965#[cfg(test)]
12983mod recovery_tests {
12984 use super::*;
12985 use crate::lexer::Lexer;
12986
12987 fn lex(src: &str) -> Vec<Token> {
12990 Lexer::new(src, "<test>").tokenize().expect("lex")
12991 }
12992
12993 fn recover(src: &str) -> ParseResult {
12996 Parser::new(lex(src)).parse_with_recovery()
12997 }
12998
12999 fn strict(src: &str) -> Result<Program, ParseError> {
13001 Parser::new(lex(src)).parse()
13002 }
13003
13004 #[test]
13007 fn strict_parse_unchanged_for_clean_source() {
13008 let src = "intent I {}";
13011 let prog = strict(src).expect("clean parse");
13012 assert_eq!(prog.declarations.len(), 1);
13013 }
13014
13015 #[test]
13016 fn strict_parse_still_raises_on_first_error() {
13017 let src = "flow F() { } not_a_keyword flow G() { }";
13022 let _ = strict(src).expect_err("must error fast in strict mode");
13023 }
13024
13025 #[test]
13026 fn recovery_clean_source_yields_no_errors() {
13027 let src = "flow F() { } flow G() { }";
13028 let pr = recover(src);
13029 assert!(pr.is_clean(), "errors: {:?}", pr.errors);
13030 assert_eq!(pr.program.declarations.len(), 2);
13031 }
13032
13033 #[test]
13036 fn single_unknown_top_level_token_recovers() {
13037 let src = "garbage_token flow F() { } flow G() { }";
13039 let pr = recover(src);
13040 assert_eq!(pr.errors.len(), 1, "errors: {:?}", pr.errors);
13041 assert_eq!(pr.program.declarations.len(), 2);
13042 }
13043
13044 #[test]
13045 fn error_in_first_decl_does_not_block_second() {
13046 let src = "flow F() { not_a_step nope } flow G() { }";
13049 let pr = recover(src);
13050 assert!(pr.has_errors(), "expected at least one error");
13051 let names: Vec<&str> = pr
13053 .program
13054 .declarations
13055 .iter()
13056 .filter_map(|d| match d {
13057 Declaration::Flow(f) => Some(f.name.as_str()),
13058 _ => None,
13059 })
13060 .collect();
13061 assert!(names.contains(&"G"), "G not found among {names:?}");
13062 }
13063
13064 #[test]
13065 fn malformed_declaration_then_clean_intent_recovers() {
13066 let src = "flow @ () { } intent I {}";
13067 let pr = recover(src);
13068 assert!(pr.has_errors());
13069 let kinds: Vec<&str> = pr
13070 .program
13071 .declarations
13072 .iter()
13073 .map(|d| match d {
13074 Declaration::Intent(_) => "intent",
13075 Declaration::Flow(_) => "flow",
13076 _ => "other",
13077 })
13078 .collect();
13079 assert!(kinds.contains(&"intent"), "kinds: {kinds:?}");
13080 }
13081
13082 #[test]
13083 fn recovery_does_not_double_count_a_single_error() {
13084 let src = "flow F() { not_a_step }";
13092 let pr = recover(src);
13093 let outer_ghosts = pr
13094 .errors
13095 .iter()
13096 .filter(|e| e.message.contains("at top level"))
13097 .count();
13098 assert_eq!(outer_ghosts, 0, "ghost errors: {:?}", pr.errors);
13099 }
13100
13101 #[test]
13104 fn three_independent_errors_yield_three_entries() {
13105 let src =
13106 "garbage1 flow F() { } garbage2 flow G() { } garbage3 flow H() { }";
13107 let pr = recover(src);
13108 assert_eq!(pr.errors.len(), 3, "errors: {:?}", pr.errors);
13109 assert_eq!(pr.program.declarations.len(), 3);
13110 }
13111
13112 #[test]
13113 fn all_errors_no_valid_declarations() {
13114 let src = "foo bar baz qux";
13115 let pr = recover(src);
13116 assert!(pr.has_errors());
13117 assert!(pr.program.declarations.is_empty());
13118 }
13119
13120 #[test]
13121 fn errors_recorded_in_source_order() {
13122 let src = "x flow A() { } y flow B() { } z flow C() { }";
13123 let pr = recover(src);
13124 assert_eq!(pr.errors.len(), 3);
13125 let lines: Vec<u32> = pr.errors.iter().map(|e| e.line).collect();
13126 assert!(
13129 lines.windows(2).all(|w| w[0] <= w[1]),
13130 "errors out of order: {lines:?}"
13131 );
13132 }
13133
13134 #[test]
13137 fn sync_to_flow_keyword() {
13138 let src = "garbage flow F() { }";
13139 let pr = recover(src);
13140 assert_eq!(pr.program.declarations.len(), 1);
13141 }
13142
13143 #[test]
13144 fn sync_to_intent_keyword() {
13145 let src = "garbage intent I {}";
13146 let pr = recover(src);
13147 assert_eq!(pr.program.declarations.len(), 1);
13148 }
13149
13150 #[test]
13151 fn sync_to_persona_keyword() {
13152 let src = "garbage persona P { name: \"P\" role: \"R\" }";
13153 let pr = recover(src);
13154 assert!(
13155 pr.program
13156 .declarations
13157 .iter()
13158 .any(|d| matches!(d, Declaration::Persona(_))),
13159 "persona not recovered: decls = {:?}",
13160 pr.program.declarations.len()
13161 );
13162 }
13163
13164 #[test]
13165 fn sync_to_run_keyword() {
13166 let src = "garbage run R { input: { user_message: \"hi\" } }";
13167 let pr = recover(src);
13168 assert!(pr.has_errors());
13170 }
13171
13172 #[test]
13175 fn parse_result_has_errors_and_is_clean_invert() {
13176 let pr_clean = recover("flow F() { }");
13177 assert!(pr_clean.is_clean());
13178 assert!(!pr_clean.has_errors());
13179
13180 let pr_err = recover("garbage");
13181 assert!(!pr_err.is_clean());
13182 assert!(pr_err.has_errors());
13183 }
13184
13185 #[test]
13186 fn parse_result_program_field_holds_partial_program() {
13187 let pr = recover("garbage flow F() { }");
13188 assert!(!pr.program.declarations.is_empty());
13189 }
13190
13191 #[test]
13192 fn parse_result_errors_carry_line_and_column() {
13193 let pr = recover("garbage");
13194 assert!(!pr.errors.is_empty());
13195 let e = &pr.errors[0];
13196 assert!(e.line >= 1);
13197 let _ = e.column;
13200 assert!(!e.message.is_empty());
13201 }
13202
13203 #[test]
13204 fn parse_result_debug_renders() {
13205 let pr = recover("flow F() { }");
13206 let s = format!("{pr:?}");
13207 assert!(s.contains("ParseResult"));
13208 }
13209
13210 #[test]
13213 fn empty_source_is_clean() {
13214 let pr = recover("");
13215 assert!(pr.is_clean());
13216 assert!(pr.program.declarations.is_empty());
13217 }
13218
13219 #[test]
13220 fn whitespace_only_source_is_clean() {
13221 let pr = recover(" \n\n\t \n");
13222 assert!(pr.is_clean());
13223 assert!(pr.program.declarations.is_empty());
13224 }
13225
13226 #[test]
13227 fn only_garbage_does_not_crash() {
13228 let pr = recover("foo bar baz { qux quux } corge { grault }");
13230 assert!(pr.has_errors());
13231 }
13232
13233 #[test]
13234 fn unbalanced_close_brace_does_not_crash() {
13235 let pr = recover("} flow F() { }");
13236 let names: Vec<&str> = pr
13238 .program
13239 .declarations
13240 .iter()
13241 .filter_map(|d| match d {
13242 Declaration::Flow(f) => Some(f.name.as_str()),
13243 _ => None,
13244 })
13245 .collect();
13246 assert!(names.contains(&"F"), "F not recovered: {names:?}");
13247 }
13248
13249 #[test]
13250 fn error_at_eof_does_not_loop() {
13251 let pr = recover("flow F() { ");
13253 let _ = pr.errors.len();
13255 }
13256
13257 #[test]
13258 fn nested_braces_inside_error_still_balance() {
13259 let src = "flow F() { not_a_step { inner } } flow G() { }";
13262 let pr = recover(src);
13263 let names: Vec<&str> = pr
13264 .program
13265 .declarations
13266 .iter()
13267 .filter_map(|d| match d {
13268 Declaration::Flow(f) => Some(f.name.as_str()),
13269 _ => None,
13270 })
13271 .collect();
13272 assert!(names.contains(&"G"), "G not recovered: {names:?}");
13273 }
13274
13275 #[derive(Clone, Copy)]
13284 struct Xorshift(u64);
13285 impl Xorshift {
13286 fn next(&mut self) -> u64 {
13287 let mut x = self.0;
13288 x ^= x << 13;
13289 x ^= x >> 7;
13290 x ^= x << 17;
13291 self.0 = x;
13292 x
13293 }
13294 fn pick<T: Copy>(&mut self, slice: &[T]) -> T {
13295 slice[(self.next() as usize) % slice.len()]
13296 }
13297 }
13298
13299 fn mutate(src: &str, rng: &mut Xorshift) -> String {
13300 let mut bytes: Vec<u8> = src.bytes().collect();
13301 if bytes.is_empty() {
13302 return src.to_string();
13303 }
13304 let op = rng.next() % 4;
13305 let pos = (rng.next() as usize) % bytes.len();
13306 let safe: &[u8] = b"abcdefghijklmnopqrstuvwxyz {}();:,_0123456789";
13310 match op {
13311 0 => {
13312 bytes.remove(pos);
13313 }
13314 1 => {
13315 let b = rng.pick(safe);
13316 bytes.insert(pos, b);
13317 }
13318 2 if pos + 1 < bytes.len() => {
13319 bytes.swap(pos, pos + 1);
13320 }
13321 _ => {
13322 let b = rng.pick(safe);
13323 bytes[pos] = b;
13324 }
13325 }
13326 bytes.retain(|b| b.is_ascii());
13329 String::from_utf8_lossy(&bytes).into_owned()
13330 }
13331
13332 #[test]
13333 fn fuzz_recovery_never_crashes() {
13334 let seed_bases = [
13335 "flow F() { }",
13336 "intent I { }",
13337 "persona P { name: \"P\" role: \"R\" }",
13338 "intent J { ask: \"a\" }",
13339 "type T = String",
13340 ];
13341 for (bucket, base) in (0..100u64).zip(seed_bases.iter().cycle()) {
13343 let mut rng = Xorshift(0x1234_5678_9abc_def0_u64.wrapping_add(bucket));
13344 let mut current = (*base).to_string();
13345 for _ in 0..10 {
13346 current = mutate(¤t, &mut rng);
13347 let toks = match Lexer::new(¤t, "<fuzz>").tokenize() {
13350 Ok(t) => t,
13351 Err(_) => continue,
13352 };
13353 let _pr = Parser::new(toks).parse_with_recovery();
13355 }
13356 }
13357 }
13358
13359 #[test]
13362 fn missing_colon_hint_preserved_under_recovery() {
13363 let src = "flow F() { run R { input { user_message: \"hi\" } } }";
13367 let pr = recover(src);
13368 if !pr.errors.is_empty() {
13372 let any_msg = pr.errors.iter().any(|e| !e.message.is_empty());
13373 assert!(any_msg);
13374 }
13375 }
13376
13377 #[test]
13380 fn recovered_declarations_appear_in_source_order() {
13381 let src = "flow A() { } garbage flow B() { } garbage flow C() { }";
13382 let pr = recover(src);
13383 let names: Vec<&str> = pr
13384 .program
13385 .declarations
13386 .iter()
13387 .filter_map(|d| match d {
13388 Declaration::Flow(f) => Some(f.name.as_str()),
13389 _ => None,
13390 })
13391 .collect();
13392 assert_eq!(names, vec!["A", "B", "C"]);
13393 }
13394}
13395
13396#[cfg(test)]
13404mod source_context_tests {
13405 use super::*;
13406 use crate::lexer::Lexer;
13407
13408 fn snippet(source: &str, line: u32, column: u32, filename: &str) -> String {
13409 SourceSnippet::new(
13410 source.to_string(),
13411 line,
13412 column,
13413 filename.to_string(),
13414 )
13415 .render()
13416 }
13417
13418 #[test]
13421 fn rustc_style_block_for_middle_line() {
13422 let src = "line one\nline two\nline three\nline four\nline five";
13423 let out = snippet(src, 3, 6, "x.axon");
13424 assert!(out.contains("--> x.axon:3:6"));
13425 assert!(out.contains("1 | line one"));
13426 assert!(out.contains("2 | line two"));
13427 assert!(out.contains("3 | line three"));
13428 assert!(out.contains("4 | line four"));
13429 assert!(out.contains("5 | line five"));
13430 assert!(out.contains("\n | ^"), "out:\n{out}");
13432 }
13433
13434 #[test]
13435 fn caret_column_one_renders_correctly() {
13436 let out = snippet("abc\n", 1, 1, "<source>");
13437 assert!(out.contains("\n | ^"));
13438 }
13439
13440 #[test]
13441 fn first_line_clamps_context_before_to_zero() {
13442 let src = "first\nsecond\nthird\nfourth\nfifth";
13443 let out = snippet(src, 1, 1, "<source>");
13444 assert!(out.contains("1 | first"));
13445 assert!(out.contains("2 | second"));
13446 assert!(out.contains("3 | third"));
13447 assert!(!out.contains("4 | fourth"));
13448 }
13449
13450 #[test]
13451 fn last_line_clamps_context_after_to_eof() {
13452 let src = "first\nsecond\nthird\nfourth\nfifth";
13453 let out = snippet(src, 5, 2, "<source>");
13454 assert!(out.contains("5 | fifth"));
13455 assert!(out.contains("3 | third"));
13456 assert!(out.contains("4 | fourth"));
13457 assert!(!out.contains("2 | second"));
13458 }
13459
13460 #[test]
13461 fn gutter_width_grows_with_line_count() {
13462 let src: String = (1..=12).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
13463 let out = snippet(&src, 12, 1, "<source>");
13464 assert!(out.contains("12 | line12"));
13465 assert!(out.contains("10 | line10"));
13466 }
13467
13468 #[test]
13471 fn empty_source_returns_empty() {
13472 assert_eq!(snippet("", 1, 1, "<source>"), "");
13473 }
13474
13475 #[test]
13476 fn zero_line_returns_empty() {
13477 assert_eq!(snippet("hi", 0, 1, "<source>"), "");
13478 }
13479
13480 #[test]
13481 fn out_of_range_line_returns_empty() {
13482 assert_eq!(snippet("hi", 99, 1, "<source>"), "");
13483 }
13484
13485 #[test]
13486 fn caret_clamps_past_eol() {
13487 let out = snippet("hello", 1, 50, "<source>");
13488 assert!(out.contains("\n | ^"), "out:\n{out}");
13489 }
13490
13491 #[test]
13492 fn unicode_codepoint_count_for_caret_clamp() {
13493 let out = snippet("héllo", 1, 99, "<source>");
13495 assert!(out.contains("\n | ^"), "out:\n{out}");
13496 }
13497
13498 #[test]
13499 fn trailing_newline_does_not_create_phantom_last_line() {
13500 let out = snippet("first\nsecond\n", 2, 1, "<source>");
13501 assert!(!out.contains("3 |"));
13502 assert!(out.contains("2 | second"));
13503 }
13504
13505 fn lex(src: &str) -> Vec<Token> {
13508 Lexer::new(src, "<test>").tokenize().expect("lex")
13509 }
13510
13511 #[test]
13512 fn strict_parse_attaches_snippet_when_source_given() {
13513 let src = "garbage_token\nflow F() { }";
13514 let err = Parser::new(lex(src))
13515 .with_source(src, "x.axon")
13516 .parse()
13517 .expect_err("must error");
13518 assert!(err.source_snippet.is_some());
13519 let display = format!("{err}");
13520 assert!(display.contains("--> x.axon:"), "display: {display}");
13521 }
13522
13523 #[test]
13524 fn strict_parse_no_snippet_when_no_source() {
13525 let src = "garbage_token";
13526 let err = Parser::new(lex(src)).parse().expect_err("must error");
13527 assert!(err.source_snippet.is_none());
13528 let display = format!("{err}");
13529 assert!(!display.contains("\n -->"));
13530 }
13531
13532 #[test]
13533 fn every_recovered_error_has_snippet() {
13534 let src = "garbage1\nflow F() { }\ngarbage2\nflow G() { }";
13535 let result = Parser::new(lex(src))
13536 .with_source(src, "multi.axon")
13537 .parse_with_recovery();
13538 assert!(!result.errors.is_empty());
13539 for err in &result.errors {
13540 assert!(err.source_snippet.is_some());
13541 let display = format!("{err}");
13542 assert!(
13543 display.contains("--> multi.axon:"),
13544 "display: {display}"
13545 );
13546 }
13547 }
13548
13549 #[test]
13550 fn recovery_no_snippet_when_no_source() {
13551 let src = "garbage1 garbage2";
13552 let result = Parser::new(lex(src)).parse_with_recovery();
13553 for err in &result.errors {
13554 assert!(err.source_snippet.is_none());
13555 }
13556 }
13557
13558 #[test]
13559 fn snippet_points_at_correct_line_for_each_error() {
13560 let src = "garbage_a\nflow F() { }\ngarbage_b\nflow G() { }";
13561 let result = Parser::new(lex(src))
13562 .with_source(src, "x")
13563 .parse_with_recovery();
13564 for err in &result.errors {
13565 let sn = err.source_snippet.as_ref().expect("snippet");
13566 assert_eq!(sn.line, err.line);
13567 }
13568 }
13569
13570 #[test]
13573 fn legacy_constructor_still_works() {
13574 let src = "flow F() { }";
13575 let prog = Parser::new(lex(src)).parse().expect("clean");
13576 assert_eq!(prog.declarations.len(), 1);
13577 }
13578
13579 #[test]
13580 fn attach_source_idempotent() {
13581 let err = ParseError {
13582 message: "bad".to_string(),
13583 line: 2,
13584 column: 3,
13585 ..Default::default()
13586 };
13587 let err2 = err.clone().attach_source("a\nb\nc\n", "f.axon");
13588 let first = format!("{err2}");
13589 let err3 = err.attach_source("a\nb\nc\n", "f.axon");
13590 let second = format!("{err3}");
13591 assert_eq!(first, second);
13592 }
13593
13594 #[test]
13595 fn attach_source_noop_when_line_zero() {
13596 let err = ParseError {
13597 message: "bad".to_string(),
13598 line: 0,
13599 column: 0,
13600 ..Default::default()
13601 };
13602 let err = err.attach_source("a\nb\nc\n", "f.axon");
13603 assert!(err.source_snippet.is_none());
13604 }
13605
13606 #[test]
13612 fn golden_simple_three_line_block() {
13613 let src = "alpha\nbeta\ngamma";
13614 let out = snippet(src, 2, 3, "g.axon");
13615 let expected = concat!(
13619 " --> g.axon:2:3\n",
13620 " |\n",
13621 "1 | alpha\n",
13622 "2 | beta\n",
13623 " | ^\n",
13624 "3 | gamma",
13625 );
13626 assert_eq!(out, expected);
13627 }
13628
13629 #[test]
13630 fn golden_first_line_caret() {
13631 let src = "abc\ndef\n";
13632 let out = snippet(src, 1, 1, "x");
13633 let expected = concat!(
13634 " --> x:1:1\n",
13635 " |\n",
13636 "1 | abc\n",
13637 " | ^\n",
13638 "2 | def",
13639 );
13640 assert_eq!(out, expected);
13641 }
13642
13643 #[test]
13644 fn golden_two_digit_gutter() {
13645 let src: String = (1..=11)
13646 .map(|i| format!("L{i}"))
13647 .collect::<Vec<_>>()
13648 .join("\n");
13649 let out = snippet(&src, 10, 2, "big");
13650 let expected = concat!(
13651 " --> big:10:2\n",
13652 " |\n",
13653 " 8 | L8\n",
13654 " 9 | L9\n",
13655 "10 | L10\n",
13656 " | ^\n",
13657 "11 | L11",
13658 );
13659 assert_eq!(out, expected);
13660 }
13661}
13662
13663#[cfg(test)]
13670mod smart_suggest_parser_tests {
13671 use super::*;
13672 use crate::lexer::Lexer;
13673
13674 fn lex(src: &str) -> Vec<Token> {
13675 Lexer::new(src, "<test>").tokenize().expect("lex")
13676 }
13677
13678 #[test]
13679 fn top_level_typo_suggests_flow() {
13680 let src = "flwo F() { }";
13681 let err = Parser::new(lex(src)).parse().expect_err("must error");
13682 assert!(
13683 err.message.contains("Did you mean `flow`?"),
13684 "msg: {}",
13685 err.message
13686 );
13687 }
13688
13689 #[test]
13690 fn top_level_unknown_far_no_suggestion() {
13691 let src = "qwerty F() { }";
13692 let err = Parser::new(lex(src)).parse().expect_err("must error");
13693 assert!(
13694 !err.message.contains("Did you mean"),
13695 "msg: {}",
13696 err.message
13697 );
13698 }
13699
13700 #[test]
13701 fn flow_body_typo_suggests_step() {
13702 let src = "flow F() { stepp S {} }";
13703 let err = Parser::new(lex(src)).parse().expect_err("must error");
13704 assert!(
13705 err.message.contains("Did you mean `step`"),
13706 "msg: {}",
13707 err.message
13708 );
13709 }
13710
13711 #[test]
13712 fn flow_body_typo_suggests_reason() {
13713 let src = "flow F() { reasn R {} }";
13714 let err = Parser::new(lex(src)).parse().expect_err("must error");
13715 assert!(
13716 err.message.contains("Did you mean `reason`?"),
13717 "msg: {}",
13718 err.message
13719 );
13720 }
13721
13722 #[test]
13723 fn recovery_mode_carries_hint() {
13724 let src = "flwo F() { }";
13725 let result = Parser::new(lex(src)).parse_with_recovery();
13726 assert!(
13727 result
13728 .errors
13729 .iter()
13730 .any(|e| e.message.contains("Did you mean `flow`?")),
13731 "errors: {:?}",
13732 result.errors
13733 );
13734 }
13735}
13736
13737#[cfg(test)]
13740mod mutate_purge_where_tests {
13741 use super::*;
13742
13743 fn parse(src: &str) -> Program {
13744 let tokens = crate::lexer::Lexer::new(src, "<test>")
13745 .tokenize()
13746 .expect("lex");
13747 Parser::new(tokens).parse().expect("parse")
13748 }
13749
13750 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13751 for d in &prog.declarations {
13752 if let Declaration::Flow(f) = d {
13753 if f.name == flow {
13754 return f.body.first().expect("flow has at least one step");
13755 }
13756 }
13757 }
13758 panic!("flow `{flow}` not found");
13759 }
13760
13761 #[test]
13762 fn mutate_captures_its_where_clause() {
13763 let prog =
13766 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13767 match first_step(&prog, "F") {
13768 FlowStep::Mutate(m) => {
13769 assert_eq!(m.store_name, "accounts");
13770 assert_eq!(m.where_expr, "id = 1");
13771 }
13772 other => panic!("expected Mutate, got {other:?}"),
13773 }
13774 }
13775
13776 #[test]
13777 fn purge_captures_its_where_clause() {
13778 let prog =
13779 parse("flow F() -> Unit { purge logs { where: \"ts < 100\" } }");
13780 match first_step(&prog, "F") {
13781 FlowStep::Purge(p) => {
13782 assert_eq!(p.store_name, "logs");
13783 assert_eq!(p.where_expr, "ts < 100");
13784 }
13785 other => panic!("expected Purge, got {other:?}"),
13786 }
13787 }
13788
13789 #[test]
13790 fn mutate_without_a_where_block_is_a_whole_store_op() {
13791 let prog = parse("flow F() -> Unit { mutate accounts }");
13794 match first_step(&prog, "F") {
13795 FlowStep::Mutate(m) => {
13796 assert_eq!(m.store_name, "accounts");
13797 assert_eq!(m.where_expr, "");
13798 }
13799 other => panic!("expected Mutate, got {other:?}"),
13800 }
13801 }
13802}
13803
13804#[cfg(test)]
13807mod persist_fields_tests {
13808 use super::*;
13809
13810 fn parse(src: &str) -> Program {
13811 let tokens = crate::lexer::Lexer::new(src, "<test>")
13812 .tokenize()
13813 .expect("lex");
13814 Parser::new(tokens).parse().expect("parse")
13815 }
13816
13817 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13818 for d in &prog.declarations {
13819 if let Declaration::Flow(f) = d {
13820 if f.name == flow {
13821 return f.body.first().expect("flow has at least one step");
13822 }
13823 }
13824 }
13825 panic!("flow `{flow}` not found");
13826 }
13827
13828 #[test]
13829 fn persist_captures_its_field_block() {
13830 let prog = parse(
13834 "flow F() -> Unit { persist into chat_history { \
13835 session_id: \"${session_id}\" sender: \"user\" \
13836 content: \"${message}\" } }",
13837 );
13838 match first_step(&prog, "F") {
13839 FlowStep::Persist(p) => {
13840 assert_eq!(p.store_name, "chat_history");
13841 assert_eq!(
13842 p.fields,
13843 vec![
13844 ("session_id".to_string(), "${session_id}".to_string()),
13845 ("sender".to_string(), "user".to_string()),
13846 ("content".to_string(), "${message}".to_string()),
13847 ]
13848 );
13849 }
13850 other => panic!("expected Persist, got {other:?}"),
13851 }
13852 }
13853
13854 #[test]
13855 fn persist_without_a_block_keeps_the_user_bindings_fallback() {
13856 let prog = parse("flow F() -> Unit { persist events }");
13859 match first_step(&prog, "F") {
13860 FlowStep::Persist(p) => {
13861 assert_eq!(p.store_name, "events");
13862 assert!(p.fields.is_empty());
13863 }
13864 other => panic!("expected Persist, got {other:?}"),
13865 }
13866 }
13867
13868 #[test]
13869 fn persist_accepts_the_optional_into_connector() {
13870 let with =
13873 parse("flow F() -> Unit { persist into accounts { id: \"1\" } }");
13874 let without =
13875 parse("flow F() -> Unit { persist accounts { id: \"1\" } }");
13876 for prog in [&with, &without] {
13877 match first_step(prog, "F") {
13878 FlowStep::Persist(p) => assert_eq!(p.store_name, "accounts"),
13879 other => panic!("expected Persist, got {other:?}"),
13880 }
13881 }
13882 }
13883
13884 #[test]
13885 fn persist_into_without_a_block_resolves_the_store_name() {
13886 let prog = parse("flow F() -> Unit { persist into events }");
13889 match first_step(&prog, "F") {
13890 FlowStep::Persist(p) => {
13891 assert_eq!(p.store_name, "events");
13892 assert!(p.fields.is_empty());
13893 }
13894 other => panic!("expected Persist, got {other:?}"),
13895 }
13896 }
13897
13898 #[test]
13899 fn persist_fields_lower_into_the_ir() {
13900 let prog = parse(
13903 "flow F() -> Unit { persist into chat { content: \"${msg}\" } }",
13904 );
13905 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
13906 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
13907 match flow.steps.first().expect("one step") {
13908 crate::ir_nodes::IRFlowNode::Persist(p) => {
13909 assert_eq!(p.store_name, "chat");
13910 assert_eq!(
13911 p.fields,
13912 vec![("content".to_string(), "${msg}".to_string())]
13913 );
13914 }
13915 other => panic!("expected IRFlowNode::Persist, got {other:?}"),
13916 }
13917 }
13918}
13919
13920#[cfg(test)]
13923mod mutate_fields_tests {
13924 use super::*;
13925
13926 fn parse(src: &str) -> Program {
13927 let tokens = crate::lexer::Lexer::new(src, "<test>")
13928 .tokenize()
13929 .expect("lex");
13930 Parser::new(tokens).parse().expect("parse")
13931 }
13932
13933 fn first_step<'a>(prog: &'a Program, flow: &str) -> &'a FlowStep {
13934 for d in &prog.declarations {
13935 if let Declaration::Flow(f) = d {
13936 if f.name == flow {
13937 return f.body.first().expect("flow has at least one step");
13938 }
13939 }
13940 }
13941 panic!("flow `{flow}` not found");
13942 }
13943
13944 #[test]
13945 fn mutate_captures_its_set_field_block() {
13946 let prog = parse(
13950 "flow F() -> Unit { mutate accounts { where: \"id = ${id}\" \
13951 balance: \"${new_balance}\" status: \"active\" } }",
13952 );
13953 match first_step(&prog, "F") {
13954 FlowStep::Mutate(m) => {
13955 assert_eq!(m.store_name, "accounts");
13956 assert_eq!(m.where_expr, "id = ${id}");
13957 assert_eq!(
13958 m.fields,
13959 vec![
13960 ("balance".to_string(), "${new_balance}".to_string()),
13961 ("status".to_string(), "active".to_string()),
13962 ]
13963 );
13964 }
13965 other => panic!("expected Mutate, got {other:?}"),
13966 }
13967 }
13968
13969 #[test]
13970 fn mutate_where_only_block_has_no_set_fields() {
13971 let prog =
13974 parse("flow F() -> Unit { mutate accounts { where: \"id = 1\" } }");
13975 match first_step(&prog, "F") {
13976 FlowStep::Mutate(m) => {
13977 assert_eq!(m.where_expr, "id = 1");
13978 assert!(m.fields.is_empty());
13979 }
13980 other => panic!("expected Mutate, got {other:?}"),
13981 }
13982 }
13983
13984 #[test]
13985 fn mutate_with_no_block_is_a_whole_store_op() {
13986 let prog = parse("flow F() -> Unit { mutate accounts }");
13989 match first_step(&prog, "F") {
13990 FlowStep::Mutate(m) => {
13991 assert_eq!(m.store_name, "accounts");
13992 assert_eq!(m.where_expr, "");
13993 assert!(m.fields.is_empty());
13994 }
13995 other => panic!("expected Mutate, got {other:?}"),
13996 }
13997 }
13998
13999 #[test]
14000 fn mutate_fields_lower_into_the_ir() {
14001 let prog = parse(
14002 "flow F() -> Unit { mutate t { where: \"id = 1\" v: \"${x}\" } }",
14003 );
14004 let ir = crate::ir_generator::IRGenerator::new().generate(&prog);
14005 let flow = ir.flows.iter().find(|f| f.name == "F").expect("flow F");
14006 match flow.steps.first().expect("one step") {
14007 crate::ir_nodes::IRFlowNode::Mutate(m) => {
14008 assert_eq!(m.where_expr, "id = 1");
14009 assert_eq!(
14010 m.fields,
14011 vec![("v".to_string(), "${x}".to_string())]
14012 );
14013 }
14014 other => panic!("expected IRFlowNode::Mutate, got {other:?}"),
14015 }
14016 }
14017}
14018