~technomancy/antifennel

8a49efb0c13871c22c7403bba1ad0035148e1fcb — Phil Hagelberg 30 days ago 9dfc5a3
Emit let instead of do+local or fn+local.
7 files changed, 101 insertions(+), 7 deletions(-)

M Makefile
M README.md
M antifennel.lua
M antifennel_expected.fnl
A letter.fnl
M test.lua
M test_expected.fnl
M Makefile => Makefile +1 -1
@@ 12,7 12,7 @@ PARSER_FENNEL=lang/reader.fnl \
		lang/lexer.fnl \
		lang/parser.fnl

antifennel: antifennel.fnl anticompiler.fnl $(PARSER_FENNEL)
antifennel: antifennel.fnl anticompiler.fnl letter.fnl $(PARSER_FENNEL)
	echo "#!/usr/bin/env luajit" > $@
	luajit fennel --require-as-include --compile $< >> $@
	chmod 755 $@

M README.md => README.md +2 -4
@@ 26,8 26,6 @@ The Antifennel compiler assumes its input file is valid Lua; it does
not attempt to give good error messages when provided with files that
won't parse.

Antifennel will always use `local` even in cases where `let` would be better.

Antifennel will not emit variadic operators.

Fennel code does not support `goto`, so neither does Antifennel.


@@ 35,8 33,8 @@ Fennel code does not support `goto`, so neither does Antifennel.
Early returns will compile to very ugly Fennel code, but they should
be correct.

Multiple values can be set, but only if the keys are static. For
instance, this is OK:
Multiple value assignment doesn't work if setting table keys that
aren't static. For instance, this is OK:

    tbl.field1.q, x = "QUEUE", 13


M antifennel.lua => antifennel.lua +2 -1
@@ 13,6 13,7 @@ local lua_ast = require('lang.lua_ast')
local reader = require('lang.reader')

local compiler = require('anticompiler')
local letter = require("letter")
local fnlfmt = require("fnlfmt")

local reservedFennel = {['doc']=true, ['lua']=true, ['hashfn']=true,


@@ 36,7 37,7 @@ 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 compiler(nil, ast_tree)
   return letter(compiler(nil, ast_tree))
end

local filename = arg[1]

M antifennel_expected.fnl => antifennel_expected.fnl +3 -1
@@ 16,6 16,8 @@

(local compiler (require "anticompiler"))

(local letter (require "letter"))

(local fnlfmt (require "fnlfmt"))

(local reserved-fennel {:band true


@@ 59,7 61,7 @@
  (local ls (lex-setup rdr filename))
  (local ast-builder (lua-ast.New mangle))
  (local ast-tree (parse ast-builder ls))
  (compiler nil ast-tree))
  (letter (compiler nil ast-tree)))

(local filename (. arg 1))


A letter.fnl => letter.fnl +68 -0
@@ 0,0 1,68 @@
;; Transform all do+local calls into let.
;; Normally we would do this within the anticompiler, but because of how we
;; do assignment tracking, we don't know if a local call could be replaced later
;; on with a var once we realize it's a mutable local. Because of that problem,
;; it's easier to do it in a second pass.

(local fennel (require :fennel))

(fn walk-tree [root f custom-iterator]
  "Walks a tree (like the AST), invoking f(node, idx, parent) on each node.
When f returns a truthy value, recursively walks the children."
  (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)

(fn locals-to-bindings [node bindings]
  (let [maybe-local (. node 3)]
    (when (and (= :table (type maybe-local))
               (= :local (tostring (. maybe-local 1))))
      (table.remove node 3)
      (table.insert bindings (. maybe-local 2))
      (table.insert bindings (. maybe-local 3))
      (locals-to-bindings node bindings))))

(fn move-body [fn-node do-node do-loc]
  (for [i (# fn-node) do-loc -1]
    (table.insert do-node 2 (table.remove fn-node i))))

(fn transform [node]
  (when (= :do (tostring (. node 1)))
    (let [bindings []]
      (table.insert node 2 bindings)
      (tset node 1 (fennel.sym :let))
      (locals-to-bindings node bindings)))
  (when (= :fn (tostring (. node 1)))
    (let [has-name? (fennel.sym? (. node 2))
          do-loc (if has-name? 4 3)
          do-node (fennel.list (fennel.sym :do))]
      (move-body node do-node do-loc)
      (table.insert node do-loc do-node))))

(fn do-local-node? [node]
  (and (= :table (type node)) (= :do (tostring (. node 1)))
       (= :table (type (. node 2))) (= :local (tostring (. node 2 1)))))

(fn fn-local-node? [node]
  (and (= :table (type node)) (= :fn (tostring (. node 1)))
       (or (and (= :table (type (. node 3))) (= :local (tostring (. node 3 1))))
           (and (= :table (type (. node 4))) (= :local (tostring (. node 4 1)))))))

(fn letter [idx node]
  (when (or (do-local-node? node) (fn-local-node? node))
    (transform node))
  (= :table (type node)))

(fn reverse-ipairs [t] ;; based on lume.ripairs
  (fn iter [t i]
    (let [i (- i 1)
          v (. t i)]
      (if (not= v nil)
          (values i v))))
  (values iter t (+ (# t) 1)))

(fn compile [ast]
  (walk-tree ast letter reverse-ipairs))

M test.lua => test.lua +14 -0
@@ 23,4 23,18 @@ end

f = 59

do
   local boo, twenty = "hoo", 20
   local fifteen = 15
   print(boo, twenty+fifteen)
end

local letter = function()
   local x = 19
   local y = 20
   return x+y
end

print((function() local x = 1 return x end)())

return (attributes(path) or {}).mode

M test_expected.fnl => test_expected.fnl +11 -0
@@ 27,5 27,16 @@

(set-forcibly! f 59)

(let [(boo twenty) (values "hoo" 20) fifteen 15]
  (print boo (+ twenty fifteen)))

(fn letter []
  (let [x 19 y 20]
    (+ x y)))

(print ((fn []
          (let [x 1]
            x))))

(. (or (attributes path) []) "mode")