use super::*;
#[derive(Debug)]
pub(super) enum SNode {
Null,
Bool(bool),
Num(serde_json::Number),
Str(String),
Arr(Vec<Sv>),
Obj(Vec<(String, Sv)>),
}
pub(super) type Sv = Rc<SNode>;
pub(super) fn to_shared(v: &Value) -> Sv {
Rc::new(match v {
Value::Null => SNode::Null,
Value::Bool(b) => SNode::Bool(*b),
Value::Number(n) => SNode::Num(n.clone()),
Value::String(s) => SNode::Str(s.clone()),
Value::Array(arr) => SNode::Arr(arr.iter().map(to_shared).collect()),
Value::Object(obj) => {
SNode::Obj(obj.iter().map(|(k, v)| (k.clone(), to_shared(v))).collect())
}
})
}
pub(super) fn to_value(s: &SNode) -> Value {
match s {
SNode::Null => Value::Null,
SNode::Bool(b) => Value::Bool(*b),
SNode::Num(n) => Value::Number(n.clone()),
SNode::Str(st) => Value::String(st.clone()),
SNode::Arr(items) => Value::Array(items.iter().map(|c| to_value(c)).collect()),
SNode::Obj(fields) => {
let mut out = Map::new();
for (k, v) in fields {
out.insert(k.clone(), to_value(v));
}
Value::Object(out)
}
}
}
pub(super) fn obj_get<'a>(fields: &'a [(String, Sv)], key: &str) -> Option<&'a Sv> {
fields.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
pub(super) fn obj_op(fields: &[(String, Sv)]) -> Option<&str> {
match obj_get(fields, "op").map(|v| &**v) {
Some(SNode::Str(s)) => Some(s.as_str()),
_ => None,
}
}
pub(super) fn sv_eq(a: &Sv, b: &Sv) -> bool {
if Rc::ptr_eq(a, b) {
return true;
}
match (&**a, &**b) {
(SNode::Null, SNode::Null) => true,
(SNode::Bool(x), SNode::Bool(y)) => x == y,
(SNode::Num(x), SNode::Num(y)) => x == y,
(SNode::Str(x), SNode::Str(y)) => x == y,
(SNode::Arr(x), SNode::Arr(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|(cx, cy)| sv_eq(cx, cy))
}
(SNode::Obj(x), SNode::Obj(y)) => {
x.len() == y.len()
&& x.iter()
.all(|(k, vx)| obj_get(y, k).is_some_and(|vy| sv_eq(vx, vy)))
}
_ => false,
}
}
pub(super) fn value_eq_sv(p: &Value, t: &SNode) -> bool {
match (p, t) {
(Value::Null, SNode::Null) => true,
(Value::Bool(x), SNode::Bool(y)) => x == y,
(Value::Number(x), SNode::Num(y)) => x == y,
(Value::String(x), SNode::Str(y)) => x == y,
(Value::Array(x), SNode::Arr(y)) => {
x.len() == y.len() && x.iter().zip(y.iter()).all(|(px, ty)| value_eq_sv(px, ty))
}
(Value::Object(x), SNode::Obj(y)) => {
x.len() == y.len()
&& x.iter()
.all(|(k, pv)| obj_get(y, k).is_some_and(|tv| value_eq_sv(pv, tv)))
}
_ => false,
}
}
pub(super) type Binds = Vec<(String, Sv)>;
pub(super) fn binds_get<'a>(binds: &'a Binds, key: &str) -> Option<&'a Sv> {
binds.iter().find(|(k, _)| k == key).map(|(_, v)| v)
}
pub(super) struct PtrMemo<T> {
map: std::collections::HashMap<*const SNode, (Sv, T)>,
}
impl<T> Default for PtrMemo<T> {
fn default() -> Self {
Self {
map: std::collections::HashMap::new(),
}
}
}
impl<T> PtrMemo<T> {
pub(super) fn get(&self, node: &Sv) -> Option<&T> {
self.map.get(&Rc::as_ptr(node)).map(|(_, v)| v)
}
pub(super) fn insert(&mut self, node: &Sv, value: T) {
self.map.insert(Rc::as_ptr(node), (node.clone(), value));
}
}
fn assert_no_nested_apply(
body: &Value,
template_name: &str,
path: &str,
) -> Result<(), ExpressionTemplateError> {
match body {
Value::Array(arr) => {
for (i, child) in arr.iter().enumerate() {
assert_no_nested_apply(child, template_name, &format!("{path}/{i}"))?;
}
}
Value::Object(obj) => {
if obj.get("op").and_then(|v| v.as_str()) == Some(APPLY_OP) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"expression_templates.{template_name}: `match` contains an \
'apply_expression_template' node at {path}; match patterns MUST NOT \
reference templates (esm-spec §9.7.3)"
),
));
}
for (k, v) in obj {
assert_no_nested_apply(v, template_name, &format!("{path}/{k}"))?;
}
}
_ => {}
}
Ok(())
}
pub(crate) fn validate_templates(
templates: &Map<String, Value>,
scope: &str,
) -> Result<(), ExpressionTemplateError> {
for (name, decl) in templates {
let decl_obj = decl.as_object().ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: entry must be an object \
with params + body"
),
)
})?;
let params = decl_obj
.get("params")
.and_then(|p| p.as_array())
.ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}.expression_templates.{name}: 'params' must be an array"),
)
})?;
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for p in params {
let p_str = p.as_str().ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}.expression_templates.{name}: param names must be strings"),
)
})?;
if p_str.is_empty() {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}.expression_templates.{name}: param names must be non-empty"),
));
}
if !seen.insert(p_str) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}.expression_templates.{name}: param '{p_str}' declared twice"),
));
}
}
let _body = decl_obj.get("body").ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!("{scope}.expression_templates.{name}: 'body' is required"),
)
})?;
if let Some(pattern) = decl_obj.get("match") {
assert_no_nested_apply(pattern, name, "/match")?;
}
if let Some(whr) = decl_obj.get("where").filter(|v| !v.is_null()) {
if decl_obj.get("match").is_none() {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: 'where' is only admissible \
alongside 'match' — constraints scope an auto-applied rewrite rule, not \
a named fragment (esm-spec §9.6.1)"
),
));
}
let whr_obj = whr.as_object().filter(|o| !o.is_empty()).ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: 'where' must be a non-empty object \
mapping declared params to constraint objects"
),
)
})?;
for (p, cobj) in whr_obj {
if !seen.contains(p.as_str()) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: 'where' constrains '{p}', which \
is not a declared param (esm-spec §9.6.1)"
),
));
}
let cobj_obj = cobj.as_object().ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: where.{p} must be a constraint \
object (v1 admits exactly the 'shape' kind)"
),
)
})?;
let is_only_shape = cobj_obj.len() == 1 && cobj_obj.contains_key("shape");
if !is_only_shape {
let mut kinds: Vec<&str> = cobj_obj.keys().map(String::as_str).collect();
kinds.sort_unstable();
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: where.{p} carries constraint \
kind(s) {}; the v1 constraint vocabulary is exactly {{shape}} \
(esm-spec §9.6.1)",
kinds.join(", ")
),
));
}
let shp = cobj_obj
.get("shape")
.and_then(|v| v.as_array())
.filter(|a| !a.is_empty())
.ok_or_else(|| {
err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: where.{p}.shape must be a \
non-empty array of index-set names"
),
)
})?;
for s in shp {
if s.as_str().is_none_or(|s| s.is_empty()) {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION,
format!(
"{scope}.expression_templates.{name}: where.{p}.shape entries \
must be non-empty strings"
),
));
}
}
}
}
}
Ok(())
}
pub const MAX_TEMPLATE_EXPANSION_DEPTH: usize = 32;
pub(crate) fn collect_apply_names(x: &Value, out: &mut Vec<String>) {
crate::json_visit::visit_values(x, &mut |_path, v| {
if let Some(obj) = v.as_object()
&& obj.get("op").and_then(|w| w.as_str()) == Some(APPLY_OP)
&& let Some(name) = obj.get("name").and_then(|w| w.as_str())
{
out.push(name.to_string());
}
});
}
pub(crate) fn validate_template_body_references(
templates: &Map<String, Value>,
scope: &str,
) -> Result<(), ExpressionTemplateError> {
if templates.is_empty() {
return Ok(());
}
let mut refs: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
let mut any_refs = false;
for (name, decl) in templates.iter() {
let mut names = Vec::new();
if let Some(body) = decl.get("body") {
collect_apply_names(body, &mut names);
}
any_refs = any_refs || !names.is_empty();
refs.insert(name.clone(), names);
}
if !any_refs {
return Ok(());
}
for (name, rs) in &refs {
for r in rs {
let Some(tdecl) = templates.get(r) else {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE,
format!(
"{scope}.expression_templates.{name}: body references undeclared \
template '{r}' (esm-spec §9.7.3)"
),
));
};
if tdecl.get("match").is_some() {
return Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE,
format!(
"{scope}.expression_templates.{name}: body references '{r}', a `match` \
rewrite rule — only match-less templates are invocable by name \
(esm-spec §9.7.3)"
),
));
}
}
}
fn visit(
name: &str,
refs: &std::collections::BTreeMap<String, Vec<String>>,
state: &mut std::collections::HashMap<String, u8>, depth: &mut std::collections::HashMap<String, usize>,
chain: &mut Vec<String>,
scope: &str,
) -> Result<usize, ExpressionTemplateError> {
match state.get(name).copied().unwrap_or(0) {
1 => {
let start = chain.iter().position(|c| c == name).unwrap_or(0);
let mut cyc: Vec<String> = chain[start..].to_vec();
cyc.push(name.to_string());
Err(err(
codes::APPLY_EXPRESSION_TEMPLATE_RECURSIVE_BODY,
format!(
"{scope}.expression_templates: template-body reference cycle {} \
(esm-spec §9.7.3)",
cyc.join(" -> ")
),
))
}
2 => Ok(depth[name]),
_ => {
state.insert(name.to_string(), 1);
chain.push(name.to_string());
let mut d = 1usize;
if let Some(rs) = refs.get(name) {
for r in rs.clone() {
d = d.max(1 + visit(&r, refs, state, depth, chain, scope)?);
}
}
chain.pop();
state.insert(name.to_string(), 2);
depth.insert(name.to_string(), d);
if d > MAX_TEMPLATE_EXPANSION_DEPTH {
return Err(err(
codes::TEMPLATE_BODY_EXPANSION_TOO_DEEP,
format!(
"{scope}.expression_templates.{name}: body-reference chain of {d} \
templates exceeds \
MAX_TEMPLATE_EXPANSION_DEPTH={MAX_TEMPLATE_EXPANSION_DEPTH} \
(esm-spec §9.7.3)"
),
));
}
Ok(d)
}
}
}
let mut state = std::collections::HashMap::new();
let mut depth = std::collections::HashMap::new();
let mut chain: Vec<String> = Vec::new();
for name in refs.keys() {
visit(name, &refs, &mut state, &mut depth, &mut chain, scope)?;
}
Ok(())
}
pub(super) fn substitute(body: &Sv, bindings: &Binds) -> Sv {
if bindings.is_empty() {
return body.clone();
}
let mut memo: PtrMemo<Sv> = PtrMemo::default();
subst_shared(body, bindings, &mut memo)
}
fn subst_shared(node: &Sv, bindings: &Binds, memo: &mut PtrMemo<Sv>) -> Sv {
match &**node {
SNode::Str(s) => match binds_get(bindings, s) {
Some(v) => v.clone(),
None => node.clone(),
},
SNode::Arr(items) => {
if let Some(hit) = memo.get(node) {
return hit.clone();
}
let mut changed = false;
let mut out = Vec::with_capacity(items.len());
for c in items {
let nc = subst_shared(c, bindings, memo);
changed |= !Rc::ptr_eq(&nc, c);
out.push(nc);
}
let res = if changed {
Rc::new(SNode::Arr(out))
} else {
node.clone()
};
memo.insert(node, res.clone());
res
}
SNode::Obj(fields) => {
if let Some(hit) = memo.get(node) {
return hit.clone();
}
let is_apply = obj_op(fields) == Some(APPLY_OP);
let mut changed = false;
let mut out = Vec::with_capacity(fields.len());
for (k, v) in fields {
if is_apply && k == "name" {
out.push((k.clone(), v.clone()));
continue;
}
let nv = subst_shared(v, bindings, memo);
changed |= !Rc::ptr_eq(&nv, v);
out.push((k.clone(), nv));
}
let res = if changed {
Rc::new(SNode::Obj(out))
} else {
node.clone()
};
memo.insert(node, res.clone());
res
}
_ => node.clone(),
}
}