プログラミングや低レイヤで遊ぶ人

基本的には遊んだことを記事にしていく

猫語しかしゃべらなくなった先輩のためにCatLangageを作成した

catlangage compiler


コンパイラといっていますがmingw用のアセンブラをはくだけです。

機能はかなり少ないですが、猫語からアセンブラをはくことが可能です。

こちらがソースコードです。

疑問・不備等があった場合は私のツイッターでご連絡ください。@jumdtw

事の経緯


ツイッターでにゃんにゃんしか言わなくなってしまった方がいらっしゃります。
f:id:jumdtw:20200530033350j:plain
※本人の許可はとっております


彼に何があったのでしょうか。それを想像するには私は未熟すぎます。しかし、何とかしてあげたい。そこで猫語からアセンブリ言語をはきだすコンパイラcatcomを作成することにしました。

catcomができること


まぁ、要するにネタコンパイラなのでできることは相当限られてきます。
・加算減算を行いその結果を終了コードとしてはく(しかも、数字は0~7までしか使えません)
・特定の文字列を出力する
これだけです。
しかし、これだけあれば彼は特定の文字列のみですが会話ができ終了コードを吐き出すことができるのです!!!!

ちなみに特定の文字列とは以下です。
・I want meet.
・goodmorning.
・Im tired.
もっといっぱい実装はできますがネタなのでこんなものでしょう。

catlangage 仕様


そんでもって猫語からアセンブリ言語をはくといいましたが、「にゃー」だの「ゴロゴロ」だの日本語でやるのは少々面倒くさいので英語表記での猫語で処理をしていきます。以下はサイトを参考にして得たものです。

・meow:にゃー
・purr:ゴロゴロ
・roar:ガオー

これを数値と演算子などに当てはめていきます。

数値


meowを0、purrを1として表現している。
数値は "meow" から始まる

- 0:meowmeowmeowmeow
- 1:meowmeowmeowpurr
- 2:meowmeowpurrmeow
- 3:meowmeowpurrpurr
- 4:meowpurrmeowmeow
- 5:meowpurrmeowpurr
- 6:meowpurrpurrmeow
- 7:meowpurrpurrpurr

演算子と文字列表示


演算子は "roar" から始まります。

+:roarmeow
-:roarpurr

文字列表示は "purr" から始まります。

・I want meet. :purrmeowmeow
・goodmorning. :purrmeowpurr
・Im tired. :purrpurrpurr

実装方法


以下のサイトがとても参考になりました。

低レイヤを知りたい人のためのCコンパイラ作成入門

私が説明しようとするとほとんど以下のサイトの反復説明になってしますので今回のcatlangage特有部分のみ説明していこうと思います。

トークンナイザ


以下がトークンナイザでの判別になります。

// tokenize.cpp
//空白文字をスキップ
if(isspace(*p)||*p=='\n'||*p=='\t'){
    p++;
    continue;
}

// ステーキの判別
if(strncmp(p,"purrmeowmeow",12)==0&&!is_alnum(p[12])){
    token.ty = TK_MEET;
    token.str = p;
    p+=12;
    tokens.push_back(token);
    continue;
}

//          .
//          .
//     こんな感じで文字列の判別をおこなう
//          .
//          .

// +の判別
if(strncmp(p,"roarmeow",8)==0&&!is_alnum(p[8])){
    token.ty = TK_PLUS;
    token.str = p;
    p+=8;
    tokens.push_back(token);
    continue;
}

//          .
//          .
//     こんな感じで+-の判別をおこなう
//          .
//          .


// 数値の判別
// 0
if(strncmp(p,"meowmeowmeowmeow",16)==0&&!is_alnum(p[16])){
    token.ty = TK_NUM;
    token.val = 0;
    token.str = p;
    p+=16;
    tokens.push_back(token);
    continue;
}

//          .
//          .
//     こんな感じで数値の判別をおこなう
//          .
//          .


このように一つ一つ判別していきます。 そのためあんまり多くの判別はできません(本来はこんなことをしないで文字か数字かで判断しますが、猫語のためにこんな判断をしています)。

パーサ


以下のようにパースしていきます。

expr = add

add = primary ("+" primary | "-" primary)* | meet | tired | hello

primary = num 


それで実際にこれをコードで実装すると以下のようになります。

Node *primary(){

    if(tokens[pos].ty == TK_NUM){
        return new_node_num(tokens[pos++].val);
    }

    printf("%s\n",tokens[pos].str);

    printf("error gen node\n");
    exit(1);
}

Node *add(){

    if(tokens[pos].ty==TK_MEET){
        return new_node(ND_MEET,NULL,NULL);
    }else if(tokens[pos].ty==TK_HELLO){
        return new_node(ND_HELLO,NULL,NULL);
    }else if(tokens[pos].ty==TK_TIRED){
        return new_node(ND_TIRED,NULL,NULL);
    }

    Node *node = primary();
    
    for(;;){
        if(tokens[pos].ty==TK_PLUS){
            pos++;
            node = new_node(ND_PLUS,node,primary());
        }else if(tokens[pos].ty==TK_MINUS){
            pos++;
            node = new_node(ND_MINUS,node,primary());
        }else{
            return node;
        }
    }

}

Node *expr(){
    return add();
}


expr始動で解析をしていきます。
最初に文字列の表示をするか否かを判別し、文字列でなかったら数字確定なので加算減算での解析をします。機能も少ない分コードもそこまで複雑ではないです。

