Slide 1

Slide 1 text

TypeScript で TypeScript で WebAssembly 処理系を WebAssembly 処理系を 書いた話 書いた話 亀山龍平 (合同会社コベリン) 1

Slide 2

Slide 2 text

自己紹介 自己紹介 合同会社コベリン代表 普段はモバイル、Web、AR をやっています Swift+ARKit, TypeScript+React, HoloLens 等 趣味で WebAssembly をやっています github.com/ryohey twitter.com/ryoheyc 2

Slide 3

Slide 3 text

話す内容 話す内容 なにを作ったか なぜ作ったか デモ どう作ったか 3

Slide 4

Slide 4 text

何を作ったか 何を作ったか TypeScript の WebAssembly 処理系 1から実装 ライブラリ使ってません ts‑wasm‑runtime 4

Slide 5

Slide 5 text

デモ デモ 5

Slide 6

Slide 6 text

できること できること テキスト形式のパース バイナリ形式のパース ブラウザ上での実行環境 6

Slide 7

Slide 7 text

せっかく JavaScript を通さず実行できる WASM を JavaScript で実行する つまり意味なし! 7

Slide 8

Slide 8 text

なぜ作ったか なぜ作ったか WebVR, WebAR に注目している WASM すごいらしい 言語を超えたモジュールが作れる? WASM に VR, ARでの未来を感じた WASMとは何なのか深く知りたい 8

Slide 9

Slide 9 text

そうだ処理系を書こう そうだ処理系を書こう 9

Slide 10

Slide 10 text

なぜ TypeScript か なぜ TypeScript か 型が高機能で楽しい 型が高機能で楽しい あと書きなれてる 10

Slide 11

Slide 11 text

タイムライン タイムライン 2018年後半 WebAssembly 知りたくなる 2018年11月 WebAssembly 処理系の実装に着手 2018年12月 S式パーサ、テキスト形式パーサを実装 2019年1月 インタプリタを実装 フィボナッチ数を計算するモジュールが動く 2019年2月 バイナリ形式パーサを実装 ブラウザ環境を実装 2019年3月 足りてない命令の実装など (現在も進行中) 11

Slide 12

Slide 12 text

何を書いたか 何を書いたか パーサコンビネータ fn‑parser S 式パーサ s‑parser 命令セットの型定義 wasm‑ast wat パーサ wat‑parser wasm パーサ wasm‑parser インタプリタ wasm‑vm ブラウザ環境 playground それぞれ npm のモジュールとして実装 (publish してないので npm install できないかも) 12

Slide 13

Slide 13 text

まずテキスト形式のパース まずテキスト形式のパース WebAssembly の人が読める形式 S 式になっている 13

Slide 14

Slide 14 text

S 式パーサ S 式パーサ これを こうしたい "(foo bar (1 22 (3)))" ["foo", "bar", ["1", "22", ["3"]]] 14

Slide 15

Slide 15 text

パーサコンビネータ パーサコンビネータ パーサの関数を生成する関数 部分をパースする関数を合成してでっかいパーサを作るこ とができる 関数型プログラミング的に書けるので気持ちがいい 15

Slide 16

Slide 16 text

fnparse.js fnparse.js パーサってどう書けばいいんだろうと思って見つけた anatoo さんの解説記事 ここからすべてが始まった anatoo/fnparse.js を TypeScript に移植した fn‑parser を実装 テキスト形式もバイナリ形式も fn‑parser で実装 http://blog.anatoo.jp/entry/2015/04/26/220026 16

Slide 17

Slide 17 text

