Closed25

chibicc ソースコードリーディング

yubrotyubrot

2020/12/07のものを読んでいく。

$ g rev-parse @
90d1f7f199cc55b13c7fdb5839d1409806633fdb
$ wc -l *.{c,h}
  1595 codegen.c
   165 hashmap.c
   791 main.c
  3368 parse.c
  1208 preprocess.c
    31 strings.c
   805 tokenize.c
   307 type.c
   189 unicode.c
   457 chibicc.h
  8916 total

Rust上でのコンパイラ実装のための学習を目的としているので、いくつかのファイルはさらっと流していく...というよりmain.cとcodegen.c以外はさらっと流していく。

yubrotyubrot

hashmap.c

typedef struct {
  char *key;
  int keylen;
  void *val;
} HashEntry;

typedef struct {
  HashEntry *buckets;
  int capacity;
  int used;
} HashMap;

void *hashmap_get(HashMap *map, char *key);
void *hashmap_get2(HashMap *map, char *key, int keylen);
void hashmap_put(HashMap *map, char *key, void *val);
void hashmap_put2(HashMap *map, char *key, int keylen, void *val);
void hashmap_delete(HashMap *map, char *key);
void hashmap_delete2(HashMap *map, char *key, int keylen);
void hashmap_test(void);

ハッシュマップの実装。ハッシュ関数にFNVを使っていてシンプル。

yubrotyubrot

strings.c

typedef struct {
  char **data;
  int capacity;
  int len;
} StringArray;

void strarray_push(StringArray *arr, char *s);
char *format(char *fmt, ...) __attribute__((format(printf, 1, 2)));

growableな文字列の配列。イテレートは直接forループで行ってるようだ。

for (int i = 0; i < include_paths.len; i++) {
  char *data = include_paths.data[i];
  ...
}
yubrotyubrot

unicode.c

int encode_utf8(char *buf, uint32_t c);
uint32_t decode_utf8(char **new_pos, char *p);
bool is_ident1(uint32_t c);
bool is_ident2(uint32_t c);
int display_width(char *p, int len);

C11は識別子に特定の範囲のユニコード文字を許容するので、そのための実装がある。ソースコードはUTF-8で記述される (これは最近のプログラミング言語で最も一般的だろう)。

yubrotyubrot

preprocess.c

CPPについては本筋から外れるので飛ばした

yubrotyubrot

token.c

// Token
typedef enum {
  TK_IDENT,   // Identifiers
  ...
  TK_EOF,     // End-of-file markers
} TokenKind;

typedef struct {
  char *name;
  int file_no;
  char *contents;
  ...
} File;

// Token type
typedef struct Token Token;
struct Token {
  TokenKind kind;   // Token kind
  Token *next;      // Next token
  int64_t val;      // If kind is TK_NUM, its value
  long double fval; // If kind is TK_NUM, its value
  char *loc;        // Token location
  int len;          // Token length
  Type *ty;         // Used if TK_NUM or TK_STR
  ...
};

...
bool equal(Token *tok, char *op);
Token *skip(Token *tok, char *op);
bool consume(Token **rest, Token *tok, char *str);
void convert_pp_tokens(Token *tok);
File **get_input_files(void);
File *new_file(char *name, int file_no, char *contents);
Token *tokenize_string_literal(Token *tok, Type *basety);
Token *tokenize(File *file);
...

READMEにもある通りunionを使ってないのが少し特徴的。
chibiccでは T *next; の形のリンクリストを用いたデータ構造が各所で使われているようで、 struct TokenToken *next をフィールドに持ち、 tokenize(File *file) も返値は単に Token* である。

yubrotyubrot

type.c

typedef enum {
  TY_VOID,
  TY_BOOL,
  ...
  TY_STRUCT,
  TY_UNION,
} TypeKind;

struct Type {
  TypeKind kind;
  int size;           // sizeof() value
  int align;          // alignment
  ...

  // Pointer-to or array-of type. We intentionally use the same member
  // to represent pointer/array duality in C.
  // ...
  Type *base;

  // Declaration
  Token *name;
  Token *name_pos;

  // Array
  int array_len;
  ...

  // Struct
  Member *members;
  bool is_flexible;
  bool is_packed;

  // Function type
  Type *return_ty;
  Type *params;
  bool is_variadic;
  Type *next;
};

// Struct member
struct Member {
  Member *next;
  Type *ty;
  Token *tok; // for error message
  Token *name;
  ...

  // Bitfield
  ...
};

extern Type *ty_void;
extern Type *ty_bool;
...

bool is_integer(Type *ty);
bool is_flonum(Type *ty);
bool is_numeric(Type *ty);
bool is_compatible(Type *t1, Type *t2);
Type *copy_type(Type *ty);
Type *pointer_to(Type *base);
Type *func_type(Type *return_ty);
Type *array_of(Type *base, int size);
Type *vla_of(Type *base, Node *expr);
Type *enum_type(void);
Type *struct_type(void);
void add_type(Node *node);

型。 Type という大きな構造体が一つ。型の全ての情報はこの構造体からアクセス可能なようだ。
例えば struct_type が以下のように実装されていて、ユニークなIDを振ったりする実装が無いことから、型の同一性はポインタ Type* の一致で判定される。

Type *struct_type(void) {
  return new_type(TY_STRUCT, 0, 1);
}

Cの仕様で compatible がどういう意味なのかは調べてないが、二つの型が互換かどうかの判定は bool is_compatible(Type *t1, Type *t2) で実装されている。この関数は、

  1. まず同一性をチェック (同一ならtrue)
  2. origin (次に追ってみる) ポインタを辿る
  3. t1->kind != t2->kind ならfalse
  4. t1->kind に基づいて、 base return_ty params といった複合型の構成要素を is_compatible で再帰的に判定
    • なお、Cのstruct等に構造的型付けはないため、この時点でstruct同士の場合はfalseとなる

Type *copy_type(Type *ty) は、calloc、Type構造体のフィールドを完全にコピーしたのち origin に引数の型を指すように設定した新しい型を返している。 struct T { .. }; struct T a; struct T b; のようなプログラムで呼ばれるのかと思うので覚えておく (ネタバレするとparse.cで呼ばれている)。

add_typeNode 型をまだ追ってないのでTODO。

yubrotyubrot

parse.c

chibiccで一番大きい。手書きの再帰下降型パーサ。ASTの構築に加え、各ASTノードに型情報の付与を行っている (READMEより)。

