We illustrate the approach by first specifying a standard type system for ..... A type annotation Ï can be either a nominal type N (the primitive type bool or a class ...
Coinductive type systems for object-oriented languages? Davide Ancona and Giovanni Lagorio DISI, Univ. of Genova, Italy {davide,lagorio}@disi.unige.it
Abstract. We propose a novel approach based on coinductive logic to specify type systems of programming languages. The approach consists in encoding programs in Horn formulas which are interpreted w.r.t. their coinductive Herbrand model. We illustrate the approach by first specifying a standard type system for a small object-oriented language similar to Featherweight Java. Then we define an idealized type system for a variant of the language where type annotations can be omitted. The type system involves infinite terms and proof trees not representable in a finite way, thus providing a theoretical limit to type inference of object-oriented programs, since only sound approximations of the system can be implemented. Approximation is naturally captured by the notions of subtyping and subsumption; indeed, rather than increasing the expressive power of the system, as it usually happens, here subtyping is needed for approximating infinite non regular types and proof trees with regular ones.
1
Introduction
In the context of object-oriented programming, many solutions have been proposed to the problem of type inference [17, 16, 1, 21, 6, 20, 12], but the increasing interest in dynamic object-oriented languages is asking for ever more precise and efficient type inference algorithms [3, 12]. Two important features which should be supported by type inference are parametric and data polymorphism [1]; the former allows invocation of a method on arguments of unrelated types, the latter allows assignment of values of unrelated types to a field. While most proposed solutions support parametric polymorphism well, only few inference algorithms are able to deal properly with data polymorphism; such algorithms, however, turn out to be quite complex and cannot be easily specified in terms of a type system. In this paper we propose a novel approach based on coinductive logic to specify type systems of programming languages. The approach consists in encoding programs in Horn formulas which are interpreted w.r.t. their coinductive Herbrand model. This is made possible by the notion of type constraint defined in ?
This work has been partially supported by MIUR EOS DUE - Extensible Object Systems for Dynamic and Unpredictable Environments.
previous work on principal typing of Java-like languages [7, 4] and is a generalization of the algorithm presented in [6]. In contrast with other approaches based on a unique kind of subtyping constraint, the encoding of a program into a Horn formula is allowed by constraints which are atoms (that is, atomic formulas) of different forms, each one corresponding to a kind of compound expression of the language. Coinduction arises naturally at two different levels: at the level of terms, since recursive types are infinite terms, and at the level of proofs, since recursive methods and the use of infinite types require proofs to be infinite as well. The paper is structured as follows: In Section 2 we provide a first example by formalizing a type system for a fully annotated language similar to Featherweight Java (FJ) [14]; the example is intended as a way for gently introducing the main concepts of the approach and to prepare the reader to the idealized type system presented in Section 3 and formalized in Section 4. In Section 5 the idealized type system is extended with subtyping and subsumption, and soundness is claimed (some proofs are sketched in Appendix B). Finally, conclusion and comments on further work can be found in Section 6.
2
A simple example
In this section we provide a first example by formalizing a type system for a fully annotated language similar to Featherweight Java (FJ) [14]. This section is mainly intended as a way for introducing the main concepts of the approach and to prepare the reader to the more advanced type system defined in the next sections; also, it shows the approach can be used for defining different kinds of type systems. 2.1
Syntax and operational semantics
The syntax of the languages is defined in Figure 1. Syntactic assumptions listed in the figure have to be verified before transforming a program into a Horn formula. We use bars for denoting sequences: for instance, e m denotes e1 , . . . , em , τ x n denotes τ1 x1 , . . . , τn xn , and so on. We assume countably infinite sets of class names c, method names m, field names f , and parameter names x . A program consists of a sequence of class declarations and a main expression from which the computation starts. A class declaration consists of the name of the declared class and of its direct superclass (hence, only single inheritance is supported), a sequence of field declarations, a constructor declaration, and a sequence of method declarations. We assume a predefined class Object, which is the root of the inheritance tree and contains no fields, no methods and a constructor with no parameters. A field declaration consists of a type annotation and a field name. A constructor declaration consists of the name of the class where the constructor is declared, a sequence of parameters with their type annotations, and the body, which consists of an invocation of the superclass constructor and a sequence of field initializations, 2
prog cd fd cn md e τ v
::= ::= ::= ::= ::= ::= ::= ::=
n
cd e n m class c1 extends c2 { fd cn md } (c1 6= Object) τ f; k c(τ x n ) {super(e m ); f = e 0 ; } n τ0 m(τ x ) {e} new c(e n ) | x | e.f | e0 .m(e n ) | if (e) e1 else e2 | false | true c | bool new c(v n ) | false | true
Assumptions: n, m, k ≥ 0, inheritance is acyclic, names of declared classes in a program, methods and fields in a class, and parameters in a method are distinct. Fig. 1. Syntax of OO programs
one for each field declared in the class.1 A method declaration consists of a return type annotation, a method name, a sequence of parameters with their type annotations, and an expression (the method body). Expressions are standard; even though the conditional expression, and the constants true and false could be encoded in the language, we have introduced them for making clearer the connection with union types in Section 3.2. For making the examples easier to read, we will also use the primitive type of integers, but leave it out in the formal treatment, which would be straightforward. As in FJ, the expression this is considered as a special implicit parameter. A type annotation can be either the primitive type bool or a class name. Finally, the definition of values v is instrumental to the (standard) small steps operational semantics of the language, indexed over the class declarations defined by the program, given in Figure 2. k
(field-1)
cbody(cds, c) = (x n , {super(. . .); f = e 0 ; }) f = fi new c(e n ).f →cds ei0 [e n /x n ]
1≤i≤k
k
m
cbody(cds, c) = (x n , {super(e 0 ); f = . . . ; }) ∀ i ∈ 1..k f 6= fi class c extends c 0 { . . .} ∈ cds 0 0 n n 0 new c (e1 [e /x ], . . . , em [e n /x n ]).f →cds e (field-2) new c(e n ).f →cds e (invk)
(if-1)
mbody(cds, c, m) = (x n , e) n n new c(e ).m(e 0 ) →cds e[e 0 /x n ][new c(e k )/this] k
if (true) e1 else e2 →cds e1
(if-2)
if (false) e1 else e2 →cds e2
Fig. 2. Reduction rules for OO programs 1
This is a generalization of constructors of FJ, which makes the encoding compositional: a constructor declaration contained in a class c can be encoded if just the name c and the fields declared in c are known.
3
For reasons of space, the rule for contextual closure and the standard definition of contexts have been omitted (to be as general as possible, no evaluation strategy is fixed); furthermore side conditions have been placed together with premises. Auxiliary functions cbody and mbody are defined in Appendix A. Rule (field-1) corresponds to the case where the field f is declared in the same class of the constructor, otherwise rule (field-2) applies and the field is searched in the direct superclass. The notation e[e n /x n ] denotes parallel substitution of xi by ei (for i = 1..n) in expression e. In rule (invk), the parameters and the body of the method to be invoked are retrieved by the auxiliary function mbody, which performs the standard method look-up. If the method is found, then the invocation reduces to the body of the method where the parameters are substituted by the corresponding arguments, and this by the receiver object (the object on which the method is invoked). The remaining rules are trivial. The one step reduction relation on programs is defined by: (cds e) → (cds e 0 ) iff e →cds e 0 . Finally, →∗ and →∗cds denote the reflexive and transitive closures of → and →cds , respectively. 2.2
Encoding of programs into Horn formulas
Since Prolog is the most direct way to implement a prototype interpreter for coinductive logic programming (see the conclusion), we follow the standard Prolog syntax notation. We assume two countably infinite sets of predicate and function symbols each associated with an arity n ≥ 0, and ranged over by p and f respectively, and a countably infinite set of logical variables ranged over by X . Functions with arity 0 are called constants. We write p/n, f /n to mean that predicate p, function f have arity n, respectively. For symbols we follow the usual convention: function and predicate symbols always begin with a lowercase letter, whereas variables always begin with an uppercase letter. A Horn formula Hf is a finite conjunction (or, more abstractly, a finite set) of clauses (ranged over by Cl ) of the form A ← B , where A is the head and B is the body. The head is an atom, while the body is a finite and possibly empty conjunction of atoms; the empty conjunction is denoted by true. A clause with an empty n body (denoted by A ← true) is called a fact. An atom has the form2 p(t ) where n the predicate p has arity n and t are terms. For list terms we use the standard notation [ ] for the empty list and [ | ] for n the list constructor, and adopt the syntax abbreviation [t ] for [t1 |[. . . [tn |[ ]]]. A formula/clause/atom/term is ground if it contains no logical variables. In the following simple examples terms are built in the usual inductive3 way from functions symbols and logical variables. In particular, the Herbrand universe of a Horn formula, obtained from a program prog, is the set of all terms 2
3
Parentheses are omitted for predicate symbols of arity 0; the same convention applies for function applications, see below. In Section 4 we will consider infinite terms as well.
4
inductively defined on top of the constant [ ] and all constants representing class, method and field names declared in prog, and on top of the binary function symbol [ | ]. The encoding of a program into a Horn formula intuitively corresponds to a reverse compilation procedure where the target language (Horn formulas) is at an higher level4 of abstraction. Following this intuition, a method declaration is encoded in a Horn clause. Consider for instance the following class declaration: c l a s s NEList extends List { Object el ; List next ; NEList ( Object e , List n ){ super (); el = e ; next = n ;} NEList add ( Object e ){new NEList (e , t h i s )} }
Method add can be encoded in the following clause: has meth(nelist, add , [E ], nelist) ← type comp(E, object), new (nelist, [E, nelist], nelist), override(list, add, [object], nelist). The clause states that for all types E , class NEList 5 has a method named add taking one argument6 of type E and returning a value of type NEList (atom has meth(nelist, add, [E], nelist)), if the following constraints are satisfied: – E is type compatible with Object (atom type comp(E, object)), – the constructor of class NEList takes two arguments, the former of type E and the latter of type NEList, and returns7 an object of type NEList (atom new (nelist, [E, nelist], nelist)), – in case of overriding, the signature of the method is compatible with method add of the direct superclass List (atom override(list, add, [object], nelist)). Note that if some of the constraints above cannot be satisfied, then the method add (hence, the whole program) is not well-typed (the conjunction of atoms Bcd n defined in Figure 4 explicitly requires that all declared methods must be well-typed). Each constraint corresponds to an atom which is directly generated from the method declaration: type comp(E, object) is derived from the type annotation of the parameter of add , new (nelist, [E, nelist], nelist) is generated from the body of add , and override(list, add, [object], nelist) depends from the direct superclass of NEList, and the signature and return type of add . 4
5
6 7
In this sense, our approach shares several commonalities with abstract interpretation, and a thorough comparison would deserve further investigation. Note that, to comply with the standard syntax of Horn formulas requiring constant symbols to begin with a lowercase letter, class names must be necessarily changed. We use lists to encode Cartesian products of types. The returned type is redundant here, but not in the type system defined in Section 3.2.
5
To encode a method body into a conjunction of constraints (that is, atoms) we follow the consolidated constraint-based approach to compositional typechecking and type inference of Java-like languages [5, 7, 4, 15, 6]: each kind of compound expression is associated with a specific predicate: n
– new (c, [t ], t) corresponds to object creation, c is the class of the invoked n constructor, t the types of the arguments, and t the type of the newly created object (recall footnote 7). – field acc(t1 , f , t2 ) corresponds to field access, t1 is the type of the accessed object, f the field name, and t2 the resulting type of the whole expression; n – invoke(t0 , m, [t ], t) corresponds to method invocation, t0 is the type of the n receiver, m the method name, t the types of the arguments, and t the type of the returned value. This predicate is completely redundant here (its definition is identical to has meth), but not in the type system defined in Section 3.2; – cond (t1 , t2 , t3 , t) corresponds to conditional expression, t1 is the type of the condition, t2 and t3 the types of the “then” and “else” branches, respectively, and t the resulting type of the whole expression. Besides those predicates needed to encode an expression, others are instrumental to the encoding, as type comp and override above. The encoding of a program is defined in Figure 3, whereas Figure 4 contains the set of clauses Hf default which are shared by all programs, and the conjunction of atoms Bcd n which imposes the requirement that each method declaration n in cd must be well-typed. Note that not all formulas in Figure 4 are Horn clauses; indeed, for brevity we have used the negation of predicates dec field and dec meth. However, since the set of all field and method names declared in a program is finite, the predicates not dec field , not dec meth could be trivially defined by conjunctions of facts, therefore all formulas could be turned into Horn clauses. For the encoding we assume bijections to translate class, field and method names to constants, and parameter names to logical variables (translations are d = denoted by b c, b f , m, b and xb, respectively). Furthermore, we assume that this This. The rules define a judgment for each syntactic category of the OO language: n
– cd e (Hf |B ): a program is translated in a pair (Hf |B ), where Hf is the union of the set Hf default of clauses shared by any program with the set of clauses generated from the class declarations. The second component B is the conjunction of Bcd n and Bm , where Bcd n is the conjunction of atoms n requiring that each method declared in cd is well-typed (see Figure 4), whereas Bm is the conjunction of atoms generated from the main expression e of the program; – fd in c Cl , md in c Hf : the encoding of a field/method declaration depends on the class c where the declaration is contained; 6
∀ i = 1..n cd i Hf i e in ∅ (t | Bm ) n cd e (Hf default ∪ (∪i=1..n Hf i )|Bcd n , Bm )
(prog)
(class)
∀ i = 1..n fd i in c1
Cl i
cn in fd n
n
(field)
τ f ; in c dec field (b c, b f , τb) ← true.
∀ j = 1..m md j in c1
Cl
Hf j
m
class md } c1 extends c2 { fd cn ff class(cb1 ) ← true. ∪ (∪i=1..n {Cl i }) ∪ {Cl } ∪ (∪j=1..m Hf j ) extends(cb1 , cb2 ) ← true. ∀ i = 1..m ei in {x :τ n }
(constr-dec)
(ti | Bi )
∀ j = 1..k ej0 in {x :τ n }
k
(tj0 | Bj0 )
k
c(τ x n ) {super(e m ); f = e 0 ; } in τ 0 f ; n m new (b c , A, b c ) ← type comp(A, [b τ ]), B , extends(b c, P ), k
m
k
k
new (P, [t ], P ), B 0 , type comp([t 0 ], [τb0 ]).
(meth-dec)
(new)
(var)
(if)
(t | B ) τ0 m(τ x ){e} in c n dec meth(b c , m, b [b τ ], τb0 ) ← true. n has meth(b c , m, b A, τb0 ) ← type comp(A, [b τ ]), extends(b c , P ), n τ ], τb0 ), B , type comp(t, τb0 ). override(P, m, b [b
∀ i = 1..n ei in V (ti | Bi ) R fresh n n new c(e n ) in V (R | B , new (b c , [t ], R))
x in V
(invk)
e in {This:c, x :τ n }
n
(b τ | true)
x :τ ∈ V
e in V
(field-acc)
e.f in V
(t | B )
R fresh (R | B , field acc(t, b f , R))
∀ i = 0..n ei in V (ti | Bi ) R fresh n n e0 .m(e n ) in V (R | B0 , B , invoke(t0 , m, b [t ], R))
e in V (t | B ) e1 in V (t1 | B1 ) e2 in V (t2 | B2 ) R fresh if (e) e1 else e2 in V (R | B , B1 , B2 , cond (t, t1 , t2 , R))
(true)
true in V
(bool | true)
(false)
false in V
(bool | true)
Fig. 3. Encoding of programs
7
– cn in fds Cl : the encoding of a constructor declaration depends on the declaration of the fields contained in the class of the constructor: the encoding is defined only if all fields in fds are initialized by the constructor in the same order8 as they appear in fds (that is, as they have been declared in the class of the constructor); – e in V (t | B ): an expression is encoded in a pair (t|B ), where t is the term encoding the type of the expression, and B the conjunction of atoms generated from the expression. The encoding depends on the type environment V , assigning types to parameters, and is defined only if all free variables of e are contained in the domain of V .
class(object) ← true. subclass(X , X ) ← class(X ). subclass(X , object) ← class(X ). subclass(X , Y ) ← extends(X , Z ), subclass(Z , Y ). type comp(bool , bool ) ← true. type comp(C1 , C2 ) ← subclass(C1 , C2 ). type comp([ ], [ ]) ← true. type comp([T1 |L1 ], [T2 |L2 ]) ← type comp(T1 , T2 ), type comp(L1 , L2 ). field acc(C , F , T ) ← has field (C , F , T ). invoke(C , M , A, R) ← has meth(C , M , A, R). new (object, [ ], object) ← true. has field (C , F , T ) ← dec field (C , F , T ). has field (C , F , T1 ) ← extends(C , P ), has field (P , F , T1 ), ¬dec field (C , F , T2 ). override(object, M , A, R) ← true. override(C , M , A, R) ← dec meth(C , M , A, R). override(C , M , A1 , R1 ) ← extends(C , P ), override(P , M , A1 , R1 ), ¬dec meth(C , M , A2 , R2 ). has meth(C , M , A1 , R1 ) ← extends(C , P ), has meth(P , M , A1 , R1 ), ¬dec meth(C , M , A2 , R2 ). cond (T1 , T2 , T3 , T4 ) ← type comp(T1 , bool ), type comp(T2 , T4 ), type comp(T3 , T4 ). n
Bcd n is the conjunction of atoms generated from cd as follows: n has meth(b c , m, b [τb1 , . . . , τbk ], τb0 ) is in Bcd n iff class c is declared in cd and contains k τ0 m(τ x ) {. . .}. Fig. 4. Definition of Hf default and Bcd n . Negation is used for brevity, but it can be easily removed (see the comments to the figure).
Rule (class) just collects all clauses generated from the field and constructor declarations, and the Horn formulas generated from the method declarations, 8
This last restriction is just for simplicity.
8
and adds to them the two new facts stating respectively that class c1 has been declared and that its direct superclass is c2 . A constructor declaration generates a single clause whose head has the form new (b c , A, b c ), where c is the class of the constructor, A is the logical variable corresponding to the list of arguments passed to the constructor, and c is the type of the object9 created by the constructor. The first atom in the body checks that the list of arguments A is type compatible w.r.t. the of parameter type annotations, that is, that A is a list of exactly n types which are subtypes of the corresponding type annotations (see the 3rd and 4th clause defining predicate type comp in Figure 4). However, all occurrences of the parameters are directly encoded by the corresponding types as specified by the environment {x :τ n } (see m m rule (var) below). The atoms B , extends(b c, P ), new (P, [t ], P ) encode the inm m vocation of the constructor of the direct superclass, where B and t are the atoms and types generated from the arguments (see the first premise). Finally, k the remaining atoms check that the field f declared in the class are initialized k k correctly; B and t are the atoms and types generated from the initializing expressions (see the second premise). Finally, note that the clause is correctly generated only if: (1) the free variables of the expressions contained in the constructor body are contained in the set {x n } of the parameters (therefore, this cannot be used); (2) all fields declared in the class are initialized exactly once and in the same order as they are declared. Rule (meth-dec) generates two clauses, one for the predicate dec meth and the other for the predicate has meth. Predicate dec meth specifies just the signatures and return types of all methods declared in c, and is used for defining predicates override and has meth (see Figure 4); predicate has meth specifies names, and argument and return types of all methods (either declared or inherited) of a class. The clauses generated by this rule correspond to the case when the method is found in the class, whereas there is a unique shared clause (defined in Figure 4) to deal with method look-up in the direct superclass. n Atoms extends(b c , P ), override(P, m, b [b τ ], τb0 ) ensure that the method overrides correctly the method (if any) inherited from the direct superclass P . Atoms B , type comp(t, τb0 ) check that the body is correct and that the type of the returned value is compatible with the declared type. Note that the variable this can be accessed in the body of the method and that it has type c, as specified in the environment {This:c, x :τ n }. Rule (var) can be instantiated only if x is defined in the environment V ; the associated type t is returned together with the empty conjunction of atoms. The other rules for expressions (except for the trivial ones on boolean constants) are very similar: premises generate types and atoms for all subexpressions, then the conclusion collects all generated atoms, adds a new atom corresponding to the whole expression, and generates a fresh variable (to avoid clashes) for the type of the whole expression. 9
We have already pointed out that the third argument of new is completely redundant here, but this is not true for the type system defined in Section 3.2.
9
Clauses defined in Figure 4 are quite straightforward. For instance, they state that Object is a predefined class which is the root of the inheritance relation and which has a default constructor with no parameters. Since Object cannot be redefined, it turns out that the class declares no fields and no methods. The definition of field acc is identical to dec field , but this is no longer true for the type system defined in the next section (see Section 3.2). The predicate override checks that methods are overridden correctly (we use the most restrictive formulation asking types of arguments and of the returned value to be all invariant); the first clause specifies that all methods of class Object are overridden correctly (since, in fact, object has no methods), the second states that a method declared in a class is overridden correctly only by a method having the same name and types of parameters and returned value, while the last clause says that a method is overridden correctly if it is not declared in the class and if it is overridden correctly in the direct superclass. Finally, the clause defining cond states that a conditional expression is correct if the type of the condition is bool (we use type comp for uniformity with the definitions given in Section 4); the corresponding type is any common super type of the expressions of the two branches. Coinductive Herbrand models Well-typed programs are defined in terms of coinductive Herbrand models, that is, greatest instead of least fixed-points are considered. As we will see in Section 3.4, coinduction arises naturally at two different levels: at the level of terms, since recursive types are infinite10 terms, and at the level of proofs, since recursive methods and the use of infinite types require proofs to be infinite as well. The Herbrand base of a Horn formula obtained from a program prog is the set of all ground atoms built from the predicates in the formula and the (ground) terms of the Herbrand universe of the formula. A substitution θ is defined in the standard way as a total function from logical variables to terms, different from the identity only for a finite set of variables. Composition of substitutions and application of a substitution to a term are defined in the usual way. A ground instance of a clause A ← A1 . . . An is a ground clause Cl s.t. Cl = Aθ ← A1 θ, . . . , An θ for a certain substitution θ. Given a Horn formula Hf , the immediate consequence operator THf is an endofunction defined on the parts of the Herbrand base of Hf as follows: THf (S) = {A | A ← B is a ground instance of a clause of Hf , B ∈ S}. A Herbrand model of a logic program Hf is a subset of the Herbrand base of Hf which is a fixed-point of the immediate consequence operator THf . Since THf is monotonic for any prog, by the Knaster-Tarski theorem there always exists the greatest Herbrand model of Hf , which is also the greatest set S s.t. 10
However, in the simple type system defined here infinite terms are not needed.
10
S ⊆ THf (S). The greatest Herbrand model of Hf is denoted by M co (Hf ) and called the coinductive Herbrand model of Hf . A conjunction of atoms B is satisfiable in Hf iff there exists a substitution θ s.t. B θ ⊆ M co (Hf ). n n (Hf |B ) and B is satisfiable in Hf . A program cd e is well-typed iff cd e Finally, we only informally state the claim and do not prove it (since it is out of the scope of this section) that the notion of well-typed program is equivalent to that given by a conventional type system (like that of FJ). n
Claim (informal equivalence). A program cd e is well-typed w.r.t. to a standard n (Hf |B ) and B is satisfiable in Hf . type system iff cd e
3
An idealized type system: an outline
In this section we present a more advanced type system supporting method and data polymorphism. The type system is idealized since it involves infinite terms and proof trees not representable in a finite way, therefore only sound approximations of the system can be implemented. Under this perspective, the system could be considered as an abstract specification for a large class of type inference algorithms for object-oriented programs which can only be sound but not complete w.r.t. the given system, and, thus, an attempt at pushing to the extreme the theoretical limits of static type analysis. 3.1
Extension of the syntax
At the syntax level the only needed extension to the language defined in Section 2 concerns type annotations which now can be empty, that is, users can omit (partially or totally) field and method type annotations: τ ::= N | N ::= c | bool This extension does not affect the operational semantics given in Figure 2. A type annotation τ can be either a nominal type N (the primitive type bool or a class name c), or empty. Consider for instance the following example: c l a s s EList { EList (){ super ();} add ( e ){new NEList (e , t h i s )} } c l a s s NEList { el ; next ; NEList (e , n ){ super (); el = e ; next = n ;} add ( e ){new NEList (e , t h i s )} }
11
Omitting types in method declarations allows method polymorphism: for instance, the two methods add are polymorphic in the type of their parameter e. Omitting types in field declarations allows data polymorphism: it is possible to build a list of elements of heterogeneous types and, as we will see, in the type system defined in the sequel each element of the list is associated with its exact type. Type annotations are intended as explicit constraints imposed by the user, but do not make type analysis less precise. For instance, if the declaration of field el is annotated with type Item, then only instances of Item or of a subclass of Item can be correctly added to a list. However, if we add to a list an instance of ExtItem which is a subclass of Item, then the type system is able to assign to the first element of the list the type ExtItem. 3.2
Structured types
To have a more expressive type system, we introduce structured types encoded by the following terms: – X , which represents a type variable; – bool , which represents the type of boolean values; – obj (b c , t), which represents the instances of class c, where t is a record [fb1 :t1 , . . . , fbn :tn ] which associates with each field fi of the object a corresponding type term ti ; as in the type system of Section 2, the methods of a class are encoded as clauses, whereas the types of fields need to be associated with each single instance, to be able to support data polymorphism; – t1 ∨ t2 , which represents a union type [8, 13]: an expression has type t1 ∨ t2 if it has type t1 or t2 . Note that nominal types are explicitly used by programmers as type annotations, whereas structured types are fully transparent to programmers. The use of structured types should now clarify why predicates type comp and invoke has been already introduced in Section 2 and why predicate new has arity 3. – The difference between predicates type comp and subclass is now evident: type comp/2 (see Figure 6) defines the relation of type compatibility between structured and nominal types. For instance, the atom type comp(obj (cb1 , t1 ) ∨ obj (cb2 , t2 ), b c) is expected to hold, whenever both c1 and c2 are subclasses of c. – The first and third argument of predicate new are now clearly different: the former is the class name of the invoked constructor, whereas the latter is the structured type of the created object. The following invariant is expected to n c ) holds as well. hold: if new (b c , [t ], t) holds, then type comp(t, b – Predicates invoke and has meth are now clearly distinct: the first argument of invoke is the structured type of the receiver, whereas the first argument of has meth is the class from which method look-up is started. 12
3.3
Typing methods
As already mentioned, type annotations are intended as explicit constraints imposed by the user, but do not make type analysis less precise. For instance, if we assume that a program declares11 classes H and P, with H subclass of P, and we annotate the parameter of method add of class EList as follows add ( P e ){new NEList (e , t h i s )} then new EList().add(e) is not well-typed if e=new Object(), while it has b :obj (H b , [ ]), next:obj \ [el \ [ ])]) if e=new H() (hence, the d type obj (NEList, (EList, type associated with field el corresponds to an instance of H rather than P). This means that, during type analysis, parameters are associated with a structural type (even when they are annotated) which depends on the specific method invocation. Similarly, the type of this cannot be fixed (we cannot simply associate with it the class name where the method is declared, as done in Section 2), therefore this is treated as an implicit parameter which is always the first of the list. The only implicit constraint on this requires its associated type to be type compatible with the class where the method is declared. Consider for instance the following class declaration: c l a s s C { val ; C ( v ){ super (); val = v ;} get (){ t h i s . val } } Method get generates the following clause: c , X ). b , get, b ), field acc(This, val c [This], X ) ← type comp(This, C has meth(C The head of the clause requires method get of class C to have just the implicit parameter this (indeed, no explicit parameters are declared), whereas the body requires this to be an instance of either C or a subclass of C, and to have a field c :t]), then the expression e.get() b , [val val of type X . Hence, if e has type obj (C has type t. A quite standard consequence of type inference [16, 17, 1, 21] is that no rule is imposed on method overriding. Consider for instance the following two class declarations: c l a s s P { P (){ super ();} m (){new A ()} } c l a s s H extends P { H (){ super ();} m (){new B ()} } In this very simple example method m of class P always returns an instance of A, but is overridden by the method of H which always returns an instance of B, where A and B are two unrelated classes. The definition of method m of class H would not be considered type safe in Java, if we assume to annotate methods m in P and in H with the return type A and B, respectively. Indeed, in Java the type of an instance of class H is a subtype of the type of an instance of class P. b , [ ]) is not a subtype In the type system defined here the structural type obj (H b of obj (P , [ ]); indeed, an object type obj (cb1 , t1 ) is a subtype of obj (cb2 , t2 ) if 11
Note that here H and P denotes concrete names and are not meta-variables.
13
and only if c1 = c2 and t1 is a record type which is a subtype of the record type t2 (w.r.t. the usual width and depth subtyping relation, see Section 5.3 for the formal definition). In this way the two method declarations above are b [ ]) if e has perfectly legal, and the method invocation e.m() has type obj (A, b b b b b , [ ]) type obj (P , [ ]), obj (B , [ ]) if e has type obj (H , [ ]), and obj (A, [ ]) ∨ obj (B b b if e has type obj (P , [ ]) ∨ obj (H , [ ]). 3.4
Regular types
Whereas in Section 2 types are just constants, here we take a coinductive approach by allowing types (and, hence, terms) to be infinite. This is essential to encode recursive types. Consider for instance the following class declarations: c l a s s List extends Object { altList (i , x ){ i f (i 0, a non empty list (an instance of NEList) whose length is i and whose elements are alternating instances of class A and B (starting from an A instance). Similarly, new List().altlist(i,new B()) returns an alternating list starting with a B instance. The following two mutually recursive types tA and tB precisely describe new List().altlist(i,new A()) and new List().altlist(i,new B()), respectively: b :obj (A, b [ ]), next:t \ [ ]) ∨ obj (NEList, \ [el d B ]) tA = obj (EList, b b \ [ ]) ∨ obj (NEList, \ [el :obj (B , [ ]), next:t d A ]) tB = obj (EList,
In fact, tA and tB correspond to regular infinite trees (see in the following). However, coinductive terms include also non regular trees12 [19] (see Section 5.1). 12
That is, infinite trees which cannot be finitely represented.
14
4
An idealized type system: a full formalization
In Section 2 types are just constants, whereas here types can be infinite terms, therefore the coinductive version of the Herbrand universe and base needs to be considered. In the rest of the paper we will identify terms with trees. The definition of tree which follows is quite standard [11, 2]. A path p is a finite and possibly empty sequence of natural numbers. The empty path is denoted by , p1 · p2 denotes the concatenation of p1 and p2 , and |p| denotes the length of p. We first give a general definition of tree, parametric in the set S of nodes, and then instantiate it in the case of terms and idealized proof trees. A tree t defined over a set S is a partial function from paths to S s.t. its domain (denoted by dom(t)) is prefix-closed, not empty, and verifies the following closure property: for all m, n and p, if p · n ∈ dom(t) and m ≤ n then p · m ∈ dom(t). If p ∈ dom(t), then the subtree of t rooted at p is the tree t 0 defined by dom(t 0 ) = {p 0 | p · p 0 ∈ dom(t)}, t 0 (p 0 ) = t(p · p 0 ); t 0 is said a proper subtree of t iff p 6= . A tree is regular (a.k.a. rational) if and only if it has a finite number of distinct subtrees. A term is a tree t defined over the set of logical variables and function symbols, satisfying the following additional property: for all paths p in dom(t) and for all natural numbers n, p · n ∈ dom(t) iff t(p) = f /m (that is, t(p) is a function symbol f of arity m), and n < m. Note that, by definition, if t(p) = X , then p · n 6∈ dom(t) for all n; the same consideration applies for constants (hence, logical variables and constants can only be leaves). Regular terms can be finitely represented by means of term unification problems [19], that is, systems of a finite number of equations [11, 2] of the form X = t (where t is a finite term which is not a variable). Note that Horn formulas are built over finite terms; infinite terms are only needed for defining coinductive Herbrand models. The definition of coinductive Herbrand universe and base of a Horn formula Hf is a straightforward extension of the conventional definition of inductive Herbrand universe and base, where terms are defined as above. A useful characterization of coinductive Herbrand models is based on the notion of idealized proof tree [19, 18]. An idealized proof tree T (proof for short) for a Horn formula Hf is a tree defined over the coinductive Herbrand base of Hf , satisfying the following additional property: for all paths p in dom(T ), if T (p) = A, m = min{n | p · n 6∈ dom(T )}, and for all n < m T (p · n) = An , then A ← A0 , . . . , Am−1 is a ground instance of a clause of Hf . A proof T for a Horn formula Hf is a proof of the atom A iff T () = A. It can be proved [19, 18] that {A | A ground, ∃ proof T for Hf of A } is the coinductive Herbrand model of Hf . 15
4.1
Encoding of programs into Horn formulas
The encoding of programs for the idealized type system is defined in Figure 5, whereas Figure 6 contains the set of clauses Hf default which are shared by all programs. For completeness, all rules and clauses have been included, even though some of them are the same as those defined in Figure 3 and Figure 4. Rules and clauses which are different are highlighted. Soundness of the encoding is claimed in Section 5, where the system is extended with subtyping and subsumption. For simplicity, as already done in Figure 4, in Figure 6 we have used some convenient abbreviation; besides ¬dec field and ¬dec meth, inequality has been introduced for field names; however, since the set of all field names declared in a program is finite, 6= could be trivially defined by conjunctions of facts, therefore all formulas could be turned into Horn clauses. Before explaining some details, it is interesting pointing out the main differences with the encoding defined in Section 2. Here only those methods which might be invoked during the execution of the main expression are required to be type safe, and no overriding rule is imposed. These differences stem from the fact that the encoding of Section 2 corresponds to the specification of a typechecking algorithm (since programs are fully annotated with types), whereas here we are specifying a type inference algorithm which has to work with programs which may have no type annotations at all. Earlier error detection is sacrificed in favor of a more precise type analysis. This approach is not new, indeed it is followed by most of the proposed solutions to the problem of type inference of object-oriented programs [17, 16, 1, 12]. More in details, these two main differences are reflected by the fact that in rule (prog) only the atoms Bm generated from the main expression are considered, and that in Figure 6 no override predicate is defined. Note that the type system could be easily made more restrictive, by adding to Bm in rule (prog) n the atoms Bcd n generated from cd as follows: all atoms contain distinct logical n variables, and has meth(b c , m, b A, R) is in Bcd n iff class c is declared in cd and declares a method m. Then it would be possible to accept only programs s.t. the formula Bm , Bcd n is satisfiable (for types different from the bottom). In this way, the type system would reject programs containing methods which are inherently type unsafe, even though unused. On the other hand, a method m which is not inherently type unsafe as m(x){x.foo()} would not be well-typed in a program where no class declares a method foo. We only comments rules and clauses which are new or significantly different w.r.t. those given in Section 2. The clause generated from rule (constr-dec) is very similar to that in Figure 3, except for the following two differences: (1) the type returned by new is the k
k k structured type obj (b c , [b f :t 0 |R]) where the types t 0 of the fields f declared in k the class are determined by the initializing expressions e 0 , whereas R is the record assigning types to the inherited fields, which is derived from the type obj (P, R) returned by the invocation of the constructor of the direct superclass;
16
(prog)
∀ i = 1..n cd i Hf i e in ∅ (t | Bm ) n cd e (Hf default ∪ (∪i=1..n Hf i )|Bcd n )
(field)
τ f ; in c c, b f , τb) ← true. dec field (b
n
∀ i = 1..n fd i in c1 Cl i cn in fd Cl ∀ j = 1..m md j in c1 Hf j Hf fds = ∪i=1..n {Cl i } Hf mds = ∪j=1..m Hf j ff (class) n m class(cb1 ) ← true. ∪ class c1 extends c2 { fd cn md } extends(cb1 , cb2 ) ← true. Hf fds ∪ {Cl } ∪ Hf mds ∀ i = 1..m ei in {x n } (constr-dec)
∀ j = 1..k ej0 in {x n }
(ti | Bi ) k
(tj0 | Bj0 )
k
c(τ x n ) {super(e m ); f = e; } in τ 0 f ; k n n n m x ], obj (b c , [b f :t 0 |R])) ← type comp([b x ], [b τ ]), B , new (b c , [b extends(b c, P ), k m new (P, [t ], obj (P, R)), B 0 , k k type comp([t 0 ], [τb0 ]). n
(meth-dec)
∀ i = 1..n ei in V (ti | Bi ) R fresh n n new c(e n ) in V (R | B , new (b c , [t ], R))
(new)
(var)
(b x | true)
x in V
(invk)
(if)
e in {This, xb } (t | B ) τ0 m(τ x n ){e} in c dec meth(b c , m) b ← true. n has meth(b c , m, b [This, xb ], t) ← type comp(This, b c ), n n type comp([b x ], [b τ ]), B , type comp(t, τb0 ).
x ∈V
(field-acc)
e in V (t | B ) R fresh e.f in V (R | B , field acc(t, b f , R))
∀ i = 0..n ei in V (ti | Bi ) R fresh n n e0 .m(e ) in V (R | B0 , B , invoke(t0 , m, b [t ], R)) n
e in V (t | B ) e1 in V (t1 | B1 ) e2 in V (t2 | B2 ) R fresh if (e) e1 else e2 in V (R | B , B1 , B2 , cond (t, t1 , t2 , R))
(true)
true in V
(bool | true)
(false)
false in V
(bool | true)
Fig. 5. Encoding of programs for the idealized type system (rules with underlined name in bold are those different w.r.t. Figure 3).
17
class(object) ← true. subclass(X , X ) ← class(X ). subclass(X , object) ← class(X ). subclass(X , Y ) ← extends(X , Z ), subclass(Z , Y ). type comp(bool , bool ) ← true. type comp([ ], [ ]) ← true. type comp([T1 |L1 ], [T2 |L2 ]) ← type comp(T1 , T2 ), type comp(L1 , L2 ). ∗type comp(obj (C1 , X ), C2 ) ← subclass(C1 , C2 ). ∗type comp(T1 ∨ T2 , C ) ← type comp(T1 , C ), type comp(T2 , C ). ∗field acc(obj (C , R), F , T ) ← has field (C , F , TA), field (R, F , T ), type comp(T , TA). ∗field acc(T1 ∨ T2 , F , FT1 ∨ FT2 ) ← field acc(T1 , F , FT1 ), field acc(T1 , F , FT1 ). ∗field ([F :T |R], F , T ) ← true. ∗field ([F1 :T1 |R], F2 , T ) ← field (R, F2 , T ), F1 6= F2 . ∗invoke(obj (C , S ), M , A, R) ← has meth(C , M , [obj (C , S )|A], R). ∗invoke(T1 ∨ T2 , M , A, R1 ∨ R2 ) ← invoke(T1 , M , A, R1 ), invoke(T2 , M , A, R2 ). ∗new (object, [ ], obj (object, [ ])) ← true. has field (C , F , T ) ← dec field (C , F , T ). has field (C , F , T1 ) ← extends(C , P ), has field (P , F , T1 ), ¬dec field (C , F , T2 ). ∗has meth(C , M , A, R) ← extends(C , P ), has meth(P , M , A, R), ¬dec meth(C , M ). ∗cond (T1 , T2 , T3 , T2 ∨ T3 ) ← type comp(T1 , bool ). Fig. 6. Definition of Hf default for the idealized type system (clauses marked with an asterisk are those different w.r.t. Figure 4).
(2) n logical variables x n need to be explicitly introduced since such variables are used for passing the actual types each time the constructor is invoked. This difference is reflected in the environment V used in the judgments for expressions which simply contains the parameters, but no associated types (see also the rule (var)). Finally, since type annotations can be empty, we have to define b ; because type comp(t, b ) must be always true (no additional constraint is imposed), for simplicity we adopt the convention that b always generates a fresh variable. In rule (meth-dec) the main differences w.r.t. Figure 3 (except for those already mentioned for (constr-dec)) are that this has to be dealt as an implicit parameter (the first of the list) of the method, and that no rule on overriding13 is imposed. Predicate dec meth has only two arguments since the types of arguments and of the returned value are no longer needed.14 For what concerns Figure 6, new clauses have been introduced to deal with union types: invoking a method M with arguments of type A on an object of type T1 ∨ T2 is correct if the same method with the same argument type can be invoked on type T1 and on type T2 , and the resulting type is the union of the two obtained types R1 and R2 . Note that conditional expressions can be typed in a more precise way with the union of the types of the two branches. 13 14
Ignoring the overriding rule is safe, as explained at the end of Section 3.3. The corresponding predicate in Figure 4 has four arguments for properly defining predicate override.
18
In the first clause of predicate field acc, after retrieving the type of the field from the record part of the type of the object (a new predicate field has been introduced), it is checked that such a type is compatible with the type annotation associated with the field declaration. This check can be useful for analyzing open expressions (for closed expressions the check is redundant since is already done at creation time).
5
Extending the system with subtyping
In this section we extend the idealized type system presented in Section 3 and Section 4 with subtyping and subsumption; rather than increasing the expressive power of the system, subtyping and subsumption allow sound (but not complete) implementation of the system, by supporting approximation of infinite non regular types and proof trees with regular ones. 5.1
Non regular types
Non regular (hence infinite) types may be inferred for quite simple expressions. For instance, assume to add to class List the following method declaration: balList ( i ){ i f (i