Type-Level Computations for Ruby Libraries
Milod Kazerounian Sankha Narayan Guria Niki Vazou University of Maryland University of Maryland IMDEA Software Institute College Park, Maryland, USA College Park, Maryland, USA Madrid, Spain [email protected] [email protected] [email protected] Jeffrey S. Foster David Van Horn Tufts University University of Maryland Medford, Massachusetts, USA College Park, Maryland, USA [email protected] [email protected]
Abstract types depend on values. Yet this case occurs surprisingly Many researchers have explored ways to bring static typing often, especially in Ruby libraries. For example, consider the to dynamic languages. However, to date, such systems are following database query, written for a hypothetical Ruby not precise enough when types depend on values, which on Rails (a web framework, called Rails henceforth) app: often arises when using certain Ruby libraries. For example, Person.joins (:apartments).where ({ name: ' Alice ' , age: 30, the type safety of a database query in Ruby on Rails depends apartments: {bedrooms: 2}}) on the table and column names used in the query. To address This query uses the ActiveRecord DSL to join two database this issue, we introduce CompRDL, a type system for Ruby tables, people1 and apartments, and then filter on the values that allows library method type signatures to include type- of various columns (name, age, bedrooms) in the result. level computations (or comp types for short). Combined with We would like to type check such code, e.g., to ensure singleton types for table and column names, comp types let the columns exist and the values being matched are of the us give database query methods type signatures that com- right types. But we face an important problem: what type pute a table’s schema to yield very precise type information. signature do we give joins? Its return type—which should Comp types for hash, array, and string libraries can also in- describe the joined table—depends on the value of its argu- crease precision and thereby reduce the need for type casts. ment. Moreover, for n tables, there are n2 ways to join two We formalize CompRDL and prove its type system sound. of them, n3 ways to join three of them, etc. Enumerating all Rather than type check the bodies of library methods with these combinations is impractical. comp types—those methods may include native code or be To address this problem, in this paper we introduce Comp- complex—CompRDL inserts run-time checks to ensure li- RDL, which extends RDL [18], a Ruby type system, to include brary methods abide by their computed types. We evaluated method types with type-level computations, henceforth re- CompRDL by writing annotations with type-level computa- ferred to as comp types. More specifically, in CompRDL we tions for several Ruby core libraries and database query APIs. can annotate library methods with type signatures in which We then used those annotations to type check two popular Ruby expressions can appear as types. During type check- Ruby libraries and four Ruby on Rails web apps. We found ing, those expressions are evaluated to produce the actual the annotations were relatively compact and could success- type signature, and then typing proceeds as usual. For ex- fully type check 132 methods across our subject programs. ample, for the call to Person.joins, by using a singleton type Moreover, the use of type-level computations allowed us for :apartments, a type-level computation can look up the arXiv:1904.03521v1 [cs.PL] 6 Apr 2019 to check more expressive properties, with fewer manually database schemas for the receiver and argument and then inserted casts, than was possible without type-level com- construct an appropriate return type.2 putations. In the process, we found two type errors and a Moreover, the same type signature can work for any model documentation error that were confirmed by the developers. class and any combination of joins. And, because CompRDL Thus, we believe CompRDL is an important step forward in allows arbitrary computation in types, CompRDL type sig- bringing precise static type checking to dynamic languages. natures have access to the full, highly dynamic Ruby envi- ronment. This allows us to provide very precise types for the Keywords type-level computations, dynamic languages, large set of Rails database query methods. It also lets us give types, Ruby, libraries, database queries precise types to methods of finite hash types (heterogeneous hashes), tuple types (heterogeneous arrays), and const string 1 Introduction 1Rails knows the plural of person is people. There is a large body of research on adding static typing 2The use of type-level computations and singleton types could be considered to dynamic languages [2–4, 21, 28, 36, 37, 42–44]. However, dependent typing, but as our type system is much more restricted we existing systems have limited support for the case when introduce new terminology to avoid confusion (see § 2.4 for discussion). types (immutable strings), which can help eliminate type type checking these benchmarks required 4.75× fewer type casts that would otherwise be required. cast annotations compared to standard types, demonstrating Note that in all these cases, we apply comp types to library comp types’ increased precision. (§5 contains the results of methods whose bodies we do not type check, in part to avoid our evaluation.) complex, potentially undecidable reasoning about whether Our results suggest that using type-level computations a method body matches a comp type, but more practically provides a powerful, practical, and precise way to statically because those library methods are either implemented in type check code written in dynamic languages. native code (hashes, arrays, strings) or are complex (database queries). This design choice makes CompRDL a particularly 2 Overview practical system which we can apply to real-world programs. The starting point for our work is RDL [18], a system for To maintain soundness, we insert dynamic checks to ensure adding type checking and contracts to Ruby programs. RDL’s that these methods abide by their computed types at runtime. type system is notable because type checking statically an- (§2 gives an overview of typing in CompRDL.) alyzes source code, but it does so at runtime. For example, C We introduce λ , a core, object-oriented language that for- line6 in Figure 1a gives a type signature for the method C malizes CompRDL type checking. In λ , library methods can defined on the subsequent line. This “annotation” is actu- be declared with signatures of the form (a<:e1/A1) → e2/A2, ally a call to the method type,3 which stores the type signa- where A1 and A2 are the conventional (likely overapproxi- ture in a global table. The type annotation includes a label mate) argument and return types of the method. The precise :model. (In Ruby, strings prefixed by colon are symbols, which argument and return types are determined by evaluating are interned strings.) When the program subsequently calls e1 and e2, respectively, and that evaluation may refer to the RDL.do_typecheck :model (not shown), RDL will type check C type of the receiver and the type a of the argument. λ also the source code of all methods whose type annotations are performs type checking on e1 and e2, to ensure they do not go labeled :model. C wrong. To avoid potential infinite recursion, λ does not use This design enables RDL to support the metaprogramming type-level computations during this type checking process, that is common in Ruby and ubiquitous in Rails. For exam- instead using the conventional types for library methods. Fi- ple, the programmer can perform type checking after meta- C nally, λ includes a rewriting step to insert dynamic checks programming code has run, when corresponding type defini- to ensure library methods abide by their computed types. We tions are available. See Ren and Foster [36] for more details. C prove λ ’s type system is sound. (See §3 for our formalism.) We note that while CompRDL benefits from this runtime We implemented CompRDL on top of RDL, an existing type checking approach—we use RDL’s representation of Ruby type checker. Since CompRDL can include type-level types in our CompRDL signatures, and our subject programs computation that relies on mutable values, CompRDL inserts include Rails apps—there is nothing specific in the design of additional runtime checks to ensure such computations eval- comp types that relies on it, and one could implement comp uate to the same result at method call time as they did at type types in a fully static system. checking time. Additionally, CompRDL uses a lightweight analysis to check that type-level computations (and thus type 2.1 Typing Ruby Database Queries checking) terminate. The termination analysis uses purity While RDL’s type system is powerful enough to type check effects to check that calls that invoke iterator methods—the Rails apps in general, it is actually very imprecise when main source of looping in Ruby, in our experience—do not reasoning about database (DB) queries. For example, consider mutate the receiver, which could introduce non-termination. Figure 1a, which shows some code from the Discourse app. Finally, we found that several kinds of comp types we devel- Among others, this app uses two tables, users and emails, oped needed to include weak type updates to handle muta- whose schemas are shown on lines2 and3. Each user has an tion in Ruby programs. (§4 describes our implementation in id, a username, and a flag indicating whether the account more detail.) was staged. Such staged accounts were created automatically We evaluated CompRDL by first using it to write type by Discourse and can be claimed by the email address owner. annotations for 482 Ruby core library methods and 104 Rails An email has an id, the email address, and the user_id of the database query methods. We found that by using helper user who owns the email address. methods, we could write very precise type annotations for Next in the figure, we show code for the class User, which all 586 methods with just a few lines of code on average. is a model, i.e., instances of the class correspond to rows in Then, we used those annotations to type check 132 methods the users table. This class has one method, available?, which across two Ruby APIs and four Ruby on Rails web apps. We returns a boolean indicating whether the username and email were able to successfully type check all these methods in ap- address passed as arguments are available. The method first proximately 15 seconds total. In doing so, we also found two checks whether the username was already reserved (line8, type errors and a documentation error, which we confirmed with the developers. We also found that, with comp types, 3In Ruby, parentheses in a method call are optional. 2 1 # Table Schema Unfortunately, the exists? call on line 11 is another story. 2 # users:{ id: Integer, username: String, staged: bool} Notice that this query calls exists? on the result of User.- 3 # emails:{ id: Integer, email: String, user_id: Integer} joins(:emails). Thus, to give exists? a type with the right 4 column information, we need to have that information re- 5 class User < ActiveRecord::Base 6 type "( String, String ) → %bool", typecheck: :model flected in the return type of joins. Unfortunately, there is no 7 def self.available? (name, email) reasonable way to do this in RDL, because the set of columns 8 return false if reserved? (name) in the table returned by joins depends on both the receiver 9 return true if !User.exists? ({ username: name}) and the value of the argument. We could in theory overload 10 # staged user accounts can be claimed joins with different return types depending on the argument 11 return User.joins ( :emails ) .exists? ({ staged: true, type—e.g., we could say that User.joins returns a certain type username: name, emails: { email: email }}) when the argument has singleton type :emails. However, we 12 end would need to generate such signatures for every possible 13 end way of joining two tables together, three tables together, etc., which quickly blows up. Thus, currently, RDL types this par- (a) Discourse code (uses ActiveRecord). ticular exists? call as taking a Hash
′ Γ ⊢CT e ,→ e : A CT(A.m) = A1 → A2 A.m ∈ U ′ Γ ⊢CT ex ,→ ex : Ax Ax ≤ A1 C-Type ′ ′ C-AppUD Γ ⊢CT A ,→ A : Type Γ ⊢CT e.m(ex ) ,→ e .m(ex ) : A2
′ Γ ⊢CT e ,→ e : A CT(A.m) = (a<:et1/At1) → et2/At2 ′ A.m ∈ L Γ ⊢CT ex ,→ ex : Ax ′ ′ Γ ⊢CT e ,→ e : A a:Type, tself:Type ⊢ CT et1 ,→ et1 : Type T′ U CT(A.m) = A1 → A2 ⟨[a 7→ Ax ][tself 7→ A], et1⟩ ⇓ A1 Ax ≤ A1 ′ A.m ∈ L Ax ≤ A1 a:Type, tself:Type ⊢ CT et2 ,→ et2 : Type ′ T U ′ Γ ⊢CT ex ,→ ex : Ax ⟨[a 7→ Ax ][tself 7→ A], et2⟩ ⇓ A2 ′ ′ C-AppLib ′ ′ C-App-Comp Γ ⊢CT e.m(ex ) ,→ ⌈A2⌉e .m(ex ) : A2 Γ ⊢CT e.m(ex ) ,→ ⌈A2⌉e .m(ex ) : A2
Figure 5. A subset of the type checking and rewriting rules for λC .
Expressions e include values v and variables x and a. By use dynamic environments E, defined in Figure4, which map convention, we use the former in regular program expres- variables to values. We define the relation ⟨E, e⟩ ⇓ e ′, mean- sions and the latter in comp types. The special variable self ing the expression e evaluates to e ′ under dynamic environ- names the receiver of a method call, and the special variable ment E. The full evaluation rules use a stack as wellA, but tself names the type of the receiver in a comp type. New we omit the stack here for simplicity. object instances are created with A.new. Expressions also include sequences e; e, conditionals if e then e else e, and Example. As an example comp type in the formalism, con- method calls e.m(e), where, to simplify the formalism, meth- sider type checking the expression true. ∧ (true), where ods take one argument. Finally, our type system translates the ∧ method returns the logical conjunction of the receiver calls to library methods into checked method calls ⌈A⌉e.m(e), and argument. Standard type checking would assign this which checks at run-time that the value returned from the expression the type Bool. However, with comp types we can call has type A. We assume this form does not appear in the do better. surface syntax. Recall that true and false are members of the singleton We assume the classes form a lattice with Nil as the bottom types True and False. Thus, we can write a comp type for ∧ and Obj as the top. We write the least upper bound of A1 and the method that yields a singleton return type when the Bool A2 as A1 ⊔ A2. For simplicity, we assume the lattice correctly arguments are singletons, and in the fallback case: models the program’s classes, i.e., if A ≤ A′, then A is a subclass of A′ by the usual definition. Lastly, three of the built- lib Bool. ∧ (x) : (a<:Bool/Bool) → ( in classes, Nil, True, and False, are singleton types, i.e., they if (tself == True). ∧ (a == True) then True contain only the values nil, true, and false, respectively. else if (tself == False). ∨ (a == False) then False Extending λC with support for more kinds of singleton types else Bool)/Bool is straightforward. The first two lines of the condition handle the singleton cases, Method Types σ are of the form A′ → A where A′ and A and the last line is the fallback case. are the domain and range types, respectively. Library Method Types δ are either method types or have the form (a<:e ′/ 3.2 Type Checking and Rewriting A′) → e/A, where e ′ and e are expressions that evaluate to C types and that can refer to the variables a and tself. The Figure5 gives a subset of the rules for type checking λ and C base types A′ and A provide an upper bound on the respective rewriting λ to insert dynamic checks at library calls. The expression types, i.e., for any a, expressions e ′ and e should remaining rules, which are straightforward, can be found evaluate to subtypes of A′ and A, respectively. These upper in AppendixA. These rules use two additional definitions bounds are used for type checking comp types (§ 3.2). from Figure4. Type environments Γ map variables to base Finally, programs are sequences of method definitions and types, and the class table CT maps methods to their type library method declarations. signatures. We omit the construction of class tables, which is standard. We also use disjoint sets U and L to refer to the Dynamic Semantics. The dynamic semantics of λC are the user-defined and library methods, respectively. small-step semantics of Ren and Foster [36], modified to The rules in Figure5 prove judgments of the form Γ ⊢CT throw blame (§ 3.3) when a checked method call fails. They e ,→ e ′ : A, meaning under type environment Γ and class 7 table CT, source expression e is rewritten to target expression Blame. The type system of λC does not prevent null-pointer e ′, which has type A. errors, i.e., nil has no methods yet we allow it to appear Rule (C-Type) is straightforward: any class ID A that is wherever any other type of object is expected. We encode used as a value is rewritten to itself, and it has type Type. We such errors as blame. We also reduce to blame when a dy- include this rule to emphasize that types are values in λC . namic check of the form ⌈A′⌉A.m(v) fails. Rule (C-AppUD) finds the receiver type A, then looks up A.m in the class table. This rule only applies when A.m is Program Checking and CT. In the AppendixA we pro- vide type checking rules not just for λC expressions but also user-defined and thus has a (standard) method type A1 → A2. Then, as is standard, the rule checks that the argument’s type for programs P. These rules are where we actually check user-defined methods against their types. We also definea Ax is a subtype of A1, and the type of the whole call is A2. notion of validity for a class table CT with respect to P, which This rule rewrites the subexpressions e and ex , but it does not itself insert any new checks, since user-defined methods enforces that CT’s types for methods and fields match the are statically checked against their type signatures (rule not declared types in P, and that appropriate subtyping relation- shown). ships hold among subclasses. Given a well typed program P, Rule (C-AppLib) is similar to Rule (C-AppUD), except it it is straightforward to construct a valid CT. applies when the callee is a library method. In this case, the Type Checking Rules. In addition to the type checking and rule inserts a check to ensure that, at run-time, the library rewriting rules of Figure5, we define a separate judgment method abides by its specified type. Γ ⊢ e : A that is identical to Γ ⊢ e ,→ e : A except it C CT CT Rule (C-App-Comp) is the crux of λ ’s type checking sys- omits the rewriting step, i.e., only performs type checking. tem. It applies at a call to a library method A.m that uses a We can then prove soundness of the judgment Γ ⊢CT e : A type-level computation, i.e., with a type signature (a<:et1/ using preservation and progress, and finally prove soundness At1) → et2/At2. The rule first type checks and rewrites of the type checking and rewriting rules as a corollary: et1 and et2 to ensure they will evaluate to a type (i.e., have type Type). These expressions may refer to a and tself, Theorem 3.1 (Soundness). For any expressions e and e’, type A, which themselves have type Type. The rule then evaluates class table CT, and program P such that CT is valid with re- ′ ′ the rewritten et1 and et2 using the dynamic semantics men- spect to P, if ∅ ⊢CT e ,→ e : A then e either reduces to a value, tioned above to yield types A1 and A2, respectively. Finally, reduces to blame, or does not terminate. the rule ensures that the argument ex has a subtype of A1; sets the return type of the whole call to A2; and inserts a 4 Implementation dynamic check that the call returns an A2 at runtime. For We implemented CompRDL as an extension to RDL, a type instance, the earlier example of the use of logical conjunction checking system for Ruby [18, 36, 37, 40]. In total, CompRDL would be rewritten to ⌈True⌉true. ∧ (true). comprises approximately 1,170 lines of code added to RDL. There is one additional subtlety in Rule (C-App-Comp). RDL’s design made it straightforward to add comp types. Recall the example above that gives a type to Bool.∧. Notice We extended RDL so that, when type checking method calls, that the type-level computation itself uses Bool.∧. This could type-level computations are first type checked to ensure potentially lead to infinite recursion, where calling Bool.∧ re- they produce a value of type Type and then are executed to quires checking that Bool.∧ produces a type, which requires produce concrete types, which are then used in subsequent recursively checking that Bool.∧ produces a type etc. type checking. Comp types use RDL’s contract mechanism To avoid this problem, we introduce a function CT that to insert dynamic checks for comp types. rewrites class table CT to drop all annotations withT U type- C level expressions. More precisely, any comp type (a<:e1/ Heap Mutation. For simplicity, λ does not include a heap. A1) → e2/A2 is rewritten to A1 → A2. Then type checking By contrast, CompRDL allows arbitrary Ruby code to appear type-level computations, in the fifth and eighth premise of in comp types. This allows great flexibility, but it means such (C-App-Comp), is done under the rewritten class table. code might depend on mutable state that could change be- Note that, while this prevents the type checking rules from tween type checking and the execution of a method call. For infinitely recursing, it does not prevent type-level expres- example, in Figure1, type-level code uses the global table sions from themselves diverging. In λC , we assume this does RDL.db_schema. If, after type checking the method avail- not happen, but in our implementation, we include a simple able?, the program (pathologically) changed the schema of termination checker that is effective in practice (§4). User to drop the username column, then available? would fail at runtime even though it had type checked. The dynamic C 3.3 Properties of λ . checks discussed in §2 and §3 are insufficient to catch this Finally, we prove type soundness for λC . For brevity, we issue, because they only check a method call against the provide only the high-level description of the proof. The initial result of evaluating a comp type; they do not consider details can be found in AppendixA. that the same comp type might yield a new result at runtime. 8 1 type :m1, ..., terminates: :+ We believe it is reasonable to forbid the use of built-in loop 2 type :m2, ..., terminates: :+ constructs, and to assume no recursion, because in practice 3 type :m3, ..., terminates: :− most iteration in Ruby occurs via methods that iterate over 4 a structure. For instance, array.map {block } returns a new 5 type Array, :map, ..., terminates: :blockdep 6 type Array, :push, ..., pure: :− array in which the block, a code block or lambda, has been 7 applied to each element of array. Since arrays are by defini- 8 def m1() tion finite, this call terminates as long as block terminates 9 m2() # allowed: m2 terminates and does not mutate the array. A similar argument holds 10 m3() # not allowed: m3 may not terminate other iterators of Array, Hash, etc. 11 while ... end # not allowed: looping Thus, CompRDL checks termination of iterators as fol- 12 lows. Iterator methods can be annotated with the special 13 array = [1,2,3] # create new array termination effect :blockdep (Line5), indicating the method 14 array.map { | val | val+1 } # allowed terminates if its block terminates and is pure. CompRDL also 15 array.map { | val | array.push (4) } includes purity effect annotations indicating whether meth- 16 # not allowed: iterator calls impure method push ods are pure (:+) or impure (:−). A pure method may not 17 end write to any instance variable, class variable, or global vari- able, or call an impure method. CompRDL determines that a Figure 6. Termination Checking with CompRDL. :blockdep method terminates as long as its block argument is pure, and otherwise it may diverge. Using this approach, To address this issue, CompRDL extends dynamic checks CompRDL will allow Line 14 but reject reject Line 15. to ensure types remain the same between type checking and Type Mutations and Weak Updates Finally, to handle execution. If a method call is type checked using a comp type, aliasing, our type annotations for Array, Hash, and String then prior to that call at runtime, CompRDL will reevaluate need to perform weak updates to type information when that same comp type on the same inputs. If it evaluates to tuple, finite hash, and const string types, respectively, are a different type, CompRDL will raise an exception to signal mutated. For example, consider the following code: a potential type error. An alternative approach would be to a = [1, ' foo ' ] ; if...then b = a else...end ; a[0] ='one' re-check the method under the new type. Here (ignoring singleton types for simplicity), a initially has Of course, the evaluation of a comp type may itself al- the type t = [Integer, String] , where t is a Ruby object, ter mutable state. Currently, CompRDL assumes that comp specifically an instance of RDL’s TupleType class. At the join type specifications are correct, including any mutable com- point after the conditional, the type of b will be a union of t putations they may perform. If a comp type does have any and its previous type. erroneous effects, program execution could fail in an un- We could potentially forbid the assignment to a[0] be- predictable manner. Other researchers have proposed safe- cause the right-hand side does not have the type Integer. guards for this issue of effectful contracts by using guarded However, this is likely too restrictive in practice. Instead, we locations [15] or region based effect systems38 [ ]. We leave would like to mutate t after the write. However, b shares this incorporating such safeguards for comp types as future work. type. Thus we perform a weak update: after the assignment We note, however, that this issue did not arise in any comp we mutate t to be [Integer ∪String, String] , to handle the types we used in our experiments. cases when a may or may not have been assigned to b. Termination of Comp Types. A standard property of type For soundness, we need to retroactively assume t was checkers is that they terminate. However, because comp always this type. Fortunately, for all tuple, finite hash, and types allow arbitrary Ruby code, CompRDL could potentially const string types τ , RDL already records all asserted con- ′ ′ lose this property. To address this issue, CompRDL includes straints τ ≤ τ and τ ≤ τ to support promotion of tuples, a lightweight termination checker for comp types. finite hashes, and const strings to types Array, Hash, and Figure6 illustrates the ideas behind termination checking. String, respectively [18]. We use this same mechanism to In CompRDL, methods can be annotated with termination replay previous constraints on these types whenever they effects :+, for methods that always terminate (e.g., m1 and are mutated. For example, if previously we had a constraint m2) and :− for methods that might diverge (e.g., m3). Comp- α ≤ [Integer, String], and subsequently we mutated the RDL allows terminating methods to call other terminating latter type to [Integer∪String, String], we would “replay” methods (Line9) but not potentially non-terminating meth- the original constraint as α ≤ [Integer ∪ String, String]. ods (Line 10). Additionally, terminating methods may not use loops (Line 11). CompRDL assumes that type-level code 5 Experiments does not use recursion, and leave checking of recursion to We evaluated CompRDL by writing comp type annotations future work. for a number of Ruby core and third party libraries (§ 5.1) 9 Table 1. Library methods with comp type definitions. annotations for 586 methods across these libraries, compris- ing 1447 lines of type-level code and using 83 helper methods. Comp Type Ruby Helper Once written, these comp types can be used to type check as Library Definitions LoC Methods many of the libraries’ clients as we would like, making the Ruby Core Library effort of writing them potentially very worthwhile. Array 114 215 15 Hash 48 247 15 5.2 Benchmarks String 114 178 12 Float* 98 12 1 We evaluated CompRDL by type checking methods from Integer* 108 12 1 two popular Ruby libraries and four Rails web apps: Database DSL • Wikipedia Client [14] is a Ruby wrapper library for the ActiveRecord 77 375 18 Sequel 27 408 22 Wikipedia API. Total 586 1447 83 • Twitter Gem [31] is a Ruby wrapper library for the ∗Helper methods for Float and Integer are shared. Twitter API. • Discourse [23] is an open-source discussion platform built on Rails. It uses ActiveRecord. and using these types to type check real-world Ruby appli- • Huginn [22] is a Rails app for setting up agents that cations (§ 5.2). We discuss the results of type checking these monitor the web for events and perform automated benchmarks, including the type errors we found in the pro- tasks in response. It uses ActiveRecord. cess (§ 5.3). In all, we wrote 586 comp type annotations for • Code.org [12] is a Ruby app that powers code.org, a site Ruby library methods, used them to type check 132 methods that encourages people, particularly students, to learn across six Ruby apps, found three bugs in the process, and programming. It uses a combination of ActiveRecord used significantly fewer manually inserted type casts than and Sequel. are needed using RDL. • Journey [6] is a web application that provides a graph- ical interface to create surveys and collect responses 5.1 Library Types from participants. It uses a combination of ActiveRe- Table1 details the library type annotations we wrote. cord and Sequel. We chose to define comp types for these libraries dueto We selected these benchmarks because they are popular, their popularity and because, as discussed in §2, they are well-maintained, and make extensive use of the libraries amenable to precise typing with comp types. These types noted in § 5.1. More specifically, the APIs often work with were written based on the libraries’ documentation as well hashes representing JSON objects received over HTTP, and as manual testing to ensure type specifications matched as- the Rails apps rely heavily on database queries. sociated method semantics. Since CompRDL performs type checking, we must provide • Ruby core libraries: These are libraries that are written a type annotation for any method we wish to type check. Our in C and automatically loaded in all Ruby programs. subject programs are very large, and hence annotating all of We annotate the methods from the Array, Hash, String, the programs’ methods is infeasible. Instead, we focused on Integer, and Float classes. methods for which comp types would be most useful. • ActiveRecord: ActiveRecord is the most used object- In Wikipedia, we annotated the entire Page API. To sim- relational model (ORM) DSL of the Ruby on Rails web plify type checking slightly, we changed the code to replace framework. We wrote comp types for ActiveRecord string hash keys with symbols, since RDL’s finite hash types database query methods. do not currently support string keys. In Twitter, we anno- • Sequel: Sequel is an alternative database ORM DSL. It tated all the methods of stream API bindings that made use offers some more expressive queries than are available of methods with comp types. in ActiveRecord. In Discourse and Huginn, we chose several larger Rails Table1 lists the number of methods for which we defined model classes, such as a User class that represents database comp types in each library and the number of Ruby lines of rows storing user information. In Code.org and Journey, we code (LoC) implementing the type computation logic. The type checked all methods that used Sequel to query the data- LoC count was calculated with sloccount [46] and does not base. Within the selected classes for these four Rails apps, include the line of the type annotation itself. we annotated a subset of the methods that query the data- In developing comp types for these libraries, we discov- base using features that CompRDL supports. The features ered that many methods have the same type checking logic. CompRDL does not currently support include the use of Rails This helped us write comp types for entire libraries using a scopes, which are essentially macros for queries, and the use few common helper methods. In total, we wrote comp type of SQL strings for methods other than where. 10 Table 2. Type checking results.
Extra Casts Time (s) Test Time Test Time Program Meths LoC Casts Errs Annots. (RDL) Median ± SIQR No Chk (s) w/Chk. (s) API client libraries Wikipedia 16 47 3 1 13 0.06 ± 0.00 6.3 ± 0.13 6.32 ± 0.11 0 Twitter 3 29 11 3 8 0.02 ± 0.00 0.07 ± 0.00 0.08 ± 0.00 0 Rails Applications Discourse 36 261 32 13 22 7.77 ± 0.39 80.24 ± 0.63 81.04 ± 0.34 0 Huginn 7 54 6 3 6 2.46 ± 0.29 4.30 ± 0.21 4.59 ± 0.48 0 Code.org 49 530 53 3 68 0.49 ± 0.01 2.49 ± 0.13 2.74 ± 0.02 1 Journey 21 419 78 14 59 4.12 ± 0.08 4.52 ± 0.22 4.76 ± 0.24 2 Total 132 1340 183 37 176 14.93 ± 0.77 97.93 ± 1.31 99.53 ± 1.20 3
Finally, because CompRDL performs type checking at run- Type checking accessed those constants, hence the method time (see § 2.1), we must first load each benchmark before creation was included in the type checking time. type checking it. We ran the type checker immediately after The next two columns show the performance overhead loading a program and its associated type annotations. of the dynamic checks inserted by CompRDL. We selected a subset of each app’s test suite that directly tested the type 5.3 Results checked methods, and ran these tests without (“No Chk”) Table2 summarizes our type checking results. In the first and with (“w/Chk”) the dynamic checks. In aggregate (last group of columns, we list the number of type checked meth- row), checks add about 1.6% overhead, which is minimal. ods and the total lines of code (computed with sloccount) Errors Found. Finally, the last column lists the number of of these methods. The third column lists the number of ad- errors found in each program. We were somewhat surprised ditional annotations we wrote for any global and instance to find any errors in large, well-tested applications. Wefound variables referenced in the method, as well as any methods three errors. In Code.org, the current_user method was doc- called that were not themselves selected for type checking. umented as returning a User. We wrote a matching Comp- The last column in this group lists the number of type casts RDL annotation, and CompRDL found that the returned we added. Many of these type casts were to the result of expression—whose typing involved a comp type—has a hash JSON.parse, which returns a nested Hash/Array data struc- type instead. We notified the Code.org developers, and they ture depending on its string input. Most of the remaining acknowledged that this was an error in the method docu- casts are to refine types after a conditional test; it may be pos- mentation and made a fix. sible to remove these casts by adding support for occurrence In Journey, CompRDL found two errors. First, it found typing [26]. We further discuss type casts, in particular the a method that referenced an undefined constant Field. We reduced type casting burden afforded by comp types, below. notified the developers, who fixed the bug by changing the constant to Question::Field . This bug had arisen due to Increased Type Checking Precision. Recall from § 2.2 that namespace changes. Second, it found a method that included comp types can potentially reduce the need for programmer- a call with an argument { :action ⇒ prompt, ... } which is inserted type casts. The next column reports how many casts a hash mapping key :action to prompt. The value prompt is were needed using normal RDL (i.e., no comp types). As supposed to be a string or symbol, but as it has neither quotes shown, approximately 4.75× fewer casts were needed when nor begins with a colon, it is actually a call to the prompt using comp types. This reflects the significantly increased method, which returns an array. The developers confirmed precision afforded by comp types, which greatly reduces the this bug. programmer’s annotation burden. When type checking the aforementioned methods in RDL Performance. The next group of columns report perfor- (i.e., without comp types), two out of three of the bugs are mance. First we give the type checking time as the median hidden by other type errors which are actually false positives. and semi-interquartile range (SIQR) of 11 runs on a 2017 These errors can be removed by adding four type casts, which MacBook Pro with a 2.3GHz i5 processor and 8GB RAM. In would then allow us to catch the true errors. With CompRDL, total, we type checked 132 methods in approximately 15 sec- however, we do not need any casts to find the errors. onds, which we believe to be reasonable. Discourse took most of the total time (8 out of 15 seconds). The reason turned out 6 Related Work to be a quirk of Discourse’s design: it creates a large number Types For Dynamic Languages. There is a large body of of methods on-the-fly when certain constants are accessed. research on adding static type systems to dynamic languages, 11 including Ruby [21, 36, 37], Racket [43, 44], JavaScript [3, 28, does not include more intricate queries like joins, which are 42], and Python [2, 4]. To the best of our knowledge, this supported in CompRDL. research does not use type-level computations. Dependent typing systems for dynamic languages have New Languages for Database Queries. Domain-specific been explored as well. Ou et al.[33] formally model type- languages have long been used to write programs with correct- level computation along with effects for a dynamic language. by-construction, type safe queries. Leijen and Meijer [27] Other projects have sought to bring dependent types to exist- implement Haskell/DB, an embedded DSL that dynamically ing dynamic languages, primarily in the form of refinement generates SQL in Haskell. Karakoidas et al.[24] introduce types [20], which are base types that are refined with expres- J%, a Java extension for embedding DSLs into Java in an sive logical predicates. Refinement types have been applied extensible, type-safe, and syntax-checked way. Fowler and to Ruby [25], Racket [26], and JavaScript [11, 45]. In contrast Brady [19] use dependent types in the language Idris to en- to CompRDL, these systems focus on type checking meth- force safety protocols associated with common web program ods which themselves have dependent types. On the other features including database queries written in a DSL. hand, CompRDL uses type-level computations only for non- Language-integrated query is featured in languages like type checked library methods, allowing us to avoid checking LINQ [30] and Links [9, 13]. This approach allows program- comp types for equality or subtyping (§ 2.4). While sacrific- mers to write database queries directly within a statically- ing some expressiveness, this makes CompRDL especially typed, general purpose language. practical for real-world programs. In contrast to new DSLs and language-integrated query, Turnstile [8] is a metalanguage, hosted in Racket, for cre- our focus in on bringing type safety to an existing language ating typed embedded languages. It lets an embedded DSL and framework rather than developing a new one. author write their DSL’s type system using the host lan- guage macro system. There is some similarity to CompRDL, Dependent Types. Traditional dependent type systems are where comp types manipulate standard RDL types. However, exemplified by languages such as Coq[34], Agda [32], and CompRDL types are not executed as macros (which do not F*[41]. These languages provide powerful type systems that exist in Ruby), but rather in standard Ruby so they have full allow programmers to prove expressive properties. However, access to the environment, e.g., so the joins type signature such expressive types may be too heavyweight for a dynamic can look up the DB schema. language like Ruby. As discussed in § 2.4, our work has fo- cused on applying a limited form of dependent types, where Types For Database Queries. There have been a number types depend on argument types and not arbitrary program of prior efforts to check the type safety of database queries. values, resulting in a system that is practical for real-world All of these target statically typed languages, an important Ruby programs. distinction from CompRDL. Haskell allows for light dependent typing using the com- Chlipala [10] presents Ur, a web-specific functional pro- bination of singleton types [16] and type families [7]. Comp- gramming language. Ur uses type-level computations over RDL’s singleton types are similar to Haskell’s, i.e., both lift- record types [35] to type check programs that construct and ing expressions to types, and comp types are analogous to run SQL queries. Indeed, CompRDL similarly uses type-level anonymous type families. However, unlike Haskell, Comp- computations over finite hash types (analogous to record RDL supports runtime evaluation during type checking, and types) to type check queries. To the best of our knowledge, thus does not require user-provided proofs. Ur focuses on computations over records. In contrast, Comp- Scala supports path dependent types, a limited form of RDL supports arbitrary type-level computations targeting type/term dependency in which types can depend on vari- unchecked library methods, making comp types more easily ables, but, as of Scala version 2, does not allow dependency extensible to checking new properties and new libraries. As on general terms [1]. This allows for reasoning about data- discussed in §2, for example, comp types can not only com- base queries. For example, the Scala library Slick [29], much pute the schema of a joined table, but also check properties like our approach, allows users to write database queries in like two joined tables having a declared Rails association. a domain specific language (a lifted embedding) and uses Further, comp types can be usefully applied to many libraries the query’s AST to type check the query using Scala’s path beyond database queries (§5). dependent types. Unlike CompRDL, Scala’s path dependent Similar to Ur, Baltopoulos et al.[5] makes use of record types do not allow the execution of the full host language types over embedded SQL tables. Using SMT-checked re- during type computations. finement types, they can statically verify expressive data in- tegrity constraints, such as the uniqueness of primary keys in a table and the validation of data inserted into a table. 7 Conclusion In addition to the contrast we draw with Ur regarding ex- We presented CompRDL, a system for adding type signatures tensibility of types, to the best of our knowledge, this work with type-level computations, which we refer to as comp 12 types, to Ruby library methods. CompRDL makes it possi- 32Nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming ble to write comp types for database queries, enabling us Languages (POPL ’05). ACM, New York, NY, USA, 1–13. https://doi. to type check such queries precisely. Comp type signatures org/10.1145/1040305.1040306 [8] Stephen Chang, Alex Knauth, and Ben Greenman. 2017. Type Systems can also be used for libraries over heterogeneous hashes As Macros. In Proceedings of the 44th ACM SIGPLAN Symposium on and arrays, and to treat strings as immutable when possi- Principles of Programming Languages (POPL 2017). ACM, New York, ble. The increased precision of comp types can reduce the NY, USA, 694–705. https://doi.org/10.1145/3009837.3009886 need for manually inserted type casts, thereby reducing the [9] James Cheney, Sam Lindley, Gabriel Radanne, and Philip Wadler. 2014. programmer’s burden when type checking. Since comp type- Effective Quotation: Relating Approaches to Language-integrated Query. In Proceedings of the ACM SIGPLAN 2014 Workshop on Par- annotated method bodies are not themselves type checked, tial Evaluation and Program Manipulation (PEPM). ACM, New York, CompRDL inserts run-time checks to ensure those methods NY, USA, 15–26. return their computed types. We formalized CompRDL as a [10] Adam Chlipala. 2010. Ur: Statically-typed Metaprogramming with core language λC and proved its type system sound. Type-level Record Computation. In Proceedings of the 31st ACM SIG- We implemented CompRDL on top of RDL, an existing PLAN Conference on Programming Language Design and Implementa- C tion (PLDI). ACM, New York, NY, USA, 122–133. type system for Ruby. In addition to the features of λ , our [11] Ravi Chugh, David Herman, and Ranjit Jhala. 2012. Dependent Types implementation includes run-time checks to ensure comp for JavaScript. In Proceedings of the ACM International Conference types that depend on mutable state yield consistent types. on Object Oriented Programming Systems Languages and Applications Our implementation also includes a termination checker (OOPSLA). ACM, New York, NY, USA, 587–606. for type-level code, and the type signatures we developed [12] Code.org. 2018. The code powering code.org and studio.code.org. (2018). https://github.com/code-dot-org/code-dot-org. perform weak updates to type certain mutable methods. [13] Ezra Cooper, Sam Lindley, Philip Wadler, and Jeremy Yallop. 2006. Finally, we used CompRDL to write comp types for several Links: Web Programming Without Tiers. In Proceedings of the 5th Ruby libraries and two database query DSLs. Using these International Conference on Formal Methods for Components and Objects type signatures, we were able to type check six popular (FMCO). Springer-Verlag, Berlin, Heidelberg, 266–296. Ruby apps and APIs, in the process discovering three errors [14] David Cyril and Ken Pratt. 2018. Ruby client for the Wikipedia API. (2018). https://github.com/kenpratt/wikipedia-client. in our subject programs. We also found that type checking [15] Christos Dimoulas, Sam Tobin-Hochstadt, and Matthias Felleisen. 2012. with comp types required 4.75× fewer type casts, due to Complete Monitors for Behavioral Contracts. In Proceedings of the the increased precision. Thus, we believe that CompRDL 21st European Conference on Programming Languages and Systems represents a practical approach to precisely type checking (ESOP’12). Springer-Verlag, Berlin, Heidelberg, 214–233. https://doi. programs written in dynamic languages. org/10.1007/978-3-642-28869-2_11 [16] Richard A. Eisenberg and Stephanie Weirich. 2012. Dependently Typed Programming with Singletons. In Proceedings of the 2012 Haskell Sym- Acknowledgments posium (Haskell ’12). ACM, New York, NY, USA, 117–130. https: Thanks to the anonymous reviewers for their helpful com- //doi.org/10.1145/2364506.2364522 ments. This research was supported in part by NSF CCF- [17] Cormac Flanagan. 2006. Hybrid Type Checking. In Conference Record of the 33rd ACM SIGPLAN-SIGACT Symposium on Principles of Pro- 1518844, CCF-1846350, DGE-1322106, and Comunidad de gramming Languages (POPL ’06). ACM, New York, NY, USA, 245–256. Madrid as part of the program S2018/TCS-4339 (BLOQUES- https://doi.org/10.1145/1111037.1111059 CM) co-funded by EIE Funds of the European Union. [18] Jeffrey Foster, Brianna Ren, Stephen Strickland, Alexander Yu,and Milod Kazerounian. 2018. RDL: Types, type checking, and contracts References for Ruby. (2018). https://github.com/plum-umd/rdl. [19] Simon Fowler and Edwin Brady. 2014. Dependent Types for Safe [1] Nada Amin, Karl Samuel Grütter, Martin Odersky, Tiark Rompf, and and Secure Web Programming. In Implementation and Application of Sandro Stucki. 2016. The Essence of Dependent Object Types. Springer Functional Languages (IFL ’13). ACM, New York, NY, USA, 49:49–49:60. International Publishing, Cham, 249–272. [20] Tim Freeman and Frank Pfenning. 1991. Refinement Types for ML. [2] Davide Ancona, Massimo Ancona, Antonio Cuni, and Nicholas D. Mat- SIGPLAN Not. 26, 6 (May 1991), 268–277. https://doi.org/10.1145/ sakis. 2007. RPython: A Step Towards Reconciling Dynamically and 113446.113468 Statically Typed OO Languages. In Proceedings of the 2007 Symposium [21] Michael Furr, Jong-hoon (David) An, Jeffrey S. Foster, and Michael on Dynamic Languages (DLS). ACM, New York, NY, USA, 53–64. Hicks. 2009. Static Type Inference for Ruby. In Proceedings of the 2009 [3] Christopher Anderson, Paola Giannini, and Sophia Drossopoulou. 2005. ACM Symposium on Applied Computing (SAC ’09). ACM, New York, Towards Type Inference for Javascript. In ECOOP 2005 - Object-Oriented NY, USA, 1859–1866. Programming (ECOOP). Springer Berlin Heidelberg, Berlin, Heidelberg, [22] Huginn. 2018. Huginn: Create agents that monitor and act on your 428–452. behalf. (2018). https://github.com/huginn/huginn. [4] John Aycock. 2000. Aggressive Type Inference. (2000). [23] Civilized Discourse Construction Kit Inc. 2018. Discourse: A platform [5] Ioannis G. Baltopoulos, Johannes Borgström, and Andrew D. Gor- for community discussion. (2018). https://github.com/discourse/ don. 2011. Maintaining Database Integrity with Refinement Types. discourse. In ECOOP 2011 – Object-Oriented Programming, Mira Mezini (Ed.). [24] Vassilios Karakoidas, Dimitris Mitropoulos, Panagiotis Louridas, and Springer Berlin Heidelberg, Berlin, Heidelberg, 484–509. Diomidis Spinellis. 2015. A Type-safe Embedding of SQL into Java [6] Nat Budin. 2018. Journey: An online questionnaire application. (2018). Using the Extensible Compiler Framework J%. Computer Languages, https://github.com/nbudin/journey/. Systems, and Structures 41, C (2015), 1–20. [7] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones, and Simon Marlow. 2005. Associated Types with Class. In Proceedings of the 13 [25] Milod Kazerounian, Niki Vazou, Austin Bourgerie, Jeffrey S. Foster, and SIGPLAN Conference on Programming Language Design and Implemen- Emina Torlak. 2018. Refinement Types for Ruby. In Verification, Model tation (PLDI). ACM, New York, NY, USA, 462–476. Checking, and Abstract Interpretation (VMCAI). Springer International [37] Brianna M. Ren, John Toman, T. Stephen Strickland, and Jeffrey S. Publishing, Cham, 269–290. Foster. 2013. The Ruby Type Checker. In Object-Oriented Program [26] Andrew M. Kent, David Kempe, and Sam Tobin-Hochstadt. 2016. Oc- Languages and Systems (OOPS) Track at ACM Symposium on Applied currence Typing Modulo Theories. In Proceedings of the 37th ACM Computing. ACM, Coimbra, Portugal, 1565–1572. SIGPLAN Conference on Programming Language Design and Implemen- [38] Taro Sekiyama and Atsushi Igarashi. 2017. Stateful Manifest Contracts. tation (PLDI). ACM, New York, NY, USA, 296–309. SIGPLAN Not. 52, 1 (Jan. 2017), 530–544. https://doi.org/10.1145/ [27] Daan Leijen and Erik Meijer. 1999. Domain Specific Embedded Com- 3093333.3009875 pilers. SIGPLAN Not. 35, 1 (Dec. 1999), 109–122. https://doi.org/10. [39] Jeremy Siek and Walid Taha. 2006. Gradual typing for functional lan- 1145/331963.331977 guages. In Seventh Workshop on Scheme and Functional Programming. [28] Benjamin S. Lerner, Joe Gibbs Politz, Arjun Guha, and Shriram Krish- ACM, Portland, OR, USA, 81–92. namurthi. 2013. TeJaS: Retrofitting Type Systems for JavaScript. In [40] T. Stephen Strickland, Brianna Ren, and Jeffrey S. Foster. 2014. Con- Proceedings of the 9th Symposium on Dynamic Languages (DLS). ACM, tracts for Domain-Specific Languages in Ruby. In Dynamic Languages New York, NY, USA, 1–16. Symposium (DLS). ACM, Portland, OR, 23–34. [29] Lightbend, Inc. 2019. Slick. (2019). http://slick.lightbend.com/. [41] Nikhil Swamy, Cătălin Hriţcu, Chantal Keller, Aseem Rastogi, An- [30] Erik Meijer, Brian Beckman, and Gavin Bierman. 2006. LINQ: Reconcil- toine Delignat-Lavaud, Simon Forest, Karthikeyan Bhargavan, Cédric ing Object, Relations and XML in the .NET Framework. In Proceedings Fournet, Pierre-Yves Strub, Markulf Kohlweiss, Jean-Karim Zinzindo- of the 2006 ACM SIGMOD International Conference on Management of houe, and Santiago Zanella-Béguelin. 2016. Dependent Types and Data (SIGMOD). ACM, New York, NY, USA, 706–706. Multi-monadic Effects in F*. In Proceedings of the 43rd Annual ACM [31] Erik Michaels-Ober, John Nunemaker, Wynn Netherland, Steve Richert, SIGPLAN-SIGACT Symposium on Principles of Programming Languages and Steve Agalloco. 2018. A Ruby interface to the Twitter API. (2018). (POPL ’16). ACM, New York, NY, USA, 256–270. https://github.com/sferik/twitter. [42] Peter Thiemann. 2005. Towards a Type System for Analyzing [32] Ulf Norell. 2009. Dependently Typed Programming in Agda. Springer JavaScript Programs. In Programming Languages and Systems. Springer Berlin Heidelberg, Berlin, Heidelberg, 230–266. Berlin Heidelberg, Berlin, Heidelberg, 408–422. [33] Xinming Ou, Gang Tan, Yitzhak Mandelbaum, and David Walker. 2004. [43] Sam Tobin-Hochstadt and Matthias Felleisen. 2006. Interlanguage Mi- Dynamic Typing with Dependent Types. In Exploring New Frontiers of gration: From Scripts to Programs. In Companion to the 21st ACM SIG- Theoretical Informatics, Jean-Jacques Levy, Ernst W. Mayr, and John C. PLAN Symposium on Object-oriented Programming Systems, Languages, Mitchell (Eds.). Springer US, Boston, MA, 437–450. and Applications (OOPSLA). ACM, New York, NY, USA, 964–974. [34] Benjamin C. Pierce, Arthur Azevedo de Amorim, Chris Casinghino, [44] Sam Tobin-Hochstadt and Matthias Felleisen. 2008. The Design and Im- Marco Gaboardi, Michael Greenberg, Catˇ alinˇ Hriţcu, Vilhelm Sjöberg, plementation of Typed Scheme. In Proceedings of the 35th Annual ACM and Brent Yorgey. 2017. Software Foundations. Electronic textbook, SIGPLAN-SIGACT Symposium on Principles of Programming Languages http://www.cis.upenn.edu/~bcpierce/sf. (POPL). ACM, New York, NY, USA, 395–406. [35] D. Rémy. 1989. Type Checking Records and Variants in a Natural [45] Panagiotis Vekris, Benjamin Cosman, and Ranjit Jhala. 2016. Refine- Extension of ML. In Proceedings of the 16th ACM SIGPLAN-SIGACT ment Types for TypeScript. In Proceedings of the 37th ACM SIGPLAN Symposium on Principles of Programming Languages (POPL ’89). ACM, Conference on Programming Language Design and Implementation New York, NY, USA, 77–88. https://doi.org/10.1145/75277.75284 (PLDI). ACM, New York, NY, USA, 310–325. [36] Brianna M. Ren and Jeffrey S. Foster. 2016. Just-in-time Static Type [46] D. A. Wheeler. 2018. SLOCCount. (2018). https://www.dwheeler.com/ Checking for Dynamic Languages. In Proceedings of the 37th ACM sloccount/.
14 A Appendix rule (E-AppUD) pushes the current environment and context This section contains the full definitions, static and dynamic onto the stack, we will push the current typing judgment on semantics, and proof of soundness for λC . to the type stack. Specifically, we will push an element ofthe ′ Figure7 once again presents the syntax of λC , as well as form (Γ[A], A ), where Γ is the environment of the current ′ some new auxiliary definitions to be used for defining the typing judgment; A is the type of the surrounding context; semantics and proving soundness. Here we see for the first and A is the type of expression v1.m(v2), i.e., the type that time object instances [A], which denote an instance of a class the method must return. A. The dynamic semantics rules are shown in Figure8. Most With this type stack, we can now define what it means for of the rules are standard. The slightly more intricate rule a type to be a subtype of the type stack, which is the crucial is (E-Context), which takes a step within a subexpression, preservation invariant we will prove: and it contains premises that differentiate it fromE-AppUD ( ), ′ Definition 2 (Stack subtyping). A0 ≤ (Γ[A], A ) :: TS if the other context cases, and ensure that the context C is A0 ≤ A. the largest possible context for purposes of disambiguation. ′ Figure9 once again defines the dynamic check insertion rules. Definition 3 (Stack consistency). Type stack element (Γ[A], A ) ′ Finally, Figure 10 defines the type checking rules, which are is consistent with dynamic stack element (E,C), written (Γ[A], A ) ∼ ′ largely a simplification of the check insertion rules; it isfor (E,C), if Γ ∼ E and Γ[□ 7→ A] ⊢CT C : A (Here we abuse these type checking rules that we prove preservation and notation and treat □ as if its a variable.) progress. We prove soundness of the check insertion rules Type stack TSis consistent with dynamic stack S, written as a corollary of the soundness of the type checking rules. TS ∼ S, is defined inductively as 1. · ∼ · A.1 Soundness 2. (Γ[A], A′) :: TS ∼ (E,C) :: S if We prove soundness of the type system of λC by first proving (a) (Γ[A], A′) ∼ (E,C) preservation and then progress. The proof of preservation is (b) TS ∼ S the more involved step here, and it requires a number of pre- (c) A′ ≤ TS if TS , · liminary definitions. First, we define a notion of consistency We will also make use of a notion of class table validity, between the type and dynamic environments: which tells us that a class table maps fields and methods to Definition 1 (Environmental consistency). Type environ- the "correct" types: ment Γ is consistent with dynamic environment E, written Γ ∼ E, if for all variables x, x ∈ dom(E) if and only if Definition 4 (Class table validity). Let A.m be an arbitrary x ∈ dom(Γ), and for all x ∈ dom(E) there exists A such that method. We say valid(CT) if ∈ U ∈ ( ) ( ) → Γ ⊢CT E(x) : A and A ≤ Γ(x). 1. if A.m , then A.m dom CT , CT A.m = A1 A for some A , A , and there exists a single method We will use the notation type_of (v), where type_of (nil) = 2 1 2 definition def A.m(x) : A → A = e such that Nil, type_of (true) = True, type_of (false) = False, type_of([A]) = 1 2 [self 7→ A, x 7→ A ] ⊢ e : A′ for some A′ where A, and type_of (A) = Type, for any A. 1 CT 2 2 A′ ≤ A . On blame: Our type system does not prevent invoking a 2 2 2. if A.m ∈ L, then A.m ∈ dom(CT), CT(A.m) = σ for method on a value that is nil. Additionally, runtime eval- some A , and there exists a single method declaration uation can fail if an inserted dynamic check fails. In or- m lib A.m(x) : σ such that σ = A → A or σ = (a<:e / der to retain soundness of our system, we add dynamic 1 2 t1 A ) → e /A for some A , A , e , e . semantics rules which step to blame in these cases, where 1 t1 2 1 2 t1 t2 3. For all A′ such that A′ ≤ A, if CT(A′.m) = A′ → A′ Γ ⊢ blame : Nil for any Γ. Additionally, we add rules which 1 2 CT and CT(A.m) = A → A then A ≤ A′ and A′ ≤ A . take a step to blame whenever a subexpression takes a step 1 2 1 1 2 2 to blame. We omit the rules here for brevity. See section A.2 for the programming type checking rules, Because we make use of a stack in our dynamic semantics, and a discussion of how to construct and check the validity a standard type preservation theorem which says that we of a class table. always step to an expression which has the same type (or Finally, we make use of the following lemmas in our proofs subtype) will not suffice. RulesE-AppUD ( ) and (E-Ret) push of soundness: and pop from the stack. In these cases, an expression e may Lemma 1 (Contextual substitution). If have an entirely different type than the expression that it ′ steps to, e ′. To account for this we incorporate a notion Γ ⊢CT e : A . of a type stack TS to mirror the runtime stack, which is . defined Figure7. As an example, suppose we want to apply , ΓC ⊢CT C[e] : AC preservation to C[v1.m(v2)]. The type checking judgment ′ ′ then ΓC [□ 7→ A ] ⊢CT C : AC . is Γ ⊢CT C[v1.m(v2)] : A . Because the dynamic semantics 15 values v ::= nil | [A] | A | true | false expressions e ::= v | x | self | tself | e; e | A.new | if e then e else e | e == e | e.m(e) | ⌈A⌉e.m(e) method types σ ::= A → A TLC method types δ ::= σ | (a<:e/A) → e/A programs P ::= def A.m(x) : σ = e | lib A.m(x) : δ | P; P x ∈ var IDs, m ∈ meth IDs, A ∈ class IDs
dyn env E : var ids → values contexts C ::= □ | C.m(e) | v.m(C) | ⌈A⌉C.m(e) | ⌈A⌉v.m(C) | C; e | if C then e else e | C == e | v == C stack S ::= · | (E,C) :: S type stack TS ::= · | (Γ[A], A) :: TS typ env Γ, ∆ : var ids → base types class table CT : class ids → meth ids → types objects O : objects object instance [·] : A → O
method sets U : set of user-defined methods L : set of library methods where L ∩ U = ∅ Nil, Obj, Bool, True, False, and Type are all presumed to be class IDs A. Subtyping is defined as Nil ≤ A, A ≤ A, and A ≤ A ⊔ A′ for all A, A′. A ⊔ A′ is the least upper bound of types A and A′.
Figure 7. λC and auxiliary definitions.
Lemma 2 (Substitution). If subtype of the type stack, and (b) says the same of e ′; this ′ is the crux of the type preservation proof, and as explained 1. ∆[□ 7→ AC ] ⊢CT C : AC 2. ∆ ⊢CT e : A above, it must be phrased in this way in order to account 3. A ≤ AC for the use of a stack, which allows us to step to expressions ′′ ′′ ′ with completely different types. Notice that (e) also tells us then ∆ ⊢CT C[e] : A where A ≤ A . C C C that if the stack goes unchanged, then A′ ≤ A; this is much With the above definitions and lemmas, we can finally closer to the standard statement of preservation. (e) also state our preservation theorem: gives us that if the stack goes unchanged, then the new type Theorem A.1 (Preservation). If environment is an extension of the old one. ′ ′ ′ (1) ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ (4) gives us consistency between type and dynamic en- (2) Γ ⊢CT e : A vironments, and (5) gives us consistency between the type (3) A ≤ TS stack and stack, the corresponding conclusions (c) and (d) (4) Γ ∼ E respectively give us the same for the new environments. (5) TS ∼ S (6) gives us validity of the class table (a corresponding con- (6) valid(CT) clusion is unnecessary since the class table goes unchanged). then there exists ∆, TS′, A′ such that Finally, we proceed with the proof of preservation. ′ ′ ′ ′ ′ (a) ∆ ⊢CT e : A Proof. By induction on ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ ′ ′ (b) A ≤ TS • Case (E-Self). By assumption we have ′ (c) ∆ ∼ E (1) ⟨E, self, S⟩ ⟨E, E(self), S⟩ by (E-Self) ′ ′ ⇝ (d) TS ∼ S (2) Γ ⊢CT self : A ′ ′ ′ ′ (e) If S = S then A ≤ A and ∆ = Γ, Γ for some Γ (3) Γ(self) ≤ TS (1) and (2) are standard: they say that some expression e (4) Γ ∼ E takes a step, and that e is well typed. Conclusion (a) states (5) TS ∼ S that e ′ is also well typed. (3) says that the type of e is a (6) valid(CT)
16 ′ ′ ′ Dynamic semantics ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ (E-Var) ⟨E, x, S⟩ ⇝ ⟨E, E(x), S⟩ (E-Self) ⟨E, self, S⟩ ⇝ ⟨E, E(self), S⟩ (E-TSelf) ⟨E, tself, S⟩ ⇝ ⟨E, E(tself), S⟩ (E-Seq) ⟨E,v; e, S⟩ ⇝ ⟨E, e, S⟩ (E-New) ⟨E, A.new, S⟩ ⇝ ⟨E, [A], S⟩ (E-IfTrue) ⟨E, if v then e2 else e3, S⟩ ⇝ ⟨E, e2, S⟩ if v is not nil or false (E-IfFalse) ⟨E, if v then e2 else e3, S⟩ ⇝ ⟨E, e3, S⟩ if v = nil or v = false (E-EqTrue) ⟨E,v1 == v2, S⟩ ⇝ ⟨E, true, S⟩ if v1 and v2 are equivalent (E-EqFalse) ⟨E,v1 == v2, S⟩ ⇝ ⟨E, false, S⟩ if v1 and v2 are not equivalent (E-AppUD) ⟨E,C[vr .m(v)], S⟩ ⇝ ⟨[self 7→ vr , x 7→ v], e, (E,C) :: S⟩ if type_of(vr ) = A and A.m ∈ U and def_of(A.m) = x.e ′ (E-AppLib) ⟨E, ⌈A⌉vr .m(v), S⟩ ⇝ ⟨E,v , S⟩ ′ ′ if type_of(vr ) = A and A.m ∈ L and v = call(A.m,vr ,v) and type_of(v ) ≤ A ′ (E-Ret) ⟨E ,v, (E,C) :: S⟩ ⇝ ⟨E,C[v], S⟩ (E-Context) ′ ′ ′ ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ ∄v,vr , A,m.(e = vr .m(v) ∧ type_of(vr ) = A ∧ A.m ∈ U) ′ ′′ ′ ′′ ∄v.e = v ∄C , e .e = C [e ] ′ ′ ′ ⟨E,C[e], S⟩ ⇝ ⟨E ,C[e ], S ⟩ where def_of(A.m) = x.e if there exists a definition def A.m(x) : σ = e. Additionally, call(A.m,v,v) is our way of dispatching a library method, where A.m identifies the method, the first v is the receiver of the method call, and the second v is the argument of the method call. It returns a value vr , the value returned by the method call. Finally, we use type_of (v), where type_of (nil) = Nil, type_of (A) = Type, type_of (true) = True, type_of (false) = False, and type_of ([A]) = A.
Figure 8. Dynamic semantics of λC
Since (2) must have been derived by (T-Self), by inver- (1) ⟨E, if v then e1 else e2, S⟩ ⇝ ⟨E, e1, S⟩ sion of this rule we have that A = Γ(self). Let ∆ = Γ (2) Γ ⊢CT if v then e1 else e2 : A and TS′ = TS. By equality of ∆ and Γ, (2), (4), and the (3) A ≤ TS definition of environmental consistency, there exists (4) Γ ∼ E ′ ′ ′ A such that ∆ ⊢CT E(self) : A and A ≤ ∆(self). (5) TS ∼ S Then (a) holds since E(self) is well typed. (b) holds (6) valid(CT) ′ since A ≤ ∆(self) = Γ(self) ≤ TS by (3). Because By (2) and inversion of (T-If), there exits Av , A1, A2 A′ ≤ Γ(self) and ∆ = Γ, ∅, we have (e). Finally, (c) such that: holds by (4), and (d) holds by (5). (7) Γ ⊢CT v : Av • Case (E-Var), (E-TSelf). Similar to (E-Self) case. (8) Γ ⊢CT e1 : A1 • Case (E-New). By assumption we have (9) Γ ⊢CT e2 : A2 (1) ⟨E, A.new, S⟩ ⇝ ⟨E, [A], S⟩ by (E-New) (10) A = A1 ∪ A2 ′ ′ (2) Γ ⊢CT A.new : A by (T-New) Let ∆ = Γ, TS = TS, and A = A1. Then from (8) we ′ (3) A ≤ TS trivially have (a). Additionally, A = A1 ≤ A1 ⊔A2, and (4) Γ ∼ E so A′ ≤ TS′ giving us (b). Finally, we have (c) by (4), (5) TS ∼ S (d) by (5), and (e) by the fact that A′ ≤ A and ∆ = Γ, ∅. (6) valid(CT) • Case (E-IfFalse). Similar to (E-IfTrue) case. Let ∆ = Γ, TS′ = TS, and A′ = A. Then we get (a) from • Case (E-Context). By assumption we have: ′ ′ ′ (T-Obj), (b) from (3), (c) from (4), (d) from (5), and (e) (1) ⟨E,C[e], S⟩ ⇝ ⟨E ,C[e ], S ⟩ where ′ ′ ′ ′ immediately by definition of A and ∆. (1a) ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ • Case (E-Seq). Trivial. (1b) ¬(e = vr .m(v) ∧ type_of(xr ) = A ∧ A.m ∈ U) for • Case (E-EqTrue), (E-EqFalse). Trivial. some v, vr , A, m • Case (E-IfTrue). By assumption we have 17 Type Checking and Check Insertion Rules. Γ ⊢CT e ,→ e : A
C-Nil C-New C-Type Γ ⊢CT nil ,→ nil : Nil Γ ⊢CT A.new ,→ A.new : A Γ ⊢CT A ,→ A : Type
C-True C-False C-Obj Γ ⊢CT true ,→ true : True Γ ⊢CT false ,→ false : False Γ ⊢CT [A] ,→ [A] : A
′ ′ Γ ⊢CT e1 ,→ e1 : A1 Γ ⊢CT e1 ,→ e1 : A1 ′ ′ Γ(x) = A Γ1 ⊢CT e2 ,→ e2 : A2 Γ1 ⊢CT e2 ,→ e2 : A2 C-Var C-Eq C-Seq ⊢ → ′ ′ ′ ′ Γ CT x , x : A Γ ⊢CT e1 == e2 ,→ e1 == e2 : Bool Γ ⊢CT e1; e2 ,→ e1; e2 : A2
CT(A.m) = A1 → A2 A.m ∈ U Ax ≤ A1 ′ ′ ′ ′ ′ Γ ⊢CT e1 ,→ e1 : A1 Γ ⊢CT e2 ,→ e2 : A2 Γ ⊢CT e3 ,→ e3 : A3 Γ ⊢CT e ,→ e : A Γ ⊢CT ex ,→ ex : Ax ′ ′ ′ C-If ′ ′ C-AppUD Γ ⊢CT if e1 then e2 else e3 ,→ if e1 then e2 else e3 : (A2 ⊔ A3) Γ ⊢CT e.m(ex ) ,→ e .m(ex ) : A2
CT(A.m) = (a<:et1/At1) → et2/At2 A.m ∈ L ′ ′ Γ ⊢CT e ,→ e : A Γ ⊢CT ex ,→ ex : Ax ′ CT(A.m) = A1 → A2 x:Type, tself:Type ⊢ CT et1 ,→ et1 : Type ′ T U ∗ A.m ∈ L Ax ≤ A1 ⟨[x 7→ A][tself 7→ A], et1, ·⟩ ⇝ ⟨E1, A1, ·⟩ Ax ≤ A1 ′ ′ Γ ⊢CT e ,→ e : A x:Type, tself:Type ⊢ CT et2 ,→ et2 : Type ′ T U′ ∗ Γ ⊢CT ex ,→ ex : Ax ⟨[x 7→ A][tself 7→ A], et2, ·⟩ ⇝ ⟨E2, A2, ·⟩ ′ ′ C-AppLib ′ ′ C-App-Comp Γ ⊢CT e.m(ex ) ,→ ⌈A2⌉e .m(ex ) : A2 Γ ⊢CT e.m(ex ) ,→ ⌈A2⌉e .m(ex ) : A2
Figure 9. Type checking and check insertion rules for λC .
Type checking rules Γ ⊢CT e : A
Γ(self) = A T-Nil T-Obj T-Self T-True Γ ⊢CT nil : Nil Γ ⊢CT [A] : A Γ ⊢CT self : A Γ ⊢CT true : True
Γ(x) = A T-False T-Type T-Var Γ ⊢CT false : False Γ ⊢CT A : Type Γ ⊢CT x : A
Γ ⊢CT e1 : A1 Γ ⊢CT e1 : A1 Γ1 ⊢CT e2 : A2 Γ(tself) = A Γ1 ⊢CT e2 : A2 T-Eq T-TSelf T-Seq T-New Γ ⊢CT e1 == e2 : Bool Γ ⊢CT tself : A Γ ⊢CT e1; e2 : A2 Γ ⊢CT A.new : A
Γ ⊢CT e0 : AA.m ∈ U Γ ⊢CT e1 : A1 Γ ⊢CT e2 : A2 Γ ⊢CT e1 : A Γ ⊢CT e0 : A Γ ⊢CT e3 : A3 CT(A.m) = A1 → A2 A ≤ A1 A.m ∈ L T-If T-App T-App-Lib Γ ⊢CT if e1 then e2 else e3 : (A2 ⊔ A3) Γ ⊢CT e0.m(e1) : A2 Γ ⊢CT ⌈A⌉e0.m(e1) : A
Figure 10. Type checking rules for λC .
(1c) e , v for some v (6) valid(CT) (1d) e , C ′[e ′] for some C ′, e ′ by (E-Context) First, note that we must have S ′ = S, because the only (2) Γ ⊢CT C[e] : A cases where this would not happen would be if (E- (3) A ≤ TS AppUD) or (E-Ret) were used to derive (1a), and this (4) Γ ∼ E cannot be the case given (1b) and because (E-Ret) only (5) TS ∼ S applies to top-level values and thus can’t apply to a
18 context. Now note that since (2) gives us Γ ⊢CT C[e] : A, (4) Γ ∼ E by inversion, there should exist Ae so that (5) TS ∼ S (7) Γ ⊢CT e : Ae (6) valid(CT) Let TSe be a type stack such that TSe ∼ S, Ae ≤ TSe , Noting the type checking rules for each context case, and all type environments in TSe are the same as those we know (2) must have been derived by some rule of in TS; it is straightforward to construct such a TSe from the form: the existing TS. Then, by (1a), (7), Ae ≤ TSe , (4), TSe ∼ S, and (6), we satisfy the premises of the preservation Γ ⊢CT vr .m(v) : Am . theorem. Therefore, applying the inductive hypothesis, . ′ ′ there exists ∆e , TSe , Ae such that: ⊢ [ . ( )] ′ ′ Γ CT C vr m v : AC (ai ) ∆e ⊢CT e : Ae ′ ′ From this and the contextual substitution lemma1, (bi ) Ae ≤ TSe [ 7→ ] ⊢ ′ we know Γ □ Am CT C : AC . Additionally, by (ci ) ∆e ∼ E inversion, we have (d ) TS′ ∼ S ′ i e (7) Γ ⊢CT vr .m(v) : Am ′ ′ ≤ ′ ′ (ei ) If S = S then Ae Ae and ∆e = Γ, Γ for some Γ By (1a), definition of type_of, and the value type check- Let ∆ = ∆ and TS′ = TS. Now, because S ′ = S, by e ing rules, we know that Γ ⊢CT vr : Arec . Because by (g ) we have ∆ = Γ, Γ′. By Lemma1 we have Γ[ 7→ i □ (1b) we know that Arec .m ∈ U and U and L are Ae ] ⊢CT C : A, and by the weakening lemma, we also by definition disjoint, the type checking judgment (7) have ∆[□ 7→ Ae ] ⊢CT C : A. By (ei ) we also have ′ must have been derived from rule T-App. Ae ≤ Ae . These, along with (ai ) and the substitution = ′ ′ ′ By instantiation of rule T-App with Am A2 we get lemma2, give us that ∆ ⊢CT C[e ] : A for some A ′ where A ≤ A. This immediately gives us (a), and ⊢ . ∈ U ′ Γ CT vr : Arec A m because A ≤ A and A ≤ TS by (3) we get (b). Because Γ ⊢ v A ′ ′ CT : arд ∆ = Γ, Γ and A ≤ A, we get (e). We get (c) from (ci ). CT(A.m) = A → A A ≤ A ′ 1 2 arд 1 (d) comes from (5) and the fact that S = S. T-App ⊢ ( ) • Case (E-AppLib). By assumption we have Γ CT vr .m v : A2 Thus, by inverting T-App, there must exist A , A , (1) ⟨E, ⌈Ares ⌉vr .m(v1), S⟩ ⇝ ⟨E,v, S⟩ where arд 1 (1a) type_of(vr ) = Arec A2 such that: ⊢ (1b) Arec .m ∈ L (8) Γ CT v : Aarд ( . ) → (1c) v = call(Arec .m,vr ,v1) (9) CT A m = A1 A2 ≤ (1d) type_of(v) ≤ Ares (10) Aarд A1 ′ by (E-AppLib). Now, let ∆ = [x 7→ A1, self 7→ Arec ]. Also, let TS = ( [ ], ) (2) Γ ⊢CT ⌈Ares ⌉vr .m(v1) : A Γ A2 AC :: TS. By (1b), (1c), (6), (8), and (9), we know ′ (3) A ≤ TS there exists A such that [self 7→ Arec , x 7→ A1] ⊢CT ′ ′ (4) Γ ∼ E e : A where A ≤ A2, which gives us (a); that is to say, (5) TS ∼ S the body of the method type checks as expected. (b) ′ (6) valid(CT) holds by construction of TS . (c) holds by construction ′ Because of (1b), we know that (2) must have been of ∆. (e) holds trivially since S , S . derived by (T-App-Lib). Instantiating this rule with the Finally, we show (d). By (4) we have Γ ∼ E, and [ 7→ ] ⊢ obvious bindings, we get that A = Ares . Let ∆ = Γ, as noted above we have Γ □ A2 CT C : AC . This ′ TS = TS, and A′ = type_of(v). If v is nil, we get (a) gives us (Γ[A2], AC ) ∼ (E,C). By (3) we have AC ≤ TS, immediately by (1d), definition of type_of, and (T-Nil); and by (5) we have TS ∼ S. Putting this all together, a similar argument holds for all other potential values by the definition of stack consistency, this gives us of v. In all cases, we get (b) from (1d) and (3). We get (c) (ΓC [A2], AC ) :: TS ∼ (E,C) :: S, which is (d). from (4), (d) from (5), and (e) from (1d) and definition • Case (E-Ret). By assumption we have ′ of ∆. (1) ⟨E ,v, (E,C) :: S⟩ ⇝ ⟨E,C[v], S⟩ • Case (E-AppUD). By assumption we have (2) Γ ⊢CT v : A ≤ ( [ ] ′ ) (1) ⟨E,C[vr .m(v)], S⟩ ⟨[self 7→ vr , x 7→ v], e, (E,C) :: (3) A ΓC AC , AC :: TS ⇝ ′ S⟩ where (4) Γ ∼ E ( [ ] ′ ) ∼ ( ) (1a) type_of(vr ) = Arec (5) ΓC AC , AC :: TS E,C :: S (1b) Arec .m ∈ U (6) valid(CT) ′ [ 7→ (1c) def_of(Arec .m) = x.e Let ∆ = ΓC and TS = TS. By (5) we have ΓC □ ] ⊢ ′ ≤ (2) Γ ⊢CT C[vr .m(v)] : AC AC CT C : AC , and by (3) we have A AC . Then by (3) AC ≤ TS these, (2), and the substitution lemma2, we have that 19 ′′ ′′ ′ ΓC ⊢CT C[v] : AC where AC ≤ AC ; this also means vr .m(v) for anyvr , A,m,v where type_of(vr ) = Ar ∧ ′′ that ∆ ⊢CT C[v] : AC . Ar .m ∈ U, then by the inductive hypothesis there ′ ′′ ′ ′ ′ ′ ′ ′ Let A = AC and we have (a). By (5) and the definition exists E , eP , S such that ⟨E, eP , S⟩ ⇝ ⟨E , eP , S ⟩ (or ′ ′′ of stack consistency we have AC ≤ TS, and since AC ≤ such that we step to blame, in which case e steps to ′ AC , we have (b). Finally, (c) and (d) hold by (5), and (e) blame and we are done). See the (E-Context) case holds trivially since S , S ′. of the preservation proof for a discussion of how □ to satisfy the premises of the inductive hypothesis, since the premises here a subset of those of preserva- Before proving progress, we introduce one assumption tion. By construction of the proper subexpression as and one lemma: ′ ′′ ′ ′′ specified in Lemma3, ∄C , e such that eP = C [e ], Assumption 1 (Library Method Termination). For any A, and eP , v for any value v. This satisfies all the m, v1, and v2 where A.m ∈ L, call(A.m,v1,v2) will terminate premises of (E-Context), therefore this rule would and return a value. apply to CP [eP ] to take a step. Otherwise, e ≡ v .m(v) with type_of(v ) = A ∧ This assumption is necessary to prove progress since we P r r r A .m ∈ U. We know that (1) must have been de- do not have the mechanism to refer to the step-by-step eval- r rived by rule (T-Eq). By inversion of this rule, this uation of library methods. A straightforward expansion of means Γ ⊢ v .m(v) : A for some A . By defini- λC would allow us to do so, but we omit such an expansion CT r m m tion of type_of, it must be that A = A . Because here for simplicity. m r Ar .m ∈ U and by definition U∩L = ∅, we therefore ′ Lemma 3 (Proper context). For any expression e, if e = C[e ] know Γ ⊢CT vr .m(v) : Am must have been derived ′ for some C, e , then there exists a proper context CP and proper from (T-App). By inversion of this rule, we know subexpression eP such that e = CP [eP ], eP , v for any value CT(Ar .m) = A1 → A2 for some A1, A2. By (5), this ′ ′′ ′ ′′ v, and ∄C , e such that eP = C [e ]. means def_of(Ar .m) = x.em for some em. If vr is nil, then we return blame. Thus, we have satisfied all the Note that it is still possible in Lemma3 that C = C and P premises of rule (E-AppUD), therefore we can apply e = e. The high-level idea behind the proof of Lemma3 is P this rule and we are done. that we construct the proper context by recursively pushing – e ≡ v and e . v . Then e ≡ C[e ] with C ≡ v == the hole [] deeper into subcontexts while possible. With 1 1 2 2 2 1 []. By a similar argument to the previous case, either these, we can proceed with defining and proving progress: (E-Context) or (E-AppUD) must apply. Theorem A.2 (Progress). If • Case e1; e2. We split cases on whether or not e1 is a (1) Γ ⊢CT e : A value: (2) A ≤ TS – e1 ≡ v. Then, the evaluation rule E-Seq on the ex- (3) Γ ∼ E pression v; e2 applies to take a step. (4) TS ∼ S – Otherwise, e ≡ C[e1] with C ≡ []; e2. By a similar (5) valid(CT) argument to the e1 == e2 case, either (E-Context) then one of the following holds or (E-AppUD) must apply. • Case A.new. Trivial. 1. e is a value ′ ′ ′ ′ ′ ′ • Case if e0 then e1 else e2. We split cases on the struc- 2. There exists E , e , S such that ⟨E, e, S⟩ ⇝ ⟨E , e , S ⟩ ture of e0. 3. ⟨E, e, S⟩ ⇝ blame – e0 ≡ nil or e0 ≡ false. The rule E-IfFalse applies Proof. By induction on e. to take a step. • Case nil, true, false, [A], or A. e is a value. – e0 ≡ v where v is not nil or false. The rule E- • Case self. By assumption (1) and (T-Self), we know IfTrue applies to take a step. self ∈ dom(Γ). By (3), this means self ∈ dom(E). – Otherwise, e ≡ C[e0] with C ≡ if [] then e1 else e2. This means rule (ESelf) can be applied, so we can take By a similar argument to the e1 == e2 case, either a step. (E-Context) or (E-AppUD) must apply. • Cases x, tself. Similar to self. • Case e1.m(e2). We split this case on the structure of • Case e1 == e2 We split into cases on whether or not e1, e2: e1, e2 are values, for any v1,v2: – e1 . v. Then e ≡ C[e1] with C ≡ [].m(e2). By a – e1 ≡ v1 and e2 ≡ v2. This case is trivial since one of similar argument to the e1 == e2 case, either (E- (E-EqTrue) or (E-EqFalse) will always apply. Context) or (E-AppUD) must apply. – e1 . v1. Then e ≡ C[e1] with C ≡ [] == e2. By – e1 ≡ v1 and e2 . v2. Then e ≡ C[e2] with C ≡ Lemma3, there exists a proper context CP and proper e1.m([]). By a similar argument to the e1 == e2 case, subexpression eP such that e ≡ CP [eP ]. If eP . either (E-Context) or (E-AppUD) must apply. 20 – e = v and e = v . If v is nil, we step to blame. 1 1 2 2 1 21 we satisfy the preconditions of progress and preservation, Because e is a non-checked method call, we know and soundness holds by standard argument. □ (1) must have been derived by (T-App). By inversion of this rule, Γ ⊢CT v1 : A1 for some A1, and that With this soundness theorem, it is straightforward to ex- A1.m ∈ U and CT(A.m) = Ain → Aout . By (7), tend soundness to the check insertion rules. We make use this means def_of(A.m) = x.em for some em. This satisfies all the premises of rule(E-AppUD), therefore of a lemma that states that the type assigned by the check we can apply this rule. insertion rules will be equivalent to the type assigned by the type checking rules. • Case ⌈Ares ⌉e1.m(e2). We split this case on the structure of e1, e2: ′ ′ Lemma 4. Γ ⊢CT e ,→ e : AC and Γ ⊢CT e : A if and only – e1 . v. Then e ≡ C[e1] with C ≡ [].m(e2). By a ′ if Γ ⊢CT e ,→ e : A. similar argument to the e1 == e2 case, either (E- Context) or (E-AppUD) must apply. Proof. Straightforward by induction on check insertion rules – e1 ≡ v1 and e2 . v2. Then e ≡ C[e2] with C ≡ ′ Γ ⊢CT e ,→ e : A. □ e1.m([]). By a similar argument to the e1 == e2 case, either (E-Context) or (E-AppUD) – e1 = v1 and e2 = v2. If v1 is nil, we return blame. Theorem A.4 (Soundness of Check Insertion). If valid(CT) ′ ′ ′ Because e is a checked method call, we know (1) and ∅ ⊢CT e ,→ e : A then either e reduces to a value, e ′ must have been derived by (T-App-Lib). By inver- reduces to blame, or e does not terminate. sion of this rule, we know Γ ⊢CT v1 : A1 for some A1, Proof. By Theorem A.3 and Lemma4. where A1.m ∈ L. Let vres = call(A.m,v1,v2); by As- □ sumption1 we know that this call will terminate and return a value. Then, if type_of(vres ) is not a subtype A of res , we will return blame. Otherwise, we will A.2 Program Type Checking and Class Table have satisfied all the preconditionsE-AppLib of( ), Construction therefore we can apply this rule and take a step. The rules for type checking a program are given in Figure 11. □ They rely on using a class table. We omit a formal definition We now introduce our theorem of soundness of the type of the class table construction here as it is straightforward. checking judgment. Informally: to construct a class table, traverse a program, adding type annotations from definitions and declarations Theorem A.3 (Soundness of Type Checking). If valid(CT) as you go. We can then check that CT |= P to ensure that the and ∅ ⊢ e ,→ e ′ : A and ∅ ⊢ e ′ : A, then either e ′ reduces CT C CT program P type checks under the constructed class table. If to a value, e ′ reduces to blame, or e ′ does not terminate. CT |= P, and the appropriate subtyping relations hold among Proof. Let Γ = ∅, E = ∅, S = (∅, □) :: ·, and TS = (Γ[A], A) :: ·. methods of subclasses, we can conclude that valid(CT) ac- By construction we have A ≤ TS, Γ ∼ E, and TS ∼ S. Thus, cording to definition4. Program type checking rules CT |= P ′ CT(A.m) = A1 → A2 [self 7→ A, x 7→ A1] ⊢CT e : A2 ′ A2 ≤ A2 CT(A.m) = σ T-PDef T-PLib CT |= def A.m(x) : A1 → A2 = e CT |= lib A.m(x) : σ
CT |= P1 CT |= P2 T-PSeq CT |= P1, P2
Figure 11. Program type checking rules.
22