Logos.Macroexpand (Logos v0.2.0)

Copy Markdown

The macroexpansion pass: Logos data --[Logos.Macroexpand]--> Logos data, entirely separate from -- and running entirely before -- Logos.Eval.

The real mechanism

A macro is just an ordinary %Logos.Fn{} whose owning Logos.Var has meta[:macro] == true -- there is no separate macro type; a Var's :macro metadata flag is the only thing distinguishing a macro from an ordinary function. macro_lookup/3 resolves the head of a list-headed form the same way Logos.Eval.resolve_symbol_location/2 resolves any other symbol (namespace vars -> refers -> implicit logos.core refer) -- not through the lexical Logos.Env chain (a local binding's value is never consulted; only whether its name shadows a macro, see below). If the Var is macro-flagged, apply_macro/4 calls its function value with the unevaluated rest of the form (via Logos.Eval.apply_macro_fn/4 -- macro args are forms, never evaluated first) and the result is re-expanded from scratch (fixed point: a macro's expansion may itself be another, or the same, macro call).

Lexical-shadow tracking (&env fidelity)

expand/3's second argument is locals :: MapSet.t(String.t()) -- just the set of locally-bound names, with no richer per-binding info (no values, no types) -- rather than a full Logos.Env.t(). Callers that have a real lexical Logos.Env.t() in hand (Logos.eval_string/3, Logos.Stdlib.load!/1) pass Logos.Env.bound_names(env) in; macroexpansion never needs to look up a binding's actual value, only whether a name is bound at all. The walker adds newly-bound names to locals as it descends into fn params and try/catch's bound exception name -- the two real special forms that ever introduce a lexical binding (quote/cond/do/def never do). let is a macro, not a special form; it doesn't need its own case here because by the time the walker's fixed-point recursion re-expands a let call's own expansion, that expansion is built from real fns (see core.logos), so the fn case below picks its bindings up for free, exactly the way let's macro-based design intends.

Before treating a list's head symbol as a possible macro call, macro_lookup/3 checks whether that name is a member of locals -- if so, it's a shadowed local, not a macro reference, and macro lookup is skipped entirely (the form is left as an ordinary call for expand_children/3/Logos.Eval to handle). For example, (let [if (fn [] 1)] (if)) calls the local if, not the if macro, once if has been shadowed.

Implicit &form/&env

apply_macro/4 binds two extra names into the macro function's call environment beyond its declared params (Logos.Eval.apply_macro_fn/4, a small variant of apply_fn/3 that accepts extra bindings): &form (the whole original, unexpanded call form, as data) and &env (the locals set visible at the call site) -- mirroring real Clojure macros' implicit &form/&env. & is already a valid SYMBOL_CHAR (used for the ordinary rest-arg marker &), so &form/&env read as ordinary symbols with no grammar changes.

Summary

Functions

Expands form against locals (the MapSet.t(String.t()) of lexically-bound names visible here, see moduledoc) and runtime (namespace registry -- needed to resolve the head symbol and to actually call the macro function) to a fixed point.

Functions

expand(form, locals, runtime)

Expands form against locals (the MapSet.t(String.t()) of lexically-bound names visible here, see moduledoc) and runtime (namespace registry -- needed to resolve the head symbol and to actually call the macro function) to a fixed point.