Node.jsでつくるNode.js
ミニインタープリター&コンパイラー
Build Node.js mini-interpreter and mini-compiler
東京Node学園祭2018 / Nodefest 2018
2018.11.23
インフォコム株式会社
がねこまさし @massie_g
自己紹介
• がねこまさし / @massie_g
• インフォコム(株)の技術調査チームのマネージャー
• WebRTC Meetup Tokyo スタッフ
• WebRTC Beginners Tokyo スタッフ
• 東京Node学園祭2017
• Node.js x Chrome headless で、お手軽WebRTC MCU
• https://bit.ly/2QmuECy
2
今日のお話
• 対象者
• プログラミング言語のしくみに興味がある人
• ちょっと複雑なプログラムを作るのに困っている人
• 内容
• 1. ミニインタープリター編
• 2. ミニコンパイラー編
1. ミニインタープリター編
詳細は Qiitaの一連の記事をご覧ください
Node.jsでつくるNode.js - もくじ
https://qiita.com/massie_g/items/3ee11c105b4458686bc1
きっかけ(1) Ruby でつくる Ruby
インクリメンタルな開発ステップ
(作っては、動かす)
↓
簡単なプログラミング言語が作れる
「Ruby でつくる Ruby」を写経
• 1, 2章 … Rubyの超入門。変数、条件分岐、繰り返し
• 3章 … あとあと重要になる木構造の解説
• 4章 … MinRuby の実装開始。まずは四則演算から
• 5章 … 変数
• 6章 … 条件分岐
• 7章 … 組み込み関数
• 8章 … ユーザ定義関数
• 9章 … 配列、ハッシュ → ブートストラップ達成
写経し終えての感想
• MinRuby の仕様の範囲がとても良く考えられている
• 条件分岐やループ、データ構造など、最低限の機能を備えている
• 複雑な処理は外部モジュール(gem)に任せ、本体はコンパクトに
• ファイルアクセス、ソースコードのパースはgemで
• → 自分自身を実行可能に(ブートストラップ)
• 自分でも小さな言語処理系を作って見たい
• 「Ruby でつくる Ruby 」を真似すれば、できそう
• やるなら、なじみのあるNode.js / JavaScript で
Node.js ミニ(マム)インタプリターの目標
MinRuby Node.js ミニインタープリター
四則演算 ○ ○
変数 ○ ○ (※let 宣言必須に)
条件分岐 if - else if - else
繰り返し while while
組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用
ユーザー定義関数 ○ ○
配列 ○ ○
ハッシュ ○ ○ (連想配列)
ブートストラップ/セルフホスト ○ ○
MinRubyの仕様をそのまま実現したい。ブートストラップが目標
MinRubyの構成
Ruby
インタープリター
Interp.rb
evaluate()
パーサー gem
minruby.rb
simplify()
対象
ソースコード
.rb
ripper
AST: 抽象構文木S-AST: 単純化AST
読み込み実行
AST … abstract syntax tree
ソースコードを構文解析し
木構造で表現したもの
(不要な情報は省略される)
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
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'
}
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
パーサーモジュールの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() を呼び出す処理になる
インタープリターの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
インタープリターでインタープリターを作る場合、その言語の機能をそのまま使える
紆余曲折をへて、ミニインタープリター完成
• × パーサーを全部作ってから、インタープリターを全部作る
• ○ 各ステップごとに、パーサー→インタプリターで実行
• 定数、四則演算
• 変数、条件分岐、繰り返し、
• 組み込み関数、ユーザ定義関数、リターン処理
• 配列、ハッシュ(連想配列)
• 詳細は Qiitaの一連の記事をご覧ください
• Node.jsでつくるNode.js - もくじ
• https://qiita.com/massie_g/items/3ee11c105b4458686bc1
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
インタープリターで実行
ブートストラップ:インタープリターでインタープリターを実行
多段ロケット
Node.js ミニインタープリターを作って分かったこと
• MinRubyの設計と進め方が、とても良い
• やること/やらないことの切り分け、ステップの刻み方
• 中間表現の単純化ASTが良い指針
• Ruby と JavaScript の違い
• Ruby … 最後の評価値が、関数の戻り値になる (return は省略可能)
• JavaScript … 値を返すには、明示的に「return 値」が必要
• MinRubyでは(おそらく意図的に)return をサポートしていない
• ミニNode.js では明示的な return 文に対応 → 思ったより厄介
• 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
Node.js ミニインタープリターを作って分かったこと(2)
• 1段目は、普通にデバッガでデバッグできる
• 2段目(ブートストラップ)になると、デバッガは使えない
Node.js
インタープリター
mininode.js
対象
ソースコード
fizzbuzz.js
インタープリター
mininode.js
• 何か自分でデバッガ的なものを作れる? → 無理
• print (console.log)でのデバッグ?
• ログが1段目のものか、2段目のものか分からなくなる
• →ほぼ同じで、メッセージが異なる2つのソースを使った
デバッガでステップ実行可 ステップ実行できない
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
作って分かったこと(3)
書籍では語られないMinRubyと仲間たちの性質
• 変数定義 … lenv[]というハッシュ(連想配列)に格納
• lenv … おそらく、local environment の意味
• 関数定義 … genv[]というハッシュ(連想配列)に格納
• genv … おそらく、global environment の意味
この実装により、素のRuby/Node.jsとは異なる性質がある
→ ※これがブートストラップ時のバグにつながった
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
対象ソースコード
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;
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;
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 は同じ変数として扱われる
※重複定義でエラー
グローバル変数や、ブロックスコープをきちんと扱うには
特別な配慮が必要なことを実感
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の解釈
2. コンパイラー編
詳細は Qiitaの一連の記事をご覧ください
Node.jsでつくるNode.jsミニコンパイラ - もくじ
https://qiita.com/massie_g/items/3ba1ba5d55499ee84b0b
きっかけ(2) Turing Complete FM
• Turing Complete FM https://turingcomplete.fm
• 言語やOSを作る話など、低レイヤーの話題がいっぱいのポッドキャスト
• オーナーのRuiさん自身がCコンパイラ(8CC, 9CC) を作った話も
• 聞きながら、2x年前の目標を思い出す
• 「コンパイラー作って見たい」→ 当時は挫折
• ミニインタープリターを作った今なら、できるかも
• コンパイラーのややこしい部分は、自分でやるのは諦める
• パーサーは外部モジュールを使う
• バイナリの生成は、LLVMにお任せ
Node.js ミニ(マム)コンパイラーの目標
MinRuby Node.js
ミニインタープリター
Node.js
ミニコンパイラー
型 整数、実数、文字列, … 整数、実数、文字列, … 32ビット符号あり整数のみ
四則演算 ○ ○ ○
変数 ○ ○
(※let 宣言必須に)
○
(※let 宣言必須に)
条件分岐 if - else if - else if - else
繰り返し while while while
組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用 画面出力(標準出力)用
ユーザー定義関数 ○(再帰呼び出しも可) ○(再帰呼び出しも可) ○ (再帰呼び出しも可)
配列 ○ ○ ×
ハッシュ ○ ○ (連想配列) ×
セフルホスト ○ ○ ×(ただしミニインタープ
リターから実行可能に)
整数のみ対応。関数を使って、FizzBuzzとフィボナッチ数列を目標に
LLVMとは
• llvm.org より
• LLVMプロジェクトは、モジュール化された再利用可能なコンパイラ
およびツールチェーン技術の集まりです
• もともとは Low Level Virtual Machine の略語
• 現在は「LLVM」が正式名称
• 最近の言語系ではよく利用さている
• Clang, Swift, Rust など
• ASM.jsやWebAssemblyを生成するEmscriptenも
LLVM のインストール
方法は3通り
• (A) ソースコードからビルドする
• (B) パッケージ管理ソフトを使ってインストール
• Mac OS Xの場合はhomebrewを使う
• (C) ビルド済みのバイナリ(Pre-build)をダウンロードする
• LLVM Download Page
• http://releases.llvm.org/download.html
LLVM の中間表現とビットコード
• Intermediate Representation(IR) … テキストの中間表現
• ビットコード … IRをバイナリにしたもの
• Javaのバイトコードのようなもの?
• 相互に変換可能
ソースコード コンパイラー
LLVM-IR
LLVM
Bitcode
llc
オブジェクト
ファイル
リンカー
実行
モジュール
LLVMのパイプライン
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 を生成
• 最低限動く状態まで付加情報を削って理解する
• 詳細は、リファレンスの該当箇所を確認する
例)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 … 以下省略
例)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を実行
例)足し算、他の四則演算
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(符号無し)
例)足し算、他の四則演算
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 など
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()
ミニコンパイラ実装の進め方
• ステップ・バイ・ステップで取り組む
• 「コンパイル→実行」できる範囲を増やしていく
• 機能追加のステップ
• 定数の扱い
• 四則演算
• 変数
• 条件分岐
• ループ
• ユーザ定義関数
ミニコンパイラ実装の進め方
• ステップ・バイ・ステップで取り組む
• 「コンパイル→実行」できる範囲を増やしていく
• 機能追加のステップ
• 定数の扱い
• 四則演算
• 変数
• 条件分岐
• ループ
• ユーザ定義関数
例として
このステップを説明
ステップ毎にやること
• コンパイル対象となるNode.jsのソースコードを準備
• 生成結果となるLLVM IR を調査
• コンパラーに実装を追加
• generate() に実装を追加
• コンパイラーを動かしてIRコード生成を確認
• 実行して動作を確認
• lli でIR を実行
• llc & リンカーでバイナリを生成
ステップ毎にやること
• コンパイル対象となるNode.jsのソースコードを準備
• 生成結果となるLLVM IR を調査
• コンパラーに実装を追加
• generate() に実装を追加
• コンパイラーを動かしてIRコード生成を確認
• 実行して動作を確認
• lli でIR を実行
• llc & リンカーでバイナリを生成
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;
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
スタック上に変数領域を確保
※関数終了時に解放される
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 … 変数への格納
補足:スタック領域
• LIFO の構造を持っている
• LLVM IR や多くのプログラミング言語で、
関数呼び出し時に利用される
• 関数から戻る場所、引数を格納
• 関数内の一時的な記憶領域として利用
• 関数から戻るときには、領域は解放される
戻り先
引数1
引数2
一時変数1
一時変数2
古い
新しい
※時間の都合で、発表時はスキップします
%0
%1
%3
%4
LLVM-IR
連番の場合
ステップ毎にやること
• コンパイル対象となるNode.jsのソースコードを準備
• 生成結果となるLLVM IR を調査
• コンパラーに実装を追加
• generate() に実装を追加
• コンパイラーを動かしてIRコード生成を確認
• 実行して動作を確認
• lli でIR を実行
• llc & リンカーでバイナリを生成
コード生成 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'
コード生成 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'
コード生成 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
右辺の処理
コード生成 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
コード生成 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
変数とローカルコンテキスト
• 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
コンパイラー内部
変数とローカルコンテキスト
• 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[] に覚える
変数とローカルコンテキスト
• 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[] から
取り出して利用
変数とローカルコンテキスト
• 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[] から
取り出して利用
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 () で生成
各ステップの中身
• コンパイル対象となるNode.jsのソースコードを準備
• 生成結果となるLLVM IR を調査
• コンパラーに実装を追加
• generate() に実装を追加
• コンパイラーを動かしてIRコード生成を確認
• 実行して動作を確認
• lli でIR を実行
• llc & リンカーでバイナリを生成
コンパイル、実行、バイナリ生成
• コンパイル
• $ 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のデモ
組み込み関数
• 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()で生成
組み込み関数を利用した場合の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)
他の機能と LLVM-IR
• (条件分岐)
• (ループ)
条件分岐→条件付きジャンプで実現
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);
}
説明はスキップ
ループ→条件付きジャンプで実現
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
ミニコンパイラ実装の進め方
• ステップ・バイ・ステップで取り組む
• 「コンパイル→実行」できる範囲を増やしていく
• 機能追加のステップ
• 定数の扱い
• 四則演算
• 変数
• 条件分岐
• ループ
• ユーザ定義関数
例として
このステップを説明
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
ユーザ定義関数とグローバルコンテキスト
• gctx: グローバルコンテキスト … プログラム全体の状況を保持する
• 文字列定数、ユーザー定義関数
• 関数
• 関数定義があったら、gctx[] に定義内容を登録
• 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成
• 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す
function add(x, y) {
return x + y;
}
対象コード
gctx[]
add() の定義内容を登録
ユーザ定義関数とグローバルコンテキスト
• gctx: グローバルコンテキスト … プログラム全体の状況を保持する
• 文字列定数、ユーザー定義関数
• 関数
• 関数定義があったら、gctx[] に定義内容を登録
• 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成
• 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す
function add(x, y) {
return x + y;
}
対象コード
let a = add(1, 2);
gctx[]
add() の定義内容を登録
add() の定義内容を参照して
呼び出し呼びしコードを生成
ユーザ定義関数とグローバルコンテキスト
• gctx: グローバルコンテキスト … プログラム全体の状況を保持する
• 文字列定数、ユーザー定義関数
• 関数
• 関数定義があったら、gctx[] に定義内容を登録
• 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成
• 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す
function add(x, y) {
return x + y;
}
対象コード
let a = add(1, 2);
gctx[]
add() の定義内容を登録
add() の定義内容を参照して
呼び出し呼びしコードを生成
LLVM IR
generateGlobalFunctions()で生成
ユーザ定義関数を含む場合のIR 生成
define i32 @main() {
; -- 実際の処理
ret i32 0;
}
ユーザ定義関数の定義
(グローバル関数)
文字列定数の内容
(グルーバル定数)
組み込み関数の定義
C言語標準ライブラリ関数の宣言
コンパイラで最終的に
生成される LLVM-IR
generateMain()で生成
generateGlobalFunctions()で生成
generateGlobalString()で生成
generateBuiltin()で生成
generate()で生成
ミニコンパイラを作ってハマったこと:再帰呼び出し1
• 関数は呼び出される前に定義されている必要がある
• 最初の実装では、関数の中身を全て確定してから登録していた
function func2() {
return 2;
}
function func1() {
return func2() + 1;
}
let a = func1();
対象ソース
グローバル
コンテキスト
gctx[]
'func2'を登録
'func1'を登録
'func2'を参照
'func1'を参照
ミニコンパイラを作ってハマったこと:再帰呼び出し2
• 再帰呼び出しの場合は、関数の内容を組み立ている最中に自分自身
を呼び出す
• 最初の実装では、まだ関数が登録されていないのでエラー
• 対処として、最初に中身の無い状態で仮登録するように変更
function fib(x) {
if (x <= 1) {
return x;
}
else {
return fib(x - 1) + fib(x - 2);
}
}
fib(5);
対象ソース
'fib'を登録
'fib'を参照
→未定義エラー
グローバル
コンテキスト
gctx[]
ミニコンパイラを作ってハマったこと:再帰呼び出し3
• 再帰呼び出しの場合は、関数の内容を組み立ている最中に自分自身
を呼び出す
• 最初の実装では、まだ関数が登録されていないのでエラー
• 対処として、最初に中身の無い状態で仮登録するように変更
function fib(x) {
if (x <= 1) {
return x;
}
else {
return fib(x - 1) + fib(x - 2);
}
}
fib(5);
対象ソース
'fib'を上書き登録
'fib'を参照
※引数の情報を利用
'fib'を仮登録
※引数に情報は保持
グローバル
コンテキスト
gctx[]
ミニコンパラーを作って苦労したこと:型の扱い
• 最初は符号あり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
説明はスキップ
ミニコンパラーを作って断念したこと:型の追加
• i32以外の型を増やしたい、けど…
• JavaScript 自由すぎる
• 関数の引数の型が決まっていない
• 関数の戻り値の型も決まっていない
• →コンパイラー泣かせ
• 型宣言が欲しい
• せめてアノテーションが欲しい
• asm.js の謎のアノテーションの気持ちがわかった
• var a = 0; // i32
• var b = 0.0; // f64
• arg1 = arg1 | 0; // 引数1はi32
• arg2 = +arg2; // 引数2はf64
説明はスキップ
まとめ
• 言語処理系も、作ってみて初めて分かることが色々ある
• 言語の仕様の意味すること … 変数/関数のスコープ
• 言語の実装の厄介なところ … 再帰呼び出し
• ちょっと複雑なプログラムでも、インクリメンタルに進めれば作れる
• 適切に機能を小分けする。本当に単純なことから始める
• 常に動かして結果を確認しながら、徐々に成長させる
• 忘れていた目標を思い出そう
• 2x年ぶりにコンパイラを作りたかったことを思い出した
• 今回実際に動かすことができて、かなり興奮した
コンパイラに興味がわいた人には
Thank You!
ご質問は?
Node.jsでつくるNode.jsミニコンパイラ - もくじ
https://qiita.com/massie_g/items/3ba1ba5d55499ee84b0b
Node.jsでつくるNode.js - もくじ(インタープリター)
https://qiita.com/massie_g/items/3ee11c105b4458686bc1
おまけ
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
単純化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
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 を表示
テスト
• テストは書いていなかった → ライオンに怒られる…
• 最近になって追加
• 正直、内部のテストを後から書くのは厄介
• その代わり、大外を「End to End」でテストすることに
• 実行結果(標準出力の内容)を比較
• Node.js で実行した結果
• ミニインタープリターで実行した結果
• コンパイルして作ったコードを、実行した結果
• テストはシェルスクリプト で実装
• Node.js でつくる Node.js ミニコンパイラ - 12 : いまさらテストを追加
• https://qiita.com/massie_g/items/8ae4b61c63716a05b1ed

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

  • 1.
    Node.jsでつくるNode.js ミニインタープリター&コンパイラー Build Node.js mini-interpreterand mini-compiler 東京Node学園祭2018 / Nodefest 2018 2018.11.23 インフォコム株式会社 がねこまさし @massie_g
  • 2.
    自己紹介 • がねこまさし /@massie_g • インフォコム(株)の技術調査チームのマネージャー • WebRTC Meetup Tokyo スタッフ • WebRTC Beginners Tokyo スタッフ • 東京Node学園祭2017 • Node.js x Chrome headless で、お手軽WebRTC MCU • https://bit.ly/2QmuECy 2
  • 3.
    今日のお話 • 対象者 • プログラミング言語のしくみに興味がある人 •ちょっと複雑なプログラムを作るのに困っている人 • 内容 • 1. ミニインタープリター編 • 2. ミニコンパイラー編
  • 4.
  • 5.
    きっかけ(1) Ruby でつくるRuby インクリメンタルな開発ステップ (作っては、動かす) ↓ 簡単なプログラミング言語が作れる
  • 6.
    「Ruby でつくる Ruby」を写経 •1, 2章 … Rubyの超入門。変数、条件分岐、繰り返し • 3章 … あとあと重要になる木構造の解説 • 4章 … MinRuby の実装開始。まずは四則演算から • 5章 … 変数 • 6章 … 条件分岐 • 7章 … 組み込み関数 • 8章 … ユーザ定義関数 • 9章 … 配列、ハッシュ → ブートストラップ達成
  • 7.
    写経し終えての感想 • MinRuby の仕様の範囲がとても良く考えられている •条件分岐やループ、データ構造など、最低限の機能を備えている • 複雑な処理は外部モジュール(gem)に任せ、本体はコンパクトに • ファイルアクセス、ソースコードのパースはgemで • → 自分自身を実行可能に(ブートストラップ) • 自分でも小さな言語処理系を作って見たい • 「Ruby でつくる Ruby 」を真似すれば、できそう • やるなら、なじみのあるNode.js / JavaScript で
  • 8.
    Node.js ミニ(マム)インタプリターの目標 MinRuby Node.jsミニインタープリター 四則演算 ○ ○ 変数 ○ ○ (※let 宣言必須に) 条件分岐 if - else if - else 繰り返し while while 組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用 ユーザー定義関数 ○ ○ 配列 ○ ○ ハッシュ ○ ○ (連想配列) ブートストラップ/セルフホスト ○ ○ MinRubyの仕様をそのまま実現したい。ブートストラップが目標
  • 9.
    MinRubyの構成 Ruby インタープリター Interp.rb evaluate() パーサー gem minruby.rb simplify() 対象 ソースコード .rb ripper AST: 抽象構文木S-AST:単純化AST 読み込み実行 AST … abstract syntax tree ソースコードを構文解析し 木構造で表現したもの (不要な情報は省略される)
  • 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.
    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.
    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.
    パーサーモジュールの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.
    インタープリターの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.
    紆余曲折をへて、ミニインタープリター完成 • × パーサーを全部作ってから、インタープリターを全部作る •○ 各ステップごとに、パーサー→インタプリターで実行 • 定数、四則演算 • 変数、条件分岐、繰り返し、 • 組み込み関数、ユーザ定義関数、リターン処理 • 配列、ハッシュ(連想配列) • 詳細は Qiitaの一連の記事をご覧ください • Node.jsでつくるNode.js - もくじ • https://qiita.com/massie_g/items/3ee11c105b4458686bc1
  • 16.
  • 17.
    Node.js ミニインタープリターを作って分かったこと • MinRubyの設計と進め方が、とても良い •やること/やらないことの切り分け、ステップの刻み方 • 中間表現の単純化ASTが良い指針 • Ruby と JavaScript の違い • Ruby … 最後の評価値が、関数の戻り値になる (return は省略可能) • JavaScript … 値を返すには、明示的に「return 値」が必要 • MinRubyでは(おそらく意図的に)return をサポートしていない • ミニNode.js では明示的な return 文に対応 → 思ったより厄介
  • 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.
    Node.js ミニインタープリターを作って分かったこと(2) • 1段目は、普通にデバッガでデバッグできる •2段目(ブートストラップ)になると、デバッガは使えない Node.js インタープリター mininode.js 対象 ソースコード fizzbuzz.js インタープリター mininode.js • 何か自分でデバッガ的なものを作れる? → 無理 • print (console.log)でのデバッグ? • ログが1段目のものか、2段目のものか分からなくなる • →ほぼ同じで、メッセージが異なる2つのソースを使った デバッガでステップ実行可 ステップ実行できない
  • 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.
    作って分かったこと(3) 書籍では語られないMinRubyと仲間たちの性質 • 変数定義 …lenv[]というハッシュ(連想配列)に格納 • lenv … おそらく、local environment の意味 • 関数定義 … genv[]というハッシュ(連想配列)に格納 • genv … おそらく、global environment の意味 この実装により、素のRuby/Node.jsとは異なる性質がある → ※これがブートストラップ時のバグにつながった
  • 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.
    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.
    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.
    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.
    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.
  • 28.
    きっかけ(2) Turing CompleteFM • Turing Complete FM https://turingcomplete.fm • 言語やOSを作る話など、低レイヤーの話題がいっぱいのポッドキャスト • オーナーのRuiさん自身がCコンパイラ(8CC, 9CC) を作った話も • 聞きながら、2x年前の目標を思い出す • 「コンパイラー作って見たい」→ 当時は挫折 • ミニインタープリターを作った今なら、できるかも • コンパイラーのややこしい部分は、自分でやるのは諦める • パーサーは外部モジュールを使う • バイナリの生成は、LLVMにお任せ
  • 29.
    Node.js ミニ(マム)コンパイラーの目標 MinRuby Node.js ミニインタープリター Node.js ミニコンパイラー 型整数、実数、文字列, … 整数、実数、文字列, … 32ビット符号あり整数のみ 四則演算 ○ ○ ○ 変数 ○ ○ (※let 宣言必須に) ○ (※let 宣言必須に) 条件分岐 if - else if - else if - else 繰り返し while while while 組み込み関数 画面出力(標準出力)用 画面出力(標準出力)用 画面出力(標準出力)用 ユーザー定義関数 ○(再帰呼び出しも可) ○(再帰呼び出しも可) ○ (再帰呼び出しも可) 配列 ○ ○ × ハッシュ ○ ○ (連想配列) × セフルホスト ○ ○ ×(ただしミニインタープ リターから実行可能に) 整数のみ対応。関数を使って、FizzBuzzとフィボナッチ数列を目標に
  • 30.
    LLVMとは • llvm.org より •LLVMプロジェクトは、モジュール化された再利用可能なコンパイラ およびツールチェーン技術の集まりです • もともとは Low Level Virtual Machine の略語 • 現在は「LLVM」が正式名称 • 最近の言語系ではよく利用さている • Clang, Swift, Rust など • ASM.jsやWebAssemblyを生成するEmscriptenも
  • 31.
    LLVM のインストール 方法は3通り • (A)ソースコードからビルドする • (B) パッケージ管理ソフトを使ってインストール • Mac OS Xの場合はhomebrewを使う • (C) ビルド済みのバイナリ(Pre-build)をダウンロードする • LLVM Download Page • http://releases.llvm.org/download.html
  • 32.
    LLVM の中間表現とビットコード • IntermediateRepresentation(IR) … テキストの中間表現 • ビットコード … IRをバイナリにしたもの • Javaのバイトコードのようなもの? • 相互に変換可能 ソースコード コンパイラー LLVM-IR LLVM Bitcode llc オブジェクト ファイル リンカー 実行 モジュール LLVMのパイプライン
  • 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.
    例)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.
    例)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.
    例)足し算、他の四則演算 int main() { return1 + 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.
    例)足し算、他の四則演算 int main() { return1 + 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.
    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.
    ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく •機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数
  • 40.
    ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく •機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数 例として このステップを説明
  • 41.
    ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVMIR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  • 42.
    ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVMIR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  • 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.
    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.
    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.
    補足:スタック領域 • LIFO の構造を持っている •LLVM IR や多くのプログラミング言語で、 関数呼び出し時に利用される • 関数から戻る場所、引数を格納 • 関数内の一時的な記憶領域として利用 • 関数から戻るときには、領域は解放される 戻り先 引数1 引数2 一時変数1 一時変数2 古い 新しい ※時間の都合で、発表時はスキップします %0 %1 %3 %4 LLVM-IR 連番の場合
  • 47.
    ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVMIR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  • 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.
    コード生成 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.
    コード生成 generate()の動作:変数 • 各行ごとに、処理内容のIRを組立てる leta = 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.
    コード生成 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.
    コード生成 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.
    変数とローカルコンテキスト • 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.
    変数とローカルコンテキスト • 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.
    変数とローカルコンテキスト • 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.
    変数とローカルコンテキスト • 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.
    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.
    各ステップの中身 • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVMIR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  • 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.
    組み込み関数 • 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.
    組み込み関数を利用した場合の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.
  • 63.
    条件分岐→条件付きジャンプで実現 int main() { inta = 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.
    ループ→条件付きジャンプで実現 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.
    ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく •機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数 例として このステップを説明
  • 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.
    ユーザ定義関数とグローバルコンテキスト • gctx: グローバルコンテキスト… プログラム全体の状況を保持する • 文字列定数、ユーザー定義関数 • 関数 • 関数定義があったら、gctx[] に定義内容を登録 • 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成 • 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す function add(x, y) { return x + y; } 対象コード gctx[] add() の定義内容を登録
  • 68.
    ユーザ定義関数とグローバルコンテキスト • gctx: グローバルコンテキスト… プログラム全体の状況を保持する • 文字列定数、ユーザー定義関数 • 関数 • 関数定義があったら、gctx[] に定義内容を登録 • 関数呼び出し時は、gctx[] を参照して呼び出しコードを生成 • 最後に、gctx[]に登録された関数定義をまとめてIRに書き出す function add(x, y) { return x + y; } 対象コード let a = add(1, 2); gctx[] add() の定義内容を登録 add() の定義内容を参照して 呼び出し呼びしコードを生成
  • 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.
    ユーザ定義関数を含む場合のIR 生成 define i32@main() { ; -- 実際の処理 ret i32 0; } ユーザ定義関数の定義 (グローバル関数) 文字列定数の内容 (グルーバル定数) 組み込み関数の定義 C言語標準ライブラリ関数の宣言 コンパイラで最終的に 生成される LLVM-IR generateMain()で生成 generateGlobalFunctions()で生成 generateGlobalString()で生成 generateBuiltin()で生成 generate()で生成
  • 71.
    ミニコンパイラを作ってハマったこと:再帰呼び出し1 • 関数は呼び出される前に定義されている必要がある • 最初の実装では、関数の中身を全て確定してから登録していた functionfunc2() { return 2; } function func1() { return func2() + 1; } let a = func1(); 対象ソース グローバル コンテキスト gctx[] 'func2'を登録 'func1'を登録 'func2'を参照 'func1'を参照
  • 72.
    ミニコンパイラを作ってハマったこと:再帰呼び出し2 • 再帰呼び出しの場合は、関数の内容を組み立ている最中に自分自身 を呼び出す • 最初の実装では、まだ関数が登録されていないのでエラー •対処として、最初に中身の無い状態で仮登録するように変更 function fib(x) { if (x <= 1) { return x; } else { return fib(x - 1) + fib(x - 2); } } fib(5); 対象ソース 'fib'を登録 'fib'を参照 →未定義エラー グローバル コンテキスト gctx[]
  • 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.
    ミニコンパラーを作って苦労したこと:型の扱い • 最初は符号あり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.
    ミニコンパラーを作って断念したこと:型の追加 • i32以外の型を増やしたい、けど… • JavaScript自由すぎる • 関数の引数の型が決まっていない • 関数の戻り値の型も決まっていない • →コンパイラー泣かせ • 型宣言が欲しい • せめてアノテーションが欲しい • asm.js の謎のアノテーションの気持ちがわかった • var a = 0; // i32 • var b = 0.0; // f64 • arg1 = arg1 | 0; // 引数1はi32 • arg2 = +arg2; // 引数2はf64 説明はスキップ
  • 76.
    まとめ • 言語処理系も、作ってみて初めて分かることが色々ある • 言語の仕様の意味すること… 変数/関数のスコープ • 言語の実装の厄介なところ … 再帰呼び出し • ちょっと複雑なプログラムでも、インクリメンタルに進めれば作れる • 適切に機能を小分けする。本当に単純なことから始める • 常に動かして結果を確認しながら、徐々に成長させる • 忘れていた目標を思い出そう • 2x年ぶりにコンパイラを作りたかったことを思い出した • 今回実際に動かすことができて、かなり興奮した
  • 77.
  • 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.
  • 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.
    単純化AST エコシステム 単純化ASTエコシステム→LLVMエコシステム MinRuby Node.js ミニインタープリター LLVM エコシステム Node.js ミニコンパイラー clanglli llc emscripten emscripten で WebAssembry に変換できるはず Node.js でつくる Node.js ミニコンパイラ - Extra01 : WebAssembry 化 https://qiita.com/massie_g/items/b5c449d4de8321a6bc68
  • 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.
    テスト • テストは書いていなかった →ライオンに怒られる… • 最近になって追加 • 正直、内部のテストを後から書くのは厄介 • その代わり、大外を「End to End」でテストすることに • 実行結果(標準出力の内容)を比較 • Node.js で実行した結果 • ミニインタープリターで実行した結果 • コンパイルして作ったコードを、実行した結果 • テストはシェルスクリプト で実装 • Node.js でつくる Node.js ミニコンパイラ - 12 : いまさらテストを追加 • https://qiita.com/massie_g/items/8ae4b61c63716a05b1ed