Skip to main content

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