phase29z(p0): minimal RC insertion overwrite release

This commit is contained in:
2025-12-27 15:03:05 +09:00
parent 2223c1309b
commit 977f105e4e
7 changed files with 207 additions and 31 deletions

View File

@ -0,0 +1,77 @@
//! Phase 29z P0: RC insertion minimal selfcheck (opt-in)
//!
//! This binary constructs a tiny synthetic MIR module containing Store overwrites
//! and asserts that the RC insertion pass inserts ReleaseStrong in the expected place.
//!
//! Build/run (opt-in):
//! - `cargo run --bin rc_insertion_selfcheck --features rc-insertion-minimal`
use nyash_rust::ast::Span;
use nyash_rust::mir::passes::rc_insertion::insert_rc_instructions;
use nyash_rust::mir::{
BasicBlock, BasicBlockId, EffectMask, FunctionSignature, MirFunction, MirInstruction, MirModule,
MirType, ValueId,
};
use std::collections::HashMap;
fn main() {
let ptr = ValueId::new(100);
let v1 = ValueId::new(1);
let v2 = ValueId::new(2);
let mut block = BasicBlock::new(BasicBlockId::new(0));
block.instructions = vec![
MirInstruction::Store { value: v1, ptr },
MirInstruction::Store { value: v2, ptr },
];
block.instruction_spans = vec![Span::unknown()];
let signature = FunctionSignature {
name: "selfcheck_fn".to_string(),
params: vec![],
return_type: MirType::Void,
effects: EffectMask::PURE,
};
let entry = block.id;
let mut func = MirFunction::new(signature, entry);
func.blocks = HashMap::from([(entry, block)]);
let mut module = MirModule::new("selfcheck_mod".to_string());
module.add_function(func);
let stats = insert_rc_instructions(&mut module);
if stats.release_inserted != 1 {
eprintln!(
"[FAIL] Expected release_inserted=1, got {}",
stats.release_inserted
);
std::process::exit(1);
}
let func = module
.get_function("selfcheck_fn")
.expect("selfcheck function exists");
let bb = func.blocks.get(&entry).expect("entry block exists");
if bb.instructions.len() != 3 {
eprintln!(
"[FAIL] Expected 3 instructions after rewrite, got {}",
bb.instructions.len()
);
std::process::exit(1);
}
if !matches!(bb.instructions[1], MirInstruction::ReleaseStrong { .. }) {
eprintln!("[FAIL] Expected ReleaseStrong inserted at index=1");
std::process::exit(1);
}
if bb.instructions.len() != bb.instruction_spans.len() {
eprintln!(
"[FAIL] Span count mismatch: insts={}, spans={}",
bb.instructions.len(),
bb.instruction_spans.len()
);
std::process::exit(1);
}
println!("[PASS] rc_insertion_selfcheck");
}

View File

