mod replan;
pub use replan::{ReplanError, Replanner};
use std::collections::{BTreeMap, BTreeSet};
use crate::core::{ArgSource, Capability, Collaboration, PlanError, PlanIR, StepId, Topology};
#[derive(Debug, Clone, Default)]
pub struct Contract {
pub provided: BTreeSet<Capability>,
pub require_verifier: bool,
pub max_steps: Option<usize>,
}
impl Contract {
#[must_use]
pub fn new(provided: impl IntoIterator<Item = Capability>) -> Self {
Self {
provided: provided.into_iter().collect(),
require_verifier: false,
max_steps: None,
}
}
#[must_use]
pub fn require_verifier(mut self) -> Self {
self.require_verifier = true;
self
}
#[must_use]
pub fn max_steps(mut self, n: usize) -> Self {
self.max_steps = Some(n);
self
}
}
pub fn validate(plan: &PlanIR, contract: &Contract) -> Result<(), PlanError> {
if plan.nodes.is_empty() {
return Err(PlanError::Empty);
}
if let Some(max) = contract.max_steps
&& plan.nodes.len() > max
{
return Err(PlanError::TooManySteps {
steps: plan.nodes.len(),
allowed: max,
});
}
let mut seen = BTreeSet::new();
for n in &plan.nodes {
if !seen.insert(n.id) {
return Err(PlanError::DuplicateStep(n.id));
}
}
check_dependencies(plan)?;
check_acyclic(plan)?;
check_terminals(plan)?;
check_capabilities(plan, contract)?;
check_arguments(plan)?;
check_verifiers(plan, contract)?;
check_topology(plan)?;
Ok(())
}
fn check_dependencies(plan: &PlanIR) -> Result<(), PlanError> {
let ids: BTreeSet<StepId> = plan.nodes.iter().map(|n| n.id).collect();
for n in &plan.nodes {
for d in &n.depends_on {
if !ids.contains(d) {
return Err(PlanError::MissingDependency {
step: n.id,
missing: *d,
});
}
}
}
Ok(())
}
#[derive(Clone, Copy, PartialEq)]
enum Mark {
Open,
Closed,
}
fn check_acyclic(plan: &PlanIR) -> Result<(), PlanError> {
let deps: BTreeMap<StepId, &[StepId]> = plan
.nodes
.iter()
.map(|n| (n.id, n.depends_on.as_slice()))
.collect();
let mut marks: BTreeMap<StepId, Mark> = BTreeMap::new();
let mut stack: Vec<(StepId, usize)> = Vec::new();
for root in plan.nodes.iter().map(|n| n.id) {
if marks.get(&root) == Some(&Mark::Closed) {
continue;
}
marks.insert(root, Mark::Open);
stack.push((root, 0));
while let Some(&mut (id, ref mut next)) = stack.last_mut() {
let children = deps.get(&id).copied().unwrap_or(&[]);
let Some(&child) = children.get(*next) else {
marks.insert(id, Mark::Closed);
stack.pop();
continue;
};
*next += 1;
match marks.get(&child) {
Some(Mark::Closed) => {}
Some(Mark::Open) => return Err(PlanError::Cycle(child)),
None => {
marks.insert(child, Mark::Open);
stack.push((child, 0));
}
}
}
}
Ok(())
}
fn check_terminals(plan: &PlanIR) -> Result<(), PlanError> {
if !plan.nodes.iter().any(|n| n.terminal) {
return Err(PlanError::NoTerminal);
}
let depended: BTreeSet<StepId> = plan
.nodes
.iter()
.flat_map(|n| n.depends_on.iter().copied())
.collect();
for n in &plan.nodes {
if !n.terminal && !depended.contains(&n.id) {
return Err(PlanError::Unreachable { step: n.id });
}
}
Ok(())
}
fn check_capabilities(plan: &PlanIR, contract: &Contract) -> Result<(), PlanError> {
for n in &plan.nodes {
if !contract.provided.contains(&n.capability) {
return Err(PlanError::NoProvider {
step: n.id,
capability: n.capability.to_string(),
});
}
}
Ok(())
}
fn check_arguments(plan: &PlanIR) -> Result<(), PlanError> {
for n in &plan.nodes {
if n.args.is_empty() {
return Err(PlanError::NoArguments { step: n.id });
}
for (name, source) in &n.args {
if let ArgSource::Node { step, .. } = source
&& !n.depends_on.contains(step)
{
return Err(PlanError::ArgumentNotUpstream {
step: n.id,
arg: name.clone(),
from_step: *step,
});
}
}
}
Ok(())
}
fn check_verifiers(plan: &PlanIR, contract: &Contract) -> Result<(), PlanError> {
for n in plan.nodes.iter().filter(|n| n.verifies) {
if n.depends_on.is_empty() {
return Err(PlanError::VerifierWithoutSubject { step: n.id });
}
}
if contract.require_verifier && !plan.nodes.iter().any(|n| n.verifies) {
return Err(PlanError::VerifierRequired);
}
Ok(())
}
fn check_topology(plan: &PlanIR) -> Result<(), PlanError> {
let Topology::Collaborative(reason) = plan.topology else {
return Ok(());
};
match reason {
Collaboration::ParallelDisjoint => {
let mut sources: BTreeMap<String, StepId> = BTreeMap::new();
for n in &plan.nodes {
for source in n.args.values() {
let fingerprint = match source {
ArgSource::RunInput { field } => {
format!("input:{}", field.as_deref().unwrap_or("*"))
}
ArgSource::Node { step, field } => {
format!("node:{}:{}", step.0, field.as_deref().unwrap_or("*"))
}
ArgSource::Const { .. } => continue,
};
if let Some(other) = sources.insert(fingerprint, n.id)
&& other != n.id
{
return Err(PlanError::FalseParallelism { a: other, b: n.id });
}
}
}
}
Collaboration::DistinctAuthority => {
let caps: BTreeSet<&Capability> = plan.nodes.iter().map(|n| &n.capability).collect();
if caps.len() <= 1
&& let Some(only) = caps.into_iter().next()
{
return Err(PlanError::NoAuthorityToSeparate {
capability: only.to_string(),
});
}
}
}
Ok(())
}