AjTalk en Javascript (1) Primeras Implementaciones desde 0

Published on Author lopezLeave a comment

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

Leave a Reply

Your email address will not be published. Required fields are marked *