A Cost-effective Estimation of Uncaught Exceptions in Standard ML Programs^{∗}

Kwangkeun Yi^{†} and Sukyoung Ryu

                      {kwang, puppy}@cs.kaist.ac.kr

Dept. of Computer Science Korea Advanced Institute of Science and Technology

Abstract

We present a static analysis that detects potential runtime exceptions that are raised and never handled inside Standard ML(SML) programs. This analysis will predict abrupt termination of SML programs, which is SML’s only one “safety hole.”

Even though SML program’s control flow and exception flow are in general mutually dependent, analyzing the two flows are safely decoupled. Program’s control-flow is firstly estimated by simple case analysis of call expressions. Using this call-graph information, program’s exception flow is derived as set-constraints, whose least model is our analysis result. Both of these two analyses are proven safe and the reasons behind each design decision are discussed.

Our implementation of this analysis has been applied to realistic SML programs and shows a promising cost-accuracy performance. For the ML-Lex program, for example, the analysis takes 1.36 seconds and it reports 3 may-uncaught exceptions, which are exactly the exceptions that can really escape. Our final goal is to make the analysis overhead less than 10% of the compilation time(compiling the ML-Lex takes 6 to 7 seconds) and to analyze modules in isolation.

1 Introduction

Exception handling facilities in Standard ML [MTHM97] allow the programmer to define, raise and handle exceptional conditions. Exceptional conditions are brought (by a raise expression) to the attention of another expression where the raised exceptions may be handled.

∗This work is supported in part by KOSEF Grant 95-0100-54-3, by Korea Ministry of Information and Communication Grant 96151-IT2-12, by Korea Ministry of Science and Technology, by Samsung Electronics Corp., by LG Information and Communications, and by Creative Research Initiatives of the Korean Ministry of Science and Technology. [A preliminary version of this paper appeared in the proceedings of the 4th International Static Analysis Symposium.]

†phone:+82.42.869.3536, fax:+82.42.869.3510, address: Dept. of Computer Science, KAIST, 373-1 Kusong-dong Yusong-gu, Taejon 305-701, Korea

Use of the exception facilities is not necessarily limited to deal with errors. The programmer can use exceptions as a “control diverter” to escape any control structure to a point where the corresponding exception is handled. Also, using the exceptions, the programmer can tailor an operation’s results to particular purposes in a wider variety of contexts than would otherwise be the case.

The exception facilities, however, can provide a hole for program safety. SML programs can abruptly halt when an exception is raised and never handled. This is the only one “safety hole” in well-typed SML programs. Uncaught exceptions are sometimes disastrous [Ar996].

In this paper, we present a static analysis that detects exceptions that may cause this abrupt halt of SML programs. Our goal is to develop an effective such analysis that has less than 10% overhead of the total compilation time.

1.1 Exception Mechanism in Standard ML

In SML, exceptions are treated just like any other values (until they are raised). They can be passed as function arguments, returned as the results of function applications, bound to identifiers, stored in locations and etc.

An exception consists of an exception name possibly paired with some argument values. For example,

                            Error("at line 10")

constructs the Error exception with the string argument. (In what follows, an exception name such as Error is called an “exception constructor.”) The exception constructor Error must be declared beforehand:

                         exception Error of string

An exception is raised by

                                  raise e

where the expression e must evaluate to an exception. For example, raise !x, where x is dereferenced for an exception value. A raised exception is particularly called an exception packet. In this paper, however, when the context is clear we will use exception, exception value, and exception packet interchangeably.

Once an exception is raised, a handler is located by dynamic means: by going up the current evaluation chain to find potential handlers. During this process, one or more levels of the currently active call chain are aborted, up to the function containing the handler.

In SML, the syntax for an exception handler is:

e handle p_{1} => e_{1} | ⋯ | p_{n} => e_{n}

Patterns p_{i}’s are compared with a raised exception from the computation of e. When the exception’s name (constructor) matches with pattern p_{k}, the corresponding expression e_{k} is evaluated. If the match fails, the raised exception continues to propagate back along the evaluation chain until it meets another handler, and so on.

1.2 Analysis Problems

• SML exceptions are first-class objects. Consider:

                           fun f(x) = · · · raise !x · · ·

Function f raises an exception !x in a location x passed to f.

• Precise exception analysis needs a precise call-graph estimation. Consider:

                       fun f(g) = · · · g(x) handle E => · · ·

In order to estimate the uncaught exceptions from g(x), we must analyze which functions are bound to g when f is called.

• Conversely, precise call-graph estimation needs a precise exception analysis. Con-

sider:

                 fun f(x) = · · · e handle E(g) => g(x) · · ·  (*)

In order to decide which functions are called at g(x), we must decide whether the e’s uncaught exceptions include E and, if so, which functions are carried by it.

1.3 Caveat

One subtlety of the SML’s exception declaration is that it is generative. (This is also true for the datatype declarations.) Each evaluation of an exception declaration binds a new, unique name to the exception constructor. An exception handler looks up this internal name to determine a match. For example, in the following incorrect definition of the factorial function, each recursive call to fact generates a new instance of exception ZERO (line (1)). Thus, the handler in line (3), which can only handle exceptions declared in its lexical scope, cannot handle another instance of ZERO that is declared and raised inside the recursive call fact(n-1). Hence this fact function always stops with an uncaught exception ZERO.

           fun fact(n) =
               let exception ZERO                       (1)
               in if n <= 0 then raise ZERO             (2)
                  else n * fact(n-1) handle ZERO => 1   (3)
               end

Our analysis cannot correctly analyze programs that utilize such generative nature of the exception (and the datatype) declarations. This limitation is not severe; exceptions (and datatypes) are largely declared at the global scope or at a module level, or we can move existing local declarations out to the global level without affecting the “observational” behavior of the programs. Programs where this hoisting is impossible cannot be analyzed correctly by our analysis.

We consider only exceptions that appear in the program’s text (including library sources). This limitation can easily be lifted if our analysis starts with a table of primitive operators and their exceptions.

program  exns^{a} w/ args^{b}  arg types^{c}  ftn arg^{d}
Knuth-Bendix.sml  1  1 string  0
ml-lex.sml  8  1 int list * string

0

SML/NJ 109 339 34 string, string*int, int list, intmap, System.Unsafe.object list, symbol list, exn, unit→unit 1 (the

                                                      Found

exception in

                                                      debug/
                                                      run.sml)

HOL  60  18 string, int, record

of string/int

0

anumber of exception declarations (static count)

bnumber of exceptions with arguments (static count)

cargument types: basic building blocks after chasing type abbreviations and datatype arguments dnumber of exceptions whose argument has a function (static count)

Figure 1: Exception Use Statistics in SML Programs

1.4 Our Approach

In the earlier work [Yi94], all the above problems were tackled by a monolithic abstract interpreter. Functions, exceptions, and other data values were parts of the abstract values. The analysis was a collecting analysis that computed stable program states at each expression point of the input program. This monolithic approach was appealing because the analysis design and its correctness proof was done at once by a sound abstraction of the SML’s concrete semantics. The collecting analyzer was, however, too expensive. It took about one hour to analyze the ML-Lex program, for example.

For a better cost-effective analysis, we surveyed SML codes and found that such a full-fledged analysis may be an overkill in almost all cases. In particular, we found that such case as (*) almost never happened(Figure 1)^{1}. This suggests that, in most cases, the call-graph estimation can be done independent of the exception analysis. Preparing for the rare case that exceptions carry functions would not pay-off in practice.

This does not mean that we don’t guarantee the safety of our call-graph estimation. For such cases when functions to call are brought by uncaught exceptions, we choose to do a crude approximation, believing that this “large” approximation would be rarely detrimental to the call-graph accuracy. Please note that we cannot use standard techniques for closure analysis [Shi91, Hei93, PS92, JW96] because their correctness does not consider languages with function-carrying exceptions.

Program’s call-graph is estimated by a set of call-graph rules. For example, “x(0)” calls functions that are bound to x when λx.e is called, “(f 0) 1” calls functions (f 0) that the f’s body(a function expression) represents. The crude approximation happens when an exception’s argument is the function to call. In this case we collect functions whose types unify with the call expression’s function type. This simple call-graph estimation, which enables us to separate the control flow analysis from the exception flow, substantially reduces the total analysis cost and the consequent loss in accuracy of our exception analysis is not high because exceptions (or datatypes) rarely carry functions. The exact definition and its correctness proof are in Proposition 4 at page 16.

1At least for hand-written codes. Situation may be different in automatically generated programs.

This call graph information is then used in exception analysis. For each function f, we express its exception flow as two classes of set constraints:

• One class is for set P_{f} of f’s uncaught exceptions^{2}. For example, P_{f} of the following

function

                               fun f(x) = e(0)+1

includes the sum ⋃_{g} P_{g} of P_{g}’s for g that may be called at “e(0).” In some cases, P_{f} is also composed of the set of exceptions that are available in f. For example, P_{f} of the following function

                               fun f(x) = raise x

includes the set of exceptions passed to f.

• The other class of constraints is for set X_{f} of exception values that are available during f’s application. In the previous example function f, X_{f} includes the sum ⋃_{g} X_{g} of X_{g}’s for g that may call f, because the caller g may pass its available exceptions to f through x.

Our exception analysis is to build the set of constraints(for P_{f} and X_{f}) and to compute its least solution(model). After the analysis, two things are reported to the programmer:

1. The solution of P_{f} for each top-level function f. The existence of such exceptions indicates that the program may terminate abnormally.

2. Uncaught exceptions from each handle expression. From this information the programmer can check the completeness of the handler patterns.

2 Language L

For presentation brevity, we present our analysis for an imaginary language L. The language is a monomorphically typed, call-by-value, higher-order language. The language L’s abstract syntax and the usual monomorphic type rules are shown in Figure 2. We use the usual notation that for a finite function f ∈ A →^{fin} B, f[a ↦ b] denotes the new function which maps a into b and all other a′ ∈ dom(f) into f(a′).

For brevity, we have omitted datatype values, numbers, strings, primitive operators, and memory operations (assignment, reference, and dereference). In reality, we work on the SML source level^{3}.

2In SML, uncaught exceptions are called exception packets. Hence “P_{f}.”

3The absyn level of SML/NJ, which is after the input program is type-inferenced.

Abstract syntax

e ::= x  variable
|  λx_{τ} .e  function
|  e_{1} e_{2}  application
|  exn κ e  exception construction
|  decon e  deconstruction
|  case e_{1} κ e_{2} e_{3}  switch
|  fix f λx_{τ} .e_{1} in e_{2} recursive function binding
|  raise e  exception raise
|  +raise e κ  exception raise-only
|  -raise e κ_{1} ⋯ κ_{n} exception raise-except
|  handle e_{1} λx_{τ} .e_{2} exception handler
| 1 constant

Type

τ ::= τ exn exception type with argument type τ
|  τ → τ function type
| ι constant type

Type rules

Γ ∈ Var →^{fin} Type ArgType(κ) = exception κ’s argument type

[ABS]  {Γ[x ↦ τ] ⊢ e: τ′ \atop Γ ⊢ λx_{τ} .e: τ → τ′}  [VAR]  {Γ(x) = τ \atop Γ ⊢ x: τ}

[FIX] Γ[f ↦ τ_{1}] ⊢ λx_{τ} .e_{1}: τ_{1} Γ[f ↦ τ_{1}] ⊢ e_{2}: τ_{2}

Γ ⊢ fix f λx_{τ} .e_{1} in e_{2}: τ_{2} [EXN]

Γ ⊢ e: τ  ArgType(κ) = τ
Γ ⊢ exn κ e: τ exn