// Variable or function
typedef struct Obj Obj;
struct Obj {
  Obj *next;
  char *name;    // Variable name
  Type *ty;      // Type
  Token *tok;    // representative token
  bool is_local; // local or global/function
  int align;     // alignment

  // Local variable
  int offset;

  // Global variable or function
  bool is_function;
  bool is_definition;
  bool is_static;

  // Global variable
  bool is_tentative;
  bool is_tls;
  char *init_data;
  Relocation *rel;

  // Function
  bool is_inline;
  Obj *params;
  Node *body;
  Obj *locals;
  Obj *va_area;
  Obj *alloca_bottom;
  int stack_size;
  ...
};

// Global variable can be initialized either by a constant expression
// or a pointer to another global variable. This struct represents the
// latter.
typedef struct Relocation Relocation;
struct Relocation {
  Relocation *next;
  int offset;
  char **label;
  long addend;
};

グローバル/ローカル問わず、変数または関数を表現する型。 Obj *next とあるようにリンクリストであり、 parse(Token *tok) の返値も Obj* となっていた。Functionの Node *body についてはすぐ下に。Global variableの char *init_data には面食らったが、グローバル変数の定数式は単にバイナリデータの形まで parse.c で落としてしまうらしい。

yubrotyubrot
// AST node
typedef enum {
  ND_NULL_EXPR, // Do nothing
  ND_ADD,       // +
  ND_SUB,       // -
  ...
} NodeKind;

// AST node type
struct Node {
  NodeKind kind; // Node kind
  Node *next;    // Next node
  Type *ty;      // Type, e.g. int or pointer to int
  Token *tok;    // Representative token

  Node *lhs;     // Left-hand side
  Node *rhs;     // Right-hand side

  // "if" or "for" statement
  Node *cond;
  Node *then;
  Node *els;
  Node *init;
  Node *inc;

  // "break" and "continue" labels
  char *brk_label;
  char *cont_label;

  // Block or statement expression
  Node *body;

  // Struct member access
  Member *member;

  // Function call
  Type *func_ty;
  Node *args;
  bool pass_by_stack;
  Obj *ret_buffer;

  ...

  // Variable
  Obj *var;

  // Numeric literal
  int64_t val;
  long double fval;
};

Node *new_cast(Node *expr, Type *ty);
int64_t const_expr(Token **rest, Token *tok);
Obj *parse(Token *tok);

Obj構造体でFunctionのbodyが Node *body と表現されているように、関数の本体などのプログラムはNode構造体のツリー(AST)で表現される。そのため NodeKind+return といったCの式や文の構成単位が列挙されている。
各フィールドの使われ方についてはparse.c本体をもう少し追ってみる。

yubrotyubrot

parse.c (実装本体)

static Obj *globals static Obj *locals といったグローバル変数があり、これをテンポラリとして用い、 parse(Token *tok) も最後に return globals; してる辺りは普段プログラミングしていて守っている領域から見ると驚いた。リエントラントでないので、もちろん並行に呼ぶことはできない。例えば、関数のパース中 (function 内) に呼ばれる new_gvarObj を生成して globals のリンクリストに加えている:

static Obj *new_var(char *name, Type *ty) {
  Obj *var = calloc(1, sizeof(Obj));
  var->name = name;
  var->ty = ty;
  var->align = ty->align;
  push_scope(name)->var = var;
  return var;
}
...
static Obj *new_gvar(char *name, Type *ty) {
  Obj *var = new_var(name, ty);
  var->next = globals;
  var->is_static = true;
  var->is_definition = true;
  globals = var;
  return var;
}

var->ty が設定されているように、型はパースと同時に ObjNode に設定されていくようだ。

static Type *declspec(Token **rest, Token *tok, VarAttr *attr) にて:

switch (counter) {
  ...
case LONG:
case LONG + INT:
case LONG + LONG:
case LONG + LONG + INT:
case SIGNED + LONG:
case SIGNED + LONG + INT:
case SIGNED + LONG + LONG:
case SIGNED + LONG + LONG + INT:
  ty = ty_long;
  break;
...
}

このswitch-caseはなるほどなあ。

  if (is_atomic) {
    ty = copy_type(ty);
    ty->is_atomic = true;
  }

前述の copy_type を使ってる例。Cの _Atomic はよく知らないが型修飾子というらしく、確かに _Atomic 関係ないところでは修飾無しの型と互換っぽい...
ただ、単に copy_type しているところなど copy_type の勘所はいまいちわかっていない。


fn->body = compound_stmt(&tok, tok);

関数本体のルートは compound_stmt{ stmt stmt } みたいのをCでは複合ステートメントと言うんですね...と初めて知った。

static Node *compound_stmt(Token **rest, Token *tok) {
  Node *node = new_node(ND_BLOCK, tok);
  Node head = {};
  Node *cur = &head;

  enter_scope();

  while (!equal(tok, "}")) {
    if (is_typename(tok) && !equal(tok->next, ":")) {
      Type *basety = declspec(&tok, tok, &attr);
      ...
      cur = cur->next = declaration(&tok, tok, basety, &attr);
    } else {
      cur = cur->next = stmt(&tok, tok);
    }
    add_type(cur);
  }

  leave_scope();

  node->body = head.next;
  *rest = tok->next;
  return node;
}

ND_BLOCK ノードの body フィールドに、 next チェインによるリンクリストの形で宣言(declaration)または文(stmt)からなるNodeの列が構築されている。一連の実装を見ても、字句的に隣り合わせのノードを next にセット... みたいな使われ方はしないと考えてよいだろう。同様に例えば Node *lhs Node *rhs なども、2項演算子のときに用いられるフィールドのようだ:

static Node *new_binary(NodeKind kind, Node *lhs, Node *rhs, Token *tok) {
  Node *node = new_node(kind, tok);
  node->lhs = lhs;
  node->rhs = rhs;
  return node;
}

少し戻って、 compound_stmt で宣言または文のパース後に add_type(Node *node) が呼ばれている。ここの他、 add_type はparse.cのあちこちで呼ばれている。例えば new_add(Node *lhs, Node *rhs, Token *tok) のような関数の冒頭で呼ばれているので、(C言語の+演算子はオーバーロードされているため) 型が確定していて欲しい/確定できるタイミングで呼ばれているのだろうか? type.cの add_type の実装に入ろう。

