A compile error points at a line nobody typed, inside a macro's output, so what makes that message actionable?
answer
- the caret lands on nobody's line
- emitted nodes need positions
- caller's text versus the macro's own text
- an expansion trace, not one location
- validate in the stage, keep expansions thin
basics
~20 sSource positions carried on every emitted node, plus an expansion trace. Text derived from the caller's argument should point back at the call site, text the macro authored at the macro, and the trace should name the chain that produced it.
solid answer
~40 sThe compiler reports errors where its nodes say they came from. A macro fabricates nodes, so unless the expanding stage attaches a **source position** to each one, the only honest answer is a synthetic location -- a line of text that exists nowhere in the repository. Good expanders attach the **call site's** position to anything derived from the caller's input and the **macro definition's** position to text the macro authored, so the caret lands on whoever can actually fix it. On top of that, the toolchain keeps an **expansion trace**: the chain of rewrites that produced the failing text, which is the part that tells you *why* that line exists. Practical mitigation is to keep expansions thin, emitting a short call into ordinary reviewable code so most errors land in real source.
code
pseudocode · 12 linesmacro require_positive(x):
return quote( if splice(x) <= 0 then fail("not positive") )
// call site, line 12: require_positive(customer_name)
// without positions on the emitted nodes:
// error: cannot compare text with a number at <expanded>:1
// with positions and an expansion trace:
// error: cannot compare text with a number
// at source line 12, argument customer_name
// in expansion of require_positivego deeper
Know that an error can point at code you never typed because a macro wrote it, and that this is expected rather than a corrupted checkout or a broken tool.
Explain how a toolchain avoids it: emitted pieces carry source positions, and text derived from the caller's argument points back at the call site that supplied it.
Show the working habit. Read the expansion trace end to end, decide whether the caller's argument or the macro's own text is at fault, and keep expansions thin so most errors land in ordinary code.
Make debuggability a condition of adoption. An expansion that cannot report failures at a place the reader can act on spends every future reader's time to save its author's.
## The symptom The build fails. The message is about a comparison, or a missing member, or a type mismatch, and the location it names is a line the author never wrote -- possibly in a file that does not exist on disk. This is the characteristic failure of compile-time expansion, and the thing that makes macros feel unreviewable to a team that has not set the mechanism up properly. Nothing mysterious is happening. A compiler reports a problem at the position its nodes claim to come from, and the failing nodes were manufactured by a macro a moment earlier. ## Why the location goes missing Every piece of code the compiler handles carries a position: file, line, column. When source is parsed, positions come for free. When an expanding stage **builds** a node, the position has to be supplied deliberately, and there are three plausible answers: - the position of the **call site** -- correct for anything derived from what the caller wrote; - the position inside the **macro definition** -- correct for text the macro authored itself; - **nothing**, in which case the toolchain invents a synthetic location, and the reader is lost. A stage that supplies nothing is not broken in any way a test will catch: the emitted program is identical. Only the diagnostics degrade, which is why this gets skipped and then hurts for years. ## What the toolchain needs to keep - **A position on every emitted node**, chosen by origin rather than applied uniformly. - **An expansion trace**: the ordered chain of rewrites that produced this text, so a nested expansion can be read end to end rather than as one anonymous blob. - **A stable identity for each expansion**, so two uses of the same macro in one file are distinguishable. - **A way to see the expanded text** on demand, which turns "I cannot read this" into ordinary debugging. ## Whose fault is it, and where should the caret point? | The actual fault | Best place for the caret | Why | |---|---|---| | The caller passed an argument the macro cannot use | The argument at the call site | That is the text the reader can change | | The macro emits text that is wrong for any input | Inside the macro definition | The caller cannot fix it and should not be sent there | | The macro used a valid argument in an invalid position | The call site, with the trace | Both parties need to see how the two combined | The second row is the one people get wrong in both directions: pointing every expansion error at the call site blames a caller who did nothing wrong, and pointing every one at the macro hides genuine misuse. ## What makes macros debuggable in practice 1. **Validate in the stage and fail there.** If an argument is unusable, have the expanding stage raise an error with its own message, positioned at the call site. A clear failure during expansion beats a confusing failure in the compiler afterwards, and it is the single highest-value habit in this material. 2. **Keep expansions thin.** Emit a short call into an ordinary, reviewable function that holds the real logic. That function has real source positions, can be read, tested and stepped through, and most type errors then surface in normal code. 3. **Make the expansion inspectable.** A way to dump what a call site became turns an argument about behaviour into an observation. 4. **Name what you emit distinctly.** Generated helpers with recognisable names make the output legible to the next reader. ## After the build, nothing remains One consequence surprises people: a run-time stack trace will not help you find a macro's contribution. By then the expansion has long finished, and what is running is ordinary emitted code, so the frames name whatever functions that code calls. Unless the stage emitted something identifying, nothing in a trace says the text was generated at all. Diagnosing expansion is a build-time activity, and the information you need must have been recorded at build time or it does not exist. ## What this costs a team A macro whose failures cannot be traced back to a call site imposes a cost on every future reader -- and that cost is paid by people who did not choose the macro. That makes traceability a reasonable condition of adoption rather than a nicety: if a proposed expansion cannot report its failures at the place a reader can act on, the convenience it offers is being bought with somebody else's time.
- Why does a run-time stack trace not help you locate a macro's contribution?Because the expansion is over by then. What runs is ordinary emitted code, so frames name whatever functions that code calls. Unless the stage emitted something identifying, such as a distinctly named helper, nothing in the trace reveals that the text was generated rather than written.
- What should a macro do when the text it emits is what fails to compile?Fail during expansion instead, with a message positioned at the call site. Validate the inputs in the stage and report clearly there. Letting bad input through so that the compiler rejects the output forces the reader to debug text they never wrote, which is the outcome worth avoiding.
saying these in an interview costs you the question
- Says an error inside generated text always means the macro is broken
- Assumes the compiler knows the call site without the stage recording it
- Believes the emitted text is a file on disk you can open and edit
- Thinks one location for the whole expansion is good enough
- Claims a run-time stack trace would show the chain of expansions