Skip to main content

state_engine/core/
parser.rs

1use std::string::{String, ToString};
2use std::vec::Vec;
3use std::format;
4
5use super::pool::DynamicPool;
6use super::fixed_bits;
7use super::codec;
8
9/// Generic value type for manifest parsing.
10/// Binding-agnostic — no serde, no std, no alloc beyond Vec/String.
11///
12/// Callers (crate/, wasi/, js/, php/) are responsible for converting
13/// their native format (YAML, JSON, etc.) into Value before calling parse().
14pub enum Value {
15    Mapping(Vec<(String, Value)>),
16    Scalar(String),
17    Null,
18}
19
20/// Thin record for a single loaded manifest file.
21/// Stores only the key_idx of the file root record in the shared keys vec.
22pub struct ParsedManifest {
23    pub file_key_idx: u16,
24}
25
26/// Parses a manifest value tree, appending into caller-owned vecs.
27/// Returns a `ParsedManifest` referencing the file root record's index.
28///
29/// - `keys`: Vec<u64> — fixed-bits key records
30/// - `values`: Vec<[u64; 2]> — fixed-bits value records
31/// - `path_map`: Vec<Vec<u16>> — path segment index sequences
32/// - `children_map`: Vec<Vec<u16>> — multi-child index lists
33///
34/// Index 0 of each vec is reserved as null by the caller.
35pub fn parse(
36    filename: &str,
37    root: Value,
38    dynamic: &mut DynamicPool,
39    keys: &mut Vec<u64>,
40    values: &mut Vec<[u64; 2]>,
41    path_map: &mut Vec<Vec<u16>>,
42    children_map: &mut Vec<Vec<u16>>,
43) -> Result<ParsedManifest, String> {
44    let Value::Mapping(mapping) = root else {
45        return Err("DSL root must be a mapping".to_string());
46    };
47
48    // filename root record (placeholder, child index filled below)
49    let dyn_idx = dynamic.intern(filename);
50    let mut file_record = fixed_bits::new();
51    file_record = fixed_bits::set(file_record, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC, dyn_idx as u64);
52    let file_idx = keys.len() as u16;
53    keys.push(file_record);
54
55    // traverse top-level keys
56    let mut child_indices: Vec<u16> = Vec::new();
57    for (key_str, value) in &mapping {
58        let child_idx = traverse_field_key(key_str, value, filename, &[], dynamic, keys, values, path_map, children_map)?;
59        child_indices.push(child_idx);
60    }
61
62    // update file record with children
63    let file_record = keys[file_idx as usize];
64    let file_record = match child_indices.len() {
65        0 => file_record,
66        1 => fixed_bits::set(file_record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, child_indices[0] as u64),
67        _ => {
68            let children_idx = children_map.len() as u16;
69            children_map.push(child_indices);
70            let r = fixed_bits::set(file_record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN, 1);
71            fixed_bits::set(r, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, children_idx as u64)
72        }
73    };
74    keys[file_idx as usize] = file_record;
75
76    Ok(ParsedManifest { file_key_idx: file_idx })
77}
78
79/// Traverses a field key node (non-meta key).
80/// `ancestors` excludes filename — only field key path segments (for qualify).
81fn traverse_field_key(
82    key_str: &str,
83    value: &Value,
84    filename: &str,
85    ancestors: &[&str],
86    dynamic: &mut DynamicPool,
87    keys: &mut Vec<u64>,
88    values: &mut Vec<[u64; 2]>,
89    path_map: &mut Vec<Vec<u16>>,
90    children_map: &mut Vec<Vec<u16>>,
91) -> Result<u16, String> {
92    let dyn_idx = dynamic.intern(key_str);
93    let mut record = fixed_bits::new();
94    record = fixed_bits::set(record, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT, fixed_bits::ROOT_NULL);
95    record = fixed_bits::set(record, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC, dyn_idx as u64);
96
97    let key_idx = keys.len() as u16;
98    keys.push(record);
99
100    let mut current: Vec<&str> = ancestors.to_vec();
101    current.push(key_str);
102
103    if let Value::Mapping(mapping) = value {
104        let mut child_indices: Vec<u16> = Vec::new();
105        let mut meta_indices: Vec<u16> = Vec::new();
106
107        for (k_str, v) in mapping {
108            if k_str.starts_with('_') {
109                let meta_idx = traverse_meta_key(k_str, v, filename, &current, dynamic, keys, values, path_map, children_map)?;
110                meta_indices.push(meta_idx);
111            } else {
112                let child_idx = traverse_field_key(k_str, v, filename, &current, dynamic, keys, values, path_map, children_map)?;
113                child_indices.push(child_idx);
114            }
115        }
116
117        let all_children: Vec<u16> = child_indices.iter()
118            .chain(meta_indices.iter())
119            .copied()
120            .collect();
121
122        let record = keys[key_idx as usize];
123        let record = match all_children.len() {
124            0 => record,
125            1 => fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, all_children[0] as u64),
126            _ => {
127                let children_idx = children_map.len() as u16;
128                children_map.push(all_children);
129                let r = fixed_bits::set(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN, 1);
130                fixed_bits::set(r, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, children_idx as u64)
131            }
132        };
133        keys[key_idx as usize] = record;
134    } else {
135        // scalar value → is_leaf
136        let val_idx = build_yaml_value(value, filename, ancestors, dynamic, values, path_map)?;
137        let record = keys[key_idx as usize];
138        let record = fixed_bits::set(record, fixed_bits::K_OFFSET_IS_LEAF, fixed_bits::K_MASK_IS_LEAF, 1);
139        let record = fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, val_idx as u64);
140        keys[key_idx as usize] = record;
141    }
142
143    Ok(key_idx)
144}
145
146/// Traverses a meta key node (_load, _store, _state).
147fn traverse_meta_key(
148    key_str: &str,
149    value: &Value,
150    filename: &str,
151    ancestors: &[&str],
152    dynamic: &mut DynamicPool,
153    keys: &mut Vec<u64>,
154    values: &mut Vec<[u64; 2]>,
155    path_map: &mut Vec<Vec<u16>>,
156    children_map: &mut Vec<Vec<u16>>,
157) -> Result<u16, String> {
158    let root_val = codec::root_encode(key_str);
159
160    let mut record = fixed_bits::new();
161    record = fixed_bits::set(record, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT, root_val);
162
163    let key_idx = keys.len() as u16;
164    keys.push(record);
165
166    if let Value::Mapping(mapping) = value {
167        let mut child_indices: Vec<u16> = Vec::new();
168
169        for (k_str, v) in mapping {
170            let child_idx = traverse_prop_key(k_str, v, filename, ancestors, dynamic, keys, values, path_map, children_map)?;
171            child_indices.push(child_idx);
172        }
173
174        let record = keys[key_idx as usize];
175        let record = match child_indices.len() {
176            0 => record,
177            1 => fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, child_indices[0] as u64),
178            _ => {
179                let children_idx = children_map.len() as u16;
180                children_map.push(child_indices);
181                let r = fixed_bits::set(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN, 1);
182                fixed_bits::set(r, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, children_idx as u64)
183            }
184        };
185        keys[key_idx as usize] = record;
186    }
187
188    Ok(key_idx)
189}
190
191/// Traverses a prop key node (client, key, ttl, table, connection, where, map, type).
192fn traverse_prop_key(
193    key_str: &str,
194    value: &Value,
195    filename: &str,
196    ancestors: &[&str],
197    dynamic: &mut DynamicPool,
198    keys: &mut Vec<u64>,
199    values: &mut Vec<[u64; 2]>,
200    path_map: &mut Vec<Vec<u16>>,
201    children_map: &mut Vec<Vec<u16>>,
202) -> Result<u16, String> {
203    let (prop_val, client_val) = if key_str == "client" {
204        (fixed_bits::PROP_NULL, codec::client_encode(
205            match value { Value::Scalar(s) => s.as_str(), _ => "" }
206        ))
207    } else {
208        (codec::prop_encode(key_str), fixed_bits::CLIENT_NULL)
209    };
210
211    let mut record = fixed_bits::new();
212    record = fixed_bits::set(record, fixed_bits::K_OFFSET_PROP, fixed_bits::K_MASK_PROP, prop_val);
213    record = fixed_bits::set(record, fixed_bits::K_OFFSET_CLIENT, fixed_bits::K_MASK_CLIENT, client_val);
214
215    if key_str == "type" {
216        let type_val = codec::type_encode(
217            match value { Value::Scalar(s) => s.as_str(), _ => "" }
218        );
219        record = fixed_bits::set(record, fixed_bits::K_OFFSET_TYPE, fixed_bits::K_MASK_TYPE, type_val);
220    }
221
222    let key_idx = keys.len() as u16;
223    keys.push(record);
224
225    if key_str == "map" {
226        if let Value::Mapping(mapping) = value {
227            let mut child_indices: Vec<u16> = Vec::new();
228            for (k_str, v) in mapping {
229                let child_idx = traverse_map_key(k_str, v, filename, ancestors, dynamic, keys, values, path_map)?;
230                child_indices.push(child_idx);
231            }
232            let record = keys[key_idx as usize];
233            let record = match child_indices.len() {
234                0 => record,
235                1 => fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, child_indices[0] as u64),
236                _ => {
237                    let children_idx = children_map.len() as u16;
238                    children_map.push(child_indices);
239                    let r = fixed_bits::set(record, fixed_bits::K_OFFSET_HAS_CHILDREN, fixed_bits::K_MASK_HAS_CHILDREN, 1);
240                    fixed_bits::set(r, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, children_idx as u64)
241                }
242            };
243            keys[key_idx as usize] = record;
244        }
245    } else if key_str != "client" {
246        let val_idx = build_yaml_value(value, filename, ancestors, dynamic, values, path_map)?;
247        let record = keys[key_idx as usize];
248        let record = fixed_bits::set(record, fixed_bits::K_OFFSET_IS_LEAF, fixed_bits::K_MASK_IS_LEAF, 1);
249        let record = fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, val_idx as u64);
250        keys[key_idx as usize] = record;
251    }
252
253    Ok(key_idx)
254}
255
256/// Traverses a map child key (is_path=true).
257fn traverse_map_key(
258    key_str: &str,
259    value: &Value,
260    filename: &str,
261    ancestors: &[&str],
262    dynamic: &mut DynamicPool,
263    keys: &mut Vec<u64>,
264    values: &mut Vec<[u64; 2]>,
265    path_map: &mut Vec<Vec<u16>>,
266) -> Result<u16, String> {
267    let qualified = build_qualified_path(filename, ancestors, key_str);
268    let seg_indices: Vec<u16> = qualified.split('.')
269        .map(|seg| dynamic.intern(seg))
270        .collect();
271    let path_idx = path_map.len() as u16;
272    path_map.push(seg_indices);
273
274    let mut record = fixed_bits::new();
275    record = fixed_bits::set(record, fixed_bits::K_OFFSET_IS_PATH, fixed_bits::K_MASK_IS_PATH, 1);
276    record = fixed_bits::set(record, fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC, path_idx as u64);
277
278    let val_idx = build_yaml_value(value, filename, ancestors, dynamic, values, path_map)?;
279    record = fixed_bits::set(record, fixed_bits::K_OFFSET_IS_LEAF, fixed_bits::K_MASK_IS_LEAF, 1);
280    record = fixed_bits::set(record, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD, val_idx as u64);
281
282    let key_idx = keys.len() as u16;
283    keys.push(record);
284    Ok(key_idx)
285}
286
287/// Builds a YAML value record ([u64; 2]) from a scalar or template string.
288fn build_yaml_value(
289    value: &Value,
290    filename: &str,
291    ancestors: &[&str],
292    dynamic: &mut DynamicPool,
293    values: &mut Vec<[u64; 2]>,
294    path_map: &mut Vec<Vec<u16>>,
295) -> Result<u16, String> {
296    let s = match value {
297        Value::Scalar(s) => s.clone(),
298        Value::Null      => return Ok(0),
299        Value::Mapping(_) => return Err("unexpected mapping as scalar value".to_string()),
300    };
301
302    let tokens = split_template(&s);
303    if tokens.len() > 6 {
304        return Err(format!("value '{}' has {} tokens, max 6", s, tokens.len()));
305    }
306    let is_template = tokens.len() > 1;
307
308    let mut vo = [0u64; 2];
309
310    if is_template {
311        vo[0] = fixed_bits::set(vo[0], fixed_bits::V_OFFSET_IS_TEMPLATE, fixed_bits::V_MASK_IS_TEMPLATE, 1);
312    }
313
314    const TOKEN_OFFSETS: [(u32, u32); 6] = [
315        (fixed_bits::V_OFFSET_T0_IS_PATH, fixed_bits::V_OFFSET_T0_DYNAMIC),
316        (fixed_bits::V_OFFSET_T1_IS_PATH, fixed_bits::V_OFFSET_T1_DYNAMIC),
317        (fixed_bits::V_OFFSET_T2_IS_PATH, fixed_bits::V_OFFSET_T2_DYNAMIC),
318        (fixed_bits::V_OFFSET_T3_IS_PATH, fixed_bits::V_OFFSET_T3_DYNAMIC),
319        (fixed_bits::V_OFFSET_T4_IS_PATH, fixed_bits::V_OFFSET_T4_DYNAMIC),
320        (fixed_bits::V_OFFSET_T5_IS_PATH, fixed_bits::V_OFFSET_T5_DYNAMIC),
321    ];
322
323    for (i, token) in tokens.iter().enumerate().take(6) {
324        let dyn_idx = if token.is_path {
325            let qualified = qualify_path(&token.text, filename, ancestors);
326            let seg_indices: Vec<u16> = qualified.split('.')
327                .map(|seg| dynamic.intern(seg))
328                .collect();
329            let path_idx = path_map.len() as u16;
330            path_map.push(seg_indices);
331            path_idx
332        } else {
333            dynamic.intern(&token.text)
334        };
335
336        let word = if i < 3 { 0 } else { 1 };
337        let (off_is_path, off_dynamic) = TOKEN_OFFSETS[i];
338        vo[word] = fixed_bits::set(vo[word], off_is_path, fixed_bits::V_MASK_IS_PATH, token.is_path as u64);
339        vo[word] = fixed_bits::set(vo[word], off_dynamic, fixed_bits::V_MASK_DYNAMIC, dyn_idx as u64);
340    }
341
342    let val_idx = values.len() as u16;
343    values.push(vo);
344    Ok(val_idx)
345}
346
347
348/// A single template token: either a literal string or a path placeholder.
349struct Token {
350    text: String,
351    is_path: bool,
352}
353
354/// Splits a string by `${}` placeholders into tokens.
355/// `"user:${session.id}"` → [Token("user:", false), Token("session.id", true)]
356fn split_template(s: &str) -> Vec<Token> {
357    let mut tokens = Vec::new();
358    let mut rest = s;
359
360    loop {
361        if let Some(start) = rest.find("${") {
362            if start > 0 {
363                tokens.push(Token { text: rest[..start].to_string(), is_path: false });
364            }
365            rest = &rest[start + 2..];
366            if let Some(end) = rest.find('}') {
367                tokens.push(Token { text: rest[..end].to_string(), is_path: true });
368                rest = &rest[end + 1..];
369            } else {
370                tokens.push(Token { text: rest.to_string(), is_path: false });
371                break;
372            }
373        } else {
374            if !rest.is_empty() {
375                tokens.push(Token { text: rest.to_string(), is_path: false });
376            }
377            break;
378        }
379    }
380
381    if tokens.is_empty() {
382        tokens.push(Token { text: s.to_string(), is_path: false });
383    }
384
385    tokens
386}
387
388/// Qualifies a placeholder path to an absolute path.
389fn qualify_path(path: &str, filename: &str, ancestors: &[&str]) -> String {
390    if path.contains('.') {
391        return path.to_string();
392    }
393    if ancestors.is_empty() {
394        format!("{}.{}", filename, path)
395    } else {
396        format!("{}.{}.{}", filename, ancestors.join("."), path)
397    }
398}
399
400/// Builds a qualified path string for map keys: `filename.ancestors.key_str`
401fn build_qualified_path(filename: &str, ancestors: &[&str], key_str: &str) -> String {
402    if ancestors.is_empty() {
403        format!("{}.{}", filename, key_str)
404    } else {
405        format!("{}.{}.{}", filename, ancestors.join("."), key_str)
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::fixed_bits;
413    use std::vec::Vec;
414
415    fn make_vecs() -> (DynamicPool, Vec<u64>, Vec<[u64; 2]>, Vec<Vec<u16>>, Vec<Vec<u16>>) {
416        (DynamicPool::new(), vec![0], vec![[0, 0]], vec![vec![]], vec![vec![]])
417    }
418
419    fn s(v: &str) -> Value { Value::Scalar(v.to_string()) }
420    fn m(pairs: Vec<(&str, Value)>) -> Value {
421        Value::Mapping(pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
422    }
423
424    // --- split_template ---
425
426    #[test]
427    fn test_split_template_static() {
428        let tokens = split_template("literal");
429        assert_eq!(tokens.len(), 1);
430        assert!(!tokens[0].is_path);
431        assert_eq!(tokens[0].text, "literal");
432    }
433
434    #[test]
435    fn test_split_template_path_only() {
436        let tokens = split_template("${connection.tenant}");
437        assert_eq!(tokens.len(), 1);
438        assert!(tokens[0].is_path);
439        assert_eq!(tokens[0].text, "connection.tenant");
440    }
441
442    #[test]
443    fn test_split_template_mixed() {
444        let tokens = split_template("user:${session.id}");
445        assert_eq!(tokens.len(), 2);
446        assert!(!tokens[0].is_path);
447        assert_eq!(tokens[0].text, "user:");
448        assert!(tokens[1].is_path);
449        assert_eq!(tokens[1].text, "session.id");
450    }
451
452    // --- qualify_path ---
453
454    #[test]
455    fn test_qualify_path_absolute() {
456        assert_eq!(qualify_path("connection.common", "cache", &["user"]), "connection.common");
457    }
458
459    #[test]
460    fn test_qualify_path_relative() {
461        assert_eq!(qualify_path("org_id", "cache", &["user"]), "cache.user.org_id");
462    }
463
464    #[test]
465    fn test_qualify_path_relative_no_ancestors() {
466        assert_eq!(qualify_path("org_id", "cache", &[]), "cache.org_id");
467    }
468
469    // --- parse: field key → ROOT_NULL ---
470
471    #[test]
472    fn test_field_key_root_is_null() {
473        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
474        let root = m(vec![("foo", m(vec![]))]);
475        let pm = parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
476
477        let file_rec = keys[pm.file_key_idx as usize];
478        let child_idx = fixed_bits::get(file_rec, fixed_bits::K_OFFSET_CHILD, fixed_bits::K_MASK_CHILD) as usize;
479        assert_eq!(fixed_bits::get(keys[child_idx], fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT), fixed_bits::ROOT_NULL);
480    }
481
482    // --- parse: meta key → ROOT bits ---
483
484    #[test]
485    fn test_meta_key_root_bits() {
486        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
487        let root = m(vec![("foo", m(vec![
488            ("_state", m(vec![("type", s("integer"))])),
489            ("_load",  m(vec![("client", s("InMemory")), ("key", s("k"))])),
490            ("_store", m(vec![("client", s("InMemory")), ("key", s("k"))])),
491        ]))]);
492        parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
493
494        let roots: Vec<u64> = keys.iter().map(|&r| fixed_bits::get(r, fixed_bits::K_OFFSET_ROOT, fixed_bits::K_MASK_ROOT)).collect();
495        assert!(roots.contains(&fixed_bits::ROOT_STATE));
496        assert!(roots.contains(&fixed_bits::ROOT_LOAD));
497        assert!(roots.contains(&fixed_bits::ROOT_STORE));
498    }
499
500    // --- parse: type encoding ---
501
502    #[test]
503    fn test_type_encoding() {
504        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
505        let root = m(vec![("foo", m(vec![
506            ("_state", m(vec![("type", s("integer"))])),
507        ]))]);
508        parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
509
510        let types: Vec<u64> = keys.iter().map(|&r| fixed_bits::get(r, fixed_bits::K_OFFSET_TYPE, fixed_bits::K_MASK_TYPE)).collect();
511        assert!(types.contains(&fixed_bits::TYPE_I64));
512    }
513
514    // --- parse: client encoding ---
515
516    #[test]
517    fn test_client_encoding() {
518        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
519        let root = m(vec![("foo", m(vec![
520            ("_store", m(vec![("client", s("KVS")), ("key", s("k")), ("ttl", s("3600"))])),
521        ]))]);
522        parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
523
524        let clients: Vec<u64> = keys.iter().map(|&r| fixed_bits::get(r, fixed_bits::K_OFFSET_CLIENT, fixed_bits::K_MASK_CLIENT)).collect();
525        assert!(clients.contains(&fixed_bits::CLIENT_KVS));
526    }
527
528    // --- parse: template value → is_template flag + path_map ---
529
530    #[test]
531    fn test_template_value() {
532        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
533        let root = m(vec![("foo", m(vec![
534            ("_store", m(vec![("client", s("KVS")), ("key", s("foo:${session.id}"))])),
535        ]))]);
536        parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
537
538        let has_template = values.iter().any(|&vo| fixed_bits::get(vo[0], fixed_bits::V_OFFSET_IS_TEMPLATE, fixed_bits::V_MASK_IS_TEMPLATE) == 1);
539        assert!(has_template);
540        assert!(path_map.len() > 1);
541    }
542
543    // --- parse: map key → path_map expansion ---
544
545    #[test]
546    fn test_map_key_path_expansion() {
547        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
548        let root = m(vec![("foo", m(vec![
549            ("_load", m(vec![
550                ("client", s("Env")),
551                ("map", m(vec![("host", s("DB_HOST")), ("port", s("DB_PORT"))])),
552            ])),
553        ]))]);
554        parse("f", root, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
555
556        // map keys produce is_path=1 records
557        let has_path = keys.iter().any(|&r| fixed_bits::get(r, fixed_bits::K_OFFSET_IS_PATH, fixed_bits::K_MASK_IS_PATH) == 1);
558        assert!(has_path);
559    }
560
561    // --- parse: two files → globally unique key indices ---
562
563    #[test]
564    fn test_two_files_unique_indices() {
565        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
566        let a = m(vec![("x", m(vec![]))]);
567        let b = m(vec![("y", m(vec![]))]);
568        let pm_a = parse("a", a, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
569        let pm_b = parse("b", b, &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).unwrap();
570
571        assert_ne!(pm_a.file_key_idx, pm_b.file_key_idx);
572
573        let dyn_a = fixed_bits::get(keys[pm_a.file_key_idx as usize], fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
574        let dyn_b = fixed_bits::get(keys[pm_b.file_key_idx as usize], fixed_bits::K_OFFSET_DYNAMIC, fixed_bits::K_MASK_DYNAMIC) as u16;
575        assert_eq!(dynamic.get(dyn_a), Some("a"));
576        assert_eq!(dynamic.get(dyn_b), Some("b"));
577    }
578
579    // --- parse: root must be Mapping ---
580
581    #[test]
582    fn test_root_must_be_mapping() {
583        let (mut dynamic, mut keys, mut values, mut path_map, mut children_map) = make_vecs();
584        assert!(parse("f", s("bad"), &mut dynamic, &mut keys, &mut values, &mut path_map, &mut children_map).is_err());
585    }
586}