Polymorphic Type Inference for Dynamic Languages

We present a type system that combines, in a controlled way, first-order polymorphism with intersectiontypes, union types, and subtyping, and prove its safety. We then define a type reconstruction algorithm that issound and terminating. This yields a system in which unannotated functions are given polymorphic types(thanks to Hindley-Milner) that can express the overloaded behavior of the functions they type (thanks tothe intersection introduction rule) and that are deduced by applying advanced techniques of type narrowing(thanks to the union elimination rule). This makes the system a prime candidate to type dynamic languages.


INTRODUCTION
Typing dynamic languages is a challenging endeavour even for very simple pieces of code.For instance, JavaScript's logical or operator "||" behaves like the following function (also in JavaScript): 1 1 function lOr (x, y) { 2 if (x) { return x; } else { return y; } 3 } A naive type for this function is (Bool, Bool) → Bool, which states that lOr is a function that takes two Boolean arguments and returns a Boolean result.This however is an overly restrictive type, that does not account for the fact that in JavaScript logical operators such as lOr can be applied to any pairs of arguments, not just to Boolean ones.JavaScript distinguishes two kinds of values: eight "falsy" values (i.e., false, "", 0, −0, 0n, undefined, null, and NaN) and the "truthy" values (all the others).The expression if executes the else code if and only if the tested value is falsy.If we want to change the previous type to account for this fact, then we should give lOr the type (Any, Any) → Any (where Any is the type of all values), which is a rather useless type since it essentially states that lOr is a binary function.To give lOr a more informative type, we need union and intersection types (which are already integrated in typed versions of JavaScript such as then, as we explain later on, our system infers that id has type ∀ .→ , viz., that id is indeed the polymorphic identity function.This is clearly better than the current state of the art.Still, it does not seem too hard a feat to deduce that if we are testing whether x is a truthy value, then when the test succeeds we can assume that x is of type Truthy.To show the more advanced capabilities of our system let us have a look at how ECMAScript specifies the semantics of JavaScript logical operators, as defined in the 2021 version of the specification [Ecma 2021, Section 13.13.1].Since in JavaScript there are no union or intersection types, then the falsy and truthy values are defined via an (abstract) function ToBoolean which simply checks whether its argument is one of the 8 falsy values and returns false, otherwise it returns true (see its definition in row 1 of Table 1 in Section 5).In our system, ToBoolean has type (Truthy → true) ' (Falsy → false).All logical operators are then defined by ECMAScript in terms of this function: this has the advantage that any change to the specification of falsy (e.g., the addition of a new falsy value, like the addition of the built-in bigint type and its constant 0n in ES2020) requires only the modification of this function, and is automatically propagated to all operators.So the actual definition of lOr for ECMAScript is the following one: If we feed this function to our system, then it infers for it the type in (2), that is, the same type it already deduced for the simpler version of lOr defined in lines 1-3.But here the deduction needed to perform type narrowing is more challenging, since the system must deduce from the type (Truthy → true) ' (Falsy → false) of ToBoolean that when the application in line 8 returns a truthy value, then the argument of ToBoolean is of type Truthy, and it is of type Falsy otherwise.More generally, we need a system which, when a test is performed on an arbitrarily complex application, can narrow the type of all the variables occurring in the application by exploiting the information provided by the overloaded behavior of the functions therein.Achieving such a degree of precision is a hard feat but, we argue, it is necessary if we want to reconstruct types for dynamic languages, that is, if we want to type their programs as they are, without requiring the addition of any type annotations.Indeed, the core operators of these languages (e.g., JavaScript's "||", "&&", "typeof", . . . ) are characterized by an "overloaded" behavior, which is then passed over to the functions that use them.So for instance a simple use of JavaScript logical or "||" such as in (x => x || 42) results in a function whose precise type, as reconstructed by our system, is (Falsy → 42) ' (Truthy ' → Truthy ' ).JavaScript functions also routinely perform dynamic checks against constants (notably null and undefined), which our system also handles as part of its more general approach to type narrowing of arbitrary expressions.

