Group A — Short Answer Questions (1 Mark Each)

Q1Define a Lexeme with an example.

Ans: Concise point-wise definition/formula for Define a Lexeme with an example. in accordance with MAKAUT examination pattern.

Q2What is a Token in compiler design?

Ans: Concise point-wise definition/formula for What is a Token in compiler design? in accordance with MAKAUT examination pattern.

Q3Define Pattern in lexical analysis.

Ans: Concise point-wise definition/formula for Define Pattern in lexical analysis. in accordance with MAKAUT examination pattern.

Q4What is a Cross Compiler?

Ans: Concise point-wise definition/formula for What is a Cross Compiler? in accordance with MAKAUT examination pattern.

Q5Define Bootstrapping in compilers.

Ans: Concise point-wise definition/formula for Define Bootstrapping in compilers. in accordance with MAKAUT examination pattern.

Q6What is a Symbol Table?

Ans: Concise point-wise definition/formula for What is a Symbol Table? in accordance with MAKAUT examination pattern.

Q7Name the two parts of a compiler (Analysis and Synthesis).

Ans: Concise point-wise definition/formula for Name the two parts of a compiler (Analysis and Synthesis). in accordance with MAKAUT examination pattern.

Q8What is Left Recursion in context-free grammar?

Ans: Concise point-wise definition/formula for What is Left Recursion in context-free grammar? in accordance with MAKAUT examination pattern.

Q9Define Left Factoring.

Ans: Concise point-wise definition/formula for Define Left Factoring. in accordance with MAKAUT examination pattern.

Q10What is an Ambiguous Grammar?

Ans: Concise point-wise definition/formula for What is an Ambiguous Grammar? in accordance with MAKAUT examination pattern.

Q11Define Parse Tree.

Ans: Concise point-wise definition/formula for Define Parse Tree. in accordance with MAKAUT examination pattern.

Q12What is an Abstract Syntax Tree (AST)?

Ans: Concise point-wise definition/formula for What is an Abstract Syntax Tree (AST)? in accordance with MAKAUT examination pattern.

Q13Define Shift-Reduce Conflict in LR parsing.

Ans: Concise point-wise definition/formula for Define Shift-Reduce Conflict in LR parsing. in accordance with MAKAUT examination pattern.

Q14Define Reduce-Reduce Conflict in LR parsing.

Ans: Concise point-wise definition/formula for Define Reduce-Reduce Conflict in LR parsing. in accordance with MAKAUT examination pattern.

Q15What is LL(1) parsing?

Ans: Concise point-wise definition/formula for What is LL(1) parsing? in accordance with MAKAUT examination pattern.

Q16Define FIRST set in top-down parsing.

Ans: Concise point-wise definition/formula for Define FIRST set in top-down parsing. in accordance with MAKAUT examination pattern.

Q17Define FOLLOW set in top-down parsing.

Ans: Concise point-wise definition/formula for Define FOLLOW set in top-down parsing. in accordance with MAKAUT examination pattern.

Q18What is $ symbol in parsing tables?

Ans: Concise point-wise definition/formula for What is $ symbol in parsing tables? in accordance with MAKAUT examination pattern.

Q19What is ε-production in grammar?

Ans: Concise point-wise definition/formula for What is ε-production in grammar? in accordance with MAKAUT examination pattern.

Q20Define Handle in bottom-up parsing.

Ans: Concise point-wise definition/formula for Define Handle in bottom-up parsing. in accordance with MAKAUT examination pattern.

Q21What is Shift action in LR parsing?

Ans: Concise point-wise definition/formula for What is Shift action in LR parsing? in accordance with MAKAUT examination pattern.

Q22What is Reduce action in LR parsing?

Ans: Concise point-wise definition/formula for What is Reduce action in LR parsing? in accordance with MAKAUT examination pattern.

Q23What is Accept action in parser?

Ans: Concise point-wise definition/formula for What is Accept action in parser? in accordance with MAKAUT examination pattern.

Q24Define Yacc tool.

Ans: Concise point-wise definition/formula for Define Yacc tool. in accordance with MAKAUT examination pattern.

Q25Define Lex/Flex tool.

Ans: Concise point-wise definition/formula for Define Lex/Flex tool. in accordance with MAKAUT examination pattern.

Q26What is Synthesized Attribute?

Ans: Concise point-wise definition/formula for What is Synthesized Attribute? in accordance with MAKAUT examination pattern.

Q27What is Inherited Attribute?

Ans: Concise point-wise definition/formula for What is Inherited Attribute? in accordance with MAKAUT examination pattern.

Q28Define S-Attributed SDT.

Ans: Concise point-wise definition/formula for Define S-Attributed SDT. in accordance with MAKAUT examination pattern.

Q29Define L-Attributed SDT.

Ans: Concise point-wise definition/formula for Define L-Attributed SDT. in accordance with MAKAUT examination pattern.

Q30What is Three-Address Code (3AC)?

Ans: Concise point-wise definition/formula for What is Three-Address Code (3AC)? in accordance with MAKAUT examination pattern.

Q31Define Quadruple representation.

Ans: Concise point-wise definition/formula for Define Quadruple representation. in accordance with MAKAUT examination pattern.

Q32Define Triple representation.

Ans: Concise point-wise definition/formula for Define Triple representation. in accordance with MAKAUT examination pattern.

Q33Define Indirect Triple representation.

Ans: Concise point-wise definition/formula for Define Indirect Triple representation. in accordance with MAKAUT examination pattern.

Q34What is Backpatching?

Ans: Concise point-wise definition/formula for What is Backpatching? in accordance with MAKAUT examination pattern.

Q35Define Activation Record. --- Page 2 ---

Ans: Concise point-wise definition/formula for Define Activation Record. --- Page 2 --- in accordance with MAKAUT examination pattern.

Q36What is Display in run-time stack management?

Ans: Concise point-wise definition/formula for What is Display in run-time stack management? in accordance with MAKAUT examination pattern.

Q37What is a Basic Block?

Ans: Concise point-wise definition/formula for What is a Basic Block? in accordance with MAKAUT examination pattern.

Q38What is a Control Flow Graph (CFG)?

Ans: Concise point-wise definition/formula for What is a Control Flow Graph (CFG)? in accordance with MAKAUT examination pattern.

Q39Define Leader in basic block identification.

Ans: Concise point-wise definition/formula for Define Leader in basic block identification. in accordance with MAKAUT examination pattern.

Q40What is Directed Acyclic Graph (DAG) in optimization?

Ans: Concise point-wise definition/formula for What is Directed Acyclic Graph (DAG) in optimization? in accordance with MAKAUT examination pattern.

Q41What is Common Subexpression Elimination?

Ans: Concise point-wise definition/formula for What is Common Subexpression Elimination? in accordance with MAKAUT examination pattern.

Q42What is Dead Code Elimination?

Ans: Concise point-wise definition/formula for What is Dead Code Elimination? in accordance with MAKAUT examination pattern.

Q43What is Constant Folding?

Ans: Concise point-wise definition/formula for What is Constant Folding? in accordance with MAKAUT examination pattern.

Q44What is Copy Propagation?

Ans: Concise point-wise definition/formula for What is Copy Propagation? in accordance with MAKAUT examination pattern.

Q45What is Loop Invariant Code Motion?

Ans: Concise point-wise definition/formula for What is Loop Invariant Code Motion? in accordance with MAKAUT examination pattern.

Q46What is Strength Reduction?

Ans: Concise point-wise definition/formula for What is Strength Reduction? in accordance with MAKAUT examination pattern.

Q47What is Peephole Optimization?

Ans: Concise point-wise definition/formula for What is Peephole Optimization? in accordance with MAKAUT examination pattern.

Q48Define Register Allocation.

Ans: Concise point-wise definition/formula for Define Register Allocation. in accordance with MAKAUT examination pattern.

Q49What is Register Interference Graph?

Ans: Concise point-wise definition/formula for What is Register Interference Graph? in accordance with MAKAUT examination pattern.

Q50Define Spooling/Spilling in register allocation.

Ans: Concise point-wise definition/formula for Define Spooling/Spilling in register allocation. in accordance with MAKAUT examination pattern.

Q51What is a Static Single Assignment (SSA) form?

Ans: Concise point-wise definition/formula for What is a Static Single Assignment (SSA) form? in accordance with MAKAUT examination pattern.

Q52Define Dominator in control flow analysis.

Ans: Concise point-wise definition/formula for Define Dominator in control flow analysis. in accordance with MAKAUT examination pattern.

Group B — Medium / Descriptive Questions (5 Marks Each)

Q1Explain the different phases of a compiler with a neat schematic diagram.

A compiler translates source code into target/machine code through a sequence of phases, each performing a well-defined transformation and passing its output to the next. The phases are grouped into an analysis part (Lexical, Syntax, Semantic Analyzer and Intermediate Code Generator, which build an intermediate representation) and a synthesis part (Code Optimizer and Code Generator, which build the target program). Two components run alongside every phase: the Symbol Table Manager, which records identifiers, their types, scopes and addresses so any phase can look them up, and the Error Handler, which detects and reports errors (lexical, syntax, semantic) without necessarily halting compilation.

Lexical Analyzer Syntax Analyzer Semantic Analyzer Intermediate Code Generator Code Optimizer Code Generator Symbol Table Manager Error Handler
The six phases of a compiler in pipeline order; the Symbol Table Manager and Error Handler are cross-cutting components accessed by every phase.
  • Lexical Analyzer: groups input characters into tokens (identifiers, keywords, operators).
  • Syntax Analyzer: checks tokens against grammar rules, building a parse tree.
  • Semantic Analyzer: checks type consistency and scope rules, builds an annotated tree.
  • Intermediate Code Generator: produces a machine-independent representation (e.g. three-address code).
  • Code Optimizer: improves the intermediate code for speed/space without changing meaning.
  • Code Generator: maps optimized intermediate code to target machine instructions.
Q2Explain Thompson's Construction to convert regular expression (a|b)*a into NFA.

Thompson's Construction builds an NFA from a regular expression by recursively composing small NFA fragments, one per operator, where every fragment has exactly one start state and one accepting state.

  • Base symbol a: start --a--> accept (a single edge).
  • Union r1|r2: a new start state gets epsilon-edges to both sub-NFA starts; both sub-NFA accept states get epsilon-edges to one new common accept state.
  • Concatenation r1r2: the accept state of N(r1) is epsilon-linked to the start state of N(r2).
  • Closure r*: a new start/accept pair is added with epsilon edges: newstart→oldstart, oldaccept→oldstart (repeat loop), newstart→newaccept (bypass, zero repetitions), oldaccept→newaccept.

For (a|b)*a: build N(a) and N(b) as base fragments, combine them with the union rule to get N(a|b), apply the closure rule to obtain N((a|b)*), then concatenate a fresh N(a) fragment at the end. This yields exactly 10 states and 12 transitions (4 symbol transitions + 8 epsilon transitions), matching Thompson's rule of 2 states per operator/symbol occurrence.

start accept ε ε ε a b ε ε ε (loop back) ε ε (bypass, zero reps of (a|b)) ε a
Thompson NFA for (a|b)*a — the star sub-block loops back on ε and also bypasses on ε, then a final 'a' edge leads to the accept state.
Q3Construct DFA from NFA using Subset Construction method for (a|b)*abb.

Subset Construction (Rabin-Scott) converts an NFA to an equivalent DFA where each DFA state is the ε-closure of a set of NFA states. Starting from the ε-closure of the NFA start state, for every unmarked DFA state T and input symbol c, compute ε-closure(move(T,c)); if this set is new, add it as a fresh DFA state. This is repeated until no new sets appear.

For (a|b)*abb (using the standard 11-state Thompson NFA for this expression), applying subset construction produces exactly 5 distinct reachable DFA states:

DFA Stateon aon bAccepting?
A (start)BCNo
BBDNo
CBCNo
DBENo
EBCYes
A B C D E a b a b b a (D→B) b b (E→C) a (E→B)
Minimal DFA for (a|b)*abb: A=start, E=accepting (double circle). B, C, D are self-loop/intermediate states tracking how much of the suffix "abb" has been matched so far.

Verification: tracing "abb" → A --a--> B --b--> D --b--> E (accept). Tracing "ababb" → A-a->B-b->D-a->B-b->D-b->E (accept) — both consistent with the language (a|b)*abb.

Q4Differentiate between Lexeme, Token, and Pattern with suitable examples.

During lexical analysis, source text is scanned and grouped into meaningful units. Three related but distinct terms describe this process:

TermMeaningExample
LexemeThe actual sequence of characters in the source program that matches a pattern for a token.total, count, 123
TokenAn abstract symbol/category returned to the parser, consisting of a token-name and an optional attribute value; represents a class of lexemes.<id, ptr>, <num, 123>
PatternThe rule (regular expression) that describes the set of strings a lexeme must match to be classified as a given token.identifier pattern: letter(letter|digit)*

For example, in the statement total = count + 10;, the lexeme total is recognized as belonging to the token class id because it matches the pattern for identifiers (a letter followed by letters or digits). Similarly 10 is a lexeme matching the pattern for numbers, classified as token num with attribute value 10. In short: the pattern is the rule, the lexeme is the actual matched text, and the token is the category the lexical analyzer reports to the parser, optionally carrying the lexeme as an attribute.

Q5Explain the role of Symbol Table across all compiler phases.

The symbol table is a data structure (commonly a hash table) that stores information about every identifier (variables, functions, constants, labels) used in the source program, such as name, type, scope, size, storage location/offset, and, for functions, parameter and return-type information. It is created during lexical analysis and consulted or updated throughout the compilation process, acting as a shared repository across phases.

PhaseUse of Symbol Table
Lexical AnalyzerInserts new identifiers as lexemes are recognized; returns a pointer to the entry as the token attribute.
Syntax AnalyzerUses entries to help distinguish identifier classes where grammar is ambiguous (e.g. typedef names in C).
Semantic AnalyzerFills in type and scope information; performs type checking and reports undeclared/redeclared identifiers using table entries.
Intermediate Code GeneratorUses type/address information to generate correctly typed three-address code.
Code OptimizerUses scope/usage information to decide safe optimizations (e.g. register allocation, dead-code elimination).
Code GeneratorUses storage/offset information from the table to generate correct memory addresses and instructions.

Because every phase reads or writes it, the symbol table effectively acts as the compiler's shared memory, ensuring consistent identifier information from the first character scanned to the last instruction emitted.

Q6Explain Lexical Error Recovery strategies (Panic mode, deletion, insertion, substitution).

When the lexical analyzer encounters a sequence of characters that cannot be matched to any pattern for a valid token (a lexical error, e.g. an illegal character or a malformed number), it cannot simply stop compilation, since only a small part of the input may be invalid. Instead it applies recovery strategies so scanning can continue and report as many genuine errors as possible in one pass.

  • Panic mode recovery: successive characters are deleted from the remaining input one at a time until the lexical analyzer can find a well-formed token at the beginning of what input is left. This is the simplest and most commonly used strategy.
  • Deletion: an extraneous/spurious character is removed from the input string.
  • Insertion: a missing character required to complete a valid token is inserted into the input.
  • Substitution/Replacement: an incorrect character is replaced by the correct one.
  • Transposition: two adjacent characters that are interchanged (a common typing error) are swapped back.