[DCON] Γ ⊢ e: τ exn Γ ⊢ decon e: τ [APP] \begin{matrix}Γ ⊢ e_{1}: τ → τ′ & Γ ⊢ e_{2}: τ \\ Γ ⊢ e_{1} e_{2}: τ′ \end{matrix}

[CASE] {Γ ⊢ e_{1}: τ′ exn \atop Γ ⊢ e_{2}: τ} ; Γ ⊢ e_{3}: τ

Γ ⊢ case e_{1} κ e_{2} e_{3}: τ [RS]

Γ ⊢ e: τ exn
Γ ⊢ raise e: τ′

[-RS]

Γ ⊢ e: τ exn Γ ⊢ -raise e κ^{+}: τ′ [+RS]

Γ ⊢ e: τ exn
Γ ⊢ +raise e κ: τ′

[HNDL] (Γ ⊢ e_{1}: τ′ Γ[x ↦ τ] ⊢ e_{2}: τ′ τ = τ′′ exn) / (Γ ⊢ handle e_{1} λx_{τ} .e_{2}: τ′) [C] Γ ⊢ 1: ι

Figure 2: L’s Abstract Syntax and Type Rules

Values in L are either exceptions or functions. An exception value is constructed by “exn κ e” where κ is an exception name and expression e is for its argument value. The argument of an exception is recovered by “decon e”. “fix f λx_{τ} .e_{1} in e_{2}” binds recursive function f = λx_{τ} .e_{1} in e_{2}. The case expression “case e_{1} κ e_{2} e_{3}” branches to e_{2} if the value of e_{1} is constructed with κ, otherwise, to e_{3}. “raise e” raises exception e. The +raise and -raise expressions are used in limited contexts, which we will discuss in the next section. The handle expression “handle e_{1} λx_{τ} .e_{2}”, where e_{2} is typically a case expression on x, evaluates e_{1} first. If e_{1}’s result is a raised exception whose type is τ, the exception is bound to x inside e_{2}. If e_{1}’s result is a normal value, then the value is returned. Note that this handle expression can handle only one type of exceptions.

The function “λx_{τ} .e_{2}” in a handle expression “handle e_{1} λx_{τ} .e_{2}” is called a handler function, the expression “e_{1}” a handlee expression, and the argument “x” of the handler function a handle variable.

The operational semantics of L is in Figure 3. Relation γ ⊢ e ⇒ v (respectively γ ⊢ e ⇒ \underline{κ v}) is read: expression e evaluates into value v (respectively raises exception κ v). Note that except for the handle rule every rule

σ_{1} ⊢ e_{1} ⇒ v_{1} ; ⋮ ; σ_{n} ⊢ e_{n} ⇒ v_{n} ; σ ⊢ e ⇒ v

represents the following n more extra rules for propagating a raised exception:

{σ_{1} ⊢ e_{1} ⇒ \underline{κ v} \atop σ ⊢ e ⇒ \underline{κ v}}  {σ_{1} ⊢ e_{1} ⇒ v \atop σ_{2} ⊢ e_{2} ⇒ \underline{κ v}} ; σ ⊢ e ⇒ \underline{κ v}  ⋯  σ_{1} ⊢ e_{1} ⇒ v_{1} ; ⋮ ; σ_{n−1} ⊢ e_{n−1} ⇒ v_{n−1} ; σ_{n} ⊢ e_{n} ⇒ \underline{κ v} ; σ ⊢ e ⇒ \underline{κ v}

This indicates that evaluation of expressions e_{i} in the hypothesis stops with the first raised exception, and this is the result of the expression e in the conclusion.

Definition 1 (type_{℘}(e)) For an L program ℘ (a closed expression) of type τ_{0}, we write type_{℘}(e) for the type τ of its sub-expression e iff Γ ⊢ e: τ is a sub-deduction of ⊢ ℘: τ_{0}. We simply write type(e) when it is clear from the context which program ℘ the expression e belongs to.

Note that the type type_{℘}(e) is uniquely defined. The typing rules for raise expressions ([RS], [-RS], and [+RS]), which can assign “arbitrary” types, will assign unique ones when the type τ_{0} of the program ℘ is fixed.

Definition 2 (Typeful program) An L program ℘ is typeful iff during the execution of ℘ (1) its every sub-expression e evaluates into a value of type(e) and (2) for each handler function λx_{τ} .e in ℘ only the exceptions of type τ are bound to x.

σ ∈ Env  = Var →^{fin} Val  environments
v ∈ Val  = Closure + Exn + {1} values
Closure = Expr × Env  lambda exprs in program ℘ and environments
κ v ∈ Exn  = Con × Val  exceptions
Con  = {κ_{1}, ⋯, κ_{N}}  exception names in program ℘
\underline{κ v} ∈ Packet = Exn  raised exceptions
σ ⊢ 1 ⇒ 1  σ ⊢ λx_{τ} .e ⇒ ⟨λx_{τ} .e, σ⟩

{σ(x) = v \atop σ ⊢ x ⇒ v} σ[f ↦ ⟨fix f λx_{τ} .e_{1}, σ⟩] ⊢ e_{2} ⇒ v

σ ⊢ fix f λx_{τ} .e_{1} in e_{2} ⇒ v
{σ ⊢ e ⇒ v \atop σ ⊢ exn κ e ⇒ κ v}  {σ ⊢ e ⇒ κ v \atop σ ⊢ decon e ⇒ v}
{σ ⊢ e_{1} ⇒ ⟨λx_{τ} .e′, σ′⟩ \atop σ ⊢ e_{2} ⇒ v_{2}} ; σ′[x ↦ v_{2}] ⊢ e′ ⇒ v ; σ ⊢ e_{1} e_{2} ⇒ v  {σ ⊢ e_{1} ⇒ κ v \atop σ ⊢ e_{2} ⇒ v} ; σ ⊢ case e_{1} κ e_{2} e_{3} ⇒ v
{σ ⊢ e_{1} ⇒ ⟨fix f λx_{τ} .e′, σ′⟩ \atop σ ⊢ e_{2} ⇒ v_{2}} ; σ′[f ↦ ⟨fix f λx_{τ} .e′, σ′⟩][x ↦ v_{2}] ⊢ e′ ⇒ v ; σ ⊢ e_{1} e_{2} ⇒ v  {σ ⊢ e_{1} ⇒ κ′ v κ′ ≠ κ \atop σ ⊢ e_{3} ⇒ v} ; σ ⊢ case e_{1} κ e_{2} e_{3} ⇒ v
{σ ⊢ e ⇒ κ v \atop σ ⊢ raise e ⇒ \underline{κ v}}  \begin{matrix}σ ⊢ e ⇒ κ v & ∀i ∈ \{1, ⋯, n\}.κ_{i} ≠ κ \\ σ ⊢ -raise e κ_{1} ⋯ κ_{n} ⇒ \underline{κ v} \end{matrix}
{σ ⊢ e ⇒ κ v \atop σ ⊢ +raise e κ ⇒ \underline{κ v}}  \begin{matrix}σ ⊢ e_{1} ⇒ \underline{v} & σ[x ↦ v] ⊢ e_{2} ⇒ \\ σ ⊢ handle e_{1} λx_{τ} .e_{2} ⇒ v \end{matrix}

Figure 3: L’s Operational Semantics The second condition requires that the exceptions that are raised and thrown to a handler should have the same type as the handler function’s argument type.

Proposition 1 Every typeful SML program can be written in a typeful L program.

Proof. Note that the typefulness of L requires the raised exceptions, as well as expression’s values, to be typeful (the second condition of Definition 2.

It is well known that any polymorphically type-checked SML program can be translated into a monomorphic program by the let-inlining. Making raised exceptions to be typeful in L is also straightforward, if L’s handle expression could have multiple handler functions. Let {τ_{1}, ⋯, τ_{n}} be the set of exception types in an SML program. Every SML handle expression is translated into an L handler:

handle e λx_{τ_{1}}e_{1} | ⋯ | λx_{τ_{n}}e_{n}

The semantics is that if an uncaught exception from e is of type τ_{i} then it is bound to x_{τ_{i}} inside e_{i}. The SML’s handling expressions for exception patterns of type τ_{i} are translated into e_{i}. If the SML handler patterns do not completely cover an exception type τ_{i}, then the corresponding e_{i} is made just to re-raise the x. Then, clearly, such L program is typeful. □

Throughout this paper, we assume, for presentation brevity, that L’s handle expression has only one handler function, and consider only typeful L programs whose variables are uniquely named (alpha-converted).

2.1 SML Programs in L

We assume that SML programs in L satisfy the following noteworthy things. It is straightforward to find such L program that corresponds to a given SML program. (Note that, in this section, some examples in L are not supported by the abstract syntax of Figure 2. For convenience we use numbers and multiple branches with the wild-card pattern, for example.)

• -raise The handler patterns are always augmented with an extra raise(-raise) expression, in order to re-raise exceptions that are not caught:

                      e handle ERROR => 1 | FAIL => 2

⟹^{is}

handle e λ x_{exn}.

                                                case x
                                                  ERROR 1
                                                  FAIL  2
                                                    -raise x ERROR FAIL

“-raise x ERROR FAIL” indicates that the re-raised exceptions are those bound to x excluding ERROR and FAIL.

• +raise If a handler’s pattern for exception’s argument part is not complete, the exception is explicitly re-raised by +raise:

            exception E of int list

⋯

            e handle E nil => 1

⟹^{is}

handle e λ x_{(ιlist)exn}.

                                               case x

E (λ y_{ιlist}.

                                                      case y
                                                        NIL 1
                                                          +raise x E) (decon x)
                                                   -raise x E

The above program’s handler can handle exception E only with nil list. Program in L makes this situation explicit by re-raising the E exceptions if their arguments are non-empty list. “+raise x E” indicates the re-raised exception shall only be the E exceptions.

• Exception constructors that need arguments are translated into a function, which is β-reduced whenever appropriate. For example,

            exception E of int

⋯ E, ⋯

⟹^{is} ⋯ (λ x_{ι}.(exn E x)), ⋯

• All functor applications are in-lined. That is, functor definitions and applications disappear and are replaced by in-lined structures.^{4}

3 Set-Constraint Systems

Our exception analysis is presented in the set-constraint framework[Hei92, AH95]. We use this formalism not because we will use its computation method (transforming set constraints into a regular tree grammar) but because the rule-based constraint formalism makes our presentation convenient. Our exception analysis is computed by the conventional iterative fixpoint method because our solution space is finite: exception names in the program. Correctness proofs are done by the fixpoint induction [Sto77] over the continuous functions that are derived [CC95] from our constraint systems.

We present three set-constraint systems: ⊳_{1}, ⊳_{2}, and ⊳_{3}. Our analysis is the last one ⊳_{3}. The other two constraint systems are stepping stones to prove ⊳_{3}’s safety. Note that 1) our analysis decouples control-flow analysis from exception analysis and 2) our interest is in uncaught exceptions from functions. These two things are done by ⊳_{2} and ⊳_{3} in order. ⊳_{2}(Section 3.3) decouples control-flow analysis(Section 3.2) from exception analysis. ⊳_{3}(Section 3.4) increases the constraint granularity to the function-level. Because exception-related expressions are sparse in programs, it is wasteful to generate constraints for every expression of the input program as in ⊳_{2}. ⊳_{3} is proven consistent with ⊳_{2}, ⊳_{2} with ⊳_{1}, and ⊳_{1}(Section 3.1) is assumed correct with respect to the standard semantics of L.

