【テクニカル・上級編】Dartの定数式(Constant Expressions)で「関数呼び出し」が制限される技術的背景 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dartの定数式(Constant Expressions)で「関数呼び出し」が制限される技術的背景:コンパイラ、AOT、そしてDart VMの深淵

Dartコアチームのアーキテクトとして、日頃から数百万行規模のFlutterアプリケーションやサーバーサイドDartのパフォーマンスプロファイルを眺めていると、開発者が「なぜこの式を `const` にできないのか」と首をかしげる場面に幾度となく遭遇する。

特に、「なぜコンパイル時定数(Constant Expressions)の中で任意の関数呼び出しが許可されないのか」という問いに対する答えは、単なる「言語仕様の制約」ではない。これは、Dartが目指す「予測可能なゼロコスト抽象化」「完全なAOT(Ahead-of-Time)コンパイルの保証」「Isolate間での安全なメモリ共有(Canonicalization)」という、言語の根幹をなす設計思想の防壁そのものである。

今回は、Dart VMの内部構造、定数の正準化(Canonicalization)メカニズム、そしてコンパイル時評価器(Constant Evaluator)の限界を、低レイヤの視点から徹底的に解剖する。

—

1. 表面的な理解の破壊:「関数呼び出し」が拒絶される真の理由

多くのプログラマは、`const` は「値が変化しないこと」を保証するものだと誤解している。しかし、Dartにおける `const` は単なるイミュータビリティ(不変性)の表現ではない。「コンパイルのフェーズにおいて、完全に評価・確定され、バイナリのデータセクションに直接埋め込まれるべき式」を指す。

次のコードを見てほしい。一見、何の問題もない純粋な関数に見える。

// 純粋関数(副作用なし)
int add(int a, int b) => a + b;

// コンパイルエラーになる
const int result = add(2, 3);

なぜコンパイラはこのコードを拒絶するのか?「`add` が純粋かどうかをコンパイラが静的に判定できないから」と思うかもしれないが、それは本質ではない。たとえボディが単一の式であっても、Dartの仕様上、任意のユーザー定義関数呼び出しはコンパイル時定数式として許可されていない。

その技術的背景は、以下の3つのレイヤに起因している。

1. Dart VMの実行モデルとステートの分離
2. 定数の正準化(Canonicalization)とメモリ上の同一性
3. AOTコンパイル時におけるコード実行のサンドボックス問題

—

2. コンパイル時評価器(Constant Evaluator)の正体

Dartのコンパイラ(CFFIを使うJITであれ、`gen_snapshot` を使うAOTであれ)の内部には、Constant Evaluator(定数評価器)と呼ばれるミニチュアのインタプリタが組み込まれている。

この評価器は、Dartのサブセット(算術演算、論理演算、文字列結合、コンストラクタ呼び出しの一部など)のみを実行できるようにハードコードされている。なぜ一般的な関数呼び出しを許さないのか?

理由A: 停止問題(The Halting Problem)とコンパイル時間の爆発

任意の関数呼び出しを許容した場合、Constant Evaluatorは事実上の汎用VMとして機能しなければならなくなる。関数内でのループ、再帰、外部ライブラリへの依存、果ては無限ループを含むコードをコンパイル時に実行させられた場合、コンパイラは停止問題に直面する。
開発者が何気なく書いた `const x = computeComplexGraph();` がコンパイルを永遠に終わらせないものにしてしまうリスクを、言語仕様のレベルで排除しているのだ。

理由B: 副作用(Side Effects)とモジュール境界の不確実性

Dartは非常に強力なライブラリエコシステムを持つ。ある関数が、一見純粋に見えても、内部でグローバルなキャッシュにアクセスしていたり、static変数をインクリメントしていたり、あるいはプラットフォーム固有の外部関数(FFI)を呼び出していたとしたらどうか?
コンパイル時の評価順序は、実行時のそれとは全く異なる。コンパイル時の評価が副作用を引き起こした場合、生成されるバイナリの整合性が破壊される。

—

3. メモリ最適化の極み:正準化(Canonicalization)とオブジェクトグラフ

Dartの `const` がもたらす最大の恩恵は、メモリフットプリントの劇的な削減と、比較演算(`==`)のO(1)化である。これをつかさどるのが正準化(Canonicalization)だ。

Dart VMは、同一のコンパイル時定数オブジェクトを、ヒープ(正確にはイミュータブル・ヒープ領域)上で単一のインスタンスとして共有する。

class Point {
final int x;
final int y;
const Point(this.x, this.y);
}

