Assignment #10: (Not for submission) Slug

Out: Wednesday, April 23rd   Due: Wednesday, April 23rd, 0:00midnight



Administrative

Same pairs.

Update the course plugin for the language level for this assignment.

In this homework you are given an extended version of the SLOTH evaluator, which you will extend. To keep things in the world of slow animals, the new language is called SLUG. Begin your work with the slug.scm source. The extensions to SLOTH that makes it SLUG are described below, make sure you understand the code before you modify it.

Don't panic! This homework requires understanding of a few lectures, and reading through some code. Make sure you understand the material, and go through this work according to the specified steps.



Side effects in a lazy language

Your first job is to implement an I/O side effects layer for the SLUG language, following the principle that we have seen in class. We begin with implementing output, and then continue to implement sequencing and input.

The idea is simple: in a lazy language, expressions are always delayed as promises, so we're dealing with computations rather than values. This makes it really convenient in some situations, but it is an obstacle when we want to deal with side-effects. So, to implement I/O, we use descriptions of input/output operations rather than the operations themselves. For example, instead of a statement that prints a string, we use a value that denotes a string printout.

SLUG extensions for I/O

SLUG is extended in several ways to support I/O. Make sure you understand these changes before you begin your work.

The “I/O Execution” section is incomplete. The perform-i/o function expects an IO value and acts accordingly, but the core parts of this function are missing (marked by ???). Your task is now to fill in these three blanks.

Output

The case for output is the simplest: it is getting a Print description, and expects it to hold a string value:

[(Print (ScmV str)) (if (string? str) ??? (error 'perform-i/o "cannot print a non-string value: ~s" str))]
(Note that the input value is first forced using strict-IO.)
If we do have a string, then all that is left for filling the blank is to display that string. Nothing else needs to be done, since we got a simple description for printing a string.

When you've done this, you will be able to test your code, for example:

(run-io "{print 'foo'}")
(Comment out these tests in your submitted code, since we don't have a framework for testing I/O, and the submission server runs your code without any interactions.)

Sequencing

Next, we move to sequencing which is also easy. The case for this:

[(Begin2 (IOV io1) (IOV io2)) ???]
makes sure that you now have two I/O descriptions, and all you need to do is to execute each one in its turn. (Remember that there is an implicit begin on the branches of cases.)

Again, testing this should be easy.

An interesting thing to see here is the correlation between values and computations. For example, translate this infinitely-looping Scheme code to SLUG:

(define (hello-loop) (display "Hello\n") (hello-loop))
and you will see that the translated SLUG code constructs an infinite data structure that corresponds to the infinite loop. (Note that it's a recursive definition, so if you try that you'll need Y, or a `bindrec' extension.)

Input

Finally, dealing with input is a little more complicated. The idea is that when the program reads a value, it needs to continue its computation with the result — but this result is only available when we execute the resulting I/O descriptions. The solution is to plant functions in the stream of descriptions: to read some input, you construct a ReadLine description (specified by the read function in SLUG programs), and hand it a function of one argument, which specifies what to do with the input.

For example, to prompt and read a name, and then print it, you build a read-description that contains a function and does the printout:

(run-io "{begin2 {print 'What is your name?'} {read {fun {name} {begin2 {print 'Your name is '''} {begin2 {print name} {print '''\n'}}}}}}")
Note how this does not depend on the order of evaluation in the lazy SLUG language: the order is enforced by the perform-i/o loop, which exists in the strict world. In this example, the last part is not forced until a value is read — so it is always executed after the first part.

The ReadLine case already ensures that it is used with a closure of one argument:

[(ReadLine (FunV names body env)) (if (= 1 (length names)) ??? (error 'perform-i/o "expecting a unary function: ~s" str))]
All you need to do is to use read-line to read a string, then apply the closure on this string and send the result to execute which will continue doing I/O (it's a simple wrapper that forces lazy values and strips off the IOV tag).

The above code should work as expected when you're done with this.



Macros

Another extension to SLUG is that it has a generic macro preprocessor. An additional input syntax — with-stx — specifies macro transformers in a way that is similar to Scheme's syntax-rules. The syntax of with-stx is:

{ with-stx {<id> { <id> ... } { <pattern> <pattern> } ...} <SLUG> }
The pattern pairs and literal keyword list are similar to the ones in syntax-rules, and the transformer is generated via a make-transformer built-in.

Macro expansion is implemented during parse, which now carries around an environment-like argument holding bindings for syntax transformers. Whenever parse encounters a with-stx input, it builds a transformer and adds it to the recursive call. All Call application forms are first checked — if their first expression is an identifier, and this identifier is a currently-known macro name, then the transformer is applied and parsing continues with the result.

For example, see the last test in the file: it implements a let macro that is equivalent to bind, except that it is translated to function calls, and then it implements a let* form as shown in class.

Your task

As seen above, you can now do I/O in your lazy language, but the syntax for doing this is much too verbose. Your job is to write a do macro that will make things easier to write. For example, the new syntax will allow running this:

(run-io "{with-stx {do ???} {do {print 'What is your name?\n'} {name <- {read}} {print 'What is your email?\n'} {email <- {read}} {print 'Your address is '''} {print name} {print ' <'} {print email} {print '>''\n'}}}")

For this question, all you need to do is to fill in the macro body which is marked by ??? in the above code. To make things easier, here is a bit more from this macro — you have three cases to complete:

(run-io "{with-stx {do {<-} {{do {id <- {read}} next more ...} ???} {{do {print str} next more ...} ???} {{do expr} ???}} {do ... ... same as above ... ...}}")
(Note that <- is a literal keyword that is expected to appear in places where do is used, and note that read and print can actually stand for any name.)
This code is included at the bottom of the file.



Mutation in a lazy language

The same approach can be used to deal with mutation: we can add boxes to the language, and handle them through operation descriptions in the same way that we handled I/O — in fact, we can extend IO with new descriptions. Do this in steps:

  1. Add three cases to the IO type definition:
    ;; I/O descriptions (define-type IO [Print (string VAL?)] [ReadLine (receiver VAL?)] [Begin2 (l VAL?) (r VAL?)] ;; mutation [NewBox (init VAL?) (receiver VAL?)] [UnBox (boxed VAL?) (receiver VAL?)] [SetBox (boxed VAL?) (newval VAL?)])
  2. Add bindings for the new operations: newbox unbox, set-box!,
  3. Update strict-IO,
  4. Extend perform-i/o with the required functionality.
For simplicity, you can assume that the VALs are all ScmVs. When you extend perform-i/o, you will need to deal with two more receiver cases. It will be easier if you abstract the receiver functionality away from perform-i/o into a helper:
;; execute-receiver : VAL Any -> Void ;; Helper for perform-i/o: this function expects a receiver value and a ;; return value to throw onto the receiver (in a ScmV wrapper). (define (execute-receiver receiver-val return-val) (cases receiver-val [(FunV names body env) (if (= 1 (length names)) ... (error 'execute-receiver "expecting a unary function"))] [else (error 'execute-receiver "expecting a receiver function")]))
You can try the following example to test your implementation:
(run-io "{bind {{incbox {fun {b} {unbox b {fun {curval} {set-box! b {+ 1 curval}}}}}}} {newbox 0 {fun {i} {begin2 {incbox i} {begin2 {print 'i now holds: '} {unbox i {fun {v} {begin2 {print {number->string v}} {print '\n'}}}}}}}}}")



Macros

Finally, use the same approach as above — extend the previous macro so the new constructs can also be used in a do context. You should be able to get to the following code:

(run-io "{with-stx {do {<-} ???} {bind {{incbox {fun {b} {do {curval <- {unbox b}} {set-box! b {+ 1 curval}}}}}} {do {i <- {newbox 0}} {incbox i} {print 'i now holds: '} {v <- {unbox i}} {print {number->string v}} {print '\n'}}}}")
Hints: (a) begin with the same syntax rewriter you had above, and generalize the patterns (replace {read} and {print str} by a general {f x ...} pattern (on both sides)); (b) you still need only three rewrite rules in the with-stx.



Bonus #1 [5%]

Why is macro expansion mixed with parsing?
(Only an exact answer will count.)



Bonus #2 [15%]

It is nice to have with-stx in the language so that users can extend their own set of syntactic constructs. However, this is a difficult job, and we want to give them some pre-defined syntaxes. Do this by implementing a global-transformers which is provided as the initial argument to parse-sexpr. Populate this with a few useful syntax transformers, like the above do (you will need to write Scheme code that generates the same transformer). Also, for fun, add a prog syntax that makes it possible to write definitions followed by a body expression — which is being translated to a sequence of nested bind expressions. It should be used like this:

{prog x := 1 y := 2 {foo n} := {+ n x} x := {+ x 1} {* x {foo y}}}
which evaluates to 6. A more complete example is writing code that looks like the above:
{prog {twice b} := {do {curval <- {unbox b}} {set-box! b {* 2 curval}}} {do {i <- {newbox 1}} {twice i} {print 'i now holds: '} {v <- {unbox i}} {print {number->string v}} {twice i} {print ', and now it holds: '} {v <- {unbox i}} {print {number->string v}} {print '\n'}}}