scratch0-5 / vm.interpreter.js
soiz1's picture
Upload folder using huggingface_hub
8f3f8db verified
"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 <stack empty>';
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;
}
},
});