VM Specification
BrowserSm Virtual Machine Specification
This document specifies the JavaScript-based implementation of the Squeak Smalltalk virtual machine that enables standard Squeak images to run in web browsers.
Table of Contents
- Overview
- Bytecode Interpreter
- Object Memory
- Primitive Operations
- Garbage Collection
- Image Format
- Platform Integration
- Performance Considerations
Overview
BrowserSm implements a complete Squeak virtual machine in JavaScript, targeting ECMAScript 2020+ and modern web browsers. The VM provides:
- Full Bytecode Compatibility: Executes all standard Squeak bytecodes
- Spur Object Memory: Implements the Spur 64-bit object format
- Standard Primitives: Maps Squeak primitives to browser capabilities
- Image Loading: Boots standard .image files without conversion
- Live Development: Full debugging and inspection capabilities
Design Goals
- Correctness: Semantically identical behavior to the reference Cog VM
- Compatibility: Run unmodified Squeak 6.0+ images
- Performance: Acceptable speed for interactive development
- Transparency: Integration with browser developer tools
- Security: Safe execution of untrusted Smalltalk code
Bytecode Interpreter
Instruction Set
The VM implements the complete Squeak bytecode instruction set as specified in the Blue Book with Spur extensions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Bytecode dispatch table
const BytecodeTable = {
// Stack operations
0x00: pushReceiver, // push self
0x01: pushTrue, // push true
0x02: pushFalse, // push false
0x03: pushNil, // push nil
0x04: pushMinusOne, // push -1
0x05: pushZero, // push 0
0x06: pushOne, // push 1
0x07: pushTwo, // push 2
// Instance variable access
0x08: pushInstVar0, // push inst var 0
0x09: pushInstVar1, // push inst var 1
// ... (0x08-0x0F)
// Temporary variable access
0x10: pushTemp0, // push temp 0
0x11: pushTemp1, // push temp 1
// ... (0x10-0x1F)
// Literal access
0x20: pushLiteral0, // push literal 0
0x21: pushLiteral1, // push literal 1
// ... (0x20-0x3F)
// Send operations
0x90: send0Args, // send with 0 args
0x91: send1Args, // send with 1 args
// ... message sends
// Jump operations
0xA0: jumpForward, // unconditional jump
0xA1: jumpBackward, // backward jump
0xA2: jumpIfTrue, // conditional jump
0xA3: jumpIfFalse, // conditional jump
// Return operations
0xF0: returnReceiver, // return self
0xF1: returnTrue, // return true
0xF2: returnFalse, // return false
0xF3: returnNil, // return nil
0xF4: returnTop, // return top of stack
};
Execution Engine
The interpreter uses a threaded code approach optimized for JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SqueakInterpreter {
constructor(vm) {
this.vm = vm;
this.instructionPointer = 0;
this.stackPointer = 0;
this.homeContext = null;
this.activeContext = null;
}
interpret() {
while (this.vm.running) {
const bytecode = this.fetchBytecode();
const method = BytecodeTable[bytecode];
if (!method) {
throw new Error(`Unknown bytecode: 0x${bytecode.toString(16)}`);
}
method.call(this);
// Yield control periodically
if (this.vm.shouldYield()) {
this.vm.scheduleResume();
return;
}
}
}
fetchBytecode() {
const method = this.activeContext.method;
const bytecode = method.bytecodes[this.instructionPointer];
this.instructionPointer++;
return bytecode;
}
}
Object Memory
Spur Object Format
BrowserSm implements the Spur 64-bit object format using JavaScript objects with hidden classes for performance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Object header structure
class SqueakObject {
constructor(classOop, size, format) {
this.classOop = classOop; // 22 bits
this.size = size; // 32 bits
this.format = format; // 5 bits
this.hash = 0; // 22 bits
this.isMarked = false; // GC mark bit
this.slots = new Array(size); // Object slots
}
// Immediate object detection
static isImmediate(oop) {
return (oop & 1) !== 0; // SmallIntegers have low bit set
}
// SmallInteger encoding/decoding
static encodeSmallInteger(value) {
return (value << 1) | 1;
}
static decodeSmallInteger(oop) {
return oop >> 1;
}
}
Object Table
Objects are managed through an object table that maps object pointers to JavaScript objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ObjectMemory {
constructor() {
this.objects = new Map(); // OOP -> Object mapping
this.freeList = []; // Available OOP slots
this.nextOOP = 1000; // Next available OOP
this.youngSpace = new Set(); // Young generation
this.oldSpace = new Set(); // Old generation
}
allocateObject(classOop, size, format) {
const oop = this.freeList.pop() || this.nextOOP++;
const object = new SqueakObject(classOop, size, format);
this.objects.set(oop, object);
this.youngSpace.add(oop);
return oop;
}
fetchObject(oop) {
if (SqueakObject.isImmediate(oop)) {
return SqueakObject.decodeSmallInteger(oop);
}
return this.objects.get(oop);
}
}
Primitive Operations
Primitive Interface
Primitives map Squeak operations to JavaScript/browser functionality:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class PrimitiveTable {
static primitives = {
1: arithmeticAdd, // SmallInteger +
2: arithmeticSubtract, // SmallInteger -
3: arithmeticLessThan, // SmallInteger <
4: arithmeticGreaterThan, // SmallInteger >
5: arithmeticLessOrEqual, // SmallInteger <=
6: arithmeticGreaterOrEqual,// SmallInteger >=
7: arithmeticEqual, // SmallInteger =
8: arithmeticNotEqual, // SmallInteger ~=
9: arithmeticMultiply, // SmallInteger *
10: arithmeticDivide, // SmallInteger /
11: arithmeticMod, // SmallInteger \\
12: arithmeticDiv, // SmallInteger //
// Array primitives
60: arrayAt, // Array at:
61: arrayAtPut, // Array at:put:
62: arraySize, // Array size
// String primitives
63: stringAt, // String at:
64: stringAtPut, // String at:put:
65: stringSize, // String size
// Stream primitives
65: streamNext, // Stream next
66: streamNextPut, // Stream next:
67: streamAtEnd, // Stream atEnd
// Block primitives
81: blockValue, // Block value
82: blockValueWith, // Block value:
83: blockValueWithWith, // Block value:value:
// Process primitives
86: processSignal, // Semaphore signal
87: processWait, // Semaphore wait
88: processResume, // Process resume
89: processSuspend, // Process suspend
// System primitives
110: objectClass, // Object class
111: objectIdentityHash, // Object identityHash
112: objectIdentical, // Object ==
113: objectClass, // Object class
114: objectSize, // Object basicSize
115: objectAt, // Object basicAt:
116: objectAtPut, // Object basicAt:put:
// Platform primitives
230: platformYield, // Yield to platform
231: platformMilliseconds, // Platform milliseconds
232: platformBeep, // Platform beep
233: platformSetCursor, // Platform cursor
// Browser-specific primitives
300: domElementCreate, // Create DOM element
301: domElementSetAttribute,// Set DOM attribute
302: domElementAddChild, // Add child to DOM
303: canvasGetContext, // Get canvas context
304: canvasDrawRect, // Draw rectangle
305: fetchURL, // HTTP fetch
};
static invoke(primitiveNumber, args) {
const primitive = this.primitives[primitiveNumber];
if (!primitive) {
throw new Error(`Primitive ${primitiveNumber} not implemented`);
}
return primitive(...args);
}
}
Browser Integration Primitives
Special primitives provide access to browser APIs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// DOM manipulation primitives
function domElementCreate(tagName) {
const element = document.createElement(tagName);
return wrapDOMElement(element);
}
function domElementSetAttribute(element, name, value) {
const domElement = unwrapDOMElement(element);
domElement.setAttribute(name, value);
return element;
}
// Canvas rendering primitives
function canvasGetContext(canvas, type) {
const canvasElement = unwrapDOMElement(canvas);
const context = canvasElement.getContext(type);
return wrapCanvasContext(context);
}
function canvasDrawRect(context, x, y, width, height) {
const ctx = unwrapCanvasContext(context);
ctx.fillRect(x, y, width, height);
return context;
}
// Network primitives
async function fetchURL(url, options) {
try {
const response = await fetch(url, options);
return wrapHTTPResponse(response);
} catch (error) {
throw new SqueakError(error.message);
}
}
Garbage Collection
Generational Collection
BrowserSm implements a generational garbage collector optimized for JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class GarbageCollector {
constructor(memory) {
this.memory = memory;
this.youngSpaceSize = 0;
this.oldSpaceSize = 0;
this.youngSpaceLimit = 1024 * 1024; // 1MB
this.collectionCount = 0;
}
// Scavenge young space
scavenge() {
const survivors = new Set();
const roots = this.getRootObjects();
// Mark phase: mark all reachable objects
for (const root of roots) {
this.mark(root, survivors);
}
// Sweep phase: promote survivors, collect garbage
for (const oop of this.memory.youngSpace) {
if (survivors.has(oop)) {
this.memory.oldSpace.add(oop);
} else {
this.memory.objects.delete(oop);
this.memory.freeList.push(oop);
}
}
this.memory.youngSpace.clear();
this.youngSpaceSize = 0;
}
// Full collection of both generations
fullCollect() {
const survivors = new Set();
const roots = this.getRootObjects();
// Mark all reachable objects
for (const root of roots) {
this.mark(root, survivors);
}
// Sweep both spaces
const allObjects = new Set([
...this.memory.youngSpace,
...this.memory.oldSpace
]);
for (const oop of allObjects) {
if (!survivors.has(oop)) {
this.memory.objects.delete(oop);
this.memory.freeList.push(oop);
}
}
this.memory.youngSpace.clear();
this.memory.oldSpace = survivors;
}
mark(oop, marked) {
if (marked.has(oop)) return;
const object = this.memory.fetchObject(oop);
if (!object) return;
marked.add(oop);
// Mark referenced objects
for (const slot of object.slots) {
if (slot && typeof slot === 'number') {
this.mark(slot, marked);
}
}
}
getRootObjects() {
const roots = new Set();
// Add VM roots
roots.add(this.memory.specialObjects);
roots.add(this.memory.activeContext);
roots.add(this.memory.homeContext);
// Add stack references
if (this.memory.activeContext) {
for (const stackItem of this.memory.stackItems) {
if (typeof stackItem === 'number') {
roots.add(stackItem);
}
}
}
return roots;
}
}
Image Format
Image Loading
BrowserSm loads standard Squeak .image files:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ImageLoader {
async loadImage(imageData) {
const reader = new ImageReader(imageData);
// Read image header
const header = reader.readHeader();
this.validateHeader(header);
// Read object table
const objectTable = reader.readObjectTable(header);
// Read object data
const objects = await reader.readObjects(objectTable);
// Initialize VM with loaded objects
return this.initializeVM(objects, header);
}
validateHeader(header) {
if (header.magic !== 0x1966) {
throw new Error('Invalid image format');
}
if (header.version < 68000) {
throw new Error('Unsupported image version');
}
}
initializeVM(objects, header) {
const vm = new SqueakVM();
vm.memory.objects = objects;
vm.memory.specialObjects = objects.get(header.specialObjectsOOP);
vm.setInitialContext(header.activeContextOOP);
return vm;
}
}
Image Saving
Images can be saved to browser storage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ImageSaver {
async saveImage(vm, filename) {
const writer = new ImageWriter();
// Write header
const header = this.createHeader(vm);
writer.writeHeader(header);
// Write object table
const objectTable = this.createObjectTable(vm.memory);
writer.writeObjectTable(objectTable);
// Write object data
writer.writeObjects(vm.memory.objects);
// Save to browser storage
const imageData = writer.getImageData();
await this.saveToStorage(filename, imageData);
}
async saveToStorage(filename, data) {
// Use IndexedDB for large files
const db = await this.openDatabase();
const transaction = db.transaction(['images'], 'readwrite');
const store = transaction.objectStore('images');
await store.put({
name: filename,
data: data,
timestamp: Date.now()
});
}
}
Platform Integration
Event Handling
Browser events are mapped to Squeak event objects:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class EventHandler {
constructor(vm) {
this.vm = vm;
this.setupEventListeners();
}
setupEventListeners() {
// Mouse events
document.addEventListener('mousedown', (e) => {
this.handleMouseEvent(e, 'mouseDown');
});
document.addEventListener('mousemove', (e) => {
this.handleMouseEvent(e, 'mouseMove');
});
// Keyboard events
document.addEventListener('keydown', (e) => {
this.handleKeyEvent(e, 'keyDown');
});
// Window events
window.addEventListener('resize', (e) => {
this.handleWindowEvent(e, 'resize');
});
}
handleMouseEvent(domEvent, eventType) {
const squeakEvent = this.vm.createMouseEvent(
eventType,
domEvent.clientX,
domEvent.clientY,
domEvent.buttons,
domEvent.timeStamp
);
this.vm.queueEvent(squeakEvent);
}
handleKeyEvent(domEvent, eventType) {
const squeakEvent = this.vm.createKeyboardEvent(
eventType,
domEvent.key,
domEvent.keyCode,
domEvent.modifiers,
domEvent.timeStamp
);
this.vm.queueEvent(squeakEvent);
}
}
Performance Considerations
Optimization Strategies
- Hidden Classes: Use consistent object shapes for JavaScript optimization
- Inline Caching: Cache method lookups and primitive operations
- Bytecode Caching: Pre-compile frequently executed methods
- Stack Management: Minimize JavaScript call stack usage
- Memory Pooling: Reuse object allocations where possible
Measurement and Profiling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class VMProfiler {
constructor(vm) {
this.vm = vm;
this.stats = {
bytecodeExecuted: 0,
primitivesInvoked: 0,
garbageCollections: 0,
methodLookups: 0
};
}
recordBytecode(bytecode) {
this.stats.bytecodeExecuted++;
// Sample execution for profiling
if (this.stats.bytecodeExecuted % 10000 === 0) {
this.sampleExecution();
}
}
sampleExecution() {
const sample = {
context: this.vm.activeContext,
method: this.vm.activeContext.method,
pc: this.vm.instructionPointer,
timestamp: performance.now()
};
this.executionSamples.push(sample);
}
getPerformanceReport() {
return {
totalBytecodes: this.stats.bytecodeExecuted,
bytecodeRate: this.calculateBytecodeRate(),
memoryUsage: this.getMemoryUsage(),
hotMethods: this.getHotMethods(),
gcPerformance: this.getGCStats()
};
}
}
This specification ensures that BrowserSm provides a complete, correct, and performant implementation of the Squeak virtual machine in JavaScript, enabling any standard Squeak application to run in web browsers without modification.