void add_type(Node *node) {
  if (!node || node->ty) return;

  add_type(node->lhs);
  add_type(node->rhs);
  add_type(node->cond);
  add_type(node->then);
  add_type(node->els);
  add_type(node->init);
  add_type(node->inc);

  for (Node *n = node->body; n; n = n->next) add_type(n);
  for (Node *n = node->args; n; n = n->next) add_type(n);

  switch (node->kind) {
  case ND_NUM:
    node->ty = ty_int;
    return;
  case ND_ADD:
  case ND_SUB:
  case ND_MUL:
  case ND_DIV:
  case ND_MOD:
  case ND_BITAND:
  case ND_BITOR:
  case ND_BITXOR:
    usual_arith_conv(&node->lhs, &node->rhs);
    node->ty = node->lhs->ty;
    return;
  ...
  case ND_ASSIGN:
    if (node->lhs->ty->kind == TY_ARRAY) error_tok(node->lhs->tok, "not an lvalue");
    if (node->lhs->ty->kind != TY_STRUCT) node->rhs = new_cast(node->rhs, node->lhs->ty);
    node->ty = node->lhs->ty;
    return;
  ...
  case ND_ADDR: {
    Type *ty = node->lhs->ty;
    if (ty->kind == TY_ARRAY) node->ty = pointer_to(ty->base);
    else node->ty = pointer_to(ty);
    return;
  }
  case ND_DEREF:
    if (!node->lhs->ty->base) error_tok(node->tok, "invalid pointer dereference");
    if (node->lhs->ty->base->kind == TY_VOID) error_tok(node->tok, "dereferencing a void pointer");
    node->ty = node->lhs->ty->base;
    return;
  ...
  }
}

switchは長いのでいくつかのcaseのみ例として残した。あるノードの子ノードを再帰的に add_type して型を決定した上で、 node->kind に基づいてlhs, rhs等との関係からこのノード自身の型を決定しているようだ。 ND_ASSIGN の例やここから呼び出している usual_arith_conv の実装を見ると、暗黙のキャストによるツリー構造の書き換えも起きうる。

yubrotyubrot

main.c

エントリポイントからコードを追ってみる。

int main(int argc, char **argv) {
  ...
  parse_args(argc, argv);

  if (opt_cc1) {
    add_default_include_paths(argv[0]);
    cc1();
    return 0;
  }

  ...
  StringArray ld_args = {};

  for (int i = 0; i < input_paths.len; i++) {
    char *input = input_paths.data[i];

    if (!strncmp(input, "-l", 2)) {
      strarray_push(&ld_args, input);
      continue;
    }
    ...

    char *output = opt_o ? opt_o : replace_extn(input, ".o");
    FileType type = get_file_type(input);

    // Handle .o or .a
    if (type == FILE_OBJ || type == FILE_AR || type == FILE_DSO) {
      strarray_push(&ld_args, input);
      continue;
    }

    // Handle .s
    if (type == FILE_ASM) {
      assemble(input, output);
      continue;
    }

    assert(type == FILE_C);
    ...

    // Compile, assemble and link
    char *tmp1 = create_tmpfile();
    char *tmp2 = create_tmpfile();
    run_cc1(argc, argv, input, tmp1);
    assemble(tmp1, tmp2);
    strarray_push(&ld_args, tmp2);
    continue;
  }

  if (ld_args.len > 0) run_linker(&ld_args, opt_o ? opt_o : "a.out");
  return 0;
}
  • parse_args によってグローバル変数 opt_XXX が引数次第でセットされる。
  • opt_cc1 が面白いところ。 cc1 オプション及び cc1() 関数があると覚えておこう。後述。
  • リンカに渡す引数群を ld_args に集める。ここで、ファイルの拡張子に応じてアセンブル (assemble()) やコンパイル (run_cc1()) が行われる。
  • 最後に run_linker でリンクする。

主要な関数を assemble run_linker run_cc1 cc1 の順に見ていく。

yubrotyubrot
static void assemble(char *input, char *output) {
  char *cmd[] = {"as", "-c", input, "-o", output, NULL};
  run_subprocess(cmd);
}

assemble はシンプルにGNU asを呼び出す。 run_subprocess はfork-execを行う。

yubrotyubrot
static void run_linker(StringArray *inputs, char *output) {
  StringArray arr = {};

  strarray_push(&arr, "ld");
  strarray_push(&arr, "-o");
  strarray_push(&arr, output);
  strarray_push(&arr, "-m");
  strarray_push(&arr, "elf_x86_64");

  if (opt_shared) {
    strarray_push(&arr, format("%s/crti.o", libpath));
    strarray_push(&arr, format("%s/crtbeginS.o", gcc_libpath));
  } else {
    strarray_push(&arr, format("%s/crt1.o", libpath));
    strarray_push(&arr, format("%s/crti.o", libpath));
    strarray_push(&arr, format("%s/crtbegin.o", gcc_libpath));
  }

  ...

  for (int i = 0; i < inputs->len; i++) strarray_push(&arr, inputs->data[i]);

  ...

  if (opt_shared)
    strarray_push(&arr, format("%s/crtendS.o", gcc_libpath));
  else
    strarray_push(&arr, format("%s/crtend.o", gcc_libpath));

  strarray_push(&arr, format("%s/crtn.o", libpath));
  strarray_push(&arr, NULL);

  run_subprocess(arr.data);
}

run_linker も同様にGNU ldを呼び出す。ldを直に呼び出した経験に乏しく、この辺を自分で無から実装するとgccのverbose outputとかの見様見真似になりそうだ。

yubrotyubrot
static void run_cc1(int argc, char **argv, char *input, char *output) {
  char **args = calloc(argc + 10, sizeof(char *));
  memcpy(args, argv, argc * sizeof(char *));
  args[argc++] = "-cc1";

  if (input) {
    args[argc++] = "-cc1-input";
    args[argc++] = input;
  }

  if (output) {
    args[argc++] = "-cc1-output";
    args[argc++] = output;
  }

  run_subprocess(args);
}

run_cc1 は面白い。カレントプロセスのコマンドライン引数を引数に取り、その配列に -cc1 -cc1-input .. -cc1-output .. を加えた形で自分自身を呼び出す。gccの場合は裏でサブプログラム cc1 を呼び出すとされているが、chibiccでは自分自身が -cc1 オプションを処理することで (opt_cc1) cc1 相当の動作に切り替えている。エントリポイントの該当部分を再掲。


  if (opt_cc1) {
    add_default_include_paths(argv[0]);
    cc1();
    return 0;
  }
