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.