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
68pub fn parse(text: &str) -> ParseResult {
70 let text = clamp_synx_text(text);
71 let parse_end = find_parse_end_bytes(text.as_bytes());
72 let text = &text[..parse_end];
73 let bytes = text.as_bytes();
74
75 let mut line_starts: Vec<usize> = Vec::new();
76 line_starts.push(0);
77 let mut scan = 0usize;
78 while scan < bytes.len() {
79 if let Some(rel) = memchr(b'\n', &bytes[scan..]) {
80 let pos = scan + rel;
81 line_starts.push(pos + 1);
82 scan = pos + 1;
83 } else {
84 break;
85 }
86 }
87 let line_count = line_starts.len();
88
89 let mut root = HashMap::new();
90 let mut stack: Vec<(i32, StackEntry)> = vec![(-1, StackEntry::Root)];
91 let mut mode = Mode::Static;
92 let mut locked = false;
93 let mut tool = false;
94 let mut schema = false;
95 let mut llm = false;
96 let mut metadata: HashMap<String, MetaMap> = HashMap::new();
97 let mut includes: Vec<IncludeDirective> = Vec::new();
98 let mut uses: Vec<UseDirective> = Vec::new();
99
100 let mut block: Option<BlockState> = None;
101 let mut list: Option<ListState> = None;
102 let mut in_block_comment = false;
103
104 let mut i = 0;
105 while i < line_count {
106 let start = line_starts[i];
108 let end = if i + 1 < line_count { line_starts[i + 1] - 1 } else { bytes.len() };
109 let end = if end > start && end > 0 && bytes.get(end - 1) == Some(&b'\r') { end - 1 } else { end };
111 let raw = &text[start..end];
112
113 let trimmed = raw.trim();
114
115 if trimmed == "!active" {
117 mode = Mode::Active;
118 i += 1;
119 continue;
120 }
121 if trimmed == "!lock" {
122 locked = true;
123 i += 1;
124 continue;
125 }
126 if trimmed == "!tool" {
127 tool = true;
128 i += 1;
129 continue;
130 }
131 if trimmed == "!schema" {
132 schema = true;
133 i += 1;
134 continue;
135 }
136 if trimmed == "!llm" {
137 llm = true;
138 i += 1;
139 continue;
140 }
141 if trimmed.starts_with("!include ") {
142 if includes.len() < MAX_INCLUDE_DIRECTIVES {
143 let rest = trimmed[9..].trim();
144 let mut parts = rest.splitn(2, char::is_whitespace);
145 let path = parts.next().unwrap_or("").to_string();
146 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
147 let name = path.rsplit(&['/', '\\'][..]).next().unwrap_or(&path);
149 name.strip_suffix(".synx").or_else(|| name.strip_suffix(".SYNX")).unwrap_or(name).to_string()
150 });
151 includes.push(IncludeDirective { path, alias });
152 }
153 i += 1;
154 continue;
155 }
156 if trimmed.starts_with("!use ") {
157 let rest = trimmed[5..].trim();
158 if rest.starts_with('@') {
159 let mut parts = rest.splitn(2, " as ");
161 let package = parts.next().unwrap_or("").trim().to_string();
162 let alias = parts.next().map(|s| s.trim().to_string()).unwrap_or_else(|| {
163 package.rsplit('/').next().unwrap_or(&package).to_string()
165 });
166 if !package.is_empty() {
167 uses.push(UseDirective { package, alias });
168 }
169 }
170 i += 1;
171 continue;
172 }
173 if trimmed.starts_with("#!mode:") {
174 let declared = trimmed.splitn(2, ':').nth(1).unwrap_or("static").trim();
175 mode = if declared == "active" { Mode::Active } else { Mode::Static };
176 i += 1;
177 continue;
178 }
179
180 if trimmed == "###" {
182 in_block_comment = !in_block_comment;
183 i += 1;
184 continue;
185 }
186 if in_block_comment {
187 i += 1;
188 continue;
189 }
190
191 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
193 i += 1;
194 continue;
195 }
196
197 let indent = (raw.len() - raw.trim_start().len()) as i32;
198
199 if let Some(ref mut blk) = block {
201 if indent > blk.indent {
202 if blk.content.len() < MAX_MULTILINE_BLOCK_BYTES {
203 if !blk.content.is_empty() {
204 blk.content.push('\n');
205 }
206 let room = MAX_MULTILINE_BLOCK_BYTES.saturating_sub(blk.content.len());
207 if room > 0 {
208 let n = trimmed.len().min(room);
209 blk.content.push_str(&trimmed[..n]);
210 }
211 }
212 i += 1;
213 continue;
214 } else {
215 let content = std::mem::take(&mut blk.content);
216 let blk_key = blk.key.clone();
217 let blk_stack_idx = blk.stack_idx;
218 block = None;
219 insert_value(&mut root, &stack, blk_stack_idx, &blk_key, Value::String(content));
220 }
221 }
222
223 if trimmed.starts_with("- ") {
225 if let Some(ref lst) = list {
226 if indent > lst.indent {
227 while stack.len() > 1 {
230 match stack.last() {
231 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
232 _ => break,
233 }
234 }
235
236 let val_str = strip_comment(trimmed[2..].trim());
237
238 let mut peek = i + 1;
242 let mut nested = false;
243 while peek < line_count {
244 let ps = line_starts[peek];
245 let pe = if peek + 1 < line_count { line_starts[peek + 1] - 1 } else { bytes.len() };
246 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
247 let pl = &text[ps..pe];
248 let pt = pl.trim();
249 if pt.is_empty() {
250 peek += 1;
251 continue;
252 }
253 let pi = (pl.len() - pl.trim_start().len()) as i32;
254 if pi > indent
255 && !pt.starts_with("- ")
256 && !pt.starts_with('#')
257 && !pt.starts_with("//")
258 {
259 nested = true;
260 }
261 break;
262 }
263
264 let list_key = lst.key.clone();
265 let list_stack_idx = lst.stack_idx;
266
267 if let Some(parent_map) = navigate_to_parent(&mut root, &stack, list_stack_idx) {
269 let arr_entry = parent_map
270 .entry(list_key.clone())
271 .or_insert_with(|| Value::Array(Vec::new()));
272 if let Value::Array(arr) = arr_entry {
273 if arr.len() >= MAX_LIST_ITEMS {
274 i += 1;
275 continue;
276 }
277 if nested {
278 let mut item_obj: HashMap<String, Value> = HashMap::new();
279 if let Some(parsed) = parse_line(&val_str) {
280 let val = if let Some(ref hint) = parsed.type_hint {
281 cast_typed(&parsed.value, hint)
282 } else if !parsed.value.is_empty() {
283 cast(&parsed.value)
284 } else {
285 Value::Object(HashMap::new())
286 };
287 item_obj.insert(parsed.key, val);
288 } else {
289 item_obj.insert("_value".to_string(), cast(&val_str));
290 }
291 let item_idx = arr.len();
292 arr.push(Value::Object(item_obj));
293 if stack.len() < MAX_PARSE_NESTING_DEPTH {
294 stack.push((indent, StackEntry::ListItem { list_key, item_idx }));
295 }
296 } else {
297 arr.push(cast(&val_str));
298 }
299 }
300 }
301
302 i += 1;
303 continue;
304 }
305 }
306 } else {
307 let close = list.as_ref().map(|lst| indent <= lst.indent).unwrap_or(false);
309 if close {
310 list = None;
311 while stack.len() > 1 {
313 match stack.last() {
314 Some((d, StackEntry::ListItem { .. })) if *d >= indent => { stack.pop(); }
315 _ => break,
316 }
317 }
318 }
319 }
320
321 if let Some(parsed) = parse_line(trimmed) {
323 if parsed.key == "__proto__"
327 || parsed.key == "constructor"
328 || parsed.key == "prototype"
329 {
330 i += 1;
331 continue;
332 }
333
334 while stack.len() > 1 && stack.last().unwrap().0 >= indent {
336 stack.pop();
337 }
338
339 let parent_idx = stack.len() - 1;
340
341 if mode == Mode::Active
343 && (!parsed.markers.is_empty()
344 || parsed.constraints.is_some()
345 || parsed.type_hint.is_some())
346 {
347 let path = build_path(&stack);
348 let meta_map = metadata.entry(path).or_default();
349 meta_map.insert(
350 parsed.key.clone(),
351 Meta {
352 markers: parsed.markers.clone(),
353 args: parsed.marker_args.clone(),
354 type_hint: parsed.type_hint.clone(),
355 constraints: parsed.constraints.clone(),
356 },
357 );
358 }
359
360 let is_block = parsed.value == "|";
361 let is_list_marker = parsed.markers.iter().any(|m| {
362 matches!(m.as_str(), "random" | "unique" | "geo" | "join")
363 });
364
365 if is_block {
366 insert_value(
367 &mut root,
368 &stack,
369 parent_idx,
370 &parsed.key,
371 Value::String(String::new()),
372 );
373 block = Some(BlockState {
374 indent,
375 key: parsed.key,
376 content: String::new(),
377 stack_idx: parent_idx,
378 });
379 } else if is_list_marker && parsed.value.is_empty() {
380 insert_value(
383 &mut root,
384 &stack,
385 parent_idx,
386 &parsed.key,
387 Value::Array(Vec::new()),
388 );
389 list = Some(ListState {
390 indent,
391 key: parsed.key,
392 items: Vec::new(),
393 stack_idx: parent_idx,
394 });
395 } else if parsed.value.is_empty() {
396 let mut peek = i + 1;
398 while peek < line_count {
399 let ps = line_starts[peek];
400 let pe = if peek + 1 < line_count {
401 line_starts[peek + 1] - 1
402 } else {
403 bytes.len()
404 };
405 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
406 let pt = text[ps..pe].trim();
407 if !pt.is_empty() {
408 break;
409 }
410 peek += 1;
411 }
412
413 if peek < line_count {
414 let ps = line_starts[peek];
415 let pe = if peek + 1 < line_count {
416 line_starts[peek + 1] - 1
417 } else {
418 bytes.len()
419 };
420 let pe = if pe > ps && bytes.get(pe - 1) == Some(&b'\r') { pe - 1 } else { pe };
421 let pt = text[ps..pe].trim();
422 if pt.starts_with("- ") {
423 insert_value(
424 &mut root,
425 &stack,
426 parent_idx,
427 &parsed.key,
428 Value::Array(Vec::new()),
429 );
430 list = Some(ListState {
431 indent,
432 key: parsed.key,
433 items: Vec::new(),
434 stack_idx: parent_idx,
435 });
436 i += 1;
437 continue;
438 }
439 }
440
441 insert_value(
442 &mut root,
443 &stack,
444 parent_idx,
445 &parsed.key,
446 Value::Object(HashMap::new()),
447 );
448 if stack.len() < MAX_PARSE_NESTING_DEPTH {
452 stack.push((indent, StackEntry::Key(parsed.key)));
453 }
454 } else {
455 let value = if let Some(ref hint) = parsed.type_hint {
456 cast_typed(&parsed.value, hint)
457 } else {
458 cast(&parsed.value)
459 };
460 insert_value(&mut root, &stack, parent_idx, &parsed.key, value);
461 }
462 }
463
464 i += 1;
465 }
466
467 if let Some(blk) = block {
469 insert_value(
470 &mut root,
471 &stack,
472 blk.stack_idx,
473 &blk.key,
474 Value::String(blk.content),
475 );
476 }
477
478 let _ = list;
481
482 let parsed_root = Value::Object(root);
483
484 ParseResult {
488 root: parsed_root,
489 mode,
490 locked,
491 tool,
492 schema,
493 llm,
494 metadata,
495 includes,
496 uses,
497 }
498}
499
500pub fn reshape_tool_output(root: &Value, schema: bool) -> Value {
512 let map = match root {
513 Value::Object(m) => m,
514 _ => return root.clone(),
515 };
516
517 if schema {
518 let mut tools = Vec::new();
520 let mut keys: Vec<&String> = map.keys().collect();
522 keys.sort();
523 for key in keys {
524 let val = &map[key];
525 let mut def = HashMap::new();
526 def.insert("name".to_string(), Value::String(key.clone()));
527 def.insert("params".to_string(), val.clone());
528 tools.push(Value::Object(def));
529 }
530 let mut out = HashMap::new();
531 out.insert("tools".to_string(), Value::Array(tools));
532 Value::Object(out)
533 } else {
534 if map.is_empty() {
536 let mut out = HashMap::new();
537 out.insert("tool".to_string(), Value::Null);
538 out.insert("params".to_string(), Value::Object(HashMap::new()));
539 return Value::Object(out);
540 }
541
542 let mut keys: Vec<&String> = map.keys().collect();
545 keys.sort();
546 let tool_key = keys[0];
547 let tool_value = &map[tool_key];
548
549 let params = match tool_value {
550 Value::Object(m) => Value::Object(m.clone()),
551 _ => Value::Object(HashMap::new()),
553 };
554
555 let mut out = HashMap::new();
556 out.insert("tool".to_string(), Value::String(tool_key.clone()));
557 out.insert("params".to_string(), params);
558 Value::Object(out)
559 }
560}
561
562#[derive(Debug)]
565enum StackEntry {
566 Root,
567 Key(String),
568 ListItem { list_key: String, item_idx: usize },
573}
574
575struct BlockState {
576 indent: i32,
577 key: String,
578 content: String,
579 stack_idx: usize,
580}
581
582struct ListState {
583 indent: i32,
584 key: String,
585 items: Vec<Value>,
586 stack_idx: usize,
587}
588
589struct ParsedLine {
590 key: String,
591 type_hint: Option<String>,
592 value: String,
593 markers: Vec<String>,
594 marker_args: Vec<String>,
595 constraints: Option<Constraints>,
596}
597
598fn parse_line(trimmed: &str) -> Option<ParsedLine> {
601 if trimmed.is_empty()
602 || trimmed.starts_with('#')
603 || trimmed.starts_with("//")
604 || trimmed.starts_with("- ")
605 {
606 return None;
607 }
608
609 let bytes = trimmed.as_bytes();
610 let len = bytes.len();
611
612 let first = bytes[0];
613 if first == b'[' || first == b':' || first == b'-' || first == b'#' || first == b'/' || first == b'(' {
614 return None;
615 }
616
617 let mut pos = 0;
619 while pos < len {
620 let ch = bytes[pos];
621 if ch == b' ' || ch == b'\t' || ch == b'[' || ch == b':' || ch == b'(' {
622 break;
623 }
624 pos += 1;
625 }
626 let key = trimmed[..pos].to_string();
627
628 let mut type_hint = None;
630 if pos < len && bytes[pos] == b'(' {
631 let start = pos + 1;
632 if let Some(c) = trimmed[start..].find(')') {
633 type_hint = Some(trimmed[start..start + c].to_string());
634 pos = start + c + 1;
635 } else {
636 pos += 1;
637 }
638 }
639
640 let mut constraints = None;
643 if pos < len && bytes[pos] == b'[' {
644 let cstart = pos + 1;
645 let mut depth = 1usize;
646 let mut scan = cstart;
647 while scan < len && depth > 0 {
648 match bytes[scan] {
649 b'[' => depth += 1,
650 b']' => {
651 depth -= 1;
652 if depth == 0 {
653 break;
654 }
655 }
656 _ => {}
657 }
658 scan += 1;
659 }
660 if depth == 0 {
661 let constraint_str = &trimmed[cstart..scan];
662 constraints = Some(parse_constraints(constraint_str));
663 pos = scan + 1; } else {
665 if let Some(rel) = trimmed[cstart..].find(']') {
667 let constraint_str = &trimmed[cstart..cstart + rel];
668 constraints = Some(parse_constraints(constraint_str));
669 pos = cstart + rel + 1;
670 } else {
671 constraints = Some(parse_constraints(&trimmed[cstart..]));
672 pos = len;
673 }
674 }
675 }
676
677 let mut markers = Vec::new();
679 let mut marker_args = Vec::new();
680 if pos < len && bytes[pos] == b':' {
681 let marker_start = pos + 1;
682 let mut marker_end = marker_start;
683 while marker_end < len && bytes[marker_end] != b' ' && bytes[marker_end] != b'\t' {
684 marker_end += 1;
685 }
686 let chain = &trimmed[marker_start..marker_end];
687 markers = chain
688 .split(':')
689 .take(MAX_MARKER_CHAIN_SEGMENTS)
690 .map(|s| s.to_string())
691 .collect();
692 pos = marker_end;
693 }
694
695 while pos < len && (bytes[pos] == b' ' || bytes[pos] == b'\t') {
697 pos += 1;
698 }
699
700 let mut raw_value = if pos < len {
702 strip_comment(&trimmed[pos..])
703 } else {
704 String::new()
705 };
706
707 if markers.contains(&"random".to_string()) && !raw_value.is_empty() {
709 let parts: Vec<&str> = raw_value.split_whitespace().collect();
710 let nums: Vec<String> = parts
711 .iter()
712 .filter(|s| s.parse::<f64>().is_ok())
713 .map(|s| s.to_string())
714 .collect();
715 if !nums.is_empty() {
716 marker_args = nums;
717 raw_value.clear();
718 }
719 }
720
721 if markers.contains(&"inherit".to_string()) && !raw_value.is_empty() {
724 marker_args = vec![raw_value.trim().to_string()];
725 raw_value.clear();
726 }
727
728 Some(ParsedLine {
729 key,
730 type_hint,
731 value: raw_value,
732 markers,
733 marker_args,
734 constraints,
735 })
736}
737
738fn parse_constraints(raw: &str) -> Constraints {
741 let mut c = Constraints::default();
742 for part in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
743 if part == "required" {
744 c.required = true;
745 } else if part == "readonly" {
746 c.readonly = true;
747 } else if let Some(colon) = part.find(':') {
748 let key = part[..colon].trim();
749 let val = part[colon + 1..].trim();
750 match key {
751 "min" => c.min = val.parse().ok(),
752 "max" => c.max = val.parse().ok(),
753 "type" => c.type_name = Some(val.to_string()),
754 "pattern" => c.pattern = Some(val.to_string()),
755 "enum" => {
756 c.enum_values = Some(
757 val.split('|')
758 .take(MAX_CONSTRAINT_ENUM_PARTS)
759 .map(|s| s.to_string())
760 .collect(),
761 );
762 }
763 _ => {}
764 }
765 }
766 }
767 c
768}
769
770fn cast(val: &str) -> Value {
773 if val.len() >= 2 {
776 let bytes = val.as_bytes();
777 if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
778 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
779 {
780 return Value::String(val[1..val.len() - 1].to_string());
781 }
782 }
783
784 match val {
785 "true" => Value::Bool(true),
786 "false" => Value::Bool(false),
787 "null" => Value::Null,
788 _ => {
789 let bytes = val.as_bytes();
790 let len = bytes.len();
791 if len == 0 {
792 return Value::String(String::new());
793 }
794
795 let mut start = 0;
796 if bytes[0] == b'-' {
797 if len == 1 {
798 return Value::String(val.to_string());
799 }
800 start = 1;
801 }
802
803 if bytes[start] >= b'0' && bytes[start] <= b'9' {
804 let mut dot_pos = None;
805 let mut all_numeric = true;
806 for j in start..len {
807 if bytes[j] == b'.' {
808 if dot_pos.is_some() {
809 all_numeric = false;
810 break;
811 }
812 dot_pos = Some(j);
813 } else if bytes[j] < b'0' || bytes[j] > b'9' {
814 all_numeric = false;
815 break;
816 }
817 }
818 if all_numeric {
819 if let Some(dp) = dot_pos {
820 if dp > start && dp < len - 1 {
821 if let Ok(f) = val.parse::<f64>() {
822 return Value::Float(f);
823 }
824 }
825 } else if let Ok(n) = val.parse::<i64>() {
826 return Value::Int(n);
827 }
828 }
829 }
830
831 Value::String(val.to_string())
832 }
833 }
834}
835
836fn cast_typed(val: &str, hint: &str) -> Value {
837 match hint {
838 "int" => Value::Int(val.parse().unwrap_or(0)),
839 "float" => Value::Float(val.parse().unwrap_or(0.0)),
840 "bool" => Value::Bool(val.trim() == "true"),
841 "string" => Value::String(val.to_string()),
842 "random" | "random:int" => Value::Int(rng::random_i64()),
843 "random:float" => Value::Float(rng::random_f64_01()),
844 "random:bool" => Value::Bool(rng::random_bool()),
845 _ => cast(val),
846 }
847}
848
849fn strip_comment(val: &str) -> String {
850 let mut result = val.to_string();
851 if let Some(idx) = result.find(" //") {
852 result.truncate(idx);
853 }
854 if let Some(idx) = result.find(" #") {
855 result.truncate(idx);
856 }
857 result.trim_end().to_string()
858}
859
860fn build_path(stack: &[(i32, StackEntry)]) -> String {
863 let mut parts = Vec::new();
867 for (_, entry) in stack.iter().skip(1) {
868 if let StackEntry::Key(ref k) = entry {
869 parts.push(k.as_str());
870 }
871 }
872 parts.join(".")
873}
874
875fn insert_value(
876 root: &mut HashMap<String, Value>,
877 stack: &[(i32, StackEntry)],
878 parent_idx: usize,
879 key: &str,
880 value: Value,
881) {
882 if let Some(target) = navigate_to_parent(root, stack, parent_idx) {
883 target.insert(key.to_string(), value);
884 }
885 }
889
890fn navigate_to_parent<'a>(
891 root: &'a mut HashMap<String, Value>,
892 stack: &[(i32, StackEntry)],
893 target_idx: usize,
894) -> Option<&'a mut HashMap<String, Value>> {
895 if target_idx == 0 {
896 return Some(root);
897 }
898
899 let mut current = root as *mut HashMap<String, Value>;
910 for (_indent, entry) in stack.iter().skip(1).take(target_idx) {
911 match entry {
912 StackEntry::Root => unreachable!("Root only appears at index 0"),
913 StackEntry::Key(k) => {
914 let child = unsafe { (*current).get_mut(k) };
915 match child {
916 Some(Value::Object(map)) => current = map as *mut HashMap<String, Value>,
917 _ => return None, }
919 }
920 StackEntry::ListItem { list_key, item_idx } => {
921 let arr_val = unsafe { (*current).get_mut(list_key) };
922 match arr_val {
923 Some(Value::Array(arr)) => {
924 if *item_idx >= arr.len() { return None; }
925 match &mut arr[*item_idx] {
926 Value::Object(map) => current = map as *mut HashMap<String, Value>,
927 _ => return None,
928 }
929 }
930 _ => return None,
931 }
932 }
933 }
934 }
935 Some(unsafe { &mut *current })
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941
942 #[test]
943 fn test_simple_key_value() {
944 let data = parse("name Wario\nage 30\nactive true\nscore 99.5\nempty null");
945 let root = data.root.as_object().unwrap();
946 assert_eq!(root["name"], Value::String("Wario".into()));
947 assert_eq!(root["age"], Value::Int(30));
948 assert_eq!(root["active"], Value::Bool(true));
949 assert_eq!(root["score"], Value::Float(99.5));
950 assert_eq!(root["empty"], Value::Null);
951 assert_eq!(data.mode, Mode::Static);
952 }
953
954 #[test]
955 fn test_nested_objects() {
956 let data = parse("server\n host 0.0.0.0\n port 8080\n ssl\n enabled true");
957 let root = data.root.as_object().unwrap();
958 let server = root["server"].as_object().unwrap();
959 assert_eq!(server["host"], Value::String("0.0.0.0".into()));
960 assert_eq!(server["port"], Value::Int(8080));
961 let ssl = server["ssl"].as_object().unwrap();
962 assert_eq!(ssl["enabled"], Value::Bool(true));
963 }
964
965 #[test]
966 fn test_lists() {
967 let data = parse("inventory\n - Sword\n - Shield\n - Potion");
968 let root = data.root.as_object().unwrap();
969 let inv = root["inventory"].as_array().unwrap();
970 assert_eq!(inv.len(), 3);
971 assert_eq!(inv[0], Value::String("Sword".into()));
972 }
973
974 #[test]
975 fn test_multiline_block() {
976 let data = parse("rules |\n Rule one.\n Rule two.\n Rule three.");
977 let root = data.root.as_object().unwrap();
978 assert_eq!(
979 root["rules"],
980 Value::String("Rule one.\nRule two.\nRule three.".into())
981 );
982 }
983
984 #[test]
985 fn test_comments() {
986 let data = parse("# comment\nname Wario # inline\nage 30 // inline");
987 let root = data.root.as_object().unwrap();
988 assert_eq!(root["name"], Value::String("Wario".into()));
989 assert_eq!(root["age"], Value::Int(30));
990 }
991
992 #[test]
993 fn test_active_mode() {
994 let data = parse("!active\nprice 100\ntax:calc price * 0.2");
995 assert_eq!(data.mode, Mode::Active);
996 let root = data.root.as_object().unwrap();
997 assert_eq!(root["price"], Value::Int(100));
998 assert_eq!(root["tax"], Value::String("price * 0.2".into()));
1000 let meta = data.metadata.get("").unwrap();
1002 assert!(meta.contains_key("tax"));
1003 assert_eq!(meta["tax"].markers, vec!["calc"]);
1004 }
1005
1006 #[test]
1007 fn test_markers_env_default() {
1008 let data = parse("!active\nport:env:default:3000 PORT");
1009 let meta = data.metadata.get("").unwrap();
1010 assert_eq!(meta["port"].markers, vec!["env", "default", "3000"]);
1011 }
1012
1013 #[test]
1014 fn test_type_hint() {
1015 let data = parse("zip(string) 90210");
1016 let root = data.root.as_object().unwrap();
1017 assert_eq!(root["zip"], Value::String("90210".into()));
1018 }
1019
1020 #[test]
1021 fn test_constraints() {
1022 let data = parse("!active\nname[min:3, max:30, required] Wario");
1023 let meta = data.metadata.get("").unwrap();
1024 let c = meta["name"].constraints.as_ref().unwrap();
1025 assert_eq!(c.min, Some(3.0));
1026 assert_eq!(c.max, Some(30.0));
1027 assert!(c.required);
1028 }
1029
1030 #[test]
1031 fn test_random_weights() {
1032 let data = parse("!active\ntier:random 90 5 5");
1033 let meta = data.metadata.get("").unwrap();
1034 assert_eq!(meta["tier"].markers, vec!["random"]);
1035 assert_eq!(meta["tier"].args, vec!["90", "5", "5"]);
1036 }
1037
1038 #[test]
1039 fn test_tool_directive_flags() {
1040 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1041 assert!(data.tool);
1042 assert!(!data.schema);
1043 assert_eq!(data.mode, Mode::Static);
1044 let root = data.root.as_object().unwrap();
1046 let ws = root["web_search"].as_object().unwrap();
1047 assert_eq!(ws["query"], Value::String("test".into()));
1048 assert_eq!(ws["lang"], Value::String("ru".into()));
1049 }
1050
1051 #[test]
1052 fn test_tool_schema_flags() {
1053 let data = parse("!tool\n!schema\nweb_search\n query string\n");
1054 assert!(data.tool);
1055 assert!(data.schema);
1056 }
1057
1058 #[test]
1059 fn test_llm_directive() {
1060 let data = parse("!llm\ncontext\n user_profile demo\ntask summarize\n");
1061 assert!(data.llm);
1062 assert!(!data.tool);
1063 let root = data.root.as_object().unwrap();
1064 assert_eq!(root["task"], Value::String("summarize".into()));
1065 let ctx = root["context"].as_object().unwrap();
1066 assert_eq!(ctx["user_profile"], Value::String("demo".into()));
1067 }
1068
1069 #[test]
1070 fn test_parse_caps_nesting_depth() {
1071 let mut s = String::new();
1074 for i in 0..(MAX_PARSE_NESTING_DEPTH as usize + 64) {
1075 s.push_str(&" ".repeat(i));
1076 s.push_str(&format!("k{i}\n"));
1077 }
1078
1079 let data = parse(&s);
1080 let mut cur = data.root.as_object().unwrap();
1081 let mut depth = 0usize;
1082 loop {
1084 if cur.len() != 1 {
1085 break;
1086 }
1087 let (_, v) = cur.iter().next().unwrap();
1088 match v {
1089 Value::Object(next) => {
1090 depth += 1;
1091 cur = next;
1092 }
1093 _ => break,
1094 }
1095 }
1096
1097 assert!(depth <= MAX_PARSE_NESTING_DEPTH);
1098 }
1099
1100 #[test]
1101 fn test_tool_call_reshape() {
1102 let data = parse("!tool\nweb_search\n query test\n lang ru\n");
1103 let shaped = reshape_tool_output(&data.root, false);
1104 let m = shaped.as_object().unwrap();
1105 assert_eq!(m["tool"], Value::String("web_search".into()));
1106 let params = m["params"].as_object().unwrap();
1107 assert_eq!(params["query"], Value::String("test".into()));
1108 assert_eq!(params["lang"], Value::String("ru".into()));
1109 }
1110
1111 #[test]
1112 fn test_tool_schema_reshape() {
1113 let data = parse("!tool\n!schema\nweb_search\n query string\n lang string\nmemory_write\n path string\n value string\n");
1114 let shaped = reshape_tool_output(&data.root, true);
1115 let m = shaped.as_object().unwrap();
1116 let tools = m["tools"].as_array().unwrap();
1117 assert_eq!(tools.len(), 2);
1118 let t0 = tools[0].as_object().unwrap();
1120 assert_eq!(t0["name"], Value::String("memory_write".into()));
1121 let p0 = t0["params"].as_object().unwrap();
1122 assert_eq!(p0["path"], Value::String("string".into()));
1123 let t1 = tools[1].as_object().unwrap();
1124 assert_eq!(t1["name"], Value::String("web_search".into()));
1125 }
1126
1127 #[test]
1128 fn test_tool_empty() {
1129 let data = parse("!tool\n");
1130 assert!(data.tool);
1131 let shaped = reshape_tool_output(&data.root, false);
1132 let m = shaped.as_object().unwrap();
1133 assert_eq!(m["tool"], Value::Null);
1134 }
1135
1136 #[test]
1137 fn test_tool_with_active() {
1138 let data = parse("!tool\n!active\nweb_search\n port:env:default:8080 PORT\n");
1139 assert!(data.tool);
1140 assert_eq!(data.mode, Mode::Active);
1141 let meta = data.metadata.get("web_search").unwrap();
1143 assert_eq!(meta["port"].markers, vec!["env", "default", "8080"]);
1144 }
1145}