void main() {
const p1 = Point(1, 2);
const p2 = Point(1, 2);

// Dart VMのメモリ上において、p1とp2は完全に同一のメモリアドレスを指す
print(identical(p1, p2)); // true
}

この正準化を成立させるためには、「そのオブジェクトが構築されるプロセスが、完全に決定論的であり、かつコンパイル時に完全に解決可能であること」が絶対条件となる。

もしユーザー定義の関数呼び出しによるオブジェクト生成を許容してしまうと、その関数が返すインスタンスが本当に同一のメモリアドレスに帰着すべきものなのか、コンパイラが静的に追跡・保証することが極めて困難になる。

—

4. 許可される演算 vs 許可されない演算の境界線

Dartの仕様(Language Specification)において、コンパイル時定数式として許可されているものと、厳格に弾かれるものの境界線を低レイヤの視点で整理する。

| 操作カテゴリ | 許可される例 (`const`) | 拒絶される例 | 技術的理由 |
| :— | :— | :— | :— |
| 算術・論理 | `2 + 3`, `true && false` | `a + b` (変数) | オペランドが定数であればコンパイル時に畳み込み(Constant Folding)可能。 |
| コンストラクタ | `const Duration(seconds: 5)` | `Duration(seconds: x)` | `const` コンストラクタのみがイミュータブル・ヒープへのアロケーション対象となるため。 |
| 関数呼び出し | 一切不可 | `int.parse(‘5’)`, `customFunc()` | 汎用コード実行エンジンをコンパイラ内に持つことを避けるため。例外や動的ディスパッチの温床になる。 |
| コレクション | `const [1, 2, 3]` | `[1, 2, computeVal()]` | 要素がすべて定数であれば、Dart VMのローダーが直接スナップショットにシリアライズできるため。 |

例外的な「関数様」の構文:`const` コンストラクタ

ここで鋭いエンジニアなら疑問に思うだろう。「なぜ `const Point(1, 2)` というコンストラクタ呼び出しは許されるのに、通常の関数呼び出しはダメなのか?」と。

答えは、`const` コンストラクタは「関数」ではなく「データ構造の宣言」であるからだ。
`const Point(1, 2)` は、実行時にコードを実行しているのではなく、コンパイラに対して「このフィールドを持つオブジェクトのバイナリ表現をスナップショットに書き込め」と指示しているに過ぎない。コンストラクタのボディに任意のロジック(制御構文など)を書くことが禁止されているのはまさにこのためである。

—

5. 高度な回避策:どうしても「計算結果」をコンタイムに得たい場合

もし、複雑な計算やデータ変換の結果を `const` として扱いたい場合、どうすればよいのか?
現代のDart(Dart 2.12以降、およびNull安全の完全導入後)では、マクロ(Macros / 実験的機能)や、事前のビルドステップ(`build_runner`)を用いたコード生成がその答えとなる。

以下は、ビルド時コード生成によって「関数呼び出しのようなこと」をコンパイル時定数として安全に実現するアーキテクチャの概念図である。

[開発者のコード (カスタムロジック)]
↓ (build_runner によるコード生成)
[静的解析 & 評価器 (Dart AST解析)]
↓
[生成されたコード: const String computed = “硬化された定数データ”;]
↓ (AOTコンパイル)
[Dart VM / AOTバイナリ (ゼロコストでロード)]

実行時にCPUサイクルを消費して関数を評価するのではなく、ビルドパイプラインの段階で計算を済ませ、生成されたDartコード(あるいはシリアライズされたバイナリ)をソースツリーに埋め込む。これが、Dartのパフォーマンス哲学を損なわずに複雑な定数を得るための唯一にして最大の正攻法である。

—

6. チーフアーキテクトからの提言

Dartの `const` と定数式の制約は、決して「表現力の欠如」ではない。むしろそれは、ランタイムの予測可能性を極限まで高め、ガベージコレクションの負荷をゼロにし、Isolate間のメッセージングコストを極小化するための、意図された強固な制約である。

「なぜこの関数を呼べないのか」と嘆く前に思い出してほしい。その関数呼び出しをコンパイル時に許容した瞬間から、あなたのアプリケーションは決定論的なバイナリから、予測不可能な挙動をはらむインタプリタ的実行体へと変貌してしまうのだ。

Dartの厳格な型システムと定数評価器をリスペクトし、レイヤの境界を理解した上でコードを構築すること。それこそが、真にスケーラブルで堅牢なDart/Flutterアーキテクチャを築く唯一の道である。

タイトルとURLをコピーしました