Sun, 21 Nov 2010 16:25:08 +0000
Implement OP_CONCAT (no metamethod yet)
var OP_MOVE = 0; var OP_LOADK = 1; var OP_LOADBOOL = 2; var OP_LOADNIL = 3; var OP_GETUPVAL = 4; var OP_GETGLOBAL = 5; var OP_GETTABLE = 6; var OP_SETGLOBAL = 7; var OP_SETUPVAL = 8; var OP_SETTABLE = 9; var OP_NEWTABLE = 10; var OP_SELF = 11; var OP_ADD = 12; var OP_SUB = 13; var OP_DIV = 15; var OP_CONCAT = 21; var OP_JMP = 22; var OP_EQ = 23; var OP_LT = 24; var OP_LE = 25; var OP_TEST = 26; var OP_TESTSET = 27; var OP_CALL = 28; var OP_RETURN = 30; var OP_FORLOOP = 31; var OP_FORPREP = 32; var OP_CLOSURE = 36; var debugMode = false; function LValue(vm, type, value) { this.vm = vm; this.type = type||"nil"; this.value = value; } LValue.prototype = { call: function (args) { var f = this.precall(); var ret = this.vm.call(f, args); if(typeof(ret) == "undefined") ret = []; return ret; }, precall: function () { if(this.type == "function") return this.value; else throw "Attempt to call a " + this.type + " value"; }, index: function (key, raw) { if(this.type == "table") { var val; if(key.value in this.value) return this.value[key.value]; else if(raw != true && this.metatable && this.metatable.type != "nil") { var __index = this.metatable.index(this.vm.LValue("__index")); if(__index.type == "function") { return this.vm.LValue(__index.call([this, key])[0]); } else if(__index.type != "nil") return __index.index(key); } return this.vm.LValue(null); } else throw "Attempt to index a " + this.type + " value"; }, setIndex: function (key, value) { if(this.type == "table") { this.value[key.value] = value; } else throw "Attempt to index a " + this.type + " value"; }, setMetatable: function (metatable) { if(metatable.type == "table") this.metatable = metatable; else if(metatable.type == "nil") this.metatable = null; else throw "Attempt to set a "+metatable.type+" value as a metatable"; }, toString: function () { switch(this.type) { case "nil": return "nil"; default: return this.value.toString(); } }, truth: function () { if(this.type == "nil" || (this.type == "boolean" && this.value == false)) return false; return true; }, add: function (op2) { var metamethod; var __add = this.vm.LValue("__add"); if(this.metatable) metamethod = this.metatable.index(__add); if((!metamethod || metamethod.type == "nil") && op2.metatable) metamethod = op2.metatable.index(__add); if(metamethod && metamethod.type != "nil") { return metamethod.call([this, op2]); } else if((this.type == "number" || this.type == "string") && (op2.type == "number" || op2.type == "string")) { // Plain addition return this.vm.LValue(parseFloat(this.value, 10) + parseFloat(op2.value, 10)); } else throw "Attempt to perform arithmetic on a "+this.type+" and "+op2.type; }, equals: function (op2) { if(this.type != op2.type) return false; if(this.value == op2.value) return true; var __eq = this.vm.LValue("__eq"); if(this.metatable && op2.metatable) { var metamethod1 = this.metatable.index(__eq); var metamethod2 = op2.metatable.index(__eq); if(metamethod1.equals(metamethod2)) { var result = metamethod1.call([this, op2]); return (result[0].type != "nil" && (result[0].type != "boolean" || result[0].value == true) ); } } return false; } }; function LBinaryChunk(vm, chunk, start, sourceName) { this.chunk = chunk; this.pos = start||12; this.sourceName = this.readString(); if(sourceName) this.sourceName = sourceName; this.lineDefined = this.readInt(); this.lastLineDefined = this.readInt(); this.numUpvalues = this.readByte(); this.numParameters = this.readByte(); this.isVararg = this.readByte(); this.maxStackSize = this.readByte(); this.instructions = []; this.numInstructions = this.readInt(); for(var i=0;i<this.numInstructions;i++) { var ins = this.readInt(); this.instructions.push([ ins&0x3F, // Opcode (ins>>6)&0xFF, // Field A (ins>>23)&0x1FF, // Field B (ins>>14)&0x1FF // Field C ]); if(debugMode) { var pi = this.instructions[this.instructions.length-1]; sys.puts("Pos: "+(this.pos-4)+" Ins: "+ins+" OP: "+INS_OPCODE(pi)+" A: "+INS_A(pi)+" B: "+INS_B(pi)+" C: "+INS_C(pi)+" Bx: "+INS_Bx(pi)+" sBx: "+(INS_Bx(pi)-0x1FFFE)); } } this.constants = []; this.numConstants = this.readInt(); for(var i=0;i<this.numConstants;i++) { var type = this.readByte(); switch(type) { case 0: // Nil this.constants.push(new LValue(vm, "nil", null)); break; case 1: // Boolean this.constants.push(new LValue(vm, "boolean", this.readByte())); // FIXME type break; case 3: // Number this.constants.push(new LValue(vm, "number", this.readNumber())); break; case 4: // String this.constants.push(new LValue(vm, "string", this.readString())); break; default: throw "Invalid constant type "+type+" in bytecode"; } } this.prototypes = []; this.numPrototypes = this.readInt(); for(var i=0;i<this.numPrototypes;i++) { var p = new LBinaryChunk(vm, chunk, this.pos, this.sourceName); this.pos = p.pos; this.prototypes.push(p); } this.sourceLines = []; this.numSourceLines = this.readInt(); for(var i=0;i<this.numSourceLines;i++) { this.sourceLines.push(this.readInt()); } this.localList = []; this.numLocalList = this.readInt(); for(var i=0;i<this.numLocalList;i++) { this.localList.push([this.readString(),this.readInt(),this.readInt()]); } this.upvalueList = []; this.numUpvalueList = this.readInt(); for(var i=0;i<this.numUpvalueList;i++) { this.upvalueList.push(this.readString()); } return this; } LBinaryChunk.prototype = { readBytes: function (n) { return this.chunk.slice(this.pos, this.pos+=n); }, readByte: function () { return this.readBytes(1).charCodeAt(0); }, readInt: function () { //FIXME: Endianness return this.readByte() | (this.readByte()<<8) | (this.readByte()<<16) | (this.readByte()<<24); }, readString: function () { var len = this.readInt(); return this.readBytes(len).substring(0,len-1); }, readNumber: function () { //FIXME: Endianness var bytes = [this.readByte(),this.readByte(),this.readByte(),this.readByte(), this.readByte(),this.readByte(),this.readByte(),this.readByte()].reverse(); var sign = (bytes[0]>>7)&0x1; var exp = (bytes[0]&0x7F)<<4 | (bytes[1]&0xf0)>>4; var frac = ((bytes[1] & 0x0f) * Math.pow(2,48)) + (bytes[2] * Math.pow(2,40)) + (bytes[3] * Math.pow(2,32)) + (bytes[4] * Math.pow(2,24)) + (bytes[5] * Math.pow(2,16)) + (bytes[6] * Math.pow(2,8)) + bytes[7]; if(exp != 0x000 && exp != 0x7FF) { var n = (sign==1?-1:1)*Math.pow(2,exp-1023)*(1+(frac/0x10000000000000)); return n; } else if(exp == 0x000) { return sign*0; } else return frac==0?sign*Infinity:NaN; } }; function INS_OPCODE(ins) { return ins[0]; } function INS_A(ins) { return ins[1]; } function INS_B(ins) { return ins[2]; } function INS_C(ins) { return ins[3]; } function INS_Bx(ins) { return ((INS_C(ins))|(INS_B(ins)<<9)); } function INS_sBx(ins) { return (INS_Bx(ins)-0x1FFFF); } function RK(frame, R) { var keysource = (R&0x100)?frame.f.constants:frame.reg; return keysource[R&0xff]; } function LFunction(vm, chunk, env) { function F() {}; F.prototype = chunk; var o = new F(); o.vm = vm; o.environment = env; o.chunk = chunk; o.upvalues = []; return o; } function LVM() { this.callstack = []; this.stack = []; return this; } LVM.prototype = { LValue: function (value) { switch(typeof(value)) { case "number": return new LValue(this, "number", value); case "string": return new LValue(this, "string", value); case "function": return new LValue(this, "function", value); case "object": if(value == null) return new LValue(this, "nil", value); else return new LValue(this, "table", value); case "undefined": return new LValue(this, "nil", null); default: throw "Not able to convert type " + typeof(value)+" from Javascript to Lua"; } }, call: function (lfFunction, args) { if(typeof(lfFunction) == "function") { return lfFunction.apply(this, args); } else { var frame = {f:lfFunction,pc:0,entry:true}; if(args) frame.reg = args.slice(0); else frame.reg = []; this.callstack.push(frame); for(var i=frame.reg.length;i<lfFunction.maxStackSize;i++) frame.reg[i] = this.LValue(null); return this.run(frame); } }, run: function(frame) { var instruction; while(this.callstack.length>0) { instruction = frame.f.instructions[frame.pc++]; if(debugMode) { sys.puts("PC: "+(frame.pc-1)+" OP: "+instruction[0]); sys.puts("STACK: "+sys.inspect(frame.reg)); } switch(INS_OPCODE(instruction)) { case OP_MOVE: frame.reg[INS_A(instruction)] = frame.reg[INS_B(instruction)]; break; case OP_LOADNIL: for(var i = INS_A(instruction);i<=INS_B(instruction);i++) frame.reg[i] = new LValue(this, "nil", null); break; case OP_LOADBOOL: frame.reg[INS_A(instruction)] = new LValue(this, "boolean", INS_B(instruction)!=0); if(INS_C(instruction)!=0) frame.pc++; break; case OP_GETUPVAL: frame.reg[INS_A(instruction)] = frame.f.upvalues[INS_B(instruction)]; break; case OP_GETGLOBAL: var name = frame.f.constants[INS_Bx(instruction)]; frame.reg[INS_A(instruction)] = frame.f.environment.index(name); break; case OP_SETUPVAL: var reg = frame.reg[INS_A(instruction)]; var upvalue = frame.f.upvalues[INS_B(instruction)]; upvalue.type = reg.type; upvalue.value = reg.value; break; case OP_SETGLOBAL: var name = frame.f.constants[INS_Bx(instruction)]; frame.f.environment.setIndex(name, frame.reg[instruction[1]]); break; case OP_LOADK: var constant = frame.f.constants[INS_Bx(instruction)]; frame.reg[INS_A(instruction)] = new LValue(this, constant.type, constant.value); break; case OP_NEWTABLE: frame.reg[INS_A(instruction)] = new LValue(this, "table", {}); break; case OP_GETTABLE: var C = INS_C(instruction); var value = frame.reg[INS_B(instruction)].index(RK(frame, C)).value; frame.reg[INS_A(instruction)] = this.LValue(value); break; case OP_SETTABLE: var C = INS_C(instruction); var B = INS_B(instruction); frame.reg[INS_A(instruction)].setIndex(RK(frame, B), RK(frame, C)); break; case OP_CALL: var f = frame.reg[INS_A(instruction)].precall(); // return JS or LValue var A = INS_A(instruction), B = INS_B(instruction), C = INS_C(instruction); var undefined; var args = frame.reg.slice(A+1, B==0?undefined:(A+B)); for(var i=args.length+1;i<f.maxStackSize;i++) args[i] = new LValue(this, "nil", null); if(typeof(f) == "function") { // JS native function var ret = this.call(f, args); // Insert ret to reg starting at R(A), with C-1 limit var nresults = ret.length; var nexpected; if(C == 0) { nexpected = nresults; frame.reg = frame.reg.slice(0, A+nexpected); } else nexpected = C-1; for(var i = 0;;i++) { if(i < nresults) frame.reg[A+i] = ret[i]; else if(i < nexpected) frame.reg[A+i] = new LValue(this, "nil", null); else break; } } else { // Lua function frame = {f:f,pc:0,reg:args, retAt:INS_A(instruction),retCount:INS_C(instruction), entry:false}; this.callstack.push(frame); } break; case OP_CLOSURE: var prototype_id = INS_Bx(instruction); var chunk = frame.f.chunk.prototypes[prototype_id]; var f = new LFunction(this, chunk, frame.f.environment); frame.reg[INS_A(instruction)] = new LValue(this, "function", f); for(var i=0;i<chunk.numUpvalues;i++) { var upval_instruction = frame.f.instructions[frame.pc++]; switch(INS_OPCODE(upval_instruction)) { case OP_MOVE: f.upvalues[i] = frame.reg[INS_B(upval_instruction)]; break; case OP_GETUPVAL: f.upvalues[i] = frame.f.upvalues[INS_B(upval_instruction)]; break; default: throw "Invalid upvalue opcode following OP_CLOSURE"; } } break; case OP_RETURN: var oldFrame = this.callstack.pop(); frame = this.callstack[this.callstack.length-1]; var rets; if(INS_B(instruction) == 0) rets = oldFrame.reg.slice(INS_A(instruction)); else rets = oldFrame.reg.slice(INS_A(instruction),INS_A(instruction)+(INS_B(instruction)-1)); if(!oldFrame.entry) { var i; for(i=0;(oldFrame.retCount == 0||i<(oldFrame.retCount-1))&&i<rets.length;i++) frame.reg[oldFrame.retAt+i] = rets[i]; if(oldFrame.retAt+i<frame.reg.length) frame.reg.slice(0,oldFrame.retAt+i); } else return rets; break; case OP_SELF: var table = frame.reg[INS_B(instruction)]; frame.reg[INS_A(instruction)+1] = table; var C = INS_C(instruction); frame.reg[INS_A(instruction)] = table.index(RK(frame, C)); break; case OP_FORPREP: frame.pc+=(INS_sBx(instruction)); var A = INS_A(instruction); frame.reg[A].value -= frame.reg[A+2].value; frame.reg[A+3] = new LValue(this, "number", null); break; case OP_FORLOOP: var A = INS_A(instruction); var RA = frame.reg[A]; RA.value += frame.reg[A+2].value; if(RA.value <= frame.reg[A+1].value) { frame.pc += INS_sBx(instruction); frame.reg[A+3].value = RA.value; } break; case OP_TEST: var RA = frame.reg[INS_A(instruction)]; var RA_bool = RA.type == "nil" || (RA.type == "boolean" && RA.value == false); if(RA_bool == (INS_C(instruction)!=0)) frame.pc++; break; case OP_TESTSET: var RB = frame.reg[INS_B(instruction)]; var RB_bool = RB.type == "nil" || (RB.type == "boolean" && RB.value == false); if(RB_bool == (INS_C(instruction)!=0)) frame.pc++; else frame.reg[INS_A(instruction)] = RB; break; case OP_JMP: frame.pc+=INS_sBx(instruction); break; case OP_CONCAT: var A = INS_A(instruction); var B = INS_B(instruction); var C = INS_C(instruction); var values = []; for(var i = B; i<=C; i++) values.push(frame.reg[i].value); frame.reg[A] = new LValue(this, "string", values.join()); case OP_ADD: var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); frame.reg[INS_A(instruction)] = RB.add(RC); break; case OP_SUB: var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); frame.reg[INS_A(instruction)] = new LValue(this, "number", RB.value - RC.value); break; case OP_DIV: var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); frame.reg[INS_A(instruction)] = new LValue(this, "number", RB.value / RC.value); break; case OP_EQ: var A = INS_A(instruction); var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); if(RB.equals(RC) != (A!=0)) frame.pc++; break; case OP_LT: var A = INS_A(instruction); var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); if(RB.value < RC.value != (A!=0)) frame.pc++; break; case OP_LE: var A = INS_A(instruction); var RB = RK(frame, INS_B(instruction)); var RC = RK(frame, INS_C(instruction)); if(RB.value <= RC.value != (A!=0)) frame.pc++; break; default: throw "Unhandled opcode: "+INS_OPCODE(instruction); } } }, registerLib: function (env, name, lib) { var t; if(name) { t = this.LValue({}); // Create env[name] and put fns in there env.setIndex(this.LValue(name), t); } else t = env; // Import directly into env for(var k in lib) t.setIndex(this.LValue(k), this.LValue(lib[k])); } }; try{ var testvm = new LVM(); var fs=require("fs"); var sys=require("sys"); var c = new LBinaryChunk(testvm, fs.readFileSync("luac.out", "binary")); var _G = testvm.LValue({}); // Standard library var baselib = { print: function () { var args = Array.prototype.slice.call(arguments); sys.print(args[0].toString()); for(var i = 1; i<args.length; i++) sys.print("\t"+args[i].toString()); sys.print("\n"); return []; }, setmetatable: function (table, metatable) { if(arguments.length!=2) throw "setmetatable expects 2 arguments, got "+arguments.length; table.setMetatable(metatable); return [table]; }, type: function (o) { return [this.LValue(o.type)]; }, assert: function (expr, message) { if(!expr.truth()) throw message; return [expr]; } }; var math = { ldexp: function (m, e) { return [this.LValue(m.value*Math.pow(2, e.value))]; }, floor: function (x) { return [this.LValue(Math.floor(x.value))]; } }; testvm.registerLib(_G, null, baselib); testvm.registerLib(_G, "math", math); // Metatable on environment to print out nil global accesses var mt = testvm.LValue({}); mt.setIndex( testvm.LValue("__index"), testvm.LValue(function (t, k) { sys.puts("Access of nil global: "+k); }) ); _G.setMetatable(mt); var f = new LFunction(testvm, c, _G); var ret = testvm.call(f); if(ret) sys.puts("Returned: "+sys.inspect(ret)); } catch(e) { var currframe = testvm.callstack[testvm.callstack.length-1]; if(currframe) { var currfunc = currframe.f; sys.print("lvm.js: "+(currfunc.sourceName||"=<unknown>").substr(1)+":"+(currfunc.sourceLines[currframe.pc]||"<unknown>")+": "); } sys.puts(e); if(typeof(e) == "object" && "stack" in e) sys.puts(e.stack); process.exit(1); }