yubrotyubrot
static void cc1(void) {
  Token *tok = NULL;

  // Process -include option
  for (int i = 0; i < opt_include.len; i++) {
    ...
  }

  // Tokenize and parse.
  Token *tok2 = must_tokenize_file(base_file);
  tok = append_tokens(tok, tok2);
  tok = preprocess(tok);
  ...

  Obj *prog = parse(tok);

  // Open a temporary output buffer.
  char *buf;
  size_t buflen;
  FILE *output_buf = open_memstream(&buf, &buflen);

  // Traverse the AST to emit assembly.
  codegen(prog, output_buf);
  fclose(output_buf);

  // Write the asembly text to a file.
  FILE *out = open_file(output_file);
  fwrite(buf, buflen, 1, out);
  fclose(out);
}
chibicc.h
void codegen(Obj *prog, FILE *out);

コンパイル処理本体。入力のプログラムを Token のリンクリストに変換し、 preprocess でCプリプロセス、 parseObj のリンクリストを得、 codegen する。ということで本題のコード生成まで辿り着いた。

yubrotyubrot

codegen.c

codegen.c はソースコードリーディング中のところ1600行。とはいえコード生成には定型処理も沢山あるかと思うので気張らず読んでいく。

void codegen(Obj *prog, FILE *out) {
  output_file = out;

  File **files = get_input_files();
  for (int i = 0; files[i]; i++)
    println("  .file %d \"%s\"", files[i]->file_no, files[i]->name);

  assign_lvar_offsets(prog);
  emit_data(prog);
  emit_text(prog);
}

codegenは3ステップ。 assign_lvar_offsets, emit_data, emit_text

yubrotyubrot

実装を読む前に、コンパイラが準拠するSystem V AMD64 ABIのFunction Calling Sequenceを読んでおいたほうが良さそうだ。この資料何度か読んでいるのに頭に全く残っていないのでいい加減まとめよう...

System V AMD64 ABI メモ

レジスタ

  • レジスタ rbx, rbp, rsp, r12..r15 はcallee-saved (non-volatile): 関数は呼び出された後にこれらが保持されていることを保証しなければらない
  • それ以外のレジスタはcaller-saved (volatile): 関数が呼び出された後にこれらが保持されていることは保証されない

スタックフレーム

  • スタックは 高位のアドレスから低位のアドレスへと延びる
  • rsp は常に一番上 (メモリアドレス上は低位) のスタックフレームの終端を指す
  • 関数のエントリポイントに制御が移るとき、
    • rsp には call によってpushされたreturn addressがあり
    • rsp + 8 は常に16 bytes-aligned (つまり call されるときの rsp は常に16 bytes-aligned)
  • 各関数は、基本的に以下のように rbp を用いる (See also: -fomit-frame-pointer)
    • prologueで
      1. push %rbp (rbp をスタックに退避)
      2. mov %rsp, %rbp (rsp (現在のスタックフレームの始点 - 16) を rbp にセット):
        このようにすると、図のように rbp からの相対位置で前回のスタックフレーム上の引数や現在のスタックフレーム上のローカル変数にアクセスできる
    • epilogueで
      1. mov %rbp, %rsp (rsp をprologueの push %rbp 直後の状態に復元)
      2. pop %rbp (rbp を復元)
      3. ret

引数の受け渡し

以下、仕様書には書かれている以下の要素について考慮していない:

  • 分類 SSEUP, X87, X87UP, COMPLEX_X87 と関連する型や振る舞い
  • Decimal型
  • __m256, __m512, __int128, ...
  • C++ object
  • 可変超引数

引数は大雑把には以下のような方針に基づいて渡される。

  1. 引数が適するレジスタがあるならレジスタを使って渡す。 (left-to-right)
  2. 引数が適するレジスタが無かったりレジスタが埋まっている場合はスタック経由で渡す。 (right-to-left)

「引数が適するレジスタ」といったように、値の分類(classification)がある。

  • INTEGER
    • 数値型の分類。汎用レジスタが適合する。 _Boolchar, 64bitまでの整数型やポインタがこれに分類される。
    • この分類の値の引数はレジスタ rdi rsi ... を順に使用して渡される。
  • SSE
    • SSEに適合する型の分類。 floatdouble がこれ。
    • この分類の値の引数はレジスタ xmm0 から xmm7 を順に使用して渡される。
  • NO_CLASS
    • パディングや空の構造体など。有意な値がないため、渡す処理が必要ない。
  • MEMORY
    • 適切なレジスタが無い。スタックメモリ領域を経由して渡される。

なお、引数のサイズは8バイト単位にround-upされるため、スタックは常に8バイト単位でアラインされる。

structやunionはどのように渡されるだろうか? 以下の手順で決定される:

  1. サイズが16バイトを超える場合やunalignedなフィールドを持つ場合、全体がメモリ渡しされる (MEMORY)。
  2. データを8バイト単位 (eightbyte) に分割し、それぞれを以下のように分類し、その分類に基づいた上述の渡し方渡す。
    1. eightbyteの位置に含まれるstructやunionの各フィールドについて、再帰的に分類を計算する
    2. eightbyteの位置に含まれるstructやunionの各フィールドの分類から、最も優先度の高い分類をそのeightbyteの分類として決定する:
      優先度は INTEGER > SSE > NO_CLASS (default)
  3. ただし、以下の場合はeightbyte単位では渡さず、やはり全体がメモリ渡しされる。
    • いずれかのフィールドの分類が MEMORY
    • eightbyteのいずれかがレジスタに収まらない (このとき、アサインされたレジスタは予約済みとなる)

具体例をいくつか見てみる。

struct struct_a {
  int a;
  float b;
};

int a;
float b;

int test_struct_a(struct struct_a p) {
           // movq %rdi, -8(%rbp)
  a = p.a; // movl -8(%rbp), %eax
           // movl %eax, a(%rip)
  b = p.b; // movss -4(%rbp), %xmm0
           // movss %xmm0, b(%rip)
}

struct_a は全体で8バイトなので、1 eightbyte。このeightbyteはintとfloatをフィールドに持つので優先度から INTEGER に分類され、レジスタ rdi で渡されている。

struct struct_b {
  struct {
    int x;
    short y, z;
  } a;
  struct {
    float x, y;
  } b;
};

int a;
float b;

