Attribute Grammars - PPL
Attribute Grammars - PPL
Static Semantics
BNF is a notation used to describe the syntax of programming languages. However, certain
characteristics, like type compatibility rules, are challenging to express solely with BNF.
For instance, in Java, stating that a floating-point value cannot be assigned to an integer type
variable isn't straightforward with BNF without adding complexity.
If all typing rules of a language were to be specified in BNF, the resulting grammar would
become overly large and complex. This is problematic because the size of the grammar
directly affects the efficiency of the syntax analyzer used during compilation.
Certain language rules, such as the requirement that variables must be declared before they are
referenced, cannot be expressed using BNF. This limitation is due to the nature of BNF,
which is designed to describe the syntax rather than the static semantics of a language.
These issues showcase the types of language rules known as static semantics rules.
The static semantics of a language is only indirectly related to the meaning of programs
during execution; rather, it has to do with the legal forms of programs (syntax rather
than semantics).
Many static semantic rules revolve around type constraints, specifying how types can interact
within the language and ensuring type safety.
Due to the limitations of BNF in describing static semantics, more powerful mechanisms like
attribute grammars have been developed. Attribute grammars, introduced by Donald Knuth in
1968, are a formalism that extends context-free grammars to describe both syntax and static
semantics of programming languages.
Attribute grammars provide a structured way to define attributes associated with
language constructs and specify rules for computing these attributes. They allow for the
formal description and verification of static semantic rules, enabling the checking of
correctness during compilation.
Basic Concepts
Attribute grammars are context-free grammars to which have been added attributes, attribute
computation functions, and predicate functions.
Attributes, which are associated with grammar symbols (the terminal and non terminal
symbols), are similar to variables in the sense that they can have values assigned to them.
Attribute computation functions, sometimes called semantic functions, are associated with
grammar rules. They are used to specify how attribute values are computed.
Predicate functions, which state the static semantic rules of the language, are associated with
grammar rules.
Grammar Symbols with Attributes: In an attribute grammar, each symbol in the grammar
(like variables or operations) has associated properties called attributes. These attributes can be
either synthesized or inherited.
Intrinsic Attributes:
Syntax Rule: Describes the structure of an Ada procedure definition. It specifies that a
procedure begins with the keyword "procedure" followed by a procedure name, and ends with
the same procedure name.
Predicate: Checks whether the name specified at the beginning of the procedure matches the
one specified at the end. This ensures consistency between the declared procedure name and its
usage.
Example:
begin
-- Procedure body
end myProcedureName;
Syntax Rule: Defines the structure of a simple assignment statement. It consists of a variable
on the left side of the assignment operator (=) and an expression on the right side.
Expression Rule: Specifies possible expressions, which can be either a sum of two variables or
just a single variable.
Variable Rule: Lists available variables (A, B, C) that can be used in the assignment.
Attributes:
Example
| <var>
<var> → A | B | C
In the Ada procedure example, the attribute grammar ensures that the procedure name declared
at the beginning matches the one specified at the end of the procedure definition. This
consistency check is crucial for maintaining clarity and correctness in the code.
The attribute grammar for the assignment statement illustrates how attributes can be used to
enforce type rules. It ensures that the types of operands on both sides of the assignment match,
and that the resulting value of the expression matches the type of the variable being assigned
to.
(<var>[3].actual_type = int)
then int
else real
end if
The look-up function looks up a given variable name in the symbol table and returns the variable’s
type.
Explanation
This rule defines the syntax of a simple assignment statement, where a variable (<var>)
is assigned the value of an expression (<expr>) using the assignment operator =.
This semantic rule sets the expected type of the expression (<expr>) to be the actual
type of the variable (<var>). It implies that the type of the variable must match the type
of the expression being assigned to it.
This rule describes an expression consisting of the sum of two variables (<var>[2] and
<var>[3]).
This semantic rule determines the actual type of the expression. It checks if both
variables involved in the addition are of type int. If they are, the expression is assigned
the type int; otherwise, it's assigned the type real.
This predicate ensures that the actual type of the expression matches its expected type,
ensuring consistency in type usage.
This semantic rule sets the actual type of the expression to be the same as the actual
type of the variable.
Similar to the previous predicate, this ensures type consistency within the expression.
This semantic rule looks up the type of the variable (<var>) in the symbol table based
on its name (<var>.string). The look-up function retrieves the type associated with the
variable name from the symbol table.
A parse tree of the sentence A = A + B generated by the grammar in Example 3.6 is shown in Figure
3.6. As in the grammar, bracketed numbers are added after the repeated node labels in the tree so they
can be referenced unambiguously.
The process of computing the attribute values of a parse tree, which is called
decorating the parse tree.
If all attributes were inherited, this could proceed in a completely top-down order, from
the root to the leaves.
Alternatively, it could proceed in a completely bottom up order, from the leaves to the
root, if all the attributes were synthesized.
Because our grammar has both synthesized and inherited attributes, the evaluation
process cannot be in any single direction.
The following is an evaluation of the attributes, in an order in which it is possible to
compute them:
The tree in Figure 3.7 shows the flow of attribute values in the example of Figure 3.6. Solid lines are
used for the parse tree; dashed lines show attribute flow in the tree.
The tree in Figure 3.8 shows the final attribute values on the nodes. In thisexample, A is defined as a
real and B is defined as an int.
Evaluation: