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.
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
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.
| 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)
}
}
}