Project 3

Deadline Tuesday, November 21, 11:59pm
Name on Marmoset P3
To Submit Standard Format: wlp4parse.cc OR wlp4parse.rkt
Prescanned Format: wlp4parse-prescanned.cc OR wlp4parse-prescanned.rkt
Marking Scheme 50 marks for release tests, 50 marks for secret tests

WLP4 Parser

In this project, you will write a parser for the WLP4 programming language. The goal is to take the output of the WLP4 scanner (Project 1B), which is a sequence of tokens, and use the LR(1) parsing algorithm to produce a parse tree for a WLP4 program.

WLP4 Grammar and SLR(1) DFA

To facilitate implementing the algorithm, we provide some files containing components representing an augmented version of the WLP4 context-free grammar, and the SLR(1) DFA corresponding to this grammar.

The CFG is represented by a CFG component, and the SLR(1) DFA is represented by a TRANSITIONS component followed by a REDUCTIONS component.

For C++ Users: wlp4data.h and wlp4data.cc

The files wlp4data.h and wlp4data.cc together provide the following five string constants:

For Racket Users: wlp4data.rkt

The file wlp4data.rkt provides the following five string constants:

Input and Output Requirements

As in Project 2, there are two input formats.

The parsing algorithm should be performed using scanned input, that is, a sequence of tokens representing the input program.
Producing The Output (Successful Parse)

You should use the LR(1) parsing algorithm to parse the tokens and build a tree representation of the program.

Finally, so Marmoset can verify that your output is correct, you need to print out a representation of the parse tree. You should do this using the following recursive algorithm (a simpler version of the printing algorithm from Question 10):
  1. Print a line representing the root node:
    • If the root node of the tree being printed is a nonterminal, print the grammar rule used to expand the nonterminal (the left-hand side, followed by a space, followed by each symbol on the right-hand side with a space between each one).
      Special Case: If the right-hand side of the rule is empty, print the left-hand side, a space, and then the special string .EMPTY.
    • If the root node of the tree being printed is a token, print the kind, followed by a space, followed by the lexeme.
  2. For each child of the root node, recursively print the child subtree.
Handling Errors (Unsuccessful Parse)

There is one error case that can occur in the LR(1) parsing algorithm. It occurs when you attempt to shift a terminal token, but there is no transition in the SLR(1) DFA corresponding to the terminal. If this occurs, print an error message containing ERROR to standard error and exit normally (without crashing or leaking memory). It does not matter if you produce partial output before printing ERROR.

There are no other error cases in the LR(1) algorithm, and no other error checking is required for this project. The structure of the DFA means that errors cannot occur during a reduce step, for example. If something unexpected happens in another part of the algorithm, it is likely a mistake in your code, or maybe the test input you're using is somehow incorrectly formatted.

Reference Implementation

If using Prescanned Format input, your program's functionality should be equivalent to the wlp4parse course tool.

If using Standard Format input, your program's functionality should be equivalent to running wlp4scan on the input and passing the result into wlp4parse.

Using the Reference Implementations

The following command lets you view the expected output for a WLP4 program provided in Standard Format.

wlp4scan < program.wlp4 | wlp4parse
The following command lets you view the expected output for a WLP4 program provided in Prescanned Format.
wlp4parse < program.prescanned

If you are using the web tools, the WLP4 web tools page has drop-down boxes that let you select whether the input is Standard Format or Prescanned Format, and set the output to wlp4parse output.

You can use cs241.treeprint with the command line option --wlp4 to format the output in the style of Question 10, which may make it easier to understand the structure of the output. This command line option tells cs241.treeprint to use the WLP4 grammar to process the derivation, and therefore you do not need to include explicit CFG and DERIVATION components in the input, and can just directly provide cs241.treeprint with the output from wlp4parse.

The following command lets you view the parse tree (using the Q10 output format) for a WLP4 program provided in Standard Format.

wlp4scan < program.wlp4 | wlp4parse | cs241.treeprint --wlp4
There is also a checkbox on the web version of cs241.treeprint which has the same effect as this command line option.

Example

Consider the following WLP4 program:

int wain(int a, int b) {
  return 241;
}

We will trace through part of the parsing algorithm for this program to illustrate how your program should process the input. (Even for a program this small, a complete trace would be extremely long.)

Partial Trace of the Parsing Algorithm

Scanning this program produces the following sequence of tokens:

INT int
WAIN wain
LPAREN (
INT int
ID a
COMMA ,
INT int
ID b
RPAREN )
LBRACE {
RETURN return
NUM 241
SEMI ;
RBRACE }

To parse this input, we first augment it with "beginning-of-file" and "end-of-file" tokens:

BOF BOF
INT int
WAIN wain
LPAREN (
INT int
ID a
COMMA ,
INT int
ID b
RPAREN )
LBRACE {
RETURN return
NUM 241
SEMI ;
RBRACE }
EOF EOF

