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 {
45 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
46 if text.len() <= MAX_SYNX_INPUT_BYTES {
47 return text;
48 }
49 let slice = &text.as_bytes()[..MAX_SYNX_INPUT_BYTES];
50 let end = core::str::from_utf8(slice)
51 .map(|s| s.len())
52 .unwrap_or_else(|e| e.valid_up_to());
53 &text[..end]
54}
55
56fn strip_boundary(s: &str, strip: usize) -> usize {
64 let mut off = 0usize;
65 for ch in s.chars() {
66 if off >= strip || !ch.is_whitespace() {
67 break;
68 }
69 off += ch.len_utf8();
70 }
71 off
72}
73
74fn is_reserved_js_key(key: &str) -> bool {
76 key == "__proto__" || key == "constructor" || key == "prototype"
77}
78
79fn utf8_prefix_len(s: &str, limit: usize) -> usize {
81 if s.len() <= limit {
82 return s.len();
83 }
84 let mut end = limit;
85 while end > 0 && !s.is_char_boundary(end) {
86 end -= 1;
87 }
88 end
89}
90
91fn find_parse_end_bytes(bytes: &[u8]) -> usize {
94 let max_newlines = MAX_LINE_STARTS.saturating_sub(1);
95 let mut seen_newlines = 0usize;
96 let mut scan = 0usize;
97 while scan < bytes.len() {
98 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
99 if seen_newlines >= max_newlines {
100 return scan + rel;
101 }
102 seen_newlines += 1;
103 scan += rel + 1;
104 } else {
105 break;
106 }
107 }
108 bytes.len()
109}
110
111#[derive(Debug, Clone, Copy)]
117pub struct ParserOptions {
118 pub directives: bool,
130
131 pub nondeterministic_hints: bool,
140}
141
142impl Default for ParserOptions {
143 fn default() -> Self {
144 Self { directives: true, nondeterministic_hints: true }
145 }
146}
147
148pub fn parse(text: &str) -> ParseResult {
150 parse_with(text, ParserOptions::default())
151}
152
153pub fn parse_with(text: &str, opts: ParserOptions) -> ParseResult {
157 let original_len = text.len();
158 let text = clamp_synx_text(text);
159 let parse_end = find_parse_end_bytes(text.as_bytes());
160 let text = &text[..parse_end];
161 let bytes = text.as_bytes();
162 let mut truncated_multiline = text.len() < original_len;
164
165 let mut line_starts: Vec<usize> = Vec::new();
166 line_starts.push(0);
167 let mut scan = 0usize;
168 while scan < bytes.len() {
169 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
170 let pos = scan + rel;
171 line_starts.push(pos + 1);
172 scan = pos + 1;
173 } else {
174 break;
175 }
176 }
177 let line_count = line_starts.len();
178
179 let mut root = HashMap::new();
180 let mut stack: Vec<(i32, StackEntry)> = vec![(-1, StackEntry::Root)];
181 let mut nav_cache: NavCache = None;
183 let mut mode = Mode::Static;
184 let mut locked = false;
185 let mut tool = false;
186 let mut schema = false;
187 let mut llm = false;
188 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
189 let mut includes: Vec<IncludeDirective> = Vec::new();
190 let mut uses: Vec<UseDirective> = Vec::new();
191
192 let mut block: Option<BlockState> = None;
193 let mut list: Option<ListState> = None;
194 let mut in_block_comment = false;
195
196 let mut i = 0;
197 while i < line_count {
198 let start = line_starts[i];
200 let end = if i + 1 < line_count { line_starts[i + 1] - 1 } else { bytes.len() };
201 let end = if end > start && end > 0 && bytes.get(end - 1) == Some(&b'\r') { end - 1 } else { end };
203 let raw = &text[start..end];
204
205 let trimmed = raw.trim();
206
207 if trimmed.is_empty() {
210 i += 1;
211 continue;
212 }
213
214 let indent = (raw.len() - raw.trim_start().len()) as i32;
215
216 if let Some(ref mut blk) = block {
225 if indent > blk.indent {
226 if blk.content.len() < MAX_MULTILINE_BLOCK_BYTES {
227 if !blk.content.is_empty() {
228 blk.content.push('\n');
229 }
230 let room = MAX_MULTILINE_BLOCK_BYTES.saturating_sub(blk.content.len());
231 if room > 0 {
232 let slice: &str = if blk.preserve_indent {
233 if blk.base_indent < 0 {
239 blk.base_indent = indent;
240 }
241 let strip = blk.base_indent.min(indent) as usize;
242 let raw_trim_end = raw.trim_end();
243 let cut = strip_boundary(raw_trim_end, strip);
244 if cut < raw_trim_end.len() {
245 &raw_trim_end[cut..]
246 } else {
247 ""
248 }
249 } else {
250 trimmed
251 };
252 let n = utf8_prefix_len(slice, room);
253 blk.content.push_str(&slice[..n]);
254 if n < slice.len() {
255 truncated_multiline = true;
256 }
257 }
258 } else {
259 truncated_multiline = true;
260 }
261 i += 1;
262 continue;
263 } else {
264 let content = std::mem::take(&mut blk.content);
265 let blk_key = blk.key.clone();
266 let blk_stack_idx = blk.stack_idx;
267 block = None;
268 insert_value(&mut root, &stack, blk_stack_idx, &blk_key, Value::String(content), &mut nav_cache);
269 }
270 }
271
272 if trimmed == "###" {
274 in_block_comment = !in_block_comment;
275 i += 1;
276 continue;
277 }
278 if in_block_comment {
279 i += 1;
280 continue;
281 }
282
283 if opts.directives && (trimmed.starts_with('!') || trimmed.starts_with("#!mode:")) {
289 if trimmed == "!active" {
291 mode = Mode::Active;
292 i += 1;
293 continue;
294 }
295 if trimmed == "!lock" {
296 locked = true;
297 i += 1;
298 continue;
299 }
300 if trimmed == "!tool" {
301 tool = true;
302 i += 1;
303 continue;
304 }
305 if trimmed == "!schema" {
306 schema = true;
307 i += 1;
308 continue;
309 }
310 if trimmed == "!llm" {
311 llm = true;
312 i += 1;
313 continue;
314 }
315 if trimmed.starts_with("!include ") {
316 if includes.len() < MAX_INCLUDE_DIRECTIVES {
317 let rest = trimmed[9..].trim();
318 let mut parts = rest.splitn(2, char::is_whitespace);
319 let path = parts.next().unwrap_or("").to_string();
320 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
321 let name = path.rsplit(&['/', '\\'][..]).next().unwrap_or(&path);
323 name.strip_suffix(".synx").or_else(|| name.strip_suffix(".SYNX")).unwrap_or(name).to_string()
324 });
325 includes.push(IncludeDirective { path, alias });
326 }
327 i += 1;
328 continue;
329 }
330 if trimmed.starts_with("!use ") {
331 let rest = trimmed[5..].trim();
332 if rest.starts_with('@') {
333 let mut parts = rest.splitn(2, " as ");
335 let package = parts.next().unwrap_or("").trim().to_string();
336 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
337 package.rsplit('/').next().unwrap_or(&package).to_string()
339 });
340 if !package.is_empty() {
341 uses.push(UseDirective { package, alias });
342 }
343 }
344 i += 1;
345 continue;
346 }
347 if trimmed.starts_with("#!mode:") {
348 let declared = trimmed.splitn(2, ':').nth(1).unwrap_or("static").trim();
349 mode = if declared == "active" { Mode::Active } else { Mode::Static };
350 i += 1;
351 continue;
352 }
353 }
354
355 if !opts.directives && trimmed.starts_with('!') {
360 i += 1;
361 continue;
362 }
363
364 if trimmed.starts_with('#') || trimmed.starts_with("//") {
367 i += 1;
368 continue;
369 }
370
371 if trimmed.starts_with("- ") {
373 if let Some(ref lst) = list {
374 if indent > lst.indent {
375 while stack.len() > 1 {
378 match stack.last() {
379 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { pop_frame(&mut stack, &mut nav_cache); }
380 _ => break,
381 }
382 }
383
384 let val_str = strip_comment(trimmed[2..].trim());
385
386 let mut peek = i + 1;
390 let mut nested = false;
391 while peek < line_count {
392 let ps = line_starts[peek];
393 let pe = if peek + 1 < line_count { line_starts[peek + 1] - 1 } else { bytes.len() };
394 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
395 let pl = &text[ps..pe];
396 let pt = pl.trim();
397 if pt.is_empty() {
398 peek += 1;
399 continue;
400 }
401 let pi = (pl.len() - pl.trim_start().len()) as i32;
402 if pi > indent
403 && !pt.starts_with("- ")
404 && !pt.starts_with('#')
405 && !pt.starts_with("//")
406 {
407 nested = true;
408 }
409 break;
410 }
411
412 let list_key = lst.key.clone();
413 let list_stack_idx = lst.stack_idx;
414
415 if let Some(parent_map) = navigate_cached(&mut root, &stack, list_stack_idx, &mut nav_cache) {
417 let arr_entry = parent_map
418 .entry(list_key.clone())
419 .or_insert_with(|| Value::Array(Vec::new()));
420 if let Value::Array(arr) = arr_entry {
421 if arr.len() >= MAX_LIST_ITEMS {
422 i += 1;
423 continue;
424 }
425 if nested {
426 let mut item_obj: HashMap<String, Value> = HashMap::new();
427 if let Some(parsed) = parse_line(&val_str) {
428 let val = if let Some(ref hint) = parsed.type_hint {
429 cast_typed_with(&parsed.value, hint, opts.nondeterministic_hints)
430 } else if !parsed.value.is_empty() {
431 cast(&parsed.value)
432 } else {
433 Value::Object(HashMap::new())
434 };
435 if !is_reserved_js_key(&parsed.key) {
439 item_obj.insert(parsed.key, val);
440 }
441 } else {
442 item_obj.insert("_value".to_string(), cast(&val_str));
443 }
444 let item_idx = arr.len();
445 arr.push(Value::Object(item_obj));
446 if stack.len() < MAX_PARSE_NESTING_DEPTH {
447 push_frame(&mut stack, &mut nav_cache, (indent, StackEntry::ListItem { list_key, item_idx }));
448 }
449 } else {
450 arr.push(cast(&val_str));
451 }
452 }
453 }
454
455 i += 1;
456 continue;
457 }
458 }
459 } else {
460 let close = list.as_ref().map(|lst| indent <= lst.indent).unwrap_or(false);
462 if close {
463 list = None;
464 while stack.len() > 1 {
466 match stack.last() {
467 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { pop_frame(&mut stack, &mut nav_cache); }
468 _ => break,
469 }
470 }
471 }
472 }
473
474 if let Some(parsed) = parse_line(trimmed) {
476 if is_reserved_js_key(&parsed.key) {
480 i += 1;
481 continue;
482 }
483
484 while stack.len() > 1 && stack.last().unwrap().0 >= indent {
486 pop_frame(&mut stack, &mut nav_cache);
487 }
488
489 let parent_idx = stack.len() - 1;
490
491 if mode == Mode::Active
493 && (!parsed.markers.is_empty()
494 || parsed.constraints.is_some()
495 || parsed.type_hint.is_some())
496 {
497 let path = build_path(&stack);
498 let meta_map = metadata.entry(path).or_default();
499 meta_map.insert(
500 parsed.key.clone(),
501 Meta {
502 markers: parsed.markers.clone(),
503 args: parsed.marker_args.clone(),
504 type_hint: parsed.type_hint.clone(),
505 constraints: parsed.constraints.clone(),
506 },
507 );
508 }
509
510 let is_block = parsed.value == "|" || parsed.value == "|+";
516 let preserve_indent = parsed.value == "|+";
517 let is_list_marker = parsed.markers.iter().any(|m| {
518 matches!(m.as_str(), "random" | "unique" | "geo" | "join")
519 });
520
521 if is_block {
522 insert_value(
523 &mut root,
524 &stack,
525 parent_idx,
526 &parsed.key,
527 Value::String(String::new()),
528 &mut nav_cache,
529 );
530 block = Some(BlockState {
531 indent,
532 key: parsed.key,
533 content: String::new(),
534 stack_idx: parent_idx,
535 preserve_indent,
536 base_indent: -1,
537 });
538 } else if is_list_marker && parsed.value.is_empty() {
539 insert_value(
542 &mut root,
543 &stack,
544 parent_idx,
545 &parsed.key,
546 Value::Array(Vec::new()),
547 &mut nav_cache,
548 );
549 list = Some(ListState {
550 indent,
551 key: parsed.key,
552 stack_idx: parent_idx,
553 });
554 } else if parsed.value.is_empty() {
555 let mut peek = i + 1;
557 while peek < line_count {
558 let ps = line_starts[peek];
559 let pe = if peek + 1 < line_count {
560 line_starts[peek + 1] - 1
561 } else {
562 bytes.len()
563 };
564 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
565 let pt = text[ps..pe].trim();
566 if !pt.is_empty() {
567 break;
568 }
569 peek += 1;
570 }
571
572 if peek < line_count {
573 let ps = line_starts[peek];
574 let pe = if peek + 1 < line_count {
575 line_starts[peek + 1] - 1
576 } else {
577 bytes.len()
578 };
579 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
580 let pt = text[ps..pe].trim();
581 if pt.starts_with("- ") {
582 insert_value(
583 &mut root,
584 &stack,
585 parent_idx,
586 &parsed.key,
587 Value::Array(Vec::new()),
588 &mut nav_cache,
589 );
590 list = Some(ListState {
591 indent,
592 key: parsed.key,
593 stack_idx: parent_idx,
594 });
595 i += 1;
596 continue;
597 }
598 }
599
600 insert_value(
601 &mut root,
602 &stack,
603 parent_idx,
604 &parsed.key,
605 Value::Object(HashMap::new()),
606 &mut nav_cache,
607 );
608 if stack.len() < MAX_PARSE_NESTING_DEPTH {
612 push_frame(&mut stack, &mut nav_cache, (indent, StackEntry::Key(parsed.key)));
613 }
614 } else {
615 let value = if let Some(ref hint) = parsed.type_hint {
616 cast_typed_with(&parsed.value, hint, opts.nondeterministic_hints)
617 } else {
618 cast(&parsed.value)
619 };
620 insert_value(&mut root, &stack, parent_idx, &parsed.key, value, &mut nav_cache);
621 }
622 }
623
624 i += 1;
625 }
626
627 if let Some(blk) = block {
629 insert_value(
630 &mut root,
631 &stack,
632 blk.stack_idx,
633 &blk.key,
634 Value::String(blk.content),
635 &mut nav_cache,
636 );
637 }
638
639 let _ = list;
642
643 let parsed_root = Value::Object(root);
644
645 ParseResult {
649 root: parsed_root,
650 mode,
651 locked,
652 tool,
653 schema,
654 llm,
655 metadata,
656 includes,
657 uses,
658 truncated: truncated_multiline,
659 }
660}
661
662pub fn reshape_tool_output(root: &Value, schema: bool) -> Value {
674 let map = match root {
675 Value::Object(m) => m,
676 _ => return root.clone(),
677 };
678
679 if schema {
680 let mut tools = Vec::new();
682 let mut keys: Vec<&String> = map.keys().collect();
684 keys.sort();
685 for key in keys {
686 let val = &map[key];
687 let mut def = HashMap::new();
688 def.insert("name".to_string(), Value::String(key.clone()));
689 def.insert("params".to_string(), val.clone());
690 tools.push(Value::Object(def));
691 }
692 let mut out = HashMap::new();
693 out.insert("tools".to_string(), Value::Array(tools));
694 Value::Object(out)
695 } else {
696 if map.is_empty() {
698 let mut out = HashMap::new();
699 out.insert("tool".to_string(), Value::Null);
700 out.insert("params".to_string(), Value::Object(HashMap::new()));
701 return Value::Object(out);
702 }
703
704 let mut keys: Vec<&String> = map.keys().collect();
707 keys.sort();
708 let tool_key = keys[0];
709 let tool_value = &map[tool_key];
710
711 let params = match tool_value {
712 Value::Object(m) => Value::Object(m.clone()),
713 _ => Value::Object(HashMap::new()),
715 };
716
717 let mut out = HashMap::new();
718 out.insert("tool".to_string(), Value::String(tool_key.clone()));
719 out.insert("params".to_string(), params);
720 Value::Object(out)
721 }
722}
723
724#[derive(Debug)]
727enum StackEntry {
728 Root,
729 Key(String),
730 ListItem { list_key: String, item_idx: usize },
735}
736
737struct BlockState {
738 indent: i32,
739 key: String,
740 content: String,
741 stack_idx: usize,
742 preserve_indent: bool,
745 base_indent: i32,
748}
749
750struct ListState {
751 indent: i32,
752 key: String,
753 stack_idx: usize,
754}
755
756struct ParsedLine {
757 key: String,
758 type_hint: Option<String>,
759 value: String,
760 markers: Vec<String>,
761 marker_args: Vec<String>,
762 constraints: Option<Constraints>,
763}
764
765fn parse_line(trimmed: &str) -> Option<ParsedLine> {
768 if trimmed.is_empty()
769 || trimmed.starts_with('#')
770 || trimmed.starts_with("//")
771 || trimmed.starts_with("- ")
772 {
773 return None;
774 }
775
776 let bytes = trimmed.as_bytes();
777 let len = bytes.len();
778
779 let first = bytes[0];
780 if first == b'[' || first == b':' || first == b'-' || first == b'#' || first == b'/' || first == b'(' {
781 return None;
782 }
783
784 let mut pos = 0;
786 while pos < len {
787 let ch = bytes[pos];
788 if ch == b' ' || ch == b'\t' || ch == b'[' || ch == b':' || ch == b'(' {
789 break;
790 }
791 pos += 1;
792 }
793 let key = trimmed[..pos].to_string();
794
795 let mut type_hint = None;
797 if pos < len && bytes[pos] == b'(' {
798 let start = pos + 1;
799 if let Some(c) = trimmed[start..].find(')') {
800 type_hint = Some(trimmed[start..start + c].to_string());
801 pos = start + c + 1;
802 } else {
803 pos += 1;
804 }
805 }
806
807 let mut constraints = None;
810 if pos < len && bytes[pos] == b'[' {
811 let cstart = pos + 1;
812 let mut depth = 1usize;
813 let mut scan = cstart;
814 while scan < len && depth > 0 {
815 match bytes[scan] {
816 b'[' => depth += 1,
817 b']' => {
818 depth -= 1;
819 if depth == 0 {
820 break;
821 }
822 }
823 _ => {}
824 }
825 scan += 1;
826 }
827 if depth == 0 {
828 let constraint_str = &trimmed[cstart..scan];
829 constraints = Some(parse_constraints(constraint_str));
830 pos = scan + 1; } else {
832 if let Some(rel) = trimmed[cstart..].find(']') {
834 let constraint_str = &trimmed[cstart..cstart + rel];
835 constraints = Some(parse_constraints(constraint_str));
836 pos = cstart + rel + 1;
837 } else {
838 constraints = Some(parse_constraints(&trimmed[cstart..]));
839 pos = len;
840 }
841 }
842 }
843
844 let mut markers = Vec::new();
846 let mut marker_args = Vec::new();
847 if pos < len && bytes[pos] == b':' {
848 let marker_start = pos + 1;
849 let mut marker_end = marker_start;
850 while marker_end < len && bytes[marker_end] != b' ' && bytes[marker_end] != b'\t' {
851 marker_end += 1;
852 }
853 let chain = &trimmed[marker_start..marker_end];
854 markers = chain
855 .split(':')
856 .take(MAX_MARKER_CHAIN_SEGMENTS)
857 .map(|s| s.to_string())
858 .collect();
859 pos = marker_end;
860 }
861
862 while pos < len && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
864 pos += 1;
865 }
866
867 let mut raw_value = if pos < len {
869 strip_comment(&trimmed[pos..])
870 } else {
871 String::new()
872 };
873
874 if markers.contains(&"random".to_string()) && !raw_value.is_empty() {
876 let parts: Vec<&str> = raw_value.split_whitespace().collect();
877 let nums: Vec<String> = parts
878 .iter()
879 .filter(|s| s.parse::<f64>().is_ok())
880 .map(|s| s.to_string())
881 .collect();
882 if !nums.is_empty() {
883 marker_args = nums;
884 raw_value.clear();
885 }
886 }
887
888 if markers.contains(&"inherit".to_string()) && !raw_value.is_empty() {
891 marker_args = vec![raw_value.trim().to_string()];
892 raw_value.clear();
893 }
894
895 Some(ParsedLine {
896 key,
897 type_hint,
898 value: raw_value,
899 markers,
900 marker_args,
901 constraints,
902 })
903}
904
905pub(crate) fn parse_constraints(raw: &str) -> Constraints {
913 let mut c = Constraints::default();
914 for part in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
915 if part == "required" {
916 c.required = true;
917 } else if part == "readonly" {
918 c.readonly = true;
919 } else if let Some(colon) = part.find(':') {
920 let key = part[..colon].trim();
921 let val = part[colon + 1..].trim();
922 match key {
923 "min" => c.min = val.parse().ok(),
924 "max" => c.max = val.parse().ok(),
925 "type" => c.type_name = Some(val.to_string()),
926 "pattern" => c.pattern = Some(val.to_string()),
927 "enum" => {
928 c.enum_values = Some(
929 val.split('|')
930 .take(MAX_CONSTRAINT_ENUM_PARTS)
931 .map(|s| s.to_string())
932 .collect(),
933 );
934 }
935 _ => {}
936 }
937 }
938 }
939 c
940}
941
942pub(crate) fn cast(val: &str) -> Value {
946 if val.len() >= 2 {
949 let bytes = val.as_bytes();
950 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
951 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
952 {
953 return Value::String(val[1..val.len() - 1].to_string());
954 }
955 }
956
957 match val {
958 "true" => Value::Bool(true),
959 "false" => Value::Bool(false),
960 "null" => Value::Null,
961 _ => {
962 let bytes = val.as_bytes();
963 let len = bytes.len();
964 if len == 0 {
965 return Value::String(String::new());
966 }
967
968 let mut start = 0;
969 if bytes[0] == b'-' {
970 if len == 1 {
971 return Value::String(val.to_string());
972 }
973 start = 1;
974 }
975
976 if bytes[start] >= b'0' && bytes[start] <= b'9' {
977 let mut dot_pos = None;
978 let mut all_numeric = true;
979 for j in start..len {
980 if bytes[j] == b'.' {
981 if dot_pos.is_some() {
982 all_numeric = false;
983 break;
984 }
985 dot_pos = Some(j);
986 } else if bytes[j] < b'0' || bytes[j] > b'9' {
987 all_numeric = false;
988 break;
989 }
990 }
991 if all_numeric {
992 if let Some(dp) = dot_pos {
993 if dp > start && dp < len - 1 {
994 if let Ok(f) = val.parse::<f64>() {
995 return Value::Float(f);
996 }
997 }
998 } else if let Ok(n) = val.parse::<i64>() {
999 return Value::Int(n);
1000 }
1001 }
1002 }
1003
1004 Value::String(val.to_string())
1005 }
1006 }
1007}
1008
1009pub(crate) fn cast_typed_with(val: &str, hint: &str, nondeterministic: bool) -> Value {
1014 match hint {
1015 "int" => Value::Int(val.parse().unwrap_or(0)),
1016 "float" => Value::Float(val.parse().unwrap_or(0.0)),
1017 "bool" => Value::Bool(val.trim() == "true"),
1018 "string" => Value::String(val.to_string()),
1019 "random" | "random:int" if nondeterministic => Value::Int(rng::random_i64()),
1020 "random:float" if nondeterministic => Value::Float(rng::random_f64_01()),
1021 "random:bool" if nondeterministic => Value::Bool(rng::random_bool()),
1022 _ => cast(val),
1023 }
1024}
1025
1026fn strip_comment(val: &str) -> String {
1027 let mut result = val.to_string();
1028 if let Some(idx) = result.find(" //") {
1029 result.truncate(idx);
1030 }
1031 if let Some(idx) = result.find(" #") {
1032 result.truncate(idx);
1033 }
1034 result.trim_end().to_string()
1035}
1036
1037fn build_path(stack: &[(i32, StackEntry)]) -> String {
1040 let mut parts = Vec::new();
1044 for (_, entry) in stack.iter().skip(1) {
1045 if let StackEntry::Key(ref k) = entry {
1046 parts.push(k.as_str());
1047 }
1048 }
1049 parts.join(".")
1050}
1051
1052type NavCache = Option<(usize, *mut HashMap<String, Value>)>;
1060
1061#[inline]
1070fn push_frame(stack: &mut Vec<(i32, StackEntry)>, cache: &mut NavCache, frame: (i32, StackEntry)) {
1071 stack.push(frame);
1072 *cache = None;
1073}
1074
1075#[inline]
1077fn pop_frame(stack: &mut Vec<(i32, StackEntry)>, cache: &mut NavCache) -> Option<(i32, StackEntry)> {
1078 let popped = stack.pop();
1079 *cache = None;
1080 popped
1081}
1082
1083fn navigate_cached<'a>(
1086 root: &'a mut HashMap<String, Value>,
1087 stack: &[(i32, StackEntry)],
1088 parent_idx: usize,
1089 cache: &mut NavCache,
1090) -> Option<&'a mut HashMap<String, Value>> {
1091 if let Some((idx, ptr)) = *cache {
1092 if idx == parent_idx {
1093 return Some(unsafe { &mut *ptr });
1098 }
1099 }
1100 let ptr = navigate_to_parent(root, stack, parent_idx)? as *mut HashMap<String, Value>;
1101 *cache = Some((parent_idx, ptr));
1102 Some(unsafe { &mut *ptr })
1105}
1106
1107fn insert_value(
1108 root: &mut HashMap<String, Value>,
1109 stack: &[(i32, StackEntry)],
1110 parent_idx: usize,
1111 key: &str,
1112 value: Value,
1113 cache: &mut NavCache,
1114) {
1115 if let Some(target) = navigate_cached(root, stack, parent_idx, cache) {
1116 target.insert(key.to_string(), value);
1117 }
1118 }
1122
1123fn navigate_to_parent<'a>(
1124 root: &'a mut HashMap<String, Value>,
1125 stack: &[(i32, StackEntry)],
1126 target_idx: usize,
1127) -> Option<&'a mut HashMap<String, Value>> {
1128 if target_idx == 0 {
1129 return Some(root);
1130 }
1131
1132 let mut current = root as *mut HashMap<String, Value>;
1143 for (_indent, entry) in stack.iter().skip(1).take(target_idx) {
1144 match entry {
1145 StackEntry::Root => unreachable!("Root only appears at index 0"),
1146 StackEntry::Key(k) => {
1147 let child = unsafe { (*current).get_mut(k) };
1148 match child {
1149 Some(Value::Object(map)) => current = map as *mut HashMap<String, Value>,
1150 _ => return None, }
1152 }
1153 StackEntry::ListItem { list_key, item_idx } => {
1154 let arr_val = unsafe { (*current).get_mut(list_key) };
1155 match arr_val {
1156 Some(Value::Array(arr)) => {
1157 if *item_idx >= arr.len() { return None; }
1158 match &mut arr[*item_idx] {
1159 Value::Object(map) => current = map as *mut HashMap<String, Value>,
1160 _ => return None,
1161 }
1162 }
1163 _ => return None,
1164 }
1165 }
1166 }
1167 }
1168 Some(unsafe { &mut *current })
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use super::*;
1174
1175 #[test]
1176 fn test_simple_key_value() {
1177 let data = parse("name Wario\nage 30\nactive true\nscore 99.5\nempty null");
1178 let root = data.root.as_object().unwrap();
1179 assert_eq!(root["name"], Value::String("Wario".into()));
1180 assert_eq!(root["age"], Value::Int(30));
1181 assert_eq!(root["active"], Value::Bool(true));
1182 assert_eq!(root["score"], Value::Float(99.5));
1183 assert_eq!(root["empty"], Value::Null);
1184 assert_eq!(data.mode, Mode::Static);
1185 }
1186
1187 #[test]
1188 fn test_nested_objects() {
1189 let data = parse("server\n host 0.0.0.0\n port 8080\n ssl\n enabled true");
1190 let root = data.root.as_object().unwrap();
1191 let server = root["server"].as_object().unwrap();
1192 assert_eq!(server["host"], Value::String("0.0.0.0".into()));
1193 assert_eq!(server["port"], Value::Int(8080));
1194 let ssl = server["ssl"].as_object().unwrap();
1195 assert_eq!(ssl["enabled"], Value::Bool(true));
1196 }
1197
1198 #[test]
1199 fn test_lists() {
1200 let data = parse("inventory\n - Sword\n - Shield\n - Potion");
1201 let root = data.root.as_object().unwrap();
1202 let inv = root["inventory"].as_array().unwrap();
1203 assert_eq!(inv.len(), 3);
1204 assert_eq!(inv[0], Value::String("Sword".into()));
1205 }
1206
1207 #[test]
1208 fn test_multiline_block() {
1209 let data = parse("rules |\n Rule one.\n Rule two.\n Rule three.");
1210 let root = data.root.as_object().unwrap();
1211 assert_eq!(
1212 root["rules"],
1213 Value::String("Rule one.\nRule two.\nRule three.".into())
1214 );
1215 }
1216
1217 #[test]
1221 fn test_multiline_block_preserve_indent() {
1222 let src = "prompt |+\n Top\n Indented two\n Indented four\n Back to two\n Top again";
1223 let data = parse(src);
1224 let root = data.root.as_object().unwrap();
1225 assert_eq!(
1226 root["prompt"],
1227 Value::String(
1228 "Top\n Indented two\n Indented four\n Back to two\nTop again".into()
1229 )
1230 );
1231 }
1232
1233 #[test]
1234 fn test_multiline_block_preserve_indent_locks_base() {
1235 let src = "code |+\n function foo() {\n return 1;\n }";
1238 let data = parse(src);
1239 let root = data.root.as_object().unwrap();
1240 assert_eq!(
1241 root["code"],
1242 Value::String("function foo() {\n return 1;\n}".into())
1243 );
1244 }
1245
1246 #[test]
1247 fn test_multiline_block_preserve_indent_ends_at_opener_indent() {
1248 let src = "intro |+\n hello\n world\nnext plain";
1249 let data = parse(src);
1250 let root = data.root.as_object().unwrap();
1251 assert_eq!(root["intro"], Value::String("hello\n world".into()));
1252 assert_eq!(root["next"], Value::String("plain".into()));
1253 }
1254
1255 #[test]
1256 fn test_comments() {
1257 let data = parse("# comment\nname Wario # inline\nage 30 // inline");
1258 let root = data.root.as_object().unwrap();
1259 assert_eq!(root["name"], Value::String("Wario".into()));
1260 assert_eq!(root["age"], Value::Int(30));
1261 }
1262
1263 #[test]
1264 fn test_active_mode() {
1265 let data = parse("!active\nprice 100\ntax:calc price * 0.2");
1266 assert_eq!(data.mode, Mode::Active);
1267 let root = data.root.as_object().unwrap();
1268 assert_eq!(root["price"], Value::Int(100));
1269 assert_eq!(root["tax"], Value::String("price * 0.2".into()));
1271 let meta = data.metadata.get("").unwrap();
1273 assert!(meta.contains_key("tax"));
1274 assert_eq!(meta["tax"].markers, vec!["calc"]);
1275 }
1276
1277 #[test]
1278 fn test_markers_env_default() {
1279 let data = parse("!active\nport:env:default:3000 PORT");
1280 let meta = data.metadata.get("").unwrap();
1281 assert_eq!(meta["port"].markers, vec!["env", "default", "3000"]);
1282 }
1283
1284 #[test]
1285 fn test_type_hint() {
1286 let data = parse("zip(string) 90210");
1287 let root = data.root.as_object().unwrap();
1288 assert_eq!(root["zip"], Value::String("90210".into()));
1289 }
1290
1291 #[test]
1292 fn test_constraints() {
1293 let data = parse("!active\nname[min:3, max:30, required] Wario");
1294 let meta = data.metadata.get("").unwrap();
1295 let c = meta["name"].constraints.as_ref().unwrap();
1296 assert_eq!(c.min, Some(3.0));
1297 assert_eq!(c.max, Some(30.0));
1298 assert!(c.required);
1299 }
1300
1301 #[test]
1302 fn test_random_weights() {
1303 let data = parse("!active\ntier:random 90 5 5");
1304 let meta = data.metadata.get("").unwrap();
1305 assert_eq!(meta["tier"].markers, vec!["random"]);
1306 assert_eq!(meta["tier"].args, vec!["90", "5", "5"]);
1307 }
1308
1309 #[test]
1310 fn test_tool_directive_flags() {
1311 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1312 assert!(data.tool);
1313 assert!(!data.schema);
1314 assert_eq!(data.mode, Mode::Static);
1315 let root = data.root.as_object().unwrap();
1317 let ws = root["web_search"].as_object().unwrap();
1318 assert_eq!(ws["query"], Value::String("test".into()));
1319 assert_eq!(ws["lang"], Value::String("ru".into()));
1320 }
1321
1322 #[test]
1323 fn test_tool_schema_flags() {
1324 let data = parse("!tool\n!schema\nweb_search\n query string\n");
1325 assert!(data.tool);
1326 assert!(data.schema);
1327 }
1328
1329 #[test]
1330 fn test_llm_directive() {
1331 let data = parse("!llm\ncontext\n user_profile demo\ntask summarize\n");
1332 assert!(data.llm);
1333 assert!(!data.tool);
1334 let root = data.root.as_object().unwrap();
1335 assert_eq!(root["task"], Value::String("summarize".into()));
1336 let ctx = root["context"].as_object().unwrap();
1337 assert_eq!(ctx["user_profile"], Value::String("demo".into()));
1338 }
1339
1340 #[test]
1341 fn test_parse_caps_nesting_depth() {
1342 let mut s = String::new();
1345 for i in 0..(MAX_PARSE_NESTING_DEPTH as usize + 64) {
1346 s.push_str(&" ".repeat(i));
1347 s.push_str(&format!("k{i}\n"));
1348 }
1349
1350 let data = parse(&s);
1351 let mut cur = data.root.as_object().unwrap();
1352 let mut depth = 0usize;
1353 loop {
1355 if cur.len() != 1 {
1356 break;
1357 }
1358 let (_, v) = cur.iter().next().unwrap();
1359 match v {
1360 Value::Object(next) => {
1361 depth += 1;
1362 cur = next;
1363 }
1364 _ => break,
1365 }
1366 }
1367
1368 assert!(depth <= MAX_PARSE_NESTING_DEPTH);
1369 }
1370
1371 #[test]
1372 fn test_tool_call_reshape() {
1373 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1374 let shaped = reshape_tool_output(&data.root, false);
1375 let m = shaped.as_object().unwrap();
1376 assert_eq!(m["tool"], Value::String("web_search".into()));
1377 let params = m["params"].as_object().unwrap();
1378 assert_eq!(params["query"], Value::String("test".into()));
1379 assert_eq!(params["lang"], Value::String("ru".into()));
1380 }
1381
1382 #[test]
1383 fn test_tool_schema_reshape() {
1384 let data = parse("!tool\n!schema\nweb_search\n query string\n lang string\nmemory_write\n path string\n value string\n");
1385 let shaped = reshape_tool_output(&data.root, true);
1386 let m = shaped.as_object().unwrap();
1387 let tools = m["tools"].as_array().unwrap();
1388 assert_eq!(tools.len(), 2);
1389 let t0 = tools[0].as_object().unwrap();
1391 assert_eq!(t0["name"], Value::String("memory_write".into()));
1392 let p0 = t0["params"].as_object().unwrap();
1393 assert_eq!(p0["path"], Value::String("string".into()));
1394 let t1 = tools[1].as_object().unwrap();
1395 assert_eq!(t1["name"], Value::String("web_search".into()));
1396 }
1397
1398 #[test]
1399 fn test_tool_empty() {
1400 let data = parse("!tool\n");
1401 assert!(data.tool);
1402 let shaped = reshape_tool_output(&data.root, false);
1403 let m = shaped.as_object().unwrap();
1404 assert_eq!(m["tool"], Value::Null);
1405 }
1406
1407 #[test]
1410 fn test_directives_disabled_discards_directive_lines() {
1411 let src = "!active\n!include /etc/passwd\n!use @scope/pkg\nname Wario\n";
1412 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1413 assert_eq!(data.mode, Mode::Static);
1414 assert!(data.includes.is_empty());
1415 assert!(data.uses.is_empty());
1416 let root = data.root.as_object().unwrap();
1417 assert_eq!(root.len(), 1);
1419 assert_eq!(root["name"], Value::String("Wario".into()));
1420
1421 let data = parse(src);
1423 assert_eq!(data.mode, Mode::Active);
1424 assert_eq!(data.includes.len(), 1);
1425 }
1426
1427 #[test]
1428 fn test_directives_disabled_preserves_bang_lines_in_multiline() {
1429 let src = "body |+\n !include /etc/passwd\n !active\n tail\n";
1432 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1433 assert_eq!(
1434 data.root.as_object().unwrap()["body"],
1435 Value::String("!include /etc/passwd\n!active\ntail".into())
1436 );
1437 assert!(data.includes.is_empty());
1438 assert_eq!(data.mode, Mode::Static);
1439
1440 let src = "body |\n !include /etc/passwd\n";
1441 let data = parse_with(src, ParserOptions { directives: false, nondeterministic_hints: false });
1442 assert_eq!(
1443 data.root.as_object().unwrap()["body"],
1444 Value::String("!include /etc/passwd".into())
1445 );
1446 }
1447
1448 #[test]
1452 fn test_leading_bom_does_not_disable_active() {
1453 let data = parse("\u{feff}!active\nsum:calc 2 + 3\n");
1454 assert_eq!(data.mode, Mode::Active);
1455 assert!(data.root.as_object().unwrap().get("\u{feff}!active").is_none());
1456 }
1457
1458 #[test]
1459 fn test_leading_bom_stripped_from_first_key() {
1460 let data = parse("\u{feff}name Wario\n");
1461 let root = data.root.as_object().unwrap();
1462 assert_eq!(root["name"], Value::String("Wario".into()));
1463 assert!(root.get("\u{feff}name").is_none());
1464 }
1465
1466 #[test]
1470 fn test_long_parent_key_is_linear() {
1471 let key = "K".repeat(1024 * 1024);
1472 let mut doc = String::with_capacity(2 * 1024 * 1024);
1473 doc.push_str(&key);
1474 doc.push('\n');
1475 for i in 0..20_000 {
1476 doc.push_str(&format!(" c{i} {i}\n"));
1477 }
1478 let start = std::time::Instant::now();
1479 let data = parse(&doc);
1480 assert!(start.elapsed().as_secs() < 5, "long-parent parse took {:?}", start.elapsed());
1481 let root = data.root.as_object().unwrap();
1482 let inner = root[&key].as_object().unwrap();
1483 assert_eq!(inner.len(), 20_000);
1484 assert_eq!(inner["c0"], Value::Int(0));
1485 assert_eq!(inner["c19999"], Value::Int(19_999));
1486 }
1487
1488 #[test]
1491 fn test_nav_cache_distinguishes_siblings() {
1492 let data = parse("a\n x 1\nb\n x 2\nc\n y 3\n z 4\n");
1493 let root = data.root.as_object().unwrap();
1494 assert_eq!(root["a"].as_object().unwrap()["x"], Value::Int(1));
1495 assert_eq!(root["b"].as_object().unwrap()["x"], Value::Int(2));
1496 let c = root["c"].as_object().unwrap();
1497 assert_eq!(c["y"], Value::Int(3));
1498 assert_eq!(c["z"], Value::Int(4));
1499 assert_eq!(c.len(), 2);
1500 assert_eq!(root["a"].as_object().unwrap().len(), 1);
1501 }
1502
1503 #[test]
1504 fn test_tool_with_active() {
1505 let data = parse("!tool\n!active\nweb_search\n port:env:default:8080 PORT\n");
1506 assert!(data.tool);
1507 assert_eq!(data.mode, Mode::Active);
1508 let meta = data.metadata.get("web_search").unwrap();
1510 assert_eq!(meta["port"].markers, vec!["env", "default", "8080"]);
1511 }
1512}