Skip to main content

verit_core/
resolve.rs

1//! Schema resolution: the piece that buys evolution without giving up
2//! zero-copy. A [`Resolver`] is built **once** per (writer schema, reader
3//! schema) pair and compiles, for every corresponding struct type, an access
4//! plan mapping each reader field ID to either a concrete byte slot in the
5//! writer's layout (with an optional lossless widening) or `Absent`. After
6//! that, reading any number of messages costs no per-message resolution work.
7
8use std::collections::HashMap;
9
10use crate::error::{Error, Result};
11use crate::layout::slot_size_align;
12use crate::schema::{Schema, Type};
13
14/// Ceiling on resolver recursion depth. `pair` ↔ `compat` descend once per
15/// nested type-expression node *and* once per struct-type reference; the memo
16/// in `pair` stops cyclic references but not a long acyclic *chain* of distinct
17/// structs (A0→A1→…→An). Without this bound a hostile inline schema (tens of
18/// thousands of chained structs, still well-formed and small on the wire) would
19/// overflow the stack when the message is resolved — e.g. via `dump_json`.
20/// Every recursive step increments `depth`, so total live frames stay ≤ this
21/// limit. It is comfortably above `schema::MAX_TYPE_DEPTH` (64) so any schema
22/// that validates resolves, and matches the 128 depth cap the dump/verify
23/// walkers already use, so a schema that resolves also renders.
24const MAX_RESOLVE_DEPTH: u32 = 128;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum NumKind {
28    U8,
29    U16,
30    U32,
31    U64,
32    I8,
33    I16,
34    I32,
35    I64,
36    F32,
37    F64,
38}
39
40pub fn num_kind(ty: &Type) -> Option<NumKind> {
41    Some(match ty {
42        Type::U8 => NumKind::U8,
43        Type::U16 => NumKind::U16,
44        Type::U32 => NumKind::U32,
45        Type::U64 => NumKind::U64,
46        Type::I8 => NumKind::I8,
47        Type::I16 => NumKind::I16,
48        Type::I32 => NumKind::I32,
49        Type::I64 => NumKind::I64,
50        Type::F32 => NumKind::F32,
51        Type::F64 => NumKind::F64,
52        _ => return None,
53    })
54}
55
56/// Lossless conversions permitted at read time: identity, unsigned widening,
57/// signed widening, f32→f64. Anything else fails loudly at plan-build time.
58fn widenable(from: NumKind, to: NumKind) -> bool {
59    use NumKind::*;
60    if from == to {
61        return true;
62    }
63    matches!(
64        (from, to),
65        (U8, U16)
66            | (U8, U32)
67            | (U8, U64)
68            | (U16, U32)
69            | (U16, U64)
70            | (U32, U64)
71            | (I8, I16)
72            | (I8, I32)
73            | (I8, I64)
74            | (I16, I32)
75            | (I16, I64)
76            | (I32, I64)
77            | (F32, F64)
78    )
79}
80
81/// How to materialize a reader-visible value from writer bytes.
82#[derive(Clone, Debug)]
83pub enum Load {
84    Bool,
85    Num {
86        from: NumKind,
87        to: NumKind,
88    },
89    Enum,
90    Str,
91    Bytes,
92    /// Plan index. In a field slot this reads a u32 offset to the block; as a
93    /// list element the block is inline at the element position.
94    Struct(usize),
95    List(Box<ElemPlan>),
96    Map(Box<MapPlan>),
97    Union(Box<UnionPlan>),
98}
99
100/// Read plan for a `union<…>`: how to load each variant and where its payload
101/// sits within the union block (in the writer's layout). The u32 tag selects
102/// the variant.
103#[derive(Clone, Debug)]
104pub struct UnionPlan {
105    pub variants: Vec<VariantPlan>,
106}
107
108#[derive(Clone, Debug)]
109pub struct VariantPlan {
110    pub load: Load,
111    pub payload_off: u32,
112}
113
114/// Read plan for a `map<K, V>`: how to load a key and a value out of each entry
115/// block, plus that block's stride and the key/value offsets within it — all in
116/// the **writer's** entry layout.
117#[derive(Clone, Debug)]
118pub struct MapPlan {
119    pub key: Load,
120    pub value: Load,
121    pub stride: u32,
122    pub align: u32,
123    pub key_off: u32,
124    pub value_off: u32,
125}
126
127#[derive(Clone, Debug)]
128pub struct ElemPlan {
129    pub load: Load,
130    /// Element stride and alignment in the *writer's* layout.
131    pub stride: u32,
132    pub align: u32,
133    /// True when elements are fixed-size struct blocks stored inline (the
134    /// element position IS the block). False when the element position holds
135    /// a u32 offset to the value (strings, bytes, nested lists, and
136    /// variable-size *packed* struct elements).
137    pub struct_inline: bool,
138}
139
140#[derive(Clone, Debug)]
141pub enum FieldSource {
142    /// Field does not exist in the writer schema: reader sees `None`.
143    Absent,
144    /// Field lives at a constant offset (fixed/dense writer struct).
145    Slot {
146        /// Slot offset relative to the struct block base (writer layout).
147        offset: u32,
148        /// Presence bit position, relative to block base. A zero mask means
149        /// the field is always present (dense writer struct: no bitmap).
150        presence_byte: u32,
151        presence_mask: u8,
152        load: Load,
153    },
154    /// Field lives in a *packed* writer struct: its presence bit and byte
155    /// offset are recovered from the per-message bitmap via the writer's
156    /// [`crate::layout::PackedLayout`] (looked up by the plan's `writer_type`).
157    Packed {
158        /// Field position (ID-sorted) in the writer struct.
159        writer_pos: u32,
160        load: Load,
161    },
162}
163
164#[derive(Clone, Debug)]
165pub struct FieldPlan {
166    pub id: u16,
167    pub source: FieldSource,
168}
169
170#[derive(Clone, Debug)]
171pub struct StructPlan {
172    /// Index of the corresponding struct in the *reader* schema.
173    pub reader_type: u16,
174    /// Index of the corresponding struct in the *writer* schema.
175    pub writer_type: u16,
176    /// Whether the writer struct uses the packed layout (offsets are dynamic).
177    pub writer_packed: bool,
178    /// Parallel to the reader struct's ID-sorted fields.
179    pub fields: Vec<FieldPlan>,
180}
181
182#[derive(Clone, Debug)]
183pub struct Resolver {
184    writer: Schema,
185    reader: Schema,
186    plans: Vec<StructPlan>,
187    root_plan: usize,
188}
189
190impl Resolver {
191    /// Resolve a writer schema against a reader schema. Struct types
192    /// correspond structurally from the root downward; fields match by ID.
193    /// Incompatibilities (e.g. integer narrowing) error here — at plan time,
194    /// once — never as silent corruption at read time.
195    pub fn new(writer: &Schema, reader: &Schema) -> Result<Resolver> {
196        let mut b = PlanBuilder {
197            writer,
198            reader,
199            map: HashMap::new(),
200            plans: Vec::new(),
201        };
202        let root_plan = b.pair(writer.root_index(), reader.root_index(), 0)?;
203        Ok(Resolver {
204            writer: writer.clone(),
205            reader: reader.clone(),
206            plans: b.plans,
207            root_plan,
208        })
209    }
210
211    /// The fast path for reading data written with your own schema.
212    pub fn identity(schema: &Schema) -> Result<Resolver> {
213        Resolver::new(schema, schema)
214    }
215
216    pub fn writer_id(&self) -> u128 {
217        self.writer.id()
218    }
219
220    pub fn writer_schema(&self) -> &Schema {
221        &self.writer
222    }
223
224    pub fn reader_schema(&self) -> &Schema {
225        &self.reader
226    }
227
228    pub(crate) fn plan(&self, index: usize) -> &StructPlan {
229        &self.plans[index]
230    }
231
232    pub(crate) fn root_plan_index(&self) -> usize {
233        self.root_plan
234    }
235}
236
237struct PlanBuilder<'a> {
238    writer: &'a Schema,
239    reader: &'a Schema,
240    map: HashMap<(u16, u16), usize>,
241    plans: Vec<StructPlan>,
242}
243
244impl<'a> PlanBuilder<'a> {
245    fn pair(&mut self, writer_idx: u16, reader_idx: u16, depth: u32) -> Result<usize> {
246        if depth > MAX_RESOLVE_DEPTH {
247            return Err(Error::DepthLimitExceeded);
248        }
249        if let Some(&i) = self.map.get(&(writer_idx, reader_idx)) {
250            return Ok(i);
251        }
252        // Insert a placeholder first so recursive type references terminate.
253        let plan_idx = self.plans.len();
254        let writer_packed = self.writer.struct_def_unchecked(writer_idx).is_packed();
255        self.plans.push(StructPlan {
256            reader_type: reader_idx,
257            writer_type: writer_idx,
258            writer_packed,
259            fields: Vec::new(),
260        });
261        self.map.insert((writer_idx, reader_idx), plan_idx);
262
263        let ws = self.writer.struct_def_unchecked(writer_idx);
264        let rs = self.reader.struct_def_unchecked(reader_idx);
265        let mut fields = Vec::with_capacity(rs.fields.len());
266        for rf in &rs.fields {
267            let source = match ws.fields.binary_search_by_key(&rf.id, |f| f.id) {
268                Err(_) => FieldSource::Absent,
269                Ok(wpos) => {
270                    let wf = &ws.fields[wpos];
271                    let load = self
272                        .compat(&wf.ty, &rf.ty, depth + 1)
273                        .map_err(|e| match e {
274                            Error::Incompatible(msg) => Error::Incompatible(format!(
275                                "field {} (id {}): {msg}",
276                                rf.name, rf.id
277                            )),
278                            other => other,
279                        })?;
280                    if writer_packed {
281                        FieldSource::Packed {
282                            writer_pos: wpos as u32,
283                            load,
284                        }
285                    } else {
286                        let wlay = self.writer.layout_unchecked(writer_idx).as_fixed();
287                        FieldSource::Slot {
288                            offset: wlay.slots[wpos],
289                            presence_byte: if ws.is_dense() { 0 } else { wpos as u32 / 8 },
290                            presence_mask: if ws.is_dense() { 0 } else { 1 << (wpos % 8) },
291                            load,
292                        }
293                    }
294                }
295            };
296            fields.push(FieldPlan { id: rf.id, source });
297        }
298        self.plans[plan_idx].fields = fields;
299        Ok(plan_idx)
300    }
301
302    fn compat(&mut self, w: &Type, r: &Type, depth: u32) -> Result<Load> {
303        if depth > MAX_RESOLVE_DEPTH {
304            return Err(Error::DepthLimitExceeded);
305        }
306        if let (Some(from), Some(to)) = (num_kind(w), num_kind(r)) {
307            return if widenable(from, to) {
308                Ok(Load::Num { from, to })
309            } else {
310                Err(Error::Incompatible(format!(
311                    "cannot read writer {} as reader {} (only lossless widening is allowed)",
312                    w.describe(self.writer),
313                    r.describe(self.reader)
314                )))
315            };
316        }
317        match (w, r) {
318            (Type::Bool, Type::Bool) => Ok(Load::Bool),
319            (Type::String, Type::String) => Ok(Load::Str),
320            (Type::Bytes, Type::Bytes) => Ok(Load::Bytes),
321            // Enums are open u32s; variant names are documentation.
322            (Type::Enum(_), Type::Enum(_)) => Ok(Load::Enum),
323            (Type::Struct(wi), Type::Struct(ri)) => {
324                Ok(Load::Struct(self.pair(*wi, *ri, depth + 1)?))
325            }
326            (Type::List(we), Type::List(re)) => {
327                let load = self.compat(we, re, depth + 1)?;
328                let (stride, align, struct_inline) = writer_elem_stride_align(self.writer, we);
329                Ok(Load::List(Box::new(ElemPlan {
330                    load,
331                    stride,
332                    align,
333                    struct_inline,
334                })))
335            }
336            (Type::Map(wk, wv), Type::Map(rk, rv)) => {
337                // Keys must match exactly: widening a key could collapse two
338                // distinct keys or reorder entries. Values evolve like any field.
339                if wk != rk {
340                    return Err(Error::Incompatible(format!(
341                        "map key type changed: writer {} vs reader {}",
342                        wk.describe(self.writer),
343                        rk.describe(self.reader)
344                    )));
345                }
346                let key = self.compat(wk, rk, depth + 1)?;
347                let value = self.compat(wv, rv, depth + 1)?;
348                let lay = crate::layout::map_entry_layout(wk, wv);
349                Ok(Load::Map(Box::new(MapPlan {
350                    key,
351                    value,
352                    stride: lay.size,
353                    align: lay.align,
354                    key_off: lay.slots[0],
355                    value_off: lay.slots[1],
356                })))
357            }
358            (Type::Union(wv), Type::Union(rv)) => {
359                // Same number of variants; each variant value evolves like a
360                // field (widening ok). The tag is positional, so variant order
361                // is part of the type — a different count/order is incompatible.
362                if wv.len() != rv.len() {
363                    return Err(Error::Incompatible(format!(
364                        "union variant count changed: writer {} vs reader {}",
365                        wv.len(),
366                        rv.len()
367                    )));
368                }
369                let mut variants = Vec::with_capacity(wv.len());
370                for (w, r) in wv.iter().zip(rv) {
371                    variants.push(VariantPlan {
372                        load: self.compat(w, r, depth + 1)?,
373                        payload_off: crate::layout::union_payload_offset(w),
374                    });
375                }
376                Ok(Load::Union(Box::new(UnionPlan { variants })))
377            }
378            _ => Err(Error::Incompatible(format!(
379                "writer {} vs reader {}",
380                w.describe(self.writer),
381                r.describe(self.reader)
382            ))),
383        }
384    }
385}
386
387/// Element stride/alignment as laid down by the writer, plus whether struct
388/// elements are stored inline (fixed layout) or by u32 offset (packed).
389fn writer_elem_stride_align(writer: &Schema, elem: &Type) -> (u32, u32, bool) {
390    match elem {
391        Type::Struct(i) => match writer.layout_unchecked(*i) {
392            crate::layout::StructLayout::Fixed(f) => (f.size, f.align, true),
393            // Variable-size packed elements ride in the list by u32 offset.
394            crate::layout::StructLayout::Packed(_) => (4, 4, false),
395        },
396        Type::String | Type::Bytes | Type::List(_) => (4, 4, false),
397        other => {
398            let (s, a) = slot_size_align(other);
399            (s, a, false)
400        }
401    }
402}