diff --git a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md index fb0d745f..557d66cc 100644 --- a/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md +++ b/docs/development/current/main/01-JoinIR-Selfhost-INDEX.md @@ -38,7 +38,9 @@ JoinIR の箱構造と責務、ループ/if の lowering パターンを把握 - `docs/development/current/main/loop_pattern_space.md` 3. Boundary / ExitLine / Carrier の具体パターン - `docs/development/current/main/joinir-boundary-builder-pattern.md` -4. Scope/BindingId(shadowing・束縛同一性の段階移行) +4. JoinIR 設計地図(現役の地図) + - `docs/development/current/main/design/joinir-design-map.md` +5. Scope/BindingId(shadowing・束縛同一性の段階移行) - `docs/development/current/main/phase73-scope-manager-design.md` - `docs/development/current/main/PHASE_74_SUMMARY.md` - `docs/development/current/main/PHASE_75_SUMMARY.md` @@ -46,16 +48,16 @@ JoinIR の箱構造と責務、ループ/if の lowering パターンを把握 - `docs/development/current/main/phase78-bindingid-promoted-carriers.md` - `docs/development/current/main/phase80-bindingid-p3p4-plan.md`(P3/P4 への配線計画) - `docs/development/current/main/phase81-pattern2-exitline-contract.md`(promoted carriers の ExitLine 契約検証) -5. Boxification feedback(Phase 78–85 の振り返りと Phase 86 推奨) +6. Boxification feedback(Phase 78–85 の振り返りと Phase 86 推奨) - `docs/development/current/main/phase78-85-boxification-feedback.md` -6. Phase 86: Carrier Init Builder + Error Tags ✅ +7. Phase 86: Carrier Init Builder + Error Tags ✅ - **Status**: COMPLETE (2025-12-13) - **Modules**: - `src/mir/builder/control_flow/joinir/merge/carrier_init_builder.rs` (+8 tests) - `src/mir/join_ir/lowering/error_tags.rs` (+5 tests) - **Achievements**: SSOT 確立(CarrierInit → ValueId 生成統一、エラータグ中央化、DebugOutputBox 完全移行) - **Impact**: 987/987 tests PASS, +13 unit tests, Single Responsibility validated -7. Phase 87: LLVM Exe Line SSOT ✅ +8. Phase 87: LLVM Exe Line SSOT ✅ - **Status**: COMPLETE (2025-12-13) - **SSOT**: `tools/build_llvm.sh` - Single pipeline for .hako → executable - **Deliverables**: @@ -64,11 +66,11 @@ JoinIR の箱構造と責務、ループ/if の lowering パターンを把握 - Integration smoke: `tools/smokes/v2/profiles/integration/apps/phase87_llvm_exe_min.sh` (SKIP if no LLVM) - **Policy**: No script duplication, integration smoke only (not quick), graceful SKIP - **Impact**: Standard procedure established, prerequisites documented -8. 代表的な Phase 文書(現役ラインとの接点だけ絞ったもの) +9. 代表的な Phase 文書(現役ラインとの接点だけ絞ったもの) - `docs/development/current/main/phase33-16-INDEX.md` - `docs/development/current/main/phase33-17-joinir-modularization-analysis.md` - `docs/development/current/main/phase183-selfhost-depth2-joinir-status.md` -9. Phase 86–90(Loop frontends)の要約(1枚) +10. Phase 86–90(Loop frontends)の要約(1枚) - `docs/development/current/main/phase86-90-loop-frontends-summary.md` Phase 文書は歴史や検証ログも含むので、「JoinIR の現役設計を確認した上で、必要なときだけ掘る」という前提で読んでね。 diff --git a/docs/development/current/main/design/joinir-design-map.md b/docs/development/current/main/design/joinir-design-map.md new file mode 100644 index 00000000..ba654920 --- /dev/null +++ b/docs/development/current/main/design/joinir-design-map.md @@ -0,0 +1,146 @@ +# JoinIR Design Map(現役の地図) + +Status: Active +Scope: JoinIR の「Loop/If を JoinIR 化して MIR に統合する」導線(検出→shape guard→lower→merge→契約検証) +Related: +- SSOT: [`docs/development/current/main/joinir-architecture-overview.md`](../joinir-architecture-overview.md) +- SSOT: [`docs/development/current/main/loop_pattern_space.md`](../loop_pattern_space.md) +- SSOT: [`docs/development/current/main/joinir-boundary-builder-pattern.md`](../joinir-boundary-builder-pattern.md) + +このドキュメントは Phase ログではなく、「JoinIR を触る人が迷子にならず、どこを直すべきかが一発で分かる」ための設計図(地図)です。 +詳細な経緯・作業ログは `docs/development/current/main/phases/` と `docs/development/current/main/investigations/` に分離します。 + +--- + +## 1枚図: レイヤー(AST → JoinIR → MIR → Backend) + +```mermaid +flowchart LR + A[AST] -->|Frontend lowering| J1[JoinIR (Structured)] + J1 -->|Normalize / Shape guard| J2[JoinIR (Normalized)] + J2 -->|JoinIR → MIR bridge| M1[MIR Module] + M1 -->|Merge into host function| M2[MIR (in builder)] + M2 --> B[Backend\n(VM / LLVM / Cranelift)] + + subgraph Frontend + A + J1 + end + + subgraph JoinIR Core + J2 + end + + subgraph MIR Builder + M1 + M2 + end +``` + +読み方: +- 「Loop/If の形が認識されない」: Pattern 検出(Feature/Kind)と shape guard を見る +- 「JoinIR は生成できるが統合で壊れる」: Merge(ValueId/PHI/ExitLine/Boundary)と契約検証を見る +- 「なぜこのエラータグが出たか」: ErrorTags(SSOT)を起点に呼び出し元へ辿る + +--- + +## “箱”の責務マップ(担当境界) + +| 領域 | 役割(何を決めるか) | 主な入口/箱(SSOT寄り) | 主な出力 | Fail-Fast(典型) | +|---|---|---|---|---| +| Pattern検出 | ループ形を分類し、どの lowerer に渡すか決める | [`src/mir/builder/control_flow/joinir/patterns/router.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/router.rs), [`src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs), [`src/mir/loop_pattern_detection/mod.rs`](../../../../../src/mir/loop_pattern_detection/mod.rs) | `LoopPatternKind` / Pattern 選択 | 「分類不能」→ 明示的に Err(サイレントな非JoinIR退避は禁止) | +| shape guard | 「この shape なら lower/merge 契約が成立する」を保証する | [`src/mir/join_ir/normalized/shape_guard.rs`](../../../../../src/mir/join_ir/normalized/shape_guard.rs), `src/mir/builder/control_flow/joinir/patterns/*_validator.rs` | shape OK / 詳細診断 | shape 不一致を握りつぶさず Err | +| lowering | JoinIR(Structured/Normalized)を生成する | [`src/mir/join_ir/lowering/mod.rs`](../../../../../src/mir/join_ir/lowering/mod.rs), `src/mir/builder/control_flow/joinir/patterns/pattern*_*.rs` | `JoinModule` | 未対応の構造は `error_tags::freeze(...)` 等で Err | +| merge | JoinIR→MIR 変換後、ホスト関数に統合する | [`src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs), [`src/mir/builder/control_flow/joinir/merge/mod.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/mod.rs) | ホスト MIR のブロック/ValueId 更新 | ValueId 競合、ExitLine 未接続、PHI 破綻を Err | +| ExitMeta | 「出口でどの carrier をどの host slot に戻すか」のメタ | [`src/mir/join_ir/lowering/carrier_info.rs`](../../../../../src/mir/join_ir/lowering/carrier_info.rs), [`src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs) | `ExitMeta` / `exit_bindings` | carrier 不整合(不足/過剰)を Err | +| CarrierInit | carrier 初期化の SSOT(FromHost/Const/LoopLocal) | [`src/mir/builder/control_flow/joinir/merge/carrier_init_builder.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/carrier_init_builder.rs), [`src/mir/join_ir/lowering/carrier_info.rs`](../../../../../src/mir/join_ir/lowering/carrier_info.rs) | 初期値 `ValueId` | 初期化経路の分岐が散らばらない(SSOT を使う) | +| ErrorTags | エラータグ整形の SSOT(検索性・一貫性) | [`src/mir/join_ir/lowering/error_tags.rs`](../../../../../src/mir/join_ir/lowering/error_tags.rs) | 文字列タグ | 文字列ハードコードを避け、タグを一元化 | + +注: +- Pattern 検出は「関数名 by-name 分岐」に依存しない(構造で決める)。必要なら dev/診断限定のガードに閉じ込める。 +- shape guard は「OK なら後工程が前提にできる契約」を固定する場所で、曖昧な許容をしない。 + +--- + +## 入口(コード側のエントリポイント) + +### Loop(builder 側の導線) + +- Router(builder 入口): [`src/mir/builder/control_flow/joinir/routing.rs`](../../../../../src/mir/builder/control_flow/joinir/routing.rs) + - `MirBuilder::try_cf_loop_joinir(...)`(JoinIR ルートへ入る最初の関数) + - `MirBuilder::cf_loop_joinir_impl(...)`(pattern router → legacy binding の順) +- Pattern router(テーブル駆動): [`src/mir/builder/control_flow/joinir/patterns/router.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/router.rs) +- Feature extraction: [`src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs) +- Pattern 実体(代表): + - [`src/mir/builder/control_flow/joinir/patterns/pattern1_minimal.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/pattern1_minimal.rs) + - [`src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs) + - [`src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs) + - [`src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs) + - [`src/mir/builder/control_flow/joinir/patterns/pattern5_infinite_early_exit.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/pattern5_infinite_early_exit.rs) +- 変換パイプライン(JoinIR→MIR→Merge の統一導線): + - [`src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs`](../../../../../src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs) +- Merge(統合の本体): [`src/mir/builder/control_flow/joinir/merge/mod.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/mod.rs) + - ExitLine: [`src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/exit_line/mod.rs) + - Merge の契約検証(debug): [`src/mir/builder/control_flow/joinir/merge/contract_checks.rs`](../../../../../src/mir/builder/control_flow/joinir/merge/contract_checks.rs) + +### JoinIR(IR/正規化/ブリッジ) + +- JoinIR 定義・入口: [`src/mir/join_ir/mod.rs`](../../../../../src/mir/join_ir/mod.rs) +- Normalized / shape guard: [`src/mir/join_ir/normalized/shape_guard.rs`](../../../../../src/mir/join_ir/normalized/shape_guard.rs) +- JoinIR → MIR bridge: [`src/mir/join_ir_vm_bridge/mod.rs`](../../../../../src/mir/join_ir_vm_bridge/mod.rs) + +### 共通(診断とタグ) + +- Trace(JoinIR ルートの統一トレース): [`src/mir/builder/control_flow/joinir/trace.rs`](../../../../../src/mir/builder/control_flow/joinir/trace.rs) +- Error tags(SSOT): [`src/mir/join_ir/lowering/error_tags.rs`](../../../../../src/mir/join_ir/lowering/error_tags.rs) + +--- + +## 不変条件(Fail-Fast) + +JoinIR を触るときは、次を破ったら「即エラーで止める」前提で設計・実装する。 + +### 形状(shape) + +- Pattern は「認識できる shape だけ」を通し、曖昧な許容をしない。 +- 形状が合わないときは `Ok(None)` で静かに進めない(非JoinIRへの退避を作らない)。 + - 例外: 明確な “routing” で「別 JoinIR 経路」を選ぶのは可(同一層内での選択)。 + +### ValueId / PHI / Boundary の世界 + +- JoinIR 内部(JoinValueSpace 等)と host MIR builder の ValueId を混ぜない。 +- Boundary は JoinIR↔host の橋渡し契約: + - `join_inputs` と `host_inputs` の対応が明示される + - Exit 側は “carrier 名” をキーにして reconnection される(ExitMeta/exit_bindings) +- PHI は「誰が確保するか」を固定し、衝突を許さない(PHI dst の予約・再利用禁止)。 + +### ExitLine 契約 + +- ExitLine は「出口へ集約する」ための契約であり、未接続の経路を残さない。 +- carrier/slot の不足・余剰・不整合は `error_tags::exit_line_contract(...)` 等で即エラーにする。 + +--- + +## 追加手順チェックリスト(新しいループ形を飲み込む最小手順) + +「新しいループ形」を JoinIR で扱えるようにするときの最小手順。 + +1. Fixture を追加(再現可能に固定) + - `apps/tests/` または `apps/smokes/` に最小の `.hako` を追加(対象形が一目で分かるもの) +2. Pattern/feature を追加(検出) + - `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`(必要なら feature 抽出を拡張) + - `src/mir/loop_pattern_detection/`(分類/補助解析が必要ならここに追加) +3. shape guard を追加(契約の固定) + - 形状・前提条件を validator として分離し、失敗は Err にする +4. lower を追加(JoinIR を生成) + - 既存 pattern のコピーではなく、「今回の shape が要求する最小構成」にする + - エラーは `src/mir/join_ir/lowering/error_tags.rs` を使いタグを固定する +5. merge/ExitLine を接続(契約が満たされるように) + - carrier/ExitMeta/Boundary が揃っているか確認する +6. Tests を追加(仕様固定) + - unit: pattern/validator/merge の局所テスト + - smoke: `tools/smokes/v2/` の profile に軽いケースを追加(quick を重くしない) +7. Docs を更新(地図を更新) + - `docs/development/current/main/loop_pattern_space.md`(パターン空間に追記が必要なら) + - `docs/development/current/main/joinir-architecture-overview.md`(箱/契約が増えたなら) + - 本ファイル(入口・責務マップの更新) diff --git a/src/llvm_py/instructions/binop.py b/src/llvm_py/instructions/binop.py index 7c951263..039f7282 100644 --- a/src/llvm_py/instructions/binop.py +++ b/src/llvm_py/instructions/binop.py @@ -125,45 +125,54 @@ def lower_binop( # pointer present? is_ptr_side = (hasattr(lhs_raw, 'type') and isinstance(lhs_raw.type, ir.PointerType)) or \ (hasattr(rhs_raw, 'type') and isinstance(rhs_raw.type, ir.PointerType)) - # Phase 131-6 FIX: Do NOT use dst_type hint from MIR JSON! - # The dst_type is a forward-looking type hint that may be incorrect - # (e.g., "i + 1" gets StringBox hint because i is later used as string in print(i)) - # We should only do string concat if operands are actually strings. - # - # OLD CODE (REMOVED): - # force_string = False - # try: - # if isinstance(dst_type, dict) and dst_type.get('kind') == 'handle' and dst_type.get('box_type') == 'StringBox': - # force_string = True - # except Exception: - # pass - # 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 + # Phase 131-11-E: Use dst_type as authoritative hint + # After Phase 131-11-E, dst_type is correctly set by BinOp re-propagation + # - "i64" means integer arithmetic (even if operands are unknown) + # - {"kind": "handle", "box_type": "StringBox"} means string concat + # - None/missing means fallback to operand analysis + explicit_integer = False + explicit_string = False try: - if resolver is not None: - # 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 + if dst_type == "i64": + explicit_integer = True + elif isinstance(dst_type, dict) and dst_type.get('kind') == 'handle' and dst_type.get('box_type') == 'StringBox': + explicit_string = True except Exception: pass - is_str = is_ptr_side or any_tagged - # Phase 131-6 DEBUG + # If explicit type hint is present, use it + if explicit_integer: + is_str = False + elif explicit_string: + is_str = True + else: + # 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: + # 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 + is_str = is_ptr_side or any_tagged + + # Phase 131-11-E DEBUG if os.environ.get('NYASH_BINOP_DEBUG') == '1': print(f"[binop +] lhs={lhs} rhs={rhs} dst={dst}") + print(f" dst_type={dst_type} explicit_integer={explicit_integer} explicit_string={explicit_string}") print(f" is_ptr_side={is_ptr_side} any_tagged={any_tagged} is_str={is_str}") - print(f" dst_type={dst_type}") if is_str: # Helper: convert raw or resolved value to string handle def to_handle(raw, val, tag: str, vid: int): diff --git a/src/mir/builder/lifecycle.rs b/src/mir/builder/lifecycle.rs index d93a6df7..7b8f8417 100644 --- a/src/mir/builder/lifecycle.rs +++ b/src/mir/builder/lifecycle.rs @@ -385,8 +385,12 @@ impl super::MirBuilder { } } + // Phase 131-11-E: BinOp type re-propagation after PHI resolution + // After PHI types are corrected, re-infer BinOp result types + self.repropagate_binop_types(&mut function); + // Phase 131-9: Update function metadata with corrected types - // MUST happen after PHI type correction above + // MUST happen after PHI type correction above AND BinOp re-propagation function.metadata.value_types = self.value_types.clone(); // Phase 82-5: lifecycle.rs バグ修正 - terminator の Return のみをチェック @@ -595,4 +599,83 @@ impl super::MirBuilder { Ok(module) } + + // Phase 131-11-E: Re-propagate BinOp result types after PHI resolution + // This fixes cases where BinOp instructions were created before PHI types were known + fn repropagate_binop_types(&mut self, function: &mut super::MirFunction) { + use crate::mir::MirInstruction; + use crate::mir::MirType; + + let mut binop_updates: Vec<(super::ValueId, MirType)> = Vec::new(); + + for (_bid, bb) in function.blocks.iter() { + for inst in bb.instructions.iter() { + if let MirInstruction::BinOp { dst, op, lhs, rhs } = inst { + // Only handle Add operations (string concat vs numeric addition) + if matches!(op, crate::mir::BinaryOp::Add) { + // Get current lhs/rhs types after PHI resolution + let lhs_type = self.value_types.get(lhs); + let rhs_type = self.value_types.get(rhs); + + // Classify types + let lhs_class = match lhs_type { + Some(MirType::String) => OperandTypeClass::String, + Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String, + Some(MirType::Integer) => OperandTypeClass::Integer, + Some(MirType::Bool) => OperandTypeClass::Integer, + _ => OperandTypeClass::Unknown, + }; + let rhs_class = match rhs_type { + Some(MirType::String) => OperandTypeClass::String, + Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String, + Some(MirType::Integer) => OperandTypeClass::Integer, + Some(MirType::Bool) => OperandTypeClass::Integer, + _ => OperandTypeClass::Unknown, + }; + + use OperandTypeClass::*; + let new_type = match (lhs_class, rhs_class) { + (String, String) => Some(MirType::Box("StringBox".to_string())), + (Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => { + Some(MirType::Integer) + } + _ => None, // Keep Unknown for mixed/unclear cases + }; + + if let Some(new_ty) = new_type { + // Check if type is missing or different + let current_type = self.value_types.get(dst); + if current_type.is_none() || current_type != Some(&new_ty) { + binop_updates.push((*dst, new_ty)); + } + } + } else { + // Other arithmetic ops: always Integer + if !self.value_types.contains_key(dst) { + binop_updates.push((*dst, MirType::Integer)); + } + } + } + } + } + + // Apply updates + for (dst, ty) in binop_updates { + if std::env::var("NYASH_BINOP_REPROP_DEBUG").is_ok() { + eprintln!( + "[binop-reprop] {} updated {:?} -> {:?}", + function.signature.name, dst, ty + ); + } + self.value_types.insert(dst, ty); + } + } +} + +// Phase 131-11-E: OperandTypeClass for BinOp type inference +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OperandTypeClass { + String, + Integer, + Unknown, } diff --git a/src/mir/builder/ops.rs b/src/mir/builder/ops.rs index 66dc1bb0..c91252dc 100644 --- a/src/mir/builder/ops.rs +++ b/src/mir/builder/ops.rs @@ -10,7 +10,40 @@ enum BinaryOpType { Comparison(CompareOp), } +// Phase 131-11-E: TypeFacts - operand type classification +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OperandTypeClass { + String, + Integer, + Unknown, +} + impl super::MirBuilder { + // Phase 131-11-E: TypeFacts - classify operand type for BinOp inference + fn classify_operand_type(&self, vid: ValueId) -> OperandTypeClass { + let result = match self.value_types.get(&vid) { + Some(MirType::String) => OperandTypeClass::String, + Some(MirType::Box(bt)) if bt == "StringBox" => OperandTypeClass::String, + Some(MirType::Integer) => OperandTypeClass::Integer, + Some(MirType::Bool) => OperandTypeClass::Integer, // Bool can be used as integer + _ => { + // Check value_origin_newbox for StringBox + if self + .value_origin_newbox + .get(&vid) + .map(|s| s == "StringBox") + .unwrap_or(false) + { + return OperandTypeClass::String; + } + OperandTypeClass::Unknown + } + }; + if std::env::var("NYASH_TYPEFACTS_DEBUG").is_ok() { + eprintln!("[typefacts] classify {:?} -> {:?}", vid, result); + } + result + } // Build a binary operation pub(super) fn build_binary_op( &mut self, @@ -68,35 +101,33 @@ impl super::MirBuilder { vec![lhs, rhs], )?; // 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, - _ => self - .value_origin_newbox - .get(&lhs) - .map(|s| s == "StringBox") - .unwrap_or(false), - }; - let rhs_is_str = match self.value_types.get(&rhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => self - .value_origin_newbox - .get(&rhs) - .map(|s| s == "StringBox") - .unwrap_or(false), - }; - 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 if !lhs_is_str && !rhs_is_str { - // NEITHER is a string: numeric addition - self.value_types.insert(dst, MirType::Integer); + // Phase 131-11-E: TypeFacts - classify operand types + let lhs_type = self.classify_operand_type(lhs); + let rhs_type = self.classify_operand_type(rhs); + + use OperandTypeClass::*; + match (lhs_type, rhs_type) { + (String, String) => { + // 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()); + } + (Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => { + // TypeFact: Integer + anything non-String = Integer + self.value_types.insert(dst, MirType::Integer); + } + (String, Integer) | (Integer, String) => { + // Mixed types: leave as Unknown for use-site coercion + } + (Unknown, Unknown) => { + // Both Unknown: cannot infer + } + (String, Unknown) | (Unknown, String) => { + // One side is String, other is Unknown: cannot infer safely + } } - // 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 { @@ -158,37 +189,36 @@ impl super::MirBuilder { // 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) { - // 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, - _ => self - .value_origin_newbox - .get(&lhs) - .map(|s| s == "StringBox") - .unwrap_or(false), - }; - let rhs_is_str = match self.value_types.get(&rhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => self - .value_origin_newbox - .get(&rhs) - .map(|s| s == "StringBox") - .unwrap_or(false), - }; - 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 if !lhs_is_str && !rhs_is_str { - // NEITHER is a string: numeric addition - self.value_types.insert(dst, MirType::Integer); + // Phase 131-11-E: TypeFacts - classify operand types + let lhs_type = self.classify_operand_type(lhs); + let rhs_type = self.classify_operand_type(rhs); + + use OperandTypeClass::*; + match (lhs_type, rhs_type) { + (String, String) => { + // 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()); + } + (Integer, Integer) | (Integer, Unknown) | (Unknown, Integer) => { + // TypeFact: Integer + anything non-String = Integer + // This handles `counter + 1` where counter might be Unknown + self.value_types.insert(dst, MirType::Integer); + } + (String, Integer) | (Integer, String) => { + // Mixed types: leave as Unknown for use-site coercion + // LLVM backend will handle string concatenation + } + (Unknown, Unknown) => { + // Both Unknown: cannot infer, leave as Unknown + } + (String, Unknown) | (Unknown, String) => { + // One side is String, other is Unknown: cannot infer safely + // Leave as Unknown + } } - // 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 0b288a95..de1f5b0f 100644 --- a/src/runner/mir_json_emit.rs +++ b/src/runner/mir_json_emit.rs @@ -343,24 +343,23 @@ 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: 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) + // Phase 131-11-E: dst_type hint based on RESULT type (not operand types) + // Use the dst type from metadata, which has been corrected by repropagate_binop_types if matches!(op, B::Add) { - let lhs_is_str = match f.metadata.value_types.get(lhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => false, - }; - let rhs_is_str = match f.metadata.value_types.get(rhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => false, - }; - // 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"}); + let dst_type = f.metadata.value_types.get(dst); + match dst_type { + Some(MirType::Box(bt)) if bt == "StringBox" => { + obj["dst_type"] = + json!({"kind":"handle","box_type":"StringBox"}); + } + Some(MirType::Integer) => { + // Explicitly mark as i64 for integer addition + obj["dst_type"] = json!("i64"); + } + _ => { + // Unknown/other: default to i64 (conservative) + obj["dst_type"] = json!("i64"); + } } } insts.push(obj); @@ -736,23 +735,23 @@ 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) + // Phase 131-11-E: dst_type hint based on RESULT type (not operand types) + // Use the dst type from metadata, which has been corrected by repropagate_binop_types if matches!(op, B::Add) { - let lhs_is_str = match f.metadata.value_types.get(lhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => false, - }; - let rhs_is_str = match f.metadata.value_types.get(rhs) { - Some(MirType::String) => true, - Some(MirType::Box(bt)) if bt == "StringBox" => true, - _ => false, - }; - // 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"}); + let dst_type = f.metadata.value_types.get(dst); + match dst_type { + Some(MirType::Box(bt)) if bt == "StringBox" => { + obj["dst_type"] = + json!({"kind":"handle","box_type":"StringBox"}); + } + Some(MirType::Integer) => { + // Explicitly mark as i64 for integer addition + obj["dst_type"] = json!("i64"); + } + _ => { + // Unknown/other: default to i64 (conservative) + obj["dst_type"] = json!("i64"); + } } } insts.push(obj);