src/pset-parse.lisp: Parse and evaluate expressions in two separate steps.
authorMark Wooding <mdw@distorted.org.uk>
Tue, 20 Aug 2019 01:04:16 +0000 (02:04 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 20 Aug 2019 01:21:40 +0000 (02:21 +0100)
commit7469d31e1a189759e6c32a5116b1b8e9da750da1
treeb6072e3b04e46843300d8af119bd39916461ecb6
parent25ffaef064be85cad56848252cd46f202ddf31c4
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.)
src/pset-parse.lisp