File: parser_work.md

package info (click to toggle)
puppet-agent 8.10.0-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 27,392 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (263 lines) | stat: -rw-r--r-- 12,053 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
Working on Parser Logic
===

This document contains advice related to doing grammar / parser work.

From Grammar to Ruby
---
The grammar is described in a `.ra` (racc) file. For the "future parser", this is in
lib/puppet/pops/parser/egrammar.ra and it is combined with the parser_support.rb file in the
same directory and processed by race. The output is the resulting parser (in eparser.rb).

Never modify the `parser.rb` by hand.

Merge conflicts
---
Simply touch the `egrammar.ra` (unless it was changed by resolving merge conflicts), and
then rebuild the parser by running make in the same directory.

The resulting `eparser.rb` should be checked in.

The eparser.rb and Racc runtime
---
If you look inside the `eparser.rb` file, you see several tables and a set of methods.
The tables are used by the racc runtime (written n C and part of Ruby), and it calls back to
the methods that implement the actions that were expressed in the grammar.

Note that the file contains source file/line references to the grammar file, thus ensuring
that runtime exceptions appear to come from the grammar file (as they should).

Grammar Ambiguities
---
If you are working with grammar changes, you may run into ambiguity problems. There are two kinds of conflicts:

* shift/reduce
* reduce/reduce

Bot of these conflicts mean that racc can not determine what to do when the sequence of source tokens have made it reach a particular state.

A "shift" can be read as "tell me more", and "reduce" as "got it". So a shift/reduce is an ambiguity
where the grammar expresses that it is both ok to accept the state as complete, or to continue and build something more elaborate. A reduce/reduce, is trickier, since this means we reached a state
where there appears to be no difference between completing one of multiple choices.

There are several reasons to why shift/reduce, and reduce/reduce conflicts occur:

* The language is truly ambiguous, i.e. there is no way to differentiate between two or more
  choices. This is poorly design language feature and it can only be fixed completely by changing
  the language. It is however possible to make such a problem less of an issue by hardcoding
  a decision and thus blocking one interpretation of the input from occurring. When doing so, the
  trick is to make this happen in a very dark corner of the programming language; i.e. in
  a sequence that is of little practical use. In all cases, avoid having ambiguities in the
  language.

* Racc only performs one token look-ahead over rule boundaries, any lookahead beyond that must
  take place in one and the same rule!
  To resolve these, you can introduce additional states, you can roll up rules into a larger rule, or
  you can assign precedence to rules.
  * "flattening" the grammar means  that you spell out a sequence of tokens even if it would
    be less repetition to refer to a rule. Remember, breaking up sequences into rules is not
    just syntactic, it changes how the parser works.
  * adding states, means that you break up sequences into pieces that are unambiguous; thus making
    it possible for the grammar to reduce them. Unfortunately this makes the grammar quite abstract
    and hard to read.
  * Assigning precedence to rules can solve a shift/reduce. The decision with the highest precedence
    will win. When doing this for rules, great care must be taken as it may mean that certain rules
    can never be triggered.
  
* The language is an expression language and racc can not on its own determine the priority
  of operators - e.g. in 1 + 2 * 3, should the addition or the multiplication be performed first?
  Issues of this kind are easy to solve by giving operators a precedence.

As a rule of thumb, do not try to implement all semantics of the language in the grammar. It
is far better to make the grammar parse non sensical input and then validate the result than
trying to capture all semantics via grammar rules. This makes the grammar simpler and there
are far fewer grammar conflicts to deal with.

### How Racc signals Ambiguities

When the grammar (.ra file) is processed racc outputs information about unused/useless rules
and the number of shift/reduce, and reduce/reduce conflicts. It will still produce a parser, so
you must pay attention to this output. If you see conflicts it means that certain parts of
the grammar may be unreachable (racc has built in defaults that **may** be what you want, but
it is most often by accident).

When an ambiguity is reported. You need to generate a more detailed report. You do that by running
the makefile target `egrammar.output`. This produces a file with the same name. At the top of this
file, you will find a more detailed report of which states/rules that are involved in the ambiguity.

It may for instance say:

     state 168 contains one shift/reduce conflict
     
To find what this means, you search for "state 168", it is probably mentioned in several places
with a "goto state 168", search until you find the state itself. There you find a description
of that exact state; how it got there, and what racc considers at that point.

Here is a simple example:

    state 66

       7) syntactic_statements : syntactic_statements syntactic_statement _
       9) syntactic_statement : syntactic_statement _ COMMA expression

      COMMA         shift, and go to state 68
      $default      reduce using rule 7 (syntactic_statements)

The current state is shown with an `_`, thus we are looking at the state where the parser
has seen a `syntactic_statement`. We see below, that if it sees a COMMA, it will shift to
state 68 (to deal with the expression), and if not, it will reduce rule 7 (it will add one
syntactic_statement to the list of syntactic_statements).

If there is a conflict of a token/rule, it will be listed multiple times in the decision table.
Say if there was a conflict on the COMMA, it may be shown as:

      COMMA         shift, and go to state 68
      COMMA         reduce using rule 666 (the_trouble_rule)
      $default      reduce using rule 7 (syntactic_statements)


