diff --git a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/dynamic.rs b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/dynamic.rs index 74a075019da..d0d6f1be3e1 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/dynamic.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/dynamic.rs @@ -17,6 +17,7 @@ use hashql_core::{ use hashql_hir::node::operation::InputOp; use super::{ + Cardinal, InformationRange, footprint::{BodyFootprint, BodyFootprintSemilattice, Footprint}, r#static::StaticSizeEstimationCache, }; @@ -28,15 +29,19 @@ use crate::{ local::{Local, LocalDecl, LocalSlice}, location::Location, operand::Operand, - place::{Place, Projection, ProjectionKind}, - rvalue::{Aggregate, Apply, ArgSlice, BinOp, Binary, Input, RValue, UnOp, Unary}, + place::{FieldIndex, Place, Projection, ProjectionKind}, + rvalue::{ + Aggregate, AggregateKind, Apply, ArgSlice, BinOp, Binary, Input, RValue, UnOp, Unary, + }, statement::{Assign, Statement, StatementKind}, }, def::{DefId, DefIdSlice}, pass::analysis::{ dataflow::{ framework::{DataflowAnalysis, Direction}, - lattice::{AdditiveMonoid as _, SaturatingSemiring}, + lattice::{ + AdditiveMonoid as _, HasBottom as _, JoinSemiLattice as _, SaturatingSemiring, + }, }, size_estimation::{ AffineEquation, estimate::Estimate, range::Cardinality, r#static::StaticSizeEstimation, @@ -82,6 +87,13 @@ impl Eval { &Self::Copy(local) => &domain.locals[local], } } + + pub(crate) fn into_footprint(self, domain: &BodyFootprint) -> Footprint { + match self { + Self::Footprint(footprint) => footprint, + Self::Copy(local) => domain.locals[local].clone(), + } + } } /// Helper for looking up operand footprints during dataflow analysis. @@ -204,6 +216,120 @@ impl<'ctx, 'footprints, 'env, 'heap, A: Allocator, C: Allocator> self.lookup } + #[expect( + clippy::integer_division, + clippy::cast_possible_truncation, + clippy::integer_division_remainder_used + )] + fn eval_rvalue_aggregate( + &self, + domain: &BodyFootprint, + aggregate: &Aggregate<'heap>, + ) -> Eval { + match aggregate { + Aggregate { + kind: AggregateKind::Struct { fields: _ } | AggregateKind::Tuple, + operands, + } => { + let mut units: Estimate = SaturatingSemiring.zero(); + + for operand in operands { + let eval = self.lookup.operand(domain, operand); + let materialized = eval.into_footprint(domain).materialize(); + + SaturatingSemiring.plus(&mut units, &materialized); + } + + Eval::Footprint(Footprint::one(units)) + } + Aggregate { + kind: AggregateKind::List, + operands, + } => { + let mut average: Estimate = SaturatingSemiring.bottom(); + + for operand in operands { + let eval = self.lookup.operand(domain, operand); + let units = eval.into_footprint(domain).materialize(); + + SaturatingSemiring.join(&mut average, &units); + } + + Eval::Footprint(Footprint { + units: average, + cardinality: Estimate::Constant(Cardinality::value(Cardinal::new( + operands.len() as u32, + ))), + }) + } + Aggregate { + kind: AggregateKind::Dict, + operands, + } => { + let mut average: Estimate = SaturatingSemiring.bottom(); + debug_assert!(operands.len() % 2 == 0); + + for [key, value] in operands.iter().array_chunks::<2>() { + let mut key_units = self + .lookup + .operand(domain, key) + .into_footprint(domain) + .materialize(); + let value_units = self + .lookup + .operand(domain, value) + .into_footprint(domain) + .materialize(); + + key_units.saturating_mul_add(&value_units, 1); + + SaturatingSemiring.join(&mut average, &key_units); + } + + Eval::Footprint(Footprint { + units: average, + cardinality: Estimate::Constant(Cardinality::value(Cardinal::new( + (operands.len() / 2) as u32, + ))), + }) + } + Aggregate { + kind: AggregateKind::Closure, + operands, + } => { + debug_assert_eq!(operands.len(), 2); + + let mut total = self + .lookup + .operand(domain, &operands[FieldIndex::FN_PTR]) + .into_footprint(domain) + .materialize(); + let env = self + .lookup + .operand(domain, &operands[FieldIndex::ENV]) + .into_footprint(domain) + .materialize(); + total.saturating_mul_add(&env, 1); + + Eval::Footprint(Footprint::one(total)) + } + Aggregate { + kind: AggregateKind::Opaque(_), + operands, + } => { + let mut total: Footprint = SaturatingSemiring.zero(); + + for operand in operands { + let eval = self.lookup.operand(domain, operand); + + SaturatingSemiring.plus(&mut total, eval.as_ref(domain)); + } + + Eval::Footprint(total) + } + } + } + /// Evaluates an rvalue to determine its footprint. fn eval_rvalue(&self, domain: &BodyFootprint, rvalue: &RValue<'heap>) -> Eval { #[expect(clippy::match_same_arms, reason = "explicit case handling for clarity")] @@ -228,17 +354,7 @@ impl<'ctx, 'footprints, 'env, 'heap, A: Allocator, C: Allocator> op: UnOp::BitNot | UnOp::Neg, operand: _, }) => Eval::Footprint(Footprint::scalar()), - RValue::Aggregate(Aggregate { kind: _, operands }) => { - let mut total: Footprint = SaturatingSemiring.zero(); - - for operand in operands { - let eval = self.lookup.operand(domain, operand); - - SaturatingSemiring.plus(&mut total, eval.as_ref(domain)); - } - - Eval::Footprint(total) - } + RValue::Aggregate(aggregate) => self.eval_rvalue_aggregate(domain, aggregate), RValue::Input(Input { op: InputOp::Exists, name: _, diff --git a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/estimate.rs b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/estimate.rs index e4b35dfccb5..3832357c4aa 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/estimate.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/estimate.rs @@ -178,6 +178,26 @@ impl Estimate { &other.constant().saturating_mul(coefficient), ); } + + pub(crate) fn saturating_coeff_mul(&mut self, other: &Estimate) + where + T: Clone, + { + match self { + Self::Constant(_) => {} + Self::Affine(equation) => { + equation.coefficients.truncate(other.coefficients().len()); + } + } + + for (coeff, &other_coeff) in self + .coefficients_mut() + .iter_mut() + .zip(other.coefficients().iter()) + { + *coeff = coeff.saturating_mul(other_coeff); + } + } } impl AdditiveMonoid> for SaturatingSemiring diff --git a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/footprint.rs b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/footprint.rs index 50a51b08c21..f4c7e144512 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/footprint.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/footprint.rs @@ -215,6 +215,14 @@ impl Footprint { } } + #[must_use] + pub const fn one(units: Estimate) -> Self { + Self { + units, + cardinality: Estimate::Constant(Cardinality::one()), + } + } + /// A footprint that tracks dependency on a function parameter. /// /// Both units and cardinality are set to equal the parameter at `index`. @@ -249,6 +257,70 @@ impl Footprint { Some(avg) } + /// Collapses this footprint into a total information estimate. + /// + /// Multiplies units by cardinality to produce a single information measure + /// representing the total information content. This is used when a value is + /// embedded as a field of a composite type, where the per-element vs. element-count + /// distinction is no longer relevant. + /// + /// For constant footprints the multiplication is exact. For affine units with + /// constant cardinality, coefficients are scaled by the cardinality upper bound + /// (over-approximation). When at least one side is affine, element-wise + /// coefficient multiplication preserves same-parameter dependencies + /// (under-approximation of the quadratic product). + pub(crate) fn materialize(&self) -> Estimate { + let Self { units, cardinality } = self; + + // Cardinality of exactly 1: units already represents the total + if *cardinality == Estimate::Constant(Cardinality::one()) { + return units.clone(); + } + + match (units, cardinality) { + // Both constant: exact range multiplication. + (Estimate::Constant(units), Estimate::Constant(cardinality)) => { + Estimate::Constant(units.saturating_mul_cardinality(*cardinality)) + } + // Affine units, constant cardinality: scale all terms by cardinality. + // Constant part is multiplied exactly. Coefficients are scaled by the + // cardinality upper bound, which is exact for point ranges and an + // over-approximation for wider ranges. + (Estimate::Affine(units_eq), Estimate::Constant(cardinality)) => { + let Some(cardinality_max) = cardinality.inclusive_max() else { + // Unbounded cardinality: cannot bound the total + return Estimate::Constant(InformationRange::full()); + }; + + let scale = u16::try_from(cardinality_max.raw).unwrap_or(u16::MAX); + + let mut result = units_eq.clone(); + result.constant = result.constant.saturating_mul_cardinality(*cardinality); + for coefficient in &mut result.coefficients { + *coefficient = coefficient.saturating_mul(scale); + } + + Estimate::Affine(result) + } + // At least one side is affine: element-wise coefficient multiplication + // gives a linear under-approximation of the quadratic product. This + // preserves parameter dependency for the common case where both + // dimensions track the same parameter. When units is constant (all + // zero coefficients), the element-wise product correctly yields zero + // coefficients, leaving only the constant-times-constant term. + _ => { + let mut result = units.clone(); + result.saturating_coeff_mul(cardinality); + let range = result + .constant() + .saturating_mul_cardinality(*cardinality.constant()); + *result.constant_mut() = range; + + result + } + } + } + /// Adds `other * coefficient` to this footprint (component-wise). pub(crate) fn saturating_mul_add( &mut self, @@ -329,7 +401,7 @@ impl HasBottom for SaturatingSemiring { mod tests { #![expect(clippy::min_ident_chars)] use alloc::alloc::Global; - use core::ops::Bound; + use core::{iter, ops::Bound}; use super::{BodyFootprint, BodyFootprintSemilattice}; use crate::{ @@ -343,8 +415,8 @@ mod tests { }, }, size_estimation::{ - Cardinal, Cardinality, Footprint, InformationRange, InformationUnit, - estimate::Estimate, + AffineEquation, Cardinal, Cardinality, Footprint, InformationRange, + InformationUnit, estimate::Estimate, }, }, }; @@ -456,4 +528,120 @@ mod tests { assert_bounded_join_semilattice(&lattice, body_a, body_b, body_c); assert_is_bottom_consistent::<_, BodyFootprint>(&lattice); } + + #[test] + fn materialize_scalar_is_identity() { + // (units=1, cardinality=1) -> 1*1 = 1 + let result = Footprint::scalar().materialize(); + assert_eq!(result, Estimate::Constant(InformationRange::one())); + } + + #[test] + fn materialize_constant_collection() { + // (units=1..=1, cardinality=5..=5) -> 1*5 = 5..=5 + let footprint = Footprint { + units: Estimate::Constant(InformationRange::one()), + cardinality: Estimate::Constant(Cardinality::value(Cardinal::new(5))), + }; + + let result = footprint.materialize(); + + assert_eq!( + result, + Estimate::Constant(InformationRange::value(InformationUnit::new(5))) + ); + } + + #[test] + fn materialize_affine_units_constant_cardinality() { + // units = 2..=3 + 1*p0, cardinality = 5..=5 + // expected: (2*5)..=(3*5) + 5*p0 = 10..=15 + 5*p0 + let footprint = Footprint { + units: Estimate::Affine(AffineEquation { + coefficients: [1, 0].into_iter().collect(), + constant: InformationRange::new( + InformationUnit::new(2), + Bound::Included(InformationUnit::new(3)), + ), + }), + cardinality: Estimate::Constant(Cardinality::value(Cardinal::new(5))), + }; + + let result = footprint.materialize(); + + let Estimate::Affine(eq) = &result else { + panic!("expected Affine, got {result:?}"); + }; + assert_eq!(eq.coefficients.as_slice(), &[5, 0]); + assert_eq!( + eq.constant, + InformationRange::new( + InformationUnit::new(10), + Bound::Included(InformationUnit::new(15)) + ) + ); + } + + #[test] + fn materialize_constant_units_affine_cardinality() { + // units = 3..=3, cardinality = 0..0 + 1*p0 + // Constant units with affine cardinality: coefficients are all zero, + // so the result is just constant * constant = 3..=3 * 0..0 = empty + let footprint = Footprint { + units: Estimate::Constant(InformationRange::value(InformationUnit::new(3))), + cardinality: Estimate::Affine(AffineEquation { + coefficients: iter::once(1).collect(), + constant: Cardinality::empty(), + }), + }; + + let result = footprint.materialize(); + + // Zero coefficients (0*1=0), constant 3*empty=empty + assert_eq!(*result.constant(), InformationRange::empty()); + assert!(result.coefficients().iter().all(|&c| c == 0)); + } + + #[test] + fn materialize_both_affine_same_parameter() { + // Both depend on param 0: units = 0..0 + 1*p0, cardinality = 0..0 + 1*p0 + // Element-wise: coeffs[0] = 1*1 = 1, constant = empty * empty = empty + let footprint = Footprint::coefficient(0, 2); + + let result = footprint.materialize(); + + let Estimate::Affine(eq) = &result else { + panic!("expected Affine, got {result:?}"); + }; + assert_eq!(eq.coefficients.as_slice(), &[1, 0]); + assert_eq!(eq.constant, InformationRange::empty()); + } + + #[test] + fn materialize_zeroes_trailing_coefficients() { + // units depends on params 0, 1, 2; cardinality depends only on param 0. + // Params 1 and 2 have implicit zero in cardinality, so the product + // should drop those coefficients rather than preserving them. + // units = 0..0 + 3*p0 + 5*p1 + 7*p2 + // cardinality = 0..0 + 2*p0 + // element-wise: [3*2] = [6] (trailing terms truncated) + let footprint = Footprint { + units: Estimate::Affine(AffineEquation { + coefficients: [3, 5, 7].into_iter().collect(), + constant: InformationRange::empty(), + }), + cardinality: Estimate::Affine(AffineEquation { + coefficients: iter::once(2).collect(), + constant: Cardinality::empty(), + }), + }; + + let result = footprint.materialize(); + + let Estimate::Affine(eq) = &result else { + panic!("expected Affine, got {result:?}"); + }; + assert_eq!(eq.coefficients.as_slice(), &[6]); + assert_eq!(eq.constant, InformationRange::empty()); + } } diff --git a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/range.rs b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/range.rs index 60acadb6d21..03c74465fc2 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/range.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/range.rs @@ -310,6 +310,37 @@ range!( pub struct InformationRange(InformationUnit) ); +impl InformationRange { + /// Multiplies this information range by a cardinality range, producing the total + /// information content. + /// + /// For non-negative integer ranges `[a, b] × [c, d] = [a*c, b*d]`. + /// Saturates to `Unbounded` on overflow. + #[must_use] + pub fn saturating_mul_cardinality(self, cardinality: Cardinality) -> Self { + if self.is_empty() || cardinality.is_empty() { + return Self::empty(); + } + + let min = InformationUnit::new(self.min.raw.saturating_mul(cardinality.min().raw)); + + let Some((units_max, cardinality_max)) = + Option::zip(self.inclusive_max(), cardinality.inclusive_max()) + else { + return Self { + min, + max: Bound::Unbounded, + }; + }; + + let max = units_max + .checked_mul(cardinality_max) + .map_or(Bound::Unbounded, Bound::Included); + + Self { min, max } + } +} + range!( /// A range of possible cardinality (element count) values. /// @@ -711,4 +742,88 @@ mod tests { assert_is_bottom_consistent::(&semiring); assert_is_bottom_consistent::(&semiring); } + + #[test] + fn saturating_mul_cardinality_exact() { + // [2, 3] * [4, 5] = [8, 15] + let units = InformationRange::new( + InformationUnit::new(2), + Bound::Included(InformationUnit::new(3)), + ); + let card = Cardinality::new(Cardinal::new(4), Bound::Included(Cardinal::new(5))); + + let result = units.saturating_mul_cardinality(card); + + assert_eq!( + result, + InformationRange::new( + InformationUnit::new(8), + Bound::Included(InformationUnit::new(15)) + ) + ); + } + + #[test] + fn saturating_mul_cardinality_by_one_is_identity() { + let units = InformationRange::new( + InformationUnit::new(3), + Bound::Included(InformationUnit::new(7)), + ); + + assert_eq!(units.saturating_mul_cardinality(Cardinality::one()), units); + } + + #[test] + fn saturating_mul_cardinality_empty_input() { + let empty = InformationRange::empty(); + let card = Cardinality::new(Cardinal::new(5), Bound::Included(Cardinal::new(10))); + + assert_eq!( + empty.saturating_mul_cardinality(card), + InformationRange::empty() + ); + + let units = InformationRange::new( + InformationUnit::new(3), + Bound::Included(InformationUnit::new(5)), + ); + + assert_eq!( + units.saturating_mul_cardinality(Cardinality::empty()), + InformationRange::empty() + ); + } + + #[test] + fn saturating_mul_cardinality_unbounded() { + let units = InformationRange::new( + InformationUnit::new(2), + Bound::Included(InformationUnit::new(3)), + ); + let card = Cardinality::new(Cardinal::new(1), Bound::Unbounded); + + let result = units.saturating_mul_cardinality(card); + + assert_eq!( + result, + InformationRange::new(InformationUnit::new(2), Bound::Unbounded) + ); + } + + #[test] + fn saturating_mul_cardinality_overflow_to_unbounded() { + let units = InformationRange::new( + InformationUnit::new(u32::MAX), + Bound::Included(InformationUnit::new(u32::MAX)), + ); + let card = Cardinality::new(Cardinal::new(2), Bound::Included(Cardinal::new(2))); + + let result = units.saturating_mul_cardinality(card); + + // min saturates, max overflows to Unbounded + assert_eq!( + result, + InformationRange::new(InformationUnit::new(u32::MAX), Bound::Unbounded) + ); + } } diff --git a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs index 64c203469bf..ad857144ea1 100644 --- a/libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +++ b/libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs @@ -619,6 +619,157 @@ fn mutual_recursion_converges() { ); } +/// List aggregates use per-element units (join) with element count as cardinality. +#[test] +fn list_aggregate_per_element_units() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let mut body = body!(interner, env; fn@0/0 -> [List Int] { + decl a: Int, b: Int, c: Int, result: [List ?]; + + bb0() { + a = load 1; + b = load 2; + c = load 3; + result = list a, b, c; + return result; + } + }); + + assert_size_estimation( + "list_aggregate_per_element_units", + slice::from_mut(&mut body), + &heap, + &interner, + &env, + ); +} + +/// Dict aggregates compute per-pair units (key + value) with pair count as cardinality. +/// Each pair contributes its combined key+value materialized size. +#[test] +fn dict_aggregate_per_pair_units() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + let types = TypeBuilder::synthetic(&env); + + let int_ty = types.integer(); + let dict_ty = types.dict(types.unknown(), types.unknown()); + + let mut builder = BodyBuilder::new(&interner); + + let k1 = builder.local("k1", int_ty); + let v1 = builder.local("v1", int_ty); + let k2 = builder.local("k2", int_ty); + let v2 = builder.local("v2", int_ty); + let result = builder.local("result", dict_ty); + + let const_1 = builder.const_int(1); + let const_2 = builder.const_int(2); + let const_3 = builder.const_int(3); + let const_4 = builder.const_int(4); + + let bb0 = builder.reserve_block([]); + builder + .build_block(bb0) + .assign_place(k1, |rv| rv.load(const_1)) + .assign_place(v1, |rv| rv.load(const_2)) + .assign_place(k2, |rv| rv.load(const_3)) + .assign_place(v2, |rv| rv.load(const_4)) + .assign_place(result, |rv| rv.dict([(k1, v1), (k2, v2)])) + .ret(result); + + let mut body = builder.finish(0, dict_ty); + body.id = DefId::new(0); + + assert_size_estimation( + "dict_aggregate_per_pair_units", + slice::from_mut(&mut body), + &heap, + &interner, + &env, + ); +} + +/// Tuple with many fields has cardinality 1: a tuple is a single composite value, +/// not a collection. +#[test] +fn tuple_many_fields_cardinality_one() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let mut body = body!(interner, env; fn@0/0 -> (Int, Int, Int, Int, Int) { + decl a: Int, b: Int, c: Int, d: Int, e: Int, result: (Int, Int, Int, Int, ?); + + bb0() { + a = load 1; + b = load 2; + c = load 3; + d = load 4; + e = load 5; + result = tuple a, b, c, d, e; + return result; + } + }); + + assert_size_estimation( + "tuple_many_fields_cardinality_one", + slice::from_mut(&mut body), + &heap, + &interner, + &env, + ); +} + +/// Struct containing a list parameter materializes the list's total information. +/// A function takes a list parameter and wraps it with a scalar in a struct. +/// The struct's units should account for the list field's full footprint, not +/// just its per-element units. +#[test] +fn struct_materializes_list_parameter() { + let heap = Heap::new(); + let interner = Interner::new(&heap); + let env = Environment::new(&heap); + + let callee_id = DefId::new(0); + let caller_id = DefId::new(1); + + // Callee wraps its list parameter in a struct with a scalar + let callee = body!(interner, env; fn@callee_id/1 -> (data: ?, tag: Int) { + decl xs: [List Int], tag: Int, result: (data: ?, tag: ?); + + bb0() { + tag = load 42; + result = struct data: xs, tag: tag; + return result; + } + }); + + // Caller passes an external list input to the callee + let caller = body!(interner, env; fn@caller_id/0 -> (data: ?, tag: Int) { + decl input: [List Int], result: ?; + + bb0() { + input = input.load! "items"; + result = apply callee_id, input; + return result; + } + }); + + let mut bodies = [callee, caller]; + assert_size_estimation( + "struct_materializes_list_parameter", + &mut bodies, + &heap, + &interner, + &env, + ); +} + /// Self-recursion converges to a stable footprint. /// Has a base case that returns the parameter, ensuring data flows. #[test] diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/dict_aggregate_per_pair_units.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/dict_aggregate_per_pair_units.snap new file mode 100644 index 00000000000..8f6201bbf8e --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/dict_aggregate_per_pair_units.snap @@ -0,0 +1,12 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 +expression: output +--- +fn@0 (0 args) + %0: units: 1..=1, cardinality: 1..=1 + %1: units: 1..=1, cardinality: 1..=1 + %2: units: 1..=1, cardinality: 1..=1 + %3: units: 1..=1, cardinality: 1..=1 + %4: units: 2..=2, cardinality: 2..=2 + returns: units: 2..=2, cardinality: 2..=2 diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/list_aggregate_per_element_units.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/list_aggregate_per_element_units.snap new file mode 100644 index 00000000000..eb9709c025c --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/list_aggregate_per_element_units.snap @@ -0,0 +1,11 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 +expression: output +--- +fn@0 (0 args) + %0: units: 1..=1, cardinality: 1..=1 + %1: units: 1..=1, cardinality: 1..=1 + %2: units: 1..=1, cardinality: 1..=1 + %3: units: 1..=1, cardinality: 3..=3 + returns: units: 1..=1, cardinality: 3..=3 diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_aggregate_sums_operands.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_aggregate_sums_operands.snap index 1dd91137548..73561a4aaf4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_aggregate_sums_operands.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_aggregate_sums_operands.snap @@ -1,9 +1,10 @@ --- source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 expression: output --- fn@0 (0 args) %0: units: 1..=1, cardinality: 1..=1 %1: units: 1..=1, cardinality: 1..=1 - %2: units: 2..=2, cardinality: 2..=2 + %2: units: 2..=2, cardinality: 1..=1 returns: units: 2..=2, cardinality: 1..=1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_materializes_list_parameter.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_materializes_list_parameter.snap new file mode 100644 index 00000000000..b0ec69c4b0b --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/struct_materializes_list_parameter.snap @@ -0,0 +1,15 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 +expression: output +--- +fn@0 (1 args) + %0: units: 0..0, cardinality: 0..0 + %1: units: 1..=1, cardinality: 1..=1 + %2: units: 1..=1 + 1*%0, cardinality: 1..=1 + returns: units: 1..=1 + 1*%0, cardinality: 1..=1 + +fn@1 (0 args) + %0: units: 0.., cardinality: 1..=1 + %1: units: 1.., cardinality: 1..=1 + returns: units: 1.., cardinality: 1..=1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_aggregate_sums_operands.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_aggregate_sums_operands.snap index 1dd91137548..73561a4aaf4 100644 --- a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_aggregate_sums_operands.snap +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_aggregate_sums_operands.snap @@ -1,9 +1,10 @@ --- source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 expression: output --- fn@0 (0 args) %0: units: 1..=1, cardinality: 1..=1 %1: units: 1..=1, cardinality: 1..=1 - %2: units: 2..=2, cardinality: 2..=2 + %2: units: 2..=2, cardinality: 1..=1 returns: units: 2..=2, cardinality: 1..=1 diff --git a/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_many_fields_cardinality_one.snap b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_many_fields_cardinality_one.snap new file mode 100644 index 00000000000..360e9039a60 --- /dev/null +++ b/libs/@local/hashql/mir/tests/ui/pass/size-estimation/tuple_many_fields_cardinality_one.snap @@ -0,0 +1,13 @@ +--- +source: libs/@local/hashql/mir/src/pass/analysis/size_estimation/tests.rs +assertion_line: 57 +expression: output +--- +fn@0 (0 args) + %0: units: 1..=1, cardinality: 1..=1 + %1: units: 1..=1, cardinality: 1..=1 + %2: units: 1..=1, cardinality: 1..=1 + %3: units: 1..=1, cardinality: 1..=1 + %4: units: 1..=1, cardinality: 1..=1 + %5: units: 5..=5, cardinality: 1..=1 + returns: units: 5..=5, cardinality: 1..=1