We follow the tree-building version of the LR(1) algorithm, using the SLR(1) DFA for WLP4, which is encoded in the "wlp4data" files linked near at the top of the page.

We begin parsing with an empty tree stack, and a state stack containing state 0, the initial state of the WLP4 DFA.

Next TokenState StackTree StackNext ActionExplanation
BOF BOF 0 Shift BOF / 45

The .REDUCTIONS component of the SLR(1) DFA contains no entries for state 0, so we shift.

The .TRANSITIONS component of the SLR(1) DFA contains the line 0 BOF 45, corresponding to a transition from state 0 to state 45 on BOF.

INT int 0 45
BOF BOF
Shift INT / 68

The .REDUCTIONS component contains no entries for state 45.

The .TRANSITIONS component contains 45 INT 68.

WAIN wain 0 45 68
BOF BOF · INT int
Shift WAIN / 128

The .REDUCTIONS component contains no entries for state 68.

The .TRANSITIONS component contains 68 INT 128.

LPAREN ( 0 45 68 128
BOF BOF · INT int · WAIN wain
Shift LPAREN / 84

The .REDUCTIONS component contains no entries for state 128.

The .TRANSITIONS component contains 128 LPAREN 84.

INT int 0 45 68 128 84
BOF BOF · INT int · WAIN wain · LPAREN (
Shift INT / 17

The .REDUCTIONS component contains no entries for state 84.

The .TRANSITIONS component contains 84 INT 17.

ID a 0 45 68 128 84 17
BOF BOF · INT int · WAIN wain · LPAREN ( · INT int
Reduce by type INT

The .REDUCTIONS component contains 17 9 ID, which means that in state 17, rule number 9 (which is type INT) has ID in the lookahead tag set. Therefore, we reduce by type INT.

Pop INT from the tree stack and 17 from the state stack. The top of the state stack is now 84.

Create a type tree with child INT and push it to the tree stack. Since the top of the state is 84, and the .TRANSITIONS component contains 84 type 48, push 48 to the state stack.

Since we reduced instead of shifting, we do not consume a token from input.

ID a 0 45 68 128 84 48
                                           type
BOF BOF · INT int · WAIN wain · LPAREN ( · ╰─INT int
Shift ID / 101

The .REDUCTIONS component contains no entries for state 48.

The .TRANSITIONS component contains 48 ID 101.

COMMA , 0 45 68 128 84 48 101
                                           type
BOF BOF · INT int · WAIN wain · LPAREN ( · ╰─INT int · ID a
Reduce by dcl type ID

The .REDUCTIONS component contains 101 14 COMMA, so we should reduce by rule number 14, which is dcl type ID.

Pop the type and ID trees from the tree stack, and 101 and 48 from the state stack. The top of the state stack is now 84.

Create a dcl tree with children type and ID and push it to the tree stack. The .TRANSITIONS component contains 84 dcl 75, so push 75 to the state stack.

Since we reduced instead of shifting, we do not consume a token from input.

COMMA , 0 45 68 128 84 75
                                           dcl
                                           ├─type
                                           │ ╰─INT int
BOF BOF · INT int · WAIN wain · LPAREN ( · ╰─ID a
...

And so on...

When the parsing algorithm terminates, you will have three trees on your stack (the BOF tree, the tree for the non-augmented input, and the EOF tree) and you can manually perform the final reduction to get the full tree.

Stepping Stones

Step 1: Setup

There are a lot of things that your program will need to represent and work with.

Here are suggestions for what data structures to use. You might want to try to come up with your own ideas first, and then compare with these suggestions to see how they differ.

Recommended Data Structures

Once all your structs and classes are set up, populate them with the information from the starter files and standard input:

With all of this set up, you can dive right into implementing the LR(1) parsing algorithm if you want. The next stepping stones just describe a more structured approach.
Step 2: Reducing

This is the trickiest part, so let's implement it first. Write a function called reduceTrees that takes a CFG rule as a parameter. The function should reduce the current tree stack by the right hand side of the rule.

In C++, your function should modify the tree stack as a side effect. (This might entail passing the tree stack by reference as a parameter, unless your tree stack is a global variable.)

In Racket, you can decide whether to functionally update the tree stack (i.e., return a new tree stack) or use mutation, depending on what programming style you are more comfortable with.

  1. Create a new tree node storing the CFG rule.
  2. Let len be the length of the right-hand side of the CFG rule.
  3. Copy the last len trees from the tree stack into the new node's children. Make sure to copy them in the right order (the tree corresponding to the leftmost symbol of the CFG rule's right-hand-side should be the first child).
  4. Pop the last len trees from the tree stack.
  5. Push the new node to the tree stack.

Now, write a similar function called reduceStates, which takes a CFG rule as a parameter, but reduces the state stack instead of the tree stack.

  1. Let len be the length of the right-hand side of the CFG rule.
  2. Pop the last len states from the state stack.
  3. Look at the current top of the state stack, and the left-hand side of the CFG rule. Look up the transition in the SLR(1) DFA corresponding to this state and symbol. Push the resulting next state from this transition to the top of the state stack.
It's possible to combine these functions into a single "reduce" function that modifies both stacks, but splitting them up will be useful in later stepping stones.
Step 3: Shifting

This is a lot simpler than reducing. Write a function called shift. Calling it should update each of the following as a side effect:

Specifically, the function should do the following

  1. Create a new tree node corresponding to the first token of unread input, and push this node to the tree stack.
  2. Look at the current top of the state stack. Check if there is a DFA transition from this state, with the symbol being the kind of the first token of unread input.
    • If there is no such transition, print an error message containing ERROR to standard error and stop the program.
    • If there is a transition to a new state, push that new state to the state stack.
  3. Consume the first token from unread input.
Step 4: Testing

You are ready to implement the main algorithm now, but debugging the main algorithm is difficult because even a small WLP4 program has a fairly complex derivation and parse tree. It might help to first test the reduceTrees function in a simpler context to make sure it is implemented correctly.

The reduceTrees function is a little difficult to test because it requires creating a sequence of trees to test it. Since the reduceTrees function doesn't do error checking though, it doesn't really matter what trees you test it with, i.e., your test input doesn't have to correspond to a valid WLP4 program (or portion of a program). Your tests should focus on making sure that the function pops the correct number of trees and connects up the children correctly.

First, write a helper function tokensToTrees that converts a sequence of tokens to a stack of trees. It should consume the tokens from the input sequence in a loop, and for each token, it should create a single-node tree corresponding to that token and add it to the stack.

Then, create a main function that reads the input sequence of tokens, calls tokensToTrees to convert it to a stack of trees, then calls reduceTrees with some CFG rule. The number of trees in the tree stack should be at least as large as the right-hand side of the CFG rule (otherwise, you will pop too many things and crash).

Print the contents of the tree stack before and after calling reduceTrees and check that the changes make sense. Solving Question 10 might be helpful because you can use your solution to print out more easily readable representations of the tree.

Here is an example. The program is provided with the input "a b c d", which is converted to tokens and then trees. Then, the program reduces the tree stack by rule 0, which is start BOF procedures EOF. There are three things on the right hand side, so the program should pop three items from the tree stack, then push a new tree with these three items as its children, in the correct order.

Before Reduce:
[Element 0]
ID a
[Element 1]
ID b
[Element 2]
ID c
[Element 3]
ID d
After Reduce:
[Element 0]
ID a
[Element 1]
start BOF procedures EOF
├─ID b
├─ID c
╰─ID d
We see that the behaviour makes sense. Before the reduce, there are four single-node trees on the stack. After, there are two trees. The first tree, for the token "a", is unmodified because there were four things on the stack and only three elements on the RHS of the rule. The second tree has three children, corresponding to the three trees on top of the stack, in the correct order (b c d). The root of the tree is the rule that we reduced by.

Note that the above sample output assumes the input has not been augmented at this point (BOF and EOF are not added to the tree stack); if the input was augmented then the children would be ID c, ID d and EOF.

Here is another example. We use the same input and setup, but reduce by rule 11, which is dcls .EMPTY.

Before Reduce:
[Element 0]
ID a
[Element 1]
ID b
[Element 2]
ID c
[Element 3]
ID d
After Reduce:
[Element 0]
ID a
[Element 1]
ID b
[Element 2]
ID c
[Element 3]
ID d
[Element 4]
dcls .EMPTY
The behaviour once again makes sense. The right hand side of the rule is empty, so we pop nothing. We then push a new tree, where the root contains the rule, and the tree has no children (because the RHS is empty).

The reduceStates and shift functions might be easier to test in the context of an actual parse, since they rely on taking transitions that actually exist in the SLR(1) DFA.

Step 5: Parsing

Make sure you've done the necessary setup: the input should be augmented with BOF and EOF, the state stack should contain the initial state 0, and the tree stack should be empty.

If your shift, reduceTrees, and reduceStates functions are all working, the main parsing loop is not that complicated. The difficult part will probably be debugging your code if you made a mistake somewhere. Good luck!

  1. Look up the current top of the state stack, and the kind of the first token in unread input, in the SLR(1) DFA reductions. If a rule is found (meaning you are in a reduce state), call reduceTrees and reduceStates to reduce by the rule.
  2. Repeat the previous step until you are no longer in a reduce state.
  3. Call shift, which will consume a token from input and update the tree stack and state stack. Note that this may also result in an error if there's no transition on the next token.
  4. If all input is consumed (EOF has been shifted) and no error was produced, the parse was successful and the main parsing loop ends. Otherwise, repeat from Step 1.

After the main parsing loop, you should have three trees in your tree stack, corresponding to BOF, the parse tree for the non-augmented input, and EOF. Call reduceTrees one last time with rule 0 (start BOF procedures EOF) to obtain the final tree.

Print out the tree as described higher up on this assignment page (under "Producing The Output") and you have a (hopefully) working parser! Yay!