software, programming, functional
home mail me! RSS (2.0) feed

Embedded Lisp – via Lua

A few months ago I had one of those nights where you just do not feel like sleeping. That time, I created an interpreting environment for a mini Lisp in Lua. I call that LuaLisp.
DISCLAIMER to all die-hard Scheme fanatics out there: I use the term Lisp losely here, to the extent of including Scheme. LuaLisp is actually more of a Scheme interpreter.

The upside of this nocturnal feat, beside showing the coolness of these two specific languages, is that the small footprint of Lua translates to an embeddable and small Lisp environment. Since I decided to complete the project in one night, the Lisp environment was forced to be small.Yes, I do get a bit slow after 4 AM :-(

There are two approaches to this post: (1) follow me through my attempts at exposing implementation techniques and decisions for a Lisp interpreter or (2) just download the files from the bottom of this post or from the Google project page and start using it.

To enjoy this post, it is helpful, but certainly not necessary, to understand the basics of Lua. You can find such information at its home page. There is only one book available about Lua - besides tiny script snippets in game development literature - as far as I know, which can also be downloaded as a PDF from the Lua site for free:
Programming in Lua, Second Edition

For an introduction to Lisp or Scheme (the relation between the two is analogous to the words "Unix" and "Linux", by the way), I would recommend two books:
The Little Schemer - 4th Edition Structure and Interpretation of Computer Programs - 2nd Edition (MIT Electrical Engineering and Computer Science).
For a more detailed treatise about Scheme, and that acted as a source of motivation for LuaLisp, buy:
The Scheme Programming Language: Third Edition.

Now that we have the knowledge groundwork covered, let us continue with LuaLisp...

How To Implement Lisp In Lua

Let us first look at the three most striking features of Lisp:

  1. The simple and regular syntax - yes, those parentheses :-)
  2. Functions and closures being first class citizens - ok, this might not sound that exotic for you lucky bastards that are raised in the modern IT world, where all popular languages (except for Java) support this to some extent, but for me, when encountering Lisp back in 1984, it was an eye opener
  3. Same syntax for data and code

Some people would add beauty as the fourth striking feature ;-)

Feature #1 makes parsing extremely simple, and it took me about 160 minutes to create one in Lua, whereof 90 minutes were spent on Lua idiosyncracies - I was a newbie of Lua that evening. This does not say much about me, but speaks a lot about the (eye-pleasing or not...) simplicity of the Lisp syntax. In fact, it probably takes longer to write this post about that system than building that system :-)

Feature #2 happens to be embeddable into Lua's closure system, which comes very handy at around 2 AM. I.e., I translated Lisp closures directly into Lua closures, and even provided callback integration, which allowed Lisp closures to be called from Lua code.

Feature #3 is focused around symbolic expressions, or S-Expr for short. This is the heart and soul of Lisp. It is best defined and explained in the must-have book Anatomy of Lisp by John Allen. This book is a bit hard to get, and when I lent my good friend Luke Flemmer my copy, he just got to smell it before I needed it back. It is that good! For Luke and other curious (double semantics...) minds, you can find it at Anatomy of Lisp (McGraw-Hill computer science series). If you think $50 for a used copy is a bit steep, wait till you buy it and read it. The joy that book brings exceeds those 4 pitchers of beer you would get around the corner. Just like this other classic, you must preempt your mind from buzz before reading it.

So, how did I implement these features and the rest of Lisp?

I started, as mentioned above, with the S-Expr parser, which would by feature #3 handle both data structures and code. Neat.

I did not use a proper compiler-compiler tool, such as discussed in previous posts, but used good old ad hoc hand-coded magic. I would not use this approach for a language with more complex syntax. But due to feature #1, it worked here.

The parser consists of two files: Token.lua and Parser.lua. Both these files are found in their entirety in the bottom of this post. These two files corresponds to what computer scientists would call lexical analyzer and parser, respectively.

Token.lua is extremly simple, and contains only five tokens. That is right: 5! Here is the complete content:

LUA:
  1. -- Token.lua
  2. --
  3. -- Tokens, which are unevaluated atoms, list delimiters or special
  4. -- operators
  5. --
  6.  
  7. Token = {}
  8.  
  9. function Token.newString(str)
  10.    return { .token="STR", .lexeme=str }
  11. end
  12.  
  13. function Token.newLeftParen()
  14.    return { .token="LEFTPAREN", .lexeme="(" }
  15. end
  16.  
  17. function Token.newRightParen()
  18.    return { .token="RIGHTPAREN", .lexeme=")" }
  19. end
  20.  
  21. function Token.newComma()
  22.    return { .token="COMMA", .lexeme=")" }
  23. end
  24.  
  25. function Token.newQuote()
  26.    return { .token="QUOTE", .lexeme"'" }
  27. end

Side Note: I just realized that my WordPress code hiliter handles Lua syntax :-)
Note: Lua has only tables, so to create a Token object, one just needs to return a new table with the keys that a Token should have. This open structure is of course familiar to both Erlang and JavaScript developers.

The stream of tokens is created by an invocation of Parser.parseTokens, in Parser.lua. The parser then acts on this stream. Both this lexical and more semantic analysis are triggered by the Parser.parseExpr function.

The aforementioned function returns Sexpr objects, which are defined in Sexpr.lua.

