Fennel wiki: Repl

Fennel's repl is part of its API. If you want to put a repl into your program, it can be done as easily as loading the fennel module and calling (fennel.repl) from your code.

One potential problem with this is that it will call Lua's io.read function in order to get input to evaluate. As you may know, Lua's IO is synchronous. What this means is that when it goes to look for input, it will wait until the enter key is pressed, and the rest of your program is blocked until then. In some contexts this might be fine, but often it's not going to cut it.

This is where coroutines come in. A coroutine is like a function which can be suspended and resumed at will. We can collect input in our program loop into a table and send it to the repl when the enter key is pressed. Then we tell the repl to use coroutine.yield as the function which reads input; that is, when you go to look for input, suspend your coroutine and wait for someone else to resume it with the data you're waiting for.

Here's a example of how to do that using the LÖVE game framework:

(local fennel (require :fennel))

(local input []) ; store characters as they are typed
(local buffer []) ; output that has been printed
(var incomplete? false)

;; put things into the output buffer
(fn out [xs] (icollect [_ x (ipairs xs) :into buffer] x))

;; display errors in red (love2d-specific convention for colored text)
(fn err [_errtype msg]
  (each [line (msg:gmatch "([^\n]+)")]
    (table.insert buffer [[0.9 0.4 0.5] line])))

;; ensure repl only has access to limited environment
(local env {: love : string : table : math : type : pcall
            : tostring : tonumber : pairs : ipairs
            :print #(out (icollect [_ x (ipairs [$...])] (tostring x)))})

;; put the repl inside a coroutine
(local repl (coroutine.create fennel.repl))

;; start it using the options table
(coroutine.resume repl {:readChunk coroutine.yield
                        :onValues out
                        :onError err
                        :env env})

(fn enter []
  ;; send the input to the repl
  (let [input-text (table.concat (doto input (table.insert "\n")))
        (_ {: stack-size}) (coroutine.resume repl input-text)]
    (set incomplete? (< 0 stack-size)))
  ;; clear the input table afterwards
  (while (next input) (table.remove input)))

(fn love.keypressed [key]
  (match key
    :return (enter)
    :backspace (table.remove input)
    :escape (love.event.quit)))

(fn love.textinput [text]
  (table.insert input text))

(fn love.draw []
  (let [(w h) (love.window.getMode)
        fh (: (love.graphics.getFont) :getHeight)]
    ;; draw every line in the buffer (TODO: don't draw *everything* in buffer)
    (for [i (length buffer) 1 -1]
      (match (. buffer i)
        line (love.graphics.print line 2 (* i (+ fh 2)))))
    ;; draw the input text at the bottom
    (love.graphics.line 0 (- h fh 4) w (- h fh 4))
    (if incomplete? ; change the prompt character
        (love.graphics.print "- " 2 (- h fh 2))
        (love.graphics.print "> " 2 (- h fh 2)))
    (love.graphics.print (table.concat input) 15 (- h fh 2))))

You can run this by putting the above in main.fnl and creating a main.lua file in the same directory containing:

require("fennel").dofile("main.fnl")

Then run love . in that directory.

Note that for a real game using LÖVE you should use the min-fennel-love2d starter repo which includes its own repl already.