use crate::lower_expression_templates::{
ExpressionTemplateError, collect_apply_names as compose_collect_apply_names,
reject_expression_templates_pre_v04, validate_template_body_references, validate_templates,
};
use indexmap::IndexMap;
use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::path::{Component, Path, PathBuf};
const COMPONENT_KINDS: [&str; 2] = ["models", "reaction_systems"];
const APPLY_OP: &str = "apply_expression_template";
const LIBRARY_FORBIDDEN_KEYS: [&str; 5] = [
"models",
"reaction_systems",
"data_sources",
"coupling",
"domain",
];
const PROTECTED_KEYS: [&str; 11] = [
"metadata",
"params",
"type",
"units",
"kind",
"description",
"name",
"expression_template_imports",
"metaparameters",
"only",
"where",
];
const RENAME_AXIS_KEYS: [&str; 3] = ["wrt", "dim", "var"];
const NODE_HEADER_KEYS: [&str; 3] = ["op", "id", "expect_cadence"];
const REGISTRY_KEYS: [&str; 9] = [
"reduce", "semiring", "manifold", "fn", "table", "side", "attrs", "members", "from_faq",
];
const OPAQUE_KEYS: [&str; 33] = [
"on",
"syms",
"arg",
"output_idx",
"of",
"output",
"handler_id",
"ref",
"model",
"reaction_system",
"prefix",
"rename",
"rebind",
"index_set_rename",
"source",
"file_variable",
"path",
"direction",
"hook",
"root_find",
"system_kind",
"element_type",
"scale",
"format",
"unmapped",
"default_units",
"label",
"location",
"notes",
"citation",
"doi",
"url",
"_comment",
];
const NAME_KEYED_MAP_KEYS: [&str; 19] = [
"variables",
"species",
"parameters",
"guesses",
"subsystems",
"expression_templates",
"ranges",
"axes",
"bindings",
"config",
"coords",
"initial_conditions",
"parameter_overrides",
"pinned_coords",
"map",
"rename",
"rebind",
"index_set_rename",
"where",
];
const RENAME_BOUND_KEYS: [&str; 2] = ["lower", "upper"];
fn is_meta_subst_skipped(k: &str) -> bool {
PROTECTED_KEYS.contains(&k)
|| RENAME_AXIS_KEYS.contains(&k)
|| NODE_HEADER_KEYS.contains(&k)
|| REGISTRY_KEYS.contains(&k)
|| OPAQUE_KEYS.contains(&k)
}
fn is_rename_protected(k: &str) -> bool {
PROTECTED_KEYS.contains(&k)
|| RENAME_AXIS_KEYS.contains(&k)
|| NODE_HEADER_KEYS.contains(&k)
|| REGISTRY_KEYS.contains(&k)
}
use crate::diagnostic::{codes, err};
pub fn reject_template_imports_pre_v08(view: &Value) -> Result<(), ExpressionTemplateError> {
let Some(obj) = view.as_object() else {
return Ok(());
};
let Some(esm) = obj.get("esm").and_then(|v| v.as_str()) else {
return Ok(());
};
let Some((major, minor, _)) = crate::diagnostic::parse_semver(esm) else {
return Ok(());
};
if !(major == 0 && minor < 8) {
return Ok(());
}
let mut offences: Vec<String> = Vec::new();
if obj.contains_key("expression_templates") {
offences.push("/expression_templates".to_string());
}
if obj.contains_key("metaparameters") {
offences.push("/metaparameters".to_string());
}
if obj.contains_key("expression_template_imports") {
offences.push("/expression_template_imports".to_string());
}
for compkind in COMPONENT_KINDS {
if let Some(comps) = obj.get(compkind).and_then(|v| v.as_object()) {
for (cname, comp) in comps {
if let Some(comp_obj) = comp.as_object()
&& comp_obj.contains_key("expression_template_imports")
{
offences.push(format!("/{compkind}/{cname}/expression_template_imports"));
}
}
}
}
if offences.is_empty() {
return Ok(());
}
Err(err(
codes::TEMPLATE_IMPORT_VERSION_TOO_OLD,
format!(
"expression_template_imports / top-level expression_templates / metaparameters \
require esm >= 0.8.0; file declares {esm}. Offending paths: {}",
offences.join(", ")
),
))
}
pub fn is_template_library_doc(raw: &Value) -> bool {
raw.as_object()
.is_some_and(|o| o.contains_key("expression_templates"))
}
pub(crate) fn reject_impure_template_library(view: &Value) -> Result<(), ExpressionTemplateError> {
if !is_template_library_doc(view) {
return Ok(());
}
let present: Vec<String> = LIBRARY_FORBIDDEN_KEYS
.iter()
.filter(|k| view.get(**k).is_some())
.map(|k| format!("`{k}`"))
.collect();
if present.is_empty() {
return Ok(());
}
Err(err(
codes::TEMPLATE_LIBRARY_ILLEGAL_PAYLOAD,
format!(
"top-level `expression_templates` makes this document a template-library file, which MUST NOT declare {} (esm-spec §9.7.1); templates declared there are visible to no component. Declare them in the component's own `expression_templates` block (§9.6.1), or move them to a template-library file and import it with `expression_template_imports` (§9.7.2)",
present.join(", ")
),
))
}
fn as_int(v: &Value) -> Option<i64> {
v.as_i64()
}
fn require_int(v: &Value, ctx: &str) -> Result<i64, ExpressionTemplateError> {
as_int(v).ok_or_else(|| {
err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{ctx}: value {v} is not an integer (esm-spec §9.7.6)"),
)
})
}
fn collect_metaparam_decls(
raw: &Value,
origin: &str,
) -> Result<Map<String, Value>, ExpressionTemplateError> {
let mut out = Map::new();
let Some(mp) = raw.get("metaparameters") else {
return Ok(out);
};
if mp.is_null() {
return Ok(out);
}
let Some(mp_obj) = mp.as_object() else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{origin}: `metaparameters` must be an object"),
));
};
for (name, v) in mp_obj {
let Some(decl) = v.as_object() else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{origin}: metaparameters.{name} must be an object with `type: \"integer\"`"
),
));
};
if decl.get("type").and_then(|t| t.as_str()) != Some("integer") {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{origin}: metaparameters.{name}: `type` must be \"integer\" (the only kind)"
),
));
}
if let Some(d) = decl.get("default")
&& !d.is_null()
{
require_int(d, &format!("{origin}: metaparameters.{name} default"))?;
}
out.insert(name.clone(), v.clone());
}
Ok(out)
}
fn substitute_metaparams(x: &Value, values: &BTreeMap<String, Value>) -> Value {
match x {
Value::String(s) => match values.get(s) {
Some(v) => v.clone(),
None => x.clone(),
},
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| substitute_metaparams(v, values))
.collect(),
),
Value::Object(obj) => {
let mut out = Map::new();
for (k, v) in obj {
out.insert(k.clone(), substitute_metaparams_field(k, v, values));
}
Value::Object(out)
}
_ => x.clone(),
}
}
fn substitute_metaparams_field(k: &str, v: &Value, values: &BTreeMap<String, Value>) -> Value {
if is_meta_subst_skipped(k) {
return v.clone();
}
match v {
Value::Object(entries) if NAME_KEYED_MAP_KEYS.contains(&k) => Value::Object(
entries
.iter()
.map(|(name, entry)| (name.clone(), substitute_metaparams(entry, values)))
.collect(),
),
_ => substitute_metaparams(v, values),
}
}
fn substitute_metaparams_decl(decl: &Value, values: &BTreeMap<String, Value>) -> Value {
let params: Vec<String> = decl
.get("params")
.and_then(|p| p.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if params.iter().any(|p| values.contains_key(p)) {
let mut shadowed = values.clone();
for p in ¶ms {
shadowed.remove(p);
}
substitute_metaparams(decl, &shadowed)
} else {
substitute_metaparams(decl, values)
}
}
fn try_fold(x: &Value, ctx: &str) -> Result<Option<i64>, ExpressionTemplateError> {
if let Some(i) = as_int(x) {
return Ok(Some(i));
}
if x.is_string() {
return Ok(None);
}
if x.is_number() {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: non-integer literal {x} in a structural integer site (esm-spec §9.7.6)"
),
));
}
let Some(obj) = x.as_object() else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: invalid metaparameter expression (expected integer, name, or {{op, args}})"
),
));
};
let (Some(op_raw), Some(args)) = (obj.get("op"), obj.get("args").and_then(|a| a.as_array()))
else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: invalid metaparameter expression (expected {{op: +|-|*|/, args: [...]}})"
),
));
};
if args.is_empty() {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: invalid metaparameter expression (expected {{op: +|-|*|/, args: [...]}})"
),
));
}
let mut vals: Vec<i64> = Vec::with_capacity(args.len());
for a in args {
match try_fold(a, ctx)? {
Some(v) => vals.push(v),
None => return Ok(None),
}
}
let op = op_raw.as_str().unwrap_or_default().to_string();
if !["+", "-", "*", "/"].contains(&op.as_str()) {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{ctx}: op '{op}' is not allowed in a metaparameter expression (only + - * /)"),
));
}
let overflow = || {
err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{ctx}: 64-bit integer overflow while folding a metaparameter expression"),
)
};
let mut acc = vals[0];
if op == "-" && vals.len() == 1 {
return Ok(Some(acc.checked_neg().ok_or_else(overflow)?));
}
for v in &vals[1..] {
acc = match op.as_str() {
"+" => acc.checked_add(*v).ok_or_else(overflow)?,
"-" => acc.checked_sub(*v).ok_or_else(overflow)?,
"*" => acc.checked_mul(*v).ok_or_else(overflow)?,
_ => {
if *v == 0 {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{ctx}: division by zero"),
));
}
if acc % *v != 0 {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!("{ctx}: {acc} / {v} does not divide exactly (esm-spec §9.7.6)"),
));
}
acc.checked_div(*v).ok_or_else(overflow)?
}
};
}
Ok(Some(acc))
}
fn collect_names(x: &Value, out: &mut Vec<String>) {
match x {
Value::String(s) => out.push(s.clone()),
Value::Array(arr) => {
for v in arr {
collect_names(v, out);
}
}
Value::Object(obj) => {
for (k, v) in obj {
if k == "op" {
continue;
}
collect_names(v, out);
}
}
_ => {}
}
}
fn validate_meta_expr(x: &Value, ctx: &str) -> Result<(), ExpressionTemplateError> {
if as_int(x).is_some() || x.is_string() {
return Ok(());
}
if x.is_boolean() || x.is_number() {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: non-integer literal {x} in a metaparameter expression (esm-spec §9.7.6)"
),
));
}
let Some(obj) = x.as_object() else {
return Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: invalid metaparameter expression (expected integer, name, or {{op, args}})"
),
));
};
let op = obj.get("op").and_then(|v| v.as_str());
let args = obj.get("args").and_then(|a| a.as_array());
let op_ok = matches!(op, Some("+" | "-" | "*" | "/"));
match args {
Some(a) if op_ok && !a.is_empty() => {
for elem in a {
validate_meta_expr(elem, ctx)?;
}
Ok(())
}
_ => Err(err(
codes::METAPARAMETER_TYPE_ERROR,
format!(
"{ctx}: invalid metaparameter expression (expected {{op: +|-|*|/, args: [...]}})"
),
)),
}
}
pub(crate) fn require_meta_expr(v: &Value, ctx: &str) -> Result<Value, ExpressionTemplateError> {
validate_meta_expr(v, ctx)?;
Ok(v.clone())
}
pub(crate) fn eval_meta_expr(
expr: &Value,
env: &BTreeMap<String, i64>,
ctx: &str,
) -> Result<i64, ExpressionTemplateError> {
let env_values: BTreeMap<String, Value> = env
.iter()
.map(|(k, v)| (k.clone(), Value::from(*v)))
.collect();
let substituted = substitute_metaparams(expr, &env_values);
match try_fold(&substituted, ctx)? {
Some(v) => Ok(v),
None => {
let mut names = Vec::new();
collect_names(expr, &mut names);
let free: std::collections::BTreeSet<String> =
names.into_iter().filter(|n| !env.contains_key(n)).collect();
let free_list = if free.is_empty() {
"a name".to_string()
} else {
free.into_iter().collect::<Vec<_>>().join(", ")
};
Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"{ctx}: metaparameter expression references {free_list} not in the importing \
document's metaparameter scope (esm-spec §9.7.6)"
),
))
}
}
}
fn fold_structural_sites(x: &mut Value, ctx: &str) -> Result<(), ExpressionTemplateError> {
crate::json_visit::try_visit_values_mut(x, &mut |v| {
let Some(obj) = v.as_object_mut() else {
return Ok(());
};
let op = obj
.get("op")
.and_then(|w| w.as_str())
.unwrap_or_default()
.to_string();
if op == "faq" {
if let Some(Value::Object(ranges)) = obj.get_mut("ranges") {
let keys: Vec<String> = ranges.keys().cloned().collect();
for k in keys {
if let Some(Value::Array(rv)) = ranges.get_mut(&k) {
for entry in rv.iter_mut() {
if as_int(entry).is_some() {
continue;
}
if let Some(f) =
try_fold(entry, &format!("{ctx}: aggregate ranges.{k}"))?
{
*entry = Value::from(f);
}
}
}
}
}
} else if op == "makearray"
&& let Some(Value::Array(regions)) = obj.get_mut("regions")
{
for region in regions.iter_mut() {
let Value::Array(region_arr) = region else {
continue;
};
for bounds in region_arr.iter_mut() {
let Value::Array(bounds_arr) = bounds else {
continue;
};
for entry in bounds_arr.iter_mut() {
if as_int(entry).is_some() {
continue;
}
if let Some(f) =
try_fold(entry, &format!("{ctx}: makearray regions bound"))?
{
*entry = Value::from(f);
}
}
}
}
}
Ok(())
})
}
fn has_open_index_set_size(raw: &Value) -> bool {
raw.get("index_sets")
.and_then(Value::as_object)
.is_some_and(|isets| {
isets
.values()
.any(|decl| decl.get("size").is_some_and(|sz| as_int(sz).is_none()))
})
}
fn fold_index_set_sizes(
index_sets: &mut Map<String, Value>,
ctx: &str,
strict: bool,
) -> Result<(), ExpressionTemplateError> {
for (name, decl) in index_sets.iter_mut() {
let Some(decl_obj) = decl.as_object_mut() else {
continue;
};
let Some(sz) = decl_obj.get("size") else {
continue;
};
if as_int(sz).is_some() {
continue;
}
match try_fold(sz, &format!("{ctx}: index_sets.{name}.size"))? {
Some(f) => {
decl_obj.insert("size".to_string(), Value::from(f));
}
None => {
if strict {
let mut names = Vec::new();
collect_names(sz, &mut names);
names.dedup();
return Err(err(
codes::METAPARAMETER_UNBOUND,
format!(
"{ctx}: index_sets.{name}.size references unbound name(s) {} \
(esm-spec §9.7.6)",
names.join(", ")
),
));
}
}
}
}
Ok(())
}
fn is_valid_dotted_name(s: &str) -> bool {
!s.is_empty() && s.split('.').all(is_name_segment)
}
fn is_name_segment(seg: &str) -> bool {
let mut chars = seg.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn name_map(
raw: Option<&Value>,
field: &str,
where_: &str,
) -> Result<IndexMap<String, String>, ExpressionTemplateError> {
let mut out = IndexMap::new();
let Some(raw) = raw.filter(|v| !v.is_null()) else {
return Ok(out);
};
let Some(obj) = raw.as_object() else {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_INVALID,
format!(
"{where_}: `{field}` must be an object mapping names to names (esm-spec §9.7.7)"
),
));
};
for (k, v) in obj {
if k.is_empty() {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_INVALID,
format!("{where_}: `{field}` has an empty key (esm-spec §9.7.7)"),
));
}
let valid_target = v.as_str().is_some_and(is_valid_dotted_name);
if !valid_target {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_INVALID,
format!(
"{where_}: `{field}`.{k} target {v} is not a valid dotted identifier \
(segments [A-Za-z_][A-Za-z0-9_]* joined by single dots; esm-spec §9.7.7)"
),
));
}
out.insert(k.clone(), v.as_str().unwrap().to_string());
}
Ok(out)
}
fn is_join_on_pairs(v: &Value) -> bool {
v.as_array().is_some_and(|arr| {
arr.iter()
.all(|p| p.as_array().is_some_and(|p| p.len() == 2))
})
}
fn rename_join_on(
v: &Value,
isetmap: &IndexMap<String, String>,
fallback: &dyn Fn(&str) -> String,
) -> Value {
Value::Array(
v.as_array()
.map(|arr| {
arr.iter()
.map(|pair| match pair.as_array() {
Some(p) => Value::Array(
p.iter()
.map(|e| match e.as_str() {
Some(s) => Value::String(match isetmap.get(s) {
Some(n) => n.clone(),
None => fallback(s),
}),
None => e.clone(),
})
.collect(),
),
None => pair.clone(),
})
.collect()
})
.unwrap_or_default(),
)
}
fn rename_walk(
x: &Value,
varmap: &IndexMap<String, String>,
isetmap: &IndexMap<String, String>,
tplmap: &IndexMap<String, String>,
) -> Value {
match x {
Value::String(s) => match varmap.get(s) {
Some(n) => Value::String(n.clone()),
None => x.clone(),
},
Value::Array(arr) => Value::Array(
arr.iter()
.map(|v| rename_walk(v, varmap, isetmap, tplmap))
.collect(),
),
Value::Object(obj) => {
let is_apply = obj.get("op").and_then(|v| v.as_str()) == Some(APPLY_OP);
let mut out = Map::new();
for (k, v) in obj {
if k == "from" && v.is_string() {
let s = v.as_str().unwrap();
out.insert(
k.clone(),
Value::String(isetmap.get(s).cloned().unwrap_or_else(|| s.to_string())),
);
} else if RENAME_AXIS_KEYS.contains(&k.as_str()) && v.is_string() {
let s = v.as_str().unwrap();
out.insert(
k.clone(),
Value::String(isetmap.get(s).cloned().unwrap_or_else(|| s.to_string())),
);
} else if RENAME_BOUND_KEYS.contains(&k.as_str())
&& v.as_str().is_some_and(|s| isetmap.contains_key(s))
{
let s = v.as_str().unwrap();
out.insert(k.clone(), Value::String(isetmap[s].clone()));
} else if k == "name" && is_apply && v.is_string() {
let s = v.as_str().unwrap();
out.insert(
k.clone(),
Value::String(tplmap.get(s).cloned().unwrap_or_else(|| s.to_string())),
);
} else if k == "where" && v.is_object() {
out.insert(k.clone(), rename_where(v, isetmap));
} else if k == "on" && is_join_on_pairs(v) {
out.insert(
k.clone(),
rename_join_on(v, isetmap, &|s| {
varmap.get(s).cloned().unwrap_or_else(|| s.to_string())
}),
);
} else if let Some(entries) = v
.as_object()
.filter(|_| NAME_KEYED_MAP_KEYS.contains(&k.as_str()))
{
let walked = entries
.iter()
.map(|(name, entry)| {
(name.clone(), rename_walk(entry, varmap, isetmap, tplmap))
})
.collect();
out.insert(k.clone(), Value::Object(walked));
} else if k == "of" || is_rename_protected(k) {
out.insert(k.clone(), v.clone());
} else {
out.insert(k.clone(), rename_walk(v, varmap, isetmap, tplmap));
}
}
Value::Object(out)
}
_ => x.clone(),
}
}
fn rename_where(whr: &Value, isetmap: &IndexMap<String, String>) -> Value {
let obj = match whr.as_object() {
Some(o) => o,
None => return whr.clone(),
};
let mut out = Map::new();
for (p, cobj) in obj {
match cobj.as_object() {
Some(cmap) => {
let mut cout = Map::new();
for (ck, cv) in cmap {
if ck == "shape" && cv.is_array() {
let shape = cv
.as_array()
.unwrap()
.iter()
.map(|e| match e.as_str() {
Some(s) => Value::String(
isetmap.get(s).cloned().unwrap_or_else(|| s.to_string()),
),
None => e.clone(),
})
.collect();
cout.insert(ck.clone(), Value::Array(shape));
} else {
cout.insert(ck.clone(), cv.clone());
}
}
out.insert(p.clone(), Value::Object(cout));
}
None => {
out.insert(p.clone(), cobj.clone());
}
}
}
Value::Object(out)
}
fn without_keys(
map: &IndexMap<String, String>,
pset: &std::collections::HashSet<String>,
) -> IndexMap<String, String> {
map.iter()
.filter(|(k, _)| !pset.contains(*k))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
fn rename_decl(
decl: &Value,
varmap: &IndexMap<String, String>,
isetmap: &IndexMap<String, String>,
tplmap: &IndexMap<String, String>,
) -> Value {
let pset: std::collections::HashSet<String> = decl
.get("params")
.and_then(|p| p.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if pset.is_empty() {
return rename_walk(decl, varmap, isetmap, tplmap);
}
let v2 = without_keys(varmap, &pset);
let i2 = without_keys(isetmap, &pset);
rename_walk(decl, &v2, &i2, tplmap)
}
fn mount_rename_walk(x: &mut Value, m: &IndexMap<String, String>) {
match x {
Value::Array(arr) => {
for v in arr.iter_mut() {
mount_rename_walk(v, m);
}
}
Value::Object(obj) => {
let is_node = obj.get("op").is_some_and(Value::is_string);
if is_node {
for k in RENAME_AXIS_KEYS.iter().chain(RENAME_BOUND_KEYS.iter()) {
if let Some(Value::String(s)) = obj.get_mut(*k)
&& let Some(n) = m.get(s.as_str())
{
*s = n.clone();
}
}
if let Some(Value::Object(ranges)) = obj.get_mut("ranges") {
for rv in ranges.values_mut() {
if let Some(Value::String(s)) = rv.get_mut("from")
&& let Some(n) = m.get(s.as_str())
{
*s = n.clone();
}
}
}
if let Some(Value::Array(join)) = obj.get_mut("join") {
for clause in join.iter_mut() {
let Some(on) = clause.get("on") else { continue };
if !is_join_on_pairs(on) {
continue;
}
let renamed = rename_join_on(on, m, &|s| s.to_string());
if let Some(c) = clause.as_object_mut() {
c.insert("on".to_string(), renamed);
}
}
}
} else if let Some(Value::Array(shape)) = obj.get_mut("shape") {
for e in shape.iter_mut() {
if let Value::String(s) = e
&& let Some(n) = m.get(s.as_str())
{
*s = n.clone();
}
}
}
if let Some(Value::String(s)) = obj.get_mut("gated_by")
&& let Some(n) = m.get(s.as_str())
{
*s = n.clone();
}
if let Some(Value::Object(coords)) = obj.get_mut("coords")
&& coords.keys().any(|k| m.contains_key(k))
{
let renamed: Map<String, Value> = coords
.iter()
.map(|(k, v)| (m.get(k).cloned().unwrap_or_else(|| k.clone()), v.clone()))
.collect();
*coords = renamed;
}
for v in obj.values_mut() {
mount_rename_walk(v, m);
}
}
_ => {}
}
}
pub(crate) fn apply_mount_index_set_rename(
doc: &mut Value,
edge: &Map<String, Value>,
where_: &str,
nested_contributed: &std::collections::BTreeSet<String>,
) -> Result<(), ExpressionTemplateError> {
let raw = edge.get("index_set_rename");
if raw.is_none_or(Value::is_null) {
return Ok(());
}
let requested = name_map(raw, "index_set_rename", where_)?;
let declared: Vec<String> = doc
.get("index_sets")
.and_then(|v| v.as_object())
.map(|o| {
o.keys()
.filter(|k| !nested_contributed.contains(*k))
.cloned()
.collect()
})
.unwrap_or_default();
for key in requested.keys() {
if !declared.iter().any(|d| d == key) {
return Err(err(
codes::SUBSYSTEM_INDEX_SET_RENAME_UNKNOWN_NAME,
format!(
"{where_}: `index_set_rename` names index set '{key}', which the resolved \
mounted document does not declare (it declares: {}). Keys speak the \
MOUNTED document's own post-resolution vocabulary (esm-spec §4.7 \
\"Mount-edge index-set renaming\")",
if declared.is_empty() {
"none".to_string()
} else {
declared.join(", ")
}
),
));
}
}
let changed: IndexMap<String, String> = requested.into_iter().filter(|(o, n)| o != n).collect();
if changed.is_empty() {
return Ok(());
}
let mut finals: Vec<String> = Vec::with_capacity(declared.len());
for name in &declared {
let final_name = changed.get(name).cloned().unwrap_or_else(|| name.clone());
if finals.contains(&final_name) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_COLLISION,
format!(
"{where_}: `index_set_rename` maps two index sets onto '{final_name}'; \
post-rename names must be distinct within one mount edge \
(esm-spec §4.7 / §9.7.7)"
),
));
}
finals.push(final_name);
}
mount_rename_walk(doc, &changed);
if let Some(Value::Object(sets)) = doc.get_mut("index_sets") {
let mut renamed = Map::new();
for (name, decl) in sets.iter() {
let mut decl = decl.clone();
if let Some(Value::Array(of)) = decl.get_mut("of") {
for e in of.iter_mut() {
if let Value::String(s) = e
&& let Some(n) = changed.get(s.as_str())
{
*s = n.clone();
}
}
}
let final_name = changed.get(name).cloned().unwrap_or_else(|| name.clone());
if let Some(existing) = renamed.get(&final_name)
&& existing != &decl
{
return Err(err(
codes::SUBSYSTEM_INDEX_SET_CONFLICT,
format!(
"{where_}: `index_set_rename` maps index set '{name}' onto '{final_name}', \
which a mount nested inside the referenced document already contributes \
with a non-deep-equal declaration (esm-spec §4.7)"
),
));
}
renamed.insert(final_name, decl);
}
*sets = renamed;
}
Ok(())
}
fn collect_bound_syms(x: &Value, out: &mut std::collections::HashSet<String>) {
crate::json_visit::visit_values(x, &mut |_path, v| {
let Some(obj) = v.as_object() else { return };
if !obj.contains_key("op") {
return;
}
if let Some(oi) = obj.get("output_idx").and_then(|w| w.as_array()) {
for e in oi {
if let Some(s) = e.as_str() {
out.insert(s.to_string());
}
}
}
if let Some(rg) = obj.get("ranges").and_then(|w| w.as_object()) {
for k in rg.keys() {
out.insert(k.clone());
}
}
});
}
fn collect_ref_names(
x: &Value,
shadowed: &std::collections::HashSet<String>,
out: &mut std::collections::HashSet<String>,
) {
match x {
Value::String(s) if !shadowed.contains(s) => {
out.insert(s.clone());
}
Value::Array(arr) => {
for v in arr {
collect_ref_names(v, shadowed, out);
}
}
Value::Object(obj) => {
for (k, v) in obj {
if k == "from"
|| RENAME_AXIS_KEYS.contains(&k.as_str())
|| k == "of"
|| is_rename_protected(k)
{
continue;
}
match v.as_object() {
Some(entries) if NAME_KEYED_MAP_KEYS.contains(&k.as_str()) => {
for entry in entries.values() {
collect_ref_names(entry, shadowed, out);
}
}
_ => collect_ref_names(v, shadowed, out),
}
}
}
_ => {}
}
}
fn apply_edge_renames(
scope: &mut TemplateScope,
entry: &Map<String, Value>,
origin: &str,
ref_str: &str,
) -> Result<(), ExpressionTemplateError> {
let where_ = format!("{origin}: import of '{ref_str}'");
let prefix_raw = entry.get("prefix").filter(|v| !v.is_null());
let rename = name_map(entry.get("rename"), "rename", &where_)?;
let rebind = name_map(entry.get("rebind"), "rebind", &where_)?;
let prefix: Option<String> = match prefix_raw {
None => None,
Some(v) => {
if !v.as_str().is_some_and(is_valid_dotted_name) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_INVALID,
format!(
"{where_}: `prefix` {v} is not a valid dotted identifier (segments \
[A-Za-z_][A-Za-z0-9_]* joined by single dots; esm-spec §9.7.7)"
),
));
}
Some(v.as_str().unwrap().to_string())
}
};
if prefix.is_none() && rename.is_empty() && rebind.is_empty() {
return Ok(());
}
let exported = exported_names(scope);
check_rename_keys_exported(&rename, &exported, &where_)?;
let maps = build_final_name_maps(scope, &rename, prefix.as_deref());
check_final_name_uniqueness(&maps, &where_)?;
let inventory = collect_name_inventory(scope);
check_rebind_keys(&rebind, &exported, &inventory, &where_)?;
check_new_name_freshness(&rebind, &maps.metamap, &inventory, &where_)?;
apply_scope_substitution(scope, &maps, &rebind);
Ok(())
}
struct EdgeNameMaps {
tplmap: IndexMap<String, String>,
isetmap: IndexMap<String, String>,
metamap: IndexMap<String, String>,
}
fn exported_names(scope: &TemplateScope) -> std::collections::HashSet<String> {
let mut exported: std::collections::HashSet<String> = std::collections::HashSet::new();
exported.extend(scope.templates.keys().cloned());
exported.extend(scope.index_sets.keys().cloned());
exported.extend(scope.metaparams.keys().cloned());
exported
}
fn check_rename_keys_exported(
rename: &IndexMap<String, String>,
exported: &std::collections::HashSet<String>,
where_: &str,
) -> Result<(), ExpressionTemplateError> {
for k in rename.keys() {
if !exported.contains(k) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_UNKNOWN_NAME,
format!(
"{where_}: `rename` names '{k}', which the target does not export at this \
edge (the surviving exports are templates after `only`, index sets, and \
metaparameters left open by this edge's `bindings`; esm-spec §9.7.7)"
),
));
}
}
Ok(())
}
fn build_final_name_maps(
scope: &TemplateScope,
rename: &IndexMap<String, String>,
prefix: Option<&str>,
) -> EdgeNameMaps {
let final_name = |n: &str| -> String {
if let Some(t) = rename.get(n) {
t.clone()
} else if let Some(p) = prefix {
format!("{p}.{n}")
} else {
n.to_string()
}
};
EdgeNameMaps {
tplmap: scope
.templates
.keys()
.map(|n| (n.clone(), final_name(n)))
.collect(),
isetmap: scope
.index_sets
.keys()
.map(|n| (n.clone(), final_name(n)))
.collect(),
metamap: scope
.metaparams
.keys()
.map(|n| (n.clone(), final_name(n)))
.collect(),
}
}
fn check_final_name_uniqueness(
maps: &EdgeNameMaps,
where_: &str,
) -> Result<(), ExpressionTemplateError> {
for (what, m) in [
("template", &maps.tplmap),
("index set", &maps.isetmap),
("metaparameter", &maps.metamap),
] {
let mut seen: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for (o, n) in m {
if let Some(prev) = seen.get(n) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_COLLISION,
format!(
"{where_}: {what} names '{prev}' and '{o}' both map to '{n}' after \
renaming (esm-spec §9.7.7)"
),
));
}
seen.insert(n.clone(), o.clone());
}
}
Ok(())
}
struct NameInventory {
free: std::collections::HashSet<String>,
bound: std::collections::HashSet<String>,
params_all: std::collections::HashSet<String>,
}
fn collect_name_inventory(scope: &TemplateScope) -> NameInventory {
let mut free: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut bound: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut params_all: std::collections::HashSet<String> = std::collections::HashSet::new();
for d in scope.templates.values() {
collect_bound_syms(d, &mut bound);
let mut shadowed: std::collections::HashSet<String> = std::collections::HashSet::new();
if let Some(params) = d.get("params").and_then(|p| p.as_array()) {
for p in params {
if let Some(s) = p.as_str() {
shadowed.insert(s.to_string());
}
}
}
params_all.extend(shadowed.iter().cloned());
collect_ref_names(d, &shadowed, &mut free);
}
for d in scope.index_sets.values() {
for f in ["offsets", "values"] {
if let Some(s) = d.get(f).and_then(|v| v.as_str()) {
free.insert(s.to_string());
}
}
}
for k in scope.metaparams.keys() {
free.remove(k); }
NameInventory {
free,
bound,
params_all,
}
}
fn check_rebind_keys(
rebind: &IndexMap<String, String>,
exported: &std::collections::HashSet<String>,
inventory: &NameInventory,
where_: &str,
) -> Result<(), ExpressionTemplateError> {
for k in rebind.keys() {
if exported.contains(k) {
return Err(err(
codes::TEMPLATE_IMPORT_REBIND_UNKNOWN_NAME,
format!(
"{where_}: `rebind` names '{k}', a declared name of the target (template / \
index set / metaparameter) — `rebind` addresses only free names; use \
`rename` for declared names (esm-spec §9.7.7)"
),
));
}
if inventory.bound.contains(k) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_INVALID,
format!(
"{where_}: `rebind` key '{k}' is a bound index symbol (`output_idx` / \
`ranges`) of an imported template, not a free name (esm-spec §9.7.7)"
),
));
}
if !inventory.free.contains(k) {
return Err(err(
codes::TEMPLATE_IMPORT_REBIND_UNKNOWN_NAME,
format!(
"{where_}: `rebind` names '{k}', which does not occur free in the imported \
declarations (esm-spec §9.7.7)"
),
));
}
}
Ok(())
}
fn check_new_name_freshness(
rebind: &IndexMap<String, String>,
metamap: &IndexMap<String, String>,
inventory: &NameInventory,
where_: &str,
) -> Result<(), ExpressionTemplateError> {
let rebind_keys: std::collections::HashSet<&str> = rebind.keys().map(String::as_str).collect();
let mut taken: std::collections::HashSet<String> = std::collections::HashSet::new();
for f in &inventory.free {
if !rebind_keys.contains(f.as_str()) {
taken.insert(f.clone());
}
}
taken.extend(inventory.bound.iter().cloned());
taken.extend(inventory.params_all.iter().cloned());
let mut newnames: Vec<String> = Vec::new();
for (o, n) in metamap {
if o != n {
newnames.push(n.clone());
}
}
for (o, n) in rebind {
if o != n {
newnames.push(n.clone());
}
}
for t in &newnames {
if taken.contains(t) {
return Err(err(
codes::TEMPLATE_IMPORT_RENAME_COLLISION,
format!(
"{where_}: renamed/rebound name '{t}' collides with a name still in use \
inside the imported declarations (a remaining free name, a bound index \
symbol, a template param, or another rename/rebind target; esm-spec §9.7.7)"
),
));
}
taken.insert(t.clone());
}
Ok(())
}
fn apply_scope_substitution(
scope: &mut TemplateScope,
maps: &EdgeNameMaps,
rebind: &IndexMap<String, String>,
) {
let mut varmap: IndexMap<String, String> = IndexMap::new();
for (o, n) in &maps.metamap {
if o != n {
varmap.insert(o.clone(), n.clone());
}
}
for (o, n) in rebind {
if o != n {
varmap.insert(o.clone(), n.clone());
}
}
let iset_changed: IndexMap<String, String> = maps
.isetmap
.iter()
.filter(|(o, n)| o != n)
.map(|(o, n)| (o.clone(), n.clone()))
.collect();
let tpl_changed: IndexMap<String, String> = maps
.tplmap
.iter()
.filter(|(o, n)| o != n)
.map(|(o, n)| (o.clone(), n.clone()))
.collect();
let mut newt = Map::new();
for (n, d) in &scope.templates {
let nd = rename_decl(d, &varmap, &iset_changed, &tpl_changed);
newt.insert(
maps.tplmap
.get(n)
.expect("tplmap covers every template")
.clone(),
nd,
);
}
scope.templates = newt;
let mut newi = Map::new();
for (n, d) in &scope.index_sets {
let mut nd = rename_walk(d, &varmap, &iset_changed, &tpl_changed);
if let Some(of) = nd.get("of").and_then(|v| v.as_array()).cloned() {
let new_of: Vec<Value> = of
.iter()
.map(|e| match e.as_str() {
Some(s) => Value::String(
iset_changed
.get(s)
.cloned()
.unwrap_or_else(|| s.to_string()),
),
None => e.clone(),
})
.collect();
if let Some(o) = nd.as_object_mut() {
o.insert("of".to_string(), Value::Array(new_of));
}
}
newi.insert(
maps.isetmap
.get(n)
.expect("isetmap covers every index set")
.clone(),
nd,
);
}
scope.index_sets = newi;
let mut newm = Map::new();
for (n, d) in &scope.metaparams {
newm.insert(
maps.metamap
.get(n)
.expect("metamap covers every metaparameter")
.clone(),
d.clone(),
);
}
scope.metaparams = newm;
}
#[derive(Default)]
struct TemplateScope {
templates: Map<String, Value>,
index_sets: Map<String, Value>,
metaparams: Map<String, Value>,
}
fn merge_named(
dst: &mut Map<String, Value>,
name: &str,
decl: Value,
code: &'static str,
what: &str,
origin: &str,
) -> Result<(), ExpressionTemplateError> {
if let Some(existing) = dst.get(name) {
if *existing == decl {
return Ok(());
}
return Err(err(
code,
format!(
"{origin}: {what} '{name}' collides with a non-deep-equal existing definition \
(esm-spec §9.7.4/§9.7.5)"
),
));
}
dst.insert(name.to_string(), decl);
Ok(())
}
fn merge_scope(
dst: &mut TemplateScope,
src: TemplateScope,
origin: &str,
) -> Result<(), ExpressionTemplateError> {
for (n, d) in src.templates {
merge_named(
&mut dst.templates,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"template",
origin,
)?;
}
for (n, d) in src.index_sets {
merge_named(
&mut dst.index_sets,
&n,
d,
codes::TEMPLATE_IMPORT_INDEX_SET_CONFLICT,
"index set",
origin,
)?;
}
for (n, d) in src.metaparams {
merge_named(
&mut dst.metaparams,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"metaparameter",
origin,
)?;
}
Ok(())
}
fn instantiate_scope(
scope: &mut TemplateScope,
values: &BTreeMap<String, Value>,
ctx: &str,
) -> Result<(), ExpressionTemplateError> {
let mut new_templates = Map::new();
for (n, d) in &scope.templates {
let mut nd = substitute_metaparams_decl(d, values);
fold_structural_sites(&mut nd, ctx)?;
new_templates.insert(n.clone(), nd);
}
scope.templates = new_templates;
let mut new_index_sets = Map::new();
for (n, d) in &scope.index_sets {
new_index_sets.insert(n.clone(), substitute_metaparams(d, values));
}
fold_index_set_sizes(&mut new_index_sets, ctx, false)?;
scope.index_sets = new_index_sets;
Ok(())
}
fn lexical_normalize(p: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in p.components() {
match comp {
Component::CurDir => {}
Component::ParentDir => {
if matches!(out.components().next_back(), Some(Component::Normal(_))) {
out.pop();
} else {
out.push("..");
}
}
other => out.push(other.as_os_str()),
}
}
out
}
fn canonical_ref(ref_str: &str, base_dir: &Path) -> String {
let expanded = crate::ref_loading::expand_env_refs(ref_str);
lexical_normalize(&base_dir.join(expanded.as_ref()))
.to_string_lossy()
.into_owned()
}
fn load_import_raw(
ref_str: &str,
base_dir: &Path,
origin: &str,
) -> Result<(Value, PathBuf), ExpressionTemplateError> {
let expanded = crate::ref_loading::expand_env_refs(ref_str);
let ref_str: &str = &expanded;
if ref_str.starts_with("http://") || ref_str.starts_with("https://") {
return Err(err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!(
"{origin}: failed to load template-library ref '{ref_str}': remote refs are not \
fetched by the Rust loader; download the file and import it by local path"
),
));
}
let path = lexical_normalize(&base_dir.join(ref_str));
let content = std::fs::read_to_string(&path).map_err(|e| {
err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!(
"{origin}: template-library file not found or unreadable: {} (from ref \
'{ref_str}'): {e}",
path.display()
),
)
})?;
let mut raw: Value = serde_json::from_str(&content).map_err(|e| {
err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!(
"{origin}: template-library ref '{}' is not valid JSON: {e}",
path.display()
),
)
})?;
crate::parse::prepare_document_ops(&mut raw).map_err(|e| {
err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!("{origin}: {}: {e}", path.display()),
)
})?;
let dir = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| base_dir.to_path_buf());
Ok((raw, dir))
}
fn resolve_import_entry(
entry: &Value,
base_dir: &Path,
stack: &mut Vec<String>,
origin: &str,
) -> Result<TemplateScope, ExpressionTemplateError> {
let Some(entry_obj) = entry.as_object() else {
return Err(err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!(
"{origin}: expression_template_imports entries must be objects with a `ref` field"
),
));
};
let ref_str = match entry_obj.get("ref").and_then(|v| v.as_str()) {
Some(s) if !s.is_empty() => s,
_ => {
return Err(err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!(
"{origin}: expression_template_imports entry requires a non-empty string `ref`"
),
));
}
};
let canonical = canonical_ref(ref_str, base_dir);
if let Some(pos) = stack.iter().position(|s| *s == canonical) {
let mut cyc: Vec<String> = stack[pos..].to_vec();
cyc.push(canonical);
return Err(err(
codes::TEMPLATE_IMPORT_CYCLE,
format!(
"{origin}: import-graph cycle detected: {} (esm-spec §9.7.2)",
cyc.join(" -> ")
),
));
}
let (raw, target_dir) = load_import_raw(ref_str, base_dir, origin)?;
reject_expression_templates_pre_v04(&raw)?;
reject_template_imports_pre_v08(&raw)?;
if crate::coupling_imports::is_coupling_library_doc(&raw) {
return Err(err(
codes::TEMPLATE_IMPORT_IS_COUPLING_LIBRARY,
format!(
"{origin}: import target '{ref_str}' is a coupling-library file (has \
`coupling_roles`), not a template library (esm-spec §10.9)"
),
));
}
if !is_template_library_doc(&raw) {
return Err(err(
codes::TEMPLATE_IMPORT_NOT_LIBRARY,
format!(
"{origin}: import target '{ref_str}' lacks top-level `expression_templates` — \
not a template-library file (esm-spec §9.7.1)"
),
));
}
for k in LIBRARY_FORBIDDEN_KEYS {
if raw.get(k).is_some() {
return Err(err(
codes::TEMPLATE_IMPORT_NOT_LIBRARY,
format!(
"{origin}: import target '{ref_str}' declares `{k}` — not a pure \
template-library file (esm-spec §9.7.1)"
),
));
}
}
if let Err(e) = crate::parse::validate_schema(&raw) {
return Err(err(
codes::TEMPLATE_IMPORT_UNRESOLVED,
format!("{origin}: import target '{ref_str}' failed schema validation: {e}"),
));
}
stack.push(canonical);
let result = process_library(&raw, &target_dir, stack, &format!("{origin} -> {ref_str}"));
stack.pop();
let mut scope = result?;
let mut values: BTreeMap<String, Value> = BTreeMap::new();
if let Some(bindings) = entry_obj.get("bindings").and_then(|v| v.as_object()) {
for (name, v) in bindings {
if !scope.metaparams.contains_key(name) {
return Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"{origin}: import of '{ref_str}' binds metaparameter '{name}', which \
the target neither declares nor re-exports (esm-spec §9.7.6)"
),
));
}
values.insert(
name.clone(),
require_meta_expr(
v,
&format!("{origin}: import of '{ref_str}', binding '{name}'"),
)?,
);
}
}
if !values.is_empty() {
instantiate_scope(&mut scope, &values, &format!("{origin} -> {ref_str}"))?;
for name in values.keys() {
scope.metaparams.remove(name);
}
}
if let Some(only) = entry_obj.get("only").and_then(|v| v.as_array()) {
let keep: Vec<String> = only
.iter()
.map(|n| n.as_str().unwrap_or_default().to_string())
.collect();
for n in &keep {
if !scope.templates.contains_key(n) {
return Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"{origin}: `only` names template '{n}', which '{ref_str}' does not \
declare (esm-spec §9.7.2)"
),
));
}
}
let keep_set: std::collections::HashSet<&str> = keep.iter().map(String::as_str).collect();
let mut closure: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut wstack: Vec<String> = Vec::new();
for n in &keep {
if let Some(d) = scope.templates.get(n)
&& let Some(body) = d.get("body")
{
compose_collect_apply_names(body, &mut wstack);
}
}
while let Some(r) = wstack.pop() {
if keep_set.contains(r.as_str())
|| closure.contains(&r)
|| !scope.templates.contains_key(&r)
{
continue;
}
closure.insert(r.clone());
if let Some(d) = scope.templates.get(&r)
&& let Some(body) = d.get("body")
{
compose_collect_apply_names(body, &mut wstack);
}
}
let mut filtered = Map::new();
for (n, d) in &scope.templates {
if keep_set.contains(n.as_str()) || closure.contains(n) {
filtered.insert(n.clone(), d.clone());
}
}
scope.templates = filtered;
}
apply_edge_renames(&mut scope, entry_obj, origin, ref_str)?;
Ok(scope)
}
fn process_library(
raw: &Value,
dir: &Path,
stack: &mut Vec<String>,
origin: &str,
) -> Result<TemplateScope, ExpressionTemplateError> {
let mut scope = TemplateScope::default();
if let Some(imports) = raw
.get("expression_template_imports")
.and_then(|v| v.as_array())
{
for entry in imports {
let sub = resolve_import_entry(entry, dir, stack, origin)?;
merge_scope(&mut scope, sub, origin)?;
}
}
let mut own = Map::new();
if let Some(tpl) = raw.get("expression_templates").and_then(|v| v.as_object()) {
for (n, d) in tpl {
own.insert(n.clone(), d.clone());
}
}
validate_templates(&own, origin)?;
for (n, d) in own.iter_mut() {
lower_library_template_enums(raw, n, d, origin)?;
}
for (n, d) in own {
merge_named(
&mut scope.templates,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"template",
origin,
)?;
}
if let Some(isets) = raw.get("index_sets").and_then(|v| v.as_object()) {
for (n, d) in isets {
merge_named(
&mut scope.index_sets,
n,
d.clone(),
codes::TEMPLATE_IMPORT_INDEX_SET_CONFLICT,
"index set",
origin,
)?;
}
}
for (n, d) in collect_metaparam_decls(raw, origin)? {
merge_named(
&mut scope.metaparams,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"metaparameter",
origin,
)?;
}
validate_template_body_references(&scope.templates, origin)?;
let own_names: Vec<String> = raw
.get("expression_templates")
.and_then(|v| v.as_object())
.map(|t| t.keys().cloned().collect())
.unwrap_or_default();
expand_library_enum_calls(raw, &mut scope.templates, &own_names, origin)?;
check_metaparam_loop_symbols(scope.metaparams.keys(), origin, &[&scope.templates])?;
Ok(scope)
}
fn expand_library_enum_calls(
library: &Value,
templates: &mut Map<String, Value>,
own_names: &[String],
origin: &str,
) -> Result<(), ExpressionTemplateError> {
let expanded =
crate::lower_expression_templates::expand_enum_bearing_calls(templates, own_names, origin)?;
let mut decls = Vec::with_capacity(expanded.len());
for (name, body) in expanded {
let Some(decl) = templates.get(&name) else {
continue;
};
let mut decl = decl.clone();
decl["body"] = body;
lower_library_template_enums(library, &name, &mut decl, origin)?;
decls.push((name, decl));
}
for (name, decl) in decls {
templates.insert(name, decl);
}
Ok(())
}
fn lower_library_template_enums(
library: &Value,
name: &str,
decl: &mut Value,
origin: &str,
) -> Result<(), ExpressionTemplateError> {
let params: HashSet<String> = decl
.get("params")
.and_then(|p| p.as_array())
.map(|ps| {
ps.iter()
.filter_map(|p| p.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let Some(body) = decl.get_mut("body") else {
return Ok(());
};
crate::lower_enums::lower_enum_ops_for_file(library, body, ¶ms).map_err(|e| {
err(
e.code,
format!(
"{origin}: template '{name}': {} — an `enum` op in a template library \
resolves against that library's own `enums` block (esm-spec §9.3)",
e.message
),
)
})
}
fn has_import_machinery(raw: &Value) -> bool {
let Some(obj) = raw.as_object() else {
return false;
};
if obj.contains_key("expression_templates")
|| obj.contains_key("metaparameters")
|| obj.contains_key("expression_template_imports")
{
return true;
}
for compkind in COMPONENT_KINDS {
if let Some(comps) = obj.get(compkind).and_then(|v| v.as_object()) {
for (_, comp) in comps {
if comp
.as_object()
.is_some_and(|c| c.contains_key("expression_template_imports"))
{
return true;
}
}
}
}
false
}
pub(crate) fn collect_mount_declared_metaparameters(
raw: &Value,
base_path: &Path,
) -> BTreeSet<String> {
collect_declared_through_refs(raw, base_path, false)
}
pub(crate) fn collect_import_reachable_metaparameters(
raw: &Value,
base_path: &Path,
) -> BTreeSet<String> {
collect_declared_through_refs(raw, base_path, true)
}
fn collect_declared_through_refs(
raw: &Value,
base_path: &Path,
follow_imports: bool,
) -> BTreeSet<String> {
let mut seen: HashSet<String> = HashSet::new();
let mut out: BTreeSet<String> = BTreeSet::new();
collect_mount_declared_into(raw, base_path, follow_imports, &mut seen, &mut out);
out
}
fn collect_mount_declared_into(
raw: &Value,
base_path: &Path,
follow_imports: bool,
seen: &mut HashSet<String>,
out: &mut BTreeSet<String>,
) {
let Some(obj) = raw.as_object() else {
return;
};
visit_import_edges(obj, base_path, follow_imports, seen, out);
for compkind in COMPONENT_KINDS {
let Some(comps) = obj.get(compkind).and_then(|v| v.as_object()) else {
continue;
};
for comp in comps.values() {
let Some(cobj) = comp.as_object() else {
continue;
};
visit_mount_ref(cobj, base_path, follow_imports, seen, out);
visit_import_edges(cobj, base_path, follow_imports, seen, out);
visit_subsystem_tree(cobj, base_path, follow_imports, seen, out);
}
}
}
fn visit_subsystem_tree(
holder: &Map<String, Value>,
base_path: &Path,
follow_imports: bool,
seen: &mut HashSet<String>,
out: &mut BTreeSet<String>,
) {
let Some(subs) = holder.get("subsystems").and_then(|v| v.as_object()) else {
return;
};
for sub in subs.values() {
let Some(sobj) = sub.as_object() else {
continue;
};
visit_mount_ref(sobj, base_path, follow_imports, seen, out);
visit_import_edges(sobj, base_path, follow_imports, seen, out);
visit_subsystem_tree(sobj, base_path, follow_imports, seen, out);
}
}
fn visit_import_edges(
holder: &Map<String, Value>,
base_path: &Path,
follow_imports: bool,
seen: &mut HashSet<String>,
out: &mut BTreeSet<String>,
) {
if !follow_imports {
return;
}
let Some(entries) = holder
.get("expression_template_imports")
.and_then(|v| v.as_array())
else {
return;
};
for entry in entries {
if let Some(eobj) = entry.as_object() {
visit_mount_ref(eobj, base_path, follow_imports, seen, out);
}
}
}
fn visit_mount_ref(
entry: &Map<String, Value>,
base_path: &Path,
follow_imports: bool,
seen: &mut HashSet<String>,
out: &mut BTreeSet<String>,
) {
let Some(ref_str) = entry.get("ref").and_then(|v| v.as_str()) else {
return;
};
let expanded = crate::ref_loading::expand_env_refs(ref_str);
let ref_str: &str = &expanded;
if ref_str.starts_with("http://") || ref_str.starts_with("https://") {
return;
}
let path = lexical_normalize(&base_path.join(ref_str));
if !seen.insert(path.to_string_lossy().into_owned()) {
return;
}
let Ok(content) = std::fs::read_to_string(&path) else {
return;
};
let Ok(child) = serde_json::from_str::<Value>(&content) else {
return;
};
if let Some(decls) = child.get("metaparameters").and_then(|v| v.as_object()) {
out.extend(decls.keys().cloned());
}
let child_dir = path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| base_path.to_path_buf());
collect_mount_declared_into(&child, &child_dir, follow_imports, seen, out);
}
pub(crate) fn fold_mount_contribution(decl: &Value, env: &BTreeMap<String, i64>) -> Value {
let Some(obj) = decl.as_object() else {
return decl.clone();
};
let Some(size) = obj.get("size") else {
return decl.clone();
};
if size.is_i64() || size.is_u64() || size.is_null() {
return decl.clone();
}
let Ok(folded) = eval_meta_expr(size, env, "index set size") else {
return decl.clone();
};
let mut out = obj.clone();
out.insert("size".to_string(), Value::from(folded));
Value::Object(out)
}
pub(crate) fn document_declares_an_extent(raw: &Value) -> bool {
raw.get("data_sources")
.and_then(|v| v.as_object())
.is_some_and(|sources| {
sources
.values()
.any(|src| src.get("extent").is_some_and(Value::is_object))
})
}
pub(crate) fn check_data_source_extents(
raw: &Value,
base_path: &Path,
mount_declared: &BTreeSet<String>,
) -> Result<(), ExpressionTemplateError> {
let Some(sources) = raw.get("data_sources").and_then(|v| v.as_object()) else {
return Ok(());
};
if document_is_in_resolved_shape(raw) {
return Ok(());
}
let declared = collect_metaparam_decls(raw, "document")?;
let mut reachable: Option<BTreeSet<String>> = None;
for (key, src) in sources {
let Some(name) = src
.get("extent")
.and_then(|e| e.get("metaparameter"))
.and_then(|v| v.as_str())
else {
continue;
};
if declared.contains_key(name) || mount_declared.contains(name) {
continue;
}
let reachable = reachable
.get_or_insert_with(|| collect_import_reachable_metaparameters(raw, base_path));
if reachable.contains(name) {
continue;
}
return Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"data_sources.{key}.extent binds metaparameter '{name}', which neither this \
document nor any document it mounts declares (esm-spec §8.9.4, §9.7.6)"
),
));
}
Ok(())
}
pub fn resolve_template_machinery(
raw_data: &Value,
base_path: &Path,
metaparameters: &BTreeMap<String, i64>,
) -> Result<Option<Value>, ExpressionTemplateError> {
resolve_template_machinery_scoped(raw_data, base_path, metaparameters, &BTreeSet::new(), false)
}
pub(crate) fn resolve_template_machinery_scoped(
raw_data: &Value,
base_path: &Path,
metaparameters: &BTreeMap<String, i64>,
mount_declared: &BTreeSet<String>,
mounted_leaf: bool,
) -> Result<Option<Value>, ExpressionTemplateError> {
if !has_import_machinery(raw_data) {
let unknown: Vec<&str> = metaparameters
.keys()
.filter(|k| !mount_declared.contains(*k))
.map(String::as_str)
.collect();
if !unknown.is_empty() {
return Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"loader API binds metaparameter(s) {} which neither this document nor any \
document it mounts declares (esm-spec §9.7.6)",
unknown.join(", ")
),
));
}
if !mounted_leaf && has_open_index_set_size(raw_data) {
let mut root = raw_data.clone();
if let Some(isets) = root.get_mut("index_sets").and_then(Value::as_object_mut) {
fold_index_set_sizes(isets, "document", true)?;
}
return Ok(Some(root));
}
return Ok(None);
}
let mut root: Map<String, Value> = raw_data
.as_object()
.cloned()
.expect("has_import_machinery implies an object");
let mut stack: Vec<String> = Vec::new();
let original_templates = root.get("expression_templates").cloned();
let original_metaparameters = root.get("metaparameters").cloned();
let mut doc_meta = collect_metaparam_decls(raw_data, "document")?;
let mut doc_isets: Map<String, Value> = root
.get("index_sets")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
let is_library = root.contains_key("expression_templates");
let mut top_templates =
resolve_top_level_scope(&root, base_path, &mut stack, &mut doc_meta, &mut doc_isets)?;
resolve_component_imports(
&mut root,
base_path,
&mut stack,
&mut doc_meta,
&mut doc_isets,
)?;
let values = close_document_metaparams(&doc_meta, metaparameters, mount_declared)?;
check_metaparam_collisions(&root, &top_templates, &doc_meta, &doc_isets)?;
substitute_closed_metaparams(&mut root, &mut top_templates, &mut doc_isets, &values);
fold_closed_document(&mut root, &mut top_templates, &mut doc_isets, !mounted_leaf)?;
if is_library {
validate_template_body_references(&top_templates, "document")?;
}
if let Some(templates) = original_templates {
root.insert("expression_templates".to_string(), templates);
}
if let Some(metaparams) = original_metaparameters {
root.insert("metaparameters".to_string(), metaparams);
}
root.remove("expression_template_imports");
if !doc_isets.is_empty() {
root.insert("index_sets".to_string(), Value::Object(doc_isets));
}
Ok(Some(Value::Object(root)))
}
fn resolve_top_level_scope(
root: &Map<String, Value>,
base_path: &Path,
stack: &mut Vec<String>,
doc_meta: &mut Map<String, Value>,
doc_isets: &mut Map<String, Value>,
) -> Result<Map<String, Value>, ExpressionTemplateError> {
if !root.contains_key("expression_templates") {
return Ok(Map::new());
}
let mut top_scope = TemplateScope::default();
if let Some(imports) = root
.get("expression_template_imports")
.and_then(|v| v.as_array())
.cloned()
{
for entry in &imports {
let sub = resolve_import_entry(entry, base_path, stack, "document")?;
merge_scope(&mut top_scope, sub, "document")?;
}
}
let mut own = Map::new();
if let Some(tpl) = root.get("expression_templates").and_then(|v| v.as_object()) {
for (n, d) in tpl {
own.insert(n.clone(), d.clone());
}
}
validate_templates(&own, "document")?;
for (n, d) in own {
merge_named(
&mut top_scope.templates,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"template",
"document",
)?;
}
for (n, d) in top_scope.index_sets {
merge_named(
doc_isets,
&n,
d,
codes::TEMPLATE_IMPORT_INDEX_SET_CONFLICT,
"index set",
"document",
)?;
}
for (n, d) in top_scope.metaparams {
merge_named(
doc_meta,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"metaparameter",
"document",
)?;
}
Ok(top_scope.templates)
}
fn resolve_component_imports(
root: &mut Map<String, Value>,
base_path: &Path,
stack: &mut Vec<String>,
doc_meta: &mut Map<String, Value>,
doc_isets: &mut Map<String, Value>,
) -> Result<(), ExpressionTemplateError> {
for compkind in COMPONENT_KINDS {
let Some(Value::Object(comps)) = root.get_mut(compkind) else {
continue;
};
let cnames: Vec<String> = comps.keys().cloned().collect();
for cname in cnames {
let corigin = format!("{compkind}.{cname}");
let Some(Value::Object(comp)) = comps.get(&cname) else {
continue;
};
let Some(imports) = comp.get("expression_template_imports").cloned() else {
continue;
};
let mut cscope = TemplateScope::default();
if let Some(entries) = imports.as_array() {
for entry in entries {
let sub = resolve_import_entry(entry, base_path, stack, &corigin)?;
merge_scope(&mut cscope, sub, &corigin)?;
}
}
if let Some(tpl) = comp.get("expression_templates").and_then(|v| v.as_object()) {
let mut own = Map::new();
for (n, d) in tpl {
own.insert(n.clone(), d.clone());
}
validate_templates(&own, &corigin)?;
for (n, d) in own {
merge_named(
&mut cscope.templates,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"template",
&corigin,
)?;
}
}
for (n, d) in cscope.index_sets {
merge_named(
doc_isets,
&n,
d,
codes::TEMPLATE_IMPORT_INDEX_SET_CONFLICT,
"index set",
&corigin,
)?;
}
for (n, d) in cscope.metaparams {
merge_named(
doc_meta,
&n,
d,
codes::TEMPLATE_IMPORT_NAME_CONFLICT,
"metaparameter",
&corigin,
)?;
}
if let Some(Value::Object(comp)) = comps.get_mut(&cname) {
comp.insert(
"expression_templates".to_string(),
Value::Object(cscope.templates),
);
comp.remove("expression_template_imports");
}
}
}
Ok(())
}
fn close_document_metaparams(
doc_meta: &Map<String, Value>,
metaparameters: &BTreeMap<String, i64>,
mount_declared: &BTreeSet<String>,
) -> Result<BTreeMap<String, i64>, ExpressionTemplateError> {
for k in metaparameters.keys() {
if !doc_meta.contains_key(k) && !mount_declared.contains(k) {
return Err(err(
codes::TEMPLATE_IMPORT_UNKNOWN_NAME,
format!(
"loader API binds metaparameter '{k}', which neither this document nor any \
document it mounts declares (esm-spec §9.7.6)"
),
));
}
}
let mut values: BTreeMap<String, i64> = BTreeMap::new();
let mut open_names: Vec<String> = Vec::new();
for (name, decl) in doc_meta {
if let Some(v) = metaparameters.get(name) {
values.insert(name.clone(), *v);
} else {
match decl.get("default").filter(|d| !d.is_null()) {
Some(d) => {
values.insert(name.clone(), as_int(d).expect("validated integer default"));
}
None => open_names.push(name.clone()),
}
}
}
if !open_names.is_empty() {
return Err(err(
codes::METAPARAMETER_UNBOUND,
format!(
"metaparameter(s) {} still open after edge bindings, loader-API bindings, and \
defaults (esm-spec §9.7.6)",
open_names.join(", ")
),
));
}
Ok(values)
}
fn check_metaparam_loop_symbols<'a>(
names: impl IntoIterator<Item = &'a String>,
origin: &str,
trees: &[&Map<String, Value>],
) -> Result<(), ExpressionTemplateError> {
let mut names = names.into_iter().peekable();
if names.peek().is_none() {
return Ok(());
}
let mut bound = std::collections::HashSet::new();
for tree in trees {
for v in tree.values() {
collect_bound_syms(v, &mut bound);
}
}
for name in names {
if bound.contains(name) {
return Err(err(
codes::METAPARAMETER_NAME_CONFLICT,
format!(
"{origin}: metaparameter '{name}' collides with a loop symbol \
(a `ranges` key or `output_idx` entry) (esm-spec §9.7.6)"
),
));
}
}
Ok(())
}
fn check_metaparam_collisions(
root: &Map<String, Value>,
top_templates: &Map<String, Value>,
doc_meta: &Map<String, Value>,
doc_isets: &Map<String, Value>,
) -> Result<(), ExpressionTemplateError> {
if doc_meta.is_empty() {
return Ok(());
}
let mut visible: std::collections::HashSet<String> = doc_isets.keys().cloned().collect();
for compkind in COMPONENT_KINDS {
if let Some(comps) = root.get(compkind).and_then(|v| v.as_object()) {
for (_, comp) in comps {
let Some(comp_obj) = comp.as_object() else {
continue;
};
for blk in ["variables", "species", "parameters"] {
if let Some(b) = comp_obj.get(blk).and_then(|v| v.as_object()) {
visible.extend(b.keys().cloned());
}
}
}
}
}
for name in doc_meta.keys() {
if visible.contains(name) {
return Err(err(
codes::METAPARAMETER_NAME_CONFLICT,
format!(
"metaparameter '{name}' collides with a visible \
variable/parameter/species/index-set name (esm-spec §9.7.6)"
),
));
}
}
check_metaparam_loop_symbols(doc_meta.keys(), "document", &[root, top_templates])
}
fn substitute_closed_metaparams(
root: &mut Map<String, Value>,
top_templates: &mut Map<String, Value>,
doc_isets: &mut Map<String, Value>,
int_values: &BTreeMap<String, i64>,
) {
if int_values.is_empty() {
return;
}
let values: BTreeMap<String, Value> = int_values
.iter()
.map(|(k, v)| (k.clone(), Value::from(*v)))
.collect();
let values = &values;
for compkind in COMPONENT_KINDS {
let Some(Value::Object(comps)) = root.get_mut(compkind) else {
continue;
};
for (_, comp_value) in comps.iter_mut() {
let Value::Object(comp) = comp_value else {
continue;
};
let keys: Vec<String> = comp.keys().cloned().collect();
for k in keys {
if k == "expression_templates"
&& comp.get(&k).map(Value::is_object).unwrap_or(false)
{
if let Some(Value::Object(tpl)) = comp.get_mut(&k) {
let tnames: Vec<String> = tpl.keys().cloned().collect();
for tn in tnames {
let nd = substitute_metaparams_decl(&tpl[&tn], values);
tpl.insert(tn, nd);
}
}
} else if let Some(v) = comp.get(&k) {
let nv = substitute_metaparams_field(&k, v, values);
comp.insert(k, nv);
}
}
}
}
let tnames: Vec<String> = top_templates.keys().cloned().collect();
for tn in tnames {
let nd = substitute_metaparams_decl(&top_templates[&tn], values);
top_templates.insert(tn, nd);
}
let mut new_isets = Map::new();
for (n, d) in doc_isets.iter() {
new_isets.insert(n.clone(), substitute_metaparams(d, values));
}
*doc_isets = new_isets;
}
fn fold_closed_document(
root: &mut Map<String, Value>,
top_templates: &mut Map<String, Value>,
doc_isets: &mut Map<String, Value>,
strict: bool,
) -> Result<(), ExpressionTemplateError> {
for compkind in COMPONENT_KINDS {
let Some(Value::Object(comps)) = root.get_mut(compkind) else {
continue;
};
for (cname, comp) in comps.iter_mut() {
fold_structural_sites(comp, &format!("{compkind}.{cname}"))?;
}
}
let tnames: Vec<String> = top_templates.keys().cloned().collect();
for tn in tnames {
let mut td = top_templates[&tn].clone();
fold_structural_sites(&mut td, &format!("document.expression_templates.{tn}"))?;
top_templates.insert(tn, td);
}
fold_index_set_sizes(doc_isets, "document", strict)
}
fn append_component_imports(comp: &mut Map<String, Value>, imports: &[Value]) {
let mut base: Vec<Value> = comp
.get("expression_template_imports")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
base.extend(imports.iter().cloned());
comp.insert(
"expression_template_imports".to_string(),
Value::Array(base),
);
}
fn apply_subsystem_ref_injection(root: &mut Value, injected: &[Value]) -> bool {
if injected.is_empty() {
return false;
}
let Some(root_obj) = root.as_object_mut() else {
return false;
};
for compkind in COMPONENT_KINDS {
let Some(Value::Object(comps)) = root_obj.get_mut(compkind) else {
continue;
};
let Some(cname) = comps.keys().next().cloned() else {
continue;
};
if let Some(Value::Object(comp)) = comps.get_mut(&cname) {
append_component_imports(comp, injected);
return true;
}
}
false
}
fn coupling_referenced_systems(entry: &Map<String, Value>) -> std::collections::HashSet<String> {
let mut out = std::collections::HashSet::new();
let ctype = entry.get("type").and_then(Value::as_str).unwrap_or("");
match ctype {
"operator_compose" | "couple" => {
if let Some(sys) = entry.get("systems").and_then(Value::as_array) {
for s in sys {
if let Some(s) = s.as_str() {
out.insert(s.to_string());
out.insert(s.split('.').next().unwrap_or(s).to_string());
}
}
}
}
"variable_map" => {
for k in ["from", "to"] {
if let Some(v) = entry.get(k).and_then(Value::as_str) {
out.insert(v.split('.').next().unwrap_or(v).to_string());
}
}
}
"event" => {
for v in entry.values() {
collect_scoped_owners(v, &mut out);
}
}
_ => {}
}
out
}
fn collect_scoped_owners(x: &Value, out: &mut std::collections::HashSet<String>) {
crate::json_visit::visit_values(x, &mut |_path, v| {
if let Value::String(s) = v
&& let Some((head, _)) = s.split_once('.')
{
out.insert(head.to_string());
}
});
}
fn apply_coupling_injections(root: &mut Value) -> Result<bool, ExpressionTemplateError> {
let Some(root_obj) = root.as_object_mut() else {
return Ok(false);
};
let has_injection = root_obj
.get("coupling")
.and_then(Value::as_array)
.is_some_and(|arr| {
arr.iter().any(|e| {
e.as_object()
.is_some_and(|o| o.contains_key("expression_template_imports"))
})
});
if !has_injection {
return Ok(false);
}
let model_keys: std::collections::HashSet<String> = root_obj
.get("models")
.and_then(Value::as_object)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
let rsys_keys: std::collections::HashSet<String> = root_obj
.get("reaction_systems")
.and_then(Value::as_object)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
let loader_keys: std::collections::HashSet<String> = root_obj
.get("data_sources")
.and_then(Value::as_object)
.map(|m| m.keys().cloned().collect())
.unwrap_or_default();
let mut plan: Vec<(&'static str, String, Vec<Value>)> = Vec::new();
{
let Some(Value::Array(coupling)) = root_obj.get_mut("coupling") else {
return Ok(false);
};
for entry in coupling.iter_mut() {
let Some(entry_obj) = entry.as_object_mut() else {
continue;
};
let Some(inj_val) = entry_obj.get("expression_template_imports").cloned() else {
continue;
};
let Some(inj_map) = inj_val.as_object() else {
return Err(err(
codes::TEMPLATE_INJECT_TARGET_NOT_COMPONENT,
"coupling entry `expression_template_imports` must be a map from a target \
system name to a list of imports (esm-spec §9.7.10 / §10.8)",
));
};
let referenced = coupling_referenced_systems(entry_obj);
for (target, imports_val) in inj_map {
if !referenced.contains(target) {
let mut refs: Vec<String> = referenced.iter().cloned().collect();
refs.sort();
let refs_str = if refs.is_empty() {
"(none)".to_string()
} else {
refs.join(", ")
};
return Err(err(
codes::TEMPLATE_INJECT_TARGET_UNKNOWN,
format!(
"coupling entry `expression_template_imports` key '{target}' names no \
system referenced by that entry (esm-spec §9.7.10 / §10.8). The entry \
references: {refs_str}."
),
));
}
let compkind = if model_keys.contains(target) {
"models"
} else if rsys_keys.contains(target) {
"reaction_systems"
} else if loader_keys.contains(target) {
return Err(err(
codes::TEMPLATE_INJECT_TARGET_IS_LOADER,
format!(
"coupling entry `expression_template_imports` key '{target}' resolves \
to a data loader, which is pure I/O with no expression positions to \
rewrite (esm-spec §9.7.10 / §14)."
),
));
} else {
return Err(err(
codes::TEMPLATE_INJECT_TARGET_NOT_COMPONENT,
format!(
"coupling entry `expression_template_imports` key '{target}' resolves \
to neither a top-level model, reaction system, nor data loader \
(esm-spec §9.7.10). Nested `Parent.Child` targets are out of scope."
),
));
};
let Some(imports_arr) = imports_val.as_array() else {
return Err(err(
codes::TEMPLATE_IMPORT_NOT_LIBRARY,
format!(
"coupling entry `expression_template_imports` value for '{target}' \
must be a list of §9.7.2 import entries (esm-spec §9.7.10 / §10.8)."
),
));
};
plan.push((compkind, target.clone(), imports_arr.clone()));
}
entry_obj.remove("expression_template_imports");
}
}
for (compkind, target, imports) in plan {
if let Some(Value::Object(comps)) = root_obj.get_mut(compkind)
&& let Some(Value::Object(comp)) = comps.get_mut(&target)
{
append_component_imports(comp, &imports);
}
}
Ok(true)
}
pub fn apply_scope_injections(
root: &mut Value,
injected: &[Value],
) -> Result<(), ExpressionTemplateError> {
apply_subsystem_ref_injection(root, injected);
apply_coupling_injections(root)?;
Ok(())
}
fn document_is_in_resolved_shape(doc: &Value) -> bool {
!document_has_unresolved_mount(doc) && index_sets_are_fully_folded(doc)
}
fn document_has_unresolved_mount(doc: &Value) -> bool {
let Some(obj) = doc.as_object() else {
return false;
};
for kind in COMPONENT_KINDS {
let Some(comps) = obj.get(kind).and_then(|v| v.as_object()) else {
continue;
};
for comp in comps.values() {
if comp.get("ref").is_some() {
return true;
}
if let Some(subs) = comp.get("subsystems").and_then(|v| v.as_object())
&& subs.values().any(|s| s.get("ref").is_some())
{
return true;
}
}
}
false
}
fn index_sets_are_fully_folded(doc: &Value) -> bool {
let Some(isets) = doc.get("index_sets").and_then(|v| v.as_object()) else {
return false;
};
if isets.is_empty() {
return false;
}
isets.values().all(|d| match d.get("size") {
None => true,
Some(v) => v.is_i64() || v.is_u64(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn dotted_name_grammar_accepts_scoped_refs_and_rejects_malformed() {
assert!(is_valid_dotted_name("a"));
assert!(is_valid_dotted_name("lib.stencil_1"));
assert!(is_valid_dotted_name("_x.y_2.z"));
assert!(!is_valid_dotted_name(""));
assert!(!is_valid_dotted_name("1abc"));
assert!(!is_valid_dotted_name("a..b"));
assert!(!is_valid_dotted_name("a."));
assert!(!is_valid_dotted_name("a-b"));
}
#[test]
fn lexical_normalize_collapses_dot_components_without_io() {
assert_eq!(
lexical_normalize(Path::new("a/./b/../c")),
PathBuf::from("a/c")
);
assert_eq!(
lexical_normalize(Path::new("./lib/tpl.json")),
PathBuf::from("lib/tpl.json")
);
assert_eq!(lexical_normalize(Path::new("../x")), PathBuf::from("../x"));
}
#[test]
fn lexical_normalize_preserves_consecutive_parent_components() {
assert_eq!(
lexical_normalize(Path::new("../../x")),
PathBuf::from("../../x")
);
assert_eq!(
lexical_normalize(Path::new("a/../../x")),
PathBuf::from("../x")
);
assert_eq!(
lexical_normalize(Path::new("./../x")),
PathBuf::from("../x")
);
assert_eq!(lexical_normalize(Path::new("../x")), PathBuf::from("../x"));
}
#[test]
fn try_fold_folds_closed_arithmetic_and_leaves_open_names_symbolic() {
assert_eq!(try_fold(&json!(3), "t").unwrap(), Some(3));
assert_eq!(
try_fold(&json!({"op": "+", "args": [1, 2, 3]}), "t").unwrap(),
Some(6)
);
assert_eq!(
try_fold(&json!({"op": "-", "args": [5]}), "t").unwrap(),
Some(-5)
);
assert_eq!(
try_fold(&json!({"op": "/", "args": [8, 2]}), "t").unwrap(),
Some(4)
);
assert_eq!(try_fold(&json!("N"), "t").unwrap(), None);
assert_eq!(
try_fold(&json!({"op": "*", "args": ["N", 2]}), "t").unwrap(),
None
);
let e = try_fold(&json!({"op": "/", "args": [7, 2]}), "t").unwrap_err();
assert_eq!(e.code, "metaparameter_type_error");
let e = try_fold(&json!(2.5), "t").unwrap_err();
assert_eq!(e.code, "metaparameter_type_error");
}
fn env(pairs: &[(&str, i64)]) -> BTreeMap<String, i64> {
pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect()
}
#[test]
fn eval_meta_expr_folds_product() {
assert_eq!(
eval_meta_expr(
&json!({"op": "*", "args": ["NX", "NY"]}),
&env(&[("NX", 18), ("NY", 20)]),
"t"
)
.unwrap(),
360
);
}
#[test]
fn eval_meta_expr_name_and_literal() {
assert_eq!(
eval_meta_expr(&json!("NX"), &env(&[("NX", 7)]), "t").unwrap(),
7
);
assert_eq!(eval_meta_expr(&json!(5), &env(&[]), "t").unwrap(), 5);
}
#[test]
fn eval_meta_expr_nested_arithmetic() {
let expr = json!({"op": "*", "args": [{"op": "+", "args": ["NX", 2]}, "NY"]});
assert_eq!(
eval_meta_expr(&expr, &env(&[("NX", 4), ("NY", 3)]), "t").unwrap(),
18
);
}
#[test]
fn require_meta_expr_returns_unfolded() {
let expr = json!({"op": "*", "args": ["NX", "NY"]});
assert_eq!(require_meta_expr(&expr, "t").unwrap(), expr); }
#[test]
fn meta_expr_helper_diagnostics() {
let cases: Vec<(Value, BTreeMap<String, i64>, &str)> = vec![
(
json!({"op": "%", "args": ["NX", 2]}),
env(&[]),
"metaparameter_type_error",
),
(
json!({"op": "*", "args": []}),
env(&[]),
"metaparameter_type_error",
),
(json!(1.5), env(&[]), "metaparameter_type_error"),
(
json!({"op": "*", "args": ["NZ", "NY"]}),
env(&[("NX", 18), ("NY", 20)]),
"template_import_unknown_name",
),
(
json!({"op": "/", "args": ["NX", 7]}),
env(&[("NX", 18)]),
"metaparameter_type_error",
),
];
for (expr, e, code) in cases {
let got = require_meta_expr(&expr, "t")
.and_then(|_| eval_meta_expr(&expr, &e, "t").map(|_| ()))
.unwrap_err();
assert_eq!(got.code, code, "expr {expr}");
}
}
#[test]
fn metaparameter_substitution_tables_match_shared_classification() {
use std::collections::BTreeSet;
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../tests/metaparameter_substitution/field_classification.json");
let cls: Value = serde_json::from_str(
&std::fs::read_to_string(&path).expect("read the classification file"),
)
.expect("parse the classification file");
let listed = |key: &str| -> BTreeSet<String> {
cls[key]
.as_array()
.unwrap_or_else(|| panic!("{key} is not an array"))
.iter()
.map(|v| v.as_str().expect("key names are strings").to_string())
.collect()
};
let skipped: BTreeSet<String> = PROTECTED_KEYS
.iter()
.chain(RENAME_AXIS_KEYS.iter())
.chain(NODE_HEADER_KEYS.iter())
.chain(REGISTRY_KEYS.iter())
.chain(OPAQUE_KEYS.iter())
.map(|k| k.to_string())
.collect();
assert_eq!(skipped, listed("skip_keys"));
for k in &skipped {
assert!(is_meta_subst_skipped(k), "{k:?} is not skipped");
}
let maps: BTreeSet<String> = NAME_KEYED_MAP_KEYS.iter().map(|k| k.to_string()).collect();
assert_eq!(maps, listed("name_keyed_map_keys"));
for k in OPAQUE_KEYS {
assert!(
!is_rename_protected(k),
"{k:?} leaked into is_rename_protected"
);
}
}
}