コードジェネレーター


私の環境では以下の環境でアセンブラを処理します。

> gcc --version
gcc (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

> as --version
GNU assembler (GNU Binutils) 2.30
Copyright (C) 2018 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or later.
This program has absolutely no warranty.
This assembler was configured for a target of `x86_64-w64-mingw32'.


なのでそのためのコードを生成します。
以下はサンプルです。

// 文字列の表示
    .text
.LC0:
    .ascii "hello\0"
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe   %rbp, 0
    subq    $32, %rsp
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
    leaq    .LC0(%rip), %rcx
    call    puts
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc



// 加算
    .text
    .def    __main; .scl    2;  .type   32; .endef
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    pushq   %rbp
    .seh_pushreg    %rbp
    movq    %rsp, %rbp
    .seh_setframe   %rbp, 0
    subq    $32, %rsp
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
    push $1       // 数値を二つpushしておく
    push $4
    pop     %rdi        // push した値をそれぞれレジスタに入れる
    pop     %rax
    add     %rdi, %rax  // その値を使って加算する
    push    %rax
    pop     %rax        // raxにpopして終了コードとして返す
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc


これらコードを生成するためのコードジェネレータが以下のようになります。

// comcat.cpp

void gen(Node *node){

    if(node->ty == ND_MEET){
        printf("    .text\n");
        printf(".LC0:\n");
        printf("    .ascii ");
        printf("\"");
        printf("I want meet.\\0");
        printf("\"");
        printf("\n");
        printf("    .globl  main\n");
        printf("    .def    main;   .scl    2;  .type   32; .endef\n");
        printf("    .seh_proc   main\n");
        printf("main:\n");
        printf("    pushq   %%rbp\n");
        printf("    .seh_pushreg    %%rbp\n");
        printf("    movq    %%rsp, %%rbp\n");
        printf("    .seh_setframe   %%rbp, 0\n");
        printf("    subq    $32, %%rsp\n");
        printf("    .seh_stackalloc 32\n");
        printf("    .seh_endprologue\n");
        printf("    call    __main\n");
        printf("    leaq    .LC0(%%rip), %%rcx\n");
        printf("    call    puts\n");
        printf("    movl    $0, %%eax\n");
        printf("    addq    $32, %%rsp\n");
        printf("    popq    %%rbp\n");
        printf("    ret\n");
        printf("    .seh_endproc\n");
        return;
    }

    //          .
    //          .
    //     ND_MEETなどだったらその段階で文字列表示確定なのでそのままサンプルのようなコードをはく
    //          .
    //          .

    // 数値は計算に必要なのでスタックにpushする
    if(node->ty == ND_NUM){
        printf("    push    $%%d\n",node->val);
        return;
    }


    // 数値・文字列でなかったらスタックの値を使って計算していく
    printf("    .text\n");
    printf("    .def    __main; .scl    2;  .type   32; .endef\n");
    printf("    .globl  main\n");
    printf("    .def    main;   .scl    2;  .type   32; .endef\n");
    printf("    .seh_proc   main\n");
    printf("main:\n");
    printf("    pushq   %%rbp\n");
    printf("    .seh_pushreg    %%rbp\n");
    printf("    movq    %%rsp, %%rbp\n");
    printf("    .seh_setframe   %%rbp, 0\n");
    printf("    subq    $32, %%rsp\n");
    printf("    .seh_stackalloc 32\n");
    printf("    .seh_endprologue\n");
    printf("    call    __main\n");

    gen(node->lhs);
    gen(node->rhs);

    printf("    pop     %%rdi\n");
    printf("    pop     %%rax\n");

    switch(node->ty){
        case ND_PLUS:
            printf("    add     %%rdi, %%rax\n");
            break;
        case ND_MINUS:
            printf("    sub     %%rdi, %%rax\n");
            break;
    }

    printf("    addq    $32, %%rsp\n");
    printf("    popq    %%rbp\n");
    printf("    ret\n");
    printf("    .seh_endproc\n");
}


実行結果


文字列出力コードは以下です。

// test.cat
purrmeowmeow


実行コードはこちらです。ちなみに、アセンブラをprintfで表示しているだけなのでパイプなどを使っていい感じにファイルに出力します。

// 文字列出力
> .\bin\comcat.exe test.cat > test.s

> as -o test.o test.s

> gcc -o test test.o

> test.exe
I want meet.


演算のコードは以下です。

// calc.cat
meowmeowmeowpurr roarmeow meowmeowmeowpurr


実行コードはこちらです。windowsでは%errorlevel%で終了コードを確認できます。

> .\bin\comcat.exe calc.cat > calc.s

> as -o calc.o calc.s

> gcc -o calc calc.o

> calc.exe

> echo %errorlevel%
2


こんな感じです。これで彼は「おはよう」・「疲れた」・「肉食べたい」を猫語のみで表現できるのです!!!
f:id:jumdtw:20200530033350j:plain

まとめ


まぁ、ネタなのでこんなもんでしょう。コンパイラの勉強に使用した以下のサイトはとても勉強になったのでおすすめです。
低レイヤを知りたい人のためのCコンパイラ作成入門

参考

MinGWのアセンブラでhello world
低レイヤを知りたい人のためのCコンパイラ作成入門
猫の英語辞典:猫の鳴き声って英語でなんて言うの?