プログラミング言語の作り方

javascript/C言語/アセンブラを用い、 字句解析、構文解析、インタプリタ、コンパイラのプログラムをスクラッチから作りながら、 「プログラミング言語の作り方」を解説する。

hello world
javascript版プログラミング言語の作り方(コンパイラ開発)

プログラミング言語を作るときに、一番最初にやるべきことは、四則演算ではない。

hello worldだ。

ページメニュー

現時点の言語仕様

言語仕様として、 以下のような、「"文字列"」と、「print関数」だけがある言語を作ろう。

自作言語で記述されたソース

自作言語の拡張子を「.3」とするため、「source.3」を作る。

内容はprint関数で"hello world"と表示するだけだ。

$ cat source.3
print("hello world")

コンパイラのファイル構成

コンパイラを作るにあたり、以下のファイル構成で実装した。

compiler.jsの実装

ファイルから読み、字句解析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"));

parser-comp.jsの実装

インタプリタの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,")");
}

utils.jsの実装

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"

このページの目次へ戻るサイトの最上位へ戻る