CS 565 October 27, 1997 Jens Palsberg Readings: OOTS, Chapter 1–4

Lecture Note 9: The Java Type System

Contents 1 Motivation 2 2 The BOPL Language 3 3 Type Checking 13 1 Motivation

• Work in the tradition of SIMULA, BETA, C++, EIFFEL, and Java.

• Initial observation: “types = sets of classes” gives simple description of existing type systems.

• Goal: to explain and improve these—not to overthrow them.

2 The BOPL Language

• Basic Object Programming Language.

• An untyped language model underlying our discussions.

• Supports the essential features:

– classes and objects;

– assignments; and

– late binding.

• Inheritance and genericity are not considered at this stage.

The Essence of Objects

• A program consists of a class library and a main expression.

• A class contains methods and instance variables.

• An object is an instance of a particular class.

• The evaluation of an expression yields a value and may modify the state.

• A value is either an integer, a boolean, a pointer to an object, or the constant nil.

Expressions

• Object creation.

• Assignment to instance variables.

• Conditionals.

• Iteration.

• Sequencing.

• The self meta-variable.

• Message sends to objects.

• Dynamic verification of the class of an object.

Syntax

• Like SMALLTALK, but with a SIMULAsque syntax.

program ::= classlist? exp

classlist? ::= ε | classlist classlist ::= class | classlist class

class ::= class id varlist? methodlist? end | class id is id

methodlist? ::= ε | methodlist methodlist ::= method | methodlist method method ::= method id formals exp end

varlist? ::= ε | varlist varlist ::= var | varlist var var ::= var dec

formals ::= ( declist? ) declist? ::= ε | declist declist ::= dec | declist ; dec dec ::= idlist idlist ::= id | idlist , id

More Syntax

exp ::= int | exp binop exp | false | true | exp not | id := exp | exp ; exp | if exp then exp else exp end | while exp do exp end | nil | self | id new | exp class new | exp instance-of { idlist } | id | exp . id ( explist? ) | ( exp )

binop ::= + | - | * | = | and | or | < explist? ::= ε | explist explist ::= exp | explist , exp

int ::= digit | digit int digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 id ::= letter | id letter | id digit letter ::= a | b | … | z | A | B | … | Z

The State

• A BOPL expression is evaluated in a state consisting of:

– the class library;

– the heap;

– the argument map; and

– the self object.

• The value of an expression is either:

– the constant nil;

– an integer;

– a boolean; or

– a pointer to an object.

• An object is a record that contains:

– the name of its class; and

– a value for each instance variable.

A Possible State

[figure: A; Heap; A; A; B; B; x; y; x; y; z; x; y; z; class A; var x,y; class B; var z; method m(p,q); end; end; Class Library; Argument Map; Self Object; p; q; 87; nil; 0; 12; 17]

• Plus run-time stack.

• Naïve implementation.

Run-Time Errors

• The possible run-time errors are:

– type mismatch in unary and binary operators;

– conditions in if and while not evaluating to booleans;

– the class of a receiver not implementing a method for the message it is being sent; or

– the receiver evaluating to nil.

• The last two are combined known as message-not-understood.

Lists

class List var head, tail method Set(h, t)

head := h;

tail := t;

self end method Cons(h)

(self class new).Set(h, self) end method Car()

head end method Cdr()

tail end

end

(List new).Cons(13).Cons(12).Cons(11).Cdr().Car()

Evaluation of Lists

head tail nil Listnil

[figure: head; tail; List; 13; head; tail; List; nil; nil]

[figure: nil; nil; head; tail; List; 13; 12; head; tail; List; head; tail; List]

[figure: nil; nil; head; tail; List; 13; head; tail; List; 11; head; tail; List; 12; head; tail; List]

[figure: nil; nil; head; tail; List; 13; head; tail; List; 11; head; tail; List; 12; head; tail; List]

3 Type Checking

What is a Type?

• A type is an invariant.

• If a variable v contain only integer values, then we may say that v has type Int.

• Thus, the declaration

var v: Int specifies an invariant about v.

• A program in which all type declarations are valid invariants is said to be type correct.

• This is formally undecidable, but can be approximated.

• If a type checker can verify that a program is type correct, then the program is statically type correct.

Why Types?

An untyped program may be:

• Unreadable. Types provide documentation; “Well-typed programs are more readable”

• Unreliable. Types provide a safety guarantee; “Well-typed programs cannot go wrong”

• Inefficient. Types enable optimizations; “Well-typed programs are faster”

World-Assumptions

• The closed-world assumption:

All parts of the program are known at compile-time. Type checking is global for the entire program.

• The open-world assumption:

Some parts of the program are unknown at compiletime. Type checking is local for each module.

