Updated stdio.fnl to use built in fennel repl.

This commit is contained in:
Alexander Griffith 2019-12-02 21:30:30 -05:00
parent 67692bd5e0
commit 373398ce36
5 changed files with 3130 additions and 155 deletions

2866
fennel.lua Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,80 +3,196 @@
local fennel_dir = arg[0]:match("(.-)[^\\/]+$")
package.path = fennel_dir .. "?.lua;" .. package.path
local fennel = require('fennel')
local unpack = unpack or table.unpack
local help = [[
Usage: fennel [FLAG] [FILE]
--repl : Launch an interactive repl session
--compile FILES : Compile files and write their Lua to stdout
--help : Display this text
Run fennel, a lisp programming language for the Lua runtime.
When not given a flag, runs the file given as the first argument.]]
--repl : Launch an interactive repl session
--compile FILES : Compile files and write their Lua to stdout
local options = {
sourcemap = true,
}
--no-searcher : Skip installing package.searchers entry
--indent VAL : Indent compiler output with VAL
--add-package-path PATH : Add PATH to package.path for finding Lua modules
--add-fennel-path PATH : Add PATH to fennel.path for finding Fennel modules
--globals G1[,G2...] : Allow these globals in addition to standard ones
--globals-only G1[,G2] : Same as above, but exclude standard ones
--require-as-include : Inline required modules in the output
--metadata : Enable function metadata, even in compiled output
--no-metadata : Disable function metadata, even in REPL
--correlate : Make Lua output line numbers match Fennel input
local function dosafe(filename, opts, arg1)
--eval SOURCE (-e) : Evaluate source code and print the result
--help (-h) : Display this text
--version (-v) : Show version
Metadata is typically considered a development feature and is not recommended
for production. It is used for docstrings and enabled by default in the REPL.
When not given a flag, runs the file given as the first argument.
When given neither flag nor file, launches a repl.
If ~/.fennelrc exists, loads it before launching a repl.]]
local options = {}
local function dosafe(filename, opts, args)
local ok, val = xpcall(function()
return fennel.dofile(filename, opts, arg1)
return fennel.dofile(filename, opts, unpack(args))
end, fennel.traceback)
if not ok then
print(val)
io.stderr:write(val .. "\n")
os.exit(1)
end
return val
end
local compileOptHandlers = {
['--indent'] = function ()
options.indent = table.remove(arg, 3)
local function allowGlobals(globalNames)
options.allowedGlobals = {}
for g in globalNames:gmatch("([^,]+),?") do
table.insert(options.allowedGlobals, g)
end
end
for i=#arg, 1, -1 do
if arg[i] == "--no-searcher" then
options.no_searcher = true
table.remove(arg, i)
elseif arg[i] == "--indent" then
options.indent = table.remove(arg, i+1)
if options.indent == "false" then options.indent = false end
table.remove(arg, 2)
end,
['--sourcemap'] = function ()
options.sourcemap = table.remove(arg, 3)
if options.sourcemap == "false" then options.sourcemap = false end
table.remove(arg, 2)
end,
['--correlate'] = function ()
table.remove(arg, i)
elseif arg[i] == "--add-package-path" then
local entry = table.remove(arg, i+1)
package.path = entry .. ";" .. package.path
table.remove(arg, i)
elseif arg[i] == "--add-fennel-path" then
local entry = table.remove(arg, i+1)
fennel.path = entry .. ";" .. fennel.path
table.remove(arg, i)
elseif arg[i] == "--correlate" then
options.correlate = true
table.remove(arg, 2)
end,
}
table.remove(arg, i)
elseif arg[i] == "--globals" then
allowGlobals(table.remove(arg, i+1))
for globalName in pairs(_G) do
table.insert(options.allowedGlobals, globalName)
end
table.remove(arg, i)
elseif arg[i] == "--globals-only" then
allowGlobals(table.remove(arg, i+1))
table.remove(arg, i)
elseif arg[i] == "--require-as-include" then
options.requireAsInclude = true
table.remove(arg, i)
elseif arg[i] == "--metadata" then
options.useMetadata = true
table.remove(arg, i)
elseif arg[i] == "--no-metadata" then
options.useMetadata = false
table.remove(arg, i)
end
end
if not options.no_searcher then
local opts = {}
for k,v in pairs(options) do opts[k] = v end
table.insert((package.loaders or package.searchers), fennel.make_searcher(opts))
end
-- Try to load readline library
local function tryReadline(opts)
local ok, readline = pcall(require, "readline")
if ok then
readline.set_options({
keeplines = 1000
})
function opts.readChunk(parserState)
local prompt = parserState.stackSize > 0 and '.. ' or '>> '
local str = readline.readline(prompt)
if str then
return str .. "\n"
end
end
-- completer is registered by the repl, until then returns empty list
local completer
function opts.registerCompleter(replCompleter)
completer = replCompleter
end
local function replCompleter(text, from, to)
if completer then
-- stop completed syms from adding extra space on match
readline.set_completion_append_character('')
return completer(text:sub(from, to))
else
return {}
end
end
readline.set_complete_function(replCompleter)
end
end
if arg[1] == "--repl" or #arg == 0 then
local ppok, pp = pcall(fennel.dofile, fennel_dir .. "fennelview.fnl", options)
if ppok then
options.pp = pp
else
ppok, pp = pcall(require, "fennelview")
if ppok then
options.pp = pp
end
end
local initFilename = (os.getenv("HOME") or "") .. "/.fennelrc"
local init = io.open(initFilename, "rb")
tryReadline(options)
if init then
init:close()
-- pass in options so fennerlrc can make changes to it
dosafe(initFilename, options, options)
end
print("Welcome to fennel!")
print("Welcome to Fennel " .. fennel.version .. "!")
if options.useMetadata ~= false then
print("Use (doc something) to view documentation.")
end
fennel.repl(options)
elseif arg[1] == "--compile" then
-- Handle options
while compileOptHandlers[arg[2]] do
compileOptHandlers[arg[2]]()
end
for i = 2, #arg do
local f = assert(io.open(arg[i], "rb"))
local f = arg[i] == "-" and io.stdin or assert(io.open(arg[i], "rb"))
options.filename=arg[i]
local ok, val = xpcall(function()
return fennel.compileString(f:read("*all"), options)
end, fennel.traceback)
print(val)
if not ok then os.exit(1) end
if ok then
print(val)
else
io.stderr:write(val .. "\n")
os.exit(1)
end
f:close()
end
elseif #arg >= 1 and arg[1] ~= "--help" then
elseif arg[1] == "--eval" or arg[1] == "-e" then
if arg[2] and arg[2] ~= "-" then
print(fennel.eval(arg[2], options))
else
local source = io.stdin:read("*a")
print(fennel.eval(source, options))
end
elseif arg[1] == "--version" or arg[1] == "-v" then
print("Fennel " .. fennel.version)
elseif #arg >= 1 and arg[1] ~= "--help" and arg[1] ~= "-h" then
local filename = table.remove(arg, 1) -- let the script have remaining args
dosafe(filename)
if filename == "-" then
local source = io.stdin:read("*a")
fennel.eval(source, options)
else
dosafe(filename, options, arg)
end
else
print(help)
end

