feat(joinir): Phase 170-A Step 1 - CaseALoweringShape enum and detection

Introduce structure-based Case-A loop routing to replace hardcoded
function name matching. Phase 170-A is about enabling generic routing
for ANY loop matching Case-A structural properties.

New Module: case_a_lowering_shape.rs
- CaseALoweringShape enum: StringExamination, ArrayAccumulation,
  IterationWithAccumulation, Generic, NotCaseA
- CaseALoweringShape::detect(scope) - heuristic-based shape detection
- Supports helper methods: is_recognized(), name()

Motivation:
- Current loop_to_join.rs:378-402 hardcodes 4 function names
- New functions with same structure can't be reused
- Structure-based approach enables generic lowering

Phase 170-A Roadmap:
- Step 1: CaseALoweringShape enum  (this commit)
- Step 2: loop_to_join.rs shape-based routing (next)
- Step 3: Deprecate is_case_a_minimal_target() (future)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-07 13:14:22 +09:00
parent 7a9ebf7fc1
commit 4b30edd6f4
2 changed files with 168 additions and 0 deletions

View File

@ -0,0 +1,165 @@
//! Case-A Loop Lowering Shape Detection
//!
//! Phase 170-A: Structure-based routing for Case-A loops
//!
//! Replaces hardcoded function name matching with AST-based pattern detection.
//! Enables reuse of Case-A lowering functions for ANY loop with matching structure,
//! not just hardcoded function names like "Main.skip/1".
/// Recognized Case-A loop body patterns
///
/// Used to dispatch to appropriate lowerer when function name is unavailable.
///
/// Case-A loops are characterized by:
/// - Single exit group (one loop exit path)
/// - Progress carrier (monotonically increasing/decreasing variable that drives termination)
/// - Pinned parameters (loop-invariant values that don't change during iteration)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaseALoweringShape {
/// String examination loop: conditional carrier update
///
/// Examples: Main.skip/1, FuncScannerBox.trim/1
///
/// Pattern:
/// - Loop body: single if statement examining character
/// - Condition: examines string character (e.g., ch == " ")
/// - Then branch: updates carrier (e.g., i = i + 1)
/// - Else branch: exits loop (break)
/// - Carriers: 1 (progress variable i or e)
/// - Return value: final progress carrier
///
/// Signature: (StringBox, ...) -> Integer
StringExamination,
/// Array accumulation loop: linear iteration with collection mutation
///
/// Examples: FuncScannerBox.append_defs/2
///
/// Pattern:
/// - Loop body: access arr[i], mutate collection (push/add)
/// - Condition: i < array.length()
/// - Increment: i = i + 1
/// - Exit: void or collection mutation
/// - Carriers: 1 (progress variable i)
/// - Pinned: 1-2 (array/collection, optional length)
///
/// Signature: (CollectionBox, ...) -> Void or CollectionBox
ArrayAccumulation,
/// Iteration with accumulation: linear iteration + value accumulation
///
/// Examples: Stage1UsingResolverBox.resolve_for_source/5
///
/// Pattern:
/// - Loop body: access arr[i], update accumulator
/// - Multiple carriers: progress (i) + result (prefix, sum, etc.)
/// - Condition: i < array.length()
/// - Exit: accumulated value
/// - Carriers: 2+ (progress i + accumulator)
/// - Pinned: 2+ (array, initial value, etc.)
///
/// Signature: (CollectionBox, InitialValueBox, ...) -> ResultBox
IterationWithAccumulation,
/// Generic Case-A: falls outside recognized patterns
///
/// Structure is Case-A (single exit + progress carrier) but body
/// doesn't match any known pattern.
///
/// Lowering: Should either:
/// 1. Implement generic extraction from LoopForm (Phase 170-B+), or
/// 2. Return None to trigger fallback to legacy LoopBuilder
Generic,
/// Unknown: not a Case-A loop at all
///
/// Structural requirements not met:
/// - Multiple exit groups, OR
/// - No progress carrier
///
/// Should use Pattern 1-4 routing instead.
NotCaseA,
}
impl CaseALoweringShape {
/// Detect lowering shape from LoopScopeShape
///
/// Phase 170-A: Heuristic-based shape detection
///
/// Returns NotCaseA if structural requirements not met.
/// Returns one of the above if structural analysis suggests a pattern.
///
/// # Heuristics
/// - Single carrier (1) → likely StringExamination or ArrayAccumulation
/// - Multiple carriers (2+) → likely IterationWithAccumulation
/// - Pinned count → disambiguation hint
///
/// Note: More sophisticated pattern matching (analyzing loop body structure)
/// is deferred to Phase 170-B.
pub fn detect(scope: &super::shape::LoopScopeShape) -> Self {
use super::shape::LoopScopeShape;
// Check if structurally Case-A
if scope.progress_carrier.is_none() {
return CaseALoweringShape::NotCaseA;
}
// Phase 170-A: Simple heuristic based on carrier count
match scope.carriers.len() {
0 => {
// This shouldn't happen if progress_carrier is Some, but be safe
CaseALoweringShape::NotCaseA
}
1 => {
// Single carrier: could be StringExamination or ArrayAccumulation
// Further distinction requires analyzing loop body (Phase 170-B)
// For now, return Generic to allow both paths to be tried
CaseALoweringShape::Generic
}
2.. => {
// Multiple carriers: likely Iteration with accumulation
// (progress carrier + accumulator)
CaseALoweringShape::IterationWithAccumulation
}
}
}
/// Is this a recognized lowering shape?
pub fn is_recognized(&self) -> bool {
!matches!(self, CaseALoweringShape::NotCaseA | CaseALoweringShape::Generic)
}
/// Get human-readable name for tracing/debugging
pub fn name(&self) -> &'static str {
match self {
CaseALoweringShape::StringExamination => "StringExamination",
CaseALoweringShape::ArrayAccumulation => "ArrayAccumulation",
CaseALoweringShape::IterationWithAccumulation => "IterationWithAccumulation",
CaseALoweringShape::Generic => "Generic",
CaseALoweringShape::NotCaseA => "NotCaseA",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shape_display() {
assert_eq!(CaseALoweringShape::StringExamination.name(), "StringExamination");
assert_eq!(CaseALoweringShape::ArrayAccumulation.name(), "ArrayAccumulation");
assert_eq!(CaseALoweringShape::IterationWithAccumulation.name(), "IterationWithAccumulation");
assert_eq!(CaseALoweringShape::Generic.name(), "Generic");
assert_eq!(CaseALoweringShape::NotCaseA.name(), "NotCaseA");
}
#[test]
fn test_is_recognized() {
assert!(CaseALoweringShape::StringExamination.is_recognized());
assert!(CaseALoweringShape::ArrayAccumulation.is_recognized());
assert!(CaseALoweringShape::IterationWithAccumulation.is_recognized());
assert!(!CaseALoweringShape::Generic.is_recognized());
assert!(!CaseALoweringShape::NotCaseA.is_recognized());
}
}

View File

@ -4,16 +4,19 @@
//! - `shape`: 変数分類のSSOTと質問系API
//! - `builder`: LoopForm / 既存箱からの組み立てCase-A ルーティング含む)
//! - `case_a`: Case-A minimal ターゲット判定Phase 30 F-3.1
//! - `case_a_lowering_shape`: Phase 170-A Case-A lowering shape detection (構造ベース)
//! - `context`: generic_case_a 用の共通コンテキスト
//! - `structural`: ループの構造的性質解析Phase 48-5.5
mod builder;
mod case_a;
mod case_a_lowering_shape;
mod context;
mod shape;
mod structural;
pub(crate) use case_a::is_case_a_minimal_target;
pub(crate) use case_a_lowering_shape::CaseALoweringShape;
pub(crate) use context::CaseAContext;
pub(crate) use shape::LoopScopeShape;