step1 - Alignof/compiler_book_again GitHub Wiki

Step1 では「引数に与えられた数字を終了コードとして返すバイナリのアセンブリを出力するだけのコンパイラ」を作成する.
また,開発環境もここで整える. compiler book は単にコンパイラの開発だけでなく,Make によるビルドの自動化や自動テストの導入,git によるバージョン管理など中規模以上の開発に必須の要素をカバーしているのがすごい.インクリメンタルな開発の練習という意味でも実に優れた教材だと思う.
C 言語の教科書を読んで小さなプログラムをいくつか書いた中級者がいきなりこれをやっても全く問題はないと思うしむしろうってつけまである.みんなもやろう!

さて,このステップで大事なのは環境構築だが,cc_sakura の開発環境は今見返すと思ったより悪くない. 特にディレクトリの構成は扱いやすいものになっていると思う.
なので今回は linter/formatter の導入に注力した.

clang-format の導入

https://clang.llvm.org/docs/ClangFormat.html
今見返すと cc_sakura は本当にひどいコードだが,その一因はコーディングルールにあると思う.
今と昔で好みが変わっているので尚更ひどく見える.
昔はイコールの前後にスペースを入れなかったり位置を揃えたり今と真逆だったんだよな.
clang-format の設定を見直して $ make fmt で修正できるようにした.

clang-tidy の導入

https://clang.llvm.org/extra/clang-tidy/
こちらは使ったことが無かったので初挑戦.
使うためにはどうやら compile_commands.json なるものが必要らしい.

[
  { "directory": "/path/to/repo/compiler_book_again/",
    "command": "/usr/bin/gcc -std=c11 -g -static -Wall ./src/main.c",
    "file": "src/main.c"
  }
]

cmake とかだと自動で生成できるらしいが,取り敢えず手書き.
これファイルを増やすごとにやる必要があるのか……

最初のコード

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "invalid number of arguments\n");
        return 1;
    }

    printf(".intel_syntax noprefix\n");
    printf(".globl main\n");
    printf("main:\n");
    printf("    mov rax, %d\n", atoi(argv[1]));
    printf("    ret\n");

    return 0;
}

を clang-tidy にかけると以下のような警告が出た.

$ make lint
clang-tidy -checks=cert-* --warnings-as-errors=* ./src/main.c
1014 warnings generated.
./src/main.c:6:9: error: the value returned by this function should be used [cert-err33-c,-warnings-as-errors]
        fprintf(stderr, "invalid number of arguments\n");
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
./src/main.c:6:9: note: cast the expression to void to silence this warning
./src/main.c:13:33: error: 'atoi' used to convert a string to an integer value, but function will not report conversion errors; consider using 'strtol' instead [cert-err34-c,-warnings-as-errors]
    printf("    mov rax, %d\n", atoi(argv[1]));
                                ^
Suppressed 1012 warnings (1012 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.
2 warnings treated as errors
make: *** [Makefile:31: lint] Error 1

fprintf の返り値は出力した文字列でエラーが発生した場合は負の値を返す.
返り値でエラーハンドリングをした方が良いのはそうかも. ちなみに printf でも同様の返り値が返されるがそっちは怒られないのでちゃんと関数の内容を見てルールが作られているっぽい.
https://clang.llvm.org/extra/clang-tidy/checks/cert/err33-c.html
↑一覧があった.

Rust っぽくint _ret = fprintf(stderr, "invalid number of arguments\n");と書いたら,

./src/main.c:7:13: error: Value stored to 'ret' during its initialization is never read [clang-analyzer-deadcode.DeadStores,-warnings-as-errors]
        int ret = fprintf(stderr, "invalid number of arguments\n");
            ^~~   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

と怒られてしまった. でももう終了するだけだしなぁ.ここらへんは難しい.

2つ目の atoi は変換エラーを返してくれない(変換できない場合は0が返るが,これが変換した結果なのか本当に0かは判断できない.また,オーバーフローの場合の返り値は未定義)ので代わりに strtol を使えとのこと.
https://clang.llvm.org/extra/clang-tidy/checks/cert/err34-c.html

linter の出力を参考に,結局以下のようなコードになった.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        int ret = fprintf(stderr, "invalid number of arguments\n");
        if (ret < 0) exit(1);
        return 1;
    }

    int exit_code = (long) strtol(argv[1], NULL, 10);
    if (errno == ERANGE) {
        int ret = fprintf(stderr, "invalid exit code\n");
        if (ret < 0) exit(1);
        return 1;
    }

    printf(".intel_syntax noprefix\n");
    printf(".globl main\n");
    printf("main:\n");
    printf("    mov rax, %d\n", exit_code);
    printf("    ret\n");

    return 0;
}

fprintf の返り値の扱いはかなり微妙(ret が負かの判定はあってもなくてもいい)だが,良くなったとは思う.

⚠️ **GitHub.com Fallback** ⚠️