abcdefGets

ゲッツ!

HTML5 Conference 2017に登壇します

ひょんなことから

HTML5 Conference

に登壇する機会をいただけることに。

タイトルはDeep dive into TypeScriptということで、TypeScriptの紹介や、RoadMap・Issuesの話とかをさせて頂くつもり

同じ時間帯に任天堂の方のセッションとかあってアレなんですが、TypeScriptに興味のある方はぜひ。

Typescript 2.5 リリース

Typescript 2.5がでたので変更点をメモ

Optional-catch-bindingの導入

catch節のエラーオブジェクトが省略可能になった。
現在Stage3のOptional catch bindingが導入された形に。

サンプル

let input = "...";
try {
    JSON.parse(input);
}
catch {
    // ^ catch節の変数宣言を省略している。
    console.log("Invalid JSON given\n\n" + input)
}

jsにJSDocベースの型アサーションとキャストを導入

javascriptファイルもJSDocコメントで型チェック可能になった。
コメントで@ts-checkをつけるか、
コンパイラオプションでcheckJsオプションをtrueにすれば動作する。

型チェックの詳細は以下のWikiに書いてある。

Type Checking JavaScript Files · Microsoft/TypeScript Wiki · GitHub

重複したパッケージのリダイレクト

簡単に言うとnode_modulesのパッケージの同一性チェックをする。

つまり、moduleResolutionがnodeの場合node_modulesからモジュールをインポートするが、
その際にそのモジュールのpackage.jsonをチェックして、すでにインポートした事のあるモジュールかどうか確認する。
もし同一のモジュールであれば、すでにimport済みのモジュールにリダイレクトして同じモジュールをロードしない。
ちなみにpackage.jsonのversionとnameをチェックして同一性判定をする。

node_modules -
             |
             [A-package] -
             |          |
             |          node_modules -
             |                       |
             |                       [C-package]
             [B-package] -
                         |
                         node_modules -
                                      |
                                      [C-package]

この例だと、A-packageとB-packageが同じC-pacakgeを必要としているが、typescriptコンパイラは一度しかC-packageをインポートしない。
B-packageの必要とするC-pacakgeはA-packageのC-packageを参照する。

–preserveSymlinksコンパイラフラグの導入

–preserveSymlinksオプションが導入される。
このオプションはシンボリックリンクされたモジュールが他のモジュールをimportする場合に、
シンボリックリンク元のパスを起点とした相対パスではなく、
シンボリックリンクが置かれている場所からの相対パスとなる。

A -
  |
  B -
  |  |
  |  C'(Symblink of C)
  C

この例だと、C'モジュールのパス解決は–preserveSymlinksをセットしない場合は、
A/Cから始まることになる。
しかし–preserveSymlinksをセットすると、
A/B/C'からパス解決が行われる。

まとめ

意外と新機能は少なかったですね。

typescript 2.4 の新機能

typescript2.4がでたので新機能を確認。

Dynamic Import Expressionsのサポート

import('...')式がサポートされた。

import式を使うことで多くのバンドラーがコード分割をすることが可能になるので、
module: esnextで出力するのがおすすめだそう。

async function getZipFile(name: string, files: File[]): Promise<File> {
  const zipUtil = await import('./utils/create-zip-file');
  const zipContents = await zipUtil.getContentAsBlob(files);
  return new File(zipContents, name);
}

String Enumsのサポート

待望?の文字列enumがサポートされた。

enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

ただし制限として、数値enumの際に可能だった、メンバプロパティからプロパティ名の取得はできない。

Colors[Colors.Red] // これはできない。

interfaceのgeneric型のサポートを強化

戻り値の推論能力の強化

戻り値から型パラメータを導出できるようになった。

function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[] {
  return a => a.map(f);
}

const lengths: (a: string[]) => number[] = arrayMap(s => s.length);

Promiseもこのようにエラーとすることができるように。

let x: Promise<string> = new Promise(resolve => {
  resolve(10);
  //      ~~ Error!
});

文脈からの型パラメータの導出

いかのような定義があった場合

let f: <T>(x: T) => T = y => y;

yanyになってしまっていた。
そのため、

