A VM perspective:
*invocation*
*dynamic receiver*
*resolution*
*NOT invoked*
*selection:*
*actual execution*
invokevirtual D::m(LDT)
D
D.m(LDT)
D.m(LDT)
invokevirtual D::m(LDT)
E
D.m(LDT)
E.m(LDT)
reverser: adapt LDT->Date
invoke local E.m(Date)
if return had changed, adapt return back
invokevirtual D::m(Date)
D
D.m(Date)
D.m(Date)
forwarder: adapt Date->LDT
invoke local m(LDT)
if return had changed, adapt
invokevirtual D.m(Date)
E
D.m(Date)
E.m(Date)
invokevirtual E.m(LDT)
E
E.m(LDT)
reverser)
E.m(LDT):
reverser: adapt LDT->Date
invoke local E.m(Date)
if return had changed, adapt return back
invokevirtual E.m(Date)
E
E.m(Date)
E.m(Date) // original - unchanged behavior
Let me try from the other direction, using the JVMS terminology rather
than appealing to as-if. Where I think we're saying slightly different
things is in the interpretation of the lines I colored in blue above
(hope the formatting came through.) You are talking about the E.m(Date)
that appears in E.class (good so far). But I'm talking about the
_members_ of E. And the E.m(Date) that appears in E.class should _not_
be considered a (new-to-E) member of E. Instead, that E.m(Date) gives
rise to a synthetic member E.m(LDT). I have colored two cases in red
because I think this is where our assumptions really parted ways; will
come back to this at the bottom.
Here's why I'm harping on this distinction; we mark methods as
"forwarders" and do something special when we see something override a
forwarder. Taking the same hierarchy:
// before
class D {
void m(Date) { }
}
class E extends D {
void m(Date) { }
}
// middle -- D migrates, but E not yet
class D {
void m(LDT) { }
@Forwarding( m(LDT) } void m(Date);
}
class E extends D {
void m(Date) { }
}
// after -- E finally gets the memo
class D {
void m(LDT) { }
@Forwarding( m(LDT) } void m(Date);
}
class E extends D {
void m(LDT) { }
}
Now, let's draw inheritance diagrams (these are not vtables, they are
member tables). I'll use your notation, where I think D.m(X) means "the
Code attribute declaredin D for m(X)".
Before
m(Date)
D
D.m(Date)
E
E.m(Date)
This part is easy; D has m(Date), and E overrides it.
Middle
m(LDT)
m(Date)
D
D.m(LDT)
forwarder -> m(LDT)
E
reverser adapted from E.m(Date)
inherits forwarder
Now, both D and E have both m(Date) and m(LDT). D has a real method for
m(LDT), and a forwarder for m(Date). E has an m(Date), which we see
overrides a forwarder. So we adapt it to be an m(LDT), but we consider
E to have inherited the forwarder from D. I'll come back to this in a
minute.
After m(LDT)
m(Date)
D
D.m(LDT)
forwarder -> m(LDT)
E
E.m(LDT)
inherits forwarder
In this nirvana, there is a forwarder still, but it doesn't affect E,
because E has already gotten the memo. It sits around purely in the
case that someone calls m(Date).
OK, so why am I saying that membership has to be tilted this way? Let's
go back to the middle case, and add
class F extends E {
void m(Date) { } // still didn't get the memo
}
Middle
m(LDT)
m(Date)
D
D.m(LDT)
forwarder -> m(LDT)
E
reverser adapted from E.m(Date)
inherits forwarder
F
reverser adapted from F.m(Date)
inherits forwarder
When we go to compute members, I want to see that _F.m(Date) overrides a
forwarder too_. If we merely put E.m(Date) in the (E, m(Date)) box,
then it looks like F is overriding an ordinary member, and no reverser
is generated. (Or, we have to keep walking up the chain to see if
E.m(Date) in turn overrides a forwarder -- yuck, plus, that makes
forwarder-overrides-forwarder even messier.
Now, back to your table. The above interpretation of what is going on
comes to the same answer for all of the rows of your table, except these:
*invocation*
*dynamic receiver*
*resolution*
*NOT invoked*
*selection:*
*actual execution*
invokevirtual D.m(Date)
E
D.m(Date)
E.m(Date)
invokevirtual E.m(Date)
E
E.m(Date)
E.m(Date) // original - unchanged behavior
You are thinking "E has a perfectly good m(Date), let's just select
that". Makes sense, but the cost of that is that it complicates
calculation of membership and overriding. I think I am content to let
invocations of m(Date) on receivers of type E go through both rounds of
adaptation: forward the call (with adaptation) to m(LDT), which, in the
case of E, does the reverse adaptations and ends up at the original Code
attribute of E.m(Date). This sounds ugly (and we'd need to justify some
potential failures) but leads us to a simpler interpretation of migration.
In your model, we basically have to split the box in two:
After m(LDT)
m(Date)
D
D.m(LDT)
forwarder -> m(LDT)
E
E.m(LDT)
E.m(Date), but also is viewed as a forwarder by subclasses
I think its a good goal, but I was trying to eliminate that complexity
by accepting the round-trip adaptation -- which goes away when E gets
the memo.