use std::collections::HashSet;
use crate::ir::analysis::{ColumnLeaf, ConstraintInfo, ConstraintShape};
#[derive(Debug, Clone, Copy)]
pub struct ChunkBudget {
pub max_leafset: u32,
pub max_constraints_per_chunk: u32,
}
impl ChunkBudget {
pub fn recommended() -> Self {
let env_u32 = |k: &str, default: u32| -> u32 {
std::env::var(k).ok().and_then(|s| s.parse().ok()).unwrap_or(default)
};
Self {
max_leafset: env_u32("CHUNKER_MAX_LEAFSET", 64),
max_constraints_per_chunk: env_u32("CHUNKER_MAX_CONSTRAINTS", 512),
}
}
}
#[derive(Debug)]
pub struct Chunk {
pub constraint_indices: Vec<usize>,
pub leafset: HashSet<ColumnLeaf>,
pub depth_max: u32,
pub shape: ConstraintShape,
}
pub fn chunk_dag(constraints: &[ConstraintInfo], budget: &ChunkBudget) -> Vec<Chunk> {
let (linear, general): (Vec<_>, Vec<_>) = (0..constraints.len())
.partition(|&i| matches!(constraints[i].shape, ConstraintShape::LinearWeightedSum));
let mut chunks = Vec::new();
chunks.extend(chunk_subset(constraints, &linear, budget));
chunks.extend(chunk_subset(constraints, &general, budget));
chunks
}
fn chunk_subset(
constraints: &[ConstraintInfo],
indices: &[usize],
budget: &ChunkBudget,
) -> Vec<Chunk> {
let mut order: Vec<usize> = indices.to_vec();
order.sort_by(|&a, &b| {
constraints[b]
.column_leaves
.len()
.cmp(&constraints[a].column_leaves.len())
.then_with(|| constraints[b].work.cmp(&constraints[a].work))
});
let mut chunks: Vec<Chunk> = Vec::new();
for ci in order {
let c = &constraints[ci];
let mut best: Option<(usize, usize)> = None; for (i, chunk) in chunks.iter().enumerate() {
if chunk.constraint_indices.len() as u32 + 1 > budget.max_constraints_per_chunk {
continue;
}
let new_leaves = c.column_leaves.difference(&chunk.leafset).count();
let new_union = chunk.leafset.len() + new_leaves;
if new_union as u32 > budget.max_leafset {
continue;
}
if best.is_none_or(|(_, bn)| new_leaves < bn) {
best = Some((i, new_leaves));
}
}
match best {
Some((idx, _)) => {
let chunk = &mut chunks[idx];
for &leaf in &c.column_leaves {
chunk.leafset.insert(leaf);
}
chunk.constraint_indices.push(ci);
chunk.depth_max = chunk.depth_max.max(c.depth);
if !matches!(c.shape, ConstraintShape::LinearWeightedSum) {
chunk.shape = ConstraintShape::General;
}
}
None => {
let mut leafset = HashSet::with_capacity(c.column_leaves.len());
for &leaf in &c.column_leaves {
leafset.insert(leaf);
}
chunks.push(Chunk {
constraint_indices: vec![ci],
leafset,
depth_max: c.depth,
shape: c.shape,
});
}
}
}
chunks
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::dag::TraceSource;
fn cinfo(idx: usize, cols: &[u32], work: u32, depth: u32) -> ConstraintInfo {
let column_leaves = cols
.iter()
.copied()
.map(|c| ColumnLeaf { source: TraceSource::MainLocal, col: c })
.collect();
ConstraintInfo {
constraint_idx: idx,
root: 0,
alpha_index: idx as u32,
total_nodes: 0,
work,
depth,
column_leaves,
shape: ConstraintShape::General,
}
}
#[test]
fn small_constraints_pack_into_one_chunk() {
let c = vec![cinfo(0, &[0, 1, 2], 5, 3), cinfo(1, &[0, 1, 3], 5, 3)];
let chunks = chunk_dag(&c, &ChunkBudget { max_leafset: 16, max_constraints_per_chunk: 16 });
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].leafset.len(), 4);
assert_eq!(chunks[0].constraint_indices.len(), 2);
}
#[test]
fn budget_forces_split() {
let c = vec![
cinfo(0, &(0..10).collect::<Vec<_>>(), 5, 3),
cinfo(1, &(10..20).collect::<Vec<_>>(), 5, 3),
];
let chunks = chunk_dag(&c, &ChunkBudget { max_leafset: 12, max_constraints_per_chunk: 16 });
assert_eq!(chunks.len(), 2);
}
#[test]
fn oversize_constraint_gets_own_chunk() {
let c = vec![cinfo(0, &(0..100).collect::<Vec<_>>(), 50, 5)];
let chunks = chunk_dag(&c, &ChunkBudget { max_leafset: 16, max_constraints_per_chunk: 16 });
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].constraint_indices, vec![0]);
}
}