HW5: Trefoil v3
This homework has three parts:
- Port Trefoil v2 to be implemented in OCaml
- Add structs and pattern matching to Trefoil
- Write a reasonably substantial amount of code in Trefoil v3
Before you start: One more OCaml package
We decided to use one more OCaml package. Please run
opam install ppx_deriving
Otherwise, the starter code will not build.
Detailed tour of the starter code
You made it into the hw5/
subdirectory. Here's what you see.
-
README.md
: this file, your guide to HW5 -
LANGUAGE.md
: the specification for Trefoil v3 -
FEEDBACK.md
: our usual questions about homework, to fill out after you're done -
src/
: OCaml files implementing and unit testing the interpreter
In src/
there are several files.
-
trefoil3.ml
: main entry point of the interpreter- Defines a
main
function that supports either keyboard or file input - In the local function
loop
, it parses one PST at a time, converts it to a binding, and then interprets that binding.
- Defines a
-
errors.ml
declares exceptions you will throw in various situations, similar to Trefoil v2 -
pstparser.ml
implements the same PST parser as we used on HW3, but now in OCaml.- You do not need to read or understand this file unless you want to.
-
pst.ml
declares the PST type in OCaml as a variant with two constructors,Symbol
andNode
.- You will need to read and understand (but not edit) the declaration of the
pst
type.
- You will need to read and understand (but not edit) the declaration of the
-
ast.ml
declares AST types for each syntactic category (expressions, bindings, and patterns).- ASTs are represented with variant types, as usual in OCaml.
- There are also functions for converting PSTs into expressions, bindings, and patterns here.
-
interpreter.ml
implements the semantics of expressions, bindings, and patterns. -
trefoil3test.ml
contains unit tests for the interpreter.
You only need to edit ast.ml
, interpreter.ml
, and trefoil3test.ml
.
Informal description of the language
The Trefoil v3 language is an extension of Trefoil v2. We only describe the differences.
Bindings
There is one new kind of binding, the struct
binding, which declares a new
struct type. For example, the binding (struct my-record name age)
declares a
struct called my-record
with fields name
and age
. We have discussed this
binding extensively in lecture. In short, it generates a constructor, a
predicate function, and field accessor functions for each field.
Expressions
There are three new user-facing expressions, all of which we have discussed in lecture.
- Trefoil symbols, which are kind of like strings without spaces in them. They start with an apostrophe:
'
. -
cond
expressions, which are a nicer syntax for nested if-else chains. -
match
expressions, which implement pattern matching.- A big deal with
match
expressions is that they use patterns, which is a new syntactic category in Trefoil v3 (see below).
- A big deal with
There are also three new "internal" forms of expression, which we have not discussed in lecture yet, but you will use to implement struct bindings. By "internal", we mean that there is no way for the Trefoil programmer to write these expressions directly in their code. Instead, when the Trefoil programmer writes a struct binding, your interpreter will autogenerate some functions that use these internal expressions in their bodies. Then the Trefoil programmer can call those autogenerated functions to indirectly use these new expressions. Here they are:
- struct constructor expressions, which represent the results of calling the struct constructor
- struct access expressions, which allow accessing fields of a struct constructor value
- struct predicate expressions, which tell you whether a value was built with a particular struct's constructor
See LANGUAGE.md
for more details about the syntax and semantics of the
internal expressions.
We also make a few changes to the Trefoil v2 semantics of expressions:
- The
=
operator now works on all values, not just integers. - The semantics of function calls is extended so that you can "call a struct
name" (which calls that structs constructor) in addition to "calling a
function name" like normal. These two kinds of function call have the same
syntax, and which one happens depends on whether the name being called is
bound to a struct or a function. See
LANGUAGE.md
.
Patterns
Patterns are a new syntactic category in Trefoil v3 that represent "the left hand side of a match clause", similar to patterns in OCaml that you are used to using.
There are 8 kinds of patterns, which we have discussed informally in lecture,
and which are discussed in detail in LANGUAGE.md
.
Your tasks: An overview
We have left several TODOs throughout the code. Here is an overview of what there is to do. Part 2 is by far the most work, and it is worth more points. See the Grading section at the end of this file.
Part 1: Port Trefoil v2 to OCaml
- Implement the Trefoil v2 spec in OCaml.
- We have done 90% of the work for you already. You have quite a bit of code to read in the starter code.
- You need to fill in a few TODOs to complete the port.
- We left out let expressions. Add them back in.
- We didn't quite finish implementing function calls. Finish them.
- We left out test bindings. Add them back in.
- We ported the starter tests from HW3, but not much more than that. Add a few more tests.
- Find and explain a difference between your two implementations of Trefoil v2.
Part 2: Implement new Trefoil v3 features
- Implement the new extended semantics of
=
. - Finish the implementation of
cond
. - Add struct bindings and the three "internal" struct expressions.
- Add match expressions and patterns.
Part 3: Extending and using Trefoil v3
- Add your own feature.
- Write a fairly substantial program of your choice in Trefoil v3.
Suggested detailed workflow
Part 1: Port Trefoil v2 to OCaml
Familiarize yourself with the provided files.
- First, skim this whole file. Then, use the "file tour" above to skim the
starter code. Pay special attention to
ast.ml
andinterpreter.ml
. Be sure you understand how ASTs are represented in OCaml as variant types, how dynamic environments are represented as association lists, and how the interpreters for expressions and bindings do their job. - Install one more opam package:
opam install ppx_deriving
- Ensure you can build the code by running
dune build
. (You can run this command either in thehw5/
directory or thehw5/src
directory. It will do the same thing.)-
dune build
will report many warnings. This is expected. Most of the warnings have to do with unused variables in functions where you have a TODO to finish writing the function. -
dune build
should not report any errors.
-
- Ensure you can run the unit tests by running
dune runtest
. You should see a bunch of messages followed byFAILED 37 / 63 tests
. - You can use
dune utop
similar to HW4.- After
dune utop
, typeopen Trefoil3lib;;
. - Then you can refer to functions from the file
ast.ml
by prefixing them withAst.
as inAst.expr_of_pst
. Or, if you don't like that, you can sayopen Ast;;
and then referexpr_of_pst
.
- After
- You can also run the interpreter interactively by running
dune exec ./src/trefoil3.exe
src
directory, rundune exec ./trefoil3.exe
.) You can also pass a filename to the interpreter like thisdune exec ./src/trefoil3.exe my-program.trefoil
- Have a look at
LANGUAGE.md
to get an idea of the list of expressions, bindings, and patterns you will be implementing.
Add let expressions to the starter code port.
- Check out the provided
expr
type inast.ml
. It has one constructor for each kind of expression. -
Fix the TODO to add a constructor to
expr
representing let.- Hint: It should have the same information as the AST class for let expressions had in HW3.
- Check out the provided function
expr_of_pst
, also inast.ml
. It has the same purpose asExpression.parsePST
from HW3.- Notice the way we have implemented the starter code. Every expression AST
whose PST syntax is a PST Node has two cases in the inner pattern match --
one for the normal case and one for syntax error case.
- For example, look at the cases for
if
expressions. - In the normal case, we use list patterns to match on the arguments and make
sure the number is correct. For
if
, there should be three arguments.| Pst.Symbol "if", [branch; thn; els] -> (* and then construct the AST node *)
- If the number is wrong, we fall through to the next case with a wildcard pattern.
| Pst.Symbol "if", _ -> (* and then raise an AbstractSyntaxError *)
- For example, look at the cases for
- Notice the way we have implemented the starter code. Every expression AST
whose PST syntax is a PST Node has two cases in the inner pattern match --
one for the normal case and one for syntax error case.
-
Fix the TODO to add cases for let expressions in
expr_of_pst
- Warning/hint:
let
is more complicated than most of the other operators in Trefoil, because it has doubly nested parentheses. For example, it would not be correct to say "well, mylet
AST node has three arguments, so I'll just copy the cases forif
expressions, which also have three arguments", because the PST syntax of lets has parentheses in weird places. You will need to exercise your knowledge of nested patterns to make sure you pare lets correctly!
- Warning/hint:
- Check out the provided tests in
trefoil3test.ml
. Find the test that has a local variable "manually_constructed_let
". This is a parser test forlet
expressions. -
Test your parser by fixing the TODO in
trefoil3test
about "manually_constructed_let
" (see the comments there) and ensuring that the test now passes (you should see the number of failures decrease). - Write a test or two to cover different ways that let expressions can be malformed. We have provided a template to help check that an abstract syntax error is thrown.
- Check out the provided function
interpret_expression
ininterpreter.ml
. It has the same purpose asInterpreter.interpretExpression
from HW3.- As in HW3, the interpreter for expressions takes a dynamic environment as
input, represented by the type
dynamic_env
, which is defined at the very top ofinterpreter.ml
as an association list of (string
,entry
) pairs, where anentry
is a variant type with constructors for variable entries, function entries, and struct entries.- One weird thing about the definition of
entry
anddynamic_env
is that they are mutually recursive, meaning that the definition ofentry
refers todynamic_env
(in the arguments toFunctionEntry
) while the definition ofdynamic_env
also refers toentry
in its definition. Normally, this is not allowed in OCaml, because bindings are ordered, and can only refer to earlier bindings. But, it is useful sometimes in situations exactly like this to allow two bindings to both refer to each other. OCaml supports this through theand
keyword used to separate the definitions ofentry
anddynamic_env
. The keywordand
replaces the keywordtype
, and it means "these are defined at the same time". - The key operation on
dynamic_env
islookup
, which is provided for you. We decided to makelookup
return anoption
instead of throwing an exception because you might want to throw a different exception in different situations. So each call site oflookup
will match and in the None case, throw an appropriate exception.- Notice that
lookup
traverses the list of pairs in left-to-right order and stops when it finds the first match. So adding a pair to the front of the list overrides any existing pair with the same key. This is key to how we will manipulate the dynamic environment below.
- Notice that
- Check out the case for variables in
interpret_expression
. It callslookup
and uses nested patterns to handle three different cases: nothing found, variable found, and something besides variable found (function or struct).
- One weird thing about the definition of
- As in HW3, the interpreter for expressions takes a dynamic environment as
input, represented by the type
-
Fix the TODO to add a case for let expressions in
interpret_expression
.- Hint: Review the HW3 semantics of let expressions and your HW3 solution.
- Hint: We have implemented the dynamic environment quite differently in OCaml
than in Java. To extend the environment, simply cons (
::
) onto the front of it. - Hint: For inspiration, it may help to check out the (provided) case for
top-level variable bindings in
interpret_binding
. It makes a similar manipulation of the dynamic environment. Note that let expressions differ from top-level variable bindings in that let expressions construct a temporary environment to evaluate the body, while top-level variable bindings construct a new environment which gets returned and propagated through the rest of the program.
-
Ensure the three provided tests for interpreting
let
expressions pass. (You should see the number of failures decrease.)
Finish the starter code's implementation of function calls.
- Check out the
expr
constructor forCall
(inast.ml
) and be sure you understand how it corresponds to the HW3LANGUAGE.md
syntax for function calls (and to your HW3 solution).- Function calls are already parsed for you. Check out the last case of
expr_of_pst
if you are curious.
- Function calls are already parsed for you. Check out the last case of
- Back in
interpreter.ml
check out the starter code forCall
expressions. We have handled the first few steps for you -- looking up the function name in thecallenv
and making sure thatlookup
returns a function binding.- The starter code contains a very important line that extends the defining environment with a recursive binding for the function being called. This is different than how we handled recursion in HW3, and avoids the need for mutation! :mind-blown:
-
Fix the TODO to implement function calls.
- Hint: Follow the same semantics as HW3.
- Hint: Use the provided helper function
interpret_expr_list
to evaluate the list of argument expressions to a value. - Hint: Perhaps the hardest part is constructing the list of variable-value
pairs that you want to extend the dynamic environment with. Remember
this motto for OCaml programming:
Whenever you feel like you need a loop, what you really need is a locally defined recursive helper function!
- Hint: Define a local recursive helper function that takes the parameter names and argument values and returns a list of variable entries combining them. Then append this list on the front of the defining environment.
- Hint: Don't forget to make sure your helper function signals a runtime error
if the number of parameter names is different than the number of argument
values. (You can do this without calling
List.length
if you are careful about it.)
-
Many more tests should now pass. Ensure that all remaining failures have
to do with test bindings,
struct
bindings,cond
expressions, ormatch
expressions. In particular, double check that the "basic function" and "lexical scope" tests pass.
Implement test bindings.
-
Fix the TODO to add a constructor to the
binding
type inast.ml
for test bindings. Follow the abstract syntax of test bindings as defined in HW3. -
Parse test bindings in
binding_of_pst
inast.ml
at the location indicated by the TODO. Similar toexpr_of_pst
, you should have two cases: one that checks for the correct number of arguments and one that uses a wildcard to accept any wrong number of arguments and report an error- Hint: Check out the other cases in
binding_of_pst
for inspiration. In particular, variable bindings are pretty similar to test bindings, just without the variable. So the code should be pretty similar.
- Hint: Check out the other cases in
- Write a normal case parsing test and a malformed parsing test for test bindings by filling out the provided test templates "test binding parsing" and "test binding parsing malformed".
-
Implement test bindings by fixing the relevant TODO in
interpret_binding
ininterpreter.ml
. - Ensure the provided "simple test binding" and "failing test binding" tests pass.
At this point, you may want to read through the rest of the provided tests and play with the language a bit.
Find and explain a difference between your Java and OCaml implementations of Trefoil v2
- You now have implemented Trefoil v2 twice, in two very different metalanguages. Chances are, your two implementations are not perfectly equivalent.
-
Find a difference in behavior between your two implementations.
- If you are stuck, we have discussed a few examples in class.
- Error messages
- Integer overflow.
- What happens when you write an infinite loop by accident.
- Try to make the difference interesting and not on the list above if you can!
- If you are stuck, we have discussed a few examples in class.
- Write an example Trefoil v2 program that demonstrates the difference.
-
Explain the difference and your example program in
FEEDBACK.md
.
Part 2: Implement new Trefoil v3 features
Trefoil Symbols and Structural Equality
- Familiarize yourself with the syntax and semantics of Trefoil-symbol literals in
LANGUAGE.md
. - Check out the provided AST declaration for symbols in
ast.ml
.- See the
Symbol
constructor of theexpr
type.
- See the
- Check out the provided parsing code in
expr_of_pst
inast.ml
-
Implement symbols in
interpret_expr
ininterpreter.ml
. -
Implement the new extended semantics of
=
.- Hint: This involves only deleting code from the starter code!!!
-
Near the top of
FEEDBACK.md
, answer the question about structural equality.
cond
.
Finish the implementation of - Familiarize yourself with the syntax of
cond
inLANGUAGE.md
. - Review the
Cond
constructor ofexpr
inast.ml
and be sure you understand how it corresponds to the syntax ofcond
described inLANGUAGE.md
. -
Parse
cond
by completing the TODO inexpr_of_pst
inside of theclause_loop
helper function. - Test the normal case and malformed case of parsing by filling out the test templates "cond parsing test" and "cond parsing malformed". Ensure these tests pass.
- Familiarize yourself with the semantics of
cond
inLANGUAGE.md
. -
Interpret
cond
by completing the TODO ininterpret_expression
in theloop
helper function inside theCond
case. - Ensure the provided "sum cond" test passes.
Add struct bindings.
- Familiarize yourself with the syntax of struct bindings in
LANGUAGE.md
. - We have already declared a constructor of
binding
for structs, calledStructBinding
, using the record typestruct_binding
. Be sure you understand how it corresponds to the syntax. -
Parse struct bindings by fixing the TODO in
binding_of_pst
.- Hint: Broadly, parsing structs is similar parsing function definitions, except that there are fewer parentheses and no function body expression.
- Hint: Use
check_symbols
to convertfield_names
to a list of strings. - Hint: Use
has_duplicates
to check if the resulting list of field names has any duplicates. If it does, report an abstract syntax error.- Note: unlike functions, which cannot have the same name as one of their
arguments, a struct is allowed (according to
LANGUAGE.md
) to have the same name as one of its fields. So the list you pass in tohas_duplicates
will be slightly different than how the starter code handles function bindings.
- Note: unlike functions, which cannot have the same name as one of their
arguments, a struct is allowed (according to
- Write a normal case parsing test and a malformed parsing test for struct bindings by filling out the provided test templates "struct binding parsing" and "struct binding parsing malformed".
- Familiarize yourself with the semantics of struct bindings in
LANGUAGE.md
.- Eventually we need to extend the dynamic environment with an entry for the struct binding, a function entry for the predicate function, and function entries for each field accessor function.
- Check out the case for
StructBinding
ininterpret_binding
. -
Fix the first TODO in this case by adding a mapping to the dynamic
environment for the struct binding as a whole, using
StructEntry
. - Now that the struct's name is bound in the dynamic environment, let's use it to implement the "constructor" functionality.
- Review the (internal) syntax of
StructValue
inLANGUAGE.md
and the corresponding AST node in theexpr
type.- Since users cannot write
StructValue
in their programs, there is no need to parse it.
- Since users cannot write
- Review the changed semantics of function calls in
LANGUAGE.md
. -
Back in the function call case in
interpret_expression
, fix the "part 2" TODO about "calling" a struct by following the semantics.- Hint: Be sure you signal a runtime error if the number of argument values is different than the number of fields of the struct.
- Ensure the provided "struct mynil constructor" and "struct mycons constructor" tests pass.
- Next, let's tackle struct predicate functions.
- Review the (internal) syntax of
StructPredicate
inLANGUAGE.md
-
Define an AST node for
StructPredicate
in theexpr
type.- Since users cannot write
StructPredicate
in their programs, there is no need to parse it.
- Since users cannot write
- Review the semantics of
StructPredicate
inLANGUAGE.md
-
Implement the semantics of
StructPredicate
ininterpret_expression
.- You cannot test your implementation until you also implement predicate function generation in the semantics of struct bindings. Let's do that now.
- Review the semantics of struct bindings again, this time focusing on how struct predicate functions get generated.
-
Back in the
StructBinding
case forinterpret_binding
, generate a function binding for the struct's predicate function.- Hint: The function's definition is described in
LANGUAGE.md
"as if by the binding ..." with a binding that is not valid user-facing syntax because it usesStructPredicate
. So, you cannot use the parser to generate this binding, and you must instead construct it manually using the AST constructors for function bindings,StructPredicate
, and variables.
- Hint: The function's definition is described in
- Ensure the provided test "struct mynil mycons predicate" passes.
- Finally, we will add struct field accessors. Review the syntax of
StructAccess
inLANGUAGE.md
. -
Define an AST node for
StructAccess
in theexpr
type.- Since users cannot write
StructAccess
in their programs, there is no need to parse it.
- Since users cannot write
- Review the semantics of
StructAccess
inLANGUAGE.md
-
Implement the semantics of
StructAccess
ininterpret_expression
.- Be sure you signal an error if the value is not built from the same struct name as the access.
- You cannot test your implementation until you also implement accessor function generation in the semantics of struct bindings. Let's do that now.
- Review the semantics of struct bindings again, this time focusing on how struct access functions get generated.
-
Back in the
StructBinding
case forinterpret_binding
, generate function bindings to access each of the structs fields.- Hint: We have provided starter code for a locally defined recursive helper
function that loops over the fields and calls
fun_entry_for_accessor
with the field name and field index. Implement this function by followingLANGUAGE.md
. As with struct predicates, since the AST nodes are "internal", you cannot use the parser to construct the binding. Call the AST constructors manually.
- Hint: We have provided starter code for a locally defined recursive helper
function that loops over the fields and calls
- Ensure the tests "struct mycons accessors" and "struct mycons accessors error case" pass.
-
Ensure the test "cond struct binding sum countdown" passes.
- This test uses
cond
as well as all three aspects of structs, so if you have a test failure here, be sure all the tests above are passing before debugging this one.
- This test uses
Add match expressions
- Skim the syntax of match expressions and patterns in
LANGUAGE.md
. - The starter code at the top of
ast.ml
implements an AST for patterns. We have provided AST nodes for wildcard patterns (_
) and cons patterns. There is also a parsing function as usual calledpattern_of_pst
, which we have implemented for the two provided patterns (plus given you some hints about other cases). - All of this provided code about patterns is currently unused in the starter code, because the only place patterns appear in Trefoil v3 is in match expressions, and the starter code does not include match expressions. Let's fix that now.
- Review the syntax of match expressions in detail in
LANGUAGE.md
. -
Declare a new AST node for match expressions by adding a constructor in
the
expr
type where the TODO is marked.- Hint: The AST node should be relatively similar to
Cond
, except that match takes a "scrutinee" expression as its first argument, in addition to a list of clauses. Also, a cond clause is two expressions, but a match expression is a pattern and an expression.
- Hint: The AST node should be relatively similar to
-
Add a case to
expr_of_pst
to parse match expressions.- Hint: It is relatively similar to parsing
cond
, with the differences noted above. Use a local recursive helper function to parse the list of clauses which is similar toclause_loop
incond
, except that it usespattern_of_pst
in one place instead ofexpr_of_pst
.
- Hint: It is relatively similar to parsing
- Test the normal case and malformed case of parsing by filling out the test templates "match parsing test" and "match parsing malformed". Ensure these tests pass.
- Skim the semantics of match expressions and patterns in
LANGUAGE.md
. - The semantics of patterns is implemented in
interpret_pattern
in the starter code. We have implemented the semantics of wildcard patterns and cons patterns for you. - Review the semantics of match expressions in detail in
LANGUAGE.md
. -
Implement match expressions in
interpret_expression
by fixing the TODO.- Hint: It is relatively similar to interpreting
cond
, with the differences noted above. After evaluating the scrutinee to a value, use a local recursive helper function to loop over the clauses. For each clause, check if the clause pattern matches the scrutinee's value by callinginterpret_pattern
. If yes, be sure to evaluate the right hand side of the clause in an appropriately extended environment. Unfortunately, you won't be able to test whether your environment is extended correctly until you add variable patterns.
- Hint: It is relatively similar to interpreting
- Ensure the provided tests "match expression with wildcards and cons 1" and "... 2" pass.
Add the remaining patterns
- The starter code only implements wildcard and cons patterns. Now let's add the rest.
- Skim the syntax of patterns again in
LANGUAGE.md
. - You can do them in any order, but here's one order.
-
Declare a pattern AST node for integer literal patterns by adding a
constructor to the
pattern
type inast.ml
.- Hint: The AST node will have the same arguments as the one for integer
literal expressions. That's because integer literals (also boolean
literals,
nil
, etc.) are syntactically valid as both expressions and patterns. Don't get confused though -- they have different semantics depending on whether they are an expression or pattern!
- Hint: The AST node will have the same arguments as the one for integer
literal expressions. That's because integer literals (also boolean
literals,
-
Parse integer literal patterns by fixing the first TODO in
pattern_of_pst
.- Hint: The starter code calls
int_of_string
for you. All you have to do is pass the result to your constructor.
- Hint: The starter code calls
- Skim the semantics of patterns in
LANGUAGE.md
.- In the semantics, patterns are described as returning "no" or "yes and
B
". In the code, we implement this using anoption
type, specificallydynamic_env option
None
in the code, and "yes andB
" corresponds toSome B
in the code, whereB
is implemented as a list ofstring * entry
, or in other words, a dynamic environment. - Review the semantics of integer literal patterns in detail.
- In the semantics, patterns are described as returning "no" or "yes and
-
Implement integer literal patterns in the interpreter by adding a case to
interpret_pattern
.- Hint: You should not need to change
interpret_expression
at all, onlyinterpret_pattern
.
- Hint: You should not need to change
- Ensure the provided tests "match expression with int literal patterns" and "match expression with int literal patterns and cons" pass.
- Boolean literal patterns
- Review syntax in
LANGUAGE.md
. -
Declare AST node in
pattern
type inast.ml
. -
Parse by adding cases in
pattern_of_pst
.- Hint: The starter code shows you where to add the case for "true". "false" is similar.
- Review semantics in
LANGUAGE.md
. -
Implement by adding a case to
interpret_pattern
ininterpreter.ml
. - Ensure provided tests "match expression with bool literal patterns 1" and "... 2" pass.
- Review syntax in
- Nil literal patterns
- Review syntax in
LANGUAGE.md
. -
Declare AST node in
pattern
type inast.ml
. -
Parse by adding a case in
pattern_of_pst
. - Review semantics in
LANGUAGE.md
. -
Implement by adding a case to
interpret_pattern
ininterpreter.ml
. - Write a simple test involving a nil pattern and ensure it passes.
- Review syntax in
- Trefoil symbol patterns
- Review syntax in
LANGUAGE.md
. -
Declare AST node in
pattern
type inast.ml
. -
Parse by fixing the corresponding TODO in
pattern_of_pst
. - Review semantics in
LANGUAGE.md
. -
Implement by adding a case to
interpret_pattern
ininterpreter.ml
. - Ensure provided test "match expression with symbol literal patterns" passes.
- Review syntax in
- Variable patterns
- Review syntax in
LANGUAGE.md
. -
Declare AST node in
pattern
type inast.ml
. -
Parse by fixing the corresponding TODO in
pattern_of_pst
. - Review semantics in
LANGUAGE.md
. -
Implement by adding a case to
interpret_pattern
ininterpreter.ml
.- Hint: This is the first pattern you implement where the bindings list is nonempty!
- Hint: Use the
VariableEntry
constructor.
- Ensure provided test "match expression with variable patterns" passes.
- Review syntax in
- Struct patterns
- Review syntax in
LANGUAGE.md
. -
Declare AST node in
pattern
type inast.ml
.- Hint: This is the most complicated kind of pattern in the language, because it has a list of sub-patterns. It also has a string (the name of the struct).
-
Parse by fixing the corresponding TODO in
pattern_of_pst
.- Hint: Parsing is challenging because of the list of sub-patterns.
- Hint: It's sort of similar to parsing a function call expression. You may
want to review that code in the starter code of
expr_of_pst
. - Hint: Use a locally defined recursive helper function to loop over the
children and call
pattern_of_pst
on each of them.
- Review semantics in
LANGUAGE.md
. -
Implement by adding a case to
interpret_pattern
ininterpreter.ml
.- Hint: Be sure to check the struct name strings are equal!
- Hint: The hard part again is the fact that the pattern has a list of sub-patterns, and the struct value has a list of sub-values. Use a local recursive helper functions to loop over these two lists "in lock step", matching each pattern against the corresponding value. Check that all the matches return "yes and ..." and append together all the bindings introduced.
- Ensure provided test "match struct binding" pass.
- Review syntax in
-
According to
LANGUAGE.md
, it is an error if any pattern in a match expression repeats a variable name. Fix this in your interpreter so that the test named "sum_with_match_error
" passes.- Hint: In
ast.ml
, write a functionvars_of_pattern
that takes a pattern and returns a list of strings containing all the variable names that appear anywhere in the pattern. - Hint: In
expr_of_pst
in the case formatch
expressions, first parse the pattern, then check thatvars_of_pattern
applied to the parsed pattern returns a list with no duplicates. (Usehas_duplicates
.)
- Hint: In
Part 3: Extending and using Trefoil v3
(Optional) Add your own feature to Trefoil v3
-
(Optional) Design and implement your own feature in Trefoil v3.
- Try to make your feature relevant to structs and/or pattern matching. Here are
some ideas.
- Add a new kind of pattern or improve match expressions.
- In OCaml, you can use an "or" pattern, like this line from the starter code
match e with | IntLit _ | BoolLit _ | Nil | StructConstructor _ -> e
- Think about what happens if any of the sub-patterns of an "or" pattern contain variable patterns.
- What about "and" patterns or "not" patterns.
- Add nicer syntax for matching on lists, sort of analogous to matching on
[p1; p2; p3]
in OCaml.- If you do this, you might as well add it as an expression as well.
- Add guards to pattern matching. In OCaml, you can write something like
match foo with | x :: xs when is_even(x) -> ...
x :: xs
matchesfoo
and ifis_even(x)
is truthy.- Think about what environment to evaluate the guard expression
(
is_even(x)
) in. - Think about whether this feature should be a new pattern or an
extension to the match expression. (What's the difference?)
- In OCaml, it's an extension to match expressions, which is fine. But it would be even more "compositional" if you make it a pattern, so that it can nest.
- Think about what environment to evaluate the guard expression
(
- In OCaml, you can use an "or" pattern, like this line from the starter code
- Improve struct bindings in some way.
- Make structs "generative".
- Currently, if the Trefoil programmer declares two different structs with the same name, then the interpreter can get confused because it uses the declared name to decide if the struct matches a pattern or if a field access should be allowed.
- "Generative" structs change the semantics of struct bindings so that each struct declaration is guaranteed to generate a separate type that can never be confused with another.
- Design and implement this feature.
- Start by writing a Trefoil program that declares two different structs with the same name but a different number of fields, and show that it can lead to confusing results where a struct predicate returns true but the struct accessor signals an error because the field index is out of bounds. After you implement generative structs, show that the struct predicate correctly returns false instead.
- Make structs "generative".
- Add predicate functions for all the built-in types:
int?
,bool?
,symbol?
.
- Add a new kind of pattern or improve match expressions.
- Or, if you have another idea unrelated to structs an pattern matching, go for it.
- We prefer that you don't "just" port your feature from HW3. Feel free to do that! But then extend it somehow.
- Try to make your feature relevant to structs and/or pattern matching. Here are
some ideas.
-
(Optional) Describe the syntax an semantics of your feature in
FEEDBACK.md
. - (Optional) Write a test that uses your new feature.
Write a substantial program in Trefoil v3
- Aim for 25 or more lines
- You are welcome to port an existing program from OCaml or Java.
- For example, you could solve some HW2 or HW4 problems in Trefoil.
- Or you could implement a tiny programming language in Trefoil.
- Or anything else you like.
- Try to do something that use structs or pattern matching in an interesting way.
- Or, use a feature you added in an interesting way.
-
Put your program in a file in this folder with a name ending in
.trefoil
and submit it with the rest of your code. -
Tell us the name of your file in
FEEDBACK.md
so we know where to look. -
Describe your program in
FEEDBACK.md
Grading
330 "syllabus points" are available on this homework.
We try to give a detailed points breakdown below, but your homework will be graded manually, and we judge your work holistically as well. In practice this means you can get some points for doing well overall even if you mess up one part.
- 80 points for part 1
- 20 points for declaring AST node, parsing, testing parsing, and interpreting
let
expressions. - 20 points for implementing function calls
- 20 points for declaring AST node for, parsing, testing parsing, and
interpreting
test
bindings. - 20 points for finding and explaining a difference between your two Trefoil v2 implementations, and for giving an example Trefoil program illustrating the difference.
- 20 points for declaring AST node, parsing, testing parsing, and interpreting
- 160 points for part 2
- 20 points for implementing the new semantics for
=
and answering the question inFEEDBACk.md
. - 10 points for parsing, testing parsing, and interpreting
cond
. - 10 points for parsing, testing parsing, and mapping the struct name in the dynamic environment for struct bindings.
- 10 points for implementing "calling a struct" to call the constructor.
- 20 points for declaring the AST node, interpreting, and generating the
user-facing wrapper binding for
StructPreciate
. - 20 points for declaring the AST node, interpreting, and generating the
user-facing wrapper binding for
StructAccess
. - 30 points for declaring, parsing, testing parsing, and interpreting match expressions.
- 10 points for declaring, parsing, and interpreting integer literal patterns.
- 10 points for boolean literal patterns, nil literal patterns, and trefoil symbol patterns.
- 10 points for variable patterns.
- 10 points for struct patterns.
- 20 points for implementing the new semantics for
- (Optional) Up to 40 points for designing, implementing, and describing your new feature.
- 60 points for writing a substantial program in Trefoil and describing it in
FEEDBACK.md
. - 30 points for filling out the "Feedback" section of
FEEDBACK.md
Total points: 370, of which 330 are "on the syllabus" and 40 are optional/extra.
Submission
Submit your code to the HW5 assignment on Gradescope. If possible, we prefer you submit via the Gitlab button. See the instructions.