metrale_circuit/config_map/
apply.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: Applying a [`ConfigMap`] to a parsed `config.json`.
4//!
5//! Owner: metrale-circuit.
6//! Invariants: see [`crate::config_map`]; the first failure is returned.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use serde_json::Value;
11
12use super::{ConfigMap, ConfigMapError, DimRule, KeyFull, KeyRule, LayerSource};
13use crate::ir::{ArchShape, LayerKind};
14
15/// 2026-09-30: What a config maps to.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MappedConfig {
18    /// 2026-09-30: The circuit arch.
19    pub arch: String,
20    /// 2026-09-30: The config's top-level `model_type`.
21    pub model_type: String,
22    /// 2026-09-30: Layer kinds and dims.
23    pub shape: ArchShape,
24    /// 2026-09-30: The math parameters, as JSON text, by name (nested ones dotted).
25    pub params: BTreeMap<String, String>,
26}
27
28/// 2026-09-30: Map `config` (the whole `config.json`) under `map`.
29pub fn map_config(map: &ConfigMap, config: &Value) -> Result<MappedConfig, ConfigMapError> {
30    let f = &map.file;
31    let root = config
32        .as_object()
33        .ok_or_else(|| ConfigMapError::Json("the top level is not an object".into()))?;
34    let model_type = root
35        .get("model_type")
36        .and_then(Value::as_str)
37        .ok_or_else(|| missing(map, "model_type"))?
38        .to_string();
39    if !f.model_types.contains(&model_type) {
40        return Err(ConfigMapError::Refused {
41            key: "model_type".into(),
42            value: json(&Value::String(model_type)),
43            why: format!("the `{}` config map serves {:?}", map.arch, f.model_types),
44        });
45    }
46    let mut params = BTreeMap::new();
47    let obj = match &f.nest {
48        Some(nest) => {
49            let inner = root
50                .get(nest)
51                .and_then(Value::as_object)
52                .ok_or_else(|| missing(map, nest))?;
53            let others: BTreeMap<&str, &Value> = root
54                .iter()
55                .filter(|(k, _)| k.as_str() != nest)
56                .map(|(k, v)| (k.as_str(), v))
57                .collect();
58            classify(map, ("", ""), &others, &f.root, &mut params)?;
59            inner
60        }
61        None => root,
62    };
63    let fields: BTreeMap<&str, &Value> = obj
64        .iter()
65        .filter(|(_, v)| !v.is_null())
66        .map(|(k, v)| (k.as_str(), v))
67        .collect();
68    let variant = f.variant.get(&model_type).cloned().unwrap_or_default();
69    let mut dims_rules = f.dims.clone();
70    dims_rules.extend(variant.dims);
71    let mut key_rules = f.keys.clone();
72    key_rules.extend(variant.keys);
73
74    let mut consumed: BTreeSet<&str> = BTreeSet::new();
75    let layer_kinds = layers(map, &fields, &mut consumed)?;
76    let dims = dims(map, &fields, &dims_rules, &mut consumed)?;
77    let rest: BTreeMap<&str, &Value> = obj
78        .iter()
79        .filter(|(k, _)| !consumed.contains(k.as_str()))
80        .map(|(k, v)| (k.as_str(), v))
81        .collect();
82    // 2026-09-30: Errors name the key's full path; params are named relative to the model's
83    // object, so one name means one thing across nested and flat configs.
84    let prefix = f.nest.as_ref().map_or(String::new(), |n| format!("{n}."));
85    classify(map, (&prefix, ""), &rest, &key_rules, &mut params)?;
86    Ok(MappedConfig {
87        arch: map.arch.clone(),
88        model_type,
89        shape: ArchShape { layer_kinds, dims },
90        params,
91    })
92}
93
94fn missing(map: &ConfigMap, key: &str) -> ConfigMapError {
95    ConfigMapError::MissingKey {
96        arch: map.arch.clone(),
97        key: key.to_string(),
98    }
99}
100
101fn json(v: &Value) -> String {
102    serde_json::to_string(v).unwrap_or_else(|_| "?".into())
103}
104
105fn uint(key: &str, v: &Value) -> Result<u64, ConfigMapError> {
106    v.as_u64().ok_or_else(|| {
107        ConfigMapError::Json(format!("`{key}` = {} is not an unsigned integer", json(v)))
108    })
109}
110
111fn layers<'a>(
112    map: &ConfigMap,
113    fields: &BTreeMap<&'a str, &'a Value>,
114    consumed: &mut BTreeSet<&'a str>,
115) -> Result<Vec<LayerKind>, ConfigMapError> {
116    let l = &map.file.layers;
117    let (count_key, count_v) = fields
118        .get_key_value(l.count.as_str())
119        .ok_or_else(|| missing(map, &l.count))?;
120    consumed.insert(count_key);
121    let count = uint(&l.count, count_v)? as usize;
122    let kind = |name: &str, key: &str| {
123        LayerKind::parse(name).ok_or_else(|| {
124            ConfigMapError::Schema(format!("`{key}` maps to unknown layer kind `{name}`"))
125        })
126    };
127    let present: Vec<(&LayerSource, &'a str, &'a Value)> = l
128        .sources
129        .iter()
130        .filter_map(|s| {
131            fields
132                .get_key_value(s.key.as_str())
133                .map(|(k, v)| (s, *k, *v))
134        })
135        .collect();
136    for (_, k, _) in &present {
137        consumed.insert(k);
138    }
139    let kinds = match (present.as_slice(), &l.uniform) {
140        ([], Some(u)) => vec![kind(u, "uniform")?; count],
141        ([], None) => {
142            let keys: Vec<&str> = l.sources.iter().map(|s| s.key.as_str()).collect();
143            return Err(missing(map, &keys.join(" | ")));
144        }
145        ([(src, key, v)], None) => from_source(src, key, v)?
146            .into_iter()
147            .map(|n| kind(&n, key))
148            .collect::<Result<_, _>>()?,
149        _ => {
150            let keys: Vec<&str> = present.iter().map(|(_, k, _)| *k).collect();
151            return Err(ConfigMapError::Refused {
152                key: keys.join(", "),
153                value: "(several)".into(),
154                why: "the layer kinds must come from exactly one source".into(),
155            });
156        }
157    };
158    if kinds.len() != count {
159        return Err(ConfigMapError::Refused {
160            key: l.count.clone(),
161            value: count.to_string(),
162            why: format!("the layer-kind source lists {} layers", kinds.len()),
163        });
164    }
165    Ok(kinds)
166}
167
168fn from_source(src: &LayerSource, key: &str, v: &Value) -> Result<Vec<String>, ConfigMapError> {
169    let refuse = |value: String, why: String| ConfigMapError::Refused {
170        key: key.to_string(),
171        value,
172        why,
173    };
174    if let Some(list) = v.as_array() {
175        list.iter()
176            .map(|e| {
177                let s = e.as_str().unwrap_or_default();
178                src.values.get(s).cloned().ok_or_else(|| {
179                    refuse(
180                        json(e),
181                        format!(
182                            "a layer kind this arch maps: {:?}",
183                            src.values.keys().collect::<Vec<_>>()
184                        ),
185                    )
186                })
187            })
188            .collect()
189    } else if let Some(s) = v.as_str() {
190        s.chars()
191            .map(|c| {
192                src.chars.get(&c.to_string()).cloned().ok_or_else(|| {
193                    refuse(
194                        format!("'{c}'"),
195                        format!(
196                            "pattern letters this arch maps: {:?}",
197                            src.chars.keys().collect::<Vec<_>>()
198                        ),
199                    )
200                })
201            })
202            .collect()
203    } else {
204        Err(ConfigMapError::Json(format!(
205            "`{key}` is neither a list nor a pattern string"
206        )))
207    }
208}
209
210fn dims<'a>(
211    map: &ConfigMap,
212    fields: &BTreeMap<&'a str, &'a Value>,
213    rules: &BTreeMap<String, DimRule>,
214    consumed: &mut BTreeSet<&'a str>,
215) -> Result<BTreeMap<String, u64>, ConfigMapError> {
216    let mut out = BTreeMap::new();
217    let mut deferred = Vec::new();
218    for (name, rule) in rules {
219        let full = match rule {
220            DimRule::Key(k) => super::DimFull {
221                key: Some(k.clone()),
222                bool: false,
223                bool_present: false,
224                constant: None,
225                or_dim: None,
226                or_div: None,
227                default: None,
228                allowed: None,
229            },
230            DimRule::Full(f) => f.clone(),
231        };
232        if let Some(c) = full.constant {
233            out.insert(name.clone(), c);
234            continue;
235        }
236        let key = full
237            .key
238            .as_deref()
239            .ok_or_else(|| ConfigMapError::Schema(format!("dim `{name}` has no key or const")))?;
240        if full.bool_present {
241            let present = fields.get_key_value(key);
242            if let Some((k, _)) = present {
243                consumed.insert(k);
244            }
245            out.insert(name.clone(), u64::from(present.is_some()));
246            continue;
247        }
248        if let Some((k, v)) = fields.get_key_value(key) {
249            consumed.insert(k);
250            let value = match (full.bool, v.as_bool()) {
251                (true, Some(b)) => u64::from(b),
252                _ => uint(key, v)?,
253            };
254            if let Some(allowed) = &full.allowed
255                && !allowed.contains(&value)
256            {
257                return Err(ConfigMapError::Refused {
258                    key: key.to_string(),
259                    value: value.to_string(),
260                    why: format!("the circuit models only {allowed:?}"),
261                });
262            }
263            out.insert(name.clone(), value);
264        } else if let Some(d) = full.default {
265            out.insert(name.clone(), d);
266        } else if full.or_dim.is_some() || full.or_div.is_some() {
267            deferred.push((name.clone(), full));
268        } else {
269            return Err(missing(map, key));
270        }
271    }
272    for (name, full) in deferred {
273        let get = |d: &str| {
274            out.get(d).copied().ok_or_else(|| {
275                ConfigMapError::Schema(format!("dim `{name}` falls back to unmapped dim `{d}`"))
276            })
277        };
278        let value = if let Some(d) = &full.or_dim {
279            get(d)?
280        } else if let Some([a, b]) = &full.or_div {
281            let (a, b) = (get(a)?, get(b)?);
282            if b == 0 || a % b != 0 {
283                return Err(ConfigMapError::Refused {
284                    key: full.key.clone().unwrap_or_default(),
285                    value: "(absent)".into(),
286                    why: format!("its fallback {a} / {b} is not exact"),
287                });
288            }
289            a / b
290        } else {
291            unreachable!("deferred only with a fallback")
292        };
293        out.insert(name, value);
294    }
295    Ok(out)
296}
297
298fn to_json(v: &toml::Value) -> Value {
299    serde_json::to_value(v).unwrap_or(Value::Null)
300}
301
302/// 2026-09-30: Classify every key of `obj` under `rules`, and apply each rule, present or not.
303/// `prefix` is `(path prefix for errors, name prefix for params)`.
304fn classify(
305    map: &ConfigMap,
306    prefix: (&str, &str),
307    obj: &BTreeMap<&str, &Value>,
308    rules: &BTreeMap<String, KeyRule>,
309    params: &mut BTreeMap<String, String>,
310) -> Result<(), ConfigMapError> {
311    let (path_prefix, name_prefix) = prefix;
312    for (k, v) in obj {
313        if !v.is_null() && !rules.contains_key(*k) {
314            return Err(ConfigMapError::UnmappedKey {
315                arch: map.arch.clone(),
316                key: format!("{path_prefix}{k}"),
317            });
318        }
319    }
320    for (key, rule) in rules {
321        let path = format!("{path_prefix}{key}");
322        let name = format!("{name_prefix}{key}");
323        let v = obj.get(key.as_str()).copied().filter(|v| !v.is_null());
324        match rule {
325            KeyRule::Word(w) if w == "ignore" => {}
326            KeyRule::Word(w) if w == "param" => {
327                if let Some(v) = v {
328                    params.insert(name, json(v));
329                }
330            }
331            KeyRule::Word(w) => {
332                return Err(ConfigMapError::Schema(format!(
333                    "`{path}`: unknown rule `{w}` (ignore | param | a table)"
334                )));
335            }
336            KeyRule::Full(f) => apply_full(map, (&path, &name), v, f, params)?,
337        }
338    }
339    Ok(())
340}
341
342fn apply_full(
343    map: &ConfigMap,
344    (path, own): (&str, &str),
345    v: Option<&Value>,
346    f: &KeyFull,
347    params: &mut BTreeMap<String, String>,
348) -> Result<(), ConfigMapError> {
349    if f.ignore.is_some() {
350        return Ok(());
351    }
352    if let Some(why) = &f.refuse {
353        return match v {
354            Some(v) => Err(ConfigMapError::Refused {
355                key: path.to_string(),
356                value: json(v),
357                why: why.clone(),
358            }),
359            None => Ok(()),
360        };
361    }
362    let name = match &f.param {
363        Some(toml::Value::String(s)) => Some(s.clone()),
364        Some(toml::Value::Boolean(true)) => Some(own.to_string()),
365        Some(other) => {
366            return Err(ConfigMapError::Schema(format!(
367                "`{path}`: `param` is {other}, want a name or true"
368            )));
369        }
370        None => None,
371    };
372    if let Some(sub) = &f.object {
373        let Some(v) = v else {
374            return Ok(());
375        };
376        let o = v
377            .as_object()
378            .ok_or_else(|| ConfigMapError::Json(format!("`{path}` is not an object")))?;
379        let fields: BTreeMap<&str, &Value> = o.iter().map(|(k, v)| (k.as_str(), v)).collect();
380        let names = format!("{}.", name.unwrap_or_else(|| own.to_string()));
381        return classify(map, (&format!("{path}."), &names), &fields, sub, params);
382    }
383    let value = match (v, &f.default) {
384        (Some(v), _) => Some(v.clone()),
385        (None, Some(d)) => Some(to_json(d)),
386        (None, None) => None,
387    };
388    let Some(mut value) = value else {
389        return Ok(());
390    };
391    if let Some(s) = value.as_str()
392        && let Some(r) = f.values.get(s)
393    {
394        value = Value::String(r.clone());
395    }
396    if let Some(req) = &f.require {
397        let allowed: Vec<Value> = match req {
398            toml::Value::Array(a) => a.iter().map(to_json).collect(),
399            one => vec![to_json(one)],
400        };
401        if !allowed.contains(&value) {
402            return Err(ConfigMapError::Refused {
403                key: path.to_string(),
404                value: json(&value),
405                why: f.why.clone().unwrap_or_else(|| {
406                    format!("the circuit models only {}", json(&Value::Array(allowed)))
407                }),
408            });
409        }
410    }
411    if let Some(n) = name {
412        params.insert(n, json(&value));
413    }
414    Ok(())
415}