HW 6: PicPlot Part 2: Parsing¶
Finish and submit HW 5 before you open this assignment's repo
The HW 6 starter repo contains PiqEvaluation.hs, a complete, working solution to HW 5's compiler. You need it there so your parser has something to feed into. But that also means you must not open, copy, or clone the piq-parser repo until you (and your partner, if you have one) have completed and submitted HW 5.
When you submit HW 6, Gradescope will ask you to check a box confirming you didn't look at HW 6's repo until after you had submitted HW 5. If you can't honestly check that box, email your instructor before you submit. Don't guess.
You're welcome to read this page before then. Nothing on it gives away HW 5.
Last week you wrote a compiler that turns piq programs, written as Haskell abstract syntax, into Python programs for a pen plotter. This week you write the parser: the part that reads piq source code and recovers its structure as abstract syntax. Once your parser works, piq becomes a real language you can write in .piq files and turn into drawings with one command. This is the full-sized version of what you started in Lab 06, built with the parser combinators from Module 6.1 and Module 6.2. Keep the Parser Combinator Reference handy. The ParserBase Reference covers the lower-level pieces, though you won't need them.
How you'll get materials and submit this assignment
Only after you've submitted HW 5 (see the box above): getting the starter code and turning in your work follows the same process as every homework this semester. See The Homework Workflow for the full walkthrough. In short: from the piq-parser template repo, make your own private copy, clone it to the course server, and submit your work on Gradescope. The rest of this page covers everything the assignment asks of you.
As in HW 5, you'll also need Python 3 with matplotlib for preview pictures. It's already set up on the course server.
Here's the kind of source code your parser will read:
define box(size, gap) {
square size
move right size + gap
}
for i from 1 to 5 {
box(i * 8, 10)
}
and the abstract syntax it must produce, the same AST you compiled in HW 5:
Program [ProcDef "box" ["size","gap"] [Square (Var "size"), Move Rt (Add (Var "size") (Var "gap"))]]
[For "i" (Num 1.0) (Num 5.0) [Call "box" [Mul (Var "i") (Num 8.0), Num 10.0]]]
programs/example.piq (source code)
|
| YOUR WORK: PiqParser.hs
v
AST
|
| PROVIDED: a complete compiler (HW 5's assignment)
v
Python ──> preview picture ──> the real pen plotter (in the lab)
As in HW 5, the work is split into many small steps, and every step ends with a Check your work box saying exactly what to type and what you should see. Don't move on until the check passes.
What You'll Do¶
- Read a grammar and turn each rule into a parser.
- Use parser combinators to build big parsers out of small ones.
- Handle reserved words, and see why a keyword must be matched as a whole word.
- Implement operator precedence (
*before+) and left associativity (10 - 3 - 2means(10 - 3) - 2). - Parse statements, blocks, loops, procedure calls and definitions, and whole programs.
- Check your parser with round trips: printing an AST as source code and parsing it back gives the same AST.
- Compile real
.piqfiles into drawings.
Checklist¶
- Submit HW 5 first (see the box at the top of this page)
- Get the assignment materials (see The Homework Workflow)
- Part 1: names and keywords
- Part 2: atoms
- Part 3: precedence and associativity
- Part 4: basic statements
- Part 5: statement sequences and blocks
- Part 6: loops
- Part 7: procedure calls
- Part 8: procedure definitions
- Part 9: whole programs
- Part 10: the whole pipeline, and your own drawing
- Turn in the assignment (see Turning It In)
Collaboration¶
You can work with a partner on this assignment. If you do, you must cooperate on every part, follow the pair-programming rules from the syllabus at all times (even while writing prose or math), and share roles equally.
Getting Started¶
Everything in this handout is run from the top folder of your repository. Read The piq Language below (it's also in your repo as SYNTAX.md). It's the exact description of what your parser must accept.
The Files¶
| File | What it is |
|---|---|
PiqParser.hs |
YOU WILL EDIT. The parser. Every place to fill in is marked -- TODO (Part N). |
SYNTAX.md |
PROVIDED. The grammar of piq, the same as The piq Language below. It also shows which parser handles which rule. |
ParserCombinators.hs, ParserBase.hs |
PROVIDED: DO NOT EDIT. The course's parser-combinator library. You do not need to understand how these files are implemented. You only use what ParserCombinators exports (see the cheat sheet). |
PiqAST.hs |
PROVIDED: DO NOT EDIT. The abstract syntax, the same as HW 5. |
PiqEvaluation.hs |
PROVIDED: DO NOT EDIT. A complete compiler: a HW 5 solution (see below). |
PiqPretty.hs |
PROVIDED: DO NOT EDIT. The pretty-printer: abstract syntax back into source code. Used for round trips. |
Python.hs, Tools.hs, piq2py.hs, py2png.hs, piq2png.hs, python/ |
PROVIDED: DO NOT EDIT. The Python side and the command-line tools. |
Makefile |
PROVIDED. make (build the tools), make test, make part1 … make part10, make clean. |
test/*.hs |
TESTS. One module per group of parts (NameSpec, ExprParserSpec, BasicStmtParserSpec, BlockParserSpec, ProcParserSpec, RoundTripSpec, PipelineSpec). |
Examples.hs |
EXAMPLES. ghci Examples.hs loads everything. It also contains HW 5's example programs, the abstract syntax of the programs in programs/, and some GHCI helpers. |
programs/*.piq |
EXAMPLES. piq programs in source form. |
The piq Language¶
This is what piq source code looks like. It is exactly what your parser (PiqParser.hs) must read, and what the
provided pretty-printer (PiqPretty.hs) writes.
Sizes and distances are in millimetres. up is toward the top of the page.
Example¶
define box(size, gap) {
square size
move right size + gap
}
x = 20
for i from 1 to 5 {
box(i * 8, 10)
}
circle x / 2
line down 30
dot
Grammar¶
program ::= procdef* stmt* -- all definitions come first
procdef ::= "define" name "(" names? ")" block
names ::= name ("," name)*
block ::= "{" stmt* "}"
stmt ::= name "=" expr -- assignment
| "dot"
| "square" expr -- side length
| "rectangle" expr expr -- width, then height
| "circle" expr -- radius
| "move" direction expr -- travel without drawing
| "line" direction expr -- travel while drawing
| "for" name "from" expr "to" expr block
| name "(" exprs? ")" -- procedure call
exprs ::= expr ("," expr)*
direction ::= "left" | "right" | "up" | "down"
expr ::= term (("+" | "-") term)* -- left associative
term ::= factor (("*" | "/") factor)* -- left associative, binds tighter
factor ::= number | name | "(" expr ")"
Which Parser Handles Which Rule¶
| Grammar rule | Parser in PiqParser.hs |
Part |
|---|---|---|
| names and reserved words | name, keyword |
1 |
factor |
factor |
2 |
expr, term, operators |
expr, term, addOp, mulOp (with combineLeft) |
3 |
direction, and each stmt alternative except for and calls |
direction, dotStmt, squareStmt, rectangleStmt, circleStmt, moveStmt, lineStmt, assignment |
4 |
stmt*, block |
stmts, block |
5 |
"for" … |
forLoop |
6 |
name "(" exprs? ")" |
call |
7 |
procdef |
procDef |
8 |
program |
program |
9 |
Tokens¶
- Whitespace: spaces, tabs and newlines are all the same, and only separate tokens. There are no semicolons, and a whole program could be written on one line.
- Names (variables, procedures, parameters): a letter followed by letters and digits. This is the course
library's
identifier, so there are no underscores:starGapis fine,star_gapis not. - Reserved words: these cannot be names.
dot square rectangle circle move line left right up down for from to define - Numbers: the course library's
double:20,2.5,-3,1.0e-2. A leading-is part of the number. There is no minus operator for anything other than a number literal; write0 - xto negate a variable. - Case: keywords are lowercase.
Rules Worth Knowing¶
- Distances are always required:
move right 10, nevermove right. rectangle W Hwith a negative H needs parentheses:rectangle 30 (-10). Without them,rectangle 30 -10reads asrectangle (30 - 10)followed by a missing height.- Calls always have parentheses, even with no arguments:
star(). - Definitions come before statements, but a procedure may call any procedure, including one defined later.
- Recursion (a procedure that calls itself, directly or indirectly) is not allowed. The compiler does not check for it.
How the Pretty-Printer Writes Programs¶
- Operations: every one gets its own parentheses.
x + 5 * 2prints as(x + (5 * 2)). This is valid input. It's just more parentheses than a person would write. - Whole numbers print without
.0(20, not20.0). - Negative numbers print in parentheses:
move left (-45). - Indentation: four spaces per block level. Each definition is followed by a blank line.
- Not preserved: blank lines and layout inside the program, and the choice of fewer parentheses. The AST does not record them.
How to Work: GHCI, Tests, the Tools¶
GHCI¶
$ ghci Examples.hs
PiqParser.hs,
type :reload (or :r).
When a parse fails, GHCI shows an exception. The first line is the message, starting with where in the input
the problem was (<input>:line:column):
ghci> parse number "x"
*** Exception: <input>:1:1 -- <num> expected
CallStack (from HasCallStack):
error, called at ./ParserBase.hs:80:13 in main:ParserBase
" when the error arrives, so you
see "*** Exception: …. That's just GHCI; the " isn't part of the message.
Tests¶
| Command | Runs |
|---|---|
make test |
all the tests |
make part1 … make part10 |
only one part's tests |
runghc -itest test/Spec.hs --match "Step 3.2" |
only one step's tests |
Each step below says which command to run and exactly what the last line should say.
About tests that pass early: many tests check that bad input is rejected ("… is rejected"). An
unfinished parser rejects everything, so those tests pass from the start. They become meaningful when your parser
starts accepting things: then they make sure it doesn't accept too much. The test summary for a step is only fully
meaningful once you've done that step.
The Tools¶
make builds three command-line tools into bin/. They need your parser, so they're most useful at the end
(Part 10):
bin/piq2py file.piq [-o other.py] piq source -> Python (default: file.py)
bin/py2png file.py [-o other.png] Python -> picture, drawn by the mock plotter (default: file.png)
bin/piq2png file.piq [-o other.png] both: file.py and file.png
The Provided Compiler¶
PiqEvaluation.hs is provided. It is one possible solution to HW 5's compiler assignment. You do not need
to modify or understand every detail of it to complete this week's work.
You may replace it with your own HW 5 PiqEvaluation.hs if you want, but you don't need to. The supplied
compiler lets you work on parsing independently of any bugs that may remain in your first assignment.
This file is a HW 5 solution
This is the reason for the warning at the top of this page: PiqEvaluation.hs is a complete, working answer to HW 5. You should only be reading it after you've submitted HW 5, and Gradescope will ask you to confirm that you did.
The Parser Library: A Cheat Sheet¶
A Parser a reads some input and produces a value of type a. parse p "input" runs parser p on the whole
input (it's an error if anything is left over). All of these skip whitespace (spaces, tabs, newlines) before what
they read.
| Combinator | Meaning | Example (in GHCI) → result |
|---|---|---|
number, double |
an integer / a decimal number | parse double "2.5" → 2.5 |
text "abc" |
exactly the characters abc |
parse (text "hello") " hello" → "hello" |
sym 'c' |
exactly the character c |
used as a separator: sym ',' |
identifier |
a letter followed by letters and digits | parse identifier "size2" → "size2" |
p <+> q |
p then q; the result is a pair |
parse (number <+> number) "3 4" → (3,4) |
p <-+> q |
p then q; keep only q's result |
parse (text "move" <-+> number) "move 5" → 5 |
p <+-> q |
p then q; keep only p's result |
|
p <|> q |
try p; if it fails, try q instead |
parse (text "a" <|> text "b") "b" → "b" |
p >>=: f |
run p, then apply the function f to its result |
parse (number >>=: (* 2)) "21" → 42 |
p >>: v |
run p, then ignore its result and give v |
parse (text "yes" >>: True) "yes" → True |
p <=> test |
run p, but fail unless its result passes test |
see Part 1 |
many p |
p zero or more times; a list of results |
parse (many digit) "123" → "123" |
sepBy p sep |
zero or more ps separated by seps |
parse (sepBy number (sym ',')) "1, 2, 3" → [1,2,3] |
parens p, braces p |
p inside ( ) / { } |
parse (parens number) "( 7 )" → 7 |
pfail |
always fails | the starting value of unfinished parsers |
How they group. >>=: and >>: bind most loosely, <|> binds more tightly, and <+>, <-+>, <+->
bind most tightly. So keyword "circle" <-+> expr >>=: Circle means "(keyword then expr), then apply Circle",
which is what you want. But when you combine alternatives, put each one in parentheses:
(p1 >>=: f) <|> (p2 >>=: g). Without them, p1 >>=: f <|> p2 >>=: g would group the wrong way.
Sequencing makes nested pairs: a <+> b <+> c gives ((ra, rb), rc). A lambda with a matching pattern takes
it apart: >>=: \((ra, rb), rc) -> …. A pair result can also go straight to a two-argument function with
uncurry: uncurry f (x, y) = f x y.
Part 0: Orientation¶
Nothing to implement yet. Get to know the library, and the compiler you've been given.
Step 0.1: The Parser Library¶
Start ghci Examples.hs and try the examples from the cheat sheet. Then try a parse that fails.
✅ Check your work (Step 0.1)
ghci> parse number "42"
42
ghci> parse (number <+> number) "3 4"
(3,4)
ghci> parse (text "move" <-+> number) "move 5"
5
ghci> parse (number >>=: (* 2)) "21"
42
ghci> parse (sepBy number (sym ',')) "1, 2, 3"
[1,2,3]
ghci> parse (identifier <=> (\w -> length w > 3)) "long"
"long"
ghci> parse number "x"
*** Exception: <input>:1:1 -- <num> expected
…
Step 0.2: The Provided Compiler, and Your Starting Point¶
The compiler works right away, on abstract syntax. previewProgram compiles an AST, writes python/<name>.py, and
draws python/<name>.png. showPretty prints an AST as source code; that's the language your parser will read.
And one parser already works: parseExpr can read a number.
✅ Check your work (Step 0.2)
ghci> mapM_ putStrLn (compileProgram (Program [] [Dot]))
pen_down()
pen_up()
ghci> previewProgram "hello" helloProgram
Writing python/hello.py
[nextdraw_backend] Using MOCK NextDraw (preview only)
[mock_nextdraw] Saved preview to …/python/hello.png
Preview: python/hello.png
ghci> showPretty exampleProgram
define target(r) {
for k from 1 to 3 {
circle ((k * r) / 3)
}
dot
}
move left 60
for i from 1 to 3 {
target((10 + (i * 5)))
move right 60
}
move left 180
move down 40
line right 120
ghci> parseExpr "42"
Num 42.0
python/hello.png shows a 40 mm square with a circle inside it and a dot at the centre. The pretty-printer
puts parentheses around every operation (circle ((k * r) / 3)); your parser will accept both that and the
simpler circle k * r / 3 in programs/example.piq.
$ runghc -itest test/Spec.hs --match "Step 2.1"
7 examples, 0 failures
Part 1: Names and Keywords¶
piq has reserved words (dot, square, …, define): they have a special meaning, so they can't be used as
names. Names are made of a letter followed by letters and digits, which is exactly what the library's
identifier reads.
Why text "dot" Is Not Good Enough¶
To read the keyword dot, you might use text "dot". But text only checks the next three characters. So given
the name dotty, it happily reads dot and leaves ty behind:
✅ Try this (no code to write)
ghci> parse (text "dot" <+> identifier) "dotty"
("dot","ty")
So dotty = 3 could be read as the statement dot followed by ty = 3, which is wrong. The fix: read a whole
identifier, then check that it's exactly the keyword.
Step 1.1: isReserved¶
Implement isReserved word: is word one of reservedWords (provided)? One line with elem.
✅ Check your work (Step 1.1)
ghci> isReserved "left"
True
ghci> isReserved "leftover"
False
$ runghc -itest test/Spec.hs --match "Step 1.1"
3 examples, 0 failures
Step 1.2: keyword¶
Implement keyword word: read an identifier, and keep it only if it's exactly word. Use <=> with a small
test function (a lambda). keyword "dot" then accepts dot but rejects dotty, because identifier reads all of
dotty, and "dotty" isn't "dot".
✅ Check your work (Step 1.2)
ghci> parse (keyword "dot") "dot"
"dot"
ghci> parse (keyword "dot") "dotty"
"*** Exception: <input>:1:6 -- No parse (via pfail)
…
$ runghc -itest test/Spec.hs --match "Step 1.2"
6 examples, 0 failures
Step 1.3: name¶
Implement name: an identifier that is not reserved. Same idea as keyword, with a different test.
Now the provided dot statement works too, because it's keyword "dot" >>: Dot.
✅ Check your work (Step 1.3, and all of Part 1)
ghci> parse name "dotty"
"dotty"
ghci> parse name "circle"
"*** Exception: <input>:1:7 -- No parse (via pfail)
…
ghci> parseStmt "dot"
Dot
$ runghc -itest test/Spec.hs --match "Step 1.3"
10 examples, 0 failures
$ make part1
19 examples, 0 failures
Part 2: Atoms¶
From here on, keep the grammar (also in your repo as SYNTAX.md) open. The grammar for expressions is
expr ::= term (("+" | "-") term)*
term ::= factor (("*" | "/") factor)*
factor ::= number | name | "(" expr ")"
A factor is an atom: the smallest piece of an expression. factor's number case is provided:
double >>=: Num reads a number and wraps it in the Num constructor.
For now expr and term are placeholders: expr = term and term = factor, so that expressions work at all.
You'll replace them in Part 3.
Step 2.1: Numbers (provided)¶
Already works: the tests passed in Part 0.
Step 2.2: Variables¶
Add an alternative to factor: a name, wrapped in Var. Add it with <|> before the <???> line (that
line only improves the error message).
✅ Check your work (Step 2.2)
ghci> parseExpr "size"
Var "size"
ghci> parseExpr "dot"
*** Exception: <input>:1:4 -- No parse (via pfail)
…
dot is rejected because it's reserved, not a name.
$ runghc -itest test/Spec.hs --match "Step 2.2"
10 examples, 0 failures
Step 2.3: Parentheses¶
Add a third alternative: an expr inside parentheses (parens). Notice that factor uses expr, and expr
(through term) uses factor. That recursion is what lets expressions nest.
✅ Check your work (Step 2.3, and all of Part 2)
ghci> parseExpr "((x))"
Var "x"
ghci> parseExpr "(1 + 2)"
*** Exception: <input>:1:3 -- ')' expected
…
+ yet (Part 3).
$ runghc -itest test/Spec.hs --match "Step 2.3"
7 examples, 0 failures
$ make part2
24 examples, 0 failures
Part 3: Precedence and Associativity¶
Two rules decide what an expression means:
- Precedence:
*and/bind more tightly than+and-. Sox + 5 * 2meansx + (5 * 2):Add (Var "x") (Mul (Num 5) (Num 2)) - Left associativity: operators of the same level group from the left. So
10 - 3 - 2means(10 - 3) - 2= 5, not10 - (3 - 2)= 9:Sub (Sub (Num 10) (Num 3)) (Num 2)
The grammar gets precedence from its levels: an expr is made of terms joined by +/-, and a term is
made of factors joined by *//. In x + 5 * 2, the 5 * 2 is one term, so the * happens "inside" the +.
Left associativity comes from how you combine the pieces. Reading 10 - 3 - 2 as "a first operand, then a list
of (operator, operand) pairs" gives
first operand: Num 10
pairs: [(Sub, Num 3), (Sub, Num 2)]
Combining from the left means: combine the first operand with the first pair, then combine that result with the next pair, and so on.
Step 3.1: combineLeft¶
Implement combineLeft :: Expr -> [(Expr -> Expr -> Expr, Expr)] -> Expr. The operators in the pairs are AST
constructors such as Sub, which are functions of two Exprs. Recursion on the list of pairs:
- no pairs: the result is the expression so far;
- a pair
(op, right)followed by more pairs: combine the expression so far withrightusingop, and carry that on to the rest of the pairs.
✅ Check your work (Step 3.1)
ghci> combineLeft (Num 10) [(Sub, Num 3), (Sub, Num 2)]
Sub (Sub (Num 10.0) (Num 3.0)) (Num 2.0)
ghci> combineLeft (Num 1) []
Num 1.0
$ runghc -itest test/Spec.hs --match "Step 3.1"
4 examples, 0 failures
Step 3.2: Terms: mulOp and term¶
Implement mulOp: a parser for * or / whose result is the AST constructor (Mul or Div). Two
alternatives, each "read the symbol, give the constructor" (>>:).
Replace the placeholder term, following its grammar rule: a factor, then zero or more (operator, factor)
pairs. Sequencing a factor with many of (mulOp then factor) produces a pair: the first factor, and a list of
(operator, factor) pairs. Those are exactly combineLeft's two arguments; hand them over with >>=: and a lambda.
✅ Check your work (Step 3.2)
ghci> parseExpr "8 / 4 / 2"
Div (Div (Num 8.0) (Num 4.0)) (Num 2.0)
ghci> parseExpr "2 * x"
Mul (Num 2.0) (Var "x")
$ runghc -itest test/Spec.hs --match "Step 3.2"
5 examples, 0 failures
Step 3.3: Expressions: addOp and expr¶
Implement addOp (+/-, giving Add/Sub) and replace the placeholder expr the same way, one level up:
terms joined by addOp.
✅ Check your work (Step 3.3)
ghci> parseExpr "2 + 3 * 4"
Add (Num 2.0) (Mul (Num 3.0) (Num 4.0))
ghci> parseExpr "x + 5 * 2"
Add (Var "x") (Mul (Num 5.0) (Num 2.0))
ghci> parseExpr "10 - 3 - 2"
Sub (Sub (Num 10.0) (Num 3.0)) (Num 2.0)
ghci> parseExpr "(1 + 2) * 3"
Mul (Add (Num 1.0) (Num 2.0)) (Num 3.0)
$ runghc -itest test/Spec.hs --match "Step 3.3"
22 examples, 0 failures
Step 3.4: Round Trips¶
The provided pretty-printer turns an AST back into source code. It puts parentheses around every operation, so its
output is unambiguous. For every expression e, parsing the printed expression must give back e exactly:
parseExpr (prettyExpr e) == e
This is a strong test: if your parser recovers the structure of what the printer wrote, precedence and grouping must be right. Nothing new to write.
✅ Check your work (Step 3.4, and all of Part 3)
ghci> prettyExpr (parseExpr "x + 5 * 2")
"(x + (5 * 2))"
ghci> parseExpr (prettyExpr (Sub (Num 10) (Sub (Num 3) (Num 2))))
Sub (Num 10.0) (Sub (Num 3.0) (Num 2.0))
$ runghc -itest test/Spec.hs --match "Step 3.4"
14 examples, 0 failures
$ make part3
45 examples, 0 failures
Part 4: Basic Statements¶
A statement is one of several kinds. The provided stmt tries each kind in turn:
stmt = assignment <|> call <|> forLoop <|> dotStmt <|> squareStmt <|> rectangleStmt
<|> circleStmt <|> moveStmt <|> lineStmt
Each unfinished one is pfail for now, so stmt already works for the kinds you've finished. Every statement
except assignment and call starts with its own keyword, which is why the keywords from Part 1 matter.
The provided example is dotStmt = keyword "dot" >>: Dot. It reads the keyword, then gives the constructor.
Step 4.1: Dot (provided)¶
Already works since Part 1.
✅ Check your work (Step 4.1)
$ runghc -itest test/Spec.hs --match "Step 4.1"
4 examples, 0 failures
Step 4.2: Square, Rectangle, Circle¶
Implement squareStmt, rectangleStmt, circleStmt.
square EXPR: the keyword, then an expression. Keep only the expression (<-+>) and applySquareto it (>>=:).circle EXPRworks the same way.rectangle EXPR EXPR: two expressions, width then height. Sequencing them gives a pair, anduncurry Rectangleturns a pair into aRectangle.
✅ Check your work (Step 4.2)
ghci> parseStmt "square 20"
Square (Num 20.0)
ghci> parseStmt "rectangle w + 1 h * 2"
Rectangle (Add (Var "w") (Num 1.0)) (Mul (Var "h") (Num 2.0))
ghci> parseStmt "circle r / 2"
Circle (Div (Var "r") (Num 2.0))
rectangle w + 1 h * 2, the first expression ends where it can't continue: after w + 1, the next token h
isn't an operator.
$ runghc -itest test/Spec.hs --match "Step 4.2"
18 examples, 0 failures
rectangle 30 -10 is rejected: 30 -10 reads as the single expression 30 - 10,
and then the height is missing. Programs write rectangle 30 (-10) instead.
Step 4.3: Directions, Move, Line¶
Add the other three alternatives to direction (right, up, down; left is provided), then implement
moveStmt and lineStmt: the keyword, a direction, an expression. The direction and the expression form a pair,
and uncurry Move turns that pair into a statement.
Distances are always required: move right without a number is an error.
✅ Check your work (Step 4.3)
ghci> parse direction "up"
Up
ghci> parseStmt "move left 20"
Move Lt (Num 20.0)
ghci> parseStmt "line down 2.5"
Line Dn (Num 2.5)
ghci> parseStmt "move right"
*** Exception: <input>:1:11 -- a number, a name, or ( expected
…
$ runghc -itest test/Spec.hs --match "Step 4.3"
17 examples, 0 failures
Step 4.4: Assignment¶
Implement assignment: NAME = EXPR. Keep the name and the expression, drop the = (<+-> keeps the left
result), and build an Assign.
Because name never accepts a reserved word, dotty = 3 is an assignment, and dot = 1 is rejected.
✅ Check your work (Step 4.4)
ghci> parseStmt "size = size * 2"
Assign "size" (Mul (Var "size") (Num 2.0))
ghci> parseStmt "dotty = 3"
Assign "dotty" (Num 3.0)
$ runghc -itest test/Spec.hs --match "Step 4.4"
14 examples, 0 failures
Step 4.5: Statement Round Trips¶
As in Step 3.4: every basic statement in HW 5's examples, printed and parsed back, must come out the same.
✅ Check your work (Step 4.5, and all of Part 4)
ghci> prettyStmt (Rectangle (Num 30) (Num (-10)))
"rectangle 30 (-10)"
ghci> parseStmt (prettyStmt (Rectangle (Num 30) (Num (-10))))
Rectangle (Num 30.0) (Num (-10.0))
$ runghc -itest test/Spec.hs --match "Step 4.5"
1 example, 0 failures
$ make part4
54 examples, 0 failures
Part 5: Statement Sequences and Blocks¶
Newlines and semicolons are not statement terminators. piq has no separators at all: whitespace, including newlines, only separates tokens. A statement simply ends where its last part ends, so these mean the same thing:
x = 10 x = 10 square x x = x * 2 square x
square x
x = x * 2
square x
Step 5.1: stmts¶
Implement stmts: zero or more statements. That's one library combinator.
✅ Check your work (Step 5.1)
ghci> parseStmts "x = 10 square x x = x * 2 square x"
[Assign "x" (Num 10.0),Square (Var "x"),Assign "x" (Mul (Var "x") (Num 2.0)),Square (Var "x")]
ghci> parseStmts "dot\n dot"
[Dot,Dot]
ghci> parseStmts ""
[]
$ runghc -itest test/Spec.hs --match "Step 5.1"
13 examples, 0 failures
text "dot", showing dotty = 3 wrongly split into a dot and ty = 3. Your keyword
is what prevents that.
Step 5.2: block¶
Implement block: statements between braces. A block may be empty.
✅ Check your work (Step 5.2, and all of Part 5)
ghci> parse block "{ dot square 3 }"
[Dot,Square (Num 3.0)]
ghci> parse block "{}"
[]
$ runghc -itest test/Spec.hs --match "Step 5.2"
8 examples, 0 failures
$ make part5
21 examples, 0 failures
Part 6: Loops¶
for i from 1 to 5 {
square i * 10
}
Step 6.1: forLoop¶
Implement forLoop from four pieces, in order:
keyword "for", then aname(the loop variable);keyword "from", then anexpr;keyword "to", then anexpr;- a
block.
Keep only the interesting results (<-+> drops each keyword). Sequencing the four pieces with <+> gives
nested pairs, (((var, from), to), body). Take them apart with a lambda and build For var from to body.
✅ Check your work (Step 6.1)
ghci> parseStmt "for i from 1 to 5 { square i * 10 }"
For "i" (Num 1.0) (Num 5.0) [Square (Mul (Var "i") (Num 10.0))]
$ runghc -itest test/Spec.hs --match "Step 6.1"
13 examples, 0 failures
Step 6.2: Nested Loops¶
Nothing new: a loop's body is a block, a block holds statements, and a statement can be a loop.
✅ Check your work (Step 6.2, and all of Part 6)
ghci> parseStmt "for i from 1 to 2 { for j from 1 to 3 { dot } }"
For "i" (Num 1.0) (Num 2.0) [For "j" (Num 1.0) (Num 3.0) [Dot]]
$ runghc -itest test/Spec.hs --match "Step 6.2"
2 examples, 0 failures
$ make part6
15 examples, 0 failures
Part 7: Procedure Calls¶
f()
box(20, 5)
foo(x + 1, y * 2)
Step 7.1: call¶
Implement call: a name, then, inside parentheses, zero or more expressions separated by commas.
sepBy does exactly that (sym ',' reads a comma). Build Call name args (uncurry again).
Calls and assignments both start with a name. stmt tries assignment first, and it fails when there's no =,
so then call gets its turn.
✅ Check your work (Step 7.1, and all of Part 7)
ghci> parseStmt "f()"
Call "f" []
ghci> parseStmt "box(20, 5)"
Call "box" [Num 20.0,Num 5.0]
ghci> parseStmt "foo(x + 1, y * 2)"
Call "foo" [Add (Var "x") (Num 1.0),Mul (Var "y") (Num 2.0)]
$ runghc -itest test/Spec.hs --match "Step 7.1"
16 examples, 0 failures
$ make part7
16 examples, 0 failures
Part 8: Procedure Definitions¶
define box(size, gap) {
square size
move right size + gap
}
Step 8.1: procDef¶
Implement procDef: keyword "define", a name, a comma-separated list of parameter names in
parentheses, and a block. As with forLoop, sequencing gives nested pairs; build ProcDef name params body.
✅ Check your work (Step 8.1, and all of Part 8)
ghci> parse procDef "define box(size, gap) { square size move right size + gap }"
ProcDef "box" ["size","gap"] [Square (Var "size"),Move Rt (Add (Var "size") (Var "gap"))]
$ runghc -itest test/Spec.hs --match "Step 8.1"
14 examples, 0 failures
$ make part8
14 examples, 0 failures
Part 9: Whole Programs¶
program ::= procdef* stmt*
define after a statement is an error.
Step 9.1: program¶
Implement program: any number of definitions, then the statements, built into a Program (uncurry once
more).
✅ Check your work (Step 9.1)
ghci> parseProgram "define star() { dot } star() star()"
Program [ProcDef "star" [] [Dot]] [Call "star" [],Call "star" []]
ghci> parseProgram "dot define f() { }"
*** Exception: <input>:1:11 -- No parse (via pfail)
…
$ runghc -itest test/Spec.hs --match "Step 9.1"
11 examples, 0 failures
Step 9.2: Program Round Trips¶
The final round trip: every one of HW 5's 21 example programs is printed as source code and parsed back, and must come out identical. Parsing the printed program must also compile to exactly the same Python. And two programs written the way a person would (fewer parentheses, different layout) must parse to exactly their ASTs.
✅ Check your work (Step 9.2, and all of Part 9)
ghci> parseProgram (prettyProgram exampleProgram) == exampleProgram
True
$ runghc -itest test/Spec.hs --match "Step 9.2"
24 examples, 0 failures
$ make part9
35 examples, 0 failures
Part 10: The Whole Pipeline¶
Your parser is complete. Now piq is a language you can write in files.
Step 10.1: From a File to a Picture¶
Build the tools, then run a program through the whole pipeline: your parser, the provided compiler, the Python file, and the mock plotter's picture.
✅ Check your work (Step 10.1)
$ make
…
$ bin/piq2png programs/example.piq
Writing programs/example.py
Rendering programs/example.py to programs/example.png
[nextdraw_backend] Using MOCK NextDraw (preview only)
[mock_nextdraw] Saved preview to …/programs/example.png
programs/example.png shows three targets, each three circles and a centre dot, growing from
left to right, with a line underneath. Blue is drawn; the thin pink lines are pen-up travel (including the trip
from the plotter's home corner at the top left). The grey dashed box is the safe area: see Step 10.3.
$ runghc -itest test/Spec.hs --match "Step 10.1"
7 examples, 0 failures
bin/piq2png programs/garden.piq (flowers on a ground line, with a sun) and
bin/piq2png programs/woven.piq (an abstract pattern: curves made only of straight lines).
Step 10.2: Errors Stop the Pipeline¶
An invalid program must never become a Python file. A syntax error stops at your parser; an error like an undefined variable stops at the compiler.
✅ Check your work (Step 10.2, and all of Part 10)
$ echo 'square 10 circle r' > programs/bad.piq
$ bin/piq2py programs/bad.piq
error: undefined variable: r
programs/bad.py is written. (Delete programs/bad.piq afterwards.)
$ runghc -itest test/Spec.hs --match "Step 10.2"
5 examples, 0 failures
$ make part10
12 examples, 0 failures
Step 10.3: Your Own Program¶
Write your own drawing in a new file, programs/mine.piq. Use SYNTAX.md for reference, and look at
programs/garden.piq for ideas. Then run bin/piq2png programs/mine.piq. Keep the drawing inside the safe area,
the grey dashed box in the preview: at least 1 inch from the paper's edges, which means within 190.5 mm left/right
and 114.3 mm up/down of the start. Anything outside it is drawn in red with a warning, and the real plotter refuses
to draw it.
Common Pitfalls¶
- Alternatives without parentheses.
p1 >>=: f <|> p2 >>=: ggroups wrongly; write(p1 >>=: f) <|> (p2 >>=: g). - Using
textfor a keyword.text "move"also matches the start ofmovement; usekeyword. - Right associativity by accident. If
10 - 3 - 2givesSub (Num 10.0) (Sub …),combineLeft(or the way you combine) is grouping from the right. - Forgetting that the first operand isn't in the list. In
a + b + c, the list of pairs has two entries, not three. - Nested pairs.
a <+> b <+> cproduces((a, b), c), not(a, b, c). - Tests that pass too early: "is rejected" tests pass while a parser rejects everything. Check the step's full summary line after you finish the step.
- Forgetting
:reloadin GHCI after editing. - The
"before*** Exceptionis GHCI starting to print a string result. It's not part of the message.
Final Checklist¶
- No placeholders are left in
PiqParser.hs:grep -nE 'error "TODO|pfail$' PiqParser.hsprints nothing. (In the starter it finds 18.) -
bin/piq2png programs/example.piqproduces the three targets. - Your own
programs/mine.piqdraws what you intended. - You didn't edit the provided files (including
ParserCombinators.hs,ParserBase.hs, andtest/).
Turning It In¶
Submit on Gradescope, under HW 06: PicPlot Part 2: Parsing:
- Confirm the HW 5 → HW 6 order. Check the box confirming that you (and your partner, if any) didn't open the
piq-parserrepo or look atPiqEvaluation.hsuntil after completing and submitting HW 5. See the box at the top of this page. - From the top of the repo, run the full test suite one last time:
make test. Copy the entire terminal output and paste it into the test-output text box. - Upload
PiqParser.hs, the only file you edit for this assignment. - Upload your own program from Step 10.3,
programs/mine.piq, together with the picture it produced,programs/mine.png.
Gradescope also asks you to explain two pieces of your parser (precedence and associativity, and keyword), plus a few short reflection questions.
The Homework Workflow covers the submission step in context.
Make sure your final code is committed and pushed to your repo as well.
Appendix: The Real Plotter¶
Drawing on the real NextDraw plotter happens in the lab, with your instructor's setup and permission. Follow the course procedure:
- Preview first. Never send a drawing to the plotter that you haven't previewed (
bin/piq2png) and checked. - The plotter refuses any drawing that leaves the safe area (190.5 mm left/right and 114.3 mm up/down of the start).
- Before plotting, the carriage must be at its home position (the top-left corner).
- On the plotter computer, which has the NextDraw software installed, a generated program runs with
--doplot:python programs/example.py --doplot