Skip to main content

sim/runtime/
cookbook_directory.rs

1//! sim-nest's product cookbook loadable-lib directory.
2//!
3//! `sim-lib-cookbook` keeps a small standalone fixture directory. The umbrella
4//! crate owns the product directory because only this crate sees the
5//! constellation feature graph without adding back-edges to the cookbook lib.
6
7use std::sync::Arc;
8
9use sim_cookbook::EmbeddedDir;
10use sim_kernel::{CodecId, Lib};
11use sim_lib_cookbook::{
12    ConfigProvider, CookbookConfig, LoadableLibConfig, LoadableLibList, LoadableLibResolver,
13    ResolvedLoadable,
14};
15
16#[macro_use]
17mod audio_stream;
18#[macro_use]
19mod codecs;
20#[macro_use]
21mod compute;
22#[macro_use]
23mod data;
24#[macro_use]
25mod device;
26#[macro_use]
27mod femm;
28#[macro_use]
29mod glasses;
30#[macro_use]
31mod interference;
32#[macro_use]
33mod music;
34#[macro_use]
35mod numbers;
36#[macro_use]
37mod runtime_libs;
38#[macro_use]
39mod watch;
40
41macro_rules! loadable_libs {
42    ($m:ident) => {
43        cookbook_directory_codecs!($m);
44        cookbook_directory_compute!($m);
45        cookbook_directory_numbers!($m);
46        cookbook_directory_runtime_libs!($m);
47        cookbook_directory_femm!($m);
48        cookbook_directory_interference!($m);
49        cookbook_directory_glasses!($m);
50        cookbook_directory_music!($m);
51        cookbook_directory_audio_stream!($m);
52        cookbook_directory_data!($m);
53        cookbook_directory_device!($m);
54        cookbook_directory_watch!($m);
55    };
56}
57
58/// Product default cookbook config for the enabled sim-nest feature set.
59pub fn default_cookbook_config() -> CookbookConfig {
60    CookbookConfig {
61        minimum_loaded: vec!["codec/lisp".to_owned(), "core".to_owned()],
62        loadable_libs: loadable_rows(),
63    }
64}
65
66/// Resolve the product default directory for the enabled sim-nest feature set.
67pub fn default_loadable_libs() -> (LoadableLibList, Vec<String>) {
68    ConfigProvider::new(default_cookbook_config(), &SimNestCookbookResolver).loadable_libs()
69}
70
71/// Loadable-lib ids compiled into a `cookbook-all` build.
72#[cfg(feature = "cookbook-all")]
73pub fn cookbook_all_lib_ids() -> Vec<&'static str> {
74    let mut ids = Vec::new();
75    macro_rules! push_id {
76        ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
77            #[cfg(feature = $feature)]
78            {
79                let _ = $title;
80                ids.push($id);
81            }
82        };
83    }
84    loadable_libs!(push_id);
85    ids
86}
87
88fn loadable_rows() -> Vec<LoadableLibConfig> {
89    let mut rows = Vec::new();
90    macro_rules! push_row {
91        ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
92            #[cfg(feature = $feature)]
93            {
94                let _ = $title;
95                rows.push(row($id));
96            }
97        };
98    }
99    loadable_libs!(push_row);
100    rows
101}
102
103fn row(id: &str) -> LoadableLibConfig {
104    LoadableLibConfig {
105        id: id.to_owned(),
106        source: format!("sim-nest:{id}"),
107    }
108}
109
110/// Resolver over the loadable libs linked into this sim-nest build.
111pub struct SimNestCookbookResolver;
112
113impl LoadableLibResolver for SimNestCookbookResolver {
114    fn resolve(&self, source: &str, id: &str) -> Option<ResolvedLoadable> {
115        if source != format!("sim-nest:{id}") {
116            return None;
117        }
118
119        macro_rules! resolve_if {
120            ($row_id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
121                #[cfg(feature = $feature)]
122                if id == $row_id {
123                    return Some(resolved($title, $recipes, $make));
124                }
125            };
126        }
127        loadable_libs!(resolve_if);
128        None
129    }
130}
131
132#[allow(dead_code)]
133fn codec_id(offset: u32) -> CodecId {
134    const PRODUCT_CODEC_BASE: u32 = 10_000;
135    CodecId(PRODUCT_CODEC_BASE + offset)
136}
137
138fn resolved<F>(title: &str, recipes: Option<EmbeddedDir>, make: F) -> ResolvedLoadable
139where
140    F: Fn() -> Box<dyn Lib + Send + Sync> + Send + Sync + 'static,
141{
142    ResolvedLoadable {
143        title: title.to_owned(),
144        recipes,
145        factory: Arc::new(make),
146    }
147}
148
149#[cfg(all(test, feature = "interference", feature = "cookbook"))]
150mod interference_tests {
151    use sim_cookbook::recipes_from_embedded;
152
153    #[test]
154    fn interference_directory_preserves_dependency_order_and_recipes() {
155        let (directory, diagnostics) = super::default_loadable_libs();
156        assert!(diagnostics.is_empty(), "unresolved rows: {diagnostics:?}");
157        let ids = directory
158            .entries()
159            .iter()
160            .map(|entry| entry.id.as_str())
161            .collect::<Vec<_>>();
162        let records = position(&ids, "interference/records");
163        let runtime = position(&ids, "interference/runtime");
164        let compute = position(&ids, "interference/compute");
165        assert!(records < runtime && runtime < compute);
166
167        for (id, recipe) in [
168            (
169                "interference/runtime",
170                "interference-runtime/01-basics/two-source-cancellation",
171            ),
172            (
173                "interference/compute",
174                "interference-compute/01-basics/modeled-resident-study",
175            ),
176        ] {
177            let recipes = directory
178                .entry(id)
179                .and_then(|entry| entry.recipes)
180                .unwrap_or_else(|| panic!("{id} recipes"));
181            let cards = recipes_from_embedded(recipes).unwrap();
182            assert!(
183                cards.iter().any(|card| card.id == recipe),
184                "{id} should expose {recipe}"
185            );
186        }
187    }
188
189    fn position(ids: &[&str], expected: &str) -> usize {
190        ids.iter()
191            .position(|id| *id == expected)
192            .unwrap_or_else(|| panic!("missing {expected} directory row"))
193    }
194}
195
196// conformance: GenAI SDK bundle cookbook rows resolve the agent and bridge libraries.
197#[cfg(all(test, feature = "genai"))]
198mod genai_tests {
199    use sim_cookbook::recipes_from_embedded;
200
201    #[test]
202    fn genai_bundle_resolves_agent_bridge_and_recipe() {
203        let (dir, diags) = super::default_loadable_libs();
204        assert!(diags.is_empty(), "unresolved rows: {diags:?}");
205        assert!(dir.entry("bridge").is_some(), "bridge row");
206
207        let agent = dir.entry("agent").expect("agent row");
208        let recipes = agent.recipes.expect("agent recipes");
209        let cards = recipes_from_embedded(recipes).expect("agent recipes parse");
210        assert!(
211            cards
212                .iter()
213                .any(|card| card.id == "agent/01-basics/genai-assembly"),
214            "agent cookbook row should include the GenAI assembly recipe"
215        );
216    }
217}
218
219#[cfg(all(test, feature = "cookbook-all"))]
220mod tests {
221    use std::collections::{BTreeMap, BTreeSet};
222
223    use sim_lib_cookbook::LibCatalog;
224
225    #[test]
226    fn discrete_graph_requirement_resolves_to_discrete_runtime() {
227        let (dir, diags) = super::default_loadable_libs();
228        assert!(diags.is_empty(), "unresolved rows: {diags:?}");
229
230        let row = dir
231            .entry("discrete-graph")
232            .expect("discrete-graph alias row");
233        assert!(
234            row.recipes.is_none(),
235            "alias row should not duplicate the discrete cookbook"
236        );
237        assert!(
238            dir.resolve("discrete-graph").is_some(),
239            "discrete-graph requirement should load the discrete runtime"
240        );
241    }
242
243    #[test]
244    fn every_loadable_lib_has_a_directory_row() {
245        let cfg = super::default_cookbook_config();
246        assert_eq!(cfg.minimum_loaded, ["codec/lisp", "core"]);
247
248        let ids = super::cookbook_all_lib_ids();
249        let mut counts = BTreeMap::new();
250        for row in &cfg.loadable_libs {
251            *counts.entry(row.id.as_str()).or_insert(0usize) += 1;
252            assert_eq!(row.source, format!("sim-nest:{}", row.id));
253        }
254        for id in &ids {
255            assert_eq!(counts.get(id).copied(), Some(1), "{id} row count");
256        }
257        assert_eq!(counts.len(), ids.len());
258
259        let (dir, diags) = super::default_loadable_libs();
260        assert!(diags.is_empty(), "unresolved rows: {diags:?}");
261        let resolved = dir
262            .entries()
263            .iter()
264            .map(|entry| entry.id.as_str())
265            .collect::<BTreeSet<_>>();
266        for id in &ids {
267            assert!(
268                dir.entry(id).is_some(),
269                "loadable lib `{id}` missing a directory row"
270            );
271            assert!(
272                resolved.contains(id),
273                "loadable lib `{id}` missing a factory"
274            );
275        }
276        assert_eq!(dir.entries().len(), ids.len());
277    }
278
279    #[test]
280    fn cookbook_all_feature_matches_directory_features() {
281        let cargo_toml = include_str!("../../Cargo.toml");
282        let cookbook_features = parse_feature(cargo_toml, "cookbook-all");
283        let mut row_features = BTreeSet::new();
284        macro_rules! push_feature {
285            ($id:literal, $title:literal, $feature:literal, $recipes:expr, $make:expr) => {
286                #[cfg(feature = $feature)]
287                {
288                    let _ = ($id, $title);
289                    row_features.insert($feature);
290                }
291            };
292        }
293        loadable_libs!(push_feature);
294
295        for feature in &row_features {
296            assert!(
297                cookbook_features.contains(*feature),
298                "`cookbook-all` does not enable `{feature}`"
299            );
300        }
301
302        let no_directory_rows = BTreeSet::from([
303            "citizen",
304            "cookbook",
305            "exec",
306            "gpu-math",
307            "interference",
308            "shape",
309            "discrete-rank",
310            "table-fs",
311            "table-http",
312        ]);
313        for feature in cookbook_features {
314            assert!(
315                row_features.contains(feature) || no_directory_rows.contains(feature),
316                "`cookbook-all` enables `{feature}` without a loadable-lib directory row"
317            );
318        }
319    }
320
321    fn parse_feature<'a>(cargo_toml: &'a str, feature: &str) -> BTreeSet<&'a str> {
322        let prefix = format!("{feature} = [");
323        let line = cargo_toml
324            .lines()
325            .find(|line| line.starts_with(&prefix))
326            .expect("feature line");
327        line[prefix.len()..]
328            .trim_end_matches(']')
329            .split(',')
330            .map(str::trim)
331            .filter_map(|part| part.strip_prefix('"')?.strip_suffix('"'))
332            .collect()
333    }
334}