use super::*;
use crate::broad_phase::{OverlapIndex, Side};
use crate::simulate_array::eval::{EqSides, narrow_reach, reach_of};
use crate::types::JoinClause;
use std::rc::Rc;
pub(in crate::simulate_array::tape) struct TupleFrame {
id: u32,
names: Vec<String>,
cols: Vec<Vec<f64>>,
slots: Vec<Option<SlotId>>,
len: usize,
}
enum DimBound {
Static(i64, i64),
Ragged {
shape: DimU,
vals: Rc<Vec<f64>>,
parents: Vec<usize>,
},
}
struct EqCand<'a> {
g: &'a crate::join::OnGate,
clause_ix: usize,
matches: usize,
n_l: usize,
n_r: usize,
sides: Option<EqSides>,
}
struct BuiltGate {
sym_src: String,
sym_tgt: String,
index: OverlapIndex,
n_src: usize,
n_tgt: usize,
clause_ix: usize,
in_filter: bool,
}
struct TupleList {
rows: Vec<u32>,
cols: Vec<Vec<f64>>,
}
impl TupleList {
fn len(&self) -> usize {
*self.rows.last().expect("rows has n_out + 1 entries") as usize
}
}
fn float_key_column(a: &ndarray::ArrayD<f64>) -> Option<(Vec<i64>, Vec<crate::relational::Key>)> {
if a.ndim() != 1 {
return None;
}
let keys = a
.iter()
.map(|&v| crate::relational::Key::Int((if v == 0.0 { 0.0 } else { v }).to_bits() as i64))
.collect();
Some(((1..=a.len() as i64).collect(), keys))
}
enum Src<'a> {
Range(i64, i64),
List(&'a [i64]),
}
#[derive(Clone, Copy)]
struct TupleSpec<'a> {
idx_names: &'a [String],
ranges: &'a [(i64, i64)],
body: &'a Expr,
contract_names: &'a [String],
bounds: &'a [DimBound],
gates: &'a [BuiltGate],
reduce: ReduceKind,
filter: Option<&'a Expr>,
init: f64,
}
pub(in crate::simulate_array) const DEFAULT_TUPLE_LIST_BUDGET: usize = 1 << 20;
thread_local! {
static TUPLE_LIST_BUDGET: std::cell::Cell<Option<usize>> =
const { std::cell::Cell::new(None) };
}
pub(in crate::simulate_array) fn tuple_list_budget() -> usize {
TUPLE_LIST_BUDGET.with(|c| match c.get() {
Some(v) => v,
None => {
static ENV: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
let v = *ENV.get_or_init(|| {
std::env::var("ESS_TUPLE_LIST_BUDGET")
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(DEFAULT_TUPLE_LIST_BUDGET)
});
c.set(Some(v));
v
}
})
}
#[cfg(test)]
pub(in crate::simulate_array) fn set_tuple_list_budget(tuples: usize) -> usize {
let prev = tuple_list_budget();
TUPLE_LIST_BUDGET.with(|c| c.set(Some(tuples)));
prev
}
impl TapeBuilder<'_> {
#[allow(clippy::too_many_arguments)]
pub(super) fn lower_tuple_contraction(
&mut self,
idx_names: &[String],
ranges: &[(i64, i64)],
body: &Expr,
contract_names: &[String],
contract_dims: &[ContractDim],
reduce: ReduceKind,
filter: Option<&Expr>,
join: Option<&[JoinClause]>,
) -> LResult<LV> {
let combine_op = if ranges.is_empty() {
scalar_combine_op(reduce)
} else {
self.array_combine_op(reduce, "contracted")?
};
if contract_names.is_empty() {
return self.lower_pointwise_gated(idx_names, ranges, body, reduce, filter, join);
}
let lo: DimI = ranges.iter().map(|(l, _)| *l).collect();
let shape: DimU = ranges
.iter()
.map(|(l, h)| (h - l + 1).max(0) as usize)
.collect();
if shape.contains(&0) {
bail_tape!("faq: empty output box");
}
let bounds = self.tuple_dim_bounds(idx_names, contract_dims)?;
let gates = match join {
Some(j) => self.build_join_gates(j)?,
None => Vec::new(),
};
let spec = TupleSpec {
idx_names,
ranges,
body,
contract_names,
bounds: &bounds,
gates: &gates,
reduce,
filter,
init: reduce.identity(),
};
let budget = tuple_list_budget() as u64;
let room = budget
.saturating_sub(self.tuples_held)
.min(MAX_PROMOTED_ELEMS as u64) as usize;
let list = match enumerate_tuples(&spec, room)? {
Some(list) => list,
None => {
if let Some(v) = self.fold_tuple_contraction(&spec, &lo, &shape, combine_op)? {
return Ok(v);
}
enumerate_tuples(&spec, MAX_PROMOTED_ELEMS)?.ok_or_else(|| Bail {
reason: format!(
"contracted: tuple list too long (over {MAX_PROMOTED_ELEMS} tuples)"
),
})?
}
};
self.tuples_held = self.tuples_held.saturating_add(list.len() as u64);
self.lower_tuple_list(&spec, list, &lo, &shape, combine_op)
}
fn lower_tuple_list(
&mut self,
spec: &TupleSpec<'_>,
list: TupleList,
lo: &DimI,
shape: &DimU,
combine_op: BinCode,
) -> LResult<LV> {
let &TupleSpec {
idx_names,
ranges,
body,
contract_names,
filter,
init,
..
} = spec;
let (lo, shape) = (lo.clone(), shape.clone());
let identity = init;
let m = list.len();
let rank0 = ranges.is_empty();
if m == 0 {
return Ok(if rank0 {
LV::Lit(identity)
} else {
self.emit_fill(&LV::Lit(identity), &shape, &lo, Cadence::Const)
});
}
let mut names: Vec<String> = idx_names.to_vec();
names.extend(contract_names.iter().cloned());
let id = self.next_tuple;
self.next_tuple += 1;
self.tuple_frames.push(TupleFrame {
id,
names,
cols: list.cols,
slots: vec![None; idx_names.len() + contract_names.len()],
len: m,
});
self.push_scope();
let bx = LBox {
syms: &[],
lo: DimI::from_elem(1, 1),
shape: DimU::from_elem(m, 1),
cnames: &[],
cvals: SmallVec::new(),
tuple: id,
visit: SmallVec::new(),
};
let lowered = (|| -> LResult<(Option<LV>, Option<LV>)> {
let mask = match filter {
None => None,
Some(f) => Some(self.lower_expr(f, &bx)?),
};
let lazy = match &mask {
None => false,
Some(LV::Lit(c)) if *c == 0.0 => return Ok((None, mask)),
Some(LV::Lit(_)) => false,
Some(_) => true,
};
self.lazy_depth += u32::from(lazy);
let term = self.lower_expr(body, &bx);
self.lazy_depth -= u32::from(lazy);
Ok((Some(term?), mask))
})();
self.pop_scope();
self.tuple_frames.pop();
let (term, mask) = lowered?;
let Some(term) = term else {
return Ok(if rank0 {
LV::Lit(identity)
} else {
self.emit_fill(&LV::Lit(identity), &shape, &lo, Cadence::Const)
});
};
let tuple_box = |s: &Self, v: &LV| -> LResult<Option<SlotId>> {
match (v, s.lv_box(v)) {
(_, None) => Ok(None),
(LV::Arr(slot), Some((sh, o))) if sh.as_slice() == [m] && o.as_slice() == [1] => {
Ok(Some(*slot))
}
_ => bail_tape!("contracted: a tuple-list term is not over the tuple list"),
}
};
let src = match tuple_box(self, &term)? {
Some(slot) => slot,
None => match self.emit_fill(&term, &[m], &[1], Cadence::Const) {
LV::Arr(slot) => slot,
_ => unreachable!("a fill defines an array slot"),
},
};
let mask = match mask {
None | Some(LV::Lit(_)) => None,
Some(v) => match tuple_box(self, &v)? {
Some(slot) => Some(slot),
None => match self.emit_fill(&v, &[m], &[1], Cadence::Const) {
LV::Arr(slot) => Some(slot),
_ => unreachable!("a fill defines an array slot"),
},
},
};
let table = tape_index(self.seg_tables.len(), "segmented reductions")?;
self.seg_tables.push(SegTable { rows: list.rows });
let mut want = self.slots[src as usize].cadence;
if let Some(mk) = mask {
want = want.max(self.slots[mk as usize].cadence);
}
let sec = self.placement(want);
let out = if rank0 {
self.new_slot(&[], &[], true, sec)
} else {
self.new_slot(&shape, &lo, false, sec)
};
self.emit(
Instr::SegReduce {
op: combine_op,
init: identity,
src,
mask,
table,
out,
},
sec,
);
Ok(if rank0 { LV::Scalar(out) } else { LV::Arr(out) })
}
fn fold_tuple_contraction(
&mut self,
spec: &TupleSpec<'_>,
lo: &DimI,
shape: &DimU,
combine_op: BinCode,
) -> LResult<Option<LV>> {
let Some(inputs) = self.tuple_fold_inputs(spec) else {
return Ok(None);
};
let budget = (tuple_list_budget() as u64).clamp(1, MAX_PROMOTED_ELEMS as u64);
if spec.ranges.is_empty() {
let acc = self.fold_cell_in_windows(spec, &inputs, combine_op, budget)?;
return Ok(acc.map(LV::Lit));
}
let counts = count_tuples(spec)?;
let n_cells: usize = shape.iter().product();
let mut strides = vec![1usize; shape.len()];
for d in (0..shape.len().saturating_sub(1)).rev() {
strides[d] = strides[d + 1] * shape[d + 1];
}
let mut values = vec![spec.reduce.identity(); n_cells];
for sub in plan_chunks(spec.ranges, shape, &strides, &counts, budget) {
let sub_shape: Vec<usize> = sub.iter().map(|(l, h)| (h - l + 1) as usize).collect();
let base: usize = sub
.iter()
.zip(spec.ranges)
.zip(&strides)
.map(|(((l, _), (l0, _)), st)| (l - l0) as usize * st)
.sum();
let mut cells: Vec<usize> = vec![base];
for (&n, &st) in sub_shape.iter().zip(&strides) {
let mut next = Vec::with_capacity(cells.len() * n);
for &c in &cells {
next.extend((0..n).map(|k| c + k * st));
}
cells = next;
}
if cells.iter().all(|&c| counts[c] == 0) {
continue; }
let chunk = TupleSpec {
ranges: &sub,
..*spec
};
if let [c] = cells[..]
&& counts[c] > budget
{
let Some(acc) = self.fold_cell_in_windows(&chunk, &inputs, combine_op, budget)?
else {
return Ok(None);
};
values[c] = acc;
continue;
}
let Some(list) = enumerate_tuples(&chunk, MAX_PROMOTED_ELEMS)? else {
return Ok(None);
};
let Some(v) = self.eval_tuple_chunk(&chunk, list, &inputs, combine_op) else {
return Ok(None);
};
if v.len() != cells.len() {
return Ok(None);
}
for (&c, x) in cells.iter().zip(v) {
values[c] = x;
}
}
let data = tape_index(self.const_data.len(), "array constants")?;
self.const_data.push(ConstArrayData {
shape: shape.clone(),
values,
});
let sec = self.placement(Cadence::Const);
let out = self.new_slot(shape, lo, false, sec);
self.emit(Instr::ConstArray { data, out }, sec);
Ok(Some(LV::Arr(out)))
}
fn fold_cell_in_windows(
&self,
cell: &TupleSpec<'_>,
inputs: &[(String, ndarray::ArrayD<f64>)],
combine_op: BinCode,
budget: u64,
) -> LResult<Option<f64>> {
let nr = cell.idx_names.len();
let width = nr + cell.contract_names.len();
let mut acc = Some(cell.reduce.identity());
let mut cols: Vec<Vec<f64>> = vec![Vec::new(); width];
let mut held = 0u64;
let flush = |cols: &mut Vec<Vec<f64>>, held: &mut u64, acc: &mut Option<f64>| {
let window = TupleList {
rows: vec![0, *held as u32],
cols: std::mem::replace(cols, vec![Vec::new(); width]),
};
*held = 0;
*acc = acc.and_then(|init| {
let chunk = TupleSpec { init, ..*cell };
match self
.eval_tuple_chunk(&chunk, window, inputs, combine_op)
.as_deref()
{
Some(&[x]) => Some(x),
_ => None,
}
});
};
visit_tuples(
cell,
&mut |c: &[i64], t: &[i64]| {
for (k, &v) in c.iter().chain(t).enumerate() {
cols[k].push(v as f64);
}
held += 1;
if held >= budget {
flush(&mut cols, &mut held, &mut acc);
}
acc.is_some()
},
&mut || Ok(()),
)?;
if held > 0 {
flush(&mut cols, &mut held, &mut acc);
}
Ok(acc)
}
fn tuple_fold_inputs(
&self,
spec: &TupleSpec<'_>,
) -> Option<Vec<(String, ndarray::ArrayD<f64>)>> {
let mut reads: HashSet<String> = HashSet::new();
standalone::collect_variable_names(spec.body, &mut reads);
if let Some(f) = spec.filter {
standalone::collect_variable_names(f, &mut reads);
}
let mut names: Vec<&String> = reads
.iter()
.filter(|n| !spec.idx_names.contains(n) && !spec.contract_names.contains(n))
.filter(|n| self.obs_defined.contains_key(n.as_str()))
.collect();
names.sort();
self.known_inputs(names)
}
fn eval_tuple_chunk(
&self,
chunk: &TupleSpec<'_>,
list: TupleList,
inputs: &[(String, ndarray::ArrayD<f64>)],
combine_op: BinCode,
) -> Option<Vec<f64>> {
let name = "\u{0}tuple-list chunk";
standalone::with_standalone_builder(
name,
&[],
self.const_arrays,
self.f32_document,
self.array_bool_reductions,
|mut b| {
for (input, value) in inputs {
let lv = match value.ndim() {
0 => LV::Lit(value[ndarray::IxDyn(&[])]),
_ => b.standalone_array_input(value).ok()?,
};
b.obs_defined.insert(input.clone(), ObsVal::Taped(lv));
}
let lo: DimI = chunk.ranges.iter().map(|(l, _)| *l).collect();
let shape: DimU = chunk
.ranges
.iter()
.map(|(l, h)| (h - l + 1) as usize)
.collect();
let v = b
.lower_tuple_list(chunk, list, &lo, &shape, combine_op)
.ok()?;
if b.fault_notes != 0
|| b.fault_first.is_some()
|| b.lv_cadence(&v) != Cadence::Const
{
return None;
}
let v = match v {
LV::Lit(x) => return Some(vec![x; shape.iter().product()]),
v @ (LV::Arr(_) | LV::Scalar(_)) => v,
_ => return None,
};
b.obs_defined.insert(name.to_string(), ObsVal::Taped(v));
let rule = AlgebraicRule::Scalar {
var: name.to_string(),
body: std::sync::Arc::new(Expr::Number(0.0)),
declared_shape: (!shape.is_empty()).then(|| shape.to_vec()),
};
let prog = b.finish_standalone(&rule);
let a = standalone::run_standalone_export(prog, name, &[], &[]).ok()?;
Some(a.iter().copied().collect())
},
)
}
fn lower_pointwise_gated(
&mut self,
idx_names: &[String],
ranges: &[(i64, i64)],
body: &Expr,
reduce: ReduceKind,
filter: Option<&Expr>,
join: Option<&[JoinClause]>,
) -> LResult<LV> {
let identity = reduce.identity();
let gates = match join {
Some(j) => self.build_join_gates(j)?,
None => Vec::new(),
};
let lo: DimI = ranges.iter().map(|(l, _)| *l).collect();
let shape: DimU = ranges
.iter()
.map(|(l, h)| (h - l + 1).max(0) as usize)
.collect();
if shape.contains(&0) {
bail_tape!("faq: empty output box");
}
let keep = pointwise_keep(idx_names, ranges, &gates)?;
if keep.as_ref().is_some_and(|k| k.iter().all(|&k| k == 0.0)) {
return Ok(if ranges.is_empty() {
LV::Lit(identity)
} else {
self.emit_fill(&LV::Lit(identity), &shape, &lo, Cadence::Const)
});
}
let bx = LBox {
syms: idx_names,
lo: lo.clone(),
shape: shape.clone(),
cnames: &[],
cvals: SmallVec::new(),
tuple: 0,
visit: SmallVec::new(),
};
let Some(keep) = keep.filter(|k| !k.iter().all(|&k| k != 0.0)) else {
return self
.lower_filtered(body, filter, &bx, LV::Lit(identity))
.map(|t| t.unwrap_or(LV::Lit(identity)));
};
if ranges.is_empty() {
bail_tape!("aggregate: a rank-0 join gate");
}
let data = tape_index(self.const_data.len(), "array constants")?;
self.const_data.push(ConstArrayData {
shape: shape.clone(),
values: keep,
});
let sec = self.placement(Cadence::Const);
let mask_slot = self.new_slot(&shape, &lo, false, sec);
self.emit(
Instr::ConstArray {
data,
out: mask_slot,
},
sec,
);
self.known.insert(mask_slot, Known::Data(data));
self.lazy_depth += 1;
let term = self.lower_filtered(body, filter, &bx, LV::Lit(identity));
self.lazy_depth -= 1;
let term = term?.unwrap_or(LV::Lit(identity));
self.emit_select(LV::Arr(mask_slot), term, LV::Lit(identity))
}
fn tuple_dim_bounds(
&mut self,
idx_names: &[String],
contract_dims: &[ContractDim],
) -> LResult<Vec<DimBound>> {
let mut out = Vec::with_capacity(contract_dims.len());
for d in contract_dims {
out.push(match d {
ContractDim::Static(l, h) => DimBound::Static(*l, *h),
ContractDim::Ragged { offsets, of } => {
let parents = of
.iter()
.map(|p| {
idx_names.iter().position(|n| n == p).ok_or_else(|| Bail {
reason: format!(
"contracted: ragged bound's parent `{p}` is not an output \
index"
),
})
})
.collect::<LResult<Vec<usize>>>()?;
let (shape, vals) = self.known_named(offsets).ok_or_else(|| Bail {
reason: format!(
"contracted: ragged offsets factor `{offsets}` is not build-time data"
),
})?;
DimBound::Ragged {
shape,
vals,
parents,
}
}
ContractDim::Derived { from_faq } => match self.ring_extent(from_faq) {
Some(n) => DimBound::Static(1, n),
None => {
bail_tape!("contracted: derived contraction bound (from `{from_faq}`)")
}
},
});
}
Ok(out)
}
pub(super) fn known_named(&self, name: &str) -> Option<(DimU, Rc<Vec<f64>>)> {
match self.obs_defined.get(name)? {
ObsVal::Taped(LV::Lit(x)) => Some((DimU::new(), Rc::new(vec![*x]))),
ObsVal::Taped(LV::Arr(s)) => {
let shape = self.slots[*s as usize].shape.clone();
match self.known_values(*s) {
Some(v) => Some((shape, v)),
None => self.fold_const_observed(name),
}
}
_ => None,
}
}
fn not_foldable_note(&self, name: &str) -> &'static str {
if self
.observed_rules
.iter()
.any(|r| observed_rule_var(r) == name)
{
" (an observed whose value reads a parameter, a state, the time or a forcing \
input, directly or through another observed, so it is not constant at build \
time; CONFORMANCE_SPEC §5.5.8)"
} else {
""
}
}
pub(super) fn known_values(&self, s: SlotId) -> Option<Rc<Vec<f64>>> {
match self.known.get(&s)? {
Known::Data(d) => Some(Rc::new(self.const_data[*d as usize].values.clone())),
Known::Vals(v) => Some(Rc::clone(v)),
}
}
fn on_gate_sides(&self, g: &crate::join::OnGate) -> LResult<EqSides> {
let side = |cols: &[crate::join::KeyColumn], exact_ints: bool| -> LResult<_> {
let mut parts = Vec::with_capacity(cols.len());
for c in cols {
parts.push(match c {
crate::join::KeyColumn::Const { positions, values } => {
if !exact_ints {
return Ok(None);
}
const_key_column(positions, values)
}
crate::join::KeyColumn::Column(name) => {
let a = self
.known_named(name)
.and_then(|(shape, vals)| {
ndarray::ArrayD::from_shape_vec(
ndarray::IxDyn(&shape),
(*vals).clone(),
)
.ok()
})
.ok_or_else(|| Bail {
reason: format!(
"aggregate: join key column `{name}` is not build-time \
data{}",
self.not_foldable_note(name)
),
})?;
let col = if exact_ints {
data_key_column(&a)
} else {
float_key_column(&a)
};
match col {
Some(col) => col,
None if exact_ints => return Ok(None),
None => bail_tape!(
"aggregate: join key column `{name}` is not a 1-D column"
),
}
}
});
}
composite_side_keys(parts).map(Some).ok_or_else(|| Bail {
reason: "aggregate: a composite join key's columns disagree in length".to_string(),
})
};
let sides = match (side(&g.cols_l, true)?, side(&g.cols_r, true)?) {
(Some(l), Some(r)) => Some((l, r)),
_ => match (side(&g.cols_l, false)?, side(&g.cols_r, false)?) {
(Some(l), Some(r)) => Some((l, r)),
_ => None,
},
};
let Some(((pos_l, keys_l), (pos_r, keys_r))) = sides else {
bail_tape!(
"aggregate: a join clause pairs a literal key with a column that is not \
exact integers"
);
};
Ok((pos_l, keys_l, pos_r, keys_r))
}
fn build_join_gates(&mut self, join: &[JoinClause]) -> LResult<Vec<BuiltGate>> {
let mut gates = Vec::new();
let mut reach: HashMap<String, usize> = HashMap::new();
let mut candidates: Vec<EqCand<'_>> = Vec::new();
let mut cheapest: Option<(usize, EqSides)> = None;
for (clause_ix, clause) in join.iter().enumerate() {
if let Some(ov) = &clause.overlap {
let (Some(sym_src), Some(sym_tgt)) = (&ov.sym_src, &ov.sym_tgt) else {
continue;
};
let mut arrays: HashMap<String, ndarray::ArrayD<f64>> = HashMap::new();
for name in ov.src_env.iter().chain(ov.tgt_env.iter()) {
let Some((shape, vals)) = self.known_named(name) else {
bail_tape!(
"aggregate: overlap join gate factor `{name}` is not build-time data"
);
};
let a =
ndarray::ArrayD::from_shape_vec(ndarray::IxDyn(&shape), (*vals).clone())
.map_err(|e| Bail {
reason: format!(
"aggregate: overlap join gate factor `{name}`: {e}"
),
})?;
arrays.insert(name.clone(), a);
}
let env = |names: &[String]| {
crate::broad_phase::envelope_vectors(names, &arrays).map_err(|e| Bail {
reason: format!("aggregate: overlap join gate: {e}"),
})
};
let (src, tgt) = (env(&ov.src_env)?, env(&ov.tgt_env)?);
let pairs =
crate::broad_phase::broad_phase_candidates(&src, &tgt, ov.eps.unwrap_or(0.0));
let index = OverlapIndex::from_zero_based(&pairs);
narrow_reach(&mut reach, sym_src, index.len());
narrow_reach(&mut reach, sym_tgt, index.len());
gates.push(BuiltGate {
sym_src: sym_src.clone(),
sym_tgt: sym_tgt.clone(),
index,
n_src: src.len(),
n_tgt: tgt.len(),
clause_ix,
in_filter: false,
});
continue;
}
let Some(g) = &clause.on_gate else {
continue;
};
if g.cols_l.len() != g.cols_r.len() || g.cols_l.is_empty() {
continue;
}
let sides = self.on_gate_sides(g)?;
let matches = crate::relational::equijoin_match_count(&sides.1, &sides.3);
let (n_l, n_r) = (sides.0.len(), sides.2.len());
if cheapest.as_ref().is_none_or(|&(i, _)| {
let c: &EqCand<'_> = &candidates[i];
matches < c.matches
}) {
cheapest = Some((candidates.len(), sides));
}
candidates.push(EqCand {
g,
clause_ix,
matches,
n_l,
n_r,
sides: None,
});
}
if let Some((slot, sides)) = cheapest {
candidates[slot].sides = Some(sides);
}
candidates.sort_by_key(|c| (c.matches, c.clause_ix));
let ratio = crate::broad_phase::gate_plan_ratio();
let floor = crate::broad_phase::gate_plan_floor();
for c in candidates {
let space = (reach_of(&reach, &c.g.sym_l, c.n_l) as u128)
.saturating_mul(reach_of(&reach, &c.g.sym_r, c.n_r) as u128);
if !gates.is_empty()
&& c.matches >= floor
&& (c.matches as u128) > space.saturating_mul(ratio)
{
crate::broad_phase::bump_gate_plan_declines();
continue;
}
let sides = match c.sides {
Some(s) => s,
None => self.on_gate_sides(c.g)?,
};
let (index, n_src, n_tgt) = index_from_sides(sides);
narrow_reach(&mut reach, &c.g.sym_l, index.len());
narrow_reach(&mut reach, &c.g.sym_r, index.len());
gates.push(BuiltGate {
sym_src: c.g.sym_l.clone(),
sym_tgt: c.g.sym_r.clone(),
index,
n_src,
n_tgt,
clause_ix: c.clause_ix,
in_filter: true,
});
}
gates.sort_by(|a, b| {
let (a1, s1) = (a.index.len() as i128, (a.n_src * a.n_tgt) as i128);
let (a2, s2) = (b.index.len() as i128, (b.n_src * b.n_tgt) as i128);
(a1 * s2)
.cmp(&(a2 * s1))
.then_with(|| a.clause_ix.cmp(&b.clause_ix))
});
Ok(gates)
}
pub(super) fn tuple_column(&mut self, id: u32, name: &str) -> LResult<Option<LV>> {
let Some(fi) = self.tuple_frames.iter().rposition(|f| f.id == id) else {
return Ok(None);
};
let Some(ci) = self.tuple_frames[fi].names.iter().position(|n| n == name) else {
return Ok(None);
};
if let Some(s) = self.tuple_frames[fi].slots[ci] {
return Ok(Some(LV::Arr(s)));
}
let frame = &mut self.tuple_frames[fi];
let len = frame.len;
let values = std::mem::take(&mut frame.cols[ci]);
let data = tape_index(self.const_data.len(), "array constants")?;
self.const_data.push(ConstArrayData {
shape: DimU::from_elem(len, 1),
values,
});
let sec = self.placement(Cadence::Const);
let out = self.new_slot(&[len], &[1], false, sec);
let branches = std::mem::take(&mut self.branch_bufs);
self.emit(Instr::ConstArray { data, out }, sec);
self.branch_bufs = branches;
self.tuple_frames[fi].slots[ci] = Some(out);
Ok(Some(LV::Arr(out)))
}
pub(super) fn tuple_known_values(
&mut self,
e: &Expr,
bx: &LBox,
) -> LResult<Option<Rc<Vec<f64>>>> {
let m = bx.shape[0];
if let Expr::Operator(n) = e
&& matches!(n.op.as_str(), "+" | "-" | "*" | "/")
&& !n.args.is_empty()
&& n.expr.is_none()
{
let mut parts = Vec::with_capacity(n.args.len());
for a in &n.args {
match self.tuple_known_values(a, bx)? {
Some(v) => parts.push(v),
None => return Ok(None),
}
}
let mut acc: Vec<f64> = if n.op == "-" && parts.len() == 1 {
parts[0].iter().map(|&x| -x).collect()
} else {
(*parts[0]).clone()
};
if n.op == "-" && parts.len() > 2 || n.op == "/" && parts.len() != 2 {
return Ok(None);
}
for p in &parts[1..] {
for (a, &b) in acc.iter_mut().zip(p.iter()) {
*a = apply_binary(&n.op, *a, b);
}
}
return Ok(Some(Rc::new(acc)));
}
Ok(match self.lower_expr(e, bx)? {
LV::Lit(x) => Some(Rc::new(vec![x; m])),
LV::Arr(s) => self.known_values(s).filter(|v| v.len() == m),
_ => None,
})
}
pub(super) fn lower_tuple_index(
&mut self,
node: &Arc<ExpressionNode>,
bx: &LBox,
) -> LResult<LV> {
let Some((base_e, subs)) = node.args.split_first() else {
bail_tape!("index: no arguments");
};
if let Expr::Variable(name) = base_e
&& self.tuple_column(bx.tuple, name)?.is_some()
{
bail_tape!("index: the base `{name}` is a loop symbol");
}
let const_name: Option<&str> = match base_e {
Expr::Variable(name) if self.const_arrays.is_const(name) => Some(name.as_str()),
b if ConstArrayScope::is_inline_const(b) => Some(INLINE_CONST_NAME),
_ => None,
};
let base = self.lower_wholesale(base_e)?;
let Some((src_shape, src_origin)) = self.lv_box(&base) else {
return if subs.is_empty() {
Ok(base)
} else {
bail_tape!(
"index: base is a scalar but {} index args given",
subs.len()
)
};
};
if subs.len() != src_shape.len() {
bail_tape!(
"index: arg count != source rank ({} args vs rank {})",
subs.len(),
src_shape.len()
);
}
if src_origin.iter().any(|&o| o != 1) {
bail_tape!("index: a tuple-list gather from an array whose origin is not 1");
}
let m = bx.shape[0];
let mut sub_vals: Vec<Rc<Vec<f64>>> = Vec::with_capacity(subs.len());
for e in subs {
match self.tuple_known_values(e, bx)? {
Some(vals) => sub_vals.push(vals),
None => bail_tape!("index: a subscript that is not build-time data over the list"),
}
}
let n_src: usize = src_shape.iter().product();
if n_src >= GATHER_GHOST as usize {
bail_tape!("index: gather source too large for a position table");
}
let mut strides: Vec<usize> = vec![1; src_shape.len()];
for d in (0..src_shape.len().saturating_sub(1)).rev() {
strides[d] = strides[d + 1] * src_shape[d + 1];
}
let mut pos: Vec<u32> = Vec::with_capacity(m);
for k in 0..m {
let mut flat = 0usize;
let mut ghost = false;
for (d, vals) in sub_vals.iter().enumerate() {
let raw = vals[k].round() as i64;
let n = src_shape[d] as i64;
let mut i1 = raw;
if raw < 1 || raw > n {
match const_name {
None => ghost = true,
Some(name) => match (n >= 1).then(|| self.const_arrays.boundary(name, d)) {
Some(BoundaryKind::Periodic) => i1 = (raw - 1).rem_euclid(n) + 1,
Some(BoundaryKind::Clamp) => i1 = raw.clamp(1, n),
Some(BoundaryKind::Error) | None => bail_tape!(
"index: const-array gather out of range (§5.5.5) in a \
tuple-list body"
),
},
}
}
flat += ((i1 - 1).max(0) as usize) * strides[d];
}
pos.push(if ghost { GATHER_GHOST } else { flat as u32 });
}
let vals = match &base {
LV::Arr(s) => self.known_values(*s).map(|b| {
pos.iter()
.map(|&p| {
if p == GATHER_GHOST {
0.0
} else {
b[p as usize]
}
})
.collect::<Vec<f64>>()
}),
_ => None,
};
let table = tape_index(self.gather_tables.len(), "gather tables")?;
self.gather_tables.push(GatherTable { src_shape, pos });
let sec = self.placement(self.lv_cadence(&base));
let out = self.new_slot(&[m], &[1], false, sec);
let instr = Instr::TableGather {
src: self.src_of(&base),
table,
out,
};
self.emit(instr, sec);
if let Some(v) = vals {
self.known.insert(out, Known::Vals(Rc::new(v)));
}
Ok(LV::Arr(out))
}
}
fn enumerate_tuples(spec: &TupleSpec<'_>, cap: usize) -> LResult<Option<TupleList>> {
let width = spec.idx_names.len() + spec.contract_names.len();
let mut cols: Vec<Vec<f64>> = vec![Vec::new(); width];
let mut rows: Vec<u32> = vec![0];
let len = std::cell::Cell::new(0usize);
let whole = visit_tuples(
spec,
&mut |cell: &[i64], t: &[i64]| {
if len.get() == cap {
return false;
}
for (k, &v) in cell.iter().chain(t).enumerate() {
cols[k].push(v as f64);
}
len.set(len.get() + 1);
true
},
&mut || {
rows.push(len.get() as u32);
Ok(())
},
)?;
Ok(whole.then_some(TupleList { rows, cols }))
}
fn count_tuples(spec: &TupleSpec<'_>) -> LResult<Vec<u64>> {
let mut counts: Vec<u64> = Vec::new();
let here = std::cell::Cell::new(0u64);
visit_tuples(
spec,
&mut |_: &[i64], _: &[i64]| {
here.set(here.get() + 1);
true
},
&mut || {
counts.push(here.replace(0));
Ok(())
},
)?;
Ok(counts)
}
fn visit_tuples(
spec: &TupleSpec<'_>,
tuple: &mut dyn FnMut(&[i64], &[i64]) -> bool,
cell_done: &mut dyn FnMut() -> LResult<()>,
) -> LResult<bool> {
let &TupleSpec {
idx_names,
ranges,
contract_names,
bounds,
gates,
..
} = spec;
let nr = idx_names.len();
let nc = contract_names.len();
let placed: Vec<(GateAxis, GateAxis)> = gates
.iter()
.map(|g| {
(
gate_axis(&g.sym_src, idx_names, contract_names),
gate_axis(&g.sym_tgt, idx_names, contract_names),
)
})
.collect();
let out_pos = |name: &str| idx_names.iter().position(|n| n == name);
let mut cell: Vec<i64> = ranges.iter().map(|(l, _)| *l).collect();
let n_cells: usize = ranges
.iter()
.map(|(l, h)| (h - l + 1).max(0) as usize)
.product::<usize>()
.max(1);
let mut cur: Vec<i64> = vec![0; nc];
for _ in 0..n_cells {
let cell_ranges: Vec<(i64, i64)> = bounds
.iter()
.map(|b| match b {
DimBound::Static(l, h) => (*l, *h),
DimBound::Ragged {
shape,
vals,
parents,
} => (1, ragged_bound(shape, vals, parents, &cell)),
})
.collect();
let mut restrict: Vec<Option<Vec<i64>>> = vec![None; nc];
let mut pair: Option<(&BuiltGate, usize, usize, bool)> = None;
let mut reject = false;
for (g, &(ps, pt)) in gates.iter().zip(&placed) {
match (ps, pt) {
(GateAxis::Output, GateAxis::Output) => {
let (Some(a), Some(b)) = (out_pos(&g.sym_src), out_pos(&g.sym_tgt)) else {
continue;
};
if !g.index.contains(cell[a], cell[b]) {
reject = true;
}
}
(GateAxis::Contracted(d), GateAxis::Output) => {
let Some(b) = out_pos(&g.sym_tgt) else {
continue;
};
let (lo, hi) = cell_ranges[d];
let parts = g.index.partners_in(Side::Tgt, cell[b], lo, hi);
if !intersect(&mut restrict[d], parts) {
reject = true;
}
}
(GateAxis::Output, GateAxis::Contracted(d)) => {
let Some(a) = out_pos(&g.sym_src) else {
continue;
};
let (lo, hi) = cell_ranges[d];
let parts = g.index.partners_in(Side::Src, cell[a], lo, hi);
if !intersect(&mut restrict[d], parts) {
reject = true;
}
}
(GateAxis::Contracted(a), GateAxis::Contracted(b)) if a != b && pair.is_none() => {
pair = Some(if a < b {
(g, a, b, true)
} else {
(g, b, a, false)
});
}
_ => {}
}
if reject {
break;
}
}
if !reject && nc > 0 {
let srcs: Vec<Src> = (0..nc)
.map(|d| match &restrict[d] {
Some(v) => Src::List(v),
None => Src::Range(cell_ranges[d].0, cell_ranges[d].1),
})
.collect();
let mut push = |t: &[i64]| tuple(&cell, t);
if !walk(&srcs, pair, 0, &mut cur, &mut push) {
return Ok(false);
}
} else if !reject && !tuple(&cell, &[]) {
return Ok(false);
}
cell_done()?;
let mut d = nr;
while d > 0 {
d -= 1;
cell[d] += 1;
if cell[d] <= ranges[d].1 {
break;
}
cell[d] = ranges[d].0;
}
}
Ok(true)
}
fn plan_chunks(
ranges: &[(i64, i64)],
shape: &[usize],
strides: &[usize],
counts: &[u64],
budget: u64,
) -> Vec<Vec<(i64, i64)>> {
fn split(
d: usize,
base: usize,
sub: &mut Vec<(i64, i64)>,
g: (&[(i64, i64)], &[usize], &[usize], &[u64], u64),
out: &mut Vec<Vec<(i64, i64)>>,
) {
let (ranges, shape, strides, counts, budget) = g;
let run = strides[d];
let l0 = ranges[d].0;
let flush =
|sub: &mut Vec<(i64, i64)>, out: &mut Vec<Vec<(i64, i64)>>, a: usize, b: usize| {
sub[d] = (l0 + a as i64, l0 + b as i64 - 1);
out.push(sub.clone());
};
let (mut start, mut acc) = (0usize, 0u64);
for v in 0..shape[d] {
let c: u64 = counts[base + v * run..base + (v + 1) * run].iter().sum();
if c > budget && d + 1 < shape.len() {
if start < v {
flush(sub, out, start, v);
}
sub[d] = (l0 + v as i64, l0 + v as i64);
split(d + 1, base + v * run, sub, g, out);
sub[d + 1..].copy_from_slice(&ranges[d + 1..]);
(start, acc) = (v + 1, 0);
continue;
}
if acc + c > budget && start < v {
flush(sub, out, start, v);
(start, acc) = (v, 0);
}
acc += c;
}
if start < shape[d] {
flush(sub, out, start, shape[d]);
}
}
let mut out = Vec::new();
let mut sub = ranges.to_vec();
split(
0,
0,
&mut sub,
(ranges, shape, strides, counts, budget),
&mut out,
);
out
}
fn pointwise_keep(
idx_names: &[String],
ranges: &[(i64, i64)],
gates: &[BuiltGate],
) -> LResult<Option<Vec<f64>>> {
let shape: Vec<usize> = ranges
.iter()
.map(|(l, h)| (h - l + 1).max(0) as usize)
.collect();
let n: usize = shape.iter().product::<usize>().max(1);
let mut strides = vec![1usize; shape.len()];
for d in (0..shape.len().saturating_sub(1)).rev() {
strides[d] = strides[d + 1] * shape[d + 1];
}
let mut keep: Option<Vec<f64>> = None;
for g in gates.iter().filter(|g| !g.in_filter) {
let pos = |s: &str| idx_names.iter().position(|x| x == s);
let (Some(a), Some(b)) = (pos(&g.sym_src), pos(&g.sym_tgt)) else {
continue;
};
let mut offsets = vec![0usize];
for d in 0..shape.len() {
if d == a || d == b {
continue;
}
let (len, stride) = (shape[d], strides[d]);
offsets = offsets
.iter()
.flat_map(|&o| (0..len).map(move |k| o + k * stride))
.collect();
}
let in_range = |d: usize, v: i64| v >= ranges[d].0 && v <= ranges[d].1;
let mut admitted = vec![0.0f64; n];
for (s, t) in g.index.pairs() {
if !in_range(a, s) || !in_range(b, t) || (a == b && s != t) {
continue;
}
let base = (s - ranges[a].0) as usize * strides[a]
+ if a == b {
0
} else {
(t - ranges[b].0) as usize * strides[b]
};
for &o in &offsets {
admitted[base + o] = match &keep {
Some(k) => k[base + o],
None => 1.0,
};
}
}
keep = Some(admitted);
}
if let Some(k) = &keep
&& n > MAX_PROMOTED_ELEMS
{
let m = k.iter().filter(|&&k| k != 0.0).count();
if m > MAX_PROMOTED_ELEMS {
bail_tape!("contracted: tuple list too long ({m} tuples)");
}
}
Ok(keep)
}
fn walk(
srcs: &[Src],
pair: Option<(&BuiltGate, usize, usize, bool)>,
d: usize,
cur: &mut Vec<i64>,
push: &mut impl FnMut(&[i64]) -> bool,
) -> bool {
if d == srcs.len() {
return push(cur);
}
if let Some((g, p, q, bound_is_src)) = pair
&& d == q
{
let side = if bound_is_src { Side::Src } else { Side::Tgt };
let bound = cur[p];
match &srcs[q] {
Src::Range(lo, hi) => {
for &v in g.index.partners_in(side, bound, *lo, *hi) {
cur[d] = v;
if !walk(srcs, pair, d + 1, cur, push) {
return false;
}
}
}
Src::List(vals) => {
let parts = g.index.partners(side, bound);
let (mut i, mut j) = (0usize, 0usize);
while i < vals.len() && j < parts.len() {
match vals[i].cmp(&parts[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
cur[d] = vals[i];
if !walk(srcs, pair, d + 1, cur, push) {
return false;
}
i += 1;
j += 1;
}
}
}
}
}
return true;
}
match &srcs[d] {
Src::Range(lo, hi) => {
for v in *lo..=*hi {
cur[d] = v;
if !walk(srcs, pair, d + 1, cur, push) {
return false;
}
}
}
Src::List(vals) => {
for &v in *vals {
cur[d] = v;
if !walk(srcs, pair, d + 1, cur, push) {
return false;
}
}
}
}
true
}
fn intersect(slot: &mut Option<Vec<i64>>, parts: &[i64]) -> bool {
let next = match slot.take() {
None => parts.to_vec(),
Some(cur) => {
let mut out = Vec::with_capacity(cur.len().min(parts.len()));
let (mut i, mut j) = (0usize, 0usize);
while i < cur.len() && j < parts.len() {
match cur[i].cmp(&parts[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
out.push(cur[i]);
i += 1;
j += 1;
}
}
}
out
}
};
let ok = !next.is_empty();
*slot = Some(next);
ok
}
fn ragged_bound(shape: &[usize], vals: &[f64], parents: &[usize], cell: &[i64]) -> i64 {
if shape.is_empty() {
return vals[0].round() as i64;
}
if parents.len() != shape.len() {
return 0;
}
let mut flat = 0usize;
for (d, &p) in parents.iter().enumerate() {
let v = cell[p];
if v < 1 || v as usize > shape[d] {
return 0;
}
flat = flat * shape[d] + (v - 1) as usize;
}
vals[flat].round() as i64
}
#[cfg(test)]
mod tests {
use super::*;
fn gate(src: &str, tgt: &str, pairs: &[(i64, i64)], clause_ix: usize) -> BuiltGate {
BuiltGate {
sym_src: src.to_string(),
sym_tgt: tgt.to_string(),
index: OverlapIndex::from_pairs(pairs),
n_src: 0,
n_tgt: 0,
clause_ix,
in_filter: false,
}
}
#[test]
fn pointwise_keep_matches_the_cell_walk() {
let names: Vec<String> = ["i", "k", "j"].iter().map(|s| s.to_string()).collect();
let ranges = [(1, 4), (0, 2), (2, 6)];
let gate_sets = vec![
vec![],
vec![gate(
"i",
"j",
&[(1, 2), (1, 5), (3, 3), (4, 6), (4, 9), (9, 2)],
0,
)],
vec![
gate("j", "i", &[(2, 1), (3, 3), (6, 4), (5, 2)], 0),
gate("k", "i", &[(0, 1), (2, 3), (1, 4), (2, 4)], 1),
],
vec![gate("i", "q", &[(1, 1)], 0)],
];
let body = Expr::Number(0.0);
for gates in &gate_sets {
let spec = TupleSpec {
idx_names: &names,
ranges: &ranges,
body: &body,
contract_names: &[],
bounds: &[],
gates,
reduce: ReduceKind::Sum,
filter: None,
init: 0.0,
};
let list = enumerate_tuples(&spec, usize::MAX)
.expect("walks")
.expect("uncapped");
let walked: Vec<f64> = list
.rows
.windows(2)
.map(|w| if w[1] > w[0] { 1.0 } else { 0.0 })
.collect();
let keep = pointwise_keep(&names, &ranges, gates)
.expect("keeps")
.unwrap_or_else(|| vec![1.0; walked.len()]);
assert_eq!(keep, walked);
}
}
}