Implement macro expander. v1 PROPOSED

Phil Hagelberg: 1
 Implement macro expander.

 1 files changed, 28 insertions(+), 31 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~technomancy/fennel/patches/9932/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH] Implement macro expander. Export this patch

This implements a macro expander which is exposed in the compiler
scope by removing the macro->special transformation and storing all
the macros in a new table on the scope table.

One gotcha is that in order to use it, you must print or fennelview
the result, otherwise you get a strange error.

    (eval-compiler (macroexpand '(when x (print x))))
    Compile error: ./fennel.lua:923: invalid value (table) at index 3 in table for 'concat'

    (eval-compiler (print (macroexpand '(when x (print x)))))
    (if x (do (print x)))

It would probably be good to make it clearer what you need to do
there. Note that printing the return value only works if there are no
table literals in the source; those will use the ugly built-in
tostring if you don't use fennelview.

    (eval-compiler (print (macroexpand '(let [x 1] (when x (print x))))))
    (let table: 0x562a78a911b0 (when x (print x)))

This feels really off, but I'm not sure what to do about it. One
option would be for eval-compiler to print+fennelview its return value
if fennelview has been loaded.

 fennel.lua | 59 ++++++++++++++++++++++++++++-------------------------------
 1 file changed, 28 insertions(+), 31 deletions(-)

diff --git a/fennel.lua b/fennel.lua
index 6663866..72f1ddd 100644
--- a/fennel.lua
+++ b/fennel.lua
@@ -491,6 +491,9 @@ local function makeScope(parent)
        specials = setmetatable({}, {
            __index = parent and parent.specials
        macros = setmetatable({}, {
            __index = parent and parent.macros
        symmeta = setmetatable({}, {
            __index = parent and parent.symmeta
@@ -709,7 +712,7 @@ end
local function checkBindingValid(symbol, scope, ast)
    -- Check if symbol will be over shadowed by special
    local name = symbol[1]
    assertCompile(not scope.specials[name],
    assertCompile(not scope.specials[name] and not scope.macros[name],
    ("symbol %s may be overshadowed by a special form or macro"):format(name), ast)
    assertCompile(not isQuoted(symbol), 'macro tried to bind ' .. name ..
                      " without gensym; try ' .. name .. '# instead", ast)
@@ -977,6 +980,21 @@ local function handleCompileOpts(exprs, parent, opts, ast)
    return exprs

-- Save current macro scope; used by gensym, in-scope?, etc
local macroCurrentScope = GLOBAL_SCOPE

local function macroexpand(ast, scope)
    local first = isSym(ast[1])
    local macro = first and scope.macros[deref(first)]
    if not macro then return ast end
    local oldScope = macroCurrentScope
    macroCurrentScope = scope
    local ok, transformed = pcall(macro, unpack(ast, 2))
    macroCurrentScope = oldScope
    assertCompile(ok, transformed, ast)
    return transformed

-- Compile an AST expression in the scope into parent, a tree
-- of lines that is eventually compiled into Lua code. Also
-- returns some information about the evaluation of the compiled expression,
@@ -1014,6 +1032,7 @@ local function compile1(ast, scope, parent, opts)
        -- Function call or special form
        local len = #ast
        assertCompile(len > 0, "expected a function to call", ast)
        ast = macroexpand(ast, scope)
        -- Test for special form
        local first = ast[1]
        if isSym(first) then -- Resolve symbol
@@ -1505,9 +1524,9 @@ SPECIALS['doc'] = function(ast, scope, parent)
    assertCompile(#ast == 2, "expected one argument", ast)

    local target = deref(ast[2])
    local special = scope.specials[target]
    if special then
        return ("print([[%s]])"):format(doc(special, target))
    local specialOrMacro = scope.specials[target] or scope.macros[target]
    if specialOrMacro then
        return ("print([[%s]])"):format(doc(specialOrMacro, target))
        local value = tostring(compile1(ast[2], scope, parent, {nval = 1})[1])
        -- need to require here since the metadata is stored in the module
@@ -2016,27 +2035,6 @@ defineUnarySpecial("length", "#")
docSpecial("length", {"x"}, "Returns the length of a table or string.")
SPECIALS["#"] = SPECIALS["length"]

--- Save current macro scope
local macroCurrentScope = GLOBAL_SCOPE

--- Covert a macro function to a special form
local function macroToSpecial(mac)
    local special = function(ast, scope, parent, opts)
        local oldScope = macroCurrentScope
        macroCurrentScope = scope
        local ok, transformed = pcall(mac, unpack(ast, 2))
        macroCurrentScope = oldScope
        assertCompile(ok, transformed, ast)
        local result = compile1(transformed, scope, parent, opts)
        return result
    if metadata[mac] then
        -- copy metadata from original function to special form function
        metadata[mac], metadata[special] = nil, metadata[mac]
    return special

local requireSpecial
local function compile(ast, options)
    local opts = copy(options)
@@ -2533,7 +2531,8 @@ local function makeCompilerEnv(ast, scope, parent)
        ["get-scope"] = function() return macroCurrentScope end,
        ["in-scope?"] = function(symbol)
            return macroCurrentScope.manglings[tostring(symbol)]
        ["macroexpand"] = function(ast) return macroexpand(ast, scope) end
    }, { __index = _ENV or _G })

@@ -2545,7 +2544,7 @@ end

local function addMacros(macros, ast, scope)
    assertCompile(isTable(macros), 'expected macros to be table', ast)
    kvmap(macros, function(k, v) return k, macroToSpecial(v) end, scope.specials)
    for k,v in pairs(macros) do scope.macros[k] = v end

local function loadMacros(modname, ast, scope, parent)
@@ -2906,11 +2905,9 @@ do
                            useMetadata = true,
                            filename = "built-ins",
                            moduleName = moduleName })
          function(name, fn) return name, macroToSpecial(fn, name) end,
    for k,v in pairs(macros) do GLOBAL_SCOPE.macros[k] = v end
    package.preload[moduleName] = nil
SPECIALS['λ'] = SPECIALS['lambda']
GLOBAL_SCOPE.macros['λ'] = GLOBAL_SCOPE.macros['lambda']

return module