Skip to main content

phon_engine/
compact.rs

1//! The compact (schema-driven) codec for the dynamic [`Value`].
2//!
3//! Compact mode carries no tags and no names: the schema says what comes next,
4//! so the bytes are just the values, back to back, with alignment padding before
5//! aligned scalars (`r[compact.schema-driven]`, `r[compact.alignment]`). This
6//! module encodes/decodes a `facet_value::Value` against a schema — the
7//! schema-less counterpart to the self-describing `Value` codec.
8//!
9//! References are resolved against a [`Registry`] (a schema closure); primitives
10//! are recognized by their canonical id intrinsically. This first cut covers
11//! primitives (except datetime/uuid/qname), struct, enum, tuple, list, set, map,
12//! array, option, and dynamic; tensor, the string-carried primitives, channel,
13//! external, and generics are not yet wired and return
14//! [`CompactError::Unsupported`].
15//!
16//! Spec: "Compact mode".
17
18use std::collections::{HashMap, HashSet};
19
20use facet_value::{VArray, VBytes, VObject, VString, Value};
21use phon_schema::bytes::{
22    Reader, write_bool, write_f32, write_f64, write_i8, write_i16, write_i32, write_i64,
23    write_i128, write_u8, write_u16, write_u32, write_u64, write_u128,
24};
25use phon_schema::{
26    DecodeError, EncodeError, Field, Primitive, Schema, SchemaId, SchemaKind, SchemaRef, Variant,
27    VariantPayload, extended_from_string, extended_to_string, primitive_id, read_value,
28    resolve_ids, write_value,
29};
30
31/// Maximum nesting depth on decode (`r[validate.depth]`).
32const MAX_DEPTH: usize = 128;
33
34// ============================================================================
35// Errors
36// ============================================================================
37
38/// Why a compact encode or decode failed.
39#[derive(Clone, Debug, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum CompactError {
42    /// A referenced schema id is not in the registry (`r[schema-identity.unknown-is-error]`).
43    // r[impl schema-identity.unknown-is-error]
44    UnknownSchema(SchemaId),
45    /// A received schema bundle member's stated id did not match its content hash.
46    BundleSchemaIdMismatch {
47        stated: SchemaId,
48        recomputed: SchemaId,
49    },
50    /// A kind or feature not yet implemented in this codec.
51    Unsupported(&'static str),
52    /// The value's shape does not match the schema it is being encoded against.
53    TypeMismatch { expected: &'static str },
54    /// An enum value names a variant the schema does not have.
55    UnknownVariant(String),
56    /// A decoded enum variant index is out of range.
57    BadVariantIndex(u32),
58    /// A generic schema applied with the wrong number of type arguments.
59    GenericArity { params: usize, args: usize },
60    /// A structurally malformed schema (e.g. an unbound type variable, or a
61    /// primitive carrying type arguments).
62    Malformed(&'static str),
63    /// Two schemas cannot be translated by a compatibility plan (`r[compat.*]`).
64    Incompatible(String),
65    /// A decoded enum variant exists in the writer schema but has no counterpart
66    /// in the reader schema (`r[compat.enum]`).
67    WriterOnlyVariant(u32),
68    /// A decode-side validation failure from the byte reader.
69    Decode(DecodeError),
70    /// A dynamic (self-describing) sub-value failed to encode.
71    Encode(EncodeError),
72}
73
74impl From<DecodeError> for CompactError {
75    fn from(e: DecodeError) -> Self {
76        CompactError::Decode(e)
77    }
78}
79
80impl From<EncodeError> for CompactError {
81    fn from(e: EncodeError) -> Self {
82        CompactError::Encode(e)
83    }
84}
85
86impl core::fmt::Display for CompactError {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        match self {
89            CompactError::UnknownSchema(id) => write!(f, "unknown schema {id}"),
90            CompactError::BundleSchemaIdMismatch { stated, recomputed } => {
91                write!(
92                    f,
93                    "schema bundle id mismatch: stated {stated}, recomputed {recomputed}"
94                )
95            }
96            CompactError::Unsupported(what) => {
97                write!(f, "compact codec does not support {what} yet")
98            }
99            CompactError::TypeMismatch { expected } => {
100                write!(f, "value does not match schema (expected {expected})")
101            }
102            CompactError::UnknownVariant(name) => write!(f, "unknown enum variant {name:?}"),
103            CompactError::BadVariantIndex(i) => write!(f, "enum variant index {i} out of range"),
104            CompactError::GenericArity { params, args } => {
105                write!(f, "generic expects {params} type arguments, got {args}")
106            }
107            CompactError::Malformed(what) => write!(f, "malformed schema: {what}"),
108            CompactError::Incompatible(why) => write!(f, "incompatible schemas: {why}"),
109            CompactError::WriterOnlyVariant(i) => {
110                write!(
111                    f,
112                    "received enum variant {i} the reader schema does not have"
113                )
114            }
115            CompactError::Decode(e) => write!(f, "decode: {e}"),
116            CompactError::Encode(e) => write!(f, "encode: {e}"),
117        }
118    }
119}
120
121impl std::error::Error for CompactError {}
122
123type Result<T> = core::result::Result<T, CompactError>;
124
125// ============================================================================
126// Registry
127// ============================================================================
128
129/// A resolved schema closure: composite schemas by id, plus intrinsic
130/// recognition of the primitive ids.
131pub struct Registry {
132    composites: HashMap<SchemaId, Schema>,
133    primitives: HashMap<SchemaId, Primitive>,
134}
135
136impl Registry {
137    /// Build a registry from a closure of composite schemas. Primitive schemas
138    /// need not be supplied — they are recognized by their canonical id.
139    #[must_use]
140    pub fn new(schemas: impl IntoIterator<Item = Schema>) -> Self {
141        let primitives = Primitive::ALL
142            .iter()
143            .map(|&p| (primitive_id(p), p))
144            .collect();
145        let composites = schemas.into_iter().map(|s| (s.id, s)).collect();
146        Registry {
147            composites,
148            primitives,
149        }
150    }
151
152    /// Validate a received schema closure before making it executable.
153    ///
154    /// This recomputes every member's content-derived [`SchemaId`], rejects
155    /// references that are neither primitive nor present in the bundle, and
156    /// bounds fixed arrays whose elements have zero wire size.
157    // r[impl validate.bundles]
158    pub fn try_new(schemas: impl IntoIterator<Item = Schema>) -> Result<Self> {
159        let schemas: Vec<_> = schemas.into_iter().collect();
160        validate_bundle(&schemas)?;
161        Ok(Self::new(schemas))
162    }
163
164    fn primitive(&self, id: SchemaId) -> Option<Primitive> {
165        self.primitives.get(&id).copied()
166    }
167
168    fn composite(&self, id: SchemaId) -> Option<&Schema> {
169        self.composites.get(&id)
170    }
171}
172
173fn validate_bundle(schemas: &[Schema]) -> Result<()> {
174    let recomputed = resolve_ids(schemas.to_vec());
175    for (schema, recomputed) in schemas.iter().zip(&recomputed) {
176        if schema.id != recomputed.id {
177            return Err(CompactError::BundleSchemaIdMismatch {
178                stated: schema.id,
179                recomputed: recomputed.id,
180            });
181        }
182    }
183
184    let provided: HashSet<_> = schemas.iter().map(|schema| schema.id).collect();
185    let primitives: HashSet<_> = Primitive::ALL.iter().map(|&p| primitive_id(p)).collect();
186    for schema in schemas {
187        validate_kind_refs(&schema.kind, &provided, &primitives)?;
188    }
189
190    let reg = Registry::new(schemas.to_vec());
191    for schema in schemas {
192        validate_fixed_array_caps(&schema.kind, &reg)?;
193    }
194
195    Ok(())
196}
197
198fn validate_kind_refs(
199    kind: &SchemaKind,
200    provided: &HashSet<SchemaId>,
201    primitives: &HashSet<SchemaId>,
202) -> Result<()> {
203    match kind {
204        SchemaKind::Primitive(_) | SchemaKind::Dynamic => Ok(()),
205        SchemaKind::Struct { fields, .. } => fields
206            .iter()
207            .try_for_each(|field| validate_ref(&field.schema, provided, primitives)),
208        SchemaKind::Enum { variants, .. } => variants
209            .iter()
210            .try_for_each(|variant| validate_payload_refs(&variant.payload, provided, primitives)),
211        SchemaKind::Tuple { elements } => elements
212            .iter()
213            .try_for_each(|element| validate_ref(element, provided, primitives)),
214        SchemaKind::List { element }
215        | SchemaKind::Set { element }
216        | SchemaKind::Array { element, .. }
217        | SchemaKind::Tensor { element, .. }
218        | SchemaKind::Option { element }
219        | SchemaKind::Channel { element, .. } => validate_ref(element, provided, primitives),
220        SchemaKind::Map { key, value } => {
221            validate_ref(key, provided, primitives)?;
222            validate_ref(value, provided, primitives)
223        }
224        SchemaKind::External { metadata, .. } => {
225            if let Some(metadata) = metadata {
226                validate_ref(metadata, provided, primitives)?;
227            }
228            Ok(())
229        }
230    }
231}
232
233fn validate_payload_refs(
234    payload: &VariantPayload,
235    provided: &HashSet<SchemaId>,
236    primitives: &HashSet<SchemaId>,
237) -> Result<()> {
238    match payload {
239        VariantPayload::Unit => Ok(()),
240        VariantPayload::Newtype(r) => validate_ref(r, provided, primitives),
241        VariantPayload::Tuple(elements) => elements
242            .iter()
243            .try_for_each(|element| validate_ref(element, provided, primitives)),
244        VariantPayload::Struct(fields) => fields
245            .iter()
246            .try_for_each(|field| validate_ref(&field.schema, provided, primitives)),
247    }
248}
249
250fn validate_ref(
251    r: &SchemaRef,
252    provided: &HashSet<SchemaId>,
253    primitives: &HashSet<SchemaId>,
254) -> Result<()> {
255    match r {
256        SchemaRef::Var { .. } => Ok(()),
257        SchemaRef::Concrete { id, args } => {
258            if !provided.contains(id) && !primitives.contains(id) {
259                return Err(CompactError::UnknownSchema(*id));
260            }
261            args.iter()
262                .try_for_each(|arg| validate_ref(arg, provided, primitives))
263        }
264    }
265}
266
267fn validate_fixed_array_caps(kind: &SchemaKind, reg: &Registry) -> Result<()> {
268    match kind {
269        SchemaKind::Primitive(_) | SchemaKind::Dynamic => Ok(()),
270        SchemaKind::Struct { fields, .. } => fields
271            .iter()
272            .try_for_each(|field| validate_fixed_array_ref(&field.schema)),
273        SchemaKind::Enum { variants, .. } => variants
274            .iter()
275            .try_for_each(|variant| validate_fixed_array_payload(&variant.payload)),
276        SchemaKind::Tuple { elements } => elements.iter().try_for_each(validate_fixed_array_ref),
277        SchemaKind::List { element }
278        | SchemaKind::Set { element }
279        | SchemaKind::Tensor { element, .. }
280        | SchemaKind::Option { element }
281        | SchemaKind::Channel { element, .. } => validate_fixed_array_ref(element),
282        SchemaKind::Map { key, value } => {
283            validate_fixed_array_ref(key)?;
284            validate_fixed_array_ref(value)
285        }
286        SchemaKind::Array {
287            element,
288            dimensions,
289        } => {
290            let count = product(dimensions)?;
291            if min_wire_size_ref(reg, element) == 0
292                && count > phon_schema::bytes::ZST_COUNT_CAP as u64
293            {
294                return Err(CompactError::Decode(DecodeError::LengthTooLarge {
295                    count,
296                    remaining: phon_schema::bytes::ZST_COUNT_CAP,
297                }));
298            }
299            validate_fixed_array_ref(element)
300        }
301        SchemaKind::External { metadata, .. } => {
302            if let Some(metadata) = metadata {
303                validate_fixed_array_ref(metadata)?;
304            }
305            Ok(())
306        }
307    }
308}
309
310fn validate_fixed_array_payload(payload: &VariantPayload) -> Result<()> {
311    match payload {
312        VariantPayload::Unit => Ok(()),
313        VariantPayload::Newtype(r) => validate_fixed_array_ref(r),
314        VariantPayload::Tuple(elements) => elements.iter().try_for_each(validate_fixed_array_ref),
315        VariantPayload::Struct(fields) => fields
316            .iter()
317            .try_for_each(|field| validate_fixed_array_ref(&field.schema)),
318    }
319}
320
321fn validate_fixed_array_ref(r: &SchemaRef) -> Result<()> {
322    match r {
323        SchemaRef::Var { .. } => Ok(()),
324        SchemaRef::Concrete { args, .. } => args.iter().try_for_each(validate_fixed_array_ref),
325    }
326}
327
328/// A reference resolved to either a primitive or a fully type-substituted kind.
329pub(crate) enum Resolved {
330    Primitive(Primitive),
331    Composite(SchemaKind),
332}
333
334/// Resolve a reference against the registry, applying generic substitution.
335/// Shared by the compact codec's walks and the compatibility planner.
336// r[impl type-system.generic-resolution]
337pub(crate) fn resolve(reg: &Registry, r: &SchemaRef) -> Result<Resolved> {
338    match r {
339        SchemaRef::Var { .. } => Err(CompactError::Malformed("unbound type variable")),
340        SchemaRef::Concrete { id, args } => {
341            if let Some(p) = reg.primitive(*id) {
342                if !args.is_empty() {
343                    return Err(CompactError::Malformed("primitive carrying type arguments"));
344                }
345                Ok(Resolved::Primitive(p))
346            } else if let Some(schema) = reg.composite(*id) {
347                if schema.type_params.len() != args.len() {
348                    return Err(CompactError::GenericArity {
349                        params: schema.type_params.len(),
350                        args: args.len(),
351                    });
352                }
353                let kind = if args.is_empty() {
354                    schema.kind.clone()
355                } else {
356                    substitute_kind(&schema.kind, &schema.type_params, args)
357                };
358                Ok(Resolved::Composite(kind))
359            } else {
360                Err(CompactError::UnknownSchema(*id))
361            }
362        }
363    }
364}
365
366// ============================================================================
367// Alignment
368// ============================================================================
369
370// r[impl compact.alignment]
371pub(crate) fn alignment(p: Primitive) -> usize {
372    match p {
373        Primitive::U16 | Primitive::I16 => 2,
374        Primitive::U32 | Primitive::I32 | Primitive::F32 | Primitive::Char => 4,
375        Primitive::U64 | Primitive::I64 | Primitive::F64 => 8,
376        Primitive::U128 | Primitive::I128 => 16,
377        _ => 1,
378    }
379}
380
381// Padding lives in phon-schema (shared with the typed path and the JIT);
382// re-exported so the compact codec, interpreter, and typed path keep reaching it
383// as `compact::pad_to` / `compact::skip_pad`.
384pub(crate) use phon_schema::bytes::{pad_to, skip_pad};
385
386// ============================================================================
387// Minimum wire size
388// ============================================================================
389//
390// The length guard `r[validate.lengths]` bounds a wire-driven count by
391// `bytes_remaining / min_element_size`. That premise breaks for *zero-sized*
392// elements (`unit`, an empty struct, an aggregate of only those), whose wire
393// footprint is 0: a list of them is just the count, and the buffer empties
394// immediately, so the buffer offers no honest bound. `read_len(0)` switches to a
395// fixed cap for exactly that case.
396//
397// This helper computes whether a referenced element is zero-sized, returning the
398// minimum wire size to hand `read_len`: `0` for a zero-sized element, `1`
399// otherwise. We need only the zero/nonzero distinction — `1` is the same loose
400// lower bound the codec has always used for nonzero elements — so anything we
401// cannot prove zero-sized (an unresolvable ref, a cycle, an unsupported kind)
402// conservatively reports `1`.
403
404const MIN_WIRE_DEPTH: usize = 64;
405
406/// The minimum wire size to pass `read_len` for a sequence element of schema
407/// `rf`: `0` only when the element provably encodes to zero bytes, else `1`.
408pub(crate) fn min_wire_size_ref(reg: &Registry, rf: &SchemaRef) -> usize {
409    usize::from(!is_zero_sized_ref(reg, rf, 0))
410}
411
412fn is_zero_sized_ref(reg: &Registry, rf: &SchemaRef, depth: usize) -> bool {
413    if depth > MIN_WIRE_DEPTH {
414        return false;
415    }
416    match resolve(reg, rf) {
417        Ok(Resolved::Primitive(p)) => is_zero_sized_primitive(p),
418        Ok(Resolved::Composite(kind)) => is_zero_sized_kind(reg, &kind, depth),
419        Err(_) => false,
420    }
421}
422
423fn is_zero_sized_primitive(p: Primitive) -> bool {
424    // `unit` writes nothing; `never` is uninhabited (it can't appear as a real
425    // element, but reporting it nonzero is the safe default). Every other
426    // primitive writes at least one byte.
427    matches!(p, Primitive::Unit)
428}
429
430fn is_zero_sized_kind(reg: &Registry, kind: &SchemaKind, depth: usize) -> bool {
431    match kind {
432        SchemaKind::Primitive(p) => is_zero_sized_primitive(*p),
433        // A struct/tuple is zero-sized iff every field/element is.
434        SchemaKind::Struct { fields, .. } => fields
435            .iter()
436            .all(|f| is_zero_sized_ref(reg, &f.schema, depth + 1)),
437        SchemaKind::Tuple { elements } => elements
438            .iter()
439            .all(|e| is_zero_sized_ref(reg, e, depth + 1)),
440        // A fixed array is zero-sized iff its element is (regardless of dims).
441        SchemaKind::Array { element, .. } => is_zero_sized_ref(reg, element, depth + 1),
442        // Everything else carries at least one wire byte: a list/set/map writes a
443        // u32 count; an option a presence byte; an enum a u32 tag; dynamic a tag;
444        // strings/bytes a length. None are zero-sized.
445        _ => false,
446    }
447}
448
449// ============================================================================
450// Public API
451// ============================================================================
452
453/// Encode `value` against the schema named by `root` in `registry`.
454///
455/// # Errors
456/// [`CompactError`] if the value does not match the schema, a referenced schema
457/// is missing, or the codec does not yet support a kind in play.
458pub fn to_bytes(value: &Value, root: SchemaId, registry: &Registry) -> Result<Vec<u8>> {
459    let mut out = Vec::new();
460    encode_ref(value, &SchemaRef::concrete(root), registry, &mut out)?;
461    Ok(out)
462}
463
464/// Decode a value of schema `root` from `bytes`, rejecting trailing bytes.
465///
466/// # Errors
467/// [`CompactError`] for malformed input or an unsupported kind.
468pub fn from_bytes(bytes: &[u8], root: SchemaId, registry: &Registry) -> Result<Value> {
469    let mut r = Reader::new(bytes);
470    let v = read_from(&mut r, root, registry)?;
471    if r.remaining() != 0 {
472        return Err(CompactError::Decode(DecodeError::TrailingBytes(
473            r.remaining(),
474        )));
475    }
476    Ok(v)
477}
478
479/// Decode one compact value from an existing message cursor.
480///
481/// The reader is advanced by exactly the bytes consumed by `root`; callers can
482/// inspect [`Reader::position`] and decode the next value from the same message.
483// r[impl decode.chained]
484pub fn read_from(r: &mut Reader<'_>, root: SchemaId, registry: &Registry) -> Result<Value> {
485    decode_ref(r, &SchemaRef::concrete(root), registry, 0)
486}
487
488// ============================================================================
489// Encoding
490// ============================================================================
491
492// r[impl type-system.generic-resolution]
493fn encode_ref(value: &Value, r: &SchemaRef, reg: &Registry, out: &mut Vec<u8>) -> Result<()> {
494    match r {
495        SchemaRef::Var { .. } => Err(CompactError::Malformed("unbound type variable")),
496        SchemaRef::Concrete { id, args } => {
497            if let Some(p) = reg.primitive(*id) {
498                if !args.is_empty() {
499                    return Err(CompactError::Malformed("primitive carrying type arguments"));
500                }
501                encode_primitive(value, p, out)
502            } else if let Some(schema) = reg.composite(*id) {
503                if schema.type_params.len() != args.len() {
504                    return Err(CompactError::GenericArity {
505                        params: schema.type_params.len(),
506                        args: args.len(),
507                    });
508                }
509                if args.is_empty() {
510                    encode_kind(value, &schema.kind, reg, out)
511                } else {
512                    let kind = substitute_kind(&schema.kind, &schema.type_params, args);
513                    encode_kind(value, &kind, reg, out)
514                }
515            } else {
516                Err(CompactError::UnknownSchema(*id))
517            }
518        }
519    }
520}
521
522fn number(value: &Value) -> Result<&facet_value::VNumber> {
523    value
524        .as_number()
525        .ok_or(CompactError::TypeMismatch { expected: "number" })
526}
527
528fn encode_primitive(value: &Value, p: Primitive, out: &mut Vec<u8>) -> Result<()> {
529    pad_to(out, alignment(p));
530    match p {
531        Primitive::Bool => write_bool(
532            out,
533            value
534                .as_bool()
535                .ok_or(CompactError::TypeMismatch { expected: "bool" })?,
536        ),
537        Primitive::U8 => write_u8(out, number(value)?.to_u64().unwrap_or(0) as u8),
538        Primitive::U16 => write_u16(out, number(value)?.to_u64().unwrap_or(0) as u16),
539        Primitive::U32 => write_u32(out, number(value)?.to_u64().unwrap_or(0) as u32),
540        Primitive::U64 => write_u64(out, number(value)?.to_u64().unwrap_or(0)),
541        Primitive::U128 => write_u128(out, number(value)?.to_u128().unwrap_or(0)),
542        Primitive::I8 => write_i8(out, number(value)?.to_i64().unwrap_or(0) as i8),
543        Primitive::I16 => write_i16(out, number(value)?.to_i64().unwrap_or(0) as i16),
544        Primitive::I32 => write_i32(out, number(value)?.to_i64().unwrap_or(0) as i32),
545        Primitive::I64 => write_i64(out, number(value)?.to_i64().unwrap_or(0)),
546        Primitive::I128 => write_i128(out, number(value)?.to_i128().unwrap_or(0)),
547        Primitive::F32 => write_f32(out, number(value)?.to_f64_lossy() as f32),
548        Primitive::F64 => write_f64(out, number(value)?.to_f64_lossy()),
549        Primitive::Char => write_u32(
550            out,
551            value
552                .as_char()
553                .ok_or(CompactError::TypeMismatch { expected: "char" })? as u32,
554        ),
555        Primitive::String => {
556            let s = value
557                .as_string()
558                .ok_or(CompactError::TypeMismatch { expected: "string" })?;
559            write_u32(out, s.as_str().len() as u32);
560            out.extend_from_slice(s.as_str().as_bytes());
561        }
562        Primitive::Bytes => {
563            let b = value
564                .as_bytes()
565                .ok_or(CompactError::TypeMismatch { expected: "bytes" })?;
566            write_u32(out, b.as_slice().len() as u32);
567            out.extend_from_slice(b.as_slice());
568        }
569        Primitive::Unit => {
570            if !value.is_null() {
571                return Err(CompactError::TypeMismatch { expected: "unit" });
572            }
573        }
574        Primitive::Never => return Err(CompactError::TypeMismatch { expected: "never" }),
575        Primitive::DateTime | Primitive::Uuid | Primitive::QName => {
576            let s = extended_to_string(value, p).map_err(CompactError::Encode)?;
577            write_u32(out, s.len() as u32);
578            out.extend_from_slice(s.as_bytes());
579        }
580    }
581    Ok(())
582}
583
584// r[impl compact.schema-driven]
585fn encode_kind(value: &Value, kind: &SchemaKind, reg: &Registry, out: &mut Vec<u8>) -> Result<()> {
586    match kind {
587        SchemaKind::Primitive(p) => encode_primitive(value, *p, out),
588        SchemaKind::Struct { fields, .. } => {
589            let obj = value
590                .as_object()
591                .ok_or(CompactError::TypeMismatch { expected: "object" })?;
592            for field in fields {
593                let fv = obj
594                    .get(&VString::new(&field.name))
595                    .ok_or(CompactError::TypeMismatch {
596                        expected: "struct field",
597                    })?;
598                encode_ref(fv, &field.schema, reg, out)?;
599            }
600            Ok(())
601        }
602        SchemaKind::Tuple { elements } => {
603            let arr = value
604                .as_array()
605                .ok_or(CompactError::TypeMismatch { expected: "tuple" })?;
606            if arr.len() != elements.len() {
607                return Err(CompactError::TypeMismatch {
608                    expected: "tuple arity",
609                });
610            }
611            for (i, e) in elements.iter().enumerate() {
612                encode_ref(arr.get(i).unwrap(), e, reg, out)?;
613            }
614            Ok(())
615        }
616        SchemaKind::List { element } | SchemaKind::Set { element } => {
617            let arr = value
618                .as_array()
619                .ok_or(CompactError::TypeMismatch { expected: "list" })?;
620            write_u32(out, arr.len() as u32);
621            for i in 0..arr.len() {
622                encode_ref(arr.get(i).unwrap(), element, reg, out)?;
623            }
624            Ok(())
625        }
626        SchemaKind::Array {
627            element,
628            dimensions,
629        } => {
630            let count = product(dimensions)?;
631            let arr = value
632                .as_array()
633                .ok_or(CompactError::TypeMismatch { expected: "array" })?;
634            if arr.len() as u64 != count {
635                return Err(CompactError::TypeMismatch {
636                    expected: "array shape",
637                });
638            }
639            for i in 0..arr.len() {
640                encode_ref(arr.get(i).unwrap(), element, reg, out)?;
641            }
642            Ok(())
643        }
644        SchemaKind::Map { key, value: val } => {
645            let obj = value
646                .as_object()
647                .ok_or(CompactError::TypeMismatch { expected: "map" })?;
648            write_u32(out, obj.len() as u32);
649            for (k, v) in obj.iter() {
650                encode_ref(&Value::from(VString::new(k.as_str())), key, reg, out)?;
651                encode_ref(v, val, reg, out)?;
652            }
653            Ok(())
654        }
655        SchemaKind::Option { element } => {
656            if value.is_null() {
657                write_u8(out, 0);
658            } else {
659                write_u8(out, 1);
660                encode_ref(value, element, reg, out)?;
661            }
662            Ok(())
663        }
664        SchemaKind::Enum { variants, .. } => {
665            let obj = value.as_object().ok_or(CompactError::TypeMismatch {
666                expected: "enum object",
667            })?;
668            if obj.len() != 1 {
669                return Err(CompactError::TypeMismatch {
670                    expected: "single-variant enum object",
671                });
672            }
673            let (name, payload) = obj.iter().next().unwrap();
674            let variant = variants
675                .iter()
676                .find(|v| v.name == name.as_str())
677                .ok_or_else(|| CompactError::UnknownVariant(name.as_str().to_string()))?;
678            write_u32(out, variant.index);
679            encode_payload(payload, &variant.payload, reg, out)
680        }
681        SchemaKind::Dynamic => {
682            write_value(out, value)?;
683            Ok(())
684        }
685        SchemaKind::Tensor { .. } => Err(CompactError::Unsupported("tensor")),
686        SchemaKind::Channel { .. } => Err(CompactError::Unsupported("channel")),
687        SchemaKind::External { .. } => Err(CompactError::Unsupported("external")),
688    }
689}
690
691fn encode_payload(
692    value: &Value,
693    payload: &VariantPayload,
694    reg: &Registry,
695    out: &mut Vec<u8>,
696) -> Result<()> {
697    match payload {
698        VariantPayload::Unit => Ok(()),
699        VariantPayload::Newtype(r) => encode_ref(value, r, reg, out),
700        VariantPayload::Tuple(refs) => {
701            let arr = value.as_array().ok_or(CompactError::TypeMismatch {
702                expected: "tuple variant",
703            })?;
704            if arr.len() != refs.len() {
705                return Err(CompactError::TypeMismatch {
706                    expected: "tuple variant arity",
707                });
708            }
709            for (i, r) in refs.iter().enumerate() {
710                encode_ref(arr.get(i).unwrap(), r, reg, out)?;
711            }
712            Ok(())
713        }
714        VariantPayload::Struct(fields) => {
715            let obj = value.as_object().ok_or(CompactError::TypeMismatch {
716                expected: "struct variant",
717            })?;
718            for field in fields {
719                let fv = obj
720                    .get(&VString::new(&field.name))
721                    .ok_or(CompactError::TypeMismatch {
722                        expected: "struct variant field",
723                    })?;
724                encode_ref(fv, &field.schema, reg, out)?;
725            }
726            Ok(())
727        }
728    }
729}
730
731pub(crate) fn product(dimensions: &[u64]) -> Result<u64> {
732    let mut p: u64 = 1;
733    for &d in dimensions {
734        p = p
735            .checked_mul(d)
736            .ok_or(CompactError::Decode(DecodeError::Malformed(
737                "array dimensions overflow",
738            )))?;
739    }
740    Ok(p)
741}
742
743/// Bound a *fixed-array* element `count` (`product(dimensions)`) before the
744/// construction loop. With each element costing at least `min_wire` bytes the
745/// count may not exceed `remaining / min_wire`; for a zero-sized element
746/// (`min_wire == 0`) the buffer gives no bound, so a fixed cap applies — the
747/// decoder-side mirror of the schema-side cap in `r[validate.bundles]`.
748/// (`r[validate.dimensions]`.)
749pub(crate) fn check_fixed_count(count: u64, min_wire: usize, remaining: usize) -> Result<()> {
750    // `checked_div` is `None` exactly when `min_wire == 0` — the zero-sized case,
751    // where the buffer offers no bound and the fixed cap applies.
752    let max = remaining
753        .checked_div(min_wire)
754        .unwrap_or(phon_schema::bytes::ZST_COUNT_CAP) as u64;
755    if count > max {
756        return Err(CompactError::Decode(DecodeError::LengthTooLarge {
757            count,
758            remaining,
759        }));
760    }
761    Ok(())
762}
763
764// ============================================================================
765// Generic resolution
766// ============================================================================
767//
768// Resolving a parametric schema substitutes its type parameters with the
769// arguments from a concrete reference, throughout its kind. Substitution is
770// eager and per-reference: each `Concrete { id, args }` produces a Var-free kind
771// before it is walked, so the walker never meets a `Var` (`r[type-system.generic-resolution]`).
772
773fn substitute_ref(r: &SchemaRef, params: &[String], args: &[SchemaRef]) -> SchemaRef {
774    match r {
775        SchemaRef::Var { name } => params
776            .iter()
777            .position(|p| p == name)
778            .map(|i| args[i].clone())
779            .unwrap_or_else(|| r.clone()),
780        SchemaRef::Concrete { id, args: inner } => SchemaRef::Concrete {
781            id: *id,
782            args: inner
783                .iter()
784                .map(|a| substitute_ref(a, params, args))
785                .collect(),
786        },
787    }
788}
789
790fn substitute_field(f: &Field, params: &[String], args: &[SchemaRef]) -> Field {
791    Field {
792        name: f.name.clone(),
793        schema: substitute_ref(&f.schema, params, args),
794        required: f.required,
795    }
796}
797
798fn substitute_kind(kind: &SchemaKind, params: &[String], args: &[SchemaRef]) -> SchemaKind {
799    match kind {
800        SchemaKind::Primitive(p) => SchemaKind::Primitive(*p),
801        SchemaKind::Dynamic => SchemaKind::Dynamic,
802        SchemaKind::Struct { name, fields } => SchemaKind::Struct {
803            name: name.clone(),
804            fields: fields
805                .iter()
806                .map(|f| substitute_field(f, params, args))
807                .collect(),
808        },
809        SchemaKind::Enum { name, variants } => SchemaKind::Enum {
810            name: name.clone(),
811            variants: variants
812                .iter()
813                .map(|v| Variant {
814                    name: v.name.clone(),
815                    index: v.index,
816                    payload: match &v.payload {
817                        VariantPayload::Unit => VariantPayload::Unit,
818                        VariantPayload::Newtype(r) => {
819                            VariantPayload::Newtype(substitute_ref(r, params, args))
820                        }
821                        VariantPayload::Tuple(rs) => VariantPayload::Tuple(
822                            rs.iter().map(|r| substitute_ref(r, params, args)).collect(),
823                        ),
824                        VariantPayload::Struct(fs) => VariantPayload::Struct(
825                            fs.iter()
826                                .map(|f| substitute_field(f, params, args))
827                                .collect(),
828                        ),
829                    },
830                })
831                .collect(),
832        },
833        SchemaKind::Tuple { elements } => SchemaKind::Tuple {
834            elements: elements
835                .iter()
836                .map(|r| substitute_ref(r, params, args))
837                .collect(),
838        },
839        SchemaKind::List { element } => SchemaKind::List {
840            element: substitute_ref(element, params, args),
841        },
842        SchemaKind::Set { element } => SchemaKind::Set {
843            element: substitute_ref(element, params, args),
844        },
845        SchemaKind::Option { element } => SchemaKind::Option {
846            element: substitute_ref(element, params, args),
847        },
848        SchemaKind::Map { key, value } => SchemaKind::Map {
849            key: substitute_ref(key, params, args),
850            value: substitute_ref(value, params, args),
851        },
852        SchemaKind::Array {
853            element,
854            dimensions,
855        } => SchemaKind::Array {
856            element: substitute_ref(element, params, args),
857            dimensions: dimensions.clone(),
858        },
859        SchemaKind::Tensor { element, rank } => SchemaKind::Tensor {
860            element: substitute_ref(element, params, args),
861            rank: *rank,
862        },
863        SchemaKind::Channel { direction, element } => SchemaKind::Channel {
864            direction: *direction,
865            element: substitute_ref(element, params, args),
866        },
867        SchemaKind::External { kind, metadata } => SchemaKind::External {
868            kind: kind.clone(),
869            metadata: metadata.as_ref().map(|r| substitute_ref(r, params, args)),
870        },
871    }
872}
873
874// ============================================================================
875// Decoding
876// ============================================================================
877
878pub(crate) fn decode_ref(
879    r: &mut Reader,
880    rf: &SchemaRef,
881    reg: &Registry,
882    depth: usize,
883) -> Result<Value> {
884    if depth > MAX_DEPTH {
885        return Err(CompactError::Decode(DecodeError::DepthExceeded));
886    }
887    match rf {
888        SchemaRef::Var { .. } => Err(CompactError::Malformed("unbound type variable")),
889        SchemaRef::Concrete { id, args } => {
890            if let Some(p) = reg.primitive(*id) {
891                if !args.is_empty() {
892                    return Err(CompactError::Malformed("primitive carrying type arguments"));
893                }
894                decode_primitive(r, p)
895            } else if let Some(schema) = reg.composite(*id) {
896                if schema.type_params.len() != args.len() {
897                    return Err(CompactError::GenericArity {
898                        params: schema.type_params.len(),
899                        args: args.len(),
900                    });
901                }
902                if args.is_empty() {
903                    decode_kind(r, &schema.kind, reg, depth + 1)
904                } else {
905                    let kind = substitute_kind(&schema.kind, &schema.type_params, args);
906                    decode_kind(r, &kind, reg, depth + 1)
907                }
908            } else {
909                Err(CompactError::UnknownSchema(*id))
910            }
911        }
912    }
913}
914
915pub(crate) fn decode_primitive(r: &mut Reader, p: Primitive) -> Result<Value> {
916    skip_pad(r, alignment(p))?;
917    Ok(match p {
918        Primitive::Bool => Value::from(r.read_bool()?),
919        Primitive::U8 => Value::from(r.read_u8()?),
920        Primitive::U16 => Value::from(r.read_u16()?),
921        Primitive::U32 => Value::from(r.read_u32()?),
922        Primitive::U64 => Value::from(r.read_u64()?),
923        Primitive::U128 => Value::from(r.read_u128()?),
924        Primitive::I8 => Value::from(r.read_i8()?),
925        Primitive::I16 => Value::from(r.read_i16()?),
926        Primitive::I32 => Value::from(r.read_i32()?),
927        Primitive::I64 => Value::from(r.read_i64()?),
928        Primitive::I128 => Value::from(r.read_i128()?),
929        Primitive::F32 => Value::from(r.read_f32()?),
930        Primitive::F64 => Value::from(r.read_f64()?),
931        Primitive::Char => Value::from(r.read_char()?),
932        Primitive::String => VString::new(r.read_str()?).into(),
933        Primitive::Bytes => VBytes::new(r.read_bytes()?).into(),
934        Primitive::Unit => Value::NULL,
935        Primitive::Never => {
936            return Err(CompactError::Decode(DecodeError::Malformed(
937                "never is uninhabited",
938            )));
939        }
940        Primitive::DateTime | Primitive::Uuid | Primitive::QName => {
941            extended_from_string(r.read_str()?, p).map_err(CompactError::Decode)?
942        }
943    })
944}
945
946fn decode_kind(r: &mut Reader, kind: &SchemaKind, reg: &Registry, depth: usize) -> Result<Value> {
947    match kind {
948        SchemaKind::Primitive(p) => decode_primitive(r, *p),
949        SchemaKind::Struct { fields, .. } => {
950            let mut obj = VObject::new();
951            for field in fields {
952                let fv = decode_ref(r, &field.schema, reg, depth)?;
953                obj.insert(VString::new(&field.name), fv);
954            }
955            Ok(obj.into())
956        }
957        SchemaKind::Tuple { elements } => {
958            let mut arr = VArray::new();
959            for e in elements {
960                arr.push(decode_ref(r, e, reg, depth)?);
961            }
962            Ok(arr.into())
963        }
964        SchemaKind::List { element } => {
965            let n = r.read_len(min_wire_size_ref(reg, element))?;
966            let mut arr = VArray::new();
967            for _ in 0..n {
968                arr.push(decode_ref(r, element, reg, depth)?);
969            }
970            Ok(arr.into())
971        }
972        SchemaKind::Set { element } => {
973            let n = r.read_len(min_wire_size_ref(reg, element))?;
974            let mut arr = VArray::new();
975            let mut seen = std::collections::HashSet::new();
976            for _ in 0..n {
977                let v = decode_ref(r, element, reg, depth)?;
978                if !seen.insert(v.clone()) {
979                    return Err(CompactError::Decode(DecodeError::DuplicateElement));
980                }
981                arr.push(v);
982            }
983            Ok(arr.into())
984        }
985        SchemaKind::Array {
986            element,
987            dimensions,
988        } => {
989            let count = product(dimensions)?;
990            check_fixed_count(count, min_wire_size_ref(reg, element), r.remaining())?;
991            let mut arr = VArray::new();
992            for _ in 0..count {
993                arr.push(decode_ref(r, element, reg, depth)?);
994            }
995            Ok(arr.into())
996        }
997        SchemaKind::Map { key, value } => {
998            let n = r.read_len(1)?;
999            let mut obj = VObject::new();
1000            for _ in 0..n {
1001                let k = decode_ref(r, key, reg, depth)?;
1002                let v = decode_ref(r, value, reg, depth)?;
1003                let ks = k
1004                    .as_string()
1005                    .ok_or(CompactError::Unsupported("map with non-string keys"))?;
1006                if obj.insert(VString::new(ks.as_str()), v).is_some() {
1007                    return Err(CompactError::Decode(DecodeError::DuplicateKey));
1008                }
1009            }
1010            Ok(obj.into())
1011        }
1012        SchemaKind::Option { element } => match r.read_u8()? {
1013            0 => Ok(Value::NULL),
1014            1 => decode_ref(r, element, reg, depth),
1015            b => Err(CompactError::Decode(DecodeError::InvalidBool(b))),
1016        },
1017        SchemaKind::Enum { variants, .. } => {
1018            let index = r.read_u32()?;
1019            let variant = variants
1020                .iter()
1021                .find(|v| v.index == index)
1022                .ok_or(CompactError::BadVariantIndex(index))?;
1023            let payload = decode_payload(r, &variant.payload, reg, depth)?;
1024            let mut obj = VObject::new();
1025            obj.insert(VString::new(&variant.name), payload);
1026            Ok(obj.into())
1027        }
1028        SchemaKind::Dynamic => Ok(read_value(r)?),
1029        SchemaKind::Tensor { .. } => Err(CompactError::Unsupported("tensor")),
1030        SchemaKind::Channel { .. } => Err(CompactError::Unsupported("channel")),
1031        SchemaKind::External { .. } => Err(CompactError::Unsupported("external")),
1032    }
1033}
1034
1035fn decode_payload(
1036    r: &mut Reader,
1037    payload: &VariantPayload,
1038    reg: &Registry,
1039    depth: usize,
1040) -> Result<Value> {
1041    match payload {
1042        VariantPayload::Unit => Ok(Value::NULL),
1043        VariantPayload::Newtype(rf) => decode_ref(r, rf, reg, depth),
1044        VariantPayload::Tuple(refs) => {
1045            let mut arr = VArray::new();
1046            for rf in refs {
1047                arr.push(decode_ref(r, rf, reg, depth)?);
1048            }
1049            Ok(arr.into())
1050        }
1051        VariantPayload::Struct(fields) => {
1052            let mut obj = VObject::new();
1053            for field in fields {
1054                let fv = decode_ref(r, &field.schema, reg, depth)?;
1055                obj.insert(VString::new(&field.name), fv);
1056            }
1057            Ok(obj.into())
1058        }
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065    use phon_schema::{Field, Schema, SchemaKind, SchemaRef, Variant};
1066
1067    fn prim(p: Primitive) -> SchemaRef {
1068        SchemaRef::concrete(primitive_id(p))
1069    }
1070
1071    fn schema(id: u64, kind: SchemaKind) -> Schema {
1072        Schema {
1073            id: SchemaId(id),
1074            type_params: Vec::new(),
1075            kind,
1076        }
1077    }
1078
1079    // r[verify compact.schema-driven]
1080    fn rt(value: Value, root: SchemaId, reg: &Registry) {
1081        let bytes = to_bytes(&value, root, reg).expect("encode");
1082        let back = from_bytes(&bytes, root, reg).expect("decode");
1083        assert_eq!(value, back);
1084        assert_eq!(to_bytes(&back, root, reg).unwrap(), bytes);
1085    }
1086
1087    #[test]
1088    // r[verify validate.bundles]
1089    fn registry_try_new_validates_received_schema_bundles() {
1090        let point = schema(
1091            1,
1092            SchemaKind::Struct {
1093                name: "Point".to_string(),
1094                fields: vec![Field {
1095                    name: "x".to_string(),
1096                    schema: prim(Primitive::U32),
1097                    required: true,
1098                }],
1099            },
1100        );
1101        let resolved = resolve_ids(vec![point]);
1102        Registry::try_new(resolved).expect("resolved bundle should validate");
1103    }
1104
1105    #[test]
1106    // r[verify validate.bundles]
1107    fn registry_try_new_rejects_stale_schema_ids() {
1108        let unit = schema(
1109            1,
1110            SchemaKind::Struct {
1111                name: "UnitLike".to_string(),
1112                fields: Vec::new(),
1113            },
1114        );
1115        let mut resolved = resolve_ids(vec![unit]);
1116        resolved[0].id = SchemaId(resolved[0].id.0 ^ 1);
1117
1118        assert!(matches!(
1119            Registry::try_new(resolved),
1120            Err(CompactError::BundleSchemaIdMismatch { stated, recomputed })
1121                if stated != recomputed
1122        ));
1123    }
1124
1125    #[test]
1126    // r[verify validate.bundles]
1127    fn registry_try_new_rejects_incomplete_schema_closures() {
1128        let holder = schema(
1129            1,
1130            SchemaKind::Struct {
1131                name: "Holder".to_string(),
1132                fields: vec![Field {
1133                    name: "missing".to_string(),
1134                    schema: SchemaRef::concrete(SchemaId(0xFEED_FACE_CAFE_BEEF)),
1135                    required: true,
1136                }],
1137            },
1138        );
1139        let resolved = resolve_ids(vec![holder]);
1140
1141        assert!(matches!(
1142            Registry::try_new(resolved),
1143            Err(CompactError::UnknownSchema(SchemaId(0xFEED_FACE_CAFE_BEEF)))
1144        ));
1145    }
1146
1147    #[test]
1148    // r[verify validate.bundles]
1149    fn registry_try_new_rejects_unbounded_zero_wire_fixed_arrays() {
1150        let array = schema(
1151            1,
1152            SchemaKind::Array {
1153                element: prim(Primitive::Unit),
1154                dimensions: vec![phon_schema::bytes::ZST_COUNT_CAP as u64 + 1],
1155            },
1156        );
1157        let resolved = resolve_ids(vec![array]);
1158
1159        assert!(matches!(
1160            Registry::try_new(resolved),
1161            Err(CompactError::Decode(DecodeError::LengthTooLarge { count, .. }))
1162                if count == phon_schema::bytes::ZST_COUNT_CAP as u64 + 1
1163        ));
1164    }
1165
1166    #[test]
1167    // r[verify decode.chained]
1168    fn compact_values_can_be_decoded_back_to_back() {
1169        let reg = Registry::new([]);
1170        let first_root = primitive_id(Primitive::U8);
1171        let second_root = primitive_id(Primitive::Bool);
1172        let mut bytes = to_bytes(&Value::from(7u8), first_root, &reg).unwrap();
1173        bytes.extend(to_bytes(&Value::from(true), second_root, &reg).unwrap());
1174
1175        let mut reader = Reader::new(&bytes);
1176        let first = read_from(&mut reader, first_root, &reg).unwrap();
1177        assert_eq!(first, Value::from(7u8));
1178        assert_eq!(reader.position(), 1);
1179
1180        let second = read_from(&mut reader, second_root, &reg).unwrap();
1181        assert_eq!(second, Value::from(true));
1182        assert_eq!(reader.position(), bytes.len());
1183        assert_eq!(reader.remaining(), 0);
1184    }
1185
1186    #[test]
1187    // r[verify compact.alignment]
1188    fn struct_with_alignment() {
1189        // Point { x: u32, y: f64 } — y is 8-aligned, so padding follows x.
1190        let point = schema(
1191            1,
1192            SchemaKind::Struct {
1193                name: "Point".to_string(),
1194                fields: vec![
1195                    Field {
1196                        name: "x".to_string(),
1197                        schema: prim(Primitive::U32),
1198                        required: true,
1199                    },
1200                    Field {
1201                        name: "y".to_string(),
1202                        schema: prim(Primitive::F64),
1203                        required: true,
1204                    },
1205                ],
1206            },
1207        );
1208        let reg = Registry::new([point]);
1209        let mut obj = VObject::new();
1210        obj.insert(VString::new("x"), Value::from(7u32));
1211        obj.insert(VString::new("y"), Value::from(2.5f64));
1212        let value: Value = obj.into();
1213
1214        let bytes = to_bytes(&value, SchemaId(1), &reg).unwrap();
1215        // 4 bytes x, 4 bytes padding (to 8-align y), 8 bytes y.
1216        assert_eq!(bytes.len(), 16);
1217        assert_eq!(&bytes[4..8], &[0, 0, 0, 0]);
1218        rt(value, SchemaId(1), &reg);
1219    }
1220
1221    #[test]
1222    // r[verify compact.alignment]
1223    fn list_run_is_aligned() {
1224        let list = schema(
1225            1,
1226            SchemaKind::List {
1227                element: prim(Primitive::U64),
1228            },
1229        );
1230        let reg = Registry::new([list]);
1231        let mut arr = VArray::new();
1232        arr.push(Value::from(1u64));
1233        arr.push(Value::from(2u64));
1234        let value: Value = arr.into();
1235        let bytes = to_bytes(&value, SchemaId(1), &reg).unwrap();
1236        // u32 count, 4 bytes pad to 8, then two contiguous u64s.
1237        assert_eq!(bytes.len(), 4 + 4 + 16);
1238        rt(value, SchemaId(1), &reg);
1239    }
1240
1241    #[test]
1242    // r[verify validate.dimensions]
1243    fn enum_tuple_option_array() {
1244        // enum E { A, B(u32), C(u8, u8) }
1245        let e = schema(
1246            1,
1247            SchemaKind::Enum {
1248                name: "E".to_string(),
1249                variants: vec![
1250                    Variant {
1251                        name: "A".to_string(),
1252                        index: 0,
1253                        payload: VariantPayload::Unit,
1254                    },
1255                    Variant {
1256                        name: "B".to_string(),
1257                        index: 1,
1258                        payload: VariantPayload::Newtype(prim(Primitive::U32)),
1259                    },
1260                    Variant {
1261                        name: "C".to_string(),
1262                        index: 2,
1263                        payload: VariantPayload::Tuple(vec![
1264                            prim(Primitive::U8),
1265                            prim(Primitive::U8),
1266                        ]),
1267                    },
1268                ],
1269            },
1270        );
1271        let opt = schema(
1272            2,
1273            SchemaKind::Option {
1274                element: SchemaRef::concrete(SchemaId(1)),
1275            },
1276        );
1277        let arr = schema(
1278            3,
1279            SchemaKind::Array {
1280                element: prim(Primitive::U16),
1281                dimensions: vec![3],
1282            },
1283        );
1284        let reg = Registry::new([e.clone(), opt, arr]);
1285
1286        // E::A
1287        let mut a = VObject::new();
1288        a.insert(VString::new("A"), Value::NULL);
1289        rt(a.into(), SchemaId(1), &reg);
1290        // E::B(42)
1291        let mut b = VObject::new();
1292        b.insert(VString::new("B"), Value::from(42u32));
1293        rt(b.into(), SchemaId(1), &reg);
1294        // E::C(1, 2)
1295        let mut cpay = VArray::new();
1296        cpay.push(Value::from(1u8));
1297        cpay.push(Value::from(2u8));
1298        let mut c = VObject::new();
1299        c.insert(VString::new("C"), Value::from(cpay));
1300        rt(c.into(), SchemaId(1), &reg);
1301
1302        // Option<E> = Some(E::A), None
1303        let mut some_inner = VObject::new();
1304        some_inner.insert(VString::new("A"), Value::NULL);
1305        rt(some_inner.into(), SchemaId(2), &reg);
1306        rt(Value::NULL, SchemaId(2), &reg);
1307
1308        // Array<u16, 3>
1309        let mut av = VArray::new();
1310        av.push(Value::from(10u16));
1311        av.push(Value::from(20u16));
1312        av.push(Value::from(30u16));
1313        rt(av.into(), SchemaId(3), &reg);
1314    }
1315
1316    #[test]
1317    fn map_and_set_and_dynamic() {
1318        let map = schema(
1319            1,
1320            SchemaKind::Map {
1321                key: prim(Primitive::String),
1322                value: prim(Primitive::U32),
1323            },
1324        );
1325        let set = schema(
1326            2,
1327            SchemaKind::Set {
1328                element: prim(Primitive::U32),
1329            },
1330        );
1331        let dynamic = schema(3, SchemaKind::Dynamic);
1332        let reg = Registry::new([map, set, dynamic]);
1333
1334        let mut m = VObject::new();
1335        m.insert(VString::new("a"), Value::from(1u32));
1336        m.insert(VString::new("b"), Value::from(2u32));
1337        rt(m.into(), SchemaId(1), &reg);
1338
1339        let mut s = VArray::new();
1340        s.push(Value::from(1u32));
1341        s.push(Value::from(2u32));
1342        rt(s.into(), SchemaId(2), &reg);
1343
1344        // Dynamic carries an arbitrary value self-describing.
1345        rt(Value::from("hello dynamic"), SchemaId(3), &reg);
1346    }
1347
1348    #[test]
1349    // r[verify schema-identity.unknown-is-error]
1350    fn unknown_schema_and_type_mismatch() {
1351        let reg = Registry::new([]);
1352        // u32 primitive is intrinsic; a bogus composite id is unknown.
1353        assert!(matches!(
1354            to_bytes(&Value::from(1u32), SchemaId(999), &reg),
1355            Err(CompactError::UnknownSchema(_))
1356        ));
1357        // a string where a u32 is expected
1358        assert!(matches!(
1359            to_bytes(&Value::from("x"), primitive_id(Primitive::U32), &reg),
1360            Err(CompactError::TypeMismatch { .. })
1361        ));
1362    }
1363
1364    #[test]
1365    // r[verify type-system.generic-resolution]
1366    fn generics_resolve() {
1367        // Pair<A, B> = (A, B); Holder<T> = { pair: Pair<T, u32>, tag: string };
1368        // Root = { h: Holder<u8> } (concrete).
1369        let pair = Schema {
1370            id: SchemaId(10),
1371            type_params: vec!["A".to_string(), "B".to_string()],
1372            kind: SchemaKind::Tuple {
1373                elements: vec![SchemaRef::var("A"), SchemaRef::var("B")],
1374            },
1375        };
1376        let holder = Schema {
1377            id: SchemaId(11),
1378            type_params: vec!["T".to_string()],
1379            kind: SchemaKind::Struct {
1380                name: "Holder".to_string(),
1381                fields: vec![
1382                    Field {
1383                        name: "pair".to_string(),
1384                        schema: SchemaRef::generic(
1385                            SchemaId(10),
1386                            vec![SchemaRef::var("T"), prim(Primitive::U32)],
1387                        ),
1388                        required: true,
1389                    },
1390                    Field {
1391                        name: "tag".to_string(),
1392                        schema: prim(Primitive::String),
1393                        required: true,
1394                    },
1395                ],
1396            },
1397        };
1398        let root = schema(
1399            12,
1400            SchemaKind::Struct {
1401                name: "Root".to_string(),
1402                fields: vec![Field {
1403                    name: "h".to_string(),
1404                    schema: SchemaRef::generic(SchemaId(11), vec![prim(Primitive::U8)]),
1405                    required: true,
1406                }],
1407            },
1408        );
1409        let reg = Registry::new([pair, holder, root]);
1410
1411        let mut pair_val = VArray::new();
1412        pair_val.push(Value::from(5u8));
1413        pair_val.push(Value::from(70_000u32));
1414        let mut holder_val = VObject::new();
1415        holder_val.insert(VString::new("pair"), Value::from(pair_val));
1416        holder_val.insert(VString::new("tag"), VString::new("hi"));
1417        let mut root_val = VObject::new();
1418        root_val.insert(VString::new("h"), Value::from(holder_val));
1419
1420        rt(root_val.into(), SchemaId(12), &reg);
1421
1422        // wrong arity is rejected
1423        let bad = schema(
1424            13,
1425            SchemaKind::Struct {
1426                name: "Bad".to_string(),
1427                fields: vec![Field {
1428                    name: "h".to_string(),
1429                    schema: SchemaRef::concrete(SchemaId(11)), // Holder needs 1 arg
1430                    required: true,
1431                }],
1432            },
1433        );
1434        let reg2 = Registry::new([
1435            Schema {
1436                id: SchemaId(11),
1437                type_params: vec!["T".to_string()],
1438                kind: SchemaKind::Option {
1439                    element: SchemaRef::var("T"),
1440                },
1441            },
1442            bad,
1443        ]);
1444        let mut bv = VObject::new();
1445        bv.insert(VString::new("h"), Value::NULL);
1446        assert!(matches!(
1447            to_bytes(&bv.into(), SchemaId(13), &reg2),
1448            Err(CompactError::GenericArity { .. })
1449        ));
1450    }
1451
1452    /// Regression (found by `tests/compat_fuzz.rs`): a `list`/`set` of zero-sized
1453    /// elements — an empty struct, a `unit` — encodes to just its u32 count, so
1454    /// the buffer is empty afterward. The `r[validate.lengths]` count guard
1455    /// assumed every element costs at least one byte and rejected the decode of a
1456    /// value it had just encoded. The element's true minimum wire size now flows
1457    /// into the guard. This is a plain encode↔decode roundtrip, so it
1458    /// pins the fix at the codec level, not just the planner.
1459    #[test]
1460    fn zero_sized_collections_roundtrip() {
1461        let empty = schema(
1462            1,
1463            SchemaKind::Struct {
1464                name: "Z".into(),
1465                fields: vec![],
1466            },
1467        );
1468        let list = schema(
1469            2,
1470            SchemaKind::List {
1471                element: SchemaRef::concrete(SchemaId(1)),
1472            },
1473        );
1474        let set = schema(
1475            3,
1476            SchemaKind::List {
1477                element: prim(Primitive::Unit),
1478            },
1479        );
1480        let array = schema(
1481            4,
1482            SchemaKind::Array {
1483                element: prim(Primitive::Unit),
1484                dimensions: vec![4],
1485            },
1486        );
1487        let reg = Registry::new([empty, list, set, array]);
1488
1489        // list<empty-struct> with several elements.
1490        let mut a = VArray::new();
1491        for _ in 0..5 {
1492            a.push(Value::from(VObject::new()));
1493        }
1494        rt(Value::from(a), SchemaId(2), &reg);
1495
1496        // list<unit> with several elements.
1497        let mut u = VArray::new();
1498        for _ in 0..3 {
1499            u.push(Value::NULL);
1500        }
1501        rt(Value::from(u), SchemaId(3), &reg);
1502
1503        // array<unit, 4>.
1504        let mut fa = VArray::new();
1505        for _ in 0..4 {
1506            fa.push(Value::NULL);
1507        }
1508        rt(Value::from(fa), SchemaId(4), &reg);
1509    }
1510
1511    #[test]
1512    fn extended_primitives() {
1513        use facet_value::{VDateTime, VQName, VUuid};
1514        let reg = Registry::new([]);
1515        rt(
1516            VUuid::from_u128(0x0123_4567_89ab_cdef_fedc_ba98_7654_3210).into(),
1517            primitive_id(Primitive::Uuid),
1518            &reg,
1519        );
1520        rt(
1521            VQName::new(VString::new("http://ns"), VString::new("el")).into(),
1522            primitive_id(Primitive::QName),
1523            &reg,
1524        );
1525        rt(
1526            VDateTime::new_offset(2026, 5, 29, 7, 32, 0, 0, 330).into(),
1527            primitive_id(Primitive::DateTime),
1528            &reg,
1529        );
1530        rt(
1531            VDateTime::new_local_date(2026, 5, 29).into(),
1532            primitive_id(Primitive::DateTime),
1533            &reg,
1534        );
1535    }
1536}