Outline
Type System (Section 2).So, how can we achieve all this?Conceptually, it is quite simple: we just merge together three of the most expressive type systems studied in the literature, namely the Hindley-Milner (HM) polymorphic types [Hindley 1969;Milner 1978], intersection types [Coppo et al. 1981], and union types [Barbanera et al. 1995;MacQueen et al. 1986].We achieve it simply by putting together in a controlled way the deduction rules characteristic of each of these systems (see Figure 2 in Section 2) and proving that the resulting system is sound (cf., Theorem 2.2).
More precisely, the type system we describe in Section 2 is pretty straightforward.Its core is a classic HM system with first order polymorphism: a program is a list of let-bindings that define polymorphic functions; these are typed by inferring a type for the expressions that define them, this type is then generalized, yielding a prenex polymorphic type for the function.As usual, the deduction of the type of each of these expressions is performed in a type environment that records the generic types for the previously-defined polymorphic functions, and the type system can instantiate these types differently for each use of the polymorphic functions in the expression.The novelty of our system is that when deducing the types of the expressions that define the polymorphic functions, the type system can use not only instantiations of polymorphic types (rule [Inst] in Figure 2), but also intersection and union types.More precisely, to type these expressions the type system can decide to use the classic rules of intersection introduction (rule [']) and union elimination (rule [(]) given in Figure 2.For instance, the intersection introduction rule is used by the system to deduce that since the function lOr (either versions) has both type (( 'Truthy, Any) → 'Truthy) and type ((Falsy, ) → ), then it has their intersection, too; this intersection type is then generalized (when lOr is defined at top-level) yielding the polymorphic type in (2).The union elimination rule is essentially used to fine-grainedly type branching expressions and tests involving applications of overloaded functions: for instance, to deduce that the function id in lines 4-6 has type → , the system can assume that x has type and separately infer the type of the body for x : ( 'Truthy) and for x : ( '¬Truthy); since the first deduction yields ( 'Truthy) and the second yields ( '¬Truthy), then the system deduces that under the hypothesis x : , the body has the union of these two types, that is .Furthermore, as observed by Castagna et al. [2022b], the combination of the union elimination with the rules of type-cases given in Figure 2 constitutes the essence of narrowing and occurrence typing.
The declarative type system given in Section 2 is all well and good, but how can we define an algorithm that infers whether a given expression can be typed in this system?Rules such as union elimination and intersection introduction are easy to understand, but they do not easily lend themselves to an implementation.In order to arrive to an effective implementation of the type system specified in Section 2 we proceed in two steps: ( ) the definition of an algorithmic system and ( ) the definition of a reconstruction algorithm.
Algorithmic System (Section 3).The first step towards an effective implementation of our type system is taken in Section 3 where we define an algorithmic system that is sound and complete with respect to the system of Section 2. The system is algorithmic since it is composed only by syntaxdirected and analytic rules4 and, as such, is immediately implementable.It is sound and complete since an expression is typable in it if and only if it is typable in the system of Section 2. To obtain this results the system is defined on pairs formed by an MSC-form (Maximal Sharing Canonical form) and an annotation tree.MSC-forms are A-normal forms [Sabry and Felleisen 1992] on steroids: they are lists of bindings associating variables to expressions in which every proper subexpression is a variable.Their characteristic is that they encode expressions and preserve typability in the sense that every expression is typable if and only if its unique MSC-form is typable.MSC-forms were introduced by Castagna et al. [2022b] to drastically reduce the range of possible applications of the union elimination rule; here we improve their definition to deal with our polymorphic setting and use them for exactly the same reason as in [Castagna et al. 2022b].Annotation trees encode canonical derivations of the system of Section 2 for the MSC-form they are paired with.They are a generalization of type annotations inserted in the code.Instead of annotating directly an MSC-form with type-annotations we used a separate annotation tree because of the union elimination rule which types several times the same expression under different type environments; this would, thus, require different annotations for the same subexpressions, each annotation depending on the typing context: this naturally yields to tree-shaped annotations in which each branching corresponds either to the different deductions performed by a union elimination rule or to the different deductions performed by an intersection introduction rule.The soundness and completeness properties of the algorithmic systems are thus stated in terms of MSC-forms and annotation trees.They essentially state that an expression has type in the declarative system of Section 2 if and only if there exists a tree annotation for the (unique) MSC-form of that is typable in the algorithmic system with (a subtype of) : see Theorem 3.4.
Reconstruction Algorithm (Section 4).The second of the two steps to achieve an effective implementation for the type system of Section 2 is to define a reconstruction algorithm for the previous algorithmic system, which we do in Section 4. The statements of the soundness and completeness properties of the algorithmic system clearly suggest what this algorithm is expected to do: given an expression that defines a polymorphic function, the algorithm must transform it into its unique MSC-form and then try to reconstruct an annotation tree for it so that the pair MSC-form and annotation tree is typable in the algorithmic system of Section 3.
The reconstruction is performed by a system of deduction rules that incrementally refines an annotation tree (initially composed of a single node "infer") while exploring the list of bindings of the MSC-form of the expression to type.It mixes two independent mechanisms: one that infers the domain(s) of -terms, and the other that performs type narrowing when a typecase is encountered.
The first mechanism is inspired by the algorithm W by Damas and Milner [1982]: whenever the application of a destructor (e.g., a function application) is encountered, an algorithm finds a substitution (if any) that makes this application well-typed.In the context of a HM type system, the algorithm at issue needs to solve a unification problem (i.e., whether for two given types and there exists a substitution such as = ) which, if solvable, has a principal solution given by a single substitution [Robinson 1965].In our system, which is based on subtyping, the algorithm at issue needs to solve a tallying problem (i.e., whether for two given types and there exists a substitution such as f ) which, if solvable, has a principal solution given by a finite set of substitutions [Castagna et al. 2015].When multiple substitutions are found, they are all considered and explored in different branches by adding an intersection branching node in the current annotation tree.
The second mechanism gets inspiration from Castagna et al. [2022b] and refines decompositions made by the union-elimination rule in order to narrow the types of variables in the branches of a typecase expression.When the system encounters a typecase that tests whether some expression has type , then the type of the variable bound to (recall that an MSC-form is a list of bindings) is split into ' and '¬ , and these splits are in turn propagated recursively in order to generate new splits for the types of the variables associated with the subexpressions composing .For instance, when the algorithm encounters the test "if (ToBoolean(x))..." at line 8, it splits the type of (the variable bound to) ToBoolean(x) in two, by intersecting it with true and ¬true, and this split in turn generates a new split Truthy and Falsy for the type of the variable x.
The reconstruction algorithm we present in Section 4 is sound: if it returns an annotation tree for an MSC-form, then the pair is typable in the algorithmic system, whose soundness implies that the expression at the origin of the MSC-form is typable in the system of Section 2. At this point, however, it should be pretty obvious that such a reconstruction algorithm cannot be complete.Our system merges three well known systems: first-order parametric polymorphism, intersection types, union elimination.Now, even if parametric polymorphism is decidable, in our system we can encode (and type, via intersection types) polymorphic fixed-point combinators, yielding a system with polymorphic recursion whose inference has been long known to be undecidable [Henglein 1993;Kfoury et al. 1993].Worse, our system includes union elimination, which is one of the most problematic rules from an algorithmic viewpoint, not only because it is neither syntax directed nor analytic, but also because determining an inversion (a.k.a., generation) lemma for this rule is considered by experts the most important open problem in the research on union and intersection types [Dezani 2020], and an inversion lemma is somehow the first step to define a type-inference algorithm, since it tells us when and how to apply the rule.We discuss in detail the reasons and implications of incompleteness in Section 4.4.
Despite being incomplete, our reconstruction algorithm is powerful enough to handle both complicated typing use-cases and common programming patterns of dynamic languages.For instance, for the fixed-point combinator for strict languages = .( .( )) ( .( .)) our algorithm reconstructs the type ∀ , , .((→ ) → (( → )' )) → (( → )' )) (i.e., in bounded polymorphic notation ∀( )( )( f → ).(( → ) → ) → , cf.Footnote 2).The combinator can then be used as is, to define and infer the type of classic polymorphic functions such as map, fold, concat, reverse, etc., often yielding types more precise than in HM: for instance if we use [ * ] to denote the type of the lists whose elements have type , then the type inferred for (a curried version of) where the second type in the intersection states that if the third argument is an empty list, then the result will be the second argument, whatever the type of the first argument is.Finally, we designed our algorithm so that it can take into account explicit type annotations to help it in the inference process.As an example, our algorithm can check that the classic filter function has type ∀ , , .((' →Bool) ' ( '¬ →False)) → [ * ] → [( ' ) * ], stating that if we pass to filter a predicate that returns false for the elements of that are not in , then filtering a list of 's will return only elements also in .
Sections 2, 3, and 4 outlined above constitute the core of our contribution.Section 5 presents our implementation.In Section 6 we discuss related work and Section 7 concludes our presentation.For space reasons we omitted in the main text some rules of the algorithmic and reconstruction systems, as well as all proofs: they are all given in the appendix, available on line as supplemental material [Castagna et al. 2024].

Discussion, Contributions, and Limitations
Intersections vs. Hindley-Milner.It is a truth universally acknowledged that intersection type systems are more powerful than HM systems: for that, one does not even need full intersections, since Rank 2 intersections suffice.Rank 2 intersection types are types that may contain intersections only to the left of a single arrow and the system of Rank 2 intersection types is able to type all ML programs (i.e., all program typable by HM), has principal typings, decidable type inference, and the complexity of type inference is of the same order as in ML [Leivant 1983].
However, intersection type systems are not compositional, and this hinders their use in a modular setting.A program that uses the polymorphic identity function to apply it to, say, an integer and a Boolean, type checks since we can infer that the polymorphic identity function has type (Int→Int)'(Bool→Bool).But if we want to export this polymorphic identity function to use it in other unforeseen contexts, then we need for it a type that covers all its admissible usages, without the need of retype-checking the function every time it is applied to an argument of a new type.In other words, in a modular usage, parametric polymorphism has an edge over intersection/ad-hoc polymorphism despite being less powerful, since a type such as ∀ .→ synthesizes the infinitely many combinations of intersection types that can be deduced for the identity function; however in a local setting, everything that does not need to be exported can be finer-grainedly typed by intersection types.This division of roles and responsibilities is at the core of our approach.As we show in the next section, programs are lists of bindings from variables to expressions.These expressions are typed in a type environment (generated by the preceding bindings) which binds variables to polymorphic types.These expressions are typed by using instantiation, intersection introduction, and union elimination, but not generalization.Generalization is performed only at top level, that is at the level of programs and reserved to expressions to be used in other contexts.
Parametricity vs. type cases.A parametric polymorphic function (a.k.a., a generic function) is a function that behaves uniformly for all possible types of its arguments, that is, whose behavior does not depend on the type of its arguments.A common way to characterize a generic function is that it is a function that cannot inspect the parametric parts of its input, that is, those parts that are typed by a type variable: these parts can only be either returned as they are, or discarded, or passed to another generic function.Our approach suggests refining this characterization by shifting the attention from inputs as a whole to some particular values among all the possible inputs.This can be seen by comparing the following two function definitions: .( ∈Int) ?: .( ∈Int) ?+ 1 : Both functions test whether their input is an integer.The function on the right-hand side returns the successor of the argument when this is true and the argument itself otherwise; the one on the left-hand side returns its argument in both cases, that is, it is the identity function.
Our system deduces for the function on the left the type → . 5For the function on the right it returns the type (Int → Int) ' ( ∖Int → ∖Int), where ∖ denotes the set-theoretic difference of the two types, that is, '¬ .These two types suggest how we can refine the intuitive characterization of parametricity.A generic function can inspect the parametric part of its input (as the function on the left-hand side shows) and its output can depend on this inspection (as the function on the right-hand side shows), but the parts of its output that are typed by a type variable-i.e., the "parametric" parts-cannot depend on it.We can speak of "partial" parametricity, and say that a function is parametric "only" for the inputs (or parts thereof) that are either returned unchanged or discarded: the type variables in its type describe such inputs.For instance, the domain of both the functions above is Any: they both can be applied to any argument.But the first function is parametric for all possible inputs, since the result of the inspection is not used to produce any particular output (it has type ∀ .→ ), while the second function is parametric only for the values of its domain that are not in Int, since it uses the result of the inspection to generate the result for the integer inputs (by subsumption, the second function has type ∀ .→ Int(( ∖Int): parametricity holds only for the arguments not in Int).
Contributions.The general contribution of our work is twofold.First, it proposes a way to mix parametric and intersection/ad-hoc polymorphism which, in hindsight, is natural: parametric polymorphism for everything defined at top-level and that can thus be used in other contexts (modularity); intersection polymorphism for everything that remains local (for which we can thus use more precise non-modular typing).Second, it proposes an effective way to implement this type discipline by defining a reconstruction algorithm; with respect to that, a fundamental role is played by the analysis of the (type-)tests performed by the expressions, since they drive the way in which types are split: externally, to split the domain of functions yielding intersection of arrows (intersection introduction); internally, to split the type of tested expressions, yielding a precise typing of branching (union elimination).In doing so, it provides the first system that reconstructs types that oncombine parametric and ad hoc polymorphism.
The technical contributions of the work can be summarized as follows: (1) We define a type system that combines parametric polymorphism with union and intersection types for a functional calculus with type-cases and prove its soundness.(2) We define an algorithmic system that we prove sound and complete with respect to the previous system.
(3) We define an algorithm to reconstruct the type annotations of the previous algorithmic system and prove it sound and terminating.The reconstruction algorithm is fully implemented.A prototype which also implements optional type annotations and pattern matching (presented in Appendix A) is available on-line at https: //www.cduce.org/dynlang,and whose sources are on Zenodo: [Castagna et al. 2023b].
Limitations.The system we present here has some limitations.Foremost, the reconstruction algorithm of Section 4 uses backtracking, and at each of its passes it may try to type the same piece of code several times.Backtracking is inherent to our algorithm, since it proceeds by successively refining in different passes, the annotation tree of an MSC-form.The checking of a same piece of code several times at each pass is inherent to the use of unions and intersections: the unionelimination rule repeatedly type-checks the same expression, using different type hypotheses for a given sub-expression; the intersection-introduction rule verifies that an expression has all the types of an intersection, by checking each of them separately.Both features are very penalizing in terms of performance, and any naive implementation of the reconstruction described in Section 4 would yield type-inference times that grow exponentially with the size of the program.Clearly, this is an issue that must be addressed if we want to apply our system to real-world dynamic languages, and further work is needed to frame and/or constrain the current system so that its performance becomes acceptable.Fortunately, the room for improvement is significant: our prototype is an unoptimized proof of concept whose implementation was defined to faithfully simulate the reconstruction inference rules, rather than to obtain an efficient execution; but the simple addition of textbook memoization techniques improved its performance by an order of magnitude (cf.Section 5).
A second limitation of our system is that it is not sound in the presence of side-effects.The algorithm transforms an initial expression into its Maximal Sharing Canonical form, which is a list of bindings, one for each sub-expression of the initial expression.As we explain in Section 3.1.2,these forms are called "maximal sharing" since all equivalent sub-expressions (in the sense stated by Definition 3.1) of the initial expression must be bound by the same variable, so that any refinement of the type of one sub-expression (e.g., as a consequence of a type-case) is passed-through to all equivalent sub-expressions.However, this is sound only if all evaluations of equivalent subexpressions return results that have the same types.While this is true for pure expressions, this can be invalidated by the presence of side-effects.In Section 7 we suggest some research directions on how to modify the equivalence relation of Definition 3.1 to make our system work in the presence of side-effects.Nevertheless, the work presented here is closer to be adapted/adaptable to pure functional languages such as Erlang and Elixir, than to languages such as JavaScript or Python.
Finally, it may be worth pointing out that our approach works only for strict languages, since it uses a semantic subtyping relation that is unsound for call-by-name evaluation strategies [Petrucciani et al. 2018].

SOURCE LANGUAGE AND TYPE SYSTEM 2.1 Syntax and Semantics
Our core language is fully defined in Figure 1.Expressions are an untyped -calculus with constants , pairs ( , ), pair projections ğ , and type-cases.A type-case ( 0 ∈ ) ? 1 : 2 is a dynamic type test that first evaluates 0 and, then, if 0 reduces to a value , evaluates 1 if has type or 2 otherwise.Type-cases cannot test arbitrary types but just ground types (i.e., types without type variables occurring in them) of the form where the only arrow type that can occur in them is 0 → 1, the type of all functions.This means that type-cases can distinguish functions from other values but they cannot distinguish, say, functions that have type Int→Int from those that do not.Programs are sequences of top-level definitions, ending with an expression that can be seen as the main entry.This notion of program is useful to capture the modularity of our type system.Indeed, top-level definitions are typed sequentially: the type we obtain for a top-level definition is considered definitive and will not be challenged by a later definition.
The reduction semantics for expressions is the one of call-by-value pure -calculus with products and with a type-case expression, together with the context rules that implement a leftmost outermost reduction strategy.We use the standard substitution operation { ′ / } that denotes the capture avoiding substitution of ′ for in , whose definition we recall in Appendix B. The relation ∈ determines whether a value is of a given type or not and holds true if and only if typeof ( ) f , where f is the subtyping relation defined by Castagna and Xu [2011] (we recall its definition in Appendix C).Note that typeof( ) maps every -abstraction to 0 → 1 and, thus, dynamic type tests do not depend on static type inference.This approximation is allowed by the restriction on arrow types in typecases.Finally, the reduction semantics for programs sequentially reduces top-level definitions, together with a context rule that allows reducing the expression of the first definition.

Types
Types are those by Castagna and Xu [2011] who add type variables to the semantic subtyping framework of Frisch et al. [2002Frisch et al. [ , 2008]].
Definition 2.1 (Types).The set of types Types is formed by the terms coinductively produced by the grammar: Types , and that satisfy the following conditions: ( ) every term has a finite number of different sub-terms (regularity) and ( ) every infinite branch of a term contains an infinite number of occurrences of the arrow or product type constructors (contractivity).

We use the abbreviations
, and 1 = def ¬0.Basic types (e.g., Int, Bool) are ranged over by , 0 and 1 respectively denote the empty (that types no value) and top (that types all values) types.Coinduction accounts for recursive types and the condition on infinite branches bars out ill-formed types such as = ( (which does not carry any information about the set denoted by the type) or = ¬ (which cannot represent any set).
For what concerns type variables, we choose not to use type-schemes but rather distinguish two kinds of type variables.Polymorphic type variables ranged over by , are type variables that have been generalized and can therefore be instantiated.In a more traditional presentation, such variables are bound by the ∀ of a type-scheme ; the set of polymorphic variables is V Č .Monomorphic type variables, ranged over by ÿ (with bold font), are variables that are not generalized and therefore cannot be instantiated; the set of monomorphic variables is V ĉ .Types that only contain monomorphic variables are dubbed monomorphic types6 : Our choice of using two disjoint sets for polymorphic and monomorphic type variables, instead of the classical approach of using type schemes ∀ 1 ... Ĥ ., is justified by two reasons.First, type schemes are expected to be equivalent modulo -renaming.In our case however, we do not want polymorphic type variables to be freely renamed because of the use, in the algorithmic type system of Section 3, of external annotations containing explicit substitutions over some polymorphic type variables of the context.Secondly, introducing type schemes would require redefining many of the usual set-theoretic type-related definitions, such as the subtyping relation f, and the type operators for application • and projections ğ .Instead, we obtain a more streamlined theory by making subtyping and these operators ignore whether a variable is polymorphic or monomorphic in the current context, and by explicitly performing instantiations in the type system when required.
The subtyping relation for these types, noted f, is the one defined by Castagna and Xu [2011], to which the reader may refer for the formal definition (cf.Appendix C).For this presentation, it suffices to consider that ground types (i.e., types with no variables) are interpreted as sets of values that have that type, and that subtyping is set containment (i.e., a type is a subtype of a type if and only if contains all the values of type ).In particular, → contains all -abstractions that when applied to a value of type , if their computation terminates, then they return a result of type (e.g., 0 → 1 is the set of all functions and 1 → 0 is the set of functions that diverge on every argument).Type connectives (i.e., union, intersection, negation) are interpreted as the corresponding settheoretic operators.For what concerns non-ground types (i.e., types with variables occurring in them) all the reader needs to know for this work is that the subtyping relation of Castagna and Xu [2011] is preserved by type-substitutions.Namely, if f , then f for every type-substitution .We use ≃ to denote the symmetric closure of f, thus ≃ (read, is equivalent to ) means that and denote the same set of values and, as such, they are semantically the same type.

Type System
Our type system is given in full in Figure 2. The typing rules for expressions are, to some extent, the usual ones., : u ¢ : ¢ : ¢ : [f] ¢ : to have either the type or its negation, in which case the corresponding branch is typed.These rules work together with Rule [(], which we describe in detail next.At first sight, the formulation of rule [(] seems odd, since the ( connector does not appear in it.To understand it, consider the classic union elimination rule by MacQueen et al. [1986]: Rule [(E] types an expression that contains occurrences of an expression ′ that has a union type 1 ( 2 ; the rule substitutes in this expression some occurrences of ′ by the variable yielding an expression , and then types first under the hypothesis that has type 1 and then under the hypothesis that has type 2 .If both succeed, then the common type is returned for the expression at issue.As shown by Castagna et al. [2022b], this rule, together with the rules for type-cases, allows the system to perform occurrence typing.For instance, consider the expression ( ∈Int) ?( ) + 1 : false, in the context where has type Any→Any and is of type Any.This expression can be typed thanks to the rule [(E], by considering the sub-expression .This subexpression has type Any, which can be seen as the union type Any ≃ Int(¬Int.We can then replace for and type, using [∈ 1 ], the expression ( ∈Int) ?+ 1 : false, with : Int.This yields a type Int (rule [∈ 1 ] ignores the second branch) and by subtyping, the expression has type Int(False.Likewise for the choice : ¬Int, using rule [∈ 2 ] the second branch has type False and therefore Int(False (again via subtyping).The whole expression has thus the desired type Int(False. A key element is that rule [(E] guessed how to split the type Any of into Int ( ¬Int.In a non-polymorphic setting, this is perfectly fine.But in a type-system featuring polymorphism, particular care must be taken when introducing (fresh) type variables.As it is stated, MacQueen et al. 's [(E] rule could choose to split, say, 1 into a union ( ¬ , with a polymorphic type variable.If so, then the rule becomes unsound.As a matter of fact, the premises of the [(E] behave as in rule [→I], in that they introduce in the typing environment a fresh type whose variables must not be instantiated.In our example, however, in one premise, the rule introduces : in the typing environment which can, for instance, be instantiated by the [Inst] rule.In the second premise, it introduces : ¬ which can also be instantiated in a completely different way.In other words, the correlation between the two occurrences of the same variable is lost, which amounts to commuting the (implicit) universal quantification with the ( type connective, yielding a non-prenex polymorphic type (∀ .) ( (∀ .¬).To avoid this unsound situation, we need to ensure that when a type is split between two components of a union, no polymorphic variable is introduced.This is achieved by rule [(] which requires the type of ′ to be split as ≡ ( ' u) ( ( ' ¬u) (here is our hidden union).
The top-level definitions of a program are typed sequentially by two specific rules: [TopLevel-Expr] ¢ : where denotes a generalization, that is a substitution transforming monomorphic variables into polymorphic ones and where # ⇐⇒ def dom( ) ∩ vars() = ∅.
After typing an expression used for a definition, its type is generalized (Rule [Toplevel-Expr]) before being added in the environment (Rule [Toplevel-Let]).Note that this is the only place where generalization takes place: no rule in the type system for expressions (Figure 2) allows the generalization of a type variable.As explained at the beginning of Section 1.2, this is not a limitation, since intersection types are more powerful than HM polymorphism, and top-level generalization is of practical importance since it is necessary to the modularity of type-checking.Nevertheless, the core of our inference system is given only by the rules in Figure 2 for expressions: the above "TopLevel" rules are only useful to inhabit variables of the typing environments used in the rules for expressions, and this makes it possible to close the expressions being typed.For instance, if a typing derivation for an expression is deduced, say, under the hypothesis : → (with polymorphic), then it is possible to obtain a closed program by inhabiting by a definition like let = .;... ; .This is the reason why, henceforth, we mainly focus on the typing of expressions.
The type system is sound (all proofs for this work are given in Appendix I): Theorem 2.2 (Soundess).If ∅ ¢ Pr : , then either diverges or Pr with ∈ .

ALGORITHMIC SYSTEM
As discussed in the introduction, the declarative type system is not syntax directed and some rules are not analytic.In order to make it algorithmic, we first introduce in Section 3.1 a canonical form for expressions that adds syntactic constructions (bindings) to indicate when to apply the union elimination rule and on which sub-expression.Then, in Section 3.2, we define a fully algorithmic type system that takes a canonical form together with an annotation and produces a type.
3.1 MSC Forms 3.1.1Canonical Forms.The [(] rule is not syntax directed since it can be applied on any expression and can split the type of any of its subexpressions.If we want an algorithmic type system, we need a syntactic way to determine when to apply this rule, and which subexpression to split.In order to achieve this, we represent our terms with a syntax called Maximal Sharing Canonical Form (MSC Form) introduced by Castagna et al. [2022b].Let us start by defining the canonical forms, which are expressions produced by the following grammar: Canonical forms, ranged over by , are binding variables (noted x, y, or z) possibly preceded by a list of definitions (from binding variables to atoms).Atoms are either a variable from a -abstraction (noted , , or ), or a constant, or a -abstraction whose body is a canonical form, or any other expression in which all proper sub-expressions are binding variables.An expression in canonical form without any free binding variable can be transformed into an expression of the source language using the unwinding operator +., that basically inlines bindings: +bind x = in , = + ,{+ ,/x} (see Appendix E.1 for the full definition).The inverse direction, that is, producing from a source language expression a canonical form that unwinds to it, is straightforward (see Appendix E.2). However for each expression of the source language there are several canonical forms that unwind to it.For our algorithmic type system we need to associate each source language expression to a unique canonical form, as we define next.
3.1.2Maximal Sharing Canonical Forms.We define a congruence on canonical forms and atoms: Definition 3.1 (Canonical eqivalence).We denote by ≡ ċ the smallest congruence on canonical forms and atoms that is closed by -conversion and such that bind To infer types for the source language, we single out canonical forms satisfying three properties: Definition 3.2 (MSC Forms).A maximal sharing canonical form (abbreviated as MSC-form) is (any canonical form -equivalent to) a canonical form such that: MSC-forms are defined modulo -conversion. 7The first condition states that distinct variables denote different definitions, that is, it enforces the maximal sharing of common sub-expressions.The second condition requires bindings to extrude -abstractions whenever possible.The third condition states that there is no useless bind (bound variables must occur in the body of the binds).
The key property of MSC-forms is that given an expression of the source language, all its MSC-forms (i.e., all MSC-form whose unwinding is ) are equivalent: Proposition 3.3.If 1 and 2 are two MSC-forms and + 1 , ≡ Ă + 2 ,, then 1 ≡ ċ 2 .
We denote the unique MSC-form whose unwinding is by MSC( ).It is easy to transform a canonical form into a MSC-form that has the same unwinding.The reader can refer to Appendix E for a set of rewriting rules implementing this operation.

Algorithmic Typing Rules
MSC-forms tell us when to apply the [(] rule: a term bind x = in means (roughly) that it must be typed by applying the union rule to the expression { /x}.Putting an expression into its MSC-form to type it, thus corresponds to applying the [(] rule on every occurrence of every subexpression of the original expression.This is a step toward a syntax-directed type system.However, there are still two issues to solve before obtaining an algorithmic type system: ( ) rules ['], [Inst], and [f] are still not syntax-directed, and ( ) rules [(], [Inst], [→I], and [f] are not analytic, meaning that some of their premises cannot be deduced just by looking at the conclusion: the [(] rule requires guessing a type decomposition (i.e., the monomorphic type u in the premises), the [Inst] rule requires guessing a substitution, the [→I] rule requires guessing the domain u of the function, and the [f] rule requires guessing the type ′ to subsume to.
The issue of [Inst] and [f] not being syntax directed can be solved by embedding them in some structural rules (in particular, in the rules for destructors).Moreover, as we will see later, the rules in which we embed [f] can be made analytic by using some type operators.As for rule ['], making it syntax-directed, is trickier.Indeed, the usual approach of merging rules [→I] and ['] does not work here, since terms in MSC-forms may hoist a bind definition outside the function where they are used, causing rule ['] to be needed on a term that is not, syntactically, a -abstraction.Lastly, there is no easy way to guess the substitutions used by [Inst] rules, or the domain used in [→I] rules, or the decompositions performed by [(] rules.To tackle these issues, our algorithmic type system will not only take a canonical form as input, but also an annotation that will where ranges over renamings of polymorphic variables, that is, injective substitutions from V Č to V Č , and Σ ranges over instantiations, that is, sets of substitutions from V Č to Types.We chose to keep annotations separate from the terms, instead of embedding them in the canonical forms, since in the latter case it would be more complicated to capture the tree structure of the derivations. The algorithmic system is defined by the rules given in Appendix G. Below we comment the most interesting rules (we just omit the rules for constants, variables and two rules for type-cases).Essentially, there is one typing rule for each annotation, the only exception being the ∅ annotation that is used both in the rule to type constants and in the two rules for variables. [→I-Alg] To type the atom ., the annotation (u, k) provides the domain u of the function, and the annotation k for its body. [→E-Alg] To type an application one must apply an instantiation and a subsumption to both the type of the function and the type of the argument.Instantiations (i.e., Σ 1 and Σ 2 ) are sets of type substitutions; their application to a type is defined as Σ = def Ă ∈Σ .Since they cannot be directly guessed, they are given by the annotation.Subsumption instead is embedded in two type operators.A first operator, dom(), computes the domain of the arrow and is used to check that the application is well-typed.A second type operator, •, computes the type of the result of the application.These type operators are defined as follows: The rules for projections To type a pair (x 1 , x 2 ) it is not necessary to instantiate (x 1 ) or (x 2 ).However, to avoid unwanted correlations, it is necessary to rename the polymorphic type variables of its components.For instance, when typing the pair (x, x) with x : → , it is better to type it with ( → , → ) rather than ( → , → ), since the former type has strictly more instances than the latter. [ To type type-cases, the annotation indicates which of the three rules must be applied (here [∈ 1 ]) and how to instantiate the polymorphic type variables occurring in the type of the tested expression, so that it satisfies the side condition of the applied rule (see also [∈ 2 -Alg] and [0-Alg] in Appendix G). [Bind In rule [Bind 1 -Alg] the annotation indicates to skip the definition of the current binding.This rule is used when the binding variable is not required for typing the body under the current context .For instance, this is the case when x only appears in a branch of a typecase that cannot be taken under .The side condition ∉ prevents a potential unsound name conflict between binding variables: as occurrences of x in denote the x binding variable that is being skipped, having the type of a former binding variable x in our environment when typing would be unsound.
This rule tries to type the bound atom and then decomposes its type according to the annotation.This decomposition corresponds to an application of the [(] rule of the declarative type system with the only difference that the type of the atom is split in several summands by intersecting it with the various u ğ (instead of just two summands as in the rule [(]) whose union covers 1.Finally, two annotations indicate when and how to apply rule ['] to atoms and canonical forms: An expression is typable if and only if its unique (modulo ≡ ċ ) MSC-form is typable, too: Theorem 3.4 (Soundness and Completeness).For every term of the source language It is easy to generate the unique MSC-form associated to a source language expression (cf.Appendix E).Theorem 3.4 states that this MSC-form is typable if and only if is: we reduced the problem of typing to the one of finding an annotation that makes the unique MSC-form of typeable with the algorithmic type system.8 Figure 3 gives an example of an MSC-form and two possible annotations for it.The term " .(∈Int) ?( ) : " (where : ∀ .→ and Final Type: : Int→Int) is put in MSC-form (on the left).In the first annotation, the function is typed with a single annotation (line 3).The interesting part is the annotation of the binding for u (line 5): the corresponding keep annotation represents an application of the union elimination rule on the occurrences of the expression whose type Ā is split into Ā'Int (line 6) and Ā∖Int (line 9).Each subcase is annotated accordingly.Notice in the second subcase that the annotation for v is skip (line 10) which indicates that this particular variable must not be used (as ( ) cannot be typed since in the "else" branch, has type ¬Int).A different annotation, yielding a better type, is the one on the right.This intersection annotation (line 2) separates the domain of the -abstraction into two cases, each typed independently, yielding for the whole function an intersection type.
The example in Figure 3 also shows why the condition of maximal sharing for our forms is necessary, not only for their uniqueness, but also for the completeness of the algorithmic system: if the two occurrences of in " .(∈Int) ?( ) : " were not bound by the same variable (as in the leftmost column of line 5 in Figure 3), viz., if the sharing were not maximal, then it would not be possible to deduce that ( ) is well typed: expects an integer, but without maximal sharing it is not possible to deduce that the occurrence of in the first branch is indeed of type Int.The problem of inferring an annotation for an MSC-form as the above-in particular the rightmost (more precise) annotation in Figure 3-is tackled in the next section.

RECONSTRUCTION
This section describes an algorithm to find an annotation for an expression in MSC-form, such that the pair expression and annotation is typable in the algorithmic system.Though this algorithm is not complete, it is sound and terminating (see Section 4.4 for the formal statements and a discussion about incompleteness).Experimental results are presented in Section 5.
The annotation reconstruction algorithm is composed of two systems of deduction rules: the main reconstruction algorithm (Section 4.2) which produces intermediate annotations containing information about the domains of -abstractions and the type decompositions to use in bindings, and the auxiliary reconstruction algorithm (Section 4.3) which converts these intermediate annotations into annotations for the algorithmic type system, by computing instantiations Σ for destructors.

The Tallying Algorithm
One key ingredient used by the reconstruction algorithm is the tallying algorithm.Roughly, tallying is the equivalent of the unification used in algorithm W [ Damas and Milner 1982], but for a type system with subtyping.The tallying algorithm was introduced by Castagna et al. [2015] to solve the following problem: given a set of pairs {( ğ , ′ ğ )} ğ ∈ą and a set of type variables representing the monomorphic type variables, find all substitutions whose domain is disjoint from (noted #) and that satisfy ∀ ∈ .ğ f ′ ğ .Castagna et al. [2015] show that this problem is decidable and give an algorithm to characterize all solutions.As for unification, for each instance of the tallying problem there is either no solution or several substitutions each of which is a solution of the problem.The difference is that while with unification all solutions are characterized by a principal substitution, with tallying they are characterized by a principal finite set of substitutions.9More precisely, all substitutions that are solutions to a tallying instance are characterized by a principal set Σ of substitutions, such that every ∈ Σ is a solution, and for any solution , we have ∃ 1 ∈Σ.∃ 2 . 2 # and ≃ 2 • 1 , where • denotes the composition of substitutions and ≃ is pointwise type equivalence.
In this work, all tallying instances use a single constraint, and we will note tally({ 1 f 2 }) the set of substitutions Σ characterizing all the solutions of the tallying instance {( 1 , 2 )}, where = V ĉ and thus ∀ ∈ Σ. dom( ) ¦ V Č (we use the symbol f, rather than f to stress that it denotes a constraint to be solved, rather than a pair in the subtyping relation).
The tallying function tally() finds substitutions for polymorphic type variables, but in order to infer the domain of -abstractions, we may need to find substitutions for monomorphic type variables.We thus introduce an additional tallying function, tally_infer({ 1 f 2 }): Definition 4.1.Let Ĕ denote the restriction of the substitution to the domain .We define where fresh( ) denotes the type where polymorphic type variables have been substituted by fresh ones; is a renaming from (vars( 1 ) ∪ vars( 2 )) ∩ V ĉ to fresh polymorphic variables; and is a substitution mapping polymorphic variables appearing in the image of ′ • to fresh monomorphic variables.
In a nutshell, polymorphic type variables in 1 and 2 are refreshed in order to decorrelate them, and monomorphic type variables are generalized using so that tally() is allowed to find solutions for them.Each solution ′ is composed with in order to restore the connection with the initial monomorphic type variables, and the polymorphic type variables in the image of the resulting substitution are transformed into monomorphic ones by composing with it.Finally, the substitution is restricted to V ĉ (i.e., to the domain of ).
For example, an instance such as tally_infer({Int' → Int' f Ā → }) can be generated during reconstruction, when a function of type Int' → Int' is applied to an argument of type Ā, but the on the right-hand of f is unrelated to the one on the left-hand side.Decorrelating them yields a unique solution {Ā Ā ′ 'Int}, that is, Ā must be substituted by Ā ′ 'Int in our context for the application to be typeable.

Main Reconstruction Algorithm
The main reconstruction algorithm, defined in this section, infers the domains of -abstractions and the decompositions of types into disjoint unions to use for bindings.It works by successively refining intermediate annotations defined below.These intermediate annotations store information about the domains of -abstractions and the decompositions of bindings.However, the instantiations Σ used to type destructors (i.e., applications, projections, and typecases) in the algorithmic type system are not stored in intermediate annotations, because they might get invalidated as the reconstruction progresses: when new information is found about the domain of a lambda or the decomposition of a binding, the algorithm will retype some intermediate definitions of the MSCform, thus invalidating the instantiations Σ of later definitions.Thus, these instantiations Σ will be recomputed whenever needed, using the auxiliary system (Section 4.3) that converts intermediate annotations into annotations for the algorithmic type system.
Atom and form intermediate annotations are defined by the grammar below: In the following, we use the metavariable to range over both atoms and expressions (i.e., ::= | ).
Similarly, the metavariable h ranges over atom annotations a and form annotations k (i.e., h ::= a | k); while the metavariable H ranges over atom intermediate annotations A and form intermediate annotations Let range over monomorphic substitution, that is, substitutions from V ĉ to monomorphic types, and « range over finite sets of monomorphic substitutions (« ::= { , . . ., }).The main reconstruction algorithm is presented as a deduction rule system, for judgments of the form where R is a result defined as follows: Let us see what each result for ¢ * R ï | H ð means: Ok(H ′ ): the reconstruction was successful and can be typed by the algorithmic type system using the annotation H ′ (after converting it into an annotation h using the auxiliary reconstruction system).This result is terminal (i.e., it is a definitive answer that cannot be further refined).Fail: the reconstruction has failed.The algorithm was not able to find an annotation that makes typable with the algorithmic system.This result is terminal.Subst(«, H 1 , H 2 ): the reconstruction found a set of substitutions « that if applied to may make typable.In practice, for each substitution ∈ «, the reconstruction will be called again on the environment and annotation H 1 .However, this does not necessarily mean that the reconstruction will fail on the current environment : might still be typeable but with a less precise type (e.g., it could yield an arrow type with a smaller domain).Thus, this default case which does not instantiate is also explored, using the annotation H 2 instead of H 1 .Split( ′ , H 1 , H 2 ): the reconstruction found some splits for the variables in dom( ′ ) that if applied to may make typable.In practice, the system generates several new environments: one is obtained by (pointwise) intersecting with ′ and then it is used to retype with the annotation H 1 ; the others are obtained by intersecting with all the possible pointwise negations of ′ and then they are used to retype with the annotation H 2 .Var (x, H 1 , H 2 ): the reconstruction found that in order to type , the definition of the bindabstracted variable x should be typed.Initially, any form or atom is annotated with infer, and this annotation is then refined until it yields a terminal result (i.e., either Ok() or Fail).The rules below are presented by decreasing priority (i.e., the first rule that applies is used).Some rules have been omitted for concision, but the reader can find the full reconstruction system in Appendix H.1.There are two different forms of judgments: We first define rules for the judgment ¢ R for every canonical form and atom.The results of these judgments are not necessarily terminal and, therefore, it may be necessary to call the reconstruction again in order to refine them.This is the purpose of ¢ * R judgments which call repetitively ¢ R judgments when relevant, so that in the end we get a terminal result.Let us first focus on ¢ R judgments. [Ok] If a canonical form or atom is annotated with typ, then reconstruction is finished for , and it is typeable in the current context .The annotation typ is never used on -abstractions and bindings because the system needs to store more information for them.Likewise, if a form or atom is annotated with untyp, then reconstruction is finished for by failing in the current context.
[AxOk] ∈ dom() If a -abstracted variable is in the environment, then it is typeable and thus the algorithm returns Ok(typ).Otherwise, is undefined and Fail is returned.
To type the application x 1 x 2 , we must first ensure that {x 1 , x 2 } ¦ dom().If it is not the case, then the two rules [AppVar ğ ] (for = 1, 2) try to remedy it by returning Var (x ğ , infer, untyp), which is the result that asks the system to try to type the atom bound to x ğ for x ğ ∉ dom().If the attempt is successful, then the algorithm will continue the reconstruction for the application with the annotation infer and x ğ ∈ dom(), otherwise it will continue with the annotation untyp making the reconstruction fail on this application. [AppInfer] If {x 1 , x 2 } ¦ dom(), then the rule [AppInfer] tries to find all instances of the current context in which the application x 1 x 2 is typeable, by subsuming (x 1 ) (the type of the function) to (x 2 )→ (a function type whose domain is the type of the argument).For that, it calls the tallying algorithm which returns a set of substitutions «.Then, Subst(«, typ, untyp) is returned, meaning that this application should be typeable under every instance of the current context (with ∈ «).The default case (i.e., when the current context is unchanged, for example, when « = ∅) cannot be typed, so it is annotated with untyp (see rule [Iterate 2 ] later on).The rules for pairs are similar and have been omitted. [CaseSplit] The key rule for type-cases is [CaseSplit], corresponding to the case where x is in , but with a type that does not allow the selection of a specific branch.Thus, we need to partition the type of x in two, one part being a subtype of and the other a subtype of ¬ .This is achieved by returning Split({(x : )}, infer, infer): this result is backtracked up to the binding of x, where it will split the associated type, accordingly. [CaseThen] x ğ ∉ dom() When the type of x allows the selection of a branch, then either the rule [CaseThen] or the rule [CaseElse] applies.If we are in the case of [CaseThen], that is (x) f , then we have to determine whether we will apply the algorithmic rule [0-Alg] or the algorithmic rule [∈ 1 -Alg].To determine it, the [CaseThen] rule calls tally_infer({(x) f 0}) which returns the set of contexts (for ∈ «) under which the algorithmic rule [0-Alg] is to be applied, that is, the contexts under which the tested expression x has an empty type.The default case, corresponding to the case in which the type of (x) is not guaranteed to be empty and, thus, in which the algorithmic rule [∈ 1 -Alg] must be applied, is annotated with ∈ 1 .This annotation is handled by the rule [CaseVar 1 ] which forces the system to type x 1 , the binding variable associated to the first branch.The case for [CaseElse] and [CaseVar 2 ] is analogous.
We omitted the remaining rules for type-cases since they are straightforward: the rule for x ∉ dom(), which triggers a Var (x, infer, untyp) result; two rules similar to [CaseVar ğ ], but where x ğ ∈ dom(), which simply return Ok(typ). [LambdaInfer] The rules for -abstractions mimic algorithm W. Rule [LambdaInfer] transforms the initial infer annotation into a (ÿ , infer) annotation.As in W, -abstracted variables are initially given a fresh type variable, which will then be substituted as needed while reconstructing the type of the body; here we use a fresh monomorphic variable, but tally_infer() will transform it into a polymorphic-thus, instantiable-one, just for the reconstruction in the body.Rule [Lambda] adds the -abstracted variable to the environment with the type specified in the annotation, recursively calls reconstruction on the body, and reestablishes the variable type annotation on the result.The notation map( ↦ → ( ), R) denotes the result R where has been applied to every annotation . [BindInfer] The [BindInfer] rule transforms an initial infer annotation into a try-skip (infer) annotation which skips the binding and annotates the body with infer.We do not try to type the definition of a binding until it is actually used, because its variable might appear only in unreachable positions (e.g., in an unreachable branch of a type-case).In other words, we implement a lazy typing discipline for bind-abstracted variables.If the variable is used at some point, then an attempt to type it will be initiated by the [BindTrySkip 1 ] rule below: This rule tries to type the body of the binding, starting with the annotation K (initially, infer).If the result is Var (x, K 1 , K 2 ), then it means that the current binding is used in the body and, thus, the system should try to type it.Consequently, the annotation for the current binding is changed into a try-keep (infer, K 1 , K 2 ) so that, at the next iteration, its definition will be reconstructed.
If typing the body of the binding yields a result different from Var (x, K 1 , K 2 ), then this result is just propagated as in [Lambda] (the corresponding rules have been omitted).
As expected, if the current annotation for the binding is a try-keep (A, K 1 , K 2 ), then the system tries to reconstruct the annotation for the definition.If it succeeds, then it becomes possible to type the definition and to continue the reconstruction of the body using K 1 .This is what [BindTryKeep 1 ] does by changing the current annotation to keep (A ′ , {(1, K 1 )}, ∅) (more details below).If the reconstruction of the definition fails (rule [BindTryKeep 2 ]), then we have no choice but to skip this definition and use the default annotation K 2 to type the body.
In an annotation keep (A, S, S ′ ) for the binding of a variable x, A is the annotation for typing the definition of x, while the two other arguments describe the type decomposition to use for x and, for each part of the decomposition, the annotation to use for the body.More precisely, S contains the parts of the type decomposition that have yet to be explored, and S ′ contains the parts that have already been fully explored.In particular, the annotation keep (A ′ , {(1, K 1 )}, ∅) used in rule [BindTryKeep 1 ] means that the type of the definition does not need to be partitioned: there is only one part, covering 1, associated with an annotation K 1 for typing the body. [BindOk] If all the parts of the type decomposition have already been explored (i.e., ∅ in the annotation in the rule above), then the reconstruction is successful.Otherwise, the following rules are applied: In both rules, the definition of the binding is typed using the annotation A. For that, it is first converted into an annotation a of the algorithmic type system, using the deduction rules for the judgment ¢ P ï | Að ⇒ a, defined in Section 4.3.Then, the type obtained for the definition is intersected with one of the parts of the type decomposition, according to the second argument of the keep() annotation (i.e., {(u, K)} ∪ S in both rules), and the corresponding annotation for the body is reconstructed recursively.Note that, since split annotations are sets, then the order in which the parts are explored is arbitrary.
The rule [BindKeep 1 ] for an annotation keep (A, S, S ′ ) is responsible for moving a branch from S to S ′ when the result for the branch is Ok().If instead the reconstruction of the body requires to further split the type of x, then the rule [BindKeep 2 ] splits the current branch into two branches.However, before exploring these two branches, some information about the split needs to be propagated, to ensure that when a split is explored, it is under a context as precise as possible.
Let us explain this by an example.Assume we have a polymorphic primitive function id of type → and an initial environment = { : Bool}.We want to type the following canonical form, and deduce for it the type True (since x and y are always bound to the same value): bind x = in bind y = id x in bind z = (y∈True) ?x : true in z At some point, the partition associated to y will change from {1} to {True, ¬True} because of the type-case (rule [CaseSplit]).However, if the case corresponding to (y : True) is immediately explored, it will yield for the body the type Bool, because x still has the type Bool in the environment.In order to obtain the more precise type True, we must deduce, before exploring the case (y : True), that when id x (the definition of y) has type True, then x also has type True.Knowing that, the type of x should be split accordingly into {True, ¬True}.
This mechanism of backward propagation of splits is initiated in the [BindKeep 2 ] rule with the two premises ¢ E ( : ¬(u ' ′ (x))) ⇒ L 1 and ¢ E ( : ¬(u ∖ ′ (x))) ⇒ L 2 .This auxiliary judgment ¢ E ( : u) ⇒ L , defined in Appendix H.3, can be read as follows: refining the current environment with one of the ′ ∈ L ensures that the atom will have type u.The refinements we obtain are stored in the annotation of the binding, using an annotation propagate (A, L , S, S ′ ).This annotation is handled by two other rules (omitted here) whose role is to propagate these refinements one after the other using successive Split( ′ , K 1 , K 2 ) results (with ′ ∈ L ), before finally restoring a keep (A, S, S ′ ) annotation. [InterEmpty] )), R) Intersection annotations are introduced by the ¢ * R judgments defined below.In an intersection annotation ( , ′ ), the annotations in ′ are fully processed (i.e., the associated reconstruction returned Ok()), while the annotations in are not: they still have to be refined one after the other (rule [Inter 3 ]).If one of them becomes fully processed, it is moved in ′ (rule [Inter 1 ]).Conversely, if one of them fails, it is removed (rule [Inter 2 ]).The process stops when is empty: then, the reconstruction fails if ′ is empty (rule [InterEmpty]), and succeed otherwise (rule [InterOk]).Finally, we formalize the rules for the judgments ¢ * R .As said earlier, the purpose of ¢ * R is to repeatedly call ¢ R judgments so that, in the end, we obtain a terminal result.
The iteration continues as long as it yields non-terminal results that are immediately usable, that is, either they return a trivial split (i.e., ′ = ∅) as in rule where ′ ≠ ∅ (i.e., [Iterate 1 ] does not apply), then [Stop] backtracks until ′ becomes empty; likewise if R = Subst({ ğ } ğ ∈ą , H 1 , H 2 ) and ğ for some (i.e.[Iterate 2 ] does not apply), then [Stop] backtracks until it exits the scope of the binders of the variables that make the side condition of [Iterate 2 ] fail.

Auxiliary Reconstruction Algorithm
The auxiliary reconstruction algorithm defined in this section converts an intermediate annotation of the main reconstruction system into an annotation for the algorithmic type system.For that, it needs to retrieve the polymorphic substitutions Σ needed to type the atoms.
Formally, the algorithm takes as input an environment , an atom or canonical form , and an intermediate annotation H , and produces an annotation h for the algorithmic type system.It is presented as a deduction rule system for judgments of the form ¢ P ï | H ð ⇒ h.Some rules have been omitted for concision (they can be found in Appendix H.2): for instance, the rules for constants and axioms are omitted since straightforward, as they just transform an intermediate annotation typ into an annotation ∅ for the algorithmic type system; likewise, the rules for -abstractions and intersections are straightforward and have been omitted, since they just proceed recursively on their children annotations.The most important rule for this system is the one for applications: where refresh( ) returns a renaming from vars( ) ∩ V Č to fresh polymorphic variables.For applications, an annotation of the form @(Σ 1 , Σ 2 ) must be produced.In order to find some instantiations Σ 1 and Σ 2 (for x 1 and x 2 respectively) that make the application typable, the [App] rule solves the tallying instance tally({ 1 1 f 2 2 → }).The purpose of 1 and 2 is to decorrelate type variables in (x 1 ) and in (x 2 ).For instance, assume we want to reconstruct the instantiations for the atom "x x" with (x) = → .The tallying instance tally({ → f ( → ) → }) yields only a very specific, uninteresting solution (i.e., = = . → )10 because of the use of the same type variable on both sides of f.But each occurrence of x has a polymorphic type that can be instantiated independently.Thus, we remove this useless and constraining dependency by refreshing the generic type variables yielding tally({ ′ → ′ f ( → ) → }) which has interesting solutions, in particular { ′ → ; → }.The side-condition Σ ≠ ∅ ensures that the tallying instance has at least one solution (otherwise the annotation produced would be invalid).The rules for projections, pairs, and type-cases are similar and, thus, omitted. [BindKeep] (where ( * ) is ğ ∈ą u ğ ≃ 1).The rule [BindKeep] takes as input an intermediate annotation keep (A, S, S ′ ), with S = ∅, since all branches must have been fully explored by the main reconstruction algorithm.The rule recursively transforms the intermediate annotation A for the definition into an annotation a for the algorithmic type system, and uses it to type .It can then update the environment and proceed recursively on the body , for each branch in S ′ .

Properties of the Reconstruction Algorithm
As recalled at the beginning of the section, reconstruction is sound, terminating, but incomplete.The incompleteness of the reconstruction algorithm is inherent to our system and derives from the lack of principal typing.A simple example is the curried function map defined in the third row of Table 1 in the next section.Our reconstruction deduces for it the type ( → ) → [ * ] → [ * ] (actually, a slightly more precise type), where [ * ] is the type of the lists of elements of type .This states that an application of map yields a function that maps lists of 's into lists of 's.But for every natural number , the declarative system can also deduce that the result maps lists of 's of length into lists of 's of the same length .Our algorithm can check each of these types, but none of them can be deduced from the type reconstructed by the algorithm.And since we do not have dependent types or infinite intersections, then the declarative system cannot have a principal type expressing all these different derivations.In other terms, incompleteness stems from the fact that the declarative system can use all the infinitely many decompositions of unions in the union elimination rule, and the infinitely many decompositions of the domain of a function when reconstructing its type as an intersection of arrows.The algorithmic counterpart of this, is that there are infinitely many annotations that the algorithmic system can use to type these expressions and that these infinite choices cannot be summarized by a notion of principal annotation: the reconstruction chooses one particular annotation, and therefore it will miss some solutions.
There is a second source of incompleteness for reconstruction, which is not inherent to the system, but a design choice, instead: the fact that reconstruction does not perform the so-called "expansion" of intersection types.This is shown by the rule [App] in Section 4.3, where tally is applied without expanding the types in the constraint (e.g., if tally({ 1 1 f 2 2 → }) fails we can expand the type of the function and try tally({ 1 1 ' 1 3 f 2 2 → }), and so on and so forth by alternating expansions on the function and on the argument types: see [Castagna et al. web-based interactive prototype hosted at https://www.cduce.org/dynlang.The web version is compiled to JavaScript using js_of_ocaml [Ocsigen], and is about 8 times slower than the native version.
Another source of inefficiency comes from the type decompositions performed after each binding.Although these type decompositions are usually small (e.g., the type of a binding is seldom split in more than two parts), it becomes an issue when typing large expressions with multiple type-cases.For instance, a preliminary and unoptimized implementation of the reconstruction algorithm took about 40 seconds to type the bal(ance) function used in the module Map of the OCaml standard library, that contains 6 different pattern matches and 4 type-cases [OCaml 2023].Adding a simple memoization mechanism that prevents the reconstruction from retyping an atom several times for equivalent contexts, decreased the inference time down to 4 seconds.
While these simple optimizations significantly improve performance, they are still far from what would be considered acceptable for real applications.To be used in mainstream languages, the type system will have to be adapted and restricted so as to ensure better and uniform performance.To this purpose, we believe that some more language-oriented optimization techniques could be of help.An example is what the development team of Luau [Luau] did on the occasion of its recent switch to semantic subtyping [Jeffrey 2022].The developers did this switch by implementing a two-phase approach: first, a sound syntactic system, fast but imprecise, is used to try to prove subtyping, and only if it fails, the computationally expensive semantic subtyping inference is used.We think not only that such a staged approach could be applied in our case, but also that the partial results of the first phase could be used to improve the performance of the later phases, as in the case of the let rec, where knowing the arity of the defined function improves the performance of the reconstruction.This could be further coupled with slicing, meaning that our type reconstruction could be applied to very delimited regions that would bound the possibility of backtracking.These techniques are language-dependent, and quite different from the algorithmic aspects developed here, though they will completely rely on it.We plan to explore them in future work.

RELATED WORK
This work can be seen as a polymorphic extension of [Castagna et al. 2022b] from which it borrows some key notions, such as ( ) the combination of the union elimination rule (from [Barbanera et al. 1995]) with three rules for type-cases, in order to capture the essence of occurrence typing ( [Tobin-Hochstadt and Felleisen 2008]), ( ) the use of MSC forms to drive the application of the union elimination rule, and ( ) the use of annotations in the algorithmic type system.However, the introduction of polymorphic types greatly modifies the meta-theory.Besides its influence on the union elimination rule, the interplay between intersection, union elimination and instantiation suggests a different style of type annotations, to be amenable to type inference.We use external annotations while [Castagna et al. 2022b] annotates terms.Further, the presence of type variables imposes to use tallying in an inference algorithm inspired by W by Damas and Milner [1982] and from [Castagna et al. 2015], where tallying was first introduced to type polymorphic applications.This yields a clear improvement over [Castagna et al. 2022b] which is unable to infer higher-order types for function arguments, while our algorithm is able to do so even for recursive functions.
The use of trees to annotate calculi with full-fledged intersection types is common.In the presence of explicitly-typed overloaded functions, one must be able to precisely describe how the types of nested -abstractions relate to the various "branches" of the outermost function.The work most similar to ours is [Liquori and Ronchi Della Rocca 2007], since the deductions are performed on pairs of marked term and proof term.A marked term is an untyped term where variables are marked with integers and a proof term is a tree that encodes the structure of the typing derivation and relates marks to types.Other approaches, such as [Bono et al. 2008;Ronchi Della Rocca 2002;Wells et al. 2002], duplicate the term typed with an intersection, such that each copy corresponds exactly to one member of the intersection.Lastly, the work of [Wells and Haack 2002] does not duplicate terms but rather decorate -abstractions with a richer concept of branching shape which essentially allows one to give names to the various branches of an overloaded function and to use these names in the annotations of nested -abstraction.Note that none of these works features type reconstruction, which was our main motivation to eschew annotations within terms, since the backtracking nature of our reconstruction would imply rewriting terms over and over.
Inference for ML systems with subtyping, unions, and intersections has been studied in MLsub [Dolan and Mycroft 2017] and extended with richer types and a limited form of negation in MLstruct [Parreaux and Chau 2022].Both works trade expressivity for principality.They define a lattice of types and an algebraic subtyping relation that ensures principality, but forbids the intersection of arrow types.This precludes them from expressing overloaded functions, but allows them to define a principal polymorphic type inference with unions and intersections.We justify our choice of set-theoretic types, with no type principality and a complex inference, by our aim to type dynamic languages, such as Erlang or JavaScript, where overloading plays an important role.We favour the expressivity necessary to type many idioms of these languages, and rely on user-defined annotations when necessary to compensate for the incompleteness of type inference.Lastly, both works implement some form of type simplifications (e.g., Dolan and Mycroft [2017] use automata techniques to simplify types), a problem of practical importance that we did not tackle, yet.Ângelo and Florido [2022] provide a principal type inference for a type system with rank-2 intersection types.In their work, overloaded behaviors are expressible using intersection types, but they are limited by the rank-2 restriction.Union types are not supported, nor are equi-recursive types (actually, it does not feature a general notion of subtyping between two arbitrary types).Their inference does not require backtracking: it generates a set of constraints that are then solved using a set unification algorithm.This approach for inference has some similarities with the one by Castagna et al. [2016] improved and further developed by Petrucciani [2019] in a context with set-theoretic types, where the set unification algorithm is replaced by tallying in the presence of subtyping.However, while [Petrucciani 2019] does support intersection types with no ranking limitation, it is not able to infer intersection types for overloaded functions.Our work aims to improve this aspect, as well as providing a more precise typing of type-cases (occurrence typing).
Work by Oliveira et al. [2016] and Rioux et al. [2023] study disjoint intersection and union types.They allow expressing overloaded behaviors by a general deterministic merge operator.In our work, we do not have a general merge operator: overloaded behaviors only emerge through the use of type-case expressions (or the application of an overloaded function).Our work can be extended with pattern-matching, in which case the first matching branch is selected.This is a different approach than the one used with disjoint intersection types, where branches are disjoint and have no priority and where ambiguous programs are rejected using a notion of mergeability and distinguishability, allowing to define a general merge operator and to support nested composition, which may be useful in some contexts such as compositional programming [Zhang et al. 2021].Jim [2000] presents a polar type system which features intersections and parametric polymorphism.In Jim's type system, quantifiers may appear only in positive positions in types, while intersections may only appear in negative positions.This yields a system that is more expressive than rank-2 intersection types, and therefore more expressive than ML.Furthermore, the system features principal types, and a decidable type inference.Some aspects of this work are similar to ours, in particular the use of MGS, an algorithm to compute the most general solution of a (syntactic) sub-typing problem, that plays the same role as our tallying algorithm.Despite these similarities, the approaches differ in the kind of programs they handle: in [Jim 2000], intersections are only deduced by applying higher-order function parameters to arguments of distinct types within the body of a function, while in our approach, they can also be caused by a type-case.
Finally, set-theoretic types are starting to be integrated into real-world languages, for instance by Schimpf et al. [2023] for Erlang, by Jeffrey [2022] for Luau, and by Castagna et al. [2023a] for Elixir.We believe that, in the future, our work could be used in these systems in order to benefit from a more precise typing of type-cases and pattern matching, as well as by providing an optional type inference that can be used in conjunction with explicit type annotations.

CONCLUSION
This work aims at providing a formal and expressive type system for dynamic languages, where type-cases can be used to give functions an overloaded behavior.It features a type inference that mixes both parametric polymorphism (for modularity) and intersection polymorphism (to capture overloaded behaviors).In that sense, our work is more than a simple study on typability: as a matter of fact, monomorphic intersection and union types are sufficient to type a closed program where all function applications are known (cf., Section 1.2), but this would be bad from a language design point of view, and it is the reason why people program using ML-style programming languages rather than intersection based ones.Separate compilation and modular definitions are requirements of any reasonable programming language.The essence of this work is thus to challenge the limits of how much precision one can obtain (through intersection types)-ideally precise enough to type idioms of dynamic languages-while preserving modularity (thanks to let-polymorphism).
While we believe our work to be an important step towards a better static typing of dynamic languages, several key features are still missing.First, the presence of side effects may invalidate our approach: if the [(] rule in Figure 2 is applied to two different occurrences of an expression ′ that is not pure, then the rule may type an expression that yields a run-time type error.This can be seen on the algorithmic system, where the transformation into an MSC-form binds the two occurrences of ′ to the same variable, thus wrongly assuming that they both yield the same result.Strictly speaking, our algorithmic approach does not require expressions to be pure; it just needs that when two occurrences of an expression may produce two distinct values that may change the result of a dynamic test, then these two occurrences must be bound by two different binds.Having only pure expressions is a straightforward way to satisfy this property.Having each subexpression bound to a distinct variable (i.e., no sharing, that is, a less precise system, in which the union rule is never used) is a way to retain safety in the presence of side-effects.But between these two extrema, there is a whole palette of less coarse solutions that make it possible to apply our approach in the presence of side-effects, and that we plan to study in future work.This poses two main challenges: ( ) how to separate problematic expressions from non-problematic ones (e.g., a gen_id: Unit→Int function performs side-effects, but if its result is tested only against Int, then it is sound to have all occurrences of gen_id bound by the same bind during typing) which, in terms of the type system, corresponds to characterize a class of subexpressions ′ that can be safely used in rule [(]; and ( ) how to do so before our type inference, at a point when type information is not available, yet.Second, while the performance of our prototype is reasonable, it can certainly be improved by using more sophisticated implementations techniques and heuristics on the lines we outlined at the end of Section 5.
Third, the interactions between code that is exported and code that is local must be better studied and understood: using intersection for local polymorphic functions and generalization for global ones, may not always be entirely satisfactory since the types of the global functions may be "polluted" by the types of the local applications, yielding less a precise reconstruction for the former.One solution can be to hoist the definition of polymorphic functions at toplevel whenever possible.
Lastly, an important future work is the support of row-polymorphism: while records can be easily added to the present work, the precise typing of functions operating on records requires rowpolymorphism.This is especially important for dynamic languages where records are seamlessly used to encode both objects and dictionaries.A first step in that direction may be to integrate the work by Castagna [2023b], which unifies dictionaries and records.

Fig. 1 .
Fig. 1.Syntax and semantics of the source language Constants and variables are typed by the corresponding axioms [Const] and [Ax].The arrow and product constructors have introduction and elimination rules.Notably, in the case of rule [→I] the type of the argument is monomorphic.The rules for intersection ([']) and subtyping ([f]) are the classical ones, and so is the rule for instantiation ([Inst]) where denotes a substitution from polymorphic variables to types.The type-case construction is handled by three rules: [0]; [∈ 1 ]; [∈ 2 ] .Rule [0] handles the case where the tested expression is known to have the empty type.The other two are symmetric and handle the case when the tested expression is known Polymorphic Type Inference for Dynamic Languages 40 ( ) indicate when to apply an intersection, and ( ) indicate which type decomposition (for [(] rules) and which type substitutions (for [Inst] rules) to use.Formally, our algorithmic system uses judgements of the form ¢ A [ | k] : for a canonical form , and ¢ A [ | a] : for an atom where k and a are respectively form annotations and atom annotations defined as follows: Atom annots a ::= ∅ | (u, k) | ( , ) | @(Σ, Σ) | (Σ) | 0(Σ) | ∈ 1 (Σ) | ∈ 2 (Σ) | ({a, ... , a}) Form annots k ::= | keep (a, {(u, k), . . ., (u, k)}) | skip k | ({k, . . ., k})
Proc.ACM Program.Lang., Vol. 8, No. POPL, Article 40.Publication date: January 2024.Polymorphic Type Inference for Dynamic Languages 40:29 [Iterate 1 ], or they return substitutions that do not affect the current environment (i.e., ğ #) as in rule [Iterate 2 ].For the latter rule, the iteration may need to introduce an intersection annotation (useless when is empty) in order to explore all the cases of a Subst({ ğ } ğ ∈ą , H 1 , H 2 ) result (where H is the intermediate annotation H in which the substitution has been applied recursively to every type in it).An important special case of the [Iterate 2 ] rule is when = ∅: in that case the iteration continues by trying to type with the default annotation H 2 and the current environment .For instance, this special case triggers a [CaseVar 1 ] after a [CaseThen] and a [CaseVar 2 ] after a[CaseElse].If the result is already terminal or if it is not immediately usable, then it is directly returned: