実践・C コンパイラ演習

コンパイル到達目標コード集

各コマ終了時点で「コンパイルできる最も複雑なプログラム」を示す(コマ1〜15)。

前提とする言語仕様: language_spec.md(第 2 版、2026 年改訂)。 ここに載せるコードは、すべてその版で受理される範囲に収めてある。

目次(コマ → 書けるようになるもの)

コマ テーマ この回で書けるようになるもの
0 環境構築 + RV64 手書きアセンブリ 手書きアセンブリ(コンパイラはまだ無い)
1 AST 理解 + インタープリター 算術式の評価(eval_ast())
2 コード生成①:算術式 算術式 → アセンブリ
3 コード生成②:変数・代入 変数宣言・代入・複数文
4 制御構文①:if / else + 三項演算子 if / else、? :
5 制御構文②:while / for + 前置 ++/-- ループ、前置 ++ / --
6 関数呼び出し・再帰 関数定義・呼出し・再帰
7 lvalue / rvalue + ポインタ & / *、ポインタ経由の代入
8 型検査の導入 char と int の使い分け、型別のロード / ストア
9 ポインタ演算とスケーリング a[i]、*(p + 2)、malloc した領域
10 文字列リテラルと .data セクション 文字列、printf
11 式の走査と libc 活用 式の中の文字列リテラル、lib.h の残りの関数
12 構造体とヒープ struct 定義・メンバアクセス・連結リスト
13 グローバル変数・スコープ管理 グローバル変数
14 複数ファイル・前処理の概念 #include / #define、複数ファイル
15 Python 版総合演習・mycc.py 統合 標準トラック完成(fixed17 テスト一覧)
— 付録: 機能とコマの対応 機能名から回を引きたいとき

機能名から回を引きたいときは language_spec.md の対応表が唯一の出典である。

凡例

項目 説明
フロントエンド mycc.py(Python 版)
期待する結果 qemu-riscv64 ./out; echo $? の終了コード(標準出力がある場合は別途記載)
検証コマンド python3 mycc.py input.c \| riscv64-linux-gnu-gcc -x assembler -static - -o out

コマ0(準備回): 環境構築 + RV64 手書きアセンブリ

コンパイラはまだ存在しない。手書きアセンブリを qemu で動かすことが目標。

# hello.s — 学習者が手で書く
    .global main
main:
    addi    a0, zero, 42    # 戻り値 = 42
    ret
riscv64-linux-gnu-gcc -static hello.s -o hello
qemu-riscv64 ./hello; echo $?   # → 42

コマ1(Phase 0): AST 理解 + インタープリター

コンパイラはまだ存在しない。教員提供の Lexer/Parser が返す AST を Python で評価する。

# 学習者が書くインタープリター(除算はゼロ方向切り捨て。教員提供の c_div を使う。
# Python の // は床除算で丸め方向が異なるため使わない)
def eval_ast(node):
    if node.kind == 'Num':  return node.val
    if node.kind == 'Add':  return eval_ast(node.lhs) + eval_ast(node.rhs)
    if node.kind == 'Sub':  return eval_ast(node.lhs) - eval_ast(node.rhs)
    if node.kind == 'Mul':  return eval_ast(node.lhs) * eval_ast(node.rhs)
    if node.kind == 'Div':  return c_div(eval_ast(node.lhs), eval_ast(node.rhs))

評価できる式の例:

1 + 2 * 3       → 7
(10 - 3) * 2    → 14
100 / 4 + 1     → 26

コマ2(Phase 1): コード生成①:算術式

フロントエンド: mycc.py 新機能: 整数定数・四則演算・剰余・カッコ → RV64 アセンブリ出力

int main() {
    return 1 + 2 * 3;
}

期待する終了コード: 7

int main() {
    return (100 - 3 * 7) / 4 + 1;
}

期待する終了コード: 20 (3*7=21, 100-21=79, 79/4=19, 19+1=20)


コマ3(Phase 1): コード生成②:変数・代入・シンボルテーブル

フロントエンド: mycc.py 新機能: ローカル変数宣言・代入・複数変数の管理、関数本体(Block ノード)からの宣言収集とフレームサイズの計算

int main() {
    int a;
    int b;
    int c;
    a = 3;
    b = 5;
    c = a + b;
    return c;
}

期待する終了コード: 8

int main() {
    int x;
    int y;
    x = 10;
    y = x * 2 + 3;
    x = y - x;
    return x;
}

期待する終了コード: 13 (y=23, x=23-10=13)

int main() {
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
    a = 1;
    b = 2;
    c = 3;
    d = 4;
    e = 5;
    f = 6;
    return a + b + c + d + e + f;
}

期待する終了コード: 21

確認ポイント: 6変数(48バイト)+ ra/s0(16バイト)= 64バイト(16の倍数 ✓)。 フレームサイズを align_to(n, 16) で16バイト境界に揃え、ローカル変数が0個でも正しく動くこと。


コマ4(Phase 1): 制御構文①:if / else + 三項演算子

フロントエンド: mycc.py 新機能: if / else if / else、比較演算子(== != < > <= >=)、三項演算子 ?:、入れ子ブロック、複数の return

int main() {
    int a;
    int b;
    a = 10;
    b = 3;
    if (a > b) {
        return a - b;
    } else {
        return b - a;
    }
}

期待する終了コード: 7

int main() {
    int score;
    score = 75;
    if (score >= 90) {
        return 4;
    } else if (score >= 70) {
        return 3;
    } else if (score >= 50) {
        return 2;
    } else {
        return 1;
    }
}

期待する終了コード: 3

int main() {
    int a;
    int b;
    a = 3;
    b = 8;
    return a > b ? a : b;
}

期待する終了コード: 8

int main() {
    int x;
    int y;         // 宣言は関数本体の先頭にまとめる
    x = 3;
    if (x == 3) {
        y = 4;     // 入れ子ブロックでは使うだけ
        return x + y;
    }
    return 0;
}

期待する終了コード: 7

宣言の位置: 宣言を書けるのは関数本体の先頭だけで、入れ子ブロックには文しか書けない。 入れ子ブロックは新しいスコープを作らないので、if の中の y は関数先頭で宣言した y そのものである。

式と文の違い: if は「実行する文」を選ぶ文であり、それ自体は値を持たない。 三項演算子 ?: は「値」を選ぶ式であり、式の中に書ける。 コード生成はどちらも同じ分岐(beqz + ラベル)だが、?: は選ばれた枝の値が レジスタに残る点だけが異なる。


コマ5(Phase 1): 制御構文②:while / for + 前置 ++/--

フロントエンド: mycc.py 新機能: while、for、break、continue、前置 ++/--

int main() {
    int i;
    int sum;
    i = 1;
    sum = 0;
    while (i <= 10) {
        sum = sum + i;
        i = i + 1;
    }
    return sum;
}

期待する終了コード: 55

int main() {
    int i;
    int fib_a;
    int fib_b;
    int tmp;
    fib_a = 0;
    fib_b = 1;
    for (i = 0; i < 10; ++i) {
        tmp   = fib_b;
        fib_b = fib_a + fib_b;
        fib_a = tmp;
    }
    return fib_a;
}

期待する終了コード: 55(フィボナッチ数列の第10項)

前置 ++: ++i は i の値を 1 増やし、増やした後の値を式の値とする。 for の更新式の慣用形として使う。実装は「左辺値のアドレスを 1 回だけ求め、 ロード → +1 → ストア」であり、代入のコード生成の応用で書ける。


コマ6(Phase 1): 関数呼び出し・再帰

フロントエンド: mycc.py 新機能: ユーザー定義関数の宣言・定義・呼び出し、引数(a0〜a7)、再帰・相互再帰