To review some notions of set constraint formalism, an interpretation I (a map from set expressions to sets) is a model (a solution) of a conjunction C of constraints if, for each constraint X ⊇ se (set variable X and set expression se) in C, I(se) is defined and I(X) ⊇ I(se). We write lm(C) for the least model of C. All our constraint systems(⊳_{1}, ⊳_{2}, ⊳_{3}) guarantee the existence of the least model because every operator is monotonic(in terms of set-inclusion) and each constraint’s left-hand-side is a single variable [Hei92].

4In SML, parameterized modules are called functors. A functor is a function that, given an argument structure, returns a new structure. A structure is a named collection of declarations.

3.1 Concrete Constraint Construction ⊳_{1}

Every expression e of the input program has two set constraints: V_{e} ⊇ se and P_{e} ⊇ se. The set variable(the unknown) V_{e} is for e’s values, P_{e} is for the uncaught exceptions(packets) during e’s evaluation. A constraint V_{e} ⊇ se(P_{e} ⊇ se) may be read as “expression e evaluates into a set of values (has uncaught exceptions) including those of se.”

In Figure 4 we index V and P sometimes with expressions, sometimes with numbers. For example, in

[RS_{⊳_{1}} ] {⊳_{1} e_{1}: C_{1} \atop ⊳_{1} raise e_{1}: \{P_{e} ⊇ V_{1}\} ∪ C_{1}}

the C_{1} has constraints, among others, for e_{1}. The set variable for e_{1} is simply written as “V_{1}.” The subscript “e” of set variables “V_{e}” and “P_{e}” denotes the current expression (raise e_{1}) to which the above rule applies. Note that, in L programs, raise expression’s argument expression does not raise exceptions (Section 2.1), hence P_{e} does not include P_{1}.

Note that var(x) indicates the values bound to variable x when the function with argument x is called:

I(var(x)) = {v | “e_{1} e_{2}” ∈ e, λx_{τ} .e ∈ I(V_{1}), v ∈ I(V_{2})},

and app_{V} (V_{1}) the values returned from functions V_{1}:

I(app_{V} (V_{1})) = {v | λx_{τ} .e ∈ I(V_{1}), v ∈ I(V_{e})}

Similarly, app_{P} (V_{1}) (with subscript P) indicates the uncaught exceptions from function calls.

Consider the rule for the handle expression:

[HNDL_{⊳_{1}} ]

⊳_{1} e_{1}: C_{1}  ⊳_{1} e_{2}: C_{2}
⊳_{1} handle e_{1} λx_{τ} .e_{2}: { V_{e} ⊇ app_{V} (λx_{τ} .e_{2}) ∪ V_{1},
P_{e} ⊇ app_{P} (λx_{τ} .e_{2}), V_{x} ⊇ P_{1}
} ∪ C_{1} ∪ C_{2}

The first constraint

V_{e} ⊇ app_{V} (λx_{τ} .e_{2}) ∪ V_{1}

indicates that the handle expression’s values V_{e} are either the values V_{1} of the handlee expression e_{1} or the values returned from the handler function. Note that the V_{x} ⊇ P_{1} indicates that the argument to the handler function “λx_{τ} .e_{2}” is the uncaught exceptions P_{1} from the handlee expression e_{1}. The second constraint

P_{e} ⊇ app_{P} (λx_{τ} .e_{2})

v ∈ Val  = Closure + Exn + {1} values
λx_{τ} .e ∈ Closure = Expr  lambda exprs in program ℘
κ v ∈ Exn  = Con × Val  exceptions
κ ∈ Con  = {κ_{1}, ⋯, κ_{N}}  exception names in program ℘
κ v ∈ Packet = Exn  raised exceptions
I(V_{e})  ⊆ Val  I(P_{e})  ⊆ Packet
I(λx_{τ} .e)  = {λx_{τ} .e}  I(1) = {1}  I(se ∪ se′)  = I(se) ∪ I(se′)
I(exn(κ, V_{1})) = {κ v | v ∈ I(V_{1})}  I(decon(V_{1})) = {v | κ v ∈ I(V_{1})}
I(var(x))  = {v | e_{1} e_{2} ∈ ℘, λx_{τ} .e ∈ I(V_{1}), v ∈ I(V_{2})}
I(app_{V} (V_{1}))  = {v | λx_{τ} .e ∈ I(V_{1}), v ∈ I(V_{e})}
I(app_{P} (V_{1}))  = {κ v | λx_{τ} .e ∈ I(V_{1}), κ v ∈ I(P_{e})}
I(case(V_{1}, κ, V_{2}, V_{3}))  = {v | v ∈ I(V_{2}), κ v′ ∈ I(V_{1})} ∪ {v | v ∈ I(V_{3}), κ′v′ ∈ I(V_{1}), κ′ ≠ κ}
I(-raise(V_{1}, κ_{1}, ⋯, κ_{n})) = {κ′v | κ′v ∈ I(V_{1}), ∀i.κ_{i} ≠ κ′}
I(+raise(V_{1}, κ))  = {κ v | κ v ∈ I(V_{1})}
[VAR_{⊳_{1}} ] ⊳_{1} x: \{V_{x} ⊇ var(x)\}  [C_{⊳_{1}} ] ⊳_{1} 1: \{V_{e} ⊇ 1\}  [ABS_{⊳_{1}} ] {⊳_{1} e_{1}: C_{1} \atop ⊳_{1} λx_{τ} .e_{1}: \{V_{e} ⊇ λx_{τ} .e_{1}\} ∪ C_{1}}
[FIX_{⊳_{1}} ]  \begin{matrix}⊳_{1} e_{1}: C_{1} & ⊳_{1} e_{2}: C_{2} \\ ⊳_{1} fix f λx_{τ} .e_{1} in e_{2}: \{V_{e} ⊇ V_{2}, P_{e} ⊇ V_{2}, V_{f} ⊇ λx_{τ} .e_{1}\} ∪ C_{1} ∪ C_{2} \end{matrix}
[EXN_{⊳_{1}} ]  {⊳_{1} e_{1}: C_{1} \atop ⊳_{1} exn κ e_{1}: \{V_{e} ⊇ exn(κ, V_{1}), P_{e} ⊇ P_{1}\} ∪ C_{1}}

[DCON_{⊳_{1}} ] (⊳_{1} e_{1}: C_{1}) / (⊳_{1} decon e_{1}: {V_{e} ⊇ decon(V_{1}), P_{e} ⊇ P_{1}} ∪ C_{1})

[APP_{⊳_{1}} ]  \begin{matrix}⊳_{1} e_{1}: C_{1} & ⊳_{1} e_{2}: C_{2} \\ ⊳_{1} e_{1} e_{2}: \{V_{e} ⊇ app_{V} (V_{1}), P_{e} ⊇ app_{P} (V_{1}) ∪ P_{1} ∪ P_{2}\} ∪ C_{1} ∪ C_{2} \end{matrix}

[CASE_{⊳_{1}} ] ⊳_{1} e_{1}: C_{1}  ⊳_{1} e_{2}: C_{2}  ⊳_{1} e_{3}: C_{3} ⊳_{1} case e_{1} κ e_{2} e_{3}: {V_{e} ⊇ case(V_{1}, κ, V_{2}, V_{3}), P_{e} ⊇ P_{1} ∪ P_{2} ∪ P_{3}} ∪ C_{1} ∪ C_{2} ∪ C_{3}

[RS_{⊳_{1}} ]  {⊳_{1} e_{1}: C_{1} \atop ⊳_{1} raise e_{1}: \{P_{e} ⊇ V_{1}\} ∪ C_{1}}

[-RS_{⊳_{1}} ] (⊳_{1} e_{1}: C_{1}) / (⊳_{1} -raise e_{1} κ_{1} ⋯ κ_{n}: {P_{e} ⊇ -raise(V_{1}, κ_{1}, ⋯, κ_{n})} ∪ C_{1})

[+RS_{⊳_{1}} ] (⊳_{1} e_{1}: C_{1}) / (⊳_{1} +raise e_{1} κ: {P_{e} ⊇ +raise(V_{1}, κ)} ∪ C_{1})

[HNDL_{⊳_{1}} ]

⊳_{1} e_{1}: C_{1}  ⊳_{1} e_{2}: C_{2}
⊳_{1} handle e_{1} λx_{τ} .e_{2}: { V_{e} ⊇ app_{V} (λx_{τ} .e_{2}) ∪ V_{1},
P_{e} ⊇ app_{P} (λx_{τ} .e_{2}), V_{x} ⊇ P_{1}
} ∪ C_{1} ∪ C_{2}

Figure 4: Constructing Concrete Constraints: ⊳_{1} indicates that uncaught exceptions of the handle expression include those from the handler function. Recall that in L uncaught exceptions from a handle expression are explicitly reraised from the handler function.

We could have used Heintze’s method[Hei93, HJ91] to compute the constraint solution. However, it is wasteful to consider all expressions and their values because exceptionrelated expressions are sparse in programs and function values(for control-flow analysis) can be separately estimated. These two observations are reflected in the forthcoming constraint systems, ⊳_{2} and ⊳_{3}.

Proposition 2 (Correctness of ⊳_{1}) For a program (a closed expression) ℘, let ⊳_{1}℘: C_{1} and let lm(C_{1}) be the least model of C_{1}. Then for every sub-expression e of ℘, lm(C_{1})(V_{e}) (respectively lm(C_{1})(P_{e})) includes all the values that results from e (respectively all the exceptions that escapes from e) during the execution of ℘.

Proof Sketch. This correctness can be proved by following the steps outlined in [Hei93, Hei92]. The key idea is to define a “set-based operational semantics” that is defined over a fixed set-environment (a map from variables to the sets of values). A set-environment is defined to be safe if the environment includes all the values that are bound to each variable during the program’s standard execution (Figure 3). Among the safe set-environments, there exists the least safe set-environment. Our least model lm(C_{1}) is proved to be equivalent to the the least safe set-environment: lm(C_{1})(V_{e}) (respectively lm(C_{1})(P_{e})) is exactly the set of values (respectively the set of escaping exceptions) that are derived for e by the set-based operational semantics with the least safe set-environment. □

Not only is ⊳_{1} correct but typeful. The following typefulness is important for the consistency of the forthcoming constraint system ⊳_{2}.

Proposition 3 (Typefulness of ⊳_{1}) For a program (a closed expression) ℘, let ⊳_{1}℘: C_{1}. Then its least model lm(C_{1}) preserves types: ∀e ∈ ℘.lm(C_{1})(V_{e}) ⊆ Val_{type(e)}(the set of values of type(e)).

Proof. The least model lm(C_{1}) is equivalent to the ⊆-least fixpoint fixF_{1} of the following continuous function F_{1} derived from C_{1} as follows[CC95]:

V  = {V_{e} | e ∈ ℘} ∪ {P_{e} | e ∈ ℘} the set of constraint variables for program ℘
2^{Val}  = the powerset of Val, ordered by ⊆

F_{1}: (V → 2^{Val}) → (V → 2^{Val})

F_{1}(ρ)(V_{e}) = \{ \begin{matrix}\{1\} & if e = 1 \\ \{λx_{τ} .e′\}  if e = f(function var) where fix f λx_{τ} .e′ ∈ ℘ \\ ρ(P_{1})  if e = x(handle var) where handle e_{1} λx_{τ} .e_{2} ∈ ℘ \\ \{v | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ ρ(V_{2})\} & if e = x(normal var) \\ \{λx_{τ} .e′\} & if e = λx_{τ} .e′ \\ ρ(V_{2}) & if e = fix f λx_{τ} .e_{1} in e_{2} \\ \{κ v | v ∈ ρ(V_{1})\} & if e = exn κ e_{1} \\ \{v | κ v ∈ ρ(V_{1})\} & if e = decon e_{1} \\ \{v | λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ ρ(V_{e′})\} & if e = e_{1} e_{2} \\ \{v | v ∈ ρ(V_{2}), κ v′ ∈ ρ(V_{1})\} ∪ \{v | v ∈ ρ(V_{3}), κ′ v′ ∈ ρ(V_{1}), κ′ ≠ κ\} \\ if e = case e_{1} κ e_{2} e_{3} \\ ρ(V_{1}) ∪ ρ(V_{2}) & if e = handle e_{1} λx_{τ} .e_{2} \\ ∅ & otherwise \end{matrix}

F_{1}(ρ)(P_{e}) = \{ \begin{matrix}ρ(P_{1}) & if e = exn κ e_{1} \\ ρ(P_{1}) & if e = decon e_{1} \\ \{κ v | λx_{τ} .e′ ∈ ρ(V_{1}), κ v ∈ ρ(P_{e′})\} ∪ ρ(P_{1}) ∪ ρ(P_{2}) \\ if e = e_{1} e_{2} \\ ρ(P_{1}) ∪ ρ(P_{2}) ∪ ρ(P_{3}) & if e = case e_{1} κ e_{2} e_{3} \\ ρ(P_{2}) & if e = fix f λx_{τ} .e_{1} in e_{2} \\ ρ(V_{1}) & if e = raise e_{1} \\ \{κ′v | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′\} & if e = -raise e_{1} κ_{1} ⋯ κ_{n} \\ \{κ v | κ v ∈ ρ(V_{1})\} & if e = +raise e_{1} κ \\ ρ(P_{2}) & if e = handle e_{1} λx_{τ} .e_{2} \\ ∅ & otherwise \end{matrix}

It is straightforward to derive this function because the ⊳_{1} generates at most one constraint per V_{e} and P_{e}. That is, every ⊇ in constraints is =.

We prove typeful(fixF_{1}) by the fixpoint induction, where the assertion typeful(ρ) for a program ℘ is:

typeful(ρ) =

∀e ∈ ℘. \{ \begin{matrix}& ρ(V_{e}) ⊆ Val_{type(e)} \\ ∧ & e’s exn value is raised and bound to a handle var x_{τ} ⇒ ρ(V_{e}) ⊆ Val_{τ} \\ ∧ & e’s uncaught exn is bound to a handle var x_{τ} ⇒ ρ(P_{e}) ⊆ Val_{τ} \end{matrix}

Base typeful(∅) is trivially true. We will show that typeful(F_{1}(ρ)) holds given the induction hypothesis (I.H.) typeful(ρ).

First, the cases for F_{1}(ρ)(V_{e}).

[C] e = 1. F_{1}(ρ)(V_{e}) = {1} ⊆ Val_{ι}.

[VAR] e = f (function variable) where fix f λx_{τ} .e′ ∈ ℘.

F_{1}(ρ)(V_{f}) = {λx_{τ} .e′} by definition. Because the program ℘ is typeful, the function λx_{τ} .e′ is in Val_{type(λx_{τ} .e′)}, which is equal to Val_{type(f)} because of L’s type rules.

[VAR] e = x (handle variable) where handle e_{1} λx_{τ} .e_{2} ∈ ℘.

F_{1}(ρ)(V_{x}) = ρ(P_{1}) by definition. Because the program ℘ is typeful, the e_{1}’s uncaught exn is bound to x_{τ}. Thus by I.H. ρ(P_{1}) ⊆ Val_{τ}.

[VAR] e = x (normal var).

F_{1}(ρ)(V_{x}) = {v | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ ρ(V_{2})} by definition. If λx_{τ} .e′ ∈ ρ(V_{1}) then by I.H. λx_{τ} .e′ ∈ Val_{type(e_{1})}. Thus, because the program ℘ is typeful, type(e_{1}) = τ → and type(e_{2}) = τ. Therefore, by I.H., value v in ρ(V_{2}) is in Val_{τ}.

Other cases are similarly proved.

Now, the cases for F_{1}(ρ)(P_{e}): assuming that e’s uncaught exception is bound to a handle var x_{τ}, we prove F_{1}(ρ)(P_{e}) ⊆ Val_{τ}.

[EXN] e = exn κ e_{1}

F_{1}(ρ)(P_{e}) = ρ(P_{1}) by definition. The assumption implies that e_{1}’s uncaught exception is bound to x_{τ}. Thus by I.H. ρ(P_{1}) ⊆ Val_{τ}.

[RS] e = raise e_{1}

F_{1}(ρ)(P_{e}) = ρ(V_{1}) by definition. The assumption implies that e_{1}’s value is bound to the x_{τ}. Thus by I.H. ρ(V_{1}) ⊆ Val_{τ}.

Other cases are similarly proved. □

3.2 Separate Call-Graph Estimation

Call-graph estimation methods[Shi91, TJ94, PS92, JW96, HM97] in the literature cannot be directly used in our exception analysis, because their high-order source languages do not have exceptions, not to mention the function-carrying exceptions.

Figure 5 shows our rules to estimate the call-graph of a program ℘. An edge e → λx.e′ indicates that during the execution of ℘ the e may evaluate into a closure of the lambda λx.e′.

One noticeable rule is the decon case where an exception’s argument is a function:

type(e_{1}) = type(decon e), e_{1} → λx.e′
decon e → λx.e′

We estimate that an exception’s argument functions are those whose types are equal to the type of the current decon expression.^{5}

5Note that the L language is monomorphic. In SML, the “is equal to” must be “unifies with.”

λx.e → λx.e

{fix f λx.e_{1} ∈ ℘ \atop f → λx.e_{1}}  {e_{2} → λy.e \atop fix f λx.e_{1} in e_{2} → λy.e}

{e_{1} e_{2} ∈ ℘, e_{1} → λx.e′_{1}, e_{2} → λy.e \atop x → λy.e}

{e_{1} → λx.e′_{1}, e′_{1} → λy.e \atop e_{1} e_{2} → λy.e}

type(e_{1}) = type(decon e), e_{1} → λx.e′

decon e → λx.e′

{e_{2} → λx.e \atop case e_{1} κ e_{2} e_{3} → λx.e}  {e_{3} → λx.e \atop case e_{1} κ e_{2} e_{3} → λx.e}

{e_{1} → λy.e \atop handle e_{1} λx.e_{2} → λy.e}  {e_{2} → λy.e \atop handle e_{1} λx.e_{2} → λy.e}

Figure 5: Call-graph Estimation Rules

This crude approximation is inevitable in order to separate the control flow analysis from the exception flow analysis. This simple call-graph analysis substantially reduces the total analysis cost and the consequent loss in accuracy of our exception analysis is not high because exceptions (or datatypes) rarely carry functions.

Proposition 4 (A safe call table Lam) Given a program ℘, let FtnExpr and 2^{Lambda} respectively be the set of function-typed expressions and the powerset of lambda expressions in ℘. Define Lam: FtnExpr → 2^{Lambda} to be

Lam(e) = {λx_{τ} .e′ | e → λx_{τ} .e′ is deducible by the rules in Figure 5}.

Then Lam is safe: ⊳_{1}℘: C → ∀e ∈ FtnExpr.Lam(e) ⊇ lm(C)(V_{e}).

Proof. Note that the Lam is equivalent to the ⊆-least fixpoint of the following continuous function L [CC95]:

L: (FtnExpr → 2^{Lambda}) → (FtnExpr → 2^{Lambda})

L(ℓ)(e) = \{ \begin{matrix}\{λx_{τ} .e_{1}\} & if e = λx_{τ} .e_{1} \\ \{λx_{τ} .e_{1} | fix f λx_{τ} .e_{1} ∈ ℘\} & if e = f \\ ∪\{ℓ(e_{2}) | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ ℓ(e_{1})\} & if e = x \\ ∪\{ℓ(e′) | λx.e′ ∈ ℓ(e_{1})\} & if e = e_{1} e_{2} \\ ℓ(e_{2}) & if e = fix f λx_{τ} e_{1} in e_{2} \\ ℓ(e_{2}) ∪ ℓ(e_{3}) & if e = case e_{1} κ e_{2} e_{3} \\ ℓ(e_{1}) ∪ ℓ(e_{2}) & if e = handle e_{1} λ_{τ} .e_{2} \\ ∪\{ℓ(e′) | type(e′) = type(decon e)\} if e = decon e \end{matrix}

We use the fixpoint induction. The assertion Q(ℓ, ρ) that we will prove is

∀e ∈ FtnExpr.ℓ(e) ⊇ ρ(V_{e}) ∧ typeful(ρ).

Note that we include the typeful(ρ) assertion that we used in the proof of Proposition 3. This typefulness of ρ is necessary in proving the decon case.

Base case Q(∅, ∅) trivially holds. We now prove that Q(ℓ, ρ) implies Q(L(ℓ), F_{1}(ρ)). Case e of a normal variable x:

L(ℓ)(x) = ∪{ℓ(e_{2}) | e_{1} e_{2} ∈ ℘, λx.e ∈ ℓ(e_{1})}  by definition
⊇ ∪{ℓ(e_{1}) | e_{1} e_{2} ∈ ℘, λx.e ∈ ρ(V_{e_{1}})} by I.H.
⊇ ∪{ρ(V_{e_{1}}) | e_{1} e_{2} ∈ ℘, λx.e ∈ ρ(V_{e_{1}})} by I.H.
= F_{1}(ρ)(V_{x})  by definition.

Other cases are done similarly, except for the case e of decon e_{1}:

L(ℓ)(e) = ∪{λx.e′ | λx.e′ ∈ ℘, type(e) = type(λx.e′)} by definition
= Val_{type(e)}, the set of lambdas of type(e) in ℘ by definition
⊇ F_{1}(ρ)(V_{e})  by that typeful(ρ) ⇒ typeful(F_{1}(ρ)),

which is proven in Proposition 3.

□

3.3 Exception Constraint Construction ⊳_{2}

We now consider a new system ⊳_{2}(Figure 6) where only exceptions are considered. Constraints for function(non-exception) values are removed and instead, a pre-computed, safe call-graph table Lam(Proposition 4) is used.

Every expression e has two set constraints: X_{e} ⊇ se and P_{e} ⊇ se. X_{e} is for exceptions and P_{e} for uncaught exceptions. For solutions of X_{e} and P_{e} we will consider only exception names. That is, X_{e} is for the set |I(V_{e})|of exception names in e’s values I(V_{e}):

I(X_{e}) ⊇ |I(V_{e})|

Definition 3 |I(V)| = {κ | κ v ∈ V } ∪ |{v | κ v ∈ I(V)}|.

κ ∈ Exn  = {κ_{1}, ⋯, κ_{N}} exception names in program ℘
κ ∈ Packet = Exn  raised exceptions
I(X_{e}) ⊆ Exn  I(P_{e}) ⊆ Packet
I(var(x))  = {κ | e_{1} e_{2} ∈ ℘, λx_{τ} .e ∈ Lam(e_{1}), κ ∈ I(X_{2})}
I(app_{X}(e_{1}))  = {κ | λx_{τ} .e ∈ Lam(e_{1}), κ ∈ I(X_{e})}
I(app_{P} (e_{1}))  = {κ | λx_{τ} .e ∈ Lam(e_{1}), κ ∈ I(P_{e})}
isExn(τ)  = true iff τ = τ′ exn
I(X_{1} \_{e} \{κ_{1}, ⋯, κ_{n}\}) = \{ \begin{matrix}I(X_{1}) & if type(e) = τ′exn ∧ isExn(τ′) \\ I(X_{1}) \ \{κ_{1}, ⋯, κ_{n}\} & otherwise \end{matrix}
I(X_{1} ∩_{e} \{κ\})  = \{ \begin{matrix}I(X_{1}) & if type(e) = τ′exn ∧ isExn(τ′) \\ I(X_{1}) ∩ \{κ\} & otherwise \end{matrix}
I(κ)  = {κ}

I(λx_{τ} .e) and I(se ∪ se) are the same as in ⊳_{1} (Figure 4, page 12).

[VAR_{⊳_{2}} ] ⊳_{2} x: {X_{x} ⊇ var(x)}  [C_{⊳_{2}} ]  ⊳_{2} 1: ∅
[ABS_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} λx_{τ} .e_{1}: C_{1}}  [FIX_{⊳_{2}} ] \begin{matrix}⊳_{2} e_{1}: C_{1} & ⊳_{2} e_{2}: C_{2} \\ ⊳_{2} fix f λx_{τ} .e_{1} in e_{2}: \{X_{e} ⊇ X_{2}, P_{e} ⊇ P_{2}\} ∪ C_{1} ∪ C_{2} \end{matrix}
[DCON_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} decon e_{1}: \{X_{e} ⊇ X_{1}, P_{e} ⊇ P_{1}\} ∪ C_{1}}
[EXN_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} exn κ e_{1}: \{X_{e} ⊇ κ ∪ X_{1}, P_{e} ⊇ P_{1}\} ∪ C_{1}}
[APP_{⊳_{2}} ]  \begin{matrix}⊳_{2} e_{1}: C_{1} & ⊳_{2} e_{2}: C_{2} \\ ⊳_{2} e_{1} e_{2}: \{X_{e} ⊇ app_{X}(e_{1}), P_{e} ⊇ app_{P} (e_{1}) ∪ P_{1} ∪ P_{2}\} ∪ C_{1} ∪ C_{2} \end{matrix}
[CASE_{⊳_{2}} ] \begin{matrix}⊳_{2} e_{1}: C_{1} & ⊳_{2} e_{2}: C_{2} & ⊳_{2} e_{3}: C_{3} \\ ⊳_{2} case e_{1} κ e_{2} e_{3}: \{X_{e} ⊇ X_{2} ∪ X_{3}, P_{e} ⊇ P_{1} ∪ P_{2} ∪ P_{3}\} ∪ C_{1} ∪ C_{2} ∪ C_{3} \end{matrix}
[RS_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} raise e_{1}: \{P_{e} ⊇ X_{1}\} ∪ C_{1}}
[-RS_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} -raise e_{1} κ_{1} ⋯ κ_{n}: \{P_{e} ⊇ (X_{1} \_{e_{1}} \{κ_{1}, ⋯, κ_{n}\})\} ∪ C_{1}}
[+RS_{⊳_{2}} ]  {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} +raise e_{1} κ: \{P_{e} ⊇ (X_{1} ∩_{e_{1}} \{κ\})\} ∪ C_{1}}

