joinir: clean pattern visibility and refactor pattern2 pipeline

This commit is contained in:
nyash-codex
2025-12-11 19:11:26 +09:00
parent 463a6240fb
commit 59a985b7fa
16 changed files with 1141 additions and 812 deletions

View File

@ -1032,3 +1032,110 @@ JoinIR は Rust 側だけでなく、将来的に .hako selfhost コンパイラ
- 根本原因: carrier 検出ロジックの name heuristic が脆弱
- **次フェーズ**: Carrier 検出修正Phase 219
- 詳細: phase218-jsonparser-if-sum-min.md
---
## 3. JoinIR → JoinIR 正規化レイヤ(構想 / Phase 26-H ライン)
### 3.1 Structured JoinIR と Normalized JoinIR概念レベル
JoinIR ラインは、今後は「同じ JoinIR をフェーズごとに正規化していく」二段構成で扱う想定だよ:
- **Structured JoinIR現行層**
- Pattern15 / CarrierInfo / ConditionEnv / UpdateEnv / Boundary / ExitLine までを終えた状態。
- while/if/break/continue/return がまだ「構造」として残っていてよい層。
- いまの JoinIR loweringPattern lowerer 群)が出力しているものはここ。
- **Normalized JoinIRJoinIR / CPS 風層)**
- 制御構造をすべて「関数+継続+ Env」の形に正規化した状態。
- ループは `loop_step(env, k_exit)` と exit 継続のペア、if は `if_branch(env, k_then, k_else)` join 継続で表現。
- while/break/continue/return は **TailCallFn/TailCallKont + If** だけに還元され、goto/生の branch は現れない。
- Env は「ループキャリア + DigitPos/num_str などの Derived + captured 変数」を 1 つの struct としてまとめる。
パイプラインのイメージ:
```text
AST
JoinIR(Structured) // Pattern15, CarrierInfo, ConditionEnv/UpdateEnv, Boundary/ExitLine
↓ JoinIR パス A: 正規化JoinIR → JoinIR
JoinIR(Normalized) // 関数 + 継続 + Env のみTailCall-only
↓ JoinIR→MIR bridge
MIR
```
型レベルでは、次の二案のどちらかで扱う想定だよ(詳細は今後の Phase で選択):
- **案① 型を分ける**
- `JoinModuleRaw`Structured、`JoinModuleCps`Normalizedを別型にする。
- BridgeJoinIR→MIRは `JoinModuleCps` だけを受け取る。
- **案② 型は 1 つ+フェーズフラグ**
- `JoinModule { phase: JoinIrPhase, ... }` として、`phase = Structured / Normalized` をメタデータで持つ。
- JoinIR パスはすべて `fn run(&mut JoinModule)`= joinir→joinirで構成し、
Verifier が「phase=Normalized なら while/if/break/continue/return 禁止」などの不変条件をチェックする。
どちらの案でも本質は同じで、「JoinIR の下に JoinIR= 正規形フェーズ)を 1 段挟む」という設計、というのがポイントだよ。
### 3.2 Normalized JoinIR の基本モデル(ラフスケッチ)
Normalized JoinIR では、制御構造を次の 3 要素だけで表現する想定だよ:
- **Env環境**
- そのループ/if に必要な情報を 1 つにまとめた struct。
- フィールド種別の例:
- `Carrier`LoopState キャリア: i, sum, result, num_state 等)
- `Derived`DigitPos 二重値, NumberAccumulation 等)
- `Captured`(外側のローカルや関数パラメータ: s, len 等)
- **Fn通常の関数**
- 典型例: `loop_step(env, k_exit)` / `loop_body(env, k_exit)`。
- 末尾は常に `TailCallFn` または `TailCallKont`。
- **Kont継続**
- 典型例: ループ exit 後の処理、if 後の join、関数 return など。
- 末尾は別の Fn/Kont への tail-call。
制御の不変条件Normalized フェーズ):
1. 制御フローは `TailCallFn` / `TailCallKont` / `If(cond, then_k, else_k, env)` のみで表現する。
2. 各ループは「`loop_step(env, k_exit)` 関数 + `k_exit(env)` 継続」のペアとして現れるbreak は必ず k_exit へ TailCall
3. continue は `loop_step` への TailCall として現れる(ヘッダ PHI 相当は Env の書き込み順で表現)。
4. return は専用の `return_kont(env)` への TailCall で表現される。
5. EnvLayoutVerifier検証箱が、常に正しい EnvLayout が使われていることをチェックする。
これにより、現在 JoinIR 層で苦労している「PHI 配線」「exit_bindings/jump_args 整合性」「評価順のねじれ」は、
Normalized JoinIR 側では「Env フィールドの更新順」と「どの継続を呼ぶか」に還元される想定だよ。
### 3.3 Phase 26-H のフェーズ分割(案)
この正規化レイヤを一度に入れるのは重いので、Phase 26-H ラインとして小さく段階分けして進める方針だよ。
(番号は仮で、実際の Phase 番号は current の進行に合わせて調整する想定)
1. **Phase 26-H.A モデル定義と Doc 固定(コード変更最小)**
- JoinIR Architecture Overviewこのファイルに Structured / Normalized の二層構造を明文化(本節)。
- `JoinIrPhase` などのメタ情報だけを追加し、既存 lowering / Bridge の挙動は変えない。
- Normalized JoinIR の不変条件TailCall-only / EnvLayout 整合性)を Verifier の設計として書き出す。
2. **Phase 26-H.B 最小サブセットでの JoinIR→JoinIR パス導入**
- Pattern1単純 while, break/continue なし)だけを対象にした Normalized パスを実装。
- パイプライン:
- 既存: `JoinIR(Structured) → MIR`
- 新規: `JoinIR(Structured) → JoinIR(Normalized, small subset) → MIR`
- 新規パスは dev フラグの裏側で比較用にのみ使い、E2E で結果が一致することを確認(既定経路は従来のまま)。
3. **Phase 26-H.C Pattern24 / DigitPos / JsonParser への拡張**
- Pattern2/3/4 と DigitPos / NumberAccumulation / Trim を Normalized パスの対象に広げる。
- JsonParser `_parse_number` / `_atoi` を優先対象にし、JoinIR(Normalized) 経由での実行を dev フラグ付きで有効化。
- JoinIR(Structured) → JoinIR(Normalized) の各ステップに構造テストを追加し、「Env の更新」「継続の呼び出し順」が期待どおりかを固定。
4. **Phase 26-H.D Normalized JoinIR を canonical route に昇格**
- JoinIR→MIR Bridge が「Normalized フェーズの JoinIR だけ」を受け取るように変更。
- Structured JoinIR → MIR の直接パスは「比較テスト専用」として残すか、段階的に削除。
- selfhost / JsonParser / hako_check の代表ループはすべて Normalized JoinIR 経由で通ることを確認。
各サブフェーズでは「既存意味論を変えない」「Fail-Fast 原則を維持する」「新旧経路の比較テストを先に用意する」をガードとして運用するよ。
詳細な API/型設計は、Phase 26-H.A/B の中で `EnvLayoutBox` / `LoopStepSynthesizer` / `JpVerifier` 等の箱として段階的に固めていく想定。
### 3.4 Phase 27-CLEAN Pattern24 の軽量整理
- Pattern2〜4/loop_with_break_minimal まわりで可視性・ログ・補助関数を整理し、joinir_dev フラグ配下のデバッグログに寄せる。意味論は変えずに「読みやすさ」「追いやすさ」を優先するクリーンアップフェーズだよ。

View File

