[Push] On re-entrant PUSH programs and how prefix is postfix
Maarten
mkeijzer at xs4all.nl
Wed Feb 4 16:28:57 EST 2004
Hi all,
here's a note on evaluating programs in the push language. As Lee might
remember, in the early days of push (gecco SF) I asked if push could be used
in time-slicing environments --- mainly because I foresaw problems with
non-halting programs (in that non-halters by definition take the most
computational resources). This was safely ignored by Lee, who just uses an
upper bound on execution time. I still think that for evolving programs,
complete control over the execution of the programs is important.
Now I've got my own implementation in python running (will make this available
at some point), and have taken the opportunity to implement my own
'reentrant' push interpreter. It turned out to be deceptively simple, with a
small twist that prefix push programs are easier (faster) to interpret than
postfix programs. This was an unforeseen side-effect, and because I think
prefix is easier to read/write than postfix, I'm posting the algorithm here.
If you don't care, you can stop reading now.
Ok, a re-entrant interpreter. What I wanted to achieve is to run a push
program for some time-steps, abort it after the limit is reached and pick up
the computation where it left of whenever I so choose. With a recursive
interpreter, this is hard. Stopping the execution after a number of steps
isn't that big a deal, but re-entering it again... don't see it happen that
easily (but then again, I never understood what call with current
continuation does in scheme either.)
How I finally chose to implement it is with YAS (yet another stack), this time
an execution stack. This one is similar to the CODE stack, but holds
everything that is yet to be executed. It has no operators defined on it.
Whenever for instance a DO* is executed, the statement will push the top of
the code stack on this execution stack, and returns. DO will first push a
CODE.POP statement and subsequently the code (while leaving the code on top
of the code stack)
Whenever an execution is performed, the interpreter will pop the execution
stack: if it's an instruction, it will get executed (in the case of DO(*)
this will add stuff on the execution stack, while in the case of QUOTE, the
next item on the execution stack will get popped and pushed on the CODE
stack.). If however the top of the execution stack is a piece of CODE itself
(a list), something interesting happens: first the elements of the list are
pushed on the executions stack, and until the top item is an instruction this
is repeated. BUT: to implement the postfix syntax of PUSH, they need to be
pushed in REVERSE order. If I were to push them in normal (efficient) order,
we would get a prefix interpretation of a push program (except of course for
CODE.QUOTE, which would become postfix). This is normal with popping stacks
on other stacks, but given that this seems to be an efficient way to
implement a reentrant (and iterative) interpreter, it might deserve some
consideration.
An example might help: suppose we have the following piece of code:
Code Stack: [ ( (4 3 2) (+ *) ) ]
Exec Stack: [ DO* ]
(types ommitted for brevity, using [a,b,c] syntax to indicate a stack and (a b
c) for a list: for readibility)
Ok, next instruction: DO* gets popped from the execution stack, and will push
the top of the code stack on the execution stack:
Exec Stack: [ ( (4 3 2) (+ *) ) ]
Next instruction: the top of the execution stack is a list, so the elements
will get pushed in REVERSE order, first (+ *)
Exec Stack: [(+*)]
then (4 3 2)
Exec Stack:[ (4 3 2) , (+ *) ]
Again, as the top of the stack is still a list, the process is repeated: first
push 2, then 3, then 4 (So REVERSE order) to get:
Exec Stack: [ 4, 3, 2, (+ *) ]
Now we're ready to evaluate the next arguments, but note that we can abort the
calculation here, and pick it up whenever we feel like it. In the end, this
will have executed 4,3,2,+,* and results in 20.
This is how it works now, and it works exactly like a normal, recursive,
interpreter. The only thing I don't like is that I have to reverse the lists
and pop them on the execution stack whenever such a list. Funnily enough, if
one were to not do that (just push the car of the list first), the
interpretation becomes prefix. Same example:
Code Stack: ( (* +) (2 3 4) ]
Exec Stack: [DO*]
Now, after pushing the code on the execution stack, we will unwrap it by first
pushing (* +), and then pushing (2 3 4): this leads to
Exec Stack: [ (2 3 4), (* +) ]
and another round: push 2,3,4 in order:
Exec Stack: [ 4, 3, 2, (* +) ]
Almost exactly the same situation as above, only now CODE is prefix while
execution is postfix. It is still reentrant: by keeping the execution stack
together with the other stacks we can pick up calculations whenever we want
to continue the evaluation. Continuing the example: execution of 4,3,2 will
lead to:
Int Stack: [2,3,4]
Exec Stack: [ (* +) ]
Another round of pushing:
Int Stack: [2,3,4]
Exec Stack: [+,*]
and the net result is 20 as above
Maybe not too big a deal, but I do see a very efficient C(++) implementation
lurking here, which is reentrant and where code is prefix. I personally like
prefix, and maybe genetic programs like it too (I'm pretty sure it does
actually, but that's another discussion). I post it here maybe for some
discussion, but mostly for your information. The reversing of lists can be
circumvented quite easily, so I highly doubt it's an efficiency issue.
With prefix CODE (yet postfix execution), the principle of least surprise is
more easily implemented. Take for example the snippet:
(- 4 3)
One would expect this to evaluate to 1. With the current push definition, this
does not happen: the execution stack will be [3 4 -] and the definition of
minus subtracts 4 from 3, resulting in -1. But ok, this could be fixed, yet
is not nice, as many operators need to be reconsidered...
So, all in all, given the amount of change involved (a reversal of practically
anything no less, and reconsideration of asymetric operators) please consider
this mainly a curiosum. I'm perfectly happy to live with reversing the code
everytime I want to push it on the execution stack, but might, for my own
experiments (Lee: I'm planning to see if I can get paper #340 to work in
push) go with a prefix approach... not sure though.
Just a small sample, with all usual push interpretation conventions in place,
but only by avoiding to reverse the list in my implementation, the infamous
factorial function is the following:
( CODE.IF
INTEGER.< 2 INTEGER.DUP
(INTEGER.* CODE.DO INTEGER.- 1 INTEGER.DUP CODE.DUP)
CODE.QUOTE
(INTEGER.POP 1)
CODE.QUOTE
)
Note that the all-important control statement CODE.IF is now at the top of the
list, and 'maybe' the thing is slightly more comprehensible.
As I said, FYI,
-Maarten-
More information about the Push
mailing list