idiocy.org

Let’s go write a Lisp ‐ part two

This is the second part of an attempt to write a Lisp‐like language as a meta‐circular evaluator in JavaScript. So far we’ve got it taking in a string and executing functions, but not much else. Here’s part one.

Variables

‘That’s all well and good’, I hear you cry, ‘but what about variables? How am I supposed to get any work done without being able to store things?’

OK, so I guess we can add variables.

First of all we’ll need somewhere to store them. Remember that object that we put all our functions into? Let’s rename that global. For convenience we’ll create a lispDefine function that stores things in it.

Finally we’ll create a lookup function for retrieving the values. We can use this in lispEval to do a look‐up for all symbols.

/* Environment object. */
const global = {}

/* A helper function for defining lisp variables. */
function lispDefine(env, name, value) {
  if (typeof name === "string") {
    env[Symbol.for(name)] = value
  }
  else if (typeof name === "symbol") {
    env[name] = value
  }
  else {
    throw "DEFINE: Unexpected type '" + typeof name
      + "' for '" + name + "'!"
  }
}

function lookupVariable(env, name) {
  if (env.hasOwnProperty(name)) {
    return env[name]
  }
  else {
    throw "LOOKUP: Unknown variable '" + Symbol.keyFor(name) + "'!"
  }
}

lispDefine(global, 'display', function(...args) {
  console.log(args.join(' '))
})

lispDefine(global, '+', function(...args) {
  return args.reduce((a, b) => a + b, 0)
})

lispDefine(global, '=', function(...args) {
  return args.reduce((carry, x) => carry & (args[0] === x), true)
})
function lispEval(item) {
  if (typeof item === "symbol") {
    return lookupVariable(global, item)
  }
  else if (Array.isArray(item)) {
    /* Check if the function is an "if". */
    if (item[0] === Symbol.for('if')) {
      lispIf(item.slice(1))
    }
    /* Or a "define". */
    else if (item[0] === Symbol.for('define')) {
      lispDefine(global, item[1], lispEval(item[2]))
    }
    else {
      /* We must always eval the function name element of the array as
       * we have no guarantee it's a function. */
      const f = lispEval(item[0])

      try {
        return lispApply(f, item.slice(1))
      }
      catch(err) {
        throw err + "\nEVAL: Calling '" + JSON.stringify(item[0]) + "'"
      }
    }
  }
  else {
    return item
  }
}
lispEval(parser(lexer('(define cows "moo 🐄")')))
lispEval(parser(lexer('(display cows)')))
moo 🐄

Functions

So far so good, but we have to write our functions in JavaScript, which is OK, I guess, but not very exciting. Let’s make a lambda function.

One thing we need to bear in mind is that we need to look up variables, which could be either local to the function or global. To save us having to do two look‐ups (local and global) for each variable we’ll build a data structure containing the local variables, and the global environment object. Then we can modify lookupVariable to check each in turn. (More recursion!)

/* Make a new environment object. */
function newEnvironment(parent) {
  const e = {}
  e.parent = parent
  return e
}

/* A helper function for defining lisp variables. */
function lispDefine(env, name, value) {
  if (typeof name === "string") {
    env[Symbol.for(name)] = value
  }
  else if (typeof name === "symbol") {
    env[name] = value
  }
  else {
    throw "DEFINE: Unexpected type '" + typeof name
          + "' for '" + name + "'!"
  }
}

function lookupVariable(env, name) {
  if (env.hasOwnProperty(name)) {
    return env[name]
  }
  else if (env.parent !== null) {
    /* Look it up in the parent environment. */
    return lookupVariable(env.parent, name)
  }
  else {
    throw "LOOKUP: Unknown variable '" + Symbol.keyFor(name) + "'!"
  }
}

const global = newEnvironment(null)

We’ll use a closure to contain the lisp code within a JavaScript function. This means we don’t have to differentiate between JavaScript and lisp functions within lispApply because they’re all just JavaScript functions!

function lambda(env, argnames, body) {
  return function(...args) {
    /* We need to build up a stack frame for this function where we
     * store local variables, eg. the arguments. */
    const localEnv = newEnvironment(env)

    /* For each arg, a, define a variable named argnames[i]. */
    args.forEach((a, i) => lispDefine(localEnv, argnames[i], a))

    /* And eval each expression in the body with our new environment.
     *
     * TODO: add a begin function and use it here. */
    return body.reduce((c, s) => lispEval(localEnv, s), null)
  }
}

Add environment handling to our other functions.

function lispIf(env, args) {
  /* Test argument 0 and evaluate argument 1 or 2 as appropriate. */
  if (lispEval(env, args[0])) {
    return lispEval(env, args[1])
  }
  // else
  return lispEval(env, args[2])
}

function lispApply(env, f, args) {
  if (typeof f !== "function") {
    throw "APPLY: Unknown function!"
  }

  const a = args.map((a) => lispEval(env, a))
  return f.apply(null, a)
}

And finally add environment handling to lispEval too, and because lambda is another special form we add it to lispEval. The arguments shouldn’t be evaluated, otherwise the function would be evaluated when we declare it instead of when we call it.

function lispEval(env, item) {
  if (typeof item === "symbol") {
    return lookupVariable(env, item)
  }
  else if (Array.isArray(item)) {
    /* Check if the function is an "if". */
    if (item[0] === Symbol.for('if')) {
      return lispIf(env, item.slice(1))
    }
    /* Or a "define". */
    else if (item[0] === Symbol.for('define')) {
      lispDefine(env, item[1], lispEval(env, item[2]))
    }
    /* Or "lambda"... */
    else if (item[0] === Symbol.for('lambda')) {
      return lambda(env, item[1], item.slice(2))
    }
    /* Or "quote". (Return the argument unmodified.) */
    else if (item[0] === Symbol.for('quote')) {
      return item[1]
    }
    else {
      try {
        return lispApply(env, lispEval(env, item[0]), item.slice(1))
      }
      catch(err) {
        throw err + "\nEVAL: Calling '" + JSON.stringify(item[0]) + "'"
      }
    }
  }
  else {
    return item
  }
}
lispEval(global, parser(lexer('(define cows (lambda (x) (display "cows" x "moo 🐄")))')))
lispEval(global, parser(lexer('(cows "go")')))
cows go moo 🐄

Looks good. Let’s try something more complex.

const sexp = `
  (define fib (lambda (n)
    (define iter (lambda (a b count)
                   (if (= n count)
                       (+ a b)
                       (iter b (+ a b) (+ count 1)))))
    (iter 1 1 3)))
`

lispEval(global, parser(lexer(sexp)))
lispEval(global, parser(lexer('(display "The 12th Fibonacci number is:" (fib 12))')))

The 12th Fibonacci number is 144. Let’s see what we get…

The 12th Fibonacci number is: 144

Wooo!

It may not be immediately obvious, but our lisp has lexical scope and you can use closures. This is because we’re passing lambda the environment of the calling function and it’s stored within the JavaScript closure that lambda creates, just like the program code itself.

REPL(ish)

I’ve hacked together a rather dodgy REPL.

I’ve added some more functions:

Arithmetic
+, -, *, /, , >, <, >, <=, %
Boolean arithmetic
and, or, not
Array handling
first, rest, push, pop, shift, unshift, array-ref, concat
Strings
split (no need for join as you can use ’+’ for that)
Flow control
map, reduce
Others
null (returns null, shockingly), get-js-context (you can use this to get JavaScript functions)

The complete code is here, and you can try it out below.











t @flxzr
g alanthird
e Alan Third