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

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

複数文対応
javascript版プログラミング言語の作り方(コンパイラ開発)

print文が何回呼ばれても良いように、複数文対応をしよう。

ページメニュー

現時点の言語仕様

複数文に対応するため、セミコロンで区切るという言語仕様を追加する。

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

ソースコードには、セミコロンで区切られたprint文を3つを並べよう。

$ cat source.3
print("hello world");
print("hello world2");
print("hello world3");

genasm.jsの実装

複数文に対応するため、セミコロンがきたら、left/rightそれぞれでgenを呼ぶ処理を追加している。

$ cat genasm.js

var {error}  = require("./utils.js");

//リテラル文字列の定義
var strs = [];

//アセンブラコードの配列
//コードだけでなく、アセンブラ上の「#コメント」を出力。
var codes = [];

//astからアセンブラへ変換
module.exports = function genasm(ast){
    //intel表記
    codes.push("\n#intel表記を使う");
    codes.push(".intel_syntax noprefix");

    //エントリポイント
    codes.push("\n#エントリポイント");
    codes.push(".global _start");
    codes.push("_start:");

    //変換開始
    gen(ast);

    //終了ステータス0でexit
    codes.push("\n#exit(0)で終了");
    codes.push("end:");
    codes.push("mov rdi,0");
    codes.push("call exit");

    //printfで使う文字列リテラル
    codes.push("\n#printfで使う文字列リテラルを定義");
    codes.push('.newline: .string "\\n"');
    codes.push('.fmtg: .string "%g"');

    //文字列リテラル
    codes.push("\n#文字列リテラル.s連番で定義");
    strs.forEach((s,i)=>{
        //.s連番: .string "文字列"
        codes.push(".s"+(i+1)+": .string "+ s);
    });

    //最後に改行がないと、asが文句言う。
    return codes.join("\n")+"\n";
}


//「おあずけ」をアセンブラでは、スタックを用いて表現する。
//スタック操作のpush/popのラッパ関数群。

//通常レジスタには、push/popはあるが、アライメント調整があるので関数化。
//printfは、アライメントを16バイトに揃えないとエラーになる。
//push(通常レジスタ名)
function push(r){
    //pushでアライメントが8バイトずれる
    codes.push("push "+r);

    //スタックポインタrspをさらに-8バイト=伸ばしてずれを16にする。pop時に戻す。
    codes.push("\n#アライメント8バイト伸ばす");
    codes.push("sub rsp,8");
}

//pop(レジスタ名)
function pop(r){
    //push時に伸ばしておいたスタックポインタrspを、先に+8バイト=縮めて戻しておく。
    codes.push("\n#アライメント8バイト戻し");
    codes.push("add rsp,8");

    //さらに、popでアライメントが8バイトずれ戻り、16バイトにそろう。
    codes.push("pop "+r);
}

//astの階層をたどりながらアセンブラを出力。
//アセンブラコードは、codesに格納
function gen(a){
    if(!a) return;
    if(!a.op){

        if(a[0] == '"'){
            //文字列リテラルとして定義
            strs.push(a);
            //「.s連番(strs.length)」という名で定義されたリテラル文字列を取得する
            codes.push("\n#.s"+strs.length+"で定義されたリテラル文字列取得");
            //文字列本体ではなく、アドレスを計算
            codes.push("lea r10,[.s"+strs.length+"]");
            //push r10(文字列) スタックに保管
            push("r10");

            //文字列だと通知
            return 1;
        }

        //それ以外(変数名、関数名などのシンボル)ならそのまま返す
        return a;
    }else (@if(a.op == ";"){
        gen(a.left) ; gen(a.right);@)
    }else if(a.op == "()"){
        var func = gen(a.left);
        if( func == "print"){
            //スタックは逆転するため、reverseしておく。
            [gen(a.right)].flat().reverse().forEach(m=>{
                //文字列
                //スタックから、printf("文字列")の第一引数rdiへセット
                pop("rdi");

                //文字列をprintfするときはrax=0でないとエラー
                codes.push("\n#文字列をprintfするときは、rax=0でないとエラー");
                codes.push("mov rax,0");

                codes.push("\n#文字列表示");
                codes.push("call printf");
            });
            //最後に改行コードを表示する
            //.newlineは"\n"
            codes.push("\n#改行コード.newline表示");
            codes.push("lea rdi,[.newline]");
            codes.push("call printf");
        }else{
            error("未実装の関数呼び出し func=",func);
        }
    }else{
        error("未実装の演算子 op=",a.op);
    }
}

コンパイラの実行

compiler.jsを実行する。

まず、tokensとastが意図どおりに分割されているか、念のため表示しているので、確認しよう。

特に、astの階層の違いによって、処理の順番が明確になっていることに注意しよう。

leftの方を、先に深く深くたどり、行き詰ると、そのノードのleft/rightの順で処理する。 次は、1つ上にもどる、という順だ。

このASTの順番通りに、「hello world」、「hello world2」、「hello world3」と表示されている。

$ ./compiler.js
処理前tokens =[
  'print',          '(',
  '"hello world"',  ')',
  ';',              'print',
  '(',              '"hello world2"',
  ')',              ';',
  'print',          '(',
  '"hello world3"', ')',
  ';'
]
抽象構文木ast={
  left: {
    left: {
      left: { left: 'print', op: '()', right: '"hello world"' },
      op: ';',
      right: { left: 'print', op: '()', right: '"hello world2"' }
    },
    op: ';',
    right: { left: 'print', op: '()', right: '"hello world3"' }
  },
  op: ';',
  right: undefined
}

hello world
hello world2
hello world3

コンパイラが出力したアセンブラ

コンパイラが出力したアセンブラ(source.s)を載せておく。

$ cat source.s

#intel表記を使う
.intel_syntax noprefix

#エントリポイント
.global _start
_start:

#.s1で定義されたリテラル文字列取得
lea r10,[.s1]
push r10

#アライメント8バイト伸ばす
sub rsp,8

#アライメント8バイト戻し
add rsp,8
pop rdi

#文字列をprintfするときは、rax=0でないとエラー
mov rax,0

#文字列表示
call printf

#改行コード.newline表示
lea rdi,[.newline]
call printf

#.s2で定義されたリテラル文字列取得
lea r10,[.s2]
push r10

#アライメント8バイト伸ばす
sub rsp,8

#アライメント8バイト戻し
add rsp,8
pop rdi

#文字列をprintfするときは、rax=0でないとエラー
mov rax,0

#文字列表示
call printf

#改行コード.newline表示
lea rdi,[.newline]
call printf

#.s3で定義されたリテラル文字列取得
lea r10,[.s3]
push r10

#アライメント8バイト伸ばす
sub rsp,8

#アライメント8バイト戻し
add rsp,8
pop rdi

#文字列をprintfするときは、rax=0でないとエラー
mov rax,0

#文字列表示
call printf

#改行コード.newline表示
lea rdi,[.newline]
call printf

#exit(0)で終了
end:
mov rdi,0
call exit

#printfで使う文字列リテラルを定義
.newline: .string "\n"
.fmtg: .string "%g"

#文字列リテラル.s連番で定義
.s1: .string "hello world"
.s2: .string "hello world2"
.s3: .string "hello world3"

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