brson · GitHub

If dada is to be capable of interactive use then it needs to be architected and tested early. And some people (like me!) can usefully dogfood even a basic repl.

I think we should add a repl and get it under test in a way that it can maintain parity with the batch interpreter, probably put it on the playground.

I have never implemented a repl before, but to my mind the main thing that distinguishes a repl is that it maintains a persistent environment of local variables that can be referenced by future input lines, something like an endless stack frame. This is something that has always been hard for Rust, with a lot of efforts at repls simply growing a large buffer of statements that are continually reexecuted. It'll be the hard part of a dada repl too, but that's why we need to figure it out now.

To get started I think we can quickly achieve a few things:

  • All the basic repl UI features of line reading, signal handling, history management, via rustyline
  • Multiline parsing by scanning for open token trees
  • Distinguishing between items and expressions
  • Compilation of items by:
    • maintaining a map of item names to source code
    • each evaluation, recreating a single source text containing all the items and updating the db.
  • Compilation of expressions by:
    • Wrapping them in a function and interpreting that function

It's going to take a lot of experimentation to figure out how to deal with expressions.

I think the repl will need to distinguish between variable assignment expressions and non-variable-assignment expressions, but even non-variable-assignment expressions will have to reference previous bindings.

@nikomatsakis has suggested a pattern of code generation that should work well to get started with expressions that reference variables, while leaning hard on dada's incremental compilation:

Given this sequence of inputs:

> x = 22
> y = x + 22

Transform it into:

async fn step1() {
    x = 22
    step2(x).await
}
async fn step2(x) {
    y = x + 22
    step3(x, y).await
}
async fn step3(x, y) {
    y
}

For every step the generated function simply passes on every variable to a stub function for the next step, preserving all the variable bindings for the next step's use. When the next step evaluates it replaces its stub function with its own definition and creates a stub for the next-next step.

dada's incremental compilation should make this quite efficient without much effort.

There are plenty of problems to figure out with this pattern, but it's worth starting and seeing how far it can go.

For testing, we'll probably add a mode that actually starts another process and pipes stdin/out. We should be able to make it so all batch tests that compile and run successfully can be fed into the repl and behave the same, using the same reference files, though confirming that both modes issue the same e.g. warning diagnostics could be tricky. It may also be easy use the same reference files for files that compile successfully but have runtime errors. Compiler errors could be harder, not sure.

Read the original on github.com ↗