let f: <T>(x: T) => T = y => y() + y.foo.bar;

のような式は型チェックをスルーしてしまっていたが、
2.4からyが正しく導出され、エラーとなるようになった。

generic関数の方チェックを厳格化

以下のような関数があった場合、

type A = <T, U>(x: T, y: U) => [T, U];
type B = <S>(x: S, y: S) => [S, S];

function f(a: A, b: B) {
  a = b;  // Error
  b = a;  // Ok
}

AがBと互換性があるかをチェックできるようになり、互換性があれば、代入も可能になった。

コールバック関数の方の反変性チェックを厳格化

2.4以前のバージョンでは以下のプログラムではエラーが起きなかった。

interface Mappable<T> {
  map<U>(f: (x: T) => U): Mappable<U>;
}

declare let a: Mappable<number>;
declare let b: Mappable<string | number>;

a = b;
b = a;

なぜなら、abがmap関数のパラメータfを通して変換可能であると判断されていたため。
しかし2.4以降は実際のabの型を比較を行うため、このプログラムはコンパイルエラーとなる。
この変更は破壊的変更となるので注意

Weak Typesの導入

以下のように、すべてがoptionalな型をWeakTypeとして区別することになった。

interface Options {
  data?: string,
  timeout?: number,
  maxRetries?: number,
}

2.4からはこのWeakTypeに対しても、存在しないプロパティを持つ型を代入するとエラーになる。

function sendMessage(options: Options) {
  // ...
}

const opts = {
  payload: "hello world!",
  retryOnFail: true,
}

// Error!
sendMessage(opts);
// optsとOptions型で一致するプロパティがないためエラー

この変更は破壊的変更となるので注意

現在以下のWorkaroundが提案されている。

  • 実際にプロパティが存在する場合のみ宣言する。
  • WeakTypeには{[propName: string]: {}}のようなインデックス型を定義する。
  • opts as Optionsのように型アサーションを使って変換する。

まとめ

型チェックの強化がメインの変更点になった。
破壊的変更が幾つかありますが、WeakTypeのとこはちょっと注意した方が良さそう。
あとはimport式をどう使うか。

babelのAsyncIterationバグ

問題

for-await-ofのボディで配列への分割代入を行うと、

Cannot read property 'file' of undefined

というエラーを投げてトランスパイルに失敗する。
どうやらscopeの解析に失敗しているらしい。

サンプルコード

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const [result] = await g(t);
  }
}

r();

package.json

"dependencies": {
  "babel-cli": "^6.24.1",
  "babel-plugin-transform-async-generator-functions": "^6.24.1",
  "babel-plugin-transform-regenerator": "^6.24.1",
  "babel-plugin-transform-runtime": "^6.23.0",
  "babel-polyfill": "^6.23.0",
  "babel-preset-es2015": "^6.22.0",
  "babel-preset-stage-3": "^6.24.1",
  "babel-runtime": "^6.23.0"
},
"babel": {
  "plugins": [
    [
      "transform-runtime",
      {
        "helpers": false,
        "polyfill": false,
        "regenerator": true,
        "moduleName": "babel-runtime"
      }
    ]
  ],
  "presets": [
    "es2015",
    "stage-3"
  ]
}

解決

とりあえずbugとしてbabel側にはissueを立てておいた。

github.com

現状で取れる選択肢

配列ではなくオブジェクト形式で受け取る

データ構造の変更が必要だがまあ許容できるか?

