Assignment #10: (Not for submission) SlugOut: Wednesday, April 23rd Due: Wednesday, April 23rd, 0:00midnight |
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.
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 is extended in several ways to support I/O. Make sure you understand these changes before you begin your work.
(run "{list 1 'foo'}") |
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.
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))] |
When you've done this, you will be able to test your code, for example:
(run-io "{print 'foo'}") |
Next, we move to sequencing which is also easy. The case for this:
[(Begin2 (IOV io1) (IOV io2))
???] |
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)) |
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'}}}}}}") |
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))] |
The above code should work as expected when you're done with this.
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> } |
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.
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 ...
...}}") |
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:
;; 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?)]) |
;; 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")])) |
(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'}}}}}}}}}") |
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'}}}}") |
Why is macro expansion mixed with parsing?
(Only an exact answer will count.)
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}}} |
{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'}}} |