Writing new Push instructions for Clojush

April 7, 2011
by Lee Spector (lspector)

A few key facts and tips for anyone who wants to write new Push instructions for use in Clojush:

  • All Push instructions in Clojush must be written to take one argument (a Push interpreter state) and return a Push interpreter state. When you think of the execution of a Push program you should generally think of the instructions as taking arguments from stacks and pushing results on stacks, but at the Clojure level the function that implements an instruction actually takes a complete Push interpreter state (containing all of the stacks with all of their contents) and returns a complete Push interpreter state (containing stacks that have possibly been modified by the execution of the instruction).
  • Push instructions in Clojush should be defined not with the normal Clojure function definition macro “defn” but rather with “define-registered”. This does the work of defn but also registers the instruction in the Push instruction table, so that the Push interpreter will know that it is a Push instruction and know how to call it properly.
  • To understand what a call to define-registered should look like it helps to first appreciate how to define Clojure functions in yet another way, with the “def” special form (which creates a var) and the “fn” special form (which creates a function). A function defined as (defn foo [x] (+ x 2)), which returns the value of its single argument plus 2, can also be defined as (def foo (fn [x] (+ x 2))). In this version the fn expression returns an anonymous function, which def then puts in a var called foo. This is just like defining a constant var, with an expression like (def bar 3), but with a function as the value. The defn version is a little neater, but defn is really just a macro that expands into the version with def and fn.
  • Calls to define-registered should look like calls to def with a function value, in particular with a function value that takes one argument (which will be a Push interpreter state) and which returns a possibly-modified Push interpreter state.
  • A Push interpreter state is a Clojure struct, which is really just a Clojure hash-map with some pre-defined keys. You can access the stacks using hash-map accessors to pull things out (for example, (:integer state) to get the integer stack) and assoc to return a state with a modified stack (for example, (assoc state :integer ()) to return state but with an empty integer stack), but it is probably simpler and clearer in most cases to use the utility function stack-ref to access stacks and the functions push-item and pop-item to return states with modified stacks.
  • One very simple instruction definition that is in a lot of examples is:
    (define-registered in
      (fn [state] (push-item (stack-ref :auxiliary 0 state) :integer state)))

    This defines and registers a function called “in” that takes one argument, a state, and returns the state that results from pushing something — specifically the thing that’s on the top of the auxiliary stack — onto the integer stack in the given state.

  • An example that demonstrates a bit more, and which is probably a good template for many more complex instructions, is float_sin:
    (define-registered float_sin
      (fn [state]
        (if (not (empty? (:float state)))
          (push-item (keep-number-reasonable (Math/sin (stack-ref :float 0 state)))
            (pop-item :float state))

    This follows a generally useful pattern of first checking to see if the conditions for executing the instruction are met — in this case, that means checking to make sure that there’s at least one thing on the float stack (that is, that it’s not empty) — and then returning either the result of executing the instruction or the original state unchanged. In this particular case executing the instruction means replacing the top float with its sine, which we get by pushing the sine onto the float stack in the state that results from popping the float stack in the original state. Here we compute the sine by grabbing the top item on the float stack in the original state, calling Math/sin on it, and then running it through a utility function called keep-number-reasonable that prevents the generation of problematic numerical values (see the implementation in clojush.clj for details).

  • Sometimes the Clojure “threading” operators like ->> can be handy for running a state through a bunch of modifications. See the definition of integer_fromfloat in clojush.clj for a simple example. You never have to do this; it’s just a convenience if you like the threading style.
  • If you use the utility function top-item to access the top item of a stack, then note that it will return :no-stack-item if there was no top item to return. This will also be returned from stack-ref when it tries to access an empty stack, but note that stack-ref is not safe for invalid positions (like trying to get the second item on a stack with only one item), so you should check that there are enough arguments (e.g. with (count (:boolean state)) to see how many things are on the boolean stack) and avoid calling stack-ref if it might be looking too deep.
  • You will notice that many of the Push instructions defined in clojush.clj are actually defined with the help of a higher-order function that returns the necessary fn form when given additional arguments, e.g. for the type on which you want to operate. For example, look at the definitions of exec_dup and all of the other dup instructions. Rather than writing all of the code for checking if the necessary argument is there, and then duplicating it if it is there (but returning the state unchanged if it is not), over again for each type, I wrote a higher-order utility called “duper” that takes a Push type and returns a function with all of the right stuff for the appropriate stack. That means that I only had to write the guts of the instruction once, and that I could define dup for each new type with a simple new statement likeĀ (define-registered boolean_dup (duper :boolean)). This is one of the nice things about working in a Lisp-like language, but it’s just a convenience and you could define all of the Push instructions that you need without using this trick.

Leave a Reply

You must be logged in to post a comment.