Return Zero

LISPy-JSON: My second attempt at explaining Interpreters

Mr Silva

I wrote a previous post trying to explain a virtual machine interpreter, but I feel I did not explain it in a simple enough manner and the primary reason for that was due to my limited understanding of these topics at the time, thus it ended up being a bit more convoluted than I intended it to be, now here is my second attempt.

You can also use the Git Repo for reference, every commit in the repository is a working interpreter.


We use very simple Javascript, no let vs const, there are no node_modules, no test files, no package.json and we work with a very limited Javascript language feature set so as to not confuse users if they come from other language and tooling backgrounds. Please understand that the Javascript posted here may not pass your linter and may not pass a lot of edge cases. It is simplified down to make it easier to understand with the intent that it should easily run on Node, Deno, Online playgrounds and in various browser consoles.

We will use JS Objects/JSON to represent our program.
Why JSON? because I feel it is a good starting point, almost everyone knows it and it helps keep ourselves away for a good while from having to go down the rabbithole of tokenization and parsing. It will act as the source program as well as the Abstract Syntax Tree. It ends up looking like a LISP program and hence the name: LISPy-JSON.

Let us start with the print instruction, the following assumes that you have parsed your JSON and the representation looks like the one below,

let program = [
  { print: 'hello' },
  { print: 'world' }
]

here print is an inbuilt function, hence we define it in within an object called builtins.

let builtins = {
  print: (text) => { console.log(text) }
}

Then we basically loop through each of the statements within the program and execute one statement at a time, let us define a function called interpret to do this

interpret = (program) => {
  for (let stmt of program) {
    exec(stmt)
  }
};

The exec function looks like the following,

exec = (stmt) => {
  let key = Object.keys(stmt)[0]
  if (typeof builtins[key] === 'function') {
    builtins[key](stmt[key])
  }else{
    console.error('unknown instruction: '+key)
  }
}

In the end, we just pass the program into the interpret function and the whole program is given below,

let program = [
  { print: 'hello' },
  { print: 'world' }
]

let builtins = {
  print: (text) => { console.log(text) }
}

interpret = (program) => {
  for (let stmt of program) {
    exec(stmt)
  }
};

exec = (stmt) => {
  let key = Object.keys(stmt)[0]
  if (typeof builtins[key] === 'function') {
    builtins[key](stmt[key])
  }else{
    console.error('unknown instruction: '+key)
  }
}

interpret(program);
$ node interpret.js
hello
world

Now what if we want to print 3+5 , a calculation, a slight improvement over printing static text, you might want to read the next part: LISP: Expressing Expressions in Interpreters

Credits:
The idea of using every commit as working version of the program comes from Rui Ueyama’s Chibicc C compiler.
A lot of ideas such as using JSON to represent a Lisp like program comes from Mikhail Khan’s blog – Writing a JSON Interpreter.

The pictures in this series of posts are machine generated and are a homage to the Dragon Book.

Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top