1use sim_kernel::{Expr, Symbol};
4use sim_value::access::field_any;
5
6use crate::{ConfigDir, ConfigSource, ConfigTable};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct ConfigLayer {
11 pub source: ConfigSource,
13 pub dir: ConfigDir,
15}
16
17impl ConfigLayer {
18 pub fn new(source: ConfigSource, dir: ConfigDir) -> Self {
20 Self { source, dir }
21 }
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct MergeTrace {
27 pub lib: Symbol,
29 pub key: String,
31 pub source: ConfigSource,
33}
34
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct EffectiveConfig {
38 pub dir: ConfigDir,
40 pub trace: Vec<MergeTrace>,
42}
43
44impl EffectiveConfig {
45 fn record_trace(&mut self, lib: &Symbol, key: String, source: &ConfigSource) {
46 if let Some(trace) = self
47 .trace
48 .iter_mut()
49 .find(|trace| &trace.lib == lib && trace.key == key)
50 {
51 trace.source = source.clone();
52 } else {
53 self.trace.push(MergeTrace {
54 lib: lib.clone(),
55 key,
56 source: source.clone(),
57 });
58 }
59 }
60}
61
62pub fn merge_layers(layers: &[ConfigLayer]) -> EffectiveConfig {
64 let mut effective = EffectiveConfig::default();
65 for layer in layers {
66 for table in &layer.dir.entries {
67 overlay_table(&mut effective, table, &layer.source);
68 }
69 }
70 effective
71}
72
73fn overlay_table(effective: &mut EffectiveConfig, table: &ConfigTable, source: &ConfigSource) {
74 let Some(index) = effective
75 .dir
76 .entries
77 .iter()
78 .position(|entry| entry.lib == table.lib)
79 else {
80 effective.dir.entries.push(table.clone());
81 for key in top_level_keys(&table.table) {
82 effective.record_trace(&table.lib, key, source);
83 }
84 return;
85 };
86
87 let changed_keys = merge_table_expr(&mut effective.dir.entries[index].table, &table.table);
88 for key in changed_keys {
89 effective.record_trace(&table.lib, key, source);
90 }
91}
92
93fn merge_table_expr(base: &mut Expr, overlay: &Expr) -> Vec<String> {
94 match (base, overlay) {
95 (Expr::Map(base_entries), Expr::Map(overlay_entries)) => {
96 let mut changed = Vec::new();
97 for (overlay_key, overlay_value) in overlay_entries {
98 let key = key_label(overlay_key);
99 if let Some((_, base_value)) = base_entries
100 .iter_mut()
101 .find(|(base_key, _)| base_key == overlay_key)
102 {
103 *base_value = merge_value(base_value, overlay_value);
104 } else {
105 base_entries.push((overlay_key.clone(), overlay_value.clone()));
106 }
107 changed.push(key);
108 }
109 changed
110 }
111 (base, _) => {
112 *base = overlay.clone();
113 top_level_keys(overlay)
114 }
115 }
116}
117
118fn merge_value(base: &Expr, overlay: &Expr) -> Expr {
119 match (base, overlay) {
120 (Expr::Map(_), Expr::Map(_)) => {
121 let mut merged = base.clone();
122 merge_table_expr(&mut merged, overlay);
123 merged
124 }
125 (Expr::List(base_items), Expr::List(overlay_items))
126 if id_keyed_items(base_items) && id_keyed_items(overlay_items) =>
127 {
128 merge_id_keyed_items(base_items, overlay_items)
129 }
130 _ => overlay.clone(),
131 }
132}
133
134fn merge_id_keyed_items(base_items: &[Expr], overlay_items: &[Expr]) -> Expr {
135 let mut merged = base_items.to_vec();
136 for overlay in overlay_items {
137 let Some(overlay_id) = item_id(overlay) else {
138 continue;
139 };
140 if let Some(slot) = merged
141 .iter_mut()
142 .find(|item| item_id(item).as_deref() == Some(overlay_id.as_str()))
143 {
144 *slot = overlay.clone();
145 } else {
146 merged.push(overlay.clone());
147 }
148 }
149 Expr::List(merged)
150}
151
152fn id_keyed_items(items: &[Expr]) -> bool {
153 !items.is_empty() && items.iter().all(|item| item_id(item).is_some())
154}
155
156fn item_id(item: &Expr) -> Option<String> {
157 match field_any(item, "id") {
158 Some(Expr::String(id)) => Some(id.clone()),
159 Some(Expr::Symbol(id)) => Some(id.as_qualified_str()),
160 _ => None,
161 }
162}
163
164fn top_level_keys(table: &Expr) -> Vec<String> {
165 match table {
166 Expr::Map(entries) => entries.iter().map(|(key, _)| key_label(key)).collect(),
167 _ => Vec::new(),
168 }
169}
170
171fn key_label(key: &Expr) -> String {
172 match key {
173 Expr::Symbol(symbol) => symbol.as_qualified_str(),
174 Expr::String(text) => text.clone(),
175 other => format!("{other:?}"),
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use sim_value::build::{entry, int, map, text};
182
183 use super::*;
184
185 fn lib() -> Symbol {
186 Symbol::qualified("sim", "cookbook")
187 }
188
189 #[test]
190 fn higher_layers_replace_scalars_and_preserve_absent_fields() {
191 let lower = ConfigDir::one(
192 lib(),
193 map(vec![("mode", text("built-in")), ("keep", int(1))]),
194 )
195 .unwrap();
196 let upper = ConfigDir::one(lib(), map(vec![("mode", text("work"))])).unwrap();
197
198 let effective = merge_layers(&[
199 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
200 ConfigLayer::new(
201 ConfigSource::Explicit {
202 label: "work".to_owned(),
203 },
204 upper,
205 ),
206 ]);
207
208 let table = effective.dir.table(&lib()).unwrap();
209 assert_eq!(field_any(&table.table, "mode"), Some(&text("work")));
210 assert_eq!(field_any(&table.table, "keep"), Some(&int(1)));
211 assert_eq!(
212 effective
213 .trace
214 .iter()
215 .find(|trace| trace.key == "mode")
216 .unwrap()
217 .source,
218 ConfigSource::Explicit {
219 label: "work".to_owned()
220 }
221 );
222 }
223
224 #[test]
225 fn id_keyed_repeated_tables_replace_by_id() {
226 let lower = ConfigDir::one(
227 lib(),
228 map(vec![(
229 "loadable_lib",
230 Expr::List(vec![
231 map(vec![("id", text("numbers")), ("source", text("stable"))]),
232 map(vec![("id", text("shape")), ("source", text("stable"))]),
233 ]),
234 )]),
235 )
236 .unwrap();
237 let upper = ConfigDir::one(
238 lib(),
239 map(vec![(
240 "loadable_lib",
241 Expr::List(vec![
242 map(vec![("id", text("shape")), ("source", text("work"))]),
243 map(vec![("id", text("music")), ("source", text("work"))]),
244 ]),
245 )]),
246 )
247 .unwrap();
248
249 let effective = merge_layers(&[
250 ConfigLayer::new(ConfigSource::BuiltIn { lib: lib() }, lower),
251 ConfigLayer::new(
252 ConfigSource::Explicit {
253 label: "work".to_owned(),
254 },
255 upper,
256 ),
257 ]);
258
259 let list = match field_any(&effective.dir.table(&lib()).unwrap().table, "loadable_lib") {
260 Some(Expr::List(items)) => items,
261 other => panic!("expected merged list, got {other:?}"),
262 };
263 assert_eq!(list.len(), 3);
264 assert_eq!(field_any(&list[1], "source"), Some(&text("work")));
265 assert_eq!(field_any(&list[2], "id"), Some(&text("music")));
266 let _ = entry("checked", Expr::Bool(true));
267 }
268}