~technomancy/fennel-lang.org

281fbc6374a429293638973adc2a6f2b146060db — jaawerth 1 year, 10 months ago 720592c
standalone see/see-worker/repl-worker

Since fengari's require does a synchronous http request, building the
see and repl sources standalone should speed up initialization in
fengari by avoiding the n+1 problem.

* Used antifennel to port see.lua, see-worker.lua, repl-worker.lua to
fennel
* Modified makefile to --compile-as-include when buliding %.lua

TODO: Do actual load speed tests/comparisons to verify this actually
speeds up fengari initialization
7 files changed, 335 insertions(+), 352 deletions(-)

M Makefile
A repl-worker.fnl
D repl-worker.lua
A see-worker.fnl
D see-worker.lua
A see.fnl
D see.lua
M Makefile => Makefile +5 -2
@@ 10,7 10,7 @@ HTML := tutorial.html api.html reference.html lua-primer.html changelog.html \
	setup.html rationale.html from-clojure.html coc.html values.html \
	macros.html security.html style.html events.html

LUA := fennelview.lua
LUA := fennelview.lua see.lua see-worker.lua repl-worker.lua

# This requires pandoc 2.0+
PANDOC ?= pandoc --syntax-definition fennel-syntax.xml \


@@ 26,7 26,7 @@ index.html: main.fnl sample.html fennel/fennel
events.html: events.fnl fennel/fennel
	fennel/fennel events.fnl $(TAGDIRS) > events.html

%.lua: fennel/%.fnl fennel/fennel ; fennel/fennel --compile $< > $@
%.lua: fennel/%.fnl fennel/fennel ; fennel/fennel --compile --require-as-include $< > $@

fennelview.lua: fennel/fennel fennel/src/fennel/view.fnl
	fennel/fennel --compile fennel/src/fennel/view.fnl > $@


@@ 48,6 48,9 @@ rationale.html: fennel/rationale.md ; $(PANDOC) -o $@ $<
%/repl.md: repl.md ; cp $^ $@
%/init.lua: init.lua ; cp $^ $@
%/repl.fnl: repl.fnl ; cp $^ $@
%/see.fnl: see.fnl ; cp $^ $@
%/repl-worker.fnl: repl-worker.fnl; cp $^ $@
%/see-worker.fnl: see-worker.fnl; cp $^ $@

v%/fennel:
	git clone --branch $* fennel $@

A repl-worker.fnl => repl-worker.fnl +67 -0
@@ 0,0 1,67 @@
(set package.path :./?.lua)

(local js (require :js))

(local _G _G)

(local pack table.pack)

(local tostring tostring)

(set _G.os.exit (fn []))

(set _G.os.getenv (fn []
                    nil))

(global io {:open (fn [filename]
                    {:read (fn [_ all]
                             (assert (= all :*all) "Can only read *all.")
                             (local xhr (js.new js.global.XMLHttpRequest))
                             (xhr:open :GET filename false)
                             (xhr:send)
                             (assert (= xhr.status 200)
                                     (.. xhr.status ": " xhr.statusText))
                             (tostring xhr.response))
                     :close (fn [])})})

(set-forcibly! post-content
               (fn [function-name lines]
                 (for [i 1 lines.n]
                   (js.global:postMessage (.. function-name :|append|
                                              (tostring (. lines i)))))
                 (js.global:postMessage (.. function-name :|dispatch|))))

(set _G.printLuacode
     (fn [...]
       (post-content :printLuacode (pack ...))))

(set _G.print (fn [...]
                (post-content :print (pack ...))))

(set _G.narrate (fn [...]
                  (post-content :narrate (pack ...))))

(set _G.printError (fn [...]
                     (post-content :printError (pack ...))))

(local fennel (require :fennel/fennel))

(set package.loaded.fennel fennel)

(global repl (coroutine.create (require :repl)))

(local welcome (.. "Welcome to Fennel " fennel.version ", running on Fengari ("
                   _VERSION ")"))

