fix(joinir): stabilize Phase 256 merge (jump_args, DCE, func names)

This commit is contained in:
2025-12-20 11:01:48 +09:00
parent 2c4268b691
commit 1028bd419c
11 changed files with 499 additions and 50 deletions

View File

@ -188,20 +188,15 @@ impl ExitArgsCollectorBox {
Ok(0) // Best effort: try direct mapping
}
} else {
// Too long - unexpected extra args
let msg = format!(
"[joinir/exit-line] jump_args length mismatch: expected {} or {} (exit_bindings carriers ±1) but got {} in block {:?}",
exit_phi_bindings_len,
exit_phi_bindings_len + 1,
jump_args_len,
block_id
// Too long - extra args beyond carriers (e.g., invariants in Pattern 7)
// Phase 256 P1.8: Allow excess args as long as we have enough for carriers
// Direct mapping: jump_args[0..N] = exit_phi_bindings[0..N], rest ignored
#[cfg(debug_assertions)]
eprintln!(
"[joinir/exit-line] jump_args has {} extra args (block {:?}), ignoring invariants",
jump_args_len - exit_phi_bindings_len, block_id
);
if strict_exit {
Err(msg)
} else {
eprintln!("[DEBUG-177] {}", msg);
Ok(0) // Best effort: try direct mapping
}
Ok(0) // Direct mapping: first N args are carriers
}
}

View File

