[go: up one dir, main page]

Academia.eduAcademia.edu
Precise Explanation of Success Typing Errors ✝ Josep Silva Konstantinos Sagonas Department of Information Technology Uppsala University, Sweden Department of Information Systems and Computation Universitat Politècnica de València, Spain tjsilva,stamarit✉@dsic.upv.es kostis@it.uu.se Abstract Nowadays, many dynamic languages come with (some sort of) type inference in order to detect type errors statically. Often, in order not to unnecessarily reject programs which are allowed under a dynamic type discipline, their type inference algorithms are based on non-standard (i.e., not unification based) type inference algorithms. Instead, they employ aggressive forwards and backwards propagation of subtype constraints. Although such analyses are effective in locating actual programming errors, the errors they report are often extremely difficult for programmers to follow and convince themselves of their validity. We have observed this phenomenon in the context of Erlang: for a number of years now its implementation comes with a static analysis tool called Dialyzer which, among other software discrepancies, detects definite type errors (i.e., code points that will result in a runtime error if executed) by inferring success typings. In this work, we extend the analysis that infers success typings, with infrastructure that maintains additional information that can be used to provide precise (i.e., minimal) explanations about the cause of a discrepancy reported by Dialyzer using program slicing. We have implemented the techniques we describe in a publicly available development branch of Dialyzer. Categories and Subject Descriptors F.3.3 [Logics and Meanings of Programs]: Studies of Program Constructs General Terms Algorithms, Languages, Theory Keywords Type inference, program slicing, Erlang 1. Introduction Dynamically typed languages provide flexibility to programmers since they allow program variables to refer to values of different types at different points during program execution. Moreover, the compilers of these languages do not require programmers to write ✝ Work partially supported by the EU IST-2011-287510 project RELEASE and by the Spanish Ministerio de Economı́a y Competitividad (Secretarı́a de Estado de Investigación, Desarrollo e Innovación) under grant TIN200806622-C03-02 and by the Generalitat Valenciana under grant PROMETEO/2011/052. Salvador Tamarit was partially supported by the Spanish MICINN under FPI grant BES-2009-015019. Most of this work was done during a three month visit of the second and third authors to Uppsala. Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. PEPM’13, January 21–22, 2013, Rome, Italy. Copyright c 2013 ACM 978-1-4503-1842-6/13/01. . . $10.00 Salvador Tamarit explicit type annotations on expressions, or to provide type signatures for functions. This ability to develop programs rapidly is currently exploited by many modern dynamic languages used in industry but it comes with a cost: many errors that would be caught by a static type system are detected only at runtime, potentially making programs written in dynamic languages less reliable. To ameliorate the situation, many dynamic languages nowadays come with various sorts of type inferencing approaches and tools, aiming to detect most type errors statically rather than dynamically [2, 5, 7]. Such tools allow these languages to combine the expressivity and flexibility of dynamic typing with the robustness of static typing. Given a program P, we consider the type of an expression as the (possibly infinite) set of values to which this expression can be evaluated using P. Given two expressions e1 , e2 , such that e1 is a subexpression of e2 , we say that e1 produces a type error if the type of e1 is τ1 , the type expected from e1 by e2 is τ2 , and τ1 ❳ τ2 ✏ ❍. Therefore, in this work the term type error refers to a specific point in the source code of the program. Example 1. Assume that the following three Erlang functions appear in a file ex1.erl (for the time being ignore the boxes). 1 2 3 4 5 6 main( A ) -> X = f(A) , case X of 0 -> g(X) ; -> g(X-1) end. 8 f(X) -> X+1 . 10 g(42) -> ok . If we analyze this program with the type analysis of Dialyzer1 we get the following warning pointing out a specific type error: ex1.erl:4: The call g(X::0) will never return since it differs in the 1st argument from the success typing arguments: (42). Because Dialyzer performs an analysis that is sound for defect detection (i.e., it produces no false positives), rather than being sound for correctness, we can be sure that the call g(X) at line 4 will never succeed for X ✏ 0. Although the type analysis has very fine-grained type information (note that it employs singleton types instead of collapsing 0 and 42 to integer), Dialyzer does not provide any explanation about why this call will not succeed. Of course, in such small programs it is easy to see why, but in big programs this information is often insufficient and the programmer must track both forwards and backwards the values that manifest an error. By using program slicing [14, 16], and taking g(X) in line 4 as the slicing criterion, we would get the portion of the program that has been boxed in the source code. Note that X at line 4 controldepends on the pattern and the variable of the case expression. Also, it data-depends on the definition of X in line 2. This definition, X = f(A), in turn depends on A and the function call. Thus, the 1 Dialyzer [9] is arguably the most advanced static analysis tool developed for Erlang; it is part of Erlang/OTP and is heavily used by Erlang projects. slice also includes the function definition because it is needed to determine the value of X in line 2. Even though this program slice is complete (it includes the cause of the type error), it contains irrelevant information as it can be seen in the slice shown below: 1 2 3 4 5 6 main(A) -> X = f(A), case X of 0 -> g(X) ; -> g(X-1) end. 8 f(X) -> X+1. 10 g(42) -> ok. where the cause of the type error has been boxed in the source code. Basically, variable X will always have the value 0 due to the pattern but the only value in the domain of the success typing of g is 42. A reader not familiar with program slicing might think that an easy way of providing a minimal slice would be to restrict the dependencies by only considering some subset of control or data dependencies instead of all of them. However, explaining a type error for some program could require to ignore some dependencies only in some cases but consider them in others. Let us see this with another example. Example 2. Consider the following Erlang program (once again, initially ignore the boxes): 1 2 3 4 5 6 7 8 main( A ) -> X = f(A), Y = g(A,0) , Z = f(Y) , case Z of 0 -> g(X, A ); -> g(X-1,0) end. 10 11 f(0) -> 0 ; f(1) -> 1 . 13 g(1,0) -> 0 . On this program, Dialyzer will report the following warning: ex2.erl:6: The call g(X::0⑤1,A::1) will never return since it differs in the 2nd argument from the success typing arguments: (1,0). Dialyzer’s analysis has inferred that the call to function g in line 6 will be either g(0,1) or g(1,1). But the only success typing arguments for this function are ♣1, 0q, thus the call will always fail. Here again, this information is useful but insufficient, because we do not know what part of the source code produces this problem. With program slicing and taking A in line 6 as the slicing criterion, we get the portion of the program that has been boxed in the code. But the cause of the type error could be explained as shown below: 1 2 3 4 5 6 7 8 main(A) -> X = f(A), Y = g ( A ,0), Z = f(Y), case X of 0 -> g (X, A ); -> g(X-1,0) end. 10 11 f(0) -> 0; f(1) -> 1. 13 g ( 1 , 0 ) -> 0. Observe how the type error gets produced: The argument of main (variable A) could initially have any value. However, after the call to f, its only possible values are 0 or 1 (otherwise, f(A) would not have succeeded). Similarly, after the call to g(A,0), the only possible value of A is 1. Note that this call is the cause of the type error, and this is a use of A, not a definition. Therefore, even uses of variables can restrict their values resulting in success typing errors. In this work we describe a technique to automatically compute program slices that minimally explain success typing errors. Contrary to previous approaches [2, 7], we advocate for a parameterized technique where one of its inputs is the particular type error we want to explain. This means that the explanation of each different Dialyzer warning is computed differently. Roughly, our technique: (i) Analyzes the source code to automatically compute a collection of type constraints that must be satisfied. This is done by an instrumentation of a constraint generation system based on subtype constraints [10] that is able to propagate source position information during the analysis and attach these positions to the constraints. (ii) Uses a constraint solver to automatically find inconsistencies in the subtype constraints. When an inconsistency is found, all source positions needed to produce the inconsistency are collected by the constraint solver. (iii) Post-processes each particular inconsistency with a final analysis that automatically determines the exact source positions that are needed to explain the particular type error. All three phases are completely automatic. Note that the parameterization is done in the third phase that performs a post-processing for each type error detected during the second phase. We present our formalization and our implementation for the functional language Erlang. The implementation has been performed in an experimental version of Dialyzer explaining errors using program slicing and its implementation is publicly available. In summary, the contributions of this paper are the following: (i) Instrumentation of a constraint generation system to collect the source positions associated with subtype constraints. (ii) Definition of a constraint solver that is able to collect source positions associated with success typing errors. (iii) Definition of a parameterizable slicer for Core Erlang based on source positions associated with these errors. (iv) A prototype implementation in an experimental branch of Dialyzer, a widely used defect detection tool for Erlang. The rest of the paper is organized as follows. In Section 2 we discuss previous approaches to explaining type errors and how we differ from them. In Section 3 we recall the syntax of Core Erlang and introduce some notation used in the rest of the paper. Section 4 presents the slicing technique we have developed to explain success typing errors; it is divided into three subsections that present a calculus to generate type constraints, a constraint solving system, and a slicing process. We state properties of our technique in Section 5, briefly describe the implementation of the Dialyzer extension in Section 6, and conclude in Section 7. 2. Related Work By now there exist many techniques and implementations devoted to improve the quality of messages provided by debuggers and compilers and explaining type errors. A good representative in this line is Helium [8], a compiler designed especially for learning Haskell. Helium provides error messages that include hints suggesting improvements that are likely to correct the program. It uses heuristics to identify the most likely part of the program that produced the error—often a different place from where it was detected—and it shows specific messages for different errors. Another line of research to explain type errors concentrates on showing to the user the part of the source code that is responsible for the type error using program slicing. Program slicing [14, 16] is a general technique of program analysis and transformation whose main aim is to extract the part of a program (the so-called slice) that influences or is influenced by a given point of interest. Thus, program slicing can be used to obtain the parts of the source code that influence a type error. The first to use program slicing to explain type errors were Tip and Dinesh [17] who adapted program slicing to the context of term rewriting systems. Unfortunately, as shown in Examples 1 and 2, standard program slicing produces slices that are too big, because they are constructed by traversing all data and control dependencies, and many of their components are unnecessary to explain a type error. A different approach for the explanation of types has been developed by Choppella and Haynes [2] based on the use of type constraint graphs, which are graphs able to represent all type constraints in a program. Their approach constructs a graph where each node represents an expression of the program and edges represent (equality) type constraints between expressions. A type error is found when a path in the graph connects two different types, meaning that one expression has been used with different types. Once a type error has been found, it is explained by collecting all expressions in the path between the two conflicting types. Of course, these expressions can be mapped to the source code producing a slice. Even though, this approach is conceptually similar to our work, the processes of finding minimal slices are very different. The minimal slices computed using type constraint graphs correspond to a minimal path in the graph. Computing all minimal slices has an exponential cost in that setting, and thus the same work [2] defines an algorithm to only concentrate in a proper subset. Moreover, in contrast to our approach, all type errors are treated in the same way (processing a path in the graph) with a consequent loss of precision. Haack and Wells were the first to use the term type error slicing in their technique to explain type errors in a reduced subset of ML programs [7]. That work was later extended by Rahli et al. [11] to cover almost all SML. This implied a number of innovations and generalizations that are in part specific for SML and in part applicable to other languages. Even though, they focus on a very different language, their work is probably the closer one to ours. In fact, they used a similar approach to ours to get the variable constraints, but they instrumented a system based on Damas’ type inference algorithm, which is based on unification (i.e., equality) rather than on subtyping constraints. Besides this major technical difference, our final post-processing stage also allows us to identify problems not treated by their system. In particular, we can identify and report other kinds of discrepancies in the code which are related to types but are not proper type errors. For instance, we can detect that a branch in a case expression will never be executed because its associated pattern will never match the argument of the case expression, or it could match, but the previous patterns completely cover the type of the argument. In our setting, these problems can also be explained with the information about source positions collected by the constraint generation system. Stuckey et al. [15] presented the Chameleon Type Debugger that performs a type error slicing very similar to the one by Rahli et al. but for a Haskell-style language (Chameleon) with a different overloading style. There is an interesting idea implemented by Chameleon: producing slices that are the intersection of all slices that explain a type error. These slices are in general neither complete nor minimal, but they often contain the portion of the source code with the highest probability to contain the cause of the error. A complementary approach to constraint-based type error slicing is source-based type error slicing [13]. Instead of generating constraints associated with the source code and producing a minimum set of unsolvable constraints, this approach iteratively replaces any term t in the source code by a term that type checks in any context. If the modified program type checks or produces a different type error, then t does belong to the slice. This process is repeated for all terms until a minimal slice is produced. The work of El Boustani and Hage [4] introduces a method for improving type error messages for generic Java programs by providing better information about type errors to the programmer (with respect to existing compilers such as Eclipse’s or Sun’s). In some sense, we want to do the same (improve Dialyzer’s warnings), but there are important differences. First, they face the problem of parametric polymorphism in the context of the object-oriented language Java. We work with subtype polymorphism on the higherorder functional language Erlang. Moreover, they do not obtain slices of the source code. They just explain errors by improving the messages and the information contained in them. A diploma thesis [6], supervised by the first author of this paper, had as its goal to explain Dialyzer warnings. It is therefore work that is quite similar to ours. However, that approach was not tightly integrated with the constraint generation system or the constraint solver. It basically implemented an extension to Dialyzer that was able to collect the line numbers associated with the constraints produced by Dialyzer. Although useful, line numbers are insufficient and very imprecise (e.g., they cannot usually distinguish between the arguments in a function definition). In our work, we can report the exact (sub)expressions that explain a type error. Other approaches exist that address the problem of explaining type errors. But some of them, e.g., [19, 20], do not produce slices to explain the errors; or the slices produced are not minimal (e.g, [3, 18]). Other important differences are the programming language targeted and the underlying formalism used in the type inference system. Unlike previous works, we focus on explaining inference of success typings which is a constraint-based formalism based on subtyping. This is a context which is significantly different both from unification-based type inferencing algorithms and of type debuggers based on soft typing (such as MrSpidey/MrFlow and the subsequent work for DrScheme [5]). 3. Preliminaries Core Erlang When an Erlang program is compiled with the Erlang/OTP compiler, it is first translated to an intermediate language called Core Erlang [1]. Core Erlang is a functional higher-order language with strict evaluation. In this paper, we will consider the following subset of Core Erlang which is expressive enough to cover a wide variety of real programs: e f gp p g ✏ v ⑤ c♣ en q ⑤ e♣ en q ⑤ f ♣Expressionq ⑤ let v ✏ e1 in e2 ⑤ letrec vn ✏ fn in e ⑤ case e of gpn Ñ en end ♣Functionq ::✏ fun ♣vn q Ñ e end ::✏ p when g ♣Guarded Patternq ♣Patternq ::✏ v ⑤ c♣ pn q ::✏ g1 ♣andalso ⑤ orelseq g2 ⑤ true ⑤ false ⑤ v1 ♣✏ ⑤ ✘ ⑤ → ⑤ ➔q v2 ♣Guardq :: In the following, we will use an to refer to the sequence a1 , . . . , an . An expression can be a variable (variables always start with uppercase letters or ), a data constructor (e.g., a literal, a list or a tuple), an application, a function, a let expression, a letrec expression or a case expression. A pattern in a case expression is represented with a variable or a data constructor together with a guard. Guards are boolean conditions, so they can be logical operations between two guards or comparison operations between two variables; or also the special atoms true and false. In the rest of the paper, we will refer indistinguishably to an Erlang program, and its associated Core Erlang representation. In order to uniquely identify each syntactical construct of a program, in a first step, we label the programs in such a way that each element of the program that can cause a type error (e.g., patterns, guards, expressions, etc.) is assigned an identifier that we call source position or just label. Example 3. This is the labeling of a function: (fun(Xl2 ) -> (case Xl4 of (2l6 when truel7 )l5 -> (case Xl9 of (1l11 when truel12 )l10 -> al13 (2l15 when truel16 )l14 -> bl17 (Yl19 when truel20 )l18 -> Yl21 end)l8 end)l3 end)l1 . Figure 1. Block diagram of the technique Types A type represents a set of possible values, and they are represented by type expressions. Their syntax is as follows: ✏ α ⑤ c♣τn q ⑤ ♣τn q Ñ τ ⑤ τ1 ❨ τ2 ⑤ pτ ♣Typeq ✏ none♣q ⑤ any♣q ⑤ integer♣q ⑤ boolean♣q ⑤ atom♣q ⑤ 42 ⑤ foo ⑤ . . . ♣Primitive Typeq τ :: pτ :: A type expression can be a type variable—represented in the following by Greek letters (e.g., α, β, τ. . .) to distinguish type variables from usual variables—structured types, function types, unions of two types or primitive types. Primitive types include singleton types (e.g., 42 and foo) or finite (e.g., boolean♣q) and infinite (e.g., integer♣q, atom♣q, etc.) unions of sets of Erlang terms. There are also the special types any♣q and none♣q used to denote the set of all Erlang terms and the empty set of terms, respectively. We define a concrete type as a type that does not contain any type variable. Subtyping is represented by set inclusion, i.e., τ1 ❸ τ2 . 4. Explanation of Success Typing Errors In this section we describe our technique to extract the part of a program that minimally explains a success typing error. Its three main phases are depicted in Figure 1 with white blocks: constraint generation, constraint solving and program slicing. These three phases are independent and must be executed sequentially because each phase takes as input the output of the previous. The first phase takes as input a labeled Core Erlang program that can be automatically produced from the Erlang source after a phase of compilation and labeling. This phase automatically infers the type of each expression in the program and produces a collection of type constraints enhanced with additional information about their associated source positions. In the second phase, all constraints are solved identifying the success typing errors and combining the source positions in the constraints to collect those that are the cause of the errors. The third phase analyzes the errors and produces a final slice associated with each error that ensures minimality (redundant positions or different causes of the same type error are reduced to obtain a minimal slice that explains the type error). This phase can produce slices for all type errors, or it can take as input one particular error and produce a specific slice for it. The information provided in the second phase is rich enough as to allow the third phase to explain other kinds of discrepancies in addition to type errors. This means that the tool can detect parts of the code where something fishy may be occurring: for example code which is clearly unreachable or is code that can definitely not execute. The rest of this section explains each phase separately. 4.1 Instrumentation of a Constraint Generation System We start from a calculus [10] that computes the set of type constraints associated with a given (labeled) Core Erlang program. Without labels, each type constraint is a subtype relation of the form τ1 ❸ τ2 where τ1 and τ2 represent the types of some expression in the program. We will use the shorthand τ1 ✏ τ2 to denote the conjunction of constraints τ1 ❸ τ2 and τ2 ❸ τ1 . We will instrument the derivation rules of this calculus to get additional information from the labels. In particular, we will calculate the set of labels that have influenced each type constraint. As a result, the calculus attaches to each inferred type constraint a set of syntactic labels. Therefore, a type constraint will be a tuple formed by a subtype relation as described above, and also a set of labels that have defined this constraint; in other words, those program labels needed to infer each type appearing in the constraint and also the label of the expressions that form the constraint itself. Formally, Definition 1 (Type constraint). Let P be a labeled Core Erlang program. A simple type constraint for P is a pair ♣τl11 ❸ τl22 , Lq where τ1 and τ2 are types, l1 and l2 are labels, and L is a set of labels from P. A composite type constraint for P is a conjunction or disjunction of type constraints. The extended calculus shown in Figure 2 automatically produces the type constraints associated with a Core Erlang program. Each rule in this calculus contains a judgment A ✩ e : τ, L, C that should be read as “given the environment A, the expression e has type τ with associated labels L provided that the type constraint C holds”. The environment A is a set of variable bindings of the form v ÞÑ τ where τ is the type of v. Finally, the set of associated labels L are those needed to assign the type τ to e. Each rule of the calculus is explained below: [VAR]: If we have a variable vl , we can consult the type τ of this variable in the environment A of the assumptions. Then, the set of labels that are assigned to v only contains l, that represents the use of variable v. No constraint is defined in this rule. [STRUCT]: The type of a data constructor cl ♣en q is c♣τn q if the type of all arguments en can be reduced to τn . The labels assigned to the final type τ are the union of all labels needed to explain the types of the arguments (Ln ) and the label l of the data constructor c. The constraint defined is the conjunction of all constraints defined in the arguments. [LET]: The type of the let expression is the same type τ2 of the expression e2 that has been computed by adding to the environment a new binding between variable v and a fresh type variable α. Clearly, α should have the same type as τ1 computed for the let expression e1 . This is represented with a new constraint ♣αlv ✏ τl1 , L1 ❨ tlv ✉q, where the labels of the constraint are lv of v together with the labels L1 needed to compute τ1 . Note that this constraint is necessary to associate the labels lv and l with types α and τ1 , and thus, if the constraint later produces an error, be able to report vlv ✏ el1 as responsible. The labels of the let expression are the labels computed for expression e2 (L2 ). The constraints of this rule are the constraints computed to find the types τ1 and τ2 , together with the new constraint. [LETREC]: This rule is a generalization of the previous rule, but in this case, the rule creates a new constraint for each defined variable. Each constraint states that the type τi , 1 ↕ i ↕ n, of a variable vi is equal to the type inferred τ✶i for the corresponding function fi . The labels associated with the new constraint are those needed to infer type τi (li ) and type τ✶i (Li ). [ABS]: The type of a function is a fresh type variable α. In order to infer the type τ of the output of the function (e), for each parameter of the function ♣vn q, this rule adds to the environment a new binding vn ÞÑ βn where βn are fresh type variables. To ensure that α represents a function, a new constraint ♣αl ✏ ♣βlnn q Ñ τle , L ❨ ln q is defined that makes α be equal ➈ A ❨ tv ÞÑ τ✉ ✩ vl : τ, tl✉, ❍ [VAR] A ✩ en : τn , Ln , Cn ✩ cl ♣en q : c♣τn q, ➈ Ln ❨ tl✉, ➍ Cn A ✩ e1 : τ1 , L1 , C1 A ❨ tv ÞÑ α✉ ✩ e2 : τ2 , L2 , C2 A ✩ let vl ✏ el1 in e2 : τ2 , L2 , ♣αl ✏ τl1 , L1 ❨ tlv ✉q ❫ C1 ❫ C2 [STRUCT] A v [LET] v ❨ tvn ÞÑ αn ✉ ✩ fn : τn , Ln , Cn e : τ, L, C A ✩ letrec ✏ f ln in e : τ, L, ➍♣Cn ❫ ♣αln ✏ τln , Ln ❨ tln ✉qq ❫ C A ❨ tvn ÞÑ βn ✉ ✩ e : τ, L, C ➈ ➈ l l A ✩ ♣fun ♣v n q Ñ e endql : α, L ❨ ln , C ❫ ♣αl ✏ ♣βln q Ñ τl , L ❨ ln q A ✩ e : τ, L, C en : τn , Ln , Cn ➍ l .n l l l l A ✩ ♣e ♣en qq : β, tl✉, C ❫ ♣τ ✏ ♣α n q Ñ α l .0 , L ❨ tl✉q ❫ ♣βl ❸ αl .0 , tl✉q ❫ ♣Cn ❫ ♣τ ln ❸ A ✩ p : τ, L p , C p g : τg , Lg , Cg A ✩ p when gl : τ, L p , ♣true0 ❸ τg l , Lg q ❫ C p ❫ Cg A ✶ vlnn n n e n e n e g A A ✩ ♣case ele n e e [LETREC] ✶ n [ABS] e n e α lne .n, Ln [APP] ❨ tl✉qq [CPAT] g ✩ e : τ, L, Ce A ❨ tv ÞÑ βv ⑤ v P Var♣cpn q✉ ✩ cpn : τpn , Lpn , C pn bn : τbn , Lbn , Cbn ➈ Lb , C ❫ ➎ ♣αl ✏ τblb , Lb q ❫ ♣τl ✏ τ pl p , L ❨ Lp q ❫ C p ❫ Cb l of cplpn Ñ blb n n n n n e n n endq : α, n n n n [CASE] n e Figure 2. Type inference calculus with source positions propagation to a function whose parameters have types βlnn and whose return value is the type of e (τle ). The set of labels defining this constraint is the union of the arguments’ labels ln , and the labels L needed to infer the type of e. ➈ [APP]: The type of an application is a fresh type variable β. This rule defines three new constraints to restrict this type variable: (1) In the first constraint we ensure that the type τ of e is a function whose co-domain is α. (2) In the second constraint we force the type β of the application to be a subtype of α. Finally, (3) the third constraint is really a conjunction of constraints used to ensure that the types of all the arguments of the application (τn ) are subtypes of the corresponding types of the arguments of τ (αn ). Note that we use sub-labels le.n for the arguments and le.0 for the result of the function that defines the type τ. Regarding the constraints, the first one is defined by L needed to infer τ, and l associated with the application. In the second constraint, the label is l because it represents the type of the application. Finally, the constraints for the arguments are labeled with Li , 1 ↕ i ↕ n (needed to infer type τi of the argument) and l defining the application itself. [CPAT]: The resulting type τ is defined by L p , which defines the type of p. A new constraint for the guard ensures that it can be evaluated to true. The constraints defined are C p to infer the type of the pattern and Cg to infer the type of the guard. [CASE]: The type α of a case expression is defined by the union of the labels Lbn needed to infer the types of the results in all branches τbn . We use the notation Var♣cpn q to denote the set of variables appearing in the patterns cpn . Two constraints are generated for each branch. The first constraint restricts the type of α forcing it to be equal to the type of the branch (τbi , 1 ↕ i ↕ n). This constraint is defined by Lbi . The second constraint makes the type τ of e equal to the type of the pattern of this branch τpi . The constraint is defined by L which is defining the type of e and Lpi that defines the type of the pattern pi . Example 4. Consider the code in Example 3. The rules of Figure 2 would infer the following information: ♣♣αl ✏ ♣βl q Ñ γl , tl2 , l13 , l17 , l21 ✉q ❫ ♣βl ✏ 2l , tl4 , l6 ✉q ❫ ♣true0 ❸ truel , tl7 ✉q ❫ ♣γl ✏ δl , tl13 , l17 , l21 ✉qq ❫ ♣♣♣βl ✏ 1l , tl9 , l11 ✉q ❫ ♣true0 ❸ truel , tl12 ✉q ❫ ♣δl ✏ al , tl13 ✉qq ❴♣♣βl ✏ 2l , tl9 , l15 ✉q❫♣true0 ❸ truel , tl16 ✉q❫♣δl ✏ bl , tl17 ✉qq ❴♣♣βl ✏ εl , tl9 , l19 ✉q❫♣true0 ❸ truel , tl20 ✉q❫♣δl ✏ εl , tl21 ✉qqq 1 2 3 7 9 10 9 14 9 18 4 3 5 8 12 8 8 16 20 8 13 17 21 Observe that the type constraint generated contains a disjunction. This happens because one of the case expressions in the function has three branches, thus, we have a constraint generated for each branch. Observe the use of type variables in the constraints. For instance, consider the second line with the constraint ♣βl4 ✏ 2l5 , tl4 , l6 ✉q. It was generated because variable X is assigned to 2 in the case branch; β represents X, and thus they both must have the same type (i.e., 2). In the following we assume the existence of a set TVars that contains all type variables generated for a given program by the type inference calculus. 4.2 Constraint Solver Instrumented for Slicing After having computed all the type constraints associated with a given program, we need a constraint solver to identify subsets of constraints that, taken together, are incompatible. We want these sets of unsolvable constraints to be minimal, so that we can identify the exact parts of the program that are actually needed to cause the type error. In addition, the constraint solver must be able to handle the labels associated with the constraints in order to collect the final set of labels related to the type error. Because we want our final slices to be minimal, we need to provide a formal notion of minimality for type error slices. Definition 2 (Type error slice). Let P be a program. A slice of P is a set of labels identifying program points. Let S be a slice and let C be the type constraint associated with S . If C is unsolvable, then we say that S is a type error slice. A type error slice S with associated type constraint C is minimal if for all slice S ✶ associated type constraint C ✶ , C ✶ is solvable. ⑨ S with Our slices are associated with constraints generated by our calculus in Figure 2. We can ensure minimality by collecting only those labels related to the part of the code that produced the incompatible types, and also the labels needed to produce the association between these types. Let us explain this with an example. Example 5. Consider the following program that, for clarity and simplicity, uses Erlang instead of Core Erlang. At the right of each comment, we explain the success typing of the variables, which is the set of values that avoid a type error. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 main(X,Y) -> f1(X), f2(X), f3(X), g1(Y), g2(Y), g3(Y), X = Y. f1(1) f2(1) f3(1) g1(1) g2(2) g3(4) -> -> -> -> -> -> a; a; a. a; b; d. % % % % % % % % ♣Xq ✏ type♣Y q ✏ any♣q ♣Xq ✏ t1, 2, 3✉ ♣Xq ✏ t1, 2✉ ♣Xq ✏ t1✉ ♣Y q ✏ t1, 2, 4✉ ♣Y q ✏ t2, 4✉ ♣Y q ✏ t4✉ ♣Xq ✏ type♣Y q ✏ none♣q type type type type type type type type f1(2) -> b; f2(2) -> b. f1(3) -> c. g1(2) -> b; g2(4) -> d. g1(4) -> d. s2 = [3,7,8] s3 = [4,6,8] Example 6. Here again, we use Erlang instead of Core Erlang for clarity. The code has comments showing the information contained in Sol. Only expressions that are of interest have been labeled. main(X,Y) -> fl1 (Xl2 ), gl3 (Xl4 ), hl5 (Yl6 ). case Xl7 of 1l8 -> X Yl9 -> a end. s4 = [4,7,8] Our constraint solver is defined in Algorithm 1. Given a type constraint, it produces a solution to it assigning a type to each type variable. In the case that it is unsolvable, it returns a type error by collecting the labels associated with the unsatisfiable constraints. In Algorithm 1, ❑♣E q represents a set of type errors (i.e., no solution exists for the type constraint), where E is a set of unsolvable constraints represented with tuples that include: (i) an unsatisfiable simple type constraint, (ii) those labels of the program that make this constraint unsatisfiable, and (iii) the solution computed so far until the error was found. Essentially, the algorithm starts with a naive solution Sol and it recursively calls function solve to refine this solution until a type error has been found (i.e., it returns ❑♣E q) or until the solution cannot be refined more (i.e., it returns Sol). The data structure Sol is the key of the algorithm. It contains the information that is being updated in every iteration and that represents the solution to the constraints. And it also contains the information related to the type error when it exists. In particular, Sol is a mapping from type variables to a pair that contains their current concrete type, and the history of previous concrete types with the labels that were needed to define these types. Moreover, we include in Sol a special mapping from ❑ to the set E. It represents a set of type errors where E contains unsolvable constraints denoted with tuples as described before. % % % % % % % ♣ q ✏ S ol♣βq ✏ ♣any♣q, ❍q ♣ q ✏ ♣t1, 2, 3✉, rt1, 2, 3✉, L1 sq ♣ q ✏ ♣t1, 2✉, r♣t1, 2✉, L2 q, ♣t1, 2, 3✉, L1 qsq ♣ q ✏ ♣t3✉, r♣t3✉, L3 qsq ♣ q ✏ ♣t1, 2✉, r♣t1, 2✉, L2 q, ♣t1, 2, 3✉, L1 qsq ♣ q ✏ ♣t1✉, r♣t1✉, L4 q, ♣t1, 2✉, L2 q, ♣t1, 2, 3✉, L1 qsq ♣❑q ✏ t♣α ✏ β, tl7 , l9 ✉ ❨ L2 ❨ L3 q✉ S ol α S ol α S ol α S ol β S ol α S ol α S ol fl9 (1l10 ) -> a; fl11 (2l12 ) -> b; fl13 (3l14 ) -> c. gl15 (1l16 ) -> 1; gl17 (2l18 ) -> 2. hl19 (3l20 ) -> 1. where L1 L2 This program is buggy, because it results in a runtime type error: At line 8, the value of X can never match the value of Y, and the program will crash at execution time. If we observe the first line, we see that at the beginning, X and Y had the same types (any♣q). But these types have been gradually restricted by successive calls to functions with these variables as arguments. If we assume that there is only one error in the code, then, this is due to one of two possibilities: either the type of X or the type of Y was too restricted during the execution. If the value of X at line 8 is correct, then lines 6 and 7 are the culprit because they restrict Y too much. In contrast, if the value of Y is correct at line 8, then lines 2, 3 and 4 are the culprit because they restrict X too much. If we only concentrate on the code of function main/1 and ignore the other code, we have the following four minimal slices: s1 = [2,7,8] Initially, all labeled expressions in the program P are mapped to the type any♣q except type variables involved in recursion that are mapped to type none♣q. This allows the constraint solver to address mutually recursive calls [10]. ❑ is initially mapped to ❍. Given a type variable α, Sol♣αq returns from the mapping Sol, a pair with the type associated with α, and the history of types of α (represented in the algorithm with Hisα ). Let us explain the use of Sol with an example. ✏ tl1 , l2 , l9 , l10 , l11 , l12 , l13 , l14 ✉ ✏ tl3 , l4 , l15 , l16 , l17 , l18 ✉ L3 L4 ✏ tl5 , l6 , l19 , l20 ✉ ✏ tl7 , l8 ✉ First, we can assume that type variables α and β represent the type of X and Y respectively. Initially, both X and Y are assigned the type any♣q and have associated an empty set of labels. After the call to f(X), X is assigned the type t1, 2, 3✉ because these are the only values that can lead to success after the call to f(X). This type is stored in the X’s history of types together with the positions L1 that forced this type. After the call to g(X), the type of X is further restricted and thus the new type is t1, 2✉. This type is stored in the X’s history of types with associated positions L2 . Observe that the history also keeps the previously stored type. After the call to h(Y), the type of Y is restricted and thus its new type is t3✉. The use of X in the case expression does not implies any restriction of the type of X. Therefore, Sol remains unchanged. In the first branch of the case expression, the type of X is restricted to t1✉ and it is stored in the history of types as before. Finally, when Y is used as a pattern in the case expression, the equality X ✏ Y is forced. But this equality will never hold because their types are incompatible (i.e., X has type t1, 2✉ and Y has type t3✉). Therefore, a type error is detected at this point. The information recorded for the type error is the unsolvable type constraint (i.e., α ✏ β), and the labels that produced this type error (in this case, those associated with the unsolvable constraint (tl7 , l9 ✉) plus those associated with the type of β (L3 ) plus those associated with the type of α the first time in its history of types that it was incompatible with the type of β (L2 )). Algorithm 1 is based on function solve that is implemented with four rules. The first rule is used when a type error is detected, hence the information of the type error is returned ❑♣E q. The second rule is used for simple type constraints, the third rule is used for conjunctions, and the fourth rule is used for disjunctions. The last three rules implement complex functionality which we explain: • In the case of simple type constraints, three cases are possible: (i) when the constraint is solved we return the solution computed so far. Note that, to know that the constraint is satisfied, we use τ✶i instead of τi because τ✶i contains the solution found (in Sol) to τi in the case that τi is a type variable. (ii) When the constraint is solvable but to be solved it needs to restrict one type of the constraint, then we update (Sol) with the restricted type. In this case, τ1 must be restricted to be a subset of τ2 . And, Algorithm 1 Constraint Solver Input: A type constraint C Output: A solution for C or a type error if C cannot be solved Preconditions: Sol is a mapping from all type variables to a tuple with the type any an empty history of types ( ) return solve Sol ♣ q ❍ ♣q (except recursion type variables that are mapped to type none♣q), and where function solve is defined as: solve ♣❑♣Eq, q ✏ ❑♣Eq ✩ Sol when ♣τ1 ❬ τ2 ✏ τ ✘ none♣q ❫ τ1 ❘ TVarsq ❴ ♣τ1 ❸ τ2 q ✬ ✬ ✬ Solrτ1 ÞÑ ♣τ, ♣♣τ, Lq : His1 qqs when ♣τ1 ❬ τ2 ✏ τ ✘ none♣q ❫ τ1 P TVarsq ✬ ✬ ✬ ✬ ✫ ❑♣t♣τ1 ❸ τ2 , incomp♣τ1 , His2 q ❨ L1 ❨ L, Solq✉ ❨ Sol♣❑qq otherwise l l solve♣Sol, ♣τ11 ❸ τ22 , Lqq ✏ ✧ ✧ ✬ ✬ ♣τi , r♣τi , tli ✉qsq when τi ❘ TVars ❫ L1 ✏ L1 when His1 ✏ ♣♣ , L1 q : q ✬ ✬ where ♣τi , Hisi q ✏ ✬ ✬ Sol♣τi q when τi P TVars ❍ otherwise ✬ ✪ ✶ ✶ ✶ ✶ ✶ ✶ ✶ ✶ ✶ ♣ solve Sol, ♣ solve Sol, ➍ ➎ Cn Cn q q ✏ ✧ ♣ ✧ ♣ ♣ ➍ Sol ➍ when solve conj Sol, ➍ Cn solve Sol✶ , Cn when solve conj Sol, Cn q q ✏ Sol q ✏ Sol ✘ Sol ✶ ÞÑ Sol♣❑q ❨ Errorss when Sols ✘ ❍ ✏ ❑♣ r❑♣❑q ❨ Errorsq when Sols ✏ ❍ Sol✶ Sol ✩ S ✬ ✬ ✬ ✫ Errors where ✬ ✬ ✬ ✪ Sols Sol ✶ ♣❑♣ q q ✏ ❑♣Eq ♣ q ✏ solve conj♣solve♣Sol, C1 q, C2 ❫ . . . ❫ Cn q where ➍ Cn ✏ Con j ♣ q ✏ solve♣Sol, Cq solve conj E , solve conj Sol, Con j solve conj Sol, C ✶ ✏ ✏ ✏ ✏ tsolve ➈ ♣Sol, Ci q ⑤ 1 ↕ i ↕ n✉ E ❑♣ E qPS t ⑤ P ✘ ❑♣ q✉ ♣ q s s S , s Sol, Sols moreover, τ1 must be a type variable. Then, we update Sol with the new type of τ1 . (iii) When the constraint is unsolvable, we return a type error. It is important to remark the way in which the algorithm computes the labels associated with the type error. Essentially, the labels collected are those related to the unsolvable type constraint (L), the labels needed to define the last type computed for τ1 (L1 ), and the first type computed for τ2 that is incompatible with the type of τ1 . This is done with the auxiliary function incomp defined in Figure 3 (incomp♣τ✶1 , His2 q). Example 7. Continuing our example, if we apply Algorithm 1 with the type constraint in Example 4, we get the following solution: • If the constraint is a conjunction, the algorithm solves all con- This is not a type error, but an actual solution that assumes that the first branch of the innermost case expression is executed. A type error was detected in the first branch and stored in Sol associated with ❑. This is done by the algorithm as follows: (1) It starts with a call to solve♣Sol, Conjq being Conj the initial type constraint. Then, the contraints are solved individually. One of them is a disjunction. Therefore, each part of this disjunction (i.e., each branch of the case expression) is evaluated separately. (2) In the second and third parts of the disjunction, solve finds a solution assigning type 2 to type variable ε, and types b and 2 to type variable δ. (3) In the first part of the disjunction, solve evaluates a conjunction, but in this case, it finds an error in the third iteration (when evaluating ♣βl9 ✏ 1l11 q and after having evaluated (βl4 ✏ 2l5 )). Observe in the history of types associated with the type error that β was bound to 2 and later to 1, but these types are incompatible. (4) After solving the disjunction, the different types assigned  to δ in the success branches (b and 2) are joined by function . This type is exactly the same assigned to γ. straints participating in the conjunction with function solve conj. Each time a constraint is solved, the resultant Sol is passed to be used in the next constraint. This process is repeated until Sol cannot change anymore (e.g., until a fix point is reached). • Disjunctions represent the constraints imposed by the branches of a case expression. First, each branch is analyzed separately, and the union of all the solutions obtained for each branch is stored in S . Then, S is divided into two disjoint subsets with the errors found (Errors) and the solutions found (Sols). We have two possibilities: (i) If none of the branches can be solved (Sols ✏ ❍), then, all the type errors (Errors) found in the branches are combined together with the errors (Sol♣❑q) already present in Sol and returned. (ii) If at least one solution exists, then all the solutions computed for each branch are also combined and returned. Inthis case, the combination is done with the auxiliary function defined in Figure 3 that essentially performs two tasks. First, it assigns to each variable the least upper bound ❭ (or supremum) of the types in each branch. For instance, if a type variable α has type 1 in one branch, and has type 2 in other branch, then the final type of α is t1, 2✉. Second, it combines the histories of all branches. This task is done using two functions: Function join builds a single history which contains the combination of all histories of the branches. Function insert his is in charge of orderly inserting these new entries in the previous history of the type variables. Sol ✏ t❑ ÞÑ t♣βl ✏ 1l , tl4 , l6 , l9 , l11 ✉, tβ ÞÑ 2, r♣2, tl4 , l6 ✉qs✉q✉, α ÞÑ ♣ ♣βq Ñ γ, r♣ ♣βq Ñ γ, tl2 , l13 , l17 , l21 ✉ qs q, β ÞÑ ♣2, r♣2, tl4 , l6 ✉qsq, γ ÞÑ ♣2 ❨ b, r♣b, tl17 ✉q, ♣2, tl21 ✉qsq, δ ÞÑ ♣2 ❨ b, r♣b, tl17 ✉q, ♣2, tl21 ✉qsq, ε ÞÑ ♣2, r♣2, tl9 , l15 ✉qsq✉ 9 11 4.3 Producing Slices for Type Errors After the second phase, we have a data structure Sol with information about type errors and also about the solution computed for type variables before2 the detection of the type errors. This infor2 Even though our formalization stops when the first type error is found, it is trivial to extend the algorithm in order to find all type errors and the complete solution. This extended version is the behavior of our implementation. ✩ ✫❍ when His ✏ rs when His ✏ His incomp♣τ, Hisq ✏ L ✪ incomp♣τ, His q when His ✏ His r♣τ , Lqs ❫ τ ❬ τ ✏ none♣q r♣τ , Lqs ❫ τ ❬ τ ✘ none♣q  ♣Sol, Solsq ✏ Solrα ÞÑ ♣τ , His qs for each α in TVars ➜♣Solsq, where ♣τα , Hisα q ✏ Sol♣αq ❫ τ ✏ tτSol ⑤ Sol P Sols ❫ ♣τSol , q ✏ Sol ♣αq✉ ❫ Hisα q ✏ Sol ♣αq✉, rsq, Hisα q His ✏ insert his♣join♣tHisSol ⑤ Sol P Sols ❫ ♣ , HisSol ✧ join♣His , join one♣♣τ, Lq, Hisaccum qq when ♣τ, Lq : His ✏ His join♣His, Hisaccum q ✏ ✶ ❭ ✶ ✶ ✶ ✶ ✶ ✶ ❭ ✶ ✶ ❭ ✶ ❭ ✶ ✶ ✶ ★ ✶ ✶ ✶ ✶ otherwise Hisaccum ♣τ , L q : His ✏ His q ✏ ♣rsτ ❭ τ , L ❨ L q : join one♣♣τ, Lq, His q when otherwise ✧ insert his♣Hisinserted , insert♣♣τ, Lq, Hisqq when ♣τ, Lq : Hisinserted ✏ Hisinserted insert his♣Hisinserted , Hisq ✏ ♣♣ q ✶ ✶ ✶ ✶ ✶ ✶ join one τ, L , His ✶ ✶ otherwise His ✩ His ✬ ✬ ✫ ♣τH , LH q : insert♣♣τ, Lq, His q insert♣♣τ, Lq, Hisq ✏ ♣τ, Lq : His ✬ ✬ ✪ ✶ r♣τ, Lqs ♣ ♣ ♣ q q q ✏ ❫ ✏ ✏ ❫ ❸ ✏ ❫ ❹ when τH , : His✶ His τ τH when τH , LH : His✶ His τ τH when τH , : His✶ His τ τH otherwise Figure 3. Auxiliary functions ♣ q✏ slicingErrors Sol ↕ ♣q sliceError e where Sol ✏ ❑♣Eq ❴ Sol♣❑q ✏ E ePE ✩ ✬ t♣e1♣l, i, τ1 , τ2 q, Lq✉ when ♣ele ♣elnn qql P P and l1 ✏ li ❫ l2 ✏ le .i with 1 ↕ i ↕ n ✬ ✬ ✬ ✬ ✬ ✬ t♣e2♣l, n, τi q, Lq✉ when ♣ele ♣elnn qql P P and li ✏ le ❫ τ j ✏ ♣τm q Ñ τr ❫ m ✘ n ✬ ✬ ✬ ✬ ✬ with ♣i, jq P t♣1, 2q, ♣2, 1q✉ ✫ l l sliceError♣♣τ11 ❸ τ22 , L, Solqq ✏ t♣e3♣l, pk , τ , τ q, Lq✉ when ♣case ele of plpnn Ñ bn endql P P and li ✏ le ❫ l j ✏ lk i j ✬ ✬ ✬ ✬ with 1 ↕ k ↕ n ❫ ♣i, jq P t♣1, 2q, ♣2, 1q✉ ✬ ✬ ✬ ... ... ✬ ✬ ✧ ✬ ✬ τi when τi ❘ TVars ✬ ✪ where τi ✏ ✶ ✶ ✶ ✶ ✶ ✶ ✶ ✶ ✷ ✷ ✶ ✶ ♣ q Sol τi when τi P TVars Figure 4. Algorithm for producing warnings from the unsatisfiable constraints mation can be exploited to detect the different causes of the type errors, and also to produce their associated slices. This process can be done automatically. When function solve detects a type error (i.e., it returns ❑♣E q), we can identify the cause (i.e., the slice) associated with this type error thanks to the information collected. However, as mentioned, the same information also allows Dialyzer to detect and explain why some code is dead or unreachable. Such code is often confusing, it pollutes the source, and can cause maintenance problems. Example 8. Consider the following Erlang code: main(X) -> Y = f(X), case Y of a -> 1; b -> 2; -> 42 end. f(one) -> a; f( ) -> b. This program will never crash at execution time. However, for the current definition of function f, the third branch of the case expression will never be executed. This is due to the fact that the first two branches completely cover all possible values of Y. The presence of such code, although not a programming error, often indicates programmer confusion or misunderstanding of APIs, in case f is a library function or a function from another module. Thanks to the information computed by function solve we can identify problems produced by a bad use of types as the one in the previous example and produce a slice to explain them. Therefore, we have two different methods; one to identify type errors and one to identify type inconsistencies. The former uses the information produced about errors (❑♣E q), and the later uses the information collected about solutions to type variables (Sol). Both methods are formalized in Figures 4 and 5. For the sake of clarity and because both methods are conceptually distinct, we present them separately. In Figure 4, function slicingErrors takes a solution produced with function solve and it extracts all the information related to type errors (E). All type errors are then processed separately with function sliceError. Therefore, function sliceError implements the main functionality of detecting the cause (and producing a slice for it) that produced a given type error. There are many different causes of a type error. In particular, the current version of Dialyzer is able to detect 26 different inconsistencies produced by type errors. Each inconsistency should be reported to the user together with a slice explaining the cause. This is the objective of function sliceError. For simplicity, we have formalized here only three different type errors with their associated slices (that are the second output produced by sliceError): ♣ q • Fun application with arguments e en will never return since it differs in the ist argument from the success typing argu- ♣ q ✏ tsliceSol♣Sol q ⑤ ♣Sol ✏ ❑♣Eq ❫ ♣ , , Sol q P Eq ❴ ♣Sol ✘ ❑♣ q ❫ Sol ✏ Solq✉ ✩ t♣e4♣l, τ q, Lq✉ l when ♣r s♣e1 , e2 qql P P and αl P T Vars and r s♣ , q ❺ τα and r s ❺ τα ✬ α ✬ and L ✏ incomp♣tr s♣ , q, r s✉, His♣αqq ✬ ✬ ✬ ✬ t♣e5♣l, pi , τα ③τLbl , τβ q, when ♣case el of plpn Ñ bn endql P P and αl P T Vars ✬ Le ❨ Li ❨ LLbl q✉ and ❉i P t➋ 1..n✉ and ❉Lbl ❸ t1..i ✁ 1✉ such that βlp➋P T Vars and ✬ ✬ τ ❸ ❫ τγ ❺ tτδ ⑤ k P t1.. j ✁ 1✉ ❫ δlp P T Vars✉ ✉ β ✬ ➋tτγ ⑤ ⑤j Pj PLblLbl❫❫γlpγlp PPTTVars ✬ ✫ where τLbl ✏ tτ➈ Vars✉ and ♣τα , Le q P Hisα and ♣τβ , Li q P Hisβ γ sliceSol♣Solq ✏ and LLbl ✏ tL j ⑤ j P Lbl ❫ γlp P T Vars ❫ ♣ , L j q P His♣γq✉ ✬ ✬ ✬ t♣e6♣l, pi , τα , τβ q, when ♣case el of plpn Ñ bn endql P P and αl P T Vars ✬ ✬ Le ❨ Li ❨ LLbl q✉ and ❉i P t➋ 1..n✉ and ❉Lbl ❸ t1..i ✁ 1✉ such that βlp➋P T Vars and τα ❸ τLbl and ✬ ✬ τ ❸ ❫ τγ ❺ tτδ ⑤ k P t1.. j ✁ 1✉ ❫ δlp P T Vars✉ ✉ β ✬ ➋tτγ ⑤ ⑤j Pj PLblLbl❫❫γlpγlp PPTTVars ✬ where τLbl ✏ tτ➈ Vars✉ and ♣τα , Le q P Hisα and ♣τβ , Li q P Hisβ γ ✬ ✬ and LLbl ✏ tL j ⑤ j P Lbl ❫ γlp P T Vars ❫ ♣ , L j q P His♣γq✉ ✬ ✪ where ♣τχ , Hisχ q ✏ Sol♣χq and P is the program ✶ slicingSol Sol ✶ ✶ 2 e 2 n e i j k j j e n e i j k j j Figure 5. Algorithm for producing warnings from a solution ments: τ✶2 . Represented as e1, this error message identifies a function call where one of the arguments has a type that is not a subtype of the corresponding parameter of the function. This is represented in the first case of function sliceError by iden- tifying a function call ♣ q in the program P—for the sake of simplicity we assume that P is a global variable—where the label of τ1 corresponds to the label of an argument ei , and the label of τ2 corresponds to the correct sub-label of the applied expression e in the same application. The slice associated with this type error is L. Note that we add some information to the error message. This information is the only information needed by a decompiler to identify the parts of the source code that correspond to the slice and to show the corresponding Dialyzer’s error. We do not formalize here how to do this because it is just a generic and simple process of parsing and decompilation. The associated slice L contains all the labels that defined the first type in the history of types that could not be a list. ③ • The pattern pi can never match the type τlδe T Lbl : Represented as e5, this error message is raised when a branch of a case expression can never be executed because the types of the pattern pi of this branch have been already covered by previous patterns. Then, the idea is to find in the case-expression a pattern (pi ) whose type is a subtype of the types of the previous patterns. The slice contains the labels defining the type of the pattern, the labels defining the matching expression e, and the set of labels corresponding to all the patterns p j with j P Lbl. l✶ ele enn l • The pattern pi can never match since previous clauses com- pletely covered the type τlδe : This case is completely analogous to the previous one, with the only difference that a branch cannot be executed because the type of the case-expression has been already covered by the types of previous branches. • Fun application will fail since e::τ✶i is not a function of arity n: Represented as e2, this error message identifies a buggy application. This happens when an expression of the incorrect type, or with the wrong number of arguments have been applied to other expressions. Note that we can identify the application in the source code thanks to the labels of the types. In this case, le is the label of the applied expression. • The pattern pk can never match the type τ✶i : Represented as e3, this error message identifies a case expression where the expression being evaluated cannot match one of the branches (thus, this branch will never be executed). In this case, function sliceError searches for a case expression where the label of τ1 is equal to the label of the matching expression e, and the label of τ2 corresponds to some pattern pk . In Figure 5, function slicingSol takes a solution produced with function solve and, contrary to Figure 4, it ignores the information related to type errors, and only concentrates in the solutions computed so far, even in the case that an error (❑♣E q) was reported. The solutions are then processed by function sliceSol to identify type errors and produce slices. For simplicity, here again, we have formalized only three different type errors with their slices: • Cons will produce an improper list since its 2nd argument is τα : Represented as e4, this error message identifies a list constructor where the inferred type of the second argument cannot be a list (i.e., a list rX ⑤Xss where Xs is not a list). This is known because a type variable α associated to the second argument cannot be evaluated to a list (neither r s nor r s♣ , q). Example 9. Continuing our running example, we can extract the slices associated with the type errors and inconsistencies. If we use functions slicingErrors and slicingSol with the solution computed in Example 7, we get the following slices: ♣ q ✏ te3♣l8 , 1, 2, 1q, tl4 , l6 , l9 , l11 ✉✉ ♣ q ✏ te6♣l8 , Y, 2, 2q, tl4 , l6 , l9 , l15 , l19 ✉✉ slicingErrors S ol slicingSol S ol The first slice, computed with the functions in Figure 4, produces a type error. In particular, the first branch of the internal case expression will crash because variable X can never match the value 1 (it is always 2). The second slice has been computed with the functions in Figure 5. This slice corresponds to a type inconsistency: the third branch of the case expression will never be reached because the pattern in the second branch (2) has covered the whole type of X (2), and thus the second branch will be always selected. 5. Termination and Minimality In this section we formulate two important properties of our technique: (1) our method to produce slices for type errors is a finite process (termination), and (2) the slices produced are minimal (minimality). Proofs can be found in the technical report [12]. Theorem 1 (Termination). Let P be a program, and let C be the type constraint associated with P. The type inference system in Figure 1 terminates with C as input. Theorem 2 (Minimality). Let P be a program, C the type constraint associated with P, and ♣Ce , Le , Sole q a type error produced by the type inference system in Figure 1 with C as input. Then, Le is a minimal slice for the unsolvable constraint Ce . 6. Implementation All the work we described has been implemented and combined with the analysis performed in Dialyzer v2.5 (in R15B). The main idea during the implementation was to be conservative with the previous Dialyzer behavior in such a way that we want the user to be able to use Dialyzer as always producing the usual information. Additionally, we also want to allow the user to see the slices associated with the type errors when she is interested in them. This objective has been achieved by introducing a new flag. In the extension, the user can produce the slices associated with the type errors detected by using a new command line argument --slice. In order to integrate the new technique, we have modified the implementation of five different Dialyzer’s modules. However, the bulk of the changes are in module dialyzer typesig.erl, which is the module generating and solving the type constraints. The explanation component consists of about 2,000 lines of Erlang code. In addition, we had to implement additional functionality related to the treatment of Core Erlang. For instance, the labeling of Core Erlang programs has been changed as described in Section 3 so that each relevant expression is now identifiable in all phases and can be isolated as part of a type error slice. All the source code of our experimental prototype implementation is publicly available at: http://www.it.uu.se/research/group/hipe/dialyzer/. The following example shows the output of this prototype. Example 10. Consider the following part of an Erlang program, in a file ex3.erl, which is an Erlang version of Example 3: 5 6 7 8 9 10 11 12 13 main(X) -> case X of 2 -> case X 1 -> 2 -> Y -> end end. of a; b; Y The new output of Dialyzer is: > dialyzer --slice ex3.erl ex3.erl:9: The pattern 1 can never match the type 2 discrepancy sources: ex3.erl:6 case X of <= Expressions: X ex3.erl:7 2 -> <= Expressions: 2 ex3.erl:8 case X of <= Expressions: X ex3.erl:9 1 -> a; <= Expressions: 1 ex3.erl:11: The variable Y can never match since previous clauses completely covered the type 2 discrepancy sources: ex3.erl:6 case X of <= Expressions: X ex3.erl:7 2 -> <= Expressions: 2 ex3.erl:8 case X of <= Expressions: X ex3.erl:10 2 -> b; <= Expressions: 2 ex3.erl:11 Y -> Y <= Expressions: Y The original output by Dialyzer omits the “discrepancy sources” information. The new information is the slices shown after these lines. Each slice indicates the exact expressions that caused the error (e.g., in the second error explanation, the expressions reported are 2, X and Y). This information helps understand the warning message provided by Dialyzer, especially if the program is big and the expressions are located in different functions or modules. 7. Concluding Remarks We described a new technique to explain the cause of a definite success typing error through the use of program slices. Given an Erlang program, the technique analyzes the type information of all its expressions trying to assign a type to all of them. Whenever an expression cannot be assigned a type, the technique detects a type error. During the detection of this error, our technique propagates information along the program about the exact position in the source code of those expressions that produced the type error. With this information, it is possible to determine what exact parts of the source code should be changed to correct the type error. Besides type errors, the technique can also use the information collected to explain the detection of dead or unreachable code. One important characteristic of our technique is that it produces minimal slices, which are composed of all and only those expressions that contribute to a success typing error. Our implementation has been integrated into a prototype extension of Dialyzer, the Erlang’s static discrepancy analyzer. For future work, we plan to extend our technique to also handle refined success typings [9], and integrate it into an Erlang editor so that the slices produced by Dialyzer can be directly highlighted in the original source code. References [1] R. Carlsson. An introduction to Core Erlang. In Proceedings of the PLI’01 Erlang Workshop, 2001. [2] V. Choppella and C. T. Haynes. Diagnosis of ill-typed programs. Technical Report 426, Indiana University, 1995. [3] D. Duggan and F. Bent. Explaining type inference. Science of Computer Programming, 27(1):37–83, 1996. [4] N. El Boustani and J. Hage. Improving type error messages for generic Java. Higher-Order and Symbolic Computation, 24(1–2):3–39, 2011. [5] R. B. Findler, J. Clements, C. Flanagan, M. Flatt, S. Krishnamurthi, P. Steckler, and M. Felleisen. DrScheme: A programming environment for Scheme. J. Funct. Program., 12(2):159–182, Mar. 2002. [6] E. Fragkaki. Explanation of success typing violations in Erlang programs. Master’s thesis, National Technical University of Athens, 2010. [7] C. Haack and J. B. Wells. Type error slicing in implicitly typed higherorder languages. Sci. Comput. Program., 50(1-3):189–224, 2004. [8] J. Hage and B. Heeren. Heuristics for type error discovery and recovery. In Proceedings of IFL’06, volume 4449 of LNCS, 2007. [9] T. Lindahl and K. Sagonas. Detecting software defects in telecom applications through lightweight static analysis: A war story. In APLAS, volume 3302 of LNCS, pages 91–106. Springer, 2004. [10] T. Lindahl and K. Sagonas. Practical type inference based on success typings. In PPDP’06, pages 167–178, New York, NY, USA, 2006. [11] V. Rahli, J. Wells, and F. Kamareddine. A constraint system for a SML type error slicer. Technical report, Herriot Watt University, 2010. [12] K. Sagonas, J. Silva, and S. Tamarit. Precise explanation of success typing errors (extended version). Technical report, DSIC, Universitat Politècnica de València, November 2012. [13] T. Schilling. Constraint-free type error slicing. In Proceedings of TFP’11, pages 1–16, 2012. [14] J. Silva. A vocabulary of program-slicing based techniques. ACM Computing Surveys, 44(3), 2012. [15] P. J. Stuckey, M. Sulzmann, and J. Wazny. Improving type error diagnosis. In Proceedings of Haskell’04, pages 80–91, 2004. [16] F. Tip. A survey of program slicing techniques. Journal on Program. Lang., 3(3):121?–189, 1995. [17] F. Tip and T. B. Dinesh. A slicing-based approach for locating type errors. ACM Trans. Softw. Eng. Methodol., 10(1):5–55, 2001. [18] M. Wand. Finding the source of type errors. In POPL, pages 38–43, New York, NY, USA, 1986. [19] J. Yang. Explaining type errors by finding the source of a type conflict. In Trends in Functional Programming, pages 58–66, 2000. [20] J. Yang, G. Michaelson, P. Trinder, and J. B. Wells. Improved type error reporting. In IFL, pages 71–86, 2000.