Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Cooper K.Engineering a compiler

.pdf
Скачиваний:
53
Добавлен:
23.08.2013
Размер:
2.31 Mб
Скачать

4.6. SUMMARY AND PERSPECTIVE

129

(a)Rewrite the grammar to enforce the restriction grammatically.

(b)Similarly, the language allows only a limited set of combinations of TypeSpecifier. long is allowed with either int or float; short is allowed only with int. Either signed or unsigned can appear with any form of int. signed may also appear on char. Can these restrictions be written into the grammar?

(c)Propose an explanation for why the authors structured the grammar as they did. (Hint: the scanner returned a single token type for any of the StorageClass values and another token type for any of the

TypeSpecifiers.)

(d)How does this strategy a ect the speed of the parser? How does it change the space requirements of the parser?

3.Sometimes, a language design will include syntactic constraints that are better handled outside the formalism of a context-free grammar, even though the grammar can handle them. Consider, for example, the following “check o ” keyword scheme:

phrase

keyword α β γ δ ζ

γ

γ-keyword

α

α-keyword

 

|

 

δ

δ-keyword

 

|

 

 

|

 

β

β-keyword

ζ

ζ-keyword

 

|

 

 

|

 

with the restrictions that α-keyword, β-keyword, γ-keyword, δ-keyword, and ζ-keyword appear in order, and that each of them appear at most once.

(a)Since the set of combinations is finite, it can clearly be encoded into a series of productions. Give one such grammar.

(b)Propose a mechanism using ad hoc syntax-directed translation to achieve the same result.

(c)A simpler encoding, however, can be done using a more permissive grammar and a hard-coded set of checks in the associated actions.

(d)Can you use an -production to further simply your syntax-directed translation scheme?

132

CHAPTER 4. CONTEXT-SENSITIVE ANALYSIS

Chapter 6

Intermediate

Representations

6.1Introduction

In designing algorithms, a critical distinction arises between problems that must be solved online, and those that can be solved o ine. In general, compilers work o ine—that is, they can make more than a single pass over the code being translated. Making multiple passes over the code should improve the quality of code generated by the compiler. The compiler can gather information in one pass and use that information to make decisions in later passes.

The notion of a multi-pass compiler (see Figure 6.1) creates the need for an intermediate representation for the code being compiled. In translation, the compiler must derive facts that have no direct representation in the source code—for example, the addresses of variables and procedures. Thus, it must use some internal form—an intermediate representation or ir—to represent the code being analyzed and translated. Each pass, except the first, consumes ir. Each pass, except the last, produces ir. In this scheme, the intermediate representation becomes the definitive representation of the code. The ir must be expressive enough to record all of the useful facts that might be passed between phases of the compiler. In our terminology, the ir includes auxiliary tables, like a symbol table, a constant table, or a label table.

Selecting an appropriate ir for a compiler project requires an understanding of both the source language and the target machine, of the properties of programs that will be presented for compilation, and of the strengths and weaknesses of the language in which the compiler will be implemented.

Each style of ir has its own strengths and weaknesses. Designing an appropriate ir requires consideration of the compiler’s task. Thus, a source-to- source translator might keep its internal information in a form quite close to the source; a translator that produced assembly code for a micro-controller might use an internal form close to the target machine’s instruction set. It requires

133

134

 

 

CHAPTER 6.

INTERMEDIATE REPRESENTATIONS

source

 

 

IR

 

 

IR

 

 

target

front

 

middle

back

 

code

 

 

 

 

 

code

 

end

 

 

end

 

end

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Multi-pass compiler

Figure 6.1: The role of irs in a multi-pass compiler

consideration of the specific information that must be recorded, analyzed, and manipulated. Thus, a compiler for C might have additional information about pointer values that are unneeded in a compiler for Perl. It requires consideration of the operations that must be performed on the ir and their costs, of the range of constructs that must be expressed in the ir; and of the need for humans to examine the ir program directly.

(The compiler writer should never overlook this final point. A clean, readable external format for the ir pays for itself. Sometimes, syntax can be added to improve readability. An example is the symbol used in the iloc examples throughout this book. It serves no real syntactic purpose; however, it gives the reader direct help in separating operands from results.)

6.2Taxonomy

To organize our thinking about irs, we should recognize that there are two major axes along which we can place a specific design. First, the ir has a structural organization. Broadly speaking, three di erent organizations have been tried.

Graphical irs encode the compiler’s knowledge in a graph. The algorithms are expressed in terms of nodes and edges, in terms of lists and trees. Examples include abstract syntax trees and control-flow graphs.

Linear irs resemble pseudo-code for some abstract machine. The algorithms iterate over simple, linear sequences of operations. Examples include bytecodes and three-address codes.