Let us try this parser before continuing! Download the LuaLisp files, a Lua implementation and open a command window or shell and direct yourself to the download directory. Create a new test app in Lua, TestParser.lua as follows:

LUA:
  1. require "Parser"
  2. sexpr = Parser.parseSexpr(arg[1])[1]
  3. print (sexpr)

Note: the parser actually returns a list of Sexpr, and we here expect only one expression, thereof the [1] at the end. Note: Lua uses index 1 as the first index, by convention.

Pass it a simple S-Expr, such as 7:

% lua TestParser.lua 7
atom[type=NUM, lex="7"]

See, we got an Sexpr object representing 7. Ok, I must show that I am not bluffing, so let us expose that object in a more user-friendly manner. This is done via pretty-printing (in fact, the pretty printing function in Sexpr.lua is probably the second largest function in the whole system...):

LUA:
  1. require "Parser"
  2. sexpr = Parser.parseSexpr(arg[1])[1]
  3. print(Sexpr.prettyPrint(sexpr))

Try with the expression 7 again:

% lua TestParser.lua 7
7

One last adventorous run before we continue beyond parsing:

% lua TestParser.lua "((lambda (x) (+ x 2)) 5)"
((lambda (x) (+ x 2)) 5)

Note: the apostrophes around the argument is stripped before arriving to Lua.
Hey, should it not be 7? After all, adding two to five should yield seven, right?

Well, remember that we are only parsing the expressions, not evaluating them.

The Sexpr.lua module is the glue between "compile-time" (i.e., parsing) and runtime.

So we have three files handling the syntax: Token.lua, Parser.lua and Sexpr.lua, where the former two modules manifest the "compile-time" syntax while the latter module represents a live syntax, which will follow the execution. In fact, the former two can be brought into execution via introspection (the Lisp function eval and cousins.)

In runtime, there are three new modules assisting: Environment.lua, LispInterpreter.lua and Prelude.lsp. What the heck is a .lsp file doing among the Lua files?! Well, the idea behind LuaLisp is to implement a minimal core language, to bootstrap the environment, upon which the actual Lisp primitives are built. This separation enables bringing in distinct Lisp dialects via separate "prelude" files.

Let us look at Prelude.lsp to get a better sense.

LISP:
  1. (defmacro defun (name params body)
  2.   (setq name (lambda params body)))
  3.  
  4. (defmacro or (a b)
  5.   (if a a b))
  6.  
  7. (defmacro and (a b)
  8.   (if a b nil))
  9.  
  10. (defun  <= (x y)
  11.   (or (< x y) (eq x y)))
  12.  
  13. (defun > (x y)
  14.   (< y x))
  15.  
  16. (defun >= (x y)
  17.   (<=  y x))
  18.  
  19. (defun - (x y)
  20.   (+ x (neg y))))
  21.  
  22. (defun nullp (x)
  23.   (eq x nil))

As the experienced Lisp and/or Scheme practioner can readily see, I am mixing Scheme syntax ((lambda (x) ...)) and Common Lisp syntax ((defun ...)). May McCarthy forgive me! But wait, that blesphamy is only an inch deep (or, two lines, to be exact), since I define that defun inside the prelude itself. This is done via a macro facility I had to create as part of the core language. Basically, a macro is a call-by-name function. In Lisp lingo, it is called a special form.

This bootstrapping makes it possible to introduce a more Schemish define construct instead, as:

LISP:
  1. (defmacro define ((name . params) body)
  2.   (setq name (lambda params body)))

after which we can define a function as in Scheme:

LISP:
  1. (define (fac n) (if (eq n 0) 1 (* n (fac (- n 1)))))

My special form defmacro is a hybrid between similarly named constructs found in Common Lisp and the special translating forms of Scheme. Just view it as a "really cool thing that does not evaluate stuff till it is really needed" :-)

To get into a bit more detail, the interpreter actually consists of three layers:

  1. The engine
  2. The core primitives, being part of the core language, and not corresponding to any concrete syntax. They are mapped to operations inside the engine
  3. The dialect primitives, mapped onto basic (and sometimes core...) primitives via a prelude Lisp file.

We have seen layer #3, so let us look at the other two layers.

The core primitives are defined in LispInterpreter.lua. They should be put in a separate "core language" Lua file, but I must have been too tired :-(

Anyway, the core primitives are

  1. prim_car - get the first part of a pair
  2. prim_cdr - get the second part of a pair
  3. prim_cons - create a new pair
  4. prim_plus - add two numbers
  5. prim_mult - multiply two numbers
  6. prim_lambda - create a parameterized closure
  7. prim_if - select one of two expressions to evaluate
  8. prim_eq - test two S-Expr for equivalence
  9. prim_lt - is one number smaller than another?
  10. prim_consp - is the given S-Expr a pair?
  11. prim_neg - take the negation of one number
  12. prim_setq - bind a variable in the current environment
  13. prim_eval - evaluate an S-Expr inside the current environment; this provides reflection
  14. prim_load - load and execute code from a file
  15. prim_echo - just pretty print the given S-Expr
  16. prim_defmacro - create a new call-by-name construct

As the attentive reader (and thinker) can guess, all Lisp code is executed in terms of core primitives in the end.

How are the core primitives brought into environment of the Lisp code? I said that the core primitives themselves did not have a concrete syntax, and without concrete syntax, it is hard to create code ;-)
Ok, first one has to understand what an environment is. An environment is a mapping from variables to their values, simplistically. So, before the Lisp code is executed - in fact even before the prelude is executed - an initial environment is setup by the following function in LispInterpreter.lua:

LUA:
  1. function Lisp.getPrimitiveScope()
  2.    return {
  3.       car = Sexpr.newFun("car", Lisp.prim_car),
  4.       cdr = Sexpr.newFun("cdr", Lisp.prim_cdr),
  5.       cons = Sexpr.newFun("cons", Lisp.prim_cons),
  6.       lambda = Sexpr.newFun("lambda", Lisp.prim_lambda, "lazy"),
  7.       setq = Sexpr.newFun("setq", Lisp.prim_setq, "lazy"),
  8.       ["<"] = Sexpr.newFun("<", Lisp.prim_lt),
  9.       ["+"] = Sexpr.newFun("+", Lisp.prim_plus),
  10.       neg = Sexpr.newFun("neg", Lisp.prim_neg),
  11.       eq = Sexpr.newFun("eq", Lisp.prim_eq),
  12.       consp = Sexpr.newFun("consp", Lisp.prim_consp),
  13.       eval = Sexpr.newFun("eval", Lisp.prim_eval),
  14.       load = Sexpr.newFun("load", Lisp.prim_load),
  15.       echo = Sexpr.newFun("echo", Lisp.prim_echo),
  16.       defmacro = Sexpr.newFun("defmacro", Lisp.prim_defmacro, "lazy"),
  17.       ["if"] = Sexpr.newFun("if", Lisp.prim_if, "lazy")
  18.    }
  19. end

The string lazy indicates that the parameters supplied to this function will not be evaluated before invoking the function, i.e., call-by-name semantics.

After this, the prelude is executed inside that environment, enrichening the environment further.

The environment is our store, for values and functions, and will follow the Lisp session wherever it goes. Lua tables can also be used to associate values with keys. So, that is basically what I use for the Lisp environments, the Lua table. I say basically cause there is one caveat: Lisp allows for nested environments, which constitute a chain of environments, from the most local one to the global environment. Since we do not want to change the global variable fib just because we define a local variable with the same name, we have to seperate these environments into distinct Lua tables. The lookup then has to traverse this chain from local to global scope:

LUA:
  1. function Env:lookup(symbol)
  2.    for i = self.scopeCount, 1, -1 do
  3.       local tab = self.scopes[i]
  4.       local val = tab[symbol]
  5.       if val then
  6.      return val
  7.       end
  8.    end
  9.    return nil
  10. end

One nice syntactic extension capability of Lua is the meta keys for any table, which allows us to overload the primitive Lua operations. We can thereby route index accesses to our chain-traversing lookup, by the following code:

LUA:
  1. Env.mt = {
  2.    __index = Env.lookup,
  3.    __newindex = Env.add,
  4.    __tostring = Env.tostring
  5. }

After that, using this simple Lua syntax will do what we want:

LUA:
  1. value = env["foo"]
  2. env["foo"] = 42
  3. env["bar"] = 33 -- this will create a new binding in the most local scope

The evaluation of parsed Sexpr is relatively simple once environments are there.

We have an expression evaluator in LispInterpreter.lua:

LUA:
  1. function Lisp.evalSexpr(env, sexpr)
  2.    local value
  3.    if not sexpr.type then
  4.       error("Invalid S-expr: " .. sexpr, 2)
  5.    end
  6.    if sexpr.type == "CONS" then
  7.       -- 1. Cons cell
  8.       local car = sexpr.car
  9.       if car.type=="OP" and car.lexeme=="'" then
  10.      value = sexpr.cdr
  11.       elseif car.type=="OP" and car.lexeme=="`" then
  12.      local cdr = Lisp.evalQuote(env, sexpr.cdr)
  13.      value = cdr
  14.       else
  15.      local fun = Lisp.evalSexpr(env, car)
  16.      if not fun or fun.type ~= "FUN" then
  17.         error("The S-expr did not evaluate to a function: " ..
  18.           tostring(car))
  19.      end
  20.      -- The function can be eithe "lazy", in that it deals with
  21.      -- evaluation of its arguments itself, a "macro", which requires
  22.      -- a second evaluation after the macro expansion, or
  23.      -- a regular eager one
  24.  
  25.      local args
  26.      if fun.special == "lazy" or fun.special == "macro"  then
  27.         args = sexpr.cdr
  28.      else
  29.         args = Lisp.evalList(env, sexpr.cdr)
  30.      end
  31.      value = fun.fun(env, args)
  32.       end
  33.    elseif sexpr.type == "SYM" then
  34.       -- a. symbol
  35.       value = env[sexpr.lexeme]
  36.       if not value then
  37.      error("The symbol '" .. sexpr.lexeme .. "' is not defined")
  38.       end
  39.    else
  40.       -- b. constant
  41.       value = sexpr
  42.    end
  43.    return value
  44. end

We see that we have three top-level cases in the evaluation function:

  1. a pair - or CONS cell, in which case we check whether the first component is a "special" operator (there are a few "escaping" operators in Lisp, such as the quote or backquote operators). If not, we evaluate the first component, which should result in a Lisp function, and if that is not a lazy function, we also evaluate the parameters, after which we apply the function
  2. a symbol, which is looked up in the passed environment and the associated value becomes the result
  3. a constant, such as a numerical literal or a quoted symbol, which is returned as is

