Weitere ähnliche Inhalte Ähnlich wie Node.jsでつくるNode.js ミニインタープリター&コンパイラー (20) Node.jsでつくるNode.js ミニインタープリター&コンパイラー2. 自己紹介
• がねこまさし / @massie_g
• インフォコム(株)の技術調査チームのマネージャー
• WebRTC Meetup Tokyo スタッフ
• WebRTC Beginners Tokyo スタッフ
• 東京Node学園祭2017
• Node.js x Chrome headless で、お手軽WebRTC MCU
• https://bit.ly/2QmuECy
2
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の仕様をそのまま実現したい。ブートストラップが目標
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() を呼び出す処理になる
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の解釈
28. きっかけ(2) Turing Complete FM
• 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 の中間表現とビットコード
• Intermediate Representation(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() {
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. 例)足し算、他の四則演算
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. 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()
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
連番の場合
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を組立てる
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. コード生成 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 () で生成
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)
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. ループ→条件付きジャンプで実現
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
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()で生成
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年ぶりにコンパイラを作りたかったことを思い出した
• 今回実際に動かすことができて、かなり興奮した
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
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