Push Instruction Documentation

Some Push instructions are trickier to understand than others, especially the ones that implement looping/control flow. This wiki page documents the non-trivial instructions found in modern Push implementations with the hopes of making it easier to implement comparable PushGP systems.

For people using Clojush as their reference implementation, there is also the clojush-playground project which provides REPL friendly utilities for testing out instructions.

Each section is dedicated to an individual instruction. It includes a description of that instruction’s logic in prose, some rationale for why the instruction should be used, the conditions under which it should NOOP, and some example “before and after” stacks to demonstrate the behavior.

Each “before and after” (B&A) stacks two EDN maps. The top of the stack being the first item in the list (furtherest to the left).

Contributing:

To add documentation of a specific instruction, copy an existing section and populate each section. When coming up with “before and after” stacks, it is helpful to include a base case and any edge cases.

The following should NOT be considered edge cases that need documenting.

  • Meeting any of the listed NOOP conditions.
  • Reaching interpreter limits, such as step limit or stack depth limits.
  • Any non-standard interpreter modes or evolutionary features. (ie. epigenetics)

code_do_then_pop

Pushes a code_pop instruction and the top item of the code stack to the exec stack. Does not consume the top item from the code stack.

Assumes the code_pop is an instruction which pops the top item off the code stack.

Rationale:

Used to perform the top code item before it is removed from the code stack. This can be used to implement recursive behavior if the top code item manipulates the code stack.

NOOP Conditions:

  • Empty code stack.

B&A: Base case

{
    :int  ()
    :code (1)
    :exec ()
}
{
    :int  ()
    :code (1)
    :exec (1 code_pop)
}

B&A: Recursion

Assuming code_dup in an instruction will duplicate the top item of the code stack, executing the code_do_then_pop instruction with the following stack will result in an infinite loop producing 1 values on the :int stack.

{
    :int  ()
    :code ((code_dup 1))
    :exec ()
}
{
    :int ()
    :code ((code_dup 1))
    :exec ((code_dup 1) code_pop)
}

code_do_range

Evaluates the top item on the code stack for each step along the range i to j.
Both i and j are taken from the int stack.

Rationale:

Implements a loop where the “body” is taken from the code stack.

NOOP Conditions:

  • Empty code stack.
  • Fewer than 2 items on the int stack.

B&A: Counting up

Loop counter is 2 and loop will stop at 5.

Question: Why is the current loop index put on the :int stack each iteration?

{
    :int  (5 2)
    :code ("A")
    :exec ()
}
{
    :int  (2)
    :code ()
    :exec ("A" (3 5 code_from_exec "A" code_do_range))
}

B&A: Counting down

Loop counter is 2 and loop will stop at 0.

{
    :int  (0 2)
    :code ("A")
    :exec ()
}
{
    :int  (2)
    :code ()
    :exec ("A" (1 0 code_from_exec "A" code_do_range))
}

B&A: Exit loop

Loop stops at the current iteration (counter and termination at 2).

{
    :int  (2 2)
    :code ("A")
    :exec ()
}
{
    :int  (2)
    :code ()
    :exec ("A")
}

Undocumented

  • exec_do_range
  • code_do_count
  • exec_do_count
  • code_do_times
  • exec_do_times
  • exec_while
  • exec_do_while
  • code_map
  • code_if
  • exec_if
  • code_when
  • exec_when
  • Add more to the list
1 Like

@lspector Do you think this wiki belongs in #orientation?

1 Like

Yes, I think that’s a great place for it.

My only very minor reservation with this whole thing is that the sections don’t include a name for the actual instruction, just an English label like “Code Do Range.” I realize this is probably intentional because the actual instruction names will vary among implementations, but it’s a little confusing/abstract especially with labels like “Code Do Then Pop,” which may cause the reader to think “wait, what?, is that an instruction?” Maybe we could add an example actual name that would make it more clear, like “(example: code_do_then_pop in PyshGP)”?

2 Likes

Do we need to document both the code_ and exec_ versions? Most users of a Push system will never use the code stack; none of the problems in either benchmark suite use them, for example.

At a minimum, I’d start by documenting the exec_ instructions, and then maybe do the code_ instructions in a separate, but related document.

I also think there’s a discussion to be had (perhaps in a different topic) about what’s the “right” thing to do when, e.g., something like exec_if no-ops. At the moment if there’s not a value on the boolean stack (in both Clojush and Propeller, at least), then exec_if no-ops and both the then and else blocks are left on the exec stack. I’m not actually convinced that’s a good idea, but I’ve never experimented with it to see. Since those blocks are fundamentally connected to the exec_if (they only exist as blocks because of the way we convert exec_if from the Plushy genome to a Push program), it seems to me that it might make more sense to remove them from the exec stack as well. As I said, I’ve never experimented with this, but this is a place where I’m not sure I’d want to “bake in” that notion of no-op before someone has looked into its impact.

