正月なので #発火村 に参加してきた

国境の長いトンネルを抜けると、そこは雪国だった。
正月発火村 #発火村 - Togetter
群馬の秘境、水上でハッカソンに参加してきた。

ハッカソンではClion.JSというECMA-335*1仮想マシンJavaScript実装を作りたかったのだけど、作業量的に一日二日でできるものでは到底なかったでので「実装を始めた」という程度でした。Clion.JSの開発は続けるつもりです。

さて、ハッカソン前半はずっとmonoを参考にしながらCLI executableのローダを書いていたのだけど、複雑すぎてまるで形になりませんでした*2。そこで急遽VMだけを先に作ることにし、最終的に発表したものが以下のスクリプトです。これにCLI executableを別途disassembleしたものを与えると実行します。
CLI VMは仕様を読むとスタックマシンのようですね。なのでスタックを用意して、disasmしたものからとってきた命令列を実行するだけです。もちろん完全な実装ではなく、文字列の出力と加減算くらいしかできませんが、100行程度のものなのでスタックマシンのVMの簡単なサンプルとしては丁度いいでしょう。

clion-vem.js:

#!/usr/bin/env node
"use strict";
var p = console.log;

var fs = require('fs');

var name  = process.argv[2] || '/dev/stdin';

// loader
var buffer = fs.readFileSync(name).toString().split(/\n+/);
var i;
for(i = 0; i < buffer.length; i++) {
    if(buffer[i].match(/\.entrypoint$/)) {
        break;
    }
}
var ops = [];
for(; i < buffer.length; i++) {
    var matched = buffer[i].match(/IL_(....):\s+(\w+)\s*(.*)$/);
    if(matched) {
        var label = matched[1];
        var name  = matched[2];
        var args  = matched[3];
        ops.push({ label: label, name: name, args: args });
    }
    else if(buffer[i].match(/^\s*\}/)) {
        break;
    }
}

// virtual machine
(function() {
    var stack = [];
    var registory = [];
    var method = {
        'System.Console::WriteLine': function(arg) {
            console.log("%s", arg);
        },
        'string::Concat': function(a, b) {
            return String(a) + String(b);
        },
    };
    var i, op;
    for(i = 0; i < ops.length; i++) {
        op = ops[i];
        switch(op.name) {
        case "box":
            // noop
            break;

        case "call":
            var m      = op.args.match(/(\w+(?:\.\w+)*::\w+)\((.*)\)/);
            var name   = m[1];
            var argc   = m[2].split(/,/).length;
            var args   = stack.splice( stack.length - argc );
            stack.push( method[name].apply(this, args) );
            break;

        case "add":
            var right = stack.pop();
            var left  = stack.pop();
            stack.push( (+left) + (+right) );
            break;
        case "sub":
            var right = stack.pop();
            var left  = stack.pop();
            stack.push( (+left) - (+right) );
            break;

        case "ldc":
            var m = op.args.match(/\w+\s*$/);
            var v = parseInt(m);
            stack.push(v);
            break;

        case "ldstr":
            stack.push( eval(op.args) );
            break;

        case "stloc":
            var idx = op.args.match(/\d+/)[0];
            registory[idx] = stack.pop();
            break;
        case "ldloc":
            var idx = op.args.match(/\d+/)[0];
            stack.push(registory[idx]);
            break;

        case "ret":
            return;
        default: throw Error("Not yet implemented: " + op.name);
        }
    }
})();

以下のようなC#コードが実行できます。

class HelloWorld {
    static void Main() {
       var x = "Hello, ";
       var y = "world!";
        System.Console.WriteLine(x + y);
    }
}

実行するにはmonoとnodeが必要で、逆にこれらがあればプラットフォームに関係なく実行できます。

$ mcs hello.cs # -> hello.exeができる
$ monodis hello.exe | ./clion-vem.js
Hello, world!

monodisするといろいろ出てきますが、見ているのは以下のところだけ。

$ monodis hello.exe
// (snip)
	IL_0000:  ldstr "Hello, "
	IL_0005:  stloc.0 
	IL_0006:  ldstr "world!"
	IL_000b:  stloc.1 
	IL_000c:  ldloc.0 
	IL_000d:  ldloc.1 
	IL_000e:  call string string::Concat(string, string)
	IL_0013:  call void class [mscorlib]System.Console::WriteLine(string)
	IL_0018:  ret 
//  (snip)

インストラクションの意味は以下のとおり。だいたい名前から推測できます。

  • ldstr 文字列定数をスタックにpush
  • stloc.x スタックから値をpopしてローカル変数xに保存
  • ldloc.x ローカル変数xから値を得てスタックにpush
  • call 関数の呼び出し
  • ret 関数からリターン

意味がわかればあとはコードに落としこむだけです。

感想

今回のような泊まりがけのハッカソンは初めて参加しました。家でコードを書くよりもずっと集中できたし、テンションを高いまま保っていられた*3
年に3回くらいはこういう泊まりがけのハッカソンがあってもいいかな、と思う。
ありがとうございました!

*1:事実上、C#の処理系と考えてよい

*2:ヘッダの読み込みはどうやらできて、現在は中身の読み込みをしているところ。

*3:そのかわり、20時頃帰宅するなり眠り込んで朝3時に目が覚めた。