Skip to main content

polydat_core/iteration/
source.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Data sources: typed sequences that drive workload iteration.
5//!
6//! A **source** is a data provider with identity. It knows what it yields
7//! (schema), how much it has (extent), where the consumer is (cursor),
8//! and how to partition across concurrent fibers.
9//!
10//! Sources replace the `cycles` counter as the workload iteration driver.
11//! A Polydat program declares one with the `cursor` keyword
12//! (`cursor q = range(0, N) [over <partition>]`). A host pulls from
13//! sources to drive dispatch; when a source is exhausted, the activation
14//! is done.
15//!
16//! ## Source Types
17//!
18//! - **Range**: `range(0, N)` — a finite sequence of ordinals, served by
19//!   [`RangeSourceFactory`]. Replaces `cycles: N`.
20//! - **Extending**: `until_elapsed(base, min_ms[, delta])` and the other
21//!   `until_*` constructors, compiled to a `CursorKind::Extending*` and
22//!   served by [`ExtendingRangeSourceFactory`].
23//! - **Host-supplied**: any [`DataSourceFactory`] a host implements, such
24//!   as a dataset reader whose items carry vectors or metadata; this
25//!   crate ships only the range factories.
26//!
27//! ## Crate Sovereignty
28//!
29//! All source API surface lives here in `polydat-core`, re-exported by
30//! `polydat` at `polydat::iteration::source`. Host runtimes and adapters
31//! consume these types but don't define them.
32
33use std::sync::Arc;
34use std::sync::atomic::{AtomicU64, Ordering};
35
36use crate::ast::{PortType, Value};
37
38/// Whether a source item can be reconstructed from its ordinal without
39/// consulting or advancing mutable source state.
40///
41/// The default is deliberately conservative. A forward-only cursor is not
42/// sufficient evidence that an earlier item is safe to render again.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub enum SourceReplayStability {
45    /// Rendering is destructive, stateful, externally mutable, or otherwise
46    /// not proven stable for repeated calls at one ordinal.
47    #[default]
48    Consumptive,
49    /// `render_item(ordinal)` is pure, total, and byte-identical for the
50    /// lifetime of the advertised source generation.
51    StableByOrdinal,
52}
53
54/// A compact description of values which can be generated without rendering
55/// an opaque source item first.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57pub enum SourceValueForm {
58    /// The source may be replayable, but the item must still be rendered.
59    #[default]
60    Opaque,
61    /// The yielded scalar value is the ordinal itself. A typed consumer may
62    /// apply its declared narrowing or wrapping conversion while vectorizing.
63    Ordinal,
64}
65
66/// Runtime source capability used by offset-stamped batch plans.
67///
68/// `generation` must change whenever rendering the same ordinal may produce a
69/// different value. Activation identity remains executor-owned because a
70/// phase rewind can intentionally replay one unchanged source generation as a
71/// new logical evaluation.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub struct SourceReplayContract {
74    /// Whether rendering an ordinal again gives the same value.
75    pub stability: SourceReplayStability,
76    /// The generation of the source's values; changes when an ordinal may render differently.
77    pub generation: u64,
78    /// What a value is: an ordinal, or opaque.
79    pub value_form: SourceValueForm,
80}
81
82impl SourceReplayContract {
83    /// The contract of a source that is consumed as it advances: no replay.
84    pub const fn consumptive() -> Self {
85        Self {
86            stability: SourceReplayStability::Consumptive,
87            generation: 0,
88            value_form: SourceValueForm::Opaque,
89        }
90    }
91
92    /// The contract of a source stable by ordinal in `generation`.
93    pub const fn stable_ordinal(generation: u64) -> Self {
94        Self {
95            stability: SourceReplayStability::StableByOrdinal,
96            generation,
97            value_form: SourceValueForm::Ordinal,
98        }
99    }
100
101    /// Whether any owned ordinal renders again without advancing the source.
102    pub const fn is_stable_by_ordinal(self) -> bool {
103        matches!(self.stability, SourceReplayStability::StableByOrdinal)
104    }
105
106    /// True when the source ordinal is simultaneously a replay key, scalar
107    /// value, packet number, and low-order lane clock.
108    pub const fn is_perfect_ordinal(self) -> bool {
109        self.is_stable_by_ordinal() && matches!(self.value_form, SourceValueForm::Ordinal)
110    }
111}
112
113/// A single item yielded by a source.
114#[derive(Clone, Debug)]
115pub struct SourceItem {
116    /// Position in the source sequence.
117    pub ordinal: u64,
118    /// Named field values. Empty for range sources (ordinal IS the data).
119    /// For dataset sources: `[("vector", Value::Json(...)), ("metadata", Value::U64(...))]`.
120    pub fields: Vec<(String, Value)>,
121}
122
123impl SourceItem {
124    /// Create a range item (ordinal only, no fields).
125    pub fn ordinal(ordinal: u64) -> Self {
126        Self {
127            ordinal,
128            fields: Vec::new(),
129        }
130    }
131
132    /// Create an item with ordinal and named fields.
133    pub fn with_fields(ordinal: u64, fields: Vec<(String, Value)>) -> Self {
134        Self { ordinal, fields }
135    }
136
137    /// Get a field value by name.
138    pub fn field(&self, name: &str) -> Option<&Value> {
139        self.fields.iter().find(|(n, _)| n == name).map(|(_, v)| v)
140    }
141}
142
143/// Schema describing what a source yields.
144#[derive(Clone, Debug)]
145pub struct SourceSchema {
146    /// Source name as declared in the Polydat graph.
147    pub name: String,
148    /// Field names and types available for projection (e.g., `ordinal: U64`, `vector: Json`).
149    pub projections: Vec<(String, PortType)>,
150    /// Known extent, if finite. `None` for infinite sources.
151    ///
152    /// `None` may also mean the extent is computable but only at runtime
153    /// (e.g., the cursor's `range(...)` bounds depend on iteration-variable
154    /// externs). In that case `extent_outputs` carries the kernel output
155    /// names whose values yield `[start, end)` once externs are bound.
156    pub extent: Option<u64>,
157    /// Optional aux output names `(start, end)` that the runtime can pull
158    /// from the kernel after externs are populated to compute extent. Set
159    /// when `range(...)` bounds are non-literal (e.g., wire-bound dataset
160    /// function calls). The compiler also writes back to `extent` if both
161    /// values fold to constants at compile time.
162    pub extent_outputs: Option<(String, String)>,
163    /// Optional cursor limit clamp, taken from
164    /// `CompileOptions::cursor_limit` (a host's `--limit`-style option).
165    /// Applied after runtime extent evaluation.
166    pub extent_limit: Option<u64>,
167    /// What kind of cursor this is. Default `Range`, the bounded
168    /// `range(start, end)` cursor; the `Extending*` variants declare a
169    /// runtime-extending cursor under a wall-clock, count, or
170    /// predicate policy. A host picks [`ExtendingRangeSourceFactory`]
171    /// for those, and `cursor_over_partitions` treats them as
172    /// open-extent when it resolves an `over` clause.
173    pub cursor_kind: CursorKind,
174    /// SRD 71: name of the kernel output that carries the
175    /// partition-narrowing source (the `over <expr>` clause on
176    /// the cursor declaration). `cursor_over_partitions` pulls it and
177    /// resolves it through `cursor_partition::resolve_over` into the
178    /// full partition list: a spec string or a `PartitionSpec`
179    /// resolves against the cursor's extent, a `Partition` or a
180    /// `PartitionList` is re-projected onto it, and `Value::None`
181    /// yields no partitions. A host, such as activation setup,
182    /// selects one partition and writes it with `set_cursor`.
183    ///
184    /// `None` means the cursor was declared without an `over`
185    /// clause; the cursor uses its full declared extent.
186    pub partition_output: Option<String>,
187    /// The partitions the `over` clause denotes, resolved by the
188    /// compiler when the clause is a literal spec and the extent is
189    /// known at build (engine_parity.md, step 3). A host reads them
190    /// without evaluating anything; `cursor_over_partitions` returns
191    /// them without a pull. `None` when the clause or the extent is
192    /// only known at run time, or the cursor has no `over` clause.
193    pub partitions: Option<Vec<crate::iteration::cursor_partition::Partition>>,
194}
195
196/// Cursor-construction discriminator. Set by the Polydat compiler
197/// when it recognises a cursor's constructor expression
198/// (`range(...)`, `until_elapsed(...)`, etc.); read by the
199/// executor at phase setup to instantiate the matching
200/// data-source factory + policy.
201///
202/// Each `Extending*` variant carries the Polydat output names the
203/// runtime should pull at phase setup to resolve the policy's
204/// parameters (same pattern as `extent_outputs` for `range`).
205/// `delta_output` is optional — when `None`, the cursor's base
206/// is used as the extension delta.
207#[derive(Clone, Debug, Default)]
208pub enum CursorKind {
209    /// Bounded `range(start, end)` cursor. Default — keeps
210    /// every existing call site working.
211    #[default]
212    Range,
213    /// `until_elapsed(base, min_ms[, delta])`.
214    /// Extends while elapsed_ms < min_ms.
215    ExtendingTimed {
216        /// The output holding the minimum elapsed milliseconds.
217        min_ms_output: String,
218        /// The output holding the extension step, if one was given.
219        delta_output: Option<String>,
220    },
221    /// `until_passes(base, min_passes[, delta])`.
222    /// Extends while completed passes < min_passes.
223    ExtendingPasses {
224        /// The output holding the minimum number of passes.
225        min_passes_output: String,
226        /// The output holding the extension step, if one was given.
227        delta_output: Option<String>,
228    },
229    /// `until_count(base, min_count[, delta])`.
230    /// Extends while raw consumed count < min_count.
231    ExtendingCount {
232        /// The output holding the minimum consumed count.
233        min_count_output: String,
234        /// The output holding the extension step, if one was given.
235        delta_output: Option<String>,
236    },
237    /// `until_elapsed_and_passes(base, min_ms, min_passes[, delta])`.
238    /// AND: stops when EITHER target is reached. Extends while
239    /// BOTH conditions are still below target.
240    ExtendingElapsedAndPasses {
241        /// The output holding the minimum elapsed milliseconds.
242        min_ms_output: String,
243        /// The output holding the minimum number of passes.
244        min_passes_output: String,
245        /// The output holding the extension step, if one was given.
246        delta_output: Option<String>,
247    },
248    /// `until_elapsed_or_passes(base, min_ms, min_passes[, delta])`.
249    /// OR: stops only when BOTH targets are reached. Extends
250    /// while EITHER condition is still below target.
251    ExtendingElapsedOrPasses {
252        /// The output holding the minimum elapsed milliseconds.
253        min_ms_output: String,
254        /// The output holding the minimum number of passes.
255        min_passes_output: String,
256        /// The output holding the extension step, if one was given.
257        delta_output: Option<String>,
258    },
259}
260
261/// Consumption API for data sources. One instance per fiber.
262///
263/// The interaction model has two phases:
264///
265/// 1. **Reserve** — `reserve(stride)` atomically claims a range of
266///    ordinals via CAS on the shared cursor. This touches shared
267///    state but is instantaneous (one atomic op). Returns `None`
268///    when the source is globally exhausted.
269///
270/// 2. **Render** — the fiber uses the reserved range with its own
271///    Polydat instance to produce field values. No shared state, no
272///    contention between fibers. For range sources, rendering is
273///    trivial (ordinal IS the data). For dataset sources, rendering
274///    reads vectors/metadata from mmap'd storage.
275///
276/// The `next_chunk` convenience method combines both phases. Use
277/// `reserve` directly when the rendering is handled by the
278/// executor's Polydat fiber.
279pub trait DataSource: Send {
280    /// Atomically reserve up to `stride` ordinals from the source.
281    ///
282    /// Returns the half-open range `[start..end)` of reserved
283    /// ordinals, or `None` if the source is exhausted. The range
284    /// may be shorter than `stride` at the tail of the source.
285    ///
286    /// This is the only method that touches shared state (the
287    /// global cursor). It must be lock-free — a single CAS or
288    /// fetch_add.
289    fn reserve(&mut self, stride: usize) -> Option<std::ops::Range<u64>>;
290
291    /// Pull the next item. `None` = source exhausted.
292    fn next(&mut self) -> Option<SourceItem> {
293        let range = self.reserve(1)?;
294        Some(self.render_item(range.start))
295    }
296
297    /// Pull up to `limit` items. Combines reserve + render.
298    /// Returns fewer than `limit` only when the source is globally
299    /// exhausted. Empty vec = exhausted.
300    fn next_chunk(&mut self, limit: usize) -> Vec<SourceItem> {
301        let range = match self.reserve(limit) {
302            Some(r) => r,
303            None => return Vec::new(),
304        };
305        (range.start..range.end)
306            .map(|ordinal| self.render_item(ordinal))
307            .collect()
308    }
309
310    /// Produce a source item for a previously reserved ordinal.
311    ///
312    /// This is the fiber-local rendering step — no shared state.
313    /// For range sources: returns `SourceItem::ordinal(ordinal)`.
314    /// For dataset sources: reads vector/metadata from storage.
315    fn render_item(&self, ordinal: u64) -> SourceItem;
316
317    /// Known extent, if finite.
318    fn extent(&self) -> Option<u64>;
319
320    /// Items consumed so far (for progress reporting).
321    fn consumed(&self) -> u64;
322
323    /// The schema of items this source yields.
324    fn schema(&self) -> &SourceSchema;
325
326    /// Replay/addressability capability for offset-stamped batch execution.
327    /// External source implementations inherit the safe consumptive default
328    /// until they explicitly prove the stronger contract.
329    fn replay_contract(&self) -> SourceReplayContract {
330        SourceReplayContract::consumptive()
331    }
332}
333
334/// Factory that creates per-fiber `DataSource` readers.
335///
336/// Holds shared state (atomic cursor, partition pool) that's
337/// distributed across readers. Each fiber gets its own reader.
338///
339/// ## Dispatch model
340///
341/// The **stride** is the stanza length — the number of source items
342/// a fiber acquires as an atomic unit. One stanza of ops processes
343/// one stride of source items. Strides are inseparable: a fiber
344/// that acquires a stride processes all items before acquiring the
345/// next.
346///
347/// The default implementation (`RangeSourceFactory`) uses a shared
348/// atomic cursor — all fibers pull strides from the same counter,
349/// producing natural monotonic striping. This is correct for range
350/// sources where items are independent ordinals.
351///
352/// For dataset sources with locality benefits (mmap prefetch),
353/// factories can implement partitioned allocation: each fiber gets
354/// a pre-assigned range of strides, and when exhausted, steals
355/// strides from a shared pool. The stride is the minimum unit of
356/// work stealing — a fiber never steals partial stanzas.
357pub trait DataSourceFactory: Send + Sync {
358    /// Create a new reader for a fiber.
359    fn create_reader(&self) -> Box<dyn DataSource>;
360
361    /// Schema for all readers from this factory.
362    fn schema(&self) -> &SourceSchema;
363
364    /// Global items consumed across all readers (for progress reporting).
365    fn global_consumed(&self) -> u64;
366
367    /// Known extent, if finite. Same as schema().extent but avoids clone.
368    fn global_extent(&self) -> Option<u64> {
369        self.schema().extent
370    }
371
372    /// Replay/addressability capability shared by readers from this factory.
373    fn replay_contract(&self) -> SourceReplayContract {
374        SourceReplayContract::consumptive()
375    }
376
377    /// Rewind the factory's shared cursor to the start so a
378    /// subsequent `create_reader()` produces a fresh stream
379    /// covering the same ordinal range: what a host that re-runs a
380    /// source between poll rounds calls after each round exhausts it.
381    ///
382    /// Default impl: returns `false` to signal the factory
383    /// doesn't support rewinding. Factories that DO support it
384    /// (RangeSourceFactory, ExtendingRangeSourceFactory)
385    /// override and reset their internal cursor / extent
386    /// state. A host that needs to rewind should reject a factory
387    /// that returns `false` with a clear diagnostic rather than
388    /// complete silently after the first round.
389    fn rewind_for_poll(&self) -> bool {
390        false
391    }
392}
393
394// =========================================================================
395// RangeSource: finite sequence of ordinals
396// =========================================================================
397
398/// Factory for range sources. Shared atomic cursor distributes
399/// ordinals across fibers.
400pub struct RangeSourceFactory {
401    cursor: Arc<AtomicU64>,
402    end: u64,
403    schema: SourceSchema,
404}
405
406impl RangeSourceFactory {
407    /// Create a range source from `[start, end)`.
408    pub fn new(start: u64, end: u64) -> Self {
409        Self {
410            cursor: Arc::new(AtomicU64::new(start)),
411            end,
412            schema: SourceSchema {
413                name: "_range".into(),
414                projections: vec![("ordinal".into(), PortType::U64)],
415                extent: Some(end.saturating_sub(start)),
416                extent_outputs: None,
417                extent_limit: None,
418                cursor_kind: CursorKind::Range,
419                partition_output: None,
420                partitions: None,
421            },
422        }
423    }
424
425    /// Create a range source with a named schema.
426    pub fn named(name: &str, start: u64, end: u64) -> Self {
427        let mut factory = Self::new(start, end);
428        factory.schema.name = name.to_string();
429        factory
430    }
431}
432
433impl DataSourceFactory for RangeSourceFactory {
434    fn create_reader(&self) -> Box<dyn DataSource> {
435        Box::new(RangeSource {
436            cursor: self.cursor.clone(),
437            end: self.end,
438            consumed: 0,
439            schema: self.schema.clone(),
440        })
441    }
442
443    fn schema(&self) -> &SourceSchema {
444        &self.schema
445    }
446
447    fn global_consumed(&self) -> u64 {
448        let pos = self.cursor.load(Ordering::Relaxed);
449        let start = self.end.saturating_sub(self.schema.extent.unwrap_or(0));
450        pos.saturating_sub(start)
451            .min(self.schema.extent.unwrap_or(u64::MAX))
452    }
453
454    fn replay_contract(&self) -> SourceReplayContract {
455        SourceReplayContract::stable_ordinal(0)
456    }
457
458    fn rewind_for_poll(&self) -> bool {
459        // Reset the shared atomic cursor back to the
460        // start-of-range. SRD-75 phase-poll uses this between
461        // iterations: each iteration covers ordinals
462        // `[start, end)` afresh, so the workload's ops see the
463        // same cycle space per iteration. With concurrency=1
464        // (mandated by phase-poll), there's a single fiber
465        // and no race on the reset.
466        let start = self.end.saturating_sub(self.schema.extent.unwrap_or(0));
467        self.cursor.store(start, Ordering::Relaxed);
468        true
469    }
470}
471
472/// Per-fiber range reader. Pulls ordinals from a shared atomic cursor.
473struct RangeSource {
474    cursor: Arc<AtomicU64>,
475    end: u64,
476    consumed: u64,
477    schema: SourceSchema,
478}
479
480impl DataSource for RangeSource {
481    fn reserve(&mut self, stride: usize) -> Option<std::ops::Range<u64>> {
482        let base = self.cursor.fetch_add(stride as u64, Ordering::Relaxed);
483        if base >= self.end {
484            return None;
485        }
486        let actual_end = (base + stride as u64).min(self.end);
487        let count = actual_end - base;
488        self.consumed += count;
489        Some(base..actual_end)
490    }
491
492    fn render_item(&self, ordinal: u64) -> SourceItem {
493        SourceItem::ordinal(ordinal)
494    }
495
496    fn extent(&self) -> Option<u64> {
497        self.schema.extent
498    }
499
500    fn consumed(&self) -> u64 {
501        self.consumed
502    }
503
504    fn schema(&self) -> &SourceSchema {
505        &self.schema
506    }
507
508    fn replay_contract(&self) -> SourceReplayContract {
509        SourceReplayContract::stable_ordinal(0)
510    }
511}
512
513// =========================================================================
514// ExtendingRangeSource: runtime-growable extent
515// =========================================================================
516
517/// Snapshot of cursor state passed to an [`ExtensionPolicy`]
518/// at each end-reach decision point. Policies are pure
519/// predicates over this context — they don't carry their own
520/// clocks or counters.
521#[derive(Clone, Copy, Debug)]
522pub struct ExtensionContext {
523    /// Wall-clock milliseconds since the source factory was
524    /// constructed (typically phase start).
525    pub elapsed_ms: u64,
526    /// Global ordinals consumed so far — `cursor.load() - start`.
527    /// Pass count is `consumed / base`.
528    pub consumed: u64,
529    /// The `base` chunk size declared by the cursor. Used by
530    /// policies that reason in passes rather than raw counts.
531    pub base: u64,
532}
533
534impl ExtensionContext {
535    /// Convenience: integer pass count (consumed / base).
536    /// Returns 0 when `base == 0` (degenerate cursor).
537    pub fn passes(&self) -> u64 {
538        self.consumed.checked_div(self.base).unwrap_or(0)
539    }
540}
541
542/// Policy that decides whether to extend an
543/// `ExtendingRangeSource` when its current end is reached.
544///
545/// Implementations are pure predicates over the
546/// [`ExtensionContext`] — no internal state, no side effects.
547/// The source provides elapsed time and consumed counts; the
548/// policy returns `Some(delta)` to grow the extent or `None`
549/// to terminate. Cheap-and-possibly-duplicated call contract
550/// (concurrent fibers may invoke under racing end-reach; only
551/// one CAS-wins extension takes effect per round).
552pub trait ExtensionPolicy: Send + Sync {
553    /// Decide how to extend (if at all) given the current
554    /// cursor context.
555    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64>;
556}
557
558/// Factory for ExtendingRangeSource. Differs from
559/// [`RangeSourceFactory`] in that `end` is an atomic that the
560/// extension policy may grow over the lifetime of the phase.
561/// `global_extent()` returns the CURRENT end so phase-status
562/// displays reflect any growth honestly.
563pub struct ExtendingRangeSourceFactory {
564    cursor: Arc<AtomicU64>,
565    end: Arc<AtomicU64>,
566    start: u64,
567    /// Per-pass chunk size — also the default extension delta
568    /// when the policy reports "continue". Exposed to the
569    /// policy via `ExtensionContext::base` so pass-count
570    /// predicates work.
571    base: u64,
572    /// SRD 71 — hard upper bound on growth. When a cursor is
573    /// narrowed by a partition (`until_elapsed(...) over p`),
574    /// the partition's end ordinal caps the extension: the
575    /// policy keeps making its time / pass / count decisions,
576    /// but the source terminates the moment the partition is
577    /// exhausted, whichever comes first. `None` = unbounded
578    /// (the policy alone decides).
579    max_end: Option<u64>,
580    /// Wall-clock baseline. Captured at factory construction
581    /// so per-phase factories yield per-phase elapsed numbers
582    /// without external clock plumbing.
583    started: std::time::Instant,
584    policy: Arc<dyn ExtensionPolicy>,
585    schema: SourceSchema,
586}
587
588impl ExtendingRangeSourceFactory {
589    /// Build with an initial extent `[start, start + initial_extent)`.
590    /// The extension policy is consulted only when the cursor
591    /// reaches the end — the first stride consumed produces
592    /// indices in the initial range.
593    pub fn new(
594        name: &str,
595        start: u64,
596        initial_extent: u64,
597        policy: Arc<dyn ExtensionPolicy>,
598    ) -> Self {
599        let end = start.saturating_add(initial_extent);
600        Self {
601            cursor: Arc::new(AtomicU64::new(start)),
602            end: Arc::new(AtomicU64::new(end)),
603            start,
604            base: initial_extent,
605            max_end: None,
606            started: std::time::Instant::now(),
607            policy,
608            schema: SourceSchema {
609                name: name.to_string(),
610                projections: vec![("ordinal".into(), PortType::U64)],
611                extent: Some(initial_extent),
612                extent_outputs: None,
613                cursor_kind: CursorKind::Range,
614                extent_limit: None,
615                partition_output: None,
616                partitions: None,
617            },
618        }
619    }
620
621    /// Cap growth at `max_end` (absolute ordinal, exclusive) —
622    /// the partition-narrowing bound from SRD 71. The initial
623    /// extent is clamped too, so a base chunk larger than the
624    /// partition never reserves past it.
625    pub fn bounded(mut self, max_end: u64) -> Self {
626        self.max_end = Some(max_end);
627        let clamped = self.end.load(Ordering::Acquire).min(max_end);
628        self.end.store(clamped, Ordering::Release);
629        self
630    }
631}
632
633impl DataSourceFactory for ExtendingRangeSourceFactory {
634    fn create_reader(&self) -> Box<dyn DataSource> {
635        Box::new(ExtendingRangeSource {
636            cursor: self.cursor.clone(),
637            end: self.end.clone(),
638            policy: self.policy.clone(),
639            start: self.start,
640            base: self.base,
641            max_end: self.max_end,
642            started: self.started,
643            consumed: 0,
644            schema: self.schema.clone(),
645        })
646    }
647
648    fn schema(&self) -> &SourceSchema {
649        &self.schema
650    }
651
652    fn global_consumed(&self) -> u64 {
653        let pos = self.cursor.load(Ordering::Relaxed);
654        pos.saturating_sub(self.start)
655    }
656
657    fn global_extent(&self) -> Option<u64> {
658        // Live extent — readers / status displays see growth.
659        Some(self.end.load(Ordering::Acquire).saturating_sub(self.start))
660    }
661
662    fn replay_contract(&self) -> SourceReplayContract {
663        SourceReplayContract::stable_ordinal(0)
664    }
665}
666
667/// Per-fiber reader for an extending range source. Reservation
668/// uses CAS so concurrent fibers race cleanly without lock
669/// contention; on end-reach the policy is consulted (possibly
670/// duplicated under concurrent end-reach), and the end atomic
671/// is grown via a single CAS (only one extension takes effect
672/// per round; losers retry and see the new end).
673struct ExtendingRangeSource {
674    cursor: Arc<AtomicU64>,
675    end: Arc<AtomicU64>,
676    policy: Arc<dyn ExtensionPolicy>,
677    start: u64,
678    base: u64,
679    /// SRD 71 partition cap — see
680    /// [`ExtendingRangeSourceFactory::bounded`].
681    max_end: Option<u64>,
682    started: std::time::Instant,
683    consumed: u64,
684    schema: SourceSchema,
685}
686
687impl DataSource for ExtendingRangeSource {
688    fn reserve(&mut self, stride: usize) -> Option<std::ops::Range<u64>> {
689        loop {
690            let cur = self.cursor.load(Ordering::Acquire);
691            let end = self.end.load(Ordering::Acquire);
692            if cur < end {
693                // Try to claim [cur, min(cur+stride, end)).
694                let target = (cur.saturating_add(stride as u64)).min(end);
695                match self
696                    .cursor
697                    .compare_exchange(cur, target, Ordering::AcqRel, Ordering::Acquire)
698                {
699                    Ok(_) => {
700                        let count = target - cur;
701                        self.consumed += count;
702                        return Some(cur..target);
703                    }
704                    Err(_) => continue, // raced; retry
705                }
706            }
707            // SRD 71: a partition-bound cursor terminates the
708            // moment the partition is exhausted, whether or not
709            // the policy's time / pass / count target was
710            // reached — no policy consultation past the cap.
711            if let Some(max) = self.max_end
712                && end >= max
713            {
714                return None;
715            }
716            // Cursor has caught up to end. Consult policy with
717            // a snapshot of the current state. Each fiber that
718            // races to this point gets its own context read;
719            // duplicate consultations are idempotent for pure
720            // predicates.
721            let ctx = ExtensionContext {
722                elapsed_ms: self.started.elapsed().as_millis() as u64,
723                consumed: end.saturating_sub(self.start),
724                base: self.base,
725            };
726            match self.policy.next_extension(&ctx) {
727                Some(delta) if delta > 0 => {
728                    // CAS the end forward, clamped at the
729                    // partition cap when one is set. If someone
730                    // else extended ahead of us, that's fine —
731                    // our duplicate policy consultation is
732                    // harmless and the new end is at least as
733                    // far as ours would have been.
734                    let mut new_end = end.saturating_add(delta);
735                    if let Some(max) = self.max_end {
736                        new_end = new_end.min(max);
737                    }
738                    if new_end == end {
739                        return None;
740                    }
741                    let _ = self.end.compare_exchange(
742                        end,
743                        new_end,
744                        Ordering::AcqRel,
745                        Ordering::Acquire,
746                    );
747                    continue;
748                }
749                _ => return None,
750            }
751        }
752    }
753
754    fn render_item(&self, ordinal: u64) -> SourceItem {
755        SourceItem::ordinal(ordinal)
756    }
757
758    fn extent(&self) -> Option<u64> {
759        // Live extent — same convention as the factory's
760        // `global_extent`: subscribers see the current ceiling
761        // even after it grows.
762        Some(self.end.load(Ordering::Acquire).saturating_sub(self.start))
763    }
764
765    fn consumed(&self) -> u64 {
766        self.consumed
767    }
768
769    fn schema(&self) -> &SourceSchema {
770        &self.schema
771    }
772
773    fn replay_contract(&self) -> SourceReplayContract {
774        SourceReplayContract::stable_ordinal(0)
775    }
776}
777
778// =========================================================================
779// Cursors: provenance-driven cursor targeting
780// =========================================================================
781
782/// A cursor target: a DataSource reader paired with its Polydat input index.
783struct CursorTarget {
784    /// The DataSource reader that provides values for this cursor.
785    reader: Box<dyn DataSource>,
786    /// The Polydat input index where the cursor's ordinal is injected.
787    input_index: usize,
788    /// Source name (for diagnostics).
789    source_name: String,
790    /// Stable process-independent identity used in ordinal packet stamps.
791    stream_id: u64,
792}
793
794/// Ownership token for one range reserved from a perfect ordinal source.
795///
796/// The token contains no reader borrow or rendered payload because the source
797/// contract proves that the scalar value is the ordinal itself. Moving this
798/// value transfers responsibility for every ordinal in `range`; the batch
799/// executor must drain all of them or process the remainder through its scalar
800/// recovery path. The shared source cursor is never rewound.
801#[derive(Debug)]
802pub struct OrdinalBatchLease {
803    source_name: String,
804    stream_id: u64,
805    source_generation: u64,
806    input_index: usize,
807    sequence: u64,
808    range: std::ops::Range<u64>,
809}
810
811impl OrdinalBatchLease {
812    /// The cursor the lease reserves from.
813    pub fn source_name(&self) -> &str {
814        &self.source_name
815    }
816
817    /// The stream the ordinals belong to.
818    pub const fn stream_id(&self) -> u64 {
819        self.stream_id
820    }
821
822    /// The generation of the source's values.
823    pub const fn source_generation(&self) -> u64 {
824        self.source_generation
825    }
826
827    /// The kernel input the cursor's ordinal is written to.
828    pub const fn input_index(&self) -> usize {
829        self.input_index
830    }
831
832    /// The lease's position in reservation order.
833    pub const fn sequence(&self) -> u64 {
834        self.sequence
835    }
836
837    /// The ordinals reserved.
838    pub fn range(&self) -> std::ops::Range<u64> {
839        self.range.clone()
840    }
841
842    /// How many ordinals the lease holds.
843    pub const fn len_u64(&self) -> u64 {
844        self.range.end - self.range.start
845    }
846
847    /// Whether the lease holds no ordinal.
848    pub const fn is_empty(&self) -> bool {
849        self.range.start == self.range.end
850    }
851}
852
853#[derive(Clone, Debug, PartialEq, Eq)]
854/// Why a cursor batch could not be reserved.
855pub enum CursorBatchError {
856    /// No ordinals were asked for.
857    ZeroDemand,
858    /// The reservation names several targets; a batch drives one.
859    RequiresSingleTarget {
860        /// The targets named.
861        targets: usize,
862    },
863    /// The source is not a perfect ordinal stream.
864    SourceNotPerfectOrdinal {
865        /// The source's name.
866        source: String,
867    },
868}
869
870impl std::fmt::Display for CursorBatchError {
871    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
872        match self {
873            Self::ZeroDemand => f.write_str("ordinal batch demand must be greater than zero"),
874            Self::RequiresSingleTarget { targets } => write!(
875                f,
876                "Tier-1 ordinal batching requires exactly one cursor target, found {targets}"
877            ),
878            Self::SourceNotPerfectOrdinal { source } => write!(
879                f,
880                "source '{source}' does not declare the perfect ordinal replay contract"
881            ),
882        }
883    }
884}
885
886impl std::error::Error for CursorBatchError {}
887
888/// Provenance-driven advancer that targets only the cursor nodes
889/// relevant to a specific set of output fields.
890///
891/// Built by a host from the output names it will read, tracing Polydat
892/// provenance from those fields back to root cursor nodes. Only those cursors
893/// advance — unused cursors are left untouched.
894pub struct Cursors {
895    targets: Vec<CursorTarget>,
896    /// Last items read from each target (for injecting into Polydat state).
897    last_items: Vec<Option<SourceItem>>,
898    /// Total advances performed.
899    advances: u64,
900    /// Reservation order for owned ordinal leases from this cursor set.
901    next_batch_sequence: u64,
902}
903
904impl Cursors {
905    /// Build an advancer from a Polydat program, a set of output field names,
906    /// and a map of source name → DataSourceFactory.
907    ///
908    /// Traces provenance: for each field, finds the output node, gets its
909    /// input provenance bitmask, and identifies which inputs are cursor
910    /// ordinals (matching `{source}__ordinal` pattern). Creates a reader
911    /// for each targeted source.
912    pub fn for_fields(
913        program: &crate::kernel::PolydatProgram,
914        field_names: &[&str],
915        source_factories: &std::collections::HashMap<String, Arc<dyn DataSourceFactory>>,
916    ) -> Self {
917        // Collect the union of input provenance for all referenced
918        // fields — exact at any input count (multi-word ProvMask;
919        // the former one-word form over-matched cursor inputs >= 63).
920        let mut combined_provenance = crate::kernel::ProvMask::empty();
921        for name in field_names {
922            if let Some((node_idx, _)) = program.resolve_output(name)
923                && let Some(prov) = program.input_provenance_for(node_idx)
924            {
925                combined_provenance.union_with(prov);
926            }
927        }
928
929        // Find cursor inputs in the provenance
930        let input_names = program.input_names();
931        let mut targets = Vec::new();
932        let mut seen_sources = std::collections::HashSet::new();
933
934        for (idx, input_name) in input_names.iter().enumerate() {
935            if !combined_provenance.contains(idx) {
936                continue;
937            }
938
939            // Check if this input is a source projection ({source}__ordinal)
940            if let Some(source_name) = input_name.strip_suffix("__ordinal") {
941                if seen_sources.contains(source_name) {
942                    continue;
943                }
944                seen_sources.insert(source_name.to_string());
945
946                if let Some(factory) = source_factories.get(source_name) {
947                    let stream_id =
948                        xxhash_rust::xxh3::xxh3_64(format!("{source_name}@{idx}").as_bytes());
949                    targets.push(CursorTarget {
950                        reader: factory.create_reader(),
951                        input_index: idx,
952                        source_name: source_name.to_string(),
953                        stream_id,
954                    });
955                }
956            }
957        }
958
959        let target_count = targets.len();
960        Cursors {
961            targets,
962            last_items: vec![None; target_count],
963            advances: 0,
964            next_batch_sequence: 0,
965        }
966    }
967
968    /// Reserve one owned batch from a single perfect ordinal cursor.
969    ///
970    /// This is the source-side entry point for Tier-1 SIMD execution. It is
971    /// intentionally unavailable for multiple cursor targets or sources that
972    /// merely happen to be monotonic: only the explicit perfect-ordinal
973    /// contract permits payload-free replay from the returned range.
974    pub fn reserve_ordinal_batch(
975        &mut self,
976        demand: usize,
977    ) -> Result<Option<OrdinalBatchLease>, CursorBatchError> {
978        if demand == 0 {
979            return Err(CursorBatchError::ZeroDemand);
980        }
981        if self.targets.len() != 1 {
982            return Err(CursorBatchError::RequiresSingleTarget {
983                targets: self.targets.len(),
984            });
985        }
986
987        let target = &mut self.targets[0];
988        let contract = target.reader.replay_contract();
989        if !contract.is_perfect_ordinal() {
990            return Err(CursorBatchError::SourceNotPerfectOrdinal {
991                source: target.source_name.clone(),
992            });
993        }
994        let Some(range) = target.reader.reserve(demand) else {
995            return Ok(None);
996        };
997        let claimed = range.end - range.start;
998        let lease = OrdinalBatchLease {
999            source_name: target.source_name.clone(),
1000            stream_id: target.stream_id,
1001            source_generation: contract.generation,
1002            input_index: target.input_index,
1003            sequence: self.next_batch_sequence,
1004            range,
1005        };
1006        self.next_batch_sequence = self.next_batch_sequence.wrapping_add(1);
1007        self.advances = self.advances.saturating_add(claimed);
1008        Ok(Some(lease))
1009    }
1010
1011    /// Advance all targeted cursors. Returns `false` if any targeted
1012    /// cursor is exhausted (no more data).
1013    ///
1014    /// After advancing, the new ordinals and field projections are
1015    /// available via `inject_into_state()`.
1016    pub fn advance(&mut self) -> bool {
1017        for (i, target) in self.targets.iter_mut().enumerate() {
1018            match target.reader.next() {
1019                Some(item) => {
1020                    self.last_items[i] = Some(item);
1021                }
1022                None => return false, // this cursor exhausted
1023            }
1024        }
1025        self.advances += 1;
1026        true
1027    }
1028
1029    /// Inject the current cursor values into a Polydat state.
1030    ///
1031    /// Sets each cursor's ordinal at its input index. Field projections
1032    /// are not written here; a host reads them from `last_items()`.
1033    pub fn inject_into_state(&self, state: &mut crate::kernel::PolydatState) {
1034        for (i, target) in self.targets.iter().enumerate() {
1035            if let Some(ref item) = self.last_items[i] {
1036                state.set_input(target.input_index, crate::ast::Value::U64(item.ordinal));
1037                // Field projections (e.g. base__vector) are the host's to
1038                // write from `last_items()`; only the ordinal lands here.
1039            }
1040        }
1041    }
1042
1043    /// The last items read from all targets, for a host to project fields from.
1044    pub fn last_items(&self) -> &[Option<SourceItem>] {
1045        &self.last_items
1046    }
1047
1048    /// Known extent of the driving cursor (smallest among targeted
1049    /// cursors with known extent). Used for progress reporting.
1050    pub fn extent(&self) -> Option<u64> {
1051        self.targets.iter().filter_map(|t| t.reader.extent()).min()
1052    }
1053
1054    /// Total advances performed so far.
1055    pub fn consumed(&self) -> u64 {
1056        self.advances
1057    }
1058
1059    /// Number of targeted cursors.
1060    pub fn target_count(&self) -> usize {
1061        self.targets.len()
1062    }
1063
1064    /// Whether this advancer has any targets.
1065    pub fn is_empty(&self) -> bool {
1066        self.targets.is_empty()
1067    }
1068}
1069
1070// =========================================================================
1071// Built-in ExtensionPolicy implementations
1072// =========================================================================
1073//
1074// Each policy is a pure predicate over `ExtensionContext` and
1075// returns `Some(delta)` when the cursor should continue or
1076// `None` when it should stop. The delta is independent of the
1077// stop condition — workloads can extend in chunks unrelated to
1078// the cursor's base size (e.g. base=10000 but delta=1000 to
1079// check the condition more often).
1080
1081/// Extend while `ctx.elapsed_ms < min_ms`, projecting the
1082/// remaining work from the observed rate and committing it in
1083/// one batch.
1084///
1085/// Per call: estimate `repeats = floor((consumed * remaining_ms
1086/// / elapsed_ms) / base)` — the number of base-sized passes
1087/// needed to fill the time remaining at the current rate. Apply
1088/// a 5% under-bias (`repeats * 95 / 100`) so successive
1089/// extensions converge from below the target rather than
1090/// overshooting once. Commit `repeats * base` cycles.
1091///
1092/// The under-bias gives a geometric convergence: each call
1093/// covers ~95% of the time remaining, so 3–4 calls typically
1094/// land within 0.01% of the time budget. The first call (no
1095/// rate signal yet — `elapsed_ms == 0` or `consumed == 0`)
1096/// falls back to one base chunk via the `delta` field.
1097pub struct UntilElapsedPolicy {
1098    /// The elapsed milliseconds to reach.
1099    pub min_ms: u64,
1100    /// The extension step for the first call, before a rate is known.
1101    pub delta: u64,
1102}
1103
1104impl ExtensionPolicy for UntilElapsedPolicy {
1105    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1106        if ctx.elapsed_ms >= self.min_ms {
1107            return None;
1108        }
1109        // No rate signal yet — first end-reach with elapsed time
1110        // below the clock resolution, or a degenerate empty
1111        // cursor. Step by the declared `delta` (which the
1112        // executor sets to `base` when the workload didn't
1113        // supply an explicit `delta` arg) so the next call has
1114        // a rate measurement to project from.
1115        if ctx.elapsed_ms == 0 || ctx.consumed == 0 {
1116            return Some(self.delta.max(1));
1117        }
1118        let remaining_ms = self.min_ms - ctx.elapsed_ms;
1119        // u128 saturating math: consumed * remaining_ms can
1120        // overflow u64 for long-running phases with high
1121        // throughput (e.g. 10^9 ops over 10^4 ms).
1122        let est_remaining =
1123            (ctx.consumed as u128).saturating_mul(remaining_ms as u128) / (ctx.elapsed_ms as u128);
1124        let biased = est_remaining.saturating_mul(95) / 100;
1125        let base = ctx.base.max(1) as u128;
1126        let repeats = biased / base;
1127        if repeats == 0 {
1128            return None;
1129        }
1130        let delta = repeats.saturating_mul(base);
1131        Some(u64::try_from(delta).unwrap_or(u64::MAX))
1132    }
1133}
1134
1135/// Extend by `delta` while `ctx.passes() < min_passes`. Passes
1136/// are whole multiples of `ctx.base`.
1137pub struct UntilPassesPolicy {
1138    /// The passes to reach.
1139    pub min_passes: u64,
1140    /// The extension step.
1141    pub delta: u64,
1142}
1143
1144impl ExtensionPolicy for UntilPassesPolicy {
1145    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1146        if ctx.passes() < self.min_passes {
1147            Some(self.delta)
1148        } else {
1149            None
1150        }
1151    }
1152}
1153
1154/// Extend by `delta` while `ctx.consumed < min_count`.
1155pub struct UntilCountPolicy {
1156    /// The consumed count to reach.
1157    pub min_count: u64,
1158    /// The extension step.
1159    pub delta: u64,
1160}
1161
1162impl ExtensionPolicy for UntilCountPolicy {
1163    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1164        if ctx.consumed < self.min_count {
1165            Some(self.delta)
1166        } else {
1167            None
1168        }
1169    }
1170}
1171
1172/// Continue (extend) while ALL child policies say continue.
1173/// Stops the moment any child policy returns `None`. The
1174/// delta is the minimum of the children's deltas — conservative
1175/// step size keeps any single condition from over-shooting its
1176/// stop point.
1177pub struct AndPolicy {
1178    /// The policies that must all ask to continue.
1179    pub policies: Vec<Arc<dyn ExtensionPolicy>>,
1180}
1181
1182impl ExtensionPolicy for AndPolicy {
1183    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1184        let mut min_delta = u64::MAX;
1185        for p in &self.policies {
1186            let d = p.next_extension(ctx)?;
1187            min_delta = min_delta.min(d);
1188        }
1189        if min_delta == u64::MAX || min_delta == 0 {
1190            None
1191        } else {
1192            Some(min_delta)
1193        }
1194    }
1195}
1196
1197/// Continue (extend) while ANY child policy says continue.
1198/// Stops only when every child policy returns `None`. The
1199/// delta is the maximum of the policies that said continue —
1200/// matches the most aggressive child still pushing forward.
1201pub struct OrPolicy {
1202    /// The policies of which any may ask to continue.
1203    pub policies: Vec<Arc<dyn ExtensionPolicy>>,
1204}
1205
1206impl ExtensionPolicy for OrPolicy {
1207    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1208        let mut max_delta: Option<u64> = None;
1209        for p in &self.policies {
1210            if let Some(d) = p.next_extension(ctx) {
1211                max_delta = Some(max_delta.map(|m| m.max(d)).unwrap_or(d));
1212            }
1213        }
1214        max_delta
1215    }
1216}
1217
1218/// Back-compat alias for the original time-only policy.
1219/// Constructs an [`UntilElapsedPolicy`] with delta = base.
1220pub struct TimeElapsedPolicy {
1221    inner: UntilElapsedPolicy,
1222}
1223
1224impl TimeElapsedPolicy {
1225    /// A policy extending in steps of `base` until `min_ms` have elapsed.
1226    pub fn new(base: u64, min_ms: u64) -> Self {
1227        Self {
1228            inner: UntilElapsedPolicy {
1229                min_ms,
1230                delta: base,
1231            },
1232        }
1233    }
1234}
1235
1236impl ExtensionPolicy for TimeElapsedPolicy {
1237    fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1238        self.inner.next_extension(ctx)
1239    }
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244    use super::*;
1245
1246    #[test]
1247    fn range_sources_declare_the_perfect_ordinal_replay_contract() {
1248        let factory = RangeSourceFactory::new(10, 20);
1249        assert!(factory.replay_contract().is_perfect_ordinal());
1250        let reader = factory.create_reader();
1251        assert!(reader.replay_contract().is_perfect_ordinal());
1252        let item = reader.render_item(13);
1253        assert_eq!(item.ordinal, 13);
1254        assert!(item.fields.is_empty());
1255    }
1256
1257    /// Policy that always extends by `base` — stands in for a
1258    /// time/pass policy whose target is far away, so the
1259    /// partition cap is the only thing that can stop growth.
1260    struct AlwaysExtend;
1261    impl ExtensionPolicy for AlwaysExtend {
1262        fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1263            Some(ctx.base.max(1))
1264        }
1265    }
1266
1267    #[test]
1268    fn extending_source_bounded_terminates_at_partition_end() {
1269        // SRD 71: `until_*(base, ...) over p` — base-sized chunks
1270        // walk within the partition; the cap stops growth even
1271        // though the policy would keep extending. Partition
1272        // [100, 125) with base 10 → 10 + 10 + 5, then exhausted.
1273        let factory =
1274            ExtendingRangeSourceFactory::new("q", 100, 10, Arc::new(AlwaysExtend)).bounded(125);
1275        let mut reader = factory.create_reader();
1276        let mut total = 0u64;
1277        let mut last_end = 100;
1278        while let Some(r) = reader.reserve(7) {
1279            assert!(r.end <= 125, "reservation past the partition cap: {r:?}");
1280            assert_eq!(r.start, last_end, "contiguous reservations");
1281            last_end = r.end;
1282            total += r.end - r.start;
1283        }
1284        assert_eq!(total, 25, "exactly the partition's cardinality");
1285        assert_eq!(last_end, 125);
1286    }
1287
1288    #[test]
1289    fn extending_source_bounded_clamps_oversized_base() {
1290        // A base chunk larger than the partition never reserves
1291        // past it.
1292        let factory =
1293            ExtendingRangeSourceFactory::new("q", 0, 1000, Arc::new(AlwaysExtend)).bounded(30);
1294        let mut reader = factory.create_reader();
1295        let r = reader.reserve(usize::MAX).unwrap();
1296        assert_eq!(r, 0..30);
1297        assert!(reader.reserve(1).is_none());
1298    }
1299
1300    #[test]
1301    fn extending_source_unbounded_keeps_policy_semantics() {
1302        // Without a cap the policy alone decides — three
1303        // extensions of a terminating policy.
1304        struct NTimes(std::sync::atomic::AtomicU64);
1305        impl ExtensionPolicy for NTimes {
1306            fn next_extension(&self, ctx: &ExtensionContext) -> Option<u64> {
1307                if self.0.fetch_add(1, Ordering::Relaxed) < 3 {
1308                    Some(ctx.base)
1309                } else {
1310                    None
1311                }
1312            }
1313        }
1314        let factory = ExtendingRangeSourceFactory::new(
1315            "q",
1316            0,
1317            10,
1318            Arc::new(NTimes(std::sync::atomic::AtomicU64::new(0))),
1319        );
1320        let mut reader = factory.create_reader();
1321        let mut total = 0u64;
1322        while let Some(r) = reader.reserve(64) {
1323            total += r.end - r.start;
1324        }
1325        assert_eq!(total, 40, "initial 10 + three 10-ordinal extensions");
1326    }
1327
1328    #[test]
1329    fn range_source_yields_ordinals() {
1330        let factory = RangeSourceFactory::new(0, 5);
1331        let mut reader = factory.create_reader();
1332        assert_eq!(reader.extent(), Some(5));
1333
1334        for i in 0..5 {
1335            let item = reader.next().unwrap();
1336            assert_eq!(item.ordinal, i);
1337            assert!(item.fields.is_empty());
1338        }
1339        assert!(reader.next().is_none());
1340        assert_eq!(reader.consumed(), 5);
1341    }
1342
1343    #[test]
1344    fn range_source_chunk() {
1345        let factory = RangeSourceFactory::new(0, 10);
1346        let mut reader = factory.create_reader();
1347
1348        let chunk = reader.next_chunk(3);
1349        assert_eq!(chunk.len(), 3);
1350        assert_eq!(chunk[0].ordinal, 0);
1351        assert_eq!(chunk[2].ordinal, 2);
1352
1353        let chunk = reader.next_chunk(100);
1354        assert_eq!(chunk.len(), 7); // only 7 remaining
1355        assert_eq!(chunk[0].ordinal, 3);
1356        assert_eq!(chunk[6].ordinal, 9);
1357
1358        let chunk = reader.next_chunk(1);
1359        assert!(chunk.is_empty()); // exhausted
1360    }
1361
1362    #[test]
1363    fn range_source_concurrent_readers() {
1364        let factory = RangeSourceFactory::new(0, 100);
1365        let mut r1 = factory.create_reader();
1366        let mut r2 = factory.create_reader();
1367
1368        // Each reader gets unique ordinals from the shared cursor
1369        let a = r1.next().unwrap().ordinal;
1370        let b = r2.next().unwrap().ordinal;
1371        assert_ne!(a, b);
1372
1373        // Drain both readers
1374        let mut total = 2;
1375        while r1.next().is_some() {
1376            total += 1;
1377        }
1378        while r2.next().is_some() {
1379            total += 1;
1380        }
1381        assert_eq!(total, 100);
1382    }
1383
1384    #[test]
1385    fn source_item_field_access() {
1386        let item = SourceItem::with_fields(
1387            42,
1388            vec![
1389                ("name".into(), Value::Str("test".into())),
1390                ("score".into(), Value::F64(0.95)),
1391            ],
1392        );
1393        assert_eq!(item.ordinal, 42);
1394        assert_eq!(item.field("name"), Some(&Value::Str("test".into())));
1395        assert_eq!(item.field("score"), Some(&Value::F64(0.95)));
1396        assert_eq!(item.field("missing"), None);
1397    }
1398
1399    #[test]
1400    fn range_source_named() {
1401        let factory = RangeSourceFactory::named("users", 0, 1000);
1402        assert_eq!(factory.schema().name, "users");
1403        assert_eq!(factory.schema().extent, Some(1000));
1404    }
1405
1406    // ── ExtendingRangeSource ─────────────────────────────────
1407
1408    /// Trivial extension policy for tests: lets the caller
1409    /// decide how many extensions remain. Each call to
1410    /// `next_extension` decrements the count.
1411    struct FixedExtensions {
1412        delta: u64,
1413        remaining: std::sync::atomic::AtomicU64,
1414    }
1415
1416    impl FixedExtensions {
1417        fn new(delta: u64, times: u64) -> Self {
1418            Self {
1419                delta,
1420                remaining: std::sync::atomic::AtomicU64::new(times),
1421            }
1422        }
1423    }
1424
1425    impl ExtensionPolicy for FixedExtensions {
1426        fn next_extension(&self, _ctx: &ExtensionContext) -> Option<u64> {
1427            let prev = self.remaining.fetch_sub(1, Ordering::Relaxed);
1428            if prev == 0 || prev > i64::MAX as u64 {
1429                self.remaining.store(0, Ordering::Relaxed);
1430                None
1431            } else {
1432                Some(self.delta)
1433            }
1434        }
1435    }
1436
1437    #[test]
1438    fn extending_source_consumes_initial_extent_then_extends() {
1439        // Initial extent = 5; policy extends once by 5 more.
1440        // Total expected: 10 ordinals consumed.
1441        let policy = Arc::new(FixedExtensions::new(5, 1));
1442        let factory = ExtendingRangeSourceFactory::new("ext", 0, 5, policy);
1443        let mut reader = factory.create_reader();
1444        let mut got: Vec<u64> = Vec::new();
1445        while let Some(item) = reader.next() {
1446            got.push(item.ordinal);
1447            if got.len() > 50 {
1448                panic!("runaway extension");
1449            }
1450        }
1451        assert_eq!(got, (0..10).collect::<Vec<u64>>());
1452        assert_eq!(reader.consumed(), 10);
1453    }
1454
1455    #[test]
1456    fn extending_source_zero_extensions_behaves_like_fixed_range() {
1457        let policy = Arc::new(FixedExtensions::new(0, 0));
1458        let factory = ExtendingRangeSourceFactory::new("ext", 0, 3, policy);
1459        let mut reader = factory.create_reader();
1460        let mut got: Vec<u64> = Vec::new();
1461        while let Some(item) = reader.next() {
1462            got.push(item.ordinal);
1463        }
1464        assert_eq!(got, vec![0, 1, 2]);
1465    }
1466
1467    #[test]
1468    fn extending_source_global_extent_grows_after_extension() {
1469        // global_extent must reflect the CURRENT end so phase
1470        // status displays show growth honestly.
1471        let policy = Arc::new(FixedExtensions::new(10, 2));
1472        let factory = ExtendingRangeSourceFactory::new("ext", 0, 5, policy);
1473        assert_eq!(factory.global_extent(), Some(5));
1474        let mut reader = factory.create_reader();
1475        // Drain the first 5 to force an extension.
1476        for _ in 0..5 {
1477            reader.next().unwrap();
1478        }
1479        // Trigger the extension by attempting one more pull.
1480        let _ = reader.next().unwrap();
1481        assert_eq!(
1482            factory.global_extent(),
1483            Some(15),
1484            "extent should grow by the extension delta"
1485        );
1486    }
1487
1488    #[test]
1489    fn extending_source_chunked_reservation_caps_at_current_end() {
1490        // A stride request that crosses the current `end` must
1491        // return a SHORT range up to `end`, not an over-claim.
1492        // Otherwise the next reservation would jump past the
1493        // extended range and lose ordinals.
1494        let policy = Arc::new(FixedExtensions::new(5, 1));
1495        let factory = ExtendingRangeSourceFactory::new("ext", 0, 3, policy);
1496        let mut reader = factory.create_reader();
1497        let range = reader.reserve(10).expect("first reserve");
1498        // Initial end was 3; the reservation must cap there.
1499        assert_eq!(range, 0..3);
1500        // Next reserve triggers the extension and consumes the
1501        // remainder.
1502        let range = reader.reserve(10).expect("second reserve");
1503        assert_eq!(range, 3..8);
1504    }
1505
1506    fn ctx_at(elapsed_ms: u64, consumed: u64, base: u64) -> ExtensionContext {
1507        ExtensionContext {
1508            elapsed_ms,
1509            consumed,
1510            base,
1511        }
1512    }
1513
1514    #[test]
1515    fn until_elapsed_policy_bootstrap_falls_back_to_delta_when_no_rate_signal() {
1516        // First end-reach: consumed or elapsed effectively zero —
1517        // no rate to project from. Policy returns `delta` exactly
1518        // so the next call has a measurement to work with.
1519        let policy = UntilElapsedPolicy {
1520            min_ms: 50,
1521            delta: 7,
1522        };
1523        assert_eq!(policy.next_extension(&ctx_at(0, 0, 0)), Some(7));
1524        assert_eq!(policy.next_extension(&ctx_at(49, 0, 0)), Some(7));
1525        assert_eq!(policy.next_extension(&ctx_at(0, 0, 100)), Some(7));
1526    }
1527
1528    #[test]
1529    fn until_elapsed_policy_stops_at_or_past_min_ms() {
1530        let policy = UntilElapsedPolicy {
1531            min_ms: 50,
1532            delta: 7,
1533        };
1534        assert_eq!(policy.next_extension(&ctx_at(50, 100, 100)), None);
1535        assert_eq!(policy.next_extension(&ctx_at(1000, 100, 100)), None);
1536    }
1537
1538    #[test]
1539    fn until_elapsed_policy_projects_remaining_cycles_with_under_bias() {
1540        // base=100, consumed=100 (one pass), elapsed=200ms,
1541        // min_ms=1000 → remaining=800ms. Rate = 100/200 = 0.5
1542        // cycles/ms; project 0.5 * 800 = 400 cycles. Under-bias
1543        // 5% → 380. Round to multiples of base (100) → 3 repeats
1544        // → 300 cycles.
1545        let policy = UntilElapsedPolicy {
1546            min_ms: 1000,
1547            delta: 100,
1548        };
1549        assert_eq!(
1550            policy.next_extension(&ctx_at(200, 100, 100)),
1551            Some(300),
1552            "expected 3-pass batch (380 under-biased, floored to base multiples)",
1553        );
1554    }
1555
1556    #[test]
1557    fn until_elapsed_policy_returns_none_when_remaining_is_under_one_pass() {
1558        // base=100, consumed=10000 over 990ms, min_ms=1000 →
1559        // remaining=10ms. Project 10000/990*10 ≈ 101 cycles.
1560        // Under-bias → 95. Rounded to base multiples → 0 repeats.
1561        // Policy terminates rather than under-shooting the budget
1562        // with a wasted partial pass.
1563        let policy = UntilElapsedPolicy {
1564            min_ms: 1000,
1565            delta: 100,
1566        };
1567        assert_eq!(policy.next_extension(&ctx_at(990, 10000, 100)), None);
1568    }
1569
1570    #[test]
1571    fn until_elapsed_policy_converges_geometrically() {
1572        // Sanity: simulate the extension loop. Each iteration
1573        // commits ~95% of the remaining time budget; the
1574        // residual is bounded below by the base-pass rounding,
1575        // so convergence is one-base-coarse rather than
1576        // arbitrarily tight.
1577        let policy = UntilElapsedPolicy {
1578            min_ms: 1000,
1579            delta: 10,
1580        };
1581        // Pretend the first pass took 10ms (rate = 1 cycle/ms).
1582        let mut elapsed = 10u64;
1583        let mut consumed = 10u64;
1584        let mut iters = 0;
1585        while let Some(delta) = policy.next_extension(&ctx_at(elapsed, consumed, 10)) {
1586            iters += 1;
1587            assert!(iters < 10, "geometric series should converge fast");
1588            consumed += delta;
1589            // Same rate: 1 cycle/ms.
1590            elapsed += delta;
1591        }
1592        assert!(elapsed <= 1000, "must under-shoot, got elapsed={elapsed}");
1593        // Residual ≤ ~5% (under-bias) + 1 base pass (rounding) ≈ 6% of target.
1594        assert!(
1595            elapsed >= 940,
1596            "must come within ~6% of target, got {elapsed}"
1597        );
1598    }
1599
1600    #[test]
1601    fn until_passes_policy_counts_in_base_multiples() {
1602        let policy = UntilPassesPolicy {
1603            min_passes: 3,
1604            delta: 100,
1605        };
1606        // 0 passes done — extend.
1607        assert_eq!(policy.next_extension(&ctx_at(0, 0, 100)), Some(100));
1608        // 2 passes done (200 consumed @ base=100) — extend.
1609        assert_eq!(policy.next_extension(&ctx_at(0, 200, 100)), Some(100));
1610        // 3 passes done — stop.
1611        assert_eq!(policy.next_extension(&ctx_at(0, 300, 100)), None);
1612        // 4 passes done — stop.
1613        assert_eq!(policy.next_extension(&ctx_at(0, 400, 100)), None);
1614    }
1615
1616    #[test]
1617    fn until_count_policy_uses_raw_consumed() {
1618        let policy = UntilCountPolicy {
1619            min_count: 250,
1620            delta: 50,
1621        };
1622        assert_eq!(policy.next_extension(&ctx_at(0, 0, 100)), Some(50));
1623        assert_eq!(policy.next_extension(&ctx_at(0, 249, 100)), Some(50));
1624        assert_eq!(policy.next_extension(&ctx_at(0, 250, 100)), None);
1625    }
1626
1627    #[test]
1628    fn and_policy_stops_when_any_child_stops() {
1629        // time<5000 AND passes<3.
1630        let time = Arc::new(UntilElapsedPolicy {
1631            min_ms: 5000,
1632            delta: 10,
1633        });
1634        let passes = Arc::new(UntilPassesPolicy {
1635            min_passes: 3,
1636            delta: 20,
1637        });
1638        let and = AndPolicy {
1639            policies: vec![time, passes],
1640        };
1641        // Both still want to continue: delta = min(10, 20) = 10.
1642        assert_eq!(and.next_extension(&ctx_at(0, 0, 100)), Some(10));
1643        // Time done — stop.
1644        assert_eq!(and.next_extension(&ctx_at(5000, 0, 100)), None);
1645        // Passes done — stop.
1646        assert_eq!(and.next_extension(&ctx_at(0, 300, 100)), None);
1647    }
1648
1649    #[test]
1650    fn or_policy_continues_if_any_child_continues() {
1651        let time = Arc::new(UntilElapsedPolicy {
1652            min_ms: 5000,
1653            delta: 10,
1654        });
1655        let passes = Arc::new(UntilPassesPolicy {
1656            min_passes: 3,
1657            delta: 20,
1658        });
1659        let or = OrPolicy {
1660            policies: vec![time, passes],
1661        };
1662        // Both want to continue → delta = max(10, 20) = 20.
1663        assert_eq!(or.next_extension(&ctx_at(0, 0, 100)), Some(20));
1664        // Time done, passes still wants → delta = 20.
1665        assert_eq!(or.next_extension(&ctx_at(5000, 0, 100)), Some(20));
1666        // Passes done, time still wants → delta = 10.
1667        assert_eq!(or.next_extension(&ctx_at(0, 300, 100)), Some(10));
1668        // Both done → stop.
1669        assert_eq!(or.next_extension(&ctx_at(5000, 300, 100)), None);
1670    }
1671
1672    #[test]
1673    fn time_elapsed_policy_compat_alias_still_works() {
1674        let p = TimeElapsedPolicy::new(7, 1);
1675        std::thread::sleep(std::time::Duration::from_millis(20));
1676        assert_eq!(p.next_extension(&ctx_at(20, 0, 0)), None);
1677    }
1678}