int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

int main() {
    return fib(10);
}

期待する終了コード: 55

int add(int a, int b) {
    return a + b;
}

int mul(int a, int b) {
    int i;
    int result;
    result = 0;
    for (i = 0; i < b; i = i + 1) {
        result = add(result, a);
    }
    return result;
}

int main() {
    return mul(6, 7);
}

期待する終了コード: 42


コマ7(Phase 1): lvalue / rvalue + ポインタ

フロントエンド: mycc.py 新機能: codegen / codegen_lval の2関数設計、アドレス取得(&)、間接参照(*)

int main() {
    int x;
    int *p;
    x = 42;
    p = &x;
    return *p;
}

期待する終了コード: 42

int main() {
    int a;
    int *p;
    a = 10;
    p = &a;
    *p = 20;
    return a;
}

期待する終了コード: 20(*p = 20 が a に書き込まれる)


コマ8(Phase 1): 型検査の導入

フロントエンド: mycc.py 新機能: ty_str 文字列による型の管理、型サイズ(int 4 / char 1 / T * 8)、型別のロード / ストア、char の昇格と縮小、文字リテラル 'a'

実装ポイント: このコマから型サイズの管理をコード生成器に導入する。 スキャフォールドでは Type クラスは使わず、ty_str 文字列と size_of_ty_str() / elem_ty_str() ヘルパーで型を扱う。 後半の C 移植では struct Type に対応させる。

Cコード ty_str サイズ ロード / ストア
int a; int 4 lw / sw
char c; char 1 lb / sb
int *p; int* 8 ld / sd

char は式の中では int に昇格し、char 変数への代入では下位 8 ビットに縮む。

int main() {
    char c;
    char d;
    int n;
    c = 'A';
    d = c + 2;
    n = d;
    if (n != 67) {
        return 1;
    }
    c = 321;
    if (c != 65) {
        return 2;
    }
    return n;
}

期待する終了コード: 67('A' + 2 は int で 67。c = 321 は下位 8 ビットの 65 に縮む)

int main() {
    char c;
    int n;
    int *p;
    c = 9;
    n = 4;
    p = &n;
    *p = 30;
    return n + c;
}

期待する終了コード: 39(char は lb / sb、int は lw / sw、ポインタは ld / sd)


コマ9(Phase 1): ポインタ演算とスケーリング

フロントエンド: mycc.py 新機能: ポインタ演算(p + n)、添字 p[i]、sizeof(型名)、多段ポインタ int **、malloc による連続領域の確保、型対応の前置 ++

実装ポイント: コマ8 で入れた型サイズを、ポインタを進めるときの尺度に使う。 連続した int の並びは、配列ではなく malloc(sizeof(int) * N) で確保した 領域として作る。p[i] は *(p + i) の略記であり、どちらも同じコードになる。 sizeof(型名) は翻訳時に値が決まる定数で、li 1 命令に落ちる。

#include "lib.h"

int main() {
    int *a;
    int i;
    int sum;
    a = malloc(sizeof(int) * 5);
    a[0] = 1;
    a[1] = 2;
    a[2] = 3;
    a[3] = 4;
    a[4] = 5;
    sum = 0;
    for (i = 0; i < 5; ++i) {
        sum = sum + a[i];
    }
    return sum;
}

期待する終了コード: 15

#include "lib.h"

int main() {
    int *a;
    int *p;
    a = malloc(sizeof(int) * 4);
    a[0] = 10;
    a[1] = 20;
    a[2] = 30;
    a[3] = 40;
    p = a;
    return *(p + 2) + *(p + 3);
}

期待する終了コード: 70(p + 2 は 2 * sizeof(int) バイト進む)


コマ10(Phase 1): 文字列リテラルと .data セクション

フロントエンド: mycc.py 新機能: 文字列リテラル、.data セクションへの出力とラベル参照、printf、#include "lib.h"

#include "lib.h"

int main() {
    printf("Hello, World!\n");
    printf("%d\n", 42);
    return 0;
}

期待する標準出力:

Hello, World!
42

コマ11(Phase 1): 式の走査と libc 活用

フロントエンド: mycc.py 新機能: 式のどこに書かれた文字列リテラルも .data に出す走査、不透明ポインタ struct FILE *、fprintf / fdopen / fopen / fread / fclose、exit

実装ポイント: 新しい仕組みはほとんど無い。コマ10 で文に対して書いた走査を、 式の種類の数だけ書き足して網羅する回である。ここまでで 「小さいが動く Python 版コンパイラ」が一度完成する。

int main() {
    if ("abc"[1] != 'b') {
        return 1;
    }
    if (*("abc" + 2) != 'c') {
        return 2;
    }
    return 42;
}

期待する終了コード: 42(添字・ポインタ加算の奥にある文字列リテラルも .data に出る)

int my_strlen(char *s) {
    int n;
    n = 0;
    while (s[n] != '\0') {
        n = n + 1;
    }
    return n;
}

int main() {
    return my_strlen("abc");
}

期待する終了コード: 3(strlen 相当は lib.h に無いので自作する)


コマ12(Phase 2): 構造体とヒープ(struct / . / -> / sizeof / malloc / 連結リスト)

フロントエンド: mycc.py 新機能: struct 定義(タグ必須)、メンバアクセス(. / ->)、構造体のレイアウトとパディング、sizeof(struct タグ)、構造体の malloc、void * の暗黙変換、自己参照構造体と連結リスト、NULL

struct Point {
    int x;
    int y;
};

int distance_sq(struct Point *p) {
    return p->x * p->x + p->y * p->y;
}

int main() {
    struct Point p;
    p.x = 3;
    p.y = 4;
    return distance_sq(&p);
}

期待する終了コード: 25(3² + 4² = 25)

#include "lib.h"

struct Node {
    int val;
    struct Node *next;
};

int list_sum(struct Node *head) {
    int sum;
    sum = 0;
    while (head != NULL) {
        sum = sum + head->val;
        head = head->next;
    }
    return sum;
}

struct Node *new_node(int v) {
    struct Node *n;
    n = malloc(sizeof(struct Node));   // struct のサイズを sizeof で求める
    n->val = v;
    n->next = NULL;
    return n;
}

int main() {
    struct Node *a;
    struct Node *b;
    struct Node *c;
    a = new_node(10);
    b = new_node(20);
    c = new_node(30);
    a->next = b;
    b->next = c;
    return list_sum(a);
}

期待する終了コード: 60


コマ13(Phase 2): グローバル変数・スコープ管理

フロントエンド: mycc.py 新機能: グローバル変数(.bss セクション。0 初期化が保証される)

#include "lib.h"

int call_count;
int total;

int add_and_count(int x) {
    total = total + x;
    call_count = call_count + 1;
    return total;
}

int main() {
    add_and_count(10);
    add_and_count(20);
    add_and_count(30);
    printf("%d\n", total);
    return call_count;
}

0 初期化の保証: グローバル変数は明示的に代入しなくても 0 で始まる (ポインタなら null)。.bss セクションに .zero で配置することで、 OS がプログラム開始時に 0 埋めしてくれる仕組みをそのまま使っている。 期待する標準出力: 60 期待する終了コード: 3


コマ14(Phase 2): 複数ファイル・前処理の概念

フロントエンド: mycc.py 新機能: #include "file.h"、オブジェクト形式 #define

// math_util.h
int my_abs(int x);
int my_max(int a, int b);
int my_pow(int base, int exp);
// math_util.c
int my_abs(int x) {
    if (x < 0) return -x;
    return x;
}

int my_max(int a, int b) {
    if (a > b) return a;
    return b;
}

