use std::path::PathBuf;
use cabin_core::standard_compatibility::{
DependencyAttributes, EffectiveRequirements, ReqOfSource, Requirement, req_of_c_with_source,
req_of_cxx_with_source,
};
use cabin_core::{CStandard, CxxStandard};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TargetNode {
pub name: String,
pub manifest_path: PathBuf,
pub attributes: DependencyAttributes,
pub sites: DeclarationSites,
pub deps: Vec<TargetEdge>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TargetEdge {
pub to: usize,
pub public: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclarationSite {
pub manifest_path: PathBuf,
pub span: Option<miette::SourceSpan>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeclarationSites {
pub decl_c: Option<DeclarationSite>,
pub decl_cxx: Option<DeclarationSite>,
pub impl_c: Option<DeclarationSite>,
pub impl_cxx: Option<DeclarationSite>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectiveTarget {
pub c: Effective<CStandard>,
pub cxx: Effective<CxxStandard>,
}
impl EffectiveTarget {
#[must_use]
pub fn requirements(&self) -> EffectiveRequirements {
EffectiveRequirements {
c: self.c.requirement,
cxx: self.cxx.requirement,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Effective<S> {
pub requirement: Requirement<S>,
pub attained: Attained,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Attained {
Own(Origin),
Via(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Origin {
pub source: ReqOfSource,
pub site: DeclarationSite,
}
#[must_use]
pub fn effective_requirements(targets: &[TargetNode]) -> Vec<EffectiveTarget> {
let mut results: Vec<Option<EffectiveTarget>> = vec![None; targets.len()];
for index in topo_order(targets) {
let node = &targets[index];
let (own_c, source_c) = req_of_c_with_source(&node.attributes);
let (own_cxx, source_cxx) = req_of_cxx_with_source(&node.attributes);
let computed = EffectiveTarget {
c: compose(
node,
own_c,
source_c,
node.sites.decl_c.as_ref(),
node.sites.impl_c.as_ref(),
&results,
|dep| &dep.c,
),
cxx: compose(
node,
own_cxx,
source_cxx,
node.sites.decl_cxx.as_ref(),
node.sites.impl_cxx.as_ref(),
&results,
|dep| &dep.cxx,
),
};
results[index] = Some(computed);
}
results
.into_iter()
.map(|result| result.expect("topological order covers every target"))
.collect()
}
#[must_use]
pub fn provenance_c(results: &[EffectiveTarget], target: usize) -> Provenance<'_> {
provenance(results, target, |dep| &dep.c.attained)
}
#[must_use]
pub fn provenance_cxx(results: &[EffectiveTarget], target: usize) -> Provenance<'_> {
provenance(results, target, |dep| &dep.cxx.attained)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Provenance<'a> {
pub path: Vec<usize>,
pub origin: &'a Origin,
}
fn provenance(
results: &[EffectiveTarget],
target: usize,
attained: impl Fn(&EffectiveTarget) -> &Attained,
) -> Provenance<'_> {
let mut path = vec![target];
let mut current = target;
loop {
match attained(&results[current]) {
Attained::Own(origin) => return Provenance { path, origin },
Attained::Via(next) => {
current = *next;
path.push(current);
}
}
}
}
fn compose<S: Copy + Ord>(
node: &TargetNode,
own: Requirement<S>,
own_source: ReqOfSource,
decl_site: Option<&DeclarationSite>,
impl_site: Option<&DeclarationSite>,
results: &[Option<EffectiveTarget>],
of: impl Fn(&EffectiveTarget) -> &Effective<S>,
) -> Effective<S> {
let mut requirement = own;
let mut via: Option<usize> = None;
for edge in &node.deps {
if !edge.public {
continue;
}
let dep = of(results[edge.to]
.as_ref()
.expect("topological order visits dependencies first"));
if dep.requirement > requirement {
requirement = dep.requirement;
via = Some(edge.to);
}
}
let attained = match via {
Some(to) => Attained::Via(to),
None => Attained::Own(origin(node, own_source, decl_site, impl_site)),
};
Effective {
requirement,
attained,
}
}
fn origin(
node: &TargetNode,
source: ReqOfSource,
decl_site: Option<&DeclarationSite>,
impl_site: Option<&DeclarationSite>,
) -> Origin {
let site = match source {
ReqOfSource::DeclaredNone | ReqOfSource::Declared => decl_site,
ReqOfSource::HeaderOnlyInference => impl_site,
ReqOfSource::CompiledNoDeclaration | ReqOfSource::CrossLanguageDefault => None,
};
Origin {
source,
site: site.cloned().unwrap_or_else(|| DeclarationSite {
manifest_path: node.manifest_path.clone(),
span: None,
}),
}
}
fn topo_order(targets: &[TargetNode]) -> Vec<usize> {
#[derive(Clone, Copy)]
enum Color {
Visiting,
Done,
}
fn visit(
node: usize,
targets: &[TargetNode],
state: &mut [Option<Color>],
order: &mut Vec<usize>,
) {
match state[node] {
Some(Color::Done) => return,
Some(Color::Visiting) => panic!(
"public dependency cycle through `{}`: the target graph must be a DAG (spec D5)",
targets[node].name
),
None => {}
}
state[node] = Some(Color::Visiting);
for edge in &targets[node].deps {
if edge.public {
visit(edge.to, targets, state, order);
}
}
state[node] = Some(Color::Done);
order.push(node);
}
let mut state = vec![None; targets.len()];
let mut order = Vec::with_capacity(targets.len());
for index in 0..targets.len() {
visit(index, targets, &mut state, &mut order);
}
order
}
#[cfg(test)]
mod tests {
use super::*;
use cabin_core::standard_compatibility::DependencyKind;
use cabin_core::{InterfaceRequirement, StandardRequirement};
fn interface_min<S>(min: S) -> InterfaceRequirement<S> {
InterfaceRequirement::Requirement(StandardRequirement { min, max: None })
}
fn compiled(impl_c: Option<CStandard>, impl_cxx: Option<CxxStandard>) -> DependencyAttributes {
DependencyAttributes {
kind: DependencyKind::Compiled,
impl_c,
impl_cxx,
decl_c: None,
decl_cxx: None,
}
}
fn site(path: &str, offset: usize) -> DeclarationSite {
DeclarationSite {
manifest_path: PathBuf::from(path),
span: Some(miette::SourceSpan::new(offset.into(), 8)),
}
}
fn node(name: &str, attributes: DependencyAttributes, deps: &[(usize, bool)]) -> TargetNode {
TargetNode {
name: name.to_owned(),
manifest_path: PathBuf::from(format!("{name}/cabin.toml")),
attributes,
sites: DeclarationSites::default(),
deps: deps
.iter()
.map(|&(to, public)| TargetEdge { to, public })
.collect(),
}
}
#[test]
fn linear_chain_propagates_declarations_with_provenance() {
let mut b = node(
"b:b",
compiled(Some(CStandard::C17), Some(CxxStandard::Cxx23)),
&[],
);
b.attributes.decl_c = Some(interface_min(CStandard::C17));
b.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx20));
b.sites.decl_c = Some(site("b/cabin.toml", 100));
b.sites.decl_cxx = Some(site("b/cabin.toml", 200));
let targets = vec![
node(
"root:root",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(1, true)],
),
node(
"a:a",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(2, true)],
),
b,
];
let results = effective_requirements(&targets);
assert_eq!(
results[0].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
assert_eq!(results[0].c.requirement, Requirement::Min(CStandard::C17));
assert_eq!(
results[1].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
let cxx = provenance_cxx(&results, 0);
assert_eq!(cxx.path, [0, 1, 2]);
assert_eq!(cxx.origin.source, ReqOfSource::Declared);
assert_eq!(cxx.origin.site, site("b/cabin.toml", 200));
let c = provenance_c(&results, 0);
assert_eq!(c.path, [0, 1, 2]);
assert_eq!(c.origin.source, ReqOfSource::Declared);
assert_eq!(c.origin.site, site("b/cabin.toml", 100));
assert_eq!(targets[*cxx.path.last().unwrap()].name, "b:b");
}
#[test]
fn diamond_joins_once_and_keeps_one_chain() {
let mut a = node(
"a:a",
compiled(Some(CStandard::C11), Some(CxxStandard::Cxx23)),
&[(3, true)],
);
a.attributes.decl_c = Some(interface_min(CStandard::C99));
a.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx17));
let mut z = node(
"z:z",
compiled(Some(CStandard::C11), Some(CxxStandard::Cxx23)),
&[],
);
z.attributes.decl_c = Some(interface_min(CStandard::C11));
z.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx20));
let targets = vec![
node(
"root:root",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(1, true), (2, true)],
),
a,
node(
"b:b",
compiled(Some(CStandard::C11), Some(CxxStandard::Cxx23)),
&[(3, true)],
),
z,
];
let results = effective_requirements(&targets);
assert_eq!(
results[0].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
assert_eq!(results[0].c.requirement, Requirement::Min(CStandard::C11));
assert_eq!(provenance_cxx(&results, 0).path, [0, 1, 3]);
assert_eq!(provenance_c(&results, 0).path, [0, 1, 3]);
assert_eq!(
results[1].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
assert_eq!(provenance_cxx(&results, 1).path, [1, 3]);
}
#[test]
fn own_declaration_wins_ties_over_dependencies() {
let mut root = node(
"root:root",
compiled(None, Some(CxxStandard::Cxx23)),
&[(1, true)],
);
root.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx20));
root.sites.decl_cxx = Some(site("root/cabin.toml", 40));
let mut dep = node("dep:dep", compiled(None, Some(CxxStandard::Cxx23)), &[]);
dep.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx20));
let targets = vec![root, dep];
let results = effective_requirements(&targets);
let cxx = provenance_cxx(&results, 0);
assert_eq!(
results[0].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
assert_eq!(cxx.path, [0]);
assert_eq!(cxx.origin.site, site("root/cabin.toml", 40));
}
#[test]
fn private_edges_do_not_propagate() {
let mut b = node(
"b:b",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[],
);
b.attributes.decl_cxx = Some(InterfaceRequirement::None);
b.attributes.decl_c = Some(interface_min(CStandard::C23));
let targets = vec![
node(
"root:root",
compiled(None, Some(CxxStandard::Cxx17)),
&[(1, true)],
),
node(
"a:a",
compiled(None, Some(CxxStandard::Cxx17)),
&[(2, false)],
),
b,
];
let results = effective_requirements(&targets);
assert_eq!(results[2].cxx.requirement, Requirement::Forbidden);
assert_eq!(results[1].cxx.requirement, Requirement::Unconstrained);
assert_eq!(results[0].cxx.requirement, Requirement::Unconstrained);
assert_eq!(results[1].c.requirement, Requirement::Forbidden);
assert_eq!(provenance_cxx(&results, 1).path, [1]);
assert_eq!(
provenance_cxx(&results, 0).origin.source,
ReqOfSource::CompiledNoDeclaration
);
}
#[test]
fn declared_none_poisons_public_ancestors_with_provenance() {
let mut b = node("b:b", compiled(None, Some(CxxStandard::Cxx17)), &[]);
b.attributes.decl_cxx = Some(InterfaceRequirement::None);
b.sites.decl_cxx = Some(site("b/cabin.toml", 64));
let targets = vec![
node(
"root:root",
compiled(None, Some(CxxStandard::Cxx26)),
&[(1, true)],
),
node(
"a:a",
compiled(None, Some(CxxStandard::Cxx17)),
&[(2, true)],
),
b,
];
let results = effective_requirements(&targets);
assert_eq!(results[0].cxx.requirement, Requirement::Forbidden);
assert_eq!(results[1].cxx.requirement, Requirement::Forbidden);
let cxx = provenance_cxx(&results, 0);
assert_eq!(cxx.path, [0, 1, 2]);
assert_eq!(cxx.origin.source, ReqOfSource::DeclaredNone);
assert_eq!(cxx.origin.site, site("b/cabin.toml", 64));
}
#[test]
fn strict_cxx_to_c_default_poisons_c_ancestors() {
let targets = vec![
node(
"root:root",
compiled(Some(CStandard::C17), None),
&[(1, true)],
),
node(
"mid:mid",
compiled(Some(CStandard::C17), None),
&[(2, true)],
),
node(
"cxxlib:cxxlib",
compiled(None, Some(CxxStandard::Cxx20)),
&[],
),
];
let results = effective_requirements(&targets);
assert_eq!(results[0].c.requirement, Requirement::Forbidden);
let c = provenance_c(&results, 0);
assert_eq!(c.path, [0, 1, 2]);
assert_eq!(c.origin.source, ReqOfSource::CrossLanguageDefault);
assert_eq!(
c.origin.site,
DeclarationSite {
manifest_path: PathBuf::from("cxxlib:cxxlib/cabin.toml"),
span: None,
}
);
assert_eq!(results[1].cxx.requirement, Requirement::Unconstrained);
}
#[test]
fn header_only_inference_feeds_composition() {
let mut h = TargetNode {
name: "h:h".to_owned(),
manifest_path: PathBuf::from("h/cabin.toml"),
attributes: DependencyAttributes {
kind: DependencyKind::HeaderOnly,
impl_c: Some(CStandard::C17),
impl_cxx: Some(CxxStandard::Cxx20),
decl_c: None,
decl_cxx: None,
},
sites: DeclarationSites::default(),
deps: Vec::new(),
};
h.sites.impl_c = Some(site("h/cabin.toml", 10));
h.sites.impl_cxx = Some(site("h/cabin.toml", 20));
let targets = vec![
node(
"root:root",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(1, true)],
),
node(
"a:a",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(2, true)],
),
h,
];
let results = effective_requirements(&targets);
assert_eq!(
results[0].cxx.requirement,
Requirement::Min(CxxStandard::Cxx20)
);
assert_eq!(results[0].c.requirement, Requirement::Min(CStandard::C17));
let cxx = provenance_cxx(&results, 0);
assert_eq!(cxx.path, [0, 1, 2]);
assert_eq!(cxx.origin.source, ReqOfSource::HeaderOnlyInference);
assert_eq!(cxx.origin.site, site("h/cabin.toml", 20));
assert_eq!(
provenance_c(&results, 0).origin.site,
site("h/cabin.toml", 10)
);
}
#[test]
fn confluence_across_two_topological_orders() {
fn build(mapping: [usize; 5]) -> Vec<TargetNode> {
let [root, a, b, z, w] = mapping;
let mut nodes = vec![node("placeholder", compiled(None, None), &[]); 5];
nodes[root] = node(
"root:root",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(a, true), (b, true)],
);
nodes[a] = node(
"a:a",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(z, true)],
);
nodes[b] = node(
"b:b",
compiled(Some(CStandard::C23), Some(CxxStandard::Cxx23)),
&[(z, true)],
);
nodes[z] = {
let mut z_node = node(
"z:z",
compiled(Some(CStandard::C11), Some(CxxStandard::Cxx20)),
&[(w, true)],
);
z_node.attributes.decl_c = Some(interface_min(CStandard::C11));
z_node.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx20));
z_node
};
nodes[w] = {
let mut w_node = node(
"w:w",
compiled(Some(CStandard::C99), Some(CxxStandard::Cxx23)),
&[],
);
w_node.attributes.decl_c = Some(interface_min(CStandard::C99));
w_node.attributes.decl_cxx = Some(interface_min(CxxStandard::Cxx23));
w_node
};
nodes
}
let forward: [usize; 5] = [0, 1, 2, 3, 4];
let backward: [usize; 5] = [4, 3, 2, 1, 0];
let forward_results = effective_requirements(&build(forward));
let backward_results = effective_requirements(&build(backward));
for logical in 0..5 {
let fwd = &forward_results[forward[logical]];
let bwd = &backward_results[backward[logical]];
assert_eq!(
fwd.requirements(),
bwd.requirements(),
"T1 confluence at logical node {logical}"
);
}
let map_back = |path: &[usize], mapping: [usize; 5]| -> Vec<usize> {
path.iter()
.map(|&index| mapping.iter().position(|&m| m == index).unwrap())
.collect()
};
let fwd_chain = map_back(&provenance_cxx(&forward_results, forward[0]).path, forward);
let bwd_chain = map_back(
&provenance_cxx(&backward_results, backward[0]).path,
backward,
);
assert_eq!(fwd_chain, bwd_chain);
assert_eq!(fwd_chain, [0, 1, 3, 4]);
}
}