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//! Bestimmt ob ein Typ `T1` einem Typ `T2` zugewiesen werden kann —
6//! entspricht "kompatibel fuer Publication/Subscription-Match". Die
7//! Regeln haengen von Extensibility (Final/Appendable/Mutable) +
8//! TypeConsistencyEnforcement ab.
9//!
10//! Core-Regeln fuer Primitives, Strings,
11//! Collections, Aliases (via Resolver), Enums + Structs mit
12//! Final/Appendable/Mutable-Semantik. Strict-vs-lax-Variante ueber
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/// Fehler beim Flatten einer Inheritance-Kette.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum InheritanceError {
23    /// `base_type` zeigt auf eine `EquivalenceHash`, die nicht in der
24    /// Registry liegt.
25    UnknownBase {
26        /// Der ungeloeste Hash.
27        hash: crate::type_identifier::EquivalenceHash,
28    },
29    /// `base_type` zeigt auf einen TypeObject, der kein Struct ist
30    /// (z.B. Enum oder Alias auf Enum).
31    BaseNotAStruct,
32    /// Inheritance-Cycle erkannt.
33    Cycle,
34    /// Maximum-Depth ueberschritten.
35    DepthExceeded {
36        /// Limit.
37        limit: usize,
38    },
39    /// Member-Name oder Member-ID kollidiert zwischen Base und Derived
40    /// (XTypes 1.3 §7.2.2.4.5).
41    InheritanceConflict {
42        /// Bezugs-ID oder -Name.
43        member_id: u32,
44        /// Beschreibung.
45        reason: &'static str,
46    },
47}
48
49/// Baut die "hypothetical flat type"-Struktur aus einer Single-Inheritance-
50/// Kette (XTypes 1.3 §7.2.2.4.5). Resolved `base_type` rekursiv und
51/// concatenated Base- gefolgt von Derived-Membern. Bei Member-Name- oder
52/// Member-ID-Kollisionen wird `InheritanceConflict` zurueckgegeben.
53///
54/// `max_depth` limitiert die Tiefe der Inheritance-Kette (Cycle-Schutz).
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 ist [Derived, Mid1, Mid2, ..., Root].
96    // Spec §7.2.2.4.5: Konkateniere von Root zu Derived (Base-Member zuerst).
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    // Resultat: Derived's Header (incl. base_type=None nach Flatten) +
120    // konkat. Member.
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/// Konfiguration fuer Assignability-Checks.
131///
132/// Die Felder ausser `max_depth` entsprechen 1:1 den Flags der DDS-QoS
133/// `TypeConsistencyEnforcement` (XTypes §7.6.3.7). `TypeMatcher`
134/// uebersetzt eine konkrete TCE-Policy in dieses Struct.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct AssignabilityConfig {
137    /// Erlaubt Type-Coercion (int32 ↔ int64 etc.)?
138    pub allow_type_coercion: bool,
139    /// Ignoriert Sequence-Bounds (Writer darf groesser als Reader sein).
140    pub ignore_sequence_bounds: bool,
141    /// Ignoriert String-Bounds.
142    pub ignore_string_bounds: bool,
143    /// Ignoriert Member-Namen — Mutable-Structs matchen dann
144    /// nur via `@id`-Member-ID, nicht ueber NameHash.
145    pub ignore_member_names: bool,
146    /// `@ignore_literal_names` global (XTypes §7.2.4.4.7) — Enum-Compat
147    /// vergleicht nur Ordinalwerte, nicht Literal-Namen. Zusaetzlich
148    /// kann das per `EnumTypeFlag::IGNORE_LITERAL_NAMES` auf einer
149    /// einzelnen Seite gesetzt werden; die Disjunktion gewinnt.
150    pub ignore_literal_names: bool,
151    /// Maximum-Depth fuer rekursives Aufloesen.
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/// Ergebnis des Assignability-Checks.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub enum Assignable {
171    /// Kompatibel.
172    Yes,
173    /// Nicht kompatibel mit Begruendung.
174    No(&'static str),
175}
176
177impl Assignable {
178    /// `true` wenn kompatibel.
179    #[must_use]
180    pub const fn is_yes(&self) -> bool {
181        matches!(self, Self::Yes)
182    }
183}
184
185/// Prueft ob der Writer-Typ `w` fuer den Reader-Typ `r` kompatibel ist.
186///
187/// zerodds-lint: recursion-depth 64
188///
189/// **Wichtig zur Tiefen-Zahl**: die `64` ist der *Default*
190/// ([`DEFAULT_MAX_RESOLVE_DEPTH`]) und **nicht** enforced durch Code —
191/// `cfg.max_depth` ist runtime-konfigurierbar. Tests fahren Werte bis
192/// 512 (`resolve_depth_exceeded`-Tests), Produktions-Aufrufer sollten
193/// die Default-`AssignabilityConfig` nutzen, sonst den Wert explizit
194/// mit der Risikobewertung abstimmen.
195///
196/// Rekursion via `check_direct` → `is_assignable` (nested Sequences,
197/// Struct-Member).
198pub fn is_assignable(
199    w: &TypeIdentifier,
200    r: &TypeIdentifier,
201    registry: &TypeRegistry,
202    cfg: &AssignabilityConfig,
203) -> Assignable {
204    // Alias-Resolution auf beiden Seiten.
205    let Ok(w) = resolve_alias_chain(w, registry, cfg.max_depth) else {
206        return Assignable::No("writer alias resolution failed");
207    };
208    let Ok(r) = resolve_alias_chain(r, registry, cfg.max_depth) else {
209        return Assignable::No("reader alias resolution failed");
210    };
211
212    check_direct(&w, &r, registry, cfg)
213}
214
215/// zerodds-lint: recursion-depth 64
216///
217/// Dispatcht die Assignability-Checks pro TypeIdentifier-Variante.
218/// Nested-Types (Sequence, Struct-Member) rufen `is_assignable` rekursiv
219/// — Depth-Cap via `cfg.max_depth`.
220fn check_direct(
221    w: &TypeIdentifier,
222    r: &TypeIdentifier,
223    registry: &TypeRegistry,
224    cfg: &AssignabilityConfig,
225) -> Assignable {
226    // Exakte Identitaet → immer Yes.
227    if w == r {
228        return Assignable::Yes;
229    }
230
231    match (w, r) {
232        // Primitive → Primitive
233        (TypeIdentifier::Primitive(wp), TypeIdentifier::Primitive(rp)) => {
234            primitive_compatible(*wp, *rp, cfg)
235        }
236        // String-Kompatibilitaet. Small/Large ist nur Encoding-Detail;
237        // Bounds werden per `ignore_string_bounds` gefiltert.
238        (
239            TypeIdentifier::String8Small { .. } | TypeIdentifier::String8Large { .. },
240            TypeIdentifier::String8Small { .. } | TypeIdentifier::String8Large { .. },
241        ) => {
242            let (wb, rb) = (string_bound_u32_s8(w), string_bound_u32_s8(r));
243            if !cfg.ignore_string_bounds && rb != 0 && wb > rb {
244                Assignable::No("writer string8 bound exceeds reader bound")
245            } else {
246                Assignable::Yes
247            }
248        }
249        (
250            TypeIdentifier::String16Small { .. } | TypeIdentifier::String16Large { .. },
251            TypeIdentifier::String16Small { .. } | TypeIdentifier::String16Large { .. },
252        ) => {
253            let (wb, rb) = (string_bound_u32_s16(w), string_bound_u32_s16(r));
254            if !cfg.ignore_string_bounds && rb != 0 && wb > rb {
255                Assignable::No("writer string16 bound exceeds reader bound")
256            } else {
257                Assignable::Yes
258            }
259        }
260
261        // Sequence-Kompatibilitaet: Small ↔ Small/Large, Large ↔ Large/Small.
262        // Small vs Large ist nur Wire-Encoding; Bounds werden auf u32
263        // normalisiert und per Policy gefiltert.
264        (
265            TypeIdentifier::PlainSequenceSmall { .. } | TypeIdentifier::PlainSequenceLarge { .. },
266            TypeIdentifier::PlainSequenceSmall { .. } | TypeIdentifier::PlainSequenceLarge { .. },
267        ) => {
268            let (we, wb) = sequence_parts(w);
269            let (re, rb) = sequence_parts(r);
270            if !cfg.ignore_sequence_bounds && rb != 0 && wb > rb {
271                return Assignable::No("writer sequence bound exceeds reader bound");
272            }
273            is_assignable(we, re, registry, cfg)
274        }
275
276        // Array: feste Dimensionen. `bound_seq`-Vergleich ist strukturell;
277        // Ignore-Bounds gilt hier nicht (Array-Bounds sind keine Policy-Sache).
278        // Array: Small + Large untereinander; Bounds auf u32-Vec normalisieren
279        // und strukturell vergleichen.
280        (
281            TypeIdentifier::PlainArraySmall { .. } | TypeIdentifier::PlainArrayLarge { .. },
282            TypeIdentifier::PlainArraySmall { .. } | TypeIdentifier::PlainArrayLarge { .. },
283        ) => {
284            let (we, wb) = array_parts(w);
285            let (re, rb) = array_parts(r);
286            if wb != rb {
287                return Assignable::No("array bounds differ");
288            }
289            is_assignable(we, re, registry, cfg)
290        }
291
292        // Map: Small ↔ Small/Large, Large ↔ Large/Small.
293        (
294            TypeIdentifier::PlainMapSmall { .. } | TypeIdentifier::PlainMapLarge { .. },
295            TypeIdentifier::PlainMapSmall { .. } | TypeIdentifier::PlainMapLarge { .. },
296        ) => {
297            let (we, wk, wb) = map_parts(w);
298            let (re, rk, rb) = map_parts(r);
299            if !cfg.ignore_sequence_bounds && rb != 0 && wb > rb {
300                return Assignable::No("writer map bound exceeds reader bound");
301            }
302            match is_assignable(wk, rk, registry, cfg) {
303                Assignable::Yes => is_assignable(we, re, registry, cfg),
304                e => e,
305            }
306        }
307
308        // Hash-Refs: beide auf Minimal → strukturelle Gleichheit.
309        (
310            TypeIdentifier::EquivalenceHashMinimal(wh),
311            TypeIdentifier::EquivalenceHashMinimal(rh),
312        ) => {
313            if wh == rh {
314                return Assignable::Yes;
315            }
316            // TypeObjects aus Registry vergleichen.
317            match (registry.get_minimal(wh), registry.get_minimal(rh)) {
318                (Some(wobj), Some(robj)) => check_minimal_types(wobj, robj, registry, cfg),
319                _ => Assignable::No("unknown type objects for hash comparison"),
320            }
321        }
322
323        _ => Assignable::No("kinds do not match"),
324    }
325}
326
327fn sequence_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, u32) {
328    match ti {
329        TypeIdentifier::PlainSequenceSmall { element, bound, .. } => (element, u32::from(*bound)),
330        TypeIdentifier::PlainSequenceLarge { element, bound, .. } => (element, *bound),
331        _ => (ti, 0),
332    }
333}
334
335fn array_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, alloc::vec::Vec<u32>) {
336    match ti {
337        TypeIdentifier::PlainArraySmall {
338            element,
339            array_bounds,
340            ..
341        } => (
342            element,
343            array_bounds.iter().map(|b| u32::from(*b)).collect(),
344        ),
345        TypeIdentifier::PlainArrayLarge {
346            element,
347            array_bounds,
348            ..
349        } => (element, array_bounds.clone()),
350        _ => (ti, alloc::vec::Vec::new()),
351    }
352}
353
354fn map_parts(ti: &TypeIdentifier) -> (&TypeIdentifier, &TypeIdentifier, u32) {
355    match ti {
356        TypeIdentifier::PlainMapSmall {
357            element,
358            key,
359            bound,
360            ..
361        } => (element, key, u32::from(*bound)),
362        TypeIdentifier::PlainMapLarge {
363            element,
364            key,
365            bound,
366            ..
367        } => (element, key, *bound),
368        _ => (ti, ti, 0),
369    }
370}
371
372fn string_bound_u32_s8(ti: &TypeIdentifier) -> u32 {
373    match ti {
374        TypeIdentifier::String8Small { bound } => u32::from(*bound),
375        TypeIdentifier::String8Large { bound } => *bound,
376        _ => 0,
377    }
378}
379
380fn string_bound_u32_s16(ti: &TypeIdentifier) -> u32 {
381    match ti {
382        TypeIdentifier::String16Small { bound } => u32::from(*bound),
383        TypeIdentifier::String16Large { bound } => *bound,
384        _ => 0,
385    }
386}
387
388fn primitive_compatible(
389    w: PrimitiveKind,
390    r: PrimitiveKind,
391    cfg: &AssignabilityConfig,
392) -> Assignable {
393    if w == r {
394        return Assignable::Yes;
395    }
396    if !cfg.allow_type_coercion {
397        return Assignable::No("primitive kinds differ (no coercion allowed)");
398    }
399    // Koerzions-Matrix fuer Numerics — widening OK, narrowing nein.
400    // Signed-Widening (Int8/16/32 → Int64), Unsigned-Widening (UInt8/16/32
401    // → UInt64 + auch in signed-wider-Typen, da alle Werte verlustfrei
402    // passen), Float-Widening (Float32 → Float64).
403    use PrimitiveKind::*;
404    let ok = matches!(
405        (w, r),
406        (Int8 | UInt8 | Byte, Int16 | Int32 | Int64)
407            | (Int16 | UInt16, Int32 | Int64)
408            | (Int32 | UInt32, Int64)
409            | (UInt8 | Byte, UInt16 | UInt32 | UInt64)
410            | (UInt16, UInt32 | UInt64)
411            | (UInt32, UInt64)
412            | (Float32, Float64)
413    );
414    if ok {
415        Assignable::Yes
416    } else {
417        Assignable::No("primitive coercion not widening-safe")
418    }
419}
420
421/// Drei-Werte-Extensibility aus den Flag-Bits. Default (keine Flags) =
422/// Appendable, entsprechend XTypes §7.2.2.4.
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
424enum StructExt {
425    Final,
426    Appendable,
427    Mutable,
428}
429
430fn struct_extensibility(flags: StructTypeFlag) -> StructExt {
431    if flags.has(StructTypeFlag::IS_FINAL) {
432        StructExt::Final
433    } else if flags.has(StructTypeFlag::IS_MUTABLE) {
434        StructExt::Mutable
435    } else {
436        StructExt::Appendable
437    }
438}
439
440fn check_minimal_types(
441    w: &MinimalTypeObject,
442    r: &MinimalTypeObject,
443    registry: &TypeRegistry,
444    cfg: &AssignabilityConfig,
445) -> Assignable {
446    match (w, r) {
447        (MinimalTypeObject::Struct(ws), MinimalTypeObject::Struct(rs)) => {
448            // Extensibility-Check: writer + reader muessen
449            // dieselbe Extensibility-Kategorie haben (§7.2.4.4).
450            // Bugfix (#10): Konsolidiere Flag-Bits auf einen drei-Werte-
451            // Enum-Vergleich, um Corner-Case "beide haben keine Flag-Bits
452            // gesetzt" = Appendable == Appendable korrekt zu erfassen.
453            let w_ext = struct_extensibility(ws.struct_flags);
454            let r_ext = struct_extensibility(rs.struct_flags);
455            if w_ext != r_ext {
456                return Assignable::No("extensibility mismatch");
457            }
458            let w_final = matches!(w_ext, StructExt::Final);
459            let w_mut = matches!(w_ext, StructExt::Mutable);
460
461            if w_final {
462                // Strict gleich (selbe Anzahl + gleiche Types in Reihenfolge).
463                if ws.member_seq.len() != rs.member_seq.len() {
464                    return Assignable::No("final struct member count mismatch");
465                }
466                for (wm, rm) in ws.member_seq.iter().zip(rs.member_seq.iter()) {
467                    if !cfg.ignore_member_names && wm.detail != rm.detail {
468                        return Assignable::No("final struct member name-hash differs");
469                    }
470                    match is_assignable(
471                        &wm.common.member_type_id,
472                        &rm.common.member_type_id,
473                        registry,
474                        cfg,
475                    ) {
476                        Assignable::Yes => {}
477                        e => return e,
478                    }
479                }
480                Assignable::Yes
481            } else if w_mut {
482                // Mutable: match per @id (member_id). Reader-Member mit
483                // @id=X muss im Writer existieren und kompatibel sein,
484                // wenn nicht optional. Mit `ignore_member_names=false`
485                // muss auch der NameHash matchen (§7.6.3.7.2.2).
486                for rm in &rs.member_seq {
487                    let rm_optional = rm
488                        .common
489                        .member_flags
490                        .has(crate::type_object::flags::StructMemberFlag::IS_OPTIONAL);
491                    match ws
492                        .member_seq
493                        .iter()
494                        .find(|wm| wm.common.member_id == rm.common.member_id)
495                    {
496                        Some(wm) => {
497                            if !cfg.ignore_member_names && wm.detail != rm.detail {
498                                return Assignable::No(
499                                    "mutable: member name-hash differs despite id match",
500                                );
501                            }
502                            match is_assignable(
503                                &wm.common.member_type_id,
504                                &rm.common.member_type_id,
505                                registry,
506                                cfg,
507                            ) {
508                                Assignable::Yes => {}
509                                e => return e,
510                            }
511                        }
512                        None if rm_optional => {}
513                        None => return Assignable::No("mutable: reader member missing in writer"),
514                    }
515                }
516                Assignable::Yes
517            } else {
518                // Appendable (default): writer muss mindestens alle
519                // reader-Felder als Prefix haben; extra writer-Felder OK.
520                if ws.member_seq.len() < rs.member_seq.len() {
521                    return Assignable::No("appendable: writer has fewer members than reader");
522                }
523                for (wm, rm) in ws.member_seq.iter().zip(rs.member_seq.iter()) {
524                    if !cfg.ignore_member_names && wm.detail != rm.detail {
525                        return Assignable::No("appendable: member name-hash differs");
526                    }
527                    match is_assignable(
528                        &wm.common.member_type_id,
529                        &rm.common.member_type_id,
530                        registry,
531                        cfg,
532                    ) {
533                        Assignable::Yes => {}
534                        e => return e,
535                    }
536                }
537                Assignable::Yes
538            }
539        }
540        (MinimalTypeObject::Enumerated(we), MinimalTypeObject::Enumerated(re)) => {
541            // §7.2.4.4.4.3: writer-Values muessen alle im reader-Set
542            // enthalten sein (writer ⊆ reader). Reader darf zusaetzliche
543            // Literale haben — er konsumiert weniger als er erkennt.
544            // Bit-Bound muss identisch sein (Wire-Breite).
545            if we.header.common.bit_bound != re.header.common.bit_bound {
546                return Assignable::No("enum bit_bound mismatch");
547            }
548            // §7.2.4.4.7 — Default vergleicht (value, name_hash); mit
549            // `@ignore_literal_names` auf einer Seite oder via Config nur (value).
550            let ignore_names = cfg.ignore_literal_names
551                || we
552                    .enum_flags
553                    .has(crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES)
554                || re
555                    .enum_flags
556                    .has(crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES);
557            for wl in &we.literal_seq {
558                let found = re.literal_seq.iter().any(|rl| {
559                    rl.common.value == wl.common.value && (ignore_names || rl.detail == wl.detail)
560                });
561                if !found {
562                    return Assignable::No("enum writer literal unknown in reader");
563                }
564            }
565            Assignable::Yes
566        }
567        _ => Assignable::No("type kinds do not match"),
568    }
569}
570
571#[cfg(test)]
572#[allow(clippy::unwrap_used)]
573mod tests {
574    use super::*;
575    use crate::builder::{Extensibility, TypeObjectBuilder};
576    use crate::hash::compute_minimal_hash;
577    use crate::type_object::TypeObject;
578
579    #[test]
580    fn primitive_same_kind_is_assignable() {
581        let reg = TypeRegistry::new();
582        let a = is_assignable(
583            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
584            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
585            &reg,
586            &AssignabilityConfig::default(),
587        );
588        assert!(a.is_yes());
589    }
590
591    #[test]
592    fn primitive_different_kind_is_not_assignable_by_default() {
593        let reg = TypeRegistry::new();
594        let a = is_assignable(
595            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
596            &TypeIdentifier::Primitive(PrimitiveKind::Int64),
597            &reg,
598            &AssignabilityConfig::default(),
599        );
600        assert!(!a.is_yes());
601    }
602
603    #[test]
604    fn primitive_widening_with_coercion_is_assignable() {
605        let reg = TypeRegistry::new();
606        let cfg = AssignabilityConfig {
607            allow_type_coercion: true,
608            ..Default::default()
609        };
610        assert!(
611            is_assignable(
612                &TypeIdentifier::Primitive(PrimitiveKind::Int32),
613                &TypeIdentifier::Primitive(PrimitiveKind::Int64),
614                &reg,
615                &cfg,
616            )
617            .is_yes()
618        );
619        // Narrowing bleibt verboten
620        assert!(
621            !is_assignable(
622                &TypeIdentifier::Primitive(PrimitiveKind::Int64),
623                &TypeIdentifier::Primitive(PrimitiveKind::Int32),
624                &reg,
625                &cfg,
626            )
627            .is_yes()
628        );
629    }
630
631    #[test]
632    fn appendable_struct_with_extra_writer_field_is_assignable() {
633        let mut reg = TypeRegistry::new();
634        let writer = MinimalTypeObject::Struct(
635            TypeObjectBuilder::struct_type("::X")
636                .extensibility(Extensibility::Appendable)
637                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
638                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
639                .build_minimal(),
640        );
641        let reader = MinimalTypeObject::Struct(
642            TypeObjectBuilder::struct_type("::X")
643                .extensibility(Extensibility::Appendable)
644                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
645                .build_minimal(),
646        );
647        let wh = compute_minimal_hash(&writer).unwrap();
648        let rh = compute_minimal_hash(&reader).unwrap();
649        reg.insert_minimal(wh, writer.clone());
650        reg.insert_minimal(rh, reader);
651
652        assert!(
653            is_assignable(
654                &TypeIdentifier::EquivalenceHashMinimal(wh),
655                &TypeIdentifier::EquivalenceHashMinimal(rh),
656                &reg,
657                &AssignabilityConfig::default(),
658            )
659            .is_yes()
660        );
661    }
662
663    #[test]
664    fn final_struct_with_extra_writer_field_is_not_assignable() {
665        let mut reg = TypeRegistry::new();
666        let writer = MinimalTypeObject::Struct(
667            TypeObjectBuilder::struct_type("::X")
668                .extensibility(Extensibility::Final)
669                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
670                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
671                .build_minimal(),
672        );
673        let reader = MinimalTypeObject::Struct(
674            TypeObjectBuilder::struct_type("::X")
675                .extensibility(Extensibility::Final)
676                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
677                .build_minimal(),
678        );
679        let wh = compute_minimal_hash(&writer).unwrap();
680        let rh = compute_minimal_hash(&reader).unwrap();
681        reg.insert_minimal(wh, writer);
682        reg.insert_minimal(rh, reader);
683
684        assert!(
685            !is_assignable(
686                &TypeIdentifier::EquivalenceHashMinimal(wh),
687                &TypeIdentifier::EquivalenceHashMinimal(rh),
688                &reg,
689                &AssignabilityConfig::default(),
690            )
691            .is_yes()
692        );
693    }
694
695    #[test]
696    fn mutable_struct_member_id_matching() {
697        let mut reg = TypeRegistry::new();
698        // Reader hat @id(1) und @id(2) — writer hat @id(2) und @id(3).
699        // Reader @id(1) fehlt im writer, aber markieren wir als optional.
700        let writer = MinimalTypeObject::Struct(
701            TypeObjectBuilder::struct_type("::X")
702                .extensibility(Extensibility::Mutable)
703                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
704                    m.id(2)
705                })
706                .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
707                    m.id(3)
708                })
709                .build_minimal(),
710        );
711        let reader = MinimalTypeObject::Struct(
712            TypeObjectBuilder::struct_type("::X")
713                .extensibility(Extensibility::Mutable)
714                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
715                    m.id(1).optional()
716                })
717                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int64), |m| {
718                    m.id(2)
719                })
720                .build_minimal(),
721        );
722        let wh = compute_minimal_hash(&writer).unwrap();
723        let rh = compute_minimal_hash(&reader).unwrap();
724        reg.insert_minimal(wh, writer);
725        reg.insert_minimal(rh, reader);
726
727        assert!(
728            is_assignable(
729                &TypeIdentifier::EquivalenceHashMinimal(wh),
730                &TypeIdentifier::EquivalenceHashMinimal(rh),
731                &reg,
732                &AssignabilityConfig::default(),
733            )
734            .is_yes()
735        );
736    }
737
738    #[test]
739    fn extensibility_mismatch_fails() {
740        let mut reg = TypeRegistry::new();
741        let writer = MinimalTypeObject::Struct(
742            TypeObjectBuilder::struct_type("::X")
743                .extensibility(Extensibility::Final)
744                .build_minimal(),
745        );
746        let reader = MinimalTypeObject::Struct(
747            TypeObjectBuilder::struct_type("::X")
748                .extensibility(Extensibility::Mutable)
749                .build_minimal(),
750        );
751        let wh = compute_minimal_hash(&writer).unwrap();
752        let rh = compute_minimal_hash(&reader).unwrap();
753        reg.insert_minimal(wh, writer);
754        reg.insert_minimal(rh, reader);
755
756        let a = is_assignable(
757            &TypeIdentifier::EquivalenceHashMinimal(wh),
758            &TypeIdentifier::EquivalenceHashMinimal(rh),
759            &reg,
760            &AssignabilityConfig::default(),
761        );
762        assert!(!a.is_yes());
763    }
764
765    #[test]
766    fn string_small_and_large_interchangeable() {
767        let reg = TypeRegistry::new();
768        assert!(
769            is_assignable(
770                &TypeIdentifier::String8Small { bound: 64 },
771                &TypeIdentifier::String8Large { bound: 100_000 },
772                &reg,
773                &AssignabilityConfig::default(),
774            )
775            .is_yes()
776        );
777    }
778
779    // Silence unused import when only some Variants are matched.
780    #[allow(dead_code)]
781    fn _unused() -> TypeObject {
782        TypeObject::Minimal(MinimalTypeObject::Struct(
783            TypeObjectBuilder::struct_type("::dummy").build_minimal(),
784        ))
785    }
786
787    // ---- Additional coverage for check_direct arms -----------------------
788
789    use crate::type_identifier::PlainCollectionHeader;
790    use alloc::boxed::Box;
791
792    fn reg() -> TypeRegistry {
793        TypeRegistry::new()
794    }
795
796    #[test]
797    fn string8_vs_string16_not_assignable() {
798        let a = is_assignable(
799            &TypeIdentifier::String8Small { bound: 16 },
800            &TypeIdentifier::String16Small { bound: 16 },
801            &reg(),
802            &AssignabilityConfig::default(),
803        );
804        assert!(!a.is_yes());
805        assert!(matches!(a, Assignable::No(msg) if msg.contains("kinds")));
806    }
807
808    #[test]
809    fn string16_small_and_large_interchangeable() {
810        let a = is_assignable(
811            &TypeIdentifier::String16Small { bound: 32 },
812            &TypeIdentifier::String16Large { bound: 10_000 },
813            &reg(),
814            &AssignabilityConfig::default(),
815        );
816        assert!(a.is_yes());
817    }
818
819    #[test]
820    fn identical_type_identifiers_short_circuit_yes() {
821        // Exact equality path (`w == r`) in check_direct.
822        let ti = TypeIdentifier::Primitive(PrimitiveKind::UInt32);
823        let a = is_assignable(&ti, &ti, &reg(), &AssignabilityConfig::default());
824        assert!(a.is_yes());
825    }
826
827    #[test]
828    fn sequence_writer_bound_exceeds_reader_bound_is_no() {
829        let w = TypeIdentifier::PlainSequenceSmall {
830            header: PlainCollectionHeader::default(),
831            bound: 20,
832            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
833        };
834        let r = TypeIdentifier::PlainSequenceSmall {
835            header: PlainCollectionHeader::default(),
836            bound: 10,
837            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
838        };
839        // `ignore_sequence_bounds=false` erzwingt den Bound-Check.
840        let cfg = AssignabilityConfig {
841            ignore_sequence_bounds: false,
842            ..Default::default()
843        };
844        let a = is_assignable(&w, &r, &reg(), &cfg);
845        assert!(!a.is_yes());
846        assert!(matches!(a, Assignable::No(msg) if msg.contains("bound")));
847    }
848
849    /// Mit `ignore_sequence_bounds=true` (TCE-Default) akzeptieren wir
850    /// die Sequence, obwohl Writer-Bound > Reader-Bound.
851    #[test]
852    fn sequence_bounds_ignored_when_policy_allows() {
853        let w = TypeIdentifier::PlainSequenceSmall {
854            header: PlainCollectionHeader::default(),
855            bound: 20,
856            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
857        };
858        let r = TypeIdentifier::PlainSequenceSmall {
859            header: PlainCollectionHeader::default(),
860            bound: 10,
861            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
862        };
863        assert!(is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
864    }
865
866    #[test]
867    fn sequence_reader_unbounded_accepts_any_writer_bound() {
868        // reader bound=0 (unbounded) → writer bound check skipped.
869        let w = TypeIdentifier::PlainSequenceSmall {
870            header: PlainCollectionHeader::default(),
871            bound: 200,
872            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int16)),
873        };
874        let r = TypeIdentifier::PlainSequenceSmall {
875            header: PlainCollectionHeader::default(),
876            bound: 0,
877            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int16)),
878        };
879        assert!(is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
880    }
881
882    #[test]
883    fn sequence_elements_must_be_assignable() {
884        // Same bound, but different incompatible element kinds.
885        let w = TypeIdentifier::PlainSequenceSmall {
886            header: PlainCollectionHeader::default(),
887            bound: 5,
888            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
889        };
890        let r = TypeIdentifier::PlainSequenceSmall {
891            header: PlainCollectionHeader::default(),
892            bound: 5,
893            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Float64)),
894        };
895        assert!(!is_assignable(&w, &r, &reg(), &AssignabilityConfig::default()).is_yes());
896    }
897
898    #[test]
899    fn nested_sequence_of_sequence_assignable_when_elements_match() {
900        let inner = TypeIdentifier::PlainSequenceSmall {
901            header: PlainCollectionHeader::default(),
902            bound: 5,
903            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
904        };
905        let outer = TypeIdentifier::PlainSequenceSmall {
906            header: PlainCollectionHeader::default(),
907            bound: 3,
908            element: Box::new(inner.clone()),
909        };
910        // outer vs outer with inner bound differing: inner writer=5, reader=10 → OK
911        let inner_wider = TypeIdentifier::PlainSequenceSmall {
912            header: PlainCollectionHeader::default(),
913            bound: 10,
914            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
915        };
916        let outer2 = TypeIdentifier::PlainSequenceSmall {
917            header: PlainCollectionHeader::default(),
918            bound: 3,
919            element: Box::new(inner_wider),
920        };
921        assert!(is_assignable(&outer, &outer2, &reg(), &AssignabilityConfig::default()).is_yes());
922    }
923
924    #[test]
925    fn plain_array_identical_assigns_yes() {
926        let a = is_assignable(
927            &TypeIdentifier::PlainArraySmall {
928                header: PlainCollectionHeader::default(),
929                array_bounds: alloc::vec![3, 4],
930                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
931            },
932            &TypeIdentifier::PlainArraySmall {
933                header: PlainCollectionHeader::default(),
934                array_bounds: alloc::vec![3, 4],
935                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
936            },
937            &reg(),
938            &AssignabilityConfig::default(),
939        );
940        assert!(a.is_yes());
941    }
942
943    #[test]
944    fn plain_array_diff_bounds_is_no() {
945        let b = is_assignable(
946            &TypeIdentifier::PlainArraySmall {
947                header: PlainCollectionHeader::default(),
948                array_bounds: alloc::vec![3, 4],
949                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
950            },
951            &TypeIdentifier::PlainArraySmall {
952                header: PlainCollectionHeader::default(),
953                array_bounds: alloc::vec![3, 5],
954                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
955            },
956            &reg(),
957            &AssignabilityConfig::default(),
958        );
959        assert!(!b.is_yes());
960        assert!(matches!(b, Assignable::No(msg) if msg.contains("array bounds")));
961    }
962
963    #[test]
964    fn equivalence_hash_identical_short_circuits_to_yes() {
965        // Bei identischen Hashes nimmt `check_direct` den Early-Exit
966        // `wh == rh → Yes` (siehe match-Arm). Voraussetzung ist nur,
967        // dass `resolve_alias_chain` erfolgreich ist — dafuer muss
968        // der Hash in der Registry auf einen Nicht-Alias verweisen.
969        let mut reg = reg();
970        let to = MinimalTypeObject::Struct(
971            TypeObjectBuilder::struct_type("::T")
972                .extensibility(Extensibility::Appendable)
973                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
974                .build_minimal(),
975        );
976        let h = compute_minimal_hash(&to).unwrap();
977        reg.insert_minimal(h, to);
978        let a = is_assignable(
979            &TypeIdentifier::EquivalenceHashMinimal(h),
980            &TypeIdentifier::EquivalenceHashMinimal(h),
981            &reg,
982            &AssignabilityConfig::default(),
983        );
984        assert!(a.is_yes());
985    }
986
987    #[test]
988    fn equivalence_hash_unresolved_writer_is_no() {
989        // Alias-Resolution schlaegt fehl, weil der Hash nicht in der
990        // Registry liegt → Frueh-Exit mit `No("writer alias resolution failed")`.
991        let reg = reg();
992        let wh = crate::type_identifier::EquivalenceHash([0x01; 14]);
993        let rh = crate::type_identifier::EquivalenceHash([0x02; 14]);
994        let a = is_assignable(
995            &TypeIdentifier::EquivalenceHashMinimal(wh),
996            &TypeIdentifier::EquivalenceHashMinimal(rh),
997            &reg,
998            &AssignabilityConfig::default(),
999        );
1000        assert!(!a.is_yes());
1001        assert!(matches!(a, Assignable::No(msg) if msg.contains("alias resolution")));
1002    }
1003
1004    #[test]
1005    fn equivalence_hash_unresolved_reader_is_no() {
1006        // Writer ist registriert, reader nicht → Fehler beim Reader-Resolve.
1007        let mut reg = reg();
1008        let to = MinimalTypeObject::Struct(
1009            TypeObjectBuilder::struct_type("::T")
1010                .extensibility(Extensibility::Appendable)
1011                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1012                .build_minimal(),
1013        );
1014        let wh = compute_minimal_hash(&to).unwrap();
1015        reg.insert_minimal(wh, to);
1016        let rh = crate::type_identifier::EquivalenceHash([0x02; 14]);
1017        let a = is_assignable(
1018            &TypeIdentifier::EquivalenceHashMinimal(wh),
1019            &TypeIdentifier::EquivalenceHashMinimal(rh),
1020            &reg,
1021            &AssignabilityConfig::default(),
1022        );
1023        assert!(!a.is_yes());
1024        assert!(matches!(a, Assignable::No(msg) if msg.contains("reader")));
1025    }
1026
1027    #[test]
1028    fn mixed_kinds_report_kinds_do_not_match() {
1029        // Primitive vs String → kein Arm matched → `No("kinds do not match")`.
1030        let a = is_assignable(
1031            &TypeIdentifier::Primitive(PrimitiveKind::Int32),
1032            &TypeIdentifier::String8Small { bound: 10 },
1033            &reg(),
1034            &AssignabilityConfig::default(),
1035        );
1036        assert!(!a.is_yes());
1037    }
1038
1039    #[test]
1040    fn enum_mismatch_writer_literal_unknown_in_reader_is_no() {
1041        let mut reg = reg();
1042        let w = MinimalTypeObject::Enumerated(
1043            TypeObjectBuilder::enum_type("::E")
1044                .bit_bound(32)
1045                .literal("A", 1)
1046                .literal("B", 2)
1047                .build_minimal(),
1048        );
1049        let r = MinimalTypeObject::Enumerated(
1050            TypeObjectBuilder::enum_type("::E")
1051                .bit_bound(32)
1052                .literal("A", 1)
1053                .build_minimal(),
1054        );
1055        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1056        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1057        reg.insert_minimal(wh, w);
1058        reg.insert_minimal(rh, r);
1059
1060        let a = is_assignable(
1061            &TypeIdentifier::EquivalenceHashMinimal(wh),
1062            &TypeIdentifier::EquivalenceHashMinimal(rh),
1063            &reg,
1064            &AssignabilityConfig::default(),
1065        );
1066        assert!(!a.is_yes());
1067    }
1068
1069    #[test]
1070    fn enum_bit_bound_mismatch_is_no() {
1071        let mut reg = reg();
1072        let w = MinimalTypeObject::Enumerated(
1073            TypeObjectBuilder::enum_type("::E")
1074                .bit_bound(32)
1075                .literal("A", 1)
1076                .build_minimal(),
1077        );
1078        let r = MinimalTypeObject::Enumerated(
1079            TypeObjectBuilder::enum_type("::E")
1080                .bit_bound(16)
1081                .literal("A", 1)
1082                .build_minimal(),
1083        );
1084        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1085        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1086        reg.insert_minimal(wh, w);
1087        reg.insert_minimal(rh, r);
1088
1089        let a = is_assignable(
1090            &TypeIdentifier::EquivalenceHashMinimal(wh),
1091            &TypeIdentifier::EquivalenceHashMinimal(rh),
1092            &reg,
1093            &AssignabilityConfig::default(),
1094        );
1095        assert!(!a.is_yes());
1096    }
1097
1098    #[test]
1099    fn enum_identical_labels_is_yes() {
1100        let mut reg = reg();
1101        let w = MinimalTypeObject::Enumerated(
1102            TypeObjectBuilder::enum_type("::E")
1103                .bit_bound(32)
1104                .literal("A", 1)
1105                .literal("B", 2)
1106                .build_minimal(),
1107        );
1108        let r = MinimalTypeObject::Enumerated(
1109            TypeObjectBuilder::enum_type("::E")
1110                .bit_bound(32)
1111                .literal("A", 1)
1112                .literal("B", 2)
1113                .literal("C", 3) // reader kennt extra label, writer-labels sind subset
1114                .build_minimal(),
1115        );
1116        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1117        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1118        reg.insert_minimal(wh, w);
1119        reg.insert_minimal(rh, r);
1120
1121        let a = is_assignable(
1122            &TypeIdentifier::EquivalenceHashMinimal(wh),
1123            &TypeIdentifier::EquivalenceHashMinimal(rh),
1124            &reg,
1125            &AssignabilityConfig::default(),
1126        );
1127        assert!(a.is_yes());
1128    }
1129
1130    // ---- §7.2.2.4.5 Inheritance flatten + edge-cases ----
1131
1132    #[test]
1133    fn flatten_inheritance_no_base_returns_struct_unchanged() {
1134        let s = TypeObjectBuilder::struct_type("::S")
1135            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1136            .build_minimal();
1137        let reg = reg();
1138        let flat = flatten_inheritance(&s, &reg, 8).unwrap();
1139        assert_eq!(flat.member_seq.len(), 1);
1140    }
1141
1142    #[test]
1143    fn flatten_inheritance_two_levels_concatenates_base_first() {
1144        // Root → Mid → Derived. Result Member-Reihenfolge: [Root, Mid, Derived].
1145        let mut reg = reg();
1146        let root = TypeObjectBuilder::struct_type("::Root")
1147            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int8), |m| {
1148                m.id(101)
1149            })
1150            .build_minimal();
1151        let root_h = compute_minimal_hash(&MinimalTypeObject::Struct(root.clone())).unwrap();
1152        reg.insert_minimal(root_h, MinimalTypeObject::Struct(root));
1153
1154        let mid = TypeObjectBuilder::struct_type("::Mid")
1155            .base(TypeIdentifier::EquivalenceHashMinimal(root_h))
1156            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int16), |m| {
1157                m.id(202)
1158            })
1159            .build_minimal();
1160        let mid_h = compute_minimal_hash(&MinimalTypeObject::Struct(mid.clone())).unwrap();
1161        reg.insert_minimal(mid_h, MinimalTypeObject::Struct(mid));
1162
1163        let derived = TypeObjectBuilder::struct_type("::Derived")
1164            .base(TypeIdentifier::EquivalenceHashMinimal(mid_h))
1165            .member("d", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1166                m.id(303)
1167            })
1168            .build_minimal();
1169
1170        let flat = flatten_inheritance(&derived, &reg, 8).unwrap();
1171        // 3 Member, base_type weg.
1172        assert_eq!(flat.header.base_type, TypeIdentifier::None);
1173        assert_eq!(flat.member_seq.len(), 3);
1174        // Erster Member ist 'r' (root); letzter 'd' (derived).
1175        let first_id = flat.member_seq[0].common.member_id;
1176        let last_id = flat.member_seq[2].common.member_id;
1177        assert_ne!(first_id, last_id);
1178    }
1179
1180    #[test]
1181    fn inheritance_conflict_same_id() {
1182        let mut reg = reg();
1183        let base = TypeObjectBuilder::struct_type("::B")
1184            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1185                m.id(7)
1186            })
1187            .build_minimal();
1188        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1189        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1190
1191        let derived = TypeObjectBuilder::struct_type("::D")
1192            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1193            .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1194                m.id(7) // selbe ID wie Base-Member 'a' → Konflikt
1195            })
1196            .build_minimal();
1197        let err = flatten_inheritance(&derived, &reg, 8).unwrap_err();
1198        assert!(matches!(
1199            err,
1200            InheritanceError::InheritanceConflict { reason, .. }
1201                if reason.contains("member_id")
1202        ));
1203    }
1204
1205    #[test]
1206    fn inheritance_conflict_same_name() {
1207        let mut reg = reg();
1208        let base = TypeObjectBuilder::struct_type("::B")
1209            .member(
1210                "dup",
1211                TypeIdentifier::Primitive(PrimitiveKind::Int32),
1212                |m| m.id(1),
1213            )
1214            .build_minimal();
1215        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1216        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1217
1218        let derived = TypeObjectBuilder::struct_type("::D")
1219            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1220            .member(
1221                "dup",
1222                TypeIdentifier::Primitive(PrimitiveKind::Int32),
1223                |m| {
1224                    m.id(2) // andere ID, aber gleicher Name → Konflikt
1225                },
1226            )
1227            .build_minimal();
1228        let err = flatten_inheritance(&derived, &reg, 8).unwrap_err();
1229        assert!(matches!(
1230            err,
1231            InheritanceError::InheritanceConflict { reason, .. }
1232                if reason.contains("name_hash")
1233        ));
1234    }
1235
1236    #[test]
1237    fn flat_type_construction_two_levels() {
1238        // §7.2.2.4.5 — die hypothetische flache Repraesentation eines
1239        // 2-Stufen-Inheritance-Konstrukts hat Members in Base-Derived-
1240        // Reihenfolge.
1241        let mut reg = reg();
1242        let base = TypeObjectBuilder::struct_type("::Base")
1243            .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1244                m.id(1)
1245            })
1246            .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1247                m.id(2)
1248            })
1249            .build_minimal();
1250        let bh = compute_minimal_hash(&MinimalTypeObject::Struct(base.clone())).unwrap();
1251        reg.insert_minimal(bh, MinimalTypeObject::Struct(base));
1252
1253        let derived = TypeObjectBuilder::struct_type("::Derived")
1254            .base(TypeIdentifier::EquivalenceHashMinimal(bh))
1255            .member("c", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1256                m.id(3)
1257            })
1258            .build_minimal();
1259
1260        let flat = flatten_inheritance(&derived, &reg, 8).unwrap();
1261        assert_eq!(flat.member_seq.len(), 3);
1262        assert_eq!(flat.member_seq[0].common.member_id, 1);
1263        assert_eq!(flat.member_seq[1].common.member_id, 2);
1264        assert_eq!(flat.member_seq[2].common.member_id, 3);
1265    }
1266
1267    #[test]
1268    fn two_level_inheritance_assignability_chain() {
1269        // Writer und Reader haben jeweils 2-Stufen-Inheritance, beide
1270        // produzieren dieselbe flache Member-Sequenz → assignable.
1271        let mut reg = reg();
1272
1273        // ---- Writer-Seite ----
1274        let w_root = TypeObjectBuilder::struct_type("::WRoot")
1275            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1276                m.id(1)
1277            })
1278            .build_minimal();
1279        let wr_h = compute_minimal_hash(&MinimalTypeObject::Struct(w_root.clone())).unwrap();
1280        reg.insert_minimal(wr_h, MinimalTypeObject::Struct(w_root));
1281
1282        let w_mid = TypeObjectBuilder::struct_type("::WMid")
1283            .base(TypeIdentifier::EquivalenceHashMinimal(wr_h))
1284            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1285                m.id(2)
1286            })
1287            .build_minimal();
1288
1289        let w_flat = flatten_inheritance(&w_mid, &reg, 8).unwrap();
1290        assert_eq!(w_flat.member_seq.len(), 2);
1291
1292        // ---- Reader-Seite (gleiche Struktur, andere Type-Namen) ----
1293        let r_root = TypeObjectBuilder::struct_type("::RRoot")
1294            .member("r", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1295                m.id(1)
1296            })
1297            .build_minimal();
1298        let rr_h = compute_minimal_hash(&MinimalTypeObject::Struct(r_root.clone())).unwrap();
1299        reg.insert_minimal(rr_h, MinimalTypeObject::Struct(r_root));
1300
1301        let r_mid = TypeObjectBuilder::struct_type("::RMid")
1302            .base(TypeIdentifier::EquivalenceHashMinimal(rr_h))
1303            .member("m", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1304                m.id(2)
1305            })
1306            .build_minimal();
1307
1308        let r_flat = flatten_inheritance(&r_mid, &reg, 8).unwrap();
1309        assert_eq!(r_flat.member_seq.len(), 2);
1310
1311        // Direkter Member-by-Member-Compare via Assignability.
1312        let w_to = MinimalTypeObject::Struct(w_flat.clone());
1313        let r_to = MinimalTypeObject::Struct(r_flat.clone());
1314        let wh = compute_minimal_hash(&w_to).unwrap();
1315        let rh = compute_minimal_hash(&r_to).unwrap();
1316        reg.insert_minimal(wh, w_to);
1317        reg.insert_minimal(rh, r_to);
1318        let a = is_assignable(
1319            &TypeIdentifier::EquivalenceHashMinimal(wh),
1320            &TypeIdentifier::EquivalenceHashMinimal(rh),
1321            &reg,
1322            &AssignabilityConfig::default(),
1323        );
1324        assert!(a.is_yes(), "got {a:?}");
1325    }
1326
1327    #[test]
1328    fn enum_not_assignable_strict_default() {
1329        // Selber Wert, aber unterschiedliche Namen → strict-Default
1330        // (keine `@ignore_literal_names`-Annotation, kein Config-Flag)
1331        // muss assignable=No liefern.
1332        let mut reg = reg();
1333        let w = MinimalTypeObject::Enumerated(
1334            TypeObjectBuilder::enum_type("::E")
1335                .bit_bound(32)
1336                .literal("RED", 1)
1337                .build_minimal(),
1338        );
1339        let r = MinimalTypeObject::Enumerated(
1340            TypeObjectBuilder::enum_type("::E")
1341                .bit_bound(32)
1342                .literal("ROUGE", 1) // selber Wert, anderer Name
1343                .build_minimal(),
1344        );
1345        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1346        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1347        reg.insert_minimal(wh, w);
1348        reg.insert_minimal(rh, r);
1349
1350        let a = is_assignable(
1351            &TypeIdentifier::EquivalenceHashMinimal(wh),
1352            &TypeIdentifier::EquivalenceHashMinimal(rh),
1353            &reg,
1354            &AssignabilityConfig::default(),
1355        );
1356        assert!(!a.is_yes());
1357    }
1358
1359    #[test]
1360    fn enum_assignable_with_ignore_literal_names() {
1361        // Selber Wert, andere Namen — aber Config oder Flag setzt
1362        // ignore_literal_names; Result muss Yes sein.
1363        let mut reg = reg();
1364        let w = MinimalTypeObject::Enumerated(
1365            TypeObjectBuilder::enum_type("::E")
1366                .bit_bound(32)
1367                .literal("RED", 1)
1368                .literal("GREEN", 2)
1369                .build_minimal(),
1370        );
1371        let r = MinimalTypeObject::Enumerated(
1372            TypeObjectBuilder::enum_type("::E")
1373                .bit_bound(32)
1374                .literal("ROUGE", 1)
1375                .literal("VERT", 2)
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 cfg = AssignabilityConfig {
1384            ignore_literal_names: true,
1385            ..AssignabilityConfig::default()
1386        };
1387        let a = is_assignable(
1388            &TypeIdentifier::EquivalenceHashMinimal(wh),
1389            &TypeIdentifier::EquivalenceHashMinimal(rh),
1390            &reg,
1391            &cfg,
1392        );
1393        assert!(a.is_yes());
1394    }
1395
1396    #[test]
1397    fn enum_assignable_with_ignore_literal_names_via_writer_flag() {
1398        // Wenn der Writer `EnumTypeFlag::IGNORE_LITERAL_NAMES` setzt,
1399        // wirkt das fuer den Vergleich genauso wie der Config-Flag.
1400        let mut reg = reg();
1401        let mut w_e = TypeObjectBuilder::enum_type("::E")
1402            .bit_bound(32)
1403            .literal("RED", 1)
1404            .build_minimal();
1405        w_e.enum_flags = crate::type_object::flags::EnumTypeFlag(
1406            crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES,
1407        );
1408        let w = MinimalTypeObject::Enumerated(w_e);
1409        let r = MinimalTypeObject::Enumerated(
1410            TypeObjectBuilder::enum_type("::E")
1411                .bit_bound(32)
1412                .literal("ROUGE", 1)
1413                .build_minimal(),
1414        );
1415        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1416        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1417        reg.insert_minimal(wh, w);
1418        reg.insert_minimal(rh, r);
1419
1420        let a = is_assignable(
1421            &TypeIdentifier::EquivalenceHashMinimal(wh),
1422            &TypeIdentifier::EquivalenceHashMinimal(rh),
1423            &reg,
1424            &AssignabilityConfig::default(),
1425        );
1426        assert!(a.is_yes());
1427    }
1428
1429    #[test]
1430    fn enum_assignable_with_ignore_literal_names_via_reader_flag() {
1431        let mut reg = reg();
1432        let w = MinimalTypeObject::Enumerated(
1433            TypeObjectBuilder::enum_type("::E")
1434                .bit_bound(32)
1435                .literal("RED", 1)
1436                .build_minimal(),
1437        );
1438        let mut r_e = TypeObjectBuilder::enum_type("::E")
1439            .bit_bound(32)
1440            .literal("ROUGE", 1)
1441            .build_minimal();
1442        r_e.enum_flags = crate::type_object::flags::EnumTypeFlag(
1443            crate::type_object::flags::EnumTypeFlag::IGNORE_LITERAL_NAMES,
1444        );
1445        let r = MinimalTypeObject::Enumerated(r_e);
1446        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1447        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1448        reg.insert_minimal(wh, w);
1449        reg.insert_minimal(rh, r);
1450
1451        let a = is_assignable(
1452            &TypeIdentifier::EquivalenceHashMinimal(wh),
1453            &TypeIdentifier::EquivalenceHashMinimal(rh),
1454            &reg,
1455            &AssignabilityConfig::default(),
1456        );
1457        assert!(a.is_yes());
1458    }
1459
1460    #[test]
1461    fn struct_vs_enum_type_object_kinds_dont_match() {
1462        let mut reg = reg();
1463        let w = MinimalTypeObject::Struct(
1464            TypeObjectBuilder::struct_type("::X")
1465                .extensibility(Extensibility::Appendable)
1466                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1467                .build_minimal(),
1468        );
1469        let r = MinimalTypeObject::Enumerated(
1470            TypeObjectBuilder::enum_type("::X")
1471                .bit_bound(32)
1472                .literal("A", 1)
1473                .build_minimal(),
1474        );
1475        let wh = crate::hash::compute_minimal_hash(&w).unwrap();
1476        let rh = crate::hash::compute_minimal_hash(&r).unwrap();
1477        reg.insert_minimal(wh, w);
1478        reg.insert_minimal(rh, r);
1479
1480        let a = is_assignable(
1481            &TypeIdentifier::EquivalenceHashMinimal(wh),
1482            &TypeIdentifier::EquivalenceHashMinimal(rh),
1483            &reg,
1484            &AssignabilityConfig::default(),
1485        );
1486        assert!(!a.is_yes());
1487    }
1488
1489    #[test]
1490    fn appendable_struct_writer_smaller_than_reader_is_no() {
1491        let mut reg = reg();
1492        let writer = MinimalTypeObject::Struct(
1493            TypeObjectBuilder::struct_type("::X")
1494                .extensibility(Extensibility::Appendable)
1495                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1496                .build_minimal(),
1497        );
1498        let reader = MinimalTypeObject::Struct(
1499            TypeObjectBuilder::struct_type("::X")
1500                .extensibility(Extensibility::Appendable)
1501                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1502                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1503                .build_minimal(),
1504        );
1505        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1506        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1507        reg.insert_minimal(wh, writer);
1508        reg.insert_minimal(rh, reader);
1509
1510        assert!(
1511            !is_assignable(
1512                &TypeIdentifier::EquivalenceHashMinimal(wh),
1513                &TypeIdentifier::EquivalenceHashMinimal(rh),
1514                &reg,
1515                &AssignabilityConfig::default(),
1516            )
1517            .is_yes()
1518        );
1519    }
1520
1521    #[test]
1522    fn mutable_reader_member_missing_in_writer_non_optional_is_no() {
1523        let mut reg = reg();
1524        let writer = MinimalTypeObject::Struct(
1525            TypeObjectBuilder::struct_type("::X")
1526                .extensibility(Extensibility::Mutable)
1527                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1528                    m.id(2)
1529                })
1530                .build_minimal(),
1531        );
1532        let reader = MinimalTypeObject::Struct(
1533            TypeObjectBuilder::struct_type("::X")
1534                .extensibility(Extensibility::Mutable)
1535                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1536                    m.id(1) // NOT optional
1537                })
1538                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| {
1539                    m.id(2)
1540                })
1541                .build_minimal(),
1542        );
1543        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1544        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1545        reg.insert_minimal(wh, writer);
1546        reg.insert_minimal(rh, reader);
1547
1548        assert!(
1549            !is_assignable(
1550                &TypeIdentifier::EquivalenceHashMinimal(wh),
1551                &TypeIdentifier::EquivalenceHashMinimal(rh),
1552                &reg,
1553                &AssignabilityConfig::default(),
1554            )
1555            .is_yes()
1556        );
1557    }
1558
1559    #[test]
1560    fn final_struct_member_count_mismatch_is_no() {
1561        let mut reg = reg();
1562        let writer = MinimalTypeObject::Struct(
1563            TypeObjectBuilder::struct_type("::X")
1564                .extensibility(Extensibility::Final)
1565                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1566                .member("b", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1567                .build_minimal(),
1568        );
1569        let reader = MinimalTypeObject::Struct(
1570            TypeObjectBuilder::struct_type("::X")
1571                .extensibility(Extensibility::Final)
1572                .member("a", TypeIdentifier::Primitive(PrimitiveKind::Int32), |m| m)
1573                .build_minimal(),
1574        );
1575        let wh = crate::hash::compute_minimal_hash(&writer).unwrap();
1576        let rh = crate::hash::compute_minimal_hash(&reader).unwrap();
1577        reg.insert_minimal(wh, writer);
1578        reg.insert_minimal(rh, reader);
1579
1580        assert!(
1581            !is_assignable(
1582                &TypeIdentifier::EquivalenceHashMinimal(wh),
1583                &TypeIdentifier::EquivalenceHashMinimal(rh),
1584                &reg,
1585                &AssignabilityConfig::default(),
1586            )
1587            .is_yes()
1588        );
1589    }
1590
1591    #[test]
1592    fn primitive_widening_int16_to_int64_is_assignable_with_coercion() {
1593        let cfg = AssignabilityConfig {
1594            allow_type_coercion: true,
1595            ..Default::default()
1596        };
1597        assert!(primitive_compatible(PrimitiveKind::Int16, PrimitiveKind::Int64, &cfg).is_yes());
1598        assert!(primitive_compatible(PrimitiveKind::Byte, PrimitiveKind::Int32, &cfg).is_yes());
1599        assert!(
1600            primitive_compatible(PrimitiveKind::Float32, PrimitiveKind::Float64, &cfg).is_yes()
1601        );
1602    }
1603
1604    #[test]
1605    fn primitive_unwidening_is_rejected_even_with_coercion() {
1606        let cfg = AssignabilityConfig {
1607            allow_type_coercion: true,
1608            ..Default::default()
1609        };
1610        assert!(!primitive_compatible(PrimitiveKind::Float64, PrimitiveKind::Int32, &cfg).is_yes());
1611    }
1612
1613    #[test]
1614    fn assignable_is_yes_matches_expectation() {
1615        assert!(Assignable::Yes.is_yes());
1616        assert!(!Assignable::No("reason").is_yes());
1617    }
1618}