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