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
292struct ScalarI64Stream {
293 input: BoxStream<i64>,
294 steps: Arc<[ScalarChunkStep<i64>]>,
295 processor: Option<ScalarChunkProcessor<i64>>,
296 output_index: usize,
297 pending_upstream_error: Option<StreamError>,
298 pending_failure: Option<StreamError>,
299 upstream_done: bool,
300 chunked: bool,
301}
302
303impl ScalarI64Stream {
304 fn new(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> Self {
305 let chunked = steps.iter().any(ScalarChunkStep::is_add_or_subtract);
306 Self {
307 input,
308 steps,
309 processor: chunked.then(ScalarChunkProcessor::new),
310 output_index: 0,
311 pending_upstream_error: None,
312 pending_failure: None,
313 upstream_done: false,
314 chunked,
315 }
316 }
317
318 fn new_scalar(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> Self {
319 Self {
320 input,
321 steps,
322 processor: None,
323 output_index: 0,
324 pending_upstream_error: None,
325 pending_failure: None,
326 upstream_done: false,
327 chunked: false,
328 }
329 }
330
331 fn next_scalar(&mut self) -> Option<StreamResult<i64>> {
332 loop {
333 let item = self.input.next()?;
334 let mut value = match item {
335 Ok(value) => Some(value),
336 Err(error) => return Some(Err(error)),
337 };
338 for step in self.steps.iter() {
339 let Some(current) = value else {
340 break;
341 };
342 match step.op.apply_checked(current, step.operand) {
343 Ok(next) => value = next,
344 Err(error) => return Some(Err(error)),
345 }
346 }
347 if let Some(value) = value {
348 return Some(Ok(value));
349 }
350 }
351 }
352
353 fn fill_chunk(&mut self) {
354 let mut input = self.input.by_ref().map_while(|item| match item {
355 Ok(value) => Some(value),
356 Err(error) => {
357 self.pending_upstream_error = Some(error);
358 self.upstream_done = true;
359 None
360 }
361 });
362 let processor = self
363 .processor
364 .as_mut()
365 .expect("chunk processor exists for add/subtract segments");
366 let loaded = processor.load_from(&mut input);
367 if loaded < SCALAR_CHUNK_SIZE && self.pending_upstream_error.is_none() {
368 self.upstream_done = true;
369 }
370 let result = processor.process(&self.steps);
371 self.output_index = 0;
372 self.pending_failure = result.failure;
373 }
374}
375
376impl Iterator for ScalarI64Stream {
377 type Item = StreamResult<i64>;
378
379 fn next(&mut self) -> Option<Self::Item> {
380 if !self.chunked {
381 return self.next_scalar();
382 }
383 loop {
384 if let Some(&value) = self
385 .processor
386 .as_ref()
387 .expect("chunk processor exists for add/subtract segments")
388 .output()
389 .get(self.output_index)
390 {
391 self.output_index += 1;
392 return Some(Ok(value));
393 }
394 if let Some(error) = self.pending_failure.take() {
395 self.upstream_done = true;
396 return Some(Err(error));
397 }
398 if let Some(error) = self.pending_upstream_error.take() {
399 return Some(Err(error));
400 }
401 if self.upstream_done {
402 return None;
403 }
404 self.fill_chunk();
405 }
406 }
407}
408
409fn filter_i64_stream<F>(input: BoxStream<i64>, predicate: F) -> BoxStream<i64>
410where
411 F: Fn(i64) -> bool + Send + Sync + 'static,
412{
413 Box::new(input.filter_map(move |item| match item {
414 Ok(value) if predicate(value) => Some(Ok(value)),
415 Ok(_) => None,
416 Err(error) => Some(Err(error)),
417 }))
418}
419
420fn scalar_i64_stream(input: BoxStream<i64>, steps: Arc<[ScalarChunkStep<i64>]>) -> BoxStream<i64> {
421 if let [step] = steps.as_ref() {
422 let operand = step.operand;
423 return match step.op {
424 ScalarI64Op::Compare(ScalarCompareOp::Equal) => {
425 filter_i64_stream(input, move |value| value == operand)
426 }
427 ScalarI64Op::Compare(ScalarCompareOp::NotEqual) => {
428 filter_i64_stream(input, move |value| value != operand)
429 }
430 ScalarI64Op::Compare(ScalarCompareOp::LessThan) => {
431 filter_i64_stream(input, move |value| value < operand)
432 }
433 ScalarI64Op::Compare(ScalarCompareOp::LessOrEqual) => {
434 filter_i64_stream(input, move |value| value <= operand)
435 }
436 ScalarI64Op::Compare(ScalarCompareOp::GreaterThan) => {
437 filter_i64_stream(input, move |value| value > operand)
438 }
439 ScalarI64Op::Compare(ScalarCompareOp::GreaterOrEqual) => {
440 filter_i64_stream(input, move |value| value >= operand)
441 }
442 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Multiply) => {
443 Box::new(input.map(move |item| {
444 item.and_then(|value| {
445 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Multiply)
446 .apply_checked(value, operand)
447 .map(|value| value.expect("multiplication retains its input"))
448 })
449 }))
450 }
451 ScalarI64Op::Arithmetic(ScalarArithmeticOp::Add | ScalarArithmeticOp::Subtract) => {
452 Box::new(ScalarI64Stream::new(input, steps))
453 }
454 };
455 }
456 Box::new(ScalarI64Stream::new(input, steps))
457}
458
459pub(crate) struct ScalarI64SourceFactory<Mat> {
460 source: Arc<dyn super::SourceFactory<i64, Mat>>,
461 steps: Arc<[ScalarChunkStep<i64>]>,
462}
463
464impl<Mat> ScalarI64SourceFactory<Mat> {
465 fn append(&self, next: &[ScalarChunkStep<i64>]) -> Arc<dyn super::SourceFactory<i64, Mat>>
466 where
467 Mat: Send + 'static,
468 {
469 let mut steps = Vec::with_capacity(self.steps.len() + next.len());
470 steps.extend_from_slice(&self.steps);
471 steps.extend_from_slice(next);
472 Arc::new(Self {
473 source: Arc::clone(&self.source),
474 steps: steps.into(),
475 })
476 }
477}
478
479impl<Mat> super::SourceFactory<i64, Mat> for ScalarI64SourceFactory<Mat>
480where
481 Mat: Send + 'static,
482{
483 fn create(
484 self: Arc<Self>,
485 materializer: &super::Materializer,
486 ) -> StreamResult<(BoxStream<i64>, Mat)> {
487 let (input, mat) = Arc::clone(&self.source).create(materializer)?;
488 Ok((scalar_i64_stream(input, Arc::clone(&self.steps)), mat))
489 }
490
491 fn append_scalar_i64(
492 self: Arc<Self>,
493 steps: &[ScalarChunkStep<i64>],
494 ) -> Option<Arc<dyn super::SourceFactory<i64, Mat>>> {
495 Some(self.append(steps))
496 }
497}
498
499fn steps_with(next: ScalarChunkStep<i64>) -> Arc<[ScalarChunkStep<i64>]> {
500 Arc::from([next])
501}
502
503impl<Mat> Source<i64, Mat>
504where
505 Mat: Send + 'static,
506{
507 fn scalar_op(self, op: ScalarI64Op, operand: i64) -> Self {
508 let steps = steps_with(ScalarChunkStep::new(op, operand));
509 if let Some(factory) = Arc::clone(&self.factory).append_scalar_i64(&steps) {
510 return Source {
511 factory,
512 terminal_factory: None,
513 hints: self.hints.without_inline_micro(),
514 attributes: self.attributes,
515 split_hook: None,
516 };
517 }
518 if self.hints.inline_micro.is_none() {
519 return match op {
520 ScalarI64Op::Arithmetic(_) => self.try_map(move |value| {
521 op.apply_checked(value, operand)
522 .map(|value| value.expect("arithmetic maps retain their input"))
523 }),
524 ScalarI64Op::Compare(compare) => match compare {
525 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
526 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
527 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
528 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
529 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
530 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
531 },
532 };
533 }
534 let Source {
535 factory,
536 terminal_factory: _,
537 hints,
538 attributes,
539 split_hook: _,
540 } = self;
541 let next_factory = Arc::new(ScalarI64SourceFactory {
542 source: factory,
543 steps,
544 });
545 Source {
546 factory: next_factory,
547 terminal_factory: None,
548 hints: hints.without_inline_micro(),
549 attributes,
550 split_hook: None,
551 }
552 }
553
554 #[must_use]
556 pub fn map_op(self, op: ScalarArithmeticOp, operand: i64) -> Self {
557 self.scalar_op(ScalarI64Op::Arithmetic(op), operand)
558 }
559
560 #[must_use]
561 pub fn map_add(self, operand: i64) -> Self {
562 self.map_op(ScalarArithmeticOp::Add, operand)
563 }
564
565 #[must_use]
566 pub fn map_subtract(self, operand: i64) -> Self {
567 self.map_op(ScalarArithmeticOp::Subtract, operand)
568 }
569
570 #[must_use]
571 pub fn map_multiply(self, operand: i64) -> Self {
572 self.map_op(ScalarArithmeticOp::Multiply, operand)
573 }
574
575 #[must_use]
577 pub fn filter_op(self, op: ScalarCompareOp, operand: i64) -> Self {
578 let step = ScalarChunkStep::new(ScalarI64Op::Compare(op), operand);
579 let steps = steps_with(step);
580 if let Some(factory) = Arc::clone(&self.factory).append_scalar_i64(&steps) {
581 return Source {
582 factory,
583 terminal_factory: None,
584 hints: self.hints.without_inline_micro(),
585 attributes: self.attributes,
586 split_hook: None,
587 };
588 }
589 if self.hints.inline_micro.is_some() {
590 return Source {
591 factory: Arc::new(ScalarI64SourceFactory {
592 source: self.factory,
593 steps,
594 }),
595 terminal_factory: None,
596 hints: self.hints.without_inline_micro(),
597 attributes: self.attributes,
598 split_hook: None,
599 };
600 }
601 match op {
602 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
603 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
604 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
605 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
606 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
607 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
608 }
609 }
610}
611
612pub(super) struct ScalarI64FlowPlan<In> {
613 pub(super) prefix: super::PureTransform<In, i64>,
614 pub(super) steps: Arc<[ScalarChunkStep<i64>]>,
615}
616
617impl<In> Clone for ScalarI64FlowPlan<In> {
618 fn clone(&self) -> Self {
619 Self {
620 prefix: Arc::clone(&self.prefix),
621 steps: Arc::clone(&self.steps),
622 }
623 }
624}
625
626fn scalar_flow_transform<In: Send + 'static>(
627 prefix: super::PureTransform<In, i64>,
628 steps: Arc<[ScalarChunkStep<i64>]>,
629) -> super::PureTransform<In, i64> {
630 Arc::new(move |input| {
631 let input = prefix(input);
632 scalar_i64_stream(input, Arc::clone(&steps))
633 })
634}
635
636fn scalar_flow_fallback_transform<In: Send + 'static>(
637 prefix: super::PureTransform<In, i64>,
638 steps: Arc<[ScalarChunkStep<i64>]>,
639) -> super::PureTransform<In, i64> {
640 Arc::new(move |input| {
641 Box::new(ScalarI64Stream::new_scalar(
642 prefix(input),
643 Arc::clone(&steps),
644 ))
645 })
646}
647
648impl<In, Mat> Flow<In, i64, Mat>
649where
650 In: Send + 'static,
651 Mat: Send + 'static,
652{
653 fn scalar_op(self, op: ScalarI64Op, operand: i64) -> Self {
654 if self.scalar_i64.is_none() && !self.hints.scalar_chunk_prefix {
655 return match op {
656 ScalarI64Op::Arithmetic(_) => self.try_map(move |value| {
657 op.apply_checked(value, operand)
658 .map(|value| value.expect("arithmetic maps retain their input"))
659 }),
660 ScalarI64Op::Compare(compare) => match compare {
661 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
662 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
663 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
664 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
665 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
666 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
667 },
668 };
669 }
670 let Flow {
671 transform,
672 materialize,
673 hints,
674 attributes,
675 scalar_i64,
676 scalar_i64_fallback,
677 } = self;
678 let next = ScalarChunkStep::new(op, operand);
679 match (transform, scalar_i64) {
680 (FlowTransform::Pure(_), Some(plan)) => {
681 let plan = match Arc::downcast::<ScalarI64FlowPlan<In>>(plan) {
682 Ok(plan) => plan,
683 Err(_) => unreachable!("scalar i64 flow plan input type is preserved"),
684 };
685 let mut steps = Vec::with_capacity(plan.steps.len() + 1);
686 steps.extend_from_slice(&plan.steps);
687 steps.push(next.clone());
688 let steps: Arc<[ScalarChunkStep<i64>]> = steps.into();
689 let transform = scalar_flow_transform(Arc::clone(&plan.prefix), Arc::clone(&steps));
690 let fallback_prefix = scalar_i64_fallback.unwrap_or_else(|| {
691 scalar_flow_fallback_transform(
692 Arc::clone(&plan.prefix),
693 Arc::clone(&plan.steps),
694 )
695 });
696 let fallback = scalar_flow_fallback_transform(fallback_prefix, steps_with(next));
697 Flow {
698 transform: FlowTransform::Pure(transform),
699 materialize,
700 hints,
701 attributes,
702 scalar_i64: Some(Arc::new(ScalarI64FlowPlan {
703 prefix: Arc::clone(&plan.prefix),
704 steps,
705 })),
706 scalar_i64_fallback: Some(fallback),
707 }
708 }
709 (FlowTransform::Pure(prefix), None) => {
710 let steps = steps_with(next);
711 let transform = scalar_flow_transform(Arc::clone(&prefix), Arc::clone(&steps));
712 let fallback_prefix = scalar_i64_fallback.unwrap_or_else(|| Arc::clone(&prefix));
713 let fallback = scalar_flow_fallback_transform(fallback_prefix, Arc::clone(&steps));
714 Flow {
715 transform: FlowTransform::Pure(transform),
716 materialize,
717 hints,
718 attributes,
719 scalar_i64: Some(Arc::new(ScalarI64FlowPlan { prefix, steps })),
720 scalar_i64_fallback: Some(fallback),
721 }
722 }
723 (FlowTransform::Runtime(runtime), _) => {
724 let steps = steps_with(next);
725 let scalar =
726 move |input: BoxStream<i64>| scalar_i64_stream(input, Arc::clone(&steps));
727 Flow {
728 transform: FlowTransform::Runtime(Arc::new(move |input, materializer| {
729 runtime(input, materializer).map(&scalar)
730 })),
731 materialize,
732 hints: FlowHints::default(),
733 attributes,
734 scalar_i64: None,
735 scalar_i64_fallback: None,
736 }
737 }
738 }
739 }
740
741 #[must_use]
742 pub fn map_op(self, op: ScalarArithmeticOp, operand: i64) -> Self {
743 self.scalar_op(ScalarI64Op::Arithmetic(op), operand)
744 }
745
746 #[must_use]
747 pub fn map_add(self, operand: i64) -> Self {
748 self.map_op(ScalarArithmeticOp::Add, operand)
749 }
750
751 #[must_use]
752 pub fn map_subtract(self, operand: i64) -> Self {
753 self.map_op(ScalarArithmeticOp::Subtract, operand)
754 }
755
756 #[must_use]
757 pub fn map_multiply(self, operand: i64) -> Self {
758 self.map_op(ScalarArithmeticOp::Multiply, operand)
759 }
760
761 #[must_use]
762 pub fn filter_op(self, op: ScalarCompareOp, operand: i64) -> Self {
763 if self.scalar_i64.is_some() || self.hints.scalar_chunk_prefix {
764 return self.scalar_op(ScalarI64Op::Compare(op), operand);
765 }
766 match op {
767 ScalarCompareOp::Equal => self.filter(move |value| *value == operand),
768 ScalarCompareOp::NotEqual => self.filter(move |value| *value != operand),
769 ScalarCompareOp::LessThan => self.filter(move |value| *value < operand),
770 ScalarCompareOp::LessOrEqual => self.filter(move |value| *value <= operand),
771 ScalarCompareOp::GreaterThan => self.filter(move |value| *value > operand),
772 ScalarCompareOp::GreaterOrEqual => self.filter(move |value| *value >= operand),
773 }
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::{Flow, Sink};
781 use std::sync::{Arc, Mutex};
782
783 #[test]
784 fn first_failure_stage_and_valid_prefix_are_exact() {
785 let emitted = Arc::new(Mutex::new(Vec::new()));
786 let observed = Arc::clone(&emitted);
787 let completion = Source::from_iter([0, i64::MIN + 5, i64::MAX])
788 .map_add(1)
789 .map_subtract(10)
790 .run_with(Sink::foreach(move |value| {
791 observed
792 .lock()
793 .expect("emitted values poisoned")
794 .push(value);
795 }))
796 .unwrap();
797
798 assert_eq!(
799 completion.wait(),
800 Err(StreamError::Failed(format!(
801 "integer overflow in map_subtract: {} - 10",
802 i64::MIN + 6
803 )))
804 );
805 assert_eq!(*emitted.lock().unwrap(), vec![-9]);
806 }
807
808 #[test]
809 fn overflow_positions_around_chunk_boundary_match_scalar_oracle() {
810 for len in [0, 1, 2, 3, 127, 128, 129, 255, 256, 257] {
811 let mut input = vec![0_i64; len];
812 if let Some(last) = input.last_mut() {
813 *last = i64::MAX;
814 }
815 let chunked = Source::from_iter(input.clone())
816 .map_add(1)
817 .run_with(Sink::collect())
818 .unwrap()
819 .wait();
820 let scalar = Source::from_iter(input)
821 .try_map(|value| {
822 value.checked_add(1).ok_or_else(|| {
823 StreamError::Failed(format!("integer overflow in map_add: {value} + 1"))
824 })
825 })
826 .run_with(Sink::collect())
827 .unwrap()
828 .wait();
829 assert_eq!(chunked, scalar, "length {len}");
830 }
831 }
832
833 #[test]
834 fn filters_compact_stably_before_and_after_fallible_maps() {
835 let result = Source::from_iter(-300_i64..300)
836 .filter_op(ScalarCompareOp::GreaterOrEqual, -250)
837 .map_add(7)
838 .filter_op(ScalarCompareOp::LessThan, 10)
839 .run_with(Sink::collect())
840 .unwrap()
841 .wait()
842 .unwrap();
843 let expected: Vec<_> = (-300_i64..300)
844 .filter(|value| *value >= -250)
845 .map(|value| value + 7)
846 .filter(|value| *value < 10)
847 .collect();
848 assert_eq!(result, expected);
849 }
850
851 #[test]
852 fn flow_builtins_fuse_but_opaque_closures_remain_per_element() {
853 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
854 let observed = Arc::clone(&calls);
855 let flow = Flow::identity()
856 .map(move |value: i64| {
857 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
858 value
859 })
860 .map_add(2)
861 .map_subtract(1);
862 let output = Source::from_iter(0_i64..600)
863 .via(flow)
864 .run_with(Sink::collect())
865 .unwrap()
866 .wait()
867 .unwrap();
868 assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 600);
869 assert_eq!(output[0], 1);
870 assert_eq!(output[599], 600);
871
872 let direct_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
873 let observed = Arc::clone(&direct_calls);
874 let head = Source::from_iter(0_i64..600)
875 .map(move |value| {
876 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
877 value
878 })
879 .map_add(1)
880 .run_with(Sink::head())
881 .unwrap()
882 .wait()
883 .unwrap();
884 assert_eq!(head, 1);
885 assert_eq!(
886 direct_calls.load(std::sync::atomic::Ordering::Relaxed),
887 1,
888 "an opaque prefix must not be pulled ahead by the chunk executor"
889 );
890
891 let flow_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
892 let observed = Arc::clone(&flow_calls);
893 let flow = Flow::identity()
894 .map(move |value: i64| {
895 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
896 value
897 })
898 .map_add(1);
899 let head = Source::from_iter(0_i64..600)
900 .via(flow)
901 .run_with(Sink::head())
902 .unwrap()
903 .wait()
904 .unwrap();
905 assert_eq!(head, 1);
906 assert_eq!(
907 flow_calls.load(std::sync::atomic::Ordering::Relaxed),
908 1,
909 "an opaque Flow prefix must remain one-element-at-a-time"
910 );
911
912 let dynamic_pulls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
913 let observed = Arc::clone(&dynamic_pulls);
914 let flow = Flow::identity()
915 .map_add(1)
916 .via(Flow::identity())
917 .map_subtract(1)
918 .map(|value| value);
919 let head = Source::unfold(0_i64, move |value| {
920 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
921 Some((value + 1, value))
922 })
923 .via(flow)
924 .run_with(Sink::head())
925 .unwrap()
926 .wait()
927 .unwrap();
928 assert_eq!(head, 0);
929 assert_eq!(
930 dynamic_pulls.load(std::sync::atomic::Ordering::Relaxed),
931 1,
932 "a dynamic source must select the scalar fallback for a chunked Flow"
933 );
934
935 let runtime_pulls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
936 let observed = Arc::clone(&runtime_pulls);
937 let flow = Flow::identity()
938 .map_add(1)
939 .via(Flow::from_runtime_transform(|input, _materializer| {
940 Ok(input)
941 }));
942 let head = Source::unfold(0_i64, move |value| {
943 observed.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
944 Some((value + 1, value))
945 })
946 .via(flow)
947 .run_with(Sink::head())
948 .unwrap()
949 .wait()
950 .unwrap();
951 assert_eq!(head, 1);
952 assert_eq!(
953 runtime_pulls.load(std::sync::atomic::Ordering::Relaxed),
954 1,
955 "runtime Flow composition must scalarize an earlier chunked segment"
956 );
957 }
958
959 #[test]
960 fn multiplication_remains_checked_scalar() {
961 let result = Source::from_iter([2_i64, i64::MAX])
962 .map_multiply(2)
963 .run_with(Sink::collect())
964 .unwrap()
965 .wait();
966 assert_eq!(
967 result,
968 Err(StreamError::Failed(format!(
969 "integer overflow in map_multiply: {} * 2",
970 i64::MAX
971 )))
972 );
973 }
974
975 #[test]
976 fn deterministic_random_add_sub_segments_match_scalar_oracle() {
977 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
978 let mut random = || {
979 state ^= state << 13;
980 state ^= state >> 7;
981 state ^= state << 17;
982 state
983 };
984 for case in 0..200 {
985 let len = (random() as usize) % 514;
986 let add = random() as i64;
987 let subtract = random() as i64;
988 let mut input = Vec::with_capacity(len);
989 for _ in 0..len {
990 input.push(random() as i64);
991 }
992 if case % 4 == 0 && !input.is_empty() {
993 input[(random() as usize) % len] = i64::MAX;
994 }
995
996 let chunked = Source::from_iter(input.clone())
997 .map_add(add)
998 .map_subtract(subtract)
999 .run_with(Sink::collect())
1000 .unwrap()
1001 .wait();
1002 let scalar = Source::from_iter(input)
1003 .try_map(move |value| {
1004 value.checked_add(add).ok_or_else(|| {
1005 StreamError::Failed(format!("integer overflow in map_add: {value} + {add}"))
1006 })
1007 })
1008 .try_map(move |value| {
1009 value.checked_sub(subtract).ok_or_else(|| {
1010 StreamError::Failed(format!(
1011 "integer overflow in map_subtract: {value} - {subtract}"
1012 ))
1013 })
1014 })
1015 .run_with(Sink::collect())
1016 .unwrap()
1017 .wait();
1018 assert_eq!(chunked, scalar, "random case {case}, length {len}");
1019 }
1020 }
1021}