(_G.print welcome)

(_G.printLuacode "Compiled Lua code")

(js.global:postMessage (.. :loaded|loaded| welcome))

(assert (coroutine.resume repl))

(set js.global.onmessage
     (fn [_ event]
       (coroutine.resume repl event.data)))


D repl-worker.lua => repl-worker.lua +0 -68
@@ 1,68 0,0 @@
package.path = "./?.lua"
local js = require "js"

local _G = _G
local pack = table.pack
local tostring = tostring

-- just make a few things not blow up
_G.os.exit = function() end
_G.os.getenv = function() return nil end

-- require-macros depends on io.open; we splice in a hacky replacement
io={open=function(filename)
    return {
        read = function(_, all)
            assert(all=="*all", "Can only read *all.")
            local xhr = js.new(js.global.XMLHttpRequest)
            xhr:open("GET", filename, false)
            xhr:send()
            assert(xhr.status == 200, xhr.status .. ": " .. xhr.statusText)
            return tostring(xhr.response)
        end,
        close = function() end,
    }
end}

function postContent(functionName, lines)
    for i = 1, lines.n do
        js.global:postMessage(functionName .. "|append|" .. tostring(lines[i]))
    end
    js.global:postMessage(functionName .. "|dispatch|")
end

_G.printLuacode = function(...)
    postContent("printLuacode", pack(...))
end

_G.print = function(...)
    postContent("print", pack(...))
end

_G.narrate = function(...)
    postContent("narrate", pack(...))
end

_G.printError = function(...)
    postContent("printError", pack(...))
end

local fennel = require("fennel/fennel")
package.loaded.fennel = fennel

repl = coroutine.create(fennel.dofile("repl.fnl"))

local welcome = "Welcome to Fennel " .. fennel.version
    .. ", running on Fengari (" .. _VERSION .. ")"

_G.print(welcome)

_G.printLuacode("Compiled Lua code")

js.global:postMessage("loaded|loaded|" .. welcome)

assert(coroutine.resume(repl))

js.global.onmessage = function(_, event)
    coroutine.resume(repl, event.data)
end

A see-worker.fnl => see-worker.fnl +28 -0
@@ 0,0 1,28 @@
(set package.loaded.ffi {:typeof (fn [])})

(global os {:getenv (fn [])})

(global io {:open (fn [])})

(global bit {:rshift (partial rshift)
             :band (partial band)})

(global unpack table.unpack)

(local antifennel (dofile :antifennel.lua))

(local fennel (require :fennel))

(local js (require :js))

(set js.global.onmessage
     (fn [_ e]
       (let [is-fennel (e.data:match "^ ")
             compiler (or (and is-fennel fennel.compileString) antifennel)
             (ok result) (pcall compiler e.data)]
         (if (not ok) (js.global:postMessage (.. result "\n")) is-fennel
             (js.global:postMessage (.. result " "))
             (js.global:postMessage (.. result "\t"))))))

(js.global:postMessage (.. "Loaded Fennel " fennel.version " in " _VERSION))


D see-worker.lua => see-worker.lua +0 -25
@@ 1,25 0,0 @@
package.loaded.ffi = {typeof=function() end}
os = {getenv=function() end}
io = {open=function() end}
bit = {band = function(a,b) return a & b end,
       rshift=function(a,b) return a >> b end}
unpack = table.unpack

local antifennel = dofile("antifennel.lua")
local fennel = require("fennel")
local js = require("js")

js.global.onmessage = function(_, e)
   local isFennel = e.data:match("^ ")
   local compiler = isFennel and fennel.compileString or antifennel
   local ok, result = pcall(compiler, e.data)
   if not ok then
      js.global:postMessage(result .. "\n")
   elseif isFennel then
      js.global:postMessage(result .. " ")
   else
      js.global:postMessage(result .. "\t")
   end
end

js.global:postMessage("Loaded Fennel " .. fennel.version .. " in " .. _VERSION)

