Assignment 7, CSC430, Winter 2015
1 Goal
Add a pattern-matching macro mechanism to the language of assignment 3.
2 Guidelines
2.1 Contracts & Test Cases
For this and all remaining assignments, every function you develop must come with the following things:
A commented header line that expresses the result of the function in terms of its inputs, written in English. Be as precise as you can within the space of a line or two. The header lines must include input and output types.
Test cases. A function without test cases is incomplete. Write the test cases first, please.
2.2 Handling errors
Your parser and interpreter must detect errors and explicitly signal them by calling (error ...). We will consider an error raised internally by racket to be a bug in your code.
For example, Racket signals a "divide by zero" error if you attempt to evaluate (/ 1 0). Since we use Racket’s division function to implement division in class, you may be tempted to leave it to Racket to signal division by zero errors for you. However, you must signal the error yourself by explicitly testing for division by zero before calling Racket’s division procedure.
2.3 Language Level & File Format
#lang plai-typed
Your solution should take the form of a single file. Solve each problem separately, and make sure that each solution appears in a separate part of the file, with comments separating each problem’s solution.
Hand in your solution using the handin server. For help with the handin server, please see the course web page.
3 Macro Definition
For this assignment, we’re starting with the GUCI3 language; no classes, no mutation, no store-passing. We want to add a simple macro-definition language. It will be based on Racket’s let-syntax, and we’ll call it let-stx.
In this system, a name will be bound to a series of clauses, where each has a pattern and a template. Within the scope of the let-syntax, any use of the macro is matched against the macro’s defined clauses; the expander picks the first clause that matches, and applies the substitutions to the associated template.
3.1 First Example
Here’s an example like one we saw in class:
{let-stx {or = {[{or a b} => {{fn {temp} {if temp temp b}} a}]}} {or false {+ 12 1}}}
This should expand into {{fn {temp} {if temp temp {+ 12 1}}} false}, because it’s the first non-false answer. This should evaluate to 13.
Here’s how it works.
First, expansion is a separate pass; it happens before parsing.
The expander looks at the expression, and sees that it’s a let-stx. It adds the binding from or to the given transformer (pattern and template) in the expander environment, and then continues expanding.
It encounters the or use, looks in its environment, and sees that or is bound to a macro use. It then considers each pattern (in this case there’s only one), finds that it matches, and then performs substitution; every a in the template is replaced by false, and every b is replaced by {+ 12 1}.
Then, the resulting expression is expanded again. Since this s-expression contains no macro uses, it’s returned unchanged.
3.2 Semantics of Syntax Bindings
The expander transforms one s-expression into another. Expressions that are not macro definitions or macro uses are unchanged by the expander.
The let-stx form has this shape:
{let-stx {name = {[pat => template] ...}} body}
...where name is an identifier, pat and template are s-expressions, and body is an s-expression. The name is the newly bound macro’s name. each [pat => template] clause contains a pattern and a template.
Within the body, macro uses look like applications of the macro’s name; that is, it matches the pattern {SYMBOL ANY ...}, where the SYMBOL matches the macro’s name.
When the expander encounters a macro use, it uses the macro’s clauses to expand the macro. More specifically, it tries each pattern in turn. When the matcher finds a pattern that matches, it uses the corresponding template to rewrite the macro use.
So, for instance, if a macro with has only one clause, with the pattern {with {n r} b} and the template {{fn {n} b} r}, and a use has the shape {with z 14}, the pattern match would fail (because the pattern would try to match the pattern {n r} against the user code z). If, on the other hand, the use was {with {f 19} {+ f 2}}, then the pattern would match, and the pieces n, r, and b would be plugged into the template to yield the expanded expression. Finally, note that the expansion may not be sensible; if the use of the macro reads {with {{+ 9 4} x} 12}, this will expand successfully into {{fn {{+ 9 4}} 12} x}, which will signal an error at parse time. It’s not your job to detect these at expansion time.
3.3 Implementation Details
Implementing the expander will require a number of helper functions. First, the expander will need to maintain an environment, indicating which macros are in scope. Each let-stx will add a binding to this environment.
When you find a macro use, you will probably make a call to an expand-macro helper function, which will probably try each macro clause using a try-macro-clause function. This in turn will probably have a try-pattern-match function, and a template-subst function. This may call a template-subst1 function.
4 Change to the semantics of If
There is one small change to the meaning of if; it should admit any value, not just a boolean, as the result of its test expression, and it should evaluate its then clause for any test value other than false.
5 Removal of with
Now that we have let-stx, there’s no need for a with rule in our parser. Get rid of it!
5.1 Syntax of GUCI7
The concrete syntax of the GUCI7 language with these additional features can be captured with the following EBNF:
GUCI7 | = | num | ||
| | true | |||
| | false | |||
| | id | |||
| | {if GUCI7 GUCI7 GUCI7} | |||
| | {fn {id ...} GUCI7} | |||
| | {operator GUCI7 GUCI7} | |||
| | {GUCI7 GUCI7 ...} | |||
| | {let-stx {any-id = {[pat-sexp => sexp] ...}} GUCI7} |
pat-sexp | = | any-id | ||
| | {pat-sexp ...} |
sexp | = | any-id | ||
| | num | |||
| | {sexp ...} |
operator | = | + | ||
| | - | |||
| | * | |||
| | / | |||
| | eq? | |||
| | <= |
... where an id is not true, false, let-stx, =, =>, if, fn or one of the operator names, but an any-id is any identifier at all
5.2 Syntax Notes:
Note that the last rule is the one for applications. Also, note that all of the ‘...’s are to be read as *zero* or more. That is, zero is legal in these cases.
6 Interface
Make sure that you include the following functions, and that they match this interface:
procedure
(expand s) → s-expression
s : s-expression
procedure
(parse s) → ExprC
s : s-expression
procedure
(interp e env) → Value
e : ExprC env : Environment
procedure
(top-eval s) → string
s : s-expression
(define (top-eval [s : s-expression]) : string (serialize (interp (parse (expand s)) empty-env)))
Your body can be different if you really want, but the types must match these.