Skip to content

Commit dfc6a40

Browse files
connortsui20claude
andcommitted
Consolidate fixed-width validation and width dispatch
Address review feedback on the shared fixed-width kernels: - Document the contract of every FixedWidthArray hook. - Validate the records buffer length once in a shared with_values helper instead of in each implementation, which are now pure constructors. - Share the byte-width dispatch between take and filter through a single match_each_record_width macro. - Fix the miri gate on the AVX2 tests, which was previously a no-op module-level ignore attribute. - Test the arbitrary-width fallback paths of both take and filter, and restore 2-byte and float filter conformance coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 9196a42 commit dfc6a40

10 files changed

Lines changed: 175 additions & 92 deletions

File tree

vortex-array/src/arrays/decimal/compute/fixed_width.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
use vortex_buffer::ByteBuffer;
55
use vortex_error::VortexResult;
6-
use vortex_error::vortex_ensure;
7-
use vortex_error::vortex_err;
86

97
use crate::array::ArrayView;
108
use crate::arrays::Decimal;
@@ -25,16 +23,9 @@ impl FixedWidthArray for Decimal {
2523
fn with_values(
2624
array: ArrayView<'_, Self>,
2725
values: ByteBuffer,
28-
len: usize,
26+
_len: usize,
2927
validity: Validity,
3028
) -> VortexResult<DecimalArray> {
31-
let expected_len = len
32-
.checked_mul(array.values_type().byte_width())
33-
.ok_or_else(|| vortex_err!("Decimal values buffer length overflows usize"))?;
34-
vortex_ensure!(
35-
values.len() == expected_len,
36-
"Decimal values buffer length does not match output length"
37-
);
3829
DecimalArray::try_new_handle(
3930
BufferHandle::new_host(values),
4031
array.values_type(),

vortex-array/src/arrays/fixed_width/array.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,60 @@
33

44
use vortex_buffer::ByteBuffer;
55
use vortex_error::VortexResult;
6+
use vortex_error::vortex_ensure_eq;
7+
use vortex_error::vortex_err;
68

79
use crate::array::Array;
810
use crate::array::ArrayView;
911
use crate::array::VTable;
1012
use crate::validity::Validity;
1113

1214
/// Type-specific access needed by shared fixed-width structural compute.
15+
///
16+
/// The shared `take` and `filter` kernels view an implementing array as `array.len()` records of
17+
/// [`byte_width`] bytes each, which lets them move whole records without knowing the logical
18+
/// type.
19+
///
20+
/// [`byte_width`]: FixedWidthArray::byte_width
1321
pub(crate) trait FixedWidthArray: VTable {
22+
/// Returns the number of bytes each record occupies.
1423
fn byte_width(array: ArrayView<'_, Self>) -> usize;
1524

25+
/// Returns the records of `array` as a single host-resident byte buffer.
26+
///
27+
/// The returned buffer must contain exactly `array.len() * byte_width` bytes.
1628
fn values(array: ArrayView<'_, Self>) -> ByteBuffer;
1729

30+
/// Rebuilds an array of this encoding from a records buffer, preserving the logical type of
31+
/// `array`.
32+
///
33+
/// Callers must provide a `values` buffer of exactly `len * byte_width` bytes and a
34+
/// `validity` with logical length `len`. The shared kernels enforce the buffer length through
35+
/// the module-level `with_values` helper rather than in each implementation.
1836
fn with_values(
1937
array: ArrayView<'_, Self>,
2038
values: ByteBuffer,
2139
len: usize,
2240
validity: Validity,
2341
) -> VortexResult<Array<Self>>;
2442
}
43+
44+
/// Rebuilds a fixed-width array from `len` records in `values`, validating the buffer length
45+
/// before dispatching to [`FixedWidthArray::with_values`].
46+
pub(crate) fn with_values<V: FixedWidthArray>(
47+
array: ArrayView<'_, V>,
48+
values: ByteBuffer,
49+
len: usize,
50+
validity: Validity,
51+
) -> VortexResult<Array<V>> {
52+
let expected_len = len
53+
.checked_mul(V::byte_width(array))
54+
.ok_or_else(|| vortex_err!("Fixed-width values buffer length overflows usize"))?;
55+
vortex_ensure_eq!(
56+
values.len(),
57+
expected_len,
58+
"Fixed-width values buffer length {} does not match expected length {expected_len}",
59+
values.len(),
60+
);
61+
V::with_values(array, values, len, validity)
62+
}

vortex-array/src/arrays/fixed_width/filter.rs

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use vortex_error::VortexExpect;
1010
use vortex_mask::MaskValues;
1111

1212
use super::FixedWidthArray;
13+
use super::match_each_record_width;
14+
use super::with_values;
1315
use crate::array::Array;
1416
use crate::arrays::filter::filter_buffer;
1517
use crate::arrays::filter::filter_buffer_byte_compress;
@@ -28,48 +30,47 @@ pub(crate) fn filter<V: FixedWidthArray>(array: &Array<V>, mask: &Arc<MaskValues
2830
.vortex_expect("fixed-width validity should be derivable"),
2931
mask,
3032
);
31-
V::with_values(array, values, mask.true_count(), validity)
33+
with_values(array, values, mask.true_count(), validity)
3234
.vortex_expect("filtering fixed-width values preserves array invariants")
3335
}
3436

3537
fn filter_records(values: ByteBuffer, byte_width: usize, mask: &MaskValues) -> ByteBuffer {
3638
let alignment = values.alignment();
3739

38-
macro_rules! filter_typed_records {
39-
($filter:path, $width:expr) => {{
40-
let records = Buffer::<[u8; $width]>::from_byte_buffer(values);
41-
return $filter(records, mask).into_byte_buffer().aligned(alignment);
42-
}};
43-
}
44-
45-
match byte_width {
46-
1 => filter_typed_records!(filter_buffer_byte_compress, 1),
47-
2 => filter_typed_records!(filter_buffer_byte_compress, 2),
48-
4 => filter_typed_records!(filter_buffer_byte_compress, 4),
49-
8 => filter_typed_records!(filter_buffer, 8),
50-
16 => filter_typed_records!(filter_buffer, 16),
51-
32 => filter_typed_records!(filter_buffer, 32),
52-
_ => {}
53-
}
54-
55-
match values.try_into_mut() {
56-
Ok(mut values) => {
57-
let mut destination = 0;
58-
mask.bit_buffer().for_each_set_index(|index| {
59-
let source = index * byte_width;
60-
values.copy_within(source..source + byte_width, destination);
61-
destination += byte_width;
62-
});
63-
values.truncate(destination);
64-
values.freeze().into_byte_buffer().aligned(alignment)
65-
}
66-
Err(values) => {
67-
let mut filtered = BufferMut::with_capacity(mask.true_count() * byte_width);
68-
mask.bit_buffer().for_each_set_index(|index| {
69-
let start = index * byte_width;
70-
filtered.extend_from_slice(&values[start..start + byte_width]);
71-
});
72-
filtered.freeze().into_byte_buffer().aligned(alignment)
40+
match_each_record_width!(
41+
byte_width,
42+
|W| {
43+
let records = Buffer::<[u8; W]>::from_byte_buffer(values);
44+
// Byte-compress processes eight records per mask byte and wins for narrow records,
45+
// while wider records move enough bytes each for the plain filter to win.
46+
let filtered = if W <= 4 {
47+
filter_buffer_byte_compress(records, mask)
48+
} else {
49+
filter_buffer(records, mask)
50+
};
51+
filtered.into_byte_buffer().aligned(alignment)
52+
},
53+
_ => {
54+
match values.try_into_mut() {
55+
Ok(mut values) => {
56+
let mut destination = 0;
57+
mask.bit_buffer().for_each_set_index(|index| {
58+
let source = index * byte_width;
59+
values.copy_within(source..source + byte_width, destination);
60+
destination += byte_width;
61+
});
62+
values.truncate(destination);
63+
values.freeze().into_byte_buffer().aligned(alignment)
64+
}
65+
Err(values) => {
66+
let mut filtered = BufferMut::with_capacity(mask.true_count() * byte_width);
67+
mask.bit_buffer().for_each_set_index(|index| {
68+
let start = index * byte_width;
69+
filtered.extend_from_slice(&values[start..start + byte_width]);
70+
});
71+
filtered.freeze().into_byte_buffer().aligned(alignment)
72+
}
73+
}
7374
}
74-
}
75+
)
7576
}

vortex-array/src/arrays/fixed_width/filter/tests.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use rstest::rstest;
5+
use vortex_buffer::Buffer;
56
use vortex_buffer::buffer;
7+
use vortex_mask::Mask;
68

9+
use super::filter_records;
710
use crate::ArrayRef;
811
use crate::IntoArray;
912
use crate::VortexSessionExecute;
@@ -17,9 +20,30 @@ use crate::dtype::DecimalDType;
1720
use crate::dtype::i256;
1821
use crate::validity::Validity;
1922

23+
#[test]
24+
fn filter_fallback_width_records() {
25+
let Mask::Values(mask) = Mask::from_iter([true, false, true, false]) else {
26+
panic!("a mixed mask must have mask values");
27+
};
28+
let expected = [0u8, 1, 2, 6, 7, 8];
29+
30+
// A uniquely owned buffer takes the in-place `copy_within` path.
31+
let owned = Buffer::from_iter(0u8..12);
32+
let filtered = filter_records(owned, 3, &mask);
33+
assert_eq!(filtered.as_slice(), &expected);
34+
35+
// Retaining a second reference forces the copying path instead.
36+
let shared = Buffer::from_iter(0u8..12);
37+
let _retained = shared.clone();
38+
let filtered = filter_records(shared, 3, &mask);
39+
assert_eq!(filtered.as_slice(), &expected);
40+
}
41+
2042
#[rstest]
2143
#[case::primitive_i8(PrimitiveArray::from_iter([-2i8, -1, 0, 1, 2]).into_array())]
44+
#[case::primitive_u16(PrimitiveArray::from_iter([1u16, 2, 3, 4, 5]).into_array())]
2245
#[case::primitive_i32(PrimitiveArray::from_iter([1i32, 2, 3, 4, 5]).into_array())]
46+
#[case::primitive_f32(PrimitiveArray::from_iter([0.1f32, 0.2, 0.3, 0.4, 0.5]).into_array())]
2347
#[case::primitive_nullable(PrimitiveArray::from_option_iter(
2448
[Some(1i64), None, Some(3), Some(4), None],
2549
).into_array())]

vortex-array/src/arrays/fixed_width/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,39 @@ pub(crate) mod take;
99
pub(crate) mod vtable;
1010

1111
pub(crate) use self::array::FixedWidthArray;
12+
pub(crate) use self::array::with_values;
13+
14+
/// Dispatches a runtime byte width to a compile-time `const $W: usize` for every record width
15+
/// with a dedicated fixed-width kernel, falling back to `$fallback` for any other width.
16+
macro_rules! match_each_record_width {
17+
($byte_width:expr, | $W:ident | $body:block,_ => $fallback:block) => {
18+
match $byte_width {
19+
1 => {
20+
const $W: usize = 1;
21+
$body
22+
}
23+
2 => {
24+
const $W: usize = 2;
25+
$body
26+
}
27+
4 => {
28+
const $W: usize = 4;
29+
$body
30+
}
31+
8 => {
32+
const $W: usize = 8;
33+
$body
34+
}
35+
16 => {
36+
const $W: usize = 16;
37+
$body
38+
}
39+
32 => {
40+
const $W: usize = 32;
41+
$body
42+
}
43+
_ => $fallback,
44+
}
45+
};
46+
}
47+
pub(crate) use match_each_record_width;

vortex-array/src/arrays/fixed_width/take/avx2/tests.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
#![cfg_attr(miri, ignore)]
5-
#![cfg(target_arch = "x86_64")]
4+
#![cfg(all(target_arch = "x86_64", not(miri)))]
65

76
use super::take_avx2;
87

vortex-array/src/arrays/fixed_width/take/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use self::scalar::take_values_scalar;
2222
use self::slices::take_slices;
2323
use self::slices::take_slices_constant_length;
2424
use super::FixedWidthArray;
25+
use super::with_values;
2526
use crate::ArrayRef;
2627
use crate::Columnar;
2728
use crate::ExecutionCtx;
@@ -144,7 +145,7 @@ pub(crate) fn take<V: FixedWidthArray>(
144145
)
145146
})?;
146147
Ok(Some(
147-
V::with_values(array, values, indices.len(), validity)?.into_array(),
148+
with_values(array, values, indices.len(), validity)?.into_array(),
148149
))
149150
}
150151

