• Dueish: Fri. Dec. 18. (But keep in mind that all work will be accepted through Sat. Jan 02.)

  • Notes:
    • This solo assignment is completely optional.
    • This is a solo assignment. You must complete all parts of this assignment entirely on your own without help from any other person and without consulting any resources other than course materials or SML documentation. You may ask Lyn for clarification, but not for help.
    • You can do all problems on this assignment based on material you learned from PS5 and PS6. Be sure to study the solutions to PS5 and PS6 as part of doing this assignment.
    • This assignment has one solo problem worth 33 points.
    • This is the only solo problem that is worth extra credit points. Any points you earn on this assignment will count as extra points toward the solo component of your grade but will not count toward the 50 point extra credit limit on extra credit points mentioned in the syllabus.
    • Of course, partial credit will be awarded where appropriate for incomplete problems.
  • How to Start Solo Assignment F

    • Follow the instructions in the GDoc CS251-F20-T2 Pset Submission Instructions for creating your SOLO-F Google Doc. Put all written answers and a copy of all code into this SOLO-F GDoc.

    • This problem involves starter files in the ~/cs251/sml/solo-f directory in your csenv Virtual Machine.

      To create this directory, execute this Linux commands in a shell on your csenv appliance:

      cd ~/cs251/sml
      git pull origin master

      If you encounter any issues when executing these commands, you can troubleshoot by following these git instructions. If these instructions don’t help, please contact Lyn.

    • Rename the starter file MiniFP.sml in ~/cs251/sml/solo-f to yourAccountName-MiniFP.sml.

    • Rename the starter file MiniFPInterp.sml in ~/cs251/sml/solo-f to yourAccountName-MiniFPInterp.sml.

    • Rename the starter file MiniFPInterpTest.sml in ~/cs251/sml/solo-f to yourAccountName-MiniFPInterpTest.sml.

    • In your renamed file yourAccountName-MiniFPInterp.sml, change the first line of the file from

      use "../solo-f/MiniFP.sml";

      to

      use "../solo-f/yourAccountName-MiniFP.sml";
    • In your renamed file yourAccountName-MiniFPInterpTest.sml, change the first line of the file from

      use "../solo-f/MiniFPInterp.sml";

      to

      use "../solo-f/yourAccountName-MiniFPInterp.sml";
    • REMINDER: ALWAYS MAKE A BACKUP OF YOUR .sml FILES AT THE END OF EACH DAY TO AVOID POTENTIAL LOSS OF WORK.

  • Submission:
    • Include all code that you write in yourAccountName-MiniFP.sml and yourAccountName-MiniFPInterp.sml and in your SOLO-F GDoc (so that I can comment on it). In your GDoc only include the function definitions you modify plus any new functions you define.
    • Please format your SML function definitions so that they’re easy to read. Format all code using a fixed-width font (Courier New or Consolas). You use a small font size if that helps.
    • Drop a copies of your yourAccountName-MiniFP.sml and yourAccountName-MiniFPInterp.sml files in your ~/cs251/drop/solo-e drop folder on cs.wellesley.edu by executing the following (replacing **all three occurrences of gdome by your cs server username):

      scp -r ~/cs251/sml/solo-f/gdome-MiniFP.sml gdome@cs.wellesley.edu:/students/gdome/cs251/drop/solo-f
      scp -r ~/cs251/sml/solo-f/gdome-MiniFPInterp.sml gdome@cs.wellesley.edu:/students/gdome/cs251/drop/solo-f

      (You do not need to drop your yourAccountName-MiniFPInterpTest.sml file)

1. MiniFP Interpreter (33 points)

This is a solo problem. This means you must complete it entirely on your own without help from any other person and without consulting resources other than course materials or online documentation. You may ask Lyn for clarification, but not for help.

Background

In Section 11 of his Turing Award lecture paper, John Backus describes an applicative language named FP, which you studied in PS5 Problem 1. (Make sure you complete that problem before you start this one!) n In this problem, you will flesh out the skeleton of an interpreter for MiniFP, a subset of FP that implements many of FP’s features. The fact that functional forms in FP (and MiniFP) have implicit parameters (rather than parameters with explicit names) significantly simplifies the structure of this interpreter. In particular, the environment structures used to handle naming in Bindex are not necessary in MiniFP.