@ -20,8 +20,8 @@
//! - Delegates to specialized analyzers for break/continue logic
use crate::ast::ASTNode;
use crate::mir::loop_pattern_detection::LoopFeatures;
use crate::mir::loop_pattern_detection::break_condition_analyzer::BreakConditionAnalyzer;
use crate::mir::loop_pattern_detection::LoopFeatures;
/// Detect if a loop body contains continue statements
///
@ -37,7 +37,7 @@ use crate::mir::loop_pattern_detection::break_condition_analyzer::BreakCondition
///
/// This is a simple recursive scan that doesn't handle nested loops perfectly,
/// but is sufficient for initial pattern detection.
pub fn detect_continue_in_body(body: &[ASTNode]) -> bool {
pub(crate) fn detect_continue_in_body(body: &[ASTNode]) -> bool {
for stmt in body {
if has_continue_node(stmt) {
return true;
@ -55,7 +55,7 @@ pub fn detect_continue_in_body(body: &[ASTNode]) -> bool {
/// # Returns
///
/// `true` if at least one break statement is found in the body or nested structures
pub fn detect_break_in_body(body: &[ASTNode]) -> bool {
pub(crate) fn detect_break_in_body(body: &[ASTNode]) -> bool {
for stmt in body {
if has_break_node(stmt) {
return true;
@ -78,11 +78,7 @@ pub fn detect_break_in_body(body: &[ASTNode]) -> bool {
/// # Returns
///
/// A LoopFeatures struct containing all detected structural characteristics
pub fn extract_features(
body: &[ASTNode],
has_continue: bool,
has_break: bool,
) -> LoopFeatures {
pub(crate) fn extract_features(body: &[ASTNode], has_continue: bool, has_break: bool) -> LoopFeatures {
// Phase 212.5: Detect ANY if statement in loop body (structural detection)
let has_if = detect_if_in_body(body);
@ -147,8 +143,12 @@ fn detect_if_else_phi_in_body(body: &[ASTNode]) -> bool {
} = node
{
// Check if both branches have assignments
let then_has_assign = then_body.iter().any(|n| matches!(n, ASTNode::Assignment { .. }));
let else_has_assign = else_body.iter().any(|n| matches!(n, ASTNode::Assignment { .. }));
let then_has_assign = then_body
.iter()
.any(|n| matches!(n, ASTNode::Assignment { .. }));
let else_has_assign = else_body
.iter()
.any(|n| matches!(n, ASTNode::Assignment { .. }));
if then_has_assign && else_has_assign {
return true;
}
@ -193,7 +193,11 @@ fn count_carriers_in_body(body: &[ASTNode]) -> usize {
}
}
// Return at least 1 if we have assignments, otherwise 0
if count > 0 { 1 } else { 0 }
if count > 0 {
1
} else {
0
}
}
/// Recursive helper to check if AST node contains continue
@ -248,7 +252,7 @@ fn has_break_node(node: &ASTNode) -> bool {
/// # Returns
///
/// `true` if an `if ... else { break }` pattern is found
pub fn has_break_in_else_clause(body: &[ASTNode]) -> bool {
pub(crate) fn has_break_in_else_clause(body: &[ASTNode]) -> bool {
BreakConditionAnalyzer::has_break_in_else_clause(body)
}
@ -286,7 +290,7 @@ pub fn has_break_in_else_clause(body: &[ASTNode]) -> bool {
/// // <- Returns the "!(ch == " ")" condition (negated)
/// }
/// ```
pub fn extract_break_condition(body: &[ASTNode]) -> Option<&ASTNode> {
pub(crate) fn extract_break_condition(body: &[ASTNode]) -> Option<&ASTNode> {
BreakConditionAnalyzer::extract_break_condition(body).ok()
}
@ -296,13 +300,17 @@ mod tests {
#[test]
fn test_detect_continue_simple() {
let continue_node = ASTNode::Continue { span: crate::ast::Span::unknown() };
let continue_node = ASTNode::Continue {
span: crate::ast::Span::unknown(),
};
assert!(has_continue_node(&continue_node));
}
#[test]
fn test_detect_break_simple() {
let break_node = ASTNode::Break { span: crate::ast::Span::unknown() };
let break_node = ASTNode::Break {
span: crate::ast::Span::unknown(),
};
assert!(has_break_node(&break_node));
}

View File

@ -32,13 +32,13 @@
//! This module is now a thin wrapper around `CarrierInfo::from_variable_map()`.
//! The primary logic lives in `carrier_info.rs` for consistency across MIR and JoinIR contexts.
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::ValueId;
use std::collections::BTreeMap;
pub struct CommonPatternInitializer;
pub(crate) struct CommonPatternInitializer;
impl CommonPatternInitializer {
/// Initialize pattern context: extract loop var, build CarrierInfo
@ -84,28 +84,24 @@ impl CommonPatternInitializer {
) -> Result<(String, ValueId, CarrierInfo), String> {
// Step 1: Extract loop variable from condition
let loop_var_name = builder.extract_loop_variable_from_condition(condition)?;
let loop_var_id = variable_map
.get(&loop_var_name)
.copied()
.ok_or_else(|| {
format!(
"[common_init] Loop variable '{}' not found in variable_map",
loop_var_name
)
})?;
let loop_var_id = variable_map.get(&loop_var_name).copied().ok_or_else(|| {
format!(
"[common_init] Loop variable '{}' not found in variable_map",
loop_var_name
)
})?;
// Phase 183-2: Delegate to CarrierInfo::from_variable_map for consistency
// Phase 222.5-D: Direct BTreeMap usage (no conversion needed)
// Step 2: Use CarrierInfo::from_variable_map as primary initialization method
let mut carrier_info = CarrierInfo::from_variable_map(
loop_var_name.clone(),
variable_map,
)?;
let mut carrier_info = CarrierInfo::from_variable_map(loop_var_name.clone(), variable_map)?;
// Step 3: Apply exclusions if provided (Pattern 2 specific)
if let Some(excluded) = exclude_carriers {
carrier_info.carriers.retain(|c| !excluded.contains(&c.name.as_str()));
carrier_info
.carriers
.retain(|c| !excluded.contains(&c.name.as_str()));
}
Ok((loop_var_name, loop_var_id, carrier_info))
@ -150,28 +146,33 @@ impl CommonPatternInitializer {
_loop_var_name: &str,
_variable_map: &BTreeMap<String, ValueId>,
) -> bool {
use crate::mir::join_ir::lowering::loop_update_analyzer::{LoopUpdateAnalyzer, UpdateExpr, UpdateRhs};
use crate::mir::join_ir::lowering::carrier_info::CarrierVar;
use crate::mir::join_ir::lowering::loop_update_analyzer::{
LoopUpdateAnalyzer, UpdateExpr, UpdateRhs,
};
// Create dummy carriers from body assignment targets for analysis
let dummy_carriers: Vec<CarrierVar> = body.iter().filter_map(|node| {
match node {
ASTNode::Assignment { target, .. } => {
if let ASTNode::Variable { name, .. } = target.as_ref() {
Some(CarrierVar {
let dummy_carriers: Vec<CarrierVar> = body
.iter()
.filter_map(|node| {
match node {
ASTNode::Assignment { target, .. } => {
if let ASTNode::Variable { name, .. } = target.as_ref() {
Some(CarrierVar {
name: name.clone(),
host_id: ValueId(0), // Dummy
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState, // Phase 227: Default
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228: Default
})
} else {
None
} else {
None
}
}
_ => None,
}
_ => None,
}
}).collect();
})
.collect();
let updates = LoopUpdateAnalyzer::analyze_carrier_updates(body, &dummy_carriers);

View File

@ -35,7 +35,7 @@ use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
use crate::mir::ValueId;
use std::collections::BTreeMap;
pub struct ConditionEnvBuilder;
pub(crate) struct ConditionEnvBuilder;
impl ConditionEnvBuilder {
/// Phase 201: Build ConditionEnv using JoinValueSpace (disjoint ValueId regions)
@ -65,10 +65,8 @@ impl ConditionEnvBuilder {
space: &mut JoinValueSpace,
) -> Result<(ConditionEnv, Vec<ConditionBinding>, ValueId), String> {
// Extract all variables used in the condition (excluding loop parameter)
let condition_var_names = extract_condition_variables(
break_condition,
&[loop_var_name.to_string()],
);
let condition_var_names =
extract_condition_variables(break_condition, &[loop_var_name.to_string()]);
let mut env = ConditionEnv::new();
let mut bindings = Vec::new();
@ -79,16 +77,13 @@ impl ConditionEnvBuilder {
// For each condition variable, allocate JoinIR-local ValueId and build binding
for var_name in &condition_var_names {
let host_id = variable_map
.get(var_name)
.copied()
.ok_or_else(|| {
format!(
"Condition variable '{}' not found in variable_map. \
let host_id = variable_map.get(var_name).copied().ok_or_else(|| {
format!(
"Condition variable '{}' not found in variable_map. \
Loop condition references undefined variable.",
var_name
)
})?;
var_name
)
})?;
// Phase 201: Allocate from Param region to avoid collision with locals
let join_id = space.alloc_param();
@ -108,7 +103,10 @@ impl ConditionEnvBuilder {
///
/// Uses JoinValueSpace to allocate the loop parameter ValueId.
#[allow(dead_code)]
pub fn build_loop_param_only_v2(loop_var_name: &str, space: &mut JoinValueSpace) -> (ConditionEnv, ValueId) {
pub fn build_loop_param_only_v2(
loop_var_name: &str,
space: &mut JoinValueSpace,
) -> (ConditionEnv, ValueId) {
let mut env = ConditionEnv::new();
let loop_var_join_id = space.alloc_param();
env.insert(loop_var_name.to_string(), loop_var_join_id);
@ -177,7 +175,10 @@ impl ConditionEnvBuilder {
let debug = env::var("NYASH_CAPTURE_DEBUG").is_ok();
if debug {
eprintln!("[capture/env_builder] Building ConditionEnv with {} captured vars", captured.vars.len());
eprintln!(
"[capture/env_builder] Building ConditionEnv with {} captured vars",
captured.vars.len()
);
}
// Step 1: Build base ConditionEnv with loop params using v2 API (Phase 222.5-B)
@ -197,17 +198,25 @@ impl ConditionEnvBuilder {
};
// 2b: Add to boundary with Condition role
boundary.add_param_with_role(&var.name, host_id, crate::mir::join_ir::lowering::inline_boundary_builder::ParamRole::Condition);
boundary.add_param_with_role(
&var.name,
host_id,
crate::mir::join_ir::lowering::inline_boundary_builder::ParamRole::Condition,
);
// 2c: Get JoinIR ValueId from boundary
let join_id = boundary.get_condition_binding(&var.name)
let join_id = boundary
.get_condition_binding(&var.name)
.expect("captured var should be in boundary after add_param_with_role");
// 2d: Add to ConditionEnv.captured map
env.captured.insert(var.name.clone(), join_id);
if debug {
eprintln!("[capture/env_builder] Added captured var '{}': host={:?}, join={:?}", var.name, host_id, join_id);
eprintln!(
"[capture/env_builder] Added captured var '{}': host={:?}, join={:?}",
var.name, host_id, join_id
);
}
}
@ -224,7 +233,11 @@ impl ConditionEnvBuilder {
if debug {
let param_count = env.iter().count();
eprintln!("[capture/env_builder] Final ConditionEnv: {} params, {} captured", param_count, env.captured.len());
eprintln!(
"[capture/env_builder] Final ConditionEnv: {} params, {} captured",
param_count,
env.captured.len()
);
}
(env, loop_var_join_id)
@ -351,9 +364,7 @@ mod tests {
// Should return error
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("undefined_var"));
assert!(result.unwrap_err().contains("undefined_var"));
}
/// Phase 201: Test that v2 API uses JoinValueSpace correctly
@ -410,7 +421,8 @@ mod tests {
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
let mut space = JoinValueSpace::new();
let (env, loop_var_join_id) = ConditionEnvBuilder::build_loop_param_only_v2("i", &mut space);
let (env, loop_var_join_id) =
ConditionEnvBuilder::build_loop_param_only_v2("i", &mut space);
// Phase 201: Should use Param region
assert_eq!(loop_var_join_id, ValueId(100));

View File

@ -3,11 +3,9 @@
//! Applies exit bindings to JoinInlineBoundary.
//! Single-responsibility box for boundary application logic.
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::inline_boundary::{
JoinInlineBoundary, LoopExitBinding,
};
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta};
use crate::mir::join_ir::lowering::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
use crate::mir::ValueId;
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
/// Apply bindings to JoinInlineBoundary
@ -27,7 +25,7 @@ use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for deter
/// # Returns
///
/// Success or error if boundary cannot be updated
pub fn apply_exit_bindings_to_boundary(
pub(crate) fn apply_exit_bindings_to_boundary(
carrier_info: &CarrierInfo,
exit_meta: &ExitMeta,
variable_map: &BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
@ -40,13 +38,14 @@ pub fn apply_exit_bindings_to_boundary(
let mut join_outputs = vec![carrier_info.loop_var_id]; // legacy field for compatibility
for carrier in &carrier_info.carriers {
let post_loop_id = variable_map.get(&carrier.name).copied().ok_or_else(|| {
format!("Post-loop ValueId not found for carrier '{}'", carrier.name)
})?;
let post_loop_id = variable_map
.get(&carrier.name)
.copied()
.ok_or_else(|| format!("Post-loop ValueId not found for carrier '{}'", carrier.name))?;
let join_exit_id = exit_meta.find_binding(&carrier.name).ok_or_else(|| {
format!("Exit value not found for carrier '{}'", carrier.name)
})?;
let join_exit_id = exit_meta
.find_binding(&carrier.name)
.ok_or_else(|| format!("Exit value not found for carrier '{}'", carrier.name))?;
bindings.push(LoopExitBinding {
carrier_name: carrier.name.clone(),
@ -83,7 +82,7 @@ pub fn apply_exit_bindings_to_boundary(
/// # Returns
///
/// LoopExitBinding for the loop variable
pub fn create_loop_var_exit_binding(carrier_info: &CarrierInfo) -> LoopExitBinding {
pub(crate) fn create_loop_var_exit_binding(carrier_info: &CarrierInfo) -> LoopExitBinding {
use crate::mir::join_ir::lowering::carrier_info::CarrierRole;
LoopExitBinding {
carrier_name: carrier_info.loop_var_name.clone(),
@ -126,16 +125,16 @@ mod tests {
let mut boundary = JoinInlineBoundary {
host_inputs: vec![],
join_inputs: vec![],
exit_bindings: vec![], // Phase 171: Add missing field
exit_bindings: vec![], // Phase 171: Add missing field
#[allow(deprecated)]
host_outputs: vec![], // legacy, unused in new assertions
host_outputs: vec![], // legacy, unused in new assertions
join_outputs: vec![],
#[allow(deprecated)]
condition_inputs: vec![], // Phase 171: Add missing field
condition_bindings: vec![], // Phase 171-fix: Add missing field
expr_result: None, // Phase 33-14: Add missing field
loop_var_name: None, // Phase 33-16: Add missing field
carrier_info: None, // Phase 228: Add missing field
condition_inputs: vec![], // Phase 171: Add missing field
condition_bindings: vec![], // Phase 171-fix: Add missing field
expr_result: None, // Phase 33-14: Add missing field
loop_var_name: None, // Phase 33-16: Add missing field
carrier_info: None, // Phase 228: Add missing field
};
apply_exit_bindings_to_boundary(&carrier_info, &exit_meta, &variable_map, &mut boundary)
@ -155,11 +154,7 @@ mod tests {
#[test]
fn test_loop_var_exit_binding() {
let carrier_info = CarrierInfo::with_carriers(
"i".to_string(),
ValueId(5),
vec![],
);
let carrier_info = CarrierInfo::with_carriers("i".to_string(), ValueId(5), vec![]);
let binding = create_loop_var_exit_binding(&carrier_info);
assert_eq!(binding.carrier_name, "i");

View File

@ -3,9 +3,9 @@
//! Constructs loop exit bindings and allocates post-loop ValueIds.
//! Single-responsibility box for binding construction logic.
use crate::mir::ValueId;
use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding;
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta};
use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding;
use crate::mir::ValueId;
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
/// Generate loop exit bindings
@ -24,7 +24,7 @@ use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for deter
/// # Returns
///
/// Vec of LoopExitBinding, one per carrier, sorted by carrier name
pub fn build_loop_exit_bindings(
pub(crate) fn build_loop_exit_bindings(
carrier_info: &CarrierInfo,
exit_meta: &ExitMeta,
variable_map: &mut BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
@ -33,7 +33,8 @@ pub fn build_loop_exit_bindings(
// Process each carrier in sorted order
for carrier in &carrier_info.carriers {
let join_exit_id = exit_meta.find_binding(&carrier.name)
let join_exit_id = exit_meta
.find_binding(&carrier.name)
.ok_or_else(|| format!("Carrier '{}' missing in ExitMeta", carrier.name))?;
bindings.push(LoopExitBinding {
@ -64,12 +65,10 @@ pub fn build_loop_exit_bindings(
/// # Returns
///
/// Newly allocated ValueId
pub fn allocate_new_value_id(variable_map: &BTreeMap<String, ValueId>) -> ValueId { // Phase 222.5-D: HashMap → BTreeMap for determinism
pub(crate) fn allocate_new_value_id(variable_map: &BTreeMap<String, ValueId>) -> ValueId {
// Phase 222.5-D: HashMap → BTreeMap for determinism
// Find the maximum ValueId in current variable_map
let max_id = variable_map.values()
.map(|v| v.0)
.max()
.unwrap_or(0);
let max_id = variable_map.values().map(|v| v.0).max().unwrap_or(0);
// Allocate next sequential ID
// Note: This is a temporary strategy and should be replaced with
@ -131,14 +130,14 @@ mod tests {
host_id: ValueId(11),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
CarrierVar {
name: "sum".to_string(),
host_id: ValueId(10),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
],
);

View File

@ -20,7 +20,7 @@ use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, ExitMeta};
/// # Returns
///
/// Ok(()) if validation passes, Err with descriptive message if validation fails
pub fn validate_exit_binding(
pub(crate) fn validate_exit_binding(
carrier_info: &CarrierInfo,
exit_meta: &ExitMeta,
) -> Result<(), String> {
@ -44,10 +44,7 @@ pub fn validate_exit_binding(
// Validate that all carriers in CarrierInfo have exit values
for carrier in &carrier_info.carriers {
if exit_meta.find_binding(&carrier.name).is_none() {
return Err(format!(
"Carrier '{}' missing in ExitMeta",
carrier.name
));
return Err(format!("Carrier '{}' missing in ExitMeta", carrier.name));
}
}
@ -91,14 +88,14 @@ mod tests {
host_id: ValueId(11),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
CarrierVar {
name: "sum".to_string(),
host_id: ValueId(10),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
],
);

View File

@ -1,9 +1,9 @@
//! Pattern 1: Simple While Loop minimal lowerer
use super::super::trace;
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::super::trace;
/// Phase 194: Detection function for Pattern 1
///
@ -11,7 +11,7 @@ use super::super::trace;
///
/// Pattern 1 matches:
/// - Pattern kind is Pattern1SimpleWhile (no break, no continue, no if-else PHI)
pub fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use crate::mir::loop_pattern_detection::LoopPatternKind;
ctx.pattern_kind == LoopPatternKind::Pattern1SimpleWhile
}
@ -19,7 +19,7 @@ pub fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext)
/// Phase 194: Lowering function for Pattern 1
///
/// Wrapper around cf_loop_pattern1_minimal to match router signature
pub fn lower(
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
@ -51,12 +51,7 @@ impl MirBuilder {
// Phase 179-B: Use PatternPipelineContext for unified preprocessing
use super::pattern_pipeline::{build_pattern_context, PatternVariant};
let ctx = build_pattern_context(
self,
condition,
body,
PatternVariant::Pattern1,
)?;
let ctx = build_pattern_context(self, condition, body, PatternVariant::Pattern1)?;
// Phase 195: Use unified trace
trace::trace().varmap("pattern1_start", &self.variable_map);
@ -85,7 +80,7 @@ impl MirBuilder {
vec![ValueId(0)], // JoinIR's main() parameter (loop variable)
vec![ctx.loop_var_id], // Host's loop variable
)
.with_loop_var_name(Some(ctx.loop_var_name.clone())) // Phase 33-16: Enable header PHI generation for SSA correctness
.with_loop_var_name(Some(ctx.loop_var_name.clone())) // Phase 33-16: Enable header PHI generation for SSA correctness
.build();
// Phase 33-22: Use JoinIRConversionPipeline for unified conversion flow
@ -116,7 +111,10 @@ impl MirBuilder {
let void_val = crate::mir::builder::emission::constant::emit_void(self);
// Phase 195: Use unified trace
trace::trace().debug("pattern1", &format!("Loop complete, returning Void {:?}", void_val));
trace::trace().debug(
"pattern1",
&format!("Loop complete, returning Void {:?}", void_val),
);
Ok(Some(void_val))
}

View File

@ -15,11 +15,11 @@
//! - Hardcoded loop condition (i <= 5), if condition (i % 2 == 1)
//! - Kept for backward compatibility with existing tests
use super::super::merge::exit_line::meta_collector::ExitMetaCollector;
use super::super::trace;
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::super::merge::exit_line::meta_collector::ExitMetaCollector;
use super::super::trace;
// Phase 213: Hardcoded ValueIds removed - now using ExitMeta-based exit binding generation
// See: ExitMetaCollector usage below (lines 115-135)
@ -32,7 +32,7 @@ use super::super::trace;
/// - Pattern kind is Pattern3IfPhi (has if-else with PHI, no break/continue)
///
/// NOTE: Priority is now handled by pattern classification, not router order
pub fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use crate::mir::loop_pattern_detection::LoopPatternKind;
ctx.pattern_kind == LoopPatternKind::Pattern3IfPhi
}
@ -40,7 +40,7 @@ pub fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext)
/// Phase 194: Lowering function for Pattern 3
///
/// Wrapper around cf_loop_pattern3_with_if_phi to match router signature
pub fn lower(
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
@ -72,22 +72,23 @@ impl MirBuilder {
) -> Result<Option<ValueId>, String> {
// Phase 179-B: Use PatternPipelineContext for unified preprocessing
use super::pattern_pipeline::{build_pattern_context, PatternVariant};
let ctx = build_pattern_context(
self,
condition,
body,
PatternVariant::Pattern3,
)?;
let ctx = build_pattern_context(self, condition, body, PatternVariant::Pattern3)?;
// Phase 213: AST-based if-sum pattern detection
// Phase 242-EX-A: Legacy mode removed - all if-sum patterns now handled dynamically
if !ctx.is_if_sum_pattern() {
// Not an if-sum pattern → let router try other patterns or fall back
trace::trace().debug("pattern3", "Not an if-sum pattern, returning None to try other patterns");
trace::trace().debug(
"pattern3",
"Not an if-sum pattern, returning None to try other patterns",
);
return Ok(None);
}
trace::trace().debug("pattern3", "Detected if-sum pattern, using AST-based lowerer");
trace::trace().debug(
"pattern3",
"Detected if-sum pattern, using AST-based lowerer",
);
self.lower_pattern3_if_sum(&ctx, condition, body, debug)
}
@ -134,35 +135,34 @@ impl MirBuilder {
for binding in &condition_bindings {
trace::trace().debug(
"pattern3/if-sum",
&format!(" '{}': HOST {:?} → JoinIR {:?}", binding.name, binding.host_value, binding.join_value),
&format!(
" '{}': HOST {:?} → JoinIR {:?}",
binding.name, binding.host_value, binding.join_value
),
);
}
// Call AST-based if-sum lowerer with ConditionEnv
let (join_module, fragment_meta) = lower_if_sum_pattern(
condition,
if_stmt,
body,
&cond_env,
&mut join_value_space,
)?;
let (join_module, fragment_meta) =
lower_if_sum_pattern(condition, if_stmt, body, &cond_env, &mut join_value_space)?;
let exit_meta = &fragment_meta.exit_meta;
trace::trace().debug(
"pattern3/if-sum",
&format!("ExitMeta: {} exit values", exit_meta.exit_values.len())
&format!("ExitMeta: {} exit values", exit_meta.exit_values.len()),
);
for (carrier_name, join_value) in &exit_meta.exit_values {
trace::trace().debug(
"pattern3/if-sum",
&format!(" {} → ValueId({})", carrier_name, join_value.0)
&format!(" {} → ValueId({})", carrier_name, join_value.0),
);
}
// Build exit bindings using ExitMetaCollector
// Phase 228-8: Pass carrier_info to include ConditionOnly carriers
let exit_bindings = ExitMetaCollector::collect(self, exit_meta, Some(&ctx.carrier_info), debug);
let exit_bindings =
ExitMetaCollector::collect(self, exit_meta, Some(&ctx.carrier_info), debug);
// Build boundary with carrier inputs
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
@ -174,9 +174,7 @@ impl MirBuilder {
// Allocate join_inputs dynamically from JoinValueSpace
// These ValueIds (0, 1, 2, ...) represent JoinIR function parameters
let join_inputs: Vec<ValueId> = (0..total_inputs)
.map(|i| ValueId(i as u32))
.collect();
let join_inputs: Vec<ValueId> = (0..total_inputs).map(|i| ValueId(i as u32)).collect();
// Build host_inputs: loop_var + exit_bindings (in order)
let mut host_inputs = vec![ctx.loop_var_id];
@ -197,15 +195,16 @@ impl MirBuilder {
"pattern3/if-sum",
&format!(
"Boundary inputs: {} total (loop_var + {} exit bindings)",
total_inputs, exit_bindings.len()
)
total_inputs,
exit_bindings.len()
),
);
// Phase 215-2: Pass expr_result to boundary
// Phase 220-D: Pass condition_bindings for variable remapping
let mut boundary_builder = JoinInlineBoundaryBuilder::new()
.with_inputs(join_inputs, host_inputs)
.with_condition_bindings(condition_bindings) // Phase 220-D: Map condition-only vars
.with_condition_bindings(condition_bindings) // Phase 220-D: Map condition-only vars
.with_exit_bindings(exit_bindings)
.with_loop_var_name(Some(ctx.loop_var_name.clone()));
@ -213,7 +212,7 @@ impl MirBuilder {
if let Some(expr_id) = fragment_meta.expr_result {
trace::trace().debug(
"pattern3/if-sum",
&format!("Passing expr_result={:?} to boundary", expr_id)
&format!("Passing expr_result={:?} to boundary", expr_id),
);
boundary_builder = boundary_builder.with_expr_result(Some(expr_id));
}
@ -234,7 +233,7 @@ impl MirBuilder {
if let Some(expr_val) = merge_result {
trace::trace().debug(
"pattern3/if-sum",
&format!("Loop complete, returning expr_result {:?}", expr_val)
&format!("Loop complete, returning expr_result {:?}", expr_val),
);
Ok(Some(expr_val))
} else {
@ -243,7 +242,7 @@ impl MirBuilder {
let void_val = constant::emit_void(self);
trace::trace().debug(
"pattern3/if-sum",
&format!("Loop complete, returning Void {:?}", void_val)
&format!("Loop complete, returning Void {:?}", void_val),
);
Ok(Some(void_val))
}

View File

@ -19,7 +19,7 @@ use crate::mir::join_ir::lowering::continue_branch_normalizer::ContinueBranchNor
use crate::mir::join_ir::lowering::loop_update_analyzer::{LoopUpdateAnalyzer, UpdateExpr};
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
pub struct Pattern4CarrierAnalyzer;
pub(crate) struct Pattern4CarrierAnalyzer;
impl Pattern4CarrierAnalyzer {
/// Analyze and filter carriers for continue pattern
@ -54,10 +54,8 @@ impl Pattern4CarrierAnalyzer {
all_carriers: &CarrierInfo,
) -> Result<CarrierInfo, String> {
// Identify which carriers are updated in loop body
let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(
loop_body,
&all_carriers.carriers,
);
let carrier_updates =
LoopUpdateAnalyzer::analyze_carrier_updates(loop_body, &all_carriers.carriers);
// Filter carriers: only keep those that have update expressions
let updated_carriers: Vec<CarrierVar> = all_carriers
@ -87,7 +85,8 @@ impl Pattern4CarrierAnalyzer {
pub fn analyze_carrier_updates(
loop_body: &[ASTNode],
carriers: &[CarrierVar],
) -> BTreeMap<String, UpdateExpr> { // Phase 222.5-D: HashMap → BTreeMap for determinism
) -> BTreeMap<String, UpdateExpr> {
// Phase 222.5-D: HashMap → BTreeMap for determinism
LoopUpdateAnalyzer::analyze_carrier_updates(loop_body, carriers)
}
@ -155,9 +154,9 @@ impl Pattern4CarrierAnalyzer {
..
} => {
then_body.iter().any(|n| Self::has_continue(n))
|| else_body.as_ref().map_or(false, |body| {
body.iter().any(|n| Self::has_continue(n))
})
|| else_body
.as_ref()
.map_or(false, |body| body.iter().any(|n| Self::has_continue(n)))
}
ASTNode::Loop { body, .. } => body.iter().any(|n| Self::has_continue(n)),
_ => false,
@ -331,9 +330,7 @@ mod tests {
}),
span: span.clone(),
}],
else_body: Some(vec![ASTNode::Continue {
span: span.clone(),
}]),
else_body: Some(vec![ASTNode::Continue { span: span.clone() }]),
span: span.clone(),
}];

View File

@ -31,12 +31,12 @@
//! - **ExitMeta**: Maps final carrier values to host variable slots
//! - **Phase 33-21 fix**: Correct remapping of function parameters to header PHI dsts
use super::super::trace;
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
use super::super::trace;
use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::loop_pattern_detection::error_messages;
use crate::mir::ValueId;
use std::collections::BTreeMap;
/// Phase 194+: Detection function for Pattern 4
@ -62,9 +62,9 @@ use std::collections::BTreeMap;
/// 3. **Phase 178**: No string/complex carrier updates (JoinIR doesn't support string concat)
///
/// If all conditions are met, Pattern 4 is detected.
pub fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use crate::mir::loop_pattern_detection::LoopPatternKind;
pub(crate) fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use super::common_init::CommonPatternInitializer;
use crate::mir::loop_pattern_detection::LoopPatternKind;
// Basic pattern check
if ctx.pattern_kind != LoopPatternKind::Pattern4Continue {
@ -97,17 +97,12 @@ pub fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext)
/// 4. Create JoinInlineBoundary for input/output mapping
/// 5. Merge MIR blocks into current_function
/// 6. Return loop result (first carrier value)
pub fn lower(
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
// Phase 33-19: Connect stub to actual implementation
builder.cf_loop_pattern4_with_continue(
ctx.condition,
ctx.body,
ctx.func_name,
ctx.debug,
)
builder.cf_loop_pattern4_with_continue(ctx.condition, ctx.body, ctx.func_name, ctx.debug)
}
impl MirBuilder {
@ -170,10 +165,10 @@ fn prepare_pattern4_context(
) -> Result<Pattern4Prepared, String> {
use super::pattern4_carrier_analyzer::Pattern4CarrierAnalyzer;
use super::pattern_pipeline::{build_pattern_context, PatternVariant};
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
use crate::mir::loop_pattern_detection::loop_body_cond_promoter::{
LoopBodyCondPromoter, ConditionPromotionRequest, ConditionPromotionResult,
ConditionPromotionRequest, ConditionPromotionResult, LoopBodyCondPromoter,
};
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox;
// Normalize continue branches for analysis/lowering
let normalized_body = Pattern4CarrierAnalyzer::normalize_continue_branches(body);
@ -195,28 +190,33 @@ fn prepare_pattern4_context(
&normalized_body,
&carrier_info_prelim.carriers,
);
let mut carrier_info = Pattern4CarrierAnalyzer::analyze_carriers(
&normalized_body,
&carrier_info_prelim,
)?;
let mut carrier_info =
Pattern4CarrierAnalyzer::analyze_carriers(&normalized_body, &carrier_info_prelim)?;
trace::trace().debug(
"pattern4",
&format!(
"CarrierInfo: loop_var={}, carriers={:?}",
carrier_info.loop_var_name,
carrier_info.carriers.iter().map(|c| &c.name).collect::<Vec<_>>()
)
carrier_info
.carriers
.iter()
.map(|c| &c.name)
.collect::<Vec<_>>()
),
);
trace::trace().debug(
"pattern4",
&format!("Analyzed {} carrier update expressions", carrier_updates.len())
&format!(
"Analyzed {} carrier update expressions",
carrier_updates.len()
),
);
for (carrier_name, update_expr) in &carrier_updates {
trace::trace().debug(
"pattern4",
&format!(" {}{:?}", carrier_name, update_expr)
&format!(" {}{:?}", carrier_name, update_expr),
);
}
@ -228,18 +228,15 @@ fn prepare_pattern4_context(
vec![condition]
};
let cond_scope = LoopConditionScopeBox::analyze(
&loop_var_name,
&conditions_to_analyze,
Some(&loop_scope),
);
let cond_scope =
LoopConditionScopeBox::analyze(&loop_var_name, &conditions_to_analyze, Some(&loop_scope));
if cond_scope.has_loop_body_local() {
let promotion_req = ConditionPromotionRequest {
loop_param_name: &loop_var_name,
cond_scope: &cond_scope,
scope_shape: Some(&loop_scope),
break_cond: None, // Pattern 4 has no break
break_cond: None, // Pattern 4 has no break
continue_cond,
loop_body: &normalized_body,
};
@ -285,13 +282,15 @@ fn prepare_pattern4_context(
} else {
return Err(error_messages::format_error_pattern4_trim_not_safe(
&helper.carrier_name,
helper.whitespace_count()
helper.whitespace_count(),
));
}
}
}
ConditionPromotionResult::CannotPromote { reason, vars } => {
return Err(error_messages::format_error_pattern4_promotion_failed(&vars, &reason));
return Err(error_messages::format_error_pattern4_promotion_failed(
&vars, &reason,
));
}
}
}
@ -311,11 +310,11 @@ fn lower_pattern4_joinir(
prepared: &Pattern4Prepared,
debug: bool,
) -> Result<Option<ValueId>, String> {
use super::super::merge::exit_line::meta_collector::ExitMetaCollector;
use super::conversion_pipeline::JoinIRConversionPipeline;
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal;
use super::super::merge::exit_line::meta_collector::ExitMetaCollector;
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
use super::conversion_pipeline::JoinIRConversionPipeline;
trace::trace().varmap("pattern4_start", &builder.variable_map);
@ -338,12 +337,12 @@ fn lower_pattern4_joinir(
trace::trace().debug(
"pattern4",
&format!("ExitMeta: {} exit bindings", exit_meta.exit_values.len())
&format!("ExitMeta: {} exit bindings", exit_meta.exit_values.len()),
);
for (carrier_name, join_value) in &exit_meta.exit_values {
trace::trace().debug(
"pattern4",
&format!(" {} → ValueId({})", carrier_name, join_value.0)
&format!(" {} → ValueId({})", carrier_name, join_value.0),
);
}
@ -356,7 +355,9 @@ fn lower_pattern4_joinir(
for carrier in &prepared.carrier_info.carriers {
if !exit_bindings.iter().any(|b| b.carrier_name == carrier.name) {
return Err(error_messages::format_error_pattern4_carrier_not_found(&carrier.name));
return Err(error_messages::format_error_pattern4_carrier_not_found(
&carrier.name,
));
}
}
@ -368,21 +369,27 @@ fn lower_pattern4_joinir(
trace::trace().debug(
"pattern4",
&format!("host_inputs: {:?}", host_inputs.iter().map(|v| v.0).collect::<Vec<_>>())
&format!(
"host_inputs: {:?}",
host_inputs.iter().map(|v| v.0).collect::<Vec<_>>()
),
);
let mut join_inputs = vec![ValueId(0)]; // ValueId(0) = i_init in JoinIR
let mut join_inputs = vec![ValueId(0)]; // ValueId(0) = i_init in JoinIR
for idx in 0..prepared.carrier_info.carriers.len() {
join_inputs.push(ValueId((idx + 1) as u32)); // ValueId(1..N) = carrier inits
join_inputs.push(ValueId((idx + 1) as u32)); // ValueId(1..N) = carrier inits
}
trace::trace().debug(
"pattern4",
&format!("join_inputs: {:?}", join_inputs.iter().map(|v| v.0).collect::<Vec<_>>())
&format!(
"join_inputs: {:?}",
join_inputs.iter().map(|v| v.0).collect::<Vec<_>>()
),
);
let boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(join_inputs, host_inputs) // Dynamic carrier count
.with_inputs(join_inputs, host_inputs) // Dynamic carrier count
.with_exit_bindings(exit_bindings)
.with_loop_var_name(Some(prepared.loop_var_name.clone()))
.with_carrier_info(prepared.carrier_info.clone())
@ -397,7 +404,10 @@ fn lower_pattern4_joinir(
)?;
let void_val = crate::mir::builder::emission::constant::emit_void(builder);
trace::trace().debug("pattern4", &format!("Loop complete, returning Void {:?}", void_val));
trace::trace().debug(
"pattern4",
&format!("Loop complete, returning Void {:?}", void_val),
);
Ok(Some(void_val))
}

View File

@ -34,14 +34,14 @@
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::condition_env::ConditionBinding;
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr;
use crate::mir::join_ir::lowering::loop_update_summary::LoopUpdateSummary; // Phase 213
use crate::mir::join_ir::lowering::loop_update_summary::LoopUpdateSummary; // Phase 213
use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;
use crate::mir::ValueId;
use crate::mir::BasicBlockId;
use crate::mir::ValueId;
use std::collections::{BTreeMap, BTreeSet}; // Phase 222.5-D: HashMap → BTreeMap for determinism
use super::common_init::CommonPatternInitializer;
@ -72,9 +72,8 @@ use super::loop_scope_shape_builder::LoopScopeShapeBuilder;
/// let join_module = lower_simple_while_minimal(ctx.loop_scope)?;
/// ```
#[derive(Debug, Clone)]
pub struct PatternPipelineContext {
pub(crate) struct PatternPipelineContext {
// === Common Data (All Patterns) ===
/// Loop variable name (e.g., "i")
pub loop_var_name: String,
@ -88,7 +87,6 @@ pub struct PatternPipelineContext {
pub loop_scope: LoopScopeShape,
// === Pattern 2/4: Break/Continue Condition ===
/// Condition environment (variable → JoinIR ValueId mapping)
/// Used by Pattern 2 (break condition) and Pattern 4 (continue condition)
#[allow(dead_code)]
@ -105,21 +103,18 @@ pub struct PatternPipelineContext {
pub carrier_updates: Option<BTreeMap<String, UpdateExpr>>, // Phase 222.5-D: HashMap → BTreeMap for determinism
// === Pattern 2/4: Trim Pattern Support ===
/// Trim loop helper (if Trim pattern detected during promotion)
/// Used by Pattern 2 (string trim) - Pattern 4 support TBD
#[allow(dead_code)]
pub trim_helper: Option<TrimLoopHelper>,
// === Pattern 2: Break Condition ===
/// Effective break condition (may be modified for Trim pattern)
/// Used only by Pattern 2
#[allow(dead_code)]
pub break_condition: Option<ASTNode>,
// === Pattern 3: If-Sum Generalization (Phase 213) ===
/// Loop condition AST node
/// Used by Pattern 3 for dynamic loop condition lowering
#[allow(dead_code)]
@ -138,7 +133,7 @@ pub struct PatternPipelineContext {
/// Pattern variant selector
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatternVariant {
pub(crate) enum PatternVariant {
/// Pattern 1: Simple while loop (no break, no continue, no if-else PHI)
Pattern1,
/// Pattern 2: Loop with break statement
@ -193,7 +188,7 @@ impl PatternPipelineContext {
// Complex conditions (e.g., i % 2 == 1) → fallback to legacy mode
if let Some(ASTNode::If { condition, .. }) = if_stmt {
use crate::mir::join_ir::lowering::condition_pattern::{
analyze_condition_pattern, normalize_comparison, ConditionPattern
analyze_condition_pattern, normalize_comparison, ConditionPattern,
};
// (a) Pattern check: must be SimpleComparison
@ -213,7 +208,10 @@ impl PatternPipelineContext {
// Phase 219: Use assignment-based carrier detection
// (1 counter like "i" + 1-2 accumulators like "sum", "count")
use crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates_from_ast;
let carrier_names: Vec<String> = self.carrier_info.carriers.iter()
let carrier_names: Vec<String> = self
.carrier_info
.carriers
.iter()
.map(|c| c.name.clone())
.collect();
@ -232,9 +230,9 @@ impl PatternPipelineContext {
///
/// Returns the first if statement found in loop_body, if any.
pub fn extract_if_statement(&self) -> Option<&ASTNode> {
self.loop_body.as_ref().and_then(|body| {
body.iter().find(|stmt| matches!(stmt, ASTNode::If { .. }))
})
self.loop_body
.as_ref()
.and_then(|body| body.iter().find(|stmt| matches!(stmt, ASTNode::If { .. })))
}
}
@ -263,20 +261,19 @@ impl PatternPipelineContext {
/// - Loop variable not found in variable_map
/// - Condition variable not found (Pattern 2/4)
/// - Trim pattern promotion fails (Pattern 2/4)
pub fn build_pattern_context(
pub(crate) fn build_pattern_context(
builder: &mut MirBuilder,
condition: &ASTNode,
body: &[ASTNode],
variant: PatternVariant,
) -> Result<PatternPipelineContext, String> {
// Step 1: Common initialization (all patterns)
let (loop_var_name, loop_var_id, carrier_info) =
CommonPatternInitializer::initialize_pattern(
builder,
condition,
&builder.variable_map,
None, // No exclusions for now (Pattern 2/4 will filter carriers later)
)?;
let (loop_var_name, loop_var_id, carrier_info) = CommonPatternInitializer::initialize_pattern(
builder,
condition,
&builder.variable_map,
None, // No exclusions for now (Pattern 2/4 will filter carriers later)
)?;
// Step 2: Build LoopScopeShape
let loop_scope = match variant {
@ -304,39 +301,46 @@ pub fn build_pattern_context(
};
// Step 3: Pattern-specific preprocessing
let (condition_env, condition_bindings, carrier_updates, trim_helper, break_condition,
loop_condition, loop_body, loop_update_summary) =
match variant {
PatternVariant::Pattern1 => {
// Pattern 1: No additional preprocessing needed
(None, None, None, None, None, None, None, None)
}
PatternVariant::Pattern3 => {
// Pattern 3: Phase 213 - Store loop condition and body for AST-based lowering
(
None, // No condition_env
None, // No condition_bindings
None, // No carrier_updates (old style)
None, // No trim_helper
None, // No break_condition
Some(condition.clone()), // loop_condition (Phase 213)
Some(body.to_vec()), // loop_body (Phase 213)
None, // loop_update_summary (TODO: Phase 213-2-3)
)
}
PatternVariant::Pattern2 | PatternVariant::Pattern4 => {
// Pattern 2/4: Full preprocessing will be handled by existing code
// For now, return empty values (will be populated by pattern-specific logic)
//
// Note: Pattern 2/4 have complex preprocessing that includes:
// - Break/continue condition analysis
// - Carrier update analysis
// - Trim pattern promotion
// These will remain in pattern2/pattern4.rs for now and will be
// gradually migrated into this pipeline in future phases.
(None, None, None, None, None, None, None, None)
}
};
let (
condition_env,
condition_bindings,
carrier_updates,
trim_helper,
break_condition,
loop_condition,
loop_body,
loop_update_summary,
) = match variant {
PatternVariant::Pattern1 => {
// Pattern 1: No additional preprocessing needed
(None, None, None, None, None, None, None, None)
}
PatternVariant::Pattern3 => {
// Pattern 3: Phase 213 - Store loop condition and body for AST-based lowering
(
None, // No condition_env
None, // No condition_bindings
None, // No carrier_updates (old style)
None, // No trim_helper
None, // No break_condition
Some(condition.clone()), // loop_condition (Phase 213)
Some(body.to_vec()), // loop_body (Phase 213)
None, // loop_update_summary (TODO: Phase 213-2-3)
)
}
PatternVariant::Pattern2 | PatternVariant::Pattern4 => {
// Pattern 2/4: Full preprocessing will be handled by existing code
// For now, return empty values (will be populated by pattern-specific logic)
//
// Note: Pattern 2/4 have complex preprocessing that includes:
// - Break/continue condition analysis
// - Carrier update analysis
// - Trim pattern promotion
// These will remain in pattern2/pattern4.rs for now and will be
// gradually migrated into this pipeline in future phases.
(None, None, None, None, None, None, None, None)
}
};
Ok(PatternPipelineContext {
loop_var_name,
@ -348,9 +352,9 @@ pub fn build_pattern_context(
carrier_updates,
trim_helper,
break_condition,
loop_condition, // Phase 213
loop_body, // Phase 213
loop_update_summary, // Phase 213
loop_condition, // Phase 213
loop_body, // Phase 213
loop_update_summary, // Phase 213
})
}
@ -398,14 +402,14 @@ mod tests {
host_id: ValueId(10),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
CarrierVar {
name: "count".to_string(),
host_id: ValueId(11),
join_id: None,
role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState,
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
init: crate::mir::join_ir::lowering::carrier_info::CarrierInit::FromHost, // Phase 228
},
],
trim_helper: None,
@ -423,9 +427,9 @@ mod tests {
carrier_updates: None,
trim_helper: None,
break_condition: None,
loop_condition: None, // Phase 213
loop_body: None, // Phase 213
loop_update_summary: None, // Phase 213
loop_condition: None, // Phase 213
loop_body: None, // Phase 213
loop_update_summary: None, // Phase 213
};
assert_eq!(ctx.carrier_count(), 2);
@ -466,9 +470,9 @@ mod tests {
whitespace_chars: vec![" ".to_string(), "\t".to_string()],
}),
break_condition: None,
loop_condition: None, // Phase 213
loop_body: None, // Phase 213
loop_update_summary: None, // Phase 213
loop_condition: None, // Phase 213
loop_body: None, // Phase 213
loop_update_summary: None, // Phase 213
};
assert!(ctx.is_trim_pattern());

View File

@ -30,7 +30,7 @@ use crate::mir::loop_pattern_detection::{LoopFeatures, LoopPatternKind};
use super::ast_feature_extractor as ast_features;
/// Context passed to pattern detect/lower functions
pub struct LoopPatternContext<'a> {
pub(crate) struct LoopPatternContext<'a> {
/// Loop condition AST node
pub condition: &'a ASTNode,
@ -69,7 +69,7 @@ impl<'a> LoopPatternContext<'a> {
/// Phase 194+: Automatically detects continue/break statements in body
/// Phase 192: Extract features and classify pattern from AST
/// Phase 193: Feature extraction delegated to ast_feature_extractor module
pub fn new(
pub(crate) fn new(
condition: &'a ASTNode,
body: &'a [ASTNode],
func_name: &'a str,
@ -99,7 +99,7 @@ impl<'a> LoopPatternContext<'a> {
}
/// Phase 200-C: Create context with fn_body for capture analysis
pub fn with_fn_body(
pub(crate) fn with_fn_body(
condition: &'a ASTNode,
body: &'a [ASTNode],
func_name: &'a str,
@ -117,19 +117,19 @@ impl<'a> LoopPatternContext<'a> {
/// Entry in the loop pattern router table.
/// Each pattern registers a detect function and a lower function.
pub struct LoopPatternEntry {
pub(crate) struct LoopPatternEntry {
/// Human-readable pattern name for debugging
pub name: &'static str,
pub(crate) name: &'static str,
/// Priority (lower = tried first). Pattern1=10, Pattern2=20, Pattern3=30
#[allow(dead_code)]
pub priority: u8,
pub(crate) priority: u8,
/// Detection function: returns true if this pattern matches
pub detect: fn(&MirBuilder, &LoopPatternContext) -> bool,
pub(crate) detect: fn(&MirBuilder, &LoopPatternContext) -> bool,
/// Lowering function: performs the actual JoinIR generation
pub lower: fn(&mut MirBuilder, &LoopPatternContext) -> Result<Option<ValueId>, String>,
pub(crate) lower: fn(&mut MirBuilder, &LoopPatternContext) -> Result<Option<ValueId>, String>,
}
/// Static table of all registered loop patterns.
@ -156,16 +156,16 @@ pub struct LoopPatternEntry {
/// - Structure: has_break && !has_continue
///
/// Note: func_name is now only used for debug logging, not pattern detection
pub static LOOP_PATTERNS: &[LoopPatternEntry] = &[
pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
LoopPatternEntry {
name: "Pattern4_WithContinue",
priority: 5, // Highest priority - continue is most specific
priority: 5, // Highest priority - continue is most specific
detect: super::pattern4_with_continue::can_lower,
lower: super::pattern4_with_continue::lower,
},
LoopPatternEntry {
name: "Pattern3_WithIfPhi",
priority: 30, // NOTE: Pattern 3 must be checked BEFORE Pattern 1 (both use "main")
priority: 30, // NOTE: Pattern 3 must be checked BEFORE Pattern 1 (both use "main")
detect: super::pattern3_with_if_phi::can_lower,
lower: super::pattern3_with_if_phi::lower,
},
@ -195,7 +195,7 @@ pub 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
pub fn route_loop_pattern(
pub(crate) fn route_loop_pattern(
builder: &mut MirBuilder,
ctx: &LoopPatternContext,
) -> Result<Option<ValueId>, String> {
@ -217,7 +217,13 @@ pub fn route_loop_pattern(
// No pattern matched - return None (caller will handle error)
// Phase 187-2: Legacy LoopBuilder removed, all loops must use JoinIR
if ctx.debug {
trace::trace().debug("route", &format!("No pattern matched for function '{}' (pattern_kind={:?})", ctx.func_name, ctx.pattern_kind));
trace::trace().debug(
"route",
&format!(
"No pattern matched for function '{}' (pattern_kind={:?})",
ctx.func_name, ctx.pattern_kind
),
);
}
Ok(None)
}

View File

@ -50,7 +50,7 @@ use crate::mir::ValueId;
/// Trim pattern lowering orchestrator
///
/// Phase 180: Single entry point for all Trim/P5 lowering operations.
pub struct TrimLoopLowerer;
pub(crate) struct TrimLoopLowerer;
/// Result of successful Trim lowering preprocessing
///
@ -59,7 +59,7 @@ pub struct TrimLoopLowerer;
/// - Updated carrier info with promoted carrier
/// - Condition environment bindings
/// - Trim helper for pattern-specific operations
pub struct TrimLoweringResult {
pub(crate) struct TrimLoweringResult {
/// Replaced break condition (e.g., `!is_carrier`)
///
/// Pattern2/4 will use this instead of the original break condition
@ -98,12 +98,14 @@ impl TrimLoopLowerer {
Self::is_var_used_in_condition(var_name, left)
|| Self::is_var_used_in_condition(var_name, right)
}
ASTNode::UnaryOp { operand, .. } => {
Self::is_var_used_in_condition(var_name, operand)
}
ASTNode::MethodCall { object, arguments, .. } => {
ASTNode::UnaryOp { operand, .. } => Self::is_var_used_in_condition(var_name, operand),
ASTNode::MethodCall {
object, arguments, ..
} => {
Self::is_var_used_in_condition(var_name, object)
|| arguments.iter().any(|arg| Self::is_var_used_in_condition(var_name, arg))
|| arguments
.iter()
.any(|arg| Self::is_var_used_in_condition(var_name, arg))
}
// Add other node types as needed
_ => false,
@ -173,11 +175,8 @@ impl TrimLoopLowerer {
// TODO: Phase 180-3 will implement full logic from Pattern2
// Step 1: Check if condition references LoopBodyLocal variables
let cond_scope = LoopConditionScopeBox::analyze(
loop_var_name,
&[loop_cond, break_cond],
Some(scope),
);
let cond_scope =
LoopConditionScopeBox::analyze(loop_var_name, &[loop_cond, break_cond], Some(scope));
eprintln!(
"[TrimLoopLowerer] Analyzing condition scope: {} variables",
@ -194,7 +193,9 @@ impl TrimLoopLowerer {
// Phase 183-2: Filter to only condition LoopBodyLocal (skip body-only)
use crate::mir::loop_pattern_detection::loop_condition_scope::CondVarScope;
let condition_body_locals: Vec<_> = cond_scope.vars.iter()
let condition_body_locals: Vec<_> = cond_scope
.vars
.iter()
.filter(|v| v.scope == CondVarScope::LoopBodyLocal)
.filter(|v| {
// Check if variable is actually used in break condition
@ -213,7 +214,10 @@ impl TrimLoopLowerer {
eprintln!(
"[TrimLoopLowerer] Phase 183: Found {} condition LoopBodyLocal variables: {:?}",
condition_body_locals.len(),
condition_body_locals.iter().map(|v| &v.name).collect::<Vec<_>>()
condition_body_locals
.iter()
.map(|v| &v.name)
.collect::<Vec<_>>()
);
// Step 2: Try promotion via LoopBodyCarrierPromoter
@ -242,14 +246,12 @@ impl TrimLoopLowerer {
);
// Step 4: Safety check via TrimLoopHelper
let trim_helper = carrier_info
.trim_helper()
.ok_or_else(|| {
format!(
"[TrimLoopLowerer] Promoted but no TrimLoopHelper attached (carrier: '{}')",
trim_info.carrier_name
)
})?;
let trim_helper = carrier_info.trim_helper().ok_or_else(|| {
format!(
"[TrimLoopLowerer] Promoted but no TrimLoopHelper attached (carrier: '{}')",
trim_info.carrier_name
)
})?;
if !trim_helper.is_safe_trim() {
return Err(format!(
@ -262,15 +264,13 @@ impl TrimLoopLowerer {
eprintln!("[TrimLoopLowerer] Safe Trim pattern detected, implementing lowering");
eprintln!(
"[TrimLoopLowerer] Carrier: '{}', original var: '{}', whitespace chars: {:?}",
trim_helper.carrier_name, trim_helper.original_var, trim_helper.whitespace_chars
trim_helper.carrier_name,
trim_helper.original_var,
trim_helper.whitespace_chars
);
// Step 5: Generate carrier initialization code
Self::generate_carrier_initialization(
builder,
body,
trim_helper,
)?;
Self::generate_carrier_initialization(builder, body, trim_helper)?;
eprintln!(
"[TrimLoopLowerer] Registered carrier '{}' in variable_map",
@ -286,11 +286,8 @@ impl TrimLoopLowerer {
);
// Step 7: Setup ConditionEnv bindings
let condition_bindings = Self::setup_condition_env_bindings(
builder,
trim_helper,
alloc_join_value,
)?;
let condition_bindings =
Self::setup_condition_env_bindings(builder, trim_helper, alloc_join_value)?;
eprintln!(
"[TrimLoopLowerer] Added {} condition bindings",
@ -333,13 +330,14 @@ impl TrimLoopLowerer {
use crate::mir::builder::control_flow::joinir::patterns::trim_pattern_validator::TrimPatternValidator;
// Extract substring pattern from body
let (s_name, start_expr) = TrimPatternValidator::extract_substring_args(body, &trim_helper.original_var)
.ok_or_else(|| {
format!(
let (s_name, start_expr) =
TrimPatternValidator::extract_substring_args(body, &trim_helper.original_var)
.ok_or_else(|| {
format!(
"[TrimLoopLowerer] Failed to extract substring pattern for Trim carrier '{}'",
trim_helper.carrier_name
)
})?;
})?;
eprintln!(
"[TrimLoopLowerer] Extracted substring pattern: s='{}', start={:?}",
@ -347,11 +345,10 @@ impl TrimLoopLowerer {
);
// Get ValueIds for string and start
let s_id = builder
.variable_map
.get(&s_name)
.copied()
.ok_or_else(|| format!("[TrimLoopLowerer] String variable '{}' not found", s_name))?;
let s_id =
builder.variable_map.get(&s_name).copied().ok_or_else(|| {
format!("[TrimLoopLowerer] String variable '{}' not found", s_name)
})?;
// Compile start expression to get ValueId
let start_id = builder.build_expression_impl(*start_expr)?;
@ -378,11 +375,17 @@ impl TrimLoopLowerer {
vec![start_id, start_plus_1],
)?;
eprintln!("[TrimLoopLowerer] Generated initial substring call: ch0 = {:?}", ch0);
eprintln!(
"[TrimLoopLowerer] Generated initial substring call: ch0 = {:?}",
ch0
);
// Generate: is_ch_match0 = (ch0 == " " || ch0 == "\t" || ...)
let is_ch_match0 =
TrimPatternValidator::emit_whitespace_check(builder, ch0, &trim_helper.whitespace_chars)?;
let is_ch_match0 = TrimPatternValidator::emit_whitespace_check(
builder,
ch0,
&trim_helper.whitespace_chars,
)?;
eprintln!(
"[TrimLoopLowerer] Generated initial whitespace check: is_ch_match0 = {:?}",
@ -486,7 +489,9 @@ mod tests {
};
assert!(TrimLoopLowerer::is_var_used_in_condition("ch", &var_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition("other", &var_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition(
"other", &var_node
));
}
#[test]
@ -506,7 +511,9 @@ mod tests {
};
assert!(TrimLoopLowerer::is_var_used_in_condition("ch", &cond_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition("other", &cond_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition(
"other", &cond_node
));
}
#[test]
@ -525,8 +532,13 @@ mod tests {
span: Span::unknown(),
};
assert!(TrimLoopLowerer::is_var_used_in_condition("digit_pos", &cond_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition("other", &cond_node));
assert!(TrimLoopLowerer::is_var_used_in_condition(
"digit_pos",
&cond_node
));
assert!(!TrimLoopLowerer::is_var_used_in_condition(
"other", &cond_node
));
}
#[test]
@ -566,6 +578,8 @@ mod tests {
};
assert!(TrimLoopLowerer::is_var_used_in_condition("ch", &or_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition("other", &or_node));
assert!(!TrimLoopLowerer::is_var_used_in_condition(
"other", &or_node
));
}
}

View File

@ -53,6 +53,8 @@ fn execute_function(
mut current_func: JoinFuncId,
mut current_args: Vec<JoinValue>,
) -> Result<JoinValue, JoinRuntimeError> {
let verbose = crate::config::env::joinir_dev_enabled();
'exec: loop {
let func = module.functions.get(&current_func).ok_or_else(|| {
JoinRuntimeError::new(format!("Function {:?} not found", current_func))
@ -137,10 +139,12 @@ fn execute_function(
} => {
// 1. Evaluate cond (Bool or Int)
let cond_value = read_var(&locals, *cond)?;
eprintln!(
"[SELECT DEBUG] cond={:?}, cond_value={:?}",
cond, cond_value
);
if verbose {
eprintln!(
"[joinir/runner/select] cond={:?}, cond_value={:?}",
cond, cond_value
);
}
let cond_bool = match cond_value {
JoinValue::Bool(b) => b,
JoinValue::Int(i) => i != 0, // Int も許す0=false, それ以外=true
@ -155,17 +159,21 @@ fn execute_function(
// 2. Select then_val or else_val
let then_value = read_var(&locals, *then_val)?;
let else_value = read_var(&locals, *else_val)?;
eprintln!(
"[SELECT DEBUG] cond_bool={}, then_val={:?}={:?}, else_val={:?}={:?}",
cond_bool, then_val, then_value, else_val, else_value
);
if verbose {
eprintln!(
"[joinir/runner/select] cond_bool={}, then_val={:?}={:?}, else_val={:?}={:?}",
cond_bool, then_val, then_value, else_val, else_value
);
}
let selected_id = if cond_bool { *then_val } else { *else_val };
let selected_value = read_var(&locals, selected_id)?;
eprintln!(
"[SELECT DEBUG] selected_id={:?}, selected_value={:?}",
selected_id, selected_value
);
if verbose {
eprintln!(
"[joinir/runner/select] selected_id={:?}, selected_value={:?}",
selected_id, selected_value
);
}
// 3. Write to dst
locals.insert(*dst, selected_value);