I can say that I’ve seen some infinite-loop programs that did some weird things with no-oping exec instructions, which made me wonder if we’re really doing the “best” thing here.

2 Likes

I just published a tiny library for testing out Clojush instructions in the REPL without needed access to a copy of the Clojush project source code.

It can be pointed to any version of Clojush (in a deps.edn or clj command) but you can try it out against the most recent Clojush version in Clojars in a REPL with this 1 liner:

clj -Sdeps '{:deps {clojush/clojush {:mvn/version "LATEST" :exclusions [bouncycastle/bctsp-jdk14]} io.github.erp12/clojush-playground {:git/sha "72deacfe157da2134878ed4702a861e1ebc8bcda"}}}'

Then mess around with trying different instructions on different states:

(require '[erp12.clojush-playground.core :as play])

(play/exercise-instruction-1
  'exec_do*count
  {:integer (list 3)
   :exec    (list true)
   :boolean (list)})
;; {:integer (), :exec ((0 2 exec_do*range true)), :boolean ()}

Paired with reading the Clojush source code, hopefully this can help demystify what some of these instructions are doing. Ideally, we would still capture the rationale for some of the wackier instruction in prose here in this thread. More information about the playground’s functionality can be found in the README on GitHub.

2 Likes

Is this an argument for at least pulling the Push interpreter out of either Clojush or Propeller and have the as stand-alone projects that the others depend on? Then, for example, we wouldn’t have all these questions about whether Clojush and Propeller “do the same thing”?

3 Likes

/me clicks “heart” 100 times

3 Likes

For the end goal I have in mind, yes!

One subset of people who care about PushGP is those who are looking for an implementation of the Push language which yielded the numbers in the tables of our papers. I think this subset would be well served by a single implementation that:

  1. Provides the “standard” set of stacks, instructions, limits, and other program execution behaviors.
  2. Allow programs to be represented by some reasonably generic data structure that could be produced in their own systems.
  3. Is callable from wide range of runtimes. (ie. not just JVM)
  4. runs roughly as fast as a Turing complete stack-machine interpreter can be expected to.

That said, there is another (smaller) subset of people who care about PushGP that will want to use as an inductive program synthesis system for their domain. These people care about adding custom stacks, instructions, and other extensions. I would imagine this would be difficult to provide in a single “standard” implementation.

3 Likes

Does anyone know the story behind the code_map instruction from Clojush? I have been starting at the code and running some simple states through it, but so far bit able to understand the purpose of the instruction.

Furthermore, I don’t think this instruction comes up much in our discussions. Do we use it other systems like Propeller? Do we often include it in our problem definitions? If so, does it end up in solutions for some problems?

1 Like

I can tell you the story (or I guess parts of what are probably several stories), although I’m not sure it does what was intended or it it’s useful as it is.

There was a map instruction in the first versions of Push, which was intended to roughly mimic Common Lisp’s mapcar (roughly equivalent to Clojure’s map), with both the function and the list over which it is mapped taken from the code stack or possibly the child stack, which also contained code. Recall that at this point there was no exec stack. FWIW here’s the underlying Common Lisp implementation from the first version of Push, written in terms of any type, but I think it was only ever built into code and maybe child instructions:

(defun &map (type)
  "Treats the item on top of the stack of the given type as a
list (coercing it to a list if necessary) and the second element 
as a body of code to apply iteratively to each element of the list.
The arguments are both popped prior to the recursive evaluations.
The results are collected and pushed as a list."
  (unless (null (rest (pushtype-stack type)))
    (let ((the-list (ensure-list (first (pushtype-stack type))))
          (the-code (second (pushtype-stack type))))
      ;; clear the args
      (pop (pushtype-stack type))
      (pop (pushtype-stack type))
      ;; do the mapping
      (let ((results nil))
        (dolist (item the-list)
          (push item (pushtype-stack type))
          (evalpush the-code)
          ;; extract, save, and remove the result
          (push (first (pushtype-stack type)) results)
          (pop (pushtype-stack type))
          (when (> (count-points results) ;; escape if result is bloating
                   *max-points-in-program*)
            (return nil)))
        (unless (> (count-points results)
                   *max-points-in-program*)
          (push (reverse (copy-tree results))
                (pushtype-stack type)))))))

Note that although this looks sort of like Clojure, it relies on destructive modification (mutation). Also, because there was no exec stack, it couldn’t work as modern Push looping/recursive/combinator instructions do, pushing the next things to do onto the exec stack for processing in a later cycle. Rather, it recursively invoked the top-level evalpush function, running each function/argument combo in a fresh interpreter. Then it collected all of the results and pushed a list of them back onto the stack in the interpreter that ran map.

So this was an attempt to do something like mapping in the Push context, with both the function and the arguments coming from stacks. It was pretty weird, among other reasons because the “function” may not have been anything like a function. But with the right things in the right places it could indeed achieve a map-like effect.

When the exec stack came along, in Push3, I think we first dropped this map idea entirely, in favor of do-style loops like exec.do*count, which were implemented both for the code and the exec stacks. We also added the combinators. At least that’s what I see from the Scheme implementation.

At some point we must have added map and maybe some other things back in to Clojush, but I think only for code. I’m not sure that we did it correctly/well, or if we ever used it, and I think most of our work has not used the code instructions at all. I guess maybe it was used for some (but not all) autoconstruction work, and maybe in “kitchen sink” configurations, etc., but I think mostly we’ve just relied on exec for everything that’s code-like. We have discussed implementing exec_map and maybe some other higher-order functions in recent discussions of rethinking Push’s iteration/recursion facilities, but I don’t think this has yet gotten very far. And I think that GPT3 used exec_map once when I asked it to write a Push program! But I don’t think we have it yet.

Although I haven’t fully traced through the code_map code above, I do think that a key is probably that the code_wrap and code_cons are being used to assemble the list of all of the results.

2 Likes

Very interesting and helpful! Thanks @lspector

With that history in mind and @mcphee’s comment here

I wonder if it would be acceptable (at least in pyshgp) to not implement any of the code instructinos.

FWIW, I did a search in Clojush problem files within the software/ folder and found a couple of problems that do use the :code stack but they are problems which I typically don’t run.

1 Like

I suppose the research question is whether :code instructions aren’t used in programs because they were never added to the problem definitions to begin with, or whether they are not needed to solve problems in a useful and interesting way.

“We don’t see them in evolved solutions” carries a lot of water for the implicit ways in which problems have been selected, especially when there are no local variables or saved states like :environment, too.

But if :exec can do the work, and produce programs that don’t make you wince when you read the source code…

2 Likes

Is there a difference between the string_contains instruction and the string_includes instruction in Clojush? Unless I am missing something, the only thing I can spot is the order we pop the arguments off the stack.

2 Likes

It’s kind of funny how every code-level decision was made in opposite ways in the two implementations.

But it sure looks like they do the same thing.

I think we should get rid of string_contains since I think all of the decisions in it are worse (especially reliance on interop and the unnecessary if).

2 Likes

It looks like my former student @pxkelly added string_includes when we were adding a bunch of new instructions for PSB2, and didn’t realize we already had this functionality. Feel free to delete either one.

1 Like

Done. I think I did it right… it has been a long time since I’ve committed a change to Clojush :). We’ll see if the automation does the right things…