@ -1,16 +1,30 @@
//! Phase 29y.1 Task 2: RC insertion pass (entry point only)
//! Phase 29z P0: RC insertion pass - Minimal overwrite release
//!
//! This pass runs after MIR optimization and verification, before backend codegen.
//! Currently a no-op skeleton - actual RC insertion logic will be added later.
//! Implements minimal RC insertion: overwrite release only (x = <new> releases old value).
//!
//! SSOT: docs/development/current/main/phases/phase-29y/20-RC-INSERTION-SSOT.md
//!
//! Design constraints:
//! - SSA last-use 禁止: weak_to_strong() で観測できるため
//! - 許可される drop 点: explicit drop, 上書き, スコープ終端のみ
//! - Default OFF: cfg(debug_assertions) only (no impact on release builds)
//!
//! Phase 29z P0 scope (minimal):
//! - ✅ Overwrite release (Store instruction): x = <new> triggers ReleaseStrong for old value
//! - ❌ PHI handling (out of scope)
//! - ❌ Loop/early-exit cleanup (out of scope)
//! - ❌ Cross-block tracking (single-block only)
use crate::mir::MirModule;
#[cfg(feature = "rc-insertion-minimal")]
use crate::ast::Span;
#[cfg(feature = "rc-insertion-minimal")]
use crate::mir::{MirInstruction, ValueId};
#[cfg(feature = "rc-insertion-minimal")]
use std::collections::HashMap;
/// Statistics from RC insertion pass
#[derive(Debug, Default, Clone)]
pub struct RcInsertionStats {
@ -24,42 +38,98 @@ pub struct RcInsertionStats {
pub blocks_visited: usize,
}
/// Phase 29y.1: RC insertion pass (skeleton only)
/// Phase 29z P0: RC insertion pass - Minimal overwrite release
///
/// This pass is called after MIR optimization and verification.
/// Currently a no-op that returns empty statistics.
/// Implements minimal case: overwrite release (x = <new> releases old value).
///
/// Future: Will insert retain/release instructions at:
/// - explicit drop points
/// - variable overwrite points
/// - scope exit points
/// **CRITICAL SAFETY NOTES**:
/// 1. ReleaseStrong does SSA alias cleanup (releases all SSA values sharing same Arc)
/// - MUST NOT release values still in use
/// - Safety guard: Skip release if `old_value == value` (same Arc, no-op overwrite)
/// 2. Span mismatch: `instruction_spans` may not match `instructions` length
/// - Fill missing spans with `Span::unknown()` to prevent panic
///
/// **Scope** (Phase 29z P0 minimal):
/// - ✅ Single-block overwrite detection
/// - ✅ Safety guards (SSA alias cleanup protection)
/// - ❌ PHI/loop/early-exit (out of scope)
/// - ❌ Cross-block tracking (out of scope)
///
/// **Opt-in**: Default OFF, enabled only with Cargo feature `rc-insertion-minimal`
pub fn insert_rc_instructions(module: &mut MirModule) -> RcInsertionStats {
let mut stats = RcInsertionStats::default();
// Skeleton: iterate module structure without modification
for (_name, func) in &module.functions {
stats.functions_processed += 1;
stats.blocks_visited += func.blocks.len();
// Phase 29z P0: Default OFF unless explicitly enabled.
// No new environment variables (per Phase 29z P0 constraints).
#[cfg(not(feature = "rc-insertion-minimal"))]
{
// No-op pass (just count structures)
for (_name, func) in &module.functions {
stats.functions_processed += 1;
stats.blocks_visited += func.blocks.len();
}
return stats;
}
// Phase 29y.1: No actual insertion yet (no-op pass)
// Future phases will add:
// - Drop point detection
// - Scope/binding tracking
// - PHI input liveness analysis
#[cfg(feature = "rc-insertion-minimal")]
{
// Implement minimal overwrite release
for (_name, func) in &mut module.functions {
stats.functions_processed += 1;
stats
}
for (_bid, block) in &mut func.blocks {
stats.blocks_visited += 1;
#[cfg(test)]
mod tests {
use super::*;
// Track what value each ptr currently holds (single-block scope)
let mut ptr_to_value: HashMap<ValueId, ValueId> = HashMap::new();
#[test]
fn test_skeleton_returns_stats() {
let mut module = MirModule::default();
let stats = insert_rc_instructions(&mut module);
assert_eq!(stats.keepalive_inserted, 0);
assert_eq!(stats.release_inserted, 0);
// Take ownership of instructions to rebuild with inserted releases
let insts = std::mem::take(&mut block.instructions);
let mut spans = std::mem::take(&mut block.instruction_spans);
// SAFETY: Ensure spans match instructions length (fill with Span::unknown() if needed)
// instruction_spans and instructions may not always match in length
while spans.len() < insts.len() {
spans.push(Span::unknown());
}
let mut new_insts = Vec::with_capacity(insts.len() * 2); // Extra space for releases
let mut new_spans = Vec::with_capacity(spans.len() * 2);
for (inst, span) in insts.into_iter().zip(spans.into_iter()) {
// Detect overwrite: Store to ptr that already holds a value
if let MirInstruction::Store { value, ptr } = &inst {
// Check if ptr already holds a value (potential overwrite)
if let Some(old_value) = ptr_to_value.get(ptr) {
// SAFETY GUARD: Don't release if old_value == new value (same Arc)
// ReleaseStrong does SSA alias cleanup, so releasing "still-needed" values breaks semantics
// If old_value == value, this is a no-op overwrite (x = x), no release needed
if old_value != value {
// Insert ReleaseStrong for old value BEFORE the Store
new_insts.push(MirInstruction::ReleaseStrong {
values: vec![*old_value],
});
new_spans.push(span.clone()); // Reuse span from Store instruction
stats.release_inserted += 1;
}
}
// Update tracking: ptr now holds new value
ptr_to_value.insert(*ptr, *value);
}
// Add original instruction (Store or any other instruction)
new_insts.push(inst);
new_spans.push(span);
}
// Replace block instructions with new sequence
block.instructions = new_insts;
block.instruction_spans = new_spans;
}
}
stats
}
}