summaryrefslogtreecommitdiff
path: root/docs/chapter22/README.ja.md
blob: 4ad6fae48c9d1b348dc84aa0c3c471d2c4b98ce6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# 第22章: Hello, World!

[English](README.md) | 日本語

この文書は [原文](README.md) の日本語訳です。

[前の章: 第21章](../chapter21/README.ja.md) |
[次の章: 第23章](../chapter23/README.ja.md)

そろそろ文字列と表示を扱う頃です。この章では基本的な文字列を作るために何が必要かを見ます。本格的な String クラスは、別の章まで待ちます。

```java
sys.io.p("Hello, World!");
```

できました。1行の `Hello, World` です。この例は `examples/A_helloWorld.smp` にあります。

分解して見てみましょう。

- `sys`: 単なる変数名です。ほかの変数と同様、現在のスコープで検索します。そのスコープは完全に空に見えますが、今ではすべての Simple プログラムがスコープ内に `sys` 変数を持って始まります。`sys` 自体は普通の `struct` で、`src/main/smp/sys.smp` に定義されています。
- `sys.io.p`: `sys` 構造体で `io` フィールドを検索すると `sys.io` 構造体が得られ、そこで `p`(print の略)を検索します。これによって関数を返します。
- `p("Hello, World!")`: `p` フィールドからロードしたばかりの関数を、文字列引数 `"Hello, World!"` で呼び出します。
- `"Hello, World!"`: `u8`(文字)の定数配列を生成する新しい構文です。それ以外は通常の範囲検査付き配列で、先頭に長さフィールド `#` があります。配列の実体データは、最終的に *定数プール*、そして ELF ファイルの RODATA セクションに入ります。

第11章の遅延メモリ分割はここでも続きます。パーサーは1つの `$mem` を保持し、BulkMemPhi/MemPhi が最適化中にフィールドのエイリアスを発見します。割り当ては部分的な MemMerge を消費し、`{ptr, $mem}` を生成します。Call はメモリ全体を運びます。ループ内の Load 検索はメモリマージのすべての枝をたどるので、BrainFuck の不変のプログラム長は主ループの外へ畳み込まれます。

