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
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    /// Apply a checked built-in integer operation.
555    #[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    /// Retain values matching a built-in integer comparison.
576    #[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}