We’re trying to finish off the last bits of refactoring in the Nudge language interpreter core soon, and then expect to release it as an open-source Mercurial repository into which we’ll post updates.
Meanwhile, rooting around inside the connecting gunk that underlies something as simple-sounding as a loop structure wrapped around an “IF:THEN” decision point (which is really what drives the interpreter, at least until somebody formalizes a state machine of some sort), one tends to get philosophical.
Two interesting misapprehensions come to light. Our own, and probably others’.
The first technically subtle point we ignored (but are no longer) is the presumptuousness of the “tree vs. linear genome” dichotomy that holds sway in the Genetic Programming literature. In a nutshell, all the earliest and most influential genetic programming systems represented programs using S-expressions. That’s still the case, and at a conference or inside a book you’ll often come across a core assumption that what you’re dealing with is a tree.
With minor variations, these languages colloquially represent programs as dataflow trees in which branchpoints are functions and leaves are literals or bound variables. Setting aside the fact that this structure deeply influences evaluation, code generation and search, it’s a tacit assumption in a lot of the literature and practical community that genetic programming operates on genomes that are trees.
That said, a substantial minority of genetic programming systems use linear genomes: representations of code that are sequences, read left to right. This is the sort of paper tape, genetic algorithm-like genome that comes a lot closer to the state machine reader paradigm, and allows folks to use the more familiar (and arguably intuitive) crossover and mutation operators they learned when working with genetic algorithms. Snip ‘em in half and swap the chunks, line up an arbitrary sequence of instructions, that sort of thing.
There are important differences between implementing tree and linear representation systems not having to do with search dynamics. In the former, semantics and syntax are typically wrapped up in the rules one uses to construct and preserve a valid tree under the operation of structure-modifying operators: an addition operator needs at least two inputs; a constant can’t have any inputs at all. The tree-based GP literature is bloated with innumerable variations on the theme of sense-preserving rules for operators and tree construction, and the theoretical and practical consequences of using those rules.
But in many cases linear representations, in setting aside some or all of those draconian sense-preserving rules so that snip-and-glue crossover can be used, tend to lead to very contingent interpreters. Sometimes the interpreters thrown at these representations draw on messy GA approaches, and “just skip over” instructions that don’t make sense. Some use cunning indirect representations (grammars, expression systems) to smear sense back into the power set of all “programs”, so that any arbitrary sequence of opcodes “means something”.
Needless to say—hang on, is it needless to say? No. It’s important to say that this sort of decision deeply affects the power and appropriateness of genetic programming for a particular problem. Not merely in the ease of modeling, of matching the structure of the programs to the function in the problem domain, but also because search can fail. It can bog down, it can prematurely converge, it can waste your time or fool you by racing to a false end. And trees, with their [typically] highly constrained rules for safe handling, and linear genomes with their [typically] lackadaisical post hoc sensibility, they act differently when you actually do the work.
So, given a long-winded background, what is the Push language? And for that matter what’s Nudge?
Here’s a snip of Nudge code:
[ [1, 2], ‘code_quote’, [1, 2], ‘int_add’ ]
Structurally—that is, in a literal sense—this is a tree. A nested list of opcodes and literals, with the two copies of the sequence [1, 2] “at a lower level” than the root of the program listing.
But the Push and Nudge are interpreted linearly. That is, we start at the left and read through the sequence of instructions, and when we “come to” a nested list we just unpack it and replace it with its contents. With minor exceptions.
So if it’s not a tree, why bother with the “nested” lists at all? Because of those “minor exceptions”, one of which appears in that program. Nested lists in Push and Nudge are not “branches” in the semantic sense used in typical S-expression languages. They’re block delimiters.
Here’s how that program is interpreted:
- The entire program is pushed onto the EXEC stack, and as a list it’s the only element
- The first item on the stack is examined. It’s the program is itself, and so the list is unwrapped and the elements pushed back onto the EXEC stack in the original order
- the new top item on the stack is the block [1, 2], and so this is unwrapped and pushed onto the stack
- the literal 1 is an integer, and is moved to the INT stack
- ditto 2
- code_quote is a Push instruction that acts on the next element down in the EXEC stack, shifting it to the CODE stack. That element is the list [1, 2], so that entire block is shifted as a unit
- the instruction int_add pops two integers from the INT stack, adds them, and pushes the result
- [we're done, since we're out of items on the EXEC stack] We’re left with the integer 3 on the top of the INT stack, and the code fragment [1, 2] on the top of the CODE stack.
Point being made: the first occurrence of [1, 2] in this program has no functional effect on the execution of the program, except insofar as it uses an extra interpreter step to unwrap the block. But the second occurrence is manipulated during the execution of other instructions as a block unit, and indeed in this example it remains a block unit even after it’s sitting on the CODE stack.
Push (and Nudge) programs are self-modifying linear code. What seems to an innocuous programmer to be a “tree” structure is in fact something subtler: the equivalent of begin:end or similar block markup. It’s control structure, and it forms the basis of recursion and looping structures in the language.
That’s important to understand as one implements and husbands an automated search. How should search operators manage these “subtrees” or “blocks”? Should they be aware of these code-modifying instructions to expedite search? How do you measure (or minimize) the complexity of a self-modifying program?
So that’s point one. As I said at the top, an observation, but an important one many folks apparently miss when they see Push code discussed in print:
Push is a strongly typed tree based language which does not enforce syntactic constraints. Each of Push’s types has its own stack.
And that leads nicely to my second observation—another that seems abstruse and theoretical but can turn out to be immensely practical—because there’s a second subtle misstatement in same extract [Sorry, Nic, Bill, Riccardo; I tried to tell you in the copy-editing :)].
The Push 3 language is specified as being typed, but it need not be. Again, this may seem to be a subtlety of computer science, but in practice I expect it can have useful consequences. Enough so that I’m leaning, as we’re developing the Nudge interpreter, far away from that consensual restriction.
Push’s typing, strong or not, arises within its interpreter loop. As we saw in example above, the Push interpreter’s root behavior—the core of its control loop—is to pop the top item on the EXEC stack and act on that. If the item is a code block, then the contents are pushed back onto the EXEC stack in the same order they originally occurred. If the item is a defined NAME (or, in the Nudge interpreter, a bound variable), then the value associated with the binding is pushed as a replacement for the original string. If the item is an instruction, then the algorithm invoked by the instruction is executed, typically with the result of moving items present in specified stacks. Finally, if the item is a “literal”—an integer, floating-point value, boolean or string—it’s pushed onto the appropriate stack.
In Push 3, the “appropriate stack” we use for integers is the INT stack, floats go to the FLOAT stack, booleans to the BOOL stack and [unbound] strings to the NAME stack. And “explicitly typed” instructions reach into clearly specified stacks, not stacks based on the type of their contents; Push’s INT.+ (Nudge ‘int_add’) reaches into the INT stack by reference for its parameters, not because the INT stack contains integers, and it pushes the returned value into that same stack because we tell it to.
But what’s wrong with duck-typing? Nothing, not even in the Push 3 specification. The specified details of contingent behavior of the interpreter’s recognizer says integers go to INT. But in practice, nothing dictates we couldn’t use a NUMBER or SCALAR stack. Or define a suite of instructions called scalar_add and scalar_negate, whose results depend on the types of the parameter values. Add a float to an integer, and you’ll get an answer cast as a float in most programming languages.
It’s a tacit assumption. Not a necessity.
Suppose we did have a SCALAR stack, where integers and floats and complex values and characters all go to live. And we implement a fancy scalar_add, that always pops the top two SCALAR values, and lets the type be defined by the first parameter, discarding “extra” information like complex components or fractional value by truncation or rounding. Suppose [3.4, 2, scalar_add] gives us 5.4, but [3, 2.4, scalar_add] gives us 5 (the integer); [3 + 4i, 2.2, scalar_add] might provide the complex result (5.2 + 4i).
Does it matter? No, insofar as the behavior is consistent and predictable, it’s a suitable substrate for both hand-designed and automated programming. The framework that results might end up being more or less brittle, but as long as the underlying rules are internally consistent and don’t result in syntax faults, it’ll still work.
Is what I’m describing still Push? No. Because what I’m suggesting is something that takes Push’s explicit statement of types and generalizes it into an arbitrary sequence of recognizers. The recognizer loop of the Nudge interpreter is becoming more like this: Is the next fragment “code” (for some definition of that class of structures)? Is it a string that appears as a key in a finite list of bound key:value pairs? Is it a string that I recognize as an instruction? Is it some other literal (integer, float, boolean, string)?
The first possibly useful consequence is the relaxation of the relationship between type and specific stack. Why not have two named “boolean” stacks: BOOL1 and BOOL2? Why not have instructions that shift boolean values between them?
And then the snowball starts rolling: Why not have a stack of “boolean” stacks, and instructions that shift items “up a level” and back down? This isn’t theoretically different from the potential of Push’s explicit stacks; you could do it as things stand right now, with the right Push code infrastructure in place. But trust me when I say it would be a royal pain to breed that infrastructure, to automatically discover the surrounding “subroutines” that subdivided the one BOOL stack into zones and flipped stuff around.
And let the snowball roll on farther: What about instructions that create new, “named” stacks? Or recognizers? Or which restructure the order or importance of recognizers in the interpreter’s core loop? Or alternate EXEC stacks came into being as parallel threads? What about an ITERATOR or GENERATOR stack? And so on, and so on.
The point is not to bloat the system with unnecessary extravagance. It’s to consider the practical utility, the reachability of useful solutions represented in a given language. Things become easier and harder to code by hand, or to discover automatically, when you make these changes to the fundamental architecture of a system that’s the substrate for algorithmic search.
And the point we’re heading for—though we’re not quite there yet—is to improve the usefulness of the Nudge interpreter. So sometime soon we’ll probably start haring off in this new direction, where types and recognized code fragments are a matter of application-specific ontological decisions, not inbuilt in the interpreter itself.
Update: In reading through this, I realize I should give credit where it’s due: I’m sure almost everybody working on the Push language projects through the years has proposed extensions and modifications, shifts in architecture and new ways of making it work on the lowest level. My point in all this, really, is to say we’re trying to make that easier to pursue, by keeping units of function in the interpreter itself cleanly separated.