Skip to content

Commit d52594c

Browse files
committed
fix: estimation for aggregates
1 parent fb7f8ba commit d52594c

11 files changed

Lines changed: 629 additions & 19 deletions

libs/@local/hashql/mir/src/pass/analysis/size_estimation/dynamic.rs

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use hashql_core::{
1717
use hashql_hir::node::operation::InputOp;
1818

1919
use super::{
20+
Cardinal, InformationRange,
2021
footprint::{BodyFootprint, BodyFootprintSemilattice, Footprint},
2122
r#static::StaticSizeEstimationCache,
2223
};
@@ -28,15 +29,19 @@ use crate::{
2829
local::{Local, LocalDecl, LocalSlice},
2930
location::Location,
3031
operand::Operand,
31-
place::{Place, Projection, ProjectionKind},
32-
rvalue::{Aggregate, Apply, ArgSlice, BinOp, Binary, Input, RValue, UnOp, Unary},
32+
place::{FieldIndex, Place, Projection, ProjectionKind},
33+
rvalue::{
34+
Aggregate, AggregateKind, Apply, ArgSlice, BinOp, Binary, Input, RValue, UnOp, Unary,
35+
},
3336
statement::{Assign, Statement, StatementKind},
3437
},
3538
def::{DefId, DefIdSlice},
3639
pass::analysis::{
3740
dataflow::{
3841
framework::{DataflowAnalysis, Direction},
39-
lattice::{AdditiveMonoid as _, SaturatingSemiring},
42+
lattice::{
43+
AdditiveMonoid as _, HasBottom as _, JoinSemiLattice as _, SaturatingSemiring,
44+
},
4045
},
4146
size_estimation::{
4247
AffineEquation, estimate::Estimate, range::Cardinality, r#static::StaticSizeEstimation,
@@ -82,6 +87,13 @@ impl Eval {
8287
&Self::Copy(local) => &domain.locals[local],
8388
}
8489
}
90+
91+
pub(crate) fn into_footprint<A: Allocator>(self, domain: &BodyFootprint<A>) -> Footprint {
92+
match self {
93+
Self::Footprint(footprint) => footprint,
94+
Self::Copy(local) => domain.locals[local].clone(),
95+
}
96+
}
8597
}
8698

8799
/// Helper for looking up operand footprints during dataflow analysis.
@@ -204,6 +216,120 @@ impl<'ctx, 'footprints, 'env, 'heap, A: Allocator, C: Allocator>
204216
self.lookup
205217
}
206218

219+
#[expect(
220+
clippy::integer_division,
221+
clippy::cast_possible_truncation,
222+
clippy::integer_division_remainder_used
223+
)]
224+
fn eval_rvalue_aggregate<B: Allocator>(
225+
&self,
226+
domain: &BodyFootprint<B>,
227+
aggregate: &Aggregate<'heap>,
228+
) -> Eval {
229+
match aggregate {
230+
Aggregate {
231+
kind: AggregateKind::Struct { fields: _ } | AggregateKind::Tuple,
232+
operands,
233+
} => {
234+
let mut units: Estimate<InformationRange> = SaturatingSemiring.zero();
235+
236+
for operand in operands {
237+
let eval = self.lookup.operand(domain, operand);
238+
let materialized = eval.into_footprint(domain).materialize();
239+
240+
SaturatingSemiring.plus(&mut units, &materialized);
241+
}
242+
243+
Eval::Footprint(Footprint::one(units))
244+
}
245+
Aggregate {
246+
kind: AggregateKind::List,
247+
operands,
248+
} => {
249+
let mut average: Estimate<InformationRange> = SaturatingSemiring.bottom();
250+
251+
for operand in operands {
252+
let eval = self.lookup.operand(domain, operand);
253+
let units = eval.into_footprint(domain).materialize();
254+
255+
SaturatingSemiring.join(&mut average, &units);
256+
}
257+
258+
Eval::Footprint(Footprint {
259+
units: average,
260+
cardinality: Estimate::Constant(Cardinality::value(Cardinal::new(
261+
operands.len() as u32,
262+
))),
263+
})
264+
}
265+
Aggregate {
266+
kind: AggregateKind::Dict,
267+
operands,
268+
} => {
269+
let mut average: Estimate<InformationRange> = SaturatingSemiring.bottom();
270+
debug_assert!(operands.len() % 2 == 0);
271+
272+
for [key, value] in operands.iter().array_chunks::<2>() {
273+
let mut key_units = self
274+
.lookup
275+
.operand(domain, key)
276+
.into_footprint(domain)
277+
.materialize();
278+
let value_units = self
279+
.lookup
280+
.operand(domain, value)
281+
.into_footprint(domain)
282+
.materialize();
283+
284+
key_units.saturating_mul_add(&value_units, 1);
285+
286+
SaturatingSemiring.join(&mut average, &key_units);
287+
}
288+
289+
Eval::Footprint(Footprint {
290+
units: average,
291+
cardinality: Estimate::Constant(Cardinality::value(Cardinal::new(
292+
(operands.len() / 2) as u32,
293+
))),
294+
})
295+
}
296+
Aggregate {
297+
kind: AggregateKind::Closure,
298+
operands,
299+
} => {
300+
debug_assert_eq!(operands.len(), 2);
301+
302+
let mut total = self
303+
.lookup
304+
.operand(domain, &operands[FieldIndex::FN_PTR])
305+
.into_footprint(domain)
306+
.materialize();
307+
let env = self
308+
.lookup
309+
.operand(domain, &operands[FieldIndex::ENV])
310+
.into_footprint(domain)
311+
.materialize();
312+
total.saturating_mul_add(&env, 1);
313+
314+
Eval::Footprint(Footprint::one(total))
315+
}
316+
Aggregate {
317+
kind: AggregateKind::Opaque(_),
318+
operands,
319+
} => {
320+
let mut total: Footprint = SaturatingSemiring.zero();
321+
322+
for operand in operands {
323+
let eval = self.lookup.operand(domain, operand);
324+
325+
SaturatingSemiring.plus(&mut total, eval.as_ref(domain));
326+
}
327+
328+
Eval::Footprint(total)
329+
}
330+
}
331+
}
332+
207333
/// Evaluates an rvalue to determine its footprint.
208334
fn eval_rvalue<B: Allocator>(&self, domain: &BodyFootprint<B>, rvalue: &RValue<'heap>) -> Eval {
209335
#[expect(clippy::match_same_arms, reason = "explicit case handling for clarity")]
@@ -228,17 +354,7 @@ impl<'ctx, 'footprints, 'env, 'heap, A: Allocator, C: Allocator>
228354
op: UnOp::BitNot | UnOp::Neg,
229355
operand: _,
230356
}) => Eval::Footprint(Footprint::scalar()),
231-
RValue::Aggregate(Aggregate { kind: _, operands }) => {
232-
let mut total: Footprint = SaturatingSemiring.zero();
233-
234-
for operand in operands {
235-
let eval = self.lookup.operand(domain, operand);
236-
237-
SaturatingSemiring.plus(&mut total, eval.as_ref(domain));
238-
}
239-
240-
Eval::Footprint(total)
241-
}
357+
RValue::Aggregate(aggregate) => self.eval_rvalue_aggregate(domain, aggregate),
242358
RValue::Input(Input {
243359
op: InputOp::Exists,
244360
name: _,

libs/@local/hashql/mir/src/pass/analysis/size_estimation/estimate.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,21 @@ impl<T> Estimate<T> {
178178
&other.constant().saturating_mul(coefficient),
179179
);
180180
}
181+
182+
pub(crate) fn saturating_coeff_mul<U>(&mut self, other: &Estimate<U>)
183+
where
184+
T: Clone,
185+
{
186+
self.resize_coefficients(other.coefficients().len());
187+
188+
for (coeff, &other_coeff) in self
189+
.coefficients_mut()
190+
.iter_mut()
191+
.zip(other.coefficients().iter())
192+
{
193+
*coeff = coeff.saturating_mul(other_coeff);
194+
}
195+
}
181196
}
182197

183198
impl<T> AdditiveMonoid<Estimate<T>> for SaturatingSemiring

0 commit comments

Comments
 (0)