[HNDL_{⊳_{2}} ]

⊳_{2} e_{1}: C_{1}  ⊳_{2} e_{2}: C_{2}
⊳_{2} handle e_{1} λx_{τ} .e_{2}: { X_{e} ⊇ app_{X}(λx_{τ} .e_{2}) ∪ X_{1},
P_{e} ⊇ app_{P} (λx_{τ} .e_{2}), X_{x} ⊇ P_{1}
} ∪ C_{1} ∪ C_{2}

Figure 6: Constructing Exception Constraints: ⊳_{2}

The use of set expression app(e_{1}) for function calls is similar to that in ⊳_{1}, except that we use the call-graph table Lam: FtnExpr → Lambdas(Proposition 4).

Set variable’s indexing convention is the same as in the previous section (⊳_{1}).

Consider the rule for -raise expression:

[-RS_{⊳_{2}} ] {⊳_{2} e_{1}: C_{1} \atop ⊳_{2} -raise e_{1} κ_{1} ⋯ κ_{n}: \{P_{e} ⊇ (X_{1} \_{e_{1}} \{κ_{1}, ⋯, κ_{n}\})\} ∪ C_{1}}

The constraint P_{e} ⊇ X_{1} \_{e} {κ_{1}, ⋯, κ_{n}} collects raised exceptions excluding the κ_{i}’s. Note the meaning of \_{e}:

I(X_{1} \_{e} \{κ_{1}, ⋯, κ_{n}\}) = \{ \begin{matrix}I(X_{1}) & if type(e) = τ′exn ∧ isExn(τ′) \\ I(X_{1}) \ \{κ_{1}, ⋯, κ_{n}\} & otherwise \end{matrix}

If an exception can have other exceptions as its arguments then the exclusion \_{e} has no effect. If blindly excluded, exceptions that are hidden as arguments of the escaping exception are considered caught. This would make the analysis unsafe. Consider a -raise expression whose argument e is an exception that hides another exception in its argument:

-raise \underbrace{κ_{1}(κ_{2}(1))}_{e} κ_{2}

The expression raises the exception κ_{1}(κ_{2}(1)) unless its constructor κ_{1} is equal to κ_{2} (which is false). Hence, the exception κ_{1}(κ_{2}) is raised. If we removed κ_{2} from the set X_{e} = {κ_{1}, κ_{2}} then the exception κ_{2} that can be available when the exception packet is later caught and deconstructed is considered missing thereafter. Therefore, the set-minus operator \_{e} is effective only when the exception values of e cannot have other exceptions hidden in its argument. (The same reason is for the definition of ∩_{e}.)

The constraint-construction rule ⊳_{2} is a safe approximation of ⊳_{1}:

Proposition 5 (Correctness of ⊳_{2}) For a program ℘, let ⊳_{1}℘: C_{1} and ⊳_{2}℘: C_{2} with their least models, I_{1} = lm(C_{1}) and I_{2} = lm(C_{2}). If Lam is safe with respect to ⊳_{1}, then for every sub-expression e ∈ ℘:

I_{2}(X_{e}) ⊇ |I_{1}(V_{e})| and I_{2}(P_{e}) ⊇ |I_{1}(P_{e})|.

Proof. The least models I_{1} and I_{2} are equivalent to the ⊆-least fixpoints fixF_{1} and fixF_{2}, respectively[CC95]. The F_{1} is defined in the proof of Proposition 3. The continuous function F_{2} is derived from C_{2} as follows:

Ψ  = {X_{e} | e ∈ ℘} ∪ {P_{e} | e ∈ ℘} the set of constraint variables for a program ℘
2^{Exn}  = the powerset of Exn, ordered by ⊆

F_{2}: (Ψ → 2^{Exn}) → (Ψ → 2^{Exn})

F_{2}(φ)(X_{e}) = \{ \begin{matrix}φ(P_{1})  if e = x(handle var) where handle e_{1} λx_{τ} .e_{2} ∈ ℘ \\ \{κ | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{2})\} & if e = x(normal var) \\ \{κ\} ∪ φ(X_{1}) & if e = exn κ e_{1} \\ φ(X_{1}) & if e = decon e_{1} \\ \{κ | λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{e′})\} & if e = e_{1} e_{2} \\ φ(X_{2}) ∪ φ(X_{3}) & if e = case e_{1} κ e_{2} e_{3} \\ φ(X_{1}) ∪ φ(X_{2}) & if e = handle e_{1} λx_{τ} .e_{2} \\ φ(X_{2}) & if e = fix f λx_{τ} .e_{1} in e_{2} \\ ∅ & otherwise \end{matrix}

F_{2}(φ)(P_{e}) = \{ \begin{matrix}φ(P_{1}) & if e = exn κ e_{1} \\ φ(P_{1}) & if e = decon e_{1} \\ \{κ | λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(P_{e′})\} ∪ φ(P_{1}) ∪ φ(P_{2})  if e = e_{1} e_{2} \\ φ(P_{1}) ∪ φ(P_{2}) ∪ φ(P_{3}) & if e = case e_{1} κ e_{2} e_{3} \\ φ(X_{1}) & if e = raise e_{1} \\ φ(X_{1})  if e = -raise e_{1} κ_{1} ⋯ κ_{n} and type(e_{1}) = τ′ exn ∧ isExn(τ′) \\ (φ(X_{1}) \ \{κ_{1}, ⋯, κ_{n}\}) \\ if e = -raise e_{1} κ_{1} ⋯ κ_{n} and type(e_{1}) = τ′ exn ∧ ¬isExn(τ′) \\ φ(X_{1})  if e = +raise e_{1} κ and type(e_{1}) = τ′ exn ∧ isExn(τ′) \\ (φ(X_{1}) ∩ \{κ\}) \\ if e = +raise e_{1} κ and type(e_{1}) = τ′ exn ∧ ¬isExn(τ′) \\ φ(P_{2}) & if e = handle e_{1} λx_{τ} .e_{2} \\ φ(P_{2}) & if e = fix f λx_{τ} .e_{1} in e_{2} \\ ∅ & otherwise. \end{matrix}

It is straightforward to derive this function because the ⊳_{2} generates at most one constraint per X_{i} and P_{i}. That is, each ⊇ in constraints is =.

We prove Q(fixF_{2}, fixF_{1}) by the fixpoint induction, where the assertion Q(φ, ρ) for a program ℘ is:

∀e ∈ ℘.φ(X_{e}) ⊇ |ρ(V_{e})| ∧ φ(P_{e}) ⊇ |ρ(P_{e})| ∧ typeful(ρ).

Note that we include the typeful(ρ) assertion that we used in the proof of Proposition 3. This typefulness of ρ is necessary in proofs for the -raise and +raise cases.

Base case Q(∅, ∅) is trivially true. We prove that Q(F_{2}(φ), F_{1}(ρ)) holds given the induction hypothesis Q(φ, ρ). That is, we need to show F_{2}(φ)(X_{e}) ⊇ |F_{1}(ρ)(V_{e})| and F_{2}(φ)(P_{e}) ⊇ |F_{1}(ρ)(P_{e})|.

[VAR] e = x(handle variable) where handle e_{1} λx_{τ} .e_{2} ∈ ℘.

F_{2}(φ)(X_{x}) = φ(P_{1})  (by definition)
⊇ |ρ(P_{1})|  (by I.H.)
= |F_{1}(ρ)(V_{x})|. (by definition)

[VAR] e = x(normal variable).

F_{2}(φ)(X_{x}) = {κ | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{2})} (by definition))
⊇ {v | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ |ρ(V_{2})|}  (by Proposition 2 and I.H.)
= |F_{1}(ρ)(V_{x})|.

[EXN] e = exn κ e_{1}.

F_{2}(φ)(X_{e}) = {κ} ∪ φ(X_{1}) (by definition)
⊇ {κ} ∪ |ρ(V_{1})| (by I.H.)

By definition, |F_{1}(ρ)(V_{e})| = |{κ v | v ∈ ρ(V_{1})}| = {κ} ∪ |ρ(V_{1})|. Therefore, F_{2}(φ)(X_{e}) ⊇ |F_{1}(ρ)(V_{e})|.

F_{2}(φ)(P_{e}) = φ(P_{1})  (by definition)
⊇ |ρ(P_{1})|  (by I.H.)
= |F_{1}(ρ)(P_{e})|. (by definition)

[DCON] e = decon e_{1}.

F_{2}(φ)(X_{e}) = φ(X_{1})  (by definition)
⊇ |ρ(V_{1})|  (by I.H.)
⊇ |F_{1}(ρ)(V_{e})|. (by definition)
F_{2}(φ)(P_{e}) = φ(P_{1})  (by definition)
⊇ |ρ(P_{1})|  (by I.H.)
= |F_{1}(ρ)(P_{e})|. (by definition)