More advanced analyzers choose the recovery action that requires the fewest character transformations (minimum edit distance) to convert the erroneous lexeme into a valid one, minimizing the chance of introducing further spurious errors.

Q7Explain Left Recursion elimination with algorithm and example.

A grammar is left recursive if it has a nonterminal A such that there is a derivation A => A alpha for some string alpha. Top-down predictive parsers (recursive descent, LL(1)) go into infinite loops on left-recursive grammars, so left recursion must be eliminated before such parsing.

Immediate left recursion elimination: for a production of the form

A -> A alpha1 | A alpha2 | ... | A alpham | beta1 | beta2 | ... | betan

(where no betai begins with A), replace it with:

A -> beta1 A' | beta2 A' | ... | betan A' A' -> alpha1 A' | alpha2 A' | ... | alpham A' | epsilon

This removes the left recursion by pushing the repeated alpha parts to the right, using a new nonterminal A' and epsilon (empty string) as the base case.

Worked example: the grammar E -> E + T | T is left recursive on E. Applying the rule (A = E, alpha = + T, beta = T):

E -> T E' E' -> + T E' | epsilon

If left recursion is indirect (spread across several nonterminals, e.g. A calls B which calls A), the nonterminals are first ordered and substitution is used to convert it into immediate left recursion, which is then removed the same way.

Q8Explain Left Factoring elimination algorithm with an example.

Left factoring is a grammar transformation used when two or more productions of a nonterminal share a common prefix, which makes it impossible for a predictive (top-down) parser to decide which alternative to expand just by looking at the next input symbol. Left factoring rewrites the grammar to defer the decision until enough input has been seen.

For a production of the general form:

A -> alpha beta1 | alpha beta2

where alpha is the common prefix, it is rewritten as:

A -> alpha A' A' -> beta1 | beta2

so the parser first matches the shared prefix alpha and only then, on seeing the next token, chooses between beta1 and beta2.

Worked example: the dangling-else style grammar

stmt -> if expr then stmt else stmt | if expr then stmt

has the common prefix if expr then stmt. Left factoring gives:

stmt -> if expr then stmt stmt' stmt' -> else stmt | epsilon

Now a predictive parser can shift if expr then stmt unconditionally and decide, only on seeing the next token (else or not), which alternative of stmt' to take.

Q9Explain the condition for a CFG to be LL(1).

A context-free grammar is said to be LL(1) if a predictive top-down parser can always decide, by looking at only the current input symbol (1 token of lookahead), which production of the current nonterminal to use, with no backtracking. Formally, a grammar G is LL(1) if and only if, for every nonterminal A having two or more alternative productions A -> alpha1 | alpha2 | ... | alphan, the following conditions hold for every distinct pair of alternatives alphai and alphaj (i not equal to j):

  • FIRST/FIRST disjointness: FIRST(alphai) and FIRST(alphaj) must be disjoint, i.e. FIRST(alphai) ∩ FIRST(alphaj) = empty set. No terminal can begin strings derivable from more than one alternative.
  • FIRST/FOLLOW disjointness on epsilon: if some alphai is nullable (can derive epsilon), then FIRST(alphaj) for every other alternative must not intersect FOLLOW(A); more precisely FIRST(alphai) and FOLLOW(A) must be disjoint. This ensures the parser can correctly decide to use the epsilon-producing alternative only when the lookahead symbol is in FOLLOW(A).
  • At most one alternative of A can derive epsilon.

If any nonterminal violates these conditions the LL(1) parsing table will have a multiply-defined (conflict) entry, and the grammar is not LL(1); it must then be left-factored and/or freed of left recursion, or an ambiguity removed, before predictive parsing can be applied.

Q10Construct LL(1) parsing table for E➔E+T | T, T➔T*F | F after eliminating left recursion.

The grammar E → E+T | T, T → T*F | F is left-recursive; assuming the usual F → (E) | id, eliminating immediate left recursion (A→Aα|β becomes A→βA', A'→αA'|ε) gives:

E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id

FIRST sets: FIRST(F)={(, id}, FIRST(T)={(, id}, FIRST(T')={*, ε}, FIRST(E)={(, id}, FIRST(E')={+, ε}.

FOLLOW sets: FOLLOW(E)={), $}, FOLLOW(E')={), $}, FOLLOW(T)={+, ), $}, FOLLOW(T')={+, ), $}, FOLLOW(F)={*, +, ), $}.

Non-Terminalid+*()$
EE→TE'E→TE'
E'E'→+TE'E'→εE'→ε
TT→FT'T→FT'
T'T'→εT'→*FT'T'→εT'→ε
FF→idF→(E)

No cell has more than one entry, so this grammar is LL(1).

Q11Differentiate Top-Down Parsing and Bottom-Up Parsing.

Parsers construct a parse tree either starting from the root (start symbol) or from the leaves (input tokens), giving two broad parsing strategies.

AspectTop-Down ParsingBottom-Up Parsing
Tree constructionStarts from the start symbol (root) and expands towards the leaves (input).Starts from the input symbols (leaves) and reduces towards the start symbol (root).
Derivation producedLeftmost derivation.Reverse of a rightmost derivation.
Core operationPrediction/expansion of a nonterminal using a production.Shift input symbols, then reduce a handle (substring) to a nonterminal.
Grammar restrictionCannot handle left recursion; needs left factoring; grammar must typically be LL(1).Can handle left-recursive grammars; broader class (LR) accepted.
ExamplesRecursive descent parser, LL(1) predictive parser.Shift-reduce parser, LR(0), SLR(1), LALR(1), CLR(1) parsers.
Power/classWeaker; LL(1) is a subset of LR(1) grammars.Stronger; LR family accepts a larger class of unambiguous grammars.
BacktrackingMay need backtracking (unless restricted, e.g. predictive parsing avoids it).Generally no backtracking with a suitable parsing table.

Both approaches ultimately verify that the input string belongs to the language defined by the grammar, but differ in the order in which the parse tree nodes are constructed.

Q12Explain Shift-Reduce parsing mechanism with stack implementation.

Shift-reduce parsing is a bottom-up parsing technique that builds the parse tree from the leaves upward by repeatedly shifting input symbols onto a stack and, when the top of the stack matches the right-hand side of a production (a handle), reducing it to the corresponding left-hand nonterminal. Parsing succeeds if the stack is reduced to just the start symbol while all input is consumed.

It uses a stack (initially holding the end marker $) and a buffer of remaining input (terminated by $), and performs four possible actions at each step:

  • Shift: push the next input symbol onto the stack.
  • Reduce: replace a handle at the top of the stack with the nonterminal on the left side of the matching production.
  • Accept: announce successful completion when the stack contains only the start symbol and input is empty.
  • Error: report a syntax error when neither shift nor reduce can be applied.

Example: for grammar E -> E + E | E * E | id, parsing id + id * id proceeds as: shift id, reduce to E, shift +, shift id, reduce to E, stack is E + E, shift *, shift id, reduce to E, stack E + E * E, reduce E*E to E, stack E + E, finally reduce to E, then accept. The stack thus always holds a prefix of the right-sentential form (viable prefix) seen so far.

Q13Compare SLR(1), LALR(1), and Canonical LR(1) parsers.

SLR(1), LALR(1) and Canonical LR(1) (CLR(1)) are three bottom-up table-driven parsers built by constructing sets of LR items, differing in how items are formed and how lookahead information is used, which gives them increasing parsing power at increasing table-construction/memory cost: SLR(1) < LALR(1) < CLR(1).

AspectSLR(1)LALR(1)CLR(1)
Items usedLR(0) items (no lookahead in item itself).LR(1) items with same core merged.Full LR(1) items (production + lookahead).
Reduce decided usingFOLLOW(A) of the nonterminal being reduced.Merged, more precise lookahead sets per state.Item-specific lookahead symbol.
Number of statesFewest (same as LR(0) automaton).Same number of states as SLR (merges LR(1) states with identical cores).Largest number of states.
Parsing powerWeakest; most reduce-reduce/shift-reduce conflicts.Stronger than SLR; used by most practical parser generators (e.g. Yacc).Strongest; accepts the largest class of unambiguous grammars.
Memory/table sizeSmallest.Same size as SLR, much smaller than CLR.Largest, often impractically big for real languages.
Possible new conflictsMay introduce reduce-reduce conflicts CLR would not have.Can occasionally introduce a reduce-reduce conflict not present in CLR due to merging.None beyond genuine grammar ambiguity.
Q14Explain LR(0) item set construction and closure operation.

