feat(joinir): Phase 171 complete - Trim pattern LoopBodyLocal promotion

Phase 171-C-4/5 + impl-Trim: Full Trim pattern validation infrastructure

## CarrierInfo 統合 (C-4)
- CarrierInfo::merge_from(): Deduplicated carrier merging
- TrimPatternInfo::to_carrier_info(): Conversion helper
- Pattern2/4: Promoted carrier merge integration
- 7 unit tests for merge logic

## TrimLoopHelper 設計 (C-5)
- TrimLoopHelper struct: Trim-specific validation box
- carrier_type(), initial_value(), whitespace helpers
- CarrierInfo::trim_helper() accessor
- 5 unit tests

## Validation-Only Integration (impl-Trim)
- TrimLoopHelper::is_safe_trim(), is_trim_like(), has_valid_structure()
- Pattern2/4: Trim exception route with safety validation
- body_locals extraction from loop body AST
- LoopBodyCarrierPromoter: ASTNode::Local handler extension
- 4 unit tests for safety validation

## Architecture
- Box Theory: TrimLoopHelper is "validation only" (no JoinIR generation)
- Fail-Fast: Non-Trim LoopBodyLocal immediately rejected
- Whitelist approach: Only Trim pattern bypasses LoopBodyLocal restriction

Tests: 16 new unit tests, all passing
E2E: test_trim_main_pattern.hako validation successful

Next: Phase 172 - Actual JoinIR lowering for Trim pattern

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-08 02:41:53 +09:00
parent a1f3d913f9
commit 14c84fc583
9 changed files with 939 additions and 12 deletions

View File

@ -47,6 +47,45 @@ pub struct TrimPatternInfo {
pub carrier_name: String,
}
impl TrimPatternInfo {
/// Phase 171-C-4: Convert to CarrierInfo with a bool carrier for the pattern
///
/// Creates a CarrierInfo containing a single bool carrier representing
/// the Trim pattern match condition (e.g., "is_whitespace").
///
/// # Returns
///
/// CarrierInfo with:
/// - loop_var_name: The promoted carrier name (e.g., "is_ch_match")
/// - loop_var_id: Placeholder ValueId(0) (will be remapped by JoinInlineBoundary)
/// - carriers: Empty (the carrier itself is the loop variable)
///
/// # Design Note
///
/// The returned CarrierInfo uses a placeholder ValueId(0) because:
/// - This is JoinIR-local ID space (not host ValueId space)
/// - The actual host ValueId will be assigned during merge_joinir_mir_blocks
/// - JoinInlineBoundary will handle the boundary mapping
pub fn to_carrier_info(&self) -> crate::mir::join_ir::lowering::carrier_info::CarrierInfo {
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::ValueId;
use super::trim_loop_helper::TrimLoopHelper;
// Phase 171-C-4/5: Create CarrierInfo with promoted carrier as loop variable
// and attach TrimLoopHelper for future lowering
let mut carrier_info = CarrierInfo::with_carriers(
self.carrier_name.clone(), // "is_ch_match" becomes the loop variable
ValueId(0), // Placeholder (will be remapped)
vec![], // No additional carriers
);
// Phase 171-C-5: Attach TrimLoopHelper for pattern-specific lowering logic
carrier_info.trim_helper = Some(TrimLoopHelper::from_pattern_info(self));
carrier_info
}
}
/// 昇格結果
pub enum PromotionResult {
/// 昇格成功: Trim パターン情報を返す
@ -204,6 +243,18 @@ impl LoopBodyCarrierPromoter {
}
}
// Phase 171-impl-Trim: Handle Local with initial values
// local ch = s.substring(...)
ASTNode::Local { variables, initial_values, .. } if initial_values.len() == variables.len() => {
for (i, var) in variables.iter().enumerate() {
if var == var_name {
if let Some(Some(init_expr)) = initial_values.get(i) {
return Some(init_expr.as_ref());
}
}
}
}
// その他のノードは無視
_ => {}
}

View File

@ -761,3 +761,7 @@ pub mod error_messages;
// Phase 171-C: LoopBodyLocal Carrier Promotion
pub mod loop_body_carrier_promoter;
// Phase 171-C-5: Trim Pattern Helper
pub mod trim_loop_helper;
pub use trim_loop_helper::TrimLoopHelper;

View File

