The Worst Programming Language (it doesn't have a logo yet)

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.

2021-08-26