I have finally found time to write an example of how to do the grammar
nullability computation using circular attribute grammars. The code below uses
our Kiama language processing library for Scala. You can find the full source
code of the example and tests in Kiama. See SemanticAnalysis.scala for the
main attribution code, e.g., nullable.
In short, the approach does the following:
represents a grammar as an abstract syntax tree structure,
performs name analysis on the tree structure to resolve references from uses
of grammar symbols to definitions of those symbols, and
computes nullability as a circular attribute on the resulting DAG structure.
The attribute definitions I use are quite similar to the ones used as examples
in the paper Circular Reference Attribute Grammars by Magnusson and Hedin from
LDTA 2003. They implement circular attributes in their JastAdd system and I
would highly recommend the paper for anyone wishing to understand this topic.
We use essentially the same algorithms in Kiama.
Here is the definition of the AST that the example uses. Tree
is a Kiama
type that provides some common behaviour.
sealed abstract class GrammarTree extends Tree
case class Grammar (startRule : Rule, rules : Seq[Rule]) extends GrammarTree
case class Rule (lhs : NonTermDef, rhs : ProdList) extends GrammarTree
sealed abstract class ProdList extends GrammarTree
case class EmptyProdList () extends ProdList
case class NonEmptyProdList (head : Prod, tail : ProdList) extends ProdList
case class Prod (symbols : SymbolList) extends GrammarTree
sealed abstract class SymbolList extends GrammarTree
case class EmptySymbolList () extends SymbolList
case class NonEmptySymbolList (head : Symbol, tail : SymbolList) extends SymbolList
sealed abstract class Symbol extends GrammarTree
case class TermSym (name : String) extends Symbol
case class NonTermSym (nt : NonTermUse) extends Symbol
sealed abstract class NonTerm extends GrammarTree {
def name : String
}
case class NonTermDef (name : String) extends NonTerm
case class NonTermUse (name : String) extends NonTerm
The code below shows the definition of the nullable
attribute. It
starts out false and then a fixed point "loop" is entered to compute until the
value stabilises. The cases show how to compute the attribute for different
types of nodes in the AST.
Kiama's circular
attribute constructor incorporates all of the implementation
of the attributes, including storage caching, fixed point detection etc.
val nullable : GrammarTree => Boolean =
circular (false) {
// nullable of the start rule
case Grammar (r, _) =>
r->nullable
// nullable of the right-hand side of the rule
case Rule (_, rhs) =>
rhs->nullable
// nullable of the component productions
case EmptyProdList () =>
false
case NonEmptyProdList (h, t) =>
h->nullable || t->nullable
// nullable of the component symbol lists
case Prod (ss) =>
ss->nullable
case EmptySymbolList () =>
true
case NonEmptySymbolList (h, t) =>
h->nullable && t->nullable
// terminals are not nullable
case TermSym (_) =>
false
// Non-terminal definitions are nullable if the rule in which they
// are defined is nullable. Uses are nullable if their associated
// declaration is nullable.
case NonTermSym (n) =>
n->nullable
case n : NonTermDef =>
(n.parent[Rule])->nullable
case n : NonTermUse =>
(n->decl).map (nullable).getOrElse (false)
}
The reference attribute decl
is the one that connects a non-terminal use to
its corresponding definition on the left-hand side of a rule. The parent
field is a reference from a node to its parent in the AST.
Since the nullability of one rule or symbol depends on the nullability of
others, what you get is a set of attribute occurrences that participate in a
dependence cycle. The result is a declarative version of the nullability calculation that
closely resembles a "textbook" definition. (The example also defines
attributes for FIRST and FOLLOW set computations that are defined in terms of
nullability.) Circular attributes combine memoisation and fixed point
computation in a convenient package for this kind of problem.