1use 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
58pub 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
66pub fn default_loadable_libs() -> (LoadableLibList, Vec<String>) {
68 ConfigProvider::new(default_cookbook_config(), &SimNestCookbookResolver).loadable_libs()
69}
70
71#[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
110pub 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#[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}