int test_struct_b(struct struct_b p) {
             // movq %rdi, %rax
             // movq %xmm0, %rcx
             // movq %rcx, %rdx
             // movq %rax, -16(%rbp) // rdi -> rax -> [rbp-16]
             // movq %rdx, -8(%rbp)  // xmm0 -> rcx -> rdx -> [rbp-8]
  a = p.a.x; // movl -16(%rbp), %eax
             // movl %eax, a(%rip)
  a = p.a.y; // movzwl -12(%rbp), %eax
             // cwtl
             // movl %eax, a(%rip)
  a = p.a.z; // movzwl -10(%rbp), %eax
             // cwtl
             // movl %eax, a(%rip)
  b = p.b.x; // movss -8(%rbp), %xmm0
             // movss %xmm0, b(%rip)
  b = p.b.y; // movss -4(%rbp), %xmm0
             // movss %xmm0, b(%rip)
}

struct_b は全体で16バイトなので、2 eightbyte。 struct { .. } a 部分は rdi で、 struct { .. } b 部分は xmm0 で渡されている。

struct struct_c {
  double x, y, z;
};

double a;

int test_struct_c(struct struct_c p) {
  a = p.x; // movsd 16(%rbp), %xmm0
           // movsd %xmm0, a(%rip)
  a = p.y; // movsd 24(%rbp), %xmm0
           // movsd %xmm0, a(%rip)
  a = p.z; // movsd 32(%rbp), %xmm0
           // movsd %xmm0, a(%rip)
}

引数全体がスタックメモリ経由で渡されている。引数 p 内の各eightbyteもright-to-leftでpushされている (右端の z が一番最初にpushされている) ことがわかる。

返値の受け渡し

  1. 引数と同様に分類を行う。
  2. MEMORY の場合、呼び出し側は返値のための空間を確保し、そのアドレスを隠し引数のように rdi にセットする。返却時はそのアドレスを rax にセットする。
    • 以下のステップは、structやunionで MEMORY でない場合それぞれのeightbyteについて適用できる。
  3. INTEGER の場合、返値を rax, rdx に順にセットする。
  4. SSE の場合、返値を xmm0, xmm1 に順にセットする。
yubrotyubrot

(改めて) codegen.c

assign_lvar_offsets

static void assign_lvar_offsets(Obj *prog) {
  for (Obj *fn = prog; fn; fn = fn->next) {
    if (!fn->is_function) continue;
    ...
  }
}

以下の処理は各関数について。

    // If a function has many parameters, some parameters are
    // inevitably passed by stack rather than by register.
    // The first passed-by-stack parameter resides at RBP+16.
    int top = 16;
    int bottom = 0;

    int gp = 0, fp = 0;

    // Assign offsets to pass-by-stack parameters.
    for (Obj *var = fn->params; var; var = var->next) {
      Type *ty = var->ty;

      switch (ty->kind) {
      case TY_STRUCT:
      case TY_UNION:
        if (ty->size <= 16) {
          bool fp1 = has_flonum(ty, 0, 8, 0);
          bool fp2 = has_flonum(ty, 8, 16, 8);
          if (fp + fp1 + fp2 < FP_MAX && gp + !fp1 + !fp2 < GP_MAX) {
            fp = fp + fp1 + fp2;
            gp = gp + !fp1 + !fp2;
            continue;
          }
        }
        break;
      case TY_FLOAT:
      case TY_DOUBLE:
        if (fp++ < FP_MAX) continue;
        break;
      case TY_LDOUBLE:
        break;
      default:
        if (gp++ < GP_MAX) continue;
      }

      top = align_to(top, 8);
      var->offset = top;
      top += var->ty->size;
    }

ABI仕様を見た後だとわかりやすい。 GP_MAX6FP_MAX8 、それぞれレジスタ渡しできるGeneral-purposeレジスタの数と浮動小数点数を格納できるレジスタの数だろう。変数 fp gp に使用済みレジスタの数を記録していき、レジスタから溢れた引数はスタックから渡されるのでそのオフセットを var->offset にセットする。

    // Assign offsets to pass-by-register parameters and local variables.
    for (Obj *var = fn->locals; var; var = var->next) {
      if (var->offset) continue;

      // AMD64 System V ABI has a special alignment rule for an array of
      // length at least 16 bytes. We need to align such array to at least
      // 16-byte boundaries. See p.14 of
      // https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-draft.pdf.
      int align = (var->ty->kind == TY_ARRAY && var->ty->size >= 16)
        ? MAX(16, var->align) : var->align;

      bottom += var->ty->size;
      bottom = align_to(bottom, align);
      var->offset = -bottom;
    }

    fn->stack_size = align_to(bottom, 16);

レジスタ渡しされたパラメータ及びローカル変数は現在のスタックフレームに領域を確保しておく。16バイトでアラインした全長を fn->stack_size に記録しておく。

yubrotyubrot

emit_data

static void emit_data(Obj *prog) {
  for (Obj *var = prog; var; var = var->next) {
    if (var->is_function || !var->is_definition) continue;
    ...
  }
}

以下の処理は is_definition な変数について。

    if (var->is_static)
      println("  .local %s", var->name);
    else
      println("  .globl %s", var->name);

    int align = (var->ty->kind == TY_ARRAY && var->ty->size >= 16)
      ? MAX(16, var->align) : var->align;

    // Common symbol
    if (opt_fcommon && var->is_tentative) {
      println("  .comm %s, %d, %d", var->name, var->ty->size, align);
      continue;
    }

    // .data or .tdata
    if (var->init_data) {
      ... // (後述)
      continue;
    }

    // .bss or .tbss
    if (var->is_tls)
      println("  .section .tbss,\"awT\",@nobits");
    else
      println("  .bss");

    println("  .align %d", align);
    println("%s:", var->name);
    println("  .zero %d", var->ty->size);

LLVMのLinkage Typesで見たような用語が並ぶなあ...という感じ。今更だがこの辺のディレクティブが全然わからない。

注意して見ておきたいのは初期化データがある場合のコード。

    // .data or .tdata
    if (var->init_data) {
      if (var->is_tls) println("  .section .tdata,\"awT\",@progbits");
      else println("  .data");

      println("  .type %s, @object", var->name);
      println("  .size %s, %d", var->name, var->ty->size);
      println("  .align %d", align);
      println("%s:", var->name);

      Relocation *rel = var->rel;
      int pos = 0;
      while (pos < var->ty->size) {
        if (rel && rel->offset == pos) {
          println("  .quad %s%+ld", *rel->label, rel->addend);
          rel = rel->next;
          pos += 8;
        } else {
          println("  .byte %d", var->init_data[pos++]);
        }
      }
      continue;
    }

