• No results found

Correctness and the Blame Theorem Finally, I observe that while blame-tracking as a practical development aid to programmers is not a part of this work, properties similar to those required by the

In document Gradual Typing for Python, Unguarded (Page 196-200)

CPython/opt PyPy/unopt

6.2. Blame Blame tracking and the blame theorem are key features of gradually typed languages (cf Chapter 5), and while the optimization technique and implementation discussed in this chapter does not

6.2.3. Correctness and the Blame Theorem Finally, I observe that while blame-tracking as a practical development aid to programmers is not a part of this work, properties similar to those required by the

blame theoremare. Recall that the blame theorem, informally described by Wadler and Findler [104] as “well-typed programs can’t be blamed,” requires that any conversion between two types (in the context of the guarded strategy, a cast) is “safe,” then if a program containing such a conversion results in a runtime enforcement failure, that particular conversion is not blamed. A corollary of this is that if all conversions in a program are safe, and assuming that runtime enforcement failures are only generated from conversion sites (as is the case in the guarded strategy), then the program will not fail—again, “well-typed programs can’t be blamed.”

A similar property also holds for the transient design presented here, as a corollary of Theorem 6.3. Corollary 6.1. Suppose

∅ ⊢ d : A; Ω

and

σ

is a solution to

.

Suppose also that for all terms

e⇓S

that appear in the derivation of

∅ ⊢ d : A; Ω

, there exists a

Γ, ρ, A

, Ω

, Σ, µ, µ

such that

Γ⊢ d : A

; Ω

′,

σ

is a solution to

′,

Σ⊢ ρ : σΓ

′,

⟨ρ(e), µ⟩ −→

⟨v, µ

, and

⌊σA

⌋ ⪯ S

. Then

⟨v⇓S, µ

⟩ ̸−→ fail

.

In other words, if all of the checks that appear in a program are safe, then the program itself will not fail (whether or not those checks are removed). This proof relies on the fact that all checks that could ever be evaluated in a program exist in its source (no new checks are generated), and that the only way for a program to step to

fail

is via a failed check—both assumptions that clearly hold from inspecting the evaluation rules in Figure 12.

One remaining question is what the relationship between the “safety” criterion presented here and that of the blame theorem in guarded systems is. Both are claims about the runtime behavior of a program,

and in both cases the antecedents of the theorem trivially hold for all programs in statically-typed sub- sets of the languages that the theorem holds in. However, there may be significant differences in what other programs can be considered “safe.” As discussed in Chapter 5, the sets of gradually typed programs that evaluate to runtime enforcement failures under guarded and under transient are overlapping, but distinct, sets, which suggests that the safety criteria between them will vary as well. An important step for understanding this may involve the development of criteria that describe safe evaluations rather than safe programs; I conjecture that such a perspective may reveal behavior relationships between the guarded and transient designs which are not apparent from simply observing the source locations where runtime enforcement code has been inserted, a perspective from which guarded and transient appear very different. In future work, I hope to develop such a perspective and use it to characterize what makes programs safe or unsafe in both settings.

7. Related Work

Gradual type systems. In our work, checks are removed when we infer that the types they verify can be trusted. This is suggestive of the strict confined gradual typing of Allende et al. [12], which allows programmers to restrict types such that their inhabitants must never have passed through dynamic code (indicated by

↓T

) or will never pass through dynamic code in the future (indicated by

↑T

). If a term has type

↓T

, the type system verifies that value originated from an introduction form for values of that type. We suspect that this information could be used to remove transient checks or perform other optimizations.

Our approach is also related to concrete types [108, 72], as discussed in Chapter 2, Section 2.4. The formulation of concrete vs. like types given by Wrigstad et al. [108] is appropriate for the guarded se- mantics, but splitting types into those which can be statically relied on and those which need runtime verification is similar to our approach.

In addition to the Reticulated Python performance study of Greenman and Migeed [42], the transient approach was also studied and evaluated by Greenman and Felleisen [41]. This work compared the tran- sient and guarded approaches (there, referred to as “first-order embedding” and “higher order embed- ding”) in the context of Racket. The use of a single host language allows these approaches to be directly