An LR(0) item is a production with a dot marking how much of the RHS has been recognized so far, e.g. A → α . β. Two functions build the canonical collection of item sets (states of the LR(0) automaton):

  • closure(I): repeatedly, for every item A → α . Bβ in I and every production B → γ, add item B → . γ to I (if not already present) until no more items can be added.
  • goto(I, X): = closure of the set of items A → αX . β for every item A → α . Xβ in I. Applying goto for every grammar symbol X to every state, starting from I0 = closure({S'→.S}), generates the whole canonical collection (the LR(0) automaton).

Worked example — augmented grammar S' → S, S → (S) | a:

StateItems
I0S'→.S, S→.(S), S→.a
I1 = goto(I0,S)S'→S. (accept)
I2 = goto(I0,()S→(.S), S→.(S), S→.a
I3 = goto(I0,a)S→a. (reduce)
I4 = goto(I2,S)S→(S.)
I5 = goto(I2,()= I2 (self loop)
I6 = goto(I2,a)= I3 (reused)
I7 = goto(I4,))S→(S). (reduce)
I0 I1 I3 I2 I4 I7 S a ( S ) (
LR(0) automaton (DFA of item sets) for S→(S)|a. I1 is the accepting state; I3 and I7 are reduce states.
Q15Explain Yacc file structure (Declarations, Rules, User C Code).

Yacc (Yet Another Compiler-Compiler) takes a grammar specification and generates a bottom-up (LALR(1)) parser in C. A Yacc source file (.y) is divided into three sections separated by %%, mirroring the structure of a Lex file:

%{ /* Declarations section: C declarations, header includes */ #include <stdio.h> %} /* Yacc declarations: tokens, precedence, start symbol */ %token NUM ID %left '+' '-' %left '*' '/' %start expr %% /* Rules section: grammar productions with semantic actions */ expr : expr '+' expr { $$ = $1 + $3; } | expr '*' expr { $$ = $1 * $3; } | NUM { $$ = $1; } ; %% /* User code section: auxiliary C functions */ int yylex(void) { /* usually supplied by Lex/flex */ } int main(void) { return yyparse(); } int yyerror(char *s) { printf("Error: %s ", s); return 0; }
  • Declarations: C code copied verbatim (between %{ %}) plus Yacc directives such as %token (terminal symbols), %left/%right/%nonassoc (precedence/associativity) and %start (start symbol).
  • Rules: grammar productions in BNF-like form; each alternative can have an embedded C action, executed on reduction, using $$ for the result and $1, $2, ... for the values of right-hand-side symbols.
  • User C Code: supporting routines, typically main(), yyerror() and, if not supplied by Lex, yylex().
Q16Explain Lex file structure (Definitions, Rules, User Functions).

Lex is a lexical analyzer generator that converts a specification of token patterns (regular expressions) into a C program (lex.yy.c) implementing a scanner. A Lex source file (.l) has three sections separated by %%:

%{ /* Definitions section: C declarations and includes */ #include "y.tab.h" %} /* Regular definitions (named patterns) */ DIGIT [0-9] LETTER [a-zA-Z] %% /* Rules section: pattern { action } pairs */ {DIGIT}+ { yylval = atoi(yytext); return NUM; } {LETTER}({LETTER}|{DIGIT})* { return ID; } [ ] { /* skip whitespace */ } . { return yytext[0]; } %% /* User functions section: auxiliary C code */ int yywrap(void) { return 1; }
  • Definitions: C declarations/includes inside %{ %} (copied verbatim to the output), plus optional named regular-expression shorthands (e.g. DIGIT, LETTER) used later in the rules section.
  • Rules: a list of pattern-action pairs; whenever the scanner matches a pattern against input, it executes the associated C action, typically returning a token code (often to a Yacc parser) with yylval/yytext carrying the matched lexeme.
  • User Functions: auxiliary C functions such as yywrap() and, when Lex is used standalone, main().
Q17Differentiate Syntax Directed Definition (SDD) and Syntax Directed Translation (SDT).

Both Syntax Directed Definitions and Syntax Directed Translations attach semantics to a context-free grammar by associating attributes and rules with grammar symbols and productions, but they differ in their level of abstraction and how strictly they specify evaluation order.

AspectSyntax Directed Definition (SDD)Syntax Directed Translation (SDT)
NatureHigh-level, declarative specification: a CFG with a set of semantic rules attached to each production.Grammar with semantic actions ({...}) embedded at specific positions within the production body.
Evaluation orderNot specified; rules just define attribute values, order is left to the implementation (any order consistent with dependencies).Implied by the position of the action in the right-hand side; actions execute exactly when parsing reaches that point.
Abstraction levelConceptual/specification tool, closer to a formal definition of translation.Implementation-oriented, closer to actual parser code.
ExampleProduction E -> E1 + T with rule E.val = E1.val + T.val (no ordering implied).Production E -> E1 + T {print('+')} T with the action fixed between E1+T and T's parsing.
Typical useDescribing translation abstractly, reasoning about correctness, attribute grammars.Directly guiding code generation in a single-pass parser (e.g. embedding in Yacc actions).

In practice an SDT is obtained by deciding a concrete evaluation order for an SDD and inserting the corresponding actions at appropriate points in the productions.

Q18Differentiate Synthesized Attributes and Inherited Attributes with examples.

Attributes carried by grammar symbols in a syntax directed definition are classified by the direction in which their values flow through the parse tree.

AspectSynthesized AttributeInherited Attribute
Value computed fromAttribute values of its own children (and possibly itself) in the parse tree.Attribute values of its parent and/or left siblings in the parse tree.
Flow directionBottom-up (leaves towards root).Top-down / left-to-right (root and left siblings towards a node).
Defined atThe production where the nonterminal is on the left-hand side (head).The production where the nonterminal appears on the right-hand side (body).
Evaluation order neededSingle bottom-up pass suffices; matches LR/bottom-up parsing naturally.Needs a specific top-down or left-to-right order; matches LL/recursive-descent parsing.
ExampleProduction E -> E1 + T with E.val = E1.val + T.val (E.val synthesized from children E1, T).Production D -> T L with L.inh = T.type (type declared in T is passed down to list L).

A parse tree annotated only with synthesized attributes can be evaluated bottom-up in one post-order traversal, while inherited attributes require care to avoid circular dependencies and are typically evaluated left-to-right, depth-first.

Q19Explain S-Attributed vs L-Attributed SDDs with parse tree evaluation.

SDDs are further classified by which evaluation orders they support, based on the mix of synthesized and inherited attributes they use.

  • S-attributed SDD: uses only synthesized attributes. Every attribute is computed from the attributes of the children, so the whole tree can be evaluated bottom-up in a single post-order traversal. S-attributed definitions fit naturally with bottom-up (LR) parsers, which can evaluate attributes on each reduction using a parallel value stack.
  • L-attributed SDD: allows both synthesized attributes and inherited attributes, with one restriction: each inherited attribute of a symbol on the right-hand side of a production may depend only on attributes of the production's head (parent) and on attributes of symbols to its left in the same production (never on symbols to its right). This restriction permits evaluation in a single left-to-right, depth-first traversal, so L-attributed definitions can be evaluated during both top-down (LL) parsing and, using a value stack, during bottom-up parsing as well.
D T L synth: T.type up inherited: L.inh down
D -> T L: T.type synthesizes upward to D; D passes it down as L.inh to sibling L, an L-attributed dependency (left-to-right, parent-and-left-sibling only).
Q20Explain intermediate code representations: Syntax Trees vs Postfix Notation.

Intermediate code represents a program in a form that is independent of both the source language and the target machine, easing optimization and retargeting. Two common tree-oriented/linear representations are syntax trees and postfix notation.

AspectSyntax Tree (DAG-like)Postfix Notation
StructureHierarchical tree; each interior node represents an operator, its children the operands.Linear sequence of operators and operands, no parentheses needed.
Operator positionOperator at the (interior) node, operands as its children.Operator written immediately after its operands (reverse Polish).
EvaluationRequires tree traversal (e.g. post-order) to evaluate/generate code.Directly evaluable left-to-right using a simple stack machine.
Sharing common subexpressionsA DAG variant can share identical subtrees, saving computation.No structural sharing; each occurrence is written out.
Example for a + a * (b - c)Tree with root +, left child leaf a, right child * with children a and (- with children b, c).a b c - * a +

A syntax tree makes the grammatical/hierarchical structure explicit and is convenient for semantic analysis and optimization (especially as a DAG, which exposes common subexpressions), while postfix notation is compact, parenthesis-free and directly executable by a simple stack-based evaluator, making it convenient for straightforward code generation.

Q21Explain Three-Address Code representations: Quadruples, Triples, and Indirect Triples.

A three-address code (TAC) statement has the general form x = y op z, using at most one operator and one assignment. It can be implemented in three ways that trade off memory versus ease of rearrangement during optimization.

RepresentationFields storedHow the result is referencedEffect of reordering statements
Quadruple(op, arg1, arg2, result)By an explicit temporary name in the result fieldEasy - statements can be freely moved since results are named
Triple(op, arg1, arg2)By the position/index number of the triple itselfHard - moving a triple changes its index, so every reference to it elsewhere must be updated
Indirect TripleAn ordered list of pointers into a triple arrayBy the pointer-list position, while the triple array stays fixedEasy - only the pointer list is reordered; the triple structures themselves never move

Example: for a = b * -c + b * -c, the TAC and its quadruple/triple forms are:

TAC: (1) t1 = uminus c (2) t2 = b * t1 (3) t3 = uminus c (4) t4 = b * t3 (5) t5 = t2 + t4 (6) a = t5 Quadruples Triples op arg1 arg2 result op arg1 arg2 uminus c - t1 (0) uminus c - * b t1 t2 (1) * b (0) uminus c - t3 (2) uminus c - * b t3 t4 (3) * b (2) + t2 t4 t5 (4) + (1) (3) = t5 - a (5) assign a (4)

Quadruples are simplest for code generation and optimization since operands and results are named explicitly, at the cost of extra temporary-name storage. Triples save space but complicate optimizations that reorder code. Indirect triples recover this flexibility by adding one level of indirection, combining the compactness of triples with the reorderability of quadruples.

Q22Explain SDT rules for Boolean Expressions evaluation.

Boolean expressions used in flow-of-control statements (if, while) are usually translated using jumping code instead of computing a 0/1 value: control simply jumps to one of two program locations depending on whether the expression is true or false, giving short-circuit evaluation. Each non-terminal E carries two backpatch lists, E.truelist and E.falselist, holding jump instructions whose target label is not yet known.

Production Semantic Action E -> E1 or M E2 backpatch(E1.falselist, M.instr) E.truelist = merge(E1.truelist, E2.truelist) E.falselist = E2.falselist E -> E1 and M E2 backpatch(E1.truelist, M.instr) E.truelist = E2.truelist E.falselist = merge(E1.falselist, E2.falselist) E -> not E1 E.truelist = E1.falselist E.falselist = E1.truelist E -> id1 relop id2 E.truelist = makelist(nextinstr) E.falselist = makelist(nextinstr + 1) emit('if' id1.place relop.op id2.place 'goto _') emit('goto _') E -> true E.truelist = makelist(nextinstr); emit('goto _') E -> false E.falselist = makelist(nextinstr); emit('goto _') M -> epsilon M.instr = nextinstr

The marker non-terminal M records the instruction number right after E1 so that or/and can backpatch E1's list to jump into E2's code, implementing short-circuiting without a separate fix-up pass over the whole expression.

Q23Explain Backpatching technique for boolean control flow expressions.

Backpatching is a one-pass code generation technique for boolean and flow-of-control constructs: jump instructions are emitted with their target left blank, the instruction number is remembered in a list, and once the real target label becomes known the blank is filled in ("patched"). This avoids a second pass to resolve forward jumps.

Three helper functions manage the lists of incomplete jumps:

  • makelist(i) - creates a new list containing only instruction number i (a single jump whose target is unknown).
  • merge(p1, p2) - concatenates the lists pointed to by p1 and p2, returning a pointer to the merged list; used to combine truelists/falselists from sub-expressions.
  • backpatch(list, target) - inserts the instruction number target into the target field of every jump instruction listed in list.
S -> if E then M S1 backpatch(E.truelist, M.instr) S.nextlist = merge(E.falselist, S1.nextlist) S -> if E then M1 S1 N else M2 S2 backpatch(E.truelist, M1.instr) backpatch(E.falselist, M2.instr) S.nextlist = merge(S1.nextlist, N.nextlist, S2.nextlist) S -> while M1 E do M2 S1 backpatch(S1.nextlist, M1.instr) backpatch(E.truelist, M2.instr) S.nextlist = E.falselist emit('goto' M1.instr) N -> epsilon N.nextlist = makelist(nextinstr); emit('goto _')

Because the actual target is patched only once the target statement's first instruction number is known, arbitrarily nested if/while/for statements with boolean conditions can be translated in a single left-to-right pass without backtracking.

Q24Explain Activation Record structure and stack allocation for subroutines.

Each call to a procedure creates an activation record (stack frame) holding all information needed for that single execution instance. Activation records are pushed onto a run-time stack on call and popped on return, which naturally supports recursion since each active call gets its own record.

Actual Parameters Return Address Control Link (dynamic) Access Link (static) Saved Registers Local Variables Temporaries stack grows downward
Layout of a single activation record on the run-time stack.

The control link points to the caller's activation record (used to restore the caller's frame on return); the access link points to the activation record of the lexically enclosing procedure, used to resolve non-local variable references in languages with nested procedures. A stack pointer and frame pointer register track the top and base of the current record so offsets into locals/temporaries can be computed at compile time.

Q25Explain Heap Memory Management and Dynamic Allocation strategies.

Data whose size or lifetime cannot be determined at compile time (e.g. objects created with new/malloc) is placed on the heap, a region of memory managed explicitly by allocation/deallocation routines rather than by the automatic call/return discipline of the stack.

The heap manager typically keeps a free list of unused blocks and services requests using one of these placement strategies:

  • First-fit - scan the free list and allocate from the first block large enough; fast but can leave many small unusable fragments near the start of the list.
  • Best-fit - scan the whole list and pick the smallest block that satisfies the request, minimizing leftover space but incurring the cost of a full scan and creating many tiny leftover slivers.
  • Worst-fit - allocate from the largest available block, leaving a large usable remainder rather than a sliver.
  • Buddy system - maintains free lists of blocks whose sizes are powers of two; a block is split into two equal "buddies" as needed and, on free, is merged back with its buddy if that buddy is also free, giving fast allocation/coalescing at the cost of internal fragmentation.

Two forms of fragmentation arise: internal fragmentation (allocated block larger than requested, wasting space inside it) and external fragmentation (enough total free memory exists but scattered in blocks too small individually to satisfy a request) - the latter is countered by coalescing adjacent free blocks and, where supported, by compaction that relocates live blocks together.

Q26Explain Garbage Collection algorithms: Mark-and-Sweep vs Copying Collector.

When a program uses heap allocation without explicit deallocation, the run-time system reclaims memory occupied by objects that are no longer reachable from the program's roots (globals, stack, registers) - this is garbage collection. Two classical algorithms are Mark-and-Sweep and Copying collection.

AspectMark-and-SweepCopying Collector
Working ideaMark phase traverses from roots marking every reachable object; sweep phase scans all of heap memory reclaiming unmarked blocksHeap is split into two semispaces (from-space, to-space); reachable objects are copied from from-space into to-space, then the roles of the spaces swap
Memory overheadUses the full heap for live dataOnly half the heap is usable at any time
FragmentationLeaves free blocks scattered where dead objects were - can fragment memoryCompacts automatically - all live objects end up contiguous in to-space, so allocation afterward is a simple pointer bump
Cost driverSweep phase cost is proportional to total heap size (touches dead objects too)Cost is proportional only to the volume of live data actually copied
Pointer updatesNot required - objects never moveAll pointers to a moved object must be updated (forwarding pointers)

Mark-and-sweep is attractive when most objects survive (little copying benefit), while copying collectors excel when most objects die young, as is common with short-lived temporaries - which is why many modern collectors use generational schemes combining both ideas.

Q27Explain algorithm for identifying Leaders and Basic Blocks in code.

Before local optimization, a program's three-address code must be partitioned into basic blocks: maximal sequences of consecutive instructions with a single entry point and single exit point, executed straight through with no jumps in or out except at the very start/end. This is done by first identifying leader statements.

Leader identification rules: 1. The first statement of the program is a leader. 2. Any statement that is the target of a conditional or unconditional goto is a leader. 3. Any statement that immediately follows a conditional or unconditional goto is a leader. Basic block construction: for each leader L: the basic block for L consists of L and every statement up to, but not including, the next leader (or end of program)

Once basic blocks are identified, a flow graph is built by adding an edge from block B1 to B2 whenever B2's leader can be reached immediately after executing the last statement of B1 - either by falling through (B1's last statement is not an unconditional jump) or by an explicit jump/branch target. This flow graph is the structure over which local optimizations (within a block) and global/loop optimizations (across blocks) are performed.

B1 (Entry) B2 (If Then) B3 (Else) B4 (Exit)
Control Flow Graph: B1 (entry, containing the leader/condition) branches to basic blocks B2 or B3, both of which merge into exit block B4.
Q28Construct Directed Acyclic Graph (DAG) for basic block expressions.

A Directed Acyclic Graph (DAG) for a basic block represents the block's computation while automatically detecting common sub-expressions: leaves are labeled with identifiers/constants, interior nodes are labeled with operators, and a node is created only once per distinct computation - if the same expression is generated again, the existing node is reused rather than duplicated.

+ a * b c
DAG for t1 = b * c; t2 = a + t1 (node * is reused if b*c recurs later in the block).

Construction algorithm: for each three-address statement x = y op z, look up nodes for y and z (creating leaf nodes if new); search for an existing interior node labeled op with those exact children - if found, reuse it, else create a new node; attach identifier x to that node's label list (removing x from any node it previously labeled). Applications of the DAG include eliminating common sub-expressions, detecting dead code (nodes with no live labels attached), and reordering statements that compute independent nodes for better register usage.

Q29Explain Local Code Optimization techniques (Constant Folding, Dead Code, Copy Propagation).

Local optimizations improve code within a single basic block without analyzing control flow across blocks. Three common techniques:

Constant folding - expressions with compile-time-known constant operands are evaluated at compile time and replaced by the result.

Before: t1 = 2 + 3 t2 = t1 * x After: t1 = 5 t2 = 5 * x

Dead code elimination - statements that compute a value never subsequently used (no live variable depends on it) are removed entirely.

Before: t1 = a + b -- t1 never used again x = a + b After: x = a + b

Copy propagation - after a copy statement x = y, later uses of x (before x or y is redefined) are replaced directly by y, which often exposes further dead code once the copy itself becomes unused.

Before: x = y z = x + 1 After: x = y z = y + 1 -- now "x = y" may be dead if x unused elsewhere

These passes are typically applied repeatedly (and combined with the DAG representation of the block) since each pass can expose new opportunities for the others - e.g. copy propagation feeding dead code elimination.

Q30Explain Loop Optimization techniques (Code Motion, Strength Reduction, Loop Unrolling).

Since instructions inside loops execute many times, optimizing loop bodies gives the largest payoff per line of code improved. Three standard loop optimizations:

Code motion (loop-invariant code motion) - computations inside a loop whose operands do not change across iterations are hoisted out to a preheader block executed once before the loop, so they are computed a single time instead of every iteration.

Before: After: while (i < n) { t = limit - 2; x = limit - 2; while (i < n) { a[i] = x; a[i] = t; i = i + 1; i = i + 1; } }

Strength reduction - replaces an expensive operation inside the loop with a cheaper equivalent, typically converting a multiplication driven by an induction variable into repeated addition.

Before: for i: t = i * 4; use(t) After: t = 0; for i: use(t); t = t + 4

Loop unrolling - replicates the loop body multiple times per iteration test, reducing the relative overhead of the loop-control test/branch and enabling better instruction scheduling.

Before: for (i=0;i

All three reduce dynamic instruction count in hot loops, which typically dominate a program's execution time.

Q31Explain Peephole Optimization techniques with 4 concrete examples.

Peephole optimization scans the generated (often target) code through a small sliding window ("peephole") of a few consecutive instructions and replaces recognized inefficient patterns with shorter or faster equivalents. It is applied repeatedly since one replacement can expose the next.

1. Redundant load/store elimination - a store immediately followed by a load of the same value from the same location is unnecessary.

Before: MOV R0, a MOV a, R0 After: MOV R0, a

2. Algebraic simplification - identity operations that don't change the value are removed.

Before: x = x + 0 y = y * 1 After: (statements removed)

3. Strength reduction - cheap machine instructions replace expensive ones for the same effect.

Before: x = x * 2 After: x = x + x -- or: x = x << 1

4. Unreachable code / jump-to-jump elimination - code after an unconditional jump/return (never reached) is deleted, and a jump to another jump is collapsed to jump straight to the final target.

Before: goto L1 ... (unreachable) L1: goto L2 After: goto L2

Because it works on a small local window, peephole optimization is cheap to run yet consistently removes redundancy left behind by naive syntax-directed code generation.

Q32Explain Issues in Code Generation phase (Register Allocation, Instruction Selection).

The code generator turns intermediate representation into target machine code while trying to produce fast, compact code; several interacting issues make this a genuinely hard combinatorial problem:

  • Instruction selection - choosing which machine instruction(s) implement a given IR operation; the same computation can often be done several ways (e.g. multiply-by-power-of-2 as a shift), and CISC-style machines add complexity through instructions that combine several IR operations (e.g. an addressing mode that does load+add).
  • Register allocation and assignment - deciding which values are kept in the limited set of physical registers (allocation) and which specific register each gets (assignment); poor decisions force values to be spilled to memory, adding load/store overhead.
  • Evaluation order - the order in which independent computations are scheduled affects the number of registers needed simultaneously and can affect pipeline/instruction-level parallelism on the target.
  • Choice of addressing modes and memory management - exploiting indexed, indirect, or auto-increment addressing modes can shorten code, but requires matching IR access patterns to what the target supports.
  • Target machine characteristics - instruction set, pipeline behavior, and cost/timing of each instruction all constrain what a "good" translation looks like, so the generator must be tailored to (or parameterized by) the target architecture.

These issues are interdependent - e.g. instruction selection affects how many registers are needed - which is why practical code generators (or separate optimization passes) address them together rather than in complete isolation.

Q33Explain Register Allocation using Chaitin's Graph Coloring approach.

Chaitin's algorithm allocates a fixed number k of physical registers to program values via graph coloring: build an interference graph whose nodes are live ranges (variables/temporaries) and whose edges connect any two live ranges simultaneously live at some point (so they cannot share a register); then find a k-coloring of this graph, where a color represents a register.

a b c d
Interference graph 2-colored with 2 registers (green/red); a and d share a color, b and c share the other, since neither pair interferes.

Algorithm outline:

  1. Build the interference graph from live-range analysis.
  2. Simplify - repeatedly remove any node with degree less than k (it is trivially colorable later) and push it onto a stack.
  3. Spill decision - if every remaining node has degree >= k, optimistically pick a node as a potential spill candidate (using a cost heuristic - low use/def frequency, high degree), remove it, and continue simplifying.
  4. Select - pop nodes off the stack one by one, assigning each a register color different from its already-colored neighbors; a node marked as a potential spill may still get a color if one happens to be free.
  5. Actual spill code - only if select genuinely fails to find a free color is the value spilled to memory, and the process (build/simplify/select) is repeated on the rewritten program.
Q34Explain Syntax Error Recovery methods: Panic Mode vs Phrase-Level.

When the parser encounters a token that cannot be legally derived at that point, an error-recovery strategy lets compilation continue (to catch further errors in one pass) instead of stopping at the first mistake. Two common strategies:

AspectPanic Mode RecoveryPhrase-Level Recovery
Basic ideaDiscard input tokens one at a time until a designated synchronizing token (e.g. ; or end) is found, then resume parsingPerform a local, context-specific fix at the exact point of error - insert, delete, or replace a token
ImplementationSimple - just needs a set of synchronizing tokens per non-terminalRequires filling every error entry in the parsing table with a specific corrective routine
PrecisionCoarse - may skip over other real tokens/errors, sometimes causing cascading errorsFine-grained - tailored fix for common mistakes (e.g. missing semicolon, unbalanced parenthesis)
RiskLow risk of infinite loop but can lose significant contextMust be designed carefully to avoid infinite loops of insert/delete
GuaranteeNever causes an infinite loop; always terminates since tokens are only ever discardedCannot introduce infinite loops if corrective actions are chosen properly

Compilers frequently combine both: phrase-level fixes for well-known common mistakes, falling back to panic mode synchronization for anything unrecognized, so error messages stay precise while parsing always terminates.

Q35Explain Static Scope vs Dynamic Scope variable resolution.

Scope rules determine which declaration a variable reference is bound to. The two disciplines differ in when that binding is decided.

AspectStatic (Lexical) ScopeDynamic Scope
Binding timeDetermined at compile time, from the program's textual nesting structureDetermined at run time, from the actual sequence of calls in execution
Resolution ruleA reference binds to the declaration in the nearest lexically enclosing block/procedureA reference binds to the most recent still-active binding on the call stack, regardless of textual location
Implementation aidAccess links / a static scope chain built from the program's nestingThe dynamic call chain itself (control links), or a runtime association list
PredictabilitySame variable reference always resolves the same way; easy to reason about and optimize at compile timeMeaning of a reference can change depending on the caller, harder to reason about statically
Used byMost modern languages - C, Java, PascalEarly Lisp, APL, and shell/macro-like environments
int x = 1; void f() { print(x); } void g() { int x = 2; f(); } main: g(); Static scope -> prints 1 (f's x is the global) Dynamic scope -> prints 2 (f's x is g's x, most recent binding)
Q36Explain Hash Table symbol table organization with scope chaining.

A compiler's symbol table stores each identifier's attributes (type, scope, offset, etc.) and must support fast insert and lookup as well as correct scoping. A common efficient organization is a hash table with scope chaining: identifier names are hashed into buckets, and entries for identifiers declared in nested scopes are chained so that inner-scope declarations shadow outer ones.

h(x) h(y) h(z) x (scope 2) x (scope 0) z (scope 1) (empty bucket)
Hash buckets chain to entries; entries for the same name across nested scopes are further chained, innermost first.

On entering a new scope, a fresh scope marker/level is pushed; declarations insert new entries at the head of their name's chain, tagged with the current scope level. A lookup follows the hash bucket's chain and returns the first (innermost, most recently declared) matching entry, correctly implementing shadowing. On leaving a scope, all entries tagged with that scope level are removed (or a scope-boundary marker is popped), restoring visibility of the outer declarations - giving average O(1) insert/lookup with correct nested-scope resolution.

Q37Explain Type Checking and Type Conversions (Implicit vs Explicit).

Type checking verifies that operations are applied to operands of compatible types, using the static or inferred types recorded in each expression's syntax tree/symbol table entries. When operand types don't match an operator's exact required types but are still compatible, a type conversion (coercion) is inserted.

AspectImplicit ConversionExplicit Conversion
Who triggers itInserted automatically by the compiler during type checkingWritten directly by the programmer using a cast operator
Exampleint i; float f = i; - i is widened to float automaticallyfloat f; int i = (int) f; - explicit cast requested
SafetyCompiler only inserts safe, information-preserving widening conversions (e.g. int to float)Can be narrowing/lossy (e.g. float to int truncates); programmer takes responsibility
VisibilityInvisible in source code, visible only in the annotated syntax tree/generated codeVisible in source code as an explicit cast expression

During semantic analysis the compiler applies coercion rules bottom-up on the expression tree: if an operator expects type T and the actual operand has a type coercible to T, a conversion node is inserted around that operand before code generation, so the emitted code always operates on matching types.

Q38Explain Static Single Assignment (SSA) form advantages.

Static Single Assignment (SSA) is an intermediate representation property in which every variable in the program is assigned a value exactly once (textually) - each new assignment to a source variable creates a fresh SSA name (e.g. x becomes x1, x2, x3...). Where control-flow paths carrying different definitions of the same original variable merge, a special phi (Φ) function is inserted at the join point to select the correct incoming value based on which predecessor block control arrived from.

Original: SSA form: if (c) x = 1; if (c) x1 = 1; else x = 2; else x2 = 2; y = x + 1; x3 = phi(x1, x2); y1 = x3 + 1;

Because every use in SSA has exactly one reaching definition, def-use chains become explicit and trivial to compute, which greatly simplifies several optimizations:

  • Constant propagation - a use trivially inherits the single definition's value if it is constant.
  • Dead code elimination - a definition with no uses can be deleted immediately, without a separate liveness pass.
  • Value numbering / common sub-expression elimination - identical SSA expressions are guaranteed to compute identical values, so redundancy detection is direct.

This is why most modern optimizing compilers (LLVM, GCC's GIMPLE-SSA) build SSA as their central optimization IR before eventually converting back out of SSA for code generation.

Q39Explain Data Flow Analysis concepts: Reaching Definitions and Live Variables.

Data flow analysis computes, for each program point, facts that hold on every possible execution path reaching (or leaving) that point, by solving equations over the control-flow graph. Two classic instances:

Reaching Definitions (a forward problem) - a definition d of variable v "reaches" a point p if there is some path from d to p along which v is not redefined. Used for constant propagation and to build def-use chains.

OUT[B] = GEN[B] U (IN[B] - KILL[B]) IN[B] = Union of OUT[P] for all predecessors P of B GEN[B] = definitions generated inside B that reach B's end KILL[B] = all other definitions of the same variables defined in B (killed because B redefines them)

Live Variables Analysis (a backward problem) - a variable v is live at point p if there exists a path from p to some use of v along which v is not redefined first. Used for register allocation (a dead variable's register can be reused) and dead-code elimination.

IN[B] = USE[B] U (OUT[B] - DEF[B]) OUT[B] = Union of IN[S] for all successors S of B USE[B] = variables used in B before any local redefinition DEF[B] = variables definitely assigned in B before any use

Both are solved by iterating over the flow graph until the IN/OUT sets reach a fixed point - reaching definitions propagates information forward from entry, live variables propagates it backward from exit.

Q40Explain Retargetable Compilers architecture (e.g. GCC/LLVM).

A retargetable compiler can generate code for a new target machine by replacing only its back end, without rewriting the front end or optimizer. This is achieved by funneling all source languages through a single, machine-independent intermediate representation (IR), keeping every machine-specific detail (instruction patterns, register classes, calling conventions) isolated behind a well-defined backend interface.

AspectGCCLLVM
Core IRRTL (Register Transfer Language) - a low-level, machine-like IR close to actual instructions, plus the higher-level GIMPLE for optimizationLLVM IR - a single typed, SSA-based IR shared uniformly by every frontend and backend
Target descriptionPer-target machine-description (.md) files specifying instruction patterns, costs, and constraints, processed to build the backendTableGen (.td) target-description files feeding a common backend framework (instruction selection DAG, register allocator, scheduler)
Pipeline shapeFrontend -> GIMPLE optimizations -> RTL -> target-specific RTL passes -> assemblyFrontend (e.g. Clang) -> LLVM IR -> shared "opt" optimization passes -> target-specific "llc" backend -> assembly
Adding a new targetWrite a new machine-description file and target-specific RTL expandersWrite a new backend (TableGen descriptions + target-specific lowering) targeting the existing LLVM IR

In both designs, retargetability comes from the same principle: a common, machine-independent representation and optimizer sit in the middle, and only a well-isolated backend module needs to change to support a new instruction set architecture.

Group C — Long / Numerical Questions (15 Marks Each)

Q1a) Construct Canonical LR(1) items and CLR(1) Parsing Table for grammar: S ➔ A A A ➔ a A | b b) Trace the parsing of input string 'aabb$' showing stack, input buffer, and actions.

Part (a): Canonical LR(1) construction. Augmented grammar and productions numbered:

(0) S' → S (1) S → A A (2) A → a A (3) A → b

LR(1) items carry a lookahead. Applying closure/goto (closure rule: for item [A→α.Bβ, a], add [B→.γ, b] for every b in FIRST(βa)) produces 10 canonical states:

StateItems (production, lookahead)
I0[S'→.S,$] [S→.AA,$] [A→.aA,a/b] [A→.b,a/b]
I1=goto(I0,S)[S'→S.,$] — accept
I2=goto(I0,A)[S→A.A,$] [A→.aA,$] [A→.b,$]
I3=goto(I0,a)[A→a.A,a/b] [A→.aA,a/b] [A→.b,a/b]
I4=goto(I0,b)=goto(I3,b)[A→b.,a] [A→b.,b]
I5=goto(I2,A)[S→AA.,$]
I6=goto(I2,a)[A→a.A,$] [A→.aA,$] [A→.b,$]
I7=goto(I2,b)=goto(I6,b)[A→b.,$]
I8=goto(I3,A)[A→aA.,a] [A→aA.,b]
I9=goto(I6,A)[A→aA.,$]

goto(I3,a)=I3 and goto(I6,a)=I6 (self loops). The CLR(1) ACTION/GOTO table (r1: S→AA, r2: A→aA, r3: A→b):

Stateab$SA
0s3s412
1accept
2s6s75
3s3s48
4r3r3
5r1
6s6s79
7r3
8r2r2
9r2

No cell has more than one action — the grammar is CLR(1) with no shift-reduce or reduce-reduce conflicts.

Part (b): Trace of "aabb$".

StepStackInputAction
10aabb$shift, goto 3
20 a 3abb$shift, goto 3
30 a 3 a 3bb$shift, goto 4
40 a 3 a 3 b 4b$reduce A→b, goto(3,A)=8
50 a 3 a 3 A 8b$reduce A→aA, goto(3,A)=8
60 a 3 A 8b$reduce A→aA, goto(0,A)=2
70 A 2b$shift, goto 7
80 A 2 b 7$reduce A→b, goto(2,A)=5
90 A 2 A 5$reduce S→AA, goto(0,S)=1
100 S 1$accept

The two reduced A's are "aab" and "b", concatenating to "aabb" = the input string, confirming the trace is correct.

Q2a) Generate Three-Address Code, Quadruples, Triples, and Indirect Triples for statement: while (a < b) do if (c < d) then x = y + z; else x = y - z; b) Draw the Control Flow Graph (CFG) and identify Basic Blocks.

Part (a): TAC and three-address representations. Three-address code for the statement (with statement numbers as labels):

1: t1 = a < b 2: ifFalse t1 goto 11 3: t2 = c < d 4: ifFalse t2 goto 8 5: t3 = y + z 6: x = t3 7: goto 1 8: t4 = y - z 9: x = t4 10: goto 1 (11: -- next statement / loop exit)
#Quadruple (op,arg1,arg2,result)Triple (op,arg1,arg2)
0<, a, b, t1<, a, b
1ifFalse, t1, _, 11ifFalse, (0), 11
2<, c, d, t2<, c, d
3ifFalse, t2, _, 8ifFalse, (2), 8
4+, y, z, t3+, y, z
5:=, t3, _, x:=, x, (4)
6goto, _, _, 0goto, (0), _
7-, y, z, t4-, y, z
8:=, t4, _, x:=, x, (7)
9goto, _, _, 0goto, (0), _

Quadruples name a temporary explicitly in the result field, so operands can be reordered freely at optimization time. Triples instead refer to another triple's result by its position/index (i), so moving a triple invalidates every reference to it; Indirect Triples fix this by adding a separate list of pointers into the triple table representing execution order — code motion during optimization then only reorders the pointer list, leaving the triple table itself untouched.

Part (b): Control Flow Graph and Basic Blocks. Applying the leader rules (first statement; targets of any goto/conditional goto; statement immediately after a goto/conditional goto) to the 10 TAC statements gives leaders {1,3,5,8}, hence 4 basic blocks:

BlockStatementsContent
B11-2t1=a<b; ifFalse t1 goto exit
B23-4t2=c<d; ifFalse t2 goto B4
B35-7t3=y+z; x=t3; goto B1
B48-10t4=y-z; x=t4; goto B1
B1 (while test) B2 (if test) B3 (then) B4 (else) EXIT
CFG: B1→B2 or EXIT; B2→B3(then) or B4(else); B3 and B4 both loop back to B1.
Q3a) Compute First and Follow sets for grammar: E ➔ T E' E' ➔ + T E' | ε T ➔ F T' T' ➔ * F T' | ε F ➔ ( E ) | id b) Construct LL(1) Parsing Table and trace parsing of 'id + id * id$'.

Part (a): FIRST and FOLLOW sets.

E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id
SymbolFIRSTFOLLOW
E{ (, id }{ ), $ }
E'{ +, ε }{ ), $ }
T{ (, id }{ +, ), $ }
T'{ *, ε }{ +, ), $ }
F{ (, id }{ *, +, ), $ }

Part (b): LL(1) table and trace of "id + id * id $".

NTid+*()$
EE→TE'E→TE'
E'E'→+TE'E'→εE'→ε
TT→FT'T→FT'
T'T'→εT'→*FT'T'→εT'→ε
FF→idF→(E)
StepStack (bottom→top)Remaining InputAction
1$Eid+id*id$E→TE'
2$E'Tid+id*id$T→FT'
3$E'T'Fid+id*id$F→id
4$E'T'idid+id*id$match id
5$E'T'+id*id$T'→ε
6$E'+id*id$E'→+TE'
7$E'T++id*id$match +
8$E'Tid*id$T→FT'
9$E'T'Fid*id$F→id
10$E'T'idid*id$match id
11$E'T'*id$T'→*FT'
12$E'T'F**id$match *
13$E'T'Fid$F→id
14$E'T'idid$match id
15$E'T'$T'→ε
16$E'$E'→ε
17$$accept
Q4a) Construct DAG for the basic block: t1 = 4 * i t2 = a[t1] t3 = 4 * i t4 = b[t3] t5 = t2 + t4 t6 = prod + t5 prod = t6 t7 = i + 1 i = t7 b) Derive optimized 3AC code from constructed DAG.

Part (a): DAG construction. Scanning the basic block, t1=4*i and t3=4*i compute the identical expression (same operator, same operand values, no intervening redefinition of i) so they collapse onto one shared DAG node with two labels:

i 4 * t1,t3 a b [ ] t2 [ ] t4 + t5 prod0 + t6,prod
DAG for the block: the 4*i node is shared by t1 and t3 (common subexpression); the final + node is labeled both t6 and prod (its live value).

Part (b): Optimized 3AC derived from the DAG. Reading the DAG in a valid topological (bottom-up) order and emitting one instruction per distinct node, using each node's surviving live label directly, eliminates the recomputation of t3 and the redundant copies via t6/t7:

t1 = 4 * i t2 = a[t1] t4 = b[t1] // reuses t1, t3 eliminated entirely t5 = t2 + t4 prod = prod + t5 // t6 eliminated, assigned straight into prod i = i + 1 // t7 eliminated, assigned straight into i

This reduces the block from 9 three-address statements to 6, removing exactly the two dead temporaries (t3, and the extra copies via t6/t7) that the DAG's shared-node structure exposed.

Q5a) Explain Chaitin's Graph Coloring Algorithm for Register Allocation. b) Construct Interference Graph and allocate 3 registers for overlapping variable lifetimes. c) Explain register spilling mechanism.

Part (a): Chaitin's algorithm. (1) Build an interference graph: one node per live range/variable, an edge between two nodes if they are simultaneously live at some program point. (2) Simplify: repeatedly remove (push onto a stack) any node with degree < k (k = number of available registers) since it is trivially colorable once its neighbors are colored. (3) If stuck (all remaining nodes have degree ≥k), optimistically pick a spill candidate (lowest spill-cost/degree ratio), mark it and push it anyway. (4) Select: pop the stack, assigning each node a register/color different from its already-colored neighbors; if a marked spill candidate genuinely cannot get a color, generate spill code (store after definition, reload before each use) and rebuild the graph.

Part (b): Worked interference graph, k=3 registers. Consider: a=10; b=20; c=a+b; d=a-b; e=c+d; print(e). Live ranges: a:[I1-I4], b:[I2-I4], c:[I3-I5], d:[I4-I5], e:[I5-I6]. By the standard convention a variable does not interfere with the destination of the statement that consumes it as its very last use, giving interference edges (a,b), (a,c), (b,c), (c,d); d and e end up isolated from a/b and c/d respectively beyond the one edge shown.

a b c d e
Interference graph: {a,b,c} form a triangle (clique of 3), d connects only to c, e is isolated.

Simplify order (remove lowest-degree first): e (deg 0), d (deg 1), a (deg 2), b (deg 1), c (deg 0) — pushed in that order onto the stack. Select (pop in reverse): c→R1 (no colored neighbor); b→R2 (avoids c=R1); a→R3 (avoids b=R2, c=R1); d→R2 (avoids c=R1 only); e→R1 (no neighbors).

Variableabcde
RegisterR3R2R1R2R1

Since the graph's largest clique has size 3 = k, all 5 variables are colored with exactly 3 registers and no spill is required.

Part (c): Register spilling. If simplify gets stuck (every remaining node has degree ≥ k) a node is optimistically marked as a potential spill and pushed anyway; during select, if it truly cannot be given a free color, the variable is spilled: it is kept in memory, a store is emitted immediately after its definition and a fresh load is emitted immediately before each use, splitting its live range into many short ranges that are individually easy to color, at the cost of extra memory traffic.

Q6a) Explain SDT for Type Checking and implicit type conversions. b) Write complete SDD rules for generating type-converted arithmetic AST nodes.

