diff --git a/docs/development/current/main/investigations/phase-29y-mir-lifecycle-vocab-consult.md b/docs/development/current/main/investigations/phase-29y-mir-lifecycle-vocab-consult.md new file mode 100644 index 00000000..1790f151 --- /dev/null +++ b/docs/development/current/main/investigations/phase-29y-mir-lifecycle-vocab-consult.md @@ -0,0 +1,173 @@ +Status: Active +Date: 2025-12-26 +Scope: Self-host後に “脱Rustランタイム(NyRT/.hako)” を進めるため、MIR命令語彙と runtime ABI(lifecycle/RC/weak)の境界をどう固めるべきか相談するパケット +Related: +- docs/reference/language/lifecycle.md +- docs/development/current/main/phases/phase-285/README.md +- docs/development/current/main/design/edgecfg-fragments.md +- docs/development/current/main/investigations/phase-286-plan-normalization-consult.md + +# Phase 29y (future): MIR lifecycle vocab freeze 相談パケット + +## 0. 相談の意図 + +この文書は「今すぐ MIR 命令を増やす」提案を求めない。 +self-host後に脱Rustを進める前提で、**MIRのどこまでを語彙として固定し、どこからを runtime ABI(NyRT/.hako)に委譲するか**を設計SSOT化するための相談パケット。 + +強調: +- “脱Rust” はゴールだが、**実装移行(NyRT/.hako化)は別フェーズ**でやる(この文書はその設計SSOTの下敷き)。 +- Phase 285(weak conformance / hidden root 根治)が未解決なら、先に Phase 285 を優先する。 + +## 1. 背景(いま分かったこと) + +- weak/strong の意味論は言語SSOT(`docs/reference/language/lifecycle.md`)で定義されている。 +- VM 実装では “hidden root” が起きうる(例: SSA値を保持する `regs` / `mem` 等が strong を保持しうる)。 +- LLVM/wasm/他言語へ広げるには、Rust VM の内部構造に依存しない **共通の境界(ABI/観測点)**が必要。 + +## 2. 相談したい焦点 + +### Q1. MIRに参照カウンタ(retain/release)を載せるべきか? + +候補: +- A) **MIRは寿命を語らず**、runtime ABI で retain/release/weak を表現する(MIR→loweringがABI呼び出しへ) +- B) MIRに **薄い寿命effect**(Retain/Release等)を追加し、後段で最適化する(liveness/use-count由来) + +求める回答: +- “移植性(LLVM/wasm)” と “実装コスト/破綻リスク(PHI/early-exit/例外)” の観点で、段階移行のおすすめ。 + +### Q2. “観測点” をどこに置くべきか? + +目的は「weak_to_strong が成功する理由(strong root の残留)」を追えること。 + +候補: +- runtime 側で root summary API(regs/mem/obj_fields/handlesの強参照数) +- MIR 側で liveness の契約を固定し、VMがそれに従って解放する + +## 2.1 推奨の設計分離(相談ベースライン) + +相談の叩き台(推奨): +- **参照カウンタの実体(カウンタ値)は runtime(NyRT)**に置く(MIRは“値”として持たない)。 +- **retain/release/weak_* の“発火点”はコンパイラ側が決める**が、語彙の増殖を避けるため “1箇所” に寄せる。 + - 候補: `CorePlan → Frag → emit_frag()` の流れの **Frag直前/直後に 1 回だけ**走る RC insertion pass。 +- backend(VM/LLVM/wasm)は、その RC event 列を **NyRT ABI 呼び出し**へ落とすだけにする。 + +狙い: +- MIR/CorePlan は “意味論の語彙(BoxRef/WeakRef/weak_to_strong)” に留め、RC 実装の詳細を押し込めない。 +- RC の発火点は “見える” ようにしつつ、分散実装を防ぐ(hidden root の再発を減らす)。 +- self-host後に NyRT を `.hako runtime` に置換しても、ABIがSSOTなら導線が崩れにくい。 + +## 2.2 最小 NyRT ABI(相談先に提示したい候補) + +### strong(参照カウント) + +- `nyrt_retain(BoxRef)` +- `nyrt_release(BoxRef)` + +注意: +- `release` が strong==0 になったとき “物理解放” は runtime の責務。 +- `fini()` は言語SSOT上 “論理終端” であり、GC/RCで勝手に呼ばない(`docs/reference/language/lifecycle.md`)。 + +### weak(弱参照) + +- `nyrt_weak_new(BoxRef) -> WeakRef` +- `nyrt_weak_drop(WeakRef)` +- `nyrt_weak_to_strong(WeakRef) -> BoxRef | null` + +SSOT整合: +- `weak_to_strong()` は Alive のみ成功し、Dead/Freed は `null` を返す(`docs/reference/language/lifecycle.md`)。 + +### optional(診断/可観測性) + +実装は後でよいが、相談では “観測点” を固定しておきたい。 +- `nyrt_debug_roots_summary()`(カテゴリ別の strong root 数など) +- `nyrt_debug_refcounts(BoxRef)`(単体の strong/weak 数など) + +制約: +- 診断は意味論を変えない(ON/OFFで挙動が変わらない)。 +- env sprawl を防ぐため、既存の verbose/trace と統合する(Phase 285 の方針と整合)。 + +## 2.3 関数 ABI(owned/borrowed の最小規約) + +相談したい焦点(推奨案): +- 引数は **borrowed**(呼び出し中は alive を呼び出し側が保証、callee は retain/release しない)。 +- 戻り値は **owned**(callee は +1 を返し、caller が release 責務を持つ)。 + +この規約の狙い: +- 関数境界の retain/release を最小語彙で固定できる。 +- VM/LLVM/wasm で ABI を揃えやすい。 + +## 2.4 “RC insertion pass” の置き場所(SSOT候補) + +目的: +- retain/release/weak_drop を “分散実装” させず、**1箇所**で決める。 + +候補(相談): +- `Frag` に入る直前(CorePlan から “CFGが確定した後”)に、RC event 列を挿入する。 + - `PHI/loop/early-exit/cleanup` を見た上で挿入できる(誤りが減る)。 + - Plan/Frag SSOT の “出口語彙(return/break/continue)” と整合しやすい。 + +相談したい点: +- “RC event” を MIR 命令として持つか(`Retain/Release/WeakDrop`)、あるいは `Frag` の effect として表すか。 +- 最初は正しさを優先し、冗長操作削除(最適化)は別フェーズに分離するのが安全か。 + +## 3. 非目標(今回はやらない) + +- MIR語彙の大改造(型システムへ所有モデルを埋め込む等) +- GCアルゴリズム刷新 +- self-host前に NyRT を .hako 化する実装 + +## 4. 期待するアウトプット(相談先に求める形式) + +- 最終形ではなく **段階移行(1フェーズ=小差分)**のマイルストーン +- 各フェーズの受け入れ条件(smoke/verify/contract) +- “やると破綻しやすい罠” の列挙(PHI/early return/exception/cleanup) + +## 4.1 マイルストーン案(相談先に求めたい “小刻み” の形) + +Milestone A(docs-first): +- NyRT ABI(最小セット)と関数 ABI(owned/borrowed)を SSOT 化 +- 受け入れ: doc が `lifecycle.md` と矛盾しない、smoke の観測点が明確 + +Milestone B(conformance-first): +- Phase 285 の weak-fail(hidden root)を根治し、VM で conformance を固定 +- 受け入れ: weak-fail fixture が PASS、quick gate が green + +Milestone C(compiler-side SSOT): +- RC insertion pass の設計SSOT(どこで何を挿入するか)を確定(まだ実装しない) +- 受け入れ: PHI/loop/return/cleanup のケース分類がある、破綻ポイントが明文化されている + +Milestone D(implementation): +- RC insertion pass を実装し、VM/LLVM/wasm が同じ ABI を呼ぶ形にする +- 受け入れ: 既存 fixture を使って VM/LLVM parity を維持、診断が意味論を変えない + +## 4.2 罠リスト(相談先に “やらない方がいい” として確認したい) + +- “SSAの last-use = 言語寿命” を意味論にしてしまう(weak_to_strong で観測できて破綻しやすい)。 +- by-name で例外処理(Box名/関数名の文字列一致で retain/release を変える)。 +- 環境変数で挙動を分岐し、既定が揺れる(env sprawl)。 +- RC と `fini()` を混ぜる(GC/RC で勝手に `fini()` を呼ぶ等)。 +- “万能 struct(Optionだらけ)” で語彙統合し、結局デバッグできなくなる。 + +## 5. 参考(現時点のSSOT) + +- lifecycle semantics SSOT: `docs/reference/language/lifecycle.md` +- Phase 285 conformance: `docs/development/current/main/phases/phase-285/README.md` + +## 6. 相談先に渡すための最小質問テンプレ + +「ソースコードを見なくても回答できる」ことを重視する。 + +1) 推奨の分離は A/B のどれか? +- A: MIRは寿命を語らず、NyRT ABI を呼ぶ +- B: MIR/Frag に薄い寿命effect(Retain/Release)を載せて“見える化”する + +2) 関数 ABI は “args borrowed / return owned” が妥当か? +- もし違うなら、最小で安全な代替案は何か? + +3) RC insertion pass を置く場所はどこが安全か? +- CorePlan→Frag 直前/直後/emit直前など、PHI/loop/cleanup を踏まえて推奨を教えてほしい + +4) weak_to_strong の失敗条件(Dead/Freed)を実装間で揃えるための “観測点” はどこが良いか? +- runtime に root summary API を置くのが妥当か? + +5) 破綻しやすい罠(特に PHI/early return/cleanup)を列挙してほしい diff --git a/docs/development/current/main/phases/phase-285/README.md b/docs/development/current/main/phases/phase-285/README.md index 783edfe5..02e323a3 100644 --- a/docs/development/current/main/phases/phase-285/README.md +++ b/docs/development/current/main/phases/phase-285/README.md @@ -1,6 +1,6 @@ # Phase 285: Box lifecycle / weakref / finalization / GC conformance -Status: P0 COMPLETE (2025-12-26); A1 series + LLVM sub-phases implemented +Status: P0/P1/P2/P2.1 ✅ COMPLETE (2025-12-26) ## Next (P0 docs-only → P1/P2) @@ -9,6 +9,32 @@ Status: P0 COMPLETE (2025-12-26); A1 series + LLVM sub-phases implemented - lifecycle/weak/fini/GC: `docs/reference/language/lifecycle.md` - `null`/`void`/truthiness: `docs/reference/language/types.md` +## P2.1(Hidden root investigation & fix)✅ COMPLETE (2025-12-26) + +P2 の weak-fail fixture(明示 drop `x = null`)が “失敗→null” にならず、`weak_to_strong()` が成功してしまう(hidden root)問題を根治した。 + +### Root cause(要旨) + +- VM の `regs` が古い `ValueId` を保持し続け、`Arc` が drop されない +- SSA last-use をそのまま寿命にすると、言語の block-scoped locals と衝突して `weak_basic` が壊れる + +### Fix(要旨) + +MIR 命令 `KeepAlive` を “スコープ維持 / 上書きdrop” の両方に使い分ける。 + +- `drop_after=false`(scope-end keepalive): スコープ終端まで値を生存維持(language scope semantics) +- `drop_after=true`(overwrite drop): 変数上書き前の旧値を解放(weak-fail を成立させる) + - SSA `Copy` により同一 `Arc` を複数 `ValueId` が参照するため、VM は **alias も含めて** `regs` から除去する + +実装の責務分離(hygiene): +- VM 側の KeepAlive 処理は `src/backend/mir_interpreter/handlers/lifecycle.rs` に隔離(dispatch から分離) + +### Verification + +- `apps/tests/phase285_weak_basic.hako`: exit 2 ✅ +- `apps/tests/phase285_p2_weak_upgrade_fail_min.hako`: exit 1 ✅ +- quick gate: `./tools/smokes/v2/run.sh --profile quick` → 154/154 PASS ✅ + ## LLVM Sub-Phases Status | Phase | Status | Summary | @@ -233,18 +259,13 @@ If any of the following are missing, treat weak smokes as **unsupported** and sc 1. `tools/smokes/v2/profiles/integration/apps/phase285_p2_weak_upgrade_success_vm.sh` - Fixture A 実行、期待: **exit 2** → **PASS** 2. `tools/smokes/v2/profiles/integration/apps/phase285_p2_weak_upgrade_fail_vm.sh` - - Fixture B 実行、**SKIP**(既知の hidden root 問題) + - Fixture B 実行、期待: **exit 1** → **PASS** **LLVM smoke scripts(2本)**: 3. `tools/smokes/v2/profiles/integration/apps/phase285_p2_weak_upgrade_success_llvm.sh` - Fixture A 実行(LLVM harness)、期待: **exit 2** → **PASS** または理由付き SKIP 4. `tools/smokes/v2/profiles/integration/apps/phase285_p2_weak_upgrade_fail_llvm.sh` - - Fixture B 実行(LLVM harness)、**SKIP**(既知の hidden root 問題、VM と同じ) - -**既知の問題(Fixture B)**: -- **Hidden root issue**: `x = null` で明示 drop しても weak_to_strong が成功(exit 0) -- 原因: 隠れた root が strong ref を保持している可能性 -- 対応: Phase 285 P2.1 (investigation) で root 保持箇所を棚卸し + - Fixture B 実行(LLVM harness)、期待: **exit 1** → **PASS** または理由付き SKIP **LLVM 対応**: - WeakNew/WeakLoad は **両バックエンド実装済み**(P1 確認済み)→ PASS が理想 @@ -256,30 +277,16 @@ If any of the following are missing, treat weak smokes as **unsupported** and sc - ✅ Fixture B 新規作成(明示 drop 方式) - ✅ VM smoke success PASS - ✅ LLVM smoke success PASS(または理由付き SKIP) -- ✅ VM/LLVM smoke fail SKIP(hidden root 問題で理由付き) +- ✅ VM smoke fail PASS(exit 1) +- ✅ LLVM smoke fail PASS(または理由付き SKIP) - ✅ quick 154/154 PASS 維持 - ✅ Finalizer は「VM のみ・LLVM 未対応」と差分表に明記済み(上記 VM/LLVM 差分分類テーブル参照) **P2 で扱わない項目**: - **Block scope drop conformance** → 別タスク(未整合の可能性あり) -- **Hidden root investigation** → Phase 285 P2.1 で root 保持箇所を棚卸し - Finalizer (`fini()`) の統一テスト → 両バックエンド未実装のため Phase 286+ で検討 - GC cycle collection → Reference Count のみで既知の制約 -### P2.1(investigation)- 提案 - -**目的**: Hidden root 問題の原因特定(`x = null` で明示 drop しても weak_to_strong が成功する理由) - -**調査対象**: -- VM の strong ref 保持箇所(VMValue/registry/handles/scope_tracker/等) -- Arc drop タイミング(明示 `x = null` 代入時に Arc が drop されるか) -- 隠れた root 候補(MIR interpreter state/local variables/等) - -**期待成果**: -- Hidden root の特定(どこが strong ref を保持しているか) -- 分類: (B) 未実装 / (C) 既知バグ / (D) 仕様外 -- 修正方針の提示(Phase 286+ で修正) - ## Non-goals - GC アルゴリズム刷新(RC→tracing 等の設計変更) diff --git a/src/backend/mir_interpreter/handlers/lifecycle.rs b/src/backend/mir_interpreter/handlers/lifecycle.rs new file mode 100644 index 00000000..b90fb9b2 --- /dev/null +++ b/src/backend/mir_interpreter/handlers/lifecycle.rs @@ -0,0 +1,48 @@ +use super::*; +use std::collections::HashSet; +use std::sync::Arc; + +impl MirInterpreter { + pub(super) fn handle_keepalive( + &mut self, + values: &[ValueId], + drop_after: bool, + ) -> Result<(), VMError> { + if drop_after { + self.release_strong_refs(values); + } + Ok(()) + } + + fn release_strong_refs(&mut self, values: &[ValueId]) { + let mut arc_ptrs: HashSet<*const dyn NyashBox> = HashSet::new(); + + for value_id in values { + if let Some(VMValue::BoxRef(arc)) = self.regs.get(value_id) { + arc_ptrs.insert(Arc::as_ptr(arc)); + } + } + + for value_id in values { + self.regs.remove(value_id); + } + + if arc_ptrs.is_empty() { + return; + } + + let to_remove: Vec = self + .regs + .iter() + .filter_map(|(value_id, value)| match value { + VMValue::BoxRef(arc) if arc_ptrs.contains(&Arc::as_ptr(arc)) => Some(*value_id), + _ => None, + }) + .collect(); + + for value_id in to_remove { + self.regs.remove(&value_id); + } + } +} + diff --git a/src/backend/mir_interpreter/handlers/mod.rs b/src/backend/mir_interpreter/handlers/mod.rs index 3c3fd951..3630da2a 100644 --- a/src/backend/mir_interpreter/handlers/mod.rs +++ b/src/backend/mir_interpreter/handlers/mod.rs @@ -21,6 +21,7 @@ mod boxes_void_guards; mod calls; mod extern_provider; mod externals; +mod lifecycle; mod memory; mod misc; mod type_ops; @@ -135,49 +136,7 @@ impl MirInterpreter { | MirInstruction::Barrier { .. } | MirInstruction::Safepoint => {} MirInstruction::KeepAlive { values, drop_after } => { - // Phase 285 P2.1: Handle KeepAlive based on drop_after flag - // - drop_after=true: Release values (for variable overwrite, enables weak ref failure) - // - drop_after=false: Just keep alive (for scope end, values may be needed for PHI) - if *drop_after { - // IMPORTANT: Due to SSA Copy instructions, a single Box may have multiple - // ValueIds pointing to it (e.g., %5 = NewBox, %6 = Copy %5). - // We need to find and remove ALL ValueIds that point to the same Arc. - use std::sync::Arc; - use super::super::vm_types::VMValue; - - // Collect raw pointers of Arcs being released - let mut arc_ptrs: Vec<*const dyn crate::box_trait::NyashBox> = Vec::new(); - for v in values { - if let Some(VMValue::BoxRef(arc)) = self.regs.get(v) { - arc_ptrs.push(Arc::as_ptr(arc)); - } - } - - // Remove the specified values first - for v in values { - self.regs.remove(v); - } - - // Find and remove ALL other ValueIds that point to the same Arcs - if !arc_ptrs.is_empty() { - let to_remove: Vec = self.regs - .iter() - .filter_map(|(vid, val)| { - if let VMValue::BoxRef(arc) = val { - let ptr = Arc::as_ptr(arc); - if arc_ptrs.contains(&ptr) { - return Some(*vid); - } - } - None - }) - .collect(); - for vid in to_remove { - self.regs.remove(&vid); - } - } - } - // If drop_after=false, do nothing (values stay alive) + self.handle_keepalive(values, *drop_after)?; } MirInstruction::Nop => {} other => {