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

@ -48,12 +48,14 @@
## 2025-12-19Phase 256StringUtils.split/2 可変 step ループ)🔜
- Phase 256 README: `docs/development/current/main/phases/phase-256/README.md`
- Current first FAIL: `StringUtils.split/2``jump_args length mismatch``Carrier 'i' has no latch incoming set`
- Current first FAIL: `StringUtils.split/2`MIR verification: “Value defined multiple times” + SSA undef
- 状況:
- `MirInstruction::Select` の導入は完了、Pattern6index_ofは PASS 維持。
- `ValueId(57)` undefined は根治(原因は `const_1` 未初期化)。
- SSA undef`%49/%67`)は P1.7 で根治continuation 関数名の SSOT 不一致)。
- 残りは Pattern7 の carrier PHI 配線(ExitLine / jump_args 契約)を直す段階
- P1.8で ExitLine/jump_args の余剰許容と関数名マッピングを整流
- P1.9で `JoinInst::Jump` を tail call として bridge に落とし、`jump_args` を SSOT として保持。
- P1.10で DCE が `jump_args` を used 扱いし、`instruction_spans` を同期SPAN MISMATCH 根治)。
- P1.5-DBG: boundary entry params の契約チェックを追加VM実行前 fail-fast
- P1.6: 契約チェックの薄い集約 `run_all_pipeline_checks()` を導入pipeline の責務を縮退)。

View File

@ -0,0 +1,169 @@
# Phase 256: JoinIR Contract Questions (for ChatGPT Pro)
目的: Phase 256 の詰まりJump/continuation/params/jump_args の暗黙契約)を、設計として固めるための相談メモ。
---
## Q1. SSOT をどこに置くべき?
JoinIR の「意味論 SSOT」をどこに置くべきか。
- A) Structured JoinIR を SSOT として維持し、bridge/merge が意味解釈する
- B) Normalized JoinIR を SSOT とし、Structured→Normalized の正規化箱を必須化する
判断材料として、現在の層の境界と責務:
- Pattern lowererStructured JoinIR 生成)
- `join_ir_vm_bridge`JoinIR→MIR 変換)
- mergeMIR inline + PHI/ExitLine wiring
---
## Q2. `JoinInst::Jump` の正規形(不変条件)
現状の詰まりは `Jump` が層を跨ぐときに「tail call 等価」になったり「Return 化」になったりして、continuation が失われる点にある。
相談したい:
- `JoinInst::Jump { cont, args, cond }` を SSOT 的にどう定義するべきか?
- cond 付き Jump は JoinIR 語彙として残すべきか?それとも IfMerge に寄せるべきか?
最小コードJoinIR 命令):
```rust
// src/mir/join_ir/mod.rs
pub enum JoinInst {
// ...
Jump {
cont: JoinContId,
args: Vec<VarId>,
cond: Option<VarId>,
},
Call {
func: JoinFuncId,
args: Vec<VarId>,
dst: Option<VarId>,
k_next: Option<JoinContId>,
},
Ret { value: Option<VarId> },
// ...
}
```
---
## Q3. boundary/params/jump_args の順序契約をどこで固定する?
Phase 256 では、次の対応関係が暗黙で、崩れると SSA undef / PHI wiring fail-fast になりやすい。
- `JoinInlineBoundary.join_inputs``JoinModule.entry.params`
- `exit_bindings` ↔ ExitLine の carrier PHI reconnect
- `jump_args`tail call args metadata↔ ExitLine の latch incoming 復元
最小コードboundary:
```rust
// src/mir/join_ir/lowering/inline_boundary.rs
pub struct JoinInlineBoundary {
pub join_inputs: Vec<ValueId>,
pub host_inputs: Vec<ValueId>,
pub loop_invariants: Vec<(String, ValueId)>,
pub exit_bindings: Vec<LoopExitBinding>,
pub expr_result: Option<ValueId>,
pub loop_var_name: Option<String>,
pub continuation_func_ids: std::collections::BTreeSet<String>,
// ...
}
```
契約の fail-fast は現在ここで行っている:
```rust
// src/mir/builder/control_flow/joinir/merge/contract_checks.rs
pub(in crate::mir::builder::control_flow::joinir) fn run_all_pipeline_checks(
join_module: &crate::mir::join_ir::JoinModule,
boundary: &JoinInlineBoundary,
) -> Result<(), String> {
verify_boundary_entry_params(join_module, boundary)?;
// ...
Ok(())
}
```
相談したい:
- この順序契約は「boundary」「normalizer」「bridge」「merge」のどの層が SSOT になるべきか?
- fail-fast の責務をどこに置くべきか(今は conversion_pipeline 直前)?
---
## Q4. continuation の識別: `JoinFuncId` vs `String`(関数名)
Phase 256 P1.7 で「continuation 関数が merge で見つからず SSA undef」になった。
原因は bridge 側と merge 側の “関数名 SSOT” 不一致。
現状の暫定 SSOT は `canonical_names`:
```rust
// src/mir/join_ir/lowering/canonical_names.rs
pub const K_EXIT: &str = "k_exit";
pub const K_EXIT_LEGACY: &str = "join_func_2";
pub const LOOP_STEP: &str = "loop_step";
pub const MAIN: &str = "main";
```
相談したい:
- continuation を `JoinFuncId` で保持し、bridge で 1 回だけ名前解決するべきか?
- それとも `String` を SSOT にして “MirModule key” と一致させ続けるべきか?
- 併存するなら、変換境界(片方→片方)をどこに置くべきか?
---
## Q5. 正規化 shadow`join_func_N`)との共存戦略
normalized_shadow 側は `join_func_2` のような命名を使う箇所がある。
この legacy をいつ・どう統一するべきか(または統一しないなら境界をどう明文化するか)。
---
## Q6. `jump_args` は MIR のどの層の SSOT か?
観測:
- ExitLine/merge 側は `BasicBlock.jump_args` を「exit/carry 値の SSOT」として参照する。
- bridge 側で tail call / Jump を生成するときに `jump_args` を落とし忘れると、ExitLine が fallback 経路へ入りやすく、
SSA/dominance の破綻につながる。
最小コードMIR basic block:
```rust
// src/mir/mod.rs
pub struct BasicBlock {
pub instructions: Vec<MirInstruction>,
pub instruction_spans: Vec<Span>,
pub terminator: Option<MirInstruction>,
pub jump_args: Option<Vec<ValueId>>,
// ...
}
```
相談したい:
- `jump_args` は terminatorJump/Branch/Return/Callに埋め込むべきか、それとも BasicBlock の外部メタのままでよいか?
- `jump_args` の契約順序・長さ・expr_result の有無・invariants の混在)をどこで固定するべきか?
---
## Q7. Optimizer/DCE の不変条件spans 同期と jump_args
観測:
- DCE が `jump_args` だけで使われる値を “unused” とみなすと、Copy/Const が消えて merge が壊れる。
- DCE が `instructions` だけを削って `instruction_spans` を同期しないと、SPAN MISMATCH が発生しデバッグが困難になる。
相談したい:
- `jump_args` は “use” として扱うのが SSOT として正しいか?
- spans 同期は「各パスの責務」か、それとも `BasicBlock` の API例: spanned filterに閉じ込めるべきか
---
## 観測された失敗例(短く)
- SSA undef関数名不一致で continuation が merge 対象にならず到達不能/未定義が露出)
- ExitLine: `jump_args` の長さ契約ミスマッチcarriers と invariants の混在)
- `JoinInst::Jump` が bridge で “Return 化” され、continuation の意味が落ちる疑い
- DCE が `jump_args` 由来の use を落とし、Copy が消えて SSA/dominance が崩れる