compared, and they found that the transient approach resulted in much better worst-case performance (in their experiments,

20.36×

compared to

1072.80×

) at the cost of losing the guarded approach’s efficiency benefits when code is fully-typed.

Type inference. Aiken and Wimmers [7] generalize equational constraints used in standard Hindley- Milner type inference [50, 62] to subset constraints

T

⊆ S

. In this work types are interpreted as subsets of a semantic domain of values and the subset relation is equivalent to subtyping. Their type system includes union and intersection types and generates systems of constraints that may be simplified by rewriting, similar to the rules for constraint simplification shown in Figure 9. Aiken and Fähndrich [6] specialize this approach to determine where coercions are needed in dynamically typed programs with tagging and untagging in order to optimize Scheme programs. This approach is similar to ours, except that while our type system is much simpler and does not include untagged types and values, transient checks must generate check constraints. Check constraints are similar to the conditional constraints of Pottier [66], but rather than allowing arbitrary implications, check constraints reason exclusively about type tags.

Soft type systems [23, 8] use a similar approach to integrating static and dynamic typing with type infer- ence, but with a different goal: to allow programs written in dynamically typed languages to leverage the benefits of static typing. Soft type systems use type inference to determine where runtime type checks must be inserted into dynamically typed programs so they are well-typed in some static type system. Cartwright and Fagan [23] reconstruct types for their language and determine where checks are needed using circular unification on equality constraints [105] over an encoding of the supertypes of each type in the program which factors out subtyping [69]. Aiken et al. [8] adapt subset constraint generation to soft typing with similar results. The runtime checks, or narrowers, used in soft typing are similar to transient checks: a narrower checks that a value corresponds to a specific type constructor. The value is returned unmodified if so, and an error is raised if not. Narrowers serve a different pur- pose than checks, however: narrowers let programs written in dynamically typed languages pass static typechecking for optimization, while checks enforce the programmer’s claims about the types of terms. The constraint system used by Flow’s type inference system, described for the Flowcore calculus by Chaudhuri et al. [25], reasons about tags and tagchecks in order to find sound typings for JavaScript

programs, though it relies on and trusts type annotations at module boundaries. Likewise Guha et al. [44] relate type tags and types similarly to our approach and their tagchecks are equivalent to transient checks, but their goal is to insert the tagchecks needed to type function bodies with respect to their (trusted) annotations, rather than to detect violations of those annotations by untrusted callers. Rastogi et al. [67] present an approach to optimizing gradually typed programs by inferring more precise types for program annotations, while preserving the program’s semantics. Our approach to ensuring interoperability is based on theirs: visible variables in the overall type of a program and their co- or con- travariant positions encodes escape analysis, and solutions that can soundly interoperate with arbitrary code can be generated by adding constraints on these variables. Our constraints, however, are based on subtyping rather than on consistency, because our constraints arise from checks and from elimination forms rather than from casts, which are appropriate for guarded gradual typing rather than transient.

8. Conclusions

Gradual typing allows programmers to combine static and dynamic typing in the same language, but allowing interaction between static and dynamic code while ensuring soundness incurs a runtime cost. Takikawa et al. [90] found that this cost can be serious obstacle to the practical use of Typed Racket, a popular gradually typed language which uses the guarded approach to gradual typing. In this paper, we perform a detailed performance analysis of the transient approach in Reticulated Python by ana- lyzing configurations from the typing lattices of ten benchmarks. We show that, in combination with the standard Python interpreter, performance under the transient design degrades as programs evolve from dynamic to static types, to a maximum of 6

×

slowdown compared to equivalent untyped Python programs. To reduce this overhead, we use an static type inference algorithm based on subtyping and check constraints to optimize programs after transient checks are inserted. This allows many redun- dant checks to be removed. We evaluated the performance of this approach with an implementation in Reticulated Python and found that performance across the typing lattices of our benchmarks improved to nearly the efficiency of the original programs—a very promising result for the practicality of gradual

typing. Finally, we re-analyzed our Reticulated Python benchmarks using PyPy, a tracing JIT, as a back- end, and found that it produced good performance even without type inference, and that it displayed a no overhead when used in combination with our static optimization.

In document Gradual Typing for Python, Unguarded (Page 196-200)