rsp

C コンパイラをつくってみる (2)

低レイヤを知りたい人のためのCコンパイラ作成入門 ステップ1-3を参考にしています。

前回 (1)

次回 (3)

整数1つからなる言語

前回の、整数1つを a0 レジスタに入れて OS に返すアセンブリをもとにする。

ソースコード(ここではコマンドライン引数)として整数1つを受け取り、それを返すことのできる RISC-V アセンブリを出力する。

  std::cout <<
    ".global main\n"
    "\n"
    "main:\n"
    "  li   a0, " << argv[1] << "\n"
    "  ret\n" << std::endl;

こんな感じにした。

全体はこちら

整数の加算・減算

この言語の機能に、加算と減算を入れる。

  • はじめに1つ整数を読み込み、それを a0 レジスタに入れる。
  • つづいて、 +- を期待し、その後続く整数を a0 レジスタに加えたり引いたりする。

加算と減算には、 RV32I の即値加算 addi を使う。即値減算はない *1

// p は、ソースとなる文字列中の文字へのポインタ
if(*p == '+'){
  p++;
  std::cout << "  addi  a0, a0, " << std::strtol(p, &p, 10) << "\n";
  continue;
}
if(*p == '-'){
  p++;
  std::cout << "  addi  a0, a0, -" << std::strtol(p, &p, 10) << "\n";
  continue;
}

差分はこちら

Tokenizer を導入する

空白が構文に影響しないように、整数を1つ、 + のようなオペレータを1つといった単位(トークン)として扱うようにする。

上記の加算・減算もトークン単位で扱う。

std::vector<Token> tokenize(char const* p){
  std::vector<Token> tokens;

  while(*p){
    if(isspace(*p)){
      p++;
      continue;
    }

    if(*p == '+'){
      Token token;
      token.type = TokenType::OPERATOR_PLUS;
      token.input = p;
      tokens.push_back(token);
      p++;
      continue;
    }
    ....

トークンを消費する(アセンブリを吐く)ほうはこんな感じ

for(size_t i=1; i<tokens.size() && tokens.at(i).type != TokenType::EOS; ){
  if(tokens.at(i).type == TokenType::OPERATOR_PLUS){
    i++;
    if(tokens.at(i).type != TokenType::NUMBER){
      error("予期しないトークン: %s", tokens.at(i).input);
    }
    std::cout << "  addi a0, a0, " << tokens.at(i).value << "\n";
    i++;
    continue;
  }
  ....

10 + 13 - 4 のように空白を含んでいてもコンパイルできるようになった。

差分はこちら

参考文献

*1:「即値は常に符号拡張される。必要なら、負の値を取れるようにするためである。したがって sub の即値版は必要ない」 RISC-V 原典 p.21