SlideShare ist ein Scribd-Unternehmen logo
1 von 83
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

Weitere ähnliche Inhalte

Was ist angesagt?

組み込みでこそC++を使う10の理由
組み込みでこそC++を使う10の理由組み込みでこそC++を使う10の理由
組み込みでこそC++を使う10の理由
kikairoya
 
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
MITSUNARI Shigeo
 
C++11概要 ライブラリ編
C++11概要 ライブラリ編C++11概要 ライブラリ編
C++11概要 ライブラリ編
egtra
 
組み込み関数(intrinsic)によるSIMD入門
組み込み関数(intrinsic)によるSIMD入門組み込み関数(intrinsic)によるSIMD入門
組み込み関数(intrinsic)によるSIMD入門
Norishige Fukushima
 

Was ist angesagt? (20)

Emcjp item21
Emcjp item21Emcjp item21
Emcjp item21
 
Boost Tour 1.50.0 All
Boost Tour 1.50.0 AllBoost Tour 1.50.0 All
Boost Tour 1.50.0 All
 
ゲーム開発者のための C++11/C++14
ゲーム開発者のための C++11/C++14ゲーム開発者のための C++11/C++14
ゲーム開発者のための C++11/C++14
 
新しい並列for構文のご提案
新しい並列for構文のご提案新しい並列for構文のご提案
新しい並列for構文のご提案
 
GPUが100倍速いという神話をぶち殺せたらいいな ver.2013
GPUが100倍速いという神話をぶち殺せたらいいな ver.2013GPUが100倍速いという神話をぶち殺せたらいいな ver.2013
GPUが100倍速いという神話をぶち殺せたらいいな ver.2013
 
Popcntによるハミング距離計算
Popcntによるハミング距離計算Popcntによるハミング距離計算
Popcntによるハミング距離計算
 
Map
MapMap
Map
 
組み込みでこそC++を使う10の理由
組み込みでこそC++を使う10の理由組み込みでこそC++を使う10の理由
組み込みでこそC++を使う10の理由
 
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
Xeon PhiとN体計算コーディング x86/x64最適化勉強会6(@k_nitadoriさんの代理アップ)
 
C++でのゲームプログラミングをしたときのお話 札幌C++勉強会 #4 〜スタートゲームプログラミング〜
C++でのゲームプログラミングをしたときのお話 札幌C++勉強会 #4 〜スタートゲームプログラミング〜C++でのゲームプログラミングをしたときのお話 札幌C++勉強会 #4 〜スタートゲームプログラミング〜
C++でのゲームプログラミングをしたときのお話 札幌C++勉強会 #4 〜スタートゲームプログラミング〜
 
SSE4.2の文字列処理命令の紹介
SSE4.2の文字列処理命令の紹介SSE4.2の文字列処理命令の紹介
SSE4.2の文字列処理命令の紹介
 
C++11概要 ライブラリ編
C++11概要 ライブラリ編C++11概要 ライブラリ編
C++11概要 ライブラリ編
 
TensorFlow XLA 「XLAとは、から、最近の利用事例について」
TensorFlow XLA 「XLAとは、から、最近の利用事例について」TensorFlow XLA 「XLAとは、から、最近の利用事例について」
TensorFlow XLA 「XLAとは、から、最近の利用事例について」
 
C++14 Overview
C++14 OverviewC++14 Overview
C++14 Overview
 
Visual C++で使えるC++11
Visual C++で使えるC++11Visual C++で使えるC++11
Visual C++で使えるC++11
 
Unityで覚えるC#
Unityで覚えるC#Unityで覚えるC#
Unityで覚えるC#
 
C++ マルチスレッドプログラミング
C++ マルチスレッドプログラミングC++ マルチスレッドプログラミング
C++ マルチスレッドプログラミング
 
Rubyの拡張をCrystalで書いてみる
Rubyの拡張をCrystalで書いてみるRubyの拡張をCrystalで書いてみる
Rubyの拡張をCrystalで書いてみる
 
組み込み関数(intrinsic)によるSIMD入門
組み込み関数(intrinsic)によるSIMD入門組み込み関数(intrinsic)によるSIMD入門
組み込み関数(intrinsic)によるSIMD入門
 
C++0x 言語の未来を語る
C++0x 言語の未来を語るC++0x 言語の未来を語る
C++0x 言語の未来を語る
 

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

エキ Py 読書会02 2010/9/7
エキ Py 読書会02 2010/9/7エキ Py 読書会02 2010/9/7
エキ Py 読書会02 2010/9/7
Tetsuya Morimoto
 
ぱっと見でわかるC++11
ぱっと見でわかるC++11ぱっと見でわかるC++11
ぱっと見でわかるC++11
えぴ 福田
 
Replace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JPReplace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JP
Akira Takahashi
 
関東GPGPU勉強会 LLVM meets GPU
関東GPGPU勉強会 LLVM meets GPU関東GPGPU勉強会 LLVM meets GPU
関東GPGPU勉強会 LLVM meets GPU
Takuro Iizuka
 
