Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

Node.jsでつくるNode.js ミニインタープリター&コンパイラー

551 vues

Publié le

東京Node学園祭2018の発表資料です。書籍「RubyでつくるRuby」を真似て、小さなインタープリター&コンパイラーを作ってみました。バイナリ生成にはLLVMを利用しています。

Publié dans : Technologie
  • Soyez le premier à commenter

  • Soyez le premier à aimer ceci

Node.jsでつくるNode.js ミニインタープリター&コンパイラー

  1. 1. Node.jsでつくるNode.js ミニインタープリター&コンパイラー Build Node.js mini-interpreter and mini-compiler 東京Node学園祭2018 / Nodefest 2018 2018.11.23 インフォコム株式会社 がねこまさし @massie_g
  2. 2. 自己紹介 • がねこまさし / @massie_g • インフォコム(株)の技術調査チームのマネージャー • WebRTC Meetup Tokyo スタッフ • WebRTC Beginners Tokyo スタッフ • 東京Node学園祭2017 • Node.js x Chrome headless で、お手軽WebRTC MCU • https://bit.ly/2QmuECy 2
  3. 3. 今日のお話 • 対象者 • プログラミング言語のしくみに興味がある人 • ちょっと複雑なプログラムを作るのに困っている人 • 内容 • 1. ミニインタープリター編 • 2. ミニコンパイラー編
  4. 4. 1. ミニインタープリター編 詳細は Qiitaの一連の記事をご覧ください Node.jsでつくるNode.js - もくじ https://qiita.com/massie_g/items/3ee11c105b4458686bc1
  5. 5. きっかけ(1) Ruby でつくる Ruby インクリメンタルな開発ステップ (作っては、動かす) ↓ 簡単なプログラミング言語が作れる
  6. 6. 「Ruby でつくる Ruby」を写経 • 1, 2章 … Rubyの超入門。変数、条件分岐、繰り返し • 3章 … あとあと重要になる木構造の解説 • 4章 … MinRuby の実装開始。まずは四則演算から • 5章 … 変数 • 6章 … 条件分岐 • 7章 … 組み込み関数 • 8章 … ユーザ定義関数 • 9章 … 配列、ハッシュ → ブートストラップ達成
  7. 7. 写経し終えての感想 • MinRuby の仕様の範囲がとても良く考えられている • 条件分岐やループ、データ構造など、最低限の機能を備えている • 複雑な処理は外部モジュール(gem)に任せ、本体はコンパクトに • ファイルアクセス、ソースコードのパースはgemで • → 自分自身を実行可能に(ブートストラップ) • 自分でも小さな言語処理系を作って見たい • 「Ruby でつくる Ruby 」を真似すれば、できそう • やるなら、なじみのあるNode.js / JavaScript で
  8. 8. Node.js ミニ(マム)インタプリターの目標 MinRuby Node.js ミニインタープリター 四則演算 ○ ○ 変数 ○ ○ (※let 宣言必須に) 条件分岐 if - else if - else 繰り返し while while 組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用 ユーザー定義関数 ○ ○ 配列 ○ ○ ハッシュ ○ ○ (連想配列) ブートストラップ/セルフホスト ○ ○ MinRubyの仕様をそのまま実現したい。ブートストラップが目標
  9. 9. MinRubyの構成 Ruby インタープリター Interp.rb evaluate() パーサー gem minruby.rb simplify() 対象 ソースコード .rb ripper AST: 抽象構文木S-AST: 単純化AST 読み込み実行 AST … abstract syntax tree ソースコードを構文解析し 木構造で表現したもの (不要な情報は省略される)
  10. 10. Node.js ミニインタープリター の構成 Node.js インタープリター mininode.js 対象 ソースコード .js esprima AST: 抽象構文木 読み込み 実行 evaluate() S-AST: 単純化AST パーサー mininode_parser.js makeTree(), simplify() Ruby インタープリター Interp.rb evaluate() パーサー gem minruby.rb simplify() 対象 ソースコード .rb ripper
  11. 11. espirmaでASTを取得 const esprima = require("esprima"); function parseSrc(src) { const ast = esprima.parseScript(src); return ast; } const ast = parseSrc('2 + 3'); console.dir(obj, {depth: 10}); Script { type: 'Program', body: [ ExpressionStatement { type: 'ExpressionStatement', expression: BinaryExpression { type: 'BinaryExpression', operator: '+', left: Literal { type: 'Literal', value: 2, raw: '2' }, right: Literal { type: 'Literal', value: 3, raw: '3' } } } ], sourceType: 'script' }
  12. 12. simplify() : ASTの単純化 Script { type: 'Program', body: [ ExpressionStatement { type: 'ExpressionStatement', expression: BinaryExpression { type: 'BinaryExpression', operator: '+', left: Literal { type: 'Literal', value: 2, raw: '2' }, right: Literal { type: 'Literal', value: 3, raw: '3' } } } ], sourceType: 'script' } [ '+', [ 'lit', 2 ], [ 'lit', 3 ] ] + 2 3
  13. 13. パーサーモジュールのsimplify() のコード抜粋 function makeTree(ast) { const exp = ast.body[0].expression; return simplify(exp); } function simplify(exp) { if (exp.type === 'Literal') { return ['lit', exp.value]; } if (exp.type === 'BinaryExpression') { if (exp.operator === '+') { return ['+', simplify(exp.left), simplify(exp.right)] } } // … 省略 … } Script { type: 'Program', body: [ ExpressionStatement { type: 'ExpressionStatement', expression: BinaryExpression { type: 'BinaryExpression', operator: '+', left: Literal { type: 'Literal', value: 2, raw: '2' }, right: Literal { type: 'Literal', value: 3, raw: '3' } } } ], sourceType: 'script' } ※ASTは木構造なので、再帰的に simplify() を呼び出す処理になる
  14. 14. インタープリターのevaluate(): 単純化ASTの実行 function evaluate(tree) { if (tree[0] === 'lit') { return tree[1]; } if (tree[0] === '+') { return evaluate(tree[1]) + evaluate(tree[2]); } // … 省略 … } [ '+', [ 'lit', 2 ], [ 'lit', 3 ] ] 単純化AST インタープリターでインタープリターを作る場合、その言語の機能をそのまま使える
  15. 15. 紆余曲折をへて、ミニインタープリター完成 • × パーサーを全部作ってから、インタープリターを全部作る • ○ 各ステップごとに、パーサー→インタプリターで実行 • 定数、四則演算 • 変数、条件分岐、繰り返し、 • 組み込み関数、ユーザ定義関数、リターン処理 • 配列、ハッシュ(連想配列) • 詳細は Qiitaの一連の記事をご覧ください • Node.jsでつくるNode.js - もくじ • https://qiita.com/massie_g/items/3ee11c105b4458686bc1
  16. 16. Demo : FizzBuzz Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js 実行 Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js インタープリター mininode.js 実行 Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js インタープリター mininode.js インタープリター mininode.js インタープリターで実行 ブートストラップ:インタープリターでインタープリターを実行 多段ロケット
  17. 17. Node.js ミニインタープリターを作って分かったこと • MinRubyの設計と進め方が、とても良い • やること/やらないことの切り分け、ステップの刻み方 • 中間表現の単純化ASTが良い指針 • Ruby と JavaScript の違い • Ruby … 最後の評価値が、関数の戻り値になる (return は省略可能) • JavaScript … 値を返すには、明示的に「return 値」が必要 • MinRubyでは(おそらく意図的に)return をサポートしていない • ミニNode.js では明示的な return 文に対応 → 思ったより厄介
  18. 18. • evaluate() で単純化ASTの木構造をたどりながら実行していく • 右下の図では、左から右、下から上の順 • どこかで return が発生したら、残りスキップして値を上位に返す • 関数を抜けるまで、上位にもどる • 「現在 return 中」を伝える必要がある • 複数戻り値(多値) or グローバルな状態、など return 処理の実装 function isBig(x) { if (x >= 10) { return "big"; } return "small"; } isBig(20); stmts if ret lit 'small' >= var_ref x lit 10 ret lit 'big' ❌実行しない 戻る 戻る今回はこっちを採用 対象ソースコード.js
  19. 19. Node.js ミニインタープリターを作って分かったこと(2) • 1段目は、普通にデバッガでデバッグできる • 2段目(ブートストラップ)になると、デバッガは使えない Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js インタープリター mininode.js • 何か自分でデバッガ的なものを作れる? → 無理 • print (console.log)でのデバッグ? • ログが1段目のものか、2段目のものか分からなくなる • →ほぼ同じで、メッセージが異なる2つのソースを使った デバッガでステップ実行可 ステップ実行できない
  20. 20. Node.js ミニインタープリターを作って分かったこと(2) • 1段目は、普通にデバッガでデバッグできる • 2段目(ブートストラップ)になると、デバッガは使えない Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js インタープリター mininode.js • 何か自分でデバッガ的なものを作れる? → 無理 • print (console.log)でのデバッグ? • ログが1段目のものか、2段目のものか分からなくなる • →ほぼ同じで、メッセージが異なる2つのソースを使った デバッガでステップ実行可 ステップ実行できない Node.js インタープリター mininode_outer.js 対象 ソースコード fizzbuzz.js インタープリター mininode_inner.js
  21. 21. 作って分かったこと(3) 書籍では語られないMinRubyと仲間たちの性質 • 変数定義 … lenv[]というハッシュ(連想配列)に格納 • lenv … おそらく、local environment の意味 • 関数定義 … genv[]というハッシュ(連想配列)に格納 • genv … おそらく、global environment の意味 この実装により、素のRuby/Node.jsとは異なる性質がある → ※これがブートストラップ時のバグにつながった
  22. 22. MinRuby / ミニNode.js の変数の実装 • 変数の実体は、lenv[]というハッシュ(連想配列) • 関数呼び出し時は、新しいハッシュを用意 function add(x, y) { let z = x + y; return z; } let a = 1; a = add(a, 2); // ---- 擬似コード(1) ---- // 変数が宣言されたら、lenvに値を格納 lenv['a'] = 1 対象ソースコード
  23. 23. MinRuby / ミニNode.js の変数の実装 • 変数の実体は、lenv[]というハッシュ(連想配列) • 関数呼び出し時は、新しいハッシュを用意 function add(x, y) { let z = x + y; return z; } let a = 1; a = add(a, 2); // ---- 擬似コード(1) ---- // 変数が宣言されたら、lenvに値を格納 lenv['a'] = 1 対象ソースコード // ---- 擬似コード(2) ---- // 関数を呼び出すときは、新しいハッシュを用意 // 引数を関数宣言の引数名で格納  関数に渡す newLenv['x'] = lenv['a'] ; newLenv['y'] = 2;
  24. 24. MinRuby / ミニNode.js の変数の実装 • 変数の実体は、lenv[]というハッシュ(連想配列) • 関数呼び出し時は、新しいハッシュを用意 function add(x, y) { let z = x + y; return z; } let a = 1; a = add(a, 2); // ---- 擬似コード(1) ---- // 変数が宣言されたら、lenvに値を格納 lenv['a'] = 1 対象ソースコード // ---- 擬似コード(3) ---- // 関数では、渡されたハッシュの中から値を取得 newLenv['z'] = newLenv['x'] + newLenv['y']; return newLenv['z']; ハッシュが違う =スコープが違う ↓ 関数内の ローカル変数 トップレベルの変数は関数内からは見えない → グローバル変数ではない // ---- 擬似コード(2) ---- // 関数を呼び出すときは、新しいハッシュを用意 // 引数を関数宣言の引数名で格納  関数に渡す newLenv['x'] = lenv['a'] ; newLenv['y'] = 2;
  25. 25. MinRuby / ミニNode.js の変数の実装 • ブロックスコープは無い function func(a) { let x = 1; if (a == 1) { let x = func1(a); // … 省略 … } else if (a == 3) { let x = func2(a); // … 省略 … } // … } 対象ソースコード Node.js / JavaScript ならブロックスコープ x は全て別の変数として扱われる ミニNode.js ではすべて同じ関数ローカルスコープ x は同じ変数として扱われる ※重複定義でエラー グローバル変数や、ブロックスコープをきちんと扱うには 特別な配慮が必要なことを実感
  26. 26. MinRuby / ミニNode.js のユーザ定義関数 • 関数定義は、genv[]というハッシュ(連想配列)に格納される • 呼び出し時に、genv[]の中を探して呼び出す • 先に定義しておく必要がある • 一見関数内のローカル関数が使えそうだが、実際はグローバル関数になる function func1(a) { function func2(x) { return x*2; } return func2(a+1); } function func2(y) { return y+2; } これは二重定義のエラー function func1(a) { function func2(x) { return x*2; } return func2(a+1); } function func2(x) { return x*2; } function func1(a) { func2(a+1); } 対象ソースコード ミニNode.jsの解釈
  27. 27. 2. コンパイラー編 詳細は Qiitaの一連の記事をご覧ください Node.jsでつくるNode.jsミニコンパイラ - もくじ https://qiita.com/massie_g/items/3ba1ba5d55499ee84b0b
  28. 28. きっかけ(2) Turing Complete FM • Turing Complete FM https://turingcomplete.fm • 言語やOSを作る話など、低レイヤーの話題がいっぱいのポッドキャスト • オーナーのRuiさん自身がCコンパイラ(8CC, 9CC) を作った話も • 聞きながら、2x年前の目標を思い出す • 「コンパイラー作って見たい」→ 当時は挫折 • ミニインタープリターを作った今なら、できるかも • コンパイラーのややこしい部分は、自分でやるのは諦める • パーサーは外部モジュールを使う • バイナリの生成は、LLVMにお任せ
  29. 29. Node.js ミニ(マム)コンパイラーの目標 MinRuby Node.js ミニインタープリター Node.js ミニコンパイラー 型 整数、実数、文字列, … 整数、実数、文字列, … 32ビット符号あり整数のみ 四則演算 ○ ○ ○ 変数 ○ ○ (※let 宣言必須に) ○ (※let 宣言必須に) 条件分岐 if - else if - else if - else 繰り返し while while while 組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用 画面出力(標準出力)用 ユーザー定義関数 ○(再帰呼び出しも可) ○(再帰呼び出しも可) ○ (再帰呼び出しも可) 配列 ○ ○ × ハッシュ ○ ○ (連想配列) × セフルホスト ○ ○ ×(ただしミニインタープ リターから実行可能に) 整数のみ対応。関数を使って、FizzBuzzとフィボナッチ数列を目標に
  30. 30. LLVMとは • llvm.org より • LLVMプロジェクトは、モジュール化された再利用可能なコンパイラ およびツールチェーン技術の集まりです • もともとは Low Level Virtual Machine の略語 • 現在は「LLVM」が正式名称 • 最近の言語系ではよく利用さている • Clang, Swift, Rust など • ASM.jsやWebAssemblyを生成するEmscriptenも
  31. 31. LLVM のインストール 方法は3通り • (A) ソースコードからビルドする • (B) パッケージ管理ソフトを使ってインストール • Mac OS Xの場合はhomebrewを使う • (C) ビルド済みのバイナリ(Pre-build)をダウンロードする • LLVM Download Page • http://releases.llvm.org/download.html
  32. 32. LLVM の中間表現とビットコード • Intermediate Representation(IR) … テキストの中間表現 • ビットコード … IRをバイナリにしたもの • Javaのバイトコードのようなもの? • 相互に変換可能 ソースコード コンパイラー LLVM-IR LLVM Bitcode llc オブジェクト ファイル リンカー 実行 モジュール LLVMのパイプライン
  33. 33. LLVM IR を学ぶ • LLVM Language Reference Manual • http://llvm.org/docs/LangRef.html • あまりに長大すぎて、手に負えない • LLVMを始めよう! 〜 LLVM IRの基礎はclangが教えてくれ た・Brainf**kコンパイラを作ってみよう 〜 • https://itchyny.hatenablog.com/entry/2017/02/27/100000 • C言語から LLVM-IR を生成 • 最低限動く状態まで付加情報を削って理解する • 詳細は、リファレンスの該当箇所を確認する
  34. 34. 例)1 を返すだけの、シンプルなプログラム int main() { return 1; } one.c clang -S -emit-llvm -O0 one.c one.ll ; ModuleID = 'one.c' source_filename = "one.c" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.12.0" ; Function Attrs: noinline nounwind ssp uwtable define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0, i32* %1, align 4 ret i32 1 } attributes #0 = { noinline nounwind ssp uwtable … 以下省略
  35. 35. 例)1 を返すだけの、シンプルなプログラム int main() { return 1; } one.c clang -S -emit-llvm -O0 one.c one.ll ; ModuleID = 'one.c' source_filename = "one.c" target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-apple-macosx10.12.0" ; Function Attrs: noinline nounwind ssp uwtable define i32 @main() #0 { %1 = alloca i32, align 4 store i32 0, i32* %1, align 4 ret i32 1 } attributes #0 = { noinline nounwind ssp uwtable … 以下省略 define i32 @main() { ret i32 1 } one_simple.ll ギリギリまで 簡略化 $ lli one_simple.ll || echo $? 1 lli で IRを実行
  36. 36. 例)足し算、他の四則演算 int main() { return 1 + 2; } add.c add_simple.ll define i32 @main() { %1 = add i32 1, 2 ret i32 %1 } 試行錯誤&簡略化 • 足し算 … add • 引き算 … sub • 掛け算 … mul • 割り算 … sdiv(符号付き)、udiv(符号無し) • 余り … srem(符号付き)、urem(符号無し)
  37. 37. 例)足し算、他の四則演算 int main() { return 1 + 2; } add.c add_simple.ll define i32 @main() { %1 = add i32 1, 2 ret i32 %1 } 試行錯誤&簡略化 %1 … レジスター CPU 演算ユニット レジスタ レジスタ レジスタ メモリー load store CPUではメモリ上のデータを 一旦レジスタに読み込んでから利用する LLVM IR では仮想的なCPUを想定している • レジスタの数は無制限 • ただし、値の代入は1回しかできない • レジスタ名 • 連番 %0, %1, %2, … • 任意の名前 … %v1, %x など
  38. 38. Node.jsミニコンパイラー の構成 • ミニインタープリターの構成に近い • evaluate()で実行する代わりに、compile()  generate()でLLVM-IRを生成 • generate() を再帰的に呼び出す • メモリ上に全て蓄えて最後に書き出す、という素朴な実装 Node.js コンパイラー mininode_compiler.js 対象 ソースコード .js esprima AST: 抽象構文木 読み込み 実行 compile() generate() S-AST: 単純化AST LLVM-IR generated.ll 書き出し パーサー mininode_parser.js makeTree(), simplify()
  39. 39. ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく • 機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数
  40. 40. ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく • 機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数 例として このステップを説明
  41. 41. ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  42. 42. ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  43. 43. IR調査例: ローカル変数 int main() { int a = 1; a = a + 2; return 0; } add_var.c define i32 @main() { %1 = alloca i32, align 4 ; 変数aの領域を確保 store i32 1, i32* %1, align 4 ; 変数aに1を代入 %2 = load i32, i32* %1, align 4 ; 変数aを読み出し %3 = add nsw i32 %2, 2 ; 2 を加算 store i32 %3, i32* %1, align 4 ; 変数aに加算結果を代入 ret i32 0 } add_var.js let a = 1; a = a + 2;
  44. 44. IR調査例: ローカル変数 int main() { int a = 1; a = a + 2; return 0; } add_var.c define i32 @main() { %1 = alloca i32, align 4 ; 変数aの領域を確保 store i32 1, i32* %1, align 4 ; 変数aに1を代入 %2 = load i32, i32* %1, align 4 ; 変数aを読み出し %3 = add nsw i32 %2, 2 ; 2 を加算 store i32 %3, i32* %1, align 4 ; 変数aに加算結果を代入 ret i32 0 } add_var.js let a = 1; a = a + 2; alloca スタック上に変数領域を確保 ※関数終了時に解放される
  45. 45. IR調査例: ローカル変数 int main() { int a = 1; a = a + 2; return 0; } add_var.c define i32 @main() { %1 = alloca i32, align 4 ; 変数aの領域を確保 store i32 1, i32* %1, align 4 ; 変数aに1を代入 %2 = load i32, i32* %1, align 4 ; 変数aを読み出し %3 = add nsw i32 %2, 2 ; 2 を加算 store i32 %3, i32* %1, align 4 ; 変数aに加算結果を代入 ret i32 0 } add_var.js let a = 1; a = a + 2; load … 変数からの読み込み store … 変数への格納
  46. 46. 補足:スタック領域 • LIFO の構造を持っている • LLVM IR や多くのプログラミング言語で、 関数呼び出し時に利用される • 関数から戻る場所、引数を格納 • 関数内の一時的な記憶領域として利用 • 関数から戻るときには、領域は解放される 戻り先 引数1 引数2 一時変数1 一時変数2 古い 新しい ※時間の都合で、発表時はスキップします %0 %1 %3 %4 LLVM-IR 連番の場合
  47. 47. ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  48. 48. コード生成 generate()の動作:変数 • 単純化したASTを再帰的に辿りながら、IRコードを生成 • ノード毎に、演算結果をレジスタに格納 let a = 1; a = a + 2; 対象コード(js) %t2 = or i32 1, 0 var_decl 'a' lit 1 ※レジスタに値を直接代入する命令が見つからないため or を利用(手抜き) stmts '+' lit 2 var_assign 'a' %t1 = alloca i32, align 4 store i32 最後のレジスタ, i32* %t1, align 4 右辺の処理 var_ref 'a'
  49. 49. コード生成 generate()の動作:変数 • ノード毎に、演算結果をレジスタに格納 • そのレジスタを、後続の処理で利用 let a = 1; a = a + 2; 対象コード(js) %t2 = or i32 1, 0 var_decl 'a' lit 1 ※レジスタに値を直接代入する命令が見つからないため or を利用(手抜き) stmts '+' lit 2 var_assign 'a' %t1 = alloca i32, align 4 store i32 %t2, i32* %t1, align 4 右辺の処理 %t2 = or i32 1, 0 var_ref 'a'
  50. 50. コード生成 generate()の動作:変数 • 各行ごとに、処理内容のIRを組立てる let a = 1; a = a + 2; 対象コード(js) var_decl 'a' lit 1 stmts '+' lit 2 var_assign 'a' var_ref 'a' load i32 %t3, i32* %t1, align 4 %t4 = or i32 2, 0 %t5 = add i32 %t3, %t4 store i32 最後のレジスタ, i32* %t1, align 4 右辺の処理
  51. 51. コード生成 generate()の動作:変数 • 各行ごとに、処理内容のIRを組立てる • 各行の処理を連結する let a = 1; a = a + 2; 対象コード(js) var_decl 'a' lit 1 stmts '+' lit 2 var_assign 'a' var_ref 'a' load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 右辺の処理 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4
  52. 52. コード生成 generate()の動作:変数 • 各行ごとに、処理内容のIRを組立てる • 各行の処理を連結する let a = 1; a = a + 2; 対象コード(js) var_decl 'a' lit 1 stmts '+' lit 2 var_assign 'a' var_ref 'a' %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 生成されたLLVM-IR
  53. 53. 変数とローカルコンテキスト • lctx: ローカルコンテキスト … 関数内の状況を保持する • レジスタ、ラベルの通し番号(関数内で連番に) • 変数宣言時 … 変数の情報(alloca()で確保した領域=レジスタ)をlctxに覚える • 変数の参照や代入時は、変数の情報(対応するレジスタ)をlctx[]から取得 • 関数呼び出し時には、新しいローカルコンテキストを生成して利用する let a = 1; a = a + 2; 対象コード 生成されるLLVM IR %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 コンパイラー内部
  54. 54. 変数とローカルコンテキスト • lctx: ローカルコンテキスト … 関数内の状況を保持する • レジスタ、ラベルの通し番号(関数内で連番に) • 変数宣言時 … 変数の情報(alloca()で確保した領域=レジスタ)をlctxに覚える • 変数の参照や代入時は、変数の情報(対応するレジスタ)をlctx[]から取得 • 関数呼び出し時には、新しいローカルコンテキストを生成して利用する let a = 1; a = a + 2; 対象コード 生成されるLLVM IR %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 コンパイラー内部 変数 'a' の情報を lctx[] に覚える
  55. 55. 変数とローカルコンテキスト • lctx: ローカルコンテキスト … 関数内の状況を保持する • レジスタ、ラベルの通し番号(関数内で連番に) • 変数宣言時 … 変数の情報(alloca()で確保した領域=レジスタ)をlctxに覚える • 変数の参照や代入時は、変数の情報(対応するレジスタ)をlctx[]から取得 • 関数呼び出し時には、新しいローカルコンテキストを生成して利用する let a = 1; a = a + 2; 対象コード 生成されるLLVM IR %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 コンパイラー内部 変数 'a' の情報を lctx[] に覚える 変数 'a' の情報を lctx[] から 取り出して利用
  56. 56. 変数とローカルコンテキスト • lctx: ローカルコンテキスト … 関数内の状況を保持する • レジスタ、ラベルの通し番号(関数内で連番に) • 変数宣言時 … 変数の情報(alloca()で確保した領域=レジスタ)をlctxに覚える • 変数の参照や代入時は、変数の情報(対応するレジスタ)をlctx[]から取得 • 関数呼び出し時には、新しいローカルコンテキストを生成して利用する let a = 1; a = a + 2; 対象コード 生成されるLLVM IR %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 コンパイラー内部 変数 'a' の情報を lctx[] に覚える 変数 'a' の情報を lctx[] から 取り出して利用
  57. 57. define i32 @main() { ret i32 0; } generateMain() : main()関数の生成 • LLVM IRではmain()関数が必要 • generate()で生成した処理をくくってあげる コンパイラの generateMain() で生成 %t1 = alloca i32, align 4 %t2 = or i32 1, 0 store i32 %t2, i32* %t1, align 4 load i32 %t3, i32* %t1, align 4 %t4 = or i32 1, 0 %t5 = add i32 %t3, %t4 store i32 %t5, i32* %t1, align 4 コンパイラの generate () で生成
  58. 58. 各ステップの中身 • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  59. 59. コンパイル、実行、バイナリ生成 • コンパイル • $ node mininode_compiler.js 対象ソース.js • → generated.ll が生成される • lli で LLVM-IR やビットコードを実行することが可能 • $ lli generated.ll • llc でオブジェクトファイルを生成→リンカーでバイナリ生成 macOS 10.13の場合 • $ llc generated.ll -O0 -march=x86-64 -filetype=obj -o=generated.o • $ ld -arch x86_64 -macosx_version_min 10.12.0 generated.o -lSystem -o バイナリ名 • $ ./バイナリ名 $ node mininode_compiler.js 対象ソース.js $ lli generated.ll $ llc generated.ll -O0 -march=x86-64 -filetype=obj -o=generated.o $ ld -arch x86_64 -macosx_version_min 10.13.0 generated.o -lSystem -o バイナリ名 $ ./バイナリ名 ここで、FizzBuzz_funcのデモ
  60. 60. 組み込み関数 • FizzBuzzが目標 → 画面出力用に2つの組み込み関数を用意 • int puts(i8*) … 文字列を渡すと、画面に出力 • C言語の標準ライブラリの puts()をそのまま呼び出す • void putn(i32) … i32 の整数を渡すと、画面に出力 • C言語の標準ライブラリの printf()を利用 declare i32 @puts(i8*) ; -- 標準ライブラリの関数を参照 -- declare i32 @printf(i8*, ...) ; -- 標準ライブラリの関数を参照 -- ; -- 文字列定数を宣言 -- @.str = private unnamed_addr constant [5 x i8] c"%d0D0A00", align 1 ; -- 関数定義 -- define void @putn(i32) { ; -- 関数呼び出し -- %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i32 0, i32 0), i32 %0) ret void } generateBuiltin()で生成
  61. 61. 組み込み関数を利用した場合のIR生成 putn(123); puts("hello"); 対象コード(js) define i32 @main() { ; -- 実際の処理 ret i32 0; } 文字列定数の内容 (グルーバル定数) 組み込み関数の定義 C言語標準ライブラリ関数の宣言 コンパイラで最終的に 生成される LLVM-IR generateMain()で生成 generateGlobalString()で生成 generateBuiltin()で生成 @.s_0 = private constant [6 x i8] c"hello00", align 1 ; -- 実際の処理 generate()で生成 %t1 = or i32 123, 0 call void @putn(i32 %t1) %t2 = getelementptr inbounds [6 x i8], [6 x i8]* @.s_0, i32 0, i32 0 %t3 = call i32 @puts(i8* %t2)
  62. 62. 他の機能と LLVM-IR • (条件分岐) • (ループ)
  63. 63. 条件分岐→条件付きジャンプで実現 int main() { int a = 3; if ( a > 1 ) { putn(333); } else { putn(111); } return 0; } if.c define i32 @main() { %2 = alloca i32, align 4 ; 変数aの領域を確保 store i32 3, i32* %2, align 4 ; 変数aに3を代入 %3 = load i32, i32* %2, align 4 ; 変数aを読み出し ; --- if (a > 1) に相当 --- %4 = icmp sgt i32 %3, 1 ; 変数aの値と、1を比較 br i1 %4, label %L5, label %L6 ; 比較がtrueならL5, falseなら L6にジャンプ L5: ; -- 条件が真(1)の場合 --- call void @putn(i32 333) br label %L7 ; -- 後続処理にジャンプ – L6: ; -- 条件が偽(0)の場合 --- call void @putn(i32 111) br label %L7 ; -- 後続処理にジャンプ – L7: ; -- 後続処理 – ret i32 0 } if.js let a = 3; if ( a > 1 ) { putn(333); } else { putn(111); } 説明はスキップ
  64. 64. ループ→条件付きジャンプで実現 let a = 0; while (a < 10) { a = a + 1; } while.js define i32 @main() { %2 = alloca i32, align 4 store i32 0, i32* %2, align 4 br label %3 ;--- 条件判定の処理にジャンプ ; --- 条件判定 --- L3: %4 = load i32, i32* %2, align 4 %5 = icmp slt i32 %4, 10 br i1 %5, label %L6, label %L10 ;--- 条件が真ならループ内の処理に、不成立なら後続処理にジャンプ L6: ; -- 条件が真(1)の場合 --- %8 = load i32, i32* %2, align 4 %9 = add i32 %8, 1 store i32 %9, i32* %2, align 4 br label %L3 ; --- 条件判定にジャンプ L10: ; -- 後続処理 – ret i32 0 } 説明はスキップ int main() { int a = 0; while (a < 10) { a = a + 1; } return 0; } while.c
  65. 65. ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく • 機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数 例として このステップを説明
  66. 66. IR調査例:ユーザ定義関数、呼び出し int add(x, y) { return x + y; } int main() { return add(1, 2); } func.c ; -- ユーザ関数定義 -- define i32 @add(i32, i32) { %3 = i32 add %0, %1 ret i32 %3 } define i32 @main() { ; -- 関数呼び出し -- %1 = call i32 @add(i32 1, i32 2) ret i32 %1 } int add(x, y) { return x + y; } add(1, 2); func.js
  67. 67. ユーザ定義関数とグローバルコンテキスト • gctx: グローバルコンテキスト … プログラム全体の状況を保持する • 文字列定数、ユーザー定義関数 • 関数 • 関数定義があったら、gctx[] に定義内容を登録 • 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成 • 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す function add(x, y) { return x + y; } 対象コード gctx[] add() の定義内容を登録
  68. 68. ユーザ定義関数とグローバルコンテキスト • gctx: グローバルコンテキスト … プログラム全体の状況を保持する • 文字列定数、ユーザー定義関数 • 関数 • 関数定義があったら、gctx[] に定義内容を登録 • 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成 • 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す function add(x, y) { return x + y; } 対象コード let a = add(1, 2); gctx[] add() の定義内容を登録 add() の定義内容を参照して 呼び出し呼びしコードを生成
  69. 69. ユーザ定義関数とグローバルコンテキスト • gctx: グローバルコンテキスト … プログラム全体の状況を保持する • 文字列定数、ユーザー定義関数 • 関数 • 関数定義があったら、gctx[] に定義内容を登録 • 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成 • 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す function add(x, y) { return x + y; } 対象コード let a = add(1, 2); gctx[] add() の定義内容を登録 add() の定義内容を参照して 呼び出し呼びしコードを生成 LLVM IR generateGlobalFunctions()で生成
  70. 70. ユーザ定義関数を含む場合のIR 生成 define i32 @main() { ; -- 実際の処理 ret i32 0; } ユーザ定義関数の定義 (グローバル関数) 文字列定数の内容 (グルーバル定数) 組み込み関数の定義 C言語標準ライブラリ関数の宣言 コンパイラで最終的に 生成される LLVM-IR generateMain()で生成 generateGlobalFunctions()で生成 generateGlobalString()で生成 generateBuiltin()で生成 generate()で生成
  71. 71. ミニコンパイラを作ってハマったこと:再帰呼び出し1 • 関数は呼び出される前に定義されている必要がある • 最初の実装では、関数の中身を全て確定してから登録していた function func2() { return 2; } function func1() { return func2() + 1; } let a = func1(); 対象ソース グローバル コンテキスト gctx[] 'func2'を登録 'func1'を登録 'func2'を参照 'func1'を参照
  72. 72. ミニコンパイラを作ってハマったこと:再帰呼び出し2 • 再帰呼び出しの場合は、関数の内容を組み立ている最中に自分自身 を呼び出す • 最初の実装では、まだ関数が登録されていないのでエラー • 対処として、最初に中身の無い状態で仮登録するように変更 function fib(x) { if (x <= 1) { return x; } else { return fib(x - 1) + fib(x - 2); } } fib(5); 対象ソース 'fib'を登録 'fib'を参照 →未定義エラー グローバル コンテキスト gctx[]
  73. 73. ミニコンパイラを作ってハマったこと:再帰呼び出し3 • 再帰呼び出しの場合は、関数の内容を組み立ている最中に自分自身 を呼び出す • 最初の実装では、まだ関数が登録されていないのでエラー • 対処として、最初に中身の無い状態で仮登録するように変更 function fib(x) { if (x <= 1) { return x; } else { return fib(x - 1) + fib(x - 2); } } fib(5); 対象ソース 'fib'を上書き登録 'fib'を参照 ※引数の情報を利用 'fib'を仮登録 ※引数に情報は保持 グローバル コンテキスト gctx[]
  74. 74. ミニコンパラーを作って苦労したこと:型の扱い • 最初は符号あり32ビット整数 (i32) だけを扱う予定で開始 • 実装を進めるにあたって、他の型も必要になった • 比較演算子、条件分岐のための 1ビット整数 (i1) … bool型に相当 • void … 戻り値が無い関数を扱うため • i8* … メッセージ用の文字列定数のアドレスのため。char*に相当 • 暗黙の型変換の例 • i32  i1 の変換 • (x != 0) を評価 • %t1bit = icmp ne i32 %t32bit, 0 • i1  i32 • LLVMの型の拡張命令 zext を使用 • %t32bit = zext i1 %t1bit to i32 説明はスキップ
  75. 75. ミニコンパラーを作って断念したこと:型の追加 • i32以外の型を増やしたい、けど… • JavaScript 自由すぎる • 関数の引数の型が決まっていない • 関数の戻り値の型も決まっていない • →コンパイラー泣かせ • 型宣言が欲しい • せめてアノテーションが欲しい • asm.js の謎のアノテーションの気持ちがわかった • var a = 0; // i32 • var b = 0.0; // f64 • arg1 = arg1 | 0; // 引数1はi32 • arg2 = +arg2; // 引数2はf64 説明はスキップ
  76. 76. まとめ • 言語処理系も、作ってみて初めて分かることが色々ある • 言語の仕様の意味すること … 変数/関数のスコープ • 言語の実装の厄介なところ … 再帰呼び出し • ちょっと複雑なプログラムでも、インクリメンタルに進めれば作れる • 適切に機能を小分けする。本当に単純なことから始める • 常に動かして結果を確認しながら、徐々に成長させる • 忘れていた目標を思い出そう • 2x年ぶりにコンパイラを作りたかったことを思い出した • 今回実際に動かすことができて、かなり興奮した
  77. 77. コンパイラに興味がわいた人には
  78. 78. Thank You! ご質問は? Node.jsでつくるNode.jsミニコンパイラ - もくじ https://qiita.com/massie_g/items/3ba1ba5d55499ee84b0b Node.jsでつくるNode.js - もくじ(インタープリター) https://qiita.com/massie_g/items/3ee11c105b4458686bc1
  79. 79. おまけ
  80. 80. MinRubyファミリー = 単純化ASTエコシステム • 単純化ASTを中間言語とすれば • Ruby  Node.js の変換、実行が可能 sample.rb MinRuby改 JSON 単純化AST mininode改 単純化AST 対処を加えれば実行可能 • 変数の事前宣言の制約 を緩める • 組み関数の違いを吸収 Node.js でつくる Node.js - Extra 1: ミニRubyの単純化Treeを実行する https://qiita.com/massie_g/items/3a4888168bb288965393
  81. 81. 単純化AST エコシステム 単純化ASTエコシステム→LLVMエコシステム MinRuby Node.js ミニインタープリター LLVM エコシステム Node.js ミニコンパイラー clang lli llc emscripten emscripten で WebAssembry に変換できるはず Node.js でつくる Node.js ミニコンパイラ - Extra01 : WebAssembry 化 https://qiita.com/massie_g/items/b5c449d4de8321a6bc68
  82. 82. emscripten で LLVM  WebAssembry • $ node mininode_compiler.js fizzbuzz_func.js • → generated.ll が生成される • $ emcc -o fizzbuzz.html generated.ll • → fizzbuzz.html が生成される • → fizzbuzz.js が生成される • → fizzbuzz.wasm が生成される • ブラウザで fizzbuzz.html を表示
  83. 83. テスト • テストは書いていなかった → ライオンに怒られる… • 最近になって追加 • 正直、内部のテストを後から書くのは厄介 • その代わり、大外を「End to End」でテストすることに • 実行結果(標準出力の内容)を比較 • Node.js で実行した結果 • ミニインタープリターで実行した結果 • コンパイルして作ったコードを、実行した結果 • テストはシェルスクリプト で実装 • Node.js でつくる Node.js ミニコンパイラ - 12 : いまさらテストを追加 • https://qiita.com/massie_g/items/8ae4b61c63716a05b1ed

×