use std::collections::HashMap;
use thiserror::Error;
use crate::compile_error::CompileError;
use crate::types::{Expr, IndexSet, Model, RangeSpec};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReduceKind {
Sum,
Product,
Max,
Min,
Or,
And,
}
impl ReduceKind {
pub fn identity(self) -> f64 {
match self {
ReduceKind::Sum => 0.0,
ReduceKind::Product => 1.0,
ReduceKind::Max => f64::NEG_INFINITY,
ReduceKind::Min => f64::INFINITY,
ReduceKind::Or => 0.0, ReduceKind::And => 1.0, }
}
pub fn combine(self, acc: f64, term: f64) -> f64 {
if crate::precision::is_f32() {
return self.combine_f32(acc, term);
}
match self {
ReduceKind::Sum => acc + term,
ReduceKind::Product => acc * term,
ReduceKind::Max => f64::max(acc, term),
ReduceKind::Min => f64::min(acc, term),
ReduceKind::Or => {
if acc != 0.0 || term != 0.0 {
1.0
} else {
0.0
}
}
ReduceKind::And => {
if acc != 0.0 && term != 0.0 {
1.0
} else {
0.0
}
}
}
}
fn combine_f32(self, acc: f64, term: f64) -> f64 {
let (a, t) = (acc as f32, term as f32);
match self {
ReduceKind::Sum => (a + t) as f64,
ReduceKind::Product => (a * t) as f64,
ReduceKind::Max => f32::max(a, t) as f64,
ReduceKind::Min => f32::min(a, t) as f64,
ReduceKind::Or => {
if a != 0.0 || t != 0.0 {
1.0
} else {
0.0
}
}
ReduceKind::And => {
if a != 0.0 && t != 0.0 {
1.0
} else {
0.0
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Semiring {
SumProduct,
MaxProduct,
MinSum,
MaxSum,
BoolAndOr,
}
impl Semiring {
pub fn from_name(name: &str) -> Option<Self> {
Some(match name {
"sum_product" => Semiring::SumProduct,
"max_product" => Semiring::MaxProduct,
"min_sum" => Semiring::MinSum,
"max_sum" => Semiring::MaxSum,
"bool_and_or" => Semiring::BoolAndOr,
_ => return None,
})
}
pub fn oplus(self) -> ReduceKind {
match self {
Semiring::SumProduct => ReduceKind::Sum,
Semiring::MaxProduct => ReduceKind::Max,
Semiring::MinSum => ReduceKind::Min,
Semiring::MaxSum => ReduceKind::Max,
Semiring::BoolAndOr => ReduceKind::Or,
}
}
pub fn otimes(self) -> ReduceKind {
match self {
Semiring::SumProduct => ReduceKind::Product,
Semiring::MaxProduct => ReduceKind::Product,
Semiring::MinSum => ReduceKind::Sum,
Semiring::MaxSum => ReduceKind::Sum,
Semiring::BoolAndOr => ReduceKind::And,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum BadOplus {
#[error(
"aggregate `semiring` is '{0}', which is not in the closed RFC §5.1 registry \
(sum_product, max_product, min_sum, max_sum, bool_and_or)"
)]
Semiring(String),
#[error(
"aggregate `reduce` is '{0}', which is not in the schema's closed enum \
(+, *, max, min); a ⊕ outside it — the boolean `or` included — is named by \
setting `semiring`, not by inventing a `reduce` spelling"
)]
Reduce(String),
}
pub fn effective_reduce_kind(
semiring: Option<&str>,
reduce: Option<&str>,
) -> Result<ReduceKind, BadOplus> {
if let Some(name) = semiring {
return Semiring::from_name(name)
.map(Semiring::oplus)
.ok_or_else(|| BadOplus::Semiring(name.to_string()));
}
match reduce {
None | Some("+") => Ok(ReduceKind::Sum),
Some("*") => Ok(ReduceKind::Product),
Some("max") => Ok(ReduceKind::Max),
Some("min") => Ok(ReduceKind::Min),
Some(other) => Err(BadOplus::Reduce(other.to_string())),
}
}
pub fn validate_oplus_spellings(model: &Model) -> Result<(), CompileError> {
for eq in &model.equations {
check_expr_oplus(&eq.lhs)?;
check_expr_oplus(&eq.rhs)?;
}
if let Some(init_eqs) = &model.initialization_equations {
for eq in init_eqs {
check_expr_oplus(&eq.lhs)?;
check_expr_oplus(&eq.rhs)?;
}
}
let mut failure = None;
for var in model.variables.values() {
var.for_each_expression(&mut |expr| {
if failure.is_none()
&& let Err(e) = check_expr_oplus(expr)
{
failure = Some(e);
}
});
}
match failure {
Some(e) => Err(e),
None => Ok(()),
}
}
fn check_expr_oplus(expr: &Expr) -> Result<(), CompileError> {
let Expr::Operator(node) = expr else {
return Ok(());
};
if is_faq_op(&node.op) {
match effective_reduce_kind(node.semiring.as_deref(), node.reduce.as_deref()) {
Err(bad) => return Err(CompileError::build_err(bad.to_string())),
Ok(ReduceKind::Or) if is_array_valued_numeric_reduction(node) => {
return Err(CompileError::build_err(format!(
"array-valued `bool_and_or` reduction{}: the numeric evaluators reject a \
`bool_and_or` faq that has output indices and contracts one \
(CONFORMANCE_SPEC §5.6.1); a scalar one, with no output index, runs",
node.id
.as_deref()
.map(|id| format!(" `{id}`"))
.unwrap_or_default()
)));
}
Ok(_) => {}
}
}
let mut failure = None;
node.for_each_child(&mut |child| {
if failure.is_none()
&& let Err(e) = check_expr_oplus(child)
{
failure = Some(e);
}
});
match failure {
Some(e) => Err(e),
None => Ok(()),
}
}
fn is_array_valued_numeric_reduction(node: &crate::types::ExpressionNode) -> bool {
if node.distinct == Some(true) || node.key.is_some() || node.id.is_some() {
return false;
}
let out: Vec<&str> = node
.output_idx
.iter()
.flatten()
.map(String::as_str)
.collect();
!out.is_empty()
&& node
.ranges
.as_ref()
.is_some_and(|r| r.keys().any(|k| !out.contains(&k.as_str())))
}
pub fn is_faq_op(op: &str) -> bool {
op == "faq"
}
pub fn resolve_aggregate_ranges(
model: &mut Model,
index_sets: &HashMap<String, IndexSet>,
) -> Result<(), CompileError> {
resolve_aggregate_ranges_with_extents(model, index_sets, empty_derived_extents())
}
pub(crate) fn empty_derived_extents() -> &'static HashMap<String, i64> {
static EMPTY: std::sync::OnceLock<HashMap<String, i64>> = std::sync::OnceLock::new();
EMPTY.get_or_init(HashMap::new)
}
pub fn resolve_aggregate_ranges_with_extents(
model: &mut Model,
index_sets: &HashMap<String, IndexSet>,
derived_extents: &HashMap<String, i64>,
) -> Result<(), CompileError> {
for eq in &mut model.equations {
resolve_expr_ranges_with_extents(&mut eq.lhs, index_sets, derived_extents)?;
resolve_expr_ranges_with_extents(&mut eq.rhs, index_sets, derived_extents)?;
}
if let Some(init_eqs) = &mut model.initialization_equations {
for eq in init_eqs {
resolve_expr_ranges_with_extents(&mut eq.lhs, index_sets, derived_extents)?;
resolve_expr_ranges_with_extents(&mut eq.rhs, index_sets, derived_extents)?;
}
}
let mut failure = None;
for var in model.variables.values_mut() {
var.for_each_expression_mut(&mut |expr| {
if failure.is_none()
&& let Err(e) = resolve_expr_ranges_with_extents(expr, index_sets, derived_extents)
{
failure = Some(e);
}
});
}
match failure {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn resolve_expr_ranges(
expr: &mut Expr,
index_sets: &HashMap<String, IndexSet>,
) -> Result<(), CompileError> {
resolve_expr_ranges_with_extents(expr, index_sets, empty_derived_extents())
}
fn contains_unresolved_range(e: &Expr) -> bool {
match e {
Expr::Operator(node) => {
node.ranges.as_ref().is_some_and(|r| {
r.values()
.any(|s| matches!(s, RangeSpec::IndexSetRef { .. }))
}) || node.any_child(&mut contains_unresolved_range)
}
_ => false,
}
}
pub fn resolve_expr_ranges_with_extents(
expr: &mut Expr,
index_sets: &HashMap<String, IndexSet>,
derived_extents: &HashMap<String, i64>,
) -> Result<(), CompileError> {
if !contains_unresolved_range(expr) {
return Ok(());
}
let Some(node) = expr.node_mut() else {
return Ok(());
};
let output_names: std::collections::HashSet<String> = node
.output_idx
.clone()
.unwrap_or_default()
.into_iter()
.collect();
if let Some(ranges) = &mut node.ranges {
for (idx_name, spec) in ranges.iter_mut() {
let is_output = output_names.contains(idx_name);
let resolved = match spec {
RangeSpec::Interval(_)
| RangeSpec::Strided(_)
| RangeSpec::RaggedDyn { .. }
| RangeSpec::DerivedDyn { .. } => continue,
RangeSpec::IndexSetRef { from, of } => resolve_index_set_ref(
from,
of.as_deref(),
idx_name,
is_output,
index_sets,
derived_extents,
)?,
};
*spec = match resolved {
ResolvedRange::Static(iv) => RangeSpec::Interval(iv),
ResolvedRange::Ragged { offsets, of } => RangeSpec::RaggedDyn { offsets, of },
ResolvedRange::Derived { from_faq } => RangeSpec::DerivedDyn { from_faq },
};
}
}
let mut err: Option<CompileError> = None;
node.for_each_child_mut(&mut |child| {
if err.is_none()
&& let Err(e) = resolve_expr_ranges_with_extents(child, index_sets, derived_extents)
{
err = Some(e);
}
});
match err {
Some(e) => Err(e),
None => Ok(()),
}
}
#[derive(Debug)]
enum ResolvedRange {
Static([i64; 2]),
Ragged {
offsets: String,
of: Vec<String>,
},
Derived {
from_faq: String,
},
}
fn resolve_index_set_ref(
from: &str,
of: Option<&[String]>,
idx_name: &str,
is_output: bool,
index_sets: &HashMap<String, IndexSet>,
derived_extents: &HashMap<String, i64>,
) -> Result<ResolvedRange, CompileError> {
let set = index_sets.get(from).ok_or_else(|| {
CompileError::build_err(format!(
"aggregate range '{idx_name}' references index set '{from}', which is not declared \
in the document `index_sets` registry (no implicit interval inference; RFC \
semiring-faq-unified-ir §5.2)"
))
})?;
match set.kind.as_str() {
"interval" => {
let size = set.size.ok_or_else(|| {
CompileError::build_err(format!(
"index set '{from}' has kind \"interval\" but no `size`"
))
})?;
Ok(ResolvedRange::Static([1, size]))
}
"categorical" => {
let n = set
.members
.as_ref()
.map(|m| m.len() as i64)
.ok_or_else(|| {
CompileError::build_err(format!(
"index set '{from}' has kind \"categorical\" but no `members`"
))
})?;
Ok(ResolvedRange::Static([1, n]))
}
"ragged" => {
if is_output {
return Err(CompileError::UnsupportedFeatureError {
feature: "ragged output index".to_string(),
message: format!(
"aggregate output index '{idx_name}' references ragged index set '{from}'; \
a ragged set's extent is per-parent dynamic and may only be a contracted \
(reduction) index, not an output index (RFC semiring-faq-unified-ir §5.2)"
),
});
}
let parents = of.unwrap_or_default();
if parents.is_empty() {
return Err(CompileError::build_err(format!(
"ragged index set '{from}' (aggregate range '{idx_name}') is referenced \
without an `of` parent index; a ragged set's length is a function of its \
parent (RFC semiring-faq-unified-ir §5.2)"
)));
}
let offsets = set.offsets.clone().ok_or_else(|| {
CompileError::build_err(format!(
"ragged index set '{from}' (aggregate range '{idx_name}') requires an \
`offsets` backing factor giving |set(parent)| per parent tuple"
))
})?;
Ok(ResolvedRange::Ragged {
offsets,
of: parents.to_vec(),
})
}
"derived" => {
let from_faq =
set.from_faq
.clone()
.ok_or_else(|| {
CompileError::build_err(format!(
"derived index set '{from}' (aggregate range '{idx_name}') is missing \
`from_faq` naming its producing FAQ node (RFC semiring-faq-unified-ir §5.5)"
))
})?;
if let Some(&n) = derived_extents.get(&from_faq) {
return Ok(ResolvedRange::Static([1, n]));
}
if is_output {
return Err(CompileError::UnsupportedFeatureError {
feature: "derived output index".to_string(),
message: format!(
"aggregate output index '{idx_name}' references derived index set '{from}'; \
a derived (FAQ-materialized) set's extent is data-dependent and may only \
be a contracted (reduction) index, not an output index, unless its \
`from_faq` producer '{from_faq}' has a build-time value-invention extent \
(RFC semiring-faq-unified-ir §5.5 / §8.1)"
),
});
}
Ok(ResolvedRange::Derived { from_faq })
}
other => Err(CompileError::build_err(format!(
"index set '{from}' has unknown kind '{other}'"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Equation, ExpressionNode};
fn interval(size: i64) -> IndexSet {
IndexSet {
kind: "interval".into(),
size: Some(size),
members: None,
from_faq: None,
member_factor: None,
of: None,
offsets: None,
values: None,
}
}
fn ragged(offsets: Option<&str>) -> IndexSet {
IndexSet {
kind: "ragged".into(),
size: None,
members: None,
from_faq: None,
member_factor: None,
of: Some(vec!["cells".into()]),
offsets: offsets.map(str::to_string),
values: Some("edgesOnCell".into()),
}
}
fn resolve_ref(
from: &str,
of: Option<&[String]>,
idx_name: &str,
is_output: bool,
index_sets: &HashMap<String, IndexSet>,
) -> Result<ResolvedRange, CompileError> {
resolve_index_set_ref(
from,
of,
idx_name,
is_output,
index_sets,
empty_derived_extents(),
)
}
fn static_bounds(r: ResolvedRange) -> [i64; 2] {
match r {
ResolvedRange::Static(iv) => iv,
ResolvedRange::Ragged { .. } => panic!("expected a static range, got ragged"),
ResolvedRange::Derived { .. } => panic!("expected a static range, got derived"),
}
}
#[test]
fn semiring_identities_match_rfc_table() {
let cases = [
(Semiring::SumProduct, 0.0, 1.0),
(Semiring::MaxProduct, f64::NEG_INFINITY, 1.0),
(Semiring::MinSum, f64::INFINITY, 0.0),
(Semiring::MaxSum, f64::NEG_INFINITY, 0.0),
(Semiring::BoolAndOr, 0.0, 1.0), ];
for (sr, zero_bar, one_bar) in cases {
assert_eq!(sr.oplus().identity(), zero_bar, "{sr:?} 0̄");
assert_eq!(sr.otimes().identity(), one_bar, "{sr:?} 1̄");
}
}
#[test]
fn semiring_is_authoritative_over_reduce() {
assert_eq!(
effective_reduce_kind(Some("min_sum"), None),
Ok(ReduceKind::Min)
);
assert_eq!(
effective_reduce_kind(Some("max_sum"), Some("+")),
Ok(ReduceKind::Max)
);
assert_eq!(
effective_reduce_kind(Some("max_product"), None),
Ok(ReduceKind::Max)
);
assert_eq!(
effective_reduce_kind(Some("bool_and_or"), None),
Ok(ReduceKind::Or)
);
assert_eq!(effective_reduce_kind(None, None), Ok(ReduceKind::Sum));
assert_eq!(effective_reduce_kind(None, Some("+")), Ok(ReduceKind::Sum));
assert_eq!(
effective_reduce_kind(None, Some("*")),
Ok(ReduceKind::Product)
);
assert_eq!(
effective_reduce_kind(None, Some("max")),
Ok(ReduceKind::Max)
);
assert_eq!(
effective_reduce_kind(None, Some("min")),
Ok(ReduceKind::Min)
);
}
#[test]
fn spellings_outside_the_closed_enums_are_rejected() {
assert_eq!(
effective_reduce_kind(Some("bogus"), Some("min")),
Err(BadOplus::Semiring("bogus".to_string()))
);
assert_eq!(
effective_reduce_kind(Some("bogus"), None),
Err(BadOplus::Semiring("bogus".to_string()))
);
for bad in ["sum", "prod", "mean", "and", ""] {
assert_eq!(
effective_reduce_kind(None, Some(bad)),
Err(BadOplus::Reduce(bad.to_string())),
"reduce={bad:?}"
);
}
assert_eq!(
effective_reduce_kind(None, Some("or")),
Err(BadOplus::Reduce("or".to_string()))
);
assert_eq!(
effective_reduce_kind(Some("bool_and_or"), None),
Ok(ReduceKind::Or)
);
}
#[test]
fn validate_oplus_spellings_names_the_offending_spelling() {
let bad_agg = |semiring: Option<&str>, reduce: Option<&str>| {
let mut node = ExpressionNode {
op: "faq".to_string(),
..Default::default()
};
node.semiring = semiring.map(str::to_string);
node.reduce = reduce.map(str::to_string);
node.expr = Some(Box::new(Expr::Variable("x".to_string())));
Expr::Operator(node.into())
};
let model_with = |rhs: Expr| {
let mut m = Model::default();
m.equations.push(Equation {
lhs: Expr::Variable("y".to_string()),
rhs,
..Default::default()
});
m
};
for ok in [
bad_agg(None, None),
bad_agg(None, Some("max")),
bad_agg(Some("bool_and_or"), None),
] {
assert!(validate_oplus_spellings(&model_with(ok)).is_ok());
}
let err = validate_oplus_spellings(&model_with(bad_agg(None, Some("mean"))))
.expect_err("an out-of-enum `reduce` must be rejected");
assert!(err.to_string().contains("mean"), "{err}");
let err = validate_oplus_spellings(&model_with(bad_agg(Some("bogus"), None)))
.expect_err("an unregistered `semiring` must be rejected");
assert!(err.to_string().contains("bogus"), "{err}");
let nested = Expr::Operator(
ExpressionNode {
op: "+".to_string(),
args: vec![Expr::Variable("z".to_string()), bad_agg(None, Some("mean"))],
..Default::default()
}
.into(),
);
let err = validate_oplus_spellings(&model_with(nested))
.expect_err("a nested aggregate must be reached");
assert!(err.to_string().contains("mean"), "{err}");
}
#[test]
fn or_and_reductions_are_crisp_boolean() {
assert_eq!(ReduceKind::Or.combine(0.0, 0.0), 0.0);
assert_eq!(ReduceKind::Or.combine(0.0, 3.0), 1.0);
assert_eq!(ReduceKind::Or.combine(2.0, 0.0), 1.0);
assert_eq!(ReduceKind::And.combine(1.0, 1.0), 1.0);
assert_eq!(ReduceKind::And.combine(1.0, 0.0), 0.0);
assert_eq!(ReduceKind::And.combine(0.0, 0.0), 0.0);
}
#[test]
fn float32_reductions_accumulate_in_binary32() {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
let terms = [0.1_f64; 10];
let mut acc = ReduceKind::Sum.identity();
for &t in &terms {
acc = ReduceKind::Sum.combine(acc, crate::precision::round(t));
}
let mut want = 0.0_f32;
for _ in 0..10 {
want += 0.1_f32;
}
assert_eq!(
acc.to_bits(),
(want as f64).to_bits(),
"the accumulator must be binary32: got {acc:?}, want {:?}",
want as f64
);
let f64_sum: f64 = terms.iter().sum();
assert_ne!(acc.to_bits(), f64_sum.to_bits());
let mut p = ReduceKind::Product.identity();
for _ in 0..5 {
p = ReduceKind::Product.combine(p, crate::precision::round(1.1));
}
let mut wp = 1.0_f32;
for _ in 0..5 {
wp *= 1.1_f32;
}
assert_eq!(p.to_bits(), (wp as f64).to_bits());
}
#[test]
fn semiring_identities_are_exact_in_binary32() {
for k in [
ReduceKind::Sum,
ReduceKind::Product,
ReduceKind::Max,
ReduceKind::Min,
ReduceKind::Or,
ReduceKind::And,
] {
let id = k.identity();
assert_eq!(
id.to_bits(),
crate::precision::Precision::Float32.round(id).to_bits(),
"{k:?}'s identity {id:?} is not exactly representable in binary32"
);
}
}
#[test]
fn float64_reductions_are_bit_unchanged() {
assert_eq!(
crate::precision::active(),
crate::precision::Precision::Float64
);
let xs = [0.1_f64, -3.25, 1e300, f64::INFINITY, 0.0, -0.0];
for &a in &xs {
for &b in &xs {
assert_eq!(ReduceKind::Sum.combine(a, b).to_bits(), (a + b).to_bits());
assert_eq!(
ReduceKind::Product.combine(a, b).to_bits(),
(a * b).to_bits()
);
assert_eq!(
ReduceKind::Max.combine(a, b).to_bits(),
f64::max(a, b).to_bits()
);
assert_eq!(
ReduceKind::Min.combine(a, b).to_bits(),
f64::min(a, b).to_bits()
);
}
}
}
#[test]
fn faq_op_tag_is_exact() {
assert!(is_faq_op("faq"));
assert!(!is_faq_op("aggregate"));
assert!(!is_faq_op("arrayop"));
assert!(!is_faq_op("makearray"));
assert!(!is_faq_op("+"));
}
#[test]
fn resolve_interval_and_categorical_from() {
let mut index_sets = HashMap::new();
index_sets.insert("cells".to_string(), interval(5));
index_sets.insert(
"county".to_string(),
IndexSet {
kind: "categorical".into(),
size: None,
members: Some(vec![
serde_json::json!("Champaign"),
serde_json::json!("Cook"),
serde_json::json!("Sangamon"),
]),
from_faq: None,
member_factor: None,
of: None,
offsets: None,
values: None,
},
);
assert_eq!(
static_bounds(resolve_ref("cells", None, "i", false, &index_sets).unwrap()),
[1, 5]
);
assert_eq!(
static_bounds(resolve_ref("county", None, "c", false, &index_sets).unwrap()),
[1, 3]
);
assert_eq!(
static_bounds(
resolve_ref("cells", Some(&["i".into()]), "i", false, &index_sets).unwrap()
),
[1, 5]
);
}
#[test]
fn undeclared_from_errors_naming_the_set() {
let index_sets: HashMap<String, IndexSet> = HashMap::new();
let err = resolve_ref("nonesuch", None, "i", false, &index_sets).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("nonesuch"), "error should name the set: {msg}");
}
#[test]
fn ragged_contracted_index_resolves_to_dynamic_bound() {
let mut index_sets = HashMap::new();
index_sets.insert("edges".to_string(), ragged(Some("nEdgesOnCell")));
let resolved = resolve_ref("edges", Some(&["i".into()]), "k", false, &index_sets).unwrap();
match resolved {
ResolvedRange::Ragged { offsets, of } => {
assert_eq!(offsets, "nEdgesOnCell");
assert_eq!(of, vec!["i".to_string()]);
}
ResolvedRange::Static(iv) => panic!("expected ragged, got static {iv:?}"),
ResolvedRange::Derived { from_faq } => {
panic!("expected ragged, got derived {from_faq}")
}
}
}
#[test]
fn ragged_as_output_index_is_rejected() {
let mut index_sets = HashMap::new();
index_sets.insert("edges".to_string(), ragged(Some("nEdgesOnCell")));
let err = resolve_ref("edges", Some(&["i".into()]), "k", true, &index_sets).unwrap_err();
let msg = format!("{err:?}");
assert!(msg.contains("ragged"), "error should mention ragged: {msg}");
}
#[test]
fn ragged_without_of_parent_is_rejected() {
let mut index_sets = HashMap::new();
index_sets.insert("edges".to_string(), ragged(Some("nEdgesOnCell")));
assert!(resolve_ref("edges", None, "k", false, &index_sets).is_err());
assert!(resolve_ref("edges", Some(&[]), "k", false, &index_sets).is_err());
}
#[test]
fn ragged_missing_offsets_factor_is_rejected() {
let mut index_sets = HashMap::new();
index_sets.insert("edges".to_string(), ragged(None));
assert!(resolve_ref("edges", Some(&["i".into()]), "k", false, &index_sets).is_err());
}
#[test]
fn derived_index_set_resolves_as_contracted_but_rejects_as_output() {
let mut index_sets = HashMap::new();
index_sets.insert(
"clip_ring".to_string(),
IndexSet {
kind: "derived".into(),
size: None,
members: None,
from_faq: Some("overlap_clip".into()),
member_factor: None,
of: None,
offsets: None,
values: None,
},
);
match resolve_ref("clip_ring", None, "v", false, &index_sets).unwrap() {
ResolvedRange::Derived { from_faq } => assert_eq!(from_faq, "overlap_clip"),
other => panic!("expected Derived, got {other:?}"),
}
let err = resolve_ref("clip_ring", None, "v", true, &index_sets).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("derived output index"),
"error should reject a derived output index: {msg}"
);
}
#[test]
fn derived_set_with_a_value_invention_extent_is_dense() {
let mut index_sets = HashMap::new();
index_sets.insert(
"emis_src_cells".to_string(),
IndexSet {
kind: "derived".into(),
size: None,
members: None,
from_faq: Some("emis_src_cells_faq".into()),
member_factor: None,
of: None,
offsets: None,
values: None,
},
);
let extents: HashMap<String, i64> = HashMap::from([("emis_src_cells_faq".to_string(), 4)]);
for is_output in [false, true] {
let r = resolve_index_set_ref(
"emis_src_cells",
None,
"s",
is_output,
&index_sets,
&extents,
)
.unwrap_or_else(|e| panic!("is_output={is_output} should resolve: {e:?}"));
assert_eq!(static_bounds(r), [1, 4]);
}
let other: HashMap<String, i64> = HashMap::from([("some_other_faq".to_string(), 7)]);
match resolve_index_set_ref("emis_src_cells", None, "s", false, &index_sets, &other)
.unwrap()
{
ResolvedRange::Derived { from_faq } => assert_eq!(from_faq, "emis_src_cells_faq"),
other => panic!("expected the deferred Derived bound, got {other:?}"),
}
}
#[test]
fn derived_index_set_without_from_faq_is_rejected() {
let mut index_sets = HashMap::new();
index_sets.insert(
"bad_set".to_string(),
IndexSet {
kind: "derived".into(),
size: None,
members: None,
from_faq: None,
member_factor: None,
of: None,
offsets: None,
values: None,
},
);
let err = resolve_ref("bad_set", None, "e", false, &index_sets).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("from_faq"),
"error should mention the missing from_faq: {msg}"
);
}
}