async function g(t) {
  return new Promise(r => setTimeout(() => r({result: true}), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const {result} = await g(t);
  }
}

r();

分割代入で受け取らない

その後にインデックスアクセスしなければならないのでちょっと面倒

async function g(t) {
  return new Promise(r => setTimeout(() => r([true]), t));
}

async function r() {
  for await (const t of [1000, 2000, 3000]) {
    const result = await g(t);
  }
}

r();

まとめ

はやく直ってくれると嬉しい

Webpackでpolyfillをちゃんと動かす

面倒だったので備忘録

Polyfillの設定

とりあえずwebpackはrequireしないと駄目。
でも既存のコードを一切変えずにpolyfillをインストールしたい。

webpack.ProvidePluginを使う。

new webpack.ProvidePlugin({
  'Promise': 'es6-promise',
  'Symbol': 'es6-symbol',
  'fetch': 'imports?this=>global!exports?global.fetch?window.fetch!whatwg-fetch',
  'Response': 'imports-loader?this=>global!exports-loader?global.Response!whatwg-fetch'
}),,

こんな感じで書いた。

次に

npm i imports-loader exports-loader -Dを実行。

Promise、Symbolはrequire('es6-promise')require('es6-symbol')がそれぞれPromiseSymbolという変数名でモジュール毎に定義される。
fetchはとても読みづらいし考えたやつ頭おかしいが、
まず、whatwg-fetchの定義の下のthisglobalを参照している箇所でmodule.exports = fetchgを定義して、
fetchを参照しているモジュールに(function(fetch){ ... })(require('whatwg-fetch'))のようなコードを追加する。
Responseも同じく。

まとめ

webpackの独自仕様を追加すぎるとESModuleへの移行がつらそうなので程々に。

jspmからWebpackに移行した

とりあえず、Webpackを導入したがそのままだと色々問題が多かったので以下の事をやった。
まあ今更感あるが。

  • typescriptのallowSyntheticDefaultImportsをfalseにする。
  • production用とdev用のconfigをいい感じにわける
  • node_modulesのdllを生成する。

typescriptのallowSyntheticDefaultImportsをfalse

今回はtypescriptとwebpackの組み合わせだったのだが、webapckに移行する前にはjspmを利用していたので、

allowSyntheticDefaultImports: true,
module: system,

で運用していた。ちなみにこのallowSyntheticDefaultImportsが何かというと、
本来typescriptのES6 Moduleはexport default ...の構文しか、直接importできない。

// a.js
module.exports = function() {}
// b.ts
import fn from './a'; // Error
import * as fn from './a'; // OK

しかし、allowSyntheticDefaultImportstrueにすると、commonjsスタイルのmoduleも直接importできるようになる。
のだが、webpackはsystemjsを取り扱えず、module: commonjsにしたところ、このallowSyntheticDefaultImportsがうまく動作しなくなった。

// a.js
module.exports = function() {}
// b.ts
import fn from './a';

fn(); // Error

ので、allowSyntheticDefaultImportsfalseにして、すべてimport * as ...の方式で読み込むことにした。

production用とdev用のconfigをいい感じにわける

まあ、webpack.config.js自体がnodejsのモジュールだったので、webpack.config.jsに設定を書いてもいいのだが、
凄くごちゃごちゃするので、webpack.dev.config.jsを作って、そこでwebpack.config.jsを読み込み、
_.cloneで設定をコピーしてdev用の設定を追加するようにした。

node_modulesのdllを生成する。

コンパイルがあまりに遅かったので、DllPluginを利用して、変更されない外部モジュールを全てまとめておいた。

dll側

output: {
  filename: '[name].dll.js',
  library: 'vendor_library' // Bundleが定義されるグローバル変数名。参照する時に使用する。
},
plugins: [
  new webpack.DllPlugin({
    path: path.join(__dirname, 'dll', '[name]-manifest.json'), // manifestファイルを出力する。とりあえず-manifestにしとけばok
    name: 'vendor_library' // output.libraryで指定した名前
  })
]

使う側

plugins: [
  ...
  new webpack.DllReferencePlugin({ // Dllを参照する
    context: __dirname,
    manifest: require('./dll/vendor.production-manifest.json') // 上のmanifestファイルへのパス
  })
]

こうすることでnode_modulesとかのあまり変化しないファイルをコンパイルしておくことができ、
そのコンパイルしたファイルを参照することでコンパイル時間を短縮できる。
ただし、DllReferencePluginはconcatしてくれないので、dllは手動でconcatするか、別途scriptで読み込む必要がある。
今回はconcatした。

gulp.task('minify', done => {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.config.js'));
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, DIST);
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    try {
      const main = `${DIST}/main.js`;
      const app = fs.readFileSync(main, 'utf8');
      const vendor = fs.readFileSync(`dll/vendor.production.dll.js`, 'utf8');
      fs.writeFileSync(main, `!function(){${vendor};\n${app}}();`, 'utf8');  //ここでconcatしてる。
    } catch(e) {
      throw e;
    }
    done();
  });
});

