Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeScript で WebAssembly 処理系を書いた話

ryohey
April 19, 2019

TypeScript で WebAssembly 処理系を書いた話

ryohey

April 19, 2019
Tweet

Other Decks in Programming

Transcript

  1. タイムライン タイムライン 2018年後半 WebAssembly 知りたくなる 2018年11月 WebAssembly 処理系の実装に着手 2018年12月 S式パーサ、テキスト形式パーサを実装

    2019年1月 インタプリタを実装 フィボナッチ数を計算するモジュールが動く 2019年2月 バイナリ形式パーサを実装 ブラウザ環境を実装 2019年3月 足りてない命令の実装など (現在も進行中) 11
  2. 何を書いたか 何を書いたか パーサコンビネータ fn‑parser S 式パーサ s‑parser 命令セットの型定義 wasm‑ast wat

    パーサ wat‑parser wasm パーサ wasm‑parser インタプリタ wasm‑vm ブラウザ環境 playground それぞれ npm のモジュールとして実装 (publish してないので npm install できないかも) 12
  3. 簡単なパーサ 簡単なパーサ より引用 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
  4. 簡単なパーサコンビネータ 簡単なパーサコンビネータ // 渡された複数のパーサのどれかが成功したら成功するパーサを生成する関数 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
  5. 例: 数式のパース 例: 数式のパース // 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
  6. 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
  7. リファレンス リファレンス で wat を書いてみる Empty Wat Project を選ぶと wat

    を書ける おかしいところではエラーがちゃんと出るので嬉しい Mozilla の解説記事 公式ドキュメント WebAssembly Studio 27
  8. 公式ドキュメント 公式ドキュメント const exportSection = seq(keyword("export"), string, exportDesc) const exportDesc

    = or( array(seq(keyword("func"), identifier)), array(seq(keyword("memory"), identifier)) ) 28
  9. 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
  10. After After // ジェネリクスパラメータに文字列そのものを渡す interface BaseOperation<T extends string> { 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
  11. 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
  12. 感想 感想 命令がなんだかんだ多い 思ったより仕様変更が頻繁で驚いた 命令の名前の変更がちょうどあった (get_local ‑> local.get 等) wasm

    にはバージョンがあるけどテキスト形式にはバー ジョンがない 今後もたくさん機能追加があるようで仕様を追い続けるの は大変そう 38
  13. 可変長数値表現の取扱い 可変長数値表現の取扱い 固定長の整数ではなく LEB128 という可変長整数 MSB が継続するかフラグ、残りの 7 bit がデータ

    プログラムに大きな数字はあまり出てこないので効率的 MIDI の VLQ を思い出す データの内容によって挙動を変えるパーサが必要 41
  14. export type InstructionSet<Code, Memory> = (code: Code) => Instruction<Memory> //

    mutates register and memory export type Instruction<Memory> = (memory: Memory) => void export interface VMMemory { programCounter: number programTerminated: boolean } /** * 特定の命令セットやプログラムに依存しない VM の抽象的な実装 * 利用側が命令セットとプログラムの組み合わせを用意する */ export const virtualMachine = <Code, Memory extends VMMemory>( instructionSet: InstructionSet<Code, Memory>, ) => (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
  15. WASMMemory WASMMemory export interface WASMMemory extends VMMemory { readonly values:

    Stack<WASMMemoryValue> 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
  16. 命令の実装 命令の実装 opType で判別して memory を操作する関数を返す const variableInstructionSet = (code:

    WASMCode): ((memory: WASMMemory) => void) => switch (code.opType) { case "drop": return ({ values }) => { values.pop() } ... 51
  17. block の実装 block の実装 AST の Block には中身の命令が入っている func と同様に、JavaScript

    の関数に変換 block, call それぞれがインタプリタを生成する たくさんのインタプリタを使い捨てる interface BlockBase<T extends string> extends Base<T> { results: ValType[]; body: AnyOperation[]; } interface Block extends BlockBase<"block"> { } interface Loop extends BlockBase<"loop"> { } 52
  18. br と return br と return br は loop と

    block で挙動が違う ジャンプ先のプログラムカウンタの計算に末尾、先頭を 指定できるようにする block を関数にマッピングしているので、return がいわゆる javascript の return に対応しない 戻り値に break する回数を返して対処 53
  19. 数値型の取扱い 数値型の取扱い i32, f32, f64: number i64: BigInt 最初はバイナリを保持して全部自前で加算や乗算をビット 演算で実装してた

    あまりにも膨大で大変なので普通に number 型を使うよう にした 仮想機械といっても実行環境でサポートされた型じゃな いととても大変 i64 は BigInt を使う。Chrome でも node でも実行可能 54
  20. DataView DataView Memory 周りの命令で使用 数値をバイト列に変換して書き込む バイト列から読み込む i32.load、i32.store8_s など 最初はメモリを Uint8Array

    として持ち、各数値型ごとに頑 張ってバイナリ表現を実装していた DataView.setUInt8 などがあることに気づいて DataView に 変えた BigInt も対応していてうってつけだった 56
  21. wast について wast について に仕様のテストコードがある 各命令について正しく動作しているか確認できる テストコードはテキスト形式の拡張仕様 assert_return などテストコード用の命令がある 1ファイルに複数の

    S式が並んでいる形式なので専用のパー サを用意 例外チェックは面倒なので値のテストだけ動かしている テストがあって安心だった WebAssembly のリポジトリ 57
  22. monorepo monorepo 最初は一つの module にディレクトリで分けて書いていた 後から分けた 効率おちるので構成が固まるまで分けないか、最初から 分けるかが良さそう 完全に分離される気持ち良さがある Lerna

    は必要なかった Typescript と相性悪そうだった npm install でローカルのモジュールに依存できる シンボリックリンクがはられるのでいちいち install し直 さなくても最新になる playground は全てのモジュールに依存している 59
  23. 処理系を書いて WebAssembly を (やや偏った方向に) 深く 知ることができた TypeScript の型に助けられた 言語を超えたモジュールづくりはできそうか? 仕様を見る限り難しそう

    型が落ちるので wasm バイナリだけでなくメタデータ的 なものが必要 WebIDL をブラウザのAPIだけでなく wasm モジュールの メタデータとしてバイナリと一緒に配る方法があったら 良いかも?詳しくないので教えてほしい JS を挟むので現状あまり意味ない 現状ではアプリケーション全体を特定の言語で書いて wasmに変換するのが実用的と感じた Unity とか Rust とか Go とかで 60