chibiccを読む~Cコンパイラコードリーディング~ ステップ4

トップページ
jupiteroak.hatenablog.com
「低レイヤを知りたい人のためのCコンパイラ作成入門」のCコンパイラを読んでいきます。
www.sigbus.info
ステップ4に該当
github.com

今回作成するコンパイラ

エラーメッセージを改良(エラーメッセージを改良したコンパイラを作成する)。

コンパイラソースコード

https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c

#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum {
  TK_RESERVED, // Keywords or punctuators
  TK_NUM,      // Integer literals
  TK_EOF,      // End-of-file markers
} TokenKind;

// Token type
typedef struct Token Token;
struct Token {
  TokenKind kind; // Token kind
  Token *next;    // Next token
  int val;        // If kind is TK_NUM, its value
  char *str;      // Token string
};

// Input program
char *user_input;

// Current token
Token *token;

// Reports an error and exit.
void error(char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
  exit(1);
}

// Reports an error location and exit.
void error_at(char *loc, char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);

  int pos = loc - user_input;
  fprintf(stderr, "%s\n", user_input);
  fprintf(stderr, "%*s", pos, ""); // print pos spaces.
  fprintf(stderr, "^ ");
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
  exit(1);
}

// Consumes the current token if it matches `op`.
bool consume(char op) {
  if (token->kind != TK_RESERVED || token->str[0] != op)
    return false;
  token = token->next;
  return true;
}

// Ensure that the current token is `op`.
void expect(char op) {
  if (token->kind != TK_RESERVED || token->str[0] != op)
    error_at(token->str, "expected '%c'", op);
  token = token->next;
}

// Ensure that the current token is TK_NUM.
int expect_number() {
  if (token->kind != TK_NUM)
    error_at(token->str, "expected a number");
  int val = token->val;
  token = token->next;
  return val;
}

bool at_eof() {
  return token->kind == TK_EOF;
}

// Create a new token and add it as the next token of `cur`.
Token *new_token(TokenKind kind, Token *cur, char *str) {
  Token *tok = calloc(1, sizeof(Token));
  tok->kind = kind;
  tok->str = str;
  cur->next = tok;
  return tok;
}

// Tokenize `user_input` and returns new tokens.
Token *tokenize() {
  char *p = user_input;
  Token head;
  head.next = NULL;
  Token *cur = &head;

  while (*p) {
    // Skip whitespace characters.
    if (isspace(*p)) {
      p++;
      continue;
    }

    // Punctuator
    if (*p == '+' || *p == '-') {
      cur = new_token(TK_RESERVED, cur, p++);
      continue;
    }

    // Integer literal
    if (isdigit(*p)) {
      cur = new_token(TK_NUM, cur, p);
      cur->val = strtol(p, &p, 10);
      continue;
    }

    error_at(p, "expected a number");
  }

  new_token(TK_EOF, cur, p);
  return head.next;
}

int main(int argc, char **argv) {
  if (argc != 2) {
    error("%s: invalid number of arguments", argv[0]);
    return 1;
  }

  user_input = argv[1];
  token = tokenize();

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");

  // The first token must be a number
  printf("  mov rax, %d\n", expect_number());

  // ... followed by either `+ <number>` or `- <number>`.
  while (!at_eof()) {
    if (consume('+')) {
      printf("  add rax, %d\n", expect_number());
      continue;
    }

    expect('-');
    printf("  sub rax, %d\n", expect_number());
  }

  printf("  ret\n");
  return 0;
}

追加・修正されたコンパイラソースコード

main関数

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR129
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L129

int main(int argc, char **argv) {
  if (argc != 2) {
    error("%s: invalid number of arguments", argv[0]);
    return 1;
  }

  user_input = argv[1];
  token = tokenize();

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");

  // The first token must be a number
  printf("  mov rax, %d\n", expect_number());

  // ... followed by either `+ <number>` or `- <number>`.
  while (!at_eof()) {
    if (consume('+')) {
      printf("  add rax, %d\n", expect_number());
      continue;
    }

    expect('-');
    printf("  sub rax, %d\n", expect_number());
  }

  printf("  ret\n");
  return 0;
}
コマンドライン引数の個数をチェックする(変更なし)
  if (argc != 2) {
    error("%s: invalid number of arguments", argv[0]);
    return 1;
  }
トークナイズを行う
  user_input = argv[1];
  token = tokenize();

文字列の何バイト目でエラーが生じたかを計算するために、文字列の先頭アドレスをグローバル変数user_inputに格納してからトークナイズを行います。

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR23
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L23

// Input program
char *user_input;
部分的にアセンブリコードを生成する①(変更なし)
  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");
部分的にアセンブリコードを生成する②(変更なし)
  // The first token must be a number
  printf("  mov rax, %d\n", expect_number());
部分的にアセンブリコードを生成する③(変更なし)
  // ... followed by either `+ <number>` or `- <number>`.
  while (!at_eof()) {
    if (consume('+')) {
      printf("  add rax, %d\n", expect_number());
      continue;
    }

    expect('-');
    printf("  sub rax, %d\n", expect_number());
  }
部分的にアセンブリコードを生成する④(変更なし)
  printf("  ret\n");
  return 0;

error_at関数

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR38
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L38

// Reports an error location and exit.
void error_at(char *loc, char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);

  int pos = loc - user_input;
  fprintf(stderr, "%s\n", user_input);
  fprintf(stderr, "%*s", pos, ""); // print pos spaces.
  fprintf(stderr, "^ ");
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
  exit(1);
}

error_at関数は、トークナイズ途中の文字を指定するアドレスloc、引数fmtで指定された文字列、可変部の引数を用いてメッセージを出力し、プログラム(プロセス)を終了させます。

va_list apについて

va_startマクロ・vprintf関数などを使用するために必要となるva_list型の変数apを宣言します。

va_start(ap, fmt)について

va_list型の変数apを初期化して可変個の引数を扱えるようにするために、va_startマクロを呼び出します。
第一引数には先ほど宣言したva_list型の変数apを、第二引数には可変部分の引数の直前にある引数fmtを指定する必要があります。

エラーが生じた文字の算出を計算する
int pos = loc - user_input;

トークナイズの対象となる文字列において、先頭の文字からエラーが生じた文字まで何文字分(何バイト分)あるかを算出します。

fprintf(stderr, "%s\n", user_input)について

fprintf関数は、第一引数のファイルポインタが指定するファイルに、第二引数のchar型へのポインタが指定する文字列データを書き込みます。
今回の場合は、標準エラー出力ファイルに、user_inputが指定する文字列が書き込ます(コンソール画面にトークナイズの対象となる文字列が表示されます)。

fprintf(stderr, "%*s", pos, "")について

fprintf関数を呼び出して、標準エラー出力ファイルに、空白文字をpos文字分書き込みます(コンソール画面に空白文字がpos文字分表示されます)。
書式指定フィールドのwidthフィールドに*が指定されている時は、引数posが指定する文字幅の分だけ表示します。

fprintf(stderr, "^ ")ついて

fprintf関数を呼び出して、標準エラー出力ファイルに ^ を書き込みます(コンソール画面に、トークナイズの対象となる文字列のうちエラーが生じた文字の下に ^ が表示されます)。

vfprintf(stderr, fmt, ap)について

vfprintf関数は、可変個の引数を扱うことができるfprintf関数です。
標準エラー出力ファイルに、fmtが指定する文字列データを書き込みます(コンソール画面に文字列が表示されます)。

fprintf(stderr, "\n")について

fprintf関数を呼び出して、標準エラー出力ファイルに、\nを書き込みます(コンソール画面で改行が行われます)。

exit(1)について

exitシステムコールを呼び出して、このプログラム(プロセス)の処理を終了させます。
引数で指定した1の値が終了ステータスになります。

tokenize関数

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR116
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L116

