use vil_ir::core::WorkflowIR;
use vil_types::LaneKind;
use crate::traits::{Diagnostic, ValidationPass, ValidationReport};
pub struct SemanticLanePass;
impl ValidationPass for SemanticLanePass {
fn name(&self) -> &'static str {
"SemanticLanePass"
}
fn run(&self, ir: &WorkflowIR) -> ValidationReport {
let mut report = ValidationReport::new();
for route in &ir.routes {
let dest_port = ir
.interfaces
.values()
.flat_map(|iface| iface.ports.values())
.find(|p| p.name == route.to_port);
let dest_port = match dest_port {
Some(p) => p,
None => continue, };
let src_port = ir
.interfaces
.values()
.flat_map(|iface| iface.ports.values())
.find(|p| p.name == route.from_port);
let src_port = match src_port {
Some(p) => p,
None => continue,
};
if dest_port.lane_kind == LaneKind::Default {
continue;
}
let message = ir.messages.get(&src_port.message_name);
let message = match message {
Some(m) => m,
None => continue, };
let context = format!(
"route {}.{} -> {}.{} (message '{}', kind={})",
route.from_process,
route.from_port,
route.to_process,
route.to_port,
message.name,
message.semantic_kind
);
let allowed_lanes = message.semantic_kind.allowed_lanes();
if !allowed_lanes.contains(&dest_port.lane_kind) {
report.push(Diagnostic::error(
"E-SEMANTIC-LANE-01",
format!(
"Semantic type '{}' (kind={}) is not allowed on {} lane. Allowed lanes: {:?}",
message.name,
message.semantic_kind,
dest_port.lane_kind,
allowed_lanes
),
&context,
));
}
let allowed_modes = message.semantic_kind.allowed_transfer_modes();
if !allowed_modes.contains(&route.transfer_mode) {
report.push(Diagnostic::error(
"E-SEMANTIC-LANE-02",
format!(
"Transfer mode '{}' is not allowed for semantic kind '{}'. Allowed: {:?}",
route.transfer_mode, message.semantic_kind, allowed_modes
),
&context,
));
}
if message.memory_class == vil_types::MemoryClass::ControlHeap
&& route.transfer_mode == vil_types::TransferMode::LoanWrite
{
report.push(Diagnostic::error(
"E-SEMANTIC-LANE-03",
format!(
"ControlHeap message '{}' cannot be sent via LoanWrite. Use Copy instead.",
message.name
),
&context,
));
}
if dest_port.lane_kind == LaneKind::Trigger {
let estimated_size = estimate_message_size(message);
if estimated_size > 64 {
report.push(Diagnostic::warning(
"W-SEMANTIC-LANE-01",
format!(
"Message '{}' (~{} bytes) sent to Trigger Lane exceeds 64-byte soft limit. Consider using Data Lane for large payloads.",
message.name,
estimated_size
),
&context,
));
}
}
}
report
}
}
fn estimate_message_size(msg: &vil_ir::core::MessageIR) -> usize {
use vil_ir::core::TypeRefIR;
let mut total = 0;
for field in &msg.fields {
total += match &field.ty {
TypeRefIR::Primitive(name) => match name.as_str() {
"u8" | "i8" | "bool" => 1,
"u16" | "i16" => 2,
"u32" | "i32" | "f32" => 4,
"u64" | "i64" | "f64" => 8,
"u128" | "i128" => 16,
_ => 8, },
TypeRefIR::VSlice(_) => 16, TypeRefIR::VRef(_) => 8, TypeRefIR::Named(_) => 8, TypeRefIR::Unknown(_) => 8, };
}
if total == 0 {
8
} else {
total
} }
#[cfg(test)]
mod tests {
use super::*;
use vil_ir::builder::*;
use vil_types::*;
#[test]
fn test_fault_on_data_lane_rejected() {
let ir = WorkflowBuilder::new("FaultOnDataLane")
.add_message(
MessageBuilder::new("MyFault")
.semantic_kind(SemanticKind::Fault)
.memory_class(MemoryClass::ControlHeap)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyFault")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "MyFault")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::Copy)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(report.has_errors());
assert!(report
.diagnostics
.iter()
.any(|d| d.code == "E-SEMANTIC-LANE-01"));
}
#[test]
fn test_fault_on_control_lane_accepted() {
let ir = WorkflowBuilder::new("FaultOnControlLane")
.add_message(
MessageBuilder::new("MyFault")
.semantic_kind(SemanticKind::Fault)
.memory_class(MemoryClass::ControlHeap)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyFault")
.lane(LaneKind::Control)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "MyFault")
.lane(LaneKind::Control)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::Copy)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(!report.has_errors());
}
#[test]
fn test_decision_on_trigger_lane_accepted() {
let ir = WorkflowBuilder::new("DecisionOnTrigger")
.add_message(
MessageBuilder::new("MyDecision")
.semantic_kind(SemanticKind::Decision)
.memory_class(MemoryClass::ControlHeap)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyDecision")
.lane(LaneKind::Trigger)
.queue(QueueKind::Mpmc, 10)
.done()
.in_port("rx", "MyDecision")
.lane(LaneKind::Trigger)
.queue(QueueKind::Mpmc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::Copy)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(!report.has_errors());
}
#[test]
fn test_decision_on_data_lane_rejected() {
let ir = WorkflowBuilder::new("DecisionOnDataLane")
.add_message(
MessageBuilder::new("MyDecision")
.semantic_kind(SemanticKind::Decision)
.memory_class(MemoryClass::ControlHeap)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyDecision")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "MyDecision")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::LoanWrite)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(report.has_errors());
assert!(report
.diagnostics
.iter()
.any(|d| d.code == "E-SEMANTIC-LANE-01"));
}
#[test]
fn test_state_on_data_lane_accepted() {
let ir = WorkflowBuilder::new("StateOnDataLane")
.add_message(
MessageBuilder::new("MyState")
.semantic_kind(SemanticKind::State)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyState")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "MyState")
.lane(LaneKind::Data)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::LoanWrite)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(!report.has_errors());
}
#[test]
fn test_control_heap_via_loan_write_rejected() {
let ir = WorkflowBuilder::new("ControlHeapLoanWrite")
.add_message(
MessageBuilder::new("MyFault")
.semantic_kind(SemanticKind::Fault)
.memory_class(MemoryClass::ControlHeap)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "MyFault")
.lane(LaneKind::Control)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "MyFault")
.lane(LaneKind::Control)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::LoanWrite)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(report.has_errors());
assert!(report
.diagnostics
.iter()
.any(|d| d.code == "E-SEMANTIC-LANE-02"));
assert!(report
.diagnostics
.iter()
.any(|d| d.code == "E-SEMANTIC-LANE-03"));
}
#[test]
fn test_message_on_any_lane_accepted() {
for lane in &[LaneKind::Trigger, LaneKind::Data, LaneKind::Control] {
let ir = WorkflowBuilder::new("MessageAnyLane")
.add_message(
MessageBuilder::new("GenericMsg")
.semantic_kind(SemanticKind::Message)
.build(),
)
.add_interface(
InterfaceBuilder::new("Iface")
.out_port("tx", "GenericMsg")
.lane(*lane)
.queue(QueueKind::Spsc, 10)
.done()
.in_port("rx", "GenericMsg")
.lane(*lane)
.queue(QueueKind::Spsc, 10)
.done()
.build(),
)
.add_process(ProcessBuilder::new("A", "Iface").build())
.add_process(ProcessBuilder::new("B", "Iface").build())
.route("A", "tx", "B", "rx", TransferMode::LoanWrite)
.build();
let pass = SemanticLanePass;
let report = pass.run(&ir);
assert!(
!report.has_errors(),
"Message should be allowed on {:?} lane",
lane
);
}
}
}