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("(.-)[^\\/]+$") local fennel_dir = arg[0]:match("(.-)[^\\/]+$")
package.path = fennel_dir .. "?.lua;" .. package.path package.path = fennel_dir .. "?.lua;" .. package.path
local fennel = require('fennel') local fennel = require('fennel')
local unpack = unpack or table.unpack
local help = [[ local help = [[
Usage: fennel [FLAG] [FILE] Usage: fennel [FLAG] [FILE]
Run fennel, a lisp programming language for the Lua runtime.
--repl : Launch an interactive repl session --repl : Launch an interactive repl session
--compile FILES : Compile files and write their Lua to stdout --compile FILES : Compile files and write their Lua to stdout
--help : Display this text
When not given a flag, runs the file given as the first argument.]] --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 options = { --eval SOURCE (-e) : Evaluate source code and print the result
sourcemap = true,
}
local function dosafe(filename, opts, arg1) --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() local ok, val = xpcall(function()
return fennel.dofile(filename, opts, arg1) return fennel.dofile(filename, opts, unpack(args))
end, fennel.traceback) end, fennel.traceback)
if not ok then if not ok then
print(val) io.stderr:write(val .. "\n")
os.exit(1) os.exit(1)
end end
return val return val
end end
local compileOptHandlers = { local function allowGlobals(globalNames)
['--indent'] = function () options.allowedGlobals = {}
options.indent = table.remove(arg, 3) 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 if options.indent == "false" then options.indent = false end
table.remove(arg, 2) table.remove(arg, i)
end, elseif arg[i] == "--add-package-path" then
['--sourcemap'] = function () local entry = table.remove(arg, i+1)
options.sourcemap = table.remove(arg, 3) package.path = entry .. ";" .. package.path
if options.sourcemap == "false" then options.sourcemap = false end table.remove(arg, i)
table.remove(arg, 2) elseif arg[i] == "--add-fennel-path" then
end, local entry = table.remove(arg, i+1)
['--correlate'] = function () fennel.path = entry .. ";" .. fennel.path
table.remove(arg, i)
elseif arg[i] == "--correlate" then
options.correlate = true options.correlate = true
table.remove(arg, 2) table.remove(arg, i)
end, 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 if arg[1] == "--repl" or #arg == 0 then
local ppok, pp = pcall(fennel.dofile, fennel_dir .. "fennelview.fnl", options) local ppok, pp = pcall(fennel.dofile, fennel_dir .. "fennelview.fnl", options)
if ppok then if ppok then
options.pp = pp options.pp = pp
else
ppok, pp = pcall(require, "fennelview")
if ppok then
options.pp = pp
end
end end
local initFilename = (os.getenv("HOME") or "") .. "/.fennelrc" local initFilename = (os.getenv("HOME") or "") .. "/.fennelrc"
local init = io.open(initFilename, "rb") local init = io.open(initFilename, "rb")
tryReadline(options)
if init then if init then
init:close() init:close()
-- pass in options so fennerlrc can make changes to it -- pass in options so fennerlrc can make changes to it
dosafe(initFilename, options, options) dosafe(initFilename, options, options)
end 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) fennel.repl(options)
elseif arg[1] == "--compile" then elseif arg[1] == "--compile" then
-- Handle options
while compileOptHandlers[arg[2]] do
compileOptHandlers[arg[2]]()
end
for i = 2, #arg do 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] options.filename=arg[i]
local ok, val = xpcall(function() local ok, val = xpcall(function()
return fennel.compileString(f:read("*all"), options) return fennel.compileString(f:read("*all"), options)
end, fennel.traceback) end, fennel.traceback)
if ok then
print(val) print(val)
if not ok then os.exit(1) end else
io.stderr:write(val .. "\n")
os.exit(1)
end
f:close() f:close()
end 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 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 else
print(help) print(help)
end end

View file

@ -1,5 +1,4 @@
(require "love.event") (require "love.event")
(local fennel (require "lib.fennel"))
(local view (require "lib.fennelview")) (local view (require "lib.fennelview"))
;; This module exists in order to expose stdio over a channel so that it ;; This module exists in order to expose stdio over a channel so that it
@ -7,51 +6,41 @@
(local (event channel) ...) (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 (when channel
(let [(bytestream clearstream) (fennel.granulate read-chunk) (let [prompt (fn [] (io.write "> ") (io.flush) (io.read "*l"))]
read (fennel.parser ((fn looper [input]
(fn [] (when input
(let [c (or (bytestream) 10)] ;; This is consumed by love.handlers[event]
(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) (love.event.push event input)
(display (: channel :demand)))))))) (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 [] {:start (fn start-repl []
(let [code (love.filesystem.read "stdio.fnl") (let [code (love.filesystem.read "stdio.fnl")
lua (if code luac (if code
(love.filesystem.newFileData (love.filesystem.newFileData
(fennel.compileString code) "io") (fennel.compileString code) "io")
(love.filesystem.read "lib/stdio.lua")) (love.filesystem.read "lib/stdio.lua"))
thread (love.thread.newThread lua) thread (love.thread.newThread luac)
io-channel (love.thread.newChannel)] 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: ;; this thread will send "eval" events for us to consume:
(coroutine.resume coro options)
(: thread :start "eval" io-channel) (: thread :start "eval" io-channel)
(set love.handlers.eval (set love.handlers.eval
(fn [input] (fn [input]
(let [(ok val) (pcall fennel.eval input)] (coroutine.resume coro (.. input "\n"))))))}
(: io-channel :push (if ok (view val) val)))))))}

View file

@ -1,78 +1,46 @@
require("love.event") require("love.event")
local fennel = require("lib.fennel") local view = require("lib.fennelview") local event, channel = ... local function _0_(...) if channel then
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() local prompt = prompt local function _0_() io.write("> ") io.flush() return io.read("*l") end prompt = _0_
return display("\n>> ") local function looper(input) if input then
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) love.event.push(event, input)
return display(channel:demand()) do local output = channel:demand()
end
end if (output[2] and ("Error:" == output[2])) then
_1_(...) print(view(output)) else
end for _, ret in ipairs(output) do
end print(ret) end end end
return nil io.flush()
end return looper(prompt()) end end return looper(prompt()) end end _0_(...)
end
_0_(...)
local function start_repl() local function start_repl()
local code = love.filesystem.read("stdio.fnl")
local function _1_() local code = love.filesystem.read("stdio.fnl") local luac = luac
if code then if code then
return love.filesystem.newFileData(fennel.compileString(code), "io") luac = love.filesystem.newFileData(fennel.compileString(code), "io") else
else
return love.filesystem.read("lib/stdio.lua") luac = love.filesystem.read("lib/stdio.lua") end
end local thread = love.thread.newThread(luac)
end
local lua = _1_()
local thread = love.thread.newThread(lua)
local io_channel = love.thread.newChannel() 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) thread:start("eval", io_channel)
local function _2_(input)
local ok, val = pcall(fennel.eval, input) local function _4_(input)
local function _3_() return coroutine.resume(coro, (input .. "\n")) end love.handlers.eval = _4_ return nil end return {start = start_repl}
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}

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. 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'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. 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) (add-hook 'fennel-mode-hook 'fennel-mode-hook-fun)
#+END_SRC #+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