[APP] e = e_{1} e_{2}.

F_{2}(φ)(X_{e}) = {κ | λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{e′})} (by definition)
⊇ {v | λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ |ρ(V_{e′})|}  (by Proposition 2 and I.H.)
= |F_{1}(ρ)(V_{e})|.  (by definition)
F_{2}(φ)(P_{e}) = {κ | λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(P_{e′})} ∪ φ(P_{1}) ∪ φ(P_{2})

(by definition)

⊇ {v | λx_{τ} .e′ ∈ ρ(V_{1}), v ∈ |ρ(P_{e′})|} ∪ |ρ(P_{1})| ∪ |ρ(P_{2})|

(by Proposition 2 and I.H.)

= |F_{1}(ρ)(P_{e})|.  (by definition)

[CASE] e = case e_{1} κ e_{2} e_{3}.

F_{2}(φ)(X_{e}) = φ(X_{2}) ∪ φ(X_{3})  (by definition)
⊇ {v | v ∈ |ρ(V_{2})|, κ v′ ∈ ρ(V_{1})} ∪ {v | v ∈ |ρ(V_{3})|, κ′ v′ ∈ ρ(V_{1}), κ′ ≠ κ}

(by I.H.)

= |F_{1}(ρ)(V_{e})|.  (by definition)

F_{2}(φ)(P_{e}) = φ(P_{1}) ∪ φ(P_{2}) ∪ φ(P_{3})  (by definition)
⊇ |ρ(P_{1})| ∪ |ρ(P_{2})| ∪ |ρ(P_{3})|  (by I.H.)
= |F_{1}(ρ)(P_{e})|.  (by definition)

[RS] e = raise e_{1}.

F_{2}(φ)(P_{e}) = φ(X_{1})  (by definition)
⊇ |ρ(V_{1})|  (by I.H.)
= |F_{1}(ρ)(P_{e})|. (by definition)

[-RS] e = -raise e_{1} κ_{1} ⋯ κ_{n} where type(e_{1}) = τ′ exn ∧ isExn(τ′).

F_{2}(φ)(P_{e}) = φ(X_{1})  (by definition)
⊇ |ρ(V_{1})|  (by I.H.)
⊇ |F_{1}(ρ)(P_{e})|. (by definition)

[-RS] e = -raise e_{1} κ_{1} ⋯ κ_{n} where type(e_{1}) = τ′ exn ∧ ¬isExn(τ′).

F_{2}(φ)(P_{e}) = φ(X_{1}) \ {κ_{1}, ⋯, κ_{n}} (by definition)
⊇ |ρ(V_{1})| \ {κ_{1}, ⋯, κ_{n}} (by I.H.)
|F_{1}(ρ)(P_{e})| = |{κ′v | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′}| (by definition)
= {κ′ | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′}
∪|{v | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′}| (by definition of)

Note that the set |{v | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′}| is empty because ¬isExn(τ′) and the I.H. typeful(ρ)(V_{1}). Thus,

= {κ′ | κ′v ∈ ρ(V_{1}), ∀i.κ_{i} ≠ κ′}
⊆ |ρ(V_{1})| \ {κ_{1}, ⋯, κ_{n}}  (by definition)

Therefore, F_{2}(φ)(P_{e}) ⊇ |F_{1}(ρ)(P_{e})|.

[+RS] e = +raise e_{1} κ where type(e_{1}) = τ′ exn ∧ isExn(τ′).

F_{2}(φ)(P_{e}) = φ(X_{1})  (by definition)
⊇ |ρ(V_{1})|  (by I.H.)
⊇ |F_{1}(ρ)(P_{e})|. (by definition)

[+RS] e = +raise e_{1} κ where type(e_{1}) = τ′ exn ∧ ¬isExn(τ′).

F_{2}(φ)(P_{e}) = φ(X_{1}) ∩ {κ} (by definition)
⊇ |ρ(V_{1})| ∩ {κ} (by I.H.)
|F_{1}(ρ)(P_{e})| = |{κ v | κ v ∈ ρ(V_{1})}|  (by definition)
= {κ | κ v ∈ ρ(V_{1})} ∪ |{v | κv ∈ ρ(V_{1})}| (by definition)
Note that the set |{v | κv ∈ ρ(V_{1})}| is empty

because ¬isExn(τ′) and I.H. typeful(ρ)(V_{1}). Thus,

= {κ | κ v ∈ ρ(V_{1})}
⊆ |ρ(V_{1})| ∩ {κ}  (by definition)

Therefore, F_{2}(φ)(P_{e}) ⊇ |F_{1}(ρ)(P_{e})|.

[HNDL] e = handle e_{1}λx_{τ} .e_{2}.

F_{2}(φ)(X_{e}) = φ(X_{2}) ∪ φ(X_{1}) (by definition)
⊇ |ρ(V_{2})| ∪ |ρ(V_{1})| (by I.H.)
= |F_{1}(ρ)(V_{e})|.  (by definition)
F_{2}(φ)(P_{e}) = φ(P_{2})  (by definition)
⊇ |ρ(P_{2})|  (by I.H.)
= |F_{1}(ρ)(P_{e})|. (by definition)

□

3.4 Function’s Exception Constraint Construction ⊳_{3}

It is wasteful to compute uncaught exceptions from every expression because exceptionrelated expressions are sparse in a program. We need to sparsely generate constraints. Using the ⊳_{2} as our stepping stone, we arrive at our constraint system ⊳_{3} that generates constraints only for functions. The number of unknowns thus becomes proportional to the number of functions, not to the number of expressions. The least model of ⊳_{3}-constraints for an input program is our analysis result: uncaught exceptions from each function.

In ⊳_{3}, set variables are indexed by the lambdas and handlee expressions of the input program. We assume that all lambdas and handlee expressions are uniquely named as f, g, h, etc. We subscript the lambda with its name: “λ_{f}x_{τ} .e”. Similarly for handlee expression such as “e_{g}” in “handle e_{g} λ_{h}x_{τ} .e_{2}”.

Every function (or handlee expression) f of the input program has two set constraints: X_{f} ⊇ se and P_{f} ⊇ se. The set variable X_{f} is for exceptions that are “available” at f, and P_{f} is for uncaught exceptions during the call to f.

Consider the rule for application expression:

[APP_{⊳_{3}} ] \begin{matrix}f ⊳_{3} e_{1}: C_{1} & f ⊳_{3} e_{2}: C_{2} \\ f ⊳_{3} e_{1} e_{2}: \{X_{f} ⊇ app_{X}(e_{1}, X_{f}), P_{f} ⊇ app_{P} (e_{1}, X_{f})\} ∪ C_{1} ∪ C_{2} \end{matrix}

The left-hand-side f of “f ⊳_{3} e” indicates that the expression e appears in f. Thus, if f has a call e_{1} e_{2}, available exceptions X_{f} in f must include the exceptions app_{X}(e_{1}, X_{f}) returned from the call. The uncaught exceptions P_{f} in f must include the exceptions app_{P} (e_{1}, X_{f}) uncaught during the call.

One noticeable rule is [VAR_{⊳_{3}} ]. Because the constraint granularity is a function, constraints for a variable x must be expressed in terms of two functions: function f that variable x provides with exceptions and another function g that provides x with exceptions. The f is the function that appears in the left-hand-side of “⊳_{3} x” and the g is the function (Owner(x)) that has x as its argument. Therefore,

[VAR_{⊳_{3}} ] f ⊳_{3} x: {X_{f} ⊇ X_{Owner(x)}}.

One missing constraint is for the effect of passing exceptions through x when its owner Owner(x)(=^{let} g) is called. This is expressed as app_{X}(e_{1}, X_{f})’s third condition I(X_{g}) ⊇ I(X_{f})

(in terms of caller f and callee g):

I(app_{X}(e_{1}, X_{f})) = {κ | λ_{g}x_{τ} .e ∈ Lam(e_{1}), κ ∈ I(X_{g}), I(X_{g}) ⊇ I(X_{f})}.

The function-level exception constraint rule ⊳_{3} is a safe approximation of ⊳_{2}:

Proposition 6 (Correctness of ⊳_{3}) For a closed term e, let ⊳_{2}e: C_{2} and main ⊳_{3} e: C_{3} with their least models, I_{2} = lm(C_{2}) and I_{3} = lm(C_{3}). Then, if g ⊳_{3} e′: C′ occurs during main ⊳_{3} e: C_{3} then

I_{3}(X_{g}) ⊇ I_{2}(X_{e′}) and I_{3}(P_{g}) ⊇ I_{2}(P_{e′}).

Proof. We will prove that for any model I of C_{3}, the above holds. Note that the least model I_{2} is equivalent to the ⊆-least fixpoints fixF_{2}. The F_{2} is

defined in the proof of Proposition 5.

We prove Q(fixF_{2}) by the fixpoint induction, where the assertion Q(φ) for a program ℘ is:

∀model I of C_{3}.“f ⊳_{3} e: C”occurs during (main⊳_{3}℘: C_{3}) ⇒ I(X_{f}) ⊇ φ(X_{e})∧I(P_{f}) ⊇ φ(P_{e}).

Base case Q(∅) trivially holds. We prove that Q(F_{2}(φ)) holds given the induction hypothesis Q(φ).

In the following proof, for each case f ⊳_{3} expr we abbreviate the expr by e.

[VAR] f ⊳_{3} x(handle variable) where handle e_{g} λ_{h}x_{τ} .e_{2} ∈ ℘.

I(X_{f}) ⊇ I(X_{Owner(x)}) = I(X_{h}) (by [VAR_{⊳_{3}} ])
⊇ I(P_{g})  (by [HNDL_{⊳_{3}} ])
⊇ φ(P_{g})  (because “g ⊳_{3} e_{g}” occurs and by I.H.)
= F_{2}(φ)(X_{x})  (by definition)

[VAR] f ⊳_{3} x(normal variable).

By [VAR_{⊳_{3}} ], I(X_{f}) ⊇ I(X_{Owner(x)}). Let a function g be the owner of x: λ_{g}x_{τ} .e′. For each “k ⊳_{3} e_{1} e_{2}” that occurs at the Owner(x)’s call site, “k ⊳_{3} e_{2}” occurs. Thus

by I.H.,

\begin{aligned}φ(X_{2}) & ⊆ & I(X_{k}) \\ & ⊆ & I(X_{g}) & (by [APP_{⊳_{3}} ]) \end{aligned}

Therefore, I(X_{f}) ⊇ {κ | e_{1} e_{2} ∈ ℘, λx_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{2})} = F_{2}(φ)(X_{x}).

[EXN] f ⊳_{3} exn κ e_{1}.

I(X_{f}) ⊇ {κ}  (by [EXN_{⊳_{3}} ])
I(X_{f}) ⊇ φ(X_{1})  (because “f ⊳_{3} e_{1}” occurs and by I.H.)
Therefore, I(X_{f}) ⊇ {κ} ∪ φ(X_{1})
= F_{2}(φ)(X_{e}) (by definition)

[APP] e = f ⊳_{3} e_{1} e_{2}.

κ ∈ Exn  = {κ_{1}, ⋯, κ_{N}} exception names in program ℘
κ ∈ Packet = Exn  raised exceptions
I(X_{f}) ⊆ Exn  I(P_{f}) ⊆ Packet  X ::= X_{f} | P_{f}
Owner(x)  = f where λ_{f} x_{τ} .e (f’s parameter is x)
I(var(x))  = I(X_{Owner(x)})
I(app_{X}(e_{1}, X)) = {κ | λ_{g}x_{τ} .e ∈ Lam(e_{1}), κ ∈ I(X_{g}), I(X_{g}) ⊇ I(X)}
I(app_{P} (e_{1}, X)) = {κ | λ_{g}x_{τ} .e ∈ Lam(e_{1}), κ ∈ I(P_{g}), I(X_{g}) ⊇ I(X)}

