Macros

Macros allow you to create new special forms at runtime. Unlike a function, the arguments to a macro are not evaluated. The result of evaluating the body of the macro is then itself evaluated.

Note: these are (essentially) Common LISP macros. Scheme has a different macro system, which avoids problems with identifiers introduced by the macro, but is more complex.

We will define macros using the following syntax:

(DEFMACRO (name arg...) body...)

This matches our DEFINE syntax for functions, but is slightly different from the form used in Common LISP.

Example

Take the macro IGNORE defined by:

(DEFMACRO (IGNORE X)
  (CONS 'QUOTE
    (CONS X NIL)))

If we then evaluate the expression

(IGNORE FOO)

where FOO need not be bound, the body of IGNORE will first be evaluated with the argument X bound to the unevaluated symbol FOO. The result of evaluating the nested CONS expressions within this environment is:

(QUOTE . (FOO . NIL))

which is of course equivalent to:

(QUOTE FOO)

Finally, evaluating this value (which is the result of evaluating the macro body) gives us:

FOO

Implementation

We will define a new type of atom:

// AtomType is the enum for the type of value in a cell.
type AtomType int

const (
    .
    .
    .
    // AtomType_Macro is a macro.
    AtomType_Macro
    .
    .
    .
)

the value of which is the same as AtomType_Closure.

And now simply teach eval_expr about our new macro type.

// eval_expr evaluates an expression with a given environment and updates the result.
// note that the result may not be updated if we find errors.
func eval_expr(expr, env Atom, result *Atom) error {
    .
    .
    .
    op, args := car(expr), cdr(expr)
    if op._type == AtomType_Symbol {
        .
        .
        .
        } else if op.value.symbol.EqualString("DEFMACRO") {
            // verify number and type of arguments
            if nilp(args) || nilp(cdr(args)) {
                return Error_Args
            } else if car(args)._type != AtomType_Pair {
                return Error_Syntax
            }

            name := car(car(args))
            if name._type != AtomType_Symbol {
                return Error_Type
            }

            var macro Atom
            if err := make_macro(env, cdr(car(args)), cdr(args), ¯o); err != nil {
                return err
            }

            *result = name
            return env_set(env, name, macro)
        }
    }

    // evaluate and update the operator
    .
    .
    .

    // is it a macro?
    if op._type == AtomType_Macro {
        op._type = AtomType_Closure
        var expansion Atom
        if err := apply(op, args, &expansion); err != nil {
            return err
        }
        return eval_expr(expansion, env, result)
    }

    // evaluate arguments by calling eval on a copy of each.
    .
    .
    .
}

We will use macros in the future to define some new special forms.

Testing

ID Input Output
1 (defmacro (ignore x) (cons 'quote (cons x nil))) IGNORE
2 (ignore foo) FOO
3 foo unbound
func TestChapter11(t *testing.T) {
    env := env_create_default()

    for _, tc := range []struct {
        id     int
        input  string
        expect string
        err    error
    }{
        {id: 1, input: "(defmacro (ignore x) (cons 'quote (cons x nil)))", expect: "IGNORE"},
        {id: 2, input: "(ignore foo)", expect: "FOO"},
        {id: 3, input: "foo", expect: "NIL", err: Error_Unbound},
    } {
        var expr Atom
        _, err := read_expr([]byte(tc.input), &expr)
        if err != nil {
            t.Errorf("%d: read error: want nil: got %v\n", tc.id, err)
            continue
        }

        var result Atom
        err = eval_expr(expr, env, &result)

        if tc.err == nil && err == nil {
            // yay
        } else if tc.err == nil && err != nil {
            t.Errorf("%d: error: want nil: got %v\n", tc.id, err)
        } else if !errors.Is(err, tc.err) {
            t.Errorf("%d: error: want %v: got %v\n", tc.id, tc.err, err)
        }
        if got := result.String(); tc.expect != got {
            t.Errorf("%d: eval: want %q: got %q\n", tc.id, tc.expect, got)
        }
    }
}