Hybrid irs combine elements of both structural and linear irs, with the goal of capturing the strengths of both. A common hybrid representation

uses a low-level linear code to represent blocks of straight-line code and a graph to represent the flow of control between those blocks.1

The structural organization of an ir has a strong impact on how the compiler writer thinks about analyzing, transforming, and translating the code. For example, tree-like irs lead naturally to code generators that either perform a

1We say very little about hybrid irs in the remainder of this chapter. Instead, we focus on the linear irs and graphical irs, leaving it to the reader to envision profitable combinations of the two.

6.2. TAXONOMY

135

tree-walk or use a tree pattern matching algorithm. Similarly, linear irs lead naturally to code generators that make a linear pass over all the instructions (the “peephole” paradigm) or that use string pattern matching techniques.

The second axis of our ir taxonomy is the level of abstraction used to represent operations. This can range from a near-source representation where a procedure call is represented in a single node, to a low-level representation where multiple ir operations are assembled together to create a single instruction on the target machine.

To illustrate the possibilities, consider the di erence between the way that a source-level abstract syntax tree and a low-level assembly-like notation might represent the reference A[i,j] into an array declared A[1..10,1..10]).

 

 

 

 

 

 

 

 

 

 

 

 

 

 

subscript

 

,

 

@

 

,,

 

 

?

 

@R@

 

 

 

A n i n j n

abstract syntax tree

load

1

r1

sub

rj , r1

r2

loadi

10

r3

mul

r2, r3

r4

sub

ri, r1

r5

add

r4, r5

r6

loadi

@A

r7

loadAO

r7, r6

rAij

low-level linear code

In the source-level ast, the compiler can easily recognize that the computation is an array reference; examining the low-level code, we find that simple fact fairly well obscured. In a compiler that tries to perform data-dependence analysis on array subscripts to determine when two di erent references can touch the same memory location, the higher level of abstraction in the ast may prove valuable. Discovering the array reference is more di cult in the low-level code; particularly if the ir has been subjected to optimizations that move the individual operations to other parts of the procedure or eliminate them altogether. On the other hand, if the compiler is trying to optimize the code generated for the array address calculation, the low-level code exposes operations that remain implicit in the ast. In this case, the lower level of abstraction may result in more e cient code for the address calculation.

The high level of abstraction is not an inherent property of tree-based irs; it is implicit in the notion of a syntax tree. However, low-level expression trees have been used in many compilers to represent all the details of computations, such as the address calculation for A[i,j]. Similarly, linear irs can have relatively highlevel constructs. For example, many linear irs have included a mvcl operation2 to encode string-to-string copy as a single operation.

On some simple Risc machines, the best encoding of a string copy involves clearing out the entire register set and iterating through a tight loop that does a multi-word load followed by a multi-word store. Some preliminary logic is needed to deal with alignment and the special case of overlapping strings. By

2The acronym is for move character long, an instruction on the ibm 370 computers.

136

CHAPTER 6. INTERMEDIATE REPRESENTATIONS

using a single ir instruction to represent this complex operation, the compiler writer can make it easier for the optimizer to move the copy out of a loop or to discover that the copy is redundant. In later stages of compilation, the single instruction is expanded, in place, into code that performs the copy or into a call to some system or library routine that performs the copy.

Other properties of the ir should concern the compiler writer. The costs of generating and manipulating the ir will directly e ect the compiler’s speed. The data space requirements of di erent irs vary over a wide range; and, since the compiler typically touches all of the space that is allocated, data-space usually has a direct relationship to running time. Finally, the compiler writer should consider the expressiveness of the ir—its ability to accommodate all of the facts that the compiler needs to record. This can include the sequence of actions that define the procedure, along with the results of static analysis, profiles of previous executions, and information needed by the debugger. All should be expressed in a way that makes clear their relationship to specific points in the ir.

6.3Graphical IRs

Many irs represent the code being translated as a graph. Conceptually, all the graphical irs consist of nodes and edges. The di erence between them lies in the relationship between the graph and the source language program, and in the restrictions placed on the form of the graph.

6.3.1Syntax Trees

The syntax tree, or parse tree, is a graphical representation for the derivation, or parse, that corresponds to the input program. The following simple expression grammar defines binary operations +, , ×, and ÷ over the domain of tokens number and id.

Goal

 

 

 

 

 

 

 

 

 

 

 

?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

XX

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Expr

XXX

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

,

?

 

 

 

 

 

 

 

 

 

 

 

 

 

,

 

 

 

 

XzX

 

 

 

 

 

 

Goal

Expr

 

 

 

 

Expr

+

 

 

 

 

Term

 

 

 

 

 

 

 

 

 

 

 

 

 

 

,

 

@

 

 

 

Expr

Expr + Term

 

 

 

 

?

 

 

 

 

,

 

 

