use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defsubstrate-domain")]
pub struct SubstrateDomain {
pub name: String,
#[serde(rename = "authoringKeywords")]
pub authoring_keywords: Vec<String>,
#[serde(rename = "gate")]
pub gate: MaturityGate,
pub purpose: String,
#[serde(rename = "cppnixMirror")]
pub cppnix_mirror: String,
#[serde(default, rename = "dependsOn")]
pub depends_on: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MaturityGate {
Working,
M2TypedOnly,
M3TypedOnly,
M4TypedOnly,
Informational,
}
pub const CANONICAL_CATALOG_LISP: &str = include_str!("../specs/catalog.lisp");
pub fn load_canonical() -> Result<Vec<SubstrateDomain>, SpecError> {
crate::loader::load_all::<SubstrateDomain>(CANONICAL_CATALOG_LISP)
}
pub fn lookup(name: &str) -> Result<SubstrateDomain, SpecError> {
load_canonical()?
.into_iter()
.find(|d| d.name == name)
.ok_or_else(|| SpecError::Load(format!(
"no (defsubstrate-domain) with :name {name:?}",
)))
}
pub fn maturity_histogram() -> Result<std::collections::BTreeMap<&'static str, usize>, SpecError> {
let cat = load_canonical()?;
let mut h: std::collections::BTreeMap<&'static str, usize> =
std::collections::BTreeMap::new();
for d in &cat {
let key = match d.gate {
MaturityGate::Working => "Working",
MaturityGate::M2TypedOnly => "M2TypedOnly",
MaturityGate::M3TypedOnly => "M3TypedOnly",
MaturityGate::M4TypedOnly => "M4TypedOnly",
MaturityGate::Informational => "Informational",
};
*h.entry(key).or_default() += 1;
}
Ok(h)
}
pub fn topological_order() -> Result<Vec<SubstrateDomain>, SpecError> {
let cat = load_canonical()?;
let by_name: std::collections::HashMap<String, SubstrateDomain> =
cat.iter().cloned().map(|d| (d.name.clone(), d)).collect();
let names: Vec<&str> = cat.iter().map(|d| d.name.as_str()).collect();
let mut indeg: std::collections::HashMap<&str, usize> =
names.iter().map(|n| (*n, 0)).collect();
for d in &cat {
for dep in &d.depends_on {
*indeg.entry(d.name.as_str()).or_default() += 1;
indeg.entry(dep.as_str()).or_default();
}
}
let mut frontier: Vec<&str> = indeg
.iter()
.filter(|(_, deg)| **deg == 0)
.map(|(name, _)| *name)
.collect();
frontier.sort();
let mut out: Vec<SubstrateDomain> = Vec::new();
while let Some(name) = frontier.pop() {
let domain = by_name.get(name).cloned().ok_or_else(|| {
SpecError::Load(format!("internal: topological_order saw unknown `{name}`"))
})?;
out.push(domain);
let children: Vec<&str> = cat
.iter()
.filter(|d| d.depends_on.iter().any(|x| x == name))
.map(|d| d.name.as_str())
.collect();
for child in children {
let entry = indeg.entry(child).or_default();
*entry = entry.saturating_sub(1);
if *entry == 0 {
frontier.push(child);
frontier.sort();
}
}
}
if out.len() != cat.len() {
return Err(SpecError::Interp {
phase: "dependency-cycle".into(),
message: format!(
"topological_order produced {} of {} entries — cycle present",
out.len(),
cat.len(),
),
});
}
Ok(out)
}
pub fn transitive_dependencies(name: &str) -> Result<std::collections::BTreeSet<String>, SpecError> {
let cat = load_canonical()?;
let by_name: std::collections::HashMap<&str, &SubstrateDomain> =
cat.iter().map(|d| (d.name.as_str(), d)).collect();
if !by_name.contains_key(name) {
return Err(SpecError::Load(format!("domain `{name}` not in catalog")));
}
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut stack: Vec<String> = vec![name.to_string()];
let mut in_path: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
while let Some(current) = stack.pop() {
if !seen.insert(current.clone()) {
continue;
}
if !in_path.insert(current.clone()) {
return Err(SpecError::Interp {
phase: "dependency-cycle".into(),
message: format!("cycle detected involving `{current}`"),
});
}
let Some(domain) = by_name.get(current.as_str()) else {
return Err(SpecError::Load(format!(
"domain `{current}` referenced but not in catalog",
)));
};
for dep in &domain.depends_on {
stack.push(dep.clone());
}
}
Ok(seen)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn catalog_parses() {
let cat = load_canonical().expect("catalog must compile");
assert!(
cat.len() >= 15,
"catalog must enumerate at least 15 domains, got {}",
cat.len(),
);
}
#[test]
fn every_authored_domain_is_in_catalog() {
let cat = load_canonical().unwrap();
let names: HashSet<&str> = cat.iter().map(|d| d.name.as_str()).collect();
for required in [
"derivation",
"realisation",
"module_system",
"activation_script",
"flake",
"lock_file",
"registry",
"fetcher",
"substituter",
"sandbox",
"store_layout",
"gc",
"hash",
"nar",
"narinfo",
"eval_cache",
"profile",
"trust_model",
"worker_protocol",
] {
assert!(
names.contains(required),
"catalog missing domain `{required}` — sui-spec/src/{required}.rs \
exists but its catalog entry doesn't",
);
}
}
#[test]
fn maturity_histogram_sums_to_catalog_size() {
let cat = load_canonical().unwrap();
let h = maturity_histogram().unwrap();
let total: usize = h.values().sum();
assert_eq!(total, cat.len());
}
#[test]
fn working_domains_include_the_known_three() {
let cat = load_canonical().unwrap();
let working: HashSet<&str> = cat
.iter()
.filter(|d| d.gate == MaturityGate::Working)
.map(|d| d.name.as_str())
.collect();
for required in ["derivation", "flake"] {
assert!(
working.contains(required),
"catalog: {required} should be Working — substrate has the impl today",
);
}
}
#[test]
fn lookup_finds_known_domain() {
let d = lookup("derivation").expect("derivation must be in catalog");
assert_eq!(d.name, "derivation");
assert!(d.purpose.len() > 10);
}
#[test]
fn lookup_errors_on_missing() {
let err = lookup("nonexistent-domain").expect_err("must error on unknown");
match err {
SpecError::Load(msg) => assert!(msg.contains("nonexistent-domain")),
_ => panic!("expected SpecError::Load"),
}
}
#[test]
fn transitive_dependencies_of_activation_includes_module_system() {
let deps = transitive_dependencies("activation_script").unwrap();
for required in [
"activation_script", "module_system",
"derivation",
"hash",
"store_layout",
] {
assert!(
deps.contains(required),
"transitive deps missing `{required}`: {deps:?}",
);
}
}
#[test]
fn transitive_dependencies_of_hash_is_just_itself() {
let deps = transitive_dependencies("hash").unwrap();
assert_eq!(deps.len(), 1);
assert!(deps.contains("hash"));
}
#[test]
fn every_declared_dependency_exists_in_catalog() {
let cat = load_canonical().unwrap();
let names: std::collections::HashSet<&str> =
cat.iter().map(|d| d.name.as_str()).collect();
for d in &cat {
for dep in &d.depends_on {
assert!(
names.contains(dep.as_str()),
"domain `{}` declares dependency on `{dep}`, \
which is not in the catalog",
d.name,
);
}
}
}
#[test]
fn substrate_graph_has_no_cycles() {
let cat = load_canonical().unwrap();
for d in &cat {
let _ = transitive_dependencies(&d.name)
.unwrap_or_else(|e| panic!("cycle starting from `{}`: {e:?}", d.name));
}
}
#[test]
fn topological_order_covers_catalog() {
let topo = topological_order().unwrap();
let cat = load_canonical().unwrap();
assert_eq!(topo.len(), cat.len(), "topo skipped some domains");
let names: std::collections::HashSet<&str> =
topo.iter().map(|d| d.name.as_str()).collect();
for c in &cat {
assert!(names.contains(c.name.as_str()), "missing `{}` in topo", c.name);
}
}
#[test]
fn topological_order_respects_dependencies() {
let topo = topological_order().unwrap();
let pos: std::collections::HashMap<&str, usize> = topo
.iter()
.enumerate()
.map(|(i, d)| (d.name.as_str(), i))
.collect();
for d in &topo {
for dep in &d.depends_on {
assert!(
pos[dep.as_str()] < pos[d.name.as_str()],
"topo violates: `{}` (pos {}) depends on `{dep}` (pos {})",
d.name,
pos[d.name.as_str()],
pos[dep.as_str()],
);
}
}
}
}