"use strict"; /* * Copyright (c) 2013-2025 Vanessa Freudenberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ Object.subclass('Squeak.Interpreter', 'initialization', { initialize: function(image, display, options) { console.log('squeak: initializing interpreter ' + Squeak.vmVersion + ' (' + Squeak.vmDate + ')'); this.Squeak = Squeak; // store locally to avoid dynamic lookup in Lively this.image = image; this.image.vm = this; this.options = options || {}; this.primHandler = new Squeak.Primitives(this, display); this.loadImageState(); this.initVMState(); this.loadInitialContext(); this.hackImage(); this.initCompiler(); console.log('squeak: ready'); }, loadImageState: function() { this.specialObjects = this.image.specialObjectsArray.pointers; this.specialSelectors = this.specialObjects[Squeak.splOb_SpecialSelectors].pointers; this.nilObj = this.specialObjects[Squeak.splOb_NilObject]; this.falseObj = this.specialObjects[Squeak.splOb_FalseObject]; this.trueObj = this.specialObjects[Squeak.splOb_TrueObject]; this.hasClosures = this.image.hasClosures; this.getGlobals = this.globalsGetter(); // hack for old image that does not support Unix files if (!this.hasClosures && !this.findMethod("UnixFileDirectory class>>pathNameDelimiter")) this.primHandler.emulateMac = true; // pre-release image has inverted colors if (this.image.version == 6501) this.primHandler.reverseDisplay = true; }, initVMState: function() { this.byteCodeCount = 0; this.sendCount = 0; this.interruptCheckCounter = 0; this.interruptCheckCounterFeedBackReset = 1000; this.interruptChecksEveryNms = 3; this.lowSpaceThreshold = 1000000; this.signalLowSpace = false; this.nextPollTick = 0; this.nextWakeupTick = 0; this.lastTick = 0; this.interruptKeycode = 2094; //"cmd-." this.interruptPending = false; this.pendingFinalizationSignals = 0; this.freeContexts = this.nilObj; this.freeLargeContexts = this.nilObj; this.reclaimableContextCount = 0; this.nRecycledContexts = 0; this.nAllocatedContexts = 0; this.methodCacheSize = 1024; this.methodCacheMask = this.methodCacheSize - 1; this.methodCacheRandomish = 0; this.methodCache = []; for (var i = 0; i < this.methodCacheSize; i++) this.methodCache[i] = {lkupClass: null, selector: null, method: null, primIndex: 0, argCount: 0, mClass: null}; this.breakOutOfInterpreter = false; this.breakOutTick = 0; this.breakOnMethod = null; // method to break on this.breakOnNewMethod = false; this.breakOnMessageNotUnderstood = false; this.breakOnContextChanged = false; this.breakOnContextReturned = null; // context to break on this.messages = {}; this.startupTime = Date.now(); // base for millisecond clock }, loadInitialContext: function() { var schedAssn = this.specialObjects[Squeak.splOb_SchedulerAssociation]; var sched = schedAssn.pointers[Squeak.Assn_value]; var proc = sched.pointers[Squeak.ProcSched_activeProcess]; this.activeContext = proc.pointers[Squeak.Proc_suspendedContext]; this.activeContext.dirty = true; this.fetchContextRegisters(this.activeContext); this.reclaimableContextCount = 0; }, globalsGetter: function() { // Globals (more specifically the pointers we are interested in) might // change during execution, because a Dictionary needs growing for example. // Therefore answer a getter function to access the actual globals (pointers). // This getter can be used, even if the Dictionary has grown (and thereby the // underlying Array is replaced by a larger one), because it uses the reference // to the 'outer' Dictionary instead of the pointers to the values. var smalltalk = this.specialObjects[Squeak.splOb_SmalltalkDictionary], smalltalkClass = smalltalk.sqClass.className(); if (smalltalkClass === "Association") { smalltalk = smalltalk.pointers[1]; smalltalkClass = smalltalk.sqClass.className(); } if (smalltalkClass === "SystemDictionary") return function() { return smalltalk.pointers[1].pointers; }; if (smalltalkClass === "SmalltalkImage") { var globals = smalltalk.pointers[0], globalsClass = globals.sqClass.className(); if (globalsClass === "SystemDictionary") return function() { return globals.pointers[1].pointers; }; if (globalsClass === "Environment") return function() { return globals.pointers[2].pointers[1].pointers; }; } console.warn("cannot find global dict"); return function() { return []; }; }, initCompiler: function() { if (!Squeak.Compiler) return console.warn("Squeak.Compiler not loaded, using interpreter only"); // some JS environments disallow creating functions at runtime (e.g. FireFox OS apps) try { if (new Function("return 42")() !== 42) return console.warn("function constructor not working, disabling JIT"); } catch (e) { return console.warn("disabling JIT: " + e); } // disable JIT on slow machines, which are likely memory-limited var kObjPerSec = this.image.oldSpaceCount / (this.startupTime - this.image.startupTime); if (kObjPerSec < 10) return console.warn("Slow machine detected (loaded " + (kObjPerSec*1000|0) + " objects/sec), using interpreter only"); // compiler might decide to not handle current image try { console.log("squeak: initializing JIT compiler"); var compiler = new Squeak.Compiler(this); if (compiler.compile) this.compiler = compiler; } catch(e) { console.warn("Compiler: " + e); } if (!this.compiler) { console.warn("SqueakJS will be running in interpreter mode only (slow)"); } }, hackImage: function() { // hack methods to make work / speed up var returnSelf = 256, returnTrue = 257, returnFalse = 258, returnNil = 259, sista = this.method.methodSignFlag(); [ // Etoys fallback for missing translation files is hugely inefficient. // This speeds up opening a viewer by 10x (!) // Remove when we added translation files. //{method: "String>>translated", primitive: returnSelf, enabled: true}, //{method: "String>>translatedInAllDomains", primitive: returnSelf, enabled: true}, // 64 bit Squeak does not flush word size on snapshot {method: "SmalltalkImage>>wordSize", literal: {index: 1, old: 8, hack: 4, skip: this.nilObj}, enabled: true}, // Squeak 5.3 disable wizard by replacing #open send with pop {method: "ReleaseBuilder class>>prepareEnvironment", bytecode: {pc: 28, old: 0xD8, hack: 0x87}, enabled: !sista & this.options.wizard===false}, // Squeak 6.0 disable wizard by replacing #openWelcomeWorkspacesWith: send with pop {method: "ReleaseBuilder class>>prepareEnvironment", bytecode: {closure: 9, pc: 5, old: 0x81, hack: 0xD8}, enabled: sista & this.options.wizard===false}, // Squeak 6.0 disable welcome workspace by replacing #open send with pop {method: "ReleaseBuilder class>>prepareEnvironment", bytecode: {closure: 9, pc: 2, old: 0x90, hack: 0xD8}, enabled: sista & this.options.welcome===false}, // Squeak source file should use UTF8 not MacRoman (both V3 and Sista) {method: "Latin1Environment class>>systemConverterClass", bytecode: {pc: 53, old: 0x45, hack: 0x49}, enabled: !this.image.isSpur}, {method: "Latin1Environment class>>systemConverterClass", bytecode: {pc: 38, old: 0x16, hack: 0x13}, enabled: this.image.isSpur && sista}, {method: "Latin1Environment class>>systemConverterClass", bytecode: {pc: 50, old: 0x44, hack: 0x48}, enabled: this.image.isSpur && !sista}, // New FFI can't detect platform – pretend to be 32 bit intel {method: "FFIPlatformDescription>>abi", literal: { index: 21, old_str: 'UNKNOWN_ABI', new_str: 'IA32'}, enabled: sista}, ].forEach(function(each) { try { var m = each.enabled && this.findMethod(each.method); if (m) { var prim = each.primitive, byte = each.bytecode, lit = each.literal, hacked = true; if (byte && byte.closure) m = m.pointers[byte.closure]; if (prim) m.pointers[0] |= prim; else if (byte && m.bytes[byte.pc] === byte.old) m.bytes[byte.pc] = byte.hack; else if (byte && m.bytes[byte.pc] === byte.hack) hacked = false; // already there else if (lit && lit.old_str && m.pointers[lit.index].bytesAsString() === lit.old_str) m.pointers[lit.index] = this.primHandler.makeStString(lit.new_str); else if (lit && m.pointers[lit.index].pointers?.[1] === lit.skip) hacked = false; // not needed else if (lit && m.pointers[lit.index].pointers?.[1] === lit.old) m.pointers[lit.index].pointers[1] = lit.hack; else if (lit && m.pointers[lit.index].pointers?.[1] === lit.hack) hacked = false; // already there else { hacked = false; console.warn("Not hacking " + each.method); } if (hacked) console.warn("Hacking " + each.method); } } catch (error) { console.error("Failed to hack " + each.method + " with error " + error); } }, this); }, }, 'interpreting', { interpretOne: function(singleStep) { if (this.method.compiled) { if (singleStep) { if (!this.compiler.enableSingleStepping(this.method)) { this.method.compiled = null; return this.interpretOne(singleStep); } this.breakNow(); } this.method.compiled(this); return; } if (this.method.methodSignFlag()) { return this.interpretOneSistaWithExtensions(singleStep, 0, 0); } var Squeak = this.Squeak; // avoid dynamic lookup of "Squeak" in Lively var b, b2; this.byteCodeCount++; b = this.nextByte(); switch (b) { /* The Main V3 Bytecode Dispatch Loop */ // load receiver variable case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x0E: case 0x0F: this.push(this.receiver.pointers[b&0xF]); return; // 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; // loadLiteral case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: case 0x28: case 0x29: case 0x2A: case 0x2B: case 0x2C: case 0x2D: case 0x2E: case 0x2F: case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: case 0x3A: case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F: this.push(this.method.methodGetLiteral(b&0x1F)); return; // loadLiteralIndirect case 0x40: case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46: case 0x47: case 0x48: case 0x49: case 0x4A: case 0x4B: case 0x4C: case 0x4D: case 0x4E: case 0x4F: case 0x50: case 0x51: case 0x52: case 0x53: case 0x54: case 0x55: case 0x56: case 0x57: case 0x58: case 0x59: case 0x5A: case 0x5B: case 0x5C: case 0x5D: case 0x5E: case 0x5F: this.push((this.method.methodGetLiteral(b&0x1F)).pointers[Squeak.Assn_value]); return; // 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; 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; // Quick push case 0x70: this.push(this.receiver); return; case 0x71: this.push(this.trueObj); return; case 0x72: this.push(this.falseObj); return; case 0x73: this.push(this.nilObj); return; case 0x74: this.push(-1); return; case 0x75: this.push(0); return; case 0x76: this.push(1); return; case 0x77: this.push(2); return; // Quick return case 0x78: this.doReturn(this.receiver); return; case 0x79: this.doReturn(this.trueObj); return; 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 0x7E: this.nono(); return; case 0x7F: this.nono(); return; // Sundry case 0x80: this.extendedPush(this.nextByte()); return; case 0x81: this.extendedStore(this.nextByte()); return; case 0x82: this.extendedStorePop(this.nextByte()); return; // singleExtendedSend case 0x83: b2 = this.nextByte(); this.send(this.method.methodGetSelector(b2&31), b2>>5, false); return; case 0x84: this.doubleExtendedDoAnything(this.nextByte()); return; // singleExtendedSendToSuper case 0x85: b2= this.nextByte(); this.send(this.method.methodGetSelector(b2&31), b2>>5, true); return; // secondExtendedSend case 0x86: b2= this.nextByte(); this.send(this.method.methodGetSelector(b2&63), b2>>6, false); return; case 0x87: this.pop(); return; // pop case 0x88: this.push(this.top()); return; // dup // thisContext case 0x89: this.push(this.exportThisContext()); return; // Closures case 0x8A: this.pushNewArray(this.nextByte()); // create new temp vector return; 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]); return; case 0x8D: b2 = this.nextByte(); // remote store into temp vector var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+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()]; vec.pointers[b2] = this.pop(); vec.dirty = true; return; case 0x8F: this.pushClosureCopy(); return; // Short jmp case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97: this.pc += (b&7)+1; return; // Short conditional jump on false case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F: this.jumpIfFalse((b&7)+1); return; // Long jump, forward and back case 0xA0: case 0xA1: case 0xA2: case 0xA3: case 0xA4: case 0xA5: case 0xA6: case 0xA7: b2 = this.nextByte(); this.pc += (((b&7)-4)*256 + b2); if ((b&7)<4) // check for process switch on backward jumps (loops) if (this.interruptCheckCounter-- <= 0) this.checkForInterrupts(); return; // Long conditional jump on true case 0xA8: case 0xA9: case 0xAA: case 0xAB: this.jumpIfTrue((b&3)*256 + this.nextByte()); return; // Long conditional jump on false case 0xAC: case 0xAD: case 0xAE: case 0xAF: this.jumpIfFalse((b&3)*256 + this.nextByte()); return; // Arithmetic Ops... + - < > <= >= = ~= * / @ lshift: lxor: land: lor: case 0xB0: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) + this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // PLUS + case 0xB1: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) - this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // MINUS - case 0xB2: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) < this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // LESS < case 0xB3: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) > this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // GRTR > case 0xB4: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) <= this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // LEQ <= case 0xB5: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) >= this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // GEQ >= case 0xB6: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) === this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // EQU = case 0xB7: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) !== this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // NEQ ~= case 0xB8: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) * this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // TIMES * case 0xB9: this.success = true; if (!this.pop2AndPushIntResult(this.quickDivide(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // Divide / case 0xBA: this.success = true; if (!this.pop2AndPushIntResult(this.mod(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // MOD \ case 0xBB: this.success = true; if (!this.primHandler.primitiveMakePoint(1, true)) this.sendSpecial(b&0xF); return; // MakePt int@int case 0xBC: this.success = true; if (!this.pop2AndPushIntResult(this.safeShift(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // bitShift: case 0xBD: this.success = true; if (!this.pop2AndPushIntResult(this.div(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // Divide // case 0xBE: this.success = true; if (!this.pop2AndPushIntResult(this.stackInteger(1) & this.stackInteger(0))) this.sendSpecial(b&0xF); return; // bitAnd: case 0xBF: this.success = true; if (!this.pop2AndPushIntResult(this.stackInteger(1) | this.stackInteger(0))) this.sendSpecial(b&0xF); return; // bitOr: // at:, at:put:, size, next, nextPut:, ... case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC4: case 0xC5: case 0xC6: case 0xC7: case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF: if (!this.primHandler.quickSendOther(this.receiver, b&0xF)) this.sendSpecial((b&0xF)+16); return; // Send Literal Selector with 0, 1, and 2 args case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7: case 0xD8: case 0xD9: case 0xDA: case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF: this.send(this.method.methodGetSelector(b&0xF), 0, false); return; case 0xE0: case 0xE1: case 0xE2: case 0xE3: case 0xE4: case 0xE5: case 0xE6: case 0xE7: case 0xE8: case 0xE9: case 0xEA: case 0xEB: case 0xEC: case 0xED: case 0xEE: case 0xEF: this.send(this.method.methodGetSelector(b&0xF), 1, false); return; case 0xF0: case 0xF1: case 0xF2: case 0xF3: case 0xF4: case 0xF5: case 0xF6: case 0xF7: case 0xF8: case 0xF9: case 0xFA: case 0xFB: case 0xFC: case 0xFD: case 0xFE: case 0xFF: this.send(this.method.methodGetSelector(b&0xF), 2, false); return; } throw Error("not a bytecode: " + b); }, interpretOneSistaWithExtensions: function(singleStep, extA, extB) { var Squeak = this.Squeak; // avoid dynamic lookup of "Squeak" in Lively var b, b2; this.byteCodeCount++; b = this.nextByte(); switch (b) { /* The Main Sista Bytecode Dispatch Loop */ // 1 Byte Bytecodes // load receiver variable case 0x00: case 0x01: case 0x02: case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: case 0x08: case 0x09: case 0x0A: case 0x0B: case 0x0C: case 0x0D: case 0x0E: case 0x0F: this.push(this.receiver.pointers[b&0xF]); return; // load literal 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.method.methodGetLiteral(b&0xF)).pointers[Squeak.Assn_value]); return; // load literal constant case 0x20: case 0x21: case 0x22: case 0x23: case 0x24: case 0x25: case 0x26: case 0x27: case 0x28: case 0x29: case 0x2A: case 0x2B: case 0x2C: case 0x2D: case 0x2E: case 0x2F: case 0x30: case 0x31: case 0x32: case 0x33: case 0x34: case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: case 0x3A: case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F: this.push(this.method.methodGetLiteral(b&0x1F)); return; // 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; case 0x48: case 0x49: case 0x4A: case 0x4B: this.push(this.homeContext.pointers[Squeak.Context_tempFrameStart+(b&0x3)+8]); return; case 0x4C: this.push(this.receiver); return; case 0x4D: this.push(this.trueObj); return; case 0x4E: this.push(this.falseObj); return; case 0x4F: this.push(this.nilObj); return; case 0x50: this.push(0); return; case 0x51: this.push(1); return; case 0x52: if (extB == 0) { this.push(this.exportThisContext()); return; } else { this.nono(); return; } case 0x53: this.push(this.top()); return; case 0x54: case 0x55: case 0x56: case 0x57: this.nono(); return; // unused case 0x58: this.doReturn(this.receiver); return; case 0x59: this.doReturn(this.trueObj); return; 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 0x5E: if (extA == 0) { this.doReturn(this.pop(), this.activeContext.pointers[Squeak.BlockContext_caller]); return; // blockReturn } else { this.nono(); return; } case 0x5F: return; // nop // Arithmetic Ops... + - < > <= >= = ~= * / @ lshift: lxor: land: lor: case 0x60: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) + this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // PLUS + case 0x61: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) - this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // MINUS - case 0x62: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) < this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // LESS < case 0x63: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) > this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // GRTR > case 0x64: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) <= this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // LEQ <= case 0x65: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) >= this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // GEQ >= case 0x66: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) === this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // EQU = case 0x67: this.success = true; if (!this.pop2AndPushBoolResult(this.stackIntOrFloat(1) !== this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // NEQ ~= case 0x68: this.success = true; this.resultIsFloat = false; if (!this.pop2AndPushNumResult(this.stackIntOrFloat(1) * this.stackIntOrFloat(0))) this.sendSpecial(b&0xF); return; // TIMES * case 0x69: this.success = true; if (!this.pop2AndPushIntResult(this.quickDivide(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // Divide / case 0x6A: this.success = true; if (!this.pop2AndPushIntResult(this.mod(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // MOD \ case 0x6B: this.success = true; if (!this.primHandler.primitiveMakePoint(1, true)) this.sendSpecial(b&0xF); return; // MakePt int@int case 0x6C: this.success = true; if (!this.pop2AndPushIntResult(this.safeShift(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // bitShift: case 0x6D: this.success = true; if (!this.pop2AndPushIntResult(this.div(this.stackInteger(1),this.stackInteger(0)))) this.sendSpecial(b&0xF); return; // Divide // case 0x6E: this.success = true; if (!this.pop2AndPushIntResult(this.stackInteger(1) & this.stackInteger(0))) this.sendSpecial(b&0xF); return; // bitAnd: case 0x6F: this.success = true; if (!this.pop2AndPushIntResult(this.stackInteger(1) | this.stackInteger(0))) this.sendSpecial(b&0xF); return; // bitOr: // at:, at:put:, size, next, nextPut:, ... case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75: case 0x76: case 0x77: case 0x78: case 0x79: case 0x7A: case 0x7B: case 0x7C: case 0x7D: case 0x7E: case 0x7F: if (!this.primHandler.quickSendOther(this.receiver, b&0xF)) this.sendSpecial((b&0xF)+16); return; // Send Literal Selector with 0, 1, and 2 args case 0x80: case 0x81: case 0x82: case 0x83: case 0x84: case 0x85: case 0x86: case 0x87: case 0x88: case 0x89: case 0x8A: case 0x8B: case 0x8C: case 0x8D: case 0x8E: case 0x8F: this.send(this.method.methodGetSelector(b&0xF), 0, false); return; case 0x90: case 0x91: case 0x92: case 0x93: case 0x94: case 0x95: case 0x96: case 0x97: case 0x98: case 0x99: case 0x9A: case 0x9B: case 0x9C: case 0x9D: case 0x9E: case 0x9F: this.send(this.method.methodGetSelector(b&0xF), 1, false); return; case 0xA0: case 0xA1: case 0xA2: case 0xA3: case 0xA4: case 0xA5: case 0xA6: case 0xA7: case 0xA8: case 0xA9: case 0xAA: case 0xAB: case 0xAC: case 0xAD: case 0xAE: case 0xAF: this.send(this.method.methodGetSelector(b&0xF), 2, false); return; // Short jmp case 0xB0: case 0xB1: case 0xB2: case 0xB3: case 0xB4: case 0xB5: case 0xB6: case 0xB7: this.pc += (b&7)+1; return; // Short conditional jump on true case 0xB8: case 0xB9: case 0xBA: case 0xBB: case 0xBC: case 0xBD: case 0xBE: case 0xBF: this.jumpIfTrue((b&7)+1); return; // Short conditional jump on false case 0xC0: case 0xC1: case 0xC2: case 0xC3: case 0xC4: case 0xC5: case 0xC6: case 0xC7: this.jumpIfFalse((b&7)+1); return; // 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; 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; case 0xD8: this.pop(); return; // pop case 0xD9: this.nono(); return; // FIXME: Unconditional trap case 0xDA: case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF: this.nono(); return; // unused // 2 Byte Bytecodes case 0xE0: b2 = this.nextByte(); this.interpretOneSistaWithExtensions(singleStep, (extA << 8) + b2, extB); return; case 0xE1: b2 = this.nextByte(); this.interpretOneSistaWithExtensions(singleStep, extA, (extB << 8) + (b2 < 128 ? b2 : b2-256)); return; case 0xE2: b2 = this.nextByte(); this.push(this.receiver.pointers[b2 + (extA << 8)]); return; case 0xE3: b2 = this.nextByte(); this.push((this.method.methodGetLiteral(b2 + (extA << 8))).pointers[Squeak.Assn_value]); return; 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; 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 case 0xE9: b2 = this.nextByte(); this.push(this.image.getCharacter(b2 + (extB << 8))); return; // push Character case 0xEA: b2 = this.nextByte(); this.send(this.method.methodGetSelector((b2 >> 3) + (extA << 5)), (b2 & 7) + (extB << 3), false); return; case 0xEB: b2 = this.nextByte(); var literal = this.method.methodGetSelector((b2 >> 3) + (extA << 5)); if (extB >= 64) { this.sendSuperDirected(literal, (b2 & 7) + ((extB & 63) << 3)); return; } else { this.send(literal, (b2 & 7) + (extB << 3), true); return; } case 0xEC: this.nono(); return; // unused case 0xED: // long jump, forward and back var offset = this.nextByte() + (extB << 8); this.pc += offset; if (offset < 0) // check for process switch on backward jumps (loops) if (this.interruptCheckCounter-- <= 0) this.checkForInterrupts(); return; case 0xEE: // long conditional jump on true this.jumpIfTrue(this.nextByte() + (extB << 8)); return; case 0xEF: // long conditional jump on false 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(); return; case 0xF1: // pop into literal var assoc = this.method.methodGetLiteral(this.nextByte() + (extA << 8)); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.pop(); return; case 0xF2: // pop into temp this.homeContext.pointers[Squeak.Context_tempFrameStart + this.nextByte()] = this.pop(); return; case 0xF3: // store into receiver this.receiver.dirty = true; this.receiver.pointers[this.nextByte() + (extA << 8)] = this.top(); return; case 0xF4: // store into literal var assoc = this.method.methodGetLiteral(this.nextByte() + (extA << 8)); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.top(); return; case 0xF5: // store into temp this.homeContext.pointers[Squeak.Context_tempFrameStart + this.nextByte()] = this.top(); return; case 0xF6: case 0xF7: this.nono(); return; // unused // 3 Byte Bytecodes case 0xF8: this.callPrimBytecode(0xF5); return; 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]); return; case 0xFC: b2 = this.nextByte(); // remote store into temp vector var vec = this.homeContext.pointers[Squeak.Context_tempFrameStart+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()]; vec.pointers[b2] = this.pop(); vec.dirty = true; return; case 0xFE: case 0xFF: this.nono(); return; // unused } throw Error("not a bytecode: " + b); }, interpret: function(forMilliseconds, thenDo) { // run for a couple milliseconds (but only until idle or break) // answer milliseconds to sleep (until next timer wakeup) // or 'break' if reached breakpoint // call thenDo with that result when done if (this.frozen) return 'frozen'; this.isIdle = false; this.breakOutOfInterpreter = false; this.breakAfter(forMilliseconds || 500); while (this.breakOutOfInterpreter === false) if (this.method.compiled) { this.method.compiled(this); } else { this.interpretOne(); } // this is to allow 'freezing' the interpreter and restarting it asynchronously. See freeze() if (typeof this.breakOutOfInterpreter == "function") return this.breakOutOfInterpreter(thenDo); // normally, we answer regularly var result = this.breakOutOfInterpreter == 'break' ? 'break' : !this.isIdle ? 0 : !this.nextWakeupTick ? 'sleep' // all processes waiting : Math.max(1, this.nextWakeupTick - this.primHandler.millisecondClockValue()); if (thenDo) thenDo(result); return result; }, goIdle: function() { // make sure we tend to pending delays var hadTimer = this.nextWakeupTick !== 0; this.forceInterruptCheck(); this.checkForInterrupts(); var hasTimer = this.nextWakeupTick !== 0; // go idle unless a timer just expired this.isIdle = hasTimer || !hadTimer; this.breakOut(); }, freeze: function(frozenDo) { // Stop the interpreter. Answer a function that can be // called to continue interpreting. // Optionally, frozenDo is called asynchronously when frozen var continueFunc; this.frozen = true; this.breakOutOfInterpreter = function(thenDo) { if (!thenDo) throw Error("need function to restart interpreter"); continueFunc = thenDo; return "frozen"; }.bind(this); var unfreeze = function() { this.frozen = false; if (!continueFunc) throw Error("no continue function"); continueFunc(0); //continue without timeout }.bind(this); if (frozenDo) self.setTimeout(function(){frozenDo(unfreeze)}, 0); return unfreeze; }, breakOut: function() { this.breakOutOfInterpreter = this.breakOutOfInterpreter || true; // do not overwrite break string }, nextByte: function() { return this.method.bytes[this.pc++]; }, nono: function() { throw Error("Oh No!"); }, forceInterruptCheck: function() { this.interruptCheckCounter = -1000; }, checkForInterrupts: function() { //Check for interrupts at sends and backward jumps var now = this.primHandler.millisecondClockValue(); if (now < this.lastTick) { // millisecond clock wrapped this.nextPollTick = now + (this.nextPollTick - this.lastTick); this.breakOutTick = now + (this.breakOutTick - this.lastTick); if (this.nextWakeupTick !== 0) this.nextWakeupTick = now + (this.nextWakeupTick - this.lastTick); } //Feedback logic attempts to keep interrupt response around 3ms... if (this.interruptCheckCounter > -100) { // only if not a forced check if ((now - this.lastTick) < this.interruptChecksEveryNms) { //wrapping is not a concern this.interruptCheckCounterFeedBackReset += 10; } else { // do a thousand sends even if we are too slow for 3ms if (this.interruptCheckCounterFeedBackReset <= 1000) this.interruptCheckCounterFeedBackReset = 1000; else this.interruptCheckCounterFeedBackReset -= 12; } } this.interruptCheckCounter = this.interruptCheckCounterFeedBackReset; //reset the interrupt check counter this.lastTick = now; //used to detect wraparound of millisecond clock if (this.signalLowSpace) { this.signalLowSpace = false; // reset flag var sema = this.specialObjects[Squeak.splOb_TheLowSpaceSemaphore]; if (!sema.isNil) this.primHandler.synchronousSignal(sema); } // if (now >= nextPollTick) { // ioProcessEvents(); //sets interruptPending if interrupt key pressed // nextPollTick= now + 500; } //msecs to wait before next call to ioProcessEvents" if (this.interruptPending) { this.interruptPending = false; //reset interrupt flag var sema = this.specialObjects[Squeak.splOb_TheInterruptSemaphore]; if (!sema.isNil) this.primHandler.synchronousSignal(sema); } if ((this.nextWakeupTick !== 0) && (now >= this.nextWakeupTick)) { this.nextWakeupTick = 0; //reset timer interrupt var sema = this.specialObjects[Squeak.splOb_TheTimerSemaphore]; if (!sema.isNil) this.primHandler.synchronousSignal(sema); } if (this.pendingFinalizationSignals > 0) { //signal any pending finalizations var sema = this.specialObjects[Squeak.splOb_TheFinalizationSemaphore]; this.pendingFinalizationSignals = 0; if (!sema.isNil) this.primHandler.synchronousSignal(sema); } if (this.primHandler.semaphoresToSignal.length > 0) this.primHandler.signalExternalSemaphores(); // signal pending semaphores, if any // if this is a long-running do-it, compile it if (!this.method.compiled) this.compileIfPossible(this.method); // have to return to web browser once in a while if (now >= this.breakOutTick) this.breakOut(); }, extendedPush: function(nextByte) { 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 2: this.push(this.method.methodGetLiteral(lobits)); break; case 3: this.push(this.method.methodGetLiteral(lobits).pointers[Squeak.Assn_value]); break; } }, extendedStore: function( nextByte) { var lobits = nextByte & 63; switch (nextByte>>6) { case 0: this.receiver.dirty = true; this.receiver.pointers[lobits] = this.top(); break; case 1: this.homeContext.pointers[Squeak.Context_tempFrameStart+lobits] = this.top(); break; case 2: this.nono(); break; case 3: var assoc = this.method.methodGetLiteral(lobits); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.top(); break; } }, extendedStorePop: function(nextByte) { var lobits = nextByte & 63; switch (nextByte>>6) { case 0: this.receiver.dirty = true; this.receiver.pointers[lobits] = this.pop(); break; case 1: this.homeContext.pointers[Squeak.Context_tempFrameStart+lobits] = this.pop(); break; case 2: this.nono(); break; case 3: var assoc = this.method.methodGetLiteral(lobits); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.pop(); break; } }, doubleExtendedDoAnything: function(byte2) { var byte3 = this.nextByte(); switch (byte2>>5) { case 0: this.send(this.method.methodGetSelector(byte3), byte2&31, false); break; case 1: this.send(this.method.methodGetSelector(byte3), byte2&31, true); break; 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 7: var assoc = this.method.methodGetLiteral(byte3); assoc.dirty = true; assoc.pointers[Squeak.Assn_value] = this.top(); break; } }, jumpIfTrue: function(delta) { var top = this.pop(); if (top.isTrue) {this.pc += delta; return;} if (top.isFalse) return; this.push(top); //Uh-oh it's not even a boolean (that we know of ;-). Restore stack... this.send(this.specialObjects[Squeak.splOb_SelectorMustBeBoolean], 0, false); }, jumpIfFalse: function(delta) { var top = this.pop(); if (top.isFalse) {this.pc += delta; return;} if (top.isTrue) return; this.push(top); //Uh-oh it's not even a boolean (that we know of ;-). Restore stack... this.send(this.specialObjects[Squeak.splOb_SelectorMustBeBoolean], 0, false); }, sendSpecial: function(lobits) { this.send(this.specialSelectors[lobits*2], this.specialSelectors[(lobits*2)+1], false); //specialSelectors is {...sel,nArgs,sel,nArgs,...) }, callPrimBytecode: function(extendedStoreBytecode) { this.pc += 2; // skip over primitive number if (this.primFailCode) { if (this.method.bytes[this.pc] === extendedStoreBytecode) this.stackTopPut(this.getErrorObjectFromPrimFailCode()); this.primFailCode = 0; } }, getErrorObjectFromPrimFailCode: function() { var primErrTable = this.specialObjects[Squeak.splOb_PrimErrTableIndex]; if (primErrTable && primErrTable.pointers) { var errorObject = primErrTable.pointers[this.primFailCode - 1]; if (errorObject) return errorObject; } return this.primFailCode; }, }, 'closures', { pushNewArray: function(nextByte) { var popValues = nextByte > 127, count = nextByte & 127, array = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassArray], count); if (popValues) { for (var i = 0; i < count; i++) array.pointers[i] = this.stackValue(count - i - 1); this.popN(count); } this.push(array); }, pushClosureCopy: function() { // The compiler has pushed the values to be copied, if any. Find numArgs and numCopied in the byte following. // Create a Closure with space for the copiedValues and pop numCopied values off the stack into the closure. // Set numArgs as specified, and set startpc to the pc following the block size and jump over that code. var numArgsNumCopied = this.nextByte(), numArgs = numArgsNumCopied & 0xF, numCopied = numArgsNumCopied >> 4, blockSizeHigh = this.nextByte(), 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; this.reclaimableContextCount = 0; // The closure refers to thisContext so it can't be reclaimed if (numCopied > 0) { for (var i = 0; i < numCopied; i++) closure.pointers[Squeak.Closure_firstCopiedValue + i] = this.stackValue(numCopied - i - 1); this.popN(numCopied); } this.pc += blockSize; this.push(closure); }, pushClosureCopyExtended: function(extA, extB) { var byteA = this.nextByte(); var byteB = this.nextByte(); var numArgs = (byteA & 7) + this.mod(extA, 16) * 8, numCopied = (byteA >> 3 & 0x7) + this.div(extA, 16) * 8, blockSize = byteB + (extB << 8), initialPC = this.encodeSqueakPC(this.pc, this.method), closure = this.newClosure(numArgs, initialPC, numCopied); closure.pointers[Squeak.Closure_outerContext] = this.activeContext; this.reclaimableContextCount = 0; // The closure refers to thisContext so it can't be reclaimed if (numCopied > 0) { for (var i = 0; i < numCopied; i++) closure.pointers[Squeak.Closure_firstCopiedValue + i] = this.stackValue(numCopied - i - 1); this.popN(numCopied); } this.pc += blockSize; this.push(closure); }, pushFullClosure: function(extA) { var byteA = this.nextByte(); var byteB = this.nextByte(); var literalIndex = byteA + (extA << 8); var numCopied = byteB & 63; var context; if ((byteB >> 6 & 1) == 1) { context = this.vm.nilObj; } else { context = this.activeContext; } var compiledBlock = this.method.methodGetLiteral(literalIndex); var closure = this.newFullClosure(context, numCopied, compiledBlock); if ((byteB >> 7 & 1) == 1) { throw Error("on-stack receiver not yet supported"); } else { closure.pointers[Squeak.ClosureFull_receiver] = this.receiver; } this.reclaimableContextCount = 0; // The closure refers to thisContext so it can't be reclaimed if (numCopied > 0) { for (var i = 0; i < numCopied; i++) closure.pointers[Squeak.ClosureFull_firstCopiedValue + i] = this.stackValue(numCopied - i - 1); this.popN(numCopied); } this.push(closure); }, newClosure: function(numArgs, initialPC, numCopied) { var closure = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassBlockClosure], numCopied); closure.pointers[Squeak.Closure_startpc] = initialPC; closure.pointers[Squeak.Closure_numArgs] = numArgs; return closure; }, newFullClosure: function(context, numCopied, compiledBlock) { var closure = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassFullBlockClosure], numCopied); closure.pointers[Squeak.Closure_outerContext] = context; closure.pointers[Squeak.ClosureFull_method] = compiledBlock; closure.pointers[Squeak.Closure_numArgs] = compiledBlock.methodNumArgs(); return closure; }, }, 'sending', { send: function(selector, argCount, doSuper) { var newRcvr = this.stackValue(argCount); var lookupClass; if (doSuper) { lookupClass = this.method.methodClassForSuper(); lookupClass = lookupClass.superclass(); } else { lookupClass = this.getClass(newRcvr); } var entry = this.findSelectorInClass(selector, argCount, lookupClass); if (entry.primIndex) { //note details for verification of at/atput primitives this.verifyAtSelector = selector; this.verifyAtClass = lookupClass; } this.executeNewMethod(newRcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); }, sendSuperDirected: function(selector, argCount) { var lookupClass = this.pop().superclass(); var newRcvr = this.stackValue(argCount); var entry = this.findSelectorInClass(selector, argCount, lookupClass); if (entry.primIndex) { //note details for verification of at/atput primitives this.verifyAtSelector = selector; this.verifyAtClass = lookupClass; } this.executeNewMethod(newRcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); }, sendAsPrimitiveFailure: function(rcvr, method, argCount) { this.executeNewMethod(rcvr, method, argCount, 0); }, /** * @param {*} trueArgCount The number of arguments for the method to be found * @param {*} argCount The number of arguments currently on the stack (may be different from trueArgCount in the context of primitive 84 etc.) */ findSelectorInClass: function(selector, trueArgCount, startingClass, argCount = trueArgCount) { this.currentSelector = selector; // for primitiveInvokeObjectAsMethod var cacheEntry = this.findMethodCacheEntry(selector, startingClass); if (cacheEntry.method) return cacheEntry; // Found it in the method cache var currentClass = startingClass; var mDict; while (!currentClass.isNil) { mDict = currentClass.pointers[Squeak.Class_mdict]; if (mDict.isNil) { // MethodDict pointer is nil (hopefully due a swapped out stub) // -- send #cannotInterpret: var cantInterpSel = this.specialObjects[Squeak.splOb_SelectorCannotInterpret], cantInterpMsg = this.createActualMessage(selector, trueArgCount, startingClass); this.popNandPush(argCount + 1, cantInterpMsg); return this.findSelectorInClass(cantInterpSel, 1, currentClass.superclass()); } var newMethod = this.lookupSelectorInDict(mDict, selector); if (!newMethod.isNil) { cacheEntry.method = newMethod; if (newMethod.isMethod()) { cacheEntry.primIndex = newMethod.methodPrimitiveIndex(); cacheEntry.argCount = newMethod.methodNumArgs(); } else { // if method is not actually a CompiledMethod, let primitiveInvokeObjectAsMethod (576) handle it cacheEntry.primIndex = 576; cacheEntry.argCount = trueArgCount; } cacheEntry.mClass = currentClass; return cacheEntry; } currentClass = currentClass.superclass(); } //Cound not find a normal message -- send #doesNotUnderstand: var dnuSel = this.specialObjects[Squeak.splOb_SelectorDoesNotUnderstand]; if (selector === dnuSel) // Cannot find #doesNotUnderstand: -- unrecoverable error. throw Error("Recursive not understood error encountered"); var dnuMsg = this.createActualMessage(selector, trueArgCount, startingClass); // The argument to doesNotUnderstand: if (this.breakOnMessageNotUnderstood) { var receiver = this.stackValue(argCount); this.breakNow("Message not understood: " + receiver + " " + startingClass.className() + ">>" + selector.bytesAsString()); } this.popNandPush(argCount, dnuMsg); return this.findSelectorInClass(dnuSel, 1, startingClass); }, lookupSelectorInDict: function(mDict, messageSelector) { //Returns a method or nilObject var dictSize = mDict.pointersSize(); var mask = (dictSize - Squeak.MethodDict_selectorStart) - 1; var index = (mask & messageSelector.hash) + Squeak.MethodDict_selectorStart; // If there are no nils (should always be), then stop looping on second wrap. var hasWrapped = false; while (true) { var nextSelector = mDict.pointers[index]; if (nextSelector === messageSelector) { var methArray = mDict.pointers[Squeak.MethodDict_array]; return methArray.pointers[index - Squeak.MethodDict_selectorStart]; } if (nextSelector.isNil) return this.nilObj; if (++index === dictSize) { if (hasWrapped) return this.nilObj; index = Squeak.MethodDict_selectorStart; hasWrapped = true; } } }, executeNewMethod: 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 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); console.log(this.sendCount + indent + 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 newContext = this.allocateOrRecycleContext(newMethod.methodNeedsLargeFrame()); var tempCount = newMethod.methodTempCount(); var newPC = 0; // direct zero-based index into byte codes var newSP = Squeak.Context_tempFrameStart + tempCount - 1; // direct zero-based index into context pointers newContext.pointers[Squeak.Context_method] = newMethod; //Following store is in case we alloc without init; all other fields get stored newContext.pointers[Squeak.BlockContext_initialIP] = this.nilObj; 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); //...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); this.reclaimableContextCount++; this.storeContextRegisters(); /////// Woosh ////// this.activeContext = newContext; //We're off and running... //Following are more efficient than fetchContextRegisters() in newActiveContext() this.activeContext.dirty = true; this.homeContext = newContext; this.method = newMethod; this.pc = newPC; this.sp = newSP; this.receiver = newContext.pointers[Squeak.Context_receiver]; if (this.receiver !== newRcvr) throw Error("receivers don't match"); if (!newMethod.compiled) this.compileIfPossible(newMethod, optClass, optSel); // check for process switch on full method activation if (this.interruptCheckCounter-- <= 0) this.checkForInterrupts(); }, compileIfPossible(newMethod, optClass, optSel) { if (!newMethod.compiled && this.compiler) { this.compiler.compile(newMethod, optClass, optSel); } }, doReturn: function(returnValue, targetContext) { // get sender from block home or closure's outerContext if (!targetContext) { var ctx = this.homeContext; if (this.hasClosures) { var closure; while (!(closure = ctx.pointers[Squeak.Context_closure]).isNil) ctx = closure.pointers[Squeak.Closure_outerContext]; } targetContext = ctx.pointers[Squeak.Context_sender]; } if (targetContext.isNil || targetContext.pointers[Squeak.Context_instructionPointer].isNil) return this.cannotReturn(returnValue); // search up stack for unwind var thisContext = this.activeContext.pointers[Squeak.Context_sender]; while (thisContext !== targetContext) { if (thisContext.isNil) return this.cannotReturn(returnValue); if (this.isUnwindMarked(thisContext)) return this.aboutToReturnThrough(returnValue, thisContext); thisContext = thisContext.pointers[Squeak.Context_sender]; } // no unwind to worry about, just peel back the stack (usually just to sender) var nextContext; thisContext = this.activeContext; while (thisContext !== targetContext) { if (this.breakOnContextReturned === thisContext) { this.breakOnContextReturned = null; this.breakNow(); } nextContext = thisContext.pointers[Squeak.Context_sender]; thisContext.pointers[Squeak.Context_sender] = this.nilObj; thisContext.pointers[Squeak.Context_instructionPointer] = this.nilObj; if (this.reclaimableContextCount > 0) { this.reclaimableContextCount--; this.recycleIfPossible(thisContext); } thisContext = nextContext; } this.activeContext = thisContext; this.activeContext.dirty = true; this.fetchContextRegisters(this.activeContext); this.push(returnValue); if (this.breakOnContextChanged) { this.breakOnContextChanged = false; this.breakNow(); } }, aboutToReturnThrough: function(resultObj, aContext) { this.push(this.exportThisContext()); this.push(resultObj); this.push(aContext); var aboutToReturnSel = this.specialObjects[Squeak.splOb_SelectorAboutToReturn]; this.send(aboutToReturnSel, 2); }, cannotReturn: function(resultObj) { this.push(this.exportThisContext()); this.push(resultObj); var cannotReturnSel = this.specialObjects[Squeak.splOb_SelectorCannotReturn]; this.send(cannotReturnSel, 1); }, tryPrimitive: function(primIndex, argCount, newMethod) { 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 context = this.activeContext; var success = this.primHandler.doPrimitive(primIndex, argCount, newMethod); if (success && this.sp !== sp - argCount && context === this.activeContext && primIndex !== 117 // named prims are checked separately (see namedPrimitive) && primIndex !== 118 // primitiveDoPrimitiveWithArgs (will call tryPrimitive again) && primIndex !== 218 // primitiveDoNamedPrimitive (will call namedPrimitive) && !this.frozen) { this.warnOnce("stack unbalanced after primitive " + primIndex, "error"); } return success; }, createActualMessage: function(selector, argCount, cls) { //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 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 message.pointers[Squeak.Message_lookupClass] = cls; return message; }, primitivePerform: function(argCount) { var selector = this.stackValue(argCount-1); var rcvr = this.stackValue(argCount); var trueArgCount = argCount - 1; var entry = this.findSelectorInClass(selector, trueArgCount, this.getClass(rcvr), argCount); if (entry.selector === selector) { // selector has been found, rearrange stack if (entry.argCount !== trueArgCount) return false; var stack = this.activeContext.pointers; // slide eveything down... var selectorIndex = this.sp - trueArgCount; this.arrayCopy(stack, selectorIndex+1, stack, selectorIndex, trueArgCount); this.sp--; // adjust sp accordingly } else { // stack has already been arranged for #doesNotUnderstand:/#cannotInterpret: rcvr = this.stackValue(entry.argCount); } this.executeNewMethod(rcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); return true; }, primitivePerformWithArgs: function(argCount, supered) { var rcvrPos = supered ? 3 : 2; var rcvr = this.stackValue(rcvrPos); var selector = this.stackValue(rcvrPos - 1); var args = this.stackValue(rcvrPos - 2); if (args.sqClass !== this.specialObjects[Squeak.splOb_ClassArray]) return false; var lookupClass = supered ? this.top() : this.getClass(rcvr); if (supered) { // verify that lookupClass is in fact in superclass chain of receiver; var cls = this.getClass(rcvr); while (cls !== lookupClass) { cls = cls.superclass(); if (cls.isNil) return false; } } var trueArgCount = args.pointersSize(); var entry = this.findSelectorInClass(selector, trueArgCount, lookupClass, argCount); if (entry.selector === selector) { // selector has been found, rearrange stack if (entry.argCount !== trueArgCount) return false; var stack = this.activeContext.pointers; var selectorIndex = this.sp - (argCount - 1); stack[selectorIndex - 1] = rcvr; this.arrayCopy(args.pointers, 0, stack, selectorIndex, trueArgCount); this.sp += trueArgCount - argCount; // pop old args then push new args } else { // stack has already been arranged for #doesNotUnderstand: or #cannotInterpret: rcvr = this.stackValue(entry.argCount); } this.executeNewMethod(rcvr, entry.method, entry.argCount, entry.primIndex, entry.mClass, selector); return true; }, primitiveInvokeObjectAsMethod: function(argCount, method) { // invoked from VM if non-method found in lookup var orgArgs = this.instantiateClass(this.specialObjects[Squeak.splOb_ClassArray], argCount); for (var i = 0; i < argCount; i++) orgArgs.pointers[argCount - i - 1] = this.pop(); var orgReceiver = this.pop(), orgSelector = this.currentSelector; // send run:with:in: to non-method object var runWithIn = this.specialObjects[Squeak.splOb_SelectorRunWithIn]; this.push(method); // not actually a method this.push(orgSelector); this.push(orgArgs); this.push(orgReceiver); this.send(runWithIn, 3, false); return true; }, findMethodCacheEntry: function(selector, lkupClass) { //Probe the cache, and return the matching entry if found //Otherwise return one that can be used (selector and class set) with method == null. //Initial probe is class xor selector, reprobe delta is selector //We do not try to optimize probe time -- all are equally 'fast' compared to lookup //Instead we randomize the reprobe so two or three very active conflicting entries //will not keep dislodging each other var entry; this.methodCacheRandomish = (this.methodCacheRandomish + 1) & 3; var firstProbe = (selector.hash ^ lkupClass.hash) & this.methodCacheMask; var probe = firstProbe; for (var i = 0; i < 4; i++) { // 4 reprobes for now entry = this.methodCache[probe]; if (entry.selector === selector && entry.lkupClass === lkupClass) return entry; if (i === this.methodCacheRandomish) firstProbe = probe; probe = (probe + selector.hash) & this.methodCacheMask; } entry = this.methodCache[firstProbe]; entry.lkupClass = lkupClass; entry.selector = selector; entry.method = null; return entry; }, flushMethodCache: function() { //clear all cache entries (prim 89) for (var i = 0; i < this.methodCacheSize; i++) { this.methodCache[i].selector = null; // mark it free this.methodCache[i].method = null; // release the method } return true; }, flushMethodCacheForSelector: function(selector) { //clear cache entries for selector (prim 119) for (var i = 0; i < this.methodCacheSize; i++) if (this.methodCache[i].selector === selector) { this.methodCache[i].selector = null; // mark it free this.methodCache[i].method = null; // release the method } return true; }, flushMethodCacheForMethod: function(method) { //clear cache entries for method (prim 116) for (var i = 0; i < this.methodCacheSize; i++) if (this.methodCache[i].method === method) { this.methodCache[i].selector = null; // mark it free this.methodCache[i].method = null; // release the method } return true; }, flushMethodCacheAfterBecome: function(mutations) { // could be selective by checking lkupClass, selector, // and method against mutations dict this.flushMethodCache(); }, }, 'contexts', { isUnwindMarked: function(ctx) { if (!this.isMethodContext(ctx)) return false; var method = ctx.pointers[Squeak.Context_method]; return method.methodPrimitiveIndex() == 198; }, newActiveContext: function(newContext) { // Note: this is inlined in executeNewMethod() and doReturn() this.storeContextRegisters(); this.activeContext = newContext; //We're off and running... this.activeContext.dirty = true; this.fetchContextRegisters(newContext); if (this.breakOnContextChanged) { this.breakOnContextChanged = false; this.breakNow(); } }, exportThisContext: function() { this.storeContextRegisters(); this.reclaimableContextCount = 0; return this.activeContext; }, fetchContextRegisters: function(ctxt) { var meth = ctxt.pointers[Squeak.Context_method]; if (this.isSmallInt(meth)) { //if the Method field is an integer, activeCntx is a block context this.homeContext = ctxt.pointers[Squeak.BlockContext_home]; meth = this.homeContext.pointers[Squeak.Context_method]; } else { //otherwise home==ctxt this.homeContext = ctxt; } this.receiver = this.homeContext.pointers[Squeak.Context_receiver]; this.method = meth; this.pc = this.decodeSqueakPC(ctxt.pointers[Squeak.Context_instructionPointer], meth); this.sp = this.decodeSqueakSP(ctxt.pointers[Squeak.Context_stackPointer]); }, storeContextRegisters: function() { //Save pc, sp into activeContext object, prior to change of context // see fetchContextRegisters for symmetry // expects activeContext, pc, sp, and method state vars to still be valid this.activeContext.pointers[Squeak.Context_instructionPointer] = this.encodeSqueakPC(this.pc, this.method); this.activeContext.pointers[Squeak.Context_stackPointer] = this.encodeSqueakSP(this.sp); }, encodeSqueakPC: function(intPC, method) { // Squeak pc is offset by header and literals // and 1 for z-rel addressing return intPC + method.pointers.length * 4 + 1; }, decodeSqueakPC: function(squeakPC, method) { return squeakPC - method.pointers.length * 4 - 1; }, encodeSqueakSP: function(intSP) { // sp is offset by tempFrameStart, -1 for z-rel addressing return intSP - (Squeak.Context_tempFrameStart - 1); }, decodeSqueakSP: function(squeakSP) { return squeakSP + (Squeak.Context_tempFrameStart - 1); }, recycleIfPossible: function(ctxt) { if (!this.isMethodContext(ctxt)) return; if (ctxt.pointersSize() === (Squeak.Context_tempFrameStart+Squeak.Context_smallFrameSize)) { // Recycle small contexts ctxt.pointers[0] = this.freeContexts; this.freeContexts = ctxt; } else { // Recycle large contexts if (ctxt.pointersSize() !== (Squeak.Context_tempFrameStart+Squeak.Context_largeFrameSize)) return; ctxt.pointers[0] = this.freeLargeContexts; this.freeLargeContexts = ctxt; } }, allocateOrRecycleContext: function(needsLarge) { //Return a recycled context or a newly allocated one if none is available for recycling." var freebie; if (needsLarge) { if (!this.freeLargeContexts.isNil) { freebie = this.freeLargeContexts; this.freeLargeContexts = freebie.pointers[0]; this.nRecycledContexts++; return freebie; } this.nAllocatedContexts++; return this.instantiateClass(this.specialObjects[Squeak.splOb_ClassMethodContext], Squeak.Context_largeFrameSize); } else { if (!this.freeContexts.isNil) { freebie = this.freeContexts; this.freeContexts = freebie.pointers[0]; this.nRecycledContexts++; return freebie; } this.nAllocatedContexts++; return this.instantiateClass(this.specialObjects[Squeak.splOb_ClassMethodContext], Squeak.Context_smallFrameSize); } }, }, 'stack access', { pop: function() { //Note leaves garbage above SP. Cleaned out by fullGC. return this.activeContext.pointers[this.sp--]; }, popN: function(nToPop) { this.sp -= nToPop; }, push: function(object) { this.activeContext.pointers[++this.sp] = object; }, popNandPush: function(nToPop, object) { this.activeContext.pointers[this.sp -= nToPop - 1] = object; }, top: function() { return this.activeContext.pointers[this.sp]; }, stackTopPut: function(object) { this.activeContext.pointers[this.sp] = object; }, stackValue: function(depthIntoStack) { return this.activeContext.pointers[this.sp - depthIntoStack]; }, stackInteger: function(depthIntoStack) { return this.checkSmallInt(this.stackValue(depthIntoStack)); }, stackIntOrFloat: function(depthIntoStack) { var num = this.stackValue(depthIntoStack); // is it a SmallInt? if (typeof num === "number") return num; if (num === undefined) {this.success = false; return 0;} // is it a Float? if (num.isFloat) { this.resultIsFloat = true; // need to return result as Float return num.float; } // maybe a 32-bit LargeInt? var bytes = num.bytes; if (bytes && bytes.length == 4) { var value = 0; for (var i = 3; i >= 0; i--) value = value * 256 + bytes[i]; if (num.sqClass === this.specialObjects[Squeak.splOb_ClassLargePositiveInteger]) return value; if (num.sqClass === this.specialObjects[Squeak.splOb_ClassLargeNegativeInteger]) return -value; } // none of the above this.success = false; return 0; }, pop2AndPushIntResult: function(intResult) {// returns success boolean if (this.success && this.canBeSmallInt(intResult)) { this.popNandPush(2, intResult); return true; } return false; }, pop2AndPushNumResult: function(numResult) {// returns success boolean if (this.success) { if (this.resultIsFloat) { this.popNandPush(2, this.primHandler.makeFloat(numResult)); return true; } if (numResult >= Squeak.MinSmallInt && numResult <= Squeak.MaxSmallInt) { this.popNandPush(2, numResult); return true; } if (numResult >= -0xFFFFFFFF && numResult <= 0xFFFFFFFF) { var negative = numResult < 0, unsigned = negative ? -numResult : numResult, lgIntClass = negative ? Squeak.splOb_ClassLargeNegativeInteger : Squeak.splOb_ClassLargePositiveInteger, lgIntObj = this.instantiateClass(this.specialObjects[lgIntClass], 4), bytes = lgIntObj.bytes; bytes[0] = unsigned & 255; bytes[1] = unsigned>>8 & 255; bytes[2] = unsigned>>16 & 255; bytes[3] = unsigned>>24 & 255; this.popNandPush(2, lgIntObj); return true; } } return false; }, pop2AndPushBoolResult: function(boolResult) { if (!this.success) return false; this.popNandPush(2, boolResult ? this.trueObj : this.falseObj); return true; }, }, 'numbers', { getClass: function(obj) { if (this.isSmallInt(obj)) return this.specialObjects[Squeak.splOb_ClassInteger]; return obj.sqClass; }, canBeSmallInt: function(anInt) { return (anInt >= Squeak.MinSmallInt) && (anInt <= Squeak.MaxSmallInt); }, isSmallInt: function(object) { return typeof object === "number"; }, checkSmallInt: function(maybeSmallInt) { // returns an int and sets success if (typeof maybeSmallInt === "number") return maybeSmallInt; this.success = false; return 1; }, quickDivide: function(rcvr, arg) { // must only handle exact case if (arg === 0) return Squeak.NonSmallInt; // fail if divide by zero var result = rcvr / arg | 0; if (result * arg === rcvr) return result; return Squeak.NonSmallInt; // fail if result is not exact }, div: function(rcvr, arg) { if (arg === 0) return Squeak.NonSmallInt; // fail if divide by zero return Math.floor(rcvr/arg); }, mod: function(rcvr, arg) { if (arg === 0) return Squeak.NonSmallInt; // fail if divide by zero return rcvr - Math.floor(rcvr/arg) * arg; }, safeShift: function(smallInt, shiftCount) { // must only be used if smallInt is actually a SmallInt! // the logic is complex because JS shifts only up to 31 bits // and treats e.g. 1<<32 as 1<<0, so we have to do our own checks if (shiftCount < 0) { if (shiftCount < -31) return smallInt < 0 ? -1 : 0; // this would wrongly return a negative result if // smallInt >= 0x80000000, but the largest smallInt // is 0x3FFFFFFF so we're ok return smallInt >> -shiftCount; // OK to lose bits shifting right } if (shiftCount > 31) return smallInt === 0 ? 0 : Squeak.NonSmallInt; var shifted = smallInt << shiftCount; // check for lost bits by seeing if computation is reversible if ((shifted>>shiftCount) !== smallInt) return Squeak.NonSmallInt; // fail return shifted; // caller will check if still within SmallInt range }, }, 'utils', { isContext: function(obj) {//either block or methodContext if (obj.sqClass === this.specialObjects[Squeak.splOb_ClassMethodContext]) return true; if (obj.sqClass === this.specialObjects[Squeak.splOb_ClassBlockContext]) return true; return false; }, isMethodContext: function(obj) { return obj.sqClass === this.specialObjects[Squeak.splOb_ClassMethodContext]; }, instantiateClass: function(aClass, indexableSize) { return this.image.instantiateClass(aClass, indexableSize, this.nilObj); }, arrayFill: function(array, fromIndex, toIndex, value) { // assign value to range from fromIndex (inclusive) to toIndex (exclusive) for (var i = fromIndex; i < toIndex; i++) array[i] = value; }, arrayCopy: function(src, srcPos, dest, destPos, length) { // copy length elements from src at srcPos to dest at destPos if (src === dest && srcPos < destPos) for (var i = length - 1; i >= 0; i--) dest[destPos + i] = src[srcPos + i]; else for (var i = 0; i < length; i++) dest[destPos + i] = src[srcPos + i]; }, signalLowSpaceIfNecessary: function(bytesLeft) { if (bytesLeft < this.lowSpaceThreshold && this.lowSpaceThreshold > 0) { var increase = prompt && prompt("Out of memory, " + Math.ceil(this.image.totalMemory/1000000) + " MB used.\nEnter additional MB, or 0 to signal low space in image", "0"); if (increase) { var bytes = parseInt(increase, 10) * 1000000; this.image.totalMemory += bytes; this.signalLowSpaceIfNecessary(this.image.bytesLeft()); } else { console.warn("squeak: low memory (" + bytesLeft + "/" + this.image.totalMemory + " bytes left), signaling low space"); this.signalLowSpace = true; this.lowSpaceThreshold = 0; var lastSavedProcess = this.specialObjects[Squeak.splOb_ProcessSignalingLowSpace]; if (lastSavedProcess.isNil) { this.specialObjects[Squeak.splOb_ProcessSignalingLowSpace] = this.primHandler.activeProcess(); } this.forceInterruptCheck(); } } }, }, 'debugging', { addMessage: function(message) { return this.messages[message] ? ++this.messages[message] : this.messages[message] = 1; }, warnOnce: function(message, what) { if (this.addMessage(message) == 1) { console[what || "warn"](message); return true; } }, printMethod: function(aMethod, optContext, optSel, optArgs) { // return a 'class>>selector' description for the method if (aMethod.sqClass != this.specialObjects[Squeak.splOb_ClassCompiledMethod]) { return this.printMethod(aMethod.blockOuterCode(), optContext, optSel, optArgs) } var found; if (optSel) { var printed = optContext.className() + '>>'; var selector = optSel.bytesAsString(); if (!optArgs || !optArgs.length) printed += selector; else { var parts = selector.split(/(?<=:)/); // keywords for (var i = 0; i < optArgs.length; i++) { if (i > 0) printed += ' '; printed += parts[i] + ' ' + optArgs[i]; } } return printed; } // this is expensive, we have to search all classes if (!aMethod) aMethod = this.activeContext.contextMethod(); this.allMethodsDo(function(classObj, methodObj, selectorObj) { if (methodObj === aMethod) return found = classObj.className() + '>>' + selectorObj.bytesAsString(); }); if (found) return found; if (optContext) { var rcvr = optContext.pointers[Squeak.Context_receiver]; return "(" + rcvr + ")>>?"; } return "?>>?"; }, allInstancesOf: function(classObj, callback) { if (typeof classObj === "string") classObj = this.globalNamed(classObj); var instances = [], inst = this.image.someInstanceOf(classObj); while (inst) { if (callback) callback(inst); else instances.push(inst); inst = this.image.nextInstanceAfter(inst); } return instances; }, globalNamed: function(name) { return this.allGlobalsDo(function(nameObj, globalObj){ if (nameObj.bytesAsString() === name) return globalObj; }); }, allGlobalsDo: function(callback) { // callback(globalNameObj, globalObj), truish result breaks out of iteration var globals = this.getGlobals(); for (var i = 0; i < globals.length; i++) { var assn = globals[i]; if (!assn.isNil) { var result = callback(assn.pointers[0], assn.pointers[1]); if (result) return result; } } }, allMethodsDo: function(callback) { // callback(classObj, methodObj, selectorObj), truish result breaks out of iteration this.allGlobalsDo(function(globalNameObj, globalObj) { if (globalObj.pointers && globalObj.pointers.length >= 9) { var clsAndMeta = [globalObj, globalObj.sqClass]; for (var c = 0; c < clsAndMeta.length; c++) { var cls = clsAndMeta[c]; var mdict = cls.pointers[1]; if (!mdict.pointers || !mdict.pointers[1]) continue; var methods = mdict.pointers[1].pointers; if (!methods) continue; var selectors = mdict.pointers; if (methods.length + 2 !== selectors.length) continue; for (var j = 0; j < methods.length; j++) { var method = methods[j]; var selector = selectors[2+j]; if (!method.isMethod || !method.isMethod()) continue; if (!selector.bytesSize || !selector.bytesSize()) continue; var result = callback.call(null, cls, method, selector); if (result) return true; } } } }); }, printStack: function(ctx, limit, indent) { // both args are optional if (typeof ctx == "number") {limit = ctx; ctx = null;} if (!ctx) ctx = this.activeContext; if (!limit) limit = 100; var contexts = [], hardLimit = Math.max(limit, 1000000); while (!ctx.isNil && hardLimit-- > 0) { contexts.push(ctx); ctx = ctx.pointers[Squeak.Context_sender]; } var extra = 200; if (contexts.length > limit + extra) { if (!ctx.isNil) contexts.push('...'); // over hard limit contexts = contexts.slice(0, limit).concat(['...']).concat(contexts.slice(-extra)); } var stack = [], i = contexts.length, indents = ''; if (indent && this.logSends) indents = Array((""+this.sendCount).length + 2).join(' '); while (i-- > 0) { var ctx = contexts[i]; if (!ctx.pointers) { stack.push('...\n'); } else { var block = '', method = ctx.pointers[Squeak.Context_method]; if (typeof method === 'number') { // it's a block context, fetch home method = ctx.pointers[Squeak.BlockContext_home].pointers[Squeak.Context_method]; block = '[] in '; } else if (!ctx.pointers[Squeak.Context_closure].isNil) { block = '[] in '; // it's a closure activation } var line = block + this.printMethod(method, ctx); if (indent) line = indents + line; stack.push(line + '\n'); if (indent) indents += indent; } } return stack.join(''); }, findMethod: function(classAndMethodString) { // classAndMethodString is 'Class>>method' var found, className = classAndMethodString.split('>>')[0], methodName = classAndMethodString.split('>>')[1]; this.allMethodsDo(function(classObj, methodObj, selectorObj) { if (methodName.length == selectorObj.bytesSize() && methodName == selectorObj.bytesAsString() && className == classObj.className()) return found = methodObj; }); return found; }, breakAfter: function(ms) { this.breakOutTick = this.primHandler.millisecondClockValue() + ms; }, breakNow: function(msg) { if (msg) console.log("Break: " + msg); this.breakOutOfInterpreter = 'break'; }, breakOn: function(classAndMethodString) { // classAndMethodString is 'Class>>method' return this.breakOnMethod = classAndMethodString && this.findMethod(classAndMethodString); }, breakOnReturnFromThisContext: function() { this.breakOnContextChanged = false; this.breakOnContextReturned = this.activeContext; }, breakOnSendOrReturn: function() { this.breakOnContextChanged = true; this.breakOnContextReturned = null; }, printContext: function(ctx, maxWidth) { if (!this.isContext(ctx)) return "NOT A CONTEXT: " + printObj(ctx); if (!maxWidth) maxWidth = 72; function printObj(obj) { var value = typeof obj === 'number' || typeof obj === 'object' ? obj.sqInstName() : "<" + obj + ">"; value = JSON.stringify(value).slice(1, -1); if (value.length > maxWidth - 3) value = value.slice(0, maxWidth - 3) + '...'; return value; } // temps and stack in current context var isBlock = typeof ctx.pointers[Squeak.BlockContext_argumentCount] === 'number'; var closure = ctx.pointers[Squeak.Context_closure]; var isClosure = !isBlock && !closure.isNil; var homeCtx = isBlock ? ctx.pointers[Squeak.BlockContext_home] : ctx; var tempCount = isClosure ? closure.pointers[Squeak.Closure_numArgs] : homeCtx.pointers[Squeak.Context_method].methodTempCount(); var stackBottom = this.decodeSqueakSP(0); var stackTop = homeCtx.contextSizeWithStack(this) - 1; var firstTemp = stackBottom + 1; var lastTemp = firstTemp + tempCount - 1; var lastArg = firstTemp + homeCtx.pointers[Squeak.Context_method].methodNumArgs() - 1; var stack = ''; for (var i = stackBottom; i <= stackTop; i++) { var value = printObj(homeCtx.pointers[i]); var label = ''; if (i === stackBottom) { label = '=rcvr'; } else { if (i <= lastTemp) label = '=tmp' + (i - firstTemp); if (i <= lastArg) label += '/arg' + (i - firstTemp); } stack += '\nctx[' + i + ']' + label +': ' + value; } if (isBlock) { stack += '\n'; var nArgs = ctx.pointers[Squeak.BlockContext_argumentCount]; var firstArg = this.decodeSqueakSP(1); var lastArg = firstArg + nArgs; var sp = ctx === this.activeContext ? this.sp : ctx.pointers[Squeak.Context_stackPointer]; if (sp < firstArg) stack += '\nblk '; for (var i = firstArg; i <= sp; i++) { var value = printObj(ctx.pointers[i]); var label = ''; if (i < lastArg) label = '=arg' + (i - firstArg); stack += '\nblk[' + i + ']' + label +': ' + value; } } return stack; }, printActiveContext: function(maxWidth) { return this.printContext(this.activeContext, maxWidth); }, printAllProcesses: function() { var schedAssn = this.specialObjects[Squeak.splOb_SchedulerAssociation], sched = schedAssn.pointers[Squeak.Assn_value]; // print active process var activeProc = sched.pointers[Squeak.ProcSched_activeProcess], result = "Active: " + this.printProcess(activeProc, true); // print other runnable processes in order of priority var lists = sched.pointers[Squeak.ProcSched_processLists].pointers; for (var priority = lists.length - 1; priority >= 0; priority--) { var process = lists[priority].pointers[Squeak.LinkedList_firstLink]; while (!process.isNil) { result += "\n------------------------------------------"; result += "\nRunnable: " + this.printProcess(process); process = process.pointers[Squeak.Link_nextLink]; } } // print all processes waiting on a semaphore in order of priority var semaClass = this.specialObjects[Squeak.splOb_ClassSemaphore], sema = this.image.someInstanceOf(semaClass), waiting = []; while (sema) { var process = sema.pointers[Squeak.LinkedList_firstLink]; while (!process.isNil) { waiting.push(process); process = process.pointers[Squeak.Link_nextLink]; } sema = this.image.nextInstanceAfter(sema); } waiting.sort(function(a, b){ return b.pointers[Squeak.Proc_priority] - a.pointers[Squeak.Proc_priority]; }); for (var i = 0; i < waiting.length; i++) { result += "\n------------------------------------------"; result += "\nWaiting: " + this.printProcess(waiting[i]); } return result; }, printProcess: function(process, active, indent) { if (!process) { var schedAssn = this.specialObjects[Squeak.splOb_SchedulerAssociation], sched = schedAssn.pointers[Squeak.Assn_value]; process = sched.pointers[Squeak.ProcSched_activeProcess], active = true; } var context = active ? this.activeContext : process.pointers[Squeak.Proc_suspendedContext], priority = process.pointers[Squeak.Proc_priority], stack = this.printStack(context, 20, indent), values = indent && this.logSends ? "" : this.printContext(context) + "\n"; return process.toString() +" at priority " + priority + "\n" + stack + values; }, printByteCodes: function(aMethod, optionalIndent, optionalHighlight, optionalPC) { if (!aMethod) aMethod = this.method; var printer = new Squeak.InstructionPrinter(aMethod, this); return printer.printInstructions(optionalIndent, optionalHighlight, optionalPC); }, logStack: function() { // useful for debugging interactively: // SqueakJS.vm.logStack() console.log(this.printStack() + this.printActiveContext() + '\n\n' + this.printByteCodes(this.method, '   ', '=> ', this.pc), this.activeContext.pointers.slice(0, this.sp + 1)); }, willSendOrReturn: function() { // Answer whether the next bytecode corresponds to a Smalltalk // message send or return var byte = this.method.bytes[this.pc]; if (this.method.methodSignFlag()) { if (0x60 <= byte && byte <= 0x7F) { selectorObj = this.specialSelectors[2 * (byte - 0x60)]; } else if (0x80 <= byte && byte <= 0xAF) { selectorObj = this.method.methodGetSelector(byte&0xF); } else if (byte == 0xEA || byte == 0xEB) { this.method.methodGetSelector((this.method.bytes[this.pc+1] >> 3)); // (extA << 5) } else if (0x58 <= byte && byte <= 0x5E) { return true; // return } } else { if (byte >= 120 && byte <= 125) return true; // return if (byte < 131 || byte == 200) return false; if (byte >= 176) return true; // special send or short send if (byte <= 134) { // long sends // long form support demands we check the selector var litIndex; if (byte === 132) { if ((this.method.bytes[this.pc + 1] >> 5) > 1) return false; litIndex = this.method.bytes[this.pc + 2]; } else litIndex = this.method.bytes[this.pc + 1] & (byte === 134 ? 63 : 31); var selectorObj = this.method.methodGetLiteral(litIndex); if (selectorObj.bytesAsString() !== 'blockCopy:') return true; } } return false; }, nextSendSelector: function() { // if the next bytecode corresponds to a Smalltalk // message send, answer the selector var byte = this.method.bytes[this.pc]; var selectorObj; if (this.method.methodSignFlag()) { if (0x60 <= byte && byte <= 0x7F) { selectorObj = this.specialSelectors[2 * (byte - 0x60)]; } else if (0x80 <= byte && byte <= 0xAF) { selectorObj = this.method.methodGetSelector(byte&0xF); } else if (byte == 0xEA || byte == 0xEB) { this.method.methodGetSelector((this.method.bytes[this.pc+1] >> 3)); // (extA << 5) } else { return null; } } else { if (byte < 131 || byte == 200) return null; if (byte >= 0xD0 ) { selectorObj = this.method.methodGetLiteral(byte & 0x0F); } else if (byte >= 0xB0 ) { selectorObj = this.specialSelectors[2 * (byte - 0xB0)]; } else if (byte <= 134) { // long form support demands we check the selector var litIndex; if (byte === 132) { if ((this.method.bytes[this.pc + 1] >> 5) > 1) return null; litIndex = this.method.bytes[this.pc + 2]; } else litIndex = this.method.bytes[this.pc + 1] & (byte === 134 ? 63 : 31); selectorObj = this.method.methodGetLiteral(litIndex); } } if (selectorObj) { var selector = selectorObj.bytesAsString(); if (selector !== 'blockCopy:') return selector; } }, });