Furtive access arises because of calls such as
other.set_
married (in non-recursive marriage, Section
4.3), where
other does not satisfy its invariant, but where we do not expect it to; in fact, the call is expressly intended as a step toward re-establishing that invariant. It is paradoxical and frustrating to be punished with an invariant violation just as we are working toward the invariant.
Although qualified, such calls are conceptually comparable to internal, unqualified calls
r (a), which can break the invariant of an object as part of a strategy to restore it. Legitimate cases of furtive access arise when two or more objects are involved in a similar strategy, trying to restore each other’s invariants through steps that may temporarily violate some of them. The Observer example (Sections
10.3 and
10.4) is typical, involving a subject and observer objects.
The Slicing Model, which will be described now, is a small update to the Callback model, addressing furtive access. The key observation for obtaining an annotation-free solution is that in practice legitimate cases of furtive access arise for calls to selectively exported features.
6.1 Selective Export Mechanisms
One of the object-oriented principles is information hiding (introduced by Parnas [
50] in the same year as the first article mentioning class invariants [
27]), which lets each class define which of its features and other properties it makes available (“exports”) to others (its clients). Many OO languages offer ways to fine-tune this decision through
selective export, whereby a class exports specific features to specific clients; examples are the “friends” mechanism of C++ and “internal” accessibility in C#. In Eiffel, one may declare a feature in a clause starting with
feature {A, B, &} to specify that it is only exported (or “visible” per the terminology defined below in Section
6.3) to classes
A,
B, … and their descendants.
Previous work seems to have missed the relationship of selective exports to the furtive access problem of class-invariant semantics, and its potential role in solving the problem without requiring additional programmer annotations.
6.2 Taking Advantage of Selected Exports
Observation of utility routines such as
set_
married and
set_
spouse and their counterparts in other furtive-access-prone code (such as the Observer pattern) reveals that in properly written code they are not publicly exported. Instead, they are selectively exported to the relevant class, in this case
PERSON itself. As a consequence, while a call
Alice.marry_
stepwise (Bob) (Section
4.3) may appear in any class (since all versions of
marry should be publicly exported),
other.set_
married is only permitted in class
PERSON (and descendants).
Using information hiding criteria in the proof rule combines design principles and correctness concerns. The reasoning is that a routine should not have to account for invariant clauses of visibility higher than its own. All three invariant clauses of PERSON involve public features, spouse and is_married, of higher availability than set_spouse and set_married; they should not have to preserve these clauses. The situation with examples such as Observer is similar.
6.3 Slicing: Definitions
As a reminder, an invariant is made of a sequence of clauses; the one for
PERSON (Section
4.1) has three clauses (here without tags):
spouse \(\ne\) Current,
is_
married =
(spouse \(\ne\) Void) and
is_
married \(\Rightarrow\) (spouse.spouse = Current). The invariant is understood as the sequential conjunction (“and then”) of these clauses.
Regardless of the language mechanism for selective exports, the
visibility V (r)of a feature r of a class
C is defined as the set of classes that may use
r as clients, in qualified calls
x.r (a) with
x of type
C. The example has assumed so far (Section
4) that the features of class
PERSON have full visibility (they are exported to all classes) except for two of them, exported only to class
PERSON itself:
V (set_
married) =
V (set_
spouse) = {
PERSON}. Visibility gives a preorder relation (reflexive and transitive) on the features of a class: we write
r \(\le\) g to express that
V (r) \(\subseteq\) V (g) (
g is exported to the same classes as
r, and possibly to some additional ones).
The queries of an invariant clause are the queries that appear in it either by themselves or as targets of calls. For example, the queries of
a and b.c
are a and b (but not c, which is the feature of a qualified call, not a feature of the invariant expression itself).
Visibility, defined above for individual features, also applies to invariant clauses. The visibility V (CL)of an invariant clause CL is the intersection \(\cap\) V (q) of the visibilities of all its queries q. The preorder relation also follows: CL1 \(\le\) CL2 means that \(\cap\) V (q1) \(\subseteq\) \(\cap\) V (q2) for queries q1 of CL1 and q2 of CL2; in other words, for any class C to which all queries of CL1 is exported, all queries of CL2 are also exported to C. The preorder relation can similarly hold between invariant clauses and features: CL \(\le\) r means that V (CL) \(\subseteq\) V (r), that is to say, r is exported to all classes to which all queries of CL are exported.
Finally, if INV is the invariant of a class, then its slicing by a feature r of that class, written INV / r, is INV restricted to its clauses CL such that CL \(\le\) r. In other words, INV / r is the part of INV that has no more visibility than r.
The clauses of the invariant of class
PERSON (repeated at the beginning of this subsection) all have
spouse or
is_
married among their features, and hence have full visibility, since these features are public (exported to all classes). Since
marry, in all its versions, is also public,
INV / marry is the full
INV. However,
INV / set_
spouse and
INV / set_
married are empty invariants, since the given features have lesser visibility (they are exported to
PERSON only). In the examples summarized in Section
10, sliced invariants are in-between these extremes.
The following Slicing Implication Theorem holds: If r \(\le\) g, then INV / g \(\Rightarrow\) INV / r. Proof: for any clause CL of INV / r, CL \(\le\) r by the definition of slicing. Hence CL \(\le\) g, implying—again by the definition of slicing, applied now to INV / g—that CL is one of the clauses of INV / g. So all the clauses of INV / r are also clauses of INV / g. As a consequence, remembering that an invariant denotes the conjunction of its clauses, INV / g implies INV / r.
A caveat about slicing is that some invariant clauses may need reinforcing to remain defined. For example, a good prover will accept an invariant clause c involving 1 / y if earlier clauses state x = y and x > 0; slicing x off removes these clauses and makes c potentially undefined, hence useless for verification. It is the programmer’s responsibility to make sure that all invariant clauses are meaningful, pre- and post-slicing (here, precede c’s condition by y \(\ne\) 0 \(\Rightarrow\) ).
6.5 Relative Consistency and the Slicing Model
Consider a non-current extended-active object, with invariant INV. It either started the execution of a qualified call x.r (a) or is one of its arguments a. It is partially consistent if it satisfies INV / r: the invariant sliced by the current routine. Obviously, consistent implies partially consistent. A non-current object is relatively consistent if it is either consistent or partially consistent.
GCP-2, the new variant of the Global Consistency Property (Figure
7), differs only slightly from GCP-1 (compare to Figure
6):
All non-current objects are relatively consistent. In particular, any (non-current) object in the call chain or attached to an argument is partially consistent (it satisfies
INV / r for its routine
r). All other non-current objects are fully consistent.
The Invariant Hypothesis (Section
2.2) similarly becomes the Relative Invariant Hypothesis, stating that
at the start of any call, the target satisfies its relative invariant (its invariant sliced by the feature of the call).
6.7 Two Programming Language Conditions
The soundness of the Sliced rule requires two conditions on programs, to be enforced by the programming language.
Export Consistency Condition: in any qualified call x.s (a) appearing in a routine r, s \(\le\) r.
In other words, a routine may only perform qualified calls to routines with the same visibility or less. In the example of Figure
8, visibility along the routine sequence
p, q, t, r can only remain the same or decrease. This reasonableness rule (do not rise above your own rank) can be compared to the practice of Web browsers such as Firefox, which let users start a new public or private window from a public window, but, from a private window, only a private one.
Under the Export Consistency Condition, the following
Safe Indirect Callback Theorem holds: In an indirect callback of routine
r, the routine
q of the target has the same visibility as
r. (See Figure
8.)
Proof: from the Export Consistency Condition,
r \(\le\) q and
q \(\le\) r. (From Section
6.3, “
\(\le\) ” on features is only a preorder, but on the associated visibility sets such as
V (r) the relation is antisymmetric, since it is simply “
\(\subseteq\) ”.)
The second language condition needed for the soundness of the Slicing Model is the
Selective Export Call Rule: in a call
x.r (a) where
r is selectively (rather than fully) exported, the target
x must be a formal argument of the enclosing routine. (In other words,
x may not be an attribute or a local variable.) This restriction does not affect the actual power of the programming language, because it is always possible to wrap a call in a routine. Informal examination of code using selective exports shows that most selective-export calls are indeed on arguments and that only a small amount of tweaking may be needed, as in two of the challenge-problem examples, marriage (Section
4.1) and circular list (Section
10.2), whose verification led to introducing an extra argument.
The soundness of the Slicing Model also assumes that a routine cannot change the values of reference variables passed to it as actual arguments in a call. In other words, such arguments are passed by value. Many modern OO languages (such as Eiffel and Java) satisfy this common-sense condition, but others may violate it, requiring adaptations of the model presented here.
6.8 Handling Furtive Access Correctly: The Slicing Rule Is Sound
As with earlier rules, the core of the soundness proof for Sliced is the proof of the Global Consistency Lemma, here stating the preservation of GCP-2 (all objects except the current one are relatively consistent) through events of all three kinds:
G1
Internal operation: The reasoning for
Callbacks (GCP-1) applies: Since internal operations only affect the current object, and we still assume SM3 (invariant locality), they preserve GCP-2. Note the importance of the rule that routines cannot change the value of their reference arguments (Section
6.7): without it, an assignment
f := Current to a formal argument
f, modifying the corresponding actual, could make an extended-active object inconsistent.
G2
Start-of-call: The following objects are involved:
–
(G2-A) The target of the call. It becomes current and hence does not affect GCP-2.
–
(G2-B) The client (the initiator of the call), previously current. It remains active and, thanks to the call’s precondition INV / r (entry BC \(_1\) ), satisfies the sliced invariant, as required of active objects under GCP-2.
–
(G2-C) Objects attached to arguments of the call (a in the caller, f in the target). They become extended-active and hence must satisfy the invariant sliced by the routine being called. That is exactly what the precondition AS \(_2\) and its counterpart BC \(_2\) enforce: a.INV / r.
G3
End-of-call: the following objects are involved:
–
(G3-A) The target. It ceases to be current. From entry AS
\(_1\) it satisfies
INV / r. If (outside of the case of a callback, in which it remains active) it could become inactive, then that would not be enough, since inactive objects must satisfy the
full invariant under GCP-2. The Selective Export Call Rule (Section
6.7) saves us: By requiring the target of a selective-export call to be a formal argument of the calling routine, it guarantees that the target remains extended-active (attached to an argument of an active routine), for which the sliced invariant suffices.
This property is key to the soundness of the Slicing Model, enabling “furtive access” calls to use the sliced invariant on both entry and return. The proofs of such challenge examples as marriage and Observer (Section
10.2) would be impossible (and the software incorrect) if furtive-access routines had to restore the full invariant.
–
(G3-B) The client. It becomes current again, hence incurs no obligation.
–
(G3-C) Objects attached to arguments of the call. During the call they were extended-active and only had to be partially consistent. On end-of-call they cease to be extended-active (except if active or attached to an argument higher up in the call chain) and must become fully consistent to preserve GCP-2. That is exactly what the postconditions AS2 and its counterpart AC \(_2\) guarantee: a.INV, using the unsliced INV.
Steps G2-A and G3-B involve an object that GCP-2 allows to be inconsistent, because it is current. It is in fact relatively consistent in both cases, yielding an updated form QCCT-2 of the Quasi-Constant Consistency Theorem: In the Slicing Model, in all states other than immediately after an internal operation, all objects are relatively consistent. Proof:
—
In start-of-call (G2-A), the new current object is the target, which may be: inactive, satisfying the full
INV; attached to an argument, hence relatively consistent by precondition BC
\(_2\) ; in the case of a direct callback, the previous current object, which is relatively consistent thanks the “self-wrapping-up” precondition BS
\(_1\) ; or (in the remaining case, indirect callback) an object up the call chain—active, not just extended-active—which is relatively consistent as a consequence of the Safe Indirect Callback Theorem (
6.7).
—
In end-of-call (G3-B), the new current object is the client, which becomes active and hence, by preservation of GCP-2, relatively consistent.
As a consequence of QCCT-2, the Slicing Model satisfies the Invariant Hypothesis and hence (see
3.3) is sound.