An Event-Based Structural Operational Semantics of Multi-Threaded Java Pietro Cenciarelli?, Alexander Knapp, Bernhard Reus, and Martin Wirsing Ludwig{Maximilians{Universitat Munchen
fcenciare,knapp,reus,
[email protected]
Abstract A structural operational semantics of a signi cant sublan-
guage of Java is presented, including the running and stopping of threads, thread interaction via shared memory, synchronization by monitoring and noti cation, and sequential control mechanisms such as exception handling and return statements. The operational semantics is parametric in the notion of \event space" [6], which formalizes the rules that threads and memory must obey in their interaction. Dierent computational models are obtained by modifying the well-formedness conditions on event spaces while leaving the operational rules untouched. In particular, we implement the prescient stores described in [10, 17.8] which allow certain intermediate code optimizations, and prove that such stores do not aect the semantics of properly synchronized programs. x
1 Introduction The object-oriented programming language Java oers simple and tightly integrated support for concurrent programming. In Java's model of concurrency multiple threads of control run in parallel and exchange information by operating on objects which reside in a shared main memory. A precise informal description of this model is given in the Java language speci cation [10]. Other notable references are [4] and [12]. This paper presents a formal semantics of a signi cant sublanguage of Java including the running and stopping of threads, thread interaction via shared memory, synchronization by monitoring and noti cation, and sequential control mechanisms such as exception handling and return statements. Here we focus on the dynamic semantics of Java and leave a detailed treatment of the static, type-related aspects of the language, e.g. class declarations, to a followup paper. Our semantics is given in the style of Plotkin's structural operational semantics (SOS) [15]. In SOS, which has been used in the past for describing SML [13], evaluation is driven by the syntactic structure of programs. This allows a powerful proof technique for semantic analysis: structural induction. The idea inspiring the present work is that the semantics of real concurrent languages such as Java, with complex, interacting control features can be given in full detail by means of simple structural rules. ?
Research partially supported by the HCM project CHRX-CT94-0591 \De Stijl."
One of the diculties in modelling concurrent Java programs consists in capturing the complex interplay of memory and thread actions during execution. Each thread of control has, in Java, a private working memory in which it keeps its own working copy of variables that it must use or assign. As the thread executes a program, it operates on these working copies. The main memory contains the master copy of each variable. There are rules about when a thread is permitted or required to transfer the contents of its working copy of a variable into the master copy or vice versa. The process of copying is asynchronous. There are also rules which regulate the locking and unlocking of objects, by means of which threads synchronize with each other. All this is described precisely in [10, 17] in terms of eight kinds of low-level actions: Use , Assign , Load , Store , Read , Write , Lock , and Unlock . Here is an example of a rule from [10, 17.6, p. 407] involving locks and variables. Let T be a thread, V a variable and L a lock: \Between an Assign action by T on V and a subsequent Unlock action by T on L, a Store action by T on V must intervene; moreover, the Write action x
x
corresponding to that Store must precede the Unlock action, as seen by the main memory."
These rules impose constraints on any implementation of Java so as to allow a correct exchange of information among threads. On the other hand they intentionally leave much freedom to the implementor, thus permitting certain standard hardware and software techniques to improve the speed and eciency of concurrent code. Therefore, it is only on the given rules that the programmer should rely to predict the possible behaviour of a concurrent program. Likewise, it is only the given rules that should constrain the possible execution traces generated by a correct operational semantics. The above considerations led us to base our semantics on the notion of event space. These correspond roughly to con gurations in Winskel's event structures [21] which are denotational, non-interleaving models of concurrent languages. The use of such structures in (interleaving) operational semantics is new. It allows us to give an abstract, \declarative" account of the Java thread model while retaining the virtues of a structural approach. This description is a straight formal paraphrase of the rules of [10]. Event spaces were introduced in [6], where we showed that their use in modelling multi-threading preserves the naive semantics of \sequential" computations (i.e. computations where one thread interacts synchronously with the memory). Basing our description of Java on the nely grained notion of event allowed us to observe phenomena which may be not readily seen when more abstract approaches are taken. For example, we realized that the asynchrony of communication between main memory and working memories (viz. the loose coupling of Read and Load actions, and similarly of Store and Write ) is actually observable in Java. Let threads 1 and 2 , respectively running the code (1 ) synchronized(p) { p.y = 2; } a = p.x; b = p.y; c = p.y; (2 ) synchronized(p) { p.y = 3; p.y = 100; } p.x = 1;
share a main memory in which p.x = p.y = 0, and let their working memories be initially empty. No parallel execution of 1 and 2 in which main and working memories interact synchronously would possibly allow the values 1, 2 and 3 to be assigned respectively to a, b and c. Any model of execution not capable of producing a run with this assignment of values, indeed possible as we show in Section 2.3, provides maybe a correct implementation, but cannot be considered correct as semantics of Java. The operational semantics presented below is parametric in the notion of event space. This allows dierent computational models to be obtained by modifying the well-formedness conditions on event spaces while leaving the operational rules untouched. To show the exibility of this approach we study the \prescient" store actions introduced in [10, 17.8]. Such actions allow optimizing compilers to perform certain kinds of code rearrangements. A bisimulation is given to prove that such rearrangements preserve the semantics of properly synchronized programs (see also [17]). x
Related work. Several other semantics of sublanguages of Java are available in the literature. Much work has also been done on the semantics of the Java Virtual Machine [7, 16, 18]; this is one half of a formal semantics of the language, the other half being a description of a Java-to-Virtual Machine bytecode compiler, not available to date. In this volume Drossopoulou and Eisenbach [8] give a \small-step" structural operational semantics which covers roughly the sequential part of our sublanguage of Java; their work, which is mainly concerned with proving type soundness, has been formalized by Syme [19]. Von Oheimb and Nipkow [14] also deal with a sequential sublanguage of Java and give a formal proof of type safety. A noteworthy dierence between [8] and [14] is that the latter follows a \big-step" approach. In [9] Flatt, Krishnamurthy and Felleisen investigate the semantics of operators for combining Java classes (so-called \mixins"). All these semantics focus on type soundness for a sequential portion of Java. As for multi-threading, non-structural descriptions based on abstract state machines (see [11]) are given by Borger and Schulte [5], and by Wallace [20]. Synopsis. Section 2 describes and formalizes the Java memory-threads communication protocol. Section 3 presents our event-based, structural operational semantics of Java. Section 4 studies the notion of prescient store action. Loose ends and future research are discussed in Section 5.
2 Event Spaces In this section we describe and formalize the memory-threads communication protocol of Java. This is done by writing the rules of [10, 17] as simple logical clauses (Section 2.2) and by adopting them as well-formedness conditions on structures called event spaces (Section 2.4). The latter are used in the operational judgements to constrain the applicability of some operational rules. An example x
of event space is given in Section 2.3, describing the \1-2-3" parallel run of the threads 1 and 2 introduced above.
2.1 Actions and events
A formal notion of event is given below in terms of ve sets of entities: { Use ; Assign ; Load ; Store ; Read ; Write ; Lock ; Unlock , the action names; { Thread id, the thread identi ers; { Obj, the objects; { LVal, the left values (or \variables," following [10]) and { RVal, the (right) values. Intuitively, Use and Assign actions do just what their names suggest, operating on the private working memories. Read and Load are used for a loosely coupled copying of data from the main memory to a working memory and dually Store and Write are used for copying data from a working memory to the main memory. Lock and Unlock are for synchronizing the access to objects. Formally, an action is either a triple (A; ; o), where A Lock ; Unlock , is a thread (identi er) and o is an object, or a 4-tuple of the form (A; ; l; v), where A Use ; Assign ; Load ; Store ; Read ; Write , l is a variable, v is a value and is as above. When A Use ; Assign ; Load ; Store , the tuple (A; ; l; v) records that the thread performs an A action on l with value v, while, if A Read ; Write , it records that the main memory performs an A action on l with value v on behalf of . If A is Lock or Unlock , (A; ; o) records that acquires, or respectively relinquishes, a lock on o. Actions with name Use , Assign , Load , Store , Lock and Unlock are called thread actions, while Read , Write , Lock and Unlock are memory actions. Events are instances of actions, which we think of as happening at dierent times during execution. We use the same tuple notation for actions and their instances: the context clari es which one is meant. When no confusion arises we may omit components of an action or event which are not immediately relevant in the context of discourse: so (Read ; l) stands for (Read ; ; l; v), for some and v. Given a thread , we write () for a generic instance of a thread action performed by . Similarly, (x) indicates a generic instance of a memory action involving a location or object x. f
g
2 f
2 f
g
g
2 f
2 f
g
g
2.2 The rules of interaction
Here we formalize the rules of [10, Chapter 17], to which we refer for a detailed discussion. These rules are translated into logical clauses describing the properties of a poset of events called the \poset of discourse." The events of such a poset, which are thought of as occurring in the given order, are meant to record the activity of memory and threads during the execution of a Java program. We assume that every chain of the poset of discourse can be counted monotonically: a0 a1 a2 : : : . The clauses in our formalization have the form: a : ( (( b1 : 1 ) ( b2 : 2 ) : : : ( bn : n )))
8
2
)
9
2
_
9
2
_
9
2
where a and bi are lists of events, is the poset of discourse and a : means that holds for all tuples of events in matching the elements of a (and similarly for bi : i ). The clauses are abbreviated by adopting the following conventions: quanti cation over a is left implicit when all events in a appear in ; quanti cation over bi is left implicit when all events in bi appear in i . Moreover, a rule of the form a : (true : : : ) is written a (: : : ). When the symbols and 0 appear in a rule, we always assume that = 0 . Similarly for values v and v0 , and for events a and a0 . The rules are the following: The actions performed by any one thread are totally ordered, and so are the actions performed by the main memory for any one variable or lock [10, 17.2, 17.5]. 8
9
2
2
8
2
)
) 6
x
x
(); 0 () (x); 0 (x)
() 0 () 0 () () (1) 0 0 (x) (x) (x) (x) (2) Hence, the occurrences of any action (A; ; x) are totally ordered in the poset of discourse. We write (A; ; x) the subposet of including only instances of (A; ; x). A Store action by on l must intervene between an Assign by of l and a subsequent Load by of l. Less formally, a thread is not permitted to lose its )
_
)
_
most recent assign [10, 17.3]: x
(Assign ; ; l) (Load ; ; l)
)
(Assign ; ; l) (Store ; ; l) (Load ; ; l)
(3)
A thread is not permitted to write data from its working memory back to main memory for no reason [10, 17.3]: x
(Store ; ; l) (Store ; ; l)0
)
(Store ; ; l) (Assign ; ; l) (Store ; ; l)0
(4)
Threads start with an empty working memory and new variables are created only in main memory and are not initially in any thread's working memory [10, 17.3]:
x
(Use ; ; l) (Store ; ; l)
) )
(Assign ; ; l) (Use ; ; l) (Load ; ; l) (Use ; ; l) (Assign ; ; l) (Store ; ; l)
_
(5) (6)
A Use action transfers the contents of the thread's working copy of a variable to the thread's execution engine [10, 17.1]: x
(Assign ; ; l; v) (Use ; ; l; v0 ) (Assign ; ; l; v) (Assign ; ; l)0 (Use ; ; l; v0 ) (Assign ; ; l; v) (Load ; ; l) (Use ; ; l; v0 ) (Load ; ; l; v) (Use ; ; l; v0 ) (Load ; ; l; v) (Assign ; ; l) (Use ; ; l; v0 ) (Load ; ; l; v) (Load ; ; l)0 (Use ; ; l; v0)
)
_
(7)
)
_
(8)
A Store action transmits the contents of the thread's working copy of a variable to main memory [10, 17.1]: x
(Assign ; ; l; v) (Store ; ; l; v0 ) (Assign ; ; l; v) (Assign ; ; l)0 (Store ; ; l; v0 )
(9)
)
The following rules require some events to be paired in the poset of discourse. Let A and B be posets, and let f : A B indicate that a function f is either a monotonic injection A B with downward closed codomain or the partial inverse of a monotonic injection B A with downward closed codomain. For every poset satisfying (1) and (2), for every thread , left value l and object o, there exist unique functions !
!
read of ;;l : (Load ; ; l) (Read ; ; l) store of ;;l : (Write ; ; l) (Store ; ; l) lock of ;;o : (Unlock ; ; o) (Lock ; ; o):
These are called the \pairing" functions. Indices are omitted when understood. The function read of matches the n-th occurrence of (Load ; ; l) in with the n-th occurrence of (Read ; ; l) if such an event exists in and is unde ned otherwise. Similarly for store of and lock of . Each Load or Write action is uniquely paired with a preceding Read or Store action respectively. Matching actions bear identical values [10, 17.2, 17.3]: x
(Load ; ; l; v) (Write ; ; l; v)
) )
x
(Read ; ; l; v) = read of (Load ; ; l; v) (Load ; ; l; v) (10) (Store ; ; l; v) = store of (Write ; ; l; v) (Write ; ; l; v) (11)
Rules (10) and (11) ensure that read of and store of are total. We call load of and write of their partial inverses. The actions on the master copy of any given variable on behalf of a thread are performed by the main memory in exactly the order that the thread requested [10, 17.3]: x
(Store ; ; l) (Load ; ; l)
)
write of (Store ; ; l) read of (Load ; ; l)
(12)
A thread is not permitted to unlock a lock it does not own [10, 17.5]: x
(Unlock ; ; o)
)
lock of (Unlock ; ; o) (Unlock ; ; o)
(13)
Rule (13) ensures that lock of is total. We write unlock of its partial inverse. Only one thread at a time is permitted to lay claim to a lock, and moreover a thread may acquire the same lock multiple times and does not relinquish ownership of it until a matching number of Unlock actions have been performed [10, 17.5]: x
(Lock ; ; o) (Lock ; 0 ; o)
)
unlock of (Lock ; ; o) (Lock ; 0 ; o)
(14)
If a thread is to perform an Unlock action on any lock, it must rst copy all assigned values in its working memory back out to main memory [10, 17.6] (this rule formalizes the quotation in the introduction): (Assign ; ; l) (Unlock ; ) (15) (Assign ; ; l) store of (Write ; ; l) (Write ; ; l) (Unlock ; ) A Lock action acts as if it ushes all variables from the thread's working memory; before use they must be assigned or loaded from main memory [10, 17.6]: (Lock ; ) (Use ; ; l) (Lock ; ) (Assign ; ; l) (Use ; ; l) (16) (Lock ; ) read of (Load ; ; l) (Load ; ; l) (Use ; ; l) (Lock ; ) (Store ; ; l) (Lock ; ) (Assign ; ; l) (Store ; ; l) (17) x
)
x
)
_
)
Discussion. Each of the above rules corresponds to one rule in [10]. Note that the language speci cation requires any Read action to be completed by a corresponding Load and similarly for Store and Write . The above theory does not include clauses expressing such requirements because it must capture \incomplete" program executions (see Section 4). Except for read and store completion, any rule in [10] which we have not included above can be derived in our axiomatization. In particular, (Load ; ; l) (Store ; ; l) ) (Load ; ; l) (Assign ; ; l) (Store ; ; l) () of [10, x17.3] holds in any model of the axioms. In fact, by (6) there must be some Assign action before the Store ; moreover, one of such Assign must intervene in between the Load and the Store , because otherwise, from (1) and (3), there would be a chain (Store ; ; l) (Load ; ; l) (Store ; ; l) with no Assign in between, which contradicts (4). Similarly, the following rule of [10, x17.3] derives from (10) and (11): (Load ; ; l) (Store ; ; l) ) read of (Load ; ; l) write of (Store ; ; l) Clauses (6) and (17) simplify the corresponding rules of [10, x17.3, x17.6] which include a condition (Load ; ; l) (Store ; ; l) to the right of the implication. This would be redundant because of ().
2.3 Example
We brie y illustrate the above formal rules on the example given in the introduction, where two threads (1 ) synchronized(p) { p.y = 2; } a = p.x; b = p.y; c = p.y; (2 ) synchronized(p) { p.y = 3; p.y = 100; } p.x = 1; start with a main memory where both instance variables p.x and p.y have value 0, and with empty working memories, and interact so that the values 1, 2
and 3 are eventually assigned to a, b, and c respectively. We shall run part of this example through our operational rules in Section 3.7. Figure 1 describes this run as a poset of events, whose ordering is represented by the arrows. The actions of the two threads and of the main memory on the two instance variables p.x and p.y are aligned vertically in four columns. We let o be the object denoted by p, while x and y stand for the left values of p.x and p.y respectively. Since all actions performed by the same thread and by the memory on the same variable must be totally ordered, each column of Figure 1 is a chain. Moreover, some memory actions must occur before or after some thread actions. For example, a (Write ; 1 ; y; 2) must come after (Assign ; 1 ; y; 2) because, as dictated by the structure of the program, an Unlock follows the assignment p.y = 2, and hence, by (15), 1 's working copy of y must be written in main memory before the Unlock and after a corresponding Store . Note that not all the assigned values must be stored in main memory. For example, it would have been legal to omit (Store ; 2 ; y; 3) and (Write ; 2 ; y; 3); in this case, however, the value 3 would have never been passed to 1 . Similarly, not all the values used by a thread must be rst loaded from main memory: in the example no (Load ; 1 ; y; 2) precedes (Use ; 1 ; y; 2). As stated in the introduction, the above assignments to a, b and c would not be possible if communication between main and working memories where \synchronous," that is if no other event were allowed to happen between a Read and a corresponding Load or, equivalently, if these two actions were executed as a single atomic step (and similarly for Store and Write ). Assume in fact that there is a synchronous run producing a = 1, b = 2, and c = 3. Since 3 must be assigned to c, an action (Read ; 1 ; y; 3) must occur, and moreover it must be after 2 writes 3 and before it writes 100 in the master copy of y. Hence, by (15), (Read ; 1 ; y; 3) must occur while 2 is executing the synchronized block. Again by (15), a (Store ; 1 ; y; 2) must occur before 1 exits its synchronized block; moreover this Store must occur before (Read ; 1 ; y; 3), otherwise the value 3 would be lost, and therefore 1 must enter its synchronized block before 2 . Then, in order to get the value 1 for a, the assignment a = p.x must occur after 2 has left the block, it has assigned, stored and written 1 in x, and after 1 has read and loaded such value in its working copy of x. However, by the time 1 can load 1 in x, the value of y in its working memory must already be 3, because a (Read ; 1 ; y; 3) occured while 2 was executing the synchronized block. Therefore, to assign 2 to b, 1 can neither rely on the content of it's working copy of y, nor on the master copy in main memory, which, by now, must contain 100.
2.4 Event spaces
An event space is a poset of events every chain of which can be counted monotonically (a0 a1 a2 : : : ) and satisfying conditions (1) to (17) of Section 2.2. Event spaces serve two purposes in our operational semantics: On the one hand they provide all the information needed to reconstruct the working memories (which in fact do not appear in the operational judgements). On the other
(Lock ; 1 ; o)
? ?
(Assign ; 1 ; y; 2) (Store ; 1 ; y; 2)
?
-
(Write ; 1 ; y; 2)
- (Lock ; ; o) ? (Assign ; ; y; 3) ? (Store ; ; y; 3) ? (Assign ; ; y; 100) ?
(Unlock ; 1 ; o)
2
2
2
? ? (Read ; ; y; 3) ?
(Write ; 2 ; y; 3)
2
(Store ; 2 ; y; 100)
1
(Write ; 2 ; y; 100)
-(Unlock?; ; o) ? (Assign ; ; x; 1) ? 2
2
(Store ; 2 ; x; 1)
(Write ; 2 ; x; 1)
? ? (Use ; ; x; 1) ? (Use ; ; y; 2) ? (Load ; ; y; 3) ?
?
(Read ; 1 ; x; 1)
(Load ; 1 ; x; 1) 1
1
1
(Use ; 1 ; y; 3)
Figure 1. An event space for Example 2.3
hand event spaces record the \historical" information on the computation which constrains the execution of certain actions according to the language speci cation, and hence the applicability of certain operational rules (see Section 3.4). Given two event spaces (X; X ) and (Y; Y ), we say that (X; X ) is a conservative extension of (Y; Y ) when Y X and Y X and, for all a; b Y , a X b implies a Y b. To adjoin a new event a to an event space = (X; X ), we use an operation de ned as follows: a denotes nondeterministically an event space 0 = (Y; Y ) such that: { 0 is a conservative extension of , with Y = X a ; { if a = () is a thread action performed by , then a0 Y a for all thread actions a0 = 0 () by in 0 ; { if a = (x) is a memory action on x, then a0 a for all memory actions a0 = 0 (x) on x in 0 . If no event space 0 exists satisfying these conditions, then a is unde ned. For example, by (5), the term (Use ; ; l) is de ned only if a suitable (Assign ; ; l) or (Load ; ; l) occurs in . If is an event space and a = (a1 ; a2 ; : : : ; an ) is a sequence of events, we write a for a1 a2 an . As little ordering may be added to an event space by the operation as is required by the rules of interaction: indeed two expressions a b and b a may denote the same event space. This re ects the fact that the same concurrent activity may be described by dierent sequences of interleaved events. More ordering can also be introduced than strictly dictated by the rules. For example, the expression (Read ; ; o) (Lock ; ; l; v) (Load ; ; l; v) may produce an event space (Lock ; ; o) (Read ; ; l; v) (Load ; ; l; v) : although no rule enforces that (Lock ; ; o) (Read ; ; l; v), it better be so in view of rule (16) if a (Use ; ; l) is to be further added to the space.
2
[f g
f
g
3 Operational Semantics The present paper focuses on the dynamic semantics of Java. Of course, the behaviour of a program may depend on type information obtained from static analysis. Part of this information we assume is retrievable at run-time from the main memory (see Section 3.1), part goes to enrich the syntactic terms upon which the operational semantics operates (see Section 3.2). In Java every variable and every expression has a type which is known at compile-time. The type limits the possible values that the variable can hold or expression can produce at run-time. Adopting the terminology of [10], every object belongs to a class (the class of the object, the one which is mentioned when the object is created). Moreover, the values contained by a variable or produced by an expression should, by the design of the language, be compatible with the type of the variable or expression. A value of primitive type (such as booleans) is only compatible with that type (boolean), while a reference to an object is compatible with any class type which is a superclass of the object's
class [10, 4.5.5]. We do not implement run-time compatibility checks in our semantics (they can be added straightforwardly). For example, like in Java, we do not check that the object produced by evaluating the expression e in throw e; is compatible with Throwable. However, we do use type information wherever it is needed to drive computation. An example is the execution of a try-catch statement (see Section 3.8). Java's modi ers are not treated in the present paper. For example, we do not consider static elds; these would require minor changes of the semantic machinery. Similarly, synchronized methods can be easily implemented by using synchronized statements (see Section 3.7), as remarked in [10, 8.4.3.5]. After introducing in Section 3.1 semantic domains such as stores and environments, we describe a \compilation" function translating Java programs into semantically enriched abstract syntax (Section 3.2). Next, we de ne operational judgements (Section 3.3) and give the SOS rules which generate them. These are presented in homogeneous groups (expressions, statements, exceptions, etc.) in Section 3.4 to 3.10. x
x
3.1 Semantic Domains Primitive semantic domains. These are the building blocks of our operational semantics, and nothing is assumed on the structure of their elements. We call RVal the primitive domain of (right) values. These are produced by the evaluation of expressions and can be assigned to variables. A distinguished subset Obj of RVal is also given as primitive; we call its elements (references to) objects. In particular, since threads are objects in Java, we choose the domain Thread id of the previous section to be Obj. Right values come equipped with a primitive function value mapping literals to the corresponding values. value : Literal ! RVal
In particular, null is the reference to the null object denoted by the literal null, that is: null = value (null). Similarly, true = value (true) and so on. In Java the object denoted by an expression e may contain several elds with the same name i; then, the type of e decides on which eld is actually accessed by the expression e:i. An identi er together with a type are therefore a non-ambiguous name for eld access. We call FieldIdenti er, ranged over by f , the set of such pairs (see Table 1). The domain of left-values introduced in the previous section is not primitive: an instance variable is addressed by a non-null object reference o together with a eld identi er f , and written o:f . LVal = (Obj n fnull g) FieldIdenti er Store is the primitive domain of stores ranged over by . This domain comes equipped with the following primitive semantic functions, where ClassType is as in Appendix A: new : ClassType Store ! Obj Store
upd : LVal RVal Store * Store rval : LVal Store * RVal:
Besides providing storage for variables, stores are assumed to contain information produced by the static analysis of a program; typically: the names and types of elds and methods for each class, the initial values of elds, the subclass relation, and so on. This information does not change during execution and it could alternatively be kept separate from stores. Given a class type C and a store , the function new produces a new object of type C with suitably initialized instance variables, and returns it in output together with updated with the new object. We write:
o
2
C;
dropping when understood, to mean that o is a reference to an object in of a class type which is compatible with C . We also assume that the partial function init : FieldIdenti er Store * RVal returns the initial values for an object's elds. The domain of this function is the set of pairs (f; ) where f = (i; C ) and i is an appropriate eld for C in . The function upd updates a store, while rval gets the right-value associated in a store with a given left-value. These functions are partial: they are unde ned on the left-values o:f where f is not an appropriate eld for o in the given store. We write [l v] and (l) for upd (l; v; ) and rval (l; ) respectively. A rather weak axiomatization of stores is given below by using a binary predicate (written in x). The meaning of e1 e2 is that if e1 is de ned, then so is e2 and they denote the same value. By e1 e2 we mean that both e1 e2 and e2 e1 hold.
7!
'
(l) init ((i; C ); ) [l v](l) [l0 v](l) [l v0 ][l v] [l0 v0 ][l v] [l (l)]
7!
7!
'
7!
7!
7!
7!
'
7!
0 (l) 0 (o:(i; C )) v (l) [l v] [l v][l0 v0 ] 7!
7!
7!
where new (C; ) = (o; 0 ) where new (C; ) = (o; 0 ) if l = l0 6
if l = l0 6
Finally, Throws is the primitive domain of exceptional results. Upon occurrence of an exception, Java allows objects to be passed to handlers as \reasons" for the exception. The primitive function throw : Obj ! Throws
turns an object into an exception throw(o) \with reason o." Note that elements of Throws are not right values.
Environments and stacks. Environments are pairs (I; ) where I is a subset of Identi er [ fthis g and is a partial function from I to right values. I = Identi er [ fthis g P Env = I I (I * RVal) The component I of an environment (I; ), called the source of , is meant to contain the local variables of a block and the formal parameters of a method body or of an exception handler. Environments are also used to store the information on which object's code is currently being executed: (this ). By abuse of notation, we write for an environment (I; ) and indicate with src () its source I . In particular, we understand that ; is an empty environment (I; ; ) such that ; (i) is unde ned for all i 2 I . As usual, [i 7! v](j ) = v if i = j and [i 7! v](j ) ' (j ) otherwise. Let Stack be the domain of stacks of environments, and let the metavariable range over this domain. The empty stack is written ; . The operation push : Env Stack ! Stack is the usual one on stacks. An instance variable declaration i = v binds v to i in the topmost environment of a stack ; we write [i = v] the result of this operation. The result of assigning v to i in the rst environment (I; ) of such that i 2 I is written [i 7! v]. The value associated with i in such an environment is denoted by (i). More precisely: (
[i = v] = push ([i
7!
v]; 0 ) if = push (; 0 ) and i src () 2
unde ned otherwise; [i 7! v]; 0 ) if = push (; 0 ) and i 2 src () [i 7! v] = >push (; 0 [i 7! v]) if = push (; 0 ) and i 2= src () : unde ned otherwise; 8 > if = push (; 0 ) and i 2 src () (i) if = push (; 0 ) and i 2= src () : unde ned otherwise. 8 >