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 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 , 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, which 1 type Table, :exists?, "( «schema_type(tself)») → Boolean" would allow type-incorrect arguments. 2 type Table, :joins, "( t<:Symbol) → 3 «if t.is_a? ( Singleton ) Comp types for DB Queries. To address this problem, Comp- 4 then Generic.new( Table, schema_type(tself ).merge( RDL allows method type signatures to include computations { t.val ⇒schema_type(t)}) ) that can, on-the-fly, determine the method’s type. Figure 1b 5 else Nominal.new(Table) gives comp type signatures for exists? and joins. It also shows 6 end »" the definition of a helper method, schema_type, that is called 7 from the comp types. The comp types also make use of a new 8 def schema_type(t) generic type Table to type a DB table whose columns are 9 if t.is_a? (Generic) ∧ ( t.base == Table) # Table described by T, which should be a finite hash type. 10 return t.param # returnT 11 elsif t.is_a? ( Singleton ) # Class or :symbol Line1 gives the type of exists?. Its argument is a comp 12 table_name = t.val # get the class/symbol vale type, which is a Ruby expression, delimited by «·», that 13 table_type = RDL.db_schema[table_name] evaluates to a standard type. When type checking a call to 14 return table_type.param exists? (including those in the body of available?), CompRDL 15 else # will only be reached for the nominal type Table runs the comp type code to yield a standard type, and then 16 return ... # returns Hash proceeds with type checking as usual with that type. 17 end In this case, to compute the argument type for exists?, 18 end we call the helper method schema_type with tself, which is a reserved variable naming the type of the receiver. The (b) Comp type annotations for query methods. schema_type method has a few different behaviors depend- ing on its argument. When given a type Table, it returns Figure 1. Discourse Type Checking Database Queries in . T, i.e., the finite hash type describing the columns. When given a singleton type representing a class or a symbol, it uses note the postfix if ). If not, it uses the database query method another helper method RDL.db_schema (not shown) to look exists? to see if the username was already taken (line9). up the corresponding table’s schema and return an appro- (Note that in Ruby, { a: b} is a hash that maps the symbol priate finite hash type. Given any other type, schema_type :a, which is suffixed with a colon when used as a key, tothe falls back to returning the type Hash. value b.) Otherwise, line 11 uses a more complex query to This type signature already allows us to type check the check whether an account was staged. More specifically, this exists? call on line9. On this line, the receiver has the sin- code joins the users and emails table and then looks for a gleton type for the User class, so schema_type will use the match across the joined tables. second arm of the conditional and look up the schema for We would like to type check the exists? calls in this code User in the DB. to ensure they are type correct, meaning that the columns Line2 shows the comp type signature for joins. The sig- they refer to exist and the values being matched are of the nature’s input type binds t to the actual argument type, and right type. The call on line9 is easy to check, as RDL can type requires it to be a subtype of Symbol. For example, for the the receiver User as having an exists? method that takes a call on line 11, t will be bound to the singleton type for particular finite hash type { c1: t1, ..., cn: tn} as an ar- :emails. The return comp type can then refer to t. Here, if gument, where the ci are singleton types for symbols naming t is a singleton type, joins returns a new Table type that the columns, and the ti are the corresponding column types. merges the schemas of the receiver and the argument tables 3 1 type Hash, :[], "( k) → v" thus hash lookup, array index, and so forth are methods → 2 type Array, :first, "() a" rather than built-in language constructs. 3 type :page, "() → { info: Array, title: String }'' The second line similarly gives a type for Array#first, 4 which returns the first element of the array. Here type vari- 5 type "() → String " 6 def image_url() able a is the array’s contents type (declaration also not 7 page[:info].first # can' t type check shown). The third line gives a type for a method page of 8 # Fix: RDL.type_cast( page[:info],"Array< String >") .first the current class, which takes no arguments and returns 9 end a hash in which :info is mapped to an Array and :title is mapped to a String. Now consider type checking the image_url method de- Figure 2. Type Casts in a Method. fined at the bottom of the figure. This code is extracted and simplified from a Wikipedia client library used in our exper- using schema_type. Otherwise, it falls back to producing a iments (§5). Here, since page is a no-argument method, it Table with no schema information. Thus, the joins call on can be invoked without any parentheses. We then invoke line 11 returns type Hash#[] on the result. Table<{staged:%bool, username:String, id: Integer, Unfortunately, at this point type checking loses precision. emails: {email:String, user_id: Integer }}> The problem is that whenever a method is invoked on a finite hash type { c1: t1, ..., cn: tn}, RDL (retroactively) That is, the type reflects the schemas of both the users gives up tracking the type precisely and promotes it to Hash< and emails tables. Given this type, we can now type check Symbol, t1 or...or tn> [18]. In this case, page’s return type the exists? call on line 11 precisely. On this line, the receiver is promoted to Hash or String>. has the table type given above, so when called by exists? the Now the type checker gets stuck. It reasons that first helper schema_type will use the first arm of the conditional could be invoked on an array or a string, but first is defined and return the Table column types, ensuring the query is only for the former and not the latter. The only currently type checked precisely. available fix is to insert a type cast, as shown in the comment Though we have only shown types for two query methods on line8. in the figure, we note that comp types are easily extensible One possible solution would be to add special-case support to other kinds of queries. Indeed, we have applied them to for [] on finite hash types. However, this is only oneof 104 methods across two DB query frameworks (§5). Further- 54 methods of Hash, which is a lot of behavior to special- more, we can also use comp types to encode sophisticated case. Moreover, Ruby programs can monkey patch any class, invariants. For example, in Rails, database tables can only be including Hash, to change library methods’ behaviors. This joined if the corresponding classes have a declared associa- makes building special support for those methods inelegant tion. We can write a comp type for joins that enforces this. and potentially brittle since the programmer would have no (We omitted this in Figure1 for brevity.) way to adjust the typing of those methods. Finally, we note that while we include a “fallback” case In CompRDL, we can solve this problem with a comp that allows comp types to default to less precise types when type annotation. More specifically, we can give Hash#[] the necessary, in practice this is rarely necessary for DB queries. following type: That is, parameters that are important for type checking, such as the name of tables being queried or joined, or the type Hash, :[], "( t<:Object ) → names of columns be queried, are almost always provided «if tself.is_a? (FiniteHash) ∧ t.is_a? ( Singleton ) statically in the code. then tself.elts[t.val] else tself.value_type end»" 2.2 Avoiding Casts using Comp Types This comp type specifies that if the receiver has a finite In addition to letting us find type errors in code we could hash type and the key has a singleton type, then Hash#[] not previously type check precisely enough, the increased returns the type corresponding to the key, otherwise it re- precision of comp types can also help eliminate type casts. turns a value type covering all possible values (computed by For example, consider the code in Figure2. The first line value_type, definition not shown). gives the type signature for a method of Hash, which is Notice that this signature allows image_url to type check parameterized by a key type k and a value type v (declara- without any additional casts. The same idea can be applied to tions of the parameters not shown). The specific method many other Hash methods to give them more precise types. is Hash#[],4 which, given a key, returns the corresponding Tuple Types. In addition to finite hash types, RDL has a spe- value. Notably, the form x[k] is desugared to x.[] (k), and cial tuple type to model heterogeneous Arrays. As with finite 4Here we use the Ruby idiom that A#m refers to the instance method m of hash types, RDL does not special-case the Array methods class A. for tuples, since there are 124 of them. This leads to a loss of 4 1 # Table Schema loading.) Then where filters the resulting table based on some 2 # posts table{ id: Integer, topic_id: Integer, ...} conditions. In this case, the conditions involve a nested SQL 3 # topics table{ id: Integer, title: String, ...} query, which cannot be expressed except using raw SQL that 4 # topic_allowed_groups table{ group_id: Integer, topic_id: Integer} will be inserted into the final generated query. 5 This example also shows another feature: any ?’s that 6 # Query with SQL strings appear in raw SQL are replaced by additional arguments to 7 Post.includes ( :topic ) where. In this case, the ? will be replaced by self.id. 8 .where (' topics.title IN (SELECT topic_id FROM We would like to extend type checking to also reason topic_allowed_groups WHERE group_id = ?)' , self.id ) about the raw SQL strings in queries, since they may have 9 errors. In this particular example, we have injected a bug. 10 type Table, :where, "( t <: «if t.is_a? (ConstString) The inner SELECT returns a set of integers, but topics.title 11 then sql_typecheck( tself, t ) is a string, and it is a type error to search for a string in an 12 else schema_type(tself ) integer set. 13 end ») → « tself »" To find this bug, we developed a simple type checker for a subset of SQL, and we wrote a comp type for where that Figure 3. Type Checking SQL Strings in Discourse. invokes it as shown on line 10. In particular, if the type of the argument to where, here referred to by t, is a const string, then we type check that string as raw SQL, and oth- precision when invoking methods on values with tuple types. erwise we compute the valid parameters of where using the However, analogously to finite hash tables, comp types can schema_type method from Figure1. The result of where has be used to recover precision. As examples, the Array#first the same type as the receiver. method can be given a comp type which returns the type of The sql_typecheck method (not shown) takes the receiver the first element of a tuple, and the comp typefor Array#[] type, which will be a Table with a type parameter describing has essentially the same logic as Hash#[]. the schema, and the SQL string. One challenge that arises Const String Types. As another example, Ruby strings are in type checking the SQL string is that it is actually only mutable, hence RDL does not give them singleton types. (In a fragment of a query, which therefore cannot be directly contrast, Ruby symbols are immutable.) This is problematic, parsed using a standard SQL parser. We solve this problem because types might depend on string values. In particular, by creating a complete, but artificial, SQL query into which in the next section we explore reasoning about string values we inject the fragment. This query is never run, but it is during type checking raw SQL queries. syntactically correct so it can be parsed. Then, we replace Using comp types, we can assign singleton types to strings any ?’s with placeholder AST nodes that store the types of wherever possible. We introduce a new const string type the corresponding arguments. representing strings that are never written to. CompRDL For example, the raw SQL in Figure3 gets translated to treats const strings as singletons, and methods on String the following SQL query: are given comp types that perform precise operations on const strings and fall back to the String type as needed. We SELECT ∗ FROM posts INNER JOIN topics discuss handling mutation for const strings, finite hashes, ON a.id = b.a_id and tuples in Section4. WHERE topics.title IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = [Integer]) 2.3 SQL Type Checking As we saw in Figure1, ActiveRecord uses a DSL that makes Notice the table names (posts, topics) occur on the first line it easier to construct queries inside of Ruby. However, some- and the ? has been replaced by a placeholder indicating the times programmers need to include raw SQL in their queries, type Integer of the argument. Also note that the column either to access a feature not supported by the DSL or to names to join on (which are arbitrary here) are ignored by improve performance compared to the DSL-generated query. our type checker, which currently only looks for errors in Figure3 gives one such example, extracted and simplified the where clause. from Discourse, one of our subject programs. Here there are Once we have a query that can be parsed, we can type three relevant tables: posts, which stores posted messages; check it using the DB schema. In this case, the type mismatch topics, which stores the topics of posts; and topic_allowed- between topics.title and the inner query will be reported. _groups, which is used to limit the topics allowed by certain In § 2.1, comp types were evaluated to produce a normal user groups. type signature. However, we use comp types in a slightly Line7 shows a query that includes raw SQL. First, the different way for checking SQL strings. The sql_typecheck posts and topics tables are joined via the includes method. method will itself perform type checking and provide a (This method does eager loading whereas joins does lazy detailed message when an error is found. If no error is found, 5 sql_typecheck will simply return the type String, allowing Values v ::= nil | true | false | A type checking to proceed. Expressions e ::= v | x | a | self | tself | A.new | e; e | e == e 2.4 Discussion | if e then e else e | e.m(e) Now that we have seen CompRDL in some detail, we can | ⌈A⌉e.m(e) discuss several parts of its design. Meth. Types σ ::= A → A Lib. Meth. Types δ ::= σ | (a<:e/A) → e/A Dynamic Checks. In type systems with type-level computa- Programs P ::= def A.m(x) : σ = e tions, or more generally dependent type systems, comparing | lib A.m(x) : δ | P; P two types for equality is often undecidable, since it requires Type Env. Γ ::= ∅ | x:A checking if computations are equivalent. Dyn. Env. E ::= ∅ | x:v To avoid this problem, CompRDL only uses comp types Class Table CT ::= ∅ | A.m:δ, CT for methods which themselves are not type checked. For Method Sets U : user-defined methods example, Hash#[] is implemented in native code, and we have L : library methods not attempted to type check ActiveRecord’s joins method, which is part of a very complex system. x, a ∈ var IDs, m ∈ method IDs, A ∈ class IDs, U ∩ L = ∅ As a result, type checking in CompRDL is decidable. Comp C types are only used to type check method calls, meaning we Figure 4. Syntax and Relations of λ . will always have access to the types of the receiver and arguments in a method call. Additionally, in all cases we have encountered in practice, the types of the receiver and Constant Folding. Finally, in RDL, integers and floats have arguments are ground types (meaning they do not contain singleton types. Thus, we can use comp types to lift some type variables). Thus, comp types can be fully evaluated to arithmetic computations to the type level. For example, Comp- non-comp types before proceeding to type checking. RDL can assign the expression 1+1 the type Singleton(2) For soundness, since we do not type check the bodies of instead of Integer. This effectively incorporates constant comp type-annotated methods, CompRDL inserts dynamic folding into the type checker. checks at calls to such methods to ensure they match their While we did write such comp types for Integer and Float computed types. For example, in Figure2, CompRDL inserts (see Table1), we found that this precision was not useful, at a check that page[:info] returns an Array. This follows the least in our subject programs. The reason is that RDL only approach of gradual [39] and hybrid [17] typing, in which assigns singleton types to constants, and typically arithmetic dynamic checks guard statically unchecked code. methods are not applied to constant values. Thus, though we We should also note that although our focus is on applying have written comp types for the Integer and Float libraries, comp types to libraries, they can be applied to any method at we have yet to find a useful application for them in practice. the cost of dynamic checks for that method rather than static We leave further exploration of this topic to future work. checks. For example, they could be applied to a user-defined library wrapper. 3 Soundness of Comp Types In this section we formalize CompRDL as λC , a core object- Termination. A second issue for the decidability of comp oriented calculus that includes comp types for library meth- types is that type-level computations could potentially not ods. We first define the syntax and semantics of λC (§ 3.1), and terminate. To avoid this possibility, we implement a termi- then we formalize type checking (§ 3.2). The type checking nation checker for comp types. At a high level, CompRDL process includes a rewriting step to insert dynamic checks to ensures termination by checking that iterators used by type- ensure library methods satisfy their type signatures. Finally, level code do not mutate their receivers and by forbidding we prove type soundness (§ 3.3). For brevity, we leave the type-level code from using looping constructs. We also as- full formalism and proofs to AppendixA. Here we provide sume there are no recursive method calls in type-level code. only the key details. We discuss termination checking in more detail in §4. Value Dependency. We note that, unlike dependent types 3.1 Syntax and Semantics (e.g., Coq [34], Agda [32], F* [41]) where types depend di- Figure4 gives the syntax of λC . Values v include nil, true, rectly on terms, in CompRDL types depend on the types of and false. To support comp types, class IDs A, which are terms. For instance, in a comp type ( t<:Object ) → tres the the base types in λC , are also values. We assume the set of result type tres can depend on the type t of the argument. class IDs includes several built-in classes: Nil, the class of Yet, since singleton types lift expressions into types, we could nil; Obj, which is the root superclass; True and False, which still use CompRDL to express some value dependencies in are the classes of true and false, respectively, as well as types in the style of dependent typing. their superclass Bool; and Type, the class of base types A. 6 Type Checking and Rewriting Rules Γ ⊢CT e ,→ e : A

′ Γ ⊢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