Interpretive Dance
- Assign: Friday, 18 October
- Due: 11:59pm Tuesday, 29 October
- Policy: Pair graded synthesis assignment
- Partner search: Find a partner here.
-
Code:
cs251 start impl - Submit:
git commit,cs251 sign, andgit pushyour completed code. - Reference:
Contents
Required Tasks
In this assignment, you will complete several parts of the implementation of a familiar-looking programming language. The first part (posted now) builds a core data structure (the environment) as an ML abstract data type. Remaining parts implement key features of the interpreter. One class meeting will be a lab-style session to get you started on the interpreter component, which involves exploring and extending provided code.
1. 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 map we have
used to define the semantics of programming languages. We separate the
notion of more basic MAP and an ENV that extends it as a
foundation for a future assignment. In this assignment, you implement
a straightforward version of the full ENV abstract data type. A
future assignment will consider alternative MAP implementations
Implement an Env structure in the file env.sml, ascribing the
ENV signature (defined in the file env.sig), by completing the
definition of all bindings in env.sml. This structure should
represent maps by association lists, a simple data structure we have
worked with in multiple past assignments. Some bindings are provided;
others are defined as raise Unimplemented. You must replace all
raise Unimplemnted by actual implementations.
- val env0 = Env.empty;
val env0 = - : (''a, 'b) Env.t
- Env.lookup "x" env0; (* It's OK to get a "type vars not generalized" warning *)
stdIn:5.1-5.43 Warning: type vars not generalized because of
value restriction are instantiated to dummy types (X1,X2,...)
val it = NONE : ?.X1 option
- val env1 = Env.bind ("x", 7) env0;
val it = - : (string, int) Env.t
- Env.lookup "x" env1;
val it = SOME 7 : int option
- val env2 = Env.bind ("y", 8) env1;
val env2 = - : (string, int) Env.t
- Env.lookup "x" env2;
val it = SOME 7 : int option
- Env.lookup "y" env2;
val it = SOME 8 : int option
- Env.lookup "x" (Env.unbind "x" env2);
val it = NONE : int option
- val env3 = Env.make [("y", 4),("x", 5),("x",2)];
val env3 = - : (string, int) Env.t
- Env.lookup "x" env3;
val it = SOME 5 : int optionAlthough the abstraction of the Env.t type prevents the REPL from
displaying the internals of such a value, The Env.rewind and
Env.replay functions (provided) are useful for enumerating the
context of environments to verify they are as expected.
- print (Env.replay (fn ((k, v), acc) => acc ^ k ^ " = " ^ (Int.toString v) ^ "\n") "" env3);
x = 2
x = 5
y = 4
val it = () : unit
- Env.replay (op::) [] env3;
val it = [("y", 4),("x", 5),("x",2)] : (string * int) listThis data structure will power the core of your interpreter (next part of the assignment), so be sure to test it carefully.
2. Explore the SMiLe Language and Interpreter
First: git pull starter master to make sure you have all the
code (if you started task 1 before this task appeared) and accept the
default merge commit message. (There should not be conflicts.)
The rest of the tasks in this assignment will step you through the implementation of several features in an interpreter for the SMiLe language, a language similar to Standard ML, but with dynamic instead of static type checking. It uses a subset of the SML syntax and a subset of the SML semantics, with the key exception that it performs dynamic type-checking instead of static type-checking.
Wow, this looks long.
The interpreter implementation tasks will require careful reading and understanding of provided code (and text). You will write possibly fewer than 50 new lines of code altogether for the required parts.
How to work 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 6Yep, 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
andis not supported. - Real numbers are not supported.
- The sequencing expression
;is not supported. Useletinstead. - 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 5alone as a line in the REPL session would not parse. Always use avalorfunbinding. - 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
lookupfunction (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
appendfunction in curried form using patterns directly in the function definition (nocaseexpressions). Remember to parenthesize the argument patterns. Applyappendto a pair of lists. - Copy some of your
bintreeorbstfunctions 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 (SmileAststructure), 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 (SmileSugarstructure)env.sig(complete) andenv.sml(partial): your implementation of environments, used by the interpreter (Envstructure)eval.sig(complete) andeval.sml(partial): evaluation of SMiLe programs and ASTs (SmileEvalstructure)
Supporting files that you will use but generally do not need to understand:
parser(complete): the parser that translates programs from strings to ASTs (SmileParserstructure). 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 (SmileMainstructure)smile.sml(complete): top-level SMiLe interpeter operations (Smilestructure)
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 (SmileTeststructure)
Files supporting optional tasks:
scope.sig(complete) andscope.sml(partial): static scope-checking for SMiLe ASTs (SmileScopestructure)
The basic operation of the interpreter goes like this:
+-------------+ +---------------+ +-------------+ +------------+
| (complete) | | (partial) | | (optional) | | (partial) |
source.sml ----> parse ----> AST ----> desugar ----> AST ----> check ----> AST ----> eval ----> output
| | | | | | | |
+-------------+ +---------------+ +-------------+ +------------+
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_patternhold an ML value of typeSmileAst.patternthat 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.smlto familiarize yourself with the Abstract Syntax Tree representation of SMiLe programs defined in theSmileAststructure 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 keywordandsupports mutually-recursive datatypes or functions that refer to each other. -
Inspect ASTs: Run the command
./smile ast examples/fact.smlto display the AST of the SMiLe program defined inexamples/fact.sml.- Read the “raw” version of the AST, represented using the
SmileAst.programtype. - 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.smlcorresponds 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.exprinstead 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.smland raw AST from the output of./smile ast examples/lists.sml. - Find two or three syntactic constructs (at least one in
funbindings 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
SmileSugarstructure insugar.smlimplements 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
SmileSugarfunctions. - Change the
desugar_exprfunction to replaceIfThenElseAST nodes withCaseAST nodes that encode the same SMiLe computation as the originalIfThenElse. The interpreter implementation will not handleIfThenElseduring evaluation. - Confirm the validity of your transformation by running
./smile ast examples/fact.smland 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 addany new files, thengit commitandgit pushyour 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.smlup through the definition of theenvironmenttype. Consider the following questions:- How do values built by the
ClosureValueconstructor 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 optioninstead 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_exprineval.sml. Read several cases fromeval_exprto 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
Envstructure 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 -> boolto compare the two SMiLe values, represented as MLSmileEval.values, 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
values 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.eqreturns false if any of its arguments contains aSmileEval.ClosureValuerepresenting a SMiLe closure. If not for this restriction, it would be possible to implementequsing ML=. Use ML pattern-matching in a recursive ML function instead.
Test your function on several cases. To do so, use
./smile metato 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 metaSML 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 metasession (an SML REPL) without any file. You may wish to repeat some of the lines from the top of ourtest-eq.smlfile toopenvarious 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 inexamplesthat 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.TupleAST 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_exprinunit-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 inexamplesthat 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 addany new files, thengit commitandgit pushyour 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
Casebranch of theSmileEval.eval_exprfunction. Note the arguments it passes toSmileEval.eval_cases.eval_casesdoes 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_casesuses the helper functionSmileEval.match. -
Implement
SmileEval.match, a recursive function that checks to see if aSmileEval.valuematches a singleSmileAst.pattern. Thematchfunction takes four arguments in curried form:- The ML function to call to add new bindings to a SMiLe
environment. - The SMiLe
valueto match against. - The SMiLe
patternto try. - The SMiLe
environmentto extend.
If the SMiLe
valueand SMiLepatternmatch (as defined below),matchreturnsSOMEof anenvironmentthat is the initial environment (as passed tomatch), extended with all the bindings the SMiLe pattern-match introduces. If there is no match,matchreturnsNONE.The implementation should use ML pattern-matching to ML-pattern-match simultaneously on an ML constructor for a
SmileEval.valueand an ML constructor for aSmileAst.pattern.Given a SMiLe
value,v, and a SMiLepattern,p, eitherpmatchesvor it does not. If they match does, the match returns anenvironmentextended with newSmileAst.var * SmileEval.valuebindings. 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:
Wildcardmatches everything and produces no bindings.VarPattern smatches any valuevand bindsstov.UnitPatternmatches onlyUnitValueand produces no bindings.BoolPattern falsematches onlyBoolValue falseand produces no bindings (and similarly fortrue).IntPattern 17matches onlyIntValue 17and produces no bindings (and similarly for other integers).StringPattern "smile"matches onlyStringValue "smile"and produces no bindings (and similarly for other strings).TuplePattern psmatches a value of the formTupleValue vsifpsandvshave the same length and each element ofpsmatches the corresponding element ofvs. The match introduces all bindings introduced by all elementwise submatches.TagPattern (s1,p)matchesTaggedValue(s2,v)ifs1ands2are the same ML string (via ML=) andpmatchesv. The match introduces all bindings introduced by the match ofpandv. We call the stringss1ands2the constructor name or tag name.- Nothing else matches.
Use
bind(the argument) instead ofEnv.bindwhen you need to introduce new bindings into an environment. This allows the caller ofmatchto decide whether to use normalEnv.bindor a version that additionally prints the bindings it introduces.Test your
matchimplementation by creating atest-match.smlwith some test cases (test-eq.smlis 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_casesiterates through the list ofcasesin order, applyingmatchto the argumentsEnv.bind,v, eachSmileAst.pattern, and the givenenv, until it finds aSmileAst.patternthat matchesv.- For the first
SmileAst.patternthat matchesv,eval_casesevaluates the associatedSmileAst.exprfrom the list, using the match environment returned bymatch(which isenvextended with all bindings introduced by the match). - No
patterns after the first match are considered. At most oneexpris evaluated. - If no pattern matches, call
SmileEval.match_errorto report the error.
Test your
eval_casesimplementation by creating atest-eval_cases.smlwith some test cases (test-eq.smlis 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
caseexpressions. Try./smile examples/pat.smland write more of your own.$ ./smile examples/pat.sml (-: SMiLe :-) :) val x = 10 (-: SMiLe :-) -
git addany new files, thengit commitandgit pushyour work! It’s a good backup in case things go wrong later.
6. Implement SMiLe Bindings and Function Application
-
Implement
SmileEval.eval_binding: ImplementValandFunbindings ineval_binding.SmileEval.eval_bindingtakes 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 forValbindings.-
SMiLe
valbindings 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 thematchfunction to your benefit. -
SMiLe
funbindings introduce a binding from the function’s name to a SMiLe closure value for the function.
Use
bind(the argument) instead ofEnv.bindwhen you need to introduce new bindings into an environment or pass a binding function to a helper function. This allows the caller ofeval_bindingsto decide whether to use normalEnv.bindor a version that additionally prints the bindings it introduces. -
-
Read and describe (in comments with the code) how the
SmileAst.Letcase inSmileEval.eval_exprand the top-levelSmileEval.eval_programwork. 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.Applycase ofSmileEval.eval_expr. If something other than aClosureValueis encountered where you expect the closure, use the functiondynamic_type_errorto 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.patternand 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.Caseexpressions!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 testto test all of the example programs in theexamplesdirectory. 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 addany new files, thengit commitandgit pushyour 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 bindings
and exprs found in programs. 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_exprto do the desugaring. - Modify
SmileEval.eval_exprto 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_expron 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.typdescribing types of SMiLe expressions. Add aSmileTypeCheckstructure with atypecheckfunction that takes aSmileAst.expr, a static typing environment mapping variables to types, and a proposedtypfor theexpr, and decides whetherexprmay have this type. Emulate the SML type system as closely as possible. Edit thesources.cmfile 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:
-
Make sure you have committed your latest changes.
$ git add ... $ git commit ... -
Run the command
cs251 signto 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.