簡単なパーサ 簡単なパーサ より引用 function parseHoge(target, position) { if (target.substr(position, 4) === 'hoge') { // 成功 return [true, 'hoge', position + 4]; } else { // 失敗 return [false, null, position]; } } parseHoge('hoge', 0) // => [true, 'hoge', 4] を返す parseHoge('ahoge', 1) // => [true, 'hoge', 5] を返す parseHoge('aaa', 0) // => [false, null, 0] を返す http://blog.anatoo.jp/entry/2015/04/26/220026 17

Slide 18

Slide 18 text

簡単なパーサコンビネータ 簡単なパーサコンビネータ // 渡された複数のパーサのどれかが成功したら成功するパーサを生成する関数 const or = (...parsers) => (target, position) => { for (let parser of parsers) { const parsed = parser(target, position) // どれか一つにマッチしたら成功 if (parsed[0]) { return parsed } } return [false, null, position] } // どちらかの文字列にマッチするパーサ const animal = or( token("Cat"), token("Dog") ) animal("Cat", 0) // => [true, "Cat", 3] animal("Dog", 0) // => [true, "Dog", 3] animal("iPhone", 0) // => [false, null, 0] 18

Slide 19

Slide 19 text

例: 数式のパース 例: 数式のパース // regexp は正規表現でマッチするパーサ // map は第一引数でマッチした結果を第二引数の関数で加工するパーサを生成する関数 const num = map(regexp(/^([0-9]+)/), parseInt) const operator = or(token("+"), token("-"), token("*"), token("/")) // lazy はパース時にパーサを生成する関数 // seq は特定の並びにマッチするパーサコンビネータ const mathExpression = lazy(() => or( num, seq(mathExpression, operator, mathExpression) )) mathExpression(“42”) == 42 mathExpression("15/3*9-3", 0) == [15, "/" , [3, "*", [9, "-", 3]]] 19

Slide 20

Slide 20 text

S 式パーサ S 式パーサ const string = regexp(/^([^)^(^ ^\n]+)/) const comment = regexp(/^;;.*\n/) const separator = many(or(regexp(/^\s+/), comment)) const list = lazy(() => map( seq(opt(separator), map(seq( token("("), opt(separator), expression, opt(many(map(seq(separator, expression), r => r[1]))), opt(separator), token(")") ), r => (r[3] !== null ? [r[2], ...r[3]] : [r[2]]) )), r => r[1] )) 20

Slide 21

Slide 21 text

型安全なパーサコンビネータ 型安全なパーサコンビネータ 21

Slide 22

Slide 22 text

パーサの定義 パーサの定義 export type Parser = (target: T, position: number) => [boolean, S, number] 22

Slide 23

Slide 23 text

型情報を維持したパーサの合 型情報を維持したパーサの合 成 成 Parser ではないところに注目 複雑なパーサを書くとき型を間違えない const fooParser = seq(token("foo"), num, token("bar")) // Parser 23

Slide 24

Slide 24 text

TypeScript のジェネリクスパラメータは可変長にできないの で地道に定義 24

Slide 25

Slide 25 text

書いてみて 書いてみて パーサコンビネータすごい fnparse.js ありがとう テストしやすい デバッグが難しい 手続き的だとデバッガで止めてどこがおかしいかわかる が、関数合成されているのでいまどこにいるのか分かり づらい 最初は S 式パーサで数値もパースしていたが、テキスト形 式の Float の仕様が複雑なので文字列だけにした 25

Slide 26

Slide 26 text

テキスト形式パーサ テキスト形式パーサ S式パーサの結果を入力してインタプリタが読める形に出 力するパーサ バイナリ形式パーサと共通化したいのでパース結果の型だ け別モジュール wasm‑ast に定義 26

Slide 27

Slide 27 text

リファレンス リファレンス で wat を書いてみる Empty Wat Project を選ぶと wat を書ける おかしいところではエラーがちゃんと出るので嬉しい Mozilla の解説記事 公式ドキュメント WebAssembly Studio 27

Slide 28

Slide 28 text

公式ドキュメント 公式ドキュメント const exportSection = seq(keyword("export"), string, exportDesc) const exportDesc = or( array(seq(keyword("func"), identifier)), array(seq(keyword("memory"), identifier)) ) 28

Slide 29

Slide 29 text

命令の型定義 命令の型定義 ひたすら書く 29

Slide 30

Slide 30 text

命令のパーサ 命令のパーサ ひたすら書く 30

Slide 31

Slide 31 text

型の集合をいい感じに扱う 型の集合をいい感じに扱う 31

Slide 32

Slide 32 text

Before Before interface BaseOperation { opType: string } interface Nop extends BaseOperation { opType: "nop" } interface Local_Get extends BaseOperation { opType: "local.get" parameter: number } // ベースの interface で受け取る関数 function runOperation(op: BaseOperation) { switch(op.opType) { case "nop": ... case "local.get": const op2 = op as Local_Get // キャストが必要 op2.parameter ... default: // 網羅してることが保証されないので default が必要 } } 32

Slide 33

Slide 33 text

After After // ジェネリクスパラメータに文字列そのものを渡す interface BaseOperation { opType: T } interface Nop extends BaseOperation<"nop"> {} interface Local_Get extends BaseOperation<"local.get"> { parameter: number } // | で繋いだ型の集合を作る const AnyOperation = Nop | Return // AnyOperation で受け取る関数 function runOperation(op: AnyOperation) { switch(op.opType) { case "nop": ... case "local.get": op.parameter // キャストせずに Local_Get 型として扱える ... // AnyOperation の全てを網羅しているので default を書かないでよい } } 33

Slide 34

Slide 34 text

fold 表記 fold 表記 分かりやすく書ける表記法 (i32.add (i32.const 3) (i32.const 14)) i32.const 3 i32.const 14 i32.add 34

Slide 35

Slide 35 text

fold 表記の対応 fold 表記の対応 入れ子をフラットに展開するパーサを追加して対応 ↓ const foldedInstructions = map( array( seq( plainInstructions, opt(map(many(lazy(() => operations)), r => flatten(r))) ) ), r => [...(r[1] ? r[1] : []), r[0]] ) ["i32.add", ["i32.const", "3"], ["i32.const", "14"]] [ { opType: "i32.const", parameter: { i32: 3 } }, { opType: "i32.const", parameter: { i32: 14 } }, { opType: "i32.add" } ] 35

Slide 36

Slide 36 text

ラベル ラベル $ で始まる部分 変数や関数に名前をつけて分かりやすく書ける表記法 バイナリ形式には無い export とは関係ない (func $fib (export "fib") (param $p0 i32) (result i32) (local $l0 i32)) 36

Slide 37

Slide 37 text

ラベルの処理 ラベルの処理 パーサで対応するにはパースしながらラベルの宣言を保持 していく必要がある パーサに状態を持たせたくないので、そのまま文字列とし て保持 特別なオペレータとして型を付ける インタプリタの前処理で対応 interface TextGlobalGet { opType: "text.global.get" parameter: string } interface GlobalGet { opType: "global.get" parameter: number } 37

Slide 38

Slide 38 text

感想 感想 命令がなんだかんだ多い 思ったより仕様変更が頻繁で驚いた 命令の名前の変更がちょうどあった (get_local ‑> local.get 等) wasm にはバージョンがあるけどテキスト形式にはバー ジョンがない 今後もたくさん機能追加があるようで仕様を追い続けるの は大変そう 38

Slide 39

Slide 39 text

WASM パーサーを書いた話 WASM パーサーを書いた話 テキスト形式より後に作ったけどこっちの方が仕様が小さ くて簡単だった 先にやればよかった セクションの順番が決まっててパースしやすい テキスト形式のパーサと同じ出力に揃える (wasm‑ast) 39

Slide 40

Slide 40 text

開発方法 開発方法 を読む でバイナリを眺める バイナリのところをマウスオーバーするとどのような値 にパースすべきか分かる 公式ドキュメント WebAssembly Code Explorer 40

Slide 41

Slide 41 text

可変長数値表現の取扱い 可変長数値表現の取扱い 固定長の整数ではなく LEB128 という可変長整数 MSB が継続するかフラグ、残りの 7 bit がデータ プログラムに大きな数字はあまり出てこないので効率的 MIDI の VLQ を思い出す データの内容によって挙動を変えるパーサが必要 41

Slide 42

Slide 42 text

セクションのパース セクションのパース 最初にセクションの id とサイズが入っている 画像ファイルの chunk みたいなもの custom section は読み飛ばす必要がある 動的に指定サイズ分を読み込むパーサが必要 42

Slide 43

Slide 43 text

seqMap seqMap 第1引数でパースした内容を使って第2引数の関数でパーサ を生成して返すパーサ 名前は適当 例: サイズの 32bit 整数を読み込んでそのサイズ分の変数を読 み込むパーサ seqMap(u32, size => variable(size)) // variable は指定したサイズを読み込むパーサ 43

Slide 44

Slide 44 text

インタプリタを書いた話 インタプリタを書いた話 分かりやすさ重視、パフォーマンスは二の次というコンセ プト なるべくクラスではなく関数として実装 44

Slide 45

Slide 45 text

最初の実装方針 最初の実装方針 最初は仮想機械にこだわってエミュレータみたいな実装に していた ALU やメモリなど装置に対応するコードを書いていた グローバルな状態が多く綺麗ではなかった 45

Slide 46

Slide 46 text

今の実装方針 今の実装方針 AST をいかに JavaScript の関数にマッピングするかという 問題として捉えた block のスコープを関数スコープに合わせる メモリを引数にとる関数をコードから生成する構造として 実装 46

Slide 47

Slide 47 text

抽象的なインタプリタを実装 抽象的なインタプリタを実装 命令を読み込んでメモリを操作するもの プログラムカウンタを増やして繰り返す 中断をサポート 命令はメモリに対する操作として一般化 メモリにプログラムカウンタも入れ、break などもメモ リの操作として表現できる 47

Slide 48

Slide 48 text

export type InstructionSet = (code: Code) => Instruction // mutates register and memory export type Instruction = (memory: Memory) => void export interface VMMemory { programCounter: number programTerminated: boolean } /** * 特定の命令セットやプログラムに依存しない VM の抽象的な実装 * 利用側が命令セットとプログラムの組み合わせを用意する */ export const virtualMachine = ( instructionSet: InstructionSet, ) => (program: Code[], memory: Memory) => { while (memory.programCounter < program.length && !memory.programTerminated) { const code = program[memory.programCounter++] t i t i t ti S t( d ) 48

Slide 49

Slide 49 text

WebAssembly インタプリタ WebAssembly インタプリタ の実装 の実装 49

Slide 50

Slide 50 text

WASMMemory WASMMemory export interface WASMMemory extends VMMemory { readonly values: Stack readonly memory: DataView readonly global: WASMMemoryValue[] readonly local: WASMMemoryValue[] readonly functions: WASMFunction[] readonly table: WASMTable readonly programCounter: number programTerminated: boolean } export type WASMMemoryValue = Int32 | Int64 | Float32 | Float64 export type WASMTable = { // index: funcId [key: number]: number } 50

Slide 51

Slide 51 text

命令の実装 命令の実装 opType で判別して memory を操作する関数を返す const variableInstructionSet = (code: WASMCode): ((memory: WASMMemory) => void) => switch (code.opType) { case "drop": return ({ values }) => { values.pop() } ... 51

Slide 52

Slide 52 text

block の実装 block の実装 AST の Block には中身の命令が入っている func と同様に、JavaScript の関数に変換 block, call それぞれがインタプリタを生成する たくさんのインタプリタを使い捨てる interface BlockBase extends Base { results: ValType[]; body: AnyOperation[]; } interface Block extends BlockBase<"block"> { } interface Loop extends BlockBase<"loop"> { } 52

Slide 53

Slide 53 text

br と return br と return br は loop と block で挙動が違う ジャンプ先のプログラムカウンタの計算に末尾、先頭を 指定できるようにする block を関数にマッピングしているので、return がいわゆる javascript の return に対応しない 戻り値に break する回数を返して対処 53

Slide 54

Slide 54 text

数値型の取扱い 数値型の取扱い i32, f32, f64: number i64: BigInt 最初はバイナリを保持して全部自前で加算や乗算をビット 演算で実装してた あまりにも膨大で大変なので普通に number 型を使うよう にした 仮想機械といっても実行環境でサポートされた型じゃな いととても大変 i64 は BigInt を使う。Chrome でも node でも実行可能 54

Slide 55

Slide 55 text

数値型への変換 数値型への変換 パースする段階で数値型に変換 インタプリタでやっても良かったが、数値がどのような表 現で入っているかは各フォーマットの仕様なのでパーサで 対応 parseInt, parseFloat, new BigInt(number) を使う 55

Slide 56

Slide 56 text

DataView DataView Memory 周りの命令で使用 数値をバイト列に変換して書き込む バイト列から読み込む i32.load、i32.store8_s など 最初はメモリを Uint8Array として持ち、各数値型ごとに頑 張ってバイナリ表現を実装していた DataView.setUInt8 などがあることに気づいて DataView に 変えた BigInt も対応していてうってつけだった 56

Slide 57

Slide 57 text

wast について wast について に仕様のテストコードがある 各命令について正しく動作しているか確認できる テストコードはテキスト形式の拡張仕様 assert_return などテストコード用の命令がある 1ファイルに複数の S式が並んでいる形式なので専用のパー サを用意 例外チェックは面倒なので値のテストだけ動かしている テストがあって安心だった WebAssembly のリポジトリ 57

Slide 58

Slide 58 text

playground を書いた話 playground を書いた話 9割くらいjestでテスト駆動開発してきた 対話的に触ってみたくなった いままで書きっぱなしで使える状態になってないプログ ラムを多く書いた反省からいつでも Web で動くものを 用意している 普段なら React を使う所だが JSX を使いたくないので lit‑ html を使った Qiita に紹介記事を書いたのでよければ見てね 58

Slide 59

Slide 59 text

monorepo monorepo 最初は一つの module にディレクトリで分けて書いていた 後から分けた 効率おちるので構成が固まるまで分けないか、最初から 分けるかが良さそう 完全に分離される気持ち良さがある Lerna は必要なかった Typescript と相性悪そうだった npm install でローカルのモジュールに依存できる シンボリックリンクがはられるのでいちいち install し直 さなくても最新になる playground は全てのモジュールに依存している 59

Slide 60

Slide 60 text

終わりに 終わりに

Slide 61

Slide 61 text

処理系を書いて WebAssembly を (やや偏った方向に) 深く 知ることができた TypeScript の型に助けられた 言語を超えたモジュールづくりはできそうか? 仕様を見る限り難しそう 型が落ちるので wasm バイナリだけでなくメタデータ的 なものが必要 WebIDL をブラウザのAPIだけでなく wasm モジュールの メタデータとしてバイナリと一緒に配る方法があったら 良いかも?詳しくないので教えてほしい JS を挟むので現状あまり意味ない 現状ではアプリケーション全体を特定の言語で書いて wasmに変換するのが実用的と感じた Unity とか Rust とか Go とかで 60

Slide 62

Slide 62 text

今後について 今後について スペックテストの網羅 勉強用に作ったけど用途があったら教えて下さい PR, Fork 大歓迎 JavaScript API を標準仕様に寄せる AssemblyScript に移植して WASM で動かす バイナリ形式とテキスト形式の相互変換 61

Slide 63

Slide 63 text

小ネタ 小ネタ テキスト形式の名称は wat か wast か local 変数があるからスタックマシンじゃない WAST vs WAT · webassemblyjs WebAssembly Is Not a Stack Machine 62