1use std::collections::HashSet;
4
5use sim_config::{ConfigView, EffectiveConfig};
6use sim_cookbook::EmbeddedDir;
7use sim_kernel::{Expr, Symbol};
8use sim_value::access::field_any;
9
10use crate::loadable::{LibFactory, LoadableLibEntry, LoadableLibList};
11
12#[derive(Clone, Debug, Default, PartialEq, Eq)]
14pub struct CookbookConfig {
15 pub minimum_loaded: Vec<String>,
17 pub loadable_libs: Vec<LoadableLibConfig>,
19}
20
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct CookbookOverrides {
24 pub minimum_loaded: Option<Vec<String>>,
26 pub add: Vec<LoadableLibConfig>,
28 pub hide: Vec<String>,
30 pub order: Vec<String>,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct LoadableLibConfig {
37 pub id: String,
39 pub source: String,
41}
42
43pub struct ResolvedLoadable {
45 pub title: String,
47 pub recipes: Option<EmbeddedDir>,
49 pub factory: LibFactory,
51}
52
53pub trait LoadableLibResolver {
55 fn resolve(&self, source: &str, id: &str) -> Option<ResolvedLoadable>;
57}
58
59pub struct ConfigProvider<'a, R: LoadableLibResolver + ?Sized> {
61 config: CookbookConfig,
62 resolver: &'a R,
63}
64
65impl<'a, R: LoadableLibResolver + ?Sized> ConfigProvider<'a, R> {
66 pub fn new(config: CookbookConfig, resolver: &'a R) -> Self {
68 Self { config, resolver }
69 }
70
71 pub fn minimum_loaded(&self) -> &[String] {
73 &self.config.minimum_loaded
74 }
75
76 pub fn config(&self) -> &CookbookConfig {
78 &self.config
79 }
80
81 pub fn loadable_libs(&self) -> (LoadableLibList, Vec<String>) {
86 let mut entries = Vec::new();
87 let mut diagnostics = Vec::new();
88 let mut seen = HashSet::new();
89
90 for (index, cfg) in self.config.loadable_libs.iter().enumerate() {
91 if !seen.insert(cfg.id.as_str()) {
92 diagnostics.push(format!("duplicate loadable-lib id `{}`", cfg.id));
93 continue;
94 }
95
96 match self.resolver.resolve(&cfg.source, &cfg.id) {
97 Some(resolved) => entries.push(LoadableLibEntry {
98 id: cfg.id.clone(),
99 source: cfg.source.clone(),
100 title: resolved.title,
101 order: index as i64,
102 recipes: resolved.recipes,
103 catalog_lib: (resolved.factory)(),
104 factory: resolved.factory,
105 }),
106 None => diagnostics.push(format!(
107 "unknown loadable-lib source `{}` for `{}`",
108 cfg.source, cfg.id
109 )),
110 }
111 }
112
113 (LoadableLibList::new(entries), diagnostics)
114 }
115}
116
117pub struct ConfigCookbookProvider<'a, R: LoadableLibResolver + ?Sized> {
119 inner: ConfigProvider<'a, R>,
120}
121
122impl<'a, R: LoadableLibResolver + ?Sized> ConfigCookbookProvider<'a, R> {
123 pub fn new(effective: &EffectiveConfig, resolver: &'a R) -> Self {
126 Self::new_with_base(effective, built_in_config(), resolver)
127 }
128
129 pub fn new_with_base(
131 effective: &EffectiveConfig,
132 base: CookbookConfig,
133 resolver: &'a R,
134 ) -> Self {
135 Self {
136 inner: ConfigProvider::new(
137 cookbook_config_from_effective_with_base(effective, base),
138 resolver,
139 ),
140 }
141 }
142
143 pub fn config(&self) -> &CookbookConfig {
145 self.inner.config()
146 }
147
148 pub fn minimum_loaded(&self) -> &[String] {
150 self.inner.minimum_loaded()
151 }
152
153 pub fn loadable_libs(&self) -> (LoadableLibList, Vec<String>) {
155 self.inner.loadable_libs()
156 }
157}
158
159pub fn cookbook_lib_symbol() -> Symbol {
161 Symbol::qualified("sim", "cookbook")
162}
163
164pub fn cookbook_config_from_effective(effective: &EffectiveConfig) -> CookbookConfig {
170 cookbook_config_from_effective_with_base(effective, built_in_config())
171}
172
173pub fn cookbook_config_from_effective_with_base(
176 effective: &EffectiveConfig,
177 base: CookbookConfig,
178) -> CookbookConfig {
179 apply_overrides(base, &cookbook_overrides_from_effective(effective))
180}
181
182pub fn cookbook_overrides_from_effective(effective: &EffectiveConfig) -> CookbookOverrides {
184 let Some(table) = effective.dir.table(&cookbook_lib_symbol()) else {
185 return CookbookOverrides::default();
186 };
187 let view = ConfigView::new(table);
188 CookbookOverrides {
189 minimum_loaded: optional_string_array(&view, "minimum_loaded"),
190 add: loadable_lib_rows(&view),
191 hide: string_array(&view, "hide"),
192 order: string_array(&view, "order"),
193 }
194}
195
196pub fn apply_overrides(base: CookbookConfig, overrides: &CookbookOverrides) -> CookbookConfig {
198 let CookbookConfig {
199 minimum_loaded,
200 loadable_libs,
201 } = base;
202 let minimum_loaded = overrides.minimum_loaded.clone().unwrap_or(minimum_loaded);
203 let mut rows: Vec<LoadableLibConfig> = loadable_libs
204 .into_iter()
205 .filter(|row| !overrides.hide.contains(&row.id))
206 .collect();
207 for add in &overrides.add {
208 match rows.iter_mut().find(|row| row.id == add.id) {
209 Some(row) => *row = add.clone(),
210 None => rows.push(add.clone()),
211 }
212 }
213 if !overrides.order.is_empty() {
214 let mut indexed = rows.into_iter().enumerate().collect::<Vec<_>>();
215 indexed.sort_by_key(|(index, row)| {
216 (
217 overrides
218 .order
219 .iter()
220 .position(|id| id == &row.id)
221 .unwrap_or(usize::MAX),
222 *index,
223 )
224 });
225 rows = indexed.into_iter().map(|(_, row)| row).collect();
226 }
227 CookbookConfig {
228 minimum_loaded,
229 loadable_libs: rows,
230 }
231}
232
233pub fn built_in_config() -> CookbookConfig {
235 CookbookConfig {
236 minimum_loaded: vec!["codec/lisp".to_owned()],
237 loadable_libs: vec![
238 loadable("numbers/i64"),
239 loadable("numbers/arith"),
240 loadable("numbers/bigint"),
241 loadable("numbers/bool"),
242 loadable("numbers/f64"),
243 loadable("numbers/rational"),
244 loadable("numbers/complex"),
245 loadable("numbers/func"),
246 loadable("numbers/cas"),
247 loadable("numbers/tensor"),
248 loadable("numbers/tensor-bcast"),
249 loadable("discrete"),
250 loadable("organ/binding"),
251 loadable("organ/control"),
252 loadable("organ/sequence"),
253 loadable("organ/pattern"),
254 loadable("codec/algol"),
255 loadable("codec/scheme-r7rs-small"),
256 loadable("midi/digest"),
257 ],
258 }
259}
260
261fn loadable(id: &str) -> LoadableLibConfig {
262 LoadableLibConfig {
263 id: id.to_owned(),
264 source: format!("symbol:{id}"),
265 }
266}
267
268fn string_array(view: &ConfigView<'_>, key: &str) -> Vec<String> {
269 view.list(key)
270 .unwrap_or_default()
271 .iter()
272 .filter_map(string_value)
273 .collect()
274}
275
276fn optional_string_array(view: &ConfigView<'_>, key: &str) -> Option<Vec<String>> {
277 view.get(key).map(|value| match value {
278 Expr::List(items) => items.iter().filter_map(string_value).collect(),
279 _ => Vec::new(),
280 })
281}
282
283fn string_value(item: &Expr) -> Option<String> {
284 match item {
285 Expr::String(value) => Some(value.clone()),
286 _ => None,
287 }
288}
289
290fn loadable_lib_rows(view: &ConfigView<'_>) -> Vec<LoadableLibConfig> {
291 view.list("loadable_lib")
292 .unwrap_or_default()
293 .iter()
294 .map(|entry| LoadableLibConfig {
295 id: string_at(entry, "id").unwrap_or_default().to_owned(),
296 source: string_at(entry, "source").unwrap_or_default().to_owned(),
297 })
298 .collect()
299}
300
301fn string_at<'a>(entry: &'a Expr, key: &str) -> Option<&'a str> {
302 field_any(entry, key).and_then(|value| match value {
303 Expr::String(text) => Some(text.as_str()),
304 _ => None,
305 })
306}