Skip to main content

context_engine/
dsl.rs

1use alloc::{vec::Vec};
2use crate::provided::{DslError, Tree};
3use crate::list::{List, VariableList};
4
5// ── meta_key keywords ─────────────────────────────────────────────────────────
6
7pub const META_GET:   &[u8] = b"_get";
8pub const META_SET:   &[u8] = b"_set";
9pub const META_STATE: &[u8] = b"_state";
10
11// ── prop keywords (within _get / _set) ───────────────────────────────────────
12
13pub const PROP_STORE: &[u8] = b"store";
14pub const PROP_KEY:   &[u8] = b"key";
15pub const PROP_MAP:   &[u8] = b"map";
16
17// ── path field layout (u64) ───────────────────────────────────────────────────
18//
19// | field         | bits  |
20// |---------------|-------|
21// | is_leaf       |     1 | bit 63
22// | keyword_id    |    15 | bits 62..48
23// | parent_id     |    16 | bits 47..32
24// | child/leaf_id |    16 | bits 31..16  is_leaf=0: children id / is_leaf=1: leaves id
25// | value_id      |    16 | bits 15..0
26
27pub const PATH_IS_LEAF_SHIFT:    u64 = 63;
28pub const PATH_KEYWORD_ID_SHIFT: u64 = 48;
29pub const PATH_PARENT_ID_SHIFT:  u64 = 32;
30pub const PATH_CHILD_ID_SHIFT:   u64 = 16;
31pub const PATH_VALUE_ID_SHIFT:   u64 = 0;
32
33pub const PATH_IS_LEAF_MASK:    u64 = 0x1     << PATH_IS_LEAF_SHIFT;
34pub const PATH_KEYWORD_ID_MASK: u64 = 0x7fff  << PATH_KEYWORD_ID_SHIFT;
35pub const PATH_PARENT_ID_MASK:  u64 = 0xffff  << PATH_PARENT_ID_SHIFT;
36pub const PATH_CHILD_ID_MASK:   u64 = 0xffff  << PATH_CHILD_ID_SHIFT;
37pub const PATH_VALUE_ID_MASK:   u64 = 0xffff;
38
39// ── leaf layout (VariableList<u16>, fixed 12 u16s) ───────────────────────────
40//
41// | Field           | u16 | Note                           |
42// |-----------------|-----|--------------------------------|
43// | get_store_id    |   0 | stores id (u8 as u16)          |
44// | get_key_id      |   1 | values id                      |
45// | set_store_id    |   2 | stores id (u8 as u16)          |
46// | set_key_id      |   3 | values id                      |
47// | get_map_key_id  |   4 | map_keys id                    |
48// | get_map_val_id  |   5 | map_vals id                    |
49// | get_args_key_id |   6 | args_keys id                   |
50// | get_args_val_id |   7 | args_vals id                   |
51// | set_map_key_id  |   8 | map_keys id                    |
52// | set_map_val_id  |   9 | map_vals id                    |
53// | set_args_key_id |  10 | args_keys id                   |
54// | set_args_val_id |  11 | args_vals id                   |
55
56pub const LEAF_WIDTH: usize = 12;
57
58pub const LEAF_GET_STORE_ID:    usize = 0;
59pub const LEAF_GET_KEY_ID:      usize = 1;
60pub const LEAF_SET_STORE_ID:    usize = 2;
61pub const LEAF_SET_KEY_ID:      usize = 3;
62pub const LEAF_GET_MAP_KEY_ID:  usize = 4;
63pub const LEAF_GET_MAP_VAL_ID:  usize = 5;
64pub const LEAF_GET_ARGS_KEY_ID: usize = 6;
65pub const LEAF_GET_ARGS_VAL_ID: usize = 7;
66pub const LEAF_SET_MAP_KEY_ID:  usize = 8;
67pub const LEAF_SET_MAP_VAL_ID:  usize = 9;
68pub const LEAF_SET_ARGS_KEY_ID: usize = 10;
69pub const LEAF_SET_ARGS_VAL_ID: usize = 11;
70
71// ── value fragment encoding (u16) ────────────────────────────────────────────
72//
73// bit15: is_placeholder, bits14..0: word_id
74
75pub const VALUE_IS_PLACEHOLDER_MASK: u16 = 0x8000;
76pub const VALUE_WORD_ID_MASK:        u16 = 0x7fff;
77
78// ── Dsl ───────────────────────────────────────────────────────────────────────
79
80pub struct Dsl;
81
82// ── Limitations ───────────────────────────────────────────────────────────────
83
84const MAX_PATH_ID:  usize = 0xffff;   // 16 bit
85const MAX_WORD_ID:  usize = 0x7fff;   // 15 bit (bit15 reserved for is_placeholder)
86const MAX_STORE_ID: usize = 0xff;     // 8 bit
87// identity arrays are emitted as u16 (offset into data); data arrays are indexed by these offsets
88const MAX_VL_U16_DATA: usize = 0xffff; // VariableList<u16> data length fits u16 offset
89const MAX_VL_U8_DATA:  usize = 0xffff; // VariableList<u8>  data length fits u16 offset (words identity emitted as u16 too)
90
91impl Dsl {
92    /// Compiles a `Tree` into static index data structures consumed by `Index`.
93    ///
94    /// `store_ids` is the ordered list of store identifier strings as defined by the caller.
95    /// The index position becomes the `store_id` (u8) baked into leaf data.
96    ///
97    /// Returns `Err(DslError::LimitExceeded)` if any compile-time limit is exceeded.
98    ///
99    /// ```
100    /// # extern crate alloc;
101    /// use context_engine::{Tree, dsl::Dsl};
102    /// let tree = Tree::Mapping(alloc::vec![
103    ///     (b"id".to_vec(), Tree::Null),
104    /// ]);
105    /// let (paths, ..) = Dsl::compile(&tree, &[]).unwrap();
106    /// assert_eq!(paths.data.len(), 2); // virtual root + id
107    /// ```
108    pub fn compile(tree: &Tree, store_ids: &[&str]) -> Result<(
109        List<u64>,
110        VariableList<u16>,
111        VariableList<u16>,
112        VariableList<u16>,
113        VariableList<u8>,
114        VariableList<u16>,
115        VariableList<u16>,
116        VariableList<u16>,
117        VariableList<u16>,
118    ), DslError> {
119        if store_ids.len() > MAX_STORE_ID {
120            return Err(DslError::LimitExceeded(alloc::format!(
121                "store_ids length {} exceeds max {}", store_ids.len(), MAX_STORE_ID
122            )));
123        }
124        let mut compiler = Compiler::new(store_ids);
125        compiler.intern_word(b"")?; // word_id=0: empty string sentinel
126        // paths[0] = virtual root; List::new(1) already reserves the slot
127
128        if let Tree::Mapping(pairs) = tree {
129            let field_pairs: Vec<_> = pairs.iter()
130                .filter(|(k, _)| k.first() != Some(&b'_'))
131                .collect();
132
133            let children_id = compiler.alloc_children_slots(field_pairs.len())?;
134
135            for (i, (k, v)) in field_pairs.iter().enumerate() {
136                let child_id = compiler.paths.data.len() as u16;
137                compiler.set_child(children_id, i, child_id);
138                compiler.walk_field_key(k, v, 0, None, None)?;
139            }
140
141            let root = (children_id as u64) << PATH_CHILD_ID_SHIFT;
142            compiler.paths.data[0] = root;
143        }
144
145        compiler.resolve_map_dst()?;
146        compiler.finish()
147    }
148
149    /// Parse YAML source, compile, and write static Rust data to `out_path`.
150    #[cfg(feature = "precompile")]
151    pub fn write(src: &[u8], store_ids: &[&str], out_path: &str) -> Result<(), alloc::string::String> {
152        extern crate std;
153        use std::string::{String, ToString};
154        use std::format;
155
156        let tree = parse_yaml(src)?;
157        let (paths, children, leaves, values, words, map_keys, map_vals, args_keys, args_vals)
158            = Self::compile(&tree, store_ids).map_err(|e| e.to_string())?;
159
160        // identity arrays are emitted as u16; verify offsets fit before casting
161        check_vl_u16_identity(&children,  "children")?;
162        check_vl_u16_identity(&leaves,    "leaves")?;
163        check_vl_u16_identity(&values,    "values")?;
164        check_vl_u16_identity_u8(&words,  "words")?;
165        check_vl_u16_identity(&map_keys,  "map_keys")?;
166        check_vl_u16_identity(&map_vals,  "map_vals")?;
167        check_vl_u16_identity(&args_keys, "args_keys")?;
168        check_vl_u16_identity(&args_vals, "args_vals")?;
169
170        let mut out = String::new();
171        out.push_str("// @generated — do not edit by hand\n\n");
172        emit_u64_slice(&mut out, "PATHS",              &paths.data);
173        emit_u16_slice(&mut out, "CHILDREN_IDENTITY",  &children.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
174        emit_u16_slice(&mut out, "CHILDREN_DATA",      &children.data);
175        emit_u16_slice(&mut out, "LEAVES_IDENTITY",    &leaves.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
176        emit_u16_slice(&mut out, "LEAVES_DATA",        &leaves.data);
177        emit_u16_slice(&mut out, "VALUES_IDENTITY",    &values.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
178        emit_u16_slice(&mut out, "VALUES_DATA",        &values.data);
179        emit_u16_slice(&mut out, "WORDS_IDENTITY",     &words.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
180        emit_u8_slice  (&mut out, "WORDS_DATA",        &words.data);
181        emit_u16_slice(&mut out, "MAP_KEYS_IDENTITY",  &map_keys.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
182        emit_u16_slice(&mut out, "MAP_KEYS_DATA",      &map_keys.data);
183        emit_u16_slice(&mut out, "MAP_VALS_IDENTITY",  &map_vals.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
184        emit_u16_slice(&mut out, "MAP_VALS_DATA",      &map_vals.data);
185        emit_u16_slice(&mut out, "ARGS_KEYS_IDENTITY", &args_keys.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
186        emit_u16_slice(&mut out, "ARGS_KEYS_DATA",     &args_keys.data);
187        emit_u16_slice(&mut out, "ARGS_VALS_IDENTITY", &args_vals.identity.iter().map(|&x| x as u16).collect::<alloc::vec::Vec<_>>());
188        emit_u16_slice(&mut out, "ARGS_VALS_DATA",     &args_vals.data);
189
190        std::fs::write(out_path, out)
191            .map_err(|e| format!("write error: {e}"))
192    }
193}
194
195// ── MetaBlock ─────────────────────────────────────────────────────────────────
196
197#[derive(Clone)]
198struct MetaBlock {
199    store_id:    u8,
200    defined_at:  u16,              // path_id of the node where this _get/_set was defined
201    key_value:   Vec<u16>,         // value fragment u16s for key
202    map_entries: Vec<(u16, u16)>,  // (dst_word_id, src_word_id)
203    arg_entries: Vec<(u16, Vec<u16>)>, // (key_word_id, val fragment u16s)
204}
205
206// ── Compiler ──────────────────────────────────────────────────────────────────
207
208struct Compiler<'s> {
209    store_ids:       &'s [&'s str],
210    paths:           List<u64>,
211    children:        VariableList<u16>,
212    leaves:          VariableList<u16>,
213    values:          VariableList<u16>,
214    words:           VariableList<u8>,
215    map_keys:        VariableList<u16>,
216    map_vals:        VariableList<u16>,
217    args_keys:       VariableList<u16>,
218    args_vals:       VariableList<u16>,
219    // (map_key_id, defined_at): used in 2nd pass to resolve dst word_id → path_id
220    map_dst_pending: Vec<(u16, u16)>,
221}
222
223impl<'s> Compiler<'s> {
224    fn new(store_ids: &'s [&'s str]) -> Self {
225        Self {
226            store_ids,
227            paths:           List::new(1),   // paths[0] placeholder
228            children:        VariableList::new(),
229            leaves:          VariableList::new(),
230            values:          VariableList::new(),
231            words:           VariableList::new(),
232            map_keys:        VariableList::new(),
233            map_vals:        VariableList::new(),
234            args_keys:       VariableList::new(),
235            args_vals:       VariableList::new(),
236            map_dst_pending: Vec::new(),
237        }
238    }
239
240    // ── path push ─────────────────────────────────────────────────────────────
241
242    fn push_path(&mut self, entry: u64) -> Result<u16, DslError> {
243        let id = self.paths.data.len();
244        if id > MAX_PATH_ID {
245            return Err(DslError::LimitExceeded(alloc::format!(
246                "path_id {} exceeds max {}", id, MAX_PATH_ID
247            )));
248        }
249        self.paths.data.push(entry);
250        Ok(id as u16)
251    }
252
253    // ── children slot helpers ─────────────────────────────────────────────────
254
255    fn alloc_children_slots(&mut self, count: usize) -> Result<usize, DslError> {
256        if count == 0 { return Ok(0); }
257        let zeros: Vec<u16> = alloc::vec![0u16; count];
258        match self.children.set(&0, &zeros, false) {
259            Ok(crate::required::SetOutcome::Created(id)) => Ok(id),
260            _ => Err(DslError::LimitExceeded("children allocation failed".into())),
261        }
262    }
263
264    fn set_child(&mut self, children_id: usize, slot: usize, path_id: u16) {
265        if children_id == 0 { return; }
266        let identity_start = children_id * 2;
267        let start = self.children.identity[identity_start];
268        self.children.data[start + slot] = path_id;
269    }
270
271    // ── walk ──────────────────────────────────────────────────────────────────
272
273    fn walk_field_key(
274        &mut self,
275        keyword:    &[u8],
276        value:      &Tree,
277        parent_id: u16,
278        inh_get:    Option<&MetaBlock>,
279        inh_set:    Option<&MetaBlock>,
280    ) -> Result<(), DslError> {
281        let path_id = self.push_path(0u64)?; // placeholder, filled below
282
283        let keyword_id = self.intern_word(keyword)?;
284
285        match value {
286            Tree::Mapping(pairs) => {
287                let get = self.resolve_meta(pairs, META_GET, inh_get, path_id)?;
288                let set = self.resolve_meta(pairs, META_SET, inh_set, path_id)?;
289
290                let field_pairs: Vec<_> = pairs.iter()
291                    .filter(|(k, _)| k.first() != Some(&b'_'))
292                    .collect();
293                let child_count = field_pairs.len();
294
295                if child_count == 0 {
296                    self.write_leaf(path_id, keyword_id, parent_id, &Tree::Null, get.as_ref(), set.as_ref())?;
297                } else {
298                    let children_id = self.alloc_children_slots(child_count)?;
299                    for (i, (k, v)) in field_pairs.iter().enumerate() {
300                        let child_id = self.paths.data.len() as u16;
301                        self.set_child(children_id, i, child_id);
302                        self.walk_field_key(k, v, path_id, get.as_ref(), set.as_ref())?;
303                    }
304                    self.paths.data[path_id as usize] =
305                        ((keyword_id as u64) << PATH_KEYWORD_ID_SHIFT)
306                        | ((parent_id as u64) << PATH_PARENT_ID_SHIFT)
307                        | ((children_id as u64) << PATH_CHILD_ID_SHIFT);
308                }
309            }
310            _ => {
311                self.write_leaf(path_id, keyword_id, parent_id, value, inh_get, inh_set)?;
312            }
313        }
314        Ok(())
315    }
316
317    // ── meta resolution ───────────────────────────────────────────────────────
318
319    fn resolve_meta(
320        &mut self,
321        pairs:           &[(Vec<u8>, Tree)],
322        meta_key:        &[u8],
323        inherited:       Option<&MetaBlock>,
324        current_path_id: u16,
325    ) -> Result<Option<MetaBlock>, DslError> {
326        let local = pairs.iter().find(|(k, _)| k.as_slice() == meta_key);
327        match (local, inherited) {
328            (None, None) => Ok(None),
329            (None, Some(inh)) => Ok(Some(inh.clone())),
330            (Some((_, Tree::Mapping(meta_pairs))), inh) => {
331                let mut store_id    = inh.map(|b| b.store_id).unwrap_or(0);
332                let mut defined_at  = inh.map(|b| b.defined_at).unwrap_or(0);
333                let mut key_value: Vec<u16>               = inh.map(|b| b.key_value.clone()).unwrap_or_default();
334                let mut map_entries: Vec<(u16, u16)>      = inh.map(|b| b.map_entries.clone()).unwrap_or_default();
335                let mut arg_entries: Vec<(u16, Vec<u16>)> = inh.map(|b| b.arg_entries.clone()).unwrap_or_default();
336
337                for (k, v) in meta_pairs {
338                    if k.as_slice() == PROP_STORE {
339                        if let Tree::Scalar(b) = v {
340                            store_id = self.resolve_store_id(b);
341                            map_entries.clear();
342                            defined_at = 0;
343                        }
344                    } else if k.as_slice() == PROP_KEY {
345                        key_value = self.encode_value(v)?;
346                    } else if k.as_slice() == PROP_MAP {
347                        if let Tree::Mapping(map_pairs) = v {
348                            map_entries.clear();
349                            for (mk, mv) in map_pairs {
350                                let dst = self.intern_word(mk)?;
351                                let src = if let Tree::Scalar(b) = mv { self.intern_word(b)? } else { 0 };
352                                map_entries.push((dst, src));
353                            }
354                            defined_at = current_path_id;
355                        }
356                    } else if k.as_slice() != META_GET
357                           && k.as_slice() != META_SET
358                           && k.as_slice() != META_STATE {
359                        let ak = self.intern_word(k)?;
360                        let av = self.encode_value(v)?;
361                        if let Some(entry) = arg_entries.iter_mut().find(|(ek, _)| *ek == ak) {
362                            entry.1 = av;
363                        } else {
364                            arg_entries.push((ak, av));
365                        }
366                    }
367                }
368                Ok(Some(MetaBlock { store_id, defined_at, key_value, map_entries, arg_entries }))
369            }
370            _ => Ok(inherited.cloned()),
371        }
372    }
373
374    // ── leaf serialization ────────────────────────────────────────────────────
375
376    fn write_leaf(
377        &mut self,
378        path_id:   u16,
379        keyword_id: u16,
380        parent_id: u16,
381        value:      &Tree,
382        get:        Option<&MetaBlock>,
383        set:        Option<&MetaBlock>,
384    ) -> Result<(), DslError> {
385        let value_frags = self.encode_value(value)?;
386        let value_id = if value_frags.is_empty() {
387            0u16
388        } else {
389            self.push_values(&value_frags)?
390        };
391
392        let (get_store_id, get_key_id, get_map_key_id, get_map_val_id, get_args_key_id, get_args_val_id)
393            = self.encode_meta(get)?;
394        let (set_store_id, set_key_id, set_map_key_id, set_map_val_id, set_args_key_id, set_args_val_id)
395            = self.encode_meta(set)?;
396
397        let leaf_data: [u16; LEAF_WIDTH] = [
398            get_store_id, get_key_id,
399            set_store_id, set_key_id,
400            get_map_key_id, get_map_val_id,
401            get_args_key_id, get_args_val_id,
402            set_map_key_id, set_map_val_id,
403            set_args_key_id, set_args_val_id,
404        ];
405
406        let leaf_id = match self.leaves.set(&0, &leaf_data, false) {
407            Ok(crate::required::SetOutcome::Created(id)) => {
408                if id > MAX_PATH_ID {
409                    return Err(DslError::LimitExceeded(alloc::format!(
410                        "leaf_id {} exceeds max {}", id, MAX_PATH_ID
411                    )));
412                }
413                id as u16
414            }
415            _ => return Err(DslError::LimitExceeded("leaf allocation failed".into())),
416        };
417
418        self.paths.data[path_id as usize] =
419            PATH_IS_LEAF_MASK
420            | ((keyword_id as u64) << PATH_KEYWORD_ID_SHIFT)
421            | ((parent_id as u64) << PATH_PARENT_ID_SHIFT)
422            | ((leaf_id as u64)    << PATH_CHILD_ID_SHIFT)
423            | (value_id as u64);
424        Ok(())
425    }
426
427    fn encode_meta(&mut self, meta: Option<&MetaBlock>) -> Result<(u16, u16, u16, u16, u16, u16), DslError> {
428        let Some(b) = meta else {
429            return Ok((0, 0, 0, 0, 0, 0));
430        };
431
432        let key_id = if b.key_value.is_empty() { 0u16 } else { self.push_values(&b.key_value)? };
433
434        let (map_key_id, map_val_id) = if b.map_entries.is_empty() {
435            (0u16, 0u16)
436        } else {
437            let dsts: Vec<u16> = b.map_entries.iter().map(|&(d, _)| d).collect();
438            let srcs: Vec<u16> = b.map_entries.iter().map(|&(_, s)| s).collect();
439            let mk_id = self.push_map_keys(&dsts)?;
440            let mv_id = self.push_map_vals(&srcs)?;
441            self.map_dst_pending.push((mk_id, b.defined_at));
442            (mk_id, mv_id)
443        };
444
445        let (args_key_id, args_val_id) = if b.arg_entries.is_empty() {
446            (0u16, 0u16)
447        } else {
448            let keys: Vec<u16> = b.arg_entries.iter().map(|&(k, _)| k).collect();
449            let mut val_ids: Vec<u16> = Vec::with_capacity(b.arg_entries.len());
450            for (_, frags) in &b.arg_entries {
451                let id = if frags.is_empty() { 0u16 } else { self.push_values(frags)? };
452                val_ids.push(id);
453            }
454            let ak = match self.args_keys.set(&0, &keys, false) {
455                Ok(crate::required::SetOutcome::Created(id)) => {
456                    if id > MAX_PATH_ID { return Err(DslError::LimitExceeded("args_keys id overflow".into())); }
457                    id as u16
458                }
459                _ => return Err(DslError::LimitExceeded("args_keys allocation failed".into())),
460            };
461            let av = match self.args_vals.set(&0, &val_ids, false) {
462                Ok(crate::required::SetOutcome::Created(id)) => {
463                    if id > MAX_PATH_ID { return Err(DslError::LimitExceeded("args_vals id overflow".into())); }
464                    id as u16
465                }
466                _ => return Err(DslError::LimitExceeded("args_vals allocation failed".into())),
467            };
468            (ak, av)
469        };
470
471        Ok((b.store_id as u16, key_id, map_key_id, map_val_id, args_key_id, args_val_id))
472    }
473
474    // ── VariableList push helpers ─────────────────────────────────────────────
475
476    fn push_values(&mut self, frags: &[u16]) -> Result<u16, DslError> {
477        match self.values.set(&0, frags, false) {
478            Ok(crate::required::SetOutcome::Created(id)) => {
479                if id > MAX_PATH_ID {
480                    return Err(DslError::LimitExceeded(alloc::format!(
481                        "values id {} exceeds max {}", id, MAX_PATH_ID
482                    )));
483                }
484                Ok(id as u16)
485            }
486            _ => Err(DslError::LimitExceeded("values allocation failed".into())),
487        }
488    }
489
490    fn push_map_keys(&mut self, data: &[u16]) -> Result<u16, DslError> {
491        match self.map_keys.set(&0, data, false) {
492            Ok(crate::required::SetOutcome::Created(id)) => {
493                if id > MAX_PATH_ID {
494                    return Err(DslError::LimitExceeded(alloc::format!(
495                        "map_keys id {} exceeds max {}", id, MAX_PATH_ID
496                    )));
497                }
498                Ok(id as u16)
499            }
500            _ => Err(DslError::LimitExceeded("map_keys allocation failed".into())),
501        }
502    }
503
504    fn push_map_vals(&mut self, data: &[u16]) -> Result<u16, DslError> {
505        match self.map_vals.set(&0, data, false) {
506            Ok(crate::required::SetOutcome::Created(id)) => {
507                if id > MAX_PATH_ID {
508                    return Err(DslError::LimitExceeded(alloc::format!(
509                        "map_vals id {} exceeds max {}", id, MAX_PATH_ID
510                    )));
511                }
512                Ok(id as u16)
513            }
514            _ => Err(DslError::LimitExceeded("map_vals allocation failed".into())),
515        }
516    }
517
518    // ── value encoding ────────────────────────────────────────────────────────
519
520    fn encode_value(&mut self, value: &Tree) -> Result<Vec<u16>, DslError> {
521        let Tree::Scalar(b) = value else { return Ok(Vec::new()); };
522        let mut frags: Vec<u16> = Vec::new();
523        let mut rest = b.as_slice();
524        while !rest.is_empty() {
525            if let Some(start) = rest.windows(2).position(|w| w == b"${") {
526                if start > 0 {
527                    let word_id = self.intern_word(&rest[..start])?;
528                    frags.push(word_id & VALUE_WORD_ID_MASK);
529                }
530                rest = &rest[start + 2..];
531                if let Some(end) = rest.iter().position(|&c| c == b'}') {
532                    let word_id = self.intern_word(&rest[..end])?;
533                    frags.push(VALUE_IS_PLACEHOLDER_MASK | (word_id & VALUE_WORD_ID_MASK));
534                    rest = &rest[end + 1..];
535                } else {
536                    let word_id = self.intern_word(rest)?;
537                    frags.push(word_id & VALUE_WORD_ID_MASK);
538                    break;
539                }
540            } else {
541                let word_id = self.intern_word(rest)?;
542                frags.push(word_id & VALUE_WORD_ID_MASK);
543                break;
544            }
545        }
546        Ok(frags)
547    }
548
549    // ── word interning ────────────────────────────────────────────────────────
550
551    fn intern_word(&mut self, s: &[u8]) -> Result<u16, DslError> {
552        match self.words.set(&0, s, true) {
553            Ok(crate::required::SetOutcome::Created(id)) => {
554                if id > MAX_WORD_ID {
555                    return Err(DslError::LimitExceeded(alloc::format!(
556                        "word_id {} exceeds max {} (15-bit limit)", id, MAX_WORD_ID
557                    )));
558                }
559                Ok(id as u16)
560            }
561            _ => Err(DslError::LimitExceeded("word interning failed".into())),
562        }
563    }
564
565    // ── store_id resolution ───────────────────────────────────────────────────
566
567    fn resolve_store_id(&self, name: &[u8]) -> u8 {
568        self.store_ids.iter().position(|s| s.as_bytes() == name)
569            .map(|i| (i + 1) as u8) // 1-based; 0 = none
570            .unwrap_or(0)
571    }
572
573    // ── 2nd pass: resolve map dst word_id → path_id ───────────────────────────
574
575    fn resolve_map_dst(&mut self) -> Result<(), DslError> {
576        let pending = core::mem::take(&mut self.map_dst_pending);
577        for (map_key_id, defined_at) in pending {
578            let mk_start = self.map_keys.identity[map_key_id as usize * 2];
579            let mk_end   = self.map_keys.identity[map_key_id as usize * 2 + 1];
580            for i in mk_start..mk_end {
581                let word_id  = self.map_keys.data[i];
582                let kw_start = self.words.identity[word_id as usize * 2];
583                let kw_end   = self.words.identity[word_id as usize * 2 + 1];
584                let keyword  = self.words.data[kw_start..kw_end].to_vec();
585                let path_id  = self.find_path_by_keyword_chain(defined_at, &keyword)?;
586                self.map_keys.data[i] = path_id;
587            }
588        }
589        Ok(())
590    }
591
592    // keyword may be dotted (e.g. b"preference.color_mode"); resolve from parent_id
593    fn find_path_by_keyword_chain(&self, parent_id: u16, keyword: &[u8]) -> Result<u16, DslError> {
594        let mut current = parent_id;
595        for segment in keyword.split(|&b| b == b'.') {
596            current = self.find_child_path(current, segment)
597                .ok_or_else(|| DslError::LimitExceeded(alloc::format!(
598                    "map dst {:?} not found under path_id {}",
599                    core::str::from_utf8(keyword).unwrap_or("?"), parent_id
600                )))?;
601        }
602        Ok(current)
603    }
604
605    fn find_child_path(&self, path_id: u16, keyword: &[u8]) -> Option<u16> {
606        let path = self.paths.data[path_id as usize];
607        if path & PATH_IS_LEAF_MASK != 0 { return None; }
608        let children_id = ((path & PATH_CHILD_ID_MASK) >> PATH_CHILD_ID_SHIFT) as usize;
609        if children_id == 0 { return None; }
610        let ck_start = self.children.identity[children_id * 2];
611        let ck_end   = self.children.identity[children_id * 2 + 1];
612        for &child_id in &self.children.data[ck_start..ck_end] {
613            let child_path = self.paths.data[child_id as usize];
614            let word_id = ((child_path & PATH_KEYWORD_ID_MASK) >> PATH_KEYWORD_ID_SHIFT) as usize;
615            let wk_start = self.words.identity[word_id * 2];
616            let wk_end   = self.words.identity[word_id * 2 + 1];
617            if &self.words.data[wk_start..wk_end] == keyword {
618                return Some(child_id);
619            }
620        }
621        None
622    }
623
624    // ── finish ────────────────────────────────────────────────────────────────
625
626    fn finish(self) -> Result<(
627        List<u64>,
628        VariableList<u16>,
629        VariableList<u16>,
630        VariableList<u16>,
631        VariableList<u8>,
632        VariableList<u16>,
633        VariableList<u16>,
634        VariableList<u16>,
635        VariableList<u16>,
636    ), DslError> {
637        // Validate data lengths fit u16 offsets (checked again in write, but also needed for runtime compile)
638        check_vl_data_u16(&self.children,  "children")?;
639        check_vl_data_u16(&self.leaves,    "leaves")?;
640        check_vl_data_u16(&self.values,    "values")?;
641        check_vl_data_u16_from_u8(&self.words, "words")?;
642        check_vl_data_u16(&self.map_keys,  "map_keys")?;
643        check_vl_data_u16(&self.map_vals,  "map_vals")?;
644        check_vl_data_u16(&self.args_keys, "args_keys")?;
645        check_vl_data_u16(&self.args_vals, "args_vals")?;
646        Ok((
647            self.paths,
648            self.children,
649            self.leaves,
650            self.values,
651            self.words,
652            self.map_keys,
653            self.map_vals,
654            self.args_keys,
655            self.args_vals,
656        ))
657    }
658}
659
660// ── compile-time limit checks ─────────────────────────────────────────────────
661
662fn check_vl_data_u16<T>(vl: &VariableList<T>, name: &str) -> Result<(), DslError> {
663    if vl.data.len() > MAX_VL_U16_DATA {
664        return Err(DslError::LimitExceeded(alloc::format!(
665            "{} data length {} exceeds max {}", name, vl.data.len(), MAX_VL_U16_DATA
666        )));
667    }
668    Ok(())
669}
670
671fn check_vl_data_u16_from_u8(vl: &VariableList<u8>, name: &str) -> Result<(), DslError> {
672    if vl.data.len() > MAX_VL_U8_DATA {
673        return Err(DslError::LimitExceeded(alloc::format!(
674            "{} data length {} exceeds max {}", name, vl.data.len(), MAX_VL_U8_DATA
675        )));
676    }
677    Ok(())
678}
679
680#[cfg(feature = "precompile")]
681fn check_vl_u16_identity<T>(vl: &VariableList<T>, name: &str) -> Result<(), alloc::string::String> {
682    for &offset in &vl.identity {
683        if offset > MAX_VL_U16_DATA {
684            return Err(alloc::format!(
685                "{} identity offset {} exceeds u16 max {}", name, offset, MAX_VL_U16_DATA
686            ));
687        }
688    }
689    Ok(())
690}
691
692#[cfg(feature = "precompile")]
693fn check_vl_u16_identity_u8(vl: &VariableList<u8>, name: &str) -> Result<(), alloc::string::String> {
694    for &offset in &vl.identity {
695        if offset > MAX_VL_U8_DATA {
696            return Err(alloc::format!(
697                "{} identity offset {} exceeds u16 max {}", name, offset, MAX_VL_U8_DATA
698            ));
699        }
700    }
701    Ok(())
702}
703
704// ── precompile helpers ────────────────────────────────────────────────────────
705
706#[cfg(feature = "precompile")]
707pub fn parse_yaml(src: &[u8]) -> Result<Tree, alloc::string::String> {
708    extern crate std;
709    use std::format;
710
711    let s = std::str::from_utf8(src)
712        .map_err(|e| format!("UTF-8 error: {e}"))?;
713    let yaml: serde_yaml_ng::Value = serde_yaml_ng::from_str(s)
714        .map_err(|e| format!("YAML parse error: {e}"))?;
715    Ok(yaml_value_to_tree(yaml))
716}
717
718#[cfg(feature = "precompile")]
719fn yaml_value_to_tree(v: serde_yaml_ng::Value) -> Tree {
720    extern crate std;
721    use std::string::ToString;
722
723    match v {
724        serde_yaml_ng::Value::Mapping(m) => Tree::Mapping(
725            m.into_iter()
726                .filter_map(|(k, v)| {
727                    if let serde_yaml_ng::Value::String(s) = k {
728                        Some((s.into_bytes(), yaml_value_to_tree(v)))
729                    } else {
730                        None
731                    }
732                })
733                .collect(),
734        ),
735        serde_yaml_ng::Value::Sequence(s) => {
736            Tree::Sequence(s.into_iter().map(yaml_value_to_tree).collect())
737        }
738        serde_yaml_ng::Value::String(s) => Tree::Scalar(s.into_bytes()),
739        serde_yaml_ng::Value::Number(n) => Tree::Scalar(n.to_string().into_bytes()),
740        serde_yaml_ng::Value::Bool(b)   => Tree::Scalar(b.to_string().into_bytes()),
741        serde_yaml_ng::Value::Null      => Tree::Null,
742        _                               => Tree::Null,
743    }
744}
745
746#[cfg(feature = "precompile")]
747fn emit_u64_slice(out: &mut alloc::string::String, name: &str, data: &[u64]) {
748    extern crate std;
749    use std::format;
750    out.push_str(&format!("pub static {name}: &[u64] = &[\n"));
751    for chunk in data.chunks(8) {
752        out.push_str("    ");
753        for v in chunk { out.push_str(&format!("0x{v:016x}, ")); }
754        out.push('\n');
755    }
756    out.push_str("];\n\n");
757}
758
759#[cfg(feature = "precompile")]
760fn emit_u16_slice(out: &mut alloc::string::String, name: &str, data: &[u16]) {
761    extern crate std;
762    use std::format;
763    out.push_str(&format!("pub static {name}: &[u16] = &[\n"));
764    for chunk in data.chunks(8) {
765        out.push_str("    ");
766        for v in chunk { out.push_str(&format!("0x{v:04x}, ")); }
767        out.push('\n');
768    }
769    out.push_str("];\n\n");
770}
771
772#[cfg(feature = "precompile")]
773fn emit_u8_slice(out: &mut alloc::string::String, name: &str, data: &[u8]) {
774    extern crate std;
775    use std::format;
776    out.push_str(&format!("pub static {name}: &[u8] = &[\n"));
777    for chunk in data.chunks(16) {
778        out.push_str("    ");
779        for v in chunk { out.push_str(&format!("0x{v:02x}, ")); }
780        out.push('\n');
781    }
782    out.push_str("];\n\n");
783}
784
785// ── tests ─────────────────────────────────────────────────────────────────────
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790    use alloc::vec;
791
792    fn scalar(s: &str) -> Tree { Tree::Scalar(s.as_bytes().to_vec()) }
793    fn mapping(pairs: Vec<(&str, Tree)>) -> Tree {
794        Tree::Mapping(pairs.into_iter().map(|(k, v)| (k.as_bytes().to_vec(), v)).collect())
795    }
796
797    fn compile(tree: &Tree) -> (List<u64>, VariableList<u16>, VariableList<u16>, VariableList<u16>, VariableList<u8>, VariableList<u16>, VariableList<u16>, VariableList<u16>, VariableList<u16>) {
798        Dsl::compile(tree, &[]).unwrap()
799    }
800
801    fn compile_with_stores<'a>(tree: &Tree, store_ids: &[&'a str]) -> (List<u64>, VariableList<u16>, VariableList<u16>, VariableList<u16>, VariableList<u8>, VariableList<u16>, VariableList<u16>, VariableList<u16>, VariableList<u16>) {
802        Dsl::compile(tree, store_ids).unwrap()
803    }
804
805    // --- single_leaf ---
806
807    #[test]
808    fn single_leaf() {
809        let (paths, ..) = compile(&mapping(vec![
810            ("name", Tree::Null),
811        ]));
812        assert_eq!(paths.data.len(), 2);                             // root(0) + name(1)
813        assert!(paths.data[0] & PATH_IS_LEAF_MASK == 0);             // root is not a leaf
814        assert!(paths.data[1] & PATH_IS_LEAF_MASK != 0);             // name is a leaf
815    }
816
817    // --- nested ---
818
819    #[test]
820    fn nested() {
821        let (paths, children, ..) = compile(&mapping(vec![
822            ("user", mapping(vec![
823                ("id",   Tree::Null),
824                ("name", Tree::Null),
825            ])),
826        ]));
827        assert_eq!(paths.data.len(), 4);                             // root(0) + user(1) + id(2) + name(3)
828        assert!(paths.data[1] & PATH_IS_LEAF_MASK == 0);             // user is not a leaf
829        assert!(children.data.len() >= 3);                           // root→user + user→id,name
830    }
831
832    // --- meta_key ---
833
834    #[test]
835    fn meta_key_excluded_from_paths() {
836        let (paths, ..) = compile(&mapping(vec![
837            ("user", mapping(vec![
838                ("_get", mapping(vec![
839                    ("store", scalar("Memory")),
840                    ("key",    scalar("user:1")),
841                ])),
842                ("id", Tree::Null),
843            ])),
844        ]));
845        // root(0) + user(1) + id(2) — _get must not appear
846        assert_eq!(paths.data.len(), 3);
847    }
848
849    // --- get in leaf (store_id) ---
850
851    #[test]
852    fn get_store_id_set_in_leaf() {
853        let store_ids = &["Memory", "Kvs"];
854        let (paths, _, leaves, ..) = compile_with_stores(&mapping(vec![
855            ("user", mapping(vec![
856                ("_get", mapping(vec![
857                    ("store", scalar("Memory")),
858                    ("key",    scalar("user:1")),
859                ])),
860                ("id", Tree::Null),
861            ])),
862        ]), store_ids);
863        // root(0), user(1), id(2) — id is leaf
864        let id_path = paths.data[2];
865        assert!(id_path & PATH_IS_LEAF_MASK != 0);
866        let leaf_id = ((id_path & PATH_CHILD_ID_MASK) >> PATH_CHILD_ID_SHIFT) as usize;
867        let leaf_start = leaves.identity[leaf_id * 2];
868        let get_store_id = leaves.data[leaf_start + LEAF_GET_STORE_ID];
869        // "Memory" is store_ids[0] → store_id = 1 (1-based)
870        assert_eq!(get_store_id, 1);
871    }
872
873    // --- store inheritance ---
874
875    #[test]
876    fn store_inherited_to_child_leaf() {
877        let store_ids = &["Memory", "Kvs"];
878        let (paths, _, leaves, ..) = compile_with_stores(&mapping(vec![
879            ("session", mapping(vec![
880                ("_set", mapping(vec![
881                    ("store", scalar("Kvs")),
882                    ("key",    scalar("session:1")),
883                ])),
884                ("user", mapping(vec![
885                    ("id", Tree::Null),
886                ])),
887            ])),
888        ]), store_ids);
889        // root(0), session(1), user(2), id(3) — id is leaf
890        let id_path = paths.data[3];
891        assert!(id_path & PATH_IS_LEAF_MASK != 0);
892        let leaf_id = ((id_path & PATH_CHILD_ID_MASK) >> PATH_CHILD_ID_SHIFT) as usize;
893        let leaf_start = leaves.identity[leaf_id * 2];
894        let set_store_id = leaves.data[leaf_start + LEAF_SET_STORE_ID];
895        // "Kvs" is store_ids[1] → store_id = 2 (1-based)
896        assert_eq!(set_store_id, 2);
897    }
898
899    // --- word intern ---
900
901    #[test]
902    fn intern_dedup() {
903        let (_, _, _, _, words, ..) = compile(&mapping(vec![
904            ("a", scalar("hello")),
905            ("b", scalar("hello")),
906        ]));
907        let hello_count = (1..words.identity.len() / 2).filter(|&i| {
908            let start = words.identity[i * 2];
909            let end   = words.identity[i * 2 + 1];
910            words.data.get(start..end) == Some(b"hello" as &[u8])
911        }).count();
912        assert_eq!(hello_count, 1);
913    }
914
915    // --- value fragments ---
916
917    #[test]
918    fn static_value_encoded_as_single_fragment() {
919        let (paths, _, _, values, words, ..) = compile(&mapping(vec![
920            ("key", scalar("hello")),
921        ]));
922        let path = paths.data[1];
923        assert!(path & PATH_IS_LEAF_MASK != 0);
924        let value_id = (path & PATH_VALUE_ID_MASK) as usize;
925        assert!(value_id != 0);
926        let vstart = values.identity[value_id * 2];
927        let vend   = values.identity[value_id * 2 + 1];
928        let frags = &values.data[vstart..vend];
929        assert_eq!(frags.len(), 1);
930        assert_eq!(frags[0] & VALUE_IS_PLACEHOLDER_MASK, 0); // static
931        let word_id = (frags[0] & VALUE_WORD_ID_MASK) as usize;
932        let wstart = words.identity[word_id * 2];
933        let wend   = words.identity[word_id * 2 + 1];
934        assert_eq!(&words.data[wstart..wend], b"hello");
935    }
936
937    #[test]
938    fn placeholder_value_encoded_as_single_fragment_is_placeholder() {
939        let (paths, _, _, values, ..) = compile(&mapping(vec![
940            ("copy", scalar("${session.user.id}")),
941        ]));
942        let path = paths.data[1];
943        let value_id = (path & PATH_VALUE_ID_MASK) as usize;
944        let vstart = values.identity[value_id * 2];
945        let vend   = values.identity[value_id * 2 + 1];
946        let frags = &values.data[vstart..vend];
947        assert_eq!(frags.len(), 1);
948        assert_ne!(frags[0] & VALUE_IS_PLACEHOLDER_MASK, 0); // placeholder
949    }
950
951    #[test]
952    fn template_value_encoded_as_multiple_fragments() {
953        let (paths, _, _, values, ..) = compile(&mapping(vec![
954            ("key", scalar("prefix.${some.path}.suffix")),
955        ]));
956        let path = paths.data[1];
957        let value_id = (path & PATH_VALUE_ID_MASK) as usize;
958        let vstart = values.identity[value_id * 2];
959        let vend   = values.identity[value_id * 2 + 1];
960        let frags = &values.data[vstart..vend];
961        assert_eq!(frags.len(), 3);
962        assert_eq!(frags[0] & VALUE_IS_PLACEHOLDER_MASK, 0); // static
963        assert_ne!(frags[1] & VALUE_IS_PLACEHOLDER_MASK, 0); // placeholder
964        assert_eq!(frags[2] & VALUE_IS_PLACEHOLDER_MASK, 0); // static
965    }
966
967    // --- resolve_meta ---
968
969    #[test]
970    fn local_get_overrides_inherited_store() {
971        let store_ids = &["ClientA", "ClientB"];
972        let (paths, _, leaves, ..) = compile_with_stores(&mapping(vec![
973            ("parent", mapping(vec![
974                ("_get", mapping(vec![
975                    ("store", scalar("ClientA")),
976                    ("key",    scalar("k")),
977                ])),
978                ("child", mapping(vec![
979                    ("_get", mapping(vec![
980                        ("store", scalar("ClientB")),
981                        ("key",    scalar("k")),
982                    ])),
983                    ("leaf", Tree::Null),
984                ])),
985            ])),
986        ]), store_ids);
987        // root(0), parent(1), child(2), leaf(3)
988        let leaf_path = paths.data[3];
989        assert!(leaf_path & PATH_IS_LEAF_MASK != 0);
990        let leaf_id = ((leaf_path & PATH_CHILD_ID_MASK) >> PATH_CHILD_ID_SHIFT) as usize;
991        let leaf_start = leaves.identity[leaf_id * 2];
992        let get_store_id = leaves.data[leaf_start + LEAF_GET_STORE_ID];
993        // "ClientB" is store_ids[1] → store_id = 2
994        assert_eq!(get_store_id, 2);
995    }
996
997    #[test]
998    fn get_inherited_when_no_local_override() {
999        let store_ids = &["Inherited"];
1000        let (paths, _, leaves, ..) = compile_with_stores(&mapping(vec![
1001            ("parent", mapping(vec![
1002                ("_get", mapping(vec![
1003                    ("store", scalar("Inherited")),
1004                    ("key",    scalar("k")),
1005                ])),
1006                ("leaf", Tree::Null),
1007            ])),
1008        ]), store_ids);
1009        // root(0), parent(1), leaf(2)
1010        let leaf_path = paths.data[2];
1011        assert!(leaf_path & PATH_IS_LEAF_MASK != 0);
1012        let leaf_id = ((leaf_path & PATH_CHILD_ID_MASK) >> PATH_CHILD_ID_SHIFT) as usize;
1013        let leaf_start = leaves.identity[leaf_id * 2];
1014        let get_store_id = leaves.data[leaf_start + LEAF_GET_STORE_ID];
1015        // "Inherited" is store_ids[0] → store_id = 1
1016        assert_eq!(get_store_id, 1);
1017    }
1018
1019    // --- precompile ---
1020
1021    #[cfg(feature = "precompile")]
1022    #[test]
1023    fn write_tenant_yml() {
1024        extern crate std;
1025        let src = std::include_bytes!("../examples/tenant.yml");
1026        let out = std::env::temp_dir().join("tenant_compiled.rs");
1027        std::fs::remove_file(&out).ok();
1028        Dsl::write(src, &["Memory", "Kvs", "TenantDb", "CommonDb", "Env"], out.to_str().unwrap()).expect("write failed");
1029        let content = std::fs::read_to_string(&out).expect("output not written");
1030        assert!(content.contains("pub static PATHS:"));
1031    }
1032}