Skip to main content

submilli_engine/typechecker/infer/
narrowing.rs

1//! Narrowing engine — pure data structures.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{ExprId, Ident, MangledName, Type, types::LiteralF64};
6
7#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct ReferencePath {
9    pub root: BindingId,
10    pub chain: Vec<PathElem>,
11}
12
13impl ReferencePath {
14    /// A local itself, with no field or element step. Only such a path can't
15    /// change behind a guard's back, through an alias or in a call.
16    pub fn is_bare_local(&self) -> bool {
17        self.chain.is_empty() && matches!(self.root, BindingId::Local { .. })
18    }
19
20    pub fn root(root: BindingId) -> Self {
21        Self {
22            root,
23            chain: Vec::new(),
24        }
25    }
26
27    /// Whether a write to `self` may change what `other` reads. A key step
28    /// of `other` (`o[key]`) may name any field or element, so any written
29    /// step matches it. A written key matches only itself, as in
30    /// TypeScript: `values[index] = 4` leaves `values[0]` narrowed.
31    pub fn is_prefix_of(&self, other: &ReferencePath) -> bool {
32        self.root == other.root
33            && self.chain.len() <= other.chain.len()
34            && self
35                .chain
36                .iter()
37                .zip(&other.chain)
38                .all(|(written, read)| read.is_written_by(written))
39    }
40
41    pub fn render(&self) -> String {
42        let mut out = self.root.render();
43        for elem in &self.chain {
44            match elem {
45                PathElem::Field(name) => {
46                    out.push('.');
47                    out.push_str(name);
48                }
49                PathElem::Key(binding, _) => {
50                    out.push('[');
51                    out.push_str(&binding.render());
52                    out.push(']');
53                }
54                PathElem::Index(lit) => {
55                    use std::fmt::Write;
56                    match lit {
57                        LiteralValue::Number(n) => {
58                            let _ = write!(out, "[{}]", n.0);
59                        }
60                        LiteralValue::String(s) => {
61                            let _ = write!(out, "[{s:?}]");
62                        }
63                        LiteralValue::Boolean(b) => {
64                            let _ = write!(out, "[{b}]");
65                        }
66                        LiteralValue::BigInt(digits) => {
67                            let _ = write!(out, "[{digits}n]");
68                        }
69                        LiteralValue::Null => {
70                            let _ = write!(out, "[null]");
71                        }
72                        LiteralValue::Undefined => {
73                            let _ = write!(out, "[undefined]");
74                        }
75                    }
76                }
77            }
78        }
79        out
80    }
81}
82
83/// `decl_scope` on `Local` is non-optional for correctness: name-only keying
84/// lets inner scopes incorrectly inherit outer narrowings.
85///
86/// `This` needs no payload: narrow scopes are per function body and a closure
87/// boundary resets them, so within one body `this` names exactly one receiver.
88#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub enum BindingId {
90    Local { name: String, decl_scope: ScopeId },
91    Global(MangledName),
92    This,
93}
94
95impl BindingId {
96    /// The name the source reads the binding by.
97    pub fn render(&self) -> String {
98        match self {
99            BindingId::Local { name, .. } => name.clone(),
100            BindingId::Global(mangled) => mangled
101                .as_str()
102                .rsplit('#')
103                .next()
104                .unwrap_or(mangled.as_str())
105                .to_string(),
106            BindingId::This => "this".to_string(),
107        }
108    }
109}
110
111#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub struct ScopeId(pub u32);
113
114/// `Index` is restricted to constant literal indices so paths stay hash-comparable;
115/// `Key` indexes by a binding that holds one value for its whole life (a
116/// `const`, or a parameter or `let` never assigned), as TypeScript narrows
117/// `obj[key]`. Other index expressions are not narrowable.
118#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
119pub enum PathElem {
120    Field(String),
121    Index(LiteralValue),
122    Key(BindingId, KeyKind),
123}
124
125impl PathElem {
126    fn is_written_by(&self, written: &PathElem) -> bool {
127        matches!(self, PathElem::Key(..)) || self == written
128    }
129}
130
131/// What a [`PathElem::Key`] reads: a property, by a string key, or an element.
132#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
133pub enum KeyKind {
134    Property,
135    Element,
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub enum LiteralValue {
140    Number(LiteralF64),
141    String(String),
142    Boolean(bool),
143    /// A bigint's canonical decimal digits, as [`Type::BigIntLiteral`] holds them.
144    BigInt(String),
145    /// Discriminant values only: a member may name its variant with `null` or
146    /// `undefined`, and an optional discriminant is `undefined` when absent.
147    Null,
148    Undefined,
149}
150
151impl LiteralValue {
152    /// The value an enum member literal type `E.A` holds.
153    pub fn of_enum_member(ty: &Type) -> Option<Self> {
154        Some(match ty.enum_member_value()? {
155            crate::types::EnumValue::Number(value) => LiteralValue::Number(value),
156            crate::types::EnumValue::String(value) => LiteralValue::String(value),
157        })
158    }
159}
160
161/// Bitmask over narrowing predicates, modeled on TypeScript's `TypeFacts`.
162#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
163pub struct TypeFacts(pub u16);
164
165impl TypeFacts {
166    pub const EMPTY: Self = Self(0);
167    pub const TRUTHY: Self = Self(1 << 0);
168    pub const FALSY: Self = Self(1 << 1);
169    pub const EQ_NULL: Self = Self(1 << 2);
170    pub const NE_NULL: Self = Self(1 << 3);
171    pub const IS_NUMBER: Self = Self(1 << 4);
172    pub const IS_STRING: Self = Self(1 << 5);
173    pub const IS_BOOLEAN: Self = Self(1 << 6);
174    pub const IS_OBJECT: Self = Self(1 << 7);
175    pub const IS_FUNCTION: Self = Self(1 << 8);
176    pub const IS_ARRAY: Self = Self(1 << 9);
177    pub const EQ_UNDEFINED: Self = Self(1 << 10);
178    pub const NE_UNDEFINED: Self = Self(1 << 11);
179    pub const IS_BIGINT: Self = Self(1 << 12);
180    pub const IS_UNDEFINED: Self = Self(1 << 13);
181
182    pub fn contains(self, other: Self) -> bool {
183        (self.0 & other.0) == other.0
184    }
185
186    pub fn is_empty(self) -> bool {
187        self.0 == 0
188    }
189}
190
191impl std::ops::BitOr for TypeFacts {
192    type Output = Self;
193    fn bitor(self, rhs: Self) -> Self {
194        Self(self.0 | rhs.0)
195    }
196}
197
198impl std::ops::BitOrAssign for TypeFacts {
199    fn bitor_assign(&mut self, rhs: Self) {
200        self.0 |= rhs.0;
201    }
202}
203
204impl std::ops::BitAnd for TypeFacts {
205    type Output = Self;
206    fn bitand(self, rhs: Self) -> Self {
207        Self(self.0 & rhs.0)
208    }
209}
210
211impl std::ops::Not for TypeFacts {
212    type Output = Self;
213    fn not(self) -> Self {
214        const ALL: u16 = (1 << 14) - 1;
215        Self((!self.0) & ALL)
216    }
217}
218
219/// Collects control transfers until the owning loop, switch or function exits.
220#[derive(Debug)]
221pub struct PendingJoinFrame {
222    pub kind: PendingJoinKind,
223    /// `narrow_scopes.len()` at the moment the construct was
224    /// entered, *before* the construct's body frame is pushed.
225    /// Snapshots taken at `break`/`continue` time use this as the
226    /// inclusive base — every frame at index >= `narrow_depth`
227    /// was pushed inside the construct and is folded into the
228    /// snapshot's narrow env (innermost wins on lookup).
229    pub narrow_depth: usize,
230    pub breaks: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
231    pub continues: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
232    pub returns: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
233}
234
235#[derive(Copy, Clone, Debug, PartialEq, Eq)]
236pub enum PendingJoinKind {
237    /// Return targets this body, independently of its nested loops and switches.
238    Function,
239    /// Its normal return paths contribute assignment facts at the call site.
240    InvokedFunction,
241    /// continue targets the innermost frame of this kind.
242    Loop,
243    /// Transparent to `continue` — search past this frame when finding the continue target.
244    Switch,
245}
246
247/// Writes a clause can complete before it throws, including inferred values.
248#[derive(Clone, Default)]
249pub(in crate::typechecker) struct ClauseWrites {
250    pub paths: BTreeSet<ReferencePath>,
251    pub evolving: NarrowEnv,
252}
253
254impl ClauseWrites {
255    pub(super) fn record_evolving(&mut self, path: ReferencePath, view: NarrowedView) {
256        let mut joined = view;
257        if let Some(previous) = self.evolving.get(&path) {
258            let evolving = join_view_inference(previous, &joined);
259            joined.narrowed_ty = join_views(previous, &joined).unwrap_or(Type::Unknown);
260            joined.evolving = evolving;
261        }
262        self.evolving.insert(path, joined);
263    }
264
265    pub(super) fn extend(&mut self, other: Self) {
266        self.paths.extend(other.paths);
267        for (path, view) in other.evolving {
268            self.record_evolving(path, view);
269        }
270    }
271}
272
273#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum EvolvingType {
275    Value,
276    Array,
277    /// An undetermined empty-array path joined with a concrete assignment.
278    UnresolvedArray,
279}
280
281#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct NarrowedView {
283    /// Empty-array inference remains open until an ordinary array is assigned.
284    pub evolving: Option<EvolvingType>,
285    pub narrowed_ty: Type,
286    pub facts: TypeFacts,
287    /// Literal values excluded by negative predicates (e.g., `s.kind !== "circle"`
288    /// adds `"circle"` here).
289    pub excluded_literals: BTreeSet<LiteralValue>,
290    /// Shadow Wasm local binding name (`#narrow_<N>`) codegen uses for in-region
291    /// references to this narrowed path.
292    pub binding: Ident,
293    /// Un-narrowed source expression codegen evaluates at `NarrowRegion` entry.
294    /// Minted fresh — not shared with other AST positions (would double-trigger
295    /// walker visits).
296    pub source: ExprId,
297}
298
299/// The cast is **unchecked**: codegen trusts the typechecker's narrowing decision. If the
300/// predicate didn't actually prove what the narrowing claims, the Wasm-level cast traps.
301#[derive(Clone, Debug, PartialEq, Eq)]
302pub struct CastInfo {
303    pub from_ty: Type,
304    pub to_ty: Type,
305    pub cast_kind: CastKind,
306}
307
308/// Dispatch hint for `emit_narrowing_cast`. Variants carry **no Wasm-level
309/// indices** — those live in codegen's own registries.
310#[derive(Copy, Clone, Debug, PartialEq, Eq)]
311pub enum CastKind {
312    /// `T | null` → `T`. Codegen emits `ref.as_non_null`.
313    NonNull,
314    /// Boxed primitive → unboxed primitive value. Codegen emits an
315    /// (optional) `ref.as_non_null` (when `from_ty` is nullable), a
316    /// `ref.cast` to the wrapper subtype (`$BoxedNumber`/`$BoxedBoolean`),
317    /// and `struct.get $value`.
318    Unbox,
319    /// Ref-typed value → more-specific ref subtype. Codegen emits `ref.cast`
320    /// to the registered subtype when `to_ty`'s Wasm repr is tighter than
321    /// `from_ty`'s; emits no instruction when reps match.
322    RefSubtype,
323}
324
325/// Keep in sync with `emit_narrowing_cast` in `codegen/function_emitter/cast.rs`.
326pub fn cast_info_for(from_ty: Type, narrowed_ty: Type) -> CastInfo {
327    let cast_kind = match &narrowed_ty {
328        Type::Number | Type::NumberLiteral(_) | Type::Boolean | Type::BooleanLiteral(_) => {
329            CastKind::Unbox
330        }
331        Type::Object { .. } | Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
332            CastKind::RefSubtype
333        }
334        // unknown → ref-typed concrete shape uses the same downcast.
335        Type::String
336        | Type::StringLiteral(_)
337        | Type::TemplateLiteral { .. }
338        | Type::Array(_)
339        | Type::Tuple(_)
340        | Type::Uint8Array
341            if matches!(from_ty.peel(), Type::Unknown) =>
342        {
343            CastKind::RefSubtype
344        }
345        _ => CastKind::NonNull,
346    };
347    CastInfo {
348        from_ty,
349        to_ty: narrowed_ty,
350        cast_kind,
351    }
352}
353
354/// One field-path narrowing that survived the `if`-join and needs a
355/// fresh shadow Wasm local materialized at the join site.
356#[derive(Clone, Debug)]
357pub struct PendingPostIfMaterialization {
358    pub path: ReferencePath,
359    pub source: ExprId,
360    pub binding: Ident,
361    pub cast_info: CastInfo,
362    pub span: crate::Span,
363}
364
365/// Diagnostic-only record of why a narrowing was invalidated. Surfaced as a
366/// `help:` line so the LLM doesn't have to guess why a narrowing went away.
367#[derive(Clone, Debug)]
368pub enum InvalidationReason {
369    /// A proven predicate had no source that codegen could rebuild in its region.
370    ShapeUnrebuildable { narrowed_ty: Type },
371    /// A write reached the path (or a prefix of it) and falsified the guard:
372    /// a field or index write, a loop back edge, or a merge of an inner block's
373    /// writes. Narrowings on the path and any extension of it are dropped.
374    Write { span: crate::Span },
375    /// An identifier was reassigned. Distinguished from [`Self::Write`] only so
376    /// the diagnostic can name the statement the reader is looking at.
377    Reassignment { span: crate::Span },
378    /// Path root is in captured_mutators; the engine refused to install a NarrowedView.
379    /// closure_span: one offending closure declaration.
380    CapturedMutator { closure_span: Option<crate::Span> },
381}
382
383impl InvalidationReason {
384    pub fn invalidates(&self) -> bool {
385        !matches!(self, Self::ShapeUnrebuildable { .. })
386    }
387
388    pub fn span(&self) -> Option<crate::Span> {
389        match self {
390            Self::ShapeUnrebuildable { .. } => None,
391            Self::Write { span } => Some(*span),
392            Self::Reassignment { span } => Some(*span),
393            Self::CapturedMutator { closure_span } => *closure_span,
394        }
395    }
396}
397
398/// A union's discriminant: the property every member types with a distinct
399/// literal, and which literal names which member.
400pub type Discriminant = (String, DiscriminantTable);
401
402/// The literals that name a union's members. Each literal names one member,
403/// except `undefined`: every member whose discriminant is optional reads it
404/// when the field is absent, so it may name several.
405#[derive(Clone, Debug, Default, PartialEq)]
406pub struct DiscriminantTable {
407    entries: Vec<(LiteralValue, VariantIdx)>,
408}
409
410/// How many members `undefined` may name in a [`DiscriminantTable`].
411#[derive(Clone, Copy, PartialEq, Eq)]
412enum UndefinedNames {
413    /// An object field: every member with an optional discriminant reads it.
414    Several,
415    /// A tuple position, which is always present: like any literal, one.
416    One,
417}
418
419impl DiscriminantTable {
420    pub fn iter(&self) -> impl Iterator<Item = (&LiteralValue, &VariantIdx)> {
421        self.entries.iter().map(|(value, variant)| (value, variant))
422    }
423
424    /// The member `value` names, when it names exactly one.
425    pub fn get(&self, value: &LiteralValue) -> Option<VariantIdx> {
426        let mut variants = self.variants_for(value);
427        let variant = variants.next()?;
428        variants.next().is_none().then_some(variant)
429    }
430
431    /// Every member `value` names.
432    pub fn variants_for<'a>(
433        &'a self,
434        value: &'a LiteralValue,
435    ) -> impl Iterator<Item = VariantIdx> + 'a {
436        self.entries
437            .iter()
438            .filter(move |(entry, _)| entry == value)
439            .map(|(_, variant)| *variant)
440    }
441
442    /// Whether some literal other than `value` also names `variant`, so a
443    /// test against `value` alone can't rule it out.
444    pub fn has_other_values(&self, variant: VariantIdx, value: &LiteralValue) -> bool {
445        self.entries
446            .iter()
447            .any(|(entry, named)| *named == variant && entry != value)
448    }
449
450    /// Whether a `switch` covering `covered` handles `variant`: every value
451    /// that names it is a case.
452    pub fn is_covered(&self, variant: VariantIdx, covered: &BTreeSet<LiteralValue>) -> bool {
453        self.entries
454            .iter()
455            .filter(|(_, named)| *named == variant)
456            .all(|(value, _)| covered.contains(value))
457    }
458
459    /// The `members` a `switch` covering `covered` still leaves possible.
460    pub fn uncovered_members(
461        &self,
462        members: &[Type],
463        covered: &BTreeSet<LiteralValue>,
464    ) -> Vec<Type> {
465        members
466            .iter()
467            .enumerate()
468            .filter(|(index, _)| {
469                u32::try_from(*index)
470                    .map_or(true, |index| !self.is_covered(VariantIdx(index), covered))
471            })
472            .map(|(_, member)| member.clone())
473            .collect()
474    }
475
476    /// The values `covered` leaves out, each once, in table order.
477    pub fn uncovered(&self, covered: &BTreeSet<LiteralValue>) -> Vec<LiteralValue> {
478        let mut missing: Vec<LiteralValue> = Vec::new();
479        for (value, _) in &self.entries {
480            if !covered.contains(value) && !missing.contains(value) {
481                missing.push(value.clone());
482            }
483        }
484        missing
485    }
486
487    /// `members` split by a test against `literal`: the members it names,
488    /// and the ones still possible when the test fails. A named member stays
489    /// on both sides when another value also names it, as an optional
490    /// `kind?: "a"` does with `undefined`. `None` when `literal` names none.
491    pub fn partition(
492        &self,
493        members: &[Type],
494        literal: &LiteralValue,
495    ) -> Option<(Vec<Type>, Vec<Type>)> {
496        let named: Vec<VariantIdx> = self.variants_for(literal).collect();
497        if named.is_empty() {
498            return None;
499        }
500        let mut matched = Vec::with_capacity(named.len());
501        let mut remaining = Vec::new();
502        for (index, member) in members.iter().enumerate() {
503            let variant = VariantIdx(u32::try_from(index).ok()?);
504            if !named.contains(&variant) {
505                remaining.push(member.clone());
506                continue;
507            }
508            matched.push(member.clone());
509            if self.has_other_values(variant, literal) {
510                remaining.push(member.clone());
511            }
512        }
513        Some((matched, remaining))
514    }
515
516    /// Add `value` for `variant`, unless it already names a member: then the
517    /// key or position is no discriminant, and this returns `None`.
518    fn insert(
519        &mut self,
520        value: LiteralValue,
521        variant: VariantIdx,
522        undefined: UndefinedNames,
523    ) -> Option<()> {
524        let shareable = undefined == UndefinedNames::Several && value == LiteralValue::Undefined;
525        if !shareable && self.entries.iter().any(|(entry, _)| *entry == value) {
526            return None;
527        }
528        self.entries.push((value, variant));
529        Some(())
530    }
531}
532
533/// Variant index in a `Type::Union`'s canonical member list.
534#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
535pub struct VariantIdx(pub u32);
536
537/// Result of analyzing a `Type::Union` for a discriminant property.
538///
539/// `Some((key_property, constituent_map))` when there exists a property that:
540///
541/// - appears on every member of the union,
542/// - has a unit (literal) type on every member,
543/// - uniquely identifies each member.
544///
545/// `None` otherwise.
546///
547/// The key is returned as a bare `String` (not `Ident`) — the discriminant
548/// property name has no meaningful source span at the union level; callers
549/// compare it against `PathElem::Field(String)` from a `ReferencePath` anyway.
550pub fn union_discriminant(members: &[Type]) -> Option<Discriminant> {
551    // Inline object shapes only. A nominal member (interface, class) needs the
552    // type registry to expand, which callers that have an `Inferer` supply
553    // through [`discriminant_from_shapes`].
554    //
555    // Peel aliases so `type Shape = Circle | Rectangle` finds its discriminant.
556    let shapes = inline_member_shapes(members)?;
557    discriminant_from_shapes(&shapes)
558}
559
560/// Each member's fields, when every member is an inline object type.
561/// Nominal members need the type registry; see `union_member_shapes`.
562fn inline_member_shapes(
563    members: &[Type],
564) -> Option<Vec<BTreeMap<String, crate::types::ObjectField>>> {
565    members
566        .iter()
567        .map(|m| match m.peel() {
568            Type::Object { fields, .. } => Some(fields.clone()),
569            _ => None,
570        })
571        .collect()
572}
573
574/// [`union_discriminant`] for the field a test names, rather than the one
575/// the union would pick on its own. Inline object members only; the
576/// typechecker's `Inferer::union_discriminant_for_key` also expands nominal ones.
577pub fn inline_union_discriminant_for_key(members: &[Type], key: &str) -> Option<DiscriminantTable> {
578    let shapes = inline_member_shapes(members)?;
579    discriminant_table_for_key(&shapes, key)
580}
581
582/// [`union_discriminant`] over already-expanded member shapes — one map per
583/// union member, in member order. A key every member requires wins over one
584/// a member may omit, so an optional `code?: 1` beside `kind` doesn't hide
585/// the tag; ties go to the first key in order.
586pub fn discriminant_from_shapes(
587    shapes: &[BTreeMap<String, crate::types::ObjectField>],
588) -> Option<Discriminant> {
589    let first = shapes.first()?;
590    let may_be_omitted = |key: &String| {
591        shapes
592            .iter()
593            .any(|shape| shape.get(key).is_some_and(|f| f.optional))
594    };
595    let (required, omittable): (Vec<&String>, Vec<&String>) =
596        first.keys().partition(|key| !may_be_omitted(key));
597    required
598        .into_iter()
599        .chain(omittable)
600        .find_map(|key| Some((key.clone(), discriminant_table_for_key(shapes, key)?)))
601}
602
603/// The table `key` makes a discriminant of, when every member types it with a
604/// literal and no explicit literal names two members.
605pub fn discriminant_table_for_key(
606    shapes: &[BTreeMap<String, crate::types::ObjectField>],
607    key: &str,
608) -> Option<DiscriminantTable> {
609    if shapes.len() < 2 {
610        return None;
611    }
612    let mut table = DiscriminantTable::default();
613    for (idx, shape) in shapes.iter().enumerate() {
614        let field = shape.get(key)?;
615        let lit = unit_value(&field.ty)?;
616        // An optional `kind?: "err"` reads `undefined` when absent, so it
617        // names its variant by both values, as TypeScript narrows it.
618        let absent =
619            (field.optional && lit != LiteralValue::Undefined).then_some(LiteralValue::Undefined);
620        let variant = VariantIdx(u32::try_from(idx).ok()?);
621        for value in std::iter::once(lit).chain(absent) {
622            table.insert(value, variant, UndefinedNames::Several)?;
623        }
624    }
625    Some(table)
626}
627
628/// The literals that name each member of a union of tuples at `position`,
629/// when every member has a literal there. A tuple shorter than `position`
630/// reads `undefined`, as a destructuring pattern reads it. A member whose
631/// position is optional may hold either, so then the position is no
632/// discriminant.
633pub fn tuple_position_discriminant(members: &[Type], position: usize) -> Option<DiscriminantTable> {
634    if members.len() < 2 {
635        return None;
636    }
637    let mut table = DiscriminantTable::default();
638    for (idx, member) in members.iter().enumerate() {
639        let Type::Tuple(elements) = member.peel() else {
640            return None;
641        };
642        let lit = if position >= elements.len() {
643            LiteralValue::Undefined
644        } else if position < elements.required_len() {
645            elements.get(position).and_then(unit_value)?
646        } else {
647            return None;
648        };
649        // Present or not, a position holds one value, so each one names one
650        // member, `undefined` included.
651        table.insert(
652            lit,
653            VariantIdx(u32::try_from(idx).ok()?),
654            UndefinedNames::One,
655        )?;
656    }
657    Some(table)
658}
659
660/// The literal a `case` label compares against: `None` for an expression
661/// label whose type is no single literal.
662pub fn switch_value_literal(value: &crate::TypedSwitchValue) -> Option<LiteralValue> {
663    Some(match value {
664        crate::TypedSwitchValue::String { value, .. } => LiteralValue::String(value.clone()),
665        crate::TypedSwitchValue::Number { value, .. } => {
666            LiteralValue::Number(crate::types::LiteralF64(*value))
667        }
668        crate::TypedSwitchValue::Boolean { value, .. } => LiteralValue::Boolean(*value),
669        crate::TypedSwitchValue::Null { .. } => LiteralValue::Null,
670        crate::TypedSwitchValue::Undefined { .. } => LiteralValue::Undefined,
671        crate::TypedSwitchValue::Expr { literal, .. } => return literal.clone(),
672        crate::TypedSwitchValue::Enum { value, .. } => match value {
673            crate::EnumVariantPayload::Number(n) => {
674                LiteralValue::Number(crate::types::LiteralF64(*n))
675            }
676            crate::EnumVariantPayload::String(s) => LiteralValue::String(s.clone()),
677        },
678    })
679}
680
681/// The literal type a compared literal value has.
682pub fn literal_type(literal: &LiteralValue) -> Type {
683    match literal {
684        LiteralValue::String(s) => Type::StringLiteral(s.clone()),
685        LiteralValue::Number(n) => Type::NumberLiteral(*n),
686        LiteralValue::Boolean(b) => Type::BooleanLiteral(*b),
687        LiteralValue::BigInt(digits) => Type::BigIntLiteral(digits.clone()),
688        LiteralValue::Null => Type::Null,
689        LiteralValue::Undefined => Type::Undefined,
690    }
691}
692
693/// Whether `ty` is, or has a member that is, a literal, `null` or
694/// `undefined`: what makes a property a discriminant in TypeScript. An enum
695/// is the union of its members' literals.
696pub fn has_unit_member(ty: &Type) -> bool {
697    match ty.peel() {
698        Type::Union(members) => members.iter().any(has_unit_member),
699        Type::Null | Type::Undefined | Type::NumberEnum { .. } | Type::StringEnum { .. } => true,
700        other => unit_literal_value(other).is_some(),
701    }
702}
703
704/// Whether `case` labels for the `covered` literals, `null` and `undefined`
705/// included, match every value of `ty`.
706pub fn is_covered_by_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> bool {
707    match ty.peel() {
708        Type::Union(members) => members
709            .iter()
710            .all(|member| is_covered_by_literals(member, covered)),
711        // `boolean` is `true | false`.
712        Type::Boolean => [true, false]
713            .into_iter()
714            .all(|value| covered.contains(&LiteralValue::Boolean(value))),
715        other => unit_value(other).is_some_and(|value| covered.contains(&value)),
716    }
717}
718
719/// Whether `ty` is a type parameter, or a union with one.
720pub fn has_type_parameter_member(ty: &Type) -> bool {
721    match ty.peel() {
722        Type::Union(members) => members.iter().any(has_type_parameter_member),
723        other => matches!(other, Type::TypeVar(_) | Type::GenericParam { .. }),
724    }
725}
726
727pub(super) fn unit_literal_value(ty: &Type) -> Option<LiteralValue> {
728    match ty.peel() {
729        Type::StringLiteral(s) => Some(LiteralValue::String(s.clone())),
730        Type::NumberLiteral(n) => Some(LiteralValue::Number(*n)),
731        Type::BooleanLiteral(b) => Some(LiteralValue::Boolean(*b)),
732        Type::BigIntLiteral(digits) => Some(LiteralValue::BigInt(digits.clone())),
733        other => LiteralValue::of_enum_member(other),
734    }
735}
736
737/// [`unit_literal_value`], with `null` and `undefined` as values too: what a
738/// discriminant or a `case` label may name.
739pub(super) fn unit_value(ty: &Type) -> Option<LiteralValue> {
740    match ty.peel() {
741        Type::Null => Some(LiteralValue::Null),
742        Type::Undefined | Type::Void => Some(LiteralValue::Undefined),
743        other => unit_literal_value(other),
744    }
745}
746
747/// Strip `Type::Null` from a union. Returns `Type::Error` for `Type::Null`
748/// itself (empty union has no representation).
749pub fn strip_null(ty: &Type) -> Type {
750    if let Type::Refined {
751        original,
752        ty: shape,
753    } = ty.without_aliases()
754    {
755        return preserve_refinement(original, strip_null(shape));
756    }
757    match ty.peel() {
758        Type::Null => Type::Error,
759        Type::Union(members) => without_nullish_refinements(Type::union(
760            members
761                .iter()
762                .filter(|m| !matches!(m.peel(), Type::Null))
763                .map(|m| {
764                    if is_type_parameter_or_its_narrowing(m) {
765                        strip_null(m)
766                    } else {
767                        m.clone()
768                    }
769                })
770                .collect(),
771        )),
772        // `unknown` is `{} | null | undefined`, as in TypeScript.
773        Type::Unknown => Type::union(vec![Type::empty_object(), Type::Undefined]),
774        // A type parameter, or a form over one (`T[K]`), may hold any value:
775        // `T` minus `null` is `T & ({} | undefined)`.
776        peeled if holds_any_value_of_a_parameter(peeled) => {
777            preserve_refinement(ty, strip_null(&Type::Unknown))
778        }
779        // `ty` itself, not its peel: peeling drops a `readonly` wrapper.
780        _ => ty.clone(),
781    }
782}
783
784/// Remove only `undefined`, preserving strict equality's distinction from null.
785/// A `void` member stays: a `void` result may hold any value, so only a
786/// `typeof` test narrows it.
787pub fn strip_undefined(ty: &Type) -> Type {
788    if let Type::Refined {
789        original,
790        ty: shape,
791    } = ty.without_aliases()
792    {
793        return preserve_refinement(original, strip_undefined(shape));
794    }
795    match ty.peel() {
796        Type::Undefined => Type::Error,
797        Type::Union(members) => without_nullish_refinements(Type::union(
798            members
799                .iter()
800                .filter(|member| !matches!(member.peel(), Type::Undefined))
801                .map(|member| {
802                    if is_type_parameter_or_its_narrowing(member) {
803                        strip_undefined(member)
804                    } else {
805                        member.clone()
806                    }
807                })
808                .collect(),
809        )),
810        Type::Unknown => Type::union(vec![Type::empty_object(), Type::Null]),
811        peeled if holds_any_value_of_a_parameter(peeled) => {
812            preserve_refinement(ty, strip_undefined(&Type::Unknown))
813        }
814        _ => ty.clone(),
815    }
816}
817
818/// A union member that a nullish test narrows in place: a type parameter or
819/// a form over one (`T[K]`), or a narrowing of one such as `T & ({} |
820/// null)`, whose shape `peel` hides.
821fn is_type_parameter_or_its_narrowing(member: &Type) -> bool {
822    holds_any_value_of_a_parameter(member.peel()) || member.refines_type_parameter()
823}
824
825/// Remove both nullish alternatives for optional access and non-null assertions.
826pub fn strip_nullish(ty: &Type) -> Type {
827    strip_undefined(&strip_null(ty))
828}
829
830/// `in` retains optional members in both branches: declaring a property does
831/// not prove that its value is defined. A present optional property can still
832/// hold `undefined`, so its read type retains optionality.
833pub fn narrow_field_presence(
834    receiver_ty: &Type,
835    field: &str,
836    lookup: &dyn Fn(&Type, &str) -> Option<crate::ObjectField>,
837) -> (Type, Type) {
838    if let Type::Refined {
839        original,
840        ty: shape,
841    } = receiver_ty.without_aliases()
842    {
843        let (present, absent) = narrow_field_presence(shape, field, lookup);
844        return (
845            preserve_refinement(original, present),
846            preserve_refinement(original, absent),
847        );
848    }
849    match receiver_ty.peel() {
850        Type::Unknown | Type::NonPrimitive => {
851            let fields = BTreeMap::from([(
852                field.to_string(),
853                crate::ObjectField::required(Type::Unknown),
854            )]);
855            (
856                Type::Object {
857                    index: None,
858                    fields,
859                },
860                receiver_ty.clone(),
861            )
862        }
863        Type::Object { fields, .. } => {
864            let mut present = fields.clone();
865            present
866                .entry(field.to_string())
867                .or_insert_with(|| crate::ObjectField::required(Type::Unknown));
868            let absent = match fields.get(field) {
869                Some(f) if !f.optional => Type::Error,
870                _ => receiver_ty.clone(),
871            };
872            (
873                Type::Object {
874                    index: None,
875                    fields: present,
876                },
877                absent,
878            )
879        }
880        Type::Union(members) => {
881            let has = members
882                .iter()
883                .filter(|m| lookup(m, field).is_some())
884                .cloned()
885                .collect();
886            let lacks = members
887                .iter()
888                .filter(|m| lookup(m, field).is_none_or(|f| f.optional))
889                .cloned()
890                .collect();
891            (Type::union(has), Type::union(lacks))
892        }
893        Type::InterfaceRef { .. } | Type::ClassRef { .. } | Type::Array(_) | Type::Tuple(_) => {
894            let absent = if lookup(receiver_ty, field).is_some_and(|f| !f.optional) {
895                Type::Error
896            } else {
897                receiver_ty.clone()
898            };
899            (receiver_ty.clone(), absent)
900        }
901        _ => (Type::Error, Type::Error),
902    }
903}
904
905/// `BTreeMap`, not `HashMap`, so a walk over an env is reproducible. Under a hash map
906/// `enter_closure_narrow_boundary` would hand the same program different `#narrow_<N>`
907/// numbering and different `ExprId`s per compile, and `install_joined_narrowings`'s
908/// `joined_rebound` and `retain_emittable_views`'s `candidates` would inherit that order.
909/// Only the region nesting reaches the emitted Wasm — [`wrap_order`] pins that, and
910/// locals are unnamed there — so this is defence in depth for everything else.
911#[derive(Clone, Debug, Default)]
912pub struct NarrowEnv {
913    views: BTreeMap<ReferencePath, NarrowedView>,
914    /// Non-optional locals assigned on every reaching path. Unlike a value
915    /// narrowing, this fact survives later writes and captured mutations.
916    pub definitely_assigned: BTreeSet<ReferencePath>,
917    /// Predicate drops belong to this outcome, never to the condition's parent
918    /// scope. A consumer that installs no frame simply discards these records.
919    pub dropped: BTreeMap<ReferencePath, Type>,
920}
921
922impl NarrowEnv {
923    pub fn new() -> Self {
924        Self::default()
925    }
926
927    pub fn insert(&mut self, path: ReferencePath, view: NarrowedView) -> Option<NarrowedView> {
928        self.dropped.remove(&path);
929        self.views.insert(path, view)
930    }
931
932    pub fn extend_env(&mut self, other: Self) {
933        self.definitely_assigned.extend(other.definitely_assigned);
934        self.dropped.extend(other.dropped);
935        self.extend(other.views);
936    }
937
938    pub fn clear(&mut self) {
939        self.definitely_assigned.clear();
940        self.views.clear();
941        self.dropped.clear();
942    }
943
944    pub fn is_empty(&self) -> bool {
945        self.views.is_empty() && self.definitely_assigned.is_empty()
946    }
947}
948
949impl std::ops::Deref for NarrowEnv {
950    type Target = BTreeMap<ReferencePath, NarrowedView>;
951    fn deref(&self) -> &Self::Target {
952        &self.views
953    }
954}
955
956impl std::ops::DerefMut for NarrowEnv {
957    fn deref_mut(&mut self) -> &mut Self::Target {
958        &mut self.views
959    }
960}
961
962impl IntoIterator for NarrowEnv {
963    type Item = (ReferencePath, NarrowedView);
964    type IntoIter = std::collections::btree_map::IntoIter<ReferencePath, NarrowedView>;
965    fn into_iter(self) -> Self::IntoIter {
966        self.views.into_iter()
967    }
968}
969
970impl<'a> IntoIterator for &'a NarrowEnv {
971    type Item = (&'a ReferencePath, &'a NarrowedView);
972    type IntoIter = std::collections::btree_map::Iter<'a, ReferencePath, NarrowedView>;
973    fn into_iter(self) -> Self::IntoIter {
974        self.views.iter()
975    }
976}
977
978impl Extend<(ReferencePath, NarrowedView)> for NarrowEnv {
979    fn extend<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(&mut self, iter: I) {
980        for (path, view) in iter {
981            self.insert(path, view);
982        }
983    }
984}
985
986impl FromIterator<(ReferencePath, NarrowedView)> for NarrowEnv {
987    fn from_iter<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(iter: I) -> Self {
988        let mut env = Self::new();
989        env.extend(iter);
990        env
991    }
992}
993
994/// Nesting order for the wrap-the-body-in-one-region-per-path loops: longest chain
995/// first, so every path is wrapped *inside* the prefixes its source reads. Ties break
996/// on the path itself, making this a total order — the emitted nesting must not depend
997/// on the order the caller happened to iterate its env in.
998pub fn wrap_order(a: &ReferencePath, b: &ReferencePath) -> std::cmp::Ordering {
999    b.chain.len().cmp(&a.chain.len()).then_with(|| a.cmp(b))
1000}
1001
1002/// Compares readable types and definite assignment. View bindings, sources and
1003/// predicate metadata are excluded: fresh mints do not change these flow facts.
1004pub fn env_flow_facts_equal(a: &NarrowEnv, b: &NarrowEnv) -> bool {
1005    if a.len() != b.len() || a.definitely_assigned != b.definitely_assigned {
1006        return false;
1007    }
1008    for (path, a_view) in a {
1009        match b.get(path) {
1010            Some(b_view) if a_view.narrowed_ty == b_view.narrowed_ty => continue,
1011            _ => return false,
1012        }
1013    }
1014    true
1015}
1016
1017/// JS-truthiness classification of a single (non-union) type: which runtime
1018/// test decides truthiness for values of that member.
1019#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1020pub enum TruthinessClass {
1021    AlwaysTruthy,
1022    AlwaysFalsy,
1023    /// Falsy iff `0`, `-0`, or `NaN`. Includes number enums (may hold `0`).
1024    NumberLike,
1025    /// Falsy iff `""`. Includes string enums (may hold `""`).
1026    StringLike,
1027    /// Falsy iff `false`.
1028    BooleanLike,
1029    /// Falsy iff `0n`.
1030    BigIntLike,
1031    /// Erased at compile time — needs the full runtime dispatch.
1032    Dynamic,
1033}
1034
1035pub fn truthiness_class(member: &Type) -> TruthinessClass {
1036    use TruthinessClass::*;
1037    match member.peel() {
1038        Type::Null | Type::Undefined => AlwaysFalsy,
1039        Type::Boolean => BooleanLike,
1040        Type::BooleanLiteral(true) => AlwaysTruthy,
1041        Type::BooleanLiteral(false) => AlwaysFalsy,
1042        Type::Number => NumberLike,
1043        Type::NumberLiteral(n) => {
1044            if n.0 == 0.0 || n.0.is_nan() {
1045                AlwaysFalsy
1046            } else {
1047                AlwaysTruthy
1048            }
1049        }
1050        Type::String => StringLike,
1051        Type::StringLiteral(s) => {
1052            if s.is_empty() {
1053                AlwaysFalsy
1054            } else {
1055                AlwaysTruthy
1056            }
1057        }
1058        Type::BigInt => BigIntLike,
1059        Type::BigIntLiteral(digits) => {
1060            if digits == "0" {
1061                AlwaysFalsy
1062            } else {
1063                AlwaysTruthy
1064            }
1065        }
1066        Type::NumberEnum {
1067            member: Some(member),
1068            ..
1069        } => truthiness_class(&Type::NumberLiteral(member.value)),
1070        Type::StringEnum {
1071            member: Some(member),
1072            ..
1073        } => truthiness_class(&Type::StringLiteral(member.value.clone())),
1074        Type::NumberEnum { .. } => NumberLike,
1075        Type::StringEnum { .. } => StringLike,
1076        // `{}` admits every value but `null` and `undefined`, falsy primitives
1077        // included.
1078        empty if empty.is_empty_object() => Dynamic,
1079        Type::Object { .. }
1080        | Type::Array(_)
1081        | Type::Tuple(_)
1082        | Type::Function { .. }
1083        | Type::Uint8Array
1084        | Type::InterfaceRef { .. }
1085        | Type::ClassRef { .. }
1086        | Type::NonPrimitive => AlwaysTruthy,
1087        _ => Dynamic,
1088    }
1089}
1090
1091/// Whether every member of `ty` is always truthy or always falsy, so a
1092/// truthiness test that rules a member out rules out every value it holds.
1093pub fn has_known_truthiness(ty: &Type) -> bool {
1094    union_members(ty).into_iter().all(|member| {
1095        matches!(
1096            truthiness_class(member),
1097            TruthinessClass::AlwaysTruthy | TruthinessClass::AlwaysFalsy
1098        )
1099    })
1100}
1101
1102/// The type of `!value` for a `value` of type `ty`, as TypeScript has it:
1103/// `false` when every value it holds is truthy, `true` when every one is
1104/// falsy, and `boolean` otherwise.
1105pub fn negation_type(ty: &Type) -> Type {
1106    let classes: Vec<TruthinessClass> = union_members(ty)
1107        .into_iter()
1108        .map(truthiness_class)
1109        .collect();
1110    if classes
1111        .iter()
1112        .all(|class| *class == TruthinessClass::AlwaysTruthy)
1113    {
1114        return Type::BooleanLiteral(false);
1115    }
1116    if classes
1117        .iter()
1118        .all(|class| *class == TruthinessClass::AlwaysFalsy)
1119    {
1120        return Type::BooleanLiteral(true);
1121    }
1122    Type::Boolean
1123}
1124
1125pub(super) fn union_members(ty: &Type) -> Vec<&Type> {
1126    match ty.peel() {
1127        Type::Union(members) => members.iter().collect(),
1128        // `ty` itself, not its peel: peeling drops a `readonly` wrapper.
1129        _ => vec![ty],
1130    }
1131}
1132
1133/// The type of `x` where `x` is known truthy: drop `null` and falsy literal
1134/// types per member. Falsy-capable base types stay whole (matching TS, which
1135/// doesn't invent a "non-empty string" type).
1136pub fn truthy_part(ty: &Type) -> Type {
1137    if let Type::Refined {
1138        original,
1139        ty: shape,
1140    } = ty.without_aliases()
1141    {
1142        return preserve_refinement(original, truthy_part(shape));
1143    }
1144    // `boolean` is exactly `true | false` (spec §1.2), so its truthy part is `true`.
1145    let kept: Vec<Type> = union_members(ty)
1146        .into_iter()
1147        .filter(|m| truthiness_class(m) != TruthinessClass::AlwaysFalsy)
1148        .map(|m| match m.without_aliases() {
1149            Type::Boolean => Type::BooleanLiteral(true),
1150            // A truthy `T` is `T & {}`, as in TypeScript, and so is a truthy
1151            // form over one (`Extract<T, string | undefined> & {}`).
1152            Type::GenericParam { .. } => intersect_with(m, TypeFacts::TRUTHY),
1153            _ if holds_any_value_of_a_parameter(m.peel()) => intersect_with(m, TypeFacts::TRUTHY),
1154            _ => m.clone(),
1155        })
1156        .collect();
1157    Type::union(kept)
1158}
1159
1160/// Whether no value of `ty` is falsy.
1161pub fn is_never_falsy(ty: &Type) -> bool {
1162    union_members(ty)
1163        .into_iter()
1164        .all(|m| truthiness_class(m) == TruthinessClass::AlwaysTruthy)
1165}
1166
1167/// The type of `x` where `x` is known falsy: keep `null` and falsy literals,
1168/// collapse `string` to `""` and `boolean` to `false`, drop never-falsy
1169/// reference types. `{}` stays `{}`: it may hold `0` or `""`, though
1170/// TypeScript's `&&` drops it. `number` stays `number` — a `0` literal would
1171/// be unsound for `NaN`/`-0`.
1172pub fn falsy_part(ty: &Type) -> Type {
1173    if let Type::Refined {
1174        original,
1175        ty: shape,
1176    } = ty.without_aliases()
1177    {
1178        return preserve_refinement(original, falsy_part(shape));
1179    }
1180    let kept: Vec<Type> = union_members(ty)
1181        .into_iter()
1182        .filter_map(|m| match truthiness_class(m) {
1183            TruthinessClass::AlwaysTruthy => None,
1184            TruthinessClass::StringLike => Some(match m.peel() {
1185                Type::String => Type::StringLiteral(String::new()),
1186                _ => m.clone(),
1187            }),
1188            _ => Some(match m.without_aliases() {
1189                Type::Boolean => Type::BooleanLiteral(false),
1190                _ => m.clone(),
1191            }),
1192        })
1193        .collect();
1194    Type::union(kept)
1195}
1196
1197/// True when every falsy value `ty` can hold is `null` — i.e. a truthiness
1198/// test is exactly a null test, so the false branch may assert `EQ_NULL`.
1199pub fn falsy_values_are_only_null(ty: &Type) -> bool {
1200    union_members(ty).into_iter().all(|m| {
1201        matches!(m.peel(), Type::Null) || truthiness_class(m) == TruthinessClass::AlwaysTruthy
1202    })
1203}
1204
1205pub fn type_matches_facts(ty: &Type, facts: TypeFacts) -> bool {
1206    // Aliases are transparent for type identity — peel before matching so an
1207    // aliased primitive/union (e.g. `type NS = number | string`) classifies the
1208    // same as its inline spelling.
1209    let ty = ty.peel();
1210    // TRUTHY/FALSY subsume the null tests (truthy ⇒ non-null), so they are
1211    // checked first: `TRUTHY | NE_NULL` filters by truthiness, bare `NE_NULL`
1212    // stays a pure null test.
1213    if facts.contains(TypeFacts::TRUTHY) {
1214        return truthiness_class(ty) != TruthinessClass::AlwaysFalsy;
1215    }
1216    if facts.contains(TypeFacts::FALSY) {
1217        return truthiness_class(ty) != TruthinessClass::AlwaysTruthy;
1218    }
1219    if facts.contains(TypeFacts::NE_NULL) || facts.contains(TypeFacts::NE_UNDEFINED) {
1220        return !(facts.contains(TypeFacts::NE_NULL) && matches!(ty, Type::Null)
1221            || facts.contains(TypeFacts::NE_UNDEFINED) && matches!(ty, Type::Undefined));
1222    }
1223    // `void` is not `undefined` here: `intersect_with` narrows it on a
1224    // `typeof` test, and `subtract` never rules it out.
1225    if facts.contains(TypeFacts::IS_UNDEFINED) {
1226        return matches!(ty, Type::Undefined);
1227    }
1228    if facts.contains(TypeFacts::EQ_UNDEFINED) {
1229        return matches!(ty, Type::Undefined);
1230    }
1231    if facts.contains(TypeFacts::EQ_NULL) {
1232        return matches!(ty, Type::Null);
1233    }
1234    if facts.contains(TypeFacts::IS_BIGINT) {
1235        return matches!(ty, Type::BigInt);
1236    }
1237    if facts.contains(TypeFacts::IS_NUMBER) {
1238        // An enum member's `typeof` is its underlying primitive, as in TypeScript.
1239        return matches!(
1240            ty,
1241            Type::Number | Type::NumberLiteral(_) | Type::NumberEnum { .. }
1242        );
1243    }
1244    if facts.contains(TypeFacts::IS_STRING) {
1245        return matches!(
1246            ty,
1247            Type::String
1248                | Type::StringLiteral(_)
1249                | Type::TemplateLiteral { .. }
1250                | Type::StringEnum { .. }
1251        );
1252    }
1253    if facts.contains(TypeFacts::IS_BOOLEAN) {
1254        return matches!(ty, Type::Boolean | Type::BooleanLiteral(_));
1255    }
1256    if facts.contains(TypeFacts::IS_ARRAY) {
1257        return matches!(ty, Type::Array(_));
1258    }
1259    if facts.contains(TypeFacts::IS_FUNCTION) {
1260        // `object` holds functions too; Submilli has no `Function` type to
1261        // narrow it to, so it stays `object`.
1262        return matches!(ty, Type::Function { .. } | Type::NonPrimitive);
1263    }
1264    if facts.contains(TypeFacts::IS_OBJECT) {
1265        return is_typeof_object(ty);
1266    }
1267    false
1268}
1269
1270/// Does `typeof` answer `"object"` for a value of `ty`?
1271///
1272/// Exhaustive on purpose. `"object"` is the complement of the primitive tags,
1273/// so a type constructor with no arm here is a value with *no* `typeof` at all —
1274/// every one of the five tags folds to `false`, which is unsatisfiable and
1275/// silent. Matching exhaustively makes a new [`Type`] variant fail to compile
1276/// until someone decides its tag.
1277fn is_typeof_object(ty: &Type) -> bool {
1278    match ty {
1279        // A union reaches here from the tuple-element rollup, where the
1280        // question is whether the whole element answers the tag.
1281        Type::Union(members) => members.iter().all(is_typeof_object),
1282
1283        Type::Object { .. }
1284        | Type::Array(_)
1285        | Type::Tuple(_)
1286        | Type::InterfaceRef { .. }
1287        | Type::ClassRef { .. }
1288        | Type::Uint8Array
1289        | Type::NonPrimitive
1290        // `typeof null === "object"` — the JS quirk.
1291        | Type::Null => true,
1292
1293        Type::Undefined
1294        | Type::Number
1295        | Type::NumberLiteral(_)
1296        | Type::NumberEnum { .. }
1297        | Type::String
1298        | Type::StringLiteral(_) | Type::TemplateLiteral { .. }
1299        | Type::StringEnum { .. }
1300        | Type::Boolean
1301        | Type::BooleanLiteral(_)
1302        | Type::BigInt
1303        | Type::BigIntLiteral(_)
1304        | Type::Function { .. } => false,
1305
1306        // Not classifiable, for four different reasons: no value at all
1307        // (`Void`, `Never`), a diagnostic already reported (`Error`), erased at
1308        // runtime or bodyless (`TypeVar`, `GenericParam`, `Unknown`, and a
1309        // recursive alias's back-edge `AliasRef`). `fold_typeof_tag` bails on
1310        // the erased set before it ever asks, so `false` here only ever means
1311        // "narrows nothing", never "answers no tag".
1312        Type::Void
1313        | Type::Never
1314        | Type::Error
1315        | Type::Unknown
1316        | Type::TypeVar(_)
1317        | Type::GenericParam { .. }
1318        | Type::KeyOf(_)
1319        | Type::IndexedAccess { .. }
1320        | Type::AliasRef { .. } => false,
1321
1322        // Peeled by the caller.
1323        Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => false,
1324    }
1325}
1326
1327/// Keep only the union members of `ty` that match `facts`. For a
1328/// non-union `ty`, returns `ty` if matched or [`Type::Error`] if not. A
1329/// `typeof "object"` test is the exception: it turns `unknown`, `{}` and a
1330/// type parameter into the object types they hold ([`typeof_object_narrowing`]).
1331pub fn intersect_with(ty: &Type, facts: TypeFacts) -> Type {
1332    if let Type::Refined {
1333        original,
1334        ty: shape,
1335    } = ty.without_aliases()
1336    {
1337        return preserve_refinement(original, intersect_with(shape, facts));
1338    }
1339    let peeled = ty.peel();
1340    if facts == TypeFacts::IS_OBJECT
1341        && let Some(narrowed) = typeof_object_narrowing(ty, peeled)
1342    {
1343        return narrowed;
1344    }
1345    if let Some(asserted) = asserted_type_for_facts(facts) {
1346        match peeled {
1347            Type::Unknown => return asserted,
1348            // `{}` holds every primitive, so a `typeof` test pins it as it does
1349            // `unknown`. It holds no `null` or `undefined`, so those tests fall
1350            // through and rule it out.
1351            empty
1352                if empty.is_empty_object() && !matches!(asserted, Type::Null | Type::Undefined) =>
1353            {
1354                return asserted;
1355            }
1356            // `Array.isArray` pins an `object` to an array.
1357            Type::NonPrimitive if matches!(asserted, Type::Array(_)) => return asserted,
1358            form if holds_any_value_of_a_parameter(form) => {
1359                return Type::Refined {
1360                    original: Box::new(ty.clone()),
1361                    ty: Box::new(asserted),
1362                };
1363            }
1364            _ => {}
1365        }
1366    }
1367    if matches!(peeled, Type::Unknown) && tests_only_presence(facts) {
1368        return recombine_unknown(intersect_with(&unknown_as_union(), facts));
1369    }
1370    if holds_any_value_of_a_parameter(peeled) && tests_only_presence(facts) {
1371        return preserve_refinement(ty, intersect_with(&Type::Unknown, facts));
1372    }
1373    match peeled {
1374        Type::Union(members) => without_nullish_refinements(Type::union(
1375            members
1376                .iter()
1377                .map(|m| intersect_with(m, facts))
1378                .filter(|ty| !matches!(ty, Type::Error))
1379                .collect(),
1380        )),
1381        // An explicit typeof test supplies the runtime fact that a void
1382        // value's static type alone cannot establish.
1383        Type::Void if facts.contains(TypeFacts::IS_UNDEFINED) => Type::Undefined,
1384        // `boolean` is `true | false`, and truthiness splits it.
1385        Type::Boolean if facts == TypeFacts::TRUTHY => Type::BooleanLiteral(true),
1386        Type::Boolean if facts == TypeFacts::FALSY => Type::BooleanLiteral(false),
1387        _ if type_matches_facts(peeled, facts) => ty.clone(),
1388        _ if matches!(peeled, Type::Unknown) => Type::Unknown,
1389        _ => Type::Error,
1390    }
1391}
1392
1393/// What `typeof x === "object"` leaves of a type holding values of every
1394/// kind, as in TypeScript: `object | null` of `unknown`, `object` of `{}`,
1395/// and `(T & object) | (T & null)` of a type parameter `T`. `None` for the
1396/// types the general rules narrow.
1397fn typeof_object_narrowing(ty: &Type, peeled: &Type) -> Option<Type> {
1398    match peeled {
1399        Type::Unknown => Some(Type::union(vec![Type::NonPrimitive, Type::Null])),
1400        empty if empty.is_empty_object() => Some(Type::NonPrimitive),
1401        Type::GenericParam { .. } => Some(Type::union(
1402            [Type::NonPrimitive, Type::Null]
1403                .into_iter()
1404                .map(|shape| Type::Refined {
1405                    original: Box::new(ty.clone()),
1406                    ty: Box::new(shape),
1407                })
1408                .collect(),
1409        )),
1410        _ => None,
1411    }
1412}
1413
1414/// A body type parameter, or a utility, `keyof T` or `T[K]` over one: a type
1415/// whose values a test narrows by refinement (`T & {}`), as it knows none of
1416/// them statically.
1417fn holds_any_value_of_a_parameter(peeled: &Type) -> bool {
1418    peeled.lowers_to_erased_slot() && !matches!(peeled, Type::TypeVar(_))
1419}
1420
1421pub fn subtract(ty: &Type, facts: TypeFacts) -> Type {
1422    if let Type::Refined {
1423        original,
1424        ty: shape,
1425    } = ty.without_aliases()
1426    {
1427        return preserve_refinement(original, subtract(shape, facts));
1428    }
1429    let peeled = ty.peel();
1430    // `unknown` minus `null` or `undefined` is the rest of `{} | null |
1431    // undefined`, as in TypeScript; minus any other type it stays `unknown`.
1432    if matches!(peeled, Type::Unknown) {
1433        if tests_only_nullishness(facts) {
1434            return recombine_unknown(subtract(&unknown_as_union(), facts));
1435        }
1436        return Type::Unknown;
1437    }
1438    if holds_any_value_of_a_parameter(peeled) {
1439        return preserve_refinement(ty, subtract(&Type::Unknown, facts));
1440    }
1441    // A `void` result may hold any value, so no test rules it out: `typeof
1442    // v !== "undefined"` keeps it, though `typeof v === "undefined"` may
1443    // narrow it to `undefined`.
1444    let ruled_out = |member: &Type| {
1445        !matches!(member.peel(), Type::Void)
1446            && !typeof_tag_keeps(member, facts)
1447            && type_matches_facts(member, facts)
1448    };
1449    match peeled {
1450        Type::Union(members) => {
1451            let kept: Vec<Type> = members.iter().filter(|m| !ruled_out(m)).cloned().collect();
1452            Type::union(kept)
1453        }
1454        _ if !ruled_out(peeled) => ty.clone(),
1455        _ => Type::Error,
1456    }
1457}
1458
1459/// Whether a `typeof` tag in `facts` leaves `member` in place: `{}` holds a
1460/// value of every tag, and `object` holds functions too. As in TypeScript,
1461/// `typeof x !== "object"` does rule out `object`.
1462fn typeof_tag_keeps(member: &Type, facts: TypeFacts) -> bool {
1463    let peeled = member.peel();
1464    let tags_object = facts.contains(TypeFacts::IS_OBJECT);
1465    let tags_function = facts.contains(TypeFacts::IS_FUNCTION);
1466    if peeled.is_empty_object() {
1467        return tags_object || tags_function;
1468    }
1469    tags_function && matches!(peeled, Type::NonPrimitive)
1470}
1471
1472pub(super) fn with_source_refinement(source: &Type, shape: Type) -> Type {
1473    match source.without_aliases() {
1474        Type::Refined { original, .. } => preserve_refinement(original, shape),
1475        _ => shape,
1476    }
1477}
1478
1479pub(super) fn preserve_refinement(original: &Type, shape: Type) -> Type {
1480    if matches!(shape, Type::Error | Type::Never) {
1481        return shape;
1482    }
1483    // A shape that proves nothing leaves the type parameter as it was.
1484    if matches!(shape, Type::Unknown) {
1485        return original.clone();
1486    }
1487    Type::Refined {
1488        original: Box::new(original.clone()),
1489        ty: Box::new(shape),
1490    }
1491}
1492
1493/// Strip covered literal values from `ty`. When the residual is `Type::Never`,
1494/// all discriminant values were covered by `case` labels.
1495///
1496/// - **Union**: filter out covered literal members, keeping the rest (`number`
1497///   in `number | "a"`); collapses to `Never` when all drop.
1498/// - **Single literal**: `Never` if covered, unchanged otherwise.
1499/// - **Anything else**: returned unchanged.
1500pub fn subtract_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> Type {
1501    if let Type::Refined {
1502        original,
1503        ty: shape,
1504    } = ty.without_aliases()
1505    {
1506        return preserve_refinement(original, subtract_literals(shape, covered));
1507    }
1508    match ty.peel() {
1509        Type::Union(members) => {
1510            let kept: Vec<Type> = members
1511                .iter()
1512                .map(|m| subtract_literals(m, covered))
1513                .collect();
1514            Type::union(kept)
1515        }
1516        // `boolean` is `true | false`, so `case true:` leaves `false`.
1517        Type::Boolean => {
1518            let remaining: Vec<Type> = [false, true]
1519                .into_iter()
1520                .filter(|b| !covered.contains(&LiteralValue::Boolean(*b)))
1521                .map(Type::BooleanLiteral)
1522                .collect();
1523            Type::union(remaining)
1524        }
1525        single @ (Type::StringLiteral(_)
1526        | Type::TemplateLiteral { .. }
1527        | Type::NumberLiteral(_)
1528        | Type::BooleanLiteral(_)
1529        | Type::BigIntLiteral(_)
1530        | Type::Null
1531        | Type::Undefined) => match unit_value(single) {
1532            Some(lit) if covered.contains(&lit) => Type::Never,
1533            _ => ty.clone(),
1534        },
1535        _ => ty.clone(),
1536    }
1537}
1538
1539/// Whether `facts` test only whether a value is present: truthiness, `null`
1540/// and `undefined`. Against those, `unknown` narrows as `{} | null |
1541/// undefined`, which is what TypeScript takes it to mean.
1542fn tests_only_presence(facts: TypeFacts) -> bool {
1543    let presence = TypeFacts::TRUTHY
1544        | TypeFacts::FALSY
1545        | TypeFacts::EQ_NULL
1546        | TypeFacts::NE_NULL
1547        | TypeFacts::EQ_UNDEFINED
1548        | TypeFacts::NE_UNDEFINED
1549        | TypeFacts::IS_UNDEFINED;
1550    facts != TypeFacts::EMPTY && presence.contains(facts)
1551}
1552
1553/// Whether `facts` test only for `null` or `undefined`, not truthiness.
1554fn tests_only_nullishness(facts: TypeFacts) -> bool {
1555    tests_only_presence(facts)
1556        && !facts.contains(TypeFacts::TRUTHY)
1557        && !facts.contains(TypeFacts::FALSY)
1558}
1559
1560/// `union` with each refined type parameter's own `null` or `undefined`
1561/// dropped where the union holds that value already, as TypeScript reduces
1562/// `(T & ({} | null)) | null` to `(T & {}) | null`.
1563fn without_nullish_refinements(union: Type) -> Type {
1564    let Type::Union(members) = &union else {
1565        return union;
1566    };
1567    let holds = |value: &Type| members.iter().any(|member| member.peel() == value);
1568    let (holds_null, holds_undefined) = (holds(&Type::Null), holds(&Type::Undefined));
1569    if !holds_null && !holds_undefined {
1570        return union;
1571    }
1572    Type::union(
1573        members
1574            .iter()
1575            .filter_map(|member| match member {
1576                Type::Refined { original, ty } => {
1577                    let mut shape = (**ty).clone();
1578                    if holds_null {
1579                        shape = strip_null(&shape);
1580                    }
1581                    if holds_undefined {
1582                        shape = strip_undefined(&shape);
1583                    }
1584                    // `T & null` beside `null` adds nothing.
1585                    (!is_ruled_out(&shape)).then(|| preserve_refinement(original, shape))
1586                }
1587                other => Some(other.clone()),
1588            })
1589            .collect(),
1590    )
1591}
1592
1593/// `unknown` as the values it holds: `{} | null | undefined`.
1594fn unknown_as_union() -> Type {
1595    Type::Union(vec![Type::empty_object(), Type::Null, Type::Undefined])
1596}
1597
1598/// What is left of `unknown` after a narrowing: `unknown` again when the
1599/// narrowing ruled nothing out, as TypeScript's `recombineUnknownType` does.
1600fn recombine_unknown(ty: Type) -> Type {
1601    if ty.holds_every_value() {
1602        return Type::Unknown;
1603    }
1604    ty
1605}
1606
1607/// Map a [`TypeFacts`] mask to the concrete `Type` it asserts when narrowing
1608/// `unknown`. Returns `None` when facts don't pin a single type (TRUTHY,
1609/// IS_OBJECT span multiple types).
1610fn asserted_type_for_facts(facts: TypeFacts) -> Option<Type> {
1611    if facts.contains(TypeFacts::IS_BIGINT) {
1612        return Some(Type::BigInt);
1613    }
1614    if facts.contains(TypeFacts::IS_NUMBER) {
1615        return Some(Type::Number);
1616    }
1617    if facts.contains(TypeFacts::IS_STRING) {
1618        return Some(Type::String);
1619    }
1620    if facts.contains(TypeFacts::IS_BOOLEAN) {
1621        return Some(Type::Boolean);
1622    }
1623    if facts.contains(TypeFacts::IS_ARRAY) {
1624        return Some(Type::Array(Box::new(Type::Unknown)));
1625    }
1626    if facts.contains(TypeFacts::EQ_NULL) {
1627        return Some(Type::Null);
1628    }
1629    if facts.contains(TypeFacts::EQ_UNDEFINED) || facts.contains(TypeFacts::IS_UNDEFINED) {
1630        return Some(Type::Undefined);
1631    }
1632    // IS_OBJECT admits arrays, objects, and null (the JS typeof-null quirk);
1633    // no single concrete type, so caller keeps `Unknown`.
1634    None
1635}
1636
1637/// Join two branch post-environments at a control-flow merge point:
1638///
1639/// - Paths narrowed in **both** branches: joined type is `Type::union` of the two.
1640/// - Paths narrowed in only one branch: dropped.
1641/// - Paths a branch *extended* by assignment (`o` assigned, `o.f` narrowed):
1642///   dropped — the write can invalidate everything below it.
1643///
1644/// An assignment to the narrowed path *itself* does not drop it: each branch's
1645/// env already carries what that branch's assignment left behind
1646/// (`install_assignment_narrowing` records the assigned type), so the union of
1647/// the two is exactly the post-join type. That is what makes
1648/// `if (s === null) { s = "x"; }` leave `s: string` behind.
1649///
1650/// Assumes both branches contributed; caller handles the unreachable-branch case.
1651pub fn union_envs(
1652    a_narrowings: NarrowEnv,
1653    a_assigned: BTreeSet<ReferencePath>,
1654    b_narrowings: NarrowEnv,
1655    b_assigned: BTreeSet<ReferencePath>,
1656) -> (NarrowEnv, BTreeSet<ReferencePath>) {
1657    let mut joined_narrowings = NarrowEnv::new();
1658    joined_narrowings.definitely_assigned = a_narrowings
1659        .definitely_assigned
1660        .intersection(&b_narrowings.definitely_assigned)
1661        .cloned()
1662        .collect();
1663    for (path, a_view) in &a_narrowings {
1664        if let Some(b_view) = b_narrowings.get(path) {
1665            let Some(joined_ty) = join_views(a_view, b_view) else {
1666                continue;
1667            };
1668            // A ruled-out view has no shadow, so the join reads through the other.
1669            let a_view = if is_ruled_out(&a_view.narrowed_ty) {
1670                b_view
1671            } else {
1672                a_view
1673            };
1674            joined_narrowings.insert(
1675                path.clone(),
1676                NarrowedView {
1677                    evolving: join_view_inference(a_view, b_view),
1678                    narrowed_ty: joined_ty,
1679                    // Facts are per-branch; the join can't preserve
1680                    // them precisely. The downstream slim-slice
1681                    // sites only consume `narrowed_ty`, so EMPTY is
1682                    // sound.
1683                    facts: TypeFacts::EMPTY,
1684                    excluded_literals: BTreeSet::new(),
1685                    // Reuse `a`'s binding + source. The join
1686                    // typically gets dropped by the assigned-paths
1687                    // filter below; if it survives, the binding /
1688                    // source remain valid since both branches saw
1689                    // the same path.
1690                    binding: a_view.binding.clone(),
1691                    source: a_view.source,
1692                },
1693            );
1694        }
1695    }
1696
1697    for (path, ty) in a_narrowings.dropped.iter().chain(&b_narrowings.dropped) {
1698        let left_ty = a_narrowings
1699            .dropped
1700            .get(path)
1701            .or_else(|| a_narrowings.get(path).map(|view| &view.narrowed_ty));
1702        let right_ty = b_narrowings
1703            .dropped
1704            .get(path)
1705            .or_else(|| b_narrowings.get(path).map(|view| &view.narrowed_ty));
1706        if left_ty == Some(ty) && right_ty == Some(ty) {
1707            joined_narrowings.dropped.insert(path.clone(), ty.clone());
1708        }
1709    }
1710
1711    let mut joined_assigned = a_assigned;
1712    joined_assigned.extend(b_assigned);
1713
1714    joined_narrowings.dropped.retain(|key, _| {
1715        !joined_assigned
1716            .iter()
1717            .any(|assigned| assigned.is_prefix_of(key))
1718    });
1719    joined_narrowings.retain(|key, _| {
1720        !joined_assigned
1721            .iter()
1722            .any(|assigned| assigned != key && assigned.is_prefix_of(key))
1723    });
1724
1725    (joined_narrowings, joined_assigned)
1726}
1727
1728/// The tuple position a number names, if it names one.
1729pub(super) fn tuple_position(index: f64) -> Option<usize> {
1730    (index >= 0.0 && index.fract() == 0.0 && index <= u32::MAX as f64).then_some(index as usize)
1731}
1732
1733/// The type of a view whose guard ruled out every value. Narrowing yields
1734/// `Error` when nothing is left; a view keeps it, rather than `never`, so
1735/// codegen gives the path no shadow local, and a read of the path turns it
1736/// into `never` where `Inferer::rules_out_to_never` allows.
1737pub const RULED_OUT: Type = Type::Error;
1738
1739/// Whether a narrowed type is [`RULED_OUT`].
1740pub fn is_ruled_out(ty: &Type) -> bool {
1741    matches!(ty, Type::Error)
1742}
1743
1744/// Evolving arrays join their elements. An inferred unknown value remains a
1745/// view: it records that a write determined the type, even though storage is unknown.
1746pub(super) fn join_views(left: &NarrowedView, right: &NarrowedView) -> Option<Type> {
1747    if is_ruled_out(&left.narrowed_ty) {
1748        return Some(right.narrowed_ty.clone());
1749    }
1750    if is_ruled_out(&right.narrowed_ty) {
1751        return Some(left.narrowed_ty.clone());
1752    }
1753    if join_view_inference(left, right) == Some(EvolvingType::Array) {
1754        let element = Type::union(vec![
1755            super::evolving::array_element(&left.narrowed_ty)?,
1756            super::evolving::array_element(&right.narrowed_ty)?,
1757        ]);
1758        return Some(Type::Array(Box::new(element)));
1759    }
1760    if left.evolving.is_some() && right.evolving.is_some() {
1761        return Some(join_flow_types(&left.narrowed_ty, &right.narrowed_ty));
1762    }
1763    join_narrowed_types(&left.narrowed_ty, &right.narrowed_ty)
1764}
1765
1766pub(super) fn join_view_inference(
1767    left: &NarrowedView,
1768    right: &NarrowedView,
1769) -> Option<EvolvingType> {
1770    if is_ruled_out(&left.narrowed_ty) {
1771        return right.evolving;
1772    }
1773    if is_ruled_out(&right.narrowed_ty) {
1774        return left.evolving;
1775    }
1776    match (left.evolving?, right.evolving?) {
1777        (EvolvingType::Array, EvolvingType::Array) => Some(EvolvingType::Array),
1778        (EvolvingType::UnresolvedArray, _) | (_, EvolvingType::UnresolvedArray) => {
1779            Some(EvolvingType::UnresolvedArray)
1780        }
1781        (EvolvingType::Array, _)
1782            if super::evolving::array_element(&left.narrowed_ty) == Some(Type::Never) =>
1783        {
1784            Some(EvolvingType::UnresolvedArray)
1785        }
1786        (_, EvolvingType::Array)
1787            if super::evolving::array_element(&right.narrowed_ty) == Some(Type::Never) =>
1788        {
1789            Some(EvolvingType::UnresolvedArray)
1790        }
1791        _ => Some(EvolvingType::Value),
1792    }
1793}
1794
1795/// The join of two narrowings of one path, or `None` when it holds every
1796/// value: then it narrows nothing, and reads see the declared type again,
1797/// `unknown` or a written `{} | null | undefined`.
1798pub(super) fn join_narrowed_types(left: &Type, right: &Type) -> Option<Type> {
1799    let joined = join_flow_types(left, right);
1800    (!joined.holds_every_value()).then_some(joined)
1801}
1802
1803/// Flow joins collapse a literal already covered by a broad primitive. Keep
1804/// authored unions unchanged: their overlap is meaningful to JSON diagnostics.
1805fn join_flow_types(left: &Type, right: &Type) -> Type {
1806    // A ruled-out side holds no value, so it adds nothing to the other.
1807    if is_ruled_out(left) {
1808        return right.clone();
1809    }
1810    if is_ruled_out(right) {
1811        return left.clone();
1812    }
1813    let joined = Type::union(vec![left.clone(), right.clone()]);
1814    let Type::Union(members) = joined else {
1815        return joined;
1816    };
1817    let mut members = crate::types::without_members_of_empty_object(members);
1818    let has_number = members
1819        .iter()
1820        .any(|ty| matches!(ty.without_aliases(), Type::Number));
1821    let has_string = members
1822        .iter()
1823        .any(|ty| matches!(ty.without_aliases(), Type::String));
1824    members.retain(|ty| match ty.without_aliases() {
1825        Type::NumberLiteral(_) => !has_number,
1826        Type::StringLiteral(_) | Type::TemplateLiteral { .. } => !has_string,
1827        _ => true,
1828    });
1829    Type::union(members)
1830}
1831
1832/// Recurses into `Type::Union` members only — composite slots have fixed runtime
1833/// classification regardless of generic contents, so their type arguments are
1834/// not walked either (`Box<T> = number` classifies as a number whatever `T` is).
1835///
1836/// Peeled: an alias of a type parameter is erased just as the parameter is, and
1837/// the caller uses this as the escape hatch that keeps a `typeof` guard on an
1838/// erased operand from folding to a constant.
1839pub fn has_erased_member(ty: &Type) -> bool {
1840    match ty.peel() {
1841        Type::Union(members) => members.iter().any(has_erased_member),
1842        // A type parameter, or a form over one such as `Readonly<T>`.
1843        member => member.lowers_to_erased_slot(),
1844    }
1845}
1846
1847/// Every type is condition-compatible under JS truthiness except `unknown`
1848/// (requires explicit narrowing first, spec §2.11) and the value-less bare
1849/// `void`. A union with another value-bearing alternative is legal; its void
1850/// member remains dynamic. `never` counts, as in TypeScript: either no value
1851/// reaches the test (a call that throws, a local after an exhausted `else if`
1852/// chain), or the value is one a `never[]` an alias filled holds, which tests
1853/// as it would anywhere. `Error` is accepted to silence cascades.
1854pub fn condition_compatible(ty: &Type) -> bool {
1855    !matches!(ty.peel(), Type::Unknown | Type::Void)
1856}
1857
1858pub fn facts_for_target_type(target: &Type) -> TypeFacts {
1859    match target.peel() {
1860        Type::Number | Type::NumberLiteral(_) => TypeFacts::IS_NUMBER,
1861        Type::String | Type::StringLiteral(_) | Type::TemplateLiteral { .. } => {
1862            TypeFacts::IS_STRING
1863        }
1864        Type::Boolean | Type::BooleanLiteral(_) => TypeFacts::IS_BOOLEAN,
1865        Type::Null => TypeFacts::EQ_NULL,
1866        Type::Undefined => TypeFacts::EQ_UNDEFINED,
1867        Type::BigInt => TypeFacts::IS_BIGINT,
1868        Type::Array(_) => TypeFacts::IS_ARRAY,
1869        _ => TypeFacts::EMPTY,
1870    }
1871}
1872
1873#[cfg(test)]
1874mod analyzer_tests {
1875    use super::*;
1876
1877    #[test]
1878    fn typeof_undefined_splits_void_without_assuming_its_truthiness() {
1879        let ty = Type::union(vec![Type::Boolean, Type::Void]);
1880        assert_eq!(
1881            intersect_with(&ty, TypeFacts::IS_UNDEFINED),
1882            Type::Undefined
1883        );
1884        assert_eq!(subtract(&ty, TypeFacts::IS_UNDEFINED), ty);
1885        assert_eq!(truthiness_class(&Type::Void), TruthinessClass::Dynamic);
1886        assert!(!type_matches_facts(&Type::Void, TypeFacts::EQ_UNDEFINED));
1887    }
1888
1889    #[test]
1890    fn undefined_facts_are_distinct_from_null() {
1891        let ty = Type::union(vec![Type::String, Type::Null, Type::Undefined]);
1892        assert_eq!(
1893            strip_null(&ty),
1894            Type::union(vec![Type::String, Type::Undefined])
1895        );
1896        assert_eq!(
1897            strip_undefined(&ty),
1898            Type::union(vec![Type::String, Type::Null])
1899        );
1900        assert_eq!(strip_nullish(&ty), Type::String);
1901        assert_eq!(
1902            intersect_with(&ty, TypeFacts::EQ_UNDEFINED),
1903            Type::Undefined
1904        );
1905        assert_eq!(
1906            intersect_with(&Type::Unknown, TypeFacts::EQ_UNDEFINED),
1907            Type::Undefined
1908        );
1909        assert_eq!(
1910            intersect_with(&ty, TypeFacts::NE_NULL | TypeFacts::NE_UNDEFINED),
1911            Type::String
1912        );
1913        assert_eq!(truthy_part(&ty), Type::String);
1914        assert_eq!(
1915            truthiness_class(&Type::Undefined),
1916            TruthinessClass::AlwaysFalsy
1917        );
1918        assert!(!type_matches_facts(&Type::Undefined, TypeFacts::IS_OBJECT));
1919    }
1920
1921    #[test]
1922    fn presence_keeps_optional_read_undefined() {
1923        let ty = Type::Object {
1924            fields: BTreeMap::from([("value".into(), crate::ObjectField::optional(Type::Number))]),
1925            index: None,
1926        };
1927        let (present, absent) = narrow_field_presence(&ty, "value", &|_, _| None);
1928        assert_eq!(present, ty);
1929        assert_eq!(absent, ty);
1930    }
1931
1932    #[test]
1933    fn refined_literal_residual_retains_generic_identity() {
1934        let original = Type::GenericParam {
1935            id: 1,
1936            name: "T".into(),
1937        };
1938        let shape = Type::union(vec![
1939            Type::StringLiteral("a".into()),
1940            Type::StringLiteral("b".into()),
1941        ]);
1942        let refined = preserve_refinement(&original, shape);
1943        let covered = BTreeSet::from([LiteralValue::String("a".into())]);
1944        assert_eq!(
1945            subtract_literals(&refined, &covered),
1946            preserve_refinement(&original, Type::StringLiteral("b".into()))
1947        );
1948    }
1949
1950    #[test]
1951    fn strip_null_from_nullable_string() {
1952        let ty = Type::union(vec![Type::String, Type::Null]);
1953        assert_eq!(strip_null(&ty), Type::String);
1954    }
1955
1956    #[test]
1957    fn strip_null_from_non_nullable_is_identity() {
1958        assert_eq!(strip_null(&Type::String), Type::String);
1959        assert_eq!(strip_null(&Type::Number), Type::Number);
1960    }
1961
1962    #[test]
1963    fn strip_null_from_null_is_error() {
1964        assert_eq!(strip_null(&Type::Null), Type::Error);
1965    }
1966
1967    #[test]
1968    fn strip_null_from_multi_member_union() {
1969        let ty = Type::union(vec![Type::String, Type::Number, Type::Null]);
1970        let stripped = strip_null(&ty);
1971        // Type::union canonicalizes; should be String | Number.
1972        match stripped {
1973            Type::Union(members) => assert_eq!(members.len(), 2),
1974            _ => panic!("expected Union, got {stripped:?}"),
1975        }
1976    }
1977
1978    #[test]
1979    fn intersect_with_is_number_filters_union() {
1980        let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1981        let kept = intersect_with(&ty, TypeFacts::IS_NUMBER);
1982        assert_eq!(kept, Type::Number);
1983    }
1984
1985    #[test]
1986    fn subtract_is_number_keeps_others() {
1987        let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1988        let kept = subtract(&ty, TypeFacts::IS_NUMBER);
1989        match kept {
1990            Type::Union(members) => {
1991                assert_eq!(members.len(), 2);
1992                assert!(members.contains(&Type::String));
1993                assert!(members.contains(&Type::Boolean));
1994            }
1995            _ => panic!("expected Union, got {kept:?}"),
1996        }
1997    }
1998
1999    #[test]
2000    fn intersect_with_truthy_strips_null() {
2001        let ty = Type::union(vec![Type::String, Type::Null]);
2002        let kept = intersect_with(&ty, TypeFacts::TRUTHY);
2003        assert_eq!(kept, Type::String);
2004    }
2005
2006    #[test]
2007    fn subtract_truthy_keeps_only_null() {
2008        let ty = Type::union(vec![Type::String, Type::Null]);
2009        let kept = subtract(&ty, TypeFacts::TRUTHY);
2010        assert_eq!(kept, Type::Null);
2011    }
2012
2013    #[test]
2014    fn intersect_with_no_match_yields_error() {
2015        let kept = intersect_with(&Type::String, TypeFacts::IS_NUMBER);
2016        assert_eq!(kept, Type::Error);
2017    }
2018
2019    #[test]
2020    fn facts_for_target_type_covers_primitives() {
2021        assert_eq!(facts_for_target_type(&Type::Number), TypeFacts::IS_NUMBER);
2022        assert_eq!(facts_for_target_type(&Type::String), TypeFacts::IS_STRING);
2023        assert_eq!(facts_for_target_type(&Type::Boolean), TypeFacts::IS_BOOLEAN);
2024        assert_eq!(facts_for_target_type(&Type::Null), TypeFacts::EQ_NULL);
2025    }
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030    use super::*;
2031    use crate::Span;
2032
2033    #[test]
2034    fn reference_path_equality_is_structural() {
2035        let a = ReferencePath::root(BindingId::Local {
2036            name: "x".to_string(),
2037            decl_scope: ScopeId(1),
2038        });
2039        let b = ReferencePath::root(BindingId::Local {
2040            name: "x".to_string(),
2041            decl_scope: ScopeId(1),
2042        });
2043        assert_eq!(a, b);
2044    }
2045
2046    #[test]
2047    fn reference_path_distinguishes_scopes() {
2048        let a = ReferencePath::root(BindingId::Local {
2049            name: "x".to_string(),
2050            decl_scope: ScopeId(1),
2051        });
2052        let b = ReferencePath::root(BindingId::Local {
2053            name: "x".to_string(),
2054            decl_scope: ScopeId(2),
2055        });
2056        assert_ne!(a, b);
2057    }
2058
2059    #[test]
2060    fn reference_path_distinguishes_chain() {
2061        let a = ReferencePath {
2062            root: BindingId::Local {
2063                name: "x".to_string(),
2064                decl_scope: ScopeId(1),
2065            },
2066            chain: vec![PathElem::Field("foo".to_string())],
2067        };
2068        let b = ReferencePath::root(BindingId::Local {
2069            name: "x".to_string(),
2070            decl_scope: ScopeId(1),
2071        });
2072        assert_ne!(a, b);
2073    }
2074
2075    #[test]
2076    fn type_facts_or_combines() {
2077        let f = TypeFacts::TRUTHY | TypeFacts::NE_NULL;
2078        assert!(f.contains(TypeFacts::TRUTHY));
2079        assert!(f.contains(TypeFacts::NE_NULL));
2080        assert!(!f.contains(TypeFacts::EQ_NULL));
2081    }
2082
2083    #[test]
2084    fn type_facts_not_complements() {
2085        let f = TypeFacts::EQ_NULL;
2086        let flipped = !f;
2087        assert!(flipped.contains(TypeFacts::NE_NULL));
2088        assert!(!flipped.contains(TypeFacts::EQ_NULL));
2089    }
2090
2091    #[test]
2092    fn binding_id_global_uses_mangled_name() {
2093        let g = BindingId::Global(crate::mangle::package_symbol("main", "x"));
2094        if let BindingId::Global(name) = &g {
2095            assert_eq!(name.as_str(), "main#x");
2096        } else {
2097            panic!("expected Global");
2098        }
2099    }
2100
2101    #[test]
2102    fn narrowed_view_construction() {
2103        let view = NarrowedView {
2104            evolving: None,
2105            narrowed_ty: Type::String,
2106            facts: TypeFacts::NE_NULL,
2107            excluded_literals: BTreeSet::new(),
2108            binding: Ident {
2109                name: "#narrow_0".to_string(),
2110                span: Span::new(crate::FileId(0), 0, 1).unwrap(),
2111            },
2112            source: crate::ExprId(0),
2113        };
2114        assert_eq!(view.narrowed_ty, Type::String);
2115        assert!(view.facts.contains(TypeFacts::NE_NULL));
2116    }
2117
2118    #[test]
2119    fn cast_info_holds_kind_and_types() {
2120        let info = CastInfo {
2121            from_ty: Type::Union(vec![Type::String, Type::Null]),
2122            to_ty: Type::String,
2123            cast_kind: CastKind::NonNull,
2124        };
2125        assert!(matches!(info.cast_kind, CastKind::NonNull));
2126    }
2127
2128    #[test]
2129    fn union_discriminant_stub_returns_none() {
2130        assert!(union_discriminant(&[Type::String, Type::Null]).is_none());
2131    }
2132
2133    #[test]
2134    fn intersect_unknown_with_is_string() {
2135        assert_eq!(
2136            intersect_with(&Type::Unknown, TypeFacts::IS_STRING),
2137            Type::String
2138        );
2139    }
2140
2141    #[test]
2142    fn intersect_unknown_with_is_number() {
2143        assert_eq!(
2144            intersect_with(&Type::Unknown, TypeFacts::IS_NUMBER),
2145            Type::Number
2146        );
2147    }
2148
2149    #[test]
2150    fn intersect_unknown_with_is_boolean() {
2151        assert_eq!(
2152            intersect_with(&Type::Unknown, TypeFacts::IS_BOOLEAN),
2153            Type::Boolean
2154        );
2155    }
2156
2157    #[test]
2158    fn intersect_unknown_with_is_array_carries_unknown_element() {
2159        // `Array.isArray<T>(unknown)` narrows to `Array<unknown>`
2160        // because the element type isn't statically determined.
2161        assert_eq!(
2162            intersect_with(&Type::Unknown, TypeFacts::IS_ARRAY),
2163            Type::Array(Box::new(Type::Unknown)),
2164        );
2165    }
2166
2167    #[test]
2168    fn intersect_unknown_with_eq_null() {
2169        assert_eq!(
2170            intersect_with(&Type::Unknown, TypeFacts::EQ_NULL),
2171            Type::Null
2172        );
2173    }
2174
2175    #[test]
2176    fn intersect_unknown_with_truthy_is_empty_object() {
2177        // A truthy `unknown` is neither `null` nor `undefined`: `{}`.
2178        assert_eq!(
2179            intersect_with(&Type::Unknown, TypeFacts::TRUTHY),
2180            Type::empty_object()
2181        );
2182    }
2183
2184    #[test]
2185    fn subtract_unknown_removes_only_nullish_values() {
2186        // `unknown` minus a type other than `null` or `undefined` stays
2187        // `unknown`; minus those, it is the rest of `{} | null | undefined`.
2188        assert_eq!(
2189            subtract(&Type::Unknown, TypeFacts::IS_STRING),
2190            Type::Unknown
2191        );
2192        assert_eq!(
2193            subtract(&Type::Unknown, TypeFacts::EQ_NULL),
2194            Type::union(vec![Type::empty_object(), Type::Undefined])
2195        );
2196        assert_eq!(subtract(&Type::Unknown, TypeFacts::TRUTHY), Type::Unknown);
2197    }
2198
2199    #[test]
2200    fn strip_null_from_unknown_leaves_empty_object_or_undefined() {
2201        assert_eq!(
2202            strip_null(&Type::Unknown),
2203            Type::union(vec![Type::empty_object(), Type::Undefined])
2204        );
2205        assert_eq!(strip_nullish(&Type::Unknown), Type::empty_object());
2206    }
2207
2208    #[test]
2209    fn condition_compatible_rejects_unknown() {
2210        assert!(!condition_compatible(&Type::Unknown));
2211    }
2212
2213    #[test]
2214    fn condition_compatible_accepts_js_truthiness_types() {
2215        for ty in [
2216            Type::Boolean,
2217            Type::String,
2218            Type::Number,
2219            Type::BigInt,
2220            Type::Null,
2221            Type::Array(Box::new(Type::Number)),
2222            Type::union(vec![Type::String, Type::Null]),
2223            Type::union(vec![Type::Number, Type::Void]),
2224        ] {
2225            assert!(condition_compatible(&ty), "{ty:?}");
2226        }
2227        assert!(condition_compatible(&Type::Never));
2228        assert!(!condition_compatible(&Type::Void));
2229    }
2230
2231    #[test]
2232    fn truthiness_class_table() {
2233        use TruthinessClass::*;
2234        let cases = [
2235            (Type::Null, AlwaysFalsy),
2236            (Type::Boolean, BooleanLike),
2237            (Type::Number, NumberLike),
2238            (
2239                Type::NumberLiteral(crate::types::LiteralF64(0.0)),
2240                AlwaysFalsy,
2241            ),
2242            (
2243                Type::NumberLiteral(crate::types::LiteralF64(-0.0)),
2244                AlwaysFalsy,
2245            ),
2246            (
2247                Type::NumberLiteral(crate::types::LiteralF64(3.0)),
2248                AlwaysTruthy,
2249            ),
2250            (Type::String, StringLike),
2251            (Type::StringLiteral(String::new()), AlwaysFalsy),
2252            (Type::StringLiteral("a".to_string()), AlwaysTruthy),
2253            (Type::BigInt, BigIntLike),
2254            (Type::Uint8Array, AlwaysTruthy),
2255            (Type::Array(Box::new(Type::Number)), AlwaysTruthy),
2256            (Type::Unknown, Dynamic),
2257        ];
2258        for (ty, expected) in cases {
2259            assert_eq!(truthiness_class(&ty), expected, "{ty:?}");
2260        }
2261    }
2262
2263    #[test]
2264    fn truthy_part_strips_null_and_falsy_literals() {
2265        assert_eq!(
2266            truthy_part(&Type::union(vec![Type::String, Type::Null])),
2267            Type::String
2268        );
2269        assert_eq!(
2270            truthy_part(&Type::union(vec![
2271                Type::StringLiteral(String::new()),
2272                Type::StringLiteral("a".to_string()),
2273                Type::Null,
2274            ])),
2275            Type::StringLiteral("a".to_string())
2276        );
2277        assert_eq!(truthy_part(&Type::Null), Type::Never);
2278        assert_eq!(truthy_part(&Type::Boolean), Type::BooleanLiteral(true));
2279        assert_eq!(truthy_part(&Type::Number), Type::Number);
2280    }
2281
2282    #[test]
2283    fn falsy_part_keeps_falsy_capable_members() {
2284        assert_eq!(
2285            falsy_part(&Type::union(vec![Type::String, Type::Null])),
2286            Type::union(vec![Type::StringLiteral(String::new()), Type::Null])
2287        );
2288        assert_eq!(falsy_part(&Type::Number), Type::Number);
2289        assert_eq!(falsy_part(&Type::Boolean), Type::BooleanLiteral(false));
2290        // Never-falsy references drop entirely.
2291        assert_eq!(
2292            falsy_part(&Type::union(vec![
2293                Type::Array(Box::new(Type::Number)),
2294                Type::Null
2295            ])),
2296            Type::Null
2297        );
2298        assert_eq!(
2299            falsy_part(&Type::Array(Box::new(Type::Number))),
2300            Type::Never
2301        );
2302        assert_eq!(falsy_part(&Type::empty_object()), Type::empty_object());
2303    }
2304
2305    #[test]
2306    fn falsy_only_null_distinguishes_reference_unions() {
2307        assert!(falsy_values_are_only_null(&Type::union(vec![
2308            Type::Array(Box::new(Type::Number)),
2309            Type::Null
2310        ])));
2311        assert!(!falsy_values_are_only_null(&Type::union(vec![
2312            Type::String,
2313            Type::Null
2314        ])));
2315        assert!(falsy_values_are_only_null(&Type::Uint8Array));
2316        assert!(!falsy_values_are_only_null(&Type::Number));
2317    }
2318
2319    #[test]
2320    fn intersect_truthy_and_falsy_respect_js_truthiness() {
2321        let string_or_null = Type::union(vec![Type::String, Type::Null]);
2322        assert_eq!(
2323            intersect_with(&string_or_null, TypeFacts::TRUTHY | TypeFacts::NE_NULL),
2324            Type::String
2325        );
2326        // `""` is falsy but not null — the falsy side keeps `string`.
2327        assert_eq!(
2328            intersect_with(&string_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2329            string_or_null
2330        );
2331        let arr_or_null = Type::union(vec![Type::Array(Box::new(Type::Number)), Type::Null]);
2332        assert_eq!(
2333            intersect_with(&arr_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2334            Type::Null
2335        );
2336        // Bare NE_NULL stays a pure null test: `x !== null` keeps `""`.
2337        assert_eq!(
2338            intersect_with(&string_or_null, TypeFacts::NE_NULL),
2339            Type::String
2340        );
2341    }
2342}