• Both make sense, but at different stages of the development process.

• This has significant influence on typing issues.

Types are Sets of Classes

• The type of E is denoted [[E]].

• [[E]] = {A, B, C, …}.

• Invariant: E can only evaluate to nil or an instance of a class mentioned in [[E]].

• Generalizes the “class types” of C++, SIMULA, BETA, and Eiffel (they correspond to special sets).

• Explains subtyping, dynamic typing, and the type of nil.

• Note: only values have types, and methods are not values.

• Limitation: no type of even integers, unless we create a subclass.

What is Subtyping?

• When can the assignment x := E be allowed?

• The invariant on x must be respected.

• Clearly, [[x]] ⊇ [[E]] is sufficient.

• So, subtyping is set inclusion.

• This is required of all statically type correct programs.

What is the Type of nil?

• It is {} (the empty set).

• This satisfies the invariant:

If nil evaluates to an instance (which it does not), then the class of that instance is in {}.

• The assignment x := nil is always allowed, since [[x]] ⊇ {}.

• This becomes awkward if types are just classes: nil must then be an instance of a paradoxical common subclass of all classes.

Type Annotations

• They are placed at e.g. instance variables:

var x: {A,B,C} Only instances of the classes A, B, C can get stored in the variable x.

• They have consequences throughout the program:

\begin{matrix}E & . m( …) \\ ↑ \\ \{A,B,C\} \end{matrix}

Control will be passed to the implementation of m in either A, B, or C.

• Uses only notions already present in the programming language.

• No elaborate language of type expressions must be mastered.

Safety Guarantee

• Avoid the error: message-not-understood.

• When the type of the receiver is known:

\begin{matrix}E & . m( …) \\ ↑ \\ \{A,B,C\} \end{matrix}

check that the classes A, B, and C each implement a method named m.

• The safety of message sends at run-time is then guaranteed at compile-time.

• But the receiver may still be nil (requires data flow analysis).

Optimization

• With safety guarantee, leave out checks for messagenot-understood.

• Inline message sends if type of receiver is a singleton.

• Reject message sends if type of receiver is empty.

• Use hash functions to dispatch messages.

• Discard “dead code”:

\begin{matrix}E & . m( …) \\ ↑ \\ \{A,B,C\} \end{matrix}

e.g. implementations of m in classes other than A, B, and C.

Constraint Technique

• Types must be declared, propagated, and verified.

• Constraints give a uniform presentation.

• Each expression E has a type variable [[E]].

• From the syntax of a program we generate constraints that express:

– the declared type information;

– the propagation of types; and

– the requirements for static type correctness.

• The program is statically type correct if these constraints are solvable.

Constraints for Pascal

• Program:

var x,y: Integer;

var z: array of Boolean;

writeln(x+y); z[87] := true;

• Type constraints:

decl [[x]] = Integer decl [[y]] = Integer decl [[z]] = array of Boolean vfy [[x]] = [[y]] prop [[x]] = Integer ⇒ [[x+y]] = Integer prop [[x]] = Real ⇒ [[x+y]] = Real prop [[87]] = Integer prop [[true]] = Boolean vfy [[z[87]]] = [[true]] prop [[z]] = array of [[z[87]]] Annotations in BOPL

• For now, types are finite sets of classes.

• We will attach types three places in programs.

• At instance variables; the associated invariant is that the instance variable can only contain nil or the values described by the type.

• At formal arguments; the associated invariant is the same as for instance variables.

• At method results; the associated invariant is that the body of the method can only evaluate to nil or the vales described by the type.

An Annotated Program

class A var x: A var y: {A,B,Int} method m(f: B) returns B

f.m(x) end

end

class B

method m(g: A) returns B

self end

end

(A new).m(B new).m(nil)

Constraints for BOPL

• Type variables as before.

• Assume that all variable and argument names are distinct (rename if necessary).

• Use the notation:

[[x]], [[f]], and [[E]]

for the type variables associated with the instance variable x, the formal argument f, and the expression E.

• Use the notation [[self-C]] for the self meta-variable in the class C.

• Generate constraints from syntax of expressions.

Type Constraints

1) “int”. For any integer constant k we have:

[[k]] = {Int}

2) “true” and “false”. For the boolean constants we have:

[[true]] = {Bool} and [[false]] = {Bool}

3) “nil”. For the nil constant we have the constraint:

[[nil]] = {}

4) “exp_{1} + exp_{2}”. For the addition we have:

[[exp_{1}]] = {Int}
[[exp_{2}]] = {Int}
[[exp_{1} + exp_{2}]] = {Int}

11) “exp not”. For the negation we have:

[[exp]] = {Bool} [[exp not]] = {Bool} 12) “id := exp”. For the assignment we have:

[[exp]] ⊆ [[id]]

[[id := exp]] = [[exp]]

13) “exp_{1} ; exp_{2}”. For the sequence we have:

[[exp_{1} ; exp_{2}]] = [[exp_{2}]]

14) “if exp_{1} then exp_{2} else exp_{3} end”. For the

conditional we have:

[[exp_{1}]] = {Bool}

[[if exp_{1} then exp_{2} else exp_{3} end]]
= [[exp_{2}]] ∪ [[exp_{3}]]

15) “while exp_{1} do exp_{2} end”. For the iteration we

have:

[[exp_{1}]] = {Bool}
[[while exp_{1} do exp_{2} end]] = {}

16) “self”. For the self meta-variable we have:

[[self-C]] = {C}

17) “id new”. For object creation we have

[[id new]] = {id}

18) “exp class new”. For indirect object creation we

have:

[[exp]] ⊆ {all classes} [[exp class new]] = [[exp]]

19) “exp instance-of {idlist}”. For dynamic check of class membership we have:

[[exp instance-of { idlist }]] = { idlist }

20) “id”. For a variable or a formal argument we have:

[[id]] = the declared type 21) “exp . id ( exp_{1}, …, exp_{n} )”. For the

message send, the constraints are:

[[exp]] ⊆ {all legal receiver classes}

C ∈ [[exp]] ⇒ [[exp_{1}]] ⊆ [[id_{1}]] ⋮ [[exp_{n}]] ⊆ [[id_{n}]] [[exp.id(exp_{1},…,exp_{n})]] ⊇ type ⊇ [[exp_{0}]]

where C implements a method for the message:

method id(id_{1}:type_{1}, …, id_{n}:type_{n})

returns type

exp_{0} end An Example

1 class A 2 var x: A 3 var y: {A,B,Int} 4 method m(f: B) returns B 5 f.m(x) 6 end 7 end 8 9 class B 10 method m(g: A) returns B 11 self 12 end 13 end 14 15 (A new).m(B new).m(nil)

Types for all subexpressions:

[[x]] = [[g]] = {A} [[y]] = {A,B,Int} [[f]] = {B} [[f.m(x)]] = {B} [[self-B]] = {B} [[A new]] = {A} [[B new]] = {B} [[(A new).m(B new)]] = {B} [[nil]] = {} [[(A new).m(B new).m(nil)]] = {B}

More Example

2) 20) [[x]] = {A}
3) 20) [[y]] = {A,B,Int}
4) 20) [[f]] = {B}
10) 20) [[g]] = {A}
5) 21) [[f]] ⊆ {A,B}
5) 22) A ∈ [[f]] ⇒ \{ \begin{matrix}[[x]] ⊆ [[f]] \\ [[f.m(x)]] ⊇ \{B\} ⊇ [[f.m(x)]] \end{matrix}
5) 22) B ∈ [[f]] ⇒ \{ \begin{matrix}[[x]] ⊆ [[g]] \\ [[f.m(x)]] ⊇ \{B\} ⊇ [[self-B]] \end{matrix}
11) 16) [[self-B]] = {B}
15) 17) [[A new]] = {A}
15) 17) [[B new]] = {B}
15) 21) [[A new]] ⊆ {A,B}
15) 22) A ∈ [[A new]] ⇒

[[B new]] ⊆ [[f]]

[[(A new).m(B new)]] ⊇ {B} ⊇ [[f.m(x)]]

15) 22) B ∈ [[A new]] ⇒

[[B new]] ⊆ [[g]]

[[(A new).m(B new)]] ⊇ {B} ⊇ [[self-B]]

15) 3) [[nil]] = {}
15) 21) [[(A new).m(B new)]] ⊆ {A,B}
15) 22) A ∈ [[(A new).m(B new)]] ⇒

[[nil]] ⊆ [[f]]

[[(A new).m(B new).m(nil)]] ⊇ {B} ⊇ [[f.m(x)]]

15) 22) B ∈ [[(A new).m(B new)]] ⇒

[[nil]] ⊆ [[g]]

[[(A new).m(B new).m(nil)]] ⊇ {B} ⊇ [[self-B]]

Dynamic Type Checks

• method TransPrint(g: {Circle,Rectangle,Triangle})

returns {Circle,Rectangle,Triangle}

g.Translate(100,200); g.Print; g end

c:=(App new).TransPrint(c)

• Not statically correct, but yields no run-time error.

• The compiler responds:

– I know what I am doing. Go away! The program has been rejected for a good reason.

– This is not my problem, its yours! The programmer may insert a dynamic check.