@@ -193,6 +194,6 @@ fn take_contiguous_ranges<V: FixedWidthArray>(
193194
}?;
194195
let validity = array.validity()?.take(indices_ref)?;
195196
Ok(Some(
196-
V::with_values(array, taken, output_len, validity)?.into_array(),
197+
with_values(array, taken, output_len, validity)?.into_array(),
197198
))
198199
}

vortex-array/src/arrays/fixed_width/take/records.rs

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use vortex_error::VortexResult;
88
use vortex_error::vortex_err;
99

1010
use super::take_values;
11+
use crate::arrays::fixed_width::match_each_record_width;
1112
use crate::dtype::UnsignedPType;
1213

1314
pub(super) fn take_byte_records<I: UnsignedPType>(
@@ -18,39 +19,31 @@ pub(super) fn take_byte_records<I: UnsignedPType>(
1819
) -> VortexResult<ByteBuffer> {
1920
let alignment = values.alignment();
2021

21-
macro_rules! take_typed_records {
22-
($width:expr) => {{
23-
let records = Buffer::<[u8; $width]>::from_byte_buffer(values.clone());
22+
match_each_record_width!(
23+
byte_width,
24+
|W| {
25+
let records = Buffer::<[u8; W]>::from_byte_buffer(values.clone());
2426
debug_assert_eq!(records.len(), record_count);
2527
Ok(take_values(records.as_slice(), indices)
2628
.into_byte_buffer()
2729
.aligned(alignment))
28-
}};
29-
}
30-
31-
match byte_width {
32-
1 => return take_typed_records!(1),
33-
2 => return take_typed_records!(2),
34-
4 => return take_typed_records!(4),
35-
8 => return take_typed_records!(8),
36-
16 => return take_typed_records!(16),
37-
32 => return take_typed_records!(32),
38-
_ => {}
39-
}
40-
41-
let output_len = indices
42-
.len()
43-
.checked_mul(byte_width)
44-
.ok_or_else(|| vortex_err!("Fixed-width take output length overflows usize"))?;
45-
let mut result = BufferMut::<u8>::with_capacity(output_len);
46-
for index in indices {
47-
let index = index.as_();
48-
assert!(
49-
index < record_count,
50-
"take index {index} out of bounds for length {record_count}"
51-
);
52-
let start = index * byte_width;
53-
result.extend_from_slice(&values[start..start + byte_width]);
54-
}
55-
Ok(result.freeze().into_byte_buffer().aligned(alignment))
30+
},
31+
_ => {
32+
let output_len = indices
33+
.len()
34+
.checked_mul(byte_width)
35+
.ok_or_else(|| vortex_err!("Fixed-width take output length overflows usize"))?;
36+
let mut result = BufferMut::<u8>::with_capacity(output_len);
37+
for index in indices {
38+
let index = index.as_();
39+
assert!(
40+
index < record_count,
41+
"take index {index} out of bounds for length {record_count}"
42+
);
43+
let start = index * byte_width;
44+
result.extend_from_slice(&values[start..start + byte_width]);
45+
}
46+
Ok(result.freeze().into_byte_buffer().aligned(alignment))
47+
}
48+
)
5649
}

vortex-array/src/arrays/fixed_width/take/tests.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ fn take_eight_byte_values() {
4545
#[case(8)]
4646
#[case(16)]
4747
#[case(32)]
48+
#[case::fallback(3)]
49+
#[case::fallback_wide(12)]
4850
fn take_runtime_width_records(#[case] byte_width: usize) -> VortexResult<()> {
4951
let values = Buffer::from_iter((0u8..).take(3 * byte_width));
5052
let expected = values[2 * byte_width..3 * byte_width]
@@ -57,6 +59,13 @@ fn take_runtime_width_records(#[case] byte_width: usize) -> VortexResult<()> {
5759
Ok(())
5860
}
5961

62+
#[test]
63+
#[should_panic(expected = "take index 3 out of bounds for length 3")]
64+
fn fallback_take_rejects_out_of_bounds_index() {
65+
let values = Buffer::from_iter((0u8..).take(9)).into_byte_buffer();
66+
drop(take_byte_records(&values, 3, 3, &[3u32]));
67+
}
68+
6069
#[test]
6170
fn take_variable_length_slices() -> VortexResult<()> {
6271
let values = buffer![10u8, 11, 12, 13, 14].into_byte_buffer();

0 commit comments

Comments
 (0)