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

Jeethu Rao jeethu at tachyontech.net
Mon Jun 26 21:47:29 CEST 2006


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

Michael wrote:
> 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.
>
>   




More information about the Python-list mailing list