Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

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

352 views

Published on

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

Published in: Technology
  • Be the first to comment

  • Be the first to like this

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

×