I've updated Fennel, and one of my projects broke in a very bizarre way. Some functions become empty. After a bit of bisecting, and debugging, I've found that this commit is where it breaks: d064522bac302c967947c7f648b4468ceb493895
Here's the minimal macro defn
which re-arranges docstring and argument list (this was the reason why my project wasn't compiling properly):
(fn defn [name doc? args ...]
(let [doc (if (= :string (type doc?)) doc?)
args* (if doc args doc?)
body (if doc `(do ,...) `(do ,args ,...))]
`(local ,name (fn ,name ,args* ,doc ,body))))
{: defn}
Here, when I import it, the first function foo
compiles as an empty function, and the bar
function compiles correctly:
(import-macros {: defn} :m)
(defn foo [bar] ; compiles to empty function
bar)
;; local foo
;; local function foo0(...)
;; end
;; foo = foo0
(defn bar "doc" [baz] ; works
baz)
;; local bar
;; local function bar0(...)
;; return baz
;; end
;; bar = bar0
However, macrodebugging these definitions in the REPL shows that the code generated is correct:
>> (macrodebug (defn foo [bar] bar))
(local foo (fn foo [bar] nil (do bar)))
>> (macrodebug (defn bar "doc" [baz] baz))
(local bar (fn bar [baz] "doc" (do baz)))
And even more interestingly, if I use macro
instead of importing the defn
macro, it compiles just fine:
;; (import-macros {: defn} :m)
(macro defn [name doc? args ...]
(let [doc (if (= :string (type doc?)) doc?)
args* (if doc args doc?)
body (if doc `(do ,...) `(do ,args ,...))]
`(local ,name (fn ,name ,args* ,doc ,body))))
(defn foo [bar]
bar)
;; local foo
;; local function foo0(bar)
;; return bar
;; end
;; foo = foo0
;;
(defn bar "doc" [baz]
baz)
;; local bar
;; local function bar0(baz)
;; return baz
;; end
;; bar = bar0
These macros are exactly the same, the only difference is how they're brought into the scope.
The root cause of this problem is that
(length ast)
behaves unpredictably on sparse tables.I think it's a good idea when writing macros to avoid sparse tables. However, we could check for these immediately following macroexpansion and replace the nils in any list with
(utils.sym :nil)
and that should solve the problem, since it is easy for sparse tables to sneak in by accident.