TypeScriptとBabelでasm.jsに挑戦する


# はじめに注意!

 * この記事で扱う内容は現時点(2015-07-19)で実用的でない
 * とりあえず触ってみた、遊んでみたというメモ
 * うごくまでには色々弄る必要があります

# なにをするの？

 1. ES6/ES7をES5/3にトランスパイルする[Babel](https://babeljs.io/)というツールがある
 1. Babelのプラグインとして[babel-plugin-asm-js](https://github.com/RReverser/babel-plugin-asm-js)というものが最近公開された
 2. [Flow](http://flowtype.org/)の型付けがされたES6を[Emscripten](https://github.com/kripken/emscripten)を使わずに[asm.js](http://asmjs.org/)化するもの
 3. Flowの型は[TypeScript](http://www.typescriptlang.org/)の型と互換性がある
 4. …つまりTypeScriptのコードをこのツールにかければasm.jsが簡単に吐き出せるんじゃね？
 5. やってみよう

# インストール

## TypeScriptのインストール:

 * Type Aliasを使うのでver1.4以降
 * **今回コンパイルはしない**
 * 文法や型チェックにつかうだけなので`--noEmit`オプションを有効にしておく

## Babelのインストール:

 * `npm i babel -D`

## babel-plugin-asm-jsのインストール:

 * `npm i babel-plugin-asm-js -D`
 * WARNINGが出るがとりあえず

# 使えるようにする

babel-plugin-asm-js は 現時点(2015-07-19)で npm installしただけでは動かない。なので手を入れる。

## package.json

`node_modules/babel-plugin-asm-js` 以下の `package.json`

`"main": "lib/index.js",`
↓
`"main": "dist/index.js",`

存在しないパスを指定してあるのでプラグインが見つからないという事になる。

## better-logのインストール

`npm i babel-plugin-asm-js`では入らないがエラー出力に必要なので入れる。

`npm i better-log`するか、`node_modules/babel-plugin-asm-js` 以下で`npm i`で入るはず。

# コード(TypeScript)

コードの書き方は色々制限がある。
以下がとりあえず動く最小コード。

```javascript:sample.ts
"use asm";

type int = number;
type double = number;
type float = number;

export function sample(intParam:int, doubleParam:double, floatParam:float):int{
	return 0;
}
```
## `use asm`ディレクティブ必須

```javascript:1行目
"use asm";
```

  * ファイルの最初に書いておかないと **トランスパイルされない**

## type alias指定が必要
  * asm.jsで指定できる３つの型を扱えるようにtype aliasを作っておく

```
type int = number;
type double = number;
type float = number;
```
 * TypeScriptの場合、型定義ファイル(*.d.ts)に宣言を持っていかないほうがよい[^1]

## 使いたい関数を`export`

 * ES6形式の`export`で関数を指定する
 * `export default`はできない
 * `Class`は未対応

## 引数と返り値に型指定

 * 現時点では関数の返り値に`void`指定できない
 * 現時点ではasm.jsの型への変換は **引数と返り値のみ**

# トランスパイル後(asm.js化)

では実際に変換してみよう。
CLIから`.ts`ファイルを直接指定できる。
ファイルとして書き出す時は`-o`オプションで指定する。

```
babel --plugins asm-js sample.ts
```

トランスパイルすると以下のコードになる。なお、このコードはそのままでは動かない（後述）。

```javascript:babel-asm-js後
"use strict";

Object.defineProperty(exports, "__esModule", {
	value: true
});
exports["default"] = initialize;

var selfGlobal = _selfGlobal(),
    foreign = {},
    strings = [];

var _sample;

exports._sample = _sample;

function asm(stdlib, foreign, heap) {
	"use asm";
	function sample(intParam, doubleParam, floatParam) {
		intParam = intParam | 0;
		doubleParam = +doubleParam;
		floatParam = Math.fround(floatParam);

		return 0 | 0;
	}
	return {
		sample: sample
	};
}

function initialize(heap) {
	var _asm;

	return (_asm = asm(selfGlobal, foreign, heap), _sample = _asm.sample, _asm);
}
```

## 核となるコード

色々出力されるが、核となるのは以下の部分。asm.jsの書き方に即した形で出力される。

```javascript
function asm(stdlib, foreign, heap) {
    "use asm";
    function sample(intParam, doubleParam, floatParam) {
        intParam = intParam | 0;
        doubleParam = +doubleParam;
        floatParam = Math.fround(floatParam);

        return 0 | 0;
    }
    return {
        sample: sample
    };
}
```

 * asm.jsの型表現に変換されている（引数、返り値）
  * `int`は`変数|0`
  * `double`は`+変数`
  * `float`は`Math.fround(変数)`
 * 関数内の 変数宣言 → 処理 → return文 という順番に入れ替えられる（asm.jsの仕様）

## 現時点では実用できない理由

`変数:int`とすると`変数|0`と置き換えてくれるなど、一瞬いいね！と思うのだが、残念ながらまだ実用できない。

### 普通の変数の型は変換してくれない

```javascript:BEFORE(ローカル変数を宣言してみる)
export function sample(intParam:int, doubleParam:double, floatParam:float):int{
	let a:int = 100;	//関数内でローカル変数を宣言してみると…？
	return 0;
}
```

```javascript:AFTER(型が失われる)
	function sample(intParam, doubleParam, floatParam) {
		intParam = intParam | 0;
		doubleParam = +doubleParam;
		floatParam = Math.fround(floatParam);
		var _a = 0;
		_a = 100;
		//関数内でローカル変数を宣言してみると…？

		return 0 | 0;
	}

```

 * `let`も使えるし、ちゃんと変数宣言を最初に持ってきてくれるのはいい
 * が！ **大事な型情報が消えてしまう**。
  * `var _a = _a|0;`としてほしい
  * まあなくても一応エラーにはならないみたいだが

### 結局、asm.jsを手書きする必要がある

速度的に早くなっているかどうかを確認するため以下の関数を書いてみた。

```javascript
export function doForLoop(from:int, to:int, add:int):int{
	console.time("asm_forloop");
	for(from=0;from < to; from += add){
	}
	console.timeEnd("asm_forloop");
	return 0;
}
```

これはトランスパイル時に次のエラーになる。

```
TypeError: sample.ts: Line 26: Unsupported operation: int < int
```

asm.jsは厳格な型の世界なので、比較演算子`<`も左右の辺の型が対応していないといけない。
比較演算子が対応するのは[仕様](http://asmjs.org/spec/latest/#binary-operators)では以下になる。

```
(signed, signed) → int ∧
(unsigned, unsigned) → int ∧
(double, double) → int ∧
(float, float) → int
```

[asm.jsの`int`型は signed/unsignedの区別のない32bit型integer](http://asmjs.org/spec/latest/#int)である。比較するためには型のキャストが必要である。`(int|0)`とか`int >>> 0`とかで変換できるのだが、冗長でめんどくさい。

最初から`signed`や`unsigned`で型指定すればいい、と思いたいのだがasm.jsにはそれを表現するイディオムがない。type aliasで`uint`とか定義しても現時点では無意味である。JavaScriptの文法上、演算子オーバーロードもないので結局以下のように書かないといけない。

```javascript:修正後
for(from=0;(from|0) < (to|0); from += add){
```

変数宣言時の`変数|0`は区別なしの`(int)`として扱われるが, 比較時の両辺での`変数|0`は`(signed)`として扱われる。 **同じ表記だが型が違う** ので注意。

さらに、この修正後のコードもFirefox上のランタイムエラーになる。`+=`がアウトらしく、さらに次のように書き直す必要がある。

```javascript:firefoxのエラー修正後
for(from=0;(from|0) < (to|0); from = from + add){
```

babel-plugin-asm-jsは内部でasm-jsのバリデータを使っているのだが、上記のエラーはバリデータでは判断できず、ランタイムで確認するしか無い。asm.jsでは現時点ではfirefoxとchromeで有効だが、chromeでは出ないエラーなので厄介。

なお、 **Firefoxはデバッガを起動するとasm.jsが無効になるという仕様** があり、エラーチェックのためにデバッガを起動したままに出来ない（コンソールだけならOK)。

TypeScript(おそらくFlowも)の型チェックやBabel-asm-jsのトランスパイル時の型チェックだけでは現状、asm.jsの型の整合性を取る事は厳しい。asm.jsの仕様を理解した上で **asm.jsを結局手書きする事になる**。

### 吐き出されるコードがそのままでは動かない(2015-07-19)

出力されたコードは現時点(2015-07-19)で動かないものである。

ざっくりと以下のように手を入れれば一応動くようにはなる。

 * `_selfGlobal`を定義する
  * `"use strict";`直後に以下のように定義する

```
var _selfGlobal = require("babel-runtime/helpers/self-global")["default"];
``` 

 * babel-runtime も`npm i`で導入しておく
 * `_selfGlobal`は関数でないというエラーになるので以下の部分を書き換える
  * BEFORE: `var selfGlobal = _selfGlobal(),`
  * AFTER: `var selfGlobal = _selfGlobal,` 
 * initializeするために一番最後の行に以下を追加。
  * `exports["default"](0);`
  * 引数はヒープサイズなので適宜変更しておく
 * exports._FUNC = _FUNC; の部分をさらに一番最後の行に移動
  * 外部から使いたい関数を登録している部分
 * さらに`_`を削る
  * BEFORE: `exports._FUNC = _FUNC;`  
  * AFTER: `exports.FUNC = _FUNC;`

以上をやることで、node上で使えるコードになる。

```javascript:別ファイルから
var asm = require(`./sample.js`);
asm.doForLoop(0,100000,1); //0: 3ms
```

なお、ブラウザで使うためにはもう一手間を入れる必要がある。

# まとめ

 * `引数:int`が`変数|0`に変換されるのは便利
  * ただし、関数の引数と返り値だけ
 * ES6に一部対応しているのも便利
 * 結局、asm.jsを手書きする必要がある
 * 吐かれるコードがそのままでは動かないので手を入れる必要がある
 * まだまだあまり実用的じゃないので今後に期待
  * asm.jsを手書きしてた人には便利？[^2]

TypeScriptは処理速度に貢献しないので、TypeScriptでasm.jsを使えると、Cのインラインアセンブラみたいに部分的に早くできる！と思ってトライしてみたのだが、実用にはまだ難あり。あと、あまりTypeScript関係なかった。

[^1]: TypeScript自体のチェックは通るが、babel-asm-jsで変換されるものがかわってしまう
[^2]: そんな人どれだけ居るのだろうか…？このツールを使っても、C/C++で書いた方が生産的だと思う