Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
node_modules/
gh-pages
images
dist/*.min.js*
*.code-workspace
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
=============================================

Expand Down Expand Up @@ -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
Expand Down
48 changes: 24 additions & 24 deletions jit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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), "]");
Expand All @@ -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++];
Expand Down Expand Up @@ -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++];
Expand All @@ -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++];
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@codefrau/squeakjs",
"version": "1.3.3",
"description": "Virtual Machine for Squeak Smalltalk and derivatives",
"author": "Vanessa Freudenberg <[email protected]> (https://twitter.com/codefrau)",
"author": "Vanessa Freudenberg <[email protected]>",
"repository": "https://github.com/codefrau/SqueakJS",
"license": "MIT",
"browser": "squeak.js",
Expand Down
46 changes: 33 additions & 13 deletions vm.image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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++;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading