Compilers

Hoop Snake

hoop snake
Hoop Snake

Due on Friday, April 28th at 11:59 PM. This is a compiler lab. If you have a partner, the two of you will complete this lab as a team. If you do not have a partner, you are effectively in a team by yourself.

If you are working with a partner, please be familiar with the Partner Etiquette guidelines. You and your partner share a single repository and, barring unusual circumstances, will receive the same grade. If you experience any difficulties in your partnership, please alert your instructor as soon as possible.

If you are working alone, please don’t hesitate to seek help if you get stuck. Since there are no ninjas for this course and you don’t have a partner, your instructor is the only interactive resource available to you and is happy to help you through any problems you might encounter.

In either case, be familiar with the academic integrity policy! You are permitted to discuss high-level details of your compiler project with other students, but you should never see or share anything that will be turned in for a grade. If you need help, please post on the Piazza forum or, if necessary, contact the instructor directly. Make sure to post privately if you are sharing code. If you have any doubts about what is okay and what is not, it’s much safer to ask than to risk violating the Academic Integrity Policy.

Overview

This lab contains two parts:

  1. In the first part, we will extend the Garbage Snake compiler to include tail call optimization. We will call the resulting languge “Hoop Snake”. Once you have completed this task, you will be able to compile arbitrary long-running computations using recursion for loops!

  2. In the second part, we will write an LL parser for a modified form of the Cobra language. The parser is the only component of the compiler we have not yet explored. Writing a Cobra parser will give you enough familiarity with parsing that, given time, you could produce a Hoop Snake parser on your own.

This lab write-up will proceed in the order given above. Strictly speaking, however, you can perform these parts in either order so long as you make the appropriate change to language.mk to switch back and forth. Neither part of the lab relies upon the other.

Part I: Tail Call Optimization

When you git pull, you’ll discover the usual updates. The most relevant for this part of the lab is the src/languages/hoopSnake directory. As usual, change the LANGUAGE variable in language.mk from garbageSnake to hoopSnake. Then, run make clean and make. Your code will not compile, but it won’t take much effort to get it back up and running.

The Hoop Snake Language

The Hoop Snake language is syntactically identical to Garbage Snake; there are no new forms of expressions or new language features available to the programmer. In fact, the only difference is a change in the A-normalized abstract syntax! The CAppl constructor has been changed from

| CAppl of expr * expr

to

| CAppl of expr * expr * bool

The additional boolean value indicates whether the CAppl in question is in a tail position: that is, whether the function call is the very last thing that this expression will do.

The first thing you should do in implementing Hoop Snake is to modify your A-normalization code to set this boolean value to false always. Then, modify compiler.ml to match on (but ignore) this value. Finally, make your unit tests and ensure that everything is still working.

Marking Tail Positions

Next, you need to mark tail positions in your AST. You can either do this during A-normalization or you can write a separate function of type a_program -> a_program which marks the tail positions of every expression in the AST (and then call that function at the right point within your compiler). The particular approach you use does not matter as long as the tail position variable is being set correctly.

Tail Calls

Consider the following program:

def f x = x end
def g y = f y end
g 4

In Garbage Snake, which has no tail calls, the following sequence of events would occur:

Tail call optimization begins with the observation that there was no point in returning to the body of g. We knew we would simply be returning its value, so the stack frame we’ve created for g is just taking up space. With tail call optimization, we want to bring about the following sequence of events instead:

This kind of optimization is especially important in functional languages, which make frequent and thorough use of recursion. With tail call optimization, recursive tail calls consume no additonal stack memory and so work much like while loops in imperative languages. Without tail call optimization, long-running recursive loops will overflow the stack.

Implementing Tail Call Optimization

In tail call optimization, there are three main participants:

  1. The original caller. In the example above, this is the program’s main expression.
  2. The tail caller. In the example above, this is g.
  3. The tail callee. In the example above, this is f.

Not every call can be optimized. For us to apply our tail call optimization strategy, we must know the following facts:

Since the tail call position is known at compile time, it is the easiest part to handle: for a CAppl which is not in a tail position, we simply generate the same assembly as we would in Garbage Snake.

If the CAppl is in a tail position, we need to generate code which might perform a tail call. Remember that a CAppl refers to two values: a closure and an argument. We’ll have our compiler generate assembly code which uses the following algorithm:

It may be helpful to get this working for tail callees with an equal number of parameters first and then extend your implementation to tail callees with fewer parameters.

Testing Tail Call Optimization

Testing a compiler optimization is somewhat different than testing the compilation of a language feature. Our tests so far have treated the program as a black box and just checked its output. But an optimization should, by its very nature, not change what task a program accomplishes: it should just make the program smarter about how to accomplish that task.

In this case, however, we do know of an observable change in behavior. In Garbage Snake, long-running recursive functions will run out of stack memory and cause a segmentation fault. We can test the Hoop Snake optimization by writing such programs and making sure they run correctly. Here are some examples of the sorts of tests you may want to run:

Part II: Parsing

The second part of this assignment focuses on writing an LL parser for Cobra. The original grammar for the Cobra language was

<expression> ::=
  | true
  | false
  | <integer>
  | <identifier>
  | let <identifier> = <expression> in <expression>
  | if <expression> then <expression> else <expression>
  | inc(<expression>)
  | dec(<expression>)
  | print(<expression>)
  | isbool(<expression>)
  | isint(<expression>)
  | <expression> + <expression>
  | <expression> - <expression>
  | <expression> * <expression>
  | <expression> < <expression>
  | <expression> > <expression>
  | <expression> = <expression>
  | <expression> && <expression>
  | <expression> || <expression>
  | (<expression>)

This grammar is left recursive, however, and so we can’t write an LL parser for it directly. Instead, we will write a parser for the following (equivalent) grammar:

<expr> ::=
  | let <IDENTIFIER> = <expr> in <expr>
  | if <expr> then <expr> else <expr>
  | <orExpr>

<orExpr> ::=
  | <andExpr>
  | <andExpr> || <andExpr> ...

<andExpr> ::=
  | <comparisonExpr>
  | <comparisonExpr> && <comparisonExpr> ...

<comparisonExpr> ::=
  | <additiveExpr>
  | <additiveExpr> <comparisonOperator> <additiveExpr> ...

<comparisonOperator> ::=
  | <
  | >
  | =

<additiveExpr> ::=
  | <multiplicativeExpr>
  | <multiplicativeExpr> <additiveOperator> <multiplicativeExpr> ...

<additiveOperator> ::=
  | +
  | -

<multiplicativeExpr> ::=
  | <primaryExpr>
  | <primaryExpr> * <primaryExpr> ...

<primaryExpr> ::=
  | true
  | false
  | <INTEGER>
  | - <INTEGER>
  | <IDENTIFIER>
  | inc(<expr>)
  | dec(<expr>)
  | print(<expr>)
  | isint(<expr>)
  | isbool(<expr>)
  | (<expr>)

The above grammar accepts the same programs but has been adjusted to avoid left recursion.

Implementing an LL Parser

To begin writing your LL parser, you should create a new directory in the src/language directory. This new directory will be a peer to the other parsers we have used throughout the course, such as adder and hoopSnake. You may name it whatever you like.

For your parser to be compatible with the rest of the code in the compiler, you should do the following:

After this, the layout and organization of the parser is up to you. You can use whatever approach or design you like as long as your submission is your own code. You are not permitted to use a parser generator to complete this assignment (although you are permitted to write your own parser combinators if you like).

Testing Your Parser

Because Cobra is a strict subset of Hoop Snake – that is, every Cobra program is a valid Hoop Snake program – you can use your Cobra-compatible unit tests to test your parser. This is as simple as commenting out those unit tests that use files from other languages. You can also write direct unit tests, passing your parse function a string to get the actual value and creating an AST by hand to get the expected value. Remember that you can use show_program or show_expr to turn ASTs into strings.

Parser Development Tips

There are a few miscellaneous tips which may help in writing your parser.

Summary

To complete this assignment, you’ll need to

Once you’ve done this, you’ve written a compiler from beginning to end! Now that you’re more familiar with OCaml, you might be interested to glance over builder.ml and the other parts of the compiler you haven’t changed much; you’ll see that they contain code to open files and run subprograms (like clang), but that’s about it. Congratulations!

Submitting

To submit your lab, just commit and push your work. The most recent pushed commit as of the due date will be graded. For more information on when assignments are due, see the Policies page.

If You Have Trouble…

…then please contact your instructor! Piazza is the preferred method, but you can reach out via e-mail as well. Good luck!