A see.fnl => see.fnl +235 -0
@@ 0,0 1,235 @@
(set package.path :./?.lua)

(local js (require :js))

;; minimal shims just to allow the compilers to load in Fengari
(set package.loaded.ffi {:typeof (fn [])})
(global os {:getenv (fn [])})
(global io {:open (fn [])})
(global bit {:rshift #(rshift $1 $2)
             :band #(band $1 $2)})
(global unpack table.unpack)
(global print (partial js.global.console:log))

(local document js.global.document)

(local compile-fennel (document:getElementById :compile-fennel))
(local compile-lua (document:getElementById :compile-lua))
(local out (document:getElementById :out))
(local fennel-source (document:getElementById :fennel-source))
(local lua-source (document:getElementById :lua-source))

(fn status [msg success]
  (set out.innerHTML msg)
  (set out.style.color (or (and success :black) "#dd1111")))

(set fennel-source.onkeydown (fn [_ e]
                               (when (not e)
                                 (set-forcibly! e js.global.event))
                               (when (and (= (or e.key e.which) :Enter)
                                          e.ctrlKey)
                                 (compile-fennel.onclick)
                                 false)))

(set lua-source.onkeydown (fn [_ e]
                            (when (not e)
                              (set-forcibly! e js.global.event))
                            (when (and (= (or e.key e.which) :Enter) e.ctrlKey)
                              (compile-lua.onclick)
                              false)))

(local anti-msg
       (.. "Compiled Lua to Fennel.\n\n"
           "Note that compiling Lua to Fennel can result in some"
           " strange-looking code when\nusing constructs that Fennel"
           " does not support natively, like early returns."))

(fn init-worker [auto-click]
  (let [worker (js.new js.global.Worker :/see-worker.js)]
    (fn send [is-fennel code]
      (let [prefix (or (and is-fennel " ") "\t")]
        (worker:postMessage (.. prefix code))))

    (set worker.onmessage (fn [_ e]
                            (set out.innerHTML e.data)
                            (set compile-fennel.onclick
                                 (fn []
                                   (send true fennel-source.value)))
                            (set compile-lua.onclick
                                 (fn []
                                   (send false lua-source.value)))
                            (set worker.onmessage
                                 (fn [_ event]
                                   (if (event.data:match " $")
                                       (do
                                         (set lua-source.value event.data)
                                         (status "Compiled Fennel to Lua." true))
                                       (event.data:match "\t$")
                                       (do
                                         (set fennel-source.value event.data)
                                         (status anti-msg true))
                                       (status event.data false))))
                            (when (and auto-click auto-click.onclick)
                              (auto-click.onclick))))))

(fn load-direct [auto-click]
  (let [antifennel (dofile :antifennel.lua)
        fennel (require :fennel)]
    (set compile-fennel.onclick
         (fn []
           (let [(ok code) (pcall fennel.compileString fennel-source.value)]
             (if ok
                 (do
                   (set lua-source.value code)
                   (status "Compiled Fennel to Lua." true))
                 (status (tostring code) false)))))
    (set compile-lua.onclick
         (fn []
           (let [(ok msg) (load lua-source.value)]
             (when (not ok)
               (let [___antifnl_rtns_1___ [(status (.. "Lua: " msg) false)]]
                 (lua "return (table.unpack or _G.unpack)(___antifnl_rtns_1___)")))
             (local (ok code) (pcall antifennel lua-source.value))
             (if ok (do
                      (set fennel-source.value code)
                      (status anti-msg true))
                 (status (tostring code) false)))))
    (set out.innerHTML (.. "Loaded Fennel " fennel.version " in " _VERSION))
    (when (and auto-click auto-click.onclick)
      (auto-click.onclick))))

(var started false)

(fn init [auto-click]
  (when started
    (lua "return "))
  (set started true)
  (set out.innerHTML :Loading...)
  (if js.global.Worker (init-worker auto-click) js.global.setTimeout
      (js.global:setTimeout (fn []
                              (load-direct auto-click)))
      (load-direct auto-click)))

(set compile-fennel.onclick init)

(set compile-lua.onclick init)

(set fennel-source.onfocus init)

(set lua-source.onfocus init)

(local fennel-samples {:walk "(fn walk-tree [root f custom-iterator]
  (fn walk [iterfn parent idx node]
    (when (f idx node parent)
      (each [k v (iterfn node)]
        (walk iterfn node k v))))
  (walk (or custom-iterator pairs) nil nil root)
  root)"
                       :fibonacci "(fn fibonacci [n]
 (if (< n 2)
  n
  (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))

(print (fibonacci 10))"
                       "pong movement" ";; Read the keyboard, move player accordingly
(local dirs {:up [0 -1] :down [0 1]
            :left [-1 0] :right [1 0]})

(each [key delta (pairs dirs)]
  (when (love.keyboard.isDown key)
    (let [[dx dy] delta
          [px py] player
          x (+ px (* dx player.speed dt))
          y (+ py (* dy player.speed dt))]
      (world:move player x y))))"})

(local lua-samples {"sample select" "local sample_lua = document:getElementById(\"samples\")
local lua_samples = {}

for name, sample in pairs(lua_samples) do
   local option = document:createElement(\"option\")
   option.innerHTML = name
   sample_lua:appendChild(option)
end

function sample_lua.onchange(self, e)
   init()
   local code = lua_samples[self.value]
   if code then lua_source.value = code end
end"
                    :love.run "function love.run()
   love.load()
   while true do
      love.event.pump()
      local needs_refresh = false
      for name, a,b,c,d,e,f in love.event.poll() do
         if(type(love[name]) == \"function\") then
            love[name](a,b,c,d,e,f)
            needs_refresh = true
         elseif(name == \"quit\") then
            os.exit()
         end
      end
      for _,c in pairs(internal.coroutines) do
         local ok, val = coroutine.resume(c)
         if(ok and val) then needs_refresh = true
         elseif(not ok) then print(val) end
      end
      for i,c in lume.ripairs(internal.coroutines) do
         if(coroutine.status(c) == \"dead\") then
            table.remove(internal.coroutines, i)
         end
      end
      if(needs_refresh) then refresh() end
      love.timer.sleep(0.05)
   end
end"
                    :antifennel "local function uncamelize(name)
   local function splicedash(pre, cap)
     return pre .. \"-\" .. cap:lower()
   end
   return name:gsub(\"([a-z0-9])([A-Z])\", splicedash)
end

local function mangle(name, field)
   if not field and reservedFennel[name] then
     name = \"___\" .. name .. \"___\"
   end
   return field and name or
      uncamelize(name):gsub(\"([a-z0-9])_\", \"%1-\")
end

local function compile(rdr, filename)
   local ls = lex_setup(rdr, filename)
   local ast_builder = lua_ast.New(mangle)
   local ast_tree = parse(ast_builder, ls)
   return letter(compiler(nil, ast_tree))
end"})

(fn init-samples [id samples target]
  (let [select (document:getElementById id)]
    (each [name sample (pairs samples)]
      (local option (document:createElement :option))
      (set option.innerHTML name)
      (select:appendChild option))
    (set select.onchange (fn [self e]
                           (init)
                           (local code (. samples self.value))
                           (when code
                             (set target.value code))))))

(init-samples :sample-fennel fennel-samples fennel-source)

(init-samples :sample-lua lua-samples lua-source)

(when js.global.URLSearchParams
  (local params (js.new js.global.URLSearchParams document.location.search))
  (local fennel-param (params:get :fennel))
  (local lua-param (params:get :lua))
  (if (not= (tostring fennel-param) :null)
      (do
        (set fennel-source.value fennel-param)
        (init compile-fennel)) (not= (tostring lua-param) :null)
      (do
        (set lua-source.value lua-param)
        (init compile-lua))))

D see.lua => see.lua +0 -257
@@ 1,257 0,0 @@
package.path = "./?.lua"
local js = require("js")

-- minimal shims just to allow the compilers to load in Fengari
package.loaded.ffi = {typeof=function() end}
os = {getenv=function() end}
io = {open=function() end}
bit = {band = function(a,b) return a & b end,
       rshift=function(a,b) return a >> b end}
unpack = table.unpack

function print(...) js.global.console:log(...) end

local document = js.global.document
local compile_fennel = document:getElementById("compile-fennel")
local compile_lua = document:getElementById("compile-lua")
local out = document:getElementById("out")

local fennel_source = document:getElementById("fennel-source")
local lua_source = document:getElementById("lua-source")

local status = function(msg, success)
   out.innerHTML = msg
   out.style.color = success and "black" or "#dd1111"
end

-- Ctrl-enter to compile
fennel_source.onkeydown = function(_, e)
   if not e then e = js.global.event end
   if (e.key or e.which) == "Enter" and e.ctrlKey then
      compile_fennel.onclick()
      return false
   end
end

lua_source.onkeydown = function(_,e)
   if not e then e = js.global.event end
   if (e.key or e.which) == "Enter" and e.ctrlKey then
      compile_lua.onclick()
      return false
   end
end

local anti_msg = "Compiled Lua to Fennel.\n\n"..
   "Note that compiling Lua to Fennel can result in some" ..
   " strange-looking code when\nusing constructs that Fennel" ..
   " does not support natively, like early returns."

local init_worker = function(auto_click)
   -- TODO: multiple Fennel versions?
   local worker = js.new(js.global.Worker, "/see-worker.js")
   local send = function(isFennel, code)
      -- we can't send tables to workers, so we have to encode everything in
      -- strings. use an initial space for Fennel and initial tab for Lua code.
      local prefix = isFennel and " " or "\t"
      worker:postMessage(prefix .. code)
   end

   worker.onmessage = function(_, e)
      out.innerHTML = e.data -- loaded message
      -- don't set up handlers until we've loaded
      compile_fennel.onclick = function() send(true, fennel_source.value) end
      compile_lua.onclick = function() send(false, lua_source.value) end

      worker.onmessage = function(_, event)
         -- because we can't send tables as events, we encode the type of the
         -- message in the last character of the string.
         if event.data:match(" $") then
            lua_source.value = event.data
            status("Compiled Fennel to Lua.", true)
         elseif event.data:match("\t$") then
            fennel_source.value = event.data
            status(anti_msg, true)
         else
            status(event.data, false)
         end
      end
      if auto_click and auto_click.onclick then auto_click.onclick() end
   end
end

local load_direct = function(auto_click)
   local antifennel = dofile("antifennel.lua")
   local fennel = require("fennel")
   compile_fennel.onclick = function()
      local ok, code = pcall(fennel.compileString, fennel_source.value)
      if ok then
         lua_source.value = code
         status("Compiled Fennel to Lua.", true)
      else
         status(tostring(code), false)
      end
   end

   compile_lua.onclick = function()
      -- for Lua that doesn't parse, antifennel gives crummy error messages
      local ok, msg = load(lua_source.value)
      if not ok then return status("Lua: " .. msg, false) end

      local ok, code = pcall(antifennel, lua_source.value)
      if ok then
         fennel_source.value = code
         status(anti_msg, true)
      else
         status(tostring(code), false)
      end
   end

   out.innerHTML = "Loaded Fennel " .. fennel.version .. " in " .. _VERSION
   if auto_click and auto_click.onclick then auto_click.onclick() end
end

local started = false

local init = function(auto_click)
   if started then return end
   started = true
   out.innerHTML = "Loading..."

   if js.global.Worker then
      init_worker(auto_click)
   elseif js.global.setTimeout then
      js.global:setTimeout(function() load_direct(auto_click) end)
   else
      return load_direct(auto_click)
   end
end

compile_fennel.onclick = init
compile_lua.onclick = init
fennel_source.onfocus = init
lua_source.onfocus = init

--- Sample snippets

local fennel_samples = {["pong movement"]=
      ";; Read the keyboard, move player accordingly\
(local dirs {:up [0 -1] :down [0 1]\
            :left [-1 0] :right [1 0]})\
\
(each [key delta (pairs dirs)]\
  (when (love.keyboard.isDown key)\
    (let [[dx dy] delta\
          [px py] player\
          x (+ px (* dx player.speed dt))\
          y (+ py (* dy player.speed dt))]\
      (world:move player x y))))",
   fibonacci="(fn fibonacci [n]\
 (if (< n 2)\
  n\
  (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))\
\
(print (fibonacci 10))",
   walk="(fn walk-tree [root f custom-iterator]\
  (fn walk [iterfn parent idx node]\
    (when (f idx node parent)\
      (each [k v (iterfn node)]\
        (walk iterfn node k v))))\
  (walk (or custom-iterator pairs) nil nil root)\
  root)"}

local lua_samples = {["sample select"]=
   "local sample_lua = document:getElementById(\"samples\")\
local lua_samples = {}\
\
for name, sample in pairs(lua_samples) do\
   local option = document:createElement(\"option\")\
   option.innerHTML = name\
   sample_lua:appendChild(option)\
end\
\
function sample_lua.onchange(self, e)\
   init()\
   local code = lua_samples[self.value]\
   if code then lua_source.value = code end\
end",
   antifennel=
   "local function uncamelize(name)\
   local function splicedash(pre, cap)\
     return pre .. \"-\" .. cap:lower()\
   end\
   return name:gsub(\"([a-z0-9])([A-Z])\", splicedash)\
end\
\
local function mangle(name, field)\
   if not field and reservedFennel[name] then\
     name = \"___\" .. name .. \"___\"\
   end\
   return field and name or\
      uncamelize(name):gsub(\"([a-z0-9])_\", \"%1-\")\
end\
\
local function compile(rdr, filename)\
   local ls = lex_setup(rdr, filename)\
   local ast_builder = lua_ast.New(mangle)\
   local ast_tree = parse(ast_builder, ls)\
   return letter(compiler(nil, ast_tree))\
end",
   ["love.run"]="function love.run()\
   love.load()\
   while true do\
      love.event.pump()\
      local needs_refresh = false\
      for name, a,b,c,d,e,f in love.event.poll() do\
         if(type(love[name]) == \"function\") then\
            love[name](a,b,c,d,e,f)\
            needs_refresh = true\
         elseif(name == \"quit\") then\
            os.exit()\
         end\
      end\
      for _,c in pairs(internal.coroutines) do\
         local ok, val = coroutine.resume(c)\
         if(ok and val) then needs_refresh = true\
         elseif(not ok) then print(val) end\
      end\
      for i,c in lume.ripairs(internal.coroutines) do\
         if(coroutine.status(c) == \"dead\") then\
            table.remove(internal.coroutines, i)\
         end\
      end\
      if(needs_refresh) then refresh() end\
      love.timer.sleep(0.05)\
   end\
end"}

local init_samples = function(id, samples, target)
   local select = document:getElementById(id)
   for name, sample in pairs(samples) do
      local option = document:createElement("option")
      option.innerHTML = name
      select:appendChild(option)
   end

   select.onchange = function(self, e)
      init()
      local code = samples[self.value]
      if code then target.value = code end
   end
end

init_samples("sample-fennel", fennel_samples, fennel_source)
init_samples("sample-lua", lua_samples, lua_source)

if js.global.URLSearchParams then
   local params = js.new(js.global.URLSearchParams, document.location.search)
   local fennel_param = params:get("fennel")
   local lua_param = params:get("lua")

   if tostring(fennel_param) ~= "null" then
      fennel_source.value = fennel_param
      init(compile_fennel)
   elseif tostring(lua_param) ~= "null" then
      lua_source.value = lua_param
      init(compile_lua)
   end
end