PEP thought experiment: Unix style exec for function/method calls

M

Michael

Hi,


[ I'm calling this PEP thought experiment because I'm discussing language
ideas for python which if implemented would probably be quite powerful
and useful, but the increased risk of obfuscation when the ideas are
used outside my expected/desired problem domain probably massively
outweigh the benefits. (if you're wondering why, it's akin to adding
a structured goto with context)

However I think as a thought experiment it's quite useful, since any
language feature can be implemented in different ways, and I'm wondering
if anyone's tried this, or if it's come up before (I can't find either
if they have...). ]

I'm having difficulty finding any previous discussion on this -- I
keep finding people either having problems calling os.exec(lepev), or
with using python's exec statement. Neither of which I mean here.

Just for a moment, let's just take one definition for one of the
os.exec* commands:

execv(...)
execv(path, args)

Execute an executable path with arguments, replacing current
process.
path: path of executable file
args: tuple or list of strings

Also: Note that execv inherits the system environment.

Suppose we could do the same for a python function - suppose we could
call the python function but either /without/ creating a new stack
frame or /replacing/ the current stack frame with the new one.

Anyway, I've been thinking recently that the same capability in python
would be useful. However, almost any possible language feature:
* Has probably already been discussed to death in the past
* There's often a nice idiom working around the lack of said feature.

So I'm more on an exploratory forage than asking for a language change
here ;)

Since os.exec* exists and "exec" already exists in python, I need to
differentiate what I mean by a unix style exec from python. So for
convenience I'll call it "cexe".

Now, suppose I have:
----------
def set_name():
name = raw_input("Enter your name! > ")
cexe greet()

def greet():
print "hello", name

cexe set_name()
print "We don't reach here"
----------

This would execute, ask for the user's name, say hello to them and then
exit - not reaching the final print "We don't reach here" statement.

Let's ignore for the moment that this example sucks (and is a good example
of the danger of this as a language feature), what I want to do here is
use this to explain the meaning of "cexe".

There's two cases to consider:
cexe some_func_noargs()

This transfers execution to the function that would normally be called
if I simply called without using "cexe" some_func_noargs() . However,
unlike a function call, we're /replacing/ the current thread of
execution with the thread of execution in some_func_noargs(), rather
than stacking the current location, in order to come back to later.

ie, in the above this could also be viewed as "call without creating a
new return point" or "call without bothering to create a new stack
frame".

It's this last point why in the above example "name" leaks between the
two function calls - due to it being used as a cexe call.

Case 2:
given...
def some_func_withargs(colour,tone, *listopts, **dictopts)

consider...
cexe some_func_withargs(foo,bar, *argv, **argd)

This would be much the same as the previous case, except in the new
execution point, the name colour & tone map to the values foo & bar had
in the original context, whilst listopts and dictopts map the values
that argv & argd had in the original content)

One consequence here though is that in actual practice the final print
statement of the code above never actually gets executed. (Much like if
that was inside a function, writing something after "return foo", wouldn't
be executed)

The reason I'm curious here about previous discussion is because
conceptually there's obviously other semantics you can apply - such as
the current stack frame is /replaced/ by the new stack frame. This is
perhaps a more accurate mapping to the Unix exec call.

If that was the case, it would mean that locals would not "leak" between
functions (which is desirable), and our example above could be rewritten
as follows:

----------
def get_and_use_value_from_user(tag, callforward):
somevalue = raw_input(tag)
cexe callforward(name)

def greet(name):
print "hello", name

cexe get_and_use_value_from_user("Enter your name! > ", greet)
print "We don't reach here"
----------

OK, so this probably seems pretty pointless to many people, but I'm
curious about improving the tools to deal with state machines. Often
people use switch statements in other languages to deal with them, and
for certain classes of state machines you can use replace them with
generators. But that's not appropriate for everything...

My particular thought that started all this off actually stems from this:

Essentially by doing a cexe we're actually creating a composite function
out of disparate functions (perhaps shared or not shared local context).
ie ...
----------
def count():
print "Counting to 3!"
cexe one()

def one():
print "one!"
cexe two()

def two():
print "two!"
cexe three()

def three():
print "three!"

count() # Note I'm not doing cexe count() here
----------
.... essentially dynamically constructs an execution context similar to a
single function, ie the above collapses to something like:

----------
def count():
print "Counting to 3!"
print "one!"
print "two!"
print "three!"

count() # Note I'm not doing cexe count() here
----------
It's this recognition that made me wonder this:

This works well for state machines, and generators are a nice model for
dealing with resumable things (and a state machine can be viewed as a
resumable "thing").

Now suppose we take all that one stage further and provide said
composite generator, with some additional context in the way we do
with Kamaelia - cf http://kamaelia.sf.net/MiniAxon/ , we could
potentially do this:

(choosing something relatively substantial to show I'm not just
being whimsical, and to provide somthing perhaps more "real")

class TCP_StateMachine(Axon.Component.component):
def CLOSED(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "appl passive open" == event.type: cexe self.LISTEN()
if "active open" == event.type:
self.send(SYN(event.payload), "network")
cexe self.SYN_SENT()

def LISTEN(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv syn" == event.type:
self.send( , "network")
cexe self.SYN_RCVD()
if "appl send data" == event.type:
self.send( , "network")
cexe self.SYN_SENT()

def SYN_RCVD(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv rst" == event.type: cexe self.LISTEN()
if "recv ack" == event.type: cexe self.ESTABLISHED()
if "appl close" == event.type:
self.send(FIN(event.payload), "network")
cexe self.FIN_WAIT1()

def SYN_SENT(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "appl close" == event.type: cexe self.CLOSED()
if "timeout" == event.type: cexe self.CLOSED()
if "recv syn-ack" == event.type:
self.send(ACK(event.payload), "network")
cexe self.ESTABLISHED()

def ESTABLISHED(self):
# more complex than others, so skipped, has its own data transfer
# state etc, so would make more sense to model as a subcomponent.

def FIN_WAIT_1(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv ack" == event.type: cexe self.FIN_WAIT_2()

if "recv fin" == event.type:
self.send(ACK(event.payload), "network")
cexe self.CLOSING()

if "recv fin, ack" == event.type:
self.send(ACK(event.payload), "network")
cexe self.TIME_WAIT()

def FIN_WAIT_2(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv fin" == event.type:
self.send(ACK(event.payload), "network")
cexe self.TIME_WAIT()

def CLOSING(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv ack" == event.type: cexe self.TIME_WAIT()

def TIME_WAIT(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "timeout 2MSL" == event.type: cexe self.CLOSED()

Now obviously that's not particularly pretty, but the clear definition
of states as methods, and clear transitions between states via the cexe
calls, is relatively easy to follow through. ie it's fairly clear it's
implementing the standard TCP state machine.

(Incidentally if you're wondering what relevance this has outside of
just TCP, this sort of thing could be useful in games for modelling
complex behaviours)

What is less clear about this is that I'm working on the assumption that
as well as the language change making "cexe" work, is that this also
allows the above set of methods to be treated as if it's one large
generator that's split over multiple function definitions. This is
conceptually very similar to the idea that cexe would effectively
"join" functions together, as alluded to above.

This has a number of downsides for the main part of the language, so
I wouldn't suggest that these changes actually happen - consider it a
thought experiment if you like. (I think the single function/no wrapping
of yield IS actually a good thing)

However, I feel the above example is quite a compelling example of how
a unix style exec for python method calls could be useful, especially
when combined with generators. (note this is a thought experiment ;)

It also struck me that any sufficiently interesting idea is likely to
have already been implemented, though perhaps not looking quite like the
above, so I thought I'd ask the questions:

* Has anyone tried this sort of thing?

* Has anyone tried simply not creating a new stack frame when doing
a function call in python? (or perhaps replacing the current one with
a new one)

* Has anyone else tried modelling the unix system exec function in
python? If so what did you find?

* Since I can't find anything in the archives, I'm presuming my
searching abilities are bust today - can anyone suggest any better
search terms or threads to look at?

* Am I mad? :)

BTW, I'm aware that this has similarities to call with continuation,
and that you can use statesaver.c & generators to achieve something
vaguely similar to continuations, but I'm more after this specific
approach, rather than that general approach. (After all, even ruby
notes that their most common use for call/cc is to obfuscate code -
often accidentally - and I'm not particularly interested in that :)

Whereas the unix style exec is well understood by many people, and
when it's appropriate can be extremely useful. My suspicion is that
my ideasabove actually maps to a common idiom, but I'm curious to
find that commonidiom.

I'm fairly certain something like this could be implemented using
greenlets, and also fairly certain that Stackless has been down this
route in the past, but I'm not able to find something like this exec
style call there. (Which is after all more constrained than your usual
call with continuation approach)

So, sorry for the length of this, but if anyone has any thoughts, I'd be
very interested. If they don't, I hope it was interesting :)

Regards,


Michael.
 
C

Carl Banks

Michael said:
Suppose we could do the same for a python function - suppose we could
call the python function but either /without/ creating a new stack
frame or /replacing/ the current stack frame with the new one.

I'm confused about what you mean. I'm guessing by "not creating a new
stack frame" you wish to execute the bytecode from a different function
in the current stack frame, correct? As for replacing the current
stack frame, do you want to replace the whole stack, down to the
__main__ module entry point, or just replace the top frame? Your
comments and examples seem to be inconsistent about this. I'm going to
assume you just want to replace or reuse the top frame in my replies.


[snip]
Since os.exec* exists and "exec" already exists in python, I need to
differentiate what I mean by a unix style exec from python. So for
convenience I'll call it "cexe".

Now, suppose I have:
----------
def set_name():
name = raw_input("Enter your name! > ")
cexe greet()

def greet():
print "hello", name

cexe set_name()
print "We don't reach here"

Only if you were to replace the whole stack. If you only replace or
reuse the top frame, I would think greet would exit and execution would
resume right after the point from which set_name was called. Or am I
misunderstanding what you want?

Let's ignore for the moment that this example sucks (and is a good example
of the danger of this as a language feature), what I want to do here is
use this to explain the meaning of "cexe".

There's two cases to consider:
cexe some_func_noargs()

This transfers execution to the function that would normally be called
if I simply called without using "cexe" some_func_noargs() . However,
unlike a function call, we're /replacing/ the current thread of
execution with the thread of execution in some_func_noargs(), rather
than stacking the current location, in order to come back to later.

ie, in the above this could also be viewed as "call without creating a
new return point" or "call without bothering to create a new stack
frame".

It's this last point why in the above example "name" leaks between the
two function calls - due to it being used as a cexe call.

I doubt this would be possible without a major change in how functions
work in Python. The most obvious problem is that functions refer to
local variables by an index, not by name. If you were to execute the
bytecode of one function in the stack frame of another, Bad Things
would happen.

Case 2:
given...
def some_func_withargs(colour,tone, *listopts, **dictopts)

consider...
cexe some_func_withargs(foo,bar, *argv, **argd)

This would be much the same as the previous case, except in the new
execution point, the name colour & tone map to the values foo & bar had
in the original context, whilst listopts and dictopts map the values
that argv & argd had in the original content)

This is a lot more doable, but I'm not sure a new statement is needed.
It seems like it wouldn't be terribly impossible to optimize the return
statement to do this (in fact there's a certain situation where it's
common to do just that, in other languages; more on that later). If
function A were to return the result of calling function B, the A could
possibly pop itself off the stack before calling B (but after
evaluating B's arguments). B would then return directly to A's caller
in place of A. Essentially, B has replaced A on the stack. Biggest
problem is you'd have to know whether an object is a Python function at
compile time--though that may not be so hard in the future.

(Now, if you're thinking that you'll replace the whole stack and not
just the top frame, Python already has the machinery to do this if you
can tolerate wrapping up your main function to catch an exception and
dispatching on it. For example:

class _Cexe(Exception):
def __init__(self,func,*args,**kwargs):
self.func = func
self.args = args
self.kwargs = kwargs

def cexe(func,*args,**kwargs):
raise _Cexe(func,*args,**kwargs)

def main_wrapper(*args,**kwargs):
func = main
while True:
try:
func(*args,**kwargs)
return
except _Cexe,e:
func = e.func
args = e.args
kwargs = e.kwargs

As an added bonus, you can do this higher up the stack if you want. A
cexe statement wouldn't improve this too much; it wouldn't even make it
more efficent. You can't just reset the stack pointer; you have to
clean things up. The only upside to cexe I can think of is it could
help reduce latency by delaying cleanup until garbage collection, but
it wouldn't help overall efficiency too much.)


[snip a lot]
It also struck me that any sufficiently interesting idea is likely to
have already been implemented, though perhaps not looking quite like the
above, so I thought I'd ask the questions:

* Has anyone tried this sort of thing?

Yes. There is a major use case for what you seem to be proposing,
namely tail-recursive functions. A tail-recursive function is a
recursive function that immediately returns the result of calling
itself. A tail-recursive function never needs to keep its state around
once it's called itself, and it's very common in functional languages,
so it's an easy and common optimization.

However, what you seem to be proposing is a generalization of that. It
seems like it's possible, theoretically, to generalize this so that it
works when you return the result of any function call, recursive or
not. Theoretically. It wouldn't surprise me if functional languages
like Haskell do this already.
* Has anyone tried simply not creating a new stack frame when doing
a function call in python? (or perhaps replacing the current one with
a new one)

Doubt it.
* Has anyone else tried modelling the unix system exec function in
python? If so what did you find?

* Since I can't find anything in the archives, I'm presuming my
searching abilities are bust today - can anyone suggest any better
search terms or threads to look at?

Maybe look to see how tail-recursive optimization in languages such as
Scheme work, and whether it can be generalized.
* Am I mad? :)

Yep. :)


Carl Banks
 
J

Jeethu Rao

This reminds me of an silly little optimization I used to use all the
times when coding in assembler on PIC MCUs.
A call followed by a return can be turned into jump. Saves one
instruction and one level on the call stack.
I think most optimizing compilers already do something of this sort, at
least in the embedded world :)

Jeethu Rao
Hi,


[ I'm calling this PEP thought experiment because I'm discussing language
ideas for python which if implemented would probably be quite powerful
and useful, but the increased risk of obfuscation when the ideas are
used outside my expected/desired problem domain probably massively
outweigh the benefits. (if you're wondering why, it's akin to adding
a structured goto with context)

However I think as a thought experiment it's quite useful, since any
language feature can be implemented in different ways, and I'm wondering
if anyone's tried this, or if it's come up before (I can't find either
if they have...). ]

I'm having difficulty finding any previous discussion on this -- I
keep finding people either having problems calling os.exec(lepev), or
with using python's exec statement. Neither of which I mean here.

Just for a moment, let's just take one definition for one of the
os.exec* commands:

execv(...)
execv(path, args)

Execute an executable path with arguments, replacing current
process.
path: path of executable file
args: tuple or list of strings

Also: Note that execv inherits the system environment.

Suppose we could do the same for a python function - suppose we could
call the python function but either /without/ creating a new stack
frame or /replacing/ the current stack frame with the new one.

Anyway, I've been thinking recently that the same capability in python
would be useful. However, almost any possible language feature:
* Has probably already been discussed to death in the past
* There's often a nice idiom working around the lack of said feature.

So I'm more on an exploratory forage than asking for a language change
here ;)

Since os.exec* exists and "exec" already exists in python, I need to
differentiate what I mean by a unix style exec from python. So for
convenience I'll call it "cexe".

Now, suppose I have:
----------
def set_name():
name = raw_input("Enter your name! > ")
cexe greet()

def greet():
print "hello", name

cexe set_name()
print "We don't reach here"
----------

This would execute, ask for the user's name, say hello to them and then
exit - not reaching the final print "We don't reach here" statement.

Let's ignore for the moment that this example sucks (and is a good example
of the danger of this as a language feature), what I want to do here is
use this to explain the meaning of "cexe".

There's two cases to consider:
cexe some_func_noargs()

This transfers execution to the function that would normally be called
if I simply called without using "cexe" some_func_noargs() . However,
unlike a function call, we're /replacing/ the current thread of
execution with the thread of execution in some_func_noargs(), rather
than stacking the current location, in order to come back to later.

ie, in the above this could also be viewed as "call without creating a
new return point" or "call without bothering to create a new stack
frame".

It's this last point why in the above example "name" leaks between the
two function calls - due to it being used as a cexe call.

Case 2:
given...
def some_func_withargs(colour,tone, *listopts, **dictopts)

consider...
cexe some_func_withargs(foo,bar, *argv, **argd)

This would be much the same as the previous case, except in the new
execution point, the name colour & tone map to the values foo & bar had
in the original context, whilst listopts and dictopts map the values
that argv & argd had in the original content)

One consequence here though is that in actual practice the final print
statement of the code above never actually gets executed. (Much like if
that was inside a function, writing something after "return foo", wouldn't
be executed)

The reason I'm curious here about previous discussion is because
conceptually there's obviously other semantics you can apply - such as
the current stack frame is /replaced/ by the new stack frame. This is
perhaps a more accurate mapping to the Unix exec call.

If that was the case, it would mean that locals would not "leak" between
functions (which is desirable), and our example above could be rewritten
as follows:

----------
def get_and_use_value_from_user(tag, callforward):
somevalue = raw_input(tag)
cexe callforward(name)

def greet(name):
print "hello", name

cexe get_and_use_value_from_user("Enter your name! > ", greet)
print "We don't reach here"
----------

OK, so this probably seems pretty pointless to many people, but I'm
curious about improving the tools to deal with state machines. Often
people use switch statements in other languages to deal with them, and
for certain classes of state machines you can use replace them with
generators. But that's not appropriate for everything...

My particular thought that started all this off actually stems from this:

Essentially by doing a cexe we're actually creating a composite function
out of disparate functions (perhaps shared or not shared local context).
ie ...
----------
def count():
print "Counting to 3!"
cexe one()

def one():
print "one!"
cexe two()

def two():
print "two!"
cexe three()

def three():
print "three!"

count() # Note I'm not doing cexe count() here
----------
... essentially dynamically constructs an execution context similar to a
single function, ie the above collapses to something like:

----------
def count():
print "Counting to 3!"
print "one!"
print "two!"
print "three!"

count() # Note I'm not doing cexe count() here
----------
It's this recognition that made me wonder this:

This works well for state machines, and generators are a nice model for
dealing with resumable things (and a state machine can be viewed as a
resumable "thing").

Now suppose we take all that one stage further and provide said
composite generator, with some additional context in the way we do
with Kamaelia - cf http://kamaelia.sf.net/MiniAxon/ , we could
potentially do this:

(choosing something relatively substantial to show I'm not just
being whimsical, and to provide somthing perhaps more "real")

class TCP_StateMachine(Axon.Component.component):
def CLOSED(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "appl passive open" == event.type: cexe self.LISTEN()
if "active open" == event.type:
self.send(SYN(event.payload), "network")
cexe self.SYN_SENT()

def LISTEN(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv syn" == event.type:
self.send( , "network")
cexe self.SYN_RCVD()
if "appl send data" == event.type:
self.send( , "network")
cexe self.SYN_SENT()

def SYN_RCVD(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv rst" == event.type: cexe self.LISTEN()
if "recv ack" == event.type: cexe self.ESTABLISHED()
if "appl close" == event.type:
self.send(FIN(event.payload), "network")
cexe self.FIN_WAIT1()

def SYN_SENT(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "appl close" == event.type: cexe self.CLOSED()
if "timeout" == event.type: cexe self.CLOSED()
if "recv syn-ack" == event.type:
self.send(ACK(event.payload), "network")
cexe self.ESTABLISHED()

def ESTABLISHED(self):
# more complex than others, so skipped, has its own data transfer
# state etc, so would make more sense to model as a subcomponent.

def FIN_WAIT_1(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv ack" == event.type: cexe self.FIN_WAIT_2()

if "recv fin" == event.type:
self.send(ACK(event.payload), "network")
cexe self.CLOSING()

if "recv fin, ack" == event.type:
self.send(ACK(event.payload), "network")
cexe self.TIME_WAIT()

def FIN_WAIT_2(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv fin" == event.type:
self.send(ACK(event.payload), "network")
cexe self.TIME_WAIT()

def CLOSING(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "recv ack" == event.type: cexe self.TIME_WAIT()

def TIME_WAIT(self):
if not self.anyReady(): yield self.pause()
event = self.recv("inbox")
if "timeout 2MSL" == event.type: cexe self.CLOSED()

Now obviously that's not particularly pretty, but the clear definition
of states as methods, and clear transitions between states via the cexe
calls, is relatively easy to follow through. ie it's fairly clear it's
implementing the standard TCP state machine.

(Incidentally if you're wondering what relevance this has outside of
just TCP, this sort of thing could be useful in games for modelling
complex behaviours)

What is less clear about this is that I'm working on the assumption that
as well as the language change making "cexe" work, is that this also
allows the above set of methods to be treated as if it's one large
generator that's split over multiple function definitions. This is
conceptually very similar to the idea that cexe would effectively
"join" functions together, as alluded to above.

This has a number of downsides for the main part of the language, so
I wouldn't suggest that these changes actually happen - consider it a
thought experiment if you like. (I think the single function/no wrapping
of yield IS actually a good thing)

However, I feel the above example is quite a compelling example of how
a unix style exec for python method calls could be useful, especially
when combined with generators. (note this is a thought experiment ;)

It also struck me that any sufficiently interesting idea is likely to
have already been implemented, though perhaps not looking quite like the
above, so I thought I'd ask the questions:

* Has anyone tried this sort of thing?

* Has anyone tried simply not creating a new stack frame when doing
a function call in python? (or perhaps replacing the current one with
a new one)

* Has anyone else tried modelling the unix system exec function in
python? If so what did you find?

* Since I can't find anything in the archives, I'm presuming my
searching abilities are bust today - can anyone suggest any better
search terms or threads to look at?

* Am I mad? :)

BTW, I'm aware that this has similarities to call with continuation,
and that you can use statesaver.c & generators to achieve something
vaguely similar to continuations, but I'm more after this specific
approach, rather than that general approach. (After all, even ruby
notes that their most common use for call/cc is to obfuscate code -
often accidentally - and I'm not particularly interested in that :)

Whereas the unix style exec is well understood by many people, and
when it's appropriate can be extremely useful. My suspicion is that
my ideasabove actually maps to a common idiom, but I'm curious to
find that commonidiom.

I'm fairly certain something like this could be implemented using
greenlets, and also fairly certain that Stackless has been down this
route in the past, but I'm not able to find something like this exec
style call there. (Which is after all more constrained than your usual
call with continuation approach)

So, sorry for the length of this, but if anyone has any thoughts, I'd be
very interested. If they don't, I hope it was interesting :)

Regards,


Michael.
 
M

Michael

Carl said:
Maybe look to see how tail-recursive optimization in languages such as
Scheme work, and whether it can be generalized.

Thanks for the feedback - I should've remembered tail recursion.
I doubt this would be possible without a major change in how functions
work in Python. The most obvious problem is that functions refer to
local variables by an index, not by name. If you were to execute the
bytecode of one function in the stack frame of another, Bad Things
would happen.

Oh that's a pity. I can see why you'd do that, but it is a pity. That would
tend to imply that _replacing_ rather than _reusing_ the top frame is the
most doable/likely/sensible approach. (It's also very obviously easier to
emulate)

(And yes, I was consider reusing or replacing *only* the top stack frame.
Replacing seems better with retrospect, even if reusing seemed like a fun
idea :)
Only if you were to replace the whole stack. If you only replace or
reuse the top frame, I would think greet would exit and execution would
resume right after the point from which set_name was called. Or am I
misunderstanding what you want?

I think you are.

In the hypothetical example, your code by definition gets called by
something. This leave a hypothetical return point. For the moment, lets
make things clearer by what I mean by changing the example to this:

at line 12, we call 8. We can argue then our stack looks like this:
[ { "context" : "main", pc : 8 }, { "context" : "__main__", pc : 12 }, ]

(I'm going to push/pop at the front of the list)

We reach line 9, before we execute the code there:
[ { "context" : "main", pc : 9 }, { "context" : "__main__", pc : 12 }, ]

After we execute, it does a cexe call of set_name, not a normal call of
set_name. This causes the top stack frame to be _replaced_ :

[ { "context" : "set_name", pc : 1 }, { "context" : "__main__", pc : 12 }, ]

We then carry on, until we reach line 3, at which point before execution our
stack would look something like:

[ { "context" : "set_name", pc : 3 }, { "context" : "__main__", pc : 12 }, ]

And after:

[ { "context" : "greet", pc : 5 }, { "context" : "__main__", pc : 12 }, ]

We'd then execute line 6, and after executing that line, our stack would
look like this:

[ { "context" : "greet", pc : 6 }, { "context" : "__main__", pc : 12 }, ]

We're falling off the end of a function at that point, so we'd pop the top
stack frame, as follows:
[ { "context" : "__main__", pc : 12 }, ]

Which means we return to the line after 12, and continue on with line 13
print "see what I mean". That means the '''print "We don't reach here"'''
code isn't executed.

Thought so!

Thanks :)


Michael.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,767
Messages
2,569,573
Members
45,046
Latest member
Gavizuho

Latest Threads

Top