Spaces:
Running
Running
; | |
/* | |
* 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.Object', | |
'initialization', { | |
initInstanceOf: function(aClass, indexableSize, hash, nilObj) { | |
this.sqClass = aClass; | |
this.hash = hash; | |
var instSpec = aClass.pointers[Squeak.Class_format], | |
instSize = ((instSpec>>1) & 0x3F) + ((instSpec>>10) & 0xC0) - 1; //0-255 | |
this._format = (instSpec>>7) & 0xF; //This is the 0-15 code | |
if (this._format < 8) { | |
if (this._format != 6) { | |
if (instSize + indexableSize > 0) | |
this.pointers = this.fillArray(instSize + indexableSize, nilObj); | |
} else // Words | |
if (indexableSize > 0) | |
if (aClass.isFloatClass) { | |
this.isFloat = true; | |
this.float = 0.0; | |
} else | |
this.words = new Uint32Array(indexableSize); | |
} else // Bytes | |
if (indexableSize > 0) { | |
// this._format |= -indexableSize & 3; //deferred to writeTo() | |
this.bytes = new Uint8Array(indexableSize); //Methods require further init of pointers | |
} | |
// Definition of Squeak's format code... | |
// | |
// Pointers only... | |
// 0 no fields | |
// 1 fixed fields only (all containing pointers) | |
// 2 indexable fields only (all containing pointers) | |
// 3 both fixed and indexable fields (all containing pointers) | |
// 4 both fixed and indexable weak fields (all containing pointers). | |
// 5 unused | |
// Bits only... | |
// 6 indexable word fields only (no pointers) | |
// 7 unused | |
// 8-11 indexable byte fields only (no pointers) (low 2 bits are low 2 bits of size) | |
// Pointer and bits (CompiledMethods only)... | |
// 12-15 compiled methods: | |
// # of literal oops specified in method header, | |
// followed by indexable bytes (same interpretation of low 2 bits as above) | |
}, | |
initAsClone: function(original, hash) { | |
this.sqClass = original.sqClass; | |
this.hash = hash; | |
this._format = original._format; | |
if (original.isFloat) { | |
this.isFloat = original.isFloat; | |
this.float = original.float; | |
} else { | |
if (original.pointers) this.pointers = original.pointers.slice(0); // copy | |
if (original.words) this.words = new Uint32Array(original.words); // copy | |
if (original.bytes) this.bytes = new Uint8Array(original.bytes); // copy | |
} | |
}, | |
initFromImage: function(oop, cls, fmt, hsh) { | |
// initial creation from Image, with unmapped data | |
this.oop = oop; | |
this.sqClass = cls; | |
this._format = fmt; | |
this.hash = hsh; | |
}, | |
classNameFromImage: function(oopMap, rawBits) { | |
var name = oopMap.get(rawBits.get(this.oop)[Squeak.Class_name]); | |
if (name && name._format >= 8 && name._format < 12) { | |
var bits = rawBits.get(name.oop), | |
bytes = name.decodeBytes(bits.length, bits, 0, name._format & 3); | |
return Squeak.bytesAsString(bytes); | |
} | |
return "Class"; | |
}, | |
renameFromImage: function(oopMap, rawBits, ccArray) { | |
var classObj = this.sqClass < 32 ? oopMap.get(ccArray[this.sqClass-1]) : oopMap.get(this.sqClass); | |
if (!classObj) return this; | |
var instProto = classObj.instProto || classObj.classInstProto(classObj.classNameFromImage(oopMap, rawBits)); | |
if (!instProto) return this; | |
var renamedObj = new instProto; // Squeak.Object | |
renamedObj.oop = this.oop; | |
renamedObj.sqClass = this.sqClass; | |
renamedObj._format = this._format; | |
renamedObj.hash = this.hash; | |
return renamedObj; | |
}, | |
installFromImage: function(oopMap, rawBits, ccArray, floatClass, littleEndian, nativeFloats) { | |
//Install this object by decoding format, and rectifying pointers | |
var ccInt = this.sqClass; | |
// map compact classes | |
if ((ccInt>0) && (ccInt<32)) | |
this.sqClass = oopMap.get(ccArray[ccInt-1]); | |
else | |
this.sqClass = oopMap.get(ccInt); | |
var bits = rawBits.get(this.oop), | |
nWords = bits.length; | |
if (this._format < 5) { | |
//Formats 0...4 -- Pointer fields | |
if (nWords > 0) { | |
var oops = bits; // endian conversion was already done | |
this.pointers = this.decodePointers(nWords, oops, oopMap); | |
} | |
} else if (this._format >= 12) { | |
//Formats 12-15 -- CompiledMethods both pointers and bits | |
var methodHeader = this.decodeWords(1, bits, littleEndian)[0], | |
numLits = (methodHeader>>10) & 255, | |
oops = this.decodeWords(numLits+1, bits, littleEndian); | |
this.pointers = this.decodePointers(numLits+1, oops, oopMap); //header+lits | |
this.bytes = this.decodeBytes(nWords-(numLits+1), bits, numLits+1, this._format & 3); | |
} else if (this._format >= 8) { | |
//Formats 8..11 -- ByteArrays (and ByteStrings) | |
if (nWords > 0) | |
this.bytes = this.decodeBytes(nWords, bits, 0, this._format & 3); | |
} else if (this.sqClass == floatClass) { | |
//These words are actually a Float | |
this.isFloat = true; | |
this.float = this.decodeFloat(bits, littleEndian, nativeFloats); | |
} else { | |
if (nWords > 0) | |
this.words = this.decodeWords(nWords, bits, littleEndian); | |
} | |
this.mark = false; // for GC | |
}, | |
decodePointers: function(nWords, theBits, oopMap) { | |
//Convert small ints and look up object pointers in oopMap | |
var ptrs = new Array(nWords); | |
for (var i = 0; i < nWords; i++) { | |
var oop = theBits[i]; | |
if ((oop & 1) === 1) { // SmallInteger | |
ptrs[i] = oop >> 1; | |
} else { // Object | |
ptrs[i] = oopMap.get(oop) || 42424242; | |
// when loading a context from image segment, there is | |
// garbage beyond its stack pointer, resulting in the oop | |
// not being found in oopMap. We just fill in an arbitrary | |
// SmallInteger - it's never accessed anyway | |
} | |
} | |
return ptrs; | |
}, | |
decodeWords: function(nWords, theBits, littleEndian) { | |
var data = new DataView(theBits.buffer, theBits.byteOffset), | |
words = new Uint32Array(nWords); | |
for (var i = 0; i < nWords; i++) | |
words[i] = data.getUint32(i*4, littleEndian); | |
return words; | |
}, | |
decodeBytes: function (nWords, theBits, wordOffset, fmtLowBits) { | |
// Adjust size for low bits and make a copy | |
var nBytes = (nWords * 4) - fmtLowBits, | |
wordsAsBytes = new Uint8Array(theBits.buffer, theBits.byteOffset + wordOffset * 4, nBytes), | |
bytes = new Uint8Array(nBytes); | |
bytes.set(wordsAsBytes); | |
return bytes; | |
}, | |
decodeFloat: function(theBits, littleEndian, nativeFloats) { | |
var data = new DataView(theBits.buffer, theBits.byteOffset); | |
// it's either big endian ... | |
if (!littleEndian) return data.getFloat64(0, false); | |
// or real little endian | |
if (nativeFloats) return data.getFloat64(0, true); | |
// or little endian, but with swapped words | |
var buffer = new ArrayBuffer(8), | |
swapped = new DataView(buffer); | |
swapped.setUint32(0, data.getUint32(4)); | |
swapped.setUint32(4, data.getUint32(0)); | |
return swapped.getFloat64(0, true); | |
}, | |
fillArray: function(length, filler) { | |
for (var array = [], i = 0; i < length; i++) | |
array[i] = filler; | |
return array; | |
}, | |
}, | |
'testing', { | |
isWords: function() { | |
return this._format === 6; | |
}, | |
isBytes: function() { | |
var fmt = this._format; | |
return fmt >= 8 && fmt <= 11; | |
}, | |
isWordsOrBytes: function() { | |
var fmt = this._format; | |
return fmt == 6 || (fmt >= 8 && fmt <= 11); | |
}, | |
isPointers: function() { | |
return this._format <= 4; | |
}, | |
isWeak: function() { | |
return this._format === 4; | |
}, | |
isMethod: function() { | |
return this._format >= 12; | |
}, | |
sameFormats: function(a, b) { | |
return a < 8 ? a === b : (a & 0xC) === (b & 0xC); | |
}, | |
sameFormatAs: function(obj) { | |
return this.sameFormats(this._format, obj._format); | |
}, | |
}, | |
'printing', { | |
toString: function() { | |
return this.sqInstName(); | |
}, | |
bytesAsString: function() { | |
if (!this.bytes) return ''; | |
return Squeak.bytesAsString(this.bytes); | |
}, | |
bytesAsNumberString: function(negative) { | |
if (!this.bytes) return ''; | |
var hex = '0123456789ABCDEF', | |
digits = [], | |
value = 0; | |
for (var i = this.bytes.length - 1; i >= 0; i--) { | |
digits.push(hex[this.bytes[i] >> 4]); | |
digits.push(hex[this.bytes[i] & 15]); | |
value = value * 256 + this.bytes[i]; | |
} | |
var sign = negative ? '-' : '', | |
approx = value > 0x1FFFFFFFFFFFFF ? '≈' : ''; | |
return sign + '16r' + digits.join('') + ' (' + approx + sign + value + 'L)'; | |
}, | |
assnKeyAsString: function() { | |
return this.pointers[Squeak.Assn_key].bytesAsString(); | |
}, | |
slotNameAt: function(index) { | |
// one-based index | |
var instSize = this.instSize(); | |
if (index <= instSize) | |
return this.sqClass.allInstVarNames()[index - 1] || 'ivar' + (index - 1); | |
else | |
return (index - instSize).toString(); | |
}, | |
sqInstName: function() { | |
if (this.isNil) return "nil"; | |
if (this.isTrue) return "true"; | |
if (this.isFalse) return "false"; | |
if (this.isFloat) {var str = this.float.toString(); if (!/\./.test(str)) str += '.0'; return str; } | |
var className = this.sqClass.className(); | |
if (/ /.test(className)) | |
return 'the ' + className; | |
switch (className) { | |
case 'String': | |
case 'ByteString': return "'" + this.bytesAsString() + "'"; | |
case 'Symbol': | |
case 'ByteSymbol': return "#" + this.bytesAsString(); | |
case 'Point': return this.pointers.join("@"); | |
case 'Rectangle': return this.pointers.join(" corner: "); | |
case 'Association': | |
case 'ReadOnlyVariableBinding': return this.pointers.join("->"); | |
case 'LargePositiveInteger': return this.bytesAsNumberString(false); | |
case 'LargeNegativeInteger': return this.bytesAsNumberString(true); | |
case 'Character': var unicode = this.pointers ? this.pointers[0] : this.hash; // Spur | |
return "$" + String.fromCharCode(unicode) + " (" + unicode.toString() + ")"; | |
case 'CompiledMethod': return this.methodAsString(); | |
case 'CompiledBlock': return "[] in " + this.blockOuterCode().sqInstName(); | |
} | |
return /^[aeiou]/i.test(className) ? 'an' + className : 'a' + className; | |
}, | |
}, | |
'accessing', { | |
pointersSize: function() { | |
return this.pointers ? this.pointers.length : 0; | |
}, | |
bytesSize: function() { | |
return this.bytes ? this.bytes.length : 0; | |
}, | |
wordsSize: function() { | |
return this.isFloat ? 2 : this.words ? this.words.length : 0; | |
}, | |
instSize: function() {//same as class.classInstSize, but faster from format | |
var fmt = this._format; | |
if (fmt > 4 || fmt === 2) return 0; //indexable fields only | |
if (fmt < 2) return this.pointersSize(); //fixed fields only | |
return this.sqClass.classInstSize(); | |
}, | |
indexableSize: function(primHandler) { | |
var fmt = this._format; | |
if (fmt < 2) return -1; //not indexable | |
if (fmt === 3 && primHandler.vm.isContext(this) && !primHandler.allowAccessBeyondSP) | |
return this.pointers[Squeak.Context_stackPointer]; // no access beyond top of stacks | |
if (fmt < 6) return this.pointersSize() - this.instSize(); // pointers | |
if (fmt < 8) return this.wordsSize(); // words | |
if (fmt < 12) return this.bytesSize(); // bytes | |
return this.bytesSize() + (4 * this.pointersSize()); // methods | |
}, | |
floatData: function() { | |
var buffer = new ArrayBuffer(8); | |
var data = new DataView(buffer); | |
data.setFloat64(0, this.float, false); | |
//1st word is data.getUint32(0, false); | |
//2nd word is data.getUint32(4, false); | |
return data; | |
}, | |
wordsAsFloat32Array: function() { | |
return this.float32Array | |
|| (this.words && (this.float32Array = new Float32Array(this.words.buffer))); | |
}, | |
wordsAsFloat64Array: function() { | |
return this.float64Array | |
|| (this.words && (this.float64Array = new Float64Array(this.words.buffer))); | |
}, | |
wordsAsInt32Array: function() { | |
return this.int32Array | |
|| (this.words && (this.int32Array = new Int32Array(this.words.buffer))); | |
}, | |
wordsAsInt16Array: function() { | |
return this.int16Array | |
|| (this.words && (this.int16Array = new Int16Array(this.words.buffer))); | |
}, | |
wordsAsUint16Array: function() { | |
return this.uint16Array | |
|| (this.words && (this.uint16Array = new Uint16Array(this.words.buffer))); | |
}, | |
wordsAsUint8Array: function() { | |
return this.uint8Array | |
|| (this.words && (this.uint8Array = new Uint8Array(this.words.buffer))); | |
}, | |
wordsOrBytes: function() { | |
if (this.words) return this.words; | |
if (this.uint32Array) return this.uint32Array; | |
if (!this.bytes) return null; | |
return this.uint32Array = new Uint32Array(this.bytes.buffer, 0, this.bytes.length >>> 2); | |
}, | |
setAddr: function(addr) { | |
// Move this object to addr by setting its oop. Answer address after this object. | |
// Used to assign an oop for the first time when tenuring this object during GC. | |
// When compacting, the oop is adjusted directly, since header size does not change. | |
var words = this.snapshotSize(); | |
this.oop = addr + words.header * 4; | |
return addr + (words.header + words.body) * 4; | |
}, | |
snapshotSize: function() { | |
// words of extra object header and body this object would take up in image snapshot | |
// body size includes one header word that is always present | |
var nWords = | |
this.isFloat ? 2 : | |
this.words ? this.words.length : | |
this.pointers ? this.pointers.length : 0; | |
// methods have both pointers and bytes | |
if (this.bytes) nWords += (this.bytes.length + 3) >>> 2; | |
nWords++; // one header word always present | |
var extraHeader = nWords > 63 ? 2 : this.sqClass.isCompact ? 0 : 1; | |
return {header: extraHeader, body: nWords}; | |
}, | |
addr: function() { // start addr of this object in a snapshot | |
return this.oop - this.snapshotSize().header * 4; | |
}, | |
totalBytes: function() { | |
// size in bytes this object would take up in image snapshot | |
var words = this.snapshotSize(); | |
return (words.header + words.body) * 4; | |
}, | |
writeTo: function(data, pos, image) { | |
// Write 1 to 3 header words encoding type, class, and size, then instance data | |
if (this.bytes) this._format |= -this.bytes.length & 3; | |
var beforePos = pos, | |
size = this.snapshotSize(), | |
formatAndHash = ((this._format & 15) << 8) | ((this.hash & 4095) << 17); | |
// write header words first | |
switch (size.header) { | |
case 2: | |
data.setUint32(pos, size.body << 2 | Squeak.HeaderTypeSizeAndClass); pos += 4; | |
data.setUint32(pos, this.sqClass.oop | Squeak.HeaderTypeSizeAndClass); pos += 4; | |
data.setUint32(pos, formatAndHash | Squeak.HeaderTypeSizeAndClass); pos += 4; | |
break; | |
case 1: | |
data.setUint32(pos, this.sqClass.oop | Squeak.HeaderTypeClass); pos += 4; | |
data.setUint32(pos, formatAndHash | size.body << 2 | Squeak.HeaderTypeClass); pos += 4; | |
break; | |
case 0: | |
var classIndex = image.compactClasses.indexOf(this.sqClass) + 1; | |
data.setUint32(pos, formatAndHash | classIndex << 12 | size.body << 2 | Squeak.HeaderTypeShort); pos += 4; | |
} | |
// now write body, if any | |
if (this.isFloat) { | |
data.setFloat64(pos, this.float); pos += 8; | |
} else if (this.words) { | |
for (var i = 0; i < this.words.length; i++) { | |
data.setUint32(pos, this.words[i]); pos += 4; | |
} | |
} else if (this.pointers) { | |
for (var i = 0; i < this.pointers.length; i++) { | |
data.setUint32(pos, image.objectToOop(this.pointers[i])); pos += 4; | |
} | |
} | |
// no "else" because CompiledMethods have both pointers and bytes | |
if (this.bytes) { | |
for (var i = 0; i < this.bytes.length; i++) | |
data.setUint8(pos++, this.bytes[i]); | |
// skip to next word | |
pos += -this.bytes.length & 3; | |
} | |
// done | |
if (pos !== beforePos + this.totalBytes()) throw Error("written size does not match"); | |
return pos; | |
}, | |
}, | |
'as class', { | |
classInstFormat: function() { | |
return (this.pointers[Squeak.Class_format] >> 7) & 0xF; | |
}, | |
classInstSize: function() { | |
// this is a class, answer number of named inst vars | |
var spec = this.pointers[Squeak.Class_format]; | |
return ((spec >> 10) & 0xC0) + ((spec >> 1) & 0x3F) - 1; | |
}, | |
classInstIsBytes: function() { | |
var fmt = this.classInstFormat(); | |
return fmt >= 8 && fmt <= 11; | |
}, | |
classInstIsPointers: function() { | |
return this.classInstFormat() <= 4; | |
}, | |
instVarNames: function() { | |
// index changed from 4 to 3 in newer images | |
for (var index = 3; index <= 4; index++) { | |
var varNames = this.pointers[index].pointers; | |
if (varNames && varNames.length && varNames[0].bytes) { | |
return varNames.map(function(each) { | |
return each.bytesAsString(); | |
}); | |
} | |
} | |
return []; | |
}, | |
allInstVarNames: function() { | |
var superclass = this.superclass(); | |
if (superclass.isNil) | |
return this.instVarNames(); | |
else | |
return superclass.allInstVarNames().concat(this.instVarNames()); | |
}, | |
superclass: function() { | |
return this.pointers[0]; | |
}, | |
className: function() { | |
if (!this.pointers) return "_NOTACLASS_"; | |
for (var nameIdx = 6; nameIdx <= 7; nameIdx++) { | |
var name = this.pointers[nameIdx]; | |
if (name && name.bytes) return name.bytesAsString(); | |
} | |
// must be meta class | |
for (var clsIndex = 5; clsIndex <= 6; clsIndex++) { | |
var cls = this.pointers[clsIndex]; | |
if (cls && cls.pointers) { | |
for (var nameIdx = 6; nameIdx <= 7; nameIdx++) { | |
var name = cls.pointers[nameIdx]; | |
if (name && name.bytes) return name.bytesAsString() + " class"; | |
} | |
} | |
} | |
return "_SOMECLASS_"; | |
}, | |
defaultInst: function() { | |
return Squeak.Object; | |
}, | |
classInstProto: function(className) { | |
if (this.instProto) return this.instProto; | |
var proto = this.defaultInst(); // in case below fails | |
try { | |
if (!className) className = this.className(); | |
var safeName = className.replace(/[^A-Za-z0-9]/g,'_'); | |
if (safeName === "UndefinedObject") safeName = "nil"; | |
else if (safeName === "True") safeName = "true_"; | |
else if (safeName === "False") safeName = "false_"; | |
else safeName = ((/^[AEIOU]/.test(safeName)) ? 'an' : 'a') + safeName; | |
// fail okay if no eval() | |
proto = new Function("return function " + safeName + "() {};")(); | |
proto.prototype = this.defaultInst().prototype; | |
} catch(e) {} | |
Object.defineProperty(this, 'instProto', { value: proto }); | |
return proto; | |
}, | |
}, | |
'as method', { | |
methodSignFlag: function() { | |
return false; | |
}, | |
methodNumLits: function() { | |
return (this.pointers[0]>>9) & 0xFF; | |
}, | |
methodNumArgs: function() { | |
return (this.pointers[0]>>24) & 0xF; | |
}, | |
methodPrimitiveIndex: function() { | |
var primBits = this.pointers[0] & 0x300001FF; | |
if (primBits > 0x1FF) | |
return (primBits & 0x1FF) + (primBits >> 19); | |
else | |
return primBits; | |
}, | |
methodClassForSuper: function() {//assn found in last literal | |
var assn = this.pointers[this.methodNumLits()]; | |
return assn.pointers[Squeak.Assn_value]; | |
}, | |
methodNeedsLargeFrame: function() { | |
return (this.pointers[0] & 0x20000) > 0; | |
}, | |
methodAddPointers: function(headerAndLits) { | |
this.pointers = headerAndLits; | |
}, | |
methodTempCount: function() { | |
return (this.pointers[0]>>18) & 63; | |
}, | |
methodGetLiteral: function(zeroBasedIndex) { | |
return this.pointers[1+zeroBasedIndex]; // step over header | |
}, | |
methodGetSelector: function(zeroBasedIndex) { | |
return this.pointers[1+zeroBasedIndex]; // step over header | |
}, | |
methodAsString: function() { | |
return 'aCompiledMethod'; | |
}, | |
}, | |
'as context', { | |
contextHome: function() { | |
return this.contextIsBlock() ? this.pointers[Squeak.BlockContext_home] : this; | |
}, | |
contextIsBlock: function() { | |
return typeof this.pointers[Squeak.BlockContext_argumentCount] === 'number'; | |
}, | |
contextMethod: function() { | |
return this.contextHome().pointers[Squeak.Context_method]; | |
}, | |
contextSender: function() { | |
return this.pointers[Squeak.Context_sender]; | |
}, | |
contextSizeWithStack: function(vm) { | |
// Actual context size is inst vars + stack size. Slots beyond that may contain garbage. | |
// If passing in a VM, and this is the activeContext, use the VM's current value. | |
if (vm && vm.activeContext === this) | |
return vm.sp + 1; | |
// following is same as decodeSqueakSP() but works without vm ref | |
var sp = this.pointers[Squeak.Context_stackPointer]; | |
return Squeak.Context_tempFrameStart + (typeof sp === "number" ? sp : 0); | |
}, | |
}); | |