diff --git a/.gitignore b/.gitignore index 245b454e..aee30f68 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store node_modules/ gh-pages +images dist/*.min.js* *.code-workspace diff --git a/README.md b/README.md index 2c215b13..24284eb5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ +V2 BRANCH +========= +This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: + +* (implemented but will reconsider) each fixed inst var gets its own property for direct access instead of being indexed in `pointers[]`. There will be a compatibility accessor for primitives that use indexed access. + + Still need to decide between named inst vars (using inst var names from image) or suffixed (like `$0`, `$1`, ...). _[Update: suffixed is more practical]_ + + The goal is faster access than via the `pointers[]` array. + Also, nicer debuggability if we use actual names. + + _Update Sept 2025: I implemented this in the v2 branch, and it works (via proxy). However, I realized it's rather impractical to convert all code (I tried to convert the interpreter so that the context is accessed using direct ivar refs but gave up). Also the `pointers` proxy is very slow but every primitive uses it, negating every potential performance win._ + +* new high-performance JIT without per-frame context allocation, but instead using direct function calls, function temps as stack, args passed directly via function parameters, and direct instance var access (see above). Contexts would only be allocated if needed (also see the existing [discussion](https://github.com/codefrau/SqueakJS/issues/121) and my [JIT experiments](https://squeak.js.org/docs/jit.md.html)) + + The goal is to make the jitted methods look as close to "normal" JavaScript functions as possible, so that the JS JIT can optimize them, even with inlining etc. + +* (maybe) use `WeakRef` and `WeakMap`? All JS runtimes now support weak objects (`WeakRef` is still pretty new, since 2021). + + The goal would be to have faster GCs while still supporting object enumeration. + +* (maybe) use WASM for BitBlt etc. To avoid copying in and out of the WASM heap, we could use binary arrays allocated via WASM (but would need to implement GC for that) + + Goal: speed + +* (maybe) `BigInt` for large integer primitives? Supported in browsers since 2020 (and allowed to fail if not available). Need to measure fastest way to convert from/to `Uint8Array` representation. (this is actually independent of v2, see the existing [discussion](https://github.com/codefrau/SqueakJS/issues/37)) + + The goal is faster LargeInteger calculations. + +* (maybe) no more `.oop` property: it's not needed at runtime, and updating it makes the GC slower. + + Goal: faster GC + +* (maybe) export as 64 bit image: on load, 64-bit images are converted to 32 bits. On export, we could store them as 64 bits (and if we get rid of the `.oop` as mentioned above it may actually be almost as fast as snapshotting in 32 bits) + + The goal here is compatibility with other VMs, which on some systems only run 64 bit images + +Feedback and ideas: please comment on the [Pull Request](https://github.com/codefrau/SqueakJS/pull/168) or use the [vm-dev](http://lists.squeak.org/mailman/listinfo/vm-dev) mailing list and `#squeakjs` channel on the [Squeak Slack](https://join.slack.com/t/squeak/shared_invite/zt-2ahdbewgl-56nPdkf1hYACBmc8xCOXRQ). + SqueakJS: A Squeak VM for the Web and Node.js ============================================= @@ -127,7 +166,7 @@ There's a gazillion exciting things to do :) Changelog --------- - 2025-05-04: 1.3.3 minor FFI, OpenGL, and other fixes/improvements + 2025-06-03: 1.3.3 minor FFI, OpenGL, and other fixes/improvements 2025-04-06: 1.3.2 use our own CORS proxy, add welcome=false option, minor fixes 2025-03-29: 1.3.1 add 'w', 'h', 'embedded' canvas options, minor fixes 2025-03-28: 1.3.0 add OpenGL support, canvas is optional, fix socket plugin bug diff --git a/jit.js b/jit.js index 6129a2b1..f8d63130 100644 --- a/jit.js +++ b/jit.js @@ -84,10 +84,9 @@ version of the generated JavaScript code: 6 <21> pushConst: 42 7 <7C> return: topOfStack - context = vm.activeContext while (true) switch (vm.pc) { case 0: - stack[++vm.sp] = inst[0]; + stack[++vm.sp] = rcvr.$0; vm.pc = 2; vm.send(#selector); // activate new method return; // return to main loop // Main loop will execute the activated method. When @@ -233,11 +232,10 @@ to single-step. if (optClass && optSel) this.source.push("// ", optClass, ">>", optSel, "\n"); this.instVarNames = optInstVarNames; - this.allVars = ['context', 'stack', 'rcvr', 'inst[', 'temp[', 'lit[']; + this.allVars = ['context', 'stack', 'rcvr', 'temp[', 'lit[']; this.sourcePos['context'] = this.source.length; this.source.push("var context = vm.activeContext;\n"); this.sourcePos['stack'] = this.source.length; this.source.push("var stack = context.pointers;\n"); this.sourcePos['rcvr'] = this.source.length; this.source.push("var rcvr = vm.receiver;\n"); - this.sourcePos['inst['] = this.source.length; this.source.push("var inst = rcvr.pointers;\n"); this.sourcePos['temp['] = this.source.length; this.source.push("var temp = vm.homeContext.pointers;\n"); this.sourcePos['lit['] = this.source.length; this.source.push("var lit = vm.method.pointers;\n"); this.sourcePos['loop-start'] = this.source.length; this.source.push("while (true) switch (vm.pc) {\ncase 0:\n"); @@ -263,7 +261,7 @@ to single-step. switch (byte & 0xF8) { // load inst var case 0x00: case 0x08: - this.generatePush("inst[", byte & 0x0F, "]"); + this.generatePush("rcvr", ".$", byte & 0x0F); break; // load temp var case 0x10: case 0x18: @@ -279,7 +277,7 @@ to single-step. break; // storeAndPop inst var case 0x60: - this.generatePopInto("inst[", byte & 0x07, "]"); + this.generatePopInto("rcvr", ".$", byte & 0x07); break; // storeAndPop temp var case 0x68: @@ -360,7 +358,7 @@ to single-step. case 0x80: byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { - case 0: this.generatePush("inst[", byte2 & 0x3F, "]"); return; + case 0: this.generatePush("rcvr", ".$", byte2 & 0x3F); return; case 1: this.generatePush("temp[", 6 + (byte2 & 0x3F), "]"); return; case 2: this.generatePush("lit[", 1 + (byte2 & 0x3F), "]"); return; case 3: this.generatePush("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; @@ -369,7 +367,7 @@ to single-step. case 0x81: byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { - case 0: this.generateStoreInto("inst[", byte2 & 0x3F, "]"); return; + case 0: this.generateStoreInto("rcvr", ".$", byte2 & 0x3F); return; case 1: this.generateStoreInto("temp[", 6 + (byte2 & 0x3F), "]"); return; case 2: throw Error("illegal store into literal"); case 3: this.generateStoreInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; @@ -379,7 +377,7 @@ to single-step. case 0x82: byte2 = this.method.bytes[this.pc++]; switch (byte2 >> 6) { - case 0: this.generatePopInto("inst[", byte2 & 0x3F, "]"); return; + case 0: this.generatePopInto("rcvr", ".$", byte2 & 0x3F); return; case 1: this.generatePopInto("temp[", 6 + (byte2 & 0x3F), "]"); return; case 2: throw Error("illegal pop into literal"); case 3: this.generatePopInto("lit[", 1 + (byte2 & 0x3F), "].pointers[1]"); return; @@ -396,11 +394,11 @@ to single-step. switch (byte2 >> 5) { case 0: this.generateSend("lit[", 1 + byte3, "]", byte2 & 31, false); return; case 1: this.generateSend("lit[", 1 + byte3, "]", byte2 & 31, true); return; - case 2: this.generatePush("inst[", byte3, "]"); return; + case 2: this.generatePush("rcvr", ".$", byte3); return; case 3: this.generatePush("lit[", 1 + byte3, "]"); return; case 4: this.generatePush("lit[", 1 + byte3, "].pointers[1]"); return; - case 5: this.generateStoreInto("inst[", byte3, "]"); return; - case 6: this.generatePopInto("inst[", byte3, "]"); return; + case 5: this.generateStoreInto("rcvr", ".$", byte3); return; + case 6: this.generatePopInto("rcvr", ".$", byte3); return; case 7: this.generateStoreInto("lit[", 1 + byte3, "].pointers[1]"); return; } // Single extended send to super @@ -486,7 +484,7 @@ to single-step. // 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.generatePush("inst[", b & 0x0F, "]"); + this.generatePush("rcvr", ".$", b & 0x0F); break; // load literal variable case 0x10: case 0x11: case 0x12: case 0x13: case 0x14: case 0x15: case 0x16: case 0x17: @@ -574,7 +572,7 @@ to single-step. this.generateJumpIf(false, (b & 0x07) + 1); break; case 0xC8: case 0xC9: case 0xCA: case 0xCB: case 0xCC: case 0xCD: case 0xCE: case 0xCF: - this.generatePopInto("inst[", b & 0x07, "]"); + this.generatePopInto("rcvr", ".$", b & 0x07); break; case 0xD0: case 0xD1: case 0xD2: case 0xD3: case 0xD4: case 0xD5: case 0xD6: case 0xD7: this.generatePopInto("temp[", 6 + (b & 0x07), "]"); @@ -597,7 +595,7 @@ to single-step. continue; case 0xE2: b2 = bytes[this.pc++]; - this.generatePush("inst[", b2 + extA * 256, "]"); + this.generatePush("rcvr", ".$", b2 + extA * 256); break; case 0xE3: b2 = bytes[this.pc++]; @@ -654,7 +652,7 @@ to single-step. break; case 0xF0: b2 = bytes[this.pc++]; - this.generatePopInto("inst[", b2 + extA * 256, "]"); + this.generatePopInto("rcvr", ".$", b2 + extA * 256); break; case 0xF1: b2 = bytes[this.pc++]; @@ -666,7 +664,7 @@ to single-step. break; case 0xF3: b2 = bytes[this.pc++]; - this.generateStoreInto("inst[", b2 + extA * 256, "]"); + this.generateStoreInto("rcvr", ".$", b2 + extA * 256); break; case 0xF4: b2 = bytes[this.pc++]; @@ -1083,9 +1081,9 @@ to single-step. }, generateDirty: function(target, arg, suffix) { switch(target) { - case "inst[": this.source.push("rcvr.dirty = true;\n"); break; + case "rcvr": this.source.push("rcvr.dirty = true;\n"); break; case "lit[": this.source.push(target, arg, "].dirty = true;\n"); break; - case "temp[": if (suffix !== "]") this.source.push(target, arg, "].dirty = true;\n"); break; + case "temp[": /* activeContext is always marked dirty */ break; default: throw Error("unexpected target " + target); } @@ -1116,11 +1114,14 @@ to single-step. case 'vm.nilObj': this.source.push('nil'); break; case 'vm.trueObj': this.source.push('true'); break; case 'vm.falseObj': this.source.push('false'); break; - case 'rcvr': this.source.push('self'); break; case 'stack[vm.sp]': this.source.push('top of stack'); break; - case 'inst[': - if (!this.instVarNames) this.source.push('inst var ', arg1); - else this.source.push(this.instVarNames[arg1]); + case 'rcvr': + if (!arg1) { + this.source.push('self'); + } else { // "rcvr", ".$", index + if (!this.instVarNames) this.source.push('inst var ', suffix1); + else this.source.push(this.instVarNames[suffix1]); + } break; case 'temp[': this.source.push('tmp', arg1 - 6); @@ -1159,7 +1160,6 @@ to single-step. }, deleteUnneededVariables: function() { if (this.needsVar['stack']) this.needsVar['context'] = true; - if (this.needsVar['inst[']) this.needsVar['rcvr'] = true; for (var i = 0; i < this.allVars.length; i++) { var v = this.allVars[i]; if (!this.needsVar[v]) diff --git a/package.json b/package.json index 672d54c0..147f2dc7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@codefrau/squeakjs", "version": "1.3.3", "description": "Virtual Machine for Squeak Smalltalk and derivatives", - "author": "Vanessa Freudenberg (https://twitter.com/codefrau)", + "author": "Vanessa Freudenberg ", "repository": "https://github.com/codefrau/SqueakJS", "license": "MIT", "browser": "squeak.js", diff --git a/vm.image.js b/vm.image.js index ea9c04e1..64620463 100644 --- a/vm.image.js +++ b/vm.image.js @@ -188,7 +188,7 @@ Object.subclass('Squeak.Image', hash = (header>>>17) & 4095, bits = readBits(nWords, format < 5); var object = new Squeak.Object(); - object.initFromImage(oop, classInt, format, hash); + object.initFromBits(oop, classInt, format, hash); if (classInt < 32) object.hash |= 0x10000000; // see fixCompactOops() if (prevObj) prevObj.nextObject = object; this.oldSpaceCount++; @@ -236,7 +236,7 @@ Object.subclass('Squeak.Image', // low class ids are internal to Spur if (classID >= 32) { var object = new Squeak.ObjectSpur(); - object.initFromImage(oop, classID, format, hash); + object.initFromBits(oop, classID, format, hash); if (prevObj) prevObj.nextObject = object; this.oldSpaceCount++; prevObj = object; @@ -296,7 +296,7 @@ Object.subclass('Squeak.Image', prevObj = null; while (object) { prevObj = renamedObj; - renamedObj = object.renameFromImage(oopMap, rawBits, cc); + renamedObj = object.renameFromBits(oopMap, rawBits, cc); if (prevObj) prevObj.nextObject = renamedObj; else this.firstOldObject = renamedObj; oopMap.set(oldBaseAddr + object.oop, renamedObj); @@ -310,26 +310,25 @@ Object.subclass('Squeak.Image', var splObs = oopMap.get(specialObjectsOopInt); var compactClasses = rawBits.get(oopMap.get(rawBits.get(splObs.oop)[Squeak.splOb_CompactClasses]).oop); var floatClass = oopMap.get(rawBits.get(splObs.oop)[Squeak.splOb_ClassFloat]); - // Spur needs different arguments for installFromImage() + // Spur needs different arguments for installFromBits() if (this.isSpur) { this.initImmediateClasses(oopMap, rawBits, splObs); compactClasses = this.spurClassTable(oopMap, rawBits, classPages, splObs); nativeFloats = this.getCharacter.bind(this); this.initSpurOverrides(); } + // figure out if Class_instVars is 3 or 4 or unknown + Squeak.Class_instVars = this.detectClassInstVarIndex(splObs, oopMap, rawBits); + // now "install" the objects, i.e. decode the bits into proper references, etc. var obj = this.firstOldObject, done = 0; var mapSomeObjects = function() { if (obj) { var stop = done + (this.oldSpaceCount / 20 | 0); // do it in 20 chunks while (obj && done < stop) { - obj.installFromImage(oopMap, rawBits, compactClasses, floatClass, littleEndian, nativeFloats, is64Bit && { - makeFloat: function makeFloat(bits) { - return this.instantiateFloat(bits); - }.bind(this), - makeLargeFromSmall: function makeLargeFromSmall(hi, lo) { - return this.instantiateLargeFromSmall(hi, lo); - }.bind(this), + obj.installFromBits(oopMap, rawBits, compactClasses, floatClass, littleEndian, nativeFloats, is64Bit && { + makeFloat: bits => this.instantiateFloat(bits), + makeLargeFromSmall: (hi, lo) => this.instantiateLargeFromSmall(hi, lo), }); obj = obj.nextObject; done++; @@ -364,6 +363,27 @@ Object.subclass('Squeak.Image', self.setTimeout(mapSomeObjectsAsync, 0); } }, + detectClassInstVarIndex: function(splObs, oopMap, rawBits) { + // the VM really should only make assumptions about inst vars 0-2 + // but we want to use the actual instance variable names + // which are at index 3 or 4 in the class + var classPoint = oopMap.get(rawBits.get(splObs.oop)[Squeak.splOb_ClassPoint]); + var classBits = rawBits.get(classPoint.oop); + // we check if the array #(x y) is anywhere in the Point class + // starting at index 3 (indices 0-2 are known to the VM) + for (var index = 3; index < classBits.length; index++) { + var names = oopMap.get(classBits[index]); if (!names) continue; + var namesBits = rawBits.get(names.oop); if (namesBits.length !== 2) continue; + var x = oopMap.get(namesBits[0]); + var xBits = rawBits.get(x.oop); + if (String.fromCharCode(xBits[0]) !== 'x') continue; + var y = oopMap.get(namesBits[1]); + var yBits = rawBits.get(y.oop); + if (String.fromCharCode(yBits[0]) !== 'y') continue; + return index; + } + return 0; // unknown + }, decorateKnownObjects: function() { var splObjs = this.specialObjectsArray.pointers; splObjs[Squeak.splOb_NilObject].isNil = true; @@ -1140,7 +1160,7 @@ Object.subclass('Squeak.Image', bits = readBits(nWords, format); var object = new Squeak.Object(); - object.initFromImage(oop + oopOffset, classInt, format, hash); + object.initFromBits(oop + oopOffset, classInt, format, hash); prevObj.nextObject = object; this.oldSpaceCount++; prevObj = object; @@ -1164,7 +1184,7 @@ Object.subclass('Squeak.Image', floatClass = this.specialObjectsArray.pointers[Squeak.splOb_ClassFloat], obj = roots; do { - obj.installFromImage(oopMap, rawBits, compactClassOops, floatClass, littleEndian, nativeFloats); + obj.installFromBits(oopMap, rawBits, compactClassOops, floatClass, littleEndian, nativeFloats); obj = obj.nextObject; } while (obj !== endMarker); return roots; diff --git a/vm.js b/vm.js index e2afbb07..9a392b80 100644 --- a/vm.js +++ b/vm.js @@ -110,7 +110,7 @@ Object.extend(Squeak, Class_superclass: 0, Class_mdict: 1, Class_format: 2, - Class_instVars: null, // 3 or 4 depending on image, see instVarNames() + Class_instVars: null, // 3 or 4 or unknown depending on image, see detectClassInstVarIndex() Class_name: 6, // ClassBinding layout: ClassBinding_value: 1, diff --git a/vm.object.js b/vm.object.js index add14611..5ddb72ac 100644 --- a/vm.object.js +++ b/vm.object.js @@ -32,8 +32,18 @@ Object.subclass('Squeak.Object', if (this._format < 8) { if (this._format != 6) { - if (instSize + indexableSize > 0) - this.pointers = this.fillArray(instSize + indexableSize, nilObj); + if (instSize > 0) { + for (var i = 0; i < instSize; i++) { + this['$' + i] = nilObj; + } + this.pointers = indexableSize > 0 + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); + } + if (indexableSize > 0) { + this.$$ = this.fillArray(indexableSize, nilObj); + if (!this.pointers) this.pointers = this.$$; + } } else // Words if (indexableSize > 0) if (aClass.isFloatClass) { @@ -73,31 +83,48 @@ Object.subclass('Squeak.Object', this.isFloat = original.isFloat; this.float = original.float; } else { - if (original.pointers) this.pointers = original.pointers.slice(0); // copy + const instSize = original.sqClass.classInstSize(); + if (instSize > 0) { + for (var i = 0; i < instSize; i++) { + this['$' + i] = original['$' + i]; + } + this.pointers = original.$$ + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); + } + if (original.$$) { + this.$$ = [...original.$$]; // copy + if (!this.pointers) this.pointers = this.$$; + } 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) { + initFromBits: 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) { + stringFromBits: function(rawBits) { + if (this._format < 8 || this._format >= 12) return ''; + var bits = rawBits.get(this.oop), + bytes = this.decodeBytes(bits.length, bits, 0, this._format & 3); + return Squeak.bytesAsString(bytes); + }, + classNameFromBits: 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"; + return name?.stringFromBits(rawBits) || "Class"; }, - renameFromImage: function(oopMap, rawBits, ccArray) { + classInstSizeFromBits: function(rawBits) { + var spec = rawBits.get(this.oop)[Squeak.Class_format] >> 1; + return ((spec >> 10) & 0xC0) + ((spec >> 1) & 0x3F) - 1; + }, + renameFromBits: 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)); + var instProto = classObj.instProto || classObj.classInstProto(classObj.classNameFromBits(oopMap, rawBits)); if (!instProto) return this; var renamedObj = new instProto; // Squeak.Object renamedObj.oop = this.oop; @@ -106,7 +133,7 @@ Object.subclass('Squeak.Object', renamedObj.hash = this.hash; return renamedObj; }, - installFromImage: function(oopMap, rawBits, ccArray, floatClass, littleEndian, nativeFloats) { + installFromBits: function(oopMap, rawBits, ccArray, floatClass, littleEndian, nativeFloats) { //Install this object by decoding format, and rectifying pointers var ccInt = this.sqClass; // map compact classes @@ -120,15 +147,33 @@ Object.subclass('Squeak.Object', //Formats 0...4 -- Pointer fields if (nWords > 0) { var oops = bits; // endian conversion was already done - this.pointers = this.decodePointers(nWords, oops, oopMap); + var pointers = this.decodePointers(nWords, oops, oopMap); + var instSize = this.sqClass.classInstSizeFromBits(rawBits); + for (var i = 0; i < instSize; i++) { + this['$' + i] = pointers[i]; + } + if (pointers.length === instSize) { + // only inst vars, no indexable fields + this.pointers = this.instVarProxy(instSize); + } else { + if (instSize === 0) { + // no inst vars, only indexable fields + this.$$ = pointers; + this.pointers = this.$$; // no proxy needed + } else { + this.$$ = pointers.slice(instSize); + this.pointers = this.instVarAndIndexableProxy(instSize); + } + } } } 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.$$ = this.decodePointers(numLits+1, oops, oopMap); //header+lits this.bytes = this.decodeBytes(nWords-(numLits+1), bits, numLits+1, this._format & 3); + this.pointers = this.$$; } else if (this._format >= 8) { //Formats 8..11 -- ByteArrays (and ByteStrings) if (nWords > 0) @@ -193,6 +238,62 @@ Object.subclass('Squeak.Object', array[i] = filler; return array; }, + instVarProxy: function(instSize) { + // emulate pointers access + return new Proxy(this, { + get: function(obj, key) { + if (key === 'length') return instSize; + if (key === 'slice') return (...args) => Array.prototype.slice.apply( + Array.from({length: instSize}, (_, i) => obj['$' + i]), + args + ); + const index = parseInt(key); + if (!isNaN(index)) return obj['$' + index]; + debugger; throw Error("unexpected getter: pointers." + key); + }, + set: function(obj, key, value) { + const index = parseInt(key); + if (isNaN(index)) { debugger; throw Error("unexpected setter: pointers." + key); } + obj['$' + index] = value; + return true; + } + }); + }, + instVarAndIndexableProxy: function(instSize) { + // emulate pointers access + return new Proxy(this, { + get: function(obj, key) { + if (key === 'length') return instSize + obj.$$.length; + if (key === 'slice') return (start, end) => { + if (start !== undefined && start === end) return []; // optimization + if (!start) start = 0; + if (start < 0) start += instSize + obj.$$.length; + if (!end) end = instSize + obj.$$.length; + if (end < 0) end += instSize + obj.$$.length; + const result = []; + for (let i = start; i < end; i++) { + if (i < instSize) result.push(obj['$' + i]); + else result.push(obj.$$[i - instSize]); + } + return result; + }; + const index = parseInt(key); + if (!isNaN(index)) { + return index < instSize + ? obj['$' + index] + : obj.$$[index - instSize]; + } + debugger; throw Error("unexpected getter: pointers." + key); + }, + set: function(obj, key, value) { + const index = parseInt(key); + if (isNaN(index)) { debugger; throw Error("unexpected setter: pointers." + key); } + if (key < instSize) obj['$' + key] = value; + else obj.$$[key - instSize] = value; + return true; + } + }); + }, }, 'testing', { isWords: function() { @@ -436,24 +537,39 @@ Object.subclass('Squeak.Object', classInstIsPointers: function() { return this.classInstFormat() <= 4; }, - instVarNames: function() { - // index changed from 4 to 3 in newer images - for (var index = 3; index <= 4; index++) { + ownInstVarNames: function() { + const index = Squeak.Class_instVars; // 3 or 4 or unknown + if (index > 0) { var varNames = this.pointers[index].pointers; if (varNames && varNames.length && varNames[0].bytes) { - return varNames.map(function(each) { - return each.bytesAsString(); - }); + return varNames.map(name => name.bytesAsString()); } } return []; }, allInstVarNames: function() { - var superclass = this.superclass(); - if (superclass.isNil) - return this.instVarNames(); - else - return superclass.allInstVarNames().concat(this.instVarNames()); + if (this._classAllInstVarNames) return this._classAllInstVarNames; + let names; + const instSize = this.classInstSize(); + if (instSize === 0) { + names = []; + } else if (Squeak.Class_instVars > 0) { + const ownInstVarNames = this.ownInstVarNames(); + if (instSize === ownInstVarNames.length) { + names = ownInstVarNames; + } else { + const superclass = this.superclass(); + const superInstVarNames = superclass.allInstVarNames(); + names = superInstVarNames.concat(ownInstVarNames); + } + if (instSize !== names.length) throw Error("allInstVarNames: wrong number of inst vars"); + } else { + names = []; + for (let i = 0; i < instSize; i++) names.push('$' + i); + } + this._classAllInstVarNames = names; + return names; + }, superclass: function() { return this.pointers[0]; @@ -522,7 +638,8 @@ Object.subclass('Squeak.Object', return (this.pointers[0] & 0x20000) > 0; }, methodAddPointers: function(headerAndLits) { - this.pointers = headerAndLits; + this.$$ = headerAndLits; + this.pointers = this.$$; }, methodTempCount: function() { return (this.pointers[0]>>18) & 63; diff --git a/vm.object.spur.js b/vm.object.spur.js index 07d332de..38a44196 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -33,8 +33,18 @@ Squeak.Object.subclass('Squeak.ObjectSpur', this._format = format; if (format < 12) { if (format < 10) { - if (instSize + indexableSize > 0) - this.pointers = this.fillArray(instSize + indexableSize, nilObj); + if (instSize > 0) { + for (var i = 0; i < instSize; i++) { + this['$' + i] = nilObj; + } + this.pointers = indexableSize > 0 + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); + } + if (indexableSize > 0) { + this.$$ = this.fillArray(indexableSize, nilObj); + if (!this.pointers) this.pointers = this.$$; + } } else // Words if (indexableSize > 0) if (aClass.isFloatClass) { @@ -64,7 +74,7 @@ Squeak.Object.subclass('Squeak.ObjectSpur', // 16-23 = 8-bit indexable (plus three odd bits, one unused in 32-bits) // 24-31 = compiled methods (CompiledMethod) (plus three odd bits, one unused in 32-bits) }, - installFromImage: function(oopMap, rawBits, classTable, floatClass, littleEndian, getCharacter, is64Bit) { + installFromBits: function(oopMap, rawBits, classTable, floatClass, littleEndian, getCharacter, is64Bit) { //Install this object by decoding format, and rectifying pointers var classID = this.sqClass; if (classID < 32) throw Error("Invalid class ID: " + classID); @@ -84,7 +94,24 @@ Squeak.Object.subclass('Squeak.ObjectSpur', case 5: // only inst vars (weak) if (nWords > 0) { var oops = bits; // endian conversion was already done - this.pointers = this.decodePointers(nWords, oops, oopMap, getCharacter, is64Bit); + var pointers = this.decodePointers(nWords, oops, oopMap, getCharacter, is64Bit); + var instSize = this.sqClass.classInstSizeFromBits(rawBits, is64Bit); + for (var i = 0; i < instSize; i++) { + this['$' + i] = pointers[i]; + } + if (pointers.length === instSize) { + // only inst vars, no indexable fields + this.pointers = this.instVarProxy(instSize); + } else { + if (instSize === 0) { + // no inst vars, only indexable fields + this.$$ = pointers; + this.pointers = this.$$; // no proxy needed + } else { + this.$$ = pointers.slice(instSize); + this.pointers = this.instVarAndIndexableProxy(instSize); + } + } } break; case 11: // 32 bit array (odd length in 64 bits) @@ -134,9 +161,10 @@ Squeak.Object.subclass('Squeak.ObjectSpur', ? this.decodeWords64(numLits+1, bits, littleEndian) : this.decodeWords(numLits+1, bits, littleEndian), ptrWords = is64Bit ? (numLits + 1) * 2 : numLits + 1; - this.pointers = this.decodePointers(numLits+1, oops, oopMap, getCharacter, is64Bit); //header+lits + this.$$ = this.decodePointers(numLits+1, oops, oopMap, getCharacter, is64Bit); //header+lits this.bytes = this.decodeBytes(nWords-ptrWords, bits, ptrWords, this._format & 3); - if (is64Bit) this.pointers[0] = (bits[1] & 0x80000000) | intHeader; // fix header + if (is64Bit) this.$$[0] = (bits[1] & 0x80000000) | intHeader; // fix header + this.pointers = this.$$; break; default: throw Error("Unknown object format: " + this._format); @@ -268,19 +296,20 @@ Squeak.Object.subclass('Squeak.ObjectSpur', // this._format |= -indexableSize & 3; //deferred to writeTo() this.bytes = new Uint8Array(size); }, - classNameFromImage: function(oopMap, rawBits) { - var name = oopMap.get(rawBits.get(this.oop)[Squeak.Class_name]); - if (name && name._format >= 16 && name._format < 24) { - var bits = rawBits.get(name.oop), - bytes = name.decodeBytes(bits.length, bits, 0, name._format & 7); - return Squeak.bytesAsString(bytes); - } - return "Class"; + stringFromBits: function(rawBits) { + if (this._format < 16 || this._format >= 24) return ''; + var bits = rawBits.get(this.oop), + bytes = this.decodeBytes(bits.length, bits, 0, this._format & 7); + return Squeak.bytesAsString(bytes); + }, + classInstSizeFromBits: function(rawBits, is64Bit) { + var format = rawBits.get(this.oop)[Squeak.Class_format] >> (is64Bit ? 3 : 1); + return format & 0xFFFF; }, - renameFromImage: function(oopMap, rawBits, classTable) { + renameFromBits: function(oopMap, rawBits, classTable) { var classObj = classTable[this.sqClass]; if (!classObj) return this; - var instProto = classObj.instProto || classObj.classInstProto(classObj.classNameFromImage(oopMap, rawBits)); + var instProto = classObj.instProto || classObj.classInstProto(classObj.classNameFromBits(oopMap, rawBits)); if (!instProto) return this; var renamedObj = new instProto; // Squeak.SpurObject renamedObj.oop = this.oop; diff --git a/vm.primitives.js b/vm.primitives.js index 5f0281fd..be333f12 100644 --- a/vm.primitives.js +++ b/vm.primitives.js @@ -983,7 +983,12 @@ Object.subclass('Squeak.Primitives', }, pointsTo: function(rcvr, arg) { if (!rcvr.pointers) return false; - return rcvr.pointers.indexOf(arg) >= 0; + if (rcvr.$$ && rcvr.$$.indexOf(arg) >= 0) return true; + const instSize = rcvr.sqClass.classInstSize(); + for (var i = 0; i < instSize; i++) { + if (rcvr['$' + i] === arg) return true; + } + return false; }, asUint8Array: function(buffer) { // A direct test of the buffer's constructor doesn't work on Safari 10.0. @@ -1200,7 +1205,8 @@ Object.subclass('Squeak.Primitives', allInstancesOf: function(clsObj) { var instances = this.vm.image.allInstancesOf(clsObj); var array = this.vm.instantiateClass(this.vm.specialObjects[Squeak.splOb_ClassArray], instances.length); - array.pointers = instances; + array.$$ = instances; + array.pointers = array.$$; return array; }, identityHash: function(obj) { @@ -1438,10 +1444,11 @@ Object.subclass('Squeak.Primitives', var bytecodeCount = this.stackInteger(1); if (!this.success) return 0; var method = this.vm.instantiateClass(this.vm.stackValue(2), bytecodeCount); - method.pointers = [header]; + method.$$ = [header]; + method.pointers = method.$$; var litCount = method.methodNumLits(); for (var i = 0; i < litCount; i++) - method.pointers.push(this.vm.nilObj); + method.$$.push(this.vm.nilObj); this.vm.popNandPush(1+argCount, method); if (this.vm.breakOnNewMethod) // break on doit this.vm.breakOnMethod = method;