1use sim_kernel::{Expr, Symbol};
4
5use crate::{ConfigDir, ConfigSource, ConfigTable, config_field_name, same_config_field};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct ConfigLayer {
10 pub source: ConfigSource,
12 pub dir: ConfigDir,
14}
15
16impl ConfigLayer {
17 pub fn new(source: ConfigSource, dir: ConfigDir) -> Self {
19 Self { source, dir }
20 }
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct MergeTrace {
26 pub lib: Symbol,
28 pub key: String,
30 pub source: ConfigSource,
32}
33
34#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct EffectiveConfig {
37 pub dir: ConfigDir,
39 pub trace: Vec<MergeTrace>,
41}
42
43impl EffectiveConfig {
44 fn record_trace(&mut self, lib: &Symbol, key: String, source: &ConfigSource) {
45 if let Some(trace) = self
46 .trace
47 .iter_mut()
48 .find(|trace| &trace.lib == lib && trace.key == key)
49 {
50 trace.source = source.clone();
51 } else {
52 self.trace.push(MergeTrace {
53 lib: lib.clone(),
54 key,
55 source: source.clone(),
56 });
57 }
58 }
59}
60
61pub fn merge_layers(layers: &[ConfigLayer]) -> EffectiveConfig {
63 let mut effective = EffectiveConfig::default();
64 for layer in layers {
65 for table in &layer.dir.entries {
66 overlay_table(&mut effective, table, &layer.source);
67 }
68 }
69 effective
70}
71
72fn overlay_table(effective: &mut EffectiveConfig, table: &ConfigTable, source: &ConfigSource) {
73 let Some(index) = effective
74 .dir
75 .entries
76 .iter()
77 .position(|entry| entry.lib == table.lib)
78 else {
79 effective.dir.entries.push(table.clone());
80 for key in top_level_keys(&table.table) {
81 effective.record_trace(&table.lib, key, source);
82 }
83 return;
84 };
85
86 let changed_keys = merge_table_expr(&mut effective.dir.entries[index].table, &table.table);
87 for key in changed_keys {
88 effective.record_trace(&table.lib, key, source);
89 }
90}
91
92fn merge_table_expr(base: &mut Expr, overlay: &Expr) -> Vec<String> {
93 match (base, overlay) {
94 (Expr::Map(base_entries), Expr::Map(overlay_entries)) => {
95 let mut changed = Vec::new();
96 for (overlay_key, overlay_value) in overlay_entries {
97 let key = key_label(overlay_key);
98 if let Some((_, base_value)) = base_entries
99 .iter_mut()
100 .find(|(base_key, _)| same_config_field(base_key, overlay_key))
101 {
102 *base_value = merge_value(base_value, overlay_value);
103 } else {
104 base_entries.push((overlay_key.clone(), overlay_value.clone()));
105 }
106 changed.push(key);
107 }
108 changed
109 }
110 (base, _) => {
111 *base = overlay.clone();
112 top_level_keys(overlay)
113 }
114 }
115}
116
117fn merge_value(base: &Expr, overlay: &Expr) -> Expr {
118 match (base, overlay) {
119 (Expr::Map(_), Expr::Map(_)) => {
120 let mut merged = base.clone();
121 merge_table_expr(&mut merged, overlay);
122 merged
123 }
124 (Expr::List(base_items), Expr::List(overlay_items))
125 if id_keyed_items(base_items) && id_keyed_items(overlay_items) =>
126 {
127 merge_id_keyed_items(base_items, overlay_items)
128 }
129 _ => overlay.clone(),
130 }
131}
132
133fn merge_id_keyed_items(base_items: &[Expr], overlay_items: &[Expr]) -> Expr {
134 let mut merged = base_items.to_vec();
135 for overlay in overlay_items {
136 let Some(overlay_id) = item_id(overlay) else {
137 continue;
138 };
139 if let Some(slot) = merged
140 .iter_mut()
141 .find(|item| item_id(item).as_deref() == Some(overlay_id.as_str()))
142 {
143 *slot = overlay.clone();
144 } else {
145 merged.push(overlay.clone());
146 }
147 }
148 Expr::List(merged)
149}
150
151fn id_keyed_items(items: &[Expr]) -> bool {
152 !items.is_empty() && items.iter().all(|item| item_id(item).is_some())
153}
154
155fn item_id(item: &Expr) -> Option<String> {
156 match sim_value::access::field_any(item, "id") {
157 Some(Expr::String(id)) => Some(id.clone()),
158 Some(Expr::Symbol(id)) => Some(id.as_qualified_str()),
159 _ => None,
160 }
161}
162
163fn top_level_keys(table: &Expr) -> Vec<String> {
164 match table {
165 Expr::Map(entries) => entries.iter().map(|(key, _)| key_label(key)).collect(),
166 _ => Vec::new(),
167 }
168}
169
170fn key_label(key: &Expr) -> String {
171 config_field_name(key)
172 .map(ToOwned::to_owned)
173 .unwrap_or_else(|| format!("{key:?}"))
174}
175
176#[cfg(test)]
177mod tests {
178 use sim_value::access::field_any;
179 use sim_value::build::{entry, int, list, map, sym, text};
180
181 use super::*;
182 use crate::ConfigView;
183
184 fn lib() -> Symbol {
185 Symbol::qualified("sim", "cookbook")
186 }
187
188 #[test]
189 fn higher_layers_replace_scalars_and_preserve_absent_fields() {
190 let lower = ConfigDir::one(
191 lib(),
192 map(vec![("mode", text("built-in")), ("keep", int(1))]),
193 )
194 .unwrap();
195 let upper = ConfigDir::one(lib(), map(vec![("mode", text("work"))])).unwrap();
196
197 let effective = merge_layers(&[
198 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
199 ConfigLayer::new(
200 ConfigSource::Explicit {
201 label: "work".to_owned(),
202 },
203 upper,
204 ),
205 ]);
206
207 let table = effective.dir.table(&lib()).unwrap();
208 assert_eq!(field_any(&table.table, "mode"), Some(&text("work")));
209 assert_eq!(field_any(&table.table, "keep"), Some(&int(1)));
210 assert_eq!(
211 effective
212 .trace
213 .iter()
214 .find(|trace| trace.key == "mode")
215 .unwrap()
216 .source,
217 ConfigSource::Explicit {
218 label: "work".to_owned()
219 }
220 );
221 }
222
223 #[test]
224 fn equivalent_scalar_keys_replace_in_both_directions() {
225 let lower = ConfigDir::one(
226 lib(),
227 Expr::Map(vec![
228 (Expr::String("mode".to_owned()), text("built-in")),
229 (sym("keep"), int(1)),
230 ]),
231 )
232 .unwrap();
233 let upper = ConfigDir::one(lib(), map(vec![("mode", text("work"))])).unwrap();
234
235 let effective = merge_layers(&[
236 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
237 ConfigLayer::new(
238 ConfigSource::Explicit {
239 label: "symbol-over-string".to_owned(),
240 },
241 upper,
242 ),
243 ]);
244 let table = effective.dir.table(&lib()).unwrap();
245 let view = ConfigView::new(table);
246
247 assert_eq!(view.string("mode"), Some("work"));
248 assert_eq!(view.i64("keep"), Some(1));
249 assert_eq!(table.entries().unwrap().len(), 2);
250 assert_eq!(
251 effective
252 .trace
253 .iter()
254 .find(|trace| trace.key == "mode")
255 .unwrap()
256 .source,
257 ConfigSource::Explicit {
258 label: "symbol-over-string".to_owned()
259 }
260 );
261
262 let lower = ConfigDir::one(lib(), map(vec![("mode", text("built-in"))])).unwrap();
263 let upper = ConfigDir::one(
264 lib(),
265 Expr::Map(vec![(Expr::String("mode".to_owned()), text("work"))]),
266 )
267 .unwrap();
268 let effective = merge_layers(&[
269 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
270 ConfigLayer::new(
271 ConfigSource::Explicit {
272 label: "string-over-symbol".to_owned(),
273 },
274 upper,
275 ),
276 ]);
277
278 assert_eq!(
279 ConfigView::new(effective.dir.table(&lib()).unwrap()).string("mode"),
280 Some("work")
281 );
282 assert_eq!(
283 effective
284 .trace
285 .iter()
286 .find(|trace| trace.key == "mode")
287 .unwrap()
288 .source,
289 ConfigSource::Explicit {
290 label: "string-over-symbol".to_owned()
291 }
292 );
293 }
294
295 #[test]
296 fn equivalent_nested_map_keys_merge_without_duplicates() {
297 let lower = ConfigDir::one(
298 lib(),
299 map(vec![(
300 "settings",
301 Expr::Map(vec![
302 (sym("mode"), text("built-in")),
303 (sym("keep"), text("lower")),
304 ]),
305 )]),
306 )
307 .unwrap();
308 let upper = ConfigDir::one(
309 lib(),
310 Expr::Map(vec![(
311 Expr::String("settings".to_owned()),
312 Expr::Map(vec![(Expr::String("mode".to_owned()), text("work"))]),
313 )]),
314 )
315 .unwrap();
316
317 let effective = merge_layers(&[
318 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
319 ConfigLayer::new(
320 ConfigSource::Explicit {
321 label: "work".to_owned(),
322 },
323 upper,
324 ),
325 ]);
326 let view = ConfigView::new(effective.dir.table(&lib()).unwrap());
327 let settings = ConfigView::from_entries(view.table("settings").unwrap());
328
329 assert_eq!(settings.string("mode"), Some("work"));
330 assert_eq!(settings.string("keep"), Some("lower"));
331 assert_eq!(view.table("settings").unwrap().len(), 2);
332 assert_eq!(
333 effective
334 .trace
335 .iter()
336 .find(|trace| trace.key == "settings")
337 .unwrap()
338 .source,
339 ConfigSource::Explicit {
340 label: "work".to_owned()
341 }
342 );
343 }
344
345 #[test]
346 fn id_keyed_repeated_tables_replace_by_id() {
347 let lower = ConfigDir::one(
348 lib(),
349 map(vec![(
350 "loadable_lib",
351 Expr::List(vec![
352 map(vec![("id", text("numbers")), ("source", text("stable"))]),
353 map(vec![("id", text("shape")), ("source", text("stable"))]),
354 ]),
355 )]),
356 )
357 .unwrap();
358 let upper = ConfigDir::one(
359 lib(),
360 map(vec![(
361 "loadable_lib",
362 Expr::List(vec![
363 map(vec![("id", text("shape")), ("source", text("work"))]),
364 map(vec![("id", text("music")), ("source", text("work"))]),
365 ]),
366 )]),
367 )
368 .unwrap();
369
370 let effective = merge_layers(&[
371 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
372 ConfigLayer::new(
373 ConfigSource::Explicit {
374 label: "work".to_owned(),
375 },
376 upper,
377 ),
378 ]);
379
380 let list = match field_any(&effective.dir.table(&lib()).unwrap().table, "loadable_lib") {
381 Some(Expr::List(items)) => items,
382 other => panic!("expected merged list, got {other:?}"),
383 };
384 assert_eq!(list.len(), 3);
385 assert_eq!(field_any(&list[1], "source"), Some(&text("work")));
386 assert_eq!(field_any(&list[2], "id"), Some(&text("music")));
387 let _ = entry("checked", Expr::Bool(true));
388 }
389
390 #[test]
391 fn id_keyed_repeated_tables_use_config_field_identity() {
392 let lower = ConfigDir::one(
393 lib(),
394 Expr::Map(vec![(
395 Expr::String("loadable_lib".to_owned()),
396 list(vec![
397 Expr::Map(vec![
398 (Expr::String("id".to_owned()), text("shape")),
399 (sym("source"), text("stable")),
400 ]),
401 map(vec![("id", text("numbers")), ("source", text("stable"))]),
402 ]),
403 )]),
404 )
405 .unwrap();
406 let upper = ConfigDir::one(
407 lib(),
408 map(vec![(
409 "loadable_lib",
410 list(vec![Expr::Map(vec![
411 (sym("id"), text("shape")),
412 (Expr::String("source".to_owned()), text("work")),
413 ])]),
414 )]),
415 )
416 .unwrap();
417
418 let effective = merge_layers(&[
419 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
420 ConfigLayer::new(
421 ConfigSource::Explicit {
422 label: "work".to_owned(),
423 },
424 upper,
425 ),
426 ]);
427
428 let list = match ConfigView::new(effective.dir.table(&lib()).unwrap()).get("loadable_lib") {
429 Some(Expr::List(items)) => items,
430 other => panic!("expected merged list, got {other:?}"),
431 };
432 assert_eq!(list.len(), 2);
433 assert_eq!(field_any(&list[0], "id"), Some(&text("shape")));
434 assert_eq!(field_any(&list[0], "source"), Some(&text("work")));
435 assert_eq!(field_any(&list[1], "id"), Some(&text("numbers")));
436 assert_eq!(
437 effective
438 .trace
439 .iter()
440 .find(|trace| trace.key == "loadable_lib")
441 .unwrap()
442 .source,
443 ConfigSource::Explicit {
444 label: "work".to_owned()
445 }
446 );
447 }
448}