Subinterpreters, resumable errors, and a nicer REPL
It’s now possible to create and run Worst interpreters from within.
Also, raising an error doesn’t stop execution.
It pauses the interpreter – even when in the middle of a Lua function –
and passes an error value back to the outer interpreter for it to deal with
as it pleases.
Resuming the interpreter will then continue as if nothing happened.
Of course, there’s nothing forcing it to use error values specifically,
and so pause
is available to do the same with arbitrary values.
The interactive environment now takes advantage of these additions,
resulting in a simpler implementation with nicer whitespace handling
that lets you know when more code is required by quote
.
Behold
Run an interpreter with interpreter-run
. It will return false on completion,
or any other value (e.g. an error) whenever a pause is encountered.
A consequence is that you cannot pause with false, but that’s probably fine.
interpreter-empty
interpreter-inherit-definitions
; (adding definitions one at a time is also an option)
[ blah blah blah here is my code ... ] interpreter-body-set
while [
interpreter-run
false? if [
; completed! yay
] [
; some error or whatever
; maybe do something useful with it
->string print
; exit loop in this example anyway
#f
]
] []
Errors are “resumable” here because you can catch them and then
pretend they never happened.
For example, the stack-empty
error is resumable by putting something on the
interpreter’s stack. Then just run it again, and the code won’t realise
the stack was ever empty.
(If you don’t put anything on the stack, it will pause again with the same error.)
A catalogue of built-in errors and how to deal with them is planned.
How?
Lua coroutines is how. Pausing is basically just coroutine.pause()
,
and the interpreter stack frames keep track of paused coroutines.
Running the interpreter checks for coroutines to resume
before stepping into any new code. That’s all there is to it.
Efficiency
There may well be a performance impact in using coroutines for everything
instead of pcall
. No idea!
Meanwhile, creating interpreters is not a big deal, as they are quite simple,
and running them is identical to running the main interpreter.
To what end?
Many ends may be whatted with pausable interpreters. CIL, the work-in-progress compiler/interpreter library, uses a fresh interpreter as a controlled environment, and pausing to interact with the unique symbol counter and current level of indentation.
But what about also perhaps:
- Regular ol’ throw/catch exceptions, which simply abandons the inner interpreter on error.
- List comprehensions, lazy sequence generators, asynchronous IO, coroutines, and other constructs that need to keep state around in order to produce values or otherwise do stuff.
- DSLs with their own semantics, interacting with regular code.
- Security boundaries. Don’t want to let some code access files? No problem, run it in an interpreter without any filesystem stuff defined. Want to add some kind of execution limit? Make a new interpreter and wrap every definition in something that pauses in order to decrement a counter.
It may be that no user code will ever use subinterpreters or pausing directly, but they should enable some neat stuff.