bakpakin / Fennel

Lua Lisp Language
https://fennel-lang.org
MIT License
2.45k stars 126 forks source link

How to compile a form into a string in a macro at compile time #392

Closed 6cdh closed 3 years ago

6cdh commented 3 years ago

For example, this macro

(macro form->string [form]
  `(let [fennel# (require :fennel)]
     (fennel#.compileString ,(view form))))

can compile a form at runtime:

(form->string (print 1))
;; =>
;; local fennel_2_auto = require("fennel")
;; return fennel_2_auto.compileString("(print 1)")

I expect it can be compiled at compile time, so it can yield this lua code:

return "return print(1)"

I've tried

(macro form->string [form]
  (let [fennel# (require :fennel)]
    `,(fennel#.compileString (view form))))

It can't work since field 'compileString' is not callable (a nil value).

Is there a way to do it?

technomancy commented 3 years ago

In order to do this you have to disable the compiler sandbox. Otherwise the sandbox would be useless and simply by requiring fennel you could escape it.

With the sandbox disabled, you can use this:

(macro form->string [form]
  (let [fennel (require :fennel)]
    (fennel.compileString (fennel.view form))))

(print (form->string (let [a (require :abc)]
                       (print (+ a 3)))))

$ fennel --no-compiler-sandbox scratch.fnl
local a = require("abc")
return print((a + 3))

(Note that since you are returning a string there is no need for quoting, unquoting, or autogensym.)

Normally the original code would fail with a better error message indicating that the compiler sandbox is to blame, but the fennel module itself is special because it's needed for compiling code that uses metadata, so we have to fake it out with a special replacement module. Perhaps we could add a __index metamethod on that fake module to make it clearer what is really happening.

6cdh commented 3 years ago

Thanks a lot for correcting my code, it works.

Normally the original code would fail with a better error message indicating that the compiler sandbox is to blame

I can't understand. Use (os.date) in macro:

(macro m []
  (os.date))
(m)

still lead to an obscure error rather than pointing to sandbox (with the latest fennel until now)

[string "local function _1_(x)..."]:3: attempt to index a nil value (global 'os')

And sure, we need a better error message.

andreyorst commented 3 years ago

Perhaps we could add a __index metamethod on that fake module to make it clearer what is really happening.

I second this solution, it's often very ambigous seing an error of accessing nil, and not being sure why, and then realizing that you're at compile time.

technomancy commented 3 years ago

Turns out this is a bug in macros where we apply strict globals checking based on runtime globals, not the compiler sandbox.

Fixed in 4f23734.