Objects

The objects manipulated by MiniFP programs are specified by the obj data type in the file MiniFP.sml:

datatype obj = 
    Int of int (* integer objects *)
  | Seq of obj list (* sequence of objects *)

MiniFP objects are either:

  1. integers (introduced by the Int constructor); or
  2. sequences of objects (introduced by the Seq constructor).

(Note: Although the Int and Seq constructors have the same names as constructors in the Sexp, PostFix, Intex, and Bindex modules, the MiniFP constructors are different. They are defined in the MiniFP module in the file MiniFP.sml in the solo-f directory. Explicit qualification can be used to disambiguate the constructors from these modules — e.g., Sexp.Int vs. MiniFP.Int.)

The following table shows how some objects in FP notation can be expressed as instances of the obj data type, and also in an s-expression notation. In the table, \(i\) denotes an integer and \(x\) denotes an object.

FP Notation SML obj Notation S-expression Notation
\(i\) Int \(i\) \(i\)
\(\langle x_1, \dots, x_n\rangle\) Seq[\(x_1\),\(\ldots\),\(x_n\)] (\(x_1 \ldots\ x_n\))
\(17\) Int 17 17
\(\langle 8, -3, 6 \rangle\) Seq[Int 8, Int ~3, Int 6] (8 -3 6)
\(\langle 17, \langle 8, -3, 6\rangle \rangle\) Seq[Int 17, Seq[Int 8, Int ~3, Int 6]] (17 (8 -3 6))
\(\langle \langle 8, -3, 6\rangle, \langle 5, 1, 7\rangle\rangle\) Seq[Seq[Int 8, Int ~3, Int 6], Seq[Int 5, Int 1, Int 7]] ((8 -3 6) (5 1 7))

Here are some definitions of MiniFP objects in the file MiniFP.sml that we will use in later examples. Each of these has type MiniFP.obj.

val vector1 = Seq[Int 2,  Int 3,  Int 5]

val vector2 = Seq[Int 10,  Int 20,  Int 30]

val vectors = Seq[vector1,  vector2] (* A pair of vectors or a 2x3 matrix *)

val matrix = Seq[Seq[Int 1,  Int 4],  Seq[Int 8,  Int 6],  Seq[Int 7,  Int 9]] (* A 3x2 matrix *)

val matrices1 = Seq[vectors,  matrix] (* A pair of a 2x3 matrix and a 3x2 matrix *)

val matrices2 = Seq[matrix,  vectors] (* A pair of a 3x2 matrix and a 2x3 matrix *)

For comparison, here are the s-expression forms of the these objects, also defined in MiniFP.sml. Each of these has type Sexp.sexp.

val vector1_sx = Sexp.stringToSexp("(2 3 5)")

val vector2_sx = Sexp.stringToSexp("(10 20 30)")

val vectors_sx = Sexp.Seq([vector1_sx, vector2_sx])

val matrix_sx =  Sexp.stringToSexp("((1 4) (8 6) (7 9))")

val matrices1_sx = Sexp.Seq([vectors_sx, matrix_sx])

val matrices2_sx = Sexp.Seq([matrix_sx, vectors_sx])

Because both MiniFP and Sexp have datatype constructors named Int and Seq, the corresponding versions of this seem similar in printed representation. For example:

- MiniFP.matrix;
val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]
  : MiniFP.obj

- MiniFP.matrix_sx;
val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]
  : Sexp.sexp

However, notice that matrix has type MiniFP.obj and matrix_sx has type Sexp.sexp. In fully qualified form, matrix would be written:

MiniFP.Seq [MiniFP.Seq [MiniFP.Int 1, MiniFP.Int 4],
            MiniFP.Seq [MiniFP.Int 8, MiniFP.Int 6],
            MiniFP.Seq [MiniFP.Int 7, MiniFP.Int 9]]

while matrix_sx would be written

