Hace unos días, migré mi proyecto AjTalk de Google Code a mi cuenta en GitHub:
https://github.com/ajlopez/AjTalk
Vengo trabajando en él, en mis tiempos libros, debe ser desde el 2008. Está escrito en C# e implementa una máquina virtual Smalltalk, basada en bytecodes. Codificado en C# plano, debería probarlo en otras plataformas, pero seguro que Mono lo soporta. En el 2011 le agregué la capacidad de compilar código Smaltalk de fileouts a Javascript, que se pueden usar tanto en el browser como en Node.js. El domingo pasado me decidí a crear otro proyecto:
https://github.com/ajlopez/AjTalkJs
como code kata del día. Tiene otro “approach”: implementar una máquina virtual AjTalk, desde 0, directamente en Javascript. La idea es compilar código Smalltalk a métodos y bloques con bytecodes, y tener el intérprete de esos método. Tanto la base del Lexer, Compiler, Block, Execution Block están funcionando. El código de la implementación principal está en:
https://github.com/ajlopez/AjTalkJs/blob/master/lib/ajtalk.js
En esa implementación, estoy usando un método sendMessage que usa un “custom lookup” para ubicar el método que atiende un mensaje, de acuerdo a su “selector”, su nombre. Un fragmento de lib/ajtalk.js:
BaseObject.prototype.sendMessage = function(selector, args)
{
var method = this.lookup(selector);
return method.apply(this, args);
};
function BaseClass(name, instvarnames, clsvarnames, supercls) {
this.name = name;
this.instvarnames = instvarnames;
this.clsvarnames = clsvarnames;
this.supercls = supercls;
this.methods = {};
};
BaseClass.prototype.__proto__ = BaseObject.prototype;
BaseClass.prototype.defineMethod = function (selector, method)
{
this.methods[selector] = method;
};
BaseClass.prototype.getInstanceSize = function() {
var result = this.instvarnames.length;
if (this.supercls)
result += this.supercls.getInstanceSize();
return result;
};
BaseClass.prototype.lookupInstanceMethod = function (selector)
{
var result = this.methods[selector];
if (result == null && this.supercls)
return this.supercls.lookupInstanceMethod(selector);
return result;
};
El lunes, se me ocurrió otra forma de hacerlo, que pensé que iba a tener dificultades: en vez de relegar la herencia a un “custom lookup”, podría aprovechar la cadena de prototipos para heredar métodos, e instancias por separado, para tener las variables de cada instancia. Escribí código en lib/ajtalknew.js:
function createClass(name, superklass, instvarnames, clsvarnames)
{
var protoklass = new Function();
if (superklass)
{
// Chain class prototypes
protoklass.prototype.__proto__ = superklass.proto;
}
else
{
// First class methods
protoklass.prototype.basicNew = function()
{
var obj = new this.func;
obj.klass = this;
return obj;
}
protoklass.prototype.defineSubclass = function(name, instvarnames, clsvarnames)
{
return createClass(name, this, instvarnames, clsvarnames);
}
protoklass.prototype.defineMethod = function(name, method)
{
var mthname = name.replace(/:/g, '_');
if (typeof method == "function")
this.func.prototype[mthname] = method;
else
this.func.prototype[mthname] = method.toFunction();
}
protoklass.prototype.defineClassMethod = function(name, method)
{
var mthname = name.replace(/:/g, '_');
if (typeof method == "function")
this.proto[mthname] = method;
else
this.proto[mthname] = method.toFunction();
}
// TODO Quick hack. It should inherits from Object prototype
protoklass.prototype.sendMessage = function(selector, args)
{
return this[selector].apply(this, args);
}
}
var klass = new protoklass;
// Function with prototype of this klass instances
klass.func = new Function();
klass.proto = protoklass.prototype;
klass.name = name;
klass.super = superklass;
klass.instvarnames = instvarnames;
klass.clsvarnames = clsvarnames;
klass.func.prototype.klass = klass;
if (superklass)
{
// Chaining instances prototypes
klass.func.prototype.__proto__ = superklass.func.prototype;
}
else
{
// First instance methods
klass.func.prototype.sendMessage = function(selector, args)
{
return this[selector].apply(this, args);
}
}
Smalltalk[name] = klass;
return klass;
}
createClass('Object');
Smalltalk.Object.defineClassMethod('compileMethod:', function(text)
{
var compiler = new Compiler();
var method = compiler.compileMethod(text, this);
this.defineMethod(method.name, method);
return method;
});
Smalltalk.Object.defineClassMethod('compileClassMethod:', function(text)
{
var compiler = new Compiler();
var method = compiler.compileMethod(text, this);
this.defineClassMethod(method.name, method);
return method;
});
Ok, es un poco “tricky” pero funciona!. Y ambos códigos fueron desarrollados con TDD, así que los refactors que hice fueron bien soportados y bienvenidos.
Esta vez, no usé QUnit para TDD en el browser. Usé línea de comando, con Node.js, y su “built-in” módulo assert (no tenía conexión a Internet cuando comencé a escribir, así que no me traje en NodeUnit). Es una experiencia interesante, porque fue la forma más simple de escribir tests: un largo programa con assert tras otro. Puedo en cualquier momento refactorizarlo, pero no ví que me complicara mucho en el desarrollo: avancé tan rápido como cuando escribo tests más organizados.
Tengo una función/”clase” javascript llamada Compilar que puede compilar código Smalltalk (un subconjunto de la gramática, por ahora) en bytecodes. Soporta mensajes unarios, binarios y de “keywords”. Sólo unos pocos bytecodes fueron necesarios para implementar la funcionalidad actual:
var ByteCodes = {
GetValue: 0,
GetArgument: 1,
GetLocal: 2,
GetInstanceVariable: 3,
GetGlobalVariable: 4,
GetSelf: 5,
SetLocal: 10,
SetInstanceVariable: 11,
SetGlobalVariable: 12,
Add: 20,
Subtract: 21,
Multiply: 22,
Divide: 23,
SendMessage: 40,
Return: 50
};
Hay ejemplos index.html, indexnew.html para probar interactivamente ambas implementaciones.
Trabajo pendiente: implementar variables de clases, metaclases, como siempre usando TDD. Mejorar los ejemplos de browser, y escribir posts sobre todo eso.
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez