When one macro call is the argument of another, does the outer expansion see the inner call already expanded?
answer
- nesting has to resolve somehow
- enclosing call first, or arguments first
- the outer may inspect its argument's shape
- a dropped argument may never expand
- output usually re-scanned to a fixed point
basics
~20 sIt depends on the expander's order. Expanding the enclosing call first hands the outer macro the inner call as written, so it can inspect or discard it. Expanding arguments first hands it finished code instead.
solid answer
~40 sBoth calls get rewritten; the order is a property of the expander, not of the macros. Under **outermost-first**, the enclosing call is rewritten first and the argument travels as written -- the outer macro sees a call, may inspect its shape, may move it, may drop it entirely, in which case the inner one is never expanded at all. Under **innermost-first**, arguments are rewritten before the enclosing call is considered, so the outer macro is handed finished code and can no longer tell what produced it. The difference is observable whenever a macro branches on the shape of its argument, or discards it: the same call site can yield two different programs. Most expanders also re-scan their own output until a pass changes nothing, which is what lets one macro build on another.
code
pseudocode · 14 linesmacro five():
return quote( 5 )
macro dispatch(e):
if e is a number literal:
return quote( fast_path(splice(e)) )
else:
return quote( slow_path(splice(e)) )
// call site: dispatch( five() )
//
// arguments first: dispatch sees the literal 5 -> fast_path(5)
// enclosing call first: dispatch sees a call -> slow_path(five())
// and five() is rewritten on a later passgo deeper
Know that a macro call can sit inside another macro call, and that something has to decide which of the two is rewritten first.
Contrast the two orders and say what each hands the outer macro: an argument still written as a call, or the finished code that call becomes.
Diagnose with it. When a macro behaves differently alone and nested, ask what its argument looked like on arrival and whether the macro branches on that shape.
Set the expectation that a macro's published contract states what it does with each argument, so that composition across teams never rests on a toolchain's ordering rule.
## The situation A call site nests one rewrite inside another: the argument of the enclosing macro call is itself a macro call. Both will be rewritten eventually. The only question is which one goes first -- and that is decided by the expander, not by either macro. Two orders exist, and the terms describe where the expander starts, not where it stops. ## Expanding the enclosing call first The expander rewrites the outer call and hands it the argument **exactly as written**. - The outer macro receives a call, not the code that call will become. - If it discards the argument, the inner call is never expanded. Its build-time work never happens and any error it would have raised while expanding never surfaces. - If it places the argument in three positions, the inner call is rewritten three times during the build. That is build cost, paid by the compiler, and it is why a macro's contract should say how often it uses each argument. - Whatever inner calls survive in the output are rewritten on a later pass. ## Expanding the arguments first The expander rewrites each argument, then considers the enclosing call. - The outer macro receives finished code and has no way to tell what produced it. A macro that wanted to recognise a particular inner call cannot. - An error raised while expanding an argument surfaces even when the outer macro would have thrown the argument away. - Each argument is expanded once, regardless of how many positions it ends up in. ## Where the two are observable | Macro behaviour at the call site | Enclosing call first | Arguments first | |---|---|---| | Branches on whether the argument is a literal | sees an unexpanded call | sees the literal it becomes | | Discards the argument entirely | inner expansion never runs | inner expansion runs, output discarded | | Splices the argument into three positions | inner expanded three times | inner expanded once | | Argument's expansion raises an error | reported only if kept | reported either way | The first row is the one that actually changes the emitted program rather than just the build. A macro that asks "is this argument a plain literal?" and takes a cheap path when it is will take the *other* path under the other order, and the artifact differs. ## Re-scanning to a fixed point Rewriting is usually not one pass. An expander commonly scans its own output again and repeats until a pass changes nothing, because a rewrite frequently produces another call that still needs rewriting -- this is how a macro can be built on top of another macro, and how one written by a library composes with one written by an application. Toolchains differ here too: some re-scan, some perform a single pass and require the author to expand explicitly. The repeating loop is what makes a budget necessary, since nothing about it guarantees the output ever stops changing. ## The working rule 1. **Do not write a macro whose correctness depends on the order** unless you have confirmed which order your expander uses and written that assumption down. 2. **State in the macro's contract what it does with each argument**: inspected, placed once, placed several times, or dropped. That is the information a caller needs in order to nest calls safely. 3. **Test with nesting.** Call the macro with another macro call in every argument position, including one you know it discards, and compare the produced code against the same call written out pre-expanded. A difference means the macro is order-sensitive; decide then whether to document it or design it away. The design that avoids the whole problem is a macro that treats every argument as an opaque fragment to be positioned and never inspected. Such a macro produces the same program under either order, and it composes with macros written by people you will never talk to. ## Diagnosing with it When a macro behaves one way alone and another way nested inside something else, this is the first thing to check, and the check is concrete: ask what the argument looked like at the moment the outer macro received it, and whether the outer macro branches on that shape. If it does, you have found the cause, and the fix is either to stop branching on shape or to force the argument to be expanded before it is inspected, if the toolchain offers a way to request that.
- Why do expanders usually re-scan their own output instead of rewriting once?Because a rewrite often produces another call that still needs rewriting, which is how one macro gets built on top of another. Re-scanning to a fixed point lets them compose across libraries. The price is that the loop has no natural end, so it needs a budget to stop it.
- How would you design a macro so that expansion order cannot affect it?Never branch on the shape of an argument. Treat each one as an opaque fragment, place it exactly once, and let the compiler judge it afterwards. Such a macro emits the same code under either order, which is what makes it safe for callers who nest it inside macros you have never seen.
saying these in an interview costs you the question
- Assumes every toolchain rewrites arguments before the enclosing call
- Says expansion order can never change the resulting program
- Thinks a discarded argument is expanded anyway in every toolchain
- Assumes output is never re-scanned, so a macro cannot emit a macro call
- Confuses the order expansions fire with the order emitted code runs