feat(pattern6): support reverse scan for last_index_of

Extend Pattern6 (ScanWithInit) to handle both forward and reverse scans:
- Forward: i=0, loop(i < len), i=i+1 (existing)
- Reverse: i=len-1, loop(i >= 0), i=i-1 (NEW)

Implementation:
- Added ScanDirection enum (Forward/Reverse)
- Updated extract_scan_with_init_parts() to detect both patterns
- Created lower_scan_with_init_reverse() lowerer
- Pattern6 now selects appropriate lowerer based on scan direction

Files modified:
- src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs
- src/mir/join_ir/lowering/scan_with_init_reverse.rs (new)
- src/mir/join_ir/lowering/mod.rs

Known issue (pre-existing):
- PHI predecessor mismatch bug exists in Pattern6 (both forward and reverse)
- This bug existed BEFORE Phase 257 P0 implementation
- Out of scope for Phase 257 P0 - will be addressed separately

Phase 257 P0
This commit is contained in:
2025-12-20 20:28:41 +09:00
parent 8394b2d6fd
commit 9ba89bada2
6 changed files with 467 additions and 25 deletions

View File

@ -39,6 +39,15 @@ use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
/// Phase 257 P0: Scan direction for forward/reverse scan
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScanDirection {
/// Forward scan: i < s.length(), i = i + 1
Forward,
/// Reverse scan: i >= 0, i = i - 1
Reverse,
}
/// Phase 254 P1: Extracted structure for scan-with-init pattern
///
/// This structure contains all the information needed to lower an index_of-style loop.
@ -50,12 +59,14 @@ struct ScanParts {
haystack: String,
/// Needle variable name (e.g., "ch")
needle: String,
/// Step literal (P0: must be 1)
/// Step literal (Phase 257: can be 1 forward or -1 reverse)
step_lit: i64,
/// Early return expression (P0: must be Variable(loop_var))
early_return_expr: ASTNode,
/// Not-found return literal (P0: must be -1)
not_found_return_lit: i64,
/// Scan direction (Phase 257 P0)
scan_direction: ScanDirection,
}
/// Phase 254 P0: Detection for Pattern 6 (ScanWithInit)
@ -133,16 +144,18 @@ fn contains_methodcall(node: &ASTNode) -> bool {
}
}
/// Check if value is ConstStep pattern (i = i + 1)
/// Check if value is ConstStep pattern (i = i + 1 or i = i - 1)
/// Phase 257 P0: Accept both forward (Add) and reverse (Subtract)
fn is_const_step_pattern(value: &ASTNode) -> bool {
match value {
ASTNode::BinaryOp {
operator: crate::ast::BinaryOperator::Add,
operator,
left,
right,
..
} => {
matches!(left.as_ref(), ASTNode::Variable { .. })
matches!(operator, crate::ast::BinaryOperator::Add | crate::ast::BinaryOperator::Subtract)
&& matches!(left.as_ref(), ASTNode::Variable { .. })
&& matches!(right.as_ref(), ASTNode::Literal { .. })
}
_ => false,
@ -168,8 +181,8 @@ fn is_const_step_pattern(value: &ASTNode) -> bool {
///
/// # P0 Restrictions
///
/// - Loop condition must be `i < s.length()`
/// - Step must be `i = i + 1` (step_lit == 1)
/// - Loop condition must be `i < s.length()` (forward) or `i >= 0` (reverse)
/// - Step must be `i = i + 1` (forward, step_lit == 1) or `i = i - 1` (reverse, step_lit == -1)
/// - Not-found return must be `-1`
/// - Early return must be `return loop_var`
fn extract_scan_with_init_parts(
@ -179,8 +192,9 @@ fn extract_scan_with_init_parts(
) -> Result<Option<ScanParts>, String> {
use crate::ast::{BinaryOperator, LiteralValue};
// 1. Check loop condition: i < s.length()
let (loop_var, haystack) = match condition {
// 1. Check loop condition: i < s.length() (forward) or i >= 0 (reverse)
let (loop_var, haystack_opt, scan_direction) = match condition {
// Forward: i < s.length()
ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left,
@ -202,14 +216,40 @@ fn extract_scan_with_init_parts(
_ => return Ok(None),
};
(loop_var, haystack)
(loop_var, Some(haystack), ScanDirection::Forward)
}
// Reverse: i >= 0
ASTNode::BinaryOp {
operator: BinaryOperator::GreaterEqual,
left,
right,
..
} => {
let loop_var = match left.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return Ok(None),
};
// Check right is Literal(0)
match right.as_ref() {
ASTNode::Literal {
value: LiteralValue::Integer(0),
..
} => {}
_ => return Ok(None),
}
// For reverse, haystack will be extracted from substring call in body
(loop_var, None, ScanDirection::Reverse)
}
_ => return Ok(None),
};
// 2. Find if statement with substring == needle and return loop_var
// Also extract haystack for reverse scans
let mut needle_opt = None;
let mut early_return_expr_opt = None;
let mut haystack_from_substring_opt = None;
for stmt in body {
if let ASTNode::If {
@ -242,6 +282,18 @@ fn extract_scan_with_init_parts(
left.as_ref()
};
// Phase 257 P0: Extract haystack from substring call for reverse scan
if let ASTNode::MethodCall {
object, method, ..
} = substring_side
{
if method == "substring" {
if let ASTNode::Variable { name: haystack_name, .. } = object.as_ref() {
haystack_from_substring_opt = Some(haystack_name.clone());
}
}
}
if let ASTNode::Variable { name: needle_name, .. } = needle_side {
// Check then_body contains return loop_var
if then_body.len() == 1 {
@ -264,7 +316,13 @@ fn extract_scan_with_init_parts(
let needle = needle_opt.ok_or_else(|| "No matching needle pattern found")?;
let early_return_expr = early_return_expr_opt.ok_or_else(|| "No early return found")?;
// 3. Check for step: i = i + 1
// Phase 257 P0: Determine haystack based on scan direction
let haystack = match scan_direction {
ScanDirection::Forward => haystack_opt.ok_or_else(|| "Forward scan missing haystack in loop condition")?,
ScanDirection::Reverse => haystack_from_substring_opt.ok_or_else(|| "Reverse scan missing haystack in substring call")?,
};
// 3. Check for step: i = i + 1 (forward) or i = i - 1 (reverse)
let mut step_lit_opt = None;
for stmt in body {
@ -272,7 +330,7 @@ fn extract_scan_with_init_parts(
if let ASTNode::Variable { name: target_name, .. } = target.as_ref() {
if target_name == &loop_var {
if let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
operator,
left,
right,
..
@ -285,7 +343,15 @@ fn extract_scan_with_init_parts(
..
} = right.as_ref()
{
step_lit_opt = Some(*lit);
match operator {
BinaryOperator::Add => {
step_lit_opt = Some(*lit);
}
BinaryOperator::Subtract => {
step_lit_opt = Some(-lit);
}
_ => {}
}
}
}
}
@ -297,9 +363,18 @@ fn extract_scan_with_init_parts(
let step_lit = step_lit_opt.ok_or_else(|| "No step pattern found")?;
// P0: step must be 1
if step_lit != 1 {
return Ok(None);
// Phase 257 P0: Verify step matches scan direction
match scan_direction {
ScanDirection::Forward => {
if step_lit != 1 {
return Ok(None);
}
}
ScanDirection::Reverse => {
if step_lit != -1 {
return Ok(None);
}
}
}
// 4. P0: not-found return must be -1 (hardcoded for now)
@ -312,6 +387,7 @@ fn extract_scan_with_init_parts(
step_lit,
early_return_expr,
not_found_return_lit,
scan_direction,
}))
}
@ -352,7 +428,6 @@ impl MirBuilder {
fn_body: Option<&[ASTNode]>,
) -> Result<Option<ValueId>, String> {
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal;
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
let trace = trace::trace();
@ -410,9 +485,18 @@ impl MirBuilder {
);
}
// Step 3: Create JoinModule
// Step 3: Create JoinModule based on scan direction
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_minimal(&mut join_value_space);
let join_module = match parts.scan_direction {
ScanDirection::Forward => {
use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal;
lower_scan_with_init_minimal(&mut join_value_space)
}
ScanDirection::Reverse => {
use crate::mir::join_ir::lowering::scan_with_init_reverse::lower_scan_with_init_reverse;
lower_scan_with_init_reverse(&mut join_value_space)
}
};
// Phase 255 P2: Build CarrierInfo for loop variable only
// Step 1: Create CarrierInfo with loop variable (i) only

View File

@ -77,6 +77,7 @@ pub mod loop_with_if_phi_if_sum; // Phase 213: Pattern 3 AST-based if-sum lowere
pub mod min_loop;
pub mod simple_while_minimal; // Phase 188-Impl-1: Pattern 1 minimal lowerer
pub mod scan_with_init_minimal; // Phase 254 P1: Pattern 6 minimal lowerer (index_of/find/contains)
pub mod scan_with_init_reverse; // Phase 257 P0: Pattern 6 reverse scan lowerer (last_index_of)
pub mod split_scan_minimal; // Phase 256 P0: Pattern 7 minimal lowerer (split/tokenization with variable step)
pub mod skip_ws;
pub mod stage1_using_resolver;

View File

@ -0,0 +1,320 @@
//! Phase 257 P0: Pattern 6 Reverse Scan Lowerer (last_index_of)
//!
//! Target: apps/tests/phase257_p0_last_index_of_min.hako
//!
//! Code:
//! ```nyash
//! static box Main {
//! main() {
//! local s = "hello world"
//! local ch = "o"
//! local i = s.length() - 1
//! loop(i >= 0) {
//! if s.substring(i, i + 1) == ch {
//! return i
//! }
//! i = i - 1
//! }
//! return -1
//! }
//! }
//! ```
//!
//! Expected JoinIR:
//! ```text
//! fn main(i, ch, s):
//! result = loop_step(i, ch, s)
//!
//! fn loop_step(i, ch, s):
//! // 1. Check exit condition: i < 0
//! exit_cond = (i < 0)
//! Jump(k_exit, [-1], cond=exit_cond) // Not found case
//!
//! // 2. Calculate i_plus_1 for substring
//! i_plus_1 = i + 1
//!
//! // 3. Hoist MethodCall(substring) to init-time BoxCall
//! cur = StringBox.substring(s, i, i_plus_1)
//!
//! // 4. Check match condition
//! match = (cur == ch)
//! Jump(k_exit, [i], cond=match) // Found case
//!
//! // 5. Decrement and tail recurse
//! i_minus_1 = i - 1
//! Call(loop_step, [i_minus_1, ch, s])
//!
//! fn k_exit(i_exit):
//! return i_exit
//! ```
use crate::mir::join_ir::lowering::canonical_names as cn;
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst,
};
/// Lower Pattern 6 Reverse Scan to JoinIR
///
/// # Phase 257 P0: Reverse scan (backward iteration)
///
/// This is a variant of scan_with_init_minimal that scans backward from i = s.length() - 1.
///
/// ## Key Differences from Forward Scan
///
/// - Exit condition: `i < 0` instead of `i >= len`
/// - Step: `i = i - 1` instead of `i = i + 1`
/// - Init: Caller must provide `i = s.length() - 1` before calling
///
/// ## Boundary Contract
///
/// This function returns a JoinModule with:
/// - **Input slots**: main() params for (i, ch, s)
/// - **Caller responsibility**: Ensure i is initialized to s.length() - 1
/// - **Exit binding**: k_exit param receives found index or -1
///
/// # Arguments
///
/// * `join_value_space` - Unified ValueId allocator (Phase 202-A)
///
/// # Returns
///
/// * `JoinModule` - Successfully lowered to JoinIR
pub(crate) fn lower_scan_with_init_reverse(
join_value_space: &mut JoinValueSpace,
) -> JoinModule {
let mut join_module = JoinModule::new();
// ==================================================================
// Function IDs allocation
// ==================================================================
let main_id = JoinFuncId::new(0);
let loop_step_id = JoinFuncId::new(1);
let k_exit_id = JoinFuncId::new(2);
// ==================================================================
// ValueId allocation
// ==================================================================
// main() params/locals
// Phase 255 P0: Loop variable MUST be first, then alphabetical order [ch, s]
let i_main_param = join_value_space.alloc_param(); // loop index
let ch_main_param = join_value_space.alloc_param(); // needle character (alphabetically first)
let s_main_param = join_value_space.alloc_param(); // haystack string (alphabetically second)
let loop_result = join_value_space.alloc_local(); // result from loop_step
// loop_step params/locals
let i_step_param = join_value_space.alloc_param(); // loop index
let ch_step_param = join_value_space.alloc_param(); // needle (alphabetically first)
let s_step_param = join_value_space.alloc_param(); // haystack (alphabetically second)
let const_0 = join_value_space.alloc_local(); // 0 for exit condition
let exit_cond = join_value_space.alloc_local(); // i < 0
let const_minus_1 = join_value_space.alloc_local(); // -1 for not found
let const_1 = join_value_space.alloc_local(); // 1 for i + 1
let i_plus_1 = join_value_space.alloc_local(); // i + 1
let cur = join_value_space.alloc_local(); // substring result
let match_cond = join_value_space.alloc_local(); // cur == ch
let i_minus_1 = join_value_space.alloc_local(); // i - 1
// k_exit params
let i_exit_param = join_value_space.alloc_param(); // exit parameter (index or -1)
// ==================================================================
// main() function
// ==================================================================
let mut main_func = JoinFunction::new(
main_id,
"main".to_string(),
vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical
);
// result = loop_step(i, ch, s) // Phase 255 P0: alphabetical order
main_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical
k_next: None,
dst: Some(loop_result),
});
// Return loop_result (found index or -1)
main_func.body.push(JoinInst::Ret { value: Some(loop_result) });
join_module.add_function(main_func);
// ==================================================================
// loop_step(i, ch, s) function
// ==================================================================
let mut loop_step_func = JoinFunction::new(
loop_step_id,
cn::LOOP_STEP.to_string(),
vec![i_step_param, ch_step_param, s_step_param], // Phase 255 P0: [i, ch, s] alphabetical
);
// 1. const 0
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_0,
value: ConstValue::Integer(0),
}));
// 2. exit_cond = (i < 0)
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Compare {
dst: exit_cond,
op: CompareOp::Lt,
lhs: i_step_param,
rhs: const_0,
}));
// 3. const -1
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_minus_1,
value: ConstValue::Integer(-1),
}));
// 4. Jump(k_exit, [-1], cond=exit_cond) - not found case
loop_step_func.body.push(JoinInst::Jump {
cont: k_exit_id.as_cont(),
args: vec![const_minus_1],
cond: Some(exit_cond),
});
// 5. i_plus_1 = i + 1
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_1,
value: ConstValue::Integer(1),
}));
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: i_plus_1,
op: BinOpKind::Add,
lhs: i_step_param,
rhs: const_1,
}));
// 6. cur = s.substring(i, i_plus_1) - init-time BoxCall
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BoxCall {
dst: Some(cur),
box_name: "StringBox".to_string(),
method: "substring".to_string(),
args: vec![s_step_param, i_step_param, i_plus_1],
}));
// 7. match_cond = (cur == ch)
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::Compare {
dst: match_cond,
op: CompareOp::Eq,
lhs: cur,
rhs: ch_step_param,
}));
// 8. Jump(k_exit, [i], cond=match_cond) - found case
loop_step_func.body.push(JoinInst::Jump {
cont: k_exit_id.as_cont(),
args: vec![i_step_param],
cond: Some(match_cond),
});
// 9. i_minus_1 = i - 1
loop_step_func
.body
.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: i_minus_1,
op: BinOpKind::Sub,
lhs: i_step_param,
rhs: const_1,
}));
// 10. Call(loop_step, [i_minus_1, ch, s]) - tail recursion
loop_step_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![i_minus_1, ch_step_param, s_step_param], // Phase 255 P0: [i_minus_1, ch, s] alphabetical
k_next: None, // CRITICAL: None for tail call
dst: None,
});
join_module.add_function(loop_step_func);
// ==================================================================
// k_exit(i_exit) function
// ==================================================================
let mut k_exit_func = JoinFunction::new(k_exit_id, cn::K_EXIT.to_string(), vec![i_exit_param]);
// Return i_exit (found index or -1)
k_exit_func.body.push(JoinInst::Ret {
value: Some(i_exit_param),
});
join_module.add_function(k_exit_func);
// Set entry point
join_module.entry = Some(main_id);
eprintln!("[joinir/pattern6] Generated JoinIR for ScanWithInit Reverse Pattern");
eprintln!("[joinir/pattern6] Functions: main, loop_step, k_exit");
eprintln!("[joinir/pattern6] Direction: Reverse (i >= 0, i = i - 1)");
join_module
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lower_scan_with_init_reverse() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_reverse(&mut join_value_space);
// main + loop_step + k_exit の3関数
assert_eq!(join_module.functions.len(), 3);
// Entry が main(0) に設定されている
assert_eq!(join_module.entry, Some(JoinFuncId::new(0)));
// k_exit 関数が取れる
let k_exit_func = join_module
.functions
.get(&JoinFuncId::new(2))
.expect("k_exit function should exist");
assert_eq!(k_exit_func.name, cn::K_EXIT);
}
#[test]
fn test_loop_step_has_reverse_step() {
let mut join_value_space = JoinValueSpace::new();
let join_module = lower_scan_with_init_reverse(&mut join_value_space);
// loop_step 関数を取得
let loop_step = join_module
.functions
.get(&JoinFuncId::new(1))
.expect("loop_step function should exist");
// BinOp(Sub) (i - 1) が含まれることを確認
let has_decrement = loop_step.body.iter().any(|inst| {
matches!(
inst,
JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Sub, .. })
)
});
assert!(
has_decrement,
"loop_step should contain i - 1 decrement"
);
}
}