var->init_data を走査して .byte を書き込みつつ、別のグローバル変数への参照がある場所では .quad でそれを書き込む必要がある。これら var->init_datavar->rel の構築についてはparse.cの gvar_initializer あたりを参照。このあたりは自作言語でネイティブバックエンドを追加する事前準備として行った変更で加えた以下のようなトレイトと似ていて面白みを感じる:

llrl0/src/backend/ee.rs
pub trait EeData {
    fn direct_data(&self) -> &[u8];
    fn traverse_indirect_data(&self, f: &mut dyn FnMut(usize, &dyn EeData));
    ...
}
yubrotyubrot

emit_text

プログラムのセクションが配置される .text セクションの生成。つまりコンパイラのコード生成の核。

static void emit_text(Obj *prog) {
  for (Obj *fn = prog; fn; fn = fn->next) {
    if (!fn->is_function || !fn->is_definition) continue;

    // No code is emitted for "static inline" functions
    // if no one is referencing them.
    if (!fn->is_live) continue;

    ...
  }
}

以下の処理は is_definition な関数について。

    if (fn->is_static) println("  .local %s", fn->name);
    else println("  .globl %s", fn->name);

    println("  .text");
    println("  .type %s, @function", fn->name);
    println("%s:", fn->name);
    current_fn = fn;

    // Prologue
    println("  push %%rbp");
    println("  mov %%rsp, %%rbp");
    println("  sub $%d, %%rsp", fn->stack_size);
    println("  mov %%rsp, %d(%%rbp)", fn->alloca_bottom->offset);

    // Save arg registers if function is variadic
    if (fn->va_area) { ... } // 興味が無いので省略

関数のprologue。ABIで見た一般的な形と同様に push %rbp して mov %rsp, %rbp する。その後、あらかじめ計算しておいた stack_sizersp から引き、その時点の rspalloca_bottom という内部で常に確保されているローカル変数にストアする (この変数は alloca のサポートに使われているようだ)。

    // Save passed-by-register arguments to the stack
    int gp = 0, fp = 0;
    for (Obj *var = fn->params; var; var = var->next) {
      if (var->offset > 0) continue;

      Type *ty = var->ty;

      switch (ty->kind) {
      case TY_STRUCT:
      case TY_UNION:
        assert(ty->size <= 16);
        if (has_flonum(ty, 0, 8, 0)) store_fp(fp++, var->offset, MIN(8, ty->size));
        else store_gp(gp++, var->offset, MIN(8, ty->size));

        if (ty->size > 8) {
          if (has_flonum(ty, 8, 16, 0)) store_fp(fp++, var->offset + 8, ty->size - 8);
          else store_gp(gp++, var->offset + 8, ty->size - 8);
        }
        break;
      case TY_FLOAT:
      case TY_DOUBLE:
        store_fp(fp++, var->offset, ty->size);
        break;
      default:
        store_gp(gp++, var->offset, ty->size);
      }
    }

レジスタ渡しされた引数だが、chibiccではいったんスタックにストアする。 store_fp(r, offset, size) store_gp(r, offset, size) はレジスタ渡しされたr番目のレジスタの値を {offset}(%rbp) にmovする。

    // Emit code
    gen_stmt(fn->body);
    assert(depth == 0);

    // [https://www.sigbus.info/n1570#5.1.2.2.3p1] The C spec defines
    // a special rule for the main function. Reaching the end of the
    // main function is equivalent to returning 0, even though the
    // behavior is undefined for the other functions.
    if (strcmp(fn->name, "main") == 0)
      println("  mov $0, %%rax");

    // Epilogue
    println(".L.return.%s:", fn->name);
    println("  mov %%rbp, %%rsp");
    println("  pop %%rbp");
    println("  ret");

そうして関数本体のコード生成 (gen_stmt) に入る。epilogueはABIで見た一般的な形と同様となっている。

yubrotyubrot

以下はおおまかな構成を追ってみる。

static void gen_stmt(Node *node) {
  println("  .loc %d %d", node->tok->file->file_no, node->tok->line_no);

  switch (node->kind) {
  case ND_IF: {
    int c = count();
    gen_expr(node->cond);
    cmp_zero(node->cond->ty);
    println("  je  .L.else.%d", c);
    gen_stmt(node->then);
    println("  jmp .L.end.%d", c);
    println(".L.else.%d:", c);
    if (node->els) gen_stmt(node->els);
    println(".L.end.%d:", c);
    return;
  }
  ...
  }

  error_tok(node->tok, "invalid statement");
}

gen_stmt は文字通りCの文に対応するコード生成。コードを見るといずれも返値が無く、スタックマシンっぽいコード生成をしている予想が付く。条件式を gen_expr して cmp_zero している。

static void cmp_zero(Type *ty) {
  switch (ty->kind) { ... } // TY_FLOAT, TY_DOUBLE, TY_LDOUBLEの場合

  if (is_integer(ty) && ty->size <= 4) println("  cmp $0, %%eax");
  else println("  cmp $0, %%rax");
}

cmp_zero を見ると rax に値がある前提となっている。大昔にやった
An Incremental Approach to Compiler Construction (yubrot/iatcc) の 3.4 Conditional Expressions っぽい。とするとバイナリ演算やレジスタに収まらない値はどうするのか...? gen_expr に進む。

yubrotyubrot

gen_expr のおおまかな構造は以下の通り。

static void gen_expr(Node *node) {
  println("  .loc %d %d", node->tok->file->file_no, node->tok->line_no);

  switch (node->kind) {
  ...
  case ND_NUM: {
    switch (node->ty->kind) {
    case TY_FLOAT: { ... }
    case TY_DOUBLE: { ... }
    case TY_LDOUBLE: { ... }
    }

    println("  mov $%ld, %%rax", node->val);
    return;
  }
  ...
  }

  switch (node->lhs->ty->kind) {
  case TY_FLOAT:
  case TY_DOUBLE: {
    gen_expr(node->rhs);
    pushf();
    gen_expr(node->lhs);
    popf(1);

    switch (node->kind) {
    case ND_ADD:
      println("  add%s %%xmm1, %%xmm0", sz);
      return;
    ...
    }

    error_tok(node->tok, "invalid expression");
  }
  case TY_LDOUBLE: { ... }
  }

  gen_expr(node->rhs);
  push();
  gen_expr(node->lhs);
  pop("%rdi");

  char *ax, *di, *dx;

  if (node->lhs->ty->kind == TY_LONG || node->lhs->ty->base) {
    ax = "%rax";
    di = "%rdi";
    dx = "%rdx";
  } else {
    ax = "%eax";
    di = "%edi";
    dx = "%edx";
  }

  switch (node->kind) {
  case ND_ADD:
    println("  add %s, %s", di, ax);
    return;
  ...
  }

  error_tok(node->tok, "invalid expression");
}

