From a2b57cf9611e91c75adbb1f113f519dd53756eba Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 5 Jun 2024 18:01:34 -0700 Subject: [PATCH 01/17] Readme for V2 --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 026d06d9..8932deca 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,34 @@ +V2 BRANCH +========= +This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: + +* each fixed inst var gets its own property instead 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 `p0`, `p1`, ...) + + The goal is faster access than via the `pointers[]` array. + Also, nicer debuggability if we use actual names. + +* new high-performance JIT without context allocation but direct function calls, args passed directly via function parameters, and direct instance var access (see above) + + 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) `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. + + 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 + SqueakJS: A Squeak VM for the Web and Node.js ============================================= From 4b63e3d5c6596fb5c4a2e2d0a419f16dc4c290ef Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 5 Jun 2024 18:06:52 -0700 Subject: [PATCH 02/17] More README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8932deca..dba4a6dd 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: The goal is faster access than via the `pointers[]` array. Also, nicer debuggability if we use actual names. -* new high-performance JIT without context allocation but direct function calls, args passed directly via function parameters, and direct instance var access (see above) +* 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 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. @@ -17,6 +17,10 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: 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. The goal is faster LargeInteger calculations. From 107ff7cdf661e1165230b20b6290a865672b0aa4 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 5 Jun 2024 18:14:27 -0700 Subject: [PATCH 03/17] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dba4a6dd..3b5f56c2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: 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 ============================================= From 05f9c17861eaf83d7c251b71fae7ba85493e28c7 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 5 Jun 2024 18:30:49 -0700 Subject: [PATCH 04/17] Add link to JIT experiments --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b5f56c2..cd7234ea 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: The goal is faster access than via the `pointers[]` array. Also, nicer debuggability if we use actual names. -* 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 +* 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 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. From 3f2a86f12ac4f4b61703ba814bc4ff93277b6123 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Thu, 6 Jun 2024 19:59:49 -0700 Subject: [PATCH 05/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cd7234ea..091b2f51 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: The goal is faster access than via the `pointers[]` array. Also, nicer debuggability if we use actual names. -* 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 my [JIT experiments](https://squeak.js.org/docs/jit.md.html)) +* 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. @@ -21,7 +21,7 @@ This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: 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. +* (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. From d35d3d42203474094ca7ca2d1004f91c10a8040f Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Thu, 6 Jun 2024 20:47:00 -0700 Subject: [PATCH 06/17] we can use modern JS now --- vm.image.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vm.image.js b/vm.image.js index 86b30b12..641e29dc 100644 --- a/vm.image.js +++ b/vm.image.js @@ -324,12 +324,8 @@ Object.subclass('Squeak.Image', 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), + makeFloat: bits => this.instantiateFloat(bits), + makeLargeFromSmall: (hi, lo) => this.instantiateLargeFromSmall(hi, lo), }); obj = obj.nextObject; done++; From e393cbd3fa7199b4db0f1c8578a0ba0f42c0098b Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Tue, 11 Jun 2024 16:51:18 -0700 Subject: [PATCH 07/17] Rename fromImage to fromBits --- vm.image.js | 14 +++++++------- vm.object.js | 10 +++++----- vm.object.spur.js | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/vm.image.js b/vm.image.js index 641e29dc..d9109587 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[oldBaseAddr + object.oop] = renamedObj; @@ -310,7 +310,7 @@ Object.subclass('Squeak.Image', var splObs = oopMap[specialObjectsOopInt]; var compactClasses = rawBits[oopMap[rawBits[splObs.oop][Squeak.splOb_CompactClasses]].oop]; var floatClass = oopMap[rawBits[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); @@ -323,7 +323,7 @@ Object.subclass('Squeak.Image', 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 && { + obj.installFromBits(oopMap, rawBits, compactClasses, floatClass, littleEndian, nativeFloats, is64Bit && { makeFloat: bits => this.instantiateFloat(bits), makeLargeFromSmall: (hi, lo) => this.instantiateLargeFromSmall(hi, lo), }); @@ -1136,7 +1136,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; @@ -1160,7 +1160,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.object.js b/vm.object.js index a0e8c1e0..e2c70634 100644 --- a/vm.object.js +++ b/vm.object.js @@ -78,14 +78,14 @@ Object.subclass('Squeak.Object', 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) { + classNameFromBits: function(oopMap, rawBits) { var name = oopMap[rawBits[this.oop][Squeak.Class_name]]; if (name && name._format >= 8 && name._format < 12) { var bits = rawBits[name.oop], @@ -94,10 +94,10 @@ Object.subclass('Squeak.Object', } return "Class"; }, - renameFromImage: function(oopMap, rawBits, ccArray) { + renameFromBits: function(oopMap, rawBits, ccArray) { var classObj = this.sqClass < 32 ? oopMap[ccArray[this.sqClass-1]] : oopMap[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 +106,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 diff --git a/vm.object.spur.js b/vm.object.spur.js index 1b1dd1e7..222347d4 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -64,7 +64,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); @@ -268,7 +268,7 @@ Squeak.Object.subclass('Squeak.ObjectSpur', // this._format |= -indexableSize & 3; //deferred to writeTo() this.bytes = new Uint8Array(size); }, - classNameFromImage: function(oopMap, rawBits) { + classNameFromBits: function(oopMap, rawBits) { var name = oopMap[rawBits[this.oop][Squeak.Class_name]]; if (name && name._format >= 16 && name._format < 24) { var bits = rawBits[name.oop], @@ -277,10 +277,10 @@ Squeak.Object.subclass('Squeak.ObjectSpur', } return "Class"; }, - 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; From b6a32befdc537ef28431378b9c1971480be3c086 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Tue, 11 Jun 2024 23:33:31 -0700 Subject: [PATCH 08/17] use actual inst var names This uses Smalltalk inst var names for property names prefixed with a `$` (e.g. a Point would use `{ $x: 3, $y: 4 }` in JS). Indexable fields are in an array named `$$` (e.g. in Context you would have `{$sender: ..., $$: [tmp0, tmp1, ...]}`. The `.pointers` property is now a proxy to allow the rest of the VM and primitives to still work. The JIT and optimized primitives will not use this proxy. --- vm.image.js | 24 ++++++ vm.js | 2 +- vm.object.js | 196 ++++++++++++++++++++++++++++++++++++++++------ vm.object.spur.js | 56 ++++++++++--- vm.primitives.js | 8 +- 5 files changed, 247 insertions(+), 39 deletions(-) diff --git a/vm.image.js b/vm.image.js index d9109587..34e6ff7f 100644 --- a/vm.image.js +++ b/vm.image.js @@ -317,6 +317,9 @@ Object.subclass('Squeak.Image', 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() { @@ -360,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[rawBits[splObs.oop][Squeak.splOb_ClassPoint]]; + var classBits = rawBits[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[classBits[index]]; if (!names) continue; + var namesBits = rawBits[names.oop]; if (namesBits.length !== 2) continue; + var x = oopMap[namesBits[0]]; + var xBits = rawBits[x.oop]; + if (String.fromCharCode(xBits[0]) !== 'x') continue; + var y = oopMap[namesBits[1]]; + var yBits = rawBits[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; diff --git a/vm.js b/vm.js index 3f8c4df7..62bb13cf 100644 --- a/vm.js +++ b/vm.js @@ -109,7 +109,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 e2c70634..3847579c 100644 --- a/vm.object.js +++ b/vm.object.js @@ -32,8 +32,19 @@ 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) { + const vars = aClass.allInstVarNames(); + for (var i = 0; i < vars.length; i++) { + this[vars[i]] = nilObj; + } + this.pointers = indexableSize > 0 + ? this.instVarAndIndexableProxy(vars) + : this.instVarProxy(vars); + } + if (indexableSize > 0) { + this.$$ = this.fillArray(indexableSize, nilObj); + if (!this.pointers) this.pointers = this.$$; + } } else // Words if (indexableSize > 0) if (aClass.isFloatClass) { @@ -73,7 +84,19 @@ Object.subclass('Squeak.Object', this.isFloat = original.isFloat; this.float = original.float; } else { - if (original.pointers) this.pointers = original.pointers.slice(0); // copy + const vars = original.sqClass.allInstVarNames(); + if (vars && vars.length) { + for (var i = 0; i < vars.length; i++) { + this[vars[i]] = original[vars[i]]; + } + this.pointers = original.$$ + ? this.instVarAndIndexableProxy(vars) + : this.instVarProxy(vars); + } + 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 } @@ -85,14 +108,56 @@ Object.subclass('Squeak.Object', this._format = fmt; this.hash = hsh; }, + stringFromBits: function(rawBits) { + if (this._format < 8 || this._format >= 12) return ''; + var bits = rawBits[this.oop], + bytes = this.decodeBytes(bits.length, bits, 0, this._format & 3); + return Squeak.bytesAsString(bytes); + }, classNameFromBits: function(oopMap, rawBits) { var name = oopMap[rawBits[this.oop][Squeak.Class_name]]; - if (name && name._format >= 8 && name._format < 12) { - var bits = rawBits[name.oop], - bytes = name.decodeBytes(bits.length, bits, 0, name._format & 3); - return Squeak.bytesAsString(bytes); + return name?.stringFromBits(rawBits) || "Class"; + }, + classInstSizeFromBits: function(rawBits) { + var spec = rawBits[this.oop][Squeak.Class_format] >> 1; + return ((spec >> 10) & 0xC0) + ((spec >> 1) & 0x3F) - 1; + }, + classOwnInstVarNamesFromBits: function(oopMap, rawBits) { + const ownInstVarNames = []; + const myBits = rawBits[this.oop]; + if (Squeak.Class_instVars > 0) { + const varNamesArray = rawBits[myBits[Squeak.Class_instVars]]; + for (let i = 0; i < varNamesArray.length; i++) { + const varName = oopMap[varNamesArray[i]]; + const varStr = varName.stringFromBits(rawBits); + if (!varStr) { debugger ; throw Error("classOwnInstVarNamesFromBits: not a string"); } + ownInstVarNames.push('$' + varStr); // add $ to avoid name clashes + } } - return "Class"; + return ownInstVarNames; + }, + classAllInstVarNamesFromBits: function(oopMap, rawBits) { + if (this._classAllInstVarNames) return this._classAllInstVarNames; + let names; + const instSize = this.classInstSizeFromBits(rawBits); + if (instSize === 0) { + names = []; + } else if (Squeak.Class_instVars > 0) { + const ownInstVarNames = this.classOwnInstVarNamesFromBits(oopMap, rawBits); + if (instSize === ownInstVarNames.length) { + names = ownInstVarNames; + } else { + const superclass = oopMap[rawBits[this.oop][Squeak.Class_superclass]]; + const superInstVarNames = superclass.classAllInstVarNamesFromBits(oopMap, rawBits); + 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; }, renameFromBits: function(oopMap, rawBits, ccArray) { var classObj = this.sqClass < 32 ? oopMap[ccArray[this.sqClass-1]] : oopMap[this.sqClass]; @@ -120,15 +185,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 instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits); + for (var i = 0; i < instVarNames.length; i++) { + this[instVarNames[i]] = pointers[i]; + } + if (pointers.length === instVarNames.length) { + // only inst vars, no indexable fields + this.pointers = this.instVarProxy(instVarNames); + } else { + if (instVarNames.length === 0) { + // no inst vars, only indexable fields + this.$$ = pointers; + this.pointers = this.$$; // no proxy needed + } else { + this.$$ = pointers.slice(instVarNames.length); + this.pointers = this.instVarAndIndexableProxy(instVarNames); + } + } } } 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 +276,59 @@ Object.subclass('Squeak.Object', array[i] = filler; return array; }, + instVarProxy: function(instVarNames) { + // emulate pointers access + return new Proxy(this, { + get: function(obj, key) { + if (key === 'length') return instVarNames.length; + if (key === 'slice') return (...args) => instVarNames.slice(...args).map(name => obj[name]); + const index = parseInt(key); + if (!isNaN(index)) return obj[instVarNames[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[instVarNames[index]] = value; + return true; + } + }); + }, + instVarAndIndexableProxy: function(instVarNames) { + // emulate pointers access + return new Proxy(this, { + get: function(obj, key) { + if (key === 'length') return instVarNames.length + obj.$$.length; + if (key === 'slice') return (start, end) => { + if (start !== undefined && start === end) return []; // optimization + if (!start) start = 0; + if (start < 0) start += instVarNames.length + obj.$$.length; + if (!end) end = instVarNames.length + obj.$$.length; + if (end < 0) end += instVarNames.length + obj.$$.length; + const result = []; + for (let i = start; i < end; i++) { + if (i < instVarNames.length) result.push(obj[instVarNames[i]]); + else result.push(obj.$$[i - instVarNames.length]); + } + return result; + }; + const index = parseInt(key); + if (!isNaN(index)) { + return index < instVarNames.length + ? obj[instVarNames[index]] + : obj.$$[index - instVarNames.length]; + } + 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 < instVarNames.length) obj[instVarNames[key]] = value; + else obj.$$[key - instVarNames.length] = value; + return true; + } + }); + }, }, 'testing', { isWords: function() { @@ -436,24 +572,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 +673,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 222347d4..38a6790b 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -33,8 +33,19 @@ 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) { + const vars = aClass.allInstVarNames(); + for (var i = 0; i < vars.length; i++) { + this[vars[i]] = nilObj; + } + this.pointers = indexableSize > 0 + ? this.instVarAndIndexableProxy(vars) + : this.instVarProxy(vars); + } + if (indexableSize > 0) { + this.$$ = this.fillArray(indexableSize, nilObj); + if (!this.pointers) this.pointers = this.$$; + } } else // Words if (indexableSize > 0) if (aClass.isFloatClass) { @@ -84,7 +95,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 instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits); + for (var i = 0; i < instVarNames.length; i++) { + this[instVarNames[i]] = pointers[i]; + } + if (pointers.length === instVarNames.length) { + // only inst vars, no indexable fields + this.pointers = this.instVarProxy(instVarNames); + } else { + if (instVarNames.length === 0) { + // no inst vars, only indexable fields + this.$$ = pointers; + this.pointers = this.$$; // no proxy needed + } else { + this.$$ = pointers.slice(instVarNames.length); + this.pointers = this.instVarAndIndexableProxy(instVarNames); + } + } } break; case 11: // 32 bit array (odd length in 64 bits) @@ -134,9 +162,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,14 +297,15 @@ Squeak.Object.subclass('Squeak.ObjectSpur', // this._format |= -indexableSize & 3; //deferred to writeTo() this.bytes = new Uint8Array(size); }, - classNameFromBits: function(oopMap, rawBits) { - var name = oopMap[rawBits[this.oop][Squeak.Class_name]]; - if (name && name._format >= 16 && name._format < 24) { - var bits = rawBits[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[this.oop], + bytes = this.decodeBytes(bits.length, bits, 0, this._format & 7); + return Squeak.bytesAsString(bytes); + }, + classInstSizeFromBits: function(rawBits) { + var format = rawBits[this.oop][Squeak.Class_format] >> 1; + return format & 0xFFFF; }, renameFromBits: function(oopMap, rawBits, classTable) { var classObj = classTable[this.sqClass]; diff --git a/vm.primitives.js b/vm.primitives.js index c57b8d1c..3ff2b6f0 100644 --- a/vm.primitives.js +++ b/vm.primitives.js @@ -1191,7 +1191,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) { @@ -1429,10 +1430,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; From 0512e2c0ae92bba653720900c1aa5545d0b30927 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Tue, 11 Jun 2024 23:45:52 -0700 Subject: [PATCH 09/17] Inst var fix --- vm.object.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vm.object.js b/vm.object.js index 3847579c..e853a958 100644 --- a/vm.object.js +++ b/vm.object.js @@ -126,7 +126,8 @@ Object.subclass('Squeak.Object', const ownInstVarNames = []; const myBits = rawBits[this.oop]; if (Squeak.Class_instVars > 0) { - const varNamesArray = rawBits[myBits[Squeak.Class_instVars]]; + const varNames = oopMap[myBits[Squeak.Class_instVars]]; + const varNamesArray = rawBits[varNames.oop]; for (let i = 0; i < varNamesArray.length; i++) { const varName = oopMap[varNamesArray[i]]; const varStr = varName.stringFromBits(rawBits); From e0bd0af4d0648b3e4372e2daf4e3b20660c3fea9 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Tue, 11 Jun 2024 23:57:55 -0700 Subject: [PATCH 10/17] Fix 64bit inst vars --- vm.object.js | 6 +++--- vm.object.spur.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vm.object.js b/vm.object.js index e853a958..b1d8e720 100644 --- a/vm.object.js +++ b/vm.object.js @@ -137,10 +137,10 @@ Object.subclass('Squeak.Object', } return ownInstVarNames; }, - classAllInstVarNamesFromBits: function(oopMap, rawBits) { + classAllInstVarNamesFromBits: function(oopMap, rawBits, is64Bit) { if (this._classAllInstVarNames) return this._classAllInstVarNames; let names; - const instSize = this.classInstSizeFromBits(rawBits); + const instSize = this.classInstSizeFromBits(rawBits, is64Bit); if (instSize === 0) { names = []; } else if (Squeak.Class_instVars > 0) { @@ -149,7 +149,7 @@ Object.subclass('Squeak.Object', names = ownInstVarNames; } else { const superclass = oopMap[rawBits[this.oop][Squeak.Class_superclass]]; - const superInstVarNames = superclass.classAllInstVarNamesFromBits(oopMap, rawBits); + const superInstVarNames = superclass.classAllInstVarNamesFromBits(oopMap, rawBits, is64Bit); names = superInstVarNames.concat(ownInstVarNames); } if (instSize !== names.length) throw Error("allInstVarNames: wrong number of inst vars"); diff --git a/vm.object.spur.js b/vm.object.spur.js index 38a6790b..6a247c8b 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -96,7 +96,7 @@ Squeak.Object.subclass('Squeak.ObjectSpur', if (nWords > 0) { var oops = bits; // endian conversion was already done var pointers = this.decodePointers(nWords, oops, oopMap, getCharacter, is64Bit); - var instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits); + var instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits, is64Bit); for (var i = 0; i < instVarNames.length; i++) { this[instVarNames[i]] = pointers[i]; } @@ -303,8 +303,8 @@ Squeak.Object.subclass('Squeak.ObjectSpur', bytes = this.decodeBytes(bits.length, bits, 0, this._format & 7); return Squeak.bytesAsString(bytes); }, - classInstSizeFromBits: function(rawBits) { - var format = rawBits[this.oop][Squeak.Class_format] >> 1; + classInstSizeFromBits: function(rawBits, is64Bit) { + var format = rawBits[this.oop][Squeak.Class_format] >> (is64Bit ? 3 : 1); return format & 0xFFFF; }, renameFromBits: function(oopMap, rawBits, classTable) { From 09dcb75b7349b61b71dc63b1b362a4f78b81dbb1 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 27 Aug 2025 19:12:25 +0200 Subject: [PATCH 11/17] lint --- .gitignore | 1 + README.md | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) 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..405f99a9 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,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/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", From aa7db9f7b5fb2bdf7986d897d98774e5f4c2a1ff Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 27 Aug 2025 21:11:01 +0200 Subject: [PATCH 12/17] Fix pointsTo --- vm.primitives.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vm.primitives.js b/vm.primitives.js index 3ff2b6f0..cad73b9b 100644 --- a/vm.primitives.js +++ b/vm.primitives.js @@ -974,7 +974,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 vars = rcvr.sqClass.allInstVarNames(); + for (var i = 0; i < vars.length; i++) { + if (rcvr[vars[i]] === arg) return true; + } + return false; }, asUint8Array: function(buffer) { // A direct test of the buffer's constructor doesn't work on Safari 10.0. From f8388b1daecc9722cb3df5b0b4f2a61b2f9c7d1f Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 17 Sep 2025 15:21:11 +0200 Subject: [PATCH 13/17] Post-merge fixups --- vm.image.js | 16 ++++++++-------- vm.object.spur.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vm.image.js b/vm.image.js index cdba99c9..64620463 100644 --- a/vm.image.js +++ b/vm.image.js @@ -367,18 +367,18 @@ Object.subclass('Squeak.Image', // 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[rawBits[splObs.oop][Squeak.splOb_ClassPoint]]; - var classBits = rawBits[classPoint.oop]; + 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[classBits[index]]; if (!names) continue; - var namesBits = rawBits[names.oop]; if (namesBits.length !== 2) continue; - var x = oopMap[namesBits[0]]; - var xBits = rawBits[x.oop]; + 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[namesBits[1]]; - var yBits = rawBits[y.oop]; + var y = oopMap.get(namesBits[1]); + var yBits = rawBits.get(y.oop); if (String.fromCharCode(yBits[0]) !== 'y') continue; return index; } diff --git a/vm.object.spur.js b/vm.object.spur.js index c7887da9..a0db16df 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -304,7 +304,7 @@ Squeak.Object.subclass('Squeak.ObjectSpur', return Squeak.bytesAsString(bytes); }, classInstSizeFromBits: function(rawBits, is64Bit) { - var format = rawBits[this.oop][Squeak.Class_format] >> (is64Bit ? 3 : 1); + var format = rawBits.get(this.oop)[Squeak.Class_format] >> (is64Bit ? 3 : 1); return format & 0xFFFF; }, renameFromBits: function(oopMap, rawBits, classTable) { From d09c8828f6c2e0a8aba996768e191096d7a139dc Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 17 Sep 2025 16:12:20 +0200 Subject: [PATCH 14/17] Don't use actual inst var names but numeric suffix for each --- vm.object.js | 112 ++++++++++++++++------------------------------ vm.object.spur.js | 25 +++++------ vm.primitives.js | 6 +-- 3 files changed, 53 insertions(+), 90 deletions(-) diff --git a/vm.object.js b/vm.object.js index 7f51315f..5ddb72ac 100644 --- a/vm.object.js +++ b/vm.object.js @@ -33,13 +33,12 @@ Object.subclass('Squeak.Object', if (this._format < 8) { if (this._format != 6) { if (instSize > 0) { - const vars = aClass.allInstVarNames(); - for (var i = 0; i < vars.length; i++) { - this[vars[i]] = nilObj; + for (var i = 0; i < instSize; i++) { + this['$' + i] = nilObj; } this.pointers = indexableSize > 0 - ? this.instVarAndIndexableProxy(vars) - : this.instVarProxy(vars); + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); } if (indexableSize > 0) { this.$$ = this.fillArray(indexableSize, nilObj); @@ -84,14 +83,14 @@ Object.subclass('Squeak.Object', this.isFloat = original.isFloat; this.float = original.float; } else { - const vars = original.sqClass.allInstVarNames(); - if (vars && vars.length) { - for (var i = 0; i < vars.length; i++) { - this[vars[i]] = original[vars[i]]; + const instSize = original.sqClass.classInstSize(); + if (instSize > 0) { + for (var i = 0; i < instSize; i++) { + this['$' + i] = original['$' + i]; } this.pointers = original.$$ - ? this.instVarAndIndexableProxy(vars) - : this.instVarProxy(vars); + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); } if (original.$$) { this.$$ = [...original.$$]; // copy @@ -122,44 +121,6 @@ Object.subclass('Squeak.Object', var spec = rawBits.get(this.oop)[Squeak.Class_format] >> 1; return ((spec >> 10) & 0xC0) + ((spec >> 1) & 0x3F) - 1; }, - classOwnInstVarNamesFromBits: function(oopMap, rawBits) { - const ownInstVarNames = []; - const myBits = rawBits.get(this.oop); - if (Squeak.Class_instVars > 0) { - const varNames = oopMap.get(myBits[Squeak.Class_instVars]); - const varNamesArray = rawBits.get(varNames.oop); - for (let i = 0; i < varNamesArray.length; i++) { - const varName = oopMap.get(varNamesArray[i]); - const varStr = varName.stringFromBits(rawBits); - if (!varStr) { debugger ; throw Error("classOwnInstVarNamesFromBits: not a string"); } - ownInstVarNames.push('$' + varStr); // add $ to avoid name clashes - } - } - return ownInstVarNames; - }, - classAllInstVarNamesFromBits: function(oopMap, rawBits, is64Bit) { - if (this._classAllInstVarNames) return this._classAllInstVarNames; - let names; - const instSize = this.classInstSizeFromBits(rawBits, is64Bit); - if (instSize === 0) { - names = []; - } else if (Squeak.Class_instVars > 0) { - const ownInstVarNames = this.classOwnInstVarNamesFromBits(oopMap, rawBits); - if (instSize === ownInstVarNames.length) { - names = ownInstVarNames; - } else { - const superclass = oopMap.get(rawBits.get(this.oop)[Squeak.Class_superclass]); - const superInstVarNames = superclass.classAllInstVarNamesFromBits(oopMap, rawBits, is64Bit); - 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; - }, renameFromBits: function(oopMap, rawBits, ccArray) { var classObj = this.sqClass < 32 ? oopMap.get(ccArray[this.sqClass-1]) : oopMap.get(this.sqClass); if (!classObj) return this; @@ -187,21 +148,21 @@ Object.subclass('Squeak.Object', if (nWords > 0) { var oops = bits; // endian conversion was already done var pointers = this.decodePointers(nWords, oops, oopMap); - var instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits); - for (var i = 0; i < instVarNames.length; i++) { - this[instVarNames[i]] = pointers[i]; + var instSize = this.sqClass.classInstSizeFromBits(rawBits); + for (var i = 0; i < instSize; i++) { + this['$' + i] = pointers[i]; } - if (pointers.length === instVarNames.length) { + if (pointers.length === instSize) { // only inst vars, no indexable fields - this.pointers = this.instVarProxy(instVarNames); + this.pointers = this.instVarProxy(instSize); } else { - if (instVarNames.length === 0) { + if (instSize === 0) { // no inst vars, only indexable fields this.$$ = pointers; this.pointers = this.$$; // no proxy needed } else { - this.$$ = pointers.slice(instVarNames.length); - this.pointers = this.instVarAndIndexableProxy(instVarNames); + this.$$ = pointers.slice(instSize); + this.pointers = this.instVarAndIndexableProxy(instSize); } } } @@ -277,55 +238,58 @@ Object.subclass('Squeak.Object', array[i] = filler; return array; }, - instVarProxy: function(instVarNames) { + instVarProxy: function(instSize) { // emulate pointers access return new Proxy(this, { get: function(obj, key) { - if (key === 'length') return instVarNames.length; - if (key === 'slice') return (...args) => instVarNames.slice(...args).map(name => obj[name]); + 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[instVarNames[index]]; + 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[instVarNames[index]] = value; + obj['$' + index] = value; return true; } }); }, - instVarAndIndexableProxy: function(instVarNames) { + instVarAndIndexableProxy: function(instSize) { // emulate pointers access return new Proxy(this, { get: function(obj, key) { - if (key === 'length') return instVarNames.length + obj.$$.length; + 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 += instVarNames.length + obj.$$.length; - if (!end) end = instVarNames.length + obj.$$.length; - if (end < 0) end += instVarNames.length + obj.$$.length; + 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 < instVarNames.length) result.push(obj[instVarNames[i]]); - else result.push(obj.$$[i - instVarNames.length]); + if (i < instSize) result.push(obj['$' + i]); + else result.push(obj.$$[i - instSize]); } return result; }; const index = parseInt(key); if (!isNaN(index)) { - return index < instVarNames.length - ? obj[instVarNames[index]] - : obj.$$[index - instVarNames.length]; + 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 < instVarNames.length) obj[instVarNames[key]] = value; - else obj.$$[key - instVarNames.length] = value; + if (key < instSize) obj['$' + key] = value; + else obj.$$[key - instSize] = value; return true; } }); diff --git a/vm.object.spur.js b/vm.object.spur.js index a0db16df..38a44196 100644 --- a/vm.object.spur.js +++ b/vm.object.spur.js @@ -34,13 +34,12 @@ Squeak.Object.subclass('Squeak.ObjectSpur', if (format < 12) { if (format < 10) { if (instSize > 0) { - const vars = aClass.allInstVarNames(); - for (var i = 0; i < vars.length; i++) { - this[vars[i]] = nilObj; + for (var i = 0; i < instSize; i++) { + this['$' + i] = nilObj; } this.pointers = indexableSize > 0 - ? this.instVarAndIndexableProxy(vars) - : this.instVarProxy(vars); + ? this.instVarAndIndexableProxy(instSize) + : this.instVarProxy(instSize); } if (indexableSize > 0) { this.$$ = this.fillArray(indexableSize, nilObj); @@ -96,21 +95,21 @@ Squeak.Object.subclass('Squeak.ObjectSpur', if (nWords > 0) { var oops = bits; // endian conversion was already done var pointers = this.decodePointers(nWords, oops, oopMap, getCharacter, is64Bit); - var instVarNames = this.sqClass.classAllInstVarNamesFromBits(oopMap, rawBits, is64Bit); - for (var i = 0; i < instVarNames.length; i++) { - this[instVarNames[i]] = pointers[i]; + var instSize = this.sqClass.classInstSizeFromBits(rawBits, is64Bit); + for (var i = 0; i < instSize; i++) { + this['$' + i] = pointers[i]; } - if (pointers.length === instVarNames.length) { + if (pointers.length === instSize) { // only inst vars, no indexable fields - this.pointers = this.instVarProxy(instVarNames); + this.pointers = this.instVarProxy(instSize); } else { - if (instVarNames.length === 0) { + if (instSize === 0) { // no inst vars, only indexable fields this.$$ = pointers; this.pointers = this.$$; // no proxy needed } else { - this.$$ = pointers.slice(instVarNames.length); - this.pointers = this.instVarAndIndexableProxy(instVarNames); + this.$$ = pointers.slice(instSize); + this.pointers = this.instVarAndIndexableProxy(instSize); } } } diff --git a/vm.primitives.js b/vm.primitives.js index aab4ddc7..be333f12 100644 --- a/vm.primitives.js +++ b/vm.primitives.js @@ -984,9 +984,9 @@ Object.subclass('Squeak.Primitives', pointsTo: function(rcvr, arg) { if (!rcvr.pointers) return false; if (rcvr.$$ && rcvr.$$.indexOf(arg) >= 0) return true; - const vars = rcvr.sqClass.allInstVarNames(); - for (var i = 0; i < vars.length; i++) { - if (rcvr[vars[i]] === arg) return true; + const instSize = rcvr.sqClass.classInstSize(); + for (var i = 0; i < instSize; i++) { + if (rcvr['$' + i] === arg) return true; } return false; }, From 2c2c732ac09227d04ca8c1e5210ad3f3a7124bba Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 17 Sep 2025 17:05:58 +0200 Subject: [PATCH 15/17] Use direct inst vars in jit --- jit.js | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/jit.js b/jit.js index 6129a2b1..2cda459d 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,7 +1081,7 @@ 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; default: @@ -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]) From 448d140f76dfdd61432c496b7bed32053e5ac0fc Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Wed, 17 Sep 2025 19:54:29 +0200 Subject: [PATCH 16/17] Fix dirty marking in jit --- jit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jit.js b/jit.js index 2cda459d..f8d63130 100644 --- a/jit.js +++ b/jit.js @@ -1083,7 +1083,7 @@ to single-step. switch(target) { 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); } From 1de1c781f00a1003b23c2f74db195674fc45cea2 Mon Sep 17 00:00:00 2001 From: Vanessa Freudenberg Date: Fri, 19 Sep 2025 16:10:49 +0200 Subject: [PATCH 17/17] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e3d14f55..24284eb5 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ V2 BRANCH ========= This is the work-in-progress branch for SqueakJS 2.0. Things I want to change: -* each fixed inst var gets its own property instead for direct access instead of being indexed in `pointers[]`. There will be a compatibility accessor for primitives that use indexed access. +* (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 `p0`, `p1`, ...) + 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.