Syntactic sugar

We will define some additional syntax to facilitate entry of some common expressions. Recall that we already allow the user to enter

(A B C)

instead of

(A . (B . (C . NIL)))

Quoting

In order to include a literal symbol or list in an expression, we need to use the QUOTE operator. As a shortcut, we will define

'EXPR

to be equivalent to

(QUOTE EXPR)

So for example the following forms are equivalent:

Abbreviation Canonical form Evaluates to
'FOO (QUOTE FOO) FOO
'(+ 1 2) (QUOTE (+ 1 2)) (+ 1 2)
'(A . B) (QUOTE (A . B)) (A . B)

The lexer needs to know that the quote mark is a prefix (i.e., it can appear immediately before another token but is not necessarily a delimeter).

var (
    // whitespace separates tokens and is generally ignored
    whitespace = []byte{' ', '\t', '\r', '\n'}
    // prefix characters can appear before another token but aren't
    // necessarily delimiters
    prefix = []byte{'(', ')', '\''}
    // delimiters are characters that are not allowed in a symbol.
    // at the minimum, this must include all whitespace and
    // reserved characters.
    delimiters = []byte{' ', '\t', '\r', '\n', '(', ')'}
)
// lex extracts the next token from the input after skipping
// comments and leading whitespace. on end of input, it
// returns nil for both the token and the remainder.
func lex(input []byte) (token []byte, remainder []byte) {
    // skip whitespace
    input = skipws(input)

    // check for end of input
    if len(input) == 0 {
        return nil, nil
    }

    // check for prefix characters
    if bytes.IndexByte(prefix, input[0]) >= 0 {
        token, remainder = input[:1], input[1:]
        return token, remainder
    }
    .
    .
    .
}

Also read_expr must convert it to the correct list expresssion.

// read_expr reads the next expression from the input. an expression is
// either an atom or a list of expressions. returns the expression along
// with the remainder of the input and any errors.
// returns NIL and Error_EndOfInput on end of input. the caller must
// decide how to handle it.
// todo: result is not always updated by read. does that lead to bugs later?
func read_expr(input []byte, result *Atom) (remainder []byte, err error) {
    token, rest := lex(input)
    if token == nil { // end of input
        return nil, Error_EndOfInput
    }

    switch token[0] {
    case '(':
        return read_list(rest, result)
    case ')':
        // unexpected close paren
        return nil, Error_Syntax
    case '\'':
        *result = cons(make_sym([]byte("QUOTE")), cons(_nil, _nil))
        // set car(cdr(result))
        return read_expr(rest, &result.value.pair.cdr.value.pair.car)
    }
    err = read_atom(token, result)
    return rest, err
}

Testing

ID Input Output
1 (define x '(a b c)) X
2 x (A B C)
3 'x X
4 (define foo 'bar) FOO
5 foo BAR
6 ''() NIL

Function definitions

It is cumbersome to have to type a lambda expression every time we wish to define a function, so we will modify the DEFINE operator to avoid this.

(DEFINE (name args...) body...)

is equivalent to

(DEFINE name (LAMBDA (args...) body...))

Here's how:

// 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 {
        // evaluate special forms
        if op.value.symbol.EqualString("QUOTE") {
            .
            .
            .
        } else if op.value.symbol.EqualString("DEFINE") {
            // verify number and type of arguments
            if nilp(args) || nilp(cdr(args)) {
                return Error_Args
            }
            var val Atom
            var err error
            sym := car(args)
            if sym._type == AtomType_Pair {
                err = make_closure(env, cdr(sym), cdr(args), &val)
                sym = car(sym)
                if sym._type != AtomType_Symbol {
                    return Error_Type
                }
            } else if sym._type == AtomType_Symbol {
                if !nilp(cdr(cdr(args))) {
                    return Error_Args
                }
                err = eval_expr(car(cdr(args)), env, &val)
            } else {
                return Error_Type
            }
            if err != nil {
                return err
            }
            *result = sym
            return env_set(env, sym, val)
        } else if op.value.symbol.EqualString("LAMBDA") {
            .
            .
            .
        }
    }
    .
    .
    .
}

Testing

ID Input Output
7 (define (square x) (* x x)) SQUARE
8 (square 3) 9

Sweet!

func TestChapter09(t *testing.T) {
    env := env_create_default()

    for _, tc := range []struct {
        id     int
        input  string
        expect string
        err    error
    }{
        {id: 1, input: "(define x '(a b c))", expect: "X"},
        {id: 2, input: "x", expect: "(A B C)"},
        {id: 3, input: "'x", expect: "X"},
        {id: 4, input: "(define foo 'bar)", expect: "FOO"},
        {id: 5, input: "foo", expect: "BAR"},
        {id: 6, input: "''()", expect: "(QUOTE NIL)"},
        {id: 7, input: "(define (square x) (* x x))", expect: "SQUARE"},
        {id: 8, input: "(square 3)", expect: "9"},
    } {
        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)
        }
    }
}