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

@ -104,38 +104,76 @@ def lower_function(builder, func_data: Dict[str, Any]):
if func is None:
func = ir.Function(builder.module, func_ty, name=name)
# Map parameters to vmap. Prefer mapping by referenced value-ids that have no
# local definition (common in v0 JSON where params appear as lhs/rhs ids).
# Map parameters to vmap.
#
# SSOT: If `func_data["params"]` is present, it defines the ValueId ↔ arg position contract.
# Use it first to avoid heuristic mis-mapping (which can silently ignore some parameters).
#
# Fallback: If params are missing (older JSON / legacy emit), use a heuristic:
# - map "used but not defined" ValueIds to args in ascending ValueId order.
try:
arity = len(func.args)
# Collect defined and used ids
defs = set()
uses = set()
for bb in (blocks or []):
for ins in (bb.get('instructions') or []):
try:
dstv = ins.get('dst')
if isinstance(dstv, int):
defs.add(int(dstv))
except Exception:
pass
for k in ('lhs','rhs','value','cond','box_val'):
params_list = func_data.get("params", []) or []
if (
isinstance(params_list, list)
and len(params_list) == arity
and all(isinstance(v, int) for v in params_list)
):
for i in range(arity):
builder.vmap[int(params_list[i])] = func.args[i]
else:
# Collect defined and used ids
defs = set()
uses = set()
for bb in (blocks or []):
for ins in (bb.get('instructions') or []):
try:
v = ins.get(k)
if isinstance(v, int):
uses.add(int(v))
dstv = ins.get('dst')
if isinstance(dstv, int):
defs.add(int(dstv))
except Exception:
pass
cand = [vid for vid in uses if vid not in defs]
cand.sort()
mapped = 0
for i in range(min(arity, len(cand))):
builder.vmap[int(cand[i])] = func.args[i]
mapped += 1
# Fallback: also map positional 0..arity-1 to args if not already mapped
for i in range(arity):
if i not in builder.vmap:
builder.vmap[i] = func.args[i]
for k in ('lhs', 'rhs', 'value', 'cond', 'box_val', 'box', 'src'):
try:
v = ins.get(k)
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
# List operands
try:
a = ins.get('args')
if isinstance(a, list):
for v in a:
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
# Unified calls: mir_call.args
try:
mc = ins.get('mir_call')
if isinstance(mc, dict):
a = mc.get('args')
if isinstance(a, list):
for v in a:
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
cand = [vid for vid in uses if vid not in defs]
cand.sort()
for i in range(min(arity, len(cand))):
builder.vmap[int(cand[i])] = func.args[i]
# Legacy fallback: map positional 0..arity-1 only when params are missing.
for i in range(arity):
if i not in builder.vmap:
builder.vmap[i] = func.args[i]
except Exception:
pass

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

View File

@ -60,6 +60,9 @@ pub(in crate::mir::builder) mod normalization;
// Phase 264: EdgeCFG Fragment API (入口SSOT)
pub(in crate::mir::builder) mod edgecfg;
// Phase 273 P0: Plan Extractor (Pure) + PlanLowerer SSOT
pub(in crate::mir::builder) mod plan;
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT (crate-wide visibility)
pub(crate) use joinir::detect_skip_whitespace_pattern;

View File