?R@

 

 

 

 

 

 

 

 

 

|

Expr

Term

 

 

Term

 

 

 

Term

×

Factor

 

Term

 

,

 

@

 

 

,

 

@

 

 

 

 

 

 

 

|

 

 

,

 

 

?R@

 

,

 

 

?R@

 

 

 

 

 

?

Term

Term × Factor

 

 

 

 

 

 

 

 

Term

×

Factor

Term

× Factor

y

 

|

Term ÷ Factor

 

?

 

 

 

 

?

 

?

 

 

 

 

 

?

 

 

 

 

|

Factor

 

Factor

 

 

 

2

Factor

 

 

 

2

 

 

 

Factor

Number

 

 

?

 

 

 

 

 

 

?

 

 

 

 

 

 

 

 

 

 

|

Id

 

 

x

 

 

 

 

 

x

 

 

 

 

 

 

 

 

 

Simple Expression Grammar

 

Syntax tree for x × 2 + x × 2 × y

The syntax tree on the right shows the derivation that results from parsing the expression x × 2 + x × 2 × y. This tree represents the complete derivation, with

6.3. GRAPHICAL IRS

137

a node for each grammar symbol (terminal or non-terminal) in the derivation. It provides a graphic demonstration of the extra work that the parser goes through to maintain properties like precedence. Minor transformations on the grammar can reduce the number of non-trivial reductions and eliminate some of these steps. (See Section 3.6.2.) Because the compiler must allocate memory for the nodes and edges, and must traverse the entire tree several times, the compiler writer might want to avoid generating and preserving any nodes and edges that are not directly useful. This observation leads to a simplified syntax tree.

6.3.2Abstract Syntax Tree

The abstract syntax tree (ast) retains the essential structure of the syntax tree, but eliminates the extraneous nodes. The precedence and meaning of the expression remain, but extraneous nodes have disappeared.

,,+PPPPqP

× ×

x ,, @R@2 × ,, @R@ y x ,, @@R2

Abstract syntax tree for x × 2 + x × 2 × y

The ast is a near source-level representation. Because of its rough correspondence to the parse of the source text, it is easily built in the parser.

Asts have been used in many practical compiler systems. Source-to-source systems, including programming environments and automatic parallelization tools, generally rely on an ast from which the source code can be easily regenerated. (This process is often called “pretty-printing;” it produces a clean source text by performing an inorder treewalk on the ast and printing each node as it is visited.) The S-expressions found in Lisp and Scheme implementations are, essentially, asts.

Even when the ast is used as a near-source level representation, the specific representations chosen and the abstractions used can be an issue. For example, in the Rn Programming Environment, the complex constants of Fortran programs, written (c1,c2), were represented with the subtree on the left. This choice worked well for the syntax-directed editor, where the programmer was able to change c1 and c2 independently; the “pair” node supplied the parentheses and the comma.

 

 

 

 

 

 

 

 

pair

 

 

 

 

 

 

, R@

 

 

const.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

?

 

 

c1

c2

 

 

 

 

 

(c1, c2)

 

 

 

 

 

 

 

 

 

ast for editing

ast for compiling

138

CHAPTER 6. INTERMEDIATE REPRESENTATIONS

Digression: Storage E ciency and Graphical Representations

Many practical systems have used abstract syntax trees to represent the source text being translated. A common problem encountered in these systems is the size of the ast relative to the input text. The ast in the Rn Programming Environment took up 1,000 bytes per Fortran source line—an amazing expansion. Other systems have had comparable expansion factors.

No single problem leads to this explosion in ast size. In some systems, it is the result of using a single size for all nodes. In others, it is the addition of myriad minor fields used by one pass or another in the compiler. Sometimes, the node size increases over time, as new features and passes are added.

Careful attention to the form and content of the ast can shrink its size. In Rn, we built programs to analyze the contents of the ast and how it was used. We combined some fields and eliminated others. (In some cases, it was less expensive to recompute information than to record it, write it, and read it.) In a few cases, we used hash-linking to record unusual facts—using one bit in the field that stores each node’s type to indicate the presence of additional information stored in a hash table. (This avoided allocating fields that were rarely used.) To record the ast on disk, we went to a preorder treewalk; this eliminated any internal pointers.

In Rn, the combination of all these things reduced to size of the ast in memory by roughly 75 percent. On disk, after the pointers were squeezed out, the files were about half that size.

However, this abstraction proved problematic for the compiler. Every part of the compiler that dealt with constants needed to include special case code to handle complex constants. The other constants all had a single const node that contained a pointer to a textual string recorded in a table. The compiler might have been better served by using that representation for the complex constant, as shown on the right. It would have simplified the compiler by eliminating much of the special case code.

6.3.3Directed Acyclic Graph