Sexp.Seq [Sexp.Seq [Sexp.Int 1, Sexp.Int 4],
          Sexp.Seq [Sexp.Int 8, Sexp.Int 6],
          Sexp.Seq [Sexp.Int 7, Sexp.Int 9]]

MiniFP.sml includes the following functions for converting between strings, s-expressions, and MiniFP objects:

- sexpToObj;
val it = fn : Sexp.sexp -> obj

- stringToObj;
val it = fn : string -> obj

- objToSexp;
val it = fn : obj -> Sexp.sexp

- objToString;
val it = fn : obj -> string

For example:

- matrix = sexpToObj matrix_sx;
val it = true : bool

- matrix = stringToObj "((1 4) (8 6) (7 9))";
val it = true : bool

- Sexp.isEqual(objToSexp matrix, matrix_sx);
val it = true : bool
(* Recall that Sexp.sexp is *not* an equality type, so can't compare 
   two sexps with =. Need to use Sexp.isEqual instead. *)

- objToString matrix;
val it = "((1 4) (8 6) (7 9))" : string

Functional Forms

MiniFP programs are functional forms which include primitive functions like \(\mathrm{id}\), \(\mathrm{+}\), and \(\mathrm{distr}\), as well as combining forms like composition (\(\circ\)), mapping (\(\alpha\)), and reduction (/).

Here is the data type funForm for representing functional forms in SML:

  and funForm = 
      Id    (* identity *)
    | Add   (* addition *)
    | Sub   (* subtraction *)
    | Mul   (* multiplication *) 
    | Div   (* division *)
    | Distl (* distribute from left *)
    | Distr (* distribute from right *)
    | Trans (* transpose *)
    | Const of obj  (* constant function *)
    | Sel of int    (* selection *)
    | Map of funForm  (* alpha = map *)
    | Reduce of funForm (* / = reduce *)
    | BinaryToUnary of funForm * obj (* bu *)
    | Compose of funForm * funForm  (* o = composition *)
    | FunSeq of funForm list (* [...] = function sequence *)

The following table shows the correspondence between FP notation for functional forms, the SML data type notation, and an s-expression notation. In the table, \(i\) denotes an integer, \(x\) denotes an object, and \(f\) denotes a functional form.

FP Notation SML funForm Notation S-expression notation
\(\mathrm{id}\) Id id
\(+\) Add +
\(-\) Sub -
\(\times\) Mul x
\(\div\) Div -:
\(\mathrm{distl}\) Distl distl
\(\mathrm{distr}\) Distr distr
\(\mathrm{trans}\) Trans trans
\(\overline{x}\) Const \(x\) ($\(\ x\))
\(i\) Sel \(i\) \(i\)
\(\alpha\,f\) Map \(f\) (a\(\ f\))
\(/\,f\) Reduce \(f\) (/ \(\,f\))
\(\mathrm{bu}\ \,f\ x\) BinaryToUnary(\(f\),\(x\)) (bu\(\ f\)\(x\))
\(f_1 \circ\ f_2\) Compose(\(f_1\),\(f_2\)) (o\(\ f_1\)\(f_2\))
\([f_1 \dots\ f_n]\) FunSeq[\(f_1\),\(\dots\),\(f_n\)] (\(f_1\ \dots\ f_n\))

For example, consider the inner product and matrix multiplication examples from Sections 11.3.2 and 11.3.3 (p. 622) of Backus’s paper:

\[ \begin{array}{rcl} IP & \equiv{} & (/+)\circ(\alpha{}\times)\circ\mathrm{trans} \
MM & \equiv{} & (\alpha\alpha\mathrm{IP})\circ(\alpha\mathrm{distl})\circ\mathrm{distr}\circ[1,\mathrm{trans}\circ2] \end{array} \]

These can be expressed as SML MiniFP.funForm instances via the following definitions (included in MiniFP.sml):

val IP = Compose(Reduce Add, Compose(Map Mul, Trans))

val MM = Compose(Map(Map IP),
                 Compose(Map Distl,
                         Compose(Distr,
                                 FunSeq[Sel 1, Compose(Trans, Sel 2)])))

