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 pub returns: Vec<(NarrowEnv, BTreeSet<ReferencePath>)>,
233}
234
235#[derive(Copy, Clone, Debug, PartialEq, Eq)]
236pub enum PendingJoinKind {
237 Function,
239 InvokedFunction,
241 Loop,
243 Switch,
245}
246
247#[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 UnresolvedArray,
279}
280
281#[derive(Clone, Debug, PartialEq, Eq)]
282pub struct NarrowedView {
283 pub evolving: Option<EvolvingType>,
285 pub narrowed_ty: Type,
286 pub facts: TypeFacts,
287 pub excluded_literals: BTreeSet<LiteralValue>,
290 pub binding: Ident,
293 pub source: ExprId,
297}
298
299#[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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
311pub enum CastKind {
312 NonNull,
314 Unbox,
319 RefSubtype,
323}
324
325pub 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 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#[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#[derive(Clone, Debug)]
368pub enum InvalidationReason {
369 ShapeUnrebuildable { narrowed_ty: Type },
371 Write { span: crate::Span },
375 Reassignment { span: crate::Span },
378 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
398pub type Discriminant = (String, DiscriminantTable);
401
402#[derive(Clone, Debug, Default, PartialEq)]
406pub struct DiscriminantTable {
407 entries: Vec<(LiteralValue, VariantIdx)>,
408}
409
410#[derive(Clone, Copy, PartialEq, Eq)]
412enum UndefinedNames {
413 Several,
415 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 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 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 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 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 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 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 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 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#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
535pub struct VariantIdx(pub u32);
536
537pub fn union_discriminant(members: &[Type]) -> Option<Discriminant> {
551 let shapes = inline_member_shapes(members)?;
557 discriminant_from_shapes(&shapes)
558}
559
560fn 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
574pub 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
582pub 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
603pub 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 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
628pub 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 table.insert(
652 lit,
653 VariantIdx(u32::try_from(idx).ok()?),
654 UndefinedNames::One,
655 )?;
656 }
657 Some(table)
658}
659
660pub 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
681pub 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
693pub 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
704pub 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 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
719pub 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
737pub(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
747pub 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 Type::Unknown => Type::union(vec![Type::empty_object(), Type::Undefined]),
774 peeled if holds_any_value_of_a_parameter(peeled) => {
777 preserve_refinement(ty, strip_null(&Type::Unknown))
778 }
779 _ => ty.clone(),
781 }
782}
783
784pub 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
818fn is_type_parameter_or_its_narrowing(member: &Type) -> bool {
822 holds_any_value_of_a_parameter(member.peel()) || member.refines_type_parameter()
823}
824
825pub fn strip_nullish(ty: &Type) -> Type {
827 strip_undefined(&strip_null(ty))
828}
829
830pub 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#[derive(Clone, Debug, Default)]
912pub struct NarrowEnv {
913 views: BTreeMap<ReferencePath, NarrowedView>,
914 pub definitely_assigned: BTreeSet<ReferencePath>,
917 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
994pub 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
1002pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1020pub enum TruthinessClass {
1021 AlwaysTruthy,
1022 AlwaysFalsy,
1023 NumberLike,
1025 StringLike,
1027 BooleanLike,
1029 BigIntLike,
1031 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 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
1091pub 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
1102pub 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 _ => vec![ty],
1130 }
1131}
1132
1133pub 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 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 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
1160pub fn is_never_falsy(ty: &Type) -> bool {
1162 union_members(ty)
1163 .into_iter()
1164 .all(|m| truthiness_class(m) == TruthinessClass::AlwaysTruthy)
1165}
1166
1167pub 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
1197pub 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 let ty = ty.peel();
1210 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 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 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 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
1270fn is_typeof_object(ty: &Type) -> bool {
1278 match ty {
1279 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 | 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 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 Type::Alias { .. } | Type::Refined { .. } | Type::Readonly(_) => false,
1324 }
1325}
1326
1327pub 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 empty
1352 if empty.is_empty_object() && !matches!(asserted, Type::Null | Type::Undefined) =>
1353 {
1354 return asserted;
1355 }
1356 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 Type::Void if facts.contains(TypeFacts::IS_UNDEFINED) => Type::Undefined,
1384 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
1393fn 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
1414fn 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 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 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
1459fn 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 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
1493pub 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 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
1539fn 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
1553fn tests_only_nullishness(facts: TypeFacts) -> bool {
1555 tests_only_presence(facts)
1556 && !facts.contains(TypeFacts::TRUTHY)
1557 && !facts.contains(TypeFacts::FALSY)
1558}
1559
1560fn 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 (!is_ruled_out(&shape)).then(|| preserve_refinement(original, shape))
1586 }
1587 other => Some(other.clone()),
1588 })
1589 .collect(),
1590 )
1591}
1592
1593fn unknown_as_union() -> Type {
1595 Type::Union(vec![Type::empty_object(), Type::Null, Type::Undefined])
1596}
1597
1598fn recombine_unknown(ty: Type) -> Type {
1601 if ty.holds_every_value() {
1602 return Type::Unknown;
1603 }
1604 ty
1605}
1606
1607fn 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 None
1635}
1636
1637pub 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 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: TypeFacts::EMPTY,
1684 excluded_literals: BTreeSet::new(),
1685 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
1728pub(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
1733pub const RULED_OUT: Type = Type::Error;
1738
1739pub fn is_ruled_out(ty: &Type) -> bool {
1741 matches!(ty, Type::Error)
1742}
1743
1744pub(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
1795pub(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
1803fn join_flow_types(left: &Type, right: &Type) -> Type {
1806 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
1832pub fn has_erased_member(ty: &Type) -> bool {
1840 match ty.peel() {
1841 Type::Union(members) => members.iter().any(has_erased_member),
1842 member => member.lowers_to_erased_slot(),
1844 }
1845}
1846
1847pub 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 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 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 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 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 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 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 assert_eq!(
2338 intersect_with(&string_or_null, TypeFacts::NE_NULL),
2339 Type::String
2340 );
2341 }
2342}