最初の switch (node->kind) { .. } ではバイナリ演算はスルーされ (各caseはearly returnしている)、switchの下で両端の gen_expr(..) からバイナリ演算のための switch (node->kind) { .. } が行われている。また、 switch (node->kind) の分岐と node->ty->kind または node->lhs->ty->kind の分岐がサンドイッチされている。

static void push(void) {
  println("  push %%rax");
  depth++;
}

static void pop(char *arg) {
  println("  pop %s", arg);
  depth--;
}
  gen_expr(node->rhs);
  push();
  gen_expr(node->lhs);
  pop("%rdi");

gen_expr の結果は rax レジスタにストアされていると考えられるのだった。バイナリ演算の場合は右辺の実行結果を push し、左辺を実行 (これも rax レジスタにストアされる)、右辺の結果を rdi レジスタにpopすることで rax <> rdi (<> はバイナリ演算子) の形にしている。これもAn Incremental Approach to Compiler Construction3.4 Binary Primitives っぽい。 TY_DOUBLE の場合も同様に xmm0 xmm1 を使用している。

static void pushf(void) {
  println("  sub $8, %%rsp");
  println("  movsd %%xmm0, (%%rsp)");
  depth++;
}

static void popf(int reg) {
  println("  movsd (%%rsp), %%xmm%d", reg);
  println("  add $8, %%rsp");
  depth--;
}

chibiccはスタックの上端を rax または xmm0 としたスタックマシン型のコードを生成しているようだ。

yubrotyubrot

左辺値やstruct/unionはどうなっているか。

  case ND_VAR:
    gen_addr(node);
    load(node->ty);
    return;

変数の読み込み。左辺値はそのままアドレスとして扱うようにしているようだ。ということは...?

  case ND_ASSIGN:
    gen_addr(node->lhs);
    push();
    gen_expr(node->rhs);
    if (node->lhs->kind == ND_MEMBER && node->lhs->member->is_bitfield) { ... }
    store(node->ty);
    return;

代入も左辺を gen_addr して、それから右辺を gen_expr して store を呼んでいる。いずれも構造体の値が結果となる場合の gen_expr についての分岐が無いなあと思ったら、 loadstore 側に分岐があった。

// Load a value from where %rax is pointing to.
static void load(Type *ty) {
  switch (ty->kind) {
  case TY_ARRAY:
  case TY_STRUCT:
  case TY_UNION:
  case TY_FUNC:
  case TY_VLA:
    // If it is an array, do not attempt to load a value to the
    // register because in general we can't load an entire array to a
    // register. As a result, the result of an evaluation of an array
    // becomes not the array itself but the address of the array.
    // This is where "array is automatically converted to a pointer to
    // the first element of the array in C" occurs.
    return;
  case TY_FLOAT: ...
  case TY_DOUBLE: ...
  case TY_LDOUBLE: ...
  }

  char *insn = ty->is_unsigned ? "movz" : "movs";
  if (ty->size == 1) println("  %sbl (%%rax), %%eax", insn);
  else if (ty->size == 2) println("  %swl (%%rax), %%eax", insn);
  else if (ty->size == 4) println("  movsxd (%%rax), %%rax");
  else println("  mov (%%rax), %%rax");
}
static void store(Type *ty) {
  pop("%rdi");

  switch (ty->kind) {
  case TY_STRUCT:
  case TY_UNION:
    for (int i = 0; i < ty->size; i++) {
      println("  mov %d(%%rax), %%r8b", i);
      println("  mov %%r8b, %d(%%rdi)", i);
    }
    return;
  ...
  }
  ...
}

構造体は関数の実行中でアドレスとして扱われていた。

yubrotyubrot

関数呼び出しと返却。簡単な返却の方から。Cでは return は文なのでこのcaseは gen_stmt にある:

  case ND_RETURN:
    if (node->lhs) {
      gen_expr(node->lhs);
      Type *ty = node->lhs->ty;

      switch (ty->kind) {
      case TY_STRUCT:
      case TY_UNION:
        if (ty->size <= 16) copy_struct_reg();
        else copy_struct_mem();
        break;
      }
    }

    println("  jmp .L.return.%s", current_fn->name);
    return;

struct, unionはABIの通りサイズが16バイトを超えるか否かによって返値の返却方法が異なる。 copy_struct_reg はレジスタにデータの各eightbyteをコピーし、 copy_struct_mem は隠れ第一引数のオフセットからアドレスを読んで、そのアドレスの指す領域にデータをコピーする。


関数呼び出し。こちらは式なので gen_expr にある:

  case ND_FUNCALL:
    if (node->lhs->kind == ND_VAR && !strcmp(node->lhs->var->name, "alloca")) {
      gen_expr(node->args);
      println("  mov %%rax, %%rdi");
      builtin_alloca();
      return;
    }

まず alloca のサポートがある。プログラムの評価にスタックを使ってるがallocaはどう実装しているのか?:

static void builtin_alloca(void) {
  // Align size to 16 bytes.
  println("  add $15, %%rdi");
  println("  and $0xfffffff0, %%edi");

  // Shift the temporary area by %rdi.
  println("  mov %d(%%rbp), %%rcx", current_fn->alloca_bottom->offset);
  println("  sub %%rsp, %%rcx");
  println("  mov %%rsp, %%rax");
  println("  sub %%rdi, %%rsp");
  println("  mov %%rsp, %%rdx");
  println("1:");
  println("  cmp $0, %%rcx");
  println("  je 2f");
  println("  mov (%%rax), %%r8b");
  println("  mov %%r8b, (%%rdx)");
  println("  inc %%rdx");
  println("  inc %%rax");
  println("  dec %%rcx");
  println("  jmp 1b");
  println("2:");

  // Move alloca_bottom pointer.
  println("  mov %d(%%rbp), %%rax", current_fn->alloca_bottom->offset);
  println("  sub %%rdi, %%rax");
  println("  mov %%rax, %d(%%rbp)", current_fn->alloca_bottom->offset);
}