View File

@ -8,12 +8,13 @@ Related:
## Current Status (SSOT)
- Current first FAIL: `StringUtils.split/2``jump_args length mismatch``Carrier 'i' has no latch incoming set`
- Current first FAIL: `StringUtils.split/2`MIR verification: “Value defined multiple times” + SSA undef
- Pattern6 は PASS 維持
- 直近の完了:
- P1.10: DCE が `jump_args` 参照を保持し、`instruction_spans` と同期するよう修正(回帰テスト追加)
- P1.7: SSA undef`%49/%67`根治continuation 関数名の SSOT 不一致)
- P1.6: pipeline contract checks を `run_all_pipeline_checks()` に集約
- 次の作業: P1.8Pattern7 の carrier PHI wiring / ExitLine + jump_args 契約の修正
- 次の作業: P1.11merge 側の ExitLine/PHI/dominance を `--verify` で緑に戻す
---
@ -384,6 +385,35 @@ Option APattern 7 新設)を推奨。
結果:
- `./target/release/hakorune --backend vm --verify apps/tests/phase256_p0_split_min.hako` で SSA undef は消滅
---
## 進捗P1.8
### P1.8: ExitLine/jump_args と関数名マッピング整流(完了)
変更(要旨):
- ExitArgsCollector 側で「余剰 jump_argsinvariants」を許容し、`expected 3 or 4 but got 5` を解消
- JoinIR→MIR bridge 側で “join_func_N” 由来の名前と “JoinFunction.name” の不一致を解消するため、関数名マッピングを導入/伝播
結果:
- 旧 first FAILjump_args length mismatchは解消
### P1.9: Jump を tail call として表現(完了)
変更(要旨):
- JoinIR→MIR bridge で `JoinInst::Jump` を “continuation への tail call” として落とす
- `BasicBlock.jump_args` を tail call と同様に SSOT として保持ExitLine/collector の復元入力)
結果:
- `JoinInst::Jump` が “ret args[0]” 相当になり continuation が失われる問題は解消
### P1.10: DCE の jump_args + spans 同期(完了)
変更(要旨):
- DCE が `jump_args` で使われる値を used として扱い、純命令の除去で Copy が消えないようにする
- `instruction_spans``instructions` の同期不変条件を維持SPAN MISMATCH 根治)
- 回帰テストを追加(`test_dce_keeps_jump_args_values`, `test_dce_syncs_instruction_spans`
### リファクタリング方針P1.6候補 / 先送り推奨)
現時点split がまだ FAILでは、箱化のための箱化で複雑さが増えやすいので、以下を推奨する:

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();