View file

@ -1,5 +1,4 @@
(require "love.event")
(local fennel (require "lib.fennel"))
(local view (require "lib.fennelview"))
;; This module exists in order to expose stdio over a channel so that it
@ -7,51 +6,41 @@
(local (event channel) ...)
(defn display [s]
(io.write s)
(io.flush))
(defn prompt []
(display "\n>> "))
(defn read-chunk []
(let [input (io.read)]
(when input
(.. input "\n"))))
(var input "")
(when channel
(let [(bytestream clearstream) (fennel.granulate read-chunk)
read (fennel.parser
(fn []
(let [c (or (bytestream) 10)]
(set input (.. input (string.char c)))
c)))]
(while true
(prompt)
(set input "")
(let [(ok ast) (pcall read)]
(if (not ok)
(do
(display (.. "Parse error:" ast "\n"))
;; fixme: not sure why the following fails
;; (clearstream)
)
(do
(love.event.push event input)
(display (: channel :demand))))))))
(let [prompt (fn [] (io.write "> ") (io.flush) (io.read "*l"))]
((fn looper [input]
(when input
;; This is consumed by love.handlers[event]
(love.event.push event input)
(let [output (: channel :demand)]
;; There is probably a more efficient way of determining an error
(if (and (. output 2) (= "Error:" (. output 2)))
(print (view output))
(each [_ ret (ipairs output)]
(print ret))))
(io.flush)
(looper (prompt)))) (prompt))))
{:start (fn start-repl []
(let [code (love.filesystem.read "stdio.fnl")
lua (if code
(love.filesystem.newFileData
(fennel.compileString code) "io")
(love.filesystem.read "lib/stdio.lua"))
thread (love.thread.newThread lua)
io-channel (love.thread.newChannel)]
luac (if code
(love.filesystem.newFileData
(fennel.compileString code) "io")
(love.filesystem.read "lib/stdio.lua"))
thread (love.thread.newThread luac)
io-channel (love.thread.newChannel)
coro (coroutine.create fennel.repl)
out (fn [val]
(: io-channel :push val))
options {:readChunk coroutine.yield
:onValues out
:onError (fn [kind ...] (out [kind "Error:" ...]))
:pp view
}]
;; this thread will send "eval" events for us to consume:
(coroutine.resume coro options)
(: thread :start "eval" io-channel)
(set love.handlers.eval
(fn [input]
(let [(ok val) (pcall fennel.eval input)]
(: io-channel :push (if ok (view val) val)))))))}
(coroutine.resume coro (.. input "\n"))))))}

View file