スタックの先頭を伸ばし (sub %rdi, %rsp)、変数 alloca_bottom の値からスタックの先頭までのデータをシンプルに順にmovして移動している。最後に alloca_bottom を更新して伸ばした分を記録する。

関数呼び出しに戻る。

  case ND_FUNCALL:
    if (node->lhs->kind == ND_VAR && !strcmp(node->lhs->var->name, "alloca")) { ... }

    int stack_args = push_args(node); // (1)
    gen_expr(node->lhs); // (2)

    int gp = 0, fp = 0;

    // If the return type is a large struct/union, the caller passes
    // a pointer to a buffer as if it were the first argument.
    if (node->ret_buffer && node->ty->size > 16) pop(argreg64[gp++]);

    // (3)
    for (Node *arg = node->args; arg; arg = arg->next) {
      Type *ty = arg->ty;

      switch (ty->kind) {
      case TY_STRUCT:
      case TY_UNION:
        if (ty->size > 16) continue;

        bool fp1 = has_flonum1(ty);
        bool fp2 = has_flonum2(ty);

        if (fp + fp1 + fp2 < FP_MAX && gp + !fp1 + !fp2 < GP_MAX) {
          if (fp1) popf(fp++);
          else pop(argreg64[gp++]);

          if (ty->size > 8) {
            if (fp2) popf(fp++);
            else pop(argreg64[gp++]);
          }
        }
        break;
      case TY_FLOAT:
      case TY_DOUBLE:
        if (fp < FP_MAX) popf(fp++);
        break;
      ...
      default:
        if (gp < GP_MAX) pop(argreg64[gp++]);
      }
    }

    // (4)
    println("  mov %%rax, %%r10");
    println("  mov $%d, %%rax", fp);
    println("  call *%%r10");

関数呼び出しは、ABIに従って引数をレジスタとスタックとに設定して call する必要があるが、chibiccでは

  1. 引数群を実行してスタックに積む
  2. calleeを実行して得る
  3. スタックの引数群のうち、レジスタ渡しの分を必要なだけpopする
  4. 呼び出し。 mov ${fp}, %rax は可変長引数関数のための暗黙の引数

という形のコードを生成している。 (3) を行いやすいように、 push_args はスタックに積む順番を調整している:

static int push_args(Node *node) {
  int stack = 0, gp = 0, fp = 0;

  // If the return type is a large struct/union, the caller passes
  // a pointer to a buffer as if it were the first argument.
  if (node->ret_buffer && node->ty->size > 16) gp++;

  // Load as many arguments to the registers as possible.
  for (Node *arg = node->args; arg; arg = arg->next) {
    Type *ty = arg->ty;

    switch (ty->kind) {
    case TY_STRUCT:
    case TY_UNION:
      if (ty->size > 16) {
        arg->pass_by_stack = true;
        stack += align_to(ty->size, 8) / 8;
      } else {
        bool fp1 = has_flonum1(ty);
        bool fp2 = has_flonum2(ty);

        if (fp + fp1 + fp2 < FP_MAX && gp + !fp1 + !fp2 < GP_MAX) {
          fp = fp + fp1 + fp2;
          gp = gp + !fp1 + !fp2;
        } else {
          arg->pass_by_stack = true;
          stack += align_to(ty->size, 8) / 8;
        }
      }
      break;
    case TY_FLOAT:
    case TY_DOUBLE:
      if (fp++ >= FP_MAX) {
        arg->pass_by_stack = true;
        stack++;
      }
      break;
    case TY_LDOUBLE:
      arg->pass_by_stack = true;
      stack += 2;
      break;
    default:
      if (gp++ >= GP_MAX) {
        arg->pass_by_stack = true;
        stack++;
      }
    }
  }

  if ((depth + stack) % 2 == 1) {
    println("  sub $8, %%rsp");
    depth++;
    stack++;
  }

  push_args2(node->args, true);
  push_args2(node->args, false);

  // If the return type is a large struct/union, the caller passes
  // a pointer to a buffer as if it were the first argument.
  if (node->ret_buffer && node->ty->size > 16) {
    println("  lea %d(%%rbp), %%rax", node->ret_buffer->offset);
    push();
  }

  return stack;
}

static void push_args2(Node *args, bool first_pass) {
  if (!args) return;
  push_args2(args->next, first_pass);
  if ((first_pass && !args->pass_by_stack) || (!first_pass && args->pass_by_stack)) return;
  gen_expr(args);

  switch (args->ty->kind) {
  case TY_STRUCT:
  case TY_UNION:
    push_struct(args->ty);
    break;
  case TY_FLOAT:
  case TY_DOUBLE:
    pushf();
    break;
  ...
  default:
    push();
  }
}
  1. 引数がスタックに積まれるかレジスタ渡しかを計算する (assign_lvar_offsets と同様) とともに、関数呼び出しのためのスタック長を計算する
  2. push_args2(node->args, true) でスタック渡しの引数をスタックに積む
  3. push_args2(node->args, false) でレジスタ渡しの引数をスタックに積む (レジスタ渡しの引数を後から積むことで呼び出し元で順番にpopできる)

関数呼び出しに戻る。 call から返った後はいくつかの後始末を行う:

    println("  add $%d, %%rsp", stack_args * 8);
    depth -= stack_args;

    // It looks like the most significant 48 or 56 bits in RAX may
    // contain garbage if a function return type is short or bool/char,
    // respectively. We clear the upper bits here.
    switch (node->ty->kind) {
    case TY_BOOL:
      println("  movzx %%al, %%eax");
      return;
    case TY_CHAR:
      if (node->ty->is_unsigned)
        println("  movzbl %%al, %%eax");
      else
        println("  movsbl %%al, %%eax");
      return;
    case TY_SHORT:
      if (node->ty->is_unsigned)
        println("  movzwl %%ax, %%eax");
      else
        println("  movswl %%ax, %%eax");
      return;
    }

    // If the return type is a small struct, a value is returned
    // using up to two registers.
    if (node->ret_buffer && node->ty->size <= 16) {
      copy_ret_buffer(node->ret_buffer);
      println("  lea %d(%%rbp), %%rax", node->ret_buffer->offset);
    }
yubrotyubrot

だいたい codegen を辿り終わった。基本的に複雑っぽいなあというところは往々にしてABIが関わっており、コード生成もわかりやすかった。次は虎本がどういう方針で進めてたか等を見て行こうと思う。

このスクラップは2022/02/23にクローズされました