1use super::flow::FlowTransform;
8use super::{BoxStream, Flow, FlowHints, Source, StreamError, StreamResult};
9use std::sync::Arc;
10
11pub(crate) const SCALAR_CHUNK_SIZE: usize = 128;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ScalarArithmeticOp {
16 Add,
17 Subtract,
18 Multiply,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum ScalarCompareOp {
24 Equal,
25 NotEqual,
26 LessThan,
27 LessOrEqual,
28 GreaterThan,
29 GreaterOrEqual,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub(crate) enum ScalarI64Op {
34 Arithmetic(ScalarArithmeticOp),
35 Compare(ScalarCompareOp),
36}
37
38impl ScalarI64Op {
39 fn apply_checked(self, value: i64, operand: i64) -> StreamResult<Option<i64>> {
40 match self {
41 Self::Arithmetic(ScalarArithmeticOp::Add) => value
42 .checked_add(operand)
43 .map(Some)
44 .ok_or_else(|| map_overflow("add", value, "+", operand)),
45 Self::Arithmetic(ScalarArithmeticOp::Subtract) => value
46 .checked_sub(operand)
47 .map(Some)
48 .ok_or_else(|| map_overflow("subtract", value, "-", operand)),
49 Self::Arithmetic(ScalarArithmeticOp::Multiply) => value
50 .checked_mul(operand)
51 .map(Some)
52 .ok_or_else(|| map_overflow("multiply", value, "*", operand)),
53 Self::Compare(compare) => Ok(compare_i64(compare, value, operand).then_some(value)),
54 }
55 }
56}
57
58fn map_overflow(name: &str, lhs: i64, symbol: &str, rhs: i64) -> StreamError {
59 StreamError::Failed(format!(
60 "integer overflow in map_{name}: {lhs} {symbol} {rhs}"
61 ))
62}
63
64fn compare_i64(op: ScalarCompareOp, lhs: i64, rhs: i64) -> bool {
65 match op {
66 ScalarCompareOp::Equal => lhs == rhs,
67 ScalarCompareOp::NotEqual => lhs != rhs,
68 ScalarCompareOp::LessThan => lhs < rhs,
69 ScalarCompareOp::LessOrEqual => lhs <= rhs,
70 ScalarCompareOp::GreaterThan => lhs > rhs,
71 ScalarCompareOp::GreaterOrEqual => lhs >= rhs,
72 }
73}
74
75pub(crate) struct ScalarChunkStep<T> {
81 op: ScalarI64Op,
82 operand: T,
83 clone_value: fn(&T) -> T,
84 copy_chunk: fn(&[T], &mut Vec<T>),
85 checked: fn(ScalarI64Op, T, T) -> StreamResult<Option<T>>,
86 map_chunk: fn(ScalarArithmeticOp, &mut [T], T) -> bool,
87 compare_chunk: fn(ScalarCompareOp, &[T], &mut [u8], T),
88}
89
90impl ScalarChunkStep<i64> {
91 pub(crate) fn new(op: ScalarI64Op, operand: i64) -> Self {
92 Self {
93 op,
94 operand,
95 clone_value: |value| *value,
96 copy_chunk: |input, output| output.extend_from_slice(input),
97 checked: |op, value, operand| op.apply_checked(value, operand),
98 map_chunk: map_i64_chunk,
99 compare_chunk: compare_i64_chunk,
100 }
101 }
102}
103
104impl<T> Clone for ScalarChunkStep<T> {
105 fn clone(&self) -> Self {
106 Self {
107 op: self.op,
108 operand: (self.clone_value)(&self.operand),
109 clone_value: self.clone_value,
110 copy_chunk: self.copy_chunk,
111 checked: self.checked,
112 map_chunk: self.map_chunk,
113 compare_chunk: self.compare_chunk,
114 }
115 }
116}
117
118impl<T> ScalarChunkStep<T> {
119 pub(crate) fn apply_checked(&self, value: T) -> StreamResult<Option<T>> {
120 (self.checked)(self.op, value, (self.clone_value)(&self.operand))
121 }
122
123 fn is_multiply(&self) -> bool {
124 self.op == ScalarI64Op::Arithmetic(ScalarArithmeticOp::Multiply)
125 }
126
127 pub(crate) fn is_add_or_subtract(&self) -> bool {
128 matches!(
129 self.op,
130 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Add | ScalarArithmeticOp::Subtract)
131 )
132 }
133}
134
135pub(crate) struct ScalarChunkProcessor<T> {
142 original: Vec<T>,
143 values: Vec<T>,
144 compact: Vec<T>,
145 mask: Vec<u8>,
146}
147
148pub(crate) struct ScalarChunkResult {
149 pub(crate) failure: Option<StreamError>,
150}
151
152impl<T> ScalarChunkProcessor<T> {
153 pub(crate) fn new() -> Self {
154 Self {
155 original: Vec::with_capacity(SCALAR_CHUNK_SIZE),
156 values: Vec::with_capacity(SCALAR_CHUNK_SIZE),
157 compact: Vec::with_capacity(SCALAR_CHUNK_SIZE),
158 mask: Vec::with_capacity(SCALAR_CHUNK_SIZE),
159 }
160 }
161
162 pub(crate) fn load_from(&mut self, input: &mut dyn Iterator<Item = T>) -> usize {
163 self.original.clear();
164 self.original.extend(input.take(SCALAR_CHUNK_SIZE));
165 self.original.len()
166 }
167
168 pub(crate) fn output(&self) -> &[T] {
169 &self.values
170 }
171
172 pub(crate) fn drain_output(&mut self) -> std::vec::Drain<'_, T> {
173 self.values.drain(..)
174 }
175
176 pub(crate) fn process(&mut self, steps: &[ScalarChunkStep<T>]) -> ScalarChunkResult {
177 assert!(
178 !steps.is_empty(),
179 "scalar chunk plan must contain an operator"
180 );
181 if steps.iter().any(ScalarChunkStep::is_multiply) {
182 return self.scalar_replay(steps);
183 }
184
185 self.values.clear();
186 (steps[0].copy_chunk)(&self.original, &mut self.values);
187 let mut overflow = false;
188
189 for step in steps {
190 match step.op {
191 ScalarI64Op::Arithmetic(op) => {
192 overflow |=
193 (step.map_chunk)(op, &mut self.values, (step.clone_value)(&step.operand));
194 }
195 ScalarI64Op::Compare(op) => {
196 self.mask.resize(self.values.len(), 0);
197 (step.compare_chunk)(
198 op,
199 &self.values,
200 &mut self.mask,
201 (step.clone_value)(&step.operand),
202 );
203 self.compact.clear();
204 for (value, &keep) in self.values.iter().zip(&self.mask) {
205 if keep != 0 {
206 self.compact.push((step.clone_value)(value));
207 }
208 }
209 std::mem::swap(&mut self.values, &mut self.compact);
210 }
211 }
212 }
213
214 if overflow {
215 self.scalar_replay(steps)
216 } else {
217 ScalarChunkResult { failure: None }
218 }
219 }
220
221 fn scalar_replay(&mut self, steps: &[ScalarChunkStep<T>]) -> ScalarChunkResult {
222 self.values.clear();
223 for input in self
224 .original
225 .iter()
226 .map(|value| (steps[0].clone_value)(value))
227 {
228 let mut value = Some(input);
229 for step in steps {
230 let Some(current) = value else {
231 break;
232 };
233 match step.apply_checked(current) {
234 Ok(next) => value = next,
235 Err(error) => {
236 return ScalarChunkResult {
237 failure: Some(error),
238 };
239 }
240 }
241 }
242 if let Some(value) = value {
243 self.values.push(value);
244 }
245 }
246 ScalarChunkResult { failure: None }
247 }
248}
249
250#[inline(never)]
251fn add_i64_chunk(values: &mut [i64], operand: i64) -> bool {
252 let mut overflow_bits = 0_u64;
253 for value in values {
254 let lhs = *value;
255 let result = lhs.wrapping_add(operand);
256 *value = result;
257 overflow_bits |= ((lhs ^ result) & (operand ^ result)) as u64;
258 }
259 overflow_bits & (1_u64 << 63) != 0
260}
261
262#[inline(never)]
263fn subtract_i64_chunk(values: &mut [i64], operand: i64) -> bool {
264 let mut overflow_bits = 0_u64;
265 for value in values {
266 let lhs = *value;
267 let result = lhs.wrapping_sub(operand);
268 *value = result;
269 overflow_bits |= ((lhs ^ operand) & (lhs ^ result)) as u64;
270 }
271 overflow_bits & (1_u64 << 63) != 0
272}
273
274fn map_i64_chunk(op: ScalarArithmeticOp, values: &mut [i64], operand: i64) -> bool {
275 match op {
276 ScalarArithmeticOp::Add => add_i64_chunk(values, operand),
277 ScalarArithmeticOp::Subtract => subtract_i64_chunk(values, operand),
278 ScalarArithmeticOp::Multiply => true,
281 }
282}
283
284#[inline(never)]
285fn compare_i64_chunk(op: ScalarCompareOp, values: &[i64], mask: &mut [u8], operand: i64) {
286 assert_eq!(values.len(), mask.len());
287 for (value, keep) in values.iter().zip(mask) {
288 *keep = u8::from(compare_i64(op, *value, operand));
289 }
290}
291
292#[doc(hidden)]
294#[inline(never)]
295pub fn scalar_add_codegen_probe(values: &mut [i64], operand: i64) -> bool {
296 add_i64_chunk(values, operand)
297}
298
299#[doc(hidden)]
301#[inline(never)]
302pub fn scalar_subtract_codegen_probe(values: &mut [i64], operand: i64) -> bool {
303 subtract_i64_chunk(values, operand)
304}
305
306#[doc(hidden)]
308#[inline(never)]
309pub fn scalar_compare_codegen_probe(values: &[i64], mask: &mut [u8], operand: i64) {
310 compare_i64_chunk(ScalarCompareOp::GreaterThan, values, mask, operand);
311}
312
313struct ScalarI64Stream {
314 input: BoxStream<i64>,
315 steps: Arc<[ScalarChunkStep<i64>]>,
316 processor: Option<ScalarChunkProcessor<i64>>,
317 output_index: usize,
318 pending_upstream_error: Option<StreamError>,
319 pending_failure: Option<StreamError>,
320 upstream_done: bool,
321 chunked: bool,
322}
323
324impl ScalarI64Stream {
325 fn new(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> Self {
326 let chunked = steps.iter().any(ScalarChunkStep::is_add_or_subtract);
327 Self {
328 input,
329 steps,
330 processor: chunked.then(ScalarChunkProcessor::new),
331 output_index: 0,
332 pending_upstream_error: None,
333 pending_failure: None,
334 upstream_done: false,
335 chunked,
336 }
337 }
338
339 fn new_scalar(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> Self {
340 Self {
341 input,
342 steps,
343 processor: None,
344 output_index: 0,
345 pending_upstream_error: None,
346 pending_failure: None,
347 upstream_done: false,
348 chunked: false,
349 }
350 }
351
352 fn next_scalar(&mut self) -> Option<StreamResult<i64>> {
353 loop {
354 let item = self.input.next()?;
355 let mut value = match item {
356 Ok(value) => Some(value),
357 Err(error) => return Some(Err(error)),
358 };
359 for step in self.steps.iter() {
360 let Some(current) = value else {
361 break;
362 };
363 match step.op.apply_checked(current, step.operand) {
364 Ok(next) => value = next,
365 Err(error) => return Some(Err(error)),
366 }
367 }
368 if let Some(value) = value {
369 return Some(Ok(value));
370 }
371 }
372 }
373
374 fn fill_chunk(&mut self) {
375 let mut input = self.input.by_ref().map_while(|item| match item {
376 Ok(value) => Some(value),
377 Err(error) => {
378 self.pending_upstream_error = Some(error);
379 self.upstream_done = true;
380 None
381 }
382 });
383 let processor = self
384 .processor
385 .as_mut()
386 .expect("chunk processor exists for add/subtract segments");
387 let loaded = processor.load_from(&mut input);
388 if loaded < SCALAR_CHUNK_SIZE && self.pending_upstream_error.is_none() {
389 self.upstream_done = true;
390 }
391 let result = processor.process(&self.steps);
392 self.output_index = 0;
393 self.pending_failure = result.failure;
394 }
395}
396
397impl Iterator for ScalarI64Stream {
398 type Item = StreamResult<i64>;
399
400 fn next(&mut self) -> Option<Self::Item> {
401 if !self.chunked {
402 return self.next_scalar();
403 }
404 loop {
405 if let Some(&value) = self
406 .processor
407 .as_ref()
408 .expect("chunk processor exists for add/subtract segments")
409 .output()
410 .get(self.output_index)
411 {
412 self.output_index += 1;
413 return Some(Ok(value));
414 }
415 if let Some(error) = self.pending_failure.take() {
416 self.upstream_done = true;
417 return Some(Err(error));
418 }
419 if let Some(error) = self.pending_upstream_error.take() {
420 return Some(Err(error));
421 }
422 if self.upstream_done {
423 return None;
424 }
425 self.fill_chunk();
426 }
427 }
428}
429
430fn filter_i64_stream<F>(input: BoxStream<i64>, predicate: F) -> BoxStream<i64>
431where
432 F: Fn(i64) -> bool + Send + Sync + 'static,
433{
434 Box::new(input.filter_map(move |item| match item {
435 Ok(value) if predicate(value) => Some(Ok(value)),
436 Ok(_) => None,
437 Err(error) => Some(Err(error)),
438 }))
439}
440
441fn scalar_i64_stream(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> BoxStream<i64> {
442 if let [step] = steps.as_ref() {
443 let operand = step.operand;
444 return match step.op {
445 ScalarI64Op::Compare(ScalarCompareOp::Equal) => {
446 filter_i64_stream(input, move |value| value == operand)
447 }
448 ScalarI64Op::Compare(ScalarCompareOp::NotEqual) => {
449 filter_i64_stream(input, move |value| value != operand)
450 }
451 ScalarI64Op::Compare(ScalarCompareOp::LessThan) => {
452 filter_i64_stream(input, move |value| value < operand)
453 }
454 ScalarI64Op::Compare(ScalarCompareOp::LessOrEqual) => {
455 filter_i64_stream(input, move |value| value <= operand)
456 }
457 ScalarI64Op::Compare(ScalarCompareOp::GreaterThan) => {
458 filter_i64_stream(input, move |value| value > operand)
459 }
460 ScalarI64Op::Compare(ScalarCompareOp::GreaterOrEqual) => {
461 filter_i64_stream(input, move |value| value >= operand)
462 }
463 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Multiply) => {
464 Box::new(input.map(move |item| {
465 item.and_then(|value| {
466 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Multiply)
467 .apply_checked(value, operand)
468 .map(|value| value.expect("multiplication retains its input"))
469 })
470 }))
471 }
472 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Add | ScalarArithmeticOp::Subtract) => {
473 Box::new(ScalarI64Stream::new(input, steps))
474 }
475 };
476 }
477 Box::new(ScalarI64Stream::new(input, steps))
478}
479
480pub(crate) struct ScalarI64SourceFactory<Mat> {
481 source: Arc<dyn super::SourceFactory<i64, Mat>>,
482 steps: Arc<[ScalarChunkStep<i64>]>,
483}
484
485impl<Mat> ScalarI64SourceFactory<Mat> {
486 fn append(&self, next: &[ScalarChunkStep<i64>]) -> Arc<dyn super::SourceFactory<i64, Mat>>
487 where
488 Mat: Send + 'static,
489 {
490 let mut steps = Vec::with_capacity(self.steps.len() + next.len());
491 steps.extend_from_slice(&self.steps);
492 steps.extend_from_slice(next);
493 Arc::new(Self {
494 source: Arc::clone(&self.source),
495 steps: steps.into(),
496 })
497 }
498}
499
500impl<Mat> super::SourceFactory<i64, Mat> for ScalarI64SourceFactory<Mat>
501where
502 Mat: Send + 'static,
503{
504 fn create(
505 self: Arc<Self>,
506 materializer: &super::Materializer,
507 ) -> StreamResult<(BoxStream<i64>, Mat)> {
508 let (input, mat) = Arc::clone(&self.source).create(materializer)?;
509 Ok((scalar_i64_stream(input, Arc::clone(&self.steps)), mat))
510 }
511
512 fn append_scalar_i64(
513 self: Arc<Self>,
514 steps: &[ScalarChunkStep<i64>],
515 ) -> Option<Arc<dyn super::SourceFactory<i64, Mat>>> {
516 Some(self.append(steps))
517 }
518}
519
520fn steps_with(next: ScalarChunkStep<i64>) -> Arc<[ScalarChunkStep<i64>]> {
521 Arc::from([next])
522}
523
524impl<Mat> Source<i64, Mat>
525where
526 Mat: Send + 'static,
527{
528 fn scalar_op(self, op: ScalarI64Op, operand: i64) -> Self {
529 let steps = steps_with(ScalarChunkStep::new(op, operand));
530 if let Some(factory) = Arc::clone(&self.factory).append_scalar_i64(&steps) {
531 return Source {
532 factory,
533 terminal_factory: None,
534 hints: self.hints.without_inline_micro(),
535 attributes: self.attributes,
536 split_hook: None,
537 };
538 }
539 if self.hints.inline_micro.is_none() {
540 return match op {
541 ScalarI64Op::Arithmetic(_) => self.try_map(move |value| {
542 op.apply_checked(value, operand)
543 .map(|value| value.expect("arithmetic maps retain their input"))
544 }),
545 ScalarI64Op::Compare(compare) => match compare {
546 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
547 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
548 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
549 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
550 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
551 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
552 },
553 };
554 }
555 let Source {
556 factory,
557 terminal_factory: _,
558 hints,
559 attributes,
560 split_hook: _,
561 } = self;
562 let next_factory = Arc::new(ScalarI64SourceFactory {
563 source: factory,
564 steps,
565 });
566 Source {
567 factory: next_factory,
568 terminal_factory: None,
569 hints: hints.without_inline_micro(),
570 attributes,
571 split_hook: None,
572 }
573 }
574
575 #[must_use]
577 pub fn map_op(self, op: ScalarArithmeticOp, operand: i64) -> Self {
578 self.scalar_op(ScalarI64Op::Arithmetic(op), operand)
579 }
580
581 #[must_use]
582 pub fn map_add(self, operand: i64) -> Self {
583 self.map_op(ScalarArithmeticOp::Add, operand)
584 }
585
586 #[must_use]
587 pub fn map_subtract(self, operand: i64) -> Self {
588 self.map_op(ScalarArithmeticOp::Subtract, operand)
589 }
590
591 #[must_use]
592 pub fn map_multiply(self, operand: i64) -> Self {
593 self.map_op(ScalarArithmeticOp::Multiply, operand)
594 }
595
596 #[must_use]
598 pub fn filter_op(self, op: ScalarCompareOp, operand: i64) -> Self {
599 let step = ScalarChunkStep::new(ScalarI64Op::Compare(op), operand);
600 let steps = steps_with(step);
601 if let Some(factory) = Arc::clone(&self.factory).append_scalar_i64(&steps) {
602 return Source {
603 factory,
604 terminal_factory: None,
605 hints: self.hints.without_inline_micro(),
606 attributes: self.attributes,
607 split_hook: None,
608 };
609 }
610 if self.hints.inline_micro.is_some() {
611 return Source {
612 factory: Arc::new(ScalarI64SourceFactory {
613 source: self.factory,
614 steps,
615 }),
616 terminal_factory: None,
617 hints: self.hints.without_inline_micro(),
618 attributes: self.attributes,
619 split_hook: None,
620 };
621 }
622 match op {
623 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
624 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
625 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
626 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
627 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
628 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
629 }
630 }
631}
632
633pub(super) struct ScalarI64FlowPlan<In> {
634 pub(super) prefix: super::PureTransform<In, i64>,
635 pub(super) steps: Arc<[ScalarChunkStep<i64>]>,
636}
637
638impl<In> Clone for ScalarI64FlowPlan<In> {
639 fn clone(&self) -> Self {
640 Self {
641 prefix: Arc::clone(&self.prefix),
642 steps: Arc::clone(&self.steps),
643 }
644 }
645}
646
647fn scalar_flow_transform<In: Send + 'static>(
648 prefix: super::PureTransform<In, i64>,
649 steps: Arc<[ScalarChunkStep<i64>]>,
650) -> super::PureTransform<In, i64> {
651 Arc::new(move |input| {
652 let input = prefix(input);
653 scalar_i64_stream(input, Arc::clone(&steps))
654 })
655}
656
657fn scalar_flow_fallback_transform<In: Send + 'static>(
658 prefix: super::PureTransform<In, i64>,
659 steps: Arc<[ScalarChunkStep<i64>]>,
660) -> super::PureTransform<In, i64> {
661 Arc::new(move |input| {
662 Box::new(ScalarI64Stream::new_scalar(
663 prefix(input),
664 Arc::clone(&steps),
665 ))
666 })
667}
668
669impl<In, Mat> Flow<In, i64, Mat>
670where
671 In: Send + 'static,
672 Mat: Send + 'static,
673{
674 fn scalar_op(self, op: ScalarI64Op, operand: i64) -> Self {
675 if self.scalar_i64.is_none() && !self.hints.scalar_chunk_prefix {
676 return match op {
677 ScalarI64Op::Arithmetic(_) => self.try_map(move |value| {
678 op.apply_checked(value, operand)
679 .map(|value| value.expect("arithmetic maps retain their input"))
680 }),
681 ScalarI64Op::Compare(compare) => match compare {
682 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
683 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
684 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
685 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
686 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
687 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
688 },
689 };
690 }
691 let Flow {
692 transform,
693 materialize,
694 hints,
695 attributes,
696 scalar_i64,
697 scalar_i64_fallback,
698 } = self;
699 let next = ScalarChunkStep::new(op, operand);
700 match (transform, scalar_i64) {
701 (FlowTransform::Pure(_), Some(plan)) => {
702 let plan = match Arc::downcast::<ScalarI64FlowPlan<In>>(plan) {
703 Ok(plan) => plan,
704 Err(_) => unreachable!("scalar i64 flow plan input type is preserved"),
705 };
706 let mut steps = Vec::with_capacity(plan.steps.len() + 1);
707 steps.extend_from_slice(&plan.steps);
708 steps.push(next.clone());
709 let steps: Arc<[ScalarChunkStep<i64>]> = steps.into();
710 let transform = scalar_flow_transform(Arc::clone(&plan.prefix), Arc::clone(&steps));
711 let fallback_prefix = scalar_i64_fallback.unwrap_or_else(|| {
712 scalar_flow_fallback_transform(
713 Arc::clone(&plan.prefix),
714 Arc::clone(&plan.steps),
715 )
716 });
717 let fallback = scalar_flow_fallback_transform(fallback_prefix, steps_with(next));
718 Flow {
719 transform: FlowTransform::Pure(transform),
720 materialize,
721 hints,
722 attributes,
723 scalar_i64: Some(Arc::new(ScalarI64FlowPlan {
724 prefix: Arc::clone(&plan.prefix),
725 steps,
726 })),
727 scalar_i64_fallback: Some(fallback),
728 }
729 }
730 (FlowTransform::Pure(prefix), None) => {
731 let steps = steps_with(next);
732 let transform = scalar_flow_transform(Arc::clone(&prefix), Arc::clone(&steps));
733 let fallback_prefix = scalar_i64_fallback.unwrap_or_else(|| Arc::clone(&prefix));
734 let fallback = scalar_flow_fallback_transform(fallback_prefix, Arc::clone(&steps));
735 Flow {
736 transform: FlowTransform::Pure(transform),
737 materialize,
738 hints,
739 attributes,
740 scalar_i64: Some(Arc::new(ScalarI64FlowPlan { prefix, steps })),
741 scalar_i64_fallback: Some(fallback),
742 }
743 }
744 (FlowTransform::Runtime(runtime), _) => {
745 let steps = steps_with(next);
746 let scalar =
747 move |input: BoxStream<i64>| scalar_i64_stream(input, Arc::clone(&steps));
748 Flow {
749 transform: FlowTransform::Runtime(Arc::new(move |input, materializer| {
750 runtime(input, materializer).map(&scalar)
751 })),
752 materialize,
753 hints: FlowHints::default(),
754 attributes,
755 scalar_i64: None,
756 scalar_i64_fallback: None,
757 }
758 }
759 }
760 }
761
762 #[must_use]
763 pub fn map_op(self, op: ScalarArithmeticOp, operand: i64) -> Self {
764 self.scalar_op(ScalarI64Op::Arithmetic(op), operand)
765 }
766
767 #[must_use]
768 pub fn map_add(self, operand: i64) -> Self {
769 self.map_op(ScalarArithmeticOp::Add, operand)
770 }
771
772 #[must_use]
773 pub fn map_subtract(self, operand: i64) -> Self {
774 self.map_op(ScalarArithmeticOp::Subtract, operand)
775 }
776
777 #[must_use]
778 pub fn map_multiply(self, operand: i64) -> Self {
779 self.map_op(ScalarArithmeticOp::Multiply, operand)
780 }
781
782 #[must_use]
783 pub fn filter_op(self, op: ScalarCompareOp, operand: i64) -> Self {
784 if self.scalar_i64.is_some() || self.hints.scalar_chunk_prefix {
785 return self.scalar_op(ScalarI64Op::Compare(op), operand);
786 }
787 match op {
788 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
789 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
790 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
791 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
792 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
793 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
794 }
795 }
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use crate::{Flow, Sink};
802 use std::sync::{Arc, Mutex};
803
804 #[test]
805 fn first_failure_stage_and_valid_prefix_are_exact() {
806 let emitted = Arc::new(Mutex::new(Vec::new()));
807 let observed = Arc::clone(&emitted);
808 let completion = Source::from_iter([0, i64::MIN + 5, i64::MAX])
809 .map_add(1)
810 .map_subtract(10)
811 .run_with(Sink::foreach(move |value| {
812 observed
813 .lock()
814 .expect("emitted values poisoned")
815 .push(value);
816 }))
817 .unwrap();
818
819 assert_eq!(
820 completion.wait(),
821 Err(StreamError::Failed(format!(
822 "integer overflow in map_subtract: {} - 10",
823 i64::MIN + 6
824 )))
825 );
826 assert_eq!(*emitted.lock().unwrap(), vec![-9]);
827 }
828
829 #[test]
830 fn overflow_positions_around_chunk_boundary_match_scalar_oracle() {
831 for len in [0, 1, 2, 3, 127, 128, 129, 255, 256, 257] {
832 let mut input = vec![0_i64; len];
833 if let Some(last) = input.last_mut() {
834 *last = i64::MAX;
835 }
836 let chunked = Source::from_iter(input.clone())
837 .map_add(1)
838 .run_with(Sink::collect())
839 .unwrap()
840 .wait();
841 let scalar = Source::from_iter(input)
842 .try_map(|value| {
843 value.checked_add(1).ok_or_else(|| {
844 StreamError::Failed(format!("integer overflow in map_add: {value} + 1"))
845 })
846 })
847 .run_with(Sink::collect())
848 .unwrap()
849 .wait();
850 assert_eq!(chunked, scalar, "length {len}");
851 }
852 }
853
854 #[test]
855 fn filters_compact_stably_before_and_after_fallible_maps() {
856 let result = Source::from_iter(-300_i64..300)
857 .filter_op(ScalarCompareOp::GreaterOrEqual, -250)
858 .map_add(7)
859 .filter_op(ScalarCompareOp::LessThan, 10)
860 .run_with(Sink::collect())
861 .unwrap()
862 .wait()
863 .unwrap();
864 let expected: Vec<_> = (-300_i64..300)
865 .filter(|value| *value >= -250)
866 .map(|value| value + 7)
867 .filter(|value| *value < 10)
868 .collect();
869 assert_eq!(result, expected);
870 }
871
872 #[test]
873 fn flow_builtins_fuse_but_opaque_closures_remain_per_element() {
874 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
875 let observed = Arc::clone(&calls);
876 let flow = Flow::identity()
877 .map(move |value: i64| {
878 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
879 value
880 })
881 .map_add(2)
882 .map_subtract(1);
883 let output = Source::from_iter(0_i64..600)
884 .via(flow)
885 .run_with(Sink::collect())
886 .unwrap()
887 .wait()
888 .unwrap();
889 assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 600);
890 assert_eq!(output[0], 1);
891 assert_eq!(output[599], 600);
892
893 let direct_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
894 let observed = Arc::clone(&direct_calls);
895 let head = Source::from_iter(0_i64..600)
896 .map(move |value| {
897 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
898 value
899 })
900 .map_add(1)
901 .run_with(Sink::head())
902 .unwrap()
903 .wait()
904 .unwrap();
905 assert_eq!(head, 1);
906 assert_eq!(
907 direct_calls.load(std::sync::atomic::Ordering::Relaxed),
908 1,
909 "an opaque prefix must not be pulled ahead by the chunk executor"
910 );
911
912 let flow_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
913 let observed = Arc::clone(&flow_calls);
914 let flow = Flow::identity()
915 .map(move |value: i64| {
916 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
917 value
918 })
919 .map_add(1);
920 let head = Source::from_iter(0_i64..600)
921 .via(flow)
922 .run_with(Sink::head())
923 .unwrap()
924 .wait()
925 .unwrap();
926 assert_eq!(head, 1);
927 assert_eq!(
928 flow_calls.load(std::sync::atomic::Ordering::Relaxed),
929 1,
930 "an opaque Flow prefix must remain one-element-at-a-time"
931 );
932
933 let dynamic_pulls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
934 let observed = Arc::clone(&dynamic_pulls);
935 let flow = Flow::identity()
936 .map_add(1)
937 .via(Flow::identity())
938 .map_subtract(1)
939 .map(|value| value);
940 let head = Source::unfold(0_i64, move |value| {
941 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
942 Some((value + 1, value))
943 })
944 .via(flow)
945 .run_with(Sink::head())
946 .unwrap()
947 .wait()
948 .unwrap();
949 assert_eq!(head, 0);
950 assert_eq!(
951 dynamic_pulls.load(std::sync::atomic::Ordering::Relaxed),
952 1,
953 "a dynamic source must select the scalar fallback for a chunked Flow"
954 );
955
956 let runtime_pulls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
957 let observed = Arc::clone(&runtime_pulls);
958 let flow = Flow::identity()
959 .map_add(1)
960 .via(Flow::from_runtime_transform(|input, _materializer| {
961 Ok(input)
962 }));
963 let head = Source::unfold(0_i64, move |value| {
964 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
965 Some((value + 1, value))
966 })
967 .via(flow)
968 .run_with(Sink::head())
969 .unwrap()
970 .wait()
971 .unwrap();
972 assert_eq!(head, 1);
973 assert_eq!(
974 runtime_pulls.load(std::sync::atomic::Ordering::Relaxed),
975 1,
976 "runtime Flow composition must scalarize an earlier chunked segment"
977 );
978 }
979
980 #[test]
981 fn multiplication_remains_checked_scalar() {
982 let result = Source::from_iter([2_i64, i64::MAX])
983 .map_multiply(2)
984 .run_with(Sink::collect())
985 .unwrap()
986 .wait();
987 assert_eq!(
988 result,
989 Err(StreamError::Failed(format!(
990 "integer overflow in map_multiply: {} * 2",
991 i64::MAX
992 )))
993 );
994 }
995
996 #[test]
997 fn deterministic_random_add_sub_segments_match_scalar_oracle() {
998 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
999 let mut random = || {
1000 state ^= state << 13;
1001 state ^= state >> 7;
1002 state ^= state << 17;
1003 state
1004 };
1005 for case in 0..200 {
1006 let len = (random() as usize) % 514;
1007 let add = random() as i64;
1008 let subtract = random() as i64;
1009 let mut input = Vec::with_capacity(len);
1010 for _ in 0..len {
1011 input.push(random() as i64);
1012 }
1013 if case % 4 == 0 && !input.is_empty() {
1014 input[(random() as usize) % len] = i64::MAX;
1015 }
1016
1017 let chunked = Source::from_iter(input.clone())
1018 .map_add(add)
1019 .map_subtract(subtract)
1020 .run_with(Sink::collect())
1021 .unwrap()
1022 .wait();
1023 let scalar = Source::from_iter(input)
1024 .try_map(move |value| {
1025 value.checked_add(add).ok_or_else(|| {
1026 StreamError::Failed(format!("integer overflow in map_add: {value} + {add}"))
1027 })
1028 })
1029 .try_map(move |value| {
1030 value.checked_sub(subtract).ok_or_else(|| {
1031 StreamError::Failed(format!(
1032 "integer overflow in map_subtract: {value} - {subtract}"
1033 ))
1034 })
1035 })
1036 .run_with(Sink::collect())
1037 .unwrap()
1038 .wait();
1039 assert_eq!(chunked, scalar, "random case {case}, length {len}");
1040 }
1041 }
1042}