Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions src/simlin-engine/src/ltm/polarity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,38 @@ pub(super) fn analyze_expr_polarity_with_context(
// the production case `SUM(x[*])` that argument lowers to
// `Subscript(x, [Wildcard], _, _)`, not `Var(x, ...)`. Mirror the Var
// handler so the identifier comparison succeeds and the reducer's
// monotonicity guarantee carries through. The subscript indices don't
// affect the polarity contract for the reducer arms because each
// reducer is monotone in every input element. For non-reducer
// contexts (e.g. `x[1] + y`), this still returns the correct
// Positive/Negative for any reference to from_var.
Expr2::Subscript(ident, _, _, _) => {
// monotonicity guarantee carries through.
//
// When the array name matches `from_var`, the indices still need
// inspection: if any index expression also references `from_var`
// (e.g. `arr[INT(arr[i])]` or `arr[arr]`), the relationship is
// non-monotone -- shifting `from_var` moves both the lookup target
// and the index in lockstep -- and we must return Unknown. The
// dominant cases (literal, wildcard, range, expressions over OTHER
// variables) leave indices independent of `from_var`, and the
// reducer's monotonicity guarantee carries through unchanged.
//
// When the array name does NOT match `from_var`, contribute Unknown:
// we can't classify references that thread through another array
// here. Combining operators above (Add/Sub/Mul/Div, Mean variadic)
// detect any `from_var` reference inside indices via their own
// `expr_references_var` checks.
Expr2::Subscript(ident, indices, _, _) => {
let normalized = normalize_module_ref(ident);
if &normalized == from_var || ident == from_var {
current_polarity
if indices.iter().any(|idx| match idx {
IndexExpr2::Expr(e) => expr_references_var(e, from_var),
IndexExpr2::Range(lo, hi, _) => {
expr_references_var(lo, from_var) || expr_references_var(hi, from_var)
}
IndexExpr2::Wildcard(_)
| IndexExpr2::StarRange(_, _)
| IndexExpr2::DimPosition(_, _) => false,
}) {
LinkPolarity::Unknown
} else {
current_polarity
}
} else {
LinkPolarity::Unknown
}
Expand Down
113 changes: 113 additions & 0 deletions src/simlin-engine/src/ltm/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,119 @@ fn test_analyze_expr_polarity_array_reducers_subscript_wildcard() {
}
}

/// The Subscript arm must distinguish between indices that are independent
/// of `from_var` (literal, wildcard, expressions over other variables) and
/// indices that themselves reference `from_var`. In the latter case the
/// relationship between `from_var` and the subscripted result is non-monotone:
/// changing `from_var` shifts both the lookup target AND the index, so no
/// single polarity describes the result. The dominant cases (`SUM(arr[*])`,
/// `arr[Region]`, indices over OTHER variables) keep their original behavior
/// of returning `current_polarity` because their indices don't reference
/// `from_var`.
#[test]
fn test_analyze_expr_polarity_subscript_self_indexing() {
use crate::ast::{Expr2, IndexExpr2, Loc};
use crate::builtins::BuiltinFn;
use LinkPolarity::{Positive, Unknown};

let arr = Ident::new("arr");
let other = Ident::new("other");
let i = Ident::new("i");

let var = |id: &Ident<Canonical>| Expr2::Var(id.clone(), None, Loc::default());
let lit = |n: f64| Expr2::Const(format!("{n}"), n, Loc::default());

// arr[*] -- wildcard index, no reference to arr in the index.
let arr_wildcard = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Wildcard(Loc::default())],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&arr_wildcard, &arr, Positive, None),
Positive,
"arr[*] preserves current_polarity",
);

// arr[3] -- literal index, no reference to arr in the index.
let arr_literal = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Expr(lit(3.0))],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&arr_literal, &arr, Positive, None),
Positive,
"arr[3] preserves current_polarity",
);

// arr[i] where i is a different variable -- index references some OTHER
// variable, but not from_var (= arr). Polarity contract still holds.
let arr_other_index = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Expr(var(&i))],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&arr_other_index, &arr, Positive, None),
Positive,
"arr[i] (i != from_var) preserves current_polarity",
);

// arr[arr] -- index trivially references arr. Result is non-monotone
// because shifting arr shifts both the lookup target and the index.
let arr_self_var = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Expr(var(&arr))],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&arr_self_var, &arr, Positive, None),
Unknown,
"arr[arr] is non-monotone",
);

// arr[INT(arr[i])] -- the canonical self-indexing case. Index references
// arr through a nested subscript; relationship is non-monotone.
let inner = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Expr(var(&i))],
None,
Loc::default(),
);
let int_inner = Expr2::App(BuiltinFn::Int(Box::new(inner)), None, Loc::default());
let arr_self_nested = Expr2::Subscript(
arr.clone(),
vec![IndexExpr2::Expr(int_inner)],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&arr_self_nested, &arr, Positive, None),
Unknown,
"arr[INT(arr[i])] is non-monotone",
);

// other[*] where from_var is arr -- subscripted array is not from_var.
// Existing behavior: contributes Unknown because the arm conservatively
// can't classify references through other arrays.
let other_wildcard = Expr2::Subscript(
other.clone(),
vec![IndexExpr2::Wildcard(Loc::default())],
None,
Loc::default(),
);
assert_eq!(
analyze_expr_polarity_with_context(&other_wildcard, &arr, Positive, None),
Unknown,
"other[*] (other != from_var) returns Unknown",
);
}

#[test]
fn test_graphical_function_polarity() {
use crate::variable::Table;
Expand Down
Loading