use super::*;
fn ref_closure(
refnames: &std::collections::BTreeSet<String>,
named: &Named,
) -> std::collections::BTreeSet<String> {
let mut out: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut stack: Vec<String> = refnames.iter().cloned().collect();
while let Some(n) = stack.pop() {
if out.contains(&n) {
continue;
}
let Some(decl) = named.get(&n) else { continue };
if decl_has_match(decl) {
continue; }
out.insert(n.clone());
let body = decl_body(decl);
let mut refs = Vec::new();
collect_apply_names_sv(&body, &mut refs, &mut PtrSet::default());
for r in refs {
stack.push(r);
}
}
out
}
fn authored_template_names(raw_source: &Value) -> std::collections::HashMap<String, Vec<String>> {
let mut authored: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let Some(root) = raw_source.as_object() else {
return authored;
};
for compkind in ["models", "reaction_systems"] {
let Some(comps) = root.get(compkind).and_then(|v| v.as_object()) else {
continue;
};
for (cname, comp) in comps {
let Some(tpl) = comp.get("expression_templates").and_then(|v| v.as_object()) else {
continue;
};
let mut names = Vec::new();
for (n, d) in tpl {
if d.as_object().is_some_and(|o| !o.contains_key("match")) {
names.push(n.clone());
}
}
authored.insert(format!("{compkind}.{cname}"), names);
}
}
authored
}
pub fn emit_document(
raw_source: &Value,
base_path: &std::path::Path,
) -> Result<Value, ExpressionTemplateError> {
let authored = authored_template_names(raw_source);
let resolved = crate::template_imports::resolve_template_machinery(
raw_source,
base_path,
&std::collections::BTreeMap::new(),
)?;
let mut loaded = resolved.unwrap_or_else(|| raw_source.clone());
lower_expression_templates(&mut loaded)?;
let Some(root) = loaded.as_object_mut() else {
return Ok(loaded);
};
let mut bump = false;
for compkind in ["models", "reaction_systems"] {
let Some(Value::Object(comps)) = root.get_mut(compkind) else {
continue;
};
for (cname, comp_value) in comps.iter_mut() {
let Value::Object(comp) = comp_value else {
continue;
};
let key = format!("{compkind}.{cname}");
let named = comp
.get("expression_templates")
.and_then(|v| v.as_object())
.map(build_named)
.unwrap_or_default();
let mut refnames: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for (k, v) in comp.iter() {
if k == "expression_templates" || k == "expression_template_imports" {
continue;
}
let mut names = Vec::new();
collect_apply_names(v, &mut names);
for n in names {
refnames.insert(n);
}
}
if !refnames.is_empty() {
bump = true;
}
let materialized = ref_closure(&refnames, &named);
let authored_here = authored.get(&key).cloned().unwrap_or_default();
let authored_set: std::collections::HashSet<&str> =
authored_here.iter().map(String::as_str).collect();
let mut emit_block = Map::new();
for n in &authored_here {
if let Some(decl) = comp
.get("expression_templates")
.and_then(|v| v.as_object())
.and_then(|t| t.get(n))
{
emit_block.insert(n.clone(), decl.clone());
}
}
for n in &materialized {
if authored_set.contains(n.as_str()) {
continue;
}
if let Some(decl) = comp
.get("expression_templates")
.and_then(|v| v.as_object())
.and_then(|t| t.get(n))
{
emit_block.insert(n.clone(), decl.clone());
bump = true;
}
}
if emit_block.is_empty() {
comp.remove("expression_templates");
} else {
comp.insert(
"expression_templates".to_string(),
Value::Object(emit_block),
);
}
comp.remove("expression_template_imports");
}
}
root.remove("expression_template_imports");
if bump {
const RULE8_FLOOR: (u32, u32, u32) = (0, 9, 0);
let declared = root
.get("esm")
.and_then(|v| v.as_str())
.and_then(crate::diagnostic::parse_semver);
if declared.is_none_or(|d| d < RULE8_FLOOR) {
root.insert("esm".to_string(), Value::String("0.9.0".to_string()));
}
}
Ok(loaded)
}
fn canon_number(n: &serde_json::Number) -> serde_json::Number {
if n.is_i64() || n.is_u64() {
return n.clone();
}
if let Some(f) = n.as_f64()
&& f.is_finite()
&& f.fract() == 0.0
&& f >= i64::MIN as f64
&& f <= i64::MAX as f64
{
return serde_json::Number::from(f as i64);
}
n.clone()
}
fn emit_write(out: &mut String, value: &Value, indent: usize, preserve: bool) {
let pad = " ".repeat(indent);
let pad1 = " ".repeat(indent + 1);
match value {
Value::Object(map) => {
if map.is_empty() {
out.push_str("{}");
return;
}
let mut keys: Vec<&String> = map.keys().collect();
if !preserve {
keys.sort_unstable();
}
out.push_str("{\n");
for (i, k) in keys.iter().enumerate() {
out.push_str(&pad1);
out.push_str(&serde_json::to_string(k).expect("string key"));
out.push_str(": ");
let child = map.get(k.as_str()).expect("key present");
emit_write(out, child, indent + 1, k.as_str() == "expression_templates");
if i + 1 < keys.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&pad);
out.push('}');
}
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]");
return;
}
out.push_str("[\n");
for (i, v) in items.iter().enumerate() {
out.push_str(&pad1);
emit_write(out, v, indent + 1, false);
if i + 1 < items.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&pad);
out.push(']');
}
Value::Number(n) => out.push_str(&canon_number(n).to_string()),
_ => out.push_str(&serde_json::to_string(value).expect("scalar")),
}
}
pub fn emit_esm_string(doc: &Value) -> String {
let mut out = String::new();
emit_write(&mut out, doc, 0, false);
out.push('\n');
out
}
pub(crate) fn rename_apply_refs(
value: &mut Value,
rename: &std::collections::HashMap<String, String>,
) {
crate::json_visit::visit_values_mut(value, &mut |v| {
let Some(map) = v.as_object_mut() else { return };
if map.get("op").and_then(|w| w.as_str()) == Some(APPLY_OP)
&& let Some(Value::String(n)) = map.get("name")
&& let Some(newname) = rename.get(n)
{
let newname = newname.clone();
map.insert("name".to_string(), Value::String(newname));
}
});
}
pub(crate) fn registry_collision_names(
byname: &[(String, Vec<(String, Value)>)],
) -> std::collections::HashSet<String> {
let mut collide: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut refs: Vec<std::collections::HashSet<String>> = Vec::with_capacity(byname.len());
for (name, occ) in byname {
if !occ.iter().all(|o| o.1 == occ[0].1) {
collide.insert(name.clone());
}
let mut seen = std::collections::HashSet::new();
for (_path, decl) in occ {
let mut names = Vec::new();
collect_apply_names(decl, &mut names);
seen.extend(names);
}
refs.push(seen);
}
let mut changed = true;
while changed {
changed = false;
for (i, (name, _)) in byname.iter().enumerate() {
if collide.contains(name) {
continue;
}
if refs[i].iter().any(|r| collide.contains(r)) {
collide.insert(name.clone());
changed = true;
}
}
}
collide
}
pub fn flatten_template_registries(loaded: &Value) -> (Value, Map<String, Value>) {
let mut root = loaded.clone();
let mut comps: Vec<(String, Map<String, Value>)> = Vec::new();
if let Some(root_obj) = root.as_object() {
for compkind in ["models", "reaction_systems"] {
let Some(cs) = root_obj.get(compkind).and_then(|v| v.as_object()) else {
continue;
};
for (cname, comp) in cs {
let mut named = Map::new();
if let Some(tpl) = comp.get("expression_templates").and_then(|v| v.as_object()) {
for (n, d) in tpl {
if d.as_object().is_some_and(|o| o.contains_key("match")) {
continue; }
named.insert(n.clone(), d.clone());
}
}
comps.push((cname.clone(), named));
}
}
}
let mut byname: Vec<(String, Vec<(String, Value)>)> = Vec::new();
for (path, named) in &comps {
let mut names: Vec<&String> = named.keys().collect();
names.sort_unstable();
for n in names {
match byname.iter_mut().find(|(k, _)| k == n) {
Some((_, occ)) => occ.push((path.clone(), named[n].clone())),
None => byname.push((n.clone(), vec![(path.clone(), named[n].clone())])),
}
}
}
byname.sort_by(|a, b| a.0.cmp(&b.0));
let mut merged: Map<String, Value> = Map::new();
let mut rename: std::collections::HashMap<String, std::collections::HashMap<String, String>> =
std::collections::HashMap::new();
let collide = registry_collision_names(&byname);
for (name, occ) in &byname {
if collide.contains(name) {
for (path, decl) in occ {
let newname = format!("{path}.{name}");
merged.insert(newname.clone(), decl.clone());
rename
.entry(path.clone())
.or_default()
.insert(name.clone(), newname);
}
} else {
merged.insert(name.clone(), occ[0].1.clone()); }
}
let paths: Vec<String> = comps.iter().map(|(p, _)| p.clone()).collect();
if let Some(root_obj) = root.as_object_mut() {
for compkind in ["models", "reaction_systems"] {
let Some(Value::Object(cs)) = root_obj.get_mut(compkind) else {
continue;
};
for (cname, comp_value) in cs.iter_mut() {
let Value::Object(comp) = comp_value else {
continue;
};
if let Some(rn) = rename.get(cname) {
let keys: Vec<String> = comp.keys().cloned().collect();
for k in keys {
if k == "expression_templates" {
continue;
}
if let Some(v) = comp.get_mut(&k) {
rename_apply_refs(v, rn);
}
}
}
comp.remove("expression_templates");
}
}
}
for path in &paths {
if let Some(rn) = rename.get(path) {
for new in rn.values() {
if let Some(decl) = merged.get_mut(new) {
rename_apply_refs(decl, rn);
}
}
}
}
(root, merged)
}