Skip to main content

tsz_solver/operations/
binary_ops.rs

1//! Binary operation type evaluation.
2//!
3//! This module handles type evaluation for binary operations like:
4//! - Arithmetic: +, -, *, /, %, **
5//! - Comparison: ==, !=, <, >, <=, >=
6//! - Logical: &&, ||, !
7//! - Bitwise: &, |, ^, ~, <<, >>, >>>
8//!
9//! ## Architecture
10//!
11//! The `BinaryOpEvaluator` evaluates the result type of binary operations
12//! and validates that operands are compatible with the operator.
13//!
14//! All functions take `TypeId` as input and return structured results,
15//! making them pure logic that can be unit tested independently.
16
17use crate::types::TypeListId;
18use crate::visitor::TypeVisitor;
19use crate::{IntrinsicKind, LiteralValue, QueryDatabase, TypeData, TypeDatabase, TypeId};
20
21/// Result of a binary operation.
22#[derive(Clone, Debug, PartialEq)]
23pub enum BinaryOpResult {
24    /// Operation succeeded, returns the result type
25    Success(TypeId),
26
27    /// Operand type error
28    TypeError {
29        left: TypeId,
30        right: TypeId,
31        op: &'static str,
32    },
33}
34
35/// Primitive type classes for overlap detection.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum PrimitiveClass {
38    String,
39    Number,
40    Boolean,
41    Bigint,
42    Symbol,
43    Null,
44    Undefined,
45}
46
47// =============================================================================
48// Visitor Pattern Implementations
49// =============================================================================
50
51/// Generate a `TypeVisitor` that checks whether a type belongs to a specific
52/// primitive class (number-like, string-like, etc.).
53///
54/// ## Arguments
55/// - `$name`: Visitor struct name
56/// - `$ik`: The `IntrinsicKind` to match
57/// - `$lit_pat`: Pattern to match `LiteralValue` against (use `_` for always-false)
58/// - `$lit_result`: Value to return when the pattern matches
59/// - Optional feature flags:
60///   - `check_union_all` — `visit_union` returns true when ALL members match
61///   - `check_constraint` — `visit_type_parameter/visit_infer` recurse into constraint
62///   - `recurse_enum`    — `visit_enum` recurses into the member type
63///   - `ref_conservative`— `visit_ref` returns true (conservative for unresolved enums)
64///   - `match_template_literal` — `visit_template_literal` returns true
65///   - `match_unique_symbol`    — `visit_unique_symbol` returns true
66///   - `check_intersection_any` — `visit_intersection` returns true when ANY member matches
67macro_rules! primitive_visitor {
68    ($name:ident, $ik:expr, $lit_pat:pat => $lit_result:expr $(, $feat:ident)*) => {
69        struct $name<'a> { _db: &'a dyn TypeDatabase }
70        impl<'a> TypeVisitor for $name<'a> {
71            type Output = bool;
72            fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> bool { kind == $ik }
73            fn visit_literal(&mut self, value: &LiteralValue) -> bool {
74                match value { $lit_pat => $lit_result, _ => false }
75            }
76            $(primitive_visitor!(@method $feat);)*
77            fn default_output() -> bool { false }
78        }
79    };
80    (@method check_union_all) => {
81        fn visit_union(&mut self, list_id: u32) -> bool {
82            let members = self._db.type_list(TypeListId(list_id));
83            !members.is_empty() && members.iter().all(|&m| self.visit_type(self._db, m))
84        }
85    };
86    (@method check_constraint) => {
87        fn visit_type_parameter(&mut self, info: &crate::types::TypeParamInfo) -> bool {
88            info.constraint.map(|c| self.visit_type(self._db, c)).unwrap_or(false)
89        }
90        fn visit_infer(&mut self, info: &crate::types::TypeParamInfo) -> bool {
91            info.constraint.map(|c| self.visit_type(self._db, c)).unwrap_or(false)
92        }
93    };
94    (@method recurse_enum) => {
95        fn visit_enum(&mut self, _def_id: u32, member_type: TypeId) -> bool {
96            self.visit_type(self._db, member_type)
97        }
98    };
99    (@method ref_conservative) => {
100        fn visit_ref(&mut self, _symbol_ref: u32) -> bool { true }
101    };
102    (@method match_template_literal) => {
103        fn visit_template_literal(&mut self, _template_id: u32) -> bool { true }
104    };
105    (@method match_unique_symbol) => {
106        fn visit_unique_symbol(&mut self, _symbol_ref: u32) -> bool { true }
107    };
108    (@method check_intersection_any) => {
109        fn visit_intersection(&mut self, list_id: u32) -> bool {
110            let members = self._db.type_list(TypeListId(list_id));
111            members.iter().any(|&m| self.visit_type(self._db, m))
112        }
113    };
114}
115
116primitive_visitor!(NumberLikeVisitor, IntrinsicKind::Number,
117    LiteralValue::Number(_) => true,
118    check_union_all, check_constraint, recurse_enum, check_intersection_any);
119
120primitive_visitor!(StringLikeVisitor, IntrinsicKind::String,
121    LiteralValue::String(_) => true,
122    check_union_all, check_constraint, recurse_enum, match_template_literal, check_intersection_any);
123
124primitive_visitor!(BigIntLikeVisitor, IntrinsicKind::Bigint,
125    LiteralValue::BigInt(_) => true,
126    check_union_all, check_constraint, recurse_enum, check_intersection_any);
127
128primitive_visitor!(BooleanLikeVisitor, IntrinsicKind::Boolean,
129    LiteralValue::Boolean(_) => true);
130
131struct InstanceofLeftOperandVisitor<'a> {
132    _db: &'a dyn TypeDatabase,
133}
134
135impl<'a> TypeVisitor for InstanceofLeftOperandVisitor<'a> {
136    type Output = bool;
137
138    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> bool {
139        matches!(
140            kind,
141            IntrinsicKind::Any | IntrinsicKind::Unknown | IntrinsicKind::Object
142        )
143    }
144
145    fn visit_literal(&mut self, _value: &LiteralValue) -> bool {
146        false
147    }
148
149    fn visit_type_parameter(&mut self, _info: &crate::types::TypeParamInfo) -> bool {
150        true
151    }
152
153    fn visit_object(&mut self, _shape_id: u32) -> bool {
154        true
155    }
156    fn visit_object_with_index(&mut self, _shape_id: u32) -> bool {
157        true
158    }
159    fn visit_array(&mut self, _element_type: TypeId) -> bool {
160        true
161    }
162    fn visit_tuple(&mut self, _list_id: u32) -> bool {
163        true
164    }
165    fn visit_function(&mut self, _shape_id: u32) -> bool {
166        true
167    }
168    fn visit_callable(&mut self, _shape_id: u32) -> bool {
169        true
170    }
171    fn visit_application(&mut self, _app_id: u32) -> bool {
172        true
173    }
174    fn visit_readonly_type(&mut self, _type_id: TypeId) -> bool {
175        true
176    }
177
178    fn visit_union(&mut self, list_id: u32) -> bool {
179        let members = self._db.type_list(crate::types::TypeListId(list_id));
180        let mut any_valid = false;
181        for &m in members.iter() {
182            if self.visit_type(self._db, m) {
183                any_valid = true;
184                break;
185            }
186        }
187        any_valid
188    }
189
190    fn visit_intersection(&mut self, list_id: u32) -> bool {
191        let members = self._db.type_list(crate::types::TypeListId(list_id));
192        let mut any_valid = false;
193        for &m in members.iter() {
194            if self.visit_type(self._db, m) {
195                any_valid = true;
196                break;
197            }
198        }
199        any_valid
200    }
201
202    fn default_output() -> bool {
203        false
204    }
205}
206
207struct SymbolLikeVisitor<'a> {
208    _db: &'a dyn TypeDatabase,
209}
210
211impl<'a> TypeVisitor for SymbolLikeVisitor<'a> {
212    type Output = bool;
213
214    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> bool {
215        kind == IntrinsicKind::Symbol
216    }
217
218    fn visit_literal(&mut self, value: &LiteralValue) -> bool {
219        let _ = value;
220        false
221    }
222
223    fn visit_ref(&mut self, _symbol_ref: u32) -> bool {
224        true
225    }
226
227    fn visit_unique_symbol(&mut self, _symbol_ref: u32) -> bool {
228        true
229    }
230
231    fn default_output() -> bool {
232        false
233    }
234}
235
236/// Visitor that checks if all union members are orderable (string/number/bigint-like).
237/// Used for relational operator validation on mixed unions.
238struct OrderableVisitor<'a> {
239    _db: &'a dyn TypeDatabase,
240}
241
242impl<'a> TypeVisitor for OrderableVisitor<'a> {
243    type Output = bool;
244
245    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> bool {
246        matches!(
247            kind,
248            IntrinsicKind::String | IntrinsicKind::Number | IntrinsicKind::Bigint
249        )
250    }
251
252    fn visit_literal(&mut self, value: &LiteralValue) -> bool {
253        matches!(
254            value,
255            LiteralValue::String(_) | LiteralValue::Number(_) | LiteralValue::BigInt(_)
256        )
257    }
258
259    fn visit_union(&mut self, list_id: u32) -> bool {
260        let members = self._db.type_list(TypeListId(list_id));
261        !members.is_empty() && members.iter().all(|&m| self.visit_type(self._db, m))
262    }
263
264    fn visit_enum(&mut self, _def_id: u32, member_type: TypeId) -> bool {
265        self.visit_type(self._db, member_type)
266    }
267
268    fn visit_template_literal(&mut self, _template_id: u32) -> bool {
269        true // Template literals are string-like
270    }
271
272    fn visit_intersection(&mut self, list_id: u32) -> bool {
273        let members = self._db.type_list(TypeListId(list_id));
274        members.iter().any(|&m| self.visit_type(self._db, m))
275    }
276
277    fn visit_type_parameter(&mut self, info: &crate::types::TypeParamInfo) -> bool {
278        info.constraint
279            .map(|c| self.visit_type(self._db, c))
280            .unwrap_or(false)
281    }
282
283    fn default_output() -> bool {
284        false
285    }
286}
287
288/// Visitor to extract primitive class from a type.
289struct PrimitiveClassVisitor;
290
291impl TypeVisitor for PrimitiveClassVisitor {
292    type Output = Option<PrimitiveClass>;
293
294    fn visit_intrinsic(&mut self, kind: IntrinsicKind) -> Self::Output {
295        match kind {
296            IntrinsicKind::String => Some(PrimitiveClass::String),
297            IntrinsicKind::Number => Some(PrimitiveClass::Number),
298            IntrinsicKind::Boolean => Some(PrimitiveClass::Boolean),
299            IntrinsicKind::Bigint => Some(PrimitiveClass::Bigint),
300            IntrinsicKind::Symbol => Some(PrimitiveClass::Symbol),
301            IntrinsicKind::Null => Some(PrimitiveClass::Null),
302            IntrinsicKind::Undefined | IntrinsicKind::Void => Some(PrimitiveClass::Undefined),
303            _ => None,
304        }
305    }
306
307    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
308        match value {
309            LiteralValue::String(_) => Some(PrimitiveClass::String),
310            LiteralValue::Number(_) => Some(PrimitiveClass::Number),
311            LiteralValue::Boolean(_) => Some(PrimitiveClass::Boolean),
312            LiteralValue::BigInt(_) => Some(PrimitiveClass::Bigint),
313        }
314    }
315
316    fn visit_template_literal(&mut self, _template_id: u32) -> Self::Output {
317        Some(PrimitiveClass::String)
318    }
319
320    fn visit_unique_symbol(&mut self, _symbol_ref: u32) -> Self::Output {
321        Some(PrimitiveClass::Symbol)
322    }
323
324    fn default_output() -> Self::Output {
325        None
326    }
327}
328
329/// Check if an intrinsic primitive type overlaps with a literal value.
330/// e.g., `string` overlaps with `"foo"`, `number` overlaps with `42`.
331const fn intrinsic_overlaps_literal(kind: IntrinsicKind, value: &LiteralValue) -> bool {
332    matches!(
333        (kind, value),
334        (IntrinsicKind::String, LiteralValue::String(_))
335            | (IntrinsicKind::Number, LiteralValue::Number(_))
336            | (IntrinsicKind::Boolean, LiteralValue::Boolean(_))
337            | (IntrinsicKind::Bigint, LiteralValue::BigInt(_))
338    )
339}
340
341/// Visitor to check type overlap for comparison operations.
342struct OverlapChecker<'a> {
343    db: &'a dyn TypeDatabase,
344    left: TypeId,
345}
346
347impl<'a> OverlapChecker<'a> {
348    fn new(db: &'a dyn TypeDatabase, left: TypeId) -> Self {
349        Self { db, left }
350    }
351
352    fn check(&mut self, right: TypeId) -> bool {
353        // Fast path: same type
354        if self.left == right {
355            return true;
356        }
357
358        // Fast path: top/bottom types
359        if matches!(
360            (self.left, right),
361            (TypeId::ANY | TypeId::UNKNOWN | TypeId::ERROR, _)
362                | (_, TypeId::ANY | TypeId::UNKNOWN | TypeId::ERROR)
363        ) {
364            return true;
365        }
366
367        if self.left == TypeId::NEVER || right == TypeId::NEVER {
368            return false;
369        }
370
371        // Check intersection first before visitor
372        if self.db.intersection2(self.left, right) == TypeId::NEVER {
373            return false;
374        }
375
376        // Use visitor to check overlap
377        self.visit_type(self.db, right)
378    }
379}
380
381impl<'a> TypeVisitor for OverlapChecker<'a> {
382    type Output = bool;
383
384    fn visit_intrinsic(&mut self, _kind: IntrinsicKind) -> Self::Output {
385        // Intrinsics can overlap with many things, check intersection above
386        true
387    }
388
389    fn visit_union(&mut self, list_id: u32) -> Self::Output {
390        let members = self.db.type_list(TypeListId(list_id));
391        members.iter().any(|&member| self.check(member))
392    }
393
394    fn visit_type_parameter(&mut self, info: &crate::types::TypeParamInfo) -> Self::Output {
395        // Unconstrained type parameters are handled in has_overlap before visitor
396        match info.constraint {
397            Some(constraint) => self.check(constraint),
398            None => panic!("TypeParameter without constraint should not reach visitor"),
399        }
400    }
401
402    fn visit_infer(&mut self, info: &crate::types::TypeParamInfo) -> Self::Output {
403        // Unconstrained type parameters are handled in has_overlap before visitor
404        match info.constraint {
405            Some(constraint) => self.check(constraint),
406            None => panic!("Infer without constraint should not reach visitor"),
407        }
408    }
409
410    fn visit_literal(&mut self, value: &LiteralValue) -> Self::Output {
411        // Check if left is a literal with same value, or a supertype of the literal
412        match self.db.lookup(self.left) {
413            Some(TypeData::Literal(left_lit)) => left_lit == *value,
414            Some(TypeData::Union(members)) => {
415                // Check if left's union contains this literal or a supertype
416                let members = self.db.type_list(members);
417                members.iter().any(|&m| match self.db.lookup(m) {
418                    Some(TypeData::Literal(lit)) => lit == *value,
419                    Some(TypeData::Intrinsic(kind)) => intrinsic_overlaps_literal(kind, value),
420                    _ => false,
421                })
422            }
423            // An intrinsic primitive type overlaps with its corresponding literal type
424            // e.g., `string` overlaps with `"foo"`, `number` overlaps with `42`
425            Some(TypeData::Intrinsic(kind)) => intrinsic_overlaps_literal(kind, value),
426            // Intersection types: if ANY member overlaps with the literal, the
427            // intersection overlaps. e.g., `string & { $Brand: any }` overlaps with `""`.
428            Some(TypeData::Intersection(members)) => {
429                let members = self.db.type_list(members);
430                members.iter().any(|&m| match self.db.lookup(m) {
431                    Some(TypeData::Literal(lit)) => lit == *value,
432                    Some(TypeData::Intrinsic(kind)) => intrinsic_overlaps_literal(kind, value),
433                    _ => false,
434                })
435            }
436            _ => false,
437        }
438    }
439
440    fn default_output() -> Self::Output {
441        // Default: check for disjoint primitive classes
442        // We conservatively return true unless we can prove they're disjoint
443        // This matches the original behavior where most types are considered to overlap
444        true
445    }
446}
447
448/// Evaluates binary operations on types.
449pub struct BinaryOpEvaluator<'a> {
450    interner: &'a dyn QueryDatabase,
451}
452
453impl<'a> BinaryOpEvaluator<'a> {
454    /// Create a new binary operation evaluator.
455    pub fn new(interner: &'a dyn QueryDatabase) -> Self {
456        Self { interner }
457    }
458
459    /// Check if a type is valid for the left side of an `instanceof` expression.
460    /// TS2358: "The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter."
461    pub fn is_valid_instanceof_left_operand(&self, type_id: TypeId) -> bool {
462        if type_id == TypeId::ERROR || type_id == TypeId::ANY || type_id == TypeId::UNKNOWN {
463            return true;
464        }
465        let mut visitor = InstanceofLeftOperandVisitor { _db: self.interner };
466        visitor.visit_type(self.interner, type_id)
467    }
468
469    /// Check if a type is valid for the right side of an `instanceof` expression.
470    /// TS2359: "The right-hand side of an 'instanceof' expression must be either of type 'any', a class, function, or other type assignable to the 'Function' interface type..."
471    pub fn is_valid_instanceof_right_operand<F>(
472        &self,
473        type_id: TypeId,
474        func_ty: TypeId,
475        assignable_check: &mut F,
476    ) -> bool
477    where
478        F: FnMut(TypeId, TypeId) -> bool,
479    {
480        if type_id == TypeId::ANY
481            || type_id == TypeId::UNKNOWN
482            || type_id == TypeId::ERROR
483            || type_id == TypeId::FUNCTION
484        {
485            return true;
486        }
487
488        if let Some(crate::TypeData::Union(list_id)) = self.interner.lookup(type_id) {
489            let members = self.interner.type_list(list_id);
490            let mut all_valid = true;
491            for &m in members.iter() {
492                if !self.is_valid_instanceof_right_operand(m, func_ty, assignable_check) {
493                    all_valid = false;
494                    break;
495                }
496            }
497            return all_valid && !members.is_empty();
498        }
499
500        if let Some(crate::TypeData::Intersection(list_id)) = self.interner.lookup(type_id) {
501            let members = self.interner.type_list(list_id);
502            let mut any_valid = false;
503            for &m in members.iter() {
504                if self.is_valid_instanceof_right_operand(m, func_ty, assignable_check) {
505                    any_valid = true;
506                    break;
507                }
508            }
509            return any_valid;
510        }
511
512        assignable_check(type_id, func_ty) || assignable_check(type_id, TypeId::FUNCTION)
513    }
514
515    /// Check if a type is valid for arithmetic operations (number, bigint, enum, or any).
516    /// This is used for TS2362/TS2363 error checking.
517    ///
518    /// Also returns true for ERROR and UNKNOWN types to prevent cascading errors.
519    /// If a type couldn't be resolved (TS2304, etc.), we don't want to add noise
520    /// with arithmetic errors - the primary error is more useful.
521    pub fn is_arithmetic_operand(&self, type_id: TypeId) -> bool {
522        // Don't emit arithmetic errors for error/unknown/never types - prevents cascading errors.
523        // NEVER (bottom type) is assignable to all types, so it's valid everywhere.
524        if type_id == TypeId::ANY
525            || type_id == TypeId::ERROR
526            || type_id == TypeId::UNKNOWN
527            || type_id == TypeId::NEVER
528        {
529            return true;
530        }
531        self.is_number_like(type_id) || self.is_bigint_like(type_id)
532    }
533
534    /// Evaluate a binary operation on two types.
535    pub fn evaluate(&self, left: TypeId, right: TypeId, op: &'static str) -> BinaryOpResult {
536        match op {
537            "+" => self.evaluate_plus(left, right),
538            "-" | "*" | "/" | "%" | "**" | "&" | "|" | "^" | "<<" | ">>" | ">>>" => {
539                self.evaluate_arithmetic(left, right, op)
540            }
541            "==" | "!=" | "===" | "!==" => {
542                if self.has_overlap(left, right) {
543                    BinaryOpResult::Success(TypeId::BOOLEAN)
544                } else {
545                    BinaryOpResult::TypeError { left, right, op }
546                }
547            }
548            "<" | ">" | "<=" | ">=" => self.evaluate_comparison(left, right),
549            "&&" | "||" | "??" => self.evaluate_logical(left, right, op),
550            _ => BinaryOpResult::TypeError { left, right, op },
551        }
552    }
553
554    /// Evaluate the + operator (can be string concatenation or addition).
555    fn evaluate_plus(&self, left: TypeId, right: TypeId) -> BinaryOpResult {
556        // Don't emit errors for unknown types - prevents cascading errors
557        if left == TypeId::UNKNOWN || right == TypeId::UNKNOWN {
558            return BinaryOpResult::Success(TypeId::UNKNOWN);
559        }
560
561        // Error types act like `any` in tsc - prevents cascading errors
562        // while still inferring the correct result type (e.g., string + error = string)
563        let left = if left == TypeId::ERROR {
564            TypeId::ANY
565        } else {
566            left
567        };
568        let right = if right == TypeId::ERROR {
569            TypeId::ANY
570        } else {
571            right
572        };
573
574        // TS2469: Symbol cannot be used in arithmetic
575        if self.is_symbol_like(left) || self.is_symbol_like(right) {
576            return BinaryOpResult::TypeError {
577                left,
578                right,
579                op: "+",
580            };
581        }
582
583        // any + anything = any (and vice versa)
584        if left == TypeId::ANY || right == TypeId::ANY {
585            return BinaryOpResult::Success(TypeId::ANY);
586        }
587
588        // String concatenation: string + primitive = string
589        if self.is_string_like(left) || self.is_string_like(right) {
590            // Check if the non-string side is a valid operand (primitive)
591            let valid_left = self.is_string_like(left) || self.is_valid_string_concat_operand(left);
592            let valid_right =
593                self.is_string_like(right) || self.is_valid_string_concat_operand(right);
594
595            if valid_left && valid_right {
596                return BinaryOpResult::Success(TypeId::STRING);
597            }
598            // TS2365: Operator '+' cannot be applied to types 'string' and 'object'
599            return BinaryOpResult::TypeError {
600                left,
601                right,
602                op: "+",
603            };
604        }
605
606        // number-like + number-like = number
607        if self.is_number_like(left) && self.is_number_like(right) {
608            return BinaryOpResult::Success(TypeId::NUMBER);
609        }
610
611        // bigint-like + bigint-like = bigint
612        if self.is_bigint_like(left) && self.is_bigint_like(right) {
613            return BinaryOpResult::Success(TypeId::BIGINT);
614        }
615
616        BinaryOpResult::TypeError {
617            left,
618            right,
619            op: "+",
620        }
621    }
622
623    /// Evaluate arithmetic operators (-, *, /, %, **).
624    fn evaluate_arithmetic(&self, left: TypeId, right: TypeId, op: &'static str) -> BinaryOpResult {
625        // Don't emit errors for unknown types - prevents cascading errors
626        if left == TypeId::UNKNOWN || right == TypeId::UNKNOWN {
627            return BinaryOpResult::Success(TypeId::UNKNOWN);
628        }
629
630        // Error types act like `any` in tsc - prevents cascading errors
631        let left = if left == TypeId::ERROR {
632            TypeId::ANY
633        } else {
634            left
635        };
636        let right = if right == TypeId::ERROR {
637            TypeId::ANY
638        } else {
639            right
640        };
641
642        // TS2469: Symbol cannot be used in arithmetic
643        if self.is_symbol_like(left) || self.is_symbol_like(right) {
644            return BinaryOpResult::TypeError { left, right, op };
645        }
646
647        // any allows all operations
648        if left == TypeId::ANY || right == TypeId::ANY {
649            return BinaryOpResult::Success(TypeId::NUMBER);
650        }
651
652        // number-like * number-like = number
653        if self.is_number_like(left) && self.is_number_like(right) {
654            return BinaryOpResult::Success(TypeId::NUMBER);
655        }
656
657        // bigint-like * bigint-like = bigint
658        if self.is_bigint_like(left) && self.is_bigint_like(right) {
659            return BinaryOpResult::Success(TypeId::BIGINT);
660        }
661
662        BinaryOpResult::TypeError { left, right, op }
663    }
664
665    /// Evaluate comparison operators (<, >, <=, >=).
666    fn evaluate_comparison(&self, left: TypeId, right: TypeId) -> BinaryOpResult {
667        // Don't emit errors for unknown types - prevents cascading errors
668        if left == TypeId::UNKNOWN || right == TypeId::UNKNOWN {
669            return BinaryOpResult::Success(TypeId::BOOLEAN);
670        }
671
672        // Error types act like `any` in tsc - prevents cascading errors
673        let left = if left == TypeId::ERROR {
674            TypeId::ANY
675        } else {
676            left
677        };
678        let right = if right == TypeId::ERROR {
679            TypeId::ANY
680        } else {
681            right
682        };
683
684        // TS2469: Symbol cannot be used in comparison operators
685        if self.is_symbol_like(left) || self.is_symbol_like(right) {
686            return BinaryOpResult::TypeError {
687                left,
688                right,
689                op: "<",
690            };
691        }
692
693        // Any allows comparison
694        if left == TypeId::ANY || right == TypeId::ANY {
695            return BinaryOpResult::Success(TypeId::BOOLEAN);
696        }
697
698        // Numbers (and Enums) can be compared
699        if self.is_number_like(left) && self.is_number_like(right) {
700            return BinaryOpResult::Success(TypeId::BOOLEAN);
701        }
702
703        // Strings can be compared
704        if self.is_string_like(left) && self.is_string_like(right) {
705            return BinaryOpResult::Success(TypeId::BOOLEAN);
706        }
707
708        // BigInts can be compared
709        if self.is_bigint_like(left) && self.is_bigint_like(right) {
710            return BinaryOpResult::Success(TypeId::BOOLEAN);
711        }
712
713        // Booleans can be compared (valid in JS/TS)
714        if self.is_boolean_like(left) && self.is_boolean_like(right) {
715            return BinaryOpResult::Success(TypeId::BOOLEAN);
716        }
717
718        // Mixed orderable unions: unions of string/number/bigint are valid for
719        // relational operators. e.g., ("ABC" | number) < ("XYZ" | number) is valid
720        // because each member is individually orderable.
721        if self.is_orderable(left) && self.is_orderable(right) {
722            return BinaryOpResult::Success(TypeId::BOOLEAN);
723        }
724
725        // Mismatch - emit TS2365
726        BinaryOpResult::TypeError {
727            left,
728            right,
729            op: "<",
730        }
731    }
732
733    /// Evaluate logical operators (&&, ||).
734    fn evaluate_logical(&self, left: TypeId, right: TypeId, op: &'static str) -> BinaryOpResult {
735        let ctx = crate::narrowing::NarrowingContext::new(self.interner);
736
737        let result = if op == "&&" {
738            // left && right
739            let falsy_left = ctx.narrow_to_falsy(left);
740            let truthy_left = ctx.narrow_by_truthiness(left);
741
742            if truthy_left == TypeId::NEVER {
743                left
744            } else if falsy_left == TypeId::NEVER {
745                right
746            } else {
747                self.interner.union2(falsy_left, right)
748            }
749        } else if op == "||" {
750            // left || right
751            let truthy_left = ctx.narrow_by_truthiness(left);
752            let falsy_left = ctx.narrow_to_falsy(left);
753
754            if falsy_left == TypeId::NEVER {
755                left
756            } else if truthy_left == TypeId::NEVER {
757                right
758            } else {
759                self.interner.union2(truthy_left, right)
760            }
761        } else {
762            // left ?? right
763            let non_nullish_left = ctx.narrow_by_nullishness(left, false);
764            let nullish_left = ctx.narrow_by_nullishness(left, true);
765
766            if nullish_left == TypeId::NEVER {
767                left
768            } else if non_nullish_left == TypeId::NEVER {
769                right
770            } else {
771                self.interner.union2(non_nullish_left, right)
772            }
773        };
774
775        BinaryOpResult::Success(result)
776    }
777
778    /// Check if a type is number-like (number, number literal, numeric enum, or any).
779    fn is_number_like(&self, type_id: TypeId) -> bool {
780        if type_id == TypeId::NUMBER || type_id == TypeId::ANY {
781            return true;
782        }
783        let mut visitor = NumberLikeVisitor { _db: self.interner };
784        visitor.visit_type(self.interner, type_id)
785    }
786
787    /// Check if a type is string-like (string, string literal, template literal, or any).
788    fn is_string_like(&self, type_id: TypeId) -> bool {
789        if type_id == TypeId::STRING || type_id == TypeId::ANY {
790            return true;
791        }
792        let mut visitor = StringLikeVisitor { _db: self.interner };
793        visitor.visit_type(self.interner, type_id)
794    }
795
796    /// Check if a type is bigint-like (bigint, bigint literal, bigint enum, or any).
797    fn is_bigint_like(&self, type_id: TypeId) -> bool {
798        if type_id == TypeId::BIGINT || type_id == TypeId::ANY {
799            return true;
800        }
801        let mut visitor = BigIntLikeVisitor { _db: self.interner };
802        visitor.visit_type(self.interner, type_id)
803    }
804
805    /// Check if a type is orderable for relational operators (<, >, <=, >=).
806    /// A type is orderable if all its union members are string-like, number-like,
807    /// or bigint-like. This allows mixed unions like `string | number` to be compared.
808    fn is_orderable(&self, type_id: TypeId) -> bool {
809        if type_id == TypeId::ANY
810            || type_id == TypeId::NUMBER
811            || type_id == TypeId::STRING
812            || type_id == TypeId::BIGINT
813        {
814            return true;
815        }
816        let mut visitor = OrderableVisitor { _db: self.interner };
817        visitor.visit_type(self.interner, type_id)
818    }
819
820    /// Check if two types have any overlap (can be compared).
821    pub fn has_overlap(&self, left: TypeId, right: TypeId) -> bool {
822        if left == right {
823            return true;
824        }
825        if left == TypeId::ANY
826            || right == TypeId::ANY
827            || left == TypeId::UNKNOWN
828            || right == TypeId::UNKNOWN
829            || left == TypeId::ERROR
830            || right == TypeId::ERROR
831        {
832            return true;
833        }
834        if left == TypeId::NEVER || right == TypeId::NEVER {
835            return false;
836        }
837
838        // Special handling for TypeParameter and Infer before visitor pattern
839        if let Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) =
840            self.interner.lookup(left)
841        {
842            if let Some(constraint) = info.constraint {
843                return self.has_overlap(constraint, right);
844            }
845            return true;
846        }
847
848        if let Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) =
849            self.interner.lookup(right)
850        {
851            if let Some(constraint) = info.constraint {
852                return self.has_overlap(left, constraint);
853            }
854            return true;
855        }
856
857        // Handle Union types explicitly (recursively check members)
858        if let Some(TypeData::Union(members)) = self.interner.lookup(left) {
859            let members = self.interner.type_list(members);
860            return members
861                .iter()
862                .any(|member| self.has_overlap(*member, right));
863        }
864
865        if let Some(TypeData::Union(members)) = self.interner.lookup(right) {
866            let members = self.interner.type_list(members);
867            return members.iter().any(|member| self.has_overlap(left, *member));
868        }
869
870        // Check primitive class disjointness before intersection
871        if self.primitive_classes_disjoint(left, right) {
872            return false;
873        }
874
875        // Check intersection before visitor pattern
876        if self.interner.intersection2(left, right) == TypeId::NEVER {
877            return false;
878        }
879
880        // Use visitor for remaining type checks
881        let mut checker = OverlapChecker::new(self.interner, left);
882        checker.check(right)
883    }
884
885    /// Check if two types belong to disjoint primitive classes.
886    fn primitive_classes_disjoint(&self, left: TypeId, right: TypeId) -> bool {
887        match (self.primitive_class(left), self.primitive_class(right)) {
888            (Some(left_class), Some(right_class)) => left_class != right_class,
889            _ => false,
890        }
891    }
892
893    /// Get the primitive class of a type (if applicable).
894    fn primitive_class(&self, type_id: TypeId) -> Option<PrimitiveClass> {
895        let mut visitor = PrimitiveClassVisitor;
896        visitor.visit_type(self.interner, type_id)
897    }
898
899    /// Check if a type is symbol-like (symbol or unique symbol).
900    pub fn is_symbol_like(&self, type_id: TypeId) -> bool {
901        if type_id == TypeId::SYMBOL {
902            return true;
903        }
904        let mut visitor = SymbolLikeVisitor { _db: self.interner };
905        visitor.visit_type(self.interner, type_id)
906    }
907
908    /// Check if a type is a valid computed property name type (TS2464).
909    ///
910    /// Valid types: string, number, symbol, any (including literals, enums,
911    /// template literals, unique symbols). For unions, ALL members must be valid.
912    /// This check is independent of strictNullChecks.
913    pub fn is_valid_computed_property_name_type(&self, type_id: TypeId) -> bool {
914        if type_id == TypeId::ANY || type_id == TypeId::NEVER || type_id == TypeId::ERROR {
915            return true;
916        }
917        // For union types, each member must individually be valid
918        if let Some(TypeData::Union(list_id)) = self.interner.lookup(type_id) {
919            let members = self.interner.type_list(list_id);
920            return !members.is_empty()
921                && members
922                    .iter()
923                    .all(|&m| self.is_valid_computed_property_name_type(m));
924        }
925        self.is_string_like(type_id) || self.is_number_like(type_id) || self.is_symbol_like(type_id)
926    }
927
928    /// Check if a type is boolean-like (boolean or boolean literal).
929    pub fn is_boolean_like(&self, type_id: TypeId) -> bool {
930        if type_id == TypeId::BOOLEAN || type_id == TypeId::ANY {
931            return true;
932        }
933        let mut visitor = BooleanLikeVisitor { _db: self.interner };
934        visitor.visit_type(self.interner, type_id)
935    }
936
937    /// Check if a type is a valid operand for string concatenation.
938    /// Valid operands are: string, number, boolean, bigint, null, undefined, void, any.
939    fn is_valid_string_concat_operand(&self, type_id: TypeId) -> bool {
940        if type_id == TypeId::ANY || type_id == TypeId::ERROR || type_id == TypeId::NEVER {
941            return true;
942        }
943        if type_id == TypeId::UNKNOWN {
944            return false;
945        }
946        if let Some(TypeData::Union(list_id)) = self.interner.lookup(type_id) {
947            let members = self.interner.type_list(list_id);
948            return !members.is_empty()
949                && members
950                    .iter()
951                    .all(|&member| self.is_valid_string_concat_operand(member));
952        }
953        if self.is_symbol_like(type_id) {
954            return false;
955        }
956        // Primitives are valid
957        if self.is_number_like(type_id)
958            || self.is_boolean_like(type_id)
959            || self.is_bigint_like(type_id)
960            || type_id == TypeId::NULL
961            || type_id == TypeId::UNDEFINED
962            || type_id == TypeId::VOID
963        {
964            return true;
965        }
966
967        // Non-nullish non-symbol object/function-like types are string-concat-compatible.
968        true
969    }
970}