さらに新たにnode_modulesがロードされても更新されるように、package.jsonにも以下の記述をした。

"scripts": {
  "postinstall": "node ./node_modules/.bin/gulp bundle-dll"
}

これでnpm install/yarn addしてもbundleが更新される。

あと、はまった点として、予めbundleしておく中にreactとかも入れていたので、process.env.NODE_ENVの値に困ってしまった。
なので、dllファイルをdev用、production用と2つ用意して対処した。

function bundleDll(env, done) {
  const webpack = require('webpack');
  const config = _.clone(require('./webpack.dll.config.js'));
  config.entry = _.clone(config.entry);
  config.output = _.clone(config.output);
  config.output.path = path.join(__dirname, 'dll');
  config.plugins = config.plugins.slice();

  const vendor = config.entry.vendor;
  const keyName = `vendor.${env}`;
  config.entry = {};
  config.entry[keyName] = vendor;
  
  if (env === 'production') {
    config.plugins.push(new webpack.optimize.UglifyJsPlugin());
  }
  config.plugins.push(new webpack.DefinePlugin({
    'process.env.NODE_ENV': `'${env}'`
  }));
  const compiler = webpack(config);
  compiler.run((err, stats) => {
    if (err) {
      throw err;
    }
    console.log(stats.toString('minimal'));
    done();
  });
}

/**
 * dev dll
 */
gulp.task('bundle-dev-dll', done => {
  bundleDll('development', done);
});


/**
 * production dll
 */
gulp.task('bundle-prod-dll', done => {
  bundleDll('production', done);
});


gulp.task('bundle-dll', () => {
  const rs = require('run-sequence');
  return rs(
    'clean-dll',
    'bundle-dev-dll',
    'bundle-prod-dll'
  );
});

まとめ

jspmよりwebpackの方が快適でした。
さよならjspm

V8 Iginition Interpreter

以前、東京Node学園25時限目で発表した内容を修正して書いていこうと思う。

というわけで、V8にバイトコードインタープリタ Ignition が搭載された。
このインタープリタは単純そうに見えて非常にわかりづらいので解説していく。

バイトコードインタープリタとは

インタープリタとは、ソースコードを逐次実行する形式のエンジン。
今までのV8はソースコードを即アセンブラコンパイルし実行していたが、
インタープリタはそれとは違い、一度ソースコードを高レベルなバイト命令に変換し、そのバイト命令を逐次実行していく。
高級アセンブラみたいな感じ。

Ignition概要

Ignitionはレジスタベースのバイトコードインタープリタである。Javaのスタックベースとは違って、CPUのレジスタに実際に値を割り付けて実行する。

IgnitionはBytecodeHandlerと呼ばれるバイトコード処理関数を予め生成しておき、バイトコードから配列のインデックスを取得、
そのインデックスに生成された処理関数を割り当て、Bytecodeの配列を次々巡回して、対応するインデックスの関数を呼び出しコードを実行する。

JSで非常に単純化されたコードを書くと以下の様になる。

var Bytecodes = [0,1,2,3,4,5];
var index = 0;
function dispatch(next) {BytecodeHandlers[next]();}
const BytecodeHandlers = {
  ['0']() {...; dispatch(Bytecodes[index++])},
  ['1']() {...; dispatch(Bytecodes[index++])},
  ['2']() {...; dispatch(Bytecodes[index++])},
  ['3']() {...; dispatch(Bytecodes[index++])},
  ['4']() {...; dispatch(Bytecodes[index++])},
  ['5']() {...; dispatch(Bytecodes[index++])},
}

このモデルを念頭にV8のコードベースを確認していく。

Ignitionの構造

バイトコード生成までの道のり

IgnitionはJavascript ASTからバイトコードを生成する。
このバイトコード生成のステップを確認していく。

BytecodeGeneratorAstVisitorを実装しているので、Javascript ASTを巡回しながら対応しているバイトコードを生成していく。 BytecodeGeneratorsrc/interpreter/bytecode-generator.hにあり、バイトコード生成メソッドはBytecodeGenerator::GenerateBytecodeである。

