diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs index 5d5365d6..3b0f3f28 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs @@ -186,8 +186,9 @@ fn prepare_pattern2_inputs( // Phase 100 P2-2: Mutable Accumulator Analysis // Detect accumulator pattern (target = target + x) and promote to carrier + // Phase 100 P3-3: Added AccumulatorKind support for string accumulators use crate::mir::loop_pattern_detection::mutable_accumulator_analyzer::{ - MutableAccumulatorAnalyzer, RhsExprKind, + AccumulatorKind, MutableAccumulatorAnalyzer, RhsExprKind, }; let mutable_spec = MutableAccumulatorAnalyzer::analyze(body)?; @@ -221,6 +222,81 @@ fn prepare_pattern2_inputs( ) })?; + // Phase 100 P3-3: Refine AccumulatorKind based on actual target type + // AST-only detection (P3-1) defaults to Int for Variables + // Here we check actual type from builder's type context + let mut refined_kind = spec.kind; // Start with AST-detected kind + if spec.rhs_expr_kind == RhsExprKind::Var && refined_kind == AccumulatorKind::Int { + // Check if target is string-typed (refine Int → String if needed) + use crate::mir::MirType; + if let Some(target_type) = builder.type_ctx.value_types.get(target_id) { + match target_type { + MirType::Box(box_name) if box_name == "StringBox" => { + refined_kind = AccumulatorKind::String; + if verbose { + log.log( + "phase100_p3", + format!( + "Refined accumulator kind: Int → String (target '{}' is StringBox)", + spec.target_name + ), + ); + } + } + MirType::Integer => { + // Confirmed Int + if verbose { + log.log( + "phase100_p3", + format!( + "Confirmed accumulator kind: Int (target '{}' is Integer)", + spec.target_name + ), + ); + } + } + _ => { + // Unknown type - keep default Int for backward compat + if verbose { + log.log( + "phase100_p3", + format!( + "Accumulator kind: Int (default, target '{}' type unknown: {:?})", + spec.target_name, target_type + ), + ); + } + } + } + } + } + + // Phase 100 P3-3: Validate String accumulator constraints + if refined_kind == AccumulatorKind::String { + // String accumulator RHS must be Variable (not Literal, not MethodCall) + match spec.rhs_expr_kind { + RhsExprKind::Var => { + // OK: Variable RHS is allowed for String accumulators + if verbose { + log.log( + "phase100_p3", + format!( + "String accumulator '{}' = '{}' + '{}' (Variable RHS: OK)", + spec.target_name, spec.target_name, spec.rhs_var_or_lit + ), + ); + } + } + RhsExprKind::Literal => { + // Fail-Fast: Literal RHS not supported in P3 (will be P3.1) + return Err(format!( + "[joinir/mutable-acc] String accumulator '{}' with Literal RHS not supported in Phase 100 P3 (will be P3.1)", + spec.target_name + )); + } + } + } + // Check RHS read-only via captured_env lookup // According to spec: x ∈ {Const, BodyLocal, Captured, Pinned, Carrier} // At this point in prepare_pattern2_inputs: diff --git a/src/mir/join_ir/lowering/common.rs b/src/mir/join_ir/lowering/common.rs index 460dd46d..505a1dab 100644 --- a/src/mir/join_ir/lowering/common.rs +++ b/src/mir/join_ir/lowering/common.rs @@ -8,6 +8,7 @@ pub mod body_local_slot; // Phase 92 P3: Read-only body-local slot for condition pub mod dual_value_rewriter; // Phase 246-EX/247-EX: name-based dual-value rewrites pub mod condition_only_emitter; // Phase 93 P0: ConditionOnly (Derived Slot) recalculation pub mod body_local_derived_emitter; // Phase 94: Derived body-local (P5b escape "ch" reassignment) +pub mod string_accumulator_emitter; // Phase 100 P3-2: String accumulator (out = out + ch) use crate::mir::loop_form::LoopForm; use crate::mir::query::{MirQuery, MirQueryBox}; diff --git a/src/mir/join_ir/lowering/common/string_accumulator_emitter.rs b/src/mir/join_ir/lowering/common/string_accumulator_emitter.rs new file mode 100644 index 00000000..c944766c --- /dev/null +++ b/src/mir/join_ir/lowering/common/string_accumulator_emitter.rs @@ -0,0 +1,116 @@ +//! Phase 100 P3-2: String Accumulator Emitter +//! +//! Dedicated emitter for string concatenation in JoinIR (VM/LLVM same semantics). +//! +//! # Responsibility +//! +//! Emit JoinIR instructions for string concatenation: `out = out + ch` +//! where ch is a Variable (not Literal, not MethodCall). +//! +//! # Design +//! +//! String concatenation uses BinOp(Add) just like integer addition. +//! Type semantics are enforced by: +//! 1. P3-1: AccumulatorKind detection (Int vs String) +//! 2. P3-3: Pattern2 wiring validation (RHS must be Variable, string-typed) +//! 3. This emitter: Emit BinOp(Add) for string operands +//! +//! The VM and LLVM backends handle BinOp(Add) polymorphically: +//! - Integer + Integer → integer addition +//! - String + String → string concatenation (via AddOperator.apply/2) +//! +//! # Phase 100 P3 Contract +//! +//! **Allowed**: `out = out + ch` where ch ∈ {Variable (string-typed)} +//! **Fail-Fast**: Literal RHS, MethodCall RHS, non-string RHS + +use crate::mir::join_ir::{BinOpKind, JoinInst, MirLikeInst}; +use crate::mir::ValueId; + +/// Emit string concatenation in JoinIR +/// +/// # Arguments +/// +/// * `target_id` - ValueId of the accumulator variable (e.g., "out") +/// * `rhs_id` - ValueId of the RHS variable (e.g., "ch") +/// * `alloc_value` - ValueId allocator closure +/// * `instructions` - Output vector to append instructions to +/// +/// # Returns +/// +/// ValueId of the concatenation result +/// +/// # Example +/// +/// ```ignore +/// // For "out = out + ch": +/// let out_next = emit_string_concat( +/// out_param, // ValueId of "out" parameter +/// ch_value, // ValueId of "ch" body-local variable +/// &mut alloc_value, +/// &mut instructions, +/// )?; +/// // Generates: +/// // out_next = BinOp(Add, out_param, ch_value) +/// ``` +pub fn emit_string_concat( + target_id: ValueId, + rhs_id: ValueId, + alloc_value: &mut dyn FnMut() -> ValueId, + instructions: &mut Vec, +) -> Result { + // Phase 100 P3-2: Emit BinOp(Add) for string concatenation + // Type semantics enforced by Pattern2 wiring (P3-3) + let result = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: result, + op: BinOpKind::Add, + lhs: target_id, + rhs: rhs_id, + })); + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_emit_string_concat() { + // Phase 100 P3-2: Unit test for string concat emission + let mut value_counter = 10u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + + let target_id = ValueId(1); // "out" parameter + let rhs_id = ValueId(2); // "ch" body-local + + let mut instructions = Vec::new(); + + let result = emit_string_concat( + target_id, + rhs_id, + &mut alloc_value, + &mut instructions, + ).unwrap(); + + // Verify result + assert_eq!(result, ValueId(10)); + + // Verify instruction + assert_eq!(instructions.len(), 1); + match &instructions[0] { + JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => { + assert_eq!(*dst, ValueId(10)); + assert_eq!(*op, BinOpKind::Add); + assert_eq!(*lhs, target_id); + assert_eq!(*rhs, rhs_id); + } + _ => panic!("Expected BinOp instruction"), + } + } +}