diff --git a/.gitignore b/.gitignore index 245b454e..695d7e14 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules/ gh-pages dist/*.min.js* *.code-workspace +perf/harness/golden.json +ws/client/cuis.changes +ws/client/CuisDebug-*.log diff --git a/jit.js b/jit.js index 6129a2b1..47cd9f3f 100644 --- a/jit.js +++ b/jit.js @@ -200,6 +200,18 @@ to single-step. // if a compiler does not support single-stepping, return false return true; }, + activationCheck: function() { + // expression that is true when the send/jump above switched activations + return this.vm.useStackZone + ? "vm.fp !== fp || vm.zonePage !== page" + : "context !== vm.activeContext"; + }, + tempIndex: function(n) { + // index expression for temp n inside this.source: + // context mode: constant (temps at homeContext.pointers[6+n]); + // stack-zone mode: frame-relative (temps at vm.temps[tempBase+n], tempBase per activation) + return this.vm.useStackZone ? "tB + " + n : Squeak.Context_tempFrameStart + n; + }, functionNameFor: function(cls, sel) { if (cls === undefined || cls === '?') { var isMethod = this.method.sqClass === this.vm.specialObjects[Squeak.splOb_ClassCompiledMethod]; @@ -234,11 +246,15 @@ to single-step. this.source.push("// ", optClass, ">>", optSel, "\n"); this.instVarNames = optInstVarNames; this.allVars = ['context', 'stack', 'rcvr', 'inst[', 'temp[', 'lit[']; - this.sourcePos['context'] = this.source.length; this.source.push("var context = vm.activeContext;\n"); - this.sourcePos['stack'] = this.source.length; this.source.push("var stack = context.pointers;\n"); + this.sourcePos['context'] = this.source.length; this.source.push(this.vm.useStackZone + ? "var fp = vm.fp, page = vm.zonePage;\n" + : "var context = vm.activeContext;\n"); + this.sourcePos['stack'] = this.source.length; this.source.push("var stack = vm.stack;\n"); this.sourcePos['rcvr'] = this.source.length; this.source.push("var rcvr = vm.receiver;\n"); this.sourcePos['inst['] = this.source.length; this.source.push("var inst = rcvr.pointers;\n"); - this.sourcePos['temp['] = this.source.length; this.source.push("var temp = vm.homeContext.pointers;\n"); + this.sourcePos['temp['] = this.source.length; this.source.push(this.vm.useStackZone + ? "var temp = vm.temps, tB = vm.tempOffset;\n" + : "var temp = vm.temps;\n"); this.sourcePos['lit['] = this.source.length; this.source.push("var lit = vm.method.pointers;\n"); this.sourcePos['loop-start'] = this.source.length; this.source.push("while (true) switch (vm.pc) {\ncase 0:\n"); if (this.sista) this.generateSista(method); @@ -267,7 +283,7 @@ to single-step. break; // load temp var case 0x10: case 0x18: - this.generatePush("temp[", 6 + (byte & 0xF), "]"); + this.generatePush("temp[", this.tempIndex((byte & 0xF)), "]"); break; // loadLiteral case 0x20: case 0x28: case 0x30: case 0x38: @@ -283,7 +299,7 @@ to single-step. break; // storeAndPop temp var case 0x68: - this.generatePopInto("temp[", 6 + (byte & 0x07), "]"); + this.generatePopInto("temp[", this.tempIndex((byte & 0x07)), "]"); break; // Quick push case 0x70: @@ -361,7 +377,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generatePush("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generatePush("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generatePush("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: this.generatePush("lit[", 1 + (byte2 & 0x3F), "]"); return; case 3: this.generatePush("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -370,7 +386,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generateStoreInto("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generateStoreInto("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generateStoreInto("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: throw Error("illegal store into literal"); case 3: this.generateStoreInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -380,7 +396,7 @@ to single-step. byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { case 0: this.generatePopInto("inst[", byte2 & 0x3F, "]"); return; - case 1: this.generatePopInto("temp[", 6 + (byte2 & 0x3F), "]"); return; + case 1: this.generatePopInto("temp[", this.tempIndex((byte2 & 0x3F)), "]"); return; case 2: throw Error("illegal pop into literal"); case 3: this.generatePopInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; } @@ -444,19 +460,19 @@ to single-step. case 0x8C: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generatePush("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generatePush("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // remote store into temp vector case 0x8D: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generateStoreInto("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // remote store and pop into temp vector case 0x8E: byte2 = this.method.bytes[this.pc++]; byte3 = this.method.bytes[this.pc++]; - this.generatePopInto("temp[", 6 + byte3, "].pointers[", byte2, "]"); + this.generatePopInto("temp[", this.tempIndex(byte3), "].pointers[", byte2, "]"); return; // pushClosureCopy case 0x8F: @@ -502,10 +518,10 @@ to single-step. break; // load temporary variable case 0x40: case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46: case 0x47: - this.generatePush("temp[", 6 + (b & 0x07), "]"); + this.generatePush("temp[", this.tempIndex((b & 0x07)), "]"); break; case 0x48: case 0x49: case 0x4A: case 0x4B: - this.generatePush("temp[", 6 + (b & 0x03) + 8, "]"); + this.generatePush("temp[", this.tempIndex((b & 0x03) + 8), "]"); break; case 0x4C: this.generatePush("rcvr"); break; @@ -577,7 +593,7 @@ to single-step. this.generatePopInto("inst[", b & 0x07, "]"); break; case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7: - this.generatePopInto("temp[", 6 + (b & 0x07), "]"); + this.generatePopInto("temp[", this.tempIndex((b & 0x07)), "]"); break; case 0xD8: this.generateInstruction("pop", "vm.sp--"); break; @@ -609,7 +625,7 @@ to single-step. break; case 0xE5: b2 = bytes[this.pc++]; - this.generatePush("temp[", 6 + b2, "]"); + this.generatePush("temp[", this.tempIndex(b2), "]"); break; case 0xE6: throw Error("unusedBytecode 0xE6"); @@ -662,7 +678,7 @@ to single-step. break; case 0xF2: b2 = bytes[this.pc++]; - this.generatePopInto("temp[", 6 + b2, "]"); + this.generatePopInto("temp[", this.tempIndex(b2), "]"); break; case 0xF3: b2 = bytes[this.pc++]; @@ -674,7 +690,7 @@ to single-step. break; case 0xF5: b2 = bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + b2, "]"); + this.generateStoreInto("temp[", this.tempIndex(b2), "]"); break; case 0xF6: case 0xF7: throw Error("unusedBytecode " + b); @@ -702,17 +718,17 @@ to single-step. case 0xFB: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generatePush("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generatePush("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFC: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generateStoreInto("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generateStoreInto("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFD: b2 = bytes[this.pc++]; b3 = bytes[this.pc++]; - this.generatePopInto("temp[", 6 + b3, "].pointers[", b2, "]"); + this.generatePopInto("temp[", this.tempIndex(b3), "].pointers[", b2, "]"); break; case 0xFE: case 0xFF: throw Error("unusedBytecode " + b); @@ -742,6 +758,16 @@ to single-step. this.generateLabel(); this.needsVar[target] = true; this.needsVar['stack'] = true; + if (this.vm.useStackZone && target === "inst[") { + // un store a un context casado-vivo debe pasar por write-through + // (flush + escritura sobre el context real); pc fresco para el resume + this.needsVar['context'] = true; // fp/page + this.source.push( + "if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", arg1, "] = stack[vm.sp]; rcvr.dirty = true; }\n", + "else { vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", arg1, ", stack[vm.sp]); if (vm.fp !== fp || vm.zonePage !== page) return; }\n"); + this.needsLabel[this.pc] = true; + return; + } this.source.push(target); if (arg1 !== undefined) { this.source.push(arg1, suffix1); @@ -757,6 +783,15 @@ to single-step. this.generateLabel(); this.needsVar[target] = true; this.needsVar['stack'] = true; + if (this.vm.useStackZone && target === "inst[") { + this.needsVar['context'] = true; // fp/page + this.source.push( + "var iv = stack[vm.sp--];\n", + "if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", arg1, "] = iv; rcvr.dirty = true; }\n", + "else { vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", arg1, ", iv); if (vm.fp !== fp || vm.zonePage !== page) return; }\n"); + this.needsLabel[this.pc] = true; + return; + } this.source.push(target); if (arg1 !== undefined) { this.source.push(arg1, suffix1); @@ -786,7 +821,7 @@ to single-step. // actually stack === context.pointers but that would look weird this.needsVar['context'] = true; this.source.push( - "vm.pc = ", this.pc, "; vm.doReturn(", retVal, ", context.pointers[0]); return;\n"); + "vm.pc = ", this.pc, "; vm.doBlockReturn(", retVal, "); return;\n"); this.needsBreak = false; // returning anyway this.done = this.pc > this.endPC; }, @@ -799,7 +834,7 @@ to single-step. if (distance < 0) this.source.push( "\nif (vm.interruptCheckCounter-- <= 0) {\n", " vm.checkForInterrupts();\n", - " if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n", + " if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n", "}\n"); if (this.singleStep) this.source.push("\nif (vm.breakOutOfInterpreter) return;\n"); this.source.push("continue;\n"); @@ -831,16 +866,28 @@ to single-step. "var a, b; if ((a=stack[vm.sp-1]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp]) === 'number' && b>0 && b<=a.pointers.length) {\n", " stack[--vm.sp] = a.pointers[b-1];", "} else { var c = vm.primHandler.objectAt(true,true,false); if (vm.primHandler.success) stack[--vm.sp] = c; else {\n", - " vm.pc = ", this.pc, "; vm.sendSpecial(16); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }}\n"); + " vm.pc = ", this.pc, "; vm.sendSpecial(16); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); this.needsLabel[this.pc] = true; return; case 0x1: // at:put: this.needsVar['stack'] = true; + if (this.vm.useStackZone) { + this.needsVar['context'] = true; // fp/page + this.source.push( + "var a, b; if ((a=stack[vm.sp-2]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp-1]) === 'number' && b>0 && b<=a.pointers.length) {\n", + " var c = stack[vm.sp]; stack[vm.sp-=2] = a.pointers[b-1] = c; a.dirty = true;", + "} else { var c = stack[vm.sp]; vm.pc = ", this.prevPC, "; vm.primHandler.objectAtPut(true,true,false); if (vm.fp !== fp || vm.zonePage !== page) return;\n", + " if (vm.primHandler.success) stack[vm.sp-=2] = c; else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); + this.needsLabel[this.prevPC] = true; + this.needsLabel[this.pc] = true; + return; + } this.source.push( "var a, b; if ((a=stack[vm.sp-2]).sqClass === vm.specialObjects[7] && a.pointers && typeof (b=stack[vm.sp-1]) === 'number' && b>0 && b<=a.pointers.length) {\n", " var c = stack[vm.sp]; stack[vm.sp-=2] = a.pointers[b-1] = c; a.dirty = true;", "} else { vm.primHandler.objectAtPut(true,true,false); if (vm.primHandler.success) stack[vm.sp-=2] = c; else {\n", - " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }}\n"); + " vm.pc = ", this.pc, "; vm.sendSpecial(17); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }}\n"); this.needsLabel[this.pc] = true; return; case 0x2: // size @@ -848,7 +895,7 @@ to single-step. this.source.push( "if (stack[vm.sp].sqClass === vm.specialObjects[7]) stack[vm.sp] = stack[vm.sp].pointersSize();\n", // Array "else if (stack[vm.sp].sqClass === vm.specialObjects[6]) stack[vm.sp] = stack[vm.sp].bytesSize();\n", // ByteString - "else { vm.pc = ", this.pc, "; vm.sendSpecial(18); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return; }\n"); + "else { vm.pc = ", this.pc, "; vm.sendSpecial(18); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return; }\n"); this.needsLabel[this.pc] = true; return; //case 0x3: return false; // next @@ -889,7 +936,7 @@ to single-step. this.source.push( "vm.pc = ", this.pc, "; if (!vm.primHandler.quickSendOther(rcvr, ", (byte & 0x0F), "))", " vm.sendSpecial(", ((byte & 0x0F) + 16), ");\n", - "if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n"); + "if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n"); this.needsBreak = false; // already checked // if falling back to a full send we need a label for coming back this.needsLabel[this.pc] = true; @@ -906,42 +953,42 @@ to single-step. this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = vm.primHandler.signed32BitIntegerFor(a + b);\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(0); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(0); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x1: // MINUS - this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = vm.primHandler.signed32BitIntegerFor(a - b);\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(1); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(1); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x2: // LESS < this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a < b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(2); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(2); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x3: // GRTR > this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a > b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(3); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(3); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x4: // LEQ <= this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a <= b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(4); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(4); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x5: // GEQ >= this.needsVar['stack'] = true; this.source.push("var a = stack[vm.sp - 1], b = stack[vm.sp];\n", "if (typeof a === 'number' && typeof b === 'number') {\n", " stack[--vm.sp] = a >= b ? vm.trueObj : vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(5); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(5); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x6: // EQU = this.needsVar['stack'] = true; @@ -950,7 +997,7 @@ to single-step. " stack[--vm.sp] = a === b ? vm.trueObj : vm.falseObj;\n", "} else if (a === b && a.float === a.float) {\n", // NaN check " stack[--vm.sp] = vm.trueObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(6); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(6); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x7: // NEQ ~= this.needsVar['stack'] = true; @@ -959,7 +1006,7 @@ to single-step. " stack[--vm.sp] = a !== b ? vm.trueObj : vm.falseObj;\n", "} else if (a === b && a.float === a.float) {\n", // NaN check " stack[--vm.sp] = vm.falseObj;\n", - "} else { vm.pc = ", this.pc, "; vm.sendSpecial(7); if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return}\n"); + "} else { vm.pc = ", this.pc, "; vm.sendSpecial(7); if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return}\n"); return; case 0x8: // TIMES * this.source.push("vm.success = true; vm.resultIsFloat = false; if(!vm.pop2AndPushNumResult(vm.stackIntOrFloat(1) * vm.stackIntOrFloat(0))) { vm.pc = ", this.pc, "; vm.sendSpecial(8); return}\n"); @@ -1002,7 +1049,7 @@ to single-step. } else { this.source.push("; vm.send(", prefix, num, suffix, ", ", numArgs, ", ", superSend, "); "); } - this.source.push("if (context !== vm.activeContext || vm.breakOutOfInterpreter !== false) return;\n"); + this.source.push("if (", this.activationCheck(), " || vm.breakOutOfInterpreter !== false) return;\n"); this.needsBreak = false; // already checked // need a label for coming back after send this.needsLabel[this.pc] = true; @@ -1028,7 +1075,7 @@ to single-step. this.needsVar['stack'] = true; this.source.push( "var closure = vm.instantiateClass(vm.specialObjects[36], ", numCopied, ");\n", - "closure.pointers[0] = context; vm.reclaimableContextCount = 0;\n", + "closure.pointers[0] = vm.activeContextObj(); vm.reclaimableContextCount = 0;\n", "closure.pointers[1] = ", from + this.method.pointers.length * 4 + 1, ";\n", // encodeSqueakPC "closure.pointers[2] = ", numArgs, ";\n"); if (numCopied > 0) { diff --git a/jit2.js b/jit2.js new file mode 100644 index 00000000..35d0bac3 --- /dev/null +++ b/jit2.js @@ -0,0 +1,865 @@ +"use strict"; +/* + * jit2: stack-to-register mapping compiler for stack-zone mode. + * + * Compiles V3(+closures) methods to JS functions where operand-stack slots + * live in JS locals (s0, s1, ...) whose depth is tracked at compile time. + * Slots are spilled to the zone only at send sites (where the frame must be + * observable/resumable) and reloaded on trampoline re-entry, distinguished + * from internal jumps by a function-local `entering` flag. Each send site + * has a monomorphic inline cache (class -> method/primitive), bypassing + * findSelectorInClass on hits. Unsupported bytecodes bail the whole method + * to the jit1 template compiler. + * + * Only active in stack-zone mode (generated code assumes frames). + */ + +Squeak.LEAF_DEOPT = { leafDeopt: true }; // sentinel: el leaf no pudo, re-ejecutar framed + +Object.subclass('Squeak.Compiler2', +'initialization', { + initialize: function(vm) { + this.vm = vm; + this.fallback = new Squeak.Compiler(vm); + this.bailCount = 0; + this.okCount = 0; + }, +}, +'accessing', { + compile: function(method, optClassObj, optSelObj) { + if (method.compiled === undefined) { + method.compiled = false; // 1st time: defer (same policy as jit1) + return; + } + if (method.methodSignFlag() // Sista: not supported yet + || (typeof process !== "undefined" && process.env && process.env.JIT2BAIL)) { + return this.fallback.compile(method, optClassObj, optSelObj); + } + // bisección de bugs: JIT2SEL="div,resto" compila con jit2 solo métodos + // cuyo fingerprint % div === alguno de los restos listados + if (typeof process !== "undefined" && process.env && process.env.JIT2SEL) { + var parts = process.env.JIT2SEL.split(",").map(Number); + var div = parts[0]; + var fpr = method.bytes ? method.bytes.length : 0; + if (method.bytes) for (var bi = 0; bi < Math.min(method.bytes.length, 16); bi++) + fpr = ((fpr * 31) + method.bytes[bi]) | 0; + var rem = ((fpr % div) + div) % div; + if (parts.indexOf(rem, 1) < 0) + return this.fallback.compile(method, optClassObj, optSelObj); + } + var clsName = optClassObj && optClassObj.className(), + sel = optSelObj && optSelObj.bytesAsString(); + var fn = this.generate2(method, clsName, sel); + if (fn) { + this.okCount++; + method.compiled = fn; + method.compiledLeaf = this.generateLeaf(method, + ((clsName || "M") + "_" + (sel || "m")).replace(/[^a-zA-Z0-9_]/g, "ː")) || null; + if (method.compiledLeaf) this.leafCount = (this.leafCount || 0) + 1; + } else { + this.bailCount++; + method.compiledLeaf = null; + return this.fallback.compile(method, optClassObj, optSelObj); + } + }, + enableSingleStepping: function(method, optClass, optSel) { + // debugging always uses the jit1 debug/single-step generator + return this.fallback.enableSingleStepping(method, optClass, optSel); + }, +}, +'generating', { + generateLeaf: function(method, funcName) { + // Forma "leaf" de un método: función JS pura que recibe (vm, rcvr, args...) + // y devuelve el resultado o Squeak.LEAF_DEOPT. Sin frame, sin zona, sin + // alocaciones (el overflow aritmético deoptimiza — el restart re-ejecuta + // el camino framed y ahí sí se aloca, exactamente una vez). Stores a inst + // vars permitidos solo si no hay puntos de deopt posteriores (restart-safe: + // los temps son locals JS, invisibles). + if (method.methodPrimitiveIndex() !== 0) return null; + var bytes = method.bytes; + var numArgs = method.methodNumArgs(); + var tempCount = method.methodTempCount(); + var src = []; + var depth = 0, maxDepth = 0; + var pc = 0, endPC = 0; + var labelDepth = {}, emitted = {}; + var knownDepth = true; + var instStoreSeen = false, deoptSeen = false; + var MIN = Squeak.MinSmallInt, MAX = Squeak.MaxSmallInt; + function push(expr) { src.push("v" + depth + " = " + expr + ";\n"); depth++; if (depth > maxDepth) maxDepth = depth; } + function pop() { depth--; return "v" + depth; } + function top() { return "v" + (depth - 1); } + function deopt() { deoptSeen = true; if (instStoreSeen) throw { bail: true }; return "return Squeak.LEAF_DEOPT;\n"; } + function jumpTo(dest, atDepth) { + if (dest <= pc) throw { bail: true }; // sin loops: trabajo acotado, sin interrupt checks + if (labelDepth[dest] === undefined) labelDepth[dest] = atDepth; + else if (labelDepth[dest] !== atDepth) throw { bail: true }; + if (dest > endPC) endPC = dest; + } + try { + var done = false; + while (!done) { + var instStart = pc; + if (!knownDepth) { + if (labelDepth[instStart] === undefined) throw { bail: true }; + depth = labelDepth[instStart]; + knownDepth = true; + } else if (labelDepth[instStart] !== undefined && labelDepth[instStart] !== depth) { + throw { bail: true }; + } + if (labelDepth[instStart] !== undefined) src.push("case " + instStart + ":\n"); + emitted[instStart] = depth; + var b = bytes[pc++], b2; + switch (b & 0xF8) { + case 0x00: case 0x08: push("inst[" + (b & 0xF) + "]"); break; + case 0x10: case 0x18: push("t" + (b & 0xF)); break; + case 0x20: case 0x28: case 0x30: case 0x38: push("lit[" + (1 + (b & 0x1F)) + "]"); break; + case 0x40: case 0x48: case 0x50: case 0x58: push("lit[" + (1 + (b & 0x1F)) + "].pointers[1]"); break; + case 0x60: // storeAndPop inst (receiver garantizado no-casado por el gate del call site) + if (deoptSeen) throw { bail: true }; // conservador: store tras posible deopt previo re-ejecutable? el restart re-ejecuta TODO: stores previos a deopts posteriores son el problema; deopts previos ya retornaron. Permitir. + instStoreSeen = true; + src.push("inst[" + (b & 7) + "] = " + pop() + "; rcvr.dirty = true;\n"); break; + case 0x68: src.push("t" + (b & 7) + " = " + pop() + ";\n"); break; + case 0x70: + switch (b) { + case 0x70: push("rcvr"); break; + case 0x71: push("vm.trueObj"); break; + case 0x72: push("vm.falseObj"); break; + case 0x73: push("vm.nilObj"); break; + case 0x74: push("-1"); break; + case 0x75: push("0"); break; + case 0x76: push("1"); break; + case 0x77: push("2"); break; + } + break; + case 0x78: + switch (b) { + case 0x78: src.push("return rcvr;\n"); break; + case 0x79: src.push("return vm.trueObj;\n"); break; + case 0x7A: src.push("return vm.falseObj;\n"); break; + case 0x7B: src.push("return vm.nilObj;\n"); break; + case 0x7C: src.push("return " + pop() + ";\n"); break; + default: throw { bail: true }; + } + knownDepth = false; + done = pc > endPC; + break; + case 0x80: case 0x88: + switch (b) { + case 0x80: + b2 = bytes[pc++]; + switch (b2 >> 6) { + case 0: push("inst[" + (b2 & 0x3F) + "]"); break; + case 1: push("t" + (b2 & 0x3F)); break; + case 2: push("lit[" + (1 + (b2 & 0x3F)) + "]"); break; + case 3: push("lit[" + (1 + (b2 & 0x3F)) + "].pointers[1]"); break; + } + break; + case 0x81: + b2 = bytes[pc++]; + if ((b2 >> 6) === 1) { src.push("t" + (b2 & 0x3F) + " = " + top() + ";\n"); break; } + if ((b2 >> 6) === 0) { instStoreSeen = true; src.push("inst[" + (b2 & 0x3F) + "] = " + top() + "; rcvr.dirty = true;\n"); break; } + throw { bail: true }; + case 0x82: + b2 = bytes[pc++]; + if ((b2 >> 6) === 1) { src.push("t" + (b2 & 0x3F) + " = " + pop() + ";\n"); break; } + if ((b2 >> 6) === 0) { instStoreSeen = true; src.push("inst[" + (b2 & 0x3F) + "] = " + pop() + "; rcvr.dirty = true;\n"); break; } + throw { bail: true }; + case 0x87: depth--; break; + case 0x88: push(top()); break; + default: throw { bail: true }; + } + break; + case 0x90: { var d = (b & 7) + 1; jumpTo(pc + d, depth); src.push("{ pcx = " + (pc + d) + "; continue; }\n"); knownDepth = false; done = pc > endPC; break; } + case 0x98: { var d = (b & 7) + 1, dest = pc + d, c = pop(); + jumpTo(dest, depth); + src.push("if (" + c + " === vm.falseObj) { pcx = " + dest + "; continue; }\n"); + src.push("else if (" + c + " !== vm.trueObj) " + deopt()); + break; } + case 0xA0: { b2 = bytes[pc++]; var d = ((b & 7) - 4) * 256 + b2; jumpTo(pc + d, depth); src.push("{ pcx = " + (pc + d) + "; continue; }\n"); knownDepth = false; done = pc > endPC; break; } + case 0xA8: { b2 = bytes[pc++]; var d = (b & 3) * 256 + b2, dest = pc + d, c = pop(), ifTrue = b < 0xAC; + jumpTo(dest, depth); + src.push("if (" + c + " === vm." + ifTrue + "Obj) { pcx = " + dest + "; continue; }\n"); + src.push("else if (" + c + " !== vm." + !ifTrue + "Obj) " + deopt()); + break; } + case 0xB0: case 0xB8: { // aritmética: smallint-in smallint-out, si no deopt + var op = b & 0xF, bb = pop(), aa = top(); + var guard = "if (typeof " + aa + " !== 'number' || typeof " + bb + " !== 'number') " + deopt(); + switch (op) { + case 0x0: case 0x1: { + var o = op === 0 ? "+" : "-"; + src.push(guard); + src.push("var r" + depth + " = " + aa + " " + o + " " + bb + "; if (r" + depth + " < " + MIN + " || r" + depth + " > " + MAX + ") " + deopt()); + src.push(aa + " = r" + depth + ";\n"); + break; + } + case 0x2: case 0x3: case 0x4: case 0x5: { + var cmp = ["<", ">", "<=", ">="][op - 2]; + src.push(guard); + src.push(aa + " = " + aa + " " + cmp + " " + bb + " ? vm.trueObj : vm.falseObj;\n"); + break; + } + case 0x6: case 0x7: { + src.push(guard); + src.push(aa + " = " + aa + " " + (op === 6 ? "===" : "!==") + " " + bb + " ? vm.trueObj : vm.falseObj;\n"); + break; + } + case 0xE: case 0xF: { + src.push(guard); + src.push(aa + " = " + aa + " " + (op === 0xE ? "&" : "|") + " " + bb + ";\n"); + break; + } + default: throw { bail: true }; + } + break; + } + case 0xC0: case 0xC8: { + var lo = b & 0xF; + if (lo === 0x6) { var b6 = pop(), a6 = top(); src.push(a6 + " = " + a6 + " === " + b6 + " ? vm.trueObj : vm.falseObj;\n"); break; } + if (lo === 0x7) { var t7 = top(); src.push(t7 + " = typeof " + t7 + " === 'number' ? vm.specialObjects[5] : " + t7 + ".sqClass;\n"); break; } + throw { bail: true }; + } + default: throw { bail: true }; // sends reales, closures, thisContext: no-leaf + } + } + } catch (e) { + if (e && e.bail) return null; + throw e; + } + // ensamblar + var head = []; + var params = ["vm", "rcvr"]; + for (var i = 0; i < numArgs; i++) params.push("t" + i); + var decls = []; + for (var i = numArgs; i < tempCount; i++) decls.push("t" + i + " = vm.nilObj"); + for (var i = 0; i < maxDepth; i++) decls.push("v" + i); + head.push("'use strict';\nvar lit = arguments[0];\nreturn function LEAF_" + funcName + "(" + params.join(", ") + ") {\n"); + head.push("var inst = rcvr.pointers;\n"); + if (decls.length) head.push("var " + decls.join(", ") + ";\n"); + var hasLabels = Object.keys(labelDepth).length > 0; + var body; + if (hasLabels) { + body = "var pcx = 0;\nwhile (true) switch (pcx) {\ncase 0:\n" + src.join("") + "default: return Squeak.LEAF_DEOPT;\n}\n"; + } else { + body = src.join("") + "return Squeak.LEAF_DEOPT;\n"; + } + // lit se pasa via bind para no leer method.pointers por llamada + var full = head.join("") + body + "}"; + try { + return new Function(full)(method.pointers); // literales cerrados en la función + } catch (e) { + return null; + } + }, + generate2: function(method, optClass, optSel) { + try { + return this.generateV3R(method, optClass, optSel); + } catch (e) { + if (e && e.bail) return null; + throw e; + } + }, + envNo: function(tag) { + return typeof process !== "undefined" && process.env && process.env.JIT2NO + && process.env.JIT2NO.split(",").indexOf(tag) >= 0; + }, + bail: function(why) { + throw { bail: true, why: why }; + }, + generateV3R: function(method, optClass, optSel) { + this.method = method; + this.pc = 0; + this.endPC = 0; + this.depth = 0; // virtual operand-stack depth + this.maxDepth = 0; + this.knownDepth = true; // false after unconditional jump/return until a label + this.labelDepth = {}; // pc -> expected depth (from forward jumps / resumes) + this.resumeDepth = {}; // pc -> depth to reload on trampoline re-entry + this.needsLabel = {}; + this.sourceParts = []; // list of {pc, code:[]} segments, one per instruction + this.ics = []; + var tempCount = method.methodTempCount(); + this.tempCount = tempCount; + this.blockRegions = []; // {from, to, ceil}: dentro de un block V3, los temps + // con índice >= args+copied ALIASAN slots del stack de + // operandos (los aloca el pushNil inicial del block); + // jit2 los tiene en locals, así que esos métodos bailean + // pass over bytecodes + this.done = false; + while (!this.done) { + this.instStart = this.pc; + this.beginInstruction(); + var b = method.bytes[this.pc++]; + this.generateByte(b); + } + // assemble + var parts = this.sourceParts; + var src = []; + var maxD = this.maxDepth; + var decl = []; + for (var i = 0; i < maxD; i++) decl.push("s" + i); + src.push("var zone = vm.stack, fp = vm.fp, page = vm.zonePage;\n"); + src.push("var rcvr = vm.receiver, inst = rcvr.pointers, lit = vm.method.pointers;\n"); + // spBase = base del stack de operandos de ESTA activación (métodos y + // blocks difieren); en entrada fresca depth=0 así que spBase = vm.sp, + // en re-entrada el prelude del label lo deriva de la depth de resume + src.push("var tB = vm.tempOffset, spBase = vm.sp;\n"); + if (decl.length) src.push("var ", decl.join(", "), ";\n"); + src.push("var entering = true;\n"); + src.push("while (true) switch (vm.pc) {\n"); + this.needsLabel[0] = true; + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + if (this.needsLabel[part.pc]) { + src.push("case ", part.pc, ":\n"); + // profundidad a recargar: la depth estática de este label — + // cubre resumes de sends Y transiciones intérprete->compilado + // a mitad de método (loop heads con contador en el stack) + var rd = this.emittedDepth[part.pc] || 0; + src.push("if (entering) { entering = false; spBase = vm.sp - ", rd, ";"); + for (var k = 0; k < rd; k++) + src.push(" s", k, " = zone[spBase + ", 1 + k, "];"); + src.push(" }\n"); + } + for (var j = 0; j < part.code.length; j++) src.push(part.code[j]); + } + src.push("default: if (vm.jit2Debug) { console.error('jit2 default pc=' + vm.pc + ' mbytes=' + vm.method.bytes.length); console.error('bytes: ' + Array.prototype.join.call(vm.method.bytes, ',')); console.error(String(vm.method.compiled).slice(0, 2000)); vm.jit2Debug = false; } vm.interpretOne(true); return;\n}"); + var funcName = (optClass && optSel) + ? (optClass + "_" + optSel).replace(/[^a-zA-Z0-9_]/g, "ː") + : "R_METHOD"; + var body = "'use strict';\nvar ics = arguments[0];\nreturn function " + funcName + + "(vm) {\n" + src.join("") + "}"; + try { + return new Function(body)(this.ics); + } catch (e) { + this.bail("syntax: " + e.message); + } + }, + beginInstruction: function() { + // labels: if this pc is a known jump target, depths must agree + var expected = this.labelDepth[this.instStart]; + if (!this.knownDepth) { + if (expected === undefined) this.bail("unreachable code with unknown depth"); + this.depth = expected; + this.knownDepth = true; + } else if (expected !== undefined && expected !== this.depth) { + this.bail("stack depth mismatch at label"); + } + if (!this.emittedDepth) this.emittedDepth = {}; + this.emittedDepth[this.instStart] = this.depth; + this.part = { pc: this.instStart, code: [] }; + this.sourceParts.push(this.part); + }, + emit: function(/* ... */) { + for (var i = 0; i < arguments.length; i++) this.part.code.push(arguments[i]); + }, + push: function(expr) { + this.emit("s", this.depth, " = ", expr, ";\n"); + this.depth++; + if (this.depth > this.maxDepth) this.maxDepth = this.depth; + }, + top: function() { return "s" + (this.depth - 1); }, + pop: function() { this.depth--; return "s" + this.depth; }, + slotAt: function(depthFromTop) { return "s" + (this.depth - 1 - depthFromTop); }, + checkTempAccess: function(index) { + // buscar la región de block MÁS INTERNA que contenga el pc actual + for (var i = this.blockRegions.length - 1; i >= 0; i--) { + var r = this.blockRegions[i]; + if (this.instStart >= r.from && this.instStart < r.to) { + if (index >= r.ceil) this.bail("block stack-temp aliasing"); + return; + } + } + }, + spillAll: function() { + for (var k = 0; k < this.depth; k++) + this.emit("zone[spBase + ", 1 + k, "] = s", k, ";\n"); + this.emit("vm.sp = spBase + ", this.depth, ";\n"); + }, + activationCheck: function() { + return "if (vm.fp !== fp || vm.zonePage !== page || vm.breakOutOfInterpreter !== false) return;\n"; + }, + markJumpTarget: function(dest) { + this.needsLabel[dest] = true; + if (dest > this.endPC) this.endPC = dest; + if (dest <= this.instStart) { // backward: el label ya se emitió + if (this.emittedDepth[dest] !== this.depth) this.bail("backward jump depth mismatch"); + return; + } + var expected = this.labelDepth[dest]; + if (expected === undefined) this.labelDepth[dest] = this.depth; + else if (expected !== this.depth) this.bail("jump depth mismatch"); + }, + markResume: function(pc, depthAfter) { + this.needsLabel[pc] = true; + if (pc > this.endPC) this.endPC = pc; // el label de resume debe generarse + if (this.resumeDepth[pc] !== undefined && this.resumeDepth[pc] !== depthAfter) + this.bail("conflicting resume depths"); + this.resumeDepth[pc] = depthAfter; + if (pc <= this.instStart) { // backward (interrupt check de back-jump) + if (this.emittedDepth[pc] !== depthAfter) this.bail("backward resume depth mismatch"); + return; + } + var expected = this.labelDepth[pc]; + if (expected === undefined) this.labelDepth[pc] = depthAfter; + else if (expected !== depthAfter) this.bail("resume depth mismatch"); + }, + endOfSegment: function() { + // after return/unconditional jump: depth unknown until next label + this.knownDepth = false; + this.done = this.pc > this.endPC; + }, +}, +'bytecodes', { + generateByte: function(byte) { + var b2, b3; + switch (byte & 0xF8) { + case 0x00: case 0x08: // push inst var + this.push("inst[" + (byte & 0x0F) + "]"); break; + case 0x10: case 0x18: // push temp + this.checkTempAccess(byte & 0xF); + this.push("zone[tB + " + (byte & 0xF) + "]"); break; + case 0x20: case 0x28: case 0x30: case 0x38: // push literal + this.push("lit[" + (1 + (byte & 0x1F)) + "]"); break; + case 0x40: case 0x48: case 0x50: case 0x58: // push literal indirect + this.push("lit[" + (1 + (byte & 0x1F)) + "].pointers[1]"); break; + case 0x60: // storeAndPop inst var + this.generateStoreInst(byte & 7, true); break; + case 0x68: // storeAndPop temp + this.checkTempAccess(byte & 7); + this.emit("zone[tB + ", byte & 7, "] = ", this.pop(), ";\n"); break; + case 0x70: // quick pushes + switch (byte) { + case 0x70: this.push("rcvr"); break; + case 0x71: this.push("vm.trueObj"); break; + case 0x72: this.push("vm.falseObj"); break; + case 0x73: this.push("vm.nilObj"); break; + case 0x74: this.push("-1"); break; + case 0x75: this.push("0"); break; + case 0x76: this.push("1"); break; + case 0x77: this.push("2"); break; + } + break; + case 0x78: // quick returns + switch (byte) { + case 0x78: this.generateReturn("rcvr"); break; + case 0x79: this.generateReturn("vm.trueObj"); break; + case 0x7A: this.generateReturn("vm.falseObj"); break; + case 0x7B: this.generateReturn("vm.nilObj"); break; + case 0x7C: this.generateReturn(this.pop()); break; + case 0x7D: this.generateBlockReturn(); break; + default: this.bail("unusedBytecode " + byte); + } + break; + case 0x80: case 0x88: + this.generateExtended(byte); break; + case 0x90: // short jump + this.generateJump((byte & 7) + 1); break; + case 0x98: // short conditional jump (jumpIfFalse) + this.generateJumpIf(false, (byte & 7) + 1); break; + case 0xA0: // long jump + b2 = this.method.bytes[this.pc++]; + this.generateJump(((byte & 7) - 4) * 256 + b2); break; + case 0xA8: // long conditional jump + b2 = this.method.bytes[this.pc++]; + this.generateJumpIf(byte < 0xAC, (byte & 3) * 256 + b2); break; + case 0xB0: case 0xB8: // arithmetic special sends + this.generateNumericOp(byte); break; + case 0xC0: case 0xC8: // quick prims + this.generateQuickPrim(byte); break; + case 0xD0: case 0xD8: // send literal selector, 0 args + this.generateSend(1 + (byte & 0xF), 0); break; + case 0xE0: case 0xE8: // 1 arg + this.generateSend(1 + (byte & 0xF), 1); break; + case 0xF0: case 0xF8: // 2 args + this.generateSend(1 + (byte & 0xF), 2); break; + default: this.bail("bytecode " + byte); + } + }, + generateExtended: function(byte) { + var b2, b3; + switch (byte) { + case 0x80: // extended push + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.push("inst[" + (b2 & 0x3F) + "]"); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.push("zone[tB + " + (b2 & 0x3F) + "]"); break; + case 2: this.push("lit[" + (1 + (b2 & 0x3F)) + "]"); break; + case 3: this.push("lit[" + (1 + (b2 & 0x3F)) + "].pointers[1]"); break; + } + break; + case 0x81: // extended store (no pop) + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.generateStoreInst(b2 & 0x3F, false); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.emit("zone[tB + ", b2 & 0x3F, "] = ", this.top(), ";\n"); break; + case 2: this.bail("store into literal"); + case 3: this.emit("var assoc = lit[", 1 + (b2 & 0x3F), "]; assoc.dirty = true; assoc.pointers[1] = ", this.top(), ";\n"); break; + } + break; + case 0x82: // extended store-pop + b2 = this.method.bytes[this.pc++]; + switch (b2 >> 6) { + case 0: this.generateStoreInst(b2 & 0x3F, true); break; + case 1: this.checkTempAccess(b2 & 0x3F); this.emit("zone[tB + ", b2 & 0x3F, "] = ", this.pop(), ";\n"); break; + case 2: this.bail("store into literal"); + case 3: this.emit("var assoc = lit[", 1 + (b2 & 0x3F), "]; assoc.dirty = true; assoc.pointers[1] = ", this.pop(), ";\n"); break; + } + break; + case 0x83: // single extended send + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 31), b2 >> 5); + break; + case 0x84: // double extended do-anything + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + switch (b2 >> 5) { + case 0: this.generateSend(1 + b3, b2 & 31); break; + case 1: if (this.envNo("super")) this.bail("super"); this.generateSend(1 + b3, b2 & 31, true); break; + case 2: this.push("inst[" + b3 + "]"); break; + case 3: this.push("lit[" + (1 + b3) + "]"); break; + case 4: this.push("lit[" + (1 + b3) + "].pointers[1]"); break; + default: this.bail("doubleExtended op " + (b2 >> 5)); + } + break; + case 0x85: // single extended send to super + if (this.envNo("super")) this.bail("super"); + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 31), b2 >> 5, true); + break; + case 0x86: // second extended send + if (this.envNo("send2")) this.bail("send2"); + b2 = this.method.bytes[this.pc++]; + this.generateSend(1 + (b2 & 0x3F), b2 >> 6); + break; + case 0x87: // pop + this.depth--; break; + case 0x88: // dup + this.push(this.top()); break; + case 0x89: // thisContext + this.spillAll(); + this.push("vm.exportThisContext()"); + break; + case 0x8A: { // closure temps (array de indirección) + if (this.envNo("ctemps")) this.bail("ctemps"); + b2 = this.method.bytes[this.pc++]; + var popValues = b2 > 127, count = b2 & 127; + this.emit("var arr = vm.instantiateClass(vm.specialObjects[7], ", count, ");\n"); + if (popValues) { + for (var ci = 0; ci < count; ci++) + this.emit("arr.pointers[", ci, "] = ", this.slotAt(count - ci - 1), ";\n"); + this.depth -= count; + } + this.push("arr"); + break; + } + case 0x8C: + if (this.envNo("vec")) this.bail("vec"); + // remote push from temp vector + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.push("zone[tB + " + b3 + "].pointers[" + b2 + "]"); + break; + case 0x8D: { // remote store into temp vector + if (this.envNo("vec")) this.bail("vec"); + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.emit("var tv = zone[tB + ", b3, "]; tv.pointers[", b2, "] = ", this.top(), "; tv.dirty = true;\n"); + break; + } + case 0x8E: { // remote store and pop + if (this.envNo("vec")) this.bail("vec"); + b2 = this.method.bytes[this.pc++]; + b3 = this.method.bytes[this.pc++]; + this.emit("var tv = zone[tB + ", b3, "]; tv.pointers[", b2, "] = ", this.pop(), "; tv.dirty = true;\n"); + break; + } + case 0x8F: // pushClosureCopy + this.generateClosureCopy(); break; + default: + this.bail("extended " + byte); // 0x8B callPrimitive: jit1 + } + }, + generateStoreInst: function(index, popIt) { + var val = popIt ? this.pop() : this.top(); + // married-context write-through, same protocol as jit1 frames templates + this.emit("if (rcvr.sqClass !== vm.contextClass_ || rcvr.frame == null) { inst[", index, "] = ", val, "; rcvr.dirty = true; }\n"); + this.emit("else { "); + this.spillAll(); + this.emit("vm.pc = ", this.pc, "; vm.storeToMarriedContext(rcvr, ", index, ", ", val, "); ", + this.activationCheck(), " }\n"); + this.markResume(this.pc, this.depth); + }, + generateReturn: function(what) { + this.emit("vm.pc = ", this.pc, "; vm.doReturn(", what, "); return;\n"); + this.endOfSegment(); + }, + generateBlockReturn: function() { + this.emit("vm.pc = ", this.pc, "; vm.doBlockReturn(", this.pop(), "); return;\n"); + this.endOfSegment(); + }, + generateJump: function(distance) { + var dest = this.pc + distance; + if (distance < 0) { + // backward jump: interrupt check; a process switch must find the + // frame resumable, so spill live slots (loop back-edges: usually 0) + this.spillAll(); + this.emit("if (vm.interruptCheckCounter-- <= 0) {\n", + " vm.pc = ", dest, "; vm.checkForInterrupts();\n", + " ", this.activationCheck(), "}\n"); + this.markResume(dest, this.depth); + } + this.markJumpTarget(dest); + this.emit("vm.pc = ", dest, "; continue;\n"); + this.endOfSegment(); + }, + generateJumpIf: function(condition, distance) { + var dest = this.pc + distance; + var cond = this.pop(); + this.emit("if (", cond, " === vm.", condition, "Obj) { vm.pc = ", dest, "; continue; }\n"); + this.emit("else if (", cond, " !== vm.", !condition, "Obj) {\n"); + this.depth++; this.spillAll(); this.depth--; // cond back on stack as receiver + this.emit(" vm.pc = ", this.pc, "; vm.send(vm.specialObjects[25], 0, false); return; }\n"); + this.markJumpTarget(dest); + this.markResume(this.pc, this.depth); + }, + generateSend: function(litIndex, argCount, superFlag) { + var ic = this.ics.length; + this.ics.push({ c: null, m: null, a: 0, p: 0, k: null }); + var rcvrSlot = this.slotAt(argCount); + var argSlots = []; + for (var i = argCount - 1; i >= 0; i--) argSlots.push(this.slotAt(i)); + this.emit("vm.pc = ", this.pc, ";\n"); + this.emit("var ic = ics[", ic, "], rc = typeof ", rcvrSlot, " === 'number' ? vm.smallIntClass_ : ", rcvrSlot, ".sqClass;\n"); + if (superFlag) { + // super: la clase de lookup es estática (superclase de la clase del + // método) — el IC se llena una sola vez y vale para todo receiver + this.emit("if (ic.c === null) vm.jit2FillSuperIC(ic, lit[", litIndex, "], ", argCount, ");\n"); + } else { + this.emit("if (rc !== ic.c) vm.jit2FillIC(ic, lit[", litIndex, "], ", argCount, ", rc);\n"); + } + // leaf fast path: sin spill, sin frame, args como argumentos JS. Solo con + // interruptCheckCounter > 0 (paridad exacta de cadencia con el golden: + // vencido => camino framed, donde executeNewMethod chequea como siempre) + this.emit("var lr = vm.LEAF_DEOPT_;\n"); + this.emit("if (ic.a === ", argCount, " && ic.m.compiledLeaf != null && rc !== vm.contextClass_ && vm.interruptCheckCounter > 0) {\n"); + this.emit(" var sc = vm.sendCount++; vm.interruptCheckCounter--;\n"); + this.emit(" lr = ic.m.compiledLeaf(vm, ", [rcvrSlot].concat(argSlots).join(", "), ");\n"); + this.emit(" if (lr === vm.LEAF_DEOPT_) { vm.sendCount--; vm.interruptCheckCounter++; vm.nLeafDeopts++; }\n"); + this.emit(" else { vm.nLeafCalls++; if (vm.jit2LeafHook) vm.jit2LeafHook(ic.m, sc, ", rcvrSlot, ", lit[", litIndex, "]); }\n"); + this.emit("}\n"); + var resultSlot = "s" + (this.depth - argCount - 1); + this.emit("if (lr !== vm.LEAF_DEOPT_) { ", resultSlot, " = lr; }\n"); + this.emit("else {\n"); + this.spillAll(); + this.emit("if (rc === vm.contextClass_ && ", rcvrSlot, ".frame != null) vm.syncMarriedContext(", rcvrSlot, ");\n"); + this.emit("if (ic.p) { vm.verifyAtSelector = lit[", litIndex, "]; vm.verifyAtClass = ", superFlag ? "ic.c" : "rc", "; }\n"); + this.emit("vm.executeNewMethod(", rcvrSlot, ", ic.m, ic.a, ic.p, ic.k, lit[", litIndex, "]);\n"); + this.emit(this.activationCheck()); + this.emit(resultSlot, " = zone[vm.sp];\n"); + this.emit("}\n"); + this.depth -= argCount + 1; + this.depth++; if (this.depth > this.maxDepth) this.maxDepth = this.depth; + this.markResume(this.pc, this.depth); + }, + generateNumericOp: function(byte) { + var a, b; + switch (byte & 0xF) { + case 0x0: case 0x1: { // + - + var op = (byte & 0xF) === 0 ? "+" : "-"; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = vm.primHandler.signed32BitIntegerFor(", a, " ", op, " ", b, ");\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0x2: case 0x3: case 0x4: case 0x5: { // < > <= >= + var cmp = ["<", ">", "<=", ">="][(byte & 0xF) - 2]; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = ", a, " ", cmp, " ", b, " ? vm.trueObj : vm.falseObj;\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0x6: case 0x7: { // = ~= + var eq = (byte & 0xF) === 6; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number') ", + a, " = ", a, " ", eq ? "===" : "!==", " ", b, " ? vm.trueObj : vm.falseObj;\n"); + this.emit("else if (", a, " === ", b, " && ", a, ".float === ", a, ".float) ", + a, " = vm.", eq ? "true" : "false", "Obj;\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + case 0xE: case 0xF: { // bitAnd: bitOr: + var bop = (byte & 0xF) === 0xE ? "&" : "|"; + b = this.pop(); a = this.top(); + this.emit("if (typeof ", a, " === 'number' && typeof ", b, " === 'number' && (", a, "|0) === ", a, " && (", b, "|0) === ", b, ") ", + a, " = ", a, " ", bop, " ", b, ";\n"); + this.generateNumericFallback(a, b, byte & 0xF); + break; + } + default: { // * / \\ @ bitShift: // : usar el camino genérico de vm + b = this.pop(); a = this.top(); + this.generateNumericSlow(byte & 0xF); + break; + } + } + }, + generateNumericFallback: function(a, b, opIndex) { + // a/b son locals; a ya tiene el resultado si el fast path aplicó. + // Si no aplicó, mandar el send especial (stack: rcvr, arg). + this.emit("else {\n"); + this.depth++; this.spillAll(); this.depth--; + this.emit(" vm.pc = ", this.pc, "; vm.sendSpecial(", opIndex, "); ", this.activationCheck()); + this.emit(" ", a, " = zone[vm.sp];\n}\n"); + this.markResume(this.pc, this.depth); + }, + generateNumericSlow: function(opIndex) { + // sin fast path inline: siempre via camino del vm (pop2AndPush... via sendSpecial-like) + this.depth++; this.spillAll(); this.depth--; + var a = "s" + (this.depth - 1); + this.emit("vm.pc = ", this.pc, "; vm.sp = spBase + ", this.depth + 1, ";\n"); + // replicar jit1: success+pop2AndPushResult según op + switch (opIndex) { + case 0x8: this.emit("vm.success = true; vm.resultIsFloat = false; if (!vm.pop2AndPushNumResult(vm.stackIntOrFloat(1) * vm.stackIntOrFloat(0))) { vm.sendSpecial(8); ", this.activationCheck(), "}\n"); break; + case 0x9: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.quickDivide(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(9); ", this.activationCheck(), "}\n"); break; + case 0xA: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.mod(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(10); ", this.activationCheck(), "}\n"); break; + case 0xB: this.emit("vm.success = true; if (!vm.primHandler.primitiveMakePoint(1, true)) { vm.sendSpecial(11); ", this.activationCheck(), "}\n"); break; + case 0xC: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.safeShift(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(12); ", this.activationCheck(), "}\n"); break; + case 0xD: this.emit("vm.success = true; if (!vm.pop2AndPushIntResult(vm.div(vm.stackInteger(1), vm.stackInteger(0)))) { vm.sendSpecial(13); ", this.activationCheck(), "}\n"); break; + default: this.bail("numeric op " + opIndex); + } + this.emit(a, " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + }, + generateQuickPrim: function(byte) { + var lo = byte & 0xF; + switch (lo) { + case 0x6: { // == + var b = this.pop(), a = this.top(); + this.emit(a, " = ", a, " === ", b, " ? vm.trueObj : vm.falseObj;\n"); + return; + } + case 0x7: { // class + var t = this.top(); + this.emit(t, " = typeof ", t, " === 'number' ? vm.specialObjects[5] : ", t, ".sqClass;\n"); + return; + } + case 0x0: { // at: + var idx = this.slotAt(0), arr = this.slotAt(1); + this.emit("if (", arr, ".sqClass === vm.specialObjects[7] && ", arr, ".pointers && typeof ", idx, " === 'number' && ", idx, " > 0 && ", idx, " <= ", arr, ".pointers.length) {\n", + " ", arr, " = ", arr, ".pointers[", idx, " - 1];\n} else {\n"); + this.spillAll(); + this.emit(" var c = vm.primHandler.objectAt(true,true,false); if (vm.primHandler.success) { vm.sp -= 1; ", arr, " = c; } else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(16); ", this.activationCheck(), + " ", arr, " = zone[vm.sp]; }\n}\n"); + this.depth--; + this.markResume(this.pc, this.depth); + return; + } + case 0x1: { // at:put: + var val = this.slotAt(0), idx = this.slotAt(1), arr = this.slotAt(2); + this.emit("if (", arr, ".sqClass === vm.specialObjects[7] && ", arr, ".pointers && typeof ", idx, " === 'number' && ", idx, " > 0 && ", idx, " <= ", arr, ".pointers.length) {\n", + " ", arr, ".pointers[", idx, " - 1] = ", val, "; ", arr, ".dirty = true; ", arr, " = ", val, ";\n} else {\n"); + this.spillAll(); + // puede flushear (context casado): pc de re-ejecución idempotente + this.emit(" vm.pc = ", this.instStart, "; vm.primHandler.objectAtPut(true,true,false); if (vm.fp !== fp || vm.zonePage !== page) return;\n", + " if (vm.primHandler.success) { vm.sp -= 2; ", arr, " = ", val, "; } else {\n", + " vm.pc = ", this.pc, "; vm.sendSpecial(17); ", this.activationCheck(), + " ", arr, " = zone[vm.sp]; }\n}\n"); + this.depth -= 2; + this.needsLabel[this.instStart] = true; + this.labelDepth[this.instStart] = this.depth + 2; + this.resumeDepth[this.instStart] = this.depth + 2; + this.markResume(this.pc, this.depth); + return; + } + case 0x2: { // size + var t = this.top(); + this.emit("if (", t, ".sqClass === vm.specialObjects[7]) ", t, " = ", t, ".pointersSize();\n", + "else if (", t, ".sqClass === vm.specialObjects[6]) ", t, " = ", t, ".bytesSize();\n", + "else {\n"); + this.spillAll(); + this.emit(" vm.pc = ", this.pc, "; vm.sendSpecial(18); ", this.activationCheck(), + " ", t, " = zone[vm.sp];\n}\n"); + this.markResume(this.pc, this.depth); + return; + } + case 0x9: case 0xA: case 0xB: { // value value: do: + this.spillAll(); + var rcvrSlot = this.slotAt(lo === 0x9 ? 0 : 1); + this.emit("vm.pc = ", this.pc, "; if (!vm.primHandler.quickSendOther(", rcvrSlot, ", ", lo, ")) vm.sendSpecial(", lo + 16, "); return;\n"); + this.depth -= (lo === 0x9 ? 0 : 1); + this.markResume(this.pc, this.depth); + this.endOfSegment(); + // tras el return el resultado queda en zone: el resume lo recarga + return; + } + case 0x3: case 0x5: case 0xC: case 0xE: case 0xF: { // next atEnd new x y (0 args) + if (this.envNo("qp0")) this.bail("qp0"); + this.spillAll(); // receiver ya está en el stack virtual + this.emit("vm.primHandler.success = true; vm.pc = ", this.pc, "; vm.sendSpecial(", lo + 16, "); ", this.activationCheck()); + this.emit(this.top(), " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + return; + } + case 0x4: case 0xD: { // nextPut: new: (1 arg) + if (this.envNo("qp1")) this.bail("qp1"); + this.spillAll(); + this.emit("vm.primHandler.success = true; vm.pc = ", this.pc, "; vm.sendSpecial(", lo + 16, "); ", this.activationCheck()); + this.depth--; + this.emit(this.top(), " = zone[vm.sp];\n"); + this.markResume(this.pc, this.depth); + return; + } + default: // blockCopy: do: -> jit1 + this.bail("quick prim " + lo); + } + }, + generateClosureCopy: function() { + var b = this.method.bytes; + var numArgsNumCopied = b[this.pc++], + numArgs = numArgsNumCopied & 0xF, + numCopied = numArgsNumCopied >> 4, + blockSize = b[this.pc++] * 256 + b[this.pc++]; + var from = this.pc, to = from + blockSize; + this.blockRegions.push({ from: from, to: to, ceil: numArgs + numCopied }); + // marry del frame activo: la zona y vm.sp deben reflejar el estado real + // (el snapshot del context casado y el GC leen vm.sp) + this.spillAll(); + this.emit("var closure = vm.instantiateClass(vm.specialObjects[36], ", numCopied, ");\n"); + this.emit("closure.pointers[0] = vm.activeContextObj(); vm.reclaimableContextCount = 0;\n"); + this.emit("closure.pointers[1] = ", from + this.method.pointers.length * 4 + 1, ";\n"); + this.emit("closure.pointers[2] = ", numArgs, ";\n"); + for (var i = 0; i < numCopied; i++) + this.emit("closure.pointers[", 3 + i, "] = ", this.slotAt(numCopied - i - 1), ";\n"); + this.depth -= numCopied; + this.push("closure"); + this.emit("vm.pc = ", to, "; continue;\n"); + this.markJumpTarget(to); + // el cuerpo del block arranca con stack vacío (activación fresca) + this.needsLabel[from] = true; + this.labelDepth[from] = 0; + this.resumeDepth[from] = 0; + if (to > this.endPC) this.endPC = to; + this.endOfSegment(); + }, +}); + +// vm-side support +Object.extend(Squeak.Interpreter.prototype, 'jit2 support', { + jit2FillSuperIC: function(ic, selector, argCount) { + // super: lookup estático en la superclase de la clase del método activo + var lookupClass = this.method.methodClassForSuper().superclass(); + this.jit2FillIC(ic, selector, argCount, lookupClass); + }, + jit2FillIC: function(ic, selector, argCount, lookupClass) { + var entry = this.findSelectorInClass(selector, argCount, lookupClass); + ic.c = lookupClass; + ic.m = entry.method; + ic.a = entry.argCount; + ic.p = entry.primIndex; + ic.k = entry.mClass; + }, +}); diff --git a/perf/README.md b/perf/README.md new file mode 100644 index 00000000..b9ff72b8 --- /dev/null +++ b/perf/README.md @@ -0,0 +1,40 @@ +# perf/ — proyecto stack zone (frames planos + reificación perezosa de Contexts) + +Ver [stack-zone-design.md](stack-zone-design.md) para el diseño completo, las reglas +de reificación verificadas contra el código, y el plan por fases. + +## spike/ — benchmarks fase 0 (sintéticos) + +Cinco variantes del mismo `fib(32)` por sends (7.05M activaciones) que aíslan +(a) contexts heap vs frames planos y (b) JS vs WASM. Resultados 2026-07-05 +(Node 20, M-series): + +| variante | ms | vs V1 | +|---|---|---| +| V1 contexts + trampolín (≈ jit.js actual) | 198 | 1.00x | +| V2 frames + trampolín (JS puro, camino incremental) | 143 | **1.38x** | +| V3 frames + intérprete de bytecodes JS | 313 | 0.63x | +| V4 frames + intérprete de bytecodes WASM | 183 | 1.08x | +| V5 frames + métodos compilados WASM (estimador fase 2) | 74 | **2.7x** | + +Correr: + +``` +npm install --no-save assemblyscript +npx asc perf/spike/as/frames.ts --target release -O3 --noAssert --runtime stub \ + --initialMemory 2 --outFile perf/spike/as/frames.wasm +node perf/spike/bench2.js # desde perf/spike/ +``` + +## harness/ — oráculo diferencial (paso 0) + +Corre `ws/client/cuis.image` headless en Node con reloj virtual determinista y +acumula un hash de la traza de ejecución (pc/método/sends en cada checkpoint). +Dos corridas del mismo VM producen el mismo hash; un VM modificado que diverge +en semántica produce otro hash → detección automática de bugs sin tests en-imagen. + +``` +node perf/harness/difftrace.js --golden # graba perf/harness/golden.json +node perf/harness/difftrace.js # compara contra el golden +node perf/harness/difftrace.js --bench # mide tiempo real (reloj de verdad) +``` diff --git a/perf/harness/difftrace.js b/perf/harness/difftrace.js new file mode 100644 index 00000000..8c7e498d --- /dev/null +++ b/perf/harness/difftrace.js @@ -0,0 +1,606 @@ +"use strict"; +// Oráculo diferencial para el proyecto stack-zone (ver ../stack-zone-design.md). +// +// Corre una imagen headless en Node con entorno determinista (reloj virtual, +// random sembrado, WebSocket inerte) y acumula un hash de la traza de ejecución +// (sendCount/pc/sp/método en cada slice). Dos corridas del mismo VM producen el +// mismo hash; cualquier divergencia semántica introducida por un VM modificado +// produce otro hash. +// +// node perf/harness/difftrace.js --golden graba perf/harness/golden.json +// node perf/harness/difftrace.js compara contra el golden (exit 1 si difiere) +// node perf/harness/difftrace.js --bench reloj real, mide wall-time y sends/s +// opciones: --sends N (default 20000000), --image ruta (default ws/client/cuis.image) + +var os = require("os"); +var fs = require("fs"); +var path = require("path"); + +var repoRoot = path.join(__dirname, "..", ".."); +var args = process.argv.slice(2); +var mode = args.indexOf("--golden") >= 0 ? "golden" + : args.indexOf("--bench") >= 0 ? "bench" + : "check"; +var useFrames = args.indexOf("--frames") >= 0; // correr con el stack zone activado +var useJit2 = args.indexOf("--jit2") >= 0; // stack-to-register jit (requiere --frames) +var noJit = args.indexOf("--nojit") >= 0; // apagar el jit (para aislar divergencias jit vs frames) +function argValue(name, deflt) { + var i = args.indexOf(name); + return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : deflt; +} +var maxSends = parseInt(argValue("--sends", "20000000"), 10); +var imagePath = path.resolve(repoRoot, argValue("--image", "ws/client/cuis.image")); +var goldenPath = path.join(__dirname, "golden.json"); +var logPath = argValue("--log", null); // traza por checkpoint, para ubicar divergencias +var logFrom = parseInt(argValue("--logfrom", "-1"), 10); // log fino por-send en [logfrom, logto] +var logTo = parseInt(argValue("--logto", "-1"), 10); + +// --------------------------------------------------------------------------- +// Entorno determinista (solo en modos de traza; --bench usa el reloj real) +// --------------------------------------------------------------------------- +var virtualMs = 0; +var VIRTUAL_EPOCH = 1735689600000; // 2025-01-01, fijo +// El reloj virtual queda congelado hasta que arranca el loop de interpretación: +// el boot de Node/carga de imagen consume Date.now un número variable de veces +// (estado frío vs caliente) y no debe correr la línea de tiempo. +var clockRunning = false; +if (mode !== "bench") { + var clockCalls = 0; + var virtualNow = function() { + if (clockRunning && ++clockCalls % 4 === 0) virtualMs++; + return VIRTUAL_EPOCH + virtualMs; + }; + // Stub de Date completo: new Date() sin args también debe ser virtual + // (el código de la imagen loguea timestamps; con reloj real divergen las trazas) + var RealDate = Date; + var FakeDate = function Date() { + if (arguments.length === 0) return new RealDate(virtualNow()); + return new (RealDate.bind.apply(RealDate, [null].concat(Array.prototype.slice.call(arguments))))(); + }; + FakeDate.prototype = RealDate.prototype; + FakeDate.now = virtualNow; + FakeDate.UTC = RealDate.UTC; + FakeDate.parse = RealDate.parse; + global.Date = FakeDate; + global.performance = { now: function() { return virtualMs; } }; + var seed = 42 >>> 0; + Math.random = function() { + seed = (seed * 1664525 + 1013904223) >>> 0; + return seed / 4294967296; + }; +} + +// --------------------------------------------------------------------------- +// Bootstrapping del VM: mismo esquema que squeak_node.js +// --------------------------------------------------------------------------- +Object.assign(global, { + self: new Proxy({}, { + get: function(obj, prop) { return global[prop]; }, + set: function(obj, prop, value) { global[prop] = value; return true; } + }) +}); + +// WebSocket inerte: la imagen de ws/client intenta conectarse al arrancar; acá +// queda CONNECTING para siempre (el timeout del lado Smalltalk corre con el +// reloj virtual, así que es determinista). +function FakeWebSocket(url) { + this.url = url; + this.readyState = 0; + this.onopen = null; this.onclose = null; this.onmessage = null; this.onerror = null; +} +FakeWebSocket.prototype.send = function() {}; +FakeWebSocket.prototype.close = function() { this.readyState = 3; }; +FakeWebSocket.prototype.addEventListener = function() {}; +FakeWebSocket.prototype.removeEventListener = function() {}; + +Object.assign(self, { + localStorage: {}, + // inerte también en --bench: el workload debe ser idéntico al de la traza + WebSocket: FakeWebSocket, + sha1: require(path.join(repoRoot, "lib/sha1")), + btoa: function(string) { return Buffer.from(string, "ascii").toString("base64"); }, + atob: function(string) { return Buffer.from(string, "base64").toString("ascii"); } +}); + +[ + "globals.js", "vm.js", "vm.object.js", "vm.object.spur.js", "vm.image.js", + "vm.interpreter.js", "vm.interpreter.proxy.js", "vm.instruction.stream.js", + "vm.instruction.stream.sista.js", "vm.instruction.printer.js", "vm.primitives.js", + "jit.js", "jit2.js", "vm.display.js", "vm.display.headless.js", "vm.input.js", + "vm.input.headless.js", "vm.plugins.js", "vm.plugins.file.node.js", + "vm.stackzone.js", +].forEach(function(f) { require(path.join(repoRoot, f)); }); + +Object.extend(Squeak, { + vmPath: path.dirname(imagePath) + path.sep, + platformSubtype: "Node.js", + osVersion: mode === "bench" + ? process.version + " " + os.platform() + " " + os.release() + " " + os.arch() + : "difftrace-deterministic", // la imagen puede leer esto; que no varíe por máquina + windowSystem: "none", +}); + +Object.extend(Squeak.Primitives.prototype, { + loadModuleDynamically: function(modName) { + try { + require(path.join(repoRoot, "plugins", modName)); + return Squeak.externalModules[modName]; + } catch (e) { + console.error("Plugin " + modName + " could not be loaded"); + } + return undefined; + } +}); + +// --------------------------------------------------------------------------- +// Driver síncrono + hash de traza +// --------------------------------------------------------------------------- +var hash = 2166136261 >>> 0; // FNV-1a +var mixLog = process.env.MIXLOG ? [] : null; +function mix(v) { + if (mixLog) mixLog.push(v); + hash = (hash ^ (v >>> 0)) >>> 0; + hash = Math.imul(hash, 16777619) >>> 0; +} +process.on("exit", function() { + if (mixLog) require("fs").writeFileSync(process.env.MIXLOG, mixLog.join("\n") + "\n"); +}); + +// La imagen escribe artefactos junto a sí misma (crea el .changes si falta, +// logs de debug). Limpiarlos antes de correr para que toda corrida parta del +// mismo estado de filesystem — si no, la primera corrida en un clone fresco +// difiere de las siguientes. +var imageDir = path.dirname(imagePath); +var imageBase = path.basename(imagePath, ".image"); +fs.readdirSync(imageDir).forEach(function(f) { + if (f === imageBase + ".changes" || /^CuisDebug-.*\.log$/.test(f)) { + fs.unlinkSync(path.join(imageDir, f)); + } +}); + +var data = fs.readFileSync(imagePath); +var image = new Squeak.Image(imagePath.replace(/\.image$/, "")); +image.readFromBuffer(data.buffer, function startRunning() { + var display = { vmOptions: ["-vm-display-null", "-nodisplay"] }; + var vm = new Squeak.Interpreter(image, display, useFrames ? { stackZone: true, jit2: useJit2 } : {}); + if (noJit) vm.compiler = null; + if (process.env.JIT2DBG) vm.jit2Debug = true; + if (process.env.SEMDBG) { + var origSS = vm.primHandler.synchronousSignal; + vm.primHandler.synchronousSignal = function(sema) { + if (vm.sendCount >= 717670 && vm.sendCount <= 717685) + console.error("SIG s=" + vm.sendCount + " semaHash=" + sema.hash + + " excess=" + sema.pointers[Squeak.Sema_excessSignals] + + " empty=" + this.isEmptyList(sema) + + " firstLink=" + (sema.pointers[Squeak.LinkedList_firstLink].isNil ? "nil" : "proc:" + sema.pointers[Squeak.LinkedList_firstLink].hash)); + return origSS.call(this, sema); + }; + var origWait = vm.primHandler.primitiveWait || null; + } + if (process.env.GCDBG) { + var origFGR = vm.frameGCRoots; + var fgrCalls = 0; + vm.frameGCRoots = function() { + var roots = origFGR.call(vm); + if (++fgrCalls <= 2) { + var desc = []; + for (var i = 0; i < vm.zonePages.length; i++) { + var pg = vm.zonePages[i]; + desc.push((pg.live ? "L" : "d") + " fp=" + pg.fp + " sp=" + pg.sp + " len=" + pg.slots.length); + } + console.error("FGR#" + fgrCalls + " s=" + vm.sendCount + " roots=" + roots.length + " | " + desc.join(" | ")); + } + return roots; + }; + } + if (process.env.CHKDBG) { + var origCFI = vm.checkForInterrupts; + vm.checkForInterrupts = function() { + console.error("CHK s=" + vm.sendCount + " vms=" + virtualMs + " reset=" + vm.interruptCheckCounterFeedBackReset + " wake=" + vm.nextWakeupTick); + return origCFI.call(vm); + }; + } + if (process.env.ZDBG9) { + // volcar bytes+literales del método activado cuando mbytes coincide + var z9size = parseInt(process.env.ZDBG9_SIZE), z9From = parseInt(process.env.ZDBG9_FROM || "0"); + var origENM9 = vm.executeNewMethod; + var dumped = 0; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + if (vm.sendCount >= z9From && dumped < 2 && newMethod.bytes && newMethod.bytes.length === z9size) { + dumped++; + var lits = []; + for (var i = 0; i < newMethod.pointers.length; i++) { + var l = newMethod.pointers[i]; + lits.push(l && l.bytesAsString ? l.bytesAsString() : (l && l.sqClass ? l.sqClass.className() : String(l))); + } + console.error("MDUMP s=" + vm.sendCount + " sel=" + (optSel && optSel.bytesAsString ? optSel.bytesAsString() : "?") + + " bytes=[" + Array.prototype.join.call(newMethod.bytes, ",") + "] lits=" + JSON.stringify(lits)); + } + return origENM9.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + }; + } + if (process.env.PFNDBG) vm.primFnDebug = true; + if (process.env.SSDBG) { + var c2 = vm.compiler; + if (c2) { + var origESS = c2.enableSingleStepping.bind(c2); + c2.enableSingleStepping = function(method, optClass, optSel) { + vm.nSingleStep = (vm.nSingleStep || 0) + 1; + if (vm.nSingleStep <= 5) console.error("SINGLESTEP #" + vm.nSingleStep + " s=" + vm.sendCount + " pc=" + vm.pc + " mbytes=" + (method.bytes ? method.bytes.length : "?")); + return origESS(method, optClass, optSel); + }; + } + } + if (process.env.FREEZE_SIM) { + // replica el patrón del FilePlugin del browser (fileContentsDo): + // el primitivo congela el VM y retorna true sin efecto de stack; un + // callback diferido hace unfreeze() y LUEGO aplica el efecto original + var wrapFrozen = function(mod, fnName) { + var orig = mod[fnName].bind(mod); + mod[fnName] = function(argCount) { + if (vm.frozen) return orig(argCount); // ya diferido: ejecutar directo + vm.nFreezeSim = (vm.nFreezeSim || 0) + 1; + vm.freeze(function(unfreeze) { + setImmediate(function() { + unfreeze(); + orig(argCount); + }); + }); + return true; + }; + }; + var origLM2 = vm.primHandler.loadModule.bind(vm.primHandler); + vm.primHandler.loadModule = function(name) { + var m = origLM2(name); + if (name === "FilePlugin" && m && !m._frozenSim) { + m._frozenSim = true; + for (var k in m) + if (typeof m[k] === "function" && /^primitive/.test(k)) + wrapFrozen(m, k); + } + return m; + }; + } + if (process.env.PAGEDBG) { vm.pageStats = {fresh:0, reused:0, flushActive:0, flushSusp:0, flushDead:0}; process.on("exit", function(){ console.error("PAGES:", JSON.stringify(vm.pageStats)); }); } + if (process.env.SMCDBG) { vm.smcStats = {}; process.on("exit", function() { console.error("SMC stores por índice:", JSON.stringify(vm.smcStats)); }); } + if (process.env.FADBG) { + // loguear llamadas a FloatArrayPlugin.primitiveAt: (sc, sp-antes, success-después, sp-después) + var faCount = 0; + var patchFA = function(mod) { + if (!mod || mod._faPatched) return !!mod; + mod._faPatched = true; + var orig = mod.primitiveAt; + mod.primitiveAt = function(argCount) { + var spBefore = vm.sp; + var r = orig.call(this, argCount); + if (++faCount <= 40) + console.error("FA#" + faCount + " s=" + vm.sendCount + " spB=" + spBefore + + " r=" + r + " succ=" + vm.primHandler.success + " spA=" + vm.sp); + return r; + }; + return true; + }; + var origLMD = vm.primHandler.loadModule.bind(vm.primHandler); + vm.primHandler.loadModule = function(name) { + var m = origLMD(name); + if (name === "FloatArrayPlugin") patchFA(m); + return m; + }; + } + if (process.env.ZDBG7) { + // dump de cadena al activar un método específico (por _traceId) + var z7id = parseInt(process.env.ZDBG7_ID), z7From = parseInt(process.env.ZDBG7_FROM || "0"); + var chainDesc = function() { + var desc = []; + if (vm.useStackZone) { + var page = vm.zonePage, fp = vm.fp, hops = 0; + while (fp >= 0 && hops++ < 20) { + var m = page.slots[fp + Squeak.Frame_method]; + var cl = page.slots[fp + Squeak.Frame_closure]; + desc.push("m" + (m.bytes ? m.bytes.length : "?") + (cl && !cl.isNil ? "b" : "")); + fp = page.slots[fp + Squeak.Frame_savedFp]; + } + if (fp < 0) desc.push("BASE:" + (page.baseCallerCtx && !page.baseCallerCtx.isNil ? "ctx" : "nil")); + } else { + var ctx = vm.activeContext, hops = 0; + while (!ctx.isNil && hops++ < 20) { + var m2 = ctx.pointers[Squeak.Context_method]; + var cl2 = ctx.pointers[Squeak.Context_closure]; + desc.push("m" + (m2.bytes ? m2.bytes.length : (vm.isSmallInt(m2) ? "INT" : "?")) + (cl2 && !cl2.isNil ? "b" : "")); + ctx = ctx.pointers[Squeak.Context_sender]; + } + } + return desc.join("<"); + }; + var origENM7 = vm.executeNewMethod; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + var r = origENM7.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + if (vm.sendCount >= z7From && newMethod._traceId === z7id) + console.error("ACT s=" + vm.sendCount + " chain: " + chainDesc()); + return r; + }; + } + if (process.env.ZDBG6) { + var origTT = vm.primHandler.transferTo; + vm.primHandler.transferTo = function(newProc) { + console.error("TT s=" + vm.sendCount); + return origTT.call(this, newProc); + }; + } + if (process.env.ZDBG5) { + // historial de activaciones de contexto explícitas (process switch / value de + // contextos dormidos) en ambos modos + var origNAC = vm.newActiveContext; + vm.newActiveContext = function(newContext) { + var m = newContext.pointers[Squeak.Context_method]; + console.error("NAC s=" + vm.sendCount + + " mbytes=" + (m && m.bytes ? m.bytes.length : "int?") + + " senderNil=" + (newContext.pointers[Squeak.Context_sender].isNil === true) + + (vm.useStackZone ? " frame=" + (newContext.frame != null) : "")); + return origNAC.call(vm, newContext); + }; + } + if (process.env.ZDBG4) { + // atrapar escrituras de campos de contexts en modo contexts: + // via bytecode (storeInstVar) y via primitivos (objectAtPut/storeStackp) + var ctxClass = vm.specialObjects[Squeak.splOb_ClassMethodContext]; + var origSIV = vm.storeInstVar; + var sivCount = 0; + vm.storeInstVar = function(index, value) { + if (++sivCount <= 3) console.error("SIV-ALIVE #" + sivCount + " s=" + vm.sendCount + " rcls=" + this.getClass(this.receiver).className()); + if (this.receiver.sqClass === ctxClass) + console.error("CTXSTORE s=" + vm.sendCount + " idx=" + index + " val=" + (value && value.isNil ? "nil" : typeof value)); + return origSIV.call(this, index, value); + }; + var origOAP = vm.primHandler.objectAtPut; + vm.primHandler.objectAtPut = function(a, b, c) { + var rcvr = this.stackNonInteger(2); + if (rcvr.sqClass === ctxClass) + console.error("CTXATPUT s=" + vm.sendCount + " idx=" + this.stackPos32BitInt(1)); + return origOAP.call(this, a, b, c); + }; + var origSSP = vm.primHandler.primitiveStoreStackp; + vm.primHandler.primitiveStoreStackp = function(argCount) { + console.error("CTXSTACKP s=" + vm.sendCount); + return origSSP.call(this, argCount); + }; + } + if (process.env.ZDBG3) { + // dump de cadenas en la ventana: frames (fp chain) vs contexts (sender chain) + var z3From = parseInt(process.env.ZDBG3_FROM || "0"), z3To = parseInt(process.env.ZDBG3_TO || "99999999"); + var origDR = vm.doReturn; + vm.doReturn = function(returnValue, targetContext) { + if (vm.sendCount >= z3From && vm.sendCount <= z3To) { + var desc = []; + if (vm.useStackZone) { + var page = vm.zonePage, fp = vm.fp, hops = 0; + while (fp >= 0 && hops++ < 12) { + var m = page.slots[fp + Squeak.Frame_method]; + var cl = page.slots[fp + Squeak.Frame_closure]; + desc.push(fp + ":m" + (m.bytes ? m.bytes.length : "?") + (cl && !cl.isNil ? "[blk]" : "")); + fp = page.slots[fp + Squeak.Frame_savedFp]; + } + if (fp < 0) desc.push("base->" + (page.baseCallerCtx && !page.baseCallerCtx.isNil ? "ctx" : "nil")); + } else { + var ctx = vm.activeContext, hops = 0; + while (!ctx.isNil && hops++ < 12) { + var m2 = ctx.pointers[Squeak.Context_method]; + var cl2 = ctx.pointers[Squeak.Context_closure]; + desc.push("m" + (m2.bytes ? m2.bytes.length : "?") + (cl2 && !cl2.isNil ? "[blk]" : "")); + ctx = ctx.pointers[Squeak.Context_sender]; + } + } + console.error("DR s=" + vm.sendCount + " pc=" + vm.pc + " chain: " + desc.join(" <- ")); + } + return origDR.call(vm, returnValue, targetContext); + }; + } + if (process.env.ZDBG) { + var zFrom = parseInt(process.env.ZDBG_FROM || "0"), zTo = parseInt(process.env.ZDBG_TO || "99999999"); + var origANC = vm.primHandler.activateNewClosureMethod; + vm.primHandler.activateNewClosureMethod = function(blockClosure, argCount) { + if (vm.sendCount >= zFrom && vm.sendCount <= zTo) { + var outer = blockClosure.pointers[Squeak.Closure_outerContext]; + var m = outer.frame != null ? outer.frame.page.slots[outer.frame.fp + Squeak.Frame_method] + : outer.pointers[Squeak.Context_method]; + console.error("ANC s=" + vm.sendCount + " startpc=" + blockClosure.pointers[Squeak.Closure_startpc] + + " mlits=" + (m.pointers ? m.pointers.length : "?") + " mbytes=" + (m.bytes ? m.bytes.length : "?") + + " outerMarried=" + (outer.frame != null) + " isNilM=" + (m.isNil === true)); + } + var r = origANC.call(this, blockClosure, argCount); + if (vm.sendCount >= zFrom && vm.sendCount <= zTo && !vm._zdbgArmed) { + vm._zdbgArmed = 60; + var origIO = vm.interpretOne.bind(vm); + vm.interpretOne = function(singleStep) { + if (vm._zdbgArmed-- > 0) + console.error("BC pc=" + vm.pc + " byte=" + vm.method.bytes[vm.pc] + + " mbytes=" + vm.method.bytes.length + " sp=" + vm.sp + " fp=" + vm.fp); + return origIO(singleStep); + }; + } + return r; + }; + } + var slices = 0, idleStreak = 0, stopReason = "maxSends"; + var logLines = logPath ? [] : null; + if (mode !== "bench") { + // Muestreo en checkpoints fijos de sendCount: independiente de la + // representación (contexts/frames) Y de la cadencia de interrupciones + // (que difiere con/sin jit). Es la señal que entra al hash. + var traceIdOf = function(m) { + if (m._traceId === undefined) { + var fpv = m.bytes ? m.bytes.length : 0; + if (m.bytes) for (var bi = 0; bi < Math.min(m.bytes.length, 16); bi++) + fpv = ((fpv * 31) + m.bytes[bi]) | 0; + m._traceId = fpv; + } + return m._traceId; + }; + // leaf-sends: mismo muestreo que executeNewMethod (sc = sendCount pre-incremento) + vm.jit2HookFires = 0; + vm.jit2LeafHook = function(method, sc, rcvr, sel) { + vm.jit2HookFires++; + if (sc < maxSends && (sc & 4095) === 0) { + mix(sc); + mix(traceIdOf(method)); + mix(vm.pc); + if (logLines) logLines.push("s=" + sc + " h=" + traceIdOf(method) + " pc=" + vm.pc); + } + if (logLines && sc >= logFrom && sc <= logTo) + logLines.push("S=" + sc + " h=" + traceIdOf(method) + " pc=" + vm.pc + " args=? prim=0" + + " cm=" + (vm.method ? traceIdOf(vm.method) : "?") + + " rcls=" + vm.getClass(rcvr).className() + " sel=" + (sel && sel.bytesAsString ? sel.bytesAsString() : "?") + " [leaf]"); + }; + var origENM = vm.executeNewMethod; + vm.executeNewMethod = function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + // método identificado por fingerprint de contenido (los oops temporales + // interleavan contexts y difieren entre representaciones; el identity + // hash de métodos de imagen V3 es 0) + if (newMethod._traceId === undefined) { + var fp = newMethod.bytes ? newMethod.bytes.length : 0; + if (newMethod.bytes) for (var bi = 0; bi < Math.min(newMethod.bytes.length, 16); bi++) + fp = ((fp * 31) + newMethod.bytes[bi]) | 0; + newMethod._traceId = fp; + } + if (vm.sendCount < maxSends && (vm.sendCount & 4095) === 0) { + mix(vm.sendCount); + mix(newMethod._traceId); + mix(vm.pc); + if (logLines) logLines.push("s=" + vm.sendCount + " h=" + newMethod._traceId + " pc=" + vm.pc + " vms=" + virtualMs + " alloc=" + (vm.image.newSpaceCount + vm.image.allocationCount)); + } + if (logLines && vm.sendCount >= logFrom && vm.sendCount <= logTo) { + if (vm.method && vm.method._traceId === undefined) { + var fp2 = vm.method.bytes ? vm.method.bytes.length : 0; + if (vm.method.bytes) for (var bj = 0; bj < Math.min(vm.method.bytes.length, 16); bj++) + fp2 = ((fp2 * 31) + vm.method.bytes[bj]) | 0; + vm.method._traceId = fp2; + } + logLines.push("S=" + vm.sendCount + " h=" + newMethod._traceId + " pc=" + vm.pc + " args=" + argumentCount + " prim=" + primitiveIndex + + " cm=" + (vm.method ? vm.method._traceId : "?") + + " rcls=" + vm.getClass(newRcvr).className() + " sel=" + (optSel && optSel.bytesAsString ? optSel.bytesAsString() : "?")); + } + return origENM.call(vm, newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel); + }; + } + var noop = function() {}; + var wallStart = process.hrtime.bigint(); + clockRunning = true; + mainLoop(); + async function mainLoop() { + try { + var frozenYields = 0; + while (vm.sendCount < maxSends) { + if (display.quitFlag) { stopReason = "quit"; break; } + if (vm.frozen) { + // ceder el event loop para que el unfreeze diferido corra + if (++frozenYields > 1000000) { stopReason = "frozen-livelock"; break; } + await new Promise(function(r) { setImmediate(r); }); + continue; + } + var result = vm.interpret(5, noop); // thenDo: freeze necesita continueFunc + if (result === "frozen") continue; + slices++; + // el hash se alimenta solo de los checkpoints por sendCount (arriba); + // los límites de slice dependen de la cadencia de interrupciones, + // que varía entre modos (jit/no-jit) sin implicar divergencia semántica + if (result === "sleep") { + // todos los procesos esperan sin timer: nada más va a pasar + if (++idleStreak >= 3) { stopReason = "idle"; break; } + warpClock(vm, 10); + } else if (typeof result === "number" && result > 1) { + // todos esperan hasta un timer futuro: saltar la espera + warpClock(vm, result); + idleStreak = 0; + } else if (result === "break") { + stopReason = "breakpoint"; break; + } else { + idleStreak = 0; + } + } + } catch (e) { + stopReason = "error: " + e.message; + if (process.env.DIFFTRACE_DEBUG) console.error(e.stack); + } + var wallMs = Number(process.hrtime.bigint() - wallStart) / 1e6; + if (logLines) fs.writeFileSync(logPath, logLines.join("\n") + "\n"); + + function warpClock(vm, ms) { + if (mode === "bench") { + // adelantar el reloj del VM (startupTime) para no esperar de verdad + var now = vm.primHandler.millisecondClockValue(); + vm.primHandler.millisecondClockValueSet(now + ms); + } else { + virtualMs += ms; + } + } + + var report = { + image: path.relative(repoRoot, imagePath), + maxSends: maxSends, + sendCount: vm.sendCount, + slices: slices, + stopReason: stopReason, + hash: hash.toString(16), + virtualMs: virtualMs, + }; + + if (mode === "bench") { + console.log("bench: " + vm.sendCount + " sends en " + wallMs.toFixed(0) + " ms (" + + (vm.sendCount / (wallMs / 1000) / 1e6).toFixed(2) + "M sends/s), stop: " + stopReason); + if (vm.useStackZone) { + console.log("leaf calls=" + vm.nLeafCalls + " deopts=" + vm.nLeafDeopts + + " (de " + vm.sendCount + " sends)"); + var live = 0, maxSlots = 0; + for (var i = 0; i < vm.zonePages.length; i++) { + if (vm.zonePages[i].live) live++; + if (vm.zonePages[i].slots.length > maxSlots) maxSlots = vm.zonePages[i].slots.length; + } + var vacias = 0; + for (var i = 0; i < vm.zonePages.length; i++) + if (vm.zonePages[i].live && vm.zonePages[i].fp < 0) vacias++; + console.log("pages live vacías (fp<0): " + vacias + " flushPage=" + (vm.nFlushPage || 0)); + console.log("zona: pages=" + vm.zonePages.length + " live=" + live + " maxSlots=" + maxSlots + + " married=" + (vm.nMarriedContexts || 0) + " flushAll=" + (vm.nFlushAll || 0) + + " byClosure=" + (vm.nMarryClosure || 0) + " byThisCtx=" + (vm.nMarryThisCtx || 0) + + " bySenderFill=" + (vm.nMarrySenderFill || 0)); + } + return; + } + + if (vm.compiler && vm.compiler.okCount !== undefined) + console.log("jit2: ok=" + vm.compiler.okCount + " bail=" + vm.compiler.bailCount + + " leaves=" + (vm.compiler.leafCount || 0) + + " leafCalls=" + vm.nLeafCalls + " deopts=" + vm.nLeafDeopts); + if (vm.useStackZone) + console.log("married=" + (vm.nMarriedContexts||0) + " byClosure=" + (vm.nMarryClosure||0) + + " byThisCtx=" + (vm.nMarryThisCtx||0) + " bySenderFill=" + (vm.nMarrySenderFill||0)); + if (vm.nFreezeSim) console.log("freezes simulados: " + vm.nFreezeSim + " singleSteps: " + (vm.nSingleStep || 0)); + console.log("trace: sends=" + report.sendCount + " slices=" + report.slices + + " stop=" + report.stopReason + " hash=" + report.hash + " virtualMs=" + report.virtualMs + + " (wall " + wallMs.toFixed(0) + " ms)"); + + if (mode === "golden") { + fs.writeFileSync(goldenPath, JSON.stringify(report, null, 2) + "\n"); + console.log("golden grabado en " + path.relative(repoRoot, goldenPath)); + } else { + if (!fs.existsSync(goldenPath)) { + console.error("no hay golden.json — corré primero con --golden"); + process.exit(2); + } + var golden = JSON.parse(fs.readFileSync(goldenPath, "utf8")); + // slices/virtualMs/sendCount-final quedan fuera de la comparación: + // dependen de la cadencia de interrupciones y la granularidad del slice, + // no de la semántica + var keys = ["image", "maxSends", "stopReason", "hash"]; + var diffs = keys.filter(function(k) { return String(golden[k]) !== String(report[k]); }); + if (diffs.length === 0) { + console.log("OK: traza idéntica al golden"); + } else { + diffs.forEach(function(k) { + console.error("DIVERGE " + k + ": golden=" + golden[k] + " actual=" + report[k]); + }); + process.exit(1); + } + } + } // fin mainLoop +}); diff --git a/perf/spike/as/frames.ts b/perf/spike/as/frames.ts new file mode 100644 index 00000000..39ae6bce --- /dev/null +++ b/perf/spike/as/frames.ts @@ -0,0 +1,273 @@ +// V4: frames planos + intérprete de bytecodes en WASM (espejo de V3 en variants.js). +// Diferencias deliberadas con el spike anterior (que perdió 2.2x): +// - load/store crudos sobre memoria lineal, sin objetos Int32Array +// (sin bounds checks ni indirección dataStart) +// - sin recursión del host: un solo loop de dispatch, frames en nuestra memoria +// - valores taggeados i32: smallint = (v<<1)|1, oop = dirección par + +// Mapa de memoria (offsets en bytes; --initialMemory 2 páginas = 128KB) +const METHOD_BASE: i32 = 4096; // bytecodes del método fib (u8) +const CACHE_BASE: i32 = 8192; // cache de métodos: 512 × [check:i32, method:i32] +const RCVR_OOP: i32 = 16384; // objeto receiver: [classId:i32]; oop = dirección +const ZONE_BASE: i32 = 65536; // stack zone + +const SEL_FIB: i32 = 7; +const CLASS_SMALLINT: i32 = 99; +const CLASS_FIB: i32 = 5; +const METH_FIB: i32 = 1; +const CACHE_MASK: i32 = 511; +const HASCTX_BIT: i32 = 1 << 17; + +// offsets en bytes de los slots del frame +const F_SAVEDFP: i32 = 0; +const F_SAVEDPC: i32 = 4; +const F_METHOD: i32 = 8; +const F_FLAGS: i32 = 12; +const F_CTX: i32 = 16; +const F_FIXED: i32 = 20; + +let sends: i32 = 0; + +export function init(): void { + // fib: n<2 ifTrue:[^n] ifFalse:[^(self fib: n-1) + (self fib: n-2)] + // opcodes: 0 PUSH_ARG0, 1 PUSH_C1, 2 PUSH_C2, 3 PUSH_SELF, 4 SUB, 5 LT, + // 6 JMPF , 7 SEND_FIB, 8 ADD, 9 RET + const b: i32 = METHOD_BASE; + store(b + 0, 0); // PUSH_ARG0 + store(b + 1, 2); // PUSH_C2 + store(b + 2, 5); // LT + store(b + 3, 6); // JMPF + store(b + 4, 7); // target: pc 7 + store(b + 5, 0); // PUSH_ARG0 + store(b + 6, 9); // RET + store(b + 7, 3); // PUSH_SELF + store(b + 8, 0); // PUSH_ARG0 + store(b + 9, 1); // PUSH_C1 + store(b + 10, 4); // SUB + store(b + 11, 7); // SEND_FIB + store(b + 12, 3); // PUSH_SELF + store(b + 13, 0); // PUSH_ARG0 + store(b + 14, 2); // PUSH_C2 + store(b + 15, 4); // SUB + store(b + 16, 7); // SEND_FIB + store(b + 17, 8); // ADD + store(b + 18, 9); // RET + store(RCVR_OOP, CLASS_FIB); + memory.fill(CACHE_BASE, 0, 4096); +} + +export function run(n: i32): i32 { + sends = 0; + let icc: i32 = 1000; + // frame base: rcvr @ ZONE_BASE, arg @ +4, frame @ +8 + store(ZONE_BASE, RCVR_OOP); + store(ZONE_BASE + 4, (n << 1) | 1); + const bfp: i32 = ZONE_BASE + 8; + store(bfp + F_SAVEDFP, 0); + store(bfp + F_SAVEDPC, 0); + store(bfp + F_METHOD, METH_FIB); + store(bfp + F_FLAGS, 1); + store(bfp + F_CTX, 0); + let fp: i32 = bfp; + let sp: i32 = bfp + F_FIXED - 4; + let pc: i32 = METHOD_BASE; + while (true) { + const op: i32 = load(pc); + pc++; + switch (op) { + case 0: { // PUSH_ARG0 + sp += 4; store(sp, load(fp - 4)); break; + } + case 1: { sp += 4; store(sp, 3); break; } // 1 taggeado + case 2: { sp += 4; store(sp, 5); break; } // 2 taggeado + case 3: { // PUSH_SELF + sp += 4; store(sp, load(fp - 8)); break; + } + case 4: { // SUB taggeado: (a-1)-(b-1)+1 = a-b+1 + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a - b2 + 1); + else unreachable(); + break; + } + case 5: { // LT (comparación taggeada es monótona) + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a < b2 ? 3 : 1); + else unreachable(); + break; + } + case 6: { // JMPF + const t: i32 = load(pc); pc++; + const c: i32 = load(sp); sp -= 4; + if (c == 1) pc = METHOD_BASE + t; + break; + } + case 7: { // SEND_FIB + sends++; + const numArgs: i32 = 1; + const rcvr: i32 = load(sp - (numArgs << 2)); + const classId: i32 = (rcvr & 1) != 0 ? CLASS_SMALLINT : load(rcvr); + const k: i32 = ((classId ^ (SEL_FIB * 31)) & CACHE_MASK) << 3; + const check: i32 = (classId << 8) | SEL_FIB; + let methodId: i32; + if (load(CACHE_BASE + k) == check) { + methodId = load(CACHE_BASE + k + 4); + } else { + store(CACHE_BASE + k, check); + store(CACHE_BASE + k + 4, METH_FIB); + methodId = METH_FIB; + } + const nfp: i32 = sp + 4; + store(nfp + F_SAVEDFP, fp); + store(nfp + F_SAVEDPC, pc); + store(nfp + F_METHOD, methodId); + store(nfp + F_FLAGS, numArgs); + store(nfp + F_CTX, 0); + const numTemps: i32 = 0; // nil-fill presente, 0 iteraciones en fib + for (let i: i32 = 0; i < numTemps; i++) store(nfp + F_FIXED + (i << 2), 0); + fp = nfp; + sp = nfp + F_FIXED - 4; + pc = METHOD_BASE; // methodStart(methodId); un solo método en el bench + icc--; if (icc <= 0) icc = 1000; + break; + } + case 8: { // ADD taggeado: a+b-1 + const b2: i32 = load(sp); sp -= 4; const a: i32 = load(sp); + if ((a & b2 & 1) != 0) store(sp, a + b2 - 1); + else unreachable(); + break; + } + case 9: { // RET + const rv: i32 = load(sp); + const flags: i32 = load(fp + F_FLAGS); + if ((flags & HASCTX_BIT) != 0) unreachable(); // widowCold: no pasa en el bench + const sfp: i32 = load(fp + F_SAVEDFP); + if (sfp == 0) return rv >> 1; + const numArgs: i32 = flags & 0xFFFF; + const rs: i32 = fp - 4 - (numArgs << 2); + store(rs, rv); + sp = rs; + pc = load(fp + F_SAVEDPC); + fp = sfp; + break; + } + default: unreachable(); + } + } + return 0; // inalcanzable +} + +export function getSends(): i32 { + return sends; +} + +// --------------------------------------------------------------------------- +// V5: frames + "métodos compilados" en WASM + trampolín (estimador de fase 2: +// codegen WASM por método). Misma estructura que V2 en JS, pero sobre memoria +// lineal. El dispatch del trampolín usa switch sobre methodId (aprox. de +// call_indirect; subestima levemente su costo). +// --------------------------------------------------------------------------- +let g_fp: i32 = 0; +let g_sp: i32 = 0; +let g_pc: i32 = 0; +let g_method: i32 = 0; +let g_running: bool = false; +let g_result: i32 = 0; +let g_icc: i32 = 1000; + +function send5(numArgs: i32): void { + sends++; + const rcvr: i32 = load(g_sp - (numArgs << 2)); + const classId: i32 = (rcvr & 1) != 0 ? CLASS_SMALLINT : load(rcvr); + const k: i32 = ((classId ^ (SEL_FIB * 31)) & CACHE_MASK) << 3; + const check: i32 = (classId << 8) | SEL_FIB; + let methodId: i32; + if (load(CACHE_BASE + k) == check) { + methodId = load(CACHE_BASE + k + 4); + } else { + store(CACHE_BASE + k, check); + store(CACHE_BASE + k + 4, METH_FIB); + methodId = METH_FIB; + } + const nfp: i32 = g_sp + 4; + store(nfp + F_SAVEDFP, g_fp); + store(nfp + F_SAVEDPC, g_pc); + store(nfp + F_METHOD, methodId); + store(nfp + F_FLAGS, numArgs); + store(nfp + F_CTX, 0); + g_fp = nfp; + g_sp = nfp + F_FIXED - 4; + g_method = methodId; + g_pc = 0; + g_icc--; if (g_icc <= 0) g_icc = 1000; +} + +function ret5(rv: i32): void { + const flags: i32 = load(g_fp + F_FLAGS); + if ((flags & HASCTX_BIT) != 0) unreachable(); + const sfp: i32 = load(g_fp + F_SAVEDFP); + if (sfp == 0) { g_running = false; g_result = rv; return; } + const numArgs: i32 = flags & 0xFFFF; + const rs: i32 = g_fp - 4 - (numArgs << 2); + store(rs, rv); + g_sp = rs; + g_pc = load(g_fp + F_SAVEDPC); + g_method = load(sfp + F_METHOD); + g_fp = sfp; +} + +function fibCompiled5(): void { + const fp: i32 = g_fp; + while (true) { + switch (g_pc) { + case 0: { + const n: i32 = load(fp - 4); + // n < 2 con n taggeado: tagged(2) = 5, comparación monótona + if ((n & 1) != 0 && n < 5) { ret5(n); return; } + g_sp += 4; store(g_sp, load(fp - 8)); // self + if ((n & 1) != 0) { g_sp += 4; store(g_sp, n - 2); } // n-1 taggeado + else unreachable(); + g_pc = 1; send5(1); return; + } + case 1: { + g_sp += 4; store(g_sp, load(fp - 8)); // self + const n: i32 = load(fp - 4); + if ((n & 1) != 0) { g_sp += 4; store(g_sp, n - 4); } // n-2 taggeado + else unreachable(); + g_pc = 2; send5(1); return; + } + case 2: { + const b: i32 = load(g_sp); const a: i32 = load(g_sp - 4); + if ((a & b & 1) != 0) { g_sp -= 4; store(g_sp, a + b - 1); } + else unreachable(); + ret5(load(g_sp)); + return; + } + default: unreachable(); + } + } +} + +export function run5(n: i32): i32 { + sends = 0; + g_icc = 1000; + store(ZONE_BASE, RCVR_OOP); + store(ZONE_BASE + 4, (n << 1) | 1); + const bfp: i32 = ZONE_BASE + 8; + store(bfp + F_SAVEDFP, 0); + store(bfp + F_SAVEDPC, 0); + store(bfp + F_METHOD, METH_FIB); + store(bfp + F_FLAGS, 1); + store(bfp + F_CTX, 0); + g_fp = bfp; + g_sp = bfp + F_FIXED - 4; + g_pc = 0; + g_method = METH_FIB; + g_running = true; + while (g_running) { + switch (g_method) { + case METH_FIB: fibCompiled5(); break; + default: unreachable(); + } + } + return g_result >> 1; +} diff --git a/perf/spike/bench2.js b/perf/spike/bench2.js new file mode 100644 index 00000000..5acdf4ba --- /dev/null +++ b/perf/spike/bench2.js @@ -0,0 +1,68 @@ +"use strict"; +// Bench fase 0 del diseño stack-zone: 5 variantes, mismo fib(32) por sends. +const fs = require("fs"); +const path = require("path"); +const mimic = require("./js/mimic.js"); // V0: contexts + recursión JS (spike anterior) +const { makeContextVM, makeFrameTrampolineVM, makeFrameInterpVM } = require("./js/variants.js"); + +const N = 32, EXPECT_RESULT = 2178309, EXPECT_SENDS = 7049155; +const WARMUP = 3, RUNS = 7; + +async function loadWasm() { + const bytes = fs.readFileSync(path.join(__dirname, "as/frames.wasm")); + const { instance } = await WebAssembly.instantiate(bytes, { + env: { abort: () => { throw new Error("wasm abort"); } }, + }); + instance.exports.init(); + return instance.exports; +} + +function bench(name, runFn) { + let best = Infinity, r; + for (let i = 0; i < WARMUP; i++) r = runFn(N); + for (let i = 0; i < RUNS; i++) { + r = runFn(N); + if (r.ns < best) best = r.ns; + } + if (r.result !== EXPECT_RESULT) throw Error(name + ": resultado " + r.result); + // V0 cuenta la activación raíz como send; V1-V4 no (la arman a mano) + if (r.sends !== EXPECT_SENDS && r.sends !== EXPECT_SENDS - 1) + throw Error(name + ": sends " + r.sends); + return { name, ms: best / 1e6, sendsPerSec: EXPECT_SENDS / (best / 1e9) }; +} + +(async () => { + const wasm = await loadWasm(); + const wasmRun = (n) => { + const t0 = process.hrtime.bigint(); + const result = wasm.run(n); + const t1 = process.hrtime.bigint(); + return { result, sends: wasm.getSends(), ns: Number(t1 - t0) }; + }; + + const rows = [ + bench("V0 contexts + recursión JS (spike anterior, referencia)", mimic), + bench("V1 contexts + trampolín (≈ jit.js actual)", makeContextVM()), + bench("V2 frames + trampolín (jit.js con stack zone, JS puro)", makeFrameTrampolineVM()), + bench("V3 frames + intérprete bytecodes JS", makeFrameInterpVM()), + bench("V4 frames + intérprete bytecodes WASM", wasmRun), + bench("V5 frames + métodos compilados WASM (estimador fase 2)", (n) => { + const t0 = process.hrtime.bigint(); + const result = wasm.run5(n); + const t1 = process.hrtime.bigint(); + return { result, sends: wasm.getSends(), ns: Number(t1 - t0) }; + }), + ]; + + const base = rows[1].ms; // V1 = arquitectura actual + console.log(`fib(${N}) = ${EXPECT_RESULT}, ${(EXPECT_SENDS / 1e6).toFixed(2)}M sends, best of ${RUNS} (tras ${WARMUP} warmup)\n`); + for (const r of rows) { + const ratio = base / r.ms; + console.log( + r.ms.toFixed(1).padStart(7) + " ms " + + (r.sendsPerSec / 1e6).toFixed(1).padStart(6) + "M sends/s " + + (ratio >= 1 ? (ratio.toFixed(2) + "x más rápido que V1") : ((r.ms / base).toFixed(2) + "x más lento que V1")).padStart(24) + " " + + r.name + ); + } +})(); diff --git a/perf/spike/js/mimic.js b/perf/spike/js/mimic.js new file mode 100644 index 00000000..b6c03eb7 --- /dev/null +++ b/perf/spike/js/mimic.js @@ -0,0 +1,90 @@ +"use strict"; +// Synthetic microbenchmark mimicking the shape of SqueakJS's hot loop as measured +// in the CPU profile: findSelectorInClass (linear-probe method dict lookup), +// executeNewMethod (context allocation from a recycle pool, arg copy, temp nil-fill), +// doReturn (context recycling). Not a real Smalltalk interpreter -- just the same +// data-structure shapes and control flow, run recursively via a fib(n) send tree +// so we get millions of sends of a realistic shape. + +function makeMethodDict(entries) { + var size = 1; + while (size < entries.length * 4) size *= 2; // keep load factor low, like a real MethodDict + var selectors = new Array(size).fill(-1); // -1 == nil + var methods = new Array(size).fill(-1); + var mask = size - 1; + for (var i = 0; i < entries.length; i++) { + var sel = entries[i][0], meth = entries[i][1]; + var idx = sel & mask; + while (selectors[idx] !== -1) idx = (idx + 1) & mask; + selectors[idx] = sel; + methods[idx] = meth; + } + return { selectors: selectors, methods: methods, mask: mask }; +} + +function lookupSelectorInDict(mDict, selectorHash) { + var idx = selectorHash & mDict.mask; + while (true) { + if (mDict.selectors[idx] === selectorHash) return mDict.methods[idx]; + if (mDict.selectors[idx] === -1) return -1; // nil + idx = (idx + 1) & mDict.mask; + } +} + +var SEL_FIB = 1; +var METH_FIB = 100; +var fibClassDict = makeMethodDict([[SEL_FIB, METH_FIB]]); + +var FRAME_SIZE = 8; // sender + arg + temps, like Squeak's Context_tempFrameStart layout +var freeList = []; +var sendCount = 0; + +function allocateContext() { + var ctx = freeList.pop(); + if (!ctx) ctx = { pointers: new Array(FRAME_SIZE) }; + return ctx; +} +function recycleContext(ctx) { + freeList.push(ctx); +} + +function executeNewMethod(senderCtx, arg) { + sendCount++; + var found = lookupSelectorInDict(fibClassDict, SEL_FIB); // findSelectorInClass equivalent + if (found !== METH_FIB) throw new Error("lookup failed"); + var ctx = allocateContext(); + ctx.sender = senderCtx; + ctx.pointers[0] = arg; + ctx.pointers[1] = 0; + for (var i = 2; i < FRAME_SIZE; i++) ctx.pointers[i] = null; // fill temps with nil + return ctx; +} + +function doReturn(ctx) { + var sender = ctx.sender; + recycleContext(ctx); + return sender; +} + +function fibSend(senderCtx, n) { + var ctx = executeNewMethod(senderCtx, n); + var result; + if (n < 2) { + result = n; + } else { + var a = fibSend(ctx, n - 1); + var b = fibSend(ctx, n - 2); + result = a + b; + } + doReturn(ctx); + return result; +} + +module.exports = function run(n) { + sendCount = 0; + freeList.length = 0; + var t0 = process.hrtime.bigint(); + var result = fibSend(null, n); + var t1 = process.hrtime.bigint(); + return { result: result, sends: sendCount, ns: Number(t1 - t0) }; +}; diff --git a/perf/spike/js/variants.js b/perf/spike/js/variants.js new file mode 100644 index 00000000..f6b887a7 --- /dev/null +++ b/perf/spike/js/variants.js @@ -0,0 +1,350 @@ +"use strict"; +// Spike fase 0 del diseño stack-zone (ver ../stack-zone-design.md). +// Tres variantes JS del mismo benchmark fib-por-sends, para aislar dos efectos: +// V1: contexts heap + trampolín ≈ arquitectura actual (jit.js + vm.interpreter.js) +// V2: frames planos + trampolín ≈ jit.js con stack zone (camino JS incremental) +// V3: frames planos + intérprete de bytecodes ≈ intérprete fase-1 (espejo del WASM V4) +// Todas hacen el mismo trabajo por send: probe de cache de métodos, chequeo de clase +// del receiver, contador de interrupciones, y (V2/V3) chequeo de hasContext al retornar. + +const SEL_FIB = 7; +const CLASS_SMALLINT = 99; +const CLASS_FIB = 5; +const METH_FIB = 1; +const CACHE_MASK = 511; + +function makeCache() { + // direct-mapped: [checkWord, methodId] × 512 + const cache = new Int32Array((CACHE_MASK + 1) * 2); + return cache; +} + +function cacheLookup(cache, classId, selId) { + const k = ((classId ^ (selId * 31)) & CACHE_MASK) << 1; + const check = (classId << 8) | selId; + if (cache[k] === check) return cache[k + 1]; + // slow path: "búsqueda" y fill (en el bench siempre encuentra METH_FIB) + cache[k] = check; + cache[k + 1] = METH_FIB; + return METH_FIB; +} + +// --------------------------------------------------------------------------- +// V1: contexts heap + trampolín (modela la arquitectura actual) +// Context = objeto con array `pointers`: [sender, pc, sp, method, receiver, arg0, ...opstack] +// Reciclado por free-list como allocateOrRecycleContext/recycleIfPossible. +// --------------------------------------------------------------------------- +const CTX_SENDER = 0, CTX_PC = 1, CTX_SP = 2, CTX_METHOD = 3, CTX_RCVR = 4, CTX_TEMP0 = 5; +const CTX_SIZE = 24; + +function makeContextVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const vm = { + activeCtx: null, pc: 0, sp: 0, methodId: 0, + running: false, result: 0, sends: 0, icc: 1000, + freeCtx: null, + }; + + function allocCtx() { + let ctx = vm.freeCtx; + if (ctx) { vm.freeCtx = ctx.pointers[CTX_SENDER]; return ctx; } + return { pointers: new Array(CTX_SIZE).fill(null) }; + } + + function send(selId, numArgs) { + vm.sends++; + const stack = vm.activeCtx.pointers; + const rcvr = stack[vm.sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, selId); + // storeContextRegisters: guardar pc/sp en el context actual + stack[CTX_PC] = vm.pc; + stack[CTX_SP] = vm.sp - numArgs - 1; // sp tras popear rcvr+args + const ctx = allocCtx(); + const p = ctx.pointers; + p[CTX_SENDER] = vm.activeCtx; + p[CTX_METHOD] = methodId; + // copiar receiver + args al nuevo context (como executeNewMethod:1046) + p[CTX_RCVR] = rcvr; + for (let i = 0; i < numArgs; i++) p[CTX_TEMP0 + i] = stack[vm.sp - numArgs + 1 + i]; + // nil-fill de temps (fib: 0 temps extra — loop presente igual) + const numTemps = 0; + for (let i = 0; i < numTemps; i++) p[CTX_TEMP0 + numArgs + i] = null; + vm.activeCtx = ctx; + vm.methodId = methodId; + vm.pc = 0; + vm.sp = CTX_TEMP0 + numArgs + numTemps - 1; // tope del opstack (vacío) + if (--vm.icc <= 0) vm.icc = 1000; + } + + function doReturn(rv) { + const ctx = vm.activeCtx; + const sender = ctx.pointers[CTX_SENDER]; + // escaneo mínimo de unwind (target === sender, un compare como hoy) + // nil sender/ip + reciclar (doReturn:1103-1107) + ctx.pointers[CTX_SENDER] = vm.freeCtx; // reuso del slot como link de free-list + ctx.pointers[CTX_PC] = null; + vm.freeCtx = ctx; + if (sender === null) { vm.running = false; vm.result = rv; return; } + // fetchContextRegisters + const sp = sender.pointers[CTX_SP]; + vm.activeCtx = sender; + vm.methodId = sender.pointers[CTX_METHOD]; + vm.pc = sender.pointers[CTX_PC]; + vm.sp = sp + 1; + sender.pointers[sp + 1] = rv; // push del resultado + } + + // fib "compilado" al estilo jit.js: switch sobre pc, return al trampolín en cada send + function fibCompiled(vm) { + const stack = vm.activeCtx.pointers; + while (true) switch (vm.pc) { + case 0: { + const n = stack[CTX_TEMP0]; + if (typeof n === "number" && n < 2) { doReturn(n); return; } + stack[++vm.sp] = stack[CTX_RCVR]; + const a = n; // inline #- con chequeo como generateNumericOp + if (typeof a === "number") stack[++vm.sp] = a - 1; else throw Error("fail"); + vm.pc = 1; send(SEL_FIB, 1); return; + } + case 1: { + stack[++vm.sp] = stack[CTX_RCVR]; + const a = stack[CTX_TEMP0]; + if (typeof a === "number") stack[++vm.sp] = a - 2; else throw Error("fail"); + vm.pc = 2; send(SEL_FIB, 1); return; + } + case 2: { + const b = stack[vm.sp], a = stack[vm.sp - 1]; + if (typeof a === "number" && typeof b === "number") stack[--vm.sp] = a + b; + else throw Error("fail"); + doReturn(stack[vm.sp]); + return; + } + } + } + + const methodFns = [null, fibCompiled]; + + return function run(n) { + vm.freeCtx = null; vm.sends = 0; vm.icc = 1000; + const base = { pointers: new Array(CTX_SIZE).fill(null) }; + base.pointers[CTX_SENDER] = null; + base.pointers[CTX_METHOD] = METH_FIB; + base.pointers[CTX_RCVR] = rcvrObj; + base.pointers[CTX_TEMP0] = n; + vm.activeCtx = base; vm.methodId = METH_FIB; vm.pc = 0; + vm.sp = CTX_TEMP0; // opstack vacío arriba de arg0 + vm.running = true; + const t0 = process.hrtime.bigint(); + while (vm.running) methodFns[vm.methodId](vm); + const t1 = process.hrtime.bigint(); + return { result: vm.result, sends: vm.sends, ns: Number(t1 - t0) }; + }; +} + +// --------------------------------------------------------------------------- +// Maquinaria de frames compartida por V2/V3 (layout del diseño): +// rcvr @ fp-1-numArgs, args hasta fp-1, +// fp+0 savedFp | fp+1 savedPc | fp+2 method | fp+3 flags | fp+4 ctxOop | temps | opstack +// --------------------------------------------------------------------------- +const F_SAVEDFP = 0, F_SAVEDPC = 1, F_METHOD = 2, F_FLAGS = 3, F_CTX = 4, F_FIXED = 5; +const HASCTX_BIT = 1 << 17; + +// --------------------------------------------------------------------------- +// V2: frames planos + trampolín (jit.js con stack zone, sin WASM) +// --------------------------------------------------------------------------- +function makeFrameTrampolineVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const zone = new Array(1 << 16).fill(0); + const vm = { + fp: 0, sp: 0, pc: 0, methodId: 0, + running: false, result: 0, sends: 0, icc: 1000, + }; + + function send(selId, numArgs) { + vm.sends++; + const rcvr = zone[vm.sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, selId); + const nfp = vm.sp + 1; + zone[nfp + F_SAVEDFP] = vm.fp; + zone[nfp + F_SAVEDPC] = vm.pc; + zone[nfp + F_METHOD] = methodId; + zone[nfp + F_FLAGS] = numArgs; + zone[nfp + F_CTX] = 0; + const numTemps = 0; // nil-fill presente + for (let i = 0; i < numTemps; i++) zone[nfp + F_FIXED + i] = null; + vm.fp = nfp; + vm.sp = nfp + F_FIXED - 1 + numTemps; + vm.methodId = methodId; + vm.pc = 0; + if (--vm.icc <= 0) vm.icc = 1000; + } + + function doReturn(rv) { + const fp = vm.fp; + const flags = zone[fp + F_FLAGS]; + if (flags & HASCTX_BIT) widowCold(fp); + const sfp = zone[fp + F_SAVEDFP]; + if (sfp === 0) { vm.running = false; vm.result = rv; return; } + const numArgs = flags & 0xFFFF; + const rs = fp - 1 - numArgs; + zone[rs] = rv; + vm.sp = rs; + vm.pc = zone[fp + F_SAVEDPC]; + vm.methodId = zone[sfp + F_METHOD]; + vm.fp = sfp; + } + + function widowCold() { throw Error("no contexts casados en este bench"); } + + function fibCompiled(vm) { + const fp = vm.fp; + while (true) switch (vm.pc) { + case 0: { + const n = zone[fp - 1]; + if (typeof n === "number" && n < 2) { doReturn(n); return; } + zone[++vm.sp] = zone[fp - 2]; + if (typeof n === "number") zone[++vm.sp] = n - 1; else throw Error("fail"); + vm.pc = 1; send(SEL_FIB, 1); return; + } + case 1: { + zone[++vm.sp] = zone[fp - 2]; + const a = zone[fp - 1]; + if (typeof a === "number") zone[++vm.sp] = a - 2; else throw Error("fail"); + vm.pc = 2; send(SEL_FIB, 1); return; + } + case 2: { + const b = zone[vm.sp], a = zone[vm.sp - 1]; + if (typeof a === "number" && typeof b === "number") zone[--vm.sp] = a + b; + else throw Error("fail"); + doReturn(zone[vm.sp]); + return; + } + } + } + + const methodFns = [null, fibCompiled]; + + return function run(n) { + vm.sends = 0; vm.icc = 1000; + // frame base: rcvr@0, arg@1, frame@2 + zone[0] = rcvrObj; zone[1] = n; + zone[2 + F_SAVEDFP] = 0; zone[2 + F_SAVEDPC] = 0; + zone[2 + F_METHOD] = METH_FIB; zone[2 + F_FLAGS] = 1; zone[2 + F_CTX] = 0; + vm.fp = 2; vm.sp = 2 + F_FIXED - 1; vm.pc = 0; vm.methodId = METH_FIB; + vm.running = true; + const t0 = process.hrtime.bigint(); + while (vm.running) methodFns[vm.methodId](vm); + const t1 = process.hrtime.bigint(); + return { result: vm.result, sends: vm.sends, ns: Number(t1 - t0) }; + }; +} + +// --------------------------------------------------------------------------- +// V3: frames planos + intérprete de bytecodes en JS (espejo exacto del WASM V4) +// --------------------------------------------------------------------------- +// mini-ISA: ver diseño; fib: n < 2 ifTrue:[^n] ifFalse:[^(self fib: n-1) + (self fib: n-2)] +const OP_PUSH_ARG0 = 0, OP_PUSH_C1 = 1, OP_PUSH_C2 = 2, OP_PUSH_SELF = 3, + OP_SUB = 4, OP_LT = 5, OP_JMPF = 6, OP_SEND_FIB = 7, OP_ADD = 8, OP_RET = 9; +const FIB_BYTES = new Uint8Array([ + OP_PUSH_ARG0, OP_PUSH_C2, OP_LT, OP_JMPF, 7, // pc 0-4: n<2? si no → pc7 + OP_PUSH_ARG0, OP_RET, // pc 5-6: ^n + OP_PUSH_SELF, OP_PUSH_ARG0, OP_PUSH_C1, OP_SUB, OP_SEND_FIB, // pc 7-11 + OP_PUSH_SELF, OP_PUSH_ARG0, OP_PUSH_C2, OP_SUB, OP_SEND_FIB, // pc 12-16 + OP_ADD, OP_RET, // pc 17-18 +]); + +function makeFrameInterpVM() { + const cache = makeCache(); + const rcvrObj = { classId: CLASS_FIB }; + const zone = new Array(1 << 16).fill(0); + const methodBytes = [null, FIB_BYTES]; + let sends = 0; + + function run(n) { + sends = 0; + let icc = 1000; + zone[0] = rcvrObj; zone[1] = n; + zone[2 + F_SAVEDFP] = 0; zone[2 + F_SAVEDPC] = 0; + zone[2 + F_METHOD] = METH_FIB; zone[2 + F_FLAGS] = 1; zone[2 + F_CTX] = 0; + let fp = 2, sp = 2 + F_FIXED - 1, pc = 0; + let bytes = FIB_BYTES; + const t0 = process.hrtime.bigint(); + let result; + loop: while (true) { + switch (bytes[pc++]) { + case OP_PUSH_ARG0: zone[++sp] = zone[fp - 1]; break; + case OP_PUSH_C1: zone[++sp] = 1; break; + case OP_PUSH_C2: zone[++sp] = 2; break; + case OP_PUSH_SELF: zone[++sp] = zone[fp - 2]; break; + case OP_SUB: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a - b; + else throw Error("fail"); + break; + } + case OP_LT: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a < b ? 1 : 0; + else throw Error("fail"); + break; + } + case OP_JMPF: { + const target = bytes[pc++]; + if (zone[sp--] === 0) pc = target; + break; + } + case OP_SEND_FIB: { + sends++; + const numArgs = 1; + const rcvr = zone[sp - numArgs]; + const classId = typeof rcvr === "number" ? CLASS_SMALLINT : rcvr.classId; + const methodId = cacheLookup(cache, classId, SEL_FIB); + const nfp = sp + 1; + zone[nfp + F_SAVEDFP] = fp; + zone[nfp + F_SAVEDPC] = pc; + zone[nfp + F_METHOD] = methodId; + zone[nfp + F_FLAGS] = numArgs; + zone[nfp + F_CTX] = 0; + const numTemps = 0; + for (let i = 0; i < numTemps; i++) zone[nfp + F_FIXED + i] = null; + fp = nfp; sp = nfp + F_FIXED - 1 + numTemps; pc = 0; + bytes = methodBytes[methodId]; + if (--icc <= 0) icc = 1000; + break; + } + case OP_ADD: { + const b = zone[sp--], a = zone[sp]; + if (typeof a === "number" && typeof b === "number") zone[sp] = a + b; + else throw Error("fail"); + break; + } + case OP_RET: { + const rv = zone[sp]; + const flags = zone[fp + F_FLAGS]; + if (flags & HASCTX_BIT) throw Error("no contexts casados en este bench"); + const sfp = zone[fp + F_SAVEDFP]; + if (sfp === 0) { result = rv; break loop; } + const numArgs = flags & 0xFFFF; + const rs = fp - 1 - numArgs; + zone[rs] = rv; + sp = rs; + pc = zone[fp + F_SAVEDPC]; + const callerMethod = zone[sfp + F_METHOD]; + bytes = methodBytes[callerMethod]; + fp = sfp; + break; + } + default: throw Error("bytecode ilegal"); + } + } + const t1 = process.hrtime.bigint(); + return { result: result, sends: sends, ns: Number(t1 - t0) }; + } + return run; +} + +module.exports = { makeContextVM, makeFrameTrampolineVM, makeFrameInterpVM }; diff --git a/perf/stack-zone-design.md b/perf/stack-zone-design.md new file mode 100644 index 00000000..69c8ce26 --- /dev/null +++ b/perf/stack-zone-design.md @@ -0,0 +1,192 @@ +# Diseño: Stack Zone con reificación perezosa de Contexts para SqueakJS + +Estado: borrador v1 (2026-07-05). Basado en el concepto de "stack zone" de Cog +(OpenSmalltalk VM), rediseñado contra la semántica concreta de SqueakJS +(`vm.interpreter.js`, `vm.primitives.js`, `jit.js`) — no es una traducción del C. + +## Qué ataca (datos del perfil de 2026-07-05, Cuis abriendo el class browser) + +Sobre CPU activa (~12s de 23.3s grabados): + +- Ciclo de vida de Context (executeNewMethod + doReturn + allocateOrRecycleContext): **14.0%** +- Dispatch de primitivos (doNamedPrimitive + doPrimitive): 7.0% +- Lookup de método (send; findMethodCacheEntry inlineado): 4.4% — la cache ya funciona, + `lookupSelectorInDict` real midió 0.26ms/23s. NO atacar esto. +- Loop base + helpers de stack: 3.1% + +El costo del Context no es la alocación (ya hay free-list: `freeContexts`/ +`freeLargeContexts`) sino el protocolo completo por send/return: +`storeContextRegisters` (encode pc/sp), copia de receiver+args (`arrayCopy`), +nil-fill de temps, 4+ stores de punteros, y a la vuelta `fetchContextRegisters` +(decode), escaneo de unwind, nil de sender/ip, manejo de free-list. + +## Idea central + +Los Contexts existen como objetos heap **solo cuando algo los observa**. El caso +común (send → return sin que nadie capture el contexto) corre sobre **frames +planos en un stack manual** (memoria lineal WASM o Array JS), con push/pop de +~6 slots. El Context objeto se crea perezosamente ("marriage" en la jerga Cog) +solo en los puntos enumerados abajo, que son exactamente los puntos donde el +código actual de SqueakJS necesita `activeContext` como objeto. + +Esto es independiente del lenguaje de implementación: funciona en JS puro +(reemplazo incremental dentro de jit.js/vm.interpreter.js) o en WASM (rewrite +del núcleo). El spike (fase 0) mide ambos para decidir. + +## Layout del frame (stack crece hacia arriba; slots de 1 palabra) + +``` + ... stack de operandos del caller ... + receiver @ fp - 1 - numArgs ← lo pusheó el caller; NO se copia + arg1 .. argN @ fp - numArgs .. fp-1 + savedFp @ fp + 0 (fp del caller; 0 = frame base de página) + savedPc @ fp + 1 (pc de reanudación del caller, entero crudo, SIN encode Squeak) + method @ fp + 2 (oop/id del método del callee) + flags @ fp + 3 (numArgs | isBlock<<16 | hasContext<<17) + contextOop @ fp + 4 (contexto casado, 0 si no hay) + temp1 .. tempK @ fp + 5 .. (nil-fill igual que hoy) + ... stack de operandos del callee ... +``` + +- **Receiver y args no se copian**: quedan donde el caller los pusheó. Elimina el + `arrayCopy` de executeNewMethod:1046. +- **savedPc es un int crudo**: elimina `encodeSqueakPC`/`decodeSqueakPC` del camino + caliente. La forma "Squeak-visible" (offset por literales, 1-based) se computa + solo al casar el frame. +- Return: `result → slot del receiver; sp := ese slot; fp := savedFp; + method := mem[savedFp+2]; pc := savedPc`. ~5 loads/stores + 1 branch (hasContext). + +## Reglas de reificación (marriage) — verificadas contra el código actual + +Un frame se casa con un Context heap SOLO en: + +1. **`thisContext`** — bytecode 0x89 (V3) / 0x52 (Sista) → `exportThisContext()` + (vm.interpreter.js:1315). +2. **Creación de closure** — el closure guarda `outerContext` + (vm.interpreter.js:850, 868, 912). Todo `[...]` casa su frame home. + (Es también por qué hoy ponen `reclaimableContextCount = 0`.) +3. **Cambio de proceso** — `transferTo` guarda `activeContext` en + `oldProc.suspendedContext` (vm.primitives.js:1773). Casa **solo el frame tope**; + el resto de la cadena queda como frames vivos en la zona. El sender de un + contexto casado se resuelve perezosamente (leerlo casa al frame caller). +4. **Evicción de página** (zona llena) → "flush": cada frame de la página víctima + se serializa a un Context heap completo (pc/sp encodeados, temps+stack copiados, + senders enlazados como oops). La página se libera. +5. **Acceso reflectivo** — primitivos que reciben un Context casado + (instVarAt:, el debugger, etc.) leen/escriben "a través" al frame vivo. + Chokepoint único en el VM; el código Smalltalk no puede saltearlo porque el + acceso a slots de otro contexto siempre pasa por primitivos o por el VM. +6. **Unwind que entrega el contexto a Smalltalk** — `aboutToReturnThrough:` / + `cannotReturn:` (vm.interpreter.js:1120-1132). El escaneo de unwind en sí + (recorrer senders chequeando `isUnwindMarked`) camina frames sin reificar. + +Ciclo de vida del contexto casado: +- **Casado**: objeto Context proxy; sus campos se leen/escriben vía el frame. +- **Viudo**: el frame retornó → el contexto queda muerto (pc=nil, sender=nil), + igual que hoy hace doReturn:1103-1104. +- **Flusheado**: el frame se serializó completo al contexto (evicción o snapshot + de imagen); el contexto pasa a ser un Context heap normal y reanudable, como + los de hoy. + +## Protocolo de marriage v1 para JS: "snapshot + sync-at-send + flush-on-write" + +Hallazgo clave (2026-07-05, mapeo de vm.primitives.js): los prims de unwind +195-199 **retornan false** en SqueakJS — la búsqueda de handlers y el unwinding +corren como código Smalltalk normal caminando contexts vía sends (`ctx sender`, +`ctx method`, ...). Consecuencia: **todo acceso Smalltalk a campos de un context +pasa por (a) un send con el context como receiver, o (b) primitivos reflectivos** +(instVarAt: 73/74, storeStackp 76, clone 148, ...). No hay tercer camino. +Eso permite un protocolo sin proxies: + +1. El slot fp+4 del frame guarda su Context casado (o null); el Context casado + lleva `ctx.frame` → frame vivo. +2. **marry(frame)** = crear el Context y llenarlo con un snapshot completo + (receiver/method/closure/pc/stackp/temps/stack). El frame sigue corriendo; + el snapshot puede quedar stale. +3. **Puntos de sync** (refrescar snapshot, baratos y acotados): + - `send()`: si el receiver es un context casado-vivo → sync. Un chequeo + `rcvr.frame !== undefined` por send cubre TODOS los reads a nivel bytecode. + - Primitivos reflectivos (objectAt/objectAtPut/storeStackp/clone/pointsTo): + chokepoint único, sync antes de leer. + - El campo `sender` de un snapshot se llena casando al frame caller + (lazy, un marry por nivel) → caminar la cadena desde `thisContext` casa + de a uno, sin flush masivo. +4. **Stores** a campos de un context casado-vivo (bytecode store-inst-var o + reflectivo) → flush de toda la cadena activa a contexts reales + continuar + con makeBaseFrame(top). Solo pasa en debugger/unwind — acá frío y correcto + le gana a rápido. +5. Return de un frame casado → **widow**: sender=nil, pc=nil, clear `.frame` + (semántica idéntica al doReturn actual). +6. `thisContext` → marry del frame activo solamente (la cadena se casa lazy + vía 3c a medida que se camina). +7. Cambio de proceso: `transferTo` casa solo el tope; las páginas quedan vivas; + resume con frame vivo = saltar a (page, fp); si no, makeBaseFrame. +8. Snapshot de imagen: flush total. GC lógico de vm.image.js: los slots de + páginas vivas entran como roots; `become:` recorre también la zona. +9. Los contexts NUNCA se ejecutan directamente: el intérprete solo corre + frames; makeBaseFrame/flush son las únicas transiciones. (Un solo engine.) + +Nota sobre closures: con closures reales (Cuis/Squeak modernos) los valores +copiados viven EN el closure y las temps compartidas van en arrays de +indirección — el `outerContext` se usa solo para identidad (target de +non-local return), home receiver/method y debugging. Un snapshot stale de +temps en el context casado es aceptable en v1 (el debugger podría mostrar +temps viejas del home mientras el frame corre; Cog lo hace exacto con +read-through — mejora futura). + +Prerequisito ya implementado: todo acceso al stack de operandos pasa por +`vm.stack` (commit "route all operand-stack access through vm.stack"), así el +modo frames solo re-apunta `vm.stack` a la página activa sin tocar los +bytecodes ni los templates de push/pop del jit. + +## Páginas y multiproceso + +- Zona = N páginas (arranque: 64 × 8KB). Una cadena de frames de un proceso puede + cruzar páginas: el frame base de cada página (savedFp=0) guarda el oop del + contexto caller (casado al cruzar). +- Overflow al pushear → página nueva (evict LRU si no hay libre). +- Return desde frame base → el caller es un Context heap → "makeBaseFrame": + inflarlo a frame en una página (camino frío; es el inverso del flush). +- `transferTo`: casar tope + guardar en suspendedContext. Al reanudar un proceso + cuyo contexto casado todavía tiene frame vivo en la zona → cambiar fp/sp/pc y + listo (cambio de proceso casi gratis, las páginas no se tocan). +- Snapshot de imagen: flush total de la zona (igual que Cog). + +## Chequeo de interrupciones + +Idéntico a hoy: `interruptCheckCounter` con feedback (vm.interpreter.js:674-726), +decrementado por send y por salto hacia atrás. `checkForInterrupts` solo necesita +contexto objeto si va a cambiar de proceso → regla 3. + +## Qué NO cambia + +- Formato de objetos (Spur / pre-Spur), imagen, GC del heap de objetos. +- Cache de métodos global (ya rinde: 0.26ms de lookup real en 23s). +- Semántica observable: mismos Contexts vistos desde Smalltalk (debugger, + excepciones, `ensure:`, procesos), solo que materializados a demanda. + +## Fases + +- **Fase 0 (spike, hoy)**: fib sintético sobre la maquinaria de frames, 5 variantes + (ver bench2). Decide: ¿el algoritmo gana en JS? ¿WASM suma sobre eso? +- **Fase 1**: según fase 0 — + (a) si JS+frames gana fuerte: integrarlo en SqueakJS real (jit.js genera código + que opera sobre frames; vm.interpreter.js mantiene el camino Context como + fallback/oracle). Incremental, sin WASM. + (b) si WASM suma claramente: intérprete WASM sobre heap Spur en memoria lineal; + todo-o-nada por sesión, validado contra el VM JS como oráculo con imágenes de test. +- **Fase 2 (solo si fase 1 = WASM)**: "Cogit-para-WASM" — generar módulos WASM + por método en runtime (JS puede compilar/instanciar bytes WASM al vuelo, + compartiendo memoria y funcref table). Elimina el costo de dispatch de bytecodes + que el intérprete WASM reintroduce (hoy jit.js ya lo elimina en JS — un + intérprete WASM fase-1 parte con esa desventaja; medir). + +## Riesgos principales + +- Read-through de contextos casados: un acceso que no pase por el chokepoint = + bug silencioso. Mitigación: build con aserciones + suite SUnit de tests/ como + oráculo diferencial. +- El intérprete WASM reintroduce dispatch por bytecode que jit.js ya había + eliminado — la fase 1(b) puede perder en código numérico aunque gane en sends. + Por eso el spike mide dispatch por separado (V2 vs V3). +- Empezar solo con Spur 32-bit (cuis.image); pre-Spur y 64-bit después. diff --git a/squeak.js b/squeak.js index fa586fc6..2e0fbd0a 100644 --- a/squeak.js +++ b/squeak.js @@ -38,6 +38,8 @@ import "./vm.instruction.stream.sista.js"; import "./vm.instruction.printer.js"; import "./vm.primitives.js"; import "./jit.js"; +import "./vm.stackzone.js"; +import "./jit2.js"; import "./vm.audio.browser.js"; import "./vm.display.js"; import "./vm.display.browser.js"; diff --git a/squeak_headless.js b/squeak_headless.js index b7945c8e..03937427 100644 --- a/squeak_headless.js +++ b/squeak_headless.js @@ -38,6 +38,8 @@ import "./vm.instruction.stream.sista.js"; import "./vm.instruction.printer.js"; import "./vm.primitives.js"; import "./jit.js"; +import "./vm.stackzone.js"; +import "./jit2.js"; import "./vm.display.js"; import "./vm.display.headless.js"; // use headless display to prevent image crashing/becoming unresponsive import "./vm.input.js"; diff --git a/squeak_node.js b/squeak_node.js index 505373c2..2cd2a0e3 100644 --- a/squeak_node.js +++ b/squeak_node.js @@ -40,6 +40,14 @@ var ignoreQuit = processArgs[0] === "-ignoreQuit"; if (ignoreQuit) { processArgs = processArgs.slice(1); } +var stackZone = processArgs[0] === "-stackZone"; +if (stackZone) { + processArgs = processArgs.slice(1); +} +var jit2 = processArgs[0] === "-jit2"; +if (jit2) { + processArgs = processArgs.slice(1); +} var fullName = processArgs[0]; if (!fullName) { console.error("No image name specified."); @@ -90,6 +98,8 @@ require("./vm.instruction.stream.sista.js"); require("./vm.instruction.printer.js"); require("./vm.primitives.js"); require("./jit.js"); +require("./vm.stackzone.js"); +require("./jit2.js"); require("./vm.display.js"); require("./vm.display.headless.js"); // use headless display to prevent image crashing/becoming unresponsive require("./vm.input.js"); @@ -133,7 +143,7 @@ fs.readFile(root + imageName + ".image", function(error, data) { // Create fake display and create interpreter var display = { vmOptions: [ "-vm-display-null", "-nodisplay" ] }; - var vm = new Squeak.Interpreter(image, display); + var vm = new Squeak.Interpreter(image, display, stackZone ? { stackZone: true, jit2: jit2 } : {}); function run() { try { vm.interpret(200, function runAgain(ms) { diff --git a/vm.image.js b/vm.image.js index ea9c04e1..982e5e75 100644 --- a/vm.image.js +++ b/vm.image.js @@ -760,11 +760,25 @@ Object.subclass('Squeak.Image', }, instantiateClass: function(aClass, indexableSize, filler) { var newObject = new (aClass.classInstProto()); // Squeak.Object - var hash = this.registerObject(newObject); + var hash = aClass === this.contextClass() ? this.registerContext(newObject) + : this.registerObject(newObject); newObject.initInstanceOf(aClass, indexableSize, hash, filler); this.hasNewInstances[aClass.oop] = true; // need GC to find all instances return newObject; }, + contextClass: function() { + return this.specialObjectsArray.pointers[Squeak.splOb_ClassMethodContext]; + }, + registerContext: function(obj) { + // like registerObject, but contexts draw their identity hashes from a + // separate stream: their allocation pattern is an implementation detail + // (recycling, or stack-zone frames that materialize contexts lazily), + // and interleaving them into the main stream would make every other + // object's identity hash depend on it + obj.oop = -(++this.newSpaceCount); + this.lastContextHash = (13849 + (27181 * (this.lastContextHash || 999))) & 0xFFFFFFFF; + return this.lastContextHash & 0xFFF; + }, clone: function(object) { var newObject = new (object.sqClass.classInstProto()); // Squeak.Object var hash = this.registerObject(newObject); diff --git a/vm.interpreter.js b/vm.interpreter.js index 58f14ad4..fc9e692b 100644 --- a/vm.interpreter.js +++ b/vm.interpreter.js @@ -32,9 +32,16 @@ Object.subclass('Squeak.Interpreter', this.primHandler = new Squeak.Primitives(this, display); this.loadImageState(); this.initVMState(); + // presencia del flag alcanza: el launcher puede reescribir #stackZone + // como stackZone= (valor string vacío, falsy) + if (this.options.stackZone !== undefined && this.options.stackZone !== false + && this.enableStackZone) this.enableStackZone(); this.loadInitialContext(); this.hackImage(); this.initCompiler(); + if (this.useStackZone && this.options.jit2 !== undefined && this.options.jit2 !== false + && this.compiler && Squeak.Compiler2) + this.compiler = new Squeak.Compiler2(this); // stack-to-register jit (WIP, opt-in) console.log('squeak: ready'); }, loadImageState: function() { @@ -55,6 +62,20 @@ Object.subclass('Squeak.Interpreter', initVMState: function() { this.byteCodeCount = 0; this.sendCount = 0; + this.stack = null; // operand stack array (activeContext.pointers; a zone page once stack frames land) + this.temps = null; // temp base array (homeContext.pointers; a zone page once stack frames land) + this.tempOffset = Squeak.Context_tempFrameStart; // index of temp 0 in this.temps + // stack-zone state, declared here for stable object shape even when unused + this.jsDepth = 0; // profundidad de llamadas JS directas (modo frames) + this.LEAF_DEOPT_ = null; + this.jit2LeafHook = null; + this.nLeafCalls = 0; + this.nLeafDeopts = 0; + this.useStackZone = false; + this.zonePages = null; + this.zonePage = null; + this.fp = -1; + this.contextClass_ = null; // set by enableStackZone; gates married-context probes this.interruptCheckCounter = 0; this.interruptCheckCounterFeedBackReset = 1000; this.interruptChecksEveryNms = 3; @@ -231,7 +252,7 @@ Object.subclass('Squeak.Interpreter', // load temporary variable case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: case 0x18: case 0x19: case 0x1A: case 0x1B: case 0x1C: case 0x1D: case 0x1E: case 0x1F: - this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&0xF)]); return; + this.push(this.temps[this.tempOffset+(b&0xF)]); return; // loadLiteral case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: @@ -250,9 +271,9 @@ Object.subclass('Squeak.Interpreter', // storeAndPop rcvr, temp case 0x60: case 0x61: case 0x62: case 0x63: case 0x64: case 0x65: case 0x66: case 0x67: this.receiver.dirty = true; - this.receiver.pointers[b&7] = this.pop(); return; + this.storeInstVar(b&7, this.pop()); return; case 0x68: case 0x69: case 0x6A: case 0x6B: case 0x6C: case 0x6D: case 0x6E: case 0x6F: - this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&7)] = this.pop(); return; + this.temps[this.tempOffset+(b&7)] = this.pop(); return; // Quick push case 0x70: this.push(this.receiver); return; @@ -270,7 +291,7 @@ Object.subclass('Squeak.Interpreter', case 0x7A: this.doReturn(this.falseObj); return; case 0x7B: this.doReturn(this.nilObj); return; case 0x7C: this.doReturn(this.pop()); return; - case 0x7D: this.doReturn(this.pop(), this.activeContext.pointers[Squeak.BlockContext_caller]); return; // blockReturn + case 0x7D: this.doBlockReturn(this.pop()); return; // blockReturn case 0x7E: this.nono(); return; case 0x7F: this.nono(); return; // Sundry @@ -295,15 +316,15 @@ Object.subclass('Squeak.Interpreter', case 0x8B: this.callPrimBytecode(0x81); return; case 0x8C: b2 = this.nextByte(); // remote push from temp vector - this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()].pointers[b2]); + this.push(this.temps[this.tempOffset+this.nextByte()].pointers[b2]); return; case 0x8D: b2 = this.nextByte(); // remote store into temp vector - var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()]; + var vec = this.temps[this.tempOffset+this.nextByte()]; vec.pointers[b2] = this.top(); vec.dirty = true; return; case 0x8E: b2 = this.nextByte(); // remote store and pop into temp vector - var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()]; + var vec = this.temps[this.tempOffset+this.nextByte()]; vec.pointers[b2] = this.pop(); vec.dirty = true; return; @@ -410,9 +431,9 @@ Object.subclass('Squeak.Interpreter', // load temporary variable case 0x40: case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46: case 0x47: - this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&0x7)]); return; + this.push(this.temps[this.tempOffset+(b&0x7)]); return; case 0x48: case 0x49: case 0x4A: case 0x4B: - this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&0x3)+8]); return; + this.push(this.temps[this.tempOffset+(b&0x3)+8]); return; case 0x4C: this.push(this.receiver); return; case 0x4D: this.push(this.trueObj); return; @@ -433,10 +454,10 @@ Object.subclass('Squeak.Interpreter', case 0x5A: this.doReturn(this.falseObj); return; case 0x5B: this.doReturn(this.nilObj); return; case 0x5C: this.doReturn(this.pop()); return; - case 0x5D: this.doReturn(this.nilObj, this.activeContext.pointers[Squeak.BlockContext_caller]); return; // blockReturn nil + case 0x5D: this.doBlockReturn(this.nilObj); return; // blockReturn nil case 0x5E: if (extA == 0) { - this.doReturn(this.pop(), this.activeContext.pointers[Squeak.BlockContext_caller]); return; // blockReturn + this.doBlockReturn(this.pop()); return; // blockReturn } else { this.nono(); return; } @@ -507,9 +528,9 @@ Object.subclass('Squeak.Interpreter', // storeAndPop rcvr, temp case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF: this.receiver.dirty = true; - this.receiver.pointers[b&7] = this.pop(); return; + this.storeInstVar(b&7, this.pop()); return; case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7: - this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&7)] = this.pop(); return; + this.temps[this.tempOffset+(b&7)] = this.pop(); return; case 0xD8: this.pop(); return; // pop case 0xD9: this.nono(); return; // FIXME: Unconditional trap @@ -529,7 +550,7 @@ Object.subclass('Squeak.Interpreter', case 0xE4: b2 = this.nextByte(); this.push(this.method.methodGetLiteral(b2 + (extA << 8))); return; case 0xE5: - b2 = this.nextByte(); this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+b2]); return; + b2 = this.nextByte(); this.push(this.temps[this.tempOffset+b2]); return; case 0xE6: this.nono(); return; // unused case 0xE7: this.pushNewArray(this.nextByte()); return; // create new temp vector case 0xE8: b2 = this.nextByte(); this.push(b2 + (extB << 8)); return; // push SmallInteger @@ -558,7 +579,7 @@ Object.subclass('Squeak.Interpreter', this.jumpIfFalse(this.nextByte() + (extB << 8)); return; case 0xF0: // pop into receiver this.receiver.dirty = true; - this.receiver.pointers[this.nextByte() + (extA << 8)] = this.pop(); + this.storeInstVar(this.nextByte() + (extA << 8), this.pop()); return; case 0xF1: // pop into literal var assoc = this.method.methodGetLiteral(this.nextByte() + (extA << 8)); @@ -566,11 +587,11 @@ Object.subclass('Squeak.Interpreter', assoc.pointers[Squeak.Assn_value] = this.pop(); return; case 0xF2: // pop into temp - this.homeContext.pointers[Squeak.Context_tempFrameStart + this.nextByte()] = this.pop(); + this.temps[this.tempOffset + this.nextByte()] = this.pop(); return; case 0xF3: // store into receiver this.receiver.dirty = true; - this.receiver.pointers[this.nextByte() + (extA << 8)] = this.top(); + this.storeInstVar(this.nextByte() + (extA << 8), this.top()); return; case 0xF4: // store into literal var assoc = this.method.methodGetLiteral(this.nextByte() + (extA << 8)); @@ -578,7 +599,7 @@ Object.subclass('Squeak.Interpreter', assoc.pointers[Squeak.Assn_value] = this.top(); return; case 0xF5: // store into temp - this.homeContext.pointers[Squeak.Context_tempFrameStart + this.nextByte()] = this.top(); + this.temps[this.tempOffset + this.nextByte()] = this.top(); return; case 0xF6: case 0xF7: this.nono(); return; // unused @@ -588,15 +609,15 @@ Object.subclass('Squeak.Interpreter', case 0xF9: this.pushFullClosure(extA); return; case 0xFA: this.pushClosureCopyExtended(extA, extB); return; case 0xFB: b2 = this.nextByte(); // remote push from temp vector - this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()].pointers[b2]); + this.push(this.temps[this.tempOffset+this.nextByte()].pointers[b2]); return; case 0xFC: b2 = this.nextByte(); // remote store into temp vector - var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()]; + var vec = this.temps[this.tempOffset+this.nextByte()]; vec.pointers[b2] = this.top(); vec.dirty = true; return; case 0xFD: b2 = this.nextByte(); // remote store and pop into temp vector - var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+this.nextByte()]; + var vec = this.temps[this.tempOffset+this.nextByte()]; vec.pointers[b2] = this.pop(); vec.dirty = true; return; @@ -728,7 +749,7 @@ Object.subclass('Squeak.Interpreter', var lobits = nextByte & 63; switch (nextByte>>6) { case 0: this.push(this.receiver.pointers[lobits]);break; - case 1: this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+lobits]); break; + case 1: this.push(this.temps[this.tempOffset+lobits]); break; case 2: this.push(this.method.methodGetLiteral(lobits)); break; case 3: this.push(this.method.methodGetLiteral(lobits).pointers[Squeak.Assn_value]); break; } @@ -738,10 +759,10 @@ Object.subclass('Squeak.Interpreter', switch (nextByte>>6) { case 0: this.receiver.dirty = true; - this.receiver.pointers[lobits] = this.top(); + this.storeInstVar(lobits, this.top()); break; case 1: - this.homeContext.pointers[Squeak.Context_tempFrameStart+lobits] = this.top(); + this.temps[this.tempOffset+lobits] = this.top(); break; case 2: this.nono(); @@ -758,10 +779,10 @@ Object.subclass('Squeak.Interpreter', switch (nextByte>>6) { case 0: this.receiver.dirty = true; - this.receiver.pointers[lobits] = this.pop(); + this.storeInstVar(lobits, this.pop()); break; case 1: - this.homeContext.pointers[Squeak.Context_tempFrameStart+lobits] = this.pop(); + this.temps[this.tempOffset+lobits] = this.pop(); break; case 2: this.nono(); @@ -781,8 +802,8 @@ Object.subclass('Squeak.Interpreter', case 2: this.push(this.receiver.pointers[byte3]); break; case 3: this.push(this.method.methodGetLiteral(byte3)); break; case 4: this.push(this.method.methodGetLiteral(byte3).pointers[Squeak.Assn_value]); break; - case 5: this.receiver.dirty = true; this.receiver.pointers[byte3] = this.top(); break; - case 6: this.receiver.dirty = true; this.receiver.pointers[byte3] = this.pop(); break; + case 5: this.receiver.dirty = true; this.storeInstVar(byte3, this.top()); break; + case 6: this.receiver.dirty = true; this.storeInstVar(byte3, this.pop()); break; case 7: var assoc = this.method.methodGetLiteral(byte3); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.top(); break; @@ -847,7 +868,7 @@ Object.subclass('Squeak.Interpreter', blockSize = blockSizeHigh * 256 + this.nextByte(), initialPC = this.encodeSqueakPC(this.pc, this.method), closure = this.newClosure(numArgs, initialPC, numCopied); - closure.pointers[Squeak.Closure_outerContext] = this.activeContext; + closure.pointers[Squeak.Closure_outerContext] = this.activeContextObj(); this.reclaimableContextCount = 0; // The closure refers to thisContext so it can't be reclaimed if (numCopied > 0) { for (var i = 0; i < numCopied; i++) @@ -865,7 +886,7 @@ Object.subclass('Squeak.Interpreter', blockSize = byteB + (extB << 8), initialPC = this.encodeSqueakPC(this.pc, this.method), closure = this.newClosure(numArgs, initialPC, numCopied); - closure.pointers[Squeak.Closure_outerContext] = this.activeContext; + closure.pointers[Squeak.Closure_outerContext] = this.activeContextObj(); this.reclaimableContextCount = 0; // The closure refers to thisContext so it can't be reclaimed if (numCopied > 0) { for (var i = 0; i < numCopied; i++) @@ -884,7 +905,7 @@ Object.subclass('Squeak.Interpreter', if ((byteB >> 6 & 1) == 1) { context = this.vm.nilObj; } else { - context = this.activeContext; + context = this.activeContextObj(); } var compiledBlock = this.method.methodGetLiteral(literalIndex); var closure = this.newFullClosure(context, numCopied, compiledBlock); @@ -1023,7 +1044,7 @@ Object.subclass('Squeak.Interpreter', var indent = ' '; var ctx = this.activeContext; while (!ctx.isNil) { indent += '| '; ctx = ctx.pointers[Squeak.Context_sender]; } - var args = this.activeContext.pointers.slice(this.sp + 1 - argumentCount, this.sp + 1); + var args = this.stack.slice(this.sp + 1 - argumentCount, this.sp + 1); console.log(this.sendCount + indent + this.printMethod(newMethod, optClass, optSel, args)); } if (this.breakOnContextChanged) { @@ -1043,7 +1064,7 @@ Object.subclass('Squeak.Interpreter', newContext.pointers[Squeak.Context_sender] = this.activeContext; //Copy receiver and args to new context //Note this statement relies on the receiver slot being contiguous with args... - this.arrayCopy(this.activeContext.pointers, this.sp-argumentCount, newContext.pointers, Squeak.Context_tempFrameStart-1, argumentCount+1); + this.arrayCopy(this.stack, this.sp-argumentCount, newContext.pointers, Squeak.Context_tempFrameStart-1, argumentCount+1); //...and fill the remaining temps with nil this.arrayFill(newContext.pointers, Squeak.Context_tempFrameStart+argumentCount, Squeak.Context_tempFrameStart+tempCount, this.nilObj); this.popN(argumentCount+1); @@ -1057,6 +1078,9 @@ Object.subclass('Squeak.Interpreter', this.method = newMethod; this.pc = newPC; this.sp = newSP; + this.stack = newContext.pointers; + this.temps = newContext.pointers; // home === newContext for method activations + this.tempOffset = Squeak.Context_tempFrameStart; this.receiver = newContext.pointers[Squeak.Context_receiver]; if (this.receiver !== newRcvr) throw Error("receivers don't match"); @@ -1117,6 +1141,22 @@ Object.subclass('Squeak.Interpreter', this.breakNow(); } }, + doBlockReturn: function(returnValue) { + // return from block to its caller (stack-zone mode rebinds this) + this.doReturn(returnValue, this.activeContext.pointers[Squeak.BlockContext_caller]); + }, + activeContextObj: function() { + // the active context as an object (stack-zone mode rebinds this to marry the frame) + return this.activeContext; + }, + storeInstVar: function(index, value) { + // receiver inst-var store from a bytecode; in stack-zone mode a store + // into a married-live context must go through the write-through path + var rcvr = this.receiver; + if (rcvr.sqClass === this.contextClass_ && rcvr.frame != null) + return this.storeToMarriedContext(rcvr, index, value); + rcvr.pointers[index] = value; + }, aboutToReturnThrough: function(resultObj, aContext) { this.push(this.exportThisContext()); this.push(resultObj); @@ -1167,7 +1207,7 @@ Object.subclass('Squeak.Interpreter', //Bundle up receiver, args and selector as a messageObject var message = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassMessage], 0); var argArray = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassArray], argCount); - this.arrayCopy(this.activeContext.pointers, this.sp-argCount+1, argArray.pointers, 0, argCount); //copy args from stack + this.arrayCopy(this.stack, this.sp-argCount+1, argArray.pointers, 0, argCount); //copy args from stack message.pointers[Squeak.Message_selector] = selector; message.pointers[Squeak.Message_arguments] = argArray; if (message.pointers.length > Squeak.Message_lookupClass) //Early versions don't have lookupClass @@ -1183,7 +1223,7 @@ Object.subclass('Squeak.Interpreter', // selector has been found, rearrange stack if (entry.argCount !== trueArgCount) return false; - var stack = this.activeContext.pointers; // slide eveything down... + var stack = this.stack; // slide eveything down... var selectorIndex = this.sp - trueArgCount; this.arrayCopy(stack, selectorIndex+1, stack, selectorIndex, trueArgCount); this.sp--; // adjust sp accordingly @@ -1215,7 +1255,7 @@ Object.subclass('Squeak.Interpreter', // selector has been found, rearrange stack if (entry.argCount !== trueArgCount) return false; - var stack = this.activeContext.pointers; + var stack = this.stack; var selectorIndex = this.sp - (argCount - 1); stack[selectorIndex - 1] = rcvr; this.arrayCopy(args.pointers, 0, stack, selectorIndex, trueArgCount); @@ -1329,6 +1369,9 @@ Object.subclass('Squeak.Interpreter', this.method = meth; this.pc = this.decodeSqueakPC(ctxt.pointers[Squeak.Context_instructionPointer], meth); this.sp = this.decodeSqueakSP(ctxt.pointers[Squeak.Context_stackPointer]); + this.stack = ctxt.pointers; // operand stack array; sp indexes into it + this.temps = this.homeContext.pointers; // temp access: temps[tempOffset+n] + this.tempOffset = Squeak.Context_tempFrameStart; }, storeContextRegisters: function() { //Save pc, sp into activeContext object, prior to change of context @@ -1392,25 +1435,25 @@ Object.subclass('Squeak.Interpreter', 'stack access', { pop: function() { //Note leaves garbage above SP. Cleaned out by fullGC. - return this.activeContext.pointers[this.sp--]; + return this.stack[this.sp--]; }, popN: function(nToPop) { this.sp -= nToPop; }, push: function(object) { - this.activeContext.pointers[++this.sp] = object; + this.stack[++this.sp] = object; }, popNandPush: function(nToPop, object) { - this.activeContext.pointers[this.sp -= nToPop - 1] = object; + this.stack[this.sp -= nToPop - 1] = object; }, top: function() { - return this.activeContext.pointers[this.sp]; + return this.stack[this.sp]; }, stackTopPut: function(object) { - this.activeContext.pointers[this.sp] = object; + this.stack[this.sp] = object; }, stackValue: function(depthIntoStack) { - return this.activeContext.pointers[this.sp - depthIntoStack]; + return this.stack[this.sp - depthIntoStack]; }, stackInteger: function(depthIntoStack) { return this.checkSmallInt(this.stackValue(depthIntoStack)); @@ -1858,7 +1901,7 @@ Object.subclass('Squeak.Interpreter', console.log(this.printStack() + this.printActiveContext() + '\n\n' + this.printByteCodes(this.method, '   ', '=> ', this.pc), - this.activeContext.pointers.slice(0, this.sp + 1)); + this.stack.slice(0, this.sp + 1)); }, willSendOrReturn: function() { // Answer whether the next bytecode corresponds to a Smalltalk diff --git a/vm.object.js b/vm.object.js index add14611..cca04567 100644 --- a/vm.object.js +++ b/vm.object.js @@ -482,6 +482,10 @@ Object.subclass('Squeak.Object', classInstProto: function(className) { if (this.instProto) return this.instProto; var proto = this.defaultInst(); // in case below fails + // Nota de experimento (2026-07-11): una única shape plana para todos los + // objetos (en vez de constructores por clase) midió ~7% MÁS LENTO en el + // bench frames+jit2 — los maps por clase + ICs polimórficos de V8 ya + // rinden bien; no repetir sin nueva evidencia. try { if (!className) className = this.className(); var safeName = className.replace(/[^A-Za-z0-9]/g,'_'); @@ -490,7 +494,7 @@ Object.subclass('Squeak.Object', else if (safeName === "False") safeName = "false_"; else safeName = ((/^[AEIOU]/.test(safeName)) ? 'an' : 'a') + safeName; // fail okay if no eval() - proto = new Function("return function " + safeName + "() {};")(); + proto = new Function("return function " + safeName + "() { this.oop = 0; this.hash = 0; this.dirty = false; this.mark = false; this.nextObject = null; };")(); proto.prototype = this.defaultInst().prototype; } catch(e) {} Object.defineProperty(this, 'instProto', { value: proto }); diff --git a/vm.primitives.js b/vm.primitives.js index 5f0281fd..7fd7a3ed 100644 --- a/vm.primitives.js +++ b/vm.primitives.js @@ -1344,7 +1344,7 @@ Object.subclass('Squeak.Primitives', primIdx = this.stackInteger(1); if (!this.success) return false; var arraySize = argumentArray.pointersSize(), - cntxSize = this.vm.activeContext.pointersSize(); + cntxSize = this.vm.activeContextObj().pointersSize(); if (this.vm.sp + arraySize >= cntxSize) return false; // Pop primIndex and argArray, then push args in place... this.vm.popN(2); @@ -1365,7 +1365,7 @@ Object.subclass('Squeak.Primitives', primMethod = this.stackNonInteger(2); if (!this.success) return false; var arraySize = argumentArray.pointersSize(), - cntxSize = this.vm.activeContext.pointersSize(); + cntxSize = this.vm.activeContextObj().pointersSize(); if (this.vm.sp + arraySize >= cntxSize) return false; // Pop primIndex, rcvr, and argArray, then push new receiver and args in place... this.vm.popN(3); @@ -1470,6 +1470,11 @@ Object.subclass('Squeak.Primitives', if (argCount > 1) copyHash = this.stackBoolean(argCount-2); if (!this.success) return false; this.success = this.vm.image.bulkBecome(rcvr.pointers, arg.pointers, doBothWays, copyHash); + // become may have swapped the active context's pointers array + if (!this.vm.useStackZone) { + this.vm.stack = this.vm.activeContext.pointers; + this.vm.temps = this.vm.homeContext.pointers; + } return this.popNIfOK(argCount); }, doStringReplace: function() { @@ -1584,11 +1589,11 @@ Object.subclass('Squeak.Primitives', if (typeof blockArgCount !== "number") return false; if (blockArgCount != argCount) return false; if (!block.pointers[Squeak.BlockContext_caller].isNil) return false; - this.vm.arrayCopy(this.vm.activeContext.pointers, this.vm.sp-argCount+1, block.pointers, Squeak.Context_tempFrameStart, argCount); + this.vm.arrayCopy(this.vm.stack, this.vm.sp-argCount+1, block.pointers, Squeak.Context_tempFrameStart, argCount); var initialIP = block.pointers[Squeak.BlockContext_initialIP]; block.pointers[Squeak.Context_instructionPointer] = initialIP; block.pointers[Squeak.Context_stackPointer] = argCount; - block.pointers[Squeak.BlockContext_caller] = this.vm.activeContext; + block.pointers[Squeak.BlockContext_caller] = this.vm.activeContextObj(); this.vm.popN(argCount+1); this.vm.newActiveContext(block); if (this.vm.interruptCheckCounter-- <= 0) this.vm.checkForInterrupts(); @@ -1607,7 +1612,7 @@ Object.subclass('Squeak.Primitives', var initialIP = block.pointers[Squeak.BlockContext_initialIP]; block.pointers[Squeak.Context_instructionPointer] = initialIP; block.pointers[Squeak.Context_stackPointer] = blockArgCount; - block.pointers[Squeak.BlockContext_caller] = this.vm.activeContext; + block.pointers[Squeak.BlockContext_caller] = this.vm.activeContextObj(); this.vm.popN(argCount+1); this.vm.newActiveContext(block); if (this.vm.interruptCheckCounter-- <= 0) this.vm.checkForInterrupts(); @@ -1770,7 +1775,7 @@ Object.subclass('Squeak.Primitives', var oldProc = sched.pointers[Squeak.ProcSched_activeProcess]; sched.pointers[Squeak.ProcSched_activeProcess] = newProc; sched.dirty = true; - oldProc.pointers[Squeak.Proc_suspendedContext] = this.vm.activeContext; + oldProc.pointers[Squeak.Proc_suspendedContext] = this.vm.activeContextObj(); oldProc.dirty = true; this.vm.newActiveContext(newProc.pointers[Squeak.Proc_suspendedContext]); newProc.pointers[Squeak.Proc_suspendedContext] = this.vm.nilObj; @@ -2149,7 +2154,7 @@ Object.subclass('Squeak.Primitives', primitiveSnapshot: function(argCount) { this.vm.popNandPush(1, this.vm.trueObj); // put true on stack for saved snapshot this.vm.storeContextRegisters(); // store current state for snapshot - this.activeProcess().pointers[Squeak.Proc_suspendedContext] = this.vm.activeContext; // store initial context + this.activeProcess().pointers[Squeak.Proc_suspendedContext] = this.vm.activeContextObj(); // store initial context this.vm.image.fullGC("snapshot"); // before cleanup so traversal works var buffer = this.vm.image.writeToBuffer(); // Write snapshot if files are supported diff --git a/vm.stackzone.js b/vm.stackzone.js new file mode 100644 index 00000000..08141ec4 --- /dev/null +++ b/vm.stackzone.js @@ -0,0 +1,817 @@ +"use strict"; +/* + * Stack zone: flat activation frames with lazy context reification. + * See perf/stack-zone-design.md for the full design and the v1 marriage + * protocol (snapshot + sync-at-send + flush-on-write). + * + * Enabled per interpreter instance via options.stackZone. When enabled, the + * interpreter NEVER executes on heap contexts: sends push flat frames into + * growable zone pages (one page per process fragment), and heap Context + * objects exist only as dormant snapshots. makeBaseFrame (context -> frame) + * and flush (frame -> context) are the only transitions. + * + * Frame layout (slot indices relative to fp, stack grows up): + * receiver+args pushed by caller end at fp-1 (rcvr at fp-1-numArgs) + * fp+0 savedFp caller's fp in same page, -1 if base frame of page + * fp+1 savedPc caller's resumption pc (raw zero-based int, no encoding) + * fp+2 method CompiledMethod + * fp+3 flags numArgs (16 bits) | hasContext<<17 + * fp+4 context married Context or null + * fp+5 closure BlockClosure/FullBlockClosure or nilObj + * fp+6 receiver + * fp+7 temps (args copied here first, then locals), then operand stack + */ + +Object.extend(Squeak, { + Frame_savedFp: 0, + Frame_savedPc: 1, + Frame_method: 2, + Frame_flags: 3, + Frame_context: 4, + Frame_closure: 5, + Frame_receiver: 6, + Frame_firstTemp: 7, + Frame_hasContext: 1 << 17, +}); + +Object.extend(Squeak.Interpreter.prototype, +'stack zone', { + enableStackZone: function() { + this.useStackZone = true; + this.zonePages = []; + this.zonePage = null; + this.fp = -1; + // no hay getter mágico: activeContext queda null y los lectores legítimos + // usan activeContextObj() (un defineProperty con getter sobre la instancia + // degrada TODOS los accesos a propiedades del vm — 10x+ en el camino caliente) + this.activeContext = null; + // cache para gatear los probes .frame: solo instancias de MethodContext + // pueden estar casadas; comparar la clase primero evita LoadICs + // megamórficos sobre receivers arbitrarios (medido: 24% del tiempo) + this.contextClass_ = this.specialObjects[Squeak.splOb_ClassMethodContext]; + this.smallIntClass_ = this.specialObjects[Squeak.splOb_ClassInteger]; + this.LEAF_DEOPT_ = Squeak.LEAF_DEOPT; + var vm = this; + // rebind execution methods to the frames variants + this.send = this.sendZ; + this.tryPrimitive = this.tryPrimitiveZ; + this.activeContextObj = this.activeContextObjZ; + this.sendSuperDirected = this.sendSuperDirectedZ; + this.executeNewMethod = this.executeNewMethodZ; + this.doReturn = this.doReturnZ; + this.doBlockReturn = this.doBlockReturnZ; + this.exportThisContext = this.exportThisContextZ; + this.newActiveContext = this.newActiveContextZ; + this.loadInitialContext = this.loadInitialContextZ; + this.storeContextRegisters = this.storeContextRegistersZ; + this.fetchContextRegisters = this.fetchContextRegistersZ; + this.allocateOrRecycleContext = this.allocateOrRecycleContextZ; + // primHandler: closure activation pushes frames; reflective prims sync/flush + var ph = this.primHandler; + ph.activateNewClosureMethod = ph.activateNewClosureMethodZ; + ph.activateNewFullClosure = ph.activateNewFullClosureZ; + var origObjectAt = ph.objectAt; + ph.objectAt = function(cameFromBytecode, convertChars, includeInstVars) { + var rcvr = this.stackNonInteger(1); + if (rcvr.sqClass === vm.contextClass_ && rcvr.frame != null) vm.syncMarriedContext(rcvr); + return origObjectAt.call(this, cameFromBytecode, convertChars, includeInstVars); + }; + var origObjectAtPut = ph.objectAtPut; + ph.objectAtPut = function(cameFromBytecode, convertChars, includeInstVars) { + var rcvr = this.stackNonInteger(2); + if (rcvr.sqClass === vm.contextClass_ && rcvr.frame != null) vm.flushAllAndContinue(); + return origObjectAtPut.call(this, cameFromBytecode, convertChars, includeInstVars); + }; + var origStoreStackp = ph.primitiveStoreStackp; + ph.primitiveStoreStackp = function(argCount) { + var ctxt = this.stackNonInteger(1); + if (ctxt.sqClass === vm.contextClass_ && ctxt.frame != null) vm.flushAllAndContinue(); + return origStoreStackp.call(this, argCount); + }; + var origArrayBecome = ph.primitiveArrayBecome; + ph.primitiveArrayBecome = function(argCount, doBothWays, copyHash) { + // frames hold raw refs the become machinery doesn't know about: + // flush everything to real contexts first, then become sees it all + vm.flushAllAndContinue(); + return origArrayBecome.call(this, argCount, doBothWays, copyHash); + }; + var origSnapshot = ph.primitiveSnapshot; + ph.primitiveSnapshot = function(argCount) { + vm.flushAllAndContinue(); + return origSnapshot.call(this, argCount); + }; + // logical GC: live page slots are roots + this.image.gcRoots = function() { + return [this.specialObjectsArray].concat(vm.frameGCRoots()); + }; + }, + allocPage: function() { + for (var i = 0; i < this.zonePages.length; i++) + if (!this.zonePages[i].live) { + var page = this.zonePages[i]; + page.slots.length = 0; + page.live = true; + page.baseCallerCtx = null; + if (this.pageStats) this.pageStats.reused++; + return page; + } + if (this.zonePages.length >= 32) { + // sin muertas y con muchas vivas: páginas de procesos terminados + // quedan vivas-inalcanzables (el terminate corta cadenas a nivel + // context). Flushear una suspendida es siempre correcto (su resume + // re-infla via makeBaseFrame) y la vuelve reutilizable. + for (var i = 0; i < this.zonePages.length; i++) { + var victim = this.zonePages[i]; + if (victim.live && victim !== this.zonePage) { + this.flushPageAndContinue(victim); + victim.slots.length = 0; + victim.live = true; + victim.baseCallerCtx = null; + if (this.pageStats) this.pageStats.evicted = (this.pageStats.evicted || 0) + 1; + return victim; + } + } + } + var fresh = { slots: [], fp: -1, sp: -1, pc: -1, live: true, baseCallerCtx: null }; + this.zonePages.push(fresh); + if (this.pageStats) this.pageStats.fresh++; + return fresh; + }, + activatePage: function(page) { + this.zonePage = page; + var slots = page.slots; + this.stack = slots; + this.temps = slots; + this.fp = page.fp; + this.sp = page.sp; + this.pc = page.pc; + this.method = slots[this.fp + Squeak.Frame_method]; + this.receiver = slots[this.fp + Squeak.Frame_receiver]; + this.tempOffset = this.fp + Squeak.Frame_firstTemp; + this.homeContext = null; // not meaningful in frames mode + }, + saveActivePage: function() { + var page = this.zonePage; + if (!page) return; + page.fp = this.fp; + page.sp = this.sp; + page.pc = this.pc; + }, + frameGCRoots: function() { + this.saveActivePage(); + var roots = []; + for (var i = 0; i < this.zonePages.length; i++) { + var page = this.zonePages[i]; + if (!page.live) continue; + var slots = page.slots; + for (var j = 0; j <= page.sp; j++) { + var v = slots[j]; + if (typeof v === "object" && v !== null) roots.push(v); + } + } + return roots; + }, +}, +'frames: marriage', { + marryFrame: function(page, fp) { + var slots = page.slots; + var ctx = slots[fp + Squeak.Frame_context]; + if (ctx) return ctx; + var method = slots[fp + Squeak.Frame_method]; + ctx = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassMethodContext], + method.methodNeedsLargeFrame() ? Squeak.Context_largeFrameSize : Squeak.Context_smallFrameSize); + // shallow snapshot: immutable fields real, mutable ones minimal-but-sane + // (syncPage fills pc/stackp/sender/temps before Smalltalk can look) + ctx.pointers[Squeak.Context_sender] = this.nilObj; + ctx.pointers[Squeak.Context_instructionPointer] = this.nilObj; + ctx.pointers[Squeak.Context_stackPointer] = 0; + ctx.pointers[Squeak.Context_method] = method; + ctx.pointers[Squeak.Context_closure] = slots[fp + Squeak.Frame_closure]; + ctx.pointers[Squeak.Context_receiver] = slots[fp + Squeak.Frame_receiver]; + ctx.frame = { page: page, fp: fp }; + ctx.dirty = true; + slots[fp + Squeak.Frame_context] = ctx; + slots[fp + Squeak.Frame_flags] |= Squeak.Frame_hasContext; + this.nMarriedContexts = (this.nMarriedContexts || 0) + 1; + return ctx; + }, + syncMarriedContext: function(ctx) { + // refresca el snapshot de UN context casado. El tope de página es O(1) + // de ubicar; para frames profundos se busca el callee desde el tope. + // El caller se casa shallow (sin fill): se sincroniza cuando se observe. + var f = ctx.frame; + if (f == null) return; + var page = f.page, fp = f.fp; + this.saveActivePage(); + var slots = page.slots; + var pc, sp; + if (fp === page.fp) { + pc = page.pc; + sp = page.sp; + } else { + // buscar el callee: el frame cuyo savedFp === fp + var t = page.fp; + while (t >= 0 && slots[t + Squeak.Frame_savedFp] !== fp) + t = slots[t + Squeak.Frame_savedFp]; + if (t < 0) throw Error("stack zone: married frame not on its page chain"); + pc = slots[t + Squeak.Frame_savedPc]; + sp = t - 2 - (slots[t + Squeak.Frame_flags] & 0xFFFF); + } + var p = ctx.pointers; + var savedFp = slots[fp + Squeak.Frame_savedFp]; + if (savedFp >= 0 && page.slots[savedFp + Squeak.Frame_context] == null) + this.nMarrySenderFill = (this.nMarrySenderFill || 0) + 1; + p[Squeak.Context_sender] = savedFp >= 0 ? this.marryFrame(page, savedFp) + : (page.baseCallerCtx || this.nilObj); + p[Squeak.Context_instructionPointer] = + this.encodeSqueakPC(pc, slots[fp + Squeak.Frame_method]); + var stackp = sp - fp - Squeak.Frame_firstTemp + 1; + p[Squeak.Context_stackPointer] = stackp; + for (var i = 0; i < stackp; i++) + p[Squeak.Context_tempFrameStart + i] = slots[fp + Squeak.Frame_firstTemp + i]; + ctx.dirty = true; + }, + syncPage: function(page) { + // refresh the snapshots of all married frames in a page (top to base); + // marries callers shallowly to fill sender fields, so a full walk from + // Smalltalk deepens the married chain one syncPage at a time + this.saveActivePage(); + var slots = page.slots; + var fp = page.fp, pc = page.pc, sp = page.sp; + while (fp >= 0) { + var ctx = slots[fp + Squeak.Frame_context]; + var savedFp = slots[fp + Squeak.Frame_savedFp]; + if (ctx) { + var p = ctx.pointers; + p[Squeak.Context_sender] = savedFp >= 0 ? this.marryFrame(page, savedFp) + : (page.baseCallerCtx || this.nilObj); + p[Squeak.Context_instructionPointer] = + this.encodeSqueakPC(pc, slots[fp + Squeak.Frame_method]); + var stackp = sp - fp - Squeak.Frame_firstTemp + 1; + p[Squeak.Context_stackPointer] = stackp; + for (var i = 0; i < stackp; i++) + p[Squeak.Context_tempFrameStart + i] = slots[fp + Squeak.Frame_firstTemp + i]; + ctx.dirty = true; + } + // move down: caller's pc/sp derive from this frame + pc = slots[fp + Squeak.Frame_savedPc]; + sp = fp - 2 - (slots[fp + Squeak.Frame_flags] & 0xFFFF); + fp = savedFp; + } + }, + storeToMarriedContext: function(ctx, index, value) { + if (this.smcStats) this.smcStats[index] = (this.smcStats[index] || 0) + 1; + // Smalltalk escribe un campo de un context con frame vivo (terminate, + // unwind, debugger): serializar SU página a contexts reales, seguir en + // un base frame fresco si era la activa, y escribir sobre el context real + this.flushPageAndContinue(ctx.frame.page); + ctx.pointers[index] = value; + ctx.dirty = true; + if (ctx.frame != null) { + // la escritura cayó sobre el context re-casado del frame base activo: + // mantener la vista del frame consistente + var page = ctx.frame.page; + if (index === Squeak.Context_sender) page.baseCallerCtx = value; + else if (index === Squeak.Context_instructionPointer && typeof value === "number") + this.pc = this.decodeSqueakPC(value, page.slots[ctx.frame.fp + Squeak.Frame_method]); + else this.warnOnce("stack zone: unusual store to active context field " + index); + } + }, + widowFrameContext: function(ctx) { + ctx.pointers[Squeak.Context_sender] = this.nilObj; + ctx.pointers[Squeak.Context_instructionPointer] = this.nilObj; + ctx.frame = null; + ctx.dirty = true; + }, + flushPageAndContinue: function(page) { + // serializar SOLO una página a contexts reales (stores a contexts casados + // suelen ser cirugía de cadena en una página; flushear todas — Dialogo + // hacía 23k flushes totales por 10M sends — era el martillo grande) + this.nFlushPage = (this.nFlushPage || 0) + 1; + this.saveActivePage(); + var isActive = page === this.zonePage; + if (this.pageStats) { this.pageStats.flushActive += isActive ? 1 : 0; this.pageStats.flushSusp += isActive ? 0 : 1; if (!page.live) this.pageStats.flushDead++; } + var top = isActive ? this.marryFrame(page, this.fp) : null; + if (!isActive && page.fp >= 0) this.marryFrame(page, page.fp); + for (var fp = page.fp; fp >= 0; fp = page.slots[fp + Squeak.Frame_savedFp]) + this.marryFrame(page, fp); + this.syncPage(page); + for (var fp = page.fp; fp >= 0; fp = page.slots[fp + Squeak.Frame_savedFp]) { + var ctx = page.slots[fp + Squeak.Frame_context]; + if (ctx) ctx.frame = null; + } + page.live = false; + if (isActive) { + this.zonePage = null; + this.makeBaseFrameZ(top); + } + }, + flushAllAndContinue: function() { + // serialize every live frame to its (married) context, kill all pages, + // and continue execution on a fresh base frame inflated from the top + this.nFlushAll = (this.nFlushAll || 0) + 1; + this.saveActivePage(); + var top = this.marryFrame(this.zonePage, this.fp); + for (var i = 0; i < this.zonePages.length; i++) { + var page = this.zonePages[i]; + if (!page.live) continue; + // marry every frame so syncPage serializes all of them + for (var fp = page.fp; fp >= 0; fp = page.slots[fp + Squeak.Frame_savedFp]) + this.marryFrame(page, fp); + this.syncPage(page); + for (var fp = page.fp; fp >= 0; fp = page.slots[fp + Squeak.Frame_savedFp]) { + var ctx = page.slots[fp + Squeak.Frame_context]; + if (ctx) ctx.frame = null; + } + page.live = false; + } + this.zonePage = null; + this.makeBaseFrameZ(top); + }, + makeBaseFrameZ: function(ctx) { + // inflate a dormant, resumable context into a fresh page's base frame + var methodField = ctx.pointers[Squeak.Context_method]; + if (this.isSmallInt(methodField)) + throw Error("stack zone: pre-closure BlockContexts not supported"); + if (ctx.sqClass !== this.contextClass_) + // los gates de sync/write-through comparan contra ClassMethodContext; + // una subclase casada los evadiría silenciosamente + throw Error("stack zone: cannot inflate a " + ctx.sqClass.className()); + var page = this.allocPage(); + var slots = page.slots; + var closure = ctx.pointers[Squeak.Context_closure]; + var stackp = ctx.pointers[Squeak.Context_stackPointer]; + var numArgs = closure && !closure.isNil + ? closure.pointers[Squeak.Closure_numArgs] + : methodField.methodNumArgs(); + slots[Squeak.Frame_savedFp] = -1; + slots[Squeak.Frame_savedPc] = 0; + slots[Squeak.Frame_method] = methodField; + slots[Squeak.Frame_flags] = numArgs | Squeak.Frame_hasContext; + slots[Squeak.Frame_context] = ctx; + slots[Squeak.Frame_closure] = closure; + slots[Squeak.Frame_receiver] = ctx.pointers[Squeak.Context_receiver]; + for (var i = 0; i < stackp; i++) + slots[Squeak.Frame_firstTemp + i] = ctx.pointers[Squeak.Context_tempFrameStart + i]; + page.baseCallerCtx = ctx.pointers[Squeak.Context_sender]; + page.fp = 0; + page.sp = Squeak.Frame_firstTemp + stackp - 1; + page.pc = this.decodeSqueakPC(ctx.pointers[Squeak.Context_instructionPointer], methodField); + ctx.frame = { page: page, fp: 0 }; + ctx.dirty = true; + this.activatePage(page); + }, +}, +'frames: execution', { + loadInitialContextZ: function() { + var schedAssn = this.specialObjects[Squeak.splOb_SchedulerAssociation]; + var sched = schedAssn.pointers[Squeak.Assn_value]; + var proc = sched.pointers[Squeak.ProcSched_activeProcess]; + this.makeBaseFrameZ(proc.pointers[Squeak.Proc_suspendedContext]); + this.reclaimableContextCount = 0; + }, + sendZ: function(selector, argCount, doSuper) { + var newRcvr = this.stack[this.sp - argCount]; + var lookupClass; + if (doSuper) { + lookupClass = this.method.methodClassForSuper(); + lookupClass = lookupClass.superclass(); + } else { + lookupClass = this.getClass(newRcvr); + } + // married-context receivers: refresh their snapshot before Smalltalk looks + // (class-identity gate first: no property probe on ordinary receivers) + if (lookupClass === this.contextClass_ && newRcvr.frame != null) + this.syncMarriedContext(newRcvr); + var entry = this.findSelectorInClass(selector, argCount, lookupClass); + if (entry.primIndex) { + this.verifyAtSelector = selector; + this.verifyAtClass = lookupClass; + } + this.executeNewMethod(newRcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); + }, + sendSuperDirectedZ: function(selector, argCount) { + var lookupClass = this.pop().superclass(); + var newRcvr = this.stack[this.sp - argCount]; + if (typeof newRcvr === "object" && newRcvr !== null + && newRcvr.sqClass === this.contextClass_ && newRcvr.frame != null) + this.syncMarriedContext(newRcvr); + var entry = this.findSelectorInClass(selector, argCount, lookupClass); + if (entry.primIndex) { + this.verifyAtSelector = selector; + this.verifyAtClass = lookupClass; + } + this.executeNewMethod(newRcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); + }, + executeNewMethodZ: function(newRcvr, newMethod, argumentCount, primitiveIndex, optClass, optSel) { + this.sendCount++; + if (newMethod === this.breakOnMethod) + this.breakNow("executing method " + this.printMethod(newMethod, optClass, optSel)); + if (this.logSends) { + var args = this.stack.slice(this.sp + 1 - argumentCount, this.sp + 1); + console.log(this.sendCount + ' ' + this.printMethod(newMethod, optClass, optSel, args)); + } + if (this.breakOnContextChanged) { + this.breakOnContextChanged = false; + this.breakNow(); + } + if (primitiveIndex > 0) + if (this.tryPrimitive(primitiveIndex, argumentCount, newMethod)) + return; //Primitive succeeded -- end of story + var slots = this.zonePage.slots; + var nfp = this.sp + 1; + slots[nfp + Squeak.Frame_savedFp] = this.fp; + slots[nfp + Squeak.Frame_savedPc] = this.pc; + slots[nfp + Squeak.Frame_method] = newMethod; + slots[nfp + Squeak.Frame_flags] = argumentCount; + slots[nfp + Squeak.Frame_context] = null; + slots[nfp + Squeak.Frame_closure] = this.nilObj; + slots[nfp + Squeak.Frame_receiver] = newRcvr; + var tempCount = newMethod.methodTempCount(); + var base = nfp + Squeak.Frame_firstTemp; + for (var i = 0; i < argumentCount; i++) + slots[base + i] = slots[nfp - argumentCount + i]; + for (var i = argumentCount; i < tempCount; i++) + slots[base + i] = this.nilObj; + this.fp = nfp; + this.sp = base + tempCount - 1; + this.pc = 0; + this.method = newMethod; + this.receiver = newRcvr; + this.tempOffset = base; + if (!newMethod.compiled) this.compileIfPossible(newMethod, optClass, optSel); + var myPage = this.zonePage; + // check for process switch on full method activation + if (this.interruptCheckCounter-- <= 0) this.checkForInterrupts(); + // direct call: run the callee as a plain JS call instead of returning to + // the trampoline. The JS stack never holds suspended state (every level + // returns as soon as its Smalltalk continuation leaves its frame, via the + // activation checks), so a process switch simply unwinds the whole chain + // one compare+return per level. Depth-capped for the JS stack limit. + if (newMethod.compiled && this.jsDepth < 512 + && this.fp === nfp && this.zonePage === myPage + && this.breakOutOfInterpreter === false) { + this.jsDepth++; + newMethod.compiled(this); + this.jsDepth--; + } + }, + doBlockReturnZ: function(returnValue) { + // return from a block activation to its caller (never non-local) + var slots = this.zonePage.slots; + var fp = this.fp; + var ctx = slots[fp + Squeak.Frame_context]; + if (ctx) this.widowFrameContext(ctx); + var savedFp = slots[fp + Squeak.Frame_savedFp]; + var numArgs = slots[fp + Squeak.Frame_flags] & 0xFFFF; + if (savedFp >= 0) { + var retSlot = fp - 1 - numArgs; + slots[retSlot] = returnValue; + this.sp = retSlot; + this.pc = slots[fp + Squeak.Frame_savedPc]; + this.fp = savedFp; + this.method = slots[savedFp + Squeak.Frame_method]; + this.receiver = slots[savedFp + Squeak.Frame_receiver]; + this.tempOffset = savedFp + Squeak.Frame_firstTemp; + } else { + this.returnFromBaseFrame(returnValue); + } + if (this.breakOnContextChanged) { + this.breakOnContextChanged = false; + this.breakNow(); + } + }, + doReturnZ: function(returnValue) { + var page = this.zonePage; + var slots = page.slots; + // 1. find home of the active frame (walk closure chain) + var homePage = page, homeFp = this.fp, homeCtx = null; + if (this.hasClosures) { + while (true) { + var closure = homeCtx ? homeCtx.pointers[Squeak.Context_closure] + : homePage.slots[homeFp + Squeak.Frame_closure]; + if (closure.isNil) break; + var outer = closure.pointers[Squeak.Closure_outerContext]; + if (outer.frame != null) { + homePage = outer.frame.page; homeFp = outer.frame.fp; homeCtx = null; + } else { + homeCtx = outer; + } + } + } + // 2. target = sender of home + var targetPage = null, targetFp = -1, targetCtx = null; + if (homeCtx) { + targetCtx = homeCtx.pointers[Squeak.Context_sender]; + } else { + var sfp = homePage.slots[homeFp + Squeak.Frame_savedFp]; + if (sfp >= 0) { targetPage = homePage; targetFp = sfp; } + else targetCtx = homePage.baseCallerCtx || this.nilObj; + } + if (targetCtx) { + if (targetCtx.frame != null) { + targetPage = targetCtx.frame.page; targetFp = targetCtx.frame.fp; targetCtx = null; + } else if (targetCtx.isNil || targetCtx.pointers[Squeak.Context_instructionPointer].isNil) { + return this.cannotReturn(returnValue); + } + } + // 3. unwind scan from caller-of-active down to target + var sPage = page, sFp = slots[this.fp + Squeak.Frame_savedFp], sCtx = null; + if (sFp < 0) { sCtx = page.baseCallerCtx || this.nilObj; sPage = null; } + while (true) { + if (sCtx === null) { + if (sPage === targetPage && sFp === targetFp) break; + var m = sPage.slots[sFp + Squeak.Frame_method]; + if (m.methodPrimitiveIndex() == 198) + return this.aboutToReturnThrough(returnValue, this.marryFrame(sPage, sFp)); + var nextFp = sPage.slots[sFp + Squeak.Frame_savedFp]; + if (nextFp >= 0) { sFp = nextFp; } + else { sCtx = sPage.baseCallerCtx || this.nilObj; sPage = null; } + } else { + if (targetCtx !== null && sCtx === targetCtx) break; + if (sCtx.isNil) return this.cannotReturn(returnValue); + if (sCtx.frame != null) { + sPage = sCtx.frame.page; sFp = sCtx.frame.fp; sCtx = null; + if (sPage === targetPage && sFp === targetFp) break; + continue; + } + if (this.isUnwindMarked(sCtx)) + return this.aboutToReturnThrough(returnValue, sCtx); + sCtx = sCtx.pointers[Squeak.Context_sender]; + } + } + // 4. pop frames from active down to target, widowing married ones + var pPage = page, pFp = this.fp; + while (true) { + var fslots = pPage.slots; + var mCtx = fslots[pFp + Squeak.Frame_context]; + if (mCtx) this.widowFrameContext(mCtx); + var lastPc = fslots[pFp + Squeak.Frame_savedPc]; + var lastNumArgs = fslots[pFp + Squeak.Frame_flags] & 0xFFFF; + var downFp = fslots[pFp + Squeak.Frame_savedFp]; + if (downFp >= 0) { + var retSlot = pFp - 1 - lastNumArgs; + pFp = downFp; + if (pPage === targetPage && pFp === targetFp) { + if (pPage !== this.zonePage) { + this.zonePage = pPage; + this.stack = pPage.slots; + this.temps = pPage.slots; + } + this.fp = pFp; + this.sp = retSlot; + pPage.slots[retSlot] = returnValue; + this.pc = lastPc; + this.method = pPage.slots[pFp + Squeak.Frame_method]; + this.receiver = pPage.slots[pFp + Squeak.Frame_receiver]; + this.tempOffset = pFp + Squeak.Frame_firstTemp; + break; + } + } else { + // popped through the page's base frame: page dies + pPage.live = false; + var cctx = pPage.baseCallerCtx || this.nilObj; + if (cctx.frame != null) { + pPage = cctx.frame.page; + pFp = cctx.frame.fp; + if (pPage === targetPage && pFp === targetFp) { + // landing on the (suspended) top frame of another page + this.activatePage(pPage); + this.push(returnValue); + break; + } + } else { + // dormant context: scan guaranteed it's the target + this.zonePage = null; + this.makeBaseFrameZ(cctx); + this.push(returnValue); + break; + } + } + } + if (this.breakOnContextChanged) { + this.breakOnContextChanged = false; + this.breakNow(); + } + }, + returnFromBaseFrame: function(returnValue) { + // active frame is a base frame; return to its caller context + var page = this.zonePage; + page.live = false; + var cctx = page.baseCallerCtx || this.nilObj; + var ctx = page.slots[this.fp + Squeak.Frame_context]; + if (ctx) this.widowFrameContext(ctx); + if (cctx.frame != null) { + this.activatePage(cctx.frame.page); + this.push(returnValue); + } else { + if (cctx.isNil || cctx.pointers[Squeak.Context_instructionPointer].isNil) + return this.cannotReturn(returnValue); + this.zonePage = null; + this.makeBaseFrameZ(cctx); + this.push(returnValue); + } + }, + exportThisContextZ: function() { + this.nMarryThisCtx = (this.nMarryThisCtx || 0) + 1; + var ctx = this.marryFrame(this.zonePage, this.fp); + this.syncMarriedContext(ctx); // solo el tope: O(frame), sin cascada + this.reclaimableContextCount = 0; + return ctx; + }, + newActiveContextZ: function(newContext) { + // process switch (transferTo) or explicit context activation + this.saveActivePage(); + if (newContext.frame != null) { + this.activatePage(newContext.frame.page); + } else { + this.makeBaseFrameZ(newContext); + } + if (this.breakOnContextChanged) { + this.breakOnContextChanged = false; + this.breakNow(); + } + }, + storeContextRegistersZ: function() { + throw Error("stack zone: storeContextRegisters should not be called"); + }, + fetchContextRegistersZ: function(ctxt) { + throw Error("stack zone: fetchContextRegisters should not be called"); + }, + allocateOrRecycleContextZ: function(needsLarge) { + throw Error("stack zone: allocateOrRecycleContext should not be called"); + }, + activeContextObjZ: function() { + // for the few legacy readers that need the active context as an object + this.nMarryClosure = (this.nMarryClosure || 0) + 1; + return this.marryFrame(this.zonePage, this.fp); + }, + tryPrimitiveZ: function(primIndex, argCount, newMethod) { + if (primIndex === 117) { + // prim con nombre: cachear la función resuelta en el método + // (el camino normal reconstruye los strings de módulo/función y + // hace lookups por nombre EN CADA llamada) + var fn = newMethod.primFn; + if (fn === undefined) fn = this.primHandler.resolveNamedPrim(newMethod); + if (fn !== null) { + var sp117 = this.sp; + var page117 = this.zonePage, fp117 = this.fp; + var ok = fn(argCount); + if (ok + && this.sp !== sp117 - argCount + && page117 === this.zonePage && fp117 === this.fp + && !this.frozen) { + this.warnOnce("stack unbalanced after primitive 117/cached", "error"); + } + return ok; + } + // sin resolver (módulo/función faltante): camino lento con warnings + } + if ((primIndex > 255) && (primIndex < 520)) { + if (primIndex >= 264) {//return instvars + this.popNandPush(1, this.top().pointers[primIndex - 264]); + return true; + } + switch (primIndex) { + case 256: //return self + return true; + case 257: this.popNandPush(1, this.trueObj); //return true + return true; + case 258: this.popNandPush(1, this.falseObj); //return false + return true; + case 259: this.popNandPush(1, this.nilObj); //return nil + return true; + } + this.popNandPush(1, primIndex - 261); //return -1...2 + return true; + } + var sp = this.sp; + var page = this.zonePage, fp = this.fp; // activation identity without touching contexts + var success = this.primHandler.doPrimitive(primIndex, argCount, newMethod); + if (success + && this.sp !== sp - argCount + && page === this.zonePage && fp === this.fp + && primIndex !== 117 && primIndex !== 118 && primIndex !== 218 + && !this.frozen) { + this.warnOnce("stack unbalanced after primitive " + primIndex, "error"); + } + return success; + }, +}); + +Object.extend(Squeak.Primitives.prototype, +'stack zone', { + resolveNamedPrim: function(method) { + if (this.vm.primFnDebug) console.error("RESOLVE intento"); + // resolver módulo+función del prim 117 una sola vez y cachear en el + // método un wrapper que replica el protocolo de namedPrimitive + method.primFn = null; + if (method.pointersSize() < 2) return null; + var firstLiteral = method.pointers[1]; + if (firstLiteral.pointersSize() !== 4) return null; + var moduleName = firstLiteral.pointers[0].bytesAsString(); + var functionName = firstLiteral.pointers[1].bytesAsString(); + if (typeof process !== "undefined" && process.env && process.env.PRIMFN_SKIP + && (process.env.PRIMFN_SKIP === "*" || process.env.PRIMFN_SKIP.split(",").indexOf(moduleName) >= 0)) return null; + var mod = moduleName === "" ? this : this.loadedModules[moduleName]; + if (mod === undefined) { + // loadModule chequea interpreterProxy.failed() tras initialiseModule, + // que lee el flag `success` AMBIENTE — en el camino original doPrimitive + // lo resetea al entrar; acá resolvemos antes de ese punto, así que un + // success=false stale del primitivo anterior hacía "fallar" la carga + this.success = true; + mod = this.loadModule(moduleName); + this.loadedModules[moduleName] = mod; + } + if (!mod) return null; + var primitive = mod[functionName]; + var ph = this, proxy = this.interpreterProxy; + var inner; + if (typeof primitive === "function") inner = primitive.bind(mod); + else if (typeof primitive === "string") inner = this[primitive].bind(this); + else return null; // missing: que el camino lento warnee + var wrapper = function(argCount) { + ph.success = true; + proxy.argCount = argCount; + proxy.primitiveName = functionName; + ph.primMethod = method; + var r = inner(argCount); + if (r === true || r === false) return r; + return ph.success; + }; + method.primFn = wrapper; + return wrapper; + }, + activateNewClosureMethodZ: function(blockClosure, argCount) { + // old-style (V3) closures: startpc into the home method + var vm = this.vm; + var outer = blockClosure.pointers[Squeak.Closure_outerContext]; + var of = outer.frame; + var method = of != null ? of.page.slots[of.fp + Squeak.Frame_method] + : outer.pointers[Squeak.Context_method]; + var receiver = of != null ? of.page.slots[of.fp + Squeak.Frame_receiver] + : outer.pointers[Squeak.Context_receiver]; + var numCopied = blockClosure.pointers.length - Squeak.Closure_firstCopiedValue; + var slots = vm.zonePage.slots; + var nfp = vm.sp + 1; + slots[nfp + Squeak.Frame_savedFp] = vm.fp; + slots[nfp + Squeak.Frame_savedPc] = vm.pc; + slots[nfp + Squeak.Frame_method] = method; + slots[nfp + Squeak.Frame_flags] = argCount; + slots[nfp + Squeak.Frame_context] = null; + slots[nfp + Squeak.Frame_closure] = blockClosure; + slots[nfp + Squeak.Frame_receiver] = receiver; + var base = nfp + Squeak.Frame_firstTemp; + for (var i = 0; i < argCount; i++) + slots[base + i] = slots[nfp - argCount + i]; + for (var i = 0; i < numCopied; i++) + slots[base + argCount + i] = blockClosure.pointers[Squeak.Closure_firstCopiedValue + i]; + // the initial block instructions nil-out remaining temps (stackp = args+copied) + vm.fp = nfp; + vm.sp = base + argCount + numCopied - 1; + vm.pc = vm.decodeSqueakPC(blockClosure.pointers[Squeak.Closure_startpc], method); + vm.method = method; + vm.receiver = receiver; + vm.tempOffset = base; + if (method.compiled && vm.jsDepth < 512 && vm.breakOutOfInterpreter === false) { + vm.jsDepth++; + method.compiled(vm); + vm.jsDepth--; + } + }, + activateNewFullClosureZ: function(blockClosure, argCount) { + var vm = this.vm; + var closureMethod = blockClosure.pointers[Squeak.ClosureFull_method]; + var numCopied = blockClosure.pointers.length - Squeak.ClosureFull_firstCopiedValue; + var receiver = blockClosure.pointers[Squeak.ClosureFull_receiver]; + var slots = vm.zonePage.slots; + var nfp = vm.sp + 1; + slots[nfp + Squeak.Frame_savedFp] = vm.fp; + slots[nfp + Squeak.Frame_savedPc] = vm.pc; + slots[nfp + Squeak.Frame_method] = closureMethod; + slots[nfp + Squeak.Frame_flags] = argCount; + slots[nfp + Squeak.Frame_context] = null; + slots[nfp + Squeak.Frame_closure] = blockClosure; + slots[nfp + Squeak.Frame_receiver] = receiver; + var tempCount = closureMethod.methodTempCount(); // args + copied + locals + var base = nfp + Squeak.Frame_firstTemp; + for (var i = 0; i < argCount; i++) + slots[base + i] = slots[nfp - argCount + i]; + for (var i = 0; i < numCopied; i++) + slots[base + argCount + i] = blockClosure.pointers[Squeak.ClosureFull_firstCopiedValue + i]; + for (var i = argCount + numCopied; i < tempCount; i++) + slots[base + i] = vm.nilObj; + vm.fp = nfp; + vm.sp = base + tempCount - 1; + vm.pc = 0; + vm.method = closureMethod; + vm.receiver = receiver; + vm.tempOffset = base; + if (!closureMethod.compiled) vm.compileIfPossible(closureMethod); + if (closureMethod.compiled && vm.jsDepth < 512 && vm.breakOutOfInterpreter === false) { + vm.jsDepth++; + closureMethod.compiled(vm); + vm.jsDepth--; + } + }, +});