mod1fig5.gif (9955 bytes)

6.0 Introduction

Computer programs, like English sentences, have both a syntactic structure, illustrated by a parse tree, and a semantic structure which describes the meaning. Although there are many formal definitions of program semantics, we will take the operational semantics point of view. This approach defines the meaning of a program to be the executable code it generates. For us, this is the assembly language code. Thus, the actions performed by the semantic analysis phase are a begining of the process which will generate code.

Specifically, semantic analysis performs two major actions: (1) it finishes the syntax analysis and also performs actions such as symbol table creation and (2) it translates the parse tree to an intermediate representation more appropriate for the later phases of optimization and code generation.

6.1 Static Checking

The term static checking refers to error checks made at compile time. The opposite of static is dynamic, which refers to the time a program is executing. The error handing described in Module 5 finds static errors which do not correspond to the BNF description of that language. BNF is a metalanguage. A metalanguage is a way of describing another language. Thus, BNF describes a class of languages called context-free. Context-free languages can describe programming language syntactic structures such as statements and loop structures. Thus, if an END is left out, the error routines described in Module 5 will discover this.

However, context-free languages cannot describe the fact that a variable has been used but not defined or that a referenced label is not there. This requires contextual constraints which context-free grammars cannot specify.

There is a widely-accepted formalism, attribute grammars, for describing the range of semantic actions needed to do static checking of programming languages.

6.2 Attribute Grammars

An attribute grammar is a context-free grammar with the addition of attributes and attribute evaluation rules called semantic functions. Thus, an attribute grammar can specify both semantics and syntax while BNF specifies only the syntax.

6.2.1 Attributes

Attributes are variables to which values are assigned. Each attribute variable is associated with one or more nonterminals or terminals of the grammar.

In the grammar of Section 6.2.2, the attribute Value is associated with the nonterminals E, T, and F as well as with the terminal Constant. For nonterminal E, this is written:

This notation indicates that nonterminal "E" has an attribute called Value attribute names will be italicized.

6.2.2 Semantic Functions

Although it is possible to evaluate some attributes at parse time, we will asume that all attributes are evaluated after the program has been parsed. Values are assigned to local attributes by equations called semantic functions. Local attributes are those which fall within the scope of a production as it appears in the parse tree. For example:

       
Syntax                 Semantics
       E0  E1 + T         E0.Value := E1.Value + T.Value

Here, the production is EE+T. The subscripts are used only to distinguish the two E's. The attribute Value is associated with both E's and with T. If this production were in a parse tree, it would be denoted:

The value of the attribute Value is passed up the parse tree because E1.Value and T.Value are used to compute E0.Value. Such attributes are termed synthesized attributes. Attributes whose values are apssed down the tree are called inherited attributes.

Terminals may have only synthesized attributes, and their values are assigned by the lexical analyzer. Inherited values of the Start symbol. If any, are given values by way of parameters when attribute evaluation begins.

6.2.3 A Word about Example 1

It is common in computer science to use examples which explain the idea clearly, but which are not necessarily the best application of the idea. Thus, recursion is often introduced using factorial, a nice clear example, but not necessarily the best way to compute a factorial ( iteratively is faster and takes up less space). So it is with our first example of an attribute grammar. This example computes the value of the expression represented in a parse tree. Most compilers would not do this since their ultimate gola is to put out code (which then gets executed).

EXAMPLE 1 Using attributes to evaluate expressions

Consider the string 4+2*3 and its parse tree:

Using the semantic functions, and starting at the bottom of the tree, the various values of Value are computed. The lexical analyzer finds the value of Constant.Value. This is the value shown in the above parse tree.

6.2.4 A More Practical Example

Example 2 is a more practical one; it might be used by a symbol table routine to attach type information to the variables in a declaration.

EXAMPLE 2 Assigning declaration types to variables

6.2.5 Other Static Checks

