skip to content

How does a compiler-hosted processor tell the build which marked declarations it handles?

level: middleimportance: must knowfreq 58%

answer

  1. routing, not scanning
  2. advertise, then dispatch
  3. two filters, not one
  4. kind check after marker match
  5. silence means rejected or never loaded

basics

~20 s

A processor advertises up front the set of metadata markers it supports. Each round the build hands it only the declarations carrying those markers, and the processor then applies its own filter for declaration kind and shape before emitting anything.

solid answer

~50 s

Routing is a two-sided contract. The processor **advertises** a set of marker names it supports (some models allow a wildcard for a whole namespace or for every marker). The build collects, for the round it is running, the declarations that carry one of those markers and hands that subset to the processor — never the whole program. The processor then applies a second, narrower filter of its own: is the marker on a declaration kind it can generate for, is the type reachable, does it have the members the template needs. Anything that fails that filter is reported as a positioned diagnostic rather than skipped in silence. In models where claiming is part of the contract, a processor can also report that it has handled those markers, in which case processors that have not yet run are not offered the same declarations.

code

pseudocode · 14 lines
pseudocode
processor CompanionMapperEmitter:

    supportedMarkers = ["Mapped"]        // the half the build routes on

    on round(declarations):
        for each decl in declarations:    // already filtered to "Mapped"
            if decl.kind is not CLASS_LIKE:
                report error at decl, "Mapped applies to a class-like declaration"
                continue
            if decl.readableFields is empty:
                report warning at decl, "nothing to map; no file emitted"
                continue
            emit file named decl.name + "Mapper" with mapperBodyFor(decl)
        return HANDLED

go deeper

for a junior

Recall that the marker on the declaration is only a request, and that a separate tool has to be installed in the build and be looking for that exact marker before anything is generated.

for a middle

Explain the two halves: the advertised marker set the build routes on, and the processor's own check of declaration kind and shape. Say why a rejected declaration must produce a diagnostic rather than silence.

for a senior

Show how you debug 'the marker is there and nothing appeared': processor path, marker namespace, the processor's internal filter, and whether another tool claimed it first. Name which of those the author can see from the source.

for a principal

Weigh owning a marker that several tools read. Decide whether your processor claims, what you commit consumers to by matching on a name with no compiler-checked link, and how a team discovers that a marker has no listener at all.

## What a build-time processor is A build-time processor is a plug-in that the compiler loads and calls **while it is compiling**. It is handed declarations — a type, a member, a parameter — in the form the compiler has already parsed and given names and types to, and its product is **new files**. It is not a text pass that runs before the compiler, and it is not code that inspects a program that is already running. Everything else about it follows from that position inside the build. A compilation may contain thousands of declarations. Offering all of them to every loaded plug-in would be slow and meaningless, so the build has to settle a routing question first: which declarations does *this* processor want? That question is what an interviewer is really asking. ## The two-sided contract 1. **The processor advertises.** It publishes the set of metadata marker names it supports. Models differ in how expressive that set is: some accept only exact names, some accept a wildcard covering a namespace, some allow a processor to declare that it wants every marked declaration. 2. **The build dispatches.** For each round it gathers the declarations carrying one of the advertised markers and passes that subset — and only that subset — to the processor. The processor is never handed the program. 3. **The processor filters again.** The marker is a coarse signal. A processor that emits a companion mapper type for every marked declaration still has to check that the marker landed on a class-like declaration rather than a parameter, that the declaration is visible to the file it is about to generate, and that its members are the shape the template assumes. The second filter is where most real processors spend their code, and it is the half candidates forget. ## What arrives, and what does not | the processor is given | the processor is not given | |---|---| | the declaration's name, kind and modifiers | permission to edit any of them | | the marker and the values written into it | the raw file text in most models — it works over a symbol view | | enclosing, nested and inherited members | declarations a later round has not generated yet | | a way to report a diagnostic at that declaration | a way to remove a declaration from the compilation | The consequence worth stating out loud: a processor **adds**, it does not amend. A marker is a request for something new beside the declaration, not an instruction to rewrite it. ## Claiming, in the models that have it Some processing models let a processor report that it has **handled** the markers it was given. Where that exists: - the claim applies to the markers dispatched in that round, not to the whole build; - processors that have not yet run in that round are not offered those declarations; - a processor that generates for a marker other tools also read should therefore usually *not* claim, or it silently disables them. Other models have no claim at all: every registered processor sees every matching declaration and the processors must tolerate each other. Because the models differ, the honest interview answer names claiming as a possibility and says what it changes, rather than asserting it as universal. ## Why nothing was generated — the diagnostic chain The question is usually asked in its failure form: *the marker is on the class and no file appeared.* A strong candidate walks the routing path in order: 1. The processor was never on the build's processor path, so it was not loaded at all. 2. It was loaded but advertises a different marker name — a marker of the same short name from another namespace does not match. 3. It was dispatched the declaration and rejected it in its own filter, and reported nothing, so the rejection is invisible. 4. Processing was switched off for that compilation, or the module being compiled is not the one carrying the marker. 5. Another processor claimed the markers first in a model where claiming is exclusive. Only the third of those is the processor author's own bug, and it is the one a positioned diagnostic would have made obvious. "Generated nothing and said nothing" is the characteristic failure of this mechanism, which is why advertise-then-filter and report-on-reject are taught together. ## The shape this leaves in a codebase The marker is the hand-written half of the contract and the advertised set is the tool's half. They are coupled by a name only, with no compiler check that anyone answers the marker — which is exactly why a team adding a marker to a declaration and getting silence cannot tell, from the source alone, whether the tool disagreed or was never there.

  • The same marker name exists in two namespaces and only one of them triggers generation. What is happening?
    The advertised set is matched on the fully qualified marker name, not the short one written at the declaration. A declaration carrying the same short name from a different namespace is simply never dispatched to that processor, so the build does exactly nothing and reports nothing. The fix is at the import, not in the processor.
  • Two processors advertise the same marker. What should each of them assume about the other?
    That the other exists and may run first, and that neither owns the marker. Neither should assume it sees the declaration exclusively, both must tolerate a companion file already existing for that declaration, and neither should claim the marker in a model where claiming is exclusive — claiming would silently disable the other tool.
  • Why report a diagnostic on a declaration the processor decides to skip?
    Because skipping is indistinguishable from not being installed. A marker is an explicit request from the author, so a processor that declines it is refusing a request, and the author needs to be told which declaration and why. Silent skipping produces the mechanism's signature bug: correct-looking source and a missing type at the call site.

It is a mailroom sort: the plug-in files a standing request for one label, the mailroom delivers only envelopes wearing it, and the plug-in still opens each one to check it can act on the contents.

saying these in an interview costs you the question

  • Says the processor is handed every declaration in the compilation
  • Thinks a matching marker alone guarantees a file is generated
  • Believes the processor scans the source text for the marker string
  • Assumes claiming a marker never affects what other tools see
  • Treats a missing generated file as a compiler bug, not a routing miss
  • Skips a declaration it cannot handle without reporting anything