### How to find where the problem is

Each conflicting token/rule-pair is displayed in the output (as shown above), thus if the
same COMMA is involved in 3 conflicts, you will see 6 entries. The valuable piece of information is the name of the reduction rule in conflict. At this point, try to manually construct the sequence of input tokens that would lead up to the ambiguity. 
It may be that the problem in the grammar is "before" reaching the ambiguity
on the COMMA. Once you understand the sequence, you need to apply reasoning to find the resolution
of the problem.

If that proves to be hard, and your grammar produces a viable parser, you can build a
debugging parser, and turn on debugging output in the runtime. This gives you a trace of
what the parser decides when it parses the input. This is sometimes easier than manually
following the state changes using only the .output file. Often, you need both, because the trace
only tells you which of the alternatives that it took, not what the alternatives were.

And yes, this is extremely tedious and time consuming. You will most certainly want to run this
on as little source input as possible to avoid having your head explode.

### Generating a Debugging Parser

To generate a debugging parser, run the make target `egrammar.debug`. This creates an
eparser.rb (it overwrites the non-debugging variant). (**Do not check in this parser**, it is
much slower than the non debugging variant).

### Turning on Debug output

To turn on debug output, you need to set an instance variable. You do this in `parser_support.rb`
in the `_parse()` method. Simply change the line that by default reads:

    @yydebug = false
    
to

    @yydebug = true
    
Again, **Do not check in this change**.

Note that the @yydebug=true does nothing unless the parser is build for debugging - i.e. you
do not have to change it while you are switching from regular to non debugging version.

### Running with debugging on

When you run with debugging turned on, the trace will be printed to stdout, and each
decision; reading a token, shifting to another rule/state, and reduction of rules
is printed out.

Armed with that output and the .output file, you can now manually step through the grammar.

### Limiting the scope

Sometimes it is just impossible to figure out what is going wrong in a complex grammar.
You can try reducing the grammar by simply commenting out large sections of the grammar. Repeat this
for as long as the problem occurs. When you removed the problem, revert that change, then continue elsewhere until you have the smallest possible reproducer

Fixing Problems
---

### Precedence

Precedence is expressed in a table at the beginning of the grammar. It lists the precedence
from high (at the top) to low (at the bottom), and for each token (real or pseudo token)
the associativity (`left`, `right` `nonasoc`) is expressed before a token (or list of tokens).

e.g.

    prechigh
      left  HIGH
      nonassoc UMINUS
      left  TIMES DIV MODULO
      left  MINUS PLUS
      right EQUALS
      left  LOW
    preclow

The associativity tells racc how to group input with the same precedence; i.e. should 1 + 2 + 3 be treated as (1 + 2) + 3, or 1 + (2 + 3). A nonassoc means that racc does not allow this multiple
times in a row, e.g. an unary minus can not occur in a sequence and --1 is an error.

The example above shows two pseudo tokens HIGH and LOW that can be used in the grammar to
make a rule have a certain precedence. 

We can now express an otherwise ambiguous grammar like this:

    expr
      : expr PLUS  expr
      | expr MINUS expr
      | expr TIMES expr
      | expr DIV   expr
      |      MINUS expr =UMINUS
      
### "Decent Precedence"

Optionally, we can deal with precedence by grouping the expressions having the same precedence

    expr
      : mulexp             # to higher precedence
      | expr PLUS mulexp
      | expr MINUS mulexp
      
    mulexp
      : primary            # to higher precedence
      | mulexp TIMES primary
      | mulexp DIV primary

    primary
      : NUMBER
      
This has the same effect as setting the precedence and associativity in the precedence
table. 

I named this "Decent Precedence" since this mimics the behavior of a "recursive decent parser",
the type of parser that is usually written by hand.

### Assigning the precedence

The precedence of a rule can be assigned like this:

    |  MINUS expression  =UMINUS

This means that the lexer delivers a MINUS token, and when that is followed by an
expression, the result is an UMINUS operation. If we did not assign =UMINUS, the rule
would be given the precedence of MINUS.

Other options for fixing problems
---
    
### Creating look ahead / look behind in the lexer

Sometimes it is possible to solve an issue by doing a bit more work in the lexer. As an example,
the puppet grammar has LBRACK and LISTSTART tokens that are issued for the input '['. The lexer
can differentiate between the tokens - a LISTSTART occurs if at the beginning of the input, or after whitespace. This helps making input such as $a[1] and $a [1] non ambiguous before fed to the grammar
(where it is impossible to differentiate between them due to whitespace tokens not being part of
the information sent to the parser). (This is actually an example of "look behind").

Beware that any lookahead in the lexer is very expensive since it visits each and every
character in the source file. Look-behinds are cheap in comparison.
  
  
Literature
===

There is almost no documentation for racc. Luckily, it is a Ruby port of Yacc, and almost everything that is described for Yacc also applies to Racc (with the major exception that Racc uses rules
written in Ruby, and that the runtime methods are slightly different).

The best book on the topic is "O'Reilly Yacc & Lex". If you want to learn more about parsers, see "Compilers, Principles, Techniques and Tools" (Aho et.al), also known as 'the dragon book').