use serde_json::{Map, Value};
use std::rc::Rc;
const APPLY_OP: &str = "apply_expression_template";
pub type ExpressionTemplateError = crate::diagnostic::DiagnosticError;
use crate::diagnostic::{codes, err};
mod emit;
mod expand;
mod mirror;
mod refaware;
mod rewrite;
pub use emit::*;
pub use expand::*;
pub use mirror::*;
use refaware::*;
pub use rewrite::*;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_untemplated_fast_path_reports_as_the_validators_do() {
let geo = json!({"op": "polygon_intersection_area", "manifold": "flat", "args": []});
let inv = json!({"op": "makearray", "regions": [[[3, 0]]], "values": [0]});
let docs = [
json!({"esm": "1.1.0", "models": {"M": {"equations": [{"lhs": "x", "rhs": inv.clone()}, {"lhs": "y", "rhs": geo.clone()}]}}}),
json!({"esm": "1.1.0", "models": {"M": {"equations": [{"lhs": "x", "rhs": inv.clone()}]}}}),
json!({"esm": "1.1.0", "models": {"M": {"expression_templates": {}, "equations": [{"lhs": "y", "rhs": geo.clone()}]}}}),
json!({"esm": "1.1.0", "models": {"M": {"expression_templates": {}, "x": {"expression_templates": [geo.clone(), inv.clone()]}}}}),
json!({"esm": "1.1.0", "models": {"M": {"equations": [{"lhs": "x", "rhs": {"op": "+", "args": [1, 2]}}]}}}),
];
for doc in docs {
let want = validate_geometry_manifolds(&doc, "")
.and_then(|()| validate_makearray_regions(&doc, ""))
.map_err(|e| e.to_string());
let mut got_doc = doc.clone();
let got = lower_expression_templates_found(&mut got_doc).map_err(|e| e.to_string());
match (&want, &got) {
(Ok(()), Ok(found)) => assert!(!found, "{doc}"),
(Err(w), Err(g)) => assert_eq!(w, g),
_ => panic!("{doc}: validators {want:?}, fast path {got:?}"),
}
assert_eq!(got_doc, doc);
}
}
fn arrhenius_fixture() -> Value {
json!({
"esm": "1.0.0",
"metadata": {"name": "expr_template_smoke", "authors": ["esm-giy"]},
"reaction_systems": {
"chem": {
"species": {"A": {"default": 1.0}, "B": {"default": 0.5}},
"parameters": {"T": {"default": 298.15}, "num_density": {"default": 2.5e19}},
"expression_templates": {
"arrhenius": {
"params": ["A_pre", "Ea"],
"body": {
"op": "*",
"args": [
"A_pre",
{"op": "exp", "args": [
{"op": "/", "args": [{"op": "-", "args": ["Ea"]}, "T"]}
]},
"num_density"
]
}
}
},
"reactions": [
{"id": "R1",
"substrates": [{"species": "A", "stoichiometry": 1}],
"products": [{"species": "B", "stoichiometry": 1}],
"rate": {"op": "apply_expression_template", "args": [],
"name": "arrhenius",
"bindings": {"A_pre": 1.8e-12, "Ea": 1500}}}
]
}
}
})
}
#[test]
fn expansion_strips_templates_block_and_replaces_apply_node() {
let mut v = arrhenius_fixture();
lower_expression_templates(&mut v).expect("expansion");
expand(&mut v).expect("expand");
let chem = &v["reaction_systems"]["chem"];
assert!(chem.get("expression_templates").is_none());
let rate = &chem["reactions"][0]["rate"];
assert_eq!(rate["op"], json!("*"));
assert_eq!(rate["args"][0], json!(1.8e-12));
}
#[test]
fn rejects_unknown_template_name() {
let mut v = arrhenius_fixture();
v["reaction_systems"]["chem"]["reactions"][0]["rate"]["name"] = json!("missing");
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "apply_expression_template_unknown_template");
}
#[test]
fn rejects_missing_binding() {
let mut v = arrhenius_fixture();
v["reaction_systems"]["chem"]["reactions"][0]["rate"]["bindings"]
.as_object_mut()
.unwrap()
.remove("Ea");
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "apply_expression_template_bindings_mismatch");
}
#[test]
fn rejects_extra_binding() {
let mut v = arrhenius_fixture();
v["reaction_systems"]["chem"]["reactions"][0]["rate"]["bindings"]["bogus"] = json!(99);
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "apply_expression_template_bindings_mismatch");
}
#[test]
fn rejects_recursive_body() {
let mut v = arrhenius_fixture();
v["reaction_systems"]["chem"]["expression_templates"]["arrhenius"]["body"] = json!({
"op": "apply_expression_template",
"args": [],
"name": "arrhenius",
"bindings": {"A_pre": 1, "Ea": 1}
});
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "apply_expression_template_recursive_body");
}
#[test]
fn deep_double_reference_chain_expands_correctly() {
const DEPTH: usize = 12;
let apply = |name: &str| -> Value {
json!({"op": APPLY_OP, "args": [], "name": name, "bindings": {}})
};
let mut templates = Map::new();
templates.insert(
"T0".to_string(),
json!({"params": [], "body": {"op": "*", "args": [
1.8e-12,
{"op": "exp", "args": [
{"op": "/", "args": [{"op": "-", "args": [1500.0]}, "T"]}
]}
]}}),
);
for i in 1..=DEPTH {
let prev = format!("T{}", i - 1);
templates.insert(
format!("T{i}"),
json!({"params": [], "body": {"op": "+", "args": [apply(&prev), apply(&prev)]}}),
);
}
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "deep_chain", "authors": ["t"]},
"reaction_systems": {"chem": {
"species": {"A": {"default": 1.0}, "B": {"default": 0.5}},
"parameters": {"T": {"default": 298.15}},
"expression_templates": Value::Object(templates),
"reactions": [{
"id": "R1",
"substrates": [{"species": "A", "stoichiometry": 1}],
"products": [{"species": "B", "stoichiometry": 1}],
"rate": apply(&format!("T{DEPTH}"))
}]
}}
});
lower_expression_templates(&mut v).expect("expansion");
expand(&mut v).expect("expand");
let chem = &v["reaction_systems"]["chem"];
assert!(chem.get("expression_templates").is_none());
let rate = &chem["reactions"][0]["rate"];
assert_eq!(rate["op"], json!("+"));
let mut leaf = rate;
while leaf["op"] == json!("+") {
leaf = &leaf["args"][0];
}
assert_eq!(leaf["op"], json!("*"));
assert_eq!(leaf["args"][0], json!(1.8e-12));
fn count(v: &Value) -> usize {
match v {
Value::Array(a) => 1 + a.iter().map(count).sum::<usize>(),
Value::Object(o) => 1 + o.values().map(count).sum::<usize>(),
_ => 1,
}
}
assert_eq!(count(rate), (1usize << DEPTH) * 18 - 3);
}
#[test]
fn rejects_pre_v04_files_using_templates() {
let mut v = arrhenius_fixture();
v["esm"] = json!("0.3.5");
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "apply_expression_template_version_too_old");
}
#[test]
fn ast_valued_bindings_substitute_into_body() {
let mut v = arrhenius_fixture();
v["reaction_systems"]["chem"]["reactions"][0]["rate"]["bindings"]["Ea"] = json!({
"op": "*", "args": [3, "T"]
});
lower_expression_templates(&mut v).expect("expansion");
expand(&mut v).expect("expand");
let rate = &v["reaction_systems"]["chem"]["reactions"][0]["rate"];
let exp_node = &rate["args"][1];
assert_eq!(exp_node["op"], json!("exp"));
let div_node = &exp_node["args"][0];
assert_eq!(div_node["op"], json!("/"));
let neg_node = &div_node["args"][0];
assert_eq!(neg_node["op"], json!("-"));
let inner = &neg_node["args"][0];
assert_eq!(inner["op"], json!("*"));
}
#[test]
fn conformance_fixture_matches_expanded_form() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.expect("repo_root from CARGO_MANIFEST_DIR")
.to_path_buf();
let fixture_path =
repo_root.join("tests/conformance/expression_templates/arrhenius_smoke/fixture.esm");
let expanded_path =
repo_root.join("tests/conformance/expression_templates/arrhenius_smoke/expanded.esm");
let src = std::fs::read_to_string(&fixture_path).expect("read fixture.esm");
let mut got: Value = serde_json::from_str(&src).expect("parse fixture");
lower_expression_templates(&mut got).expect("expansion");
expand(&mut got).expect("expand");
let expanded_src = std::fs::read_to_string(&expanded_path).expect("read expanded.esm");
let want: Value = serde_json::from_str(&expanded_src).expect("parse expanded");
let got_reactions = &got["reaction_systems"]["chem"]["reactions"];
let want_reactions = &want["reaction_systems"]["chem"]["reactions"];
assert_eq!(got_reactions, want_reactions);
}
#[test]
fn coupling_transform_expression_conformance_fixture_matches_expanded_form() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let repo_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.expect("repo_root from CARGO_MANIFEST_DIR")
.to_path_buf();
let case =
repo_root.join("tests/conformance/expression_templates/coupling_transform_expression");
let src = std::fs::read_to_string(case.join("fixture.esm")).expect("read fixture.esm");
let mut got: Value = serde_json::from_str(&src).expect("parse fixture");
lower_expression_templates(&mut got).expect("expansion");
expand(&mut got).expect("expand");
let expanded_src =
std::fs::read_to_string(case.join("expanded.esm")).expect("read expanded.esm");
let want: Value = serde_json::from_str(&expanded_src).expect("parse expanded");
assert_eq!(&got["coupling"], &want["coupling"]);
assert_eq!(&got["models"], &want["models"]);
}
#[test]
fn no_templates_block_is_a_noop() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "no_templates", "authors": ["t"]},
"reaction_systems": {
"chem": {
"species": {"A": {}},
"parameters": {"k": {"default": 1.0}},
"reactions": [{
"id": "R1",
"substrates": [{"species": "A", "stoichiometry": 1}],
"products": null,
"rate": "k"
}]
}
}
});
lower_expression_templates(&mut v).expect("expansion");
assert_eq!(
v["reaction_systems"]["chem"]["reactions"][0]["rate"],
json!("k")
);
}
#[test]
fn match_rule_lowers_grad_operator() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "grad_lowering", "authors": ["t"]},
"models": {
"Diff": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"central_grad_x": {
"params": ["f"],
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {
"op": "-",
"args": [
{"op": "index", "args": ["f", {"op": "+", "args": ["i", 1]}]},
{"op": "index", "args": ["f", {"op": "-", "args": ["i", 1]}]}
]
}
}
},
"equations": [
{"lhs": {"op": "D", "args": ["u"], "wrt": "t"},
"rhs": {"op": "grad", "args": ["u"], "dim": "x"}}
]
}
}
});
lower_expression_templates(&mut v).expect("rewrite");
expand(&mut v).expect("expand");
let model = &v["models"]["Diff"];
assert!(model.get("expression_templates").is_none());
let rhs = &model["equations"][0]["rhs"];
assert_eq!(rhs["op"], json!("-"));
assert_eq!(rhs["args"][0]["op"], json!("index"));
assert_eq!(rhs["args"][0]["args"][0], json!("u"));
assert_eq!(rhs["args"][1]["args"][0], json!("u"));
assert_eq!(model["equations"][0]["lhs"]["op"], json!("D"));
}
#[test]
fn match_rule_binds_scalar_field_metavariable() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "scalar_meta", "authors": ["t"]},
"models": {
"M": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"grad_to_deriv": {
"params": ["f", "d"],
"match": {"op": "grad", "args": ["f"], "dim": "d"},
"body": {"op": "deriv", "args": ["f"], "wrt": "d"}
}
},
"equations": [
{"lhs": "u", "rhs": {"op": "grad", "args": ["u"], "dim": "y"}}
]
}
}
});
lower_expression_templates(&mut v).expect("rewrite");
let rhs = &v["models"]["M"]["equations"][0]["rhs"];
assert_eq!(rhs["op"], json!("deriv"));
assert_eq!(rhs["args"][0], json!("u")); assert_eq!(rhs["wrt"], json!("y")); }
#[test]
fn rejects_nonterminating_match_rule() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "nonterm", "authors": ["t"]},
"models": {
"M": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"loop_rule": {
"params": ["f"],
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {"op": "+", "args": [
{"op": "grad", "args": ["f"], "dim": "x"}, 1]}
}
},
"equations": [
{"lhs": "u", "rhs": {"op": "grad", "args": ["u"], "dim": "x"}}
]
}
}
});
let e = lower_expression_templates(&mut v).expect_err("should fail");
assert_eq!(e.code, "rewrite_rule_nonterminating");
}
#[test]
fn match_rules_apply_in_declaration_order() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "order", "authors": ["t"]},
"models": {
"M": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"z_rule": {
"params": ["f"],
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {"op": "winner", "args": ["f"]}
},
"a_rule": {
"params": ["f"],
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {"op": "loser", "args": ["f"]}
}
},
"equations": [
{"lhs": "u", "rhs": {"op": "grad", "args": ["u"], "dim": "x"}}
]
}
}
});
lower_expression_templates(&mut v).expect("rewrite");
let rhs = &v["models"]["M"]["equations"][0]["rhs"];
assert_eq!(rhs["op"], json!("winner"));
}
#[test]
fn higher_priority_rule_wins_over_earlier_declared() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "prio", "authors": ["t"]},
"models": {
"M": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"low": {
"params": ["f"],
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {"op": "loser", "args": ["f"]}
},
"high": {
"params": ["f"],
"priority": 100,
"match": {"op": "grad", "args": ["f"], "dim": "x"},
"body": {"op": "winner", "args": ["f"]}
}
},
"equations": [
{"lhs": "u", "rhs": {"op": "grad", "args": ["u"], "dim": "x"}}
]
}
}
});
lower_expression_templates(&mut v).expect("rewrite");
assert_eq!(
v["models"]["M"]["equations"][0]["rhs"]["op"],
json!("winner")
);
}
#[test]
fn produced_body_is_rescanned_in_a_later_pass() {
let mut v = json!({
"esm": "1.0.0",
"metadata": {"name": "fixpoint", "authors": ["t"]},
"models": {
"M": {
"variables": {"u": {"type": "unknown"}},
"expression_templates": {
"sugar": {
"params": ["f"],
"match": {"op": "sugar", "args": ["f"]},
"body": {"op": "inner", "args": ["f"]}
},
"inner_to_leaf": {
"params": ["f"],
"match": {"op": "inner", "args": ["f"]},
"body": {"op": "*", "args": ["k", "f"]}
}
},
"equations": [
{"lhs": "u", "rhs": {"op": "sugar", "args": ["u"]}}
]
}
}
});
lower_expression_templates(&mut v).expect("rewrite");
let rhs = &v["models"]["M"]["equations"][0]["rhs"];
assert_eq!(*rhs, json!({"op": "*", "args": ["k", "u"]}));
}
fn scalar_field_doc(templates: Value, bindings: Value, name: &str) -> Value {
json!({
"esm": "1.0.0",
"metadata": {"name": "scalar_field_param_unit", "authors": ["t"]},
"models": {"M": {
"variables": {
"pa": {"type": "parameter"},
"pb": {"type": "parameter"},
"area": {"type": "unknown"}
},
"equations": [
{"lhs": "area", "rhs": {"op": "apply_expression_template", "args": [],
"name": name, "bindings": bindings}}],
"expression_templates": templates
}}
})
}
#[test]
fn scalar_field_substitution_happy_path() {
let mut v = scalar_field_doc(
json!({"overlap_area": {
"params": ["K_manifold", "a", "b"],
"body": {"op": "polygon_intersection_area",
"manifold": "K_manifold", "args": ["a", "b"]}}}),
json!({"K_manifold": "planar", "a": "pa", "b": "pb"}),
"overlap_area",
);
lower_expression_templates(&mut v).expect("rewrite");
expand(&mut v).expect("expand");
assert_eq!(
*crate::classification::observed_definition_json(&v["models"]["M"], "area")
.expect("area defining equation"),
json!({"op": "polygon_intersection_area", "manifold": "planar",
"args": ["pa", "pb"]})
);
}
#[test]
fn scalar_field_param_threads_through_body_composition() {
let mut v = scalar_field_doc(
json!({
"inner": {
"params": ["m", "x", "y"],
"body": {"op": "polygon_intersection_area", "manifold": "m",
"args": ["x", "y"]}},
"outer": {
"params": ["K", "p", "q"],
"body": {"op": "*", "args": [
{"op": "apply_expression_template", "args": [], "name": "inner",
"bindings": {"m": "K", "x": "p", "y": "q"}},
2.0]}}
}),
json!({"K": "spherical", "p": "pa", "q": "pb"}),
"outer",
);
lower_expression_templates(&mut v).expect("rewrite");
expand(&mut v).expect("expand");
assert_eq!(
*crate::classification::observed_definition_json(&v["models"]["M"], "area")
.expect("area defining equation"),
json!({"op": "*", "args": [
{"op": "polygon_intersection_area", "manifold": "spherical",
"args": ["pa", "pb"]},
2.0]})
);
}
#[test]
fn scalar_field_invalid_substituted_manifold_rejected() {
let mut v = scalar_field_doc(
json!({"overlap_area": {
"params": ["K_manifold", "a", "b"],
"body": {"op": "polygon_intersection_area",
"manifold": "K_manifold", "args": ["a", "b"]}}}),
json!({"K_manifold": "bogus", "a": "pa", "b": "pb"}),
"overlap_area",
);
let err = lower_expression_templates(&mut v).expect_err("must reject");
assert_eq!(err.code, "geometry_manifold_invalid");
}
#[test]
fn scalar_field_params_shadow_literals() {
let mut v = scalar_field_doc(
json!({"shadowed": {
"params": ["planar", "x", "y"],
"body": {"op": "polygon_intersection_area",
"manifold": "planar", "args": ["x", "y"]}}}),
json!({"planar": "spherical", "x": "pa", "y": "pb"}),
"shadowed",
);
lower_expression_templates(&mut v).expect("rewrite");
expand(&mut v).expect("expand");
assert_eq!(
crate::classification::observed_definition_json(&v["models"]["M"], "area")
.expect("area defining equation")["manifold"],
json!("spherical")
);
}
}