By the end of this chapter you'll be able to…

  • 1Explain why an intermediate representation turns m times n into m plus n
  • 2State why optimisation belongs at the intermediate level
  • 3Write three-address code for an arbitrary expression
  • 4Count the temporaries an expression requires
  • 5Recall the three-address instruction repertoire
  • 6Compare quadruples, triples and indirect triples on reordering cost
  • 7Explain why triples resist reordering
  • 8Use a directed acyclic graph to expose common subexpressions
  • 9Explain why expression translation is S-attributed
  • 10Apply Sethi-Ullman reasoning to minimise live temporaries
  • 11Distinguish full from short-circuit Boolean evaluation
  • 12Explain why short-circuiting changes semantics rather than speed
  • 13Translate conditional and loop constructs into jumps
  • 14Describe backpatching and its three list operations
  • 15Explain how true and false lists propagate upward
  • 16Compute an array element address in row-major order
  • 17Fold the compile-time constant out of an address computation
  • 18State how column-major order changes both formula and locality
  • 19Distinguish static from dynamic type checking
  • 20Distinguish widening from narrowing and name coercion
  • 21Distinguish structural from name type equivalence
💡
Why this chapter matters in GATE
Intermediate code sits between the front end and the back end for an economic reason before a technical one: without it, supporting m source languages on n target machines needs m times n translators, while a common representation needs only m plus n. For four languages and five targets that is 20 against 9, and the ratio worsens as either grows. The second reason is that optimisation belongs in the middle, because the source is too structurally rich to analyse cleanly and the target is too cluttered with machine detail to reveal intent. So every design decision about the representation answers one question: what does the optimiser need to do to this code? Quadruples exist because optimisers move instructions, explicit temporaries because optimisers track values, and explicit jumps because optimisers build control-flow graphs.

Before you start — revise these

🔗
Lexical Analysis, Parsing & Syntax-Directed Translation
Intermediate code is emitted by attribute rules attached to productions, and whether a definition is S-attributed decides whether it can run during the parse.
🔗
Machine Instructions & Addressing Modes
Three-address code resembles assembly with unlimited temporaries, and array address computation is the base-plus-scaled-index mode made explicit.
🔗
Programming in C & Recursion
Row-major layout, structure field offsets and alignment padding are the same facts examined from the language side there.

Intermediate Code Generation

Intermediate code sits between the front end and the back end, and the reason it exists is economic rather than technical.

Without it, supporting source languages on target machines requires separate translators. With a common intermediate representation, each language needs one front end and each machine one back end, giving .

For four languages and five targets that is 20 translators against 9, and the ratio worsens as either number grows.

The second reason is that optimisation belongs in the middle. Optimising the source is impractical because the structure is too rich; optimising the target is impractical because machine detail obscures the intent. An intermediate form is deliberately simple enough to analyse and abstract enough to remain machine-independent.

So every design decision about the representation follows from one question: what does the optimiser need to do to this code? Quadruples exist because optimisers move instructions. Explicit temporaries exist because optimisers track values. Explicit jumps exist because optimisers build control-flow graphs.

1. Three-Address Code

The standard intermediate form allows at most three addresses per instruction and exactly one operator.

Complex expressions are broken into a sequence of such instructions using compiler-generated temporaries. The expression becomes two instructions: one computing into a temporary, and one adding to it.

The instruction repertoire is small and is worth knowing exactly.

FormMeaning
Binary operation
Unary operation
Copy
goto LUnconditional jump
if x relop y goto LConditional jump
param x and call p, nProcedure call
and Indexed assignment
and Address and pointer

The number of temporaries needed equals the number of interior nodes in the expression tree, since each operator produces one intermediate value. That count is a standard numerical question.

Three-address code is close to assembly but machine-independent: it has unlimited temporaries, no registers, and no addressing modes.

2. Representations

Three ways of storing three-address code differ in how easily instructions can be moved, which matters entirely because of optimisation.

A quadruple has four fields: operator, first argument, second argument and result. The result is a named temporary, so an instruction can be moved anywhere without disturbing others.

A triple has three fields and no result field. The result is referred to by the instruction's own position, so instruction 3's output is written as "(3)".

That positional reference is the problem. Moving a triple changes its position and therefore invalidates every reference to it, so reordering requires renumbering everything.

An indirect triple adds a separate list of pointers to triples. Reordering means permuting the pointer list while the triples themselves stay put, so references remain valid.