さて、BytecodeGenerator::GenerateBytecodeはどこから呼ばれるかというと、InterpreterCompilationJob::ExecuteJobImpl(src/interpreter/interpreter.cc)内で呼び出される。
InterpreterCompilationJob::ExecuteJobImplstatic Interpreter::NewCompilationJobで実行される。

Interpreter::NewCompilationJobの階層は以下のようになっている。

Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

このstatic Interpreter::NewCompilationJobコンパイラパイプラインのジョブを生成するメソッドなので、compiler.cc(src/compiler.cc)を見ていこう。

compiler.cc(src/compiler.cc)は非常に複雑でわかりづらい呼び出し階層をもっており、さらにオプションの設定パーサーの設定も相まって非常に読みづらい。
static Interpreter::NewCompilationJobを呼び出すまでのコールスタックは以下の様になっている。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
Interpreter::NewCompilationJob
  |
  FullCodeGenerator::NewCompilationJob

ScriptCompiler::CompileがV8のJavascript Compilerのエントリーポイントとなっており、そこから順次関数を呼び出し、最終的にInterpreterのJobを生成する。

最終的なBytecodeGenerator::GenerateBytecodeまでの呼び出しコールスタックは以下のようになる。

ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
| FullCodeGenerator::NewCompilationJob
|
Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

バイトコード生成

さて、呼び出し階層を把握したところで、バイトコードの生成方法を見ていく。
バイトコード生成は先程も書いたとおりAstVisitorを継承しているので、各種Visit***メソッドを実装する必要がある。
ので、各種Visit***の実装を見ていけば何をしているか理解できるはず。

ただ、闇雲にコードを見てもバイトコード自体は理解できないので、一旦trace_bytecodeでd8を実行してみる。

javascript

var a = 1;

bytecodes

0  [generating bytecode for function: ]
1  Parameter count 1
2  Frame size 32
3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          0x3f5e20aafe08 @   18 : 1f f9             Star r1
12          0x3f5e20aafe0a @   20 : 02                LdaZero
13          0x3f5e20aafe0b @   21 : 1f f8             Star r2
14          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
15          0x3f5e20aafe0f @   25 : 1f f7             Star r3
16          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
17          0x3f5e20aafe16 @   32 : 04                LdaUndefined
18   118 S> 0x3f5e20aafe17 @   33 : 96                Return
19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

そうするとこのような結果が得られる。

さてバイトコードを出力したのはいいが、見方がわからないと意味が無いので、見方も解説。

ここは関数のbytecodeの場合に関数名が入る。今回はグローバルなので空。

0  [generating bytecode for function: ]

これはstackのパラメータの数。
今回のバイトコードはグローバルなので無視。

1  Parameter count 1

FrameSizeは割り当てたレジスタの数 * ポインタのサイズ
ポインタのサイズは大体の環境で、32bitでは4byte、64bitでは8byteになる。
今回の場合、割り当てたレジスタ数の数が4 64bit環境なので、ポインタサイズが8byte
4 * 8 = 32となる。

2  Frame size 32

各バイト列は 現在のアドレス アドレスのオフセット バイトコードの数値 バイトコードの名前 オペランド となっている。

3  0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]

ここは定数値プールの中身。
今回は変数名のaがプールされている。

19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21  - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22  - length: 2
23            0: 0x3f5e20aafd71 <FixedArray[4]>
24            1: 0x2315b1a87ef9 <String[1]: a>

さてこれらの情報を踏まえて、先程のソースコードバイトコードを見ていこう。

以下の部分はすっとばしてよい。ここはインタープリタの準備なので。

3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4           0x3f5e20aafdf8 @    2 : 1f f9             Star r1
5           0x3f5e20aafdfa @    4 : 02                LdaZero
6           0x3f5e20aafdfb @    5 : 1f f8             Star r2
7           0x3f5e20aafdfd @    7 : 20 fe f7          Mov <closure>, r3
8           0x3f5e20aafe00 @   10 : 55 aa 01 f9 03    CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9      0 E> 0x3f5e20aafe05 @   15 : 92                StackCheck

