@powerlang/egg-js
Version:
A Smalltalk VM that runs on top of JavaScript
508 lines (394 loc) • 18.2 kB
JavaScript
// here we define a few base objects/classes/methods that we are going to add to the js environment
// for our smalltalk VM.
// The code of VM itself is transpiled to native JS, so VM's true is JS true, VM numbers are JS numbers.
// On the other hand, the Smalltalk image to be loaded has a true object that is a JS object (diferent from
// true), same for false, etc.
// Support for transpiling Smalltalk cascades in VM code:
// JS doesn't support Smalltalk cascades directly. Our transliterator uses this _cascade function
// instead, generating code for evaluating the receiver with a closure that executes all the messages
// with that receiver.
globalThis._cascade = function(receiver, cascadeStatements) { return cascadeStatements(receiver);}
// Support for nil in VM code:
// Unfortunately it is not possible to extend JS null with methods, so we define an object for nil
// and try to avoid using null anywhere
globalThis.nil = {}
Object.prototype.ifNil_ = function(closure) { return nil === this? closure() : this }
Object.prototype.ifNotNil_ = function(closure) { return nil === this? nil : closure(this)}
Object.prototype.ifNil_ifNotNil_ = function(closureNil, closureNotNil) {
return nil === this? closureNil() : closureNotNil(this) }
Object.prototype.isNil = function() { return (nil === this) }
Object.prototype.notNil = function() { return (!this.isNil()) }
//Object.prototype.value = function () { return this; }
Object.prototype.eval_ = function(string) {
return eval(string) }
Object.prototype.isSmallInteger = function(value) { return Number.isInteger(value); }
Object.prototype.isCollection = function() { return false; }
Array.prototype.isCollection = function() { return true; }
Map.prototype.isCollection = function() { return true; }
Object.prototype.isBlock = function() { return false; }
Function.prototype.isBlock = function() { return true; }
Object.prototype.initialize = function() { return this;}
Object.prototype.basicNew = function() { return new this; }
Object.prototype.new = function() { const obj = new this; obj.initialize(); return obj; }
Object.prototype.class = function() { return this.constructor; }
Object.prototype.asString = function() { return this.toString(); }
// add some Smalltalk-ish methods to JS booleans
Boolean.prototype.ifTrue_ = function(closure) { return true === this? closure() : nil }
Boolean.prototype.ifFalse_ = function(closure) { return true === this? nil : closure() }
Boolean.prototype.ifTrue_ifFalse_ = function(closureTrue, closureFalse) { return true === this? closureTrue() : closureFalse() }
Boolean.prototype.or_ = function (closure) { return this || closure() }
Boolean.prototype.orNot_ = function (closure) { return this || !closure() }
Boolean.prototype.and_ = function (closure) { return this && closure() }
Boolean.prototype.andNot_ = function (closure) { return this && !closure() }
Boolean.prototype.not = function () { return !this }
Array.new_ = function(size) { return new Array(size).fill(nil); }
Array.newBytes_ = function(size) { return new Array(size).fill(0); }
Array.new_withAll_ = function(size, value) { return Array(size).fill(value); }
Array.with_with_ = function(first, second) { return [first, second]; }
Array.prototype.size = function() { return this.length; }
Array.prototype.isEmpty = function() { return this.length == 0; }
Array.prototype.asArray = function() { return this; }
Array.prototype.at_ = function(index) { return this[index-1]; }
Array.prototype.at_put_ = function(index, object) { return this[index-1] = object; }
Array.prototype.atAllPut_ = function(value) { return this.fill(value); }
Array.prototype.allButLast = function() { return this.slice(0,-1); }
Array.prototype.first = function() { return this[0]; }
Array.prototype.second = function() { return this[1]; }
Array.prototype.last = function() { return this[this.length-1]; }
Array.prototype.asString = function() { return String.fromCharCode.apply(null, this); }
Array.prototype.replaceFrom_to_with_startingAt_ = function(start, end, other, first) {
const size = end - start + 1;
this.splice(start - 1, size, ...other.slice(first - 1, first - 1 + size));
return this;
}
Array.prototype.do_ = function(closure) {
this.forEach(closure);
}
Array.prototype.withIndexDo_ = function(closure) {
this.forEach((element, index) => {closure(element, index + 1)} );
}
Array.prototype.do_separatedBy_ = function(closure, separated) {
this.forEach((value, index) => {
closure(value);
if (!Object.is(this.length - 1, index)) {separated()}
});
}
Array.prototype.detect_ = function(block) { return this.find(block); }
String.prototype.asSymbol = function() { return this; }
String.prototype.asString = function() { return this; }
String.prototype._comma = function(string) { return this.concat(string); }
Array.prototype.add_ = function(object) { return this.push(object); }
Array.prototype._comma = function(array) { return this.concat(array); }
Array.prototype.copyWith_ = function(object) { return this.concat([object]); }
Array.prototype._equal = function(value) {
return Array.isArray(value) &&
this.length === value.length &&
this.every((val, index) => val === value[index]);
}
Array.prototype.asLittleEndianPositiveInteger = function() {
let bigInteger = 0n;
for (let i = 0; i < this.length; i++) {
bigInteger += BigInt(this[i]) << BigInt(8 * i);
}
return bigInteger;
}
Array.prototype.asLittleEndianNegativeInteger = function() {
return this.asLittleEndianPositiveInteger() - (BigInt(1) << BigInt(64));
}
String.prototype.asByteArray = function() { return Array.from(this).map((char) => char.charCodeAt(0)); }
String.prototype.beginsWith_ = function(string) { return this.startsWith(string); }
Map.withAll_ = function(associations) {
const result = new this;
associations.forEach((assoc) => result.set(assoc.key(), assoc.value()));
return result;
}
Map.prototype.at_ = function(key) { return this.get(key); }
Map.prototype.at_put_ = function(key, value) { return this.set(key, value); }
Map.prototype.at_ifPresent_ = function(key, closure) {
if (this.has(key))
return closure(this.get(key));
else
return nil;
}
Map.prototype.at_ifAbsentPut_ = function(key, closure) {
let v = this.get(key);
if (v)
return v;
else
v = closure();
this.set(key, v);
return v;
}
Map.prototype.removeKey_ifAbsent_ = function(key, closure) {
if (!this.delete(key)) { closure();}
}
Map.prototype.keysAndValuesDo_ = function(closure) {
this.forEach((value, key) => closure(key, value));
}
Map.prototype.isEmpty = function() { return this.size == 0}
// ~~~~~~~~~~~~~~~~~~~~ Block Closures ~~~~~~~~~~~~~~~~~~~~~~~~
Function.prototype.value = function () {
return this();
}
Function.prototype.value_ = function (a) {
return this(a);
}
Function.prototype.value_value_ = function (a, b) {
return this(a, b);
}
Function.prototype.value_value_value_ = function (a, b, c) {
return this(a, b, c);
}
// loop helpers
Function.prototype.whileTrue = function () {
while(this()) { }
return nil;
}
Function.prototype.whileFalse = function () {
while(!this()) { }
return nil;
}
Function.prototype.whileTrue_ = function (block) {
while(this()) { block() }
return nil;
}
Function.prototype.whileFalse_ = function (block) {
while(!this()) { block() }
return nil;
}
Object.prototype._arrow = function(value) {
const t = this;
return {
key: function() {return t; },
value: function() {return value; },
key_: function(k) {this.key = function() { return k; }; return this;},
value_: function(v) {this.value = function() { return v; }; return this;}
};
}
Object.prototype._equal = function(value) { return this == value; }
Object.prototype._notEqual = function(value) { return this != value; }
Object.prototype._equalEqual = function(value) { return this === value; }
Object.prototype._notEqualEqual = function(value) { return this !== value; }
// add some Smalltalk-ish methods to JS numbers
Number.prototype._lessEqualThan = function(value) { return this <= value; }
Number.prototype._lessThan = function(value) { return this < value; }
Number.prototype._greaterEqualThan = function(value) { return this >= value; }
Number.prototype._greaterThan = function(value) { return this > value; }
Number.prototype.byteAt_ = function(index) {
const shift = (index - 1) << 3;
return (this >> shift) & 0xFF;
}
Number.prototype._plus = function(value) { return this + value; }
Number.prototype._minus = function(value) { return this - value; }
Number.prototype._times = function(value) { return this * value; }
Number.prototype._slash = function(value) { return this / value; }
Number.prototype._modulo = function(value) { return this % value; }
Number.prototype._integerQuotient = function(value) { return Math.floor(this / value); }
Number.prototype._and = function(value) { return this.bitAnd_(value); }
Number.prototype._or = function(value) { return this.bitOr_(value); }
Number.prototype.bitAnd_ = function(value) { return Number(BigInt(this) & BigInt(value)); }
Number.prototype.bitOr_ = function(value) { return Number(BigInt(this) | BigInt(value)); }
Number.prototype.bitXor_ = function(value) { return Number(BigInt(this) ^ BigInt(value)); }
Number.prototype.bitShift_ = function(value) {
return value > 0 ?
Number(BigInt(this) << BigInt(value)) :
Number(BigInt(this) >> BigInt(-value)); }
Number.prototype._shiftLeft = function(value) { return Number(BigInt(this) << BigInt(value)); }
Number.prototype._shiftRight = function(value) { return Number(BigInt(this) >> BigInt(value)); }
Number.prototype.anyMask_ = function(value) { return (this.bitAnd_(value)) != 0; }
Number.prototype.noMask_ = function(value) { return (this.bitAnd_(value)) == 0; }
Number.prototype.timesRepeat_ = function(closure) { for (let i = 0; i < this; i++) { closure(); } }
Number.prototype.to_do_ = function(limit, closure) { for (let i = this; i <= limit; i++) { closure(i); } }
Number.prototype.to_by_do_ = function(limit, increment, closure) {
if (increment > 0)
for (let i = this; i <= limit; i=i+increment) { closure(i); }
else
for (let i = this; i >= limit; i=i+increment) { closure(i); }
}
import EggObject from "./interpreter/EggObject.js";
import EggByteObject from "./interpreter/EggByteObject.js";
import EggSlotObject from "./interpreter/EggSlotObject.js";
import EggHeapObject from "./interpreter/EggHeapObject.js";
import EggSmallInteger from "./interpreter/EggSmallInteger.js";
Object.prototype.isEggObject = function() { return this instanceof EggObject; }
EggSlotObject.prototype.pointersSize = function() { return this.size(); }
EggSlotObject.prototype.size = function() { return this._slots.length; }
EggByteObject.prototype.size = function() { return this._bytes.length; }
// ~~~~~~~~~~~~~~~~~~~~ Stretch ~~~~~~~~~~~~~~~~~~~~~~~~
let Stretch = class {
constructor(start, end) {this.start = start; this.end = end;};
length() { return this.end - this.start + 1;}
}
Number.prototype.thru = function(value) { return new Stretch(this, value); }
Number.prototype.bitsAt_ = function(stretch) {
let shifted = this >> (stretch.start - 1);
let mask = 1 << stretch.length();
return shifted & (mask - 1)
}
Number.prototype.bitsAt_put_ = function(stretch, value) {
let shifted = this >> (stretch.start - 1);
let max = 1 << stretch.length();
if (value >= max)
{ throw 'invalid argument'; };
return this.bitsClear_(stretch) | shifted;
}
Number.prototype.bitsClear_ = function(stretch, value) {
let mask = (1 << stretch.end) - (1 << (stretch.start - 1));
return this & (mask ^ -1)
}
// ~~~~~~~~~~~~~~~~~~~~ Interval ~~~~~~~~~~~~~~~~~~~~~~~~
let Interval = class {
constructor(start, end, step = 1) {this.start = start; this.end = end; this.step = 1};
at_(anInteger) {
if (anInteger > 0) {
const result = this.start + (anInteger - 1 * this.step);
const min = Math.min(this.start, this.end);
const max = Math.max(this.start, this.end);
if (min <= result && result <= max) return result;
if (anInteger == this.size() ) return this.end;
}
throw "outOfBoundsIndex";
}
do_(closure) {
if (this.step > 0) {
for (let i = this.start; i <= this.end; i=i+this.step) { closure(i) }
} else {
for (let i = this.start; i >= this.end; i=i+this.step) { closure(i) }
}
}
collect_(closure) {
const result = [];
const s = this.size();
for (let i = 1; i <= s; i++)
{
result.push(closure(this.at_(i)));
}
return result;
}
size() {
let _size = Math.max(0, Math.floor((this.end - this.start) / this.step) + 1);
const x = this.step * _size + this.start;
if ((this.step < 0 && this.end <= x) || (this.step > 0 && x <= this.end))
_size = _size + 1;
return _size;
}
}
Number.prototype.to_ = function(value) { return new Interval(this, value); }
Number.prototype.to_by_ = function(limit, increment) { return new Interval(this, limit, increment); }
// ~~~~~~~~~~~~~~~~~~~~ ReadStream ~~~~~~~~~~~~~~~~~~~~~~~~
let ReadStream = class {
constructor(contents) { this.contents = contents; this.position = 0; }
next() {
if (this.atEnd())
throw "reached the end of the stream";
return this.contents[this.position++];
}
next_(n) {
if (this.position + n > this.contents.length)
throw "reached the end of the stream";
const start = this.position;
this.position = this.position+n;
return this.contents.slice(start, this.position);
}
peek() {
if (this.atEnd())
return nil;
return this.contents[ this.position];
}
int64() {
// integers are stored as big endian
const start = this.position;
this.position = this.position + 8;
return this.contents[start+7] +
(this.contents[start+6] << 8) +
(this.contents[start+5] << 16) +
(this.contents[start+4] << 24) +
(this.contents[start+3] << 32) +
(this.contents[start+2] << 40) +
(this.contents[start+1] << 48) +
(this.contents[start+0] << 56);
}
atEnd() {
return this.position >= this.contents.length;
}
}
Array.prototype.readStream = function() { return new ReadStream(this); }
// ~~~~~~~~~~~~~~~~~~~~ WriteStream ~~~~~~~~~~~~~~~~~~~~~~~~
let WriteStream = class {
constructor() { this.collection = ""; }
nextPut_(character) {this.collection = this.collection + character; return this;}
nextPutAll_(string) {this.collection = this.collection + string; return this;}
space() {return this.nextPutAll_(" ");}
cr() {return this.nextPutAll_("\n");}
crtab() {return this.nextPutAll_("\n\t");}
crtab_(n) {
this.nextPutAll_("\n" + "\t".repeat(n));
}
tab() {return this.nextPutAll_("\t");}
store_(anObject) { this.nextPutAll_(JSON.stringify(anObject)); return this; }
contents() { return this.collection;}
reset() { this.collection = ""; return this;}
print_(object) {return this.nextPutAll_(object.toString());}
}
String.prototype.writeStream = function() { return new WriteStream(); }
String.__proto__.streamContents_ = function(block) { let w = new WriteStream(); block(w); return w.contents(); }
// ~~~~~~~~~~~~~~~~~~~~ EggProcessSuspended ~~~~~~~~~~~~~~~~~~~~~~~~
globalThis.EggProcessSuspended = class {
exception_(exception) {this._exception = exception; return this;}
exception() { return this._exception; }
process_(process) {this._process = process; return this;}
process() { return this._process; }
context_(context) {this._context = context; return this;}
context() { return this._context; }
signal() { throw this; }
}
// ~~~~~~~~~~~~~~ extra for debugging ~~~~~~~~~~~~~~~~~
Object.prototype.ASSERT_ = function (bool) { if (!bool) debugger; }
Object.prototype.halt = function () { debugger; }
Object.prototype.error_ = function(string) { debugger; throw string; }
EggSmallInteger.prototype.toString = function () { return "<" + this._value + ">"; }
EggHeapObject.prototype.toString = function () { return "a " + this.classname(); }
EggSlotObject.prototype.toString = function () {
if (this === nil)
return "nil";
const classname = this.classname();
if (classname == "CompiledMethod") {
let klass = this.slotAt_(4);
if (!klass) klass = nil;
let selector = this.slotAt_(5);
if (!selector) selector = nil;
return klass.toString() + ">>" + selector.toString();
}
if (classname == "Behavior")
{
let klass = this.slotAt_(1);
return klass.toString() + ' behavior';
}
const species = this.species();
if (!species.speciesIsClass()) // species is metaclass, then this is a class
return this.className().asLocalString();
if (classname=="Metaclass") // species is class,then this is a metaclass
return this.speciesClassname();
return "a " + this.classname();;
}
EggByteObject.prototype.toString = function () {
const classname = this.classname();
if (classname == "Symbol")
return "#" + this.asLocalString();
if (classname == "String")
return '"' + this.asLocalString() + '"';
return "a " + this.classname();;
}
EggHeapObject.prototype.species = function () { return this._header._behavior._slots[0]; }
EggHeapObject.prototype.speciesIsClass = function () { return this._slots[5].constructor == EggByteObject; }
EggHeapObject.prototype.speciesInstanceClass = function () {
return this.speciesIsClass() ? this : this._slots[5];
}
EggHeapObject.prototype.className = function () { return this._slots[5]; }
EggHeapObject.prototype.classname = function () { return this.species().speciesClassname() }
EggHeapObject.prototype.speciesClassname = function () {
if (this.speciesIsClass())
return this.className().asLocalString();
else
return this.speciesInstanceClass().className().asLocalString() + " class";
}