RepresentationSpaceReorderingSuits
QuadrupleLargestFreeOptimising compilers
TripleSmallestExpensiveNon-optimising compilers
Indirect tripleMiddleCheapOptimising compilers

Quadruples also make temporaries explicit, which lets an optimiser reason about a value's lifetime. Triples save the space of naming temporaries and lose exactly that ability.

A directed acyclic graph is a fourth representation used for a basic block. Identical subexpressions share a node, which exposes common subexpressions directly and makes their elimination a matter of reading the graph.

3. Translating Expressions

Expression translation is a syntax-directed definition with a synthesised attribute holding the place where the value lives.

For a production , generate code for both operands, allocate a temporary, and emit an addition into it.

Because every attribute flows upward, the definition is S-attributed and evaluates during a bottom-up parse with no separate tree traversal.

The temporary count can be reduced. Evaluating the more complex operand first minimises the number of temporaries live simultaneously, which is the Sethi-Ullman numbering idea and matters when registers are scarce.

For an expression tree, the minimum number of registers needed with no spilling is computed recursively: a leaf needs one, and an interior node needs the maximum of its children's requirements if they differ, or one more than either if they are equal.

4. Boolean Expressions and Control Flow

Boolean expressions can be translated two ways, and the choice is visible to the programmer.

Full evaluation computes the whole expression and produces a value, treating true and false as 1 and 0.

Short-circuit evaluation generates jumps and never computes a value at all. For A and B, if A is false the code jumps directly to the false target without evaluating B.

Short-circuiting is not merely an optimisation; it changes semantics. An expression such as p != null and p->x > 0 is correct only under short-circuiting, since full evaluation would dereference a null pointer.

Control flow translation uses inherited attributes carrying the true and false targets down the tree.

For if E then S, the true target of is the start of and the false target is the code after it. The condition never produces a value; it simply jumps to one of two labels.

For a while loop, the false target is the exit and the code after jumps back to re-test the condition.

5. Backpatching

The difficulty with generating jumps in one pass is that a jump's target is often not yet known. When translating if E then S, the address following is unknown until has been generated.

Backpatching solves this by emitting jumps with blank targets and recording their addresses in a list, then filling the targets in once they become known.

Three operations manage the lists.

Makelist creates a list containing one instruction index. Merge concatenates two lists. Backpatch fills a given label into every instruction in a list.

The mechanism lets a one-pass compiler generate correct jumps without building a tree or making a second pass.

Each Boolean expression carries two lists: the true list of jumps taken when it evaluates true, and the false list for when it evaluates false. A statement carries a next list of jumps to whatever follows it.

For E1 and E2, the false lists merge — either operand being false makes the whole expression false — while 's true list is backpatched to the start of , since being true means must be tested.

6. Arrays and Records

Array element addressing is arithmetic on the base address.

For a one-dimensional array with base address , element size and lower bound :

For a two-dimensional array in row-major order with columns:

The compiler factors this into a part computable at compile time and a part requiring run-time arithmetic. The constant portion is folded into the base, leaving only the index multiplications for run time.

Column-major order reverses the roles of the two indices, which changes the generated code and, more importantly, the memory access pattern.

For a record, each field sits at a fixed offset from the record's base, computed at compile time from the sizes of preceding fields. Alignment padding may be inserted, so the offset of a field is not simply the sum of the preceding sizes.

7. Type Checking and Conversion

The semantic analyser attaches a type to every expression and verifies that operators receive operands they accept.

Static type checking happens at compile time and dynamic type checking at run time. A statically typed language catches type errors before execution at the cost of rejecting some programs that would have run correctly.

Type conversion is widening when it loses nothing and narrowing when it may. Converting an integer to a floating-point value is widening; the reverse is narrowing and typically requires an explicit cast.

Implicit conversion inserted by the compiler is called coercion. In an expression mixing an integer and a float, the compiler generates a conversion instruction for the integer, and that instruction appears in the intermediate code.

Type equivalence comes in two flavours. Structural equivalence treats two types as the same if they have the same shape; name equivalence treats them as the same only if they have the same name. The distinction decides whether two identically-shaped records with different names are interchangeable.

8. Worked Examples

Example 1. Generate three-address code for , and state how many temporaries a naive translation uses.

A naive left-to-right translation treats each operator independently.

Four temporaries, matching the four interior nodes of the expression tree.

Now build the directed acyclic graph for the same expression. The subexpression appears twice, and in a DAG both occurrences share one node.

Regenerating from the DAG gives:

Three temporaries and one fewer multiplication. The saving is common subexpression elimination, and the DAG is what made it visible without any separate analysis.

Example 2. Convert this three-address code to quadruples and triples: , , .

Quadruples name the result explicitly in a fourth field.

#oparg1arg2result
0+ab
1c
2=d

Triples drop the result field and refer to an instruction by its position.

#oparg1arg2
0+ab
1(0)c
2=(1)d

The triple form is more compact, saving the space of naming two temporaries.

Now suppose the optimiser wants to move instruction 0 later. With quadruples, the instruction moves and nothing else changes, because is a name rather than a position.

With triples, instruction 1 refers to "(0)", so moving instruction 0 to position 2 requires rewriting that reference — and every other reference in the block. Any reordering means renumbering.

Indirect triples fix this by keeping the triples fixed and permuting a separate pointer array, so references stay valid.

Example 3. Translate if (a < b) x = 1; else x = 2; into three-address code with jumps.

Boolean conditions in control flow generate jumps rather than values.

    if a < b goto L1
    goto L2
L1: x = 1
    goto L3
L2: x = 2
L3:

The pattern is standard: test, jump to the true branch, fall through to the false branch, and have the true branch jump over the false one.

A common optimisation inverts the test to remove one jump.

    if a >= b goto L2
    x = 1
    goto L3
L2: x = 2
L3:

This saves an instruction on the true path, which is the more common case in most code.

Note that the condition produces no value anywhere. No temporary holds true or false; the entire meaning of the comparison is expressed in which label control reaches.

Example 4. Explain backpatching using if (a < b) then S.

Translation proceeds left to right in one pass, and the difficulty is that the address following is unknown when the condition is translated.

Step 1. Translate the condition. Emit two jumps with blank targets.

Instruction 100: if a < b goto ___

Instruction 101: goto ___

Record instruction 100 in the condition's true list and instruction 101 in its false list.

Step 2. The next instruction generated will be the start of , at address 102. Since the condition being true means executes, backpatch the true list with 102.

Instruction 100 becomes if a < b goto 102.

Step 3. Generate , occupying instructions 102 onward, say through 105.

Step 4. The statement is now complete. Its next list contains instruction 101, the false jump, which must go to whatever follows the whole if.

That address is still unknown, so instruction 101 stays blank and its index is passed upward in the next list of the if statement, to be backpatched by whatever encloses it.

The key insight is that a jump target is filled in as soon as it becomes known and no sooner, and lists carry the pending jumps upward until the enclosing construct can resolve them. This is what makes single-pass code generation possible without building a tree.

Example 5. An array is declared A[10][20] with 4-byte elements, base address 2000, and lower bounds of 1 in both dimensions. Generate the address computation for A[i][j] in row-major order.

The general formula with lower bounds is base plus the offset in elements times the element size.

Expand to separate the compile-time constants from the run-time work.

The constant 1916 is computed at compile time, so the generated code performs only two multiplications and two additions.

The three-address code is:

Under column-major order the roles reverse, giving , which folds to .

The difference matters for more than arithmetic: traversing a row-major array along rows walks contiguous memory, while walking columns strides across it and misses the cache repeatedly.

Example 6. Why does short-circuit evaluation change program meaning rather than merely improving speed?

Under full evaluation, both operands of a Boolean operator are computed before the operator is applied. Under short-circuiting, the second operand is evaluated only if the first does not already determine the answer.

If the second operand has no side effects and cannot fail, the two produce identical results and short-circuiting is purely an optimisation.

The difference appears the moment the second operand can fail or has an effect.

Consider p != null and p->value > 0. Under short-circuiting, a null p makes the first operand false, the answer is determined, and the dereference never happens. Under full evaluation, the dereference happens regardless and the program crashes.

Consider i < n and a[i] > 0. Short-circuiting prevents an out-of-bounds read when i equals n; full evaluation performs it.

Consider flag or increment(counter). Under short-circuiting, a true flag means the counter is never incremented, so the two evaluation strategies produce different program states.

This is why languages specify the behaviour rather than leaving it to the compiler. C, Java and most modern languages mandate short-circuiting for their logical operators and provide separate bitwise operators for the full-evaluation case.

For the compiler writer, the consequence is that the jump-based translation is not an alternative implementation of the value-based one. It is the required implementation, and generating code that evaluates both operands would be a correctness bug.

Summary

An intermediate representation turns translators into , and it is where optimisation belongs because the source is too rich to analyse and the target too detailed.

Three-address code allows one operator and at most three addresses, breaking expressions apart with compiler temporaries. The number of temporaries equals the number of interior nodes in the expression tree.

Quadruples name the result and can be reordered freely. Triples refer to results by position and cannot be reordered without renumbering. Indirect triples permute a pointer list instead, recovering cheap reordering.

A directed acyclic graph shares identical subexpressions, exposing common subexpressions directly and reducing both temporaries and operations.

Expression translation is S-attributed and evaluates during a bottom-up parse. Evaluating the harder operand first minimises simultaneously live temporaries.

Boolean expressions in control flow generate jumps rather than values. Short-circuit evaluation changes semantics, not merely speed, and is mandated by most language specifications.

Backpatching emits jumps with blank targets, records their indices in lists, and fills targets in as soon as they are known. Makelist, merge and backpatch manage the lists, and pending jumps propagate upward until an enclosing construct resolves them.

Array addressing folds the compile-time constant into the base, leaving only index arithmetic at run time. Row-major and column-major differ in both the formula and the cache behaviour.

Type checking may be static or dynamic; widening loses nothing while narrowing may; implicit conversion is coercion and appears explicitly in the intermediate code. Structural and name equivalence differ on whether identically-shaped named types are interchangeable.

Key formulas & results

Everything to memorise for the exam hall, in one card. Screenshot this for revision.

