use crate::{
Decision, DecisionId, DecisionStatus, DependencyId, EventTime, Key, Plan, WorkItem, WorkItemId,
WorkKind,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
mod packages;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum Applicability {
Applicable,
Undecided {
decision: Key,
option: String,
},
NotSelected {
decision: Key,
option: String,
selected: String,
},
AwaitingChoice {
predecessor: Key,
},
Stranded {
dependency: DependencyId,
predecessor: Key,
},
EmptyJoin,
AllChildrenExcluded {
decisions: Vec<Key>,
},
ChildrenStranded {
child: Key,
},
}
impl Applicability {
#[must_use]
pub fn is_applicable(&self) -> bool {
matches!(self, Self::Applicable)
}
#[must_use]
pub fn is_not_selected(&self) -> bool {
matches!(
self,
Self::NotSelected { .. } | Self::AllChildrenExcluded { .. }
)
}
#[must_use]
pub fn condition_selected(&self) -> bool {
!matches!(self, Self::NotSelected { .. } | Self::Undecided { .. })
}
}
pub(crate) struct Derived {
pub(crate) states: BTreeMap<WorkItemId, Applicability>,
pub(crate) choice_at: BTreeMap<WorkItemId, EventTime>,
}
pub(crate) fn derive(plan: &Plan) -> Derived {
derive_indexed(plan, &crate::graph_index::GraphIndex::new(plan))
}
pub(crate) fn derive_indexed(plan: &Plan, index: &crate::graph_index::GraphIndex) -> Derived {
let mut derived = Derived {
states: BTreeMap::new(),
choice_at: BTreeMap::new(),
};
for work in plan.work_items.values() {
let (state, at) = conditions(plan, work);
if let Some(at) = at {
derived.choice_at.insert(work.id, at);
}
derived.states.insert(work.id, state);
}
for id in dependency_order(plan) {
let Some(work) = plan.work_items.get(&id) else {
continue;
};
let finished = work.is_executable() && work.execution.status.satisfies_dependency();
if work.kind == WorkKind::WorkPackage
|| finished
|| !derived
.states
.get(&id)
.is_some_and(Applicability::is_applicable)
{
continue;
}
let state = through_constraints(plan, index, work, &derived.states);
derived.states.insert(id, state);
}
packages::derive(plan, index, &mut derived);
derived
}
impl Plan {
#[must_use]
pub fn applicability(&self) -> BTreeMap<WorkItemId, Applicability> {
derive(self).states
}
#[must_use]
pub fn effective_decision(&self, id: DecisionId) -> Option<&Decision> {
effective_decision(self, id)
}
}
pub(crate) fn effective_decision(plan: &Plan, id: DecisionId) -> Option<&Decision> {
let mut current = plan.decisions.get(&id)?;
let mut steps = 0_usize;
while let Some(next) = plan
.decisions
.values()
.find(|d| d.supersedes == Some(current.id))
{
current = next;
steps = steps.wrapping_add(1);
if steps > plan.decisions.len() {
break;
}
}
Some(current)
}
pub(crate) fn standing_since(plan: &Plan, decision: &Decision) -> Option<EventTime> {
if decision.status != DecisionStatus::Decided {
return None;
}
let mut since = own_time(decision);
let mut current = decision;
let mut steps = 0_usize;
while let Some(previous) = current.supersedes.and_then(|id| plan.decisions.get(&id)) {
steps = steps.wrapping_add(1);
if previous.outcome != decision.outcome || steps > plan.decisions.len() {
break;
}
since = earliest(since, own_time(previous));
current = previous;
}
Some(since)
}
fn own_time(decision: &Decision) -> EventTime {
decision
.resolved_at
.map_or(EventTime::Unrecorded, EventTime::Recorded)
}
fn earliest(a: EventTime, b: EventTime) -> EventTime {
match (a, b) {
(EventTime::Recorded(a), EventTime::Recorded(b)) => EventTime::Recorded(a.min(b)),
_ => EventTime::Unrecorded,
}
}
fn conditions(plan: &Plan, work: &WorkItem) -> (Applicability, Option<EventTime>) {
let mut undecided = None;
let mut decided: Option<EventTime> = None;
let mut next = Some(work);
let mut depth = 0_usize;
while let Some(item) = next {
if let Some(condition) = &item.contract.condition
&& let Some(decision) = effective_decision(plan, condition.decision)
{
match (standing_since(plan, decision), decision.outcome.as_deref()) {
(Some(at), Some(selected)) if selected != condition.option => {
let state = Applicability::NotSelected {
decision: decision.key.clone(),
option: condition.option.clone(),
selected: selected.into(),
};
return (state, Some(at));
}
(Some(at), Some(_)) => decided = Some(decided.map_or(at, |d| d.latest(at))),
_ => {
undecided.get_or_insert_with(|| Applicability::Undecided {
decision: decision.key.clone(),
option: condition.option.clone(),
});
}
}
}
depth = depth.wrapping_add(1);
next = item
.parent
.and_then(|id| plan.work_items.get(&id))
.filter(|_| depth <= plan.work_items.len());
}
match undecided {
Some(state) => (state, None),
None => (Applicability::Applicable, decided),
}
}
fn through_constraints(
plan: &Plan,
index: &crate::graph_index::GraphIndex,
work: &WorkItem,
states: &BTreeMap<WorkItemId, Applicability>,
) -> Applicability {
let mut awaiting = None;
let (mut incoming, mut active) = (0_usize, 0_usize);
for edge in index
.incoming
.get(&work.id)
.into_iter()
.flatten()
.filter_map(|i| plan.dependencies.get(*i))
{
incoming = incoming.wrapping_add(1);
let Some(predecessor) = plan.work_items.get(&edge.predecessor) else {
continue;
};
match states.get(&edge.predecessor) {
Some(state) if state.is_not_selected() && work.contract.join.skips_unselected() => {}
Some(
Applicability::NotSelected { .. }
| Applicability::AllChildrenExcluded { .. }
| Applicability::Stranded { .. }
| Applicability::ChildrenStranded { .. }
| Applicability::EmptyJoin,
) => {
return Applicability::Stranded {
dependency: edge.id,
predecessor: predecessor.key.clone(),
};
}
Some(Applicability::Undecided { .. } | Applicability::AwaitingChoice { .. }) => {
awaiting.get_or_insert_with(|| predecessor.key.clone());
active = active.wrapping_add(1);
}
Some(Applicability::Applicable) | None => active = active.wrapping_add(1),
}
}
if let Some(predecessor) = awaiting {
return Applicability::AwaitingChoice { predecessor };
}
let permits_empty = matches!(
work.contract.join,
crate::JoinPolicy::ActiveBranches { allow_empty: true }
);
if work.contract.join.skips_unselected() && incoming > 0 && active == 0 && !permits_empty {
return Applicability::EmptyJoin;
}
Applicability::Applicable
}
fn dependency_order(plan: &Plan) -> Vec<WorkItemId> {
let mut indegree: BTreeMap<_, usize> = plan.work_items.keys().map(|id| (*id, 0)).collect();
let mut outgoing: BTreeMap<_, Vec<_>> = BTreeMap::new();
for edge in &plan.dependencies {
if let Some(count) = indegree.get_mut(&edge.successor) {
*count = count.wrapping_add(1);
outgoing
.entry(edge.predecessor)
.or_default()
.push(edge.successor);
}
}
let mut queue: VecDeque<_> = indegree
.iter()
.filter_map(|(id, n)| (*n == 0).then_some(*id))
.collect();
let mut order = Vec::with_capacity(plan.work_items.len());
while let Some(id) = queue.pop_front() {
order.push(id);
for child in outgoing.get(&id).into_iter().flatten() {
if let Some(count) = indegree.get_mut(child) {
*count = count.saturating_sub(1);
if *count == 0 {
queue.push_back(*child);
}
}
}
}
order
}
#[cfg(test)]
mod tests;