use vil_ir::core::WorkflowIR;
use vil_types::{CleanupPolicy, TransferMode};
use crate::traits::{Diagnostic, ValidationPass, ValidationReport};
pub struct OwnershipLegalityPass;
impl ValidationPass for OwnershipLegalityPass {
fn name(&self) -> &'static str {
"OwnershipLegalityPass"
}
fn run(&self, ir: &WorkflowIR) -> ValidationReport {
let mut report = ValidationReport::new();
for route in &ir.routes {
let implies_ownership_risk = matches!(
route.transfer_mode,
TransferMode::LoanWrite | TransferMode::LoanRead | TransferMode::PublishOffset
);
if implies_ownership_risk {
let mut check_proc = |proc_name: &str, role: &str| {
if let Some(proc_ir) = ir.processes.get(proc_name) {
if proc_ir.cleanup_policy != CleanupPolicy::ReclaimOrphans {
report.push(Diagnostic::warning(
"W-OWNERSHIP-01",
format!("Process '{}' acts as {} using zero-copy transfer {:?} but its cleanup policy is {:?}", proc_name, role, route.transfer_mode, proc_ir.cleanup_policy),
proc_name.to_string(),
));
}
}
};
check_proc(&route.from_process, "producer");
check_proc(&route.to_process, "consumer");
}
}
for transfer in &ir.transfers {
let implies_ownership = matches!(
transfer.transfer_mode,
TransferMode::LoanWrite | TransferMode::LoanRead | TransferMode::PublishOffset
);
if implies_ownership {
let has_published = transfer
.expected_flow
.contains(&vil_ir::OwnershipState::Published);
let has_released = transfer
.expected_flow
.contains(&vil_ir::OwnershipState::Released);
if has_published && !has_released {
report.push(Diagnostic::error(
"E-OWNERSHIP-LEAK-01",
format!(
"Transfer '{}' (mode {:?}) expects 'Published' state but missing 'Released' state. This will cause a memory leak.",
transfer.name, transfer.transfer_mode
),
&transfer.name,
));
}
}
}
report
}
}
#[cfg(test)]
mod tests {
use super::*;
use vil_ir::builder::*;
use vil_ir::{OwnershipState, TransferExprIR};
use vil_types::{CleanupPolicy, QueueKind};
#[test]
fn test_ownership_leak_detection() {
let mut ir = WorkflowBuilder::new("LeakDemo")
.add_message(MessageBuilder::new("Data").build())
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "Data")
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "Data")
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(
ProcessBuilder::new("Producer", "Iface")
.cleanup(CleanupPolicy::ReclaimOrphans)
.build(),
)
.add_process(
ProcessBuilder::new("Consumer", "Iface")
.cleanup(CleanupPolicy::ReclaimOrphans)
.build(),
)
.route("Producer", "tx", "Consumer", "rx", TransferMode::LoanWrite)
.build();
ir.transfers.push(TransferExprIR {
name: "valid_transfer".into(),
from_process: "Producer".into(),
from_port: "tx".into(),
to_process: "Consumer".into(),
to_port: "rx".into(),
transfer_mode: TransferMode::LoanWrite,
message_name: "Data".into(),
expected_flow: vec![
OwnershipState::Allocated,
OwnershipState::Published,
OwnershipState::Received,
OwnershipState::Released,
],
});
let pass = OwnershipLegalityPass;
let report = pass.run(&ir);
assert!(
!report.has_errors(),
"Valid transfer should not have errors"
);
ir.transfers.clear();
ir.transfers.push(TransferExprIR {
name: "leaking_transfer".into(),
from_process: "Producer".into(),
from_port: "tx".into(),
to_process: "Consumer".into(),
to_port: "rx".into(),
transfer_mode: TransferMode::LoanWrite,
message_name: "Data".into(),
expected_flow: vec![
OwnershipState::Allocated,
OwnershipState::Published,
OwnershipState::Received,
],
});
let report = pass.run(&ir);
assert!(report.has_errors());
assert!(report
.diagnostics
.iter()
.any(|d| d.code == "E-OWNERSHIP-LEAK-01"));
}
}