1use std::collections::HashMap;
5use memchr::memchr;
6use crate::value::*;
7use crate::rng;
8
9pub(crate) const MAX_SYNX_INPUT_BYTES: usize = 16 * 1024 * 1024;
14
15const MAX_LINE_STARTS: usize = 2_000_000;
17
18const MAX_PARSE_NESTING_DEPTH: usize = 128;
20
21const MAX_MULTILINE_BLOCK_BYTES: usize = 1024 * 1024;
23
24const MAX_LIST_ITEMS: usize = 1_048_576;
26
27const MAX_INCLUDE_DIRECTIVES: usize = 4096;
29
30const MAX_CONSTRAINT_ENUM_PARTS: usize = 4096;
32
33const MAX_MARKER_CHAIN_SEGMENTS: usize = 512;
35
36pub(crate) fn clamp_synx_text(text: &str) -> &str {
38 if text.len() <= MAX_SYNX_INPUT_BYTES {
39 return text;
40 }
41 let slice = &text.as_bytes()[..MAX_SYNX_INPUT_BYTES];
42 let end = core::str::from_utf8(slice)
43 .map(|s| s.len())
44 .unwrap_or_else(|e| e.valid_up_to());
45 &text[..end]
46}
47
48fn find_parse_end_bytes(bytes: &[u8]) -> usize {
51 let max_newlines = MAX_LINE_STARTS.saturating_sub(1);
52 let mut seen_newlines = 0usize;
53 let mut scan = 0usize;
54 while scan < bytes.len() {
55 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
56 if seen_newlines >= max_newlines {
57 return scan + rel;
58 }
59 seen_newlines += 1;
60 scan += rel + 1;
61 } else {
62 break;
63 }
64 }
65 bytes.len()
66}
67
68#[derive(Debug, Clone, Copy)]
74pub struct ParserOptions {
75 pub directives: bool,
87}
88
89impl Default for ParserOptions {
90 fn default() -> Self {
91 Self { directives: true }
92 }
93}
94
95pub fn parse(text: &str) -> ParseResult {
97 parse_with(text, ParserOptions::default())
98}
99
100pub fn parse_with(text: &str, opts: ParserOptions) -> ParseResult {
104 let text = clamp_synx_text(text);
105 let parse_end = find_parse_end_bytes(text.as_bytes());
106 let text = &text[..parse_end];
107 let bytes = text.as_bytes();
108
109 let mut line_starts: Vec<usize> = Vec::new();
110 line_starts.push(0);
111 let mut scan = 0usize;
112 while scan < bytes.len() {
113 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
114 let pos = scan + rel;
115 line_starts.push(pos + 1);
116 scan = pos + 1;
117 } else {
118 break;
119 }
120 }
121 let line_count = line_starts.len();
122
123 let mut root = HashMap::new();
124 let mut stack: Vec<(i32, StackEntry)> = vec![(-1, StackEntry::Root)];
125 let mut mode = Mode::Static;
126 let mut locked = false;
127 let mut tool = false;
128 let mut schema = false;
129 let mut llm = false;
130 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
131 let mut includes: Vec<IncludeDirective> = Vec::new();
132 let mut uses: Vec<UseDirective> = Vec::new();
133
134 let mut block: Option<BlockState> = None;
135 let mut list: Option<ListState> = None;
136 let mut in_block_comment = false;
137
138 let mut i = 0;
139 while i < line_count {
140 let start = line_starts[i];
142 let end = if i + 1 < line_count { line_starts[i + 1] - 1 } else { bytes.len() };
143 let end = if end > start && end > 0 && bytes.get(end - 1) == Some(&b'\r') { end - 1 } else { end };
145 let raw = &text[start..end];
146
147 let trimmed = raw.trim();
148
149 if opts.directives && (trimmed.starts_with('!') || trimmed.starts_with("#!mode:")) {
155 if trimmed == "!active" {
157 mode = Mode::Active;
158 i += 1;
159 continue;
160 }
161 if trimmed == "!lock" {
162 locked = true;
163 i += 1;
164 continue;
165 }
166 if trimmed == "!tool" {
167 tool = true;
168 i += 1;
169 continue;
170 }
171 if trimmed == "!schema" {
172 schema = true;
173 i += 1;
174 continue;
175 }
176 if trimmed == "!llm" {
177 llm = true;
178 i += 1;
179 continue;
180 }
181 if trimmed.starts_with("!include ") {
182 if includes.len() < MAX_INCLUDE_DIRECTIVES {
183 let rest = trimmed[9..].trim();
184 let mut parts = rest.splitn(2, char::is_whitespace);
185 let path = parts.next().unwrap_or("").to_string();
186 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
187 let name = path.rsplit(&['/', '\\'][..]).next().unwrap_or(&path);
189 name.strip_suffix(".synx").or_else(|| name.strip_suffix(".SYNX")).unwrap_or(name).to_string()
190 });
191 includes.push(IncludeDirective { path, alias });
192 }
193 i += 1;
194 continue;
195 }
196 if trimmed.starts_with("!use ") {
197 let rest = trimmed[5..].trim();
198 if rest.starts_with('@') {
199 let mut parts = rest.splitn(2, " as ");
201 let package = parts.next().unwrap_or("").trim().to_string();
202 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
203 package.rsplit('/').next().unwrap_or(&package).to_string()
205 });
206 if !package.is_empty() {
207 uses.push(UseDirective { package, alias });
208 }
209 }
210 i += 1;
211 continue;
212 }
213 if trimmed.starts_with("#!mode:") {
214 let declared = trimmed.splitn(2, ':').nth(1).unwrap_or("static").trim();
215 mode = if declared == "active" { Mode::Active } else { Mode::Static };
216 i += 1;
217 continue;
218 }
219 }
220
221 if trimmed == "###" {
223 in_block_comment = !in_block_comment;
224 i += 1;
225 continue;
226 }
227 if in_block_comment {
228 i += 1;
229 continue;
230 }
231
232 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
234 i += 1;
235 continue;
236 }
237
238 let indent = (raw.len() - raw.trim_start().len()) as i32;
239
240 if let Some(ref mut blk) = block {
242 if indent > blk.indent {
243 if blk.content.len() < MAX_MULTILINE_BLOCK_BYTES {
244 if !blk.content.is_empty() {
245 blk.content.push('\n');
246 }
247 let room = MAX_MULTILINE_BLOCK_BYTES.saturating_sub(blk.content.len());
248 if room > 0 {
249 let slice: &str = if blk.preserve_indent {
250 if blk.base_indent < 0 {
256 blk.base_indent = indent;
257 }
258 let strip = blk.base_indent.min(indent) as usize;
264 let raw_trim_end = raw.trim_end();
265 if strip < raw_trim_end.len() {
266 &raw_trim_end[strip..]
267 } else {
268 ""
269 }
270 } else {
271 trimmed
272 };
273 let n = slice.len().min(room);
274 blk.content.push_str(&slice[..n]);
275 }
276 }
277 i += 1;
278 continue;
279 } else {
280 let content = std::mem::take(&mut blk.content);
281 let blk_key = blk.key.clone();
282 let blk_stack_idx = blk.stack_idx;
283 block = None;
284 insert_value(&mut root, &stack, blk_stack_idx, &blk_key, Value::String(content));
285 }
286 }
287
288 if !opts.directives && trimmed.starts_with('!') {
293 i += 1;
294 continue;
295 }
296
297 if trimmed.starts_with("- ") {
299 if let Some(ref lst) = list {
300 if indent > lst.indent {
301 while stack.len() > 1 {
304 match stack.last() {
305 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
306 _ => break,
307 }
308 }
309
310 let val_str = strip_comment(trimmed[2..].trim());
311
312 let mut peek = i + 1;
316 let mut nested = false;
317 while peek < line_count {
318 let ps = line_starts[peek];
319 let pe = if peek + 1 < line_count { line_starts[peek + 1] - 1 } else { bytes.len() };
320 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
321 let pl = &text[ps..pe];
322 let pt = pl.trim();
323 if pt.is_empty() {
324 peek += 1;
325 continue;
326 }
327 let pi = (pl.len() - pl.trim_start().len()) as i32;
328 if pi > indent
329 && !pt.starts_with("- ")
330 && !pt.starts_with('#')
331 && !pt.starts_with("//")
332 {
333 nested = true;
334 }
335 break;
336 }
337
338 let list_key = lst.key.clone();
339 let list_stack_idx = lst.stack_idx;
340
341 if let Some(parent_map) = navigate_to_parent(&mut root, &stack, list_stack_idx) {
343 let arr_entry = parent_map
344 .entry(list_key.clone())
345 .or_insert_with(|| Value::Array(Vec::new()));
346 if let Value::Array(arr) = arr_entry {
347 if arr.len() >= MAX_LIST_ITEMS {
348 i += 1;
349 continue;
350 }
351 if nested {
352 let mut item_obj: HashMap<String, Value> = HashMap::new();
353 if let Some(parsed) = parse_line(&val_str) {
354 let val = if let Some(ref hint) = parsed.type_hint {
355 cast_typed(&parsed.value, hint)
356 } else if !parsed.value.is_empty() {
357 cast(&parsed.value)
358 } else {
359 Value::Object(HashMap::new())
360 };
361 item_obj.insert(parsed.key, val);
362 } else {
363 item_obj.insert("_value".to_string(), cast(&val_str));
364 }
365 let item_idx = arr.len();
366 arr.push(Value::Object(item_obj));
367 if stack.len() < MAX_PARSE_NESTING_DEPTH {
368 stack.push((indent, StackEntry::ListItem { list_key, item_idx }));
369 }
370 } else {
371 arr.push(cast(&val_str));
372 }
373 }
374 }
375
376 i += 1;
377 continue;
378 }
379 }
380 } else {
381 let close = list.as_ref().map(|lst| indent <= lst.indent).unwrap_or(false);
383 if close {
384 list = None;
385 while stack.len() > 1 {
387 match stack.last() {
388 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
389 _ => break,
390 }
391 }
392 }
393 }
394
395 if let Some(parsed) = parse_line(trimmed) {
397 if parsed.key == "__proto__"
401 || parsed.key == "constructor"
402 || parsed.key == "prototype"
403 {
404 i += 1;
405 continue;
406 }
407
408 while stack.len() > 1 && stack.last().unwrap().0 >= indent {
410 stack.pop();
411 }
412
413 let parent_idx = stack.len() - 1;
414
415 if mode == Mode::Active
417 && (!parsed.markers.is_empty()
418 || parsed.constraints.is_some()
419 || parsed.type_hint.is_some())
420 {
421 let path = build_path(&stack);
422 let meta_map = metadata.entry(path).or_default();
423 meta_map.insert(
424 parsed.key.clone(),
425 Meta {
426 markers: parsed.markers.clone(),
427 args: parsed.marker_args.clone(),
428 type_hint: parsed.type_hint.clone(),
429 constraints: parsed.constraints.clone(),
430 },
431 );
432 }
433
434 let is_block = parsed.value == "|" || parsed.value == "|+";
440 let preserve_indent = parsed.value == "|+";
441 let is_list_marker = parsed.markers.iter().any(|m| {
442 matches!(m.as_str(), "random" | "unique" | "geo" | "join")
443 });
444
445 if is_block {
446 insert_value(
447 &mut root,
448 &stack,
449 parent_idx,
450 &parsed.key,
451 Value::String(String::new()),
452 );
453 block = Some(BlockState {
454 indent,
455 key: parsed.key,
456 content: String::new(),
457 stack_idx: parent_idx,
458 preserve_indent,
459 base_indent: -1,
460 });
461 } else if is_list_marker && parsed.value.is_empty() {
462 insert_value(
465 &mut root,
466 &stack,
467 parent_idx,
468 &parsed.key,
469 Value::Array(Vec::new()),
470 );
471 list = Some(ListState {
472 indent,
473 key: parsed.key,
474 items: Vec::new(),
475 stack_idx: parent_idx,
476 });
477 } else if parsed.value.is_empty() {
478 let mut peek = i + 1;
480 while peek < line_count {
481 let ps = line_starts[peek];
482 let pe = if peek + 1 < line_count {
483 line_starts[peek + 1] - 1
484 } else {
485 bytes.len()
486 };
487 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
488 let pt = text[ps..pe].trim();
489 if !pt.is_empty() {
490 break;
491 }
492 peek += 1;
493 }
494
495 if peek < line_count {
496 let ps = line_starts[peek];
497 let pe = if peek + 1 < line_count {
498 line_starts[peek + 1] - 1
499 } else {
500 bytes.len()
501 };
502 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
503 let pt = text[ps..pe].trim();
504 if pt.starts_with("- ") {
505 insert_value(
506 &mut root,
507 &stack,
508 parent_idx,
509 &parsed.key,
510 Value::Array(Vec::new()),
511 );
512 list = Some(ListState {
513 indent,
514 key: parsed.key,
515 items: Vec::new(),
516 stack_idx: parent_idx,
517 });
518 i += 1;
519 continue;
520 }
521 }
522
523 insert_value(
524 &mut root,
525 &stack,
526 parent_idx,
527 &parsed.key,
528 Value::Object(HashMap::new()),
529 );
530 if stack.len() < MAX_PARSE_NESTING_DEPTH {
534 stack.push((indent, StackEntry::Key(parsed.key)));
535 }
536 } else {
537 let value = if let Some(ref hint) = parsed.type_hint {
538 cast_typed(&parsed.value, hint)
539 } else {
540 cast(&parsed.value)
541 };
542 insert_value(&mut root, &stack, parent_idx, &parsed.key, value);
543 }
544 }
545
546 i += 1;
547 }
548
549 if let Some(blk) = block {
551 insert_value(
552 &mut root,
553 &stack,
554 blk.stack_idx,
555 &blk.key,
556 Value::String(blk.content),
557 );
558 }
559
560 let _ = list;
563
564 let parsed_root = Value::Object(root);
565
566 ParseResult {
570 root: parsed_root,
571 mode,
572 locked,
573 tool,
574 schema,
575 llm,
576 metadata,
577 includes,
578 uses,
579 }
580}
581
582pub fn reshape_tool_output(root: &Value, schema: bool) -> Value {
594 let map = match root {
595 Value::Object(m) => m,
596 _ => return root.clone(),
597 };
598
599 if schema {
600 let mut tools = Vec::new();
602 let mut keys: Vec<&String> = map.keys().collect();
604 keys.sort();
605 for key in keys {
606 let val = &map[key];
607 let mut def = HashMap::new();
608 def.insert("name".to_string(), Value::String(key.clone()));
609 def.insert("params".to_string(), val.clone());
610 tools.push(Value::Object(def));
611 }
612 let mut out = HashMap::new();
613 out.insert("tools".to_string(), Value::Array(tools));
614 Value::Object(out)
615 } else {
616 if map.is_empty() {
618 let mut out = HashMap::new();
619 out.insert("tool".to_string(), Value::Null);
620 out.insert("params".to_string(), Value::Object(HashMap::new()));
621 return Value::Object(out);
622 }
623
624 let mut keys: Vec<&String> = map.keys().collect();
627 keys.sort();
628 let tool_key = keys[0];
629 let tool_value = &map[tool_key];
630
631 let params = match tool_value {
632 Value::Object(m) => Value::Object(m.clone()),
633 _ => Value::Object(HashMap::new()),
635 };
636
637 let mut out = HashMap::new();
638 out.insert("tool".to_string(), Value::String(tool_key.clone()));
639 out.insert("params".to_string(), params);
640 Value::Object(out)
641 }
642}
643
644#[derive(Debug)]
647enum StackEntry {
648 Root,
649 Key(String),
650 ListItem { list_key: String, item_idx: usize },
655}
656
657struct BlockState {
658 indent: i32,
659 key: String,
660 content: String,
661 stack_idx: usize,
662 preserve_indent: bool,
665 base_indent: i32,
668}
669
670struct ListState {
671 indent: i32,
672 key: String,
673 items: Vec<Value>,
674 stack_idx: usize,
675}
676
677struct ParsedLine {
678 key: String,
679 type_hint: Option<String>,
680 value: String,
681 markers: Vec<String>,
682 marker_args: Vec<String>,
683 constraints: Option<Constraints>,
684}
685
686fn parse_line(trimmed: &str) -> Option<ParsedLine> {
689 if trimmed.is_empty()
690 || trimmed.starts_with('#')
691 || trimmed.starts_with("//")
692 || trimmed.starts_with("- ")
693 {
694 return None;
695 }
696
697 let bytes = trimmed.as_bytes();
698 let len = bytes.len();
699
700 let first = bytes[0];
701 if first == b'[' || first == b':' || first == b'-' || first == b'#' || first == b'/' || first == b'(' {
702 return None;
703 }
704
705 let mut pos = 0;
707 while pos < len {
708 let ch = bytes[pos];
709 if ch == b' ' || ch == b'\t' || ch == b'[' || ch == b':' || ch == b'(' {
710 break;
711 }
712 pos += 1;
713 }
714 let key = trimmed[..pos].to_string();
715
716 let mut type_hint = None;
718 if pos < len && bytes[pos] == b'(' {
719 let start = pos + 1;
720 if let Some(c) = trimmed[start..].find(')') {
721 type_hint = Some(trimmed[start..start + c].to_string());
722 pos = start + c + 1;
723 } else {
724 pos += 1;
725 }
726 }
727
728 let mut constraints = None;
731 if pos < len && bytes[pos] == b'[' {
732 let cstart = pos + 1;
733 let mut depth = 1usize;
734 let mut scan = cstart;
735 while scan < len && depth > 0 {
736 match bytes[scan] {
737 b'[' => depth += 1,
738 b']' => {
739 depth -= 1;
740 if depth == 0 {
741 break;
742 }
743 }
744 _ => {}
745 }
746 scan += 1;
747 }
748 if depth == 0 {
749 let constraint_str = &trimmed[cstart..scan];
750 constraints = Some(parse_constraints(constraint_str));
751 pos = scan + 1; } else {
753 if let Some(rel) = trimmed[cstart..].find(']') {
755 let constraint_str = &trimmed[cstart..cstart + rel];
756 constraints = Some(parse_constraints(constraint_str));
757 pos = cstart + rel + 1;
758 } else {
759 constraints = Some(parse_constraints(&trimmed[cstart..]));
760 pos = len;
761 }
762 }
763 }
764
765 let mut markers = Vec::new();
767 let mut marker_args = Vec::new();
768 if pos < len && bytes[pos] == b':' {
769 let marker_start = pos + 1;
770 let mut marker_end = marker_start;
771 while marker_end < len && bytes[marker_end] != b' ' && bytes[marker_end] != b'\t' {
772 marker_end += 1;
773 }
774 let chain = &trimmed[marker_start..marker_end];
775 markers = chain
776 .split(':')
777 .take(MAX_MARKER_CHAIN_SEGMENTS)
778 .map(|s| s.to_string())
779 .collect();
780 pos = marker_end;
781 }
782
783 while pos < len && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
785 pos += 1;
786 }
787
788 let mut raw_value = if pos < len {
790 strip_comment(&trimmed[pos..])
791 } else {
792 String::new()
793 };
794
795 if markers.contains(&"random".to_string()) && !raw_value.is_empty() {
797 let parts: Vec<&str> = raw_value.split_whitespace().collect();
798 let nums: Vec<String> = parts
799 .iter()
800 .filter(|s| s.parse::<f64>().is_ok())
801 .map(|s| s.to_string())
802 .collect();
803 if !nums.is_empty() {
804 marker_args = nums;
805 raw_value.clear();
806 }
807 }
808
809 if markers.contains(&"inherit".to_string()) && !raw_value.is_empty() {
812 marker_args = vec![raw_value.trim().to_string()];
813 raw_value.clear();
814 }
815
816 Some(ParsedLine {
817 key,
818 type_hint,
819 value: raw_value,
820 markers,
821 marker_args,
822 constraints,
823 })
824}
825
826pub(crate) fn parse_constraints(raw: &str) -> Constraints {
834 let mut c = Constraints::default();
835 for part in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
836 if part == "required" {
837 c.required = true;
838 } else if part == "readonly" {
839 c.readonly = true;
840 } else if let Some(colon) = part.find(':') {
841 let key = part[..colon].trim();
842 let val = part[colon + 1..].trim();
843 match key {
844 "min" => c.min = val.parse().ok(),
845 "max" => c.max = val.parse().ok(),
846 "type" => c.type_name = Some(val.to_string()),
847 "pattern" => c.pattern = Some(val.to_string()),
848 "enum" => {
849 c.enum_values = Some(
850 val.split('|')
851 .take(MAX_CONSTRAINT_ENUM_PARTS)
852 .map(|s| s.to_string())
853 .collect(),
854 );
855 }
856 _ => {}
857 }
858 }
859 }
860 c
861}
862
863pub(crate) fn cast(val: &str) -> Value {
867 if val.len() >= 2 {
870 let bytes = val.as_bytes();
871 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
872 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
873 {
874 return Value::String(val[1..val.len() - 1].to_string());
875 }
876 }
877
878 match val {
879 "true" => Value::Bool(true),
880 "false" => Value::Bool(false),
881 "null" => Value::Null,
882 _ => {
883 let bytes = val.as_bytes();
884 let len = bytes.len();
885 if len == 0 {
886 return Value::String(String::new());
887 }
888
889 let mut start = 0;
890 if bytes[0] == b'-' {
891 if len == 1 {
892 return Value::String(val.to_string());
893 }
894 start = 1;
895 }
896
897 if bytes[start] >= b'0' && bytes[start] <= b'9' {
898 let mut dot_pos = None;
899 let mut all_numeric = true;
900 for j in start..len {
901 if bytes[j] == b'.' {
902 if dot_pos.is_some() {
903 all_numeric = false;
904 break;
905 }
906 dot_pos = Some(j);
907 } else if bytes[j] < b'0' || bytes[j] > b'9' {
908 all_numeric = false;
909 break;
910 }
911 }
912 if all_numeric {
913 if let Some(dp) = dot_pos {
914 if dp > start && dp < len - 1 {
915 if let Ok(f) = val.parse::<f64>() {
916 return Value::Float(f);
917 }
918 }
919 } else if let Ok(n) = val.parse::<i64>() {
920 return Value::Int(n);
921 }
922 }
923 }
924
925 Value::String(val.to_string())
926 }
927 }
928}
929
930pub(crate) fn cast_typed(val: &str, hint: &str) -> Value {
932 match hint {
933 "int" => Value::Int(val.parse().unwrap_or(0)),
934 "float" => Value::Float(val.parse().unwrap_or(0.0)),
935 "bool" => Value::Bool(val.trim() == "true"),
936 "string" => Value::String(val.to_string()),
937 "random" | "random:int" => Value::Int(rng::random_i64()),
938 "random:float" => Value::Float(rng::random_f64_01()),
939 "random:bool" => Value::Bool(rng::random_bool()),
940 _ => cast(val),
941 }
942}
943
944fn strip_comment(val: &str) -> String {
945 let mut result = val.to_string();
946 if let Some(idx) = result.find(" //") {
947 result.truncate(idx);
948 }
949 if let Some(idx) = result.find(" #") {
950 result.truncate(idx);
951 }
952 result.trim_end().to_string()
953}
954
955fn build_path(stack: &[(i32, StackEntry)]) -> String {
958 let mut parts = Vec::new();
962 for (_, entry) in stack.iter().skip(1) {
963 if let StackEntry::Key(ref k) = entry {
964 parts.push(k.as_str());
965 }
966 }
967 parts.join(".")
968}
969
970fn insert_value(
971 root: &mut HashMap<String, Value>,
972 stack: &[(i32, StackEntry)],
973 parent_idx: usize,
974 key: &str,
975 value: Value,
976) {
977 if let Some(target) = navigate_to_parent(root, stack, parent_idx) {
978 target.insert(key.to_string(), value);
979 }
980 }
984
985fn navigate_to_parent<'a>(
986 root: &'a mut HashMap<String, Value>,
987 stack: &[(i32, StackEntry)],
988 target_idx: usize,
989) -> Option<&'a mut HashMap<String, Value>> {
990 if target_idx == 0 {
991 return Some(root);
992 }
993
994 let mut current = root as *mut HashMap<String, Value>;
1005 for (_indent, entry) in stack.iter().skip(1).take(target_idx) {
1006 match entry {
1007 StackEntry::Root => unreachable!("Root only appears at index 0"),
1008 StackEntry::Key(k) => {
1009 let child = unsafe { (*current).get_mut(k) };
1010 match child {
1011 Some(Value::Object(map)) => current = map as *mut HashMap<String, Value>,
1012 _ => return None, }
1014 }
1015 StackEntry::ListItem { list_key, item_idx } => {
1016 let arr_val = unsafe { (*current).get_mut(list_key) };
1017 match arr_val {
1018 Some(Value::Array(arr)) => {
1019 if *item_idx >= arr.len() { return None; }
1020 match &mut arr[*item_idx] {
1021 Value::Object(map) => current = map as *mut HashMap<String, Value>,
1022 _ => return None,
1023 }
1024 }
1025 _ => return None,
1026 }
1027 }
1028 }
1029 }
1030 Some(unsafe { &mut *current })
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036
1037 #[test]
1038 fn test_simple_key_value() {
1039 let data = parse("name Wario\nage 30\nactive true\nscore 99.5\nempty null");
1040 let root = data.root.as_object().unwrap();
1041 assert_eq!(root["name"], Value::String("Wario".into()));
1042 assert_eq!(root["age"], Value::Int(30));
1043 assert_eq!(root["active"], Value::Bool(true));
1044 assert_eq!(root["score"], Value::Float(99.5));
1045 assert_eq!(root["empty"], Value::Null);
1046 assert_eq!(data.mode, Mode::Static);
1047 }
1048
1049 #[test]
1050 fn test_nested_objects() {
1051 let data = parse("server\n host 0.0.0.0\n port 8080\n ssl\n enabled true");
1052 let root = data.root.as_object().unwrap();
1053 let server = root["server"].as_object().unwrap();
1054 assert_eq!(server["host"], Value::String("0.0.0.0".into()));
1055 assert_eq!(server["port"], Value::Int(8080));
1056 let ssl = server["ssl"].as_object().unwrap();
1057 assert_eq!(ssl["enabled"], Value::Bool(true));
1058 }
1059
1060 #[test]
1061 fn test_lists() {
1062 let data = parse("inventory\n - Sword\n - Shield\n - Potion");
1063 let root = data.root.as_object().unwrap();
1064 let inv = root["inventory"].as_array().unwrap();
1065 assert_eq!(inv.len(), 3);
1066 assert_eq!(inv[0], Value::String("Sword".into()));
1067 }
1068
1069 #[test]
1070 fn test_multiline_block() {
1071 let data = parse("rules |\n Rule one.\n Rule two.\n Rule three.");
1072 let root = data.root.as_object().unwrap();
1073 assert_eq!(
1074 root["rules"],
1075 Value::String("Rule one.\nRule two.\nRule three.".into())
1076 );
1077 }
1078
1079 #[test]
1083 fn test_multiline_block_preserve_indent() {
1084 let src = "prompt |+\n Top\n Indented two\n Indented four\n Back to two\n Top again";
1085 let data = parse(src);
1086 let root = data.root.as_object().unwrap();
1087 assert_eq!(
1088 root["prompt"],
1089 Value::String(
1090 "Top\n Indented two\n Indented four\n Back to two\nTop again".into()
1091 )
1092 );
1093 }
1094
1095 #[test]
1096 fn test_multiline_block_preserve_indent_locks_base() {
1097 let src = "code |+\n function foo() {\n return 1;\n }";
1100 let data = parse(src);
1101 let root = data.root.as_object().unwrap();
1102 assert_eq!(
1103 root["code"],
1104 Value::String("function foo() {\n return 1;\n}".into())
1105 );
1106 }
1107
1108 #[test]
1109 fn test_multiline_block_preserve_indent_ends_at_opener_indent() {
1110 let src = "intro |+\n hello\n world\nnext plain";
1111 let data = parse(src);
1112 let root = data.root.as_object().unwrap();
1113 assert_eq!(root["intro"], Value::String("hello\n world".into()));
1114 assert_eq!(root["next"], Value::String("plain".into()));
1115 }
1116
1117 #[test]
1118 fn test_comments() {
1119 let data = parse("# comment\nname Wario # inline\nage 30 // inline");
1120 let root = data.root.as_object().unwrap();
1121 assert_eq!(root["name"], Value::String("Wario".into()));
1122 assert_eq!(root["age"], Value::Int(30));
1123 }
1124
1125 #[test]
1126 fn test_active_mode() {
1127 let data = parse("!active\nprice 100\ntax:calc price * 0.2");
1128 assert_eq!(data.mode, Mode::Active);
1129 let root = data.root.as_object().unwrap();
1130 assert_eq!(root["price"], Value::Int(100));
1131 assert_eq!(root["tax"], Value::String("price * 0.2".into()));
1133 let meta = data.metadata.get("").unwrap();
1135 assert!(meta.contains_key("tax"));
1136 assert_eq!(meta["tax"].markers, vec!["calc"]);
1137 }
1138
1139 #[test]
1140 fn test_markers_env_default() {
1141 let data = parse("!active\nport:env:default:3000 PORT");
1142 let meta = data.metadata.get("").unwrap();
1143 assert_eq!(meta["port"].markers, vec!["env", "default", "3000"]);
1144 }
1145
1146 #[test]
1147 fn test_type_hint() {
1148 let data = parse("zip(string) 90210");
1149 let root = data.root.as_object().unwrap();
1150 assert_eq!(root["zip"], Value::String("90210".into()));
1151 }
1152
1153 #[test]
1154 fn test_constraints() {
1155 let data = parse("!active\nname[min:3, max:30, required] Wario");
1156 let meta = data.metadata.get("").unwrap();
1157 let c = meta["name"].constraints.as_ref().unwrap();
1158 assert_eq!(c.min, Some(3.0));
1159 assert_eq!(c.max, Some(30.0));
1160 assert!(c.required);
1161 }
1162
1163 #[test]
1164 fn test_random_weights() {
1165 let data = parse("!active\ntier:random 90 5 5");
1166 let meta = data.metadata.get("").unwrap();
1167 assert_eq!(meta["tier"].markers, vec!["random"]);
1168 assert_eq!(meta["tier"].args, vec!["90", "5", "5"]);
1169 }
1170
1171 #[test]
1172 fn test_tool_directive_flags() {
1173 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1174 assert!(data.tool);
1175 assert!(!data.schema);
1176 assert_eq!(data.mode, Mode::Static);
1177 let root = data.root.as_object().unwrap();
1179 let ws = root["web_search"].as_object().unwrap();
1180 assert_eq!(ws["query"], Value::String("test".into()));
1181 assert_eq!(ws["lang"], Value::String("ru".into()));
1182 }
1183
1184 #[test]
1185 fn test_tool_schema_flags() {
1186 let data = parse("!tool\n!schema\nweb_search\n query string\n");
1187 assert!(data.tool);
1188 assert!(data.schema);
1189 }
1190
1191 #[test]
1192 fn test_llm_directive() {
1193 let data = parse("!llm\ncontext\n user_profile demo\ntask summarize\n");
1194 assert!(data.llm);
1195 assert!(!data.tool);
1196 let root = data.root.as_object().unwrap();
1197 assert_eq!(root["task"], Value::String("summarize".into()));
1198 let ctx = root["context"].as_object().unwrap();
1199 assert_eq!(ctx["user_profile"], Value::String("demo".into()));
1200 }
1201
1202 #[test]
1203 fn test_parse_caps_nesting_depth() {
1204 let mut s = String::new();
1207 for i in 0..(MAX_PARSE_NESTING_DEPTH as usize + 64) {
1208 s.push_str(&" ".repeat(i));
1209 s.push_str(&format!("k{i}\n"));
1210 }
1211
1212 let data = parse(&s);
1213 let mut cur = data.root.as_object().unwrap();
1214 let mut depth = 0usize;
1215 loop {
1217 if cur.len() != 1 {
1218 break;
1219 }
1220 let (_, v) = cur.iter().next().unwrap();
1221 match v {
1222 Value::Object(next) => {
1223 depth += 1;
1224 cur = next;
1225 }
1226 _ => break,
1227 }
1228 }
1229
1230 assert!(depth <= MAX_PARSE_NESTING_DEPTH);
1231 }
1232
1233 #[test]
1234 fn test_tool_call_reshape() {
1235 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1236 let shaped = reshape_tool_output(&data.root, false);
1237 let m = shaped.as_object().unwrap();
1238 assert_eq!(m["tool"], Value::String("web_search".into()));
1239 let params = m["params"].as_object().unwrap();
1240 assert_eq!(params["query"], Value::String("test".into()));
1241 assert_eq!(params["lang"], Value::String("ru".into()));
1242 }
1243
1244 #[test]
1245 fn test_tool_schema_reshape() {
1246 let data = parse("!tool\n!schema\nweb_search\n query string\n lang string\nmemory_write\n path string\n value string\n");
1247 let shaped = reshape_tool_output(&data.root, true);
1248 let m = shaped.as_object().unwrap();
1249 let tools = m["tools"].as_array().unwrap();
1250 assert_eq!(tools.len(), 2);
1251 let t0 = tools[0].as_object().unwrap();
1253 assert_eq!(t0["name"], Value::String("memory_write".into()));
1254 let p0 = t0["params"].as_object().unwrap();
1255 assert_eq!(p0["path"], Value::String("string".into()));
1256 let t1 = tools[1].as_object().unwrap();
1257 assert_eq!(t1["name"], Value::String("web_search".into()));
1258 }
1259
1260 #[test]
1261 fn test_tool_empty() {
1262 let data = parse("!tool\n");
1263 assert!(data.tool);
1264 let shaped = reshape_tool_output(&data.root, false);
1265 let m = shaped.as_object().unwrap();
1266 assert_eq!(m["tool"], Value::Null);
1267 }
1268
1269 #[test]
1272 fn test_directives_disabled_discards_directive_lines() {
1273 let src = "!active\n!include /etc/passwd\n!use @scope/pkg\nname Wario\n";
1274 let data = parse_with(src, ParserOptions { directives: false });
1275 assert_eq!(data.mode, Mode::Static);
1276 assert!(data.includes.is_empty());
1277 assert!(data.uses.is_empty());
1278 let root = data.root.as_object().unwrap();
1279 assert_eq!(root.len(), 1);
1281 assert_eq!(root["name"], Value::String("Wario".into()));
1282
1283 let data = parse(src);
1285 assert_eq!(data.mode, Mode::Active);
1286 assert_eq!(data.includes.len(), 1);
1287 }
1288
1289 #[test]
1290 fn test_directives_disabled_preserves_bang_lines_in_multiline() {
1291 let src = "body |+\n !include /etc/passwd\n !active\n tail\n";
1294 let data = parse_with(src, ParserOptions { directives: false });
1295 assert_eq!(
1296 data.root.as_object().unwrap()["body"],
1297 Value::String("!include /etc/passwd\n!active\ntail".into())
1298 );
1299 assert!(data.includes.is_empty());
1300 assert_eq!(data.mode, Mode::Static);
1301
1302 let src = "body |\n !include /etc/passwd\n";
1303 let data = parse_with(src, ParserOptions { directives: false });
1304 assert_eq!(
1305 data.root.as_object().unwrap()["body"],
1306 Value::String("!include /etc/passwd".into())
1307 );
1308 }
1309
1310 #[test]
1311 fn test_tool_with_active() {
1312 let data = parse("!tool\n!active\nweb_search\n port:env:default:8080 PORT\n");
1313 assert!(data.tool);
1314 assert_eq!(data.mode, Mode::Active);
1315 let meta = data.metadata.get("web_search").unwrap();
1317 assert_eq!(meta["port"].markers, vec!["env", "default", "8080"]);
1318 }
1319}