feat(plan): Phase 273 P2 Step 0 - CoreEffectPlan side effect support

Prepare CoreEffectPlan::MethodCall for Pattern7 (split) support:
- dst: ValueId → Option<ValueId> (for void methods like push)
- effects: EffectMask field added (PURE+Io vs MUT)

Changes:
- plan/mod.rs: MethodCall struct updated
- plan/lowerer.rs: emit_effect() uses dst/effects from plan
- plan/normalizer.rs: MethodCall with explicit effects
- plan/verifier.rs: Handle Option<ValueId> dst

Test: phase258_p0_index_of_string_vm PASS (exit=6)

🤖 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 23:07:49 +09:00
parent 960241795d
commit ec792983cc
4 changed files with 27 additions and 13 deletions

View File

@ -14,7 +14,7 @@
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};
use crate::mir::{MirInstruction, ValueId};
/// Phase 273 P1: PlanLowerer - CorePlan → MIR 生成 (SSOT)
pub(in crate::mir::builder) struct PlanLowerer;
@ -218,14 +218,15 @@ impl PlanLowerer {
value: value.clone(),
})?;
}
CoreEffectPlan::MethodCall { dst, object, method, args } => {
CoreEffectPlan::MethodCall { dst, object, method, args, effects } => {
// P2: dst and effects are now specified by Normalizer
builder.emit_instruction(MirInstruction::BoxCall {
dst: Some(*dst),
dst: *dst,
box_val: *object,
method: method.clone(),
method_id: None,
args: args.clone(),
effects: EffectMask::PURE.add(Effect::Io),
effects: *effects,
})?;
}
CoreEffectPlan::BinOp { dst, lhs, op, rhs } => {

View File

@ -24,7 +24,7 @@
//! This prevents "second language processor" from growing inside Lowerer.
use crate::ast::ASTNode;
use crate::mir::{BasicBlockId, BinaryOp, CompareOp, ConstValue, ValueId};
use crate::mir::{BasicBlockId, BinaryOp, CompareOp, ConstValue, EffectMask, ValueId};
pub(in crate::mir::builder) mod lowerer;
pub(in crate::mir::builder) mod normalizer;
@ -186,14 +186,21 @@ pub(in crate::mir::builder) struct CoreIfPlan {
///
/// Effect vocabulary is minimal (scan-specific variants forbidden):
/// - MethodCall, BinOp, Compare, Const only
///
/// Phase 273 P2: MethodCall now supports:
/// - dst: Option<ValueId> for void methods (e.g., push)
/// - effects: EffectMask for side effects (e.g., MUT for push)
#[derive(Debug, Clone)]
pub(in crate::mir::builder) enum CoreEffectPlan {
/// Method call (args are ValueIds, not Strings!)
///
/// Phase 273 P2: dst is Option for void methods, effects for side effects
MethodCall {
dst: ValueId,
dst: Option<ValueId>, // P2: Option for void methods (push)
object: ValueId,
method: String, // Method name only (OK as String)
method: String, // Method name only (OK as String)
args: Vec<ValueId>,
effects: EffectMask, // P2: Side effect mask (PURE+Io or MUT)
},
/// Binary operation

View File

@ -16,7 +16,7 @@ use super::{
};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::builder::MirBuilder;
use crate::mir::{BinaryOp, CompareOp, ConstValue, MirType};
use crate::mir::{BinaryOp, CompareOp, ConstValue, Effect, EffectMask, MirType};
/// Phase 273 P1: PlanNormalizer - DomainPlan → CorePlan 変換 (SSOT)
pub(in crate::mir::builder) struct PlanNormalizer;
@ -189,19 +189,21 @@ impl PlanNormalizer {
// needle_len = needle.length() if dynamic, else reuse one_val
if parts.dynamic_needle {
header_effects.push(CoreEffectPlan::MethodCall {
dst: needle_len_val,
dst: Some(needle_len_val),
object: needle_host,
method: "length".to_string(),
args: vec![],
effects: EffectMask::PURE.add(Effect::Io),
});
}
// len = s.length()
header_effects.push(CoreEffectPlan::MethodCall {
dst: len_val,
dst: Some(len_val),
object: s_host,
method: "length".to_string(),
args: vec![],
effects: EffectMask::PURE.add(Effect::Io),
});
// bound = len - needle_len
@ -231,10 +233,11 @@ impl PlanNormalizer {
}),
// window = s.substring(i, i + needle_len)
CorePlan::Effect(CoreEffectPlan::MethodCall {
dst: window_val,
dst: Some(window_val),
object: s_host,
method: "substring".to_string(),
args: vec![i_current, i_plus_needle_len],
effects: EffectMask::PURE.add(Effect::Io),
}),
// cond_match = window == needle
CorePlan::Effect(CoreEffectPlan::Compare {

View File

@ -142,8 +142,11 @@ impl PlanVerifier {
/// 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")?;
CoreEffectPlan::MethodCall { dst, object, method, args, effects: _ } => {
// P2: dst is now Option<ValueId>
if let Some(dst_val) = dst {
Self::verify_value_id_basic(*dst_val, depth, "MethodCall.dst")?;
}
Self::verify_value_id_basic(*object, depth, "MethodCall.object")?;
if method.is_empty() {
return Err(format!(