Part (a): Syntax-Directed Translation (SDT) for type checking attaches semantic type attributes to grammar symbols and computes them with rules tied to each production. Every expression non-terminal carries a synthesized attribute type which is computed bottom-up from the types of its children. A type checker uses two building blocks: (1) a table of basic type compatibility (e.g. int op int -> int, real op real -> real) and (2) a set of coercion (implicit conversion) rules that say which types may be automatically widened to a common type when the operand types differ. In most languages, only widening conversions (int -> real, char -> int) are performed implicitly; a narrowing conversion (real -> int) requires an explicit cast because it can lose information. When the type checker finds E1 op E2 with different types, instead of rejecting the program it inserts a unary conversion operator such as inttoreal around the narrower operand's AST node, producing a new synthesized type for the whole expression. This is exactly how languages like C and Java silently promote int to double in mixed arithmetic. Type checking and conversion insertion therefore happen together, in the same pass, as the parse tree (or an equivalent syntax-directed evaluation during LR parsing) is built or walked.

Part (b): The SDD below defines type and node (the AST pointer) synthesized attributes for arithmetic expressions, inserting explicit conversion nodes wherever the operand types differ.

Production Semantic Rule ------------------------- ------------------------------------------------------ E -> num E.type := int E.node := mkleaf(num, num.val) E -> realnum E.type := real E.node := mkleaf(realnum, realnum.val) E -> id E.type := lookup(id.entry) E.node := mkleaf(id, id.entry) E -> E1 + E2 if E1.type = int and E2.type = int then E.type := int E.node := mknode('+', E1.node, E2.node) elseif E1.type = real and E2.type = real then E.type := real E.node := mknode('+', E1.node, E2.node) elseif E1.type = int and E2.type = real then E.type := real E.node := mknode('+', mknode('inttoreal', E1.node), E2.node) elseif E1.type = real and E2.type = int then E.type := real E.node := mknode('+', E1.node, mknode('inttoreal', E2.node)) else E.type := type_error E -> E1 * E2 /* identical widening rules as '+' above, operator '*' */ E -> E1 - E2 /* identical widening rules as '+' above, operator '-' */ E -> E1 / E2 /* division always widens both operands to real: E.type := real E.node := mknode('/', coerce(E1), coerce(E2)) */ E -> ( E1 ) E.type := E1.type E.node := E1.node

Here coerce(Ei) denotes wrapping Ei.node in an inttoreal unary AST node only if Ei.type = int, otherwise returning Ei.node unchanged. Each conversion node is later translated by the code generator into an explicit CVTI2R (integer-to-real) machine instruction, so the widening that was implicit at the source level becomes explicit in the intermediate/target code.

Q7a) Explain Lex/Flex tool file structure in detail. b) Write complete Lex program to count keywords, identifiers, integers, floats, and line numbers from C source code.

Part (a): A Lex/Flex source file (conventionally .l) is divided into three sections separated by the delimiter %%:

  • Definitions section: everything before the first %%. C declarations, global variables and header includes are written inside %{ ... %} markers and are copied verbatim into the generated lex.yy.c. Below that, regular-expression name definitions (e.g. DIGIT [0-9]) may be given so later rules can reuse them as {DIGIT}.
  • Rules section: between the first and second %%. Each line is a pair pattern  action where pattern is a regular expression and action is a fragment of C executed whenever the longest input text matching that pattern is found (maximal-munch / longest-match rule; ties broken by rule order). The matched text is available in the global char *yytext and its length in int yyleng.
  • User code section: everything after the second %%, copied verbatim - normally contains main() (which calls yylex()) and any helper functions such as a manual yywrap().

Flex compiles the rules into a single DFA-driven scanning loop; when run it repeatedly calls the internal matcher, and for every token recognized it executes the associated action and then resumes scanning, until end of input (yywrap() returning 1) is reached.

Part (b): A complete Lex program that scans C source and counts keywords, identifiers, integer constants, floating-point constants, and source lines:

%{ #include <stdio.h> #include <string.h> int kw_count = 0, id_count = 0, int_count = 0, float_count = 0, line_count = 1; char *keywords[] = {"int","float","char","double","if","else","for","while", "return","void","break","continue","switch","case","do", "struct","union","typedef","static","const","long","short"}; int nkw = 22; int is_keyword(char *s){ int i; for (i = 0; i < nkw; i++) if (strcmp(s, keywords[i]) == 0) return 1; return 0; } %} DIGIT [0-9] LETTER [a-zA-Z_] ID {LETTER}({LETTER}|{DIGIT})* INT {DIGIT}+ FLOAT {DIGIT}+"."{DIGIT}+([eE][+-]?{DIGIT}+)? %% {ID} { if (is_keyword(yytext)) kw_count++; else id_count++; } {FLOAT} { float_count++; } {INT} { int_count++; } { line_count++; } [ ]+ { /* skip whitespace */ } . { /* skip everything else: operators, punctuation */ } %% int yywrap(void) { return 1; } int main(int argc, char **argv) { if (argc > 1) { FILE *f = fopen(argv[1], "r"); if (!f) { perror("fopen"); return 1; } yyin = f; } yylex(); printf("Keywords : %d ", kw_count); printf("Identifiers : %d ", id_count); printf("Integers : %d ", int_count); printf("Floats : %d ", float_count); printf("Lines : %d ", line_count); return 0; }

Compiled with flex scanner.l && gcc lex.yy.c -lfl -o scanner. The order of rules matters: {FLOAT} is placed before {INT} so a token like 3.14 is not first partially matched as the integer 3.

Q8a) Explain Yacc/Bison parser generator structure and precedence rules. b) Write complete Yacc specification for an infix Arithmetic Calculator supporting +, -, *, /, and parentheses.

Part (a): A Yacc/Bison specification (.y) has the same three-section layout as Lex: a declarations section (before the first %%) declares terminal tokens with %token, non-terminal result types with %type, and, crucially, precedence and associativity with lines such as %left '+' '-' followed by %left '*' '/' - operators declared on a later line bind tighter, and the keyword (%left, %right, %nonassoc) fixes how a chain of same-precedence operators associates. This lets an otherwise ambiguous grammar such as E : E '+' E | E '*' E be resolved automatically by the LALR table generator without needing to rewrite the grammar into unambiguous non-terminals for every precedence level. The grammar rules section (between the two %%) lists each production together with a semantic action in braces, where $$ denotes the value (attribute) of the left-hand non-terminal and $1, $2, ... denote the values of the right-hand symbols in order; Yacc builds a push-down automaton with a state/action table (shift, reduce, accept, error) from these rules, resolving shift-reduce conflicts using the declared precedence of the token about to be shifted versus the precedence of the rule about to be reduced. The final C code section supplies yylex() (usually generated by a companion Lex file), main(), and yyerror().

Part (b): A complete Yacc specification for an infix arithmetic calculator:

/* calc.y */ %{ #include <stdio.h> #include <stdlib.h> int yylex(void); void yyerror(const char *s); %} %token NUMBER %left '+' '-' %left '*' '/' %right UMINUS %type <dval> expr %union { double dval; } %token <dval> NUMBER %% program : expr ' ' { printf("Result = %g ", $1); } ; expr : expr '+' expr { $$ = $1 + $3; } | expr '-' expr { $$ = $1 - $3; } | expr '*' expr { $$ = $1 * $3; } | expr '/' expr { if ($3 == 0) { yyerror("divide by zero"); $$ = 0; } else $$ = $1 / $3; } | '-' expr %prec UMINUS { $$ = -$2; } | '(' expr ')' { $$ = $2; } | NUMBER { $$ = $1; } ; %% void yyerror(const char *s) { fprintf(stderr, "Error: %s ", s); } int main(void) { return yyparse(); }

Companion lexer (calc.l) that supplies NUMBER tokens:

%{ #include "y.tab.h" %} %% [0-9]+(\.[0-9]+)? { yylval.dval = atof(yytext); return NUMBER; } [-+*/() ] { return yytext[0]; } [ ] { /* skip */ } . { /* ignore */ } %% int yywrap(void) { return 1; }

Built with yacc -d calc.y && lex calc.l && gcc y.tab.c lex.yy.c -o calc. The %left/%right declarations make *,/ bind tighter than +,-, and %prec UMINUS gives unary minus the highest precedence, correctly parsing expressions like 2+3*4 and -2*(3+4).

Q9a) Explain ICG for 2D Array References `A[i][j]` in Row-Major and Column-Major order. b) Derive address calculation formula and generate 3AC for assignment `x = A[i][j]`.

Part (a): Address calculation for A[i][j]. For an array with row bounds [low1..high1], column bounds [low2..high2], n2 = number of columns = high2-low2+1, and element width w:

  • Row-major order (rows stored contiguously, used by C/C++/Java): addr(A[i][j]) = base + ((i-low1)*n2 + (j-low2)) * w. Row-major visits an entire row before moving to the next, so the rightmost index j varies fastest.
  • Column-major order (used by FORTRAN): with n1 = number of rows, addr(A[i][j]) = base + ((j-low2)*n1 + (i-low1)) * w. Here the leftmost index i varies fastest.

Part (b): 3AC for x = A[i][j] (row-major, 0-based indices, so low1=low2=0). A compile-time constant c = base - (low1*n2+low2)*w is precomputed so no subtraction is needed at run time:

t1 = i * n2 t2 = t1 + j t3 = t2 * w t4 = c[t3] // c is the constant base offset; t4 = *(c + t3) x = t4

This is exactly the standard "address descriptor" pattern used by real compilers: the multiply by n2 and the final scale by w are the only run-time arithmetic operations, with the bounds-dependent constant folded away at compile time.

