src/pset-parse.lisp: Parse and evaluate expressions in two separate steps.
Previously, we'd parse and evaluate expressions in an interleaved
fashion: the items on the parser's value stack were (TYPE . VALUE)
pairs, fully evaluated.
This causes a couple of problems.
* Firstly, and most obviously, it means that we can't have operators
which perform short-circuit evaluation -- sometimes failing to
evaluate their operands. This doesn't seem like such a big deal:
after all, we don't have any assignment operators. But we /do/ have
an escape to Lisp's `eval' function via the `?' SEXP primary, and
evaluating Lisp can have arbitrary side-effects.
* Secondly, it makes error handling more annoying. There's logic
throughout for reporting continuable errors and then returning a
`:invalid' marker. This logic is currently concentrated in the
internal `dispatch' function, but continuing to maintain this
convention if and when we crack open `dispatch' to implement more
complex operators would be messy. (I know: I've tried it.)
Deferring evaluation to a second pass solves both problems. Control-
flow operators can just decide not to evaluate operands they're not
interested in. And errors can be caught and turned into an immediate
return from the `parse-expression' toplevel. (The `:invalid' convention
is still part of the external interface, for good reason, but it we can
just use Lisp's non-local control flow forms inside.)
The way this works is unsurprising. The `value' stack now holds thunks
which will calculate and return their values when invoked. Operators
close over the thunks for their operands and produce a new thunk which
will call the operand thunks as required to calculate their result. The
handling of `:invalid' in `dispatch' can be eliminated in favour of a
`restart-case' form around invoking the final value thunk.
The only remaining clever footwork is in `?' SEXP: if there's a read-
time error then we yields a thunk which immediately escapes and returns
`:invalid' from the toplevel. (We do this using `continue' on the
grounds that the read error has already been reported -- along,
probably, with a lot of knock-on parse errors while we try to
resynchronize with the source.)