These can also be expressed as SML Sexp.sexp instances via the following definitions (also included in MiniFP.sml):

val IP_sx = Sexp.stringToSexp "(o (/ +) (o (a x) trans))"

val MM_sx = Sexp.stringToSexp "(o (a (a (o (/ +) (o (a x) trans))))\
                               \  (o (a distl)                     \
                               \     (o distr                      \
                               \        (1 (o trans 2)))))"

(The MM_sx definition illustrates the SML convention for entering multiline strings. Each nonterminal line must end with a backslash character, and the next line must begin with a backslash character.)

MiniFP.sml includes the following functions for converting between strings, s-expressions, and MiniFP functional forms:

- sexpToFunForm;
val it = fn : Sexp.sexp -> funForm

- stringToFunForm;
val it = fn : string -> funForm

- funFormToSexp;
val it = fn : funForm -> Sexp.sexp

- funFormToString;
val it = fn : funForm -> string

For example:

- IP = sexpToFunForm IP_sx;
val it = true : bool

- IP = stringToFunForm "(o (/ +) (o (a x) trans))";
val it = true : bool

- Sexp.isEqual(funFormToSexp IP, IP_sx);
val it = true : bool

- funFormToString IP;
val it = "(o (/ +) (o (a x) trans))" : string

1a. A MiniFP Program (5 points)

In PS5 Problem 1h, you studied this functional form:

\[ F \equiv{} (\alpha{}/+) \circ (\alpha\alpha{}\times) \circ\ (\alpha\mathrm{distl}) \circ \mathrm{distr} \circ [\mathrm{id},\mathrm{id}] \]

In the file yourAccountName-MiniFP.sml, express this functional form in SML via two definitions that flesh out the skeleton:

val F = ... 

val F_sx = ...

The value of F should have type funForm and the value of F_sx should have type Sexp.sexp.

Notes:

  • You can safely ignore this warning when when send your yourAccountName-MiniFP.sml buffer into the *sml* interpreter buffer:

    ../utils/Utils.sml:35.48 Warning: calling polyEqual
  • After you send your yourAccountName-MiniFP.sml buffer into the *sml* interpreter buffer, the following equalities should hold with your definitions:

    - open MiniFP;
    ... many lines of printout omitted ...
    
    - F = sexpToFunForm F_sx;
    val it = true : bool
    
    - Sexp.isEqual(funFormToSexp F, F_sx);
    val it = true : bool

1b. A MiniFP Interpreter (28 points)

A MiniFP interpreter specifies the object that results from applying a program (i.e., functional form) to an object. So a MiniFP interpreter is simply an apply function with the following type:

val apply: MiniFP.funForm -> MiniFP.obj -> MiniFP.obj

The semantics (meaning) of all MiniFP functional forms is given in Sections 11.2.3 and 11.2.4 of Backus’s paper. The only difference between MiniFP and FP semantics is that in MiniFP, the Reduce functional form is defined only on nonempty sequences. So applying Reduce Add to Seq[] is an error in MiniFP, but not in FP (where it denotes 0).

In this task, your goal is to implement a complete MiniFP interpreter by fleshing out the skeleton of the apply function in the file yourAccountName-MiniFPInterp.sml. In this sekelton, the clauses for Id, Add, and Trans have been written for you, and all other clauses are stubs that act like Id. Your task is to flesh out the clauses for all the other MiniFP functional forms so that they work as expected. x