Fork/Join Framework。そしてLambdaへ。
Fork/Join Framework。そしてLambdaへ。Fork/Join Framework。そしてLambdaへ。
Fork/Join Framework。そしてLambdaへ。
Yuichi Sakuraba
 
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
Akira Inoue
 
T69 c++cli ネイティブライブラリラッピング入門
T69 c++cli ネイティブライブラリラッピング入門T69 c++cli ネイティブライブラリラッピング入門
T69 c++cli ネイティブライブラリラッピング入門
伸男 伊藤
 

Ähnlich wie Node.jsでつくるNode.js ミニインタープリター&コンパイラー (20)

asm.js x emscripten: The foundation of the next level Web games
asm.js x emscripten: The foundation of the next level Web gamesasm.js x emscripten: The foundation of the next level Web games
asm.js x emscripten: The foundation of the next level Web games
 
たのしい関数型
たのしい関数型たのしい関数型
たのしい関数型
 
Boost Fusion Library
Boost Fusion LibraryBoost Fusion Library
Boost Fusion Library
 
関数型言語&形式的手法セミナー(3)
関数型言語&形式的手法セミナー(3)関数型言語&形式的手法セミナー(3)
関数型言語&形式的手法セミナー(3)
 
つくってあそぼ ラムダ計算インタプリタ
つくってあそぼ ラムダ計算インタプリタつくってあそぼ ラムダ計算インタプリタ
つくってあそぼ ラムダ計算インタプリタ
 
エキ Py 読書会02 2010/9/7
エキ Py 読書会02 2010/9/7エキ Py 読書会02 2010/9/7
エキ Py 読書会02 2010/9/7
 
ぱっと見でわかるC++11
ぱっと見でわかるC++11ぱっと見でわかるC++11
ぱっと見でわかるC++11
 
Replace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JPReplace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JP
 
関東GPGPU勉強会 LLVM meets GPU
関東GPGPU勉強会 LLVM meets GPU関東GPGPU勉強会 LLVM meets GPU
関東GPGPU勉強会 LLVM meets GPU
 
エキ Py 読書会02 2章後半
エキ Py 読書会02 2章後半エキ Py 読書会02 2章後半
エキ Py 読書会02 2章後半
 
Prosym2012
Prosym2012Prosym2012
Prosym2012
 
言語処理系入門€10
言語処理系入門€10言語処理系入門€10
言語処理系入門€10
 
中3女子でもわかる constexpr
中3女子でもわかる constexpr中3女子でもわかる constexpr
中3女子でもわかる constexpr
 
Fork/Join Framework。そしてLambdaへ。
Fork/Join Framework。そしてLambdaへ。Fork/Join Framework。そしてLambdaへ。
Fork/Join Framework。そしてLambdaへ。
 
Python physicalcomputing
Python physicalcomputingPython physicalcomputing
Python physicalcomputing
 
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
TypeScript ファーストステップ (Rev.2) ~ Any browser. Any host. Any OS. Open Source. ~
 
思ったほど怖くない! Haskell on JVM 超入門 #jjug_ccc #ccc_l8
思ったほど怖くない! Haskell on JVM 超入門 #jjug_ccc #ccc_l8思ったほど怖くない! Haskell on JVM 超入門 #jjug_ccc #ccc_l8
思ったほど怖くない! Haskell on JVM 超入門 #jjug_ccc #ccc_l8
 
T69 c++cli ネイティブライブラリラッピング入門
T69 c++cli ネイティブライブラリラッピング入門T69 c++cli ネイティブライブラリラッピング入門
T69 c++cli ネイティブライブラリラッピング入門
 
ラズパイでデバイスドライバを作ってみた。
ラズパイでデバイスドライバを作ってみた。ラズパイでデバイスドライバを作ってみた。
ラズパイでデバイスドライバを作ってみた。
 
Lisp Tutorial for Pythonista : Day 3
Lisp Tutorial for Pythonista : Day 3Lisp Tutorial for Pythonista : Day 3
Lisp Tutorial for Pythonista : Day 3
 

Mehr von mganeko

WebRTC multistream
WebRTC multistreamWebRTC multistream
WebRTC multistream
mganeko
 

Mehr von mganeko (20)

Google Meet でもバーチャル背景を使いたい (WebRTC Meetup Online)
Google Meet でもバーチャル背景を使いたい (WebRTC Meetup Online)Google Meet でもバーチャル背景を使いたい (WebRTC Meetup Online)
Google Meet でもバーチャル背景を使いたい (WebRTC Meetup Online)
 
Amazon Kinesis Video Streams WebRTC 使ってみた
Amazon Kinesis Video Streams WebRTC 使ってみたAmazon Kinesis Video Streams WebRTC 使ってみた
Amazon Kinesis Video Streams WebRTC 使ってみた
 
Build Node.js-WASM/WASI Tiny compiler with Node.js
Build Node.js-WASM/WASI Tiny compiler with Node.jsBuild Node.js-WASM/WASI Tiny compiler with Node.js
Build Node.js-WASM/WASI Tiny compiler with Node.js
 
