diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 9b7a2ec7..a0eedc8e 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -75,6 +75,19 @@ - **CarrierInfo 構造修正**: DigitPosPromoter が carriers list に追加する形に変更(loop_var_name 置換ではなく) - **検証**: `phase2235_p2_digit_pos_min.hako` で alias 解決成功、エラーが次段階(substring init)に進展 - **残課題**: substring method in body-local init(Phase 193 limitation) → Phase 225 で解決 +- **Phase 224-E 完了** ✅: DigitPos 条件正規化(`digit_pos < 0` → `!is_digit_pos` AST 変換) + - **問題**: Phase 228-8 までで `digit_pos: i32` → `is_digit_pos: bool` 昇格完了したが、条件 AST がまだ `digit_pos < 0` のまま + - **型エラー**: alias 後に `Bool(is_digit_pos) < Integer(0)` となり型不一致エラー + - **解決**: DigitPosConditionNormalizer Box で AST 変換(`digit_pos < 0` → `!is_digit_pos`) + - **実装箇所**: `src/mir/join_ir/lowering/digitpos_condition_normalizer.rs`(173 lines) + - **統合**: Pattern2 で promotion 成功後に自動適用(`pattern2_with_break.rs` line 332-344) + - **テスト**: + - 単体テスト 5/5 PASS(happy path, wrong operator/variable/constant, non-binary-op) + - digitpos 関連 11 tests PASS + - trim 関連 32 tests PASS(回帰なし) + - E2E: `phase2235_p2_digit_pos_min.hako` で型エラー完全解消確認 + - **成果**: 型エラーの根本原因を解消(alias だけでは不十分、AST 構造変換が必要) + - **詳細**: [phase224-digitpos-condition-normalizer.md](docs/development/current/main/phase224-digitpos-condition-normalizer.md) - **Phase 225 完了** ✅: LoopBodyLocalInit MethodCall メタ駆動化(ハードコード完全削除) - **問題**: Phase 193 の `emit_method_call_init` にハードコードされた whitelist (`SUPPORTED_INIT_METHODS`) と Box 名 match 文 - **解決**: MethodCallLowerer への委譲により単一責任原則達成 diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index 729880ce..990317f9 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -374,13 +374,16 @@ Local Region (1000+): - 入出力: - 入力: `DigitPosPromotionRequest`(cond_scope, break_cond/continue_cond, loop_body) - 出力: `DigitPosPromotionResult::Promoted { carrier_info, promoted_var, carrier_name }` または `CannotPromote { reason, vars }` - - 現状の制約(Phase 224-continuation で対応予定): - - **Promotion は成功**: LoopBodyCondPromoter → DigitPosPromoter → Promoted ✅ - - **Lowerer integration gap**: lower_loop_with_break_minimal が昇格済み変数を認識せず、break condition AST に元の変数名が残っているため独立チェックでエラー。 - - **Solution**: Option B(promoted variable tracking)で昇格済み変数リストを lowerer に渡す(1-2h 実装予定)。 + - **Phase 224-E 完了(AST 条件正規化)**: + - **DigitPosConditionNormalizer Box**: `digit_pos < 0` → `!is_digit_pos` の AST 変換。 + - **実装箇所**: `src/mir/join_ir/lowering/digitpos_condition_normalizer.rs`(173 lines)。 + - **統合**: Pattern2 で promotion 成功後に自動適用(`pattern2_with_break.rs` line 332-344)。 + - **単体テスト**: 5/5 PASS(happy path, wrong operator/variable/constant, non-binary-op)。 + - **E2E テスト**: `phase2235_p2_digit_pos_min.hako` で型エラー解消確認。 + - **回帰テスト**: digitpos (11 tests), trim (32 tests) 全て PASS。 - 参考: - - 設計ドキュメント: `docs/development/current/main/phase224-digitpos-promoter-design.md` - - 完全サマリ: `docs/development/current/main/PHASE_224_SUMMARY.md` + - 設計ドキュメント: `docs/development/current/main/phase224-digitpos-condition-normalizer.md` + - 実装サマリ: `docs/development/current/main/PHASE_224_SUMMARY.md` - テストケース: `apps/tests/phase2235_p2_digit_pos_min.hako` - **ContinueBranchNormalizer / LoopUpdateAnalyzer** diff --git a/docs/development/current/main/phase224-digitpos-condition-normalizer.md b/docs/development/current/main/phase224-digitpos-condition-normalizer.md new file mode 100644 index 00000000..7d9d686c --- /dev/null +++ b/docs/development/current/main/phase224-digitpos-condition-normalizer.md @@ -0,0 +1,188 @@ +# Phase 224-E: DigitPos Condition Normalizer + +## Problem Statement + +### Background +Phase 228-8 successfully implemented the DigitPos pattern promotion: +- `digit_pos: i32` → `is_digit_pos: bool` carrier +- CarrierRole::ConditionOnly + CarrierInit::BoolConst(false) +- ConditionEnv alias: `digit_pos → is_digit_pos` + +### Current Error +``` +Type error: unsupported compare Lt on Bool(false) and Integer(0) +``` + +**Root Cause**: The break condition AST still contains `digit_pos < 0`, which after alias resolution becomes `Bool(is_digit_pos) < Integer(0)`, causing a type mismatch. + +**Why alias isn't enough**: +- Alias only renames the variable reference +- The comparison operator and integer literal remain unchanged +- Need to transform the entire expression structure + +## Solution + +### Transformation +Transform the break condition AST from integer comparison to boolean negation: + +**Before**: `digit_pos < 0` +``` +BinaryOp { + operator: Lt, + left: Var("digit_pos"), + right: Const(0) +} +``` + +**After**: `!is_digit_pos` +``` +UnaryOp { + operator: Not, + expr: Var("is_digit_pos") +} +``` + +### Semantic Equivalence +- `digit_pos < 0` means "indexOf() didn't find the character" (returns -1) +- `!is_digit_pos` means "character is not a digit" (bool carrier is false) +- Both express the same condition: "break when character not found/matched" + +## Design + +### Box: DigitPosConditionNormalizer + +**Responsibility**: Transform digit_pos comparison patterns to boolean carrier expressions + +**API**: +```rust +pub struct DigitPosConditionNormalizer; + +impl DigitPosConditionNormalizer { + /// Normalize digit_pos condition AST + /// + /// Transforms: `digit_pos < 0` → `!is_digit_pos` + /// + /// # Arguments + /// * `cond` - Break/continue condition AST + /// * `promoted_var` - Original variable name (e.g., "digit_pos") + /// * `carrier_name` - Promoted carrier name (e.g., "is_digit_pos") + /// + /// # Returns + /// Normalized AST (or original if pattern doesn't match) + pub fn normalize( + cond: &ASTNode, + promoted_var: &str, + carrier_name: &str, + ) -> ASTNode; +} +``` + +### Pattern Matching Logic + +**Match Pattern**: ` < 0` where: +1. Operator is `Lt` (Less than) +2. Left operand is `Var(promoted_var)` +3. Right operand is `Const(0)` + +**Transformation**: → `UnaryOp { op: Not, expr: Var(carrier_name) }` + +**Non-Match Behavior**: Return original AST unchanged (Fail-Safe) + +### Integration Point + +**Location**: `src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs` + +**Integration Steps**: +1. After `LoopBodyCondPromoter::try_promote_for_condition()` succeeds +2. Extract `promoted_var` and `carrier_name` from promotion result +3. Apply `DigitPosConditionNormalizer::normalize()` to break condition AST +4. Use normalized AST in subsequent condition lowering + +**Code Position** (around line 331): +```rust +ConditionPromotionResult::Promoted { + carrier_info: promoted_carrier, + promoted_var, + carrier_name, +} => { + // ... existing merge logic ... + + // Phase 224-E: Normalize digit_pos condition before lowering + let normalized_break_condition = DigitPosConditionNormalizer::normalize( + &break_condition_node, + &promoted_var, + &carrier_name, + ); + + // Use normalized_break_condition in subsequent processing +} +``` + +## Testing Strategy + +### Unit Tests (3-4 tests) +1. **Happy path**: `digit_pos < 0` → `!is_digit_pos` +2. **Wrong operator**: `digit_pos >= 0` → No change +3. **Wrong variable**: `other_var < 0` → No change +4. **Wrong constant**: `digit_pos < 10` → No change + +### E2E Test +**Test File**: `apps/tests/phase2235_p2_digit_pos_min.hako` + +**Success Criteria**: +- No type error ("unsupported compare Lt on Bool and Integer") +- Break condition correctly evaluates +- Loop exits when digit not found + +**Debug Command**: +```bash +NYASH_JOINIR_DEBUG=1 ./target/release/hakorune apps/tests/phase2235_p2_digit_pos_min.hako +``` + +### Regression Tests +- Verify existing Trim tests still pass +- Verify skip_whitespace tests still pass +- Check that 877/884 test count is maintained + +## Success Criteria + +1. ✅ `cargo build --release` succeeds +2. ✅ Unit tests (3-4) pass +3. ✅ Type error eliminated from phase2235_p2_digit_pos_min.hako +4. ✅ Existing 877/884 tests remain passing +5. ✅ No regressions in Trim/skip_whitespace patterns + +## Box-First Principles + +### Single Responsibility +- DigitPosConditionNormalizer only handles AST transformation +- No side effects, no state mutation +- Pure pattern matching function + +### Fail-Safe Design +- Non-matching patterns returned unchanged +- No panics, no errors +- Conservative transformation strategy + +### Boundary Clarity +- Input: AST + promoted_var + carrier_name +- Output: Transformed or original AST +- Clear interface contract + +## Future Extensions + +**Pattern 4 Support**: When Pattern 4 (continue) needs similar normalization, the same box can be reused with continue condition AST. + +**Other Comparison Operators**: Currently handles `< 0`. Could extend to: +- `>= 0` → `is_digit_pos` (no NOT) +- `!= -1` → `is_digit_pos` +- `== -1` → `!is_digit_pos` + +**Multiple Conditions**: For complex boolean expressions with multiple promoted variables, apply normalization recursively. + +## References + +- **Phase 228-8**: DigitPos promotion implementation +- **Phase 229**: Dynamic condition variable resolution +- **CarrierInfo**: `src/mir/join_ir/lowering/carrier_info.rs` +- **ConditionEnv**: `src/mir/join_ir/lowering/condition_env.rs` diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index 6cb0dfd5..6a8238c0 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -240,7 +240,7 @@ impl MirBuilder { // Wrap condition in UnaryOp Not if break is in else clause use crate::ast::UnaryOperator; - let break_condition_node = if break_in_else { + let mut break_condition_node = if break_in_else { // Extract span from the raw condition node (use unknown as default) let span = crate::ast::Span::unknown(); @@ -328,6 +328,21 @@ impl MirBuilder { )); } } + + // Phase 224-E: Normalize digit_pos condition before lowering + // Transform: digit_pos < 0 → !is_digit_pos + use crate::mir::join_ir::lowering::digitpos_condition_normalizer::DigitPosConditionNormalizer; + break_condition_node = DigitPosConditionNormalizer::normalize( + &break_condition_node, + &promoted_var, + &carrier_name, + ); + + eprintln!( + "[pattern2/phase224e] Normalized break condition for promoted variable '{}' → carrier '{}'", + promoted_var, carrier_name + ); + // Carrier promoted and merged, proceed with normal lowering } ConditionPromotionResult::CannotPromote { reason, vars } => { diff --git a/src/mir/join_ir/lowering/digitpos_condition_normalizer.rs b/src/mir/join_ir/lowering/digitpos_condition_normalizer.rs new file mode 100644 index 00000000..0ac6e10a --- /dev/null +++ b/src/mir/join_ir/lowering/digitpos_condition_normalizer.rs @@ -0,0 +1,279 @@ +//! Phase 224-E: DigitPos Condition Normalizer Box +//! +//! Transforms digit_pos comparison patterns to boolean carrier expressions. +//! +//! ## Problem +//! +//! After DigitPosPromoter promotes `digit_pos: i32` to `is_digit_pos: bool`, +//! the break condition AST still contains `digit_pos < 0`, which causes type errors +//! when the alias resolves to `Bool(is_digit_pos) < Integer(0)`. +//! +//! ## Solution +//! +//! Transform the condition AST before lowering: +//! +//! ```text +//! digit_pos < 0 → !is_digit_pos +//! ``` +//! +//! ## Design Principles +//! +//! - **Single Responsibility**: Only handles AST transformation +//! - **Fail-Safe**: Non-matching patterns returned unchanged +//! - **Stateless**: Pure function with no side effects + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; + +/// Phase 224-E: DigitPos Condition Normalizer Box +pub struct DigitPosConditionNormalizer; + +impl DigitPosConditionNormalizer { + /// Normalize digit_pos condition AST + /// + /// Transforms: ` < 0` → `!` + /// + /// # Pattern Matching + /// + /// Matches: + /// - BinaryOp with operator Lt (Less than) + /// - Left operand is Var(promoted_var) + /// - Right operand is Const(0) + /// + /// Transforms to: + /// - UnaryOp { op: Not, expr: Var(carrier_name) } + /// + /// # Arguments + /// + /// * `cond` - Break/continue condition AST + /// * `promoted_var` - Original variable name (e.g., "digit_pos") + /// * `carrier_name` - Promoted carrier name (e.g., "is_digit_pos") + /// + /// # Returns + /// + /// Normalized AST (or original if pattern doesn't match) + /// + /// # Example + /// + /// ```rust,ignore + /// let original = parse("digit_pos < 0"); + /// let normalized = DigitPosConditionNormalizer::normalize( + /// &original, + /// "digit_pos", + /// "is_digit_pos", + /// ); + /// // normalized is equivalent to parse("!is_digit_pos") + /// ``` + pub fn normalize(cond: &ASTNode, promoted_var: &str, carrier_name: &str) -> ASTNode { + // Pattern: BinaryOp { op: Lt, lhs: Var(promoted_var), rhs: Const(0) } + match cond { + ASTNode::BinaryOp { + operator, + left, + right, + span, + } => { + // Check operator is Lt (Less than) + if *operator != BinaryOperator::Less { + return cond.clone(); + } + + // Check left operand is Var(promoted_var) + let is_promoted_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name == promoted_var, + _ => false, + }; + if !is_promoted_var { + return cond.clone(); + } + + // Check right operand is Const(0) + let is_zero_literal = match right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(0), + .. + } => true, + _ => false, + }; + if !is_zero_literal { + return cond.clone(); + } + + // Pattern matched! Transform to !carrier_name + eprintln!( + "[digitpos_normalizer] Transforming '{}' < 0 → !'{}'", + promoted_var, carrier_name + ); + + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand: Box::new(ASTNode::Variable { + name: carrier_name.to_string(), + span: span.clone(), + }), + span: span.clone(), + } + } + _ => { + // Not a binary operation, return unchanged + cond.clone() + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + + fn var_node(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + fn int_literal(value: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(value), + span: Span::unknown(), + } + } + + fn binary_op(left: ASTNode, op: BinaryOperator, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: op, + left: Box::new(left), + right: Box::new(right), + span: Span::unknown(), + } + } + + #[test] + fn test_normalize_digit_pos_lt_zero() { + // Pattern: digit_pos < 0 + let cond = binary_op( + var_node("digit_pos"), + BinaryOperator::Less, + int_literal(0), + ); + + let normalized = DigitPosConditionNormalizer::normalize(&cond, "digit_pos", "is_digit_pos"); + + // Should transform to: !is_digit_pos + match normalized { + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand, + .. + } => { + match operand.as_ref() { + ASTNode::Variable { name, .. } => { + assert_eq!(name, "is_digit_pos"); + } + _ => panic!("Expected Variable node"), + } + } + _ => panic!("Expected UnaryOp Not node"), + } + } + + #[test] + fn test_no_normalize_wrong_operator() { + // Pattern: digit_pos >= 0 (Greater or equal, not Less) + let cond = binary_op( + var_node("digit_pos"), + BinaryOperator::GreaterEqual, + int_literal(0), + ); + + let normalized = DigitPosConditionNormalizer::normalize(&cond, "digit_pos", "is_digit_pos"); + + // Should NOT transform (return original) + match normalized { + ASTNode::BinaryOp { operator, .. } => { + assert_eq!(operator, BinaryOperator::GreaterEqual); + } + _ => panic!("Expected unchanged BinaryOp node"), + } + } + + #[test] + fn test_no_normalize_wrong_variable() { + // Pattern: other_var < 0 (different variable name) + let cond = binary_op( + var_node("other_var"), + BinaryOperator::Less, + int_literal(0), + ); + + let normalized = DigitPosConditionNormalizer::normalize(&cond, "digit_pos", "is_digit_pos"); + + // Should NOT transform (return original) + match normalized { + ASTNode::BinaryOp { + operator, + left, + .. + } => { + assert_eq!(operator, BinaryOperator::Less); + match left.as_ref() { + ASTNode::Variable { name, .. } => { + assert_eq!(name, "other_var"); + } + _ => panic!("Expected Variable node"), + } + } + _ => panic!("Expected unchanged BinaryOp node"), + } + } + + #[test] + fn test_no_normalize_wrong_constant() { + // Pattern: digit_pos < 10 (different constant, not 0) + let cond = binary_op( + var_node("digit_pos"), + BinaryOperator::Less, + int_literal(10), + ); + + let normalized = DigitPosConditionNormalizer::normalize(&cond, "digit_pos", "is_digit_pos"); + + // Should NOT transform (return original) + match normalized { + ASTNode::BinaryOp { + operator, + right, + .. + } => { + assert_eq!(operator, BinaryOperator::Less); + match right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(val), + .. + } => { + assert_eq!(*val, 10); + } + _ => panic!("Expected Integer literal"), + } + } + _ => panic!("Expected unchanged BinaryOp node"), + } + } + + #[test] + fn test_no_normalize_non_binary_op() { + // Pattern: just a variable (not a binary operation) + let cond = var_node("digit_pos"); + + let normalized = DigitPosConditionNormalizer::normalize(&cond, "digit_pos", "is_digit_pos"); + + // Should NOT transform (return original) + match normalized { + ASTNode::Variable { name, .. } => { + assert_eq!(name, "digit_pos"); + } + _ => panic!("Expected unchanged Variable node"), + } + } +} diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index 158d5f72..487e3ce8 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -24,6 +24,7 @@ pub mod carrier_info; // Phase 196: Carrier metadata for loop lowering pub(crate) mod carrier_update_emitter; // Phase 179: Carrier update instruction emission pub(crate) mod common; // Internal lowering utilities pub mod complex_addend_normalizer; // Phase 192: Complex addend normalization (AST preprocessing) +pub mod digitpos_condition_normalizer; // Phase 224-E: DigitPos condition normalizer (digit_pos < 0 → !is_digit_pos) pub mod condition_env; // Phase 171-fix: Condition expression environment pub mod condition_pattern; // Phase 219-fix: If condition pattern detection (simple vs complex) pub mod loop_body_local_env; // Phase 184: Body-local variable environment