– I will take care of this!

The compiler will insert a dynamic check.

• c:=((App new).TransPrint(c) instance-of Circle)

When to Use Them

• If dynamic checks are inserted blindly, then any program will be accepted by the compiler.

• Want to make use of the declared type information as far as possible.

1) When a program fragment is statically correct, then it is accepted and no dynamic checks are inserted.

2) When a program fragment is statically incorrect but might succeed at run-time, then it is accepted and dynamic checks are inserted.

3) When a program fragment is statically incorrect and cannot succeed at run-time, then it is rejected.

• Minimize the number of dynamic checks and filter out most meaningless programs.

How to Use Them

• At an assignment id := exp.

• Constraint is [[id]] ⊇ [[exp]].

1) [[id]] ⊇ [[exp]]. The expression will always evaluate to an object of a legal class.

2) [[id]] ∩ [[exp]] ≠ {}. The expression might evaluate to an object of a legal class. The program is accepted after the assignment has been transformed into id := (exp instance-of [[id]]).

3) [[id]] ∩ [[exp]] = {}. The expression cannot possibly evaluate to an object of a legal class. The program is rejected.

• Similarly for arguments and return values.

An Example

• Before: class A

var x: {Int, Bool} var y: Int method m(f: A) returns Int

   y := x

end end

class B

method m(g: {A, B}) returns Int

g.m(g) end

end

• After: class A

var x: {Int, Bool} var y: Int method m(f: A) returns Int

y := (x instance-of Int) end

end

class B

method m(g: {A, B}) returns Int

g.m( g instance-of A ) end

end

Inheritance: Class Types

• type ::= Void | Int | Bool | ↑C

• Special sets:

– the expression Void specifies the empty set;

– the expressions Int and Bool specify the sets {Int} and {Bool}; and

– the expression ↑C specifies the set of C and all its subclasses.

• Under the closed-world assumption, they denote finite sets.

• Under the open-world assumption, they denote infinite sets.

• Type constraints can be systematically derived from the general ones.

Properties

• Subset inclusion can be decided:

⊆ Void Int Bool ↑C
Void true true true true
Int false true false false
Bool false false true false
↑D false false false D ≤ C

• D ≤ C is transitive, reflexive subclassing.

• For \underline{class types}, subtyping is subclassing.

• [figure: D; C]

• Class types are closed under intersection, but not under union.

Derived Type Constraints

1) “int”. For any integer constant k we have: [[k]] = Int

2) “true” and “false”. We have:

[[true]] = Bool and [[false]] = Bool

3) “nil”. For the nil constant we have:

[[nil]] = Void

4) “exp_{1} + exp_{2}”. For the addition we have: [[exp_{1}]] = Int ∧ [[exp_{2}]] = Int

[[exp_{1} + exp_{2}]] = Int

12) “id := exp”. For the assignment we have: [[exp]] ⊆ [[id]] ∧ [[id := exp]] = [[exp]] Derived Type Constraints

14) “if exp_{1} then exp_{2} else exp_{3} end”. We have:

[[exp_{1}]] = Bool
[[if exp_{1} then exp_{2} else exp_{3} end]]
= [[exp_{2}]] = [[exp_{3}]]

19) “exp instance-of ↑id”. For the dynamic check of class membership we have:

[[exp instance-of ↑id]] = ↑id

21) Message sends:

[[exp]] ⊆ {all legal receiver classes}

This constraint is false if [[exp]] is Int or Bool. If [[exp]] = ↑C then C must implement a method for the message.

C ∈ [[exp]] ⇒ …

Suppose [[exp]] is a cone set of the form ↑D for some class D. The condition C ∈ [[exp]] can then be verified by checking that C is a subclass of D.

No Surprises

• The derived constraints correspond closely to the type rules for C++, SIMULA, BETA, and EIFFEL.

• Supports the open-world assumption and, hence, separate compilation.

• Gives quite a few more dynamic checks, since the types are much coarser.

Other Type Systems

• A general recipe for new type systems.

• Define new type expressions.

• Show how to expand into sets.

• Under the closed-world assumption, that’s it!

• Under the open-world assumption, specialize the type constraints.

• This may involve development of new algorithms for ⊆, ∩, and ∪ on type expressions.

• Suggestions: union types, interfaces.

Summary

• Types improve the readability, reliability, and efficiency of programs.

• A type is a set of classes, and subtyping is set inclusion.

• A type expresses an invariant of run-time behavior.

• The programs that satisfy this invariant are called type correct.

• Static type correctness is a decidable property that implies type correctness.

• Dynamic type correctness holds when the type invariants are checked at run-time.

• Dynamic typing is necessary for realistic applications.