Besides these entry points to the system, there also is an interactive shell, to test or execute Lisp code. That interactive shell is defined in a seventh file, Interaction.lua. A guide and sample runs is found below.

Interactive Lisp Shell

An interactive session is started by
lua Interpreter.lua

Here is a sample session:

> 42
42
> 'foo
foo
> foo
#error: LispInterpreter.lua:92: The symbol 'foo' is undefined
> (lambda (x) (+ x 1))
#'(lambda (x) (+ x 1))
> +
#'+
> (defun !(n) (if (eq n 0) 1 (* n (! (- n 1)))))
#'(lambda (n) (if (eq n 0) 1 (* n (! (- n 1)))))
> (! 5)
120

Note 1: I use a Common Lisp syntax for showing a lambda closure, preceded by #' (which is shorthand for function.)
Note 2: you can use quite special names for variables (and functions), such as !.

Please start the interactive shell and try yourself.

I actually created a newer version of LuaLisp a few days later, but cannot find it right now :-( That version included reflection mechanisms and transparent integration between Lisp and Lua closures, enabling Lisp functions to be called from native Lua, with automatic S-Expr wrapping of native values. Will inform you when I find it...

The Code

Here are the source files, which all adher to the M.I.T. License - read the license.txt file. The project is hosted at Google's code site, wherefrom you can download the code as well.

First, we need a few tokens:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
-- Token.lua
-- 
-- Tokens, which are unevaluated atoms, list delimiters or special
-- operators
--
 
Token = {}
 
function Token.newString(str)
   return { .token="STR", .lexeme=str }
end
 
function Token.newLeftParen()
   return { .token="LEFTPAREN", .lexeme="(" }
end
 
function Token.newRightParen()
   return { .token="RIGHTPAREN", .lexeme=")" }
end
 
function Token.newComma()
   return { .token="COMMA", .lexeme=")" }
end
 
function Token.newQuote()
   return { .token="QUOTE", .lexeme"'" }
end

Download this code: Token.lua

Then we can parse those tokens:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
-- Parser.lua
-- Author: David Bergman
-- 
-- A Scheme parser
--
 
require "Sexpr"
 
Parser = {
   operators = { ["("] = true, [")"] = true, [","] = true,
      ["'"] = true, ["`"] = true, ["."] = true }
}
 
-- Parse the code snippet, yielding a list of (unevaluated) S-expr
function Parser.parseSexpr(expr)
   local tokenList = Parser.parseTokens(expr)
   local expr
   local next = 1
   local sexprList = {}
   repeat
      next, sexpr = Parser.createSexpr(tokenList, next)
      if sexpr then
	 table.insert(sexprList, sexpr)
      end
   until not sexpr
   return sexprList
end
 
function Parser.createSexpr(tokens, start)
   -- If the first token is a '(', we should expect a "list"
   local firstToken = tokens[start]
   if not firstToken then
      return start, nil
   end
   if firstToken.type == "LEFTPAREN" then
      return Parser.createCons(tokens, start+1)
   elseif firstToken.type == "OP" then
      local next, cdr = Parser.createSexpr(tokens, start+1)
      return next, Sexpr.cons(firstToken, cdr)
   else
      return start+1, firstToken
   end
end
 
function Parser.createCons(tokens, start)
   -- If the first token is a '.', we just return the second token,
   -- as is,
   -- while skipping a subsequent ')',
   -- else if it is a ')' we return NIL, else
   -- we get the first Sexpr and CONS it with the rest
 
   local firstTok = tokens[start]
   if not firstTok then
      error("Token index " .. start ..
	    " is out of range when creating CONS S-Expr", 2)
   end
   if firstTok.type == "OP" and firstTok.lexeme == "." then
      -- We skip the last ')'
      local next, cdr = Parser.createSexpr(tokens, start+1)
      if not tokens[next] or tokens[next].type~="RIGHTPAREN" then
	 error("The CDR part ending with " .. tokens[next-1].lexeme ..
	       " was not followed by a ')'")
      end
      return next+1, cdr
   elseif firstTok.type == "RIGHTPAREN" then
      return start+1, Sexpr.newAtom("nil")
   else
      local next, car = Parser.createSexpr(tokens, start)
      local rest, cdr = Parser.createCons(tokens, next)
      return rest, Sexpr.cons(car, cdr)
   end
end
 
-- Parse a sub expression, returning both an expression and
-- the index following this sub expression 
function Parser.parseTokens(expr)
   tokens = {}
 
   -- We do it character by character, using queues to
   -- handle strings as well as regular lexemes
 
   local currentToken = {}
   local inString = false
   local isEscaping = false
   for i = 1, string.len(expr) do
      local c = string.sub(expr, i, i)
      -- We have seven (7) main cases:
      if isEscaping then
	 -- 1. Escaping this character, whether in a string
	 -- or not
	 --
	 table.insert(currentToken, c)
	 isEscaping = false
      elseif c == "\\\\" then
	 -- 2. An escape character
	 --
	 isEscaping = true
      elseif c == "\\""  then
	 -- 3. A quotation mark
	 --
	 -- Two sub cases:
	 if not inString then
	    -- a. starting a new string
	    -- If we already had a token, let us finish that
	    -- up first
	    if table.getn(currentToken) > 0 then
	       table.insert(tokens,
			Sexpr.newAtom(table.concat(currentToken)))
	    end
	    currentToken = {}
	    inString = true
	 else
	    -- b. ending a string
	    table.insert(tokens,
		Sexpr.newString(table.concat(currentToken)))
	    currentToken = {}
	    inString = false
	 end
      elseif inString then
	 -- 4. inside a string, so just add the character
	 --
	 table.insert(currentToken, c)
      elseif Parser.operators[c] then
	 -- 5. special operator (and not inside string)
	 --
	 -- We add any saved token
	 if table.getn(currentToken) > 0 then
	    table.insert(tokens,
		Sexpr.newAtom(table.concat(currentToken)))
	    currentToken = {}
	 end
	 table.insert(tokens, Sexpr.newOperator(c))
      elseif string.find(c, "%s") then
	 -- 6. A blank character, which should add the current
	 -- token, if any
	 -- 
	 if table.getn(currentToken) > 0 then
	    table.insert(tokens,
		Sexpr.newAtom(table.concat(currentToken)))
	    currentToken = {}
	 end
      else
	 -- 7. A non-blank character being part of the a symbol
	 --
	 table.insert(currentToken, c)
      end
   end
   -- Add any trailing token...
   if table.getn(currentToken) > 0 then
      local atom
      if inString then
	 atom = Sexpr.newString(table.concat(currentToken))
      else
	 atom = Sexpr.newAtom(table.concat(currentToken))
      end
      table.insert(tokens, atom)
   end
   return tokens
end
 
 

Download this code: Parser.lua

The parsed objects are S-Expr, represented by:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
-- Sexpr.lua
-- Author: David Bergman
--
-- Deals with (unevaluated or not) S-expressions, which are simply
-- atoms or CONS cells
--
-- The atoms are either
-- 1. Literals (t or nil)
-- 2. Numericals
-- 3. Operators [',`] 
-- 4. Symbols
-- 5. Function references
 
Sexpr = {}
 
Sexpr.constants = { ["t"] = true, ["nil"] = true }
Sexpr.mt = {}
function Sexpr.mt.__tostring(expr)
   local str
   if expr.type == "CONS" then
      str = "(" .. tostring(expr.car) .. " . " .. tostring(expr.cdr) .. ")"
   else
      str = "atom[type=" .. expr.type .. ", lex=\\"" .. expr.lexeme .. "\\"]"
   end
   return str
end
 
-- Atoms
 
function Sexpr.newBool(cond)
   local atom
   if cond then
      atom = Sexpr.newAtom("t")
   else
      atom = Sexpr.newAtom("nil")
   end
   return atom
end
 
function Sexpr.newString(content)
   local atom = { type="STR", lexeme=content }
   setmetatable(atom, Sexpr.mt)
   return atom
end
 
function Sexpr.newOperator(op)
   local type
   if op == "(" then
      type = "LEFTPAREN"
   elseif op == ")" then
      type = "RIGHTPAREN"
   else
      type = "OP"
   end
   local atom = { type=type, lexeme=op }
   setmetatable(atom, Sexpr.mt)
   return atom
end
 
function Sexpr.newAtom(atom)
   -- Make sure to use the string from here on
   atom = tostring(atom)
   local sexpr
   -- three cases
   if Sexpr.constants[atom] then
      -- 1. Constant
      sexpr = { type="LITERAL", lexeme=atom }
   elseif string.find(atom, "^%d+$") then
      -- 2. Numerical
      sexpr = { type="NUM", lexeme=atom }
   else
      -- 3. Symbol
      sexpr = { type="SYM", lexeme=atom }
   end
   setmetatable(sexpr, Sexpr.mt)
   return sexpr
end
 
-- Create a new function reference, where the
-- special parameter can be nil (for a normal function)
-- or 'lazy' for functions handling their own internal
-- evaluation, or 'macro' for functions mereley replacing
-- their body, for further evaluation
function Sexpr.newFun(name, fun, special)
   return { type="FUN", lexeme=name, fun=fun, special=special }
end
 
function Sexpr:car()
   return self.car
end
 
function Sexpr:cdr()
   return self.cdr
end
 
function Sexpr.cons(a, b)
   local sexpr = { type="CONS", car = a, cdr = b }
   setmetatable(sexpr, Sexpr.mt)
   return sexpr
end
 
 
-- Pretty printer
 
function Sexpr.prettyPrint(sexpr, inList)
   local pretty
   if sexpr.type == "CONS" then
      local str = {}
      -- If we are inside a list, we skip the initial
      -- '('
      if inList then
	 table.insert(str, " ")
      else
	 table.insert(str, "(")
      end
      table.insert(str, Sexpr.prettyPrint(sexpr.car))      

      -- Pretty print the CDR part in list mode
      table.insert(str, Sexpr.prettyPrint(sexpr.cdr, true))

      -- Close with a ')' if we were not in a list mode already
      if not inList then
	 table.insert(str, ")")
      end
      pretty = table.concat(str)
   else
      local str = {}
      if inList and
	 (sexpr.type ~= "LITERAL" or sexpr.lexeme ~= "nil") then
	 table.insert(str, " . ")
      end
      if sexpr.type == "FUN" then
	 if sexpr.special == "macro" then
	    table.insert(str, "#macro'")
	 else
	    table.insert(str, "#'")
	 end
      end
      -- We just add the lexeme, unless we are a nil in the
      -- end of a list...
      if not inList or sexpr.type ~= "LITERAL" or
	 sexpr.lexeme ~= "nil" then
	 if sexpr.type == "STR" then
	    table.insert(str, "\\"")
	 end
	 table.insert(str, sexpr.lexeme)
	 if sexpr.type == "STR" then
	    table.insert(str, "\\"")
	 end
      end
      pretty = table.concat(str)
   end
   return pretty
end
 

Download this code: Sexpr.lua

Values, being S-Expr are stored and retrieved from environments:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
-- Environment class
-- Author: David Bergman
-- Env.new : Creating a new environment
-- env[symbol] : lookup symbol (i.e., symbol:lookup(symbol) )
-- Env:lookup : lookup symbol
-- Env:addScope : add local scope
 
Env = { }
 
-- Lookup a symbol, going from the most local to the most
-- global scope
function Env:lookup(symbol)
   for i = self.scopeCount, 1, -1 do
      local tab = self.scopes[i]
      local val = tab[symbol]
      if val then
	 return val
      end
   end
   return nil
end
 
-- Add a new key or change an existing one in the most
-- local scope
function Env:add(key, value)
   self.scopes[self.scopeCount][key] = value
   return self.scopes[self.scopeCount][key]
end
 
-- Create a string representation of the environment
function Env:tostring()
   local str = {}
   table.insert(str, "Environment[scopeCount=" ..
		self.scopeCount .. "\\n")
   for _, scope in ipairs(self.scopes) do
      table.insert(str, "Scope[")
      for key, value in pairs(scope) do
	 table.insert(str, tostring(key))
	 table.insert(str, "=")
	 table.insert(str, tostring(value))
	 table.insert(str, " ")
      end
      table.insert(str, "]\\n")
   end
   table.insert(str, "]")
   return table.concat(str)
end
 
function Env:addBindings(formalList, actualList)
   local localScope = {}
   Env.bind(localScope, formalList, actualList)
   return self:addLocalScope(localScope)
end
 
function Env.bind(scope, formalList, actualList)
   if formalList.type=="CONS" then
      scope[formalList.car.lexeme] = actualList.car
      Env.bind(scope, formalList.cdr, actualList.cdr)
   end
end
 
-- Create local scope and return new extended environment
function Env:addLocalScope(localScope)
   -- Add a new empty local scope
   local newScopes = {}
   for _, scope in ipairs(self.scopes) do
      table.insert(newScopes, scope)
   end
   table.insert(newScopes, localScope)
   local newEnv = { scopeCount = self.scopeCount + 1,
      scopes = newScopes,
      add = Env.add, addBindings = Env.addBindins,
      addLocalScope=Env.addLocalScope }
    setmetatable(newEnv, Env.mt)
    return newEnv
end
 
Env.mt = {
   __index = Env.lookup,
   __newindex = Env.add,
   __tostring = Env.tostring
}
 
function Env.new(initialScope)
   -- The scopes are stored from most global to most local
   local env =  { scopeCount = 1, scopes = {initialScope},
      add = Env.add,
      addBindings = Env.addBindings,
      addLocalScope = Env.addLocalScope,
      lookup=Env.lookup }
   setmetatable(env, Env.mt)
   return env
end

 

Download this code: Environment.lua

The operations, implemented by the engine and exposed as core Lisp primitives:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
-- LispInterpreter.lua
-- Author: David Bergman
--
-- This is a Scheme/Lisp interpreter, written in Lua
--
 
require "Environment"
require "Parser"
 
Lisp = { }
 
function Lisp.evalExpr(env, expr)
   return Lisp.evalSexprList(env, Parser.parseSexpr(expr))
end
 
function Lisp.evalQuote(env, sexpr)
   local value
   if not sexpr.type then
      error("Invalid S-expr: ", 2)
   end
   if sexpr.type == "CONS" then
      local car = sexpr.car
      if car.type=="OP" and car.lexeme=="," then
	 value = Lisp.evalSexpr(env, sexpr.cdr)
      else
	 local evalCar = Lisp.evalQuote(env, car)
	 local cdr = Lisp.evalQuote(env, sexpr.cdr)
	 value = Sexpr.cons(evalCar, cdr)
      end
   else
      value = sexpr
   end
   return value
end
 
function Lisp.evalSexprList(env, sexprList, index)
   if not index then
      index = 1
   end
   local count = table.getn(sexprList)
   if index > count then
      return nil
   else
      local firstValue = Lisp.evalSexpr(env, sexprList[index])
      if index == count then
	 return firstValue
      else
	 return Lisp.evalSexprList(env, sexprList, index+1)
      end
   end
end
 
function Lisp.evalSexpr(env, sexpr)
   local value
   if not sexpr.type then
      error("Invalid S-expr: " .. sexpr, 2)
   end
   if sexpr.type == "CONS" then
      -- 1. Cons cell
      local car = sexpr.car
      if car.type=="OP" and car.lexeme=="'" then
	 value = sexpr.cdr
      elseif car.type=="OP" and car.lexeme=="`" then
	 local cdr = Lisp.evalQuote(env, sexpr.cdr)
	 value = cdr
      else
	 local fun = Lisp.evalSexpr(env, car)
	 if not fun or fun.type ~= "FUN" then
	    error("The S-expr did not evaluate to a function: " ..
		  tostring(car))
	 end
	 -- The function can be eithe "lazy", in that it deals
	 -- with evaluation of its arguments itself, a "macro",
	 -- which requires
	 -- a second evaluation after the macro expansion, or
	 -- a regular eager one
 
	 local args
	 if fun.special == "lazy" or fun.special == "macro"  then
	    args = sexpr.cdr
	 else
	    args = Lisp.evalList(env, sexpr.cdr)
	 end
	 value = fun.fun(env, args)
      end
   elseif sexpr.type == "SYM" then
      -- a. symbol
      value = env[sexpr.lexeme]
      if not value then
	 error("The symbol '" .. sexpr.lexeme .. "' is not defined")
      end
   else
      -- b. constant
      value = sexpr
   end
   return value
end
 
-- Evaluate each item in a list
function Lisp.evalList(env, list)
   local value
   if list.type=="CONS" then
      value = Sexpr.cons(Lisp.evalSexpr(env, list.car),
			 Lisp.evalList(env, list.cdr))
   else
      value = list
   end
   return value
end
 
-- Apply an environment and get the substituted S-exp
function Lisp.applyEnv(env, expr)
   local newSexpr
   if expr.type == "CONS" then
      newSexpr = Sexpr.cons(Lisp.applyEnv(env, expr.car),
			    Lisp.applyEnv(env, expr.cdr))
   elseif expr.type == "SYM" then
      newSexpr = env[expr.lexeme]
      if not newSexpr then
	 newSexpr = expr
      end
   else
      newSexpr = expr
   end
   return newSexpr
end
 
-- Some primitives
 
function Lisp.prim_car(env, args)
   return args.car.car
end
 
function Lisp.prim_cdr(env, args)
   return args.car.cdr
end
 
function Lisp.prim_cons(env, args)
   return Sexpr.cons(args.car, args.cdr.car)
end
 
function Lisp.prim_plus(env, args)
   local num1 = args.car.lexeme + 0
   local num2 = args.cdr.car.lexeme + 0
   local sum = num1 + num2
   return Sexpr.newAtom(sum)
end
 
function Lisp.prim_mult(env, args)
   local num1 = args.car.lexeme + 0
   local num2 = args.cdr.car.lexeme + 0
   local prod = num1 * num2
   return Sexpr.newAtom(prod)
end
 
function Lisp.prim_lambda(env, args)
   local formalParams = args.car
   local body = args.cdr.car
   return Sexpr.newFun("(lambda " ..
		       Sexpr.prettyPrint(formalParams) ..
			  " " .. Sexpr.prettyPrint(body)
			  .. ")",
		       function(env2, actualParams)
			  local localEnv =
			     env:addBindings(formalParams,
					     actualParams)
			  return Lisp.evalSexpr(localEnv,
						body)
		       end)
end
 
function Lisp.prim_if(env, args)
   local cond = Lisp.evalSexpr(env, args.car)
   local expr
   if cond.type == "LITERAL" and cond.lexeme=="nil" then
      expr = args.cdr.cdr.car
   else
      expr = args.cdr.car
   end
   return Lisp.evalSexpr(env, expr)
end
 
function Lisp.prim_eq(env, args)
   local arg1 = args.car
   local arg2 = args.cdr.car
   return Sexpr.newBool(arg1.type == arg2.type and
			arg1.type ~= "CONS" and
			   arg1.lexeme == arg2.lexeme)
end
 
function Lisp.prim_lt(env, args)
   return Sexpr.newBool(args.car.lexeme+0 <
			args.cdr.car.lexeme+0)
end
 
function Lisp.prim_consp(env, args)
   return Sexpr.newBool(args.car.type == "CONS")
end
 
function Lisp.prim_neg(env, args)
   return Sexpr.newAtom(- args.car.lexeme)
end
 
function Lisp.prim_setq(env, args)
   local sym = args.car
   local value = Lisp.evalSexpr(env, args.cdr.car)
   env[sym.lexeme] = value
   return value
end
 
-- Our eval handles both strings and S-exprs
function Lisp.prim_eval(env, sexpr)
   local value
   local car = sexpr.car
   if car.type == "STR" then
      value = Lisp.evalExpr(env, car.lexeme)
   else
      value = Lisp.evalSexpr(env, car)
   end
   return value
end
 
-- Evaluate a whole lisp file, and return 't'
 
function Lisp.prim_load(env, sexpr)
   local filename = sexpr.car.lexeme
   local lastValue = Lisp.runFile(env, filename)
   return Sexpr.newBool(true)
end
 
-- Echo S-expr standard output
function Lisp.prim_echo(env, sexpr)
   print(Sexpr.prettyPrint(sexpr.car))
   return Sexpr.newBool(true)
end
 
function Lisp.prim_defmacro(env, sexpr)
   local name = sexpr.car
   local params = sexpr.cdr.car
   local body = sexpr.cdr.cdr.car
   local macro =
      function (env2, e)
	 local paramScope = {}
	 Env.bind(paramScope, params, e)
	 local subsEnv = Env.new(paramScope)
	 local expanded = Lisp.applyEnv(subsEnv, body)
	 local value = Lisp.evalSexpr(env2, expanded)
	 return value
      end
   local fun = Sexpr.newFun("(defmacro " .. name.lexeme ..
			    " " .. Sexpr.prettyPrint(params) ..
			       " " .. Sexpr.prettyPrint(body) ..
			       ")", macro, "macro")
   env[name.lexeme] = fun
   return fun
end
 
function Lisp.getPrimitiveScope()
   return {
      car = Sexpr.newFun("car", Lisp.prim_car),
      cdr = Sexpr.newFun("cdr", Lisp.prim_cdr),
      cons = Sexpr.newFun("cons", Lisp.prim_cons),
      lambda = Sexpr.newFun("lambda", Lisp.prim_lambda, "lazy"),
      setq = Sexpr.newFun("setq", Lisp.prim_setq, "lazy"),
      ["<"] = Sexpr.newFun("<", Lisp.prim_lt),
      ["+"] = Sexpr.newFun("+", Lisp.prim_plus),
      ["*"] = Sexpr.newFun("*", Lisp.prim_mult),
      neg = Sexpr.newFun("neg", Lisp.prim_neg),
      eq = Sexpr.newFun("eq", Lisp.prim_eq),
      consp = Sexpr.newFun("consp", Lisp.prim_consp),
      eval = Sexpr.newFun("eval", Lisp.prim_eval),
      load = Sexpr.newFun("load", Lisp.prim_load),
      echo = Sexpr.newFun("echo", Lisp.prim_echo),
      defmacro = Sexpr.newFun("defmacro", Lisp.prim_defmacro,
			      "lazy"),
      ["if"] = Sexpr.newFun("if", Lisp.prim_if, "lazy")
   }
end
 
function Lisp.getGlobalEnv()
   local env =  Env.new(Lisp.getPrimitiveScope())
   -- Run the prelude
   Lisp.runFile(env, "Prelude.lsp")
   return env
end
 
function Lisp.runFile(env, filename)
   -- Read the file
   local file = io.open(filename, "r")
   local code = file:read("*all")
   local lastValue = Lisp.evalExpr(env, code)
   file:close()
   return lastValue
end
 
-- The top read-eval loop...
 
function Lisp.readEval()
   local env = Lisp.getGlobalEnv()
   local line
   repeat
      io.write("> ")
      line = io.read()
      if line and not line ~= ":q" then
	 local ok
	 local value
	 ok, value = pcall(Lisp.evalExpr, env, line)
	 if ok then
	    if value then
	       print(Sexpr.prettyPrint(value))
	    end
	 else
	    print("#error: ", value)
	 end
      end
   until not line or line == ":q"
end

Download this code: LispInterpreter.lua

Then we add a sugar coat on top of it, matching specific Lisp dialects:

(defmacro defun (name params body)
  (setq name (lambda params body)))
 
(defmacro or (a b)
  (if a a b))
 
(defmacro and (a b)
  (if a b nil))
 
(defun <= (x y)
  (or (< x y) (eq x y)))
 
(defun > (x y)
  (< y x))
 
(defun >= (x y)
  (<= y x))
 
(defun - (x y)
  (+ x (neg y))))
 
(defun nullp (x)
  (eq x nil))

Download this code: Prelude.lsp

Ok, we could use an interactive shell:

-- This software is licensed under the M.I.T. license.
-- The license text is found in "license.txt"
--
 
require "Environment"
require "Parser"
require "LispInterpreter"
 
Lisp.readEval()

Download this code: Interaction.lua

Sverker said,

September 8, 2006 @ 7:26 pm

Neat, and it ran straight out of the box. LuaLisp lacks a few primitives needed by my little Prolog in Lisp (dug up from a sibling directory to where resides groeb.pl), but adding them wouldn’t be a lot of work…

mhyst said,

March 28, 2008 @ 1:37 pm

Hello,
I was creating a new language (lisp based) for my w3s personal web crawler, to serve as interpreter for user searching algorithms. The entire project is written in java. Then I discovered this post and I got shocked. Lol. Your code is really short!

I got some good ideas from your post.

Thank you!

davber said,

March 28, 2008 @ 7:18 pm

I would love to share ideas with you, since neat and short implementation of (interpreted or not) languages is one of my hobbies.

There are mainly two reasons for the shortness of my implementation:

1. I decided to do it as a one-night stunt :-)

2. The (static) closures and chains of environments in Lua are isomorphic to what is found in (most) LISP implementations; I used that isomorphism heavily.

Hope to see some code from you somewhere; please post an URL for anything you can expose to the big wide world :-)

Thanks,

David

mhyst said,

March 30, 2008 @ 7:04 pm

I haven’t any problem to comment it with you. After all it’s open source. w3s project is already released to the publi (http://sourceforge.net/projects/w3s/). Also I wrote a paper some months ago (http://searchlores.org/mhyst_w3s.htm).

As for the w3s language, now I’m writing an interface to access Java Objects from it. I’ll write some article about it as soon as I have something working.

Data types management sucks. I wanted for my language a loose types management. But as long as I need to instantiate java objects and pass arguments to it, I need a hard typed model.

If you’re really interested, I can send you the source code.

Thanks

RSS feed for comments on this post · TrackBack URI

Leave a Comment

You must be logged in to post a comment.