I have written a simple VM in JavaScript and it interprets source code written in JSON.
The JSON object must have a "exports" property, which is a dictionary that matches a string into a integer value. This value is an index to "entries" property, which is an array of entries.
For each entry it contains a "binds" and a "execs" property. They are all arrays of object that contains "callee" and "params". "callee" is a single object that contains "type" and "name"(string) or "index"(integer). "type" can be "extern" (refer to some function implemented in the VM) or "entry" (refer to entries array) or "bind" (refer to binds array) or "param" (at runtime, refer to the parameters given to the call). "params" is an array of exactly the same type of "callee".
At runtime, to call the VM one must specify a name listed in "exports" array, and some parameters. The VM will then look at the referred entry and create bind objects (record all resolved parameters, but not actually call them) for each item in the binds array. And then for each item in the "execs" array, execute at least one of them (how to do so is not specified).
Here is a simple implementation that only execute the first item in the "execs" array, and only provides externs for + - * 1 and < (it also allow to "run" a boolean typed value, the rule is to run the first argument if the value is true otherwise the second argument):
function evalJson(json){
return eval("(" + json + ")");
}
function RunEngine(){
var log = function(str){
var p = document.createElement("p");
p.appendChild(document.createTextNode(str));
document.getElementById("log").appendChild(p);
};
var externs = {
"<":function(n1,n2,c){
log(n1 + " < "+n2+"? " + (n1<n2));
this.callobj(c,n1<n2);
}.bind(this),
"1":1,
"+":function(n1,n2,c){
log(n1 + " + "+n2+" = " + (n1+n2));
this.callobj(c,n1+n2);
}.bind(this),
"-":function(n1,n2,c){
log(n1 + " - "+n2+" = " + (n1-n2));
this.callobj(c,n1-n2);
}.bind(this),
"*":function(n1,n2,c){
log(n1 + " * "+n2+" = " + (n1*n2));
this.callobj(c,n1*n2);
}.bind(this)
};
this.getExtern = function(name){
return externs[name];
};
var calls=[];
var running = false;
this.callobj = function(obj){
var args=[].slice.apply(arguments);
args.shift();
if(typeof(obj)==="boolean") this.callobj(obj?args[0]:args[1]);
if(typeof(obj)==="function") {
calls.push(obj.bind.apply(obj,[this].concat(args)));
if(!running)running=true;
else return;
while(calls.length>0){
calls.pop()();
}
running = false;
};
};
this.bindobj = function(obj){
var args=[].slice.apply(arguments);
args.shift();
return this.callobj.bind.apply(this.callobj,[this,obj].concat(args));
};
}
function JsonVM(code,engine){
var entries = [];
var getRefItem = function(refitem,ent,binds,args){
if(refitem.type==="entry") return ent[refitem.index];
else if(refitem.type==="bind") return binds[refitem.index];
else if(refitem.type==="param") return args[refitem.index];
else if(refitem.type=="extern") return engine.getExtern(refitem.name);
}.bind(this);
var getBind = function(callspec,ent,binds,args){
var objs = [getRefItem(callspec.callee,ent,binds,args)];
for(var i=0;i<callspec.params.length;++i){
objs.push(getRefItem(callspec.params[i],ent,binds,args));
}
return engine.bindobj.apply(engine,objs);
}.bind(this);
var getEntry = function(entry){
var f = function(){
var binds=[];
var args = [].slice.apply(arguments);
entry.binds.forEach(function(bind){
binds.push(getBind(bind,entries,binds,args));
});
var v = getBind(entry.execs[0],entries,binds,args);
engine.callobj(v);
};
return f.bind(this);
}.bind(this);
code.entries.forEach(function(entry){
entries.push(getEntry(entry));
});
this.runExport = function(name){
var args=[].slice.apply(arguments);
args.shift();
engine.callobj.apply(engine,[entries[code.exports[name]]].concat(args));
};
}
window.onload = function(){
document.getElementById("run").onclick=function(){
var code = evalJson(document.getElementById("code").value);
var vm = new JsonVM(code,new RunEngine());
vm.runExport("Factorial",10,function(n){ alert(n); });
};
};
The following HTML file is associated with it:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>JSON VM test</title>
<script type="text/javascript" src="jsonvm.js" ></script>
</head>
<body>
<textarea id="code" rows="60" cols="80"></textarea>
<input type="button" id="run" value="run" />
<div id="log" ></div>
</body>
</html>
and here is a sample JSON code that implements the "Factorial" function :
{
"exports":{"Factorial":0},
"entries":[{
"binds":[{
"callee":{"type":"entry","index":1},
"params":[{"type":"param","index":0},
{"type":"param","index":1}]
}],
"execs":[{
"callee":{"type":"extern","name":"<"},
"params":[{"type":"param","index":0},
{"type":"extern","name":"1"},
{"type":"bind","index":0}]
}]
},{ "binds":[{
"callee":{"type":"param","index":1},
"params":[{"type":"extern","name":"1"}]
},{
"callee":{"type":"entry","index":2},
"params":[{"type":"param","index":0},
{"type":"param","index":1}]
}],
"execs":[{
"callee":{"type":"param","index":2},
"params":[{"type":"bind","index":0},
{"type":"bind","index":1}]
}]
},{ "binds":[{
"callee":{"type":"entry","index":3},
"params":[{"type":"param","index":0},
{"type":"param","index":1}]
}],
"execs":[{
"callee":{"type":"extern","name":"-"},
"params":[{"type":"param","index":0},
{"type":"extern","name":"1"},
{"type":"bind","index":0}]
}]
},{ "binds":[{
"callee":{"type":"entry","index":4},
"params":[{"type":"param","index":0},
{"type":"param","index":1}]
}],
"execs":[{
"callee":{"type":"entry","index":0},
"params":[{"type":"param","index":2},
{"type":"bind","index":0}]
}]
},{ "binds":[],
"execs":[{
"callee":{"type":"extern","name":"*"},
"params":[{"type":"param","index":0},
{"type":"param","index":2},
{"type":"param","index":1}]
}]}]
}
To test, create the js file and the html file, and open the html file in a browser, and then paste the JSON code to the text area, click "run". an alert will show the result ant prints the steps in the page (may require scrolling)
One more thing needed to know is that this VM implementation uses a trick to avoid keep using the call stack. Without this trick it will be even simpler.
The given code is working good. However, to make it working I have did some dirty hacks (I think). So I want this code to be reviewed and to see how to remove those hacks and makes the code more clear. Also please give me advice about OO design, code style and so on.
NOTES: Please do not advise about evalJson function, I understand using eval is not good but for this simple example it is OK and simple enough. If I were to move it to production I can simply replace the evalJson function implementation.
UPDATE: updated version that come from the answer(s).
function evalJson(json){
return eval("(" + json + ")");
}
function RunEngine(){
var log = function(str){
var p = document.createElement("p");
p.appendChild(document.createTextNode(str));
document.getElementById("log").appendChild(p);
};
var externs = {
"<":function(n1,n2,c){
log(n1 + " < "+n2+"? " + (n1<n2));
callobj(c,n1<n2);
},
"1":1,
"+":function(n1,n2,c){
log(n1 + " + "+n2+" = " + (n1+n2));
callobj(c,n1+n2);
},
"-":function(n1,n2,c){
log(n1 + " - "+n2+" = " + (n1-n2));
callobj(c,n1-n2);
},
"*":function(n1,n2,c){
log(n1 + " * "+n2+" = " + (n1*n2));
callobj(c,n1*n2);
},
"/":function(n1,n2,c){
log(n1 + " / "+n2+" = " + (n1*n2));
callobj(c,n1/n2);
}
};
var calls=[];
var running = false;
function runitem(item){
calls.push(item);
if(!running) running = true;
else return;
while(calls.length>0){
calls.pop()();
}
running = false;
}
var callobj = function(obj){
var args=[].slice.apply(arguments);
args.shift();
do_callobj(obj,args);
};
var do_callobj = function(obj,args){
if(typeof(obj)==="boolean") do_callobj(obj?args[0]:args[1]);
if(typeof(obj)==="function") {
runitem(function(){
obj.apply(this,args);
});
};
};
var bindobj = function(obj,args){
return function(){
do_callobj(obj,args.concat([].slice.apply(arguments)));
};
};
this.getExtern = function(name){
return externs[name];
};
this.bindobj = bindobj;
}
function JsonVM(code,engine){
var entries = [];
var getRefItem = function(refitem,ent,binds,args){
if(refitem.type==="entry") return ent[refitem.index];
else if(refitem.type==="bind") return binds[refitem.index];
else if(refitem.type==="param") return args[refitem.index];
else if(refitem.type==="extern") return engine.getExtern(refitem.name);
};
var getBind = function(callspec,ent,binds,args){
var objs = [];
for(var i=0;i<callspec.params.length;++i){
objs.push(getRefItem(callspec.params[i],ent,binds,args));
}
return engine.bindobj(getRefItem(callspec.callee,ent,binds,args),objs);
};
var getEntry = function(entry){
var f = function(){
var binds=[];
var args = arguments;
entry.binds.forEach(function(bind){
binds.push(getBind(bind,entries,binds,args));
});
entry.execs.forEach(function(exec){
getBind(exec,entries,binds,args)();
});
};
return f;
};
code.entries.forEach(function(entry){
entries.push(getEntry(entry));
});
this.runExport = function(name){
var args=[].slice.apply(arguments);
args.shift();
engine.bindobj(entries[code.exports[name]],args)();
};
}
window.onload = function(){
document.getElementById("run").onclick=function(){
var code = evalJson(document.getElementById("code").value);
var vm = new JsonVM(code,new RunEngine());
vm.runExport("Factorial",10,function(n){ alert(n); });
};
};
UPDATE
This is the starting point of a far more ambitious project. The final target is to create an online programming platform that do not use explicit text-based programming language. Actually, the JSON in this example is what to be used by the platform, but most of them will be generated during UI interaction.
And now I move the previous "answer" here, as what tomdemuyt has suggested.
Problems that fingered by myself so far:
The JavaScript closure is not like other language's "object". Consider the following example:
function anobject(){ var privatefunction = function(){ //publicfunction(); //wrong; you have no access to it console.log(this); //refer to the global object, not the object creating }; this.publicfunction = function(){ console.log(this); //refer to the object creating } }And this is why there are some "hacks" in the code (for example, the use of
bind(this)have been used just to make it acts like other languages).It is a simple change to execute all items in execs array.
The
getRefItemfunction mixes operator===and==and this is a mistake; all should be===.
I appreciate more reviews; and this post is only to save your time. I will update the main post to include the updated code (but keep the original code for history record).
BTW I have created a Google code project for this, I appreciate any participation and if you want to join just send me an email I will join you.