• No results found

Collaborative Blame

In document Gradual Typing for Python, Unguarded (Page 129-133)

isinstance :

3. Collaborative Blame

While the transient approach recovers pointer identity and open-world soundness, it does so by remov- ing proxies. In previous work, proxies served as the mechanism for tracking and propagating blame information [35, 104, 93]. The runtime system uses this information when a runtime type error is en- countered to report the source of the error—not just the location where the error was discovered and the error was raised, but also which implicit conversion site was violated. This information helps the programmer debug the issue more efficiently.

Traditionally, blame information is propagated through programs at runtime by being included in prox- ies so that when cast errors occur, the information can be included in errors. Consider the following example, in which isEven is cast to

→ ⋆

and then invoked on a string (all within a gradually typed module):

1 ## Guarded Translation

2 isEven =∆ fun isEven n. (n % 2) = 0 3

4 let dyFunc = (isEven ::intbool ⇒ℓ0→⋆) 5 in dyFunc ("Hi" ::str ⇒ℓ1)

When the cast onisEvenis applied at runtime, the result is a proxy aroundisEvenwhich includes the information that the proxy was created by a cast with the label

0. When this proxy is applied to the string"Hi"(which has been casted to⋆), it casts the argument toint. This cast fails and the resulting error message blames

0, indicating that the castintbool ⇒ℓ0⋆→⋆is at fault.

Because the transient strategy lacks proxies, it is initially unclear that implementing blame tracking is possible in our system. However, without blame information, the programmer may not have enough

details to properly diagnose errors and, as such, we develop an alternative mechanism to maintain and propagate this information and report blame errors.

3.1. Runtime Blame Management. We solve this problem by tracking blame information in a global blame map, updating the relevant blame information at every implicit conversion. We track when values are passed between different static types by statically inserting casts into the program as in the guarded strategy; rather than serving as a type enforcement mechanism, these casts only update the blame map—checks are still the main mechanism for detecting type errors. When a check fails, this blame map is used in conjunction with the type information at the failure site to construct the full error account to the programmer. Furthermore, this construction process provides a blame history, indicating the conflicting assumptions that different pieces of the program made to produce this error.

Consider the previous example withisEven, now using the transient translation:

1 ## Transient Translation

2 isEven =∆ fun isEven n. n⇓⟨int, isEven, Arg⟩; (n % 2) = 0 3

4 let dyFunc = (isEven ::intbool ⇒ℓ0→⋆) 5 in dyFunc ("Hi" ::str ⇒ℓ1)

When the cast

0 is applied at runtime toisEven, the blame map records the cast. Then, when the check at the beginning ofisEven’s function body detects thatnis not an integer, the runtime attempts to determine which cast (if any) was responsible by looking up the address of theisEvenfunction in the blame map, where it will find the castintbool ⇒ℓ0→⋆. Next, the runtime determines if this cast was potentially responsible for the error: the Arg context tag at the failed check indicates that it was checking a function argument, and that the castintbool ⇒ℓ0→⋆is unsafe in its argument positions (due to contravariance). Finally, the runtime finds that the actual argument to the function call isstr, which conflicts with the domain of the function typeint, and therefore indicates that the

intbool ⇒ℓ0→⋆cast is at fault.

It is not always possible, however, to go directly from a check failure to an incompatible cast. In the fol- lowing program,makeEqCheckertakes a string and returns a function that checks its argument against

{strstrstr ⇒ℓ0str→⋆→⋆} makeEqChecker

{⟨ makeEqChecker, Res⟩} eqChecker

Figure 1. The blame map foreqCheckerandmakeEqChecker.

the string. ThemakeEqCheckerfunction is then cast to str

→ ⋆ → ⋆

, applied to a string, and then the resulting function is applied to an integer.

1 # Transient translation 2 fun makeEqChecker v. 3 v⇓ ⟨str, makeEqChecker, Arg⟩; 4 fun eqChecker w. 5 w⇓ ⟨str, eqChecker, Arg⟩; 6 v = w 7

8 let castFunc=(makeEqChecker ::strstrbool ⇒ℓ0str→⋆→⋆) 9 in ((castFunc "Hi")⇓⟨→, castFunc, Res⟩) (42 ::int⇒ℓ1)

At runtime, a check insideeqCheckerwill detect that42is not a string. At this point, the runtime will look upeqCheckerin the blame map in an attempt to find the responsible cast, buteqCheckernever passed through a cast; it was implicitly cast as a result of the cast onmakeEqChecker.