Q10a) Explain Next-Use Information calculation and Register Descriptor algorithms. b) Trace register descriptors for code segment `x = (a - b) + (a - c) + (a - b)`.

Part (a): Next-use / register descriptor algorithm. Scanning a basic block backward, for statement i: x = y op z: (1) attach to statement i the current liveness/next-use info of x, y, z (as known from statements below i); (2) in the symbol table, set x to "not live, no next-use" (it is about to be (re)defined going forward); (3) set y and z to "live, next-use = i". Forward code generation then uses getReg(): prefer an already-empty register; else reuse a register whose current value is dead (no next use and not live-out); else spill the value with the least-urgent next use to memory.

Part (b): Trace for x = (a-b) + (a-c) + (a-b). TAC: t1=a-b; t2=a-c; t3=t1+t2; t4=a-b; x=t3+t4.

StmtNext-use attached (before update)
1: t1=a-bt1:(dead,-) a:(live,2) b:(live,4)
2: t2=a-ct2:(dead,-) a:(live,4) c:(live,-)
3: t3=t1+t2t3:(dead,-) t1:(live,3) t2:(live,3)
4: t4=a-bt4:(dead,-) a:(live,-) b:(live,-)
5: x=t3+t4x:(live,-) t3:(live,5) t4:(live,5)

Using 2 registers R0,R1 (a,b,c stay in memory, never needing a permanent register since operations can read a memory operand directly):

StmtEmitted codeR0R1
1MOV R0,a | SUB R0,bt1-
2MOV R1,a | SUB R1,ct1t2
3ADD R0,R1 (t1,t2 both dead after use → reuse R0)t3free
4MOV R1,a | SUB R1,bt3t4
5ADD R0,R1 | MOV x,R0xfree

Because next-use correctly showed t1 and t2 die at statement 3, R0 was safely reused for t3 instead of spilling — the whole expression is evaluated with only 2 registers and zero spills.

Q11a) Explain Global Data Flow Analysis and Iterative Data Flow Equations. b) Derive Reaching Definitions equations (In[B], Out[B], Gen[B], Kill[B]) and solve until convergence.

Part (a): Global data flow analysis computes, for every basic block in a program's control flow graph (CFG), facts that depend on paths through the whole procedure rather than on a single block in isolation (e.g. "which assignments to a variable might still be live/valid at this point"). Because a CFG can contain cycles (loops), these facts cannot be computed in one top-down or bottom-up sweep; instead they are expressed as data flow equations relating each block's input set to its predecessors'/successors' output sets, and solved iteratively: initialize all sets (commonly to the empty set), then repeatedly recompute every block's IN/OUT using the equations until no set changes between two consecutive passes - this is the fixed point, guaranteed to exist and to be reached in a bounded number of iterations because the sets only grow (for "may" analyses) or shrink (for "must" analyses) monotonically over a finite universe of facts (a lattice with finite height).

Part (b): For Reaching Definitions (a forward, "may"/union analysis) the equations are:

GEN[B] = definitions generated (created) inside B and not later killed within B KILL[B] = all other definitions, anywhere else in the program, of the same variables that B defines OUT[B] = GEN[B] U ( IN[B] - KILL[B] ) IN[B] = U OUT[P] for every predecessor P of B (IN[entry] = {})

Illustrative CFG (4 blocks): B1 (entry): d1: a=1; then B2: d2: b=2; then B3: d3: a=3 which branches back to B2 (loop) or falls through to B4 (exit). Edges: B1->B2, B2->B3, B3->B2, B3->B4.

Block GEN KILL (definitions of 'a': d1,d3 ; of 'b': d2) B1 {d1} {d3} B2 {d2} {} B3 {d3} {d1} B4 {} {} Iteration 1 (initial IN/OUT = {} everywhere): B1: IN={} OUT = {d1} U ({} - {d3}) = {d1} B2: IN=OUT[B1]={d1} OUT = {d2} U ({d1} - {}) = {d1,d2} B3: IN=OUT[B2]={d1,d2} OUT = {d3} U ({d1,d2} - {d1}) = {d2,d3} B4: IN=OUT[B3]={d2,d3} OUT = {} U ({d2,d3} - {}) = {d2,d3} Iteration 2 (recompute; B2's IN now also includes OUT[B3]): B1: unchanged -> OUT={d1} B2: IN=OUT[B1] U OUT[B3] = {d1} U {d2,d3} = {d1,d2,d3} OUT = {d2} U ({d1,d2,d3} - {}) = {d1,d2,d3} B3: IN=OUT[B2]={d1,d2,d3} OUT = {d3} U ({d1,d2,d3} - {d1}) = {d3} U {d2,d3} = {d2,d3} (unchanged) B4: IN=OUT[B3]={d2,d3} OUT={d2,d3} (unchanged) Iteration 3 (check for convergence): B2: IN=OUT[B1] U OUT[B3] = {d1} U {d2,d3} = {d1,d2,d3} -- same as iteration 2 B3, B4: identical to iteration 2 => FIXED POINT REACHED

The result shows d1 (a=1) reaches B2 but is killed before reaching B4 (since d3 overwrites a on every path into B4), while d2 (b=2) and d3 (a=3) both reach the loop exit B4 - exactly the information a compiler needs to detect that the initial value of a is dead by the time the loop exits, enabling dead-code/constant-propagation optimizations.

Q12a) Explain Live Variable Analysis and Available Expressions Analysis. b) Write backward data flow equations and explain applications in optimization.

Part (a): Live Variable Analysis determines, at each program point, the set of variables whose currently held value may still be used along some path before being overwritten - a variable is "live" going into a block if some path from there reads it before redefining it. This is a backward analysis (information flows from a block's successors back into it) and a "may" analysis (a variable is live if it is live along any one outgoing path, so information sets are unioned). Available Expressions Analysis determines, at each program point, the set of expressions (e.g. a+b) that have already been computed on every path reaching that point and whose operands have not been reassigned since - it is a forward analysis and a "must" analysis (an expression is available only if it is available along every incoming path, so sets are intersected).

Part (b): Data flow equations:

Live Variables (backward, may / union): USE[B] = variables read in B before any local (re)definition of them DEF[B] = variables (unconditionally) assigned in B IN[B] = USE[B] U ( OUT[B] - DEF[B] ) OUT[B] = U IN[S] for every successor S of B (OUT[exit] = {}) Available Expressions (forward, must / intersection): EGEN[B] = expressions computed in B and not invalidated afterward within B EKILL[B] = expressions whose operand(s) are (re)assigned somewhere in B OUT[B] = EGEN[B] U ( IN[B] - EKILL[B] ) IN[B] = ∩ OUT[P] for every predecessor P of B (IN[entry] = {})

Applications: live variable sets drive register allocation - a variable's live range (the span between definition and last use) determines which temporaries can share a physical register via graph coloring, and a variable that is dead immediately after assignment identifies dead code that can be deleted. Available expressions drive global common subexpression elimination - if an expression is available at a point, a later re-evaluation of the exact same expression can be replaced by a reference to the previously computed temporary, and also support loop-invariant code motion, hoisting an available, loop-independent expression out of a loop into its preheader.

Q13a) Explain Symbol Table organization for Object-Oriented nested scopes. b) Draw scope tree structure and explain symbol lookup in nested classes and methods.

Part (a): In an object-oriented language, scopes nest not only through blocks but through the class hierarchy: the global/file scope contains class declarations, each class scope contains its own members (fields, methods, nested/inner classes) and also implicitly re-exposes the members inherited from its superclass, and each method scope nested inside a class contains parameters and local variables, and additionally can see the enclosing class's fields/methods without qualification. The symbol table is therefore organized as a tree of scopes (a "scope tree") mirroring this nesting: each scope has its own hash table of symbol->attribute-record entries, plus a pointer to its parent scope (lexically enclosing scope) and, for a class, an additional pointer to its superclass's scope (which is not lexically enclosing but must still be searched for inherited members).

Part (b):

Global Scope class Base class Derived : Base method Base.f() method g() inner class inherits (superclass link)
Scope tree for nested classes/methods: solid edges are lexical nesting (parent-scope pointers); the dashed red edge is the extra superclass-scope pointer used only for member lookup, not for ordinary lexical name resolution.

Lookup of an identifier used inside Derived.g() proceeds: (1) search g()'s own local scope (parameters/locals); (2) if not found, search the enclosing class scope Derived (its own fields/methods); (3) if still not found, follow the superclass link to Base's scope and search it (repeating up the inheritance chain for multi-level inheritance); (4) finally search the enclosing lexical scope, Global. The first matching entry found, in this order, is bound to the reference; if a name exists in both a class and its superclass, the class's own entry shadows (hides) the inherited one, which is how method overriding and field shadowing are resolved at compile time.

Q14a) Explain Runtime Storage Allocation for procedural languages. b) Detail Activation Record creation, parameter passing, return value handling, and control link pointers.

Part (a): Runtime storage for a procedural language is split into three regions: (1) a static area for global variables and constants, whose addresses are fixed at compile time; (2) a stack, which holds one activation record (also called a stack frame) per currently active procedure call, pushed on call and popped on return - this works because procedure activations are naturally last-in-first-out for languages without coroutines/closures that outlive their creator; and (3) a heap, for dynamically allocated data whose lifetime is not tied to any single procedure call (via malloc/new). Stack-based allocation is what makes recursion possible: every recursive call gets its own fresh activation record with its own locals, even though the code is the same.

Part (b):

Actual Parameters Return Value Control Link (dynamic link) Access Link (static link) Saved Machine Status Local Data Temporaries stack grows
Layout of one activation record (stack frame) for a procedure call, fields shown top to bottom in the order typically pushed.

On a call, the caller pushes actual parameter values, then a control link (pointer to the caller's activation record, used to restore the caller's frame on return) and, for languages with nested procedures, an access link (pointer to the activation record of the lexically enclosing procedure, used to resolve non-local variable references); the caller also saves its own machine status (return address, register contents) before transferring control. The callee then allocates space for its local variables and temporaries on top of this. Return value handling: the callee writes its result into the reserved return-value slot (or a dedicated register, per the calling convention) before executing the return sequence, which pops the callee's frame, restores the saved machine status and jumps to the return address recorded via the control link, leaving the stack pointer back where the caller left it. Parameter passing may be by value (copy actual into the parameter slot), by reference (copy the address instead), or by value-result, all of which only change what is written into the "Actual Parameters" area, not the overall frame structure.

Q15a) Explain Garbage Collection algorithms: Mark-and-Sweep, Copying Collector, and Reference Counting. b) Detail Generational Garbage Collection hypothesis and performance advantages.

Part (a): Mark-and-Sweep runs in two phases: starting from a set of roots (globals, stack references), it recursively marks every reachable heap object, then sweeps the whole heap linearly, reclaiming any unmarked (unreachable) object back to a free list. It never moves objects, so pointers stay valid, but it leaves the heap fragmented and its sweep cost is proportional to heap size regardless of how much garbage exists. A Copying Collector divides the heap into two equal semispaces; it allocates only in the active (from-space) semispace and, when that fills, traces all reachable objects from the roots (Cheney's breadth-first algorithm using a work queue in the new space itself) and copies them compactly into the other (to-space) semispace, updating every pointer to the new location; the old semispace is then discarded wholesale. This automatically compacts memory and makes allocation a trivial pointer bump, at the cost of needing double the address space and copying live (not dead) objects. Reference Counting stores, in every object, a count of how many pointers reference it; incrementing on each new reference and decrementing (freeing at zero) on each reference removal, reclamation is immediate and spread out rather than a stop-the-world pause, but it cannot reclaim cyclic garbage (two objects referencing each other with no external reference) and imposes overhead on every pointer assignment.

AlgorithmReclaims cycles?Compacts heap?Pause behaviorOverhead
Mark-and-SweepYesNo (fragments)Stop-the-world, O(heap)Marking + full sweep
Copying CollectorYesYesStop-the-world, O(live)2x memory, copy cost
Reference CountingNoNoIncremental, no big pausePer-assignment inc/dec

Part (b): The generational hypothesis observes empirically that "most objects die young" - the large majority of allocated objects become garbage very shortly after creation (e.g. short-lived temporaries), while a small minority survive for a long time. A generational collector exploits this by splitting the heap into a small young/nursery generation and one or more older generations; new objects are always allocated in the nursery, which is collected frequently using a cheap copying collector (a "minor GC") because it is small and most of its contents are dead, so scanning it is fast; objects that survive several minor collections are promoted (tenured) into the old generation, which is collected only rarely with a more expensive full/major GC. This gives large performance gains because it concentrates collection effort where garbage actually accumulates and avoids repeatedly re-scanning long-lived, mostly stable old-generation data. The one correctness subtlety is inter-generational pointers (an old object referencing a young one); collectors track these with a write barrier that records such pointers in a remembered set, so a minor GC can treat the remembered set as additional roots without having to scan the entire old generation.

Q16a) Explain Error Detection, Reporting, and Recovery across all compiler phases. b) Detail Panic Mode vs Phrase-Level recovery mechanisms in LL and LR parsers.

Part (a): Every compiler phase detects and reports its own class of errors. The lexical analyzer flags illegal characters or malformed tokens (e.g. an unterminated string literal) and typically recovers by deleting the bad character(s) and resuming scanning. The syntax analyzer (parser) flags token sequences that do not match any grammar production (e.g. a missing semicolon or unbalanced parenthesis); since it has the most structural context it does the bulk of recovery work (panic mode / phrase-level / error productions, discussed below). The semantic analyzer flags type mismatches, undeclared identifiers, and redeclaration errors, typically continuing analysis by assuming an error type or inserting the identifier into the symbol table so that a single mistake does not trigger a cascade of false-positive errors downstream. The intermediate-code/optimizer and code-generator phases rarely see genuine source errors (a well-formed AST that passed semantic checks should always be generatable) but still validate invariants (e.g. detecting overflow of fixed-size constant folds) and report internal-compiler-errors if an invariant is violated. In all phases, good error reporting gives the line/column of the offending token, a human-readable message, and - crucially - continues compilation afterward (error recovery) so the user sees many real errors from one compile run instead of just the first.

Part (b):

AspectPanic Mode RecoveryPhrase-Level Recovery
StrategyDiscard input tokens one at a time until a token in a predefined "synchronizing set" (e.g. ;, }) is found, then resume parsingPerform a local, context-specific correction at the point of error - insert/delete/replace a token so parsing can continue exactly there
PrecisionCoarse - skips potentially large chunks of input; may miss real errors inside the skipped regionFine-grained - fixes only the specific malformed construct
Implementation costSimple - just needs sync-token sets per non-terminalComplex - needs custom error-handling code/table entries for every anticipated mistake
RiskMay under-report (swallows some real errors)Risk of infinite loops or cascading false errors if the "correction" is wrong
In LL(1) parsersOn seeing a token not in a non-terminal's predict set, pop/skip until a token in that non-terminal's FOLLOW set (or a designated sync set) is seenTable cells left blank by construction are filled with pointers to specific error-handling routines instead of being blank/error
In LR parsersOn reaching an error state, pop states off the stack until one with a valid shift on a designated synchronizing (nonterminal) token is exposed, then shift itAugment the grammar with explicit error-productions (e.g. stmt : error ';') so the parser generator builds recovery actions directly into the LR table
Q17a) Explain Peephole Optimization pattern matching and instruction replacement. b) Apply peephole transformations to an unoptimized 8086 assembly snippet.