@ -0,0 +1,338 @@
//! Phase 171-C-5: TrimLoopHelper - Trim Pattern Lowering Helper
//!
//! This module provides a helper struct for Trim pattern lowering.
//! It encapsulates pattern-specific logic for converting LoopBodyLocal-based
//! conditions to carrier-based conditions.
//!
//! ## Purpose
//!
//! When a Trim pattern is detected (e.g., trim leading/trailing whitespace),
//! the LoopBodyCarrierPromoter promotes the LoopBodyLocal variable (like `ch`)
//! to a bool carrier (like `is_whitespace`).
//!
//! TrimLoopHelper stores the pattern information needed to:
//! 1. Generate carrier initialization code
//! 2. Generate carrier update code
//! 3. Map the promoted carrier back to the original variable semantics
//!
//! ## Example Use Case
//!
//! **Original pattern**:
//! ```nyash
//! loop(start < end) {
//! local ch = s.substring(start, start+1)
//! if ch == " " || ch == "\t" { start = start + 1 } else { break }
//! }
//! ```
//!
//! **After promotion**:
//! - Original variable: `ch`
//! - Promoted carrier: `is_whitespace` (bool)
//! - Comparison literals: `[" ", "\t"]`
//!
//! **TrimLoopHelper usage**:
//! ```rust
//! let helper = TrimLoopHelper {
//! original_var: "ch".to_string(),
//! carrier_name: "is_whitespace".to_string(),
//! whitespace_chars: vec![" ".to_string(), "\t".to_string()],
//! };
//!
//! // Generate carrier initialization: is_whitespace = true
//! let init_value = helper.initial_value(); // true
//!
//! // Generate carrier update: is_whitespace = (ch == " " || ch == "\t")
//! let carrier_type = helper.carrier_type(); // "Bool"
//! ```
use super::loop_body_carrier_promoter::TrimPatternInfo;
/// Helper for Trim pattern lowering
///
/// Encapsulates the pattern-specific logic for converting
/// LoopBodyLocal-based conditions to carrier-based conditions.
///
/// # Fields
///
/// * `original_var` - The original LoopBodyLocal variable name (e.g., "ch")
/// * `carrier_name` - The promoted carrier name (e.g., "is_whitespace")
/// * `whitespace_chars` - The whitespace characters to compare against (e.g., [" ", "\t", "\n", "\r"])
///
/// # Design Philosophy
///
/// This struct follows Box Theory principles:
/// - **Single Responsibility**: Only handles Trim pattern lowering logic
/// - **Reusability**: Can be used by both Pattern2 and Pattern4 lowerers
/// - **Testability**: Pure data structure with simple accessors
#[derive(Debug, Clone)]
pub struct TrimLoopHelper {
/// The original variable name (e.g., "ch")
pub original_var: String,
/// The promoted carrier name (e.g., "is_whitespace")
pub carrier_name: String,
/// Whitespace characters to compare against (e.g., [" ", "\t", "\n", "\r"])
pub whitespace_chars: Vec<String>,
}
impl TrimLoopHelper {
/// Create TrimLoopHelper from TrimPatternInfo
///
/// # Arguments
///
/// * `info` - The TrimPatternInfo from LoopBodyCarrierPromoter
///
/// # Returns
///
/// A new TrimLoopHelper with the same information
///
/// # Example
///
/// ```ignore
/// let trim_info = TrimPatternInfo {
/// var_name: "ch".to_string(),
/// comparison_literals: vec![" ".to_string(), "\t".to_string()],
/// carrier_name: "is_whitespace".to_string(),
/// };
///
/// let helper = TrimLoopHelper::from_pattern_info(&trim_info);
/// assert_eq!(helper.original_var, "ch");
/// assert_eq!(helper.carrier_name, "is_whitespace");
/// ```
pub fn from_pattern_info(info: &TrimPatternInfo) -> Self {
TrimLoopHelper {
original_var: info.var_name.clone(),
carrier_name: info.carrier_name.clone(),
whitespace_chars: info.comparison_literals.clone(),
}
}
/// Get the carrier type (always Bool for Trim pattern)
///
/// Trim patterns always use bool carriers to represent
/// "does the character match the whitespace set?"
///
/// # Returns
///
/// "Bool" - the carrier type name
pub fn carrier_type(&self) -> &str {
"Bool"
}
/// Get initial carrier value (true = continue looping)
///
/// The carrier is initialized to `true` to represent
/// "keep looping initially". When the character doesn't match
/// whitespace, the carrier becomes `false` and the loop breaks.
///
/// # Returns
///
/// `true` - initial value for the carrier
pub fn initial_value(&self) -> bool {
true
}
/// Get the number of whitespace characters in the comparison set
///
/// Useful for diagnostics and code generation.
///
/// # Returns
///
/// The count of whitespace characters (e.g., 4 for [" ", "\t", "\n", "\r"])
pub fn whitespace_count(&self) -> usize {
self.whitespace_chars.len()
}
/// Check if a specific character is in the whitespace set
///
/// # Arguments
///
/// * `ch` - The character to check (as a string slice)
///
/// # Returns
///
/// `true` if the character is in the whitespace set, `false` otherwise
///
/// # Example
///
/// ```ignore
/// let helper = TrimLoopHelper {
/// whitespace_chars: vec![" ".to_string(), "\t".to_string()],
/// ..Default::default()
/// };
///
/// assert!(helper.is_whitespace(" "));
/// assert!(helper.is_whitespace("\t"));
/// assert!(!helper.is_whitespace("a"));
/// ```
pub fn is_whitespace(&self, ch: &str) -> bool {
self.whitespace_chars.iter().any(|wc| wc == ch)
}
/// Check if this is a safe Trim pattern that can bypass LoopBodyLocal restrictions
///
/// A safe Trim pattern must:
/// 1. Have a valid carrier name
/// 2. Have at least one whitespace character to compare
/// 3. Have the expected structure (substring + OR chain + break)
///
/// # Returns
///
/// `true` if this is a safe Trim pattern, `false` otherwise
///
/// # Example
///
/// ```ignore
/// let helper = TrimLoopHelper {
/// original_var: "ch".to_string(),
/// carrier_name: "is_whitespace".to_string(),
/// whitespace_chars: vec![" ".to_string(), "\t".to_string()],
/// };
/// assert!(helper.is_safe_trim());
/// ```
pub fn is_safe_trim(&self) -> bool {
// Basic validation
!self.carrier_name.is_empty() && !self.whitespace_chars.is_empty()
}
/// Alias for is_safe_trim() - checks if this follows the Trim-like pattern
///
/// This method provides a semantic alias for safety checks.
///
/// # Returns
///
/// `true` if this pattern is Trim-like, `false` otherwise
pub fn is_trim_like(&self) -> bool {
self.is_safe_trim()
}
/// Check if this pattern has the expected Trim structure:
/// - substring() method call
/// - OR chain of equality comparisons
/// - break on non-match
///
/// # Returns
///
/// `true` if the pattern has valid structure, `false` otherwise
///
/// # Implementation Note
///
/// For now, just check basic requirements.
/// The full structure was already validated by LoopBodyCarrierPromoter.
pub fn has_valid_structure(&self) -> bool {
!self.original_var.is_empty()
&& !self.carrier_name.is_empty()
&& !self.whitespace_chars.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_pattern_info() {
let trim_info = TrimPatternInfo {
var_name: "ch".to_string(),
comparison_literals: vec![" ".to_string(), "\t".to_string()],
carrier_name: "is_ch_match".to_string(),
};
let helper = TrimLoopHelper::from_pattern_info(&trim_info);
assert_eq!(helper.original_var, "ch");
assert_eq!(helper.carrier_name, "is_ch_match");
assert_eq!(helper.whitespace_chars.len(), 2);
assert!(helper.whitespace_chars.contains(&" ".to_string()));
assert!(helper.whitespace_chars.contains(&"\t".to_string()));
}
#[test]
fn test_carrier_type() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![],
};
assert_eq!(helper.carrier_type(), "Bool");
}
#[test]
fn test_initial_value() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![],
};
assert_eq!(helper.initial_value(), true);
}
#[test]
fn test_whitespace_count() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string(), "\n".to_string(), "\r".to_string()],
};
assert_eq!(helper.whitespace_count(), 4);
}
#[test]
fn test_is_whitespace() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string()],
};
assert!(helper.is_whitespace(" "));
assert!(helper.is_whitespace("\t"));
assert!(!helper.is_whitespace("\n"));
assert!(!helper.is_whitespace("a"));
}
#[test]
fn test_is_safe_trim() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string()],
};
assert!(helper.is_safe_trim());
assert!(helper.is_trim_like());
}
#[test]
fn test_is_safe_trim_empty_carrier() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "".to_string(), // Empty!
whitespace_chars: vec![" ".to_string()],
};
assert!(!helper.is_safe_trim());
}
#[test]
fn test_is_safe_trim_no_whitespace() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![], // Empty!
};
assert!(!helper.is_safe_trim());
}
#[test]
fn test_has_valid_structure() {
let helper = TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string()],
};
assert!(helper.has_valid_structure());
}
}