Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 126 additions & 1 deletion crates/spar-analysis/src/connectivity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use rustc_hash::FxHashSet;
use spar_hir_def::instance::{ComponentInstanceIdx, FeatureInstanceIdx, SystemInstance};
use spar_hir_def::item_tree::Direction;

use crate::{Analysis, AnalysisDiagnostic, Severity, component_path};
use spar_hir_def::instance::SystemOperationMode;

use crate::{Analysis, AnalysisDiagnostic, ModalAnalysis, Severity, component_path};

/// Analyzes connection completeness across the instance model.
///
Expand All @@ -23,6 +25,10 @@ impl Analysis for ConnectivityAnalysis {
"connectivity"
}

fn as_modal(&self) -> Option<&dyn ModalAnalysis> {
Some(self)
}

fn analyze(&self, instance: &SystemInstance) -> Vec<AnalysisDiagnostic> {
// Severity rationale (STPA-REQ-016):
// Warning — unconnected required/provided port, or featureless component with connections
Expand Down Expand Up @@ -140,6 +146,125 @@ impl Analysis for ConnectivityAnalysis {
}
}

impl ModalAnalysis for ConnectivityAnalysis {
fn analyze_in_mode(
&self,
instance: &SystemInstance,
som: &SystemOperationMode,
) -> Vec<AnalysisDiagnostic> {
use crate::modal::is_component_active_in_som;

let mut diags = Vec::new();

// Build set of connected features considering only connections active in this SOM.
let connected_features = collect_connected_features_in_som(instance, som);

for (comp_idx, comp) in instance.all_components() {
// Skip components that are not active in this SOM.
if !is_component_active_in_som(instance, comp_idx, som) {
continue;
}

for &feat_idx in &comp.features {
let feat = &instance.features[feat_idx];

if !is_port_feature(feat_idx, instance) {
continue;
}

let feat_name = feat.name.as_str();
let is_connected = connected_features.contains(&(comp_idx, feat_name.to_string()));

if !is_connected {
if is_intentionally_unconnected(instance, comp_idx, feat_name) {
continue;
}

let path = component_path(instance, comp_idx);
match feat.direction {
Some(Direction::In) | Some(Direction::InOut) => {
diags.push(AnalysisDiagnostic {
severity: Severity::Warning,
message: format!(
"input port '{}' has no incoming connection",
feat.name
),
path,
analysis: self.name().to_string(),
});
}
Some(Direction::Out) => {
diags.push(AnalysisDiagnostic {
severity: Severity::Warning,
message: format!(
"output port '{}' has no outgoing connection",
feat.name
),
path,
analysis: self.name().to_string(),
});
}
None => {
diags.push(AnalysisDiagnostic {
severity: Severity::Info,
message: format!(
"feature '{}' has no direction and no connections",
feat.name
),
path,
analysis: self.name().to_string(),
});
}
}
}
}
}

diags
}
}

/// Build a set of connected features considering only connections active in a SOM.
fn collect_connected_features_in_som(
instance: &SystemInstance,
som: &SystemOperationMode,
) -> FxHashSet<(ComponentInstanceIdx, String)> {
use crate::modal::is_connection_active_in_som;

let mut connected: FxHashSet<(ComponentInstanceIdx, String)> = FxHashSet::default();

for (_idx, conn) in instance.connections.iter() {
// Skip connections not active in this SOM.
if !is_connection_active_in_som(instance, conn.owner, &conn.in_modes, som) {
continue;
}
if let Some(ref src) = conn.src
&& let Some(comp_idx) = resolve_subcomponent(instance, conn.owner, &src.subcomponent)
{
connected.insert((comp_idx, src.feature.as_str().to_string()));
}
if let Some(ref dst) = conn.dst
&& let Some(comp_idx) = resolve_subcomponent(instance, conn.owner, &dst.subcomponent)
{
connected.insert((comp_idx, dst.feature.as_str().to_string()));
}
}

// Semantic connections are not mode-filtered (they are already traced).
for sc in &instance.semantic_connections {
connected.insert((
sc.ultimate_source.0,
sc.ultimate_source.1.as_str().to_string(),
));
connected.insert((
sc.ultimate_destination.0,
sc.ultimate_destination.1.as_str().to_string(),
));
}

connected
}