@ -0,0 +1,250 @@
//! Phase 273 P1: PlanLowerer - CorePlan → MIR 生成 (SSOT)
//!
//! # Responsibilities
//!
//! - Receive CorePlan from PlanNormalizer
//! - Emit MIR instructions using pre-allocated ValueIds
//! - No pattern-specific knowledge (pattern-agnostic)
//!
//! # Key Design Decision
//!
//! Lowerer processes CorePlan ONLY. It does not know about scan, split, or
//! any other pattern-specific semantics. All pattern knowledge is in Normalizer.
use super::{CoreEffectPlan, CoreLoopPlan, CorePlan};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::builder::MirBuilder;
use crate::mir::{Effect, EffectMask, MirInstruction, ValueId};
/// Phase 273 P1: PlanLowerer - CorePlan → MIR 生成 (SSOT)
pub(in crate::mir::builder) struct PlanLowerer;
impl PlanLowerer {
/// CorePlan を受け取り、MIR を生成
///
/// # Arguments
///
/// * `builder` - MIR builder (mutable access for instruction emission)
/// * `plan` - CorePlan from Normalizer (pre-allocated ValueIds)
/// * `ctx` - Loop pattern context for debug/func_name
pub(in crate::mir::builder) fn lower(
builder: &mut MirBuilder,
plan: CorePlan,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
match plan {
CorePlan::Seq(plans) => Self::lower_seq(builder, plans, ctx),
CorePlan::Loop(loop_plan) => Self::lower_loop(builder, loop_plan, ctx),
CorePlan::If(_if_plan) => {
// P1: If is handled inline in Loop body
Err("[lowerer] Standalone CorePlan::If not yet supported".to_string())
}
CorePlan::Effect(effect) => {
Self::emit_effect(builder, &effect)?;
Ok(None)
}
CorePlan::Exit(_exit) => {
// P1: Exit is handled by edge CFG in Loop
Err("[lowerer] Standalone CorePlan::Exit not yet supported".to_string())
}
}
}
/// Seq: process plans in order
fn lower_seq(
builder: &mut MirBuilder,
plans: Vec<CorePlan>,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
let mut result = None;
for plan in plans {
result = Self::lower(builder, plan, ctx)?;
}
Ok(result)
}
/// Loop: emit blocks, effects, PHI, and edge CFG
///
/// This is pattern-agnostic. All pattern knowledge is in Normalizer.
fn lower_loop(
builder: &mut MirBuilder,
loop_plan: CoreLoopPlan,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
use crate::mir::builder::control_flow::joinir::trace;
let trace_logger = trace::trace();
let debug = ctx.debug;
if debug {
trace_logger.debug(
"lowerer/loop",
&format!(
"Phase 273 P1: Lowering CoreLoopPlan for {}",
ctx.func_name
),
);
}
// Step 1: Emit Jump from preheader (current block) to header
if builder.current_block.is_some() {
builder.emit_instruction(MirInstruction::Jump {
target: loop_plan.header_bb,
edge_args: None,
})?;
}
// Step 2: Start header block and emit header effects
builder.start_new_block(loop_plan.header_bb)?;
for effect in &loop_plan.header_effects {
Self::emit_effect(builder, effect)?;
}
if debug {
trace_logger.debug(
"lowerer/loop",
&format!("Header effects emitted: {} effects", loop_plan.header_effects.len()),
);
}
// Step 3: Start body block and emit body plans
builder.start_new_block(loop_plan.body_bb)?;
for plan in loop_plan.body {
match plan {
CorePlan::Effect(effect) => {
Self::emit_effect(builder, &effect)?;
}
_ => {
// P1: Only Effect plans in body for now
return Err("[lowerer] Non-Effect plan in Loop body not yet supported".to_string());
}
}
}
if debug {
trace_logger.debug("lowerer/loop", "Body plans emitted");
}
// Step 4: Start step block and emit step effects
builder.start_new_block(loop_plan.step_bb)?;
for effect in &loop_plan.step_effects {
Self::emit_effect(builder, effect)?;
}
if debug {
trace_logger.debug(
"lowerer/loop",
&format!("Step effects emitted: {} effects", loop_plan.step_effects.len()),
);
}
// Step 5: Ensure found_bb and after_bb exist
builder.ensure_block_exists(loop_plan.found_bb)?;
builder.ensure_block_exists(loop_plan.after_bb)?;
// Step 6: Insert PHI at header for each carrier
use crate::mir::builder::emission::phi::insert_loop_phi;
for carrier in &loop_plan.carriers {
insert_loop_phi(
builder,
loop_plan.header_bb,
carrier.phi_dst,
vec![
(loop_plan.preheader_bb, carrier.init_value),
(loop_plan.step_bb, carrier.step_value),
],
"lowerer/loop_phi",
)?;
}
if debug {
trace_logger.debug("lowerer/loop", "PHI inserted at header_bb");
}
// Step 7: Emit edge CFG (terminators)
use crate::mir::builder::emission::loop_scan_with_init::emit_scan_with_init_edgecfg;
emit_scan_with_init_edgecfg(
builder,
loop_plan.header_bb,
loop_plan.body_bb,
loop_plan.step_bb,
loop_plan.after_bb,
loop_plan.found_bb,
loop_plan.cond_loop,
loop_plan.cond_match,
loop_plan.carriers.first().map(|c| c.phi_dst).unwrap_or(ValueId(0)),
)?;
if debug {
trace_logger.debug("lowerer/loop", "Edge CFG emitted");
}
// Step 8: Update variable_map for carriers
for carrier in &loop_plan.carriers {
builder
.variable_ctx
.variable_map
.insert(carrier.name.clone(), carrier.phi_dst);
}
// Step 9: Setup after_bb for subsequent AST lowering
builder.start_new_block(loop_plan.after_bb)?;
// Step 10: Return Void (pattern applied successfully)
use crate::mir::builder::emission::constant::emit_void;
let void_val = emit_void(builder);
if debug {
trace_logger.debug(
"lowerer/loop",
&format!("Loop complete, returning Void {:?}", void_val),
);
}
Ok(Some(void_val))
}
/// Emit a single CoreEffectPlan as MirInstruction
fn emit_effect(builder: &mut MirBuilder, effect: &CoreEffectPlan) -> Result<(), String> {
match effect {
CoreEffectPlan::Const { dst, value } => {
builder.emit_instruction(MirInstruction::Const {
dst: *dst,
value: value.clone(),
})?;
}
CoreEffectPlan::MethodCall { dst, object, method, args } => {
builder.emit_instruction(MirInstruction::BoxCall {
dst: Some(*dst),
box_val: *object,
method: method.clone(),
method_id: None,
args: args.clone(),
effects: EffectMask::PURE.add(Effect::Io),
})?;
}
CoreEffectPlan::BinOp { dst, lhs, op, rhs } => {
builder.emit_instruction(MirInstruction::BinOp {
dst: *dst,
lhs: *lhs,
op: *op,
rhs: *rhs,
})?;
}
CoreEffectPlan::Compare { dst, lhs, op, rhs } => {
builder.emit_instruction(MirInstruction::Compare {
dst: *dst,
lhs: *lhs,
op: *op,
rhs: *rhs,
})?;
}
}
Ok(())
}
}