Part (a): Peephole optimization examines the generated target/assembly code through a small, sliding "peephole" window (typically 2-4 consecutive instructions) and replaces the pattern currently in the window with a shorter or faster equivalent whenever a recognized pattern matches, repeating (sliding the window and re-matching) until no further pattern fires. It is applied as a late, machine-dependent cleanup pass after code generation. Common pattern families include: redundant load/store elimination (a value stored to memory and immediately reloaded into the same register); redundant push/pop elimination (a value pushed and immediately popped back into the same register with no intervening use); algebraic simplification (e.g. ADD reg, 0 or MUL reg, 1 removed entirely); strength reduction (replacing a multiply by a power of two with a shift); unreachable code elimination (code after an unconditional jump/return, before the next label, is dead); and jump-to-jump / branch chaining (a jump whose target is itself an unconditional jump is rewritten to jump straight to the final target, collapsing the chain). Because each rewrite is purely local, peephole optimization is cheap to run and is typically applied repeatedly (a fixed point) since one rewrite can expose a new pattern for the next.

Part (b): Unoptimized 8086-style assembly containing a redundant PUSH/POP pair, a useless add, and a jump chain:

; ---- BEFORE peephole optimization ---- MOV AX, BX ; AX = BX PUSH AX ; save AX POP AX ; immediately restore AX into itself (redundant) MOV CX, AX ; CX = AX ADD CX, 0 ; add zero (no-op) JMP L1 L1: JMP L2 ; jump chain: L1 just jumps again L2: MOV DX, CX
; ---- AFTER peephole optimization ---- MOV AX, BX ; AX = BX MOV CX, AX ; CX = AX (PUSH AX / POP AX removed: no net effect) JMP L2 ; jump-to-jump collapsed: JMP L1 -> JMP L2 becomes JMP L2 directly L2: MOV DX, CX ; (ADD CX,0 removed: adding zero is a no-op)

Three independent transformations fired here: the PUSH/POP pair around AX was eliminated because AX's value was unchanged and never used from the stack; ADD CX,0 was deleted as an algebraic identity; and the two-instruction jump chain JMP L1 -> JMP L2 was collapsed into a single direct JMP L2, also making the now-unreferenced label L1 eligible for removal.

Q18a) Explain Loop Unrolling, Loop Jamming, and Software Pipelining. b) Demonstrate loop unrolling on a matrix multiplication loop to increase instruction-level parallelism.

Part (a): Loop unrolling replicates the body of a loop several times inside a single iteration (reducing the trip count by the same factor), cutting the relative overhead of loop-control instructions (increment, compare, branch) per unit of real work and exposing more independent instructions that the processor or compiler scheduler can overlap/pipeline. Loop jamming (loop fusion) merges two or more adjacent loops that iterate over the same index range into a single loop body, amortizing one loop's control overhead across both computations and improving data locality when both loops touch the same array (fewer separate passes over memory, better cache reuse) - it is legal only when no data dependence crosses from the second original loop back into an earlier iteration of the first. Software pipelining restructures a loop so that instructions from several different logical iterations are issued in an overlapping fashion within one physical loop iteration (a steady-state "kernel" preceded by a short prologue and followed by an epilogue), keeping every functional unit of the pipelined processor busy every cycle instead of stalling on a chain of true dependencies within one iteration.

Part (b): Original inner loop of a matrix-multiply kernel (accumulating one output element c[i][j]):

/* original */ for (k = 0; k < n; k++) sum += a[i][k] * b[k][j];

Unrolled by a factor of 4, exposing four independent multiply-add chains per iteration (with a cleanup loop for any remainder when n is not a multiple of 4):

/* unrolled by 4 */ for (k = 0; k <= n - 4; k += 4) { sum += a[i][k] * b[k][j]; sum += a[i][k+1] * b[k+1][j]; sum += a[i][k+2] * b[k+2][j]; sum += a[i][k+3] * b[k+3][j]; } for (; k < n; k++) /* cleanup loop for n % 4 leftover iterations */ sum += a[i][k] * b[k][j];

The loop-control overhead (increment and bounds check on k) is now paid once per four multiply-accumulate operations instead of once per one, and the four a[i][k+m]*b[k+m][j] products have no dependence on each other, so an out-of-order or VLIW/pipelined processor (or a vectorizing compiler) can issue and execute them concurrently instead of serializing on the single accumulator - the accumulation into sum can also be split into partial sums to break even that final dependency chain, a common further refinement.

Q19a) Explain LLVM Compiler Infrastructure Architecture (Front-End, LLVM IR, Optimizer, Back-End). b) Detail LLVM Pass Manager and transformation passes.

Part (a): LLVM structures a compiler into three independent, replaceable stages connected by a common intermediate representation. The front end (e.g. Clang for C/C++/Objective-C, or any other language's front end) parses source into an AST and lowers it into LLVM IR - a strongly typed, target-independent, Static Single Assignment (SSA) form three-address-code-like representation with an infinite set of virtual registers, available in an in-memory form, a human-readable textual .ll form, and a compact binary bitcode .bc form. The optimizer (driven by the opt tool, or invoked automatically at a chosen -O level) runs a pipeline of analysis and transformation passes purely on LLVM IR, so the same optimizations work for every source language and every target. The back end (per-target, driven by llc or the integrated code generator) lowers optimized IR through instruction selection, register allocation, and instruction scheduling into target-specific assembly or object code. Because only the back end is target-specific (its machine description is generated from declarative TableGen files), retargeting LLVM to a new CPU only requires writing a new back end, while every front end and every IR-level optimization is reused unchanged - this front end/middle end/back end separation around a single IR is LLVM's central architectural idea.

Clang / Frontend LLVM IR Optimizer (opt: pass pipeline) Backend (llc) Target ASM
LLVM pipeline: source is lowered to LLVM IR once, optimized entirely at the IR level, then handed to a target-specific backend.

Part (b): The Pass Manager schedules and runs an ordered pipeline of analysis and transformation passes over the IR. Passes are scoped at different granularities - function passes (operate on one function at a time, e.g. -mem2reg which promotes stack-allocated variables into SSA registers), loop passes (operate on individual natural loops, e.g. loop-invariant code motion), and module passes (operate across the whole translation unit, e.g. -inline, which needs the whole call graph). Newer LLVM versions replaced the legacy Pass Manager with the "new Pass Manager", which represents passes and their required/preserved analyses more explicitly, enabling better caching of analysis results across passes and finer control over pipeline ordering. Representative passes include -mem2reg (SSA construction), -gvn (Global Value Numbering, redundant computation elimination), -dce/-adce (dead code elimination), -inline (inline expansion), and -instcombine (peephole-style IR simplification); the optimization level flags (-O1, -O2, -O3) simply select a curated, ordered sequence of such passes.

Q20a) Complete Trace: Trace statement `a = b + c * 60` through ALL 6 phases of a compiler. b) Show exact output produced at Lexical, Syntax, Semantic, ICG, Optimization, and Target Code generation phases.

Trace of a = b + c * 60 (assume a, b, c are declared real; the literal 60 is an integer constant, forcing a type coercion), through all six phases:

PhaseOutput
1. Lexical Analysis<id,a> <=> <id,b> <+> <id,c> <*> <num,60> ; symbol-table entries created/looked up for a,b,c
2. Syntax AnalysisParse tree / AST: assign-stmt(id a, expr(expr(id b) + term(term(id c) * factor(60))))
3. Semantic AnalysisType checking finds 60 is integer but c is real; inserts an inttoreal conversion node so c * 60 becomes type-consistent real multiplication
4. Intermediate Code Gen.t1=inttoreal(60); t2=c*t1; t3=b+t2; a=t3
5. Code Optimization60 is a compile-time constant, so inttoreal(60) is folded at compile time: t1=c*60.0; t2=b+t1; a=t2 (one fewer temporary/instruction)
6. Target Code GenerationMOVF c,R2 | MULF #60.0,R2 | MOVF b,R1 | ADDF R2,R1 | MOVF R1,a
= a + b * c 60.0
AST after optimization: the inttoreal node has been eliminated by folding 60 to the real constant 60.0 directly.
Q21a) Construct SLR(1) Parsing Table for grammar: S ➔ L = R | R L ➔ * R | id R ➔ L b) Check if SLR(1) table has S-R or R-R conflicts.

Part (a): SLR(1) construction. Augmented grammar (0: S'→S, 1: S→L=R, 2: S→R, 3: L→*R, 4: L→id, 5: R→L). FOLLOW sets (mutually recursive via L↔R): FOLLOW(S)={$}, FOLLOW(L)={=,$}, FOLLOW(R)={=,$}.

StateItems
I0S'→.S, S→.L=R, S→.R, L→.*R, L→.id, R→.L
I1=goto(I0,S)S'→S. (accept)
I2=goto(I0,L)S→L.=R, R→L.
I3=goto(I0,R)S→R. (reduce, FOLLOW(S)={$})
I4=goto(I0,*)L→*.R, R→.L, L→.*R, L→.id
I5=goto(I0,id)L→id. (reduce, FOLLOW(L)={=,$})
I6=goto(I2,=)S→L=.R, R→.L, L→.*R, L→.id
I7=goto(I4,R)L→*R. (reduce, FOLLOW(L)={=,$})
I8=goto(I4,L)=goto(I6,L)R→L. (reduce, FOLLOW(R)={=,$})
I9=goto(I6,R)S→L=R. (reduce, FOLLOW(S)={$})

Part (b): Conflict check. State I2 = {S→L.=R, R→L.}. This state has a shift action on '=' (from S→L.=R) and, because FOLLOW(R) contains '=' (via R→L and L's own FOLLOW set), also a reduce R→L action on '=' — a genuine shift-reduce conflict at I2 on the symbol '='. Every other reduce state (I3, I5, I7, I8, I9) is reduce-only with no competing shift, so no other conflicts exist.

Conclusion: this grammar is not SLR(1). The conflict is an artifact of SLR's imprecision — it uses the single global FOLLOW(R) set, which is forced to include '=' because R can derive L and L precedes '=' in S→L=R, even though in the specific context "after an L reached directly under S", R→L should only reduce on lookahead $. A canonical LR(1) or LALR(1) parser carries the more precise per-item lookahead and parses this grammar without conflict.

Q22a) Explain SLR(1) vs LALR(1) vs CLR(1) item sets generation. b) Show how merging LR(1) items with same core produces LALR(1) states without introducing S-R conflicts.

Part (a): Item-set generation compared.

MethodItem formReduce decided byStates (typical)Power
SLR(1)LR(0) item [A→α.β]global FOLLOW(A)fewestweakest
LALR(1)LR(1) item [A→α.β, a] with same-core states mergedmerged, per-state lookahead setsame as SLR's LR(0) core countmiddle (used by yacc/bison)
CLR(1)LR(1) item, no mergingprecise per-item lookaheadmost (can blow up)strongest

Part (b): Safe merging, demonstrated on S→AA, A→aA|b. The CLR(1) construction for this grammar (see the canonical LR(1) automaton worked out for this same grammar elsewhere in this bank) produces states I4={[A→b.,a],[A→b.,b]} and I7={[A→b.,$]} that share the identical core {A→b.} and differ only in lookahead; likewise I8={[A→aA.,a],[A→aA.,b]} and I9={[A→aA.,$]} share core {A→aA.}. LALR(1) merges each same-core pair: I4∪I7 → one state {[A→b., a/b/$]}, and I8∪I9 → {[A→aA., a/b/$]}.

Since I4/I7 and I8/I9 each contain only reduce items (no shift or other action to clash with), the merged states remain pure "reduce on {a,b,$}" states — merging introduces no new conflict here, so this grammar is LALR(1) with only 8 states instead of CLR(1)'s 10, at identical parsing power for this input language.

General risk: merging is only guaranteed conflict-free with respect to shift actions (a state's shift/goto transitions depend only on the core, so they are unaffected by merging). It can introduce new reduce-reduce conflicts when two states with the same core have two different complete items whose merged lookahead sets overlap (e.g. core {[A→γ.,a1],[B→δ.,a2]} in one state and {[A→γ.,a2],[B→δ.,a1]} in the other core-sharing state) — a case that never arises in this particular grammar but is the classical reason "some LR(1) grammars are not LALR(1)".

Q23a) Explain Backpatching for boolean expressions with control flow jumps. b) Generate backpatched 3AC for `if (a < b or c < d) and e < f then x = 1;`

Part (a): Backpatching. Rather than fixing jump targets immediately (which is impossible for forward jumps whose target instruction doesn't exist yet), the code generator emits jump instructions with the target left blank and records each blank in a synthesized list (truelist/falselist) of instruction numbers still needing a target. makelist(i) creates a new one-element list {i}; merge(p1,p2) concatenates two lists; backpatch(list,target) fills every instruction in list with the address target once it becomes known. A marker non-terminal M → ε {M.instr = nextinstr} is inserted at the grammar position right where the next instruction's address must be captured (e.g. right after 'or'/'and').

Part (b): Backpatched 3AC for if (a<b or c<d) and e<f then x=1; using B→id1 relop id2 (emits a conditional-goto and a goto, both blank), B→B1 or M B2 (backpatch(B1.falselist,M.instr)), and B→B1 and M B2 (backpatch(B1.truelist,M.instr)):

100: if a < b goto 104 101: goto 102 102: if c < d goto 104 103: goto 107 104: if e < f goto 106 105: goto 107 106: x = 1 107: ... (next statement)

Trace of how the jumps were resolved: (a<b).falselist={101} is backpatched to 102 (start of c<d, the other 'or' operand) once that code is generated. B1=(a<b or c<d) then has truelist={100,102} and falselist={103}. When "and e<f" is processed, B1.truelist={100,102} is backpatched to 104 (start of e<f) — so either a<b or c<d being true jumps straight to testing e<f. The whole expression's truelist={104} is backpatched to 106 (the x=1 statement) once it is generated, and falselist={103,105} both fall through to statement 107. Manually tracing all four truth combinations of the three relations confirms x=1 executes iff (a<b or c<d) and e<f is true.

Q24a) Explain Memory Management for Object-Oriented Languages (C++/Java). b) Detail Object layout, Virtual Method Tables (VTable), and dynamic dispatch resolution.

Part (a): C++ uses largely manual memory management: new allocates an object on the heap and invokes its constructor, delete invokes the destructor and frees the storage; because there is no collector, C++ idiomatically relies on RAII (Resource Acquisition Is Initialization) and smart pointers (unique_ptr/shared_ptr) so that destructors deterministically release memory when an owning object's scope ends, converting manual deallocation into automatic, scope-driven deallocation. Java (and most managed OOP languages) instead allocates every object on a garbage-collected heap; the programmer never frees memory explicitly, and the runtime reclaims unreachable objects using one of the collection algorithms discussed in gc-15 (typically a generational collector). The tradeoff is predictability versus safety: C++ gives precise, immediate, low-overhead control at the cost of manual-memory bugs (leaks, dangling pointers, double frees), while Java trades some throughput and pause-time unpredictability for the elimination of that entire bug class.

