Files
hakorune/docs/development/current/main/investigations/phase-256-joinir-contract-questions.md

9.4 KiB
Raw Blame History

Phase 256: JoinIR Contract Questions (for ChatGPT Pro)

目的: Phase 256 の詰まりJump/continuation/params/jump_args の暗黙契約)を、設計として固めるための相談メモ。


Q1. SSOT をどこに置くべき?

JoinIR の「意味論 SSOT」をどこに置くべきか。

  • A) Structured JoinIR を SSOT として維持し、bridge/merge が意味解釈する
  • B) Normalized JoinIR を SSOT とし、Structured→Normalized の正規化箱を必須化する

判断材料として、現在の層の境界と責務:

  • Pattern lowererStructured JoinIR 生成)
  • join_ir_vm_bridgeJoinIR→MIR 変換)
  • mergeMIR inline + PHI/ExitLine wiring

Q2. JoinInst::Jump の正規形(不変条件)

現状の詰まりは Jump が層を跨ぐときに「tail call 等価」になったり「Return 化」になったりして、continuation が失われる点にある。

相談したい:

  • JoinInst::Jump { cont, args, cond } を SSOT 的にどう定義するべきか?
  • cond 付き Jump は JoinIR 語彙として残すべきか?それとも IfMerge に寄せるべきか?

最小コードJoinIR 命令):

// src/mir/join_ir/mod.rs
pub enum JoinInst {
    // ...
    Jump {
        cont: JoinContId,
        args: Vec<VarId>,
        cond: Option<VarId>,
    },
    Call {
        func: JoinFuncId,
        args: Vec<VarId>,
        dst: Option<VarId>,
        k_next: Option<JoinContId>,
    },
    Ret { value: Option<VarId> },
    // ...
}

Q3. boundary/params/jump_args の順序契約をどこで固定する?

Phase 256 では、次の対応関係が暗黙で、崩れると SSA undef / PHI wiring fail-fast になりやすい。

  • JoinInlineBoundary.join_inputsJoinModule.entry.params
  • exit_bindings ↔ ExitLine の carrier PHI reconnect
  • jump_argstail call args metadata↔ ExitLine の latch incoming 復元

最小コードboundary:

// src/mir/join_ir/lowering/inline_boundary.rs
pub struct JoinInlineBoundary {
    pub join_inputs: Vec<ValueId>,
    pub host_inputs: Vec<ValueId>,
    pub loop_invariants: Vec<(String, ValueId)>,
    pub exit_bindings: Vec<LoopExitBinding>,
    pub expr_result: Option<ValueId>,
    pub loop_var_name: Option<String>,
    pub continuation_func_ids: std::collections::BTreeSet<String>,
    // ...
}

契約の fail-fast は現在ここで行っている:

// src/mir/builder/control_flow/joinir/merge/contract_checks.rs
pub(in crate::mir::builder::control_flow::joinir) fn run_all_pipeline_checks(
    join_module: &crate::mir::join_ir::JoinModule,
    boundary: &JoinInlineBoundary,
) -> Result<(), String> {
    verify_boundary_entry_params(join_module, boundary)?;
    // ...
    Ok(())
}

相談したい:

  • この順序契約は「boundary」「normalizer」「bridge」「merge」のどの層が SSOT になるべきか?
  • fail-fast の責務をどこに置くべきか(今は conversion_pipeline 直前)?

Q4. continuation の識別: JoinFuncId vs String(関数名)

Phase 256 P1.7 で「continuation 関数が merge で見つからず SSA undef」になった。 原因は bridge 側と merge 側の “関数名 SSOT” 不一致。

現状の暫定 SSOT は canonical_names:

// src/mir/join_ir/lowering/canonical_names.rs
pub const K_EXIT: &str = "k_exit";
pub const K_EXIT_LEGACY: &str = "join_func_2";
pub const LOOP_STEP: &str = "loop_step";
pub const MAIN: &str = "main";

相談したい:

  • continuation を JoinFuncId で保持し、bridge で 1 回だけ名前解決するべきか?
  • それとも String を SSOT にして “MirModule key” と一致させ続けるべきか?
  • 併存するなら、変換境界(片方→片方)をどこに置くべきか?

Q5. 正規化 shadowjoin_func_N)との共存戦略

normalized_shadow 側は join_func_2 のような命名を使う箇所がある。 この legacy をいつ・どう統一するべきか(または統一しないなら境界をどう明文化するか)。


Q6. jump_args は MIR のどの層の SSOT か?

観測:

  • ExitLine/merge 側は BasicBlock.jump_args を「exit/carry 値の SSOT」として参照する。
  • bridge 側で tail call / Jump を生成するときに jump_args を落とし忘れると、ExitLine が fallback 経路へ入りやすく、 SSA/dominance の破綻につながる。

最小コードMIR basic block:

// src/mir/mod.rs
pub struct BasicBlock {
    pub instructions: Vec<MirInstruction>,
    pub instruction_spans: Vec<Span>,
    pub terminator: Option<MirInstruction>,
    pub jump_args: Option<Vec<ValueId>>,
    // ...
}

相談したい:

  • jump_args は terminatorJump/Branch/Return/Callに埋め込むべきか、それとも BasicBlock の外部メタのままでよいか?
  • jump_args の契約順序・長さ・expr_result の有無・invariants の混在)をどこで固定するべきか?

Q7. Optimizer/DCE の不変条件spans 同期と jump_args

観測:

  • DCE が jump_args だけで使われる値を “unused” とみなすと、Copy/Const が消えて merge が壊れる。
  • DCE が instructions だけを削って instruction_spans を同期しないと、SPAN MISMATCH が発生しデバッグが困難になる。

相談したい:

  • jump_args は “use” として扱うのが SSOT として正しいか?
  • spans 同期は「各パスの責務」か、それとも BasicBlock の API例: spanned filterに閉じ込めるべきか

観測された失敗例(短く)

  • SSA undef関数名不一致で continuation が merge 対象にならず到達不能/未定義が露出)
  • ExitLine: jump_args の長さ契約ミスマッチcarriers と invariants の混在)
  • JoinInst::Jump が bridge で “Return 化” され、continuation の意味が落ちる疑い
  • DCE が jump_args 由来の use を落とし、Copy が消えて SSA/dominance が崩れる

ChatGPT Pro からの設計回答(要約)

診断(短く):

  • JoinIR/MIR 間に「暗黙 ABI呼び出し規約」が生えており、SSOT が分裂している
  • Jump/cont/params/jump_args が層を跨ぐたびに意味が揺れて、SSA/dominance/DCE が壊れやすい

命名:

  • この収束先north starJoin-Explicit CFG Construction と呼ぶ

提案の大枠3案:

案1: JoinIR ABI / Contract モジュール(推奨)

狙い:

  • いま暗黙のまま散っている契約(順序/長さ/名前/役割)を “ABI オブジェクト” として 1 箇所に封印する
  • Vec の順序契約に魂を預けないpack/unpack を ABI 経由にする)

最小イメージ(雰囲気):

pub struct JoinAbi {
    pub cont_sigs: Vec<ContSig>,      // continuation signature SSOT
    pub special: SpecialConts,        // main/loop_step/k_exit
    pub legacy_alias: AliasTable,     // join_func_2 -> k_exit 等
}

pub struct ContSig {
    pub params: Vec<Param>,
}

pub enum ParamRole {
    Carrier,
    Invariant,
    Result,
    // ...
}

Jump の正規形Normalized JoinIR:

  • Jump は cond を持たず、必ず終端
  • cond 付きは Branch { then_cont+args, else_cont+args } に寄せる

案2: MIR を “ブロック引数 SSA” に昇格(強力)

狙い:

  • JoinIR の cont/args と MIR の block/jump_args を同型化し、bridge/merge の解釈余地を消す

即効の 2 点(最優先):

  • jump_args を BasicBlock 外メタではなく terminator に埋め込むuse-def / DCE が自然に追える)
  • spans を並行 Vec から Vec<Spanned<_>>SPAN MISMATCH を構造で防ぐ)

案3: JoinIR をやめて CPS CFG 一本化(最終収束案)

狙い:

  • SSOT を 1 個にするJoinIR/MIR の “橋” を消す)
  • ただし移行は重いので、案2 の延長として収束させるのが現実的

推奨(この repo の制約込み)

Phase 256 の “今日の詰まり” に効く順で:

  1. 案1JoinIR ABI/Contractを設計 SSOT として採用
    • まず “順序契約を殺す” のが最大のデバッグ短縮になる
  2. 案2 のうち即効ポイントを段階導入
    • jump_args の SSOT は “terminator operand” に移す(大工事なので Phase 256 を緑に戻した後に着手が安全)
    • spans の Spanned 化も同様に段階導入(いまは pass 側で不変条件をテストで固定)

ここまでで、JoinIR を増やさずに「暗黙 ABI を明文化」できる。


次に設計として決めたいことDecision 候補)

  • JoinInst::Jump の SSOT は “tail call 等価” ではなく、Normalized JoinIR の terminator 語彙として固定する
  • continuation の識別は ID SSOTString は debug/serialize 用に限る)
  • jump_args を SSOT にするなら、最終的に MIR terminator に埋め込むDCE/CFG の整合性が自然になる)

Phase 256 実装から得た追加の教訓SSOT

  • expr_result と LoopState carrier が同一 ValueId になるケースが現実に起きる(例: ループ式の返り値が result)。 このとき “legacy expr_result slotjump_args[0])” を機械的に仮定すると、offset がずれて ExitLine の配線が崩れる。
  • 対策として ExitArgsCollector には “slot があるかどうか” を推測させず、呼び出し側が expect_expr_result_slot を明示して渡すのが安全Fail-Fast + 構造)。