Other static checks include type checking expressions to make sure operators are compatible with
their operands, checking that a variable used has been declared, checking that an array declared
to be of a certain dimension is used with the correct number of subscripts, and a myriad of other
tasks. Some static checks are language dependent since not all languages have the same
constraints. FORTRAN, for instance, does not require variables to be declared before being used.

6.3 Translation to an Intermediate
Representation

In order to perform optimizations on a program and to generate code, it is convenient to produce an intermediate representation better suited to these tasks than the parse tree representation. Abstract syntax trees (AST's), are used in many modern compilers. Diana, the intermediate language for Ada, is an attributed AST.

We will show the following intermediate representations:

6.3.1 Polish Postfix

Polish postfix is a linear line of code which is more useful for code generation that for the optimization phases since it is difficult to do the sorts of ransformations on it which are performed during the optimization phase.

Anyone who has ever used an old Hewlett Packard calculator is familiar with Polish postfix (for expressions). Essentially, Polish postfix puts the operands first followed by their operator.

Thus,

becomes

and

becomes

while

becomes

The assignment statement:

is denoted

It is more difficult to devise a reasonable postfix notation for other programming language constructs such as IF statements.

6.3.2 Abstract Syntax Tree (AST)

An abstract syntax tree is a parse tree stripped of unnecessary information such as singleton productions (e.g., ETF). Each nonleaf represents an operator and each leaf represents an operand.

Thus A + B might have the following parse tree and abstract syntax tree:

S := A + B * C has abstract syntax tree:

which can be represented in parenthesized form as

The parenthesized form is often used by the compiler designer for debugging output. It is essentially a preorder transversal of the tree.

IF A<B THEN Max := B is represented

and A[i]:=B is

Even though ASt's are more convenient for optimization, there is a one-one correspondence between AST's and Polish postfix:

Thus, AST's can be easily converted to Polish prefix if desired. The "tree" can be seen by starting at the right-hand end and taking the upper of the two arrows as the left child and the lower as the right child.

6.3.3 Three-Address Code

This intermediate form is called three-address because each "line" of code contains one operator and up to three operands, represented as addresses. Since most assembly languages represent a single operation in an instruction, three-address code is closer to the target code than the parse tree representation. There are a number of variants of three-address code, some more appropriate than others for optimization.

Quadruples (A Three-Address Code)

Quadruples ("quads" for short) consist of an operation, (up to) two operands, and a result.

A + B would be translated into quads as:

A + B * C would be translated into two quads:

         *    B    C    T1         
+    A    T1   T2

and S := A + B * C would be:

          
*   B    C     T1         
+   A    T1    T2         
=   T2   _     S

Notice that the last quad here had only one operand rather than two.

An alternative notation for quads, one we will use in the chapters on optimization, is to write them as a sequence of assignment statements. Thus S = A + B + C would be written:

        
T1 = B * C        
T2 = A + T1         
S = T2

If we think of GOTO L as an operator and a result, the quad would be:

A[i] := B would be:

    

[ ] A I T1 = T1 _ B

The reasoning for the intermediate code for IF statements is similar to the reasoning used to implement such constructs into assembly language. For example, IF A<B THEN Max := B would be:

     
<        A   B   Label 1     
GOTO             Label 2                      
Label 1     =        B       Max                      
Label 2

Here, the first line can be thought of as "if A<B then execute the code at Label 1". The second line can be thought of as "Otherwise execute the code at Label 2". The third line has only a result field--Label 1. These lines can be translated easily into assembly language code.

Triples(A Three-Address Code)

Quadruples use a name, sometimes called a temporary name or "temp", to represent the single operation. Triples are a form of three-address code which do not use an extra temporary variable; when a reference to another triple's value is needed, a pointer to that triple is used. We show the same examples as above for triples:

A + B would be translated into triples as:

A + B * C would be translated into two triples:

   
(1)   *   B   C   
(2)   +   A  (1)

Here, the triples have been numbered and a reference to the first triple is used as an operand of the second. S := A + B * C would be

  
(1)   *   B    C   
(2)   +   A   (1)   
(3)   =   S   (2)

Triples are really an abstract syntax tree in disguise:

Triples are difficult to optimize because optimization involves moving intermediate code. When a triple is moved, any other triple referring to it must be updated also. A variant of triples called indirect triples is easier to optimize.

Indirect Triples(A Three-Address Code)

Here, the triples are separated from their execution order:

Since the execution order here is the same as the order of the triples themselves, it is difficult to see the use of indirect triples. Example 3 shows a case where the execution order is not the same as the order of the indirectriple list.

With indirect triples, optimization can change the execution order, rather than the triples themselves, so few references need be changed.

EXAMPLE 3 Optimizing indirect triples

As an indication of how intermediate form might be generated during parsing, Section 6.3.6 describes how to generate abstract syntax trees during recursive descent parsing. Similar (semantic) routines would be written for generating AST's if the parser were generated using a tool. Most tools allow such routines to be written.

6.3.4 Other Intermediate Representations

Strictly speaking, any form between the source program and the object program might be
called an intermediate representation. The optimization process often creates a graphical
representation called a control flow graph. A parse tree or an abstract syntax tree with attached
attributes is often called an attribute tree or a semantic tree - yet another possible intermediate
form.

6.3.5 Arrays

Array references are often translated to intermediate code with an array operator and two children. The left child is the name of the array, and the right child is the subscript expression.

Thus,

as an abstract syntax tree would be

and as quadruples would be

Using the notation, the code generator would have to compute the array offset for machines which do not have an indexing addressing mode. The alternative is to expand the intermediate representation.

Using "( )" to mean "contents of" and "addr" to mean "address of," the quadruples for "Temp = List[i]" would be:

        T1 = addr(List)         T2 = T1 + i        Temp = (T2)

Similarly, if the array reference is on the left-hand side of an assignment statement as in "List[i] = Temp", low-level quadruples would be:

        T1 = addr(List)         T2 = T1 + i        (T2) = Temp 

Even if a machine has an indexing addressing mode, translating array references into their low-level format may allow optimizations (for example, if the array subscript were a common subexpression).

6.3.6 Adding AST Routines to Recursive
Descent Parsing

An abstract syntax tree node consists of an information field and a number of pointer fields. We will show an example for the expression grammar with assignment statements added and presume a binary tree so that there are two pointer fields to a left and right child in addition to the information field.

Thus if an AST node is called Node, these three field will be denoted as Node.Info, Node.Left and Node.Right. Get(Node) will create a new emplty AST node.

The method consists of adding a parameter to each of the procedures which will carry the (partial) AST from procedure to procedure, adding on to it as appropriate.

Consider the recursive descent procedure for an assignment statement:

    {Assignment  Variable = Expression}

PROCEDURE Assignment (Tree:AST)

{...

IF Next Token = Variable THEN

Get(Node)

Node.Info = Variable

Node.Left = Nil

Node.Right = Nil

Tree = Node

IF NextToken is "=" THEN

Get(Node)

Node.Info = "="

Node.Left = Tree

Node.Right = Nil

Tree = Node

Expression (Tree.Right)

....

}

Following this pseudo-code for the assignment statement:

we obtain the following abstract syntax tree, presuming the call to Expression(Tree.Right) returns a node whose information field contains B and Tree.Right is a pointer to it:

To show this, we will continue this process for Expression, Term and Factor (in outline form):

    {Expression  Term {+Term} }

PROCEDURE Expression (Tree:AST)

{...

Term(Tree)

WHILE Next Token = "+" DO

Get(Node)

Node.Info = "+"

Node.Left = Tree

Term (Tree.Right)

Tree = Node

...

}

    {Term  Factor {* Factor} }

PROCEDURE Term (Tree:AST)

{...

Factor(Tree)

WHILE Next Token = "*" DO

Get(Node)

Node.Info = "*"

Node.Left = Tree

Factor (Tree.Right)

Tree = Node

...

}

    {Factor  Const | ( Expression ) }

PROCEDURE Factor (Tree:AST)

{...

IF Next Token <> "(" THEN

Get(Node)

Node.Info = Token

Node.Left = Nil

Node.Right = Nil

Tree = Node

...

}

Try tracing this for A = B and A = B + C * D

6.4 Semantic Analyzer Generators

      A semantic analyzer generator does not follow the table/ driver model developed in the previous chapters. The input, an attribute grammar, is more of a specification language than a true metalanguage.

      Most parser generators allow semantic actions to be attached to the productions in the BNF. A few allow these semantic actions to be described using attribute grammars.

The amount of processing done at generation time depends on the attribute grammar itself. Consider the picture shown in Figure 1:

      In some cases, the evaluator generator will be null, that is, an evaluator will not be generated. If the generator is null, it may be that the attributes were in a form that could be evaluated during parsing. Another possibility is that the attributes are evaluated one. We will discuss all of these possibilities.

6.5 More on Attribute Grammars

Attribute grammars are a formalism for expressing semantics much as a context-free grammar is a formalism for expressing syntax. We will look at some further definitions relating to attribute grammars.

6.5.1 Fundamental Definitions

As we know, an attribute grammar is a context-free grammar to which attribute and semantic functions have been added:

      Consider a production p in the set of production P :

              p : X0 X1 X2 X3. . . Xn                    n > = 1

      For any Xi in the production p, there may be finite disjoint sets I(Xi) and S(Xi.) The are the inherited attributes and synthesized attributes, respectively.

      In general, the values of inherited attributes are passed down the parse tree. The value of synthesized attributes are passed up the parse tree.

      The set of all attributes for X i is denoted A(Xi):

              A(Xi) = I(Xi) U S(Xi)

      An attribute a of Xi is denoted Xi.a whether it is a reference to attribute a in the parse tree or the grammar.

      Predefined attribute values are called intrinsic. Synthesized, intrinsic attributes of terminals are computed by the lexical analysis phase. Inherited intrinsic attributes of the start symbol are passed as parameters before evaluation begins from the parser.

      A production p : X0 X1 X2 X3. . . Xn possesses an attribute occurrence, (a, k), if Xk is at node #m ( in some tree-numbering scheme), then we say Xk possesses an attribute realization, (a, m), if Xk has an attribute a. A semantic function, sometimes called an attribute rule, gives a value to an attribute occurence(a, k), in production p. Such a function is denoted fp(a,k)

      The set of values upon which fp(a,k)depends is called the dependency set for (a, k) and is denoted Dp(a,k). Thus,

                Dp(a,k) = { (b, j) | fp(a,k) = g (. . .Xj.b, . . . ) }

      This is read "the value of attribute occurrence (a, k), or Xk. a depends on the set of attributes { (b, j) } or { Xj.b } where each Xj.b is used in the calculation of Xk. a".

      Example 1 illustrates these definitions. This example is an adaptation from Knuth (1968) where he first defined attribute grammars.

      The attributes in Example 1 are:

       I (Number)                 =
       I (Sign)                    =
       I (List)                    = {Scale}
              I (BinaryDigit)      = {Scale}
       S (Number)                = { Value }
       S (Sign)                    = { Neg }
       S (List)                    = {Value }
       S (BinaryDigit)      = {Value }

Thus, Scale is an inherited attribute, and Value and Neg are synthesized attributes. The intrinsic attributes are Sign.Neg in productions one and two, BinaryDigit.Value in production five, and List.Scale in production zero.

      The function to compute List0.Value in production four is denoted f4(Value, 0).

      Since depends List0.Value depends on List1.Value and BinaryDigit.Value,

                D4(Value, 0) = { (Value, 1 ) , (Value, 2) }

      Example 1 generates strings of binary digits. Example 2 shows an unevaluated parse tree for the string - 1 0:

      Section 6.6 discusses efficient methods for attribute evaluation, but we can "guess" at a method here: since we know      value is a synthesized attribute, and Scale is an inherited attribute, we can try to evaluate values by moving up the tree, then down, or by moving down the tree, then up. The latter suffices, and Example 3 shows the evaluated tree.

      Example 3 shows that the "semantics" of this example computes the decimal value of the string and leaves this value at the root of the tree.

     Example 1 through 3 are a short and simple illustration of attribute grammars. It is possible to evaluate all the attributes in one pass down and then up the parse tree.

When a grammar is large and contains many attributes, it is not always easy to see how to evaluate the attributes. One method is to restrict the form of the attributes and attribute functions so that the method of evaluation is known. A second way is to create a graph of dependencies and check to see that it has no cycles. If it has no cycles, the dependency graph itself can be used to give an order in which to evaluate the attributes. We discuss all of these in the next sections.

      Example 5 shows calculation of array offsets. This calculation might be performed in order to allocate the relative position of each array element.

      Exercise 5 asks the reader to create attributes that will pass information about the array A (the dimensions d1, d2, d3) down the tree and then to calculate this offset while reascending the tree. Exercise 6 asks the reader to consider a different grammar for this same example.

6.5.2 L-Attributed Attribute Grammars

A top-down parser can evaluate attributes as it parses if the attribute values can be computed in a top-down fashion. Such attribute grammars are termed L-Attributed.       First, we introduce a new type of symbol called an action symbol. Action symbols appear in the grammar in any place a terminal or nonterminal may appear. They may also have their own attributes. They may, however, be pushed onto their own stack, called a semantic stack or attribute stack.

      We illustrate action symbols using the notation "<>" which indicates that the symbol within the brackets is to be pushed onto the semantic stack when it appears at the top of the parse stack. By inserting this action in appropriate places, we will create a translator which converts from infix expressions to postfix expressions.

We parse and translate a + b * c. The top is on the left for both stacks.

When the semantic stack is popped, the translated string is:

           a b c * +

the input string translated to postfix. In Example 6, the action symbol did not have any attached attributes.

     The BNF in Example 6 is in LL(1) form. This is necessary for the top-down parse.

      The formal definition of an L-attributed grammars is as follows. An attribute grammar is L-attributed if and only if for each production X0 X1 X2. . . Xi. . . Xn,

 (1) {Xi.inh} = f ({Xj.inh} , {Xk.att})            i, j >= 1, 0<=k<i

 (2) {X0.syn} = f ({X0.inh} , {Xj.att})            1<=j<=n

 (3) {ActionSymbol.Syn} = f ({ActionSymbol.Inh})

     (1) says that each inherited attribute of a symbol on the right-hand side depends only on inherited attributes of the right-hand side and arbitrary attributes of the symbols to the left of the given right-hand side symbol.

     (2) says that each synthesized attributes of the left-hand-side symbol depends only on inherited attributes of that symbol and arbitrary attributes of right-hand-side symbols.

     (2) says that the synthesized attributes of any action symbol depend only on the inherited attributes of the action symbol.

      Conditions (1), (2), and (3) allow attributes to be evaluated in one left-to-right pass (see Exercise 2).

      If the underlying grammar is LL(1), then an L-attributed grammar allows attributes to be evaluated while parsing. The reader may wish to review the LL(1) parsing driver in Chapter 4. The evaluation algorithm is:

      Exercise 3 asks the reader to combine this algorithm with the LL(1) driver algorithm given in Chapter 4.

      For the next few sections, we focus on restrictions to attributes that allow for efficient attribute evaluation.

6.5.3 S-Attributed Attribute Grammars

Attribute in an S-attributed grammar can be evaluated at parse time by a bottom-up parser. Interestingly, these grammars form a subset of the L-attributed grammars.

      An attribute grammar is S-attributed if and only if:

           This allows attributes to be evaluated during LR-parsing.

      L-attributed and S-attributed grammars allow efficient evaluation---either during parsing or as a single pass after parsing.

      In Section 6.6, we see another method and a different attribute restriction that allows attribute evaluation during top-down parsing.

     For the next few sections, we focus on restrictions to attributes that allow for efficient attribute evaluation.

6.5.4 Noncircular Attribute Grammars

If the calculation of an attribute comes around to using its own value in the calculation, then we have a circularity. We will, however, need a more formal definition of circularity in terms of dependency graphs.

Dependency Graph of a Production

The dependency graph of a production, p, is a set of vertices and edges. The vertices are the attribute occurrences (see Section 6.1.1) and the edges are dependencies.

Formally, for each production

            p: Xo X1 X2. . . Xn,

           DVp = { (a, k) | a is in A(Xk), 0<=k<=n }

and

           DEp = {<(b, i) ,(a, k)> | < (b,i) is in Dp(a, k)}

Thus, if O1 is in the dependency set for O2, then there is an arc between attribute occurrence O1 and attribute occurrence O2.

In Example 7,

         DV4 = { (Value, 0), (Scale, 0), (Value, 1), (Scale, 1), (Value, 2), (Scale, 2) }

         DE4 = { <(Value, 1), (Value, 0)>, <(Scale, 0), (Scale, 1)>, <(Value, 2), (Value, 0)>, <(Scale, 0), (Scale, 2)> }

     The dependency graph for an input program is produced by merging the dependency graphs for each production used to parse the program. It is defined in terms of the parse tree, T.

     Since the dependency graph for an input is defined by augmenting the nodes of the parse tree with attributes, the definition of noncircularity is in terms of this parse tree:

     An attribute grammar G is noncircular if there does not exist even one tree, T, such that DGT contains a cycle.

     Attribute grammars which are circular are undesirable. Knuth (1968, 1971) presents an algorithm which tests a grammar for noncircularity. It is exponential in the worst case. Jazayeri et al. (1975) show that there is no algorithm that tests a grammar for circularity which is not exponential in the worse case. Fortunately, most grammars do not exhibit this worst case behavior.

6.5.5 Absolutely Noncircular Attribute
Grammars

In Section 6.6 we present some efficient attribute evaluation methods. One of them requires that the attribute grammar be noncircular in an even stronger way than the noncircular definition above.

      We start with the dependency graphs for the productions of a grammar. Figure 2 shows the nodes of the dependency graph for the binary digit grammar.

      Example 8 shows these graphs merged to create the dependency graph for a parse tree, T. List's Scale attribute is handed down the tree to the BinaryDigit node where it is used to calculate the value of Scale. This value is handed back up the tree, changing as it goes, but ultimately it is used to calculate List's Value attribute. In functional notation, we would write:

               List.Value = f (List.Scale)

where function, f (the accumulation of the changes to Scale as it descends the tree), is used to compute Value and then is handed back up to the node. We indicate these closure steps by drawing an arc in the list node labeled with an asterisk, and we label the nodes themselves with asterisks, calling the new graph augmented dependency graphs.

      With augmented dependency graphs defined, we can define absolute noncircularity. An attribute grammar is absolutely noncircular if none of the augmented dependency graphs, DG*p, contains a cycle.

     Absolute noncircularity is a stronger restriction than noncircularity.

6.7 Summary

This module decsribes semantic analysis, the bridge between the front-end and the back-end of compiling. The specification of semantics is often done using attribute grammars, and much of this module is devoted to attribute grammars. Attributes are evaluated at compile-time; however, some processing toward generation of the evaluator may be moved back to compiler generation time.

Grammatical restrictions and efficient evaluation are interconnected in many cases. Thus, the attributes described by L-attributed grammars can be evaluated at parse-time by a top-down parser or after the parse in a single pass down the tree.

The attributes described by S-attributed grammars can be evaluated at parse-time by a bottom-up parser or after the parse by a single pass up the tree.

For unrestricted attribute grammars, tree-walk evaluators may require multiple passes over the parse tree. More efficient evaluators may be obtained by creating a dependency graph from the parse tree and the semantic functions. A dependency graph ensures that the attributes upon which a semantic function depends are evaluated first.

Another task of semantic analysis is creation of an intermediate representation of the program.

Yet another task of semantic analysis is Symbol Table creation.