How it works

We keep a Magma interpreter around for the whole Julia session, send it expressions to evaluate, and read its output.

Because every operation involves sending and receiving synchronous IO messages, which also invoke the Magma interpreter (and parser, etc.), this is all very slow, on the order of 1ms per operation. Hence the Caveat.

Interacting with the interpreter

Calling MagmaCall.interact(f) will wait for the interpreter to become available, lock it, call f(io) where io is the interpreter process, unlock it, and return whatever f(io) returned. In order to support asyncronous interaction (in particular, the Julia garbage collector is asyncronous, and we have finalizers that delete Magma objects), the function f must complete everything it is doing and leave the interpreter in a state ready for the next operation.

A typical interaction looks like this:

interact() do io
    putcmd(io, ..., err=true)
    for line in eachlinetotoken(io, missing)
        # process output...
    end
    checkerr(io)
end

Here, putcmd sends a command to the interpreter. This essentially is the same as print, except that a semicolon is appended automatically. Further, if err=true (recommended) then the whole thing is wrapped in a try ... catch block to support checkerr.

Next, we must process all output that occurred as a result of the command. Typically we do this by generating a random token, instructing Magma to print it, then reading everything up to that token. In this example, eachlinetotoken(io, tok) is an iterator over each line of output until the token tok is observed. If tok is missing, then a new token is randomly generated and sent to Magma first.

There are also skiptotoken (ignores everything), echototoken (prints each line to a given IO stream) and readtotoken (reads everything as a string).

After all output is processed, the call to checkerr(io) checks to see if an error occurred. If so, an appropriate error is raised.

Since we must process all output, we cannot break out of the loop early. Alternatively, we can call skiptotoken(tok) then break.

If the loop might throw an error, it must be caught, any remaining output must be processed (e.g. with skiptotoken), and checkerr called before rethrowing it:

interact() do io
    putcmd(io, ..., err=true)
    tok = puttoken()
    try
        for line in eachlinetotoken(io, tok)
            # process output...
        end
    catch err
        skiptotoken(io, tok)
        checkerr(io)
        rethrow(err)
    end
    checkerr(io)
end