How do you decide how many intermediate representation levels a small language's compiler should carry between its syntax tree and target code?
answer
- a level is a vocabulary
- work is easy where it is expressible
- each level is a permanent tax
- split under pressure, not by diagram
- printer, verifier and positions per level
basics
~20 sAdd a level only when a whole class of work is natural there and awkward at both neighbours — because each level costs another lowering step, printer, verifier and position mapping to keep correct forever. Start with two and split under pressure.
solid answer
~60 sLevels exist to make work natural, not to mirror a textbook pipeline. A **high** level keeps source constructs intact, which is the only place a rewrite that must recognise a construct can run. A **middle**, flat level makes order and provenance explicit and is where most machine-independent work belongs. A **low** level exposes addressing and target detail. Each extra level is a permanent cost: another data structure, another lowering step, another printer and verifier, another place a bug can hide, and one more hop that source positions and debug information must survive. So the decision rule is pressure-driven: start with the syntax tree plus one flat level, and split only when you can point at a class of work that is clumsy at both neighbours — a construct-level rewrite that needs shape the flat form has destroyed, or target detail that pollutes the machine-independent level. For a small language with one target and a small team, two levels is often the honest answer, and a third is a commitment you should be able to justify by name.
go deeper
Recall that a compiler may hold the program in more than one form on the way down, each simpler and closer to the machine than the last, rather than jumping straight from a tree to machine code.
Be able to say what each level makes easy: source shape near the top, explicit order and provenance in the flat middle, addressing and target detail at the bottom.
Show the operational cost you have lived with — a lowering step to keep correct, a printer and verifier to maintain, and diagnostics that degrade when positions are dropped at a boundary.
Own the call: state a default you would start from, the named trigger that would make you split, and the signals you would refuse as justification, since a level is far easier to add than to retire.
## What a level is for An intermediate representation level is a **vocabulary**: a set of node or instruction kinds, and the invariants they obey. Work is easy at a level when the thing the work reasons about is expressible in that vocabulary, and hard when it is not. That is the whole basis for the decision — not the number of boxes in a pipeline diagram. The classic three-level shape: 1. **Near the source.** Constructs are intact, names and positions are attached, surface structure is visible. Only here can work that must *recognise a construct* run — once an iteration has become a test and two jumps, the fact that it was an iteration is gone unless something reconstructs it. 2. **Flat and machine-independent.** Expressions are broken into simple instructions, control flow is explicit, intermediates are named. Order and provenance are written down, which is what most rewrites want. 3. **Near the target.** Address arithmetic, calling conventions and target-specific operations are explicit. Decisions that depend on the machine belong here and nowhere above. ## The cost side, stated plainly Every level you add is a permanent tax: - another data structure, with its own construction and invariants; - another **lowering step** from the level above, which must be correct for every construct; - another **printer**, because a level you cannot read is a level you cannot debug; - another **verifier**, or you will ship malformed intermediate code and find out three stages later; - another hop across which **source positions and debug information** must be carried, or diagnostics degrade; - another place a bug can hide, and one more boundary every new contributor must learn. None of that is paid once. It is paid on every future change, by everyone. ## The decision rule **Add a level when a class of work is natural at it and awkward at both of its neighbours — and not otherwise.** Concretely, the signals that justify a split: 1. A rewrite you want needs structure the lower level has destroyed, and reconstructing that structure is itself a project. 2. Target-specific detail is leaking upward and making a level that was meant to be machine-independent carry machine assumptions. 3. One level's invariants are being violated in two different modes by different passes, which usually means it is really two levels wearing one name. 4. You have more than one target, and everything above the divergence point is genuinely shared. Signals that do **not** justify a split: the node kinds have grown numerous; the pipeline diagram in a paper has more boxes; a contributor would like a nicer printed form (that is a printer, not a level); a pass is slow. ## Sizing it for the case in hand For a small stream-transformation language with one target, a small team and a compiler you intend to keep for years: | Levels | Fits when | Watch for | |---|---|---| | Syntax tree only | The output is another high-level form, or a straightforward walk emits the target | Any analysis needing order or provenance becomes painful | | Tree plus one flat level | The common case — construct-aware work above, everything else below | Target detail creeping into the flat level | | Three or more | Several targets, or a genuinely distinct near-machine vocabulary | The lowering steps and verifiers becoming the bulk of the work | The trajectory that usually goes wrong is the opposite of the one people fear. Teams rarely regret starting with two; they regret starting with four, because each was defensible on paper and the cost only showed up as the pace of every later change. ## The things you must preserve across every level Whatever number you land on, three obligations cross all of them, and they are the ones that get forgotten in the design discussion: 1. **Position mapping**, so a message about generated code can be phrased in terms of what the programmer wrote. 2. **A printed form per level**, so a defect can be localised to a lowering step rather than bisected through the whole compiler. 3. **A verifier per level**, so the invariants you are relying on are checked rather than assumed. A team that adds a level without adding these three has bought the cost without the benefit. ## What interviewers listen for This has no single right answer, and the interviewer is listening for the shape of the reasoning: does the candidate tie levels to *classes of work*, do they name the recurring cost rather than only the structural elegance, and do they have a default they would actually start from and a named trigger for changing it? An answer that recites three levels because that is the standard picture, with no account of what each one earns, misses the question.
- What is the strongest argument for keeping a level close to the source?Some rewrites need to recognise a construct, and that recognition is only free while the construct still exists. Once it has been shredded into tests and jumps, the information is not recoverable without a reconstruction analysis — so the work either happens up there or effectively cannot happen at all.
- Why is retiring a level harder than adding one?Everything written against that vocabulary — passes, tests, printers, tooling and anyone's mental model — assumes it. Removing it means rehoming each of those at a neighbour whose invariants differ. The cost of the decision is therefore asymmetric, which argues for starting with fewer.
- What do you build alongside a new level, before the first pass that uses it?A printed form and a verifier. Without the printer you cannot localise a defect to one lowering step; without the verifier you discover malformed intermediate code much later, in a stage that did not cause it. Both are cheap at the start and expensive to retrofit.
saying these in an interview costs you the question
- Assumes three levels because that is the standard picture
- Counts only the structural elegance, never the recurring cost
- Adds a level because node kinds have grown numerous
- Forgets that positions must survive every lowering step
- Believes a level can be removed later as easily as added