[linear](https://github.com/SeaOfNodes/Simple/tree/linear) ブランチ上の直線的な Git リビジョン履歴で [この章](https://github.com/SeaOfNodes/Simple/tree/linear-chapter22) を読み、前の章と [比較](https://github.com/SeaOfNodes/Simple/compare/linear-chapter21...linear-chapter22) することもできます。

この章の[完全な言語文法](22-grammar.ja.md)はこちらです。

## 入れ子の型と静的フィールド

Simple は入れ子の型をサポートします。これは名前空間だけの変更で、新しい意味規則はなく、型定義を入れ子にできるだけです。

```java
struct Outer {
    struct Inner {
        int in;
        flt pi = 3.14;
    };
    int out;
};
```

コンストラクタではなく宣言内で設定した final フィールドは、自動的に「静的」フィールドになります。すべてのインスタンスで final なので、各インスタンスに保存する必要がありません。代わりに大域領域に1つだけ保持し、そこからロードします。まだ本当の *クラス* オブジェクトではありません。

この例では:

- `Outer` 構造体のインスタンスは、単一の `int` フィールド `out` を持ちます。
- `Inner` 構造体のインスタンスは、単一の `int` フィールド `in` を持ちます。
- 大域領域には、値 `3.14` の `Outer.Inner.pi` フィールドが存在します。

`sys` 構造体も、少なくとも入れ子の `libc` と `io` 構造体を持つ例です。これらの構造体のフィールドはすべて final なので静的です。

## FFI と I/O 呼び出し

外部/FFI 関数を呼び出せるようになりました。ほかの変数と同様に宣言しますが、`"C"` を代入します。libc の `write` 呼び出しの束縛は次のとおりです。

`{ i32 fd, i64 buf, u32 len -> u32 } write = "C";`

`"C"` を代入された final 変数は、外部で定義されたとみなします。同じ `"C"` 文字列の代入との曖昧さを避けるため、構文は少し変わるかもしれません。

正しいシグネチャや名前空間を取得するリンカのサポートはありません。リンク時に名前 `write` が一意である必要があります。

## 自動インポートと「sys.」名前空間

`sys` 構造体には `libc` 束縛と、表示やコレクションなどの既定ライブラリコードを含みます。この章の時点では、最小限の libc 束縛と簡単な表示があります。以下の抜粋は、上で使った部分を示します。

```java
// top-level default import
struct sys {
    // libc bindings
    /** https://www.man7.org/linux/man-pages/man2/<libc call>.2.html */
    struct libc {
        // fd  buf len -> len
        {  i32 i64 u32 -> u32 } write = "C";
        // addr len prot flags fd  off -> void*
        {  i64  i64 i32  i32   i32 i32 -> i64 } mmap = "C";
        // mmap flags
        i32 PROT_EXEC = 4;
        i32 PROT_READ = 1;
        i32 PROT_WRITE= 2;
        i32 PROT_NONE = 0;
        i32 MAP_PRIVATE = 2;
        i32 MAP_ANON = 32;
    };
    struct io {
        val p = { u8[~] str ->
            i64 ptr = str;  // cast array base to i64
            return sys.libc.write(1,ptr,str#);
        };
    };
};
```

`sys` インポート自体は、ほかと同じ単なる `struct` です。内部にフィールド、代入、型を宣言します。これらの代入はすべて final で構造体宣言内にあるため、すべて *静的* フィールドであることに注意してください。

## ポインタから `i64` へのキャスト

ポインタを i64 にキャストできるようになりました。ただし逆方向はできません。
`i64 ptr = str; // cast array base to i64`

配列の場合、長さフィールド `#` を飛ばした配列の *先頭* へのキャストであり、外部の「C」呼び出しへ渡すのに適しています。構造体の場合は単に構造体の先頭です。上の `sys.io.p` 関数では、長さ検査付き Simple 配列を libc の `write` 呼び出しへ渡すために使います。

## 定数の配列と定数配列

Simple の型システムは「定数配列」、すなわち固定された定数の配列をサポートします。ELF ファイルの RODATA セクションに保存します。現在、パーサーのレベルでは文字列だけをサポートします。`"Hello, World!"` は13個の ASCII バイトを含む `u8[13]` 配列を作ります。すべての配列と同様に長さがあり、どこかで範囲検査を受けます。

非定数配列の定数版は `u8[~]` 構文で代入できます。

```java
// Make a mutable array of ints
int N=4;
i32[] is = new i32[N];
// Mutate the array
for( int i=0; i<N; i++ )
    is[i] = i*i;
// Function "sum" can read but not write the array
val sum = { i32[~] nums ->   // Notice the [~] signature marking nums as immutable
    int sum = 0;
    for( int i=0; i<nums#; i++ )
        sum += nums[i];
    return sum;
};
return sum(is);
```

これは配列変数そのものではなく、配列の深い内部の内容に影響することに注意してください。

## 引用された文字

形式 ``` `0` ``` は、ASCII 文字 `0` を持つ `u8` 値を作ります。

## 継続的な改良

より大きなプログラムを実行すると、グラフだけのテストが見逃す誤りが明らかになります。この章の初期開発では、x86 命令エンコーディングとフラグ破壊、ループ後退辺やメモリストア周辺のスケジューリング、インライン化中の古い支配ノード深さなどがありました。読者が動作するコンパイラのスナップショットを土台にできるよう、修正を適用可能な最も早い章へ移しています。ネイティブテストは出力とプロセス終了ステータスの両方を確認します。エミュレータテストは、実行成功に加えて戻り値とメモリを確認します。

## RegAlloc の改良: よりよい色バイアス

第21章では保守的なコピー併合を導入しました。ここでは、関係する区間を併合できない場合のレジスタ優先度を改善します。コピーの連鎖は、末端の定義でも割り当て済みレジスタを使えます。ループの Phi は後退辺のレジスタを優先し、可能なら各反復の移動を避けます。

自明に彩色可能な区間がないとき、安い値はよりよいスピル候補です。唯一の使用にすでに隣接するものより、遠い使用や複数の使用を持つ複製可能な定義を優先し、次に長い範囲を安く分割できる callee-save 値を優先します。callee-save 間の同点はレジスタ順で決めます。これは小さな優先設定で、一般的なコストモデルではありません。よく使う値をレジスタクラスでまとめる処理は第23章、低頻度ループの分割は第24章、面積/コストの順位付けは第25章に残します。

第21章の隣接コピー転送も引き継ぎます。彩色後、次の命令に唯一の使用がある場合、オペランドマスクが許し、2アドレスの結び付きを壊さなければコピー元レジスタを読めます。以下の表は、この整理と遅延メモリの前方移植を含みます。第22章の群には、以前の監査後に追加した移動0の C 戻り値 ABI 確認2件も含むようになりました(24ではなく26項目)。

このディレクトリで `make spill-stats` を実行してください。以下の各行は **この章のコンパイラ**、ワークリストの既定シード123、以前の群と同じソース/対象の組み合わせを使います。Windows 実行は x86 SystemV/Win64 と RISC-V/ARM SystemV を合わせます。診断グラフとマスクの回帰テストは合計に寄与しません。

| プログラム群 | コンパイル数 | 残存する移動 | ループ重み付き移動 |
|---|---:|---:|---:|
| 第20章 | 39 | 324 | 443 |
| 第21章 | 52 | 432 | 964 |
| 第22章 | 26 | 67 | 67 |
| **合計** | **117** | **823** | **1,474** |

`_spills` はレジスタ移動を含む残存 SplitNode を数え、`_spillScaled` は各々を `8^loopDepth` で重み付けします。これはコンパイラの推定で、測定された実行時メモリアクセスではありません。報告器は個別の割り当てと CPU/ABI 合計も表示します。第21章の元の `int age` の person 例は `person21` として保存し、この章の `i32 age` 版と改訂された無限ループ例は第22章の群に属します。

隣接コピー転送前、同じコンパイラと正しさの修正で、第21章の色優先度とスピル順序を使った制御比較は、885移動と1,543重み付き移動でした。この章は **47重み付き移動(3.0%)** を減らします。RISC-V BrainFuck は2つの群それぞれで42から28、ARM は34から28に改善します。一部の MergeSort と RISC-V Sieve は各1移動増えます。スイート全体の合計が、その局所的なトレードオフに見返りがあるかを示します。

後のヒューリスティックをいくつか混在させた元の第22章スナップショットは、同じ115コンパイルで854移動、1,736重み付き移動でした。修正して段階化した版は240重み付き移動(13.8%)を減らします。どちらの比較も、コンパイラの章全体どうしの比較と混同してはいけません。下位表現への変換処理も変わるためです。たとえば固定した第20章の String 入力には明示的な return がなく、以前の監査では第22章の既定戻り値処理がその処理対象を除去しました。遅延メモリを使うと、一部の対象コンパイルは呼び出し可能な本体を残し、現在の表にはそれらの移動が含まれます。

完全なスイートと統計実行器は成功します。通常のバックエンド比較では最適化器のシードを固定します。最適化器のワークリストをシャッフルしても本質的に同じグラフへ正規化するはずなので、それらのグラフで割り当てを繰り返しても、検証範囲はあまり増えません。最適化器の正規化やワークリスト順序の失敗を調べるときに、シードを変えてください。

[バックポート記録](../chapter-backports.md) には、解決された String の fall-through と、返される関数の保持/再配置の失敗が、再現方法と検証履歴を含めて記録されています。