Skip to main content

zerodds_types/
assignability.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Type-Assignability (XTypes 1.3 §7.2.4.1).
4//!
5//! Determines whether a type `T1` can be assigned to a type `T2` —
6//! corresponds to "compatible for publication/subscription match". The
7//! rules depend on extensibility (final/appendable/mutable) +
8//! TypeConsistencyEnforcement.
9//!
10//! Core rules for primitives, strings,
11//! collections, aliases (via the resolver), enums + structs with
12//! final/appendable/mutable semantics. Strict-vs-lax variant via
13//! `AssignabilityConfig`.
14
15use crate::resolve::{TypeRegistry, resolve_alias_chain};
16use crate::type_identifier::{PrimitiveKind, TypeIdentifier};
17use crate::type_object::flags::StructTypeFlag;
18use crate::type_object::minimal::{MinimalStructType, MinimalTypeObject};
19
20/// Error while flattening an inheritance chain.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum InheritanceError {
23    /// `base_type` points to an `EquivalenceHash` that is not in the
24    /// registry.
25    UnknownBase {
26        /// The unresolved hash.
27        hash: crate::type_identifier::EquivalenceHash,
28    },
29    /// `base_type` points to a TypeObject that is not a struct
30    /// (e.g. an enum or an alias to an enum).
31    BaseNotAStruct,
32    /// Inheritance cycle detected.
33    Cycle,
34    /// Maximum depth exceeded.
35    DepthExceeded {
36        /// Limit.
37        limit: usize,
38    },
39    /// Member name or member ID collides between base and derived
40    /// (XTypes 1.3 §7.2.2.4.5).
41    InheritanceConflict {
42        /// Reference ID or name.
43        member_id: u32,
44        /// Description.
45        reason: &'static str,
46    },
47}
48
49/// Builds the "hypothetical flat type" structure from a single-inheritance
50/// chain (XTypes 1.3 §7.2.2.4.5). Resolves `base_type` recursively and
51/// concatenates the base members followed by the derived members. On member-name
52/// or member-ID collisions, `InheritanceConflict` is returned.
53///
54/// `max_depth` limits the depth of the inheritance chain (cycle protection).
55///
56/// # Errors
57/// Siehe [`InheritanceError`].
58pub fn flatten_inheritance(
59    s: &MinimalStructType,
60    registry: &TypeRegistry,
61    max_depth: usize,
62) -> Result<MinimalStructType, InheritanceError> {
63    use alloc::collections::BTreeSet;
64
65    let mut visited: BTreeSet<crate::type_identifier::EquivalenceHash> = BTreeSet::new();
66    let mut chain: alloc::vec::Vec<MinimalStructType> = alloc::vec::Vec::new();
67    let mut current = s.clone();
68    for _ in 0..max_depth {
69        let base_ti = current.header.base_type.clone();
70        chain.push(current.clone());
71        match base_ti {
72            TypeIdentifier::None => break,
73            TypeIdentifier::EquivalenceHashMinimal(h)
74            | TypeIdentifier::EquivalenceHashComplete(h) => {
75                if !visited.insert(h) {
76                    return Err(InheritanceError::Cycle);
77                }
78                let to = match registry.get_minimal(&h) {
79                    Some(MinimalTypeObject::Struct(b)) => b.clone(),
80                    Some(_) => return Err(InheritanceError::BaseNotAStruct),
81                    None => return Err(InheritanceError::UnknownBase { hash: h }),
82                };
83                current = to;
84            }
85            _ => return Err(InheritanceError::BaseNotAStruct),
86        }
87    }
88    if chain
89        .last()
90        .is_none_or(|c| c.header.base_type != TypeIdentifier::None)
91    {
92        return Err(InheritanceError::DepthExceeded { limit: max_depth });
93    }
94
95    // chain is [Derived, Mid1, Mid2, ..., Root].
96    // Spec §7.2.2.4.5: concatenate from root to derived (base members first).
97    let mut flat_members: alloc::vec::Vec<crate::type_object::minimal::MinimalStructMember> =
98        alloc::vec::Vec::new();
99    let mut seen_ids: BTreeSet<u32> = BTreeSet::new();
100    let mut seen_names: BTreeSet<crate::type_object::common::NameHash> = BTreeSet::new();
101    for st in chain.iter().rev() {
102        for m in &st.member_seq {
103            if !seen_ids.insert(m.common.member_id) {
104                return Err(InheritanceError::InheritanceConflict {
105                    member_id: m.common.member_id,
106                    reason: "member_id collides between base and derived",
107                });
108            }
109            if !seen_names.insert(m.detail) {
110                return Err(InheritanceError::InheritanceConflict {
111                    member_id: m.common.member_id,
112                    reason: "member name_hash collides between base and derived",
113                });
114            }
115            flat_members.push(m.clone());
116        }
117    }
118
119    // Result: the derived's header (incl. base_type=None after flatten) +
120    // concatenated members.
121    let Some(derived) = chain.first().cloned() else {
122        return Err(InheritanceError::DepthExceeded { limit: max_depth });
123    };
124    let mut flat = derived;
125    flat.header.base_type = TypeIdentifier::None;
126    flat.member_seq = flat_members;
127    Ok(flat)
128}
129
130/// Configuration for assignability checks.
131///
132/// The fields other than `max_depth` correspond 1:1 to the flags of the DDS-QoS
133/// `TypeConsistencyEnforcement` (XTypes §7.6.3.7). `TypeMatcher`
134/// translates a concrete TCE policy into this struct.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct AssignabilityConfig {
137    /// Allow type coercion (int32 ↔ int64 etc.)?
138    pub allow_type_coercion: bool,
139    /// Ignore sequence bounds (the writer may be larger than the reader).
140    pub ignore_sequence_bounds: bool,
141    /// Ignore string bounds.
142    pub ignore_string_bounds: bool,
143    /// Ignore member names — mutable structs then match
144    /// only via the `@id` member ID, not via the NameHash.
145    pub ignore_member_names: bool,
146    /// `@ignore_literal_names` globally (XTypes §7.2.4.4.7) — enum compat
147    /// compares only ordinal values, not literal names. Additionally
148    /// this can be set per `EnumTypeFlag::IGNORE_LITERAL_NAMES` on a
149    /// single side; the disjunction wins.
150    pub ignore_literal_names: bool,
151    /// Maximum depth for recursive resolution.
152    pub max_depth: usize,
153}
154
155impl Default for AssignabilityConfig {
156    fn default() -> Self {
157        Self {
158            allow_type_coercion: false,
159            ignore_sequence_bounds: true,
160            ignore_string_bounds: true,
161            ignore_member_names: false,
162            ignore_literal_names: false,
163            max_depth: crate::resolve::DEFAULT_MAX_RESOLVE_DEPTH,
164        }
165    }
166}
167
168/// Result of the assignability check.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum Assignable {
171    /// Compatible.
172    Yes,
173    /// Not compatible, with a reason.
174    No(&'static str),
175}
176
177impl Assignable {
178    /// `true` if compatible.
179    #[must_use]
180    pub const fn is_yes(&self) -> bool {
181        matches!(self, Self::Yes)
182    }
183}
184
185/// Checks whether the writer type `w` is compatible for the reader type `r`.
186///
187/// zerodds-lint: recursion-depth 64
188///
189/// **Important on the depth number**: the `64` is the *default*
190/// ([`DEFAULT_MAX_RESOLVE_DEPTH`]) and **not** enforced by code —
191/// `cfg.max_depth` is runtime-configurable. Tests run values up to
192/// 512 (`resolve_depth_exceeded` tests); production callers should
193/// use the default `AssignabilityConfig`, otherwise tune the value
194/// explicitly with the risk assessment.
195///
196/// Recursion via `check_direct` → `is_assignable` (nested sequences,
197/// struct members).
198pub fn is_assignable(
199    w: &TypeIdentifier,
200    r: &TypeIdentifier,
201    registry: &TypeRegistry,
202    cfg: &AssignabilityConfig,
203) -> Assignable {
204    // Identity short-circuit FIRST (XTypes 1.3 §7.2.4.1: a type is always
205    // assignable to itself). This must run *before* alias resolution, because
206    // for a hash-referenced type (Minimal **or** Complete) that is absent from
207    // the registry (e.g. a typed endpoint whose generated type carries a
208    // complete TypeIdentifier and whose TypeObject was never registered),
209    // `resolve_alias_chain` returns `Unknown` and would otherwise mask the
210    // identity match. Two endpoints carrying the *same* complete TypeIdentifier
211    // are by definition the same type.
212    if w == r {
213        return Assignable::Yes;
214    }
215
216    // Alias resolution on both sides.
217    let Ok(w) = resolve_alias_chain(w, registry, cfg.max_depth) else {
218        return Assignable::No("writer alias resolution failed");
219    };
220    let Ok(r) = resolve_alias_chain(r, registry, cfg.max_depth) else {
221        return Assignable::No("reader alias resolution failed");
222    };
223
224    check_direct(&w, &r, registry, cfg)
225}
226
227/// zerodds-lint: recursion-depth 64
228///
229/// Dispatches the assignability checks per TypeIdentifier variant.
230/// Nested types (sequence, struct member) call `is_assignable` recursively
231/// — depth cap via `cfg.max_depth`.
232fn check_direct(
233    w: &TypeIdentifier,
234    r: &TypeIdentifier,
235    registry: &TypeRegistry,
236    cfg: &AssignabilityConfig,
237) -> Assignable {
238    // Exact identity → always Yes.
239    if w == r {
240        return Assignable::Yes;
241    }
242
243    match (w, r) {
244        // Primitive → Primitive
245        (TypeIdentifier::Primitive(wp), TypeIdentifier::Primitive(rp)) => {
246            primitive_compatible(*wp, *rp, cfg)
247        }
248        // String compatibility. Small/Large is only an encoding detail;
249        // bounds are filtered via `ignore_string_bounds`.
250        (
251            TypeIdentifier::String8Small { .. } | TypeIdentifier::String8Large { .. },
252            TypeIdentifier::String8Small { .. } | TypeIdentifier::String8Large { .. },
253        ) => {
254            let (wb, rb) = (string_bound_u32_s8(w), string_bound_u32_s8(r));
255            if !cfg.ignore_string_bounds && rb != 0 && wb > rb {
256                Assignable::No("writer string8 bound exceeds reader bound")
257            } else {
258                Assignable::Yes
259            }
260        }
261        (
262            TypeIdentifier::String16Small { .. } | TypeIdentifier::String16Large { .. },
263            TypeIdentifier::String16Small { .. } | TypeIdentifier::String16Large { .. },
264        ) => {
265            let (wb, rb) = (string_bound_u32_s16(w), string_bound_u32_s16(r));
266            if !cfg.ignore_string_bounds && rb != 0 && wb > rb {
267                Assignable::No("writer string16 bound exceeds reader bound")
268            } else {
269                Assignable::Yes
270            }
271        }
272
273        // Sequence compatibility: Small ↔ Small/Large, Large ↔ Large/Small.
274        // Small vs Large is only wire encoding; bounds are normalized to u32
275        // and filtered per policy.
276        (
277            TypeIdentifier::PlainSequenceSmall { .. } | TypeIdentifier::PlainSequenceLarge { .. },
278            TypeIdentifier::PlainSequenceSmall { .. } | TypeIdentifier::PlainSequenceLarge { .. },
279        ) => {
280            let (we, wb) = sequence_parts(w);
281            let (re, rb) = sequence_parts(r);
282            if !cfg.ignore_sequence_bounds && rb != 0 && wb > rb {
283                return Assignable::No("writer sequence bound exceeds reader bound");
284            }
285            is_assignable(we, re, registry, cfg)
286        }
287
288        // Array: fixed dimensions. The `bound_seq` comparison is structural;
289        // ignore-bounds does not apply here (array bounds are not a policy matter).
290        // Array: Small + Large among each other; normalize bounds to a u32 vec
291        // and compare structurally.
292        (
293            TypeIdentifier::PlainArraySmall { .. } | TypeIdentifier::PlainArrayLarge { .. },
294            TypeIdentifier::PlainArraySmall { .. } | TypeIdentifier::PlainArrayLarge { .. },
295        ) => {
296            let (we, wb) = array_parts(w);
297            let (re, rb) = array_parts(r);
298            if wb != rb {
299                return Assignable::No("array bounds differ");
300            }
301            is_assignable(we, re, registry, cfg)
302        }
303
304        // Map: Small ↔ Small/Large, Large ↔ Large/Small.
305        (
306            TypeIdentifier::PlainMapSmall { .. } | TypeIdentifier::PlainMapLarge { .. },
307            TypeIdentifier::PlainMapSmall { .. } | TypeIdentifier::PlainMapLarge { .. },
308        ) => {
309            let (we, wk, wb) = map_parts(w);
310            let (re, rk, rb) = map_parts(r);
311            if !cfg.ignore_sequence_bounds && rb != 0 && wb > rb {
312                return Assignable::No("writer map bound exceeds reader bound");
313            }
314            match is_assignable(wk, rk, registry, cfg) {
315                Assignable::Yes => is_assignable(we, re, registry, cfg),
316                e => e,
317            }
318        }
319
320        // Hash refs: both on Minimal → structural equality.
321        (
322            TypeIdentifier::EquivalenceHashMinimal(wh),
323            TypeIdentifier::EquivalenceHashMinimal(rh),
324        ) => {
325            if wh == rh {
326                return Assignable::Yes;
327            }
328            // Compare TypeObjects from the registry.
329            match (registry.get_minimal(wh), registry.get_minimal(rh)) {
330                (Some(wobj), Some(robj)) => check_minimal_types(wobj, robj, registry, cfg),
331                _ => Assignable::No("unknown type objects for hash comparison"),
332            }
333        }
334        // Hash refs: both on Complete (XTypes 1.3 §7.3.4.1 — a complete
335        // TypeIdentifier carries the EquivalenceHashComplete). Equal hashes are
336        // the same type. With both TypeObjects absent from the registry (the
337        // typed-endpoint same-runtime case), equal hashes are the only thing we
338        // can compare and are sufficient: identical complete hashes ⇒ identical
339        // types. When the complete TypeObjects are registered we down-project to
340        // the structural minimal comparison.
341        (
342            TypeIdentifier::EquivalenceHashComplete(wh),
343            TypeIdentifier::EquivalenceHashComplete(rh),
344        ) => {
345            if wh == rh {
346                return Assignable::Yes;
347            }
348            // Differing complete hashes: only assignable if the registry holds
349            // both minimal projections and they are structurally compatible.
350            match (registry.get_minimal(wh), registry.get_minimal(rh)) {
351                (Some(wobj), Some(robj)) => check_minimal_types(wobj, robj, registry, cfg),
352                _ => Assignable::No("unknown complete type objects for hash comparison"),
353            }
354        }
355
356        _ => Assignable::No("kinds do not match"),
357    }
358}
359
360fn sequence_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, u32) {
361    match ti {
362        TypeIdentifier::PlainSequenceSmall { element, bound, .. } => (element, u32::from(*bound)),
363        TypeIdentifier::PlainSequenceLarge { element, bound, .. } => (element, *bound),
364        _ => (ti, 0),
365    }
366}
367
368fn array_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, alloc::vec::Vec<u32>) {
369    match ti {
370        TypeIdentifier::PlainArraySmall {
371            element,
372            array_bounds,
373            ..
374        } => (
375            element,
376            array_bounds.iter().map(|b| u32::from(*b)).collect(),
377        ),
378        TypeIdentifier::PlainArrayLarge {
379            element,
380            array_bounds,
381            ..
382        } => (element, array_bounds.clone()),
383        _ => (ti, alloc::vec::Vec::new()),
384    }
385}
386
387fn map_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, &TypeIdentifier, u32) {
388    match ti {
389        TypeIdentifier::PlainMapSmall {
390            element,
391            key,
392            bound,
393            ..
394        } => (element, key, u32::from(*bound)),
395        TypeIdentifier::PlainMapLarge {
396            element,
397            key,
398            bound,
399            ..
400        } => (element, key, *bound),
401        _ => (ti, ti, 0),
402    }
403}
404
405fn string_bound_u32_s8(ti: &TypeIdentifier) -> u32 {
406    match ti {
407        TypeIdentifier::String8Small { bound } => u32::from(*bound),
408        TypeIdentifier::String8Large { bound } => *bound,
409        _ => 0,
410    }
411}
412
413fn string_bound_u32_s16(ti: &TypeIdentifier) -> u32 {
414    match ti {
415        TypeIdentifier::String16Small { bound } => u32::from(*bound),
416        TypeIdentifier::String16Large { bound } => *bound,
417        _ => 0,
418    }
419}
420
421fn primitive_compatible(
422    w: PrimitiveKind,
423    r: PrimitiveKind,
424    cfg: &AssignabilityConfig,
425) -> Assignable {
426    if w == r {
427        return Assignable::Yes;
428    }
429    if !cfg.allow_type_coercion {
430        return Assignable::No("primitive kinds differ (no coercion allowed)");
431    }
432    // Coercion matrix for numerics — widening OK, narrowing no.
433    // Signed widening (Int8/16/32 → Int64), unsigned widening (UInt8/16/32
434    // → UInt64 + also into signed-wider types, since all values fit
435    // losslessly), float widening (Float32 → Float64).
436    use PrimitiveKind::*;
437    let ok = matches!(
438        (w, r),
439        (Int8 | UInt8 | Byte, Int16 | Int32 | Int64)
440            | (Int16 | UInt16, Int32 | Int64)
441            | (Int32 | UInt32, Int64)
442            | (UInt8 | Byte, UInt16 | UInt32 | UInt64)
443            | (UInt16, UInt32 | UInt64)
444            | (UInt32, UInt64)
445            | (Float32, Float64)
446    );
447    if ok {
448        Assignable::Yes
449    } else {
450        Assignable::No("primitive coercion not widening-safe")
451    }
452}
453
454/// Three-valued extensibility from the flag bits. Default (no flags) =
455/// Appendable, per XTypes §7.2.2.4.
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457enum StructExt {
458    Final,
459    Appendable,
460    Mutable,
461}
462
463fn struct_extensibility(flags: StructTypeFlag) -> StructExt {
464    if flags.has(StructTypeFlag::IS_FINAL) {
465        StructExt::Final
466    } else if flags.has(StructTypeFlag::IS_MUTABLE) {
467        StructExt::Mutable
468    } else {
469        StructExt::Appendable
470    }
471}
472
473fn check_minimal_types(
474    w: &MinimalTypeObject,
475    r: &MinimalTypeObject,
476    registry: &TypeRegistry,
477    cfg: &AssignabilityConfig,
478) -> Assignable {
479    match (w, r) {
480        (MinimalTypeObject::Struct(ws), MinimalTypeObject::Struct(rs)) => {
481            // Extensibility check: writer + reader must have
482            // the same extensibility category (§7.2.4.4).
483            // Bugfix (#10): consolidate the flag bits into a three-valued
484            // enum comparison to correctly capture the corner case "both have
485            // no flag bits set" = Appendable == Appendable.
486            let w_ext = struct_extensibility(ws.struct_flags);
487            let r_ext = struct_extensibility(rs.struct_flags);
488            if w_ext != r_ext {
489                return Assignable::No("extensibility mismatch");
490            }
491            let w_final = matches!(w_ext, StructExt::Final);
492            let w_mut = matches!(w_ext, StructExt::Mutable);
493
494            if w_final {
495                // Strictly equal (same count + same types in order).
496                if ws.member_seq.len() != rs.member_seq.len() {
497                    return Assignable::No("final struct member count mismatch");
498                }
499                for (wm, rm) in ws.member_seq.iter().zip(rs.member_seq.iter()) {
500                    if !cfg.ignore_member_names && wm.detail != rm.detail {
501                        return Assignable::No("final struct member name-hash differs");
502                    }
503                    match is_assignable(
504                        &wm.common.member_type_id,
505                        &rm.common.member_type_id,
506                        registry,
507                        cfg,
508                    ) {
509                        Assignable::Yes => {}
510                        e => return e,
511                    }
512                }
513                Assignable::Yes
514            } else if w_mut {
515                // Mutable: match per @id (member_id). A reader member with
516                // @id=X must exist in the writer and be compatible,
517                // if not optional. With `ignore_member_names=false`
518                // the NameHash must also match (§7.6.3.7.2.2).
519                for rm in &rs.member_seq {
520                    let rm_optional = rm
521                        .common
522                        .member_flags
523                        .has(crate::type_object::flags::StructMemberFlag::IS_OPTIONAL);
524                    match ws
525                        .member_seq
526                        .iter()
527                        .find(|wm| wm.common.member_id == rm.common.member_id)
528                    {
529                        Some(wm) => {
530                            if !cfg.ignore_member_names && wm.detail != rm.detail {
531                                return Assignable::No(
532                                    "mutable: member name-hash differs despite id match",
533                                );
534                            }
535                            match is_assignable(
536                                &wm.common.member_type_id,
537                                &rm.common.member_type_id,
538                                registry,
539                                cfg,
540                            ) {
541                                Assignable::Yes => {}
542                                e => return e,
543                            }
544                        }
545                        None if rm_optional => {}
546                        None => return Assignable::No("mutable: reader member missing in writer"),
547                    }
548                }
549                Assignable::Yes
550            } else {
551                // Appendable (default): the writer must have at least all
552                // reader fields as a prefix; extra writer fields OK.
553                if ws.member_seq.len() < rs.member_seq.len() {
554                    return Assignable::No("appendable: writer has fewer members than reader");
555                }
556                for (wm, rm) in ws.member_seq.iter().zip(rs.member_seq.iter()) {
557                    if !cfg.ignore_member_names && wm.detail != rm.detail {
558                        return Assignable::No("appendable: member name-hash differs");
559                    }
560                    match is_assignable(
561                        &wm.common.member_type_id,
562                        &rm.common.member_type_id,
563                        registry,
564                        cfg,
565                    ) {
566                        Assignable::Yes => {}
567                        e => return e,
568                    }
569                }
570                Assignable::Yes
571            }
572        }
573        (MinimalTypeObject::Enumerated(we), MinimalTypeObject::Enumerated(re)) => {
574            // §7.2.4.4.4.3: all writer values must
575            // be contained in the reader set (writer ⊆ reader). The reader may have additional
576            // literals — it consumes less than it recognizes.
577            // The bit bound must be identical (wire width).
578            if we.header.common.bit_bound != re.header.common.bit_bound {
579                return Assignable::No("enum bit_bound mismatch");
580            }
581            // §7.2.4.4.7 — by default compares (value, name_hash); with
582            // `@ignore_literal_names` on one side or via config only (value).
583            let ignore_names = cfg.ignore_literal_names
584                || we
585                    .enum_flags
586                    .has(crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES)
587                || re
588                    .enum_flags
589                    .has(crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES);
590            for wl in &we.literal_seq {
591                let found = re.literal_seq.iter().any(|rl| {
592                    rl.common.value == wl.common.value && (ignore_names || rl.detail == wl.detail)
593                });
594                if !found {
595                    return Assignable::No("enum writer literal unknown in reader");
596                }
597            }
598            Assignable::Yes
599        }
600        _ => Assignable::No("type kinds do not match"),
601    }
602}
603
604#[cfg(test)]
605#[allow(clippy::unwrap_used)]
606mod tests {
607    use super::*;
608    use crate::builder::{Extensibility, TypeObjectBuilder};
609    use crate::hash::compute_minimal_hash;
610    use crate::type_object::TypeObject;
611
612    #[test]
613    fn primitive_same_kind_is_assignable() {
614        let reg = TypeRegistry::new();
615        let a = is_assignable(
616            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
617            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
618            &reg,
619            &AssignabilityConfig::default(),
620        );
621        assert!(a.is_yes());
622    }
623
624    #[test]
625    fn primitive_different_kind_is_not_assignable_by_default() {
626        let reg = TypeRegistry::new();
627        let a = is_assignable(
628            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
629            &TypeIdentifier::Primitive(PrimitiveKind::Int64),
630            &reg,
631            &AssignabilityConfig::default(),
632        );
633        assert!(!a.is_yes());
634    }
635
636    #[test]
637    fn primitive_widening_with_coercion_is_assignable() {
638        let reg = TypeRegistry::new();
639        let cfg = AssignabilityConfig {
640            allow_type_coercion: true,
641            ..Default::default()
642        };
643        assert!(
644            is_assignable(
645                &TypeIdentifier::Primitive(PrimitiveKind::Int32),
646                &TypeIdentifier::Primitive(PrimitiveKind::Int64),
647                &reg,
648                &cfg,
649            )
650            .is_yes()
651        );
652        // Narrowing remains forbidden
653        assert!(
654            !is_assignable(
655                &TypeIdentifier::Primitive(PrimitiveKind::Int64),
656                &TypeIdentifier::Primitive(PrimitiveKind::Int32),
657                &reg,
658                &cfg,
659            )
660            .is_yes()
661        );
662    }
663
664    #[test]
665    fn appendable_struct_with_extra_writer_field_is_assignable() {
666        let mut reg = TypeRegistry::new();
667        let writer = MinimalTypeObject::Struct(
668            TypeObjectBuilder::struct_type("::X")
669                .extensibility(Extensibility::Appendable)
670                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
671                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
672                .build_minimal(),
673        );
674        let reader = MinimalTypeObject::Struct(
675            TypeObjectBuilder::struct_type("::X")
676                .extensibility(Extensibility::Appendable)
677                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
678                .build_minimal(),
679        );
680        let wh = compute_minimal_hash(&writer).unwrap();
681        let rh = compute_minimal_hash(&reader).unwrap();
682        reg.insert_minimal(wh, writer.clone());
683        reg.insert_minimal(rh, reader);
684
685        assert!(
686            is_assignable(
687                &TypeIdentifier::EquivalenceHashMinimal(wh),
688                &TypeIdentifier::EquivalenceHashMinimal(rh),
689                &reg,
690                &AssignabilityConfig::default(),
691            )
692            .is_yes()
693        );
694    }
695
696    #[test]
697    fn final_struct_with_extra_writer_field_is_not_assignable() {
698        let mut reg = TypeRegistry::new();
699        let writer = MinimalTypeObject::Struct(
700            TypeObjectBuilder::struct_type("::X")
701                .extensibility(Extensibility::Final)
702                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
703                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
704                .build_minimal(),
705        );
706        let reader = MinimalTypeObject::Struct(
707            TypeObjectBuilder::struct_type("::X")
708                .extensibility(Extensibility::Final)
709                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
710                .build_minimal(),
711        );
712        let wh = compute_minimal_hash(&writer).unwrap();
713        let rh = compute_minimal_hash(&reader).unwrap();
714        reg.insert_minimal(wh, writer);
715        reg.insert_minimal(rh, reader);
716
717        assert!(
718            !is_assignable(
719                &TypeIdentifier::EquivalenceHashMinimal(wh),
720                &TypeIdentifier::EquivalenceHashMinimal(rh),
721                &reg,
722                &AssignabilityConfig::default(),
723            )
724            .is_yes()
725        );
726    }
727
728    #[test]
729    fn mutable_struct_member_id_matching() {
730        let mut reg = TypeRegistry::new();
731        // Reader has @id(1) and @id(2) — writer has @id(2) and @id(3).
732        // Reader @id(1) is missing in the writer, but we mark it as optional.
733        let writer = MinimalTypeObject::Struct(
734            TypeObjectBuilder::struct_type("::X")
735                .extensibility(Extensibility::Mutable)
736                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
737                    m.id(2)
738                })
739                .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
740                    m.id(3)
741                })
742                .build_minimal(),
743        );
744        let reader = MinimalTypeObject::Struct(
745            TypeObjectBuilder::struct_type("::X")
746                .extensibility(Extensibility::Mutable)
747                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
748                    m.id(1).optional()
749                })
750                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
751                    m.id(2)
752                })
753                .build_minimal(),
754        );
755        let wh = compute_minimal_hash(&writer).unwrap();
756        let rh = compute_minimal_hash(&reader).unwrap();
757        reg.insert_minimal(wh, writer);
758        reg.insert_minimal(rh, reader);
759
760        assert!(
761            is_assignable(
762                &TypeIdentifier::EquivalenceHashMinimal(wh),
763                &TypeIdentifier::EquivalenceHashMinimal(rh),
764                &reg,
765                &AssignabilityConfig::default(),
766            )
767            .is_yes()
768        );
769    }
770
771    #[test]
772    fn extensibility_mismatch_fails() {
773        let mut reg = TypeRegistry::new();
774        let writer = MinimalTypeObject::Struct(
775            TypeObjectBuilder::struct_type("::X")
776                .extensibility(Extensibility::Final)
777                .build_minimal(),
778        );
779        let reader = MinimalTypeObject::Struct(
780            TypeObjectBuilder::struct_type("::X")
781                .extensibility(Extensibility::Mutable)
782                .build_minimal(),
783        );
784        let wh = compute_minimal_hash(&writer).unwrap();
785        let rh = compute_minimal_hash(&reader).unwrap();
786        reg.insert_minimal(wh, writer);
787        reg.insert_minimal(rh, reader);
788
789        let a = is_assignable(
790            &TypeIdentifier::EquivalenceHashMinimal(wh),
791            &TypeIdentifier::EquivalenceHashMinimal(rh),
792            &reg,
793            &AssignabilityConfig::default(),
794        );
795        assert!(!a.is_yes());
796    }
797
798    #[test]
799    fn string_small_and_large_interchangeable() {
800        let reg = TypeRegistry::new();
801        assert!(
802            is_assignable(
803                &TypeIdentifier::String8Small { bound: 64 },
804                &TypeIdentifier::String8Large { bound: 100_000 },
805                &reg,
806                &AssignabilityConfig::default(),
807            )
808            .is_yes()
809        );
810    }
811
812    // Silence unused import when only some Variants are matched.
813    #[allow(dead_code)]
814    fn _unused() -> TypeObject {
815        TypeObject::Minimal(MinimalTypeObject::Struct(
816            TypeObjectBuilder::struct_type("::dummy").build_minimal(),
817        ))
818    }
819
820    // ---- Additional coverage for check_direct arms -----------------------
821
822    use crate::type_identifier::PlainCollectionHeader;
823    use alloc::boxed::Box;
824
825    fn reg() -> TypeRegistry {
826        TypeRegistry::new()
827    }
828
829    #[test]
830    fn string8_vs_string16_not_assignable() {
831        let a = is_assignable(
832            &TypeIdentifier::String8Small { bound: 16 },
833            &TypeIdentifier::String16Small { bound: 16 },
834            &reg(),
835            &AssignabilityConfig::default(),
836        );
837        assert!(!a.is_yes());
838        assert!(matches!(a, Assignable::No(msg) if msg.contains("kinds")));
839    }
840
841    #[test]
842    fn string16_small_and_large_interchangeable() {
843        let a = is_assignable(
844            &TypeIdentifier::String16Small { bound: 32 },
845            &TypeIdentifier::String16Large { bound: 10_000 },
846            &reg(),
847            &AssignabilityConfig::default(),
848        );
849        assert!(a.is_yes());
850    }
851
852    #[test]
853    fn identical_type_identifiers_short_circuit_yes() {
854        // Exact equality path (`w == r`) in check_direct.
855        let ti = TypeIdentifier::Primitive(PrimitiveKind::UInt32);
856        let a = is_assignable(&ti, &ti, &reg(), &AssignabilityConfig::default());
857        assert!(a.is_yes());
858    }
859
860    #[test]
861    fn sequence_writer_bound_exceeds_reader_bound_is_no() {
862        let w = TypeIdentifier::PlainSequenceSmall {
863            header: PlainCollectionHeader::default(),
864            bound: 20,
865            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
866        };
867        let r = TypeIdentifier::PlainSequenceSmall {
868            header: PlainCollectionHeader::default(),
869            bound: 10,
870            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
871        };
872        // `ignore_sequence_bounds=false` forces the bound check.
873        let cfg = AssignabilityConfig {
874            ignore_sequence_bounds: false,
875            ..Default::default()
876        };
877        let a = is_assignable(&w, &r, &reg(), &cfg);
878        assert!(!a.is_yes());
879        assert!(matches!(a, Assignable::No(msg) if msg.contains("bound")));
880    }
881
882    /// With `ignore_sequence_bounds=true` (TCE default) we accept
883    /// the sequence even though the writer bound > reader bound.
884    #[test]
885    fn sequence_bounds_ignored_when_policy_allows() {
886        let w = TypeIdentifier::PlainSequenceSmall {
887            header: PlainCollectionHeader::default(),
888            bound: 20,
889            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
890        };
891        let r = TypeIdentifier::PlainSequenceSmall {
892            header: PlainCollectionHeader::default(),
893            bound: 10,
894            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
895        };
896        assert!(is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
897    }
898
899    #[test]
900    fn sequence_reader_unbounded_accepts_any_writer_bound() {
901        // reader bound=0 (unbounded) → writer bound check skipped.
902        let w = TypeIdentifier::PlainSequenceSmall {
903            header: PlainCollectionHeader::default(),
904            bound: 200,
905            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int16)),
906        };
907        let r = TypeIdentifier::PlainSequenceSmall {
908            header: PlainCollectionHeader::default(),
909            bound: 0,
910            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int16)),
911        };
912        assert!(is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
913    }
914
915    #[test]
916    fn sequence_elements_must_be_assignable() {
917        // Same bound, but different incompatible element kinds.
918        let w = TypeIdentifier::PlainSequenceSmall {
919            header: PlainCollectionHeader::default(),
920            bound: 5,
921            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
922        };
923        let r = TypeIdentifier::PlainSequenceSmall {
924            header: PlainCollectionHeader::default(),
925            bound: 5,
926            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Float64)),
927        };
928        assert!(!is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
929    }
930
931    #[test]
932    fn nested_sequence_of_sequence_assignable_when_elements_match() {
933        let inner = TypeIdentifier::PlainSequenceSmall {
934            header: PlainCollectionHeader::default(),
935            bound: 5,
936            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
937        };
938        let outer = TypeIdentifier::PlainSequenceSmall {
939            header: PlainCollectionHeader::default(),
940            bound: 3,
941            element: Box::new(inner.clone()),
942        };
943        // outer vs outer with inner bound differing: inner writer=5, reader=10 → OK
944        let inner_wider = TypeIdentifier::PlainSequenceSmall {
945            header: PlainCollectionHeader::default(),
946            bound: 10,
947            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
948        };
949        let outer2 = TypeIdentifier::PlainSequenceSmall {
950            header: PlainCollectionHeader::default(),
951            bound: 3,
952            element: Box::new(inner_wider),
953        };
954        assert!(is_assignable(&outer, &outer2, &reg(), &AssignabilityConfig::default()).is_yes());
955    }
956
957    #[test]
958    fn plain_array_identical_assigns_yes() {
959        let a = is_assignable(
960            &TypeIdentifier::PlainArraySmall {
961                header: PlainCollectionHeader::default(),
962                array_bounds: alloc::vec![3, 4],
963                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
964            },
965            &TypeIdentifier::PlainArraySmall {
966                header: PlainCollectionHeader::default(),
967                array_bounds: alloc::vec![3, 4],
968                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
969            },
970            &reg(),
971            &AssignabilityConfig::default(),
972        );
973        assert!(a.is_yes());
974    }
975
976    #[test]
977    fn plain_array_diff_bounds_is_no() {
978        let b = is_assignable(
979            &TypeIdentifier::PlainArraySmall {
980                header: PlainCollectionHeader::default(),
981                array_bounds: alloc::vec![3, 4],
982                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
983            },
984            &TypeIdentifier::PlainArraySmall {
985                header: PlainCollectionHeader::default(),
986                array_bounds: alloc::vec![3, 5],
987                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
988            },
989            &reg(),
990            &AssignabilityConfig::default(),
991        );
992        assert!(!b.is_yes());
993        assert!(matches!(b, Assignable::No(msg) if msg.contains("array bounds")));
994    }
995
996    #[test]
997    fn equivalence_hash_identical_short_circuits_to_yes() {
998        // For identical hashes, `check_direct` takes the early exit
999        // `wh == rh → Yes` (see the match arm). The only precondition is
1000        // that `resolve_alias_chain` succeeds — for that the
1001        // hash must point to a non-alias in the registry.
1002        let mut reg = reg();
1003        let to = MinimalTypeObject::Struct(
1004            TypeObjectBuilder::struct_type("::T")
1005                .extensibility(Extensibility::Appendable)
1006                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1007                .build_minimal(),
1008        );
1009        let h = compute_minimal_hash(&to).unwrap();
1010        reg.insert_minimal(h, to);
1011        let a = is_assignable(
1012            &TypeIdentifier::EquivalenceHashMinimal(h),
1013            &TypeIdentifier::EquivalenceHashMinimal(h),
1014            &reg,
1015            &AssignabilityConfig::default(),
1016        );
1017        assert!(a.is_yes());
1018    }
1019
1020    #[test]
1021    fn equivalence_hash_unresolved_writer_is_no() {
1022        // Alias resolution fails because the hash is not in the
1023        // registry → early exit with `No("writer alias resolution failed")`.
1024        let reg = reg();
1025        let wh = crate::type_identifier::EquivalenceHash([0x01; 14]);
1026        let rh = crate::type_identifier::EquivalenceHash([0x02; 14]);
1027        let a = is_assignable(
1028            &TypeIdentifier::EquivalenceHashMinimal(wh),
1029            &TypeIdentifier::EquivalenceHashMinimal(rh),
1030            &reg,
1031            &AssignabilityConfig::default(),
1032        );
1033        assert!(!a.is_yes());
1034        assert!(matches!(a, Assignable::No(msg) if msg.contains("alias resolution")));
1035    }
1036
1037    #[test]
1038    fn equivalence_hash_unresolved_reader_is_no() {
1039        // Writer is registered, reader is not → error on the reader resolve.
1040        let mut reg = reg();
1041        let to = MinimalTypeObject::Struct(
1042            TypeObjectBuilder::struct_type("::T")
1043                .extensibility(Extensibility::Appendable)
1044                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1045                .build_minimal(),
1046        );
1047        let wh = compute_minimal_hash(&to).unwrap();
1048        reg.insert_minimal(wh, to);
1049        let rh = crate::type_identifier::EquivalenceHash([0x02; 14]);
1050        let a = is_assignable(
1051            &TypeIdentifier::EquivalenceHashMinimal(wh),
1052            &TypeIdentifier::EquivalenceHashMinimal(rh),
1053            &reg,
1054            &AssignabilityConfig::default(),
1055        );
1056        assert!(!a.is_yes());
1057        assert!(matches!(a, Assignable::No(msg) if msg.contains("reader")));
1058    }
1059
1060    #[test]
1061    fn mixed_kinds_report_kinds_do_not_match() {
1062        // Primitive vs string → no arm matched → `No("kinds do not match")`.
1063        let a = is_assignable(
1064            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
1065            &TypeIdentifier::String8Small { bound: 10 },
1066            &reg(),
1067            &AssignabilityConfig::default(),
1068        );
1069        assert!(!a.is_yes());
1070    }
1071
1072    #[test]
1073    fn enum_mismatch_writer_literal_unknown_in_reader_is_no() {
1074        let mut reg = reg();
1075        let w = MinimalTypeObject::Enumerated(
1076            TypeObjectBuilder::enum_type("::E")
1077                .bit_bound(32)
1078                .literal("A", 1)
1079                .literal("B", 2)
1080                .build_minimal(),
1081        );
1082        let r = MinimalTypeObject::Enumerated(
1083            TypeObjectBuilder::enum_type("::E")
1084                .bit_bound(32)
1085                .literal("A", 1)
1086                .build_minimal(),
1087        );
1088        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1089        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1090        reg.insert_minimal(wh, w);
1091        reg.insert_minimal(rh, r);
1092
1093        let a = is_assignable(
1094            &TypeIdentifier::EquivalenceHashMinimal(wh),
1095            &TypeIdentifier::EquivalenceHashMinimal(rh),
1096            &reg,
1097            &AssignabilityConfig::default(),
1098        );
1099        assert!(!a.is_yes());
1100    }
1101
1102    #[test]
1103    fn enum_bit_bound_mismatch_is_no() {
1104        let mut reg = reg();
1105        let w = MinimalTypeObject::Enumerated(
1106            TypeObjectBuilder::enum_type("::E")
1107                .bit_bound(32)
1108                .literal("A", 1)
1109                .build_minimal(),
1110        );
1111        let r = MinimalTypeObject::Enumerated(
1112            TypeObjectBuilder::enum_type("::E")
1113                .bit_bound(16)
1114                .literal("A", 1)
1115                .build_minimal(),
1116        );
1117        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1118        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1119        reg.insert_minimal(wh, w);
1120        reg.insert_minimal(rh, r);
1121
1122        let a = is_assignable(
1123            &TypeIdentifier::EquivalenceHashMinimal(wh),
1124            &TypeIdentifier::EquivalenceHashMinimal(rh),
1125            &reg,
1126            &AssignabilityConfig::default(),
1127        );
1128        assert!(!a.is_yes());
1129    }
1130
1131    #[test]
1132    fn enum_identical_labels_is_yes() {
1133        let mut reg = reg();
1134        let w = MinimalTypeObject::Enumerated(
1135            TypeObjectBuilder::enum_type("::E")
1136                .bit_bound(32)
1137                .literal("A", 1)
1138                .literal("B", 2)
1139                .build_minimal(),
1140        );
1141        let r = MinimalTypeObject::Enumerated(
1142            TypeObjectBuilder::enum_type("::E")
1143                .bit_bound(32)
1144                .literal("A", 1)
1145                .literal("B", 2)
1146                .literal("C", 3) // reader knows an extra label, writer labels are a subset
1147                .build_minimal(),
1148        );
1149        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1150        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1151        reg.insert_minimal(wh, w);
1152        reg.insert_minimal(rh, r);
1153
1154        let a = is_assignable(
1155            &TypeIdentifier::EquivalenceHashMinimal(wh),
1156            &TypeIdentifier::EquivalenceHashMinimal(rh),
1157            &reg,
1158            &AssignabilityConfig::default(),
1159        );
1160        assert!(a.is_yes());
1161    }
1162
1163    // ---- §7.2.2.4.5 Inheritance flatten + edge-cases ----
1164
1165    #[test]
1166    fn flatten_inheritance_no_base_returns_struct_unchanged() {
1167        let s = TypeObjectBuilder::struct_type("::S")
1168            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1169            .build_minimal();
1170        let reg = reg();
1171        let flat = flatten_inheritance(&s, &reg, 8).unwrap();
1172        assert_eq!(flat.member_seq.len(), 1);
1173    }
1174
1175    #[test]
1176    fn flatten_inheritance_two_levels_concatenates_base_first() {
1177        // Root → Mid → Derived. Result member order: [Root, Mid, Derived].
1178        let mut reg = reg();
1179        let root = TypeObjectBuilder::struct_type("::Root")
1180            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int8), |m| {
1181                m.id(101)
1182            })
1183            .build_minimal();
1184        let root_h = compute_minimal_hash(&MinimalTypeObject::Struct(root.clone())).unwrap();
1185        reg.insert_minimal(root_h, MinimalTypeObject::Struct(root));
1186
1187        let mid = TypeObjectBuilder::struct_type("::Mid")
1188            .base(TypeIdentifier::EquivalenceHashMinimal(root_h))
1189            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int16), |m| {
1190                m.id(202)
1191            })
1192            .build_minimal();
1193        let mid_h = compute_minimal_hash(&MinimalTypeObject::Struct(mid.clone())).unwrap();
1194        reg.insert_minimal(mid_h, MinimalTypeObject::Struct(mid));
1195
1196        let derived = TypeObjectBuilder::struct_type("::Derived")
1197            .base(TypeIdentifier::EquivalenceHashMinimal(mid_h))
1198            .member("d", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1199                m.id(303)
1200            })
1201            .build_minimal();
1202
1203        let flat = flatten_inheritance(&derived, &reg, 8).unwrap();
1204        // 3 members, base_type gone.
1205        assert_eq!(flat.header.base_type, TypeIdentifier::None);
1206        assert_eq!(flat.member_seq.len(), 3);
1207        // First member is 'r' (root); last is 'd' (derived).
1208        let first_id = flat.member_seq[0].common.member_id;
1209        let last_id = flat.member_seq[2].common.member_id;
1210        assert_ne!(first_id, last_id);
1211    }
1212
1213    #[test]
1214    fn inheritance_conflict_same_id() {
1215        let mut reg = reg();
1216        let base = TypeObjectBuilder::struct_type("::B")
1217            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1218                m.id(7)
1219            })
1220            .build_minimal();
1221        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1222        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1223
1224        let derived = TypeObjectBuilder::struct_type("::D")
1225            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1226            .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1227                m.id(7) // same ID as base member 'a' → conflict
1228            })
1229            .build_minimal();
1230        let err = flatten_inheritance(&derived, &reg, 8).unwrap_err();
1231        assert!(matches!(
1232            err,
1233            InheritanceError::InheritanceConflict { reason, .. }
1234                if reason.contains("member_id")
1235        ));
1236    }
1237
1238    #[test]
1239    fn inheritance_conflict_same_name() {
1240        let mut reg = reg();
1241        let base = TypeObjectBuilder::struct_type("::B")
1242            .member(
1243                "dup",
1244                TypeIdentifier::Primitive(PrimitiveKind::Int32),
1245                |m| m.id(1),
1246            )
1247            .build_minimal();
1248        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1249        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1250
1251        let derived = TypeObjectBuilder::struct_type("::D")
1252            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1253            .member(
1254                "dup",
1255                TypeIdentifier::Primitive(PrimitiveKind::Int32),
1256                |m| {
1257                    m.id(2) // different ID, but same name → conflict
1258                },
1259            )
1260            .build_minimal();
1261        let err = flatten_inheritance(&derived, &reg, 8).unwrap_err();
1262        assert!(matches!(
1263            err,
1264            InheritanceError::InheritanceConflict { reason, .. }
1265                if reason.contains("name_hash")
1266        ));
1267    }
1268
1269    #[test]
1270    fn flat_type_construction_two_levels() {
1271        // §7.2.2.4.5 — the hypothetical flat representation of a
1272        // 2-level inheritance construct has members in base-derived
1273        // order.
1274        let mut reg = reg();
1275        let base = TypeObjectBuilder::struct_type("::Base")
1276            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1277                m.id(1)
1278            })
1279            .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1280                m.id(2)
1281            })
1282            .build_minimal();
1283        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1284        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1285
1286        let derived = TypeObjectBuilder::struct_type("::Derived")
1287            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1288            .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1289                m.id(3)
1290            })
1291            .build_minimal();
1292
1293        let flat = flatten_inheritance(&derived, &reg, 8).unwrap();
1294        assert_eq!(flat.member_seq.len(), 3);
1295        assert_eq!(flat.member_seq[0].common.member_id, 1);
1296        assert_eq!(flat.member_seq[1].common.member_id, 2);
1297        assert_eq!(flat.member_seq[2].common.member_id, 3);
1298    }
1299
1300    #[test]
1301    fn two_level_inheritance_assignability_chain() {
1302        // Writer and reader each have 2-level inheritance, both
1303        // produce the same flat member sequence → assignable.
1304        let mut reg = reg();
1305
1306        // ---- Writer side ----
1307        let w_root = TypeObjectBuilder::struct_type("::WRoot")
1308            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1309                m.id(1)
1310            })
1311            .build_minimal();
1312        let wr_h = compute_minimal_hash(&MinimalTypeObject::Struct(w_root.clone())).unwrap();
1313        reg.insert_minimal(wr_h, MinimalTypeObject::Struct(w_root));
1314
1315        let w_mid = TypeObjectBuilder::struct_type("::WMid")
1316            .base(TypeIdentifier::EquivalenceHashMinimal(wr_h))
1317            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1318                m.id(2)
1319            })
1320            .build_minimal();
1321
1322        let w_flat = flatten_inheritance(&w_mid, &reg, 8).unwrap();
1323        assert_eq!(w_flat.member_seq.len(), 2);
1324
1325        // ---- Reader side (same structure, different type names) ----
1326        let r_root = TypeObjectBuilder::struct_type("::RRoot")
1327            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1328                m.id(1)
1329            })
1330            .build_minimal();
1331        let rr_h = compute_minimal_hash(&MinimalTypeObject::Struct(r_root.clone())).unwrap();
1332        reg.insert_minimal(rr_h, MinimalTypeObject::Struct(r_root));
1333
1334        let r_mid = TypeObjectBuilder::struct_type("::RMid")
1335            .base(TypeIdentifier::EquivalenceHashMinimal(rr_h))
1336            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1337                m.id(2)
1338            })
1339            .build_minimal();
1340
1341        let r_flat = flatten_inheritance(&r_mid, &reg, 8).unwrap();
1342        assert_eq!(r_flat.member_seq.len(), 2);
1343
1344        // Direct member-by-member compare via assignability.
1345        let w_to = MinimalTypeObject::Struct(w_flat.clone());
1346        let r_to = MinimalTypeObject::Struct(r_flat.clone());
1347        let wh = compute_minimal_hash(&w_to).unwrap();
1348        let rh = compute_minimal_hash(&r_to).unwrap();
1349        reg.insert_minimal(wh, w_to);
1350        reg.insert_minimal(rh, r_to);
1351        let a = is_assignable(
1352            &TypeIdentifier::EquivalenceHashMinimal(wh),
1353            &TypeIdentifier::EquivalenceHashMinimal(rh),
1354            &reg,
1355            &AssignabilityConfig::default(),
1356        );
1357        assert!(a.is_yes(), "got {a:?}");
1358    }
1359
1360    #[test]
1361    fn enum_not_assignable_strict_default() {
1362        // Same value, but different names → the strict default
1363        // (no `@ignore_literal_names` annotation, no config flag)
1364        // must return assignable=No.
1365        let mut reg = reg();
1366        let w = MinimalTypeObject::Enumerated(
1367            TypeObjectBuilder::enum_type("::E")
1368                .bit_bound(32)
1369                .literal("RED", 1)
1370                .build_minimal(),
1371        );
1372        let r = MinimalTypeObject::Enumerated(
1373            TypeObjectBuilder::enum_type("::E")
1374                .bit_bound(32)
1375                .literal("ROUGE", 1) // same value, different name
1376                .build_minimal(),
1377        );
1378        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1379        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1380        reg.insert_minimal(wh, w);
1381        reg.insert_minimal(rh, r);
1382
1383        let a = is_assignable(
1384            &TypeIdentifier::EquivalenceHashMinimal(wh),
1385            &TypeIdentifier::EquivalenceHashMinimal(rh),
1386            &reg,
1387            &AssignabilityConfig::default(),
1388        );
1389        assert!(!a.is_yes());
1390    }
1391
1392    #[test]
1393    fn enum_assignable_with_ignore_literal_names() {
1394        // Same value, different names — but config or a flag sets
1395        // ignore_literal_names; the result must be Yes.
1396        let mut reg = reg();
1397        let w = MinimalTypeObject::Enumerated(
1398            TypeObjectBuilder::enum_type("::E")
1399                .bit_bound(32)
1400                .literal("RED", 1)
1401                .literal("GREEN", 2)
1402                .build_minimal(),
1403        );
1404        let r = MinimalTypeObject::Enumerated(
1405            TypeObjectBuilder::enum_type("::E")
1406                .bit_bound(32)
1407                .literal("ROUGE", 1)
1408                .literal("VERT", 2)
1409                .build_minimal(),
1410        );
1411        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1412        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1413        reg.insert_minimal(wh, w);
1414        reg.insert_minimal(rh, r);
1415
1416        let cfg = AssignabilityConfig {
1417            ignore_literal_names: true,
1418            ..AssignabilityConfig::default()
1419        };
1420        let a = is_assignable(
1421            &TypeIdentifier::EquivalenceHashMinimal(wh),
1422            &TypeIdentifier::EquivalenceHashMinimal(rh),
1423            &reg,
1424            &cfg,
1425        );
1426        assert!(a.is_yes());
1427    }
1428
1429    #[test]
1430    fn enum_assignable_with_ignore_literal_names_via_writer_flag() {
1431        // When the writer sets `EnumTypeFlag::IGNORE_LITERAL_NAMES`,
1432        // it acts for the comparison just like the config flag.
1433        let mut reg = reg();
1434        let mut w_e = TypeObjectBuilder::enum_type("::E")
1435            .bit_bound(32)
1436            .literal("RED", 1)
1437            .build_minimal();
1438        w_e.enum_flags = crate::type_object::flags::EnumTypeFlag(
1439            crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES,
1440        );
1441        let w = MinimalTypeObject::Enumerated(w_e);
1442        let r = MinimalTypeObject::Enumerated(
1443            TypeObjectBuilder::enum_type("::E")
1444                .bit_bound(32)
1445                .literal("ROUGE", 1)
1446                .build_minimal(),
1447        );
1448        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1449        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1450        reg.insert_minimal(wh, w);
1451        reg.insert_minimal(rh, r);
1452
1453        let a = is_assignable(
1454            &TypeIdentifier::EquivalenceHashMinimal(wh),
1455            &TypeIdentifier::EquivalenceHashMinimal(rh),
1456            &reg,
1457            &AssignabilityConfig::default(),
1458        );
1459        assert!(a.is_yes());
1460    }
1461
1462    #[test]
1463    fn enum_assignable_with_ignore_literal_names_via_reader_flag() {
1464        let mut reg = reg();
1465        let w = MinimalTypeObject::Enumerated(
1466            TypeObjectBuilder::enum_type("::E")
1467                .bit_bound(32)
1468                .literal("RED", 1)
1469                .build_minimal(),
1470        );
1471        let mut r_e = TypeObjectBuilder::enum_type("::E")
1472            .bit_bound(32)
1473            .literal("ROUGE", 1)
1474            .build_minimal();
1475        r_e.enum_flags = crate::type_object::flags::EnumTypeFlag(
1476            crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES,
1477        );
1478        let r = MinimalTypeObject::Enumerated(r_e);
1479        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1480        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1481        reg.insert_minimal(wh, w);
1482        reg.insert_minimal(rh, r);
1483
1484        let a = is_assignable(
1485            &TypeIdentifier::EquivalenceHashMinimal(wh),
1486            &TypeIdentifier::EquivalenceHashMinimal(rh),
1487            &reg,
1488            &AssignabilityConfig::default(),
1489        );
1490        assert!(a.is_yes());
1491    }
1492
1493    #[test]
1494    fn struct_vs_enum_type_object_kinds_dont_match() {
1495        let mut reg = reg();
1496        let w = MinimalTypeObject::Struct(
1497            TypeObjectBuilder::struct_type("::X")
1498                .extensibility(Extensibility::Appendable)
1499                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1500                .build_minimal(),
1501        );
1502        let r = MinimalTypeObject::Enumerated(
1503            TypeObjectBuilder::enum_type("::X")
1504                .bit_bound(32)
1505                .literal("A", 1)
1506                .build_minimal(),
1507        );
1508        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1509        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1510        reg.insert_minimal(wh, w);
1511        reg.insert_minimal(rh, r);
1512
1513        let a = is_assignable(
1514            &TypeIdentifier::EquivalenceHashMinimal(wh),
1515            &TypeIdentifier::EquivalenceHashMinimal(rh),
1516            &reg,
1517            &AssignabilityConfig::default(),
1518        );
1519        assert!(!a.is_yes());
1520    }
1521
1522    #[test]
1523    fn appendable_struct_writer_smaller_than_reader_is_no() {
1524        let mut reg = reg();
1525        let writer = MinimalTypeObject::Struct(
1526            TypeObjectBuilder::struct_type("::X")
1527                .extensibility(Extensibility::Appendable)
1528                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1529                .build_minimal(),
1530        );
1531        let reader = MinimalTypeObject::Struct(
1532            TypeObjectBuilder::struct_type("::X")
1533                .extensibility(Extensibility::Appendable)
1534                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1535                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1536                .build_minimal(),
1537        );
1538        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1539        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1540        reg.insert_minimal(wh, writer);
1541        reg.insert_minimal(rh, reader);
1542
1543        assert!(
1544            !is_assignable(
1545                &TypeIdentifier::EquivalenceHashMinimal(wh),
1546                &TypeIdentifier::EquivalenceHashMinimal(rh),
1547                &reg,
1548                &AssignabilityConfig::default(),
1549            )
1550            .is_yes()
1551        );
1552    }
1553
1554    #[test]
1555    fn mutable_reader_member_missing_in_writer_non_optional_is_no() {
1556        let mut reg = reg();
1557        let writer = MinimalTypeObject::Struct(
1558            TypeObjectBuilder::struct_type("::X")
1559                .extensibility(Extensibility::Mutable)
1560                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1561                    m.id(2)
1562                })
1563                .build_minimal(),
1564        );
1565        let reader = MinimalTypeObject::Struct(
1566            TypeObjectBuilder::struct_type("::X")
1567                .extensibility(Extensibility::Mutable)
1568                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1569                    m.id(1) // NOT optional
1570                })
1571                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1572                    m.id(2)
1573                })
1574                .build_minimal(),
1575        );
1576        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1577        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1578        reg.insert_minimal(wh, writer);
1579        reg.insert_minimal(rh, reader);
1580
1581        assert!(
1582            !is_assignable(
1583                &TypeIdentifier::EquivalenceHashMinimal(wh),
1584                &TypeIdentifier::EquivalenceHashMinimal(rh),
1585                &reg,
1586                &AssignabilityConfig::default(),
1587            )
1588            .is_yes()
1589        );
1590    }
1591
1592    #[test]
1593    fn final_struct_member_count_mismatch_is_no() {
1594        let mut reg = reg();
1595        let writer = MinimalTypeObject::Struct(
1596            TypeObjectBuilder::struct_type("::X")
1597                .extensibility(Extensibility::Final)
1598                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1599                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1600                .build_minimal(),
1601        );
1602        let reader = MinimalTypeObject::Struct(
1603            TypeObjectBuilder::struct_type("::X")
1604                .extensibility(Extensibility::Final)
1605                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1606                .build_minimal(),
1607        );
1608        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1609        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1610        reg.insert_minimal(wh, writer);
1611        reg.insert_minimal(rh, reader);
1612
1613        assert!(
1614            !is_assignable(
1615                &TypeIdentifier::EquivalenceHashMinimal(wh),
1616                &TypeIdentifier::EquivalenceHashMinimal(rh),
1617                &reg,
1618                &AssignabilityConfig::default(),
1619            )
1620            .is_yes()
1621        );
1622    }
1623
1624    #[test]
1625    fn primitive_widening_int16_to_int64_is_assignable_with_coercion() {
1626        let cfg = AssignabilityConfig {
1627            allow_type_coercion: true,
1628            ..Default::default()
1629        };
1630        assert!(primitive_compatible(PrimitiveKind::Int16, PrimitiveKind::Int64, &cfg).is_yes());
1631        assert!(primitive_compatible(PrimitiveKind::Byte, PrimitiveKind::Int32, &cfg).is_yes());
1632        assert!(
1633            primitive_compatible(PrimitiveKind::Float32, PrimitiveKind::Float64, &cfg).is_yes()
1634        );
1635    }
1636
1637    #[test]
1638    fn primitive_unwidening_is_rejected_even_with_coercion() {
1639        let cfg = AssignabilityConfig {
1640            allow_type_coercion: true,
1641            ..Default::default()
1642        };
1643        assert!(!primitive_compatible(PrimitiveKind::Float64, PrimitiveKind::Int32, &cfg).is_yes());
1644    }
1645
1646    #[test]
1647    fn assignable_is_yes_matches_expectation() {
1648        assert!(Assignable::Yes.is_yes());
1649        assert!(!Assignable::No("reason").is_yes());
1650    }
1651
1652    // Bug QT (#76): a typed endpoint whose generated type carries a *complete*
1653    // TypeIdentifier (EquivalenceHashComplete) whose TypeObject is absent from
1654    // the registry must still match itself. Before the fix, `is_assignable`
1655    // resolved the alias chain FIRST and returned `Unknown` for the absent
1656    // hash, BEFORE the identity short-circuit — so a writer + reader of the
1657    // SAME complete type failed to match.
1658    #[test]
1659    fn equal_complete_hash_is_assignable_with_empty_registry() {
1660        let reg = TypeRegistry::new();
1661        let h = crate::type_identifier::EquivalenceHash([0xCE; 14]);
1662        let ti = TypeIdentifier::EquivalenceHashComplete(h);
1663        assert!(
1664            is_assignable(&ti, &ti, &reg, &AssignabilityConfig::default()).is_yes(),
1665            "identical complete TypeIdentifiers must be assignable even with an empty registry"
1666        );
1667    }
1668
1669    #[test]
1670    fn equal_minimal_hash_is_assignable_with_empty_registry() {
1671        let reg = TypeRegistry::new();
1672        let h = crate::type_identifier::EquivalenceHash([0x4D; 14]);
1673        let ti = TypeIdentifier::EquivalenceHashMinimal(h);
1674        assert!(is_assignable(&ti, &ti, &reg, &AssignabilityConfig::default()).is_yes());
1675    }
1676
1677    #[test]
1678    fn differing_complete_hashes_unknown_objects_are_not_assignable() {
1679        let reg = TypeRegistry::new();
1680        let wh = crate::type_identifier::EquivalenceHash([0x01; 14]);
1681        let rh = crate::type_identifier::EquivalenceHash([0x02; 14]);
1682        assert!(
1683            !is_assignable(
1684                &TypeIdentifier::EquivalenceHashComplete(wh),
1685                &TypeIdentifier::EquivalenceHashComplete(rh),
1686                &reg,
1687                &AssignabilityConfig::default(),
1688            )
1689            .is_yes()
1690        );
1691    }
1692}