<< §2.2 Lowering | ↑ Table of Contents ↑ | §2.4 Explicit role creation >> |
§2.3 Lifting
Lifting is the reverse translation of lowering. However, lifting is a bit more demanding, since a given base object may have zero to many role objects bound to it. Therefor, the lifting translation requires more context information and may require to create role objects on demand.
(a) Definition of lifting
Retrieving a role for a given base object is called lifting. Lifting is guaranteed to yield the same role object for subsequent calls regarding the same base object, the same team instance and the same role class (see §2.3.4 for cases of ambiguity that are signaled by compiler warnings and possibly runtime exceptions).
(b) Places of lifting
The lifting translation is not meant to be invoked by client code, but translations are inserted by the compiler at the following locations:
- Callout bindings (§3.3.(c)) (result)
- Callin bindings (§4.5.(a)) (call target and parameters)
- Declared lifting (§2.3.2)
(c) Typing
A lifting translation statically expects a specific role class.
This expected role class must have a playedBy
clause
(either directly, or inherited (explicitly or implicitly)
from a super role), to which the given base type is conform.
(d) Lifting of arrays
Lifting also works for arrays of role objects. For lifting an array of base objects a new array is created and filled with role objects, one for each base object in the original array. In contrast to the role objects themselves, lifted arrays are never reused for subsequent lifting invocations.
The term translation polymorphism
describes the fact that at certain points values can be passed which are not
conform to the respective declared type considering only regular
inheritance (extends
). With translation polymorphism
it suffices that a value can be translated using lifting or lowering.
§2.3.1 Implicit role creation
↑ §2.3
Lifting tries to reuse existing role objects so that role state persists across lifting and lowering. If no suitable role instance is found during lifting, a new role is created.
(a) Reuse of existing role objects
A role object is considered suitable for reuse during lifting, if these three items are identical:
- the given base object
- the given team object
- the statically required role type
For the relation between the statically required role type and the actual type of the role object see "smart lifting" (§2.3.3).
(b) Default lifting constructor
Lifting uses a default constructor which takes exactly one argument of the type
of the declared base class (after playedBy
).
By default the compiler generates such a constructor for each bound role.
On the other hand, default constructors that take no arguments
(as in JLS §8.8.7) are never generated for bound roles.
The super-constructor to be invoked by a default lifting constructor
depends on whether the role's super class is a bound role or not.
- If the super-class is a bound role, the default lifting constructor will invoke the default lifting constructor of the super-class.
- If the super-class is not a bound role, the default lifting constructor will invoke the normal argumentless default constructor of the super-class.
(c) Custom lifting constructor
If a role class declares a custom constructor with the same signature
as the default lifting constructor, this constructor is used during lifting.
This custom constructor may pre-assume that the role has been setup
properly regarding its base-link and registered in the team's internal map of roles.
If a bound role has an unbound super-class without an argumentless
constructor, providing a custom lifting constructor is obligatory,
because no legal default lifting constructor can be generated.
(d) Fine-tuning role instantiation
If the lifting operation as defined above degrades the program performance, the lifting semantics can be modified per role
class
by adding the annotation @org.objectteams.Instantiation
which requires an argument of type
org.objectteams.InstantiationPolicy
in order to select between the following behaviors:
- ONDEMAND
- This is the default behavior as defined above.
- ALWAYS
- This strategy avoids maintaining the internal role cache, but instead a fresh role instance is created for each lifting request.
This may increase the number of role instances but cuts the costs of accessing the cache, which could otherwise become
expensive if a cache grows large. As a result of this strategy role state can no longer be shared
over time, thus it is discouraged to define fields in a role with this strategy. Also, comparing roles could lead
to
unexpected results. Therefor, roles with this strategy should implement custom
equals
andhashCode
methods, which should simply delegate to the base instance (using callout §3). - NEVER
- Roles with this instantiation policy are never instantiated by lifting.
Such roles cannot define non-static fields.
Otherwise this optimization is fully transparent, specifically callout bindings will refer to the correct base instance.
As of version 2.0 the OT/J compiler does not implement this strategy. - SINGLETON
- Roles declaring this strategy will be instantiated at most once per team. Subsequent lifting requests in the same team
will always answer the same role instance. Such roles may receive triggers from callin bindings, but cannot define
callout bindings.
As of version 2.0 the OT/J compiler does not implement this strategy.
§2.3.2 Declared lifting
↑ §2.3
(a) Parameters with declared lifting
A non-static team-level method or constructor may declare a parameter with two types in order to explicitly denote a place of lifting. Using the syntax
public void m (BaseClass as RoleClass param) { stmts }
a liftable parameter can be declared, provided the second type
(RoleClass
) is a role of (playedBy
) the first type (BaseClass
).
Furthermore, the role type must be a role of the enclosing team class defining the given method.
The role type must be given by its simple (i.e., unqualified) name.
Such a signature requires the caller to provide a base object (here BaseClass
), but
the callee receives a role object (here RoleClass
).
In fact, the client sees a signature in which the "as RoleClass
" part is omitted.
Compatibility between caller and callee sides is achieved by an implicitly inserted lifting translation.
A signature using declared lifting is only valid, if the requested lifting is possible
(see §2.3.3 and §2.3.4 for details).
(b) Super in the context of declared lifting
Calling super
or tsuper
in a method or constructor which
declares lifting for one or more parameters refers to a method or constructor with role type parameters,
i.e., lifting takes place before super invocation. Nevertheless, the super method may also
have a declared lifting signature. It will then see the same role instance(s) as the current method.
(c) Declared lifting of arrays
If a parameter involving explicit lifting should be of an array type, the syntax is
public void m (BaseClass as RoleClass param[]) ...
Here the brackets denoting the array apply to both types, BaseClass
and RoleClass
.
(d) Declared lifting for catch blocks
Also the argument of a catch block may apply declared lifting like in:
catch (BaseException as RoleClass param) { stmts }
This syntax is only valid in a non-static scope of a team (directly or nested).
In the given example, RoleClass
must be played by BaseException
.
Note, that RoleClass
itself need not be a throwable.
As the effect of this declaration the catch block will catch any exception of type BaseException
and provides it wrapped with a RoleClass
instance to the subsequent block.
Also note, that re-throwing the given instance param
has the semantics of implicitly lowering
the role to its base exception before throwing, because the role conforms to the required type
Throwable
only via lowering.
(e) Generic declared lifting
A method with declared lifting may introduce a type parameter that is bounded relative to a given role type. Such bound is declared as:
<AnyBase base SuperRole>
void teamMethod(AnyBase as SuperRole arg) {
// body using arg as of type SuperRole
}
This means that AnyBase
is a type parameter whose instantiations must all be liftable to role SuperRole
.
The given type bound requires the call site to supply an argument that is compatible to any base class
for which the current team contains a bound role that is a sub class of SuperRole
, including SuperRole
itself.
However, SuperRole
itself need not be bound to any base class.
On the other hand, different valid substitutions for AnyBase
need not be related by inheritance.
Note:
This feature supports generalized treatment of otherwise unrelated base classes. This is done by defining one bound role for each base under consideration and by having all these roles extend a common unbound role.Example code (Declared Lifting):
1 | team class Super { |
2 | public class MyRole playedBy MyBase { ... } |
3 | void m (MyRole o) { ... }; |
4 | } |
5 | team class Sub extends Super { |
6 | void m (MyBase as MyRole o) { |
7 | // inside this method o is of type MyRole |
8 | super.m(o); |
9 | } |
10 | } |
11 | Sub s_team = new Sub(); |
12 | MyBase b = new MyBase(); |
13 | s_team.m(b); // clients see a parameter "MyBase o" |
§2.3.3 Smart lifting
↑ §2.3
In situations where role and base classes are part of some inheritance
hierarchies (extends
), choosing the appropriate role class during
lifting involves the following rules:
(a) Static adjustment
If a base class B
shall be lifted to a role class
R
that is not bound to (playedBy
)
B
, but if a subclass of R
— say R2
—
is bound to B
, lifting is statically setup to use
R2
, the most general subclass of R
that
is bound to B
or one of its super-types.
Restriction:
This step is not applicable for parameter mappings ofreplace
callin bindings (§4.5.(d)).
(b) Dynamic selection of a role class
At runtime also the dynamic type of a base object is considered:
Lifting always tries to use a role class that is bound to the
exact class of the base object. Lifting considers all role–base
pairs bound by playedBy
such that the role class is a
sub-class of the required (statically declared) role type
and the base class is a super-class of the
dynamic type of the base object.
From those possible pairs the most specific base class is chosen.
If multiple role classes are bound to this base class the most
specific of these classes is chosen.
(c) Team as closed world
In the above analysis gathering all role-base pairs is performed at
compile-time. From this follows, that a team class can only be
compiled when all its contained role classes are known and a role class
can never be compiled without its team.
The analysis includes all roles and their bindings that are inherited
from the super-team.
(d) Selection regardless of abstractness
Smart lifting is not affected by abstractness of role classes. For the effect of abstract role classes see §2.5.
Complex Example:
role class | base class |
---|---|
class R1 | |
class R2 extends R1 playedBy B2 | class B2 |
class R3 extends R2 /* inherited: playedBy B2 */ | class B3 extends B2 |
class R4 extends R3 playedBy B4 | class B4 extends B3 |
class R5 extends R4 /* inherited: playedBy B4 */ | |
class B6 extends B4 | |
class R7 extends R5 playedBy B7 | class B7 extends B6 |
- If declarations require lifting
B3
toR1
this is statically refined to useR2
instead, because this is the most general class declaring a binding to a super–class ofB3
. - If the dynamic base type in the same situation is
B6
, three steps select the appropriate role:- By searching all
playedBy
clauses (including those that are inherited) the following role–base pairs are candidates:(R2,B2), (R3,B2), (R4,B4)
and(R5,B4)
. - From these pairs the two containing the most specific base class
B4
are chosen. - This makes
R4
andR5
role candidates, from which the most specificR5
is finally chosen.
- By searching all
If the inheritance hierarchies of the involved base and role classes are given (like in the figure above)
the smart lifting algorithm can be rephrased to the following "graphical" rule:
B6
in the example) move upwards the the inheritance
relation until you reach a base class bound to a role class indicated by a «playedBy»
arrow pointing to the base class (B4
). This role class must be conform to the requested role type.
Switch to the role side along this arrow (R4
). Now move downwards the role inheritance hierarchy
as long as the subrole does not refine the playedBy relationship (indicated by another «playedBy» arrow).
The bottom role you reach this way (R5
) is the role type selected by smart lifting.
§2.3.4 Binding ambiguities
↑ §2.3
While all examples so far have only shown 1-to-1 class bindings, several cases of multiple bindings are allowable. Ambiguities may be detected at compile time and/or at runtime.
(a) Potential ambiguity
A potential ambiguity is given,
if two role classes R1
and R2
exist such that
R1
andR2
are played by the same base classB
, andR1
andR2
have a common super roleR0
, which is also bound to a base classB0
, and- neither role class
R1
norR2
is a (indirect) sub-class of the other.
Effect:
In this case the compiler issues a warning, stating that theB
may not be liftable, because both role classes R1
and R2
are candidates and there is no reason to prefer one over the other.
If no potential ambiguity is detected, lifting will always be unambiguous.
In the above situation, trying to lift an instance of type B
to the role type
R0
is an illegal lifting request. If R0
is bound
to the same base class B
as its sub-roles R1
and R2
are,
role R0
is unliftable, meaning that no instance of R0
can ever by obtained by lifting.
Example code (Potential Ambiguity):
1 | team class MyTeam { |
2 | public class SuperRole playedBy MyBase {...} |
3 | public class SubRoleA extends SuperRole {...} |
4 | public class SubRoleB extends SuperRole {...} |
5 | } |
(b) Definite ambiguity
A definite ambiguity is given if
- the situation of potential ambiguity according to (a) above is given and
- lifting is requested (either by method binding or explicitly
(§2.3.2)) from the shared base class
B
to any role classR0
that is a common super role forR1
andR2
.
Definite binding ambiguity also occurs in cases of generic declared lifting §2.3.2.(e)
if the specified role R
is unbound and if two independent sub-roles R1
and R2
exist that introduce a playedBy binding to the same base class BX
.
In this case no potential ambiguity is flagged because roles R1
and R2
have no shared bound super-role.
Effect:
Code causing definite ambiguity is required to handleorg.objectteams.LiftingFailedException
.
In cases of definite binding ambiguity lifting will indeed fail except for some corner cases. Such corner cases may arise if lifting already finds an appropriate role in the cache or if an (indirect) subrole of the ambiguously bound role is an unambiguous lift target for the concrete type of the base object at run-time. See also §2.3.5.
Example code (Definite Ambiguity):
1 | team class MyTeam { |
2 | public class SuperRole playedBy MyBase {...} |
3 | public class SubRoleA extends SuperRole playedBy SubBase {...} |
4 | public class SubRoleB extends SuperRole playedBy SubBase {...} |
5 | |
6 | public void useSuperRole(SubBase as SuperRole r) {...} // must declare LiftingFailedException |
7 | } |
(c) Actual ambiguity
At runtime actual ambiguity may occur if for the dynamic type of a base to be lifted the conditions of (b) above hold accordingly. Actual ambiguity is only possible in cases reported by the compiler as potential or definite ambiguity.
Effect:
An actual ambiguity is reported at runtime by throwing aorg.objectteams.LiftingFailedException
.
Example code (Actual Ambiguity):
1 | import org.objectteams.LiftingFailedException; |
2 | team class MyTeam { |
3 | public class SuperRole playedBy MyBase {...} |
4 | public class SubRoleA extends SuperRole playedBy SubBase {...} |
5 | public class SubRoleB extends SuperRole playedBy SubBase {...} |
6 | |
7 | public void useSuperRole(MyBase as SuperRole r) throws LiftingFailedException {...} |
8 | } |
9 | // plus these calls: |
10 | MyTeam mt = new MyTeam(); |
11 | mt.useSuperRole(new SubBase()); // will throw a LiftingFailedException |
(d) Mismatching role
In cases of potential ambiguity another runtime error may occur: a mismatching role is encountered when a role is found in the cache, which is not conform to the required type. This happens, if the base object has previously been lifted to a type that is incompatible with the currently requested type.
Effect:
This is reported by throwing aorg.objectteams.WrongRoleException
.
Example code (Mismatching Role):
1 | import org.objectteams.LiftingFailedException; |
2 | team class MyTeam { |
3 | public class SuperRole playedBy MyBase {...} |
4 | public class SubRoleA extends SuperRole {...} |
5 | public class SubRoleB extends SuperRole {...} |
6 | |
7 | public void useRoleA(MyBase as SubRoleA r) throws LiftingFailedException {...} |
8 | public void useRoleB(MyBase as SubRoleB r) throws LiftingFailedException {...} |
9 | } |
10 | // plus these calls: |
11 | MyTeam mt = new MyTeam(); |
12 | MyBase b = new MyBase(); |
13 | mt.useRoleA(b); // creates a SubRoleA for b |
14 | mt.useRoleB(b); // finds the SubRoleA which is not compatible |
15 | // to the expected type SubRoleB. |
From the second item of §2.3.4.(a) follows, that for binding ambiguities different
role hierarchies are analyzed in isolation.
For this analysis only those role classes are considered that are bound to a
base class (directly using playedBy
or by inheriting this relation
from another role class).
I.e., two role classes that have no common bound super role will never cause
any ambiguity.
§2.3.5 Consequences of lifting problems
↑ §2.3
The rules for lifting and role binding allow (after issuing a warning) two problematic situations:
- A potential binding ambiguity makes selection of the approprate role type impossible (§2.3.4.(a))
- A role which might be relevant for lifting is abstract (§2.5.(b))
Whenever lifting fails for one of these reasons an org.objectteams.LiftingFailedException
(§6.2.(d))
is thrown.
Given that this is a checked exception and depending on the location requiring lifting this has the following consequences:
(a) Problematic declared lifting
A method with declared lifting (§2.3.2) may have to declare org.objectteams.LiftingFailedException
.
(b) Problematic callout binding
The role method of a callout binding with result lifting (§3.3.(c)) may have to declare org.objectteams.LiftingFailedException
.
(c) Problematic callin binding
A callin binding (§4) may silently fail due to a org.objectteams.LiftingFailedException
.
This exception will actually remain hidden because the callin binding is not explicitly invoked from any source code
but implicitly
by the runtime dispatch mechanism. To signal this situation the compiler raises an error against such callin binding.
However, the compiler should allow to configure this error and understand the warning token "hidden-lifting-problem"
for suppressing this problem (§4.1.(b)).
If the problem is ignored/suppressed and if at runtime the lifting problem occurs,
triggering of the callin binding will silently fail, i.e., the program will continue in this situation as if the binding
hadn't existed in the first place.
(d) Incompatible redefinition of a role hierarchy
Consider a team T1
with a method m
with declared lifting regarding role R
,
where no lifting problems are detected.
Consider next a sub-team T2
which modifies the hierarchy of role R
such that lifting
to T2.R
is problematic due to a binding ambiguity.
In this case clients invoking T1.m()
could face the situation at runtime that an instance
of T2
is used that unexpectedly fails to lift to its role R
.
Here, the compiler signals a specific error against T2
alerting of the incompatible change.
<< §2.2 Lowering | ↑ Table of Contents ↑ | §2.4 Explicit role creation >> |
Effects:
m
with a base instance (typeMyBase
) as its argument (line 13).m
, the argument is lifted such that the method body receives the argument as of typeMyRole
(line 8).