This post is a continuation of LISPy-JSON Part 1 and Part 2.
Most programming language support some way of referring to data in memory, we call them variables. Javascript syntax looks like the following:
let a = 10
But instead of the ‘let’ keyword, we will use the ‘def’ (define) keyword, This is just so that it looks a bit different from the source language and to make it easier to follow when traversing interpreter code and the code to be interpreted. Feel free to use your own keyword.
Variable definition will look as follows,
let program = [ { def: { 'a': 10 }}, { def: { 'b': 20 }}, ]
To view what is in variables a & b, we could just try printing it.
let program = [ { def: { 'a': 10 }}, { def: { 'b': 20 }}, { print: 'a' }, ]
But now how would our interpreter know if we want to print the string ‘a’ or the data within variable ‘a’. JSON has its limitations, so we introduce a new function called val to fetch the value within the variable before it is printed. Let us write a program that sets the values of a & b, fetches them using val, adds them up and prints them. This is how our input program will look like,
let program = [ { def: { 'a': 10 }}, { def: { 'b': 20 }}, { print: { '+': [{ 'val': 'a' }, { 'val': 'b' }] } }, ]
Our interpret function will need a state, so we pass empty state(a Map) as an argument and every existing or new function that interprets our program will now need to accept state as the argument. Defining variables would basically mean add new records to the state map.
we introduce two new conditions, one for defining the variable using def and other for fetching the value within the variable using val.
As we can see in the image above, we also pass around state as an argument to every existing or new function that interprets our program starting with an empty state when we first call the interpret function.
Side note: In the future, We could also pass environment variables or command line arguments instead of the empty state.
We define two new functions varDefinition and varValue to basically read and write from the state variable,
varDefinition = (defObj, state) => { let variable = Object.keys(defObj)[0]; state[variable] = exec(defObj[variable],state) } varValue = (varKey,state) => { return state[varKey] }
The assigned value can also be an expression like def a = 5 + 4, hence we ensure we exec the value before assigning it. The entire program is given below,
let program = [ { def: { 'a': 10 }}, { def: { 'b': 20 }}, { print: { '+': [{ 'val': 'a' }, { 'val': 'b' }] } }, ] let builtins = { print: (text) => { console.log(text) } } let binaryOperators = { '+': (a,b) => { return a+b }, '-': (a,b) => { return a-b }, '*': (a,b) => { return a*b }, } interpret = (program,state) => { for (let stmt of program) { exec(stmt,state) } }; varDefinition = (defObj, state) => { let variable = Object.keys(defObj)[0]; state[variable] = exec(defObj[variable],state) } varValue = (varKey,state) => { return state[varKey] } exec = (stmt,state) => { if(typeof stmt === 'number' || typeof stmt === 'string'){ return stmt; } let key = Object.keys(stmt)[0] if (typeof builtins[key] === 'function') { builtins[key](exec(stmt[key],state),state) } else if (typeof binaryOperators[key] === 'function') { let firstArgument = stmt[key][0]; let secondArgument = stmt[key][1]; return binaryOperators[key](exec(firstArgument,state), exec(secondArgument,state)) }else if (key === 'def') { let firstArgument = stmt[key] return varDefinition(firstArgument, state) }else if (key === 'val') { let firstArgument = stmt[key] return varValue(firstArgument, state) }else{ console.error('unknown instruction: '+key) } } interpret(program,{})
and the diff from our previous implementation can be found here: https://github.com/avierr/LISPy-JSON-interpreter/commit/3a7c474d2506fcc86be28aeafd39b1f6e8dbce93
In the next post, we will talk about defining AND (&&) and OR (||) operators, and also some comparison operators like >,==, != etc.
Tags: interpreter javascript Virtual Machine