UNB/ CS/ David Bremner/ teaching/ cs3613/ tutorials/ CS3613 Tutorial 7

Background

We have seen several cases where the syntax supported by an interpreter can be modified by using some kind of preprocessor (e.g. converting with* into nested with. This tutorial is on macros, which are a way for the programmer to change the syntax of a language without changing the language interpreter / compiler.

All of the following should be in #lang plai-typed. You can use one source file for the whole tutorial; otherwise you may need to duplicate some definitions which are re-used.

Part 1. Adding FLANG syntax to racket

The first new tool is define-syntax-rule

The key idea here is a pattern. In the example below, the (with (id val) body) s-expression acts something like a type variant in type-case. The with is treated literally, and the other identifiers are bound to whatever s-expression is in that position. This is a bit analogous to a regular define, except notice the extra parens in the with syntax are no problem here.

#lang plai-typed
(define-syntax-rule (with (id val) body)
  (let [(id val)]
    body))

The body of the define-syntax-rule the racket code to replace the with form. Here is one test; add a couple more to convince yourself it is working.

(test (with (x 1) (+ x x)) 2)

Notice there is no quoting of the with in this test; it is now a Racket expression just like let.

Use define-syntax-rule to define syntax for fun and call so that the following tests (borrowed from the lectures) pass

  (test {with {add3 {fun {x} {+ x 3}}}
              {with {add1 {fun {x} {+ x 1}}}
                    {with {x 3}
                          {call add1 {call add3 x}}}}}
        7)

  (test {call {call {fun {x} {call x 1}}
                    {fun {x} {fun {y} {+ x y}}}}
              123}
        124))

Part 2. Short circuit

Some Racket forms like and and or look like function calls, but we know they are not because they support short-circuit evaluation. In this part we use define-syntax-rule to define And and Or, which are just like the lower case version except the short-circuit evaluation is from the right.

(define-syntax-rule (And a b)
  (if b a #f))

(define (die)
  (error 'die "don't run this"))

(test (And (die) #f) #f)
(test/exn (and (die) #f) "don't run this")

Define Or in a similar way to And.

(test (Or (die) #t) #t)
(test/exn (or (die) #t) "don't run this")

Part 3, multiple cases.

The syntax-rules form can be used test a sequence of patterns against a form starting with one identifier. The following code almost works to satisfy the tests. Thinking about cond, what small change do you need to make?

(define-syntax arghh
  (syntax-rules ()
    [(arghh) 0]
    [(arghh a) 1]
    [(arghh (a)) 2]))

(test (arghh) 0)
(test (arghh thptt) 1)
(test (arghh (die)) 2)

Part 4, with*, again.

Recall with* construct from Assignment 4. One of the ways to solve this was to convert with* into a nested set of with forms. It turns out this is fairly easy to do with syntax rules and recursion. A key ingredient is the ability to match sequences with syntax patterns.

Fill out the following syntax-rules in order to expand the with* into a with containing a with* (or if you prefer to skip a macro level, into a let containing a with*.

Note that ... is really valid racket syntax here, and you can place it in your expansion to mean "copy the matched sequence". Roughly speaking, (id val) ... matches a sequence of (id1 val1) (id2 val2) (id3 val3)

(define-syntax with*
  (syntax-rules ()
    [(with* () body) body]
    [(with* ((first-id first-val) (id val) ...) body)
        ]

(test {with* {{x 5} {y {- x 3}}} {+ y y}} 4)
(test {with* {{x 5} {y {- x 3}} {y x}} {* y y}} 25)