I(X_{f} \_{e} {κ_{1}, ⋯, κ_{n}}), I(X_{f} ∩_{e} {κ}), I(κ), and I(λx_{τ} .e) are the same as in ⊳_{2} (Figure 6, page 18).

[VAR_{⊳_{3}} ]  f ⊳_{3} x: {X_{f} ⊇ var(x)}  [C_{⊳_{3}} ]  f ⊳_{3} 1: ∅

[ABS_{⊳_{3}} ]  {g ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} λ_{g}x_{τ} .e_{1}: C_{1}}  [FIX_{⊳_{3}} ]  \begin{matrix}g ⊳_{3} e_{1}: C_{1} & f ⊳_{3} e_{2}: C_{2} \\ f ⊳_{3} fix g λ_{g}x_{τ} .e_{1} in e_{2}: C_{1} ∪ C_{2} \end{matrix}

[DCON_{⊳_{3}} ]  {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} decon e_{1}: C_{1}}  [EXN_{⊳_{3}} ]  {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} exn κ e_{1}: \{X_{f} ⊇ κ\} ∪ C_{1}}
[APP_{⊳_{3}} ]  \begin{matrix}f ⊳_{3} e_{1}: C_{1} & f ⊳_{3} e_{2}: C_{2} \\ f ⊳_{3} e_{1} e_{2}: \{X_{f} ⊇ app_{X}(e_{1}, X_{f}), P_{f} ⊇ app_{P} (e_{1}, X_{f})\} ∪ C_{1} ∪ C_{2} \end{matrix}

[CASE_{⊳_{3}} ]  \begin{matrix}f ⊳_{3} e_{1}: C_{1} & f ⊳_{3} e_{2}: C_{2} & f ⊳_{3} e_{3}: C_{3} \\ f ⊳_{3} case e_{1} κ e_{2} e_{3}: C_{1} ∪ C_{2} ∪ C_{3} \end{matrix}

[RS_{⊳_{3}} ]  {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} raise e_{1}: \{P_{f} ⊇ X_{f} \} ∪ C_{1}}

[-RS_{⊳_{3}} ]  {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} -raise e_{1} κ_{1} ⋯ κ_{n}: \{P_{f} ⊇ X_{f} \_{e_{1}} \{κ_{1}, ⋯, κ_{n}\}\} ∪ C_{1}}

[+RS_{⊳_{3}} ]  {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} +raise e_{1} κ: \{P_{f} ⊇ X_{f} ∩_{e_{1}} \{κ\}\} ∪ C_{1}}

[HNDL_{⊳_{3}} ]

g ⊳_{3} e_{g}: C_{1}  h ⊳_{3} e_{2}: C_{2}
f ⊳_{3} handle e_{g} λ_{h}x_{τ} .e_{2}: { X_{f} ⊇ app_{X}(λ_{h}x_{τ} .e_{2}, P_{g}) ∪ X_{g},
P_{f} ⊇ app_{P} (λ_{h}x_{τ} .e_{2}, P_{g})
} ∪ C_{1} ∪ C_{2}

Figure 7: Constructing Function’s Exception Constraints: ⊳_{3}

I(X_{f}) ⊇ {κ | λ_{g}x_{τ} .e′ ∈ Lam(e_{1}), κ ∈ I(X_{g}), I(X_{g}) ⊇ I(X_{f})} (by [APP_{⊳_{3}} ])
⊇ {κ | λ_{g}x_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(X_{e′})} (because “g ⊳_{3} e′” occurs and by I.H.)
= F_{2}(φ)(X_{e}).  (by definition)
I(P_{f}) ⊇ {κ | λ_{g}x_{τ} .e′ ∈ Lam(e_{1}), κ ∈ I(P_{g}), I(X_{g}) ⊇ I(X_{f})} (by [APP_{⊳_{3}} ])
⊇ {κ | λ_{g}x_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(P_{e′})} (because “g ⊳_{3} e′” occurs and by I.H.)
I(P_{f}) ⊇ φ(P_{1})  (because “f ⊳_{3} e_{1}” occurs and by I.H.)
I(P_{f}) ⊇ φ(P_{2})  (because “f ⊳_{3} e_{2}” occurs and by I.H.)

Therefore,

I(P_{f}) ⊇ {κ | λ_{g}x_{τ} .e′ ∈ Lam(e_{1}), κ ∈ φ(P_{e′})} ∪ φ(P_{1}) ∪ φ(P_{2})
= F_{2}(φ)(P_{e}). (by definition)

[RS] e = f ⊳_{3} raise e_{1}.

I(P_{f}) ⊇ I(X_{f})  (by [RS_{⊳_{3}} ])
⊇ φ(X_{1})  (because “f ⊳_{3} e_{1}” occurs and by I.H.)
= F_{2}(φ)(P_{e}). (by definition)

[-RS] e = -raise e_{1} κ_{1} ⋯ κ_{n} where type(e_{1}) = τ′ exn ∧ isExn(τ′).

I(P_{f}) ⊇ I(X_{f} \_{e_{1}} {κ_{1}, ⋯, κ_{n}}) (by [-RS_{⊳_{3}} ])
= I(X_{f})  (by definition of \_{e_{1}})
⊇ φ(X_{1})  (because “f ⊳_{3} e_{1}” occurs and by I.H.)
= F_{2}(φ)(P_{e}).  (by definition)

[-RS] e = -raise e_{1} κ_{1} ⋯ κ_{n} where type(e_{1}) = τ′ exn ∧ ¬isExn(τ′).

I(P_{f}) ⊇ I(X_{f} \_{e_{1}} {κ_{1}, ⋯, κ_{n}}) (by [-RS_{⊳_{3}} ])
= I(X_{f}) \ {κ_{1}, ⋯, κ_{n}} (by definition of \_{e_{1}})
⊇ φ(X_{1}) \ {κ_{1}, ⋯, κ_{n}} (because “f ⊳_{3} e_{1}” occurs and by I.H.)
= F_{2}(φ)(P_{e}).  (by definition)

[+RS] e = +raise e_{1} κ where type(e_{1}) = τ′ exn ∧ isExn(τ′).

I(P_{f}) ⊇ I(X_{f} ∩_{e_{1}} {κ}) (by [+RS_{⊳_{3}} ])
= I(X_{f})  (by definition of ∩_{e_{1}})
⊇ φ(X_{1})  (because “f ⊳_{3} e_{1}” occurs and by I.H.)
= F_{2}(φ)(P_{e}).  (by definition)

[+RS] e = +raise e_{1} κ where type(e_{1}) = τ′ exn ∧ ¬isExn(τ′).

I(P_{f}) ⊇ I(X_{f} ∩_{e_{1}} {κ}) (by [+RS_{⊳_{3}} ])
= I(X_{f}) ∩ {κ} (by definition of ∩_{e_{1}})
⊇ φ(X_{1}) ∩ {κ} (because “f ⊳_{3} e_{1}” occurs and by I.H.)
= F_{2}(φ)(P_{e}).  (by definition)

[HNDL] e = f ⊳_{3} handle e_{g}λ_{h}x_{τ} .e_{2}.

I(X_{f}) ⊇ {κ | κ ∈ I(X_{h}), I(X_{h}) ⊇ I(P_{g})} ∪ I(X_{g}) (by [HNDL_{⊳_{3}} ])
= I(X_{h}) ∪ I(X_{g}) (because any model I of C_{3} satisfies I(X_{h}) ⊇ I(P_{g}))
⊇ φ(X_{2}) ∪ I(X_{g}) (because “h ⊳_{3} e_{2}” occurs and by I.H.)
⊇ φ(X_{2}) ∪ φ(X_{g}) (because “g ⊳_{3} e_{g}” occurs and by I.H.)
= F_{2}(φ)(X_{e})  (by definition)
I(P_{f}) ⊇ {κ | κ ∈ I(P_{h}), I(X_{h}) ⊇ I(P_{g})} (by [HNDL_{⊳_{3}} ])
= I(P_{h})  (because any model I of C_{3} satisfies I(X_{h}) ⊇ I(P_{g}))
⊇ φ(P_{2})  (because “h ⊳_{3} e_{2}” occurs and by I.H.)
= F_{2}(φ)(P_{e}) (by definition)

□

Example 1 As an analysis example, consider the following program:

(1)           fun m() = f(exn κ 1)
(2)           fun f(x) = handle g(x) λ_{h} y. 1  ( in SML g(x) handle   => 1)
(3)           fun g(x) = raise x

From line (1),

X_{m} ⊇ κ
X_{f} ⊇ X_{m}, X_{m} ⊇ X_{f} (from X_{m} ⊇ app_{X}(f, X_{m}))
P_{m} ⊇ P_{f}  (from P_{m} ⊇ app_{P} (f, X_{m}))

From line (2)

X_{g} ⊇ X_{f}, X_{f} ⊇ X_{g} (from X_{f} ⊇ app_{X}(g, X_{f}))
P_{f} ⊇ P_{h}, X_{h} ⊇ P_{g} (from P_{f} ⊇ app_{P} (h, P_{g}))

From line (3)

P_{g} ⊇ X_{g}

The least model of the above 9 constraints is the least solution of the equations:

\begin{matrix}X_{m} = \{κ\} & X_{f} = X_{m} ∪ X_{g} & X_{g} = X_{f} & X_{h} = P_{g} \\ P_{m} = P_{f} & P_{f} = P_{h} & P_{g} = X_{g} \end{matrix}

The least solution is:

\begin{matrix}X_{m} = \{κ\} & X_{f} = \{κ\} & X_{g} = \{κ\} \\ P_{m} = ∅ & P_{f} = ∅ & P_{g} = \{κ\} \end{matrix}

3.5 Typeful Constraints for Improved Accuracy

Some constraint rules of ⊳_{3} can be safely sharpened using types. Our actual analysis uses this sharpened ⊳_{3} rules. 1) a function f has exceptions through a variable x only when the x is of an exception type and 2) exceptions X_{f} in f are returned only when f’s return type is an exception type:

I(app_{X}(e_{1}, X)) = {κ | λ_{f}x_{τ} .e ∈ Lam(e_{1}), κ ∈ I(X_{f}|isExn(type(e))), I(X_{f}) ⊇ I(X |isExn(τ))}
I(app_{P} (e_{1}, X)) = {κ | λ_{f}x_{τ} .e ∈ Lam(e_{1}), κ ∈ I(P_{f}), I(X_{f}) ⊇ I(X |isExn(τ))}
I(var(x))  = I(X_{Owner(x)}|isExn(type(x)))
X_{f}  ⊇ app_{X}(⋯) ∪ (X_{g}|isExn(type(e_{g})))  in [HNDL_{⊳_{3}} ]

where I(X |cond) = I(X) if the cond true, ∅ otherwise.

The correctness of this new ⊳_{3} can be proved with respect to a typeful version of ⊳_{2}. The least solution of typeful ⊳_{2}-constraints maps non-exception-typed expressions to the empty set. The typeful ⊳_{2} is consistent with ⊳_{1} because ⊳_{1} is already typeful. The ⊳_{2} becomes typeful by the new [DCON_{⊳_{2}} ] rule:

[DCON_{⊳_{2}} ] ⊳_{2} e_{1}: C_{1} ⊳_{2} decon e_{1}: {X_{e} ⊇ (X_{1}|isExn(type(e))), P_{e} ⊇ P_{1}} ∪ C_{1}