Does anyone who is up to date on the instruction sets used in modern PushGP publication results know if exec_y is typically used? My reading of the master branch of Clojush seems to imply the answer is yes.

I ask because I am seeing in pyshgp that more CPU time spent performing exec_y than all of parent selection combined, which seems like a red flag. I see some evidence that this instruction is causing a lot of stragglers in the population that hold up the entire generation.

If it’s important for finding solutions, I am fine with the cost. I somewhat recall discussions about exec_y being considered dangerous but I forget if that had to do with solution rates, non-termination, or something else. Anybody else remember? Mostly likely for @lspector @thelmuth @aks @mcphee

2 Likes

Well by definition it makes the program running it non-terminating

1 Like

A program executing exec_y can terminate if something within the body can cause it to escape the loop. The most helpful versions of this probably involve exec_if and exec_pop, but there are other things you could do including exec_flush.

@thelmuth, @aks, and @mcphee probably have better memory than I do about what we’ve learned about exec_y’s benefits and dangers. I do recall that it shows up in solutions, but that doesn’t mean we wouldn’t solve problems without it.

FWIW, one of the GPTP papers this year was (in part) about benefits from reducing looping options, using recursion patterns that are all versions of fold and reduce, if I recall correctly.

2 Likes

Negative anecdotal evidence from @mcphee:

Positive anecdotal evidence:

  • I vaguely remember that when my students were working on adaptive genetic source, we found some data showing that exec_y was being used more than expected and in more useful programs. I just searched for a while here and couldn’t find that history.
  • We’ve certainly seen exec_y used in some solutions. Does that mean we’d find fewer solutions without it? No, it could certainly be the case that it’s still doing more harm than good.

The empiricist in me says we should just do some runs using INSERT YOUR FAVORITE PUSHGP SYSTEM HERE with and without it, and compare times and solution rates. If time is much higher with it and solution rates aren’t much higher (or are lower), then we should ditch it.

2 Likes