Skip to main content

synx_core/
engine.rs

1//! SYNX Engine — resolves active markers (:random, :calc, :env, :alias, :secret, etc.)
2//! in a parsed SYNX value tree. Only runs in !active mode.
3
4use std::collections::HashMap;
5use std::sync::{Mutex, OnceLock};
6use std::time::{Duration, Instant};
7use crate::calc::safe_calc;
8use crate::parser;
9use crate::rng;
10use crate::value::*;
11
12static SPAM_BUCKETS: OnceLock<Mutex<HashMap<String, Vec<Instant>>>> = OnceLock::new();
13
14/// Maximum expression length accepted by :calc (prevents ReDoS/stack abuse).
15const MAX_CALC_EXPR_LEN: usize = 4096;
16/// Maximum file size for :include / :watch reads (10 MB).
17const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
18/// Default maximum include depth.
19const DEFAULT_MAX_INCLUDE_DEPTH: usize = 16;
20/// Maximum object nesting depth for active-mode resolution (prevents stack overflow).
21const MAX_RESOLVE_DEPTH: usize = 512;
22
23/// Validate that `full` path stays within the `base` directory (jail).
24/// Returns `Ok(canonical)` or an `Err` describing the violation.
25fn jail_path(base: &str, file_path: &str) -> Result<std::path::PathBuf, String> {
26    // Block absolute paths in the value itself
27    let fp = std::path::Path::new(file_path);
28    if fp.is_absolute() {
29        return Err(format!("SECURITY: absolute paths are not allowed: '{}'", file_path));
30    }
31    let base_canonical = match std::fs::canonicalize(base) {
32        Ok(p) => p,
33        Err(_) => std::path::PathBuf::from(base),
34    };
35    let full = base_canonical.join(file_path);
36    let full_canonical = match std::fs::canonicalize(&full) {
37        Ok(p) => p,
38        Err(_) => {
39            // File may not exist yet — at least verify no ".." escapes
40            let normalized = full.to_string_lossy();
41            if normalized.contains("..") {
42                return Err(format!("SECURITY: path traversal detected: '{}'", file_path));
43            }
44            return Ok(full);
45        }
46    };
47    if !full_canonical.starts_with(&base_canonical) {
48        return Err(format!("SECURITY: path escapes base directory: '{}'", file_path));
49    }
50    Ok(full_canonical)
51}
52
53/// Check file size before reading.
54fn check_file_size(path: &std::path::Path) -> Result<(), String> {
55    match std::fs::metadata(path) {
56        Ok(meta) if meta.len() > MAX_FILE_SIZE => {
57            Err(format!("SECURITY: file too large ({} bytes, max {})", meta.len(), MAX_FILE_SIZE))
58        }
59        _ => Ok(()),
60    }
61}
62
63/// Resolve all active-mode markers in a ParseResult.
64/// Returns the resolved root Value.
65pub fn resolve(result: &mut ParseResult, options: &Options) {
66    if result.mode != Mode::Active {
67        return;
68    }
69    let metadata = std::mem::take(&mut result.metadata);
70    let includes_directives = std::mem::take(&mut result.includes);
71
72    // ── Load !include files ──
73    let includes_map = load_includes(&includes_directives, options);
74
75    // ── :inherit pre-pass ──
76    apply_inheritance(&mut result.root, &metadata);
77    // Remove private blocks (keys starting with _)
78    if let Value::Object(ref mut root_map) = result.root {
79        root_map.retain(|k, _| !k.starts_with('_'));
80    }
81
82    // ── Build type registry ──
83    let type_registry = build_type_registry(&metadata);
84    // ── Build constraint registry ──
85    let constraint_registry = build_constraint_registry(&metadata);
86
87    // SAFETY: `root_ptr` is a raw pointer to `result.root` used exclusively
88    // for *immutable* read access inside marker handlers (:calc, :alias,
89    // :map, :watch) that need to look up other keys in the root
90    // while also holding a mutable reference to a child object.
91    // The invariants that keep this sound:
92    //   1. We never write through `root_ptr` — only reads via `&*root_ptr`.
93    //   2. Mutable writes go through `map` (the current object), which is
94    //      always a distinct subtree from what we read via `root_ptr`.
95    //   3. The pointer is valid for the entire duration of `resolve_value`.
96    let root_ptr = &mut result.root as *mut Value;
97    resolve_value(&mut result.root, root_ptr, options, &metadata, "", &includes_map, 0);
98
99    // ── Validate field constraints (global, by field name) ──
100    validate_field_constraints(&mut result.root, &constraint_registry);
101    
102    // ── Validate field types ──
103    validate_field_types(&mut result.root, &type_registry, "");
104    
105    result.metadata = metadata;
106    result.includes = includes_directives;
107}
108
109fn resolve_value(
110    value: &mut Value,
111    root_ptr: *mut Value,
112    options: &Options,
113    metadata: &HashMap<String, MetaMap>,
114    path: &str,
115    includes: &HashMap<String, Value>,
116    depth: usize,
117) {
118    // Guard: prevent stack overflow from deeply nested objects
119    if depth >= MAX_RESOLVE_DEPTH {
120        // Safety: recursion only descends into Object variants (see lines below),
121        // so value is always an Object here. Non-Object values are safe to skip.
122        if let Value::Object(ref mut map) = value {
123            for val in map.values_mut() {
124                *val = Value::String(
125                    "NESTING_ERR: maximum object nesting depth exceeded".to_string()
126                );
127            }
128        }
129        return;
130    }
131
132    let meta_map = metadata.get(path).cloned();
133
134    if let Value::Object(ref mut map) = value {
135        let keys: Vec<String> = map.keys().cloned().collect();
136
137        // First pass: recurse into nested objects/arrays
138        for key in &keys {
139            let child_path = if path.is_empty() {
140                key.clone()
141            } else {
142                format!("{}.{}", path, key)
143            };
144
145            if let Some(child) = map.get_mut(key) {
146                match child {
147                    Value::Object(_) => {
148                        resolve_value(child, root_ptr, options, metadata, &child_path, includes, depth + 1);
149                    }
150                    Value::Array(arr) => {
151                        for item in arr.iter_mut() {
152                            if let Value::Object(_) = item {
153                                resolve_value(item, root_ptr, options, metadata, &child_path, includes, depth + 1);
154                            }
155                        }
156                    }
157                    _ => {}
158                }
159            }
160        }
161
162        // Second pass: apply markers
163        if let Some(ref mm) = meta_map {
164            for key in &keys {
165                let meta = match mm.get(key) {
166                    Some(m) => m.clone(),
167                    None => continue,
168                };
169
170                apply_markers(map, key, &meta, root_ptr, options, path, metadata, includes);
171            }
172        }
173
174        // Third pass: auto-{} interpolation on all string values
175        let keys2: Vec<String> = map.keys().cloned().collect();
176        for key in &keys2 {
177            if let Some(Value::String(s)) = map.get(key) {
178                if s.contains('{') {
179                    let root_ref = unsafe { &*root_ptr };
180                    let result = resolve_interpolation(s, root_ref, map, includes);
181                    if result != *s {
182                        map.insert(key.to_string(), Value::String(result));
183                    }
184                }
185            }
186        }
187    }
188}
189
190fn apply_markers(
191    map: &mut HashMap<String, Value>,
192    key: &str,
193    meta: &Meta,
194    root_ptr: *mut Value,
195    options: &Options,
196    path: &str,
197    metadata: &HashMap<String, MetaMap>,
198    _includes: &HashMap<String, Value>,
199) {
200    let markers = &meta.markers;
201
202    // ── :spam ──
203    // Syntax: key:spam:MAX_CALLS:WINDOW_SEC target
204    // WINDOW_SEC defaults to 1 when omitted.
205    // If target is a key path, resolves its value after passing the limit check.
206    if markers.contains(&"spam".to_string()) {
207        let spam_idx = markers.iter().position(|m| m == "spam").unwrap();
208        let max_calls = markers
209            .get(spam_idx + 1)
210            .and_then(|s| s.parse::<usize>().ok())
211            .unwrap_or(0);
212        let window_sec = markers
213            .get(spam_idx + 2)
214            .and_then(|s| s.parse::<u64>().ok())
215            .unwrap_or(1);
216
217        if max_calls == 0 {
218            map.insert(
219                key.to_string(),
220                Value::String("SPAM_ERR: invalid limit, use :spam:MAX[:WINDOW_SEC]".to_string()),
221            );
222            return;
223        }
224
225        let target = map
226            .get(key)
227            .map(value_to_string)
228            .unwrap_or_else(|| key.to_string());
229        let bucket_key = format!("{}::{}", key, target);
230
231        if !allow_spam_access(&bucket_key, max_calls, window_sec) {
232            map.insert(
233                key.to_string(),
234                Value::String(format!(
235                    "SPAM_ERR: '{}' exceeded {} calls per {}s",
236                    target, max_calls, window_sec
237                )),
238            );
239            return;
240        }
241
242        if let Some(resolved) = map
243            .get(key)
244            .and_then(|v| {
245                let t = value_to_string(v);
246                let root_ref = unsafe { &*root_ptr };
247                deep_get(root_ref, &t).or_else(|| map.get(t.as_str()).cloned())
248            })
249        {
250            map.insert(key.to_string(), resolved);
251        }
252    }
253
254    // ── :include / :import ──
255    if markers.contains(&"include".to_string()) || markers.contains(&"import".to_string()) {
256        if let Some(Value::String(file_path)) = map.get(key) {
257            let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
258            if options._include_depth >= max_depth {
259                map.insert(
260                    key.to_string(),
261                    Value::String(format!("INCLUDE_ERR: max include depth ({}) exceeded", max_depth)),
262                );
263                return;
264            }
265            let base = options
266                .base_path
267                .as_deref()
268                .unwrap_or(".");
269            let full = match jail_path(base, file_path) {
270                Ok(p) => p,
271                Err(e) => {
272                    map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
273                    return;
274                }
275            };
276            if let Err(e) = check_file_size(&full) {
277                map.insert(key.to_string(), Value::String(format!("INCLUDE_ERR: {}", e)));
278                return;
279            }
280            match std::fs::read_to_string(&full) {
281                Ok(text) => {
282                    let mut included = parser::parse(&text);
283                    if included.mode == Mode::Active {
284                        let mut child_opts = options.clone();
285                        child_opts._include_depth += 1;
286                        if let Some(parent) = full.parent() {
287                            child_opts.base_path = Some(parent.to_string_lossy().into_owned());
288                        }
289                        resolve(&mut included, &child_opts);
290                    }
291                    map.insert(key.to_string(), included.root);
292                }
293                Err(e) => {
294                    map.insert(
295                        key.to_string(),
296                        Value::String(format!("INCLUDE_ERR: {}", e)),
297                    );
298                }
299            }
300        }
301        return;
302    }
303
304    // ── :env ──
305    if markers.contains(&"env".to_string()) {
306        if let Some(Value::String(var_name)) = map.get(key) {
307            let env_val = if let Some(ref env_map) = options.env {
308                env_map.get(var_name.as_str()).cloned()
309            } else {
310                std::env::var(var_name).ok()
311            };
312
313            let force_string = meta.type_hint.as_deref() == Some("string");
314            let default_idx = markers.iter().position(|m| m == "default");
315            if let Some(val) = env_val.filter(|v| !v.is_empty()) {
316                let resolved = if force_string {
317                    Value::String(val)
318                } else {
319                    cast_primitive(&val)
320                };
321                map.insert(key.to_string(), resolved);
322            } else if let Some(di) = default_idx {
323                if markers.len() > di + 1 {
324                    // Join all parts after 'default' back with ':'
325                    // to preserve IPs (0.0.0.0) and compound values
326                    let fallback = markers[di + 1..].join(":");
327                    let resolved = if force_string {
328                        Value::String(fallback)
329                    } else {
330                        cast_primitive(&fallback)
331                    };
332                    map.insert(key.to_string(), resolved);
333                } else {
334                    map.insert(key.to_string(), Value::Null);
335                }
336            } else {
337                map.insert(key.to_string(), Value::Null);
338            }
339        }
340    }
341
342    // ── :random ──
343    if markers.contains(&"random".to_string()) {
344        if let Some(Value::Array(arr)) = map.get(key) {
345            if arr.is_empty() {
346                map.insert(key.to_string(), Value::Null);
347                return;
348            }
349            let picked = if !meta.args.is_empty() {
350                let weights: Vec<f64> = meta.args.iter().filter_map(|s| s.parse().ok()).collect();
351                weighted_random(arr, &weights)
352            } else {
353                arr[rng::random_usize(arr.len())].clone()
354            };
355            map.insert(key.to_string(), picked);
356        }
357    }
358
359    // ── :ref ──
360    // Like :alias but feeds the resolved value into subsequent markers.
361    // Supports :ref:calc shorthand: key:ref:calc:*2 base_rate → resolves base_rate, then applies "VALUE * 2".
362    if markers.contains(&"ref".to_string()) {
363        if let Some(Value::String(target)) = map.get(key) {
364            let root_ref = unsafe { &*root_ptr };
365            let resolved = deep_get(root_ref, target)
366                .or_else(|| map.get(target.as_str()).cloned())
367                .unwrap_or(Value::Null);
368
369            // If :calc follows with a shorthand expression
370            if markers.contains(&"calc".to_string()) {
371                if let Some(n) = value_as_number(&resolved) {
372                    let calc_idx = markers.iter().position(|m| m == "calc").unwrap();
373                    if let Some(calc_expr) = markers.get(calc_idx + 1) {
374                        let first = calc_expr.chars().next().unwrap_or(' ');
375                        if "+-*/%".contains(first) {
376                            let expr = format!("{} {}", format_number(n), calc_expr);
377                            match safe_calc(&expr) {
378                                Ok(result) => {
379                                    let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
380                                        Value::Int(result as i64)
381                                    } else {
382                                        Value::Float(result)
383                                    };
384                                    map.insert(key.to_string(), v);
385                                }
386                                Err(e) => {
387                                    map.insert(key.to_string(), Value::String(format!("CALC_ERR: {}", e)));
388                                }
389                            }
390                        } else {
391                            map.insert(key.to_string(), resolved);
392                        }
393                    } else {
394                        map.insert(key.to_string(), resolved);
395                    }
396                } else {
397                    map.insert(key.to_string(), resolved);
398                }
399            } else {
400                map.insert(key.to_string(), resolved);
401            }
402        }
403    }
404
405    // ── :i18n ──
406    // Selects a localized value from a nested object based on options.lang.
407    // Supports pluralization: key:i18n:COUNT_FIELD
408    //   When count field is specified, the language entry must contain plural forms:
409    //   title:i18n:item_count
410    //     en
411    //       one {count} item
412    //       other {count} items
413    //     ru
414    //       one {count} предмет
415    //       few {count} предмета
416    //       many {count} предметов
417    //       other {count} предметов
418    if markers.contains(&"i18n".to_string()) {
419        if let Some(Value::Object(translations)) = map.get(key) {
420            let lang = options.lang.as_deref().unwrap_or("en");
421            let val = translations.get(lang)
422                .or_else(|| translations.get("en"))
423                .or_else(|| translations.values().next())
424                .cloned()
425                .unwrap_or(Value::Null);
426
427            // Check for pluralization: i18n:count_field
428            let i18n_idx = markers.iter().position(|m| m == "i18n").unwrap();
429            let count_field = markers.get(i18n_idx + 1).cloned();
430
431            if let (Some(ref cf), Value::Object(ref plural_forms)) = (&count_field, &val) {
432                // Look up count value from current map or root
433                let count_val = map.get(cf)
434                    .and_then(value_as_number)
435                    .or_else(|| {
436                        let root_ref = unsafe { &*root_ptr };
437                        deep_get(root_ref, cf).and_then(|v| value_as_number(&v))
438                    })
439                    .unwrap_or(0.0) as i64;
440
441                let category = plural_category(lang, count_val);
442                let chosen = plural_forms.get(category)
443                    .or_else(|| plural_forms.get("other"))
444                    .or_else(|| plural_forms.values().next())
445                    .cloned()
446                    .unwrap_or(Value::Null);
447
448                // Substitute {count} in the result string
449                if let Value::String(ref s) = chosen {
450                    let replaced = s.replace("{count}", &count_val.to_string());
451                    map.insert(key.to_string(), Value::String(replaced));
452                } else {
453                    map.insert(key.to_string(), chosen);
454                }
455            } else {
456                map.insert(key.to_string(), val);
457            }
458        }
459    }
460
461    // ── :calc ──
462    if markers.contains(&"calc".to_string()) {
463        if let Some(Value::String(expr)) = map.get(key) {
464            if expr.len() > MAX_CALC_EXPR_LEN {
465                map.insert(
466                    key.to_string(),
467                    Value::String(format!("CALC_ERR: expression too long ({} chars, max {})", expr.len(), MAX_CALC_EXPR_LEN)),
468                );
469                return;
470            }
471            let mut resolved = expr.clone();
472
473            // Substitute variables from root (flat keys)
474            let root_ref = unsafe { &*root_ptr };
475            if let Value::Object(ref root_map) = root_ref {
476                for (rk, rv) in root_map {
477                    if let Some(n) = value_as_number(rv) {
478                        resolved = replace_word(&resolved, rk, &format_number(n));
479                    }
480                }
481            }
482
483            // Substitute from current object (flat keys)
484            for (rk, rv) in map.iter() {
485                if rk != key {
486                    if let Some(n) = value_as_number(rv) {
487                        resolved = replace_word(&resolved, rk, &format_number(n));
488                    }
489                }
490            }
491
492            // Substitute dot-path references (e.g., base.hp, server.port)
493            let root_ref2 = unsafe { &*root_ptr };
494            let mut dot_resolved = String::new();
495            let bytes = resolved.as_bytes();
496            let len = bytes.len();
497            let mut i = 0;
498            while i < len {
499                if is_word_char(bytes[i]) {
500                    let start = i;
501                    let mut has_dot = false;
502                    while i < len && (is_word_char(bytes[i]) || bytes[i] == b'.') {
503                        if bytes[i] == b'.' { has_dot = true; }
504                        i += 1;
505                    }
506                    let token = &resolved[start..i];
507                    if has_dot && token.contains('.') {
508                        if let Some(val) = deep_get(root_ref2, token) {
509                            if let Some(n) = value_as_number(&val) {
510                                dot_resolved.push_str(&format_number(n));
511                                continue;
512                            }
513                        }
514                    }
515                    dot_resolved.push_str(token);
516                } else {
517                    dot_resolved.push(bytes[i] as char);
518                    i += 1;
519                }
520            }
521            resolved = dot_resolved;
522
523            match safe_calc(&resolved) {
524                Ok(result) => {
525                    let v = if result.fract() == 0.0 && result.abs() < i64::MAX as f64 {
526                        Value::Int(result as i64)
527                    } else {
528                        Value::Float(result)
529                    };
530                    map.insert(key.to_string(), v);
531                }
532                Err(e) => {
533                    map.insert(
534                        key.to_string(),
535                        Value::String(format!("CALC_ERR: {}", e)),
536                    );
537                }
538            }
539        }
540    }
541
542    // ── :alias ──
543    if markers.contains(&"alias".to_string()) {
544        if let Some(Value::String(target)) = map.get(key) {
545            let target = target.clone();
546            // Build the full dot-path of the current key
547            let current_path = if path.is_empty() {
548                key.to_string()
549            } else {
550                format!("{}.{}", path, key)
551            };
552            // Detect direct self-reference: key:alias key
553            if target == key || target == current_path {
554                map.insert(
555                    key.to_string(),
556                    Value::String(format!("ALIAS_ERR: self-referential alias: {} → {}", current_path, target)),
557                );
558            } else {
559                // Detect one-hop cycle: a → b → a
560                // Only flag as cycle if the target key ALSO has an :alias marker.
561                // Without this check, plain string values that happen to match the current
562                // key name would produce false-positive ALIAS_ERR results.
563                let root_ref = unsafe { &*root_ptr };
564                let target_val = deep_get(root_ref, &target);
565                // Determine the metadata path of the target key
566                let (target_parent, target_key_name) = if let Some(dot) = target.rfind('.') {
567                    (target[..dot].to_string(), target[dot + 1..].to_string())
568                } else {
569                    (String::new(), target.clone())
570                };
571                let target_has_alias = metadata
572                    .get(&target_parent)
573                    .and_then(|mm| mm.get(&target_key_name))
574                    .map(|m| m.markers.contains(&"alias".to_string()))
575                    .unwrap_or(false);
576                let is_cycle = target_has_alias && match &target_val {
577                    Some(Value::String(s)) => s == key || s == &current_path,
578                    _ => false,
579                };
580                if is_cycle {
581                    map.insert(
582                        key.to_string(),
583                        Value::String(format!("ALIAS_ERR: circular alias detected: {} → {}", current_path, target)),
584                    );
585                } else {
586                    let val = target_val.unwrap_or(Value::Null);
587                    map.insert(key.to_string(), val);
588                }
589            }
590        }
591    }
592
593    // ── :secret ──
594    if markers.contains(&"secret".to_string()) {
595        if let Some(val) = map.get(key) {
596            let s = value_to_string(val);
597            map.insert(key.to_string(), Value::Secret(s));
598        }
599    }
600
601    // ── :unique ──
602    if markers.contains(&"unique".to_string()) {
603        if let Some(Value::Array(arr)) = map.get(key) {
604            let mut seen = Vec::new();
605            let mut unique = Vec::new();
606            for item in arr {
607                let s = value_to_string(item);
608                if !seen.contains(&s) {
609                    seen.push(s);
610                    unique.push(item.clone());
611                }
612            }
613            map.insert(key.to_string(), Value::Array(unique));
614        }
615    }
616
617    // ── :geo ──
618    if markers.contains(&"geo".to_string()) {
619        if let Some(Value::Array(arr)) = map.get(key) {
620            let region = options.region.as_deref().unwrap_or("US");
621            let prefix = format!("{} ", region);
622            let found = arr.iter().find(|item| {
623                if let Value::String(s) = item {
624                    s.starts_with(&prefix)
625                } else {
626                    false
627                }
628            });
629
630            let result = if let Some(Value::String(s)) = found {
631                Value::String(s[prefix.len()..].trim().to_string())
632            } else if let Some(first) = arr.first() {
633                if let Value::String(s) = first {
634                    if let Some(space) = s.find(' ') {
635                        Value::String(s[space + 1..].trim().to_string())
636                    } else {
637                        first.clone()
638                    }
639                } else {
640                    first.clone()
641                }
642            } else {
643                Value::Null
644            };
645            map.insert(key.to_string(), result);
646        }
647    }
648
649    // ── :template (legacy — handled by auto-{} in resolve_value) ──
650
651    // ── :split ──
652    if markers.contains(&"split".to_string()) {
653        if let Some(Value::String(s)) = map.get(key) {
654            let split_idx = markers.iter().position(|m| m == "split").unwrap();
655            let sep = if split_idx + 1 < markers.len() {
656                delimiter_from_keyword(&markers[split_idx + 1])
657            } else {
658                ",".to_string()
659            };
660            let items: Vec<Value> = s
661                .split(&sep)
662                .map(|p| p.trim())
663                .filter(|p| !p.is_empty())
664                .map(|p| cast_primitive(p))
665                .collect();
666            map.insert(key.to_string(), Value::Array(items));
667        }
668    }
669
670    // ── :join ──
671    if markers.contains(&"join".to_string()) {
672        if let Some(Value::Array(arr)) = map.get(key) {
673            let join_idx = markers.iter().position(|m| m == "join").unwrap();
674            let sep = if join_idx + 1 < markers.len() {
675                delimiter_from_keyword(&markers[join_idx + 1])
676            } else {
677                ",".to_string()
678            };
679            let joined: String = arr
680                .iter()
681                .map(|v| value_to_string(v))
682                .collect::<Vec<_>>()
683                .join(&sep);
684            map.insert(key.to_string(), Value::String(joined));
685        }
686    }
687
688    // ── :default (standalone, without :env) ──
689    if markers.contains(&"default".to_string()) && !markers.contains(&"env".to_string()) {
690        let is_empty = match map.get(key) {
691            Some(Value::Null) | None => true,
692            Some(Value::String(s)) if s.is_empty() => true,
693            _ => false,
694        };
695        if is_empty {
696            let di = markers.iter().position(|m| m == "default").unwrap();
697            if markers.len() > di + 1 {
698                let fallback = markers[di + 1..].join(":");
699                let resolved = if meta.type_hint.as_deref() == Some("string") {
700                    Value::String(fallback)
701                } else {
702                    cast_primitive(&fallback)
703                };
704                map.insert(key.to_string(), resolved);
705            }
706        }
707    }
708
709    // ── :clamp ──
710    // Syntax: key:clamp:MIN:MAX value
711    // Clamps a numeric value to [MIN, MAX].
712    if markers.contains(&"clamp".to_string()) {
713        let clamp_idx = markers.iter().position(|m| m == "clamp").unwrap();
714        let min_s = markers.get(clamp_idx + 1).cloned().unwrap_or_default();
715        let max_s = markers.get(clamp_idx + 2).cloned().unwrap_or_default();
716        if let (Ok(lo), Ok(hi)) = (min_s.parse::<f64>(), max_s.parse::<f64>()) {
717            if lo > hi {
718                map.insert(key.to_string(), Value::String(
719                    format!("CONSTRAINT_ERR: clamp min ({}) > max ({})", lo, hi),
720                ));
721            } else if let Some(n) = map.get(key).and_then(value_as_number) {
722                let clamped = n.clamp(lo, hi);
723                let v = if clamped.fract() == 0.0 && clamped.abs() < i64::MAX as f64 {
724                    Value::Int(clamped as i64)
725                } else {
726                    Value::Float(clamped)
727                };
728                map.insert(key.to_string(), v);
729            }
730        }
731    }
732
733    // ── :round ──
734    // Syntax: key:round:N value  (N = decimal places, default 0)
735    // Works standalone or after :calc: key:calc:round:2 expr
736    if markers.contains(&"round".to_string()) {
737        let round_idx = markers.iter().position(|m| m == "round").unwrap();
738        let decimals: u32 = markers.get(round_idx + 1)
739            .and_then(|s| s.parse().ok())
740            .unwrap_or(0);
741        if let Some(n) = map.get(key).and_then(value_as_number) {
742            let factor = 10f64.powi(decimals as i32);
743            let rounded = (n * factor).round() / factor;
744            let v = if decimals == 0 {
745                Value::Int(rounded as i64)
746            } else {
747                Value::Float(rounded)
748            };
749            map.insert(key.to_string(), v);
750        }
751    }
752
753    // ── :map ──
754    // Syntax: key:map:source_key\n  - lookup_val result
755    // Looks up `source_key` in root, finds matching "lookup_val result" entry in the array.
756    if markers.contains(&"map".to_string()) {
757        if let Some(Value::Array(arr)) = map.get(key) {
758            let map_idx = markers.iter().position(|m| m == "map").unwrap();
759            let source_key = markers.get(map_idx + 1).cloned().unwrap_or_default();
760            let lookup_val = if !source_key.is_empty() {
761                let root_ref = unsafe { &*root_ptr };
762                deep_get(root_ref, &source_key)
763                    .or_else(|| map.get(&source_key).cloned())
764                    .map(|v| value_to_string(&v))
765                    .unwrap_or_default()
766            } else {
767                // Use the current string value as lookup key
768                match map.get(key) {
769                    Some(Value::String(s)) => s.clone(),
770                    _ => String::new(),
771                }
772            };
773
774            // Find matching entry: "lookup_val result_text"
775            let arr_clone = arr.clone();
776            let result = arr_clone.iter().find_map(|item| {
777                if let Value::String(s) = item {
778                    if let Some(space) = s.find(' ') {
779                        if s[..space].trim() == lookup_val {
780                            return Some(cast_primitive(s[space + 1..].trim()));
781                        }
782                    }
783                }
784                None
785            });
786            map.insert(key.to_string(), result.unwrap_or(Value::Null));
787        }
788    }
789
790    // ── :format ──
791    // Syntax: key:format:PATTERN value  (printf-style: %.2f, %d, %05d, %e)
792    // Converts numeric or string value to a formatted string.
793    if markers.contains(&"format".to_string()) {
794        let fmt_idx = markers.iter().position(|m| m == "format").unwrap();
795        let pattern = markers.get(fmt_idx + 1).cloned().unwrap_or_else(|| "%s".to_string());
796        if let Some(current) = map.get(key) {
797            let formatted = apply_format_pattern(&pattern, current);
798            map.insert(key.to_string(), Value::String(formatted));
799        }
800    }
801
802    // ── :fallback ──
803    // Syntax: key:fallback:DEFAULT_PATH value
804    // If the value (treated as a file path) doesn't exist on disk, use the fallback.
805    // Falls back to default if value is also null/empty.
806    if markers.contains(&"fallback".to_string()) {
807        let fb_idx = markers.iter().position(|m| m == "fallback").unwrap();
808        let default_val = markers.get(fb_idx + 1).cloned().unwrap_or_default();
809        let use_fallback = match map.get(key) {
810            None | Some(Value::Null) => true,
811            Some(Value::String(s)) if s.is_empty() => true,
812            Some(Value::String(s)) => {
813                let base = options.base_path.as_deref().unwrap_or(".");
814                match jail_path(base, s) {
815                    Ok(safe) => !safe.exists(),
816                    Err(_) => true, // path escapes jail → treat as missing → use fallback
817                }
818            }
819            _ => false,
820        };
821        if use_fallback && !default_val.is_empty() {
822            map.insert(key.to_string(), Value::String(default_val));
823        }
824    }
825
826    // ── :once ──
827    // Syntax: key:once  or  key:once:uuid  or  key:once:random  or  key:once:timestamp
828    // Generates a value once and persists it in a .synx.lock sidecar file.
829    if markers.contains(&"once".to_string()) {
830        let once_idx = markers.iter().position(|m| m == "once").unwrap();
831        let gen_type = markers.get(once_idx + 1).map(|s| s.as_str()).unwrap_or("uuid");
832        let lock_path = options.base_path.as_deref()
833            .map(|b| std::path::Path::new(b).join(".synx.lock"))
834            .unwrap_or_else(|| std::path::Path::new(".synx.lock").to_path_buf());
835
836        // Try to read existing value from lock file
837        let existing = read_lock_value(&lock_path, key);
838        if let Some(locked) = existing {
839            map.insert(key.to_string(), Value::String(locked));
840        } else {
841            let generated = match gen_type {
842                "uuid" => rng::generate_uuid(),
843                "timestamp" => std::time::SystemTime::now()
844                    .duration_since(std::time::UNIX_EPOCH)
845                    .unwrap_or_default()
846                    .as_secs()
847                    .to_string(),
848                "random" => rng::random_usize(u32::MAX as usize).to_string(),
849                _ => rng::generate_uuid(),
850            };
851            write_lock_value(&lock_path, key, &generated);
852            map.insert(key.to_string(), Value::String(generated));
853        }
854    }
855
856    // ── :version ──
857    // Syntax: key:version:OP:REQUIRED value
858    // Compares the value (current version) against REQUIRED using OP (>=, <=, >, <, ==, !=).
859    // Returns a bool.
860    if markers.contains(&"version".to_string()) {
861        if let Some(Value::String(current_ver)) = map.get(key) {
862            let ver_idx = markers.iter().position(|m| m == "version").unwrap();
863            let op = markers.get(ver_idx + 1).map(|s| s.as_str()).unwrap_or(">=");
864            let required = markers.get(ver_idx + 2).cloned().unwrap_or_default();
865            let result = compare_versions(current_ver, op, &required);
866            map.insert(key.to_string(), Value::Bool(result));
867        }
868    }
869
870    // ── :watch ──
871    // Syntax: key:watch:KEY_PATH ./file.json  (or ./file.synx)
872    // Reads the referenced file at parse time. Optionally extracts a key path (JSON/SYNX).
873    if markers.contains(&"watch".to_string()) {
874        if let Some(Value::String(file_path)) = map.get(key) {
875            let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
876            if options._include_depth >= max_depth {
877                map.insert(
878                    key.to_string(),
879                    Value::String(format!("WATCH_ERR: max include depth ({}) exceeded", max_depth)),
880                );
881                return;
882            }
883            let base = options.base_path.as_deref().unwrap_or(".");
884            let full = match jail_path(base, file_path) {
885                Ok(p) => p,
886                Err(e) => {
887                    map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
888                    return;
889                }
890            };
891            if let Err(e) = check_file_size(&full) {
892                map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
893                return;
894            }
895            let watch_idx = markers.iter().position(|m| m == "watch").unwrap();
896            let key_path = markers.get(watch_idx + 1).cloned();
897
898            match std::fs::read_to_string(&full) {
899                Ok(content) => {
900                    let value = if let Some(ref kp) = key_path {
901                        extract_from_file_content(&content, kp, full.extension().and_then(|e| e.to_str()).unwrap_or("")).unwrap_or(Value::Null)
902                    } else {
903                        Value::String(content.trim().to_string())
904                    };
905                    map.insert(key.to_string(), value);
906                }
907                Err(e) => {
908                    map.insert(key.to_string(), Value::String(format!("WATCH_ERR: {}", e)));
909                }
910            }
911        }
912    }
913
914    // ── Constraint validation (always last, after all markers resolved) ──
915    if let Some(ref c) = meta.constraints {
916        validate_constraints(map, key, c);
917    }
918}
919
920// ─── Constraint enforcement ───────────────────────────────
921
922fn validate_constraints(map: &mut HashMap<String, Value>, key: &str, c: &Constraints) {
923    let val = match map.get(key) {
924        Some(v) => v.clone(),
925        None => {
926            if c.required {
927                map.insert(key.to_string(), Value::String(
928                    format!("CONSTRAINT_ERR: '{}' is required", key),
929                ));
930            }
931            return;
932        }
933    };
934
935    // required
936    if c.required {
937        let empty = matches!(val, Value::Null)
938            || matches!(&val, Value::String(s) if s.is_empty());
939        if empty {
940            map.insert(key.to_string(), Value::String(
941                format!("CONSTRAINT_ERR: '{}' is required", key),
942            ));
943            return;
944        }
945    }
946
947    // type check
948    if let Some(ref type_name) = c.type_name {
949        let ok = match type_name.as_str() {
950            "int"    => matches!(val, Value::Int(_)),
951            "float"  => matches!(val, Value::Float(_) | Value::Int(_)),
952            "bool"   => matches!(val, Value::Bool(_)),
953            "string" => matches!(val, Value::String(_)),
954            _        => true,
955        };
956        if !ok {
957            map.insert(key.to_string(), Value::String(
958                format!("CONSTRAINT_ERR: '{}' expected type '{}'", key, type_name),
959            ));
960            return;
961        }
962    }
963
964    // enum check
965    if let Some(ref enum_vals) = c.enum_values {
966        let val_str = match &val {
967            Value::String(s) => s.clone(),
968            Value::Int(n)    => n.to_string(),
969            Value::Float(f)  => f.to_string(),
970            Value::Bool(b)   => b.to_string(),
971            _                => String::new(),
972        };
973        if !enum_vals.contains(&val_str) {
974            map.insert(key.to_string(), Value::String(
975                format!("CONSTRAINT_ERR: '{}' must be one of [{}]", key, enum_vals.join("|")),
976            ));
977            return;
978        }
979    }
980
981    // min / max  (numbers: value range; strings: length range)
982    let num = match &val {
983        Value::Int(n)    => Some(*n as f64),
984        Value::Float(f)  => Some(*f),
985        Value::String(s) if c.min.is_some() || c.max.is_some() => Some(s.len() as f64),
986        _                => None,
987    };
988    if let Some(n) = num {
989        if let Some(min) = c.min {
990            if n < min {
991                map.insert(key.to_string(), Value::String(
992                    format!("CONSTRAINT_ERR: '{}' value {} is below min {}", key, n, min),
993                ));
994                return;
995            }
996        }
997        if let Some(max) = c.max {
998            if n > max {
999                map.insert(key.to_string(), Value::String(
1000                    format!("CONSTRAINT_ERR: '{}' value {} exceeds max {}", key, n, max),
1001                ));
1002                return;
1003            }
1004        }
1005    }
1006    // Note: `pattern` validation is intentionally skipped in the Rust engine
1007    // (adding a regex crate would bloat the dependency tree).  Pattern
1008    // validation is fully implemented in the JS engine.
1009}
1010
1011// ─── New-marker helpers ───────────────────────────────────
1012
1013/// Apply a printf-style format pattern to a value.
1014fn apply_format_pattern(pattern: &str, value: &Value) -> String {
1015    match value {
1016        Value::Int(n) => {
1017            if pattern.contains('d') || pattern.contains('i') {
1018                format_int_pattern(pattern, *n)
1019            } else if pattern.contains('f') || pattern.contains('e') {
1020                format_float_pattern(pattern, *n as f64)
1021            } else {
1022                n.to_string()
1023            }
1024        }
1025        Value::Float(f) => {
1026            if pattern.contains('f') || pattern.contains('e') {
1027                format_float_pattern(pattern, *f)
1028            } else {
1029                format_number(*f)
1030            }
1031        }
1032        Value::String(s) => s.clone(),
1033        other => value_to_string(other),
1034    }
1035}
1036
1037fn format_int_pattern(pattern: &str, n: i64) -> String {
1038    if let Some(s) = pattern.strip_prefix('%') {
1039        if let Some(inner) = s.strip_suffix('d').or_else(|| s.strip_suffix('i')) {
1040            if let Some(w) = inner.strip_prefix('0') {
1041                if let Ok(width) = w.parse::<usize>() {
1042                    return format!("{:0>width$}", n, width = width);
1043                }
1044            }
1045            if let Ok(width) = inner.parse::<usize>() {
1046                return format!("{:>width$}", n, width = width);
1047            }
1048        }
1049    }
1050    n.to_string()
1051}
1052
1053fn format_float_pattern(pattern: &str, f: f64) -> String {
1054    if let Some(s) = pattern.strip_prefix('%') {
1055        if let Some(inner) = s.strip_suffix('f').or_else(|| s.strip_suffix('e')) {
1056            if let Some(prec_s) = inner.strip_prefix('.') {
1057                if let Ok(prec) = prec_s.parse::<usize>() {
1058                    return format!("{:.prec$}", f, prec = prec);
1059                }
1060            }
1061        }
1062    }
1063    f.to_string()
1064}
1065
1066/// Read a persisted value from the .synx.lock file.
1067fn read_lock_value(lock_path: &std::path::Path, key: &str) -> Option<String> {
1068    let content = std::fs::read_to_string(lock_path).ok()?;
1069    for line in content.lines() {
1070        if let Some(rest) = line.strip_prefix(key) {
1071            if rest.starts_with(' ') {
1072                return Some(rest.trim_start().to_string());
1073            }
1074        }
1075    }
1076    None
1077}
1078
1079/// Write/update a key value pair in the .synx.lock file.
1080fn write_lock_value(lock_path: &std::path::Path, key: &str, value: &str) {
1081    let mut lines: Vec<String> = std::fs::read_to_string(lock_path)
1082        .unwrap_or_default()
1083        .lines()
1084        .map(|l| l.to_string())
1085        .collect();
1086
1087    let new_line = format!("{} {}", key, value);
1088    let mut found = false;
1089    for line in lines.iter_mut() {
1090        if line.starts_with(key) && line[key.len()..].starts_with(' ') {
1091            *line = new_line.clone();
1092            found = true;
1093            break;
1094        }
1095    }
1096    if !found {
1097        lines.push(new_line);
1098    }
1099    let _ = std::fs::write(lock_path, lines.join("\n") + "\n");
1100}
1101
1102/// Compare two version strings using a comparison operator.
1103fn compare_versions(current: &str, op: &str, required: &str) -> bool {
1104    let parse_ver = |s: &str| -> Vec<u64> {
1105        s.split('.').filter_map(|p| p.parse().ok()).collect()
1106    };
1107    let cv = parse_ver(current);
1108    let rv = parse_ver(required);
1109    let len = cv.len().max(rv.len());
1110    let mut ord = std::cmp::Ordering::Equal;
1111    for i in 0..len {
1112        let a = cv.get(i).copied().unwrap_or(0);
1113        let b = rv.get(i).copied().unwrap_or(0);
1114        if a != b {
1115            ord = a.cmp(&b);
1116            break;
1117        }
1118    }
1119    match op {
1120        ">=" => ord != std::cmp::Ordering::Less,
1121        "<=" => ord != std::cmp::Ordering::Greater,
1122        ">"  => ord == std::cmp::Ordering::Greater,
1123        "<"  => ord == std::cmp::Ordering::Less,
1124        "==" | "=" => ord == std::cmp::Ordering::Equal,
1125        "!=" => ord != std::cmp::Ordering::Equal,
1126        _ => false,
1127    }
1128}
1129
1130fn allow_spam_access(bucket_key: &str, max_calls: usize, window_sec: u64) -> bool {
1131    let now = Instant::now();
1132    let window = Duration::from_secs(window_sec.max(1));
1133
1134    let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1135    let mut guard = match buckets.lock() {
1136        Ok(g) => g,
1137        Err(poisoned) => poisoned.into_inner(),
1138    };
1139
1140    let calls = guard.entry(bucket_key.to_string()).or_default();
1141    calls.retain(|ts| now.duration_since(*ts) <= window);
1142
1143    if calls.len() >= max_calls {
1144        return false;
1145    }
1146
1147    calls.push(now);
1148    true
1149}
1150
1151#[cfg(test)]
1152fn clear_spam_buckets() {
1153    let buckets = SPAM_BUCKETS.get_or_init(|| Mutex::new(HashMap::new()));
1154    if let Ok(mut guard) = buckets.lock() {
1155        guard.clear();
1156    }
1157}
1158
1159/// Extract a value from file content by key path (JSON dot-path or SYNX key).
1160fn extract_from_file_content(content: &str, key_path: &str, ext: &str) -> Option<Value> {
1161    if ext == "json" {
1162        let search = format!("\"{}\"", key_path);
1163        if let Some(pos) = content.find(&search) {
1164            let after = content[pos + search.len()..].trim_start();
1165            if let Some(rest) = after.strip_prefix(':') {
1166                let val_s = rest.trim_start()
1167                    .trim_end_matches(',')
1168                    .trim_end_matches('}')
1169                    .trim()
1170                    .trim_matches('"');
1171                return Some(cast_primitive(val_s));
1172            }
1173        }
1174        None
1175    } else {
1176        for line in content.lines() {
1177            let trimmed = line.trim_start();
1178            if trimmed.starts_with(key_path) {
1179                let rest = &trimmed[key_path.len()..];
1180                if rest.starts_with(' ') {
1181                    return Some(cast_primitive(rest.trim_start()));
1182                }
1183            }
1184        }
1185        None
1186    }
1187}
1188
1189// ─── Helpers ─────────────────────────────────────────────
1190
1191fn cast_primitive(val: &str) -> Value {
1192    // Quoted strings preserve literal value
1193    if val.len() >= 2 {
1194        let bytes = val.as_bytes();
1195        if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
1196            || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'')
1197        {
1198            return Value::String(val[1..val.len() - 1].to_string());
1199        }
1200    }
1201    match val {
1202        "true" => Value::Bool(true),
1203        "false" => Value::Bool(false),
1204        "null" => Value::Null,
1205        _ => {
1206            if let Ok(i) = val.parse::<i64>() {
1207                Value::Int(i)
1208            } else if let Ok(f) = val.parse::<f64>() {
1209                Value::Float(f)
1210            } else {
1211                Value::String(val.to_string())
1212            }
1213        }
1214    }
1215}
1216
1217fn delimiter_from_keyword(keyword: &str) -> String {
1218    match keyword {
1219        "space" => " ".to_string(),
1220        "pipe" => "|".to_string(),
1221        "dash" => "-".to_string(),
1222        "dot" => ".".to_string(),
1223        "semi" => ";".to_string(),
1224        "tab" => "\t".to_string(),
1225        "slash" => "/".to_string(),
1226        other => other.to_string(),
1227    }
1228}
1229
1230fn value_as_number(v: &Value) -> Option<f64> {
1231    match v {
1232        Value::Int(n) => Some(*n as f64),
1233        Value::Float(f) => Some(*f),
1234        _ => None,
1235    }
1236}
1237
1238fn value_to_string(v: &Value) -> String {
1239    match v {
1240        Value::String(s) => s.clone(),
1241        Value::Int(n) => n.to_string(),
1242        Value::Float(f) => format_number(*f as f64),
1243        Value::Bool(b) => b.to_string(),
1244        Value::Null => "null".to_string(),
1245        Value::Secret(s) => s.clone(),
1246        Value::Array(_) | Value::Object(_) => String::new(),
1247    }
1248}
1249
1250fn format_number(n: f64) -> String {
1251    if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
1252        (n as i64).to_string()
1253    } else {
1254        n.to_string()
1255    }
1256}
1257
1258/// Replace whole-word occurrences of `word` with `replacement`.
1259fn replace_word(haystack: &str, word: &str, replacement: &str) -> String {
1260    let word_bytes = word.as_bytes();
1261    let word_len = word_bytes.len();
1262    let hay_bytes = haystack.as_bytes();
1263    let hay_len = hay_bytes.len();
1264
1265    if word_len > hay_len {
1266        return haystack.to_string();
1267    }
1268
1269    let mut result = String::with_capacity(hay_len);
1270    let mut i = 0;
1271
1272    while i <= hay_len - word_len {
1273        if &hay_bytes[i..i + word_len] == word_bytes {
1274            let before_ok = i == 0 || !is_word_char(hay_bytes[i - 1]);
1275            let after_ok = i + word_len >= hay_len || !is_word_char(hay_bytes[i + word_len]);
1276            if before_ok && after_ok {
1277                result.push_str(replacement);
1278                i += word_len;
1279                continue;
1280            }
1281        }
1282        result.push(hay_bytes[i] as char);
1283        i += 1;
1284    }
1285    while i < hay_len {
1286        result.push(hay_bytes[i] as char);
1287        i += 1;
1288    }
1289    result
1290}
1291
1292fn is_word_char(b: u8) -> bool {
1293    b.is_ascii_alphanumeric() || b == b'_'
1294}
1295
1296fn weighted_random(items: &[Value], weights: &[f64]) -> Value {
1297    let mut w: Vec<f64> = weights.to_vec();
1298    if w.len() < items.len() {
1299        let assigned: f64 = w.iter().sum();
1300        // If the explicit weights already exceed 100, give unassigned items
1301        // the same average weight as the assigned ones so they remain visible.
1302        // If there is room left under 100, distribute the remainder equally.
1303        let per_item = if assigned < 100.0 {
1304            (100.0 - assigned) / (items.len() - w.len()) as f64
1305        } else {
1306            assigned / w.len() as f64
1307        };
1308        while w.len() < items.len() {
1309            w.push(per_item);
1310        }
1311    }
1312    let total: f64 = w.iter().sum();
1313    if total <= 0.0 {
1314        return items[rng::random_usize(items.len())].clone();
1315    }
1316
1317    let rand_val = rng::random_f64_01();
1318    let mut cumulative = 0.0;
1319    for (i, item) in items.iter().enumerate() {
1320        cumulative += w[i] / total;
1321        if rand_val <= cumulative {
1322            return item.clone();
1323        }
1324    }
1325    items.last().cloned().unwrap_or(Value::Null)
1326}
1327
1328// ─── Inheritance pre-pass ─────────────────────────────────
1329
1330fn apply_inheritance(root: &mut Value, metadata: &HashMap<String, MetaMap>) {
1331    let root_meta = match metadata.get("") {
1332        Some(m) => m.clone(),
1333        None => return,
1334    };
1335
1336    let root_map = match root.as_object_mut() {
1337        Some(m) => m as *mut HashMap<String, Value>,
1338        None => return,
1339    };
1340
1341    // Collect inherit targets: child_key → [parent1, parent2, ...]
1342    let mut inherits: Vec<(String, Vec<String>)> = Vec::new();
1343    for (key, meta) in &root_meta {
1344        if meta.markers.contains(&"inherit".to_string()) {
1345            let idx = meta.markers.iter().position(|m| m == "inherit").unwrap();
1346            // All markers after "inherit" are parent names (multi-parent support)
1347            let parents: Vec<String> = meta.markers[idx + 1..].to_vec();
1348            if !parents.is_empty() {
1349                inherits.push((key.clone(), parents));
1350            }
1351        }
1352    }
1353
1354    let map = unsafe { &mut *root_map };
1355    for (child_key, parents) in &inherits {
1356        // Merge parents left-to-right: first parent is base, each subsequent overrides
1357        let mut merged: HashMap<String, Value> = HashMap::new();
1358        for parent_name in parents {
1359            if let Some(Value::Object(p)) = map.get(parent_name) {
1360                for (k, v) in p {
1361                    merged.insert(k.clone(), v.clone());
1362                }
1363            }
1364        }
1365        // Child fields override all parents
1366        if let Some(Value::Object(c)) = map.get(child_key) {
1367            for (k, v) in c {
1368                merged.insert(k.clone(), v.clone());
1369            }
1370        }
1371        map.insert(child_key.clone(), Value::Object(merged));
1372    }
1373}
1374
1375fn deep_get(root: &Value, path: &str) -> Option<Value> {
1376    // Try direct key
1377    if let Value::Object(map) = root {
1378        if let Some(val) = map.get(path) {
1379            return Some(val.clone());
1380        }
1381    }
1382    // Dot-path traversal
1383    let parts: Vec<&str> = path.split('.').collect();
1384    let mut current = root;
1385    for part in parts {
1386        match current {
1387            Value::Object(map) => match map.get(part) {
1388                Some(v) => current = v,
1389                None => return None,
1390            },
1391            _ => return None,
1392        }
1393    }
1394    Some(current.clone())
1395}
1396
1397/// Resolve {placeholder} references in a template string.
1398/// Supports: {key}, {key.nested}, {key:alias}, {key:include}
1399fn resolve_interpolation(
1400    tpl: &str,
1401    root: &Value,
1402    local_map: &HashMap<String, Value>,
1403    includes: &HashMap<String, Value>,
1404) -> String {
1405    let bytes = tpl.as_bytes();
1406    let len = bytes.len();
1407    let mut result = String::with_capacity(len);
1408    let mut i = 0;
1409
1410    while i < len {
1411        if bytes[i] == b'{' {
1412            if let Some(close) = tpl[i + 1..].find('}') {
1413                let inner = &tpl[i + 1..i + 1 + close];
1414                // Check for scope separator ':'
1415                if let Some(colon) = inner.find(':') {
1416                    let ref_name = &inner[..colon];
1417                    let scope = &inner[colon + 1..];
1418                    // Valid ref name?
1419                    if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1420                        let resolved = if scope == "include" {
1421                            // {key:include} — first/only include
1422                            if includes.len() == 1 {
1423                                let first = includes.values().next().unwrap();
1424                                deep_get(first, ref_name)
1425                            } else {
1426                                None
1427                            }
1428                        } else {
1429                            // {key:alias} — look up by alias
1430                            includes.get(scope).and_then(|inc| deep_get(inc, ref_name))
1431                        };
1432                        if let Some(val) = resolved {
1433                            result.push_str(&value_to_string(&val));
1434                        } else {
1435                            result.push('{');
1436                            result.push_str(inner);
1437                            result.push('}');
1438                        }
1439                        i += 2 + close;
1440                        continue;
1441                    }
1442                } else {
1443                    // {key} — local
1444                    let ref_name = inner;
1445                    if ref_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '.') {
1446                        let resolved = deep_get(root, ref_name).or_else(|| {
1447                            local_map.get(ref_name).cloned()
1448                        });
1449                        if let Some(val) = resolved {
1450                            result.push_str(&value_to_string(&val));
1451                        } else {
1452                            result.push('{');
1453                            result.push_str(ref_name);
1454                            result.push('}');
1455                        }
1456                        i += 2 + close;
1457                        continue;
1458                    }
1459                }
1460            }
1461        }
1462        result.push(bytes[i] as char);
1463        i += 1;
1464    }
1465    result
1466}
1467
1468/// Load !include files into a map<alias, Value>.
1469fn load_includes(
1470    directives: &[IncludeDirective],
1471    options: &Options,
1472) -> HashMap<String, Value> {
1473    let mut map = HashMap::new();
1474    let base = options.base_path.as_deref().unwrap_or(".");
1475    let max_depth = options.max_include_depth.unwrap_or(DEFAULT_MAX_INCLUDE_DEPTH);
1476    if options._include_depth >= max_depth {
1477        return map;
1478    }
1479    for inc in directives {
1480        let full = match jail_path(base, &inc.path) {
1481            Ok(p) => p,
1482            Err(_) => continue,
1483        };
1484        if check_file_size(&full).is_err() {
1485            continue;
1486        }
1487        if let Ok(text) = std::fs::read_to_string(&full) {
1488            let mut included = parser::parse(&text);
1489            if included.mode == Mode::Active {
1490                let mut child_opts = options.clone();
1491                child_opts._include_depth += 1;
1492                if let Some(parent) = full.parent() {
1493                    child_opts.base_path = Some(parent.to_string_lossy().into_owned());
1494                }
1495                resolve(&mut included, &child_opts);
1496            }
1497            map.insert(inc.alias.clone(), included.root);
1498        }
1499    }
1500    map
1501}
1502
1503// ─── Type validation ──────────────────────────────────────
1504
1505/// Build a global type registry from all metadata.
1506/// Maps field name → expected type (e.g., "hp" → "int").
1507fn build_type_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, String> {
1508    let mut registry: HashMap<String, String> = HashMap::new();
1509
1510    for meta_map in metadata.values() {
1511        for (key, meta) in meta_map {
1512            if let Some(ref type_hint) = meta.type_hint {
1513                // If type already registered, check for conflict
1514                if let Some(existing) = registry.get(key) {
1515                    if existing != type_hint {
1516                        // Type conflict: same field defined with different types
1517                        // For now, first definition wins; could also log error
1518                    }
1519                } else {
1520                    registry.insert(key.clone(), type_hint.clone());
1521                }
1522            }
1523        }
1524    }
1525
1526    registry
1527}
1528
1529/// Build a global constraint registry from all metadata.
1530/// Maps field name → merged constraints from [] declarations.
1531fn build_constraint_registry(metadata: &HashMap<String, MetaMap>) -> HashMap<String, Constraints> {
1532    let mut registry: HashMap<String, Constraints> = HashMap::new();
1533
1534    for meta_map in metadata.values() {
1535        for (key, meta) in meta_map {
1536            if let Some(ref constraints) = meta.constraints {
1537                registry
1538                    .entry(key.clone())
1539                    .and_modify(|existing| merge_constraints(existing, constraints))
1540                    .or_insert_with(|| constraints.clone());
1541            }
1542        }
1543    }
1544
1545    registry
1546}
1547
1548/// Merge constraints when the same field is declared multiple times.
1549/// Strategy is intentionally strict to keep schemas consistent across templates.
1550fn merge_constraints(base: &mut Constraints, incoming: &Constraints) {
1551    if incoming.required {
1552        base.required = true;
1553    }
1554    if incoming.readonly {
1555        base.readonly = true;
1556    }
1557
1558    // Stricter numeric bounds win.
1559    base.min = match (base.min, incoming.min) {
1560        (Some(a), Some(b)) => Some(a.max(b)),
1561        (None, Some(b)) => Some(b),
1562        (a, None) => a,
1563    };
1564    base.max = match (base.max, incoming.max) {
1565        (Some(a), Some(b)) => Some(a.min(b)),
1566        (None, Some(b)) => Some(b),
1567        (a, None) => a,
1568    };
1569
1570    // Keep first non-empty type/pattern/enum declaration.
1571    if base.type_name.is_none() {
1572        base.type_name = incoming.type_name.clone();
1573    }
1574    if base.pattern.is_none() {
1575        base.pattern = incoming.pattern.clone();
1576    }
1577    if base.enum_values.is_none() {
1578        base.enum_values = incoming.enum_values.clone();
1579    }
1580}
1581
1582/// Validate [] constraints recursively for all object fields that have
1583/// a registered constraint rule.
1584fn validate_field_constraints(value: &mut Value, registry: &HashMap<String, Constraints>) {
1585    if let Value::Object(ref mut map) = value {
1586        let keys: Vec<String> = map.keys().cloned().collect();
1587        for key in &keys {
1588            if let Some(constraints) = registry.get(key) {
1589                validate_constraints(map, key, constraints);
1590            }
1591
1592            if let Some(child) = map.get_mut(key) {
1593                match child {
1594                    Value::Object(_) => validate_field_constraints(child, registry),
1595                    Value::Array(arr) => {
1596                        for item in arr.iter_mut() {
1597                            if let Value::Object(_) = item {
1598                                validate_field_constraints(item, registry);
1599                            }
1600                        }
1601                    }
1602                    _ => {}
1603                }
1604            }
1605        }
1606    }
1607}
1608
1609/// Validate that all values in the tree match their registered types.
1610fn validate_field_types(value: &mut Value, registry: &HashMap<String, String>, path: &str) {
1611    match value {
1612        Value::Object(ref mut map) => {
1613            let keys: Vec<String> = map.keys().cloned().collect();
1614            for key in &keys {
1615                if let Some(expected_type) = registry.get(key) {
1616                    if let Some(val) = map.get(key) {
1617                        if !value_matches_type(val, expected_type) {
1618                            // Type mismatch: replace with error string
1619                            let current_type = value_type_name(val);
1620                            map.insert(key.clone(), Value::String(
1621                                format!("TYPE_ERR: '{}' expected {} but got {}", key, expected_type, current_type)
1622                            ));
1623                        }
1624                    }
1625                }
1626                
1627                // Recurse into nested objects and arrays
1628                if let Some(child) = map.get_mut(key) {
1629                    match child {
1630                        Value::Object(_) => {
1631                            let child_path = if path.is_empty() {
1632                                key.clone()
1633                            } else {
1634                                format!("{}.{}", path, key)
1635                            };
1636                            validate_field_types(child, registry, &child_path);
1637                        }
1638                        Value::Array(ref mut arr) => {
1639                            for item in arr.iter_mut() {
1640                                if let Value::Object(_) = item {
1641                                    validate_field_types(item, registry, path);
1642                                }
1643                            }
1644                        }
1645                        _ => {}
1646                    }
1647                }
1648            }
1649        }
1650        _ => {}
1651    }
1652}
1653
1654/// Check if a value matches an expected type.
1655fn value_matches_type(value: &Value, expected_type: &str) -> bool {
1656    match expected_type {
1657        "int" => matches!(value, Value::Int(_)),
1658        "float" => matches!(value, Value::Float(_) | Value::Int(_)),
1659        "bool" => matches!(value, Value::Bool(_)),
1660        "string" => matches!(value, Value::String(_) | Value::Secret(_)),
1661        "array" => matches!(value, Value::Array(_)),
1662        "object" => matches!(value, Value::Object(_)),
1663        _ => true, // Unknown types are accepted
1664    }
1665}
1666
1667/// Get the human-readable name of a value's type.
1668fn value_type_name(value: &Value) -> String {
1669    match value {
1670        Value::Int(_) => "int".to_string(),
1671        Value::Float(_) => "float".to_string(),
1672        Value::Bool(_) => "bool".to_string(),
1673        Value::String(_) => "string".to_string(),
1674        Value::Secret(_) => "secret".to_string(),
1675        Value::Array(_) => "array".to_string(),
1676        Value::Object(_) => "object".to_string(),
1677        Value::Null => "null".to_string(),
1678    }
1679}
1680
1681// ─── CLDR plural rules ───────────────────────────────────
1682
1683/// Return the CLDR plural category for a given language and integer count.
1684/// Categories: "zero", "one", "two", "few", "many", "other".
1685fn plural_category(lang: &str, n: i64) -> &'static str {
1686    let abs_n = n.unsigned_abs();
1687    let n10 = abs_n % 10;
1688    let n100 = abs_n % 100;
1689
1690    match lang {
1691        // East Slavic: Russian, Ukrainian, Belarusian
1692        "ru" | "uk" | "be" => {
1693            if n10 == 1 && n100 != 11 {
1694                "one"
1695            } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
1696                "few"
1697            } else {
1698                "many"
1699            }
1700        }
1701        // West/South Slavic: Polish
1702        "pl" => {
1703            if n10 == 1 && n100 != 11 {
1704                "one"
1705            } else if (2..=4).contains(&n10) && !(12..=14).contains(&n100) {
1706                "few"
1707            } else {
1708                "many"
1709            }
1710        }
1711        // Czech, Slovak
1712        "cs" | "sk" => {
1713            if abs_n == 1 { "one" }
1714            else if (2..=4).contains(&abs_n) { "few" }
1715            else { "other" }
1716        }
1717        // Arabic
1718        "ar" => {
1719            if abs_n == 0 { "zero" }
1720            else if abs_n == 1 { "one" }
1721            else if abs_n == 2 { "two" }
1722            else if (3..=10).contains(&n100) { "few" }
1723            else if (11..=99).contains(&n100) { "many" }
1724            else { "other" }
1725        }
1726        // French, Portuguese (Brazilian) — 0 and 1 are "one"
1727        "fr" | "pt" => {
1728            if abs_n <= 1 { "one" } else { "other" }
1729        }
1730        // Japanese, Chinese, Korean, Vietnamese, Thai — no plural forms
1731        "ja" | "zh" | "ko" | "vi" | "th" => "other",
1732        // English, German, Spanish, Italian, Dutch, Swedish, Norwegian, Danish, etc.
1733        _ => {
1734            if abs_n == 1 { "one" } else { "other" }
1735        }
1736    }
1737}
1738
1739#[cfg(test)]
1740mod tests {
1741    use crate::{parse, Options, Value};
1742    use super::resolve;
1743
1744    #[test]
1745    fn test_ref_simple() {
1746        let mut r = parse("!active\nbase_rate 50\nquick_rate:ref base_rate");
1747        resolve(&mut r, &Options::default());
1748        let map = r.root.as_object().unwrap();
1749        assert_eq!(map["quick_rate"], Value::Int(50));
1750    }
1751
1752    #[test]
1753    fn test_ref_calc_shorthand() {
1754        let mut r = parse("!active\nbase_rate 50\ndouble_rate:ref:calc:*2 base_rate");
1755        resolve(&mut r, &Options::default());
1756        let map = r.root.as_object().unwrap();
1757        assert_eq!(map["double_rate"], Value::Int(100));
1758    }
1759
1760    #[test]
1761    fn test_inherit() {
1762        let mut r = parse("!active\n_base\n  weight 10\n  stackable true\nsteel:inherit:_base\n  weight 25\n  material metal");
1763        resolve(&mut r, &Options::default());
1764        let map = r.root.as_object().unwrap();
1765        assert!(!map.contains_key("_base"));
1766        let steel = map["steel"].as_object().unwrap();
1767        assert_eq!(steel["weight"], Value::Int(25));
1768        assert_eq!(steel["stackable"], Value::Bool(true));
1769        assert_eq!(steel["material"], Value::String("metal".into()));
1770    }
1771
1772    #[test]
1773    fn test_i18n_select_lang() {
1774        let mut r = parse("!active\ntitle:i18n\n  en Hello\n  ru Привет\n  de Hallo");
1775        let opts = Options { lang: Some("ru".into()), ..Default::default() };
1776        resolve(&mut r, &opts);
1777        let map = r.root.as_object().unwrap();
1778        assert_eq!(map["title"], Value::String("Привет".into()));
1779    }
1780
1781    #[test]
1782    fn test_i18n_fallback_en() {
1783        let mut r = parse("!active\ntitle:i18n\n  en Hello\n  ru Привет");
1784        let opts = Options { lang: Some("fr".into()), ..Default::default() };
1785        resolve(&mut r, &opts);
1786        let map = r.root.as_object().unwrap();
1787        assert_eq!(map["title"], Value::String("Hello".into()));
1788    }
1789
1790    #[test]
1791    fn test_auto_interpolation_simple() {
1792        let mut r = parse("!active\nname Wario\ngreeting Hello, {name}!");
1793        resolve(&mut r, &Options::default());
1794        let map = r.root.as_object().unwrap();
1795        assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
1796    }
1797
1798    #[test]
1799    fn test_auto_interpolation_nested() {
1800        let mut r = parse("!active\nserver\n  host localhost\n  port 8080\nurl http://{server.host}:{server.port}/api");
1801        resolve(&mut r, &Options::default());
1802        let map = r.root.as_object().unwrap();
1803        assert_eq!(map["url"], Value::String("http://localhost:8080/api".into()));
1804    }
1805
1806    #[test]
1807    fn test_template_legacy_still_works() {
1808        let mut r = parse("!active\nname Wario\ngreeting:template Hello, {name}!");
1809        resolve(&mut r, &Options::default());
1810        let map = r.root.as_object().unwrap();
1811        assert_eq!(map["greeting"], Value::String("Hello, Wario!".into()));
1812    }
1813
1814    #[test]
1815    fn test_type_validation() {
1816        // Test that type validation works: hp(int) defined in _base_unit,
1817        // then used in other places with correct type
1818        let mut r = parse(
1819            "!active\n\
1820            _base_unit\n  \
1821              hp(int) 100\n  \
1822              speed(float) 1.5\n\
1823            infantry:inherit:_base_unit\n  \
1824              name Infantry\n  \
1825              hp 80"
1826        );
1827        resolve(&mut r, &Options::default());
1828        let map = r.root.as_object().unwrap();
1829        
1830        // _base_unit should be removed (private)
1831        assert!(!map.contains_key("_base_unit"));
1832        
1833        // infantry should exist with correct types
1834        let infantry = map["infantry"].as_object().unwrap();
1835        assert_eq!(infantry["hp"], Value::Int(80));  // Correct: int
1836        assert_eq!(infantry["speed"], Value::Float(1.5));  // Correct: float
1837    }
1838
1839    #[test]
1840    fn test_type_validation_error() {
1841        // Test that type mismatch is detected and replaced with error
1842        let mut r = parse(
1843            "!active\n\
1844            _base_unit\n  \
1845              hp(int) 100\n\
1846            infantry:inherit:_base_unit\n  \
1847              hp hello"  // Type mismatch: string instead of int
1848        );
1849        resolve(&mut r, &Options::default());
1850        let map = r.root.as_object().unwrap();
1851        
1852        let infantry = map["infantry"].as_object().unwrap();
1853        // Should be replaced with error message
1854        if let Value::String(s) = &infantry["hp"] {
1855            assert!(s.contains("TYPE_ERR"));
1856        } else {
1857            panic!("Expected error string for type mismatch");
1858        }
1859    }
1860
1861    #[test]
1862    fn test_constraint_validation_inherited_range() {
1863        let mut r = parse(
1864            "!active\n\
1865            _base_unit\n  \
1866              hp[min:1, max:50000] 1000\n\
1867            infantry:inherit:_base_unit\n  \
1868              hp 60000"
1869        );
1870        resolve(&mut r, &Options::default());
1871        let map = r.root.as_object().unwrap();
1872        let infantry = map["infantry"].as_object().unwrap();
1873
1874        if let Value::String(s) = &infantry["hp"] {
1875            assert!(s.contains("CONSTRAINT_ERR"));
1876            assert!(s.contains("exceeds max"));
1877        } else {
1878            panic!("Expected constraint error string");
1879        }
1880    }
1881
1882    #[test]
1883    fn test_constraint_validation_required() {
1884        let mut r = parse(
1885            "!active\n\
1886            _base_unit\n  \
1887                            description[type:string, required] hello\n\
1888            scout:inherit:_base_unit\n  \
1889                            description null"
1890        );
1891        resolve(&mut r, &Options::default());
1892        let map = r.root.as_object().unwrap();
1893        let scout = map["scout"].as_object().unwrap();
1894
1895        if let Value::String(s) = &scout["description"] {
1896            assert!(s.contains("CONSTRAINT_ERR"));
1897            assert!(s.contains("required"));
1898        } else {
1899            panic!("Expected required-constraint error string");
1900        }
1901    }
1902
1903    #[test]
1904    fn test_multi_parent_inherit() {
1905        let mut r = parse(
1906            "!active\n\
1907            _movable\n  \
1908              speed 10\n  \
1909              can_move true\n\
1910            _damageable\n  \
1911              hp 100\n  \
1912              armor 5\n\
1913            tank:inherit:_movable:_damageable\n  \
1914              name Tank\n  \
1915              armor 20"
1916        );
1917        resolve(&mut r, &Options::default());
1918        let map = r.root.as_object().unwrap();
1919
1920        assert!(!map.contains_key("_movable"));
1921        assert!(!map.contains_key("_damageable"));
1922
1923        let tank = map["tank"].as_object().unwrap();
1924        assert_eq!(tank["speed"], Value::Int(10));        // from _movable
1925        assert_eq!(tank["can_move"], Value::Bool(true));   // from _movable
1926        assert_eq!(tank["hp"], Value::Int(100));           // from _damageable
1927        assert_eq!(tank["armor"], Value::Int(20));         // child overrides _damageable's 5
1928        assert_eq!(tank["name"], Value::String("Tank".into()));
1929    }
1930
1931    #[test]
1932    fn test_calc_dot_path() {
1933        let mut r = parse(
1934            "!active\n\
1935            stats\n  \
1936              base_hp 100\n  \
1937              multiplier 3\n\
1938            total_hp:calc stats.base_hp * stats.multiplier"
1939        );
1940        resolve(&mut r, &Options::default());
1941        let map = r.root.as_object().unwrap();
1942        assert_eq!(map["total_hp"], Value::Int(300));
1943    }
1944
1945    #[test]
1946    fn test_i18n_plural_en() {
1947        let mut r = parse(
1948            "!active\n\
1949            count 5\n\
1950            items:i18n:count\n  \
1951              en\n    \
1952                one item\n    \
1953                other items"
1954        );
1955        let opts = Options { lang: Some("en".into()), ..Default::default() };
1956        resolve(&mut r, &opts);
1957        let map = r.root.as_object().unwrap();
1958        assert_eq!(map["items"], Value::String("items".into()));
1959    }
1960
1961    #[test]
1962    fn test_i18n_plural_en_one() {
1963        let mut r = parse(
1964            "!active\n\
1965            count 1\n\
1966            items:i18n:count\n  \
1967              en\n    \
1968                one {count} item\n    \
1969                other {count} items"
1970        );
1971        let opts = Options { lang: Some("en".into()), ..Default::default() };
1972        resolve(&mut r, &opts);
1973        let map = r.root.as_object().unwrap();
1974        assert_eq!(map["items"], Value::String("1 item".into()));
1975    }
1976
1977    #[test]
1978    fn test_i18n_plural_ru() {
1979        let mut r = parse(
1980            "!active\n\
1981            count 3\n\
1982            items:i18n:count\n  \
1983              ru\n    \
1984                one предмет\n    \
1985                few предмета\n    \
1986                many предметов\n    \
1987                other предметов"
1988        );
1989        let opts = Options { lang: Some("ru".into()), ..Default::default() };
1990        resolve(&mut r, &opts);
1991        let map = r.root.as_object().unwrap();
1992        assert_eq!(map["items"], Value::String("предмета".into()));
1993    }
1994
1995    #[test]
1996    fn test_quoted_null_preserved() {
1997        let r = parse("status \"null\"\nenabled \"true\"\ncount \"42\"");
1998        let map = r.root.as_object().unwrap();
1999        assert_eq!(map["status"], Value::String("null".into()));
2000        assert_eq!(map["enabled"], Value::String("true".into()));
2001        assert_eq!(map["count"], Value::String("42".into()));
2002    }
2003
2004    #[test]
2005    fn test_unquoted_null_is_null() {
2006        let r = parse("status null\nenabled true\ncount 42");
2007        let map = r.root.as_object().unwrap();
2008        assert_eq!(map["status"], Value::Null);
2009        assert_eq!(map["enabled"], Value::Bool(true));
2010        assert_eq!(map["count"], Value::Int(42));
2011    }
2012
2013    #[test]
2014    fn test_spam_rate_limit_exceeded() {
2015        super::clear_spam_buckets();
2016
2017        let mut r1 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2018        resolve(&mut r1, &Options::default());
2019        let map1 = r1.root.as_object().unwrap();
2020        assert_eq!(map1["access"], Value::String("abc".into()));
2021
2022        let mut r2 = parse("!active\nsecret_token abc\naccess:spam:1:5 secret_token");
2023        resolve(&mut r2, &Options::default());
2024        let map2 = r2.root.as_object().unwrap();
2025        match &map2["access"] {
2026            Value::String(s) => assert!(s.starts_with("SPAM_ERR:")),
2027            _ => panic!("Expected SPAM_ERR string"),
2028        }
2029    }
2030
2031    #[test]
2032    fn test_spam_default_window_sec_is_one() {
2033        super::clear_spam_buckets();
2034
2035        let mut r = parse("!active\na 1\nx:spam:2 a");
2036        resolve(&mut r, &Options::default());
2037        let map = r.root.as_object().unwrap();
2038        assert_eq!(map["x"], Value::Int(1));
2039    }
2040
2041    #[test]
2042    fn test_deep_nesting_does_not_overflow() {
2043        // Build 600-level deep SYNX: each level has one child key
2044        let mut synx = String::from("!active\n");
2045        let mut indent = String::new();
2046        for i in 0..600 {
2047            synx.push_str(&format!("{}level_{}\n", indent, i));
2048            indent.push_str("  ");
2049        }
2050        synx.push_str(&format!("{}value deep\n", indent));
2051
2052        // Must not crash
2053        let mut result = parse(&synx);
2054        resolve(&mut result, &Default::default());
2055        assert!(matches!(result.root, Value::Object(_)));
2056
2057        // Walk down to level 510 (should resolve normally)
2058        let mut cur = &result.root;
2059        for i in 0..510 {
2060            match cur {
2061                Value::Object(map) => {
2062                    let key = format!("level_{}", i);
2063                    cur = map.get(&key).expect(&format!("key '{}' should exist", key));
2064                }
2065                _ => panic!("expected object at level {}", i),
2066            }
2067        }
2068        // At depth 510 we should still have an object (within limit)
2069        assert!(matches!(cur, Value::Object(_)), "level_510 should be Object");
2070
2071        // Walk down to level 512 — this is the limit, its children should be NESTING_ERR
2072        let mut cur2 = &result.root;
2073        for i in 0..512 {
2074            match cur2 {
2075                Value::Object(map) => {
2076                    let key = format!("level_{}", i);
2077                    cur2 = map.get(&key).expect(&format!("key '{}' should exist", key));
2078                }
2079                _ => break,
2080            }
2081        }
2082        // At or beyond depth 512, values should be NESTING_ERR strings
2083        if let Value::Object(map) = cur2 {
2084            for v in map.values() {
2085                if let Value::String(s) = v {
2086                    assert!(s.starts_with("NESTING_ERR:"), "expected NESTING_ERR at depth limit, got: {}", s);
2087                }
2088            }
2089        }
2090    }
2091
2092    #[test]
2093    fn test_circular_alias_returns_error() {
2094        let mut r = parse("!active\na:alias b\nb:alias a");
2095        resolve(&mut r, &Default::default());
2096        let root = r.root.as_object().unwrap();
2097        let a_val = root.get("a").unwrap();
2098        let b_val = root.get("b").unwrap();
2099        assert!(
2100            matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2101            "expected ALIAS_ERR for 'a', got: {:?}", a_val
2102        );
2103        assert!(
2104            matches!(b_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2105            "expected ALIAS_ERR for 'b', got: {:?}", b_val
2106        );
2107    }
2108
2109    #[test]
2110    fn test_self_alias_returns_error() {
2111        let mut r = parse("!active\na:alias a");
2112        resolve(&mut r, &Default::default());
2113        let root = r.root.as_object().unwrap();
2114        let a_val = root.get("a").unwrap();
2115        assert!(
2116            matches!(a_val, Value::String(s) if s.starts_with("ALIAS_ERR:")),
2117            "expected ALIAS_ERR for self-alias, got: {:?}", a_val
2118        );
2119    }
2120
2121    #[test]
2122    fn test_valid_alias_still_works() {
2123        let mut r = parse("!active\nbase 42\ncopy:alias base");
2124        resolve(&mut r, &Default::default());
2125        let root = r.root.as_object().unwrap();
2126        assert_eq!(root.get("copy"), Some(&Value::Int(42)));
2127    }
2128
2129    #[test]
2130    fn test_alias_to_string_valued_key_no_false_positive() {
2131        // 'a' holds a literal string "b". 'b' aliases 'a'.
2132        // b should resolve to "b" — NOT trigger ALIAS_ERR.
2133        let mut r = parse("!active\na b\nb:alias a");
2134        resolve(&mut r, &Default::default());
2135        let root = r.root.as_object().unwrap();
2136        assert_eq!(
2137            root.get("b"),
2138            Some(&Value::String("b".to_string())),
2139            "alias to a string-valued key should not produce ALIAS_ERR"
2140        );
2141    }
2142}