Skip to main content

tsz_solver/relations/
subtype_explain.rs

1//! Error Explanation API for subtype checking.
2//!
3//! This module implements the "slow path" for generating structured failure reasons
4//! when a subtype check fails. It re-runs subtype logic with tracing to produce
5//! detailed error diagnostics (TS2322, TS2739, TS2740, TS2741, etc.).
6
7use crate::diagnostics::SubtypeFailureReason;
8use crate::instantiation::instantiate::{TypeSubstitution, instantiate_type};
9use crate::relations::subtype::SubtypeChecker;
10use crate::type_resolver::TypeResolver;
11use crate::types::{
12    FunctionShape, IntrinsicKind, LiteralValue, ObjectShape, ObjectShapeId, PropertyInfo,
13    TupleElement, TypeId, Visibility,
14};
15use crate::utils;
16use crate::visitor::{
17    array_element_type, callable_shape_id, function_shape_id, intrinsic_kind, literal_value,
18    object_shape_id, object_with_index_shape_id, tuple_list_id, union_list_id,
19};
20
21impl<'a, R: TypeResolver> SubtypeChecker<'a, R> {
22    /// Explain why `source` is not assignable to `target`.
23    ///
24    /// This is the "slow path" - called only when `is_assignable_to` returns false
25    /// and we need to generate an error message. Re-runs the subtype logic with
26    /// tracing enabled to produce a structured failure reason.
27    ///
28    /// Returns `None` if the types are actually compatible (shouldn't happen
29    /// if called correctly after a failed check).
30    pub fn explain_failure(
31        &mut self,
32        source: TypeId,
33        target: TypeId,
34    ) -> Option<SubtypeFailureReason> {
35        let pair = (source, target);
36        match self.guard.enter(pair) {
37            crate::recursion::RecursionResult::Entered => {}
38            crate::recursion::RecursionResult::Cycle
39            | crate::recursion::RecursionResult::DepthExceeded
40            | crate::recursion::RecursionResult::IterationExceeded => {
41                return Some(SubtypeFailureReason::TypeMismatch {
42                    source_type: source,
43                    target_type: target,
44                });
45            }
46        }
47        let result = self.explain_failure_guarded(source, target);
48        self.guard.leave(pair);
49        result
50    }
51
52    fn explain_failure_guarded(
53        &mut self,
54        source: TypeId,
55        target: TypeId,
56    ) -> Option<SubtypeFailureReason> {
57        // Fast path: if types are equal, no failure
58        if source == target {
59            return None;
60        }
61
62        if !self.strict_null_checks && source.is_nullish() {
63            return None;
64        }
65
66        // Check for any/unknown/never special cases
67        if source.is_any() || target.is_any_or_unknown() {
68            return None;
69        }
70        if source.is_never() {
71            return None;
72        }
73        // ERROR types should produce ErrorType failure reason
74        if source.is_error() || target.is_error() {
75            return Some(SubtypeFailureReason::ErrorType {
76                source_type: source,
77                target_type: target,
78            });
79        }
80
81        // Note: Weak type checking is handled by CompatChecker (compat.rs:167-170).
82        // Removed redundant check here to avoid double-checking which caused false positives.
83
84        self.explain_failure_inner(source, target)
85    }
86
87    fn explain_failure_inner(
88        &mut self,
89        source: TypeId,
90        target: TypeId,
91    ) -> Option<SubtypeFailureReason> {
92        // Resolve ref types (interfaces, type aliases) to their structural forms.
93        // Without this, interface types (TypeData::Lazy) won't match the object_shape_id
94        // check below, causing TS2322 instead of TS2741/TS2739/TS2740.
95        let mut resolved_source = self.resolve_ref_type(source);
96        let mut resolved_target = self.resolve_ref_type(target);
97
98        // Expand applications (like Array<number>, MyGeneric<string>) to structural forms
99        if let Some(app_id) = crate::visitor::application_id(self.interner, resolved_source)
100            && let Some(expanded) = self.try_expand_application(app_id)
101        {
102            resolved_source = self.resolve_ref_type(expanded);
103        }
104        if let Some(app_id) = crate::visitor::application_id(self.interner, resolved_target)
105            && let Some(expanded) = self.try_expand_application(app_id)
106        {
107            resolved_target = self.resolve_ref_type(expanded);
108        }
109
110        if let Some(shape) = self.apparent_primitive_shape_for_type(resolved_source) {
111            if let Some(t_shape_id) = object_shape_id(self.interner, resolved_target) {
112                let t_shape = self.interner.object_shape(t_shape_id);
113                return self.explain_object_failure(
114                    source,
115                    target,
116                    &shape.properties,
117                    None,
118                    &t_shape.properties,
119                );
120            }
121            if let Some(t_shape_id) = object_with_index_shape_id(self.interner, resolved_target) {
122                let t_shape = self.interner.object_shape(t_shape_id);
123                return self.explain_indexed_object_failure(source, target, &shape, None, &t_shape);
124            }
125        }
126
127        if let (Some(s_shape_id), Some(t_shape_id)) = (
128            object_shape_id(self.interner, resolved_source),
129            object_shape_id(self.interner, resolved_target),
130        ) {
131            let s_shape = self.interner.object_shape(s_shape_id);
132            let t_shape = self.interner.object_shape(t_shape_id);
133            return self.explain_object_failure(
134                source,
135                target,
136                &s_shape.properties,
137                Some(s_shape_id),
138                &t_shape.properties,
139            );
140        }
141
142        if let (Some(s_shape_id), Some(t_shape_id)) = (
143            object_with_index_shape_id(self.interner, resolved_source),
144            object_with_index_shape_id(self.interner, resolved_target),
145        ) {
146            let s_shape = self.interner.object_shape(s_shape_id);
147            let t_shape = self.interner.object_shape(t_shape_id);
148            return self.explain_indexed_object_failure(
149                source,
150                target,
151                &s_shape,
152                Some(s_shape_id),
153                &t_shape,
154            );
155        }
156
157        if let (Some(s_shape_id), Some(t_shape_id)) = (
158            object_with_index_shape_id(self.interner, resolved_source),
159            object_shape_id(self.interner, resolved_target),
160        ) {
161            let s_shape = self.interner.object_shape(s_shape_id);
162            let t_shape = self.interner.object_shape(t_shape_id);
163            return self.explain_object_with_index_to_object_failure(
164                source,
165                target,
166                &s_shape,
167                s_shape_id,
168                &t_shape.properties,
169            );
170        }
171
172        if let (Some(s_shape_id), Some(t_shape_id)) = (
173            object_shape_id(self.interner, resolved_source),
174            object_with_index_shape_id(self.interner, resolved_target),
175        ) {
176            let s_shape = self.interner.object_shape(s_shape_id);
177            let t_shape = self.interner.object_shape(t_shape_id);
178            if let Some(reason) = self.explain_object_failure(
179                source,
180                target,
181                &s_shape.properties,
182                Some(s_shape_id),
183                &t_shape.properties,
184            ) {
185                return Some(reason);
186            }
187            if let Some(ref number_idx) = t_shape.number_index {
188                return Some(SubtypeFailureReason::IndexSignatureMismatch {
189                    index_kind: "number",
190                    source_value_type: TypeId::ANY,
191                    target_value_type: number_idx.value_type,
192                });
193            }
194            if let Some(ref string_idx) = t_shape.string_index {
195                for prop in &s_shape.properties {
196                    let prop_type = self.optional_property_type(prop);
197                    if !self
198                        .check_subtype(prop_type, string_idx.value_type)
199                        .is_true()
200                    {
201                        return Some(SubtypeFailureReason::IndexSignatureMismatch {
202                            index_kind: "string",
203                            source_value_type: prop_type,
204                            target_value_type: string_idx.value_type,
205                        });
206                    }
207                }
208            }
209            return None;
210        }
211
212        // Object source vs array target: resolve Array<T> to its interface properties
213        // and find missing members. TSC emits TS2740 here (missing properties from array).
214        if let Some(t_elem) = array_element_type(self.interner, resolved_target) {
215            let s_shape_id = object_shape_id(self.interner, resolved_source)
216                .or_else(|| object_with_index_shape_id(self.interner, resolved_source));
217            if let Some(s_sid) = s_shape_id
218                && let Some(array_base) = self.resolver.get_array_base_type()
219            {
220                let params = self.resolver.get_array_base_type_params();
221                let instantiated = if params.is_empty() {
222                    array_base
223                } else {
224                    let subst = TypeSubstitution::from_args(self.interner, params, &[t_elem]);
225                    instantiate_type(self.interner, array_base, &subst)
226                };
227                let resolved_inst = self.resolve_ref_type(instantiated);
228                // The Array interface may resolve to an object shape or a callable shape
229                // (with properties like length, push, concat, etc.)
230                let s_shape = self.interner.object_shape(s_sid);
231                if let Some(t_obj_sid) = object_shape_id(self.interner, resolved_inst)
232                    .or_else(|| object_with_index_shape_id(self.interner, resolved_inst))
233                {
234                    let t_shape = self.interner.object_shape(t_obj_sid);
235                    return self.explain_object_failure(
236                        source,
237                        target,
238                        &s_shape.properties,
239                        Some(s_sid),
240                        &t_shape.properties,
241                    );
242                }
243                // Array interface resolved to a callable shape — use its properties
244                if let Some(callable_sid) = callable_shape_id(self.interner, resolved_inst) {
245                    let callable = self.interner.callable_shape(callable_sid);
246                    if !callable.properties.is_empty() {
247                        return self.explain_object_failure(
248                            source,
249                            target,
250                            &s_shape.properties,
251                            Some(s_sid),
252                            &callable.properties,
253                        );
254                    }
255                }
256            }
257        }
258
259        // Array source vs Object target: resolve Array<T> to its interface properties
260        // and find missing members. TSC emits TS2739/TS2741 here.
261        if let Some(s_elem) = array_element_type(self.interner, resolved_source) {
262            let t_shape_id = object_shape_id(self.interner, resolved_target)
263                .or_else(|| object_with_index_shape_id(self.interner, resolved_target));
264            if let Some(t_sid) = t_shape_id
265                && let Some(array_base) = self.resolver.get_array_base_type()
266            {
267                let params = self.resolver.get_array_base_type_params();
268                let instantiated = if params.is_empty() {
269                    array_base
270                } else {
271                    let subst = TypeSubstitution::from_args(self.interner, params, &[s_elem]);
272                    instantiate_type(self.interner, array_base, &subst)
273                };
274                let resolved_inst = self.resolve_ref_type(instantiated);
275                // The Array interface may resolve to an object shape or a callable shape
276                let t_shape = self.interner.object_shape(t_sid);
277                if let Some(s_obj_sid) = object_shape_id(self.interner, resolved_inst)
278                    .or_else(|| object_with_index_shape_id(self.interner, resolved_inst))
279                {
280                    let s_shape = self.interner.object_shape(s_obj_sid);
281                    return self.explain_object_failure(
282                        source,
283                        target,
284                        &s_shape.properties,
285                        Some(s_obj_sid),
286                        &t_shape.properties,
287                    );
288                }
289                if let Some(callable_sid) = callable_shape_id(self.interner, resolved_inst) {
290                    let callable = self.interner.callable_shape(callable_sid);
291                    if !callable.properties.is_empty() {
292                        return self.explain_object_failure(
293                            source,
294                            target,
295                            &callable.properties,
296                            None,
297                            &t_shape.properties,
298                        );
299                    }
300                }
301            }
302        }
303
304        if let (Some(s_fn_id), Some(t_fn_id)) = (
305            function_shape_id(self.interner, source),
306            function_shape_id(self.interner, target),
307        ) {
308            let s_fn = self.interner.function_shape(s_fn_id);
309            let t_fn = self.interner.function_shape(t_fn_id);
310            return self.explain_function_failure(&s_fn, &t_fn);
311        }
312
313        if let (Some(s_elem), Some(t_elem)) = (
314            array_element_type(self.interner, source),
315            array_element_type(self.interner, target),
316        ) {
317            if !self.check_subtype(s_elem, t_elem).is_true() {
318                return Some(SubtypeFailureReason::ArrayElementMismatch {
319                    source_element: s_elem,
320                    target_element: t_elem,
321                });
322            }
323            return None;
324        }
325
326        if let (Some(s_elems), Some(t_elems)) = (
327            tuple_list_id(self.interner, source),
328            tuple_list_id(self.interner, target),
329        ) {
330            let s_elems = self.interner.tuple_list(s_elems);
331            let t_elems = self.interner.tuple_list(t_elems);
332            return self.explain_tuple_failure(&s_elems, &t_elems);
333        }
334
335        if let Some(members) = union_list_id(self.interner, target) {
336            let members = self.interner.type_list(members);
337            return Some(SubtypeFailureReason::NoUnionMemberMatches {
338                source_type: source,
339                target_union_members: members.as_ref().to_vec(),
340            });
341        }
342
343        if let (Some(s_kind), Some(t_kind)) = (
344            intrinsic_kind(self.interner, source),
345            intrinsic_kind(self.interner, target),
346        ) {
347            if s_kind != t_kind {
348                return Some(SubtypeFailureReason::IntrinsicTypeMismatch {
349                    source_type: source,
350                    target_type: target,
351                });
352            }
353            return None;
354        }
355
356        if literal_value(self.interner, source).is_some()
357            && literal_value(self.interner, target).is_some()
358        {
359            return Some(SubtypeFailureReason::LiteralTypeMismatch {
360                source_type: source,
361                target_type: target,
362            });
363        }
364
365        if let (Some(lit), Some(t_kind)) = (
366            literal_value(self.interner, source),
367            intrinsic_kind(self.interner, target),
368        ) {
369            let compatible = match lit {
370                LiteralValue::String(_) => t_kind == IntrinsicKind::String,
371                LiteralValue::Number(_) => t_kind == IntrinsicKind::Number,
372                LiteralValue::BigInt(_) => t_kind == IntrinsicKind::Bigint,
373                LiteralValue::Boolean(_) => t_kind == IntrinsicKind::Boolean,
374            };
375            if !compatible {
376                return Some(SubtypeFailureReason::LiteralTypeMismatch {
377                    source_type: source,
378                    target_type: target,
379                });
380            }
381            return None;
382        }
383
384        if intrinsic_kind(self.interner, source).is_some()
385            && literal_value(self.interner, target).is_some()
386        {
387            return Some(SubtypeFailureReason::TypeMismatch {
388                source_type: source,
389                target_type: target,
390            });
391        }
392
393        Some(SubtypeFailureReason::TypeMismatch {
394            source_type: source,
395            target_type: target,
396        })
397    }
398
399    /// Explain why an object type assignment failed.
400    fn explain_object_failure(
401        &mut self,
402        source: TypeId,
403        target: TypeId,
404        source_props: &[PropertyInfo],
405        source_shape_id: Option<ObjectShapeId>,
406        target_props: &[PropertyInfo],
407    ) -> Option<SubtypeFailureReason> {
408        // First pass: collect all missing required property names.
409        // tsc emits TS2739 (multiple missing) or TS2741 (single missing) before
410        // checking property type compatibility.
411        let mut missing_props: Vec<tsz_common::interner::Atom> = Vec::new();
412        for t_prop in target_props {
413            if !t_prop.optional {
414                let s_prop = self.lookup_property(source_props, source_shape_id, t_prop.name);
415                if s_prop.is_none() {
416                    missing_props.push(t_prop.name);
417                }
418            }
419        }
420
421        if missing_props.len() > 1 {
422            return Some(SubtypeFailureReason::MissingProperties {
423                property_names: missing_props,
424                source_type: source,
425                target_type: target,
426            });
427        }
428        if missing_props.len() == 1 {
429            return Some(SubtypeFailureReason::MissingProperty {
430                property_name: missing_props[0],
431                source_type: source,
432                target_type: target,
433            });
434        }
435
436        // Second pass: check property type compatibility
437        for t_prop in target_props {
438            let s_prop = self.lookup_property(source_props, source_shape_id, t_prop.name);
439
440            if let Some(sp) = s_prop {
441                // Check nominal identity for private/protected properties
442                if t_prop.visibility != Visibility::Public {
443                    if sp.parent_id != t_prop.parent_id {
444                        return Some(SubtypeFailureReason::PropertyNominalMismatch {
445                            property_name: t_prop.name,
446                        });
447                    }
448                }
449                // Cannot assign private/protected source to public target
450                else if sp.visibility != Visibility::Public {
451                    return Some(SubtypeFailureReason::PropertyVisibilityMismatch {
452                        property_name: t_prop.name,
453                        source_visibility: sp.visibility,
454                        target_visibility: t_prop.visibility,
455                    });
456                }
457
458                // Check optional/required mismatch
459                if sp.optional && !t_prop.optional {
460                    return Some(SubtypeFailureReason::OptionalPropertyRequired {
461                        property_name: t_prop.name,
462                    });
463                }
464
465                // Check property type compatibility
466                let source_type = self.optional_property_type(sp);
467                let target_type = self.optional_property_type(t_prop);
468                let allow_bivariant = sp.is_method || t_prop.is_method;
469                if !self
470                    .check_subtype_with_method_variance(source_type, target_type, allow_bivariant)
471                    .is_true()
472                {
473                    let nested = self.explain_failure_with_method_variance(
474                        source_type,
475                        target_type,
476                        allow_bivariant,
477                    );
478                    return Some(SubtypeFailureReason::PropertyTypeMismatch {
479                        property_name: t_prop.name,
480                        source_property_type: source_type,
481                        target_property_type: target_type,
482                        nested_reason: nested.map(Box::new),
483                    });
484                }
485                if !t_prop.readonly
486                    && (sp.write_type != sp.type_id || t_prop.write_type != t_prop.type_id)
487                {
488                    let source_write = self.optional_property_write_type(sp);
489                    let target_write = self.optional_property_write_type(t_prop);
490                    if !self
491                        .check_subtype_with_method_variance(
492                            target_write,
493                            source_write,
494                            allow_bivariant,
495                        )
496                        .is_true()
497                    {
498                        let nested = self.explain_failure_with_method_variance(
499                            target_write,
500                            source_write,
501                            allow_bivariant,
502                        );
503                        return Some(SubtypeFailureReason::PropertyTypeMismatch {
504                            property_name: t_prop.name,
505                            source_property_type: source_write,
506                            target_property_type: target_write,
507                            nested_reason: nested.map(Box::new),
508                        });
509                    }
510                }
511            }
512        }
513
514        None
515    }
516
517    /// Explain why an indexed object type assignment failed.
518    fn explain_indexed_object_failure(
519        &mut self,
520        source: TypeId,
521        target: TypeId,
522        source_shape: &ObjectShape,
523        source_shape_id: Option<ObjectShapeId>,
524        target_shape: &ObjectShape,
525    ) -> Option<SubtypeFailureReason> {
526        // First check properties
527        if let Some(reason) = self.explain_object_failure(
528            source,
529            target,
530            &source_shape.properties,
531            source_shape_id,
532            &target_shape.properties,
533        ) {
534            return Some(reason);
535        }
536
537        // Check string index signature
538        if let Some(ref t_string_idx) = target_shape.string_index {
539            match &source_shape.string_index {
540                Some(s_string_idx) => {
541                    if s_string_idx.readonly && !t_string_idx.readonly {
542                        return Some(SubtypeFailureReason::TypeMismatch {
543                            source_type: source,
544                            target_type: target,
545                        });
546                    }
547                    if !self
548                        .check_subtype(s_string_idx.value_type, t_string_idx.value_type)
549                        .is_true()
550                    {
551                        return Some(SubtypeFailureReason::IndexSignatureMismatch {
552                            index_kind: "string",
553                            source_value_type: s_string_idx.value_type,
554                            target_value_type: t_string_idx.value_type,
555                        });
556                    }
557                }
558                None => {
559                    for prop in &source_shape.properties {
560                        let prop_type = self.optional_property_type(prop);
561                        if !self
562                            .check_subtype(prop_type, t_string_idx.value_type)
563                            .is_true()
564                        {
565                            return Some(SubtypeFailureReason::IndexSignatureMismatch {
566                                index_kind: "string",
567                                source_value_type: prop_type,
568                                target_value_type: t_string_idx.value_type,
569                            });
570                        }
571                    }
572                }
573            }
574        }
575
576        // Check number index signature
577        if let Some(ref t_number_idx) = target_shape.number_index
578            && let Some(ref s_number_idx) = source_shape.number_index
579        {
580            if s_number_idx.readonly && !t_number_idx.readonly {
581                return Some(SubtypeFailureReason::TypeMismatch {
582                    source_type: source,
583                    target_type: target,
584                });
585            }
586            if !self
587                .check_subtype(s_number_idx.value_type, t_number_idx.value_type)
588                .is_true()
589            {
590                return Some(SubtypeFailureReason::IndexSignatureMismatch {
591                    index_kind: "number",
592                    source_value_type: s_number_idx.value_type,
593                    target_value_type: t_number_idx.value_type,
594                });
595            }
596        }
597
598        if let Some(reason) =
599            self.explain_properties_against_index_signatures(&source_shape.properties, target_shape)
600        {
601            return Some(reason);
602        }
603
604        None
605    }
606
607    fn explain_object_with_index_to_object_failure(
608        &mut self,
609        source: TypeId,
610        target: TypeId,
611        source_shape: &ObjectShape,
612        source_shape_id: ObjectShapeId,
613        target_props: &[PropertyInfo],
614    ) -> Option<SubtypeFailureReason> {
615        for t_prop in target_props {
616            if let Some(sp) =
617                self.lookup_property(&source_shape.properties, Some(source_shape_id), t_prop.name)
618            {
619                // Check nominal identity for private/protected properties
620                // Private and protected members are nominally typed - they must
621                // originate from the same declaration (same parent_id)
622                if t_prop.visibility != Visibility::Public {
623                    if sp.parent_id != t_prop.parent_id {
624                        return Some(SubtypeFailureReason::PropertyNominalMismatch {
625                            property_name: t_prop.name,
626                        });
627                    }
628                }
629                // Cannot assign private/protected source to public target
630                else if sp.visibility != Visibility::Public {
631                    return Some(SubtypeFailureReason::PropertyVisibilityMismatch {
632                        property_name: t_prop.name,
633                        source_visibility: sp.visibility,
634                        target_visibility: t_prop.visibility,
635                    });
636                }
637
638                if sp.optional && !t_prop.optional {
639                    return Some(SubtypeFailureReason::OptionalPropertyRequired {
640                        property_name: t_prop.name,
641                    });
642                }
643                // NOTE: TypeScript allows readonly source to satisfy mutable target
644                // (readonly is a constraint on the reference, not structural compatibility)
645
646                let source_type = self.optional_property_type(sp);
647                let target_type = self.optional_property_type(t_prop);
648                let allow_bivariant = sp.is_method || t_prop.is_method;
649                if !self
650                    .check_subtype_with_method_variance(source_type, target_type, allow_bivariant)
651                    .is_true()
652                {
653                    let nested = self.explain_failure_with_method_variance(
654                        source_type,
655                        target_type,
656                        allow_bivariant,
657                    );
658                    return Some(SubtypeFailureReason::PropertyTypeMismatch {
659                        property_name: t_prop.name,
660                        source_property_type: source_type,
661                        target_property_type: target_type,
662                        nested_reason: nested.map(Box::new),
663                    });
664                }
665                if !t_prop.readonly
666                    && (sp.write_type != sp.type_id || t_prop.write_type != t_prop.type_id)
667                {
668                    let source_write = self.optional_property_write_type(sp);
669                    let target_write = self.optional_property_write_type(t_prop);
670                    if !self
671                        .check_subtype_with_method_variance(
672                            target_write,
673                            source_write,
674                            allow_bivariant,
675                        )
676                        .is_true()
677                    {
678                        let nested = self.explain_failure_with_method_variance(
679                            target_write,
680                            source_write,
681                            allow_bivariant,
682                        );
683                        return Some(SubtypeFailureReason::PropertyTypeMismatch {
684                            property_name: t_prop.name,
685                            source_property_type: source_write,
686                            target_property_type: target_write,
687                            nested_reason: nested.map(Box::new),
688                        });
689                    }
690                }
691                continue;
692            }
693
694            let mut checked = false;
695            let target_type = self.optional_property_type(t_prop);
696
697            if utils::is_numeric_property_name(self.interner, t_prop.name)
698                && let Some(number_idx) = &source_shape.number_index
699            {
700                checked = true;
701                if number_idx.readonly && !t_prop.readonly {
702                    return Some(SubtypeFailureReason::ReadonlyPropertyMismatch {
703                        property_name: t_prop.name,
704                    });
705                }
706                if !self
707                    .check_subtype_with_method_variance(
708                        number_idx.value_type,
709                        target_type,
710                        t_prop.is_method,
711                    )
712                    .is_true()
713                {
714                    return Some(SubtypeFailureReason::IndexSignatureMismatch {
715                        index_kind: "number",
716                        source_value_type: number_idx.value_type,
717                        target_value_type: target_type,
718                    });
719                }
720            }
721
722            if let Some(string_idx) = &source_shape.string_index {
723                checked = true;
724                if string_idx.readonly && !t_prop.readonly {
725                    return Some(SubtypeFailureReason::ReadonlyPropertyMismatch {
726                        property_name: t_prop.name,
727                    });
728                }
729                if !self
730                    .check_subtype_with_method_variance(
731                        string_idx.value_type,
732                        target_type,
733                        t_prop.is_method,
734                    )
735                    .is_true()
736                {
737                    return Some(SubtypeFailureReason::IndexSignatureMismatch {
738                        index_kind: "string",
739                        source_value_type: string_idx.value_type,
740                        target_value_type: target_type,
741                    });
742                }
743            }
744
745            if !checked && !t_prop.optional {
746                return Some(SubtypeFailureReason::MissingProperty {
747                    property_name: t_prop.name,
748                    source_type: source,
749                    target_type: target,
750                });
751            }
752        }
753
754        None
755    }
756
757    fn explain_properties_against_index_signatures(
758        &mut self,
759        source: &[PropertyInfo],
760        target: &ObjectShape,
761    ) -> Option<SubtypeFailureReason> {
762        let string_index = target.string_index.as_ref();
763        let number_index = target.number_index.as_ref();
764
765        if string_index.is_none() && number_index.is_none() {
766            return None;
767        }
768
769        for prop in source {
770            let prop_type = self.optional_property_type(prop);
771            let allow_bivariant = prop.is_method;
772
773            if let Some(number_idx) = number_index {
774                let is_numeric = utils::is_numeric_property_name(self.interner, prop.name);
775                if is_numeric {
776                    if !number_idx.readonly && prop.readonly {
777                        return Some(SubtypeFailureReason::ReadonlyPropertyMismatch {
778                            property_name: prop.name,
779                        });
780                    }
781                    if !self
782                        .check_subtype_with_method_variance(
783                            prop_type,
784                            number_idx.value_type,
785                            allow_bivariant,
786                        )
787                        .is_true()
788                    {
789                        return Some(SubtypeFailureReason::IndexSignatureMismatch {
790                            index_kind: "number",
791                            source_value_type: prop_type,
792                            target_value_type: number_idx.value_type,
793                        });
794                    }
795                }
796            }
797
798            if let Some(string_idx) = string_index {
799                if !string_idx.readonly && prop.readonly {
800                    return Some(SubtypeFailureReason::ReadonlyPropertyMismatch {
801                        property_name: prop.name,
802                    });
803                }
804                if !self
805                    .check_subtype_with_method_variance(
806                        prop_type,
807                        string_idx.value_type,
808                        allow_bivariant,
809                    )
810                    .is_true()
811                {
812                    return Some(SubtypeFailureReason::IndexSignatureMismatch {
813                        index_kind: "string",
814                        source_value_type: prop_type,
815                        target_value_type: string_idx.value_type,
816                    });
817                }
818            }
819        }
820
821        None
822    }
823
824    /// Explain why a function type assignment failed.
825    fn explain_function_failure(
826        &mut self,
827        source: &FunctionShape,
828        target: &FunctionShape,
829    ) -> Option<SubtypeFailureReason> {
830        // Check return type
831        if !(self
832            .check_subtype(source.return_type, target.return_type)
833            .is_true()
834            || self.allow_void_return && target.return_type == TypeId::VOID)
835        {
836            let nested = self.explain_failure(source.return_type, target.return_type);
837            return Some(SubtypeFailureReason::ReturnTypeMismatch {
838                source_return: source.return_type,
839                target_return: target.return_type,
840                nested_reason: nested.map(Box::new),
841            });
842        }
843
844        // Check parameter count
845        let target_has_rest = target.params.last().is_some_and(|p| p.rest);
846        let rest_elem_type = if target_has_rest {
847            target
848                .params
849                .last()
850                .map(|param| self.get_array_element_type(param.type_id))
851        } else {
852            None
853        };
854        let rest_is_top = self.allow_bivariant_rest
855            && matches!(rest_elem_type, Some(TypeId::ANY | TypeId::UNKNOWN));
856        let source_required = self.required_param_count(&source.params);
857        let target_required = self.required_param_count(&target.params);
858        // When the target has rest parameters, skip arity check entirely —
859        // the rest parameter can accept any number of arguments, and type
860        // compatibility of extra source params is checked later against the rest element type.
861        // This aligns with check_function_subtype which also skips the arity check when
862        // target_has_rest is true.
863        let too_many_params = !self.allow_bivariant_param_count
864            && !rest_is_top
865            && !target_has_rest
866            && source_required > target_required;
867        if !target_has_rest && too_many_params {
868            return Some(SubtypeFailureReason::TooManyParameters {
869                source_count: source_required,
870                target_count: target_required,
871            });
872        }
873
874        // Check parameter types
875        let source_has_rest = source.params.last().is_some_and(|p| p.rest);
876        let target_fixed_count = if target_has_rest {
877            target.params.len().saturating_sub(1)
878        } else {
879            target.params.len()
880        };
881        let source_fixed_count = if source_has_rest {
882            source.params.len().saturating_sub(1)
883        } else {
884            source.params.len()
885        };
886        let fixed_compare_count = std::cmp::min(source_fixed_count, target_fixed_count);
887        // Constructor and method signatures are bivariant even with strictFunctionTypes
888        let is_method_or_ctor =
889            source.is_method || target.is_method || source.is_constructor || target.is_constructor;
890        for i in 0..fixed_compare_count {
891            let s_param = &source.params[i];
892            let t_param = &target.params[i];
893            // Check parameter compatibility (contravariant in strict mode, bivariant in legacy)
894            if !self.are_parameters_compatible_impl(
895                s_param.type_id,
896                t_param.type_id,
897                is_method_or_ctor,
898            ) {
899                return Some(SubtypeFailureReason::ParameterTypeMismatch {
900                    param_index: i,
901                    source_param: s_param.type_id,
902                    target_param: t_param.type_id,
903                });
904            }
905        }
906
907        if target_has_rest {
908            let Some(rest_elem_type) = rest_elem_type else {
909                return None; // Invalid rest parameter
910            };
911            if rest_is_top {
912                return None;
913            }
914
915            for i in target_fixed_count..source_fixed_count {
916                let s_param = &source.params[i];
917                if !self.are_parameters_compatible_impl(
918                    s_param.type_id,
919                    rest_elem_type,
920                    is_method_or_ctor,
921                ) {
922                    return Some(SubtypeFailureReason::ParameterTypeMismatch {
923                        param_index: i,
924                        source_param: s_param.type_id,
925                        target_param: rest_elem_type,
926                    });
927                }
928            }
929
930            if source_has_rest {
931                let s_rest_param = source.params.last()?;
932                let s_rest_elem = self.get_array_element_type(s_rest_param.type_id);
933                if !self.are_parameters_compatible_impl(
934                    s_rest_elem,
935                    rest_elem_type,
936                    is_method_or_ctor,
937                ) {
938                    return Some(SubtypeFailureReason::ParameterTypeMismatch {
939                        param_index: source_fixed_count,
940                        source_param: s_rest_elem,
941                        target_param: rest_elem_type,
942                    });
943                }
944            }
945        }
946
947        if source_has_rest {
948            let rest_param = source.params.last()?;
949            let rest_elem_type = self.get_array_element_type(rest_param.type_id);
950            let rest_is_top = self.allow_bivariant_rest && rest_elem_type.is_any_or_unknown();
951
952            if !rest_is_top {
953                for i in source_fixed_count..target_fixed_count {
954                    let t_param = &target.params[i];
955                    if !self.are_parameters_compatible(rest_elem_type, t_param.type_id) {
956                        return Some(SubtypeFailureReason::ParameterTypeMismatch {
957                            param_index: i,
958                            source_param: rest_elem_type,
959                            target_param: t_param.type_id,
960                        });
961                    }
962                }
963            }
964        }
965
966        None
967    }
968
969    /// Explain why a tuple type assignment failed.
970    fn explain_tuple_failure(
971        &mut self,
972        source: &[TupleElement],
973        target: &[TupleElement],
974    ) -> Option<SubtypeFailureReason> {
975        let source_required = source.iter().filter(|e| !e.optional && !e.rest).count();
976        let target_required = target.iter().filter(|e| !e.optional && !e.rest).count();
977
978        if source_required < target_required {
979            return Some(SubtypeFailureReason::TupleElementMismatch {
980                source_count: source.len(),
981                target_count: target.len(),
982            });
983        }
984
985        for (i, t_elem) in target.iter().enumerate() {
986            if t_elem.rest {
987                let expansion = self.expand_tuple_rest(t_elem.type_id);
988                let outer_tail = &target[i + 1..];
989                // Combined suffix = expansion.tail + outer_tail
990                let combined_suffix: Vec<_> = expansion
991                    .tail
992                    .iter()
993                    .chain(outer_tail.iter())
994                    .cloned()
995                    .collect();
996
997                let mut source_end = source.len();
998                for tail_elem in combined_suffix.iter().rev() {
999                    if source_end <= i {
1000                        if !tail_elem.optional {
1001                            return Some(SubtypeFailureReason::TupleElementMismatch {
1002                                source_count: source.len(),
1003                                target_count: target.len(),
1004                            });
1005                        }
1006                        break;
1007                    }
1008                    let s_elem = &source[source_end - 1];
1009                    if s_elem.rest {
1010                        if !tail_elem.optional {
1011                            return Some(SubtypeFailureReason::TupleElementMismatch {
1012                                source_count: source.len(),
1013                                target_count: target.len(),
1014                            });
1015                        }
1016                        break;
1017                    }
1018                    let assignable = self
1019                        .check_subtype(s_elem.type_id, tail_elem.type_id)
1020                        .is_true();
1021                    if tail_elem.optional && !assignable {
1022                        break;
1023                    }
1024                    if !assignable {
1025                        return Some(SubtypeFailureReason::TupleElementTypeMismatch {
1026                            index: source_end - 1,
1027                            source_element: s_elem.type_id,
1028                            target_element: tail_elem.type_id,
1029                        });
1030                    }
1031                    source_end -= 1;
1032                }
1033
1034                let mut source_iter = source.iter().enumerate().take(source_end).skip(i);
1035
1036                for t_fixed in &expansion.fixed {
1037                    match source_iter.next() {
1038                        Some((j, s_elem)) => {
1039                            if s_elem.rest {
1040                                return Some(SubtypeFailureReason::TupleElementMismatch {
1041                                    source_count: source.len(),
1042                                    target_count: target.len(),
1043                                });
1044                            }
1045                            if !self
1046                                .check_subtype(s_elem.type_id, t_fixed.type_id)
1047                                .is_true()
1048                            {
1049                                return Some(SubtypeFailureReason::TupleElementTypeMismatch {
1050                                    index: j,
1051                                    source_element: s_elem.type_id,
1052                                    target_element: t_fixed.type_id,
1053                                });
1054                            }
1055                        }
1056                        None => {
1057                            if !t_fixed.optional {
1058                                return Some(SubtypeFailureReason::TupleElementMismatch {
1059                                    source_count: source.len(),
1060                                    target_count: target.len(),
1061                                });
1062                            }
1063                        }
1064                    }
1065                }
1066
1067                if let Some(variadic) = expansion.variadic {
1068                    let variadic_array = self.interner.array(variadic);
1069                    for (j, s_elem) in source_iter {
1070                        let target_type = if s_elem.rest {
1071                            variadic_array
1072                        } else {
1073                            variadic
1074                        };
1075                        if !self.check_subtype(s_elem.type_id, target_type).is_true() {
1076                            return Some(SubtypeFailureReason::TupleElementTypeMismatch {
1077                                index: j,
1078                                source_element: s_elem.type_id,
1079                                target_element: target_type,
1080                            });
1081                        }
1082                    }
1083                    return None;
1084                }
1085
1086                if source_iter.next().is_some() {
1087                    return Some(SubtypeFailureReason::TupleElementMismatch {
1088                        source_count: source.len(),
1089                        target_count: target.len(),
1090                    });
1091                }
1092                return None;
1093            }
1094
1095            if let Some(s_elem) = source.get(i) {
1096                if s_elem.rest {
1097                    // Source has rest but target expects fixed element
1098                    return Some(SubtypeFailureReason::TupleElementMismatch {
1099                        source_count: source.len(), // Approximate "infinity"
1100                        target_count: target.len(),
1101                    });
1102                }
1103
1104                if !self.check_subtype(s_elem.type_id, t_elem.type_id).is_true() {
1105                    return Some(SubtypeFailureReason::TupleElementTypeMismatch {
1106                        index: i,
1107                        source_element: s_elem.type_id,
1108                        target_element: t_elem.type_id,
1109                    });
1110                }
1111            } else if !t_elem.optional {
1112                return Some(SubtypeFailureReason::TupleElementMismatch {
1113                    source_count: source.len(),
1114                    target_count: target.len(),
1115                });
1116            }
1117        }
1118
1119        // Target is closed. Check for extra elements in source.
1120        if source.len() > target.len() {
1121            return Some(SubtypeFailureReason::TupleElementMismatch {
1122                source_count: source.len(),
1123                target_count: target.len(),
1124            });
1125        }
1126
1127        for s_elem in source {
1128            if s_elem.rest {
1129                return Some(SubtypeFailureReason::TupleElementMismatch {
1130                    source_count: source.len(), // implies open
1131                    target_count: target.len(),
1132                });
1133            }
1134        }
1135
1136        None
1137    }
1138
1139    /// Check if two types are structurally identical using De Bruijn indices for cycles.
1140    ///
1141    /// This is the O(1) alternative to bidirectional subtyping for identity checks.
1142    /// It transforms cyclic graphs into trees to solve the Graph Isomorphism problem.
1143    pub fn are_types_structurally_identical(&self, a: TypeId, b: TypeId) -> bool {
1144        if a == b {
1145            return true;
1146        }
1147
1148        // Task #49: Use cached canonical_id when query_db is available (O(1) path)
1149        if let Some(db) = self.query_db {
1150            return db.canonical_id(a) == db.canonical_id(b);
1151        }
1152
1153        // Fallback for cases without query_db: compute directly (O(N) path)
1154        let mut canonicalizer =
1155            crate::canonicalize::Canonicalizer::new(self.interner, self.resolver);
1156        let canon_a = canonicalizer.canonicalize(a);
1157        let canon_b = canonicalizer.canonicalize(b);
1158
1159        // After canonicalization, structural identity reduces to TypeId equality
1160        canon_a == canon_b
1161    }
1162}