Skip to content

Commit 832c73a

Browse files
alexisthedevtheosotr
authored andcommitted
Refactor and fully outline the procedure for type unification for union types.
1 parent 4bf17c6 commit 832c73a

File tree

3 files changed

+198
-27
lines changed

3 files changed

+198
-27
lines changed

src/ir/type_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,8 +1040,10 @@ def unify_types(t1: tp.Type, t2: tp.Type, factory,
10401040
class A<T>
10411041
class B : A<String>()
10421042
"""
1043-
if t1.is_combound() and not t1.is_parameterized():
1044-
return t1.unify_types(t2, factory, same_type)
1043+
if t2.is_combound() and not t2.is_parameterized():
1044+
return t2.unify_types(t1, factory, same_type)
1045+
elif t1.is_combound() and not t1.is_parameterized() and t2.is_type_var():
1046+
return {t2: t1}
10451047

10461048
if same_type and type(t1) != type(t2):
10471049
return {}

src/ir/typescript_types.py

Lines changed: 191 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,10 @@ def is_combound(self):
438438
@two_way_subtyping
439439
def is_subtype(self, other):
440440
if isinstance(other, UnionType):
441-
return set(self.types).issubset(other.types)
441+
for t in self.types:
442+
if not any(t.is_subtype(other_t) for other_t in other.types):
443+
return False
444+
return True
442445
return other.name == 'Object'
443446

444447
def dynamic_subtyping(self, other):
@@ -491,7 +494,7 @@ def to_type_variable_free(self, factory):
491494
else t)
492495
return UnionType(new_types)
493496

494-
def unify_types(self, other, factory, same_type=True):
497+
def unify_types(self, t1, factory, same_type=True):
495498
"""
496499
This is used in src.ir.type_utils in the function
497500
unify_types.
@@ -501,31 +504,196 @@ def unify_types(self, other, factory, same_type=True):
501504
502505
For more information on the function see the detailed
503506
explanation at the unify_types function definition.
507+
"""
508+
t2 = self
509+
type_var_map = {}
510+
511+
if not t2.has_type_variables():
512+
return {}
513+
514+
# If T1 is a union type, then get all its types.
515+
t1_types = (t1.types if t1.is_combound() and
516+
not t1.is_parameterized()
517+
else [t1])
518+
519+
if not t1.is_subtype(t2):
520+
# Get the Type Variables of T2
521+
t_vars = dict(t2.get_type_variables(factory))
522+
523+
# Find which types of t1 are not already in t2
524+
add_to_t2 = set(t1_types) - set(t2.types)
525+
526+
# If T1 is a union type like 100 | number | string,
527+
# we do not need to substitute both 100 and number
528+
# in the type variables of T2.
529+
# Since number is a supertype of 100, if we only
530+
# substitute number, then we have also covered
531+
# the subtypes of 100 too!
532+
# Hence, we only substitute necessary types in T2
533+
# by ensuring that the T1 type we will subtitute
534+
# is NOT a subtype of any other types in the T1 union type.
535+
for t1_t in t1_types:
536+
if any(t1_t.is_subtype(other_t1_t)
537+
and t1_t is not other_t1_t
538+
for other_t1_t in t1_types):
539+
add_to_t2.remove(t1_t)
540+
541+
# If T1 is a union type, and its types that we need
542+
# to substitute in T2 are more than the type variables of T2,
543+
# then there is no substitution that ensures T1 <: T2.
544+
if len(add_to_t2) > len(t_vars):
545+
return {}
546+
547+
# Get bounds of type variables of T2
548+
bounds = [b for b in t_vars.values() if b != {None}]
549+
550+
# If the type variables have no bounds, then we can just assign
551+
# the types from T1 to any type variable.
552+
if not bounds:
553+
temp = list(t_vars.keys())
554+
for t in add_to_t2:
555+
tv = temp.pop(0)
556+
type_var_map[tv] = t
557+
return type_var_map
558+
559+
# Get all the possible substitutions between T1 types (add_to_t2)
560+
# and type variables of T2. A type variable can be substituted
561+
# with a type in T1 if the type variable has no bound or
562+
# if the type is a subtype of a bound.
563+
possible_substitutions = {}
564+
for t in add_to_t2:
565+
subs = set()
566+
# Below remember that:
567+
# - k is the type variable
568+
# - v is a set containing its bounds
569+
for k,v in t_vars.items():
570+
if v == {None} or any(t.is_subtype(b) for b in v):
571+
subs.add(k)
572+
573+
# If there are no possible substitutions with type variables
574+
# for any given type in T1 (add_to_t2) then there is no
575+
# substitution that ensures T1 <: T2.
576+
if not subs:
577+
return {}
578+
possible_substitutions[t] = subs
579+
580+
# Decide the order of assignments (if possible)
581+
assignments, flag = self.assign_types_to_type_vars(possible_substitutions)
582+
if not flag:
583+
return {}
584+
type_var_map.update(assignments)
585+
586+
587+
# Instantiate any not-utilized T2 type variables with their bound (if they have one).
588+
# If they don't have a bound instantiate them with a type from T1.
589+
leftover_type_vars = [t for t in t2.types if t.is_type_var() and t not in type_var_map]
590+
for type_var in leftover_type_vars:
591+
type_var_map[type_var] = type_var.bound if type_var.bound else t1_types[0]
592+
593+
return type_var_map
594+
595+
def assign_types_to_type_vars(self, possible_subs):
596+
"""
597+
This method is a helper for the method unify_types of union types (see above)
598+
599+
Args:
600+
- possible_subs: A dict containing (types.Type: set) pairs, which represents possible
601+
T2 type variable (value) susbstitutions with a T1 type (key).
602+
The set values contain compatible type vars of T2.
603+
604+
- t2_t_vars: The type variables of T2
605+
606+
- t1_types: The types of T1 that we want to substitute in T2
607+
608+
Returns:
609+
- A dict of (TypeVariable: types.Type) pairs representing the substitutions in T2
610+
611+
This method is needed because we need to find the correct substitutions
612+
of the type variables in the T2 union type, in order for all T1 types (T1 can be a union type itself)
613+
to be substituted in T2 (if possible).
504614
505-
The only way a union type type-unification can be
506-
achieved is when the first of the two types
507-
(here: self) is a union type and the second
508-
is either a type variable or another union type
509-
with similar union structure and has at least one
510-
type variable in its union.
615+
Consider the following case:
511616
617+
T1: number | string
618+
T2: boolean | X | Y extends number
619+
620+
In this case, if we first substituted X with the type number from T1,
621+
we would have been left with the type variable Y, which is not compatible
622+
with the type string.
623+
624+
As a result we would falsely conclude that we can not unify the types T1 and T2,
625+
when in reality, had we just substituted Y with number and X with string,
626+
we would have been ably to correctly unify the two types.
627+
628+
A naive solution would be to find all the possible substitution permutations
629+
between T1 types and T2 type variables.
630+
631+
Using our approach, after first creating the possible_subs dict,
632+
which contains all compatible T2 type variables for each T1 type,
633+
we first substitute the T1 type that is compatible with the FEWEST
634+
T2 type variables at any given moment.
635+
636+
Going back two the above case, here is how we tackle it with our new approach:
637+
638+
T1: number | string
639+
T2: boolean | X | Y extends number
640+
641+
(1) We find the possible substitutions for each T1 type (done outside this method)
642+
possible_subs = {number: {X, Y},
643+
string: {X}
644+
}
645+
646+
(2) We sort the dict based on the length of the type variable sets corresponding to each type
647+
sorted_type_subs = [(string, {X}), (number, {X, Y})]
648+
649+
(3) Now we work on the first element, the pair of string and {X}. We assign the substitution
650+
X: string. We remove X from all possible substitutions for other T1 types and then
651+
delete the pair with key string from our possible_subs dict.
652+
653+
(4) We sort the dict again, it now looks like this:
654+
sorted_type_subs = [(number, {Y})]
655+
656+
(5) Assign the substitution Y: number and repeat the rest of step (3)
657+
658+
(6) The possible_subs dict is now empty, so we return our substitution dictionary
659+
return {X: string, Y: number}
512660
"""
513661
type_var_map = {}
514-
if (isinstance(other, UnionType)
515-
and len(self.types) == len(other.types)
516-
and other.has_type_variables()):
517-
for i, t in enumerate(self.types):
518-
t1 = t
519-
t2 = other.types[i]
520-
is_type_var = t2.is_type_var()
521-
if not is_type_var:
522-
if not t2.is_subtype(t1):
523-
return {}
524-
else:
525-
type_var_map[t2] = t1
526-
elif other.is_type_var():
527-
type_var_map[other] = self
528-
return type_var_map
662+
663+
# Continue trying to find type variable susbstitutions until
664+
# all T1 types are substituted in T2.
665+
while possible_subs:
666+
# Sort the possible_subs dict, in order to first find a substitution for the T1
667+
# type with the fewest compatible T2 type variables.
668+
sorted_type_subs = sorted(possible_subs.items(), key=lambda x: len(x), reverse=True)
669+
670+
# Get the first (T1 type, T2 type variable) pair (sorted_type_subs is a tuples list)
671+
type_to_substitute, compatible_tvars = sorted_type_subs[0]
672+
673+
# If there aren't any compatible_tvars, then that means that there is no possible
674+
# order of substitutions that ensures all types in T1 are substituted in T2.
675+
# Hence, we return a False flag to indicate that the type unification is not possible.
676+
# Note: at this point this happens if at previous iterations of the while loop
677+
# we substituted all the type variables that are compatible with this specific type_to_substitute.
678+
if not compatible_tvars:
679+
return ({}, False)
680+
681+
# Get any of the compatible type variables and substitute it with the T1 type
682+
chosen_tvar = compatible_tvars.pop()
683+
type_var_map[chosen_tvar] = type_to_substitute
684+
685+
# Remove the substituted type variable from the possible substitutions
686+
# of all other T1 types.
687+
for k in list(possible_subs.keys()):
688+
if chosen_tvar in possible_subs[k]:
689+
possible_subs.remove(chosen_tvar)
690+
691+
# Delete the possible substitutions of the T1 type we just substituted in T2
692+
del possible_subs[type_to_substitute]
693+
694+
# Return the substitutions we found and a flag confirming that there is a possible
695+
# order of substitutions that gurantees type unification.
696+
return (type_var_map, True)
529697

530698
def get_name(self):
531699
return self.name

tests/test_typescript.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def test_unify_two_union_types():
134134
type_param = tp.TypeParameter("T")
135135
union1 = tst.UnionType([tst.NumberLiteralType(1410), tst.NumberType(), tst.StringType()])
136136
union2 = tst.UnionType([type_param, tst.NumberType(), tst.StringType()])
137+
assert union1.is_subtype(union2)
137138

138139
type_var_map = tu.unify_types(union1, union2, tst.TypeScriptBuiltinFactory())
139140
assert len(type_var_map) == 1
@@ -144,5 +145,5 @@ def test_unify_two_union_types():
144145

145146
type_var_map = tu.unify_types(union1, union3, tst.TypeScriptBuiltinFactory())
146147
assert len(type_var_map) == 2
147-
assert type_var_map == {type_param: union1.types[0],
148-
type_param2: union1.types[1]}
148+
assert type_param, type_param2 in type_var_map
149+
assert union1.types[1], union1.types[2] in type_var_map.values()

0 commit comments

Comments
 (0)