View File

@ -0,0 +1,230 @@
//! Phase 273 P1: DomainPlan/CorePlan 二層構造 + PlanNormalizer + PlanVerifier
//!
//! This module provides a two-layer Plan architecture for loop pattern lowering:
//!
//! # Architecture
//!
//! ```text
//! DomainPlan (Pattern固有)
//! ↓ PlanNormalizer (SSOT)
//! CorePlan (固定語彙 - 構造ノードのみ)
//! ↓ PlanLowerer
//! MIR (block/value/phi)
//! ```
//!
//! - **DomainPlan**: Pattern-specific plans (ScanWithInit etc.)
//! - **PlanNormalizer**: DomainPlan → CorePlan conversion (SSOT, scan knowledge here)
//! - **CorePlan**: Fixed vocabulary, expressions as ValueId references (no String parsing)
//! - **PlanVerifier**: Fail-fast validation for CorePlan invariants
//! - **PlanLowerer**: Processes CorePlan only (no string interpretation)
//!
//! # Key Design Decision (String式禁止)
//!
//! CorePlan expressions use **ValueId references only** (String expressions forbidden).
//! This prevents "second language processor" from growing inside Lowerer.
use crate::ast::ASTNode;
use crate::mir::{BasicBlockId, BinaryOp, CompareOp, ConstValue, ValueId};
pub(in crate::mir::builder) mod lowerer;
pub(in crate::mir::builder) mod normalizer;
pub(in crate::mir::builder) mod verifier;
// ============================================================================
// DomainPlan (Pattern固有)
// ============================================================================
/// Phase 273 P1: DomainPlan - Pattern-specific plan vocabulary
///
/// DomainPlan contains pattern-specific knowledge (e.g., scan semantics).
/// Normalizer converts DomainPlan → CorePlan with ValueId generation.
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum DomainPlan {
/// Pattern6: index_of / find scan
ScanWithInit(ScanWithInitPlan),
// P2+: Split(SplitPlan), BoolPredicate(BoolPredicatePlan), etc.
}
/// Phase 273 P0: Scan direction for forward/reverse scan
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::mir::builder) enum ScanDirection {
/// Forward scan: i < s.length(), i = i + 1
Forward,
/// Reverse scan: i >= 0, i = i - 1
Reverse,
}
/// Phase 273 P0: Extracted structure for scan-with-init pattern
///
/// This structure contains all the information needed to lower an index_of-style loop.
/// Moved from pattern6_scan_with_init.rs for centralization.
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct ScanWithInitPlan {
/// Loop variable name (e.g., "i")
pub loop_var: String,
/// Haystack variable name (e.g., "s")
pub haystack: String,
/// Needle variable name (e.g., "ch")
pub needle: String,
/// Step literal (Phase 257: can be 1 forward or -1 reverse)
pub step_lit: i64,
/// Early return expression (P0: must be Variable(loop_var))
pub early_return_expr: ASTNode,
/// Not-found return literal (P0: must be -1)
pub not_found_return_lit: i64,
/// Scan direction (Phase 257 P0)
pub scan_direction: ScanDirection,
/// Phase 258 P0: True if dynamic needle (substr.length()), false if fixed (ch)
pub dynamic_needle: bool,
}
// ============================================================================
// CorePlan (固定語彙 - 構造ノードのみ)
// ============================================================================
/// Phase 273 P1: CorePlan - Fixed vocabulary plan (structure nodes only)
///
/// CorePlan expressions use **ValueId references only** (no String parsing).
/// This prevents "second language processor" from growing inside Lowerer.
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum CorePlan {
/// Sequence: execute plans in order
Seq(Vec<CorePlan>),
/// Loop with carriers (PHI variables)
Loop(CoreLoopPlan),
/// Conditional branching
If(CoreIfPlan),
/// Side effect (already lowered to ValueId)
Effect(CoreEffectPlan),
/// Control flow exit (Return/Break/Continue)
Exit(CoreExitPlan),
}
/// Phase 273 P1: Loop plan with carriers
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct CoreLoopPlan {
// === Block IDs (pre-allocated by Normalizer) ===
/// Preheader block (entry to loop)
pub preheader_bb: BasicBlockId,
/// Header block (loop condition check)
pub header_bb: BasicBlockId,
/// Body block (loop body start)
pub body_bb: BasicBlockId,
/// Step block (increment and back-edge)
pub step_bb: BasicBlockId,
/// After block (loop exit)
pub after_bb: BasicBlockId,
/// Found block (early exit on match)
pub found_bb: BasicBlockId,
// === Effects per block (emitted by Lowerer) ===
/// Header effects (emitted in header_bb before branch)
pub header_effects: Vec<CoreEffectPlan>,
/// Body plans (emitted in body_bb, can contain If/Exit)
pub body: Vec<CorePlan>,
/// Step effects (emitted in step_bb before back-edge)
pub step_effects: Vec<CoreEffectPlan>,
// === Loop control ===
/// Loop carriers (PHI variables)
pub carriers: Vec<CoreCarrierInfo>,
/// Loop condition (for header→body/after branch)
pub cond_loop: ValueId,
/// Match condition (for body→found/step branch)
pub cond_match: ValueId,
/// Loop variable name (for variable_map update after loop)
pub loop_var_name: String,
}
/// Phase 273 P1: Loop carrier (PHI variable)
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct CoreCarrierInfo {
/// Variable name (for variable_map update)
pub name: String,
/// Initial value (from preheader)
pub init_value: ValueId,
/// Step value (from step block, back-edge)
pub step_value: ValueId,
/// PHI destination (loop variable inside loop)
pub phi_dst: ValueId,
}
/// Phase 273 P1: Conditional plan
#[derive(Debug, Clone)]
pub(in crate::mir::builder) struct CoreIfPlan {
/// Condition (ValueId reference, not String!)
pub condition: ValueId,
/// Then branch plans
pub then_plans: Vec<CorePlan>,
/// Else branch plans (optional)
pub else_plans: Option<Vec<CorePlan>>,
}
/// Phase 273 P1: Effect plan (side effects already lowered to ValueId)
///
/// Effect vocabulary is minimal (scan-specific variants forbidden):
/// - MethodCall, BinOp, Compare, Const only
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum CoreEffectPlan {
/// Method call (args are ValueIds, not Strings!)
MethodCall {
dst: ValueId,
object: ValueId,
method: String, // Method name only (OK as String)
args: Vec<ValueId>,
},
/// Binary operation
BinOp {
dst: ValueId,
lhs: ValueId,
op: BinaryOp,
rhs: ValueId,
},
/// Comparison
Compare {
dst: ValueId,
lhs: ValueId,
op: CompareOp,
rhs: ValueId,
},
/// Constant
Const { dst: ValueId, value: ConstValue },
}
/// Phase 273 P1: Exit plan (control flow exit)
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum CoreExitPlan {
/// Return with optional value
Return(Option<ValueId>),
/// Break from loop
Break,
/// Continue to next iteration
Continue,
}