@ -1,78 +1,46 @@
require("love.event")
local fennel = require("lib.fennel")
local view = require("lib.fennelview")
local event, channel = ...
local function display(s)
io.write(s)
return io.flush()
end
do local _ = display end
local function prompt()
return display("\n>> ")
end
do local _ = prompt end
local function read_chunk()
local input = io.read()
if input then
return (input .. "\n")
end
end
do local _ = read_chunk end
local input = ""
local function _0_(...)
if channel then
local bytestream, clearstream = fennel.granulate(read_chunk)
local function _0_()
local c = (bytestream() or 10)
input = (input .. string.char(c))
return c
end
local read = fennel.parser(_0_)
while true do
prompt()
input = ""
do
local ok, ast = pcall(read)
local function _1_(...)
if not ok then
return display(("Parse error:" .. ast .. "\n"))
else
love.event.push(event, input)
return display(channel:demand())
end
end
_1_(...)
end
end
return nil
end
end
_0_(...)
local function start_repl()
local code = love.filesystem.read("stdio.fnl")
local function _1_()
if code then
return love.filesystem.newFileData(fennel.compileString(code), "io")
else
return love.filesystem.read("lib/stdio.lua")
end
end
local lua = _1_()
local thread = love.thread.newThread(lua)
local io_channel = love.thread.newChannel()
thread:start("eval", io_channel)
local function _2_(input)
local ok, val = pcall(fennel.eval, input)
local function _3_()
if ok then
return view(val)
else
return val
end
end
return io_channel:push(_3_())
end
love.handlers.eval = _2_
return nil
end
return {start = start_repl}
require("love.event")
local view = require("lib.fennelview") local event, channel = ... local function _0_(...) if channel then
local prompt = prompt local function _0_() io.write("> ") io.flush() return io.read("*l") end prompt = _0_
local function looper(input) if input then
love.event.push(event, input)
do local output = channel:demand()
if (output[2] and ("Error:" == output[2])) then
print(view(output)) else
for _, ret in ipairs(output) do
print(ret) end end end
io.flush()
return looper(prompt()) end end return looper(prompt()) end end _0_(...)
local function start_repl()
local code = love.filesystem.read("stdio.fnl") local luac = luac
if code then
luac = love.filesystem.newFileData(fennel.compileString(code), "io") else
luac = love.filesystem.read("lib/stdio.lua") end
local thread = love.thread.newThread(luac)
local io_channel = love.thread.newChannel()
local coro = coroutine.create(fennel.repl) local out = out
local function _2_(val)
return io_channel:push(val) end out = _2_ local options = options
local function _3_(kind, ...) return out({kind, "Error:", ...}) end options = {onError = _3_, onValues = out, pp = view, readChunk = coroutine.yield}
coroutine.resume(coro, options)
thread:start("eval", io_channel)
local function _4_(input)
return coroutine.resume(coro, (input .. "\n")) end love.handlers.eval = _4_ return nil end return {start = start_repl}

View file

@ -6,6 +6,13 @@ The first step is to see how fennel works as a standalone execution environment,
This repo contains the minimal viable setup to get started with Phil Hegelberg's game design process, plus some additional libraries.
#+BEGIN_SRC bash
PROJECT=project-name
git clone https://gitlab.com/alexjgriffith/min-love2d-fennel.git $PROJECT
cd $PROJECT
rm -rf .git
#+END_SRC
* Phil's Modal Callbacks (PMC)
Phil Hegelberg's [[https://gitlab.com/technomancy/exo-encounter-667/][exo-encounter-667]] is structured using a modal callback system. Each game state has a mode and each mode has a series of specific callbacks.
@ -38,3 +45,32 @@ If you design your game as a series of states in a very simple state machine, fo
(add-hook 'fennel-mode-hook 'fennel-mode-hook-fun)
#+END_SRC
* Known Issues
Presently fennel.lua has to be in the main directory for =fennel-reload= from fennel-mode.el to work. The issue is
#+BEGIN_SRC elisp
(let [old (require :module-name)
_ (tset package.loaded :module-name nil)
(ok new) (pcall require :module-name) ;; fails if fennel not in base directory
new (if (not ok) (do (print new) old) new)]
(when (= (type new) :table)
(each [k v (pairs new)] (tset old k v))
(each [k (pairs old)] (when (not (. new k)) (tset old k nil)))
(tset package.loaded :module-name old)))
#+END_SRC
The error that gets thrown is.
#+BEGIN_SRC
./module-name.fnl:#Some Number#: module 'fennel' not found:
no field package.preload['fennel']
no 'fennel' in LOVE game directories.
no file 'fennel' in LOVE paths.
no file './fennel.lua'
no file '/usr/local/share/luajit-2.0.5/fennel.lua'
no file '/usr/local/share/lua/5.1/fennel.lua'
no file '/usr/local/share/lua/5.1/fennel/init.lua'
no file './fennel.so'
no file '/usr/local/lib/lua/5.1/fennel.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
#+END_SRC