Interpretive Dance
- Policy: Pair practice
- Partner search: Find a partner here.
-
Code:
cs251 start
- Submit:
git commit
,cs251 sign
, andgit push
your completed code. - Reference:
Contents
Time Info
In Fall 2019, students self-reported spending a median of 13.0 hours on this assignment.
Compared to that assignment, the implementation of one previously required task is now provided.
Required Tasks
In this assignment, you will complete several parts of the implementation of a familiar-looking programming language by completing several gaps in a mostly complete programming language interpreter implementation.
Prepare carefully; use the workshop session well.
The interpreter implementation tasks will require careful reading and understanding of provided code (and text). You will think carefully about what to write, but you will write likely fewer than 50 new lines of code in total for the required parts of the assignment, despite the provided code being much larger than that.
One class meeting will be a workshop session in the lab (see the calendar) to ensure everyone has time to ask some questions while working on the assignment, even if you are not regularly able to catch drop-in hours. To make that session most useful to you, you must start work on the assignment ahead of the workshop so that you are familiar with the provided code and ready to ask informed questions. You should complete tasks 1 and 2, and ideally, attempt tasks 3 and 4.
1. Learn the MAP
and ENV
Abstract Data Types
MAP
and ENV
signatures are provided and documented in env.sig
.
Maps map keys to values. Environments are a form of mapping data
type we have used to define the semantics of programming languages. We
separate the notion of more basic MAP
and the extended notion of an
ENV
. In this assignment, you use a straightforward version of the
full ENV
abstract data type. A future assignment will require you to
implement alternative MAP
implementations
Read the Env
structure in the file env.sml
. It ascribes the ENV
signature (defined in the file env.sig
). This module structure
represents maps by association lists, a simple data structure we
have worked with in multiple past assignments. Practice a few uses of
the provided environment data type to understand how it works. Here
are some suggested experiments to try at the REPL.
val env0 = Env.empty;
Env.lookup "x" env0; (* It's OK to get a "type vars not generalized" warning *)
val env1 = Env.bind ("x", 7) env0;
Env.lookup "x" env1;
val env2 = Env.bind ("y", 8) env1;
Env.lookup "x" env2;
Env.lookup "y" env2;
Env.lookup "x" (Env.unbind "x" env2);
val env3 = Env.make [("y", 4),("x", 5),("x",2)];
Env.lookup "x" env3;
print (Env.show env3);
Env.replay (op::) [] env3;
To simplify testing, we have provided an Env
structure that uses
open ascription of the ENV
signature, meaning it is possible to
see or use the internals directly.
Temporarily, try changing the structure Env : ENV =
line to
structure Env :> ENV =
. (Change the open ascription :
to closed
ascription :>
.) Write answers to these questions in a comment at the
end of the file env.sml
.
- Retry the experiments you tried before closing the ascription. How does the
REPL act differently when manipulating values of type
Env.t
with closed signature ascription vs. open ascription? - Give an example expression involve at least one function from the
Env
structure that type-checks with open ascription, but does not type-check with closed ascription.
When you are done with part, it is a good idea to revert to open ascription to simplify testing in the rest of the assignment.
This data structure will power the core of your interpreter (next part of the assignment), so be sure to understand it well.
2. Explore the SMiLe Language and Interpreter
The tasks in this assignment step you through the implementation of several features in an interpreter for the SMiLe language, a made-up language similar to Standard ML. SMiLe’s syntax is a subset of the SML syntax; SMiLe’ semantics is a subset of the SML semantics, with the key exception that it uses dynamic type-checking instead of static type-checking.
Tips for working effectively:
- Follow through the tasks in order. They build on each other.
- Your job here is to take evaluation rules (or other definitions) that we have written in English or formal semantics and translate them – usually in the most direct way possible – to SML code. Some rules are specified directly in the assignment; look up and review the rest.
- The focus is on understanding and applying the evaluation rules concretely. While you will need to understand the rules and the representation of programs clearly, programming the rules is generally straightforward once you understand those things. This programming is not about being “clever” in how you program things; it’s about translating the rules directly and cleanly.
- Believe in the power of ML pattern matching!
- Test everything. A lot. Write your tests in files so you can rerun them easily.
- Commit and push early and often.
Informal SMiLe Language Definition
Concrete Syntax
Your SMiLe interpter will manipulate Abstract Syntax Trees (ASTs) representing SMiLe programs as ML data structures. The concrete syntax defined here makes it easier to write SMiLe programs. We provide a parser that translates the following concrete syntax to ASTs.
We use grammar in a form similar to Extended Backus Normal Form
(EBNF) to
describe the concrete syntax of SMiLe programs. Non-terminal symbols
are unquoted, e.g., expr
. Terminal symbols are quoted, e.g., 'val'
or '+'
. Non-terminal symbol definitions use =
and separate their
productions by |
, with the list of productions terminated by :
The
notation ( )*
indicates that whatever sequence of symbols appears
inside the parentheses can be repeated zero or more times.
The grammar below is a fairly close match for our abstract syntax
representation. Since this grammar is ambiguous, the parser enforces
precedence and associativity rules. Since the details of the concrete
syntax are not the focus of this assignment, it suffices to understand
that the precedence rules match those of SML. (Take CS 235 and CS 301
to learn more.) Four non-terminal symbols in this grammar are not
defined: int
refers to any non-negative integer literal; string
refers to any double-quoted string literal; var
refers to any valid
SML identifier starting with a lower case letter; tag
refers to any
valid SML constructor name staring with an upper case letter.
program = binding program
| binding ;
binding = 'val' pattern '=' expr
| 'fun' var funcases ;
funcases = pattern '=' expr '|' funcases
| pattern '=' expr ;
pattern = '_' | '()' | 'true' | 'false' | int | string
| '(' ( pattern ',' )* pattern ')' | tag pattern ;
expr = '()' | 'true' | 'false' | int | string
| unop expr | expr binop expr
| '(' ( expr ',' )* expr ')' | '[' ( expr ',' )* expr] | '[]' | tag expr
| 'if' expr 'then' expr 'else' expr
| 'fn' cases | expr expr | var
| 'let' program 'in' expr 'end'
| 'case' expr 'of' cases ;
unop = 'not' | '~' ;
binop = '+' | '-' | '*' | 'div' | 'mod' | '^'
| '=' | '<>' | '<' | '<=' | '>' | '>=' | '::'
| 'andalso' | 'orelse' ;
cases = pattern '=' expr '|' cases
| pattern '=' expr ;
Here is an example SMiLe program defining and using a factorial function:
fun fact x =
if x <= 1
then 1
else x * (fact (x-1))
val fact5 = fact 5
val fact6 = fact 6
Yep, it looks just like an SML program.
Type-checking and Evaluation Rules
The dynamic evaluation rules of SMiLe are the same as those of SML (for the subset of the language SMiLe includes), except that SMiLe performs dynamic type-checking. SMiLe is a dynamically typed language, so there is no static type checking of SMiLe programs.
Summary of Differences with SML
- Type-related syntax is not supported. As a result, datatypes and constructors are never declared. Just use constructors (called “tags” in our implementation) as if already declared. SMiLe constructors are not first-class. (They can be used as values.)
- Records and record/tuple accessors (
#1
, etc.) are not supported. Use pattern-matching to extract tuple parts. - Mutual recursion with
and
is not supported. - Real numbers are not supported.
- The sequencing expression
;
is not supported. Uselet
instead. - There is no standard basis library. Everything must be written from scratch.
Many SML syntax features are available, including:
- Multi-branch pattern-matching and curried form in function bindings.
- Left-associative function application.
- Syntactic sugar for lists, including
[]
and::
. - Full patterns in any binding.
Run the SMiLe Interpreter
In your local working copy of the assignment repository, run ./smile
help
to see how to invoke various features of the SMiLe
interpreter. Since you must implement several parts to complete the
interpreter, many of the ./smile
commands will not do anything
useful yet. However, you can run any command on the reference
implementation (a completed implementation, without source code) of
the interpreter by prefixing the command with ref
. Note: the
reference interpreter is compiled for the computing environment on the
CS Linux machines; it may not work elsewhere.
Interactive REPL
To start a SMiLe REPL on the reference interpreter:
$ ./smile ref
(-: SMiLe :-)
Type Control-D to exit
=) fun fact n = if n <= 1 then 1 else fact (n - 1)
:) val fact = <closure for fact>
=) val fact5 = fact 5
:) val fact5 = 120
=) val fact6 = fact 6
:) val fact6 = 720
=) ^D
(-: SMiLe :-)
Unlike the SML/NJ REPL:
- The SMiLe REPL does not support multi-line inputs and SMiLe does not
support semicolon (
;
). - The SMiLe REPL supports only bindings, not stand-alone expressions.
Entering
fact 5
alone as a line in the REPL session would not parse. Always use aval
orfun
binding. - The SMiLe REPL does not support
use
.
Source Files
SMiLe source code files use the extension .sml
. To run a full
SMiLe program from a file with the reference interpreter:
$ ./smile ref examples/fact.sml
(-: SMiLe :-)
:) val fact = <closure for fact>
:) val fact5 = 120
:) val fact6 = 720
(-: SMiLe :-)
Additional ways of interacting with the interpreter internals will be introduced with specific tasks below.
Practice Writing SMiLe Programs
Write a few SMiLe functions and test applications of those
functions in a file examples/practice.sml
, then run them with the
SMiLe reference interpreter. Do try this, but do not spend too
long on it.
-
Write an association list
lookup
function (you should be able to copy this fromenv.sml
) and apply it to an association list[("y", 4),("x", 5),("x",2)]
. The result of running the reference interpreter with this file should be something like:(-: SMiLe :-) :) val lookup = <closure for lookup> :) val assocs = [("y", 4), ("x", 5), ("x",2)] :) val lookup_x_assocs = (SOME 5) (-: SMiLe :-)
- Write a list
append
function in curried form using patterns directly in the function definition (nocase
expressions). Remember to parenthesize the argument patterns. Applyappend
to a pair of lists. - Copy some of your
bintree
orbst
functions and call them on the sample trees from the previous assignment. Remember, SMiLe does not support static types or type declarations, so just go ahead and use the tree constructors without declaring them.
Skim the Interpreter Code
We provide the code of the reference interpreter, but with several
core features removed. Tasks below will guide to implement the missing
features by editing the eval.sml
, sugar.sml
, and scope.sml
files. Take a quick look through the following files to get your
bearings.
Key files that you will use or edit:
ast.sml
: the definition of Abstract Syntax Trees (ASTs) for the SMiLe language (SmileAst
structure), annotated with their correspondence to the concrete syntax, plus functions to pretty-print SMiLe ASTs in the concrete syntaxsugar.sig
(complete) andsugar.sml
(partial): static desugaring operations to simplify syntactic sugar in SMiLe ASTs (SmileSugar
structure)env.sig
(complete) andenv.sml
(complete): implementation of environments, used by the interpreter (Env
structure)eval.sig
(complete) andeval.sml
(partial): evaluation of SMiLe programs and ASTs (SmileEval
structure)
Supporting files that you will use but generally do not need to understand:
parser
(complete): the parser that translates programs from strings to ASTs (SmileParser
structure). Ignore unless you are curious. (Take CS 301 for more on how this works.)smile
(complete) andmain.sml
(complete): command-line interface to the SMiLe interpreter (SmileMain
structure)smile.sml
(complete): top-level SMiLe interpeter operations (Smile
structure)
Files supporting testing:
unit-tests
(add more): tests of interpreter functions that act by calling those ML functions directlyexamples
(add more): example SMiLe programs that also act as full-interpreter teststest.sml
(add more): testing infrastructure for running full-interpreter tests (SmileTest
structure)
Files supporting optional tasks:
scope.sig
(complete) andscope.sml
(partial): static scope-checking for SMiLe ASTs (SmileScope
structure)
The basic operation of the interpreter goes like this:
+-------------+ +---------------+ +-------------+ +------------+
| (complete) | | (partial) | | (optional) | | (partial) |
source.sml ----> parse ----> AST ----> desugar ----> AST ----> check ----> AST ----> eval ----> output
| | | Task 3 | | | | Tasks 4-6 |
+-------------+ +---------------+ +-------------+ +------------+
- Parsing: The parser reads in a SMiLe source file, e.g.,
source.sml
, and converts it to an AST. - Desugaring: The desugarer (required Task 3, optional Task 8) takes that AST, makes some additional simplifications, and returns a new transformed AST.
- Checking: The optional static scope checker (optional Task 7) takes the desugared AST and checks that all variable uses are well-scoped.
- Evaluation: The evaluator (required Tasks 4-6) takes the desugared AST and evaluates it, producing an output or result.
On Metaprogramming
Metaprograms are programs written in an implementation language whose inputs or outputs are programs in a source or target language. Implementing a source language using a similar implementation language, as we are implementing the source language SMiLe with a metaprogram in the implementation language SML, has benefits and pitfalls.
- The most visible pitfall is potential confusion between the source
language features you are implementing and the implementation
language features you are using in the implementation.
- It helps to prefix each feature name by the language name when you speak about it. “To implement SMiLe pattern-matching I need to ML pattern-match on the ML value representing a SMiLe pattern to determine what kind of SMiLe pattern it is.”
- It can also help to use this naming convention in code, e.g., an
ML variable called
smile_pattern
hold an ML value of typeSmileAst.pattern
that represents a SMiLe pattern.
- On the other hand, this forces you to be vigilant in keeping the source-vs.-implementation distinction straight, helping you learn to think clearly about the distinction between the source language and its implementation.
- Implementing many of the evaluation rules of SML as evaluation rules for SMiLe will help you understand SML more clearly (I’m looking at you, pattern-matching!) and improve your SML and general programming skills.
3. Abstract Syntax Trees and Syntactic Sugar
In this part you will explore the ML representation of SMiLe
programs as Abstract Syntax Trees (ASTs) and desugaring of SMiLe
programs. You will read the file ast.sml
in detail and edit the
file sugar.sml
. You will submit the code that you write for this
task, but you do not need to submit answers to the other
questions. They push you to understand key features of the AST
representation before you start programming with it.
-
Read
ast.sml
to familiarize yourself with the Abstract Syntax Tree representation of SMiLe programs defined in theSmileAst
structure in that file. ML datatypes and pattern-matching are beautifully suited to this sort of task. Roughly: one ML type corresponds to one non-terminal in the (simplest) SMiLe grammar; each constructor/tag of this type corresponds to one production of that non-terminal. The ML keywordand
supports mutually-recursive datatypes or functions that refer to each other. -
Inspect ASTs: Run the command
./smile ast examples/fact.sml
to display the AST of the SMiLe program defined inexamples/fact.sml
.- Read the “raw” version of the AST, represented using the
SmileAst.program
type. - For each ML datatype constructor appearing as a node in the raw
AST, determine what part of the concrete syntax of the source
program
examples/fact.sml
corresponds to that node in the AST.
- Read the “raw” version of the AST, represented using the
-
Design question: Why are SMiLe tuple expressions represented with by an ML list of
SmileAst.expr
instead of an ML tuple ofSmileAst.expr
? -
Desugaring in the parser: The provided parser for SMiLe’s concrete syntax does some desugaring before constructing ASTs.
- Examine the provided SMiLe file
examples/lists.sml
and raw AST from the output of./smile ast examples/lists.sml
. - Find two or three syntactic constructs (at least one in
fun
bindings and one in constructors/tags) used in these programs that are not representable directly by our ASTs. Describe how the parser appears to rewrite each syntax to fit our AST representation.
- Examine the provided SMiLe file
-
Desugaring ASTs after parsing: The
SmileSugar
structure insugar.sml
implements functions for additional desugaring of ASTs by replacing all occurrences of AST nodes with combinations of other nodes that have equivalent behavior.- Read the implementations of the
SmileSugar
functions. - Change the
desugar_expr
function to replaceIfThenElse
AST nodes withCase
AST nodes that encode the same SMiLe computation as the originalIfThenElse
. The interpreter implementation will not handleIfThenElse
during evaluation. - Confirm the validity of your transformation by running
./smile ast examples/fact.sml
and inspecting the difference between the raw and desugared ASTs as well as the “pretty-printed” desugared AST, which is created by callingSmileAst.show desugared_ast
. The resulting desugared program should behave just like the original.
- Read the implementations of the
-
git add
any new files, thengit commit
andgit push
your work! It’s a good backup in case things go wrong later.
4. Implement Evaluation Rules of Basic SMiLe Features
In this part you will explore the ML SmileEval
structure in
eval.sml
, which contains code for evaluating SMiLe programs
represented as ML SmileAst.program
values, and extend it by
implementing evaluation rules for a couple basic SMiLe language
features: structural equivalence checks (used by SMiLe =
and
SMiLe pattern-matching) and evaluation rules for SMiLe tuple
expressions.
-
Warmup: Read
eval.sml
up through the definition of theenvironment
type. Consider the following questions:- How do values built by the
ClosureValue
constructor differ from the closures we have drawn in environment diagrams? Speculate on reasons for these differences. - Why is the function name represented by a
var option
instead of just avar
?
Consider what you learned about creating cyclic structures in past reading about Lisp. You do not need to submit answers to these questions.
- How do values built by the
-
Warmup: Read the definition of
eval_expr
ineval.sml
. Read several cases fromeval_expr
to get a feel for how the SMiLe evaluator works and inventory which kinds of SMiLe expressions are currently supported and which are not.With your
Env
structure implemented, you should be ready to run the simplest of SMiLe programs with your interpreter, even though it is missing several features. Try./smile examples/simple.sml
.$ ./smile examples/simple.sml (-: SMiLe :-) :) val x = 5 :) val y = 10 :) val z = false (-: SMiLe :-)
-
Implement the helper function
SmileEval.eq : value * value -> bool
to compare the two SMiLe values, represented as MLSmileEval.value
s, recursively using deep structural equivalence. This function is used to implement the SMiLe=
operator (as well as part of pattern matching).- Two non-closure SMiLe
value
s are equivalent if and only if they have the same SMiLe type (represented by an ML constructor) and their corresponding sub-values are equivalent. SmileEval.eq
returns false if any of its arguments contains aSmileEval.ClosureValue
representing a SMiLe closure. If not for this restriction, it would be possible to implementeq
using ML=
. Use ML pattern-matching in a recursive ML function instead.
Test your function on several cases. To do so, use
./smile meta
to run an SML session with the SMiLe libraries loaded. You are encouraged to write down tests in files so that they can be run repeatedly. In the./smile meta
SML session, you canuse "unit-tests/test-eq.sml";
or you can simply run./smile meta < unit-tests/test-eq.sml
. We have provided a few starter tests in the fileunit-tests/test-eq.sml
. You should add more!Debugging: If you wish to experiment interactively with the internals of your implementation, you can use the
./smile meta
session (an SML REPL) without any file. You may wish to repeat some of the lines from the top of ourtest-eq.sml
file toopen
various structures and import their names (otherwise you must use, e.g.,SmileEval.eq
) and to set the maximum depth and length of values that the SML REPL will print when showing results (seeControl.Print....
).You should now also be able to run your interpreter on SMiLe programs that use the
=
operator. Write and test some additional programs inexamples
that use just the basics available so far.$ ./smile (-: SMiLe :-) Type Control-D to exit =) val yes = 3 + 4 = 5 + 2 :) val yes = true =) val no = 3 + 4 = 7 + 8 :) val no = false =) ^D (-: SMiLe :-)
- Two non-closure SMiLe
-
Implement the evaluation rule for SMiLe tuple expressions (represented by
SmileAst.Tuple
AST nodes) inSmileEval.eval_expr
.- Evaluating a SMiLe tuple expression evaluates each SMiLe subexpression in order to create a SMiLe tuple of values.
- Try to accomplish this in just one extra line of ML code. (Hint: think higher-order functions!)
Test your implementation by writing some unit tests for
SmileEval.eval_expr
inunit-tests.sml/test-eval_expr.sml
, copying the general approach of the unit test file foreq
. Run these tests with./smile meta < unit-tests/test-eval_expr
.You should now also be able to run your interpreter on SMiLe programs that use tuples.
./smile examples/tuples.sml
. Write and test some additional programs inexamples
that use just the basics available so far.$ ./smile examples/tuples.sml (-: SMiLe :-) :) val t1 = (7, ("hello world", true)) :) val t2 = ((7, ("hello world", true)), 251, (7, ("hello world", true))) :) val not_same = false :) val same = true (-: SMiLe :-)
-
git add
any new files, thengit commit
andgit push
your work! It’s a good backup in case things go wrong later.
5. Implement SMiLe Pattern Matching
In this part you will implement pattern matching support for SMiLe
case
expressions. A SmileAst.Case
AST node carries a
SmileAst.expr
, e
, representing the object (target) to evaluate and
match against a (SmileAst.pattern * SmileAst.expr) list
representing
the branches of the SMiLe case
expression. You will implement
pattern-matching support with two SML functions, SmileEval.match
and
SmileEval.eval_cases
, that will require about 25-40 lines
altogether, but they both consist of a collection of small simple
cases. This is the largest single programming task in this
assignment.
-
Review the
Case
branch of theSmileEval.eval_expr
function. Note the arguments it passes toSmileEval.eval_cases
.eval_cases
does the work of search for the first matching SMiLe pattern and evaluating the associated SMiLe expression. To check whether a SMiLe matches a SMiLe value and extract any SMiLe bindings this match introduces,eval_cases
uses the helper functionSmileEval.match
. -
Implement
SmileEval.match
, a recursive function that checks to see if aSmileEval.value
matches a singleSmileAst.pattern
. Thematch
function takes four arguments in curried form:- The ML function to call to add new bindings to a SMiLe
environment
. - The SMiLe
value
to match against. - The SMiLe
pattern
to try. - The SMiLe
environment
to extend.
If the SMiLe
value
and SMiLepattern
match (as defined below),match
returnsSOME
of anenvironment
that is the initial environment (as passed tomatch
), extended with all the bindings the SMiLe pattern-match introduces. If there is no match,match
returnsNONE
.The implementation should use ML pattern-matching to ML-pattern-match simultaneously on an ML constructor for a
SmileEval.value
and an ML constructor for aSmileAst.pattern
.Given a SMiLe
value
,v
, and a SMiLepattern
,p
, eitherp
matchesv
or it does not. If they match does, the match returns anenvironment
extended with newSmileAst.var * SmileEval.value
bindings. The order in which bindings are added in a match does not matter. Assume that one pattern binds each possible variable at most once. In other words, no pattern will includeVarPattern "x"
more than once, so shadowing and binding order are not issues.The rules for SMiLe pattern matching should be unsurprising:
Wildcard
matches everything and produces no bindings.VarPattern s
matches any valuev
and bindss
tov
.UnitPattern
matches onlyUnitValue
and produces no bindings.BoolPattern false
matches onlyBoolValue false
and produces no bindings (and similarly fortrue
).IntPattern 17
matches onlyIntValue 17
and produces no bindings (and similarly for other integers).StringPattern "smile"
matches onlyStringValue "smile"
and produces no bindings (and similarly for other strings).TuplePattern ps
matches a value of the formTupleValue vs
ifps
andvs
have the same length and each element ofps
matches the corresponding element ofvs
. The match introduces all bindings introduced by all elementwise submatches.TagPattern (s1,p)
matchesTaggedValue(s2,v)
ifs1
ands2
are the same ML string (via ML=
) andp
matchesv
. The match introduces all bindings introduced by the match ofp
andv
. We call the stringss1
ands2
the constructor name or tag name.- Nothing else matches.
Use
bind
(the argument) instead ofEnv.bind
when you need to introduce new bindings into an environment. This allows the caller ofmatch
to decide whether to use normalEnv.bind
or a version that additionally prints the bindings it introduces.Test your
match
implementation by creating atest-match.sml
with some test cases (test-eq.sml
is a great template; change the inputs) and running it with./smile meta < unit-tests/test-match.sml
. - The ML function to call to add new bindings to a SMiLe
-
Implement
eval_cases
, a function that takes three arguments:v : value
, the SMiLe value to match against.cases : (pattern * expr) list
, the ordered list of pairs of SMiLe pattern and corresponding SMiLe result expression.env : environment
, the environment to extend with match bindings and use to evaluate the chosen result expression.
eval_cases
iterates through the list ofcases
in order, applyingmatch
to the argumentsEnv.bind
,v
, eachSmileAst.pattern
, and the givenenv
, until it finds aSmileAst.pattern
that matchesv
.- For the first
SmileAst.pattern
that matchesv
,eval_cases
evaluates the associatedSmileAst.expr
from the list, using the match environment returned bymatch
(which isenv
extended with all bindings introduced by the match). - No
pattern
s after the first match are considered. At most oneexpr
is evaluated. - If no pattern matches, call
SmileEval.match_error
to report the error.
Test your
eval_cases
implementation by creating atest-eval_cases.sml
with some test cases (test-eq.sml
is a great template; change the inputs) and running it with./smile meta < unit-tests/test-eval_cases.sml
.You should also be able to run concrete source programs that use
case
expressions. Try./smile examples/pat.sml
and write more of your own.$ ./smile examples/pat.sml (-: SMiLe :-) :) val x = 10 (-: SMiLe :-)
-
git add
any new files, thengit commit
andgit push
your work! It’s a good backup in case things go wrong later.
6. Implement SMiLe Bindings and Function Application
-
Implement
SmileEval.eval_binding
: ImplementVal
andFun
bindings ineval_binding
.SmileEval.eval_binding
takes a function for adding bindings to environments, a SMiLebinding
, and the current SMiLeenvironment
. It evaluates the SMiLe binding, and returns the SMiLe environment extended by the binding. This requires 5-10 lines of ML code, most forVal
bindings.-
SMiLe
val
bindings evaluate their SMiLe expression, then match their SMiLe pattern against the result value. If the result matches the pattern, any bindings from the match are added to the current environment. Use thematch
function to your benefit. -
SMiLe
fun
bindings introduce a binding from the function’s name to a SMiLe closure value for the function.
Use
bind
(the argument) instead ofEnv.bind
when you need to introduce new bindings into an environment or pass a binding function to a helper function. This allows the caller ofeval_bindings
to decide whether to use normalEnv.bind
or a version that additionally prints the bindings it introduces. -
-
Read and describe (in comments with the code) how the
SmileAst.Let
case inSmileEval.eval_expr
and the top-levelSmileEval.eval_program
work. Notice that these two are highly similar. -
Implement SMiLe function application (call) expressions: Review the evaluation rule for function application and implement it in the
SmileAst.Apply
case ofSmileEval.eval_expr
. If something other than aClosureValue
is encountered where you expect the closure, use the functiondynamic_type_error
to report it.Recall that SMiLe functions all support multi-branch definitions. Rather than a single argument and a single body expression, SMiLe functions have lists of pairs of
SmileAst.pattern
and associatedSmileAst.expr
, matching the multi-branch function defintions familiar from ML. This means that evaluating a function call requires pattern-matching to choose the right branch and binding variables in the pattern to parts of the argument value. This should feel highly similar to the treatment ofSmileAst.Case
expressions!An important distinction with our earlier graphical representation of closures is that SMiLe closures in this implementation carry an environment that does not bind the original name of the function. (That would require mutation and we can get away without it!) Instead, a function’s name is carried separately in the closure. To support recursive functions, you must ensure that the function’s name (if any) is bound in the environment where you evaluate its body. (Use
Env.bind
.)Testing: At this point, your interpreter should be ready to run all valid SMiLe programs. You can test individual programs with
./smile examples/fact.sml
, for example, or run a suite of full-interpreter tests with./smile test
to test all of the example programs in theexamples
directory. You should also write some of your own test programs. The provided examples are by no means exhaustive. Often you will be able to repurpose older SML code for this, removing use of any types/exceptions to work with SMiLe. See how much of prior assignments you can get running. Write new code. Go wild!8^)
-
git add
any new files, thengit commit
andgit push
your work! It’s a good backup in case things go wrong later.
Optional Tasks
Be sure to complete the required tasks and submit them before trying any of these optional extensions. These are here if you are curious; they are not intended for extra credit.
7. Static Scope Checking (optional)
Implement the SmileScope.freevars
function in scope.sml
. Its job
is to find all the unbound (or “free”) variables used in a SMiLe
program
AST. Once you have implemented this function any SMiLe
programs with scope errors will be rejected before they are sent to
the evaluator.
Your implementation will traverse the AST, tracking a simple static
environment of type (Ast.var, unit) Env.t
that tracks the set of
variables that will be bound in the current scope. As you descend
through a binding, you will grow this environment, binding any bound
variables to the unit value to indicate they are bound in the current
scope. When the functions reach lists of cases (pattern/expression
pairs), they should consider all possible cases.
Your implementation will follow a shape that is fairly similar to the
SmileEval
functions (and their supporting helpers), but
instead of evaluating and following just one path through the AST,
freevars
will perform one full tree traversal, visiting every part
of the tree once.
You will want to define ML helper functions to work over binding
s
and expr
s found in program
s. Use the ML and
form for mutually
recursive functions:
fun freevars prog = ...
and freevars_binding b env = ...
and freevars_cases cases env = ...
and freevars_expr e env = ...
While checking for free variables, also check for conflicting bindings
in a single pattern. Patterns should bind any given variable at most
once. A variable cannot appear twice in a pattern. For example, the
SMiLe (or SML) pattern (x,x)
is illegal. (As an AST, this would
be TuplePattern [VarPattern "x", VarPattern "x"]
.) Raise an
exception if you find conflicting bindings in a pattern.
8. A Simpler Language (optional)
In this part, you will explore one last instance of syntactic sugar.
You have already investigated several examples of desugaring in our implementation of the SMiLe language. If all instances of an expression form can be rewritten to other forms in the language to produce an equivalent program, we know that this form is “just syntax;” it does not contribute any “interesting” semantics to the language beyond the semantics of the other existing expression forms. We will soon take this to the extreme in class, showing that it is possible to implement any computation using no more than single-argument functions, function application, and literally nothing else. For now, we content ourselves with writing off a couple more essential expression forms in our language as mere syntactic sugar.
At this point we have seen that there are many similarities between
SMiLe/SML let
expresssions, case
expressions, and function
application expressions. Evaluation of each of these involves:
- evaluating one or more argument expressions, pattern-matching
against the result, and introducing bindings to parts (or the
entirety) of the result:
- in function calls: the argument expression
- in let expressions: the binding expressions
- in case expressions: the target expression
- and evaluating one “body expression” in some environment extended
with these bindings:
- in function calls: evaluate the body expression in the definition environment extended by the match
- in let expressions: evaluate the body expression in the current environment extended by the matches
- in case expressions: evaluate the expression following the matching pattern in the current environment extended by the match
Currently, the interpreter exploits these similarities to share significant code among the evaluation rules for these three expression forms. Nonetheless, the interpreter would be simpler if we had fewer evaluation rules.
Implement two of the three (let
, case
, or function application) as
sugar in terms of the third:
- Modify
SmileSugar.desugar_expr
to do the desugaring. - Modify
SmileEval.eval_expr
to eliminate the cases that have now been handled by desugaring.
9. More Ideas (optional)
-
Closure Size Optimization: Adapt your implementation of helper functions for
freevars
(functions that find free variables) to optimize the space used for function closures. Our current implementation of closures keeps around the entire environment, even the parts that the closure will never need. By runningfreevars_expr
on the body of a function starting from a static environment that contains only variables that will be bound as part of the function application evaluation rule, you can detect the free variables of the closure – those variables that may be referenced when no binding is in scope within the give expression. These are the only variables the closure could ever need from its environment. Thus, we might save space by storing an environment in the closure that binds only these variables. Do this. -
For a much larger challenge, add an ML datatype
SmileAst.typ
describing types of SMiLe expressions. Add aSmileTypeCheck
structure with atypecheck
function that takes aSmileAst.expr
, a static typing environment mapping variables to types, and a proposedtyp
for theexpr
, and decides whetherexpr
may have this type. Emulate the SML type system as closely as possible. Edit thesources.cm
file to include the new structure in the build. Not satisfied? Think about how type inference might work. I can share some literature on this if you are curious.
Submission
Remember to git add
any new files (such as tests) that you created.
Submit: The course staff will collect your work directly from your hosted repository. To submit your work:
-
Test your source code files one last time. Make sure that, at a minimum, submitted source code is free of syntax errors and any other static errors (such as static type errors or name/scope errors). In other words: the code does not need to complete the correct computation when invoked, but it must be a valid program. We will not grade files that do not pass this bar.
-
Make sure you have committed your latest changes.
$ git add ... $ git commit ...
-
Run the command
cs251 sign
to sign your work and respond to any assignment survey questions.$ cs251 sign
-
Push your signature and your latest local commits to the hosted repository.
$ git push
Confirm: All local changes have been submitted if the output of
git status
shows both:
Your branch is up to date with 'origin/master'
, meaning all local commits have been pushednothing to commit
, meaning all local changes have been committed
Resubmit: If you realize you need to change something later, just repeat this process.