View File

@ -0,0 +1,290 @@
//! Phase 273 P1: PlanNormalizer - DomainPlan → CorePlan 変換 (SSOT)
//!
//! # Responsibilities
//!
//! - Convert DomainPlan to CorePlan (SSOT for pattern-specific knowledge)
//! - Generate ValueIds for CorePlan expressions
//! - Expand pattern-specific operations into generic CoreEffectPlan
//!
//! # Key Design Decision
//!
//! Normalizer is the ONLY place that knows pattern-specific semantics.
//! Lowerer processes CorePlan without any pattern knowledge.
use super::{
CoreCarrierInfo, CoreEffectPlan, CoreLoopPlan, CorePlan, DomainPlan, ScanWithInitPlan,
};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::builder::MirBuilder;
use crate::mir::{BinaryOp, CompareOp, ConstValue, MirType};
/// Phase 273 P1: PlanNormalizer - DomainPlan → CorePlan 変換 (SSOT)
pub(in crate::mir::builder) struct PlanNormalizer;
impl PlanNormalizer {
/// Normalize DomainPlan to CorePlan
///
/// This is the SSOT for pattern-specific knowledge expansion.
/// All pattern semantics (scan, split, etc.) are expanded here.
pub(in crate::mir::builder) fn normalize(
builder: &mut MirBuilder,
domain: DomainPlan,
ctx: &LoopPatternContext,
) -> Result<CorePlan, String> {
match domain {
DomainPlan::ScanWithInit(parts) => Self::normalize_scan_with_init(builder, parts, ctx),
}
}
/// ScanWithInit → CorePlan 変換
///
/// Expands scan-specific semantics into generic CorePlan:
/// - header_effects: one=1, needle_len, len, bound, cond_loop
/// - body: i+needle_len, substring, cond_match
/// - step_effects: i_next = i + 1
fn normalize_scan_with_init(
builder: &mut MirBuilder,
parts: ScanWithInitPlan,
ctx: &LoopPatternContext,
) -> Result<CorePlan, String> {
use crate::mir::builder::control_flow::joinir::trace;
let trace_logger = trace::trace();
let debug = ctx.debug;
if debug {
trace_logger.debug(
"normalizer/scan_with_init",
&format!(
"Phase 273 P1: Normalizing ScanWithInit for {}",
ctx.func_name
),
);
}
// P1 Scope: Forward scan (step=1) only
if parts.step_lit != 1 {
return Err(format!(
"[normalizer] P1 scope: only forward scan supported (step={})",
parts.step_lit
));
}
// Step 1: Get host ValueIds for variables
let s_host = builder
.variable_ctx
.variable_map
.get(&parts.haystack)
.copied()
.ok_or_else(|| format!("[normalizer] Variable {} not found", parts.haystack))?;
let needle_host = builder
.variable_ctx
.variable_map
.get(&parts.needle)
.copied()
.ok_or_else(|| format!("[normalizer] Variable {} not found", parts.needle))?;
let i_init_val = builder
.variable_ctx
.variable_map
.get(&parts.loop_var)
.copied()
.ok_or_else(|| format!("[normalizer] Loop variable {} not found", parts.loop_var))?;
// Step 2: Capture preheader block (entry to loop) for PHI input
let preheader_bb = builder
.current_block
.ok_or_else(|| "[normalizer] No current block for loop entry".to_string())?;
// Step 3: Allocate BasicBlockIds for 5 blocks
let header_bb = builder.next_block_id();
let body_bb = builder.next_block_id();
let step_bb = builder.next_block_id();
let after_bb = builder.next_block_id();
let found_bb = builder.next_block_id();
// Step 4: Allocate ValueIds for CorePlan
let i_current = builder.next_value_id(); // PHI destination
builder
.type_ctx
.value_types
.insert(i_current, MirType::Integer);
let one_val = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(one_val, MirType::Integer);
let needle_len_val = if parts.dynamic_needle {
let v = builder.next_value_id();
builder.type_ctx.value_types.insert(v, MirType::Integer);
v
} else {
one_val // reuse one_val for fixed needle
};
let len_val = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(len_val, MirType::Integer);
let bound_val = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(bound_val, MirType::Integer);
let cond_loop = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(cond_loop, MirType::Bool);
let i_plus_needle_len = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(i_plus_needle_len, MirType::Integer);
let window_val = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(window_val, MirType::String);
let cond_match = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(cond_match, MirType::Bool);
let i_next_val = builder.next_value_id();
builder
.type_ctx
.value_types
.insert(i_next_val, MirType::Integer);
if debug {
trace_logger.debug(
"normalizer/scan_with_init",
&format!(
"Allocated: preheader={:?}, header={:?}, body={:?}, step={:?}, after={:?}, found={:?}",
preheader_bb, header_bb, body_bb, step_bb, after_bb, found_bb
),
);
}
// Step 5: Build header_effects (emitted in header_bb)
let mut header_effects = vec![
// one = 1
CoreEffectPlan::Const {
dst: one_val,
value: ConstValue::Integer(1),
},
];
// needle_len = needle.length() if dynamic, else reuse one_val
if parts.dynamic_needle {
header_effects.push(CoreEffectPlan::MethodCall {
dst: needle_len_val,
object: needle_host,
method: "length".to_string(),
args: vec![],
});
}
// len = s.length()
header_effects.push(CoreEffectPlan::MethodCall {
dst: len_val,
object: s_host,
method: "length".to_string(),
args: vec![],
});
// bound = len - needle_len
header_effects.push(CoreEffectPlan::BinOp {
dst: bound_val,
lhs: len_val,
op: BinaryOp::Sub,
rhs: needle_len_val,
});
// cond_loop = i <= bound
header_effects.push(CoreEffectPlan::Compare {
dst: cond_loop,
lhs: i_current,
op: CompareOp::Le,
rhs: bound_val,
});
// Step 6: Build body (emitted in body_bb)
let body = vec![
// i_plus_needle_len = i + needle_len
CorePlan::Effect(CoreEffectPlan::BinOp {
dst: i_plus_needle_len,
lhs: i_current,
op: BinaryOp::Add,
rhs: needle_len_val,
}),
// window = s.substring(i, i + needle_len)
CorePlan::Effect(CoreEffectPlan::MethodCall {
dst: window_val,
object: s_host,
method: "substring".to_string(),
args: vec![i_current, i_plus_needle_len],
}),
// cond_match = window == needle
CorePlan::Effect(CoreEffectPlan::Compare {
dst: cond_match,
lhs: window_val,
op: CompareOp::Eq,
rhs: needle_host,
}),
];
// Step 7: Build step_effects (emitted in step_bb)
let step_effects = vec![
// i_next = i + 1
CoreEffectPlan::BinOp {
dst: i_next_val,
lhs: i_current,
op: BinaryOp::Add,
rhs: one_val,
},
];
// Step 8: Build CoreLoopPlan
let loop_plan = CoreLoopPlan {
preheader_bb,
header_bb,
body_bb,
step_bb,
after_bb,
found_bb,
header_effects,
body,
step_effects,
carriers: vec![CoreCarrierInfo {
name: parts.loop_var.clone(),
init_value: i_init_val,
step_value: i_next_val,
phi_dst: i_current,
}],
cond_loop,
cond_match,
loop_var_name: parts.loop_var,
};
if debug {
trace_logger.debug(
"normalizer/scan_with_init",
"CorePlan construction complete",
);
}
Ok(CorePlan::Loop(loop_plan))
}
}

