Skip to main content

datum/stream/
scalar.rs

1//! Planner-visible scalar integer operators and their safe chunk executor.
2//!
3//! Opaque `map`/`filter` closures deliberately do not enter this module.  Only
4//! the built-in operators represented by [`ScalarArithmeticOp`] and
5//! [`ScalarCompareOp`] are eligible for speculative chunk execution.
6
7use 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/// A planner-visible checked integer map operation.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum ScalarArithmeticOp {
16    Add,
17    Subtract,
18    Multiply,
19}
20
21/// A planner-visible integer comparison used by built-in filters.
22#[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
75/// Monomorphic step metadata stored by both linear streams and GraphDSL maps.
76///
77/// The function pointers let the generic graph planner downcast a
78/// `ScalarChunkStep<In>` only when `In == i64`, while keeping the production
79/// kernels themselves monomorphic and inspectable in generated code.
80pub(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
135/// Process one private chunk through a pure built-in segment.
136///
137/// `original` retains the unmodified input.  `values`, `compact`, and `mask`
138/// are allocated once per materialization and reused.  A flagged add/sub
139/// chunk is discarded and replayed element-major through `apply_checked`,
140/// preserving the first failing element/stage and the valid output prefix.
141pub(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        // Multiplication is deliberately kept on scalar replay: baseline x86-64
279        // synthesizes packed i64 multiply and loses on the real cost model.
280        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/// Production add-kernel probe used by the checked-in S2 codegen evidence.
293#[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/// Production subtract-kernel probe used by the checked-in S2 codegen evidence.
300#[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/// Production comparison-kernel probe used by the checked-in S2 codegen evidence.
307#[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    /// Apply a checked built-in integer operation.
576    #[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    /// Retain values matching a built-in integer comparison.
597    #[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}