The organising tool
AN INTERMEDIATE REPRESENTATION TURNS m TIMES n TRANSLATORS INTO m PLUS n, AND EVERY DESIGN DECISION FOLLOWS FROM WHAT THE OPTIMISER MUST DO NEXT.
QUADRUPLES EXIST BECAUSE OPTIMISERS MOVE INSTRUCTIONS, TEMPORARIES BECAUSE THEY TRACK VALUES, AND EXPLICIT JUMPS BECAUSE THEY BUILD CONTROL-FLOW GRAPHS.
Why optimisation sits in the middle
THE SOURCE IS TOO STRUCTURALLY RICH TO ANALYSE CLEANLY AND THE TARGET IS TOO CLUTTERED WITH MACHINE DETAIL TO REVEAL INTENT.
AN INTERMEDIATE FORM IS DELIBERATELY SIMPLE ENOUGH TO ANALYSE AND ABSTRACT ENOUGH TO REMAIN MACHINE-INDEPENDENT.
Three-address code
AT MOST THREE ADDRESSES AND EXACTLY ONE OPERATOR PER INSTRUCTION, IN THE FORM x = y op z.
COMPLEX EXPRESSIONS ARE BROKEN APART USING COMPILER-GENERATED TEMPORARIES. IT HAS UNLIMITED TEMPORARIES, NO REGISTERS AND NO ADDRESSING MODES.
Counting temporaries
THE NUMBER OF TEMPORARIES A NAIVE TRANSLATION NEEDS EQUALS THE NUMBER OF INTERIOR NODES IN THE EXPRESSION TREE.
EACH OPERATOR PRODUCES ONE INTERMEDIATE VALUE. THIS COUNT IS A STANDARD NUMERICAL QUESTION AND IS REDUCED BY COMMON SUBEXPRESSION SHARING.
Quadruples
FOUR FIELDS: OPERATOR, FIRST ARGUMENT, SECOND ARGUMENT AND RESULT. THE RESULT IS A NAMED TEMPORARY.
AN INSTRUCTION CAN BE MOVED ANYWHERE WITHOUT DISTURBING OTHERS, BECAUSE REFERENCES USE NAMES RATHER THAN POSITIONS. NAMED TEMPORARIES ALSO LET AN OPTIMISER TRACK LIFETIMES.
Triples
THREE FIELDS AND NO RESULT FIELD. THE RESULT IS REFERRED TO BY THE INSTRUCTION'S OWN POSITION.
MOVING A TRIPLE CHANGES ITS POSITION AND INVALIDATES EVERY REFERENCE TO IT, SO ANY REORDERING REQUIRES RENUMBERING THE WHOLE BLOCK.
Indirect triples
A SEPARATE LIST OF POINTERS TO TRIPLES. REORDERING PERMUTES THE POINTER LIST WHILE THE TRIPLES STAY PUT.
REFERENCES REMAIN VALID, SO CHEAP REORDERING IS RECOVERED AT LESS SPACE COST THAN QUADRUPLES.
Directed acyclic graphs
IDENTICAL SUBEXPRESSIONS SHARE A SINGLE NODE WITHIN A BASIC BLOCK.
COMMON SUBEXPRESSIONS BECOME VISIBLE DIRECTLY, SO ELIMINATING THEM IS A MATTER OF READING THE GRAPH RATHER THAN RUNNING A SEPARATE ANALYSIS.
Expression translation
A SYNTHESISED ATTRIBUTE HOLDS THE PLACE WHERE EACH SUBEXPRESSION'S VALUE LIVES, SO THE DEFINITION IS S-ATTRIBUTED.
IT THEREFORE EVALUATES DURING A BOTTOM-UP PARSE WITH NO SEPARATE TREE TRAVERSAL, EMITTING CODE AT EACH REDUCTION.
Minimising live temporaries
EVALUATING THE MORE COMPLEX OPERAND FIRST MINIMISES THE NUMBER OF TEMPORARIES LIVE SIMULTANEOUSLY.
THE MINIMUM REGISTER COUNT IS 1 FOR A LEAF, THE MAXIMUM OF THE CHILDREN'S REQUIREMENTS IF THEY DIFFER, AND ONE MORE THAN EITHER IF THEY ARE EQUAL.
Full versus short-circuit evaluation
FULL EVALUATION COMPUTES A VALUE FOR THE WHOLE EXPRESSION. SHORT-CIRCUIT EVALUATION GENERATES JUMPS AND NEVER COMPUTES A VALUE.
SHORT-CIRCUITING CHANGES SEMANTICS RATHER THAN MERELY SPEED, WHICH IS WHY LANGUAGE SPECIFICATIONS MANDATE IT RATHER THAN LEAVING IT TO THE COMPILER.
Control flow translation
INHERITED ATTRIBUTES CARRY THE TRUE AND FALSE TARGETS DOWN THE TREE. THE CONDITION JUMPS TO ONE OF TWO LABELS AND PRODUCES NO VALUE.
FOR AN IF, THE TRUE TARGET IS THE START OF THE BODY AND THE FALSE TARGET IS THE CODE AFTER IT. FOR A WHILE, THE FALSE TARGET IS THE EXIT.
Backpatching
EMIT JUMPS WITH BLANK TARGETS, RECORD THEIR ADDRESSES IN A LIST, AND FILL THE TARGETS IN ONCE THEY BECOME KNOWN.
THIS LETS A ONE-PASS COMPILER GENERATE CORRECT JUMPS WITHOUT BUILDING A TREE OR MAKING A SECOND PASS.
The three list operations
MAKELIST CREATES A LIST WITH ONE INSTRUCTION INDEX. MERGE CONCATENATES TWO LISTS. BACKPATCH FILLS A LABEL INTO EVERY INSTRUCTION IN A LIST.
EACH BOOLEAN EXPRESSION CARRIES A TRUE LIST AND A FALSE LIST; EACH STATEMENT CARRIES A NEXT LIST OF JUMPS TO WHATEVER FOLLOWS IT.
Backpatching an AND
FOR E1 AND E2, THE FALSE LISTS MERGE, AND E1'S TRUE LIST IS BACKPATCHED TO THE START OF E2.
EITHER OPERAND BEING FALSE MAKES THE WHOLE EXPRESSION FALSE, WHILE E1 BEING TRUE MEANS E2 MUST STILL BE TESTED.
One-dimensional array addressing
THE ADDRESS OF A[i] IS THE BASE PLUS (i MINUS THE LOWER BOUND) TIMES THE ELEMENT SIZE.
THE COMPILER FOLDS THE CONSTANT PORTION INTO THE BASE AT COMPILE TIME, LEAVING ONLY THE INDEX MULTIPLICATION FOR RUN TIME.
Two-dimensional array addressing
ROW-MAJOR: BASE PLUS ((i MINUS l1) TIMES n2 PLUS (j MINUS l2)) TIMES THE ELEMENT SIZE.
COLUMN-MAJOR REVERSES THE ROLES OF THE TWO INDICES, CHANGING BOTH THE GENERATED CODE AND THE MEMORY ACCESS PATTERN AND THEREFORE THE CACHE BEHAVIOUR.
Record field offsets
EACH FIELD SITS AT A FIXED COMPILE-TIME OFFSET COMPUTED FROM THE SIZES OF PRECEDING FIELDS.
ALIGNMENT PADDING MAY BE INSERTED, SO A FIELD'S OFFSET IS NOT SIMPLY THE SUM OF THE PRECEDING SIZES.
Type checking and conversion
STATIC CHECKING HAPPENS AT COMPILE TIME AND DYNAMIC AT RUN TIME. WIDENING LOSES NOTHING; NARROWING MAY.
IMPLICIT CONVERSION INSERTED BY THE COMPILER IS COERCION, AND IT APPEARS AS AN EXPLICIT CONVERSION INSTRUCTION IN THE INTERMEDIATE CODE.
Type equivalence
STRUCTURAL EQUIVALENCE TREATS TWO TYPES AS THE SAME IF THEY HAVE THE SAME SHAPE. NAME EQUIVALENCE REQUIRES THE SAME NAME.
THE DISTINCTION DECIDES WHETHER TWO IDENTICALLY-SHAPED RECORDS WITH DIFFERENT NAMES ARE INTERCHANGEABLE.
⚠️

