If you haven’t read the first part, please do so before reading this.
In the previous post, we created an interpreter that implemented a print function but it could only print static text or numbers but could not print a calculation like, print 3+5
From now on, we will call all calculations like 3+5, 2+3, 5+5 etc., as expressions. Now what we want our print function to do is evaluate an expression before printing it.
print Expr
If you remember, print is a function that takes one argument, it could be text or a number that you want to print, similarly + is a function too, but it takes two arguments instead of one which also makes + a binary operator.
Similar to print 42, we could have add 3 5, which could be further simplified to + 3 5 and then print it using print ( add ( 3 5)) or simply print ( + ( 3 5)). We could represent this in our JS Object as follows,
let program = [ { print: { '+': [3,5] } }, ]
Then we create an object called binaryOperators which will contain our ‘+’ operator implementation and other operators in the future.
let binaryOperators = { '+': (a,b) => { return a+b }, }
Then we change the exec function slightly to support the ‘+’ operator from within the binaryOperators object. We also introduce a change wherein instead of printing the argument directly, we exec it prior to printing it. But we also need to make sure that we don’t exec simple values likes strings and numbers, and hence we add a gaurd statement at the beginning of the function to return the value back up the print function as an argument instead of trying to execute it.
exec = (stmt) => { 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])) } else if (typeof binaryOperators[key] === 'function') { let firstArgument = stmt[key][0]; let secondArgument = stmt[key][1]; return binaryOperators[key](firstArgument, secondArgument) }else{ console.error('unknown instruction: '+key) } }
The entire program is given below,
let program = [ { print: { '+': [3,5] } }, ] let builtins = { print: (text) => { console.log(text) } } let binaryOperators = { '+': (a,b) => { return a+b }, } interpret = (program) => { for (let stmt of program) { exec(stmt) } }; exec = (stmt) => { 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])) } else if (typeof binaryOperators[key] === 'function') { let firstArgument = stmt[key][0]; let secondArgument = stmt[key][1]; return binaryOperators[key](firstArgument, secondArgument) }else{ console.error('unknown instruction: '+key) } } interpret(program)
The output will look as follows:
$ node interpret.js
8
The diff of this can be seen in this PR: https://github.com/avierr/json-lisp-interpreter/pull/1/files
Now let us try adding the ‘-‘ and ‘*’ operator. We can add these directly to the binaryOperators object,
let binaryOperators = { '+': (a,b) => { return a+b }, '-': (a,b) => { return a-b }, '*': (a,b) => { return a*b }, }
and we then update the input program:
let program = [ { print: { '+': [3,5] } }, { print: { '*': [3,5] } }, { print: { '-': [3,5] } }, ]
The entire program is given below:
let program = [ { print: { '+': [3,5] } }, { print: { '*': [3,5] } }, { print: { '-': [3,5] } }, ] 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) => { for (let stmt of program) { exec(stmt) } }; exec = (stmt) => { 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])) } else if (typeof binaryOperators[key] === 'function') { let firstArgument = stmt[key][0]; let secondArgument = stmt[key][1]; return binaryOperators[key](firstArgument, secondArgument) }else{ console.error('unknown instruction: '+key) } } interpret(program)
The output will look as follows:
$ node interpret.js
8
15
-2
The diff can be seen in this PR: https://github.com/avierr/json-lisp-interpreter/pull/2/files
Now what if you want to do something like, print 3+(5*6)?
we already support the form print Expr what we essentially need is to further define what Expr is,
print Expr
where Expr could be a number or string or just Expr -> number | string
Expr could be another Expr like a+b or a-b: Expr -> number + number | number – number and so on,
what we need now is something of the form Expr -> Expr + Expr | Expr * Expr and so on.
Achieving the above is really simple, let us first reset the input program object,
let program = [ { print: { '+': [3, {'*': [5, 6]}] } }, ]
Then we modify the call to binaryOperator function to exec the argument before it is sent into the function, This is similar to what we did with the print function, but instead of one argument, we now have two,
exec = (stmt) => {
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]))
} else if (typeof binaryOperators[key] === 'function') {
let firstArgument = stmt[key][0];
let secondArgument = stmt[key][1];
return binaryOperators[key](exec(firstArgument), exec(secondArgument))
}else{
console.error('unknown instruction: '+key)
}
}
The entire program is given below:
let program = [ { print: { '+': [3, {'*': [5, 6]}] } }, ] 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) => { for (let stmt of program) { exec(stmt) } }; exec = (stmt) => { 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])) } else if (typeof binaryOperators[key] === 'function') { let firstArgument = stmt[key][0]; let secondArgument = stmt[key][1]; return binaryOperators[key](exec(firstArgument), exec(secondArgument)) }else{ console.error('unknown instruction: '+key) } } interpret(program)
The output will look as follows:
$ node interpret.js
33
The diff can be seen in this commit: https://github.com/avierr/json-lisp-interpreter/commit/ab9b2ea083cf19b791eff8314cafaa4b9becaee1
The next chapter will be focused on program state and variables.