javascript/C言語/アセンブラを用い、 字句解析、構文解析、インタプリタ、コンパイラのプログラムをスクラッチから作りながら、 「プログラミング言語の作り方」を解説する。
プログラミング言語を作るときに、一番最初にやるべきことは、四則演算ではない。
hello worldだ。
言語仕様として、 以下のような、「"文字列"」と、「print関数」だけがある言語を作ろう。
自作言語の拡張子を「.3」とするため、「source.3」を作る。
内容はprint関数で"hello world"と表示するだけだ。
$ cat source.3 print("hello world")
コンパイラを作るにあたり、以下のファイル構成で実装した。
ファイルから読み、字句解析lexerで、tokens配列を取得。 tokensの内容を表示してから、parserに渡している。
今回は、簡略化のため、parser内部で即アセンブラ(source.s)を出力している。 コンパイラ自体の役目としては、ここまでで終了だ。
gcc内部でも同じことをやっているが、追加で、外部コマンドの実行を行う。
asコマンドは、アセンブラ(テキストファイルsource.s)を機械語(オブジェクトファイルsource.o=バイナリ)に変換している。
ldコマンドは、(分割コンパイルされた複数の)オブジェクトファイルを結合して、1つの実行ファイル(exec)にしている。
gccではやってないが、利便性のため、実行ファイルexecの実行まで行っている。
$ cat compiler.js #!/usr/bin/node //別ファイルに記述している処理を読み込む var {exec,write,read,show,error} = require("./utils.js"); var lexer = require("./lexer.js"); var parser = require("./parser-comp.js"); var genasm = require("./genasm.js"); //source.3から読み込む var source = read("source.3"); var tokens = lexer(source); //filter後、処理前のtokensを表示 show("処理前tokens =",tokens); //構文解析し、即アセンブラを出力 parser(tokens); console.log("-------------"); //ここまででコンパイラ自体の処理は終わり。 //ここからは外部コマンドを実行する。 //asコマンドで、機械語(オブジェクトファイル)にする。 exec("as source.s -o source.o"); //ldコマンドで、libcを動的リンク、ローダーを指定して、実行ファイルexecにする。 exec("ld -lc --dynamic-linker /lib64/ld-linux-x86-64.so.2 -o exec source.o"); //execコマンドを実行し、結果を表示。 console.log(exec("./exec"));
インタプリタのhello worldと同じように parserの途中で、アセンブラを出力してみた。
$ cat parser-comp.js module.exports = parser; var {write,expect,accept,show,error} = require("./utils.js"); var tokens; //構文解析開始 function parser(t){ tokens = t; return callprint(); } //print関数呼び出しの構文解析 function callprint(){ if(tokens.length==0)return; //関数名がprintであること expect(tokens,"print"); //関数呼び出しの丸カッコであること expect(tokens,"("); //文字列を取得 var msg = tokens.shift(); var codes = []; //intel表記 codes.push("\n#intel表記を使う"); codes.push(".intel_syntax noprefix"); //エントリポイント codes.push("\n#エントリポイント"); codes.push(".global _start"); codes.push("_start:"); codes.push("lea rdi,[.s1]"); codes.push("call printf"); //終了ステータス0でexit codes.push("\n#exit(0)で終了"); codes.push("end:"); codes.push("mov rdi,0"); codes.push("call exit"); codes.push("\n#文字列リテラル.s連番で定義"); //.s連番: .string "文字列" codes.push(".s1: .string "+ msg); //最後に改行がないと、asが文句言う。 var asm = codes.join("\n")+"\n"; write("source.s",asm); //閉じカッコであること expect(tokens,")"); }
write,execを追加した。
$ cat utils.js //---------------ユーティリティ関数定義---------------// module.exports = {exec,write,read,show,error,accept,expect}; //外部コマンド実行して結果を取得 function exec(cmd){ return require('child_process').execSync(cmd,{encoding:"utf8"}); } //ファイルに書き込む function write(filename,data){ require('fs').writeFileSync(filename,data); } //ファイルを読み込む function read(filename){ return require('fs').readFileSync(filename,"utf-8"); } //深い階層まですべて表示する。 function show(msg,obj){ obj = require('util') .inspect(obj,{ showHidden: false, depth: null, maxArrayLength: null,colors: true }); console.log(msg + obj); } //エラーメッセージを表示して終了する function error(...msgs){ console.log(msgs.join("")); process.exit(); } //tokens[0]が指定されたcsに含まれるならshiftして返す。そうでないなら何もしない function accept(tokens,...cs){ //tokensがもうない if(tokens.length==0) return; //tokensの先頭が、指示されたcsに含まれるので、shiftして返す if(cs.includes(tokens[0])) return tokens.shift(); //なにもしない return; } //acceptのエラー版 function expect(tokens,...cs){ var t = accept(tokens,...cs); //取得できたらそのまま返す if(t) return t; //エラー表示して終了。 error("tokens[0]=",tokens[0],"が",cs,"に含まれないため終了。"); }
compiler.jsを実行する。
まず、tokensが意図どおりに分割されているか、念のため表示しているので、確認しよう。
その後、「hello world」と表示されており、 問題なく、自作プログラミング言語を実装できていることが分かる。
$ ./compiler.js 処理前tokens =[ 'print', '(', '"hello world"', ')' ] hello world
hello worldだけができる「プログラミング言語」だから、とても簡単に作れた。
念のため、できた実行ファイルexecを、ちゃんとバイナリ実行ファイルになっているのかをfileコマンドで調べて、 実際にコマンドラインから、直接実行しておく。
$ file exec exec: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
$ ./exec hello world
コンパイラが出力したアセンブラ(source.s)を載せておく。
$ cat source.s #intel表記を使う .intel_syntax noprefix #エントリポイント .global _start _start: lea rdi,[.s1] call printf #exit(0)で終了 end: mov rdi,0 call exit #文字列リテラル.s連番で定義 .s1: .string "hello world"