@ambers/lang
Version:
An implementation of the Smalltalk language that runs on top of the JS runtime.
427 lines (351 loc) • 17.1 kB
JavaScript
//jshint eqnull:true
define(['./junk-drawer'], function ($goodies) {
"use strict";
var inherits = $goodies.inherits;
var declareJsMethod = $goodies.declareJsMethod;
var addElement = $goodies.addElement;
var removeElement = $goodies.removeElement;
var extend = $goodies.extend;
var deleteKeysFrom = $goodies.deleteKeysFrom;
MethodCompositionBrik.deps = ["methods"];
function MethodCompositionBrik (brikz, st) {
var setLocalMethods = brikz.methods.setLocalMethods;
var updateMethod = brikz.methods.updateMethod;
function aliased (selector, method) {
var result = st.method(method, method.instantiateFn);
if (method.selector !== selector) {
result.selector = selector;
result.source = '"Aliased as ' + selector + '"\n' + method.source;
}
result.owner = method.owner;
return result;
}
function fillTraitTransformation (traitTransformation, obj) {
// assert(Object.getOwnProperties(obj).length === 0)
var traitMethods = traitTransformation.trait.methods;
Object.keys(traitMethods).forEach(function (selector) {
obj[selector] = aliased(selector, traitMethods[selector]);
});
var traitAliases = traitTransformation.aliases;
if (traitAliases) {
Object.keys(traitAliases).forEach(function (aliasSelector) {
var aliasedMethod = traitMethods[traitAliases[aliasSelector]];
if (aliasedMethod) obj[aliasSelector] = aliased(aliasSelector, aliasedMethod);
// else delete obj[aliasSelector]; // semantically correct; optimized away
});
}
var traitExclusions = traitTransformation.exclusions;
if (traitExclusions) {
deleteKeysFrom(traitExclusions, obj);
}
return obj;
}
function buildCompositionChain (traitComposition) {
return traitComposition.reduce(function (soFar, each) {
return fillTraitTransformation(each, Object.create(soFar));
}, null);
}
st.setTraitComposition = function (traitComposition, traitOrBehavior) {
var oldLocalMethods = traitOrBehavior.localMethods,
newLocalMethodsTemplate = Object.create(buildCompositionChain(traitComposition));
setLocalMethods(traitOrBehavior, extend(newLocalMethodsTemplate, oldLocalMethods));
(traitOrBehavior.traitComposition || []).forEach(function (each) {
removeElement(each.trait.traitUsers, traitOrBehavior);
});
traitOrBehavior.traitComposition = traitComposition && traitComposition.length ? traitComposition : null;
(traitOrBehavior.traitComposition || []).forEach(function (each) {
addElement(each.trait.traitUsers, traitOrBehavior);
});
};
function aliasesOfSelector (selector, traitAliases) {
if (!traitAliases) return [selector];
var result = Object.keys(traitAliases).filter(function (aliasSelector) {
return traitAliases[aliasSelector] === selector;
});
if (!traitAliases[selector]) result.push(selector);
return result;
}
function applyTraitMethodAddition (selector, method, traitTransformation, obj) {
var changes = aliasesOfSelector(selector, traitTransformation.aliases);
changes.forEach(function (aliasSelector) {
obj[aliasSelector] = aliased(aliasSelector, method);
});
var traitExclusions = traitTransformation.exclusions;
if (traitExclusions) {
deleteKeysFrom(traitExclusions, obj);
}
return changes;
}
function applyTraitMethodDeletion (selector, traitTransformation, obj) {
var changes = aliasesOfSelector(selector, traitTransformation.aliases);
deleteKeysFrom(changes, obj);
return changes;
}
function traitMethodChanged (selector, method, trait, traitOrBehavior) {
var traitComposition = traitOrBehavior.traitComposition,
chain = traitOrBehavior.localMethods,
changes = [];
for (var i = traitComposition.length - 1; i >= 0; --i) {
chain = Object.getPrototypeOf(chain);
var traitTransformation = traitComposition[i];
if (traitTransformation.trait !== trait) continue;
changes.push.apply(changes, method ?
applyTraitMethodAddition(selector, method, traitTransformation, chain) :
applyTraitMethodDeletion(selector, traitTransformation, chain));
}
// assert(chain === null);
changes.forEach(function (each) {
updateMethod(each, traitOrBehavior);
});
}
this.traitMethodChanged = traitMethodChanged;
}
function LanguageFactory (specialConstructors, emit) {
function declareEvent (name) {
declareJsMethod(emit, name);
}
TraitsBrik.deps = ["behaviorals", "methods", "composition", "root"];
function TraitsBrik (brikz, st) {
var SmalltalkObject = brikz.root.Object;
var setupMethods = brikz.methods.setupMethods;
var traitMethodChanged = brikz.composition.traitMethodChanged;
var buildTraitOrClass = brikz.behaviorals.buildTraitOrClass;
function SmalltalkTrait () {
}
specialConstructors.Trait = inherits(SmalltalkTrait, SmalltalkObject);
SmalltalkTrait.prototype.trait = true;
declareJsMethod(SmalltalkTrait.prototype, "toString");
declareJsMethod(SmalltalkTrait.prototype, "added");
declareJsMethod(SmalltalkTrait.prototype, "removed");
declareJsMethod(SmalltalkTrait.prototype, "methodAdded");
declareJsMethod(SmalltalkTrait.prototype, "methodRemoved");
SmalltalkTrait.prototype.toString = function () {
return 'Smalltalk Trait ' + this.name;
};
SmalltalkTrait.prototype.methodAdded = function (method) {
propagateMethodChange(this, method.selector, method);
};
SmalltalkTrait.prototype.methodRemoved = function (method) {
propagateMethodChange(this, method.selector, null);
};
function propagateMethodChange (trait, selector, method) {
trait.traitUsers.forEach(function (each) {
traitMethodChanged(selector, method, trait, each);
});
}
function traitBuilder (traitName, category) {
return {
name: traitName,
make: function () {
var that = new SmalltalkTrait();
that.name = traitName;
that.category = category;
that.traitUsers = [];
setupMethods(that);
return that;
},
updateExisting: function (trait) {
}
};
}
st.addTrait = function (className, category) {
return buildTraitOrClass(traitBuilder(className, category));
};
}
ClassModelBrik.deps = ["root", "nil"];
function ClassModelBrik (brikz, st) {
var SmalltalkRoot = brikz.root.Root;
var SmalltalkObject = brikz.root.Object;
var nilAsReceiver = brikz.nil.nilAsReceiver;
function SmalltalkBehavior () {
}
function SmalltalkClass () {
}
function SmalltalkMetaclass () {
}
this.newMetaclass = function () {
return new SmalltalkMetaclass();
};
specialConstructors.Behavior = inherits(SmalltalkBehavior, SmalltalkObject);
specialConstructors.Class = inherits(SmalltalkClass, SmalltalkBehavior);
specialConstructors.Metaclass = inherits(SmalltalkMetaclass, SmalltalkBehavior);
SmalltalkMetaclass.prototype.meta = true;
declareJsMethod(SmalltalkClass.prototype, "toString");
declareJsMethod(SmalltalkMetaclass.prototype, "toString");
declareJsMethod(SmalltalkClass.prototype, "added");
declareJsMethod(SmalltalkClass.prototype, "removed");
declareJsMethod(SmalltalkBehavior.prototype, "methodAdded");
declareJsMethod(SmalltalkBehavior.prototype, "methodRemoved");
SmalltalkClass.prototype.toString = function () {
return 'Smalltalk ' + this.name;
};
SmalltalkMetaclass.prototype.toString = function () {
return 'Smalltalk Metaclass ' + this.instanceClass.name;
};
declareEvent("classCreated");
SmalltalkClass.prototype.added = function () {
registerToSuperclass(this);
emit.classCreated(this);
};
SmalltalkClass.prototype.removed = function () {
unregisterFromSuperclass(this);
};
declareEvent("behaviorMethodAdded");
SmalltalkBehavior.prototype.methodAdded = function (method) {
emit.behaviorMethodAdded(method, this);
};
declareEvent("behaviorMethodRemoved");
SmalltalkBehavior.prototype.methodRemoved = function (method) {
emit.behaviorMethodRemoved(method, this);
};
// Fake root class of the system.
// Effective superclass of all classes created with `nil subclass: ...`.
var nilAsClass = this.nilAsClass = {
fn: SmalltalkRoot,
subclasses: [],
a$cls: {fn: SmalltalkClass, methods: Object.create(null)}
};
this.bootstrapHierarchy = function (realClass) {
nilAsClass.a$cls = realClass;
nilAsClass.subclasses.forEach(function (each) {
each.a$cls.superclass = realClass;
Object.setPrototypeOf(each.a$cls.methods, realClass.methods);
registerToSuperclass(each.a$cls);
});
};
function registerToSuperclass (klass) {
addElement((klass.superclass || nilAsClass).subclasses, klass);
}
function unregisterFromSuperclass (klass) {
removeElement((klass.superclass || nilAsClass).subclasses, klass);
}
function metaSubclasses (metaclass) {
return metaclass.instanceClass.subclasses
.filter(function (each) {
return !each.meta;
})
.map(function (each) {
return each.a$cls;
});
}
st.metaSubclasses = metaSubclasses;
st.traverseClassTree = function (klass, fn) {
var queue = [klass], sentinel = {};
for (var i = 0; i < queue.length; ++i) {
var item = queue[i];
if (fn(item, sentinel) === sentinel) continue;
var subclasses = item.meta ? metaSubclasses(item) : item.subclasses;
queue.push.apply(queue, subclasses);
}
};
/**
* This function is used all over the compiled amber code.
* It takes any value (JavaScript or Smalltalk)
* and returns a proper Amber Smalltalk receiver.
*
* null or undefined -> nilAsReceiver,
* object having Smalltalk signature -> unchanged,
* otherwise wrapped foreign (JS) object
*/
this.asReceiver = function (o) {
if (o == null) return nilAsReceiver;
else if (o.a$cls != null) return o;
else return st.wrapJavaScript(o);
};
}
ClassConstructionBrik.deps = ["classModel", "behaviorals", "methods"];
function ClassConstructionBrik (brikz, st) {
var nilAsClass = brikz.classModel.nilAsClass;
var newMetaclass = brikz.classModel.newMetaclass;
var buildTraitOrClass = brikz.behaviorals.buildTraitOrClass;
var setupMethods = brikz.methods.setupMethods;
var removeTraitOrClass = brikz.behaviorals.removeTraitOrClass;
declareEvent("slotsChanged");
function setSlots (klass, slots) {
slots.forEach(function (name) {
if (!name.match(/^[a-zA-Z][a-zA-Z0-9]*$/))
throw new Error("Wrong identifier name: " + name);
});
klass.slots = slots;
emit.slotsChanged(klass);
}
st.setSlots = setSlots;
/* Smalltalk class creation. A class is an instance of an automatically
created metaclass object. Newly created classes (not their metaclass)
should be added to the system, see smalltalk.addClass().
Superclass linking is *not* handled here, see api.initialize() */
function classBuilder (className, superclass, category, fn) {
var logicalSuperclass = superclass;
if (superclass == null || superclass.a$nil) {
superclass = nilAsClass;
logicalSuperclass = null;
}
function klass () {
var that = metaclass().instanceClass;
that.superclass = logicalSuperclass;
that.fn = fn || inherits(function () {
}, superclass.fn);
that.slots = [];
that.name = className;
that.category = category;
that.subclasses = [];
setupMethods(that);
return that;
}
function metaclass () {
var that = newMetaclass();
that.superclass = superclass.a$cls;
that.fn = inherits(function () {
}, that.superclass.fn);
that.slots = [];
that.instanceClass = new that.fn();
wireKlass(that);
setupMethods(that);
return that;
}
return {
name: className,
make: klass,
updateExisting: function (klass) {
if (logicalSuperclass == null && klass.superclass != null || logicalSuperclass != null && klass.superclass !== logicalSuperclass || fn != null && fn !== klass.fn)
throw new Error("Incompatible change of class: " + klass.name);
}
};
}
function wireKlass (klass) {
Object.defineProperty(klass.fn.prototype, "a$cls", {
value: klass,
enumerable: false, configurable: true, writable: true
});
}
this.wireKlass = wireKlass;
/* Add a class to the system, creating a new one if needed.
A Package is lazily created if one with given name does not exist. */
st.addClass = function (className, superclass, category) {
// TODO remove, backward compatibility (note: only deprecated as of this note)
if (arguments[3]) {
var added = st.addClass(className, superclass, arguments[3]);
setSlots(added, category);
return added;
}
// While subclassing nil is allowed, it might be an error, so
// warn about it.
if (typeof superclass === 'undefined' || superclass && superclass.a$nil) {
console.warn('Compiling ' + className + ' as a subclass of `nil`. A dependency might be missing.');
}
return buildTraitOrClass(classBuilder(className, superclass, category, specialConstructors[className]));
};
st.removeClass = removeTraitOrClass;
}
/* Making smalltalk that can load */
function configure (brikz) {
brikz.traits = TraitsBrik;
brikz.composition = MethodCompositionBrik;
brikz.classModel = ClassModelBrik;
brikz.classConstruction = ClassConstructionBrik;
brikz();
}
return {configure: configure};
}
return LanguageFactory;
});