PLI Lecture 10 Code generation I: Intermediate code generation Code generation may involve target language attribute computation, source and target code optimisation, generation of intermediate representations, generation of assembly language code, assembly and linking. This provides opportunities for further modularisation. Possible target languages are common high level languages (e.g., C), virtual machine languages (e.g., JVM, CLR), assembly language (for some real machine), and machine code (for some real machine). In almost every case, the structure tree needs to be linearised. 1. INTERMEDIATE CODE LANGUAGES Intermediate code is often generated first to decompose and simplify the code generation task, and to provide opportunities for target machine-independent code optimisation, before generation of target machine code and target machine optimisation. Intermediate code also makes it easier to retarget a compiler. Intermediate code is central to the Gnu Compiler Collection (gcc). Possible intermediate representations are three-address code (quadruples) and P-code. Intermediate languages may be designed by the compiler writer to simplify translation to and translation from. We discuss only three-address code in detail. 1.1 Three-address code Three-address code consists of a sequence of quadruples, each consisting of an operation, a destination, and two operands. Other variations are possible. For example, the arithmetic expression 2*a + (b-3) might be represented by the sequence of instructions t1 = 2 * a ; or, equivalently: times t1 2 a t2 = b - 3 t3 = t1 + t2 This ignores issues of whether data is stored in global storage, on the stack, in registers, or wherever. Temporary names t1, t2, etc., must be (lexically) distinct from source variable names. A more complete TINY example (Figure 8.1): { Sample TINY program } read x; if x > 0 then fact := 1; repeat fact := fact * x; x := x - 1 until x = 0; write fact end Corresponding three-address code (Figure 8.2): read x t1 = x > 0 if_false t1 goto L1 fact = 1 label L2 t2 = fact * x fact = t2 t3 = x - 1 x = t3 t4 = x == 0 if_false t4 goto L2 write fact label L1 halt Note that not all addresses of an instruction need be used, e.g., the read operation uses a single (destination) operand, the label operation has a single operand, the operation (=) has anly one operand. Note that the (first) if_false instruction has operation if_false_goto and operands t1 and L1. We translate the TINY statement "fact := fact * x" into two quadruples t2 = fact * x fact = t2 rather than one ("fact = fact * x") for technical reasons (8.2). (This may be undone in a later optimisation stage.) In practice, the list of quadruples is stored as a main memory data structure, not as a text file. The data structure is a list of quadruples. Some fields of each quadruple may be unused, as indicated clearly above (and in Figure 8.3): (rd,x,_,_) (gt,x,0,t1) (if_f,t1,L1,_) ... A possible C representation of the quadruples is given in Figure 8.4: typedef enum (rd,gt,if_f,asn,lab,mul,sub,eq,wri,halt,...} OpKind; typedef enum {Empty,IntConst,String} AddrKind; typedef struct { AddrKind kind; union { int val; char *name; } contents; } Address; typedef struct { OpKind op; Address addr1,addr2,addr3; } Quad; Many variations are possible. Exercises: 1. Propose quadruple sequences suitable for passing parameters, calling procedures and returning from procedures. 2. Propose quadruple sequencess suitable for accessing nonlocal variables within a procedure. 3. Propose quadruple sequencess suitable for accessing (one-dimensional) arrays, and structures. 1.2 P-code P-code is an abstract stack machine language. For example, the first expression above ( 2*a + (b-3) ) might be translated into: ldc 2 ; push constant 2 lod a ; push value of variable a mpi ; integer multiplication lod b ldc 3 sbi ; integer subtraction adi ; integer addition An assignment statement ( x := y + 1 ) might be translated into: lda x ; push address of x lod y ldc 1 adi sto ; store top to address below and pop both A more complete example, a translation of the TINY fact program above, is given in Figure 8.6, using "label" instructions, comparison instructions, and conditional jump instructions: lda x ; load address of x rdi ; read an integer and store in addres on stack (& pop) lod x ; push value of x ldc 0 ; load constant 0 grt ; pop and compare top two values, push boolean result fjp L1 ; pop boolean value, jump to label L1 if false ... Exercise. Complete this example without referring to the text. Code for handling procedures and parameters is simpler for P-code than for three-address code. But quadruples are more flexible and map more easily to real target machines, and are hence more widely used. 2. BASIC CODE GENERATION 2.1 Code generation as synthesized attribute computation Intermediate code may be defined as a synthesized attribute of nodes in the structure tree (syntax tree). For example: exp -> id = exp | aexp aexp -> aexp op factor | factor factor -> id | num | ( exp ) op -> + | - is a grammar for expressions with embedded assignments, e.g. (2+(a=5)) + (b-a). To generate P-code, assume also a nondestructive store instruction stn, which is like sto but leaves the value on the stack (the compiler writer has this freedom!). We can then generate P-code for expressions in the above language using the following attribute grammar (Table 8.1): exp -> id = exp exp_1.code = "lda" || id.strval ++ exp_2.code ++ "stn" exp -> aexp exp.code = aexp.code aexp -> aexp op factor aexp_1.code = aexp_2.code ++ factor.code ++ op.code aexp -> factor aexp.code = factor.code factor -> id factor.code = "lod" || id.strval factor -> num factor.code = "ldc" || num.strval factor -> ( exp ) factor.code = exp.code op -> + op.code = "adi" op -> - op.code = "sbi" (Here, || is string concatenation with a separating space within a single instruction, and ++ is string concatenation with a separating new line between instructions.) Generation of three-address code is slightly more complicated (Table 8.2): exp -> id = exp exp_1.name = exp_2.name exp_1.code = exp_2.code ++ id.strval || "=" || exp_2.name exp -> aexp exp.name = aexp.name exp.code = aexp.code aexp -> aexp + factor aexp_1.name = newname() aexp_1.code = aexp_2.code ++ factor.code ++ aexp_1.name || "=" || aexp_2.name || "+" || factor.name aexp -> factor aexp.name = factor.name aexp.code = term.code factor -> id factor.name = id.strval factor.code = "" factor -> num factor.name = num.strval factor.code = "" factor -> ( exp ) factor.name = exp.name factor.code = exp.code Here, attribute name is the temporary name for the expression value, function newname() returns unique new names (t1, t2, ...), and no code is generated for identifiers and numbers. E.g., the expression (x=x+3)+4 has the code attribute t1 = x + 3 x = t1 t2 = t1 + 4 Exercise. What is the code attribute for 2*(a=5) + (b-a)? 2.2 Practical code generation In practice, code generation by construction of a synthesized code attribute is insufficiently powerful (code generation also requires access to inherited attributes and the symbol table) and is too inefficient (because of string copying, etc.). Hence, code generation is actually performed incrementally during tree traversal using the following general, recursive tree traversal schema: void genCode (Tree t) { if (t is not null) { generate code to prepare for code of left child of t; genCode(left child of t); generate code to prepare for code of right child of t; genCode(right child of t); generate code to implement the action of t; } } Imagine C type definitions for a structure tree for integer expressions as defined above (p.410, types Optype, NodeKind, SyntaxTree, etc.). Then we can implement function genCode() to generate code from such structure trees as follows. We start with the simpler procedure for P-code: void genCode (SyntaxTree t) { char codestr[MAXLINELENGTH+1]; if (t != NULL) { switch (t->kind) { case IdKind: sprintf(codestr, "%s %s", "lod", t->strval); emitCode(codestr); break; case ConstKind: sprintf(codestr, "%s %s", "ldc", t->strval); emitCode(codestr); break; case OpKind: switch (t->op) { case Plus: genCode(t->lchild); genCode(t->rchild); emitCode("adi"); break; case Minus: /* similarly */ case Assign: sprintf(codestr, "%s %s", "lda", t->strval); emitCode(codestr); genCode(t->lchild); emitCode("stn"); break; default: emitCode("Error"); break; } break; default: emitCode("Error"); break; } } } Here, char array codestr holds the instruction being constructed, and function emitCode() outputs an instruction. Note that Plus nodes require only postorder traversal, but Assign nodes require preorder and postorder processing. Exercise: Modify this function to generate three-address code. Here is a possible solution: char *genCode (SyntaxTree t) { char codestr[MAXLINELENGTH+1]; char *n1, *n2, *n3; /* temporary names */ if (t != NULL) { switch (t->kind) { case IdKind: return t->strval; case ConstKind: n1 = newTemp(); sprintf(codestr, "%s = %s", n1, t->strval); emitCode(codestr); return n1; case OpKind: switch (t->op) { case Plus: if (isLeaf(t->lchild)) n1 = t1->lchild->strval; else n1 = genCode(t->lchild); if (isLeaf(t->rchild)) n2 = t->rchild->strval; else n2 = genCode(t->rchild); n3 = newtemp(); sprintf(codestr, "%s = %s + %s", n3, n1, n2); emitCode(codestr); return n3; case Minus: /* similarly */ case Assign: n1 = genCode(t->lchild); sprintf(codestr, "%s = %s", t->strval, n1); emitCode(codestr); return n1; default: emitCode("Error"); return (char *)NULL; } break; default: emitCode("Error"); return (char *)NULL; } } } int isLeaf(SyntaxTree t) { return t.kind == ConstKind || t.kind == IdKind; } Exercise. Simulate the behaviour of these functions on the expressions (x=x+3)+4 and (2+(a=5)) + (b-a). Study Figure 8.8 (pp. 412-413) to see how a Yacc specification can be written for a parser to generate P-code in the same way, but without first having to construct a structure tree. Here it is: %{ #define YYSTPE char * /* other inclusion code ... */ %} %token NUM ID %% exp : ID { sprintf(codestr,"%s %s","lda",$1); emitCode(codestr); } '=' exp { emitCode("stn"); } | aexp ; aexp : aexp '+' factor { emitCode("adi"); } | factor ; factor : '(' exp ')' | NUM { sprintf(codestr,"%s %s","ldc",$1); emitCode(codestr); } |ID { sprintf(codestr,"%s %s","lod",$1); emitCode(codestr); } ; %% /* utility functions ... */ The main novelty in this Yacc specification is that there may be several semantic actions in a single rule, e.g.: exp : ID { sprintf(codestr, "%s %s", "lda", $1); } "=" exp { emitCode("stn"); } | aexp ; (Note that code for exp_2 is generated in the rule for exp_2 and that omitted semantic actions are effectively "$$ = $1;".) Exercise. Write a complete yacc specification for this grammar to generate three-address code. 2.3 From intermediate code to target code Intermediate code hides many details such as variable locations, stack addressing, use of registers, that are required for actual target code. Macro expansion involves replacing each kind of intermediate code instructions by an equivalent sequence of target code instructions, using pattern matching. Static simulation involves replacing a straight-line sequence of intermediate code instructions by an sequence of target code instructions that has the same effect, using evaluation. Abstract interpretation involves symbolically evaluating sequences of intermediate code instructions and replacing them by shorter sequences of target code instructions with the same effect. These are complex techniques. We consider here only their use to translate P-code to three-address code and vice versa. E.g., consider again the expression (x=x+3)+4 and its P-code instruction sequence: lda x lod x ldc 3 adi stn ldc 4 adi By (statically) simulating the (stack) behaviour of this instruction sequence, we can translate it into its three-address equivalent: t1 = x + 3 x = t1 t2 = t1 + 4 To translate three-address code to P-code, we use macro expansion. E.g., a three-address instruction a = b + c is translated into the P-code sequence lda a lod b ; or "ldc b" if b is a constant lod c ; or "ldc c" if c is a constant adi sto Applying this rule repeatedly to the above sequence of three three-address instructions results in: lda t1 lod x ldc 3 adi sto lda x lod t1 sto lda t2 lod t1 ldc 4 adi sto This is rather unsatisfactory (being so much longer than the original P-code instruction sequence). A more sophisticated scheme is required to eliminate these unnecessary temporary variables. (Details are omitted here.) We return to this issue of generating target code from intermediate code later. 3. DATA STRUCTURE REFERENCES Eventually, we need to replace names in intermediate code by registers, absolute memory addresses, or activation record offsets or code to access nonlocal variables. But information required to access and update arrays, structures and pointers must be represented in intermediate code itself. In three-address code, for each address, we need to indicate whether it is an "address of" (&) or "indirect" (*), as in C. This requires adding an additional AddrMode field with possible values None, AddressOf and Indirect to the Address struct. Then, to store the constant 2 at the address of variable x plus 10 locations (e.g., x[10] = 2), we could write: t1 = &x + 10 *t1 = 2 Consider the array assignment: a[i+1] = a[j*2] + 3; In three-address code, this could be implemented as follows: t1 = j * 2 t2 = t1 * elem_size(a) ; this quantity is known at compile time t3 = &a + t2 ; address of a[j*2] t4 = *t3 ; value of a[j*2] t5 = t4 + 3 ; a[j*2] + 3 t6 = i + 1 t7 = t6 * elem_size(a) t8 = &a + t7 ; address of a[i+1] *t8 = t5 In P-code, we can make similar extensions by introducing index (ixa) and indirect load (ind) instructions: lda a lod i ldc 1 adi ixa elem_size(a) lda a lod j ldc 2 mpi ixa elem_size(a) ind 0 ldc 3 adi sto Now, consider the following grammar for expressions extended with array references: exp -> subs = exp | aexp aexp -> aexp + factor | factor factor -> id | num | ( exp ) subs -> id | id [ exp ] ; subscript We extend the previous type definitions as follows: typedef enum {Plus,Assign,Subs} OpType; Assignment nodes now have two children and array reference nodes have the subscript as a single child (and the id as a strval). The following procedure generates P-code for such expressions (Figure 8.9): void genCode(SyntaxTree t, int isAddr) { char codestr[MAXLINELENGTH+1]; if (t != NULL) { switch (t->kind) { case IdKind: if (isAddr) sprintf(codestr, "%s %s", "lda", t->strval); else sprintf(codestr, "%s %s", "lod", t->strval); emitCode(codestr); break; case ConstKind: if (isAddr) emitCode("Error"); else { sprintf(codestr, "%s %s", "ldc", t->strval); emitCode(codestr); } break; case OpKind: switch (t->op) { case Plus: if (isAddr) emitCode("Error"); else { genCode(t->lchild, FALSE); genCode(t->rchild, FALSE); emitCode("adi"); } break; case Minus: /* similarly */ case Assign: genCode(t->lchild, TRUE); genCode(t->rchild, FALSE); emitCode("stn"); break; case Subs: sprintf(codestr, "%s %s", "lda", t->strval); emitCode(codestr); genCode(t->lchild, FALSE); sprintf(codestr, "%s%s%s", "ixa elem_size(", t->strval, ")"); emitCode(codestr); if (!isAddr) emitCode("ind 0"); break; default: emitCode("Error"); break; } break; default: emitCode("Error"); break; } } } Exercises: 1. Apply this method to the expression (a[i+1]=2)+a[j]. 2. Modify this method to generate three-address code. Here is a possible solution to Exercise 2. It is simpler in that no isAddr seems to be required, but more complex in that genCode() must return a string again and some internal tests are required. char *genCode(SyntaxTree t) { char codestr[MAXLINELENGTH+1]; if (t != NULL) { switch (t->kind) { case IdKind: return(t->strval); case ConstKind: return t->strval; case OpKind: switch (t->op) { case Plus: if (isLeaf(t->lchild)) n1 = t1->lchild->strval; else n1 = genCode(t->lchild, FALSE); if (isLeaf(t->rchild)) n2 = t->rchild->strval; else n2 = genCode(t->rchild, FALSE); n3 = newtemp(); sprintf(codestr, "%s = %s + %s", n3, n1, n2); emitCode(codestr); return n3; case Minus: /* similarly */ case Assign: /* there may be a better way... */ n1 = genCode(t->lchild); n2 = genCode(t->rchild); if (t->lchild->kind == OpKind && t->lchild->op == Subs) n1 = "*" || n1; sprintf(codestr, "%s = %s", n1, n2); emitCode(codestr); return n2; case Subs: n1 = "&" || genCode(t->lchild); n2 = newtemp(); sprintf(codestr, %s = &%s + %s", n2, t->strval, n1); emitCode(codestr); return n2; default: emitCode("Error"); break; } break; default: emitCode("Error"); break; } } } To compute the address of a structure's field, we must first compute the base address of the structure, then find the (fixed) offset of the named field, then add the two to get the required address. For example, in three-address code, the address of x.j is computed by: t1 = &x + field_offset(x,j) (The offset is known at compile time.) A C field assignment "x.j = x.i;" is thus computed by: t1 = &x + field_offset(x,j) t2 = &x + field_offset(x,i) *t1 = *t2 Suppose x is a pointer to an int. Then the C assignment "*x = i" is computed trivially by the three-address instruction *x = i and the C assignment "i = *x;" is similarly computed by i = *x Consider the C type definition: typedef struct treeNode { int val; struct TreeNode *lchild, *rchild; } TreeNode; ... TreeNode *p; Then the two C assignments p->lchild = p; p = p->rchild; are translated into the three-address instructions: t1 = p + field_offset(*p,lchild) *t1 = p t2 = p + field_offset(*p,rchild) p = *t2 All of these operations can be implemented by macro expansion and easily incorporated into a code generator for expressions with embedded assignments.