However, there is enough information in the inserted casts and checks to tie the check failure with the cast onmakeEqChecker: whenmakeEqCheckeris applied, a check ensures that the result corresponds with the type tag

. This check updates the blame map before returning the result, adding an internal pointer from the result of the function call (the address of this particular instance ofeqChecker) to the value that returned it (heremakeEqChecker). This blame map appears in Figure 1.

When the check fails, the runtime must construct blame information. To do so, it traverses the pointer within the blame map fromeqCheckertomakeEqChecker, including the context tag Res that indicates

eqCheckeris the result of a call tomakeEqChecker. The runtime uses this data in collaboration with the cast onmakeEqCheckerto discern that the cast is potentially responsible for the check failure, and ultimately blame it.

3.2. Transient Blame is not Guarded Blame. Through use of the blame map, casts and checks collab- orate to reconstruct the chains of responsibility that proxies provide with the guarded strategy. Even so, the transient blame behavior differs from that of the guarded strategy: the algorithm may blame multiple casts if each of them is reachable in the blame map and may be responsible for the check fail- ure occurring (similar to the behavior of the monotonic approach discussed in Chapter 3). For example, consider the following variation on theisEvenprogram above, in whichisEvenis cast twice to

→ ⋆

. 1 ## Transient Translation

2 isEven =∆ fun isEven n. n⇓⟨int, isEven, Arg⟩; (n % 2) = 0 3

4 let dyFunc1 = (isEven ::intbool ⇒ℓ0→⋆)

5 in let dyFunc2 = (isEven ::intbool ⇒ℓ1→⋆)

6 in dyFunc1 ("Hi" ::str ⇒ℓ2)

At runtime, the casts on lines 4 and 5 both are recorded in the blame map. When the check inisEven detects that an error has occurred, it will find that both casts are potentially at fault. Since the casts both simply returnisEven, at runtimedyFunc1 anddyFunc2are the same identical value, and the blame tracking system cannot distinguish between them. Therefore, since both casts are unsafe, and both casts could have been responsible for this error, both

0and

1are blamed. By contrast, in a system using the guarded strategy as defined by Wadler and Findler [104],dyFunc1anddyFunc2evaluate to separate and distinct proxies, and so when the call at line 6 is evaluated, only

0is blamed.

Similarly, the transient strategy only raises errors if ill-typed values are used, and so transient and guarded can blame entirely different casts if multiple errors are present in a program. For example, the following program castsisEvento

→ ⋆

and names the resultdyFunc, as above. It then castsdyFunc to int

int and names itbadFunc, then callsdyFuncon a string as before, and finally callsbadFunc on a number.

1 ## Transient Translation

2 isEven =∆ fun isEven n. n⇓⟨int, isEven, Arg⟩; (n % 2) = 0 3

4 let dyFunc = (isEven ::intbool ⇒ℓ0→⋆)

5 in let badFunc = (dyFunc ::⋆→⋆⇒ℓ1intint)

6 in dyFunc ("Hi" ::str ⇒ℓ2); 7 (badFunc 42)⇓⟨bool, badFunc, Res

In the transient system, the casts on lines 4 and 5 updated the blame map and returnisEven. Then the call on line 6 results in a check failure withinisEven, blaming

0as above. With the guarded semantics using the eager strategy of Siek et al. [80], however, the cast on line 5 would fail because it is inconsistent with the previous cast ondyFunc, represented at runtime by a proxy. This cast failure would blame

1— an entirely different result than that produced by the transient strategy.

Despite these differences, the transient blame tracking strategy does result in a system that satisfies the blame-subtyping theorem [104] (as shown in Section 5.1).

4. The Transient Gradual Lambda Calculus

λ

In this section, we present the first formal semantics for the transient strategy, including a source-to- target translation, runtime semantics, and a blame system.

We begin with the source language

λ

with expressions

e

s in Figure 2, which includes variables, re-

cursive functions, mutable references, numbers, and addition.

λ

also has types

T

which range over

function types

T

1

→ T

2, reference types ref

T

, integer types int, and the dynamic type

. Following previous approaches to gradual typing [75, 80, 91, 93], the semantics of

λ

are defined by

translation into a target language

λ

(as expressions

e

in Figure 2) which contains type checks as well as the usual type casts. We then define a single-step reduction relation over the

λ

language. Unlike the targets of cast insertion from previous work [75, 48, 104],

λ

is a dynamically typed calculus. Types appear syntactically in the casts of

λ

, and shallow type tags

S

[13] are used in its checks.

In document Gradual Typing for Python, Unguarded (Page 129-133)