/// Check if a feature is a port-like feature (data port, event port, event data port).
fn is_port_feature(_feat_idx: FeatureInstanceIdx, instance: &SystemInstance) -> bool {
use spar_hir_def::item_tree::FeatureKind;
Expand Down
49 changes: 48 additions & 1 deletion crates/spar-analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub mod weight_power;
pub mod wrpc_binding;

use serde::Serialize;
use spar_hir_def::instance::SystemInstance;
use spar_hir_def::instance::{SystemInstance, SystemOperationMode};

/// A single analysis that can be run on an AADL system instance.
pub trait Analysis {
Expand All @@ -61,6 +61,26 @@ pub trait Analysis {

/// Run the analysis on a system instance. Returns diagnostics.
fn analyze(&self, instance: &SystemInstance) -> Vec<AnalysisDiagnostic>;

/// Return this analysis as a `ModalAnalysis` if it supports per-SOM analysis.
///
/// The default returns `None`, meaning the analysis is mode-independent.
fn as_modal(&self) -> Option<&dyn ModalAnalysis> {
None
}
}

/// A mode-dependent analysis that can be run per System Operation Mode (SOM).
///
/// Analyses implementing this trait will be invoked once per SOM by
/// [`AnalysisRunner::run_all_per_som`], receiving the specific SOM context.
pub trait ModalAnalysis: Analysis {
/// Run the analysis on a system instance within a specific SOM context.
fn analyze_in_mode(
&self,
instance: &SystemInstance,
som: &SystemOperationMode,
) -> Vec<AnalysisDiagnostic>;
}

/// A diagnostic produced by an analysis pass.
Expand Down Expand Up @@ -187,6 +207,33 @@ impl AnalysisRunner {
}
all_diagnostics
}

/// Run mode-independent analyses once, then run mode-dependent analyses
/// once per System Operation Mode (SOM).
///
/// Diagnostics from per-SOM analyses are prefixed with `[mode: <name>]`.
pub fn run_all_per_som(&self, instance: &SystemInstance) -> Vec<AnalysisDiagnostic> {
let mut all = Vec::new();

// Run mode-independent analyses once (the normal path).
all.extend(self.run_all(instance));

// Run mode-dependent analyses per SOM.
for som in &instance.system_operation_modes {
let som_name = &som.name;
for analysis in &self.analyses {
if let Some(modal) = analysis.as_modal() {
let diags = modal.analyze_in_mode(instance, som);
for mut d in diags {
d.message = format!("[mode: {som_name}] {}", d.message);
all.push(d);
}
}
}
}

all
}
}

impl Default for AnalysisRunner {
Expand Down
77 changes: 76 additions & 1 deletion crates/spar-analysis/src/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! use these helpers to note when they used default (non-modal) property
//! values despite SOMs being defined.

use spar_hir_def::instance::SystemInstance;
use spar_hir_def::instance::{ComponentInstanceIdx, SystemInstance, SystemOperationMode};
use spar_hir_def::name::Name;

/// Check if an instance has any system operation modes.
Expand All @@ -29,6 +29,81 @@ pub fn mode_names(instance: &SystemInstance) -> Vec<String> {
/// - If `current_mode` is `None` (no mode context) the element is always active.
/// - Otherwise the element is active when `current_mode` matches any entry in
/// `in_modes` (case-insensitive comparison).
///
/// Check whether a component instance is active in a given System Operation Mode (SOM).
///
/// A component is active in a SOM when:
/// 1. Its `in_modes` list is empty (non-modal — active in all modes), or
/// 2. The SOM selects a mode on the component's parent, and that mode name
/// appears in the component's `in_modes` list.
///
/// If the component's parent has no mode selection in the SOM (i.e. the parent
/// is not a modal component), the component is treated as active.
pub fn is_component_active_in_som(
instance: &SystemInstance,
comp_idx: ComponentInstanceIdx,
som: &SystemOperationMode,
) -> bool {
let comp = instance.component(comp_idx);

// Non-modal component: always active.
if comp.in_modes.is_empty() {
return true;
}

// Find the mode that the SOM selects for this component's parent.
let parent_idx = match comp.parent {
Some(p) => p,
// Root component with in_modes set — unusual, treat as active.
None => return true,
};

// Look through the SOM's mode selections for one owned by the parent.
for &(_sel_comp, mode_inst_idx) in &som.mode_selections {
let mode_inst = &instance.mode_instances[mode_inst_idx];
if mode_inst.owner == parent_idx {
// Found the mode selected on the parent — check if it matches.
return comp
.in_modes
.iter()
.any(|m| m.as_str().eq_ignore_ascii_case(mode_inst.name.as_str()));
}
}

// Parent has no mode selection in this SOM — component is active.
true
}

/// Check whether a connection is active in a given System Operation Mode (SOM).
///
/// A connection is active when its `in_modes` list is empty (non-modal) or
/// the SOM selects a mode on its owner that matches one of the connection's
/// mode names.
pub fn is_connection_active_in_som(
instance: &SystemInstance,
conn_owner: ComponentInstanceIdx,
conn_in_modes: &[Name],
som: &SystemOperationMode,
) -> bool {
// Non-modal connection: always active.
if conn_in_modes.is_empty() {
return true;
}

// Find the mode that the SOM selects for the connection's owner.
for &(_sel_comp, mode_inst_idx) in &som.mode_selections {
let mode_inst = &instance.mode_instances[mode_inst_idx];
if mode_inst.owner == conn_owner {
return conn_in_modes
.iter()
.any(|m| m.as_str().eq_ignore_ascii_case(mode_inst.name.as_str()));
}
}

// Owner has no mode selection in this SOM — connection is active.
true
}

pub fn is_active_in_mode(in_modes: &[Name], current_mode: Option<&str>) -> bool {
// Non-modal element: always active.
if in_modes.is_empty() {
Expand Down
Loading
Loading