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
18pub(crate) const 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 strip_boundary(s: &str, strip: usize) -> usize {
56 let mut off = 0usize;
57 for ch in s.chars() {
58 if off >= strip || !ch.is_whitespace() {
59 break;
60 }
61 off += ch.len_utf8();
62 }
63 off
64}
65
66fn is_reserved_js_key(key: &str) -> bool {
68 key == "__proto__" || key == "constructor" || key == "prototype"
69}
70
71fn utf8_prefix_len(s: &str, limit: usize) -> usize {
73 if s.len() <= limit {
74 return s.len();
75 }
76 let mut end = limit;
77 while end > 0 && !s.is_char_boundary(end) {
78 end -= 1;
79 }
80 end
81}
82
83fn find_parse_end_bytes(bytes: &[u8]) -> usize {
86 let max_newlines = MAX_LINE_STARTS.saturating_sub(1);
87 let mut seen_newlines = 0usize;
88 let mut scan = 0usize;
89 while scan < bytes.len() {
90 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
91 if seen_newlines >= max_newlines {
92 return scan + rel;
93 }
94 seen_newlines += 1;
95 scan += rel + 1;
96 } else {
97 break;
98 }
99 }
100 bytes.len()
101}
102
103#[derive(Debug, Clone, Copy)]
109pub struct ParserOptions {
110 pub directives: bool,
122
123 pub nondeterministic_hints: bool,
132}
133
134impl Default for ParserOptions {
135 fn default() -> Self {
136 Self { directives: true, nondeterministic_hints: true }
137 }
138}
139
140pub fn parse(text: &str) -> ParseResult {
142 parse_with(text, ParserOptions::default())
143}
144
145pub fn parse_with(text: &str, opts: ParserOptions) -> ParseResult {
149 let original_len = text.len();
150 let text = clamp_synx_text(text);
151 let parse_end = find_parse_end_bytes(text.as_bytes());
152 let text = &text[..parse_end];
153 let bytes = text.as_bytes();
154 let mut truncated_multiline = text.len() < original_len;
156
157 let mut line_starts: Vec<usize> = Vec::new();
158 line_starts.push(0);
159 let mut scan = 0usize;
160 while scan < bytes.len() {
161 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
162 let pos = scan + rel;
163 line_starts.push(pos + 1);
164 scan = pos + 1;
165 } else {
166 break;
167 }
168 }
169 let line_count = line_starts.len();
170
171 let mut root = HashMap::new();
172 let mut stack: Vec<(i32, StackEntry)> = vec![(-1, StackEntry::Root)];
173 let mut mode = Mode::Static;
174 let mut locked = false;
175 let mut tool = false;
176 let mut schema = false;
177 let mut llm = false;
178 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
179 let mut includes: Vec<IncludeDirective> = Vec::new();
180 let mut uses: Vec<UseDirective> = Vec::new();
181
182 let mut block: Option<BlockState> = None;
183 let mut list: Option<ListState> = None;
184 let mut in_block_comment = false;
185
186 let mut i = 0;
187 while i < line_count {
188 let start = line_starts[i];
190 let end = if i + 1 < line_count { line_starts[i + 1] - 1 } else { bytes.len() };
191 let end = if end > start && end > 0 && bytes.get(end - 1) == Some(&b'\r') { end - 1 } else { end };
193 let raw = &text[start..end];
194
195 let trimmed = raw.trim();
196
197 if trimmed.is_empty() {
200 i += 1;
201 continue;
202 }
203
204 let indent = (raw.len() - raw.trim_start().len()) as i32;
205
206 if let Some(ref mut blk) = block {
215 if indent > blk.indent {
216 if blk.content.len() < MAX_MULTILINE_BLOCK_BYTES {
217 if !blk.content.is_empty() {
218 blk.content.push('\n');
219 }
220 let room = MAX_MULTILINE_BLOCK_BYTES.saturating_sub(blk.content.len());
221 if room > 0 {
222 let slice: &str = if blk.preserve_indent {
223 if blk.base_indent < 0 {
229 blk.base_indent = indent;
230 }
231 let strip = blk.base_indent.min(indent) as usize;
232 let raw_trim_end = raw.trim_end();
233 let cut = strip_boundary(raw_trim_end, strip);
234 if cut < raw_trim_end.len() {
235 &raw_trim_end[cut..]
236 } else {
237 ""
238 }
239 } else {
240 trimmed
241 };
242 let n = utf8_prefix_len(slice, room);
243 blk.content.push_str(&slice[..n]);
244 if n < slice.len() {
245 truncated_multiline = true;
246 }
247 }
248 } else {
249 truncated_multiline = true;
250 }
251 i += 1;
252 continue;
253 } else {
254 let content = std::mem::take(&mut blk.content);
255 let blk_key = blk.key.clone();
256 let blk_stack_idx = blk.stack_idx;
257 block = None;
258 insert_value(&mut root, &stack, blk_stack_idx, &blk_key, Value::String(content));
259 }
260 }
261
262 if trimmed == "###" {
264 in_block_comment = !in_block_comment;
265 i += 1;
266 continue;
267 }
268 if in_block_comment {
269 i += 1;
270 continue;
271 }
272
273 if opts.directives && (trimmed.starts_with('!') || trimmed.starts_with("#!mode:")) {
279 if trimmed == "!active" {
281 mode = Mode::Active;
282 i += 1;
283 continue;
284 }
285 if trimmed == "!lock" {
286 locked = true;
287 i += 1;
288 continue;
289 }
290 if trimmed == "!tool" {
291 tool = true;
292 i += 1;
293 continue;
294 }
295 if trimmed == "!schema" {
296 schema = true;
297 i += 1;
298 continue;
299 }
300 if trimmed == "!llm" {
301 llm = true;
302 i += 1;
303 continue;
304 }
305 if trimmed.starts_with("!include ") {
306 if includes.len() < MAX_INCLUDE_DIRECTIVES {
307 let rest = trimmed[9..].trim();
308 let mut parts = rest.splitn(2, char::is_whitespace);
309 let path = parts.next().unwrap_or("").to_string();
310 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
311 let name = path.rsplit(&['/', '\\'][..]).next().unwrap_or(&path);
313 name.strip_suffix(".synx").or_else(|| name.strip_suffix(".SYNX")).unwrap_or(name).to_string()
314 });
315 includes.push(IncludeDirective { path, alias });
316 }
317 i += 1;
318 continue;
319 }
320 if trimmed.starts_with("!use ") {
321 let rest = trimmed[5..].trim();
322 if rest.starts_with('@') {
323 let mut parts = rest.splitn(2, " as ");
325 let package = parts.next().unwrap_or("").trim().to_string();
326 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
327 package.rsplit('/').next().unwrap_or(&package).to_string()
329 });
330 if !package.is_empty() {
331 uses.push(UseDirective { package, alias });
332 }
333 }
334 i += 1;
335 continue;
336 }
337 if trimmed.starts_with("#!mode:") {
338 let declared = trimmed.splitn(2, ':').nth(1).unwrap_or("static").trim();
339 mode = if declared == "active" { Mode::Active } else { Mode::Static };
340 i += 1;
341 continue;
342 }
343 }
344
345 if !opts.directives && trimmed.starts_with('!') {
350 i += 1;
351 continue;
352 }
353
354 if trimmed.starts_with('#') || trimmed.starts_with("//") {
357 i += 1;
358 continue;
359 }
360
361 if trimmed.starts_with("- ") {
363 if let Some(ref lst) = list {
364 if indent > lst.indent {
365 while stack.len() > 1 {
368 match stack.last() {
369 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
370 _ => break,
371 }
372 }
373
374 let val_str = strip_comment(trimmed[2..].trim());
375
376 let mut peek = i + 1;
380 let mut nested = false;
381 while peek < line_count {
382 let ps = line_starts[peek];
383 let pe = if peek + 1 < line_count { line_starts[peek + 1] - 1 } else { bytes.len() };
384 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
385 let pl = &text[ps..pe];
386 let pt = pl.trim();
387 if pt.is_empty() {
388 peek += 1;
389 continue;
390 }
391 let pi = (pl.len() - pl.trim_start().len()) as i32;
392 if pi > indent
393 && !pt.starts_with("- ")
394 && !pt.starts_with('#')
395 && !pt.starts_with("//")
396 {
397 nested = true;
398 }
399 break;
400 }
401
402 let list_key = lst.key.clone();
403 let list_stack_idx = lst.stack_idx;
404
405 if let Some(parent_map) = navigate_to_parent(&mut root, &stack, list_stack_idx) {
407 let arr_entry = parent_map
408 .entry(list_key.clone())
409 .or_insert_with(|| Value::Array(Vec::new()));
410 if let Value::Array(arr) = arr_entry {
411 if arr.len() >= MAX_LIST_ITEMS {
412 i += 1;
413 continue;
414 }
415 if nested {
416 let mut item_obj: HashMap<String, Value> = HashMap::new();
417 if let Some(parsed) = parse_line(&val_str) {
418 let val = if let Some(ref hint) = parsed.type_hint {
419 cast_typed_with(&parsed.value, hint, opts.nondeterministic_hints)
420 } else if !parsed.value.is_empty() {
421 cast(&parsed.value)
422 } else {
423 Value::Object(HashMap::new())
424 };
425 if !is_reserved_js_key(&parsed.key) {
429 item_obj.insert(parsed.key, val);
430 }
431 } else {
432 item_obj.insert("_value".to_string(), cast(&val_str));
433 }
434 let item_idx = arr.len();
435 arr.push(Value::Object(item_obj));
436 if stack.len() < MAX_PARSE_NESTING_DEPTH {
437 stack.push((indent, StackEntry::ListItem { list_key, item_idx }));
438 }
439 } else {
440 arr.push(cast(&val_str));
441 }
442 }
443 }
444
445 i += 1;
446 continue;
447 }
448 }
449 } else {
450 let close = list.as_ref().map(|lst| indent <= lst.indent).unwrap_or(false);
452 if close {
453 list = None;
454 while stack.len() > 1 {
456 match stack.last() {
457 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
458 _ => break,
459 }
460 }
461 }
462 }
463
464 if let Some(parsed) = parse_line(trimmed) {
466 if is_reserved_js_key(&parsed.key) {
470 i += 1;
471 continue;
472 }
473
474 while stack.len() > 1 && stack.last().unwrap().0 >= indent {
476 stack.pop();
477 }
478
479 let parent_idx = stack.len() - 1;
480
481 if mode == Mode::Active
483 && (!parsed.markers.is_empty()
484 || parsed.constraints.is_some()
485 || parsed.type_hint.is_some())
486 {
487 let path = build_path(&stack);
488 let meta_map = metadata.entry(path).or_default();
489 meta_map.insert(
490 parsed.key.clone(),
491 Meta {
492 markers: parsed.markers.clone(),
493 args: parsed.marker_args.clone(),
494 type_hint: parsed.type_hint.clone(),
495 constraints: parsed.constraints.clone(),
496 },
497 );
498 }
499
500 let is_block = parsed.value == "|" || parsed.value == "|+";
506 let preserve_indent = parsed.value == "|+";
507 let is_list_marker = parsed.markers.iter().any(|m| {
508 matches!(m.as_str(), "random" | "unique" | "geo" | "join")
509 });
510
511 if is_block {
512 insert_value(
513 &mut root,
514 &stack,
515 parent_idx,
516 &parsed.key,
517 Value::String(String::new()),
518 );
519 block = Some(BlockState {
520 indent,
521 key: parsed.key,
522 content: String::new(),
523 stack_idx: parent_idx,
524 preserve_indent,
525 base_indent: -1,
526 });
527 } else if is_list_marker && parsed.value.is_empty() {
528 insert_value(
531 &mut root,
532 &stack,
533 parent_idx,
534 &parsed.key,
535 Value::Array(Vec::new()),
536 );
537 list = Some(ListState {
538 indent,
539 key: parsed.key,
540 stack_idx: parent_idx,
541 });
542 } else if parsed.value.is_empty() {
543 let mut peek = i + 1;
545 while peek < line_count {
546 let ps = line_starts[peek];
547 let pe = if peek + 1 < line_count {
548 line_starts[peek + 1] - 1
549 } else {
550 bytes.len()
551 };
552 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
553 let pt = text[ps..pe].trim();
554 if !pt.is_empty() {
555 break;
556 }
557 peek += 1;
558 }
559
560 if peek < line_count {
561 let ps = line_starts[peek];
562 let pe = if peek + 1 < line_count {
563 line_starts[peek + 1] - 1
564 } else {
565 bytes.len()
566 };
567 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
568 let pt = text[ps..pe].trim();
569 if pt.starts_with("- ") {
570 insert_value(
571 &mut root,
572 &stack,
573 parent_idx,
574 &parsed.key,
575 Value::Array(Vec::new()),
576 );
577 list = Some(ListState {
578 indent,
579 key: parsed.key,
580 stack_idx: parent_idx,
581 });
582 i += 1;
583 continue;
584 }
585 }
586
587 insert_value(
588 &mut root,
589 &stack,
590 parent_idx,
591 &parsed.key,
592 Value::Object(HashMap::new()),
593 );
594 if stack.len() < MAX_PARSE_NESTING_DEPTH {
598 stack.push((indent, StackEntry::Key(parsed.key)));
599 }
600 } else {
601 let value = if let Some(ref hint) = parsed.type_hint {
602 cast_typed_with(&parsed.value, hint, opts.nondeterministic_hints)
603 } else {
604 cast(&parsed.value)
605 };
606 insert_value(&mut root, &stack, parent_idx, &parsed.key, value);
607 }
608 }
609
610 i += 1;
611 }
612
613 if let Some(blk) = block {
615 insert_value(
616 &mut root,
617 &stack,
618 blk.stack_idx,
619 &blk.key,
620 Value::String(blk.content),
621 );
622 }
623
624 let _ = list;
627
628 let parsed_root = Value::Object(root);
629
630 ParseResult {
634 root: parsed_root,
635 mode,
636 locked,
637 tool,
638 schema,
639 llm,
640 metadata,
641 includes,
642 uses,
643 truncated: truncated_multiline,
644 }
645}
646
647pub fn reshape_tool_output(root: &Value, schema: bool) -> Value {
659 let map = match root {
660 Value::Object(m) => m,
661 _ => return root.clone(),
662 };
663
664 if schema {
665 let mut tools = Vec::new();
667 let mut keys: Vec<&String> = map.keys().collect();
669 keys.sort();
670 for key in keys {
671 let val = &map[key];
672 let mut def = HashMap::new();
673 def.insert("name".to_string(), Value::String(key.clone()));
674 def.insert("params".to_string(), val.clone());
675 tools.push(Value::Object(def));
676 }
677 let mut out = HashMap::new();
678 out.insert("tools".to_string(), Value::Array(tools));
679 Value::Object(out)
680 } else {
681 if map.is_empty() {
683 let mut out = HashMap::new();
684 out.insert("tool".to_string(), Value::Null);
685 out.insert("params".to_string(), Value::Object(HashMap::new()));
686 return Value::Object(out);
687 }
688
689 let mut keys: Vec<&String> = map.keys().collect();
692 keys.sort();
693 let tool_key = keys[0];
694 let tool_value = &map[tool_key];
695
696 let params = match tool_value {
697 Value::Object(m) => Value::Object(m.clone()),
698 _ => Value::Object(HashMap::new()),
700 };
701
702 let mut out = HashMap::new();
703 out.insert("tool".to_string(), Value::String(tool_key.clone()));
704 out.insert("params".to_string(), params);
705 Value::Object(out)
706 }
707}
708
709#[derive(Debug)]
712enum StackEntry {
713 Root,
714 Key(String),
715 ListItem { list_key: String, item_idx: usize },
720}
721
722struct BlockState {
723 indent: i32,
724 key: String,
725 content: String,
726 stack_idx: usize,
727 preserve_indent: bool,
730 base_indent: i32,
733}
734
735struct ListState {
736 indent: i32,
737 key: String,
738 stack_idx: usize,
739}
740
741struct ParsedLine {
742 key: String,
743 type_hint: Option<String>,
744 value: String,
745 markers: Vec<String>,
746 marker_args: Vec<String>,
747 constraints: Option<Constraints>,
748}
749
750fn parse_line(trimmed: &str) -> Option<ParsedLine> {
753 if trimmed.is_empty()
754 || trimmed.starts_with('#')
755 || trimmed.starts_with("//")
756 || trimmed.starts_with("- ")
757 {
758 return None;
759 }
760
761 let bytes = trimmed.as_bytes();
762 let len = bytes.len();
763
764 let first = bytes[0];
765 if first == b'[' || first == b':' || first == b'-' || first == b'#' || first == b'/' || first == b'(' {
766 return None;
767 }
768
769 let mut pos = 0;
771 while pos < len {
772 let ch = bytes[pos];
773 if ch == b' ' || ch == b'\t' || ch == b'[' || ch == b':' || ch == b'(' {
774 break;
775 }
776 pos += 1;
777 }
778 let key = trimmed[..pos].to_string();
779
780 let mut type_hint = None;
782 if pos < len && bytes[pos] == b'(' {
783 let start = pos + 1;
784 if let Some(c) = trimmed[start..].find(')') {
785 type_hint = Some(trimmed[start..start + c].to_string());
786 pos = start + c + 1;
787 } else {
788 pos += 1;
789 }
790 }
791
792 let mut constraints = None;
795 if pos < len && bytes[pos] == b'[' {
796 let cstart = pos + 1;
797 let mut depth = 1usize;
798 let mut scan = cstart;
799 while scan < len && depth > 0 {
800 match bytes[scan] {
801 b'[' => depth += 1,
802 b']' => {
803 depth -= 1;
804 if depth == 0 {
805 break;
806 }
807 }
808 _ => {}
809 }
810 scan += 1;
811 }
812 if depth == 0 {
813 let constraint_str = &trimmed[cstart..scan];
814 constraints = Some(parse_constraints(constraint_str));
815 pos = scan + 1; } else {
817 if let Some(rel) = trimmed[cstart..].find(']') {
819 let constraint_str = &trimmed[cstart..cstart + rel];
820 constraints = Some(parse_constraints(constraint_str));
821 pos = cstart + rel + 1;
822 } else {
823 constraints = Some(parse_constraints(&trimmed[cstart..]));
824 pos = len;
825 }
826 }
827 }
828
829 let mut markers = Vec::new();
831 let mut marker_args = Vec::new();
832 if pos < len && bytes[pos] == b':' {
833 let marker_start = pos + 1;
834 let mut marker_end = marker_start;
835 while marker_end < len && bytes[marker_end] != b' ' && bytes[marker_end] != b'\t' {
836 marker_end += 1;
837 }
838 let chain = &trimmed[marker_start..marker_end];
839 markers = chain
840 .split(':')
841 .take(MAX_MARKER_CHAIN_SEGMENTS)
842 .map(|s| s.to_string())
843 .collect();
844 pos = marker_end;
845 }
846
847 while pos < len && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
849 pos += 1;
850 }
851
852 let mut raw_value = if pos < len {
854 strip_comment(&trimmed[pos..])
855 } else {
856 String::new()
857 };
858
859 if markers.contains(&"random".to_string()) && !raw_value.is_empty() {
861 let parts: Vec<&str> = raw_value.split_whitespace().collect();
862 let nums: Vec<String> = parts
863 .iter()
864 .filter(|s| s.parse::<f64>().is_ok())
865 .map(|s| s.to_string())
866 .collect();
867 if !nums.is_empty() {
868 marker_args = nums;
869 raw_value.clear();
870 }
871 }
872
873 if markers.contains(&"inherit".to_string()) && !raw_value.is_empty() {
876 marker_args = vec![raw_value.trim().to_string()];
877 raw_value.clear();
878 }
879
880 Some(ParsedLine {
881 key,
882 type_hint,
883 value: raw_value,
884 markers,
885 marker_args,
886 constraints,
887 })
888}
889
890pub(crate) fn parse_constraints(raw: &str) -> Constraints {
898 let mut c = Constraints::default();
899 for part in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
900 if part == "required" {
901 c.required = true;
902 } else if part == "readonly" {
903 c.readonly = true;
904 } else if let Some(colon) = part.find(':') {
905 let key = part[..colon].trim();
906 let val = part[colon + 1..].trim();
907 match key {
908 "min" => c.min = val.parse().ok(),
909 "max" => c.max = val.parse().ok(),
910 "type" => c.type_name = Some(val.to_string()),
911 "pattern" => c.pattern = Some(val.to_string()),
912 "enum" => {
913 c.enum_values = Some(
914 val.split('|')
915 .take(MAX_CONSTRAINT_ENUM_PARTS)
916 .map(|s| s.to_string())
917 .collect(),
918 );
919 }
920 _ => {}
921 }
922 }
923 }
924 c
925}
926
927pub(crate) fn cast(val: &str) -> Value {
931 if val.len() >= 2 {
934 let bytes = val.as_bytes();
935 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
936 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
937 {
938 return Value::String(val[1..val.len() - 1].to_string());
939 }
940 }
941
942 match val {
943 "true" => Value::Bool(true),
944 "false" => Value::Bool(false),
945 "null" => Value::Null,
946 _ => {
947 let bytes = val.as_bytes();
948 let len = bytes.len();
949 if len == 0 {
950 return Value::String(String::new());
951 }
952
953 let mut start = 0;
954 if bytes[0] == b'-' {
955 if len == 1 {
956 return Value::String(val.to_string());
957 }
958 start = 1;
959 }
960
961 if bytes[start] >= b'0' && bytes[start] <= b'9' {
962 let mut dot_pos = None;
963 let mut all_numeric = true;
964 for j in start..len {
965 if bytes[j] == b'.' {
966 if dot_pos.is_some() {
967 all_numeric = false;
968 break;
969 }
970 dot_pos = Some(j);
971 } else if bytes[j] < b'0' || bytes[j] > b'9' {
972 all_numeric = false;
973 break;
974 }
975 }
976 if all_numeric {
977 if let Some(dp) = dot_pos {
978 if dp > start && dp < len - 1 {
979 if let Ok(f) = val.parse::<f64>() {
980 return Value::Float(f);
981 }
982 }
983 } else if let Ok(n) = val.parse::<i64>() {
984 return Value::Int(n);
985 }
986 }
987 }
988
989 Value::String(val.to_string())
990 }
991 }
992}
993
994pub(crate) fn cast_typed_with(val: &str, hint: &str, nondeterministic: bool) -> Value {
999 match hint {
1000 "int" => Value::Int(val.parse().unwrap_or(0)),
1001 "float" => Value::Float(val.parse().unwrap_or(0.0)),
1002 "bool" => Value::Bool(val.trim() == "true"),
1003 "string" => Value::String(val.to_string()),
1004 "random" | "random:int" if nondeterministic => Value::Int(rng::random_i64()),
1005 "random:float" if nondeterministic => Value::Float(rng::random_f64_01()),
1006 "random:bool" if nondeterministic => Value::Bool(rng::random_bool()),
1007 _ => cast(val),
1008 }
1009}
1010
1011fn strip_comment(val: &str) -> String {
1012 let mut result = val.to_string();
1013 if let Some(idx) = result.find(" //") {
1014 result.truncate(idx);
1015 }
1016 if let Some(idx) = result.find(" #") {
1017 result.truncate(idx);
1018 }
1019 result.trim_end().to_string()
1020}
1021
1022fn build_path(stack: &[(i32, StackEntry)]) -> String {
1025 let mut parts = Vec::new();
1029 for (_, entry) in stack.iter().skip(1) {
1030 if let StackEntry::Key(ref k) = entry {
1031 parts.push(k.as_str());
1032 }
1033 }
1034 parts.join(".")
1035}
1036
1037fn insert_value(
1038 root: &mut HashMap<String, Value>,
1039 stack: &[(i32, StackEntry)],
1040 parent_idx: usize,
1041 key: &str,
1042 value: Value,
1043) {
1044 if let Some(target) = navigate_to_parent(root, stack, parent_idx) {
1045 target.insert(key.to_string(), value);
1046 }
1047 }
1051
1052fn navigate_to_parent<'a>(
1053 root: &'a mut HashMap<String, Value>,
1054 stack: &[(i32, StackEntry)],
1055 target_idx: usize,
1056) -> Option<&'a mut HashMap<String, Value>> {
1057 if target_idx == 0 {
1058 return Some(root);
1059 }
1060
1061 let mut current = root as *mut HashMap<String, Value>;
1072 for (_indent, entry) in stack.iter().skip(1).take(target_idx) {
1073 match entry {
1074 StackEntry::Root => unreachable!("Root only appears at index 0"),
1075 StackEntry::Key(k) => {
1076 let child = unsafe { (*current).get_mut(k) };
1077 match child {
1078 Some(Value::Object(map)) => current = map as *mut HashMap<String, Value>,
1079 _ => return None, }
1081 }
1082 StackEntry::ListItem { list_key, item_idx } => {
1083 let arr_val = unsafe { (*current).get_mut(list_key) };
1084 match arr_val {
1085 Some(Value::Array(arr)) => {
1086 if *item_idx >= arr.len() { return None; }
1087 match &mut arr[*item_idx] {
1088 Value::Object(map) => current = map as *mut HashMap<String, Value>,
1089 _ => return None,
1090 }
1091 }
1092 _ => return None,
1093 }
1094 }
1095 }
1096 }
1097 Some(unsafe { &mut *current })
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102 use super::*;
1103
1104 #[test]
1105 fn test_simple_key_value() {
1106 let data = parse("name Wario\nage 30\nactive true\nscore 99.5\nempty null");
1107 let root = data.root.as_object().unwrap();
1108 assert_eq!(root["name"], Value::String("Wario".into()));
1109 assert_eq!(root["age"], Value::Int(30));
1110 assert_eq!(root["active"], Value::Bool(true));
1111 assert_eq!(root["score"], Value::Float(99.5));
1112 assert_eq!(root["empty"], Value::Null);
1113 assert_eq!(data.mode, Mode::Static);
1114 }
1115
1116 #[test]
1117 fn test_nested_objects() {
1118 let data = parse("server\n host 0.0.0.0\n port 8080\n ssl\n enabled true");
1119 let root = data.root.as_object().unwrap();
1120 let server = root["server"].as_object().unwrap();
1121 assert_eq!(server["host"], Value::String("0.0.0.0".into()));
1122 assert_eq!(server["port"], Value::Int(8080));
1123 let ssl = server["ssl"].as_object().unwrap();
1124 assert_eq!(ssl["enabled"], Value::Bool(true));
1125 }
1126
1127 #[test]
1128 fn test_lists() {
1129 let data = parse("inventory\n - Sword\n - Shield\n - Potion");
1130 let root = data.root.as_object().unwrap();
1131 let inv = root["inventory"].as_array().unwrap();
1132 assert_eq!(inv.len(), 3);
1133 assert_eq!(inv[0], Value::String("Sword".into()));
1134 }
1135
1136 #[test]
1137 fn test_multiline_block() {
1138 let data = parse("rules |\n Rule one.\n Rule two.\n Rule three.");
1139 let root = data.root.as_object().unwrap();
1140 assert_eq!(
1141 root["rules"],
1142 Value::String("Rule one.\nRule two.\nRule three.".into())
1143 );
1144 }
1145
1146 #[test]
1150 fn test_multiline_block_preserve_indent() {
1151 let src = "prompt |+\n Top\n Indented two\n Indented four\n Back to two\n Top again";
1152 let data = parse(src);
1153 let root = data.root.as_object().unwrap();
1154 assert_eq!(
1155 root["prompt"],
1156 Value::String(
1157 "Top\n Indented two\n Indented four\n Back to two\nTop again".into()
1158 )
1159 );
1160 }
1161
1162 #[test]
1163 fn test_multiline_block_preserve_indent_locks_base() {
1164 let src = "code |+\n function foo() {\n return 1;\n }";
1167 let data = parse(src);
1168 let root = data.root.as_object().unwrap();
1169 assert_eq!(
1170 root["code"],
1171 Value::String("function foo() {\n return 1;\n}".into())
1172 );
1173 }
1174
1175 #[test]
1176 fn test_multiline_block_preserve_indent_ends_at_opener_indent() {
1177 let src = "intro |+\n hello\n world\nnext plain";
1178 let data = parse(src);
1179 let root = data.root.as_object().unwrap();
1180 assert_eq!(root["intro"], Value::String("hello\n world".into()));
1181 assert_eq!(root["next"], Value::String("plain".into()));
1182 }
1183
1184 #[test]
1185 fn test_comments() {
1186 let data = parse("# comment\nname Wario # inline\nage 30 // inline");
1187 let root = data.root.as_object().unwrap();
1188 assert_eq!(root["name"], Value::String("Wario".into()));
1189 assert_eq!(root["age"], Value::Int(30));
1190 }
1191
1192 #[test]
1193 fn test_active_mode() {
1194 let data = parse("!active\nprice 100\ntax:calc price * 0.2");
1195 assert_eq!(data.mode, Mode::Active);
1196 let root = data.root.as_object().unwrap();
1197 assert_eq!(root["price"], Value::Int(100));
1198 assert_eq!(root["tax"], Value::String("price * 0.2".into()));
1200 let meta = data.metadata.get("").unwrap();
1202 assert!(meta.contains_key("tax"));
1203 assert_eq!(meta["tax"].markers, vec!["calc"]);
1204 }
1205
1206 #[test]
1207 fn test_markers_env_default() {
1208 let data = parse("!active\nport:env:default:3000 PORT");
1209 let meta = data.metadata.get("").unwrap();
1210 assert_eq!(meta["port"].markers, vec!["env", "default", "3000"]);
1211 }
1212
1213 #[test]
1214 fn test_type_hint() {
1215 let data = parse("zip(string) 90210");
1216 let root = data.root.as_object().unwrap();
1217 assert_eq!(root["zip"], Value::String("90210".into()));
1218 }
1219
1220 #[test]
1221 fn test_constraints() {
1222 let data = parse("!active\nname[min:3, max:30, required] Wario");
1223 let meta = data.metadata.get("").unwrap();
1224 let c = meta["name"].constraints.as_ref().unwrap();
1225 assert_eq!(c.min, Some(3.0));
1226 assert_eq!(c.max, Some(30.0));
1227 assert!(c.required);
1228 }
1229
1230 #[test]
1231 fn test_random_weights() {
1232 let data = parse("!active\ntier:random 90 5 5");
1233 let meta = data.metadata.get("").unwrap();
1234 assert_eq!(meta["tier"].markers, vec!["random"]);
1235 assert_eq!(meta["tier"].args, vec!["90", "5", "5"]);
1236 }
1237
1238 #[test]
1239 fn test_tool_directive_flags() {
1240 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1241 assert!(data.tool);
1242 assert!(!data.schema);
1243 assert_eq!(data.mode, Mode::Static);
1244 let root = data.root.as_object().unwrap();
1246 let ws = root["web_search"].as_object().unwrap();
1247 assert_eq!(ws["query"], Value::String("test".into()));
1248 assert_eq!(ws["lang"], Value::String("ru".into()));
1249 }
1250
1251 #[test]
1252 fn test_tool_schema_flags() {
1253 let data = parse("!tool\n!schema\nweb_search\n query string\n");
1254 assert!(data.tool);
1255 assert!(data.schema);
1256 }
1257
1258 #[test]
1259 fn test_llm_directive() {
1260 let data = parse("!llm\ncontext\n user_profile demo\ntask summarize\n");
1261 assert!(data.llm);
1262 assert!(!data.tool);
1263 let root = data.root.as_object().unwrap();
1264 assert_eq!(root["task"], Value::String("summarize".into()));
1265 let ctx = root["context"].as_object().unwrap();
1266 assert_eq!(ctx["user_profile"], Value::String("demo".into()));
1267 }
1268
1269 #[test]
1270 fn test_parse_caps_nesting_depth() {
1271 let mut s = String::new();
1274 for i in 0..(MAX_PARSE_NESTING_DEPTH as usize + 64) {
1275 s.push_str(&" ".repeat(i));
1276 s.push_str(&format!("k{i}\n"));
1277 }
1278
1279 let data = parse(&s);
1280 let mut cur = data.root.as_object().unwrap();
1281 let mut depth = 0usize;
1282 loop {
1284 if cur.len() != 1 {
1285 break;
1286 }
1287 let (_, v) = cur.iter().next().unwrap();
1288 match v {
1289 Value::Object(next) => {
1290 depth += 1;
1291 cur = next;
1292 }
1293 _ => break,
1294 }
1295 }
1296
1297 assert!(depth <= MAX_PARSE_NESTING_DEPTH);
1298 }
1299
1300 #[test]
1301 fn test_tool_call_reshape() {
1302 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1303 let shaped = reshape_tool_output(&data.root, false);
1304 let m = shaped.as_object().unwrap();
1305 assert_eq!(m["tool"], Value::String("web_search".into()));
1306 let params = m["params"].as_object().unwrap();
1307 assert_eq!(params["query"], Value::String("test".into()));
1308 assert_eq!(params["lang"], Value::String("ru".into()));
1309 }
1310
1311 #[test]
1312 fn test_tool_schema_reshape() {
1313 let data = parse("!tool\n!schema\nweb_search\n query string\n lang string\nmemory_write\n path string\n value string\n");
1314 let shaped = reshape_tool_output(&data.root, true);
1315 let m = shaped.as_object().unwrap();
1316 let tools = m["tools"].as_array().unwrap();
1317 assert_eq!(tools.len(), 2);
1318 let t0 = tools[0].as_object().unwrap();
1320 assert_eq!(t0["name"], Value::String("memory_write".into()));
1321 let p0 = t0["params"].as_object().unwrap();
1322 assert_eq!(p0["path"], Value::String("string".into()));
1323 let t1 = tools[1].as_object().unwrap();
1324 assert_eq!(t1["name"], Value::String("web_search".into()));
1325 }
1326
1327 #[test]
1328 fn test_tool_empty() {
1329 let data = parse("!tool\n");
1330 assert!(data.tool);
1331 let shaped = reshape_tool_output(&data.root, false);
1332 let m = shaped.as_object().unwrap();
1333 assert_eq!(m["tool"], Value::Null);
1334 }
1335
1336 #[test]
1339 fn test_directives_disabled_discards_directive_lines() {
1340 let src = "!active\n!include /etc/passwd\n!use @scope/pkg\nname Wario\n";
1341 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1342 assert_eq!(data.mode, Mode::Static);
1343 assert!(data.includes.is_empty());
1344 assert!(data.uses.is_empty());
1345 let root = data.root.as_object().unwrap();
1346 assert_eq!(root.len(), 1);
1348 assert_eq!(root["name"], Value::String("Wario".into()));
1349
1350 let data = parse(src);
1352 assert_eq!(data.mode, Mode::Active);
1353 assert_eq!(data.includes.len(), 1);
1354 }
1355
1356 #[test]
1357 fn test_directives_disabled_preserves_bang_lines_in_multiline() {
1358 let src = "body |+\n !include /etc/passwd\n !active\n tail\n";
1361 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1362 assert_eq!(
1363 data.root.as_object().unwrap()["body"],
1364 Value::String("!include /etc/passwd\n!active\ntail".into())
1365 );
1366 assert!(data.includes.is_empty());
1367 assert_eq!(data.mode, Mode::Static);
1368
1369 let src = "body |\n !include /etc/passwd\n";
1370 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1371 assert_eq!(
1372 data.root.as_object().unwrap()["body"],
1373 Value::String("!include /etc/passwd".into())
1374 );
1375 }
1376
1377 #[test]
1378 fn test_tool_with_active() {
1379 let data = parse("!tool\n!active\nweb_search\n port:env:default:8080 PORT\n");
1380 assert!(data.tool);
1381 assert_eq!(data.mode, Mode::Active);
1382 let meta = data.metadata.get("web_search").unwrap();
1384 assert_eq!(meta["port"].markers, vec!["env", "default", "8080"]);
1385 }
1386}