Traps GATE sets — and how to dodge them

These are the exact option-traps and misreads that cost marks under negative marking.

WATCH OUT
Counting temporaries without eliminating common subexpressions
A naive translation needs one temporary per interior node, but a directed acyclic graph shares repeated subexpressions and reduces both the temporary count and the operation count. Questions often expect the DAG figure.
WATCH OUT
Claiming triples save time as well as space
They save space by omitting the result field, but the positional reference makes reordering expensive, which is exactly what an optimiser needs to do. Indirect triples recover cheap reordering at intermediate space cost.
WATCH OUT
Treating short-circuit evaluation as an optimisation
It changes program meaning whenever the second operand can fail or has side effects. A null-check followed by a dereference is correct only under short-circuiting, which is why language specifications mandate it.
WATCH OUT
Expecting a Boolean condition in control flow to produce a value
Under jump-based translation the condition produces no value at all; its entire meaning lies in which label control reaches. No temporary ever holds true or false.
WATCH OUT
Backpatching a jump before its target is known
The whole point is that the target is filled in only when it becomes known, and until then the instruction index travels in a list. Pending jumps propagate upward until an enclosing construct can resolve them.
WATCH OUT
Merging the wrong lists for an AND
The false lists merge, since either operand being false decides the result. The first operand's true list is backpatched to the start of the second, because a true first operand means the second must still be tested.
WATCH OUT
Forgetting the lower bound in array addressing
The offset uses the index minus the lower bound. A language with one-based arrays produces a different constant from a zero-based one, and the difference is folded into the base at compile time.
WATCH OUT
Applying the row-major formula to a column-major language
Row-major multiplies the first index by the number of columns; column-major multiplies the second index by the number of rows. The two give different addresses for the same element.
WATCH OUT
Computing a record field offset as the sum of preceding field sizes
Alignment padding may be inserted between fields, so the offset is the sum of the sizes plus any padding. Reordering fields can change the total record size for this reason.
WATCH OUT
Treating coercion as free
An implicit conversion generates a real instruction in the intermediate code and costs run-time work. Mixing integer and floating-point operands in a loop inserts a conversion on every iteration.
WATCH OUT
Assuming structural and name equivalence agree
Two records with identical fields but different type names are equivalent under structural rules and distinct under name rules. Which applies is a language design decision, not a compiler one.
WATCH OUT
Believing the intermediate form is machine-specific
It deliberately has unlimited temporaries, no registers and no addressing modes, precisely so that one front end can serve many back ends. Machine detail enters only at code generation.

Exam-pattern practice

PYQ-style questions with full solutions. Work through them as a readiness check — mark yourself honestly and get your gap report at the end.

Readiness check

Are you exam-ready for Intermediate Code Generation?

9 problems from this chapter. Try each one, reveal the worked solution, mark yourself honestly — get your gap report at the end.

9 questions~6 min

5-minute revision

The whole chapter, distilled. Read this the night before the exam.

  • An intermediate form turns m times n into m plus n.
  • Optimisation belongs in the middle, not at either end.
  • Three-address code has one operator per instruction.
  • Temporaries equal the interior nodes of the expression tree.
  • Quadruples name the result explicitly.
  • Quadruples reorder freely.
  • Triples refer to results by position.
  • Triples cannot reorder without renumbering.
  • Indirect triples permute a pointer list instead.
  • A DAG shares identical subexpressions.
  • DAG construction performs the recognition automatically.
  • Expression translation is S-attributed.
  • Evaluate the harder operand first to save temporaries.
  • Short-circuiting generates jumps, not values.
  • Short-circuiting changes semantics, not just speed.
  • Language specifications mandate short-circuiting.
  • Control flow uses inherited true and false targets.
  • Backpatching fills targets when they become known.
  • Makelist, merge and backpatch manage the lists.
  • For AND, false lists merge and the true list is patched forward.
  • Pending jumps propagate upward in a next list.
  • Array addressing subtracts the lower bound.
  • The constant part folds into the base at compile time.
  • Row-major multiplies the first index by the column count.
  • Column-major reverses that and changes locality.
  • Record offsets include alignment padding.
  • Static checking is at compile time, dynamic at run time.
  • Widening loses nothing; narrowing may.
  • Coercion generates a real conversion instruction.
  • Structural equivalence ignores type names; name equivalence does not.

