use std::collections::BTreeMap;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::ast_graph::{AstGraph, NodeId as AstNodeId};
use crate::SpecError;
pub type ModuleId = u32;
pub type SetterId = u32;
#[derive(
DeriveTataraDomain,
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
)]
#[tatara(keyword = "defmodule-graph-hash")]
#[rkyv(derive(Debug))]
pub struct ModuleGraphHash {
pub bytes: [u8; 32],
}
#[derive(
DeriveTataraDomain,
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[tatara(keyword = "defmodule-graph")]
#[rkyv(derive(Debug))]
pub struct ModuleGraph {
pub schema_version: u32,
pub root_id: ModuleId,
pub modules: Vec<ModuleNode>,
pub canonical_hash: ModuleGraphHash,
}
#[derive(
DeriveTataraDomain,
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[tatara(keyword = "defmodule-node")]
#[rkyv(derive(Debug))]
pub struct ModuleNode {
pub id: ModuleId,
pub label: String,
pub ast_graph_hash: [u8; 32],
pub option_decls: Vec<OptionDecl>,
pub setters: Vec<ConfigSetter>,
pub imports: Vec<ImportEdge>,
pub body_env_prefix: Vec<EnvPrefixBinding>,
}
#[derive(
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[rkyv(derive(Debug))]
pub struct EnvPrefixBinding {
pub name: String,
pub value_node_id: super::ast_graph::NodeId,
pub kind: EnvPrefixKind,
}
#[derive(
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
Copy,
PartialEq,
Eq,
)]
#[rkyv(derive(Debug))]
pub enum EnvPrefixKind {
Let,
With,
}
#[derive(
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[rkyv(derive(Debug))]
pub struct OptionDecl {
pub path: Vec<String>,
pub type_tag: String,
pub has_default: bool,
pub description: Option<String>,
}
#[derive(
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[rkyv(derive(Debug))]
pub struct ConfigSetter {
pub id: SetterId,
pub assigns_path: Vec<String>,
pub slice: Vec<Vec<String>>,
pub body_ast_root: AstNodeId,
pub condition_ast_root: Option<AstNodeId>,
pub priority: u32,
}
#[derive(
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
Debug,
Clone,
PartialEq,
)]
#[rkyv(derive(Debug))]
pub struct ImportEdge {
pub target: ModuleId,
pub condition_ast_root: Option<AstNodeId>,
}
#[derive(Debug, thiserror::Error)]
pub enum ModuleGraphError {
#[error("rkyv archive failed: {0}")]
Archive(String),
#[error("module {label:?} declared an import path that didn't resolve")]
UnresolvedImport { label: String },
#[error("module compiler failed: {source}")]
Compiler {
#[from]
source: crate::module_compiler::ModuleCompilerError,
},
}
impl ModuleGraph {
#[must_use]
pub fn new() -> Self {
Self {
schema_version: SCHEMA_VERSION,
root_id: 0,
modules: Vec::new(),
canonical_hash: ModuleGraphHash { bytes: [0u8; 32] },
}
}
pub fn push_module(&mut self, node: ModuleNode) -> ModuleId {
let id = self.modules.len() as ModuleId;
let mut n = node;
n.id = id;
self.modules.push(n);
id
}
pub fn set_root(&mut self, id: ModuleId) {
self.root_id = id;
}
pub fn from_ast_graphs(modules: &[(String, AstGraph)]) -> Result<Self, ModuleGraphError> {
let mut g = Self::new();
let mut label_to_id: BTreeMap<String, ModuleId> = BTreeMap::new();
for (label, ast) in modules {
let next_id = g.modules.len() as ModuleId;
let node = crate::module_compiler::compile_module(label, ast, next_id)
.map_err(|e| ModuleGraphError::Compiler { source: e })?;
let id = g.push_module(node);
label_to_id.insert(label.clone(), id);
}
Ok(g)
}
pub fn archive_and_hash(mut self) -> Result<(Self, Vec<u8>), ModuleGraphError> {
let initial = rkyv::to_bytes::<rkyv::rancor::Error>(&self)
.map_err(|e| ModuleGraphError::Archive(e.to_string()))?;
let hash = blake3::hash(&initial);
self.canonical_hash = ModuleGraphHash { bytes: hash.into() };
let stamped = rkyv::to_bytes::<rkyv::rancor::Error>(&self)
.map_err(|e| ModuleGraphError::Archive(e.to_string()))?;
Ok((self, stamped.to_vec()))
}
}
impl Default for ModuleGraph {
fn default() -> Self {
Self::new()
}
}
pub const SCHEMA_VERSION: u32 = 1;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defmodule-graph-fixture")]
pub struct ModuleGraphFixture {
pub name: String,
#[serde(rename = "moduleCount")]
pub module_count: u32,
#[serde(rename = "setterCount")]
pub setter_count: u32,
#[serde(rename = "optionCount")]
pub option_count: u32,
#[serde(rename = "sliceCount")]
pub slice_count: u32,
pub notes: String,
}
pub const CANONICAL_MODULE_GRAPH_FIXTURES_LISP: &str =
include_str!("../specs/module_graph.lisp");
pub fn load_fixtures() -> Result<Vec<ModuleGraphFixture>, SpecError> {
crate::loader::load_all::<ModuleGraphFixture>(CANONICAL_MODULE_GRAPH_FIXTURES_LISP)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn ast(src: &str) -> AstGraph {
AstGraph::from_source(src).unwrap()
}
#[test]
fn empty_graph_round_trips() {
let g = ModuleGraph::new();
assert_eq!(g.schema_version, SCHEMA_VERSION);
assert_eq!(g.root_id, 0);
assert!(g.modules.is_empty());
}
#[test]
fn push_module_assigns_dense_ids() {
let mut g = ModuleGraph::new();
let id0 = g.push_module(ModuleNode {
id: 99, label: "root".into(),
ast_graph_hash: [0u8; 32],
option_decls: Vec::new(),
setters: Vec::new(),
imports: Vec::new(),
body_env_prefix: Vec::new(),
});
let id1 = g.push_module(ModuleNode {
id: 99,
label: "child".into(),
ast_graph_hash: [0u8; 32],
option_decls: Vec::new(),
setters: Vec::new(),
imports: Vec::new(),
body_env_prefix: Vec::new(),
});
assert_eq!(id0, 0);
assert_eq!(id1, 1);
assert_eq!(g.modules[0].id, 0);
assert_eq!(g.modules[1].id, 1);
}
#[test]
fn from_ast_graphs_seeds_per_module_hash() {
let a = ast("{ networking.hostName = \"rio\"; }");
let b = ast("{ boot.kernelParams = [ \"amd_pstate=active\" ]; }");
let modules = vec![
("root.nix".to_string(), a.clone()),
("child.nix".to_string(), b.clone()),
];
let g = ModuleGraph::from_ast_graphs(&modules).unwrap();
assert_eq!(g.modules.len(), 2);
assert_eq!(g.modules[0].label, "root.nix");
assert_eq!(g.modules[0].ast_graph_hash, a.canonical_hash.bytes);
assert_eq!(g.modules[1].ast_graph_hash, b.canonical_hash.bytes);
}
#[test]
fn archive_and_hash_stamps_canonical_hash() {
let mut g = ModuleGraph::new();
g.push_module(ModuleNode {
id: 0,
label: "x".into(),
ast_graph_hash: [1u8; 32],
option_decls: Vec::new(),
setters: Vec::new(),
imports: Vec::new(),
body_env_prefix: Vec::new(),
});
assert_eq!(g.canonical_hash.bytes, [0u8; 32]);
let (stamped, bytes) = g.archive_and_hash().unwrap();
assert_ne!(stamped.canonical_hash.bytes, [0u8; 32]);
assert!(!bytes.is_empty());
}
#[test]
fn archive_is_deterministic_for_same_input() {
let mk = || {
let mut g = ModuleGraph::new();
g.push_module(ModuleNode {
id: 0,
label: "y".into(),
ast_graph_hash: [7u8; 32],
option_decls: Vec::new(),
setters: Vec::new(),
imports: Vec::new(),
body_env_prefix: Vec::new(),
});
g
};
let (a, ba) = mk().archive_and_hash().unwrap();
let (b, bb) = mk().archive_and_hash().unwrap();
assert_eq!(a.canonical_hash.bytes, b.canonical_hash.bytes);
assert_eq!(ba, bb);
}
#[test]
fn archive_roundtrips_via_rkyv() {
let mut g = ModuleGraph::new();
g.push_module(ModuleNode {
id: 0,
label: "rt".into(),
ast_graph_hash: [3u8; 32],
option_decls: vec![OptionDecl {
path: vec!["services".into(), "atticd".into(), "enable".into()],
type_tag: "bool".into(),
has_default: true,
description: Some("enable atticd".into()),
}],
setters: vec![ConfigSetter {
id: 0,
assigns_path: vec!["services".into(), "atticd".into(), "enable".into()],
slice: Vec::new(),
body_ast_root: 0,
condition_ast_root: None,
priority: 100,
}],
imports: Vec::new(),
body_env_prefix: Vec::new(),
});
let (_, bytes) = g.archive_and_hash().unwrap();
let archived =
rkyv::access::<ArchivedModuleGraph, rkyv::rancor::Error>(&bytes).unwrap();
assert_eq!(archived.modules.len(), 1);
assert_eq!(archived.modules[0].label.as_str(), "rt");
assert_eq!(archived.modules[0].option_decls.len(), 1);
assert_eq!(archived.modules[0].setters.len(), 1);
}
#[test]
fn fixtures_load_from_lisp() {
let f = load_fixtures().unwrap();
let names: Vec<_> = f.iter().map(|f| f.name.as_str()).collect();
assert!(names.contains(&"single-module-hostname"));
assert!(names.contains(&"two-modules-with-slice"));
assert!(names.contains(&"imports-chain-depth-4"));
}
}