CAPITULO IV: ANÁLISIS E INTERPRETACIÓN DE RESULTADOS
4.4 Evaluación Ambiental
After the loader has done its work and the object program begins to run therun-
time support sections of the object program take over. Before the object code
fragments which specify calculation, input and output can operate a run-time
environmentmust be set up: pointers to the stack must be initialised, storage
4.3. AFTER THE LOADER 73 can’t easily be delegated to the loader – unless it’s a special loader used by a single compiler – but they can easily be carried out by standard subroutines which the loader automatically extracts from the subroutine library and inserts into every object program.
In an implementation of a recursive language which uses a stack, or any more general storage management system, references to data objects cannot be fixed up by the loader but must be made via some pointer which indicates a position in a stack of ‘data frames’. These data frames make up the Activation Record Structure introduced in chapter 1. Section III contains detailed discussion of object code fragments that manipulate the Activation Record Structure and maintain the pointers which indicate data frames within it.
Summary
The input and output phases of a compiler aren’t hard to write or hard to design. The input phases have to be efficient because their speed essentially determines the speed of the entire compiler. They can be written as subroutines of the analysis phase because of the simplicity of their task.
The translator can take advantage of the existence of a linking and relocating loader to allow it to compile a program in sections, to output partially-assembled instructions and to insert program-wide debugging information.
Section II
Translation and Crucial
Code Fragments
77 Translation and code optimisation are the compilation tasks which professional compiler-writers enjoy thinking and talking about most. Given a statement or some other fragment of program, discussion centres around the eventual machine instructions which ought to be generated as its translation and how a translator might be written that could effectively generate those instructions. Certain kinds of program fragments are usually regarded as more important than others because they are the sort of operations that occur often in programs or because they occur often at the centre of the program’s inner loop. Such a fragment as
x := x+1
would usually be accepted as an important fragment, worthy of special attention by the compiler writer, whereas
x := sin(y)^2 + cos(y)^2
might not. I’d say that if you write the second you deserve the code you get, although in theory it might be optimally compiled as if you’d written ‘x := 1.0’. The problem is to design a translator that will generate the best possible code for important source fragments and correct code for all other possible fragments. The technique of tree walking allows even the novice to approach this ideal, because it makes it easy to build a translator which gives correct, but sub- optimal, code for all cases, then later to improve the translator by giving special attention to the translation of important code fragments. Chapter 2 shows how a tree walking translator can be built to give correct and fairly efficient translations of some specific fragments: in this section I show how this technique can be used to give as good results as any other mechanism of simple translation. Amongst the important program fragments will usually be found the following:
Calculation of an arithmetic value (chapter 5) Conditional transfer of control (chapter 6) Data structure access (chapter 9) Procedure call and return (chapter 11) Argument passing (chapter 12) Access to a variable (chapter 13) Stack addressing
(environment link or display vector) (chapter 13)
The calculation of arithmetic values is included in this list because practically everything you write in a program involves an expression – if only the calculation of the value of a single variable. Many of the others are included because history has proved that compilers can easily generate disastrous code for these program fragments if their designers don’t take care. Truly these are crucial source program fragments and the quality of the code fragments which you choose to generate from them will be crucial for both the speed of execution of the object program and the number of instructions included in it.
78
In this section, chapter 5 concentrates on code for arithmetic expressions. Chap- ter 6 deals with the generation of code which is used – e.g. in anifstatement – to select between alternative paths of execution. Chapter 7 shows how the pro- cedures which translate expressions can be used in the translation of statements and declarations. Chapter 8 deals with the building of the symbol table and the descriptors which it contains. Chapter 9 discusses code for data structure access. Chapter 10 discusses the principles of code optimisation and queries its cost-effectiveness.
At first sight it is perhaps surprising that loading of the value of a variable is listed as a crucial code fragment above. In this section I assume for the most part that the value of a variable can always be accessed by a single instruction, but section III and, in particular, chapter 13 shows that this is not so and that the code which accesses the memory is perhaps the most crucial code fragment of all!
Communication between Translation Procedures
In this section and in section III the translation mechanism illustrated is based on the use of ‘switch’ procedures which select an appropriate translation pro- cedure for a tree node: TranArithExpr in chapter 5, TranBoolExpr in chapter 6, TranStatement and TranDecl in chapter 7. Each of these procedures looks at the ‘type’ field of a node, which contains an integer uniquely identifying the type of the node, and calls the relevant procedure.
Although it would be possible to use a single switch procedure for all kinds of node, the use of separate procedures enables the translator to check some contextual errors – for example an arithmetic expression node presented to TranBoolExpr indicates the use of an arithmetic expression in a Boolean context – which chapter 3 and section IV argue should be checked by the translator rather than the syntax analyser. Each switch procedure accepts only a limited range of node-types: this makes the compiler as a whole more ‘self-checking’ since the switch procedures in effect partially check the structure of the tree which is built by the syntax analyser.
Node format
The translation procedures almost invariably have a parameter ‘nodep’, the value of which is a pointer to a tree node. Each node contains an identification field or ‘tag field’ called ‘type’ and several other fields, which in most cases contain pointers to other tree nodes. The value held in the ‘type’ field defines the node-type. Nodes of different types have different collections of fields with different names – e.g. the pointer fields of a binary arithmetic operation node are called ‘left’ and ‘right’, a conditional statement node contains pointer fields called ‘test’, ‘thenstat’ and ‘elsestat’, the symbol table descriptor of a variable
79 will contain a field called ‘address’, and so on. The expression ‘nodep.x’ accesses the field ‘x’ in the node pointed to by ‘nodep’.
Chapter 5
Translating Arithmetic
Expressions
Figure 5.1 shows an example arithmetic expression which might appear any- where in a typical program – say on the right-hand side of an assignment state- ment – together with the parse tree which describes its structure. It also shows the object code which is the best translation of this expression – ‘best’, that is, if the expression is considered in isolation. The example shows the point from which a compiler-writers’ discussion will start – what translation algorithm will generate this ‘best’ code in practice?
The first step in the solution of this problem is to devise a translator that will generate correct code for every arithmetical fragment: the tree-walking mechanism of chapter 2 will certainly do so. In figure 5.1 and in the examples below I assume that the object program must calculate the value of an expression in some register or other, in order that the value may be stored, passed as an argument, compared with another value, etc. It’s simplest also if I assume that registers 1,2, ... (up to some maximum register number ‘maxreg’) are available for the calculation of expressions. Figure 5.2 shows TranArithExpr and TranBinOp procedures which, when given the tree of figure 5.1 and the register number ‘1’, will produce the code shown in figure 5.3.
These procedures, for all their faults, generate code which would work if you ran it. As a compiler writer, I find that correctness of code is more impor- tant than efficiency of execution. Inefficient object programs are annoying, but incorrectly translated object programs are useless! It wouldn’t be reasonable, however, to accept the code generated by the procedures in figure 5.2 since it is so clearly possible to do better. To reiterate the lesson of chapter 2: a tree- walking translator can be improved by choosing strategies and tactics according to the characteristics of the node being translated.
If I incorporate the improvements sketched in chapter 2, the TranBinOp proce- dure of figure 5.2 becomes the procedure of figure 5.4. This procedure shows two
82 CHAPTER 5. TRANSLATING ARITHMETIC EXPRESSIONS
Expression: minutes + hours*60
Tree: +
minutes
*
hours 60 ‘Best’ code: LOAD 1, hours
MULTn 1, 60 ADD 1, minutes
Figure 5.1: A simple example expression
let TranArithExpr(nodep, regno) be switchon nodep.type into
{ case ‘+’: TranBinOp(ADD, nodep, regno); endcase case ‘-’: TranBinOp(SUB, nodep, regno); endcase case ‘*’: TranBinOp(MULT, nodep, regno); endcase case ‘/’: TranBinOp(DIV, nodep, regno); endcase
...
case name:
Gen(LOAD, regno, nodep.descriptor.address); endcase case number:
Gen(LOADn, regno, nodep.value); endcase
default: CompilerFail("invalid node in TranArithExpr")
}
let TranBinOp(op, nodep, regno) be
{ TranArithExpr(nodep.left, regno) TranArithExpr(nodep.right, regno+1)
Gen(op++‘r’, regno, regno+1) /* change XXX to XXXr */
}
83 LOAD 1, minutes LOAD 2, hours LOADn 3, 60 MULTr 2, 3 ADDr 1, 2
Figure 5.3: Straightforward object code
let TranBinOp(op, nodep, regno) be
{ let first, second = nodep.left, nodep.right /* test for <leaf> op <non-leaf> */
if IsLeaf(first) & not IsLeaf(second) then
{ /* interchange and reverse the operation */ first, second := nodep.right, nodep.left op := reverse(op) /* changes SUB to xSUB */
}
TranArithExpr(first, regno)
if IsLeaf(second) then
TranLeaf(op, second, regno)
else
{ TranArithExpr(second, regno+1) Gen(op++‘r’, regno, regno+1)
} }
let IsLeaf(nodep) = (nodep.type=name | nodep.type=number)
let TranLeaf(op, nodep, regno) be switchon nodep.type into
{ case name:
Gen(op, regno, nodep.descriptor.address); endcase case number:
Gen(op++‘n’, regno, nodep.value); endcase
default: CompilerFail("invalid arg. to TranLeaf")
}
84 CHAPTER 5. TRANSLATING ARITHMETIC EXPRESSIONS major improvements. First, it notices when the second sub-node is a name or a number, and in this case it calls TranLeaf, which generates only one instruction rather than two. Second, it notices when the left-hand sub-node is a name or a number and the right-hand sub-node is a more complicated expression and in this case it reverses the order of translation. The first improvement saves registers and instructions in many cases
LOAD n, alpha LOAD n+1, beta ADDr n, n+1
becomes LOAD n, alpha
ADD n, beta
and the second improvement allows this technique to become effective in more situations, when the original expression is ‘leaf op <expr>’ and could have been more efficiently written ‘<expr>opleaf’. Given the tree of figure 5.1, the TranBinOp procedure of figure 5.4 will generate exactly the ‘best’ possible code sequence. Although it isn’t possible to do better in this particular case, I show below how a similar mechanism can be developed which can be applied in more complicated situations.
5.1
Reverse Operations
Strictly speaking the translator can’t always reverse the order of node evaluation – ‘a-b’ is not the same as ‘b-a’. If it reverses the order of evaluation of a ‘-’ node, therefore, it must generate a different subtract instruction – a so-called ‘reverse subtract’. Prefix ‘x’ denotes a reverse instruction,1so whereas ‘SUB reg,store’
means “reg:=reg-store”, ‘xSUB reg,store’ means “reg:=store-reg”. This is the reason for the statement ‘op:=reverse(op)’ in figure 5.4. While the xADD and xMULT instructions are indistinguishable from ADD and MULT, unfortu- nately not every machine has a complete set of reverse instructions. Reverse divide or reverse subtract is often missing, for example.
Reverse operations are important only when the machine doesn’t have a com- plete set of ‘reversed’ instructions. In the absence of proper instructions, the reverse operation can take many more instructions than the forward variant. Figure 5.5 shows some example code for a machine without a reverse integer di- vide (xDIV) instruction – code (a) shows a solution which uses no more registers than necessary (but uses three instructions to carry out the division); code (b) shows a solution which performs the operation without reversing (using ‘only’ two instructions for the divide yet occupying an extra register during compu- tation of the right-hand operand); code (c) uses an extra register instead of a store location yet then must either leave the result in the ‘wrong’ register or must include an extra instruction to transfer it to the correct register; code (d) uses the minimum of registers by directing the right hand operand to another register;2 code (e) is only possible if the xDIV instruction is available.
1 I use ‘x’ rather than ‘r’ to avoid confusion between, say, xSUB (reverse subtract) and SUBr
5.2. REGISTER DUMPING 85