Skip to main content

vox_types/
schema.rs

1//! Schema extraction and tracking for vox wire protocol.
2//!
3//! The canonical schema model lives in `vox-schema`. This module re-exports
4//! those shared types and adds vox-specific extraction plus per-connection
5//! send/receive tracking.
6
7pub use vox_schema::*;
8
9use std::sync::Arc;
10
11use facet::Facet;
12use facet_core::{DeclId, Def, ScalarType, Shape, StructKind, Type, UserType};
13use indexmap::IndexMap;
14use std::collections::{HashMap, HashSet};
15use std::sync::Mutex;
16
17use crate::{MethodId, RequestCall, RequestResponse, is_rx, is_tx};
18
19// ============================================================================
20// Schema extraction
21// ============================================================================
22
23/// Errors that can occur during schema extraction.
24#[derive(Debug)]
25pub enum SchemaExtractError {
26    /// Encountered a type that schema extraction doesn't know how to handle.
27    UnhandledType { type_desc: String },
28
29    /// A pointer type had no type_params to follow.
30    PointerWithoutTypeParams { shape_desc: String },
31
32    /// A temporary ID was not resolved during finalization.
33    UnresolvedTempId { temp_id: CycleSchemaIndex },
34
35    /// A DeclId was expected in the assigned map but wasn't found.
36    MissingAssignment { context: String },
37}
38
39impl std::fmt::Display for SchemaExtractError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::UnhandledType { type_desc } => {
43                write!(f, "schema extraction: unhandled type: {type_desc}")
44            }
45            Self::PointerWithoutTypeParams { shape_desc } => {
46                write!(
47                    f,
48                    "schema extraction: Pointer type without type_params: {shape_desc}"
49                )
50            }
51            Self::UnresolvedTempId { temp_id } => {
52                write!(
53                    f,
54                    "schema extraction: unresolved temp ID {temp_id:?} during finalization"
55                )
56            }
57            Self::MissingAssignment { context } => {
58                write!(f, "schema extraction: missing DeclId assignment: {context}")
59            }
60        }
61    }
62}
63
64/// A value for which a schema can be attached
65pub trait Schematic {
66    fn direction(&self) -> BindingDirection;
67    fn attach_schemas(&mut self, schemas: CborPayload);
68}
69
70impl<'payload> Schematic for RequestCall<'payload> {
71    fn direction(&self) -> BindingDirection {
72        BindingDirection::Args
73    }
74
75    fn attach_schemas(&mut self, schemas: CborPayload) {
76        self.schemas = schemas;
77    }
78}
79
80impl<'payload> Schematic for RequestResponse<'payload> {
81    fn direction(&self) -> BindingDirection {
82        BindingDirection::Response
83    }
84
85    fn attach_schemas(&mut self, schemas: CborPayload) {
86        self.schemas = schemas;
87    }
88}
89
90impl std::error::Error for SchemaExtractError {}
91
92// ============================================================================
93// SchemaSendTracker — outbound dedup, owned by SessionCore (no Arc, no Mutex)
94// ============================================================================
95
96/// Tracks which schemas have been sent on the current connection.
97///
98/// Plain struct — owned by `SessionCore` behind the same Mutex as the
99/// conduit tx. Reset on reconnection.
100// r[impl schema.tracking.sent]
101// r[impl schema.type-id.per-connection]
102pub struct SchemaSendTracker {
103    /// Per-method, per-direction: the CborPayload that was sent. Keyed by
104    /// (method_id, direction). If present, schemas were already sent.
105    sent_bindings: HashSet<(MethodId, BindingDirection)>,
106
107    /// SchemaHashes already sent on this connection.
108    sent_schemas: HashSet<SchemaHash>,
109
110    /// All extracted schemas, kept for the operation store to pull from.
111    registry: SchemaRegistry,
112}
113
114/// Structured schema plan computed before send ordering is known.
115#[derive(Debug, Clone)]
116pub struct PreparedSchemaPlan {
117    pub schemas: Vec<Schema>,
118    pub root: TypeRef,
119}
120
121impl PreparedSchemaPlan {
122    pub fn to_cbor(&self) -> CborPayload {
123        SchemaPayload {
124            schemas: self.schemas.clone(),
125            root: self.root.clone(),
126        }
127        .to_cbor()
128    }
129}
130
131impl SchemaSendTracker {
132    pub fn new() -> Self {
133        SchemaSendTracker {
134            registry: HashMap::new(),
135            sent_bindings: HashSet::new(),
136            sent_schemas: HashSet::new(),
137        }
138    }
139
140    /// Reset connection-scoped state — call on reconnection.
141    /// The registry is preserved (schemas don't change across connections).
142    pub fn reset(&mut self) {
143        self.sent_bindings.clear();
144        self.sent_schemas.clear();
145    }
146
147    /// Borrow the schema registry. Used by the operation store to pull
148    /// schemas it hasn't stored yet.
149    pub fn registry(&self) -> &SchemaRegistry {
150        &self.registry
151    }
152
153    /// Whether this method+direction binding has already been sent on the wire.
154    pub fn has_sent_binding(&self, method_id: MethodId, direction: BindingDirection) -> bool {
155        self.sent_bindings.contains(&(method_id, direction))
156    }
157
158    /// Compute the full schema payload for a shaped value without mutating
159    /// any per-connection send tracking.
160    pub fn plan_for_shape(shape: &'static Shape) -> Result<PreparedSchemaPlan, SchemaExtractError> {
161        let extracted = extract_schemas(shape)?;
162        Ok(PreparedSchemaPlan {
163            schemas: extracted.schemas.to_vec(),
164            root: extracted.root.clone(),
165        })
166    }
167
168    /// Compute the full schema payload for a canonical root type and schema
169    /// source without mutating any per-connection send tracking.
170    pub fn plan_from_source(root_type: &TypeRef, source: &dyn SchemaSource) -> PreparedSchemaPlan {
171        let mut all_schemas = Vec::new();
172        let mut visited = HashSet::new();
173        let mut queue = Vec::new();
174        root_type.collect_ids(&mut queue);
175
176        while let Some(id) = queue.pop() {
177            if !visited.insert(id) {
178                continue;
179            }
180            if let Some(schema) = source.get_schema(id) {
181                for child_id in schema_child_ids(&schema.kind) {
182                    queue.push(child_id);
183                }
184                all_schemas.push(schema);
185            }
186        }
187
188        PreparedSchemaPlan {
189            schemas: all_schemas,
190            root: root_type.clone(),
191        }
192    }
193
194    fn register_prepared_plan(&mut self, prepared: &PreparedSchemaPlan) {
195        for schema in &prepared.schemas {
196            self.registry
197                .entry(schema.id)
198                .or_insert_with(|| schema.clone());
199        }
200    }
201
202    fn unsent_schemas_for_prepared_plan(&self, prepared: &PreparedSchemaPlan) -> Vec<Schema> {
203        prepared
204            .schemas
205            .iter()
206            .filter(|schema| !self.sent_schemas.contains(&schema.id))
207            .cloned()
208            .collect()
209    }
210
211    /// Compute the schema payload that would be sent for a binding without
212    /// mutating connection-scoped send tracking.
213    pub fn preview_prepared_plan(
214        &mut self,
215        method_id: MethodId,
216        direction: BindingDirection,
217        prepared: &PreparedSchemaPlan,
218    ) -> CborPayload {
219        let key = (method_id, direction);
220        if self.sent_bindings.contains(&key) {
221            return CborPayload::default();
222        }
223
224        self.register_prepared_plan(prepared);
225
226        let schema_payload = SchemaPayload {
227            schemas: self.unsent_schemas_for_prepared_plan(prepared),
228            root: prepared.root.clone(),
229        };
230        schema_payload.to_cbor()
231    }
232
233    /// Mark a previously previewed schema payload as successfully sent.
234    pub fn mark_prepared_plan_sent(
235        &mut self,
236        method_id: MethodId,
237        direction: BindingDirection,
238        prepared: &PreparedSchemaPlan,
239    ) {
240        let key = (method_id, direction);
241        if self.sent_bindings.contains(&key) {
242            return;
243        }
244
245        self.register_prepared_plan(prepared);
246
247        for schema in &prepared.schemas {
248            self.sent_schemas.insert(schema.id);
249        }
250        self.sent_bindings.insert(key);
251    }
252
253    /// Commit a previously prepared schema payload against the live
254    /// per-connection tracking state, returning only the schemas that still
255    /// need to be sent on the wire for this binding.
256    pub fn commit_prepared_plan(
257        &mut self,
258        method_id: MethodId,
259        direction: BindingDirection,
260        prepared: PreparedSchemaPlan,
261    ) -> CborPayload {
262        let schema_payload = SchemaPayload {
263            schemas: self.unsent_schemas_for_prepared_plan(&prepared),
264            root: prepared.root.clone(),
265        };
266        dlog!(
267            "[schema] commit binding: method={:?} direction={:?} root={:?} schema_count={}",
268            method_id,
269            direction,
270            schema_payload.root,
271            schema_payload.schemas.len()
272        );
273        let cbor = schema_payload.to_cbor();
274        self.mark_prepared_plan_sent(method_id, direction, &prepared);
275        cbor
276    }
277
278    /// Prepare schemas for a method call/response, returning a CBOR payload
279    /// to inline in the request/response. Returns empty payload if schemas
280    /// were already sent for this shape.
281    ///
282    // r[impl schema.tracking.transitive]
283    // r[impl schema.exchange.idempotent]
284    // r[impl schema.principles.once-per-type]
285    // r[impl schema.principles.sender-driven]
286    // r[impl schema.principles.no-roundtrips]
287    pub fn attach_schemas_for_shape_if_needed(
288        &mut self,
289        method_id: MethodId,
290        shape: &'static Shape,
291        schematic: &mut impl Schematic,
292    ) -> Result<CborPayload, SchemaExtractError> {
293        let key = (method_id, schematic.direction());
294
295        // Fast path: already sent for this method+direction.
296        if self.sent_bindings.contains(&key) {
297            let empty = CborPayload::default();
298            schematic.attach_schemas(empty.clone());
299            return Ok(empty);
300        }
301
302        let prepared = Self::plan_for_shape(shape)?;
303        let cbor = self.commit_prepared_plan(method_id, schematic.direction(), prepared);
304        schematic.attach_schemas(cbor.clone());
305        Ok(cbor)
306    }
307
308    /// Prepare schemas for sending, sourcing them from a `SchemaSource`.
309    ///
310    /// Used for replay paths where we don't have a live value shape but do
311    /// have the bound root `TypeRef` and a schema source.
312    pub fn prepare_send(
313        &mut self,
314        method_id: MethodId,
315        direction: BindingDirection,
316        root_type: &TypeRef,
317        source: &dyn SchemaSource,
318    ) -> CborPayload {
319        let prepared = Self::plan_from_source(root_type, source);
320        self.commit_prepared_plan(method_id, direction, prepared)
321    }
322
323    pub fn commit_prepared_send(
324        &mut self,
325        method_id: MethodId,
326        direction: BindingDirection,
327        prepared: &CborPayload,
328    ) -> CborPayload {
329        let prepared_payload = SchemaPayload::from_cbor(&prepared.0)
330            .expect("prepared schema payloads must be valid CBOR");
331        self.commit_prepared_plan(
332            method_id,
333            direction,
334            PreparedSchemaPlan {
335                schemas: prepared_payload.schemas,
336                root: prepared_payload.root,
337            },
338        )
339    }
340
341    /// Compatibility shim: schema extraction is now independent from
342    /// connection-scoped send tracking.
343    pub fn extract_schemas(
344        &mut self,
345        shape: &'static Shape,
346    ) -> Result<Arc<ExtractedSchemas>, SchemaExtractError> {
347        self::extract_schemas(shape)
348    }
349}
350
351impl Default for SchemaSendTracker {
352    fn default() -> Self {
353        Self::new()
354    }
355}
356
357impl std::fmt::Debug for SchemaSendTracker {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        f.debug_struct("SchemaSendTracker").finish_non_exhaustive()
360    }
361}
362
363// ============================================================================
364// SchemaRecvTracker — inbound storage, shared via Arc
365// ============================================================================
366
367/// Tracks schemas received from the remote peer on the current connection.
368///
369/// Uses interior mutability (Mutex) so it can be shared via `Arc` between the
370/// session recv loop and in-flight handler tasks. Created fresh on each
371/// connection — NOT reused across reconnections.
372// r[impl schema.tracking.received]
373// r[impl schema.type-id.per-connection]
374pub struct SchemaRecvTracker {
375    /// Type schemas received from the remote peer.
376    received: Mutex<HashMap<SchemaHash, Schema>>,
377    /// Args bindings received: method_id → root TypeRef for args.
378    received_args_bindings: Mutex<HashMap<MethodId, TypeRef>>,
379    /// Response bindings received: method_id → root TypeRef for response.
380    received_response_bindings: Mutex<HashMap<MethodId, TypeRef>>,
381    /// Type-erased plan cache. Keyed by (method, direction, local Shape ptr).
382    /// Populated by higher-level crates (e.g. vox) that know the concrete plan type.
383    plan_cache: Mutex<HashMap<PlanCacheKey, Box<dyn std::any::Any + Send + Sync>>>,
384}
385
386/// Cache key for resolved translation plans.
387#[derive(Clone, Copy, PartialEq, Eq, Hash)]
388pub struct PlanCacheKey {
389    pub method_id: MethodId,
390    pub direction: BindingDirection,
391    pub local_shape: &'static Shape,
392}
393
394/// Error returned when recording received schemas detects a protocol violation.
395#[derive(Debug)]
396pub struct DuplicateSchemaError {
397    pub type_id: SchemaHash,
398}
399
400impl std::fmt::Display for DuplicateSchemaError {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        write!(
403            f,
404            "duplicate TypeSchemaId {:?} received on same connection — protocol error",
405            self.type_id
406        )
407    }
408}
409
410impl std::error::Error for DuplicateSchemaError {}
411
412impl SchemaRecvTracker {
413    pub fn new() -> Self {
414        SchemaRecvTracker {
415            received: Mutex::new(HashMap::new()),
416            received_args_bindings: Mutex::new(HashMap::new()),
417            received_response_bindings: Mutex::new(HashMap::new()),
418            plan_cache: Mutex::new(HashMap::new()),
419        }
420    }
421
422    /// Record a parsed schema message from the remote peer.
423    ///
424    /// Returns `Err` if a TypeSchemaId was already received — this is a
425    /// protocol error (the send tracker didn't reset on reconnection).
426    pub fn record_received(
427        &self,
428        method_id: MethodId,
429        direction: BindingDirection,
430        payload: SchemaPayload,
431    ) -> Result<(), DuplicateSchemaError> {
432        {
433            let mut received = self.received.lock().unwrap();
434            for schema in &payload.schemas {
435                dlog!("[schema] record_received: id={:?}", schema.id);
436            }
437            for schema in payload.schemas {
438                if let Some(existing) = received.get(&schema.id) {
439                    dlog!(
440                        "[schema] DUPLICATE: id={:?} existing={:?} new={:?}",
441                        schema.id,
442                        existing,
443                        schema
444                    );
445                    return Err(DuplicateSchemaError { type_id: schema.id });
446                }
447                received.insert(schema.id, schema);
448            }
449        }
450        let map = match direction {
451            BindingDirection::Args => &self.received_args_bindings,
452            BindingDirection::Response => &self.received_response_bindings,
453        };
454        dlog!(
455            "[schema] record binding: method={:?} direction={:?} root={:?}",
456            method_id,
457            direction,
458            payload.root
459        );
460        map.lock().unwrap().insert(method_id, payload.root);
461        Ok(())
462    }
463
464    /// Look up the remote's root TypeRef for a method's args.
465    pub fn get_remote_args_root(&self, method_id: MethodId) -> Option<TypeRef> {
466        self.received_args_bindings
467            .lock()
468            .unwrap()
469            .get(&method_id)
470            .cloned()
471    }
472
473    /// Look up the remote's root TypeRef for a method's response.
474    pub fn get_remote_response_root(&self, method_id: MethodId) -> Option<TypeRef> {
475        self.received_response_bindings
476            .lock()
477            .unwrap()
478            .get(&method_id)
479            .cloned()
480    }
481
482    /// Look up a received schema by type ID.
483    pub fn get_received(&self, type_id: &SchemaHash) -> Option<Schema> {
484        self.received.lock().unwrap().get(type_id).cloned()
485    }
486
487    /// Get a snapshot of the received schema registry for building translation plans.
488    pub fn received_registry(&self) -> SchemaRegistry {
489        self.received.lock().unwrap().clone()
490    }
491
492    /// Look up a cached plan by key, downcasting to `T`.
493    pub fn get_cached_plan<T: Send + Sync + 'static>(
494        &self,
495        key: &PlanCacheKey,
496    ) -> Option<std::sync::Arc<T>> {
497        let cache = self.plan_cache.lock().unwrap();
498        cache.get(key)?.downcast_ref::<std::sync::Arc<T>>().cloned()
499    }
500
501    /// Insert a plan into the cache.
502    pub fn insert_cached_plan<T: Send + Sync + 'static>(
503        &self,
504        key: PlanCacheKey,
505        plan: std::sync::Arc<T>,
506    ) {
507        self.plan_cache.lock().unwrap().insert(key, Box::new(plan));
508    }
509}
510
511impl Default for SchemaRecvTracker {
512    fn default() -> Self {
513        Self::new()
514    }
515}
516
517impl SchemaSource for SchemaRecvTracker {
518    fn get_schema(&self, id: SchemaHash) -> Option<Schema> {
519        self.get_received(&id)
520    }
521}
522
523impl std::fmt::Debug for SchemaRecvTracker {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        f.debug_struct("SchemaRecvTracker").finish_non_exhaustive()
526    }
527}
528
529/// Result of schema extraction: the schemas and the root TypeRef.
530#[derive(Clone)]
531pub struct ExtractedSchemas {
532    /// All schemas in dependency order (dependencies before dependents).
533    pub schemas: Vec<Schema>,
534
535    /// The root TypeRef — may be generic (e.g. `Concrete { id: result_id, args: [i64, MathError] }`).
536    pub root: TypeRef,
537}
538
539/// Extract schemas without a tracker (uses a temporary counter).
540/// Useful for tests and one-off schema extraction.
541pub fn extract_schemas(shape: &'static Shape) -> Result<Arc<ExtractedSchemas>, SchemaExtractError> {
542    use std::sync::OnceLock;
543
544    static CACHE: OnceLock<Mutex<HashMap<&'static Shape, Arc<ExtractedSchemas>>>> = OnceLock::new();
545    let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
546
547    if let Some(cached) = cache.lock().unwrap().get(shape) {
548        return Ok(Arc::clone(cached));
549    }
550
551    let result = Arc::new(extract_schemas_uncached(shape)?);
552    cache.lock().unwrap().insert(shape, Arc::clone(&result));
553    Ok(result)
554}
555
556fn extract_schemas_uncached(shape: &'static Shape) -> Result<ExtractedSchemas, SchemaExtractError> {
557    let mut ctx = ExtractCtx {
558        next_id: CycleSchemaIndex::first(),
559        schemas: IndexMap::new(),
560        assigned: HashMap::new(),
561        seen: HashSet::new(),
562    };
563    let root_mixed_ref = ctx.extract(shape)?;
564    let schemas: Vec<MixedSchema> = ctx.schemas.into_values().collect();
565    let (finalized, temp_to_final) = finalize_content_hashes(schemas)?;
566
567    let resolve = |mid: MixedId| -> SchemaHash {
568        match mid {
569            MixedId::Final(tid) => tid,
570            MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
571        }
572    };
573    let root_type_ref = root_mixed_ref.map(resolve);
574
575    Ok(ExtractedSchemas {
576        schemas: finalized,
577        root: root_type_ref,
578    })
579}
580
581/// Replace temporary incrementing IDs with blake3 content hashes.
582///
583/// Schemas must be in dependency order (dependencies before dependents).
584/// For non-recursive types, this is a simple bottom-up pass. For recursive
585/// types, the 4-step algorithm from r[schema.hash.recursive] is used.
586// r[impl schema.type-id.hash]
587// r[impl schema.hash.recursive]
588/// Resolve a MixedId to a TypeSchemaId for hashing purposes.
589fn resolve_mixed(id: MixedId, temp_to_final: &HashMap<CycleSchemaIndex, SchemaHash>) -> SchemaHash {
590    match id {
591        MixedId::Final(tid) => tid,
592        MixedId::Temp(t) => temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0)),
593    }
594}
595
596#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
597enum ExtractKey {
598    Decl(DeclId),
599    AnonymousTupleArity(usize),
600}
601
602/// Convert a Vec<MixedSchema> (from extraction) into Vec<Schema> with
603/// content-hashed TypeSchemaIds.
604///
605/// Schemas must be in dependency order (dependencies before dependents).
606/// For non-recursive types, this is a simple bottom-up pass. For recursive
607/// types, the 4-step algorithm from r[schema.hash.recursive] is used.
608///
609/// Returns the finalized schemas and a mapping from temp IDs to final IDs.
610// r[impl schema.type-id.hash]
611// r[impl schema.hash.recursive]
612fn finalize_content_hashes(
613    schemas: Vec<MixedSchema>,
614) -> Result<(Vec<Schema>, HashMap<CycleSchemaIndex, SchemaHash>), SchemaExtractError> {
615    // Only Temp entries need hashing. Build index of temp IDs.
616    let temp_to_idx: HashMap<CycleSchemaIndex, usize> = schemas
617        .iter()
618        .enumerate()
619        .filter_map(|(i, s)| match s.id {
620            MixedId::Temp(t) => Some((t, i)),
621            MixedId::Final(_) => None,
622        })
623        .collect();
624
625    fn collect_refs(kind: &MixedSchemaKind) -> Vec<MixedId> {
626        let mut refs = Vec::new();
627        kind.for_each_type_ref(&mut |tr: &TypeRef<MixedId>| tr.collect_ids(&mut refs));
628        refs
629    }
630
631    // Detect recursive groups among temp schemas.
632    let n = schemas.len();
633    let mut in_recursive_group: Vec<bool> = vec![false; n];
634
635    for (i, schema) in schemas.iter().enumerate() {
636        if matches!(schema.id, MixedId::Final(_)) {
637            continue; // Already finalized, skip.
638        }
639        for r in collect_refs(&schema.kind) {
640            if let MixedId::Temp(t) = r
641                && let Some(&ref_idx) = temp_to_idx.get(&t)
642                && ref_idx >= i
643            {
644                in_recursive_group[i] = true;
645                in_recursive_group[ref_idx] = true;
646            }
647        }
648    }
649
650    // Map from temp ID -> final content hash.
651    let mut temp_to_final: HashMap<CycleSchemaIndex, SchemaHash> = HashMap::new();
652
653    // Phase 1: Hash non-recursive temp types bottom-up.
654    for (i, schema) in schemas.iter().enumerate() {
655        if in_recursive_group[i] {
656            continue;
657        }
658        if let MixedId::Temp(temp) = schema.id {
659            let final_id = compute_content_hash(&schema.kind, &schema.type_params, &|mid| {
660                resolve_mixed(mid, &temp_to_final)
661            });
662            temp_to_final.insert(temp, final_id);
663        }
664    }
665
666    // Phase 2: Hash recursive groups using the 4-step algorithm.
667    let mut i = 0;
668    while i < n {
669        if !in_recursive_group[i] {
670            i += 1;
671            continue;
672        }
673
674        let group_start = i;
675        while i < n && in_recursive_group[i] {
676            i += 1;
677        }
678        let group_end = i;
679
680        // Collect the temp IDs in this group.
681        let group_temp_ids: HashSet<CycleSchemaIndex> = schemas[group_start..group_end]
682            .iter()
683            .filter_map(|s| match s.id {
684                MixedId::Temp(t) => Some(t),
685                _ => None,
686            })
687            .collect();
688
689        // Step 1: Preliminary hashes — intra-group refs become sentinel (0).
690        let mut prelim_hashes: Vec<SchemaHash> = Vec::new();
691        for schema in &schemas[group_start..group_end] {
692            let prelim =
693                compute_content_hash(&schema.kind, &schema.type_params, &|mid| match mid {
694                    MixedId::Final(tid) => tid,
695                    MixedId::Temp(t) => {
696                        if group_temp_ids.contains(&t) {
697                            SchemaHash(0) // sentinel
698                        } else {
699                            temp_to_final.get(&t).copied().unwrap_or(SchemaHash(0))
700                        }
701                    }
702                });
703            prelim_hashes.push(prelim);
704        }
705
706        // Step 3: Canonical ordering.
707        let mut order: Vec<usize> = (0..prelim_hashes.len()).collect();
708        order.sort_by_key(|&i| prelim_hashes[i].0);
709
710        // Step 4: Final hashes.
711        let mut group_hasher = blake3::Hasher::new();
712        for &idx in &order {
713            group_hasher.update(&prelim_hashes[idx].0.to_le_bytes());
714        }
715        let gh = group_hasher.finalize();
716        let group_hash = u64::from_le_bytes(gh.as_bytes()[0..8].try_into().unwrap());
717
718        for (position, &idx) in order.iter().enumerate() {
719            let mut fh = blake3::Hasher::new();
720            fh.update(&group_hash.to_le_bytes());
721            fh.update(&(position as u64).to_le_bytes());
722            let fo = fh.finalize();
723            let final_hash =
724                SchemaHash(u64::from_le_bytes(fo.as_bytes()[0..8].try_into().unwrap()));
725
726            if let MixedId::Temp(t) = schemas[group_start + idx].id {
727                temp_to_final.insert(t, final_hash);
728            }
729        }
730    }
731
732    // Phase 3: Convert MixedSchema -> Schema by resolving all MixedIds.
733    let resolve = |mid: MixedId| -> Result<SchemaHash, SchemaExtractError> {
734        match mid {
735            MixedId::Final(tid) => Ok(tid),
736            MixedId::Temp(t) => temp_to_final
737                .get(&t)
738                .copied()
739                .ok_or(SchemaExtractError::UnresolvedTempId { temp_id: t }),
740        }
741    };
742
743    let mut resolve_type_ref =
744        |type_ref: TypeRef<MixedId>| -> Result<TypeRef<SchemaHash>, SchemaExtractError> {
745            type_ref.try_map(&resolve)
746        };
747
748    let mut seen_ids = HashSet::new();
749    let finalized: Vec<Schema> = schemas
750        .into_iter()
751        .map(|s| {
752            let type_id = resolve(s.id)?;
753            Ok(Schema {
754                id: type_id,
755                type_params: s.type_params,
756                kind: s.kind.try_map_type_refs(&mut resolve_type_ref)?,
757            })
758        })
759        .collect::<Result<Vec<_>, _>>()?
760        .into_iter()
761        .filter(|s| seen_ids.insert(s.id))
762        .collect();
763
764    Ok((finalized, temp_to_final))
765}
766
767struct ExtractCtx {
768    /// Counter for assigning temp IDs
769    next_id: CycleSchemaIndex,
770    /// Schemas being built in this extraction pass, keyed by extraction identity.
771    /// Insertion order is dependency order.
772    schemas: IndexMap<ExtractKey, MixedSchema>,
773    /// ExtractKey → MixedId for types we've started extracting (may not be
774    /// fully built yet — needed for cycle references).
775    assigned: HashMap<ExtractKey, MixedId>,
776    /// Shapes we've started walking. If we encounter a shape already in
777    /// this set, we're in a cycle.
778    seen: HashSet<&'static Shape>,
779}
780
781impl ExtractCtx {
782    /// Get or assign a MixedId for an extraction key.
783    fn id_for_key(&mut self, key: ExtractKey) -> MixedId {
784        if let Some(&id) = self.assigned.get(&key) {
785            return id;
786        }
787        let id = MixedId::Temp(self.next_id.next_index());
788        self.assigned.insert(key, id);
789        id
790    }
791
792    /// Emit a schema for an extraction key (if not already emitted in this pass).
793    fn emit_schema(&mut self, key: ExtractKey, schema: MixedSchema) {
794        self.schemas.entry(key).or_insert(schema);
795    }
796
797    fn key_for_shape(&self, shape: &'static Shape) -> ExtractKey {
798        match anonymous_tuple_arity(shape) {
799            Some(arity) => ExtractKey::AnonymousTupleArity(arity),
800            None => ExtractKey::Decl(shape.decl_id),
801        }
802    }
803
804    /// Build a TypeRef for a field/element shape, substituting Var references
805    /// for shapes that match a type parameter.
806    fn type_ref_for_shape(
807        &mut self,
808        shape: &'static Shape,
809        param_map: &[(&'static Shape, TypeParamName)],
810    ) -> Result<TypeRef<MixedId>, SchemaExtractError> {
811        if let Some((_, name)) = param_map
812            .iter()
813            .find(|(param_shape, _)| shape.is_shape(param_shape))
814        {
815            // This shape is a type parameter — emit Var reference.
816            // But we still need to extract the concrete type's schema.
817            self.extract(shape)?;
818            Ok(TypeRef::Var { name: name.clone() })
819        } else {
820            self.extract(shape)
821        }
822    }
823
824    /// Extract a schema for the given shape, returning a TypeRef to it.
825    /// Recursively extracts dependencies first.
826    fn extract(&mut self, shape: &'static Shape) -> Result<TypeRef<MixedId>, SchemaExtractError> {
827        // Channel types: emit a Channel schema with direction and element type.
828        if is_tx(shape) || is_rx(shape) {
829            let direction = if is_tx(shape) {
830                ChannelDirection::Tx
831            } else {
832                ChannelDirection::Rx
833            };
834            if let Some(inner) = shape.type_params.first() {
835                let elem_ref = self.extract(inner.shape)?;
836                let key = self.key_for_shape(shape);
837                let id = self.id_for_key(key);
838                // For channels, the element in the schema body uses Var("T")
839                // since channels are generic over their element type.
840                let type_params = vec![TypeParamName("T".to_string())];
841                self.emit_schema(
842                    key,
843                    MixedSchema {
844                        id,
845                        type_params,
846                        kind: SchemaKind::Channel {
847                            direction,
848                            element: TypeRef::Var {
849                                name: TypeParamName("T".to_string()),
850                            },
851                        },
852                    },
853                );
854                self.seen.insert(shape);
855                return Ok(TypeRef::Concrete {
856                    type_id: id,
857                    args: vec![elem_ref],
858                });
859            }
860        }
861
862        // Transparent wrappers: follow inner.
863        if shape.is_transparent()
864            && let Some(inner) = shape.inner
865        {
866            return self.extract(inner);
867        }
868
869        // Pointer types (Box, Arc, etc.): follow through to pointee.
870        // Must be before id_for_decl to avoid orphaned temp IDs.
871        if let Def::Pointer(ptr_def) = shape.def
872            && let Some(pointee) = ptr_def.pointee
873        {
874            return self.extract(pointee);
875        }
876
877        let key = self.key_for_shape(shape);
878        let id = self.id_for_key(key);
879
880        // r[impl schema.format.recursive]
881        // Cycle detection: if we've already started walking this shape,
882        // return the assigned id without re-entering.
883        if !self.seen.insert(shape) {
884            // Already seen — either fully processed or a cycle.
885            // Extract type args if generic (they may contain new types).
886            let args = self.extract_instantiation_args(shape)?;
887            return Ok(if args.is_empty() {
888                TypeRef::concrete(id)
889            } else {
890                TypeRef::generic(id, args)
891            });
892        }
893
894        // If we've already emitted a schema for this extraction key (in this pass),
895        // we still need to extract type args for this particular instantiation.
896        let already_emitted = self.schemas.contains_key(&key);
897        if already_emitted {
898            let args = self.extract_instantiation_args(shape)?;
899            return Ok(if args.is_empty() {
900                TypeRef::concrete(id)
901            } else {
902                TypeRef::generic(id, args)
903            });
904        }
905
906        // Build a map from shape pointer → type param name for this type.
907        // Used to emit Var references in the schema body.
908        let param_map: Vec<(&'static Shape, TypeParamName)> = shape
909            .type_params
910            .iter()
911            .map(|tp| (tp.shape, TypeParamName(tp.name.to_string())))
912            .collect();
913        let type_param_names: Vec<TypeParamName> = shape
914            .type_params
915            .iter()
916            .map(|tp| TypeParamName(tp.name.to_string()))
917            .collect();
918
919        // r[impl schema.format.primitive]
920        // Scalars
921        if let Some(scalar) = shape.scalar_type() {
922            self.emit_schema(
923                key,
924                MixedSchema {
925                    id,
926                    type_params: vec![],
927                    kind: SchemaKind::Primitive {
928                        primitive_type: scalar_to_primitive(scalar),
929                    },
930                },
931            );
932            return Ok(TypeRef::concrete(id));
933        }
934
935        // r[impl schema.format.container]
936        // Containers
937        match shape.def {
938            Def::List(list_def) => {
939                if let Some(ScalarType::U8) = list_def.t().scalar_type() {
940                    self.emit_schema(
941                        key,
942                        MixedSchema {
943                            id,
944                            type_params: vec![],
945                            kind: SchemaKind::Primitive {
946                                primitive_type: PrimitiveType::Bytes,
947                            },
948                        },
949                    );
950                    return Ok(TypeRef::concrete(id));
951                }
952                let elem_ref = self.type_ref_for_shape(list_def.t(), &param_map)?;
953                let args = self.extract_type_args(shape)?;
954                self.emit_schema(
955                    key,
956                    MixedSchema {
957                        id,
958                        type_params: type_param_names,
959                        kind: SchemaKind::List { element: elem_ref },
960                    },
961                );
962                return Ok(if args.is_empty() {
963                    TypeRef::concrete(id)
964                } else {
965                    TypeRef::generic(id, args)
966                });
967            }
968            Def::Array(array_def) => {
969                let elem_ref = self.type_ref_for_shape(array_def.t(), &param_map)?;
970                let args = self.extract_type_args(shape)?;
971                self.emit_schema(
972                    key,
973                    MixedSchema {
974                        id,
975                        type_params: type_param_names,
976                        kind: SchemaKind::Array {
977                            element: elem_ref,
978                            length: array_def.n as u64,
979                        },
980                    },
981                );
982                return Ok(if args.is_empty() {
983                    TypeRef::concrete(id)
984                } else {
985                    TypeRef::generic(id, args)
986                });
987            }
988            Def::Slice(slice_def) => {
989                if let Some(ScalarType::U8) = slice_def.t().scalar_type() {
990                    self.emit_schema(
991                        key,
992                        MixedSchema {
993                            id,
994                            type_params: vec![],
995                            kind: SchemaKind::Primitive {
996                                primitive_type: PrimitiveType::Bytes,
997                            },
998                        },
999                    );
1000                    return Ok(TypeRef::concrete(id));
1001                }
1002                let elem_ref = self.type_ref_for_shape(slice_def.t(), &param_map)?;
1003                let args = self.extract_type_args(shape)?;
1004                self.emit_schema(
1005                    key,
1006                    MixedSchema {
1007                        id,
1008                        type_params: type_param_names,
1009                        kind: SchemaKind::List { element: elem_ref },
1010                    },
1011                );
1012                return Ok(if args.is_empty() {
1013                    TypeRef::concrete(id)
1014                } else {
1015                    TypeRef::generic(id, args)
1016                });
1017            }
1018            Def::Map(map_def) => {
1019                let key_ref = self.type_ref_for_shape(map_def.k(), &param_map)?;
1020                let val_ref = self.type_ref_for_shape(map_def.v(), &param_map)?;
1021                let args = self.extract_type_args(shape)?;
1022                self.emit_schema(
1023                    key,
1024                    MixedSchema {
1025                        id,
1026                        type_params: type_param_names,
1027                        kind: SchemaKind::Map {
1028                            key: key_ref,
1029                            value: val_ref,
1030                        },
1031                    },
1032                );
1033                return Ok(if args.is_empty() {
1034                    TypeRef::concrete(id)
1035                } else {
1036                    TypeRef::generic(id, args)
1037                });
1038            }
1039            Def::Set(set_def) => {
1040                let elem_ref = self.type_ref_for_shape(set_def.t(), &param_map)?;
1041                let args = self.extract_type_args(shape)?;
1042                self.emit_schema(
1043                    key,
1044                    MixedSchema {
1045                        id,
1046                        type_params: type_param_names,
1047                        kind: SchemaKind::List { element: elem_ref },
1048                    },
1049                );
1050                return Ok(if args.is_empty() {
1051                    TypeRef::concrete(id)
1052                } else {
1053                    TypeRef::generic(id, args)
1054                });
1055            }
1056            Def::Option(opt_def) => {
1057                let elem_ref = self.type_ref_for_shape(opt_def.t(), &param_map)?;
1058                let args = self.extract_type_args(shape)?;
1059                self.emit_schema(
1060                    key,
1061                    MixedSchema {
1062                        id,
1063                        type_params: type_param_names,
1064                        kind: SchemaKind::Option { element: elem_ref },
1065                    },
1066                );
1067                return Ok(if args.is_empty() {
1068                    TypeRef::concrete(id)
1069                } else {
1070                    TypeRef::generic(id, args)
1071                });
1072            }
1073            Def::Result(result_def) => {
1074                let ok_ref = self.type_ref_for_shape(result_def.t(), &param_map)?;
1075                let err_ref = self.type_ref_for_shape(result_def.e(), &param_map)?;
1076                let args = self.extract_type_args(shape)?;
1077                self.emit_schema(
1078                    key,
1079                    MixedSchema {
1080                        id,
1081                        type_params: type_param_names,
1082                        kind: SchemaKind::Enum {
1083                            name: shape.type_identifier.to_string(),
1084                            variants: vec![
1085                                VariantSchema {
1086                                    name: "Ok".to_string(),
1087                                    index: 0,
1088                                    payload: VariantPayload::Newtype { type_ref: ok_ref },
1089                                },
1090                                VariantSchema {
1091                                    name: "Err".to_string(),
1092                                    index: 1,
1093                                    payload: VariantPayload::Newtype { type_ref: err_ref },
1094                                },
1095                            ],
1096                        },
1097                    },
1098                );
1099                return Ok(if args.is_empty() {
1100                    TypeRef::concrete(id)
1101                } else {
1102                    TypeRef::generic(id, args)
1103                });
1104            }
1105            _ => {}
1106        }
1107
1108        // User-defined types.
1109        let kind = match shape.ty {
1110            // r[impl schema.format.struct]
1111            // r[impl schema.format.tuple]
1112            Type::User(UserType::Struct(struct_type)) => match struct_type.kind {
1113                StructKind::Unit => {
1114                    let primitive_type = if is_infallible_shape(shape) {
1115                        PrimitiveType::Never
1116                    } else {
1117                        PrimitiveType::Unit
1118                    };
1119                    SchemaKind::Primitive { primitive_type }
1120                }
1121                StructKind::TupleStruct | StructKind::Tuple => {
1122                    if let Some(arity) = anonymous_tuple_arity(shape) {
1123                        let args = self.extract_instantiation_args(shape)?;
1124                        let type_params = tuple_type_params(arity);
1125                        let elements = type_params
1126                            .iter()
1127                            .cloned()
1128                            .map(|name| TypeRef::Var { name })
1129                            .collect();
1130                        self.emit_schema(
1131                            key,
1132                            MixedSchema {
1133                                id,
1134                                type_params,
1135                                kind: SchemaKind::Tuple { elements },
1136                            },
1137                        );
1138                        return Ok(TypeRef::generic(id, args));
1139                    }
1140                    let mut elements = Vec::with_capacity(struct_type.fields.len());
1141                    for f in struct_type.fields {
1142                        elements.push(self.type_ref_for_shape(f.shape(), &param_map)?);
1143                    }
1144                    SchemaKind::Tuple { elements }
1145                }
1146                StructKind::Struct => {
1147                    let mut fields = Vec::with_capacity(struct_type.fields.len());
1148                    for f in struct_type.fields {
1149                        fields.push(FieldSchema {
1150                            name: f.name.to_string(),
1151                            type_ref: self.type_ref_for_shape(f.shape(), &param_map)?,
1152                            required: f.default.is_none(),
1153                        });
1154                    }
1155                    SchemaKind::Struct {
1156                        name: shape.type_identifier.to_string(),
1157                        fields,
1158                    }
1159                }
1160            },
1161            // r[impl schema.format.enum]
1162            Type::User(UserType::Enum(enum_type)) => {
1163                let mut variants = Vec::with_capacity(enum_type.variants.len());
1164                for (i, v) in enum_type.variants.iter().enumerate() {
1165                    let payload = match v.data.kind {
1166                        StructKind::Unit => VariantPayload::Unit,
1167                        StructKind::TupleStruct | StructKind::Tuple => {
1168                            if v.data.fields.len() == 1 {
1169                                VariantPayload::Newtype {
1170                                    type_ref: self
1171                                        .type_ref_for_shape(v.data.fields[0].shape(), &param_map)?,
1172                                }
1173                            } else {
1174                                let mut types = Vec::with_capacity(v.data.fields.len());
1175                                for f in v.data.fields {
1176                                    types.push(self.type_ref_for_shape(f.shape(), &param_map)?);
1177                                }
1178                                VariantPayload::Tuple { types }
1179                            }
1180                        }
1181                        StructKind::Struct => {
1182                            let mut fields = Vec::with_capacity(v.data.fields.len());
1183                            for f in v.data.fields {
1184                                fields.push(FieldSchema {
1185                                    name: f.name.to_string(),
1186                                    type_ref: self.type_ref_for_shape(f.shape(), &param_map)?,
1187                                    required: true,
1188                                });
1189                            }
1190                            VariantPayload::Struct { fields }
1191                        }
1192                    };
1193                    variants.push(VariantSchema {
1194                        name: v.name.to_string(),
1195                        index: i as u32,
1196                        payload,
1197                    });
1198                }
1199                SchemaKind::Enum {
1200                    name: shape.type_identifier.to_string(),
1201                    variants,
1202                }
1203            }
1204            Type::User(UserType::Opaque) => SchemaKind::Primitive {
1205                primitive_type: PrimitiveType::Payload,
1206            },
1207            other => {
1208                return Err(SchemaExtractError::UnhandledType {
1209                    type_desc: format!("{other:?} for shape {shape} (def={:?})", shape.def),
1210                });
1211            }
1212        };
1213
1214        let args = self.extract_type_args(shape)?;
1215        self.emit_schema(
1216            key,
1217            MixedSchema {
1218                id,
1219                type_params: type_param_names,
1220                kind,
1221            },
1222        );
1223
1224        Ok(if args.is_empty() {
1225            TypeRef::concrete(id)
1226        } else {
1227            TypeRef::generic(id, args)
1228        })
1229    }
1230
1231    /// Extract the concrete type arguments for a generic shape.
1232    /// For `Vec<u32>`, this extracts u32 and returns `[TypeRef::concrete(u32_id)]`.
1233    /// For non-generic types, returns an empty vec.
1234    fn extract_type_args(
1235        &mut self,
1236        shape: &'static Shape,
1237    ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1238        if shape.type_params.is_empty() {
1239            return Ok(vec![]);
1240        }
1241        let mut args = Vec::with_capacity(shape.type_params.len());
1242        for tp in shape.type_params {
1243            args.push(self.extract(tp.shape)?);
1244        }
1245        Ok(args)
1246    }
1247
1248    /// Extract the concrete instantiation arguments for a shape.
1249    ///
1250    /// Most generic shapes get their args from facet `type_params`.
1251    /// Anonymous tuples are synthesized as generic families per arity,
1252    /// so their "type args" come from their element shapes.
1253    fn extract_instantiation_args(
1254        &mut self,
1255        shape: &'static Shape,
1256    ) -> Result<Vec<TypeRef<MixedId>>, SchemaExtractError> {
1257        if anonymous_tuple_arity(shape).is_some()
1258            && let Type::User(UserType::Struct(struct_type)) = shape.ty
1259        {
1260            let mut args = Vec::with_capacity(struct_type.fields.len());
1261            for field in struct_type.fields {
1262                args.push(self.extract(field.shape())?);
1263            }
1264            return Ok(args);
1265        }
1266        self.extract_type_args(shape)
1267    }
1268}
1269
1270fn anonymous_tuple_arity(shape: &'static Shape) -> Option<usize> {
1271    match shape.ty {
1272        Type::User(UserType::Struct(struct_type))
1273            if struct_type.kind == StructKind::Tuple && shape.type_identifier.starts_with('(') =>
1274        {
1275            Some(struct_type.fields.len())
1276        }
1277        _ => None,
1278    }
1279}
1280
1281fn tuple_type_params(arity: usize) -> Vec<TypeParamName> {
1282    (0..arity)
1283        .map(|index| TypeParamName(format!("T{index}")))
1284        .collect()
1285}
1286
1287fn is_infallible_shape(shape: &'static Shape) -> bool {
1288    shape.is_shape(<std::convert::Infallible as Facet<'static>>::SHAPE)
1289}
1290
1291fn scalar_to_primitive(scalar: ScalarType) -> PrimitiveType {
1292    match scalar {
1293        ScalarType::Unit => PrimitiveType::Unit,
1294        ScalarType::Bool => PrimitiveType::Bool,
1295        ScalarType::Char => PrimitiveType::Char,
1296        ScalarType::Str | ScalarType::String | ScalarType::CowStr => PrimitiveType::String,
1297        ScalarType::F32 => PrimitiveType::F32,
1298        ScalarType::F64 => PrimitiveType::F64,
1299        ScalarType::U8 => PrimitiveType::U8,
1300        ScalarType::U16 => PrimitiveType::U16,
1301        ScalarType::U32 => PrimitiveType::U32,
1302        ScalarType::U64 => PrimitiveType::U64,
1303        ScalarType::U128 => PrimitiveType::U128,
1304        ScalarType::USize => PrimitiveType::U64,
1305        ScalarType::I8 => PrimitiveType::I8,
1306        ScalarType::I16 => PrimitiveType::I16,
1307        ScalarType::I32 => PrimitiveType::I32,
1308        ScalarType::I64 => PrimitiveType::I64,
1309        ScalarType::I128 => PrimitiveType::I128,
1310        ScalarType::ISize => PrimitiveType::I64,
1311        ScalarType::ConstTypeId => PrimitiveType::U64,
1312        _ => PrimitiveType::Unit,
1313    }
1314}
1315
1316#[cfg(test)]
1317mod tests {
1318    use super::*;
1319    use facet::Facet;
1320
1321    struct TestSchematic {
1322        direction: BindingDirection,
1323        shape: &'static Shape,
1324        attached: CborPayload,
1325    }
1326
1327    impl TestSchematic {
1328        fn new(direction: BindingDirection, shape: &'static Shape) -> Self {
1329            Self {
1330                direction,
1331                shape,
1332                attached: CborPayload::default(),
1333            }
1334        }
1335    }
1336
1337    impl Schematic for TestSchematic {
1338        fn direction(&self) -> BindingDirection {
1339            self.direction
1340        }
1341
1342        fn attach_schemas(&mut self, schemas: CborPayload) {
1343            self.attached = schemas;
1344        }
1345    }
1346
1347    // r[verify schema.type-id]
1348    #[test]
1349    fn type_ids_are_u64_content_hashes() {
1350        let id = SchemaHash(42);
1351        assert_eq!(id.0, 42);
1352        assert_eq!(id, SchemaHash(42));
1353        assert_ne!(id, SchemaHash(43));
1354    }
1355
1356    // r[verify schema.principles.cbor]
1357    // r[verify schema.format.self-contained]
1358    #[test]
1359    fn cbor_round_trip() {
1360        let schema = Schema {
1361            id: SchemaHash(1),
1362            type_params: vec![],
1363            kind: SchemaKind::Primitive {
1364                primitive_type: PrimitiveType::U32,
1365            },
1366        };
1367        let bytes = SchemaPayload {
1368            schemas: vec![schema.clone()],
1369            root: TypeRef::concrete(schema.id),
1370        }
1371        .to_cbor();
1372        let payload = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
1373        assert_eq!(payload.schemas.len(), 1);
1374        assert_eq!(payload.schemas[0].id, schema.id);
1375        assert_eq!(payload.root, TypeRef::concrete(schema.id));
1376    }
1377
1378    // r[verify schema.format.primitive]
1379    #[test]
1380    fn primitive_u32() {
1381        let schemas = extract_schemas(<u32 as Facet>::SHAPE)
1382            .unwrap()
1383            .schemas
1384            .clone();
1385        assert_eq!(schemas.len(), 1);
1386        assert!(matches!(
1387            schemas[0].kind,
1388            SchemaKind::Primitive {
1389                primitive_type: PrimitiveType::U32
1390            }
1391        ));
1392    }
1393
1394    #[test]
1395    fn primitive_string() {
1396        let schemas = extract_schemas(<String as Facet>::SHAPE)
1397            .unwrap()
1398            .schemas
1399            .clone();
1400        assert_eq!(schemas.len(), 1);
1401        assert!(matches!(
1402            schemas[0].kind,
1403            SchemaKind::Primitive {
1404                primitive_type: PrimitiveType::String
1405            }
1406        ));
1407    }
1408
1409    #[test]
1410    fn primitive_bool() {
1411        let schemas = extract_schemas(<bool as Facet>::SHAPE)
1412            .unwrap()
1413            .schemas
1414            .clone();
1415        assert_eq!(schemas.len(), 1);
1416        assert!(matches!(
1417            schemas[0].kind,
1418            SchemaKind::Primitive {
1419                primitive_type: PrimitiveType::Bool
1420            }
1421        ));
1422    }
1423
1424    // r[verify schema.format.struct]
1425    #[test]
1426    fn simple_struct() {
1427        #[derive(Facet)]
1428        struct Point {
1429            x: f64,
1430            y: f64,
1431        }
1432
1433        let schemas = extract_schemas(Point::SHAPE).unwrap().schemas.clone();
1434        assert!(schemas.len() >= 2);
1435
1436        let point_schema = schemas.last().unwrap();
1437        match &point_schema.kind {
1438            SchemaKind::Struct { name, fields } => {
1439                assert!(
1440                    name.contains("Point"),
1441                    "expected name to contain Point, got {name}"
1442                );
1443                assert_eq!(fields.len(), 2);
1444                assert_eq!(fields[0].name, "x");
1445                assert_eq!(fields[1].name, "y");
1446                assert!(fields[0].required);
1447                assert_eq!(fields[0].type_ref, fields[1].type_ref);
1448            }
1449            other => panic!("expected Struct, got {other:?}"),
1450        }
1451    }
1452
1453    // r[verify schema.format.enum]
1454    #[test]
1455    fn simple_enum() {
1456        #[derive(Facet)]
1457        #[repr(u8)]
1458        enum Color {
1459            Red,
1460            Green,
1461            Blue,
1462        }
1463
1464        let schemas = extract_schemas(Color::SHAPE).unwrap().schemas.clone();
1465        let color_schema = schemas.last().unwrap();
1466        match &color_schema.kind {
1467            SchemaKind::Enum { variants, .. } => {
1468                assert_eq!(variants.len(), 3);
1469                assert_eq!(variants[0].name, "Red");
1470                assert_eq!(variants[1].name, "Green");
1471                assert_eq!(variants[2].name, "Blue");
1472                assert!(matches!(variants[0].payload, VariantPayload::Unit));
1473            }
1474            other => panic!("expected Enum, got {other:?}"),
1475        }
1476    }
1477
1478    // r[verify schema.format.enum]
1479    #[test]
1480    fn enum_with_payloads() {
1481        #[derive(Facet)]
1482        #[repr(u8)]
1483        #[allow(dead_code)]
1484        enum Shape {
1485            Circle(f64),
1486            Rect { w: f64, h: f64 },
1487            Empty,
1488        }
1489
1490        let schemas = extract_schemas(Shape::SHAPE).unwrap().schemas.clone();
1491        let shape_schema = schemas.last().unwrap();
1492        match &shape_schema.kind {
1493            SchemaKind::Enum { variants, .. } => {
1494                assert_eq!(variants.len(), 3);
1495                assert!(matches!(
1496                    variants[0].payload,
1497                    VariantPayload::Newtype { .. }
1498                ));
1499                match &variants[1].payload {
1500                    VariantPayload::Struct { fields } => {
1501                        assert_eq!(fields.len(), 2);
1502                        assert_eq!(fields[0].name, "w");
1503                        assert_eq!(fields[1].name, "h");
1504                    }
1505                    other => panic!("expected Struct variant, got {other:?}"),
1506                }
1507                assert!(matches!(variants[2].payload, VariantPayload::Unit));
1508            }
1509            other => panic!("expected Enum, got {other:?}"),
1510        }
1511    }
1512
1513    // r[verify schema.format.container]
1514    #[test]
1515    fn container_vec() {
1516        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE)
1517            .unwrap()
1518            .schemas
1519            .clone();
1520        assert_eq!(schemas.len(), 2);
1521        assert!(matches!(
1522            schemas[0].kind,
1523            SchemaKind::Primitive {
1524                primitive_type: PrimitiveType::U32
1525            }
1526        ));
1527        assert!(matches!(schemas[1].kind, SchemaKind::List { .. }));
1528    }
1529
1530    // r[verify schema.format.container]
1531    #[test]
1532    fn container_option() {
1533        let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
1534            .unwrap()
1535            .schemas
1536            .clone();
1537        assert_eq!(schemas.len(), 2);
1538        assert!(matches!(
1539            schemas[0].kind,
1540            SchemaKind::Primitive {
1541                primitive_type: PrimitiveType::String
1542            }
1543        ));
1544        assert!(matches!(schemas[1].kind, SchemaKind::Option { .. }));
1545    }
1546
1547    // r[verify schema.format.recursive]
1548    #[test]
1549    fn recursive_type_terminates() {
1550        #[derive(Facet)]
1551        struct Node {
1552            value: u32,
1553            next: Option<Box<Node>>,
1554        }
1555
1556        let schemas = extract_schemas(Node::SHAPE).unwrap().schemas.clone();
1557        assert!(schemas.len() >= 2);
1558
1559        let node_schema = schemas.last().unwrap();
1560        assert!(matches!(node_schema.kind, SchemaKind::Struct { .. }));
1561    }
1562
1563    // r[verify schema.format.primitive]
1564    #[test]
1565    fn vec_u8_is_bytes() {
1566        let schemas = extract_schemas(<Vec<u8> as Facet>::SHAPE)
1567            .unwrap()
1568            .schemas
1569            .clone();
1570        assert_eq!(schemas.len(), 1);
1571        assert!(matches!(
1572            schemas[0].kind,
1573            SchemaKind::Primitive {
1574                primitive_type: PrimitiveType::Bytes
1575            }
1576        ));
1577    }
1578
1579    #[test]
1580    fn slice_u8_is_bytes() {
1581        let schemas = extract_schemas(<&[u8] as Facet>::SHAPE)
1582            .unwrap()
1583            .schemas
1584            .clone();
1585        assert_eq!(schemas.len(), 1);
1586        assert!(matches!(
1587            schemas[0].kind,
1588            SchemaKind::Primitive {
1589                primitive_type: PrimitiveType::Bytes
1590            }
1591        ));
1592    }
1593
1594    #[test]
1595    fn cbor_payload_is_bytes() {
1596        let schemas = extract_schemas(CborPayload::SHAPE).unwrap().schemas.clone();
1597        assert_eq!(schemas.len(), 1);
1598        assert!(matches!(
1599            schemas[0].kind,
1600            SchemaKind::Primitive {
1601                primitive_type: PrimitiveType::Bytes
1602            }
1603        ));
1604    }
1605
1606    // r[verify zerocopy.framing.value.opaque]
1607    #[test]
1608    fn opaque_payload_is_payload_primitive() {
1609        let schemas = extract_schemas(crate::Payload::<'static>::SHAPE)
1610            .unwrap()
1611            .schemas
1612            .clone();
1613        assert_eq!(schemas.len(), 1);
1614        assert!(matches!(
1615            schemas[0].kind,
1616            SchemaKind::Primitive {
1617                primitive_type: PrimitiveType::Payload
1618            }
1619        ));
1620    }
1621
1622    #[test]
1623    fn infallible_is_never_primitive() {
1624        let schemas = extract_schemas(<std::convert::Infallible as Facet>::SHAPE)
1625            .unwrap()
1626            .schemas
1627            .clone();
1628        assert_eq!(schemas.len(), 1);
1629        assert!(matches!(
1630            schemas[0].kind,
1631            SchemaKind::Primitive {
1632                primitive_type: PrimitiveType::Never
1633            }
1634        ));
1635    }
1636
1637    // r[verify schema.principles.once-per-type]
1638    #[test]
1639    fn deduplication_two_u32_fields() {
1640        #[derive(Facet)]
1641        struct TwoU32 {
1642            a: u32,
1643            b: u32,
1644        }
1645
1646        let schemas = extract_schemas(TwoU32::SHAPE).unwrap().schemas.clone();
1647        let u32_count = schemas
1648            .iter()
1649            .filter(|s| {
1650                matches!(
1651                    s.kind,
1652                    SchemaKind::Primitive {
1653                        primitive_type: PrimitiveType::U32
1654                    }
1655                )
1656            })
1657            .count();
1658        assert_eq!(u32_count, 1, "u32 schema should appear exactly once");
1659        assert_eq!(schemas.len(), 2);
1660    }
1661
1662    // r[verify schema.format.container]
1663    #[test]
1664    fn container_map() {
1665        let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
1666            .unwrap()
1667            .schemas
1668            .clone();
1669        let map_schema = schemas.last().unwrap();
1670        assert!(matches!(map_schema.kind, SchemaKind::Map { .. }));
1671    }
1672
1673    // r[verify schema.format.container]
1674    #[test]
1675    fn container_array() {
1676        let schemas = extract_schemas(<[u32; 4] as Facet>::SHAPE)
1677            .unwrap()
1678            .schemas
1679            .clone();
1680        let arr_schema = schemas.last().unwrap();
1681        match &arr_schema.kind {
1682            SchemaKind::Array { length, .. } => assert_eq!(*length, 4),
1683            other => panic!("expected Array, got {other:?}"),
1684        }
1685    }
1686
1687    // r[verify schema.format.tuple]
1688    #[test]
1689    fn tuple_type() {
1690        let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
1691            .unwrap()
1692            .schemas
1693            .clone();
1694        let tuple_schema = schemas.last().unwrap();
1695        match &tuple_schema.kind {
1696            SchemaKind::Tuple { elements } => {
1697                assert_eq!(elements.len(), 2);
1698                assert_ne!(elements[0], elements[1]);
1699            }
1700            other => panic!("expected Tuple, got {other:?}"),
1701        }
1702    }
1703
1704    // r[verify schema.format]
1705    #[test]
1706    fn extract_schemas_returns_all_kinds() {
1707        #[derive(Facet)]
1708        struct Mixed {
1709            count: u32,
1710            tags: Vec<String>,
1711            pair: (u8, u8),
1712        }
1713
1714        let schemas = extract_schemas(Mixed::SHAPE).unwrap().schemas.clone();
1715        assert!(schemas.len() >= 4);
1716    }
1717
1718    // r[verify schema.principles.once-per-type]
1719    // r[verify schema.exchange.idempotent]
1720    #[test]
1721    fn tracker_prepare_send_returns_payload_then_empty() {
1722        let mut tracker = SchemaSendTracker::new();
1723        let method = MethodId(1);
1724        let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1725        let first = tracker
1726            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1727            .unwrap();
1728        assert!(
1729            !first.is_empty(),
1730            "first prepare_send should return payload"
1731        );
1732        assert_eq!(schematic.attached.0, first.0);
1733        let second = tracker
1734            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1735            .unwrap();
1736        assert!(
1737            second.is_empty(),
1738            "second prepare_send for same method should return empty"
1739        );
1740        assert!(schematic.attached.is_empty());
1741    }
1742
1743    // r[verify schema.tracking.transitive]
1744    // r[verify schema.tracking.sent]
1745    #[test]
1746    fn tracker_prepare_send_includes_transitive_deps() {
1747        #[derive(Facet)]
1748        struct Outer {
1749            inner: u32,
1750            name: String,
1751        }
1752
1753        let mut tracker = SchemaSendTracker::new();
1754        let method = MethodId(1);
1755        let mut schematic = TestSchematic::new(BindingDirection::Args, Outer::SHAPE);
1756        let first = tracker
1757            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1758            .unwrap();
1759        assert!(!first.is_empty(), "should return schemas");
1760        let parsed = SchemaPayload::from_cbor(&first.0).expect("should parse CBOR");
1761        assert!(
1762            parsed.schemas.len() >= 3,
1763            "should include transitive deps, got {}",
1764            parsed.schemas.len()
1765        );
1766
1767        // Same method again — nothing to send
1768        schematic.shape = <u32 as Facet>::SHAPE;
1769        let again = tracker
1770            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
1771            .unwrap();
1772        assert!(
1773            again.is_empty(),
1774            "u32 was already sent as transitive dep, method already bound"
1775        );
1776    }
1777
1778    // r[verify schema.tracking.received]
1779    #[test]
1780    fn tracker_record_and_get_received() {
1781        let tracker = SchemaRecvTracker::new();
1782        let schemas = extract_schemas(<u32 as Facet>::SHAPE)
1783            .unwrap()
1784            .schemas
1785            .clone();
1786        let id = schemas[0].id;
1787        assert!(tracker.get_received(&id).is_none());
1788        tracker
1789            .record_received(
1790                MethodId(7),
1791                BindingDirection::Args,
1792                SchemaPayload {
1793                    schemas,
1794                    root: TypeRef::concrete(id),
1795                },
1796            )
1797            .expect("first record should succeed");
1798        assert!(tracker.get_received(&id).is_some());
1799        assert_eq!(
1800            tracker.get_remote_args_root(MethodId(7)),
1801            Some(TypeRef::concrete(id))
1802        );
1803    }
1804
1805    // r[verify schema.type-id]
1806    // r[verify schema.type-id.hash]
1807    #[test]
1808    fn type_ids_are_content_hashes() {
1809        let mut tracker = SchemaSendTracker::new();
1810        let extracted = tracker
1811            .extract_schemas(<(u32, String) as Facet>::SHAPE)
1812            .unwrap();
1813        let schemas = extracted.schemas.clone();
1814        assert!(schemas.len() >= 3);
1815
1816        // Same type extracted again must produce the same content hash.
1817        let mut tracker2 = SchemaSendTracker::new();
1818        let schemas2 = tracker2
1819            .extract_schemas(<(u32, String) as Facet>::SHAPE)
1820            .unwrap()
1821            .schemas
1822            .clone();
1823        assert_eq!(schemas.len(), schemas2.len());
1824        for (a, b) in schemas.iter().zip(schemas2.iter()) {
1825            assert_eq!(a.id, b.id, "content hash should be deterministic");
1826        }
1827
1828        // Different types must produce different hashes.
1829        let mut tracker3 = SchemaSendTracker::new();
1830        let extracted3 = tracker3
1831            .extract_schemas(<(u64, String) as Facet>::SHAPE)
1832            .unwrap();
1833        assert_ne!(
1834            extracted.root, extracted3.root,
1835            "different types should produce different root refs"
1836        );
1837    }
1838
1839    // r[verify schema.type-id.hash.primitives]
1840    #[test]
1841    fn primitive_content_hashes_are_stable() {
1842        // These are the canonical hash values for primitive types.
1843        // Other implementations MUST produce identical values.
1844        let primitives = [
1845            PrimitiveType::Bool,
1846            PrimitiveType::U8,
1847            PrimitiveType::U16,
1848            PrimitiveType::U32,
1849            PrimitiveType::U64,
1850            PrimitiveType::U128,
1851            PrimitiveType::I8,
1852            PrimitiveType::I16,
1853            PrimitiveType::I32,
1854            PrimitiveType::I64,
1855            PrimitiveType::I128,
1856            PrimitiveType::F32,
1857            PrimitiveType::F64,
1858            PrimitiveType::Char,
1859            PrimitiveType::String,
1860            PrimitiveType::Unit,
1861            PrimitiveType::Never,
1862            PrimitiveType::Bytes,
1863            PrimitiveType::Payload,
1864        ];
1865
1866        // All primitive hashes must be unique.
1867        let hashes: Vec<SchemaHash> = primitives
1868            .iter()
1869            .map(|p| {
1870                compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id)
1871            })
1872            .collect();
1873        let unique: HashSet<SchemaHash> = hashes.iter().copied().collect();
1874        assert_eq!(
1875            unique.len(),
1876            hashes.len(),
1877            "all primitive hashes must be unique"
1878        );
1879
1880        // Verify they're deterministic (same computation, same result).
1881        for (i, p) in primitives.iter().enumerate() {
1882            let hash2 =
1883                compute_content_hash(&SchemaKind::Primitive { primitive_type: *p }, &[], &|id| id);
1884            assert_eq!(hashes[i], hash2, "hash for {:?} must be deterministic", p);
1885        }
1886    }
1887
1888    // r[verify schema.type-id.hash.struct]
1889    #[test]
1890    fn struct_hash_is_deterministic() {
1891        #[derive(Facet)]
1892        struct Point {
1893            x: f64,
1894            y: f64,
1895        }
1896
1897        let schemas1 = extract_schemas(Point::SHAPE).unwrap().schemas.clone();
1898        let schemas2 = extract_schemas(Point::SHAPE).unwrap().schemas.clone();
1899        assert_eq!(
1900            schemas1.last().unwrap().id,
1901            schemas2.last().unwrap().id,
1902            "same struct must produce the same content hash"
1903        );
1904    }
1905
1906    // r[verify schema.hash.recursive]
1907    #[test]
1908    fn recursive_type_hash_is_deterministic() {
1909        #[derive(Facet)]
1910        struct TreeNode {
1911            label: String,
1912            children: Vec<TreeNode>,
1913        }
1914
1915        let schemas1 = extract_schemas(TreeNode::SHAPE).unwrap().schemas.clone();
1916        let schemas2 = extract_schemas(TreeNode::SHAPE).unwrap().schemas.clone();
1917
1918        // Must have at least String, Vec<TreeNode>, TreeNode
1919        assert!(schemas1.len() >= 2);
1920
1921        // Same recursive type must produce identical hashes.
1922        let root1 = schemas1.last().unwrap().id;
1923        let root2 = schemas2.last().unwrap().id;
1924        assert_eq!(root1, root2, "recursive type hash must be deterministic");
1925
1926        // All type IDs in the output must be valid content hashes (non-zero).
1927        for s in &schemas1 {
1928            assert_ne!(s.id.0, 0, "content hash must not be zero");
1929        }
1930    }
1931
1932    #[test]
1933    fn bidirectional_bindings_are_independent() {
1934        let mut tracker = SchemaSendTracker::new();
1935        let method = MethodId(1);
1936
1937        // Send args binding
1938        let mut args_schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
1939        let args = tracker
1940            .attach_schemas_for_shape_if_needed(method, args_schematic.shape, &mut args_schematic)
1941            .unwrap();
1942        assert!(!args.is_empty(), "should send args");
1943        let args_parsed = SchemaPayload::from_cbor(&args.0).expect("parse args CBOR");
1944
1945        // Send response binding for the same method — should NOT be deduplicated
1946        let mut response_schematic =
1947            TestSchematic::new(BindingDirection::Response, <String as Facet>::SHAPE);
1948        let response = tracker
1949            .attach_schemas_for_shape_if_needed(
1950                method,
1951                response_schematic.shape,
1952                &mut response_schematic,
1953            )
1954            .unwrap();
1955        assert!(!response.is_empty(), "should send response");
1956        let response_parsed = SchemaPayload::from_cbor(&response.0).expect("parse response CBOR");
1957        assert_ne!(args_parsed.root, response_parsed.root);
1958
1959        // Record received bindings and verify they go to separate maps
1960        let recv_tracker = SchemaRecvTracker::new();
1961        recv_tracker
1962            .record_received(
1963                MethodId(42),
1964                BindingDirection::Args,
1965                SchemaPayload {
1966                    schemas: extract_schemas(<u64 as Facet>::SHAPE)
1967                        .unwrap()
1968                        .schemas
1969                        .clone(),
1970                    root: TypeRef::concrete(SchemaHash(100)),
1971                },
1972            )
1973            .expect("record should succeed");
1974        recv_tracker
1975            .record_received(
1976                MethodId(42),
1977                BindingDirection::Response,
1978                SchemaPayload {
1979                    schemas: vec![],
1980                    root: TypeRef::concrete(SchemaHash(200)),
1981                },
1982            )
1983            .expect("record should succeed");
1984
1985        assert_eq!(
1986            recv_tracker.get_remote_args_root(MethodId(42)),
1987            Some(TypeRef::concrete(SchemaHash(100)))
1988        );
1989        assert_eq!(
1990            recv_tracker.get_remote_response_root(MethodId(42)),
1991            Some(TypeRef::concrete(SchemaHash(200)))
1992        );
1993    }
1994
1995    #[test]
1996    fn duplicate_schema_is_protocol_error() {
1997        let tracker = SchemaRecvTracker::new();
1998        let schemas = extract_schemas(<u32 as Facet>::SHAPE)
1999            .unwrap()
2000            .schemas
2001            .clone();
2002        tracker
2003            .record_received(
2004                MethodId(9),
2005                BindingDirection::Args,
2006                SchemaPayload {
2007                    schemas: schemas.clone(),
2008                    root: TypeRef::concrete(schemas[0].id),
2009                },
2010            )
2011            .expect("first record should succeed");
2012        let err = tracker
2013            .record_received(
2014                MethodId(9),
2015                BindingDirection::Args,
2016                SchemaPayload {
2017                    schemas: schemas.clone(),
2018                    root: TypeRef::concrete(schemas[0].id),
2019                },
2020            )
2021            .expect_err("duplicate should fail");
2022        assert_eq!(err.type_id, schemas[0].id);
2023    }
2024
2025    #[test]
2026    fn send_tracker_reset_clears_all_state() {
2027        let mut tracker = SchemaSendTracker::new();
2028        let method = MethodId(1);
2029        let mut schematic = TestSchematic::new(BindingDirection::Args, <u32 as Facet>::SHAPE);
2030        let first = tracker
2031            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
2032            .unwrap();
2033        assert!(!first.is_empty(), "first should return payload");
2034
2035        tracker.reset();
2036
2037        let after_reset = tracker
2038            .attach_schemas_for_shape_if_needed(method, schematic.shape, &mut schematic)
2039            .unwrap();
2040        assert!(
2041            !after_reset.is_empty(),
2042            "after reset, prepare_send should return payload again"
2043        );
2044    }
2045
2046    // ========================================================================
2047    // Generic type deduplication tests
2048    // ========================================================================
2049
2050    #[test]
2051    fn generic_vec_uses_var_in_body() {
2052        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE)
2053            .unwrap()
2054            .schemas
2055            .clone();
2056        let list_schema = schemas
2057            .iter()
2058            .find(|s| matches!(s.kind, SchemaKind::List { .. }))
2059            .unwrap();
2060        assert_eq!(
2061            list_schema.type_params.len(),
2062            1,
2063            "Vec should have 1 type param"
2064        );
2065        match &list_schema.kind {
2066            SchemaKind::List { element } => {
2067                assert!(
2068                    matches!(element, TypeRef::Var { .. }),
2069                    "element should be Var, got {element:?}"
2070                );
2071            }
2072            other => panic!("expected List, got {other:?}"),
2073        }
2074    }
2075
2076    #[test]
2077    fn generic_option_uses_var_in_body() {
2078        let schemas = extract_schemas(<Option<String> as Facet>::SHAPE)
2079            .unwrap()
2080            .schemas
2081            .clone();
2082        let opt_schema = schemas
2083            .iter()
2084            .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
2085            .unwrap();
2086        assert_eq!(
2087            opt_schema.type_params.len(),
2088            1,
2089            "Option should have 1 type param"
2090        );
2091        match &opt_schema.kind {
2092            SchemaKind::Option { element } => {
2093                assert!(
2094                    matches!(element, TypeRef::Var { .. }),
2095                    "element should be Var, got {element:?}"
2096                );
2097            }
2098            other => panic!("expected Option, got {other:?}"),
2099        }
2100    }
2101
2102    #[test]
2103    fn generic_tuple_uses_vars_in_body() {
2104        let schemas = extract_schemas(<(u32, String) as Facet>::SHAPE)
2105            .unwrap()
2106            .schemas
2107            .clone();
2108        let tuple_schema = schemas
2109            .iter()
2110            .find(|s| matches!(s.kind, SchemaKind::Tuple { .. }))
2111            .unwrap();
2112        assert_eq!(
2113            tuple_schema.type_params.len(),
2114            2,
2115            "tuple arity 2 should have 2 type params"
2116        );
2117        match &tuple_schema.kind {
2118            SchemaKind::Tuple { elements } => {
2119                assert_eq!(elements.len(), 2);
2120                assert!(matches!(elements[0], TypeRef::Var { .. }));
2121                assert!(matches!(elements[1], TypeRef::Var { .. }));
2122            }
2123            other => panic!("expected Tuple, got {other:?}"),
2124        }
2125    }
2126
2127    #[test]
2128    fn generic_vox_error_uses_var_in_user_payload() {
2129        use crate::VoxError;
2130
2131        let schemas = extract_schemas(<VoxError<::core::convert::Infallible> as Facet>::SHAPE)
2132            .unwrap()
2133            .schemas
2134            .clone();
2135        let vox_error_schema = schemas
2136            .iter()
2137            .find(|s| matches!(&s.kind, SchemaKind::Enum { name, .. } if name == "VoxError"))
2138            .expect("VoxError schema should be present");
2139        match &vox_error_schema.kind {
2140            SchemaKind::Enum { variants, .. } => {
2141                let user = variants
2142                    .iter()
2143                    .find(|variant| variant.name == "User")
2144                    .expect("VoxError should have User variant");
2145                let VariantPayload::Newtype { type_ref } = &user.payload else {
2146                    panic!("User variant should be newtype");
2147                };
2148                assert!(
2149                    matches!(type_ref, TypeRef::Var { .. }),
2150                    "User payload should be a type variable, got {type_ref:?}"
2151                );
2152            }
2153            other => panic!("expected enum, got {other:?}"),
2154        }
2155    }
2156
2157    #[test]
2158    fn vec_of_option_of_u32_deduplicates() {
2159        // Vec<Option<u32>> should produce: u32, Option<T>, Vec<T>
2160        // NOT: u32, Option<u32>, Vec<Option<u32>>
2161        let schemas = extract_schemas(<Vec<Option<u32>> as Facet>::SHAPE)
2162            .unwrap()
2163            .schemas
2164            .clone();
2165
2166        let list_count = schemas
2167            .iter()
2168            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2169            .count();
2170        let option_count = schemas
2171            .iter()
2172            .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2173            .count();
2174        assert_eq!(list_count, 1, "should have exactly 1 List schema");
2175        assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2176    }
2177
2178    #[test]
2179    fn vec_u32_and_vec_string_share_one_list_schema() {
2180        #[derive(Facet)]
2181        struct Both {
2182            a: Vec<u32>,
2183            b: Vec<String>,
2184        }
2185
2186        let schemas = extract_schemas(Both::SHAPE).unwrap().schemas.clone();
2187        let list_count = schemas
2188            .iter()
2189            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2190            .count();
2191        assert_eq!(
2192            list_count, 1,
2193            "Vec<u32> and Vec<String> should share one List schema"
2194        );
2195    }
2196
2197    #[test]
2198    fn resolve_kind_substitutes_vars() {
2199        let schemas = extract_schemas(<Vec<u32> as Facet>::SHAPE)
2200            .unwrap()
2201            .schemas
2202            .clone();
2203        let registry = build_registry(&schemas);
2204
2205        // The root schema is Vec<u32> — find it
2206        let root = schemas.last().unwrap();
2207        assert!(matches!(root.kind, SchemaKind::List { .. }));
2208
2209        // Build a TypeRef that says "Vec applied to u32"
2210        let u32_schema = schemas
2211            .iter()
2212            .find(|s| {
2213                matches!(
2214                    s.kind,
2215                    SchemaKind::Primitive {
2216                        primitive_type: PrimitiveType::U32
2217                    }
2218                )
2219            })
2220            .unwrap();
2221        let type_ref = TypeRef::generic(root.id, vec![TypeRef::concrete(u32_schema.id)]);
2222
2223        // resolve_kind should substitute Var("T") → concrete u32 id
2224        let resolved = type_ref.resolve_kind(&registry).expect("should resolve");
2225        match &resolved {
2226            SchemaKind::List { element } => match element {
2227                TypeRef::Concrete { type_id, args } => {
2228                    assert_eq!(*type_id, u32_schema.id);
2229                    assert!(args.is_empty());
2230                }
2231                other => panic!("expected concrete after resolution, got {other:?}"),
2232            },
2233            other => panic!("expected List, got {other:?}"),
2234        }
2235    }
2236
2237    #[test]
2238    fn extract_result_tuple_root_preserves_ok_tuple() {
2239        use crate::VoxError;
2240
2241        let extracted = extract_schemas(
2242            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2243        )
2244        .unwrap();
2245        let registry = build_registry(&extracted.schemas);
2246        let root = extracted
2247            .root
2248            .resolve_kind(&registry)
2249            .expect("result root should resolve");
2250
2251        let SchemaKind::Enum { variants, .. } = root else {
2252            panic!("expected Result enum root");
2253        };
2254        let ok_variant = variants
2255            .iter()
2256            .find(|variant| variant.name == "Ok")
2257            .expect("Result should have Ok variant");
2258        let VariantPayload::Newtype { type_ref } = &ok_variant.payload else {
2259            panic!("Ok variant should be newtype");
2260        };
2261        let ok_kind = type_ref
2262            .resolve_kind(&registry)
2263            .expect("Ok payload should resolve");
2264        match ok_kind {
2265            SchemaKind::Tuple { elements } => {
2266                assert_eq!(elements.len(), 2, "Ok tuple should have two elements");
2267            }
2268            other => panic!("expected Ok payload to be tuple, got {other:?}"),
2269        }
2270    }
2271
2272    #[test]
2273    fn result_ok_tuple_uses_generic_tuple_schema() {
2274        use crate::VoxError;
2275
2276        let result_shape =
2277            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE;
2278        let ok_shape = result_shape.type_params[0].shape;
2279        let extracted = extract_schemas(
2280            <Result<(String, i32), VoxError<::core::convert::Infallible>> as Facet>::SHAPE,
2281        )
2282        .unwrap();
2283        let TypeRef::Concrete { args, .. } = &extracted.root else {
2284            panic!("Result root should be concrete");
2285        };
2286        assert_eq!(
2287            args.len(),
2288            2,
2289            "Result root should have Ok and Err type args"
2290        );
2291        let TypeRef::Concrete { args: ok_args, .. } = &args[0] else {
2292            panic!("Ok type arg should be concrete tuple ref");
2293        };
2294        assert_eq!(
2295            ok_args.len(),
2296            2,
2297            "Ok tuple ref should carry concrete tuple element args; root={:?}; ok_shape={}; ok_shape_ty={:?}",
2298            extracted.root,
2299            ok_shape.type_identifier,
2300            ok_shape.ty
2301        );
2302    }
2303
2304    #[test]
2305    fn unary_tuple_root_preserves_nested_tuple() {
2306        let extracted = extract_schemas(<((i32, String),) as Facet>::SHAPE).unwrap();
2307        let registry = build_registry(&extracted.schemas);
2308
2309        let root = extracted
2310            .root
2311            .resolve_kind(&registry)
2312            .expect("root should resolve");
2313        let SchemaKind::Tuple { elements } = root else {
2314            panic!("expected unary tuple root");
2315        };
2316        assert_eq!(elements.len(), 1, "outer tuple should remain unary");
2317
2318        let inner = elements[0]
2319            .resolve_kind(&registry)
2320            .expect("inner tuple should resolve");
2321        match inner {
2322            SchemaKind::Tuple { elements } => {
2323                assert_eq!(elements.len(), 2, "inner tuple should remain binary");
2324            }
2325            other => panic!("expected inner tuple, got {other:?}"),
2326        }
2327
2328        let tuple_count = extracted
2329            .schemas
2330            .iter()
2331            .filter(|schema| matches!(schema.kind, SchemaKind::Tuple { .. }))
2332            .count();
2333        assert_eq!(tuple_count, 2, "should emit one tuple schema per arity");
2334    }
2335
2336    #[test]
2337    fn nested_generic_vec_of_vec_of_u32() {
2338        // Vec<Vec<u32>> — should produce u32, Vec<T>, not u32, Vec<u32>, Vec<Vec<u32>>
2339        let schemas = extract_schemas(<Vec<Vec<u32>> as Facet>::SHAPE)
2340            .unwrap()
2341            .schemas
2342            .clone();
2343        let list_count = schemas
2344            .iter()
2345            .filter(|s| matches!(s.kind, SchemaKind::List { .. }))
2346            .count();
2347        assert_eq!(
2348            list_count, 1,
2349            "Vec<Vec<u32>> should have exactly 1 List schema (Vec<T>)"
2350        );
2351    }
2352
2353    #[test]
2354    fn recursive_type_with_option_box() {
2355        #[derive(Facet)]
2356        struct Node {
2357            value: u32,
2358            next: Option<Box<Node>>,
2359        }
2360
2361        let schemas = extract_schemas(Node::SHAPE).unwrap().schemas.clone();
2362        // Should have: u32, Option<T>, Node
2363        let option_count = schemas
2364            .iter()
2365            .filter(|s| matches!(s.kind, SchemaKind::Option { .. }))
2366            .count();
2367        assert_eq!(option_count, 1, "should have exactly 1 Option schema");
2368
2369        // The Option schema should use Var, not concrete
2370        let opt_schema = schemas
2371            .iter()
2372            .find(|s| matches!(s.kind, SchemaKind::Option { .. }))
2373            .unwrap();
2374        match &opt_schema.kind {
2375            SchemaKind::Option { element } => {
2376                assert!(
2377                    matches!(element, TypeRef::Var { .. }),
2378                    "element should be Var"
2379                );
2380            }
2381            _ => unreachable!(),
2382        }
2383
2384        // All type IDs should be non-zero (properly hashed)
2385        for s in &schemas {
2386            assert_ne!(s.id.0, 0, "content hash must not be zero: {:?}", s.kind);
2387        }
2388    }
2389
2390    #[test]
2391    fn map_schema_is_generic() {
2392        let schemas = extract_schemas(<std::collections::HashMap<String, u32> as Facet>::SHAPE)
2393            .unwrap()
2394            .schemas
2395            .clone();
2396        let map_schema = schemas
2397            .iter()
2398            .find(|s| matches!(s.kind, SchemaKind::Map { .. }))
2399            .unwrap();
2400        assert_eq!(
2401            map_schema.type_params.len(),
2402            2,
2403            "HashMap should have 2 type params"
2404        );
2405        match &map_schema.kind {
2406            SchemaKind::Map { key, value } => {
2407                assert!(matches!(key, TypeRef::Var { .. }), "key should be Var");
2408                assert!(matches!(value, TypeRef::Var { .. }), "value should be Var");
2409            }
2410            _ => unreachable!(),
2411        }
2412    }
2413
2414    #[test]
2415    fn schema_payload_cbor_round_trip() {
2416        let payload = SchemaPayload {
2417            schemas: vec![],
2418            root: TypeRef::Concrete {
2419                type_id: SchemaHash(123),
2420                args: vec![TypeRef::concrete(SchemaHash(456))],
2421            },
2422        };
2423        let bytes = payload.to_cbor();
2424        let parsed = SchemaPayload::from_cbor(&bytes.0).expect("should parse CBOR");
2425        match &parsed.root {
2426            TypeRef::Concrete { type_id, args } => {
2427                assert_eq!(*type_id, SchemaHash(123));
2428                assert_eq!(args.len(), 1);
2429                match &args[0] {
2430                    TypeRef::Concrete { type_id, args } => {
2431                        assert_eq!(*type_id, SchemaHash(456));
2432                        assert!(args.is_empty());
2433                    }
2434                    other => panic!("expected concrete arg, got {other:?}"),
2435                }
2436            }
2437            other => panic!("expected concrete root, got {other:?}"),
2438        }
2439    }
2440}