Node.js x Headless Chrome for WeRTC MCU / Node.js x Chrome headless で、お手軽WebR...
Node.js x Headless Chrome for WeRTC MCU / Node.js x Chrome headless で、お手軽WebR...Node.js x Headless Chrome for WeRTC MCU / Node.js x Chrome headless で、お手軽WebR...
Node.js x Headless Chrome for WeRTC MCU / Node.js x Chrome headless で、お手軽WebR...
 
Skywayのビデオチャットを録画しよう。そう、ブラウザでね
Skywayのビデオチャットを録画しよう。そう、ブラウザでねSkywayのビデオチャットを録画しよう。そう、ブラウザでね
Skywayのビデオチャットを録画しよう。そう、ブラウザでね
 
WebRTC mediasoup on raspberrypi3
WebRTC mediasoup on raspberrypi3WebRTC mediasoup on raspberrypi3
WebRTC mediasoup on raspberrypi3
 
WebRTC SFU Mediasoup Sample update
WebRTC SFU Mediasoup Sample updateWebRTC SFU Mediasoup Sample update
WebRTC SFU Mediasoup Sample update
 
ブラウザでWebRTC - iOSゲートウェイ作ってみた
ブラウザでWebRTC - iOSゲートウェイ作ってみたブラウザでWebRTC - iOSゲートウェイ作ってみた
ブラウザでWebRTC - iOSゲートウェイ作ってみた
 
WebRTC SFU mediasoup sample
WebRTC SFU mediasoup sampleWebRTC SFU mediasoup sample
WebRTC SFU mediasoup sample
 
Inside of 聖徳玉子 by O2
Inside of 聖徳玉子 by O2Inside of 聖徳玉子 by O2
Inside of 聖徳玉子 by O2
 
Node.js with WebRTC DataChannel
Node.js with WebRTC DataChannelNode.js with WebRTC DataChannel
Node.js with WebRTC DataChannel
 
WebRTC Build MCU on browser
WebRTC Build MCU on browserWebRTC Build MCU on browser
WebRTC Build MCU on browser
 
PeerConnectionリレーとMediaRecorder
PeerConnectionリレーとMediaRecorderPeerConnectionリレーとMediaRecorder
PeerConnectionリレーとMediaRecorder
 
ここがつらいよWebRTC - WebRTC開発の落とし穴
ここがつらいよWebRTC - WebRTC開発の落とし穴ここがつらいよWebRTC - WebRTC開発の落とし穴
ここがつらいよWebRTC - WebRTC開発の落とし穴
 
Webrtc bootcamp handson
Webrtc bootcamp handsonWebrtc bootcamp handson
Webrtc bootcamp handson
 
WebRTC multitrack / multistream
WebRTC multitrack / multistreamWebRTC multitrack / multistream
WebRTC multitrack / multistream
 
WebRTC UserMedia Catalog: いろんなユーザメディア(MediaStream)を使ってみよう
WebRTC UserMedia Catalog: いろんなユーザメディア(MediaStream)を使ってみようWebRTC UserMedia Catalog: いろんなユーザメディア(MediaStream)を使ってみよう
WebRTC UserMedia Catalog: いろんなユーザメディア(MediaStream)を使ってみよう
 
WebRTC multistream
WebRTC multistreamWebRTC multistream
WebRTC multistream
 
Inside WebM
Inside WebMInside WebM
Inside WebM
 
MediaRecorder と WebM で、オレオレ Live Streaming
MediaRecorder と WebM で、オレオレ Live StreamingMediaRecorder と WebM で、オレオレ Live Streaming
MediaRecorder と WebM で、オレオレ Live Streaming
 

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

  • 1. Node.jsでつくるNode.js ミニインタープリター&コンパイラー Build Node.js mini-interpreter and 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. ミニコンパイラー編
  • 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
  • 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()
  • 39. ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく • 機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数
  • 40. ミニコンパイラ実装の進め方 • ステップ・バイ・ステップで取り組む • 「コンパイル→実行」できる範囲を増やしていく • 機能追加のステップ • 定数の扱い • 四則演算 • 変数 • 条件分岐 • ループ • ユーザ定義関数 例として このステップを説明
  • 41. ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • generate() に実装を追加 • コンパイラーを動かしてIRコード生成を確認 • 実行して動作を確認 • lli でIR を実行 • llc & リンカーでバイナリを生成
  • 42. ステップ毎にやること • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • 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のソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • 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を組立てる 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 () で生成
  • 58. 各ステップの中身 • コンパイル対象となるNode.jsのソースコードを準備 • 生成結果となるLLVM IR を調査 • コンパラーに実装を追加 • 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)
  • 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
  • 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 • 関数は呼び出される前に定義されている必要がある • 最初の実装では、関数の中身を全て確定してから登録していた function func2() { 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年ぶりにコンパイラを作りたかったことを思い出した • 今回実際に動かすことができて、かなり興奮した
  • 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
  • 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 ミニコンパイラー clang lli 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