Notes:

  • Each functional form takes a MiniFP.obj as an input and returns a MiniFP.obj as an output. The expected type, size (for sequences), etc. of the object depends on the details of the functional form. E.g., distl expects the object to be a sequence of exactly two elements, where the first element can be any object but the second element must be a sequence (with any number of elements). These constraints can be concisely specified via pattern matching in an apply clause; for example:

    apply Distl (Seq[y,Seq zs]) = ...

    When implementing a clause, think carefully about the constraints and be sure not to overconstrain the object. E.g., in the Distl clause, the first sequence value can be any object y; you should not force it to be a sequence value Seq ys, which would preclude it from being an integer.

  • You can test your interpreter by sending your yourAccountName-MiniFPInterp.sml to the *sml* buffer. After you open MiniFPInterp;, you can interactively test your apply function on the kinds of examples shown below. See further down for how to run a suite of test cases on your interpreter.

  • You can safely ignore this warning when when send your yourAccountName-MiniFPInterp.sml buffer into the *sml* interpreter buffer:

    ../utils/Utils.sml:35.48 Warning: calling polyEqual
  • It is strongly recommended that you flesh out apply incrementally. That is, flesh out one clause and a time and test that before going to the next clause.

  • Feel free to define and use helper functions.

  • You should raise an EvalError with an (using raise EvalError error-message-string-goes-here) when an error situation is encountered. The skeleton intepreter already raises two such errors: (1) Error: Ill-formed application for an ill-formed application and Error: transpose -- sequences not all of same length for transpose. Your intepreter clauses should raise an EvalError in the following three cases:

    • division by zero
    • index out of bounds for Sel
    • reduction on an empty list

    Here are some sample error messages show using the testApply function described later:

    - open MiniFPInterp; 
    ... many lines of printout omitted ...
    
    - testApply Div (Seq[Int 5, Int 0]);
    val it = "EvalError: Division by zero" : string
    
    - testApply (Sel 4) vector1;
    val it = "EvalError: Selection index 4 out of bounds [1..3]" : string
    
    - testApply (Reduce Add) (Seq[]);
    val it = "EvalError: Reduction of empty sequence" : string
  • Below are numerous examples of the correct behavior of MiniFP’s apply function. Your apply function should behave the same way. (See further down for how to run a suite of automated test cases.)

    - open MiniFPInterp; 
    ... many lines of printout omitted ...
    
    - apply;
    val it = fn : funForm -> obj -> obj
    
    - apply Id (Int 17);
    val it = Int 17 : obj
    
    - apply Add (Seq[Int 17, Int 3]);
    val it = Int 20 : obj
    
    - apply Mul (Seq[Int 17, Int 3]);
    val it = Int 51 : obj
    
    - apply Sub (Seq[Int 17, Int 3]);
    val it = Int 14 : obj
    
    - apply Div (Seq[Int 17, Int 3]);
    val it = Int 5 : obj
    
    - vector1;
    val it = Seq [Int 2,Int 3,Int 5] : obj
    
    - apply Id vector1;
    val it = Seq [Int 2,Int 3,Int 5] : obj
    
    - apply (Const (Int 17)) vector1;
    val it = Int 17 : obj
    
    - apply (Sel 1) vector1;
    val it = Int 2 : obj
    
    - apply (Sel 2) vector1;
    val it = Int 3 : obj
    
    - apply (Sel 3) vector1;
    val it = Int 5 : obj
    
    - apply Distl (Seq[Int 7, vector1]);
    val it = Seq [Seq [Int #,Int #],Seq [Int #,Int #],Seq [Int #,Int #]] : obj
    
    - Control.Print.printDepth := 1000;
    ... many lines of printout omitted ...
    val it = () : unit
    
    - apply Distl (Seq[Int 7, vector1]);
    val it = Seq [Seq [Int 7,Int 2],Seq [Int 7,Int 3],Seq [Int 7,Int 5]] : obj
    
    - apply Distr (Seq[vector1, Int 7]);
    val it = Seq [Seq [Int 2,Int 7],Seq [Int 3,Int 7],Seq [Int 5,Int 7]] : obj
    
    - apply (BinaryToUnary (Add, Int 1)) (Int 17);
    val it = Int 18 : obj
    
    - apply (Map (Const (Int 17))) vector1;
    val it = Seq [Int 17,Int 17,Int 17] : obj
    
    - apply (Map (BinaryToUnary (Mul, Int 2))) vector1;
    val it = Seq [Int 4,Int 6,Int 10] : obj
    
    - apply (Reduce Add) vector1;
    val it = Int 10 : obj (* 2 + (3 + 5) *)
    
    - apply (Reduce Mul) vector1;
    val it = Int 30 : obj (* 2 * (3 * 5) *)
    
    - apply (Reduce Sub) vector1;
    val it = Int 4 : obj (* 2 - (3 - 5) *)
    
    - apply (Compose(Map (BinaryToUnary (Add, Int 1)), Map (BinaryToUnary (Mul, Int 2)))) vector1;
    val it = Seq [Int 5,Int 7,Int 11] : obj
    
    (* You can use stringToFunForm and stringToObj to simplify testing *)
    - apply (stringToFunForm "(o (a (bu + 1)) (a (bu x 2)))") (stringToObj "(2 3 5)");
    val it = Seq [Int 5,Int 7,Int 11] : obj
    
    - apply (FunSeq[Const (Int 17), Id, Map (BinaryToUnary (Add, Int 1))]) vector1;
    val it = Seq [Int 17,Seq [Int 2,Int 3,Int 5],Seq [Int 3,Int 4,Int 6]] : obj
    
    - matrix;
    val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]] : obj
    
    - apply Trans matrix;
    val it = Seq [Seq [Int 1,Int 8,Int 7],Seq [Int 4,Int 6,Int 9]] : obj
    
    - apply (Compose(Trans,Trans)) matrix;
    val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]] : obj
    
    - vectors;
    val it = Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]] : obj
    
    - apply IP vectors;
    val it = Int 230 : obj
    
    - matrices1;
    val it =
      Seq
        [Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]],
         Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]] : obj
    
    - apply MM matrices1;
    val it = Seq [Seq [Int 61,Int 71],Seq [Int 380,Int 430]] : obj
    
    - matrices2;
    val it =
      Seq
        [Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]],
         Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]]] : obj
    
    - apply MM matrices2;
    val it =
      Seq
        [Seq [Int 42,Int 83,Int 125],
         Seq [Int 76,Int 144,Int 220], (* manually reformated to look better *)
         Seq [Int 104,Int 201,Int 305]] : obj
  • By default, SML does not display the message of an exception, which makes examples like the following harder to understand. For example:

    - apply (Sel 4) vector1;
    
    uncaught exception EvalError
      raised at: /tmp/emacs-region1678BTc:17.10-21.15

    The issue here is an index-out-of-bounds exception, but because SML does not display the exception message, this may not be obvious.

    For better testing of cases involving exceptions, MiniFPInterp.sml includes the following helper function:

    (* testApply: funForm -> obj -> string *)
    and testApply funForm obj =
        objToString (apply funForm obj)
        handle (EvalError msg) => "EvalError: " ^ msg
             | (SyntaxError msg) => "SyntaxError: " ^ msg
             | other => "Exception: " ^ (exnMessage other)

    Here are some examples of using testApply to show error messages:

    - testApply (Sel 4) vector1;
    val it = "EvalError: Selection index 4 out of bounds [1..3]" : string
    
    - testApply (Reduce Add) (Seq[]);
    val it = "EvalError: Reduction of empty sequence" : string
    
    - testApply Div (Seq[Int 5, Int 0]);
    val it = "EvalError: Division by zero" : string
  • You can test your interpreter on a suite of test cases by sending your yourAccountName-MiniFPTest.sml buffer to your *sml* interpreter buffer. This will load the MiniFPTest structure defined in this file and invoke MiniFPTest.test(), which will test your interpreter on over 100 test cases.

    The test cases are defined in a variable testSuite, each of whose entries has the form:

    ((function-form-string, object-string), result-string)

    The tester first parses function-form-string and object-string into a function form f and object x, respectively. It then applies f to x, and converts the resulting object into a string. If this string matches result-string, the test passes by displaying OK!. Otherwise, the test fails by displaying ***TESTING MISMATCH*** and showing the difference between the expected result and the actual result.

    If the application of f to x raises an exception, the tester converts the exception message to an error string; this makes it possible for the tester to verify that the interpreter handles error situations correctly.

    Your implementation should be able to pass all the test cases in yourAccountName-MiniFPTest.sml. In order to do this, you will have to make the error messages in any EvalError exceptions you raise match the ones in the testing suite.

    You are encouraged to add additional test cases to yourAccountName-miniFPTest.sml