レッスン1 — add をステップで組み立てる:シグネチャと本体
問い
ADD は EVM Opcode で最も単純な非自明: 数値 2 つ pop、和を push。Revm は 4 行で済ませているが、その 4 行に型パラメータ 2 つのジェネリック + ?Sized opt-out + アンダーフローガード + 分岐予測ヒントへ展開されるマクロ + wrapping_add(+ だと最初のオーバーフローでメインネットから分岐)が詰まっている — どうやって到達するか?
原理(最小モデル)
- 本物の形.
pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result { popn_top!(...); *op2 = op1.wrapping_add(*op2); Ok(()) }。 - 5 ステップ積み上げ. 素朴版 →
<H: Host>→?Sized→IT: ITy→&mutその場 +wrapping_add。 - モノモーフ化.
<H: Host>は具象Hごとに特殊化バイナリ = 静的ディスパッチ、vtable なし。 ?Sizedopt-out. Rust の暗黙Sizedを外すと&mut dyn Hostが渡せる、vtable 間接化と引き換え。IT: ITy. 実行モード(本番 / トレース / Inspector)をコンパイル時に選ぶ marker、特殊化バイナリで実行時切替コストゼロ。&mutその場書き込み. 3 スタック操作(pop + pop + push)→ 1 操作(pop + 上書き)。wrapping_add必須. EVM コンセンサスは mod 2²⁵⁶ wrap を要求、+は debug panic / release wrap で分岐、saturating_addはネットワークフォーク。
具体例
ステップ 0 — 素朴版:
pub fn add(stack: &mut Vec<U256>) -> Result<(), &'static str> {
let a = stack.pop().ok_or("underflow")?;
let b = stack.pop().ok_or("underflow")?;
stack.push(a + b);
Ok(())
}
2 つ落とし穴: ① &mut Vec<U256> は具象型、トレーサー / ファザー / Inspector のスタックに差し替え不能、② pop + pop + push = 3 回スタック操作。
ステップ 1 — ホストジェネリック:
pub fn add<H: Host>(host: &mut H) -> Result {
// ... 本体は同じだが、具象 Vec ではなく H に対して呼ぶ
}
<H: Host> = 「Host トレイトを実装する任意の型」、コンパイラがモノモーフ化で具象 H ごとに特殊化バイナリ出力。落とし穴: trait object &mut dyn Host を渡せない(<H: Host> はコンパイル時サイズ既知のみ)。
ステップ 2 — ?Sized で trait object 許可:
pub fn add<H: Host + ?Sized>(host: &mut H) -> Result {
// ...
}
?Sized で Rust の暗黙の Sized 制約を外す = &mut dyn Host が有効引数に = 1 バイナリで全 Host 実装、vtable 間接化と引き換え。
ステップ 3 — IT: ITy で実行モード抽象化:
pub fn add<IT: ITy, H: Host + ?Sized>(host: &mut H) -> Result {
// ...
}
IT = 「interpreter-types」マーカー、ストラテジパラメータ。同じソースが本番 / トレース / Inspector サンドボックスごとに特殊化バイナリへコンパイル。本物の add シグネチャ:
pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
(Host 制約は Ictx 側に移っている — add に残るのは IT: ITy と H: ?Sized。)
ステップ 4 — 本体を &mut その場書き込み:
let a = stack.pop().ok_or(StackUnderflow)?; // op1 を pop
let b = stack.last_mut().ok_or(StackUnderflow)?; // 新トップ &mut
*b = a + *b; // その場上書き
pop 1 + その場書き込み 1 = push なし。-> Result に連結値なし、データの流れは参照経由。
ステップ 5 — wrapping_add:
*b = a.wrapping_add(*b);
EVM コンセンサス = ADD mod 2²⁵⁶ wrap、+ は debug panic / release wrap で分岐、saturating_add は最初のオーバーフローでネットワークフォーク。U256::MAX.wrapping_add(U256::from(1)) = 0x0。
到達した本物の形:
pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
let op1 = context.interpreter.stack.pop().ok_or(StackUnderflow)?;
let op2 = context.interpreter.stack.last_mut().ok_or(StackUnderflow)?;
*op2 = op1.wrapping_add(*op2);
Ok(())
}
中央 2 行は次レッスンで popn_top! マクロに抽出。
失敗例(誤解)
「<H: Host> だけで十分」— 間違い。trait object &mut dyn Host(設定フラグ / 動的テストハーネス)には ?Sized 必須。
「IT: ITy は飾り」— 間違い。これがないと add を本番 / トレース / Inspector の 3 回書く羽目、または実行時分岐で速度ペナルティ。
「+ でも release ビルドなら大丈夫」— 致命的。Rust の + は debug panic / release wrap = ビルド分岐 = コンセンサス契約に載せられない。wrapping_add で明示 = EVM コンセンサスが要求する正確な挙動。
ステップで組み立てる
Step 1: 素朴版を確認
pop + pop + push、3 操作、ホスト固定。
Step 2: <H: Host> でホストジェネリック
モノモーフ化で特殊化バイナリ。
Step 3: + ?Sized で trait object 許可
&mut dyn Host 有効、vtable 間接化と引き換え。
Step 4: <IT: ITy, H: ?Sized> で実行モード抽象化
IT ストラテジパラメータ、特殊化バイナリ。
Step 5: &mut その場書き込み + wrapping_add
1 操作、-> Result 連結値なし、mod 2²⁵⁶ wrap。
答え合わせ
IT: ITyがくれるもの: 実行モードの 静的選択、特殊化バイナリで実行時切替コストゼロ。なければ 3 回書くか実行時分岐。?Sizedが許すもの: trait object&mut dyn Host。Rust の暗黙Sized制約を外す、vtable 間接化と引き換え。- その場書き込みが勝つ理由: 3 操作 → 1 操作、ホットパス(毎 tx 毎 Opcode 数万 + 毎ブロック数百万 + CI 秒間数億)で実測される差。
U256::MAX.wrapping_add(U256::from(1)):0x0(mod 2²⁵⁶ wrap)= EVM コンセンサス要求。saturating_addだとU256::MAXのまま → メインネットフォーク。
合格基準
- 本物の
add4 行を即書ける。 - 5 ステップ積み上げを順に言える。
- モノモーフ化と
?Sizedの trade-off を 1 文で説明できる。 IT: ITyの役割を即答できる。wrapping_add必須の理由を言える。
まとめ(3行)
- 本物の
addは 4 行: ジェネリック<IT: ITy, H: ?Sized>+popn_top!+wrapping_addその場書き込み +Ok(())。 - 5 ステップ積み上げ(素朴 →
<H: Host>→?Sized→IT: ITy→&mut+wrapping_add)、各ステップに明確な動機。 - EVM コンセンサスは mod 2²⁵⁶ wrap を要求、
+やsaturating_add置換は分岐 / フォーク、次レッスンで中央 2 行をpopn_top!マクロに抽出。