use std::collections::HashMap;
use std::sync::Arc;
use crate::compile_error::CompileError;
use crate::precision::{Env, Precision, VarPrecisions};
use crate::types::{Equation, Expr, ExpressionNode, Model};
pub const MARKER_OP: &str = "__precision";
#[must_use]
pub fn marker_precision(node: &ExpressionNode) -> Option<Precision> {
if node.op != MARKER_OP {
return None;
}
Precision::from_element_type(node.name.as_deref()).ok()
}
#[must_use]
pub fn marker_precision_json(node: &serde_json::Value) -> Option<Precision> {
if node.get("op").and_then(serde_json::Value::as_str) != Some(MARKER_OP) {
return None;
}
Precision::from_element_type(node.get("name").and_then(serde_json::Value::as_str)).ok()
}
pub(crate) fn mark_exact_key_comparison(expr: Expr) -> Expr {
mark(expr, Precision::Float64)
}
fn mark(expr: Expr, p: Precision) -> Expr {
Expr::Operator(Arc::new(ExpressionNode {
op: MARKER_OP.to_string(),
args: vec![expr],
name: Some(p.element_type().to_string()),
..Default::default()
}))
}
fn is_predicate_op(op: &str) -> bool {
matches!(
op,
"==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "not"
)
}
#[derive(Clone, Copy)]
struct Source<'a> {
prec: Precision,
var: &'a str,
}
type Inferred<'a> = Option<Source<'a>>;
pub fn annotate_model(model: &mut Model, document: Precision) -> Result<(), CompileError> {
annotate_model_with(model, document, &HashMap::new())
}
fn variable_precision(
var: &crate::types::ModelVariable,
document: Precision,
) -> Result<Precision, CompileError> {
match var.element_type.as_deref() {
None => Ok(document),
some => Precision::from_element_type(some),
}
}
pub fn annotate_models(
models: &mut indexmap::IndexMap<String, Model>,
document: Precision,
) -> Result<(), CompileError> {
if !models
.values()
.any(|m| m.variables.values().any(|v| v.element_type.is_some()))
{
return Ok(());
}
let mut qualified: HashMap<String, Precision> = HashMap::new();
for (mname, model) in models.iter() {
for (vname, var) in &model.variables {
qualified.insert(
format!("{mname}.{vname}"),
variable_precision(var, document)?,
);
}
}
for (_, model) in models.iter_mut() {
annotate_model_with(model, document, &qualified)?;
}
Ok(())
}
fn annotate_model_with(
model: &mut Model,
document: Precision,
qualified: &HashMap<String, Precision>,
) -> Result<(), CompileError> {
if !model.variables.values().any(|v| v.element_type.is_some())
&& !qualified.values().any(|p| *p != document)
{
return Ok(());
}
let mut env: HashMap<String, Precision> = qualified.clone();
env.reserve(model.variables.len());
for (name, var) in &model.variables {
env.insert(name.clone(), variable_precision(var, document)?);
}
let equations = std::mem::take(&mut model.equations);
let annotated = annotate_equations(equations, &env, document);
let init = model.initialization_equations.take();
let annotated_init = init.map(|eqs| annotate_equations(eqs, &env, document));
model.equations = annotated?;
model.initialization_equations = annotated_init.transpose()?;
Ok(())
}
pub fn env_of_file(file: &crate::types::EsmFile) -> Result<Env, CompileError> {
let document =
Precision::from_element_type(file.domain.as_ref().and_then(|d| d.element_type.as_deref()))?;
let vars = match file.models.as_ref() {
Some(models) => {
VarPrecisions::from_declarations(&model_declarations(models), document, None)?
}
None => VarPrecisions::default(),
};
Ok(Env {
document,
variables: std::sync::Arc::new(vars),
})
}
#[must_use]
pub fn model_declarations(
models: &indexmap::IndexMap<String, crate::types::Model>,
) -> Vec<crate::precision::ModelDeclarations<'_>> {
models
.iter()
.map(|(mname, model)| {
let subs = model
.subsystems
.as_ref()
.map(|s| serde_json::to_value(s).unwrap_or(serde_json::Value::Null));
crate::precision::ModelDeclarations {
model: mname,
variables: model
.variables
.iter()
.map(|(v, d)| (v.as_str(), d.element_type.as_deref()))
.collect(),
subsystem_variables: crate::precision::subsystem_declarations(subs.as_ref()),
}
})
.collect()
}
pub fn annotated(
file: &crate::types::EsmFile,
) -> Result<Option<crate::types::EsmFile>, CompileError> {
let declares = file
.models
.iter()
.flatten()
.any(|(_, m)| m.variables.values().any(|v| v.element_type.is_some()));
if !declares {
return Ok(None);
}
let document = env_of_file(file)?.document;
let mut out = file.clone();
if let Some(models) = out.models.as_mut() {
annotate_models(models, document)?;
}
Ok(Some(out))
}
pub fn check_flattened(flat: &crate::flatten::FlattenedSystem) -> Result<(), CompileError> {
let registries = [
&flat.state_variables,
&flat.parameters,
&flat.observed_variables,
&flat.algebraic_variables,
&flat.brownian_parameters,
&flat.discrete_parameters,
];
if !registries
.iter()
.any(|r| r.values().any(|v| v.element_type.is_some()))
{
return Ok(());
}
let document =
Precision::from_element_type(flat.domain.as_ref().and_then(|d| d.element_type.as_deref()))?;
let mut env: HashMap<String, Precision> = HashMap::new();
for r in registries {
for (name, var) in r {
env.insert(name.clone(), variable_precision(var, document)?);
}
}
for eq in &flat.equations {
annotate_equation(eq.clone(), &env, document)?;
}
Ok(())
}
fn annotate_equations(
equations: Vec<Equation>,
env: &HashMap<String, Precision>,
document: Precision,
) -> Result<Vec<Equation>, CompileError> {
equations
.into_iter()
.map(|eq| annotate_equation(eq, env, document))
.collect()
}
fn annotate_equation(
mut eq: Equation,
env: &HashMap<String, Precision>,
document: Precision,
) -> Result<Equation, CompileError> {
let (lhs_src, _) = infer(&eq.lhs, env)?;
let target = lhs_src.map_or(document, |s| s.prec);
let lhs_name = lhs_src.map_or("the left-hand side", |s| s.var).to_string();
let (rhs_src, rhs) = infer(&eq.rhs, env)?;
if let Some(src) = rhs_src
&& src.prec != target
{
return Err(CompileError::MixedElementType {
op: "equation".to_string(),
lhs_name,
lhs_type: target.element_type().to_string(),
rhs_name: src.var.to_string(),
rhs_type: src.prec.element_type().to_string(),
});
}
eq.rhs = rhs;
Ok(eq)
}
fn infer<'a>(
expr: &Expr,
env: &'a HashMap<String, Precision>,
) -> Result<(Inferred<'a>, Expr), CompileError> {
match expr {
Expr::Integer(_) | Expr::Number(_) => Ok((None, expr.clone())),
Expr::Variable(name) => match env.get_key_value(name.as_str()) {
Some((k, p)) => Ok((
Some(Source {
prec: *p,
var: k.as_str(),
}),
expr.clone(),
)),
None => Ok((None, expr.clone())),
},
Expr::Operator(node) => infer_op(node, env),
}
}
fn infer_op<'a>(
node: &ExpressionNode,
env: &'a HashMap<String, Precision>,
) -> Result<(Inferred<'a>, Expr), CompileError> {
let mut children: Vec<(Inferred<'a>, Expr)> = Vec::new();
let mut err: Option<CompileError> = None;
let mut collect = |child: &Expr| {
if err.is_some() {
return;
}
match infer(child, env) {
Ok(pair) => children.push(pair),
Err(e) => err = Some(e),
}
};
node.for_each_child(&mut collect);
if let Some(e) = err {
return Err(e);
}
let mut resolved: Inferred<'a> = None;
for (src, _) in &children {
let Some(src) = src else { continue };
match resolved {
None => resolved = Some(*src),
Some(prev) if prev.prec == src.prec => {}
Some(prev) => {
return Err(CompileError::MixedElementType {
op: node.op.clone(),
lhs_name: prev.var.to_string(),
lhs_type: prev.prec.element_type().to_string(),
rhs_name: src.var.to_string(),
rhs_type: src.prec.element_type().to_string(),
});
}
}
}
let mut rebuilt = node.clone();
let mut rewritten = children.iter().map(|(_, e)| e.clone());
rebuilt.for_each_child_mut(&mut |slot: &mut Expr| {
if let Some(e) = rewritten.next() {
*slot = e;
}
});
let out = Expr::Operator(Arc::new(rebuilt));
match resolved {
None => Ok((None, out)),
Some(src) if is_predicate_op(&node.op) => {
Ok((None, mark(out, src.prec)))
}
Some(src) => Ok((Some(src), out)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ModelVariable, VariableType};
fn var(element_type: Option<&str>) -> ModelVariable {
ModelVariable {
var_type: VariableType::Parameter,
element_type: element_type.map(str::to_string),
..Default::default()
}
}
fn model(vars: &[(&str, Option<&str>)], eqs: Vec<Equation>) -> Model {
let mut m = Model::default();
for (n, et) in vars {
m.variables.insert((*n).to_string(), var(*et));
}
m.equations = eqs;
m
}
fn eq(lhs: &str, rhs: Expr) -> Equation {
Equation {
lhs: Expr::Variable(lhs.to_string()),
rhs,
comment: None,
}
}
fn op(name: &str, args: Vec<Expr>) -> Expr {
Expr::Operator(Arc::new(ExpressionNode {
op: name.to_string(),
args,
..Default::default()
}))
}
fn v(n: &str) -> Expr {
Expr::Variable(n.to_string())
}
fn shape(e: &Expr) -> String {
match e {
Expr::Integer(i) => i.to_string(),
Expr::Number(n) => n.to_string(),
Expr::Variable(s) => s.clone(),
Expr::Operator(node) => {
let mut parts = Vec::new();
node.for_each_child(&mut |c| parts.push(shape(c)));
let head = match node.name.as_deref() {
Some(n) if node.op == MARKER_OP => format!("{}:{n}", node.op),
_ => node.op.clone(),
};
format!("{head}({})", parts.join(","))
}
}
}
#[test]
fn no_declaration_is_a_no_op() {
let mut m = model(
&[("a", None), ("b", None)],
vec![eq("a", op("*", vec![v("b"), Expr::Number(0.1)]))],
);
let before = shape(&m.equations[0].rhs);
annotate_model(&mut m, Precision::Float32).unwrap();
assert_eq!(shape(&m.equations[0].rhs), before, "no marker inserted");
}
#[test]
fn exempt_arithmetic_is_marked_float64() {
let mut m = model(
&[("scc", Some("Float64")), ("bucket", Some("Float64"))],
vec![eq(
"bucket",
op(
"*",
vec![
op("floor", vec![op("/", vec![v("scc"), Expr::Integer(1000)])]),
Expr::Integer(1000),
],
),
)],
);
annotate_model(&mut m, Precision::Float32).unwrap();
assert_eq!(
shape(&m.equations[0].rhs),
"*(floor(/(scc,1000)),1000)",
"no marker: the equation is uniformly binary64 and the RULE carries \
that, so every pass that matches on the right-hand side's root \
still sees the operator it did"
);
}
#[test]
fn a_predicate_over_exempt_keys_is_marked_inside_a_float32_equation() {
let mut m = model(
&[("q", None), ("quant", None), ("key", Some("Float64"))],
vec![eq(
"q",
op(
"*",
vec![
v("quant"),
op("==", vec![v("key"), Expr::Integer(2_260_007_005)]),
],
),
)],
);
annotate_model(&mut m, Precision::Float32).unwrap();
assert_eq!(
shape(&m.equations[0].rhs),
"*(quant,__precision:Float64(==(key,2260007005)))",
"the comparison runs in binary64; the product stays binary32"
);
}
#[test]
fn mixing_two_element_types_in_one_operator_is_refused() {
let mut m = model(
&[("q", None), ("quant", None), ("key", Some("Float64"))],
vec![eq("q", op("*", vec![v("quant"), v("key")]))],
);
let err = annotate_model(&mut m, Precision::Float32).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("quant"), "{msg}");
assert!(msg.contains("key"), "{msg}");
assert!(msg.contains("mixed_element_type"), "{msg}");
}
#[test]
fn storing_across_element_types_is_refused() {
let mut m = model(
&[("q", None), ("key", Some("Float64"))],
vec![eq("q", op("+", vec![v("key"), Expr::Integer(1)]))],
);
let err = annotate_model(&mut m, Precision::Float32).unwrap_err();
assert!(format!("{err}").contains("equation"), "{err}");
}
#[test]
fn a_float64_document_can_declare_a_float32_variable() {
let mut m = model(
&[("q", Some("Float32")), ("r", Some("Float32"))],
vec![eq("q", op("*", vec![v("r"), Expr::Number(0.1)]))],
);
annotate_model(&mut m, Precision::Float64).unwrap();
assert_eq!(
shape(&m.equations[0].rhs),
"*(r,0.1)",
"uniformly binary32 inside a binary64 document; the rule carries it"
);
}
#[test]
fn a_scoped_reference_to_another_model_carries_that_model_s_element_type() {
let mut models: indexmap::IndexMap<String, Model> = indexmap::IndexMap::new();
models.insert(
"A".to_string(),
model(
&[("out", Some("Float64")), ("key", Some("Float64"))],
vec![eq("out", op("*", vec![v("key"), v("B.q")]))],
),
);
models.insert("B".to_string(), model(&[("q", None)], vec![]));
let err = annotate_models(&mut models, Precision::Float32).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("B.q"), "{msg}");
assert!(msg.contains("key"), "{msg}");
}
#[test]
fn an_unknown_element_type_is_refused() {
let mut m = model(&[("q", Some("Float16"))], vec![]);
assert!(annotate_model(&mut m, Precision::Float64).is_err());
}
}