Part (b):

Object (Derived) vptr (header) inherited fields own fields VTable (class-wide) slot0 -> Derived::f() slot1 -> Base::g() slot2 -> Derived::h()
Object layout: the hidden vptr in the header points to a single, shared per-class VTable; instance fields (inherited, then own) follow the header.

Every object of a class with virtual methods carries a hidden pointer (vptr) as the first field of its header, set by the constructor to point at that class's single shared VTable - an array of function pointers with one fixed slot index per virtual method, shared by all instances of the class. A virtual call obj->f() compiles to obj->vptr[slot_of_f](): the slot index is resolved at compile time (from the static declaration order), but which function pointer sits at that slot is resolved at run time because each subclass's VTable stores its own overriding implementation at the same slot - this indirection is exactly dynamic dispatch. Non-virtual methods need no such indirection since their target is fixed at compile time. Java's JVM uses an analogous mechanism (a per-class method table plus interface method tables/"itables" for interface dispatch). Single inheritance keeps this a simple one-vptr, one-vtable, fixed-slot scheme; multiple inheritance requires an object to carry several vptrs (one per base-class sub-object) and the compiler to generate small "thunk" functions that adjust the this pointer before forwarding a call through the correct base's vtable.

Q25a) Explain Code Generation for Risk Architectures (RISC vs CISC). b) Detail Instruction Selection using Tree-Rewriting rules.

Part (a): Code generation must target the actual instruction set, and RISC and CISC machines demand different strategies. RISC (Reduced Instruction Set Computer) machines have fixed-length instructions, a strict load-store architecture (only explicit load/store instructions touch memory; arithmetic operates only on registers), a large uniform register file, and simple, single-cycle-friendly instructions - the code generator must therefore explicitly schedule separate load instructions before any computation and explicitly manage a comparatively large but still finite register set, relying heavily on the compiler (not the hardware) for instruction scheduling and pipeline-hazard avoidance. CISC (Complex Instruction Set Computer) machines allow variable-length instructions, rich addressing modes, and operands that may reside directly in memory (memory-to-memory or memory-to-register operations in a single instruction), with many complex operations implemented internally as microcode - the code generator can therefore often emit fewer, denser instructions per source statement (e.g. one ADD [mem], reg instead of separate load/add/store), shifting complexity from the compiler into the hardware's microcode decoder.

AspectRISCCISC
Instruction lengthFixedVariable
Memory accessLoad-store only (registers for compute)Memory-to-memory / memory operands allowed
Addressing modesFew, simpleMany, complex
RegistersMany general-purposeFewer, often special-purpose
Execution modelHardwired, single-cycle-friendlyMicrocoded, multi-cycle
Compiler roleHeavy - scheduling, register allocation criticalLighter - hardware absorbs some complexity

Part (b): Instruction selection by tree rewriting represents each IR expression as an expression tree and matches it against a library of tiles - small subtree patterns, each pre-associated with a target instruction and a cost. A tiling covers the whole tree with non-overlapping tiles, and a maximal-munch / dynamic-programming algorithm chooses, at each node, the minimum-total-cost combination of tiles bottom-up (computing the cheapest tiling of each subtree once and reusing it, which is what makes it dynamic programming rather than brute-force search over all tilings). For x = a + b * c, the IR tree is Assign(x, Add(a, Mul(b, c))). Tiling proceeds bottom-up: the subtree Mul(b, c) is matched by a single "MUL-tile" (target instruction MUL Rt, b, c), producing a temporary result; that temporary then combines with a under an "ADD-tile" (ADD Rt2, a, Rt); finally a "STORE-tile" covers the outer Assign node (MOV x, Rt2). Because the two-instruction tiling (one MUL-tile, one ADD-tile, one STORE) has lower total cost than any decomposition using more, smaller tiles, the dynamic-programming selector picks exactly this combination, directly producing the instruction sequence MUL Rt,b,c ; ADD Rt2,a,Rt ; MOV x,Rt2.

Q26a) Explain Dominator Trees and Natural Loops in Control Flow Graphs. b) Algorithm to identify natural loops and loop pre-headers.

Part (a): In a control flow graph, node d dominates node n (written d dom n) if every path from the entry node to n passes through d (every node trivially dominates itself). The dominance relation for a CFG is captured compactly as a dominator tree: each node's parent is its immediate dominator - the unique closest dominator that is itself dominated by all of the node's other dominators - so ancestry in the dominator tree exactly encodes the dominance relation. A natural loop is identified from a back edge n -> h, an edge whose target h dominates its source n (i.e. control flows from later in the traversal back "up" to a point that always executes first); h is the loop's unique header, and the loop body is h together with every node that can reach n without passing through h.

B1 (entry) B2 (header) B3 B4 (exit) back edge B3->B2
B2 dominates B3, so B3->B2 is a back edge; the natural loop header is B2 and the loop body is {B2, B3}.

Part (b): To find all natural loops: (1) compute the dominator tree of the CFG (e.g. via the standard iterative dominator algorithm); (2) scan every CFG edge n -> h and classify it as a back edge whenever h dom n; (3) for each back edge, compute the loop body by a reverse-CFG traversal starting at n: initialize the body set to {h, n}, then repeatedly walk predecessors of nodes already in the set (excluding h itself, which is never re-entered) adding any newly reached predecessor, stopping when no new node is added - every node collected this way can reach n without passing through h, which is exactly the definition of loop membership. A loop pre-header is then constructed by inserting a new, empty basic block immediately before h and redirecting every edge that enters h from outside the loop to instead target this new block, which falls through to h; edges from inside the loop (including the back edge) still go directly to h. The pre-header gives optimizations like loop-invariant code motion a single, safe location - executed exactly once per loop entry - to hoist invariant computations into.

Q27a) Explain Global Value Numbering (GVN) vs Local Value Numbering (LVN). b) Apply Value Numbering to optimize basic block statements.

Part (a): Local Value Numbering (LVN) processes one basic block at a time, maintaining a hash table that maps a canonicalized (operator, operand value numbers) key to a value number. Each new variable/constant gets a fresh value number the first time it is seen; for a computation like t = x op y, LVN looks up (op, VN(x), VN(y)) (with operands of a commutative operator sorted into a canonical order so that a+b and b+a hash identically) - if that key already exists, the new computation is provably redundant (computes the same value) regardless of what the source variable/temporary names were, and can be replaced by a copy of the earlier result; otherwise a new value number is assigned. Because LVN only tracks facts within a single block, it misses redundancies whose two occurrences are in different blocks. Global Value Numbering (GVN) extends the same idea across the whole procedure/CFG, typically operating on SSA-form IR (where each variable has exactly one definition, making value identity easier to reason about across blocks and merge points via phi-functions); GVN therefore catches cross-block redundant computations that LVN, restricted to one block, structurally cannot see.

AspectLVNGVN
ScopeSingle basic blockWhole procedure / CFG
Typical representationPlain three-address codeSSA form (phi-functions)
ImplementationSimple hash table, linear scanMore complex - partitioning / congruence-closure over the CFG
Redundancy caughtOnly within one blockAcross block boundaries too
CostCheap, fastMore expensive but more powerful

Part (b): Basic block with a redundant computation via commutative operands and a repeated expression:

/* before value numbering */ t1 = a + b t2 = b + a t3 = t1 * t2 t4 = a + b t5 = t3 + t4

Value-numbering trace: VN(a)=1, VN(b)=2. For t1 = a+b, key (+, 1, 2) is new -> assign VN(t1)=3. For t2 = b+a, canonicalizing commutative operands gives the same key (+, 1, 2) -> already present, so VN(t2)=3 too and t2 is redundant (equal to t1). For t3 = t1*t2, key (*, 3, 3) is new -> VN(t3)=4. For t4 = a+b, key (+, 1, 2) is already present -> VN(t4)=3, also redundant with t1. For t5 = t3+t4, key (+, 4, 3) is new -> VN(t5)=5.

/* after value numbering (redundant recomputations replaced) */ t1 = a + b t2 = t1 /* was b+a: same value number as t1 */ t3 = t1 * t1 /* t2 replaced by t1 since VN(t2)=VN(t1) */ t4 = t1 /* was a+b again: same value number as t1 */ t5 = t3 + t1 /* t4 replaced by t1 */

Only one real addition (a+b) and one real multiplication are computed; the second and third additions are eliminated as provably redundant, without any control-flow or cross-block reasoning being required since all four statements live in one basic block.

Q28a) Explain Interprocedural Optimization (IPO) and Inline Expansion. b) Tradeoffs of function inlining on code size and instruction cache.

Part (a): Interprocedural Optimization (IPO) is the family of optimizations that analyze and transform code across function/procedure boundaries rather than one function at a time, since purely intraprocedural (local) analysis must otherwise make worst-case conservative assumptions about what a called function might do to memory or globals. Typical interprocedural analyses include interprocedural constant propagation (propagating a constant actual argument into the callee's body), interprocedural alias/points-to analysis (determining whether two pointers in different functions can refer to the same memory), and call-graph construction (needed before any of the above, especially in the presence of function pointers/virtual calls). Inline expansion (inlining) is the most impactful and widely used IPO transformation: the compiler replaces a call site with a full copy of the callee's body, substituting actual arguments for formal parameters. This removes the call/return overhead (stack frame setup, parameter marshalling, jump) outright, and - more importantly - exposes the callee's statements to the caller's local optimizer, enabling further constant folding, dead code elimination, and common subexpression elimination across what used to be a call boundary, which is why inlining is often called an "enabling" optimization rather than just a speed optimization in itself.

Part (b):

EffectBenefit of inliningCost of inlining
Call overheadEliminated entirely (no push/jump/return)-
Further optimizationExposes callee body to constant propagation, CSE, dead-code elimination at the call site-
Code size-Grows with every inlined copy; a function called from N sites can multiply its body size by N
Instruction cache-Larger code footprint increases i-cache misses, which can outweigh the saved call overhead, especially for hot but rarely-reused call sites
Compile time-Larger functions take longer to optimize/compile after inlining
When it winsSmall, frequently-called functions (accessors, small helpers) on hot paths-
When it loses-Large functions, or functions called from many distinct sites (code bloat with little reuse benefit)

Because of this tradeoff, real compilers use a cost/heuristic-based inliner: a callee is inlined only if its estimated size is below a threshold (or the call site is judged "hot" by profile data), and recursive or very large functions are excluded outright, balancing the execution-speed benefit against the code-size and instruction-cache cost.

Q29a) Explain Target Machine Architecture model for code generation. b) Generate assembly code for `x = a + b * c` using 2 registers.

Part (a): A generic target machine model used for code generation typically assumes a small set of general-purpose registers, a byte-addressable memory, and a simple load-store/two-address instruction set: MOV dst, src for moves/loads/stores, and two-address arithmetic instructions of the form OP dst, src meaning dst := dst OP src (the destination is also an implicit source operand). Each instruction has an assumed cost (often uniform, e.g. 1 unit, possibly higher if either operand is a memory location rather than a register), and the code generator's job is to produce a correct instruction sequence for an expression while minimizing register usage and total instruction cost - critically, computing which operand subtree needs more registers first (Sethi-Ullman labeling) and evaluating that subtree first, so that if only a limited number of registers is available, "spilling" an intermediate result to memory is avoided or minimized.

Part (b): For x = a + b * c using only two registers R0, R1: the expression tree is Add(a, Mul(b,c)). By Sethi-Ullman reasoning, the Mul(b,c) subtree needs one register to hold its result but must be evaluated first and kept live while a is subsequently loaded, so it is computed into R0 before touching a; if the multiplication were evaluated last instead, its two leaf operands plus the pending value of a would together need three registers simultaneously, forcing a spill. Evaluating the heavier (multiply) subtree first keeps everything within two registers:

MOV R0, c ; R0 = c MUL R0, b ; R0 = b * c (two-address: R0 := R0 * b) MOV R1, a ; R1 = a ADD R1, R0 ; R1 = a + (b*c) (two-address: R1 := R1 + R0) MOV x, R1 ; store result: x = R1

Register allocation reasoning: R0 is dedicated to holding the (higher register-need) multiplication result across the two multiply instructions; R1 is only brought in afterward for a, once R0's value is stable, and is immediately combined with R0 - at no point are more than two live values needed simultaneously, so no spill to memory is required, and total code length is minimal (five instructions, one per leaf load/store plus one per operator) for this two-register model.

Q30a) Explain Static Program Analysis tools and linters. b) Detail Abstract Interpretation and Model Checking concepts in compiler verification.

Part (a): Static program analysis examines source or intermediate code without executing it, computing conservative facts about all possible runs of the program. A linter (e.g. the original Unix lint for C, or modern tools such as Clang Static Analyzer, ESLint, or Pylint) is a lightweight static analysis tool built on top of a compiler front end that flags suspicious-but-not-illegal constructs the compiler itself would silently accept - unused variables, unreachable code, format-string mismatches, implicit narrowing conversions, possible null-pointer dereferences - typically using the same data-flow and type-checking machinery a compiler already builds (reaching definitions, live variables) but applying stricter/heuristic rules aimed at catching likely bugs and style problems rather than establishing outright language-legality. Because static analysis reasons about all possible executions at once, without running the program, it can find bugs on rarely-exercised paths that testing would miss, at the cost of possible false positives (over-conservative warnings on paths that can never actually occur).

Part (b): Abstract Interpretation formalizes this "approximate all runs at once" idea: instead of executing a program over its real (concrete) domain of values, it executes a sound approximation of the program over an abstract domain - e.g. the "sign" domain {negative, zero, positive, unknown} instead of exact integers, or an interval domain [low, high] instead of exact values - with abstract versions of every operator (e.g. positive + positive = positive) defined so that the abstract result always safely over-approximates every concrete result that could actually occur. Because the abstract domain is small and has finite (or well-controlled infinite) height, fixed-point iteration over it (exactly the same iterative technique used for data-flow analyses like reaching definitions) terminates and yields provably sound bounds - e.g. proving an array index can never go negative - which is precisely how compilers and static analyzers prove properties that would be undecidable to check exactly. Model Checking takes a complementary, more expensive approach: it represents a program's (or protocol's, or hardware design's) behavior as a finite-state transition system and exhaustively explores its reachable state space to verify that a temporal-logic property (e.g. "no reachable state has a race" / "every request is eventually followed by a response") holds on every path, or produces a concrete counterexample trace if it does not. In compiler verification specifically, model checking and abstract interpretation are used to prove an optimization is behavior-preserving (the optimized and unoptimized programs are observationally equivalent) or to verify safety properties of generated code, complementing testing by offering exhaustive, mathematically certain coverage of the state space rather than sampled coverage.