1use 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 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 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#[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 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#[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#[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 BigInt(String),
145 Null,
148 Undefined,
149}
150
151impl LiteralValue {
152 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#[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#[derive(Debug)]
221pub struct PendingJoinFrame {
222 pub kind: PendingJoinKind,
223 pub narrow_depth: usize,
230 pub breaks: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
231 pub continues: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
232}
233
234#[derive(Copy, Clone, Debug, PartialEq, Eq)]
235pub enum PendingJoinKind {
236 Loop,
238 Switch,
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
243pub struct NarrowedView {
244 pub narrowed_ty: Type,
245 pub facts: TypeFacts,
246 pub excluded_literals: BTreeSet<LiteralValue>,
249 pub binding: Ident,
252 pub source: ExprId,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct CastInfo {
262 pub from_ty: Type,
263 pub to_ty: Type,
264 pub cast_kind: CastKind,
265}
266
267#[derive(Copy, Clone, Debug, PartialEq, Eq)]
270pub enum CastKind {
271 NonNull,
273 Unbox,
278 RefSubtype,
282}
283
284pub fn cast_info_for(from_ty: Type, narrowed_ty: Type) -> CastInfo {
286 let cast_kind = match &narrowed_ty {
287 Type::Number | Type::NumberLiteral(_) | Type::Boolean | Type::BooleanLiteral(_) => {
288 CastKind::Unbox
289 }
290 Type::Object { .. } | Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
291 CastKind::RefSubtype
292 }
293 Type::String
295 | Type::StringLiteral(_)
296 | Type::Array(_)
297 | Type::Tuple(_)
298 | Type::Uint8Array
299 if matches!(from_ty.peel(), Type::Unknown) =>
300 {
301 CastKind::RefSubtype
302 }
303 _ => CastKind::NonNull,
304 };
305 CastInfo {
306 from_ty,
307 to_ty: narrowed_ty,
308 cast_kind,
309 }
310}
311
312#[derive(Clone, Debug)]
315pub struct PendingPostIfMaterialization {
316 pub path: ReferencePath,
317 pub source: ExprId,
318 pub binding: Ident,
319 pub cast_info: CastInfo,
320 pub span: crate::Span,
321}
322
323#[derive(Clone, Debug)]
326pub enum InvalidationReason {
327 ShapeUnrebuildable { narrowed_ty: Type },
329 Write { span: crate::Span },
333 Reassignment { span: crate::Span },
336 CapturedMutator { closure_span: Option<crate::Span> },
339}
340
341impl InvalidationReason {
342 pub fn invalidates(&self) -> bool {
343 !matches!(self, Self::ShapeUnrebuildable { .. })
344 }
345
346 pub fn span(&self) -> Option<crate::Span> {
347 match self {
348 Self::ShapeUnrebuildable { .. } => None,
349 Self::Write { span } => Some(*span),
350 Self::Reassignment { span } => Some(*span),
351 Self::CapturedMutator { closure_span } => *closure_span,
352 }
353 }
354}
355
356pub type Discriminant = (String, DiscriminantTable);
359
360#[derive(Clone, Debug, Default, PartialEq)]
364pub struct DiscriminantTable {
365 entries: Vec<(LiteralValue, VariantIdx)>,
366}
367
368#[derive(Clone, Copy, PartialEq, Eq)]
370enum UndefinedNames {
371 Several,
373 One,
375}
376
377impl DiscriminantTable {
378 pub fn iter(&self) -> impl Iterator<Item = (&LiteralValue, &VariantIdx)> {
379 self.entries.iter().map(|(value, variant)| (value, variant))
380 }
381
382 pub fn get(&self, value: &LiteralValue) -> Option<VariantIdx> {
384 let mut variants = self.variants_for(value);
385 let variant = variants.next()?;
386 variants.next().is_none().then_some(variant)
387 }
388
389 pub fn variants_for<'a>(
391 &'a self,
392 value: &'a LiteralValue,
393 ) -> impl Iterator<Item = VariantIdx> + 'a {
394 self.entries
395 .iter()
396 .filter(move |(entry, _)| entry == value)
397 .map(|(_, variant)| *variant)
398 }
399
400 pub fn has_other_values(&self, variant: VariantIdx, value: &LiteralValue) -> bool {
403 self.entries
404 .iter()
405 .any(|(entry, named)| *named == variant && entry != value)
406 }
407
408 pub fn is_covered(&self, variant: VariantIdx, covered: &BTreeSet<LiteralValue>) -> bool {
411 self.entries
412 .iter()
413 .filter(|(_, named)| *named == variant)
414 .all(|(value, _)| covered.contains(value))
415 }
416
417 pub fn uncovered_members(
419 &self,
420 members: &[Type],
421 covered: &BTreeSet<LiteralValue>,
422 ) -> Vec<Type> {
423 members
424 .iter()
425 .enumerate()
426 .filter(|(index, _)| {
427 u32::try_from(*index)
428 .map_or(true, |index| !self.is_covered(VariantIdx(index), covered))
429 })
430 .map(|(_, member)| member.clone())
431 .collect()
432 }
433
434 pub fn uncovered(&self, covered: &BTreeSet<LiteralValue>) -> Vec<LiteralValue> {
436 let mut missing: Vec<LiteralValue> = Vec::new();
437 for (value, _) in &self.entries {
438 if !covered.contains(value) && !missing.contains(value) {
439 missing.push(value.clone());
440 }
441 }
442 missing
443 }
444
445 pub fn partition(
450 &self,
451 members: &[Type],
452 literal: &LiteralValue,
453 ) -> Option<(Vec<Type>, Vec<Type>)> {
454 let named: Vec<VariantIdx> = self.variants_for(literal).collect();
455 if named.is_empty() {
456 return None;
457 }
458 let mut matched = Vec::with_capacity(named.len());
459 let mut remaining = Vec::new();
460 for (index, member) in members.iter().enumerate() {
461 let variant = VariantIdx(u32::try_from(index).ok()?);
462 if !named.contains(&variant) {
463 remaining.push(member.clone());
464 continue;
465 }
466 matched.push(member.clone());
467 if self.has_other_values(variant, literal) {
468 remaining.push(member.clone());
469 }
470 }
471 Some((matched, remaining))
472 }
473
474 fn insert(
477 &mut self,
478 value: LiteralValue,
479 variant: VariantIdx,
480 undefined: UndefinedNames,
481 ) -> Option<()> {
482 let shareable = undefined == UndefinedNames::Several && value == LiteralValue::Undefined;
483 if !shareable && self.entries.iter().any(|(entry, _)| *entry == value) {
484 return None;
485 }
486 self.entries.push((value, variant));
487 Some(())
488 }
489}
490
491#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
493pub struct VariantIdx(pub u32);
494
495pub fn union_discriminant(members: &[Type]) -> Option<Discriminant> {
509 let shapes = inline_member_shapes(members)?;
515 discriminant_from_shapes(&shapes)
516}
517
518fn inline_member_shapes(
521 members: &[Type],
522) -> Option<Vec<BTreeMap<String, crate::types::ObjectField>>> {
523 members
524 .iter()
525 .map(|m| match m.peel() {
526 Type::Object { fields, .. } => Some(fields.clone()),
527 _ => None,
528 })
529 .collect()
530}
531
532pub fn inline_union_discriminant_for_key(members: &[Type], key: &str) -> Option<DiscriminantTable> {
536 let shapes = inline_member_shapes(members)?;
537 discriminant_table_for_key(&shapes, key)
538}
539
540pub fn discriminant_from_shapes(
545 shapes: &[BTreeMap<String, crate::types::ObjectField>],
546) -> Option<Discriminant> {
547 let first = shapes.first()?;
548 let may_be_omitted = |key: &String| {
549 shapes
550 .iter()
551 .any(|shape| shape.get(key).is_some_and(|f| f.optional))
552 };
553 let (required, omittable): (Vec<&String>, Vec<&String>) =
554 first.keys().partition(|key| !may_be_omitted(key));
555 required
556 .into_iter()
557 .chain(omittable)
558 .find_map(|key| Some((key.clone(), discriminant_table_for_key(shapes, key)?)))
559}
560
561pub fn discriminant_table_for_key(
564 shapes: &[BTreeMap<String, crate::types::ObjectField>],
565 key: &str,
566) -> Option<DiscriminantTable> {
567 if shapes.len() < 2 {
568 return None;
569 }
570 let mut table = DiscriminantTable::default();
571 for (idx, shape) in shapes.iter().enumerate() {
572 let field = shape.get(key)?;
573 let lit = unit_value(&field.ty)?;
574 let absent =
577 (field.optional && lit != LiteralValue::Undefined).then_some(LiteralValue::Undefined);
578 let variant = VariantIdx(u32::try_from(idx).ok()?);
579 for value in std::iter::once(lit).chain(absent) {
580 table.insert(value, variant, UndefinedNames::Several)?;
581 }
582 }
583 Some(table)
584}
585
586pub fn tuple_position_discriminant(members: &[Type], position: usize) -> Option<DiscriminantTable> {
592 if members.len() < 2 {
593 return None;
594 }
595 let mut table = DiscriminantTable::default();
596 for (idx, member) in members.iter().enumerate() {
597 let Type::Tuple(elements) = member.peel() else {
598 return None;
599 };
600 let lit = if position >= elements.len() {
601 LiteralValue::Undefined
602 } else if position < elements.required_len() {
603 elements.get(position).and_then(unit_value)?
604 } else {
605 return None;
606 };
607 table.insert(
610 lit,
611 VariantIdx(u32::try_from(idx).ok()?),
612 UndefinedNames::One,
613 )?;
614 }
615 Some(table)
616}
617
618pub fn switch_value_literal(value: &crate::TypedSwitchValue) -> Option<LiteralValue> {
621 Some(match value {
622 crate::TypedSwitchValue::String { value, .. } => LiteralValue::String(value.clone()),
623 crate::TypedSwitchValue::Number { value, .. } => {
624 LiteralValue::Number(crate::types::LiteralF64(*value))
625 }
626 crate::TypedSwitchValue::Boolean { value, .. } => LiteralValue::Boolean(*value),
627 crate::TypedSwitchValue::Null { .. } => LiteralValue::Null,
628 crate::TypedSwitchValue::Undefined { .. } => LiteralValue::Undefined,
629 crate::TypedSwitchValue::Expr { literal, .. } => return literal.clone(),
630 crate::TypedSwitchValue::Enum { value, .. } => match value {
631 crate::EnumVariantPayload::Number(n) => {
632 LiteralValue::Number(crate::types::LiteralF64(*n))
633 }
634 crate::EnumVariantPayload::String(s) => LiteralValue::String(s.clone()),
635 },
636 })
637}
638
639pub fn literal_type(literal: &LiteralValue) -> Type {
641 match literal {
642 LiteralValue::String(s) => Type::StringLiteral(s.clone()),
643 LiteralValue::Number(n) => Type::NumberLiteral(*n),
644 LiteralValue::Boolean(b) => Type::BooleanLiteral(*b),
645 LiteralValue::BigInt(digits) => Type::BigIntLiteral(digits.clone()),
646 LiteralValue::Null => Type::Null,
647 LiteralValue::Undefined => Type::Undefined,
648 }
649}
650
651pub fn has_unit_member(ty: &Type) -> bool {
655 match ty.peel() {
656 Type::Union(members) => members.iter().any(has_unit_member),
657 Type::Null | Type::Undefined | Type::NumberEnum { .. } | Type::StringEnum { .. } => true,
658 other => unit_literal_value(other).is_some(),
659 }
660}
661
662pub fn is_covered_by_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> bool {
665 match ty.peel() {
666 Type::Union(members) => members
667 .iter()
668 .all(|member| is_covered_by_literals(member, covered)),
669 Type::Boolean => [true, false]
671 .into_iter()
672 .all(|value| covered.contains(&LiteralValue::Boolean(value))),
673 other => unit_value(other).is_some_and(|value| covered.contains(&value)),
674 }
675}
676
677pub fn has_type_parameter_member(ty: &Type) -> bool {
679 match ty.peel() {
680 Type::Union(members) => members.iter().any(has_type_parameter_member),
681 other => matches!(other, Type::TypeVar(_) | Type::GenericParam { .. }),
682 }
683}
684
685pub(super) fn unit_literal_value(ty: &Type) -> Option<LiteralValue> {
686 match ty.peel() {
687 Type::StringLiteral(s) => Some(LiteralValue::String(s.clone())),
688 Type::NumberLiteral(n) => Some(LiteralValue::Number(*n)),
689 Type::BooleanLiteral(b) => Some(LiteralValue::Boolean(*b)),
690 Type::BigIntLiteral(digits) => Some(LiteralValue::BigInt(digits.clone())),
691 other => LiteralValue::of_enum_member(other),
692 }
693}
694
695pub(super) fn unit_value(ty: &Type) -> Option<LiteralValue> {
698 match ty.peel() {
699 Type::Null => Some(LiteralValue::Null),
700 Type::Undefined | Type::Void => Some(LiteralValue::Undefined),
701 other => unit_literal_value(other),
702 }
703}
704
705pub fn strip_null(ty: &Type) -> Type {
708 if let Type::Refined {
709 original,
710 ty: shape,
711 } = ty.without_aliases()
712 {
713 return preserve_refinement(original, strip_null(shape));
714 }
715 match ty.peel() {
716 Type::Null => Type::Error,
717 Type::Union(members) => without_nullish_refinements(Type::union(
718 members
719 .iter()
720 .filter(|m| !matches!(m.peel(), Type::Null))
721 .map(|m| {
722 if is_type_parameter_or_its_narrowing(m) {
723 strip_null(m)
724 } else {
725 m.clone()
726 }
727 })
728 .collect(),
729 )),
730 Type::Unknown => Type::union(vec![Type::empty_object(), Type::Undefined]),
732 Type::GenericParam { .. } => preserve_refinement(ty, strip_null(&Type::Unknown)),
735 _ => ty.clone(),
737 }
738}
739
740pub fn strip_undefined(ty: &Type) -> Type {
744 if let Type::Refined {
745 original,
746 ty: shape,
747 } = ty.without_aliases()
748 {
749 return preserve_refinement(original, strip_undefined(shape));
750 }
751 match ty.peel() {
752 Type::Undefined => Type::Error,
753 Type::Union(members) => without_nullish_refinements(Type::union(
754 members
755 .iter()
756 .filter(|member| !matches!(member.peel(), Type::Undefined))
757 .map(|member| {
758 if is_type_parameter_or_its_narrowing(member) {
759 strip_undefined(member)
760 } else {
761 member.clone()
762 }
763 })
764 .collect(),
765 )),
766 Type::Unknown => Type::union(vec![Type::empty_object(), Type::Null]),
767 Type::GenericParam { .. } => preserve_refinement(ty, strip_undefined(&Type::Unknown)),
768 _ => ty.clone(),
769 }
770}
771
772fn is_type_parameter_or_its_narrowing(member: &Type) -> bool {
775 matches!(member.peel(), Type::GenericParam { .. }) || member.refines_type_parameter()
776}
777
778pub fn strip_nullish(ty: &Type) -> Type {
780 strip_undefined(&strip_null(ty))
781}
782
783pub fn narrow_field_presence(
787 receiver_ty: &Type,
788 field: &str,
789 lookup: &dyn Fn(&Type, &str) -> Option<crate::ObjectField>,
790) -> (Type, Type) {
791 if let Type::Refined {
792 original,
793 ty: shape,
794 } = receiver_ty.without_aliases()
795 {
796 let (present, absent) = narrow_field_presence(shape, field, lookup);
797 return (
798 preserve_refinement(original, present),
799 preserve_refinement(original, absent),
800 );
801 }
802 match receiver_ty.peel() {
803 Type::Unknown | Type::NonPrimitive => {
804 let fields = BTreeMap::from([(
805 field.to_string(),
806 crate::ObjectField::required(Type::Unknown),
807 )]);
808 (
809 Type::Object {
810 index: None,
811 fields,
812 },
813 receiver_ty.clone(),
814 )
815 }
816 Type::Object { fields, .. } => {
817 let mut present = fields.clone();
818 present
819 .entry(field.to_string())
820 .or_insert_with(|| crate::ObjectField::required(Type::Unknown));
821 let absent = match fields.get(field) {
822 Some(f) if !f.optional => Type::Error,
823 _ => receiver_ty.clone(),
824 };
825 (
826 Type::Object {
827 index: None,
828 fields: present,
829 },
830 absent,
831 )
832 }
833 Type::Union(members) => {
834 let has = members
835 .iter()
836 .filter(|m| lookup(m, field).is_some())
837 .cloned()
838 .collect();
839 let lacks = members
840 .iter()
841 .filter(|m| lookup(m, field).is_none_or(|f| f.optional))
842 .cloned()
843 .collect();
844 (Type::union(has), Type::union(lacks))
845 }
846 Type::InterfaceRef { .. } | Type::ClassRef { .. } => {
847 let absent = if lookup(receiver_ty, field).is_some_and(|f| !f.optional) {
848 Type::Error
849 } else {
850 receiver_ty.clone()
851 };
852 (receiver_ty.clone(), absent)
853 }
854 _ => (Type::Error, Type::Error),
855 }
856}
857
858#[derive(Clone, Debug, Default)]
865pub struct NarrowEnv {
866 views: BTreeMap<ReferencePath, NarrowedView>,
867 pub dropped: BTreeMap<ReferencePath, Type>,
870}
871
872impl NarrowEnv {
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn insert(&mut self, path: ReferencePath, view: NarrowedView) -> Option<NarrowedView> {
878 self.dropped.remove(&path);
879 self.views.insert(path, view)
880 }
881
882 pub fn extend_env(&mut self, other: Self) {
883 self.dropped.extend(other.dropped);
884 self.extend(other.views);
885 }
886
887 pub fn clear(&mut self) {
888 self.views.clear();
889 self.dropped.clear();
890 }
891}
892
893impl std::ops::Deref for NarrowEnv {
894 type Target = BTreeMap<ReferencePath, NarrowedView>;
895 fn deref(&self) -> &Self::Target {
896 &self.views
897 }
898}
899
900impl std::ops::DerefMut for NarrowEnv {
901 fn deref_mut(&mut self) -> &mut Self::Target {
902 &mut self.views
903 }
904}
905
906impl IntoIterator for NarrowEnv {
907 type Item = (ReferencePath, NarrowedView);
908 type IntoIter = std::collections::btree_map::IntoIter<ReferencePath, NarrowedView>;
909 fn into_iter(self) -> Self::IntoIter {
910 self.views.into_iter()
911 }
912}
913
914impl<'a> IntoIterator for &'a NarrowEnv {
915 type Item = (&'a ReferencePath, &'a NarrowedView);
916 type IntoIter = std::collections::btree_map::Iter<'a, ReferencePath, NarrowedView>;
917 fn into_iter(self) -> Self::IntoIter {
918 self.views.iter()
919 }
920}
921
922impl Extend<(ReferencePath, NarrowedView)> for NarrowEnv {
923 fn extend<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(&mut self, iter: I) {
924 for (path, view) in iter {
925 self.insert(path, view);
926 }
927 }
928}
929
930impl FromIterator<(ReferencePath, NarrowedView)> for NarrowEnv {
931 fn from_iter<I: IntoIterator<Item = (ReferencePath, NarrowedView)>>(iter: I) -> Self {
932 let mut env = Self::new();
933 env.extend(iter);
934 env
935 }
936}
937
938pub fn wrap_order(a: &ReferencePath, b: &ReferencePath) -> std::cmp::Ordering {
943 b.chain.len().cmp(&a.chain.len()).then_with(|| a.cmp(b))
944}
945
946pub fn envs_per_path_narrowed_ty_equal(a: &NarrowEnv, b: &NarrowEnv) -> bool {
949 if a.len() != b.len() {
950 return false;
951 }
952 for (path, a_view) in a {
953 match b.get(path) {
954 Some(b_view) if a_view.narrowed_ty == b_view.narrowed_ty => continue,
955 _ => return false,
956 }
957 }
958 true
959}
960
961#[derive(Clone, Copy, Debug, PartialEq, Eq)]
964pub enum TruthinessClass {
965 AlwaysTruthy,
966 AlwaysFalsy,
967 NumberLike,
969 StringLike,
971 BooleanLike,
973 BigIntLike,
975 Dynamic,
977}
978
979pub fn truthiness_class(member: &Type) -> TruthinessClass {
980 use TruthinessClass::*;
981 match member.peel() {
982 Type::Null | Type::Undefined => AlwaysFalsy,
983 Type::Boolean => BooleanLike,
984 Type::BooleanLiteral(true) => AlwaysTruthy,
985 Type::BooleanLiteral(false) => AlwaysFalsy,
986 Type::Number => NumberLike,
987 Type::NumberLiteral(n) => {
988 if n.0 == 0.0 || n.0.is_nan() {
989 AlwaysFalsy
990 } else {
991 AlwaysTruthy
992 }
993 }
994 Type::String => StringLike,
995 Type::StringLiteral(s) => {
996 if s.is_empty() {
997 AlwaysFalsy
998 } else {
999 AlwaysTruthy
1000 }
1001 }
1002 Type::BigInt => BigIntLike,
1003 Type::BigIntLiteral(digits) => {
1004 if digits == "0" {
1005 AlwaysFalsy
1006 } else {
1007 AlwaysTruthy
1008 }
1009 }
1010 Type::NumberEnum {
1011 member: Some(member),
1012 ..
1013 } => truthiness_class(&Type::NumberLiteral(member.value)),
1014 Type::StringEnum {
1015 member: Some(member),
1016 ..
1017 } => truthiness_class(&Type::StringLiteral(member.value.clone())),
1018 Type::NumberEnum { .. } => NumberLike,
1019 Type::StringEnum { .. } => StringLike,
1020 empty if empty.is_empty_object() => Dynamic,
1023 Type::Object { .. }
1024 | Type::Array(_)
1025 | Type::Tuple(_)
1026 | Type::Function { .. }
1027 | Type::Uint8Array
1028 | Type::InterfaceRef { .. }
1029 | Type::ClassRef { .. }
1030 | Type::NonPrimitive => AlwaysTruthy,
1031 _ => Dynamic,
1032 }
1033}
1034
1035pub fn has_known_truthiness(ty: &Type) -> bool {
1038 union_members(ty).into_iter().all(|member| {
1039 matches!(
1040 truthiness_class(member),
1041 TruthinessClass::AlwaysTruthy | TruthinessClass::AlwaysFalsy
1042 )
1043 })
1044}
1045
1046pub fn negation_type(ty: &Type) -> Type {
1050 let classes: Vec<TruthinessClass> = union_members(ty)
1051 .into_iter()
1052 .map(truthiness_class)
1053 .collect();
1054 if classes
1055 .iter()
1056 .all(|class| *class == TruthinessClass::AlwaysTruthy)
1057 {
1058 return Type::BooleanLiteral(false);
1059 }
1060 if classes
1061 .iter()
1062 .all(|class| *class == TruthinessClass::AlwaysFalsy)
1063 {
1064 return Type::BooleanLiteral(true);
1065 }
1066 Type::Boolean
1067}
1068
1069pub(super) fn union_members(ty: &Type) -> Vec<&Type> {
1070 match ty.peel() {
1071 Type::Union(members) => members.iter().collect(),
1072 _ => vec![ty],
1074 }
1075}
1076
1077pub fn truthy_part(ty: &Type) -> Type {
1081 if let Type::Refined {
1082 original,
1083 ty: shape,
1084 } = ty.without_aliases()
1085 {
1086 return preserve_refinement(original, truthy_part(shape));
1087 }
1088 let kept: Vec<Type> = union_members(ty)
1090 .into_iter()
1091 .filter(|m| truthiness_class(m) != TruthinessClass::AlwaysFalsy)
1092 .map(|m| match m.without_aliases() {
1093 Type::Boolean => Type::BooleanLiteral(true),
1094 Type::GenericParam { .. } => intersect_with(m, TypeFacts::TRUTHY),
1096 _ => m.clone(),
1097 })
1098 .collect();
1099 Type::union(kept)
1100}
1101
1102pub fn is_never_falsy(ty: &Type) -> bool {
1104 union_members(ty)
1105 .into_iter()
1106 .all(|m| truthiness_class(m) == TruthinessClass::AlwaysTruthy)
1107}
1108
1109pub fn falsy_part(ty: &Type) -> Type {
1115 if let Type::Refined {
1116 original,
1117 ty: shape,
1118 } = ty.without_aliases()
1119 {
1120 return preserve_refinement(original, falsy_part(shape));
1121 }
1122 let kept: Vec<Type> = union_members(ty)
1123 .into_iter()
1124 .filter_map(|m| match truthiness_class(m) {
1125 TruthinessClass::AlwaysTruthy => None,
1126 TruthinessClass::StringLike => Some(match m.peel() {
1127 Type::String => Type::StringLiteral(String::new()),
1128 _ => m.clone(),
1129 }),
1130 _ => Some(match m.without_aliases() {
1131 Type::Boolean => Type::BooleanLiteral(false),
1132 _ => m.clone(),
1133 }),
1134 })
1135 .collect();
1136 Type::union(kept)
1137}
1138
1139pub fn falsy_values_are_only_null(ty: &Type) -> bool {
1142 union_members(ty).into_iter().all(|m| {
1143 matches!(m.peel(), Type::Null) || truthiness_class(m) == TruthinessClass::AlwaysTruthy
1144 })
1145}
1146
1147pub fn type_matches_facts(ty: &Type, facts: TypeFacts) -> bool {
1148 let ty = ty.peel();
1152 if facts.contains(TypeFacts::TRUTHY) {
1156 return truthiness_class(ty) != TruthinessClass::AlwaysFalsy;
1157 }
1158 if facts.contains(TypeFacts::FALSY) {
1159 return truthiness_class(ty) != TruthinessClass::AlwaysTruthy;
1160 }
1161 if facts.contains(TypeFacts::NE_NULL) || facts.contains(TypeFacts::NE_UNDEFINED) {
1162 return !(facts.contains(TypeFacts::NE_NULL) && matches!(ty, Type::Null)
1163 || facts.contains(TypeFacts::NE_UNDEFINED) && matches!(ty, Type::Undefined));
1164 }
1165 if facts.contains(TypeFacts::IS_UNDEFINED) {
1168 return matches!(ty, Type::Undefined);
1169 }
1170 if facts.contains(TypeFacts::EQ_UNDEFINED) {
1171 return matches!(ty, Type::Undefined);
1172 }
1173 if facts.contains(TypeFacts::EQ_NULL) {
1174 return matches!(ty, Type::Null);
1175 }
1176 if facts.contains(TypeFacts::IS_BIGINT) {
1177 return matches!(ty, Type::BigInt);
1178 }
1179 if facts.contains(TypeFacts::IS_NUMBER) {
1180 return matches!(
1182 ty,
1183 Type::Number | Type::NumberLiteral(_) | Type::NumberEnum { .. }
1184 );
1185 }
1186 if facts.contains(TypeFacts::IS_STRING) {
1187 return matches!(
1188 ty,
1189 Type::String | Type::StringLiteral(_) | Type::StringEnum { .. }
1190 );
1191 }
1192 if facts.contains(TypeFacts::IS_BOOLEAN) {
1193 return matches!(ty, Type::Boolean | Type::BooleanLiteral(_));
1194 }
1195 if facts.contains(TypeFacts::IS_ARRAY) {
1196 return matches!(ty, Type::Array(_));
1197 }
1198 if facts.contains(TypeFacts::IS_FUNCTION) {
1199 return matches!(ty, Type::Function { .. } | Type::NonPrimitive);
1202 }
1203 if facts.contains(TypeFacts::IS_OBJECT) {
1204 return is_typeof_object(ty);
1205 }
1206 false
1207}
1208
1209fn is_typeof_object(ty: &Type) -> bool {
1217 match ty {
1218 Type::Union(members) => members.iter().all(is_typeof_object),
1221
1222 Type::Object { .. }
1223 | Type::Array(_)
1224 | Type::Tuple(_)
1225 | Type::InterfaceRef { .. }
1226 | Type::ClassRef { .. }
1227 | Type::Uint8Array
1228 | Type::NonPrimitive
1229 | Type::Null => true,
1231
1232 Type::Undefined
1233 | Type::Number
1234 | Type::NumberLiteral(_)
1235 | Type::NumberEnum { .. }
1236 | Type::String
1237 | Type::StringLiteral(_)
1238 | Type::StringEnum { .. }
1239 | Type::Boolean
1240 | Type::BooleanLiteral(_)
1241 | Type::BigInt
1242 | Type::BigIntLiteral(_)
1243 | Type::Function { .. } => false,
1244
1245 Type::Void
1252 | Type::Never
1253 | Type::Error
1254 | Type::Unknown
1255 | Type::TypeVar(_)
1256 | Type::GenericParam { .. }
1257 | Type::AliasRef { .. } => false,
1258
1259 Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => false,
1261 }
1262}
1263
1264pub fn intersect_with(ty: &Type, facts: TypeFacts) -> Type {
1269 if let Type::Refined {
1270 original,
1271 ty: shape,
1272 } = ty.without_aliases()
1273 {
1274 return preserve_refinement(original, intersect_with(shape, facts));
1275 }
1276 let peeled = ty.peel();
1277 if facts == TypeFacts::IS_OBJECT
1278 && let Some(narrowed) = typeof_object_narrowing(ty, peeled)
1279 {
1280 return narrowed;
1281 }
1282 if let Some(asserted) = asserted_type_for_facts(facts) {
1283 match peeled {
1284 Type::Unknown => return asserted,
1285 empty
1289 if empty.is_empty_object() && !matches!(asserted, Type::Null | Type::Undefined) =>
1290 {
1291 return asserted;
1292 }
1293 Type::NonPrimitive if matches!(asserted, Type::Array(_)) => return asserted,
1295 Type::GenericParam { .. } => {
1296 return Type::Refined {
1297 original: Box::new(ty.clone()),
1298 ty: Box::new(asserted),
1299 };
1300 }
1301 _ => {}
1302 }
1303 }
1304 if matches!(peeled, Type::Unknown) && tests_only_presence(facts) {
1305 return recombine_unknown(intersect_with(&unknown_as_union(), facts));
1306 }
1307 if matches!(peeled, Type::GenericParam { .. }) && tests_only_presence(facts) {
1308 return preserve_refinement(ty, intersect_with(&Type::Unknown, facts));
1309 }
1310 match peeled {
1311 Type::Union(members) => without_nullish_refinements(Type::union(
1312 members
1313 .iter()
1314 .map(|m| intersect_with(m, facts))
1315 .filter(|ty| !matches!(ty, Type::Error))
1316 .collect(),
1317 )),
1318 Type::Void if facts.contains(TypeFacts::IS_UNDEFINED) => Type::Undefined,
1321 Type::Boolean if facts == TypeFacts::TRUTHY => Type::BooleanLiteral(true),
1323 Type::Boolean if facts == TypeFacts::FALSY => Type::BooleanLiteral(false),
1324 _ if type_matches_facts(peeled, facts) => ty.clone(),
1325 _ if matches!(peeled, Type::Unknown) => Type::Unknown,
1326 _ => Type::Error,
1327 }
1328}
1329
1330fn typeof_object_narrowing(ty: &Type, peeled: &Type) -> Option<Type> {
1335 match peeled {
1336 Type::Unknown => Some(Type::union(vec![Type::NonPrimitive, Type::Null])),
1337 empty if empty.is_empty_object() => Some(Type::NonPrimitive),
1338 Type::GenericParam { .. } => Some(Type::union(
1339 [Type::NonPrimitive, Type::Null]
1340 .into_iter()
1341 .map(|shape| Type::Refined {
1342 original: Box::new(ty.clone()),
1343 ty: Box::new(shape),
1344 })
1345 .collect(),
1346 )),
1347 _ => None,
1348 }
1349}
1350
1351pub fn subtract(ty: &Type, facts: TypeFacts) -> Type {
1352 if let Type::Refined {
1353 original,
1354 ty: shape,
1355 } = ty.without_aliases()
1356 {
1357 return preserve_refinement(original, subtract(shape, facts));
1358 }
1359 let peeled = ty.peel();
1360 if matches!(peeled, Type::Unknown) {
1363 if tests_only_nullishness(facts) {
1364 return recombine_unknown(subtract(&unknown_as_union(), facts));
1365 }
1366 return Type::Unknown;
1367 }
1368 if matches!(peeled, Type::GenericParam { .. }) {
1369 return preserve_refinement(ty, subtract(&Type::Unknown, facts));
1370 }
1371 let ruled_out = |member: &Type| {
1375 !matches!(member.peel(), Type::Void)
1376 && !typeof_tag_keeps(member, facts)
1377 && type_matches_facts(member, facts)
1378 };
1379 match peeled {
1380 Type::Union(members) => {
1381 let kept: Vec<Type> = members.iter().filter(|m| !ruled_out(m)).cloned().collect();
1382 Type::union(kept)
1383 }
1384 _ if !ruled_out(peeled) => ty.clone(),
1385 _ => Type::Error,
1386 }
1387}
1388
1389fn typeof_tag_keeps(member: &Type, facts: TypeFacts) -> bool {
1393 let peeled = member.peel();
1394 let tags_object = facts.contains(TypeFacts::IS_OBJECT);
1395 let tags_function = facts.contains(TypeFacts::IS_FUNCTION);
1396 if peeled.is_empty_object() {
1397 return tags_object || tags_function;
1398 }
1399 tags_function && matches!(peeled, Type::NonPrimitive)
1400}
1401
1402pub(super) fn with_source_refinement(source: &Type, shape: Type) -> Type {
1403 match source.without_aliases() {
1404 Type::Refined { original, .. } => preserve_refinement(original, shape),
1405 _ => shape,
1406 }
1407}
1408
1409pub(super) fn preserve_refinement(original: &Type, shape: Type) -> Type {
1410 if matches!(shape, Type::Error | Type::Never) {
1411 return shape;
1412 }
1413 if matches!(shape, Type::Unknown) {
1415 return original.clone();
1416 }
1417 Type::Refined {
1418 original: Box::new(original.clone()),
1419 ty: Box::new(shape),
1420 }
1421}
1422
1423pub fn subtract_literals(ty: &Type, covered: &BTreeSet<LiteralValue>) -> Type {
1431 if let Type::Refined {
1432 original,
1433 ty: shape,
1434 } = ty.without_aliases()
1435 {
1436 return preserve_refinement(original, subtract_literals(shape, covered));
1437 }
1438 match ty.peel() {
1439 Type::Union(members) => {
1440 let kept: Vec<Type> = members
1441 .iter()
1442 .map(|m| subtract_literals(m, covered))
1443 .collect();
1444 Type::union(kept)
1445 }
1446 Type::Boolean => {
1448 let remaining: Vec<Type> = [false, true]
1449 .into_iter()
1450 .filter(|b| !covered.contains(&LiteralValue::Boolean(*b)))
1451 .map(Type::BooleanLiteral)
1452 .collect();
1453 Type::union(remaining)
1454 }
1455 single @ (Type::StringLiteral(_)
1456 | Type::NumberLiteral(_)
1457 | Type::BooleanLiteral(_)
1458 | Type::BigIntLiteral(_)
1459 | Type::Null
1460 | Type::Undefined) => match unit_value(single) {
1461 Some(lit) if covered.contains(&lit) => Type::Never,
1462 _ => ty.clone(),
1463 },
1464 _ => ty.clone(),
1465 }
1466}
1467
1468fn tests_only_presence(facts: TypeFacts) -> bool {
1472 let presence = TypeFacts::TRUTHY
1473 | TypeFacts::FALSY
1474 | TypeFacts::EQ_NULL
1475 | TypeFacts::NE_NULL
1476 | TypeFacts::EQ_UNDEFINED
1477 | TypeFacts::NE_UNDEFINED
1478 | TypeFacts::IS_UNDEFINED;
1479 facts != TypeFacts::EMPTY && presence.contains(facts)
1480}
1481
1482fn tests_only_nullishness(facts: TypeFacts) -> bool {
1484 tests_only_presence(facts)
1485 && !facts.contains(TypeFacts::TRUTHY)
1486 && !facts.contains(TypeFacts::FALSY)
1487}
1488
1489fn without_nullish_refinements(union: Type) -> Type {
1493 let Type::Union(members) = &union else {
1494 return union;
1495 };
1496 let holds = |value: &Type| members.iter().any(|member| member.peel() == value);
1497 let (holds_null, holds_undefined) = (holds(&Type::Null), holds(&Type::Undefined));
1498 if !holds_null && !holds_undefined {
1499 return union;
1500 }
1501 Type::union(
1502 members
1503 .iter()
1504 .filter_map(|member| match member {
1505 Type::Refined { original, ty } => {
1506 let mut shape = (**ty).clone();
1507 if holds_null {
1508 shape = strip_null(&shape);
1509 }
1510 if holds_undefined {
1511 shape = strip_undefined(&shape);
1512 }
1513 (!is_ruled_out(&shape)).then(|| preserve_refinement(original, shape))
1515 }
1516 other => Some(other.clone()),
1517 })
1518 .collect(),
1519 )
1520}
1521
1522fn unknown_as_union() -> Type {
1524 Type::Union(vec![Type::empty_object(), Type::Null, Type::Undefined])
1525}
1526
1527fn recombine_unknown(ty: Type) -> Type {
1530 if ty.holds_every_value() {
1531 return Type::Unknown;
1532 }
1533 ty
1534}
1535
1536fn asserted_type_for_facts(facts: TypeFacts) -> Option<Type> {
1540 if facts.contains(TypeFacts::IS_BIGINT) {
1541 return Some(Type::BigInt);
1542 }
1543 if facts.contains(TypeFacts::IS_NUMBER) {
1544 return Some(Type::Number);
1545 }
1546 if facts.contains(TypeFacts::IS_STRING) {
1547 return Some(Type::String);
1548 }
1549 if facts.contains(TypeFacts::IS_BOOLEAN) {
1550 return Some(Type::Boolean);
1551 }
1552 if facts.contains(TypeFacts::IS_ARRAY) {
1553 return Some(Type::Array(Box::new(Type::Unknown)));
1554 }
1555 if facts.contains(TypeFacts::EQ_NULL) {
1556 return Some(Type::Null);
1557 }
1558 if facts.contains(TypeFacts::EQ_UNDEFINED) || facts.contains(TypeFacts::IS_UNDEFINED) {
1559 return Some(Type::Undefined);
1560 }
1561 None
1564}
1565
1566pub fn union_envs(
1581 a_narrowings: NarrowEnv,
1582 a_assigned: BTreeSet<ReferencePath>,
1583 b_narrowings: NarrowEnv,
1584 b_assigned: BTreeSet<ReferencePath>,
1585) -> (NarrowEnv, BTreeSet<ReferencePath>) {
1586 let mut joined_narrowings = NarrowEnv::new();
1587 for (path, a_view) in &a_narrowings {
1588 if let Some(b_view) = b_narrowings.get(path) {
1589 let Some(joined_ty) = join_narrowed_types(&a_view.narrowed_ty, &b_view.narrowed_ty)
1590 else {
1591 continue;
1592 };
1593 let a_view = if is_ruled_out(&a_view.narrowed_ty) {
1595 b_view
1596 } else {
1597 a_view
1598 };
1599 joined_narrowings.insert(
1600 path.clone(),
1601 NarrowedView {
1602 narrowed_ty: joined_ty,
1603 facts: TypeFacts::EMPTY,
1608 excluded_literals: BTreeSet::new(),
1609 binding: a_view.binding.clone(),
1615 source: a_view.source,
1616 },
1617 );
1618 }
1619 }
1620
1621 for (path, ty) in a_narrowings.dropped.iter().chain(&b_narrowings.dropped) {
1622 let left_ty = a_narrowings
1623 .dropped
1624 .get(path)
1625 .or_else(|| a_narrowings.get(path).map(|view| &view.narrowed_ty));
1626 let right_ty = b_narrowings
1627 .dropped
1628 .get(path)
1629 .or_else(|| b_narrowings.get(path).map(|view| &view.narrowed_ty));
1630 if left_ty == Some(ty) && right_ty == Some(ty) {
1631 joined_narrowings.dropped.insert(path.clone(), ty.clone());
1632 }
1633 }
1634
1635 let mut joined_assigned = a_assigned;
1636 joined_assigned.extend(b_assigned);
1637
1638 joined_narrowings.dropped.retain(|key, _| {
1639 !joined_assigned
1640 .iter()
1641 .any(|assigned| assigned.is_prefix_of(key))
1642 });
1643 joined_narrowings.retain(|key, _| {
1644 !joined_assigned
1645 .iter()
1646 .any(|assigned| assigned != key && assigned.is_prefix_of(key))
1647 });
1648
1649 (joined_narrowings, joined_assigned)
1650}
1651
1652pub(super) fn tuple_position(index: f64) -> Option<usize> {
1654 (index >= 0.0 && index.fract() == 0.0 && index <= u32::MAX as f64).then_some(index as usize)
1655}
1656
1657pub const RULED_OUT: Type = Type::Error;
1662
1663pub fn is_ruled_out(ty: &Type) -> bool {
1665 matches!(ty, Type::Error)
1666}
1667
1668pub(super) fn join_narrowed_types(left: &Type, right: &Type) -> Option<Type> {
1672 let joined = join_flow_types(left, right);
1673 (!joined.holds_every_value()).then_some(joined)
1674}
1675
1676fn join_flow_types(left: &Type, right: &Type) -> Type {
1679 if is_ruled_out(left) {
1681 return right.clone();
1682 }
1683 if is_ruled_out(right) {
1684 return left.clone();
1685 }
1686 let joined = Type::union(vec![left.clone(), right.clone()]);
1687 let Type::Union(members) = joined else {
1688 return joined;
1689 };
1690 let mut members = crate::types::without_members_of_empty_object(members);
1691 let has_number = members
1692 .iter()
1693 .any(|ty| matches!(ty.without_aliases(), Type::Number));
1694 let has_string = members
1695 .iter()
1696 .any(|ty| matches!(ty.without_aliases(), Type::String));
1697 members.retain(|ty| match ty.without_aliases() {
1698 Type::NumberLiteral(_) => !has_number,
1699 Type::StringLiteral(_) => !has_string,
1700 _ => true,
1701 });
1702 Type::union(members)
1703}
1704
1705pub fn has_erased_member(ty: &Type) -> bool {
1713 match ty.peel() {
1714 Type::TypeVar(_) | Type::GenericParam { .. } => true,
1715 Type::Union(members) => members.iter().any(has_erased_member),
1716 _ => false,
1717 }
1718}
1719
1720pub fn condition_compatible(ty: &Type) -> bool {
1728 !matches!(ty.peel(), Type::Unknown | Type::Void)
1729}
1730
1731pub fn facts_for_target_type(target: &Type) -> TypeFacts {
1732 match target.peel() {
1733 Type::Number | Type::NumberLiteral(_) => TypeFacts::IS_NUMBER,
1734 Type::String | Type::StringLiteral(_) => TypeFacts::IS_STRING,
1735 Type::Boolean | Type::BooleanLiteral(_) => TypeFacts::IS_BOOLEAN,
1736 Type::Null => TypeFacts::EQ_NULL,
1737 Type::Undefined => TypeFacts::EQ_UNDEFINED,
1738 Type::BigInt => TypeFacts::IS_BIGINT,
1739 Type::Array(_) => TypeFacts::IS_ARRAY,
1740 _ => TypeFacts::EMPTY,
1741 }
1742}
1743
1744#[cfg(test)]
1745mod analyzer_tests {
1746 use super::*;
1747
1748 #[test]
1749 fn typeof_undefined_splits_void_without_assuming_its_truthiness() {
1750 let ty = Type::union(vec![Type::Boolean, Type::Void]);
1751 assert_eq!(
1752 intersect_with(&ty, TypeFacts::IS_UNDEFINED),
1753 Type::Undefined
1754 );
1755 assert_eq!(subtract(&ty, TypeFacts::IS_UNDEFINED), ty);
1756 assert_eq!(truthiness_class(&Type::Void), TruthinessClass::Dynamic);
1757 assert!(!type_matches_facts(&Type::Void, TypeFacts::EQ_UNDEFINED));
1758 }
1759
1760 #[test]
1761 fn undefined_facts_are_distinct_from_null() {
1762 let ty = Type::union(vec![Type::String, Type::Null, Type::Undefined]);
1763 assert_eq!(
1764 strip_null(&ty),
1765 Type::union(vec![Type::String, Type::Undefined])
1766 );
1767 assert_eq!(
1768 strip_undefined(&ty),
1769 Type::union(vec![Type::String, Type::Null])
1770 );
1771 assert_eq!(strip_nullish(&ty), Type::String);
1772 assert_eq!(
1773 intersect_with(&ty, TypeFacts::EQ_UNDEFINED),
1774 Type::Undefined
1775 );
1776 assert_eq!(
1777 intersect_with(&Type::Unknown, TypeFacts::EQ_UNDEFINED),
1778 Type::Undefined
1779 );
1780 assert_eq!(
1781 intersect_with(&ty, TypeFacts::NE_NULL | TypeFacts::NE_UNDEFINED),
1782 Type::String
1783 );
1784 assert_eq!(truthy_part(&ty), Type::String);
1785 assert_eq!(
1786 truthiness_class(&Type::Undefined),
1787 TruthinessClass::AlwaysFalsy
1788 );
1789 assert!(!type_matches_facts(&Type::Undefined, TypeFacts::IS_OBJECT));
1790 }
1791
1792 #[test]
1793 fn presence_keeps_optional_read_undefined() {
1794 let ty = Type::Object {
1795 fields: BTreeMap::from([("value".into(), crate::ObjectField::optional(Type::Number))]),
1796 index: None,
1797 };
1798 let (present, absent) = narrow_field_presence(&ty, "value", &|_, _| None);
1799 assert_eq!(present, ty);
1800 assert_eq!(absent, ty);
1801 }
1802
1803 #[test]
1804 fn refined_literal_residual_retains_generic_identity() {
1805 let original = Type::GenericParam {
1806 id: 1,
1807 name: "T".into(),
1808 };
1809 let shape = Type::union(vec![
1810 Type::StringLiteral("a".into()),
1811 Type::StringLiteral("b".into()),
1812 ]);
1813 let refined = preserve_refinement(&original, shape);
1814 let covered = BTreeSet::from([LiteralValue::String("a".into())]);
1815 assert_eq!(
1816 subtract_literals(&refined, &covered),
1817 preserve_refinement(&original, Type::StringLiteral("b".into()))
1818 );
1819 }
1820
1821 #[test]
1822 fn strip_null_from_nullable_string() {
1823 let ty = Type::union(vec![Type::String, Type::Null]);
1824 assert_eq!(strip_null(&ty), Type::String);
1825 }
1826
1827 #[test]
1828 fn strip_null_from_non_nullable_is_identity() {
1829 assert_eq!(strip_null(&Type::String), Type::String);
1830 assert_eq!(strip_null(&Type::Number), Type::Number);
1831 }
1832
1833 #[test]
1834 fn strip_null_from_null_is_error() {
1835 assert_eq!(strip_null(&Type::Null), Type::Error);
1836 }
1837
1838 #[test]
1839 fn strip_null_from_multi_member_union() {
1840 let ty = Type::union(vec![Type::String, Type::Number, Type::Null]);
1841 let stripped = strip_null(&ty);
1842 match stripped {
1844 Type::Union(members) => assert_eq!(members.len(), 2),
1845 _ => panic!("expected Union, got {stripped:?}"),
1846 }
1847 }
1848
1849 #[test]
1850 fn intersect_with_is_number_filters_union() {
1851 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1852 let kept = intersect_with(&ty, TypeFacts::IS_NUMBER);
1853 assert_eq!(kept, Type::Number);
1854 }
1855
1856 #[test]
1857 fn subtract_is_number_keeps_others() {
1858 let ty = Type::union(vec![Type::Number, Type::String, Type::Boolean]);
1859 let kept = subtract(&ty, TypeFacts::IS_NUMBER);
1860 match kept {
1861 Type::Union(members) => {
1862 assert_eq!(members.len(), 2);
1863 assert!(members.contains(&Type::String));
1864 assert!(members.contains(&Type::Boolean));
1865 }
1866 _ => panic!("expected Union, got {kept:?}"),
1867 }
1868 }
1869
1870 #[test]
1871 fn intersect_with_truthy_strips_null() {
1872 let ty = Type::union(vec![Type::String, Type::Null]);
1873 let kept = intersect_with(&ty, TypeFacts::TRUTHY);
1874 assert_eq!(kept, Type::String);
1875 }
1876
1877 #[test]
1878 fn subtract_truthy_keeps_only_null() {
1879 let ty = Type::union(vec![Type::String, Type::Null]);
1880 let kept = subtract(&ty, TypeFacts::TRUTHY);
1881 assert_eq!(kept, Type::Null);
1882 }
1883
1884 #[test]
1885 fn intersect_with_no_match_yields_error() {
1886 let kept = intersect_with(&Type::String, TypeFacts::IS_NUMBER);
1887 assert_eq!(kept, Type::Error);
1888 }
1889
1890 #[test]
1891 fn facts_for_target_type_covers_primitives() {
1892 assert_eq!(facts_for_target_type(&Type::Number), TypeFacts::IS_NUMBER);
1893 assert_eq!(facts_for_target_type(&Type::String), TypeFacts::IS_STRING);
1894 assert_eq!(facts_for_target_type(&Type::Boolean), TypeFacts::IS_BOOLEAN);
1895 assert_eq!(facts_for_target_type(&Type::Null), TypeFacts::EQ_NULL);
1896 }
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901 use super::*;
1902 use crate::Span;
1903
1904 #[test]
1905 fn reference_path_equality_is_structural() {
1906 let a = ReferencePath::root(BindingId::Local {
1907 name: "x".to_string(),
1908 decl_scope: ScopeId(1),
1909 });
1910 let b = ReferencePath::root(BindingId::Local {
1911 name: "x".to_string(),
1912 decl_scope: ScopeId(1),
1913 });
1914 assert_eq!(a, b);
1915 }
1916
1917 #[test]
1918 fn reference_path_distinguishes_scopes() {
1919 let a = ReferencePath::root(BindingId::Local {
1920 name: "x".to_string(),
1921 decl_scope: ScopeId(1),
1922 });
1923 let b = ReferencePath::root(BindingId::Local {
1924 name: "x".to_string(),
1925 decl_scope: ScopeId(2),
1926 });
1927 assert_ne!(a, b);
1928 }
1929
1930 #[test]
1931 fn reference_path_distinguishes_chain() {
1932 let a = ReferencePath {
1933 root: BindingId::Local {
1934 name: "x".to_string(),
1935 decl_scope: ScopeId(1),
1936 },
1937 chain: vec![PathElem::Field("foo".to_string())],
1938 };
1939 let b = ReferencePath::root(BindingId::Local {
1940 name: "x".to_string(),
1941 decl_scope: ScopeId(1),
1942 });
1943 assert_ne!(a, b);
1944 }
1945
1946 #[test]
1947 fn type_facts_or_combines() {
1948 let f = TypeFacts::TRUTHY | TypeFacts::NE_NULL;
1949 assert!(f.contains(TypeFacts::TRUTHY));
1950 assert!(f.contains(TypeFacts::NE_NULL));
1951 assert!(!f.contains(TypeFacts::EQ_NULL));
1952 }
1953
1954 #[test]
1955 fn type_facts_not_complements() {
1956 let f = TypeFacts::EQ_NULL;
1957 let flipped = !f;
1958 assert!(flipped.contains(TypeFacts::NE_NULL));
1959 assert!(!flipped.contains(TypeFacts::EQ_NULL));
1960 }
1961
1962 #[test]
1963 fn binding_id_global_uses_mangled_name() {
1964 let g = BindingId::Global(crate::mangle::package_symbol("main", "x"));
1965 if let BindingId::Global(name) = &g {
1966 assert_eq!(name.as_str(), "main#x");
1967 } else {
1968 panic!("expected Global");
1969 }
1970 }
1971
1972 #[test]
1973 fn narrowed_view_construction() {
1974 let view = NarrowedView {
1975 narrowed_ty: Type::String,
1976 facts: TypeFacts::NE_NULL,
1977 excluded_literals: BTreeSet::new(),
1978 binding: Ident {
1979 name: "#narrow_0".to_string(),
1980 span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1981 },
1982 source: crate::ExprId(0),
1983 };
1984 assert_eq!(view.narrowed_ty, Type::String);
1985 assert!(view.facts.contains(TypeFacts::NE_NULL));
1986 }
1987
1988 #[test]
1989 fn cast_info_holds_kind_and_types() {
1990 let info = CastInfo {
1991 from_ty: Type::Union(vec![Type::String, Type::Null]),
1992 to_ty: Type::String,
1993 cast_kind: CastKind::NonNull,
1994 };
1995 assert!(matches!(info.cast_kind, CastKind::NonNull));
1996 }
1997
1998 #[test]
1999 fn union_discriminant_stub_returns_none() {
2000 assert!(union_discriminant(&[Type::String, Type::Null]).is_none());
2001 }
2002
2003 #[test]
2004 fn intersect_unknown_with_is_string() {
2005 assert_eq!(
2006 intersect_with(&Type::Unknown, TypeFacts::IS_STRING),
2007 Type::String
2008 );
2009 }
2010
2011 #[test]
2012 fn intersect_unknown_with_is_number() {
2013 assert_eq!(
2014 intersect_with(&Type::Unknown, TypeFacts::IS_NUMBER),
2015 Type::Number
2016 );
2017 }
2018
2019 #[test]
2020 fn intersect_unknown_with_is_boolean() {
2021 assert_eq!(
2022 intersect_with(&Type::Unknown, TypeFacts::IS_BOOLEAN),
2023 Type::Boolean
2024 );
2025 }
2026
2027 #[test]
2028 fn intersect_unknown_with_is_array_carries_unknown_element() {
2029 assert_eq!(
2032 intersect_with(&Type::Unknown, TypeFacts::IS_ARRAY),
2033 Type::Array(Box::new(Type::Unknown)),
2034 );
2035 }
2036
2037 #[test]
2038 fn intersect_unknown_with_eq_null() {
2039 assert_eq!(
2040 intersect_with(&Type::Unknown, TypeFacts::EQ_NULL),
2041 Type::Null
2042 );
2043 }
2044
2045 #[test]
2046 fn intersect_unknown_with_truthy_is_empty_object() {
2047 assert_eq!(
2049 intersect_with(&Type::Unknown, TypeFacts::TRUTHY),
2050 Type::empty_object()
2051 );
2052 }
2053
2054 #[test]
2055 fn subtract_unknown_removes_only_nullish_values() {
2056 assert_eq!(
2059 subtract(&Type::Unknown, TypeFacts::IS_STRING),
2060 Type::Unknown
2061 );
2062 assert_eq!(
2063 subtract(&Type::Unknown, TypeFacts::EQ_NULL),
2064 Type::union(vec![Type::empty_object(), Type::Undefined])
2065 );
2066 assert_eq!(subtract(&Type::Unknown, TypeFacts::TRUTHY), Type::Unknown);
2067 }
2068
2069 #[test]
2070 fn strip_null_from_unknown_leaves_empty_object_or_undefined() {
2071 assert_eq!(
2072 strip_null(&Type::Unknown),
2073 Type::union(vec![Type::empty_object(), Type::Undefined])
2074 );
2075 assert_eq!(strip_nullish(&Type::Unknown), Type::empty_object());
2076 }
2077
2078 #[test]
2079 fn condition_compatible_rejects_unknown() {
2080 assert!(!condition_compatible(&Type::Unknown));
2081 }
2082
2083 #[test]
2084 fn condition_compatible_accepts_js_truthiness_types() {
2085 for ty in [
2086 Type::Boolean,
2087 Type::String,
2088 Type::Number,
2089 Type::BigInt,
2090 Type::Null,
2091 Type::Array(Box::new(Type::Number)),
2092 Type::union(vec![Type::String, Type::Null]),
2093 Type::union(vec![Type::Number, Type::Void]),
2094 ] {
2095 assert!(condition_compatible(&ty), "{ty:?}");
2096 }
2097 assert!(condition_compatible(&Type::Never));
2098 assert!(!condition_compatible(&Type::Void));
2099 }
2100
2101 #[test]
2102 fn truthiness_class_table() {
2103 use TruthinessClass::*;
2104 let cases = [
2105 (Type::Null, AlwaysFalsy),
2106 (Type::Boolean, BooleanLike),
2107 (Type::Number, NumberLike),
2108 (
2109 Type::NumberLiteral(crate::types::LiteralF64(0.0)),
2110 AlwaysFalsy,
2111 ),
2112 (
2113 Type::NumberLiteral(crate::types::LiteralF64(-0.0)),
2114 AlwaysFalsy,
2115 ),
2116 (
2117 Type::NumberLiteral(crate::types::LiteralF64(3.0)),
2118 AlwaysTruthy,
2119 ),
2120 (Type::String, StringLike),
2121 (Type::StringLiteral(String::new()), AlwaysFalsy),
2122 (Type::StringLiteral("a".to_string()), AlwaysTruthy),
2123 (Type::BigInt, BigIntLike),
2124 (Type::Uint8Array, AlwaysTruthy),
2125 (Type::Array(Box::new(Type::Number)), AlwaysTruthy),
2126 (Type::Unknown, Dynamic),
2127 ];
2128 for (ty, expected) in cases {
2129 assert_eq!(truthiness_class(&ty), expected, "{ty:?}");
2130 }
2131 }
2132
2133 #[test]
2134 fn truthy_part_strips_null_and_falsy_literals() {
2135 assert_eq!(
2136 truthy_part(&Type::union(vec![Type::String, Type::Null])),
2137 Type::String
2138 );
2139 assert_eq!(
2140 truthy_part(&Type::union(vec![
2141 Type::StringLiteral(String::new()),
2142 Type::StringLiteral("a".to_string()),
2143 Type::Null,
2144 ])),
2145 Type::StringLiteral("a".to_string())
2146 );
2147 assert_eq!(truthy_part(&Type::Null), Type::Never);
2148 assert_eq!(truthy_part(&Type::Boolean), Type::BooleanLiteral(true));
2149 assert_eq!(truthy_part(&Type::Number), Type::Number);
2150 }
2151
2152 #[test]
2153 fn falsy_part_keeps_falsy_capable_members() {
2154 assert_eq!(
2155 falsy_part(&Type::union(vec![Type::String, Type::Null])),
2156 Type::union(vec![Type::StringLiteral(String::new()), Type::Null])
2157 );
2158 assert_eq!(falsy_part(&Type::Number), Type::Number);
2159 assert_eq!(falsy_part(&Type::Boolean), Type::BooleanLiteral(false));
2160 assert_eq!(
2162 falsy_part(&Type::union(vec![
2163 Type::Array(Box::new(Type::Number)),
2164 Type::Null
2165 ])),
2166 Type::Null
2167 );
2168 assert_eq!(
2169 falsy_part(&Type::Array(Box::new(Type::Number))),
2170 Type::Never
2171 );
2172 assert_eq!(falsy_part(&Type::empty_object()), Type::empty_object());
2173 }
2174
2175 #[test]
2176 fn falsy_only_null_distinguishes_reference_unions() {
2177 assert!(falsy_values_are_only_null(&Type::union(vec![
2178 Type::Array(Box::new(Type::Number)),
2179 Type::Null
2180 ])));
2181 assert!(!falsy_values_are_only_null(&Type::union(vec![
2182 Type::String,
2183 Type::Null
2184 ])));
2185 assert!(falsy_values_are_only_null(&Type::Uint8Array));
2186 assert!(!falsy_values_are_only_null(&Type::Number));
2187 }
2188
2189 #[test]
2190 fn intersect_truthy_and_falsy_respect_js_truthiness() {
2191 let string_or_null = Type::union(vec![Type::String, Type::Null]);
2192 assert_eq!(
2193 intersect_with(&string_or_null, TypeFacts::TRUTHY | TypeFacts::NE_NULL),
2194 Type::String
2195 );
2196 assert_eq!(
2198 intersect_with(&string_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2199 string_or_null
2200 );
2201 let arr_or_null = Type::union(vec![Type::Array(Box::new(Type::Number)), Type::Null]);
2202 assert_eq!(
2203 intersect_with(&arr_or_null, TypeFacts::FALSY | TypeFacts::EQ_NULL),
2204 Type::Null
2205 );
2206 assert_eq!(
2208 intersect_with(&string_or_null, TypeFacts::NE_NULL),
2209 Type::String
2210 );
2211 }
2212}