@ -1162,15 +1162,44 @@ pub(super) fn merge_and_rewrite(
// Choosing `.iter().next()` can therefore pick the wrong function and skip
// host→JoinIR Copy injection. Instead, pick the function whose params match
// the boundary.join_inputs (the entry env params).
let (entry_func_name, entry_func) = mir_module
.functions
.iter()
.find(|(_, func)| func.params == boundary.join_inputs)
.or_else(|| mir_module.functions.iter().next())
.ok_or("JoinIR module has no functions")?;
//
// Phase 256 P1.10.1: Prefer "main" if its params match the boundary join_inputs.
let (entry_func_name, entry_func) = {
use crate::mir::join_ir::lowering::canonical_names as cn;
if let Some(main) = mir_module.functions.get(cn::MAIN) {
if main.params == boundary.join_inputs {
(cn::MAIN, main)
} else {
mir_module
.functions
.iter()
.find(|(_, func)| func.params == boundary.join_inputs)
.or_else(|| mir_module.functions.iter().next())
.map(|(name, func)| (name.as_str(), func))
.ok_or("JoinIR module has no functions")?
}
} else {
mir_module
.functions
.iter()
.find(|(_, func)| func.params == boundary.join_inputs)
.or_else(|| mir_module.functions.iter().next())
.map(|(name, func)| (name.as_str(), func))
.ok_or("JoinIR module has no functions")?
}
};
let entry_block_remapped = remapper
.get_block(entry_func_name, entry_func.entry_block)
.ok_or_else(|| format!("Entry block not found for {}", entry_func_name))?;
log!(
true,
"[cf_loop/joinir] Phase 256 P1.10.1: Boundary entry selection: func='{}' entry_block={:?} remapped={:?} join_inputs={:?} entry_params={:?}",
entry_func_name,
entry_func.entry_block,
entry_block_remapped,
boundary.join_inputs,
entry_func.params
);
// Create BTreeMap from remapper for BoundaryInjector (temporary adapter)
// Phase 222.5-E: HashMap → BTreeMap for determinism

View File

@ -6,9 +6,10 @@
//! - ブロックID マッピング管理
use crate::ast::Span;
use crate::mir::join_ir::{JoinInst, MirLikeInst};
use crate::mir::join_ir::{JoinFuncId, JoinInst, MirLikeInst};
use crate::mir::{BasicBlockId, EffectMask, MirFunction, MirInstruction, MirType, ValueId};
use crate::mir::types::ConstValue;
use std::collections::BTreeMap;
use super::{convert_mir_like_inst, join_func_name, JoinIrVmBridgeError};
@ -22,6 +23,9 @@ pub struct JoinIrBlockConverter {
current_block_id: BasicBlockId,
current_instructions: Vec<MirInstruction>,
next_block_id: u32,
/// Phase 256 P1.8: Map from JoinFuncId to actual function name
/// When set, handle_call uses this instead of join_func_name()
func_name_map: Option<BTreeMap<JoinFuncId, String>>,
}
impl JoinIrBlockConverter {
@ -30,6 +34,18 @@ impl JoinIrBlockConverter {
current_block_id: BasicBlockId(0), // entry block
current_instructions: Vec::new(),
next_block_id: 1, // start from 1 (0 is entry)
func_name_map: None,
}
}
/// Phase 256 P1.8: Create converter with function name map
/// This ensures Call instructions use actual function names instead of "join_func_N"
pub fn new_with_func_names(func_name_map: BTreeMap<JoinFuncId, String>) -> Self {
Self {
current_block_id: BasicBlockId(0),
current_instructions: Vec::new(),
next_block_id: 1,
func_name_map: Some(func_name_map),
}
}
@ -349,7 +365,12 @@ impl JoinIrBlockConverter {
));
}
let func_name = join_func_name(*func);
// Phase 256 P1.8: Use actual function name if available
let func_name = if let Some(ref map) = self.func_name_map {
map.get(func).cloned().unwrap_or_else(|| join_func_name(*func))
} else {
join_func_name(*func)
};
// Phase 131 P2: Stable function name ValueId (module-global SSOT)
//
@ -420,21 +441,31 @@ impl JoinIrBlockConverter {
fn handle_jump(
&mut self,
mir_func: &mut MirFunction,
_cont: &crate::mir::join_ir::JoinContId,
cont: &crate::mir::join_ir::JoinContId,
args: &[ValueId],
cond: &Option<ValueId>,
) -> Result<(), JoinIrVmBridgeError> {
// Phase 246-EX: Preserve ALL Jump args as metadata for exit PHI construction
// Phase 27-shortterm S-4.4-A: Jump → Branch/Return
// Phase 256 P1.9: Jump → tail call to continuation function
// Previously was just `ret args[0]`, now generates `call cont(args...); ret result`
debug_log!(
"[joinir_block] Converting Jump args={:?}, cond={:?}",
"[joinir_block] Converting Jump to tail call: cont={:?}, args={:?}, cond={:?}",
cont,
args,
cond
);
// Get continuation function name
let cont_name = self.get_continuation_name(cont);
// Phase 256 P1.9: Use distinct ValueIds for Jump tail call
// FUNC_NAME_ID_BASE for call targets, 99992 for Jump result (distinct from 99991 in handle_call)
const JUMP_FUNC_NAME_ID_BASE: u32 = 91000; // Different from handle_call's 90000
let func_name_id = ValueId(JUMP_FUNC_NAME_ID_BASE + cont.0);
let call_result_id = ValueId(99992); // Distinct from handle_call's 99991
match cond {
Some(cond_var) => {
// Conditional jump
// Conditional jump → Branch + tail call to continuation
let exit_block_id = BasicBlockId(self.next_block_id);
self.next_block_id += 1;
let continue_block_id = BasicBlockId(self.next_block_id);
@ -453,16 +484,27 @@ impl JoinIrBlockConverter {
branch_terminator,
);
// Phase 246-EX: Store all Jump args in exit block metadata
// Exit block: Create with all Jump args stored as metadata
let exit_value = args.first().copied();
// Exit block: tail call to continuation function
let mut exit_block = crate::mir::BasicBlock::new(exit_block_id);
// Phase 246-EX: Store Jump args in a new metadata field
// This preserves carrier values for exit PHI construction
// Phase 246-EX: Store Jump args in metadata for exit PHI construction
exit_block.jump_args = Some(args.to_vec());
exit_block.terminator = Some(MirInstruction::Return { value: exit_value });
// Phase 256 P1.9: Generate tail call to continuation
exit_block.instructions.push(MirInstruction::Const {
dst: func_name_id,
value: crate::mir::ConstValue::String(cont_name.clone()),
});
exit_block.instruction_spans.push(Span::unknown());
exit_block.instructions.push(MirInstruction::Call {
dst: Some(call_result_id),
func: func_name_id,
callee: None,
args: args.to_vec(),
effects: EffectMask::PURE,
});
exit_block.instruction_spans.push(Span::unknown());
exit_block.terminator = Some(MirInstruction::Return { value: Some(call_result_id) });
mir_func.blocks.insert(exit_block_id, exit_block);
// Continue block
@ -472,9 +514,28 @@ impl JoinIrBlockConverter {
self.current_block_id = continue_block_id;
}
None => {
// Unconditional jump
let exit_value = args.first().copied();
let return_terminator = MirInstruction::Return { value: exit_value };
// Unconditional jump → tail call to continuation
// Finalize current block with tail call
self.current_instructions.push(MirInstruction::Const {
dst: func_name_id,
value: crate::mir::ConstValue::String(cont_name),
});
self.current_instructions.push(MirInstruction::Call {
dst: Some(call_result_id),
func: func_name_id,
callee: None,
args: args.to_vec(),
effects: EffectMask::PURE,
});
// Preserve jump args as metadata (SSOT for ExitLine/jump_args wiring).
if let Some(block) = mir_func.blocks.get_mut(&self.current_block_id) {
if block.jump_args.is_none() {
block.jump_args = Some(args.to_vec());
}
}
let return_terminator = MirInstruction::Return { value: Some(call_result_id) };
Self::finalize_block(
mir_func,
@ -487,6 +548,18 @@ impl JoinIrBlockConverter {
Ok(())
}
/// Phase 256 P1.9: Get continuation function name from func_name_map
fn get_continuation_name(&self, cont: &crate::mir::join_ir::JoinContId) -> String {
// JoinContId.0 == JoinFuncId.0 (same underlying ID via as_cont())
if let Some(ref map) = self.func_name_map {
if let Some(name) = map.get(&JoinFuncId(cont.0)) {
return name.clone();
}
}
// Fallback: use join_func_name()
join_func_name(JoinFuncId(cont.0))
}
fn handle_select(
&mut self,
mir_func: &mut MirFunction,

View File

@ -5,8 +5,9 @@
//! - ブロック変換の統合
//! - 関数署名の管理
use crate::mir::join_ir::{JoinFunction, JoinModule};
use crate::mir::join_ir::{JoinFuncId, JoinFunction, JoinModule};
use crate::mir::{BasicBlockId, EffectMask, FunctionSignature, MirFunction, MirModule, MirType};
use std::collections::BTreeMap;
use super::join_func_name;
use super::joinir_block_converter::JoinIrBlockConverter;
@ -116,6 +117,53 @@ impl JoinIrFunctionConverter {
Ok(mir_func)
}
/// Phase 256 P1.8: Convert function with actual function name map
///
/// This variant ensures Call instructions use actual function names ("main", "loop_step", "k_exit")
/// instead of generated names ("join_func_0", "join_func_1", etc.)
pub(crate) fn convert_function_with_func_names(
join_func: &JoinFunction,
func_name_map: BTreeMap<JoinFuncId, String>,
) -> Result<MirFunction, JoinIrVmBridgeError> {
let entry_block = BasicBlockId(0);
let param_types = join_func
.params
.iter()
.map(|_| MirType::Unknown)
.collect::<Vec<_>>();
let signature = FunctionSignature {
name: join_func.name.clone(),
params: param_types,
return_type: MirType::Unknown,
effects: EffectMask::PURE,
};
let mut mir_func = MirFunction::new(signature, entry_block);
mir_func.params = join_func.params.clone();
// Phase 256 P1.8: Use BlockConverter with function name map
let mut block_converter = JoinIrBlockConverter::new_with_func_names(func_name_map);
block_converter.convert_function_body(&mut mir_func, &join_func.body)?;
debug_log!(
"[joinir_vm_bridge] Function '{}' has {} blocks:",
mir_func.signature.name,
mir_func.blocks.len()
);
for (block_id, block) in &mir_func.blocks {
debug_log!(
" Block {:?}: {} instructions, terminator={:?}",
block_id,
block.instructions.len(),
block.terminator
);
}
Ok(mir_func)
}
}
#[cfg(test)]

View File

@ -1,8 +1,9 @@
// Phase 190: Use modularized converter
use super::{JoinIrFunctionConverter, JoinIrVmBridgeError};
use crate::mir::join_ir::frontend::JoinFuncMetaMap;
use crate::mir::join_ir::JoinModule;
use crate::mir::join_ir::{JoinFuncId, JoinModule};
use crate::mir::{MirFunction, MirModule};
use std::collections::BTreeMap;
/// Phase 40-1実験用: JoinFuncMetaを使ったMIR変換
///
@ -29,6 +30,15 @@ pub fn convert_join_module_to_mir_with_meta(
let mut mir_module = MirModule::new("joinir_bridge_with_meta".to_string());
// Phase 256 P1.8: Build function name map for all functions in the module
// This ensures Call instructions use actual names ("main", "loop_step", "k_exit")
// instead of generated names ("join_func_0", "join_func_1", etc.)
let func_name_map: BTreeMap<JoinFuncId, String> = module
.functions
.iter()
.map(|(id, func)| (*id, func.name.clone()))
.collect();
// 1. 各関数を変換
for (func_id, join_func) in &module.functions {
debug_log!(
@ -37,8 +47,11 @@ pub fn convert_join_module_to_mir_with_meta(
join_func.name
);
// 2. 基本のMIR変換Phase 190: modularized converter
let mir_func = JoinIrFunctionConverter::convert_function(join_func)?;
// 2. 基本のMIR変換Phase 256 P1.8: with func_name_map
let mir_func = JoinIrFunctionConverter::convert_function_with_func_names(
join_func,
func_name_map.clone(),
)?;
// Phase 189 DEBUG: Dump MirFunction blocks to check PHI presence
// Guarded to avoid polluting stdout/stderr in normal runs.

View File

@ -3,8 +3,9 @@
//! Separated from canonicalizer.rs for better maintainability.
use super::canonicalizer::canonicalize_loop_expr;
use super::skeleton_types::{CarrierRole, LoopPatternKind, SkeletonStep, UpdateKind};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::LoopPatternKind;
use super::skeleton_types::{CarrierRole, SkeletonStep, UpdateKind};
#[test]
fn test_canonicalize_rejects_non_loop() {

View File

@ -522,4 +522,80 @@ mod tests {
"TypeOp should not be dropped by DCE when used by console.log (ExternCall)"
);
}
#[test]
fn test_dce_keeps_jump_args_values() {
let signature = FunctionSignature {
name: "main".to_string(),
params: vec![],
return_type: MirType::Void,
effects: super::super::effect::EffectMask::PURE,
};
let mut func = MirFunction::new(signature, BasicBlockId::new(0));
let bb0 = BasicBlockId::new(0);
let mut b0 = BasicBlock::new(bb0);
let v0 = ValueId::new(0);
let v1 = ValueId::new(1);
b0.add_instruction(MirInstruction::Const {
dst: v0,
value: ConstValue::Integer(1),
});
b0.add_instruction(MirInstruction::Copy { dst: v1, src: v0 });
b0.add_instruction(MirInstruction::Return { value: None });
b0.jump_args = Some(vec![v1]);
func.add_block(b0);
let mut module = MirModule::new("test".to_string());
module.add_function(func);
crate::mir::passes::dce::eliminate_dead_code(&mut module);
let f = module.get_function("main").unwrap();
let block = f.get_block(bb0).unwrap();
let has_copy = block
.instructions
.iter()
.any(|inst| matches!(inst, MirInstruction::Copy { .. }));
assert!(has_copy, "Copy used only by jump_args should not be eliminated");
}
#[test]
fn test_dce_syncs_instruction_spans() {
let signature = FunctionSignature {
name: "main".to_string(),
params: vec![],
return_type: MirType::Integer,
effects: super::super::effect::EffectMask::PURE,
};
let mut func = MirFunction::new(signature, BasicBlockId::new(0));
let bb0 = BasicBlockId::new(0);
let mut b0 = BasicBlock::new(bb0);
let v0 = ValueId::new(0);
let v1 = ValueId::new(1);
b0.add_instruction(MirInstruction::Const {
dst: v0,
value: ConstValue::Integer(1),
});
b0.add_instruction(MirInstruction::Const {
dst: v1,
value: ConstValue::Integer(2),
});
b0.add_instruction(MirInstruction::Return { value: Some(v0) });
func.add_block(b0);
let mut module = MirModule::new("test".to_string());
module.add_function(func);
crate::mir::passes::dce::eliminate_dead_code(&mut module);
let f = module.get_function("main").unwrap();
let block = f.get_block(bb0).unwrap();
assert_eq!(
block.instructions.len(),
block.instruction_spans.len(),
"Instruction spans must stay aligned after DCE"
);
let has_unused_const = block.instructions.iter().any(|inst| {
matches!(inst, MirInstruction::Const { dst, .. } if *dst == v1)
});
assert!(!has_unused_const, "Unused const should be eliminated by DCE");
}
}

View File

@ -36,6 +36,11 @@ fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize {
used_values.insert(u);
}
}
if let Some(args) = &block.jump_args {
for &u in args {
used_values.insert(u);
}
}
}
// Backward propagation: if a value is used, mark its operands as used
@ -61,12 +66,15 @@ fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize {
let mut eliminated = 0usize;
let dce_trace = std::env::var("NYASH_DCE_TRACE").ok().as_deref() == Some("1");
for (bbid, block) in &mut function.blocks {
block.instructions.retain(|inst| {
let insts = std::mem::take(&mut block.instructions);
let spans = std::mem::take(&mut block.instruction_spans);
let mut kept_insts = Vec::with_capacity(insts.len());
let mut kept_spans = Vec::with_capacity(spans.len());
for (inst, span) in insts.into_iter().zip(spans.into_iter()) {
let mut keep = true;
if inst.effects().is_pure() {
if let Some(dst) = inst.dst_value() {
if !used_values.contains(&dst) {
// Keep indices stable is not required here; remove entirely
// NYASH_DCE_TRACE=1 enables logging for debugging DCE issues
if dce_trace {
eprintln!(
"[dce] Eliminating unused pure instruction in bb{}: %{} = {:?}",
@ -74,12 +82,17 @@ fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize {
);
}
eliminated += 1;
return false;
keep = false;
}
}
}
true
});
if keep {
kept_insts.push(inst);
kept_spans.push(span);
}
}
block.instructions = kept_insts;
block.instruction_spans = kept_spans;
}
if eliminated > 0 {
function.update_cfg();