本番はここから
解説はコード中に書いていく。

            // 定数プールのインデックス1(変数名a)から値をaccumulatorにロードする。
10   116 S> 0x3f5e20aafe06 @   16 : 09 01             LdaConstant [1]
11          // accumulator(変数名a)からr1レジスタに値をロードする。
12          0x3f5e20aafe08 @   18 : 1f f9             Star r1
13          // accumulatorに0をロードする。
14          0x3f5e20aafe0a @   20 : 02                LdaZero
15          // accumulator(0)からr2レジスタに値をロードする。
16          0x3f5e20aafe0b @   21 : 1f f8             Star r2
17          // accumulatorに即値1をロードする。
18          0x3f5e20aafe0d @   23 : 03 01             LdaSmi [1]
19          // accumulator(1)からr3レジスタに値をロードする。
20          0x3f5e20aafe0f @   25 : 1f f7             Star r3
21          // r1レジスタからr3レジスタの値(a, 0, 1)を使ってInitializeVarGlobalランタイムを呼び出す。
22          0x3f5e20aafe11 @   27 : 55 ab 01 f9 03    CallRuntime [InitializeVarGlobal], r1-r3
23          // accumulatorにundefinedをセット
24          0x3f5e20aafe16 @   32 : 04                LdaUndefined
25          // 終了
26   118 S> 0x3f5e20aafe17 @   33 : 96                Return

これがバイトコードの実行である。
ちなみにCallRuntimeの場合、各Runtime毎に呼び出し規約が決まっているので、それぞれに合わせたレジスタの割り当てが必要になる。
InitializeVarGlobalランタイム呼び出しは以下のレジスタを期待している。

  • r0 = 束縛される変数名
  • r1 = LaunguageMode SLOPPY(通常) STRICT(strictモード) LAUNGUAGE_END(不明)
  • r2 = 束縛される値

そのため、上記のコードは

  • accumulatorに値をロード
  • レジスタに値をロード

を繰り返して、Runtime呼び出しのコードを生成している。

とこの調子でIgnitionはバイトコードを実行していくが、
そのバイトコードを実行しているのはBytecodeHandlerとよばれるクラスである。

バイトコード実行

BytecodeHandler

バイトコードの実行はBytecodeHandlerによって行われる。
このBytecodeHandlerはV8の初期化時に生成され、配置される。

以下がBytecodeHandlerの例である。

IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
  Node* zero_value = NumberConstant(0.0);
  SetAccumulator(zero_value);
  Dispatch();
}

LadZeroの処理を行うBytecoeHandlerで、中では単純にaccumulatorに0をセットするだけ。
このような調子で各バイトコードにつき一つのBytecodeHandlerが実装されている。

BytecodeHandlerは直接次のBytecodeHandlerを呼び出す。

このDispatchが次のBytecodeHandlerを呼び出している。

Dispatch();

しかし、このBytecodeHandlerの実装をみるとわかるのだが、BytecodeHandlerはあくまで、
実行予定Nodeを組み立てているだけで、実際には何かを実行するわけではない。

Ignitionインタープリタは最初にBytecodeの処理手順をグラフノードで生成し、生成したグラフからマシンコードを生成する。
これをBytecodeのdispatch-tableに設定することで、各バイトコード毎に行う処理が設定されたBytecodeHandlerが実装される。

以下の図はBytecodeHandlerの生成

f:id:brn_take:20170501171341p:plain

InterpreterEntryTrampoline

Ignitionは最終的にBytecodeArrayを生成し終わった後に、
InterpreterEntryTrampolineというbuiltinsからIgnitionのDispatchTableを発火するコード生成し、
BytecodeArrayからバイトコードを取り出し、対応するDispatchTableの処理を実行して回っていく。

以下の図はIgnitionが実行される様子

f:id:brn_take:20170501171955p:plain

まとめ

一通りIgnitionの実行パスを眺めた。
また、Ignitionのバイトコードアセンブラコードのキーとして振る舞い、
実際にはベースラインで生成されたコードが実行されている事を確認した。

TurboFan経由の最適化部分等については今後の記事を書く予定。