diff --git a/AGENTS.md b/AGENTS.md index dcd59fbe..5ade0f31 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -216,6 +216,16 @@ fn check_layer_boundary() { 3. テストを書く 4. 最後に実装 +### 7.1 docs 置き場所 SSOT(迷子防止) + +`docs/development/current/` は文書が増えやすいので、**入口/設計図/Phaseログ/調査ログを混ぜない**運用を必須にするよ。 + +- SSOT: `docs/development/current/main/DOCS_LAYOUT.md` +- 追加ルール(最小): + - 新しい Phase 文書は `docs/development/current/main/phases/` 配下に置く(`main/` 直下に増やさない) + - 長期参照の設計図は `docs/development/current/main/design/` に置く + - 切り分けログは `docs/development/current/main/investigations/` に置き、結論だけ `10-Now.md` / `20-Decisions.md` に反映する + --- **Fail-Fast原則**: フォールバック処理は原則禁止。過去に分岐ミスでエラー発見が遅れた経験から、エラーは早期に明示的に失敗させること。特にChatGPTが入れがちなフォールバック処理には要注意だよ! diff --git a/CLAUDE.md b/CLAUDE.md index 9473f0f8..03743a48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -760,6 +760,19 @@ box MyBox { ## 📚 ドキュメント構造 +### 📋 **ドキュメント配置ルール(SSOT)** ⭐NEW +**入口**: [docs/development/current/main/DOCS_LAYOUT.md](docs/development/current/main/DOCS_LAYOUT.md) + +**3つの最小ルール**(必読!): +1. **Phase 文書** → `docs/development/current/main/phases/phase-/` (`main/` 直下に増やさない) +2. **設計図** → `docs/development/current/main/design/` (複数 Phase で参照される) +3. **調査ログ** → `docs/development/current/main/investigations/` (結論は 10-Now.md に反映) + +**受け皿フォルダ別 README**: +- [phases/README.md](docs/development/current/main/phases/README.md) - Phase ログの説明 +- [design/README.md](docs/development/current/main/design/README.md) - 設計図の説明 +- [investigations/README.md](docs/development/current/main/investigations/README.md) - 調査ログの説明 + ### 🎯 最重要ドキュメント(開発者向け) - **[Phase 15 セルフホスティング計画](docs/private/roadmap2/phases/phase-15/self-hosting-plan.txt)** - 80k→20k行革命 - **[Phase 15 ROADMAP](docs/private/roadmap2/phases/phase-15/ROADMAP.md)** - 現在の進捗チェックリスト diff --git a/docs/development/current/main/00-Overview.md b/docs/development/current/main/00-Overview.md index 604b50ff..6d812956 100644 --- a/docs/development/current/main/00-Overview.md +++ b/docs/development/current/main/00-Overview.md @@ -18,6 +18,8 @@ - 「JoinIR / Selfhost まわりで、まずどのドキュメントを読むべきか」は `docs/development/current/main/01-JoinIR-Selfhost-INDEX.md` を入口として使ってね。 +- 「docs が増えて迷子になる」問題のための置き場所ルール(SSOT)は + `docs/development/current/main/DOCS_LAYOUT.md` を参照してね。 - JoinIR 全体のアーキテクチャと箱の関係は `docs/development/current/main/joinir-architecture-overview.md` を SSOT として参照するよ。 - selfhost / .hako 側から JoinIR を使うときも、この JoinIR 設計を前提にして設計・実装する方針だよ。 diff --git a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md index fa36ab21..fb0d745f 100644 --- a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md +++ b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md @@ -6,6 +6,10 @@ Scope: JoinIR と Selfhost(Stage‑B/Stage‑1/Stage‑3)に関する「最 このファイルは、JoinIR と Selfhost ラインの主戦場をすばやく把握するためのインデックスだよ。 歴史メモや詳細な Phase 文書に飛ぶ前に、まずここに載っている現役ドキュメントから辿っていくことを想定しているよ。 +docs の置き場所(SSOT/Phase/調査ログの分離ルール)は、先にこれを読むと迷子になりにくいよ。 + +- `docs/development/current/main/DOCS_LAYOUT.md` + --- diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index f0c4fd02..1bf1bb3a 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,6 +2,10 @@ ## 2025‑12‑14:現状サマリ +(補足)docs が増えて迷子になったときの「置き場所ルール(SSOT)」: + +- `docs/development/current/main/DOCS_LAYOUT.md` + ### JoinIR / Loop / If ライン - LoopBuilder は Phase 186‑187 で完全削除済み。**JoinIR が唯一の loop lowering 経路**。 diff --git a/docs/development/current/main/DOCS_LAYOUT.md b/docs/development/current/main/DOCS_LAYOUT.md new file mode 100644 index 00000000..0fe354d8 --- /dev/null +++ b/docs/development/current/main/DOCS_LAYOUT.md @@ -0,0 +1,81 @@ +# Docs Layout (SSOT) + +Status: SSOT +Scope: `docs/development/current/` 以下の「置き場所ルール」と、SSOT/履歴メモの混在を防ぐための最小ガイド。 + +## 目的 + +- 入口(読む順序)と、詳細(設計図/調査/Phaseログ)を分離して迷子を防ぐ。 +- “Phase 文書が増えても” SSOT が埋もれないようにする。 +- 大規模移動はしない(リンク切れ回避)。以後の追加分から秩序を作る。 + +## ディレクトリの役割(推奨) + +### `docs/development/current/main/`(入口・現状) + +ここは「まず読む」入口を置く場所。SSOT を全部ここに置かない。 + +- 入口(例): `00-Overview.md`, `01-JoinIR-Selfhost-INDEX.md`, `10-Now.md`, `20-Decisions.md`, `30-Backlog.md` + +### `docs/development/current/main/design/`(設計図・SSOT寄り) + +設計の SSOT / 長期参照の設計図を置く。 + +- 原則: Phase 依存のログ/作業記録は置かない(それは phases へ)。 +- 例: JoinIR の設計、Boundary/ExitLine の契約、Loop パターン空間、runtime/box 解決の地図。 + +### `docs/development/current/main/investigations/`(調査ログ) + +不具合調査のログ、切り分け、暫定メモを置く。 + +- 原則: “結論” は `10-Now.md` / `20-Decisions.md` / 該当 design doc に反映し、調査ログ自体は参照用に残す。 +- 原則: 調査ログを SSOT にしない(参照元を明記して“歴史化”できる形にする)。 + +### `docs/development/current/main/phases/`(Phaseログ) + +Phase ごとの記録・完了サマリ・実装チェックリストを置く。 + +- 推奨構造: + - `docs/development/current/main/phases/phase-131/` + - `docs/development/current/main/phases/phase-131/131-03-llvm-lowering-inventory.md` + - `docs/development/current/main/phases/phase-131/131-11-case-c-summary.md` + +## ドキュメントの種別(ファイル先頭に明記) + +追加/更新する文書の先頭に、最低限これを付ける。 + +``` +Status: SSOT | Active | Historical +Scope: ... +Related: +- <入口/SSOT> +``` + +- `SSOT`: 現行の正本(同じテーマの“別名ファイル”を増やさない)。 +- `Active`: 現行だが SSOT ではない(実装の手順書/チェックリスト等)。 +- `Historical`: 参照用(当時の調査・ログ)。入口や Now から “歴史” としてリンクする。 + +## 移行ポリシー(リンク切れ防止) + +既存のファイルは大量移動しない。移動が必要な場合は必ず旧パスに“転送スタブ”を残す。 + +例(旧ファイルの内容を最小化): + +``` +# Moved + +Moved to: docs/development/current/main/phases/phase-131/131-03-llvm-lowering-inventory.md +``` + +## 命名(推奨) + +- Phase 文書: `phase-/` + `--.md`(同一フェーズ内で並べ替えが自然) +- 調査ログ: `-investigation-YYYY-MM-DD.md` など(時系列が分かる形) +- 入口/SSOT: “Phase番号を入れない” ことを基本にする(寿命が長いので) + +## 運用の最小ルール + +- 新しい Phase 文書は `main/phases/` に入れる(`main/` 直下に増やさない)。 +- 設計図(SSOT)は `main/design/` に寄せる(Phase の完了サマリと混ぜない)。 +- `10-Now.md` は「現状の要約+正本リンク」に徹し、詳細ログの本文は抱え込まない。 + diff --git a/docs/development/current/main/design/README.md b/docs/development/current/main/design/README.md new file mode 100644 index 00000000..61a4f82f --- /dev/null +++ b/docs/development/current/main/design/README.md @@ -0,0 +1,7 @@ +# design/ + +`docs/development/current/main/design/` は、長期参照する設計図(SSOT 寄り)を置く場所。 + +- 原則: “Phaseの作業ログ/完了報告” は `../phases/` に置く。 +- 原則: “不具合調査ログ” は `../investigations/` に置く。 + diff --git a/docs/development/current/main/investigations/README.md b/docs/development/current/main/investigations/README.md new file mode 100644 index 00000000..6e61d65d --- /dev/null +++ b/docs/development/current/main/investigations/README.md @@ -0,0 +1,61 @@ +# 調査ログ・根本原因分析 + +このフォルダは、バグ修正・最適化の過程で発見した根本原因分析・調査プロセスを保管します。 + +## 参照方法 + +1. **「このバグの根本原因は?」** → investigations/ で検索 +2. **「この設計決定の背景は?」** → [../20-Decisions.md](../20-Decisions.md) で確認 +3. **「実装の詳細は?」** → [../phases/](../phases/README.md) で確認 + +## 命名規則 + +- **形式**: `-investigation-YYYY-MM-DD.md` または `-root-cause-analysis.md` +- **目的**: 時系列が分かる形、または主題ごとに整理 + +## 最新調査 + +- `python-resolver-investigation.md` - Python LLVM バックエンド resolver.is_stringish() 調査 +- `phase131-11-root-cause-analysis.md` - PHI 型推論循環依存分析 + +## 作成ルール(SSOT) + +詳しくは [../DOCS_LAYOUT.md](../DOCS_LAYOUT.md) を参照。 + +- ✅ **置き場所**: `investigations/` 配下のみ +- ✅ **内容**: 詳細な根本原因分析、デバッグプロセス、試行錯誤の記録 +- ✅ **結論反映**: 調査結果の結論は以下に反映 + - [../10-Now.md](../10-Now.md) - 現在の進行状況サマリー + - [../20-Decisions.md](../20-Decisions.md) - 設計決定記録 + - [../design/](../design/README.md) - アーキテクチャ設計書(必要な場合) +- ❌ **避けるべき**: 調査ログそのものを SSOT にしない + +## 使用例 + +### 調査ログ作成時 +```markdown +# Python LLVM バックエンド resolver.is_stringish() 調査 + +**日時**: 2025-12-14 +**担当**: taskちゃん +**目的**: Case C で Result: 0 が出力される原因特定 + +## 調査フロー +1. ... +2. ... + +## 根本原因 +``` + +### 結論反映時(10-Now.md) +```markdown +## 🔍 Phase 131-11-E: TypeFacts/TypeDemands 分離 + +**根本原因**: MIR Builder の後方伝播型推論 +- **詳細**: [investigations/python-resolver-investigation.md](investigations/python-resolver-investigation.md) +- **修正**: PhiTypeResolver が TypeFacts のみ参照 +``` + +--- + +**最終更新**: 2025-12-14 diff --git a/docs/development/current/main/phases/README.md b/docs/development/current/main/phases/README.md new file mode 100644 index 00000000..446cc050 --- /dev/null +++ b/docs/development/current/main/phases/README.md @@ -0,0 +1,44 @@ +# Phase ドキュメント + +このフォルダは、実装フェーズ(Phase 131, Phase 33 等)ごとの詳細記録を保管します。 + +## 現在の Phase + +- **Phase 131**: LLVM Lowering & InfiniteEarlyExit パターン実装 🚀 +- **Phase 33**: Box Theory Modularization + +## Phase フォルダ構成(推奨) + +``` +phases/phase-131/ +├── README.md (Phase 全体概要) +├── 131-03-llvm-lowering-inventory.md (LLVM 部分のテスト・検証) +├── 131-11-case-c-summary.md (Case C 実装サマリー) +└── phase131-11-case-c-root-cause-analysis.md (根本原因分析) +``` + +## 参照方法 + +1. **現在の Phase を知りたい** → [../10-Now.md](../10-Now.md) +2. **該当 Phase を詳しく知りたい** → フォルダを開く +3. **設計背景を知りたい** → [../design/](../design/README.md) +4. **調査ログを見たい** → [../investigations/](../investigations/README.md) + +## Phase 命名規則 + +- **ファイル名**: `phase--/` (例: `phase-131/`) +- **文書名**: `<N>-<NN>-<topic>.md` (例: `131-11-case-c-summary.md`) + - Phase 番号で自然にソート可能 + - 同一 Phase 内で段階的に追跡可能 + +## 作成ルール(SSOT) + +詳しくは [../DOCS_LAYOUT.md](../DOCS_LAYOUT.md) を参照。 + +- ✅ **置き場所**: `phases/phase-<N>/` 配下のみ +- ✅ **内容**: Phase の実装記録・進捗・チェックリスト・検証結果 +- ❌ **避けるべき**: 複数 Phase で参照される設計・アーキテクチャ(→ design/ へ) + +--- + +**最終更新**: 2025-12-14 diff --git a/src/llvm_py/instructions/binop.py b/src/llvm_py/instructions/binop.py index 0186c828..7c951263 100644 --- a/src/llvm_py/instructions/binop.py +++ b/src/llvm_py/instructions/binop.py @@ -138,18 +138,25 @@ def lower_binop( # except Exception: # pass - # tagged string handles?(どちらかが string-ish のとき) + # Phase 196: TypeFacts SSOT - Only check for actual string types (not use-site demands) + # Check if BOTH operands are known to be strings from their definition any_tagged = False try: if resolver is not None: - if hasattr(resolver, 'is_stringish'): - any_tagged = resolver.is_stringish(lhs) or resolver.is_stringish(rhs) - # literal strings are tracked separately - if not any_tagged and hasattr(resolver, 'string_literals'): + # Only check string_literals (TypeFacts), NOT is_stringish (TypeDemands) + if hasattr(resolver, 'string_literals'): any_tagged = (lhs in resolver.string_literals) or (rhs in resolver.string_literals) + # Check if resolver has explicit type information (MirType::String or StringBox) + if not any_tagged and hasattr(resolver, 'value_types'): + lhs_ty = resolver.value_types.get(lhs) + rhs_ty = resolver.value_types.get(rhs) + lhs_str = lhs_ty and (lhs_ty.get('kind') == 'string' or + (lhs_ty.get('kind') == 'handle' and lhs_ty.get('box_type') == 'StringBox')) + rhs_str = rhs_ty and (rhs_ty.get('kind') == 'string' or + (rhs_ty.get('kind') == 'handle' and rhs_ty.get('box_type') == 'StringBox')) + any_tagged = lhs_str or rhs_str except Exception: pass - # Phase 131-6: Removed force_string from this check is_str = is_ptr_side or any_tagged # Phase 131-6 DEBUG @@ -184,12 +191,25 @@ def lower_binop( return val return ir.Constant(i64, 0) - # Decide route: handle+handle when both sides are string-ish; otherwise pointer+int route. + # Phase 196: TypeFacts SSOT - Use handle+handle only when BOTH are strings lhs_tag = False; rhs_tag = False try: - if resolver is not None and hasattr(resolver, 'is_stringish'): - lhs_tag = resolver.is_stringish(lhs) - rhs_tag = resolver.is_stringish(rhs) + if resolver is not None: + # Check string_literals (actual string constants) + if hasattr(resolver, 'string_literals'): + lhs_tag = lhs in resolver.string_literals + rhs_tag = rhs in resolver.string_literals + # Check value_types for String/StringBox types + if not lhs_tag and hasattr(resolver, 'value_types'): + lhs_ty = resolver.value_types.get(lhs) + if lhs_ty and (lhs_ty.get('kind') == 'string' or + (lhs_ty.get('kind') == 'handle' and lhs_ty.get('box_type') == 'StringBox')): + lhs_tag = True + if not rhs_tag and hasattr(resolver, 'value_types'): + rhs_ty = resolver.value_types.get(rhs) + if rhs_ty and (rhs_ty.get('kind') == 'string' or + (rhs_ty.get('kind') == 'handle' and rhs_ty.get('box_type') == 'StringBox')): + rhs_tag = True except Exception: pass if lhs_tag and rhs_tag: diff --git a/src/mir/builder/ops.rs b/src/mir/builder/ops.rs index f4b1a2f6..66dc1bb0 100644 --- a/src/mir/builder/ops.rs +++ b/src/mir/builder/ops.rs @@ -67,7 +67,7 @@ impl super::MirBuilder { super::builder_calls::CallTarget::Global(name), vec![lhs, rhs], )?; - // 型注釈(Phase 3-B: value_origin_newbox もチェック&両方登録) + // Phase 196: TypeFacts SSOT - AddOperator call type annotation let lhs_is_str = match self.value_types.get(&lhs) { Some(MirType::String) => true, Some(MirType::Box(bt)) if bt == "StringBox" => true, @@ -86,14 +86,17 @@ impl super::MirBuilder { .map(|s| s == "StringBox") .unwrap_or(false), }; - if lhs_is_str || rhs_is_str { + if lhs_is_str && rhs_is_str { + // BOTH are strings: result is string self.value_types .insert(dst, MirType::Box("StringBox".to_string())); self.value_origin_newbox .insert(dst, "StringBox".to_string()); - } else { + } else if !lhs_is_str && !rhs_is_str { + // NEITHER is a string: numeric addition self.value_types.insert(dst, MirType::Integer); } + // else: Mixed - leave Unknown for use-site coercion } else if all_call { // Lower other arithmetic ops to operator boxes under ALL flag let (name, guard_prefix) = match op { @@ -152,8 +155,10 @@ impl super::MirBuilder { } else { self.emit_instruction(MirInstruction::BinOp { dst, op, lhs, rhs })?; } + // Phase 196: TypeFacts SSOT - BinOp type is determined by operands only + // String concatenation is handled at use-site in LLVM lowering if matches!(op, crate::mir::BinaryOp::Add) { - // Phase 3-B: value_origin_newbox もチェック&両方登録 + // Check if BOTH operands are known to be strings (TypeFacts) let lhs_is_str = match self.value_types.get(&lhs) { Some(MirType::String) => true, Some(MirType::Box(bt)) if bt == "StringBox" => true, @@ -172,14 +177,18 @@ impl super::MirBuilder { .map(|s| s == "StringBox") .unwrap_or(false), }; - if lhs_is_str || rhs_is_str { + if lhs_is_str && rhs_is_str { + // BOTH are strings: result is definitely a string self.value_types .insert(dst, MirType::Box("StringBox".to_string())); self.value_origin_newbox .insert(dst, "StringBox".to_string()); - } else { + } else if !lhs_is_str && !rhs_is_str { + // NEITHER is a string: numeric addition self.value_types.insert(dst, MirType::Integer); } + // else: Mixed types (string + int or int + string) + // Leave dst type as Unknown - LLVM will handle coercion at use-site } else { self.value_types.insert(dst, MirType::Integer); } @@ -195,8 +204,10 @@ impl super::MirBuilder { } else { self.emit_instruction(MirInstruction::BinOp { dst, op, lhs, rhs })?; } + // Phase 196: TypeFacts SSOT - BinOp type is determined by operands only + // String concatenation is handled at use-site in LLVM lowering if matches!(op, crate::mir::BinaryOp::Add) { - // Phase 3-B: value_origin_newbox もチェック&両方登録 + // Check if BOTH operands are known to be strings (TypeFacts) let lhs_is_str = match self.value_types.get(&lhs) { Some(MirType::String) => true, Some(MirType::Box(bt)) if bt == "StringBox" => true, @@ -215,14 +226,18 @@ impl super::MirBuilder { .map(|s| s == "StringBox") .unwrap_or(false), }; - if lhs_is_str || rhs_is_str { + if lhs_is_str && rhs_is_str { + // BOTH are strings: result is definitely a string self.value_types .insert(dst, MirType::Box("StringBox".to_string())); self.value_origin_newbox .insert(dst, "StringBox".to_string()); - } else { + } else if !lhs_is_str && !rhs_is_str { + // NEITHER is a string: numeric addition self.value_types.insert(dst, MirType::Integer); } + // else: Mixed types (string + int or int + string) + // Leave dst type as Unknown - LLVM will handle coercion at use-site } else { self.value_types.insert(dst, MirType::Integer); } diff --git a/src/runner/mir_json_emit.rs b/src/runner/mir_json_emit.rs index 3db81bab..0b288a95 100644 --- a/src/runner/mir_json_emit.rs +++ b/src/runner/mir_json_emit.rs @@ -343,7 +343,8 @@ pub fn emit_mir_json_for_harness( B::Or => "|", }; let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()}); - // dst_type hint for string concatenation: if either side is String-ish and op is '+', mark result as String handle + // dst_type hint for string concatenation: ONLY if BOTH sides are explicitly String-ish and op is '+', mark result as String handle + // Option C: Unknown/None types default to Integer arithmetic (conservative) if matches!(op, B::Add) { let lhs_is_str = match f.metadata.value_types.get(lhs) { Some(MirType::String) => true, @@ -355,7 +356,9 @@ pub fn emit_mir_json_for_harness( Some(MirType::Box(bt)) if bt == "StringBox" => true, _ => false, }; - if lhs_is_str || rhs_is_str { + // Changed: require BOTH to be explicitly String (lhs_is_str && rhs_is_str) + // Default: Unknown → Integer arithmetic + if lhs_is_str && rhs_is_str { obj["dst_type"] = json!({"kind":"handle","box_type":"StringBox"}); } @@ -733,6 +736,7 @@ pub fn emit_mir_json_for_harness_bin( B::Or => "|", }; let mut obj = json!({"op":"binop","operation": op_s, "lhs": lhs.as_u32(), "rhs": rhs.as_u32(), "dst": dst.as_u32()}); + // Option C: Unknown/None types default to Integer arithmetic (conservative) if matches!(op, B::Add) { let lhs_is_str = match f.metadata.value_types.get(lhs) { Some(MirType::String) => true, @@ -744,7 +748,9 @@ pub fn emit_mir_json_for_harness_bin( Some(MirType::Box(bt)) if bt == "StringBox" => true, _ => false, }; - if lhs_is_str || rhs_is_str { + // Changed: require BOTH to be explicitly String (lhs_is_str && rhs_is_str) + // Default: Unknown → Integer arithmetic + if lhs_is_str && rhs_is_str { obj["dst_type"] = json!({"kind":"handle","box_type":"StringBox"}); }