Example 2 Consider the following program:

(1)             fun f(x) = · · · (exn κ_{1} 1)· · · g(1)· · ·
(2)             fun g(x) = raise (exn κ_{2} x)

Note that g raises only κ_{2}. If our constraints are un-typed, we generate constraints that passes f’s exception κ_{1} to g because of the call g(1). This will not happen in our new rules, because g’s argument type is not exception. From line (1),

X_{f} ⊇ κ_{1}, X_{f} ⊇ app_{X}(g, X_{f}), P_{f} ⊇ app_{P} (g, X_{f})

From line (2),

X_{g} ⊇ κ_{2}, P_{g} ⊇ κ_{2}, P_{g} ⊇ X_{g}

The X_{f} ⊇ app_{X}(g, X_{f}) implies X_{g} ⊇ ∅ because g’s argument type is int. The least solution hence maps P_{g} to {κ_{2}}. Meanwhile, untypeful definition of X_{f} ⊇ app_{X}(g, X_{f}) generates X_{g} ⊇ X_{f} and the least solution becomes to map X_{g} to {κ_{1}, κ_{2}}, concluding that P_{g} may raise all these exceptions.

Similar techniques of type-directed improvement of analyses have been reported: accuracy improvement of control flow analysis [JWW97] and stratification of alias analysis [Ruf97].

3.6 Handling of Exception’s Arguments

Because the analysis does not recognize exception’s arguments unless the arguments were exceptions, it may lead into a too conservative result for some programs.

Example 3 Consider the following program that has no uncaught exception:

          exception Fail of int
(1)       fun f() = g() handle Fail(1) => 1
          fun g() = raise (exn Fail 1)

Because the handler pattern “Fail(1)” is not exhaustive for the argument part, the handler is annotated with “+raise x Fail” expression. This +raise expression makes our analysis conclude that f has an escaping exception Fail.

Resolving this problem by adding constraints for non-exception values and risking the subsequent increase of the analysis cost is not appealing for two reasons. Incomplete handler patterns for exception’s argument(like the above example) is rare, and the existing pattern compiler^{6} already can warn of incomplete patterns for exception’s arguments unless the argument type is an exception.

Our analysis reports the pair of an exception name κ and the index of the expression (exn κ e) where the exception is made.^{7} Given a pair of exception name and its birth place information, the programmer can decide which may-uncaught exceptions are real, assuming that the birth place expression has the argument data explicit in the text. In the above example, the programmer safely decides the may-uncaught exception Fail is not real because its birth place is “(exn Fail 1)” hence the handler pattern “Fail(1)” is exhaustive enough.

This can be achieved by a slight change to ⊳_{3}. The exception space becomes the set of tuples:

Exn = {κ_{1}, ⋯, κ_{N}} × Expr

The rule for exn expression becomes:

[EXN_{⊳_{3}} ] {f ⊳_{3} e_{1}: C_{1} \atop f ⊳_{3} exn κ e_{1}: \{X_{f} ⊇ ⟨κ, e⟩\} ∪ C_{1}} where e = exn κ e_{1}

And

X_{1} \_{e} {κ_{1}, ⋯, κ_{n}} and X_{1} ∩_{e} {κ}

removes(respectively, selects) tuples headed by κ_{i}’s(respectively, by κ).

3.7 Adapting ⊳_{3} to Standard ML

Because of SML’s polymorphic types,

• isExn needs to be conservative. The new definition is:

isExn(ι ∨ τ → τ′) = false  constant or function type
isExn(α ∨ τ exn) = true  generic type var or exn type
isExn(τ ref )  = isExn(τ)  reference type
isExn(τ_{1} × τ_{2})  = isExn(τ_{1}) or isExn(τ_{2})  record type
isExn(u)  = ∃κ ∈ Con(u).isExn(ArgType(κ)) user-defined datatype u

The last case is for when a data type u’s constructor κ ∈ Con(u) receives exceptions as its argument.

• Lam’s last case must test for type unifiability (≈) instead of type equality: In this case, type_{℘}(e) - for an expression e of a program ℘ - indicates the SML type of the expression, determined by the let-polymorphic type inference system[Mil78, MTHM97, Tof90].

6An SML’s datatype has a fixed number of ways to construct its values, and patterns are combinations of such constructors.

7This can be understood as abstracting the expression values by the expression index. A similar technique has been widely used in abstracting memory locations: each malloc expression is an abstract location, representing all the locations allocated at that point during execution.

program  lines cfa + setup(sec)^{a} solve(sec)^{b} analysis result
Knuth-Bendix.sml 519  0.54^{c}  0.07^{d} 1 (1x,1r,10h)^{e}
ml-lex.sml  1204  0.89  0.47 3 (10x,19r,10h)
instantiate.sml  1384  2.74  0.04 2 (7x,8r,18h)
typecheck.sml  648  6.07  0.03 0 (1x,2r,17h)
moduleutil.sml  847  4.37  0.08 3 (3x,25r,23h)
pathname.sml  426  0.09  0.01 4 (4x,6r,3h)
string-cvt.sml  454  0.13  0.03 1 (1x,10r,4h)
class compiler  3511  3.65  0.10 3 (11x,34r,4h)

aControl-flow analysis and constraints set-up: in SML, run on DEC Alpha Server1000(4/200), compiled by SML/NJ 108.13

bSolving constraints: in SML, run on DEC Alpha Server1000(4/200), compiled by SML/NJ 108.13 cSML user+system+gc time dC user+system time e1 may-uncaught exceptions from top-level functions among 1 exns(1x), 1 raise exprs(1r), and 10

handlers(10h).

Figure 8: Experimental Results

4 Experimental Results

A prototype’s preliminary performance is shown in Figure 8.

Currently, the analysis speed ranges from 110 to 4000 SML-lines/sec. ([Yi94] ran at 0.2 SML-lines/sec and [FA96] at about 10 SML-lines/sec). We still expect some improvements in the analysis speed as we better implement the control flow analysis part. In particular, a performance bottleneck is in computing the table that partitions user functions into unifiable ones. This process’ cost is proportional to the “size” of function types in the program. This is why the control flow analysis speed is not proportional to the program size.

Computing the Lam uses the fixpoint iteration of cubic complexity. Computing the constraints’ least solution also uses the conventional fixpoint iteration^{8} of cubic complexity.

The analysis accuracy is satisfying. We manually checked the above test programs and found that the reported exceptions for Knuth-Bendix.sml, pathname.sml, string-cvt.sml, and compiler.sml can actually be uncaught. For ml-lex.sml, the 3 may-uncaught exceptions are exactly those that can really escape.

5 Conclusion

We found that even though the exception flow and control flow are in general intertwined in SML programs, the two analyses could be safely and cost-effectively decoupled. For cases where exceptions carry functions(i.e., where control flow analysis needs exception analysis) our control flow analysis uses a crude approximation to assure its safety against the decoupling. Our early experimental evidence suggests that this separation is not detrimental to the accuracy of the exception analysis, while it makes the analysis significantly faster than the earlier methods. We are optimistic that we are near to a right-balance of the cost-accuracy performance.

8The iterative method is possible because the set domain is finite (the set of exception names in the input program) and all set operators(set union “∪”, “X_{f} ∩ {κ}”, “X_{f} \ {κ_{1} ⋯ κ_{n}}”, etc.) are monotonic.

We showed the safety of our exception analysis(constraint system ⊳_{3}) in two steps, using two intermediate systems(⊳_{1} and ⊳_{2}). This safety proofs were done by showing the consistencies between the three constraint systems. We used the fixpoint induction for continuous functions that were derived from the constraint rules [CC95]. Our method may be seen as a kind of abstract interpretation [CC77]. This paper’s technique for enlarging the constraint granularity and proving its consistency with smaller-grained constraint systems can be applied to other analysis problems where the data to analyze are sparse in programs.

We are currently working on analyzing SML modules in isolation, which will be the last thing to make the analysis realistic.

Acknowledgment

We thank Dave MacQueen for his encouragement and keen interest in this work.

References

[AH95] Alex Aiken and Nevin Heintze. Constraint-based program analysis. Tutorial of the ACM Symposium on Principles of Programming Languages, January 1995.

[Ar996]  Ariane 5: Flight 501 Failure.  http://www.esrin.esa.it/htdocs/tidc/Press/

Press96/ariane5rep.html, July 1996.

[CC77] Patrick Cousot and Radhia Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In ACM Symposium on Principles of Programming Languages, pages 238–252, 1977.

[CC95] Patrick Cousot and Radhia Cousot. Compositional and inductive semantic definitions in fixpoint, equational, constraint, closure-condition, rule-based and game-theoretic form. In Lecture Notes in Computer Science, volume 939, pages 293–308. Springer-Verlag, proceedings of the 7th international conference on computer-aided verification edition, 1995.

[FA96] Manuel Fahndrich and Alexander Aiken. Making set-constraint program analyses scale. In Workshop on Set Constraints, August 1996.

[Hei92] Nevin Heintze. Set Based Program Analysis. PhD thesis, Carnegie Mellon University, October 1992.

[Hei93] Nevin Heintze. Set based analysis of ML programs. Technical Report CMU-CS-93-193, Carnegie Mellon University, July 1993.

[HJ91] Nevin Heintze and Joxan Jaffar. A decision procedure for a class of set constraints. Technical Report CMU-CS-91-110, Carnegie-Mellon University, February 1991.

[HM97] Nevin Heintze and David McAllester. Linear-time subtransitive control flow analysis. In Proceedings of the SIGPLAN Conference on Programming Language Design and Implementation, pages 261–272, June 1997.

[JW96] Suresh Jagannathan and Andrew Wright. Flow-directed inlining. In Proceedings of the SIGPLAN Conference on Programming Language Design and Implementation, pages 193–205, May 1996.

[JWW97] Suresh Jagannathan, Stephen Weeks, and Andrew Wright. Type-directed flow analysis for typed intermediate languages. In Lecture Notes in Computer Science, volume 1302, pages 232–249. Springer-Verlag, proceedings of the 4th international static analysis symposium edition, 1997.

[Mil78] Robin Milner. A theory of type polymorphism in programming. Journal of Computer and System Sciences, 17:348–375, 1978.

[MTHM97] Robin Milner, Mads Tofte, Robert Harper, and David MacQueen. The Definition of Standard ML (Revised). MIT Press, 1997.

[PS92] Jens Palsberg and Michael I. Schwartzbach. Safety analysis versus type inference. Information and Computation, 118(1):128–141, 1992.

[Ruf97] Erik Ruf. Partitioning dataflow analyses using types. In ACM Symposium on Principles of Programming Languages, pages 15–26, January 1997.

[Shi91] Olin Shivers. Control-Flow Analysis of Higher-Order Languages. PhD thesis, Carnegie Mellon University, May 1991. Technical Report CMU-CS-91-145.

[Sto77] Joseph E. Stoy. Denotational Semantics: the Scott-Strachey Approach to Programming Language Theory. MIT Press, 1977.

[TJ94] Yan Mei Tang and Pierre Jouvelot. Separate abstract interpretation for control-flow analysis. In Lecture Notes in Computer Science, volume 789, pages 224–243. Springer-Verlag, proceedings of the theoretical aspect in computer science edition, 1994.

[Tof90] Mads Tofte. Type inference for polymorphic references. Information and Computation, 89:1–34, 1990.

[Yi94] Kwangkeun Yi. Compile-time detection of uncaught exceptions for Standard ML programs. In Lecture Notes in Computer Science, volume 864, pages 238–254. Springer-Verlag, proceedings of the first international static analysis symposium edition, 1994.