feat(mir/llvm): Phase 273 P0-P1 DomainPlan→CorePlan + LLVM arg fix

Phase 273 P0-P1: Two-layer plan architecture
- DomainPlan: Pattern-specific knowledge (ScanWithInit)
- CorePlan: Fixed vocabulary (Seq, Loop, If, Effect, Exit)
- ValueId references only (String expressions forbidden)
- Pipeline: Extractor→Normalizer→Verifier→Lowerer

New plan/ module:
- mod.rs: Type definitions, SSOT spec
- normalizer.rs: DomainPlan→CorePlan + ID allocation
- verifier.rs: V1-V6 invariant checks (fail-fast)
- lowerer.rs: CorePlan→MIR (pattern-agnostic)

LLVM fix (ChatGPT):
- function_lower.py: Fix argument reference bug
- Phase 258 index_of_string now PASS on LLVM backend

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 22:42:56 +09:00
parent f07c2e7874
commit 960241795d
20 changed files with 1728 additions and 388 deletions

View File

@ -39,7 +39,13 @@ use crate::ast::{ASTNode, BinaryOperator, LiteralValue};
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
/// Phase 257 P0: Scan direction for forward/reverse scan
// Phase 273 P1: Import DomainPlan types (Plan renamed to DomainPlan)
use crate::mir::builder::control_flow::plan::{DomainPlan, ScanDirection as PlanScanDirection, ScanWithInitPlan};
// Phase 273 P0.1: Local ScanDirection and ScanParts removed (migrated to plan/mod.rs)
// Only keeping internal helper enum for legacy extract_scan_with_init_parts()
/// Phase 273 P0.1: Internal scan direction for legacy helper (temporary)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScanDirection {
/// Forward scan: i < s.length(), i = i + 1
@ -48,76 +54,57 @@ enum ScanDirection {
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.
/// Phase 273 P0.1: Internal structure for legacy helper (temporary)
#[derive(Debug, Clone)]
struct ScanParts {
/// Loop variable name (e.g., "i")
loop_var: String,
/// Haystack variable name (e.g., "s")
haystack: String,
/// Needle variable name (e.g., "ch")
needle: String,
/// 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 258 P0: True if dynamic needle (substr.length()), false if fixed (ch)
dynamic_needle: bool,
}
/// Phase 254 P0: Detection for Pattern 6 (ScanWithInit)
/// Phase 273 P1: Pure extractor that returns DomainPlan (SSOT)
///
/// Detects index_of/find/contains pattern:
/// - Loop with `i < x.length()` or `i < len` condition
/// - If with MethodCall in condition and early return
/// - ConstStep `i = i + 1`
/// - Post-loop return
/// This is the new entry point for Pattern6 extraction.
/// Router calls this directly, then passes to Normalizer + Verifier + Lowerer.
///
/// Detection is structure-based only (no function name checks).
pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
// Phase 257 P1.1: SSOT between detect and extract
// Call extract to check if pattern is actually extractable
// This prevents false positives where detection is too broad
match extract_scan_with_init_parts(ctx.condition, ctx.body, None) {
Ok(Some(_)) => {
// Pattern is extractable
if ctx.debug {
trace::trace().debug(
"pattern6/can_lower",
"accept: pattern extractable (SSOT verified)",
);
}
true
}
Ok(None) => {
// Not this pattern (fall through to other patterns)
if ctx.debug {
trace::trace().debug(
"pattern6/can_lower",
"reject: pattern not extractable (Ok(None))",
);
}
false
}
Err(e) => {
// Extraction error (log in debug mode, fall through)
if ctx.debug {
trace::trace().debug(
"pattern6/can_lower",
&format!("reject: extraction error: {}", e),
);
}
false
}
}
/// # Returns
///
/// * `Ok(Some(DomainPlan::ScanWithInit(...)))` - Successfully extracted the pattern
/// * `Ok(None)` - Not a scan-with-init pattern (try next pattern)
/// * `Err(String)` - Internal consistency error
pub(crate) fn extract_scan_with_init_plan(
condition: &ASTNode,
body: &[ASTNode],
fn_body: Option<&[ASTNode]>,
) -> Result<Option<DomainPlan>, String> {
// Call internal extraction helper
let parts = extract_scan_with_init_parts(condition, body, fn_body)?;
// Wrap in DomainPlan if extracted successfully
Ok(parts.map(|p| {
DomainPlan::ScanWithInit(ScanWithInitPlan {
loop_var: p.loop_var,
haystack: p.haystack,
needle: p.needle,
step_lit: p.step_lit,
early_return_expr: p.early_return_expr,
not_found_return_lit: p.not_found_return_lit,
scan_direction: match p.scan_direction {
ScanDirection::Forward => PlanScanDirection::Forward,
ScanDirection::Reverse => PlanScanDirection::Reverse,
},
dynamic_needle: p.dynamic_needle,
})
}))
}
// Phase 273 P0.1: can_lower() removed (router now calls extract_scan_with_init_plan() directly)
/// Check if AST node contains MethodCall
fn contains_methodcall(node: &ASTNode) -> bool {
match node {
@ -511,292 +498,10 @@ fn extract_scan_with_init_parts(
}))
}
/// Phase 254 P0: Lowering function for Pattern 6
///
/// Delegates to MirBuilder implementation.
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
builder.cf_loop_pattern6_scan_with_init_impl(
ctx.condition,
ctx.body,
ctx.func_name,
ctx.debug,
ctx.fn_body,
)
}
// Phase 273 P0.1: lower() removed (router now uses PlanLowerer::lower_scan_with_init())
impl MirBuilder {
/// Phase 272 P0.1: Pattern 6 (ScanWithInit) Frag-based implementation
///
/// Lowers index_of-style loops using EdgeCFG Frag construction (replacing JoinIRConversionPipeline).
///
/// # P0 Scope
/// - Forward scan only (step = 1)
/// - Reverse scan / dynamic needle → fallback to Ok(None)
///
/// # Arguments
///
/// * `condition` - Loop condition AST
/// * `body` - Loop body statements
/// * `func_name` - Function name for debugging
/// * `debug` - Enable debug output
/// * `fn_body` - Optional function body for capture analysis
pub(crate) fn cf_loop_pattern6_scan_with_init_impl(
&mut self,
condition: &ASTNode,
body: &[ASTNode],
func_name: &str,
debug: bool,
fn_body: Option<&[ASTNode]>,
) -> Result<Option<ValueId>, String> {
use crate::mir::{BinaryOp, CompareOp, ConstValue, EffectMask, Effect, MirInstruction, MirType};
use crate::mir::ssot::cf_common::insert_phi_at_head_spanned;
let trace = trace::trace();
if debug {
trace.debug(
"pattern6/lower",
&format!("Phase 272 P0.1: ScanWithInit Frag lowering for {}", func_name),
);
}
// Step 1: Extract pattern parts
let parts = extract_scan_with_init_parts(condition, body, fn_body)?
.ok_or_else(|| format!("[pattern6] Not a scan-with-init pattern in {}", func_name))?;
// P0 Scope: Forward scan (step=1) only
if parts.step_lit != 1 {
// Reverse scan / dynamic needle: Pattern6 not applicable
// Return Ok(None) to let other patterns/generic lowering handle this
if debug {
trace.debug(
"pattern6/lower",
&format!("P0 fallback: step_lit={} (not forward scan)", parts.step_lit),
);
}
return Ok(None);
}
if parts.dynamic_needle {
// Dynamic needle not supported in P0
if debug {
trace.debug("pattern6/lower", "P0 fallback: dynamic_needle=true");
}
return Ok(None);
}
if debug {
trace.debug(
"pattern6/lower",
&format!(
"Extracted: loop_var={}, haystack={}, needle={}, step={}, not_found={}",
parts.loop_var, parts.haystack, parts.needle, parts.step_lit, parts.not_found_return_lit
),
);
}
// Step 2: Get host ValueIds for variables
let s_host = self
.variable_ctx
.variable_map
.get(&parts.haystack)
.copied()
.ok_or_else(|| format!("[pattern6] Variable {} not found", parts.haystack))?;
let needle_host = self
.variable_ctx
.variable_map
.get(&parts.needle)
.copied()
.ok_or_else(|| format!("[pattern6] Variable {} not found", parts.needle))?;
// Step 3: Get initial loop variable value from variable_map (Pattern8 方式)
let i_init_val = self
.variable_ctx
.variable_map
.get(&parts.loop_var)
.copied()
.ok_or_else(|| format!("[pattern6] Loop variable {} not found", parts.loop_var))?;
if debug {
trace.debug(
"pattern6/lower",
&format!(
"Host ValueIds: s={:?}, needle={:?}, i_init={:?}",
s_host, needle_host, i_init_val
),
);
}
// Step 4a: Capture preheader block (entry to loop) for PHI input
let preheader_bb = self.current_block
.ok_or_else(|| "[pattern6] No current block for loop entry".to_string())?;
// Step 4b: Allocate PHI destination for loop variable BEFORE generating blocks
let i_current = self.next_value_id();
self.type_ctx.value_types.insert(i_current, MirType::Integer);
// Step 4c: Allocate BasicBlockIds for 5 blocks
let header_bb = self.next_block_id();
let body_bb = self.next_block_id();
let step_bb = self.next_block_id();
let after_bb = self.next_block_id();
let ret_found_bb = self.next_block_id();
// Add Jump from current block to header_bb (to terminate the previous block)
if let Some(_current) = self.current_block {
self.emit_instruction(MirInstruction::Jump {
target: header_bb,
edge_args: None,
})?;
}
// Build header_bb: len = s.length(), cond_loop = (i < len)
self.start_new_block(header_bb)?;
// Note: PHI node for i_current will be inserted AFTER all blocks are generated
// (see Step 7 below, after step_bb generates i_next_val)
let len_val = self.next_value_id();
self.emit_instruction(MirInstruction::BoxCall {
dst: Some(len_val),
box_val: s_host,
method: "length".to_string(),
method_id: None,
args: vec![],
effects: EffectMask::PURE.add(Effect::Io),
})?;
self.type_ctx.value_types.insert(len_val, MirType::Integer);
let cond_loop = self.next_value_id();
self.emit_instruction(MirInstruction::Compare {
dst: cond_loop,
lhs: i_current, // Use PHI result, not initial value
op: CompareOp::Lt,
rhs: len_val,
})?;
self.type_ctx.value_types.insert(cond_loop, MirType::Bool);
// Build body_bb: ch = s.substring(i, i+1), cond_match = (ch == needle)
self.start_new_block(body_bb)?;
let one_val = self.next_value_id();
self.emit_instruction(MirInstruction::Const {
dst: one_val,
value: ConstValue::Integer(1),
})?;
self.type_ctx.value_types.insert(one_val, MirType::Integer);
let i_plus_one = self.next_value_id();
self.emit_instruction(MirInstruction::BinOp {
dst: i_plus_one,
lhs: i_current, // Use PHI result, not initial value
op: BinaryOp::Add,
rhs: one_val,
})?;
self.type_ctx.value_types.insert(i_plus_one, MirType::Integer);
let ch_val = self.next_value_id();
self.emit_instruction(MirInstruction::BoxCall {
dst: Some(ch_val),
box_val: s_host,
method: "substring".to_string(),
method_id: None,
args: vec![i_current, i_plus_one], // Use PHI result, not initial value
effects: EffectMask::PURE.add(Effect::Io),
})?;
self.type_ctx.value_types.insert(ch_val, MirType::String);
let cond_match = self.next_value_id();
self.emit_instruction(MirInstruction::Compare {
dst: cond_match,
lhs: ch_val,
op: CompareOp::Eq,
rhs: needle_host,
})?;
self.type_ctx.value_types.insert(cond_match, MirType::Bool);
// Build step_bb: i_next = i + 1
self.start_new_block(step_bb)?;
let i_next_val = self.next_value_id();
self.emit_instruction(MirInstruction::BinOp {
dst: i_next_val,
lhs: i_current, // Use PHI result, not initial value
op: BinaryOp::Add,
rhs: one_val, // Reuse one_val from body_bb
})?;
self.type_ctx.value_types.insert(i_next_val, MirType::Integer);
// Note: Do NOT update variable_map here - PHI will handle SSA renaming
// Ensure ret_found_bb and after_bb exist (they don't have instructions, but must exist for emit_frag)
self.ensure_block_exists(ret_found_bb)?;
self.ensure_block_exists(after_bb)?;
// Step 7: Insert PHI at head of header_bb - Phase 272 P0.2 Refactoring: use emission/phi.rs
use crate::mir::builder::emission::phi::insert_loop_phi;
insert_loop_phi(
self,
header_bb,
i_current,
vec![
(preheader_bb, i_init_val), // Entry edge: initial value
(step_bb, i_next_val), // Latch edge: updated value
],
"pattern6/header_phi",
)?;
if debug {
trace.debug("pattern6/lower", "PHI inserted at header_bb");
}
// Step 8: Call emission entrypoint
use crate::mir::builder::emission::loop_scan_with_init::emit_scan_with_init_edgecfg;
emit_scan_with_init_edgecfg(
self,
header_bb,
body_bb,
step_bb,
after_bb,
ret_found_bb,
cond_loop,
cond_match,
i_current, // Return value for found case
)?;
if debug {
trace.debug("pattern6/lower", "Frag emitted successfully");
}
// Step 9: Update variable_map to use final loop variable value
// (This is the value when loop exits normally via i >= len)
self.variable_ctx.variable_map.insert(parts.loop_var.clone(), i_current);
// Step 10: Setup after_bb for subsequent AST lowering (return -1)
// CRITICAL: Use start_new_block() to create actual block, not just set current_block
self.start_new_block(after_bb)?;
// Step 11: Return Void (pattern applied successfully)
use crate::mir::builder::emission::constant::emit_void;
let void_val = emit_void(self);
if debug {
trace.debug(
"pattern6/lower",
&format!("Pattern 6 Frag complete, returning Void {:?}", void_val),
);
}
Ok(Some(void_val))
}
}
// Phase 255 P2: var() function removed - now using super::common::var
// Phase 273 P0.1: cf_loop_pattern6_scan_with_init_impl() removed (migrated to plan/lowerer.rs)
// The implementation is now in PlanLowerer::lower_scan_with_init()
#[cfg(test)]
mod tests {

View File

@ -25,6 +25,11 @@ use crate::mir::ValueId;
use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
// Phase 273 P1: Import Plan components (DomainPlan → Normalizer → Verifier → Lowerer)
use crate::mir::builder::control_flow::plan::lowerer::PlanLowerer;
use crate::mir::builder::control_flow::plan::normalizer::PlanNormalizer;
use crate::mir::builder::control_flow::plan::verifier::PlanVerifier;
/// Phase 193: Import AST Feature Extractor Box
/// (declared in mod.rs as pub module, import from parent)
use super::ast_feature_extractor as ast_features;
@ -229,11 +234,8 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
detect: super::pattern4_with_continue::can_lower,
lower: super::pattern4_with_continue::lower,
},
LoopPatternEntry {
name: "Pattern6_ScanWithInit", // Phase 254 P0: index_of/find/contains pattern (before P3)
detect: super::pattern6_scan_with_init::can_lower,
lower: super::pattern6_scan_with_init::lower,
},
// Phase 273 P0.1: Pattern6 entry removed (migrated to Plan-based routing)
// Pattern6_ScanWithInit now handled via extract_scan_with_init_plan() + PlanLowerer
LoopPatternEntry {
name: "Pattern7_SplitScan", // Phase 256 P0: split/tokenization with variable step (before P3)
detect: super::pattern7_split_scan::can_lower,
@ -278,17 +280,57 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
/// - Pattern detection: `ctx.pattern_kind` (from `loop_pattern_detection::classify`)
/// - No redundant pattern detection in detect functions
/// - All patterns use structure-based classification
///
/// # Phase 273 P1: DomainPlan → Normalizer → Verifier → Lowerer
///
/// Pattern6 uses the new two-layer Plan architecture:
/// - extract_scan_with_init_plan() → DomainPlan (pure extraction)
/// - PlanNormalizer::normalize() → CorePlan (pattern knowledge expansion)
/// - PlanVerifier::verify() → fail-fast validation
/// - PlanLowerer::lower() → MIR emission (pattern-agnostic)
pub(crate) fn route_loop_pattern(
builder: &mut MirBuilder,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
use super::super::trace;
// Phase 273 P1: Try Plan-based Pattern6 first (before table iteration)
// Flow: Extract → Normalize → Verify → Lower
match super::pattern6_scan_with_init::extract_scan_with_init_plan(
ctx.condition,
ctx.body,
ctx.fn_body,
)? {
Some(domain_plan) => {
// DomainPlan extracted successfully
trace::trace().pattern("route", "Pattern6_ScanWithInit (DomainPlan)", true);
// Step 1: Normalize DomainPlan → CorePlan
let core_plan = PlanNormalizer::normalize(builder, domain_plan, ctx)?;
// Step 2: Verify CorePlan invariants (fail-fast)
PlanVerifier::verify(&core_plan)?;
// Step 3: Lower CorePlan → MIR
return PlanLowerer::lower(builder, core_plan, ctx);
}
None => {
// Not Pattern6 - continue to other patterns
if ctx.debug {
trace::trace().debug(
"route",
"Pattern6 Plan extraction returned None, trying other patterns",
);
}
}
}
// Phase 183: Route based on pre-classified pattern kind
// Pattern kind was already determined by ctx.pattern_kind in LoopPatternContext::new()
// This eliminates duplicate detection logic across routers.
// Find matching pattern entry based on pattern_kind
// Phase 273 P0.1: Pattern6 skip logic removed (entry no longer in LOOP_PATTERNS)
for entry in LOOP_PATTERNS {
if (entry.detect)(builder, ctx) {
// Phase 195: Use unified trace for pattern matching