View File

@ -0,0 +1,245 @@
//! Phase 273 P1: PlanVerifier - CorePlan 不変条件検証 (fail-fast)
//!
//! # Responsibilities
//!
//! - Validate CorePlan invariants before lowering
//! - Fail fast on close-but-unsupported patterns
//! - Prevent silent miscompilation
//!
//! # Invariants (V1-V6)
//!
//! - V1: Carrier completeness (name/init_value/step_value present)
//! - V2: Condition validity (valid ValueId)
//! - V3: Exit validity (Return in function, Break/Continue in loop)
//! - V4: Seq non-empty
//! - V5: If completeness (then_plans non-empty)
//! - V6: ValueId validity (all ValueIds pre-generated)
use super::{CoreCarrierInfo, CoreEffectPlan, CoreExitPlan, CoreIfPlan, CoreLoopPlan, CorePlan};
use crate::mir::ValueId;
/// Phase 273 P1: PlanVerifier - CorePlan 不変条件検証 (fail-fast)
pub(in crate::mir::builder) struct PlanVerifier;
impl PlanVerifier {
/// Verify CorePlan invariants
///
/// Returns Ok(()) if all invariants hold, Err with details otherwise.
pub(in crate::mir::builder) fn verify(plan: &CorePlan) -> Result<(), String> {
Self::verify_plan(plan, 0, false)
}
fn verify_plan(plan: &CorePlan, depth: usize, in_loop: bool) -> Result<(), String> {
match plan {
CorePlan::Seq(plans) => Self::verify_seq(plans, depth, in_loop),
CorePlan::Loop(loop_plan) => Self::verify_loop(loop_plan, depth),
CorePlan::If(if_plan) => Self::verify_if(if_plan, depth, in_loop),
CorePlan::Effect(effect) => Self::verify_effect(effect, depth),
CorePlan::Exit(exit) => Self::verify_exit(exit, depth, in_loop),
}
}
/// V4: Seq non-empty
fn verify_seq(plans: &[CorePlan], depth: usize, in_loop: bool) -> Result<(), String> {
if plans.is_empty() {
return Err(format!(
"[V4] Empty Seq at depth {} (Seq must have at least one plan)",
depth
));
}
for (i, plan) in plans.iter().enumerate() {
Self::verify_plan(plan, depth + 1, in_loop).map_err(|e| {
format!("[Seq[{}]] {}", i, e)
})?;
}
Ok(())
}
/// V1: Carrier completeness, V2: Condition validity
fn verify_loop(loop_plan: &CoreLoopPlan, depth: usize) -> Result<(), String> {
// V1: Carrier completeness
for (i, carrier) in loop_plan.carriers.iter().enumerate() {
Self::verify_carrier(carrier, depth, i)?;
}
// V2: Condition validity (basic check - ValueId should be non-zero for safety)
// Note: Full ValueId validity check would require builder context
Self::verify_value_id_basic(loop_plan.cond_loop, depth, "cond_loop")?;
Self::verify_value_id_basic(loop_plan.cond_match, depth, "cond_match")?;
// Verify header_effects
for (i, effect) in loop_plan.header_effects.iter().enumerate() {
Self::verify_effect(effect, depth).map_err(|e| {
format!("[Loop.header_effects[{}]] {}", i, e)
})?;
}
// Verify body plans (now in_loop = true)
for (i, plan) in loop_plan.body.iter().enumerate() {
Self::verify_plan(plan, depth + 1, true).map_err(|e| {
format!("[Loop.body[{}]] {}", i, e)
})?;
}
// Verify step_effects
for (i, effect) in loop_plan.step_effects.iter().enumerate() {
Self::verify_effect(effect, depth).map_err(|e| {
format!("[Loop.step_effects[{}]] {}", i, e)
})?;
}
Ok(())
}
/// V1: Carrier completeness
fn verify_carrier(carrier: &CoreCarrierInfo, depth: usize, index: usize) -> Result<(), String> {
if carrier.name.is_empty() {
return Err(format!(
"[V1] Carrier[{}] at depth {} has empty name",
index, depth
));
}
Self::verify_value_id_basic(carrier.init_value, depth, &format!("carrier[{}].init_value", index))?;
Self::verify_value_id_basic(carrier.step_value, depth, &format!("carrier[{}].step_value", index))?;
Self::verify_value_id_basic(carrier.phi_dst, depth, &format!("carrier[{}].phi_dst", index))?;
Ok(())
}
/// V5: If completeness
fn verify_if(if_plan: &CoreIfPlan, depth: usize, in_loop: bool) -> Result<(), String> {
// V2: Condition validity
Self::verify_value_id_basic(if_plan.condition, depth, "if.condition")?;
// V5: then_plans non-empty
if if_plan.then_plans.is_empty() {
return Err(format!(
"[V5] If at depth {} has empty then_plans",
depth
));
}
for (i, plan) in if_plan.then_plans.iter().enumerate() {
Self::verify_plan(plan, depth + 1, in_loop).map_err(|e| {
format!("[If.then[{}]] {}", i, e)
})?;
}
if let Some(else_plans) = &if_plan.else_plans {
for (i, plan) in else_plans.iter().enumerate() {
Self::verify_plan(plan, depth + 1, in_loop).map_err(|e| {
format!("[If.else[{}]] {}", i, e)
})?;
}
}
Ok(())
}
/// V6: Effect ValueId validity
fn verify_effect(effect: &CoreEffectPlan, depth: usize) -> Result<(), String> {
match effect {
CoreEffectPlan::MethodCall { dst, object, method, args } => {
Self::verify_value_id_basic(*dst, depth, "MethodCall.dst")?;
Self::verify_value_id_basic(*object, depth, "MethodCall.object")?;
if method.is_empty() {
return Err(format!(
"[V6] MethodCall at depth {} has empty method name",
depth
));
}
for (i, arg) in args.iter().enumerate() {
Self::verify_value_id_basic(*arg, depth, &format!("MethodCall.args[{}]", i))?;
}
}
CoreEffectPlan::BinOp { dst, lhs, op: _, rhs } => {
Self::verify_value_id_basic(*dst, depth, "BinOp.dst")?;
Self::verify_value_id_basic(*lhs, depth, "BinOp.lhs")?;
Self::verify_value_id_basic(*rhs, depth, "BinOp.rhs")?;
}
CoreEffectPlan::Compare { dst, lhs, op: _, rhs } => {
Self::verify_value_id_basic(*dst, depth, "Compare.dst")?;
Self::verify_value_id_basic(*lhs, depth, "Compare.lhs")?;
Self::verify_value_id_basic(*rhs, depth, "Compare.rhs")?;
}
CoreEffectPlan::Const { dst, value: _ } => {
Self::verify_value_id_basic(*dst, depth, "Const.dst")?;
}
}
Ok(())
}
/// V3: Exit validity
fn verify_exit(exit: &CoreExitPlan, depth: usize, in_loop: bool) -> Result<(), String> {
match exit {
CoreExitPlan::Return(opt_val) => {
if let Some(val) = opt_val {
Self::verify_value_id_basic(*val, depth, "Return.value")?;
}
// Return is always valid (in function context)
}
CoreExitPlan::Break => {
if !in_loop {
return Err(format!(
"[V3] Break at depth {} outside of loop",
depth
));
}
}
CoreExitPlan::Continue => {
if !in_loop {
return Err(format!(
"[V3] Continue at depth {} outside of loop",
depth
));
}
}
}
Ok(())
}
/// V6: Basic ValueId validity check
///
/// Note: This is a basic check. Full validity would require builder context.
fn verify_value_id_basic(value_id: ValueId, depth: usize, context: &str) -> Result<(), String> {
// ValueId(0) might be valid in some contexts, so we don't check for zero
// This is a placeholder for more sophisticated checks if needed
let _ = (value_id, depth, context);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::{BasicBlockId, BinaryOp, CompareOp, ConstValue, ValueId};
#[test]
fn test_verify_empty_seq_fails() {
let plan = CorePlan::Seq(vec![]);
let result = PlanVerifier::verify(&plan);
assert!(result.is_err());
assert!(result.unwrap_err().contains("[V4]"));
}
#[test]
fn test_verify_break_outside_loop_fails() {
let plan = CorePlan::Exit(CoreExitPlan::Break);
let result = PlanVerifier::verify(&plan);
assert!(result.is_err());
assert!(result.unwrap_err().contains("[V3]"));
}
#[test]
fn test_verify_const_effect_succeeds() {
let plan = CorePlan::Effect(CoreEffectPlan::Const {
dst: ValueId(1),
value: ConstValue::Integer(42),
});
let result = PlanVerifier::verify(&plan);
assert!(result.is_ok());
}
}

View File

@ -398,6 +398,17 @@ impl NyashRunner {
}
#[cfg(all(not(feature = "llvm-inkwell-legacy")))]
{
// Fail-fast: if the user explicitly requested the llvmlite harness but this binary
// was built without the `llvm-harness` feature, do not silently fall back to mock.
if crate::config::env::env_bool("NYASH_LLVM_USE_HARNESS") {
crate::console_println!(
"❌ LLVM harness requested (NYASH_LLVM_USE_HARNESS=1), but this binary was built without `--features llvm` (llvm-harness)."
);
crate::console_println!(
" Fix: cargo build --release --features llvm"
);
process::exit(1);
}
crate::console_println!("🔧 Mock LLVM Backend Execution:");
crate::console_println!(" Build with --features llvm-inkwell-legacy for Rust/inkwell backend, or set NYASH_LLVM_OBJ_OUT and NYASH_LLVM_USE_HARNESS=1 for harness.");
// NamingBox SSOT: Select entry (arity-aware, Main.main → main fallback)