use std::collections::{BTreeMap, BTreeSet};
use omena_abstract_value::{
AbstractCssValueV0, ControlFlowEdgeGraphV0, MAX_FLOW_ANALYSIS_ITERATIONS,
reachable_control_flow_block_ids,
};
use omena_parser::StyleDialect;
use omena_transform_cst::STABLE_NODE_KEY_TYPE_LABEL_V0;
use super::{
analyze_scss_control_flow_values_with_initial_bindings, build_scss_control_flow_graph,
dialect_label,
model::{
OmenaScssEvalControlFlowBlockIdV0, OmenaScssEvalControlFlowEdgeV0,
OmenaScssEvalControlFlowPruneReachabilityV0,
},
};
pub fn summarize_scss_control_flow_prune_reachability(
source: &str,
dialect: StyleDialect,
) -> Option<OmenaScssEvalControlFlowPruneReachabilityV0> {
summarize_scss_control_flow_prune_reachability_with_initial_bindings(
source,
dialect,
&BTreeMap::new(),
)
}
pub(crate) fn summarize_scss_control_flow_prune_reachability_with_initial_bindings(
source: &str,
dialect: StyleDialect,
initial_bindings: &BTreeMap<String, AbstractCssValueV0>,
) -> Option<OmenaScssEvalControlFlowPruneReachabilityV0> {
let graph = build_scss_control_flow_graph(source, dialect)?;
let block_ids = graph
.blocks
.iter()
.map(|block| block.id)
.collect::<BTreeSet<_>>();
let original_reachable_block_ids = reachable_control_flow_block_ids(&graph);
let original_edge_count = graph
.edges
.iter()
.filter(|edge| edge.target_block_id.is_some())
.count();
if graph.blocks.is_empty() {
return Some(OmenaScssEvalControlFlowPruneReachabilityV0 {
schema_version: "0",
product: "omena-scss-eval.control-flow-prune-reachability",
mode: "oracleOnlyPrunedReachability",
dialect: dialect_label(dialect),
block_id_type: "u32",
node_key_type: STABLE_NODE_KEY_TYPE_LABEL_V0,
max_iterations: MAX_FLOW_ANALYSIS_ITERATIONS,
iteration_count: 0,
converged: true,
flat_css_cfg_built: true,
merged_cross_file_graph: false,
block_count: 0,
original_edge_count,
pruned_edge_count: 0,
reachable_block_count: 0,
unreachable_block_count: 0,
have_terminals_changed: false,
reachable_block_ids: Vec::new(),
unreachable_block_ids: Vec::new(),
});
}
let truthiness_by_block_id =
control_flow_truthiness_by_block_id(source, dialect, initial_bindings)?;
let mut reachable_block_ids = original_reachable_block_ids.clone();
let mut pruned_edge_count = 0usize;
let mut iteration_count = 0usize;
let mut converged = false;
for iteration in 1..=MAX_FLOW_ANALYSIS_ITERATIONS {
iteration_count = iteration;
let pruned_graph = PrunedScssControlFlowGraph::new(
&graph.edges,
&truthiness_by_block_id,
&reachable_block_ids,
);
pruned_edge_count = original_edge_count.saturating_sub(pruned_graph.edge_count());
let next_reachable_block_ids = reachable_control_flow_block_ids(&pruned_graph);
if next_reachable_block_ids == reachable_block_ids {
converged = true;
break;
}
reachable_block_ids = next_reachable_block_ids;
}
let unreachable_block_ids = block_ids
.difference(&reachable_block_ids)
.copied()
.collect::<Vec<_>>();
let reachable_block_ids = reachable_block_ids.into_iter().collect::<Vec<_>>();
let have_terminals_changed = pruned_edge_count > 0
|| reachable_block_ids != sorted_block_ids(&original_reachable_block_ids);
Some(OmenaScssEvalControlFlowPruneReachabilityV0 {
schema_version: "0",
product: "omena-scss-eval.control-flow-prune-reachability",
mode: "oracleOnlyPrunedReachability",
dialect: dialect_label(dialect),
block_id_type: "u32",
node_key_type: STABLE_NODE_KEY_TYPE_LABEL_V0,
max_iterations: MAX_FLOW_ANALYSIS_ITERATIONS,
iteration_count,
converged,
flat_css_cfg_built: true,
merged_cross_file_graph: false,
block_count: graph.blocks.len(),
original_edge_count,
pruned_edge_count,
reachable_block_count: reachable_block_ids.len(),
unreachable_block_count: unreachable_block_ids.len(),
have_terminals_changed,
reachable_block_ids,
unreachable_block_ids,
})
}
fn control_flow_truthiness_by_block_id(
source: &str,
dialect: StyleDialect,
initial_bindings: &BTreeMap<String, AbstractCssValueV0>,
) -> Option<BTreeMap<OmenaScssEvalControlFlowBlockIdV0, bool>> {
let graph = build_scss_control_flow_graph(source, dialect)?;
let analysis =
analyze_scss_control_flow_values_with_initial_bindings(source, dialect, initial_bindings)?;
let block_id_by_node_key = graph
.blocks
.iter()
.map(|block| (block.node_key.as_str().to_string(), block.id))
.collect::<BTreeMap<_, _>>();
Some(
analysis
.blocks
.iter()
.filter_map(|block| {
let truthy = match block.transfer_truthiness {
Some("truthy") => true,
Some("falsey") => false,
_ => return None,
};
block_id_by_node_key
.get(block.node_key.as_str())
.copied()
.map(|block_id| (block_id, truthy))
})
.collect(),
)
}
fn sorted_block_ids(
block_ids: &BTreeSet<OmenaScssEvalControlFlowBlockIdV0>,
) -> Vec<OmenaScssEvalControlFlowBlockIdV0> {
block_ids.iter().copied().collect()
}
struct PrunedScssControlFlowGraph {
successor_ids_by_block_id:
BTreeMap<OmenaScssEvalControlFlowBlockIdV0, Vec<OmenaScssEvalControlFlowBlockIdV0>>,
}
impl PrunedScssControlFlowGraph {
fn new(
edges: &[OmenaScssEvalControlFlowEdgeV0],
truthiness_by_block_id: &BTreeMap<OmenaScssEvalControlFlowBlockIdV0, bool>,
reachable_block_ids: &BTreeSet<OmenaScssEvalControlFlowBlockIdV0>,
) -> Self {
let mut successor_ids_by_block_id = BTreeMap::new();
for edge in edges {
let Some(target_block_id) = edge.target_block_id else {
continue;
};
successor_ids_by_block_id
.entry(edge.source_block_id)
.or_insert_with(Vec::new);
if !reachable_block_ids.contains(&edge.source_block_id)
|| !control_flow_edge_survives_prune(edge, truthiness_by_block_id)
{
continue;
}
successor_ids_by_block_id
.entry(edge.source_block_id)
.or_insert_with(Vec::new)
.push(target_block_id);
}
Self {
successor_ids_by_block_id,
}
}
fn edge_count(&self) -> usize {
self.successor_ids_by_block_id.values().map(Vec::len).sum()
}
}
impl ControlFlowEdgeGraphV0 for PrunedScssControlFlowGraph {
type BlockId = OmenaScssEvalControlFlowBlockIdV0;
fn entry_block_id(&self) -> Option<Self::BlockId> {
Some(OmenaScssEvalControlFlowBlockIdV0(0))
}
fn successor_block_ids_by_source(&self) -> Vec<(Self::BlockId, Vec<Self::BlockId>)> {
self.successor_ids_by_block_id
.iter()
.map(|(block_id, successor_ids)| (*block_id, successor_ids.clone()))
.collect()
}
}
fn control_flow_edge_survives_prune(
edge: &OmenaScssEvalControlFlowEdgeV0,
truthiness_by_block_id: &BTreeMap<OmenaScssEvalControlFlowBlockIdV0, bool>,
) -> bool {
let Some(truthy) = truthiness_by_block_id.get(&edge.source_block_id).copied() else {
return true;
};
match edge.outcome {
"then" | "body" => truthy,
"else" | "fallthrough" => !truthy,
_ => true,
}
}