One criticism of an ast is that it represents the original code too faithfully. In the ongoing example, x × 2 + x × 2 × y, the ast contains multiple instances of the expression x × 2. The directed acyclic graph (dag) is a contraction of the ast that avoids unnecessary duplication. In a dag, nodes can have multiple parents; thus, the two occurrences of x × 2 in our example would be represented with a single subtree. Because the dag avoids duplicating identical nodes and edges, it is more compact than the corresponding ast.

For expressions without assignment, textually identical expressions must produce identical values. The dag for our example, shown in Figure 6.2, reflects this fact by containing only one copy of x × 2. In this way, the dag encodes an explicit hint about how to evaluate the expression. The compiler can generate code that evaluates the subtree for x × 2 once and uses the result twice; know-

6.3. GRAPHICAL IRS

139

+

@@R× ×?,, @R@y

x ,, @@R2

Figure 6.2: dag for x × 2 + x × 2 × y

ing that the subtrees are identical might let the compiler produce better code for the whole expression. The dag exposed a redundancy in the source-code expression that can be eliminated by a careful translation.

The compiler can build a dag in two distinct ways.

1.It can replace the constructors used to build an ast with versions that remember each node already constructed. To do this, the constructors would record the arguments and results of each call in a hash table. On each invocation, the constructor checks the hash table; if the entry already exists, it returns the previously constructed node. If the entry does not exist, it builds the node and creates a new entry so that any future invocations with the same arguments will find the previously computed answer in the table. (See the discussion of “memo functions” in Section 14.2.1.)

2.It can traverse the code in another representation (source or ir) and build the dag. The dag construction algorithm for expressions (without assignment) closely resembles the single-block value numbering algorithm (see Section 14.1.1). To include assignment, the algorithm must invalidate subtrees as the values of their operands change.

Some Lisp implementations achieve a similar e ect for lists constructed with the cons function by using a technique called “hash-consing.” Rather than simply constructing a cons node, the function consults a hash table to see if an identical node already exists. If it does, the cons function returns the value of the preexisting cons node. This ensures that identical subtrees have precisely one representation. Using this technique to construct the ast for x × 2 + x × 2 × y would produce the dag directly. Since hash-consing relies entirely on textual equivalence, the resulting dag could not be interpreted as making any assertions about the value-equivalence of the shared subtrees.

6.3.4Control-Flow Graph

The control-flow graph (cfg) models the way that the control transfers between the blocks in the procedure. The cfg has nodes that correspond to basic blocks in the procedure being compiled. Edges in the graph correspond to possible

140

CHAPTER 6.

INTERMEDIATE REPRESENTATIONS

 

if (x = y)

if (x = y)

 

,

@

 

then stmt1

,

R@

 

stmt1

stmt2

 

else stmt2

 

@

,

 

stmt3

R@ stmt3

,

 

A simple code fragment

Its control-flow graph

Figure 6.3: The control-flow graph

transfers of control between basic blocks. The cfg provides a clean graphical representation of the possibilities for run-time control flow. It is one of the oldest representations used in compilers; Lois Haibt built a cfg for the register allocator in the original Ibm Fortran compiler.

Compilers typically use a cfg in conjunction with another ir. The cfg represents the relationships between blocks, while the operations inside a block are represented with another ir, such as an expression-level ast, a dag, or a linear three-address code.

Some authors have advocated cfgs that use a node for each statement, at either the source or machine level. This formulation leads to cleaner, simpler algorithms for analysis and optimization; that improvement, however, comes at the cost of increased space. This is another engineering tradeo ; increasing the size of the ir simplifies the algorithms and increases the level of confidence in their correctness. If, in a specific application, the compiler writer can a ord the additional space, a larger ir can be used. As program size grows, however, the space and time penalty can become significant issues.

6.3.5Data-dependence Graph or Precedence Graph

Another graphical ir that directly encodes the flow of data between definition points and use points is the dependence graph, or precedence graph. Dependence graphs are an auxiliary intermediate representation; typically, the compiler constructs the dg to perform some specific analysis or transformation that requires the information.

To make this more concrete, Figure 6.4 reproduces the example from Section 1.3 on the left and shows its data-dependence graph is on the right. Nodes in the dependence graph represent definitions and uses. An edge connects two nodes if one uses the result of the other. A typical dependence graph does not model control flow or instruction sequencing, although the latter can be be inferred from the graph (see Chapter 11). In general, the data-dependence graph is not treated as the the compiler’s definitive ir. Typically, the compiler maintains another representation, as well.

Dependence graphs have proved useful in program transformation. They are used in automatic detection of parallelism, in blocking transformations that improve memory access behavior, and in instruction scheduling. In more sophisticated applications of the data-dependence graph, the compiler may perform

Соседние файлы в предмете Электротехника