GATE question blueprint

How this topic is asked, tier by tier — so you can prep to the pattern.

Typical weightage: Compiler Design contributes roughly 6-8 of the 72 core-CS marks; intermediate code generation supplies 1-2 of those

Question styleMarks eachTypical countWhat it tests
Three-address code1~1Generating code and counting temporaries from the expression tree
Common subexpressions2~1Using a directed acyclic graph to reduce operations and temporaries
Representations2~1Comparing quadruples, triples and indirect triples on reordering cost
Array addressing2~1Row-major computation and folding the compile-time constant
Control flow translation2~1Translating loops and conditionals into jumps
Backpatching2~1List operations and how one-pass generation resolves unknown targets
Short-circuit evaluation2~1Why the choice affects correctness rather than performance
Type conversion1~1Widening, narrowing, coercion and type equivalence

Exam-hall strategy

Battle-tested tips from mentors and toppers for this topic under the sectional clock.

  1. Count temporaries from the expression tree, then check for repeated subexpressions.
  2. For representation questions, ask whether reordering is required.
  3. Expand array formulas fully and fold the constants before writing code.
  4. Check the lower bounds and the storage order before applying an address formula.
  5. For Boolean translation, remember the condition produces no value.
  6. For backpatching, track which list is merged and which is patched immediately.
  7. Temporary counts and array addresses are commonly set as NAT, which carries no negative marking, so never leave one blank.
  8. For 1-mark and 2-mark MCQs, negative marking is -1/3 and -2/3, so guess only after eliminating an option.
  9. GATE gives a single freely-navigable 180-minute window, so flag a long backpatching trace and return to it.

Beyond the exam

Where this skill shows up in the job you're competing for — and in life.

Retargeting a compiler

Adding support for a new processor means writing one back end against the intermediate form, not rewriting the compiler once per source language.

Reading compiler-generated assembly

Recognising a folded array address constant explains why generated code contains a strange-looking base value that appears nowhere in the source.

Relying on a null guard

Writing a null check and a dereference in one condition is safe only because the language specification mandates short-circuit evaluation, not because the compiler chose it.

Understanding a struct's size

Field offsets include alignment padding, which is why reordering members can shrink a structure and why its size is not the sum of its fields.

Where else this topic is tested

Prepare once, score in every exam that asks it.

GATE DALow overlap — compiler design is not a component of that paper
UGC NET Computer ScienceHigh overlap — three-address code forms, representation comparison and type checking are examined as direct recall
ISRO / BARC / DRDO computer science papersHigh overlap — array address computation and quadruple-versus-triple comparison are recurring MCQ topics

Questions aspirants ask

Pulled from the Q&A community and mentor sessions.

Because each end is the wrong shape for the analysis. Source code carries too much structure: nested expressions, implicit control flow in constructs such as short-circuit operators and loop headers, syntactic sugar, and language-specific features that differ between every front end. Writing an optimiser against it means writing one per language, and reasoning about data flow requires first flattening the structure anyway. Target code has the opposite problem. By the time registers have been allocated, addressing modes chosen and instructions scheduled, the original intent is buried. Recognising that two expressions compute the same value is hard when both have been rewritten into machine-specific sequences, and any optimiser is tied to one instruction set. An intermediate representation is designed to be exactly what the analysis needs: one operator per instruction so dependences are explicit, unlimited temporaries so no register-allocation artefacts obscure values, explicit jumps so a control-flow graph can be built mechanically, and no machine detail at all so the same optimiser serves every back end. The economic argument reinforces the technical one. With m front ends and n back ends, an optimiser written at the intermediate level is written once rather than m times or n times, and it improves every language-target combination simultaneously. This is why real compilers place the bulk of their optimisation there, with only peephole and scheduling work left for the back end.

