Arithmetic

So far all we've been able to do is create and name objects. Some of those objects have been numbers — naturally we would like to do calculations with those numbers.

In the last chapter we saw how to create built-in functions to tell eval_expr how to process arguments into a return value. We will now create four more builtins to perform the basic arithmetic operations.

Expression Result
(+ X Y) The sum of X and Y
(- X Y) The difference of X and Y
(* X Y) The product of X and Y
(/ X Y) The quotient of X and Y

In the definitions above, when we write "the sum of X and Y", what we really mean is "the sum of the values obtained by evaluating X and Y". Remember that eval_expr will evaluate all the arguments to a functions by default; this is usually what we want to happen, so from now on we will not explicitly state this where the intent is obvious.

Implementation

Once again almost all of our function consists of checking that the correct arguments were supplied. Finally the result is constructed by the call to make_int.

// builtin_add implements a function for calculating the sum of two numbers.
// note that the result may not be updated if we find errors.
func builtin_add(args Atom, result *Atom) error {
    // verify number and type of arguments
    if nilp(args) || nilp(cdr(args)) || !nilp(cdr(cdr(args))) {
        return Error_Args
    }
    a, b := car(args), car(cdr(args))
    if a._type != AtomType_Integer || b._type != AtomType_Integer {
        return Error_Type
    }

    *result = make_int(a.value.integer + b.value.integer)
    return nil
}

The other three functions differ by only one character, so I will omit them here. (One note: divide will panic on divide by zero.)

Finally we need to create bindings for our new functions in the initial environment:

// env_create_default creates a new environment with some native
// functions added to the symbol table.
func env_create_default() Atom {
    .
    .
    .
    env_set(env, make_sym([]byte{'+'}), make_builtin(builtin_add));
    env_set(env, make_sym([]byte{'-'}), make_builtin(builtin_subtract));
    env_set(env, make_sym([]byte{'*'}), make_builtin(builtin_multiply));
    env_set(env, make_sym([]byte{'/'}), make_builtin(builtin_divide));

    // return the new environment
    return env
}

Testing

We now have our very own LISP-style calculator.

ID Input Output
1 (+ 1 1) FOO
2 (define x (* 6 9)) X
3 x 54
4 (- x 12) 42
5 (/ 108 x) 2

In input 4 above, note that X is a symbol, not an integer. We have to evaluate the arguments so that builtin_subtract can operate on the integer value bound to X and not the symbol X itself. Similarly the value bound to X is the integer result of evaluating the expression (* 6 9).

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

    for _, tc := range []struct {
        id     int
        input  string
        expect string
        err    error
    }{
        {id: 1, input: "(+ 1 1)", expect: "2"},
        {id: 2, input: "(define x (* 6 9))", expect: "X"},
        {id: 3, input: "x", expect: "54"},
        {id: 4, input: "(- x 12)", expect: "42"},
        {id: 5, input: "(/ 108 x)", expect: "2"},
    } {
        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)
        }
    }
}