that g
pops and jmp
s to f
s body. In code:
START_F:
...
L0 push addr(L0); location of instr to be run after call.
jmp START_G
L1:
=========
g's body
START_G:
...
retloc = pop ; pop location to jump to
RETG: jmp retloc
Rather than push
ing and pop
ing, we can rewrite the code of g
, to
change retloc
before a call to g
. In made-up-pseudocode, here's what that
would look like:
§ The jump based solution
* f's body
START_F:
...
L0 store loc(RETG) assemble(jmp addr(L1))
jmp START_G
L1:
=========
* g's body
START_G:
...
RETG: <###-to-be-filled-dummy-###>
instead of having a call stack, before f
calls g, f
modify g
's code at location RETG
into a jmp
instruction by store
ing the instruction jmp addr(L1)
.
This effectively creates a 'custom' g
that knows how to return
control flow into f
.
* g's body (after execution of L0)
START_G:
...
RETG: jump addr(L1)
This way, we have obviated the need for a push/pop
sequence, by directly
modifying g
's code. This is really neat --- we replace the overhead of
a push/pop
with a single store
.
§ Why recursion breaks.
We don't actually need a call stack, as long as we don't want to write recursive functions.
We can't have recursion, or more generally "re-entrance": consider a call chain of the form:
-
A -> B -> C -> B
. - during
A -> B
, A
will scribble a
into B
. - during
B -> C
, B
will scribble a
into C
. - during
C -> B
, C
will scribble
into B
, destroying the previous
. - This creates a cycle, where
C
will attempt to return to B
and vice versa.