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
@spec expand(Logos.Form.t(), MapSet.t(String.t()), Logos.Runtime.t()) :: Logos.Form.t()
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.