[go: up one dir, main page]

Academia.eduAcademia.edu
Alberto Pettorossi Techniques for Searching, Parsing, and Matching Fourth Edition ARACNE Contents Preface 7 Chapter 1. Preliminary Definitions on Languages and Grammars 1.1. Free Monoids and Languages 1.2. Formal Grammars 9 9 10 Chapter 2. Exploring Search Spaces 2.1. Exploring Linear Search Spaces 2.2. Backtracking Algorithms 2.3. Visiting Trees While Looking for Good Nodes 2.3.1. Depth First Visit of Trees: Basic Version 2.3.2. Depth First Visit of Trees: Burstall’s Version 2.3.3. Breadth First Visit of Trees 17 17 21 29 29 31 33 Chapter 3. Chop-and-Expand Parsers for Context-Free Languages 3.1. Chop-and-Expand Context-Free Parser in a Functional Language 3.2. Chop-and-Expand Context-Free Parser in Java 3.3. Chop-and-Expand Context-Free Parser in a Logic Language 35 35 39 50 Chapter 4. Parsers for Deterministic Context-Free Languages: LL(k ) Parsers 4.1. Introduction to LL(k ) Parsing 4.2. LL(1) Parsers 4.3. LL(k ) Parsers (for k ≥ 1) 51 51 53 63 Chapter 5. Parsers for Deterministic Context-Free Languages: LR(k ) Parsers 81 5.1. Introduction to LR(k ) Parsing 81 5.2. LR(0) Parsers 83 5.2.1. Avoiding the Powerset Construction for LR(0) Parsing 94 5.2.2. Remarks on the Hypotheses for LR(k ) Parsing 95 5.3. SLR(1) Parsers 97 5.4. LR(1) Parsers 102 5.4.1. Avoiding the Powerset Construction for LR(1) Parsing 118 5.4.2. More Remarks on the Hypotheses for LR(k ) Parsing 119 5.5. LALR(1) Parsers 123 5.6. Time and Space Complexity Results for Context-Free Parsing 134 5.7. Conventions for LL(k ) and LR(k ) Parsing 137 5.8. Subclasses of Context-free Languages 138 Chapter 6. Parsers for Operator Grammars and Parser Generators 6.1. Operator-Precedence Parsers 5 145 145 6 CONTENTS 6.2. Use of Parser Generators 6.2.1. Generation of Parsers Using Bison 6.2.2. Generation of Lexical Analyzers Using Flex 6.2.3. Suggestions for Constructing Parsers 6.3. Summary on Parsers of Context-Free Languages 6.3.1. Summary on LR(0) and LR(1) Parsing 150 150 165 167 168 170 Chapter 7. Visits of Trees and Graphs and Evaluation of Expressions 7.1. Depth First Visit of Trees 7.2. Evaluator of Boolean Expressions 7.3. A Theorem Prover for the Propositional Calculus 7.4. Encoding of n-ary Trees Using Binary Trees 7.5. Minimal Spanning Tree of an Undirected Graph 173 173 183 194 207 223 Chapter 8. Path Problems in Directed Graphs 8.1. Matrix Multiplication Algorithms 8.2. Comparing Matrix Multiplication Algorithms 8.3. Fast Boolean Matrix Multiplication 8.4. IC-Semirings and Path Problems in Directed Graphs 8.5. Transitive Closure in Directed Graphs: the Reachability Problem 8.6. Reducing Transitive Closure to Boolean Matrix Multiplication 8.7. Transitive Closure in IC-Semirings: the Shortest Path Problem 8.8. Single Source Shortest Paths in Directed Graphs 8.9. From Nondeterministic Finite Automata to Regular Expressions 235 235 237 238 239 241 242 245 247 258 Chapter 9. String Matching 9.1. Knuth-Morris-Pratt Pattern Matching 9.1.1. Time Complexity Analysis of the Knuth-Morris-Pratt Algorithm 9.1.2. Java Implementation of the Knuth-Morris-Pratt Algorithm 9.2. String Matching in Prolog 261 261 267 269 271 Chapter 10. Appendices 10.1. Simple Prolog Programs and Parsing Sentences in Prolog 10.2. Decidability Results for LL(k ) and LR(k ) Grammars and Languages 273 273 277 List of Algorithms and Programs 279 Index 281 Bibliography 287 Preface These lecture notes present some basic techniques for: (i) exploring search spaces, (ii) parsing context-free languages, and (iii) matching patterns in strings. These techniques are taught in a course on Automata, Languages, and Translators at the University of Roma “Tor Vergata”. We assume that the reader is familiar with the basic notions of Automata Theory and Formal Languages. These notions can be found in many books such as [9, 10, 21]. Some of the algorithms we have presented in these notes are written in Java 1.5 and some others in Prolog. For the Java language the reader may refer to the Java Tutorial at http://java.sun.com/docs/books/tutorial/. (Recall that this Java version allows the use of parametrized types, also called generics.) All Java programs have been compiled using the Java compiler 1.5.0 13 running under Mac OS X 10.4.11 Darwin 8.11.1. For the Prolog language the reader may refer to [5]. The Prolog language incorporates a backtracking mechanism which is useful for exploring search spaces and solving parsing and matching problems. Acknowledgements I would like to thank my colleagues at the Department of Informatics, Systems, and Production of the University of Roma “Tor Vergata”. I am also grateful to my students and to my co-workers Lorenzo Clemente, Corrado Di Pietro, Fulvio Forni, Fabio Lecca, Maurizio Proietti, and Valerio Senni for their support and encouragement. Many thanks to the Aracne Publishing Company for its helpful cooperation. Roma, September 2008 In the second edition (September 2009) we have corrected a few mistakes and we have improved Chapter 5 and Chapter 9. In the third edition (July 2011) and in the fourth edition (July 2013) we have made a few minor improvements in various chapters and added a few proofs. Roma, July 2013 Alberto Pettorossi DICII, Informatics, University of Roma “Tor Vergata” Via del Politecnico 1, I-00133 Roma, Italy pettorossi@info.uniroma2.it http://www.iasi.cnr.it/~ adp 7 CHAPTER 1 Preliminary Definitions on Languages and Grammars In this chapter we recall some preliminary definitions of formal languages. Let us consider a countable set V of elements, called symbols. The set V is also called the alphabet. 1.1. Free Monoids and Languages The free monoid over (or generated by) the set V is the set V ∗ consisting of all finite sequences of symbols in V , that is, V ∗ = {v1 . . . vn | n ≥ 0 and for each 0 ≤ i ≤ n, vi ∈ V }. The unary operation ∗ (pronounced ‘star’) is called the Kleene closure, or the ∗ closure (pronounced ‘the star closure’). Sequences of symbols are also called words or strings. The length of a sequence w = v1 . . . vn , where for i = 1, . . . , n, vi ∈ V , is n. The length of w is also denoted |w|. The sequence of length 0 is called the empty sequence (or empty word or empty string) and it is denoted by ε. For all w ∈ V ∗ , for all a ∈ V, the number of symbols a in w is denoted by |w|a. Given two sequences w1 and w2 in V ∗ their concatenation, denoted w1  w2 or w1 w2 , is the sequence in V ∗ defined by recursion on the length of w1 as follows: w2 if w1 = ε v1 ((v2 . . . vn )  w2 ) if w1 = v1 v2 . . . vn , for some symbols v1 v2 . . . vn , with n > 0. We have that the length of w1  w2 is the length of w1 plus the length of w2 . Concatenation is an associative binary operation on V ∗ whose neutral element is the empty sequence. Any set of sequences which is a subset of V ∗ is called a language (or a formal language) over the alphabet V . Given two languages A and B, their concatenation, denoted A  B or simply A B, is defined as follows: A  B = {w1  w2 | w1 ∈ A and w2 ∈ B}. Concatenation of languages is associative and its neutral element is the singleton {ε}. For reasons of simplicity, when A is a singleton, say {w}, the concatenation A  B will also be written as w  B or simply w B. Analogously for B, instead of A. We have that: V ∗ = V 0 ∪ V 1 ∪ V 2 ∪ . . . ∪ V k ∪ . . ., where for each i ≥ 0, V k is the set of all sequences of length k of symbols in V , that is, for each k ≥ 0, V k = {v1 . . . vk | for i = 1, . . . , k, vi ∈ V }. Obviously, V 0 = {ε}, V 1 = V , and for i, j ≥ 0, V i  V j = V j  V i = V i+j . By V + we denote V ∗ − {ε}. The unary operation + (pronounced ‘plus’) is also called the positive closure or the + closure (pronounced ‘the plus closure’). The set V 0 ∪ V 1 is also denoted by V 0,1 . 9 10 1. PRELIMINARY DEFINITIONS ON LANGUAGES AND GRAMMARS Given an element a in a set V , (i) a∗ denotes the set of all finite sequence of zero or more a’s (thus, a∗ is an abbreviation for {a}∗ ), (ii) a+ denotes the set of all finite sequence of one or more a’s (thus, a+ is an abbreviation for {a}+ ), (iii) a 0,1 denotes the set {ε, a} (thus, a 0,1 is an abbreviation for {a} 0,1 ), and (iv) aω denotes the infinite sequence made out of all a’s. Given a word w, for any k ≥ 0, the prefix of w of length k, denoted w k , is defined as follows: w if 0 ≤ |w| ≤ k wk = u if w = u v for some u, v ∈ V ∗ , and |u| = k. In particular, for any w, we have that: w 0 = ε and w |w| = w. From now on, unless otherwise specified, when referring to an alphabet, we will assume that it is a finite set of symbols. { 1.2. Formal Grammars Let us begin by recalling the following notions. For other notions of automata theory and formal language theory the reader may refer to [10, 21]. Definition 1.2.1. [Formal Grammar] A formal grammar (or a type 0 grammar, or a grammar, for short) is a 4-tuple hVT , VN , P, Si, where: (i) VT is a finite set of symbols, called terminal symbols, (ii) VN is a finite set of symbols, called nonterminal symbols or variables, such that VT ∩ VN = ∅, (iii) P is a finite set of pairs of strings, called productions (or type 0 productions), each pair hα, βi being denoted by α → β, where α ∈ (VT ∪ VN )+ and β ∈ (VT ∪ VN )∗ , and (iv) S is an element of VN , called axiom or start symbol. The set VT is called the terminal alphabet. The set VN is called the nonterminal alphabet. The set V = VT ∪ VN is called the alphabet of the grammar. In a production α → β, α is called the left hand side (lhs, for short) and β is called the right hand side (rhs, for short). Given a grammar G = hVT , VN , P, Si we may define a set of elements in VT∗ , called the language generated by G, as we now indicate. Let us first introduce the binary relation →G ⊆ V ∗ ×V ∗ which is defined as follows: for every sequence α ∈ V + and every sequence β, γ, and δ in V ∗ , γαδ →G γβδ iff there exists a production α → β in P . For any k ≥ 0, the k-fold composition of the relation →G is denoted →kG . Thus, for instance, for every sequence σ0 ∈ V + and every sequence σ2 ∈ V ∗ , we have that: σ0 →2G σ2 iff there exists σ1 ∈ V + such that σ0 →G σ1 and σ1 →G σ2 . The transitive closure of →G is denoted →+ G . The reflexive, transitive closure of →G ∗ is denoted →G . When the grammar G is understood from the context, we will feel 1.2. FORMAL GRAMMARS 11 ∗ free to omit the subscript G, and instead of writing →G , →kG , →+ G , and →G , we will simply write →, →k , →+ , and →∗ , respectively. Definition 1.2.2. [Language Generated by a Grammar] Given a grammar G = hVT , VN , P, Si, the language generated by G, denoted L(G), is the set L(G) = {w | w ∈ VT∗ and S →∗G w}. The elements of the language L(G) are said to be the words or the strings generated by the grammar G. A language generated by a type 0 grammar is said to be a type 0 language. For productions, grammars, and languages, the term ‘unrestricted’ is often used instead of the term ‘type 0’. Definition 1.2.3. [Language Generated by a Nonterminal Symbol of a Grammar] Given a grammar G = hVT , VN , P, Si, the language generated by the nonterminal A ∈ VN , denoted LG (A), is the set LG (A) = {w | w ∈ VT∗ and A →∗G w}. We will write L(A), instead of LG (A), when the grammar G is understood from the context. Definition 1.2.4. [Type 1, Context-Sensitive, Type 2, and Type 3 Production] (1) Given a grammar G = hVT , VN , P, Si we say that a production is of type 1 iff (1.1) either it is of the form α → β, where α ∈ (VT ∪ VN )+ , β ∈ (VT ∪ VN )+ , and |α| ≤ |β|, or it is S → ε, and (1.2) if the production S → ε is in P then the axiom S does not occur on the right hand side of any production. (cs) Given a grammar hVT , VN , P, Si, we say that a production in P is context-sensitive iff (cs.1) either it is of the form u A v → u w v, where u, v ∈ V ∗ , A ∈ VN , and w ∈ (VT ∪ VN )+ , or it is S → ε, and (cs.2) if the production S → ε is in P then the axiom S does not occur on the right hand side of any production. (2) Given a grammar G = hVT , VN , P, Si we say that a production in P is of type 2 (or context-free) iff it is of the form α → β, where α ∈ VN and β ∈ V ∗ . (3) Given a grammar G = hVT , VN , P, Si we say that a production in P is of type 3 (or regular, or right linear ) iff it is of the form α → β, where α ∈ VN and β ∈ VT VN ∪ VT ∪ {ε}. Definition 1.2.5. [Type 1, Context-Sensitive, Type 2, and Type 3 Grammar and Language] A grammar is of type 1, context-sensitive, of type 2 (or contextfree), and of type 3 (or regular, or right linear) iff all its productions are of type 1, context-sensitive, of type 2, and of type 3, respectively. A type 1, context-sensitive, type 2 (or context-free), and type 3 (or regular or right linear) language is the language generated by a type 1, context-sensitive, type 2, and type 3 grammar, respectively. One can show that for each type 1 grammar there exists an equivalent contextsensitive grammar, and vice versa. Thus, the notions of type 1 and context-sensitive 12 1. PRELIMINARY DEFINITIONS ON LANGUAGES AND GRAMMARS languages coincide. For this reason, instead of saying ‘a language of type 1’, we will also say ‘a context-sensitive language’ and vice versa. One can also show that for each type 3 (or regular, or right linear) grammar there exists an equivalent grammar whose productions are all of the form α → β, where α ∈ VN and β ∈ VN VT ∪ VT ∪ {ε}. These grammars are said to be left linear grammars. Given a grammar G = hVT , VN , P, Si, an element of V ∗ is called a sentential form of G. The following fact is an immediate consequence of the definitions. Fact 1.2.6. Given a grammar G = hVT , VN , P, Si and a word w ∈ VT∗ , we have that w belongs to L(G) iff there exists a sequence hα1 , . . . , αn i of n (> 1) sentential forms such that: (i) α1 = S, (ii) for every i = 1, . . . , n−1, there exist γ, δ ∈ V ∗ such that αi = γαδ, αi+1 = γβδ, and α → β is a production in P , and (iii) αn = w. Let us now introduce the following concepts. Definition 1.2.7. [Derivation of a Word and Derivation of a Sentential Form] Given a grammar G = hVT , VN , P, Si and a word w ∈ VT∗ in L(G), any sequence hα1 , α2 , . . . , αn−1 , αn i of n (> 1) sentential forms satisfying Conditions (i), (ii), and (iii) of Fact 1.2.6 above, is called a derivation of the word w from S in the grammar G. A derivation hS, α2 , . . . , αn−1 , wi is also written as: S → α2 → . . . → αn−1 → w. If there is a derivation from S to w, we have that S →+ w and S →∗ w. A derivation of a sentential form ϕ ∈ V ∗ from S in the grammar G is any sequence hα1 , α2 , . . . , αn−1 , αn i of n (≥ 1) sentential forms such that Conditions (i) and (ii) of Fact 1.2.6 hold and αn = ϕ. That derivation is also written as: S → α2 → . . . → αn−1 → ϕ. If there is a derivation from S to ϕ, we have that S →∗ ϕ. Definition 1.2.8. [Derivation Step] Given a derivation hα1 , . . . , αn i of n (≥ 1) sentential forms, for any i = 1, . . . , n−1, the pair hαi , αi+1 i is called a derivation step from αi to αi+1 (or a rewriting step from αi to αi+1 ). A derivation step hαi , αi+1 i is also denoted by αi → αi+1 . Given a sentential form γαδ for some γ, δ ∈ V ∗ and α ∈ V + , if we apply the production α → β, we perform the derivation step γαδ → γβδ. Given a context-free grammar G and a word w, a derivation α0 → α1 → . . . → αn in G, where α0 = S and αn = w, is said to be a leftmost derivation of w from S iff for i = 0, . . . , n−1, in the derivation step αi → αi+1 the sentential form αi+1 is obtained from the sentential form αi by replacing the leftmost nonterminal symbol occurring in αi . A derivation step αi → αi+1 in which we get the sentential form αi+1 by replacing the leftmost nonterminal symbol in αi , is also denoted by αi →lm αi+1 (the subscripts lm stands for leftmost). 1.2. FORMAL GRAMMARS 13 Analogous definitions are assumed by replacing the word ‘leftmost’ by the word ‘rightmost’. In particular, a rightmost derivation is a derivation where at each step we replace the rightmost nonterminal symbol of the sentential form at hand. A rightmost derivation step is usually denoted by →rm (the subscripts rm stands for rightmost). Example 1.2.9. Let us consider the grammar with axiom E and whose productions are: E → E+T E → T T → T ×F T → F F → (E) F → a Let us also consider the following three derivations D1, D2, and D3, where for each derivation step αi → αi+1 , we have underlined in the sentential form αi the nonterminal symbol which is replaced in that step: D1 : E →lm E + T →lm T + T →lm F + T →lm a + T →lm a + F →lm a + a D2 : E →rm E + T →rm E + F →rm E + a →rm T + a →rm F + a →rm a + a D3 : E →lm E + T →rm E + F →lm T + F →rm T + a →lm F + a →lm a + a We have that: derivation D1 is a leftmost derivation, derivation D2 is a rightmost derivation, and derivation D3 is neither a rightmost nor a leftmost derivation.  Theorem 1.2.10. Given a context-free grammar G, for every word w ∈ L(G) there exists a leftmost derivation of w and a rightmost derivation of w. Proof. By structural induction on the derivation tree of w.  For context-free grammars we can associate a derivation tree, also called a parse tree, with every derivation of a word w from the axiom S. Given a context-free grammar G = hVT , VN , P, Si and a derivation of a word w from the axiom S, that is, a sequence α1 → α2 → . . . → αn of n (> 1) sentential forms such that α1 = S and αn = w (see Definition 1.2.7 on page 12), the corresponding derivation tree T is constructed by applying the following two rules. Derivation Tree Rule (1). The root of T is a node labeled by S. Derivation Tree Rule (2). For i = 1, . . . , n−1, we consider in the given derivation α1 → α2 → . . . → αn the i-th derivation step αi → αi+1 . Let us assume that in the i-th derivation step we have applied the production A → β, where: (i) A ∈ VN , (ii) β = c1 . . . ck , for some k ≥ 0, and (iii) for j = 1, . . . , k, we have that cj ∈ VN ∪ VT . 14 1. PRELIMINARY DEFINITIONS ON LANGUAGES AND GRAMMARS Thus, αi = γAδ and αi+1 = γβδ for some γ and δ in (VT ∪ VN )∗ . In the derivation tree constructed so far, we consider the leaf node, say ℓi , labeled by the symbol A which is replaced by β in that derivation step. That node ℓi is identified by the fact that γ is the left-to-right concatenation of the labels of the leaves to the left of ℓi in the derivation tree constructed so far. If k ≥ 1 then we generate k son nodes of the node ℓi and they will be labeled, from left to right, by c1 , . . . , ck , respectively. (Obviously, after the generation of these k son nodes, the leaf node ℓi will no longer be a leaf node and will become an internal node of the new derivation tree.) If k = 0 then we generate one son node of the node ℓi . The label of that new node will be the empty word ε. We have that at the end of the application of Rule 2 the left-to-right concatenation of the labels of all the leaves of the resulting derivation tree T is the word w. The word w is said to be the yield of the derivation tree T . Example 1.2.11. Let us consider the grammar with axiom S and whose productions are: S → aAS S→a A → S bA A → ba A→SS Let us also consider the following derivation D of the word a a b b a a: D: S → aA S → aS bAS → aabA S → aabbaS → aabbaa (1) (2) (3) (4) (5) where in each sentential form αi we have underlined the nonterminal symbol which is replaced in the derivation step αi → αi+1 . The corresponding derivation tree is depicted in Figure 1.2.1. In the derivation D the numbers below the underlined nonterminal symbols denote the correspondence between the derivation steps and the nodes with the same number in the derivation tree of Figure 1.2.1.  S (1) a A (2) S (5) a S (3) A (4) b Figure 1.2.1. A derivation tree for the word a a b b a a and the grammar given in Example 1.2.11. This tree corresponds to the derivation D: a → aabbaa. The numbers, S → aA S → aS bAS →a aabA S →baabbaS from 1 to 5, associated with the nonterminal symbols in the nodes of the tree, denote the correspondence between the nonterminal symbols and the five derivation steps of D. Now let us introduce the following definitions. 1.2. FORMAL GRAMMARS 15 Definition 1.2.12. [Unfold of a Context-Free Production] Let us consider a context-free grammar G = hVT , VN , P, Si. Let A and B be elements of VN and α, β1 , . . . , βn , γ be elements of (VT ∪ VN )∗ . Let A → αBγ be a production in P , and B → β1 | . . . | βn , for n ≥ 0, be all the productions in P whose left hand side is B. The unfolding of B in A → αBγ with respect to P (or the unfolding of B in A → αBγ by using P , or simply, the unfolding of B in A → αBγ) is the replacement of the production: A → αBγ by the n productions: A → αβ1 γ ··· A → αβn γ. Definition 1.2.13. [Fold of a Context-Free Production] Let us consider a context-free grammar G = hVT , VN , P, Si. Let A and B be elements of VN and α, β1 , . . . , βn , γ be elements of (VT ∪ VN )∗ . Let A → αβ1 γ | . . . | αβn γ, for n ≥ 1, be some productions in P whose left hand side is A, and B → β1 | . . . | βn be all the productions in P whose left hand side is B. The folding of β1 , . . . , βn in A → αβ1 γ | . . . | αβn γ with respect to P (or simply, the folding of β1 , . . . , βn in A → αβ1 γ | . . . | αβn γ) is the replacement of the n productions: A → αβ1 γ ··· A → αβn γ by the production: A → αBγ. Definition 1.2.14. [Useful Symbol and Useless Symbol] Given a contextfree grammar G = hVT , VN , P, Si a symbol X ∈ VT ∪VN is useful iff S →∗G αXβ →∗G w for some α, β ∈ (VT ∪ VN )∗ and w ∈ VT∗ . A symbol is useless iff it is not useful. Definition 1.2.15. [Reduced Grammar] [24, page 130] A context-free grammar is said to be reduced if it has no useless symbols. In what follows, unless otherwise specified, we will assume that in every production the right hand side is different from the left hand side. In particular, for context-free grammars we will assume that no production is of the form A → A, for some A ∈ VN . Definition 1.2.16. [Epsilon Production] A production α → β is said to be an epsilon production, also written ε-production, iff β is the empty word ε. Definition 1.2.17. [Nullable Symbol] Given a context-free grammar G, we say that a nonterminal symbol A is nullable iff A →∗G ε. Definition 1.2.18. [Production for a Nonterminal Symbol] A production of the form A → β, with A ∈ VN and β ∈ (VT ∪ VN )∗ , is said to be a production for the nonterminal symbol A. 16 1. PRELIMINARY DEFINITIONS ON LANGUAGES AND GRAMMARS Definition 1.2.19. [Left Recursive Context-Free Production and Left Recursive Context-Free Grammar] Let us consider a context-free grammar G = hVT , VN , P, Si. We say that a production in P is left recursive if it is the form: A → A α with A ∈ VN and α ∈ (VT ∪ VN )∗ . A context-free grammar is said to be left recursive if there exists a nonterminal symbol A such that for some α, β, γ ∈ (VT ∪VN )∗ we have that: (i) S →∗ α A β, and (ii) A →+ A γ. If we consider context-free grammars without trivial unit productions (that is, productions of the form A → A) and, in general, context-free grammars where there are no derivations of the form: A →+ A, then in the above Definition 1.2.19 in A α we have that α 6= ε, and in A γ we have that γ 6= ε. If a reduced, context-free grammar G has a left recursive production, then G is left recursive. We conclude this chapter by illustrating the following Figure 1.2.2 where we stratify the set N N of the computable and non-computable functions from N to N (as usual, N denotes the set of the natural numbers). The cardinality of N N is ℵ1 . (Recall that ℵ1 denotes the cardinality of the real numbers and ℵ0 denotes the cardinality of the natural numbers.) The subset of the computable functions from N to N, whose cardinality is ℵ0 , is stratified according to the types of languages associated with those functions. The classes of languages: (i) type 0, (ii) type 1, (iii) type 2, (iv) deterministic context-free, and (v) type 3 are associated, respectively, with the following classes of automata: (i) Turing Machines (TM), (ii) linear bounded automata, (iii) pushdown automata (pda), (iv) deterministic pushdown automata (dpda), and (v) finite automata. The set ‘rec’ is the set of all recursive languages associated with the Turing Machines which halt for all input values. non-computable functions ℵ0 type 0 = TM = λ-calculus = r.e. rec type 1 = linear bounded automata type 2 = context-free = pda deterministic context-free = dpda type 3 = finite automata Figure 1.2.2. A stratification of the set N N of all (computable and non-computable) functions from N to N. CHAPTER 2 Exploring Search Spaces In this chapter we introduce the reader to some algorithms and techniques for exploring search spaces. In particular, we will consider: (i) the technique for exploring linear search spaces represented as arrays (or linear lists), (ii) the backtracking technique for exploring general search spaces represented as graphs, and (iii) the depth first and breadth first algorithms for exploring tree-like search spaces with the objective of finding leaves satisfying a given property. The presentation of these algorithms can be viewed as an introduction to the parsing algorithms which we will consider in the following chapters. Indeed, we will see that every parsing algorithm performs the exploration of a search space. (Actually, every algorithm performs the exploration of a suitably defined search space.) 2.1. Exploring Linear Search Spaces In this section we will indicate how to explore a linear search space represented as an array. As a typical example of this technique, now we present the following elementary program Sum for summing up the elements of a given array A of length n ≥ 1. We assume that the elements of the array are all integers. The array A can also be viewed as a finite function from the set {1, . . . , n} to the set of integers. Here is the Program Sum. Program Sum {n ≥ 1} i := 0; sum := 0; I ≡ {sum = : precondition Pi j=1 A[j] establishing the invariant I. ∧ i ≤ n} : invariant I working towards termination and re-establishing the invariant I. while i < n do i := i + 1; sum := sum + A[i] {sum = od Pn j=1 A[j]} (I ∧ ¬(i < n)) → postcondition : postcondition Let us make the following comments of the structure of this program. (i) The program is made out of two initialization statements followed by a whileloop. As indicated by Kleene’s Normal Form Theorem, the structure of this program is universal, in the sense that every Turing computable function can be computed via a unique while-loop (apart from a preliminary evaluation of a primitive recursive 17 18 2. EXPLORING SEARCH SPACES function and then, at the end of the while-loop, a subsequent evaluation of a primitive recursive function). (ii) The computation of the while-loop is characterized by the invariant I which holds before the execution of the while-loop, after every execution of the body of the while-loop, and at the end of the execution of the while-loop. The reader may easily check that, indeed, the initialization statements establish the invariant P0 I and, in our case, they do so in a trivial way because if n ≥ 1, we have that (0 = j=1 A[j]) ∧ 0 ≤ n. In the body of the while-loop, once the value of i has been increased by 1, in order to re-establish the invariant we need to modify the value of the variable sum and to increase it by A[i]. Finally, when we exit from the while-loop and thus, it isPno longer the case that i < n, the invariant should imply the postcondition sum = nj=1 A[j], and indeed, this is the case. (iii) Termination of the while-loop is guaranteed by the fact that at every execution of the body of the while-loop, i is increased by 1 and thus, sooner or later, it will be the case that i < n does not hold and the execution of the while-loop terminates. Thus, as indicated by the usual approach to termination, there exists a function, namely λi. n−i, which decreases at every execution of the body of the while-loop and it bounded from below by the value 0 because i cannot become larger than n. Now, let us suppose that, instead of summing up all the elements of an array, we want to visit a given array A for finding out whether or not one of its elements satisfies a given property (or predicate) prop : Σ → {true, false}, where Σ is the set from which the elements of the array A are taken. Thus, when viewing the array A of n (≥ 1) elements as a finite function, we have that A : {1, . . . , n} → Σ. In order to perform the visit of the array A we can use the following Linear Search program which is a simple variant of the Sum program on page 17. {n ≥ 1} i := 0; res := false; W I = {res = ij=1 prop(A[j]) ∧ i ≤ n} while i < n do i := i + 1; res := res ∨ prop(A[i]) od W {res = nj=1 prop(A[j])} Program for Linear Search Starting from this program Linear Search, now we derive a more efficient Linear Search program by applying some simple transformations. In particular, we may use the following properties: (i) true ∨ x = true, (ii) false ∨ x = x, and (iii) we may halt when we find an element A[i] of A such that prop(A[i]) is true. We derive the following program: 2.1. EXPLORING LINEAR SEARCH SPACES 19 i := 0; res := false; W {false = i−1 j=1 prop(A[j]) ∧ (if i ≥ 1 then res = prop(A[i])) ∧ i ≤ n} while i < n ∧ ¬res do i := i + 1; res := prop(A[i]) od By replacing i by a new identifier, also named i, which gets the value of the old i increased by 1, we derive the following program: i := 1; res := false; while i ≤ n ∧ ¬res do i := i + 1; res := prop(A[i−1]) od Now we can avoid the use of the index i−1 by anticipating, so to speak, the access to the array A. We swap the two instructions in the body of the while-loop and we get: i := 1; res := false; while i ≤ n ∧ ¬res do res := prop(A[i]); i := i+1 od In order to avoid accessing the element A[n+1] which is not defined, we may use the cand operator and we get the following Program P 1. Recall that ‘a cand b’ is equivalent to ‘if a then b else false’, and thus, b is not evaluated if a is false. {n ≥ 1} i := 1; res := false; while i ≤ n cand ¬prop(A[i]) do i := i + 1 od; if i ≤ n then res := true W {res = nj=1 prop(A[j])} Program P 1 We can transform Program P 1 by using conditional rewrite rules and recursion, and we get the following Program P 2: {n ≥ 1} Program P 2 i := 1; search(i ): (1) i ≤ n ∧ ¬prop(A[i]) → search(i+1) (2) i ≤ n ∧ prop(A[i]) → res := true (3) i > n → res := false Wn {res = j=1 prop(A[j])} We will not formally define here the operational semantics of the conditional rewrite rules which we have used in the above Program P 2. It will suffice to say that, in order to perform search(i), if it is the case that i ≤ n ∧ ¬prop(A[i]) holds then Rule (1) tells us to perform search(i+1), that is, to apply again one of the three rules either (1), or (2), or (3), having increased the value of i by one unit. The equivalence between Program P 1 and Program P 2 is shown in the following Exercise 2.1.1. 20 2. EXPLORING SEARCH SPACES Exercise 2.1.1. (i) Show that P 1 terminates iff P 2 terminates. Hint. Both programs terminate iff i > n ∨ (∃i 1 ≤ i ≤ n ∧ A[i] = true). (ii) Show that if P 1 terminates then (P 1 sets res to the boolean value b iff P 2 sets res to the same boolean value b). Now we want to present a different kind of search of a linear search space. We call it State Dependent Linear Search. In this kind of search we want to visit an array, say A, whose elements are taken from a set Σ, for finding out whether or not one of its elements satisfies a given property (or predicate) prop, but this time the truth value of prop depends on both: (i) a state q belonging to a set Q of states, and (ii) the element of the array which is currently visited. The visit of the array A begins in the initial state q0 ∈ Q and any time a new element of the array is visited, we get to a new state which is computed by the so called next-state function δ : Q×Σ → Q. The function δ depends on the previous state and the element of the array which is currently visited. Thus, we have: (i) an array A whose elements are taken from a set Σ, (ii) an initial state q0 ∈ Q, (iii) a next-state function δ : Q×Σ → Q, and (iv) a property prop : Q×Σ → {true, false}. We want a procedure which gives us the value true iff there exist a state q and an element ai in the array A such that prop(q, ai ) = true. Here is the program for the State Dependent Linear Search. {n ≥ 1} Program for State Dependent Linear Search i := 1; q := q0 ; st-search(q, i): (1) i ≤ n ∧ ¬prop(q, A[i]) → st-search(δ(q, A[i]), i+1) (2) i ≤ n ∧ prop(q, A[i]) → res := true (3) i > n → res := false Wn {res = j=1 prop(δ(. . . δ(δ(q0 , A[1]), A[2]) . . . , A[j −1]), A[j])} By using the result of the above Exercise 2.1.1 which shows the equivalence between Program P 1 and Program P 2, we get the following program which is equivalent to the program for the State Dependent Linear Search: i := 1; q := q0 ; res := false while i ≤ n cand ¬prop(q, A[i]) do q := δ(q, A[i]); i := i+1 od; if i ≤ n then res := true Exercise 2.1.2. Write a program to check whether or not in a given array there is an element which is equal to the sum of all the preceding elements. Solution. Instantiating the above State Dependent Linear Search program, we get: i := 1; sum := 0; res := false while i ≤ n cand ¬(sum = A[i]) do sum := sum + A[i]; i := i+1 od; if i ≤ n then res := true  2.2. BACKTRACKING ALGORITHMS 21 2.2. Backtracking Algorithms Backtracking is a method for visiting all the nodes of a given finite tree starting from its root, when at each node we know: (i) the list of all the son nodes which are assumed to be ordered in a linear order, which is the left-to-right order of the elements of the list, (ii) a pointer p which identifies the node of that list which should be visited next, and (iii) the father node, if any. When reasoning about our programs and also in our pictures below, we assume that the head of every list of nodes is ‘to the left’ of all the other elements of the list. When we visit a list of nodes, we first visit its head and then, recursively, we visit the tail of the list. In what follows we will also use the following terminology. Given a list of son nodes, (i) the head of the list is said to be oldest son node, and (ii) the node, if any, which is immediately to the right of the given node m in the list, is said to be the immediately younger brother node of m, or the next node after m. The general idea of the backtracking algorithms can be explained as follows [6]. When the visit of a given node n of a tree is completed (and thus also the visits of all the descendant nodes of that node n are completed), the visit of the tree continues by performing the visit of the subtree, if any, which is rooted at the node which is the immediately younger brother node of n. If that immediately younger brother node of n does not exist, then the visit of the father node of the node n is completed. Thus, by applying the backtracking technique, the visit of the tree search space is the preorder visit, that is, at each node of the tree we first visit that node and then, recursively, we make the preorder visit of each node in the list of the son nodes of that node. The son nodes of that list are visited according to the left-to-right order of the elements of the list. The following program, called Dispositions.java, uses the backtracking technique, implemented as a recursion inside a do-while loop, for computing all the dispositions of length D whose elements are taken from the set {1, . . . , B} of B elements. In particular, if B = 2, that program computes in increasing order the following sequence of tuples each of which is made out of D elements (thus, for instance, the first tuple has D occurrences of 1’s, the second tuple has D − 1 occurrences of 1’s and 1 occurrence of 2, and the last tuple has D occurrences of 2’s): h1, 1, . . . , 1, 1i, h1, 1, . . . , 1, 2i, h1, 1, . . . , 2, 1i, h1, 1, . . . , 2, 2i, . . . , . . ., h1, 2, . . . , 2, 2i, h2, 1, . . . , 1, 1i, . . . , h2, 2, . . . , 1, 1i . . . , h2, 2, . . . , 2, 2i The program Dispositions.java has the structure that we have indicate in Figure 2.2.1 on the following page (see also Figure 2.2.2 on page 23). In that program we have that: (i) the first local choice identifies the oldest son node of the given initial node, (ii) « to make the next local choice » is to increment the value of the index of breadth ib, that is, to move the pointer p one position to the right along the list of son nodes, (iii) « to go down the tree » is to increment by 1 the value of the index of depth id, that is, to begin the visit of a son node, 22 2. EXPLORING SEARCH SPACES /* procedure generate() */ void generate(); begin select the first local choice; do (α): make the local choice; (β): down the tree (global value); if deepest level reached then do the leaf operation else generate() (β ′ ): up the tree (global value) (α′): remove local choice; select the next local choice; while there is one more local choice to make; end /* main program */ id = 0; generate(); Figure 2.2.1. Structure of a backtracking algorithm. The ‘local choice’ refers to the index of breadth ib and the ‘global value’ refers to the index of depth id. (iv) « to go up the tree » is to decrement by 1 the value of id, that is, to go to the father node for beginning the visit of the immediately younger brother node, if any, and (v) « to remove the local choice » is the step we have to make before making the next local choice (see the above Point (ii)). By removing the local choice we complete the visit of the node, say n, which is our current local choice, and then, the next local choice, if any, can be made and the visit of the immediately younger brother node of n, if any, begins by executing one more time the body of the do-while loop. For the index of breadth ib we have that: 1 ≤ ib ≤ B, and for the index of depth id we have that: 0 ≤ id ≤ D. The deepest level is reached when id = D. Notice that « to make the local choice » does not increase the value of ib. When the deepest level is reached, instead of going below a leaf node with id = D, we perform an operation associated with that leaf. That leaf operation, in general, depends on some global data structure which is manipulated while the visit of the tree progresses. In the case of the program Dispositions.java that global structure is the array arrayDisp. The removal of the local choice, denoted by (α′), may not be necessary if the action of making the next local choice is performed destructively. This means that if the action of making a choice is done by assigning a value to a variable (as it is in our case, because we assign a value to the variable ib) then the action of making a new local choice automatically removes the old value of that variable (in our case the variable ib) and thus, automatically removes the local choice which was previously made. This is why in Figure 2.2.2 on the facing page there is no reference to the action (α′ ). 2.2. BACKTRACKING ALGORITHMS depth: (global value) ✲ • id = 0 (α) vertical move: by recursion on generate() ② local choices: ☞❅ ✲ ☞2 ❅ B ib = 1, 2, . . . , B ✒☞ ❅ horizontal move: ❅ ☞ ❅ by do-while ☞ ☞ ❅ ... 1 down the tree (β) ✲• • id = 1 .. • id = D−1 ❄ • ✲• • 23 ... ✠ ② up the tree ✡☞❚ (β ′ ) ✡☞ ❚ ❚ ✡☞ ❚ ☞ ✡ ◗ ❚ ✡ ◗ ◗ ❚ ✡ ✱ ❚ ✡ ✱ ❚ ✡ ✱ ❚ ✱ ✡ ❚ ❏ ✡ ❚ ❏ ✡ ✡ ❏♠ ✇ ❚ do the leaf operation Figure 2.2.2. Visit of a tree of nodes by backtracking, starting from the topmost node. The dots (•) in the pictures of the array on the left represent the local choices which have been already made. In the program Dispositions.java listed on page 24 we have that D = 3 and B = 2, and the dispositions of length 3 whose elements are taken from the set {1, . . . , B}, that is, {1, 2}, are the 8 (= BD ) sequences: 111, 112, 121, 122, 211, 212, 221, 222. When computing these sequences we get the tree of nodes depicted in Figure 2.2.3. The nodes of that tree are visited using the program Dispositions.java, starting from the root node a, in the following depth first order (which realizes the so called preorder traversal of the tree): a, b, d, h, i, e, j, k, c, f, ℓ, m, g, n, and o. a: − − − 1 2 b: 1 − − 1 d : 11− 1 h : 111 2 i : 112 c: 2 − − 1 2 e : 12− 1 j : 121 2 k : 122 f : 21− 1 ℓ : 211 2 m : 212 2 g : 22− 1 n : 221 2 o : 222 Figure 2.2.3. The tree of nodes when computing the 8 dispositions of length 3 of the elements 1 and 2, from 111 to 222. The dispositions are at the leaves. 24 2. EXPLORING SEARCH SPACES /** * ======================================================================== * Backtracking: DISPOSITIONS * Filename: "Dispositions.java" * * A disposition is a sequence of length D whose elements (not necessarily * distinct) are taken from a set of B elements. * ======================================================================== */ public class Dispositions { private int id = 0; private final int D = 3; private final int B = 2; private int[] arrayDisp = new int[D];// by default: initially the elements // of arrayDisp are all 0’s private void printar(int[] array, int size) { for (int k=0; k<size; k++) {System.out.print(array[k]+" ");} System.out.println(); } private void generate() { int ib=1; do {arrayDisp[id]=ib; id++; if (id==D) {printar(arrayDisp,D);} else generate(); id--; ib++;} while (ib <= B); } public static void main(String[] args) { Dispositions disp = new Dispositions(); disp.generate(); } } /** * input: output: * -----------------------------------------------------------------------* javac Dispositions.java * java Dispositions * 1 1 1 * 1 1 2 * 1 2 1 * 1 2 2 * 2 1 1 * 2 1 2 * 2 2 1 * 2 2 2 * -----------------------------------------------------------------------*/ Now we present a program, called Combinations.java, for computing all the combinations of B (≥ 1) elements taken in groups of cardinality D, with 1 ≤ D ≤ B. They are also called the B-over -D combinations. The number of B-over-D combinations   B! is the value of the binomial coefficient BD , that is, . (B−D)! D! The program Combinations.java is a variant of the program Dispositions.java (see page 24) in the sense that in this case the local choices that can be made, depend on some global information. Indeed, the computation of the B-over-D combinations can be viewed as the computation of the dispositions of length D whose elements are taken from the set {1, . . . , B} with the following constraint: 2.2. BACKTRACKING ALGORITHMS 25 « if an element has been already used for making a local choice at an ancestor node, then it cannot be used again for making a local choice at the current node. » In order to recall all the choices made at the ancestor nodes, one can show that, since a combination of D elements is stored in the array arrayComb of D elements, it is enough to test whether or not ((id == 0) || (ib > arrayComb[id-1])) holds. (We leave it to the reader the proof of this simple fact.) The identifier ib, with 1 ≤ ib ≤ B, is the index of breadth and id, with 0 ≤ id ≤ D-1, is the index of depth (see also Figure 2.2.2 on page 23). In our case, in order to recall all the choices made at the ancestor nodes, we could have also used a boolean vector. /** * ======================================================================== * Backtracking: COMBINATIONS * Filename: "Combinations.java" * * Combinations are dispositions, where the order of the elements * is NOT significant. In the dispositions the order is significant. * * The combinations of ‘B over D’ are the different SETS (not sequences) of * the B elements taken in groups of cardinality D. * The B elements are assumed to be: 1,...,B. * The combinations will be presented in lexicographic order, that is, * X_1,...,X_D comes before Y_1,...,Y_D iff there exists k, with 0<=k<D, * such that X_1 = Y_1, ..., X_k = Y_k, X_{k+1} < Y_{k+1}. * * In order to compute the combinations of ‘B over D’ we use a backtraking * algorithm which takes into account some global information to know * whether or not a given element can be used for making the current * local choice. That global information depends on previous local choices * made at the nodes which are ancestors of the node where the current * local choice is made. * ======================================================================== */ public class Combinations { private int id = 0; private final int B = 4; // B should be positive private final int D = 2; // D should be positive and D <= B private int[] arrayComb = new int[D];// by default: initially the elements // of arrayComb are all 0’s private void printar(int[] array, int size) { for (int k=0; k<size; k++) {System.out.print(array[k]+" ");} System.out.println(); } private void generate() { int ib=1; do {// ------ For id == 0 the first element is always ok. // ------ Combinations are generated in ‘increasing order’: // this is due to the test (ib > arrayComb[id-1]) below. if ((id == 0) || (ib > arrayComb[id-1])) {arrayComb[id] = ib; id++; if (id == D) {printar(arrayComb,D);} else generate(); id--;}; ib++;} while (ib <= B); } 26 2. EXPLORING SEARCH SPACES public static void main(String[] args) { Combinations comb = new Combinations(); comb.generate(); } } /** * input: output: * -----------------------------------------------------------------------* javac Combinations.java * java Combinations * 1 2 * 1 3 * 1 4 * 2 3 * 2 4 * 3 4 * -----------------------------------------------------------------------*/ Finally, we present a program, called Queens.java, for solving the n-queens problem [6]. This program implements a backtracking algorithm which can easily be understood because it is a variant of the algorithm we have presented in the program Combinations.java (see page 25). This variant is based on the fact that when making the local choices, we have to comply with the following constraint: « when making any local choice, we have to determine whether or not the candidate local choice is available, that is, we have to determine whether or not the following global condition holds: the new queen to be placed on the board, is not under attack of any other queen already placed on the board. » Attacks to a queen can come from a queen which is either in the same column, or in the same up-diagonal, or in the same down-diagonal. No attack to a queen can come from a queen on the same row simply because, by construction, every queen is placed on a different row (see Figure 2.2.4 on page 28). The columns taken by the queens on the board are identified by the value false in the array col[0, . . . , m−1]. Analogously for the up-diagonals and the down-diagonals for which we use the boolean arrays up[0, . . . , 2(m−1)] and down[0, . . . , 2(m−1)], respectively. A new queen can be placed in column k, up-diagonal u, and down-diagonal d iff the corresponding elements of the arrays col, up, and down, that is, col[k], up[u], and down[d], are all true. Once a queen is placed in that position, all those elements are set to false, meaning that that position is no longer available for a subsequent queen to be placed on the board. All the elements of the arrays col, up, and down are initialized to true meaning that all positions are available for the first queen. In our program Queens.java which we now present, the number of queens is stored in the variables nqueens and m. /** * ======================================================================== * Backtracking: n QUEENS * Filename: "Queens.java" * * From: E.W. Dijkstra: ’A Short Introduction to the Art of Programming’. * EWD 316 August 1971. The command line for executing this program is: * java Queens n (n should be at least 1) * The queens are placed from top to bottom, one for each row. * ======================================================================== */ 2.2. BACKTRACKING ALGORITHMS 27 public class Queens { int nq = 0; // index of queen to be placed in the current solution. // See generate(). int nqueens, nqueens1; // number of queens, number of queens minus one int[] arrayQ; boolean[] col, up, down; // constructor of the class Queens Queens(int[] arrayQ, boolean[] col, boolean[] up, boolean[] down, int nqueens) { this.nqueens = nqueens; this.nqueens1 = nqueens-1; this.arrayQ = arrayQ; this.col = col; this.up = up; this.down = down; } /** * -----------------------------------------------------------------------* Dijkstra uses different ’up’ and ’down’ boolean arrays corresponding to * different notions of the up-diagonal and down-diagonal. * -----------------------------------------------------------------------*/ private void printar(int[] arrayA, int size) { for (int k=0; k<size; k++) {System.out.print(arrayA[k]+" ");} System.out.println();} private void generate() {int h=0; do {if (col[h] && up[nq+h] && down[nq-h+nqueens1]) {arrayQ[nq]=h; col[h]=false; up[nq+h]=false; down[nq-h+nqueens1]=false; nq=nq+1; if (nq==nqueens) {printar(arrayQ,nqueens);} else generate(); nq=nq-1;col[h]=true; up[nq+h]=true; down[nq-h+nqueens1]=true;}; h=h+1;} while (h < nqueens);} //------------------------------------------------------------------------public static void main(String[] args) { int m=0; // m is the number of queens. See the field nqueens. try {m = Integer.parseInt(args[0]); if (m <= 0) {throw new Exception();} } catch (Exception e) { System.out.print("The number of queens should be >= 1."); System.out.println(" By default we assume 1 queen only."); m=1; }; System.out.println("Number of queens = "+m); int[] arrayQ = new int[m]; // integer array [0..m-1] boolean[] col = new boolean[m]; // boolean array [0..m-1] boolean[] up = new boolean[2*m-1]; // boolean array [0..2(m-1)] boolean[] down = new boolean[2*m-1]; // boolean array [0..2(m-1)] //------------ initialization of the arrays to ’true’ values -----------int k=0; do {col[k]=true; k=k+1;} while (k<m); k=0; do {up[k]=true; down[k]=true; k=k+1;} while (k<2*m-1); //------------------------------------------------------------------------Queens queens = new Queens(arrayQ, col, up, down, m); queens.generate(); } } 28 2. EXPLORING SEARCH SPACES /** * input: output: * -----------------------------------------------------------------------* javac Queens.java * Java Queens 6 * Number of queens = 6 * 1 3 5 0 2 4 * 2 5 1 4 0 3 * 3 0 4 1 5 2 * 4 2 0 5 3 1 * -----------------------------------------------------------------------*/ up : 0 col : 0 h F nq+h F arrayQ : 0 2∗(nqueens−1) nq : h Q 0 nqueens−1 F down : nq−h+nqueens−1 2∗(nqueens−1) Figure 2.2.4. The queen Q in position [nq,h], that is, in row nq and column h, forces the following values, where F stands for false: col[h] = F, up[nq+h] = F, and down[nq-h+nqueens-1] = F. These false values tell us the column, the up-diagonal, and the downdiagonal which are taken by that queen. These positions are not available for a different queen. The array col is from col[0] to col[nqueens-1]. 2.3. VISITING TREES WHILE LOOKING FOR GOOD NODES 29 2.3. Visiting Trees While Looking for Good Nodes In this section we will present some algorithms for visiting trees with the objective of finding ‘good’ nodes, that is, nodes which satisfy a given property (or predicate) p. We assume that the trees are generated in a dynamic fashion, in the sense that any given tree is specified by: (i) its root node, and (ii) a function f which for each node of the tree, returns the list of its son nodes. Any such function f will be called a tree-generating function. We assume that the nodes of the trees we will consider, are of type α. We also assume that: (i) the predicate p is a function whose type is: α → bool , where bool = {true, false}, and (ii) the tree-generating function f is a function whose type is: α → αlist, where α list denotes the type of the lists whose elements are of type α. In what follows the infix append function between two lists is denoted by <>. Thus, <> has type: (α list) × (α list) → (α list). 2.3.1. Depth First Visit of Trees: Basic Version. Now we present a function, called existsev (short for exists eventually), which given: (i) a predicate p, (ii) a tree-generating function f , and (iii) a list L of nodes, returns true if there exists a node n in L which is the root of the tree, say tn , generated by the function f such that in tn there exists a node m such that p(m) = true, otherwise it returns false. The correctness of the algorithm we will give for evaluating the function existsev is based on the assumption that for any node n the tree which is rooted in n is finite. This finiteness assumption is required because given any node n, the tree rooted in n is generated and visited in a depth first manner (see the line marked with (†)). If a node is the root of an infinite tree (because of the particular tree-generating function f which is given), then the evaluation of existsev may not terminate even if in that infinite tree there exists a node which satisfies p. The existsev function is defined as follows. (In the first line we have given its type and in the other lines we have given its definition.) existsev : (α → bool ) × (α → α list) × (α list) → bool existsev p f L = if L = [ ] then false else if p(hd (L)) then true else existsev p f (f (hd (L)) <> tl (L)) (†) Let us consider a predicate p, a tree-generating function f , and a node n. Suppose that f generates a finite tree rooted in n. Then we have that there exists a node m in the tree rooted in n generated by f such that p(m) = true iff existsev (p, f, [n]) = true. The inductive proof of this statement is left to the reader. The following example illustrates the behaviour of the function existsev while performing the depth first visit of a tree of natural numbers (thus, in this example the type α is the type of the natural numbers.) 30 2. EXPLORING SEARCH SPACES Example 2.3.1. Let N denote the set of natural numbers. Let us consider the predicates p : N → bool and odd : N → bool, and the tree-generating function f defined as follows: p(x) = x = 25 odd (0) = false odd (n+1) = not(odd (n)) f (x) = [x+4, x+5] if x < 25 and odd (x) f (x) = [x+5] if x < 25 and not(odd (x)) f (x) = [] otherwise where not(false) = true and not(true) = false. The search space is the tree of natural numbers depicted in Figure 2.3.1. In that figure we have also depicted the first six frontiers which are determined by the depth first visit of the tree, as we will explain below. The other frontiers are depicted on Figure 2.3.2 on the facing page. : (i) : (ii) : (iii) : (iv) : (v) : (vi) 7 11 15 19 23 27 28 16 20 24 12 21 17 21 25 25 26 25 26 22 27 29 Figure 2.3.1. The tree generated from the initial node 7 by applying the tree-generating function f of Example 2.3.1. We have also depicted the first six frontiers of the depth first visit of the tree. Every recursive call of the function existsev is of the form: existsev p f l, where the predicate p and the function f do not change, while the list l of natural numbers which is initially [7] (because 7 is the root of the tree), changes as follows for each new recursive call (see also the frontiers depicted on Figure 2.3.1 and Figure 2.3.2 on the next page): (i ) (ii ) (iii ) (iv ) (v ) (vi ) : : : : : : [7] [11, 12] [15, 16, 12] [19, 20, 16, 12] [23, 24, 20, 16, 12] [27, 28, 24, 20, 16, 12] 2.3. VISITING TREES WHILE LOOKING FOR GOOD NODES 31 (vii ) : [28, 24, 20, 16, 12] (viii ) : [24, 20, 16, 12] (ix ) : [29, 20, 16, 12] (x ) : [20, 16, 12] (xi ) : [25, 16, 12] Note that the head of the last list [25, 16, 12] is 25 and, since p(25) is true, the execution of the function existsev stops. As the reader may verify, this sequence of eleven lists of nodes is the sequence of the frontiers of the tree, which are determined by the depth first visit of the tree with root 7. Indeed, in each successive call of the function existsev, the leftmost element of the list is replaced by its son nodes. In Figure 2.3.1 on the facing page we have depicted the first frontier [7] (the node 7 is the initial node), the second frontier [11, 12], the third frontier [15, 16, 12], . . ., until the sixth frontier which is [27, 28, 24, 20, 16, 12]. The other frontiers, from the seventh frontier which is [28, 24, 20, 16, 12], until the eleventh one which is [25, 16, 12], are shown in Figure 2.3.2.  7 11 15 19 23 27 24 28 12 16 20 : (vii) : (viii) : (ix) : (x) : (xi) 21 17 21 25 25 26 25 26 22 27 29 Figure 2.3.2. The frontiers, from the seventh one to the eleventh one, of the depth first visit of the tree generated from the initial node 7 by applying the tree-generating function f of Example 2.3.1 on the facing page. 2.3.2. Depth First Visit of Trees: Burstall’s Version. In this section we present a different algorithm for visiting trees in a depth first manner. It has been suggested to us by R. M. Burstall. As in the previous Section 2.3.1, we are given a predicate p, a tree-generating function f , and a node n. Suppose that f generates a finite tree rooted in n. The following function existsev 1(p, f, n) returns true iff in the tree rooted in n and generated from n by the function f there exists a node m such that p(m) = true, otherwise it returns false. As for the function existsev which is defined on page 29, the correctness of the existsev1 function is based on the assumption that for any node n, the tree rooted 32 2. EXPLORING SEARCH SPACES in n, is finite. This assumption is necessary because the tree rooted in n is generated and visited in a depth first manner (see the line marked with (††)). Given a predicate p and a list L of nodes, the function exists(p, L) returns true if in the list L there exists a node m such that p(m) is true, otherwise it returns false. The functions existsev 1 and exists are defined as follows. existsev 1 : (α → bool ) × (α → α list) × α → bool existsev 1 p f x = if p(x) then true else exists (existsev 1 p f ) f (x) (††) exists : (α → bool ) × (α list) → bool exists p L = if L = [ ] then false else if p(hd (L)) then true else exists p tl (L) Note that in the above function we use the partial application technique: the predicate which is the first argument of the function exists in the line marked with (††), is the result of the application of the function existsev 1 (which is of arity 3) to the predicate p and the function f . Thus, at line (††) the type of existsev 1 p f is α → bool , which is the type of a predicate on nodes, and the type of f (x) is α list, which is the type of a list of α’s. Note 2.3.2. The functions existsev and existsev 1 are equivalent in the sense that for any given predicate p of type α → bool , any tree generating function f of type α → αlist, and any given initial node n of type α, we have that: existsev p f [n] = existsev 1 p f n.  As we will see in more detail in Chapter 3, one can use this depth first visit algorithm for parsing strings (or words) generated by formal grammars. Here is an example where we consider the regular grammar G with axiom P and whose productions are: P →b P → bQ Q→a Q → aQ We want to check whether or not the string ba belongs to the language L(G) generated by G. We can do so by constructing a tree whose root node is the pair hP, bai of the axiom P and the string-to-parse ba. The nodes of that tree are constructed by applying at any node hσ1 , σ2 i, where σ1 is a sentential form and σ2 is a string of terminal symbols, either (i) the chop operation, or (ii) the expand operation, which we will describe below. The construction of the tree finishes when a node of the form hε, εi is constructed, and this fact indicates that the string ba belongs to the language generated by G. Indeed, one can show that, in general, given a grammar H with axiom S, and a string σ, we have that σ ∈ L(H) iff the node hε, εi belongs to the tree constructed from the root node hS, σi. Here is the description of the chop and expand operations. (i) The chop operation is applied at a node of the form haσ1 , aσ2 i, where a is a terminal symbol, and it constructs the single son node hσ1 , σ2 i. Thus, the chop operation consists in deleting the leftmost terminal symbol of both components of the pair in the node, if they are the same terminal symbol. 2.3. VISITING TREES WHILE LOOKING FOR GOOD NODES 33 hP, bai ❅ ❅ expand ❅ ❘ ❅ ✠ hb, bai P : P → b | bQ hbQ, bai chop b chop b ❄ ❄ hε, ai false hQ, ai ✠ ha, ai chop a ❄ hε, εi true ❅ ❅ expand ❅ ❘ ❅ Q: Q → a | aQ haQ, ai chop a ❄ hQ, εi ... Figure 2.3.3. The tree of nodes which shows that the string ba is generated by the grammar G with axiom P and whose productions are: P → b | bQ, Q → a | aQ. (ii) The expand operation is applied at a node of the form hAσ1 , σ2 i, where A is a nonterminal symbol, and it constructs the list of the son nodes of that node which is [hα1 σ1 , σ2 i, . . ., hαn σ1 , σ2 i], if α1 , . . . , αn are the right hand sides of all the productions of the given grammar whose left hand side is A. Thus, the expand operation is based on the replacement in all possible ways of the leftmost nonterminal symbol of the sentential form of the node. For instance, given the node hP, bai, the expand operation constructs the list [hb, bai, hbQ, bai] of son nodes, because for the symbol P in the grammar G we have the two productions P → b and P → bQ. Figure 2.3.3 shows the tree with root hP, bai which is generated by the chop and expand operations. The fact that in that tree there is the node hε, εi indicates that the string ba belongs to the language generated by G. The construction of the tree which we have described above in terms of the chop and expand operations, can also be described in terms of a suitable tree-generating function f . We leave the definition of that tree-generating function to the reader. The predicate p which tells us when we have found the node hε, εi, may be defined as follows: for any given node hσ1 , σ2 i, we have that p(hσ1 , σ2 i) = true iff σ1 = σ2 = ε. 2.3.3. Breadth First Visit of Trees. In this section we present a third algorithm for visiting trees. The visit is performed in a breadth first manner and thus, in order to ensure the correctness of this algorithm, we do not need the hypothesis that the tree generated from any given node by the given tree-generating function, is finite. Given a function f with type α → αlist and a list L of elements of type α, the function flatmap(f, L) returns the concatenation of the lists produced by applying the function f to every element of L. 34 2. EXPLORING SEARCH SPACES The function exists is the one we have presented on page 32. The function bf-existsev (short for breadth first exists eventually) is very similar to the function existsev presented on page 29. They have the same type. However, in the case of the function bf-existsev, the tree is generated and visited in a breadth first manner (see the line marked with (†††)). Indeed, at each recursive call of the function bf-existsev, we construct the list of the son nodes of all nodes occurring in the list of the previous call of bf-existsev. Thus, the tree is generated level-by-level, by generating the nodes at distance k+1 from the root, only after the generation of all nodes at distance k, for any k ≥ 0. The functions flatmap, exists, and bf-existsev are defined as follows. bf-existsev : (α → bool ) × (α → α list) × (α list) → bool bf-existsev p f L = if L = [ ] then false else if exists p L then true else bf-existsev p f (flatmap f L) (†††) exists : (α → bool ) × (α list) → bool exists p L = if L = [ ] then false else if p(hd (L)) then true else exists p tl (L) flatmap : (α → α list) × (α list) → (α list) flatmap f L = if L = [ ] then [ ] else (f (hd (L)) <> (flatmap f tl (L)) Note that the functions existsev and bf-existsev may not terminate if starting from a given node, the iterated application of the function f generates an infinite tree. Exercise 2.3.3. Show that the following function definition of existsev is not correct: existsev p f L = if exists p L then true else existsev p f (f (hd(L)) <> tl (L)) Hint: Consider the case when L = [ ].  Exercise 2.3.4. Show that the following function definition for bf-existsev is not correct: bf-existsev p f L = if L = [ ] then false else if p(hd (L)) then true else bf-existsev p f (flatmap f L) Hint: Some nodes are used for generating their son nodes, but for them we never test whether or not p holds.  Exercise 2.3.5. Show that the following function definition for bf-existsev is not correct: bf-existsev p f L = if exists p L then true else bf-existsev p f (flatmap f L) Hint: Consider the case when L = [ ].  CHAPTER 3 Chop-and-Expand Parsers for Context-Free Languages In this chapter we illustrate the Chop-and-Expand parser for context-free languages in a functional language (similar to Standard ML [17]), an imperative language (Java 1.5), and a logic language (Prolog [5]). The idea of this parser is to perform a depth first visit of tree-like search space as indicated in Chapter 2. This parser works for context-free grammars which are not left recursive, that is, grammars in which no nonterminal symbol A exists such that: (i) S →∗ αAβ for some α, β ∈ (VT ∪ VN )∗ , and (ii) A →+ Aγ for some γ ∈ (VT ∪ VN )∗ (see Definition 1.2.19 on page 16). If the given grammar is left recursive, the Chop-and Expand parser may not be able to explore via the depth first search the whole search space, and thus parsing may not terminate. The reader will find in the companion book [21] the description of two general algorithms which work for any context-free grammar (left recursive or not left recursive): (i) the Cocke-Younger-Kasami parser, and (ii) the Earley parser. We will not describe these algorithms here. 3.1. Chop-and-Expand Context-Free Parser in a Functional Language As indicated in Section 2.3.2 on page 31, in order to solve a parsing problem in the case of context-free grammars, we have to explore a search space which is a tree, whose root node is made out of the pair hS, string-to-parsei, where S is the axiom of the grammar and string-to-parse holds the input string to be parsed. Below we present a program written in a functional language which performs the chop-and-expand parsing of a string generated by a context-free grammar G = hVT , VN , P, Si. We use the recursive function exists which takes two arguments: (i) a list of states, also called nodes, and (ii) a context-free grammar which is not left recursive. The list of states represents the current frontier of the tree which must be visited with the objective of finding in the tree a state of the form hε, εi (where ε is the empty string). The context-free grammar tells us how to construct the tree to be visited starting from its root, and indeed, by using that grammar we can construct, as we will see below, the son states of any given state by applying the function expand. The portion of the tree which is strictly above the current frontier, has been already visited and no state of the form hε, εi exists in that portion of the tree. Contrary to the function shown on page 32, the function exists presented here is a first order function (and not a higher-order function, because none of its arguments is a function or a predicate). This first order function exists has the advantage of allowing us to derive a chop-and-expand parser written in Java (see page 42) by a straightforward translation. 35 36 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES Now we indicate: (i) the conventions, (ii) the variables, and (iii) the data structures which are used in our first order function exists. Any list of characters is written as a list and also as a string. For instance, [b,c,c] is also written as bcc. Given a list ℓ, the head function hd(ℓ) and the tail function tl(ℓ) are defined as usual. Thus, we have that hd(bcc) = b and tl(bcc) = cc. In particular, for any character c, the list [c] made out of that character only, is also written as the string c. The empty string is denoted by ε. The type of the list of characters is called char list. It is also called string. In what follows, when we write lists we will separate their members by either commas or blanks. Pairs are denoted by using the angle brackets ‘h’ and ‘i’. For instance, the pair of the strings b and cb is denoted by hb,cbi. The type of the lists of states is also called state list. The type of the lists of strings is also called string list. This type is also called right-hand-sides when the elements of the lists denote the right hand sides of all the productions for a given nonterminal symbol. For instance, given a context-free grammar whose productions for the nonterminal symbol P are P → b and P → aQQ, the list [b, aQQ] has type right-hand-sides. Given a nonterminal symbol, say P, and the productions for it, say P → b and P → aQQ, these productions can be grouped together and represented as the pair hP, [b, aQQ]i whose type is symbol×right-hand-sides. The type of the lists of symbol ×right-hand-sides pairs is called (symbol ×righthand-sides) list. This type is also called grammar when the first components of the pairs of the list are all distinct symbols. For instance, given the context-free grammar whose productions are: P→b P → aQQ Q→ε Q→b Q → bP it can be represented as the list of pairs [hP, [b, aQQ]i, hQ, [ε, b, bP]i] which has type grammar. Each element in that list is a pair of a nonterminal symbol and the list of the right hand sides of all the productions for that nonterminal symbol. In Table 1 on page 37 we show the syntactic categories of the identifiers occurring in the function exists and some examples of these syntactic categories. As usual, we write the terminal symbols and the nonterminal symbols using lower case and upper case letters, respectively. The variable sf whose type the value of a sentential form. sentential form and a string of state hσ1 , σ2 i is the state hε, εi terminal symbols. is char list (also called sentform in Table 1), stores A state, that is, a node of the tree, is a pair of a terminal symbols. We have that a descendant of a iff the sentential form σ1 generates the string σ2 of The function rhs (short for right hand sides) takes in input a nonterminal symbol, that is, an element of type char, and produces in output the list of the right hand sides of the productions in the given grammar whose left hand side is the given nonterminal symbol. The type of the function rhs is: char → ((char list) list), which can also be written as: char → (string list) or as: char → right-hand-sides. 3.1. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN A FUNCTIONAL LANGUAGE Types of identifiers. 37 Examples. (a and b are terminals. P, Q, and R are nonterminals.) • Sentential form (with terminal and nonterminal symbols). sentform sf : char list PRb • String (with terminal symbols only). string s : char list bb • State. hsentform sf, string si : state hPRb, bbi • List of states. stl : state list [hPRb, bbi hbPQ, abi] • The right hand sides of the productions for the same nonterminal symbol. rhs : char → (char list) list Given the productions P → b | aQQ we have that: rhs(P) = [b, aQQ] • Grammar as a list of pairs. Each pair is made out of a nonterminal symbol and the list of all the right hand sides of the productions for that symbol. gr : (symbol ×right-hand -sides) list The grammar with the productions P → b | aQQ and Q → ε | b | bP is represented as: [hP, [b, aQQ]i hQ, [ε, b, bP]i] Table 1. Types and examples of the identifiers occurring in the function exists. The members of the lists of states and the members of the lists of productions are separated by blanks (not by commas). Note that our representation of a context-free grammar as a list of hsymbol, righthand -sidesi pairs makes it easy to evaluate the function rhs. Indeed, (i) no two pairs in the list which represents a grammar have the same nonterminal symbol as their first component, and (ii) for each element of the form hA, Rsi in the list which represents a grammar, we have that: rhs(A) = Rs. Given a list of states which is the frontier of a tree to be visited, the function expand allows us to produce the new frontier of the expanded tree to be visited. This new frontier is obtained by replacing the leftmost state in the given list of states (that is, the head of the list of states) by its son states. These son states are generated according to the given context-free grammar by using the function expand, as we now indicate. Let the frontier of the given tree be the state list h : T , where the state h is the pair of the form hAσ, si, for some nonterminal A ∈ VN , string σ ∈ (VT ∪ VN )∗ , and string s ∈ VT∗ . Then the frontier of the new tree is: [hα1 σ, si, . . .,hαn σ, si] <> T 38 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES where A → α1 | . . . | αn are all the productions of the given context-free grammar whose left hand side is A, and <> denotes the infix append operation on lists. The function expand has the following type: expand : (char list) list × char list × char list → state list that is, expand : right-hand-sides ×sentform × string → state list, and it is defined as follows expand (rhs(A), σ, s) = [hα1 σ, si, . . .,hαn σ, si]. For instance, given the two productions P → b | aQQ and the state list stl which is [hPRb, bbi hbPQ, abi], after the execution of the statement new-stl = expand (rhs(P), Rb, bb) <> tl (stl ) we get that a new value for new-stl which is [hbRb, bbi haQQRb, bbi hbPQ, abi] Here is the code of the function exists for the Chop-and-Expand parser for a context-free grammar which is not left recursive. terminal (x) and nonterminal(x) are predicates which return true iff x is a terminal or a nonterminal symbol, respectively. The function exists has the following type: exists : state list × grammar → bool where, as already mentioned, the type grammar is (symbol ×right-hand-sides) list. Chop-and-Expand Parser exists stl gr = if stl =[ ] then false else let hd (stl )=hsf, si in if sf = ε and s = ε then true else if sf 6= ε and nonterminal(hd(sf )) then (†) begin // expand new-stl = expand(rhs(hd(sf )), tl (sf ), s) <> tl (stl ); exists new-stl gr end else if sf 6= ε and s 6= ε and terminal (hd(sf )) and hd (sf ) = hd (s) then (††) exists (htl(sf ), tl(s)i : tl(stl)) gr // chop else exists tl (stl ) gr Note that in line (††) above, the condition ‘and terminal (hd(sf ))’ is not necessary. We have added it for reasons of symmetry with respect to the above line (†). The initial call of the function exists is of the form: exists [hS, string-to-parsei] given-grammar where S is the axiom of the given-grammar and the first argument of the function exists is a list of states which consists of one state only, and that state is the pair hS, string-to-parsei. 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 39 3.2. Chop-and-Expand Context-Free Parser in Java In this section we present the two Java programs: List.java and CFParser.java, which realize the context-free parsing using the Chop-and-Expand algorithm illustrated in Section 2.3.2 on page 31 and Section 3.1 on page 35. In those sections that algorithm is presented using a functional language. The C++ versions of those programs can be found in [19, page 69]. A similar version of the program List.java can be found (with the same name) in Section 7.4 on page 212. /** * ========================================================================= * Generic Class List <T> (class used for parsing) * Filename: "List.java" * * Every object of the class List<T> is a list of elements of type T. * The following methods are available: * * cons(T d), head(), tail(), isSingleton(), isEmpty(), * append(List<T> list2), makeEmpty(), copy(), and listPrint(). * * Also cloneCopy() is available if we uncomment line (***) below and * comment the previous line. * * The head of the list is to the right, not to the left. * * Note that tail() is destructive. * After a tail operation, if we need the original value of the list, we * should reconstruct it by ‘consing’ the head of the list. * ========================================================================= */ import java.util.*; public class List<T> { // needed for using ArrayList<T> and Iterator // it is ok if we do not use cloneCopy() // public class List<T> implements Cloneable { // (***) In order to use // cloneCopy(), uncomment this line and comment the previous one. private ArrayList<T> list; public List() { list = new ArrayList<T>(); } // constructor public void cons(T datum) { list.add(datum); } // the head of the list is to the right, // not to the left. // add(_) is a method of ArrayList<T> public T head() { // the head of the list is to the right. if (list.isEmpty()) // It is the last element: >------------+ {System.out.println("Error: head of empty list!");};// | T obj = list.get(list.size() - 1); // <-------------------------+ return obj; // isEmpty(), size(), and get(int i) } // are methods of ArrayList<T> 40 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES public void tail() { // destructive tail() method if (list.isEmpty()) {System.out.println("Error: tail of empty list!");}; list.remove(list.size() - 1); // isEmpty(), size(), and remove(int i) } // are methods of ArrayList<T> public boolean isSingleton() { return list.size() == 1; // size() is a method of ArrayList<T> } public boolean isEmpty() { return list.isEmpty(); } // isEmpty() is a method of ArrayList<T> // As usual, the head of the list ‘this.append(list2)’, after appending // the ‘list2’ to the list ‘this’, is the head of the list ‘this’, before // the append operation. Thus, ‘this’ = ‘this’ <> ‘list2’. // --- append(l2) is not destructive! public List<T> append(List<T> list2) { List<T> l2 = list2.copy(); // l2 is an argument to a recursive call // to append. list2 should be copied! if (this.isEmpty()) {return l2;} else {T a = this.head(); this.tail(); List<T> l = this.append(l2); l.cons(a); this.cons(a); // reconstructing the list ‘this’ return l;} } /** * ---------------- Examples of isEmpty() ---------------------------------* System.out.println(new List<Integer>().isEmpty()); prints true * System.out.println(new List().isEmpty()); prints true * List <Integer> v = new List(); * System.out.println(v.isEmpty()); prints true * System.out.println(v == null); prints false * ------------------------------------------------------------------------*/ public void makeEmpty() { list.clear(); // clear() is a method of ArrayList<T> } // -------------------------- Printing a list ---------------------------// We assume that an element of the list has a toString() method. public void listPrint() { System.out.print("[ "); for (Iterator iter = list.iterator(); iter.hasNext(); ) { System.out.print((iter.next()).toString() + " "); }; System.out.print("]"); } // -------------------------- Making a copy of a list without clone() ---public List<T> copy() { List<T> copyList = new List<T>(); for (Iterator<T> iter = list.iterator(); iter.hasNext(); ) { copyList.list.add(iter.next()); }; return copyList; } 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 41 // -------------------------- Making a copy of a list with clone() ------public List<T> cloneCopy() { // see (***) above try { List<T> copyList = (List<T>)super.clone(); copyList.list = (ArrayList<T>)list.clone(); return copyList; } catch (CloneNotSupportedException e) { throw new InternalError(); } } } /** * ------------------------------------------------------------------------* Let us consider the program fragment: * Integer k = new Integer(6); * List<Integer> la = new List<Integer>(); la.cons(k); * System.out.print("\nla: "); la.listPrint(); * la.cons(new Integer(4)); la.cons(new Integer(9)); * // in Java 1.5: we can write: 9, instead of: new Integer(9) * System.out.print("\nla: "); la.listPrint(); * List<Integer> lb = new List<Integer>(); lb = la.copy(); * System.out.print("\nlb: "); lb.listPrint(); * lb.tail(); System.out.print("\nlb.tail(): "); * lb.listPrint(); // tail() is destructive! * la.append(lb); // append(l2) is not destructive! * System.out.print("\nla: ");la.listPrint(); lb = la.append(lb.append(lb)); * System.out.print("\nlb: ");lb.listPrint(); * --- After compilation we get: * Note: ./List.java uses unchecked or unsafe operations. * Note: Recompile with -Xlint:unchecked for details. * --- When running the program we get (the list head is to the right): * la: [ 6 ] * la: [ 6 4 9 ] * lb: [ 6 4 9 ] * lb.tail(): [ 6 4 ] * la: [ 6 4 9 ] * lb: [ 6 4 6 4 6 4 9 ] * ------------------------------------------------------------------------*/ Now we will present the program CFParser.java. The method exists, which is listed immediately above the main method, implements in Java the exists function (see the Chop-and-Expand parser presented on page 38). The program CFParser.java takes as input a context-free grammar as a string gg of characters and a string stp of characters, and produces as output a boolean value which tells us whether or not stp belongs to the language generated by gg. The details of the external and internal representations of the grammar gg are indicated in the initial comments. For instance, the grammar with axiom P and productions: P → b | a Q Q, Q → ε | b | b P , is externally represented as the string P>b|aQQ;Q>e|b|bP and internally represented as the list [h‘P’, [“b”, “a Q Q”]i, h‘Q’, [“ ”, “b”, “b P ”]i], where “ ” denotes the empty string ε. The internal representation of gg is obtained from its external representation by applying a recursive descent parsing technique, that we do not explain in this book. The reader may refer to [2, pages 181–182]. Other examples of application of that technique can be found in Sections 7.1 on page 173, 7.2 on page 183, 7.3 on page 194, and 7.4 on page 207. 42 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES /** * ========================================================================= * CHOP-AND-EXPAND PARSER FOR CONTEXT FREE GRAMMARS * Filename:"CFParser.java". Thanks to R. M. Burstall and E. W. Dijkstra. * * ------------------------------------------------------------------------* --- TYPES --* List<char> = String * State = <sentential_form, string> * List<State> = list of states * Rsides = list of String : list of right hand sides * SyRs = <Sy,Rs> = <char:symbol, Rsides:Rs = its right hand sides> * Gram = List<SyRs> : list of SyRs pairs (type of a grammar) * ------------------------------------------------------------------------* We use GENERIC TYPES to avoid the redefinition of the functions head(), * tail(), cons(), isEmpty(), makeEmpty(), and append(List<String> l2) for: * * - list of String (type: List<String>). It is the type of the list of all * right hand sides of a nonterminal. This type is also called: Rsides. * - list of States (type: List<State>) * - list of SyRs (type: List<SyRs>). It is the type of a grammar. It is * the type of the list of <nonterminal, list of its right hand sides>. * This type is also called: Gram. * Redefinition of functions is necessary for printing lists of different * types. * ------------------------------------------------------------------------* The input grammar is given as a string, named ’gg’, generated from Gram * according to the following productions: * * character ::= ’a’..’d’ | ’f’..’z’ | ’A’..’Z’ * (’e’ is reserved: see below) * charlist ::= character | character charlist (that is, String) * Rsides ::= ’e’ | charlist | charlist ’|’ Rsides * SyRs ::= char ’>’ Rsides * Gram ::= SyRs | SyRs ’;’ Gram * * The empty productions, also called epsilon-rules, are allowed. * The character ’e’ is reserved for denoting the right hand side of * an empty production, that is, ’e’ denotes the empty charlist. * For instance: * gg="P>b|aQQ;Q>e|b|bP" stands for: P->b, P->aQQ, Q->e, Q->b, Q->bP * In P>b|aQQ;Q>e|b|bP we have that: * \_/ aQQ is a string: "aQQ" * \___/ b|aQQ is a list of elements of type String, i.e., an Rsides. * It is: ["b", "aQQ"] * \_____/ P>b|aQQ is a SyRs, i.e., a pair of a symbol and its * right hand sides. It is: <’P’, ["b", "aQQ"]> * The axiom of the grammar is the nonterminal of the left hand side * of the first production of the grammar. * * The string to parse, named ’stp’, is either a non-empty sequence of * characters from the set {’a’,...,’d’,’f’,...,’z’} or is the empty * sequence denoted by ’e’. * -------------------- ASSUMPTIONS ON THE INPUT --------------------------* (1) The input grammar is given as a string generated from Gram according * to the rules stated above. Actually, we can add a final dot because of * the getCh() method of the IndexString class (see below). For instance, * we can write "P>b|aQQ;Q>e|b|bP.", instead of "P>b|aQQ;Q>e|b|bP". 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 43 * (2) When writing the input grammar, the productions relative to the same * nonterminal should be grouped together. For instance, we should write * "P>a|aQQ;Q>e|b|bP", instead of "P>a;Q>e;P>aQQ;Q>b;Q>bP". * ------------------------------------------------------------------------* The grammar should not be left recursive, that is, it should not be the * case that a nonterminal A exists such that A r ->* A s, for some strings * r and s of terminal and/or nonterminal symbols of the grammar. * ------------------------------------------------------------------------* This context-free parser works by exploring in a depth first fashion all * possible words generated by the grammar and consistent with the * character at hand in the string to parse ’stp’. * ------------------------------------------------------------------------* There is a global variable ’traceon’. If it is set to ’true’ then we * can see the various steps through the execution of the program, and in * particular, the sequence of the lists of states which are generated * during the depth first visit of search space. * ------------------------------------------------------------------------* After performing a tail() operation on a list we need to reconstruct * the list which, otherwise, is modified (see statements marked by (***)). * ========================================================================= */ import java.util.*; // ------------------------------------------------------------------------// string with an index pointing at the next available character. class IndexString { public String string; public int index; public IndexString(String string) { // constructor this.string = string; this.index = 0; // index points to the next character to be read } public void back(){ index--; } public char getCh() { char x; if (index < string.length()) { x = string.charAt(index); } else x = ’.’; index++; return x; // index is incremented by one unit. } // If index==string.length(), then char x is ’.’ } //-------------------------------------------------------------------------// state : <String:sentential_form, String:input_string> class State { public String sentForm; public String inString; public State() { this.sentForm = ""; this.inString = ""; // ----------------------------------------} // state : printing function // for reasons of readability the empty string is printed as "e" public void statePrint() { String sentForm1 = sentForm.equals("") ? "e" : sentForm; String inString1 = inString.equals("") ? "e" : inString; System.out.print("(" + sentForm1 + ", " + inString1 + ")"); } 44 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES // ----------------------------------------// List<State> : printing functions public static void stlPrintNE(List<State> L) { if (!L.isEmpty()) {State h = L.head(); h.statePrint(); L.tail(); if (!L.isEmpty()) {System.out.print(" ");}; stlPrintNE(L); L.cons(h); //reconstructing the list L of states (***) }; } public static void stlPrint(List<State> L) { if (L.isEmpty()){System.out.print(" []");} else {System.out.print(" ["); stlPrintNE(L); System.out.print("]");} } } // ------------------------------------------------------------------------// (left hand side), (right hand sides) // SyRs: < char: symb, List<String>: rhss > class SyRs { public char symb; public List<String> rhss; public SyRs() { this.symb = ’-’; this.rhss = null; } // ----------------------------------------// List<String> = Rsides:printing functions public static void rPrintNE(List<String> L) { if (!L.isEmpty()) { String h = L.head(); if (h.equals("")) {System.out.print("e");} else {System.out.print(h);}; L.tail(); if (!L.isEmpty()) {System.out.print(" | ");}; rPrintNE(L); L.cons(h); // reconstructing the list L (***) } } public static void rPrint(List<String> L) { if (L.isEmpty()) {System.out.print("<>");} else {System.out.print(" "); rPrintNE(L); System.out.print(" ");} } // ----------------------------------------// SyRs : printing function public static void SyRsPrint(SyRs r) { System.out.print(r.symb + " -> "); rPrint(r.rhss); } } // ------------------------------------------------------------------------// Gram : List<SyRs> (it is the type of a grammar) class Gram { public List<SyRs> gg; public Gram() { this.gg = new List<SyRs>(); } 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 45 // ----------------------------------------// List<SyRs> = Gram: printing functions public static void printGramNE(List<SyRs> L) { if (!L.isEmpty()) {SyRs h = L.head(); SyRs.SyRsPrint(h); System.out.print("\n"); L.tail(); if (!L.isEmpty()) {System.out.print(" ");}; printGramNE(L); L.cons(h); } } public static void printGram(List<SyRs> L) { if (L.isEmpty()) {System.out.print("{ }");} else {System.out.print("{ "); printGramNE(L); System.out.print("}");} } } // ------------------------------------------------------------------------public class CFParser { //-------------------------------------------------------------------------/* RECURSIVE DESCENT PARSING * for making the grammar from the string gg. * The function ’parseString’ gets one of the alternatives (as a list of * characters) of the right hand sides of the productions of a nonterminal. */ public static String parseString(IndexString f) { char x = f.getCh(); String chl; if ((’a’<=x && x<=’z’)||(’A’<=x && x<=’Z’)) { chl = x + parseString(f); } else { chl = ""; f.back(); }; return chl; } // At the end, the ’index’ of the class IndexString points to either ’>’ // or ’|’ or ’;’ or is equal to the length of the string of the // IndexString f, thus, returning ’.’ // ----------------------------------------public static List<String> parseRsides(IndexString f) { List<String> rs = new List<String>(); String chl2 = parseString(f); //this is the case of the epsilon production if (chl2.equals("e")) {chl2 = "";}; char x = f.getCh(); if (x==’|’) { rs = parseRsides(f); rs.cons(chl2); return rs; } else {//Here x can be either ’;’ or ’.’. No other characters are possible. f.back(); // ready to get again either ’;’ or ’.’. rs.cons(chl2); return rs; } // At the end, in rs we have all the productions of a nonterminal. } // ----------------------------------------public static SyRs parseProds(IndexString f) throws RuntimeException { SyRs sRs1 = new SyRs(); sRs1.symb = f.getCh(); char x = f.getCh(); if (x==’>’) { List<String> rs1 = new List<String>(); rs1 = parseRsides(f); sRs1.rhss=rs1; return sRs1; } else { // x should have been ’>’. If it is not ’>’ we throw an exception. throw new RuntimeException("Error in production!\n"); } } 46 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES // ----------------------------------------public static List<SyRs> parseGram(IndexString f) throws RuntimeException { SyRs sRs1 = parseProds(f); char x = f.getCh(); // Here x can be either ’;’ or ’.’. No other characters are possible. if (x==’;’) { List<SyRs> gram2 = new List<SyRs>(); gram2 = parseGram(f); gram2.cons(sRs1); return gram2; } else if (x==’.’) { List<SyRs> gram1 = new List<SyRs>(); gram1.cons(sRs1); return gram1; } else { throw new RuntimeException("Error parseGram!"); } } //-------------------------------------------------------------------------// trace function public static void trace(boolean traceon, String s, List<State> stl) { if (traceon) {System.out.print("\n" + s +":\n"); State.stlPrint(stl);}; } //-------------------------------------------------------------------------// visiting the search space: constructing new lists of states public static List<String> rightSides(char s, List<SyRs> gr) throws RuntimeException { if (gr.isEmpty()) { throw new RuntimeException ("No production for "+ s + " in the grammar.\n");} else {SyRs sR = new SyRs(); sR = gr.head(); if (sR.symb == s) { return sR.rhss; } else { List<String> rs = new List<String>(); gr.tail(); rs = rightSides(s,gr); gr.cons(sR); // reconstructing the grammar gr (***) return rs; } } } //-------------------------------------------------------------------------// final function public static boolean isFinal(State st) { return (st.sentForm.equals("")) && (st.inString.equals("")); } //-------------------------------------------------------------------------// chop function public static void chop(State st) {// The state st is modified. The first // characters of the strings are chopped st.sentForm = st.sentForm.substring(1, st.sentForm.length()); st.inString = st.inString.substring(1, st.inString.length()); } //-------------------------------------------------------------------------// expand function public static List<State> expand(List<String> rs, String v, String w) { State st = new State(); List<State> stl = new List<State>(); if (rs.isEmpty()) { stl.makeEmpty(); return stl; } 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 47 else {String cs = rs.head(); rs.tail(); stl = expand(rs,v,w); rs.cons(cs); // reconstructing the list rs (***) st.sentForm = cs.concat(v); st.inString = w; stl.cons(st); return stl; } } //-------------------------------------------------------------------------// exists function // // This code is directly derived from the code of the exists function // in functional style. The search space is visited in a depth first manner. // private static boolean exists (boolean traceon, List<State> stl1, List<SyRs> gr) { if (stl1.isEmpty()) { return false; } else { State st1 = new State(); st1 = stl1.head(); String st1sentForm = st1.sentForm; String st1inString = st1.inString; if (isFinal(st1)) { return true; } else if (!(st1sentForm.equals("")) && (’A’<=st1sentForm.charAt(0)) && (st1sentForm.charAt(0)<=’Z’)) { List<State> stl2 = new List<State>(); List<String> rs = new List<String>(); stl1.tail(); // rs : rhs’s of the nonterminal symbol rs = rightSides(st1sentForm.charAt(0),gr); // delete the nonterminal symbol st1sentForm = st1sentForm.substring(1, st1sentForm.length()); // EXPAND the nonterminal symbol stl2 = expand(rs,st1sentForm,st1inString); stl1 = stl2.append(stl1); // stl1 = stl2 <> stl1 trace(traceon,"expand",stl1); // tracing --return exists(traceon,stl1,gr); } else if (!(st1sentForm.equals("")) && !(st1inString.equals("")) && (((’a’<=st1sentForm.charAt(0)) && (st1sentForm.charAt(0)<=’d’)) || ((’f’<=st1sentForm.charAt(0)) && (st1sentForm.charAt(0)<=’z’))) && (st1sentForm.charAt(0) == st1inString.charAt(0))) { chop(st1); stl1.tail(); stl1.cons(st1); // CHOP the terminal symbol trace(traceon,"chop",stl1); // tracing --return exists(traceon,stl1,gr); } else { stl1.tail(); trace(traceon,"fail",stl1); // tracing --return exists(traceon,stl1,gr); } } } //-------------------------------------------------------------------------// main program public static void main(String[] args) { /** -----------------------------------------------------------------------* gg : given-grammar * stp : string-to-parse * ------------------------------------------------------------------------*/ String gg ="P>b|aQQ;Q>e|b|bP"; // Example 0 (with an empty production) String stp="ab"; // true boolean traceon = true; // variable for tracing the execution 48 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES //-------------------------------------------------------------------------// From the string gg to the List<SyRs> gr0. // We first construct the IndexString f from the String gg. // gr0 is the grammar encoded by the given string gg. //-------------------------------------------------------------------------IndexString f = new IndexString(gg); List<SyRs> gr0 = new List<SyRs>(); char axiom = gg.charAt(0); if ((’A’<=axiom) && (axiom<=’Z’)) { System.out.print("\nThe input grammar is "); System.out.print("(\"e\" denotes the empty string in the rhs):\n"); gr0 = parseGram(f); /** * ------------------------------------------------------------------------* In order to avoid parsing, the above statement ’gr0 = parseGram(f);’ can * be replaced by the following statements (erase the *’s, please): * * SyRs Prs = new SyRs(); Prs.symb=’P’; // P * String chl1P = "b"; // b * String chl2P = "aQQ"; // |aQQ * List<String> rsP = new List<String>(); * rsP.cons(chl2P); rsP.cons(chl1P); // b|aQQ * Prs.rhss = rsP; // P>b|aQQ * SyRs Qrs = new SyRs(); Qrs.symb=’Q’; // Q * String chl1Q = ""; // e * String chl2Q = "b"; // |b * String chl3Q = "bP"; // |bP * List<String> rsQ = new List<String>(); * rsQ.cons(chl3Q); rsQ.cons(chl2Q); rsQ.cons(chl1Q);// e|b|bP * Qrs.rhss = rsQ; * ----------------------------- gr0 corresponding to gg="P>b|aQQ;Q>e|b|bP" * gr0.cons(Qrs); gr0.cons(Prs); * [<symb, rhss> <symb, rhss> ] * | / \ | / | \ * gr0 is the list of pairs: [<’P’, ["b", "aQQ"]>, <’Q’, ["", "b", "bP"]>] * "" is the empty string. * ------------------------------------------------------------------------*/ Gram.printGram(gr0); } else System.out.print("Not a grammar in input."); System.out.print("\n\nThe axiom is " + axiom + ".\n"); State st = new State(); st.sentForm = axiom + ""; // from char to String (of length 1). st.inString = stp; List<State> stl = new List<State>(); stl.cons(st); System.out.print("The initial list of states is:\n"); State.stlPrint(stl); boolean ans = exists(traceon,stl,gr0); System.out.print("\n\nThe following string of characters:\n System.out.print(stp + "\nis "); if (!ans) {System.out.print("NOT ");}; System.out.print("generated by the given grammar.\n"); } // end of main } "); 3.2. CHOP-AND-EXPAND CONTEXT-FREE PARSER IN JAVA 49 /** * When printing the statelist, the head is to the left. * input: output: * ------------------------------------------------------------------------* javac CFParser.java * java CFParser * * The input grammar is ("e" denotes the empty string in the rhs): * { P -> b | aQQ * Q -> e | b | bP * } * * The axiom is P. * The initial statelist is: * [(P, ab)] * expand: * [(b, ab) (aQQ, ab)] * fail: * [(aQQ, ab)] * chop: * [(QQ, b) * expand: * [(Q, b) (bQ, b) (bPQ, b)] * expand: * [(e, b) (b, b) (bP, b) (bQ, b) (bPQ, b)] * fail: * [(b, b) (bP, b) (bQ, b) (bPQ, b)] * chop: * [(e, e) (bP, b) (bQ, b) (bPQ, b)] * * The following string of characters: * ab * is generated by the given grammar. * ------------------------------------------------------------------------* Various Examples * * String gg="P>b|aQQ;Q>e|b|bP"; // Example 0 (empty production in gg) * String stp="a"; // true * String stp="abaa"; // false * String stp="aba"; // true * String gg="P>a|aQ;Q>b|bP"; // Example 1 -----------------------* String stp="ab"; // true * String stp="ababa"; // true * String stp="aaba"; // false * String gg="P>a|bQ;Q>b|aQ"; // Example 2 -----------------------* String stp="baab"; // true * String stp="bbaaba"; // false * String gg="S>a|aAS;A>SbA|ba|SS"; // Example 2.7 of Hopcroft-Ullman --* String stp="aabaaa"; // true * String stp="aabba"; // false * String stp="aabbaa"; // true * String gg="S>aB|bA;A>bAA|a|aS;B>aBB|b|bS";// same numbers of a’s and b’s * String stp="bbabaaab"; // true * String stp="babaaab"; // false * ------------------------------------------------------------------------*/ 50 3. CHOP-AND-EXPAND PARSERS FOR CONTEXT-FREE LANGUAGES 3.3. Chop-and-Expand Context-Free Parser in a Logic Language Here is a logic program which realizes a Chop-and-Expand Parser. Chop-and-Expand Parser 1. parse(G, [ ], [ ]) ← 2. parse(G, [A|X], [A|Y ]) ← terminal(A), parse(G, X, Y ) // CHOP 3. parse(G, [A|X], Y ) ← nonterminal(A), member(A → B, G), // EXPAND append(B, X, Z), parse(G, Z, Y ) 4. member(A, [A|X]) ← 5. member(A, [B|X]) ← member(A, X) 6. append ([ ], L, L) ← 7. append ([A|X], Y, [A|Z]) ← append(X, Y, Z) Together with these seven clauses we also need the clauses which define the terminal symbols and the nonterminal symbols. Recall that in logic programming [A|X] represents the list whose head is the element A and whose tail is the list X. In particular, if [A|X] = [2, 3, 1, 1] then A = 2 and X = [3, 1, 1]. All constants in Prolog should begin with lower case letters, so that, in particular, the axiom S is represented by the character s. The first argument of parse is a context-free grammar, the second argument is a list of terminal symbols and nonterminal symbols (that is, a sentential form), and the third argument is a word represented as a list of terminal symbols. We assume that context-free grammars are represented as lists of productions of the form x → y, where x is a nonterminal symbol and y is a list of terminal and nonterminal symbols. We have that parse(G, [s], W ) holds iff from the symbol s we can derive the word W using the grammar G, that is, W is a word of the language generated by G. Example 3.3.1. Let us consider the context-free grammar G = h{a, b}, {S}, {S → aSb, S → ab}, Si. It is represented by the clauses: terminal (a) ← terminal (b) ← nonterminal(s) ← together with the list [s → [a, s, b], s → [a, b]] which represents the productions of G. The left hand side of the first production is assumed to be the start symbol. For this grammar G the query ← parse([s → [a, s, b], s → [a, b]], [s], [a, a, b, b]) evaluates to true, and this means that the word aabb belongs to the language generated by G. Note that in the clause member(A, [B|X]) ← member(A, X), we do not require that A is different from B. Indeed with this clause, the query of the form ← member(A, l), where A is a variable and l is a ground list, generates by backtracking all members of the list l with their multiplicity.  More examples of Prolog programs for parsing words generated by grammars can be found in Section 10.1 on page 273. CHAPTER 4 Parsers for Deterministic Context-Free Languages: LL(k ) Parsers In this chapter we will present the so called LL(k) parsers for deterministic context-free languages. We assume that the reader is familiar with the basic notions of context-free languages and context-free grammars which can be found in classical books such as [9, 10]. In particular, we assume that the reader knows the notions of unfolding and folding of context-free productions we have given in Definitions 1.2.12 (on page 15) and 1.2.13 (on page 15). Unless otherwise specified, VT , VN , and S denote respectively, the set of terminal symbols, the set of nonterminal symbols, and the start symbol of the grammars we will consider. By V we denote the set VT ∪ VN . 4.1. Introduction to LL(k ) Parsing The LL(k) parsers are algorithms for parsing the languages which are generated by the LL(k) grammars (see Definition 4.1.1 below). In those grammars we assume the following hypotheses. Hypotheses for LL(k) Parsing, for k ≥ 0. (i) (ii) (iii) (iv) No useless symbols occur in the productions. ε-productions may be present for the start symbol or other symbols of VN . The symbol $ does not belong to VT ∪ VN . The input string in VT∗ to be parsed is terminated by $. We begin by giving the formal definition of an LL(k) grammar, for any k ≥ 0. Recall that, as indicated on page 10, given a string w and a natural number k ≥ 0, the prefix w k of length k of the string w is defined as follows: w if |w| ≤ k wk = u if w = uv for some u, v ∈ V ∗ and |u| = k. { Definition 4.1.1. [LL(k ) Grammar] [3, page 336] For any k ≥ 0, a context-free grammar (possibly with ε-productions) is an LL(k) grammar if for any x, y, z ∈ VT∗ and for any α, β, γ ∈ (VN ∪ VT )∗ , we have that: if (1) S →∗lm xAα →lm xβα →∗lm xy (2) S →∗lm xAα →lm xγα →∗lm xz (3) y k = z k then β = γ. 51 and and 52 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS Note that (1) and (2) are two leftmost derivations of two (possibly different) words xy and xz. Definition 4.1.2. [LL(k ) Language] For any k ≥ 0, an LL(k) language is a language such that there exists an LL(k) grammar which generates it. In what follows we will also need the following definition of a strong LL(k) grammar (see, in particular, Fact 4.1.4, Theorem 4.2.12 on page 58, Fact 4.2.13 on page 58, Theorem 4.3.5 on page 70, and Fact 4.3.8 on page 70). Definition 4.1.3. [Strong LL(k ) Grammar] For any k ≥ 0, a context-free grammar (possibly with ε-productions) is a strong LL(k) grammar if for any x1 , x2 , y, z ∈ VT∗ and for any α1 , α2 , β, γ ∈ (VN ∪ VT )∗ we have that: if (1) S →∗lm x1 Aα1 →lm x1 βα1 →∗lm x1 y and (2) S →∗lm x2 Aα2 →lm x2 γα2 →∗lm x2 z and (3) y k = z k then β = γ. Note that (1) and (2) are two leftmost derivations of two (possibly different) words x1 y and x2 z. One can show the following fact (see also Fact 4.3.16 on page 76). Fact 4.1.4. [LL(0) Grammar and LL(0) Language] Any LL(0) grammar or strong LL(0) grammar generates a language L ⊆ VT∗ which is either the empty set of words or it consists of one word only. If one assume, as we do, that for any k ≥ 0, there are no useless symbols in the LL(k) grammars, then L is consists of one word only. As a consequence of this fact, Fact 4.2.13 on page 58, and Theorem 4.3.17 on page 76, we have that the parsing problem when we deal with LL(0) grammars, or strong LL(0) grammars, or LL(0) languages, is trivial. Thus, unless otherwise specified, when referring to LL(k) grammars (or languages) or strong LL(k) grammars (or languages), we will assume that k is greater than or equal to 1. An LL(k) parser, also called a k-predictive parsing algorithm, is a deterministic pushdown automaton (see Figure 4.1.1 on the next page) which once initialized, performs its moves according to a table T , called a parsing table, as we will indicate. If the string to parse is the string w, the string given in input to the parsing pushdown automaton is w $. The initial configuration of the stack is the string: S $ and S is at the top of the stack. Recall that, unless otherwise specified, we assume that the stack is represented as a string with the top symbol on the left. Definition 4.1.5. [Words Accepted by LL(k ) Parsers] A word w is accepted by an LL(k) parser, for k ≥ 1, if the parser, starting from the configuration in which: (i) the input tape holds the word w $, (ii) the head of the input tape (or input head) points at the leftmost symbol of w, and (iii) the stack holds S $, being S the top of the stack, eventually reaches the configuration where: (i) the head of the input tape points at $, and (ii) the top of the stack is $ and no other symbols are on the stack. 4.2. LL(1) PARSERS 53 LL(k) parser using the parsing table T : top of the stack ✛ ✲ ❄ S $ : stack ✲ nonterminals w1 w2 . . . . . . wn $ : the input string is the string to be parsed terminated by $ ✲ ✻ The head moves from left to right terminals a b + − ... $ S A ... parsing table T (depicted for LL(1) parsing) Figure 4.1.1. A deterministic pushdown automaton for LL(k) parsing, with k ≥ 1. The string to be parsed is w1 w2 . . . wn . Initially, the stack has two symbols only: (i) S on top of the stack, and (ii) $ at the bottom of the stack. The input string is the string to be parsed with the extra rightmost symbol $. We have depicted the parsing table T for the LL(1) parsers. For the LL(k) parsers, with k > 1, different tables should be used. Note that when presenting the LL(k) parsers, some textbooks use different conventions. For instance, some authors do not use the symbol $ at the bottom of the stack and do not add the symbol $ at the right end of the input string. 4.2. LL(1) Parsers In this section we will study the LL(1) parsers and the LL(1) grammars. In these parsers the parsing automaton depicted in Figure 4.1.1 makes its moves according to a table T , called the parsing table, which is a matrix whose rows are labeled by the nonterminal symbols in VN , and whose columns are labeled by the terminal symbols in VT . In the table T there is also one extra column which is labeled by the symbol $. A move of the parsing automaton is either a chop move or an expand move. These moves are specified as follows (see also Figure 4.2.1 on the next page). chop move: if the input head is pointing at a terminal symbol, say a, and the same symbol a is at the top of the stack, then the input head is moved one cell to the right and the stack is popped; 54 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS expand move: if the input head is pointing at a terminal symbol, say a, and the top of the stack is a nonterminal symbol, say A, then the stack is popped and a new string α1 α2 . . . αn , with αi ∈ VT ∪ VN , for i = 1, . . . , n, is pushed onto the stack if the production A → α1 α2 . . . αn is at the entry (A, a) of the parsing table T (thus, after this move the new top symbol of the stack will be α1 ). chop a before : ... a b ... $ after : input string ... a b ... $ ✻✲ ✻ ✲ ❄ a Z ... $ ❄ stack Z ... $ expand A with production A → B1 B2 . . . Bk before : ... a ... ... $ after : input string ... a ... ... $ ✻ ✻ ✛ ❄ ❄ A Z ... $ stack B1 B2 . . . Bk Z . . . $ Figure 4.2.1. The chop move and the expand move of an LL(1) parser. a and b are symbols in VT and Z is a symbol in VT ∪ VN ∪ {$}. In order to explain how to construct the parsing table T of the parsing automaton for the language generated by an LL(1) grammar, we need the following notions of First 1 and Follow 1 [2]. We first introduce the notion of First 1 (x), for any x ∈ VT ∪ VN . Definition 4.2.1. [The Set First 1 (x) for Symbols] Let us consider a contextfree grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. Let V be VT ∪ VN . For every x ∈ V , First 1 (x) is the set of the terminal symbols in VT beginning any string β such that S →∗ x →∗ β, with the stipulation that First 1 (x) should have the extra element ε iff either x is ε or x is a nullable nonterminal symbol (see Definition 1.2.17 on page 15). 4.2. LL(1) PARSERS 55 We can compute the value of First 1 (x), for any x ∈ V , as indicated by the following algorithm. Note that for every x ∈ V , the value of First 1 (x) depends, in general, on the value of First 1 (y), for all y ∈ V . Algorithm 4.2.2. [Computing the Set First 1 (x) for Symbols] Let us consider a context-free grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. Let V be VT ∪ VN . First 1 is a function from V ∪ {ε} to the set of all subsets of VT ∪ {ε}, that is, First 1 : V ∪ {ε} → 2VT ∪{ε} . For every x ∈ V ∪ {ε}, First 1 (x) is the smallest subset of VT ∪ {ε} defined as follows: (i) First1 (ε) = {ε}. (ii) For a ∈ VT , First1 (a) = {a}. (iii) For A ∈ VN , First 1 (A) is a set of terminal symbols which is initialized to ∅ and is modified according to the following rule. For each production A → B1 B2 . . . Bk−1 Bk , where: (1) k ≥ 0, and (2) for i = 1, . . . , k, Bi ∈ VT ∪ VN , add the elements of First 1 (B1 ) − {ε} and if B1 →∗ ε then add the elements of First 1 (B2 ) − {ε} and if B1 B2 →∗ ε then add the elements of First 1 (B3 ) − {ε} and ··· if B1 B2 . . . Bk−1 →∗ ε then add the elements of First 1 (Bk ) − {ε} and if B1 B2 . . . Bk−1 Bk →∗ ε then add ε. Note that if B1 B2 . . . Bk →∗ ε, then A →∗ ε and ε ∈ First 1 (A). The following fact is a straightforward consequence of the definition of the function First1 . Fact 4.2.3. For any A ∈ VN , A →∗ ε iff the empty string ε ∈ First 1 (A). In particular, if A → ε, then ε ∈ First 1 (A). In Algorithm 4.2.2 we have stipulated that First 1 (x) is the smallest subset of VT ∪ {ε} satisfying Conditions (i), (ii), and (iii). The reason for that stipulation is that, since First 1 (x) may depend on itself (and, this happens, for instance, when in the given grammar there is a production of the form A → AB), there could be more than one subset of VT ∪ {ε} satisfying Conditions (i), (ii), and (iii). We can extend the above definition of the function First 1 from elements in V ∪{ε} to strings in V ∗ , by defining a new function, also called First 1 , from V ∗ to the set of all subsets of VT ∪ {ε}, as follows. Definition 4.2.4. [The Set First 1 (α) for Strings] Let us consider a contextfree grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. Let V be VT ∪ VN . First 1 is a function from V ∗ to the set of all subsets of VT ∪ {ε}, that is, First 1 : V ∗ ∪ {ε} → 2VT ∪{ε} , such that for all x ∈ V and α ∈ V ∗ , (i) First 1 (ε) = {ε} (ii) First 1 (xα) = if x →∗ ε then (First 1 (x) − {ε}) ∪ First 1 (α) else First 1 (x) where First 1 (x) on the right S hand side is constructed as specified by Algorithm 4.2.2. Note that First 1 (A) = ni=1 First 1 (βi ), if A → β1 | . . . | βn are the productions for the nonterminal A. 56 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS Fact 4.2.5. For any string α ∈ V ∗ , we have that ε ∈ First 1 (α) iff α →∗ ε. Now we define the set Follow 1(A) for any nonterminal A. Follow 1 is a function from VN to the set of all subsets of VT ∪ {$}, that is, Follow 1 : VN → 2VT ∪{$} . Definition 4.2.6. [The Set Follow 1(A) for Nonterminals] Let us consider a context-free grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. Let V be VT ∪ VN . For any nonterminal A ∈ VN , Follow 1(A) is the smallest subset of VT ∪ {$} which includes every symbol occurring in a sentential form derived from S $ immediately to the right of A, that is, for all α, β ∈ V ∗ and b ∈ VT ∪ {$}, if S $ →∗ αAbβ then b ∈ Follow 1(A). We can compute the value of Follow 1(A), for every nonterminal symbol A, as indicated by the following algorithm. Algorithm 4.2.7. [Computing the Set Follow 1(A) for Nonterminals] Let us consider a context-free grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. Follow 1 is a function from VN to the set of all subsets of VT ∪ {$} such that for each nonterminal A ∈ VN , Follow 1(A) is the smallest subset of VT ∪ {$} which satisfies the following rules. Rule (i). If the nonterminal A is the axiom S then $ ∈ Follow 1(A), that is, $ ∈ Follow 1 (S). Rule (ii). If there is a production B → αAβ with α, β ∈ V ∗ and β →∗ ε then Follow 1(B) ⊆ Follow 1(A). Rule (iii). If there is a production B → αAβ with α, β ∈ V ∗ , then (First 1 (β) − {ε}) ⊆ Follow1(A). Note that for every A ∈ VN , the definition of Follow 1(A) depends, in general, on the definition of Follow 1(B) for all B ∈ VN , and the definition of First 1 (x) for all x ∈ V . Note also that the premises of Rules (ii) and (iii) are not mutually exclusive, because the premise of Rule (iii) may hold independently of the fact that β →∗ ε or β 6→∗ ε. If β = ε then Rule (iii) is superfluous because its conclusion says that ∅ ⊆ Follow 1 (A). In Algorithm 4.2.7 we have stipulated that Follow 1(x) is the smallest subset (and not any subset) of VT ∪ {$} satisfying Rules (i), (ii), and (iii). The reason for that stipulation is that, since Follow 1(x) may depend on itself (and, this happens, for instance, when in the given grammar there is a production of the form A → BA), there could be more than one subset of VT ∪ {$} satisfying Rules (i), (ii), and (iii). Rule (i) of Algorithm 4.2.7 is motivated by the fact that we have stipulated that the string given in input to the parsing automaton, is the string to be parsed followed by the symbol $. We have that Follow 1(S) = {$} if S does not occur on the right hand side of any production. In order to motivate Rule (ii) and, in particular, the inclusion Follow 1(B) ⊆ Follow 1(A), let us consider the grammar with axiom S and the following productions: S → Bb B→A A → Aa | b 4.2. LL(1) PARSERS 57 This grammar generates the language ba∗ b. (Indeed, A and B generate the language ba∗ .) For this grammar, since there is the production B → A, we have that Follow 1(B) = {b} and Follow 1(A) = {a, b}, as one can see from the derivation S → Bb → Ab → Aab → bab. Note that, as a particular instance of Rule (iii), we have that if there is a production B → αAxβ with α, β ∈ V ∗ , x ∈ V , and x 6→∗ ε then First 1 (x) ⊆ Follow 1(A). This is a consequence of Fact 4.2.5 on the facing page. The following fact is a straightforward consequence of the definition of the function Follow1. Fact 4.2.8. For any A ∈ VN , the empty string ε does not belong to Follow 1(A). Note that in the theory of LL(k) parsing we do not consider augmented grammars (see Definition 5.1.3 on page 82) and thus, in particular, we consider neither the new start symbol S ′ nor the extra production S ′ → S $. In the case of an LL(1) parser the table T is constructed as follows. Algorithm 4.2.9. Constructing the LL(1) parsing table for a given LL(1) grammar G. For each production A → α of G with A ∈ VN and α ∈ (VT ∪ VN )∗ , Rule (1): if a ∈ First 1 (α), then we place A → α in row A and column a of the table T , and Rule (2): if ε ∈ First 1 (α) (that is, α →∗ ε), then for each b ∈ Follow 1(A) we place A → α in row A and column b of the table T . Note that, in particular, Rule (2) is applied if α = ε, because in that case ε ∈ First 1 (α). One of the columns of the LL(1) parsing table is labeled by $ because we stipulate that $ ∈ Follow 1(S), where as usual, S is the axiom of the grammar. Note that given any context-free grammar G, we can construct a parsing table T by using this Algorithm 4.2.9. One can show that if an entry of the parsing table T is multiply defined (that is, more than one production must be placed in the same entry of the table T ) then the given grammar G is not an LL(1) grammar. Once the parsing table T has been constructed, it can be used by the deterministic pushdown automaton of Figure 4.1.1 on page 53 for parsing any given input string w $. We have the following fact which we state without proof. Fact 4.2.10. Let us consider the parsing automaton of Figure 4.1.1 on page 53 and let us assume that it is working using the table T . Initially, the stack of the automaton has the string S $ (with S as top symbol and $ on the bottom of the stack) and the input string is w $. (i) If that automaton should use an entry of T without any production, then the input string w does not belong to the language generated by the given grammar G. (ii) If the input head of that automaton points at the symbol $ in the input string and the top of the stack is $ (in this case it could make a chop move of $), then the input string w is accepted by the parsing automaton and w belongs to the language generated by the given grammar G. 58 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS We have the following theorem which we state without proof. Theorem 4.2.11. [LL(1) Grammars and LL(1) Languages] The LL(1) grammars are those which generate languages whose words are accepted by the deterministic pushdown automaton of Figure 4.1.1 on page 53, with the parsing table constructed as indicated by Algorithm 4.2.9 on the previous page. The following result which we state without proof, characterizes the LL(1) grammars and the strong LL(1) grammars [3, page 343]. Theorem 4.2.12. [LL(1) Grammars and Strong LL(1) Grammars] (i) A context-free grammar G is an LL(1) grammar iff for every pair of distinct productions A → α and A → β with α 6= β (recall that α or β may be ε), and for every string w A σ such that S →∗lm w A σ with w ∈ VT∗ and σ ∈ V ∗ , we have that: First 1 (α σ) ∩ First 1 (β σ) = ∅. (ii) A context-free grammar G is a strong LL(1) grammar iff for every pair of distinct productions A → α and A → β with α 6= β (recall that α or β may be ε), we have that: First 1 (α Follow 1(A)) ∩ First 1 (β Follow 1(A)) = ∅. As a consequence of this theorem we have the following fact [3, page 344]. Fact 4.2.13. [Equivalence of LL(1) Grammars and Strong LL(1) Grammars] The notions of an LL(1) grammar and a strong LL(1) grammar coincide. We also have the following fact [3, page 344]. Fact 4.2.14. [Left Recursive Grammar and LL(k ) Grammar] For all k ≥ 0, every grammar that is left recursive (see Definition 1.2.19 on page 16), is not an LL(k) grammar. An informal proof of this Fact 4.2.14 can be given as follows. Let us consider the grammar G with axiom S and productions S → S b | a. In order to parse the word a bn , for some n ≥ 0, given the initial stack configuration S $ with top symbol S, we have to perform n expand moves using the production S → S b and one expand move using the production S → a. Since the number n can only be known by having a lookahead of n input symbols and n is unbounded, we have that it does not exist any k ≥ 0 such that the grammar G is LL(k). Since we know how to construct an LL(1) parsing table we can give a formal proof of the following fact which is an obvious consequence of Fact 4.2.14. Fact 4.2.15. [Left Recursive Grammar and LL(k ) Grammar] Consider any context-free grammar G (recall that we allow productions of the form B → ε, for any B ∈ VN ) without useless symbols such that: (1) there exists a production of the form A → A α with α ∈ (VT ∪ VN )+ , and (2) for all A ∈ VN , it is not the case that A →+ A. Then the grammar G is not LL(1). Proof. By hypothesis we have a production of the form A → A α such that: (i) α ∈ (VT ∪ VN )+ , (ii) α 6→∗ ε, and (iii) there exist w ∈ VT+ and α →∗ w. Since A is not useless, there should be at least one more production for A of the form A → β with β ∈ (VT ∪ VN )∗ . Since for A there are at least the two productions A → A α 4.2. LL(1) PARSERS 59 and A → β, we have that First 1 (β) ⊆ First 1 (A α). (In general, it is not the case that First 1 (β) = First 1 (A α) because, besides A → A α and A → β, there could be other productions for A.) Now there are two cases: (i) β 6→∗ ε, and (ii) β →∗ ε. Case (i): β 6→∗ ε. In this case for all x ∈ First 1 (β), we have that x ∈ VT . Since First 1 (β) ⊆ First 1 (A α), we have the following portion of the LL(1) parsing table for any x ∈ First 1 (β): ... x ... A → Aα A A→β Case (ii): β →∗ ε. In this case A → A α → β α →∗ α. Thus, First 1 (α) ⊆ First 1 (A α). Since α 6→∗ ε, there exists y ∈ First 1 (α)∩VT such that we have to place the production A → A α in the LL(1) parsing table at row A and column y. Also the production A → β should be placed in that position because β →∗ ε and y ∈ Follow 1 (A) (indeed, by the production A → A α we have that First 1 (α) ⊆ Follow 1 (A)). Thus, we have the following portion of the LL(1) parsing table: ... y ... A → Aα A A→β Hence, having examined the two cases: (i) β 6→∗ ε and (ii) β →∗ ε, we conclude that the given grammar G is not LL(1).  Example 4.2.16. [An LL(1) Grammar and Its LL(1) Parsing Table] Let us consider the grammar G with axiom S and whose productions are: S → aAb | b A→a | bSA We have that: First 1 (aAb) = {a}, First 1 (b) = {b}, First 1 (a) = {a}, First 1 (bSA) = {b}. The parsing table is: a b S S → aAb S → b A A→a $ A → bSA In Figure 4.2.2 on the next page we have depicted the sequence of the input string and stack configurations while parsing the string a b b a b $. (That sequence from configuration (1) to configuration (10) is divided into three subsequences, each of them to be read from left to right.) The black triangle K indicates the symbols at hand in the input string and the top of the stack (as usual in our pictures of this section, in every stack configuration the top of the stack is the leftmost symbol). 60 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS input string: abbab$ K (1) stack: abbab$ K expand S −→ (2) S$ K abbab$ K chop a −→ (3) abbab$ K expand A −→ (4) aAb$ K Ab$ K bS Ab$ K abbab$ K abbab$ K abbab$ K chop b −→ (5) expand S −→ (6) chop b −→ (7) S Ab$ K bAb$ K Ab$ K abbab$ K abbab$ K abbab$ K expand A −→ (8) ab$ K chop a −→ (9) b$ K chop b −→ (10) $ K Figure 4.2.2. LL(1) parsing of the string a b b a b $. The given grammar has axiom S and the following productions: S → a A b | b, A → a | b S A. The black triangle N indicates the symbol at hand in the input string and the top of the stack (which is the leftmost symbol of the stack). In the last configuration of Figure 4.2.2 we have that both the top of the stack and the input head are pointing at the symbol $. Thus, the given string a b b a b belongs to the language generated by the grammar G.  Example 4.2.17. [LL(1) Parsing of Arithmetic Expressions] Let us consider the context-free grammar G with axiom E and the following productions: E →E+T | T T → T ×F | F F → (E) |a This grammar is left recursive and thus, for all k ≥ 0 it is not LL(k) (see Fact 4.2.14 on page 58). If we want to use an LL(k) parsing algorithm we have to look for an equivalent grammar which is not left recursive. One such context-free grammar is e (with ε-productions): the following grammar G 4.2. LL(1) PARSERS e E→TE T → F Te F → (E) | a 61 e →ε | +T E e E Te → ε | × F Te e can be obtained from the grammar G by avoiding the left recursion The grammar G e from G is based on and using ε-productions. (The derivation of the grammar G the fact that, for instance, the language a b∗ is generated from the axiom S by the e is an LL(1) grammar as productions S → a Z and Z → ε | b Z.) The grammar G shown by the LL(1) parsing table depicted in Figure 4.2.3 on the next page. In order to construct that parsing table, we have first to compute the First1 sets and the Follow1 sets. They are: First 1 (ε) = {ε} e = {(, a} First 1 (T E) First 1 (F Te) = {(, a} First 1 ((E)) = {(} Follow 1(E) = {), $} Follow 1(T ) = {+, ), $} e = {+} First 1 (+T E) First 1 (×F Te) = {×} First 1 (a) = {a} e = {), $} Follow 1(E) Follow 1(Te) = {+, ), $} e = {ε, +} First 1 (E) First 1 (Te) = {ε, ×} First 1 (F ) = {(, a} Follow 1(F ) = {×, +, ), $} (Recall that for constructing the LL(1) parsing table we need to compute the Follow 1 e = {), $} set for every nullable symbol.) In particular, the fact that F ollow1 (E) and F ollow1 (Te) = {+, ), $} is shown by following derivations (see the underlined sentential forms, of which the first two show the presence of $ in these Follow 1 sets): e → T → F Te → F → (E) → (T E) e → (T ) → (F Te) →∗ (a) E→TE ↓ e → (F Te + T E) e →∗ (a + a) (T + T E) Figure 4.2.4 on the following page shows the sequence of input string and stack configurations while parsing the arithmetic expression a (thus, the input string is a $) e according to the grammar G.  With reference to Example 4.2.17 above, note that the following grammar H, which is not left recursive and has no ε-productions, is not LL(1): e E→T |TE T → F | F Te F → (E) | a e → +T | + T E e E Te → ×F | × F Te This grammar H can be obtained from the given grammar G by eliminating the left recursion without using ε-productions, and thus grammar H is equivalent to e (The derivation of the grammar H from the grammar G is grammars G and G. based on the fact that, for instance, the language a b∗ is generated from the axiom S by the productions S → a | a Z and Z → b | b Z.) 62 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS a E e E T Te F ( e E→TE e E→TE T → F Te T → F Te F →a F → (E) ) × + $ e→ε E e → +T E e E Te → ε Te → ε e→ε E Te → ×F Te Te → ε Figure 4.2.3. LL(1) parsing table for the grammar with axiom E and e E e → ε | +T E, e T → F Te, Te → ε | ×F Te, productions: E → T E, F → (E) | a. input string: a$ K (1) stack: a$ K expand E −→ (2) E$ K a$ K expand T −→ (3) a$ K expand F −→ (4) e$ TE K e$ F TeE K e$ aTeE K a$ K a$ K a$ K chop a −→ (5) e$ TeE K expand Te −→ (6) e$ E K e expand E −→ (7) $ K Figure 4.2.4. LL(1) parsing of the string a $ according to the grame with axiom E and productions: E → T E, e e → ε | +T E, e mar G E T → F Te, Te → ε | ×F Te, F → (E) | a. The black triangle N indicates the symbol at hand in the input string and the top of the stack (which is the leftmost symbol of the stack). To show that grammar H is not LL(1), let us consider the following grammar which is a simplified version of H. This grammar has axiom T and the productions: T → F | F Te Te → ×F | × F Te F → (T ) | a 4.3. LL(k ) PARSERS (FOR k ≥ 1) We have that: First 1 (F ) = First1 (F Te) = {(, a} First 1 ((T )) = {(} 63 First 1 (×F ) = First 1 (×F Te) = {×} First 1 (a) = {a} We get the following parsing table (there is no need to compute the Follow 1 sets because ε does not occur in any of the First 1 sets, that is, no symbol is nullable): T Te F a ( T →F T → F Te T →F T → F Te F →a F → (T ) × ) $ Te → ×F Te → ×F Te Grammar H is not LL(1) because in this table there exists an entry which has more than one production (actually, there are three such entries).  Example 4.2.18. [A Grammar Which is Not LL(1)] Let us consider the grammar G whose axiom is S and whose productions are: S→ε | abA A → S aa | b We have that: First 1 (ε) = {ε} First 1 (S a a) = {a} Follow 1(S) = {$, a} First 1 (a bA) = {a} First 1 (b) = {b} First 1 (S) = {ε, a} Follow 1(A) = {$, a} The parsing table is: a S b S → a bA $ S→ε S→ε A A → S aa A→b A → S aa The given grammar is not LL(1) because in this parsing table for the symbol S on the top of the stack and the input symbol a, there are two productions.  4.3. LL(k ) Parsers (for k ≥ 1) In this section we consider the general case of LL(k) parsers and LL(k) grammars, with k ≥ 1. We first need the following definition which is a generalization of Definition 4.2.1 on page 54. 64 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS Definition 4.3.1. [The Set First k (α)] Let us consider a context-free grammar, possibly with ε-productions. For k ≥ 0, First k is a function from V ∗ to the set of all subsets of VT0 ∪ VT1 ∪ VT2 ∪ . . . ∪ VTk , that is, {ε} ∪ VT ∪ VT2 ∪ . . . ∪ VTk . Given a string α ∈ V ∗ , we have that: Firstk (α) =def {w | (α →∗ w β and w ∈ VTk and β ∈ VT∗ ) or (α →∗ w and w ∈ VTi for some 0 ≤ i < k)}. For any α ∈ V ∗ and β ∈ VT∗ , we have that: (i) if α →∗ β then F irst0 (α) = {ε}, and (ii) for any k ≥ 1, ε ∈ F irstk (α) iff α →∗ ε. We will also need the following binary operation on languages. Definition 4.3.2. [k -bounded Concatenation] Given two languages L1 and L2 , their k-bounded concatenation, denoted ⊙k , for any k ≥ 1, is defined as follows: L1 ⊙k L2 = {w | (|w| ≤ k and w ∈ L1  L2 ) or (|w| = k and ∃z, wz ∈ L1  L2 )}. where the concatenation operation ‘  ’ on languages is defined as usual (see Chapter 1). For instance, given the languages L1 = {ε, abb} and L2 = {b, bab}, we have that: L1  L2 = {b, bab, abbb, abbbab} and L1 ⊙2 L2 = {b, ba, ab}. Note that in L1 ⊙k L2 with k ≥ 1, there may be words whose length is less than k. Now we will describe how the LL(k) parsing is performed, for any k ≥ 1. Supposed we are given a string v to be parsed and an LL(k) grammar G. Suppose also that we have parsed a proper prefix p of the given string v, that is, v = p u for some u ∈ VT+ and the input head is pointing at the leftmost symbol of u. We assume that the sentential form we have generated so far is: p A z, for some z ∈ V ∗ , that is, all symbols in p have been chopped and the stack is holding A z $ with the top of the stack pointing at A. In order to determine the production for expanding A, the parser uses a parsing table which is constructed as we will indicate, after the construction of some other tables, each of which is parameterized by: (i) a nonterminal symbol, and (ii) a language subset of VT∗ $0,1 . A table named T and whose parameters are the nonterminal B and the language L, will be denoted by T [B, L]. The table T [B, L] can be represented as a matrix whose rows have three components (or columns): (i) the first one is a word w in VT∗ $ 0,1 , (ii) the second one is a production for B, and (iii) the third one is a list of sets of words which depends on: (iii.1) the production for B which is the second component, and (iii.2) the language L. Each set in this list of sets of words is called a local follow sets for B. The rows of table T [B, L] are constructed by considering the productions for B. For each one of these productions, say B → α, we first compute the set of words: First k (α) ⊙k L and we then construct a row of the table T [B, L] of the form: 4.3. LL(k ) PARSERS (FOR k ≥ 1) w 65 B→α M for each word w in First k (α) ⊙k L. In each of these rows M is a list of set of words such that: (i) if α ∈ VT∗ then M = [ ], and (ii) if α is of the form: x0 B1 x1 . . . Bm xm , with m ≥ 0, and for h = 0, . . . , m, xh ∈ VT∗ , and for i = 1, . . . , m, Bi ∈ VN , then M = [Y1 , . . . , Ym], where for i = 1, . . . , m, Yi =First k (xi Bi+1 xi+1 . . . Bm xm ) ⊙k L. The first table to be constructed is T [S, {$}]. When constructing the various tables, we maintain a set of tables to be constructed. Thus, initially, that set of tables to be constructed has exactly one element which is the table T [S, {$}]. At the end of the construction of a table, say T [B, L], we update that set of tables to be constructed as follows. For each row of the table T [B, L] of the form: w B → x0 B1 x1 . . . Bm xm [Y1 , . . . , Ym ] where: (i) w ∈ VT∗ , (ii) m ≥ 1, (iii) for h = 0, . . . , m, xh ∈ VT∗ , and (iv) for i = 1, . . . , m, Bi ∈ VN , we add to the set of tables to be constructed the m tables T [Bi , Yi ], for i = 1, . . . , m. The process of constructing tables terminates when we have constructed all the tables which occur in the set of tables to be constructed. At that point we can construct the parsing table for the LL(k) parsing for k ≥ 1. In that parsing table the rows are indexed by the parameters of the tables we have constructed (thus, the rows are as many as those tables), and the columns are indexed by the words in V 0 $ ∪ V 1 $ ∪ . . . ∪ V k−1 $ ∪ V k (recall that V 0 $ is equal to {$}). The entry of the parsing table in row [Bi , Yi ] and column w is the second component of the row of table T [Bi , Yi ] whose first component is w. Thus, (i) the index [Bi , Yi ] of each row of the parsing table consists of the symbol Bi on the top of the stack and the string Yi which is equal to First k (σi ), where σi is the string of the symbols below the top of the stack, and (ii) the index w of each column of the parsing table is the string of at most k (≥ 1) symbols to the right of the input head (which is pointing at the leftmost symbol of w). Now we will give an example of the construction of the tables in the case of an LL(2) context-free grammar G. This example will clarify the rules we have given above for the general case of the LL(k) parsing, for any k ≥ 1. Example 4.3.3. Let us consider the following grammar G with axiom S and whose productions are: S → aAaa | bAba A→b |ε The construction of the LL(2) parsing table will show that this grammar is an LL(2) grammar. 66 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS Construction of the initial table T [S, {$}]. For S we have the two productions: S → aAaa and S → bAba. For each of them, say S → α, we have compute the set of words: First 2 (α) ⊙2 {$}. Thus, we have: First 2 (aAaa) ⊙2 {$} = {ab, aa} ⊙2 {$} = {ab, aa} First 2 (bAba) ⊙2 {$} = {bb} ⊙2 {$} = {bb} From these values we get the three rows of the table T [S, {$}]. We have that: T [S, {$}]: ab S → aAaa L1 aa S → aAaa L2 S → bAba L3 bb where the lists L1, L2, and L3 of set of words are defined as follows: L1 = [First 2 (aa) ⊙2 {$}] = [{aa}] (Note that aa is the word in VT∗ which follows A in the right hand side of the production S → aAaa, and there is only the nonterminal symbol A in aAaa). L2 = [First 2 (aa) ⊙2 {$}] = [{aa}] L3 = [First 2 (ba) ⊙2 {$}] = [{ba}] (Note that ba is the word in VT∗ which follows A in the right hand side of the production S → bAba and there is only the nonterminal symbol A in bAba.) Thus, we get the following table T [S, {$}]: T [S, {$}]: ab S → aAaa [{aa}] aa S → aAaa [{aa}] S → bAba [{ba}] bb Having constructed this table, we insert in the set of tables to be constructed the following two tables: T [A, {aa}] and T [A, {ba}]. Note that either the first row or the second row of table T [S, {$}] forces us to insert table T [A, {aa}] in the set of tables to be constructed. Construction of the table T [A, {aa}]. For A we have the two productions: A → b and A → ε. Thus, we have that: First 2 (b) ⊙2 {aa} = {ba} First 2 (ε) ⊙2 {aa} = {aa} From these values we get the two rows of the table T [A, {aa}]. We have that: 4.3. LL(k ) PARSERS (FOR k ≥ 1) T [A, {aa}]: ba A → b M1 aa A → ε M2 67 where the lists M1 and M2 of sets of words are both empty because in the right hand sides of the productions A → b and A → ε there are no nonterminal symbols. Thus, we get the following table T [A, {aa}]: T [A, {aa}]: ba A → b [] aa A → ε [] After the construction of this table we do not add any new table to the set of tables to be constructed because in the right hand sides of the productions A → b and A → ε there are no nonterminal symbols. Construction of the table T [A, {ba}]. For A we have the two productions: A → b and A → ε. Thus, we have that: First 2 (b) ⊙2 {ba} = {bb} First 2 (ε) ⊙2 {ba} = {ba} From these values we get the two rows of the table T [A, {ba}]. We have that: T [A, {ba}]: bb A → b N1 ba A → ε N2 where the lists N1 and N2 of sets of words are both empty because in the right hand sides of the productions A → b and A → ε there are no nonterminal symbols. Thus, we get the following table T [A, {ba}]: T [A, {ba}]: bb A → b [] ba A → ε [] After the construction of this table we do not add any new table to the set of tables to be constructed because in the right hand sides of the productions A → b and A → ε there are no nonterminal symbols. Construction of the parsing table. Having constructed the three tables T [S, {$}], T [A, {aa}], and T [A, {ba}], we can construct the LL(2) parsing table. We have depicted it in Figure 4.3.1 on the following page. In Figure 4.3.2 on the next page we have shown the sequence of the input string and stack configurations when parsing the string b b a $. The symbol K indicates the symbol at hand in the input string (that is, the position of the input head) and the top of the stack (as usual in this section, in every stack configuration the top of the stack is the leftmost symbol). 68 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS aa ab [S, {$}] S → aAaa S → aAaa [A, {aa}] A→ε ba bb $ a$ b$ S → bAba A→b [A, {ba}] A→ε A→b Figure 4.3.1. The LL(2) parsing table for the grammar with axiom S and productions: S → aAaa | bAba, A → b | ε. input string: bba$ K (1) stack: bba$ K expand S −→ (2) S$ K bba$ K chop b −→ (3) bAba$ K Aba$ K bba$ K bba$ K chop b −→ (5) a$ K bba$ K expand A −→ (4) ba$ K chop a −→ (6) $ K Figure 4.3.2. LL(2) parsing of the string b b a $. The given grammar with axiom S has the following productions: S → aAaa | bAba, A → b | ε. The symbol K indicates the symbol at hand in the input string and the top of the stack (which is the leftmost symbol). Note that the expansion of S (see configurations (1) and (2) of Figure 4.3.2) is done using the production S → bAba (see the entry at row [S, {$}] and column b b of the parsing table depicted in Figure 4.3.1) because: (i) below S on the stack there is $ (and [S, {$}] identifies the row), and (ii) the symbols in the position of the input head and at its right are b b (and b b identifies the column). Analogously, the expansion of A is done using the production A → ε (see the entry at row [A, {ba}] and column b a of the parsing table depicted in Figure 4.3.1) because: (i) below A on the stack there are the symbols b a (and [A, {ba}] identifies the row), and (ii) the symbols in the position of the input head and at its right are b a (and b a identifies the column). 4.3. LL(k ) PARSERS (FOR k ≥ 1) 69 In the final configuration (6) of Figure 4.3.2 on the preceding page we have that both the input head and the top of the stack are pointing at the symbol $. Thus, the given string b b a belongs to the language generated by the grammar G.  We have the following property of the LL(k) parsing, for any k ≥ 1. LL(k) Parsing, for k ≥ 1. The production which has to be applied by the Chop-and-Expand parser for expanding the nonterminal A is uniquely determined by (see also Figure 4.3.3): (i) A itself, (ii) the leftmost k symbols of z (or z itself if |z| < k), and (iii) the leftmost k symbols of u (or u itself if |u| < k). (The reader may want to look also at what we will state on page 70 with reference to strong LL(k) grammars.) S sentential form pAz : p input string pu$ : p leftmost derivation z A Follow k (A) : first k symbols of z β u $ u k : first k symbols of u Figure 4.3.3. LL(k) parsing, for k ≥ 1. The derivation from S to p A z is a leftmost derivation. Follow k (A) depends on the given grammar only. Now in order to state Theorem 4.3.5 on the next page, we define for any nonterminal A and for any k ≥ 1, the set Follow k (A). Definition 4.3.4. [The Set Follow k (A)] Let us consider a context-free grammar hVT , VN , P, Si, possibly with ε-productions, without useless symbols. For any k ≥ 1, Follow k is a function from VN to the set of all subsets of V ∗ $ 0,1 . For any A ∈ VN , Follow k (A) is the smallest set such that for any string w ∈ (V 0 $ ∪ V 1 $ ∪ V 2 $ ∪ . . . ∪ V k−1 $ ∪ V k ), if S $ →∗ αAβ for some α ∈ V ∗ and β ∈ V ∗ $ and w ∈ First k (β) then w ∈ Follow k (A). Thus, by definition, for every nonterminal A ∈ VN , Follow k (A) is a set of words w in V 0 $ ∪ V 1 $ ∪ V 2 $ ∪ . . . ∪ V k−1$ ∪ V k which occur in a sentential form derived from S $ immediately to the right of A. 70 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS We have that for any k ≥ 1 and A ∈ VN , the empty string ε is not an element of Follow k (A). Note that the above Definition 4.3.4 does not provide an algorithm that, for any k ≥ 1, given the productions of an LL(k) grammar, constructs the set Follow k (A) for any nonterminal A. However, for k = 1, we did provide an algorithm for constructing the set Follow 1(A) (see Algorithm 4.2.7 on page 56). The following result which we state without proof, characterizes the LL(k) grammars and the strong LL(k) grammars for any k ≥ 1 [3, pages 342–344]. Theorem 4.3.5. [LL(k ) Grammars and Strong LL(k ) Grammars] (i) A context-free grammar G is an LL(k) grammar iff for every pair of distinct productions A → α and A → β with α 6= β, and for every string w A σ such that S →∗lm w A σ, we have that: First k (α σ) ∩ First k (β σ) = ∅. (ii) A context-free grammar G is a strong LL(k) grammar iff for every pair of distinct productions A → α and A → β with α 6= β, we have that: First k (α Follow k (A)) ∩ First k (β Follow k (A)) = ∅. Example 4.3.6. The grammar G with axiom S and the following productions: S → aAaa | bAba A→b |ε is an LL(2) grammar as the above Example 4.3.3 shows, but it is not a strong LL(2) grammar. Indeed, we have that: Follow2(A)) = {aa, ba} and First2 (b Follow2(A)) ∩ First2 (ε Follow2(A)) = {ba, bb} ∩ {aa, ba} = {ba} = 6 ∅. We leave it to the reader to check the following facts which we state without proofs. Fact 4.3.7. The rules we have given above for constructing the parsing table for LL(1) parsing, can be obtained by instantiating for k = 1 the rules for LL(k) parsing for k ≥ 1. Fact 4.3.8. [LL(k ) Languages Can Be Generated by Strong LL(k ) Grammars] For any k ≥ 0, if a language L can be generated by an LL(k) grammar, then L can be generated by a strong LL(k) grammar. In view of this fact, if we consider the strong LL(k) grammar which generates a given LL(k) language L, then the language L can parsed by using a Chop-and-Expand parser whose expansion steps satisfy the following property. 4.3. LL(k ) PARSERS (FOR k ≥ 1) 71 Strong LL(k) Parsing, for k ≥ 1. The production which has to be applied by the Chop-and-Expand parser for expanding the nonterminal A is uniquely determined by: (i) A itself, and (ii) the first k symbols of the input string which are to the right of the last symbol which has been chopped (or the whole string to the right of that symbol, if there are less than k symbols to the the right of it). The reader may want to look also at what we stated on page 69, where the Chop-andExpand LL(k) parsing process has been described with respect to LL(k) grammars, rather than strong LL(k) grammars. Now we recall the notions of the Chomsky normal form and Greibach normal form for extended context-free grammars, that is, context-free grammars where every nonterminal symbol may have an ε-production (see, for instance, [21]). Definition 4.3.9. [Chomsky Normal Form. Version with Epsilon Productions] An extended context-free grammar G = hVT , VN , P, Si is said to be in Chomsky normal form if its productions are of the form: A → BC A→a for A, B, C ∈ VN or for A ∈ VN and a ∈ VT , and if ε ∈ L(G), then (i) the set of productions of G includes also the production S → ε, and (ii) S does not occur on the right hand side of any production [2]. Definition 4.3.10. [Greibach Normal Form. Version with Epsilon Productions] An extended context-free grammar G = hVT , VN , P, Si is said to be in Greibach normal form if its productions are of the form: A → aα for A ∈ VN , a ∈ VT , α ∈ VN∗ , and if ε ∈ L(G), then the set of productions of G includes also the production S → ε. Fact 4.3.11. [From LL(k ) Grammars to LL(k +1) Grammars in Greibach Normal Form] [3, page 362] Let L be a language that can be generated by an LL(k) grammar for some k ≥ 0. Then: (i) L−{ε} can be generated by an LL(k+1) grammar in Greibach normal form without ε-productions, and (ii) L can be generated by an LL(k+1) grammar G in Greibach normal form without ε-productions and we have to add to that grammar the production S → ε iff ε ∈ L [22, Theorems 3, Theorem 4, and Corollary 3]. In the following two examples the reader may see in action the techniques for producing from an LL(1) grammar an equivalent LL(2) grammar in Greibach normal form. These techniques constitute the basis for the proof of the above Fact 4.3.11. Without loss of generality, we assume that the given LL(1) grammar has no unit productions, that is, productions of the form A → B, for some A, B ∈ VN [21]. 72 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS Example 4.3.12. [From LL(1) Grammars to LL(2) Grammars in Greibach Normal Form: Example 1] Let us consider the LL(1) grammar G with axiom S and the following productions: S → AB A → aA | ε B → bA | ε This grammar G is, indeed, an LL(1) grammar, as the reader may verify, because the LL(1) parsing table is as follows: a b $ S S → AB S → AB S → AB A A → aA B A→ε A→ε B → bA B→ε This table is constructed after computing the following sets: First 1 (S) = {a, b, ε}, First 1 (A) = {a, ε}, First 1 (B) = {b, ε}, First 1 (AB) = {a, b, ε}, Follow 1(S) = {$}, Follow 1(A) = {b, $}, and Follow 1(B) = {$}. We want to derive a grammar in Greibach normal form which is equivalent to G. In that equivalent grammar we allow also the production S → ε if ε ∈ L(G). In our case, indeed, we have the production S → ε because ε ∈ L(G). We start from the axiom S and we perform leftmost unfolding steps (that is, we unfold the leftmost nonterminals of the sentential forms), thereby producing new sentential forms from old ones. We stop these unfolding steps when we get: either (H1) ε, or (H2) a terminal symbol a ∈ VT , or (H3) a string of the form aσ1 . . . σn , for some n ≥ 1, where a ∈ VT , and for i = 1, . . . , n, we have that σi ∈ VT VN∗ ∪ {S}. One can show that by doing so, we always construct a finite tree of sentential forms (see Figure 4.3.4 on the next page for the case of our grammar G). Every leaf of that tree allows us to generate, as we will indicate, a production of a grammar in Greibach normal form equivalent to G. Note that, since we perform only leftmost unfolding steps, the decomposition of the sentential form into a sequence of the form aσ1 . . . σn satisfying the Condition (H3) above, may not be unique. For instance, abAS can be decomposed either into the two substrings: a and bAS, or into the three substrings: a, bA, and S. 4.3. LL(k ) PARSERS (FOR k ≥ 1) e → aAB A S B aB a bA bA a b aA a bA e → aA C bA a aAB aB aAB a aAB e → bA B aAB AB 73 b aA b aA a aA a a ε b Figure 4.3.4. Trees of sentential forms obtained by unfolding the leftmost nonterminals. The given grammar has axiom S and productions: S → AB, A → aA | ε, B → bA | ε. The nullable symbols are S, A, and B. We have indicated within boxes the productions which are generated by the roots of the trees of sentential forms whose root is not the axiom S. Then we apply as long as possible the following Tree Closure Rule. Tree Closure Rule. For each leaf where Condition (H3) holds, for i = 1, . . . , n, with n ≥ 1, and for every σi which is not a terminal symbol and is different from S, we construct a new tree of sentential forms whose root is σi and whose nodes are obtained from the root by performing leftmost unfolding steps until at each leaf one of the above two Conditions (H2) or (H3) becomes true. (Note that Condition (H1) can never become true because σi ∈ VT VN∗ .) The leaves of these new trees we will construct, may determine the construction of more trees of sentential forms, because we have to apply the above Tree Closure Rule until all leaves of all trees of sentential forms are either: (i) ε, or (ii) a terminal symbol, or (iii) a string of the form aσ1 . . . σn , for some n ≥ 1, where a ∈ VT , and for i = 1, . . . , n, each substring σi is the label of the root of a tree. Having terminated the constructions of all these trees, we consider for each tree with root σ different from S, a new nonterminal Aσ and we introduce a production of the form Aσ → σ. We also introduce a production of the form Aa → a, where Aa is a new nonterminal symbol, for each leaf of a tree which is made out of the terminal symbol a only. 74 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS We then perform all possible folding steps, both at the roots and at the leaves of the trees we have constructed, by using the productions of the form Aσ → σ or Aa → a we have introduced. In particular, a leaf with the sentential form aσ1 . . . σn , with n ≥ 0, is replaced by the leaf with the sentential form aAσ1 . . . Aσn . Finally, we get the desired productions of the grammar in Greibach normal form by considering for every root-to-leaf path in every tree, a production of the form A → γ, where A is the label of the root and γ is the label of the leaf. In the case of our grammar G above, starting from the trees of sentential forms depicted in Figure 4.3.4 on the preceding page, we first introduce the following three productions (see the roots, different from S, of the trees of Figure 4.3.4): e → aAB A e → bA B e → aA C Then, by using these productions, we perform the folding steps at the leaves and at the roots of those trees (see the underlined substrings in Figure 4.3.4 on the previous page). Finally, from the root-to-leaf paths of those trees, after the folding steps, we get the following productions in Greibach normal form (see also Figure 4.3.5): e S → aA e → aA e A e → bCe B e → aC e C | | | | e | a | bC e |b |ε aB e |a aB b a e A S e aA e aA ε e aB a bCe e aB e B a bCe e C b aCe a b Figure 4.3.5. Root-to-leaf paths of the trees of Figure 4.3.4 on the previous page after performing the folding steps. Note that no unit productions are generated. We leave it to the reader to check that these productions belong to an LL(2) grammar.  The following example is a bit more complex than the above Example 4.3.12 on page 72, in the sense that the sentential forms of some of the leaves will be viewed as concatenations of the form aσ1 . . . σn with n > 1, while in Example 4.3.12 we always had n = 1. 4.3. LL(k ) PARSERS (FOR k ≥ 1) 75 Example 4.3.13. [From LL(1) Grammars to LL(2) Grammars in Greibach Normal Form: Example 2] Let us consider the LL(1) grammar G1 with axiom T and the following productions: T → F T′ T′ → ε | × F T′ F → (T ) | a (G1 ) This grammar G1 is an LL(1) grammar which can be derived from the following left recursive grammar G2 (which is not an LL(1) grammar) with productions: T →F | T ×F F → (T ) | a (G2 ) Starting from the axiom T of the grammar G1 , by performing some unfolding steps by using the productions of the grammar G1 itself, according to the rules described in the above Example 4.3.12 on page 72, we get the three trees of sentential forms depicted in Figure 4.3.6. T F T′ (T )T′ ) R → )T′ B → ×F T ′ A → aT′ )T′ ×F T ′ aT′ ) ×F T ′ ×( T ) T ′ ×aT′ a a×F T ′ aT′ a a ×F T ′ Figure 4.3.6. Trees of sentential forms obtained by unfolding the leftmost nonterminals. The given grammar has axiom T and productions: T → F T ′, T ′ → ε | ×F T ′ , F → (T ) | a. The only nullable symbol is T ′ . We have indicated within boxes the productions which are generated by the roots of trees of sentential forms whose root is not the axiom T . Note that: (i) the string ×(T )T ′ has been viewed as the concatenation ×σ1 σ2 σ3 where σ1 = (, σ2 = T , and σ3 = )T ′ , and (ii) the string ×aT ′ has been viewed as the concatenation ×σ1 where σ1 = aT ′ . Then we introduce the four productions: R →)T′ B → ×F T ′ P →( A → aT′ 76 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS In particular, (i) the production P → ( comes from σ1 = ( in ×(T )T ′ = ×σ1 σ2 σ3 , and (ii) the production A → aT ′ comes from σ1 = aT ′ in ×aT ′ = ×σ1 . Then if we perform the folding steps by using these productions, we get from the root-to-leaf paths of the trees of Figure 4.3.6 on the previous page, the grammar G3 which has the following productions in Greibach normal form: T → (T R |a | aB R→) | )B (G3 ) B → ×P T R | ×A P →( A→a | aB No unit productions are generated, and since the axiom T is not nullable, we do not have the production T → ε. We leave it to the reader to check that the grammar G3 is an LL(2) grammar (see, in particular, the productions for B).  Fact 4.3.14. [Grammars in Greibach Normal Form and LL(1) Grammars and Languages] If a grammar G is in Greibach normal form and for each nonterminal A and terminal a, there exists at most one production of the form A → a α for some α ∈ VN∗ and the only ε-production allowed is S → ε, then G is LL(1). Recall that in any LL(1) grammar we allow ε-productions for each nonterminal symbol, and thus the class of such grammars properly includes the class of the grammars in Greibach normal form mentioned in the above Fact 4.3.14. Fact 4.3.15. [Grammars in Chomsky Normal Form and LL(k ) Grammars and Languages] [4, page 689] For any k ≥ 0, every LL(k) language has an LL(k+1) grammar in Chomsky normal form. As already mentioned, we have the following result. Fact 4.3.16. [LL(0) Languages] A language L ∈ LL(0) iff L = ∅ or L is a singleton. If one assume, as we do, that there are no useless symbols in the LL(k) grammars, for any k ≥ 0 (see the hypotheses listed on page 51), then L is a singleton. Theorem 4.3.17. [Hierarchy of LL(k ) Grammars and Languages] For every k ≥ 0, (i) the class of the LL(k) grammars is properly contained in the class of the LL(k + 1) grammars, and (ii) the class of the LL(k) languages is properly contained in the class of the LL(k + 1) languages. The proof of this Theorem 4.3.17 is obvious for k = 0. The proof for all k ≥ 1 is based on the fact that the language: Lk = {an w | n ≥ 1 and w ∈ {b, c, bk d}n } (Lk ) is an LL(k) language and it is not an LL(k −1) language [4, page 686]. The LL(k) grammar Gk which generates Lk , has axiom S and the following productions: S → aT T → S A |A A → bB |c B → bk−1 d | ε (Gk ) Moreover, every LL(k) grammar which generates Lk should have an epsilon production, that is, a production whose right hand side is the empty word ε. [4, page 687]. 4.3. LL(k ) PARSERS (FOR k ≥ 1) 77 Fact 4.3.18. [LL(k ) Grammars and Epsilon Productions] For all k ≥ 1, if a language is generated by an LL(k +1) grammar without epsilon productions then it is generated by an LL(k) grammar (possibly with epsilon productions) [4, page 688]. The grammar Gk+1 with axiom S and the following productions (none of which is an epsilon production): S → aS A |aA A → bk−1 d | b | c (Gk+1 ) is an LL(k+1) grammar which generates the LL(k) language Lk (see above on page 76). Recall that the language Lk is generated by the LL(k) grammar Gk , and thus it is an LL(k) language. Fact 4.3.19. [LL(k ) Languages Contained in LR(1) Languages] For every k ≥ 0, the class of the LL(k) languages is properly contained in the class of the LR(1) languages [22] (see also Section 5.8 on page 138). Since, as we will state in the following Section 5.1, the class of the LR(1) languages coincides with the class of the deterministic context-free languages, we have the following consequence of Fact 4.3.19: every language which is generated by an LL(k) grammar, for some k ≥ 0, is a deterministic context-free language, and there exists a proper subclass C of the deterministic context-free languages such that for every language L in C there is no k ≥ 0 such that L can be generated by an LL(k) grammar. Fact 4.3.20. [LL(k ) Languages Contained in LR(1) Languages: an Example] The language L = {an bn | n ≥ 1} ∪ {an cn | n ≥ 1} is a deterministic context-free language (and thus an LR(1) language) and there is no k ≥ 0 such that L is an LL(k) language [4, page 689]. One can show that the language L = {an bn | n ≥ 1} ∪ {an cn | n ≥ 1} is actually an LR(0) language (and thus, it is an LR(1) language) by using the techniques we will present in the following Chapter 5. The following fact is an immediate consequence of the Kleene Theorem (see, for instance, [21, Section 2.5]). Fact 4.3.21. [Regular Languages and LL(1) Grammars] Every regular language has an LL(1) grammar. Fact 4.3.22. [Avoiding the Occurrence of the Axiom in the Right Hand Sides of the Productions of LL(k ) Grammars] For any k ≥ 1, for any LL(k) grammar G = hVT , VN , P, Si, the grammar G′ = hVT , VN , P ∪ {S ′ → S}, S ′i, where S ′ is a new nonterminal symbol, is an LL(k) grammar such that: (i) L(G′ ) = L(G), and (ii) the axiom S ′ does not occur in the right hand side of any production. Note that if we unfold S in the production S ′ → S of G′ , we also get a grammar which is an LL(k) grammar such that Conditions (i) and (ii) hold. Exercise 4.3.23. (i) Show that the grammar G1 with axiom S and the productions: S → aA A → ε | aS 78 4. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LL(k ) PARSERS is LL(1). (ii) Show that the grammar G2 with axiom S and the productions: S → aA | a A → aS is not LL(1) and it is LL(2). Note that both grammars G1 and G2 generate the regular language a (a a)∗ . Solution. (i) Here is the LL(1) parsing table for the grammar G1 : a $ S S → aA A A → aS A→ε Note that Follow 1 (S) = Follow 1 (A) = {$}. (ii) Here is the LL(1) parsing table for the grammar G2 : a S S→a S → aA A A → aS $ Since there are two productions for the symbol S on the top of the stack and the input symbol a, the grammar G2 is not LL(1). Here is the LL(2) parsing table for the grammar G2 : aa a$ [S, {$}] S → aA S → a [A, {$}] A → aS $  We have the following decidability result which is a consequence of [23]. Fact 4.3.24. [Decidability of Equivalence for LL(k ) Grammars] There exists an algorithm which always terminates and for all h, k ≥ 0, given an LL(h) grammar G1 and an LL(k) grammar G2, tells us whether or not they are equivalent, that is, L(G1) = L(G2). More decidability results are listed in Section 10.2 on page 277. Let us conclude this section by stating the following complexity result. Fact 4.3.25. [Parsing of LL(k ) Languages in Linear Time] Any LL(k) language can be parsed in O(n) time, where n is the length of the input word to be parsed. We assume that time complexity is given by the number of chop and expand actions performed on the stack. 4.3. LL(k ) PARSERS (FOR k ≥ 1) 79 Proof. We will consider the case of k = 1 (for k > 1 the proof is similar and we leave it to the reader). We have to show that the number of expand actions between any two chop actions is bounded by a constant (independent of the length of the input word to be parsed). Let us consider the given LL(1) grammar G = hVT , VN , P, Si and its LL(1) parsing table T . We have the following two cases. Case (i): in P there are no ε-productions, and Case (ii): in P there at least one ε-production. Case (i). In this case between any two chop actions it is impossible to perform more than |VN | expand actions because, otherwise, the grammar G would be left recursive (recall that: (1) for each column in the parsing table T there are at most |VN | entries each being filled with a production, and (2) any LL(1) grammar is not left recursive as stated by the Fact 4.2.14 on page 58). Case (ii). In this case we first transform the parsing table T by performing as long as possible the following replacement (which is the deletion of a nullable symbol): for all productions A → ε and B → αAβ, with α, β ∈ (VT ∪ VN )∗ , both occurring in the same column of the table T , we replace B → αAβ by B → αβ. Note that a single production may undergo more than one deletion. For instance, if in a column we have the productions A → ε, B → A, and C → ABa then the production C → ABa will undergo two deletions and eventually it will be replaced by C → a. Since in the table T there are |VT |+1 columns, and in each column there are at most |VN | ε-productions and at most |VN | productions which are not ε-productions, after O(|VT | × |VN |2 × ℓ) steps, where ℓ is the length of the longest right hand side of a production, we get a new table T ′ which is equivalent to the old table T , in the sense that we can parse L(G) by using T ′ , instead of T . Now, if parsing is done by using the transformed table T ′ , then, as in Case (i), between any two chop actions it is impossible to perform more than |VN | expand actions because, otherwise, the grammar G would be left recursive. Thus, if LL(1) parsing is done by using table T , instead of table T ′ , then between any two chop actions, since we only use the column of table T relative to the input symbol which is currently read, it is impossible to perform more than O(|VN |2 ×ℓ) expand actions.  CHAPTER 5 Parsers for Deterministic Context-Free Languages: LR(k ) Parsers In this chapter we will present the so called LR(k) parsers for deterministic context-free languages. 5.1. Introduction to LR(k ) Parsing The LR(k) parsers, for any k ≥ 0, are algorithms for parsing the languages which are generated by the LR(k) grammars which are defined as follows [3, pages 372-373]. Definition 5.1.1. [LR(k ) Grammar. Version 1] For any k ≥ 0, a context-free grammar G = hVT , VN , P, Si, possibly with ε-productions and without any occurrence of the start symbol S on the right hand side of any production, is an LR(k) grammar if for $ 6∈ VT , for every sentential form αβγ ∈ (VT ∪ VN )∗ , there exists a rightmost derivation of the form: S $k →∗rm α A γ $k →rm α β γ $k where: (i) the symbol $ occurs only on the rightmost positions of every sentential form, (ii) A ∈ VN , and (iii) the substring β which replaces A, can be determined by knowing the leftmost k symbols of γ $k (see also Figure 5.4.14 on page 117). Here is an equivalent definition of the LR(k) grammars [3, page 372]. Definition 5.1.2. [LR(k ) Grammar. Version 2] For any k ≥ 0, a context-free grammar G = hVT , VN , P, Si, possibly with ε-productions and without any occurrence of the start symbol S on the right hand side of any production, is an LR(k) grammar if for $ 6∈ VT , for any w, x, y ∈ VT∗ , for any α, β, γ ∈ (VT ∪VN )∗ , and for any A, B ∈ VN , if (1) S →∗rm α A w →rm α β w (2) S →∗rm and γ B x →rm α β y and (3) w k = y k  then α = γ, A = B, and x = y. Note that in the case of the rightmost derivations in Conditions (1) and (2) of the above definition, the production which is applied in the last step is A → β. Note also that given a context-free grammar G = hVT , VN , P, Si, we can avoid every occurrence of the start symbol S on the right hand side of the productions, by considering the new, equivalent grammar G′ = hVT , VN ∪ {S ′ }, P ∪ {S ′ → S}, S ′ i, where S ′ is a new start symbol. It is easy to see that L(G) = L(G′ ) because the language generated by the symbol S in G is equal to the language generated by the symbol S ′ in G′ . 81 82 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS In what follows we will introduce a few algorithms for parsing various subclasses of context-free languages. In particular, we will introduce algorithms for parsing: (i) LR(0) languages (see page 84), (ii) SLR(1) languages (see page 97), (iii) LR(1) languages (see page 102), and (iv) LALR(1) languages (see page 123). Unless otherwise specified, in this section we assume that VT , VN , and S denote, respectively, the set of the terminal symbols, the set of nonterminal symbols, and the start symbol. As usual, we will use the symbol V to denote the set VT ∪ VN . When presenting the LR(k) parsers, for any given context-free grammar G = hVT , VN , P, Si, we consider its augmented version which is defined as follows. Definition 5.1.3. [Augmented Context-Free Grammars] Given a contextfree grammar G = hVT , VN , P, Si with start symbol S, its augmented grammar, call it G′ = hVT ∪ {$}, VN ∪ {S ′}, P ′ , S ′i, is the grammar obtained by considering: (i) the new start symbol S ′ 6∈ VN , (ii) a new terminal symbol $ 6∈ VT , and (iii) the extra production S ′ → S $, that is, P ′ = P ∪ {S ′ → S $}. In some cases we will assume that the extra production is S ′ → S, instead of S ′ → S $. We will always indicate whether in the augmented grammar the extra production is S ′ → S $ or S ′ → S (see also Table 2 on page 137). We have the following fact. Fact 5.1.4. [Languages Generated by Grammars and Augmented Grammars] The language L(G) generated by a context-free grammar G is equal to the language L(G′ ) generated by the augmented grammar G′ if G′ has the extra production S ′ → S, while if G′ has the extra production S ′ → S $ then L(G′ ) = L(G) $. For the grammars we consider when presenting the LR(k) parsers we assume the following hypotheses. Hypotheses for LR(k) Parsing, for k ≥ 0. (i) The start symbol S (or S ′ in case of augmented grammars) does not occur on the right hand side of any production. (ii) No useless symbols occur in the productions. (iii) ε-productions may be present for the start symbol S or any other symbols of VN . (iv) The symbol $ does not belong to VT ∪ VN , where VT ∪ VN is the alphabet of the non-augmented grammar. (v) The input string to be parsed is terminated by the symbol $ that can occur only at the right end of the input string. (vi) The symbol $ does not occur on the right hand side of any production with the exception that $ may occur in the production S ′ → S $ for the new start symbol S ′ of the augmented grammar. On Section 5.2.2 on page 95 and Section 5.4.2 on page 119 we will comment on Hypothesis (i) that the start symbol S (or S ′ in case of augmented grammars) does 5.2. LR(0) PARSERS 83 not occur on the right hand side of any production. We will also comment on a condition based on the number of productions for start symbol S. 5.2. LR(0) Parsers In this section we will study the LR(0) parsers and the LR(0) grammars. Given a context-free grammar G = hVT , VN , S, P i, an LR(0) parser for G, if any, uses a parsing automaton (see Figure 5.2.1 on the next page) which makes its moves according to a table, called the LR(0) parsing table. This table is a matrix whose rows are labeled by the states of a finite automaton M we will define, and whose columns are labeled by the terminal symbols in VT and the nonterminal symbols in VN . In this table there is one extra column labeled by the symbol $ which is a new terminal symbol not belonging to VT . The columns of the table labeled by the elements in VT ∪ {$} constitute the so called action part of the table, while the columns labeled by the elements in VN constitute the so called goto part of the table. One can show that, for any given context-free grammar G, if the LR(0) parsing table for G has at most one action in every entry (we will clarify this condition later), then the grammar G generates a language L such that L$ is an LR(0) language. In the sequel we need the following definition. Definition 5.2.1. [Item and Complete Item] Given a context-free grammar G = hVT , VN , P, Si, a production of G with one extra symbol  (pronounced ‘dot’) in any position in the right hand side, is called an item of G. Thus, for a production whose right hand side has length n we have n+1 items. In particular, for a production of the form A → ε, where A is a nonterminal symbol and ε is the empty string, we have the following item only: A →  (whose right hand side consists of the dot only). An item is said to be complete if it is of the form A → β  , where β ∈ (VT ∪ VN )∗ , that is, the dot is at the rightmost position. Example 5.2.2. Let us consider the context-free grammar G with axiom S and whose productions are: 1. S → SA 2. S → A 3. A → aSb 4. A → ab We have the following twelve items: S →  SA S → S A S → SA  S → A S → A A →  aSb A → a  Sb A →  ab A → ab A → aS  b A → aSb  A → ab  Among those items we have four complete items which are those within rectangles. Now we present a five step algorithm which given a context-free grammar, constructs the LR(0) parsing table, call it T , which is required by the parsing automaton of Figure 5.2.1 on the following page. Then, we will explain how, once the table T has been constructed and there is at most one action in each of its entries, we can use the automaton of Figure 5.2.1 with table T for performing the LR(0) parsing of the language L$. Indeed, we can use that automaton for checking whether or not any given word belongs to the language L$. 84 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS w1 w2 . . . . . . wn $ : the input string is the string to be parsed terminated by $ ✲ ✻ action goto The head moves from left to right terminals nonterminals top of the stack ✛ ✲ ❄ q0 : stack a ✲ state on top of the stack LR(k) parser using the parsing table T : b + − ... $ S A B ... q0 q1 ... parsing table T (depicted for LR(0) or LR(1) parsing) Figure 5.2.1. A deterministic pushdown automaton for LR(k) parsing, with k ≥ 0. The string to be parsed is w1 w2 . . . wn . Initially, the stack has one symbol only: the initial state q0 at the bottom of the stack. The input string is the string to be parsed with the extra rightmost symbol $. We have depicted the parsing table T for LR(0) or LR(1) parsers. For the LR(k) parsers, with k ≥ 2, different parsing tables should be used. In order to fix our ideas, each step of the algorithm will be illustrated using a running example which refers to the context-free grammar G of the above Example 5.2.2 on the previous page. Algorithm 5.2.3. Constructing the LR(0) Parsing Table. We are given a context-free grammar G = hVT , VN , P, Si and we want to construct the LR(0) parsing table for the automaton of Figure 5.2.1. Step (1). First we consider the augmented grammar G′ = hVT ∪{$}, VN ∪{S ′ }, P ′, S ′ i, where the set P ′ of productions is P ∪ {S ′ → S $}. As usual, we assume that $ 6∈ VT and S ′ 6∈ VN . For example, in the case of the grammar G with axiom S and whose productions are: 1. S → SA 2. S → A 3. A → aSb 4. A → ab we consider the augmented grammar G′ = hVT ∪ {$}, VN ∪ {S ′ }, P ′, S ′ i with axiom S ′ and whose productions are: 0. S ′ → S $ 1. S → SA 2. S → A 3. A → aSb 4. A → ab Remark 5.2.4. [Avoiding the augmented grammar for LR(0) parsing] Note that if the start symbol S of a given grammar H does not occur on the right hand side of any production of H, then we can take the augmented grammar H ′ to be H itself, where in each production for S we have added $ as the rightmost symbol.  5.2. LR(0) PARSERS 85 Step (2). For each production in G′ of the form A → α1 α2 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪ VN ∪ {$} (obviously, only αn can be $), we construct the associated big production as the following linear graph made out of n+1 nodes and n arcs: α1 α2 αn A →  α1 α2 . . . αn A → α1  α2 . . . αn ... A → α1 α2 . . . αn  In particular, if the production is A → ε (in this case n = 0) then the associated big production is the following graph with one node only (recall that  ε = ε  =  ): A→  Note also that in a big production: (i) the leftmost node is an item of the grammar G′ of the form A →  α1 . . . αn with A ∈ VN and for i = 1, . . . , n, we have that αi ∈ VT ∪ VN ∪ {$}, and (ii) any other node is an item of the grammar G′ with the dot in a different position, and (iii) the rightmost node is a complete item of the grammar G′ . In a big production the arc from a node m to a node n is labeled by the symbol in VT ∪ VN ∪ {$} which in the node m occurs on the right of the dot and in the node n occurs on the left of the dot. Step (3). We consider the graph made out of all the big productions of the grammar G′ we have constructed at Step (2). Then, for all nonterminals A, B ∈ VN ∪ {S ′ } and strings α, β, γ ∈ (VT ∪ VN ∪ {$})∗ , we add an arc labeled by ε from every node whose label is an item of the form A → α  Bβ to every node whose label is the item B →  γ. These ε-arcs may relate either nodes into two different big productions or nodes of the same big production. • If the augmented grammar G′ is the grammar G itself, then we also add an ε-arc between any two nodes whose label has an item of the form S →  α $, for some production S → α, where S is the start symbol of G. In the case of our augmented grammar G′ , at the end of Step (3) we get the graph of Figure 5.2.2 on the next page. Step (4). By applying the Powerset Construction Procedure (see, for instance, [21]) to the graph of the big productions obtained at the end of Step (3), we get a finite automaton. Let that finite automaton be called M. In the case of our augmented grammar G′ , at the end of Step (4) we get the finite automaton of Figure 5.2.3 on page 87. As a result of the Powerset Construction, each state of M is labeled by a set of items of the grammar G′ . The initial state of M is the one whose label has the item of the leftmost node of the big production associated with S ′ → S $, that is, the item S ′ →  S $. The items in the states of the automaton M include those of the grammar G and the three extra items S ′ →  S $, S ′ → S  $, and S ′ → S $  , which are generated by the production S ′ → S $ of the augmented grammar G′ . The final state of M is the one whose label has the item S ′ → S $  . (As usual we write double lines around the final states.) • If the augmented grammar G′ is equal to the given grammar G with axiom S (and thus, as indicated in Remark 5.2.4 on the preceding page, we have that S does not occur on the right hand side of any production of G), then: 86 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S′ →  S $ ε ε S S′ → S  $ $ S′ → S $  S S → SA A S → SA  ε S →  SA ε S →A ε ε A ε S → A ε ε ε A →  aSb a A → a  Sb S A → aS  b A →  ab a A → ab b A → ab  b A → aSb  Figure 5.2.2. The graph of the big productions with the ε-arcs for the grammar G′ with axiom S ′ and productions: 0. S ′ → S $, 1. S → SA, 2. S → A, 3. A → aSb, 4. A → ab. Each big production is depicted as a sequence of nodes which are horizontally aligned. (i) the initial state of M is the one whose label has the set of items {S →  α $ | S → α where α ∈ V ∗ }, and (ii) a final state of M is every state whose label has an item of the form: S → α $  , for some production S → α, with α ∈ V ∗ . Step (5). The LR(0) parsing table T is then constructed from the automaton M as follows. The meaning of the various entries of that table will be clarified when we will describe the behaviour of the parsing automaton of Figure 5.2.1 on page 84 which uses that parsing table. a qj qi (5.1) For each transition , where a is terminal symbol different from $, we insert in the entry T [qi , a] which is in the action part of the table T , the action sh qj (short for shift qj ). This means that the parsing automaton should take the input symbol a at hand, push it on the stack, and push the state qj on the stack. Then the new input symbol at hand is the one to the right of the previous symbol a at hand. This action of the parsing automaton is called a shift action. $ qj qi (5.2) For each transition we insert in the entry T [qi , $] which is in the action part of the table T , the action acc (short for accept). This means that the parsing automaton should accept the given input string. This action of the parsing automaton is called the accept action. 5.2. LR(0) PARSERS S′ →  S $ q0 : S →  S A S → A A →  aSb A →  ab S q1 : S′ → S  $ S → S A A →  aSb A →  ab $ A A a 87 a q2 : S → A  q5 : S → SA  q4 : S ′ → S $  a A A → a  Sb A → ab q3 : S →  SA S → A A A →  aSb A →  ab S a q6 : A → aS  b S → SA A →  aSb A →  ab b b q7 : A → ab  q8 : A → aSb  Figure 5.2.3. The finite automaton M for the LR(0) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S $, 1. S → SA, 2. S → A, 3. A → aSb, 4. A → ab. The label of each state is a set of items which is computed by the Powerset Construction Procedure from the graph of Figure 5.2.2 on the facing page. (Note that, in the case where we take the augmented grammar G′ to be the given grammar G, the number of the final states of the automaton M is equal to the number of the productions for S.) (5.3) For each transition qi A qj with the nonterminal symbol A, we insert in the entry T [qi , A] which is in the goto part of the table T , the state qj . This means that the parsing automaton should push on the stack the state qj (see the following Step (5.4)). This action of the parsing automaton is called a goto action. (5.4) For each complete item A → α1 . . . αn  for some n ≥ 0, where the αi ’s are symbols in VT ∪ VN , in the label of a state qi , we insert in each entry of row qi in the action part of the table T , the action red p (short for reduce production p), where p is the number identifying the production A → α1 . . . αn of the grammar G′ . This means that the parsing automaton should replace the string α1 qα1 . . . αn qαn which is on the stack (the top element being state qαn ) by the nonterminal A. This action of the parsing automaton is called a reduce action. 88 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS In the case of our augmented grammar G′ at the end of Step (5) we get the parsing table T of Figure 5.2.4. The numbers which identify the productions of the grammar G′ are necessary for specifying the reduce actions, and they are as follows: 0. S ′ → S $ 1. S → SA 2. S → A action a b 3. A → a S b 4. A → a b goto $ q0 sh q3 q1 sh q3 q2 red 2 red 2 red 2 q3 sh q3 sh q7 q4 red 0 red 0 red 0 q5 red 1 red 1 red 1 q6 sh q3 sh q8 q7 red 4 red 4 red 4 q8 red 3 red 3 red 3 S A q1 q2 acc q5 q6 q2 where: 0. 1. 2. 3. 4. S′ → S $ S → SA S→A A → aSb A → ab q5 Figure 5.2.4. LR(0) parsing table T for the grammar G′ with axiom S ′ and productions: 0. S ′ → S $, 1. S → SA, 2. S → A, 3. A → aSb, 4. A → ab. It has been derived from the finite automaton of Figure 5.2.3 on the previous page. The row for state q4 can be eliminated, because in state q1 , when $ is read, the input string is accepted. Now let us explain how the parsing table T is used by the parsing automaton of Figure 5.2.1 on page 84 for performing the LR(0) parsing. Initially, the stack has the initial state q0 only. Then the parsing automaton reads the input string (which is terminated by the symbol $), one symbol at a time from left to right, and it performs the actions as indicated by the table T . If the top of the stack is the state q and the input symbol is a, for any a ∈ VT ∪ {$}, then the parsing automaton considers the entry T [q, a]. There are three cases. Case (1): shift and goto. If T [q, a] = sh q ′ , then the parsing automaton pushes the symbol a from the input string on the stack, and then it pushes the state q ′ on the stack. Case (2): accept. If T [q, a] = acc, then the parsing automaton accepts the whole input string. Case (3): reduce and goto. If T [q, a] = red p, where p is the number identifying the production A → α1 . . . αn (with αi ∈ VT ∪ VN ∪ {$}, for i = 1, . . . , n) of the grammar G′ , then the parsing automaton pops from the stack the whole string α1 qα1 . . . αn qαn (the top of the stack being the state qαn ). This leaves a state, say qe, 5.2. LR(0) PARSERS 89 on the top of the stack. Then the parsing automaton pushes A on the stack and then pushes also the state T [e q , A]. Thus, after every reduce action the parsing automaton performs a goto action. In the reduce and goto actions no symbol is read on the input, that is, an ε-move is made on the input. The behaviour of the parsing automaton we have described, satisfies the following property: every symbol α in VT ∪ VN ∪ {$} on the stack has above itself a state of the automaton M. We call that state the cover state of the symbol α. Figure 5.2.5 on the next page shows the behaviour of the automaton of Figure 5.2.1 on page 84 using the LR(0) parsing table of Figure 5.2.4 on page 88, while parsing the input string a a b a b b $ which is read from left to right. The symbol N points at the input character at hand. The stack grows and shrinks at the right end, and the symbol N points at the top of the stack. In that figure time flows, so to speak, from top to bottom in the sense that, for any i ≥ 0, the hstack, inputi configuration at time i+1 is generated by the hstack, inputi configuration at time i according to the actions described in the rightmost column, which we named the shift/reduce-goto column. In the literature, for every state q of the automaton M and for every symbol a ∈ VT ∪ {$}, the entry T [q, a] is also called ‘action(q, a)’, and for every A ∈ VN , the entry T [q, A] is also called ‘goto(q, A)’ (see also Figure 5.2.5 on the next page). With reference to Figure 5.2.5, the reader should note that the sequence of stack configurations shows in reverse order the rightmost derivation of the input string when it is accepted. The accept action stands for the reduce action: red 0 (S ′ → S $). In our case, in fact, by looking at the reduce action, we get the following rightmost derivation of a a b a b b $ from the axiom S ′ : S ′ →rm (12) S $ →rm (8) a S A b $ →rm (11) A $ →rm (10) a S b $ →rm →rm (7) a S a b b $ →rm (4) a A a b b $ →rm →rm (3) a a b a b b $. Since, according to our conventions production 0 of the grammar G′ is S ′ → S $, and the symbol $ occurs in this production only, we have that the action red 0 is never present in the sequence of configurations of the parsing automaton of Figure 5.2.1 on page 84, such as those we have depicted in Figure 5.2.5 on the next page. Moreover, in the LR(0) parsing table we can always eliminate the row corresponding to the final state, because in that state, when $ is read, the input string is accepted (see Step (5.2) of Algorithm 5.2.3 on page 84). For example, the row for state q4 in Table 5.2.4 on the preceding page can be eliminated. Therefore, in Step (5.4) of Algorithm 5.2.3 (see page 87) we could require that the state qi be not final and we could eliminate from the LR(0) parsing table the row corresponding to the final state. Now let us make a few remarks on Algorithm 5.2.3 for constructing the LR(0) parsing table. Remark 5.2.5. [Reduce action is independent of the input symbol] The reduce action does not depend on the input symbol at hand. For instance, in table T of 90 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS time stack (the top is marked by N) input (the symbol at hand is marked by N) shift/reduce-goto 0 q0 N aababb$ N action(q0 , a) = sh q3 1 q0 a q3 N a ababb$ N action(q3 , a) = sh q3 2 q0 a q3 a q3 N aa babb$ N action(q3 , b) = sh q7 3 q0 a q3 a q3 b q7 N aab abb$ N action(q7 , a) = red 4 (A → ab) goto(q3 , A) = q2 4 q0 a q3 A q2 N aab abb$ N action(q2 , a) = red 2 (S → A) goto(q3 , S) = q6 5 q0 a q3 S q6 N aab abb$ N action(q6 , a) = sh q3 6 q0 a q3 S q6 a q3 N aaba bb$ N action(q3 , b) = sh q7 7 q0 a q3 S q6 a q3 b q7 a a b a b b $ N N action(q7 , b) = red 4 (A → ab) goto(q6 , A) = q5 8 q0 a q3 S q6 A q5 N aabab b$ N action(q5 , b) = red 1 (S → SA) goto(q3 , S) = q6 9 q0 a q3 S q6 N aabab b$ N action(q6 , b) = sh q8 10 q0 a q3 S q6 b q8 N aababb $ N action(q8 , $) = red 3 (A → aSb) goto(q0 , A) = q2 11 q0 A q2 N aababb $ N action(q2 , $) = red 2 (S → A) goto(q0 , S) = q1 12 q0 S q1 N aababb $ N action(q1 , $) = accept Figure 5.2.5. The sequence of configurations of the parsing automaton of Figure 5.2.1 on page 84 while parsing the string a a b a b b $ using the LR(0) parsing table of Figure 5.2.4 on page 88 for the grammar G′ with axiom S ′ and productions: 0. S ′ → S $, 1. S → SA, 2. S → A, 3. A → aSb, 4. A → ab. 5.2. LR(0) PARSERS 91 Figure 5.2.4 on page 88 for the state q2 we have that: T [q2 , a] = T [q2 , b] = T [q2 , $] = red 2. Analogously, for the states q4 , q5 , q7 , and q8 .  Remark 5.2.6. [Multiple actions] One can show that if during the construction of the table T we have to place more than one action in a given entry or, in particular, more than one complete item occurs as the label of a state of the finite automaton M, then the augmented grammar G′ which has been derived from the given grammar G, is not an LR(0) grammar [10]. This means that the language L(G′ ) = L(G) $ which is generated by the grammar G′ cannot be parsed using the LR(0) parsing algorithm for G′ . If we have to place more than one action in a given entry of the table T , we say that there is a conflict. Depending on the actions simultaneously present in the same entry, a conflict can be either a shift-shift conflict, or a shift-reduce conflict, or reduce-reduce conflict.  Remark 5.2.7. [Undefined entr y] If during the parsing process of the input word w $, the pushdown automaton of Figure 5.2.1 on page 84 when using the table T , finds an undefined entry, then w $ is not in the LR(0) language L(G′ ) generated by the augmented grammar G′ and thus, the word w is not in the language generated by the given grammar G.  One can show that the following is an alternative definition of LR(0) grammars. This definition is based on the construction of the finite automaton M of Step (4) of Algorithm 5.2.3 on page 84 [10, page 252]. Definition 5.2.8. [LR(0) Grammar and LR(0) Language] A context-free grammar G is an LR(0) grammar if (i) the start symbol does not occur in the right hand side of any production, and (ii) in every state of the automaton M of Step (4) of Algorithm 5.2.3 on page 84 if there is a complete item of G then in that state no other complete item occurs nor any item with a terminal symbol immediately to the right of the dot. A language is an LR(0) language if it is generated by an LR(0) grammar. Note that when constructing the finite automaton M of Step (4) of Algorithm 5.2.3, it never happens that in a state with a complete item: (i) there is an item with a nonterminal symbol immediately to the right of the dot, and (ii) there is no item with a terminal symbol immediately to the right of the dot. In that case, in fact, in the given grammar there would be a useless symbol, contrary to our hypotheses. Note also that the grammar G of Example 5.2.2 on page 83 is not an LR(0) grammar, but as shown in the parsing table depicted in Figure 5.2.4 on page 88, the language L(G) $ is an LR(0) language. It is generated by the LR(0) grammar G′ which is the augmented version of the grammar G. Now we give an example of a grammar which is an LR(0) grammar and it is not LL(k) for any k ≥ 0. Example 5.2.9. [An LR(0) Grammar] Let us consider the grammar G with axiom S and whose productions are: S→A |B A → aAb | 0 B → aBbb | 1 92 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS The language generated by this grammar is: L = {an 0 bn | n ≥ 0} ∪ {an 1 b2n | n ≥ 0}. The grammar G is LR(0) and it is not LL(k) for any k ≥ 0. Now we will present a few results which we state without proof. First, we recall the following definitions (see [21, Section 3.3]). Definition 5.2.10. [Prefix-Free Language] A language L is said to be prefix free (or to enjoy the prefix property) iff no word in L is a proper prefix of another word in L, that is, for every word u ∈ L, the word uv for v 6= ε is not in L. Definition 5.2.11. [Deterministic Pushdown Automaton] A pushdown automaton hQ, Σ, Γ, q0 , Z0 , F, δi is said to be a deterministic pushdown automaton (or a dpda, for short) iff (i) ∀q ∈ Q, ∀Z ∈ Γ, if δ(q, ε, Z) 6= {} then ∀a ∈ Σ, δ(q, a, Z) = {} (that is, no other moves are allowed when an ε-move is allowed), and (ii) ∀q ∈ Q, ∀Z ∈ Γ, ∀x ∈ Σ ∪ {ε}, δ(q, x, Z) is either {} or a singleton (that is, if a move is allowed, then that move can be made in one way only, that is, there exists only one next configuration for the dpda). Definition 5.2.12. [Deterministic Context-Free Language] A context-free language L is said to be a deterministic context-free language iff it is accepted by a deterministic pushdown automaton by final state, that is, for each word w ∈ L, the deterministic pushdown automaton, starting from the input string w $ and the initial state q0 , eventually reaches a final state, having read all the symbols of w $. Definition 5.2.13. [Language Accepted by a Parsing Pushdown Automaton] Let us consider the parsing automaton A of Figure 5.2.1 on page 84, with its parsing table T . That table specifies the moves of A for each symbol in input and each symbol on the top of the stack. An input word w is accepted by the automaton A if starting from the configuration where: (i) w is the string to be read with the extra symbol $ at the right end (that is, initially the input head points at the leftmost symbol of w), and (ii) the initial state q0 is on the stack, then A eventually performs the accept action. The language accepted by a parsing pushdown automaton A with table T is the set of input words accepted by A. Let us consider an algorithm P which given a context-free grammar G, constructs its augmented grammar G′ and the parsing table TG′ for the parsing automaton A of Figure 5.2.1 on page 84. Depending on the augmented grammar G′ , the algorithm P may or may not generate a parsing table TG′ such that the language accepted by the parsing automaton A using that table, is equal to L(G′ ). In particular, if the parsing table TG′ has conflicts, then the language accepted by the parsing automaton A using that table, is not equal to L(G′ ). One can show that the augmented grammar G′ is an LR(0) grammar iff Algorithm 5.2.3 on page 84 produces a parsing table TG′ such that the language accepted by the parsing automaton A using that table, is equal to L(G′ ) [10]. Now we define the language which is accepted by an algorithm for constructing parsing tables. 5.2. LR(0) PARSERS 93 Definition 5.2.14. [Language Accepted by an Algorithm for Constructing Parsing Tables] Let us consider an algorithm P that, given a context-free grammar G, constructs the augmented grammar G′ and the parsing table TG′ for the parsing automaton A of Figure 5.2.1 on page 84. We say that the algorithm P accepts the language L if L is the language accepted by the parsing automaton A when it uses the parsing table TG′ . The following Theorems 5.2.15 and 5.2.16, which we state without proof, provide two alternative definitions of the class of the LR(0) languages and relate the class of deterministic context-free languages to the class of the LR(0) languages. Theorem 5.2.15. The class of the LR(0) languages is the class of the context-free languages which are accepted by Algorithm 5.2.3 on page 84. (Thus, it is appropriate to call Algorithm 5.2.3 an algorithm for constructing LR(0) parsing tables.) Theorem 5.2.16. [LR(0) Languages and Deterministic Context-Free Languages] (i) The class of the LR(0) languages is the class of deterministic context-free languages which enjoy the prefix property [10, pages 121 and 260]. (ii) A language L ⊆ Σ∗ is a deterministic context-free language iff L $, with $ not in VT , can be generated by an LR(0) grammar [10, page 260]. As for all LR(0) grammars, also for this LR(0) grammar, call it G′ , we have that its axiom, say S ′ , does not occur on the right hand side of any production. However, we do not insist that G′ be the result of augmenting a given context-free grammar (and thus, we do not insist that for S ′ there is only one production of the form: S ′ → S $). This last theorem is important because it relates the class of the LR(0) languages to the class of the deterministic context-free languages, and as we will see in Theorem 5.4.15 on page 116, the class of the deterministic context-free languages coincides with the class of the LR(1) languages . Note that, given a language L which is generated by an LR(1) grammar G with axiom S, an LR(0) grammar which generates the language L $ cannot, in general, be derived from the grammar G by adding the production S ′ → S $, where S ′ is the new axiom, as the following example shows. Example 5.2.17. Let us consider the grammar G with axiom S and productions: S→E E →E+T E→T T →T ×a T →a It is an LR(1) grammar as the reader may easily check after studying of the LR(1) grammars on Section 5.4 on page 102. Let L be the language generated by G. (Note that the grammar with axiom E and productions: E →E+T E→T T →T ×a T →a which also generates L, is not an LR(1) grammar simply because the axiom E occurs on the right hand side of a production.) Now, let us consider the grammar G′ obtained from G by adding the production S ′ → S $. Thus, the grammar G′ has the axiom S ′ and the following productions: 94 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S′ → S $ S→E E →E+T E→T T →T ×a T →a It generates the language L $, but it is not an LR(0) grammar. An LR(0) grammar which generates L $ is the following one with axiom S ′ and productions: S′ → E E →T +E E→T$ T →T ×a T →a (Note the productions E → T +E and E → T $, instead of the productions E → E+T and E → T .)  5.2.1. Avoiding the Powerset Construction for LR(0) Parsing. Algorithm 5.2.3 on page 84 for constructing the LR(0) parsing tables, can be replaced by the following algorithm which, given a context-free grammar, first constructs the corresponding augmented grammar G′ , then constructs the deterministic finite automaton M, and finally constructs the LR(0) parsing table. After the construction of the augmented grammar G′ , the construction of the automaton M is done by applying the following rules for generating: (i) states, (ii) sets of items in the labels of the states, and (iii) arcs. The Closure Rules (1), (2), and (3) should be applied as long as possible. Initialization Rule. The initial state q0 of the finite automaton M has its set of items initialized by the item S ′ →  S $. If the axiom S does not occur on the right hand side of any production, and we take the augmented grammar G′ to be G itself where in every production for S we have added a rightmost symbol $, then the label of the initial state q0 of the finite automaton M has an item of the form S →  α1 . . . αn $, for each production of the form S → α1 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪ VN . Closure Rule (1). For all A, B ∈ VN and α, β, γ ∈ V ∗ , if in the label of a state there is an item of the form A → α  Bβ, then we add to that label all items of the grammar G′ of the form: B →  γ. Closure Rule (2). For all A1 , . . . , An , B ∈ VN and α1 , β1 , . . . , αn , βn ∈ V ∗ , if all the items in the label of a state p with the dot immediately to the left of the nonterminal B are: A1 → α1  Bβ1 , . . . , An → αn  Bβn , for some n > 0, then we add to M: (i) one new state, call it q, whose label includes the items A1 → α1 B  β1 , . . . , An → αn B  βn , and (ii) one new arc with label B from the state p to the state q. 5.2. LR(0) PARSERS 95 Closure Rule (3). For all a ∈ VT , A1 , . . . , An ∈ VN , and α1 , β1 , . . . , αn , βn ∈ V ∗ , if all the items in the label of a state p with the dot immediately to the left of the terminal a are: A1 → α1  aβ1 , . . . , An → αn  aβn , for some n > 0, then we add to M: (i) one new state, call it q, whose label includes the items A1 → α1 a  β1 , . . . , An → αn a  βn , and (ii) one new arc with label a from the state p to the state q. The final state of the finite automaton M is the one whose label has the item S′ → S $  . If the axiom S does not occur on the right hand side of any production, and we take the augmented grammar G′ to be G itself where in every production for S we have added a rightmost symbol $, then every state of the finite automaton M whose label has the item S → α1 . . . αn $  , for some production of the form S → α1 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪ VN , is a final state. The correctness of these rules is a consequence of the correctness of the application of the Powerset Construction Procedure. These rules allow us to construct the deterministic finite automaton M without constructing the so called big productions and without applying the Powerset Construction Procedure. From M we then construct the LR(0) parsing table T as indicated in Step (5) of Algorithm 5.2.3 on page 86.  5.2.2. Remarks on the Hypotheses for LR(k ) Parsing. In this section we will make a few remarks on the hypotheses we made on page 82 for the LR(k) parsing algorithms, for k ≥ 0. More remarks will be made on Section 5.4.2 on page 119. On page 82 we have assumed that the start symbol S (or S ′ in case of augmented grammars) does not occur on the right hand side of any production. Indeed, if the start symbol occurs on the right hand side of a production, then we may get an LR(0) parsing table which does not allow a correct parsing as the following example shows. Example 5.2.18. Let us consider the grammar G with axiom S and the following productions: 1. S → A a $ 2. A → S a 3. A → a The language generated by this grammar is the regular language denoted by the regular expression (a a)+ $. The finite automaton M to be constructed for generating the LR(0) parsing table, is shown in Figure 5.2.6 on the next page. The corresponding parsing table is depicted in Figure 5.2.7 on the following page. This parsing table does not allow a correct parsing. Indeed, if we parse the word a a a a $ which belongs to (a a)+ $ (and thus it should be accepted), we get, instead, an error as indicated by the sequence of stack configurations depicted in Figure 5.2.8 on page 97 (indeed, there is no transition for the state q2 and the input symbol a). 96 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S → Aa$ q0 : A →  S a A → a A q1 : S → A  a $ a q2 : S → A a  $ $ S a q4 : A → a  q3 : S → A a $  q5 : A → S  a a q6 : A → S a  Figure 5.2.6. The finite automaton M for the LR(0) parsing of the grammar G with axiom S and productions: 1. S → A a $, 2. A → S a, 3. A → a. action a q0 sh q4 q1 sh q2 goto $ A q5 q1 acc q2 S q3 red 1 red 1 q4 red 3 red 3 q5 sh q6 q6 red 2 red 2 where: 1. S → A a $ 2. A → S a 3. A → a Figure 5.2.7. LR(0) parsing table for the grammar G with axiom S and productions: 1. S ′ → S $, 2. S → SA, 3. S → A. It has been derived from the finite automaton of Figure 5.2.6. We leave it to the reader to check that, on the contrary, the grammar G′ with axiom S ′ and productions: 0. S ′ → S $ 1. S → A a 2. A → S a 3. A → a is an LR(0) grammar, and we get a correct LR(0) parsing table. This grammar G′ that also generates the language (a a)+ $, is obtained from the grammar G by: (i) erasing the rightmost symbol $ of the production S → A a $, and (ii) adding the production S ′ → S $ so that the axiom S ′ does not occur on the right hand side of any production.  5.3. SLR(1) PARSERS time stack (the top is marked by N) input (the symbol at hand is marked by N) 97 shift/reduce-goto 0 q0 N aaaa$ N action(q0 , a) = sh q4 1 q0 a q4 N a aaa$ N action(q4 , a) = red 3 (A → a) goto(q0 , A) = q1 2 q0 A q1 N a aaa$ N action(q1 , a) = sh q2 3 q0 A q1 a q2 N aa aa$ N no action is defined Figure 5.2.8. The sequence of configurations of the parsing automaton of Figure 5.2.6 on the preceding page while parsing the string a a a a $ using the LR(0) parsing table of Figure 5.2.7 on the facing page for the grammar G with axiom S and productions: 1. S → A a $, 2. A → S a, 3. A → a. 5.3. SLR(1) Parsers In this section we will describe a new algorithm for constructing the parsing table of the automaton of Figure 5.2.1 on page 84. This algorithm is a variant of Algorithm 5.2.3 on page 84 for constructing LR(0) parsing tables. It provides the definition of a subclass of the deterministic context-free languages, called SLR(1) (short for Simple LR(1)) languages, as specified by the following definition. Definition 5.3.1. [SLR(1) Languages] A language is an SLR(1) language iff it is accepted by the following Algorithm 5.3.2 (this notion of acceptance is given in Definition 5.2.14 on page 93). Here is the algorithm for constructing SLR(1) parsing tables. Algorithm 5.3.2. Constructing the SLR(1) Parsing Table. Given a context-free grammar G, we first consider its augmented grammar G′ . Then, in order to fill the entries of the SLR(1) parsing table T for the parsing automaton of Figure 5.2.1 on page 84, we use the Algorithm 5.2.3 on page 84 for constructing LR(0) parsing tables, with the following rule (5.4*), instead of rule (5.4) on page 87: (5.4*) for every state q of the deterministic finite automaton M, if there exists in q a complete item A → α  , for some A ∈ VN and α ∈ V ∗ , then for every terminal symbol a of the set Follow 1(A), we insert in the entry T [q, a] of the parsing table the reduce action red p, where p identifies the production A → α of the augmented grammar G′ . If by using this algorithm we fill some entries of the table T by either (i) shift actions (in the action columns), or (ii) reduce actions (in the action columns), or (iii) goto 98 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS actions (in the goto columns), and no more than one action is placed in every entry of that table, then the augmented grammar G′ is said to be an SLR(1) grammar. Note that the parameter 1 in the qualifier ‘SLR(1)’ is due to the fact that in our algorithm we have used the set Follow 1(A), rather than the set Follow k (A), for some k > 1. Here is an example of an SLR(1) grammar and the construction of corresponding parsing table according to Algorithm 5.3.2 on the preceding page. Example 5.3.3. [SLR(1) Parsing: An Example] Let us consider the following augmented grammar: 0. E ′ → E $ 1. E → E + T 2. E → T 3. T → T × F 4. T → F 5. F → (E) 6. F → a The finite automaton constructed at the end of Step (4) by Algorithm 5.2.3 on page 84 is shown in Figure 5.3.1 on the facing page. The parsing table constructed from the finite automaton of Figure 5.3.1, is shown in Figure 5.3.2 on page 100. To construct that table we have first to compute the following Follow 1 sets: Follow 1(E) = {+, ), $}, Follow 1(T ) = {+, ×, ), $}, and Follow 1(F ) = {+, ×, ), $} Note that, contrary to the LR(0) parsing tables, in the SLR(1) parsing tables we may have in the same row both a reduce action and a shift action (which, obviously, are placed in different columns). In particular, state q9 shows that the given grammar is not an LR(0) grammar. In state q9 , by using one symbol of lookahead on the input string, we do the following actions: (i) if the lookahead symbol is ×, then we perform a shift action by placing × on the top of the stack, and we look for an F , because of the item T → T  × F in which the symbol × is followed by F , and otherwise, (ii) if the lookahead symbol is different from × and belongs to Follow 1(E), then we perform a reduce action by reducing production 1 which is: E → E+T . Indeed, in state q9 we have only the complete item E → E+T  , besides the item T → T  ×F . The action red 0 has been inserted in the row for state q12 in the SLR(1) parsing table of Figure 5.3.2 on page 100 in the hypothesis that $ belongs to the Follow 1 set of the axiom of every augmented grammar (as it is the case for the axiom of every grammar). Thus, in our case, for the axiom E ′ of the augmented grammar we have assumed that $ ∈ Follow 1 (E ′ ). On the contrary, if we assume that Follow 1 (E ′ ) = ∅, then the action red 0 should have not been inserted. 5.3. SLR(1) PARSERS E′ →  E $ E →  E +T q0 : E →  T T →  T ×F T → F a ( F F →  (E) F → a T E E→T T → T  ×F T E′ → E  $ E → E  +T $ q12 : E ′ → E $  + E → E+  T F →  (E) q6 : T →  T ×F F → a T → F a F q2 : q1 : 99 × ( T → T × F q7 : F →  (E) F → a ( T q : E → E +T  9 T → T  ×F × F a q10 : T → T×F  + q3 : T → F  F ( F → (  E) E →  E +T F →  (E) E q4 : E →  T F → a T →  T ×F T → F q8 : F → (E  ) E → E  +T ) a q5 : F → a  q11 : F → (E)  Figure 5.3.1. The finite automaton for the SLR(1) parsing of the grammar with axiom E ′ and productions: 0. E ′ → E $, 1. E → E+T , 2. E → T , 3. T → T ×F , 4. T → F , 5. F → (E), 6. F → a. The label of each state is a set of items and it is generated by the Powerset Construction Procedure starting from the graph of the big productions whose construction is left to the reader (see Algorithm 5.2.3 on page 84 for constructing the LR(0) parsing tables). Note, however, that the way in which we fill the row for state q12 does not make any difference during parsing, because in state q1 , when the symbol $ is read, the input string is accepted, and thus state q12 is never reached. As a consequence, the row for state q12 can be eliminated. Now we give an example of a grammar which is not SLR(1). 100 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS action a q0 + × goto ( sh q5 ) $ sh q4 q1 sh q6 q2 red 2 sh q7 red 2 red 2 q3 red 4 red 4 red 4 red 4 q4 T F q1 q2 q3 acc sh q5 sh q4 red 6 red 6 q5 E red 6 q6 sh q5 sh q4 q7 sh q5 sh q4 q8 q2 q3 q9 q3 red 6 q10 q8 sh q6 sh q11 q9 red 1 sh q7 red 1 red 1 q10 red 3 red 3 red 3 red 3 q11 red 5 red 5 red 5 red 5 0. 1. 2. where: 3. 4. 5. 6. E′ → E $ E → E +T E →T T → T×F T →F F → (E) F →a red 0 q12 Figure 5.3.2. SLR(1) parsing table for the grammar with axiom E ′ and productions: 0. E ′ → E $, 1. E → E + T , 2. E → T , 3. T → T ×F , 4. T → F , 5. F → (E), 6. F → a. It has been derived from the automaton of Figure 5.3.1 on the previous page. The row for state q12 can be eliminated because in state q1 , when $ is read, the input string is accepted. Example 5.3.4. [A Grammars Which Is Not SLR(1)] Let us consider the grammar with axiom S and the following productions: 1. S → L = R 2. S → R 3. L → ∗R 4. L → a 5. R → L Its augmented grammar has axiom S ′ and the extra production S ′ → S $. The language generated by this grammar is the language of the so called left-values and right-values of assignments. In this language the string ∗R denotes the content of the location R. If we apply Algorithm 5.2.3 on page 84 for constructing the LR(0) parsing table, we get a state with items: S → L  = R and R → L  (see state q3 in Figure 5.3.3 on the facing page). We also have that Follow 1(L) = {=} and Follow 1(R) = {=}. Thus, in the SLR(1) parsing table for that state q3 and terminal symbol ‘=’, we should insert according to Algorithm 5.3.2 on page 97, both a shift action (because of the symbol ‘=’) and a reduce action (because R → L  is a complete item and the symbol ‘=’ is an element of Follow 1(R)). Then we would have a so called shift-reduce conflict, and thus the given grammar is not SLR(1). 5.3. SLR(1) PARSERS S′ →  S $ L → ∗R q0 : S →  L = R L →  a S → R R → L S ∗ R q3 : q9 : L → a  a $ q1 : S ′ → S  $ q2 : S ′ → S $  L a ∗ 101 q4 : S → R  L → ∗  R L →  ∗R q5 : R → L L → a S → L  =R R → L S → L = R R → L q6 : L→ ∗R L → a = ∗ L L R R q8 : R → L  q10 : S → L = R  q7 : L → ∗R  Figure 5.3.3. The finite automaton for the grammar with axiom S ′ and productions: 0. S ′ → S $, 1. S → L = R, 2. S → R, 3. L → ∗R, 4. L → a, 5. R → L. The label of each state is generated by the Powerset Construction Procedure (see Algorithm 5.2.3 on page 84). State q3 shows that this grammar is not an SLR(1) grammar. 102 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS 5.4. LR(1) Parsers In this section we describe a new algorithm for constructing the parsing table of the automaton of Figure 5.2.1 on page 84. This algorithm is similar to Algorithm 5.2.3 on page 84 (which was used for constructing LR(0) parsing tables) and provides the definition of a class of the deterministic context-free languages, the so called LR(1) languages. Actually, one can show (see Theorem 5.4.15 on page 116) that every deterministic context-free language admits an LR(1) grammar which generates it. Algorithm 5.4.1. Constructing the LR(1) Parsing Table. Given a context-free grammar G = hVT , VN , P, Si we want to construct the LR(1) parsing table for the parsing automaton of Figure 5.2.1 on page 84. Step (1). First we consider the augmented grammar G′ = hVT ∪{$}, VN ∪{S ′ }, P ′, S ′ i, where the set P ′ of productions is P ∪{S ′ → S}. We assume that $ 6∈ VT and S ′ 6∈ VN . We have that the start symbol S ′ of the augmented grammar does not occur on the right hand side of any production of the augmented grammar. For instance, in the case of the grammar G with axiom S and the three productions 1. S → C C 2. C → c C we consider the following augmented grammar G′ : 0. S ′ → S 1. S → C C 2. C → c C 3. C → d 3. C → d Remark 5.4.2. [Avoiding the augmented grammar for LR(1) parsing] Note that if the start symbol S of the grammar G does not occur on the right hand side of any production of G, then we can take the augmented grammar G′ to be G itself.  Step (2). We construct a set P of big productions with lookahead sets as we now indicate. A big production with a lookahead set is a linear graph like the one described in Step (2) of Algorithm 5.2.3 on page 84 for constructing LR(0) parsing tables, but in the label of each node of the graph there is also a set of symbols in VT ∪ {$}, called the lookahead set. The construction of the set P of big productions with lookahead sets is done incrementally by applying to the following Rules (2.1) and (2.2). The Closure Rule (2.2) should be applied as long as possible. Initialization Rule 2.1 Initially, the set P consists of one big production only. It is the following one with lookahead set {$}: S S ′ →  S, {$} S ′ → S  , {$} If the axiom S does not occur on the right hand side of any production, and we take the augmented grammar G′ to be G itself, then the set P initially includes a big production with lookahead set {$} of the form: S →  α1 α2 . . . αn , {$} α1 S → α1 α2 . . . αn , {$} α2 ... αn S → α1 α2 . . . αn , {$} for each production for the axiom S of the form S → α1 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪ VN . 5.4. LR(1) PARSERS 103 Closure Rule 2.2 For every node in the graph constructed so far, of the form: B → α  A β, L where A, B ∈ VN and α, β ∈ V ∗ , and for every production A → γ, with γ = γ1 . . . γn , for n ≥ 0, where the γi ’s belong to VT ∪ VN , in the augmented grammar G′ , we add to the set P the following big production: γ1 γ2 γn A →  γ1 γ2 . . . γn , L′ A → γ1 γ2 . . . γn , L′ ... A → γ1 γ2 . . . γn , L′ whose lookahead set L′ is defined as follows: L′ = if β = ε then L ∗ if β = 6 ε and β → ε then L ∪ (First 1 (β)−{ε}) if β = 6 ε and β 6→∗ ε then First 1 (β) Step (3). We consider the graph constructed at Step (2), made out of all the big productions with lookahead sets. Then, for all nonterminals A, B ∈ VN , strings α, β, γ ∈ (VT ∪ VN )∗ , and lookahead sets L, we add an arc labeled by ε from every node of the form: B → α  Aβ, L to every node of the form: A →  γ, L′ , where L′ is con- structed from L as indicated at the end of Step (2). • If the augmented grammar G′ is the grammar G itself, then we also add an εarc between any two nodes whose label has an item of the form S →  α, for some production S → α, where S is the start symbol of G. Step (4). By applying the Powerset Construction Procedure (see, for instance, [21]) to the graph of the big productions with lookahead sets obtained at the end of Step (3), we get a finite automaton. Let that finite automaton be called M. Note that by the Powerset Construction the label of each state of M is a set of items of the grammar G′ , each item being associated with its lookahead set. In what follows we use the following notation and terminology. Notation 5.4.3. An item A → β with its lookahead set L will be denoted by A → β, L or, if that item is in a final state, by A → β, L . A state q whose label has the n (≥ 1) items A1 → β1 , L1 , . . ., An → βn , Ln , will also be denoted by A1 → β1 , L1 q : ··· An → βn , Ln these n items is in the label of the state . In this case we also say that each of q . By abuse of language, we will feel free to say ‘an item’, instead of ‘an item with its lookahead set’.  104 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS The initial state of the finite automaton M is the one whose label has the item S →  S, {$} . ′ The final state of M is the one whose label has the item S ′ → S  , {$} . • If the augmented grammar G′ is equal to the given grammar G with axiom S (and thus, as indicated in Remark 5.4.2 on page 102, we have that S does not occur on the right hand side of any production of G), then: (i) the initial state of M is the one whose label has, possibly among others, the set of items { S → α1 α2 . . . αn , {$} | S → α1 α2 . . . αn , where for i = 1, . . . , n, αi ∈ VT ∪VN }, and (ii) a final state of M is every state whose label has an item of the form: S → α1 α2 . . . αn  , {$} , for some production S → α1 α2 . . . αn , where for i = 1, . . . , n, αi ∈ VT ∪ VN . Step (5). The LR(1) parsing table T is then constructed from the automaton M as follows. a qj where a is a terminal symbol, (5.1) For each transition qi we insert in the entry T [qi , a] which is in the action part of the table T , the action sh qj (short for shift qj ). This means that the parsing automaton of Figure 5.2.1 on page 84 should take the input symbol a at hand, push it on the stack, and push the state qj on the stack. Then the new input symbol at hand is the one to the right of the previous symbol a at hand. This action of the parsing automaton is called a shift action. (5.2) For the final state qi whose label has the item S ′ → S  , {$} , we insert in the entry T [qi , $] which is in the action part of the table T , the action acc (short for accept). This means that the parsing automaton of Figure 5.2.1 should accept the given input string. This action of the parsing automaton is called an accept action. Recall that in the final state qi , instead of the item S ′ → S  , {$} , we have the item S → α1 . . . αn  , {$} , for some n ≥ 0 and some α1 , . . . , αn in VT ∪ VN , if according to Remark 5.4.2 on page 102, we take the augmented grammar to be the given grammar itself (and we can do so if the axiom S does not occur on the right hand side of any production). (5.3) For each transition qi A qj with the nonterminal A, we insert in the entry T [qi , A] which is in the goto part of the table T , the state qj . This means that the parsing automaton of Figure 5.2.1 should push on the stack the state qj (see Step (5.4) below). This action of the parsing automaton is called a goto action. 5.4. LR(1) PARSERS 105 (5.4) For each complete item A → α1 . . . αn  , L , for some n ≥ 0 and some α1 , . . . , αn in VT ∪ VN , in the label of a (final or not final) state qi , for each a ∈ L, if a 6= $, we insert in the entry T [qi , a] in the action part of the table T , the action red p (short for reduce production p), where p is the number identifying the production A → α1 . . . αn of the grammar G′ . This means that the parsing automaton of Figure 5.2.1 should replace the string α1 qα1 . . . αn qαn which is on the stack (the top element being state qαn ) by the nonterminal A. This action of the parsing automaton is called a reduce action. We have the condition ‘a 6= $’ because for a = $ during Step (5.2) we have already considered the complete item with $ in its lookahead set and we have inserted the action acc in the entry T [qi , $] (in this case the state qi is final). Note that in this Step (5.4) the nonterminal symbol A on the left hand side of a complete item A → α1 . . . αn  , L in the label of a state qi which is not final, cannot be the axiom S ′ of the augmented grammar G′ that we have considered at the end of Step (1), because of Hypothesis (i) we have made when performing the LR(k) parsing (see page 82). Similarly, in the case where the augmented grammar G′ is equal to the given grammar G, then the nonterminal symbol A cannot be the axiom S. Having constructed the LR(1) parsing table, we can use the pushdown automaton of Figure 5.2.1 on page 84 with that table, for parsing a given input word w. Initially, the stack has the initial state q0 only, and the input string is w $. Then the pushdown automaton works by using the LR(1) parsing table as it were an LR(0) parsing table. As in the case of LR(0) parsing (see page 89), the sequence of the stack configurations shows in the reverse order the rightmost derivation of the input string w $, when that string is generated by the augmented grammar G′ (see, for instance, Figure 5.4.4 on page 110). The accept action stands for the reduce action: red 0 (S ′ → S).  Remark 5.4.4. [Multiple actions] As for LR(0) grammars, also in the case of the LR(1) grammars, if we have to place more than one action in an entry of the LR(1) parsing table (that is, we have conflicts), then the language generated by the given grammar cannot be parsed by using a pushdown automaton which uses that table and the augmented grammar G′ which has been derived from the given grammar G, is not an LR(1) grammar. Remark 5.4.5. [Undefined entry] If during the process of parsing a given input word w $, the pushdown automaton requires an entry of the table, but that entry is empty, then the word w does not belong to the language generated by the augmented grammar G′ and w does not belong to the language generated by the given grammar G.  Remark 5.4.6. [Replacing many items by a single item within the same state: union of the lookahead sets] When applying Algorithm 5.4.1 on page 102, in any state, for any n ≥ 2, we can replace n items of the form: A → γ, L1 , . . ., A → γ, Ln , where A ∈ VN and γ ∈ (VT ∪VN )∗ ∪ {  } ∪(VT ∪VN )∗ (that is, γ is a string of terminal or nonterminal symbols with a dot  at the beginning, or in between, or at the end), 106 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS by a single item of the form: A → γ, L1 ∪ . . . ∪ Ln . Indeed, this replacement does not modify the outcome of that algorithm (see Step (5.4) on page 105).  Note that in the LR(1) parsing table there is no column labeled by S ′ , because no arc in the graph representing the finite automaton M constructed at the end of Step (4) of Algorithm 5.4.1 on page 103, has label S ′ . We leave it to the reader to check that, if the axiom of the augmented grammar is S ′ and the axiom of the given grammar is S, when the action accept is performed, then the pushdown automaton is in a final state q whose label has the item S ′ → S  , {$} , the input symbol at hand is $, and the stack is of the form (the top being the state q): q0 S q If, according to Remark 5.4.2 on page 102, we take the augmented grammar to be the given grammar itself, then when the action accept is performed, (i) the pushdown automaton is in the final state q whose label has the item S → α1 . . . αn  , {$} , for some production for the axiom S of the form S → α1 . . . αn , for some n ≥ 0 and the αi ’s in VT ∪ VN , (ii) the input symbol at hand is $, and (iii) the stack is of the form (the top being the state q): q0 α1 qr . . . qs αn q The following fact states a property which is enjoyed by the lookahead sets of the items of the LR(1) grammars. Fact 5.4.7. [Lookahead Sets and Follow 1 Sets] For any item of the form B → α  β, L , with B ∈ (VN ∪ {S ′ }) and α, β ∈ (VT ∪ VN )∗ , we have that: L ⊆ Follow 1(B). Proof. The proof is by cases. Case (1). For the item S ′ →  S, {$} where S ′ is the axiom of the augmented grammar and the production S ′ → S is the only production for S ′ , we get the thesis because Follow1(S ′ ) = {$}. Case (2). For the item B → α  Aβ, L with β 6= ε and β →∗ ε, we have to consider items of the form A → . . . , L′ , where L′ = L ∪ (First 1 (β) − {ε}). The thesis follows from the following Points (2.1) and (2.2). Point (2.1). By induction hypothesis we have that: L ⊆ F ollow 1 (B). We also have that F ollow 1 (B) ⊆ F ollow1 (A), by definition of the function Follow 1 when β 6= ε and β →∗ ε (see Algorithm 4.2.7 on page 56). Thus, L ⊆ F ollow 1 (A). 5.4. LR(1) PARSERS 107 Point (2.2). We have that: (First 1 (β) − {ε}) ⊆ F ollow1 (A), by definition of the function Follow 1 when β 6= ε and β →∗ ε. Case (3). For the item B → α  Aβ, L with β 6= ε and β 6→∗ ε, we have to consider items of the form A → . . . , L′ , where L′ = First 1 (β). The thesis follows because First 1 (β) − {ε}) = First 1 (β) (recall that, when β 6→∗ ε, we have that ε 6∈ First 1 (β)) and (First 1 (β)−{ε}) ⊆ F ollow1 (A) (by definition of the function Follow 1 when β 6= ε  and β 6→∗ ε). One can show that the following is an alternative definition of LR(1) grammars. The proof of this fact is based on Algorithm 5.4.1 on page 102 and can be found in [10, page 263]. Definition 5.4.8. [LR(1) Grammar and LR(1) Language] A context-free grammar G is an LR(1) grammar iff (i) the start symbol does not occur in the right hand side of any production, and (ii) if in the label of a state of the automaton M of Step (4) of Algorithm 5.4.1 for constructing the LR(1) parsing tables, there is a complete item of the form A → α, L (recall that the label of a state is a set of items, each item being associ- ated with its lookahead set), then (ii.1) for any other complete item B → β  , M in the same label, L∩M = ∅, and (ii.2) for any other incomplete item B → β  aγ, M , with a ∈ VT ∪ {$} in the same label, we have that a 6∈ L. A language is an LR(1) language if it is generated by an LR(1) grammar. Now we will present some examples of LR(1) parsing. Example 5.4.9. [LR(1) Parsing: Example 1] Let us consider the grammar G with axiom S and the following productions: 1. S → C C 2. C → c C 3. C → d 2. C → c C 3. C → d The augmented grammar G′ is: 0. S ′ → S 1. S → C C Note that for the grammar G we can take the augmented grammar G′ to be G itself, because there is only one production for S and S does not occur on the right hand side of any production in G. The graph of the big productions with the lookahead sets is depicted in Figure 5.4.1 on the following page. Note the item S →  C C, {$} in the second row from above, has an ε-arc both to the item C →  c C, {c, d} in the third row and to the item C →  d, {c, d} in the fifth row, both with lookahead set {c, d}, because C 6→∗ ε and First 1 (C) = {c, d}. 108 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S S ′ →  S, {$} S ′ → S  , {$} ε C S →  CC, {$} ε S → C  C, {$} ε C S → CC  , {$} C → c  C, {c, d} C C → c C  , {c, d} C C → c C  , {$} ε ε c C →  c C, {c, d} ε ε C →  c C, {$} c ε C → c  C, {$} ε d C →  d, {c, d} C →  d, {$} d C → d  , {c, d} C → d  , {$} Figure 5.4.1. The graph of the big productions with lookahead sets and ε-arcs for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. Each big production with its lookahead set is depicted as a sequence of nodes which are horizontally aligned. From the graph of Figure 5.4.1 the Powerset Construction Procedure constructs the finite automaton depicted in Figure 5.4.2 on the next page. From that automaton we get the LR(1) parsing table of Figure 5.4.3 on page 110. Figure 5.4.4 on page 110 shows the behaviour of the automaton of Figure 5.2.1 on page 84, while parsing the input string d c d $ which is read from left to right. In Figure 5.4.4 we use the same conventions of Figure 5.2.5 on page 90 and, in particular, the symbol N points at the input character at hand. The stack grows and shrinks at the right end, and the symbol N points at the top of the stack.  5.4. LR(1) PARSERS S ′ →  S, {$} S →  C C, {$} q0 : C →  c C, {c, d} C →  d, {c, d} S 109 q1 : S ′ → S  , {$} q5 : S → C C  , {$} d C C S → C  C, {$} q2 : C →  c C, {$} C →  d, {$} c c d c c C → c  C, {$} q6 : C →  c C, {$} C →  d, {$} C q9 : C → c C  , {$} d q7 : C → d  , {$} C → c  C, {c, d} q3 : C →  c C, {c, d} C →  d, {c, d} C q : C → c C  , {c, d} 8 d q4 : C → d  , {c, d} Figure 5.4.2. The finite automaton M for the LR(1) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. The label of each state is a set of items which is computed by the Powerset Construction Procedure from the graph of Figure 5.4.1. 110 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS c q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 action d $ sh q3 sh q4 goto S C q1 q2 acc sh q6 sh q7 sh q3 sh q4 red 3 red 3 q5 q8 red 1 sh q6 sh q7 where: 0. 1. 2. 3. S′ → S S →CC C → cC C→d q9 red 3 red 2 red 2 red 2 Figure 5.4.3. LR(1) parsing table for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. It has been derived from the automaton of Figure 5.4.2 on page 109. time stack (the top is marked by N) input (the symbol at hand is marked by N) 0 q0 N dcd$ N 1 q0 d q4 N q0 C q2 N q0 C q2 c q6 N d cd$ N d cd$ N dcd$ N 4 q0 C q2 c q6 d q7 N dcd$ N 5 q0 C q2 c q6 C q9 N dcd$ N 6 q0 C q2 C q5 N q0 S q1 N dcd$ N dcd$ N 2 3 7 shift/reduce-goto action(q0 , d) = sh q4 action(q4 , c) = red 3 (C → d) goto(q0 , C) = q2 action(q2 , c) = sh q6 action(q6 , d) = sh q7 action(q7 , $) = red 3 (C → d) goto(q6 , C) = q9 action(q9 , $) = red 2 (C → c C) goto(q2 , C) = q5 action(q5 , $) = red 1 (S → C C) goto(q0 , S) = q1 action(q1 , $) = accept Figure 5.4.4. Sequence of configurations of the pushdown automaton of Figure 5.2.1 on page 84 while parsing the string d c d $ using the LR(1) parsing table of Figure 5.4.3, for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. 5.4. LR(1) PARSERS 111 Before presenting some more examples of LR(1) parsing, let us make the following remark. Remark 5.4.10. The label of the final state of the finite automaton M derived at the end of Step (4) of Algorithm 5.4.1 on page 103, may have, together with the complete item S ′ → S  , {$} (or S → α1 . . . αn  , {$} , for some production S → α1 . . . αn for the axiom S, where n ≥ 0 and, for i = 1, . . . , n, αi ∈ (VT ∪ VN )∗ , in case we take the augmented grammar to be the given grammar itself), some more complete items. If $ belongs to one of the lookahead sets of those other complete items, then the given grammar is not LR(1) because, together with the accept action, we have to insert also a reduce action in the same entry of the LR(1) parsing table. This case occurs, for instance, for the augmented grammar G′1 with axiom S ′ and productions: 0. S ′ → S 1. S → A 2. S → c 3. A → S 4. A → a ′ The grammar G1 generates the language {a, c} and is not LR(1). The finite automaton for this grammar G′1 constructed at the end of Step (4) of Algorithm 5.4.1 on page 103, is depicted in Figure 5.4.5 and the associated parsing table is depicted in Figure 5.4.6 on the following page (note the double entry for state q1 and the symbol $). In Theorem 5.4.15 on page 116 we will see that every LR(1) grammar generates a deterministic context-free language, and in Fact 5.8.12 on page 141 we will see that every word of a language generated by an LR(1) grammar has a unique parse tree. This is not the case for the word a of the language {a, c}. Indeed, a has (among infinitely many others) the following two parse trees: S ′ → S → A → a and S ′ → S → A → S → A → a. S ′ →  S, {$} A →  S, {$} q0 : S →  c, {$} A →  a, {$} S →  A, {$} S q1 : S ′ → S  , {$} A → S  , {$} A c a q4 : S → A  , {$} q2 : S → c  , {$} q3 : A → a  , {$} Figure 5.4.5. The finite automaton for the grammar G′1 with axiom S ′ and productions: 0. S ′ → S, 1. S → c, 2. S → A, 3. A → S 4. A → a. On the contrary, the augmented grammar G′2 with axiom S ′ and productions: 0. S ′ → S 1. S → A a 2. S → c 3. A → S 4. A → a ∗ that generates the language (a a + c) a , is LR(1). The finite automaton for this grammar G′2 is depicted in Figure 5.4.7 on the next page and the associated LR(1) parsing table is depicted in Figure 5.4.8 on the following page.  112 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS action c a q0 q1 q2 q3 q4 goto S A $ sh q3 sh q2 0. 1. 2. 3. 4. q1 q4 red 3/acc red 1 red 4 red 2 where: S′ → S S→c S→A A→S A→a Figure 5.4.6. Parsing table for the grammar G′1 with axiom S ′ and productions: 0. S ′ → S, 1. S → c, 2. S → A, 3. A → S 4. A → a. It has been derived from the automaton of Figure 5.4.5 on page 111. S ′ →  S, {$} A →  S, {a} S ′ → S  , {$} S q0 : S →  c, {$, a} q1 : A →  a, {a} A → S  , {a} S →  A a, {$, a} A a c q4 : S → A  a, {$, a} a q5 : S → A a  , {$, a} q2 : S → c  , {$, a} q3 : A → a  , {a} Figure 5.4.7. The finite automaton for the augmented grammar G′2 with axiom S ′ and productions: 0. S ′ → S, 1. S → c, 2. S → A a, 3. A → S 4. A → a. a q0 q1 q2 q3 q4 q5 action c $ sh q3 sh q2 red 3 acc red 1 red 1 red 4 sh q5 red 2 red 2 goto S A q1 q4 where: 0. 1. 2. 3. 4. S′ → S S→c S → Aa A→S A→a Figure 5.4.8. LR(1) parsing table for the augmented grammar G′2 with axiom S ′ and productions: 0. S ′ → S, 1. S → c, 2. S → A a, 3. A → S 4. A → a. It has been derived from the automaton of Figure 5.4.7 on page 112. 5.4. LR(1) PARSERS 113 Example 5.4.11. [LR(1) Parsing: Example 2] Let us consider the grammar G with axiom A and the following productions: 1. A → B A 2. A → ε 3. B → a B 4. B → b The augmented grammar G′ with axiom S has the productions: 0. S → A 1. A → B A 2. A → ε 3. B → a B 4. B → b (Note that for reasons of simplicity, we have used the symbol S, instead of S ′ .) The language generated by this grammar is (a∗ b)∗ . The graph of the big productions with the lookahead sets is depicted in Figure 5.4.9. Note that the item A →  B A, {$} has an ε-arc both to the item B →  a B, {$,a,b} and to the B →  b, {$,a,b} , both with lookahead set {$, a, b}, because A → ε and item First 1 (B) = {a, b}. S →  A, {$} ε A →  BA, {$} ε ε A S → A  , {$} B ε ε A → B  A, {$} a ε ε B → a  B, {$,a,b} A A → BA  , {$} A →  , {$} ε B →  a B, {$,a,b} B →  b, {$,a,b} b B B → a B  , {$,a,b} B → b  , {$,a,b} Figure 5.4.9. The graph of the big productions with lookahead sets and ε-arcs for the grammar G′ with axiom S and productions: 0. S → A, 1. A → B A, 2. A → ε, 3. B → a B, 4. B → b. Each big production with its lookahead set is depicted as a sequence of nodes which are horizontally aligned. The Powerset Construction Procedure constructs from the graph of Figure 5.4.9 the finite automaton depicted in Figure 5.4.10 on the following page. From that automaton we get the LR(1) parsing table of Figure 5.4.11 on the following page. Figure 5.4.12 on page 115 shows the behaviour of the pushdown automaton of Figure 5.2.1 on page 84 while parsing the input string a a b b $ which is read from left to right using the parsing table of Figure 5.4.11 on the following page. In Figure 5.4.12 we use the same conventions of Figure 5.2.5 on page 90 and, in particular, the symbol N points at the input character at hand. The stack grows and shrinks at the right end, and the symbol N points at the top of the stack.  114 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S →  A, {$} B →  a B, {$,a,b} q0 : A →  B A, {$} B →  b, {$,a,b} A →  , {$} A q6 : B → a B  , {$,a,b} a B b a B B → a  B, {$,a,b} q3 : B →  a B, {$,a,b} B →  b, {$,a,b} a B q1 : S → A  , {$} A → B  A, {$} B →  a B, {$,a,b} q2 : A →  B A, {$} B →  b, {$,a,b} A →  , {$} b b q4 : B → b  , {$,a,b} A q5 : A → B A  , {$} Figure 5.4.10. The finite automaton M for the LR(1) parsing of the grammar G′ with axiom S and productions: 0. S → A, 1. A → B A, 2. A → ε, 3. B → a B, 4. B → b. The label of each state is a set of items which is computed by the Powerset Construction Procedure from the graph of Figure 5.4.9 on the previous page. q0 q1 q2 q3 q4 q5 q6 action goto a b $ sh q3 sh q4 red 2 acc sh q3 sh q4 red 2 sh q3 sh q4 red 4 red 4 red 4 red 1 red 3 red 3 red 3 A B q1 q2 q5 q2 q6 where: 0. 1. 2. 3. 4. S→A A→BA A→ε B → aB B→b Figure 5.4.11. LR(1) parsing table for the grammar G′ with axiom S and productions: 0. S → A, 1. A → B A, 2. A → ε, 3. B → a B, 4. B → b. It has been derived from the finite automaton of Figure 5.4.10. 5.4. LR(1) PARSERS time 0 1 2 3 4 5 6 7 8 9 10 11 stack (the top is marked by N) q0 N q0 a q3 N q0 a q3 a q3 N q0 a q3 a q3 b q4 N q0 a q3 a q3 B q6 N q0 a q3 B q6 N q0 B q2 N q0 B q2 b q4 N q0 B q2 B q2 N q0 B q2 B q2 A q5 N q0 B q2 A q5 N q0 A q1 N input (the symbol at hand is marked by N) aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N aabb$ N 115 shift/reduce-goto action(q0 , a) = sh q3 action(q3 , a) = sh q3 action(q3 , b) = sh q4 action(q4 , b) = red 4 (B → b) goto(q3 , B) = q6 action(q6 , b) = red 3 (B → a B) goto(q3 , B) = q6 action(q6 , b) = red 3 (B → a B) goto(q0 , B) = q2 action(q2 , b) = sh q4 action(q4 , $) = red goto(q2 , B) = q2 action(q2 , $) = red goto(q2 , A) = q5 action(q5 , $) = red goto(q2 , A) = q5 action(q5 , $) = red goto(q0 , A) = q1 4 (B → b) 2 (A → ε) 1 (A → B A) 1 (A → B A) action(q1 , $) = accept Figure 5.4.12. Sequence of configurations of the pushdown automaton of Figure 5.2.1 on page 84 while parsing the string a a b b $ using the LR(1) parsing table of Figure 5.4.11 on the facing page, for the grammar G′ with axiom S and productions: 0. S → A, 1. A → B A, 2. A → ε, 3. B → a B, 4. B → b. Example 5.4.12. [LR(1) Parsing: Example 3] Let us consider the grammar G with axiom S and the following production only: 1. S → ε The augmented grammar G′ with axiom S has the productions: 0. S ′ → S 1. S → ε We get the following finite automaton: q0 : S ′ →  S, {$} S →  ε, {$} S q1 : S ′ → S  , {$} 116 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS and the following LR(1) parsing table for the grammar G′ : action goto a q0 q1 $ red 1 acc S q1 where: 0. S ′ → S 1. S → ε The following Figure 5.4.13 shows the configurations of the pushdown automaton of Figure 5.2.1 on page 84 while parsing the string ε $ (that is, $).  stack (the top is marked by N) input (the symbol at hand is marked by N) 0 q0 N $ N action(q0 , $) = red 1 (S → ε) goto(q0 , S) = q1 1 q0 S q1 N $ N action(q1 , $) = accept time shift/reduce-goto Figure 5.4.13. Sequence of configurations of the pushdown automaton of Figure 5.2.1 on page 84 while parsing the string $ using the LR(1) parsing table for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → ε. Now we state the following results concerning the LR(k) grammars and LR(k) languages. For the proofs the interested reader may refer to [3, 4, 10]. Theorem 5.4.13. [Hierarchy of LR(k ) Grammars] For any k ≥ 0, the class of the LR(k) grammars is properly contained in the class of the LR(k + 1) grammars. Theorem 5.4.14. [Collapse of LR(k ) Languages for k ≥ 1] (i) The class LR(0) of languages is properly contained in the class of the LR(1) languages. (ii) For any k ≥ 1, the class of LR(k) languages is equal to the class of LR(k + 1) languages (see also Fact 5.8.10 on page 141). The following theorem is important because it relates the class of the LR(1) languages to the class of the deterministic context-free languages (see also Theorem 5.2.16 on page 93). Theorem 5.4.15. [LR(1) Grammars, LR(1) Languages, and Deterministic Context-Free Languages] (i) A language L is a deterministic contextfree language iff there exists k ≥ 1 such that L is generated by an LR(k) grammar. (ii) A language L is a deterministic context-free language iff it is generated by an LR(1) grammar. Point (ii) of this Theorem 5.4.15 follows from Point (i) of the same theorem and from Theorem 5.4.14. For related results and results concerning ambiguity of LR(1) grammars see Fact 5.8.11 on page 141 and Fact 5.8.12 on page 141. 5.4. LR(1) PARSERS 117 Theorem 5.4.16. [LR(1) Grammars in Chomsky and Greibach Normal Form] (i) Every deterministic context-free language has an LR(1) grammar in Chomsky normal form. (ii) Every deterministic context-free language has an LR(1) grammar in Greibach normal form [4, page 708] and [9, page 567]. The following result refers to LR(k) grammars and has been already mentioned with respect to LR(k) languages (see Fact 4.3.19 on page 77). Reduced grammars have been introduced in Definition 1.2.15 on page 15. Theorem 5.4.17. [Reduced LL(k ) Grammars Are Properly Included in Reduced LR(k) Grammars] For any k ≥ 0, the class of reduced LL(k) grammars is properly contained in the class of reduced LR(k) grammars [25, page 248]. Let us close this section by recalling a property of the LR(k) parsers when they make either a shift action or a reduce-goto action. LR(k) Parsing, for k ≥ 0. The action to take by the LR(k) parser (either a shift action or a reduce-goto action) is uniquely determined by (see Figure 5.4.14): (i) the top of the stack, which is either a state or a symbol in VT ∪ VN ∪ {$}, (ii) the leftmost k (≥ 0) symbols of z (or z itself if |z| < k), and (iii) the current symbol u 1 and the leftmost k (≥ 0) symbols of u (or u itself if |u| < k). S sentential form pAz : p input string pu$ : p rightmost derivation z A Follow k (A) : first k symbols of z β u $ u k : first k symbols of u Figure 5.4.14. LR(k) parsing, for k ≥ 0. Note that, if k = 0, the action of the LR(0) parser is a function of the current symbol which is pointed by the input head, that is, the symbol u 1 . The derivation from S to p A z is a rightmost derivation. Recall that Follow k (A) depends on the given grammar only. 118 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS 5.4.1. Avoiding the Powerset Construction for LR(1) Parsing. Analogously to the case of the LR(0) parsing (see Section 5.2.1 on page 94), we can avoid the application of the Powerset Construction Procedure at Step 4 of Algorithm 5.4.1 (see page 103) by using the following rules for constructing the finite automaton M which is the outcome of that Step 4. These rules should be applied at the end of Step 1 of Algorithm 5.4.1 (see page 102), that is, after the construction of the augmented grammar G′ of the given context-free grammar G. During the application of these rules, in any given state, for any n ≥ 2, we can replace n items of the form: A → γ, L1 , . . ., A → γ, Ln , where A ∈ VN and γ ∈ (VT ∪ VN )∗ ∪ {  } ∪ (VT ∪ VN )∗ (these items differ for the lookahead sets only) by a single item of the form: A → γ, L1 ∪ . . . ∪ Ln (see Remark 5.4.6 on page 105). The correctness of these rules is a consequence of the correctness of the application of the Powerset Construction Procedure. The Closure Rules (1), (2), and (3) should be applied as long as possible. Initialization Rule. The initial state q0 of the finite automaton M has its set of items initialized by the item S ′ →  S, {$} . If the axiom S does not occur on the right hand side of any production, and we take the augmented grammar G′ to be G itself, then the label of the initial state q0 of the finite automaton M has an item of the form S →  α1 . . . αn , {$} for each production of the form S → α1 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪VN . Closure Rule (1). For all A, B ∈ VN and α, β, γ ∈ V ∗ , if in the label of a state there is an item of the form A → α  Bβ, L , then we add to that label all items of the form B →  γ, L′ , where B → γ is a production of the extended grammar G′ and the lookahead set L′ is constructed as follows: L′ = if β = ε then L if β 6= ε and β →∗ ε then L ∪ (First 1 (β)−{ε}) if β 6= ε and β 6→∗ ε then First 1 (β) Closure Rule (2). For all A1 , . . . , An , B ∈ VN , α1 , β1 , . . . , αn , βn ∈ V ∗ , and lookahead sets L1 , . . . , Ln , if all the items in the label of a state p with the dot immediately to the left of the nonterminal B are: A1 → α1  Bβ1 , L1 , . . . , An → αn  Bβn , Ln , for some n > 0, then we add to M: (i) one new state, call it q, whose label includes the items A1 → α1 B  β1 , L1 , . . . , An → αn B  βn , Ln , and (ii) one new arc with label B from the state p to the state q. 5.4. LR(1) PARSERS 119 Closure Rule (3). For all a ∈ VT , A1 , . . . , An ∈ VN , α1 , β1 , . . . , αn , βn ∈ V ∗ , and lookahead sets L1 , . . . , Ln , if all the items in the label of a state p with the dot immediately to the left of the terminal a are: A1 → α1  aβ1 , L1 , . . . , An → αn  aβn , Ln , for some n > 0, then we add to M: (i) one new state, call it q, whose label includes the items A1 → α1 a  β1 , L1 , . . . , An → αn a  βn , Ln , and (ii) one new arc with label a from the state p to the state q. The final state of the finite automaton M is the one whose label has the item S ′ → S  , {$} . If the axiom S does not occur on the right hand side of any production, and we take the augmented grammar G′ to be G itself, then every state of the finite automaton M whose label has an item of the form S → α1 . . . αn  , {$} , for some production of the form S → α1 . . . αn , where n ≥ 0 and for i = 1, . . . , n, αi ∈ VT ∪VN , is a final state. 5.4.2. More Remarks on the Hypotheses for LR(k ) Parsing. Before closing this section on LR(1), we would like to add some more remarks (besides those made on Section 5.2.2 on page 95) on the hypotheses we made on page 82 for the LR(k) parsing algorithms, for k ≥ 0. In particular, we will show that there are grammars for which the LR(1) parsing table is not correct, if the start symbol occurs on the right hand side of a production. Here is an example. Let us consider the grammar G with axiom S and productions: 1. S → a S 2. S → b If we construct the finite automaton M for generating the LR(1) parsing table for that grammar and we assume that in the initial state of M there are the items S →  a S, {$} and S →  b, {$} (see Figure 5.4.15 on the next page), as indicated at the end of Section 5.2.2 on page 95, then we get an LR(1) parsing table (see Figure 5.4.16 on the next page) which allows the correct parsing of every word in the language L(G) generated by G. However, when accepting a word w ∈ L(G), the sequence of stack configurations does not show the rightmost derivations of w from S in the reverse order (see Figure 5.4.17 on the following page). Note that, even if the axiom occurs on the right hand side of a production, we may get from a given context-free grammar, an LR(1) parsing table which allows the correct parsing of every word generated by that grammar. Indeed, let us consider the grammar G1 with axiom S and the following productions: 1. S → A 2. A → S a 3. A → b 120 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS S →  a S, {$} q0 : S →  b, {$} S → a  S, {$} q1 : S →  a S, {$} S →  b, {$} a S q2 : S → a S  , {$} b b a q3 : S → b  , {$} Figure 5.4.15. The finite automaton M for the LR(1) parsing of the grammar G with axiom S and productions: 1. S → a S, 2. S → b. action a goto b $ q0 sh q1 sh q3 q1 sh q1 sh q3 S q2 q2 acc q3 acc where: 1. S → a S 2. S → b Figure 5.4.16. LR(1) parsing table for the grammar G with axiom S and productions: 1. S → a S, 2. S → Sb. It has been derived from the finite automaton of Figure 5.4.15. time stack (the top is marked by N) input (the symbol at hand is marked by N) shift/reduce-goto 0 q0 N ab$ N action(q0 , a) = sh q1 1 q0 a q1 N a b$ N action(q1 , b) = sh q3 2 q0 a q1 b q3 N ab $ N action(q3 , $) = acc Figure 5.4.17. The sequence of configurations of the parsing automaton of Figure 5.4.16 while parsing the string a b $ using the LR(1) parsing table of Figure 5.4.15 for the grammar G with axiom S and productions: 1. S → a S, 2. S → b. The language generated by this grammar is the regular language denoted by the regular expression b a∗ . The finite automaton M to be constructed for generating the LR(1) parsing table, is shown in Figure 5.4.18 on the facing page. 5.4. LR(1) PARSERS S →  A, {$} q0 : A →  S a, {$} A →  b, {$} S →  A, {a} A →  S a, {a} A →  b, {a} S A b q3 : A → b  , {$, a} 121 q1 : A → S  a, {$, a} a q2 : A → S a  , {$, a} q4 : S → A  , {$, a} Figure 5.4.18. The finite automaton M for the LR(1) parsing of the grammar G1 with axiom S and productions: 1. S → A, 2. A → S a, 3. A → b. The corresponding parsing table is depicted in Figure 5.4.19. Note that in row q4 and column a we have inserted the action red 1, even if state q4 is final (recall Step (5.4) on page 105). action a goto b $ sh q3 q0 S A q1 q4 where: q1 sh q2 q2 red 2 red 2 q3 red 3 red 3 q4 red 1 acc 1. S → A 2. A → S a 3. A → b Figure 5.4.19. LR(1) parsing table for the grammar G1 with axiom S and productions: 1. S → A, 2. A → S a, 3. S → b. It has been derived from the finite automaton of Figure 5.4.18. This parsing table does allow the correct parsing of every word in L(G1). In particular, if we parse the word b a $ which belongs to b a∗ $, we get the sequence of stack configurations depicted in Figure 5.4.20 on the next page. We leave it to the reader to check that the grammar G1′ obtained from the grammar G1 by adding the production S ′ → S, so that the axiom S ′ does not occur on the right hand side of any production, is an LR(1) grammar. Thus, from the grammar G1′ we get an LR(1) parsing table which allows the correct parsing of every word in L(G1′ ). The grammar G1′ has the following productions: 0. S ′ → S 1. S → A 2. A → S a 3. A → b 122 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS time stack (the top is marked by N) input (the symbol at hand is marked by N) shift/reduce-goto 0 q0 N ba$ N action(q0 , b) = sh q3 1 q0 b q3 N b a$ N action(q3 , a) = red 3 (A → b) goto(q0 , A) = q4 2 q0 A q4 N b a$ N action(q4 , a) = red 1 (S → A) goto(q0 , S) = q1 3 q0 S q1 N b a$ N action(q1 , a) = sh q2 4 q0 S q1 a q2 N ba $ N action(q2 , $) = red 2 (A → S a) goto(q0 , A) = q4 5 q0 A q4 N ba $ N action(q4 , $) = accept Figure 5.4.20. The sequence of configurations of the parsing automaton of Figure 5.4.18 on the preceding page while parsing the string b a $ using the LR(1) parsing table of Figure 5.4.19 on the previous page for the grammar G1 with axiom S and productions: 1. S → A, 2. A → S a, 3. A → b. Let us make a final note concerning the power of unfolding. By unfolding a grammar which is not an LR(1) grammar can be transformed into an equivalent LR(1) grammar. Consider, for instance, the grammar with axiom S ′ and productions: 0. S ′ → S 1. S → c 2. S → A 3. A → c This grammar is not an LR(1) grammar (in particular, the word c has two rightmost derivations). By unfolding S and A, we get a grammar with the single production: 0. S ′ → c and, obviously, this grammar is an LR(1) grammar. Similarly, by unfolding a grammar which is not an LR(0) grammar can be transformed into an equivalent LR(0) grammar. Consider, for instance, the grammar with axiom S ′ and productions: 0. S ′ → S $ 1. S ′ → A $ 2. S → c 3. A → c This grammar is not an LR(0) grammar (in particular, the word c has two rightmost derivations). By unfolding S and A, we get a grammar with the single production: 0. S ′ → c $ and obviously this grammar is an LR(0) grammar. 5.5. LALR(1) PARSERS 123 5.5. LALR(1) Parsers In this section we will study the class of LALR(1) grammars and we will present a technique for parsing the languages generated by those grammars. Basically, LALR(1) parsing is like LR(1) parsing, but it uses parsing tables which are derived by compaction from the LR(1) parsing tables. There are algorithms for directly constructing the tables for LALR(1) parsing without first constructing the tables for LR(1) parsing [2, page 242], but we do not consider them here. The compaction of an LR(1) parsing table which makes it an LALR(1) parsing table is performed, as we now indicate, on the finite automaton from which the table is derived. Algorithm 5.5.1. Constructing the LALR(1) Parsing Table. Given a context-free grammar G = hVT , VN , P, Si, where VT = {a1 , . . . , ar } and VN = {A1 , . . . , As }, we consider its augmented grammar G′ with the new start symbol S ′ , and the finite automaton M which can be constructed from G′ by applying Algorithm 5.4.1 on page 102 for constructing the LR(1) parsing tables. Then, we replace as long as possible (and maybe no times) any subset {q1 , . . . , qn } of the states of M of the form: q1 : item 1 , L11 ··· item m , L1m , ... , qn : item 1 , Ln1 ··· item m , Lnm (that is, any subset of states with the same first component) by the following single state q1...n q1...n item 1 , L11 ∪ . . . ∪ Ln1 : ··· item m , L1m ∪ . . . ∪ Lnm After each of these replacements which can be viewed as ‘fusions of states’, we readjust the arcs, that is, we replace, for all k = 1, . . . , n, (i) any arc from a state qk by an arc from the new state q1...n , and (ii) any arc to a state qk by an arc to the new state q1...n . Each of the above replacements of states in the finite automaton M, corresponds in the LR(1) parsing table T to the replacement of a set {q1 , . . . , qn } of rows of the form: 124 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS action a1 ... ar goto $ A1 ... As q1 : .. . T [1, a1 ] . . . T [1, ar ] T [1, $] .. .. .. . . . T [1, A1 ] . . . T [1, As ] .. .. . . qn : T [n, a1 ] . . . T [n, ar ] T [n, $] T [n, A1 ] . . . T [n, As ] where some of the T [i, j] entries may be absent, by the following single row q1...n goto action a1 q1...n : ... ar $ A1 ... As T [1, a1 ], T [1, ar ], T [1, $], T [1, A1 ], T [1, As ], .. .. .. .. .. ., ., ., ., ., ... ... T [n, a1 ] T [n, ar ] T [n, $] T [n, A1 ] T [n, As ] where in each position of the row q1...n we have kept one copy only for each distinct entry, in the sense that for each column j = a1 , . . . , ar , $, A1 , . . . , As , and for each i1 , i2 = 1, . . . , n, we have that T [i1 , j] 6= T [i2 , j]. At the end of all these row replacements, if in the resulting parsing table T for each row i and for each column j = a1 , . . . , ar , $, A1 , . . . , As , there exists at most one element (that is, there are no conflicts), then the parsing table T is said to be an LALR(1) parsing table. In this case, that is, in the case when there are no conflicts, the augmented grammar G′ is an LALR(1) grammar and the language generated by G′ is an LALR(1) language. Remark 5.5.2. [Multiple actions] As for LR(1) grammars, if we have to place more than one action in an entry of the LALR(1) parsing table, then there are conflicts and the language generated by the given grammar cannot be parsed by using a pushdown automaton which uses that table, and the augmented grammar G′ which has been derived from the given grammar G, is not an LALR(1) grammar. Remark 5.5.3. [Undefined entry] If during the process of parsing a given input word w $, the pushdown automaton requires an entry of the LALR(1) parsing table, but that entry is empty, then the word w does not belong to the language generated by the augmented grammar G′ and w does not belong to the language generated by the given grammar G. Example 5.5.4. [LALR(1) Parsing: Example 1] In this example we construct the LALR(1) parsing table starting from the LR(1) parsing table of Figure 5.4.3 on page 110 relative to Example 5.4.9 on page 107. We have already constructed that LR(1) parsing table for the augmented grammar G′ : 5.5. LALR(1) PARSERS 125 0. S ′ → S 1. S → C C 2. C → c C 3. C → d For LALR(1) parsing we have the finite automaton of Figure 5.5.1, instead of the finite automaton of Figure 5.4.2 on page 109, and we have the LALR(1) parsing table of Figure 5.5.2 on the following page, instead of the LR(1) parsing table of Figure 5.4.3 on page 110. The states of the finite automaton of Figure 5.4.2 on page 109 which have been fused are: (i) states q3 and q6 , fused into the state q36 , (ii) states q4 and q7 , fused into the state q47 , and (iii) states q8 and q9 , fused into the state q89 .  S ′ →  S, {$} S →  C C, {$} q0 : C →  c C, {c, d} C →  d, {c, d} S q5 : S → C C  , {$} c d C c C S → C  C, {$} C q2 : →  c C, {$} C →  d, {$} q1 : S ′ → S  , {$} c C → c  C, {$,c,d} q36 : C →  c C, {$,c,d} C →  d, {$,c,d} C q89 : C → c C  , {$,c,d} d d q47 : C → d  , {$,c,d} Figure 5.5.1. The finite automaton for the LALR(1) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. The fusion of the states which is performed by Algorithm 5.5.1 on page 123 does not create conflicts in the goto part of the parsing table because in that part the entries depend only on the first components of the LR(1) items which are all equal in the states which are fused. One can show [2, page 237] that when applying Algorithm 5.5.1 for constructing the LALR(1) parsing tables, never shift-reduce conflicts may be produced, because shift actions depend on the first components of the LR(1) items only. On the contrary, reduce-reduce conflicts may be produced. One can also show [2, page 239–240] that for any input word which belongs to the language L(G′ ) generated by the augmented grammar G′ , the LALR(1) parser mimics 126 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS action c q0 d goto $ sh q36 sh q47 S C q1 q2 acc q1 q2 sh q36 sh q47 q5 q36 sh q36 sh q47 q89 q47 red 3 red 3 S′ → S S →CC C → cC C→d red 3 red 1 q5 q89 where: 0. 1. 2. 3. red 2 red 2 red 2 Figure 5.5.2. LALR(1) parsing table for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → C C, 2. C → c C, 3. C → d. It has been derived from the automaton of Figure 5.5.1 on the previous page. the corresponding LR(1) parser, in the sense that, with reference to Example 5.5.4 on page 124, it goes through the states q36 , q47 , and q89 iff the LR(1) parser goes through the states (q3 or q6 ), (q4 or q7 ), and (q8 or q9 ), respectively. One can show that for any input word w which does not belong to the language L(G′ ), the behaviour of the LR(1) parser and the corresponding LALR(1) parser, if there exists one, are similar, in the sense that after the LR(1) parser has detected an error, the LALR(1) parser may do some extra reduce actions (but not extra shift actions) and then it will also detect an error. Example 5.5.5. [LALR(1) Parsing: Example 2] Let us consider the following grammar G: 1. S → a A 2. S → b B 3. A → ε 4. A → c A d 5. B → ε and its augmented version G′ : 0. S ′ → S 1. S → a A 2. S → b B 3. A → ε 4. A → c A d 5. B → ε By applying Algorithm 5.4.1 on page 102 for constructing the LR(1) parsing tables, we get the finite automaton of Figure 5.5.3 on the facing page and the LR(1) parsing table of Figure 5.5.4 on page 128. By applying Algorithm 5.5.1 on page 123 for constructing the LALR(1) parsing tables from the LR(1) parsing tables, we get the finite automaton of Figure 5.5.5 on page 128 and the LALR(1) parsing table of Figure 5.5.6 on page 129 (see also [25, pages 34, 60]). The states of the finite automaton of Figure 5.5.3 on the next page which have been fused are: 5.5. LALR(1) PARSERS 127 (i) states q6 and q9 , fused into the state q6 9 , (ii) states q7 and q10 , fused into the state q7 10 , and (iii) states q8 and q11 , fused into the state q8 11 . Show that if in the given grammar G we replace the production A → c A d by the production A → c A c, then the resulting grammar is not LR(1).  S ′ →  S, {$} q0 : S →  a A, {$} S →  b B, {$} S b a S → a  A, {$} q3 : A →  , {$} A →  c A d, {$} q1 : S ′ → S  , {$} q2 : S → b  B, {$} B →  , {$} B q4 : S → b B  , {$} d q8 : A → c A d  , {$} A q5 : S → a A  , {$} A q7 : A → c A  d, {$} A q10 : A → c A  d, {d} d q11 : A → c A d  , {d} c A → c  A d, {$} q6 : A →  , {d} A →  c A d, {d} c c A → c  A d, {d} q9 : A →  , {d} A →  c A d, {d} Figure 5.5.3. The finite automaton for the LR(1) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → a A, 2. S → b B, 3. A → ε, 4. A → c A d, 5. B → ε. 128 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS action q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 a b sh q3 sh q2 goto c d $ acc red 5 red 3 red 2 red 1 sh q6 sh q9 red 3 sh q8 S q1 A B q4 q5 where: q7 0. 1. 2. 3. 4. 5. S′ → S S → aA S → bB A→ε A → cAd B→ε red 4 sh q9 red 3 sh q11 red 4 q10 Figure 5.5.4. LR(1) parsing table for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → a A, 2. S → b B, 3. A → ε, 4. A → c A d, 5. B → ε. It has been derived from the automaton of Figure 5.5.3 on the preceding page. S ′ →  S, {$} q0 : S →  a A, {$} S →  b B, {$} S b a S → a  A, {$} q3 : A →  , {$} A →  c A d, {$} c q1 : S ′ → S  , {$} q2 : A S → b  B, {$} B →  , {$} B q4 : S → b B  , {$} q5 : S → a A  , {$} c A → c Ad, {$,d} q6 9 : A →  , {d} A →  cAd, {d} A q7 10 : A → cA  d, {$,d} d q8 11 : A → cAd  , {$,d} Figure 5.5.5. The finite automaton for the LALR(1) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → a A, 2. S → b B, 3. A → ε, 4. A → c A d, 5. B → ε. 5.5. LALR(1) PARSERS action a q0 q1 q2 q3 q4 q5 q6 9 q7 10 q8 11 b c 129 goto d $ sh q3 sh q2 S A B q1 sh q6 9 sh q6 9 acc red 5 red 3 red 2 red 1 red 3 sh q8 11 red 4 red 4 q4 q5 where: 0. 1. 2. 3. 4. 5. S′ → S S → aA S → bB A→ε A → cAd B→ε q7 10 Figure 5.5.6. LALR(1) parsing table for the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → a A, 2. S → b B, 3. A → ε, 4. A → c A d, 5. B → ε. It has been derived from the automaton of Figure 5.5.5 on the preceding page. 130 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS Example 5.5.6. [LALR(1) Parsing: Example 3] Let us consider the augmented grammar G′ : 0. S ′ → S 1. S → L = R 2. S → R 3. L → ∗R 4. L → a 5. R → L By applying Algorithm 5.4.1 on page 102 we get the finite automaton of Figure 5.5.7 and the LR(1) parsing table of Figure 5.5.8 on the facing page. By applying Algorithm 5.5.1 on page 123 for constructing the LALR(1) parsing table from the LR(1) parsing table, we have to make no changes to that parsing table and thus, the table of Figure 5.5.8 is also the LALR(1) parsing table of the grammar G′ .  S ′ →  S, S →  L = R, S →  R, q0 : L →  ∗R, L →  a, R →  L, {$} {$} {$} {$,=} {$,=} {$} S q1 : S ′ → S  , {$} L S → L = R, {$} R → L, {$} S → L  = R, {$} = q6 : R q2 : L →  ∗R, {$} R → L , {$} L → a, {$} a ∗ q3 : S → R  , {$} R a ∗ L q5 : L → a  , {$,=} q9 : S → L = R  , {$} ∗ q8 : R → L  , {$,=} a L → ∗R, {$,=} R → L, {$,=} L R q4 : q7 : L → ∗R  , {$,=} L →  ∗R, {$,=} L → a, {$,=} Figure 5.5.7. The finite automaton for the LR(1) parsing of the grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → L = R, 2. S → R, 3. L → ∗ R, 4. L → a, 5. R → L. This automaton is also the LALR(1) parsing automaton for this grammar G′ . 5.5. LALR(1) PARSERS action a q0 = sh q5 goto ∗ $ sh q4 L R acc sh q6 q2 red 5 red 2 q3 q4 sh q5 sh q4 q5 red 4 sh q5 q8 q7 red 4 sh q4 q8 q9 q7 red 3 red 3 q8 red 5 red 5 q9 S q1 q2 q3 q1 q6 131 where: 0. 1. 2. 3. 4. 5. S′ → S S→L=R S→R L → ∗R L→a R→L red 1 Figure 5.5.8. LR(1) parsing table for the augmented grammar G′ with axiom S ′ and productions: 0. S ′ → S, 1. S → L = R, 2. S → R, 3. S → ∗R, 4. L → a, 5. R → L. This table is also the LALR(1) parsing table for this grammar G′ . It has been derived from the automaton of Figure 5.5.7 on the facing page. Example 5.5.7. [LALR(1) Parsing: Example 4] Let us consider the grammar G with axiom S and the following productions: 1. S → B B a 2. B → B b 3. B → c Since the axiom S has one production only and S does not occur on the right hand side of any production, we can take the given grammar G to be the augmented grammar of G itself. By applying Algorithm 5.4.1 on page 102 for constructing the LR(1) parsing tables and recalling that First 1 (B) = {c}, we get the finite automaton of Figure 5.5.9 on the following page and the LR(1) parsing table of Figure 5.5.10 on the next page. By applying Algorithm 5.5.1 on page 123 for constructing the LALR(1) parsing tables from the LR(1) parsing tables, we get the finite automaton of Figure 5.5.11 on page 133 and the LALR(1) parsing table of Figure 5.5.12 on page 133. The states of the finite automaton of Figure 5.5.9 on the next page which have been fused are: (i) states q4 and q5 , fused into the state q45 , and (ii) states q6 and q7 , fused into the state q67 .  132 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS B →  B b, {a} S → B  Ba, {$} B q : B → B  b, {c} B →  c, {a} 1 B →  B b, {b} B → B  b, {b} B →  c, {b} S →  BBa, {$} B →  B b, {b} q0 : B →  B b, {c} B →  c, {b} B →  c, {c} c c B → c  , {c} q4 : B → c  , {b} b B → c  , {a} q5 : B → c  , {b} B S → B B  a, {$} q2 : B → B  b, {a} B → B  b, {b} q6 : q7 : B → B b  , {c} B → B b  , {b} B → B b  , {a} B → B b  , {b} b a q3 : S → B B a  , {$} Figure 5.5.9. The finite automaton for the LR(1) parsing of the grammar G with axiom S and productions: 1. S → B B a, 2. B → B b, 3. B → b. action q0 q1 q2 q3 q4 q5 q6 q7 a b sh q3 sh q6 sh q7 c sh q4 sh q5 goto $ acc red red 3 red red red 2 red 3 red 3 3 2 red 2 2 B q1 q2 where: 1. S → B B a 2. B → B b 3. B → c Figure 5.5.10. LR(1) parsing table for the grammar G with axiom S and productions: 1. S → B B a, 2. B → B b, 3. B → b. It has been derived from the finite automaton of Figure 5.5.9. 5.5. LALR(1) PARSERS S →  BBa, {$} B →  B b, {b} q0 : B →  B b, {c} B →  c, {b} B →  c, {c} c 133 B →  B b, {a} S → B  Ba, {$} B q : B → B  b, {c} B →  c, {a} 1 B →  B b, {b} B → B  b, {b} B →  c, {b} c B b B → c  , {a} q45 : B → c  , {b} B → c  , {c} S → B B  a, {$} q2 : B → B  b, {a} B → B  b, {b} B → B b  , {a} q67 : B → B b  , {b} B → B b  , {c} b a q3 : S → B B a  , {$} Figure 5.5.11. The finite automaton for the LALR(1) parsing of the grammar G with axiom S and productions: 1. S → B B a, 2. B → B b, 3. B → b. action q0 q1 q2 q3 q45 q67 a b sh q3 sh q67 sh q67 goto c sh q45 sh q45 $ acc red 3 red 2 red 3 red 2 B q1 q2 where: 1. S → B B a 2. B → B b 3. B → c red 3 red 2 Figure 5.5.12. LALR(1) parsing table for the grammar G with axiom S and productions: 1. S → B B a, 2. B → B b, 3. B → b. It has been derived from the finite automaton of Figure 5.5.11. 134 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS 5.6. Time and Space Complexity Results for Context-Free Parsing We have the following complexity result [3, page 395]. Fact 5.6.1. [Parsing of LR(k ) Languages in Linear Time] Any LR(k) language can be parsed in O(n) time, where n is the length of the input word to be parsed. We assume that time complexity is given by the number of shift and reducegoto actions performed on the stack. Proof. We will consider the case of k = 1 (for k = 0 and k > 1 the proof is similar and we leave it to the reader). Let us consider the LR(1) grammar G = hVT , VN , P, Si and the pda that performs the LR(1) parsing of an input word w ∈ VT∗ , using the LR(1) parsing table for the grammar G. Let us introduce the following definitions which apply to every pda that performs the LR(1) parsing. A p-configuration (short for parsing configuration) of a pda is a pair of the form hs, vi, where: (i) s is a string in Q ((VT ∪ VN ) Q)∗ , where Q is the set of states of the pda, which denotes the current stack of the pda, and (ii) v is the suffix of the input word w which remains to be read by the pda. In a p-configuration hs, vi the rightmost symbol of s is the current state of the pda and the leftmost symbol of v is the symbol which is currently read. The initial p-configurations is hq0 , wi, where q0 is the initial state of the pda and w is the word to be parsed. A sequence of p-configurations of a pda is the one determined by the sequence actions (each of which is either a shift action or a reduce-goto action) performed by the pda while parsing a given input word. A sequence of c-configurations of a pda is a subsequence of the sequence of the p-configurations of the pda, starting from the initial p-configuration hq0 , wi by considering the following p-configurations only: - the initial p-configuration hq0 , wi, - every p-configuration immediately after a shift action, and - every p-configuration immediately after a reduce-goto action that makes the stack shorter than the stack of the previous c-configuration of the sequence. First, note that during the LR(1) parsing of an input word w, the length of the sequence of the c-configurations of the parsing pda cannot be greater than 1+3|w|. Indeed, if we associate with every c-configuration hs, vi the integer |s|+3|v|, then in any given sequence of c-configurations, from every c-configuration to the next, this integer decreases by at least one unit (recall that in a shift action two symbols are pushed onto the stack), and the integer associated with the initial c-configuration hq0 , wi is 1 + 3|w| (the summand 1 is due to symbol q0 which, initially, is the only symbol on the stack). Thus, there exist at most 1 + 3|w| c-configurations and, in order to show that LR(1) parsing can be done in linear time with respect to |w|, it remains to show that between any two c-configurations, the pda may perform a sequence of actions whose maximal length is independent of the input word w to be parsed. More formally, it remains to show that there exists a constant k such that for all c-configurations γ, 5.6. TIME AND SPACE COMPLEXITY RESULTS FOR CONTEXT-FREE PARSING 135 there exists h, with 0 ≤ h ≤ k, such that the pda, starting from γ with stack s, after performing a sequence σ of h actions, either (i) stops by accepting w or rejecting w, or (ii) performs a shift action, thereby constructing a new c-configuration, or (iii) performs a reduce-goto action that makes the length of the stack smaller than |s|, thereby constructing a new c-configuration, or (iv) h = k and every action in σ is a reduce-goto action that makes the length of the stack equal to or greater than |s|. As we will see below, we can take the constant k to be |Q|2 ×|VN |. Now, in Cases (i), (ii), and (iii) it is straightforward to see that the parsing is done in linear time. Also in Case (iv) the parsing is done in linear time because, as we now show, the pda enters an infinite run of ε-moves on the input, and thus, after at most k actions, the parsing process can be stopped and the input word w can be rejected. Let us first state the following two facts whose proof is immediate. These facts will help the reader to understand the analysis of Case (iv) we will do below. Fact (α). If the pda performs a reduce-goto action using an ε-production of the form A → ε, then the stack becomes longer (say, from stack s to stack s A q, for some nonterminal A and some state q). There is no other single reduce-goto action which makes the stack longer. Fact (β). If the pda performs a reduce-goto action by using a unit production of the form A′ → A, then the stack remains of the same length (say, from stack s A q to stack s A′ q ′ , for some nonterminals A and A′ and some states q and q ′ ). There is no other single reduce-goto action which keeps the stack of the same length. Let us suppose that Case (iv) occurs. Without loss of generality, we may assume that at the beginning of the sequence σ of actions, the stack s of the c-configuration γ has at least three symbols, because since Case (i) does not hold, the pda does not stops and the first action it performs is a reduce-goto action using an ε-production and this action leaves three symbols on the resulting stack. Let us then assume that at the beginning of the sequence σ, the stack s has three topmost symbols and let they be p A q, the topmost one being q. Then, there exists an action of the sequence σ after which the pda constructs a stack s′ with the same three topmost symbols p A q such that |s′ | ≥ |s|. This is a consequence of the following facts. Fact (1): during σ the pda always performs reduce-goto actions (that is, ε-moves on the input) and thus, it always uses the same column of the LR(1) parsing table, Fact (2): starting from the stack s, a sequence of ε-moves each of which makes the length of the stack equal to or greater than |s|, determines a sequence of stacks which depends only on the LR(1) parsing table and the three topmost symbols of the stack s (not on other symbols of that stack), and Fact (3): there are at most |Q|2 ×|VN | different strings of the form p A q which may occur at the top of the stack s, and Fact (4): the length of the sequence σ is at least |Q|2 ×|VN | (note that a sequence of h actions determines a sequence of h + 1 stacks if we take into account also 136 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS the stack of the configuration from which the pda performs the first action of the sequence). The construction of the stack s′ from the stack s shows that, if Case (iv) occurs, then the pda enters an infinite run of ε-moves (and thus, the input word w can be rejected). Indeed, since the pda is deterministic, if the pda constructs the stack s′ from the stack s, then it will also construct a stack s′′ from the stack s′ , because the stacks s and s′ have the same three topmost symbols (recall Fact (2) above). Therefore, an infinite sequence of stacks will be constructed by performing always ε-moves.  In Table 1 we give the measures of the time-complexity and the space-complexity for testing a grammar and constructing a parser for the following subclasses of context-free grammars: LL(1) grammars, SLR(1) grammars, LR(1) grammars, and LALR(1) grammars [25, page 400]. In that table the size n of the given input grammar is assumed to be the total number of occurrences of symbols in its productions. deterministic time for testing a grammar deterministic time for constructing the parser 2 +4 LL(1) O(n2 ) O(2n SLR(1) O(n2 ) O(2n + 3 log n ) LR(1) O(n3 ) O(2n LALR(1) O(2 n + 3 log n ) O(2 2 +4 log n log n n + 3 log n ) ) ) size of the parser O(2n 2+2 log n ) O(2n + 2 log n ) O(2n O(2 2+2 log n n + 2 log n ) ) Table 1. Deterministic time and space complexities (that is, time and space taken by a deterministic Turing Machine) for testing a grammar of size n and for constructing a parser of various classes of grammars: LL(1), SLR(1), LR(1), and LALR(1). We have that the problem, which we call P 1, of testing whether or not given an arbitrary context-free grammar G and an integer k ≥ 1 in unary notation, the grammar G is not an LL(k) grammar is NP-complete. The same complexity result holds if we replace LL(k) by: (i) SLR(k), thereby getting a problem which we call P 2, or (ii) LR(k), thereby getting a problem which we call P 3. However, if we replace LL(k) by LALR(k), we get a problem which we call P 4, and we have that problem P 4 is PSPACE-complete [25, Table 10.1 on page 399]. For the complexity measures of these problems P 1–P 4 the size of the input is assumed to be k (in unary notation) plus the size of the grammar G. Note that since PSPACE is a complexity class which refers to deterministic computations, also the complement of Problem P 4 which is the problem of testing whether 5.7. CONVENTIONS FOR LL(k ) AND LR(k ) PARSING 137 or not given an arbitrary context-free grammar G and an integer k ≥ 1 in unary notation, the grammar G is an LALR(k) grammar, is PSPACE-complete [25, page 398]. If k (≥ 1) is given in binary notation (not in unary notation), then Problems P 1–P 4 all become NEXPTIME-complete (recall that the class of NEXPTIME probS k lems is k≥0 NTIME(2n ).) Note also that the entries of Table 1 on the preceding page are consistent with the complexity results we have now stated about Problems P 1–P 4, because the algorithms which justify those entries, take advantage of the fact that k is 1. Finally, recall that the problem to decide whether or not, given an arbitrary context-free grammar G, there exists k ≥ 0 such that G is an LL(k) grammar, is unsolvable. The same unsolvability result holds if we replace LL(k) by SLR(k), or by LR(k), or by LALR(k) [25, page 386]. 5.7. Conventions for LL(k ) and LR(k ) Parsing In Table 2 below we recall some hypotheses we made concerning the parsing of various kinds of LL(k) languages and LR(k) languages and, in particular: (i) (ii) (iii) (iv) the the the the use of a rightmost, new symbol $ in the input string, use of augmented grammars with the production of the axiom, initial stack configuration, and lookahead sets. Obviously, when we consider augmented grammars, we have that the new axiom S ′ does not occur on the right hand side of any production. input string augmented grammar production of the axiom LL(k) ended by $ no axiom S LR(0) and SLR(1) ended by $ yes LR(1) and LALR(1) ended by $ yes axiom S ′ add: S ′ → S $ axiom S ′ ′ add: S → S initial stack lookahead set S$ N none N q0 none q0 {$} N Table 2. Our conventions on the input string, the augmented grammar with the production of the axiom, the initial stack configuration (q0 is the initial state), and the lookahead set for various classes of context-free grammars. 138 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS 5.8. Subclasses of Context-free Languages This section is devoted to the illustration of Figure 5.8.1 on page 142 in which we consider various subclasses of the context-free languages and their relationships. We will also discuss some other relationships among subclasses of context-free languages. We have already seen on page 91 that the grammar G with axiom S and the following productions: S→A | B A → aAb | 0 B → aB bb | 1 is LR(0) and it is not LL(k), for any k ≥ 0. The language generated by that grammar is: {an 0 bn | n ≥ 0} ∪ {an 1 (b b)n | n ≥ 0}. It is a deterministic context-free language which enjoys the prefix property. This fact shows the proper containment between the deterministic context-free languages and the union of the LL(k) languages, for all k ≥ 0, as indicated in Figure 5.8.1. The following notion is necessary for presenting Fact 5.8.2 below. It relates SLR(k) grammars and LR(k) grammars. Definition 5.8.1. [Structurally Equivalent Grammars] Two unambiguous context-free grammars G1 and G2 are said to be structurally equivalent iff L(G1 ) = L(G2 ) and for every word w ∈ L(G1 ) (which is equal to L(G2 )) the derivation trees of w in G1 and G2 are the same except, possibly, for the labeling of the nodes with nonterminal symbols. For instance, the following two grammars G1 and G2 are structurally equivalent: (i) the grammar G1 has axiom S and productions S → Ac | bA A→ε (ii) the grammar G2 has axiom S and productions S → Ac | bB A→ε B→ε Indeed, in particular, for the word b we have the following two derivation trees which differ only for the labeling of a node with a nonterminal symbol. In the grammar G1 : In the grammar G2 : S A b ε S B b ε 5.8. SUBCLASSES OF CONTEXT-FREE LANGUAGES 139 Fact 5.8.2. [Transforming LR(k ) Grammars into Equivalent SLR(k ) Grammars] For any k ≥ 0, any context-free grammar G can be transformed into a structurally equivalent grammar which is SLR(k) iff G is LR(k) [25, page 84]. Note that, an ambiguous grammar may generate a deterministic context-free language. Consider, for instance, the grammar G with axiom S and productions: S→A | a A→a It is an ambiguous grammar, but its language L(G) = {a} is a deterministic contextfree language (because it is a regular language) and it has an LR(0) parser [25, page 45]. Recall that if a word w has two distinct parse trees, then it has two distinct parse trees which are generated by leftmost derivations (or, equivalently, rightmost derivations). Now we will present some hierarchies of grammars (and languages) which are all subclasses of the context-free grammars (and languages). Unless otherwise specified, we will assume that the grammars are all reduced (see Definition 1.2.15 on page 15). Let us first recall the different ways in which one can present a class of grammars and, indeed, in this book various classes of grammars has been introduced in these different ways. Different ways of presenting of a class of grammars (or languages). A class of grammars (or languages) can be introduced in various ways. (i) A first way is by providing the rules for constructing the productions of the grammars of the class (for instance, the class of context-free grammars has been defined in this way by stipulating that their productions are of the form: A → σ, where A ∈ VN and σ ∈ (VT ∪ VN )∗ ). (ii) A second way is by giving some conditions which should be satisfied by the derivations in the grammars of the class (for instance, the class of the LR(k) grammars has been introduced in Definition 5.1.2 on page 81 in this way). (iii) A third way is by presenting an algorithm for parsing the languages generated by the grammars of the class, and giving some conditions on that algorithm (for instance, the class of LR(0) grammars has been introduced in Definition 5.2.8 on page 91 in this way). Every class of grammars defines a class of languages in the sense that, given a class A of grammars, the corresponding class of languages is made out of all languages L such that there exists a grammar in the class A which generates L. For instance, the class of the LR(1) languages is made out of all languages L such that there exists an LR(1) grammar which generates L. We say that a class A of grammars is contained in a class B of grammars, and we write A ⊆ B, if every grammar of the class A is also a grammar of the class B. We say that a class A of languages is contained in the class B of languages, and we write A ⊆ B, if for every language L in A there is a grammar G of the class B which generates L. 140 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS We have the following hierarchy of grammars and languages. As usual, (i) A = B means A ⊆ B and B ⊆ A, and (ii) A ⊂ B means A ⊆ B and A = 6 B. Hierarchy of grammars. For any k > 1: LL(0) ⊂ LL(1) ⊂ . . . ⊂ LL(k) ⊂ LL(k+1) ⊂ . . . ⊂ LR(1) ⊂ . . . ⊂ LR(k) Hierarchy of grammars: LR(0) = SLR(0) = LALR(0) ⊂ SLR(1) ⊂ LALR(1) ⊂ LR(1) Hierarchy of languages. For any k ≥ 1: LL(0) ⊂ LL(1) ⊂ . . . ⊂ LL(k) ⊂ LL(k+1) ⊂ . . . ⊂ LR(1) =. . . = LR(k) Some of the above results have been presented in Theorem 4.3.17 on page 76. We also have that: (i) L ∈ LL(0) iff L = ∅ or there exists a word w such that L = {w} (that is, L is a singleton) (recall that if we assume that a grammar G is an LL(0) grammar and has no useless symbols, then the language it generates is a singleton), and (ii) L ∈ LR(0) implies that L = {ε} or ε 6∈ L [9, page 352]. We have the following facts. Fact 5.8.3. [Relationships Between SLR(k ) Grammars and LALR(k ) Grammars] The class of SLR(0) grammars coincides with the class of LALR(0) grammars. For k ≥ 1 the class of SLR(k) grammars is properly contained in the class of LALR(k) grammars. Example 5.8.4. The grammar with axiom S and productions: S → Ac | bA | bc A→ε is an LALR(1) grammar which is not SLR(k) for any k ≥ 0 [25, page 72]. Fact 5.8.5. [Relationships Between LALR(k ) Grammars and LR(k ) Grammars] The class of LALR(0) grammars coincides with the class of LR(0) grammars. For k ≥ 1 the class of LALR(k) grammars is properly contained in the class of LR(k) grammars. Example 5.8.6. The grammar with axiom S and productions: S → aAa | bAb | aBb | bBa A→c B→c is an LR(1) grammar which is not LALR(k) for any k ≥ 0. 5.8. SUBCLASSES OF CONTEXT-FREE LANGUAGES 141 Fact 5.8.7. [Hierarchy of the LR(k ) Grammars] For all k ≥ 0, the grammar with axiom S and the productions: S → abk c | Abk d A→a is an LR(k+1) grammar which is not an LR(k) grammar [25, page 53]. Fact 5.8.8. [Relationships Between LR(k ) Grammars and SLR(k ) Grammars] For any k ≥ 0, any language generated by an LR(k) grammar can be generated by an SLR(k) grammar [25, page 75]. Actually, we have the following stronger result. Fact 5.8.9. [Relationships Between the Classes of SLR(k ) Languages, LR(k ) Languages, and LALR(k ) Languages] For every k ≥ 0, we have that the classes of SLR(k) languages, LR(k) languages, and LALR(k) languages are all equal. [25, page 84]. Fact 5.8.10. [Computable Collapse of LR(k ) Languages, for k ≥ 1, to LR(1) Languages and SLR(1) Languages] (i) For any k > 1, given any LR(k) grammar G there exists an equivalent LR(1) grammar G1 which is Turing computable from G (see also Theorem 5.4.14 on page 116). (ii) Given any LR(1) grammar G1 it can be transformed in a Turing computable way into an equivalent SLR(1) grammar [25, page 84]. As a consequence of the above facts we have also the following facts. Related results have been stated in Theorem 5.4.15 on page 116. Fact 5.8.11.[Deterministic Context-Free Languages areLR(1)Languages, SLR(1) Languages, and LALR(1) Languages] (i) Any deterministic context-free language can be generated by an LR(1) grammar. (ii) Any deterministic context-free language can be generated by an SLR(1) grammar. (iii) Any deterministic contextfree language can be generated by an LALR(1) grammar. Fact 5.8.12. [Deterministic Context-Free Languages are Generated by Unambiguous Grammars] (i) For every deterministic context-free language L there exists an unambiguous context-free grammar G which generates L and, by the previous Fact 5.8.11, this grammar can be taken to be an LR(1) grammar. (ii) For all k ≥ 0, any LR(k) grammar is unambiguous. Figure 5.8.1 on the following page shows the relationships among various subclasses of context-free languages. Note that what we stated in Figure 5.8.1 about the class of the Simple Languages, is consistent with the following two facts about grammars (not languages): (i) the grammar with axiom S and productions: S → a S a | b S b | ε (these productions are in Greibach normal form) is not an LR(1) grammar, because the axiom S occurs on the right hand side of a production, and (ii) the grammar with axiom S ′ and productions: S ′ → S, S → a S a | b S b | ε (these productions are not in Greibach normal form because there is an ε-production for S which is not the axiom) is not an LR(1) grammar. 142 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS Context-free languages They are accepted by PDA’s by final state or by empty stack. {ai bj ck | (i = j or j = k) and i, j, k ≥ 1} is a nondeterministic context-free, inherently ambiguous language. ⊃ Non-inherently ambiguous context-free languages ⊃ ⊃ {ak bm | (m = k or m = 2k) and k ≥ 1 and m ≥ 1} is a nondeterministic contextfree, non-inherently ambiguous language. A non-ambiguous grammar with axiom S which generates this language is: S → L |R L → aLb |ab R → aRbb |abb In Figure 5.8.2 on the next page we have depicted a pda which accepts this language by final state. S k≥1 LR(k) S k≥1 LL(k) languages (Deterministic context-free languages) They are accepted by deterministic PDA’s by final state. For any deterministic context-free language L, there exists k ≥ 1 such that L can be generated by an LR(k) grammar. For all k ≥ 0, a language which is generated by an LR(k) grammar, is generated by an LR(1) grammar. For all k ≥ 0, every LR(k) grammar is unambiguous. For any deterministic context-free language L, the language L $, with $ 6∈ VT , can be generated by an LR(0) grammar. The class of LR(1) languages is strictly larger than the union, for all k ≥ 0, of the classes of the LL(k) languages (see Fact 4.3.19 on page 77) [22]. {an bn | n ≥ 1}∪{an cn | n ≥ 1} is an LR(1) language and not an LL(k) language, for any k ≥ 0 (see Fact 4.3.20 on page 77). Recall also Example 5.2.9 on page 91. languages For any k ≥ 0, the class of LL(k) languages is the class of languages generated by LL(k) grammars. In this class of grammars ε-productions are allowed for each nonterminal symbol. For all k ≥ 0, the class of LL(k+1) languages properly includes the class of LL(k) languages and the class of LL(k+1) grammars properly includes the class of LL(k) grammars (see Theorem 4.3.17 on page 76). S → A A → aBb | b B → a | bAB is an LL(1) grammar. ⊃ Simple languages Languages generated by grammars in Greibach normal form such that for each nonterminal A no two productions of the form A → a α and A → a β exist, for some a ∈ VT and some α, β ∈ VN ∗ , with α 6= β. In this class of grammars the only ε-production allowed is S → ε, where S is the axiom. ⊃ LL(0) languages A language is LL(0) iff it is empty or it is a singleton. Figure 5.8.1. Hierarchy of subclasses of context-free languages. All containments ⊃ are proper. We allow ε-productions of the form A → ε with A ∈ VN . We assume, without loss of generality, that the axiom S does not occur on the right hand side of any production. 5.8. SUBCLASSES OF CONTEXT-FREE LANGUAGES a, b bZ0 bb q1 ε q0 a, b b bZ0 bbb q2 ε b, b b, b a, Z0 a, Z0 143 q3 ε, Z0 Z0 q4 ε b, b Figure 5.8.2. A nondeterministic pda which accepts by final state the language L generated by the grammar with axiom S and productions: S → L | R, L → a L b | a b, R → a R b b | a b b. The nondeterminism is due to the moves from state q0 . We have that: L = {ak bm | (m = k or m = 2k) and k ≥ 1 and m ≥ 1}. An arc from state qi to state qj with label ‘x, Z w’ means that the pda moves from state qi to state qj if: (i) the symbol x is read from the input, (ii) the top of the stack is Z, and (iii) the string w is pushed onto the stack so that the leftmost symbol of w will be the new top of the stack. In this move if x 6= ε then the input head moves one character to the right, otherwise it does not move. We state without proof also the following results. Fact 5.8.13. [Properties of LR(0) Languages] (i) A language L is an LR(0) language iff (i.1) it is an LR(1) language, and (i.2) for all u, v, x if u ∈ L and u x ∈ L and v ∈ L, then v x ∈ L [9, page 515] and [25]. (ii) Given an LR(0) language L if u ∈ L and u x ∈ L then u x∗ ∈ L. Fact 5.8.14. [Regular Expressions in Right Hand Sides of Context-Free Grammars] Every context-free grammar whose productions have their right hand sides written as regular expressions over the alphabet VT ∪ VN , can be transformed into an equivalent context-free grammar whose productions have their right hand sides written, as usual, as strings in (VT ∪ VN )∗ . This transformation can be done in linear time with respect to the size of the given grammar [25, page 116]. For instance, the grammar whose axiom is E and whose productions are: E → T (+T )∗ T → F (×F )∗ (G1 ) F → a + (E) can be transformed into the equivalent grammar whose axiom is E and whose productions are: 144 5. PARSERS FOR DETERMINISTIC CONTEXT-FREE LANGUAGES: LR(k ) PARSERS E →T | E+T T →F | T ×F F → a | (E) (G2 ) Obviously, during this transformation one should be careful about the use of overloaded symbols because, for instance, the symbols which are used for writing regular expressions, such as +, (, and ), may also be used as terminal symbols of the given grammar. For instance, in the case of our grammar G1 above, the round parentheses ( and ) are symbols for writing regular expressions in the productions E → T (+T )∗ and T → F (×F )∗ , while they are terminal symbols in the production F → a + (E). Let us conclude this section by stating the following decidability result which follows from [23]. Fact 5.8.15. [Decidability of Equivalence for LR(k ) Grammars] There exists an algorithm which always terminates and for all h, k ≥ 0, given an LR(h) grammar G1 and an LR(k) grammar G2, tells us whether or not they are equivalent, that is, L(G1) = L(G2). More decidability results are listed in Section 10.2 on page 277. Exercise 5.8.16. [Language of Balanced Parentheses] (i) Show that the grammar G1 with axiom S ′ and productions: S′ → S S → aS b | ε | S S is not an LR(1) grammar. The language generated by G1 is: L(G1 ) = {w | w ∈ {a, b}∗ ∧ |w|a = |w|b ∧ for all prefixes u of w, |u|a ≥ |u|b}, that is, the grammar G1 generates the deterministic context-free language of balanced parentheses. For instance, the word w = a a a b a a b b b b a b belongs to L(G1 ) (the symbols a and b play the role of the open parenthesis and the closed parenthesis, respectively). We can depict the word w as follows: a a a a ba b b b ba b Since the parentheses are balanced, the left and the right end of any word w ∈ L(G1 ) are ‘at the same level’. (ii) Show that the language L(G1 ) is a deterministic context-free language by constructing a deterministic pda which accepts it by final state [21]. (iii) Show that the grammar G2 with axiom S ′ and productions: S′ → S S → AS | ε A → aS b is an LR(1) grammar and L(G2 ) = L(G1 ). (Recall that for every deterministic contextfree language L there exists an LR(1) grammar G which generates L.)  CHAPTER 6 Parsers for Operator Grammars and Parser Generators In this chapter we will present the parsers for operator grammars and we will show how to make use of the parser generators for generating parsers. 6.1. Operator-Precedence Parsers Let us introduce the following concept of an operator grammar. Definition 6.1.1. [Operator Grammar] An operator grammar is a context-free grammar without ε-productions such that there are no two consecutive nonterminals on the right hand side of the productions. We have the following fact whose proof is based on the existence of a procedure for generating an equivalent operator grammar from any given unambiguous context-free grammar [3]. Fact 6.1.2. Every context-free language L is such that L − {ε} can be generated by an operator grammar. Instead of presenting the general theory of parsing languages which are generated by operator grammars, we now present an example of parsing a particular expression language, which is generated by an operator grammar where each operator has an associated precedence. This notion of precedence is the usual notion of precedence used in Mathematics so that, for instance, the expression 5 + 2 × 4 is evaluated to 13 and not to 28, because × has a higher precedence than +. In the literature the operator grammars whose operators have an associated precedence are called operator-precedence grammars and the techniques for parsing those grammars are referred to as operator-precedence parsing techniques. Let us consider the operator-precedence grammar with axiom E and the following four productions: E → E +E | E ×E | E ↑ E | a where, as usual, +, ×, and ↑ denote sum, product, and exponentiation, respectively. We first introduce the following augmented grammar G′ with axiom E ′ so to place every generated word between two occurrences of the symbol $ which is assumed not to be in VT ∪ VN . Thus, in G′ we introduce the extra production E ′ → $E$. Then we also introduce the following parsing table which encodes the precedence among the operators +, ×, and ↑, the symbol $, and the identifier a of the given grammar: 145 146 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS a  right left  a T: + × ↑ $ > > > > + < > < < > × < > > < > ↑ < > > < > $ < < < < This table T can be generated from the given grammar G′ as we will indicate below. We have that: (i) the rows and the columns of that table are labeled by the terminal symbols of the grammar G′ , (ii) the symbols < and > denote the precedence relation between operators, and (iii) the fact that, for instance, × as a higher precedence with respect to +, is denoted in the table T by the two entries: + < × and × > +, as indicated by the subtable: ··· + × ··· ··· + × < > ··· Now we will see how the table T is used for parsing a given word w of the language generated by G′ . Let us consider, for instance, the word w = a+ a×a. We add two extra $ symbols as a first and last character of w and we get $ a + a × a $. Then we add the symbol < or the symbol > between any two symbols of $ w $ according to the table T . We get: $ < a > + < a > × < a > $ (for instance, we have: $ < a because in the table T at row $ and column a we have the symbol <, and we have: + < a because in the table T at row + and column a we have the symbol <). Then the parsing process can be described as a left-to-right move from the leftmost $ until a symbol > is found, followed by a right-to-left move until a symbol < is found. Then the string between the symbols < and > (and including them) is reduced according to the grammar at hand and we erase also the symbols < and >. Then we proceed by a new left-to-right move until a symbol > is found, followed by a new right-to-left move until a symbol < is found. Again the string between the symbols < and > is reduced. We continue with these left-to-right and right-to-left moves and reductions, until all <’s and >’s have been erased. At that point the first phase of the parsing process terminates. In the case of our grammar G′ , we get from $ w $ = $ a + a × a $ the following sequence of sentential forms: 6.1. OPERATOR-PRECEDENCE PARSERS 1. 2. 3. 4. 147 $<a>+<a>×<a>$ $E + < a > × < a > $ $E + E× < a > $ $E + E × E $ reducing < a >: reducing < a >: reducing < a >: Then a second phase of the parsing process begins. We forget about the derived nonterminal symbols, E in our case, and we use again the table T to insert <’s and >’s in the sentential form at hand. In our case by forgetting E, we get: $ + × $, and after the insertion of the <’s and >’s we get: $ < + < × > $. Now we proceed as in the first phase of the parsing process and we perform a left-to-right move from the leftmost $ until a symbol > is found, followed by a rightto-left move until a symbol < is found. Then we reduce that part of the sentential form which is identified by the substring between the two symbols < and > which have been found. We repeat this process again by forgetting nonterminals, adding <’s and >’s, performing moves, and reducing substrings until we are left only with the string $ $. In our case we get: 5. 6. 7. 8. 9. 10. $<+<×>$ $ < +E $ $ < +$ $<+>$ $E$ $$ reducing < × > : forgetting E : adding > : reducing <+> : forgetting E : By recalling the sequence of reductions we can reconstruct the parse tree of the given word w. In our case we get the following parse tree: E Parse tree for a+a×a E E a E + E a × a Now we present some heuristics for filling the table T . More can be found in [2, Chapter 4]. Empty entries in the table T denote errors and, during the parsing process, on those entries we have to invoke suitable error recovery routines. (1) If an operator × has higher priority than an operator +, we have the following subtable: + × + × < > (2) If an operator + has the same priority as an operator −, we have the following subtable: 148 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS + − + − > > This table enforces a left associative parsing, that is, for instance, a − a + a is parsed as (a − a) + a. (3) If we want an operator to be right associative (such as, for instance, the exponentiation operator, denoted ↑), we have the following subtable: ↑ ↑ < The reader may easily verify that, indeed, this subtable enforces a right associative parsing, that is, for instance, a ↑ a ↑ a is parsed as a ↑ (a ↑ a). Analogously, we use > if we want an operator to be left associative. By using the table T we get the following two parse trees for the words a + a + a and a ↑ a ↑ a, respectively. E Parse tree for a+a+a E E a E + a + E Parse tree for a↑a↑a E E E a a E ↑ a E ↑ a Instead of using the parsing table T which takes quadratic space with respect to the number of operators in the grammar, we may sometimes use two functions which we call the left function, denoted ℓ, and the right function, denoted r. To store those functions it takes linear space only. In the rest of this section, by abuse of language when we say ‘operator’ we mean any element of the set Op ∪ {$, a} which labels a row or a column of the table T . The left function and the right function are used, instead of the parsing table T , as follows. For instance, given two operators α and β, in order to know whether α < β or α > β, we compare the values of ℓ(α) and r(β), and (i) if ℓ(α) < r(β) then we have α < β, and (ii) if ℓ(α) > r(β) then we have α > β. Now we illustrate how to derive the left function and the right function, if they exist, from a parsing table T [2, page 209] in the case when between two operators there may be only either the precedence relation < or the precedence relation >. The interested reader may look at [2, Chapter 4] to see how to deal with the case in which extra relations, besides < and >, exist between two operators of operator-precedence grammars. The derivation of the left function and the right function is based on a graph which is constructed as follows. 6.1. OPERATOR-PRECEDENCE PARSERS 149 For each operator α of the grammar we introduce two nodes: ℓα and rα . For each pair of operators α and β, if α > β then we draw an edge from ℓα to rβ , and if α < β then we draw an edge from rβ to ℓα . If the graph we have constructed has a cycle than the left function and the right function do not exist. If the graph has no cycle, then for each operator α, ℓ(α) is the length of the longest path in the graph starting from ℓα , and r(α) is the length of the longest path in the graph starting from rα . For instance, in the case of the given expression grammar with operators +, ×, ↑, $, and a, we get the following graph: ℓa ra r↑ ℓ↑ ℓ× r× r+ ℓ+ ℓ$ r$ In this drawing of the graph for each operator α, the position of the node ℓα and the position of the node rα has been chosen for minimizing the number of the edge crossings. As the reader may verify, in our case we get the following values for the left function and the right function: a + × ↑ $ left function ℓ: 6 2 right function r: 5 1 4 4 0 3 5 0 For instance, the entry 6 for ℓ(a) is due to the path (in bold in the graph above): ℓ a → r↑ → ℓ ↑ → r× → ℓ + → r+ → ℓ $ which is the longest path in the graph starting from ℓa and it has, indeed, 6 edges. In case when we have also the binary operators − (minus) and / (divide), and the round brackets, as usual for arithmetic expressions, we get the following values for the left function and the right function [2, page 209]: a + − × / ↑ ( ) $ left function ℓ: 6 2 right function r: 5 1 2 1 4 4 4 0 6 0 3 3 5 5 0 0 As already mentioned, more information on the operator-precedence grammars and their parsing procedures can be found in [2, Chapter 4]. 150 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS 6.2. Use of Parser Generators In this section we illustrate the use of the parser generator Bison [7] and also the use of the lexical analyzer generator Flex [18]. More information on these tools can be found in http://dinosaur.compilertools.net/. We will end this section by providing some suggestions for constructing parsers in practical cases. All programs of this section have been executed using: (i) the Bison version 1.28 (GNU), (ii) the Flex version 2.5, and (iii) the C++ compiler i686-apple-darwin8-gcc4.0.1, under the operating system Mac OS X 10.4.11. 6.2.1. Generation of Parsers Using Bison. Bison is a parser generator, that is, a program which given a description of a grammar, produces a C or a C++ program that parses words of the language generated by that grammar. In particular, Bison is a parser generator for LALR(1) context-free grammars and it is compatible with Yacc (acronym for ‘Yet Another Compiler Compiler’) [11], a well-known parser generator for LALR(1) grammars, to which people usually refer when mentioning work in the area of compiler -compilers. Compilercompilers are programs which given a description of the syntax of a programming language, automatically generate compilers for that language. Bison is compatible with Yacc in the sense that almost all grammar descriptions which can be given in input to Yacc for producing a correct parser, can also be given in input to Bison which will also produce a correct parser. For reasons of simplicity, we will not discuss here the behaviour of Bison when: (i) the given grammar is not an LALR(1) grammar, or (ii) the given grammar is an ambiguous grammar, or (iii) the word to be parsed does not belong to the language generated by the given grammar. For these issues the interested reader may refer to [7] for the case of Bison, and to [2, page 261–266] for the case of Yacc. Let us see how to use the parser generator Bison by presenting a few examples. Example 6.2.1. Generating a parser for a simple LALR(1) language. Let us consider the following grammar G whose axiom is S and whose productions are: 1. S → C C 2. C → c C 3. C → d In order to get a parser for the language L(G) generated for this grammar by Bison, we have first to prepare a file whose extension is ‘.y’. Let that file be called grammar.y. It should be structured in three parts separated by %% as follows: declarations %% productions and semantic actions %% lexical analyzer and auxiliary C or C++ functions For instance, in the case of our grammar G, we prepare the following file. 6.2. USE OF PARSER GENERATORS 151 /** * ======================================================================= * Use of Bison: an LALR(1) grammar * Filename: "grammar.y" * * S -> C C ’\n’ * C -> ’c’ C | ’d’ * * S is the axiom. ’c’ and ’d’ are the characters c and d, respectively. * ’\n’ is the carriage-return character. ’\t’ is the tab character. * ======================================================================= */ %{ #include <stdio.h> /* needed for using printf */ %} %% /* Grammar and Semantic Actions. * The semantic value is the number of characters. */ S: C C ’\n’ { printf ("--> result: %d\n", $1 + $2); } ; C: ’c’ C { $$ = 1 + $2; } | ’d’ { $$ = 1; } ; %% /* --- Lexical Analyzer ---- */ /* The lexical analyzer returns the numeric code of the character read. * It skips all blanks and tabs. */ int yylex (void) { int k; /* Skip blanks and tabs. */ while ((k = getchar ()) == ’ ’ || k == ’\t’) ; /* Return the numeric code k of a char read. */ return k; } /* ----------------------------------------------------------------------* input: output: * ----------------------------------------------------------------------* $ bison grammar.y * $ cc grammar.tab.c -ly -o grammar * $ ./grammar * ccd cccd * --> result: 7 * $ ./grammar * cdcdd * parse error * -------------------------------------------------------------------- */ In the first part of the file grammar.y, that is, the declarations part, besides some comments, we have only the command ‘#include <stdio.h>’. That command is required for using the printing function printf (see the second part of the file grammar.y). In the second part we write the productions of the grammar. The nonterminal of the left hand side of the first production is assumed to be the axiom of the grammar. Each production is associated with a C++ command (or, in general, a sequence of C++ commands) which specifies the so-called semantic actions. In our case, we have chosen that the semantic actions compute the number of c’s and d’s which occur in the given string to parse. 152 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS Due to the presence of semantic actions, Bison produces, from a given grammar, ‘more’ than a simple parser. Indeed, it produces a translator which, when given in input a word of the language generated by the given grammar, constructs an output value (as it is done by the so-called attribute grammars [13]). In the specifications of the semantic actions: (i) the identifier $$ denotes the value associated with the nonterminal in the left hand side of a production, and (ii) $n denotes the value associated with the nonterminal occurring on the n-th position on the right hand side of the production. For instance, for the production C → c C (which is written as C: ’c’ C), the associated semantic action ‘$$ = 1 + $2’ computes for the nonterminal C on the left hand side a value which is one unit more than the value computed for the nonterminal C on the right hand side (which occurs in the second position, being the first position that of the character ’c’). Indeed, since we want to count the number of the c’s and d’s in the string to parse, when we encounter a ’c’, we have to add 1 to that number. In the third part of the file grammar.y we write the lexical analyzer, called yylex(), that is, a piece of code for passing to the parser a terminal at a time. In our case a terminal is any character, excluding blank and tab (see the code of yylex()). The functions printf and getchar are used in the second part and the third part of the file grammar.y. The function int printf(const char * format, x) prints its second argument x as indicated by the string format specified in its first argument. In particular, (i.1) if format is "%d\n" then x is printed as a decimal integer, (i.2) if format is "%g\n" then x is printed as the shorter between a decimal floating point value or as a mantissa-exponent value, and (i.3) if format is "%s\n" then x is printed as a string of characters. The newline character \n has the effect that, after printing x the next printing action will take place on the next line. The function int getchar(void) returns the next character from the standard input as an integer value of type int. Having prepared the file grammar.y, if we issue the following three commands (where $ denotes the system prompt): $ bison grammar.y $ cc grammar.tab.c -ly -o grammar $ ./grammar and we give in input a word w ∈ L(G), then we will get in output the value specified by the semantic actions. The option ‘-ly’ in the second command invokes suitable library routines for the C++ compiler. In our case, if we input the word w is ‘ccd cccd’, then we get the number of characters different from blank and tab which are present in ‘ccd cccd’, that is, 7. In particular, as indicated in the comment lines at the end of the file grammar.y, we get: ccd cccd --> result: 7 (typed in input) (printed in output) 6.2. USE OF PARSER GENERATORS 153 input word grammar.y Bison grammar.tab.c C++ grammar output Figure 6.2.1. Generation of a parser using Bison for the grammar description in the file grammar.y. The suffixes ‘.y’ and ‘.tab.c’ cannot be modified. If the input word is: ccd cccd then the output is: --> result: 7. In Figure 6.2.1 we have depicted the process of generating a parser using Bison as we have indicated above. Remark 6.2.2. When invoking Bison, if we add the option ‘-v’, that is, we type the command ‘bison -v grammar.y’, instead of ‘bison grammar.y’, we will also get in output a file named grammar.output with a description of the finite automaton which directs the LALR(1) parsing. Indeed, for our grammar G above, we get the following file which: (1) associates the rule numbers 1, 2, and 3 to the three productions S → C C, C → c C, and C → d, respectively, and (ii) describes the finite automaton represented in Figure 5.5.1 on page 125: Grammar rule 1 rule 2 rule 3 S -> C C ’\n’ C -> ’c’ C C -> ’d’ ... omissis ... state 0 ’c’ shift, and go to state 1 ’d’ shift, and go to state 2 S go to state 7 C go to state 3 state 1 C -> ’c’ . C (rule 2) ’c’ shift, and go to state 1 ’d’ shift, and go to state 2 C go to state 4 state 2 C -> ’d’ . (rule 3) $default reduce using rule 3 (C) state 3 S -> C . C ’\n’ (rule 1) ’c’ shift, and go to state 1 ’d’ shift, and go to state 2 C go to state 5 state 4 C -> ’c’ C . (rule 2) $default reduce using rule 2 (C) state 5 S -> C C . ’\n’ (rule 1) ’\n’shift, and go to state 6 154 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS state 6 S -> C C ’\n’ . (rule 1) $default reduce using rule 1 (S) state 7 $ go to state 8 state 8 $ go to state 9 state 9 $default accept As the reader may verify, we have that the initial state 0 corresponds to state q0 of Figure 5.5.1 on page 125 and, similarly, state 1 corresponds to state q36 , state 2 corresponds to state q47 , state 3 corresponds to state q2 , state 4 corresponds to state q89 , state 5 corresponds to state q5 , and state 7 corresponds to state q1 . In the above file grammar.output we have a description of the finite automaton of Figure 5.5.1 on page 125, but there are also the extra states 6, 8, and 9 which do not occur in that figure. The reason for these extra states is as follows: (i) state 6 is due to the fact that in the actual representation of the grammar G the production S → C C is terminated by the newline character ‘\n’ and so a transition from state 5 to a new state 6 is required, and (ii) states 8 and 9 are due to the fact that in LALR(1) parsers we actually use augmented grammars with the extra production S ′ → S, where S ′ is the new axiom symbol. In our case this extra production requires a transition from state 7 to state 8 and from state 8 to state 9 (for more details on this point the reader may refer to the Bison manual [7]). Remark 6.2.3. When invoking Bison, if we add the option ‘-y’, that is, we type the command ‘bison -y grammar.y’, instead of ‘bison grammar.y’, then Bison behaves like Yacc in the sense that we will get in output a file named y.tab.c, instead of grammar.tab.c. Then, in our case, after the command ‘cc y.tab.c -ly’, we will get in output the file named a.out, instead of the file named grammar. Example 6.2.4. Generating a parser for an expression calculator. The following example illustrates the use of Bison in the presence of: (i) tokens, (ii) priorities of operators, and (iii) the yyerror routine which deals with the errors which may occur during parsing. Let us consider the following grammar whose axiom is line and whose productions are: line → ε | line \n | line expr \n expr → NUMBER | expr + expr | expr − expr | expr ∗ expr | expr / expr | − expr | expr ^ expr | (expr) where: (i) ε is the empty word, (ii) \n is the newline character, (iii) +, −, ×, /, ∗, and ^ denote addition, subtraction, multiplication, division, and exponentiation, respectively, and (iv) NUMBER is a token string and it will be introduced below. We 6.2. USE OF PARSER GENERATORS 155 have to construct the following file, named calculator.y, to be given in input to Bison. /** * ======================================================================= * Use of Bison: an expression calculator * Filename: "calculator.y" * * ======================================================================= */ %{ #define YYSTYPE double #include <stdio.h> #include <ctype.h> #include <math.h> %} /* /* /* /* YYSTYPE is needed for needed for needed for the type of yylval using printf NUMBER using pow (exponential) /* Precedence of operators is determined by the sequence of lines. * For instance, * ties more than +, and ^ ties more than UMINUS. */ */ */ */ */ %token NUMBER /* %left ’+’ ’-’ %left ’*’ ’/’ %left UMINUS %right ’^’ /* /* /* /* left left left right associative: 2-1-1 = (2-1)-1 associative associative associative: 2^3^2 = 2^(3^2) PRECEDENCE weakest | | strongest %% /* Grammar for input to calculator and Semantic Actions. */ */ */ */ */ */ line: // empty production // | line ’\n’ | line expr ’\n’ { printf ("--> result: %.10g\n", $2);} | error ’\n’ { yyerror(": bad line!\n"); } // <-- ERR uncomment this line ERR if parsing should continue upon error */ // /* ; expr: | | | | | | | NUMBER expr ’+’ expr ’-’ expr ’*’ expr ’/’ ’-’ expr expr ’^’ ’(’ expr { $$ expr { $$ expr { $$ expr { $$ expr { $$ %prec UMINUS { $$ expr { $$ ’)’ { $$ = = = = = = = = $1; $1 + $3; $1 - $3; $1 * $3; $1 / $3; - $2; pow($1,$3); $2; } } } } } } } } // default action // // unary minus // // exponentiation // ; %% /* /* * * ----- Lexical Analyzer ------ */ The lexical analyzer returns a double floating point number on the stack and the token NUMBER, or the numeric code of the character read, if not a number. It skips all blanks and tabs. int yylex (void) { int k; /* Skip blanks and tabs. */ while ((k = getchar ()) == ’ ’ || k == ’\t’) ; */ 156 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS /* Process numbers. */ if (k == ’.’ || isdigit (k)) { ungetc (k, stdin); scanf ("%lf", &yylval); /* value of token */ return NUMBER; /* type of token */ } /* Return the numeric code k of a char read. */ return k; } /* Function needed if line ERR above is uncommented */ /* Called by yyparse on error. */ int yyerror (char const *s) { printf ("%s", s); } /* * * * * * * * * * * * * ----------------------------------------------------------------------input: output: ----------------------------------------------------------------------$ bison calculator.y $ cc calculator.tab.c -ly -o calculator $ ./calculator 3.2^2.3 --> result: 14.51593284 b parse error: bad line! // if line ERR is uncommented 3+2 --> result: 5 -------------------------------------------------------------------- */ In the first part of the file calculator.y we have, together with the command ‘#include <stdio>.h’ which is needed for using the function printf, also the commands: (i) ‘#include <ctype>.h’ which is needed for using token strings which are of the form NUMBER, and (ii) ‘#include <math>.h’ which is needed for using the function pow which performs the exponentiation. All these commands are included between the delimiters %{ and %}. Between these delimiters we also have the command ‘#define YYSTYPE double’ which specifies the type of the variable yylval which is used by Bison for returning the value of a token (see below). Outside the delimiters %{ and %} in the first part of the file calculator.y there are also: (i) the token declarations, and (ii) the declarations of precedence between operators. In our case we have the declaration of the token NUMBER. As specified by the third part of the file calculator.y, a token NUMBER is generated by the lexical analyzer yylex() from a sequence of non-blank and non-tab characters read from the standard input, by using the functions ungetc and scanf which we will present below. A token NUMBER is a string generated by the following grammar (which is equivalent to a regular grammar): 6.2. USE OF PARSER GENERATORS 157 NUMBER → digit + | digit ∗  digit + | digit +  digit ∗ digit → 0 | ... | 9 (Note that in view of Fact 5.8.14 on page 143 we have allowed ourselves to use regular expressions in the right hand sides of the productions.) In general, the lexical analyzer returns: (i) a value which is stored in the variable yylval, and (ii) a type of that value which is the name of the token. In our case the type is NUMBER and it corresponds to a double number in C++, that is, a double-precision floating point number as specified by the command ‘#define YYSTYPE double’ in the first part of the file calculator.y. Tokens are generated by the lexical analyzer yylex() by calling the function scanf. However, before calling scanf the lexical analyzer calls the function ungetc for taking into account the last character read by the function getchar which was required for skipping blanks and tabs. The function int ungetc(int character, FILE * stream) puts back the first argument character into the second argument stream, and it puts it back into the same position where the last character was read from the stream. When the next reading operation from the stream is performed, one gets that character again. Note, however, that the physical content of the stream is not modified by the execution of the function ungetc: only the next reading operation is affected. The function int scanf(const char * format, &x) reads a sequence of characters from the standard input. That sequence has to be understood as a value according to the first argument format. Then that value is stored in the location x. For instance, given in input 28.7 and having defined yylval as a double-precision floating point number by the command ‘#define YYSTYPE double’, the function call scanf("%lf", &yylval) assigns to the variable yylval a number which will be printed as 28.700000 when using the command printf("%lf",yylval) (lf stands for long float). Analogously, given in input the string abca, scanf("%s", str) generates a string str whose value is abca. Ambiguity of the grammar is resolved by adding priorities to the operators. These priorities are specified in the first part of the file calculator.y after the part between the delimiters %{ and %}. In particular, we write the command %left op1 for specifying that the operator op1 is left associative, and we write the command %right op2 for specifying that the operator op2 is right associative. The precedence between operators is established by their relative order in the list of commands which specifies the associativity of the operators: the operator with the weakest precedence is listed first, and the operator with strongest precedence is listed last. The unary minus ‘-’ is denoted by the identifier UMINUS and its precedence is indicated by the position of UMINUS in the list of precedences. In our case unary minus ties more than the binary minus, also denoted as ‘-’. For instance, the value of -6-4 is -10 (not -2), because it is equal to (-6)-4. In the third part of the file calculator.y we write the function yyerror. This function is invoked when a parsing error occurs. In our case, when a parsing error 158 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS occurs, the parser skips symbols until the next ‘\n’ (the newline character) is encountered and then, after printing an error message as specified by the semantic action, if any, the parser continues as no error had occurred. In our case, we can type a new input line with a new expression to be evaluated. At the end of the file calculator.y we have listed, as lines of comments, some examples of execution of the executable file named calculator. For the input 3.2^2.3 we get in output: ––> result: 14.51593284, and for the input b we get in output: parse error: bad line! Example 6.2.5. Generating a parser for a simple assignment language. This example illustrates: (i) the use of Flex [18] as a generator of lexical analyzers which can be used in conjunction with Bison, and (ii) the use of a symbol table during parsing. The lexical analyzer, also called a scanner, or a tokenizer, produced by Flex passes to Bison a token at a time, so that Bison can perform the successive parsing phase according to a given LALR(1) grammar. Flex is a lexical analyzer generator similar to Lex [15]. In order to produce a lexical analyzer we have first to prepare for Flex a file whose extension is ‘.lex’. As in the case of Bison, that file is structured in three parts separated by %%. In the first part there are declarations, in the second part there are productions and semantic actions, and in the third part there are auxiliary functions. Each production takes the form of a regular expression. Flex generates as output a C++ program named lex.yy.c, which defines a function yylex(). Once the program lex.yy.c is compiled, if from the standard input (or a given input file) yylex() recognizes a word which belongs to the language denoted by a regular expression of the list of productions, then yylex() executes the corresponding semantic actions. If we use Flex in conjunction with Bison, the corresponding semantic action returns a value for the variable yylval and a type identifying the token which has been recognized (in our example either the token VARIABLE or the token NUMBER). The grammar we will consider in this example is a grammar which defines a simple programming language of assignments. line → ε | line expr \n | line VARIABLE := expr \n expr → NUMBER | VARIABLE | expr + expr | expr − expr | expr ∗ expr | expr/expr | − expr | expr ^ expr | (expr) where: (i) ε is the empty word, (ii) \n is the newline character, (iii) +, −, ×, /, ∗, and ^ denote addition, subtraction, multiplication, division, and exponentiation, respectively, (iv) NUMBER is a double-precision floating point number, and (v) VARIABLE is a single character variable identifier in the set {a, . . . , z}. There are three files to be prepared: (1) one for Bison, which we name fSAL.y (the extension .y is mandatory), 6.2. USE OF PARSER GENERATORS 159 (2) one for Flex, which we name fSAL.lex (the extension .lex is mandatory), and (3) one for operating on the symbol table storing the values of the variables. We name this file fSAL.h (the extension .h is mandatory). In Figure 6.2.2 on page 161 we have depicted the process of generating a parser using Bison in conjunction with the lexical analyzer generator Flex. Let us first consider the file fSAL.y which we now list. /** * ======================================================================= * Use of Bison: a simple assignment language SAL (1) * Filename: "fSAL.y" * * Parser with Lexical Analyzer. Running Flex and then Bison. * This file uses the file: "fSAL.lex" * * Note: variables have names made out of one character only. * ======================================================================= */ %{ #include <stdio.h> #include <math.h> /* needed for using pow (exponential) */ #include "fSAL.h" /* needed for using putVar, getVar, and printTable */ /* see file "fSAL.lex": * structure for information on VARIABLE’s and NUMBER’s struct tokenInfo { char *name; /* the name of a NUMBER is assumed to be NULL double value; /* the initial value of a VARIABLE is assumed to be 0 }; %} %union %token %token %type { struct tokenInfo ti; } <ti> VARIABLE <ti> NUMBER <ti> expr // // // // */ */ */ type of yylval VARIABLE is a token of type ti NUMBER is a token of type ti expr has type ti %token ASSIGN_SYMB PLUS MINUS MULTIPLY DIVIDE POWER OPEN CLOSED NEWLINE %left %left %left %right %% line: PLUS MINUS MULTIPLY DIVIDE UMINUS POWER /* /* /* /* left left left right associative associative associative associative */ */ */ */ // empty production // | line expr NEWLINE { printf("--> result: %.10g\n", $2.value); } | line VARIABLE ASSIGN_SYMB expr NEWLINE { putVar($2.name, $4.value); } // | error NEWLINE {yyerror(": bad input line!\n");} // <-- ERR /* uncomment this line ERR if parsing should continue upon error. */ ; expr: NUMBER { $$ = $1; } | VARIABLE { if (getVar($1.name) != NULL) { $$.value = (getVar($1.name))->value; } } 160 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS | | | | | | | ; expr PLUS expr expr MINUS expr expr MULTIPLY expr expr DIVIDE expr MINUS expr %prec UMINUS expr POWER expr OPEN expr CLOSED { { { { { { { $$.value $$.value $$.value $$.value $$.value $$.value $$.value = = = = = = = $1.value + $3.value; $1.value - $3.value; $1.value * $3.value; $1.value / $3.value; - $2.value; pow($1.value,$3.value); $2.value; } } } } } } } %% #include "lex.yy.c" /* Function needed if the above line ERR is uncommented */ int yyerror (char const *s) { printf ("%s", s); } /* ----------------------------------------------------------------------* Execution tests with the line "ERR" (in this file "fSAL.y") and the two * lines "SYMB_TABLE" (in the file "fSAL.h") uncommented: we notify errors * and we print the Symbol Table after inserting new values for the * variables. * input: output: * ----------------------------------------------------------------------* $ flex fSAL.lex * $ bison fSAL.y (or: bison -v fSAL.y) * $ cc fSAL.tab.c -ly -o fSAL * $ ./fSAL * b := 3 * Symbol Table after putVar: (b,3) nil * a := 2 * Symbol Table after putVar: (b,3) (a,2) nil * c:=2+a * Symbol Table after putVar: (b,3) (a,2) (c,4) nil * a:=1 * Symbol Table after putVar: (b,3) (a,1) (c,4) nil * a+b^(-2+c) * --> result: 10 // 1+3^2 = 10 * b3 * parse error: bad input line! * 2+b * --> result: 5 * -------------------------------------------------------------------- */ The value of the variable yylval is not a double-precision floating point number as in the previous Example 6.2.1 on page 150, but it is a pointer to a structure named tokenInfo. That structure has two fields: (i) a name and (ii) a value. (i) For a token VARIABLE the structure tokenInfo stores the name of the variable and its value (which is initialized to 0), and (ii) for a token NUMBER the structure tokenInfo stores the fictitious name NULL and the numeric value of the token NUMBER. The %union declaration specifies the list of the possible types of the variable yylval. In our case we have one type only: ti which is the type of the structure tokenInfo. In the first part of the file fSAL.y we also have the list of the tokens which are returned by Flex (see the file fSAL.lex). For instance, OPEN denotes the open parenthesis ‘(’. 6.2. USE OF PARSER GENERATORS fSAL.lex Flex lex.yy.c input lines included in fSAL.y Bison 161 fSAL.tab.c C++ fSAL output Figure 6.2.2. Generation of a parser using Bison, in conjunction with the lexical analyzer generator Flex, for the grammar description in the file fSAL.y. The name lex.yy.c and the suffixes ‘.lex’, ‘.y’, and ‘.tab.c’ cannot be modified. If the input lines are: a:=1 and 2+a then the output is: --> result: 3. In the second part of the file fSAL.y we have the productions and the associated semantic actions. Note that, since the values of the variable yylval are instances of the structure tokenInfo, we need to refer to the fields .name and .value. If a line is an expression expr, the semantic action prints the value of that expression. If the line is an assignment of the form a:=expr, the semantic action initializes a record (or updates the record) for the variable a with the value of the expression expr using the command ‘putVar($2.name, $4.value)’. If a record for the variable a is already present in the symbol table, then the value of the record for a is updated with the value of the expression expr. If the expression is a VARIABLE token, say a, the semantic action looks in the symbol table for a structure tokenInfo whose name field is a, if any, and returns the value field of that structure. In the third part of the file fSAL.y we have specified the lexical analyzer. The specification is not done via the definition of the function yylex(). It is done, instead, via the command ‘#include "lex.yy.c"’ which has the effect of including the lexical analyzer lex.yy.c generated by Flex. Here is the file fSAL.lex. When it is given in input to Flex, we get in output the file lex.yy.c to be included in the file fSAL.y. /** * ======================================================================= * Use of Bison: a simple assignment language SAL (2) * file: "fSAL.lex" (flex_SimpleAssignmentLanguage) * * This file has to be given in input to Flex which will produce in output * the file "lex.yy.c" which contains the code of the lexical analyzer * (also called the scanner). * * The file "lex.yy.c" has to be included in the file "fSAL.y" * which is processed by Bison. * ------ 162 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS * This version of the lexical analyzer is NOT for STAND ALONE use. * ======================================================================= */ %{ #include <string.h> /* needed for using char *strdup( const char *str ) */ %} %% [a-z] { yylval.ti.value = 0; /* yytext contains the matched character in [a-z] yylval.ti.name = (char *) strdup(yytext); return VARIABLE; } [0-9]+|[0-9]*\.[0-9]+|[0-9]+\.[0-9]* { /* yytext contains the matched number yylval.ti.value = atof(yytext); /* from string to double yylval.ti.name = NULL; return NUMBER; } := return ASSIGN_SYMB; \+ return PLUS; \return MINUS; \* return MULTIPLY; \/ return DIVIDE; \^ return POWER; \( return OPEN; \) return CLOSED; \n return NEWLINE; [ \t] ; /* nothing to return in case of blank or tab /* . (dot) matches any input character except \n . printf("Invalid character.\n"); */ */ */ */ */ %% int yywrap() { // Flex terminates after processing the input file return 1; } /* ----------------------------------------------------------------------* input: output: * ----------------------------------------------------------------------* $ flex fSAL.lex * $ bison fSAL.y (or: bison -v fSAL.y) * $ cc fSAL.tab.c -ly -o fSAL * $ ./fSAL * 3+2 * --> result: 5 * -------------------------------------------------------------------- */ As in the case of the file with extension ‘.y’ to be given in input to Bison, the file fSAL.lex is structured in three parts separated by %%. As already mentioned, (i) in the first part there are declarations, (ii) in second part there is a list of regular expressions and semantic actions which are self-explanatory, and (iii) in the third part of the file fSAL.lex there is the function yywrap which is required for making Flex to terminate when the input file has been processed. Let us note only the following few points concerning the file fSAL.lex. 6.2. USE OF PARSER GENERATORS 163 (i) When a character of the set {a, . . . , z} is found, it is recognized as the name of a variable. A tokenInfo structure is constructed with name field which is the character found (stored in the variable yytext) and a value field initialized to 0. Then a token VARIABLE is returned. (ii) When a string in the language denoted by the regular expression digit + + digit ∗  digit + + digit +  digit ∗ is recognized as double-precision floating point number, a tokenInfo structure is constructed with the fictitious name field NULL and and a value field initialized to the value produced by atof(yytext). The function double atof(const char * str), given a string of characters, produces the corresponding floating point value. For instance, double w = atof("43.3") produces a double-precision floating point number w whose value will be printed, by using printf("%f",w), as 43.3. Then a token NUMBER is returned. (iii) \+ denotes the symbol ‘+’. (iv) [ \t] denotes a regular expression which matches either the blank symbol ‘ ’ (note the blank symbol between ‘[’ and ‘\t’) or the tab symbol ‘\t’. (v) The dot ‘.’ denotes a regular expression which matches any symbol except the newline symbol ‘\n’. Now we present the file fSAL.h which is required for manipulating the symbol table which contains a record for each variable. /** * ======================================================================== * Use of Bison: a simple assignment language SAL (3) * Filename: "fSAL.h" * This file is required by "fSAL.y" (which is input to Bison) * It contains the code for the Symbol Table for variables * with the auxiliary functions: printTable, getVar, and putVar. * ======================================================================= */ #include <stdlib.h> #include <string.h> /* for using strlen, strcpy, and strcmp */ /* --------------------------------------------------------------------* varRec: a record for a variable in the Symbol Table */ struct varRec { char *name; double value; struct varRec /* this structure is a record for a variable /* the name of the variable /* the value of the variable is a double *next; /* a pointer to the next record of the Symbol Table */ */ */ */ }; typedef struct varRec varRecType; /* -----------------------------------------------------------------------* varTable and lastRec are global variables initialized to NULL. * varTable points to the first varRec of the Symbol Table. * lastRec points to the last varRec of the Symbol Table. */ varRecType *varTable = NULL; varRecType *lastRec = NULL; 164 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS /* * varTable lastRec * | | * | +------+-------+-------+ +--->+------+-------+------+ * +--->| name | value | next--|---->...----->| name | value | NULL | * +------+-------+-------+ +------+-------+------+ * * --------------------------------------------------------------------- */ /* printTable prints the Symbol Table. */ void printTable(varRecType *T) { if (T==NULL) {printf(" nil");} else {printf(" (%s",T->name); printf(",%g",T->value); printf(")"); printTable(T->next);}; }; /* --------------------------------------------------------------------- */ /* getVar returns a pointer to the record of the argument variable name. */ varRecType *getVar(char *varName) { varRecType *ptr=NULL; for (ptr = varTable; ptr != NULL; ptr = (varRecType *) ptr->next) { // strcmp(s1,s2) returns 0 iff the string s1 is equal to the string s2 if (strcmp(ptr->name, varName) == 0) return ptr; } return NULL; }; /* --------------------------------------------------------------------- */ /* Given a variable name and a value, putVar updates the value in the * record of that variable in the Symbol Table. If there is no record * for that variable in the Symbol Table, putVar initializes a new * record for that variable using the given value. * --------------------------------------------------------------------- */ varRecType *putVar( char *varName, double varValue ) { varRecType *ptr = getVar(varName); if ( ptr == NULL ) { ptr = (varRecType *) malloc (sizeof(varRecType)); if (varTable == NULL) { //If varTable == NULL the Symbol Table is empty varTable = ptr; lastRec = ptr; } else lastRec->next = ptr; // new record at the end of the list. ptr->name = (char *) malloc(strlen(varName) + 1); strcpy(ptr->name, varName); ptr->value = varValue; ptr->next = (varRecType *) NULL; lastRec = ptr; } else { ptr->value = varValue; } // printf("Symbol Table after putVar:"); // <-- SYMB_TABLE // printTable(varTable); printf("\n"); // <-- SYMB_TABLE return ptr; }; /* --------------------------------------------------------------------- */ 6.2. USE OF PARSER GENERATORS 165 The symbol table is a list of structures each of which, called varRec, contains the information relative to a variable. A structure varRec has three fields: (i) a field name for the name of the variable, (ii) a field value for the numeric value of the variable, and (iii) a field next which has a pointer to the next structure in the list. The pointer to the first structure of the list is varTable and the pointer to the last structure in the list is lastRec. Both are initialized to NULL. The field next of the record pointed by lastRec is always NULL. 6.2.2. Generation of Lexical Analyzers Using Flex. In this section we see how Flex can be used for generating a lexical analyzer in a standalone mode, that is, without necessarily providing an input to Bison for generating a parser. We will see this use of Flex through an example. Let us assume that we want to construct a lexical analyzer for a simple assignment language similar to that of Example 6.2.5 on page 158. The tokens to be recognized are: (i) (ii) (iii) (iv) (v) (vi) (vii) (viii) single character variable names in the set {a,...,z} integer numbers as a sequence of 1 or more digits in the set {0,...,9} := + * \n (newline) ‘ ’ (blank) \t (tab). When tokens are recognized they will not be returned to Bison as in Example 6.2.5 on page 158, but they will be printed out by a printf(...) command. Below we present a file, called faSAL.lex, which can be given in input to Flex for producing of the desired lexical analyzer. This file is structured in three parts separated by %% as follows: declarations %% productions and semantic actions %% auxiliary C or C++ functions and main program /** * ======================================================================= * SIMPLE ASSIGNMENT LANGUAGE * Filename: "faSAL.lex" (faSAL = flex_alone_SimpleAssignmentLanguage) * * Lexical Analyzer for the input to "fSAL.y" in STAND ALONE mode. * * -----* This version of the lexical analyzer is for STAND ALONE use. * ======================================================================= */ 166 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS %{ #include <stdlib.h> #include <stdio.h> #include <string.h> // needed for using char *strdup( const char *str ) struct tokenInfo { char *name; int value; }; struct tokenInfo %} yylval; %% [a-z] { yylval.value = 0; /* yytext contains the matched character in [a-z] */ yylval.name = (char *) strdup(yytext); printf("VARIABLE: %s\n", yylval.name); } [0-9]+ { /* yytext contains the matched number in [0-9]+ */ yylval.value = atoi(yytext); /* from string to int */ yylval.name = NULL; printf("INTEGER: %d\n", yylval.value); } := printf("ASSIGN_SYMB\n"); \+ printf("PLUS\n"); \* printf("MULTIPLY\n"); \n printf("NEWLINE\n"); [ \t] ; /* nothing to return in case of blank and tab */ /* . (dot) matches any input character except \n */ . printf("Invalid character.\n"); %% int yywrap() { // Flex terminates after processing the input file return 1; } int main() { yylex(); } /* ----------------------------------------------------------------------* Compiling and executing in STAND ALONE mode. * input: output: * ----------------------------------------------------------------------* $ flex faSAL.lex * $ cc lex.yy.c -o faSAL * $ ./faSAL * 3:=8a * // This is not a valid expression, but the scanner works * // correctly and the tokens are recognized in a correct way. * INTEGER: 3 * ASSIGN_SYMB * INTEGER: 8 * VARIABLE: a * NEWLINE * -------------------------------------------------------------------- */ In Figure 6.2.3 on the next page we have represented the process of generating a lexical analyzer using Flex as we have illustrated above. 6.2. USE OF PARSER GENERATORS 167 input line faSAL.lex Flex lex.yy.c C++ faSAL output Figure 6.2.3. Generation of a lexical analyzer using Flex for the grammar description in the file faSAL.y. The suffix ‘.lex’ and the name lex.yy.c cannot be modified. If the input line is: 3:= then the output are the three lines: INTEGER: 3, ASSIGN_SYMB, and NEWLINE. With respect to the file fSAL.lex that we have presented in Example 6.2.5 on page 161, the reader will note in the above file faSAL.lex: (i) the introduction of the definition of the structure tokenInfo and (ii) the declaration of the type ‘struct tokenInfo’ of the variable yytext. This variable stores the string which matches the pattern of a production during the lexical analysis. Variables are treated as in Example 6.2.5 on page 158, while numbers are now integer numbers, rather than double-precision floating point numbers. Given a string of characters, the function int atoi(const char * str) produces the corresponding integer value. For instance, int n = atoi("52") produces the integer number n whose value is 52. Note that the lexical analyzer works correctly also for inputs which are not legal expressions of Example 6.2.5. For instance, if we issue the following three commands (where $ denotes the system prompt): $ flex faSAL.lex $ cc lex.yy.c -o faSAL $ ./faSAL and then we type in input the string 8c:=\n, (that is, 8c:= followed by the newline character), we will get the following output: INTEGER: 8 VARIABLE: c ASSIGN_SYMB NEWLINE Finally, in the file fSAL.lex we have the main function main() which consists only of the function call yylex() which performs the lexical analysis. 6.2.3. Suggestions for Constructing Parsers. We close this section by providing suggestions for constructing parsers in some practical cases. The first suggestion for constructing parsers is to use a parser generator, such as Yacc or Bison [7, 11]. If we decide not to use a parser generator, we have the choice between constructing either (1) a top-down parser, or (2) a bottom-up parser. This 168 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS choice depends on the manipulations we have to make after the parse tree has been generated by the parser (see [4, Chapter 9]). Case (1). If we choose to construct a top-down parser, then an LL(1) parser is recommended. In this case we transform the grammar, if possible, into an LL(1) grammar and then we produce the parsing table for the LL(1) grammar we have generated. We may also try to reduce the size of the LL(1) parsing table by applying the techniques indicated in [4, page 662]. Case (2). If we choose to construct a bottom-up parser, then an LALR(1) parser is recommended (or an SLR(1) parser, if possible). We can also construct a recursive descent parser (see, for instance, the examples of Sections 7.1 on page 173, 7.2 on page 183, 7.3 on page 194, and 7.4 on page 207). 6.3. Summary on Parsers of Context-Free Languages In this section we recall a few basic results and ideas on context-free parsing (see also the companion book [21]). (A) General Parsers. We have the following general algorithms for context-free parsing. • The Cocke-Younger-Kasami parser that requires the context-free grammar to be given in Chomsky normal form [21]. This algorithm has O(n3 ) time complexity, where n is the size of the input string to be parsed. This algorithm is basically a dynamic programming algorithm that solves a problem of size k by solving p problems of size k − 1. Actually, one can show that this algorithm has the same complexity of the n×n matrix multiplication [27]. • The Earley parser that has O(n3 ) time complexity, where n is the size of the input string to be parsed [21]. The Earley parser has O(n2 ) time complexity if the given context-free grammar is strongly unambiguous, and it has O(n) time complexity if the language is deterministic context-free (that is, it can be generated by an LR(1) grammar). (B) Chop-and-Expand Parsers. We have the following algorithms for parsing non-left recursive, context-free grammars. • The algorithm that uses the higher order function existsev with the three arguments: p, f, and L (see Section 2.3 on page 29 and Chapter 3 on page 35). This function explores a tree whose root is the node n, looking for a leaf satisfying the predicate p such that every node generates a list of son-nodes using the function f . The initial value of the list L is [n]. The higher order function existsev can be avoided if p and f do not depend on the node being visited. The tail recursive program keeps the list of the frontier nodes to be visited. Thus, we get the Chop-and-Expand parsers (thanks to BurstallDijkstra) on page 35 and page 39. • The backtracking algorithm for right linear regular grammars corresponding to nondeterministic finite automata [21, Section 2.10]. This algorithm searches the space of all possible derivations by backtracking using a do-while command and recursion (see Section 2.2 on page 21). But, in fact, (i) the do-while command is 6.3. SUMMARY ON PARSERS OF CONTEXT-FREE LANGUAGES 169 avoided in favour of tail recursion, and (ii) recursion is implemented by keeping the list of the ancestor nodes. • The algorithms with lookahead. These algorithms all have O(n) time complexity, where n is the size of the input string to be parsed. - LL(1) parsers: Chop-and-Expand parsers of non-left recursive context-free grammars. - Recursive descent parsers: bottom-up deterministic parsers for non-left recursive context-free grammars (see, for instance, the parser we have used for Propositional Theorem Prover on page 194). - Parsers of right linear regular grammars corresponding to deterministic finite automata [21, Section 2.10]. (C) Shift-and-Reduce Parsers. We have the following algorithms for parsing deterministic context-free languages with lookahead. These algorithms all have O(n) time complexity, where n is the size of the input string to be parsed. - LR(0) parsers: for prefix-free, deterministic context-free languages. - LR(1) parsers: for deterministic context-free languages (and LALR(1) parsing). (D) Operator-Precedence Grammar Parsers. These algorithms have been presented in Section 6.1 on page 145. They are based on the fact that for every contextfree language L, the language L−{ε} can be generated by an operator-precedence grammar. Now let us recall a few basic results related to the problem of parsing context-free languages. • Rosenkrantz-Stearns’ result about LL(k) and LR(k) languages: LL(1) ⊂ LL(2) ⊂ . . . ⊂ LL(k) ⊂ . . . ⊂ LR(1) = deterministic context-free language • For all k ≥ 1, S → aT T →SA | A A → bB | c B → bk−1 d | ε is an LL(k) grammar and not an LL(k−1) grammar. • For all k ≥ 1, {an w | n ≥ 1 and w ∈ {b, c, bk , d}n } is an LL(k) language and it is not an LL(k−1) language. • A language is LL(0) iff it is empty or it is a singleton. If we assume that in the grammars there are no useless symbols, then a language is LL(0) iff it is a singleton. Note that we do not define the parsing tables for LL(0) parsing. The following two examples show how to construct the parsers for LL(0) languages. Example 6.3.1. Given the alphabet Σ = {a, b}, the algorithm for accepting the LL(0) language which is empty, is any finite automaton without final states (see Figure 6.3.1 on the next page). 170 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS a S b Figure 6.3.1. A finite automaton accepting the empty language. S is not a final state. Given the alphabet Σ = {a, b}, the algorithm for accepting the LL(0) language which is the singleton {a b a a}, is a finite automaton with a sequence of states, no cycles and exactly one final state (see Figure 6.3.2). There are n+1 states in the sequence if n is the length of the word in the singleton. S a A b B a C a D Figure 6.3.2. The finite automaton accepting the word a b a a only. • For all k ≥ 1, every LR(k) language is an LR(1) language. That is, for every k ≥ 1, for every LR(k) language L (that is, for every language L generated by an LR(k) grammar), there exists an LR(1) grammar which generates L. • For all k ≥ 0, there are LR(k+1) grammars which are not LR(k) grammars. For all k ≥ 0, S → a bk c | A bk d A→a is an LR(k+1) grammar and not an LR(k) grammar. 6.3.1. Summary on LR(0) and LR(1) Parsing. A language L is deterministic context-free, that is, it is parsable by a deterministic pda (dpda, for short) with acceptance by final state iff L is LR(1) iff L$, with $ 6∈ VT , is LR(0) (recall Theorem 5.2.16 on page 93). For a deterministic pda, acceptance by final state is more powerful than acceptance by empty stack. A language L enjoys the prefix property (or it is prefix-free) iff no word in L is a proper prefix of another word in L. Every deterministic context-free language L which enjoys the prefix property is recognized by a dpda by final state iff L is recognized by a dpda by empty stack iff L is LR(0). 6.3. SUMMARY ON PARSERS OF CONTEXT-FREE LANGUAGES 171 • D = {0i 1k a 2i | i, k ≥ 1} ∪ {0i 1k b 2k | i, k ≥ 1} is a deterministic context-free language and every grammar for D in Greiback normal form must have at least two productions of the form A → a α and A → a β, with α 6= β, and the dpda which accepts by final state should make at least an ε-move. We can always take this dpda such that if it has to make an ε-move, then it makes it while the input is not completely read. (This follows from a theorem holding for any dpda which: (i) accepts a language by final state, and (ii) should perform at least one ε-move [21].) Note that the language D enjoys the prefix property. A context-free grammar which generates the language D has axiom S and the following productions: S → 0LT | T →2 L → 0LT | 1A A → 1A | a 0R R → 0R | 1BT B → 1BT | b • The language generated by the grammar with axiom S and the following productions: S → aS b | ab is a prefix-free deterministic context-free language (it is in zone (B) of Figure 6.3.3). • The language D ∪ {c, c c} is a deterministic context-free language, but it is not prefix-free (it is in zone (A) of Figure 6.3.3). • The language generated by the grammar with axiom S and the following productions: S → 0A$0 | 1A$1 A → 0A0 | 1A1 | ε is prefix-free, but it is not deterministic context-free (it is in zone (C) of Figure 6.3.3). context-free prefix-free context-free: (B)+(C) (C) (A) deterministic context-free: (A)+(B) (B) Figure 6.3.3. Deterministic context-free languages: (A)+(B). Prefixfree context-free languages: (B)+(C). It is decidable whether or not a deterministic context-free language, given by a dpda accepting by final state or an LR(k) grammar, for some k ≥ 0 (recall that, without loss of generality, we may assume k = 1), is prefix-free [9, page 355]. It is undecidable whether or not a context-free language (given by a context-free grammar) is prefix-free [9, page 262]. 172 6. PARSERS FOR OPERATOR GRAMMARS AND PARSER GENERATORS The class of the deterministic context-free languages (see zone (A)+(B) of Figure 6.3.3) is a proper superset of the class of the deterministic context-free languages which are prefix-free (see zone (B) of Figure 6.3.3). Deterministic contextfree languages which are prefix-free are also called strict deterministic context-free languagesin [9, pages 355–358]. We have the following two characterization of the languages which are in zone (B): (i) the languages in zone (B) are all the languages generated by LR(0) grammars, and (ii) the languages in zone (B) are all the languages generated by strict deterministic context-free grammars [9, page 347]. Let us now define this subclass of the contextfree grammars. First we need the following definitions. Definition 6.3.2. [Partition on a Set] A partition on a set D based on an equivalence relation ρ, is the set of disjoint non-empty subsets Di ’s of D such that: (i) the union of the Di ’s is D, and (ii) every Di is a ρ-equivalence class, that is, for all x, y ∈ D, we have that xρ y iff (x ∈ Di and y ∈ Di ). Definition 6.3.3. [Strict Partition on the Nonterminals of a Contextfree Grammar] Given a context-free grammar G = hVT , VN , P, Si, a partition π on VN based on the equivalence ≡, is said to be strict if for all (not necessarily distinct) nonterminals A, A′ ∈ VN , A ≡ A′ iff for all α, β, β ′ ∈ (VT ∪ VN )∗ , for all two distinct productions A → α β and A′ → α β ′ , we have that:  β 6= ε and β ′ 6= ε and (β 1 ≡ β ′1 ) or (β 1 and β ′1 are possibly distinct terminals) . (Recall that given an alphabet V and a word w ∈ V + , by w1 we denote the leftmost symbol of V occurring in w.) Definition 6.3.4. [Strict Deterministic Context-free Grammar] A contextfree grammar G = hVT , VN , P, Si is said to be strict deterministic if there exists a strict partition of the set VN [9, page 347]. For instance, the grammar G0 with axiom S and the following productions: S → aA | aB A → aAa | bC C → bC | a B → aB | bD D → bDc | c has the strict partition {{S}, {A, B}, {C, D}} on VN . The grammar G1 for arithmetic expressions with axiom S and the following productions: S → (E) E → T | E+T T → F | T ∗F F → S | a does not have a strict partition on VN (this is due to the two productions for F ). The grammar G2 , which is equivalent to G1 , with axiom S and the following productions: S → (E E → T1 E | T2 F1 → ( E ∗ | a ∗ T1 → F1 T1 | F2 F2 → ( E + | a + T2 → F1 T2 | F3 F3 → ( E ) | a ) has the strict partition {{S}, {E}, {T1, T2 }, {F1, F2 , F3 }} on VN . CHAPTER 7 Visits of Trees and Graphs and Evaluation of Expressions In this chapter we will consider a few programs for parsing expressions and visiting trees and graphs. In particular, we will consider programs for: (i) parsing strings written according to a given context-free grammar, and then visiting the trees they denote, (ii) parsing and evaluating boolean expressions, and (iii) testing whether or not propositional formulas are tautologies. We will also describe an algorithm for encoding n-ary trees using binary trees, and two algorithms proposed by E. W. Dijkstra: (i) an algorithm which given an undirected, connected, weighted graph, constructs one of its minimal spanning trees, and (ii) an algorithm which given a directed, connected, weighted graph, and one of its nodes p, constructs all paths from the node p to every other node of the graph. 7.1. Depth First Visit of Trees The program presented in this section: (i) first, parses using the recursive descent technique a given input string denoting a tree, (ii) then, generates an internal form of that tree, and (iii) finally, visits that tree in a depth first manner according to the so called postorder traversal (not the preorder traversal we have described on page 23). In particular, for a binary tree we first visit the left son node, then the right son node, and finally, the root node. The productions of the context-free grammar which generates the input string are the following ones: tree ::= char | - char tree | (tree char tree) char ::= ’0’ | ’1’ |...| ’9’ The axiom is tree. No blank characters are allowed before, in between, and after the input string. A string which is a single character c, denotes a leaf with the character c. A string of the form ‘- c s’ denotes a node with the character c and a single son node denoted by the string s (short for son node). A string of the form ‘(l c r)’ denotes a node with the character c, a left son node denoted by the string l (short for left son node), and a right son node denoted by the string r (short for right son node). For instance, (57-62) denotes the tree: 7 5 6 2 The internal tree which corresponds to a given input string, is printed as a string which is generated by the following productions with axiom tree: 173 174 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS tree ::= n | (n.tree) | (tree.n.tree) n ::= 0 | 1 |...| 9 (i) The first production tree ::= n is for the case of a leaf, (ii) the second production tree ::= (n.tree) is for the case of a unary tree, and (iii) the third production tree ::= (tree.n.tree) is for the case of a binary tree. For instance, the tree denoted by the string (57-62), is printed as: (5.7.(6.2)) If we issue the following two commands: javac DepthFirstVisit.java java DepthFirstVisit we get the following sentence in output: Input a tree according to the grammar given in the program, please. Then, if we type the following input string (note the absence of blank characters): (-231(-564-78)) we get: Tree in input: ((2.3).1.((5.6).4.(7.8))) Depth first visit of the tree: 3 2 6 5 8 7 4 1 Indeed, the string (-231(-564-78)) denotes the tree: 1 2 3 4 5 7 6 8 whose depth first visit is given by the sequence: 3 2 6 5 8 7 4 1. Remark 7.1.1. [Terminology for Nodes and Trees] In our context here, we will consider the words ‘node’ and ‘tree’ as interchangeable, because every node in a tree can be viewed as the root of the subtree rooted in that node (see also Remark 7.4.1 on page 207). In the class DepthFirstVisit of the program below, we have that the expression Character.getNumericValue(c) is the same as c-48, and returns the integer value corresponding to the character c representing that value. For instance, the integer value corresponding to the character ’8’ is 8 (= 56−48). Here is the program for parsing a string denoting a tree and computing the first depth visit of that tree. /** * ======================================================================= * DEPTH FIRST VISIT OF A TREE * Filename: "DepthFirstVisit.java" * * Grammar G for the input tree: * tree ::= char | - char tree | (tree char tree) * char ::= ’0’ | ’1’ | ... | ’9’ 7.1. DEPTH FIRST VISIT OF TREES 175 * Grammar for printing the input tree: * tree ::= n | (n.tree) | (tree.n.tree) * n ::= 0 | 1 | ... | 9 * No blanks are allowed before, in between, and after the input string. * A tree is input as a string generated by the grammar G and then * it is visited in the depth first manner. * ======================================================================= */ import java.io.*; /** * ======================================================================= * Class which constructs a Node * * This class is used for constructing a Node of a binary tree. * By default, a Node is a binary node. * * A Node can be extended for constructing a Node which is * either a binary node, or a unary node, or a leaf node. * ======================================================================= */ class Node { public Object public Node public Node value; left; right; // value field // reference to the left child node // reference to the right child node /** * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ // constructor of a node public Node( Object v, Node l, Node r) { if ( v == null ) throw new IllegalArgumentException( ); value = v; left = l; right = r; } } /** * ======================================================================= * Class which constructs a binary node with two child nodes * ======================================================================= */ class BinaryNode extends Node { // constructor of a binary node /** * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ public BinaryNode(Object v, Node l, Node r) { super(v, l, r); } /** * @return a string representing the tree whose root is the given node */ public String toString(){ return "("+ left.toString() +"."+ value +"."+ right.toString() +")"; } } 176 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * ======================================================================= * Class which constructs a unary node with one child node * ======================================================================= */ class UnaryNode extends Node { // constructor of a unary node /** * @param v : value at the node * @param child : reference to the child node */ public UnaryNode(Object v, Node child) { super(v, child, null); } /** * @return a string representing the tree whose root is the given node */ public String toString() { return "(" + value + "." + left.toString() + ")"; } } /** * ======================================================================= * Class which constructs a leaf node * ======================================================================= */ class LeafNode extends Node { // constructor of a leaf /** * @param v : value at the node */ public LeafNode(Object v) { super(v, null, null); } /** * @return a string representing the leaf node */ public String toString() { return "" + value; } } /** * ======================================================================= * Class ParseException * ======================================================================= */ class ParseException extends Exception { // constructor of a new instance of ParseException public ParseException() { super("Parse exception!"); } } // ----------------------------------------------------------------------- 7.1. DEPTH FIRST VISIT OF TREES 177 public class DepthFirstVisit { // ======================================================================= /** * This class DepthFirstVisit transforms the input string into a tree * of type Node. Parsing is done using the recursive descent technique. * The string encodes the tree as indicated by the grammar G. * @param string: the string which encodes the given tree * @return the root of the tree generated by the parsing * @throws IOException or ParseException */ // stringReader is a sequence of characters, which are read one // at a time, by the method read() of the class StringReader. // RECURSIVE DESCENT PARSING public static Node parse(BufferedReader stringReader) throws IOException, ParseException { char c = (char)stringReader.read(); if ( (’0’ <= c ) && ( c <= ’9’) ) { return new LeafNode(new Integer(Character.getNumericValue(c))); } else if ( c == ’-’) { char c2 = (char)stringReader.read(); Integer value = new Integer(Character.getNumericValue(c2)); Node childTree = parse(stringReader); UnaryNode node = new UnaryNode(value, childTree); return node; } else if ( c == ’(’ ) { Node leftTree = parse(stringReader); char c2 = (char)stringReader.read(); Integer value = new Integer(Character.getNumericValue(c2)); Node rightTree = parse(stringReader); BinaryNode node = new BinaryNode(value, leftTree, rightTree); c2 = (char)stringReader.read(); return node; } else { throw new ParseException(); } } /** Depth first visit of the given tree * @param tree : tree to be visited in a depth first manner * @return the sequence of nodes which is the depth first visit of * the given tree */ public static String depthFirstVisit(Node tree) { StringBuffer buffer = new StringBuffer(); // ---------------------- RECURSIVE VERSION -----------------------------if ( tree instanceof LeafNode ) { // buffer.append(((LeafNode)tree).value + " "); // } else // if ( tree instanceof UnaryNode ) { // buffer.append(depthFirstVisit(((UnaryNode)tree).left) // + tree.value + " "); // } else // if ( tree instanceof BinaryNode ) { // buffer.append(depthFirstVisit(((BinaryNode)tree).left) // + depthFirstVisit(((BinaryNode)tree).right) // + tree.value + " "); // }; // // ---------------------- END OF RECURSIVE VERSION ----------------------return buffer.toString(); } 178 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS // ----------------------------------------------------------------------public static void main(String[] args) throws IOException { System.out.println("Input a tree according to the grammar given " + "in the program, please."); Node tree = null; try { tree = parse(new BufferedReader(new InputStreamReader(System.in))); } catch ( IOException e ) { e.printStackTrace(); } catch ( ParseException pex ) { System.out.println(pex.getMessage()); } if (tree != null) { System.out.println("\nTree in input:\n" + tree); System.out.println("\nDepth first visit of the tree:"); System.out.println(depthFirstVisit(tree)); } } } /** * input: output: * ----------------------------------------------------------------------* javac DepthFirstVisit.java * java DepthFirstVisit * * Input a tree according to the grammar given in the program, please. * (-231(-564-78)) * * Tree in input: * ((2.3).1.((5.6).4.(7.8))) * * Depth first visit of the tree: * 3 2 6 5 8 7 4 1 * ----------------------------------------------------------------------*/ The following program is like the above one: it parses a string denoting a tree and computes the first depth visit of that tree, but it uses a graphical interface. It can be run by typing the following three commands: javac DepthFirstVisit.java javac DepthFirstVisitGUI.java java DepthFirstVisitGUI /** * ======================================================================= * DEPTH FIRST VISIT OF A TREE * with a Graphical User Interface * Filename: "DepthFirstVisitGUI.java" * * This program should be compiled after the compilation of the program * DepthFirstVisit.java. * See also the initial comments in the file DepthFirstVisit.java * * This program uses the class DepthFirstVisit in the file named * DepthFirstVisit.java. The file DepthFirstVisit.java should be stored * in the same folder where this file DepthFirstVisitGUI.java is stored. * 7.1. DEPTH FIRST VISIT OF TREES 179 * To get a dialog window with font size 14 (instead of 28): set the * boolean variable bw (short for big_window) to false (see line ***) * ======================================================================= */ import java.io.*; import javax.swing.JFrame; import java.awt.Font; // needed for changing fonts // ---------------------------------------------------public class DepthFirstVisitGUI extends JFrame { /** Creates new form DepthFirstVisitGUI */ public DepthFirstVisitGUI() { initComponents(); } /** This method initComponents() is called from within the constructor * to initialize the form. */ private void initComponents() {//GEN-BEGIN:initComponents jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); txInput = new javax.swing.JTextField(); txOutTree = new javax.swing.JTextField(); txOutDPF = new javax.swing.JTextField(); btParse = new javax.swing.JButton(); menuBar = new javax.swing.JMenuBar(); fileMenu = new javax.swing.JMenu(); exitMenuItem = new javax.swing.JMenuItem(); // ----------------------------------------------------------boolean bw = true; // true (false) for big (small) window. Font sansSerifFont = bw ? (new Font("SansSerif",Font.PLAIN,28)): (new Font("SansSerif",Font.PLAIN,14)); // ----------------------------------------------------------txInput.setFont(sansSerifFont); txOutTree.setFont(sansSerifFont); txOutDPF.setFont(sansSerifFont); *** getContentPane().setLayout(null); setTitle("DEPTH FIRST VISIT OF A TREE"); setLocationRelativeTo(null); addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent evt) { exitForm(evt); } }); // ----------------------------------------------------------jLabel1.setBackground(new java.awt.Color(255, 255, 150)); jLabel1.setFont(sansSerifFont); jLabel1.setText("<HTML>\n<PRE>\n" +" Grammar for the input tree:\n" +" tree ::= char | - char tree |" +" (tree char tree)\n" +" char ::= ’0’ | ’1’ | ... | ’9’\n" +" Grammar for the internal tree:\n" +" tree ::= n | (n.tree) | (tree.n.tree)\n" +" n ::= 0 | 1 | ... | 9\n" +"<PRE>\n</HTML>"); 180 // // // // // 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS jLabel1.setVerticalAlignment(javax.swing.SwingConstants.TOP); jLabel1.setOpaque(true); getContentPane().add(jLabel1); if (bw) {jLabel1.setBounds(10, 10, 1080, 190);} else {jLabel1.setBounds(5, 5, 525, 95);} ----------------------------------------------------------jLabel2.setFont(sansSerifFont); jLabel2.setText("Input Tree:"); getContentPane().add(jLabel2); if (bw) {jLabel2.setBounds(20, 208, 180, 32);} else {jLabel2.setBounds(10, 104, 90, 16);} getContentPane().add(txInput); if (bw) {txInput.setBounds(270, 208, 810, 40);} else {txInput.setBounds(140, 104, 380, 20);} ----------------------------------------------------------jLabel3.setFont(sansSerifFont); jLabel3.setText("Internal Tree:"); getContentPane().add(jLabel3); if (bw) {jLabel3.setBounds(20, 288, 200, 32);} else {jLabel3.setBounds(10, 144, 110, 16);} txOutTree.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txOutTree); if (bw) {txOutTree.setBounds(270, 288, 810, 40);} else {txOutTree.setBounds(140, 144, 380, 20);} ----------------------------------------------------------jLabel4.setFont(sansSerifFont); jLabel4.setText("Depth First Visit:"); getContentPane().add(jLabel4); if (bw) {jLabel4.setBounds(20, 368, 280, 32);} else {jLabel4.setBounds(10, 184, 140, 16);} txOutDPF.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txOutDPF); if (bw) {txOutDPF.setBounds(270, 368, 810, 50);} else {txOutDPF.setBounds(140, 184, 380, 20);} ----------------------------------------------------------btParse.setFont(sansSerifFont); btParse.setText("Parse and Visit"); btParse.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { btParseActionPerformed(evt); } }); getContentPane().add(btParse); if (bw) {btParse.setBounds(340, 440, 576, 52);} else {btParse.setBounds(240, 210, 138, 25);} ----------------------------------------------------------fileMenu.setText("FILE_MENU"); exitMenuItem.setText("Exit"); exitMenuItem.addActionListener( new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { exitMenuItemActionPerformed(evt); } }); fileMenu.add(exitMenuItem);// adding a File Menu with an Exit button menuBar.add(fileMenu); // setJMenuBar(menuBar); // 7.1. DEPTH FIRST VISIT OF TREES 181 java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); if (bw) {setBounds((screenSize.width-944)/2, (screenSize.height-700)/2, 1095, 600);} else {setBounds((screenSize.width-944)/2, (screenSize.height-700)/2, 544, 300);} } //GEN-END:initComponents private void btParseActionPerformed(java.awt.event.ActionEvent evt) { //GEN-FIRST:event_btParseActionPerformed // Add your handling code here: Node tree = null; try { // from String to StringReader and to BufferedReader BufferedReader inputline = new BufferedReader( new StringReader(txInput.getText())); tree = DepthFirstVisit.parse(inputline); } catch ( IOException e ) { e.printStackTrace(); txOutTree.setText("IOException!"); } catch ( ParseException pex ){ txOutTree.setText(pex.getMessage()); } if (tree != null) { txOutTree.setText(tree.toString()); txOutDPF.setText(DepthFirstVisit.depthFirstVisit(tree)); } } //GEN-LAST:event_btParseActionPerformed private void exitMenuItemActionPerformed( java.awt.event.ActionEvent evt) { //GEN-FIRST:event_exitMenuItemActionPerformed System.exit(0); } //GEN-LAST:event_exitMenuItemActionPerformed /** Exit the Application */ private void exitForm(java.awt.event.WindowEvent evt) { //GEN-FIRST:event_exitForm System.exit(0); } //GEN-LAST:event_exitForm /** * @param args the command line arguments */ public static void main(String args[]) { new DepthFirstVisitGUI().show(); } // Variables declaration - do not modify //GEN-BEGIN:variables private javax.swing.JMenuItem exitMenuItem; private javax.swing.JMenu fileMenu; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; private javax.swing.JMenuBar menuBar; private javax.swing.JTextField txInput; private javax.swing.JTextField txOutTree; private javax.swing.JTextField txOutDPF; private javax.swing.JButton btParse; // End of variables declaration //GEN-END:variables } 182 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * input: output: * ----------------------------------------------------------------------* javac DepthFirstVisit.java // This compilation is needed * javac DepthFirstVisitGUI.java * java DepthFirstVisitGUI * * (see figures below) * ----------------------------------------------------------------------*/ After typing the string (-231(-564-78)) we have: Then by clicking the button ‘Parse and Visit’ we get: 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS 183 7.2. Evaluator of Boolean Expressions The program presented in this section: (i) first, parses using the recursive descent technique a given input string denoting a boolean expression, (ii) then, generates the internal tree which represents that expression, and (iii) finally, returns the value of that boolean expression by evaluating every node of that internal tree. In this section, as concrete instances of boolean expressions, we take the formulas of the propositional calculus built out of the atoms true and false and the operators ¬, ∨, ∧, and →, which, respectively, denote negation, disjunction, conjunction, and implication. The productions of the context-free grammar which generates the input string are the following ones: b c l a ::= ::= ::= ::= c | l | a | ’t’ c>b l+c |lxc -l | ’f’ | (b) (b (c (l (a is is is is a boolean expression) a condition) a literal) an atom) The axiom is b. ’t’ stands for true, ’f’ stands for false, - denotes negation, + denotes disjunction, x denotes conjunction, and > denotes implication. We assume that: (i) + and x bind more than >, and (ii) - binds more than + and x. We also assume right associativity for subexpressions involving + and x. Parentheses override priorities. No blanks are allowed before, in between, and after the input string. For instance, the string -t>f+t denotes the boolean expression: ¬ true → (false ∨ true) The internal tree which corresponds to the input string is then printed as a string which is generated by the following productions from the axiom b: b ::= true | false | (-b) | (b + b) | (b x b) | (b > b) For instance, the boolean expression: ¬true → (false ∨ true) is printed as: ((-true) > (false + true)) If we execute the following two commands: javac BooleanExpression.java java BooleanExpression we get the following sentence in output: Input a boolean expression according to the given grammar, please. Then, if we type the following input string (note the absence of blank characters): t>(f+f)+-tx-f we get: Parse tree of the boolean expression: (true > ((false + false) + ((-true) x (-false)))) Value of the boolean expression: false Indeed, we have that the string t>(f+f)+-tx-f represents according to our conventions, the boolean expression true → ((false ∨ false) ∨ (¬ true ∧ ¬ false)). 184 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS That boolean expression can be depicted as a tree as follows: → ∨ true ∨ ∧ false false ¬ ¬ true false Its value is: false. The following program parses and evaluates boolean expressions. /** * ======================================================================== * EVALUATOR OF BOOLEAN EXPRESSIONS * Filename: "BooleanExpressionEvaluator.java" * * Grammar G for the input boolean expression b: * * boolean expressions b ::= c | c>b <-- bexprParse * conditions c ::= l | l+c | lxc <-- condParse * literals l ::= a | -l <-- litParse * atoms a ::= ’t’ | ’f’ | (b) <-- atomParse * * ’t’ stands for true, ’f’ stands for false, - denotes negation, * + denotes disjunction, x denotes conjunction, and > denotes implication. * The grammar should not be left recursive. * Internal tree: * ------------* b ::= true | false | (-b) | (b x b) | (b + b) | (b > b) * (Leaf) (Leaf) (Unary) (Binary) (Binary) (Binary) * Output value: * -----------* value ::= true | false * -----------* Priorities: + and x bind more than > * - binds more than + or x * Right associativity for + and x * Parentheses override priorities. * No blanks are allowed before, in between, and after the input string. * * A boolean expression is given in input as a string generated by * the grammar G and then it is parsed and evaluated. * The word ’node’ is assumed to be equivalent to the word ’tree’. * * --------* If "C2 extends C1" then the class C2 has the methods of the class C1. * This may not be desirable, but it allows the use of objects of the class * C1 and then the use of "instanceof" to test whether or not an object of * the class C1 is indeed of the class C2. * * --------* Line (1) below checks whether or not the input boolean expression is * terminated by a carriage-return ’\n’. * ========================================================================= */ import java.io.*; 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS 185 /** * ========================================================================= * Class which constructs a Node * This class is used for constructing a Node of a binary tree. * By default, a Node is a binary node. * A Node can be extended for constructing a Node which is * either a binary node, or a unary node, or a leaf node. * ========================================================================= */ class Node { public Object value; // value field public Node left; // reference to the left child node public Node right; // reference to the right child node // constructor /** * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ // constructor of a binary node public Node( Object v, Node l, Node r) { if ( v == null ) throw new IllegalArgumentException( ); value = v; left = l; right = r; } } /** * ========================================================================= * Class which constructs a binary node with two child nodes * ========================================================================= */ class BinaryNode extends Node { /** // constructor of binary node * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ public BinaryNode(Object v, Node l, Node r) { super(v, l, r); } /** * @return a string representing the tree whose root is the given node */ public String toString(){ return "("+ left.toString() +" "+ value +" "+ right.toString() +")"; } } /** * ========================================================================= * Class which constructs a unary node with one child node * ========================================================================= */ class UnaryNode extends Node { /** // constructor of unary node * @param v : value at the node * @param child : reference to the child node */ public UnaryNode(Object v, Node child) { super(v, child, null); } 186 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * @return a string representing the tree whose root is the given node */ public String toString() { return "(" + value + left.toString() + ")"; } } /** * ========================================================================= * Class which constructs a leaf node * ========================================================================= */ class LeafNode extends Node { /** // constructor of a leaf * @param v : value at the node */ public LeafNode(Object v) { super(v, null, null); } /** * @return a string representing the leaf node */ public String toString() { return "" + value; } } /** * ========================================================================= * Class ParseException * ========================================================================= */ class ParseException extends Exception { // constructor of a new instance of ParseException public ParseException() { super("parse exception!"); } } /** * ========================================================================= * Class EvalException * ========================================================================= */ class EvalException extends Exception { // constructor of a new instance of EvalException public EvalException() { super("eval exception!"); } } // ------------------------------------------------------------------------public class BooleanExpressionEvaluator { // ========================================================================= /** * This class BooleanExpressionEvaluator transforms the input string * into a tree of type Node. Parsing is done using the recursive descent * technique. * The string encodes the tree as indicated by the grammar G. * @param string: the string which encodes the given tree * @return the root of the tree generated by the parsing * @throws IOException or ParseException or EvalException. */ 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS public static char c; // static field. // inputLine is a sequence of characters, // which are read one at a time, by // the method read() of the class StringReader // RECURSIVE DESCENT PARSING public static Node bexprParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = condParse(inputLine); if ( c == ’>’ ) { c = (char)inputLine.read(); Node node2 = bexprParse(inputLine); return new BinaryNode(new Character(’>’), node1, node2); } else { return node1; } } public static Node condParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = litParse(inputLine); if ( c == ’x’ ) { c = (char)inputLine.read(); Node node2 = condParse(inputLine); return new BinaryNode(new Character(’x’), node1, node2); } else if ( c == ’+’ ) { c = (char)inputLine.read(); Node node2 = condParse(inputLine); return new BinaryNode(new Character(’+’), node1, node2); } else { return node1; } } public static Node litParse(BufferedReader inputLine) throws IOException, ParseException { Node node = null; if ( c == ’-’ ) { c = (char)inputLine.read(); Node node1 = litParse(inputLine); node = new UnaryNode(new Character(’-’), node1); } else { node = atomParse(inputLine); }; return node; } public static Node atomParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = null; if ((c == ’t’) || (c == ’f’)) { node1 = new LeafNode(new Boolean((c==’t’)?true:false)); c = (char)inputLine.read(); return node1; } else { if ( c==’(’ ) { c = (char)inputLine.read(); node1 = bexprParse(inputLine); if ( c==’)’ ) { c = (char)inputLine.read(); }// reads past ’)’ else { throw new ParseException(); }; return node1; }; }; throw new ParseException(); } 187 188 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS // ------------------------------------------------------------------------/** Evaluation of the tree which represents the input boolean expression * @param tree : tree to be evaluated * @return the boolean value of the tree */ public static boolean evalTree(Node tree) throws EvalException { // ---------------------- RECURSIVE VERSION -------------------------------if ( tree instanceof LeafNode ) { // return ((Boolean)((LeafNode)tree).value).booleanValue(); // } else // if ( tree instanceof UnaryNode ) { // return !(evalTree(((UnaryNode)tree).left)); // } else // if ( tree instanceof BinaryNode ) { // boolean left = evalTree(((BinaryNode)tree).left); // boolean right = evalTree(((BinaryNode)tree).right); // if (((Character)(((BinaryNode)tree).value)).charValue() == ’+’)// { return left || right; } else // if (((Character)(((BinaryNode)tree).value)).charValue() == ’x’)// { return left && right; } else // if (((Character)(((BinaryNode)tree).value)).charValue() == ’>’)// { return (!left) || right; } else { // throw new EvalException(); // } // } else { // throw new EvalException(); // } // // ---------------------- END OF RECURSIVE VERSION ------------------------} // ------------------------------------------------------------------------public static void main(String[] args) throws IOException, ParseException { System.out.println("Input a boolean expression according to the " + "given grammar, please."); Node tree = null; try { // from String to StringReader and to BufferedReader BufferedReader inputLine = new BufferedReader(new InputStreamReader(System.in)); c = (char)inputLine.read(); tree = bexprParse(inputLine); if (c != ’\n’) { throw new ParseException(); }; // <--- (1) } catch ( IOException e ) { e.printStackTrace(); System.out.println("IOException!"); } catch ( ParseException pex ) { System.out.println(pex.getMessage()); return; }; // -----if (tree != null) { System.out.println("\nParse tree of the boolean expression:\n" + tree); System.out.println("\nValue of the boolean expression:"); 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS 189 try { System.out.println(evalTree(tree)); } catch ( EvalException evalex ) { System.out.println(evalex.getMessage()); } } } } /** * input: output: * ------------------------------------------------------------------------* javac BooleanExpressionEvaluator.java * java BooleanExpressionEvaluator * * Input a boolean expression according to the given grammar, please. * t>(f+f)+-tx-f * * Parse tree of the boolean expression: * (true > ((false + false) + ((-true) x (-false)))) * * Value of the boolean expression: * false * ------------------------------------------------------------------------*/ The following program is like the above one: it parses and evaluates boolean expressions, but it uses a graphical interface. It can be run by typing the following three commands: javac BooleanExpressionEvaluator.java javac BooleanExpressionEvaluatorGUI.java java BooleanExpressionEvaluatorGUI /** * ========================================================================= * EVALUATOR OF BOOLEAN EXPRESSIONS * with a Graphical User Interface * Filename: "BooleanExpressionEvaluatorGUI.java" * * This program should be compiled after the compilation of the program * BooleanExpressionEvaluator.java. * See also the initial comments of the file BooleanExpressionEvaluator.java * * This program uses the class BooleanExpressionEvaluator in the file named * BooleanExpressionEvaluator.java. The file BooleanExpressionEvaluator.java * should be stored in the same folder where this file * BooleanExpressionEvaluatorGUI.java is stored. * * To get a dialog window with font size 14 (instead of 28): set the * boolean variable bw (short for big_window) to false (see line ***) * ========================================================================= */ import java.io.*; import javax.swing.JFrame; import java.awt.Font; // needed for changing fonts // ---------------------------------------------------- 190 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS public class BooleanExpressionEvaluatorGUI extends JFrame { /** Creates new form BooleanExpressionEvaluatorGUI */ public BooleanExpressionEvaluatorGUI() { initComponents(); } /** This method initComponents() is called from within the constructor * to initialize the form. */ private void initComponents() {//GEN-BEGIN:initComponents jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); txInExpr = new javax.swing.JTextField(); txOutExpr = new javax.swing.JTextField(); txOutVal = new javax.swing.JTextField(); btParse = new javax.swing.JButton(); menuBar = new javax.swing.JMenuBar(); fileMenu = new javax.swing.JMenu(); exitMenuItem = new javax.swing.JMenuItem(); // ----------------------------------------------------------boolean bw = true; // true (false) for big (small) window. *** Font sansSerifFont = bw ? (new Font("SansSerif",Font.PLAIN,28)): (new Font("SansSerif",Font.PLAIN,14)); // ----------------------------------------------------------txInExpr.setFont(sansSerifFont); txOutExpr.setFont(sansSerifFont); txOutVal.setFont(sansSerifFont); getContentPane().setLayout(null); setTitle("EVALUATION OF A BOOLEAN EXPRESSION"); setLocationRelativeTo(null); addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent evt) { exitForm(evt); } }); // ----------------------------------------------------------jLabel1.setBackground(new java.awt.Color(255, 255, 150)); jLabel1.setFont(sansSerifFont); jLabel1.setText("<HTML>\n<PRE>\n" +" Grammar for the input boolean expression b:\n" +" b ::= c | c>b c ::= l | l+c | lxc l ::= a | -l\n" +" a ::= ’t’ | ’f’ | (b)\n" +" ’t’ stands for true. ’f’ stands for false.\n" +" Internal tree: \n" +" b ::= true | false | (-b) | (b + b)\n" +" | (b x b) | (b > b)\n" +" Evaluation of the boolean expression:\n" +" true | false\n" +"<PRE>\n</HTML>"); jLabel1.setVerticalAlignment(javax.swing.SwingConstants.TOP); jLabel1.setOpaque(true); getContentPane().add(jLabel1); if (bw) {jLabel1.setBounds(10, 10, 1164, 288);} else {jLabel1.setBounds(5, 5, 582, 144);} // ----------------------------------------------------------jLabel2.setFont(sansSerifFont); jLabel2.setText("Input Expression:"); getContentPane().add(jLabel2); 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS // // // // 191 if (bw) {jLabel2.setBounds(20, 336, 312, 40);} else {jLabel2.setBounds(10, 168, 156, 20);} getContentPane().add(txInExpr); if (bw) {txInExpr.setBounds(280, 336, 800, 50);} else {txInExpr.setBounds(10, 168, 156, 20);} ----------------------------------------------------------jLabel3.setFont(sansSerifFont); jLabel3.setText("Internal Tree:"); getContentPane().add(jLabel3); if (bw) {jLabel3.setBounds(20, 416, 312, 40);} else {jLabel3.setBounds(10, 208, 156, 20);} txOutExpr.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txOutExpr); if (bw) {txOutExpr.setBounds(280, 416, 800, 50);} else {txOutExpr.setBounds(140, 208, 400, 25);} ----------------------------------------------------------jLabel4.setFont(sansSerifFont); jLabel4.setText("Evaluation:"); getContentPane().add(jLabel4); if (bw) {jLabel4.setBounds(20, 496, 280, 32);} else {jLabel4.setBounds(10, 253, 140, 16);} txOutVal.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txOutVal); if (bw) {txOutVal.setBounds(280, 496, 800, 50);} else {txOutVal.setBounds(140, 253, 400, 25);} ----------------------------------------------------------btParse.setFont(sansSerifFont); btParse.setText("Parse and Evaluate"); btParse.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { btParseActionPerformed(evt); } }); getContentPane().add(btParse); if (bw) {btParse.setBounds(460, 596, 388, 50);} else {btParse.setBounds(230, 288, 194, 26);} ----------------------------------------------------------fileMenu.setText("FILE_MENU"); exitMenuItem.setText("Exit"); exitMenuItem.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { exitMenuItemActionPerformed(evt); } }); fileMenu.add(exitMenuItem);// adding a File Menu with an Exit button menuBar.add(fileMenu); // setJMenuBar(menuBar); // java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); if (bw) {setBounds((screenSize.width-944)/2, (screenSize.height-700)/2, 1112, 748);} else {setBounds((screenSize.width-944)/2, (screenSize.height-700)/2, 556, 374);} } //GEN-END:initComponents private void btParseActionPerformed(java.awt.event.ActionEvent evt) { //GEN-FIRST:event_btParseActionPerformed // Add your handling code here: // This part is like the main of the class BooleanExpressionEvaluator // in the file BooleanExpressionEvaluator.java. Node tree = null; 192 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS try {// from String to StringReader and to BufferedReader. Added // an extra ’\n’ at the end of the input for terminating the // input as in the BooleanExpressionEvaluator class. BufferedReader inputLine = new BufferedReader( new StringReader((txInExpr.getText()) + ’\n’)); BooleanExpressionEvaluator.c = (char)inputLine.read(); tree = BooleanExpressionEvaluator.bexprParse(inputLine); if (BooleanExpressionEvaluator.c != ’\n’) { throw new ParseException(); }; } catch ( IOException e ) { e.printStackTrace(); txOutExpr.setText("IOException!"); } catch ( ParseException pex ){ txOutExpr.setText(pex.getMessage()); return; } // -----if (tree != null) { txOutExpr.setText(tree.toString()); try { txOutVal.setText( (BooleanExpressionEvaluator.evalTree(tree))?"true":"false"); } catch ( EvalException evalex ) { System.out.println(evalex.getMessage()); } } } //GEN-LAST:event_btParseActionPerformed private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) { //GEN-FIRST:event_exitMenuItemActionPerformed System.exit(0); } //GEN-LAST:event_exitMenuItemActionPerformed /** Exit the Application */ private void exitForm(java.awt.event.WindowEvent evt) { //GEN-FIRST:event_exitForm System.exit(0); } //GEN-LAST:event_exitForm /** * @param args the command line arguments */ public static void main(String args[]) { new BooleanExpressionEvaluatorGUI().show(); } // Variables declaration - do not modify //GEN-BEGIN:variables private javax.swing.JMenuItem exitMenuItem; private javax.swing.JMenu fileMenu; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; private javax.swing.JMenuBar menuBar; private javax.swing.JTextField txInExpr; private javax.swing.JTextField txOutExpr; private javax.swing.JTextField txOutVal; private javax.swing.JButton btParse; // End of variables declaration //GEN-END:variables } /** * input: output: * ------------------------------------------------------------------------* javac BooleanExpressionEvaluator.java // This compilation is needed * javac BooleanExpressionEvaluatorGUI.java 7.2. EVALUATOR OF BOOLEAN EXPRESSIONS 193 * java BooleanExpressionEvaluatorGUI * * (see figures below) * ------------------------------------------------------------------------*/ After typing the string t>(f+f)+-tx-f we have: Then by clicking the button ‘Parse and Evaluate’ we get: 194 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS 7.3. A Theorem Prover for the Propositional Calculus The program presented in this section: (i) first, parses using the recursive descent technique a given input string denoting a propositional formula, (ii) then, generates the internal tree which represents that formula, and (iii) finally, tells us whether or not that formula is a tautology. In case it is not a tautology the program returns the values of the propositional variables for which the given formula evaluates to false. The productions of the context-free grammar which generates the input string are the following ones: p c l a ::= ::= ::= ::= c | l | a | ’0’ c>p l+c |lxc -l | ... | ’9’ | (p) (p (c (l (a is is is is a propositional formula) a condition) a literal) an atom) The axiom is p. The propositional variables are denoted by the characters ’0’, ..., ’9’, and thus, we assume that there are at most ten of them. As in Section 7.2 on page 183, - denotes negation, + denotes disjunction, x denotes conjunction, and > denotes implication. For reasons of brevity, we will also write true as t and false as f. We assume that: (i) + and x bind more than >, and (ii) - binds more than + and x. We also assume right associativity for subexpressions involving + and x. Parentheses override priorities. No blank characters are allowed before, in between, and after the input string. For instance, the string 1x3+1 denotes the propositional formula P1 ∧ (P3 ∨ P1 ) where we have used the propositional variables P1 and P3 , instead of ’1’ and ’3’, respectively. The internal tree which corresponds to the input string is then printed as a string which is generated by the following productions from the axiom p: p ::= d | (-p) | (p + p) | (p x p) | (p > p) d ::= 0 | 1 |...| 9 Note that the propositional variables ’0’, . . ., ’9’ are printed as the digits 0, . . ., 9, respectively . For instance, the propositional formula P1 ∧ (P3 ∨ P1 ) is printed as (1 x (3 + 1)). If we execute the following two commands: javac PropositionalTheoremProver.java java PropositionalTheoremProver we get the following sentence in output: Input a propositional formula according to the given grammar, please. The variables should be between 0 and 3. The value 3 is due to the fact that in our implementation we have set the value of the variable DIM (which tells us the maximum number of the propositional variables which may occur in the input formula) to 4. Thus, our propositional variables are: ’0’, ’1’, ’2’, and ’3’. The value of DIM can be modified by the programmer and in our implementation it is a positive integer which it can be at most 10. Then, if we type the following input string (note the absence of blank characters): 0>(1+1)+-0x-2 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 195 we get: Parse tree of the propositional formula: (0 > ((1 + 1) + ((-0) x (-2))))) Is it a tautology? NO. The falsity assignment is: 0=true; 1=false; 2=false; 3=false; Indeed, the string (0 > ((1 + 1) + ((-0) x (-2)))) represents, according to our conventions, the propositional formula P0 → ((P1 ∨ P1 ) ∨ (¬P0 ∧ ¬P2 )), where we have used the propositional variables P0 , P1 , and P2 , instead of ’0’, ’1’, and ’2’, respectively. That propositional formula can be depicted as a tree as follows: → ∨ P0 ∨ P1 ∧ P1 ¬ ¬ P0 P2 The value of that formula is false if P0 =true, and P1 = P2 = false. Here is the program for parsing a given propositional formula and checking whether or not it is a tautology. /** * ========================================================================= * PROPOSITIONAL THEOREM PROVER * Filename: "PropositionalTheoremProver.java" * * Grammar G for the input propositional formula p: * * propositional formulas p ::= c | c>p <-- propParse * conditions c ::= l | l+c | lxc <-- condParse * literals l ::= a | -l <-- litParse * atoms a ::= ’0’ | ... | ’9’ | (p) <-- atomParse * Propositional variables are named: ’0’, ..., ’9’. * The value of DIM is set to the maximum number of propositional variables * which are actually present in any given formula. * If DIM = 4, then the variables are: ’0’, ’1’, ’2’, and ’3’. * (see line (1) below). DIM should be at most 10. * - denotes negation, + denotes disjunction, x denotes conjunction, and * > denotes implication. * Internal tree: * ------------* p ::= 0 | ... | 9 | (-p) | (p x p) | (p + p) | (p > p) * (Leaf) (Leaf) (Unary) (Binary) (Binary) (Binary) * * Output value of a propositional formula: * --------------------------------------* value ::= true | false * -----------* Priorities: + and x bind more than > * - binds more than + or x 196 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS * Right associativity for + and x * Parentheses override priorities. * No blanks are allowed before, in between, and after the input string. * The propositional formula in input should be a string generated by * the grammar G and then it is parsed and evaluated for all assignments * of the propositional variables. * * The word ’node’ is assumed to be equivalent to the word ’tree’. * --------* If "C2 extends C1" then the class C2 has the methods of the class C1. * This may not be desirable, but it allows the use of objects of the class * C1 and then the use of "instanceof" to test whether or not an object of * the class C1 is indeed of the class C2. * --------* Line (1) below depends on the number of propositional variables. * Line (2) below checks whether or not the input propositional formula is * terminated by a carriage-return ’\n’. * ========================================================================= */ import java.io.*; /** * ========================================================================= * Class which constructs a Node * * This class is used for constructing a Node of a binary tree. * By default, a Node is a binary node. * A Node can be extended for constructing a Node which is * either a binary node, or a unary node, or a leaf node. * ========================================================================= */ class Node { public Object value; // value field public Node left; // reference to the left child node public Node right; // reference to the right child node // constructor /** * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ // constructor of a binary node public Node( Object v, Node l, Node r) { if ( v == null ) throw new IllegalArgumentException( ); value = v; left = l; right = r; } } /** * ========================================================================= * Class which constructs a binary node with two child nodes * ========================================================================= */ class BinaryNode extends Node { /** // constructor of binary node * @param v : value at the node * @param l : reference to the left child node * @param r : reference to the right child node */ public BinaryNode(Object v, Node l, Node r) { super(v, l, r); } 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 197 /** * @return a string representing the tree whose root is the given node */ public String toString(){ return "("+ left.toString() +" "+ value +" "+ right.toString() +")"; } } /** * ========================================================================= * Class which constructs a unary node with one child node * ========================================================================= */ class UnaryNode extends Node { /** // constructor of unary node * @param v : value at the node * @param child : reference to the child node */ public UnaryNode(Object v, Node child) { super(v, child, null); } /** * @return a string representing the tree whose root is the given node */ public String toString() { return "(" + value + left.toString() + ")"; } } /** * ========================================================================= * Class which constructs a leaf node * ========================================================================= */ class LeafNode extends Node { /** // constructor of a leaf * @param v : value at the node */ public LeafNode(Object v) { super(v, null, null); } /** * @return a string representing the leaf node */ public String toString() { return "" + value; } } /** * ========================================================================= * Class ParseException * ========================================================================= */ class ParseException extends Exception { // constructor of a new instance of ParseException public ParseException() { super("parse exception!"); } } 198 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * ========================================================================= * Class EvalException * ========================================================================= */ class EvalException extends Exception { // constructor of a new instance of EvalException public EvalException() { super("eval exception!"); } } // ------------------------------------------------------------------------public class PropositionalTheoremProver { // ========================================================================= /** * This class PropositionalTheoremProver transforms the input string * into a tree of type Node using the recursive descent technique. * The string encodes the tree as indicated by the grammar G. * @param string: the string which encodes the given tree * @return the root of the tree generated by the parsing * @throws IOException or ParseException or EvalException. */ // Three static fields of the class PropositionalTheoremProver // DIM: maximum number of the distinct variables // in the propositional formula public static final int DIM = 4; // <--- (1) // At most DIM propositional variables in the // propositional formula. By default, every // element of tValues is initialized to "false" public static boolean [] tValues = new boolean [DIM]; public static char c;// static field (global variable). // inputLine is a sequence of characters, // which are read one at a time, by // the method read() of the class StringReader // ----------------------- RECURSIVE DESCENT PARSING ----------------------public static Node propParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = condParse(inputLine); if ( c == ’>’ ) { c = (char)inputLine.read(); Node node2 = propParse(inputLine); return new BinaryNode(new Character(’>’), node1, node2); } else { return node1; } } public static Node condParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = litParse(inputLine); if ( c == ’x’ ) { c = (char)inputLine.read(); Node node2 = condParse(inputLine); return new BinaryNode(new Character(’x’), node1, node2); } else if ( c == ’+’ ) { c = (char)inputLine.read(); Node node2 = condParse(inputLine); return new BinaryNode(new Character(’+’), node1, node2); } else { return node1; } } 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 199 public static Node litParse(BufferedReader inputLine) throws IOException, ParseException { Node node = null; if ( c == ’-’ ) { c = (char)inputLine.read(); Node node1 = litParse(inputLine); node = new UnaryNode(new Character(’-’), node1); } else { node = atomParse(inputLine); }; return node; } public static Node atomParse(BufferedReader inputLine) throws IOException, ParseException { Node node1 = null; if ((’0’ <= c) && (c <= ’9’)) { // from the character to the integer: -48 // node1 = new LeafNode(new Integer(Character.getNumericValue(c))); c = (char)inputLine.read(); return node1; } else { if ( c==’(’ ) { c = (char)inputLine.read(); node1 = propParse(inputLine); if ( c==’)’ ) { c = (char)inputLine.read(); } // reads past ’)’ else { throw new ParseException(); }; return node1; }; }; throw new ParseException(); } // ------------------------------------------------------------------------/** Evaluation of the tree which represents the input boolean expression * @param tree : tree to be evaluated * @return the boolean value of the tree */ public static boolean evalTree(Node tree) throws EvalException { // ---------------------- RECURSIVE VERSION -------------------------------if ( tree instanceof LeafNode ) { // return tValues[(Integer)(((LeafNode)tree).value)]; // } else // if ( tree instanceof UnaryNode ) { // return !(evalTree(((UnaryNode)tree).left)); // } else // if ( tree instanceof BinaryNode ) { // boolean left = evalTree(((BinaryNode)tree).left); // boolean right = evalTree(((BinaryNode)tree).right); // if (((Character)(((BinaryNode)tree).value)).charValue() == ’+’)// { return left || right; } else // if (((Character)(((BinaryNode)tree).value)).charValue() == ’x’)// { return left && right; } else // if (((Character)(((BinaryNode)tree).value)).charValue() == ’>’)// { return (!left) || right; } // }; // throw new EvalException(); // // ---------------------- END OF RECURSIVE VERSION ------------------------} // ------------------------------------------------------------------------- 200 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS public static void main(String[] args) throws IOException, ParseException { boolean answer = true; boolean bval = true; System.out.println("Input a propositional formula according to the " + "given grammar, please."); System.out.println("The variables should be between 0 and " + (DIM-1) +"."); Node tree = null; try { // from String to StringReader and to BufferedReader BufferedReader inputLine = new BufferedReader(new InputStreamReader(System.in)); c = (char)inputLine.read(); tree = propParse(inputLine); if (c != ’\n’) { throw new ParseException(); }; // <--- (2) } catch ( IOException e ) { e.printStackTrace(); System.out.println("IOException!"); } catch ( ParseException pex ) { System.out.println(pex.getMessage()); return; }; // ====== (begin 0) ==================================== if (tree != null) { System.out.println("\nParse tree of the propositional formula:\n" +tree); System.out.print("\nIs it a tautology? "); /** -----------------------------------------------------------------------* The following fragment computes the dispositions for tValues[0],..., * tValues[DIM-1] from false,...,false to true,...,true until we finds * a disposition which makes bval == false, if any. In that case the given * formula is NOT a tautology. If, otherwise, all dispositions make * bval == true, then the given formula is a tautology. * ---------------------------------------------------------------------- */ for (int i = 0; i < (int)Math.pow(2,DIM); i++) { try { bval = evalTree(tree); } catch ( EvalException evalex ) { System.out.println(evalex.getMessage()); }; if (!bval) { answer = false; break; } else { for (int k = 0; k < DIM; k++) { if (tValues[k]) { tValues[k] = false; } else { tValues[k] = true; break; } } } }; // -------------------------------------------------------if (answer) { System.out.println("YES."); } else { System.out.println("NO.\nThe falsity assignment is:\n"); for (int k = 0; k < DIM; k++) { System.out.print(k + "=" + tValues[k] + "; ");}; System.out.println(); } // -------------------------------------------------------} // ====== (end 0) ==================================== } } 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 201 /** * input: output: * ------------------------------------------------------------------------* javac PropositionalTheoremProver.java * java PropositionalTheoremProver * * Input a propositional formula according to the given grammar, please. * The variables should be between 0 and 3. * 0>1>0 * * Parse of the propositional formula: * (0 > (1 > 0)) * * Is it a tautology? YES. * ------------------------------------------------------------------------* Input a propositional formula according to the given grammar, please. * The variables should be between 0 and 3. * 0>-1 * * Parse of the propositional formula: * (0 > (-1)) * * Is it a tautology? NO. * The falsity assignment is: * * 0=true; 1=true; 2=false; 3=false; * ------------------------------------------------------------------------* Input a propositional formula according to the given grammar, please. * The variables should be between 0 and 3. * 0>(1+1)+-0x-2 * * Parse tree of the propositional formula: * (0 > ((1 + 1) + ((-0) x (-2)))) * * Is it a tautology? NO. * The falsity assignment is: * * 0=true; 1=false; 2=false; 3=false; * ------------------------------------------------------------------------*/ The following program is like the above one: it parses a given propositional formula and checks whether or not it is a tautology, but it uses a graphical interface. It can be run by typing the following three commands: javac PropositionalTheoremProver.java javac PropositionalTheoremProverGUI.java java PropositionalTheoremProverGUI /** * ========================================================================= * PROPOSITIONAL THEOREM PROVER * with a Graphical User Iterface * Filename: "PropositionalTheoremProverGUI.java" * * This program should be compiled after the compilation of the program * PropositionalTheoremProver.java. * * See also the initial comments of the file PropositionalTheoremProver.java 202 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS * * This program uses the class PropositionalTheoremProver in the file named * PropositionalTheoremProver.java. The file PropositionalTheoremProver.java * should be stored in the same folder where this file * PropositionalTheoremProverGUI.java is stored. * -----------* Lines (1) below depend on the number of propositional variables. * Line (2) below checks whether or not the input propositional formula * is terminated by a carriage-return ’\n’. * -----------* To get a dialog window with font size 14 (instead of 28): set the * boolean variable bw (short for big_window) to false (see line ***). * This program has been generated by a graphical tool with which one can * draw windows and add buttons. * ========================================================================= */ import java.io.*; import javax.swing.JFrame; import java.awt.Font; // needed for changing fonts // ---------------------------------------------------public class PropositionalTheoremProverGUI extends JFrame { /** Creates new form PropositionalTheoremProverGUI */ public PropositionalTheoremProverGUI() { // constructor initComponents(); } /** This method initComponents() is called from within the constructor * to initialize the form. */ private void initComponents() {//GEN-BEGIN:initComponents jLabel1 = new javax.swing.JLabel(); jLabel2 = new javax.swing.JLabel(); jLabel3 = new javax.swing.JLabel(); jLabel4 = new javax.swing.JLabel(); txInForm = new javax.swing.JTextField(); txParsedForm = new javax.swing.JTextField(); txAnswTaut = new javax.swing.JTextField(); btParse = new javax.swing.JButton(); menuBar = new javax.swing.JMenuBar(); fileMenu = new javax.swing.JMenu(); exitMenuItem = new javax.swing.JMenuItem(); getContentPane().setLayout(null); setTitle("PROPOSITIONAL THEOREM PROVER"); setLocationRelativeTo(null); addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent evt) { exitForm(evt); } }); // ----------------------------------------------------------boolean bw = true; // true (false) for big (small) window. *** Font sansSerifFont = bw ? (new Font("SansSerif",Font.PLAIN,28)): (new Font("SansSerif",Font.PLAIN,14)); // ----------------------------------------------------------txInForm.setFont(sansSerifFont); txParsedForm.setFont(sansSerifFont); txAnswTaut.setFont(sansSerifFont); // ----------------------------------------------------------- 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 203 jLabel1.setBackground(new java.awt.Color(255, 255, 150)); jLabel1.setFont(sansSerifFont); jLabel1.setText("<HTML>\n<PRE>\n" +" Grammar for the input propositional formula p:\n" +" prop: p ::= c | c>p cond: c ::= l | l+c | lxc\n" +" lit: l ::= a | -l" +" atom: a ::= ’0’ | ... | ’3’ | (p)\n" // <--- (1) +" Internal tree: p ::= 0 | ... | 3 | (-p) | (p x p)\n"// <--- (1) +" | (p + p) | (p > p)\n" +" Abbreviations: f/false t/true -/not x/and +/or >/implies" +"<PRE>\n</HTML>"); jLabel1.setVerticalAlignment(javax.swing.SwingConstants.TOP); jLabel1.setOpaque(true); getContentPane().add(jLabel1); if (bw) {jLabel1.setBounds(10, 10, 1200, 190);} else {jLabel1.setBounds(5, 5, 565, 97);} // ----------------------------------------------------------jLabel2.setFont(sansSerifFont); jLabel2.setText("Input Formula:"); getContentPane().add(jLabel2); if (bw) {jLabel2.setBounds(20, 208, 200, 32);} else {jLabel2.setBounds(10, 104, 130, 16);} getContentPane().add(txInForm); if (bw) {txInForm.setBounds(280, 208, 860, 40);} else {txInForm.setBounds(130, 104, 430, 20);} // ----------------------------------------------------------jLabel3.setFont(sansSerifFont); jLabel3.setText("Parsed Formula:"); getContentPane().add(jLabel3); if (bw) {jLabel3.setBounds(20, 288, 220, 32);} else {jLabel3.setBounds(10, 144, 130, 16);} txParsedForm.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txParsedForm); if (bw) {txParsedForm.setBounds(280, 288, 860, 40);} else {txParsedForm.setBounds(130, 144, 430, 20);} // ----------------------------------------------------------jLabel4.setFont(sansSerifFont); jLabel4.setText("Is it a tautology?"); getContentPane().add(jLabel4); if (bw) {jLabel4.setBounds(20, 368, 280, 32);} else {jLabel4.setBounds(10, 184, 140, 16);} txAnswTaut.setBackground(new java.awt.Color(25, 255, 25)); getContentPane().add(txAnswTaut); if (bw) {txAnswTaut.setBounds(280, 368, 860, 50);} else {txAnswTaut.setBounds(130, 184, 430, 20);} // ----------------------------------------------------------btParse.setFont(sansSerifFont); btParse.setText("Parse and Evaluate"); btParse.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { btParseActionPerformed(evt); } }); getContentPane().add(btParse); if (bw) {btParse.setBounds(430, 440, 576, 52);} else {btParse.setBounds(240, 215, 158, 26);} // ----------------------------------------------------------- 204 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS fileMenu.setText("FILE_MENU"); exitMenuItem.setText("Exit"); exitMenuItem.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { exitMenuItemActionPerformed(evt); } }); fileMenu.add(exitMenuItem);// adding a File Menu with an Exit button menuBar.add(fileMenu); // setJMenuBar(menuBar); // java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize(); if (bw) {setBounds((screenSize.width-1300)/2, (screenSize.height-700)/2, 1160, 590);} else {setBounds((screenSize.width-444)/2, (screenSize.height-300)/2, 580, 300);} } //GEN-END:initComponents private void btParseActionPerformed(java.awt.event.ActionEvent evt) { // // // // GEN-FIRST:event_btParseActionPerformed Add your handling code here: This part is like the main of the class PropositionalTheoremProver in the file PropositionalTheoremProver.java. boolean answer = true; boolean bval = true; int DIM = PropositionalTheoremProver.DIM; // <--- (1) Node tree = null; try { // from String to StringReader and to BufferedReader. // Added an extra ’\n’ at the end of the input for // terminating the input as in the // PropositionalTheoremProver class. BufferedReader inputLine = new BufferedReader( new StringReader((txInForm.getText()) + ’\n’)); PropositionalTheoremProver.c = (char)inputLine.read(); tree=PropositionalTheoremProver.propParse(inputLine);// <--- (2) if (PropositionalTheoremProver.c != ’\n’) { throw new ParseException(); }; } catch ( IOException e ) { e.printStackTrace(); txParsedForm.setText("IOException!"); } catch ( ParseException pex ){ txParsedForm.setText(pex.getMessage()); return; }; // ------ (begin Parse and Evaluate) if (tree != null) { txParsedForm.setText(tree.toString()); // -------------------------------------------------------for (int i = 0; i < (int)Math.pow(2,DIM); i++) { try { bval = PropositionalTheoremProver.evalTree(tree); } catch ( EvalException evalex ) { txAnswTaut.setText(evalex.getMessage()); }; if (!bval) { answer = false; break; } 7.3. A THEOREM PROVER FOR THE PROPOSITIONAL CALCULUS 205 else { for (int k = 0; k < DIM; k++) { if (PropositionalTheoremProver.tValues[k]) { PropositionalTheoremProver.tValues[k] = false; } else { PropositionalTheoremProver.tValues[k] = true; break; } } } }; // -------------------------------------------------------if (answer) { txAnswTaut.setText("YES."); } else { String outstring = "NO. False if: "; for (int k = 0; k < DIM; k++) { outstring = outstring + k + "=" + ((PropositionalTheoremProver.tValues[k])?"t; ":"f; ");} txAnswTaut.setText(outstring); } // -------------------------------------------------------} // ------ (end Parse and Evaluate) } //GEN-LAST:event_btParseActionPerformed private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) { //GEN-FIRST:event_exitMenuItemActionPerformed System.exit(0); } //GEN-LAST:event_exitMenuItemActionPerformed /** Exit the Application */ private void exitForm(java.awt.event.WindowEvent evt) { //GEN-FIRST:event_exitForm System.exit(0); } //GEN-LAST:event_exitForm /** * @param args the command line arguments */ public static void main(String args[]) { new PropositionalTheoremProverGUI().show(); } // Variables declaration - do not modify //GEN-BEGIN:variables private javax.swing.JMenuItem exitMenuItem; private javax.swing.JMenu fileMenu; private javax.swing.JLabel jLabel1; private javax.swing.JLabel jLabel2; private javax.swing.JLabel jLabel3; private javax.swing.JLabel jLabel4; private javax.swing.JMenuBar menuBar; private javax.swing.JTextField txInForm; private javax.swing.JTextField txParsedForm; private javax.swing.JTextField txAnswTaut; private javax.swing.JButton btParse; // End of variables declaration //GEN-END:variables } 206 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * input: output: * ------------------------------------------------------------------------* javac PropositionalTheoremProver.java // This compilation is needed * javac PropositionalTheoremProverGUI.java * java PropositionalTheoremProverGUI * * (see figures below) * ------------------------------------------------------------------------*/ After typing the string 0>(1+1)+-0x-2 we have: Then by clicking the button ‘Parse and Evaluate’ we get: 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 207 7.4. Encoding of n-ary Trees Using Binary Trees Before formalizing the problem of encoding n-ary trees using binary trees, let us first present the following three data structures which are used in that formalization: (i) binary trees with objects of type T in the nodes, (ii) n-ary trees with objects of type T in the nodes, and (iii) lists of n-ary trees with objects of type T in the nodes. For reasons of brevity, instead of saying ‘binary trees with objects of type T in the nodes’, we will also say: ‘binary trees with nodes of type T’. Analogous terminology will also be used for n-ary trees and lists of n-ary trees. Remark 7.4.1. [Terminology for Nodes and Trees] When referring to trees, we will equivalently use the term ‘node’ and ‘tree’, because every node in a tree denotes the subtree rooted in that node (see also Remark 7.1.1 on page 174). Binary Trees with Nodes of Type T Let V denote the set of objects of type T. We define the set B of the binary trees with objects of type T in the nodes, to be the solution of the following domain equation (see [20, pages 107–124]): B = 1 + B ×V ×B (1) The empty binary tree in the domain 1 is the element e ∈ B, and a non-empty binary tree in the domain B×V ×B is a triple (lb, v, rb), where: v ∈ V is an object of type T, and lb and rb are two binary trees in B with objects of type T. These two trees are said to be the left and right subtrees, respectively, of the given binary tree (lb, v, rb). In the Java 1.5 implementation which we will present below, we have that: (•) the empty binary tree e with nodes of type T is constructed by the nullary constructor BinNode<T>() by using the command new BinNode<T>(), and (•) the non-empty binary tree (lb, v, rb) with nodes of type T is constructed by the ternary constructor BinNode<T>(lb,v,rb) by using the command new BinNode<T>(lb,v,rb) where v is an object of type T, and lb and rb denote two already constructed binary trees with nodes of type T. The selector which tests whether or not a binary tree with nodes of type T is empty, is: isEmpty() Thus, for instance, in the case of binary trees with nodes of type Integer we have that new BinNode<Integer>().isEmpty() returns true. Here is a program which constructs binary trees with nodes of type T. /** * ========================================================================= * Class which constructs a binary node with two child nodes * Filename: "BinNode.java" * * Binary trees with nodes of type T are internally represented as follows: * * bt<T> ::= e | (bt<T>, val, bt<T>) where val is of type T * 208 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS * e is the empty binary node * constructors: new BinNode<T>() | new BinNode<T>(-,-,-) * selector: new BinNode<T>().isEmpty() is true (the BinNode<T> is e) * * -----------* constructing e: new BinNode() * * constructing an non-empty binary node of type Integer, which is * 7 with empty child nodes: * * new BinNode<Integer>(new BinNode<Integer>(),7, new BinNode<Integer>()) * * Notice that Java 1.5 accepts: 7, instead of: new Integer(7). * ========================================================================= */ public class BinNode<T> { private T value; private BinNode left; private BinNode right; // value field of type T // reference to the left child node // reference to the right child node public BinNode ( ) { this.value = null; } // constructor 1: empty BinNode e // for constructor 1: this.left == null is true // for constructor 1: this.right == null is true /* * Constructor of a Node with: * - value v, * - parameters l and r, which are references to the left and right * child nodes, respectively. */ public BinNode ( BinNode l, T v, BinNode r ) { // constructor 2 this.left = l; this.value = v; this.right = r; } /** * @return the value. */ public T getValue() { return value; } /** * @return the left child node. */ public BinNode getLeftChild(){ return left; } /** * @return the right child node. */ public BinNode getRightChild(){ return right; } /** * @return true iff the value of the BinNode is the null reference. * * Each of the following prints: true 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 209 * System.out.print(new BinNode().isEmpty()); * System.out.print(new BinNode<Integer>().isEmpty()); * System.out.print(new BinNode(null,null,null).isEmpty()); * System.out.print(new BinNode<Integer>(null,null,null).isEmpty()); * * The following two instructions: * BinNode<Integer> b = null; * System.out.println(b.isEmpty()); * give a NullPointerException */ public boolean isEmpty() { return value == null; } /** * This method prints the tree whose root is the given node. * We assume that ‘value’ can be printed as a string. */ public void binNodePrint(){ if (value == null) {System.out.print("e");} else { System.out.print("("); left.binNodePrint(); System.out.print(" "+value+" "); right.binNodePrint(); System.out.print(")"); } } } // ------------------------------------------------------------------------- n-ary Trees with Nodes of Type T Let V denote the set of objects of type T. We define the set T of the n-ary trees with objects of type T in the nodes, to be the solution of the following domain equations: T = 1 + V ×Tlist Tlist = 1 + T ×Tlist (2) The empty n-ary tree with nodes of type T in the domain 1 is the element E ∈ T , and a non-empty n-ary tree with nodes of type T in the domain V ×Tlist is a pair (v, L), where: v ∈ V is an object of type T, and L ∈ Tlist is a list of n-ary trees with nodes of type T, which is constructed as indicated on page 212. In the Java 1.5 implementation which we will present below, we have that: (•) the empty n-ary tree E with nodes of type T is constructed by the nullary constructor NaryNode<T>() by using the command new NaryNode<T>(), and (•) the non-empty n-ary tree (v, L) with nodes of type T is constructed by the binary constructor NaryNode<T>(v,L) by using the command new NaryNode<T>(v,L) where v is an object of type T and L is a list of n-ary trees with nodes of type T. The selector which tests whether or not an n-ary tree with nodes of type T is empty, is: isEmpty() Thus, for instance, in the case of n-ary trees with nodes of type Integer we have that new NaryNode<Integer>().isEmpty() returns true. 210 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS Here is a program which constructs n-ary trees with nodes of type T. /** * ========================================================================= * Class which constructs an n-ary node with a list of child nodes * Filename: "NaryNode.java" * * N-ary trees with nodes of type T are internally represented as follows: * * nt<T> ::= E | (val, list_nt<T>) where val is of type T * * E is the empty n-ary tree. * list_nt<T> is a list of n-ary trees with nodes of type T. * constructors: new NaryNode<T>() | new NaryNode<T>(-,-) * selector: new NaryNode<Integer>().isEmpty() is true * (indeed, the NaryNode<Integer> is E) * --------* Lists of n-ary trees are internally represented as follows: * * list_nt<T> ::= [] | nt<T> : list_nt<T> * * cons is denoted by the infix ‘:’. In the implementation using ArrayList * the last element which is cons-ed is to the right (not to the left). * * constructors: new List<T>() | l.cons(nt) where l is a list of n-ary trees * of type <T> and nt is an n-ary tree of type <T> * selector: new List<Integer>().isEmpty() is true * (indeed, the List<Integer> is []) * ------------------------------------------------------------------------* constructing E: new NaryNode<Integer>() * * constructing a non-empty n-ary node of type Integer, which is * 8 with an empty list of child nodes: * * new NaryNode<Integer>(8, new List<NaryNode<Integer>>()) * * Notice that Java 1.5 accepts: 8, instead of: new Integer(8). * ========================================================================= */ import java.util.*; // needed for using ArrayList<T> and Iterator public class NaryNode<T> { private T value; private List <NaryNode<T>> list; // value of the n-ary node of type T // list of child nodes /* * Constructor of a Node with: * - value v, * - parameter l, which is a reference to the list of child nodes. */ public NaryNode ( ) { this.value = null; } // constructor 1: empty NaryNode E // for constructor 1: this.list == null is true public NaryNode ( T value, List<NaryNode<T>> list ) { // constructor 2 this.value = value; // for: NaryNode <Integer> n1 this.list = list; // = new NaryNode(1,new List()) } // n1.list.isEmpty() is true 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 211 /** * @return the list of child nodes. */ public List <NaryNode<T>> getListOfChildren(){ return list; } /** * @return the value at the node. */ public T getValue(){ return value; } /** * @return true iff the value is the null reference. * * The following two instructions: * System.out.println(new NaryNode().isEmpty()); * System.out.println(new NaryNode(null,null).isEmpty()); * both return true. */ public boolean isEmpty() { return value == null; } /** * This method prints the tree whose root is the given node. * We assume that ‘value’ can be printed as a string. */ public void naryNodePrint(){ if (value == null) { System.out.print("E"); } // empty n-ary tree else { System.out.print(value+" ["); for (int i=list.size()-1; i>=0; i--) { ((NaryNode)(list.get(i))).naryNodePrint(); if (i > 0) {System.out.print(", ");} }; System.out.print("]"); } } } /** * ------------------------------------------------------------------------* We have that: * * NaryNode <Integer> n0 = new NaryNode(); * System.out.println(n0.list == null); // prints: true * System.out.println((n0.list).isEmpty());// raises a NullPointerException! * * System.out.println(new NaryNode().isEmpty()); // prints: true * System.out.println(new NaryNode<Integer>().isEmpty()); // prints: true * * NaryNode <Integer> n1 = new NaryNode(1,new List()); * System.out.println(n1.list == null); // prints: false * System.out.println(n1.list.isEmpty()) // prints: true * ------------------------------------------------------------------------*/ 212 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS Lists of n-ary Trees with Nodes of Type T The lists of n-ary trees with objects of type T are elements of the domain Tlist which has been defined by the domain equations (2) on page 209. For any type T, the empty list of n-ary trees with nodes of type T in the domain 1 is the element [ ], and a non-empty list of n-ary trees with nodes of type T is a pair h : H, where: (i) h ∈ T is an n-ary tree with nodes of type T, (ii) ‘:’ denotes the infix cons operation on lists, and (iii) H ∈ Tlist is a list of n-ary trees with nodes of type T. In the Java 1.5 implementation of the lists with nodes of trees of type T which we will present below, we have that: (•) the empty list [ ] of n-ary trees with nodes of type T is constructed by the nullary constructor List<NaryNode<T>>() by using the command new List<NaryNode<T>>(), and (•) the non-empty list of n-ary trees with nodes of type T is constructed from an already constructed, shorter list H of n-ary trees with nodes of type T, by cons-ing onto it an n-ary tree nt with nodes of type T by using the following command: H.cons(nt) The selector which tests whether or not a list of n-ary trees with nodes of type T is empty, is: isEmpty() Thus, for instance, in the case of lists of n-ary trees with nodes of type Integer we have that new List<NaryNode<Integer>>().isEmpty() returns true, because new List<NaryNode<Integer>>() is the empty list [ ] of n-ary trees with nodes of type Integer. Here is the program List.java which is the Java 1.5 implementation of the generic class of the lists of elements of type T. It is similar to the program, also called List.java, presented in Section 3.2 on page 39 where the reader may found some examples of use of the methods of the generic class List. /** ======================================================================== * Generic Class List <T> (class used for encoding trees) * Filename: "List.java" * * Every object of the class List<T> is a list of elements of type T. * The following methods are available: * cons(T d), head(), tail(), size(), get(int i), isSingleton(), * isEmpty(), makeEmpty(), copy(), and listPrint(). * Also cloneCopy() is available if we uncomment line (***) below and * comment the previous line. * * Notice that the tail() method is destructive. * After a tail operation, if we need the original value of the list, we * should reconstruct it by ‘consing’ the head of the list. * --------------------* Lists of elements nt of type T are internally represented as follows: * * list_nt<T> ::= [] | nt<T> : list_nt<T> * * cons is denoted by the infix ‘:’. In the implementation using ArrayList 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 213 * the last element which is cons-ed is to the right (not to the left). * * constructors: new List<T>() | l.cons(t) where l is a list of objects * type <T> and t is an object of type <T> * selector: new List<Integer>().isEmpty() is true * (indeed, the new List<Integer> is []) * new List().isEmpty() is true * --------------------* List la = new List(); la.cons(3); la.listPrint(); // prints: [ 3 ] * List lb = new List<Integer>(); * lb.cons(6); lb.listPrint(); // prints: [ 6 ] * ========================================================================= */ import java.util.*; public class List<T> { // needed for using ArrayList<T> and Iterator // it is ok if we do not use cloneCopy() // public class List<T> implements Cloneable { // (***) In order to use // cloneCopy(), uncomment this line and comment the previous one. private ArrayList<T> list; public List() { list = new ArrayList<T>();// constructor } public void cons(T datum) { list.add(datum); } // the head of the list is to the right, // not to the left. // add(_) is a method of ArrayList<T> public T head() { // the head of the list is to the right. if (list.isEmpty()) // It is the last element: >----------------+ {System.out.println("Error: head of empty list!");};// | T obj = list.get(list.size() - 1); // <----------------------+ return obj; // isEmpty(), size(), and get(int i) } // are methods of ArrayList<T> public void tail() { // destructive tail() method if (list.isEmpty()) {System.out.println("Error: tail of empty list!");}; list.remove(list.size() - 1); // isEmpty(), size(), and remove(int i) } // are methods of ArrayList<T> public int size() { return list.size(); } public T get(int i) { return list.get(i); } // size() is a method of ArrayList<T> // 0 <= i <= size() - 1 // get(int i) is a method of ArrayList<T> public boolean isSingleton() { return list.size() == 1; // size() is a method of ArrayList<T> } public boolean isEmpty() { return list.isEmpty(); } // isEmpty() is a method of ArrayList<T> 214 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS public void makeEmpty() { list.clear(); } // clear() is a method of ArrayList<T> // -------------------------- Printing a list ---------------------------// We assume that an element of the list has a toString() method. public void listPrint() { if ( !isEmpty() ){ System.out.print("[ "); for (Iterator iter = list.iterator(); iter.hasNext(); ) { System.out.print((iter.next()).toString() + " "); }; System.out.print("]"); } else {System.out.print("[]");} } // -------------------------- Making a copy of a list without clone() ---// -------------------------- The given list is NOT destroyed. ----------public List<T> copy() { List<T> copyList = new List<T>(); for (Iterator<T> iter = list.iterator(); iter.hasNext(); ) { copyList.list.add(iter.next()); }; return copyList; } // -------------------------- Making a copy of a list with clone() ------// -------------------------- The given list is NOT destroyed. ----------public List<T> cloneCopy() { // see (***) above try { List<T> copyList = (List<T>)super.clone(); copyList.list = (ArrayList<T>)list.clone(); return copyList; } catch (CloneNotSupportedException e) { throw new InternalError(); } } } /* ---------------------------------------------------------------------- */ Solving the Encoding Problem Let us consider the set N of the natural numbers. In what follows by B and T we denote, respectively, the set of the binary trees and n-ary trees with natural numbers in the nodes which we have defined on pages 207 and 209. Every n-ary tree in T can be encoded into a binary tree in B via the encoding function Kin : T → B, and can be decoded from a binary tree via the decoding function Hin : B → T . These two functions, which are defined in the Java program EncodingTrees.java listed on page 215, are such that for every nary tree nt, we have that: Hin(Kin(nt)) = nt. In order to execute the program EncodingTrees.java we need the following Java programs: (i) BinNode.java, (ii) NaryNode.java, and (iii) List.java, which implement, respectively, (i) the binary trees in B with nodes of type T (see page 207), (ii) the n-ary trees in T with nodes of type T (see page 209), and (iii) the 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 215 lists in Tlist of elements of type T, for some type parameter T (see page 212). The actual type of those trees and lists is given by instantiating the type parameter T, as allowed in Java 1.5. For instance, in the case of lists, if we instantiate the type T to <NaryNode <Integer>>, we get the lists of n-ary trees with nodes of type Integer. In the program EncodingTrees.java an n-ary tree with nodes of type Integer to be encoded into a binary tree with nodes of type Integer, is given as a string nt generated by the following productions: nt ::= E | ne_nt ne_nt ::= m | m ( ne_list ) ne_list ::= ne_nt | ne-nt , ne_list where: (i) E is the empty n-ary tree, (ii) comma and parentheses are terminal symbols, and (iii) m is any integer number in the set {...,-2,-1,0,1,2,...}. In this representation we have used the notions of an ne_nt (that is, a non-empty n-ary tree) and ne_list (that is, a non-empty list of non-empty n-ary trees). This choice of using non-empty n-ary trees has the advantage that a node with value m and no son nodes, can be given in input as the string m, rather than the longer string m ([ ]). The internal representation of an n-ary tree nt is done according to domain equations (2) (see page 209), that is, it is generated by the following productions: nt ::= E | m ( list ) list ::= [ ] | nt : list where we have used the empty list [ ] and the infix cons operation denoted ‘:’. Here is a program for encoding n-ary trees into binary trees and decoding binary trees back to n-ary trees by using the functions Kin and Hin. /** * ========================================================================= * ENCODING n-ARY TREES INTO BINARY TREES * Filename: "EncodingTrees.java" * * This program defines the encoding method Kin from n-ary trees to binary * trees. It also defines an inverse method Hin such that * for every n-ary tree nt, we have that Hin(Kin(nt)) = nt. * Grammar of the input n-ary tree nt: * n-ary tree: nt ::= E | ne_nt * * non-empty n-ary tree: ne_nt ::= m | m(ne_list) * * non-empty list of non-empty n-ary trees: * ne_list ::= ne_nt | ne_nt , list_list * * where: E is the empty n-ary tree, * m can be any integer in {...,-2,-1,0,1,2,...} * Comma and parentheses are terminal symbols. * ------------------------------------------------------------------------* The internal representations of binary trees, n-trees, and lists of n-ary * trees are provided by objects of the classes: BinNode, NaryNode, and List * ========================================================================= */ import java.io.*; import java.util.*; // needed for using ArrayList<T> and Iterator 216 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS /** * ========================================================================= * Class ParseException * ========================================================================= */ class ParseException extends Exception { // constructor of a new instance of ParseException public ParseException() { super("parse exception!"); } }; public class EncodingTrees { /** * ------------------------------------------------------------------------* In order to define the encoding and decoding functions Kin, K, Hin, and H * which are given below, we use the following representation of binary * trees and n-ary trees. These representations make use of the ternary * constructor b(-,-,-) and the binary constructor T(-,-). These * constructors are used only for this purpose and in the comments below. * * binary tree: bt = e | b(bt1,n,bt2) * n-ary tree: nt = E | T(n,list_nt) (list_nt is the list of sons of n) * list_nt = [] | nt : list_nt * * e is the empty binary tree * E is the empty n-ary tree * n is a natural number * ------------------------------------------------------------------------* +++ Kin : nt -> bt // Kin encodes an nt into a bt * --- Kin(E) = e * --- Kin(T(n,l)) = K(T(n,l),[]) * * +++ K : nt x list_nt -> bt // nt is not equal to E * --- K(T(n,[]),[]) = b(e,n,e) // ex: node 9 * --- K(T(n,[]),h:l) = b(e,n,K(h,l)) // ex: node 2 * --- K(T(n,h:l),[]) = b(K(h,l),n,e) // ex: node 4 * --- K(T(n,h:l),h1:l1) = b(K(h,l),n,K(h1,l1))// ex: node 3 * ------------------------------------------------------------------------*/ public static BinNode<Integer> Kin (NaryNode<Integer> nt) { if (nt.isEmpty()) { return new BinNode(); } else { return K(nt, new List <NaryNode<Integer>> ());} } public static BinNode<Integer> K (NaryNode <Integer> nt, List <NaryNode <Integer>> lnt ) { if ( { else { nt.getListOfChildren().isEmpty() && lnt.isEmpty()) return new BinNode(new BinNode(), nt.getValue(), new BinNode());} if ( nt.getListOfChildren().isEmpty() && !lnt.isEmpty() ) NaryNode<Integer> hd1 = lnt.head(); lnt.tail(); BinNode<Integer> res = new BinNode(new BinNode(), nt.getValue(), K(hd1, lnt)); lnt.cons(hd1); // restoring the list return res; } else if ( !nt.getListOfChildren().isEmpty() && lnt.isEmpty() ) { NaryNode<Integer> hd2 = (nt.getListOfChildren()).head(); (nt.getListOfChildren()).tail(); 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 217 BinNode<Integer> res = new BinNode(K(hd2, nt.getListOfChildren()), nt.getValue(), new BinNode()); (nt.getListOfChildren()).cons(hd2); // restoring the list return res; } else { NaryNode<Integer> hd3 = (nt.getListOfChildren()).head(); (nt.getListOfChildren()).tail(); NaryNode<Integer> hd4 = lnt.head(); lnt.tail(); BinNode<Integer> res = new BinNode(K(hd3, nt.getListOfChildren()), nt.getValue(), K(hd4, lnt)); (nt.getListOfChildren()).cons(hd3); // restoring the list lnt.cons(hd4); // restoring the list return res; } } /** * ------------------------------------------------------------------------* +++ Hin : bt -> nt * --- Hin(e) = E * --- Hin(b(bt1,n,e)) = T(n,H(bt1)) //bt1 may be equal to e * * +++ H : bt -> list_nt * --- H(e) = [] * --- H(b(bt1,n,bt2)) = T(n,H(bt1)) : H(bt2)//bt1 and bt2 may be equal to e * ------------------------------------------------------------------------*/ public static NaryNode <Integer> Hin (BinNode <Integer> bt) { if (bt.isEmpty()) { return new NaryNode(); } else { return new NaryNode(bt.getValue(), H(bt.getLeftChild()) );} } public static List <NaryNode <Integer>> H (BinNode <Integer> bt) { if ( bt.isEmpty()) { return new List(); } else { NaryNode<Integer> hd = new NaryNode(bt.getValue(), H(bt.getLeftChild()) ); List <NaryNode<Integer>> lnt = new List(); lnt = H(bt.getRightChild()); lnt.cons(hd); return lnt; } } // ------------------- parsing the input n-ary node -----------------------// RECURSIVE DESCENT PARSING public static char c; // inputLine is a sequence of characters, // which are read one at a time, by // the method read() of the class StringReader public static NaryNode <Integer> nodeParse(BufferedReader inputLine) throws IOException, ParseException { NaryNode <Integer> node1 = new NaryNode(); if ( c == ’E’ ) { c = (char)inputLine.read(); return node1; } else if ((’0’ <= c) && (c <= ’9’)) { int v = c-48; node1 = new NaryNode(v, new List()); c = (char)inputLine.read(); 218 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS if ( c == ’(’ ) { c = (char)inputLine.read(); List <NaryNode <Integer>> listNodes = listNodeParse(inputLine); node1 = new NaryNode(v, listNodes); if ( c==’)’ ) { c = (char)inputLine.read(); } // reads past ’)’ else { throw new ParseException(); }; return node1; } else { return node1; } }; throw new ParseException(); } public static List <NaryNode <Integer>> listNodeParse( BufferedReader inputLine) throws IOException, ParseException { List <NaryNode <Integer>> listNodes = new List(); NaryNode <Integer> node1 = nodeParse(inputLine); if ( c == ’,’ ) { c = (char)inputLine.read(); listNodes = listNodeParse(inputLine); }; listNodes.cons(node1); return listNodes; } // ---------------- end of parsing ----------------------------------------// ---------------- main --------------------------------------------------public static void main(String [] args ) { System.out.println("Input an n-ary tree nt according to the given " + "grammar, please."); NaryNode <Integer> nNode = new NaryNode(); BinNode <Integer> bNode = new BinNode(); try { // from String to StringReader and to BufferedReader BufferedReader inputLine = new BufferedReader(new InputStreamReader(System.in)); c = (char)inputLine.read(); nNode = nodeParse(inputLine); if (c != ’\n’) { throw new ParseException(); }; } catch ( IOException e ) { e.printStackTrace(); System.out.println("IOException!"); } catch ( ParseException pex ) { System.out.println(pex.getMessage()); return; }; // -----System.out.print("\nParsing of the n-ary tree:\n nt = "); nNode.naryNodePrint(); System.out.print("\nBy applying Kin " + "we get the encoding binary tree:\n bt = "); bNode = Kin(nNode); bNode.binNodePrint(); NaryNode <Integer> nNode1 = new NaryNode(); nNode1 = Hin(bNode); System.out.print("\nBack to the n-ary tree nt using Hin:\n" +" nt = "); nNode1.naryNodePrint(); System.out.println(); } } 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 219 /** * input: output * ------------------------------------------------------------------------* javac EncodingTrees.java * java EncodingTrees * Input an n-ary tree nt according to the given grammar, please. * E * * Parsing of the n-ary tree: * nt = E * By applying Kin we get the encoding binary tree: * bt = e * Back to the n-ary tree nt using Hin: * nt = E * * ------------------------------------------------------------------------* 1(2) * * Parsing of the n-ary tree: * nt = 1 [2 []] * By applying Kin we get the encoding binary tree: * bt = ((e 2 e) 1 e) * Back to the n-ary tree nt using Hin: * nt = 1 [2 []] * * ------------------------------------------------------------------------* 1(2,3(5,6),4(7(9),8)) * * Parsing of the n-ary tree: * nt = 1 [2 [], 3 [5 [], 6 []], 4 [7 [9 []], 8 []]] * By applying Kin we get the encoding binary tree: * bt = ((e 2 ((e 5 (e 6 e)) 3 (((e 9 e) 7 (e 8 e)) 4 e))) 1 e) * Back to the n-ary tree nt using Hin: * nt = 1 [2 [], 3 [5 [], 6 []], 4 [7 [9 []], 8 []]] * * -------* that is: nt bt * * 1 1 * / | \ Kin / \ * 2 3 4 -----> 2 e * /| | \ <----/ \ * 5 6 7 8 Hin e 3 * | / \ * 9 5 4 * /| |\ * e 6 7 e * /| |\ * e e 9 8 * /| |\ * e e e e * * ------------------------------------------------------------------------*/ We have the following properties. (1) In T(n,list-nt), that is, in NaryNode(n,list-nt) we have that list-nt is the list of the son nodes, if any, of the node n in the left-to-right order. 220 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS (2) In K(t,lt) the list lt is the list of the brother nodes, if any, of the node t in the left-to-right order. (3) The first argument of K is never equal to the empty n-ary tree E. The proof of this property is based on the fact that for each call of K(T(n,l1),l2), the tree E is a member of neither the list l1 nor the list l2. The easy proof by computational induction is left to the reader. Exercise 7.4.2. Define the domain B ′ of the binary trees and the domain T ′ of the n-ary trees to be the solutions of the following domain equations, where N denotes the set of natural numbers and Tlist ′ denotes the domain of the list of the n-ary trees in T ′ : B ′ = 1 + N + B ′ ×N ×B ′ T ′ = 1 + N + N ×Tlist ′ Tlist ′ = 1 + T ′ ×Tlist ′ Define the functions Rin : T ′ → B ′ and Sin : B ′ → T ′ such that for every n-ary tree nt we have that: Sin(Rin(nt)) = nt. Hint: A possible implementation of these domains is as follows: /** * ---------------------------------------------------------------------* binary tree: bt = e | l(n) | b(bt1,n,bt2) * n-ary tree: nt = E | L(n) | T(n,list_nt) * list_nt = [] | nt : list_nt * ---------------------------------------------------------------------*/ Here are some examples of how the functions Rin and Sin should behave: n-ary tree NT1 binary tree BT1 1 2 3 4 ✛ 1 Rin ✲ e 2 Sin e 5 6 7 8 9 3 5 4 e 6 7 e 9 8 The following picture shows that: Rin(T(L(1),[L(3)])) = b(l(3),1,e). n-ary tree NT2 binary tree BT2 1 ✛ 3 1 Rin ✲ Sin 3 e 7.4. ENCODING OF n -ARY TREES USING BINARY TREES 221 The following picture shows that: Rin(L(1)) = l(1). n-ary tree NT3 ✛ 1 Rin ✲ binary tree BT3 1 Sin Here are the definitions of the functions Rin and Sin. They use the auxiliary functions R and S, respectively. The example nodes 2, . . . , 9 which we will consider in the comments of these auxiliary definitions, refer to the n-ary tree NT1 and the binary tree BT1 we have depicted above. ------------------------------------------------------------------------+++ ------- Rin : nt -> Rin(E) Rin(L(n)) Rin(T(n,l)) bt = e = l(n) = R(T(n,l),[]) +++ --------- R : nt x list_nt -> R(L(n),[]) R(L(n),a:l) R(T(n,a:l),[]) R(T(n,a:l),a1:l1) // Rin encodes an nt into a bt // empty list of brothers of t bt // list_nt is = l(n) = b(e,n,R(a,l)) = b(R(a,l),n,e) = b(R(a,l),n,R(a1,l1)) the list of // example: // example: // example: // example: brothers of nt node 9 node 5 node 4 node 3 ------------------------------------------------------------------------+++ Sin : bt -> nt --- Sin(e) = E --- Sin(l(n)) = L(n) --- Sin(b(bt1,n,e)) = T(n,S(bt1)) // bt1 is not equal to e +++ S : bt -> list_nt --- S(l(n)) = [L(n)] // nodes 6, 9, and 8: // leaves with no younger brothers. --- S(b(e,n,bt1)) = L(n) : S(bt1) // nodes 2 and 5: // leaves with younger brothers. --- S(b(bt1,n,e)) = [T(n,S(bt1)] // node 4. --- S(b(bt1,n,bt2) = T(n,S(bt1)) : S(bt2)// nodes 3 and 7. ------------------------------------------------------------------------- We have the following properties. (1) The first argument of R is never equal to the empty n-ary tree E. The proof of this property is based on the fact that for each call of R(T(n,l1),l2) and R(L(n),l2), the tree E is a member of neither the list l1 nor the list l2. The easy proof by computational induction is left to the reader. (2) Let us assume that Sin takes as input a binary tree which can be obtained as the result of applying Rin to an n-ary tree. Then, in the above equations defining the function S (the ones preceded by –--’s), the trees bt1 and bt2 are not equal to the empty binary tree e.  We will end this section by providing the definitions of the binary trees and the n-ary trees in Pascal and C++. In Pascal using variant records, binary trees are realized by the following type definition: 222 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS ------------------------------------------------------------------------type B = ^cell; cell = record case tag : (no_son, two_sons) of no_son: (); two_sons: (value: integer; lson, rson: B) end; ------------------------------------------------------------------------- In C++ binary trees are realized by the following class definition: ------------------------------------------------------------------------class cell { public : int value; cell* lson; cell* rson; cell(int v, cell* l, cell* r) : value(v), lson(l), rson(r) { }; }; typedef cell* B; ------------------------------------------------------------------------- Exercise 7.4.3. Define in Pascal and in C++ the domain B of the binary trees which are solutions of the following domain equation, where N denotes the set of natural numbers: B = 1 + N + B ×N ×B  In Pascal using variant records n-ary trees are realized by the following type definition: ------------------------------------------------------------------------type T = ^Tcell; Tlist = ^Tlistcell; Tcell = record case tag : (empty, non_empty) of empty: (); non_empty: (atom: integer; next: Tlist) end; Tlistcell = record case tag : (emptyl, non_emptyl) of emptyl: (); non_emptyl: (value: T; sons: Tlist) end; ------------------------------------------------------------------------- We could have also defined the record Tlistcell as follows: Tlistcell = record value: T; sons: Tlist end; In this case: (i) a pointer to Tlistcell implements an element of the domain T×Tlist (see page 209), and (ii) the NULL pointer implements the element of the domain 1 in the equation Tlist = 1 + T ×Tlist (see again page 209). In C++ n-ary trees are realized by the following class definition: ------------------------------------------------------------------------class Tlistcell; typedef Tlistcell* Tlist; class Tcell { public : int atom; Tlist next; Tcell(int at, Tlist nx) : atom(at), next(nx) { }; }; typedef Tcell* T; class Tlistcell { public : T value; Tlist sons; Tlistcell(T val, Tlist ss) : value(val), sons(ss) { }; }; ------------------------------------------------------------------------- 7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 223 Exercise 7.4.4. Define in Pascal and in C++ the domain T of the n-ary trees which are solutions of the following domain equations, where N denotes the set of natural numbers and Tlist denotes the domain of the list of the n-ary trees in T : T = 1 + N + N ×Tlist Tlist = 1 + T ×Tlist  7.5. Minimal Spanning Tree of an Undirected Graph In this section we describe an algorithm for constructing a minimal spanning tree of an undirected, connected, weighted graph with N nodes. The weight (also called length) of any arc is an integer which can be either positive or null or negative. The lengths can be non-Euclidean, in the sense that if the arc i j has length dij and the arc j k has length djk then the length dik of the arc i k, if it exists, need not be related to the values of dij and djk . The length of an arc i j is also called the distance from node i to node j (which is equal to the distance from node j to node i because the graph is undirected). There could be cycles with total negative length. In the adjacency matrix N ×N which represents the lengths of the arcs of the graph, the entry ‘∞’ at row i and column j, for i, j = 0, . . . , N −1, denotes the absence of the arc from node i to node j. We assume that there is no arc from node i to node i itself, for i = 0, . . . , N −1, but in the adjacency matrix at row i and column i, we have put the value 0 (because 0 is the neutral element for addition). In the program below, for simplicity reasons, we have assumed that the lengths are all non-negative integers, and we have encoded the absence of an arc by the value ‘−1’. A brute force algorithm for constructing a minimal spanning tree of an undirected, connected, weighted graph consists in: (i) constructing all possible spanning trees, and then (ii) taking a minimal one. This obvious algorithm is very inefficient because, by Caley’s formula, there are N N −2 distinct spanning trees of a given undirected, connected graph with N nodes. In Figure 7.5.1 on the next page we have depicted the 16 spanning trees of a graph with four nodes placed at the vertexes of a square and the six arcs connecting every pair of nodes. Such graphs with all possible arcs are called clicks. Now we will present an algorithm which is much more efficient than the brute force one. This efficient algorithm was first proposed by R. C. Prim and independently by E. W. Dijkstra. We will call this algorithm the Minimal Spanning Tree algorithm, or MST algorithm, for short. The basic idea of the algorithm is as follows. We start from a node, say p, of the given graph whose nodes can be colored either red or blue. We assume that initially all nodes are blue, except for the chosen node p which is red. Then we choose a blue node, say q, which is connected to node p by an arc which has a minimal length among all the arcs which have node p as one of their two vertexes. We color red this second node q. By doing so we have a first red arc between p and q which will be part of the minimal spanning tree to be constructed. We proceed by acquiring in the red part of the graph a new blue node which will then be colored red together with the arc which connects this new red node to the red part, until all nodes are colored red. 224 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS 1 2 1 2 1 2 1 2 1 2 4 3 4 3 4 3 4 3 4 3 1 2 1 2 1 2 1 2 G: ← (α) 4 3 4 3 4 3 4 3 (µ) 1 2 1 2 1 2 1 2 ← (β) 4 3 4 3 4 3 4 3 1 2 1 2 1 2 1 2 4 3 4 3 4 3 4 3 Figure 7.5.1. The 16 (= 44−2 ) spanning trees of a undirected, connected graph G with four nodes. The spanning trees of rows (α) and (β) are mirrow images (modulo the names of the nodes) w.r.t. the line (µ). Thus, during the execution of the algorithm the given graph of which we must compute a minimal spanning tree, is divided into two parts: (i) a red part, called Red , for which we have already computed a minimal spanning tree, and (ii) a blue part, called Blue, from which we have to choose a node which will become red and by doing so, a new arc is added to the minimal spanning tree to be computed. Ties of the length of the arcs can be resolved in any way we like, and this is why through this algorithm we actually compute a minimal spanning tree, rather than the minimal spanning tree. (Obviously, for all minimal spanning trees the total length of the arcs is the same.) In what follows, for reasons of simplicity, we will free to say ‘the minimal spanning tree’, instead of ‘a minimal spanning tree’. Similarly, we will feel free to say ‘the shortest arc’, instead of ‘a shortest arc’. The correctness of the MST algorithm can be derived, as shown in [6], from the following three points. Point (i). The local optimum is a global optimum. Point (ii). The use of the function inversion technique. Point (iii). The use of an (N −1)×2 array. Point (i). The local optimum is a global optimum. This fact allows us to derive a greedy algorithm for computing the spanning tree which we compute ‘by addition of nodes’, as described above. Suppose that V is the shortest arc connecting a red node to a blue node (see Figure 7.5.2 on the facing page). 7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 225 Red part V V′ expanding the Red part Blue part Figure 7.5.2. Construction of a minimal spanning tree. The Red part shows the portion of the final minimal spanning tree for the points which now are red. The Blue part shows the portion of the final minimal spanning tree for the points which now are blue. This arc V must be an arc of the spanning tree. We prove this fact by contradiction. Let us assume that: (i) a different arc, call it V ′ , instead of V , is in the minimal spanning tree connecting a red node to a blue node, and (ii) the weight of V ′ is greater than the weight of V . Call T ′ this pretending minimal spanning tree which includes the arc V ′ , instead of the arc V . Now from the tree T ′ we construct a different spanning tree T which is shorter than T ′ by replacing the arc V ′ by the arc V . Indeed, (i) T connects all nodes of the graph because the blue node of V is connected to the blue node of V ′ , and (ii) T is a tree because no loops are generated by the substitution of V ′ by V , because if a loop is generated, it should go through another arc from the red nodes to the blue nodes and there is no such arc, simply because we took the arc V ′ away. At this point the structure of the algorithm is as follows, where we called violet an arc which has a red node and a blue node. Recall that N is the number of nodes in the given undirected graph. color red one node and color blue all the remaining nodes; while the number of red nodes is less than N do begin Step (1). Select the shortest violet arc. Step (2). Color it red and color red its blue node. end Point (ii). The use of the function inversion technique. The choice of the shortest violet arc can be done in two ways: either (ii.1) for each red node we compute the shortest arc from it to a blue node, and then we take the shortest arc among these minimal arcs, one for each red node, or (ii.2) for each blue node we compute the shortest arc from it to a red node, and then we take the shortest arc among these minimal arcs, one for each blue node. 226 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS Red part R1 Blue part 3 R2 4 B1 R3 5 6 7 min(3, 4, 7) = 3 2 B2 5 min(2, 6, 5) = 2 7 9 B3 min(7, 5, 9) = 5 min( 3 , 2 , 5 ) = 2 Figure 7.5.3. Computation of the minimal arc connecting the Red part to the Blue part. The arcs with thicker lines are the so called ultraviolet arcs. There is exactly one ultraviolet arc for each blue node. Given a blue node its ultraviolet arc is the shortest arc from that blue node to a red node. We did not draw every arc: more arcs may be present within the Red part or the Blue part. Since the given graph is undirected, these two choices are both correct. However, from a complexity point of view they are not equivalent, because we are computing the spanning tree by making the Red part larger, while the Blue part is shrinking. Indeed, in the case of Choice (ii.2), when a blue node becomes red, the computation of the shortest arc for each blue node requires one comparison only, while in the case of Choice (ii.1), when a blue node becomes red, the computation of the shortest arc for each blue node requires a number of comparisons which is the product ‘current number of red nodes’ × ‘current number of blue nodes’. This technique which looks at the distances for each blue nod e towards to red nodes, instead of looking at the distances from each red node towards the blue nodes, is called function inversion. The word ‘inversion’ comes from the fact that we are extending the Red part, while looking at the distances from each blue node. In Figure 7.5.3 we show how the computation of the shortest violet arc is done in the case of three red nodes: R1, R2, and R3, and three blue nodes: B1, B2, and B3. First, we compute the shortest violet arc for the node B1, the node B2, and the node B3. These three shortest arcs, which are called ultraviolet arcs, are drawn with thicker lines in that figure. The ultraviolet arc of a blue node is the shortest arc which connects that blue node to a red node. Then we compute the shortest arc among these three shortest ultraviolet arcs. In our case it is the arc from node B2 to node R1 whose length is 2. When the node B2 becomes red, we have to recompute: (•) the ultraviolet arc for the node B1, that is, the shortest violet arc from the blue node B1 to the Red part (this is done by comparing the length of the ultraviolet arc B1 R1 with the length of the arc B1 B2, if any), and 7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 227 (•) the ultraviolet arc for the node B3, that is, the shortest violet arc from the blue node B3 to the Red part (this is done by comparing the length of the arc B3 R2 with the length of the arc B3 B2, if any). Note that, instead of saying ‘the shortest violet (or ultraviolet) arc’ we should say ‘a shortest violet (or ultraviolet) arc’ because there may be ties. Indeed, as already mentioned, Algorithm 7.5.1 for computing minimal spanning trees is a nondeterministic algorithm and computes a minimal spanning tree. Thus, while the computation of the minimal spanning tree progresses, in order to make Choice (ii.2), for each node which is presently blue, we have to store its ultraviolet arc and the length of this ultraviolet arc. This is done by keeping an array of ultraviolet arcs as described in Point (iii) below. We have the following version of the algorithm. Algorithm 7.5.1. Computation of a Minimal Spanning Tree of an Undirected Graph. (Dijkstra’s MST algorithm) color red one node and color blue all the remaining nodes; while the number of red nodes is less than N do begin Step (1). Select the shortest ultraviolet arc. Step (2). Color it red and color red its blue node, say P. Step (3). Adjust the set of ultraviolet arcs: for each blue node B, compare the length ℓ of its ultraviolet arc with the length m of the arc B P which, if m < ℓ, becomes the new ultraviolet arc for the node B. end Point (iii). The use of an (N −1) × 2 array. This array, called uvArcs in the program MST.java on page 231, keeps the ultraviolet arcs, each arc being a pair of nodes which are stored in a row of the array. The array uvArcs is initialized by the arcs from the chosen initial red node, say A, to the other N −1 blue nodes. Thus, initially the array uvArcs looks like this: Red (1) (2) A A A A B C ... ... Blue where the left part is of red nodes and the right part is of blue nodes. Initially, the only red node is A and all other nodes are blue nodes. The number of rows of the array uvArcs is the number of the nodes which are initially blue, that is, N − 1. Now let us see how the array uvArcs is modified during the computation of the minimal spanning tree (see also Example 7.5.2 on page 229). Suppose that we have constructed the minimal spanning tree of the subgraph whose nodes are colored red, and supposed that the arcs of that minimal spanning 228 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS trees are stored in the upper part, named Red , of the array uvArcs. The lower part, named Blue, of that array stores the ultraviolet arcs, one for each blue node. In the following array uvArcs the blue nodes are C, D, and B. Red (1) (2) ... A E E A ... E C D B Red Blue Every node which occurs in the left-upper part (that is, the nodes either in the column (1), or in the column (2) of the Red rows) of the array uvArcs is colored red, while every node which occur in the lower-right part (that is, the nodes in the column (2) of the Blue rows) of the array uvArcs is colored blue. In our case the only blue nodes are C, D, and B. Let us now consider the three steps of the body of the while-do loop of our Algorithm 7.5.1 on the preceding page. Step (1). Select the shortest ultraviolet arc. Among the arcs in the lower part of the array uvArcs (in our case, E C, E D, and A B) we select the arc with the shortest length. Let us assume that it is the arc E D. Step (2). Color it red and color red its blue node. In our case we color red the arc E D and its blue node D. We do so by swapping the rows E D and E C. We get the following array uvArcs where: (i) the blue part now has two arcs only, namely, E C and A B, and (ii) the arc E D and the node D now are red and they belong to the Red part: (1) Red (2) ... ... A E E D E A C B Red Blue Step (3). Adjust the set of ultraviolet arcs. For each arc X Y in the set Blue of ultraviolet arcs (that is, in the Blue rows), we compare its length ℓ with the length m of the arc, if any, from the blue node Y to the node D which has been colored red at the preceding Step (2). If m < ℓ then we write D, instead of X. If we assume that length(D B) < length(A B) and length(D C) 6< length(E C), we get: Red (1) (2) ... A E E D ... E D C B Red Blue 7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 229 At the end of Step (3) we have extended the red part of the given graph by one more node (in our case the node D), having preserved the invariant that: (•) the Red part stores the arcs of the minimal spanning tree of the subgraph whose nodes are currently red, and (•) the Blue part stores the ultraviolet arcs of the nodes which are currently blue, that is, the shortest arc which connects the blue node in column (2) to a red node of the graph. Note also that during the computation of the minimal spanning tree, we have the following invariant: number-of-red -arcs + number-of-blue-nodes = N −1 where: (i) number-of-red-arcs is the current number of the arcs of the minimal spanning tree of the subgraph whose nodes are colored red, and (ii) number-of-blue-nodes is the current number of the blue nodes (which is equal to the current number of the ultraviolet arcs). The time complexity of our algorithm O(N 2 ) and the space complexity is O(N) where N is the number of nodes in the given graph. Another possible algorithm for computing a minimal spanning tree of an undirected graph is Kruskal’s algorithm (see, for instance, [20, page 101]) whose worst case time complexity is O(N 2 log N), where N is the number of nodes in the given graph and thus, the number of arcs is of order O(N 2 ). In Kruskal’s algorithm the extra log N factor is due to the need of sorting the arcs of the graph according to their length. Example 7.5.2. [Minimal Spanning Tree of an Undirected Graph] Let us consider the undirected, connected, weighted graph of Figure 7.5.4. 4 A 1 2 B C 3 3 4 E 4 2 D Figure 7.5.4. A five node graph of which we compute a minimal spanning tree starting from node A. The successive configurations of the array storing the ultraviolet arcs are shown on page 230. The arcs with thicker lines are those belonging to the resulting minimal spanning tree. We have the following symmetric matrix of distances: A B C D E A 0 4 ∞ ∞ 1 B 4 0 2 3 4 C ∞ 2 0 4 3 D ∞ 3 4 0 2 E 1 4 3 2 0 230 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS Let us consider node A as first red node. All other nodes are blue. Thus, we have the following array uvArcs of ultraviolet arcs (actually, some of them are not existing, that is, they have length ∞): A A A A B C D E Then, we have the following sequence of pairs of actions: (i) take a shortest ultraviolet arc, and (ii) adjust the ultraviolet arcs. (1.i) Take A E which is a shortest ultraviolet arc and swap rows: A A A A E C D B (1.ii) Adjust ultraviolet arcs after node E became red: A E E A E C D B (2.i) Take E D which is a shortest ultraviolet arc and swap rows: A E E A E D C B (2.ii) Adjust ultraviolet arcs after node D became red: A E E D E D C B (3.i) Take E C which is a shortest ultraviolet arc and swap rows: A E E D E D C B (3.ii) Adjust ultraviolet arcs after node C became red: A E E C E D C B Finally, we take the only available ultraviolet arc C B and we get the following arrays uvArcs (no swap of rows is required and hence, the array is not changed): A E E C E D C B Therefore, at the end of the MST algorithm (see Algorithm 7.5.1 on page 227) we get the minimal spanning tree which is indicated in Figure 7.5.4 on page 229 by ticker lines. That minimal spanning tree consists of the following four arcs: A E, E D, E C, and C B.  7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 231 /** * ========================================================================= * MINIMAL SPANNING TREE OF A UNDIRECTED GRAPH * * Filename: "MST.java" * * N is the number of nodes of the graph. We assume that N > 1. N1 is N-1. * The element graph[i][j] holds the distance, which is an integer number, * from node i to node j of the graph. * The algorithm is correct also for distances which are negative, null, or * positive, and even for non-Euclidean distances, but some changes should * be made to the code. * We assume that for any node i, distance[i][i]=0. * For distances which are real numbers, instead of integer numbers, we * should write ’double’, instead of ’int’. * The algorithm is taken from E. W. Dijkstra: * ’A Short Introduction to the Art of Programming’, EWD 316 August 1971. * Note. We stipulate that distance[i][i] == -1 when there is no arc from * node i to node j. Thus, when the algorithm is used for distances which * are negative, null, or positive, and even for non-Euclidean distances, * suitable changes are to be made (see lines -1- and -2-) because -1 can no * longer be used for encoding the absence af an arc. * If all distances are >= 0 then * line -1- becomes: if (l < uvMinLength) and * lines -2- should be suitably changed. * ========================================================================= */ public class MST { // MST : Minimal Spanning Tree public static void main(String[] args) { int N = 0; // - to be read from the input: args[0] int initNode = 0; // - to be read from the input: args[1] // ---------------------------------- N: number of Nodes of the graph. // N > 1. try {N = Integer.parseInt(args[0]); if ( N < 2) { throw new RuntimeException (); }; } catch (Exception e) {System.out.print("*** Wrong number of nodes!\n");} // ---------------------------------- initNode: initial node. // 0 <= initNode <= N-1. try {initNode = Integer.parseInt(args[1]); if ( ( 0 > initNode ) || ( initNode > N-1 ) ) { throw new RuntimeException (); }; } catch (Exception e) {System.out.print("*** Wrong initial node!\n");} // ------------------------------------------------------------------------// MAXLENGTH: maximum length of the arcs. 1 <= length <= MAXLENGTH. final int MAXLENGTH = 9; // ------------------------------------------------------------------------// PerCentPROB: probability of connectivity. 0 < PerCentPROB <= 100. // If PerCentPROB = k, every node is connected with probability k/100 to the // other N-1 nodes. final int PerCentPROB = 60; // ------------------------------------------------------------------------// Generation of a random graph with N nodes. // The length of each arc belongs to the set {1,..., MAXLENGTH}. // Every node has (N-1) x PerCentPROB / 100 successor nodes (in average). UGraph g = new UGraph(N, initNode, MAXLENGTH, PerCentPROB); 232 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS g.graphPrint(); System.out.println("Every node is connected to the other nodes with " + "probability " + PerCentPROB + " percent."); System.out.println("The length of each arc belongs to the set {1,..., " + MAXLENGTH + "}."); System.out.println("The initial node is: " + initNode); // int i, uvMin, p, N1 = N-1; int l, uvMinLength; // ’double’, not ’int’, for real distances //-------------------------------------------------------------------------// The g.uvArcs are the ultraviolet arcs of the graph g (see Dijkstra) //-------------------------------------------------------------------------int k = 0; while (k < N1) {g.uvArcs[k][0] = 0; g.uvArcs[k][1] = k+1; k++;}; k = 0; while (k < N1) { // <<========================== uvMin = k; uvMinLength = g.graph[g.uvArcs[k][0]] [g.uvArcs[k][1]];//it may be -1 //-------------------------------------------------------------------------i = k+1; while (i < N1) { // <<--------------------+ l = g.graph[g.uvArcs[i][0]] [g.uvArcs[i][1]]; // | if ( (l < uvMinLength && l >= 0) || uvMinLength == -1 ) // | { uvMin = i; uvMinLength = l; }; // | i++; // | }; // end of while <<--------------------+ if (uvMinLength >= 0) { // // p = g.uvArcs[uvMin][1]; // nearest blue node to red zone -1- -2- //-------------------------------------------------------------------------if (uvMin != k) {g.swap(k,uvMin);}; i = k+1; while (i < N1) { // <<--------------------+ if ( (g.graph[g.uvArcs[i][0]] [g.uvArcs[i][1]] // | > g.graph[p] [g.uvArcs[i][1]] // | && g.graph[p] [g.uvArcs[i][1]] >= 0 // | -2) // | || (g.graph[g.uvArcs[i][0]] [g.uvArcs[i][1]] == -1 // | -2&& g.graph[p] [g.uvArcs[i][1]] >= 0) // | -2) { g.uvArcs[i][0] = p; }; // | i++; // | }; // end of while <<--------------------+ k++; } else { break;} // // -2}; // end of while <<========================== g.uvPrint(k); } } /** * input: * ------------------------------------------------------------------------* javac MST.java * java MST 9 0 * * output: * -----* The random undirected graph has 9 node(s) (from node 0 to node 8). * The symmetric matrix of the arc lengths is: 7.5. MINIMAL SPANNING TREE OF AN UNDIRECTED GRAPH 233 * 0 : 0 4 7 - - - 2 4 9 * 1 : 4 0 - 1 7 - - 4 * 2 : 7 - 0 5 - 7 3 6 3 * 3 : - 1 5 0 - 5 8 3 4 * 4 : - 7 - - 0 8 5 4 6 * 5 : - - 7 5 8 0 - - 6 * 6 : 2 - 3 8 5 - 0 6 5 * 7 : 4 4 6 3 4 - 6 0 * 8 : 9 - 3 4 6 6 5 - 0 * * Every node is connected to the other nodes with probability 60 percent. * The length of each arc belongs to the set {1, ..., 9}. * The initial node is: 0 * * The arcs of the minimal spanning tree, * starting from node 0 and reaching all reachable nodes, are: * 0 -- 6 * 6 -- 2 * 2 -- 8 * 0 -- 1 * 1 -- 3 * 3 -- 7 * 7 -- 4 * 3 -- 5 * ------------------------------------------------------------------------*/ /** * ========================================================================= * The UGraph class * Filename: "UGraph.java" * * This class UGraph defines a undirected, weighted graph. * The class UGraph is not a monitor, because not all fields are "private". * However, the non-private fields are read, but never assigned to. * All methods are "synchronized". * Thus, this class UGraph is "almost" a monitor. * For real distances, instead of integer distances, write ’double’, instead * of ’int’. * ========================================================================= */ import java.util.Random; public class UGraph { final int N; final int initNode; static int [][] graph; static int [][] uvArcs; // // // // // N = number of nodes of the graph initial node of the minimal spanning tree an N x N array an N-1 x 2 array: arcs of the minimal spanning tree // --------------------- beginning of constructor -------------------------public UGraph(int N, int initNode, int MAXLENGTH, int PerCentPROB) { /** Generation of a random, undirected graph with N nodes. * * The random variable randomConnect controls the connectivity: * node i is connected with node j, for i != j, with percent * probability PerCentPROB: 0 <= PerCentPROB <= 100 * The random variable randomLength controls the length of the arcs: * 1 <= length of an arc <= MAXLENGTH */ 234 7. VISITS OF TREES AND GRAPHS AND EVALUATION OF EXPRESSIONS this.N this.initNode this.graph this.uvArcs = = = = N; initNode; new int [N][N]; new int [N-1][2]; Random randomConnect = new Random(); Random randomLength = new Random(); // random graph connectivity // random arc length // initializing the graph: -1 (printed as "-") means "unconnected node" // graph[i][j] = m with m >= 0 // iff m is the length of the arc from node i to node j for(int i=0;i<N;i++) { for(int j=i;j<N;j++) { if ((i != j) && (randomConnect.nextInt(100) < PerCentPROB)) { graph[i][j] = 1 + randomLength.nextInt(MAXLENGTH); graph[j][i] = graph[i][j]; // symmetric graph } else if (i == j ) { graph[i][j] = 0; } else { graph[i][j] = -1; graph[j][i] = -1; };// symmetric graph } } } // --------------------- end of constructor -------------------------------// --------------------- swapping two rows of the array uvArcs ------------public static void swap (int i, int j) { int temp; temp = uvArcs[i][0]; uvArcs[i][0] = uvArcs[j][0]; uvArcs[j][0] = temp; temp = uvArcs[i][1]; uvArcs[i][1] = uvArcs[j][1]; uvArcs[j][1] = temp; } // --------------------- printing the graph -------------------------------public synchronized void graphPrint () { System.out.println("The random undirected graph has "+ N +" node(s) " + "(from node 0 to node " + (N-1) + ").\n" + "The symmetric matrix of the arc lengths is:"); for(int i=0;i<N;i++) { System.out.print(i + " : "); for(int j=0;j<N;j++) { if (graph[i][j] < 0) { System.out.print("- "); } else System.out.print( graph[i][j] + " "); }; System.out.println(); }; System.out.println(); } // --------------- printing the arcs of the minimal spanning tree ---------public synchronized void uvPrint (int N) { if (N==0) {System.out.println("\nThe minimal spanning tree is the "+ "initial node "+ initNode +" only.");} else { System.out.print("\nThe arcs of the minimal spanning tree,\n" + "starting from node " + initNode + " and reaching " + "all reachable nodes, are: \n"); for (int k=0; k<N; k++) {System.out.print(uvArcs[k][0] + " -- " + uvArcs[k][1] + "\n");};} } } // ------------------------------------------------------------------------- CHAPTER 8 Path Problems in Directed Graphs In this chapter we present some techniques for solving path problems in directed graphs. We will first recall some algorithms for matrix multiplication because, as we will see, matrix multiplication can be used for solving path problems, when directed graphs are represented by adjacency matrices (see, for instance, [20, pages 8–10]). By definition, we have that given a graph G with n nodes, its adjacency matrix M is an n×n matrix made out of 0’s and 1’s such that for all i, j, with 1 ≤ i, j ≤ n, M[i, j] = 1, that is, an element of M in position hi, ji is 1, iff in the graph G there is an arc from node i to node j. In particular, M[i, i] = 1 iff there exists an arc from node i to node i itself. We assume that the length of a path is given by the number of its arcs. Thus, in particular, in any graph G there is a path of length 0 from any node of G to that same node. We have that there exists in G a path of length k (≥ 0) from node i to node j iff the in k-th power of M, denoted M k , the element in position hi, ji, denoted M k [i, j], is 1. 8.1. Matrix Multiplication Algorithms The matrix multiplication problem can be formalized as follows. Problem 8.1.1. Matrix Multiplication. Input: two n×n matrices of integers A= a11 . . . a1n ... ... ... an1 . . . ann and B = b11 . . . b1n ... ... ... bn1 . . . bnn Output: the n×n matrix C = A×B, P that is, for every i, j = 1, . . . , n, cij = 1≤k≤n aik bkj . In the sequel we will feel free to write the multiplication of the matrices A and B as A B, instead of A×B. The elementary matrix multiplication algorithm is as follows. Algorithm 8.1.2. Elementary Algorithm for Matrix Multiplication. Given any two n×n matrices A and B, the matrix C = A×B Pis obtained by performing the n multiplications and n−1 additions for each cij = 1≤k≤n aik bkj , as indicated in the specification of the Matrix Multiplication Problem (see Problem 8.1.1). 235 236 8. PATH PROBLEMS IN DIRECTED GRAPHS This elementary algorithm takes n2 (2n−1) = 2n3 −n2 arithmetic operations for n ≥ 2. Let us now consider the following algorithm for matrix multiplication. Algorithm 8.1.3. Matrix Multiplication by Partition. Consider the following n×n matrices A, B, and C, and the partitions of each of these matrices into four (n/2)×(n/2) blocks: A= A11 A12 ,B= A21 A22 B11 B12 , and C = B21 B22 C11 C12 . C21 C22 Perform the 8 multiplications of (n/2)×(n/2) matrices and the 4 additions of (n/2)× (n/2) matrices as indicated by the equalities: C11 = A11 B11 +A12 B21 , C12 = A11 B12 +A12 B22 , C21 = A21 B11 +A22 B21 , C22 = A21 B12 +A22 B22 . This algorithm takes O(n3 ) arithmetic operations, because their number T (n) satisfies the following recurrence relation for n ≥ 2: T (n) = 8T (n/2) + 4(n2 /4), with T (1) = 1, whose solution is: T (n) = 2n3 − n2 . We have also the following algorithm. Algorithm 8.1.4. Strassen Matrix Multiplication. Consider the following n×n matrices A, B, and C, and the partitions of each of these matrices into four (n/2)×(n/2) blocks: A= A11 A12 ,B= A21 A22 B11 B12 , and C = B21 B22 C11 C12 . C21 C22 Perform the 7 multiplications of (n/2)×(n/2) matrices and the 10 additions of (n/2)× (n/2) matrices (we count the subtractions as additions of negated values) indicated by the following equalities: m1 = (A12 −A22 )(B21 +B22 ), m2 = (A11 +A22 )(B11 +B22 ), m3 = (A11 −A21 )(B11 +B12 ), m4 = (A11 +A12 )B22 , m5 = A11 (B12 −B22 ), m6 = A22 (B21 −B11 ), m7 = (A21 +A22 )B11 , Then, by performing the following 8 additions of (n/2)×(n/2) matrices, we get: C11 = m1 + m2 − m4 + m6 , C12 = m4 + m5 , C21 = m6 + m7 , C22 = m2 − m3 + m5 − m7 . This algorithm due to V. Strassen in 1969 takes O(nlog2 7 ), which is about O(n2.81 ), because their number satisfies the following recurrence relation for n ≥ 2: T (n) = 7T (n/2) + 18(n2 /4), with T (1) = 1. Indeed, we have that: 8.2. COMPARING MATRIX MULTIPLICATION ALGORITHMS 237 T (n) = (18/4)[n + (7/4)n2 + (7/4)2n2 + . . . + (7/4)(log2 n)−1 n2 ] + 7log2 n T (1) = = (18n2 /4)[((7/4)log2 n − 1)/((7/4) − 1)] + 7log2 n = = 6nlog2 7 − 6n2 + nlog2 7 < 7nlog2 7 . (Recall that xlogy z = z logy x for all x, y, z > 0 and y 6= 1.) If n is not an exact power of 2 we can pad the given matrix with extra rows (on the bottom) and columns (on the right) made out of all 0’s, so that it becomes an m×m matrix, where m = 2⌈log2 n⌉ . A better method is as follows: if n is even then make a recursive call to n/2, if n is odd then pad with one row and one column of 0’s (getting an (n+1)×(n+1) matrix) and make a recursive call to (n+1)/2. Following Strassen’s idea, new algorithms have being discovered which multiply matrices even faster than O(nlog2 7 ). The current asymptotic upper bound is about O(n2.55 ). However, we cannot hope to multiply two n×n matrices faster than O(n2 ), because there are n2 elements to compute. 8.2. Comparing Matrix Multiplication Algorithms Let us recall some definitions from Abstract Algebra for comparing the above algorithms for matrix multiplication. Definition 8.2.1. [Semiring] A semiring is an algebra (S, +,  ) with carrier set S and the two operations + and  , such that: (i) + is associative and commutative, (ii)  is associative, and (iii)  distributes over + on both sides, that is, for all a, b, c ∈ S, a  (b + c) = (a  b) + (a  c) and (b + c)  a = (b  a) + (c  a). An element u of S is an identity for the operation op iff for all x ∈ S, x op u = u op x = x. An element z of S is an absorbent element for the operation op iff for all x ∈ S, x op z = z op x = z. It can be shown that in any semiring: - if an identity for + exists then it is unique, - if an identity for  exists then it is unique, and - if an absorbent element for  exists then it is unique. Definition 8.2.2. [Semiring with 0 and 1] A semiring with 0 and 1 is a semiring (S, +,  ) with the identity for  , denoted 1, and the absorbent element for  , denoted 0, which is also the identity for +. We will write (S, +,  , 0, 1)-semiring to denote such a semiring. Definition 8.2.3. [Ring] A ring is a semiring (S, +,  ) where the algebra (S, +) is a commutative group, that is, + is associative and commutative, + has an identity, denoted 0, and every element x has an additive inverse, denoted −x, that is, for all x ∈ S, x + (−x) = (−x) + x = 0. 238 8. PATH PROBLEMS IN DIRECTED GRAPHS As for any group, we have that in a ring the additive inverse of any element is unique. One can prove that in any ring the identity for + is the absorbent element for  , that is, for all x ∈ S, x  0 = 0  x = 0. As proved in [8], one can also show that: for all x ∈ S, x  (−y) = (−x)  y = −(x  y) and (−x)  (−y) = x  y . Definition 8.2.4. [Ring with 1] A ring (S, +,  ) with the identity for  , denoted 1, is said to be a ring with 1. Such a ring will be denoted by (S, +,  , 0, 1)-ring. Definition 8.2.5. [Field] An (S, +,  , 0, 1)-ring is said to be a field iff  is commutative, and every element x different from 0, has a multiplicative inverse, denoted x−1 , that is, for all x ∈ S, x  x−1 = x−1  x = 1. Given any (S, +,  , 0, 1)-semiring, we may consider the set, denoted Mn , of the n×n matrices whose elements are taken from the set S. Mn is said to be the set of the n×n matrices over the (S, +,  , 0, 1)-semiring (or over S, for short). Let 0n denote the n×n matrix with all 0’s, and In denote the n×n matrix which is like 0n , except that for all i, with 1 ≤ i ≤ n, (In )ii = 1, that is, each element of the main diagonal of In is 1. One can show that (Mn , +n ,  n , 0n , In ) is a semiring with 0 and 1, denoted (Mn , +n ,  n , 0n , In )-semiring, where: (i) +n is defined elementwise, that is, for all n×n matrices A and B, we have that A +n B is the n×n matrix C such that for all i, j, with 1 ≤ i, j ≤ n, Cij = Aij + Bij , and (ii)  n is defined as follows: for all n×n matrices A and B, we have that A  n B is the n×n matrix C such that for all i, j, with 1 ≤ i, j ≤ n, Cij = (Ai1  B1j ) + (Ai2  B2j ) + . . . + (Ain  Bnj ). Analogously, we have that, given any (S, +,  , 0, 1)-ring, instead of any (S, +,  , 0, 1)semiring, we get the ring with 1 of the n×n matrices over S, instead of the semiring with 0 and 1 of the n×n matrices over S. The ring of the n×n matrices over a given (S, +,  , 0, 1)-ring will be denoted by (Mn , +n ,  n , 0n , In )-ring. Notice that any (Mn , +n ,  n , 0n , In )-ring of matrices is not commutative even if the corresponding (S, +,  , 0, 1)-ring of their elements is commutative. Fact 8.2.6. [Comparing the Matrix Multiplication Algorithms] The Elementary Algorithm and the Partition Algorithm for matrix multiplication work for all n×n matrices whose elements are taken from any (S, +,  , 0, 1)-semiring. Strassen algorithm for matrix multiplication works for all n×n matrices whose elements are taken from any (S, +,  , 0, 1)-ring. 8.3. Fast Boolean Matrix Multiplication If we look for an asymptotically fast method for multiplying boolean matrices, we cannot use Strassen algorithm because the boolean values 1 and 0 with the additive operation ∨ (that is, the least upper bound) and the multiplicative operation ∧ (that is, the greatest lower bound), do not form a ring with 1. Indeed, there is no inverse w.r.t. the operation ∨. 8.4. IC-SEMIRINGS AND PATH PROBLEMS IN DIRECTED GRAPHS 239 However, given two n×n boolean matrices A and B, we can multiply them together as they were matrices of elements in Zn+1 , that is, the integers modulo n+1, for n ≥ 2, where we consider ∨ as + (i.e., addition), and ∧ as  (i.e., multiplication). It can be shown that if we multiply the two n × n boolean matrices A and B e then the as they were matrices of elements in Zn+1 , thereby getting the matrix C, boolean matrix C = A ∧n B can be computed as follows: eij = 0 then Cij = 0 and if 1 ≤ C eij ≤ n then Cij = 1. for all i, j, with 1 ≤ i, j ≤ n, if C Thus, also boolean matrices can be multiplied within the same time complexity (counting the number of arithmetic operations) which is required for multiplying two integer matrices, that is, they can by multiplied with time complexity O(nlog2 7 ). If, instead, we count the number of bit operations, as it is reasonable for boolean matrices, then the time complexity is O(nlog2 7 IM (log2 n)), where IM (n) is the time complexity for multiplying two integers, each of which being made out of n digits in eij has at most ⌈log2 (n + 1)⌉ bits, base 2. (Recall that for all i, j, with 1 ≤ i, j ≤ n, C eij is at most n.) because C Recalling that by the Schönhage-Strassen method we have that IM (n) = O(n (log2 n) log2 (log2 n)), we get that the time complexity for multiplying two n×n boolean matrices is: O(n log2 7 (log2 n) (log2 log2 n) (log2 log2 log2 n)). We will see below that fast algorithms for boolean matrix multiplication allow us to construct fast algorithms for computing both the transitive closure of binary relations and the paths between nodes in graphs. 8.4. IC-Semirings and Path Problems in Directed Graphs In order to study path problems in labeled directed graphs it is convenient to consider the n × n matrices whose elements are taken from suitable semirings, called ic-semirings, which we will introduce in this section. We will have that the algebra of the n×n matrices whose elements are taken from an ic-semiring, is an ic-semiring as well. We assume that paths of labeled directed graphs are represented as elements of matrices associated with the graphs and, in particular, we assume that a path with label p from node h to node k of a graph G with n nodes, is represented by the element p taken from the semiring (S, +,  , 0, 1) and placed in row h and column k of an n×n matrix M associated with the graph G. Thus, for the matrix M we have that Mhk = p. In order to motivate our definitions below, we now list a few properties that we want to hold for the paths of labeled directed graphs whose labels are taken from the semiring (S, +,  , 0, 1). (i) Two paths from node m to node n, the first one with label p and the second one with label q, are equal to a single path from m to n with label p + q. (ii) For any node n1 , n2 , and n3 , a path from n1 to n2 with label p followed by a path from n2 to n3 with label q, is equal to a path from n1 to n3 with label p  q. (iii) The absence of a path from node m to node n is equal to a path from node m to node n with label 0. 240 8. PATH PROBLEMS IN DIRECTED GRAPHS (iv) The path of length 0 (that is, a path of no arcs) from a node to itself is equal to a path from that node to itself with label 1. (v) For any node n1 , n2 , and n3 , a path from n1 to n2 with label p, followed by a path from n2 to n3 with label q1 + q2 , is equal to a path from n1 to n3 with label (p  q1 ) + (p  q2 ) (this property is the distributivity of  over +). (vi) Two paths both with label p, between the same two nodes, are equal to a single path with label p (this property is the idempotency of +). (vii) A path from a node to itself with label p, that is, a loop with label p, is equal to a path from that node to itself with label p∗ , that is, 1 + p + p  p + p  p  p + . . . (this property motivates the need for countably infinite sums). Definition 8.4.1. [ic-semiring] An (S, +,  , 0, 1)-semiring, that is, a semiring with 0 and 1, is said to be idempotent complete semiring (or ic-semiring, for short) iff (i) + is idempotent, that is, for all x ∈ S, x + x = x, and P (ii) there are countably infinite sums, that is, there exists the value of i≥0 ai , that is, a0 + a1 + . . . for any infinite sequence ha0 , a1 , . . .i of elements of S, and (iii)  distributes on both sides over countably infinite sums, that is, (a0 + a1 + a2 + . . .)  (b0 + b1 + b2 + . . .) = (a0 + (a1 + (a2 + ...  b0 ) + (a0  b0 ) + (a1  b0 ) + (a2  b1 ) + (a0  b1 ) + (a1  b1 ) + (a2  b2 ) + . . .  b2 ) + . . .  b2 ) + . . . These semirings are called closed semirings in [1]. We prefer to call them idempotent complete semirings to recall that the operation + is idempotent and there exist countably infinite sums. For ic-semirings we will use the unary postfixed operation ∗ to denote the infinite P sum of all powers, that is, for all s ∈ S, s∗ =def i≥0 si , that is, s∗ =def 1 + s + s  s + s  s  s + . . . (recall that there is no need for parentheses among the summands and the factors, because + and  are associative). Thus, 0∗ = 1. (One can informally state this property by saying that a loop of no path from a node m to itself is the same as the path of length 0 from m to itself.) Here are three examples of ic-semirings with 0 and 1. (i) ({0, 1}, ∨, ∧, 0, 1)-semiring, called the boolean ic-semiring, where: x ∨ y =def if x = 1 or y = 1 then 1 else 0, and x ∧ y =def if x = 1 and y = 1 then 1 else 0. This ic-semiring is used for testing whether or not in a given directed graph there is a path from node i to node j. (ii) (R≥0 ∪{+∞}, min, +, +∞, 0)-semiring, called the non-negative ic-semiring, where R≥0 is the set of the non-negative reals, and for all x in R≥0 ∪ {+∞} we have that min(x, +∞) = min(+∞, x) = x. This ic-semiring is used for computing, given any directed graph whose arcs are labeled by non-negative reals, the minimum distance from any node to any node of the given graph. 8.5. TRANSITIVE CLOSURE IN DIRECTED GRAPHS: THE REACHABILITY PROBLEM 241 (iii) (REG(Σ∗ ), ∪,  , ∅, {ε})-semiring, called the Regular Expressions ic-semiring, where Σ is any given finite alphabet, Σ∗ is the set of all words over Σ, that is, the set of all sequences of symbols in Σ, and REG(Σ∗ ) is the set of all regular languages over Σ. In what follows we will denote a regular language also by its corresponding regular expression (hence the name Regular Expressions ic-semiring for this ic-semiring). Recall that, given two languages L1 and L2 , their concatenation L1  L2 is defined in terms of concatenation of words, also denoted  , as follows: L1  L2 = {w1  w2 | w1 ∈ L1 and w2 ∈ L2 }. This ic-semiring can be used for computing, given any directed graph whose arcs are labeled by regular expressions, the regular expression which denotes the set of all paths from any node to any node of the given graph. From any given ic-semiring (S, +,  , 0, 1) we may get, as specified in Section 8.2 on page 238, the (Mn , +n ,  n , 0n , In )-semiring of the n×n matrices whose elements are taken from the given semiring. It can be shown that (Mn , +n ,  n , 0n , In )-semiring is an ic-semiring. Let us now look at some problems for which the matrices over ic-semirings we have introduced, are particularly useful. 8.5. Transitive Closure in Directed Graphs: the Reachability Problem Given a directed graph G with n nodes, the reachability problem in G consists in testing for each pair of nodes i and j whether or not there exists a path from i to j. We can solve this problem by considering the ic-semiring of the n×n matrices over the boolean ic-semiring h{0, 1}, ∨, ∧, 0, 1i as follows. We associate with G the n×n boolean matrix A(G), called adjacency matrix, (see, for instance, [20, Chapter 1]) such that for all i and j with 1 ≤ i, j ≤ n, A(G)ij =def if there is an arc from node i to node j then 1 else 0. In particular, A(G)ii = 0 if there is no arc from node i to itself. From now on the matrix A(G) will also be simply called A when the graph to which it refers, is understood from the context. We assume that A0 = In (where In is the identity matrix, that is, the n×n matrix with all elements 0’s except those in the main diagonal which are 1’s). The definition of the identity boolean matrix In is based on the facts that: (•) for all i and j with 1 ≤ i, j ≤ n, there is no path of one or more arcs from a node i to a node j, and (•) if i = j then node j is reachable from node i (and this fact is encoded by the values 1’s in the main diagonal of In ) else node j is not reachable from node i (and this is encoded by the values 0’s outside the main diagonal of In ). Then, given any two n×n boolean matrices A and B, we stipulate that, for all i, j with 1 ≤ i, j ≤ n, (A ∧n B)ij = (Ai1 ∧ B1j ) ∨ (Ai2 ∧ B2j ) ∨ . . . ∨ (Ain ∧ Bnj ). We have that: A1 = A and An+1 = An ∧n A = A ∧n An . 242 8. PATH PROBLEMS IN DIRECTED GRAPHS Below we will feel free to omit writing the operators ∧n and ∧, and we will use juxtaposition, instead. Often, for reasons of simplicity, we will also write ∧, instead of ∧n . Analogously, we will also write ∨, instead of ∨n . One can show by induction on r (≥ 0) that Arij = 1 iff in the graph G there exists a path of length r from node i to node j. Thus, let us consider the matrix A∗ =def In ∨ A ∨ A2 ∨ A3 ∨ . . . ∨ An ∨ . . .. We have that given any two nodes i and j, the node j is reachable from node i (that is, there is a path from node i to node j) iff A∗ij = 1. One can also show that: A∗ = In ∨ A ∨ A2 ∨ A3 ∨ . . . ∨ An−1 This result follows from the fact that any path which, so to speak, touches more than n nodes of the graph G, should touch at least twice a node of G. Moreover, we have that: A∗ = (In ∨ A)n−1 . (#1) ∗ 2 3 n−1 This result follows from the fact that A = In ∨ A ∨ A ∨ A ∨ . . . ∨ A and 2 3 n−1 n−1 In ∨ A ∨ A ∨ A ∨ . . . ∨ A = (In ∨ A) because any path p of length k, with 0 ≤ k ≤ n−1, can be viewed as a path of length n−1 which is the concatenation of the given path p of length k and a path of length (n−1)−k from the final node of p to that same final node. As a consequence of Equation (#1), the matrix A∗ can be computed by successive squaring of (In ∨ A) for ⌈log2 (n−1)⌉ times. (For any real number x, by ⌈x⌉ we denote the least integer m such that m ≥ x.) This takes T (n) ≤ M(n) ⌈log2 n⌉ steps, where by M(n) we denoted the time complexity for boolean matrix multiplication. In the following section we will see that the log2 n factor is actually redundant, that is, T (n) ≤ M(n). 8.6. Reducing Transitive Closure to Boolean Matrix Multiplication We have the following fact. Fact 8.6.1. [Reducing Boolean Transitive Closure to Boolean Matrix Multiplication] Given a directed graph G with n nodes, let us consider the corresponding n×n adjacency, boolean matrix A, and its partition into four (n/2) × (n/2) A11 A12 . We have that: blocks (assume that n is even): A = A21 A22 A∗ = A∗11 ∨ (A∗11 ∧ A12 ∧ E ∗ ∧ A21 ∧ A∗11 ) E ∗ ∧ A21 ∧ A∗11 A∗11 ∧ A12 ∧ E ∗ E∗ (#2) where E = (A21 ∧ A∗11 ∧ A12 ) ∨ A22 . Proof. It is based on the interpretation of the matrix E and the four submatrices in the block partition of A∗ via the diagrams of Figure 8.6.1 on the next page and Figure 8.6.2 on the facing page. Let A11 denote a subgraph G1 of G with n/2 nodes, and A22 denote the subgraph G2 of G with the remaining n/2 nodes. In Figure 8.6.1 on the next page and Figure 8.6.2 on the facing page we use the following conventions: (i) a solid arrow denotes an arc of the graph G, in the graph G1 or G2 denotes a path of any length (ii) a solid double arrow (≥ 0) within the subgraph G1 or G2 , respectively, and 8.6. REDUCING TRANSITIVE CLOSURE TO BOOLEAN MATRIX MULTIPLICATION 243 Paths from G2 to G2 : E ∗ , where E = (A21 ∧ A∗11 ∧ A21 ) ∨ A22 can be depicted as follows: ∨ G1 G2 G2 Figure 8.6.1. The set E of paths that go from the subgraph G2 to the same subgraph G2 . They provide an interpretation of the matrix E = (A21 ∧A∗11 ∧A12 ) ∨ A22 . Paths from G1 to G2 : A∗11 ∧A12 ∧E ∗ G1 G2 G1 G2 G1 G2 Paths from G2 to G1 : E ∗ ∧A21 ∧A∗11 Paths from G1 to G1 : A∗11 ∨ (A∗11 ∧A12 ∧E ∗ ∧A21 ∧A∗11 ) Figure 8.6.2. Three sets of paths that provide an interpretation of the three matrices A∗11 ∧A12 ∧E ∗ , E ∗ ∧A21 ∧A∗11 , and A∗11 ∨ (A∗11 ∧A12 ∧ E ∗ ∧A21 ∧A∗11 ), respectively. (iii) a dashed double arrow denotes a path of any length (≥ 0) going anywhere through the graph G. Thus, for instance, the left upper corner of A∗ , that is, A∗11 ∨(A∗11 ∧A12 ∧E ∗ ∧A21 ∧ ∗ A11 ), can be interpreted as a path going from a node in G1 to a node in G1 (see also Figure 8.6.2). It is either entirely inside G1 (see A∗11 ) or it is inside G1 (see A∗11 ) and then it goes to G2 (see A12 ), it returns zero or more times to G2 (see E ∗ ), it returns to G1 (see A21 ), and finally, it continues entirely inside G1 (see A∗11 ). Analogous 244 8. PATH PROBLEMS IN DIRECTED GRAPHS interpretation can be done for the other blocks of the matrix A∗ (see again Figure 8.6.2 on the preceding page).  Fact 8.6.2. Let M(n) denote the time complexity for n×n boolean matrix multiplication. If M(n) = O(nα ) with α > 2, then the time for computing the transitive closure of an n×n boolean matrix is T (n) = O(nα ). Proof. From Fact 8.6.1 on page 242 we get that: T (n) ≤ 2T (n/2) + 6M(n) + O(n2 ). Thus, T (n) = O(nα ) if M(n) = O(nα) with α > 2. The factor 6 comes from the following six matrix multiplications which are necessary for obtaining the elements of the matrix A∗ : M1 = A∗11 ∧ A12 M2 = A21 ∧ A∗11 M3 = M1 ∧ E ∗ M4 = M3 ∧ M2 M5 = E ∗ ∧ M2 M6 = A21 ∧ M1 . If n is not a power of 2 then we can embed a given n×n boolean matrix, say A, into an m×m boolean matrix, say D, with m being a power of 2, with m > n, which has the form: A 0 D= 0 I where I is the (m−n) × (m−n) boolean identity matrix. Since m is not larger than 2n the time complexity will increase of at most 23 times, which is a constant factor.  From this Fact 8.6.2 it follows that, in order to obtain a fast algorithm for computing the transitive closure of a graph, we simply need a fast algorithm for boolean matrix multiplication. Remark 8.6.3. The matrix A∗ in Equation (#2) of the above Fact 8.6.1 on page 242 is not symmetric with respect to the partition of the given graph G into the two subgraphs G1 and G2 in the sense that the second row cannot be derived from the first row by interchanging the subscripts 1 and 2 and interchanging the two columns. On the contrary, the following expression of the matrix A∗ is symmetric with respect to that partition (and thus, in this expression of the matrix A∗ the second row can be derived from the first row by interchanging the subscripts 1 and 2 and interchanging the two columns): D∗ D ∗ ∧ A12 ∧ A∗22 (#3) A∗ = E ∗ ∧ A21 ∧ A∗11 E∗ where D = (A12 ∧ A∗22 ∧ A21 ) ∨ A11 and E = (A21 ∧ A∗11 ∧ A12 ) ∨ A22 . In the expression D ∗ ∧ A12 ∧ A∗22 the matrix A12 denotes the last time the path from the subgraph G1 enters the subgraph G2 . Other moves from G1 to G2 , if any, are denoted within D ∗ . Note also that the value of E can be derived from the value of D by interchanging the subscripts 1 and 2. Using the symmetric expression (#3), the evaluation of A∗ requires T (n) time units where T (n) satisfies the following inequation: T (n) ≤ 4T (n/2) + 6M(n) + O(n2 ). 8.7. TRANSITIVE CLOSURE IN IC-SEMIRINGS: THE SHORTEST PATH PROBLEM 245 Thus, again, we have that T (n) = O(nα ) if M(n) = O(nα ) with α > 2. The factor 4 comes from the four transitive closures we have to compute: E ∗ , D ∗ , A∗11 , and A∗22 . The factor 6 comes from the following six matrix multiplications we have to perform: M1 = A12 ∧ A∗22 M2 = M1 ∧ A21 M3 = D ∗ ∧ M1 M4 = A21 ∧ A∗11 M5 = M4 ∧ A12 M6 = E ∗ ∧ M4 .  The proof of Fact 8.6.1 on page 242 is based only on the property that the boolean ic-semiring is indeed an ic-semiring. Thus, if we consider directed graphs with labels which are elements of a non-negative ic-semiring (see page 240) or a Regular Expressions ic-semiring (see page 241), we have that the statements analogous to Fact 8.6.1 on page 242 and Fact 8.6.2 on the preceding page hold also for the matrices whose elements are non-negative reals or regular expressions. The following fact shows that we can reduce the multiplication of two boolean matrices to the computation of the transitive closure of a boolean matrix. Fact 8.6.4. [Reducing Boolean Matrix Multiplication to Boolean Transitive Closure] Let T (n) denote the time complexity for computing the transitive closure of an n × n boolean matrix. If T (n) = O(nα ) with α > 2, then the time complexity for the n×n boolean matrix multiplication is M(n) = O(nα ). Proof. It is easy to verify that 0 A 0 0 0 B 0 0 0 ∗ = I 0 A∧B 0 I 0 0 0 I Thus, in order to multiply two given n×n boolean matrices A and B, it is enough to 0 A 0 compute the transitive closure of the (3n)×(3n) boolean matrix T = 0 0 B . It 0 0 0 takes O((3n)α ) time. Now O((3n)α ) is equal to O(nα ), and this time complexity is the time complexity required for computing the transitive closure of an n×n boolean matrix. Having computed that transitive closure T ∗ of the matrix T , we then consider the top right corner of T ∗ (which is equal to A ∧ B).  8.7. Transitive Closure in IC-Semirings: the Shortest Path Problem Given a labeled (or weighted) directed graph G with n nodes, the problem of finding the shortest paths in G consists in finding for each pair of nodes i and j, a path with minimum sum of the labels of its arcs. If the labels of G form a non-negative icsemiring (see Definition 8.4.1 on page 240), we can solve this problem by considering the n×n matrices over that semiring as follows. Given a labeled directed graph G with n nodes, we associate with G the n×n matrix M(G) such that for all i and j, with 1 ≤ i, j ≤ n, M(G)ij = λ(i, j), where λ(i, j) is the label of the arc from node i to node j. We assume that for all i and j, with 1 ≤ i, j ≤ n, λ(i, j) = +∞ if there is no arc from node i to node j (even if i = j). In what follows the matrix M(G) will also be called M when the graph to which it refers, is understood from the context. 246 8. PATH PROBLEMS IN DIRECTED GRAPHS Then, for any two nodes i and j of G, we can find a path from i to j with the minimum sum of the labels of its arcs by applying the analogous of Fact 8.6.1 on page 242 for matrices over a non-negative ic-semiring. Notice that in the identity matrix over the non-negative ic-semiring, each element of the main diagonal is 0 (which is the identity for +), while every other element is +∞ (which is the identity for min). This definition of the identity matrix for the non-negative ic-semiring (R≥0 ∪ {+∞}, min, +, +∞, 0) is based on the fact that for all i and j, with 1 ≤ i, j ≤ n, (•) if i = j then a shortest path from node i to node j is a path of length 0 whose sum of the labels is 0 and, otherwise, (•) if i 6= j and there is no path from node i to node j then a shortest path from i to j is assumed to have the sum of the labels equal to +∞ (because from node i it is not possible to reach node j). It can be shown that one can compute the shortest paths between every pair of nodes of a labeled directed graph G by computing the transitive closure of the matrix M over the non-negative ic-semiring of the labels of G. We have the following fact which is analogous to Fact 8.6.2 on page 244. Fact 8.7.1. Let M(n) denote the time complexity for multiplying n×n matrices. If M(n) = O(nα ) with α > 2, then the time for computing a shortest path between any two nodes in labeled direct graph whose labels are non-negative reals is T (n) = O(nα ). Example 8.7.2. Let us consider the labeled directed graph with depicted in Figure 8.7.1. We have that: 0 0 8 5 ∗ 3−1 = 3 min(I, M) = 3 0 ∞ and (min(I, M)) = (min(I, M)) 5 ∞ 2 0 3 1 5 7 5 0 8 .  2 0 2 8 2 three nodes 2 3 Figure 8.7.1. A directed graph with non-negative labels on the arcs. We leave it to the reader to show that if the labels of the arcs of the given directed graph G are not taken from a non-negative ic-semiring, as for instance in the case of labels which may have negative values, the technique based on the computation of (min(I, M))∗ for finding the shortest paths between every pair of nodes of G, is still correct, provided that there are no negative cycles, that is, cycles whose sum of the labels of the arcs is a negative value. If there are negative cycles in the given graph, the technique based on the computation of (min(I, M))∗ is not correct, and we have to apply other algorithms, like 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 247 the Floyd-Warshall’s algorithm or a variant of Algorithm 8.8.1 (Dijkstra’s SSSP algorithm) which we will present on page 247. If there are negative cycles, the time complexity of those algorithms goes up to O(n3 ) [16, Chapter 6, page 133]. 8.8. Single Source Shortest Paths in Directed Graphs In this section we consider the problem of finding, given a labeled directed graph, the shortest paths starting from an initial node, also called the source, to all other nodes of the graph. This problem is called the Single Source Shortest Path problem. For solving this problem there exists an algorithm which is faster than the one based on the computation of the transitive closure of the adjacency matrix associated with the graph, which we have considered in the previous Section 8.7. We will call this faster algorithm the Single Source Shortest Path algorithm, or SSSP algorithm, for short. This algorithm is based on the Dynamic Programming idea. It is due to E. W. Dijkstra, as the algorithm we have presented in Section 7.5 for computing a minimal spanning tree of an undirected graph (see Algorithm 7.5.1 on page 227), and it takes O(n2 ) time for graphs of n nodes, if the labels on the arcs are taken from a nonnegative ic-semiring. Dijkstra’s SSSP algorithm is correct also for any labeled directed graph provided that there are no negative cycles, that is, there are no cycles with negative sum of the labels of their arcs. Dijkstra’s SSSP algorithm can be viewed as a particular instance of the PrimalDual algorithm (see [16, Capter 5]) and thus, its correctness can be derived from that of the Primal-Dual algorithm. Note that between any two given nodes of the graph there could be more than one path with the minimal sum of the labels of its arcs. In this case Dijkstra’s SSSP algorithm computes one of these minimal paths. However, by abuse of language, in what follows we will feel free to say ‘the shortest path’, instead of ‘a shortest path’. Now, for reasons of simplicity, we will present Dijkstra’s SSSP algorithm in the case when the labels of the arcs are non-negative reals. Algorithm 8.8.1. Computation of the Single Source Shortest Paths in Directed Graphs. (Dijkstra’s SSSP algorithm) Input: We are given a labeled, directed, connected graph G with n nodes. We are also given a node n0 of G, called the source. No assumptions are made on the arcs which depart from n0 or arrive at n0 . For each arc hp, qi, we assume that there exists a label λ(p, q) which is a non-negative real number. That label is also called the length of the arc. For each node p, we assume that there exists the arc hp, pi whose label λ(p, p) is 0. Output: for each node q in G, we compute a shortest path from node n0 to node q, that is, a path from n0 to q with minimal length. That path has minimal sum of the labels of its arcs, being the minimum taken over all paths from n0 to q. A. With each node q ∈ G we associate a label Λ(q) which is the length of a shortest path from n0 to q. 248 8. PATH PROBLEMS IN DIRECTED GRAPHS 1. Λ(n0 ) := 0; Reach := {n0 }; 2. for each arc hn0 , qi, Λ(q) := λ(n0 , q); 3. Repeat among all nodes not in Reach with a label, take a node, say p, with minimal label Λ(p); 3.1 Reach := Reach ∪ {p}; 3.2 for each arc hp, mi with m not in Reach, if q has already a label then (3.2.1) Λ(m) := min{Λ(p)+λ(p, m), Λ(m)} else (3.2.2) Λ(m) := Λ(p)+λ(p, m); B. We find by backtracking a shortest path from node n0 to a node q ∈ G. 4. Choose any node p, different from q, such that Λ(p) = −λ(p, q) + Λ(q); 5. Recursively, find a shortest path from n0 to p. The following points illustrate a few important facts about of the above SSSP algorithm. (i) The source node n0 can be any node of the given graph G. In particular, there may be arcs going into the source node. (ii) At Point 4 if there exists more than one node p, different from q, such that Λ(p) = −λ(p, q) + Λ(q), then we may choose any one of them. If this is the case, then there exists more than one path from n0 to q with minimal length. (iii) If hn0 , n1 , . . . , nk−1 , nk i is a shortest path from n0 to nk , Pk−1 then Λ(nk ) = i≥0 λ(ni , ni+1 ). (iv) This SSSP algorithm is similar to Dijkstra’s MST algorithm [6] for computing a minimal spanning tree of undirected graphs with labels on the arcs (see also Algorithm 7.5.1 on page 227). Analogously to Dijkstra’s MST algorithm: (•) the SSSP algorithm is a greedy algorithm, that is, the local optimum is a global optimum (recall what has been said for the MST algorithm on page 224), (•) according to what we have said about the MST algorithm, the nodes in Reach may be viewed as red nodes and the nodes not in Reach may be viewed as blue nodes, and (•) the SSSP algorithm, after transforming a blue node p into a red node (see Point 3.1), readjusts (see Point 3.2.1) or generates (see Point 3.2.2) the label of each blue node m such that there exists an arc hp, mi. Now we present the invariants of the loops of the above Dijkstra’s SSSP Algorithm 8.8.1. Those invariants clarify the way in which that algorithm works and are the basis of the proof of its correctness which we leave to the reader. The invariant of the Repeat statement at Point 3 is: « the label of any node q in the set Reach is the length of a shortest path from the source node n0 to q ». 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 249 The invariant at Point 3.2 is: « each node m not in Reach such that there is an arc hp, mi with p ∈ Reach, has a label which is the length of a shortest path hn0 , . . . , p, mi from the source node n0 to m such that the nodes n0 , . . . , p are all in Reach ». The invariant at Point 5 is: « in a shortest path from the source node n0 to the node m, the last arc is hp, mi ». The correctness of Dijkstra’s SSSP algorithm is based on the following property. Let us assume that we have computed a shortest path from the source node n0 to every node in a subset W of the nodes such that n0 ∈ W . Let us consider a shortest path, say π, from the node n0 to any node y not in W such that for every node x if there is the arc hx, yi in G then x ∈ W . Then the path π has the property that it goes through nodes of W (except for y itself). Note that if at Point 3 we take a node p whose label Λ(p) is not minimal, the algorithm is not correct. Example 8.8.2. Given the graph of Figure 8.8.1 on the following page and the node n0 , after Point 3 of Dijkstra’s SSSP algorithm we get the ‘boxed labels’ at each node. Then, having labeled all nodes with the boxed labels, we may find by backtracking every arc whose label is exactly the difference of the boxed labels of its nodes. By doing so (see Points 4 and 5 of the SSSP algorithm), we get the following two sequences of values: 6 1 2 3 (i) 12 ←− 6 ←− 5 ←− 3 ←− 0 and 3 4 2 3 (ii) 12 ←− 9 ←− 5 ←− 3 ←− 0 which correspond to the following two shortest paths from node n0 to node n6 (see the thicker arcs of the graph of Figure 8.8.1): (i) n0 −→ n2 −→ n3 −→ n5 −→ n6 and (ii) n0 −→ n2 −→ n3 −→ n4 −→ n6 , respectively. The total length of each of those shortest paths is 12 which is, indeed, the boxed label of the final node n6 . Note that by backtracking, from node n6 we do not go back to node n3 (because 12−9 6= 5) and, instead, we go back either to node n4 (because 12−6 = 6) or to node n5 (because 12−3 = 9).  For a graph of n nodes the time complexity of Dijkstra’s SSSP algorithm is O(n2 ) and this is due to the fact that: (i) the body of the Repeat statement (see Point 3) is performed at most n times because there are at most n nodes not in Reach, and (ii) for each node p not in Reach there exist at most n arcs of the form hp, mi with m not in Reach (see Point 3.2). Dijkstra’s SSSP algorithm shows that if we fix the initial node and the labels of the given graph are taken from a non-negative ic-semiring, we may solve the Single Source Shortest Path problem in O(n2 ) time. This is an improvement over the time complexity for solving the shortest path problem between any two nodes. Indeed, 250 8. PATH PROBLEMS IN DIRECTED GRAPHS 7 n2 3 2 3 n0 0 n4 1 6 9 n3 8 5 n1 5 6 n6 4 8 5 12 3 n5 9 Figure 8.8.1. A directed graph with non-negative labels (also called lengths) on the arcs. The thicker arcs are those belonging to the two shortest paths of length 12 from node n0 to node n6 . Worst Case Time Complexity Shortest paths from any node to any node: (the complexity is that of matrix multiplication) O(n2.81 ) Shortest paths from a fixed node to any node: O(n2 ) Shortest path from a fixed node to a fixed node: O(n2 ) (it is an open problem whether or not one can do better) Figure 8.8.2. Time complexity of the computation of the shortest paths in directed graphs with n nodes. as we have shown in Section 8.6 on page 242, this last problem requires the same amount of time which is needed for matrix multiplication, say O(n2.81 ) time (see also Figure 8.8.2). One may wonder whether or not it is possible to derive an even faster algorithm for the shortest path problem when we fix both the initial node and the target node. This is the so called Single Source Single Target Shortest Path problem. The answer, to our knowledge, is still open, in the sense that no algorithm has been found for this last problem which has a better time performance, in the worst case, than the best algorithm known for the Single Source Shortest Path problem which has an O(n2 ) time complexity for graphs of n nodes. A final point is worth noticing. Let us consider an undirected graph with weights on its arcs as a particular case of directed graph, where for each arc from node x to node y with label w, there exists a symmetric arc from node y to node x with the same label w. In an undirected graph any pair of directed arcs from node x to node y and vice versa will be denoted by x y. Now, given an undirected graph G with 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 251 non-negative weights on its arcs, we have that a shortest path between two nodes may be made out of arcs none of which belongs to the minimal spanning tree of G. This fact is illustrated in the graph of Figure 8.8.3. In that figure we use the usual convention of drawing every pair of directed arcs of an undirected graph by a single arc without arrowheads. In Figure 8.8.3 a shortest path from node b to node c is the arc b c with weight 3, while a minimal spanning tree of the graph G has total weight 4 and it is made out of the arcs a b and a c. 2 a b 2 3 c Figure 8.8.3. An undirected graph with non-negative labels on the arcs. The thicker arcs are those of the minimal spanning tree. In what follows we present a Java program which implements Dijkstra’s Single Source Shortest Path algorithm. It is made out of two files: SSSP.java and Graph.java, which are located in the same folder. 252 8. PATH PROBLEMS IN DIRECTED GRAPHS /** * ======================================================================== * Single Source Shortest Path: Main Program * Filename: "SSSP.java" * * This program computes a shortest path from a node to any other node * of a weighted, directed graph. The weight of an arc is the length of the * arc. * * In what follows we will feel free to say ’the shortest path’ (or ’the * shortest distance’), instead of ’a shortest path’ (or ’a shortest * distance’), even if the shortest path may not be unique. * * In the command: * java SSSP nn in fn * the integers nn, in, and fn denote the number of nodes, the initial * node, and the final node, respectively. * For instance, we should issue the command: java SSSP 5 0 3 * for a graph of 5 nodes in which we look for a shortest path from node 0 * to node 3. * ======================================================================== */ import java.util.ArrayList; import java.util.Iterator; public class SSSP { // SSSP : Single Source Shortest Path public static int min (int a, int b) { if (a>b) {return b;} else {return a;} } public static void main(String[] args) { int N = 0; int initNode = 0; int finalNode = 0; // - to be read from the input: args[0] // - to be read from the input: args[1] // - to be read from the input: args[2] // ---------------------------------- N: number of Nodes of the graph. // N >= 1. try {N = Integer.parseInt(args[0]); if ( N < 1) { throw new RuntimeException (); }; } catch (Exception e) {System.out.print("*** Wrong number of nodes!\n");} // ---------------------------------- initNode: initial node. // 0 <= initNode <= N-1. try {initNode = Integer.parseInt(args[1]); if ( ( 0 > initNode ) || ( initNode > N-1 ) ) { throw new RuntimeException (); }; } catch (Exception e) {System.out.print("*** Wrong initial node!\n");} // ---------------------------------- finalNode: final node. // 0 <= finalNode <= N-1. 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 253 try {finalNode = Integer.parseInt(args[2]); if ( ( 0 > finalNode ) || ( finalNode > N-1 ) ) { throw new RuntimeException (); }; } catch (Exception e) {System.out.print("*** Wrong final node!\n");} // -----------------------------------------------------------------------// MAXLENGTH: maximum length of the arcs. 1 <= length <= MAXLENGTH. final int MAXLENGTH = 9; // // // // -----------------------------------------------------------------------PerCentPROB: probability of connectivity. 0 < PerCentPROB <= 100. If PerCentPROB = k, every node is connected with probability k/100 to the other N-1 nodes. final int PerCentPROB = 40; // // // // -----------------------------------------------------------------------Generation of a random graph with N nodes. The length of each arc belongs to the set {1,..., MAXLENGTH}. Every node has (N-1) x PerCentPROB / 100 successor nodes (in average). Graph g = new Graph(N, initNode, finalNode, MAXLENGTH, PerCentPROB); g.graphPrint(); System.out.println("Every node is connected to the other nodes with " + "probability " + PerCentPROB + " percent."); System.out.println("The length of each arc belongs to the set {1,..., " + MAXLENGTH + "}."); System.out.println("The initial node is: " + initNode); System.out.println("The final node is: " + finalNode); // ---------------------------------------------int p = g.nearestOutsideNode(); while (p >= 0) { // <<-------------------------------------+ g.reach[p] = true; // Reach = Reach U {p} // | for (int m=0; m<N; m++) { // | if (!g.reach[m] && g.graph[p][m] >= 0) { // | if (g.shortestDistances[m] >= 0) // | {g.shortestDistances[m] = // | min (g.shortestDistances[p] + g.graph[p][m], // | g.shortestDistances[m]);} // | else {g.shortestDistances[m] = // | g.shortestDistances[p] + g.graph[p][m];}; // | }; // | }; // | p = g.nearestOutsideNode(); // | } // end of while <<------------------------+ // ---------------------------------------------// add(0,e) inserts the element e at position 0, and shifts // the element at position 0 and all subsequent elements to the right // (adds 1 to their indices). g.itinerary.add(0, new Integer(finalNode)); int m = finalNode; while (m != initNode) { // <<-------------------------------------+ int j=0; // | while ( j == m || g.shortestDistances[j] != // | -g.graph[j][m]+g.shortestDistances[m] ) { j++;}; // | g.itinerary.add(0, new Integer(j)); // | 254 8. PATH PROBLEMS IN DIRECTED GRAPHS m=j; // | }; // end of while <<------------------------+ g.shortestDistancesPrint(); g.itineraryPrint(); } } // -----------------------------------------------------------------------/** * input: * -----------------------------------------------------------------------* javac SSSP.java * java SSSP 5 0 3 * * output: * ------* * The random graph has 5 node(s) (from node 0 to node 4). * The matrix of the arc lengths is: * 0 : 0 - 9 - * 1 : - 0 - - 7 * 2 : - - 0 4 * 3 : - - 6 0 * 4 : - - 6 - 0 * * Every node is connected to the other nodes with probability 40 percent. * The length of each arc belongs to the set {1, ..., 9}. * The initial node is: 0 * The final node is: 3 * * The shortest distances from the initial node 0 to every node of * the graph are (’-’ means ’unreachable node’): * 0 - 9 13 * * The nodes of the shortest path from node 0 to node 3 are: * 0 2 3 * * The cumulative distances of the shortest path from node 0 to node 3 are: * 0 9 13 * * -----------------------------------------------------------------------*/ 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 255 /** * ========================================================================= * The Graph class * Filename: "Graph.java" * * This class Graph is not a monitor, because not all fields are "private". * However, the non-private fields are read, but never assigned to. * All methods are "synchronized". * We can say that this class Graph is "almost" a monitor. * * In what follows we will feel free to say ’the shortest path’ (or ’the * shortest distance’), instead of ’a shortest path’ (or ’a shortest * distance’), even if the shortest path may not be unique. * ========================================================================= */ import java.util.Random; import java.util.ArrayList; import java.util.Iterator; public class Graph { final final final static int int int int N; initNode; finalNode; [][] graph; // // // // N = number of nodes of the graph 0 <= initNode <= N-1 0 <= finalNode <= N-1 an N x N array // shortestDistances is an array of N integers belonging to the object // of the class Graph static int [] shortestDistances; static boolean [] reach; static ArrayList itinerary = new ArrayList(); // --------------- beginning of constructor -------------------------------public Graph(int N, int initNode, int finalNode, int MAXLENGTH, int PerCentPROB) { /** Generation of a random, directed graph with N nodes. * The random variable randomConnect controls the connectivity: * node i is connected with node j, for i != j, with percent * probability PerCentPROB: 0 <= PerCentPROB <= 100 * The random variable randomLength controls the length of the arcs: * 1 <= length of an arc <= MAXLENGTH */ this.N this.initNode this.finalNode this.graph this.shortestDistances this.reach Random randomConnect Random randomLength = = = = = = = = N; initNode; finalNode; new int [N][N]; new int [N]; // new boolean [N];// new Random(); // new Random(); // \Lambda(n) for each node n the Reach set random graph connectivity random arc length 256 8. PATH PROBLEMS IN DIRECTED GRAPHS // initializing the graph: -1 (printed as "-") means "unconnected node" // graph[i][j] = m with m >= 0 // iff m is the length of the arc from node i to node j for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { if ((i != j) && (randomConnect.nextInt(100) < PerCentPROB)) { graph[i][j] = 1 + randomLength.nextInt(MAXLENGTH); } else if (i == j ) { graph[i][j] = 0; } else { graph[i][j] = -1; }; } } // initializing the array shortestDistances: // -1 (printed as "-") means "unreached node". // shortestDistances[i] = d with d >= 0 // iff d is the shortest distance from the initial node to node i // computed so far. for(int j=0;j<N;j++) shortestDistances[j] = graph[initNode][j]; // initializing the array reach // -1 (printed as "-") means "node not in the array reach" // initially for(int j=0;j<N;j++) reach[j] = false; reach[initNode] = true; // initializing the shortest path, called ‘itinerary’, // from initNode to finalNode ArrayList itinerary = new ArrayList(); } // --------------- end of constructor -------------------------------------// --------------- updating the array shortestDistances -------------------public synchronized void shortestDistancesUpdate (int i, int distance) { shortestDistances[i] = distance; } // --------------- accessing the array shortestDistances ------------------public synchronized int shortestDistancesAt (int i) { return shortestDistances[i]; } // --------------- finding a p node ---------------------------------------// if node == -1 there is no such nearest node outside the array reach. public synchronized int nearestOutsideNode () { int min = -1; int node = -1; for(int j=0; j<N; j++) { if (reach[j] == false && shortestDistances[j] >= 0 && min == -1) { node = j; min = shortestDistances[j]; } else if (reach[j] == false && shortestDistances[j] >= 0 && shortestDistances[j] < min) { node = j; min = shortestDistances[j]; }; }; return node; } 8.8. SINGLE SOURCE SHORTEST PATHS IN DIRECTED GRAPHS 257 // --------------------- printing the graph -------------------------------public synchronized void graphPrint () { System.out.println("The random graph has "+ N +" node(s) " + "(from node 0 to node " + (N-1) + ").\n" + "The matrix of the arc lengths is:"); for(int i=0;i<N;i++) { System.out.print(i + " : "); for(int j=0;j<N;j++) { if (graph[i][j] < 0) { System.out.print("- "); } else System.out.print( graph[i][j] + " "); }; System.out.println(); }; System.out.println(); } // --------------- printing the array shortestDistances ------------------ public synchronized void shortestDistancesPrint () { System.out.println("\nThe shortest distances from the initial node " + initNode + " to every node of\nthe graph " + "are (’-’ means ’unreachable node’):"); for(int i=0;i<N;i++) { if (shortestDistances[i] < 0) { System.out.print("- "); } else System.out.print( shortestDistances[i] + " "); } System.out.println(); } // --------------- printing the array reach ------------------------------- // This method is NOT needed. This method is useful for debugging. public synchronized void reachPrint () { System.out.print("\nThe reach array is: \n"); for(int i=0;i<N;i++) { if (!reach[i]) { System.out.print("- "); } else System.out.print("1 "); }; System.out.println(); } // --------------- printing the shortest path from initNode to finalNode --// the shortest path from initNode to finalNode is called ‘itinerary’ // public synchronized void itineraryPrint () { if ( shortestDistances[finalNode] >= 0 ) { System.out.println("\nThe nodes of the shortest path from node " + initNode + " to node "+finalNode + " are:"); for (Iterator iter = itinerary.iterator(); iter.hasNext(); ) { System.out.print((iter.next()).toString() + " "); }; System.out.println(); 258 8. PATH PROBLEMS IN DIRECTED GRAPHS // -------------------System.out.println("\nThe cumulative distances of the shortest " + "path from node " + initNode + " to node "+finalNode + " are:"); int d = 0; int i = initNode; int j = 0; for (Iterator iter1 = itinerary.iterator(); iter1.hasNext(); ) { j = ((Integer)iter1.next()).intValue(); d = d + graph[i][j]; i = j; System.out.print(d + " "); }; System.out.println(); // -------------------} else { System.out.println("There is no path from node " + initNode + " to node " + finalNode + "."); }; } } // ------------------------------------------------------------------------- 8.9. From Nondeterministic Finite Automata to Regular Expressions Given a directed graph G of n nodes with every arc labeled by a regular expression over an alphabet Σ (in particular, a single symbol of Σ), let us consider the problem of finding for each pair of nodes i and j in G, the regular expression which denotes the language made out of all words which take from node i to node j as we now specify. A word w takes from node i to node j iff (i) w is a0 . . . am+1 , where for all i = 0, . . . , m, ai ∈ Σ, and (ii) there is a path hn0 , n1 , . . . , nm+1 i such that: (ii.1) n0 = i, (ii.2) nm+1 = j, and (ii.3) for all i = 0, . . . , m, ai belongs to the language denoted by the regular expression which is the label of the arc hni , ni+1 i. This problem of finding regular expressions can be solved by applying the analogous of Fact 8.6.1 on page 242 for matrices over the Regular Expressions ic-semiring (see page 241). Given a directed graph G with n nodes with regular expressions as labels of the arcs, let us associate with G the n×n matrix R(G) such that for all i, j, with 1 ≤ i, j ≤ n, R(G)ij = expr (i, j), where expr(i, j) is the regular expression which labels the arc from node i to node j. For all i, j (not necessarily distinct), with 1 ≤ i, j ≤ n, we have that expr (i, j) = ∅ if there is no arc from node i to node j. It can be shown that, given any pair hi, ji of nodes of the graph G, one may compute the language made out of the words which take from i to j by computing the transitive closure of the matrix R(G) over the Regular Expressions ic-semiring of the labels of G. Notice that in the identity matrix over the Regular Expressions ic-semiring each element of the main diagonal is {ε} (which is the identity for  ), while every other element is ∅ (which is the identity for ∪). This definition of the identity matrix is based on the facts that: 8.9. FROM NONDETERMINISTIC FINITE AUTOMATA TO REGULAR EXPRESSIONS 259 (1) for all i and j with 1 ≤ i, j ≤ n, there is no path of one or more arcs from a node i to a node j, and (2) if i = j then node i is reachable from node i itself via a path of length 0 (and this explains why {ε} is in the main diagonal of the identity matrix) else node j is not reachable from node i (and this is why ∅ is outside the main diagonal of the identity matrix). For the case of matrices over the Regular Expressions ic-semiring it is not natural to establish a fact analogous to Fact 8.6.2 on page 244, because the operations ∪ and  cannot reasonably be considered as atomic computation steps. However, by using a fact analogous to Fact 8.6.1 on page 242, we can prove Kleene Theorem which states that for every finite automaton there exists an equivalent regular expression, that is, a regular expression which denotes the language accepted by the finite automaton. Indeed, if we assume that each element of the matrices A11 , A12 , A21 , and A22 is a symbol in Σ, the graph is a (deterministic or nondeterministic) finite automaton. A11 A12 computes Let that automaton be called A. The transitive closure of A21 A22 the language from a node to any other node by using the operations  , +, and ∗ , instead of the operations ∧, ∨, and ∗ occurring in the statement of Fact 8.6.1. Thus, in particular, we get a regular expression from the initial state to any final state of the finite automaton A. The regular expression denoting the language accepted by A is the sum of all regular expressions from the initial state to a final state of A. CHAPTER 9 String Matching In this chapter we present two matching algorithms for testing whether or not a pattern occurs in string. The first algorithm is a naive algorithm (see page 261) whose Prolog implementation will be given in Section 9.2 on page 271. The second algorithm is the Knuth-Morris-Pratt algorithm [14]. 9.1. Knuth-Morris-Pratt Pattern Matching In this section we present a pattern matching algorithm taken from [14]. This algorithm detects all occurrences, possibly overlapped, of a substring p, called the pattern, in a string s, called the subject. The main idea of this algorithm is the construction of a finite automaton. We will clarify this point in what follows. The time complexity of this algorithm is O(m + n), where m is the length of the string s and n is the length of the substring p. First we present the naive pattern matching algorithm which has time complexity O(n m). It can be expressed as follows. Algorithm 9.1.1. Naive Pattern Matcher. Input: the subject string s[0], . . . , s[n−1] and the pattern string p[0], . . . , p[m−1]. We assume that n ≥ 1 and m ≥ 1. Output: a list λ = [i1 , . . . , ik ] of non-negative integers such that for all i in λ, we have that p occurs in position i in the subject s, that is, for all j = 0, . . . , m−1 we have that s[i+j] = p[j]. λ := [ ]; for i = 0, . . . , n−m do occurs := true; for j = 0, . . . , m−1 do if s[i+j] 6= p[j] then occurs := false; if occurs then λ :=append (λ, [i]) (†) In the instruction (†) of the external for-loop we have used the append function, instead of the less expensive cons function, because we want that a pattern occurrence i1 which is located in the subject string on the left of another pattern occurrence i2 , that is, i1 < i2 , should be listed in λ on the left of i2 . This naive pattern matcher works by repeating, for each position in the subject string, a test which checks whether or not the substring of the subject string starting at that position coincides with the given pattern. We can improve over this naive pattern matcher algorithm by using the KnuthMorris-Pratt pattern matcher which we now present. This algorithm improves the 261 262 9. STRING MATCHING naive pattern matcher because, if during the process of checking an occurrence of the pattern in the subject we find a mismatch between the character of the pattern and the character of the subject string, then we resume the matching process not by a right-shift of the pattern by one position only, but by performing a right-shift of r (≥ 1) positions, as indicated by the so called failure function which we will introduce below. The failure function determines the value of r by taking into account the structure of the pattern and, by doing so, it allows us to reduce the total number of character comparisons required during the matching process. Here is the Knuth-Marris-Pratt pattern matcher. It is made out of two parts that have a very similar structure: (i) the computation of the failure function, and (ii) the computation of the occurrences of the pattern. Algorithm 9.1.2. Knuth-Morris-Pratt Pattern Matcher. Input: the subject string s[0], . . . , s[n−1] and the pattern string p[0], . . . , p[m−1]. We assume that n ≥ 1 and m ≥ 1. Output: a list λ = [i1 , . . . , ik ] of k (≥ 0) non-negative integers such that for all i in λ, we have that the pattern p occurs at position i in the subject s, that is, for all j = 0, . . . , m−1 we have that s[i+j] = p[j]. Computation of the failure function: π[0], . . . , π[m−1]. π[0] := 0; j := 0; for i = 1, . . . , m−1 do while j > 0 and p[j] 6= p[i] do j := π[j −1]; if p[j] = p[i] then j := j +1; π[i] := j; (1.α) (1.β) (1.γ) (1.δ) Computation of the occurrences of the pattern: p[0], . . . , p[m−1]. λ := [ ]; j := 0; for i = 0, . . . , n−1 do while j > 0 and p[j] 6= s[i] do j := π[j −1]; if p[j] = s[i] then j := j +1; if j = m then begin λ := append (λ, [i−m+1]); j := π[j −1] end (2.α) (2.β) (2.γ) (2.δ) Now we introduce the failure function π. It is represented as the array π[0, . . . , m−1] which has the same length m of the pattern p. We first need the following notions. 9.1. KNUTH-MORRIS-PRATT PATTERN MATCHING 263 Definition 9.1.3. [Prefix and Suffix] Let us consider a pattern p[0, 1, . . . , m−1] of length m. The non-empty prefixes of that pattern are: p[0], p[0, 1], . . ., and p[0, 1, . . . , m−1]. For j = 0, . . . , m−1, the prefix p[0, . . . , j] is also denoted by p0j . A proper prefix of that pattern is a prefix which is different from the pattern itself. The suffixes of the pattern p[0, 1, . . . , m−1] are: p[0, 1, . . . , m−1], p[1, . . . , m−1], . . ., p[m−2, m−1], p[m−1]. A proper suffix of that pattern is a suffix which is different from the pattern itself. Given the pattern p[0, 1, . . . , m−1], for j = 0, . . . , m−1, we have that the prefix p0j is the sequence of the j+1 leftmost characters of the pattern. The following tables shows the prefixes of the pattern p[0, 1, . . . , m−1]. For j = 0, the prefix p00 is the leftmost character p[0] of the pattern. prefixes p0j of the pattern p[0, 1, . . . , m−1] length of the prefix j=0 j=1 ... j = m−1 p00 = p[0] p01 = p[0, 1] ... ... p0 m−1 = p[0, 1, . . . , m−1] 1 2 ... m Definition 9.1.4. [Failure Function] Given a pattern p of length m, its failure function π is defined for each j = 0, . . . , m−1, and π[j] is the length of the longest proper prefix of p0j which is also a proper suffix of p0j . We have that: π[0] = 0 π[1] = if p[0] = p[1] then 1 else 0 If we consider the pattern a a b a a, whose length m is 5, we get the failure function which is represented in the following table where, for j = 0, . . . , m−1, the value of p[j] is written in column j of the row named ‘pattern p’, and the value of π[j] is written in column j of the row named ‘failure function π’: p[0] p[1] p[2] p[3] p[4] pattern p: a a b a a failure function π: 0 1 0 1 2 π[0] π[1] π[2] π[3] π[4] Let us explain the entries of the above table. We have that: (i) p00 is a and the longest proper prefix of a which is also a proper suffix of a, is ε and the length of ε is 0. Thus, π[0] = 0. (ii) p01 is a a and the longest proper prefix of a a which is also a proper suffix of a a, is a and the length of a is 1. Thus, π[1] = 1. (iii) p02 is a a b and the longest proper prefix of a a b which is also a proper suffix of a a b, is ε and the length of ε is 0. Thus, π[2] = 0. (iv) p03 is a a b a and the longest proper prefix of a a b a which is also a proper suffix of a a b a, is a and the length of a is 1. Thus, π[3] = 1. (v) p04 is a a b a a and the longest proper prefix of a a b a a which is also a proper suffix of a a b a a, is a a and the length of a a is 2. Thus, π[4] = 2. 264 9. STRING MATCHING If all characters in the pattern p are equal, that is, p[0] = p[1] = . . . = p[m−1], then the failure function π is such that: π[0] = 0, π[1] = 1, π[2] = 2, . . . , π[m−1] = m−1. Thus, if we assume that the pattern p is a sequence of a’s, we get the following table, where for j = 0, . . . , m−1, the j-th column indicates that p[j] = a and π[j] = j: p[0] p[1] p[2] . . . p[m−1] pattern p: a a a ... a failure function π: 0 1 2 . . . m−1 π[0] π[1] π[2] . . . π[m−1] The failure function derived at the end of the first part of the Knuth-Morris-Pratt algorithm, called Computation of the failure function, is used during the second phase, called Computation of the occurrences of the pattern, as we now indicate. Let us consider the above pattern a a b a a and suppose that we have found the matching substring a a b a and we get a mismatch on the last character which should be a. That is, s[i] 6= a. The value of j is 4. This mismatch tells us that we have to consider a new value for j which is π[j−1] (see instruction (2.β)). In our case we get j = 1. Then, we check whether or not the current character in the subject string is a (because p[1] is a). We fail again and we get the new value of j which is 0. Indeed, according to the instruction (2.β), we have to take as new value of j the value of π[j−1], that is, π[0] which is 0. At this point it is not the case that j > 0, we exit the loop of instruction (2.β), and we perform instruction (2.γ). Since p[0] = a and s[i] 6= a and j 6= m, we increase i by 1 (which corresponds to a shift of the pattern on the subject string by one character) and we execute again the instructions (2.β), (2.γ), and (2.δ) with this new value of i (actually, in our case, since j = 0, the instruction (2.β) will not be executed). The failure function for a pattern p[0] p[1] . . . p[m − 1] can be represented as a finite automaton whose states are 0, 1, . . . , m−1, m (see in Figure 9.1.1 on the facing page the failure function for the pattern a a b a a). For each k = 0, . . . , m−1, there is a forward arc from state k to state k+1 labeled by the predicate ‘= p[k]’. For each k = 1, . . . , m, there is a backward arc from state k to state π[k−1] labeled by the predicate ‘6= p[k]’. The label of the backward arc from state m to state π[m−1] is labeled by the predicate true. From state 0 there is an arc to state 0 itself labeled by ‘6= p[0]’. This automaton is used as an acceptor of the subject string in the second part of the Knuth-Morris-Pratt algorithm called Computation of the occurrences of the pattern, according to the following rule: if for the input subject string s the automaton gets from the initial state 0 to the final state m, then there is an occurrence of the pattern p in the subject string s. The predicate in each arc of the automaton allows the transition iff it is satisfied by the current character of the subject string. For the pattern a a b a a of length m = 5 the automaton which represents the failure function is depicted in Figure 9.1.1 on the next page. For the pattern a b a b a a a of 9.1. KNUTH-MORRIS-PRATT PATTERN MATCHING p[0] a p[1] a 6= a • 0 =a • 1 6= a 6= a =a • p[2] b 2 p[3] a =b • 3 265 p[4] a =a • 4 =a • 5 6= a 6= b true π[j] : 0 1 0 1 2 j: 0 1 2 3 4 Figure 9.1.1. The finite automaton corresponding to the failure function for the pattern a a b a a. We have that m = 5. When the characters of the subject string are fed into this automaton for finding the occurrences of the pattern, we consider the next character in input only when we make a transition with the dot •. length m = 7 the automaton which represents the failure function is depicted in Figure 9.1.2 on the following page. At each state the automaton which represents the failure function, reads the character at hand and that character is used for making the transition to the next state. As already stated, in the figures representing the failure functions as finite automata, the failure function is used for drawing the backward arcs according to the following rules: (i) for all i, j, with i ≥ j, if π[i−1] = j then there is a backward arc from state i to state j, and (ii) there is a backward arc from state 0 to itself labeled by the negation of the label of the arc from state 0 to state 1. For instance, in Figure 9.1.1 we have drawn a backward arc from state 4 to state 1 because π[4−1] = 1. Now we would like to make the following two remarks. Remark 9.1.5. When an occurrence of the pattern has been found (see, for instance, the arc labeled by true from state 5 to state 2 in the automaton of Figure 9.1.1), the automaton representing the failure function makes a state transition as if a failure occurred. Indeed, the pattern occurrence we have found and the next pattern occurrence may overlap. Remark 9.1.6. Some of the tests which the failure function forces us to make, can be avoided because we know in advance the results of these tests. Let us consider, for instance, the case of the failure function represented by the automaton of Figure 9.1.1. If in state 4 we have a character of the subject which is different from a we go to state 1 where the test ‘= a’ is bound fail and we have to go to state 0. By avoiding all 266 9. STRING MATCHING p[0] a 6 a • = 0 p[1] b 6= a =a • 1 p[2] a =b • 2 p[3] b =a • 3 p[4] a =b • 6= b 6= b 4 p[5] a 6 a = =a • 5 p[6] a =a • 6= a 6 =a • true 6= a π[j] : 0 0 1 2 3 1 1 j: 0 1 2 3 4 5 6 7 Figure 9.1.2. The finite automaton corresponding to the failure function for the pattern a b a b a a a. We have that the length m of the pattern is 7. When the characters of the subject string are fed into this automaton for finding the occurrences of the pattern, we consider the next character in input only when we make a transition with the dot •. redundant tests such as the test ‘= a’, we get the automaton of Figure 9.1.3, instead of the automaton of Figure 9.1.1 on the previous page. p[0] a p[1] a p[2] b 6= a • 6= a ∧ 6= b • 6= a • =a =a • • 1 =a • 0 • 2 p[3] a p[4] a • =b • 6 a = 3 • =a • 4 6= a • =a =a • =b • 5 6= a ∧ 6= b Figure 9.1.3. The finite automaton corresponding to the failure function for the pattern a a b a a. When the characters of the subject string are fed into this automaton for finding the occurrences of the pattern, we always consider the next character in input (indeed, all transitions have the dot •). Note, however, that if we use the automaton of Figure 9.1.3, instead of the one of Figure 9.1.1, we do not improve the asymptotic time complexity of the KnuthMorris-Pratt algorithm (see Section 9.1.1 on the facing page). It is interesting to notice that the first part of the Knuth-Morris-Pratt algorithm which computes the failure function, that is, the length of the longest proper prefix 9.1. KNUTH-MORRIS-PRATT PATTERN MATCHING 267 of the pattern which is also a proper suffix of pattern, is very similar to the second part of the algorithm which computes the occurrences of the pattern in the subject string. The only differences between the two parts are the following ones. (i) The for-do loop in instruction (1.α) starts with i = 1 and not i = 0, because for every pattern the value of π[0] is 0 and thus, we can skip the loop for i = 0. (ii) We record the fact that an occurrence of the pattern has been found, by inserting in the list λ the initial position of the occurrence of the pattern in the subject string (see instruction (2.δ)). Then we continue our matching process by looking for a new, possibly overlapping, occurrence of the pattern, and we do so by acting as if a matching failure did occur. Indeed, as in the case of a failure, we update the value of j and the new value of j is set to π[j −1]. We have that the Knuth-Morris-Pratt algorithm for the input subject s = aabacaabaabaa with length n = 13 and the input pattern p = aabaa computes the list [5, 8] of occurrence positions (see Figure 9.1.4). a a b a c a a b a a b a a 0 1 2 3 4 5 6 7 8 9 10 11 12 Figure 9.1.4. The two occurrences at positions 5 and 8 of the pattern a a b a a in the subject string a a b a c a a b a a b a a. 9.1.1. Time Complexity Analysis of the Knuth-Morris-Pratt Algorithm. We start off by evaluating the time complexity for computing the failure function. We will measure this time complexity in terms of the number of assignments to the variable j. The for-loop of the computation of the failure function is performed from i = 1 to i = m−1. The while-loop at instruction (1.β) in the body of the for-loop is controlled by the value of j which may be at most m−1 (and this case occurs when the characters in the pattern are all equal) and at each execution of the body of the loop, as we will prove in Lemma 9.1.7 below, the value of j can only be decreased. Thus, we conclude that the time complexity of the computation of the failure function is O(m2 ). The following analysis, based on an amortized analysis, shows that the cost of the computation of the failure function is, in fact, O(m). First we prove the following lemma. Lemma 9.1.7. For all i = 1, . . . , m−1, for all h = 0, . . . , i, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ i. Proof. By induction. (Basis: i = 1). We want to show that after the execution of instruction (1.δ) in the first iteration of the for-loop, that is, for i = 1, we have that: for h = 0, 1, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ 1. Indeed, since initially j = 0, instruction (1.β) is not performed. Thus, (i) by (1.γ) we have that if p[0] = p[1] then j = 1 else j = 0, and (ii) by (1.δ) we have 268 9. STRING MATCHING that π[1] = j. By recalling that: (i) π[0] = 0, and (ii) if p[0] = p[1] then π[1] = 1, we get, as desired, that 0 ≤ π[0] ≤ 0, 0 ≤ π[1] ≤ 1, and 0 ≤ j ≤ 1. (Step) We assume that for all h = 0, . . . , i, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ i holds for i = k and we want to show that for all h = 0, . . . , i, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ i holds for i = k+1. By using Hoare’s triples we have to show the validity of the following triple: { for h = 0, . . . , k, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ k } while j > 0 and p[j] 6= p[k+1] do j := π[j −1]; if p[j] = p[k+1] then j := j +1; π[k+1] = j (1.β) (1.γ) (1.δ) { for h = 0, . . . , k+1, 0 ≤ π[h] ≤ h and 0 ≤ j ≤ k+1 } We have the following two points. Point (1). At instruction (1.β) the value of the variable j never increases and may only decrease or remain unchanged, as we now show. By the precondition, we have that for h = 0, . . . , k, 0 ≤ π[h] ≤ h and thus, by changing h to h′ = h+ 1, we get that for h′ = 1, . . . , k + 1, 0 ≤ π[h′ −1] ≤ h′ −1. As a consequence, for h′ = 1, . . . , k +1, 0 ≤ π[h′ −1] < h′ . We have that the assignment j := π[j −1] decreases the value of j, because it is performed only for some values of j such that 1 ≤ j ≤ k (see the precondition 0 ≤ j ≤ k and the test j > 0 of the while-loop) and for all such values of j, we have that 0 ≤ π[j] ≤ j holds (see again the precondition), and thus for j = 1, . . . , k, before the assignment we have that π[j−1] < j (because π[j −1] ≤ j −1). Moreover, since for h = 0, . . . , k, 0 ≤ π[h], the variable j never becomes negative. Point (2). At instruction (1.γ) the variable j increases by at most one unit. From Points (1) and (2) and the precondition 0 ≤ j ≤ k, after instruction (1.δ) we get that: 0 ≤ j ≤ k+1. In order to get the postcondition it remains to show that for h = 0, . . . , k+1, we have that 0 ≤ π[h] ≤ h. By the precondition and the fact that for h = 0, . . . , k, the value of π[h] is not changed, we get that for h = 0, . . . , k, 0 ≤ π[h] ≤ h. Moreover, we have that 0 ≤ π[k + 1] ≤ k + 1, because at instruction (1.δ) π[k + 1] is assigned the value of j and we have already proved that 0 ≤ j ≤ k+1. This completes the proof of the lemma.  During the computation of the failure function, j is initially 0 and it may be increased by at most m−1 units during the m−1 iterations of the body of the forloop. During the execution of the while-loop at instruction (1.β) we have that j is strictly decreased (by Lemma 9.1.7 on the preceding page). Thus, during the whole computation of the failure function the variable j is assigned at most 2(m−1) times, that is, the time complexity of the failure function is O(m). Since the first part of the Knuth-Morris-Pratt algorithm which computes the failure function, has the same structure of the second part which computes the occurrences of the pattern, in order to measure the time complexity of the KnuthMorris-Pratt algorithm we can measure the computation of two failure functions, one for a pattern of length m and one for a pattern of length n. We conclude that the overall time complexity of the Knuth-Morris-Pratt algorithm is O(m+n). 9.1. KNUTH-MORRIS-PRATT PATTERN MATCHING 269 Note that the time complexity is O(m+n) also if we measure the number of tests performed by the Knuth-Morris-Pratt algorithm, instead of the number of assignments to j. Indeed, for all values of j, we make an assignment to j after at most two tests, which are: (i) j > 0, and (ii) p[j] 6= p[i]. We do not have to count the test p[j] = p[i] because if we know the value of the test p[j] 6= p[i] we also know the value of the test p[j] = p[i]. It is easy to see that the same time complexity of O(m+n) for the Knuth-MorrisPratt algorithm holds if we measure the sum of the number of the assignments to j and the number of tests. 9.1.2. Java Implementation of the Knuth-Morris-Pratt Algorithm. Below we present a Java program which implements to Knuth-Morris-Pratt algorithm for pattern matching. It runs under the Java compiler 1.5.0-13 and Mac OS X. In this program, instead of constructing the list λ of positions, we have printed out the position of every occurrence of the pattern as soon as it has been computed. The test case presented in the final comments of the program shows the values of the variables i and j during the execution of the Knuth-Morris-Pratt algorithm for the subject a a b a c a a b a a b a a and the pattern a a b a a which occurs at positions 5 and 8. /** * ========================================================================= * KNUTH-MORRIS-PRATT PATTERN MATCHER * Filename: "KMP.java" * * ========================================================================= */ public class KMP { private static void printar(int[] array, int size) { for (int k=0; k<size; k++) {System.out.print(array[k]+" ");} System.out.println(); } /** * -----------------------------------------------------------------------* main * -----------------------------------------------------------------------*/ public static void main(String[] args) { boolean traceon = true; // traceon: tracing variable String s, p; int sl,pl; s = "aabacaabaabaa"; p = "aabaa"; sl = s.length(); pl = p.length(); // s: given subject // p: pattern to search 270 9. STRING MATCHING System.out.println("\nThe given subject string s of " + sl + " elements is:\n " + s); System.out.println("\nThe given pattern p of "+pl+" elements is:\n "+ p); /** * ------------------------------------------------------------------------* Recall that indexing in Java begins with 0 (as in C++). Thus, * * stp=abcab length(stp)=5 stp.charAt(2) is c. * index: 01234 * * lstp1=4 * ------------------------------------------------------------------------*/ /** * -----------------------------------------------------------------------* computing the failure function pi * -----------------------------------------------------------------------*/ int [] pi = new int [pl]; // the length of pi is the length of the pattern int i, j; pi[0]=0; j=0; for (i=1; i<pl; i=i+1) { // <<-----------------------+ while (j>0 && p.charAt(j)!=p.charAt(i)) {j=pi[j-1];}; // | if (p.charAt(j)==p.charAt(i)) {j=j+1;}; // | pi[i]=j; // | }; // end of for // <<-----------------------+ //-------------------------------------------------------------------------System.out.print( "\nThe failure function is given by the following array pi" + "\nof " + pl + " elements:\n "); printar(pi,pl); System.out.println(); //-------------------------------------------------------------------------/** * ------------------------------------------------------------------------* finding the occurrences of the pattern * ------------------------------------------------------------------------*/ j=0; for (i=0; i<sl; i=i+1) { // <<-----------------------+ while (j>0 && p.charAt(j)!=s.charAt(i)) {j=pi[j-1];}; // | if (p.charAt(j)==s.charAt(i)) {j=j+1;}; // | if (traceon) {System.out.println(" i="+i+" j="+j);}; // tracing // | if (j==pl) { // | System.out.println("The pattern occurs at position "+(i-pl+1)+"\n");// | j=pi[j-1]; // | }; // | }; // end of for // <<-----------------------+ } // end of main } 9.2. STRING MATCHING IN PROLOG 271 /** * input: output: (with traceon == true) * ------------------------------------------------------------------------* javac KMP.java * java KMP * * The given subject string s of 13 elements is: * aabacaabaabaa * * The given pattern p of 5 elements is: * aabaa * * The failure function is given by the following array pi * of 5 elements: * 0 1 0 1 2 * * i=0 j=1 * i=1 j=2 * i=2 j=3 * i=3 j=4 * i=4 j=0 * i=5 j=1 * i=6 j=2 * i=7 j=3 * i=8 j=4 * i=9 j=5 * The pattern occurs at position 5 * * i=10 j=3 * i=11 j=4 * i=12 j=5 * The pattern occurs at position 8 * * ------------------------------------------------------------------------*/ 9.2. String Matching in Prolog The following Prolog program tests whether or not a pattern P (viewed as list of characters) occurs in a string S (also viewed as a list of characters). The pattern P occurs in S iff in S there exists a consecutive list of characters which is a substring of S and is equal to P . In order to understand the way in which this program works it is enough to recall the usual definition of the append predicate, that is, append (Xs, Ys, Zs) holds iff the string Zs is the concatenation of the strings Xs and Ys. For instance, we have that: append ([2, 3], [4, 2], Zs) holds iff Zs = [2, 3, 4, 2] append (Xs, [4, 4], [4, 3, 4, 4]) holds iff Xs = [4, 3]. String Matching in Prolog: basic version. 1. occur (P, S) ← append(I, R, S), append(L, P, I) 2. append ([ ], Ys, Ys) ← 3. append ([X|Xs], Ys, [X|Zs]) ← append (Xs, Ys, Zs) 272 9. STRING MATCHING We have that occur (P, S) holds iff the pattern P occurs in the string S. Indeed, clause 1 says that P occurs in S iff there exist three lists of characters I (short for Intermediate), R (short for right), and L (short for left) such that both append (I, R, S) and append (L, P, I) hold. We can depict this situation as in the following figure, where the list I is the concatenation of the lists L and P , and the list S is the concatenation of the lists I and R. I subject S : L R pattern P Note that, although the position of the atoms does not influence the logical meaning of a clause, it may influence its operational behaviour. In particular, due to the implementation of the append predicate in Prolog, the position of the atoms append(I, R, S) and append(L, P, I) in the body of clause 1 should not be interchanged because, otherwise, the computation may not terminate. From the length of the list L we can get the position of the pattern P in the subject string S. Then the various positions in which the pattern occurs in the subject, can be obtained by using the backtracking facility available in Prolog. This can done by using the findall predicate as indicated in the following program. String Matching in Prolog: finding all occurrence positions. 1. 2. 3. 4. 5. occur (P, S, N) ← append(I , R, S ), append(L, P, I), length(L, N) append ([ ], Ys, Ys) ← append ([X|Xs], Ys, [X|Zs]) ← append (Xs, Ys, Zs) length([ ], 0). length([H|T ], N1) ← length(T, N), N1 is N +1 Recall that in Prolog findall (N, Goal, Ns) returns a list Ns which represents the multiset of all instances of the variable N such that Goal is satisfied. The variable N should appear within the clause only in the term Goal. By submitting the query: ← findall (N , occur ([a a b a a],[a a b a c a a b a a b a a], N ), Ns) we get the value [5, 8] for the variable Ns (see also Figure 9.1.4 on page 267). CHAPTER 10 Appendices 10.1. Simple Prolog Programs and Parsing Sentences in Prolog In this section we present a few simple Prolog programs for: (i) checking natural numbers, (ii) sorting strings, and (iii) parsing strings. Checking Natural Numbers. The following program tests whether or not a given term X is a natural number in the set {0, s(0), s(s(0)), . . .}. X is a natural number iff nat(X). The following two clauses define the unary predicate nat(X). 1. 2. Natural Numbers nat(0) ← nat(s(X)) ← nat(X) For instance, we have that the query ← nat(s(s(s(0)))) evaluates to true. Sorting Strings. The following program sorts a given list of natural numbers. Each natural number is an element of the set {0, 1, 2, . . .}. This program is a very slow program because it constructs all possible permutations of the elements of the given list and then it outputs the permutation which is ordered. Since there are n! distinct permutations of n distinct elements, this program has worst case time complexity of order O(n!). Recall that: (i) 2n = O(n!) and (ii) n! = O(nn ), while (iii) n! 6= O(2n ) and (iv) nn 6= O(n!). 1. 2. 3. 4. 5. 6. Permutation Sort sort(L1, L2) ← permutation(L1, L2), sorted (L2) permutation([ ], [ ]) ← permutation(L, [H|T ]) ←append(U,[H|V ],L), : place H in a position in W append(U, V, W ), : break W into U and V permutation(W, T ) : take a permutation W of T sorted (L) ← sorted 1(0, L) sorted 1(N, [ ]) ← sorted 1(N, [H|T ]) ← N ≤ H, sorted 1(H, T ) For instance, we have that the query ← sort([3, 2, 2, 4], L2) evaluates to true with the variable L2 bound to [2, 2, 3, 4]. 273 274 10. APPENDICES Now we present a different Prolog program for sorting a given list of natural numbers. This program is based on the Quicksort algorithm, and it is much faster than the previous program, because its worst case time complexity is of order O(n2 ), where n is the length of the list. Quicksort 1. 2. 3. 4. 5. partition(H, [A|X], [A|Y ], Z) ← A ≤ H, partition(H, X, Y, Z) partition(H, [A|X], Y, [A|Z]) ← A > H, partition(H, X, Y, Z) partition(H, [ ], [ ], [ ]) ← quicksort([ ], [ ]) ← quicksort([H|T ], S) ← partition(H, T, A, B), quicksort(A, A1), quicksort(B, B1), append (A1, [H|B1], S) Parsing Sentences. The following programs show how to parse sentences generated by context-free grammars. We follow the approach described in [5, Chapter 9]. Here we will not present the general parsing methodology and the reader may refer to [5, Chapter 9] for more information on this topic. Let us consider the context-free grammar G with axiom sentence and the following productions: sentence noun-phrase verb-phrase verb-phrase determiner noun verb → → → → → → → noun-phrase verb-phrase determiner noun verb noun-phrase verb the apple | man eats | sings Here is a simple Prolog program for parsing strings of the language generated by that grammar. Parsing Sentences. Program P 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 sentence(X) ← append(Y, Z, X), noun phrase(Y ), verb phrase(Z) ¯ ¯ noun phrase(X) ← append(Y, Z, X), determiner(Y ), noun(Z) verb ¯phrase(X) ← append(Y, Z, X), verb(Y ), noun phrase(Z) ¯ verb¯phrase(X) ← verb(X) ¯ determiner([the]) ← noun([apple]) ← noun([man]) ← verb([eats]) ← verb([sings]) ← 10.1. SIMPLE PROLOG PROGRAMS AND PARSING SENTENCES IN PROLOG 275 If we want to parse, for instance, the string ‘the man eats the apple’, we have to submit to the above Prolog program P 1 the following query: ← sentence([the, man, eats, the, apple]) By evaluating that query we get the empty clause, and the trace of that evaluation gives us the derivation tree of the given string ‘the man eats the apple’. Here is an alternative Prolog program for parsing strings of the language generated by the grammar G given above. It runs faster than Program P 1, because it avoids the use of append and uses, instead, the so called difference lists [26, page 283]. Parsing Sentences (Use of difference lists). Program P 2 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 sentence(S0, S) ← noun phrase(S0, S1), verb phrase(S1, S) ¯ ¯ noun phrase(S0, S) ← determiner(S0, S1), noun(S1, S) ¯ verb phrase(S0, S) ← verb(S0, S1), noun phrase(S1, S) ¯ verb¯phrase(S0, S) ← verb(S0, S) ¯ determiner([the|S], S) ← noun([apple|S], S) ← noun([man|S], S) ← verb([eats|S], S) ← verb([sings|S], S) ← For this program if we want to parse the string ‘the man eats the apple’, we have to submit the following query: ← sentence([the, man, eats, the, apple], [ ]) Now we present a third Prolog program for parsing strings of the language generated by the same context-free grammar G given above. This program uses the so called definite clause grammars (or DCG’s) which, indeed, were introduced for making it easier the specification and the solution of parsing problems [5, Section 9.1]. Rules 3.1–3.9 below encode the rules of the given context-free grammar. We write each rule using the arrow ‘-->’ to separate the left hand side from the right hand side, and we write a dot at the right end of each rule. Parsing Sentences (Use of definite clause grammars). Program P 3 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 sentence noun-phrase verb-phrase verb-phrase determiner noun noun verb verb --> --> --> --> --> --> --> --> --> noun-phrase, verb-phrase. determiner, noun. verb, noun-phrase. verb. [the]. [apple]. [man]. [eats]. [sings]. 276 10. APPENDICES If we want to parse the string ‘the man eats the apple’, we have to submit the following query: ← sentence([the, man, eats, the, apple], [ ]) If we also want to construct the parse tree of the sentence being parsed, we need the following program which uses a definite clause grammar with arguments: Parsing Sentences with Parse Trees (Use of definite clause grammars).ProgramP 4 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 sentence(st(NP , VP)) noun-phrase(np(D, N)) verb-phrase(vp(V, NP )) verb-phrase(vp(V )) determiner (d(the)) noun(n(apple)) noun(n(man)) verb(v(eats)) verb(v(sings)) --> --> --> --> --> --> --> --> --> noun-phrase(NP), verb-phrase(VP). determiner(D), noun(N). verb(V ), noun-phrase(VP). verb(V ). [the]. [apple]. [man]. [eats]. [sings]. If we want to parse the string ‘the man eats the apple’ and we want to construct the parse tree of that string, we have to submit the following query: ← sentence(X, [the, man, eats, the, apple], [ ]) and we get the following parse tree represented as a term bound to the variable X (see also Figure 10.1.1): X = st(np(d(the), n(man)), vp(v(eats), np(d(the), n(apple)))) st np vp d n the man v np eats d n the apple Figure 10.1.1. The parse tree of the string ‘the man eats the apple’.  10.2. DECIDABILITY RESULTS FOR LL(k ) AND LR(k ) GRAMMARS AND LANGUAGES 277 10.2. Decidability Results for LL(k ) and LR(k ) Grammars and Languages In this section we present some decidability and undecidability results about the LL(k) languages and the LR(k) languages (see also [10, Section 8.5]). Some other decidability and undecidability results concerning the context-free languages have been reported in [21]. Theorem 10.2.1. [Undecidability of Equivalence of Context-Free and LL(k ) Grammars] (i) It is undecidable whether or not given a context-free grammar G, there exist k ≥ 0 and an LL(k) grammar G1 such that L(G) = L(G1) (see [22, Theorem 13] and [3, page 361]). (ii) It is undecidable whether or not given a context-free grammar G and a number k ≥ 0, there exists an LL(k) grammar G1 such that L(G) = L(G1) ([22, Theorem 13] and [3, page 361]). Theorem 10.2.2. [Undecidability of Equivalence of Context-Free and LR(k ) Grammars] (i) It is undecidable whether or not given a context-free grammar G there exist k ≥ 0 and an LR(k) grammar G1 such that G and G1 generate the same language (see [12] and [3, pages 397–399, Exercise 5.2.12]). (ii) It is undecidable whether or not given a context-free grammar G and a number k ≥ 0, the grammar G equivalent to an LR(k) grammar [3, pages 397–399, Exercise 5.2.12]. The undecidability result of Point (i) of the above Theorem 10.2.2 can be derived as follows. We have that: (1) the class of the LR(0) languages is contained in the class of the LR(1) languages (which is the class of deterministic context-free languages), and (2) for any k > 0 the class of the LR(k) languages is equal to the class of the LR(1) languages. Thus, undecidability holds because, otherwise, it would have been decidable the problem of knowing whether or not a context-free grammar is equivalent to a deterministic context-free grammar. The undecidability result of Point (ii) for k > 0 can be derived as follows. Since for any k > 0, the class of the LR(k) languages is equal to the class of the LR(1) languages (which is the class of deterministic context-free languages), this problem is undecidable for k > 0 because otherwise, it would have been decidable the problem of testing whether or not a context-free grammar generates a deterministic context-free language. Theorem 10.2.3. [Undecidability of Existence of k for LL(k )-Testing and LR(k )-Testing of Context-Free Grammars] (i) It is undecidable whether or not given a context-free grammar G, there exists k ≥ 0 such that G is an LL(k) grammar (see [22, Theorem 11] and [25, page 399]). (ii) It is undecidable whether or not given a context-free grammar G, there exists k ≥ 0 such that G is an LR(k) grammar [25, page 399]. We have, however, the following decidability results. Theorem 10.2.4. [Decidability of Fixed-k LL(k )-Testing and Fixed-k LR(k )-Testing of Context-Free Grammars] (i) It is decidable whether or not given k ≥ 0 and a context-free grammar G, the grammar G is an LL(k) grammar. 278 10. APPENDICES (ii) It is decidable whether or not given k ≥ 0 and a context-free grammar G, the grammar G is an LR(k) grammar. The two decidable problems of this Theorem 10.2.4 are solvable in nondeterministic 1-exponential time [25, page 399] when k is given in binary, that is, they are solvable in O(2p(n)) time by a nondeterministic Turing Machine, where p(n) is a polynomial on the size of the input grammar G (that is, the sum of the number of the characters occurring in its productions) plus the size of k written in binary. Theorem 10.2.5. [Decidability of Existence of k for LL(k )-Testing of LR(k ) Grammars] It is decidable whether or not given h ≥ 0 and an LR(h) grammar G, there exists k ≥ 0 such that G is an LL(k) grammar [22, Theorem 12] and [4, page 690]. (In [3, page 397] it is incorrectly stated that the latter problem is undecidable.) The following Facts 10.2.6 and 10.2.7 follow from [23] because every LL(k) or LR(k) grammar generates a deterministic context-free language. Fact 10.2.6. [Decidability of Equivalence for LL(k ) Grammars] [3, page 362] For all k ≥ 0, given any two LL(k) grammars G1 and G2, it is decidable whether or not L(G1) = L(G2) (see also Fact 4.3.24 on page 78). Note that if G1 is an LL(k) grammar, for some k ≥ 0, and G2 is an LL(h) grammar, for some h ≥ 0, we can consider both grammars G1 and G2 to be LL(max(k, h)) grammars. Fact 10.2.7. [Decidability of Equivalence for LR(k ) Grammars] For any k ≥ 0, given any two LR(k) grammars G1 and G2, it is decidable whether or not L(G1) = L(G2) (see also Fact 5.8.15 on page 144). As in the case of LL(k) grammars, if G1 is an LR(k) grammar, for some k ≥ 0, and G2 is an LR(h) grammar, for some h ≥ 0, we can consider both grammars G1 and G2 to be LR(max(k, h)) grammars. List of Algorithms and Programs Chapter 2. Exploring Search Spaces Sum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Linear search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . State dependent linear search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dispositions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Combinations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . n-Queens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Depth first visit of trees: basic version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Depth first visit of trees: Burstall’s version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Breadth first visit of trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 18 20 24 25 26 29 32 33 Chapter 3. Chop-and-Expand Parsers for Context-Free Languages Chop-and-Expand Parser in a functional language SML-like . . . . . . . . . . . . . . . . Chop-and-Expand Parser in a Java: the class List.java . . . . . . . . . . . . . . . . . . . Chop-and-Expand Parser in a Java: CFParser.java . . . . . . . . . . . . . . . . . . . . . . . . Chop-and-Expand Parser in Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 39 42 50 Chapter 4. Parsers for Deterministic Context-Free Languages: LL(k) Parsers Construction of LL(1) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 Construction of LL(2) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 Chapter 5. Parsers for Deterministic Context-Free Languages: LR(k) Parsers Construction of RL(0) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 Construction of RL(0) parsing tables: avoiding the Powerset Construction . . 95 Construction of SLR(1) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Construction of RL(1) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 Construction of LARL(1) parsing tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123 Chapter 6. Parsers for Operator Grammars and Parser Generators Parsing using operator-precedence grammars . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Constructing left and right functions for operator-precedence grammars . . . Generation of parsers using Bison and Flex: parser for an LALR(1) grammar: grammar.y . . . . . . . . . . . . . . . . . . . . . . . . . . parser for a calculator: calculator.y . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . parser for a simple assignment language: fSAL.y . . . . . . . . . . . . . . . . . . . . . . . parser for a simple assignment language: fSAL.lex . . . . . . . . . . . . . . . . . . . . parser for a simple assignment language: fSAL.h . . . . . . . . . . . . . . . . . . . . . . . Generation of lexical analyzers using Flex alone: faSAL.lex . . . . . . . . . . . . . . . 146 148 150 155 159 161 163 165 Chapter 7. Visits of Trees and Graphs and Evaluation of Expressions Depth first visit of a tree: DepthFirstVisit.java . . . . . . . . . . . . . . . . . . . . . . . . 174 279 280 LIST OF ALGORITHMS AND PROGRAMS Depth first visit of a tree with graphical user interface: DepthFirstVisitGUI.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evaluator of a boolean expression: BooleanExpressionEvaluator.java . . . Evaluator of a boolean expression with user graphical interface: BooleanExpressionEvaluatorGUI.lex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Evaluator of a boolean expression: PropositionalTheoremProver.java . . . Evaluator of a boolean expression with user graphical interface: PropositionalTheoremProverGUI.lex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encoding n-ary trees into binary trees Binary trees: the class BinNode.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . n-ary trees: the class NaryNode.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . List of n-ary trees: the class List.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Encoding n-ary trees into binary trees: EncodingTrees.java . . . . . . . . . . . . . . Encoding n-ary trees into binary trees in a functional language: Rin . . . . . . . Decoding binary trees back into n-ary trees in a functional language: Sin . . Computation of a minimal spanning tree of an undirected graph: Dijkstra’s algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The class MST.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The class UGraph.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 184 189 195 201 207 210 212 215 221 221 227 231 233 Chapter 8. Path Problems in Directed Graphs Matrix multiplication Elementary algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Partition algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Strassen’s algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Computation of the transitive closure of boolean matrices . . . . . . . . . . . . . . . . . Computation of the single source shortest paths in directed graphs: Dijkstra’s algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The class SSSP.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . The class Graph.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 252 255 Chapter 9. String Matching Naive pattern matcher: KMP.java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Knuth-Morris-Pratt pattern matcher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Computation of the failure function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Computation of the occurrences of the pattern . . . . . . . . . . . . . . . . . . . . . . . . . Knuth-Morris-Pratt pattern matcher: KMP.java . . . . . . . . . . . . . . . . . . . . . . . . . . . String matching in Prolog: basic version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . String matching in Prolog: finding all occurrence positions . . . . . . . . . . . . . . . . 261 262 262 262 269 271 272 Chapter 10. Appendices Permutation sort in Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quicksort in Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Parsing of sentences in Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Use of difference lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Use of definite clause grammars (DCG’s) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Use of definite clause grammars and generating parse trees . . . . . . . . . . . . . 273 274 274 275 275 276 235 236 236 242 Index (S, +,  , 0, 1)-ring: ring with 0 and 1, 238 (S, +,  , 0, 1)-semiring of matrices: semiring of matrices with 0 and 1, 238 (S, +,  , 0, 1)-semiring: semiring with 0 and 1, 237 ε-move, 89, 92 ε-production, 15, 71, 76, 77 ∗ closure, 9 + closure, 9 k-bounded concatenation of languages, 64 k-predictive parsing algorithm, 52 n queens, 26 n-ary tree, 207, 209 n-ary trees in C++, 222 n-ary trees in Pascal, 222 B ′ : a domain of binary trees, 220 B : a domain of binary trees, 207 First1 for strings: First1 (α), 55 First1 for symbols: First1 (x), 55 Firstk for strings, 64 Follow1 for nonterminal symbols, 56, 97, 106 Followk for nonterminal symbols, 69 LALR(1) grammars, 124 LALR(1) languages, 124 LALR(1) parsers, 124 LL(0) languages, 76 LL(1) grammars, 53, 58 LL(1) languages, 58 LL(1) parsers, 53 LL(2) grammars, 70 LL(2) parsing: an example, 65 LL(k) grammars, 51, 58, 70 LL(k) grammars in Chomsky normal form, 76 LL(k) grammars in Greibach normal form, 71, 76 LL(k) languages, 52 LL(k) parsers, 51 LL(k) parsers (k ≥ 1), 51, 69 LL(k) parsers (k ≥ 1), 63 LL(0) languages, 52 LR(0) complete item, 83 LR(0) grammar: an example, 91 LR(0) grammars, 91 LR(0) item, 83 LR(0) languages, 91, 93, 277 LR(0) parsers, 83 LR(1) grammars, 107 LR(1) languages, 107, 277 LR(1) parsers, 102 LR(k) grammars, 81, 277 LR(k) grammars in Chomsky normal form, 117 LR(k) languages, 277 LR(k) parsers, 81, 82, 117 SLR(1) grammars, 98 SLR(1) languages, 97 SLR(1) parsers, 97 Tlist ′ : a domain of list of n-ary trees, 220 Tlist : a domain of lists of n-ary trees, 209 T ′ : a domain of n-ary trees, 220 T : a domain of n-ary trees, 209 append operation on lists, 29, 38, 261 cons operation on lists, 212, 215, 261 hd operation on lists (head function), 36 tl operation on lists (tail function), 36 Hin: decoding function, 214 Kin: encoding function, 214 absorbent element, 237 accept action for LR(0) parsing, 86, 88 accept action for LR(1) parsing, 104 adjacency matrix, 235, 241 algorithm for constructing the LALR(1) parsing table, 123 algorithm for constructing the LL(1) parsing table, 57 algorithm for constructing the LR(0) parsing table, 84 algorithm for constructing the LR(0) parsing table without applying the Powerset Construction Procedure, 94 algorithm for constructing the LR(1) parsing table, 102 281 282 algorithm for constructing the LR(1) parsing table without applying the Powerset Construction Procedure, 118 algorithm for constructing the SLR(1) parsing table, 97 algorithm for matching: Knuth-Morris-Pratt version, 262 algorithm for matching: naive version, 261 algorithm for matrix multiplication: elementary version, 235 algorithm for matrix multiplication: partition method, 236 algorithm for matrix multiplication: Strassen’s method, 236 algorithm: Minimal Spanning Tree (by Dijkstra), 227 algorithm: Single Source Shortest Path (by Dijkstra), 247 alphabet, 9 alphabet of a grammar, 10 ambiguous grammar, 150, 157 amortized time complexity analysis, 267 atom, 183, 194 attribute grammars, 152 augmented context-free grammars, 82 axiom of a grammar, 10, 151 backtracking algorithms, 21 backtracking for shortest path algorithms, 248 backward arc, 264 bf-existsev function, 34 big production, 85, 95 big productions with lookahead sets for LR(1) parsing, 102 binary tree, 207 binary trees in C++, 222 binary trees in Pascal, 221 Bison: parser generator, 150 blue node, 223 boolean expression, 183 boolean ic-semiring, 240 boolean multiplication and transitive closure, 242 bottom-up parser, 167, 252 breadth first visit, 33 Chomsky normal form (with ε-productions), 71 chop move, 53 chop operation, 32 chop-and-expand parser, 35, 39, 50 click, 223 closed semiring, 240 INDEX closure rules for LR(0) parsing, 94 closure rules for LR(1) parsing, 103, 118 Cocke-Younger-Kasami parser, 35 combinations, 25 comparison of matrix multiplication algorithms, 235, 237 compiler-compilers, 150 complete item for SLR(1) parsing, 97 complete item for LR(0) parsing, 83, 91 complete item for LR(1) parsing, 107 complexity of parsing LL(k) languages, 78, 134 concatenation of words, denoted  , 9 condition, 183, 194 conflicts in LR(0) parsing, 91 conflicts in LR(1) parsing, 105 conflicts in LALR(1) parsing, 124 context-free grammar, 11 context-free language, 11 context-free production, 11 context-sensitive grammar, 11 context-sensitive language, 11 context-sensitive production, 11 cover state, 89 decidability of equivalence for LL(k) grammars, 78, 278 decidability of equivalence for LR(k) grammars, 144, 278 decidability of fixed-k, LL(k) testing and LR(k) testing of context-free grammars, 277 decidability of reduction of LR(k) grammars to LL(k) grammars, 278 decoding function Hin, 214 definite clause grammar (DCG), 275 depth first visit (basic version), 29 depth first visit (Burstall’s version), 31 depth first visit of a tree, 173 derivation of a sentential form, 12 derivation of a word, 12 derivation step, 12 derivation tree, 13 descendant of a state, 36 deterministic context-free language, 92, 116 deterministic pushdown automaton: acceptance by final state, 92 deterministic pushdown automaton (or dpda), 92 deterministic time for constructing parsers, 136 deterministic time for testing grammars, 136 difference lists in Prolog, 275 dispositions, 24 INDEX distance between nodes, 223 domain equation for binary trees, 207 domain equations for n-ary trees, 209 domain equations for lists of n-ary trees, 209 domain B of binary trees, 207 domain B ′ of binary trees, 220 domain Tlist of lists of n-ary trees, 209 domain Tlist ′ of lists of n-ary trees, 220 domain T of n-ary trees, 209 domain T ′ of n-ary trees, 220 dpda (or deterministic pushdown automaton), 92 dynamic programming, 247 Earley parser, 35 empty n-ary tree, 209 empty binary tree, 207 empty list, 212 empty sequence ε, 9 empty string ε, 9, 15, 76 empty word ε, 9, 15, 76 encoding function Kin, 214 encoding of n-ary trees by binary trees, 207 epsilon production, 15, 76, 77 error recovery routine, 147 evaluator of a boolean expression, 183 exists function, 32 existsev function, 29, 31 existsev1 function, 32 expand function, 35, 37 expand move, 54 expand operation, 33 failure function, 262, 263 fast boolean matrix multiplication, 238 field, 238 flatmap function, 34 Flex: lexical analyzer generator, scanner generator, 150, 158 Floyd-Warshall’s algorithm, 247 folding, 15 formal grammar, 10 forward arc, 264 free monoid, 9 function inversion, 225, 226 fusion of states, 123 generic class List in Java, 39, 212 global optimum, 224 goto action for SLR(1) parsing, 97 goto action for LR(0) parsing, 87, 88 goto action for LR(1) parsing, 104 grammar, 10 graph: undirected, connected, weighted, 223 greedy algorithm, 224 283 Greibach normal form (with ε-productions), 71 head function hd, 36 head of the input tape, 52 hierarchy of LL(k) grammars, 76 hierarchy of LL(k) languages, 76 hierarchy of LR(k) grammars, 116 hierarchy of LR(k) languages: collapse, 116, 141 ic-semirings, 239, 240 idempotent complete semirings (ic-semirings), 240 identity element, 237 incomplete item for LR(1) parsing, 107 index of breadth ib, 21 index of depth id, 21 input head, 52 item for LR(0) parsing, 83 Kleene closure (∗ closure), 9 Kruskal’s algorithm, 229 language accepted by a parsing pushdown automaton, 92 language accepted by an algorithm for constructing parsing tables, 93 language accepted by LL(k) parsers, 52 language concatenation, 9, 64 language generated by a grammar, 10, 11 language generated by a nonterminal symbol of a grammar, 11 language or formal language, 9 left associative parsing, 148 left function ℓ, 148 left hand side of a production, 10 left linear regular grammar, 12 left recursive context-free grammar, 16, 58 left recursive context-free production, 16 left-values, 100 leftmost derivation, 12 length of a path in a graph, 235 length of a word, 9 length of an arc, 223, 247 lexical analyzer, 152 lexical analyzer generator: Flex, 150, 158 lexical analyzer without parser generator, 165 lhs of a production, 10 Linear Search program, 18 linear search spaces, 17 lists, 212 literal, 183, 194 local follow set, 64 local optimum, 224 284 location, 100 lookahead set for LR(1) parsing, 102 loop on a labeled directed graph, 240 matching algorithm: Knuth-Morris-Pratt version, 261, 262 matching algorithm: Knuth-Morris-Pratt version, Java implementation, 269 matching algorithm: naive version, 261 matching algorithm: naive version, Prolog implementation, 271 matching algorithms, 261 matrix multiplication, 235 minimal spanning tree, 223 Minimal Spanning Tree (MST) algorithm (by Dijkstra), 223, 248 MST algorithm, 223, 248 multiple action in an entry of the LR(1) parsing table, 105 multiple actions in an entry of the LALR(1) parsing table, 124 multiple actions in an entry of the LR(0) parsing table, 91 multiplicative inverse, 238 natural number checking in Prolog, 273 non-empty n-ary tree, 209 non-empty binary tree, 207 non-empty list, 212 non-negative ic-semiring, 240, 245 nondeterministic finite automata and regular expressions, 258 nonterminal alphabet of a grammar, 10 nonterminal symbols, 10 nullable symbol, 15 operator grammar, 145 operator-precedence grammars, 145 operator-precedence parsers, 145 parse tree, 13 parser generator: Bison, 150 parser generator: Yacc, 150 parsing sentences in Prolog, 274 parsing table for LL(1) parsers, 53, 54 parsing table for LALR(1) parsers, 123 parsing table for LR(0) parsers, 84 parsing table for LR(1) parsers, 102 parsing table for SLR(1) parsers, 97 path problems in directed graphs, 235, 239 pattern string, 261 Permutation Sort program, 273 plus closure (+ closure), 9 positive closure (+ closure), 9 postorder traversal of a tree, 173 INDEX postorder visit of a tree, 173 Powerset Construction Procedure, 85, 95, 103, 118 precedence between operators, 156, 157 precedence of an operator, 145 prefix of a string, 263 prefix of length k of a word w, 10 prefix property of a language, 92 prefix-free deterministic context-free language, 93 prefix-free language, 92 preorder traversal of a tree, 23 preorder visit of a tree, 21 Primal-Dual algorithm, 247 production, 10 production for a nonterminal symbol, 15 proper prefix of a string, 263 proper suffix of a string, 263 propositional formula, 194 propositional variables, 194 Quicksort program, 274 reachability problem, 241 recursive descent parsing technique, 41, 173, 183, 194 red node, 223 reduce action for SLR(1) parsing, 97 reduce action for LR(0) parsing, 87–89 reduce action for LR(1) parsing, 105 reduce-reduce conflict in LR(0) parsing, 91 reduced grammars, 15, 117 Regular Expression ic-semiring, 241, 258 regular expressions and finite automata, 258 regular grammar, 11 regular language, 11 regular production, 11 rewriting step, 12 rhs of a production, 10 rhs: right hand side function, 36 right associative parsing, 148 right function r, 148 right hand side function: rhs, 36 right hand side of a production, 10 right linear grammar, 11 right linear production, 11 right-values, 100 rightmost derivation, 13 ring, 237 ring with 0 and 1: (S, +,  , 0, 1)-ring, 238 scanner, 158 scanner generator: Flex, 158 Schönhage-Strassen method, 239 search spaces, 17 INDEX selector, 209, 212 semantic actions, 151, 161 semiring, 237 semiring of matrices with 0 and 1: (S, +,  , 0, 1)-semiring of matrices, 238 semiring with 0 and 1: (S, +,  , 0, 1)-semiring, 237 sentential form, 12, 32, 36 shift action for SLR(1) parsing, 97 shift action for LR(0) parsing, 86, 88 shift action for LR(1) parsing, 104 shift-reduce conflict in LR(0) parsing, 91, 100 shift-shift conflict in LR(0) parsing, 91 shortest path, 247 shortest path problem, 245 simple languages, 142 simple LR(1) (or SLR(1)) parsers, 97 Single Source Shortest Path (SSSP) algorithm (by Dijkstra), 247 Single Source Shortest Path problem, 247 Single Source Shortest Paths in directed graphs, 247 size of parsers, 136 sorting strings in Prolog, 273 space complexity for context-free parsing, 134 spanning tree, 223 SSSP algorithm, 247, 248 star closure (∗ closure), 9 start symbol of a grammar, 10 state dependent linear search, 20 State Dependent Linear Search program, 20 Strassen’s algorithm for matrix multiplication, 236 strict deterministic context-free grammars, 172 strict deterministic context-free languages, 172 string, 9 string-to-parse, 32 strong LL(0) grammars, 52 strong LL(1) grammars, 58 strong LL(2) grammars, 70 strong LL(k) grammars, 52, 70 strong LL(k) parsers, 71 structurally equivalent grammars, 138 subclasses of context-free languages, 138 subject string, 261 suffix of a string, 263 Sum program, 17 symbol, 9 tail function tl, 36 terminal alphabet of a grammar, 10 terminal symbols, 10 285 theorem prover for the propositional calculus, 194 time complexity for computing the shortest paths in directed graphs, 250 time complexity for context-free parsing, 134 time complexity for multiplying boolean matrices, 239 token, 156 tokenizer, 158 top-down parser, 167 transitive closure and boolean multiplication, 242 transitive closure in directed graphs, 241 transitive closure in ic-semirings, 245 translator, 152 tree-generating function, 29 type 0 grammar, 10 type 0 language, 11 type 0 production, 10 type 1 grammar, 11 type 1 language, 11 type 1 production, 11 type 2 grammar, 11 type 2 language, 11 type 2 production, 11 type 3 grammar, 11 type 3 language, 11 type 3 production, 11 ultraviolet arc, 226 undecidability of equivalence of context-free and LL(k) grammars, 277 undecidability of equivalence of context-free and LR(k) grammars, 277 undecidability of existence of k for LL(k) testing of context-free grammars, 277 undecidability of existence of k for LR(k) testing of context-free grammars, 277 undefined entry in the LALR(1) parsing table, 124 undefined entry in the LR(0) parsing table, 91 undefined entry in the LR(1) parsing table, 105 unfolding, 15 unsolvability results for context-free grammars, 137 useful symbol, 15 useless symbol, 15 variables, 10 variant records, 221 violet arc, 225 visiting trees, 29 286 weight of an arc, 223 word, 9 INDEX Yacc: parser generator, 150 yield of a derivation tree, 14 Bibliography [1] A. Aho, J. E. Hopcroft, and J. D. Ullman. Design and Analysis of Computer Algorithms. Addison-Wesley, 1974. [2] A. Aho, R. Sethi, and J. D. Ullman. Compilers: Principles, Techniques, and Tools. Addison-Wesley, 1986. [3] A. V. Aho and J. D. Ullman. The Theory of Parsing, Translation and Compiling, volume 1. Prentice Hall, 1972. [4] A. V. Aho and J. D. Ullman. The Theory of Parsing, Translation and Compiling, volume 2. Prentice Hall, 1973. [5] W. F. Clocksin and C. S. Mellish. Programming in Prolog. Springer-Verlag, New York, Second edition, 1984. [6] E. W. Dijkstra. A Short Introduction to the Art of Programming. Technical Report, EWD 316, 1971. [7] C. Donnelly and R. Stallman. Bison: The Yacc-compatible Parser Generator. Technical report, Free Software Foundation, 51 Franklin Street, Boston, MA 02110-1301, USA, 30 May 2006. ISBN 1-882114-44-2. http://www.gnu.org/software/bison/manual/index.html. [8] J. B. Fraleigh. A First Course in Abstract Algebra. Addison -Wesley, 1977. Second Edition. [9] M. A. Harrison. Introduction to Formal Language Theory. Addison Wesley, 1978. [10] J. E. Hopcroft and J. D. Ullman. Introduction to Automata Theory, Languages and Computation. Addison-Wesley, 1979. [11] S. C. Johnson. YACC: Yet another compiler-compiler. Unix Programmer’s Manual, volume 2b. AT&T, 1979. http://plan9.bell-labs.com/7thEdMan/. [12] D. E. Knuth. On the translation of languages from left to right. Information and Control, 8:607–639, 1965. [13] D. E. Knuth. Semantics of context-free languages. Mathematical Systems Theory, 2:127–145, 1968. Correction: Mathematical Systems Theory, 5:95–96, 1971. [14] D. E. Knuth, J. H. Morris, and V. R. Pratt. Fast pattern matching in strings. SIAM Journal on Computing, 6(2):323–350, 1977. [15] M. E. Lesk and E. Schmidt. LEX – Lexical Analyzer Generator. In A. G. Hume and M. D. McIlroy, editors, Unix Programmer’s Manual. Tenth Edition. Volume 2. AT&T Bell Laboratories, Murray Hill, NJ., 1990. [16] C. H. Papadimitriou and K. Steiglitz. Combinatorial Optimization: Algorithms and Complexity. Prentice-Hall, 1982. [17] L. C. Paulson. The foundation of a generic theorem prover. J. Automated Reasoning, 5:363–397, 1989. 287 288 BIBLIOGRAPHY [18] V. Paxson, W. Estes, and J. Millaway. Flex: The Fast Lexical Analyzer. Manual Edition 2.5.35. Technical report, The Flex Project, 10 September 2007. Available from: http://flex.sourceforge.net/. [19] A. Pettorossi. Programming in C ++. Aracne Editrice, 2001. ISBN 88-7999-323-7. [20] A. Pettorossi. Quaderni di Informatica. Parte I. Aracne, Second edition, 2004. [21] A. Pettorossi. Automata Theory and Formal Languages. Aracne Editrice, Fourth edition, 2013. [22] D. J. Rosenkrantz and R. E. Stearns. Properties of deterministic top-down parsing. Information and Control, 17:226–256, 1970. [23] G. Sénizergues. The equivalence problem for deterministic pushdown automata is decidable. In Proceedings ICALP ’97, Lecture Notes in Computer Science 1256, pages 671–681, 1997. [24] S. Sippu and E. Soisalon-Soininen. Theory of Parsing: Languages and Parsing, Volume 1. Springer-Verlag, 1988. [25] S. Sippu and E. Soisalon-Soininen. Theory of Parsing: LR(k) and LL(k) Parsing, Volume 2. Springer-Verlag, 1990. [26] L. S. Sterling and E. Shapiro. The Art of Prolog. The MIT Press, Cambridge, Massachusetts, 1994. Second Edition. [27] L. G. Valiant. General context-free recognition in less than cubic time. Journal of Computer and System Sciences, 10:308–315, 1975.