When space matters more than reordering, which in practice means a non-optimising compiler or a memory-constrained environment. A quadruple carries four fields, and the fourth names a temporary that must itself be recorded in the symbol table, so the representation costs both the field and the name. A triple omits both, referring to a result simply by the position of the instruction that produced it. For a large program the saving is real. The cost is that positional references make the representation rigid. Moving an instruction changes its position, so every reference to it becomes wrong, and every reference to instructions between the old and new positions shifts too. Repairing this requires a full scan of the block per move, so an optimiser making many moves pays quadratically. Since instruction motion is fundamental to code motion, common subexpression elimination and scheduling, triples are effectively incompatible with serious optimisation. Indirect triples resolve the tension. The triples live in a fixed array and never move; a separate pointer list determines execution order, and reordering permutes only that list. Internal references stay valid because the triples keep their storage positions. The cost is one extra pointer per instruction, which is less than a quadruple's result field plus symbol-table entry. What indirect triples still lack is explicit temporary names, which an optimiser wants for reasoning about live ranges during register allocation. That is the remaining reason quadruples dominate in optimising compilers despite being the largest of the three.

By emitting incomplete instructions and deferring only the part that is unknown, while carrying the record of what is pending in the parser's attributes. The problem it solves is concrete. When translating a conditional, the jump that skips the body must target the instruction after the body, but that address is unknown until the body has been generated. A two-pass compiler would build a syntax tree, generate code in a second traversal, and know all addresses by then. Backpatching instead emits the jump immediately with a blank target and records its instruction index in a list attached to the grammar symbol. When the enclosing production is later reduced and the address becomes known, the backpatch operation writes that address into every instruction whose index is in the list. Three operations suffice: makelist to create a single-element list, merge to combine two, and backpatch to fill a label into all members. The attribute discipline is what makes it work. Each Boolean expression carries a true list and a false list, holding the jumps taken in each case. Each statement carries a next list of jumps to whatever follows it. When a production combines subexpressions, it decides which lists to merge and which to backpatch immediately. For an AND, the false lists merge because either operand failing decides the result, while the first operand's true list is backpatched to the start of the second. Any list still unresolved travels upward until an enclosing construct knows the address, so every jump is filled exactly once and nothing is revisited.

Because the full formula contains several terms that are constant for a given declaration, and evaluating them at run time would waste instructions on every single access. Take a two-dimensional array in row-major order with lower bounds. The address is the base plus the quantity (i minus the first lower bound, times the number of columns, plus j minus the second lower bound), all times the element size. Expanding that expression separates it into terms involving i, terms involving j, and terms involving neither. The terms involving neither — the base, and the corrections for both lower bounds scaled by the element size — combine into a single constant the compiler computes once. What remains at run time is one multiplication for each index and two additions. For a five-by-eight array of eight-byte elements based at 4000 with one-based indexing, the folded constant is 4000 minus 64 minus 8, giving 3928, and the generated code computes 64i plus 8j plus 3928. Three run-time operations instead of seven or eight. The saving compounds inside loops, where an array access may execute millions of times. Optimisers go further still: within a loop over i, the term 64i changes by a constant 64 per iteration, so strength reduction replaces the multiplication with an addition, and induction variable elimination may remove the index variable entirely. All of that is possible only because the address computation was made explicit in the intermediate code rather than hidden inside an addressing mode.

Whether two types that happen to have the same shape are interchangeable. Under structural equivalence, a compiler compares the internal composition: two records are the same type if they have the same fields in the same order with the same types, regardless of what they are called. Under name equivalence, they are the same type only if they are the same named declaration, so two identically-shaped records declared separately are distinct and cannot be assigned to one another. The trade is between flexibility and safety. Structural equivalence lets code written against one type accept anything of the same shape, which is convenient and is why it appears in languages emphasising duck typing or in interface definitions. Name equivalence catches errors where two conceptually different things happen to share a layout — a point and a complex number both being two floats, say, or a metre count and a second count both being integers. Assigning one to the other is almost certainly a bug, and name equivalence rejects it while structural equivalence permits it. Most mainstream languages use name equivalence for user-declared record and class types, and structural equivalence for constructed types such as arrays and pointers, where insisting on a shared declaration would be impractical. The compiler-writing consequence is that the type checker needs two comparison routines and a rule for which applies where, and that recursive types complicate the structural case, since naive comparison of two mutually recursive definitions does not terminate without a memo of pairs already assumed equal.
Header Logo