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

  1. Overview
  2. Bytecode Interpreter
  3. Object Memory
  4. Primitive Operations
  5. Garbage Collection
  6. Image Format
  7. Platform Integration
  8. 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

  1. Correctness: Semantically identical behavior to the reference Cog VM
  2. Compatibility: Run unmodified Squeak 6.0+ images
  3. Performance: Acceptable speed for interactive development
  4. Transparency: Integration with browser developer tools
  5. 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

  1. Hidden Classes: Use consistent object shapes for JavaScript optimization
  2. Inline Caching: Cache method lookups and primitive operations
  3. Bytecode Caching: Pre-compile frequently executed methods
  4. Stack Management: Minimize JavaScript call stack usage
  5. 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.