11// SPDX-License-Identifier: Apache-2.0
22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
4- use num_traits:: AsPrimitive ;
54use num_traits:: NumCast ;
65use vortex_buffer:: Buffer ;
76use vortex_buffer:: BufferMut ;
8- use vortex_buffer:: try_map_with_mask;
7+ use vortex_buffer:: lane_ops_indexed:: try_map_no_validity;
8+ use vortex_buffer:: lane_ops_indexed:: try_map_with_mask;
99use vortex_error:: VortexResult ;
1010use vortex_error:: vortex_bail;
1111use vortex_error:: vortex_err;
@@ -103,56 +103,65 @@ impl CastKernel for Primitive {
103103 }
104104}
105105
106- /// Cast values from `F` to `T`. For infallible casts this is a pure pass; for fallible casts
107- /// each valid value goes through a checked `NumCast::from` and the kernel bails if any of them
108- /// overflow `T`. Invalid positions use the wrapping `as` cast since their values are masked out.
106+ /// Cast values from `F` to `T`. Always routes through the fallible lane-op kernels with
107+ /// `NumCast::from`. The kernel branches once on the mask shape:
108+ ///
109+ /// - `Mask::AllTrue` → [`try_map_no_validity`] — no per-lane validity work.
110+ /// - `Mask::AllFalse` → bulk zero — the closure is never invoked.
111+ /// - `Mask::Values` → [`try_map_with_mask`] — the closure neutralizes null lanes
112+ /// via the `* valid as F` multiply trick so out-of-range null-lane values don't
113+ /// trigger spurious errors.
114+ ///
115+ /// For statically-infallible casts (e.g. widening) LLVM proves `NumCast::from` always
116+ /// returns `Some` and strips the fail-tracking machinery, generating the same bare
117+ /// `ushll` widen loop the old hand-written `as_()` fast path produced.
109118fn cast_values < F , T > (
110119 array : ArrayView < ' _ , Primitive > ,
111120 new_validity : Validity ,
112121 ctx : & mut ExecutionCtx ,
113122) -> VortexResult < ArrayRef >
114123where
115- F : NativePType + AsPrimitive < T > ,
124+ F : NativePType ,
116125 T : NativePType ,
117126{
118127 let values = array. as_slice :: < F > ( ) ;
119-
120- // Fast path: statically infallible, or cached min/max prove every valid value fits in `T`.
121- // The cached check never triggers a stats computation — if the bounds aren't already known
122- // we fall through to the per-lane loop below.
123- if values_always_fit ( F :: PTYPE , T :: PTYPE ) || values_fit_in ( array, T :: PTYPE , ctx, false ) {
124- return Ok ( PrimitiveArray :: new ( cast :: < F , T > ( values) , new_validity) . into_array ( ) ) ;
125- }
126-
127- // TODO(joe): if the values source and target have the same bit-width we can
128- // mutate in place.
129-
130- // Fallible: invalid lanes are pre-multiplied to zero so the checked cast always succeeds for
131- // them; valid lanes go through `NumCast::from` and the whole cast bails on the first overflow.
132128 let mask = array. validity ( ) ?. execute_mask ( array. len ( ) , ctx) ?;
133129 let overflow = || {
134130 vortex_err ! (
135131 Compute : "Cannot cast {} to {} — value exceeds target range" ,
136132 F :: PTYPE , T :: PTYPE ,
137133 )
138134 } ;
135+
139136 let buffer: Buffer < T > = match & mask {
140- Mask :: AllTrue ( _) => BufferMut :: try_from_trusted_len_iter (
141- values
142- . iter ( )
143- . map ( |& v| <T as NumCast >:: from ( v) . ok_or_else ( overflow) ) ,
144- ) ?
145- . freeze ( ) ,
137+ Mask :: AllTrue ( _) => {
138+ let mut buffer = BufferMut :: < T > :: with_capacity ( values. len ( ) ) ;
139+ try_map_no_validity (
140+ values,
141+ & mut buffer. spare_capacity_mut ( ) [ ..values. len ( ) ] ,
142+ |v| <T as NumCast >:: from ( v) ,
143+ )
144+ . map_err ( |_| overflow ( ) ) ?;
145+ // SAFETY: try_map_no_validity returned Ok, so it initialized every lane.
146+ unsafe { buffer. set_len ( values. len ( ) ) } ;
147+ buffer. freeze ( )
148+ }
146149 Mask :: AllFalse ( _) => BufferMut :: < T > :: zeroed ( values. len ( ) ) . freeze ( ) ,
147150 Mask :: Values ( m) => {
148151 let mut buffer = BufferMut :: < T > :: with_capacity ( values. len ( ) ) ;
149152 try_map_with_mask (
150153 values,
151154 m. bit_buffer ( ) ,
152155 & mut buffer. spare_capacity_mut ( ) [ ..values. len ( ) ] ,
156+ // Lazy validity: only consult `valid` on the failure branch. For
157+ // widening / statically-infallible casts, `NumCast::from` is always
158+ // `Some` so the `or_else` is provably dead — LLVM DCEs the validity
159+ // path entirely, giving the same codegen as the maskless kernel.
160+ // For narrowing, `valid` is only read at lanes that actually
161+ // overflowed (a cold check on top of the cast).
153162 |v, valid| {
154- let factor = if valid { F :: one ( ) } else { F :: zero ( ) } ;
155- < T as NumCast > :: from ( v * factor )
163+ < T as NumCast > :: from ( v )
164+ . or_else ( || ( !valid ) . then ( T :: zero ) )
156165 } ,
157166 )
158167 . map_err ( |_| overflow ( ) ) ?;
@@ -165,12 +174,6 @@ where
165174 Ok ( PrimitiveArray :: new ( buffer, new_validity) . into_array ( ) )
166175}
167176
168- /// Out-of-range values at invalid positions are truncated/wrapped by `as`, which is fine because
169- /// they are masked out by validity.
170- fn cast < F : NativePType + AsPrimitive < T > , T : NativePType > ( array : & [ F ] ) -> Buffer < T > {
171- BufferMut :: from_trusted_len_iter ( array. iter ( ) . map ( |& src| src. as_ ( ) ) ) . freeze ( )
172- }
173-
174177fn reinterpret (
175178 array : ArrayView < ' _ , Primitive > ,
176179 new_ptype : PType ,
@@ -188,23 +191,6 @@ fn reinterpret(
188191 . into_array ( )
189192}
190193
191- /// Returns `true` if every value of `src` is guaranteed representable in `target` without
192- /// overflow. Precision may be lost (e.g. large integers cast to `f32`), but the cast can never
193- /// produce an out-of-range result.
194- fn values_always_fit ( src : PType , target : PType ) -> bool {
195- if src == target {
196- return true ;
197- }
198- if src. is_int ( ) && target. is_int ( ) {
199- return target. byte_width ( ) > src. byte_width ( )
200- && ( src. is_unsigned_int ( ) || target. is_signed_int ( ) ) ;
201- }
202- if src. is_float ( ) && target. is_float ( ) {
203- return target. byte_width ( ) > src. byte_width ( ) ;
204- }
205- src. is_int ( ) && matches ! ( target, PType :: F32 | PType :: F64 )
206- }
207-
208194/// Returns `true` if all valid values in `array` are representable as `target_ptype`.
209195///
210196/// Cached min/max statistics are consulted first. If either bound is missing, the function either
0 commit comments