// Tokenize `user_input` and returns new tokens.
Token *tokenize() {
  char *p = user_input;
  Token head;
  head.next = NULL;
  Token *cur = &head;

  while (*p) {
    // Skip whitespace characters.
    if (isspace(*p)) {
      p++;
      continue;
    }

    // Punctuator
    if (*p == '+' || *p == '-') {
      cur = new_token(TK_RESERVED, cur, p++);
      continue;
    }

    // Integer literal
    if (isdigit(*p)) {
      cur = new_token(TK_NUM, cur, p);
      cur->val = strtol(p, &p, 10);
      continue;
    }

    error_at(p, "expected a number");
  }

  new_token(TK_EOF, cur, p);
  return head.next;
}
文字列の先頭アドレスを取得する
char *p = user_input;

グローバル変数user_inputから、トークナイズの対象となる文字列の先頭アドレスを取得します。

トークンからなる連結リストのヘッダーを作成する(変更なし)
  Token head;
  head.next = NULL;
  Token *cur = &head;
空白文字の場合(変更なし)
    if (isspace(*p)) {
      p++;
      continue;
    }
+ または - の場合(変更なし)
    // Punctuator
    if (*p == '+' || *p == '-') {
      cur = new_token(TK_RESERVED, cur, p++);
      continue;
    }
数字の場合(変更なし)
    // Integer literal
    if (isdigit(*p)) {
      cur = new_token(TK_NUM, cur, p);
      cur->val = strtol(p, &p, 10);
      continue;
    }
その他の場合
    error_at(p, "expected a number");

上記のいずれにも該当しなかった場合は、error_at関数を呼び出して、メッセージを出力してからプログラムを終了させます。

トークン列の終端を表すトークンを生成する(変更なし)
  new_token(TK_EOF, cur, p);
連結リストの先頭トークンを戻り値としてリターンする(変更なし)
return head.next;

expect関数

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR63
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L63

// Ensure that the current token is `op`.
void expect(char op) {
  if (token->kind != TK_RESERVED || token->str[0] != op)
    error_at(token->str, "expected '%c'", op);
  token = token->next;
}
現在着目しているトークンの文字列を調べる
  if (token->kind != TK_RESERVED || token->str[0] != op)
    error_at(token->str, "expected '%c'", op);

現在着目しているトークンの型がTK_RESERVEDではない場合(トークンの種類が記号ではない場合)、または、トークンの文字列と引数opで指定された文字列が異なる場合は、error_at関数を呼び出して、メッセージを出力し処理を終了します。

着目しているトークンを次のトークンに変更する(変更なし)
token = token->next;

expect_number関数

https://github.com/rui314/chibicc/commit/c6ff1d98a1419e69c31902447e2caa85af4e9844#diff-a0cb465674c1b01a07d361f25a0ef2b0214b7dfe9412b7777f89add956da10ecR70
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/main.c#L70

int expect_number() {
  if (token->kind != TK_NUM)
    error_at(token->str, "expected a number");
  int val = token->val;
  token = token->next;
  return val;
}
現在着目しているトークンの種類が整数であることを確認する
  if (token->kind != TK_NUM)
    error_at(token->str, "expected a number");

現在着目しているトークンの型がTK_NUMではない場合(トークンの種類が整数ではない場合)、error_at関数を呼び出して、メッセージを出力し処理を終了します。

着目しているトークンを次のトークンに変更する(変更なし)
  int val = token->val;
  token = token->next;
  return val;

テストコード

test.sh
https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/test.sh

#!/bin/bash
assert() {
  expected="$1"
  input="$2"

  ./chibicc "$input" > tmp.s
  gcc -static -o tmp tmp.s
  ./tmp
  actual="$?"

  if [ "$actual" = "$expected" ]; then
    echo "$input => $actual"
  else
    echo "$input => $expected expected, but got $actual"
    exit 1
  fi
}

assert 0 0
assert 42 42
assert 21 '5+20-4'
assert 41 ' 12 + 34 - 5 '

echo OK

Makefile

https://github.com/rui314/chibicc/blob/c6ff1d98a1419e69c31902447e2caa85af4e9844/Makefile

CFLAGS=-std=c11 -g -static

chibicc: main.o
	$(CC) -o $@ $? $(LDFLAGS)

test: chibicc
	./test.sh

clean:
	rm -f chibicc *.o *~ tmp*

.PHONY: test clean