use super::*;
use crate::json_visit::JsonPath;
const REWRITE_TARGET_OPS: [&str; 3] = ["D", "table_lookup", "enum"];
fn op_in_t(op: &str) -> bool {
if op == APPLY_OP {
return false;
}
if REWRITE_TARGET_OPS.contains(&op) {
return true;
}
!crate::op_registry::is_core_op(op)
}
#[derive(Default)]
pub(super) struct PtrSet {
set: std::collections::HashMap<*const SNode, Sv>,
}
impl PtrSet {
pub(super) fn insert(&mut self, node: &Sv) -> bool {
self.set.insert(Rc::as_ptr(node), node.clone()).is_none()
}
}
fn direct_t_op(node: &Sv, seen: &mut PtrSet) -> bool {
direct_op(node, seen, &op_in_t)
}
fn direct_op(node: &Sv, seen: &mut PtrSet, pred: &dyn Fn(&str) -> bool) -> bool {
match &**node {
SNode::Arr(items) => {
if !seen.insert(node) {
return false;
}
items.iter().any(|c| direct_op(c, seen, pred))
}
SNode::Obj(fields) => {
if !seen.insert(node) {
return false;
}
if let Some(op) = obj_op(fields)
&& pred(op)
{
return true;
}
fields.iter().any(|(_, v)| direct_op(v, seen, pred))
}
_ => false,
}
}
pub(super) fn collect_apply_names_sv(node: &Sv, out: &mut Vec<String>, seen: &mut PtrSet) {
match &**node {
SNode::Arr(items) => {
if !seen.insert(node) {
return;
}
for c in items {
collect_apply_names_sv(c, out, seen);
}
}
SNode::Obj(fields) => {
if !seen.insert(node) {
return;
}
if obj_op(fields) == Some(APPLY_OP)
&& let Some(SNode::Str(nm)) = obj_get(fields, "name").map(|v| &**v)
{
out.push(nm.clone());
}
for (_, v) in fields {
collect_apply_names_sv(v, out, seen);
}
}
_ => {}
}
}
pub(super) type Named = std::collections::HashMap<String, Sv>;
pub(super) fn decl_body(decl: &Sv) -> Sv {
match &**decl {
SNode::Obj(fields) => obj_get(fields, "body")
.cloned()
.unwrap_or_else(|| Rc::new(SNode::Null)),
_ => Rc::new(SNode::Null),
}
}
pub(super) fn decl_has_match(decl: &Sv) -> bool {
matches!(&**decl, SNode::Obj(fields) if obj_get(fields, "match").is_some())
}
pub(super) fn transitive_reachable(
named: &Named,
direct_pred: impl Fn(&Sv) -> bool,
) -> std::collections::HashMap<String, bool> {
fn visit(
name: &str,
named: &Named,
flag: &mut std::collections::HashMap<String, bool>,
inprogress: &mut std::collections::HashSet<String>,
direct_pred: &impl Fn(&Sv) -> bool,
) -> bool {
if let Some(v) = flag.get(name) {
return *v;
}
if inprogress.contains(name) {
return false;
}
let Some(decl) = named.get(name) else {
flag.insert(name.to_string(), false);
return false;
};
inprogress.insert(name.to_string());
let body = decl_body(decl);
let mut res = direct_pred(&body);
if !res {
let mut refs = Vec::new();
collect_apply_names_sv(&body, &mut refs, &mut PtrSet::default());
for r in refs {
if named.contains_key(&r) && visit(&r, named, flag, inprogress, direct_pred) {
res = true;
break;
}
}
}
inprogress.remove(name);
flag.insert(name.to_string(), res);
res
}
let mut flag: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
let mut inprogress: std::collections::HashSet<String> = std::collections::HashSet::new();
for name in named.keys() {
visit(name, named, &mut flag, &mut inprogress, &direct_pred);
}
flag
}
fn template_target_bearing(named: &Named) -> std::collections::HashMap<String, bool> {
transitive_reachable(named, |body| direct_t_op(body, &mut PtrSet::default()))
}
fn ref_is_eager(
fields: &[(String, Sv)],
target_bearing: &std::collections::HashMap<String, bool>,
) -> bool {
let Some(SNode::Str(name)) = obj_get(fields, "name").map(|v| &**v) else {
return false;
};
if target_bearing.get(name).copied().unwrap_or(false) {
return true;
}
match obj_get(fields, "bindings") {
Some(b) => direct_t_op(b, &mut PtrSet::default()),
None => false,
}
}
const MAX_REWRITE_PASSES: usize = 64;
#[derive(Clone)]
struct MatchRule {
name: String,
param_set: std::collections::HashSet<String>,
pattern: Value,
body: Sv,
priority: i64,
where_c: Option<std::collections::BTreeMap<String, Vec<String>>>,
}
struct RewriteCtx<'a> {
named: &'a Named,
rules: &'a [MatchRule],
shape_env: &'a std::collections::BTreeMap<String, Vec<String>>,
target_bearing: &'a std::collections::HashMap<String, bool>,
}
fn rule_priority(decl: &Map<String, Value>) -> i64 {
match decl.get("priority") {
Some(Value::Number(n)) => n
.as_i64()
.or_else(|| n.as_f64().map(|f| f.round() as i64))
.unwrap_or(0),
_ => 0,
}
}
fn component_shape_env(
comp: &Map<String, Value>,
) -> std::collections::BTreeMap<String, Vec<String>> {
let mut env = std::collections::BTreeMap::new();
let Some(vars) = comp.get("variables").and_then(|v| v.as_object()) else {
return env;
};
for (vn, vd) in vars {
let Some(shp) = vd.get("shape").and_then(|s| s.as_array()) else {
continue;
};
if !shp.iter().all(|s| s.is_string()) {
continue;
}
let shape: Vec<String> = shp
.iter()
.map(|s| s.as_str().unwrap_or_default().to_string())
.collect();
env.insert(vn.clone(), shape);
}
env
}
fn where_satisfied(
where_c: &Option<std::collections::BTreeMap<String, Vec<String>>>,
bindings: &Binds,
shape_env: &std::collections::BTreeMap<String, Vec<String>>,
) -> bool {
let Some(where_c) = where_c else {
return true;
};
for (p, req) in where_c {
let Some(bound) = binds_get(bindings, p) else {
return false;
};
let SNode::Str(b) = &**bound else {
return false;
};
let Some(shp) = shape_env.get(b) else {
return false;
};
if shp != req {
return false;
}
}
true
}
fn registered_where(
decl: &Map<String, Value>,
iset_names: &std::collections::HashSet<String>,
scope: &str,
tname: &str,
) -> Result<Option<std::collections::BTreeMap<String, Vec<String>>>, ExpressionTemplateError> {
let Some(whr) = decl.get("where").and_then(|v| v.as_object()) else {
return Ok(None);
};
let mut out = std::collections::BTreeMap::new();
for (p, cobj) in whr {
let shp = cobj.get("shape").and_then(|v| v.as_array());
let req: Vec<String> = shp
.map(|a| {
a.iter()
.map(|s| s.as_str().unwrap_or_default().to_string())
.collect()
})
.unwrap_or_default();
for s in &req {
if !iset_names.contains(s) {
return Err(err(
codes::TEMPLATE_CONSTRAINT_UNKNOWN_INDEX_SET,
format!(
"{scope}.expression_templates.{tname}: where.{p}.shape names index set \
'{s}', which the consuming document's index_sets registry does not \
declare (esm-spec §9.6.1/§9.6.6)"
),
));
}
}
out.insert(p.clone(), req);
}
Ok(Some(out))
}
fn collect_match_rules(
templates: &Map<String, Value>,
named: &Named,
iset_names: &std::collections::HashSet<String>,
scope: &str,
) -> Result<Vec<MatchRule>, ExpressionTemplateError> {
let mut rules = Vec::new();
for (name, decl) in templates {
let Some(obj) = decl.as_object() else {
continue;
};
let Some(pattern) = obj.get("match") else {
continue;
};
let param_set: std::collections::HashSet<String> = obj
.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();
let body = named
.get(name)
.map(decl_body)
.unwrap_or_else(|| Rc::new(SNode::Null));
let where_c = registered_where(obj, iset_names, scope, name)?;
rules.push(MatchRule {
name: name.clone(),
param_set,
pattern: pattern.clone(),
body,
priority: rule_priority(obj),
where_c,
});
}
rules.sort_by_key(|r| std::cmp::Reverse(r.priority));
Ok(rules)
}
fn try_match(
pattern: &Value,
target: &Sv,
params: &std::collections::HashSet<String>,
binds: &mut Binds,
) -> bool {
match pattern {
Value::String(s) => {
if params.contains(s.as_str()) {
match binds.iter().position(|(k, _)| k == s) {
Some(i) => {
let prev = binds[i].1.clone();
sv_eq(&prev, target)
}
None => {
binds.push((s.clone(), target.clone()));
true
}
}
} else {
value_eq_sv(pattern, target)
}
}
Value::Array(parr) => match &**target {
SNode::Arr(tarr) if parr.len() == tarr.len() => parr
.iter()
.zip(tarr.iter())
.all(|(p, t)| try_match(p, t, params, binds)),
_ => false,
},
Value::Object(pobj) => match &**target {
SNode::Obj(tfields) => pobj.iter().all(|(k, pv)| match obj_get(tfields, k) {
Some(tv) => try_match(pv, tv, params, binds),
None => false,
}),
_ => false,
},
_ => value_eq_sv(pattern, target),
}
}
fn expand_apply(
node: &[(String, Sv)],
named: &Named,
scope: &str,
) -> Result<Sv, ExpressionTemplateError> {
let name = match obj_get(node, "name").map(|v| &**v) {
Some(SNode::Str(s)) => Some(s.as_str()),
_ => None,
}
.ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}: apply_expression_template node missing or empty 'name'"),
)
})?;
if name.is_empty() {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}: apply_expression_template 'name' must be non-empty"),
));
}
let decl = named.get(name).ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE,
format!("{scope}: apply_expression_template references undeclared template '{name}'"),
)
})?;
let SNode::Obj(decl_fields) = &**decl else {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}: template '{name}' declaration is not an object"),
));
};
let bindings: &[(String, Sv)] = match obj_get(node, "bindings").map(|v| &**v) {
Some(SNode::Obj(fields)) => fields,
_ => {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!("{scope}: apply_expression_template '{name}' missing 'bindings' object"),
));
}
};
let params: Vec<&str> = match obj_get(decl_fields, "params").map(|v| &**v) {
Some(SNode::Arr(items)) => items
.iter()
.filter_map(|v| match &**v {
SNode::Str(s) => Some(s.as_str()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let declared: std::collections::HashSet<&str> = params.iter().copied().collect();
let provided: std::collections::HashSet<&str> =
bindings.iter().map(|(k, _)| k.as_str()).collect();
for p in ¶ms {
if !provided.contains(p) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!(
"{scope}: apply_expression_template '{name}' missing binding for param '{p}'"
),
));
}
}
for (p, _) in bindings {
if !declared.contains(p.as_str()) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!("{scope}: apply_expression_template '{name}' supplies unknown param '{p}'"),
));
}
}
let resolved: Binds = bindings.to_vec();
let body = decl_body(decl);
Ok(substitute(&body, &resolved))
}
fn expand_eager(
node: &Sv,
named: &Named,
target_bearing: &std::collections::HashMap<String, bool>,
scope: &str,
memo: &mut PtrMemo<Sv>,
) -> Result<Sv, ExpressionTemplateError> {
expand_refs_where(node, named, scope, memo, &|fields| {
ref_is_eager(fields, target_bearing)
})
}
type ExpandRefPredicate<'a> = dyn Fn(&[(String, Sv)]) -> bool + 'a;
fn expand_refs_where(
node: &Sv,
named: &Named,
scope: &str,
memo: &mut PtrMemo<Sv>,
expand: &ExpandRefPredicate,
) -> Result<Sv, ExpressionTemplateError> {
match &**node {
SNode::Obj(fields) => {
if let Some(hit) = memo.get(node) {
return Ok(hit.clone());
}
let res = if obj_op(fields) == Some(APPLY_OP) {
let mut newfields = fields.clone();
let mut b_changed = false;
if let Some(b_idx) = newfields.iter().position(|(k, _)| k == "bindings")
&& let SNode::Obj(b) = &*newfields[b_idx].1.clone()
{
let mut nb = Vec::with_capacity(b.len());
for (k, v) in b {
let rv = expand_refs_where(v, named, scope, memo, expand)?;
b_changed |= !Rc::ptr_eq(&rv, v);
nb.push((k.clone(), rv));
}
if b_changed {
newfields[b_idx].1 = Rc::new(SNode::Obj(nb));
}
}
if expand(&newfields) {
let body = expand_apply(&newfields, named, scope)?;
expand_refs_where(&body, named, scope, memo, expand)?
} else if b_changed {
Rc::new(SNode::Obj(newfields))
} else {
node.clone()
}
} else {
let mut changed = false;
let mut out = Vec::with_capacity(fields.len());
for (k, v) in fields {
let rv = expand_refs_where(v, named, scope, memo, expand)?;
changed |= !Rc::ptr_eq(&rv, v);
out.push((k.clone(), rv));
}
if changed {
Rc::new(SNode::Obj(out))
} else {
node.clone()
}
};
memo.insert(node, res.clone());
Ok(res)
}
SNode::Arr(items) => {
if let Some(hit) = memo.get(node) {
return Ok(hit.clone());
}
let mut changed = false;
let mut out = Vec::with_capacity(items.len());
for v in items {
let rv = expand_refs_where(v, named, scope, memo, expand)?;
changed |= !Rc::ptr_eq(&rv, v);
out.push(rv);
}
let res = if changed {
Rc::new(SNode::Arr(out))
} else {
node.clone()
};
memo.insert(node, res.clone());
Ok(res)
}
_ => Ok(node.clone()),
}
}
pub(crate) fn expand_enum_bearing_calls(
templates: &Map<String, Value>,
names: &[String],
scope: &str,
) -> Result<Vec<(String, Value)>, ExpressionTemplateError> {
let named: Named = templates
.iter()
.map(|(k, v)| (k.clone(), to_shared(v)))
.collect();
let is_enum = |op: &str| op == "enum";
let bearing = transitive_reachable(&named, |body| {
direct_op(body, &mut PtrSet::default(), &is_enum)
});
let calls_bearing = |fields: &[(String, Sv)]| {
matches!(obj_get(fields, "name").map(|v| &**v),
Some(SNode::Str(n)) if bearing.get(n).copied().unwrap_or(false))
};
let mut out = Vec::new();
for name in names {
let Some(decl) = named.get(name) else {
continue;
};
let body = decl_body(decl);
let mut refs = Vec::new();
collect_apply_names_sv(&body, &mut refs, &mut PtrSet::default());
if !refs
.iter()
.any(|r| bearing.get(r).copied().unwrap_or(false))
{
continue;
}
let mut memo = PtrMemo::default();
let expanded = expand_refs_where(&body, &named, scope, &mut memo, &calls_bearing)?;
out.push((name.clone(), to_value(&expanded)));
}
Ok(out)
}
fn expand_eager_root(
node: &Sv,
named: &Named,
target_bearing: &std::collections::HashMap<String, bool>,
scope: &str,
) -> Result<Sv, ExpressionTemplateError> {
let mut memo = PtrMemo::default();
expand_eager(node, named, target_bearing, scope, &mut memo)
}
pub(super) fn expand_all(
node: &Sv,
named: &Named,
scope: &str,
memo: &mut PtrMemo<Sv>,
) -> Result<Sv, ExpressionTemplateError> {
match &**node {
SNode::Obj(fields) => {
if let Some(hit) = memo.get(node) {
return Ok(hit.clone());
}
let res = if obj_op(fields) == Some(APPLY_OP) {
let mut newfields = fields.clone();
if let Some(b_idx) = newfields.iter().position(|(k, _)| k == "bindings")
&& let SNode::Obj(b) = &*newfields[b_idx].1.clone()
{
let mut nb = Vec::with_capacity(b.len());
let mut b_changed = false;
for (k, v) in b {
let rv = expand_all(v, named, scope, memo)?;
b_changed |= !Rc::ptr_eq(&rv, v);
nb.push((k.clone(), rv));
}
if b_changed {
newfields[b_idx].1 = Rc::new(SNode::Obj(nb));
}
}
let body = expand_apply(&newfields, named, scope)?;
expand_all(&body, named, scope, memo)?
} else {
let mut changed = false;
let mut out = Vec::with_capacity(fields.len());
for (k, v) in fields {
let rv = expand_all(v, named, scope, memo)?;
changed |= !Rc::ptr_eq(&rv, v);
out.push((k.clone(), rv));
}
if changed {
Rc::new(SNode::Obj(out))
} else {
node.clone()
}
};
memo.insert(node, res.clone());
Ok(res)
}
SNode::Arr(items) => {
if let Some(hit) = memo.get(node) {
return Ok(hit.clone());
}
let mut changed = false;
let mut out = Vec::with_capacity(items.len());
for v in items {
let rv = expand_all(v, named, scope, memo)?;
changed |= !Rc::ptr_eq(&rv, v);
out.push(rv);
}
let res = if changed {
Rc::new(SNode::Arr(out))
} else {
node.clone()
};
memo.insert(node, res.clone());
Ok(res)
}
_ => Ok(node.clone()),
}
}
fn validate_apply_ref(
fields: &[(String, Sv)],
named: &Named,
scope: &str,
) -> Result<(), ExpressionTemplateError> {
let name = match obj_get(fields, "name").map(|v| &**v) {
Some(SNode::Str(s)) => s.as_str(),
_ => {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}: apply_expression_template node missing 'name'"),
));
}
};
let decl = named.get(name).ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE,
format!("{scope}: apply_expression_template references undeclared template '{name}'"),
)
})?;
if decl_has_match(decl) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE,
format!(
"{scope}: apply_expression_template references '{name}', a `match` rewrite rule — \
only match-less templates are invocable by name (esm-spec §9.6.2)"
),
));
}
let bindings: &[(String, Sv)] = match obj_get(fields, "bindings").map(|v| &**v) {
Some(SNode::Obj(b)) => b,
_ => {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!("{scope}: apply_expression_template '{name}' missing 'bindings' object"),
));
}
};
let SNode::Obj(decl_fields) = &**decl else {
return Ok(());
};
let params: Vec<&str> = match obj_get(decl_fields, "params").map(|v| &**v) {
Some(SNode::Arr(items)) => items
.iter()
.filter_map(|v| match &**v {
SNode::Str(s) => Some(s.as_str()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let declared: std::collections::HashSet<&str> = params.iter().copied().collect();
let provided: std::collections::HashSet<&str> =
bindings.iter().map(|(k, _)| k.as_str()).collect();
for p in ¶ms {
if !provided.contains(p) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!(
"{scope}: apply_expression_template '{name}' missing binding for param '{p}'"
),
));
}
}
for (p, _) in bindings {
if !declared.contains(p.as_str()) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH,
format!("{scope}: apply_expression_template '{name}' supplies unknown param '{p}'"),
));
}
}
Ok(())
}
fn check_surviving_refs(
node: &Sv,
named: &Named,
scope: &str,
seen: &mut PtrSet,
) -> Result<(), ExpressionTemplateError> {
match &**node {
SNode::Arr(items) => {
if !seen.insert(node) {
return Ok(());
}
for c in items {
check_surviving_refs(c, named, scope, seen)?;
}
}
SNode::Obj(fields) => {
if !seen.insert(node) {
return Ok(());
}
if obj_op(fields) == Some(APPLY_OP) {
validate_apply_ref(fields, named, scope)?;
}
for (_, v) in fields {
check_surviving_refs(v, named, scope, seen)?;
}
}
_ => {}
}
Ok(())
}
fn rewrite_pass(
node: &Sv,
ctx: &RewriteCtx,
scope: &str,
last: &mut String,
memo: &mut PtrMemo<(Sv, bool, Option<String>)>,
) -> Result<(Sv, bool), ExpressionTemplateError> {
match &**node {
SNode::Arr(items) => {
if let Some((res, ch, l)) = memo.get(node) {
if let Some(l) = l {
*last = l.clone();
}
return Ok((res.clone(), *ch));
}
let mut changed = false;
let mut out = Vec::with_capacity(items.len());
for c in items {
let (nc, ch) = rewrite_pass(c, ctx, scope, last, memo)?;
out.push(nc);
changed |= ch;
}
let res = if changed {
Rc::new(SNode::Arr(out))
} else {
node.clone()
};
memo.insert(node, (res.clone(), changed, changed.then(|| last.clone())));
Ok((res, changed))
}
SNode::Obj(fields) => {
if let Some((res, ch, l)) = memo.get(node) {
if let Some(l) = l {
*last = l.clone();
}
return Ok((res.clone(), *ch));
}
let op = obj_op(fields);
if op == Some(APPLY_OP) {
if ref_is_eager(fields, ctx.target_bearing) {
*last = APPLY_OP.to_string();
let res = expand_eager_root(node, ctx.named, ctx.target_bearing, scope)?;
memo.insert(node, (res.clone(), true, Some(last.clone())));
return Ok((res, true));
}
memo.insert(node, (node.clone(), false, None));
return Ok((node.clone(), false));
}
for rule in ctx.rules {
let mut binds = Binds::new();
if try_match(&rule.pattern, node, &rule.param_set, &mut binds)
&& where_satisfied(&rule.where_c, &binds, ctx.shape_env)
{
*last = format!("{} (rule '{}')", op.unwrap_or(""), rule.name);
let body = substitute(&rule.body, &binds);
let res = expand_eager_root(&body, ctx.named, ctx.target_bearing, scope)?;
memo.insert(node, (res.clone(), true, Some(last.clone())));
return Ok((res, true));
}
}
let mut changed = false;
let mut out = Vec::with_capacity(fields.len());
for (k, v) in fields {
let (nv, ch) = rewrite_pass(v, ctx, scope, last, memo)?;
out.push((k.clone(), nv));
changed |= ch;
}
let res = if changed {
Rc::new(SNode::Obj(out))
} else {
node.clone()
};
memo.insert(node, (res.clone(), changed, changed.then(|| last.clone())));
Ok((res, changed))
}
_ => Ok((node.clone(), false)),
}
}
fn rewrite_to_fixpoint(
node: &Sv,
ctx: &RewriteCtx,
scope: &str,
) -> Result<Sv, ExpressionTemplateError> {
let mut current = expand_eager_root(node, ctx.named, ctx.target_bearing, scope)?;
let mut last = String::new();
for _ in 0..MAX_REWRITE_PASSES {
let mut memo = PtrMemo::default();
let (next, changed) = rewrite_pass(¤t, ctx, scope, &mut last, &mut memo)?;
current = next;
if !changed {
return Ok(current); }
}
Err(err(
codes::REWRITE_RULE_NONTERMINATING,
format!(
"{scope}: expression-template rewriting did not converge within \
MAX_REWRITE_PASSES={MAX_REWRITE_PASSES} passes (last rewritten op '{last}'). \
A `match` rule likely re-introduces its own pattern (esm-spec §9.6.3)."
),
))
}
fn find_apply_paths(view: &Value, hits: &mut Vec<String>) {
crate::json_visit::visit_values(view, &mut |path, v| {
if let Some(obj) = v.as_object()
&& obj.get("op").and_then(|w| w.as_str()) == Some(APPLY_OP)
{
hits.push(path.to_string());
}
});
}
pub fn reject_expression_templates_pre_v04(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 < 4) {
return Ok(());
}
let mut offences: Vec<String> = Vec::new();
for compkind in ["models", "reaction_systems"] {
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_templates")
{
offences.push(format!("/{compkind}/{cname}/expression_templates"));
}
}
}
}
find_apply_paths(view, &mut offences);
if !offences.is_empty() {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_VERSION_TOO_OLD,
format!(
"expression_templates / apply_expression_template require esm >= 0.4.0; \
file declares {esm}. Offending paths: {}",
offences.join(", ")
),
));
}
Ok(())
}
pub(super) struct CompRegistry {
pub(super) named: Named,
rules: Vec<MatchRule>,
shape_env: std::collections::BTreeMap<String, Vec<String>>,
target_bearing: std::collections::HashMap<String, bool>,
}
pub(super) fn build_named(templates: &Map<String, Value>) -> Named {
templates
.iter()
.map(|(n, d)| (n.clone(), to_shared(d)))
.collect()
}
pub fn expand_against_registry(
node: &Value,
templates: &Map<String, Value>,
scope: &str,
) -> Result<Value, ExpressionTemplateError> {
let named = build_named(templates);
let mut memo = PtrMemo::default();
let out = expand_all(&to_shared(node), &named, scope, &mut memo)?;
Ok(to_value(&out))
}
fn declares_templates(value: &Value) -> bool {
let Some(obj) = value.as_object() else {
return false;
};
if obj
.get("expression_templates")
.and_then(|v| v.as_object())
.is_some_and(|t| !t.is_empty())
{
return true;
}
for compkind in ["models", "reaction_systems"] {
if let Some(comps) = obj.get(compkind).and_then(|v| v.as_object()) {
for (_, comp) in comps {
if comp
.get("expression_templates")
.and_then(|v| v.as_object())
.is_some_and(|t| !t.is_empty())
{
return true;
}
}
}
}
false
}
#[derive(Default)]
struct UntemplatedScan {
uses_apply: bool,
geometry: Option<ExpressionTemplateError>,
makearray: Option<ExpressionTemplateError>,
}
fn scan_without_templates(
tree: &Value,
at: &JsonPath<'_>,
in_templates: bool,
out: &mut UntemplatedScan,
visit: &mut dyn FnMut(&JsonPath<'_>, &Value),
) {
visit(at, tree);
match tree {
Value::Array(arr) => {
for (i, child) in arr.iter().enumerate() {
scan_without_templates(child, &JsonPath::Index(at, i), in_templates, out, visit);
if out.uses_apply {
return;
}
}
}
Value::Object(obj) => {
let op = crate::json_visit::small_get(obj, "op").and_then(|v| v.as_str());
if op == Some(APPLY_OP) {
out.uses_apply = true;
return;
}
if !in_templates && let Some(op) = op {
if out.geometry.is_none() && GEOMETRY_MANIFOLD_OPS.contains(&op) {
out.geometry = check_geometry_manifold(obj, at).err();
}
if out.makearray.is_none() && op == "makearray" {
out.makearray = check_makearray_regions(obj, at).err();
}
}
for (k, v) in obj {
let skip = in_templates || k == "expression_templates";
scan_without_templates(v, &JsonPath::Key(at, k), skip, out, visit);
if out.uses_apply {
return;
}
}
}
_ => {}
}
}
pub fn lower_expression_templates(value: &mut Value) -> Result<(), ExpressionTemplateError> {
lower_expression_templates_found(value).map(|_| ())
}
pub(crate) fn lower_expression_templates_found(
value: &mut Value,
) -> Result<bool, ExpressionTemplateError> {
lower_expression_templates_visiting(value, &mut |_, _| {}).map(|(found, _)| found)
}
pub(crate) fn lower_expression_templates_visiting(
value: &mut Value,
visit: &mut dyn FnMut(&JsonPath<'_>, &Value),
) -> Result<(bool, bool), ExpressionTemplateError> {
reject_expression_templates_pre_v04(value)?;
if value.as_object().is_none() {
return Ok((false, false));
}
if !declares_templates(value) {
let mut scan = UntemplatedScan::default();
scan_without_templates(value, &JsonPath::Root(""), false, &mut scan, visit);
if !scan.uses_apply {
if let Some(e) = scan.geometry.or(scan.makearray) {
return Err(e);
}
return Ok((false, true));
}
}
let root = value.as_object_mut().expect("checked object above");
let iset_names: std::collections::HashSet<String> = root
.get("index_sets")
.and_then(|v| v.as_object())
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let mut registries: std::collections::HashMap<String, CompRegistry> =
std::collections::HashMap::new();
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 scope_base = format!("{compkind}.{cname}");
let shape_env = component_shape_env(comp);
let templates: Map<String, Value> = comp
.get("expression_templates")
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default();
validate_templates(&templates, &scope_base)?;
validate_template_body_references(&templates, &scope_base)?;
let named = build_named(&templates);
let rules = collect_match_rules(&templates, &named, &iset_names, &scope_base)?;
let target_bearing = template_target_bearing(&named);
let ctx = RewriteCtx {
named: &named,
rules: &rules,
shape_env: &shape_env,
target_bearing: &target_bearing,
};
let keys: Vec<String> = comp.keys().cloned().collect();
for k in keys {
if k == "expression_templates" {
continue;
}
let scope = format!("{scope_base}.{k}");
let Some(child) = comp.get(&k) else { continue };
let shared = to_shared(child);
let rewritten = rewrite_to_fixpoint(&shared, &ctx, &scope)?;
check_surviving_refs(&rewritten, &named, &scope, &mut PtrSet::default())?;
if !Rc::ptr_eq(&rewritten, &shared) {
comp.insert(k, to_value(&rewritten));
}
}
registries.entry(cname.clone()).or_insert(CompRegistry {
named,
rules,
shape_env,
target_bearing,
});
}
}
if let Some(Value::Array(entries)) = root.get_mut("coupling") {
for (idx, entry) in entries.iter_mut().enumerate() {
let Some(obj) = entry.as_object_mut() else {
continue;
};
if obj.get("type").and_then(|v| v.as_str()) != Some("variable_map") {
continue;
}
let Some(transform) = obj.get("transform").filter(|t| t.is_object()).cloned() else {
continue;
};
let Some(comp_name) = obj
.get("to")
.and_then(|v| v.as_str())
.map(|t| t.split('.').next().unwrap_or(""))
else {
continue;
};
let Some(reg) = registries.get(comp_name) else {
continue;
};
let ctx = RewriteCtx {
named: ®.named,
rules: ®.rules,
shape_env: ®.shape_env,
target_bearing: ®.target_bearing,
};
let scope = format!("coupling[{idx}].transform");
let shared = to_shared(&transform);
let rewritten = rewrite_to_fixpoint(&shared, &ctx, &scope)?;
check_surviving_refs(&rewritten, ®.named, &scope, &mut PtrSet::default())?;
if !Rc::ptr_eq(&rewritten, &shared) {
obj.insert("transform".to_string(), to_value(&rewritten));
}
}
}
validate_geometry_manifolds_refaware(value, ®istries)?;
validate_makearray_regions(value, "")?;
validate_makearray_regions_in_registries(®istries)?;
Ok((true, false))
}
pub(super) const GEOMETRY_MANIFOLD_OPS: [&str; 2] =
["intersect_polygon", "polygon_intersection_area"];
const GEOMETRY_MANIFOLD_VALUES: [&str; 3] = ["planar", "spherical", "geodesic"];
pub fn validate_geometry_manifolds(
tree: &Value,
path: &str,
) -> Result<(), ExpressionTemplateError> {
validate_geometry_manifolds_at(tree, &JsonPath::Root(path))
}
fn validate_geometry_manifolds_at(
tree: &Value,
at: &JsonPath<'_>,
) -> Result<(), ExpressionTemplateError> {
match tree {
Value::Array(arr) => {
for (i, child) in arr.iter().enumerate() {
validate_geometry_manifolds_at(child, &JsonPath::Index(at, i))?;
}
Ok(())
}
Value::Object(obj) => {
check_geometry_manifold(obj, at)?;
for (k, v) in obj {
if k == "expression_templates" {
continue;
}
validate_geometry_manifolds_at(v, &JsonPath::Key(at, k))?;
}
Ok(())
}
_ => Ok(()),
}
}
fn check_geometry_manifold(
obj: &Map<String, Value>,
at: &JsonPath<'_>,
) -> Result<(), ExpressionTemplateError> {
if let Some(op) = obj.get("op").and_then(|v| v.as_str())
&& GEOMETRY_MANIFOLD_OPS.contains(&op)
&& let Some(m) = obj.get("manifold")
{
let ok = m
.as_str()
.is_some_and(|s| GEOMETRY_MANIFOLD_VALUES.contains(&s));
if !ok {
return Err(err(
codes::GEOMETRY_MANIFOLD_INVALID,
format!(
"{at}: `{op}` carries manifold {m}, not a member of the \
closed set {{planar, spherical, geodesic}}. The manifold \
enum is enforced on the expanded form (esm-spec §9.6.4; \
CONFORMANCE_SPEC §5.8.4) — a template parameter substituted \
into this scalar field must be bound to one of the \
closed-set literals."
),
));
}
}
Ok(())
}
pub fn validate_makearray_regions(tree: &Value, path: &str) -> Result<(), ExpressionTemplateError> {
validate_makearray_regions_at(tree, &JsonPath::Root(path))
}
fn validate_makearray_regions_at(
tree: &Value,
at: &JsonPath<'_>,
) -> Result<(), ExpressionTemplateError> {
match tree {
Value::Array(arr) => {
for (i, child) in arr.iter().enumerate() {
validate_makearray_regions_at(child, &JsonPath::Index(at, i))?;
}
Ok(())
}
Value::Object(obj) => {
check_makearray_regions(obj, at)?;
for (k, v) in obj {
if k == "expression_templates" {
continue;
}
validate_makearray_regions_at(v, &JsonPath::Key(at, k))?;
}
Ok(())
}
_ => Ok(()),
}
}
fn check_makearray_regions(
obj: &Map<String, Value>,
at: &JsonPath<'_>,
) -> Result<(), ExpressionTemplateError> {
if obj.get("op").and_then(|v| v.as_str()) == Some("makearray")
&& let Some(regions) = obj.get("regions").and_then(|v| v.as_array())
{
for (ri, region) in regions.iter().enumerate() {
let Some(region_arr) = region.as_array() else {
continue;
};
for (di, bounds) in region_arr.iter().enumerate() {
let Some(bounds_arr) = bounds.as_array() else {
continue;
};
if bounds_arr.len() != 2 {
continue;
}
let (Some(lo), Some(hi)) = (bounds_arr[0].as_i64(), bounds_arr[1].as_i64()) else {
continue;
};
if hi < lo - 1 {
return Err(err(
codes::MAKEARRAY_REGION_INVERTED,
format!(
"{at}: makearray regions[{ri}] dimension {di} bound pair \
[{lo}, {hi}] is inverted (stop < start - 1). An empty bound \
is spelled [start, start-1] and contributes no elements \
(esm-spec §4.3.2); a further-inverted pair is an authoring \
error — e.g. an interior stencil region [2, N-1] instantiated \
at N below the scheme's minimum extent (§9.6.8)."
),
));
}
}
}
}
Ok(())
}