int my_pow(int base, int exp) {
    int result;
    int i;
    result = 1;
    for (i = 0; i < exp; i = i + 1) {
        result = result * base;
    }
    return result;
}
// main.c
#include "math_util.h"
#define BASE 2
#define EXP  6

int main() {
    int a;
    int b;
    a = my_abs(-7);
    b = my_pow(BASE, EXP);
    return my_max(a, b);
}

期待する終了コード: 64(max(7, 64) = 64)


コマ15(Phase 2): Python 版総合演習・mycc.py 統合 ← 標準トラック達成

フロントエンド: mycc.py 確認: コマ15 時点で python3 scaffold/test_runner.py(final/mycc.py + final/tests/)を実行し、fixed17 全17問の通過を標準トラック完成の目安とする。

fixed17 テスト一覧(全17問)

# ファイル 出題意図 使用機能
1 f01_arith.c 四則演算の優先順位 加減乗除・カッコ
2 f02_vars.c 複数ローカル変数の宣言・代入・演算 ローカル変数・代入
3 f03_if.c if/else if/else チェーン + 関数呼び出し if/else if/else・比較演算子・関数定義
4 f04_while.c while ループで累積和 while・変数更新
5 f05_for.c for ループで階乗計算 for・複数変数同時更新
6 f06_break.c break によるループ脱出 break・for・if
7 f07_continue.c continue によるスキップ continue・for・if
8 f08_func.c 多引数関数の合成呼び出し 関数定義・引数・戻り値
9 f09_recur.c 再帰呼び出し(フィボナッチ) 再帰・スタックフレーム
10 f10_ptr.c ポインタ渡し swap &・*・間接代入・void 関数
11 f11_ptr_arith.c malloc 領域の添字・ポインタ走査 malloc・sizeof・添字・ポインタ引数
12 f12_struct.c 構造体 + ドット・アロー両方 struct 定義・. / -> 演算子
13 f13_global.c グローバル変数 .bss セクション・0 初期化・スコープ
14 f14_string.c printf 文字列出力 文字列リテラル・可変長引数関数呼出
15 f15_define.c #define マクロ + for ループ #define・for
16 f16_ternary.c 三項演算子(値を持つ分岐) ?:・入れ子・関数引数
17 f17_incr.c 前置インクリメント 前置 ++(int とポインタ)

標準判定用テストは final/tests/ に配置されている。python3 scaffold/test_runner.py で一括実行できる。

fixed17 は総合判定であって、仕様の全機能を 1 対 1 で覆う集合ではない。 機能ごとの代表テストは各コマの sessions/NN_xxx/tests/ にあり、 その対応は language_spec.md の対応表にまとめてある。

固定テストセットの代表例(malloc + ポインタ + 関数の複合):

// f11: ポインタ演算 — malloc した領域を sizeof と添字・ポインタ走査で使う
#include "lib.h"

int sum(int *a, int n) {
    int i;
    int s;
    s = 0;
    for (i = 0; i < n; ++i) {
        s = s + *(a + i);
    }
    return s;
}

int main() {
    int *a;
    a = malloc(sizeof(int) * 5);
    a[0] = 10;
    a[1] = 20;
    a[2] = 30;
    a[3] = 5;
    a[4] = 7;
    return sum(a, 5);
}

期待する終了コード: 72


付録: 機能とコマの対応

どの機能をどのコマで導入し、どのテストで検証するかは language_spec.md の対応表にまとめてある。 仕様の全機能を行に持つ表なので、この文書のコマ別の節と併せて使う (この文書は「各コマの終わりに何が動くか」を実例で示し、対応表は 「仕様のどの項目がどこで埋まるか」を網羅する)。

コマ15(final/tests 全通)で実装は完成する。学習経路の完了はコマ16(発表会)だが、 発表会を待たずに選択制の発展課題(../advanced/README.md)へ進んでよい。 C 移植・セルフホスト(P1)の進め方は ../advanced/P1_selfhost/README.md にある。

この資料の原稿を見る