diff --git a/src/mir/builder/calls/lowering.rs b/src/mir/builder/calls/lowering.rs index 84cc7f5a..a73faad3 100644 --- a/src/mir/builder/calls/lowering.rs +++ b/src/mir/builder/calls/lowering.rs @@ -215,22 +215,19 @@ impl MirBuilder { match shadow_result { Ok(Some((module, _meta))) => { // Phase 122: Verify Normalized JoinModule structure - let expected_env_fields = tree.contract.writes.len(); - let verify_result = parity::verify_normalized_structure(&module, expected_env_fields); - - if !verify_result.ok { - let msg = format!( - "phase122/emit: structure verification failed for {}: {}", - func_name, - verify_result.hint.unwrap_or_else(|| "".to_string()) + let expected_env_fields = + StepTreeNormalizedShadowLowererBox::expected_env_field_count( + &tree, + &available_inputs, ); + + if let Err(err) = + parity::verify_normalized_structure(&module, expected_env_fields) + { if strict { - return Err(format!( - "Phase122 Normalized structure verification failed (strict mode): {}", - msg - )); + return Err(err); } - trace.dev("phase122/emit/error", &msg); + trace.dev("phase122/emit/error", &err); } else { // Shadow lowering succeeded + structure verified let status = format!( diff --git a/src/mir/control_tree/normalized_shadow/builder.rs b/src/mir/control_tree/normalized_shadow/builder.rs index 716ea5b9..5abc8301 100644 --- a/src/mir/control_tree/normalized_shadow/builder.rs +++ b/src/mir/control_tree/normalized_shadow/builder.rs @@ -14,7 +14,7 @@ //! - Single responsibility: structure → JoinIR conversion use crate::mir::control_tree::step_tree::StepTree; -use crate::mir::join_ir::lowering::carrier_info::{ExitMeta, JoinFragmentMeta}; +use crate::mir::join_ir::lowering::carrier_info::JoinFragmentMeta; use crate::mir::join_ir::JoinModule; use crate::mir::ValueId; // Phase 126 use std::collections::BTreeMap; // Phase 126 @@ -56,8 +56,6 @@ impl EnvLayout { contract: &crate::mir::control_tree::step_tree_contract_box::StepTreeContract, available_inputs: &std::collections::BTreeMap, ) -> Self { - use std::collections::BTreeSet; - // Phase 125 P2: writes from contract let writes: Vec = contract.writes.iter().cloned().collect(); @@ -77,6 +75,26 @@ impl EnvLayout { pub struct StepTreeNormalizedShadowLowererBox; impl StepTreeNormalizedShadowLowererBox { + /// Phase 129-B: Expected env field count (writes + inputs) + pub fn expected_env_field_count( + step_tree: &StepTree, + available_inputs: &BTreeMap, + ) -> usize { + let env_layout = EnvLayout::from_contract(&step_tree.contract, available_inputs); + env_layout.writes.len() + env_layout.inputs.len() + } + + /// Phase 129-B: If-as-last shape detection (no post-if) + pub fn expects_join_k_as_last(step_tree: &StepTree) -> bool { + use crate::mir::control_tree::step_tree::StepNode; + + match &step_tree.root { + StepNode::If { .. } => true, + StepNode::Block(nodes) => matches!(nodes.last(), Some(StepNode::If { .. })), + _ => false, + } + } + /// Try to lower an if-only StepTree to normalized form /// /// ## Returns @@ -150,12 +168,19 @@ impl StepTreeNormalizedShadowLowererBox { step_tree: &StepTree, available_inputs: &BTreeMap, ) -> Result, String> { - use crate::mir::join_ir::{JoinFunction, JoinFuncId, JoinInst}; + use crate::mir::join_ir::{JoinFunction, JoinFuncId}; use crate::mir::ValueId; // Phase 126: EnvLayout 生成(available_inputs を使用) let env_layout = EnvLayout::from_contract(&step_tree.contract, available_inputs); + // Phase 129-B: If-as-last join_k lowering (dev-only) + if let Some((module, meta)) = + Self::lower_if_node_as_last_stmt(step_tree, &env_layout)? + { + return Ok(Some((module, meta))); + } + let main_func_id = JoinFuncId::new(0); // Phase 125 P2: writes 用の ValueId 生成 @@ -170,20 +195,14 @@ impl StepTreeNormalizedShadowLowererBox { }) .collect(); - // Phase 125 P2: inputs 用の ValueId 生成(今は空だが、P3 で available_inputs から参照) + // Phase 129-B: inputs 用の ValueId 生成(writes + inputs が env params のSSOT) let inputs_params: Vec = env_layout .inputs .iter() - .map(|name| { - // Phase 125 P3: available_inputs から取得 - // 今は空なので到達しないはずだが、念のため placeholder - available_inputs - .get(name) - .copied() - .unwrap_or_else(|| { - // Should not reach here (inputs は available_inputs にある前提) - ValueId(0) // placeholder - }) + .map(|_| { + let vid = ValueId(next_value_id); + next_value_id += 1; + vid }) .collect(); @@ -196,9 +215,9 @@ impl StepTreeNormalizedShadowLowererBox { env.insert(name.clone(), *vid); } - // Phase 125 P2: 関数パラメータは writes のみ(inputs は外側から来る前提) - // TODO P3: inputs も params に含める必要があるか検討 - let env_params = writes_params; + // Phase 129-B: 関数パラメータは writes + inputs(env params SSOT) + let mut env_params = writes_params; + env_params.extend(inputs_params); // main 関数生成 let mut main_func = JoinFunction::new( @@ -246,6 +265,379 @@ impl StepTreeNormalizedShadowLowererBox { Ok(Some((module, meta))) } + /// Phase 129-B: Lower if node as last statement using join_k (dev-only) + /// + /// Scope: "if-as-last" only (no post-if). If it doesn't match, return Ok(None). + fn lower_if_node_as_last_stmt( + step_tree: &StepTree, + env_layout: &EnvLayout, + ) -> Result, String> { + use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind}; + use crate::mir::join_ir::{ConstValue, JoinFunction, JoinFuncId, JoinInst, MirLikeInst}; + use crate::mir::join_ir::lowering::error_tags; + use crate::mir::ValueId; + use std::collections::BTreeMap; + + let (prefix_nodes, if_node) = match &step_tree.root { + StepNode::If { .. } => (&[][..], &step_tree.root), + StepNode::Block(nodes) => { + let last = nodes.last(); + if matches!(last, Some(StepNode::If { .. })) { + (&nodes[..nodes.len() - 1], nodes.last().unwrap()) + } else { + return Ok(None); + } + } + _ => return Ok(None), + }; + + let if_node = match if_node { + StepNode::If { .. } => if_node, + _ => return Ok(None), + }; + + let env_fields: Vec = env_layout + .writes + .iter() + .chain(env_layout.inputs.iter()) + .cloned() + .collect(); + + fn alloc_value_id(next_value_id: &mut u32) -> ValueId { + let vid = ValueId(*next_value_id); + *next_value_id += 1; + vid + } + + let mut next_value_id: u32 = 1; + + let alloc_env_params = + |fields: &[String], next_value_id: &mut u32| -> Vec { + fields + .iter() + .map(|_| alloc_value_id(next_value_id)) + .collect() + }; + + let build_env_map = |fields: &[String], params: &[ValueId]| -> BTreeMap { + let mut env = BTreeMap::new(); + for (name, vid) in fields.iter().zip(params.iter()) { + env.insert(name.clone(), *vid); + } + env + }; + + let collect_env_args = |fields: &[String], env: &BTreeMap| -> Result, String> { + let mut args = Vec::with_capacity(fields.len()); + for name in fields { + let vid = env.get(name).copied().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/env_missing", + &format!("env missing required field '{name}'"), + "ensure env layout and env map are built from the same SSOT field list", + ) + })?; + args.push(vid); + } + Ok(args) + }; + + // IDs (stable, dev-only) + let main_id = JoinFuncId::new(0); + let k_then_id = JoinFuncId::new(1); + let k_else_id = JoinFuncId::new(2); + let join_k_id = JoinFuncId::new(3); + + // main(env) + let main_params = alloc_env_params(&env_fields, &mut next_value_id); + let mut env_main = build_env_map(&env_fields, &main_params); + let mut main_func = JoinFunction::new(main_id, "main".to_string(), main_params); + + // Lower prefix (pre-if) statements into main + for n in prefix_nodes { + match n { + StepNode::Stmt { kind, .. } => match kind { + StepStmtKind::Assign { target, value_ast } => { + if Self::lower_assign_stmt( + target, + value_ast, + &mut main_func.body, + &mut next_value_id, + &mut env_main, + ) + .is_err() + { + return Ok(None); + } + } + StepStmtKind::LocalDecl { .. } => {} + _ => { + return Ok(None); + } + }, + _ => { + return Ok(None); + } + } + } + + // Extract return variable and branch bodies. + fn split_branch_for_as_last( + branch: &StepNode, + ) -> Result<(&[StepNode], &crate::mir::control_tree::step_tree::AstNodeHandle), String> { + use crate::mir::control_tree::step_tree::StepNode; + use crate::mir::control_tree::step_tree::StepStmtKind; + use crate::mir::join_ir::lowering::error_tags; + + match branch { + StepNode::Stmt { kind, .. } => match kind { + StepStmtKind::Return { value_ast } => { + let ast_handle = value_ast.as_ref().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/branch_return_void", + "branch return must return a variable (not void)", + "use `return x` in both then/else branches", + ) + })?; + Ok((&[][..], ast_handle)) + } + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/branch_not_return", + "branch must end with return", + "use `return x` in both then/else branches", + )), + }, + StepNode::Block(nodes) => { + let last = nodes.last().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/branch_empty", + "branch is empty", + "add `return x` as the last statement of the branch", + ) + })?; + match last { + StepNode::Stmt { kind, .. } => match kind { + StepStmtKind::Return { value_ast } => { + let ast_handle = value_ast.as_ref().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/branch_return_void", + "branch return must return a variable (not void)", + "use `return x` in both then/else branches", + ) + })?; + Ok((&nodes[..nodes.len() - 1], ast_handle)) + } + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/branch_not_return", + "branch must end with return", + "add `return x` as the last statement of the branch", + )), + }, + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/branch_last_not_stmt", + "branch last node must be a statement return", + "ensure the branch ends with `return x`", + )), + } + } + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/branch_node_unsupported", + "unsupported branch node", + "Phase 129-B supports only Block/Return branches", + )), + } + } + + fn extract_return_var_name(ast_handle: &crate::mir::control_tree::step_tree::AstNodeHandle) -> Result { + use crate::ast::ASTNode; + use crate::mir::join_ir::lowering::error_tags; + match ast_handle.0.as_ref() { + ASTNode::Variable { name, .. } => Ok(name.clone()), + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/return_expr_unsupported", + "branch return expression must be a variable", + "use `return x` (variable) in both then/else branches", + )), + } + } + + let (cond_ast, then_branch, else_branch) = match if_node { + StepNode::If { + cond_ast, + then_branch, + else_branch, + .. + } => (cond_ast, then_branch.as_ref(), else_branch.as_deref()), + _ => unreachable!(), + }; + + let else_branch = match else_branch { + Some(b) => b, + None => return Ok(None), + }; + + let (then_prefix, then_ret_ast) = match split_branch_for_as_last(then_branch) { + Ok(v) => v, + Err(_msg) => return Ok(None), + }; + let (else_prefix, else_ret_ast) = match split_branch_for_as_last(else_branch) { + Ok(v) => v, + Err(_msg) => return Ok(None), + }; + + let then_ret_var = match extract_return_var_name(then_ret_ast) { + Ok(v) => v, + Err(_msg) => return Ok(None), + }; + let else_ret_var = match extract_return_var_name(else_ret_ast) { + Ok(v) => v, + Err(_msg) => return Ok(None), + }; + + if then_ret_var != else_ret_var { + return Ok(None); + } + + let ret_var = then_ret_var; + if !env_layout.writes.iter().any(|w| w == &ret_var) { + return Ok(None); + } + + // join_k(env_phi): return env_phi[ret_var] + let join_k_params = alloc_env_params(&env_fields, &mut next_value_id); + let env_join_k = build_env_map(&env_fields, &join_k_params); + let ret_vid = env_join_k.get(&ret_var).copied().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/ret_vid_missing", + "return variable not found in join_k env", + "ensure env layout includes the return variable in writes", + ) + })?; + let mut join_k_func = JoinFunction::new(join_k_id, "join_k".to_string(), join_k_params); + join_k_func.body.push(JoinInst::Ret { value: Some(ret_vid) }); + + // k_then(env_in): ; tailcall join_k(env_out) + let then_params = alloc_env_params(&env_fields, &mut next_value_id); + let mut env_then = build_env_map(&env_fields, &then_params); + let mut then_func = JoinFunction::new(k_then_id, "k_then".to_string(), then_params); + for n in then_prefix { + match n { + StepNode::Stmt { kind, .. } => match kind { + StepStmtKind::Assign { target, value_ast } => { + if Self::lower_assign_stmt( + target, + value_ast, + &mut then_func.body, + &mut next_value_id, + &mut env_then, + ) + .is_err() + { + return Ok(None); + } + } + StepStmtKind::LocalDecl { .. } => {} + _ => { + return Ok(None); + } + }, + _ => { + return Ok(None); + } + } + } + let then_args = collect_env_args(&env_fields, &env_then)?; + then_func.body.push(JoinInst::Call { + func: join_k_id, + args: then_args, + k_next: None, + dst: None, + }); + + // k_else(env_in): ; tailcall join_k(env_out) + let else_params = alloc_env_params(&env_fields, &mut next_value_id); + let mut env_else = build_env_map(&env_fields, &else_params); + let mut else_func = JoinFunction::new(k_else_id, "k_else".to_string(), else_params); + for n in else_prefix { + match n { + StepNode::Stmt { kind, .. } => match kind { + StepStmtKind::Assign { target, value_ast } => { + if Self::lower_assign_stmt( + target, + value_ast, + &mut else_func.body, + &mut next_value_id, + &mut env_else, + ) + .is_err() + { + return Ok(None); + } + } + StepStmtKind::LocalDecl { .. } => {} + _ => { + return Ok(None); + } + }, + _ => { + return Ok(None); + } + } + } + let else_args = collect_env_args(&env_fields, &env_else)?; + else_func.body.push(JoinInst::Call { + func: join_k_id, + args: else_args, + k_next: None, + dst: None, + }); + + // main: cond compare + conditional jump to k_then, else to k_else + let (lhs_var, op, rhs_literal) = Self::parse_minimal_compare(&cond_ast.0)?; + let lhs_vid = env_main.get(&lhs_var).copied().ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/cond_lhs_missing", + &format!("condition lhs var '{lhs_var}' not found in env"), + "ensure the if condition uses a variable from writes or captured inputs", + ) + })?; + let rhs_vid = alloc_value_id(&mut next_value_id); + main_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: rhs_vid, + value: ConstValue::Integer(rhs_literal), + })); + let cond_vid = alloc_value_id(&mut next_value_id); + main_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cond_vid, + op, + lhs: lhs_vid, + rhs: rhs_vid, + })); + + let main_args = collect_env_args(&env_fields, &env_main)?; + main_func.body.push(JoinInst::Jump { + cont: k_then_id.as_cont(), + args: main_args.clone(), + cond: Some(cond_vid), + }); + main_func.body.push(JoinInst::Jump { + cont: k_else_id.as_cont(), + args: main_args, + cond: None, + }); + + // Build module + let mut module = JoinModule::new(); + module.add_function(main_func); + module.add_function(then_func); + module.add_function(else_func); + module.add_function(join_k_func); + module.entry = Some(main_id); + module.mark_normalized(); + + Ok(Some((module, JoinFragmentMeta::empty()))) + } + /// Phase 123-128 P1-P3: Lower node from StepTree /// /// ## Support (Phase 123-128) @@ -263,10 +655,8 @@ impl StepTreeNormalizedShadowLowererBox { env: &mut std::collections::BTreeMap, contract: &crate::mir::control_tree::step_tree_contract_box::StepTreeContract, ) -> Result<(), String> { - use crate::ast::{ASTNode, LiteralValue}; use crate::mir::control_tree::step_tree::{StepNode, StepStmtKind}; use crate::mir::join_ir::JoinInst; - use crate::mir::ValueId; match node { StepNode::Block(nodes) => { @@ -402,9 +792,8 @@ impl StepTreeNormalizedShadowLowererBox { env: &mut std::collections::BTreeMap, contract: &crate::mir::control_tree::step_tree_contract_box::StepTreeContract, ) -> Result<(), String> { - use crate::ast::{ASTNode, BinaryOperator}; use crate::mir::control_tree::step_tree::StepNode; - use crate::mir::join_ir::{CompareOp, ConstValue, JoinInst, MirLikeInst}; + use crate::mir::join_ir::{ConstValue, JoinInst, MirLikeInst}; use crate::mir::ValueId; if let StepNode::If { @@ -995,7 +1384,6 @@ mod tests { // Phase 124 P3: Test Return(Variable) when variable is in env (writes) use crate::ast::{ASTNode, Span}; use crate::mir::control_tree::step_tree::AstNodeHandle; - use std::collections::BTreeSet; // Create StepTree with "local x; return x" let mut tree = make_if_only_tree(); diff --git a/src/mir/control_tree/normalized_shadow/parity.rs b/src/mir/control_tree/normalized_shadow/parity.rs index 20d45c2d..d1a1e212 100644 --- a/src/mir/control_tree/normalized_shadow/parity.rs +++ b/src/mir/control_tree/normalized_shadow/parity.rs @@ -13,8 +13,6 @@ //! - Focus on "did we extract the same information?" use crate::mir::control_tree::step_tree_contract_box::StepTreeContract; -use std::collections::BTreeSet; - /// Mismatch classification #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MismatchKind { @@ -143,54 +141,164 @@ pub fn check_full_parity( /// /// - Checks function count, continuation count, tail-call form, env args /// - Does NOT check actual execution (RC comparison is optional) -/// - Returns mismatch with hint if structure is invalid +/// - Returns Err(freeze_with_hint) if structure is invalid pub fn verify_normalized_structure( module: &crate::mir::join_ir::JoinModule, expected_env_fields: usize, -) -> ShadowParityResult { - use crate::mir::join_ir::JoinIrPhase; +) -> Result<(), String> { + use crate::mir::join_ir::{JoinFuncId, JoinInst}; + use crate::mir::join_ir::lowering::error_tags; // Check phase if !module.is_normalized() { - let hint = format!( - "module phase is not Normalized: {:?}", - module.phase - ); - return ShadowParityResult::mismatch(MismatchKind::UnsupportedKind, hint); + return Err(error_tags::freeze_with_hint( + "phase129/join_k/not_normalized", + &format!("module phase is not Normalized: {:?}", module.phase), + "ensure the shadow lowering marks module as Normalized", + )); } - // Check function count (Phase 122 minimal: 1 main function) if module.functions.is_empty() { - let hint = "no functions in module".to_string(); - return ShadowParityResult::mismatch(MismatchKind::UnsupportedKind, hint); + return Err(error_tags::freeze_with_hint( + "phase129/join_k/no_functions", + "no functions in module", + "ensure the shadow lowering emits at least the entry function", + )); } // Check entry point - if module.entry.is_none() { - let hint = "no entry point in module".to_string(); - return ShadowParityResult::mismatch(MismatchKind::UnsupportedKind, hint); + let entry_id = module.entry.ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/no_entry", + "no entry point in module", + "ensure the shadow lowering sets JoinModule.entry", + ) + })?; + + // Check entry function exists + let entry_func = module.functions.get(&entry_id).ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/entry_missing", + &format!("entry function {:?} not found", entry_id), + "ensure the emitted module includes the entry function id", + ) + })?; + + // Env layout: writes + inputs (SSOT) + if entry_func.params.len() != expected_env_fields { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/env_arity_mismatch", + &format!( + "env args mismatch: expected {}, got {}", + expected_env_fields, + entry_func.params.len() + ), + "ensure env params are built from (writes + inputs) SSOT", + )); } - // Check main function exists - let entry_id = module.entry.unwrap(); - let main_func = module.functions.get(&entry_id); - if main_func.is_none() { - let hint = format!("entry function {:?} not found", entry_id); - return ShadowParityResult::mismatch(MismatchKind::UnsupportedKind, hint); + // All functions in this shadow module must share the same env param arity. + for (fid, func) in &module.functions { + if func.params.len() != expected_env_fields { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/env_arity_mismatch", + &format!( + "env args mismatch in {:?}: expected {}, got {}", + fid, + expected_env_fields, + func.params.len() + ), + "ensure all continuations share the same env layout (writes + inputs)", + )); + } } - // Check env args count (Phase 122: writes only) - let main_func = main_func.unwrap(); - if main_func.params.len() != expected_env_fields { - let hint = format!( - "env args mismatch: expected {}, got {}", - expected_env_fields, - main_func.params.len() - ); - return ShadowParityResult::mismatch(MismatchKind::UnsupportedKind, hint); + // PHI prohibition (Phase 129-B scope): no IfMerge/NestedIfMerge in shadow output. + for (fid, func) in &module.functions { + for inst in &func.body { + if matches!(inst, JoinInst::IfMerge { .. } | JoinInst::NestedIfMerge { .. }) { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/phi_forbidden", + &format!("PHI-like merge instruction found in {:?}", fid), + "Phase 129-B join_k path forbids IfMerge/NestedIfMerge; use join_k tailcall merge instead", + )); + } + } } - ShadowParityResult::ok() + // Detect join_k tailcall form (if present) and validate it. + fn tailcall_target(func: &crate::mir::join_ir::JoinFunction) -> Option<(JoinFuncId, usize)> { + match func.body.last()? { + JoinInst::Call { + func, + args, + k_next: None, + dst: None, + } => Some((*func, args.len())), + _ => None, + } + } + + let mut tailcall_targets: Vec<(JoinFuncId, usize)> = Vec::new(); + for func in module.functions.values() { + if let Some((target, argc)) = tailcall_target(func) { + tailcall_targets.push((target, argc)); + } + } + + if tailcall_targets.is_empty() { + return Ok(()); + } + + // join_k merge should have at least two branch continuations tailcalling the same target. + if tailcall_targets.len() < 2 { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/tailcall_count", + &format!( + "join_k tailcall form requires >=2 tailcalls, got {}", + tailcall_targets.len() + ), + "ensure both then/else branches tailcall join_k as the last instruction", + )); + } + + let first_target = tailcall_targets[0].0; + for (target, argc) in &tailcall_targets { + if *target != first_target { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/tailcall_target_mismatch", + "tailcalls do not target a single join_k function", + "ensure then/else both tailcall the same join_k function id", + )); + } + if *argc != expected_env_fields { + return Err(error_tags::freeze_with_hint( + "phase129/join_k/tailcall_arg_arity_mismatch", + &format!( + "tailcall env arg count mismatch: expected {}, got {}", + expected_env_fields, argc + ), + "ensure join_k is called with the full env fields list (writes + inputs)", + )); + } + } + + let join_k_func = module.functions.get(&first_target).ok_or_else(|| { + error_tags::freeze_with_hint( + "phase129/join_k/join_k_missing", + "tailcall target join_k function not found in module", + "ensure join_k is registered in JoinModule.functions", + ) + })?; + + match join_k_func.body.last() { + Some(JoinInst::Ret { value: Some(_) }) => Ok(()), + _ => Err(error_tags::freeze_with_hint( + "phase129/join_k/join_k_not_ret", + "join_k must end with Ret(Some(value))", + "ensure join_k returns the merged env variable and has no post-if continuation in Phase 129-B", + )), + } } #[cfg(test)]